Преглед на файлове

增加就餐信息和小程序整体修改

sunshibo преди 2 седмици
родител
ревизия
2a2dfaee9b

+ 34 - 12
api/dining.js

@@ -45,13 +45,42 @@ export const uploadFileToSever = uploadFileToServerImpl; // 兼容拼写错误
 // 点餐页数据(入参 dinerCount 就餐人数、tableId 桌号)
 export const DiningOrderFood = (params) => api.get({ url: '/store/dining/page-info', params });
 
-// 查询桌位是否已选就餐人数(GET /dining/table-dining-status?tableId=xxx)
-export const GetTableDiningStatus = (tableId) =>
+// 服务费估算(GET /store/dining/service-fee/estimate,query:storeId、tableId、dinerCount、goodsSubtotal 菜品成交价小计元)
+export const GetServiceFeeEstimate = (params) => {
+  const q = {};
+  if (params?.storeId != null && params.storeId !== '') q.storeId = String(params.storeId);
+  if (params?.tableId != null && params.tableId !== '') q.tableId = String(params.tableId);
+  if (params?.dinerCount != null && params.dinerCount !== '') q.dinerCount = params.dinerCount;
+  if (params?.goodsSubtotal != null && params.goodsSubtotal !== '') q.goodsSubtotal = params.goodsSubtotal;
+  return api.get({ url: '/store/dining/service-fee/estimate', params: q, loading: false });
+};
+
+// 桌台预约记录详情(GET /store/dining/reservation/detail-by-store-table-record,query:storeId、userReservationTableId 用户预约餐桌记录ID)
+export const GetReservationDetailByStoreTableRecord = (params) => {
+  const q = {};
+  if (params?.storeId != null && params?.storeId !== '') q.storeId = String(params.storeId);
+  if (params?.userReservationTableId != null && params?.userReservationTableId !== '') {
+    q.userReservationTableId = String(params.userReservationTableId);
+  }
+  return api.get({
+    url: '/store/dining/reservation/detail-by-store-table-record',
+    params: q,
+    loading: false
+  });
+};
+
+// 查询桌位是否已选就餐人数(GET /store/dining/table-dining-status?tableId=xxx,返回含 tableNumber 等)
+export const GetTableDiningStatus = (tableId, extra = {}) =>
   api.get({
     url: '/store/dining/table-dining-status',
-    params: { tableId: tableId != null && tableId !== '' ? String(tableId) : '' }
+    params: { tableId: tableId != null && tableId !== '' ? String(tableId) : '' },
+    ...extra
   });
 
+// 到店就餐信息提交(POST /store/dining/walk-in/reservation,body:DiningWalkInReservationDTO;startTime/endTime 为 yyyy-MM-dd HH:mm)
+export const PostDiningWalkInReservation = (dto) =>
+  api.post({ url: '/store/dining/walk-in/reservation', params: dto });
+
 // 门店详情(GET /store/info/detail/{storeId},入参 storeId)
 export const GetStoreDetail = (storeId) =>
   api.get({ url: `/store/info/detail/${encodeURIComponent(storeId)}` });
@@ -125,10 +154,6 @@ 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) : '';
@@ -170,7 +195,7 @@ function postOrderSettlementUnlockImpl(params) {
 }
 export const PostOrderSettlementUnlock = postOrderSettlementUnlockImpl;
 
-// 创建订单(POST /store/order/create,入参 dto:tableId、contactPhone、totalAmount 菜品总价、couponId、discountAmount 优惠金额、tablewareFee 餐具费、payAmount 实付金额、dinerCount、immediatePay、remark
+// 创建订单(POST /store/order/create,入参 dto:tableId、contactPhone、totalAmount 菜品金额、serviceFee 服务费、tablewareFee(可传 0)、payAmount、couponId、discountAmount、dinerCount、immediatePay、remark 等
 export const PostOrderCreate = (dto) =>
   api.post({ url: '/store/order/create', params: dto });
 
@@ -186,7 +211,7 @@ export const GetMyOrders = (params) =>
 export const GetOrderInfo = (orderId) =>
   api.get({ url: `/store/order/info/${encodeURIComponent(orderId)}` });
 
-// 订单支付(GET /payment/prePay,入参 orderNo、payType、payer、price、subject、storeId、couponId、payerId、tablewareFee 餐具费、discountAmount 优惠金额、payAmount 支付金额;返回微信支付调起参数)
+// 订单支付(GET /payment/prePay,入参 orderNo、payType、payer、price、subject、storeId、couponId、payerId、discountAmount 优惠金额、payAmount 支付金额;返回微信支付调起参数)
 export const PostOrderPay = (params) => {
   const query = {
     orderNo: params.orderNo,
@@ -202,9 +227,6 @@ export const PostOrderPay = (params) => {
   if (params.payerId != null && params.payerId !== '') {
     query.payerId = params.payerId;
   }
-  if (params.tablewareFee != null && params.tablewareFee !== '') {
-    query.tablewareFee = params.tablewareFee;
-  }
   if (params.discountAmount != null && params.discountAmount !== '') {
     query.discountAmount = params.discountAmount;
   }

+ 6 - 0
pages.json

@@ -30,6 +30,12 @@
 			}
 		},
 		{
+			"path": "pages/diningInfo/index",
+			"style": {
+				"navigationBarTitleText": "就餐信息"
+			}
+		},
+		{
 			"path": "pages/webview/index",
 			"style": {
 				"navigationBarTitleText": "",

+ 271 - 28
pages/checkout/index.vue

@@ -11,9 +11,27 @@
           <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 info-item--diners">
           <view class="info-item-label">用餐人数</view>
-          <view class="info-item-value">{{ orderInfo.diners || '—' }}人</view>
+          <view class="diner-stepper">
+            <view
+              class="diner-stepper__btn"
+              :class="{ 'diner-stepper__btn--disabled': dinerCount <= 1 }"
+              hover-class="diner-stepper__btn--active"
+              @tap.stop="adjustDiners(-1)"
+            >
+              <view class="diner-stepper__icon diner-stepper__icon--minus" />
+            </view>
+            <text class="diner-stepper__value">{{ dinerCount }}</text>
+            <view
+              class="diner-stepper__btn"
+              :class="{ 'diner-stepper__btn--disabled': dinerCount >= MAX_DINERS }"
+              hover-class="diner-stepper__btn--active"
+              @tap.stop="adjustDiners(1)"
+            >
+              <view class="diner-stepper__icon diner-stepper__icon--plus" />
+            </view>
+          </view>
         </view>
         <view class="info-item">
           <view class="info-item-label">联系电话</view>
@@ -62,11 +80,11 @@
       <view class="card-content">
         <view class="info-item">
           <view class="info-item-label">菜品总价</view>
-          <view class="info-item-value">¥{{ formatPrice(dishTotal) }}</view>
+          <view class="info-item-value">¥{{ formatPrice(foodSubtotalForDisplay) }}</view>
         </view>
         <view class="info-item">
-          <view class="info-item-label">餐具费</view>
-          <view class="info-item-value">¥{{ formatPrice(orderInfo.utensilFee ?? 0) }}</view>
+          <view class="info-item-label">服务费</view>
+          <view class="info-item-value">¥{{ formatPrice(orderInfo.serviceFee ?? 0) }}</view>
         </view>
         <view class="info-item info-item--coupon info-item--clickable" @click="onCouponRowClick">
           <view class="info-item-label">优惠券</view>
@@ -117,6 +135,10 @@ import * as diningApi from '@/api/dining.js';
 import SelectCouponModal from '@/pages/orderFood/components/SelectCouponModal.vue';
 
 const orderId = ref('');
+/** 用餐人数上限(与选人数页「查看更多」一致) */
+const MAX_DINERS = 16;
+const MIN_DINERS = 1;
+
 const orderInfo = ref({
   orderNo: '',
   storeId: '',
@@ -127,30 +149,67 @@ const orderInfo = ref({
   remark: '',
   totalAmount: 0,
   dishTotal: null,
-  utensilFee: 0,
   couponId: null,
   couponName: '',
   couponType: null,   // 1 满减 2 折扣
   discountRate: null, // 折扣券力度,如 5.5 表示 5.5折
   nominalValue: null, // 满减券面额
   discountAmount: 0,
-  payAmount: 0
+  payAmount: 0,
+  serviceFee: 0
 });
 
 const foodList = ref([]);
 
+/** 服务费估算接口返回的 feeType;为 1 表示按人数计费,修改人数时需重新估算 */
+const estimateFeeType = ref(null);
+
 // 优惠券选择(结算页)
 const couponModalOpen = ref(false);
 const couponList = ref([]);
 const selectedCouponId = ref(null);
 
-// 菜品清单展示:包含所有项(含餐具 id/cuisineId === -1)
-const displayFoodList = computed(() => foodList.value ?? []);
+// 菜品清单展示(排除特殊占位 id=-1)
+const displayFoodList = computed(() =>
+  (foodList.value ?? []).filter((it) => Number(it?.id ?? it?.cuisineId) !== -1)
+);
+
+function parseDinerCount(val) {
+  const n = Number(val);
+  if (!Number.isFinite(n) || n < MIN_DINERS) return MIN_DINERS;
+  return Math.min(MAX_DINERS, Math.floor(n));
+}
+
+/** 当前用餐人数(数字,用于步进器展示) */
+const dinerCount = computed(() => parseDinerCount(orderInfo.value.diners));
+
+function adjustDiners(delta) {
+  const cur = parseDinerCount(orderInfo.value.diners);
+  const next = cur + delta;
+  if (next < MIN_DINERS || next > MAX_DINERS) return;
+  orderInfo.value.diners = String(next);
+  uni.setStorageSync('currentDiners', String(next));
+  // 仅当接口判定为「按人数计费」(feeType === 1) 时,改人数才重新估算服务费
+  if (Number(estimateFeeType.value) === 1) {
+    fetchServiceFeeEstimate().catch((err) => console.warn('按人数重算服务费失败:', err));
+  }
+}
 
-// 菜品总价:绑定 totalAmount
-const dishTotal = computed(() => {
+// 展示用菜品小计:有明细时与行成交价一致;否则用接口 dishTotal 或 totalAmount − 服务费
+const foodSubtotalForDisplay = computed(() => {
+  const list = displayFoodList.value;
+  if (list.length > 0) {
+    const sum = list.reduce((s, it) => s + (Number(it.lineSubtotal) || 0), 0);
+    return Math.max(0, Math.round(sum * 100) / 100);
+  }
+  const dt = orderInfo.value.dishTotal;
+  if (dt != null && dt !== '') {
+    const d = Number(dt);
+    if (!Number.isNaN(d)) return Math.max(0, d);
+  }
   const total = Number(orderInfo.value.totalAmount) || 0;
-  return Math.max(0, total);
+  const fee = Number(orderInfo.value.serviceFee) || 0;
+  return Math.max(0, total - fee);
 });
 
 // 优惠券展示:满减券显示 nominalValue+元,折扣券显示 discountRate+折,否则显示 couponName 或 已使用优惠券
@@ -234,11 +293,13 @@ const handleCouponSelect = ({ coupon, selectedId }) => {
     orderInfo.value.couponType = Number(coupon.couponType) ?? null;
     orderInfo.value.discountRate = coupon.discountRate != null ? coupon.discountRate : null;
     orderInfo.value.nominalValue = coupon.amount != null ? coupon.amount : null;
-    const total = Number(orderInfo.value.totalAmount) || 0;
+    const food = foodSubtotalForDisplay.value;
+    const fee = Number(orderInfo.value.serviceFee) || 0;
+    const baseForDiscount = food + fee;
     const couponType = Number(coupon.couponType) || 0;
-    if (couponType === 2 && coupon.discountRate != null && total > 0) {
+    if (couponType === 2 && coupon.discountRate != null && baseForDiscount > 0) {
       const rate = Number(coupon.discountRate) || 0;
-      orderInfo.value.discountAmount = Math.round(total * (1 - rate / 10) * 100) / 100;
+      orderInfo.value.discountAmount = Math.round(baseForDiscount * (1 - rate / 10) * 100) / 100;
     } else {
       orderInfo.value.discountAmount = Number(coupon.amount) || 0;
     }
@@ -247,14 +308,80 @@ const handleCouponSelect = ({ coupon, selectedId }) => {
   couponModalOpen.value = false;
 };
 
-// 根据菜品总价、餐具费、优惠额计算应付金额
+// 应付金额 = 菜品总价 + 服务费 − 优惠金额
 const updateCheckoutPayAmount = () => {
-  const total = Number(orderInfo.value.totalAmount) || 0;
-  const utensil = Number(orderInfo.value.utensilFee) || 0;
+  const food = Number(foodSubtotalForDisplay.value) || 0;
+  const fee = Number(orderInfo.value.serviceFee) || 0;
   const discount = Number(orderInfo.value.discountAmount) || 0;
-  orderInfo.value.payAmount = Math.max(0, total + utensil - discount);
+  orderInfo.value.payAmount = Math.max(0, Math.round((food + fee - discount) * 100) / 100);
 };
 
+/** 服务费变化后:折扣券(按菜品+服务费为基数)需重算优惠金额 */
+function recalcDiscountAfterServiceFeeChange() {
+  const ct = Number(orderInfo.value.couponType) || 0;
+  if (ct !== 2 || orderInfo.value.discountRate == null || !orderInfo.value.couponId) return;
+  const food = Number(foodSubtotalForDisplay.value) || 0;
+  const fee = Number(orderInfo.value.serviceFee) || 0;
+  const base = food + fee;
+  const rate = Number(orderInfo.value.discountRate) || 0;
+  if (base <= 0) return;
+  orderInfo.value.discountAmount = Math.round(base * (1 - rate / 10) * 100) / 100;
+}
+
+/**
+ * 从估算接口 data 解析 feeType:后端可能放在顶层,也可能在 items[].feeType(如按人数规则)
+ * 任一为 1 则视为按人数计费,修改人数需重新请求 estimate
+ */
+function parseEstimateFeeType(res) {
+  if (res == null || typeof res !== 'object') return null;
+  const top = res.feeType != null && res.feeType !== '' ? Number(res.feeType) : NaN;
+  if (top === 1) return 1;
+  const items = Array.isArray(res.items) ? res.items : [];
+  for (const it of items) {
+    const ft = it?.feeType != null && it.feeType !== '' ? Number(it.feeType) : NaN;
+    if (ft === 1) return 1;
+  }
+  if (!Number.isNaN(top)) return top;
+  for (const it of items) {
+    const ft = it?.feeType != null && it.feeType !== '' ? Number(it.feeType) : NaN;
+    if (!Number.isNaN(ft)) return ft;
+  }
+  return null;
+}
+
+/**
+ * 调用 /store/dining/service-fee/estimate,更新服务费与 estimateFeeType
+ */
+async function fetchServiceFeeEstimate() {
+  const storeId = orderInfo.value.storeId || uni.getStorageSync('currentStoreId') || '';
+  const tableId = getTableIdForServiceFeeEstimate();
+  if (!storeId || !tableId) return;
+  const dinerCount = parseDinerCount(orderInfo.value.diners);
+  const goodsSubtotal = Math.max(0, Math.round((Number(foodSubtotalForDisplay.value) || 0) * 100) / 100);
+  try {
+    const res = await diningApi.GetServiceFeeEstimate({
+      storeId: String(storeId),
+      tableId,
+      dinerCount,
+      goodsSubtotal
+    });
+    estimateFeeType.value = parseEstimateFeeType(res);
+    const fee =
+      Number(
+        res?.serviceFee ??
+          res?.estimatedServiceFee ??
+          res?.fee ??
+          res?.amount ??
+          (typeof res === 'number' ? res : 0)
+      ) || 0;
+    orderInfo.value.serviceFee = fee;
+    recalcDiscountAfterServiceFeeChange();
+    updateCheckoutPayAmount();
+  } catch (e) {
+    console.warn('结算页服务费估算失败:', e);
+  }
+}
+
 function formatPrice(price) {
   const num = Number(price);
   return Number.isNaN(num) ? '0.00' : num.toFixed(2);
@@ -277,7 +404,6 @@ function firstImage(val) {
 }
 
 function getItemImage(item) {
-  if (Number(item?.id ?? item?.cuisineId) === -1) return '/static/utensilFee.png';
   const raw = item?.image ?? item?.cuisineImage ?? item?.imageUrl ?? item?.images ?? item?.pic ?? item?.cover ?? '';
   const url = firstImage(raw) || (typeof raw === 'string' ? raw.split(/[,,]/)[0]?.trim() : '');
   if (url && typeof url === 'string' && (url.startsWith('http') || url.startsWith('//'))) return url;
@@ -307,16 +433,38 @@ function normalizeOrderItem(item) {
   const rawTags = item?.tags ?? item?.tagList ?? item?.tagNames ?? item?.labels ?? item?.tag;
   const rawImg = item?.images ?? item?.image ?? item?.cuisineImage ?? item?.imageUrl ?? item?.pic ?? item?.cover ?? '';
   const imageUrl = firstImage(rawImg) || (typeof rawImg === 'string' ? rawImg.split(/[,,]/)[0]?.trim() : '') || 'img/icon/shop.png';
+  const qty = Number(item?.quantity ?? 1) || 1;
+  /** 行成交价小计(元):与购物车/服务费估算 goodsSubtotal 一致 */
+  let lineSubtotal = 0;
+  if (item?.subtotalAmount != null && item.subtotalAmount !== '') {
+    lineSubtotal = Number(item.subtotalAmount) || 0;
+  } else if (item?.totalPrice != null && item.totalPrice !== '') {
+    lineSubtotal = Number(item.totalPrice) || 0;
+  } else {
+    const unit = Number(item?.unitPrice ?? item?.price ?? 0) || 0;
+    lineSubtotal = unit * qty;
+  }
+  const unitForDisplay =
+    Number(item?.unitPrice ?? item?.price ?? 0) ||
+    (qty > 0 ? lineSubtotal / qty : 0);
   return {
     id: item?.id ?? item?.cuisineId,
     name: item?.cuisineName ?? item?.name ?? '',
-    price: item?.totalPrice ?? item?.unitPrice ?? item?.price ?? 0,
+    price: unitForDisplay,
+    lineSubtotal,
     image: imageUrl,
-    quantity: item?.quantity ?? 1,
+    quantity: qty,
     tags: normalizeTags(rawTags)
   };
 }
 
+/** 桌台主键:store_table.id,不可用桌号展示文案代替 */
+function getTableIdForServiceFeeEstimate() {
+  const t = orderInfo.value.tableId;
+  if (t != null && String(t).trim() !== '') return String(t).trim();
+  return String(uni.getStorageSync('currentTableId') || '').trim();
+}
+
 const fetchOrderDetail = async () => {
   const id = orderId.value || '';
   if (!id) return;
@@ -325,15 +473,22 @@ const fetchOrderDetail = async () => {
     const raw = res?.data ?? res ?? {};
     orderInfo.value.orderNo = raw?.orderNo ?? raw?.orderId ?? '';
     orderInfo.value.storeId = raw?.storeId ?? raw?.store_id ?? '';
-    orderInfo.value.tableId = raw?.tableId ?? raw?.tableNumber ?? '';
+    // tableId 须为门店桌台表主键(store_table.id),勿用 tableNumber(展示用桌号)冒充
+    orderInfo.value.tableId =
+      raw?.tableId ??
+      raw?.storeTableId ??
+      raw?.store_table_id ??
+      raw?.diningTableId ??
+      '';
     orderInfo.value.tableNumber = raw?.tableNumber ?? raw?.tableNo ?? '';
-    orderInfo.value.diners = raw?.dinerCount ?? '';
+    const dc = raw?.dinerCount ?? raw?.diners ?? '';
+    orderInfo.value.diners =
+      dc !== '' && dc != null ? String(parseDinerCount(dc)) : String(MIN_DINERS);
     orderInfo.value.contactPhone = raw?.contactPhone ?? raw?.phone ?? '';
     orderInfo.value.remark = raw?.remark ?? '';
     const total = Number(raw?.totalAmount ?? raw?.orderAmount ?? raw?.foodAmount ?? 0) || 0;
     orderInfo.value.totalAmount = total;
     orderInfo.value.dishTotal = raw?.dishTotal != null ? Number(raw.dishTotal) : null;
-    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;
@@ -342,9 +497,13 @@ const fetchOrderDetail = async () => {
     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;
+    orderInfo.value.serviceFee =
+      Number(raw?.serviceFee ?? raw?.serviceCharge ?? raw?.tablewareFee ?? 0) || 0;
     const list = raw?.orderItemList ?? raw?.orderItems ?? raw?.items ?? raw?.detailList ?? [];
     foodList.value = (Array.isArray(list) ? list : []).map(normalizeOrderItem);
     selectedCouponId.value = orderInfo.value.couponId != null && orderInfo.value.couponId !== '' ? String(orderInfo.value.couponId) : null;
+    updateCheckoutPayAmount();
+    await fetchServiceFeeEstimate();
   } catch (err) {
     console.error('获取订单详情失败:', err);
     uni.showToast({ title: '加载失败', icon: 'none' });
@@ -385,7 +544,6 @@ const handleConfirmPay = async () => {
         : selectedCouponId.value != null && selectedCouponId.value !== ''
           ? String(selectedCouponId.value)
           : '';
-    const tablewareFeeVal = Number(orderInfo.value.utensilFee) || 0;
     const discountAmountVal = Number(orderInfo.value.discountAmount) || 0;
     const res = await diningApi.PostOrderPay({
       orderNo,
@@ -395,7 +553,6 @@ const handleConfirmPay = async () => {
       storeId: storeId || undefined,
       couponId: couponIdVal || undefined,
       payerId: payerId ? String(payerId) : undefined,
-      tablewareFee: tablewareFeeVal,
       discountAmount: discountAmountVal,
       payAmount: payAmountVal
     });
@@ -446,7 +603,10 @@ onLoad(async (options) => {
   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?.diners != null && options?.diners !== '') {
+    orderInfo.value.diners = String(parseDinerCount(options.diners));
+    uni.setStorageSync('currentDiners', orderInfo.value.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;
@@ -546,6 +706,89 @@ onUnload(() => {
     &--clickable {
       cursor: pointer;
     }
+
+    &--diners {
+      align-items: center;
+    }
+  }
+
+  /* 用餐人数:胶囊步进器(与设计稿一致) */
+  .diner-stepper {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-between;
+    background: #f2f3f5;
+    border-radius: 999rpx;
+    padding: 3rpx 6rpx;
+    min-width: 196rpx;
+    box-sizing: border-box;
+  }
+
+  .diner-stepper__btn {
+    width: 44rpx;
+    height: 44rpx;
+    border-radius: 50%;
+    background: #ffffff;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+    box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.06);
+  }
+
+  .diner-stepper__btn--active:active {
+    opacity: 0.88;
+  }
+
+  .diner-stepper__btn--disabled {
+    opacity: 0.35;
+    pointer-events: none;
+  }
+
+  .diner-stepper__value {
+    flex: 1;
+    text-align: center;
+    font-size: 26rpx;
+    font-weight: 500;
+    color: #151515;
+    line-height: 44rpx;
+    min-width: 36rpx;
+  }
+
+  .diner-stepper__icon--minus {
+    width: 20rpx;
+    height: 2rpx;
+    background: #c8c8c8;
+    border-radius: 2rpx;
+  }
+
+  .diner-stepper__icon--plus {
+    position: relative;
+    width: 18rpx;
+    height: 18rpx;
+  }
+
+  .diner-stepper__icon--plus::before,
+  .diner-stepper__icon--plus::after {
+    content: '';
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    background: #fc743d;
+    border-radius: 2rpx;
+  }
+
+  .diner-stepper__icon--plus::before {
+    width: 18rpx;
+    height: 2rpx;
+    transform: translate(-50%, -50%);
+  }
+
+  .diner-stepper__icon--plus::after {
+    width: 2rpx;
+    height: 18rpx;
+    transform: translate(-50%, -50%);
   }
 
   .price-line {

+ 361 - 208
pages/coupon/components/RulesModal.vue

@@ -1,51 +1,76 @@
 <template>
-  <!-- 使用规则弹窗 -->
   <BasicModal type="bottom" v-model:open="getOpen" :is-mack="true">
     <view class="rules-modal">
       <!-- 标题栏 -->
       <view class="rules-modal__header">
         <text class="header-title">使用规则</text>
         <view class="close-btn" @click="handleClose">
-          <view class="close-icon"></view>
+          <view class="close-icon" />
         </view>
       </view>
 
-      <!-- 优惠券卡片 - 与列表页样式一致 -->
-      <view class="coupon-card-wrapper">
-        <view class="coupon-card">
-          <image :src="getFileUrl('img/personal/coupon.png')" mode="widthFix" class="coupon-card-bg"></image>
-          <image :src="getFileUrl('img/personal/couponLeft.png')" mode="heightFix" class="coupon-card-bgLeft"></image>
-          <view class="coupon-card-content">
-            <!-- 左侧金额区域 -->
-            <view class="coupon-card__left">
-              <view class="amount-wrapper">
-                <text class="amount-number">{{ couponData?.amount || 0 }}</text>
-                <text class="amount-unit">元</text>
-              </view>
-              <text class="condition-text">{{ (couponData?.minAmount != null && Number(couponData.minAmount) > 0) ? ('满' + couponData.minAmount + '可用') : '无门槛' }}</text>
+      <view class="rules-modal__body">
+        <!-- 券摘要卡片:左金额区 + 右名称/到期 -->
+        <view class="card card--summary">
+          <view class="summary-left">
+            <view class="amount-line">
+              <text class="amount-num">{{ couponData?.amount ?? 0 }}</text>
+              <text class="amount-unit">{{ couponData?.amountUnit || '元' }}</text>
             </view>
+            <text class="summary-condition">{{ conditionLine }}</text>
+          </view>
+          <view class="summary-right">
+            <text class="summary-name">{{ couponData?.name || '优惠券' }}</text>
+            <text class="summary-expire">{{ expireLine }}</text>
+          </view>
+        </view>
 
-            <!-- 右侧信息区域 -->
-            <view class="coupon-card__right">
-              <view class="coupon-info">
-                <text class="coupon-name">{{ couponData?.name || '6元通用优惠券' }}</text>
-                <text class="coupon-expire">{{ couponData?.expireDate || '' }}到期</text>
+        <!-- 使用凭证:二维码 -->
+        <view class="card card--voucher">
+          <text class="card-title">使用凭证</text>
+          <view class="qr-wrap">
+            <image
+              v-if="qrImageSrc"
+              :src="qrImageSrc"
+              mode="aspectFit"
+              class="qr-image"
+              :style="{ width: qrBoxRpx + 'rpx', height: qrBoxRpx + 'rpx' }"
+            />
+            <view
+              v-else-if="qrMatrix.length"
+              class="qr-grid"
+              :style="{ width: qrBoxRpx + 'rpx', height: qrBoxRpx + 'rpx' }"
+            >
+              <view
+                v-for="(row, ri) in qrMatrix"
+                :key="ri"
+                class="qr-row"
+                :style="{ height: qrCellRpx + 'rpx' }"
+              >
+                <view
+                  v-for="(dark, ci) in row"
+                  :key="ci"
+                  class="qr-cell"
+                  :style="{ width: qrCellRpx + 'rpx', height: qrCellRpx + 'rpx' }"
+                  :class="{ 'qr-cell--dark': dark }"
+                />
               </view>
             </view>
+            <view v-else class="qr-placeholder">暂无核销码</view>
           </view>
         </view>
-      </view>
 
-      <!-- 规则详情 -->
-      <view class="rules-content">
-        <view class="rules-item">
-          <text class="rules-label">有效期</text>
-          <text class="rules-value">{{ (couponData?.specifiedDay != null && couponData?.specifiedDay !== '') ? (couponData.specifiedDay + '天') : (couponData?.specifiedDay ?? '') }}</text>
-        </view>
-        
-        <view class="rules-item">
-          <text class="rules-label">补充说明</text>
-          <text class="rules-value">{{ couponData?.supplementaryInstruction || '暂无说明' }}</text>
+        <!-- 使用须知 -->
+        <view class="card card--notice">
+          <text class="card-title">使用须知</text>
+          <view class="notice-row">
+            <text class="notice-label">有效期</text>
+            <text class="notice-value">{{ validityText }}</text>
+          </view>
+          <view class="notice-row">
+            <text class="notice-label">补充说明</text>
+            <text class="notice-value">{{ supplementText }}</text>
+          </view>
         </view>
       </view>
     </view>
@@ -53,9 +78,11 @@
 </template>
 
 <script setup>
-import { computed } from 'vue';
+import { computed, ref, watch } from 'vue';
 import BasicModal from '@/components/Modal/BasicModal.vue';
 import { getFileUrl } from '@/utils/file.js';
+import UQRCodeModule from 'uqrcodejs';
+const UQRCode = UQRCodeModule?.default || UQRCodeModule;
 
 const props = defineProps({
   open: {
@@ -64,14 +91,7 @@ const props = defineProps({
   },
   couponData: {
     type: Object,
-    default: () => ({
-      amount: 6,
-      minAmount: 66,
-      name: '6元通用优惠券',
-      expireDate: '2025/07/12',
-      specifiedDay: '',
-      supplementaryInstruction: ''
-    })
+    default: () => ({})
   }
 });
 
@@ -82,209 +102,342 @@ const getOpen = computed({
   set: (val) => emit('update:open', val)
 });
 
-// 关闭弹窗
-const handleClose = () => {
+const qrMatrix = ref([]);
+/** 二维码展示边长(rpx),约卡片宽 30%~40%,与设计稿一致 */
+const qrBoxRpx = 220;
+
+const conditionLine = computed(() => {
+  const c = props.couponData;
+  if (c?.conditionText) return c.conditionText;
+  const m = Number(c?.minAmount) || 0;
+  return m > 0 ? `满${m}可用` : '无门槛';
+});
+
+const expireLine = computed(() => {
+  const d = props.couponData?.expireDate;
+  if (!d) return '';
+  return `${d} 到期`;
+});
+
+const validityText = computed(() => {
+  const s = props.couponData?.specifiedDay;
+  if (s == null || s === '') return '—';
+  const str = String(s).trim();
+  if (str.includes('天')) return str;
+  const n = Number(str);
+  if (!Number.isNaN(n) && str !== '') return `${n}天`;
+  return str;
+});
+
+const supplementText = computed(() => {
+  const t = props.couponData?.supplementaryInstruction;
+  if (t != null && String(t).trim() !== '') return String(t).trim();
+  return '暂无说明';
+});
+
+const qrImageSrc = computed(() => {
+  const raw = props.couponData?.qrCodeUrl ?? props.couponData?.qrcodeUrl ?? '';
+  if (!raw) return '';
+  if (typeof raw === 'string' && (raw.startsWith('http') || raw.startsWith('//'))) return raw;
+  return getFileUrl(raw);
+});
+
+const qrPayload = computed(() => {
+  const c = props.couponData || {};
+  const id = c.id ?? c.userCouponId ?? c.couponId ?? '';
+  const code = c.verificationCode ?? c.couponCode ?? '';
+  const sid = uni.getStorageSync('currentStoreId') || '';
+  if (code) return String(code);
+  if (id) return `coupon:${id}:${sid}`;
+  return 'coupon';
+});
+
+const qrCellRpx = computed(() => {
+  const n = qrMatrix.value.length;
+  if (!n) return 0;
+  return qrBoxRpx / n;
+});
+
+function buildQrMatrix(text) {
+  try {
+    const qr = new UQRCode();
+    qr.data = String(text || ' ');
+    qr.size = 200;
+    qr.make();
+    const n = qr.moduleCount;
+    const mods = qr.modules;
+    if (!n || !mods) return [];
+    const rows = [];
+    for (let r = 0; r < n; r++) {
+      const row = [];
+      for (let c = 0; c < n; c++) {
+        const cell = mods[r][c];
+        const dark =
+          typeof cell === 'object' && cell !== null ? !!cell.isBlack : !!cell;
+        row.push(dark);
+      }
+      rows.push(row);
+    }
+    return rows;
+  } catch (e) {
+    console.warn('生成二维码失败:', e);
+    return [];
+  }
+}
+
+watch(
+  () => ({
+    open: props.open,
+    id: props.couponData?.id,
+    qrUrl: props.couponData?.qrCodeUrl ?? props.couponData?.qrcodeUrl,
+    payload: qrPayload.value
+  }),
+  ({ open }) => {
+    if (!open) {
+      qrMatrix.value = [];
+      return;
+    }
+    if (qrImageSrc.value) {
+      qrMatrix.value = [];
+      return;
+    }
+    qrMatrix.value = buildQrMatrix(qrPayload.value);
+  },
+  { flush: 'post', deep: true }
+);
+
+function handleClose() {
   getOpen.value = false;
-};
+}
 </script>
 
 <style lang="scss" scoped>
 .rules-modal {
   width: 100%;
-  background: #FFFFFF;
+  background: #f5f5f5;
   border-radius: 24rpx 24rpx 0 0;
-  padding: 32rpx 30rpx;
-  padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
+  padding: 28rpx 0 calc(28rpx + env(safe-area-inset-bottom));
   box-sizing: border-box;
+  max-height: 85vh;
+}
+
+.rules-modal__header {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 24rpx;
+  padding: 0 30rpx;
+  position: relative;
+
+  .header-title {
+    font-size: 34rpx;
+    font-weight: bold;
+    color: #151515;
+  }
 
-  // 标题栏
-  &__header {
+  .close-btn {
+    position: absolute;
+    right: 24rpx;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 56rpx;
+    height: 56rpx;
     display: flex;
     align-items: center;
     justify-content: center;
-    margin-bottom: 32rpx;
-    position: relative;
+  }
 
-    .header-title {
-      font-size: 36rpx;
-      font-weight: bold;
-      color: #151515;
-    }
+  .close-icon {
+    width: 28rpx;
+    height: 28rpx;
+    position: relative;
 
-    .close-btn {
+    &::before,
+    &::after {
+      content: '';
       position: absolute;
-      right: 0;
+      width: 28rpx;
+      height: 3rpx;
+      background: #999999;
       top: 50%;
-      transform: translateY(-50%);
-      width: 48rpx;
-      height: 48rpx;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-
-      .close-icon {
-        width: 28rpx;
-        height: 28rpx;
-        position: relative;
-
-        &::before,
-        &::after {
-          content: '';
-          position: absolute;
-          width: 28rpx;
-          height: 2rpx;
-          background: #999999;
-          top: 50%;
-          left: 50%;
-          transform-origin: center;
-        }
-
-        &::before {
-          transform: translate(-50%, -50%) rotate(45deg);
-        }
-
-        &::after {
-          transform: translate(-50%, -50%) rotate(-45deg);
-        }
-      }
+      left: 50%;
+      transform-origin: center;
+    }
+
+    &::before {
+      transform: translate(-50%, -50%) rotate(45deg);
+    }
+
+    &::after {
+      transform: translate(-50%, -50%) rotate(-45deg);
     }
   }
+}
+
+.rules-modal__body {
+  padding: 0 24rpx;
+  box-sizing: border-box;
+}
+
+.card {
+  background: #ffffff;
+  border-radius: 16rpx;
+  margin-bottom: 20rpx;
+  overflow: hidden;
+  box-sizing: border-box;
 
-  // 优惠券卡片容器
-  .coupon-card-wrapper {
-    margin-bottom: 32rpx;
+  &:last-child {
+    margin-bottom: 0;
   }
+}
 
-  // 优惠券卡片样式 - 与列表页完全一致
-  .coupon-card {
-    display: flex;
-    align-items: center;
-    overflow: hidden;
-    position: relative;
-    box-sizing: border-box;
+.card-title {
+  display: block;
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #151515;
+  margin-bottom: 20rpx;
+}
 
-    .coupon-card-bg {
-      position: absolute;
-      top: 0;
-      left: 0;
-      width: 100%;
-      height: 100%;
-      z-index: 1;
-    }
+/* 券摘要 */
+.card--summary {
+  display: flex;
+  flex-direction: row;
+  align-items: stretch;
+  min-height: 168rpx;
+  padding: 0;
+}
 
-    .coupon-card-bgLeft {
-      position: absolute;
-      top: 0;
-      left: 0;
-      width: auto;
-      height: 190rpx;
-      z-index: 1;
-    }
+.summary-left {
+  width: 220rpx;
+  flex-shrink: 0;
+  background: #fff4e6;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 24rpx 16rpx;
+  box-sizing: border-box;
+}
 
-    .coupon-card-content {
-      position: relative;
-      z-index: 3;
-      display: flex;
-      align-items: center;
-      width: 100%;
-      height: 200rpx;
-    }
+.amount-line {
+  display: flex;
+  flex-direction: row;
+  align-items: baseline;
+  margin-bottom: 10rpx;
+}
 
-    &__left {
-      width: 200rpx;
-      padding: 32rpx 0;
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      justify-content: center;
-
-      .amount-wrapper {
-        display: flex;
-        align-items: baseline;
-        margin-bottom: 8rpx;
-
-        .amount-number {
-          font-size: 64rpx;
-          font-weight: bold;
-          color: #F47D1F;
-          line-height: 1;
-        }
-
-        .amount-unit {
-          font-size: 28rpx;
-          color: #F47D1F;
-          margin-left: 4rpx;
-        }
-      }
+.amount-num {
+  font-size: 56rpx;
+  font-weight: bold;
+  color: #f47d1f;
+  line-height: 1;
+}
 
-      .condition-text {
-        font-size: 22rpx;
-        color: #F47D1F;
-      }
-    }
+.amount-unit {
+  font-size: 28rpx;
+  color: #f47d1f;
+  margin-left: 4rpx;
+}
 
-    &__right {
-      flex: 1;
-      display: flex;
-      align-items: center;
-      justify-content: space-between;
-      padding: 32rpx 24rpx;
-
-      .coupon-info {
-        flex: 1;
-        display: flex;
-        flex-direction: column;
-
-        .coupon-name {
-          font-size: 28rpx;
-          font-weight: bold;
-          color: #151515;
-          margin-bottom: 12rpx;
-        }
-
-        .coupon-expire {
-          font-size: 22rpx;
-          color: #999999;
-        }
-      }
-    }
-  }
+.summary-condition {
+  font-size: 22rpx;
+  color: #f47d1f;
+}
+
+.summary-right {
+  flex: 1;
+  min-width: 0;
+  padding: 28rpx 24rpx;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  background: #ffffff;
+}
 
-  // 规则详情
-  .rules-content {
-    margin-bottom: 40rpx;
-    padding: 0 8rpx;
+.summary-name {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #151515;
+  line-height: 1.4;
+  margin-bottom: 12rpx;
+}
 
-    .rules-item {
-      display: flex;
-      align-items: flex-start;
-      margin-bottom: 20rpx;
-      font-size: 26rpx;
-      line-height: 40rpx;
+.summary-expire {
+  font-size: 24rpx;
+  color: #999999;
+}
 
-      &:last-child {
-        margin-bottom: 0;
-      }
+/* 使用凭证 */
+.card--voucher {
+  padding: 28rpx 24rpx 32rpx;
+}
 
-      .rules-label {
-        color: #666666;
-        flex-shrink: 0;
-      }
+.qr-wrap {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 8rpx 0 4rpx;
+}
 
-      .rules-value {
-        color: #151515;
-        margin-left: 16rpx;
-        flex: 1;
-      }
-    }
+.qr-image {
+  display: block;
+}
+
+.qr-grid {
+  background: #ffffff;
+  overflow: hidden;
+}
+
+.qr-row {
+  display: flex;
+  flex-direction: row;
+}
+
+.qr-cell {
+  flex-shrink: 0;
+  box-sizing: border-box;
+  background: #ffffff;
+
+  &--dark {
+    background: #151515;
   }
+}
 
+.qr-placeholder {
+  font-size: 26rpx;
+  color: #aaaaaa;
 }
 
-.hover-active {
-  opacity: 0.8;
-  transform: scale(0.98);
-  transition: all 0.2s;
+/* 使用须知 */
+.card--notice {
+  padding: 28rpx 24rpx 32rpx;
+}
+
+.notice-row {
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+  justify-content: space-between;
+  margin-bottom: 20rpx;
+  font-size: 28rpx;
+  line-height: 1.5;
+
+  &:last-child {
+    margin-bottom: 0;
+  }
+}
+
+.notice-label {
+  color: #888888;
+  flex-shrink: 0;
+  margin-right: 24rpx;
 }
-</style>
 
+.notice-value {
+  flex: 1;
+  text-align: right;
+  color: #151515;
+  word-break: break-all;
+}
+</style>

+ 234 - 40
pages/coupon/index.vue

@@ -2,16 +2,51 @@
   <!-- 优惠券页面 -->
   <view class="page">
 
-    <!-- 标签页 -->
-    <view class="tabs">
-      <view v-for="(tab, index) in tabs" :key="index"
-        :class="['tab-item', { 'tab-item--active': currentTab === index }]" @click="handleTabChange(index)">
-        {{ tab }}
+    <!-- 主 Tab + 右侧「全部」类型筛选 -->
+    <view class="tabs-wrap">
+      <view class="tabs-row">
+        <view class="tabs-main">
+          <view
+            v-for="(tab, index) in tabs"
+            :key="index"
+            :class="['tab-item', { 'tab-item--active': currentTab === index }]"
+            @click="handleTabChange(index)"
+          >
+            {{ tab }}
+          </view>
+        </view>
+        <view class="tabs-trigger-gap" />
+        <view class="tabs-trigger" @click="toggleTypePanel">
+          <text
+            :class="[
+              'tabs-trigger__text',
+              {
+                'tabs-trigger__text--open': typePanelOpen,
+                'tabs-trigger__text--filtered': !typePanelOpen && typeFilter !== 'all'
+              }
+            ]"
+          >{{ typeTriggerLabel }}</text>
+          <view
+            :class="['tabs-trigger__chevron', { 'tabs-trigger__chevron--open': typePanelOpen }]"
+          />
+        </view>
+      </view>
+      <view v-if="typePanelOpen" class="type-filter-row">
+        <view
+          v-for="opt in typeFilterOptions"
+          :key="opt.value"
+          :class="['type-pill', { 'type-pill--active': typeFilter === opt.value }]"
+          @click="selectTypeFilter(opt.value)"
+        >
+          {{ opt.label }}
+        </view>
       </view>
     </view>
 
-    <!-- 优惠券列表 -->
-    <scroll-view class="content" scroll-y>
+    <!-- 列表区域:展开类型筛选时盖半透明遮罩,点击关闭 -->
+    <view class="page-body">
+      <view v-if="typePanelOpen" class="type-mask" @tap="closeTypePanel" />
+      <scroll-view class="content" scroll-y :class="{ 'content--dimmed': typePanelOpen }">
       <view class="coupon-list" v-if="filteredCoupons.length > 0">
         <view v-for="(coupon, index) in filteredCoupons" :key="coupon.id || index"
           :class="['coupon-card', getCouponCardClass(coupon)]">
@@ -48,7 +83,8 @@
         <image :src="getFileUrl('img/icon/noCoupon.png')" mode="widthFix" class="empty-icon"></image>
         <text class="empty-text">暂无优惠券</text>
       </view>
-    </scroll-view>
+      </scroll-view>
+    </view>
 
     <!-- 使用规则弹窗 -->
     <RulesModal v-model:open="showRulesModal" :couponData="selectedCoupon" />
@@ -58,7 +94,6 @@
 <script setup>
 import { onShow } from "@dcloudio/uni-app";
 import { ref, computed } from "vue";
-import { go } from "@/utils/utils.js";
 import { getFileUrl } from "@/utils/file.js";
 import RulesModal from "./components/RulesModal.vue";
 import * as diningApi from "@/api/dining.js";
@@ -67,15 +102,48 @@ import * as diningApi from "@/api/dining.js";
 const tabs = ['未使用', '即将过期', '已使用', '已过期'];
 const currentTab = ref(0);
 
+/** 右侧:展开折扣券 / 满减券筛选(couponType 1 满减 2 折扣) */
+const typePanelOpen = ref(false);
+const typeFilter = ref('all');
+const typeFilterOptions = [
+  { value: 'all', label: '全部' },
+  { value: 'discount', label: '折扣券' },
+  { value: 'reduction', label: '满减券' }
+];
+
+/** 右上角文案与当前选中的券类型一致 */
+const typeTriggerLabel = computed(() => {
+  const hit = typeFilterOptions.find((o) => o.value === typeFilter.value);
+  return hit?.label ?? '全部';
+});
+
+function toggleTypePanel() {
+  typePanelOpen.value = !typePanelOpen.value;
+}
+
+function closeTypePanel() {
+  typePanelOpen.value = false;
+}
+
+function selectTypeFilter(value) {
+  typeFilter.value = value;
+  // 仅在「切换券类型」流程中展示选项,选完后收起
+  typePanelOpen.value = false;
+}
+
 // 弹窗控制
 const showRulesModal = ref(false);
 const selectedCoupon = ref({
   amount: 0,
+  amountUnit: '元',
   minAmount: 0,
   name: '',
   expireDate: '',
   specifiedDay: '',
-  supplementaryInstruction: ''
+  supplementaryInstruction: '',
+  conditionText: '',
+  qrCodeUrl: '',
+  verificationCode: ''
 });
 
 // 优惠券数据(接口返回)
@@ -110,16 +178,26 @@ function normalizeCouponItem(raw) {
     discountRate,
     conditionText,
     specifiedDay: raw.specifiedDay ?? raw.validDays ?? '',
-    supplementaryInstruction: raw.supplementaryInstruction ?? raw.description ?? ''
+    supplementaryInstruction: raw.supplementaryInstruction ?? raw.description ?? '',
+    qrCodeUrl: raw.qrCodeUrl ?? raw.qrcodeUrl ?? raw.qrUrl ?? '',
+    verificationCode:
+      raw.verificationCode ?? raw.verifyCode ?? raw.couponCode ?? raw.pickUpCode ?? ''
   };
 }
 
-// 优惠券列表
-const filteredCoupons = computed(() => couponList.value);
+// 列表:主 Tab 数据 + 类型筛选(全部 / 折扣券 / 满减券)
+const filteredCoupons = computed(() => {
+  const list = couponList.value;
+  const t = typeFilter.value;
+  if (t === 'discount') return list.filter((c) => Number(c.couponType) === 2);
+  if (t === 'reduction') return list.filter((c) => Number(c.couponType) === 1);
+  return list;
+});
 
 // 切换标签页
 const handleTabChange = (index) => {
   currentTab.value = index;
+  typePanelOpen.value = false;
   fetchCouponList();
 };
 
@@ -183,37 +261,147 @@ onShow(() => {
   }
 }
 
-.tabs {
+.tabs-wrap {
+  flex-shrink: 0;
+  background: #ffffff;
+  position: relative;
+  z-index: 20;
+}
+
+.page-body {
+  flex: 1;
+  min-height: 0;
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.type-mask {
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  z-index: 10;
+  background: rgba(0, 0, 0, 0.45);
+}
+
+.tabs-row {
+  display: flex;
+  flex-direction: row;
+  align-items: flex-end;
+  padding: 0 24rpx 0 20rpx;
+  box-sizing: border-box;
+}
+
+.tabs-main {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: row;
+  align-items: flex-end;
+}
+
+.tabs-trigger-gap {
+  flex-shrink: 0;
+  width: 24rpx;
+}
+
+.tabs-trigger {
   flex-shrink: 0;
-  background: #FFFFFF;
   display: flex;
+  flex-direction: row;
   align-items: center;
-  padding: 0 30rpx;
+  justify-content: flex-end;
+  padding: 28rpx 0 24rpx 8rpx;
+  box-sizing: border-box;
+}
 
-  .tab-item {
-    flex: 1;
-    text-align: center;
-    font-size: 28rpx;
-    color: #666666;
-    padding: 28rpx 0;
-    position: relative;
-    transition: all 0.3s;
-
-    &--active {
-      color: #151515;
-      font-weight: bold;
-
-      &::after {
-        content: '';
-        position: absolute;
-        bottom: 20rpx;
-        left: 50%;
-        transform: translateX(-50%);
-        width: 40rpx;
-        height: 6rpx;
-        background: linear-gradient(90deg, #FF8A57 0%, #F47D1F 100%);
-        border-radius: 3rpx;
-      }
+.tabs-trigger__text {
+  font-size: 26rpx;
+  color: #151515;
+  line-height: 1;
+}
+
+.tabs-trigger__text--open {
+  color: #f47d1f;
+}
+
+/* 已选折扣券/满减券且面板收起时,右上角保持主题色提示当前筛选 */
+.tabs-trigger__text--filtered {
+  color: #f47d1f;
+}
+
+.tabs-trigger__chevron {
+  width: 0;
+  height: 0;
+  margin-left: 8rpx;
+  border-left: 8rpx solid transparent;
+  border-right: 8rpx solid transparent;
+  border-top: 10rpx solid #999999;
+  transform: translateY(2rpx);
+}
+
+.tabs-trigger__chevron--open {
+  border-top: none;
+  border-bottom: 10rpx solid #f47d1f;
+  border-left: 8rpx solid transparent;
+  border-right: 8rpx solid transparent;
+  transform: translateY(-2rpx);
+}
+
+.type-filter-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 16rpx;
+  padding: 8rpx 30rpx 20rpx;
+  box-sizing: border-box;
+  background: #ffffff;
+}
+
+.type-pill {
+  padding: 14rpx 32rpx;
+  border-radius: 999rpx;
+  font-size: 24rpx;
+  color: #333333;
+  background: #f2f3f5;
+  line-height: 1.2;
+}
+
+.type-pill--active {
+  background: #fff4e6;
+  color: #f47d1f;
+  font-weight: 500;
+}
+
+.tab-item {
+  flex: 1;
+  min-width: 0;
+  text-align: center;
+  font-size: 26rpx;
+  color: #666666;
+  padding: 28rpx 4rpx 24rpx;
+  position: relative;
+  transition: color 0.2s;
+  box-sizing: border-box;
+
+  &--active {
+    color: #151515;
+    font-weight: bold;
+
+    &::after {
+      content: '';
+      position: absolute;
+      bottom: 16rpx;
+      left: 50%;
+      transform: translateX(-50%);
+      width: 48rpx;
+      height: 6rpx;
+      background: linear-gradient(90deg, #ff8a57 0%, #f47d1f 100%);
+      border-radius: 3rpx;
     }
   }
 }
@@ -221,11 +409,17 @@ onShow(() => {
 .content {
   flex: 1;
   min-height: 0;
+  position: relative;
+  z-index: 1;
   padding: 24rpx 30rpx;
   padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
   box-sizing: border-box;
 }
 
+.content--dimmed {
+  pointer-events: none;
+}
+
 .coupon-list {
   position: relative;
 

+ 859 - 0
pages/diningInfo/index.vue

@@ -0,0 +1,859 @@
+<template>
+  <view class="page">
+    <view class="card">
+      <view class="card-title">信息填写</view>
+
+      <!-- 时间:首行 时间* + 右侧橙色链接;次行并排两个时间选择框 -->
+      <view class="section section--time">
+        <view class="time-label-row">
+          <text class="label label--req">时间</text>
+          <text class="link" @tap.stop="openTableInfoPopup">(查看桌台信息)</text>
+        </view>
+        <view class="time-row">
+          <view class="time-col">
+            <view class="time-box time-box--readonly">
+              <text class="time-clock">🕐</text>
+              <text class="time-text">{{ form.startTime || '—' }}</text>
+            </view>
+          </view>
+          <view class="time-col">
+            <picker class="time-picker" mode="time" :value="form.endTime" @change="onEndTimeChange">
+              <view class="time-box">
+                <text class="time-clock">🕐</text>
+                <text :class="['time-text', { 'is-placeholder': !form.endTime }]">
+                  {{ form.endTime || '选择结束时间(次日)' }}
+                </text>
+              </view>
+            </picker>
+          </view>
+        </view>
+      </view>
+
+      <view class="divider" />
+
+      <view class="row">
+        <text class="label">姓名</text>
+        <input
+          v-model="form.name"
+          class="input"
+          type="text"
+          placeholder="请输入姓名"
+          placeholder-class="input-ph"
+        />
+      </view>
+
+      <view class="divider" />
+
+      <view class="row row-gender">
+        <text class="label">性别</text>
+        <view class="gender-group">
+          <view
+            class="gender-opt"
+            :class="{ active: form.gender === 'mr' }"
+            @tap.stop="selectGender('mr')"
+          >
+            <view class="gender-dot" :class="{ on: form.gender === 'mr' }">
+              <view v-if="form.gender === 'mr'" class="gender-dot-inner" />
+            </view>
+            <text class="gender-txt">先生</text>
+          </view>
+          <view
+            class="gender-opt"
+            :class="{ active: form.gender === 'ms' }"
+            @tap.stop="selectGender('ms')"
+          >
+            <view class="gender-dot" :class="{ on: form.gender === 'ms' }">
+              <view v-if="form.gender === 'ms'" class="gender-dot-inner" />
+            </view>
+            <text class="gender-txt">女士</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="divider" />
+
+      <view class="row">
+        <text class="label">电话</text>
+        <input
+          v-model="form.phone"
+          class="input"
+          type="number"
+          maxlength="11"
+          placeholder="请输入手机号"
+          placeholder-class="input-ph"
+        />
+      </view>
+
+      <view class="divider" />
+
+      <view class="row row-remark">
+        <text class="label label-top">备注</text>
+        <view class="remark-wrap">
+          <textarea
+            v-model="form.remark"
+            class="textarea"
+            placeholder="请输入"
+            placeholder-class="input-ph"
+            maxlength="30"
+            :show-confirm-bar="false"
+          />
+          <text class="remark-count">{{ (form.remark || '').length }}/30</text>
+        </view>
+      </view>
+    </view>
+
+    <view class="footer-spacer" />
+    <view class="footer-bar">
+      <view class="footer-btn hover-active" @click="handleConfirm">确定</view>
+    </view>
+
+    <!-- 桌台信息:底部弹窗(可预约时段) -->
+    <view v-if="tableInfoPopupOpen" class="table-popup-root">
+      <view class="table-popup-mask" @click="closeTableInfoPopup" />
+      <view class="table-popup-sheet" @click.stop>
+        <view class="table-popup-head">
+          <text class="table-popup-title">桌台信息 ({{ tableDisplayCode }})</text>
+          <view class="table-popup-close" hover-class="table-popup-close--active" @click="closeTableInfoPopup">
+            <text class="table-popup-close-x">×</text>
+          </view>
+        </view>
+        <view class="table-slot-grid">
+          <view v-if="tableInfoLoading" class="table-popup-hint">加载中…</view>
+          <view v-else-if="!tableTimeSlots.length" class="table-popup-hint table-popup-hint--muted">暂无可预约时段</view>
+          <template v-else>
+            <view v-for="(slot, i) in tableTimeSlots" :key="i" class="table-slot-tag">
+              <text class="table-slot-tag-text">{{ slot.label }}</text>
+            </view>
+          </template>
+        </view>
+        <view class="table-popup-safe" />
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { onLoad } from '@dcloudio/uni-app';
+import { reactive, ref } from 'vue';
+import { useUserStore } from '@/store/user.js';
+import {
+  GetReservationDetailByStoreTableRecord,
+  GetTableDiningStatus,
+  PostDiningWalkInReservation
+} from '@/api/dining.js';
+
+const userStore = useUserStore();
+
+/** 桌台信息弹窗 */
+const tableInfoPopupOpen = ref(false);
+const tableInfoLoading = ref(false);
+const tableDisplayCode = ref('—');
+/** 桌台可预约时段(接口返回),仅展示 */
+const tableTimeSlots = ref([]);
+
+const form = reactive({
+  startTime: '',
+  endTime: '',
+  name: '',
+  gender: '', // mr 先生 / ms 女士
+  phone: '',
+  remark: ''
+});
+
+/** 统一性别为 mr/ms,兼容接口/缓存/用户信息中的多种写法 */
+function normalizeGender(val) {
+  if (val == null || val === '') return '';
+  const raw = String(val).trim();
+  const s = raw.toLowerCase();
+  if (raw === '男' || raw === '先生') return 'mr';
+  if (raw === '女' || raw === '女士') return 'ms';
+  if (['mr', 'male', 'm', '1'].includes(s)) return 'mr';
+  if (['ms', 'female', 'f', '2'].includes(s)) return 'ms';
+  return '';
+}
+
+function selectGender(v) {
+  form.gender = v === 'ms' ? 'ms' : 'mr';
+}
+
+function syncPhoneFromUser() {
+  const u = userStore.getUserInfo || {};
+  const p = u.phone ?? u.mobile ?? u.contactPhone ?? '';
+  if (p && !form.phone) form.phone = String(p);
+  const n = u.nickName ?? u.nickname ?? u.name ?? '';
+  if (n && !form.name) form.name = String(n);
+}
+
+/** 开始时间固定为进入页时的当前时间(HH:mm),不可改 */
+function initStartTimeToNow() {
+  const d = new Date();
+  const h = String(d.getHours()).padStart(2, '0');
+  const m = String(d.getMinutes()).padStart(2, '0');
+  form.startTime = `${h}:${m}`;
+}
+
+onLoad(() => {
+  initStartTimeToNow();
+  syncPhoneFromUser();
+  try {
+    const raw = uni.getStorageSync('diningForm');
+    if (raw && typeof raw === 'string') {
+      const o = JSON.parse(raw);
+      if (o && typeof o === 'object') {
+        // 不恢复 startTime,始终以当前时间为准
+        if (o.endTime) form.endTime = o.endTime;
+        if (o.name) form.name = o.name;
+        const gCached = normalizeGender(o.gender);
+        if (gCached) form.gender = gCached;
+        if (o.phone) form.phone = o.phone;
+        if (o.remark) form.remark = o.remark;
+      }
+    }
+  } catch {
+    /* ignore */
+  }
+  // 缓存未带性别时,从登录用户信息同步(男/女、mile/female 等)
+  if (!form.gender) {
+    const u = userStore.getUserInfo || {};
+    const gUser = normalizeGender(u.gender);
+    if (gUser) form.gender = gUser;
+  }
+  // 缓存恢复的结束时间若早于开始时间(同日),清空需重选
+  if (form.endTime && form.startTime && !isEndAfterStartSameDay(form.endTime, form.startTime)) {
+    form.endTime = '';
+  }
+});
+
+/** HH:mm 转当天分钟数 */
+function timeStrToMinutes(t) {
+  if (t == null || t === '') return NaN;
+  const parts = String(t).trim().split(':');
+  const h = parseInt(parts[0], 10);
+  const m = parseInt(parts[1] ?? '0', 10);
+  if (Number.isNaN(h) || Number.isNaN(m)) return NaN;
+  return h * 60 + m;
+}
+
+/** 同日:结束时间须严格大于开始时间 */
+function isEndAfterStartSameDay(endStr, startStr) {
+  const end = timeStrToMinutes(endStr);
+  const start = timeStrToMinutes(startStr);
+  if (Number.isNaN(end) || Number.isNaN(start)) return false;
+  return end > start;
+}
+
+const END_AFTER_START_TIP = '结束时间大于开始时间(同日)';
+
+function onEndTimeChange(e) {
+  const end = e.detail.value || '';
+  if (!end) {
+    form.endTime = '';
+    return;
+  }
+  if (form.startTime && !isEndAfterStartSameDay(end, form.startTime)) {
+    uni.showToast({ title: END_AFTER_START_TIP, icon: 'none' });
+    return;
+  }
+  form.endTime = end;
+}
+
+function resolveTableDisplayCode() {
+  const no =
+    uni.getStorageSync('currentTableNumber') ||
+    uni.getStorageSync('currentTableNo') ||
+    uni.getStorageSync('tableNumber') ||
+    '';
+  const tid = uni.getStorageSync('currentTableId') || '';
+  if (no) return String(no);
+  if (tid) return String(tid);
+  return '—';
+}
+
+/** 桌台信息弹窗标题:优先用 /store/dining/table-dining-status 返回的 tableNumber */
+function pickTableNumberFromDiningStatus(data) {
+  if (data == null || typeof data !== 'object') return '';
+  const d = data.data != null && typeof data.data === 'object' ? data.data : data;
+  const n = d.tableNumber ?? d.tableNo ?? d.tableName ?? d.tableCode;
+  return n != null && n !== '' ? String(n) : '';
+}
+
+/** 从预约详情里取桌号展示文案 */
+function pickTableDisplayFromDetail(data, fallbackTableId) {
+  const r = data?.record && typeof data.record === 'object' ? data.record : data;
+  if (!r || typeof r !== 'object') {
+    const fb = resolveTableDisplayCode();
+    return fb !== '—' ? fb : String(fallbackTableId || '—');
+  }
+  const code =
+    r.tableNumber ??
+    r.tableNo ??
+    r.tableName ??
+    r.tableCode ??
+    data?.tableNumber ??
+    data?.tableNo ??
+    r.tableId ??
+    data?.tableId;
+  if (code != null && code !== '') return String(code);
+  const fb = resolveTableDisplayCode();
+  return fb !== '—' ? fb : String(fallbackTableId || '—');
+}
+
+function mapListToSlotLabels(list) {
+  if (!Array.isArray(list)) return [];
+  return list
+    .map((item) => {
+      if (item == null) return null;
+      if (typeof item === 'string') return { label: item };
+      const start = item.startTime ?? item.start ?? item.begin ?? '';
+      const end = item.endTime ?? item.end ?? item.finish ?? '';
+      if (start !== '' && end !== '') {
+        const s = String(start).length > 5 ? String(start).slice(11, 16) : String(start).slice(0, 5);
+        const e = String(end).length > 5 ? String(end).slice(11, 16) : String(end).slice(0, 5);
+        return { label: `${s}–${e}` };
+      }
+      const label = item.label ?? item.timeRange ?? item.slotText ?? item.name ?? item.text ?? '';
+      return label ? { label: String(label) } : null;
+    })
+    .filter(Boolean);
+}
+
+/** 从预约详情里解析可预约时段列表 */
+function mapReservationSlots(data) {
+  if (Array.isArray(data)) return mapListToSlotLabels(data);
+  if (!data || typeof data !== 'object') return [];
+  const nested = data.record && typeof data.record === 'object' ? data.record : {};
+  const list =
+    data.timeSlots ??
+    data.timeSlotList ??
+    data.slots ??
+    data.reservableTimeSlots ??
+    data.periods ??
+    data.availableSlots ??
+    data.reservationSlots ??
+    nested.timeSlots ??
+    nested.slots ??
+    (Array.isArray(data.list) ? data.list : null) ??
+    (Array.isArray(data.records) ? data.records : null);
+  return mapListToSlotLabels(list);
+}
+
+async function openTableInfoPopup() {
+  const storeId = uni.getStorageSync('currentStoreId') || '';
+  const tableId = uni.getStorageSync('currentTableId') || '';
+  if (!storeId || !tableId) {
+    uni.showToast({ title: '缺少门店或桌台信息', icon: 'none' });
+    return;
+  }
+  tableDisplayCode.value = resolveTableDisplayCode();
+  tableTimeSlots.value = [];
+  tableInfoLoading.value = true;
+  tableInfoPopupOpen.value = true;
+  let titleFromDiningStatus = '';
+  try {
+    const statusData = await GetTableDiningStatus(tableId, { loading: false });
+    titleFromDiningStatus = pickTableNumberFromDiningStatus(statusData);
+    if (titleFromDiningStatus) tableDisplayCode.value = titleFromDiningStatus;
+  } catch {
+    /* 标题保持本地缓存/桌号兜底 */
+  }
+  try {
+    const data = await GetReservationDetailByStoreTableRecord({
+      storeId,
+      userReservationTableId: tableId
+    });
+    if (!titleFromDiningStatus) {
+      tableDisplayCode.value = pickTableDisplayFromDetail(data, tableId);
+    }
+    tableTimeSlots.value = mapReservationSlots(data);
+  } catch {
+    tableTimeSlots.value = [];
+    if (!titleFromDiningStatus) {
+      tableDisplayCode.value = resolveTableDisplayCode();
+    }
+  } finally {
+    tableInfoLoading.value = false;
+  }
+}
+
+function closeTableInfoPopup() {
+  tableInfoPopupOpen.value = false;
+}
+
+/** 表单性别 mr/ms → 接口 reservationUserGender:0 男 1 女 */
+function genderToReservationCode(g) {
+  if (g === 'mr') return '0';
+  if (g === 'ms') return '1';
+  return '';
+}
+
+/** walk-in 接口要求 startTime/endTime 为 yyyy-MM-dd HH:mm;日期用当天(本地日历),时间与页面 HH:mm 一致 */
+function toWalkInApiDateTime(hhmm) {
+  if (hhmm == null || hhmm === '') return '';
+  const parts = String(hhmm).trim().split(':');
+  let h = parseInt(parts[0], 10);
+  let m = parseInt(parts[1] ?? '0', 10);
+  if (Number.isNaN(h)) h = 0;
+  if (Number.isNaN(m)) m = 0;
+  const hStr = String(Math.min(23, Math.max(0, h))).padStart(2, '0');
+  const mStr = String(Math.min(59, Math.max(0, m))).padStart(2, '0');
+  const d = new Date();
+  const y = d.getFullYear();
+  const mo = String(d.getMonth() + 1).padStart(2, '0');
+  const day = String(d.getDate()).padStart(2, '0');
+  return `${y}-${mo}-${day} ${hStr}:${mStr}`;
+}
+
+async function handleConfirm() {
+  if (!form.startTime || !form.endTime) {
+    uni.showToast({ title: '请选择用餐时间', icon: 'none' });
+    return;
+  }
+  if (!isEndAfterStartSameDay(form.endTime, form.startTime)) {
+    uni.showToast({ title: END_AFTER_START_TIP, icon: 'none' });
+    return;
+  }
+  const tableid = uni.getStorageSync('currentTableId') || '';
+  const diners = uni.getStorageSync('currentDiners') ?? '1';
+  if (!tableid) {
+    uni.showToast({ title: '缺少桌台信息', icon: 'none' });
+    return;
+  }
+  const phone = (form.phone || '').trim();
+  if (!phone) {
+    uni.showToast({ title: '请输入手机号', icon: 'none' });
+    return;
+  }
+  const guestCount = parseInt(String(diners), 10);
+  if (!Number.isFinite(guestCount) || guestCount < 1) {
+    uni.showToast({ title: '就餐人数异常', icon: 'none' });
+    return;
+  }
+  const tableIdNum = parseInt(String(tableid), 10);
+  if (Number.isNaN(tableIdNum)) {
+    uni.showToast({ title: '桌台信息异常', icon: 'none' });
+    return;
+  }
+
+  const dto = {
+    tableId: tableIdNum,
+    guestCount,
+    reservationUserPhone: phone,
+    startTime: toWalkInApiDateTime(form.startTime),
+    endTime: toWalkInApiDateTime(form.endTime)
+  };
+  const name = (form.name || '').trim();
+  if (name) dto.reservationUserName = name;
+  const genderCode = genderToReservationCode(form.gender);
+  if (genderCode !== '') dto.reservationUserGender = genderCode;
+  const remark = (form.remark || '').trim().slice(0, 30);
+  if (remark) dto.remark = remark;
+
+  try {
+    await PostDiningWalkInReservation(dto);
+  } catch {
+    return;
+  }
+
+  uni.setStorageSync(
+    'diningForm',
+    JSON.stringify({
+      startTime: form.startTime,
+      endTime: form.endTime,
+      endNextDay: true,
+      name: form.name.trim(),
+      gender: form.gender === 'mr' || form.gender === 'ms' ? form.gender : '',
+      phone: form.phone.trim(),
+      remark: (form.remark || '').trim()
+    })
+  );
+
+  uni.redirectTo({
+    url: `/pages/orderFood/index?tableid=${encodeURIComponent(String(tableid))}&diners=${encodeURIComponent(String(diners))}`
+  });
+}
+</script>
+
+<style lang="scss" scoped>
+.page {
+  min-height: 100vh;
+  background: #f2f3f5;
+  padding: 24rpx 30rpx 200rpx;
+  box-sizing: border-box;
+}
+
+.card {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 36rpx 30rpx 32rpx;
+  box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
+}
+
+.card-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #151515;
+  margin-bottom: 28rpx;
+  letter-spacing: 0.5rpx;
+}
+
+.section--time {
+  padding-bottom: 8rpx;
+}
+
+/* 时间:「时间*」与「查看桌台信息」均靠左同一行 */
+.time-label-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: flex-start;
+  flex-wrap: wrap;
+  gap: 8rpx 20rpx;
+  margin-bottom: 20rpx;
+}
+
+.label {
+  font-size: 28rpx;
+  color: #333333;
+  flex-shrink: 0;
+  min-width: 100rpx;
+}
+
+.label--req::after {
+  content: '*';
+  color: #e61f19;
+  margin-left: 2rpx;
+}
+
+.link {
+  font-size: 26rpx;
+  color: #ff7a2e;
+  flex-shrink: 0;
+}
+
+.time-row {
+  display: flex;
+  flex-direction: row;
+  gap: 20rpx;
+}
+
+.time-col {
+  flex: 1;
+  min-width: 0;
+}
+
+.time-picker {
+  display: block;
+  width: 100%;
+}
+
+.time-box {
+  width: 100%;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 10rpx;
+  padding: 22rpx 18rpx;
+  background: #ffffff;
+  border-radius: 8rpx;
+  border: 1rpx solid #e8e8e8;
+  min-height: 80rpx;
+  box-sizing: border-box;
+}
+
+.time-box--readonly {
+  pointer-events: none;
+}
+
+.time-clock {
+  font-size: 30rpx;
+  line-height: 1;
+  flex-shrink: 0;
+  opacity: 0.45;
+}
+
+.time-text {
+  font-size: 26rpx;
+  color: #333333;
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.time-text.is-placeholder {
+  color: #bbbbbb;
+}
+
+.divider {
+  height: 1rpx;
+  background: #eeeeee;
+  margin: 0 -6rpx;
+}
+
+.row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: flex-start;
+  gap: 24rpx;
+  min-height: 100rpx;
+  padding: 8rpx 0;
+}
+
+.row .label {
+  width: 120rpx;
+  min-width: 120rpx;
+}
+
+.row-gender {
+  align-items: center;
+}
+
+.row-remark {
+  align-items: flex-start;
+  padding-top: 20rpx;
+  padding-bottom: 12rpx;
+}
+
+.label-top {
+  padding-top: 12rpx;
+}
+
+.input {
+  flex: 1;
+  min-width: 0;
+  text-align:start;
+  font-size: 28rpx;
+  color: #333333;
+  height: 72rpx;
+  line-height: 72rpx;
+}
+
+.input-ph {
+  color: #bbbbbb;
+}
+
+.gender-group {
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  gap: 48rpx;
+  min-width: 0;
+}
+
+.gender-opt {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 14rpx;
+}
+
+.gender-txt {
+  font-size: 28rpx;
+  color: #666666;
+}
+
+.gender-opt.active .gender-txt {
+  color: #333333;
+}
+
+/* 未选:空心圆;已选:橙圈 + 内点 */
+.gender-dot {
+  width: 32rpx;
+  height: 32rpx;
+  border-radius: 50%;
+  border: 2rpx solid #cccccc;
+  box-sizing: border-box;
+  flex-shrink: 0;
+  background: transparent;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.gender-dot.on {
+  border-color: #fc743d;
+}
+
+.gender-dot-inner {
+  width: 16rpx;
+  height: 16rpx;
+  border-radius: 50%;
+  background: #fc743d;
+}
+
+.remark-wrap {
+  flex: 1;
+  min-width: 0;
+  position: relative;
+}
+
+.textarea {
+  width: 100%;
+  min-height: 176rpx;
+  padding: 20rpx 20rpx 48rpx 0;
+  box-sizing: border-box;
+  font-size: 28rpx;
+  color: #333333;
+  line-height: 1.5;
+  background: #ffffff;
+  border-radius: 8rpx;
+  border: none;
+}
+
+.remark-count {
+  position: absolute;
+  right: 20rpx;
+  bottom: 16rpx;
+  font-size: 22rpx;
+  color: #cccccc;
+}
+
+.footer-spacer {
+  height: 24rpx;
+}
+
+.footer-bar {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  padding: 24rpx 40rpx calc(24rpx + env(safe-area-inset-bottom));
+  background: linear-gradient(180deg, rgba(242, 243, 245, 0) 0%, #f2f3f5 28%);
+  box-sizing: border-box;
+  z-index: 100;
+}
+
+.footer-btn {
+  height: 92rpx;
+  line-height: 92rpx;
+  text-align: center;
+  font-size: 32rpx;
+  font-weight: 500;
+  color: #ffffff;
+  border-radius: 46rpx;
+  background: linear-gradient(90deg, #fcb73f 0%, #fc743d 100%);
+  box-shadow: 0 8rpx 24rpx rgba(252, 116, 61, 0.35);
+}
+
+.footer-btn.hover-active:active {
+  opacity: 0.92;
+}
+
+/* ========== 桌台信息底部弹窗 ========== */
+.table-popup-root {
+  position: fixed;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  z-index: 1000;
+  pointer-events: none;
+}
+
+.table-popup-mask {
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  pointer-events: auto;
+}
+
+.table-popup-sheet {
+  position: absolute;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: #ffffff;
+  border-radius: 24rpx 24rpx 0 0;
+  padding: 28rpx 30rpx 0;
+  box-sizing: border-box;
+  max-height: 72vh;
+  pointer-events: auto;
+  box-shadow: 0 -8rpx 40rpx rgba(0, 0, 0, 0.12);
+}
+
+.table-popup-head {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding-bottom: 28rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.table-popup-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #151515;
+}
+
+.table-popup-close {
+  width: 56rpx;
+  height: 56rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: -8rpx;
+}
+
+.table-popup-close-x {
+  font-size: 44rpx;
+  color: #999999;
+  line-height: 1;
+  font-weight: 300;
+}
+
+.table-popup-close--active .table-popup-close-x,
+.table-popup-close:active .table-popup-close-x {
+  color: #666666;
+}
+
+.table-slot-grid {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  padding: 32rpx 0 8rpx;
+  gap: 20rpx 16rpx;
+}
+
+.table-popup-hint {
+  width: 100%;
+  padding: 24rpx 0 40rpx;
+  font-size: 28rpx;
+  color: #666666;
+  text-align: center;
+}
+
+.table-popup-hint--muted {
+  color: #999999;
+}
+
+.table-slot-tag {
+  width: calc((100% - 32rpx) / 3);
+  box-sizing: border-box;
+  padding: 22rpx 12rpx;
+  background: #f5f5f5;
+  border-radius: 12rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 72rpx;
+}
+
+.table-slot-tag-text {
+  font-size: 26rpx;
+  color: #666666;
+  text-align: center;
+  line-height: 1.35;
+}
+
+.table-popup-safe {
+  height: calc(24rpx + env(safe-area-inset-bottom));
+  min-height: 24rpx;
+}
+</style>

+ 5 - 6
pages/numberOfDiners/index.vue

@@ -45,12 +45,11 @@ const selectDiners = (item) => {
   currentDiners.value = item;
 };
 
-// 手机号授权登录成功:再跳转点餐页
+// 手机号授权登录成功:先进入就餐信息页,再在该页确认后进入点餐
 const handleLoginSuccess = () => {
   if (pendingNavigate.value) {
-    const { tableid, dinersVal } = pendingNavigate.value;
     pendingNavigate.value = null;
-    go(`/pages/orderFood/index?tableid=${tableid}&diners=${dinersVal}`);
+    go('/pages/diningInfo/index');
   }
 };
 
@@ -67,13 +66,13 @@ const toOrderFood = () => {
 
   const token = userStore.getToken || uni.getStorageSync(TOKEN) || '';
   if (!token) {
-    // 未登录:弹出手机号授权一键登录弹窗,登录成功后再跳转
+    // 未登录:弹出手机号授权一键登录弹窗,登录成功后再跳转就餐信息页
     pendingNavigate.value = { tableid, dinersVal };
     showLoginModal.value = true;
     return;
   }
-  uni.reLaunch({
-    url: `/pages/orderFood/index?tableid=${tableid}&diners=${dinersVal}`
+  uni.navigateTo({
+    url: '/pages/diningInfo/index'
   });
 }
 

+ 1 - 23
pages/orderFood/components/CartModal.vue

@@ -31,8 +31,7 @@
                         </view>
                     </view>
 
-                    <!-- 数量选择器(餐具费 0 元时不可修改) -->
-                    <view class="cart-item__actions" :class="{ 'cart-item__actions--disabled': !canEditItemQuantity(item) }">
+                    <view class="cart-item__actions">
                         <view class="action-btn minus" :class="{ disabled: item.quantity === 0 }"
                             @click="handleDecrease(item)" hover-class="hover-active">
                             <image :src="getFileUrl('img/icon/reduce1.png')" mode="aspectFit" class="action-icon"
@@ -97,9 +96,7 @@ const formatPrice = (price) => {
     return Number.isNaN(num) ? '0.00' : num.toFixed(2);
 };
 
-// 菜品图片地址:餐具(id=-1)固定用本地 /static/utensilFee.png,其他取接口 cuisineImage
 const getItemImageSrc = (item) => {
-    if (isTablewareItem(item)) return '/static/utensilFee.png';
     const raw = item?.cuisineImage ?? item?.image ?? item?.images ?? item?.imageUrl ?? item?.pic ?? item?.cover ?? '';
     const url = typeof raw === 'string' ? raw.split(',')[0].trim() : raw;
     return url ? getFileUrl(url) : '';
@@ -119,37 +116,18 @@ const getItemLinePrice = (item) => {
     return qty * unitPrice;
 };
 
-// 餐具(cuisineId 或 id 为 -1)可修改数量(含减至 0);餐具费为 0 元时不允许修改数量
-const isTablewareItem = (item) => {
-    if (!item) return false;
-    const id = item.cuisineId ?? item.id;
-    return Number(id) === -1;
-};
-
-// 该项是否允许修改数量:非餐具允许;餐具仅当单价>0 时允许
-const canEditItemQuantity = (item) => {
-    if (!item) return false;
-    if (!isTablewareItem(item)) return true;
-    const price = getItemPrice(item);
-    return Number(price) > 0;
-};
-
 // 处理关闭
 const handleClose = () => {
     getOpen.value = false;
     emit('close');
 };
 
-// 增加数量(餐具费 0 元时不触发)
 const handleIncrease = (item) => {
-    if (!canEditItemQuantity(item)) return;
     if (item.quantity >= 99) return;
     emit('increase', item);
 };
 
-// 减少数量(餐具费 0 元时不触发)
 const handleDecrease = (item) => {
-    if (!canEditItemQuantity(item)) return;
     if (item && item.quantity > 0) {
         emit('decrease', item);
     }

+ 0 - 8
pages/orderFood/components/FoodCard.vue

@@ -66,14 +66,6 @@ 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;
-});
-
 // 商品图片:与首页 index 一致,取第一张(逗号分隔则截取逗号前第一张),支持对象 { url/path/src },再通过 getFileUrl 转成完整地址
 function firstImage(val) {
   if (val == null || val === '') return '';

+ 125 - 101
pages/orderFood/index.vue

@@ -3,7 +3,10 @@
   <view class="page-wrap">
     <view class="top-fixed">
       <NavBar :title="navTitle" only-home />
-      <view class="top-info">桌号:{{ displayTableNumber }}</view>
+      <view class="top-info">
+        <text class="top-info__item">桌号:{{ displayTableNumber }}</text>
+        <text class="top-info__item">就餐人数:{{ displayDinersCount }}</text>
+      </view>
       <view class="search-box">
         <view class="search-box-inner">
           <image :src="getFileUrl('img/personal/search.png')" mode="widthFix" class="search-icon"></image>
@@ -25,9 +28,13 @@
         </scroll-view>
       </view>
 
-      <!-- 右侧菜品长列表:按分类分段展示,每段标题 + 该分类下菜品 -->
+      <!-- 右侧菜品长列表:按分类分段展示,每段标题 + 该分类下菜品;搜索无结果时展示空状态 -->
       <view class="food-list-wrap">
+        <view v-if="showSearchEmptyState" class="food-search-empty">
+          <image src="/static/no.png" mode="widthFix" class="food-search-empty__img" />
+        </view>
         <scroll-view
+          v-else
           class="food-list"
           scroll-y
           :scroll-into-view="scrollIntoViewId"
@@ -70,7 +77,7 @@
 <script setup>
 import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
 import NavBar from "@/components/NavBar/index.vue";
-import { ref, computed, nextTick, getCurrentInstance } from "vue";
+import { ref, computed, watch, nextTick, getCurrentInstance } from "vue";
 import FoodCard from "./components/FoodCard.vue";
 import BottomActionBar from "./components/BottomActionBar.vue";
 import CouponModal from "./components/CouponModal.vue";
@@ -78,7 +85,7 @@ import CartModal from "./components/CartModal.vue";
 import SelectCouponModal from "./components/SelectCouponModal.vue";
 import { go } from "@/utils/utils.js";
 import { getFileUrl } from "@/utils/file.js";
-import { DiningOrderFood, GetCategoriesWithCuisines, GetStoreDetail, getOrderSseConfig, GetOrderCart, PostOrderCartAdd, PostOrderCartUpdate, PostOrderCartClear, PutOrderCartUpdateTableware, GetUserOwnedCouponList } from "@/api/dining.js";
+import { DiningOrderFood, GetCategoriesWithCuisines, GetStoreDetail, getOrderSseConfig, GetOrderCart, PostOrderCartAdd, PostOrderCartUpdate, PostOrderCartClear, GetUserOwnedCouponList } from "@/api/dining.js";
 import { createSSEConnection } from "@/utils/sse.js";
 
 // 商品图片:取第一张,若为逗号分隔字符串则截取逗号前的第一张
@@ -103,6 +110,13 @@ const displayTableNumber = computed(() => {
   if (tableNumberFetched.value && tableId.value) return tableId.value;
   return '—';
 });
+
+/** 桌号右侧:就餐人数(与选人数页传入 / currentDiners 缓存一致) */
+const displayDinersCount = computed(() => {
+  const d = String(currentDiners.value ?? '').trim();
+  if (!d) return '—';
+  return /人\s*$/.test(d) ? d : `${d}人`;
+});
 let sseRequestTask = null; // 订单 SSE 连接(封装后兼容小程序),页面卸载时需 abort()
 const currentCategoryIndex = ref(0);
 const foodListScrollTop = ref(0);
@@ -115,19 +129,25 @@ const couponModalOpen = ref(false);
 const cartModalOpen = ref(false);
 const selectCouponModalOpen = ref(false);
 const selectCouponViewOnly = ref(false); // true=左下角仅查看,false=购物车内可选
-const fromNumberOfDiners = ref(false); // 是否从选座页(就餐人数页)进入,仅此时才调用 update-tableware 接口
 const discountAmount = ref(12); // 优惠金额,示例数据
 const selectedCouponId = ref(null); // 选中的优惠券ID
 
 // 分类列表(由接口 /store/info/categories 返回后赋值)
 const categories = ref([]);
+/** 无关键词拉菜单成功时的分类快照;搜索无结果时用来保留左侧标签 */
+const lastCategoriesForSidebar = ref([]);
 
 // 菜品列表(由接口 /store/info/cuisines 按分类拉取并合并,每项含 quantity、categoryId)
 const foodList = ref([]);
 // SSE 连接建立后拉取的购物车数据,在 foodList 就绪后合并
 let pendingCartData = null;
-// 购物车接口返回的完整数据:{ items, totalAmount, totalQuantity, tablewareFee },用于按接口格式展示
-const cartData = ref({ items: [], totalAmount: 0, totalQuantity: 0, tablewareFee: 0 });
+// 购物车接口返回的完整数据:{ items, totalAmount, totalQuantity, serviceFee }(已过滤特殊占位项 cuisineId=-1)
+const cartData = ref({ items: [], totalAmount: 0, totalQuantity: 0, serviceFee: 0 });
+
+function filterDishCartItems(arr) {
+  if (!Array.isArray(arr)) return [];
+  return arr.filter((it) => Number(it?.cuisineId ?? it?.id) !== -1);
+}
 
 // 右侧长列表:按分类分段,每段为 { category, cuisines },支持一菜多分类(categoryIds)
 const sectionList = computed(() => {
@@ -144,6 +164,13 @@ const sectionList = computed(() => {
   });
 });
 
+// 已输入搜索关键词且当前无菜品:右侧展示空状态(与接口返回空列表一致)
+const showSearchEmptyState = computed(() => {
+  const kw = (searchKeyword.value ?? '').trim();
+  if (!kw) return false;
+  return (foodList.value?.length ?? 0) === 0;
+});
+
 // 点击左侧分类时滚动到右侧对应段(scroll-into-view 用)
 const scrollIntoViewId = ref('');
 
@@ -159,20 +186,15 @@ const cartList = computed(() => {
   });
 });
 
-// 用于展示的购物车列表:数量>0 的项展示;餐具(id=-1)始终展示(含数量为 0);若接口未返回餐具则补默认项
+// 用于展示的购物车列表:仅菜品(排除 cuisineId/id=-1);数量>0 的项展示
 const displayCartList = computed(() => {
   const apiItems = cartData.value?.items;
   let base = [];
   if (Array.isArray(apiItems) && apiItems.length > 0) {
-    base = apiItems.filter((it) => (Number(it?.quantity) || 0) > 0 || Number(it?.cuisineId ?? it?.id) === -1);
+    base = filterDishCartItems(apiItems).filter((it) => (Number(it?.quantity) || 0) > 0);
   } else {
     base = cartList.value;
   }
-  const hasTableware = base.some((it) => Number(it?.cuisineId ?? it?.id) === -1);
-  if (!hasTableware) {
-    const fee = Number(cartData.value?.tablewareFee) || 0;
-    return [...base, { cuisineId: -1, cuisineName: '餐具费', quantity: 0, unitPrice: fee || 0, subtotalAmount: 0 }];
-  }
   return base;
 });
 
@@ -283,7 +305,8 @@ const sameCategory = (a, b) => String(a ?? '') === String(b ?? '');
 
 // 将购物车数量同步到菜品列表:用购物车接口的 items 按 cuisineId 匹配 foodList 中的菜品并更新 quantity;items 为空时清空所有菜品数量
 const mergeCartIntoFoodList = () => {
-  const cartItems = pendingCartData ?? cartData.value?.items ?? [];
+  const raw = pendingCartData ?? cartData.value?.items ?? [];
+  const cartItems = filterDishCartItems(raw);
   if (!Array.isArray(cartItems)) return;
   if (!foodList.value.length) {
     if (cartItems.length > 0) console.log('购物车已缓存,等 foodList 加载后再同步到菜品列表');
@@ -335,14 +358,13 @@ const fetchAndMergeCart = async (tableid) => {
   if (!tableid) return;
   try {
     const cartRes = await GetOrderCart(tableid);
-    const list = parseCartListFromResponse(cartRes);
+    const list = filterDishCartItems(parseCartListFromResponse(cartRes));
     pendingCartData = list;
-    // 按接口格式绑定:data.items / totalAmount / totalQuantity / tablewareFee
     cartData.value = {
       items: list,
       totalAmount: Number(cartRes?.totalAmount) || 0,
       totalQuantity: Number(cartRes?.totalQuantity) || 0,
-      tablewareFee: Number(cartRes?.tablewareFee ?? cartRes?.tablewareAmount ?? 0) || 0
+      serviceFee: Number(cartRes?.serviceFee ?? cartRes?.serviceCharge ?? 0) || 0
     };
     mergeCartIntoFoodList();
     console.log('购物车接口返回(data 层):', cartRes, '解析条数:', list.length);
@@ -408,7 +430,9 @@ const fetchCategoriesWithCuisines = async (storeId, keyword = '') => {
       const cuisinesRaw = raw?.cuisines ?? raw?.cuisineList ?? [];
       cuisines = Array.isArray(cuisinesRaw) ? cuisinesRaw : [];
     }
-    categories.value = Array.isArray(cats) ? cats : [];
+    const keywordTrim = params.keyword != null && String(params.keyword).trim() !== '' ? String(params.keyword).trim() : '';
+    const nextCats = Array.isArray(cats) ? cats : [];
+    categories.value = nextCats;
     const normalized = cuisines.map((item) => {
       const categoryId = item.categoryId ?? (categories.value[0]?.id ?? categories.value[0]?.categoryId);
       const rawImg = item.images ?? item.cuisineImage ?? item.image ?? item.imageUrl ?? item.pic ?? item.cover ?? '';
@@ -429,6 +453,14 @@ const fetchCategoriesWithCuisines = async (storeId, keyword = '') => {
     });
     foodList.value = Array.from(byId.values());
     mergeCartIntoFoodList();
+    // 全量菜单(无搜索词)成功时更新左侧分类快照
+    if (!keywordTrim && nextCats.length > 0) {
+      lastCategoriesForSidebar.value = nextCats.map((c) => ({ ...c }));
+    }
+    // 搜索无菜品时:接口常把分类滤空,左侧仍展示上次全量分类
+    if (keywordTrim && foodList.value.length === 0 && lastCategoriesForSidebar.value.length > 0) {
+      categories.value = lastCategoriesForSidebar.value.map((c) => ({ ...c }));
+    }
     return true;
   } catch (err) {
     console.error('获取分类与菜品失败:', err);
@@ -506,11 +538,26 @@ const doSearch = async () => {
   if (!storeId) return;
   const keyword = searchKeyword.value?.trim() ?? '';
   const ok = await fetchCategoriesWithCuisines(storeId, keyword);
-  if (ok) {
+  if (ok && foodList.value.length > 0) {
     nextTick(() => scrollToSection(0));
   }
 };
 
+// 搜索框从有内容变为空时,自动拉全量菜单(无需再点键盘「搜索」)
+watch(
+  () => (searchKeyword.value ?? '').trim(),
+  async (trimmed, prevTrimmed) => {
+    if (trimmed !== '') return;
+    if (prevTrimmed === undefined || prevTrimmed === '') return;
+    const storeId = uni.getStorageSync('currentStoreId') || '';
+    if (!storeId) return;
+    const ok = await fetchCategoriesWithCuisines(storeId, '');
+    if (ok && foodList.value.length > 0) {
+      nextTick(() => scrollToSection(0));
+    }
+  }
+);
+
 // 根据展示项(可能来自接口 cuisineId 或 foodList 的 id)找到 foodList 中的菜品
 const findFoodByCartItem = (item) => {
   const id = item?.id ?? item?.cuisineId;
@@ -520,7 +567,7 @@ const findFoodByCartItem = (item) => {
 
 // 同步 cartData:根据 cuisineId 更新 items 中对应项的 quantity、subtotalAmount,并重算 totalAmount、totalQuantity
 const syncCartDataFromFoodList = () => {
-  const items = cartData.value?.items ?? [];
+  const items = filterDishCartItems(cartData.value?.items ?? []);
   if (!items.length) return;
   let totalAmount = 0;
   let totalQuantity = 0;
@@ -536,22 +583,12 @@ const syncCartDataFromFoodList = () => {
   cartData.value = { ...cartData.value, items: nextItems, totalAmount, totalQuantity };
 };
 
-// 判断是否为餐具(cuisineId 或 id 为 -1),餐具可修改数量(含减至 0)
-const isTableware = (item) => {
-  if (!item) return false;
-  const id = item.id ?? item.cuisineId;
-  return Number(id) === -1;
-};
-
 // 更新菜品数量:菜品 id 一致则全部同步为同一数量,触发响应式;新增加入购物车时调接口;并同步 cartData
-// Update 接口返回 400 时不改页面数量和金额(会回滚本地状态);餐具费为 0 元时不允许修改餐具数量
+// Update 接口返回 400 时不改页面数量和金额(会回滚本地状态)
 const updateFoodQuantity = (food, delta) => {
   if (!food) return;
   const id = food.id ?? food.cuisineId;
-  if (Number(id) === -1) {
-    const unitPrice = Number(food?.unitPrice ?? food?.price ?? 0) || 0;
-    if (unitPrice <= 0) return;
-  }
+  if (Number(id) === -1) return;
   const prevQty = food.quantity || 0;
   const nextQty = Math.max(0, prevQty + delta);
   const sameId = (item) =>
@@ -594,18 +631,7 @@ const updateFoodQuantity = (food, delta) => {
 
   if (tableId.value && delta !== 0) {
     const needAdd = delta > 0 && (idx < 0 || nextQty === 1);
-    const isTablewareItem = Number(id) === -1;
-    if (isTablewareItem && fromNumberOfDiners.value) {
-      PutOrderCartUpdateTableware({
-        quantity: nextQty,
-        tableId: tableId.value
-      }).catch((err) => {
-        console.error('更新餐具数量失败:', err);
-        applyQuantity(prevQty);
-      });
-    } else if (isTablewareItem) {
-      // 非选座页进入:餐具仅本地更新,不调 update-tableware
-    } else if (needAdd) {
+    if (needAdd) {
       PostOrderCartAdd({
         cuisineId: id,
         quantity: nextQty,
@@ -744,32 +770,23 @@ const handleCartClose = () => {
 // 清空购物车:调用 /store/order/cart/clear,成功后用接口返回的数据更新购物车
 const handleCartClear = () => {
   const items = cartData.value?.items ?? [];
-  const dishItems = items.filter((it) => Number(it?.cuisineId ?? it?.id) !== -1);
+  const dishItems = filterDishCartItems(items);
 
   const applyClearResult = (res) => {
-    const list = parseCartListFromResponse(res) ?? [];
+    const list = filterDishCartItems(parseCartListFromResponse(res) ?? []);
     pendingCartData = list;
     const totalAmount = Number(res?.totalAmount) || 0;
     const totalQuantity = Number(res?.totalQuantity) || 0;
-    const tablewareFee = Number(res?.tablewareFee ?? res?.tablewareAmount ?? 0) || list.reduce((s, it) => {
-      if (Number(it?.cuisineId ?? it?.id) !== -1) return s;
-      const line = it?.subtotalAmount != null ? Number(it.subtotalAmount) : (Number(it?.quantity) || 0) * (Number(it?.unitPrice ?? it?.price) || 0);
-      return s + line;
-    }, 0);
-    cartData.value = { items: list, totalAmount, totalQuantity, tablewareFee };
+    const serviceFee = Number(res?.serviceFee ?? res?.serviceCharge ?? 0) || 0;
+    cartData.value = { items: list, totalAmount, totalQuantity, serviceFee };
     mergeCartIntoFoodList();
     cartModalOpen.value = false;
     uni.showToast({ title: '已清空购物车', icon: 'success' });
   };
 
   if (!tableId.value) {
-    const tablewareItems = items.filter((it) => Number(it?.cuisineId ?? it?.id) === -1);
-    const utensilFee = tablewareItems.reduce((sum, it) => {
-      const line = it?.subtotalAmount != null ? Number(it.subtotalAmount) : (Number(it?.quantity) || 0) * (Number(it?.unitPrice ?? it?.price) || 0);
-      return sum + line;
-    }, 0);
-    const utensilQty = tablewareItems.reduce((s, i) => s + (Number(i?.quantity) || 0), 0);
-    applyClearResult({ items: tablewareItems, totalAmount: utensilFee, totalQuantity: utensilQty, tablewareFee: utensilFee });
+    foodList.value = foodList.value.map((f) => ({ ...f, quantity: 0 }));
+    applyClearResult({ items: [], totalAmount: 0, totalQuantity: 0, serviceFee: 0 });
     return;
   }
   if (dishItems.length === 0) {
@@ -784,44 +801,25 @@ const handleCartClear = () => {
     });
 };
 
-// 从购物车项中计算餐具费: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 items = displayCartList.value ?? [];
-  const hasDish = items.some((it) => Number(it?.cuisineId ?? it?.id) !== -1);
-  if (!hasDish && items.length > 0) {
-    uni.showToast({ title: '请至少选择一道菜品', icon: 'none' });
-    return;
-  }
   if (items.length === 0) {
     uni.showToast({ title: '请先选择菜品', icon: 'none' });
     return;
   }
-  const fromApi = Number(cartData.value?.tablewareFee) || 0;
-  const fromItems = getTablewareFeeFromCart();
-  const utensilFee = fromApi > 0 ? fromApi : fromItems;
   const totalAmount = displayTotalAmount.value;
-  const dishTotal = Math.max(0, Number(totalAmount) - Number(utensilFee));
+  const serviceFeeVal = Number(cartData.value?.serviceFee) || 0;
   const cartPayload = {
     list: displayCartListWithTags.value,
     totalAmount,
-    dishTotal,
+    dishTotal: totalAmount,
+    serviceFee: serviceFeeVal,
     totalQuantity: displayTotalQuantity.value,
     tableId: tableId.value,
     tableNumber: tableNumber.value,
     diners: currentDiners.value,
-    utensilFee: Number(utensilFee) || 0
+    storeId: uni.getStorageSync('currentStoreId') || ''
   };
   uni.setStorageSync('placeOrderCart', JSON.stringify(cartPayload));
   const query = [];
@@ -837,19 +835,10 @@ onLoad(async (options) => {
   const diners = options.diners || '';
   tableId.value = tableid;
   currentDiners.value = diners;
-  fromNumberOfDiners.value = diners !== '' && diners != null; // 仅从选座页进入时带 diners 参数
   if (tableid) uni.setStorageSync('currentTableId', tableid);
   if (diners) uni.setStorageSync('currentDiners', diners);
   console.log('点餐页接收参数 - 桌号(tableid):', tableid, '就餐人数(diners):', diners);
 
-  // 更新餐具数量:仅从选座页进入时调用 update-tableware 接口
-  if (fromNumberOfDiners.value && tableid) {
-    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));
@@ -865,12 +854,14 @@ onLoad(async (options) => {
         if (msg.event === 'cart_update' && msg.data) {
           try {
             const payload = typeof msg.data === 'string' ? JSON.parse(msg.data) : msg.data;
-            const items = payload?.items ?? [];
+            const items = filterDishCartItems(payload?.items ?? []);
             cartData.value = {
               items: Array.isArray(items) ? items : [],
               totalAmount: Number(payload?.totalAmount) || 0,
               totalQuantity: Number(payload?.totalQuantity) || 0,
-              tablewareFee: Number(payload?.tablewareFee ?? payload?.tablewareAmount ?? 0) || (cartData.value?.tablewareFee ?? 0)
+              serviceFee:
+                Number(payload?.serviceFee ?? payload?.serviceCharge ?? 0) ||
+                (cartData.value?.serviceFee ?? 0)
             };
             pendingCartData = null;
             mergeCartIntoFoodList();
@@ -902,9 +893,6 @@ onLoad(async (options) => {
       tableNumber.value = data?.tableNumber ?? data?.tableNo ?? '';
       tableNumberFetched.value = true;
       storeName.value = data?.storeName ?? data?.storeInfo?.storeName ?? '';
-      // 餐具费:点餐页接口可能返回,若购物车未带则用此处兜底
-      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 ?? '';
@@ -979,14 +967,24 @@ onUnload(() => {
 }
 
 .top-info {
-  font-size: 28rpx;
-  color: #888888;
-  text-align: center;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  flex-wrap: wrap;
+  gap: 48rpx;
   width: 100%;
-  padding: 20rpx 0;
+  padding: 16rpx 30rpx 12rpx;
+  box-sizing: border-box;
   background-color: #fff;
 }
 
+.top-info__item {
+  font-size: 26rpx;
+  color: #aaaaaa;
+  line-height: 1.4;
+}
+
 .search-box {
   width: 90%;
   margin-left: 5%;
@@ -1079,6 +1077,32 @@ onUnload(() => {
   overflow: hidden;
   margin: 0 20rpx;
 }
+
+.food-search-empty {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 48rpx 32rpx 80rpx;
+  box-sizing: border-box;
+  background-color: #f7f9fa;
+}
+
+.food-search-empty__img {
+  width: 360rpx;
+  max-width: 80%;
+  margin-bottom: 32rpx;
+}
+
+.food-search-empty__text {
+  font-size: 28rpx;
+  color: #999999;
+  text-align: center;
+  line-height: 1.5;
+}
+
 .food-list {
   flex: 1;
   min-height: 0;

+ 33 - 18
pages/orderInfo/orderDetail.vue

@@ -14,7 +14,6 @@
       <view class="card-content">
         <view class="info-food">
           <view v-for="(item, index) in displayFoodList" :key="item.id || index" class="food-item">
-            <!-- 菜品图片:餐具用本地图 -->
             <image :src="getItemImage(item)" mode="aspectFill" class="food-item__image"></image>
 
             <!-- 菜品信息 -->
@@ -46,9 +45,9 @@
           <view class="info-item-label">菜品总价</view>
           <view class="info-item-value">¥{{ priceDetail.dishTotal }}</view>
         </view>
-        <view class="info-item">
-          <view class="info-item-label">餐具费</view>
-          <view class="info-item-value">¥{{ priceDetail.tablewareFee }}</view>
+        <view v-if="priceDetail.serviceFeeAmount > 0" class="info-item">
+          <view class="info-item-label">服务费</view>
+          <view class="info-item-value">¥{{ priceDetail.serviceFee }}</view>
         </view>
         <!-- 已完成订单:优惠金额(满减用 nominalValue;折扣按菜品总价与折扣率计算,与结算页选券逻辑一致) -->
         <view
@@ -152,7 +151,8 @@ const orderDetail = ref({
 // 价格明细(两位小数)
 const priceDetail = ref({
   dishTotal: '0.00',
-  tablewareFee: '0.00',
+  serviceFee: '0.00',
+  serviceFeeAmount: 0,
   couponDiscount: '0.00',
   discountAmount: 0,
   couponName: '',
@@ -210,14 +210,13 @@ const completedOrderDiscountDisplay = computed(() => {
   return Math.max(0, Number(p.discountAmount) || 0);
 });
 
-// 合计 / 已完成实付展示:已完成 = 菜品总价 + 餐具费 − 优惠(与结算页 payAmount 一致);未完成 = 接口应付 total
+// 合计 / 已完成实付展示:已完成 = 菜品总价 − 优惠(与结算页 payAmount 一致);未完成 = 接口应付 total
 const orderSummaryDisplayAmount = computed(() => {
   const p = priceDetail.value;
   if (orderDetail.value.orderStatus === 3) {
     const dish = Number(p.dishTotal) || 0;
-    const utensil = Number(p.tablewareFee) || 0;
     const discount = completedOrderDiscountDisplay.value;
-    return Math.max(0, Math.round((dish + utensil - discount) * 100) / 100);
+    return Math.max(0, Math.round((dish - discount) * 100) / 100);
   }
   return Number(p.total) || 0;
 });
@@ -225,8 +224,10 @@ const orderSummaryDisplayAmount = computed(() => {
 // 菜品列表(接口订单明细)
 const foodList = ref([]);
 
-// 菜品详情展示:包含所有项(含餐具 id/cuisineId === -1)
-const displayFoodList = computed(() => foodList.value ?? []);
+// 菜品详情展示(排除特殊占位 id=-1)
+const displayFoodList = computed(() =>
+  (foodList.value ?? []).filter((it) => Number(it?.id ?? it?.cuisineId) !== -1)
+);
 
 // 取第一张图:逗号分隔取首段,数组取首项,对象取 url/path/src(与 orderInfo/index、FoodCard 一致)
 function firstImage(val) {
@@ -244,9 +245,7 @@ function firstImage(val) {
   return str ? str.split(/[,,]/)[0].trim() : '';
 }
 
-// 菜品图片:餐具(id/cuisineId=-1)用本地图;其他取首图并 getFileUrl
 function getItemImage(item) {
-  if (Number(item?.id ?? item?.cuisineId) === -1) return '/static/utensilFee.png';
   const raw = item?.image ?? item?.cuisineImage ?? item?.imageUrl ?? item?.pic ?? item?.cover ?? item?.images ?? '';
   const url = firstImage(raw) || (typeof raw === 'string' ? raw.split(/[,,]/)[0]?.trim() : '');
   if (url && typeof url === 'string' && (url.startsWith('http') || url.startsWith('//'))) return url;
@@ -284,9 +283,15 @@ function normalizeOrderItem(item) {
   const rawImg = item?.images ?? item?.image ?? item?.cuisineImage ?? item?.imageUrl ?? item?.pic ?? item?.cover ?? '';
   const imageUrl = firstImage(rawImg) || (typeof rawImg === 'string' ? rawImg.split(/[,,]/)[0]?.trim() : '') || 'img/icon/shop.png';
   return {
-    id: item?.id ?? item?.cuisineId,
-    name: item?.cuisineName ?? item?.name ?? '',
-    price: item?.totalPrice ?? item?.unitPrice ?? item?.price ?? 0,
+    id: item?.id ?? item?.cuisineId ?? item?.orderItemId ?? item?.skuId,
+    name:
+      item?.cuisineName ??
+      item?.name ??
+      item?.goodsName ??
+      item?.skuName ??
+      item?.productName ??
+      '',
+    price: item?.totalPrice ?? item?.unitPrice ?? item?.price ?? item?.salePrice ?? 0,
     image: imageUrl,
     quantity: item?.quantity ?? 1,
     tags: normalizeTags(rawTags)
@@ -313,16 +318,18 @@ function applyOrderData(data) {
   };
   const couponInfo = raw?.couponInfo ?? raw?.coupon ?? {};
   const dishTotal = raw?.totalAmount ?? raw?.dishTotal ?? raw?.orderAmount ?? raw?.foodAmount ?? 0;
-  const tablewareFee = raw?.tablewareFee ?? raw?.tablewareAmount ?? 0;
   const couponDiscount = raw?.couponAmount ?? raw?.couponDiscount ?? 0;
   const discountAmountVal = Number(raw?.discountAmount ?? raw?.couponAmount ?? raw?.couponDiscount ?? 0) || 0;
   const total = raw?.payAmount ?? raw?.totalAmount ?? raw?.totalPrice ?? 0;
+  const serviceFeeRaw =
+    Number(raw?.serviceFee ?? raw?.serviceCharge ?? raw?.tablewareFee ?? 0) || 0;
   const rawDiscountRate =
     raw?.discountRate ?? couponInfo?.discountRate ?? raw?.coupon?.discountRate;
   const discountRateVal = rawDiscountRate != null && rawDiscountRate !== '' ? (Number(rawDiscountRate) || 0) / 10 : null;
   priceDetail.value = {
     dishTotal: formatPrice(dishTotal),
-    tablewareFee: formatPrice(tablewareFee),
+    serviceFee: formatPrice(serviceFeeRaw),
+    serviceFeeAmount: serviceFeeRaw,
     couponDiscount: formatPrice(couponDiscount),
     discountAmount: discountAmountVal,
     couponName: raw?.couponName ?? couponInfo?.couponName ?? couponInfo?.name ?? '',
@@ -331,7 +338,15 @@ function applyOrderData(data) {
     discountRate: discountRateVal,
     total: formatPrice(total)
   };
-  const list = raw?.orderItemList ?? raw?.orderItems ?? raw?.items ?? raw?.detailList ?? [];
+  const list =
+    raw?.orderItemList ??
+    raw?.orderItems ??
+    raw?.orderLines ??
+    raw?.orderLineList ??
+    raw?.detailList ??
+    raw?.order?.orderItemList ??
+    raw?.order?.orderItems ??
+    (Array.isArray(raw?.items) ? raw.items : []);
   foodList.value = (Array.isArray(list) ? list : []).map(normalizeOrderItem);
 }
 

+ 81 - 20
pages/placeOrder/index.vue

@@ -14,7 +14,7 @@
         </view>
         <view class="info-item">
           <view class="info-item-label">用餐人数</view>
-          <view class="info-item-value">{{ orderInfo.diners || '—' }}</view>
+          <view class="info-item-value">{{ orderInfo.diners || '—' }}</view>
         </view>
         <view class="info-item">
           <view class="info-item-label">联系电话</view>
@@ -79,8 +79,8 @@
           <view class="info-item-value">¥{{ formatPrice(dishTotal) }}</view>
         </view>
         <view class="info-item">
-          <view class="info-item-label">餐具费</view>
-          <view class="info-item-value">¥{{ formatPrice(orderInfo.utensilFee ?? 0) }}</view>
+          <view class="info-item-label">服务费</view>
+          <view class="info-item-value">¥{{ formatPrice(orderInfo.serviceFee ?? 0) }}</view>
         </view>
 
         <view class="price-line">
@@ -116,7 +116,7 @@ const orderInfo = ref({
   remark: '',
   totalAmount: 0,
   dishTotal: null,
-  utensilFee: 0,
+  serviceFee: 0,
   discountAmount: 0,
   payAmount: 0,
   couponId: null
@@ -125,26 +125,27 @@ const orderInfo = ref({
 // 菜品列表(从购物车带过来)
 const foodList = ref([]);
 
-// 菜品清单展示:排除餐具(id/cuisineId === -1)
+// 菜品清单展示:排除特殊占位项(id/cuisineId === -1)
 const displayFoodList = computed(() => {
   return (foodList.value ?? []).filter((it) => Number(it?.id ?? it?.cuisineId) !== -1);
 });
 
-// 菜品总价(不含餐具费):仅按菜品清单中非餐具项汇总,不包含餐具费
+// 菜品成交价小计(元,不含服务费):与点餐页购物车、服务费估算 goodsSubtotal 一致
 const dishTotal = computed(() => {
   const list = displayFoodList.value ?? [];
   const sum = list.reduce((s, it) => {
+    if (it?.subtotalAmount != null && it.subtotalAmount !== '') {
+      return s + (Number(it.subtotalAmount) || 0);
+    }
     const qty = Number(it?.quantity) || 0;
-    const price = Number(it?.unitPrice ?? it?.price ?? it?.totalPrice ?? it?.salePrice ?? 0) || 0;
-    return s + qty * price;
+    const unitPrice =
+      Number(it?.unitPrice ?? it?.price ?? it?.salePrice ?? it?.totalPrice ?? 0) || 0;
+    return s + qty * unitPrice;
   }, 0);
-  return Math.max(0, sum);
+  return Math.max(0, Math.round(sum * 100) / 100);
 });
 
-// 菜品图片:餐具(id/cuisineId=-1)固定用本地 /static/utensilFee.png,其他取接口图
 function getItemImage(item) {
-  const id = item?.id ?? item?.cuisineId;
-  if (Number(id) === -1) return '/static/utensilFee.png';
   const raw = item?.cuisineImage ?? item?.image ?? item?.imageUrl ?? '';
   const url = typeof raw === 'string' ? raw.split(',')[0].trim() : raw;
   return url ? getFileUrl(url) : '';
@@ -161,13 +162,62 @@ function formatPrice(price) {
   return Number.isNaN(num) ? '0.00' : num.toFixed(2);
 }
 
-// 根据菜品总价(不含餐具)、餐具费计算应付金额(优惠券在结算页选择,确认订单页不选券
+// 应付金额 = 菜品总价 + 服务费(优惠券在结算页选择
 const updatePayAmount = () => {
   const dish = dishTotal.value;
-  const utensil = Number(orderInfo.value.utensilFee) || 0;
-  orderInfo.value.payAmount = Math.max(0, dish + utensil);
+  const fee = Number(orderInfo.value.serviceFee) || 0;
+  orderInfo.value.payAmount = Math.max(0, dish + fee);
+  orderInfo.value.totalAmount = orderInfo.value.payAmount;
 };
 
+/** 解析 /service-fee/estimate 的 data:顶层 serviceFee 或 items[].amount 汇总(与结算页接口结构一致) */
+function parseServiceFeeFromEstimate(res) {
+  if (res == null) return 0;
+  if (typeof res === 'number' && Number.isFinite(res)) return Math.max(0, res);
+  if (typeof res !== 'object') return 0;
+  const top = Number(
+    res.serviceFee ?? res.estimatedServiceFee ?? res.fee ?? res.amount ?? NaN
+  );
+  if (Number.isFinite(top) && top > 0) return top;
+  const items = Array.isArray(res.items) ? res.items : [];
+  const sumItems = items.reduce(
+    (s, it) => s + (Number(it?.amount ?? it?.fee ?? it?.serviceFee ?? 0) || 0),
+    0
+  );
+  if (sumItems > 0) return sumItems;
+  if (Number.isFinite(top)) return Math.max(0, top);
+  return 0;
+}
+
+/** 加载时按门店/桌台/人数/菜品小计拉取服务端服务费估算 */
+async function fetchServiceFeeEstimate(storeIdFromCart) {
+  const storeId =
+    (storeIdFromCart != null && String(storeIdFromCart).trim() !== '' ? String(storeIdFromCart).trim() : '') ||
+    uni.getStorageSync('currentStoreId') ||
+    '';
+  const tableId =
+    (orderInfo.value.tableId != null && String(orderInfo.value.tableId).trim() !== ''
+      ? String(orderInfo.value.tableId).trim()
+      : '') || String(uni.getStorageSync('currentTableId') || '').trim();
+  if (!storeId || !tableId) return;
+  const rawDiners = Number(orderInfo.value.diners);
+  const dinerCount = Number.isFinite(rawDiners) && rawDiners > 0 ? Math.floor(rawDiners) : 1;
+  const goodsSubtotal = Number(dishTotal.value.toFixed(2));
+  try {
+    const res = await diningApi.GetServiceFeeEstimate({
+      storeId,
+      tableId,
+      dinerCount,
+      goodsSubtotal
+    });
+    const fee = parseServiceFeeFromEstimate(res);
+    orderInfo.value.serviceFee = fee;
+    updatePayAmount();
+  } catch (e) {
+    console.warn('服务费估算失败:', e);
+  }
+}
+
 // 将 tags 统一为 [{ text, type }] 格式
 function normalizeTags(raw) {
   if (raw == null) return [];
@@ -206,7 +256,10 @@ function normalizeCartItem(item) {
 }
 
 const handleConfirmOrder = async () => {
-  const tableId = orderInfo.value.tableId;
+  const tableId =
+    (orderInfo.value.tableId != null && String(orderInfo.value.tableId).trim() !== ''
+      ? String(orderInfo.value.tableId).trim()
+      : '') || String(uni.getStorageSync('currentTableId') || '').trim();
   const contactPhone = orderInfo.value.contactPhone ?? '';
   const dinerCount = Number(orderInfo.value.diners) || 0;
   if (!tableId) {
@@ -220,14 +273,15 @@ const handleConfirmOrder = async () => {
   const remark = (orderInfo.value.remark ?? '').trim().slice(0, 30);
   uni.showLoading({ title: '提交中...' });
   try {
-    const tablewareFee = Number(orderInfo.value.utensilFee) || 0;
+    const serviceFeeVal = Number(orderInfo.value.serviceFee) || 0;
     const payAmount = Number((Number(orderInfo.value.payAmount) ?? 0).toFixed(2));
     const totalAmount = Number((Number(dishTotal.value) ?? 0).toFixed(2));
     const createParams = {
       tableId,
       contactPhone,
       totalAmount,
-      tablewareFee,
+      tablewareFee: 0,
+      serviceFee: serviceFeeVal,
       payAmount,
       dinerCount: dinerCount || undefined,
       immediatePay: 0,
@@ -268,6 +322,7 @@ onLoad((options) => {
   const userStore = useUserStore();
   const contactPhone = userStore.getUserInfo?.phone ?? userStore.getUserInfo?.contactPhone ?? userStore.getUserInfo?.mobile ?? '';
   orderInfo.value.contactPhone = contactPhone;
+  let cartStoreId = '';
   const raw = uni.getStorageSync('placeOrderCart');
   if (raw) {
     try {
@@ -278,17 +333,23 @@ onLoad((options) => {
       if (data.tableNumber != null) orderInfo.value.tableNumber = data.tableNumber;
       if (data.diners != null) orderInfo.value.diners = data.diners;
       if (data.remark != null) orderInfo.value.remark = data.remark;
+      if (data.storeId != null && data.storeId !== '') cartStoreId = String(data.storeId);
       const total = Number(data.totalAmount) || 0;
       orderInfo.value.totalAmount = total;
       if (data.dishTotal != null && data.dishTotal !== '') orderInfo.value.dishTotal = Number(data.dishTotal) || 0;
-      const fee = data.utensilFee ?? data.tablewareFee;
-      if (fee != null && fee !== '') orderInfo.value.utensilFee = Number(fee) || 0;
+      orderInfo.value.serviceFee =
+        Number(data.serviceFee ?? data.serviceCharge ?? data.utensilFee ?? data.tablewareFee ?? 0) ||
+        0;
       updatePayAmount();
     } catch (e) {
       console.error('解析购物车数据失败:', e);
+      orderInfo.value.serviceFee = 0;
     }
+  } else {
+    orderInfo.value.serviceFee = 0;
   }
   if (orderInfo.value.tableId) uni.setStorageSync('currentTableId', String(orderInfo.value.tableId));
+  fetchServiceFeeEstimate(cartStoreId);
   // 不再主动调用 userOwnedByStore,优惠券在点击「优惠券」行打开弹窗时再拉取
 });
 

BIN
static/no.png