فهرست منبع

feat: 添加足迹功能及优化订单和商品详情页

- 实现完整的足迹页面功能,包括浏览记录展示、日期分组、批量删除等
- 在商品详情页增加到货通知的取消功能及状态显示
- 优化订单页的"再来一单"功能,直接跳转确认订单页
- 修复商品详情页分享海报的二维码显示问题
- 为订单详情页添加缺货处理方式显示
- 改进价格计算精度,防止浮点数计算问题
- 优化移动端底部安全区域适配
ylong 2 هفته پیش
والد
کامیت
47465f91b0

+ 9 - 0
api/modules/mall.js

@@ -142,5 +142,14 @@ export const useMallApi = (Vue, vm) => {
 
         // 取消收藏
         removeCollectAjax: (isbnList) => vm.$u.post('/token/shop/user/removeCollect', { isbnList }),
+
+        // 获取我的足迹列表
+        getLookLogListAjax: (params) => vm.$u.get('/token/shop/user/lookLogList', params),
+
+        // 删除我的足迹
+        clearLookLogAjax: (idList) => vm.$u.post('/token/shop/user/clearLookLog', idList),
+
+         // 获取商品分享二维码
+        getIsbnQrcodeAjax: (isbn) => vm.$u.http.get('/token/shop/order/getIsbnQrcode', { isbn }),
 	}
 }

+ 8 - 1
components/price-reduction-popup/price-reduction-popup.vue

@@ -16,7 +16,7 @@
 				</view>
 				<view class="price-row">
 					<text class="price-label">助力成功减至</text>
-					<text class="price-value increased">¥ {{ bookInfo.price - (bookInfo.reduceMoney || 0) }}</text>
+					<text class="price-value increased">¥ {{ reducedPrice }}</text>
 					<image class="up-icon" src="/static/img/activity/up2.png" mode="widthFix"
 						style="transform: rotate(180deg);">
 					</image>
@@ -77,6 +77,13 @@
 				inviteUsers: [],
 			};
 		},
+		computed: {
+			reducedPrice() {
+				const price = parseFloat(this.bookInfo.price || 0);
+				const reduceMoney = parseFloat(this.bookInfo.reduceMoney || 0);
+				return (price - reduceMoney).toFixed(2);
+			}
+		},
 		methods: {
 			open(data) {
 				this.showPopup = true;

+ 7 - 1
pages-car/pages/complaint.vue

@@ -240,6 +240,12 @@
                 if (!this.phone) {
                     return uni.$u.toast("请输入联系方式");
                 }
+                
+                // 手机号格式校验
+                const phoneReg = /^1[3-9]\d{9}$/;
+                if (!phoneReg.test(this.phone)) {
+                    return uni.$u.toast("请输入正确的手机号码");
+                }
 
                 // 准备投诉数据
                 const complaintData = {
@@ -281,7 +287,7 @@
         min-height: 100vh;
         background: #f8f8f8;
         padding: 20rpx;
-        padding-bottom: 120rpx;
+        padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
 
         .status-block {
             background: #ffffff;

+ 13 - 3
pages-car/pages/confirm-order.vue

@@ -207,9 +207,19 @@
                 //拼接提交数据
                 this.submitData.cartIdList = preSubmitOrderData.cartIdList || [];
                 this.submitData.addressId = this.defaultAddr.id || "";
-            } else {
-                this.$u.toast('参数错误');
-                setTimeout(() => uni.navigateBack(), 1500);
+            }
+            if (options.cartIdList) {
+                // 从 URL 参数接收 cartIdList(再来一单场景)
+                try {
+                    this.submitData.cartIdList = JSON.parse(options.cartIdList);
+                    // 加载订单预览数据
+                    this.refreshPreOrderData();
+                } catch (e) {
+                    console.error('解析 cartIdList 失败:', e);
+                    this.$u.toast('参数错误');
+                    setTimeout(() => uni.navigateBack(), 1500);
+                    return;
+                }
             }
         },
         methods: {

+ 251 - 1
pages-car/pages/my-footprint.vue

@@ -1,3 +1,253 @@
 <template>
-    <div>我的足迹页面</div>
+    <view class="my-footprint">
+        <!-- 顶部操作栏 -->
+        <view class="header" v-if="bookList.length">
+            <view class="left">
+                <u-checkbox v-if="isEditMode" class="checkbox-item" v-model="isAllSelected" @change="toggleSelectAll"
+                    shape="circle" active-color="#38C148"></u-checkbox>
+                <text v-if="isEditMode" class="select-text">全选</text>
+            </view>
+            <view class="right">
+                <text v-if="!isEditMode" @tap="toggleEditMode">管理</text>
+                <text v-else @tap="handleDelete">清除浏览记录</text>
+            </view>
+        </view>
+
+        <!-- 书籍列表 -->
+        <page-scroll :page-size="12" url="/token/shop/user/lookLogList" @updateList="handleUpdateList" ref="pageRef" slotEmpty emptyText="您暂无浏览记录">
+            <view class="list-container">
+                <view class="date-group" v-for="group in groupedList" :key="group.date">
+                    <view class="date-header">{{ group.date }}</view>
+                    <view class="book-list">
+                        <BookListItem v-for="book in group.books" :key="book.id" :book="book" :isEditMode="isEditMode"
+                            @click="handleBookClick" @checked="handleChecked" />
+                    </view>
+                </view>
+            </view>
+        </page-scroll>
+
+        <!-- 删除确认弹窗 -->
+        <common-dialog ref="deleteDialog" title="温馨提示" @confirm="confirmDelete">
+            <text>确定清除选中的浏览记录吗?</text>
+        </common-dialog>
+    </view>
 </template>
+
+<script>
+import BookListItem from '../components/book-list-item.vue'
+import commonDialog from '@/components/common-dialog.vue'
+import pageScroll from '@/components/pageScroll/index.vue'
+
+export default {
+    components: {
+        BookListItem,
+        commonDialog,
+        pageScroll
+    },
+    data() {
+        return {
+            isEditMode: false,
+            bookList: [],
+            isAllSelected: false
+        }
+    },
+    computed: {
+        groupedList() {
+            const groups = {};
+            this.bookList.forEach(book => {
+                // "2026-04-19T15:16:42.827Z" -> "2026-04-19" 或者直接使用截取
+                let date = '未知日期';
+                if (book.createTime) {
+                    if (book.createTime.includes('T')) {
+                        date = book.createTime.split('T')[0];
+                    } else {
+                        date = book.createTime.split(' ')[0];
+                    }
+                }
+                
+                if (!groups[date]) {
+                    groups[date] = [];
+                }
+                groups[date].push(book);
+            });
+            
+            // 按日期倒序排列
+            const sortedDates = Object.keys(groups).sort((a, b) => new Date(b) - new Date(a));
+            return sortedDates.map(date => {
+                return {
+                    date,
+                    books: groups[date]
+                };
+            });
+        }
+    },
+    // #ifdef MP-ALIPAY
+    onPullDownRefresh() {
+        this.$refs.pageRef?.loadData(true)
+    },
+    // #endif
+    onShow() {
+        this.$refs.pageRef?.loadData(true)
+    },
+    methods: {
+        handleChecked({ book, checked }) {
+            this.$nextTick(() => {
+                let item = this.bookList.find(item => item.id === book.id)
+                let index = this.bookList.findIndex(item => item.id === book.id)
+                if (item) {
+                    item.selected = checked
+                    this.$set(this.bookList, index, item)
+                    this.isAllSelected = this.bookList.every(item => item.selected)
+                }
+            })
+        },
+        handleBookClick(book) {
+             uni.navigateTo({
+                url: '/pages-sell/pages/detail?isbn=' + book.isbn
+            });
+        },
+        handleUpdateList(data) {
+            const oldSelectedIds = new Set(this.bookList.filter(v => v.selected).map(v => v.id));
+            
+            this.bookList = data.map(v => {
+                return {
+                    ...v,
+                    selected: oldSelectedIds.has(v.id),
+                    // BookListItem 显示组件使用 price 作为售价,producePrice 作为原价
+                    // 足迹接口返回的是 sellPrice (售价) 和 price (原价)
+                    price: v.sellPrice,
+                    producePrice: v.price
+                }
+            })
+            // 更新全选状态
+            if (this.bookList.length > 0) {
+                this.isAllSelected = this.bookList.every(item => item.selected);
+            } else {
+                this.isAllSelected = false;
+            }
+        },
+
+        // 切换编辑模式
+        toggleEditMode() {
+            this.isEditMode = !this.isEditMode
+            if (!this.isEditMode) {
+                this.bookList.forEach(book => book.selected = false)
+            } else {
+                // 进入编辑模式不默认全选
+                this.isAllSelected = false
+                this.bookList.forEach(book => book.selected = false)
+            }
+        },
+
+        // 切换全选
+        toggleSelectAll() {
+            const newValue = !this.isAllSelected
+            this.bookList.forEach(book => book.selected = newValue)
+        },
+
+        // 处理删除
+        handleDelete() {
+            let deleteIds = this.bookList.filter(book => book.selected)
+            if (deleteIds.length === 0) {
+                uni.showToast({
+                    title: '请选择要清除的记录',
+                    icon: 'none'
+                })
+                return
+            }
+            this.$refs.deleteDialog.openPopup()
+        },
+
+        // 确认删除
+        confirmDelete() {
+            let deleteIds = this.bookList.filter(book => book.selected).map(v => v.id)
+            this.$u.api.clearLookLogAjax(deleteIds).then(res => {
+                if (res.code === 200) {
+                    uni.showToast({
+                        title: '清除成功',
+                        icon: 'success'
+                    })
+                    this.$refs.pageRef?.loadData(true)
+                    this.isEditMode = false;
+                }
+            })
+        }
+    }
+}
+</script>
+
+<style lang="scss">
+.my-footprint {
+    min-height: 100vh;
+    background: #F5F5F5;
+
+    ::v-deep .checkbox-item {
+        .u-checkbox__label {
+            margin: 0 !important;
+        }
+    }
+
+    .header {
+        position: sticky;
+        top: 0;
+        left: 0;
+        right: 0;
+        z-index: 100;
+        height: 88rpx;
+        background: #FFFFFF;
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: 0 30rpx;
+        font-size: 28rpx;
+        border-bottom: 1rpx solid #EEEEEE;
+
+        .left {
+            display: flex;
+            align-items: center;
+
+            .select-text {
+                margin-left: 12rpx;
+                color: #333333;
+            }
+        }
+
+        .right {
+            text {
+                color: #333333;
+
+                &.delete-btn {
+                    color: #FF5B5B;
+                }
+            }
+        }
+    }
+
+    .scroll-view {
+        height: calc(100vh - 88rpx);
+    }
+
+    .list-container {
+        padding: 20rpx;
+        
+        .date-group {
+            margin-bottom: 30rpx;
+            
+            .date-header {
+                font-size: 32rpx;
+                color: #333333;
+                font-weight: bold;
+                margin-bottom: 20rpx;
+                padding-left: 10rpx;
+            }
+            
+            .book-list {
+                display: flex;
+                flex-wrap: wrap;
+                justify-content: flex-start;
+                gap: 20rpx;
+            }
+        }
+    }
+}
+</style>

+ 242 - 218
pages-car/pages/my-order.vue

@@ -31,248 +31,272 @@
 </template>
 
 <script>
-    import BuyOrderItem from '../components/buy-order-item.vue';
-    import pageScroll from '@/components/pageScroll/index.vue';
-    import FastRefundDialog from '../components/fast-refund-dialog.vue';
-    import UrgeDeliveryDialog from '../components/urge-delivery-dialog.vue';
-    import CancelOrderPopup from '../components/cancel-order-popup.vue';
+import BuyOrderItem from '../components/buy-order-item.vue';
+import pageScroll from '@/components/pageScroll/index.vue';
+import FastRefundDialog from '../components/fast-refund-dialog.vue';
+import UrgeDeliveryDialog from '../components/urge-delivery-dialog.vue';
+import CancelOrderPopup from '../components/cancel-order-popup.vue';
 
-    export default {
-        components: {
-            BuyOrderItem,
-            pageScroll,
-            FastRefundDialog,
-            UrgeDeliveryDialog,
-            CancelOrderPopup
-        },
-        data() {
-            return {
-                // value用于前端标识,params用于后端查询
-                tabList: [
-                    { name: '全部', value: '0', params: {} },
-                    { name: '待付款', value: '1', params: { status: '1' } },
-                    { name: '待发货', value: '2', params: { status: '2' } },
-                    { name: '待收货', value: '3', params: { status: '3' } },
-                    { name: '已完成', value: '4', params: { status: '4' } },
-                    { name: '退款/售后', value: '5', params: { status: '5' } },
-                ],
-                currentTab: 0,
-                orderList: [],
-                params: {},
-                showActionSheet: false,
-                actionSheetList: [],
-                currentOrder: null,
-                modifyingOrderId: null
-            };
-        },
-        onLoad(options) {
-            if (options.status) {
-                const index = this.tabList.findIndex(item => item.value == options.status);
-                if (index !== -1) {
-                    this.currentTab = index;
-                    this.params = this.tabList[index].params;
-                }
+export default {
+    components: {
+        BuyOrderItem,
+        pageScroll,
+        FastRefundDialog,
+        UrgeDeliveryDialog,
+        CancelOrderPopup
+    },
+    data() {
+        return {
+            // value用于前端标识,params用于后端查询
+            tabList: [
+                { name: '全部', value: '0', params: {} },
+                { name: '待付款', value: '1', params: { status: '1' } },
+                { name: '待发货', value: '2', params: { status: '2' } },
+                { name: '待收货', value: '3', params: { status: '3' } },
+                { name: '已完成', value: '4', params: { status: '4' } },
+                { name: '退款/售后', value: '5', params: { status: '5' } },
+            ],
+            currentTab: 0,
+            orderList: [],
+            params: {},
+            showActionSheet: false,
+            actionSheetList: [],
+            currentOrder: null,
+            modifyingOrderId: null
+        };
+    },
+    onLoad(options) {
+        if (options.status) {
+            const index = this.tabList.findIndex(item => item.value == options.status);
+            if (index !== -1) {
+                this.currentTab = index;
+                this.params = this.tabList[index].params;
             }
+        }
 
-            this.loadOrders(true, this.params);
+        this.loadOrders(true, this.params);
 
-            // 监听地址选择
-            uni.$on('selectAddr', this.onAddressSelected);
+        // 监听地址选择
+        uni.$on('selectAddr', this.onAddressSelected);
+    },
+    onUnload() {
+        uni.$off('selectAddr', this.onAddressSelected);
+    },
+    methods: {
+        onAddressSelected(addr) {
+            if (this.modifyingOrderId && addr && addr.id) {
+                this.$u.api.modifyOrderAddressAjax({
+                    orderId: this.modifyingOrderId,
+                    addressId: addr.id
+                }).then(res => {
+                    uni.hideLoading();
+                    if (res.code == 200) {
+                        uni.showToast({ title: '修改成功', icon: 'success' });
+                        setTimeout(() => {
+                            this.loadOrders(true, this.params);
+                        }, 1000)
+                    }
+                }).finally(() => {
+                    this.modifyingOrderId = null;
+                });
+            }
         },
-        onUnload() {
-            uni.$off('selectAddr', this.onAddressSelected);
+        loadOrders(refresh = false, params = {}) {
+            this.$nextTick(() => {
+                this.$refs.pageRef?.loadData(refresh, params);
+            });
         },
-        methods: {
-            onAddressSelected(addr) {
-                if (this.modifyingOrderId && addr && addr.id) {
-                    this.$u.api.modifyOrderAddressAjax({
-                        orderId: this.modifyingOrderId,
-                        addressId: addr.id
-                    }).then(res => {
-                        uni.hideLoading();
-                        if (res.code == 200) {
-                            uni.showToast({ title: '修改成功', icon: 'success' });
-                            setTimeout(() => {
-                                this.loadOrders(true, this.params);
-                            }, 1000)
-                        }
-                    }).finally(() => {
-                        this.modifyingOrderId = null;
-                    });
-                }
-            },
-            loadOrders(refresh = false, params = {}) {
-                this.$nextTick(() => {
-                    this.$refs.pageRef?.loadData(refresh, params);
-                });
-            },
-            handleTabChange(index) {
-                this.currentTab = index;
-                this.params = this.tabList[index].params;
-                this.loadOrders(true, this.params);
-            },
-            handleUpdateList(list) {
-                this.orderList = list;
-            },
-            handleAction({ type, order, data }) {
-                console.log('Action:', type, order);
-                this.currentOrder = order;
+        handleTabChange(index) {
+            this.currentTab = index;
+            this.params = this.tabList[index].params;
+            this.loadOrders(true, this.params);
+        },
+        handleUpdateList(list) {
+            this.orderList = list;
+        },
+        handleAction({ type, order, data }) {
+            console.log('Action:', type, order);
+            this.currentOrder = order;
 
-                if (type === 'more') {
-                    // data contains the list of actions to show in sheet
-                    // Map internal keys to display text
-                    const actionMap = {
-                        'applyAfterSales': { text: '申请售后', type: 'refund' },
-                        'logistics': { text: '查看物流', type: 'logistics' },
-                        'invoice': { text: '申请开票', type: 'invoice' }
-                    };
+            if (type === 'more') {
+                // data contains the list of actions to show in sheet
+                // Map internal keys to display text
+                const actionMap = {
+                    'applyAfterSales': { text: '申请售后', type: 'refund' },
+                    'logistics': { text: '查看物流', type: 'logistics' },
+                    'invoice': { text: '申请开票', type: 'invoice' }
+                };
 
-                    this.actionSheetList = data.map(key => actionMap[key]).filter(Boolean);
-                    this.showActionSheet = true;
-                    return;
-                }
+                this.actionSheetList = data.map(key => actionMap[key]).filter(Boolean);
+                this.showActionSheet = true;
+                return;
+            }
 
-                this.processAction(type, order);
-            },
-            handleActionSheetClick(index) {
-                const action = this.actionSheetList[index];
-                if (action && action.type) {
-                    this.processAction(action.type, this.currentOrder);
-                }
-            },
-            processAction(type, order) {
-                if (type === 'rebuy' || type === 'addToCart') {
-                    this.$u.api.orderAddToCartAjax({
-                        orderId: order.orderId
-                    }).then(res => {
-                        if (res.code == 200) {
-                            uni.showToast({
-                                title: '已加入购物车',
-                                icon: 'success',
-                                duration: 3000
-                            });
-                            this.$updateCartBadge();
-                        }
-                    });
-                } else if (type === 'remind') {
-                    this.$refs.urgeDialog.open(order);
-                } else if (type === 'overtime') {
-                    // 超时发货补偿
-                    uni.showModal({
-                        title: '提示',
-                        content: '确认申请超时发货补偿?',
-                        success: (res) => {
-                            if (res.confirm) {
-                                this.$u.api.sendTimeoutCompensationAjax(order.orderId).then(res => {
-                                    if (res.code == 200) {
-                                        uni.showToast({
-                                            title: '申请成功',
-                                            icon: 'success'
-                                        });
-                                        this.loadOrders(true, this.params);
-                                    }
-                                });
-                            }
-                        }
-                    });
-                } else if (type === 'priceMatch') {
-                    // 降价补差
-                    uni.showModal({
-                        title: '提示',
-                        content: '确认申请降价补差?',
-                        success: (res) => {
-                            if (res.confirm) {
-                                this.$u.api.priceReductionCompensationAjax(order.orderId).then(res => {
-                                    if (res.code == 200) {
-                                        uni.showToast({
-                                            title: '申请成功',
-                                            icon: 'success'
-                                        });
-                                        this.loadOrders(true, this.params);
-                                    }
-                                });
-                            }
-                        }
+            this.processAction(type, order);
+        },
+        handleActionSheetClick(index) {
+            const action = this.actionSheetList[index];
+            if (action && action.type) {
+                this.processAction(action.type, this.currentOrder);
+            }
+        },
+        addCartAjax(orderId) {
+            uni.showLoading({ title: '处理中' });
+            this.$u.api.orderAddToCartAjax({
+                orderId: orderId
+            }).then(res => {
+                uni.hideLoading();
+                if (res.code == 200) {
+                    uni.showToast({
+                        title: '已加入购物车',
+                        icon: 'success',
+                        duration: 3000
                     });
-                } else if (type === 'refund') {
-                    if (order.status == '2') {
-                        this.$refs.refundDialog.open(order);
-                    } else {
-                        // 跳转到申请售后页面
+                    this.$updateCartBadge();
+                }
+            });
+        },
+        processAction(type, order) {
+            if (type === 'rebuy') {
+                uni.showLoading({ title: '加载中...' });
+                this.$u.api.orderAddToCartAjax({
+                    orderId: order.orderId
+                }).then(res => {
+                    uni.hideLoading();
+                    if (res.code === 200) {
+                        // 加入购物车成功,跳转到确认订单页面
+                        // 将 cartIdList 作为参数传递
+                        const cartIdList = res.data || [];
                         uni.navigateTo({
-                            url: `/pages-car/pages/apply-refund?orderId=${order.orderId}`
+                            url: '/pages-car/pages/confirm-order?cartIdList=' + JSON.stringify(cartIdList)
                         });
+                    } else {
+                        this.$u.toast(res.msg || '加入购物车失败');
                     }
-                } else if (type === 'confirm') {
-                    uni.showModal({
-                        title: '提示',
-                        content: '确认已收到商品?',
-                        success: (res) => {
-                            if (res.confirm) {
-                                uni.showToast({ title: '确认收货成功', icon: 'none' });
-                                this.loadOrders(true, this.params);
-                            }
+                }).catch(() => {
+                    uni.hideLoading();
+                });
+            } else if (type === 'addToCart') {
+                this.addCartAjax(order.orderId);
+            } else if (type === 'remind') {
+                this.$refs.urgeDialog.open(order);
+            } else if (type === 'overtime') {
+                // 超时发货补偿
+                uni.showModal({
+                    title: '提示',
+                    content: '确认申请超时发货补偿?',
+                    success: (res) => {
+                        if (res.confirm) {
+                            this.$u.api.sendTimeoutCompensationAjax(order.orderId).then(res => {
+                                if (res.code == 200) {
+                                    uni.showToast({
+                                        title: '申请成功',
+                                        icon: 'success'
+                                    });
+                                    this.loadOrders(true, this.params);
+                                }
+                            });
                         }
-                    });
-                } else if (type === 'logistics') {
-                    uni.navigateTo({
-                        url: `/pages-car/pages/logistics-detail?orderId=${order.orderId}`
-                    });
-                } else if (type === 'address') {
-                    this.modifyingOrderId = order.orderId;
-                    // 兼容列表和详情可能的字段差异
-                    const addressId = order.addressId || order.receiverAddressId || '';
-                    uni.navigateTo({
-                        url: `/pages-mine/pages/address/list?id=${addressId}&isSelect=1`
-                    });
-                } else if (type === 'pay') {
-                    // 跳转到收银台
-                    uni.navigateTo({
-                        url: `/pages-car/pages/cashier-desk?id=${order.orderId}`
-                    });
-                } else if (type === 'cancel') {
-                    this.$refs.cancelDialog.open(order.orderId);
-                } else if (type === 'extend') {
-                    uni.showModal({
-                        title: '提示',
-                        content: '每笔订单只能延长一次收货时间,确认延长收货?',
-                        success: (res) => {
-                            if (res.confirm) {
-                                this.$u.api.orderAddDeadlineAjax(order.orderId).then(res => {
-                                    if (res.code == 200) {
-                                        uni.showToast({
-                                            title: '延长收货成功',
-                                            icon: 'success'
-                                        });
-                                        this.loadOrders(true, this.params);
-                                    }
-                                });
-                            }
+                    }
+                });
+            } else if (type === 'priceMatch') {
+                // 降价补差
+                uni.showModal({
+                    title: '提示',
+                    content: '确认申请降价补差?',
+                    success: (res) => {
+                        if (res.confirm) {
+                            this.$u.api.priceReductionCompensationAjax(order.orderId).then(res => {
+                                if (res.code == 200) {
+                                    uni.showToast({
+                                        title: '申请成功',
+                                        icon: 'success'
+                                    });
+                                    this.loadOrders(true, this.params);
+                                }
+                            });
                         }
-                    });
+                    }
+                });
+            } else if (type === 'refund') {
+                if (order.status == '2') {
+                    this.$refs.refundDialog.open(order);
                 } else {
-                    uni.showToast({ title: '功能开发中', icon: 'none' });
+                    // 跳转到申请售后页面
+                    uni.navigateTo({
+                        url: `/pages-car/pages/apply-refund?orderId=${order.orderId}`
+                    });
                 }
+            } else if (type === 'confirm') {
+                uni.showModal({
+                    title: '提示',
+                    content: '确认已收到商品?',
+                    success: (res) => {
+                        if (res.confirm) {
+                            uni.showToast({ title: '确认收货成功', icon: 'none' });
+                            this.loadOrders(true, this.params);
+                        }
+                    }
+                });
+            } else if (type === 'logistics') {
+                uni.navigateTo({
+                    url: `/pages-car/pages/logistics-detail?orderId=${order.orderId}`
+                });
+            } else if (type === 'address') {
+                this.modifyingOrderId = order.orderId;
+                // 兼容列表和详情可能的字段差异
+                const addressId = order.addressId || order.receiverAddressId || '';
+                uni.navigateTo({
+                    url: `/pages-mine/pages/address/list?id=${addressId}&isSelect=1`
+                });
+            } else if (type === 'pay') {
+                // 跳转到收银台
+                uni.navigateTo({
+                    url: `/pages-car/pages/cashier-desk?id=${order.orderId}`
+                });
+            } else if (type === 'cancel') {
+                this.$refs.cancelDialog.open(order.orderId);
+            } else if (type === 'extend') {
+                uni.showModal({
+                    title: '提示',
+                    content: '每笔订单只能延长一次收货时间,确认延长收货?',
+                    success: (res) => {
+                        if (res.confirm) {
+                            this.$u.api.orderAddDeadlineAjax(order.orderId).then(res => {
+                                if (res.code == 200) {
+                                    uni.showToast({
+                                        title: '延长收货成功',
+                                        icon: 'success'
+                                    });
+                                    this.loadOrders(true, this.params);
+                                }
+                            });
+                        }
+                    }
+                });
+            } else {
+                uni.showToast({ title: '功能开发中', icon: 'none' });
             }
         }
     }
+}
 </script>
 
 <style lang="scss" scoped>
-    .my-order-page {
-        min-height: 100vh;
-        background-color: #F5F5F5;
+.my-order-page {
+    min-height: 100vh;
+    background-color: #F5F5F5;
 
-        .tabs-wrapper {
-            position: sticky;
-            top: 0;
-            z-index: 99;
-            background: #FFFFFF;
-            border-bottom: 1rpx solid #eee;
-        }
+    .tabs-wrapper {
+        position: sticky;
+        top: 0;
+        z-index: 99;
+        background: #FFFFFF;
+        border-bottom: 1rpx solid #eee;
+    }
 
-        .order-list-container {
-            padding: 20rpx;
-        }
+    .order-list-container {
+        padding: 20rpx;
     }
+}
 </style>

+ 13 - 1
pages-car/pages/order-detail.vue

@@ -95,8 +95,12 @@
             </view>
         </view>
 
-        <!-- 订单时间信息 -->
+        <!-- 订单时间及其他信息 -->
         <view class="info-card time-card">
+            <view class="row" v-if="orderInfo.outOfStock">
+                <text class="label">如遇缺货</text>
+                <text class="value" style="color: #ff4500; font-weight: 500;">{{ outOfStockText }}</text>
+            </view>
             <view class="row">
                 <text class="label">下单时间</text>
                 <text class="value">{{ orderInfo.createTime }}</text>
@@ -176,6 +180,14 @@ export default {
                 '7': '交易关闭'
             };
             return map[String(this.orderInfo.status)] || '未知状态';
+        },
+        outOfStockText() {
+            const map = {
+                '1': '缺货时电话与我沟通',
+                '2': '其他商品继续发货(缺货商品退款)',
+                '3': '有缺货直接取消订单'
+            };
+            return map[String(this.orderInfo.outOfStock)] || '其他商品继续发货(缺货商品退款)';
         }
     },
     onLoad(options) {

+ 10 - 2
pages-sell/components/detail/footer-bar.vue

@@ -22,8 +22,8 @@
 			<view v-if="hasStock" class="action-btn in-stock" @click="onAddCart">
 				<text>加入购物车</text>
 			</view>
-			<view v-else class="action-btn no-stock" @click="onNotify">
-				<text>到货通知</text>
+			<view v-else class="action-btn no-stock" :class="{ 'gray-bg': hasArrivalNotice === 1 }" @click="onNotify">
+				<text>{{ hasArrivalNotice === 1 ? '取消到货通知' : '到货通知' }}</text>
 			</view>
 		</view>
 	</view>
@@ -40,6 +40,10 @@
 			isCollect: {
 				type: Boolean,
 				default: false
+			},
+			hasArrivalNotice: {
+				type: Number,
+				default: 0
 			}
 		},
 		data() {
@@ -155,6 +159,10 @@
 				color: #fff;
 				font-weight: bold;
 				background: linear-gradient(0deg, #38C248 0%, #5FEA6F 100%);
+				
+				&.gray-bg {
+					background: #ccc !important;
+				}
 			}
 		}
 	}

+ 62 - 59
pages-sell/components/detail/poster-popup.vue

@@ -1,9 +1,10 @@
 <template>
 	<CustomPopup v-model="show" mode="center" width="680rpx" bgColor="transparent" :maskClosable="true">
 		<view class="poster-popup-content">
-			
+
 			<!-- Canvas (Hidden) -->
-			<canvas canvas-id="posterCanvas" class="poster-canvas" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"></canvas>
+			<canvas canvas-id="posterCanvas" class="poster-canvas"
+				:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"></canvas>
 
 			<!-- Generated Image -->
 			<view class="image-container" v-if="posterImage">
@@ -34,6 +35,10 @@ export default {
 		userInfo: {
 			type: Object,
 			default: () => ({})
+		},
+		qrcodeUrl: {
+			type: String,
+			default: ''
 		}
 	},
 	data() {
@@ -60,23 +65,23 @@ export default {
 			try {
 				// 确保Canvas元素已渲染
 				await new Promise(resolve => setTimeout(resolve, 100));
-				
+
 				const ctx = uni.createCanvasContext('posterCanvas', this);
 				this.ctx = ctx;
-				
+
 				// Setup dimensions
 				const sysInfo = uni.getSystemInfoSync();
 				const baseScale = (sysInfo.windowWidth || 375) / 750;
 				// 清晰度与稳定性折中:按设备像素比绘制,但最多放大到 2 倍
 				const renderRatio = Math.min(Math.max(sysInfo.pixelRatio || 1, 1), 2);
 				const scale = baseScale * renderRatio;
-				
+
 				// User request: Canvas 340px, Padding 10px.
 				// Mapping to rpx: 340px -> 680rpx, 10px -> 20rpx.
 				const width = Math.floor(680 * scale);
 				const height = Math.floor(1140 * scale);
-				const padding = 20 * scale; 
-				
+				const padding = 20 * scale;
+
 				// 确保Canvas尺寸在合理范围内
 				const maxCanvasSize = 2000; // 保守上限,兼顾清晰度与稳定性
 				if (width > maxCanvasSize || height > maxCanvasSize) {
@@ -90,15 +95,15 @@ export default {
 					this.canvasWidth = width;
 					this.canvasHeight = height;
 				}
-				
+
 				// 1. Draw Background (Gradient, Rounded)
 				// linear-gradient(0deg, #FBFFFF 0%, #BFFCFE 100%) -> Bottom to Top
 				const grd = ctx.createLinearGradient(0, this.canvasHeight, 0, 0);
 				grd.addColorStop(0, '#FBFFFF');
 				grd.addColorStop(1, '#BFFCFE');
-				
+
 				this.roundRect(ctx, 0, 0, this.canvasWidth, this.canvasHeight, 36 * scale, grd);
-				
+
 				// 2. Draw Product Cover
 				const coverPath = await this.getImageInfo(this.product.cover || '/static/img/1.png', '/static/img/1.png');
 				// Image width = Canvas width - 2 * padding
@@ -106,7 +111,7 @@ export default {
 				const coverHeight = imgWidth; // Square cover
 				const coverX = padding;
 				const coverY = padding;
-				
+
 				ctx.save();
 				// Rounded corners for the cover image itself? 
 				// Prototype usually has rounded corners for the cover too.
@@ -125,27 +130,27 @@ export default {
 					ctx.fillRect(coverX, coverY, imgWidth, coverHeight);
 				}
 				ctx.restore();
-				
+
 				// 3. Draw User Info
 				const moveDownY = 20 * scale;
 				const contentPadding = 30 * scale;
 
 				const avatarSize = 80 * scale;
 				const avatarX = padding;
-				const avatarY = coverY + coverHeight - (avatarSize / 2) + moveDownY; 
-				
+				const avatarY = coverY + coverHeight - (avatarSize / 2) + moveDownY;
+
 				// Avatar Border/Background
 				ctx.beginPath();
-				ctx.arc(avatarX + avatarSize/2, avatarY + avatarSize/2, avatarSize/2 + 2*scale, 0, 2 * Math.PI);
+				ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2 + 2 * scale, 0, 2 * Math.PI);
 				ctx.setFillStyle('#FFFFFF');
 				ctx.fill();
-				
+
 				// Avatar Image
-				const avatarUrl = this.userInfo.avatar || 'https://img.yzcdn.cn/vant/cat.jpeg';
-				const avatarPath = await this.getImageInfo(avatarUrl, '/static/img/1.png');
+				const avatarUrl = this.userInfo.imgPath || 'https://shuhi.oss-cn-qingdao.aliyuncs.com/mini/logo3.png';
+				const avatarPath = await this.getImageInfo(avatarUrl, avatarUrl);
 				ctx.save();
 				ctx.beginPath();
-				ctx.arc(avatarX + avatarSize/2, avatarY + avatarSize/2, avatarSize/2, 0, 2 * Math.PI);
+				ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, 2 * Math.PI);
 				ctx.clip();
 				try {
 					if (avatarPath) {
@@ -160,27 +165,27 @@ export default {
 					ctx.fillRect(avatarX, avatarY, avatarSize, avatarSize);
 				}
 				ctx.restore();
-				
+
 				// User Name & Recomm Text
 				// Align with avatar
 				const userInfoY = coverY + coverHeight + 25 * scale + moveDownY;
 				const textLeftX = avatarX + avatarSize + 16 * scale;
-				
+
 				ctx.setFillStyle('#1D1D1D');
 				ctx.setFontSize(28 * scale);
 				ctx.setTextAlign('left');
 				ctx.fillText(this.userInfo.nickName || '微信用户', textLeftX, userInfoY);
-				
+
 				ctx.setFillStyle('#666666');
 				ctx.setFontSize(26 * scale);
 				ctx.setTextAlign('right');
 				// Align to right padding edge
 				ctx.fillText('推荐您一本好书', this.canvasWidth - padding, userInfoY);
 				ctx.setTextAlign('left'); // Reset
-				
+
 				// 4. Product Info
 				let currentY = coverY + coverHeight + 110 * scale + moveDownY;
-				
+
 				// Title
 				ctx.setFillStyle('#1D1D1D');
 				const titleFontSize = 34 * scale;
@@ -189,32 +194,32 @@ export default {
 				// Width constraint: Canvas width - 2*contentPadding
 				const lastTitleY = this.drawText(ctx, title, contentPadding, currentY, this.canvasWidth - 2 * contentPadding, titleFontSize * 1.5);
 				currentY = lastTitleY + 60 * scale;
-				
+
 				// Author
 				ctx.setFillStyle('#666666');
 				const authorFontSize = 26 * scale;
 				ctx.setFontSize(authorFontSize);
 				ctx.fillText(`作者: ${this.product.author || '未知'}`, contentPadding, currentY);
 				currentY += 70 * scale;
-				
+
 				// Price
 				ctx.setFillStyle('#D81A00');
 				const priceFontSize = 44 * scale;
 				ctx.setFontSize(priceFontSize);
-				const price = `¥ ${this.product.balanceMoney || '0.00'}`;
+				const price = `¥ ${this.product.sellPrice || '0.00'}`;
 				const priceWidth = ctx.measureText(price).width;
 				ctx.fillText(price, contentPadding, currentY);
-				
+
 				// Original Price
 				ctx.setFillStyle('#999999');
 				ctx.setFontSize(26 * scale);
-				const origPrice = `原价: ¥ ${this.product.sellPrice || '0.00'}`;
+				const origPrice = `原价: ¥ ${this.product.price || '0.00'}`;
 				ctx.fillText(origPrice, contentPadding + priceWidth + 20 * scale, currentY);
-				
+
 				// Divider
 				const footerHeight = 180 * scale;
 				const dividerY = this.canvasHeight - footerHeight;
-				
+
 				ctx.setStrokeStyle('#dddddd');
 				if (typeof ctx.setLineDash === 'function') {
 					ctx.setLineDash([8, 8]);
@@ -226,17 +231,17 @@ export default {
 				if (typeof ctx.setLineDash === 'function') {
 					ctx.setLineDash([]);
 				}
-				
+
 				// 5. Footer (Store & QR)
 				const footerContentY = dividerY + 50 * scale;
-				
+
 				// Store Name
 				ctx.setFillStyle('#1D1D1D');
 				ctx.setFontSize(32 * scale);
 				const storeName = '书嗨循环书店';
 				const storeNameWidth = ctx.measureText(storeName).width;
 				ctx.fillText(storeName, padding, footerContentY + 10 * scale);
-				
+
 				// Mascot Icon (Behind Store Name)
 				const iconSize = 70 * scale;
 				const iconX = padding + storeNameWidth + 40 * scale;
@@ -255,27 +260,25 @@ export default {
 				ctx.setFillStyle('#666666');
 				ctx.setFontSize(22 * scale);
 				ctx.fillText('不辜负每一个爱书的人', padding, footerContentY + 54 * scale);
-				
+
 				// QR Code
 				const qrSize = 130 * scale;
 				const qrX = this.canvasWidth - padding - qrSize;
 				const qrY = footerContentY - 20 * scale;
-				
+
 				// Use gzh.png as the QR code (Figure 2)
 				try {
-					const qrPath = await this.getImageInfo('/pages-sell/static/goods/icon-shuhai.png', '/pages-sell/static/goods/icon-shuhai.png');
-					if (qrPath) {
-						ctx.drawImage(qrPath, qrX, qrY, qrSize, qrSize);
-					} else {
-						throw new Error('qrPath is empty');
+					if (this.qrcodeUrl) {
+						const qrPath = await this.getImageInfo(this.qrcodeUrl);
+						if (qrPath) {
+							ctx.drawImage(qrPath, qrX, qrY, qrSize, qrSize);
+						}
 					}
 				} catch (e) {
 					console.error('Draw QR code error:', e);
-					// 绘制失败时使用默认颜色块
-					ctx.setFillStyle('#f0f0f0');
-					ctx.fillRect(qrX, qrY, qrSize, qrSize);
 				}
-				
+				// 如果二维码获取失败,不影响海报创建,继续后续绘制
+
 				// 使用Promise包装draw方法,确保绘制完成
 				await new Promise((resolve) => {
 					ctx.draw(false, () => {
@@ -285,7 +288,7 @@ export default {
 						}, 500);
 					});
 				});
-				
+
 				// 转换Canvas为图片(增加重试,兼容部分机型导出时机问题)
 				const tempFilePath = await this.canvasToTempFilePathWithRetry({
 					canvasId: 'posterCanvas',
@@ -297,7 +300,7 @@ export default {
 				}, 3);
 				this.posterImage = tempFilePath;
 				uni.hideLoading();
-				
+
 			} catch (e) {
 				console.error('Generate poster error:', e);
 				uni.hideLoading();
@@ -376,16 +379,16 @@ export default {
 			ctx.moveTo(x + r, y);
 			if (tr) ctx.arcTo(x + w, y, x + w, y + h, r);
 			else ctx.lineTo(x + w, y);
-			
+
 			if (br) ctx.arcTo(x + w, y + h, x, y + h, r);
 			else ctx.lineTo(x + w, y + h);
-			
+
 			if (bl) ctx.arcTo(x, y + h, x, y, r);
 			else ctx.lineTo(x, y + h);
-			
+
 			if (tl) ctx.arcTo(x, y, x + w, y, r);
 			else ctx.lineTo(x, y);
-			
+
 			ctx.closePath();
 		},
 		drawText(ctx, str, x, y, maxWidth, lineHeight) {
@@ -414,7 +417,7 @@ export default {
 					uni.showToast({ title: '保存成功', icon: 'success' });
 				},
 				fail: (err) => {
-                    console.log(err);
+					console.log(err);
 					// Handle auth denial
 					uni.showModal({
 						title: '提示',
@@ -438,8 +441,8 @@ export default {
 	flex-direction: column;
 	align-items: center;
 	position: relative;
-    padding: 0; // Remove padding
-    background: transparent; 
+	padding: 0; // Remove padding
+	background: transparent;
 }
 
 .poster-canvas {
@@ -452,16 +455,16 @@ export default {
 	flex-direction: column;
 	align-items: center;
 	width: 680rpx; // Match canvas width
-	
+
 	.poster-img {
-		width: 100%; 
+		width: 100%;
 		height: auto;
 		border-radius: 36rpx;
 	}
-	
+
 	.tips {
 		margin-top: 20rpx;
-		color: #fff; 
+		color: #fff;
 		font-size: 24rpx;
 	}
 }
@@ -475,7 +478,7 @@ export default {
 	align-items: center;
 	background: linear-gradient(0deg, #FBFFFF 0%, #BFFCFE 100%);
 	border-radius: 36rpx;
-	
+
 	.loading-text {
 		margin-top: 20rpx;
 		color: #666;

+ 14 - 8
pages-sell/components/detail/share-popup.vue

@@ -28,9 +28,9 @@
 				</view>
 			</view>
 		</CustomPopup>
-		
+
 		<!-- Poster Popup -->
-		<PosterPopup ref="posterPopup" :product="product" :userInfo="userInfo"></PosterPopup>
+		<PosterPopup ref="posterPopup" :product="product" :userInfo="userInfo" :qrcodeUrl="qrcodeUrl"></PosterPopup>
 	</view>
 </template>
 
@@ -51,17 +51,23 @@ export default {
 			default: () => ({})
 		}
 	},
-	computed: {
-		...mapState('user', ['userInfo'])
-	},
 	data() {
 		return {
-			show: false
+			show: false,
+			userInfo: {},
+			qrcodeUrl: ''
 		};
 	},
 	methods: {
 		open() {
 			this.show = true;
+			this.userInfo = uni.getStorageSync('userInfo') || {};
+			// this.$u.api.getIsbnQrcodeAjax(this.product.isbn).then(res => {
+			// 	if (res.code == 200) {
+			// 		console.log(res);
+			// 		this.qrcodeUrl = res.data;
+			// 	}
+			// });
 		},
 		close() {
 			this.show = false;
@@ -135,7 +141,7 @@ export default {
 			display: flex;
 			flex-direction: column;
 			align-items: center;
-			
+
 			.share-btn {
 				display: flex;
 				flex-direction: column;
@@ -144,7 +150,7 @@ export default {
 				padding: 0;
 				margin: 0;
 				line-height: 1.5;
-				
+
 				&::after {
 					border: none;
 				}

+ 14 - 5
pages-sell/components/select-good-popup/index.vue

@@ -72,9 +72,9 @@
 					<text class="price">¥{{ currentProduct.maxReducePrice }}</text>
 					<text class="desc">分享一人可降 {{ currentProduct.reducePrice }} 元</text>
 				</view>
-				<view class="btn btn-green" @click="handleAction">
+				<view class="btn btn-green" @click="handleAction" :class="{ 'btn-gray': !hasStock && currentProduct.hasArrivalNotice === 1 }">
 					<text v-if="hasStock" class="price">¥{{ displayTotalPrice }}</text>
-					<text class="desc" :class="{ 'notice': !hasStock }">{{ hasStock ? '加入购物车' : '到货通知' }}</text>
+					<text class="desc" :class="{ 'notice': !hasStock }">{{ hasStock ? '加入购物车' : (currentProduct.hasArrivalNotice === 1 ? '取消到货通知' : '到货通知') }}</text>
 				</view>
 			</view>
 			</view>
@@ -178,12 +178,17 @@ export default {
 			this.handleConfirm();
 		},
 		handleNotify() {
-			//设置到货通知
-			uni.$u.http.post('/token/shop/user/noticeArrival', {
+			const isCancel = this.currentProduct.hasArrivalNotice === 1;
+			const apiUrl = isCancel ? '/token/shop/user/noticeArrivalCancel' : '/token/shop/user/noticeArrival';
+			
+			uni.$u.http.post(apiUrl, {
 				isbn: this.currentProduct.isbn,
 			}).then(res => {
 				if (res.code === 200) {
-					this.$u.toast('到货通知设置成功');
+					const newValue = isCancel ? 0 : 1;
+					this.$set(this.currentProduct, 'hasArrivalNotice', newValue);
+					this.$emit('notice-change', newValue);
+					this.$u.toast(isCancel ? '已取消到货通知' : '到货通知设置成功');
 				}
 			}).finally(() => {
 				this.close();
@@ -565,6 +570,10 @@ export default {
 			background: linear-gradient(0deg, #38C248 0%, #5FEA6F 100%);
 			border-radius: 0 50rpx 50rpx 0;
 		}
+
+		&.btn-gray {
+			background: #ccc !important;
+		}
 	}
 }
 </style>

+ 27 - 7
pages-sell/pages/detail.vue

@@ -9,11 +9,11 @@
         </Navbar>
 
         <!-- Notification Bar -->
-        <view class="notification-bar">
+        <!-- <view class="notification-bar">
             <u-avatar size="40" src="https://img.yzcdn.cn/vant/cat.jpeg"></u-avatar>
 
             <text class="notif-text">微 ** 用户 购买了 《苏菲的世界》</text>
-        </view>
+        </view> -->
 
         <view class="content-scroll">
             <!-- Book Cover Area -->
@@ -72,10 +72,10 @@
         </view>
 
         <!-- Footer -->
-        <FooterBar :hasStock="hasStock" :isCollect="product.isCollect" @addCart="openSelectPopup" @notify="handleNotify" @collect="handleCollect"></FooterBar>
+        <FooterBar :hasStock="hasStock" :isCollect="product.isCollect" :hasArrivalNotice="product.hasArrivalNotice" @addCart="openSelectPopup" @notify="handleNotify" @collect="handleCollect"></FooterBar>
 
         <!-- Select Popup -->
-        <SelectGoodPopup ref="selectPopup" @confirm="onPopupConfirm"></SelectGoodPopup>
+        <SelectGoodPopup ref="selectPopup" @confirm="onPopupConfirm" @notice-change="onNoticeChange"></SelectGoodPopup>
 
         <!-- Share Popup -->
         <SharePopup ref="sharePopup" :product="product"></SharePopup>
@@ -162,10 +162,30 @@
             openSelectPopup(sourceFrom) {
                 this.$refs.selectPopup.open(this.product, sourceFrom);
             },
+            onNoticeChange(val) {
+                this.$set(this.product, 'hasArrivalNotice', val);
+            },
             handleNotify() {
-                uni.showToast({
-                    title: '已订阅到货通知',
-                    icon: 'success'
+                const isCancel = this.product.hasArrivalNotice === 1;
+                const apiUrl = isCancel ? '/token/shop/user/noticeArrivalCancel' : '/token/shop/user/noticeArrival';
+                
+                uni.showLoading({ mask: true });
+                uni.$u.http.post(apiUrl, { isbn: this.product.isbn }).then(res => {
+                    uni.hideLoading();
+                    if (res.code === 200) {
+                        this.$set(this.product, 'hasArrivalNotice', isCancel ? 0 : 1);
+                        uni.showToast({
+                            title: isCancel ? '已取消到货通知' : '已订阅到货通知',
+                            icon: 'success'
+                        });
+                    } else {
+                        uni.showToast({
+                            title: res.msg || '操作失败',
+                            icon: 'none'
+                        });
+                    }
+                }).catch(() => {
+                    uni.hideLoading();
                 });
             },
             onPopupConfirm(data) {