Parcourir la source

优惠劵功能

sunshibo il y a 1 mois
Parent
commit
ebb1690f22

+ 66 - 1
api/dining.js

@@ -63,6 +63,33 @@ export const GetCuisineDetail = (cuisineId, tableId) => {
   return api.get({ url, params });
 };
 
+// 门店全部优惠券列表(GET /dining/coupon/getStoreAllCouponList,分页 page、size 每页10条)
+function getStoreAllCouponListImpl(params) {
+  return api.get({
+    url: '/dining/coupon/getStoreAllCouponList',
+    params: {
+      storeId: params?.storeId,
+      tab: 1,
+      page: params?.page ?? 1,
+      size: params?.size ?? 10
+    }
+  });
+}
+export const GetStoreAllCouponList = getStoreAllCouponListImpl;
+// 兼容旧引用
+export const GetStoreUsableCouponList = getStoreAllCouponListImpl;
+
+// 用户已领/可用优惠券(GET /dining/coupon/userOwnedByStore,入参 storeId 门店ID)
+function getUserOwnedCouponListImpl(params) {
+  return api.get({
+    url: '/dining/coupon/userOwnedByStore',
+    params: {
+      storeId: params?.storeId
+    }
+  });
+}
+export const GetUserOwnedCouponList = getUserOwnedCouponListImpl;
+
 // 获取购物车(GET /store/order/cart/{tableId},建立 SSE 连接之后调用)
 export const GetOrderCart = (tableId) =>
   api.get({ url: `/store/order/cart/${encodeURIComponent(tableId)}` });
@@ -75,6 +102,10 @@ export const PostOrderCartAdd = (dto) =>
 export const PostOrderCartUpdate = (params) =>
   api.put({ url: '/store/order/cart/update', params, formUrlEncoded: true });
 
+// 更新餐具数量(PUT /store/order/cart/update-tableware,入参 quantity 餐具数量/用餐人数、tableId 桌号ID,form-urlencoded)
+export const PutOrderCartUpdateTableware = (params) =>
+  api.put({ url: '/store/order/cart/update-tableware', params, formUrlEncoded: true });
+
 // 清空购物车(DELETE /store/order/cart/clear,入参 tableId:query + body 双传,兼容不同后端)
 export const PostOrderCartClear = (tableId) => {
   const id = tableId != null ? String(tableId) : '';
@@ -82,7 +113,41 @@ export const PostOrderCartClear = (tableId) => {
   return api.delete({ url, params: id ? { tableId: id } : {} });
 };
 
-// 创建订单(POST /store/order/create,入参 dto:tableId 桌号ID、contactPhone 联系电话、couponId 优惠券ID 默认 null、dinerCount 就餐人数、immediatePay 是否立即支付 默认 0、remark 备注信息 最多30字)
+// 锁定订单(POST /store/dining/order/lock,入参 tableId 桌号ID)
+function postOrderLockImpl(params) {
+  const tableId = params?.tableId != null ? String(params.tableId) : '';
+  const url = tableId ? `/store/dining/order/lock?tableId=${encodeURIComponent(tableId)}` : '/store/dining/order/lock';
+  return api.post({ url, params: {} });
+}
+export const PostOrderLock = postOrderLockImpl;
+
+// 解锁订单(POST /store/dining/order/unlock,入参 tableId 桌号ID,与 lock 请求方式一致)
+function postOrderUnlockImpl(params) {
+  const tableId = params?.tableId != null ? String(params.tableId) : '';
+  const url = tableId ? `/store/dining/order/unlock?tableId=${encodeURIComponent(tableId)}` : '/store/dining/order/unlock';
+  return api.post({ url, params: {} });
+}
+export const PostOrderUnlock = postOrderUnlockImpl;
+// 兼容:通过 diningApi.postOrderUnlock 调用
+export const postOrderUnlock = postOrderUnlockImpl;
+
+// 结算页锁定订单(POST /store/dining/order/settlement/lock,入参 orderId 订单ID)
+function postOrderSettlementLockImpl(params) {
+  const orderId = params?.orderId != null ? String(params.orderId) : '';
+  const url = orderId ? `/store/dining/order/settlement/lock?orderId=${encodeURIComponent(orderId)}` : '/store/dining/order/settlement/lock';
+  return api.post({ url, params: {} });
+}
+export const PostOrderSettlementLock = postOrderSettlementLockImpl;
+
+// 结算页解锁订单(POST /store/dining/order/settlement/unlock,入参 orderId 订单ID)
+function postOrderSettlementUnlockImpl(params) {
+  const orderId = params?.orderId != null ? String(params.orderId) : '';
+  const url = orderId ? `/store/dining/order/settlement/unlock?orderId=${encodeURIComponent(orderId)}` : '/store/dining/order/settlement/unlock';
+  return api.post({ url, params: {} });
+}
+export const PostOrderSettlementUnlock = postOrderSettlementUnlockImpl;
+
+// 创建订单(POST /store/order/create,入参 dto:tableId、contactPhone、couponId、discountAmount 优惠金额、tablewareFee 餐具费、payAmount 实付金额(菜品总价-优惠金额+餐具费)、dinerCount、immediatePay、remark)
 export const PostOrderCreate = (dto) =>
   api.post({ url: '/store/order/create', params: dto });
 

+ 11 - 5
components/NavBar/index.vue

@@ -6,7 +6,7 @@
 			<view class="nav-bar flex">
 				<view class="nav-bar_left" v-if="!hideLeft">
 					<slot name='left'>
-						<view class="left-btns" :class="{ 'left-btns_home': !getIsHome && !onlyBack, 'left-btns_only-back': onlyBack }">
+						<view class="left-btns" :class="{ 'left-btns_home': onlyHome || (!getIsHome && !onlyBack), 'left-btns_only-back': onlyBack && !onlyHome }">
 							<view class="back" hover-class="hover-active" @click="go('back')">
 								<image src="https://cdn.aliyinba.com/UploadFiles/shop2/number/nav_bar/nav-back.png" mode="aspectFill"></image>
 							</view>
@@ -44,10 +44,13 @@
 	menuButtonInfo = uni.getMenuButtonBoundingClientRect() // 右上角胶囊信息
 	// #endif
 	
+	const emit = defineEmits(['back'])
 	const props = defineProps({
 		title: { type: String },
 		hideLeft: { type: Boolean, default: false },
 		onlyBack: { type: Boolean, default: false }, // 仅显示返回键,不显示首页
+		onlyHome: { type: Boolean, default: false }, // 仅显示返回主页(首页)按钮,左上角永远回首页
+		customBack: { type: Boolean, default: false }, // 为 true 时点击返回触发 @back,由父组件处理
 		warn: { type: Boolean, default: false }, // 警告
 		shadow: { type: Boolean, default: false }, // 警告
 	})
@@ -89,12 +92,15 @@
 	function go(type){
 		switch (type){
 			case 'back':
-				uni.navigateBack()
+				if (props.customBack) {
+					emit('back')
+				} else {
+					uni.navigateBack()
+				}
 				break;
 			case 'home':
-				uni.reLaunch({
-					url: '/pages/index/index'
-				})
+				// 首页为 tabBar 页面,用 switchTab
+				uni.switchTab({ url: '/pages/index/index' });
 				break;
 			default:
 				uni.navigateTo({ url: type })

+ 8 - 1
pages.json

@@ -31,7 +31,8 @@
 		{
 			"path": "pages/orderFood/index",
 			"style": {
-				"navigationBarTitleText": "点餐"
+				"navigationBarTitleText": "点餐",
+				"navigationStyle": "custom"
 			}
 		},
 		{
@@ -48,6 +49,12 @@
 			}
 		},
 		{
+			"path": "pages/checkout/index",
+			"style": {
+				"navigationBarTitleText": "确认订单"
+			}
+		},
+		{
 			"path": "pages/result/index",
 			"style": {
 				"navigationBarTitleText": ""

+ 469 - 0
pages/checkout/index.vue

@@ -0,0 +1,469 @@
+<template>
+  <!-- 确认支付页面:订单信息、菜品清单、价格明细、确认支付 -->
+  <view class="content">
+    <view class="card">
+      <view class="card-header">
+        <view class="tag"></view>
+        <view class="card-header-title">订单信息</view>
+      </view>
+      <view class="card-content">
+        <view class="info-item">
+          <view class="info-item-label">就餐桌号</view>
+          <view class="info-item-value">{{ orderInfo.tableNumber || orderInfo.tableId || '—' }}</view>
+        </view>
+        <view class="info-item">
+          <view class="info-item-label">用餐人数</view>
+          <view class="info-item-value">{{ orderInfo.diners || '—' }}人</view>
+        </view>
+        <view class="info-item">
+          <view class="info-item-label">联系电话</view>
+          <view class="info-item-value">{{ orderInfo.contactPhone || '—' }}</view>
+        </view>
+        <view class="info-item">
+          <view class="info-item-label">备注信息</view>
+          <view class="info-item-value remark-text">{{ orderInfo.remark || '—' }}</view>
+        </view>
+      </view>
+    </view>
+
+    <view class="card" v-if="foodList.length > 0">
+      <view class="card-header">
+        <view class="tag"></view>
+        <view class="card-header-title">菜品清单</view>
+      </view>
+      <view class="card-content">
+        <view class="info-food">
+          <view v-for="(item, index) in foodList" :key="item.id || index" class="food-item">
+            <image :src="getItemImage(item)" mode="aspectFill" class="food-item__image"></image>
+            <view class="food-item__info">
+              <view class="food-item__name">{{ item.name }}</view>
+              <view class="food-item__desc" v-if="item.tags && item.tags.length > 0">
+                <text v-for="(tag, tagIndex) in item.tags" :key="tagIndex" class="food-item__tag">
+                  {{ tag.text }}<text v-if="tagIndex < item.tags.length - 1">,</text>
+                </text>
+              </view>
+            </view>
+            <view class="food-item__right">
+              <view class="food-item__price">
+                <text class="price-main">¥{{ formatPrice(item.price) }}</text>
+              </view>
+              <view class="food-item__quantity">{{ item.quantity || 1 }}份</view>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <view class="card">
+      <view class="card-header">
+        <view class="tag"></view>
+        <view class="card-header-title">价格明细</view>
+      </view>
+      <view class="card-content">
+        <view class="info-item">
+          <view class="info-item-label">菜品总价</view>
+          <view class="info-item-value">¥{{ formatPrice(orderInfo.totalAmount) }}</view>
+        </view>
+        <view class="info-item">
+          <view class="info-item-label">餐具费</view>
+          <view class="info-item-value">¥{{ formatPrice(orderInfo.utensilFee ?? 0) }}</view>
+        </view>
+        <view class="info-item info-item--coupon">
+          <view class="info-item-label">优惠券</view>
+          <view class="info-item-value coupon-value">
+            <text v-if="(orderInfo.discountAmount ?? 0) > 0" class="coupon-amount">{{ couponDisplayText }}</text>
+            <text v-else class="coupon-placeholder">—</text>
+          </view>
+        </view>
+        <view v-if="(orderInfo.discountAmount ?? 0) > 0" class="info-item info-item--coupon">
+          <view class="info-item-label">优惠金额</view>
+          <view class="info-item-value coupon-value">
+            <text class="coupon-amount">-¥{{ formatPrice(orderInfo.discountAmount) }}</text>
+          </view>
+        </view>
+        <view class="price-line">
+          <view class="price-line-label">应付金额</view>
+          <view class="price-line-value">¥{{ formatPrice(orderInfo.payAmount ?? 0) }}</view>
+        </view>
+      </view>
+    </view>
+
+    <view class="bottom-button">
+      <view class="bottom-button-text" hover-class="hover-active" @click="handleConfirmPay">确认支付 ¥{{ formatPrice(orderInfo.payAmount ?? 0) }}</view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { onLoad, onShow, onUnload } from '@dcloudio/uni-app';
+import { ref, computed } from 'vue';
+import { go } from '@/utils/utils.js';
+import { getFileUrl } from '@/utils/file.js';
+import { useUserStore } from '@/store/user.js';
+import * as diningApi from '@/api/dining.js';
+
+const orderId = ref('');
+const orderInfo = ref({
+  orderNo: '',
+  tableId: '',
+  tableNumber: '',
+  diners: '',
+  contactPhone: '',
+  remark: '',
+  totalAmount: 0,
+  utensilFee: 0,
+  couponId: null,
+  couponName: '',
+  couponType: null,   // 1 满减 2 折扣
+  discountRate: null, // 折扣券力度,如 5.5 表示 5.5折
+  nominalValue: null, // 满减券面额
+  discountAmount: 0,
+  payAmount: 0
+});
+
+const foodList = ref([]);
+
+// 优惠券展示:满减券显示 nominalValue+元,折扣券显示 discountRate+折,否则显示 couponName 或 已使用优惠券
+const couponDisplayText = computed(() => {
+  const o = orderInfo.value;
+  if ((o.discountAmount ?? 0) <= 0) return '';
+  const type = Number(o.couponType);
+  if (type === 1 && (o.nominalValue != null && o.nominalValue !== '')) {
+    const val = Number(o.nominalValue);
+    return Number.isNaN(val) ? (o.couponName || '已使用优惠券') : val + '元';
+  }
+  if (type === 2 && (o.discountRate != null && o.discountRate !== '')) {
+    const rate = Number(o.discountRate);
+    return Number.isNaN(rate) ? (o.couponName || '已使用优惠券') : rate + '折';
+  }
+  return o.couponName || '已使用优惠券';
+});
+
+function formatPrice(price) {
+  const num = Number(price);
+  return Number.isNaN(num) ? '0.00' : num.toFixed(2);
+}
+
+function getItemImage(item) {
+  const raw = item?.image ?? item?.cuisineImage ?? item?.imageUrl ?? '';
+  if (typeof raw === 'string' && (raw.startsWith('http') || raw.startsWith('//'))) return raw;
+  return getFileUrl(raw || 'img/icon/shop.png');
+}
+
+function normalizeOrderItem(item) {
+  const tags = item?.tags ?? [];
+  const tagArr = Array.isArray(tags) ? tags : [];
+  const images = item?.images ?? item?.image;
+  const imageUrl = Array.isArray(images) ? images[0] : images;
+  return {
+    id: item?.id ?? item?.cuisineId,
+    name: item?.cuisineName ?? item?.name ?? '',
+    price: item?.totalPrice ?? item?.unitPrice ?? item?.price ?? 0,
+    image: imageUrl ?? item?.image ?? item?.cuisineImage ?? '',
+    quantity: item?.quantity ?? 1,
+    tags: tagArr.map((t) => (typeof t === 'string' ? { text: t } : { text: t?.text ?? t?.tagName ?? '' }))
+  };
+}
+
+const fetchOrderDetail = async () => {
+  const id = orderId.value || '';
+  if (!id) return;
+  try {
+    const res = await diningApi.GetOrderInfo(id);
+    const raw = res?.data ?? res ?? {};
+    orderInfo.value.orderNo = raw?.orderNo ?? raw?.orderId ?? '';
+    orderInfo.value.tableId = raw?.tableId ?? raw?.tableNumber ?? '';
+    orderInfo.value.tableNumber = raw?.tableNumber ?? raw?.tableNo ?? '';
+    orderInfo.value.diners = raw?.dinerCount ?? '';
+    orderInfo.value.contactPhone = raw?.contactPhone ?? raw?.phone ?? '';
+    orderInfo.value.remark = raw?.remark ?? '';
+    orderInfo.value.totalAmount = Number(raw?.dishTotal ?? raw?.orderAmount ?? raw?.foodAmount ?? raw?.totalAmount ?? 0) || 0;
+    orderInfo.value.utensilFee = Number(raw?.tablewareFee ?? raw?.tablewareAmount ?? 0) || 0;
+    orderInfo.value.couponId = raw?.couponId ?? null;
+    orderInfo.value.couponName = raw?.couponName ?? '';
+    orderInfo.value.couponType = raw?.couponType ?? null;
+    orderInfo.value.discountRate = raw?.discountRate ?? null;
+    orderInfo.value.nominalValue = raw?.nominalValue ?? null;
+    orderInfo.value.discountAmount = Number(raw?.discountAmount ?? raw?.couponAmount ?? raw?.couponDiscount ?? 0) || 0;
+    orderInfo.value.payAmount = Number(raw?.payAmount ?? raw?.totalAmount ?? raw?.totalPrice ?? 0) || 0;
+    const list = raw?.orderItemList ?? raw?.orderItems ?? raw?.items ?? raw?.detailList ?? [];
+    foodList.value = (Array.isArray(list) ? list : []).map(normalizeOrderItem);
+  } catch (err) {
+    console.error('获取订单详情失败:', err);
+    uni.showToast({ title: '加载失败', icon: 'none' });
+  }
+};
+
+const handleConfirmPay = async () => {
+  const id = orderId.value || '';
+  if (!id) {
+    uni.showToast({ title: '缺少订单ID', icon: 'none' });
+    return;
+  }
+  const userStore = useUserStore();
+  const openid = userStore.getOpenId || '';
+  if (!openid) {
+    uni.showToast({ title: '请先登录', icon: 'none' });
+    return;
+  }
+  const priceAmount = Number(orderInfo.value.payAmount ?? 0) || 0;
+  if (priceAmount <= 0) {
+    uni.showToast({ title: '订单金额异常', icon: 'none' });
+    return;
+  }
+  const orderNo = orderInfo.value.orderNo || '';
+  if (!orderNo) {
+    uni.showToast({ title: '缺少订单号', icon: 'none' });
+    return;
+  }
+  const price = Math.round(priceAmount * 100);
+  uni.showLoading({ title: '拉起支付...' });
+  try {
+    const res = await diningApi.PostOrderPay({
+      orderNo,
+      payer: openid,
+      price,
+      subject: '订单支付'
+    });
+    uni.hideLoading();
+    uni.requestPayment({
+      provider: 'wxpay',
+      timeStamp: res.timestamp,
+      nonceStr: res.nonce,
+      package: res.prepayId,
+      signType: res.signType,
+      paySign: res.sign,
+      success: () => {
+        uni.showToast({ title: '支付成功', icon: 'success' });
+        const oid = orderId.value || '';
+        if (oid) {
+          diningApi.PostOrderSettlementUnlock({ orderId: oid }).catch((e) => console.warn('解锁订单失败:', e));
+        }
+        const payType = 'wechatPayMininProgram';
+        const transactionId = orderNo;
+        const oid = orderId.value || '';
+        const q = [`id=${encodeURIComponent(oid)}`, `payType=${encodeURIComponent(payType)}`, `transactionId=${encodeURIComponent(transactionId)}`];
+        setTimeout(() => go(`/pages/paymentSuccess/index?${q.join('&')}`), 1500);
+      },
+      fail: (err) => {
+        const msg = err?.errMsg ?? err?.message ?? '支付失败';
+        if (String(msg).includes('cancel')) {
+          uni.showToast({ title: '已取消支付', icon: 'none' });
+        } else {
+          uni.showToast({ title: msg || '支付失败', icon: 'none' });
+        }
+      }
+    });
+  } catch (e) {
+    uni.hideLoading();
+    uni.showToast({ title: e?.message || '获取支付参数失败', icon: 'none' });
+  }
+};
+
+onLoad(async (options) => {
+  const id = options?.orderId ?? options?.id ?? '';
+  orderId.value = id;
+  if (options?.orderNo) orderInfo.value.orderNo = options.orderNo;
+  if (options?.tableId) orderInfo.value.tableId = options.tableId;
+  if (options?.tableNumber) orderInfo.value.tableNumber = options.tableNumber;
+  if (options?.diners) orderInfo.value.diners = options.diners;
+  if (options?.remark != null && options?.remark !== '') orderInfo.value.remark = decodeURIComponent(options.remark);
+  if (options?.totalAmount != null && options?.totalAmount !== '') {
+    orderInfo.value.payAmount = Number(options.totalAmount) || 0;
+  }
+  if (id) {
+    await fetchOrderDetail();
+  }
+});
+
+onShow(() => {
+  const id = orderId.value || '';
+  if (id) {
+    diningApi.PostOrderSettlementLock({ orderId: id }).catch((e) => console.warn('锁定订单失败:', e));
+  }
+});
+
+onUnload(() => {
+  const id = orderId.value || '';
+  if (id) {
+    diningApi.PostOrderSettlementUnlock({ orderId: id }).catch((e) => console.warn('解锁订单失败:', e));
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.content {
+  padding: 0 30rpx 300rpx;
+}
+
+.card {
+  background-color: #fff;
+  border-radius: 24rpx;
+  padding: 30rpx 0;
+  margin-top: 20rpx;
+
+  .card-header {
+    display: flex;
+    align-items: center;
+    padding: 0 30rpx;
+    height: 40rpx;
+    position: relative;
+    font-size: 27rpx;
+    color: #151515;
+    font-weight: bold;
+  }
+
+  .tag {
+    width: 10rpx;
+    height: 42rpx;
+    background: linear-gradient(35deg, #FCB73F 0%, #FC733D 100%);
+    border-radius: 0;
+    position: absolute;
+    left: 0;
+    top: 0;
+  }
+
+  .card-content {
+    padding: 0 30rpx;
+  }
+
+  .info-item {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-top: 20rpx;
+    font-size: 27rpx;
+
+    .info-item-label {
+      color: #666666;
+    }
+
+    .info-item-value {
+      color: #151515;
+
+      &.remark-text {
+        flex: 1;
+        text-align: right;
+        word-break: break-all;
+      }
+    }
+
+    &--coupon .coupon-value {
+      display: flex;
+      align-items: center;
+      gap: 8rpx;
+    }
+    .coupon-amount {
+      color: #E61F19;
+    }
+    .coupon-placeholder {
+      color: #999999;
+    }
+  }
+
+  .price-line {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-top: 24rpx;
+    padding-top: 24rpx;
+    border-top: 1rpx solid #f0f0f0;
+    font-size: 28rpx;
+    font-weight: bold;
+
+    .price-line-label {
+      color: #151515;
+    }
+
+    .price-line-value {
+      color: #E61F19;
+    }
+  }
+
+  .info-food {
+    .food-item {
+      display: flex;
+      align-items: center;
+      padding: 20rpx 0;
+      border-bottom: 1rpx solid #f5f5f5;
+
+      &:last-child {
+        border-bottom: none;
+      }
+
+      &__image {
+        width: 120rpx;
+        height: 120rpx;
+        border-radius: 12rpx;
+        flex-shrink: 0;
+        background: #f5f5f5;
+      }
+
+      &__info {
+        flex: 1;
+        margin-left: 24rpx;
+        min-width: 0;
+      }
+
+      &__name {
+        font-size: 28rpx;
+        color: #151515;
+        font-weight: 500;
+      }
+
+      &__desc {
+        font-size: 22rpx;
+        color: #999;
+        margin-top: 6rpx;
+      }
+
+      &__tag {
+        margin-right: 4rpx;
+      }
+
+      &__right {
+        flex-shrink: 0;
+        text-align: right;
+      }
+
+      &__price .price-main {
+        font-size: 28rpx;
+        color: #151515;
+        font-weight: 600;
+      }
+
+      &__quantity {
+        font-size: 24rpx;
+        color: #999;
+        margin-top: 6rpx;
+      }
+    }
+  }
+}
+
+.bottom-button {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  padding: 20rpx 30rpx;
+  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
+  background: #fff;
+  box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.06);
+
+  .bottom-button-text {
+    height: 88rpx;
+    line-height: 88rpx;
+    text-align: center;
+    background: linear-gradient(90deg, #FCB73F 0%, #FC733D 100%);
+    color: #fff;
+    font-size: 32rpx;
+    font-weight: bold;
+    border-radius: 44rpx;
+  }
+
+  .hover-active {
+    opacity: 0.9;
+  }
+}
+</style>

+ 19 - 57
pages/orderFood/components/CartModal.vue

@@ -31,9 +31,9 @@
                         </view>
                     </view>
 
-                    <!-- 数量选择器 -->
+                    <!-- 数量选择器(餐具 cuisineId 为 -1 时不可修改) -->
                     <view class="cart-item__actions">
-                        <view class="action-btn minus" :class="{ disabled: item.quantity === 0 }"
+                        <view class="action-btn minus" :class="{ disabled: item.quantity === 0 || isTablewareItem(item) }"
                             @click="handleDecrease(item)" hover-class="hover-active">
                             <image :src="getFileUrl('img/icon/reduce1.png')" mode="aspectFit" class="action-icon"
                                 v-show="item.quantity == 0"></image>
@@ -41,7 +41,7 @@
                                 v-show="item.quantity != 0"></image>
                         </view>
                         <view class="quantity">{{ item.quantity || 0 }}</view>
-                        <view class="action-btn plus" @click="handleIncrease(item)" hover-class="hover-active">
+                        <view class="action-btn plus" :class="{ disabled: isTablewareItem(item) }" @click="handleIncrease(item)" hover-class="hover-active">
                             <image :src="getFileUrl('img/icon/add2.png')" mode="widthFix" class="action-icon"
                                 v-show="item.quantity < 99"></image>
                             <image :src="getFileUrl('img/icon/add1.png')" mode="widthFix" class="action-icon"
@@ -50,15 +50,6 @@
                     </view>
                 </view>
 
-                <!-- 优惠券部分 -->
-                <view class="coupon-section" v-if="showCouponSection" @click="handleCouponClick">
-                    <view class="coupon-section__label">优惠券</view>
-                    <view class="coupon-section__value" >
-                        <text class="discount-amount" v-if="discountAmount > 0">-¥{{ formatPrice(discountAmount)
-                            }}</text>
-                        <text class="arrow-text">›</text>
-                    </view>
-                </view>
             </scroll-view>
 
         </view>
@@ -78,18 +69,10 @@ const props = defineProps({
     cartList: {
         type: Array,
         default: () => []
-    },
-    discountAmount: {
-        type: Number,
-        default: 0
-    },
-    showCouponSection: {
-        type: Boolean,
-        default: true
     }
 });
 
-const emit = defineEmits(['update:open', 'increase', 'decrease', 'clear', 'coupon-click', 'order-click', 'close']);
+const emit = defineEmits(['update:open', 'increase', 'decrease', 'clear', 'order-click', 'close']);
 
 const getOpen = computed({
     get: () => props.open,
@@ -135,6 +118,13 @@ const getItemLinePrice = (item) => {
     return qty * unitPrice;
 };
 
+// 餐具(cuisineId 或 id 为 -1)不可修改数量
+const isTablewareItem = (item) => {
+    if (!item) return false;
+    const id = item.cuisineId ?? item.id;
+    return Number(id) === -1;
+};
+
 // 处理关闭
 const handleClose = () => {
     getOpen.value = false;
@@ -143,12 +133,20 @@ const handleClose = () => {
 
 // 增加数量
 const handleIncrease = (item) => {
+    if (isTablewareItem(item)) {
+        uni.showToast({ title: '不允许更改餐具数量', icon: 'none' });
+        return;
+    }
     if (item.quantity >= 99) return;
     emit('increase', item);
 };
 
 // 减少数量
 const handleDecrease = (item) => {
+    if (isTablewareItem(item)) {
+        uni.showToast({ title: '不允许更改餐具数量', icon: 'none' });
+        return;
+    }
     if (item && item.quantity > 0) {
         emit('decrease', item);
     }
@@ -167,11 +165,6 @@ const handleClear = () => {
     });
 };
 
-// 优惠券点击
-const handleCouponClick = () => {
-    emit('coupon-click');
-};
-
 </script>
 
 <style scoped lang="scss">
@@ -352,37 +345,6 @@ const handleCouponClick = () => {
     font-weight: bold;
 }
 
-.coupon-section {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    padding: 24rpx 0;
-    margin-top: 20rpx;
-
-    &__label {
-        font-size: 28rpx;
-        color: #151515;
-    }
-
-    &__value {
-        display: flex;
-        align-items: center;
-        gap: 8rpx;
-
-        .discount-amount {
-            font-size: 28rpx;
-            color: #E61F19;
-            font-weight: 500;
-        }
-
-        .arrow-text {
-            font-size: 32rpx;
-            color: #999999;
-            line-height: 1;
-        }
-    }
-}
-
 .cart-modal__footer {
     display: flex;
     align-items: center;

+ 35 - 86
pages/orderFood/components/CouponModal.vue

@@ -11,12 +11,14 @@
 
       <!-- 优惠券列表 -->
       <scroll-view class="coupon-modal__list" scroll-y v-if="couponList.length > 0">
-        <view v-for="(coupon, index) in couponList" :key="coupon.id || index"
-          :class="['coupon-card', { 'coupon-card--received': coupon.isReceived }]">
-          <!-- 左侧金额信息 -->
+        <view v-for="(coupon, index) in couponList" :key="coupon.id || index" class="coupon-card">
+          <!-- 左侧金额信息:数字大号,“元/折”小号 -->
           <view class="coupon-card__left">
-            <text class="amount-text">{{ coupon.amount }}元</text>
-            <text class="condition-text">满{{ coupon.minAmount }}可用</text>
+            <view class="amount-row">
+              <text class="amount-num">{{ amountNum(coupon) }}</text>
+              <text class="amount-unit">{{ amountUnit(coupon) }}</text>
+            </view>
+            <text class="condition-text">{{ (coupon.minAmount && Number(coupon.minAmount) > 0) ? ('满' + coupon.minAmount + '可用') : '无门槛' }}</text>
           </view>
 
           <!-- 中间信息 -->
@@ -24,29 +26,13 @@
             <text class="name-text">{{ coupon.name }}</text>
             <text class="expire-text">{{ formatExpireDate(coupon.expireDate) }}到期</text>
           </view>
-
-          <!-- 右侧操作区域 -->
-          <view class="coupon-card__right">
-            <!-- 未领取:显示领取按钮 -->
-            <view v-if="!coupon.isReceived" class="receive-btn" @click="handleReceive(coupon, index)"
-              hover-class="hover-active">
-              领取
-            </view>
-            <!-- 已领取:显示已领取标记 -->
-            <image :src="getFileUrl('img/icon/ylq.png')" mode="widthFix" v-else class="received-mark-img"></image>
-          </view>
         </view>
       </scroll-view>
 
-      <!-- 没有优惠券 -->
+      <!-- 无数据时显示 -->
       <view class="no-coupon-tip" v-if="couponList.length === 0">
         <image :src="getFileUrl('img/icon/noCoupon.png')" mode="widthFix" class="no-coupon-tip-img"></image>
         <view class="no-coupon-tip-text">暂无优惠券</view>
-        <view class="no-coupon-tip-url">您可以去
-          <text
-            class="no-coupon-tip-url-text" @click='copyUrl'>http:https://modao.cc/proto/KQv3fh5Kt7fy7tfvLMTCx/sharing?view_mode=read_only</text>
-          进行下载领取优惠券
-        </view>
       </view>
 
 
@@ -70,7 +56,7 @@ const props = defineProps({
   }
 });
 
-const emit = defineEmits(['update:open', 'receive', 'close']);
+const emit = defineEmits(['update:open', 'close']);
 
 const getOpen = computed({
   get: () => props.open,
@@ -97,17 +83,22 @@ const formatExpireDate = (date) => {
   return date;
 };
 
+// 金额数字与单位分开展示,单位用小字号
+const amountNum = (coupon) => {
+  const str = coupon.amountDisplay || (coupon.amount + '元');
+  return str.length > 0 ? str.slice(0, -1) : '';
+};
+const amountUnit = (coupon) => {
+  const str = coupon.amountDisplay || (coupon.amount + '元');
+  return str.length > 0 ? str.slice(-1) : '元';
+};
+
 // 处理关闭
 const handleClose = () => {
   getOpen.value = false;
   emit('close');
 };
 
-// 处理领取
-const handleReceive = (coupon, index) => {
-  emit('receive', { coupon, index });
-};
-
 const copyUrl = () => {
   uni.setClipboardData({
     data: 'http:https://modao.cc/proto/KQv3fh5Kt7fy7tfvLMTCx/sharing?view_mode=read_only',
@@ -178,7 +169,7 @@ const copyUrl = () => {
 
 .coupon-card {
   height: 191rpx;
-  background: #FEF2F2;
+  background: #FFFFFF;
   border: 1rpx solid #FFFFFF;
   display: flex;
   align-items: center;
@@ -195,17 +186,28 @@ const copyUrl = () => {
   &__left {
     display: flex;
     flex-direction: column;
-    // align-items: flex-start;
+    align-items: center;
     margin-right: 24rpx;
     min-width: 120rpx;
     text-align: center;
 
-    .amount-text {
+    .amount-row {
+      display: flex;
+      align-items: baseline;
+      justify-content: center;
+      margin-bottom: 8rpx;
+    }
+    .amount-num {
       font-size: 50rpx;
       font-weight: 600;
       color: #F47D1F;
       line-height: 1.2;
-      margin-bottom: 8rpx;
+    }
+    .amount-unit {
+      font-size: 28rpx;
+      font-weight: 500;
+      color: #F47D1F;
+      margin-left: 2rpx;
     }
 
     .condition-text {
@@ -238,66 +240,13 @@ const copyUrl = () => {
     }
   }
 
-  &__right {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    min-width: 100rpx;
-
-    .receive-btn {
-      width: 100rpx;
-      height: 56rpx;
-      background: #FF6B35;
-      border-radius: 28rpx;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      font-size: 28rpx;
-      color: #FFFFFF;
-      font-weight: 500;
-      transition: all 0.3s;
-
-      &:active {
-        opacity: 0.8;
-        transform: scale(0.95);
-      }
-    }
-
-    .received-mark {
-      width: 100rpx;
-      height: 100rpx;
-      border: 2rpx dashed #CCCCCC;
-      border-radius: 50%;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      background: rgba(255, 255, 255, 0.6);
-      position: relative;
-      opacity: 0.7;
-
-      .received-text {
-        font-size: 22rpx;
-        color: #999999;
-        transform: rotate(-15deg);
-        font-weight: 400;
-      }
-    }
-  }
-
-  .received-mark-img {
-    width: 114rpx;
-    height: 114rpx;
-  }
-
-  &--received {
-    background: #FFFFFF;
-  }
 }
 
 .no-coupon-tip {
   height: 700rpx;
   text-align: center;
   padding-top: 40rpx;
+  background: transparent;
 
   .no-coupon-tip-img {
     width: 343rpx;

+ 18 - 2
pages/orderFood/components/FoodCard.vue

@@ -25,12 +25,12 @@
         <text class="sales-text">月售:{{ food.monthlySales || 0 }}</text>
       </view>
       <view class="food-actions">
-        <view class="action-btn minus" :class="{ disabled: food.quantity === 0 }" @click="handleDecrease" hover-class="hover-active">
+        <view class="action-btn minus" :class="{ disabled: food.quantity === 0 || isTableware }" @click="handleDecrease" hover-class="hover-active">
           <image :src="getFileUrl('img/icon/reduce1.png')" mode="aspectFit" class="action-icon" v-show="food.quantity == 0"></image>
           <image :src="getFileUrl('img/icon/reduce2.png')" mode="aspectFit" class="action-icon" v-show="food.quantity != 0"></image>
         </view>
         <view class="quantity">{{ food.quantity || 0 }}</view>
-        <view class="action-btn plus" @click="handleIncrease" hover-class="hover-active">
+        <view class="action-btn plus" :class="{ disabled: isTableware }" @click="handleIncrease" hover-class="hover-active">
           <image :src="getFileUrl('img/icon/add2.png')" mode="widthFix" class="action-icon" v-show="food.quantity < 99"></image>
           <image :src="getFileUrl('img/icon/add1.png')" mode="widthFix" class="action-icon" v-show="food.quantity >= 99"></image>
         </view>
@@ -66,6 +66,14 @@ const props = defineProps({
 
 const emit = defineEmits(['increase', 'decrease']);
 
+// 餐具(cuisineId/id 为 -1)不可修改数量
+const isTableware = computed(() => {
+  const f = props.food;
+  if (!f) return false;
+  const id = f.id ?? f.cuisineId;
+  return Number(id) === -1;
+});
+
 // 后端返回的 tags 统一为 [{ text, type }] 便于绑定(兼容多种字段名与格式)
 const normalizedTags = computed(() => {
   const food = props.food;
@@ -119,11 +127,19 @@ const handleFoodClick = () => {
 };
 
 const handleIncrease = () => {
+  if (isTableware.value) {
+    uni.showToast({ title: '不允许更改餐具数量', icon: 'none' });
+    return;
+  }
   if (props.food.quantity >= 99) return;
   emit('increase', props.food);
 };
 
 const handleDecrease = () => {
+  if (isTableware.value) {
+    uni.showToast({ title: '不允许更改餐具数量', icon: 'none' });
+    return;
+  }
   if (props.food.quantity > 0) {
     emit('decrease', props.food);
   }

+ 77 - 19
pages/orderFood/components/SelectCouponModal.vue

@@ -9,18 +9,22 @@
         </view>
       </view>
 
-      <!-- 优惠券列表 -->
-      <scroll-view class="select-coupon-modal__list" scroll-y>
+      <!-- 优惠券列表:仅查看模式下不展示选中态、点击不触发选择 -->
+      <scroll-view class="select-coupon-modal__list" scroll-y v-if="couponList.length > 0">
         <view 
           v-for="(coupon, index) in couponList" 
           :key="coupon.id || index" 
           class="coupon-card"
-          @click="handleSelect(coupon, index)"
+          :class="{ 'coupon-card--view-only': viewOnly }"
+          @click="handleCardClick(coupon, index)"
         >
-          <!-- 左侧金额信息 -->
+          <!-- 左侧金额信息:数字大号,“元/折”小号 -->
           <view class="coupon-card__left">
-            <text class="amount-text">{{ coupon.amount }}元</text>
-            <text class="condition-text">满{{ coupon.minAmount }}可用</text>
+            <view class="amount-row">
+              <text class="amount-num">{{ amountNum(coupon) }}</text>
+              <text class="amount-unit">{{ amountUnit(coupon) }}</text>
+            </view>
+            <text class="condition-text">{{ (coupon.minAmount && Number(coupon.minAmount) > 0) ? ('满' + coupon.minAmount + '可用') : '无门槛' }}</text>
           </view>
 
           <!-- 中间信息 -->
@@ -29,13 +33,17 @@
             <text class="expire-text">{{ formatExpireDate(coupon.expireDate) }}到期</text>
           </view>
 
-          <!-- 右侧复选框 -->
-          <view class="coupon-card__right">
-            <image :src="getFileUrl('img/icon/sele1.png')" mode="widthFix" class="selected-icon" v-show="isSelected(coupon)"></image>
-            <image :src="getFileUrl('img/icon/sele2.png')" mode="widthFix" class="selected-icon" v-show="!isSelected(coupon)"></image>
+          <!-- 右侧单选(仅在选择模式下显示) -->
+          <view class="coupon-card__right" v-if="!viewOnly">
+            <image :src="getFileUrl('img/icon/sele1.png')" mode="widthFix" class="selected-icon" v-show="!isSelected(coupon)"></image>
+            <image :src="getFileUrl('img/icon/sele2.png')" mode="widthFix" class="selected-icon" v-show="isSelected(coupon)"></image>
           </view>
         </view>
       </scroll-view>
+      <!-- 无数据时显示 -->
+      <view class="select-coupon-modal__empty" v-else>
+        <text class="empty-text">暂无优惠券</text>
+      </view>
     </view>
   </BasicModal>
 </template>
@@ -57,6 +65,11 @@ const props = defineProps({
   selectedCouponId: {
     type: [Number, String],
     default: null
+  },
+  // 仅查看模式:来自左下角优惠券入口,只展示列表,不可选择
+  viewOnly: {
+    type: Boolean,
+    default: false
   }
 });
 
@@ -87,14 +100,25 @@ const formatExpireDate = (date) => {
   return date;
 };
 
-// 判断优惠券是否被选中
+// 金额数字部分(用于与“元/折”分开展示,单位用小字号)
+const amountNum = (coupon) => {
+  const str = coupon.amountDisplay || (coupon.amount + '元');
+  return str.length > 0 ? str.slice(0, -1) : '';
+};
+const amountUnit = (coupon) => {
+  const str = coupon.amountDisplay || (coupon.amount + '元');
+  return str.length > 0 ? str.slice(-1) : '元';
+};
+
+// 判断优惠券是否被选中(统一转字符串比较,避免 number/string 不一致)
 const isSelected = (coupon) => {
-  return props.selectedCouponId !== null && coupon.id === props.selectedCouponId;
+  if (props.selectedCouponId == null || props.selectedCouponId === '') return false;
+  return String(coupon.id) === String(props.selectedCouponId);
 };
 
-// 处理选择优惠券
-const handleSelect = (coupon, index) => {
-  // 如果已经选中,则取消选择;否则选中
+// 卡片点击:仅查看模式下不触发选择;否则按单选逻辑
+const handleCardClick = (coupon, index) => {
+  if (props.viewOnly) return;
   const newSelectedId = isSelected(coupon) ? null : coupon.id;
   emit('select', { coupon, index, selectedId: newSelectedId });
 };
@@ -113,7 +137,8 @@ const handleClose = () => {
   border-radius: 24rpx 24rpx 0 0;
   padding: 0;
   box-sizing: border-box;
-  max-height: 80vh;
+  height: 55vh;
+  max-height: 88vh;
   display: flex;
   flex-direction: column;
   overflow: hidden;
@@ -156,7 +181,24 @@ const handleClose = () => {
     padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
     box-sizing: border-box;
     overflow-y: auto;
-    min-height: 0;
+    min-height: 400rpx;
+  }
+
+  &__empty {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 80rpx 30rpx;
+    min-height: 400rpx;
+    background: transparent;
+    margin: 24rpx 30rpx 100rpx 30rpx;
+    box-sizing: border-box;
+
+    .empty-text {
+      font-size: 30rpx;
+      color: #666666;
+    }
   }
 }
 
@@ -180,19 +222,35 @@ const handleClose = () => {
     opacity: 0.8;
   }
 
+  &--view-only:active {
+    opacity: 1;
+  }
+
   &__left {
     display: flex;
     flex-direction: column;
+    align-items: center;
     margin-right: 24rpx;
     min-width: 120rpx;
     text-align: center;
 
-    .amount-text {
+    .amount-row {
+      display: flex;
+      align-items: baseline;
+      justify-content: center;
+      margin-bottom: 8rpx;
+    }
+    .amount-num {
       font-size: 50rpx;
       font-weight: 600;
       color: #F47D1F;
       line-height: 1.2;
-      margin-bottom: 8rpx;
+    }
+    .amount-unit {
+      font-size: 28rpx;
+      font-weight: 500;
+      color: #F47D1F;
+      margin-left: 2rpx;
     }
 
     .condition-text {

+ 157 - 96
pages/orderFood/index.vue

@@ -1,6 +1,8 @@
 <template>
-  <!-- 点餐页 -->
-  <view class="content">
+  <!-- 点餐页:左上角永远返回主页 -->
+  <view class="page-wrap">
+    <NavBar title="点餐" only-home />
+    <view class="content">
     <view class="top-info">桌号:{{ displayTableNumber }} &nbsp;&nbsp;就餐人数:{{ currentDiners }}人</view>
     <!-- 搜索 -->
     <input type="text" placeholder="请输入菜品名称" class="search-input" />
@@ -28,30 +30,31 @@
       @order-click="handleOrderClick" />
 
     <!-- 领取优惠券弹窗 -->
-    <CouponModal v-model:open="couponModalOpen" :coupon-list="couponList" @receive="handleCouponReceive"
-      @close="handleCouponClose" />
+    <CouponModal v-model:open="couponModalOpen" :coupon-list="couponList" @close="handleCouponClose" />
 
     <!-- 购物车弹窗:按接口格式展示 items(cuisineName/cuisineImage/quantity/unitPrice/subtotalAmount/remark) -->
-    <CartModal v-model:open="cartModalOpen" :cart-list="displayCartList" :discount-amount="discountAmount"
+    <CartModal v-model:open="cartModalOpen" :cart-list="displayCartList"
       @increase="handleIncrease" @decrease="handleDecrease" @clear="handleCartClear"
-      @coupon-click="handleSelectCouponClick" @order-click="handleOrderClick" @close="handleCartClose" />
+      @order-click="handleOrderClick" @close="handleCartClose" />
 
-    <!-- 选择优惠券弹窗 -->
+    <!-- 选择优惠券弹窗:左下角入口为仅查看,购物车内入口为可选 -->
     <SelectCouponModal v-model:open="selectCouponModalOpen" :coupon-list="availableCoupons"
-      :selected-coupon-id="selectedCouponId" @select="handleCouponSelect" @close="handleSelectCouponClose" />
+      :selected-coupon-id="selectedCouponId" :view-only="selectCouponViewOnly" @select="handleCouponSelect" @close="handleSelectCouponClose" />
+    </view>
   </view>
 </template>
 
 <script setup>
-import { onLoad, onUnload } from "@dcloudio/uni-app";
-import { ref, computed } from "vue";
+import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
+import NavBar from "@/components/NavBar/index.vue";
+import { ref, computed, nextTick } from "vue";
 import FoodCard from "./components/FoodCard.vue";
 import BottomActionBar from "./components/BottomActionBar.vue";
 import CouponModal from "./components/CouponModal.vue";
 import CartModal from "./components/CartModal.vue";
 import SelectCouponModal from "./components/SelectCouponModal.vue";
 import { go } from "@/utils/utils.js";
-import { DiningOrderFood, GetStoreCategories, GetStoreCuisines, getOrderSseConfig, GetOrderCart, PostOrderCartAdd, PostOrderCartUpdate, PostOrderCartClear } from "@/api/dining.js";
+import { DiningOrderFood, GetStoreCategories, GetStoreCuisines, getOrderSseConfig, GetOrderCart, PostOrderCartAdd, PostOrderCartUpdate, PostOrderCartClear, PutOrderCartUpdateTableware, GetUserOwnedCouponList } from "@/api/dining.js";
 import { createSSEConnection } from "@/utils/sse.js";
 
 const tableId = ref(''); // 桌号ID,来自上一页 url 参数 tableid,用于接口入参
@@ -70,6 +73,7 @@ const currentCategoryIndex = ref(0);
 const couponModalOpen = ref(false);
 const cartModalOpen = ref(false);
 const selectCouponModalOpen = ref(false);
+const selectCouponViewOnly = ref(false); // true=左下角仅查看,false=购物车内可选
 const discountAmount = ref(12); // 优惠金额,示例数据
 const selectedCouponId = ref(null); // 选中的优惠券ID
 
@@ -80,8 +84,8 @@ const categories = ref([]);
 const foodList = ref([]);
 // SSE 连接建立后拉取的购物车数据,在 foodList 就绪后合并
 let pendingCartData = null;
-// 购物车接口返回的完整数据:{ items, totalAmount, totalQuantity },用于按接口格式展示
-const cartData = ref({ items: [], totalAmount: 0, totalQuantity: 0 });
+// 购物车接口返回的完整数据:{ items, totalAmount, totalQuantity, tablewareFee },用于按接口格式展示
+const cartData = ref({ items: [], totalAmount: 0, totalQuantity: 0, tablewareFee: 0 });
 
 // 当前分类的菜品列表(按当前选中的分类 id 过滤)
 const currentFoodList = computed(() => {
@@ -138,46 +142,42 @@ const displayTotalAmount = computed(() => {
   return displayCartList.value.reduce((sum, item) => sum + getItemLinePrice(item), 0);
 });
 
-// 优惠券列表(示例数据)
-const couponList = ref([
-  {
-    id: 1,
-    amount: 38,
-    minAmount: 158,
-    name: '优惠券名称',
-    expireDate: '2024/07/28',
-    isReceived: false
-  },
-  {
-    id: 2,
-    amount: 8,
-    minAmount: 158,
-    name: '优惠券名称',
-    expireDate: '2024/07/28',
-    isReceived: false
-  },
-  {
-    id: 3,
-    amount: 682,
-    minAmount: 1580,
-    name: '优惠券名称',
-    expireDate: '2024/07/28',
-    isReceived: true
-  },
-  {
-    id: 4,
-    amount: 1038,
-    minAmount: 1580,
-    name: '优惠券名称',
-    expireDate: '2024/07/28',
-    isReceived: true
+// 优惠券列表(由接口 /dining/coupon/storeUsableList 返回后赋值)
+const couponList = ref([]);
+// 门店可用优惠券列表(选择优惠券弹窗用,与 couponList 同步自同一接口)
+const storeUsableCouponList = ref([]);
+
+// 规范化接口优惠券项为弹窗所需格式(对接 userOwnedByStore 返回的 data 数组:id/couponId/userCouponId、name、couponType、discountRate、minimumSpendingAmount、expirationTime 等)
+// couponType 1=满减券显示 nominalValue 为金额,2=折扣券显示 discountRate 为折扣力度
+function normalizeCouponItem(item) {
+  if (!item || typeof item !== 'object') return null;
+  const raw = item;
+  const couponType = Number(raw.couponType) || 0;
+  const nominalValue = Number(raw.nominalValue ?? raw.amount ?? 0) || 0;
+  const discountRate = Number(raw.discountRate) || 0;
+  const minAmount = Number(raw.minimumSpendingAmount ?? raw.minAmount ?? raw.min_amount ?? 0) || 0;
+  const isReceived = raw.canReceived === false;
+  let amountDisplay = nominalValue + '元';
+  if (couponType === 1) {
+    amountDisplay = nominalValue + '元';
+  } else if (couponType === 2 && discountRate > 0) {
+    amountDisplay = Math.round(discountRate) + '折';
   }
-]);
+  return {
+    id: raw.userCouponId ?? raw.id ?? raw.couponId ?? raw.coupon_id ?? '',
+    amount: nominalValue,
+    minAmount,
+    name: raw.name ?? raw.title ?? raw.couponName ?? '',
+    expireDate: raw.expirationTime ?? raw.endGetDate ?? raw.validDate ?? raw.expireDate ?? raw.endTime ?? '',
+    isReceived,
+    couponType,
+    discountRate,
+    amountDisplay
+  };
+}
 
-// 可用的优惠券列表(已领取的优惠券)
-const availableCoupons = computed(() => {
-  return couponList.value.filter(coupon => coupon.isReceived);
-});
+// 可用的优惠券列表(选择优惠券弹窗用,来自 storeUsableCouponList)
+const availableCoupons = computed(() => storeUsableCouponList.value);
 
 // 分类 id 统一转字符串,避免 number/string 比较导致误判
 const sameCategory = (a, b) => String(a ?? '') === String(b ?? '');
@@ -238,11 +238,12 @@ const fetchAndMergeCart = async (tableid) => {
     const cartRes = await GetOrderCart(tableid);
     const list = parseCartListFromResponse(cartRes);
     pendingCartData = list;
-    // 按接口格式绑定:data.items / totalAmount / totalQuantity
+    // 按接口格式绑定:data.items / totalAmount / totalQuantity / tablewareFee
     cartData.value = {
       items: list,
       totalAmount: Number(cartRes?.totalAmount) || 0,
-      totalQuantity: Number(cartRes?.totalQuantity) || 0
+      totalQuantity: Number(cartRes?.totalQuantity) || 0,
+      tablewareFee: Number(cartRes?.tablewareFee ?? cartRes?.tablewareAmount ?? 0) || 0
     };
     mergeCartIntoFoodList();
     console.log('购物车接口返回(data 层):', cartRes, '解析条数:', list.length);
@@ -315,10 +316,21 @@ const syncCartDataFromFoodList = () => {
   cartData.value = { ...cartData.value, items: nextItems, totalAmount, totalQuantity };
 };
 
+// 判断是否为餐具(cuisineId 或 id 为 -1),餐具不可修改数量
+const isTableware = (item) => {
+  if (!item) return false;
+  const id = item.id ?? item.cuisineId;
+  return Number(id) === -1;
+};
+
 // 更新菜品数量:菜品 id 一致则全部同步为同一数量,触发响应式;新增加入购物车时调接口;并同步 cartData
 // Update 接口返回 400 时不改页面数量和金额(会回滚本地状态)
 const updateFoodQuantity = (food, delta) => {
   if (!food) return;
+  if (isTableware(food)) {
+    uni.showToast({ title: '不允许更改餐具数量', icon: 'none' });
+    return;
+  }
   const id = food.id ?? food.cuisineId;
   const prevQty = food.quantity || 0;
   const nextQty = Math.max(0, prevQty + delta);
@@ -396,37 +408,63 @@ const handleDecrease = (item) => {
   if (food && (food.quantity || 0) > 0) updateFoodQuantity(food, -1);
 };
 
-// 优惠券点击(领取优惠券弹窗)
-const handleCouponClick = () => {
-  // 先关闭其他弹窗
-  if (cartModalOpen.value) {
-    cartModalOpen.value = false;
+// 拉取用户已领/可用优惠券(GET /dining/coupon/userOwnedByStore),列表在 SelectCouponModal 中展示,空时弹窗内显示「暂无可用优惠券」
+const fetchUserOwnedCoupons = async () => {
+  const storeId = uni.getStorageSync('currentStoreId') || '';
+  try {
+    const res = await GetUserOwnedCouponList({ storeId });
+    // 接口返回 { code, data: [...], msg, success },请求层可能只返回 data,故 res 可能为数组
+    const list = Array.isArray(res) ? res : (res?.data ?? res?.records ?? res?.list ?? []);
+    const normalized = (Array.isArray(list) ? list : []).map((item) => normalizeCouponItem(item)).filter(Boolean);
+    storeUsableCouponList.value = normalized;
+    return true;
+  } catch (err) {
+    console.error('获取用户优惠券失败:', err);
+    uni.showToast({ title: '获取优惠券失败', icon: 'none' });
+    return false;
   }
-  if (selectCouponModalOpen.value) {
-    selectCouponModalOpen.value = false;
+};
+
+// 打开选择优惠券弹窗。viewOnly=true 仅查看(左下角),false 可选择并默认选中第一张(购物车内)
+const openSelectCouponModal = async (viewOnly = false) => {
+  if (cartModalOpen.value) cartModalOpen.value = false;
+  if (couponModalOpen.value) couponModalOpen.value = false;
+  selectCouponViewOnly.value = viewOnly;
+  const ok = await fetchUserOwnedCoupons();
+  if (ok) {
+    if (!viewOnly) {
+      const list = storeUsableCouponList.value;
+      if (list && list.length > 0) {
+        const first = list[0];
+        const firstId = first.id != null && first.id !== '' ? String(first.id) : null;
+        selectedCouponId.value = firstId;
+        discountAmount.value = first.amount ?? 0;
+      } else {
+        selectedCouponId.value = null;
+        discountAmount.value = 0;
+      }
+    }
+    await nextTick();
+    selectCouponModalOpen.value = true;
   }
-  couponModalOpen.value = true;
 };
 
-// 选择优惠券点击(从购物车弹窗中点击)
+// 优惠券点击(左下角):仅查看,不可选择
+const handleCouponClick = () => {
+  openSelectCouponModal(true);
+};
+
+// 选择优惠券点击(购物车内):可选择,默认选中第一张
 const handleSelectCouponClick = () => {
-  // 先关闭其他弹窗
-  if (cartModalOpen.value) {
-    cartModalOpen.value = false;
-  }
-  if (couponModalOpen.value) {
-    couponModalOpen.value = false;
-  }
-  // 打开选择优惠券弹窗
-  selectCouponModalOpen.value = true;
+  openSelectCouponModal(false);
 };
 
-// 处理优惠券选择
+// 处理优惠券选择(单选:只保留当前选中的一张)
 const handleCouponSelect = ({ coupon, index, selectedId }) => {
-  selectedCouponId.value = selectedId;
+  selectedCouponId.value = selectedId != null && selectedId !== '' ? String(selectedId) : null;
   // 根据选中的优惠券更新优惠金额
-  if (selectedId) {
-    discountAmount.value = coupon.amount;
+  if (selectedCouponId.value) {
+    discountAmount.value = coupon.amount ?? 0;
   } else {
     discountAmount.value = 0;
   }
@@ -443,18 +481,6 @@ const handleSelectCouponClose = () => {
   selectCouponModalOpen.value = false;
 };
 
-// 优惠券领取
-const handleCouponReceive = ({ coupon, index }) => {
-  console.log('领取优惠券:', coupon);
-  // 更新优惠券状态
-  couponList.value[index].isReceived = true;
-  uni.showToast({
-    title: '领取成功',
-    icon: 'success'
-  });
-  // TODO: 调用接口领取优惠券
-};
-
 // 优惠券弹窗关闭
 const handleCouponClose = () => {
   couponModalOpen.value = false;
@@ -486,35 +512,49 @@ const handleCartClose = () => {
 
 // 清空购物车:调用 /store/order/cart/clear,入参 tableId,成功后清空本地并关闭弹窗
 const handleCartClear = () => {
-  if (!tableId.value) {
+  const doClear = () => {
+    pendingCartData = null;
     foodList.value = foodList.value.map((f) => ({ ...f, quantity: 0 }));
     cartData.value = { items: [], totalAmount: 0, totalQuantity: 0 };
     cartModalOpen.value = false;
     uni.showToast({ title: '已清空购物车', icon: 'success' });
+  };
+  if (!tableId.value) {
+    doClear();
     return;
   }
   PostOrderCartClear(tableId.value)
-    .then(() => {
-      foodList.value = foodList.value.map((f) => ({ ...f, quantity: 0 }));
-      cartData.value = { items: [], totalAmount: 0, totalQuantity: 0 };
-      cartModalOpen.value = false;
-      uni.showToast({ title: '已清空购物车', icon: 'success' });
-    })
+    .then(doClear)
     .catch((err) => {
       console.error('清空购物车失败:', err);
       uni.showToast({ title: '清空失败,请重试', icon: 'none' });
     });
 };
 
+// 从购物车项中计算餐具费:cuisineId/id 为 -1 的项为餐具,其金额合计为餐具费
+const getTablewareFeeFromCart = () => {
+  const items = cartData.value?.items ?? displayCartList.value ?? [];
+  return (Array.isArray(items) ? items : []).reduce((sum, it) => {
+    const id = it?.cuisineId ?? it?.id;
+    if (Number(id) !== -1) return sum;
+    const line = it?.subtotalAmount != null ? Number(it.subtotalAmount) : (Number(it?.quantity) || 0) * (Number(it?.unitPrice ?? it?.price) || 0);
+    return sum + line;
+  }, 0);
+};
+
 // 下单点击:先带购物车数据跳转确认订单页,创建订单在确认页点击「确认下单」时再调
 const handleOrderClick = () => {
+  const fromApi = Number(cartData.value?.tablewareFee) || 0;
+  const fromItems = getTablewareFeeFromCart();
+  const utensilFee = fromApi > 0 ? fromApi : fromItems;
   const cartPayload = {
     list: displayCartList.value,
     totalAmount: displayTotalAmount.value,
     totalQuantity: displayTotalQuantity.value,
     tableId: tableId.value,
     tableNumber: tableNumber.value,
-    diners: currentDiners.value
+    diners: currentDiners.value,
+    utensilFee: Number(utensilFee) || 0
   };
   uni.setStorageSync('placeOrderCart', JSON.stringify(cartPayload));
   const query = [];
@@ -531,8 +571,17 @@ onLoad(async (options) => {
   tableId.value = tableid;
   currentDiners.value = diners;
   if (tableid) uni.setStorageSync('currentTableId', tableid);
+  if (diners) uni.setStorageSync('currentDiners', diners);
   console.log('点餐页接收参数 - 桌号(tableid):', tableid, '就餐人数(diners):', diners);
 
+  // 更新餐具数量:quantity 传用餐人数,tableId 传桌号ID
+  if (tableid && diners !== '' && diners != null) {
+    PutOrderCartUpdateTableware({
+      quantity: Number(diners) || 1,
+      tableId: parseInt(tableid, 10) || 0
+    }).catch((err) => console.warn('更新餐具数量失败:', err));
+  }
+
   // 先拉取购物车并缓存,再加载菜品,保证合并时 pendingCartData 已就绪,避免不返显
   if (tableid) {
     await fetchAndMergeCart(tableid).catch((err) => console.error('获取购物车失败:', err));
@@ -552,7 +601,8 @@ onLoad(async (options) => {
             cartData.value = {
               items: Array.isArray(items) ? items : [],
               totalAmount: Number(payload?.totalAmount) || 0,
-              totalQuantity: Number(payload?.totalQuantity) || 0
+              totalQuantity: Number(payload?.totalQuantity) || 0,
+              tablewareFee: Number(payload?.tablewareFee ?? payload?.tablewareAmount ?? 0) || (cartData.value?.tablewareFee ?? 0)
             };
             pendingCartData = null;
             mergeCartIntoFoodList();
@@ -583,6 +633,9 @@ onLoad(async (options) => {
       const data = res?.data ?? res ?? {};
       tableNumber.value = data?.tableNumber ?? data?.tableNo ?? '';
       tableNumberFetched.value = true;
+      // 餐具费:点餐页接口可能返回,若购物车未带则用此处兜底
+      const fee = Number(data?.tablewareFee ?? data?.tablewareAmount ?? 0) || 0;
+      if (fee > 0) cartData.value = { ...cartData.value, tablewareFee: fee };
 
       // 成功后调接口获取菜品种类(storeId 取自点餐页接口返回,若无则需从别处获取)
       const storeId = res?.storeId ?? data?.storeId ?? '';
@@ -622,6 +675,14 @@ onLoad(async (options) => {
   }
 });
 
+// 回到点餐页时重新获取购物车(从接口拉取最新数据并合并到列表)
+onShow(() => {
+  const tableid = tableId.value || uni.getStorageSync('currentTableId') || '';
+  if (tableid) {
+    fetchAndMergeCart(tableid).catch((err) => console.warn('重新获取购物车失败:', err));
+  }
+});
+
 onUnload(() => {
   if (sseRequestTask && typeof sseRequestTask.abort === 'function') {
     sseRequestTask.abort();

+ 1 - 1
pages/orderInfo/index.vue

@@ -285,7 +285,7 @@ const handleCheckout = (order) => {
   if (diners !== '' && diners != null) q.push(`diners=${encodeURIComponent(String(diners))}`);
   if (totalAmount != null && totalAmount !== '') q.push(`totalAmount=${encodeURIComponent(String(totalAmount))}`);
   if (remark !== '') q.push(`remark=${encodeURIComponent(remark)}`);
-  go(q.length ? `/pages/placeOrder/index?${q.join('&')}` : '/pages/placeOrder/index');
+  go(q.length ? `/pages/checkout/index?${q.join('&')}` : '/pages/checkout/index');
 };
 
 onLoad((options) => {

+ 1 - 1
pages/orderInfo/orderDetail.vue

@@ -218,7 +218,7 @@ const handleConfirmPay = () => {
   if (diners !== '') q.push(`diners=${encodeURIComponent(String(diners))}`);
   if (totalAmount !== '' && totalAmount != null) q.push(`totalAmount=${encodeURIComponent(String(totalAmount))}`);
   if (remark !== '') q.push(`remark=${encodeURIComponent(remark)}`);
-  go(q.length ? `/pages/placeOrder/index?${q.join('&')}` : '/pages/placeOrder/index');
+  go(q.length ? `/pages/checkout/index?${q.join('&')}` : '/pages/checkout/index');
 };
 
 onLoad(async (e) => {

+ 227 - 98
pages/placeOrder/index.vue

@@ -28,8 +28,6 @@
           placeholder="请输入特殊需求,如少辣、不要香菜等(最多30字)"
           class="info-item-textarea"
           maxlength="30"
-          :disabled="fromCheckout"
-          :readonly="fromCheckout"
         ></textarea>
       </view>
     </view>
@@ -84,9 +82,20 @@
           <view class="info-item-label">餐具费</view>
           <view class="info-item-value">¥{{ formatPrice(orderInfo.utensilFee ?? 0) }}</view>
         </view>
-        <view class="info-item">
+        <view class="info-item info-item--coupon info-item--clickable" @click="onCouponRowClick">
           <view class="info-item-label">优惠券</view>
-          <view class="info-item-value">¥{{ formatPrice(orderInfo.discountAmount ?? 0) }}</view>
+          <view class="info-item-value coupon-value">
+            <text v-if="selectedCouponDisplay" class="coupon-amount">{{ selectedCouponDisplay }}</text>
+            <text v-else class="coupon-placeholder">请选择</text>
+            <text class="coupon-arrow">›</text>
+          </view>
+        </view>
+        <view v-if="(orderInfo.discountAmount ?? 0) > 0" class="info-item info-item--coupon info-item--clickable" @click="onCouponRowClick">
+          <view class="info-item-label">优惠金额</view>
+          <view class="info-item-value coupon-value">
+            <text class="coupon-amount">-¥{{ formatPrice(orderInfo.discountAmount) }}</text>
+            <text class="coupon-arrow">›</text>
+          </view>
         </view>
         <view class="price-line">
           <view class="price-line-label">应付金额</view>
@@ -95,43 +104,54 @@
       </view>
     </view>
 
-    <!-- 底部按钮:带金额(来自去结算)显示「确认支付」且备注不可改;不带金额显示「确认下单」且备注可改 -->
+    <!-- 底部按钮:确认下单(去结算请走 pages/checkout/index) -->
     <view class="bottom-button">
-      <view class="bottom-button-text" hover-class="hover-active" @click="handleConfirmPay" v-if="fromCheckout">确认支付 ¥{{ formatPrice(orderInfo.payAmount ?? orderInfo.totalAmount) }}</view>
-      <view class="bottom-button-text" hover-class="hover-active" @click="handleConfirmOrder" v-else>确认下单</view>
+      <view class="bottom-button-text" hover-class="hover-active" @click="handleConfirmOrder">确认下单</view>
     </view>
 
+    <!-- 选择优惠券弹窗(仅确认下单时可选) -->
+    <SelectCouponModal
+      v-model:open="couponModalOpen"
+      :coupon-list="couponList"
+      :selected-coupon-id="selectedCouponId"
+      :view-only="false"
+      @select="handleCouponSelect"
+      @close="couponModalOpen = false"
+    />
   </view>
 </template>
 
 <script setup>
-import { onLoad } from "@dcloudio/uni-app";
+import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
 import { ref } from "vue";
 import { go } from "@/utils/utils.js";
 import { getFileUrl } from "@/utils/file.js";
 import { useUserStore } from "@/store/user.js";
-import { PostOrderCreate, PostOrderPay } from "@/api/dining.js";
-
-const payType = ref('confirmOrder'); // confirmOrder 确认下单 confirmPay 确认支付
-/** 仅当从订单列表/结果页等「去结算」进入时为 true,底部显示确认支付 */
-const fromCheckout = ref(false);
-/** 待支付订单ID,确认支付时用于调起微信支付 */
-const payOrderId = ref('');
+import * as diningApi from "@/api/dining.js";
+import SelectCouponModal from "@/pages/orderFood/components/SelectCouponModal.vue";
 
 // 订单信息(从购物车带过来或 URL 参数)
 const orderInfo = ref({
   orderNo: '',
-  tableId: '',      // 桌号ID,接口入参用
-  tableNumber: '',  // 桌号展示(如 2),优先显示
+  tableId: '',
+  tableNumber: '',
   diners: '',
   contactPhone: '',
   remark: '',
   totalAmount: 0,
   utensilFee: 0,
   discountAmount: 0,
-  payAmount: 0
+  payAmount: 0,
+  couponId: null
 });
 
+// 优惠券选择
+const couponModalOpen = ref(false);
+const selectedCouponId = ref(null);
+const couponList = ref([]);
+/** 选中券的展示文案(如 "5.5折"、"10元"),用于价格明细展示 */
+const selectedCouponDisplay = ref('');
+
 // 菜品列表(从购物车带过来)
 const foodList = ref([]);
 
@@ -153,6 +173,135 @@ function formatPrice(price) {
   return Number.isNaN(num) ? '0.00' : num.toFixed(2);
 }
 
+// 规范化优惠券项(与 orderFood 的 normalizeCouponItem 一致,供 SelectCouponModal 使用)
+function normalizeCouponItem(item) {
+  if (!item || typeof item !== 'object') return null;
+  const raw = item;
+  const couponType = Number(raw.couponType) || 0;
+  const nominalValue = Number(raw.nominalValue ?? raw.amount ?? 0) || 0;
+  const discountRate = Number(raw.discountRate) || 0;
+  const minAmount = Number(raw.minimumSpendingAmount ?? raw.minAmount ?? 0) || 0;
+  let amountDisplay = nominalValue + '元';
+  if (couponType === 1) amountDisplay = nominalValue + '元';
+  else if (couponType === 2 && discountRate > 0) amountDisplay = Math.round(discountRate) + '折';
+  return {
+    id: raw.userCouponId ?? raw.id ?? raw.couponId ?? '',
+    amount: nominalValue,
+    minAmount,
+    name: raw.name ?? raw.title ?? raw.couponName ?? '',
+    expireDate: raw.expirationTime ?? raw.endGetDate ?? raw.expireDate ?? '',
+    couponType,
+    discountRate,
+    amountDisplay
+  };
+}
+
+// 点击优惠券行:打开选择弹窗
+const onCouponRowClick = () => {
+  openCouponModal();
+};
+
+// 打开优惠券弹窗并拉取用户可用券
+const openCouponModal = async () => {
+  const storeId = uni.getStorageSync('currentStoreId') || '';
+  couponModalOpen.value = false;
+  try {
+    const res = await diningApi.GetUserOwnedCouponList({ storeId });
+    const list = Array.isArray(res) ? res : (res?.data ?? res?.records ?? res?.list ?? []);
+    const normalized = (Array.isArray(list) ? list : []).map(normalizeCouponItem).filter(Boolean);
+    couponList.value = normalized;
+    if (normalized.length > 0 && !selectedCouponId.value) {
+      const first = normalized[0];
+      selectedCouponId.value = first.id != null && first.id !== '' ? String(first.id) : null;
+      orderInfo.value.couponId = selectedCouponId.value;
+      selectedCouponDisplay.value = first.amountDisplay || (first.amount ? first.amount + '元' : '');
+      const total = Number(orderInfo.value.totalAmount) || 0;
+      const couponType = Number(first.couponType) || 0;
+      if (couponType === 2 && first.discountRate != null && total > 0) {
+        const rate = Number(first.discountRate) || 0;
+        orderInfo.value.discountAmount = Math.round(total * (1 - rate / 10) * 100) / 100;
+      } else {
+        orderInfo.value.discountAmount = Number(first.amount) || 0;
+      }
+      updatePayAmount();
+    }
+    couponModalOpen.value = true;
+  } catch (err) {
+    console.error('获取优惠券失败:', err);
+    uni.showToast({ title: '获取优惠券失败', icon: 'none' });
+  }
+};
+
+// 选择优惠券后更新订单优惠与应付金额(折扣券按折扣率计算优惠金额)
+const handleCouponSelect = ({ coupon, selectedId }) => {
+  selectedCouponId.value = selectedId != null && selectedId !== '' ? String(selectedId) : null;
+  orderInfo.value.couponId = selectedCouponId.value;
+  if (!coupon) {
+    selectedCouponDisplay.value = '';
+    orderInfo.value.discountAmount = 0;
+  } else {
+    selectedCouponDisplay.value = coupon.amountDisplay || (coupon.amount ? coupon.amount + '元' : '');
+    const total = Number(orderInfo.value.totalAmount) || 0;
+    const couponType = Number(coupon.couponType) || 0;
+    if (couponType === 2 && coupon.discountRate != null && total > 0) {
+      // 折扣券:优惠金额 = 菜品总价 × (1 - 折数/10),如 5.5折 即 优惠 = 总价 × 0.45
+      const rate = Number(coupon.discountRate) || 0;
+      orderInfo.value.discountAmount = Math.round(total * (1 - rate / 10) * 100) / 100;
+    } else {
+      orderInfo.value.discountAmount = Number(coupon.amount) || 0;
+    }
+  }
+  updatePayAmount();
+  couponModalOpen.value = false;
+};
+
+// 根据总价、餐具费、优惠额计算应付金额
+const updatePayAmount = () => {
+  const total = Number(orderInfo.value.totalAmount) || 0;
+  const utensil = Number(orderInfo.value.utensilFee) || 0;
+  const discount = Number(orderInfo.value.discountAmount) || 0;
+  orderInfo.value.payAmount = Math.max(0, total + utensil - discount);
+};
+
+// 进入页面时调一次优惠券接口:有券则默认选第一张,无券则显示请选择
+const fetchCouponsOnEnter = async () => {
+  const storeId = uni.getStorageSync('currentStoreId') || '';
+  try {
+    const res = await diningApi.GetUserOwnedCouponList({ storeId });
+    const list = Array.isArray(res) ? res : (res?.data ?? res?.records ?? res?.list ?? []);
+    const normalized = (Array.isArray(list) ? list : []).map(normalizeCouponItem).filter(Boolean);
+    couponList.value = normalized;
+    if (normalized.length === 0) {
+      selectedCouponId.value = null;
+      selectedCouponDisplay.value = '';
+      orderInfo.value.couponId = null;
+      orderInfo.value.discountAmount = 0;
+      updatePayAmount();
+      return;
+    }
+    const first = normalized[0];
+    selectedCouponId.value = first.id != null && first.id !== '' ? String(first.id) : null;
+    orderInfo.value.couponId = selectedCouponId.value;
+    selectedCouponDisplay.value = first.amountDisplay || (first.amount ? first.amount + '元' : '');
+    const total = Number(orderInfo.value.totalAmount) || 0;
+    const couponType = Number(first.couponType) || 0;
+    if (couponType === 2 && first.discountRate != null && total > 0) {
+      const rate = Number(first.discountRate) || 0;
+      orderInfo.value.discountAmount = Math.round(total * (1 - rate / 10) * 100) / 100;
+    } else {
+      orderInfo.value.discountAmount = Number(first.amount) || 0;
+    }
+    updatePayAmount();
+  } catch (err) {
+    console.error('获取优惠券失败:', err);
+    selectedCouponDisplay.value = '';
+    orderInfo.value.discountAmount = 0;
+    orderInfo.value.couponId = null;
+    selectedCouponId.value = null;
+    updatePayAmount();
+  }
+};
+
 // 将存储的购物车项转为确认页展示格式(兼容 cuisineName/cuisineImage/unitPrice 等)
 function normalizeCartItem(item) {
   const image = item?.cuisineImage ?? item?.image ?? item?.imageUrl ?? '';
@@ -186,10 +335,16 @@ const handleConfirmOrder = async () => {
   const remark = (orderInfo.value.remark ?? '').trim().slice(0, 30);
   uni.showLoading({ title: '提交中...' });
   try {
-    const res = await PostOrderCreate({
+    const tablewareFee = Number(orderInfo.value.utensilFee) || 0;
+    const payAmount = Number((Number(orderInfo.value.payAmount) ?? 0).toFixed(2));
+    // couponId 传入接口返回的当前选择的优惠券 id(userOwnedByStore 返回的 userCouponId/id/couponId)
+    const res = await diningApi.PostOrderCreate({
       tableId,
       contactPhone,
-      couponId: null,
+      couponId: (selectedCouponId.value ?? orderInfo.value.couponId) || null,
+      discountAmount: orderInfo.value.discountAmount ?? 0,
+      tablewareFee,
+      payAmount,
       dinerCount: dinerCount || undefined,
       immediatePay: 0,
       remark: remark || undefined
@@ -208,6 +363,11 @@ const handleConfirmOrder = async () => {
       totalAmount: total,
       remark: (orderInfo.value.remark ?? '').trim().slice(0, 30)
     }));
+    // 确认下单成功后即将跳转,此时页面不会触发 onUnload,需主动调用解锁
+    const unlockFn = diningApi.PostOrderUnlock || diningApi.postOrderUnlock;
+    if (typeof unlockFn === 'function' && tableId) {
+      unlockFn({ tableId }).catch((err) => console.warn('解锁订单失败:', err));
+    }
     if (orderId != null) {
       go(`/pages/result/index?id=${encodeURIComponent(orderId)}`);
     } else {
@@ -219,87 +379,10 @@ const handleConfirmOrder = async () => {
   }
 };
 
-// 确认支付:调起微信支付
-const handleConfirmPay = async () => {
-  const orderId = payOrderId.value || '';
-  if (!orderId) {
-    uni.showToast({ title: '缺少订单ID', icon: 'none' });
-    return;
-  }
-  const userStore = useUserStore();
-  const openid = userStore.getOpenId || '';
-  if (!openid) {
-    uni.showToast({ title: '请先登录', icon: 'none' });
-    return;
-  }
-  const priceAmount = Number(orderInfo.value.payAmount ?? orderInfo.value.totalAmount ?? 0) || 0;
-  if (priceAmount <= 0) {
-    uni.showToast({ title: '订单金额异常', icon: 'none' });
-    return;
-  }
-  const orderNo = orderInfo.value.orderNo || '';
-  if (!orderNo) {
-    uni.showToast({ title: '缺少订单号', icon: 'none' });
-    return;
-  }
-  // 后端要求金额乘 100 传入(分)
-  const price = Math.round(priceAmount * 100);
-  uni.showLoading({ title: '拉起支付...' });
-  try {
-    const res = await PostOrderPay({
-      orderNo,
-      payer: openid,
-      price,
-      subject: '订单支付'
-    });
-    uni.hideLoading();
-   console.log(res)
-    uni.requestPayment({
-      provider: 'wxpay',
-      timeStamp:res.timestamp,
-      nonceStr:res.nonce,
-      package:res.prepayId,
-      signType:res.signType,
-      paySign:res.sign,
-      success: () => {
-        uni.showToast({ title: '支付成功', icon: 'success' });
-        const payType = 'wechatPayMininProgram';
-        const transactionId = orderNo;
-        const q = [`id=${encodeURIComponent(orderId)}`, `payType=${encodeURIComponent(payType)}`, `transactionId=${encodeURIComponent(transactionId)}`];
-        setTimeout(() => go(`/pages/paymentSuccess/index?${q.join('&')}`), 1500);
-      },
-      fail: (err) => {
-        const msg = err?.errMsg ?? err?.message ?? '支付失败';
-        if (String(msg).includes('cancel')) {
-          uni.showToast({ title: '已取消支付', icon: 'none' });
-        } else {
-          uni.showToast({ title: msg || '支付失败', icon: 'none' });
-        }
-      }
-    });
-  } catch (e) {
-    uni.hideLoading();
-    uni.showToast({ title: e?.message ?? '获取支付参数失败', icon: 'none' });
-  }
-};
-
 onLoad((options) => {
-  if (options?.payType) payType.value = options.payType;
   const userStore = useUserStore();
   const contactPhone = userStore.getUserInfo?.phone ?? userStore.getUserInfo?.contactPhone ?? userStore.getUserInfo?.mobile ?? '';
   orderInfo.value.contactPhone = contactPhone;
-  if (options?.orderId != null && options?.orderId !== '') payOrderId.value = options.orderId;
-  if (options?.orderNo != null && options?.orderNo !== '') orderInfo.value.orderNo = options.orderNo;
-  if (options?.tableId) orderInfo.value.tableId = options.tableId;
-  if (options?.tableNumber != null && options?.tableNumber !== '') orderInfo.value.tableNumber = options.tableNumber;
-  if (options?.diners) orderInfo.value.diners = options.diners;
-  if (options?.remark != null && options?.remark !== '') orderInfo.value.remark = decodeURIComponent(options.remark);
-  if (options?.totalAmount != null && options?.totalAmount !== '') {
-    fromCheckout.value = true;
-    const total = Number(options.totalAmount) || 0;
-    orderInfo.value.totalAmount = total;
-    orderInfo.value.payAmount = total;
-  }
   const raw = uni.getStorageSync('placeOrderCart');
   if (raw) {
     try {
@@ -312,12 +395,38 @@ onLoad((options) => {
       if (data.remark != null) orderInfo.value.remark = data.remark;
       const total = Number(data.totalAmount) || 0;
       orderInfo.value.totalAmount = total;
-      orderInfo.value.payAmount = total;
+      const fee = data.utensilFee ?? data.tablewareFee;
+      if (fee != null && fee !== '') orderInfo.value.utensilFee = Number(fee) || 0;
+      if (data.discountAmount != null) orderInfo.value.discountAmount = data.discountAmount;
+      if (data.couponId != null) selectedCouponId.value = data.couponId;
+      updatePayAmount();
     } catch (e) {
       console.error('解析购物车数据失败:', e);
     }
   }
   if (orderInfo.value.tableId) uni.setStorageSync('currentTableId', String(orderInfo.value.tableId));
+  fetchCouponsOnEnter();
+});
+
+
+// 每次进入页面(含从其他页返回)都调用锁定订单接口
+onShow(() => {
+  const tableId = orderInfo.value?.tableId;
+  if (tableId) {
+    (diningApi.PostOrderLock || diningApi.postOrderLock)({ tableId }).catch((err) => {
+      console.warn('锁定订单失败:', err);
+    });
+  }
+});
+
+// 离开页面时调用解锁订单接口
+onUnload(() => {
+  const tableId = orderInfo.value?.tableId;
+  if (tableId && typeof (diningApi.PostOrderUnlock || diningApi.postOrderUnlock) === 'function') {
+    (diningApi.PostOrderUnlock || diningApi.postOrderUnlock)({ tableId }).catch((err) => {
+      console.warn('解锁订单失败:', err);
+    });
+  }
 });
 </script>
 
@@ -372,6 +481,26 @@ onLoad((options) => {
     .info-item-value {
       color: #151515;
     }
+
+    &--clickable {
+      cursor: pointer;
+    }
+
+    &--coupon .coupon-value {
+      display: flex;
+      align-items: center;
+      gap: 8rpx;
+    }
+    .coupon-amount {
+      color: #E61F19;
+    }
+    .coupon-placeholder {
+      color: #999999;
+    }
+    .coupon-arrow {
+      color: #999999;
+      font-size: 32rpx;
+    }
   }
 
   .info-item-textarea {

+ 4 - 3
pages/result/index.vue

@@ -19,14 +19,15 @@ import { getFileUrl } from "@/utils/file.js";
 
 const resultOrderId = ref(''); // 本页 URL 上的订单ID(下单成功跳转时带的 id)
 
-// 去加餐:从缓存取桌号、人数并带上,点餐页依赖 tableid、diners 拉取列表和购物车
+// 去加餐:销毁当前页再跳转点餐页(不用 go,用 redirectTo)
 const handleGoAddFood = () => {
   const tableid = uni.getStorageSync('currentTableId') ?? '';
   const diners = uni.getStorageSync('currentDiners') ?? '';
   const q = [];
   if (tableid !== '' && tableid != null) q.push(`tableid=${encodeURIComponent(String(tableid))}`);
   if (diners !== '' && diners != null) q.push(`diners=${encodeURIComponent(String(diners))}`);
-  go(q.length ? `/pages/orderFood/index?${q.join('&')}` : '/pages/orderFood/index');
+  const url = q.length ? `/pages/orderFood/index?${q.join('&')}` : '/pages/orderFood/index';
+  uni.redirectTo({ url });
 };
 
 // 去结算:带上桌号、人数、金额、备注,确认订单页才显示金额并走「确认支付」
@@ -62,7 +63,7 @@ const handleGoSettle = () => {
   if (diners !== '') q.push(`diners=${encodeURIComponent(diners)}`);
   if (totalAmount !== '') q.push(`totalAmount=${encodeURIComponent(totalAmount)}`);
   if (remark !== '') q.push(`remark=${encodeURIComponent(remark)}`);
-  go(q.length ? `/pages/placeOrder/index?${q.join('&')}` : '/pages/placeOrder/index');
+  go(q.length ? `/pages/checkout/index?${q.join('&')}` : '/pages/checkout/index');
 };
 
 onLoad((options) => {

+ 2 - 12
utils/request.js

@@ -104,12 +104,7 @@ export class Request {
 							uni.showModal({
 								title: data.msg || '系统异常,请联系管理员',
 								confirmText: '确定',
-								success: (res) => {
-									if (res.confirm) {
-									} else if (res.cancel) {
-										uni.redirectTo({ url: '/pages/index/index' });
-									}
-								}
+								success: () => {}
 							});
 							reject();
 							break;
@@ -118,12 +113,7 @@ export class Request {
 							uni.showModal({
 								title: data.msg || '系统异常,请联系管理员',
 								confirmText: '确定',
-								success: (res) => {
-									if (res.confirm) {
-									} else if (res.cancel) {
-										uni.redirectTo({ url: '/pages/index/index' });
-									}
-								}
+								success: () => {}
 							});
 							reject();
 							break;

+ 8 - 6
utils/utils.js

@@ -29,6 +29,9 @@ export const getDate = (date, AddDayCount = 0) => {
 	};
 };
 
+// tabBar 页面路径(与 pages.json 一致),跳转这些页面须用 switchTab,不能用 redirectTo/navigateTo
+const TABBAR_PATHS = ['/pages/index/index', '/pages/numberOfDiners/index', '/pages/personal/index'];
+
 // 通用跳转方法
 export function go(url, mode = 'navigateTo') {
 	if (url == -1) {
@@ -37,14 +40,13 @@ export function go(url, mode = 'navigateTo') {
 		if (beforePage) {
 			uni.navigateBack();
 		} else {
-			uni.reLaunch({
-				url: '/pages/index/index'
-			});
+			uni.switchTab({ url: '/pages/index/index' });
 		}
 	} else {
-		uni[mode]({
-			url
-		});
+		const path = typeof url === 'string' ? url.split('?')[0] : '';
+		const isTabBar = TABBAR_PATHS.some((p) => path === p || path.endsWith(p));
+		const actualMode = isTabBar ? 'switchTab' : mode;
+		uni[actualMode]({ url });
 	}
 }