瀏覽代碼

修改购物车逻辑

sunshibo 1 月之前
父節點
當前提交
62332785b3

+ 4 - 4
api/dining.js

@@ -97,15 +97,15 @@ function getUserOwnedCouponListImpl(params) {
 }
 export const GetUserOwnedCouponList = getUserOwnedCouponListImpl;
 
-// 用户优惠券列表(GET /dining/coupon/getUserCouponList,入参 tabType、page、size、phoneId 当前登录人手机号前拼接 user_
+// 用户优惠券列表(GET /dining/coupon/getUserCouponList,入参 storeId、tabType、page、size)
 export const GetUserCouponList = (params) =>
   api.get({
     url: '/dining/coupon/getUserCouponList',
     params: {
+      storeId: params?.storeId,
       tabType: params?.tabType ?? 0,
       page: params?.page ?? 1,
-      size: params?.size ?? 20,
-      phoneId: params?.phoneId
+      size: params?.size ?? 20
     }
   });
 
@@ -166,7 +166,7 @@ function postOrderSettlementUnlockImpl(params) {
 }
 export const PostOrderSettlementUnlock = postOrderSettlementUnlockImpl;
 
-// 创建订单(POST /store/order/create,入参 dto:tableId、contactPhone、couponId、discountAmount 优惠金额、tablewareFee 餐具费、payAmount 实付金额(菜品总价-优惠金额+餐具费)、dinerCount、immediatePay、remark)
+// 创建订单(POST /store/order/create,入参 dto:tableId、contactPhone、totalAmount 菜品总价、couponId、discountAmount 优惠金额、tablewareFee 餐具费、payAmount 实付金额、dinerCount、immediatePay、remark)
 export const PostOrderCreate = (dto) =>
   api.post({ url: '/store/order/create', params: dto });
 

+ 8 - 9
pages/checkout/index.vue

@@ -26,14 +26,14 @@
       </view>
     </view>
 
-    <view class="card" v-if="foodList.length > 0">
+    <view class="card" v-if="displayFoodList.length > 0">
       <view class="card-header">
         <view class="tag"></view>
         <view class="card-header-title">菜品清单</view>
       </view>
       <view class="card-content">
         <view class="info-food">
-          <view v-for="(item, index) in foodList" :key="item.id || index" class="food-item">
+          <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>
             <view class="food-item__info">
               <view class="food-item__name">{{ item.name }}</view>
@@ -125,15 +125,13 @@ const orderInfo = ref({
 
 const foodList = ref([]);
 
-// 菜品总价(不含餐具费):优先 API 的 dishTotal,否则 totalAmount - utensilFee
+// 菜品清单展示:包含所有项(含餐具 id/cuisineId === -1)
+const displayFoodList = computed(() => foodList.value ?? []);
+
+// 菜品总价:绑定 totalAmount
 const dishTotal = computed(() => {
-  const fromApi = orderInfo.value.dishTotal;
-  if (fromApi != null && fromApi !== '' && !Number.isNaN(Number(fromApi))) {
-    return Math.max(0, Number(fromApi));
-  }
   const total = Number(orderInfo.value.totalAmount) || 0;
-  const utensil = Number(orderInfo.value.utensilFee) || 0;
-  return Math.max(0, total - utensil);
+  return Math.max(0, total);
 });
 
 // 优惠券展示:满减券显示 nominalValue+元,折扣券显示 discountRate+折,否则显示 couponName 或 已使用优惠券
@@ -158,6 +156,7 @@ function formatPrice(price) {
 }
 
 function getItemImage(item) {
+  if (Number(item?.id ?? item?.cuisineId) === -1) return '/static/utensilFee.png';
   const raw = item?.image ?? item?.cuisineImage ?? item?.imageUrl ?? '';
   if (typeof raw === 'string' && (raw.startsWith('http') || raw.startsWith('//'))) return raw;
   return getFileUrl(raw || 'img/icon/shop.png');

+ 1 - 30
pages/coupon/components/RulesModal.vue

@@ -48,13 +48,6 @@
           <text class="rules-value">{{ couponData?.description || '周一、周五不可用' }}</text>
         </view>
       </view>
-
-      <!-- 底部按钮 -->
-      <view class="rules-modal__footer">
-        <view class="use-btn" hover-class="hover-active" @click="handleUse">
-          去使用
-        </view>
-      </view>
     </view>
   </BasicModal>
 </template>
@@ -82,7 +75,7 @@ const props = defineProps({
   }
 });
 
-const emit = defineEmits(['update:open', 'use']);
+const emit = defineEmits(['update:open']);
 
 const getOpen = computed({
   get: () => props.open,
@@ -93,12 +86,6 @@ const getOpen = computed({
 const handleClose = () => {
   getOpen.value = false;
 };
-
-// 使用优惠券
-const handleUse = () => {
-  emit('use', props.couponData);
-  getOpen.value = false;
-};
 </script>
 
 <style lang="scss" scoped>
@@ -292,22 +279,6 @@ const handleUse = () => {
     }
   }
 
-  // 底部按钮
-  &__footer {
-    .use-btn {
-      width: 100%;
-      height: 88rpx;
-      background: linear-gradient(135deg, #FF8A57 0%, #F47D1F 100%);
-      border-radius: 44rpx;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      font-size: 32rpx;
-      font-weight: bold;
-      color: #FFFFFF;
-      letter-spacing: 2rpx;
-    }
-  }
 }
 
 .hover-active {

+ 9 - 20
pages/coupon/index.vue

@@ -59,7 +59,7 @@
     </scroll-view>
 
     <!-- 使用规则弹窗 -->
-    <RulesModal v-model:open="showRulesModal" :couponData="selectedCoupon" @use="handleUseCoupon" />
+    <RulesModal v-model:open="showRulesModal" :couponData="selectedCoupon" />
   </view>
 </template>
 
@@ -71,7 +71,7 @@ import { getFileUrl } from "@/utils/file.js";
 import RulesModal from "./components/RulesModal.vue";
 import * as diningApi from "@/api/dining.js";
 
-// 标签页数据
+// 标签页:0未使用 1即将过期 2已使用 3已过期
 const tabs = ['未使用', '即将过期', '已使用', '已过期'];
 const currentTab = ref(0);
 
@@ -120,7 +120,7 @@ function normalizeCouponItem(raw) {
   };
 }
 
-// 根据当前标签页展示的优惠券(接口按 tabType 分页返回,直接使用)
+// 优惠券列表
 const filteredCoupons = computed(() => couponList.value);
 
 // 切换标签页
@@ -129,17 +129,13 @@ const handleTabChange = (index) => {
   fetchCouponList();
 };
 
-// 拉取优惠券列表
+// 拉取优惠券列表(coupon/getUserCouponList)
 const fetchCouponList = async () => {
   loading.value = true;
   try {
-    const res = await diningApi.GetUserCouponList({
-      tabType: currentTab.value, // 0未使用 1即将过期 2已使用 3已过期
-      page: 1,
-      size: 20
-    });
-    const raw = res?.data ?? res ?? {};
-    const list = raw?.records ?? raw?.list ?? (Array.isArray(res) ? res : []);
+    const storeId = uni.getStorageSync('currentStoreId') || '';
+    const res = await diningApi.GetUserCouponList({ storeId, tabType: currentTab.value, page: 1, size: 20 });
+    const list = Array.isArray(res) ? res : (res?.data ?? res?.records ?? res?.list ?? []);
     const arr = Array.isArray(list) ? list : [];
     couponList.value = arr.map(normalizeCouponItem).filter(Boolean);
   } catch (err) {
@@ -153,18 +149,11 @@ const fetchCouponList = async () => {
 
 // 获取优惠券卡片样式类
 const getCouponCardClass = (coupon) => {
-  if (coupon.status === 2) return 'coupon-card--used';
-  if (coupon.status === 3) return 'coupon-card--expired';
+  if (coupon?.status === 2) return 'coupon-card--used';
+  if (coupon?.status === 3) return 'coupon-card--expired';
   return '';
 };
 
-// 使用优惠券
-const handleUseCoupon = (coupon) => {
-  console.log('使用优惠券:', coupon);
-  // 跳转到点餐页面
-  go('/pages/orderFood/index');
-};
-
 // 查看使用规则
 const handleShowRules = (coupon) => {
   selectedCoupon.value = {

+ 63 - 4
pages/launch/index.vue

@@ -13,13 +13,72 @@ import { TOKEN } from '@/settings/enums.js';
 
 const userStore = useUserStore();
 
-async function doRedirect() {
-  const tableid = uni.getStorageSync('currentTableId') || '';
+/** 从 scene 解析 storeId、tableId(与 App.vue parseSceneToStoreTable 逻辑一致) */
+function parseSceneToStoreTable(sceneStr) {
+  const trim = (v) => (v == null ? '' : String(v).trim());
+  let storeId = '';
+  let tableId = '';
+  if (!sceneStr) return { storeId, tableId };
+  let decoded = String(sceneStr).trim();
+  try { decoded = decodeURIComponent(decoded); } catch (_) {}
+  const parseKv = (text) => {
+    text.split('&').forEach((pair) => {
+      const eq = pair.indexOf('=');
+      if (eq > 0) {
+        const k = pair.substring(0, eq).trim().toLowerCase();
+        const v = pair.substring(eq + 1).trim();
+        if (['s', 'storeid', 'store_id'].includes(k)) storeId = v;
+        if (['t', 'tableid', 'table_id', 'tableno', 'table'].includes(k)) tableId = v;
+      }
+    });
+  };
+  try {
+    if (decoded.startsWith('{') && decoded.endsWith('}')) {
+      const obj = JSON.parse(decoded);
+      return { storeId: trim(obj?.storeId ?? obj?.store_id ?? obj?.s ?? ''), tableId: trim(obj?.tableId ?? obj?.table_id ?? obj?.tableid ?? obj?.t ?? obj?.tableNo ?? obj?.table ?? '') };
+    }
+    if (/^\d+_\d+$/.test(decoded)) {
+      const [s, t] = decoded.split('_');
+      return { storeId: trim(s), tableId: trim(t) };
+    }
+    if (/^\d+$/.test(decoded)) return { storeId: '', tableId: decoded };
+    parseKv(decoded);
+  } catch (_) {}
+  return { storeId: trim(storeId), tableId: trim(tableId) };
+}
+
+/** 从启动参数/query 中解析 tableid、storeId,兼容微信小程序 scene、query 等 */
+function getIdsFromOptions(options) {
+  if (!options || typeof options !== 'object') return { tableid: '', storeId: '' };
+  const q = options.query || options;
+  const directTable = q.tableId ?? q.tableid ?? q.table_id ?? q.t ?? '';
+  if (directTable) {
+    const directStore = q.storeId ?? q.storeid ?? q.s ?? '';
+    return { tableid: String(directTable).trim(), storeId: String(directStore || '').trim() };
+  }
+  const scene = q.scene ?? q.q ?? '';
+  if (scene) {
+    const { storeId: s, tableId: t } = parseSceneToStoreTable(scene);
+    return { tableid: t, storeId: s };
+  }
+  return { tableid: '', storeId: '' };
+}
+
+async function doRedirect(options = {}) {
+  let tableid = uni.getStorageSync('currentTableId') || '';
+  if (!tableid) {
+    const { tableid: t, storeId: s } = getIdsFromOptions(options);
+    tableid = t;
+    if (tableid) uni.setStorageSync('currentTableId', tableid);
+    if (s) uni.setStorageSync('currentStoreId', s);
+  }
   if (!tableid) {
+    console.log('[launch] 无 tableid,跳转首页');
     uni.reLaunch({ url: '/pages/index/index' });
     return;
   }
   try {
+    console.log('[launch] 调用 GetTableDiningStatus, tableid:', tableid);
     const res = await diningApi.GetTableDiningStatus(tableid);
     const raw = (res && typeof res === 'object') ? res : {};
     // 兼容多种返回:{ inDining: true }、{ data: { inDining: true } }、直接返回 true
@@ -52,8 +111,8 @@ async function doRedirect() {
   }
 }
 
-onLoad(() => {
-  doRedirect();
+onLoad((options) => {
+  doRedirect(options);
 });
 </script>
 

+ 6 - 13
pages/orderFood/components/CartModal.vue

@@ -31,9 +31,9 @@
                         </view>
                     </view>
 
-                    <!-- 数量选择器(餐具 cuisineId 为 -1 时不可修改) -->
+                    <!-- 数量选择器 -->
                     <view class="cart-item__actions">
-                        <view class="action-btn minus" :class="{ disabled: item.quantity === 0 || isTablewareItem(item) }"
+                        <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"
                                 v-show="item.quantity == 0"></image>
@@ -41,7 +41,7 @@
                                 v-show="item.quantity != 0"></image>
                         </view>
                         <view class="quantity">{{ item.quantity || 0 }}</view>
-                        <view class="action-btn plus" :class="{ disabled: isTablewareItem(item) }" @click="handleIncrease(item)" hover-class="hover-active">
+                        <view class="action-btn plus" @click="handleIncrease(item)" hover-class="hover-active">
                             <image :src="getFileUrl('img/icon/add2.png')" mode="widthFix" class="action-icon"
                                 v-show="item.quantity < 99"></image>
                             <image :src="getFileUrl('img/icon/add1.png')" mode="widthFix" class="action-icon"
@@ -97,8 +97,9 @@ const formatPrice = (price) => {
     return Number.isNaN(num) ? '0.00' : num.toFixed(2);
 };
 
-// 菜品图片地址:接口返回 cuisineImage(可能逗号分隔,取首图),兼容 image/imageUrl/pic/cover
+// 菜品图片地址:餐具(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) : '';
@@ -118,7 +119,7 @@ const getItemLinePrice = (item) => {
     return qty * unitPrice;
 };
 
-// 餐具(cuisineId 或 id 为 -1)可修改数量
+// 餐具(cuisineId 或 id 为 -1)可修改数量(含减至 0)
 const isTablewareItem = (item) => {
     if (!item) return false;
     const id = item.cuisineId ?? item.id;
@@ -133,20 +134,12 @@ const handleClose = () => {
 
 // 增加数量
 const handleIncrease = (item) => {
-    if (isTablewareItem(item)) {
-        uni.showToast({ title: '不允许更改餐具数量', icon: 'none' });
-        return;
-    }
     if (item.quantity >= 99) return;
     emit('increase', item);
 };
 
 // 减少数量
 const handleDecrease = (item) => {
-    if (isTablewareItem(item)) {
-        uni.showToast({ title: '不允许更改餐具数量', icon: 'none' });
-        return;
-    }
     if (item && item.quantity > 0) {
         emit('decrease', item);
     }

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

@@ -25,12 +25,12 @@
         <text class="sales-text">月售:{{ food.monthlySales || 0 }}</text>
       </view>
       <view class="food-actions">
-        <view class="action-btn minus" :class="{ disabled: food.quantity === 0 || isTableware }" @click="handleDecrease" hover-class="hover-active">
+        <view class="action-btn minus" :class="{ disabled: food.quantity === 0 }" @click="handleDecrease" hover-class="hover-active">
           <image :src="getFileUrl('img/icon/reduce1.png')" mode="aspectFit" class="action-icon" v-show="food.quantity == 0"></image>
           <image :src="getFileUrl('img/icon/reduce2.png')" mode="aspectFit" class="action-icon" v-show="food.quantity != 0"></image>
         </view>
         <view class="quantity">{{ food.quantity || 0 }}</view>
-        <view class="action-btn plus" :class="{ disabled: isTableware }" @click="handleIncrease" hover-class="hover-active">
+        <view class="action-btn plus" @click="handleIncrease" hover-class="hover-active">
           <image :src="getFileUrl('img/icon/add2.png')" mode="widthFix" class="action-icon" v-show="food.quantity < 99"></image>
           <image :src="getFileUrl('img/icon/add1.png')" mode="widthFix" class="action-icon" v-show="food.quantity >= 99"></image>
         </view>
@@ -127,19 +127,11 @@ const handleFoodClick = () => {
 };
 
 const handleIncrease = () => {
-  if (isTableware.value) {
-    uni.showToast({ title: '不允许更改餐具数量', icon: 'none' });
-    return;
-  }
   if (props.food.quantity >= 99) return;
   emit('increase', props.food);
 };
 
 const handleDecrease = () => {
-  if (isTableware.value) {
-    uni.showToast({ title: '不允许更改餐具数量', icon: 'none' });
-    return;
-  }
   if (props.food.quantity > 0) {
     emit('decrease', props.food);
   }

+ 74 - 25
pages/orderFood/index.vue

@@ -3,7 +3,7 @@
   <view class="page-wrap">
     <view class="top-fixed">
       <NavBar :title="navTitle" only-home />
-      <view class="top-info">桌号:{{ displayTableNumber }} &nbsp;&nbsp;就餐人数:{{ currentDiners }}人</view>
+      <view class="top-info">桌号:{{ displayTableNumber }}</view>
       <input type="text" placeholder="请输入菜品名称" class="search-input" />
     </view>
     <view class="content">
@@ -58,7 +58,7 @@ import CouponModal from "./components/CouponModal.vue";
 import CartModal from "./components/CartModal.vue";
 import SelectCouponModal from "./components/SelectCouponModal.vue";
 import { go } from "@/utils/utils.js";
-import { DiningOrderFood, GetStoreCategories, GetStoreCuisines, GetStoreDetail, getOrderSseConfig, GetOrderCart, PostOrderCartAdd, PostOrderCartUpdate, PostOrderCartClear, PutOrderCartUpdateTableware, GetUserOwnedCouponList } from "@/api/dining.js";
+import { DiningOrderFood, GetStoreCategories, GetStoreCuisines, GetStoreDetail, getOrderSseConfig, GetOrderCart, PostOrderCartAdd, PostOrderCartUpdate, PostOrderCartClear, PutOrderCartUpdateTableware, GetUserCouponList } from "@/api/dining.js";
 import { createSSEConnection } from "@/utils/sse.js";
 
 const storeName = ref(''); // 店铺名称,用于导航栏标题
@@ -82,6 +82,7 @@ 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
 
@@ -115,13 +116,21 @@ const cartList = computed(() => {
   });
 });
 
-// 用于展示的购物车列表:优先使用接口返回的 items(含 cuisineName/cuisineImage/unitPrice/subtotalAmount/remark),只展示数量>0
+// 用于展示的购物车列表:数量>0 的项展示;餐具(id=-1)始终展示(含数量为 0);若接口未返回餐具则补默认项
 const displayCartList = computed(() => {
   const apiItems = cartData.value?.items;
+  let base = [];
   if (Array.isArray(apiItems) && apiItems.length > 0) {
-    return apiItems.filter((it) => (Number(it?.quantity) || 0) > 0);
+    base = apiItems.filter((it) => (Number(it?.quantity) || 0) > 0 || Number(it?.cuisineId ?? it?.id) === -1);
+  } 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 cartList.value;
+  return base;
 });
 
 // 行小计:接口项用 subtotalAmount,否则 数量×单价(与 BottomActionBar/CartModal 一致)
@@ -155,15 +164,14 @@ const couponList = ref([]);
 // 门店可用优惠券列表(选择优惠券弹窗用,与 couponList 同步自同一接口)
 const storeUsableCouponList = ref([]);
 
-// 规范化接口优惠券项为弹窗所需格式(对接 userOwnedByStore 返回的 data 数组:id/couponId/userCouponId、name、couponType、discountRate、minimumSpendingAmount、expirationTime 等)
+// 规范化接口优惠券项为弹窗所需格式(对接 getUserCouponList 返回的 data:id/couponId/userCouponId、name、couponType、discountRate、minimumSpendingAmount、expirationTime 等)
 // couponType 1=满减券显示 nominalValue 为金额,2=折扣券显示 discountRate 为折扣力度
 function normalizeCouponItem(item) {
   if (!item || typeof item !== 'object') return null;
   const raw = item;
   const couponType = Number(raw.couponType) || 0;
   const nominalValue = Number(raw.nominalValue ?? raw.amount ?? 0) || 0;
-  // userOwnedByStore 接口返回的 discountRate 需除以 10(如 55 表示 5.5折)
-  const discountRate = (Number(raw.discountRate) || 0) / 10;
+  const discountRate = Number(raw.discountRate) || 0;
   const minAmount = Number(raw.minimumSpendingAmount ?? raw.minAmount ?? raw.min_amount ?? 0) || 0;
   const isReceived = raw.canReceived === false;
   let amountDisplay = nominalValue + '元';
@@ -325,7 +333,7 @@ const syncCartDataFromFoodList = () => {
   cartData.value = { ...cartData.value, items: nextItems, totalAmount, totalQuantity };
 };
 
-// 判断是否为餐具(cuisineId 或 id 为 -1),餐具可修改数量
+// 判断是否为餐具(cuisineId 或 id 为 -1),餐具可修改数量(含减至 0)
 const isTableware = (item) => {
   if (!item) return false;
   const id = item.id ?? item.cuisineId;
@@ -336,10 +344,6 @@ const isTableware = (item) => {
 // Update 接口返回 400 时不改页面数量和金额(会回滚本地状态)
 const updateFoodQuantity = (food, delta) => {
   if (!food) return;
-  if (isTableware(food)) {
-    uni.showToast({ title: '不允许更改餐具数量', icon: 'none' });
-    return;
-  }
   const id = food.id ?? food.cuisineId;
   const prevQty = food.quantity || 0;
   const nextQty = Math.max(0, prevQty + delta);
@@ -383,7 +387,18 @@ const updateFoodQuantity = (food, delta) => {
 
   if (tableId.value && delta !== 0) {
     const needAdd = delta > 0 && (idx < 0 || nextQty === 1);
-    if (needAdd) {
+    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) {
       PostOrderCartAdd({
         cuisineId: id,
         quantity: nextQty,
@@ -417,11 +432,11 @@ const handleDecrease = (item) => {
   if (food && (food.quantity || 0) > 0) updateFoodQuantity(food, -1);
 };
 
-// 拉取用户已领/可用优惠券(GET /dining/coupon/userOwnedByStore),列表在 SelectCouponModal 中展示,空时弹窗内显示「暂无可用优惠券」
+// 拉取用户优惠券列表(GET /dining/coupon/getUserCouponList),列表在 SelectCouponModal 中展示,空时弹窗内显示「暂无可用优惠券」
 const fetchUserOwnedCoupons = async () => {
   const storeId = uni.getStorageSync('currentStoreId') || '';
   try {
-    const res = await GetUserOwnedCouponList({ storeId });
+    const res = await GetUserCouponList({ storeId });
     // 接口返回 { code, data: [...], msg, success },请求层可能只返回 data,故 res 可能为数组
     const list = Array.isArray(res) ? res : (res?.data ?? res?.records ?? res?.list ?? []);
     const normalized = (Array.isArray(list) ? list : []).map((item) => normalizeCouponItem(item)).filter(Boolean);
@@ -519,21 +534,43 @@ const handleCartClose = () => {
   cartModalOpen.value = false;
 };
 
-// 清空购物车:调用 /store/order/cart/clear,入参 tableId,成功后清空本地并关闭弹窗
+// 清空购物车:调用 /store/order/cart/clear,成功后用接口返回的数据更新购物车
 const handleCartClear = () => {
-  const doClear = () => {
-    pendingCartData = null;
-    foodList.value = foodList.value.map((f) => ({ ...f, quantity: 0 }));
-    cartData.value = { items: [], totalAmount: 0, totalQuantity: 0 };
+  const items = cartData.value?.items ?? [];
+  const dishItems = items.filter((it) => Number(it?.cuisineId ?? it?.id) !== -1);
+
+  const applyClearResult = (res) => {
+    const list = 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 };
+    mergeCartIntoFoodList();
     cartModalOpen.value = false;
     uni.showToast({ title: '已清空购物车', icon: 'success' });
   };
+
   if (!tableId.value) {
-    doClear();
+    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 });
+    return;
+  }
+  if (dishItems.length === 0) {
+    applyClearResult(cartData.value);
     return;
   }
   PostOrderCartClear(tableId.value)
-    .then(doClear)
+    .then(applyClearResult)
     .catch((err) => {
       console.error('清空购物车失败:', err);
       uni.showToast({ title: '清空失败,请重试', icon: 'none' });
@@ -553,6 +590,17 @@ const getTablewareFeeFromCart = () => {
 
 // 下单点击:先带购物车数据跳转确认订单页,创建订单在确认页点击「确认下单」时再调
 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;
@@ -582,12 +630,13 @@ 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);
 
-  // 更新餐具数量:quantity 传用餐人数,tableId 传桌号ID
-  if (tableid && diners !== '' && diners != null) {
+  // 更新餐具数量:仅从选座页进入时调用 update-tableware 接口
+  if (fromNumberOfDiners.value && tableid) {
     PutOrderCartUpdateTableware({
       quantity: Number(diners) || 1,
       tableId: parseInt(tableid, 10) || 0

+ 2 - 1
pages/orderInfo/index.vue

@@ -102,11 +102,12 @@ function normalizeOrder(record) {
     .map((it) => it?.cuisineImage ?? it?.image ?? it?.imageUrl ?? '')
     .filter((url) => url && String(url).trim());
 
+  // storeName 取自 data.records[].order.storeName
   return {
     id: order?.id,
     orderNo: order?.orderNo ?? order?.orderNumber ?? order?.orderId ?? '',
     storeId: order?.storeId,
-    storeName: order?.storeName ?? order?.store?.name ?? (order?.storeId != null ? `门店${order.storeId}` : '—'),
+    storeName: order?.storeName ?? order?.store_name ?? record?.storeName ?? record?.store_name ?? '—',
     tableId: order?.tableId,
     tableNumber: order?.tableNumber ?? order?.tableId ?? '—',
     contactPhone: order?.contactPhone ?? '',

+ 8 - 3
pages/orderInfo/orderDetail.vue

@@ -13,7 +13,7 @@
       </view>
       <view class="card-content">
         <view class="info-food">
-          <view v-for="(item, index) in foodList" :key="item.id || index" class="food-item">
+          <view v-for="(item, index) in displayFoodList" :key="item.id || index" class="food-item">
             <!-- 菜品图片 -->
             <image :src="getFileUrl(item.image)" mode="aspectFill" class="food-item__image"></image>
 
@@ -106,7 +106,7 @@
 
 <script setup>
 import { onLoad } from "@dcloudio/uni-app";
-import { ref } from "vue";
+import { ref, computed } from "vue";
 import { go } from "@/utils/utils.js";
 import { getFileUrl } from "@/utils/file.js";
 import { GetOrderInfo } from "@/api/dining.js";
@@ -139,6 +139,11 @@ const priceDetail = ref({
 // 菜品列表(接口订单明细)
 const foodList = ref([]);
 
+// 菜品详情展示:排除餐具(id/cuisineId === -1)
+const displayFoodList = computed(() => {
+  return (foodList.value ?? []).filter((it) => Number(it?.id ?? it?.cuisineId) !== -1);
+});
+
 // 金额保留两位小数
 const formatPrice = (price) => {
   if (price === '' || price === null || price === undefined) return '0.00';
@@ -177,7 +182,7 @@ function applyOrderData(data) {
     contactPhone: raw?.contactPhone ?? raw?.phone ?? '',
     remark: raw?.remark ?? ''
   };
-  const dishTotal = raw?.dishTotal ?? raw?.orderAmount ?? raw?.foodAmount ?? 0;
+  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 total = raw?.totalAmount ?? raw?.totalPrice ?? raw?.payAmount ?? 0;

+ 13 - 3
pages/personal/index.vue

@@ -20,7 +20,7 @@
 
 				<!-- 列表 -->
 				<view class="card-list-container">
-					<view class="card-list" hover-class="hover-active" @click='go("/pages/personal/userInfo")'>
+					<view class="card-list" hover-class="hover-active" @click='handleNavClick("/pages/personal/userInfo")'>
 						<view class='left'>
 							<image :src="getFileUrl('img/personal/wszl.png')" mode="widthFix" class="card-icon"></image>
 							<view class="title">完善资料</view>
@@ -30,7 +30,7 @@
 							</image>
 						</view>
 					</view>
-					<view class="card-list" hover-class="hover-active" @click='go("/pages/orderInfo/index")'>
+					<view class="card-list" hover-class="hover-active" @click='handleNavClick("/pages/orderInfo/index")'>
 						<view class='left'>
 							<image :src="getFileUrl('img/personal/wddd.png')" mode="widthFix" class="card-icon"></image>
 							<view class="title">我的订单</view>
@@ -40,7 +40,7 @@
 							</image>
 						</view>
 					</view>
-					<view class="card-list" hover-class="hover-active" @click='go("/pages/coupon/index")'>
+					<view class="card-list" hover-class="hover-active" @click='handleNavClick("/pages/coupon/index")'>
 						<view class='left'>
 							<image :src="getFileUrl('img/personal/wdqb.png')" mode="widthFix" class="card-icon"></image>
 							<view class="title">我的券包</view>
@@ -139,6 +139,16 @@ const handleLoginClick = () => {
 	}
 };
 
+// 需登录才能访问的导航:未登录时禁止跳转,提示并弹出登录框
+const handleNavClick = (url) => {
+	if (!userStore.getToken) {
+		uni.showToast({ title: '请先登录', icon: 'none' });
+		showLoginModal.value = true;
+		return;
+	}
+	go(url);
+};
+
 // 登录成功回调:立即从 storage 同步用户信息,保证头像/昵称马上更新
 const handleLoginSuccess = () => {
 	syncUserFromStorage();

+ 21 - 14
pages/placeOrder/index.vue

@@ -156,19 +156,26 @@ const selectedCouponDisplay = ref('');
 // 菜品列表(从购物车带过来)
 const foodList = ref([]);
 
-// 菜品总价(不含餐具费):优先缓存中的 dishTotal,否则 totalAmount - utensilFee
+// 菜品清单展示:排除餐具(id/cuisineId === -1)
+const displayFoodList = computed(() => {
+  return (foodList.value ?? []).filter((it) => Number(it?.id ?? it?.cuisineId) !== -1);
+});
+
+// 菜品总价(不含餐具费):仅按菜品清单中非餐具项汇总,不包含餐具费
 const dishTotal = computed(() => {
-  const fromCache = orderInfo.value.dishTotal;
-  if (fromCache != null && fromCache !== '' && !Number.isNaN(Number(fromCache))) {
-    return Math.max(0, Number(fromCache));
-  }
-  const total = Number(orderInfo.value.totalAmount) || 0;
-  const utensil = Number(orderInfo.value.utensilFee) || 0;
-  return Math.max(0, total - utensil);
+  const list = displayFoodList.value ?? [];
+  const sum = list.reduce((s, it) => {
+    const qty = Number(it?.quantity) || 0;
+    const price = Number(it?.unitPrice ?? it?.price ?? it?.totalPrice ?? it?.salePrice ?? 0) || 0;
+    return s + qty * price;
+  }, 0);
+  return Math.max(0, sum);
 });
 
-// 菜品图片:兼容 cuisineImage/image,逗号分隔取首
+// 菜品图片:餐具(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) : '';
@@ -191,8 +198,7 @@ function normalizeCouponItem(item) {
   const raw = item;
   const couponType = Number(raw.couponType) || 0;
   const nominalValue = Number(raw.nominalValue ?? raw.amount ?? 0) || 0;
-  // userOwnedByStore 接口返回的 discountRate 需除以 10(如 55 表示 5.5折)
-  const discountRate = (Number(raw.discountRate) || 0) / 10;
+  const discountRate = Number(raw.discountRate) || 0;
   const minAmount = Number(raw.minimumSpendingAmount ?? raw.minAmount ?? 0) || 0;
   let amountDisplay = nominalValue + '元';
   if (couponType === 1) amountDisplay = nominalValue + '元';
@@ -219,7 +225,7 @@ const openCouponModal = async () => {
   const storeId = uni.getStorageSync('currentStoreId') || '';
   couponModalOpen.value = false;
   try {
-    const res = await diningApi.GetUserOwnedCouponList({ storeId });
+    const res = await diningApi.GetUserCouponList({ storeId });
     const list = Array.isArray(res) ? res : (res?.data ?? res?.records ?? res?.list ?? []);
     const normalized = (Array.isArray(list) ? list : []).map(normalizeCouponItem).filter(Boolean);
     couponList.value = normalized;
@@ -265,7 +271,7 @@ const updatePayAmount = () => {
 const fetchCouponsOnEnter = async () => {
   const storeId = uni.getStorageSync('currentStoreId') || '';
   try {
-    const res = await diningApi.GetUserOwnedCouponList({ storeId });
+    const res = await diningApi.GetUserCouponList({ storeId });
     const list = Array.isArray(res) ? res : (res?.data ?? res?.records ?? res?.list ?? []);
     const normalized = (Array.isArray(list) ? list : []).map(normalizeCouponItem).filter(Boolean);
     couponList.value = normalized;
@@ -319,10 +325,11 @@ const handleConfirmOrder = async () => {
   try {
     const tablewareFee = Number(orderInfo.value.utensilFee) || 0;
     const payAmount = Number((Number(orderInfo.value.payAmount) ?? 0).toFixed(2));
-    // couponId 传入接口返回的当前选择的优惠券 id(userOwnedByStore 返回的 userCouponId/id/couponId)
+    const totalAmount = Number((Number(dishTotal.value) ?? 0).toFixed(2));
     const res = await diningApi.PostOrderCreate({
       tableId,
       contactPhone,
+      totalAmount,
       couponId: (selectedCouponId.value ?? orderInfo.value.couponId) || null,
       discountAmount: orderInfo.value.discountAmount ?? 0,
       tablewareFee,

二進制
static/utensilFee.png