فهرست منبع

对接购物车

sunshibo 2 ماه پیش
والد
کامیت
67838db49f
4فایلهای تغییر یافته به همراه340 افزوده شده و 51 حذف شده
  1. 48 1
      api/dining.js
  2. 31 17
      pages/orderFood/components/BottomActionBar.vue
  3. 34 15
      pages/orderFood/components/CartModal.vue
  4. 227 18
      pages/orderFood/index.vue

+ 48 - 1
api/dining.js

@@ -1,4 +1,6 @@
 import { api } from '@/utils/request.js';
+import { BASE_API_URL } from '@/settings/siteSetting.js';
+import { useUserStore } from '@/store/user.js';
 
 // 微信登录
 export const DiningUserWechatLogin = (params) => api.post({ url: '/dining/user/wechatLogin', params });
@@ -10,4 +12,49 @@ export const DiningOrderFood = (params) => api.get({ url: '/store/dining/page-in
 export const GetStoreCategories = (params) => api.get({ url: '/store/info/categories', params });
 
 // 根据菜品种类获取菜品(入参 categoryId,GET /store/info/cuisines?categoryId=)
-export const GetStoreCuisines = (params) => api.get({ url: '/store/info/cuisines', params });
+export const GetStoreCuisines = (params) => api.get({ url: '/store/info/cuisines', params });
+
+// 获取购物车(GET /store/order/cart/{tableId},建立 SSE 连接之后调用)
+export const GetOrderCart = (tableId) =>
+  api.get({ url: `/store/order/cart/${encodeURIComponent(tableId)}` });
+
+// 加入购物车(POST /store/order/cart/add,dto: { cuisineId 菜品ID, quantity 数量, tableId 桌号ID })
+export const PostOrderCartAdd = (dto) =>
+  api.post({ url: '/store/order/cart/add', params: dto });
+
+// 将对象转为 x-www-form-urlencoded 字符串(兼容小程序环境无 URLSearchParams)
+function toFormUrlEncoded(obj) {
+  return Object.keys(obj || {})
+    .filter((k) => obj[k] != null && obj[k] !== '')
+    .map((k) => encodeURIComponent(k) + '=' + encodeURIComponent(obj[k]))
+    .join('&');
+}
+
+// 更新购物车(POST /store/order/cart/update,请求类型 x-www-form-urlencoded)
+export const PostOrderCartUpdate = (params) => {
+  const body = toFormUrlEncoded(params);
+  return api.post({
+    url: '/store/order/cart/update',
+    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+    params: body
+  });
+};
+
+/**
+ * 创建订单 SSE 连接(GET /store/order/sse/{tableId})
+ * 返回 uni.request 的 requestTask,需在页面 onUnload 时 abort()
+ */
+export function createOrderSseConnection(tableId, options = {}) {
+  const userStore = useUserStore();
+  const url = `${BASE_API_URL}/store/order/sse/${encodeURIComponent(tableId)}`;
+  return uni.request({
+    url,
+    method: 'GET',
+    header: {
+      Authorization: userStore.getToken || ''
+    },
+    enableChunked: true,
+    timeout: 0,
+    ...options
+  });
+}

+ 31 - 17
pages/orderFood/components/BottomActionBar.vue

@@ -14,10 +14,10 @@
       <view class="shop-cart">
         <view class="shop-cart-content" @click="handleCartClick">
           <image :src="getFileUrl('img/icon/shoppingCart.png')" mode="widthFix" class="icon-shopping-cart"></image>
-          <view class='number'>{{ totalQuantity }}</view>
+          <view class='number'>{{ displayTotalQuantity }}</view>
           <view class="price">
             <text class="price-symbol">¥</text>
-            <text class="price-number">{{ formatPrice(totalPrice) }}</text>
+            <text class="price-number">{{ formatPrice(displayTotalPrice) }}</text>
           </view>
         </view>
         <!-- 下单 -->
@@ -32,33 +32,47 @@ import { computed } from 'vue';
 import { getFileUrl } from '@/utils/file.js';
 
 const props = defineProps({
-  // 购物车数据列表
+  // 购物车数据列表(可为接口返回的 items:cuisineId/cuisineName/quantity/unitPrice/subtotalAmount)
   cartList: {
     type: Array,
     default: () => []
+  },
+  // 可选:接口返回的 totalQuantity,传入时优先使用
+  totalQuantity: {
+    type: Number,
+    default: null
+  },
+  // 可选:接口返回的 totalAmount,传入时优先使用
+  totalAmount: {
+    type: Number,
+    default: null
   }
 });
 
 const emit = defineEmits(['coupon-click', 'cart-click', 'order-click']);
 
-// 取单品金额(与 FoodCard 第 10 行显示的菜品金额一致:优先 totalPrice,再兼容 price 等)
+// 取单品金额:接口项用 unitPrice,兼容 totalPrice/price 等
 const getItemPrice = (item) => {
-  const p = item?.totalPrice ?? item?.price ?? item?.salePrice ?? item?.currentPrice ?? item?.unitPrice ?? 0;
+  const p = item?.unitPrice ?? item?.price ?? item?.totalPrice ?? item?.salePrice ?? item?.currentPrice ?? 0;
   return Number(p) || 0;
 };
 
-// 计算总数量(与 FoodCard 第 32 行当前菜品数量一致)
-const totalQuantity = computed(() => {
+// 行小计:接口项用 subtotalAmount,否则 数量×单价
+const getItemLinePrice = (item) => {
+  if (item?.subtotalAmount != null) return Number(item.subtotalAmount);
+  return (Number(item?.quantity) || 0) * getItemPrice(item);
+};
+
+// 展示总数量:优先接口 totalQuantity,否则从列表计算
+const displayTotalQuantity = computed(() => {
+  if (props.totalQuantity != null && props.totalQuantity !== '') return Number(props.totalQuantity);
   return props.cartList.reduce((sum, item) => sum + (Number(item?.quantity) || 0), 0);
 });
 
-// 总金额 = 菜品金额×当前菜品数量 + 其他选中项(菜品金额×当前数量)… 即 Σ(菜品金额 × 当前菜品数量)
-const totalPrice = computed(() => {
-  return props.cartList.reduce((sum, item) => {
-    const quantity = Number(item?.quantity) || 0;
-    const price = getItemPrice(item);
-    return sum + quantity * price;
-  }, 0);
+// 展示总金额:优先接口 totalAmount,否则从列表计算(接口项用 subtotalAmount)
+const displayTotalPrice = computed(() => {
+  if (props.totalAmount != null && props.totalAmount !== '') return Number(props.totalAmount);
+  return props.cartList.reduce((sum, item) => sum + getItemLinePrice(item), 0);
 });
 
 // 格式化总金额
@@ -79,11 +93,11 @@ const handleCartClick = () => {
 
 // 下单点击
 const handleOrderClick = () => {
-  if (totalQuantity.value === 0) return;
+  if (displayTotalQuantity.value === 0) return;
   emit('order-click', {
     cartList: props.cartList,
-    totalPrice: totalPrice.value,
-    totalQuantity: totalQuantity.value
+    totalPrice: displayTotalPrice.value,
+    totalQuantity: displayTotalQuantity.value
   });
 };
 </script>

+ 34 - 15
pages/orderFood/components/CartModal.vue

@@ -11,13 +11,14 @@
 
             <!-- 购物车列表 -->
             <scroll-view class="cart-modal__list" scroll-y>
-                <view v-for="(item, index) in cartList" :key="item.id || index" class="cart-item">
-                    <!-- 菜品图片 -->
+                <view v-for="(item, index) in cartList" :key="item.cuisineId || item.id || index" class="cart-item">
+                    <!-- 菜品图片:接口返回 cuisineImage(可能逗号分隔多图,取首图) -->
                     <image :src="getItemImageSrc(item)" mode="aspectFill" class="cart-item__image"></image>
 
-                    <!-- 菜品信息 -->
+                    <!-- 菜品信息:接口返回 cuisineName / unitPrice / subtotalAmount / remark -->
                     <view class="cart-item__info">
-                        <view class="cart-item__name">{{ item.name }}</view>
+                        <view class="cart-item__name">{{ item.cuisineName || item.name }}</view>
+                        <view class="cart-item__remark" v-if="item.remark">{{ item.remark }}</view>
                         <view class="cart-item__tags" v-if="item.tags && item.tags.length > 0">
                             <view v-for="(tag, tagIndex) in item.tags" :key="tagIndex" class="cart-item__tag"
                                 :class="tag.type">
@@ -26,7 +27,7 @@
                         </view>
                         <view class="cart-item__price">
                             <text class="price-symbol">¥</text>
-                            <text class="price-number">{{ formatPrice(item.price) }}</text>
+                            <text class="price-number">{{ formatPrice(getItemLinePrice(item)) }}</text>
                         </view>
                     </view>
 
@@ -102,26 +103,38 @@ const totalQuantity = computed(() => {
     }, 0);
 });
 
-// 计算总价格
+// 计算总价格:接口项用 subtotalAmount,否则 数量×单价
 const totalPrice = computed(() => {
-    return props.cartList.reduce((sum, item) => {
-        const quantity = item.quantity || 0;
-        const price = item.price || 0;
-        return sum + (quantity * price);
-    }, 0);
+    return props.cartList.reduce((sum, item) => sum + getItemLinePrice(item), 0);
 });
 
-// 格式化价格(显示为整数
+// 格式化价格(保留两位小数,避免 NaN)
 const formatPrice = (price) => {
-    return Math.round(price).toFixed(0);
+    const num = Number(price);
+    return Number.isNaN(num) ? '0.00' : num.toFixed(2);
 };
 
-// 菜品图片地址:兼容接口字段 image/imageUrl/pic/cover,相对路径经 getFileUrl 补全 CDN
+// 菜品图片地址:接口返回 cuisineImage(可能逗号分隔,取首图),兼容 image/imageUrl/pic/cover
 const getItemImageSrc = (item) => {
-    const url = item?.image ?? item?.imageUrl ?? item?.pic ?? item?.cover ?? '';
+    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) : '';
 };
 
+// 单品单价:接口返回 unitPrice,兼容 price/salePrice 等
+const getItemPrice = (item) => {
+    const p = item?.unitPrice ?? item?.price ?? item?.salePrice ?? item?.currentPrice ?? item?.totalPrice ?? 0;
+    return Number(p) || 0;
+};
+
+// 行小计:接口返回 subtotalAmount,否则 数量×单价
+const getItemLinePrice = (item) => {
+    if (item?.subtotalAmount != null) return Number(item.subtotalAmount);
+    const qty = Number(item?.quantity) || 0;
+    const unitPrice = getItemPrice(item);
+    return qty * unitPrice;
+};
+
 // 处理关闭
 const handleClose = () => {
     getOpen.value = false;
@@ -253,6 +266,12 @@ const handleCouponClick = () => {
         margin-bottom: 8rpx;
     }
 
+    &__remark {
+        font-size: 24rpx;
+        color: #999;
+        margin-bottom: 8rpx;
+    }
+
     &__tags {
         display: flex;
         gap: 10rpx;

+ 227 - 18
pages/orderFood/index.vue

@@ -22,16 +22,17 @@
       </scroll-view>
     </view>
 
-    <!-- 底部下单 -->
-    <BottomActionBar :cart-list="cartList" @coupon-click="handleCouponClick" @cart-click="handleCartClick"
+    <!-- 底部下单:按接口格式展示 totalQuantity / totalAmount -->
+    <BottomActionBar :cart-list="displayCartList" :total-quantity="displayTotalQuantity"
+      :total-amount="displayTotalAmount" @coupon-click="handleCouponClick" @cart-click="handleCartClick"
       @order-click="handleOrderClick" />
 
     <!-- 领取优惠券弹窗 -->
     <CouponModal v-model:open="couponModalOpen" :coupon-list="couponList" @receive="handleCouponReceive"
       @close="handleCouponClose" />
 
-    <!-- 购物车弹窗 -->
-    <CartModal v-model:open="cartModalOpen" :cart-list="cartList" :discount-amount="discountAmount"
+    <!-- 购物车弹窗:按接口格式展示 items(cuisineName/cuisineImage/quantity/unitPrice/subtotalAmount/remark) -->
+    <CartModal v-model:open="cartModalOpen" :cart-list="displayCartList" :discount-amount="discountAmount"
       @increase="handleIncrease" @decrease="handleDecrease" @clear="handleCartClear"
       @coupon-click="handleSelectCouponClick" @order-click="handleOrderClick" @close="handleCartClose" />
 
@@ -42,7 +43,7 @@
 </template>
 
 <script setup>
-import { onLoad } from "@dcloudio/uni-app";
+import { onLoad, onUnload } from "@dcloudio/uni-app";
 import { ref, computed } from "vue";
 import FoodCard from "./components/FoodCard.vue";
 import BottomActionBar from "./components/BottomActionBar.vue";
@@ -50,10 +51,11 @@ 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 } from "@/api/dining.js";
+import { DiningOrderFood, GetStoreCategories, GetStoreCuisines, createOrderSseConnection, GetOrderCart, PostOrderCartAdd, PostOrderCartUpdate } from "@/api/dining.js";
 
 const tableId = ref(''); // 桌号,来自上一页 url 参数 tableid
 const currentDiners = ref(uni.getStorageSync('currentDiners') || '');
+let sseRequestTask = null; // 订单 SSE 连接,页面卸载时需 abort
 const currentCategoryIndex = ref(0);
 const couponModalOpen = ref(false);
 const cartModalOpen = ref(false);
@@ -66,6 +68,10 @@ const categories = ref([]);
 
 // 菜品列表(由接口 /store/info/cuisines 按分类拉取并合并,每项含 quantity、categoryId)
 const foodList = ref([]);
+// SSE 连接建立后拉取的购物车数据,在 foodList 就绪后合并
+let pendingCartData = null;
+// 购物车接口返回的完整数据:{ items, totalAmount, totalQuantity },用于按接口格式展示
+const cartData = ref({ items: [], totalAmount: 0, totalQuantity: 0 });
 
 // 当前分类的菜品列表(按当前选中的分类 id 过滤)
 const currentFoodList = computed(() => {
@@ -87,6 +93,35 @@ const cartList = computed(() => {
   });
 });
 
+// 用于展示的购物车列表:优先使用接口返回的 items(含 cuisineName/cuisineImage/unitPrice/subtotalAmount/remark),只展示数量>0
+const displayCartList = computed(() => {
+  const apiItems = cartData.value?.items;
+  if (Array.isArray(apiItems) && apiItems.length > 0) {
+    return apiItems.filter((it) => (Number(it?.quantity) || 0) > 0);
+  }
+  return cartList.value;
+});
+
+// 展示用总数量:优先接口 totalQuantity,否则从列表计算
+const displayTotalQuantity = computed(() => {
+  if (Array.isArray(cartData.value?.items) && cartData.value.items.length > 0 && cartData.value.totalQuantity != null) {
+    return cartData.value.totalQuantity;
+  }
+  return displayCartList.value.reduce((sum, item) => sum + (Number(item?.quantity) || 0), 0);
+});
+
+// 展示用总金额:优先接口 totalAmount,否则从列表计算
+const displayTotalAmount = computed(() => {
+  if (Array.isArray(cartData.value?.items) && cartData.value.items.length > 0 && cartData.value.totalAmount != null) {
+    return cartData.value.totalAmount;
+  }
+  return displayCartList.value.reduce((sum, item) => {
+    const qty = Number(item?.quantity) || 0;
+    const price = Number(item?.subtotalAmount ?? item?.unitPrice ?? item?.price ?? item?.totalPrice ?? 0) || 0;
+    return sum + (item?.subtotalAmount != null ? Number(item.subtotalAmount) : qty * price);
+  }, 0);
+});
+
 // 优惠券列表(示例数据)
 const couponList = ref([
   {
@@ -131,6 +166,76 @@ const availableCoupons = computed(() => {
 // 分类 id 统一转字符串,避免 number/string 比较导致误判
 const sameCategory = (a, b) => String(a ?? '') === String(b ?? '');
 
+// 将购物车数量同步到菜品列表:用购物车接口的 items 按 cuisineId 匹配 foodList 中的菜品并更新 quantity
+const mergeCartIntoFoodList = () => {
+  const cartItems = pendingCartData ?? cartData.value?.items ?? [];
+  if (!Array.isArray(cartItems) || cartItems.length === 0) return;
+  if (!foodList.value.length) {
+    console.log('购物车已缓存,等 foodList 加载后再同步到菜品列表');
+    return;
+  }
+  foodList.value = foodList.value.map((item) => {
+    const itemId = String(item?.id ?? item?.cuisineId ?? '');
+    const cartItem = cartItems.find((c) => {
+      const cId = String(c?.id ?? c?.cuisineId ?? c?.foodId ?? c?.dishId ?? '');
+      return cId && cId === itemId;
+    });
+    const qty = cartItem
+      ? Number(cartItem.quantity ?? cartItem.num ?? cartItem.count ?? 0) || 0
+      : (item.quantity ?? 0);
+    return { ...item, quantity: qty };
+  });
+  pendingCartData = null;
+  console.log('购物车数量已同步到菜品列表');
+};
+
+// 从接口返回的 data 层中解析出购物车数组(/store/order/cart/{id} 返回在 items 下)
+function parseCartListFromResponse(cartRes) {
+  if (cartRes == null) return [];
+  if (Array.isArray(cartRes.items)) return cartRes.items;
+  // 优先 items(当前接口约定),再兼容 list / data / records 等
+  const firstLevel =
+    cartRes.items ??
+    cartRes.list ??
+    cartRes.data ??
+    cartRes.records ??
+    cartRes.cartList ??
+    cartRes.cartItems ??
+    cartRes.cart?.list ??
+    cartRes.cart?.items ??
+    cartRes.result?.list ??
+    cartRes.result?.data ??
+    cartRes.result?.items;
+  if (Array.isArray(firstLevel)) return firstLevel;
+  if (typeof cartRes === 'object') {
+    const arr = Object.values(cartRes).find((v) => Array.isArray(v));
+    if (arr) return arr;
+  }
+  return [];
+}
+
+// 调获取购物车接口并合并到 foodList(需在 foodList 有数据后调用才能返显到购物车)
+// 注意:request 层在 code=200 时只返回 res.data.data,故 cartRes 已是后端 data 层
+const fetchAndMergeCart = async (tableid) => {
+  if (!tableid) return;
+  try {
+    const cartRes = await GetOrderCart(tableid);
+    const list = parseCartListFromResponse(cartRes);
+    pendingCartData = list;
+    // 按接口格式绑定:data.items / totalAmount / totalQuantity
+    cartData.value = {
+      items: list,
+      totalAmount: Number(cartRes?.totalAmount) || 0,
+      totalQuantity: Number(cartRes?.totalQuantity) || 0
+    };
+    mergeCartIntoFoodList();
+    console.log('购物车接口返回(data 层):', cartRes, '解析条数:', list.length);
+  } catch (err) {
+    console.error('获取购物车失败:', err);
+    throw err;
+  }
+};
+
 // 根据分类 id 拉取菜品并合并到 foodList,切换分类时保留其他分类已选菜品及本分类已选数量
 const fetchCuisinesByCategoryId = async (categoryId) => {
   if (!categoryId) return;
@@ -147,12 +252,14 @@ const fetchCuisinesByCategoryId = async (categoryId) => {
     // 本分类:按菜品 id 从整份列表里取已选数量,id 一致则自动带上数量
     const merged = normalized.map((newItem) => {
       const existing = foodList.value.find(
-        (e) => String(e?.id ?? '') === String(newItem?.id ?? '')
+        (e) => String(e?.id ?? e?.cuisineId ?? '') === String(newItem?.id ?? newItem?.cuisineId ?? '')
       );
       const quantity = existing ? existing.quantity : (newItem.quantity ?? 0);
       return { ...newItem, quantity };
     });
     foodList.value = [...rest, ...merged];
+    // 切换分类后,把购物车数量再同步到当前分类的菜品列表
+    mergeCartIntoFoodList();
   } catch (err) {
     console.error('获取菜品失败:', err);
   }
@@ -167,26 +274,89 @@ const selectCategory = async (index) => {
   await fetchCuisinesByCategoryId(categoryId);
 };
 
-// 更新菜品数量:菜品 id 一致则全部同步为同一数量,触发响应式使底部金额重新计算
+// 根据展示项(可能来自接口 cuisineId 或 foodList 的 id)找到 foodList 中的菜品
+const findFoodByCartItem = (item) => {
+  const id = item?.id ?? item?.cuisineId;
+  if (id == null) return null;
+  return foodList.value.find((f) => String(f?.id ?? f?.cuisineId ?? '') === String(id));
+};
+
+// 同步 cartData:根据 cuisineId 更新 items 中对应项的 quantity、subtotalAmount,并重算 totalAmount、totalQuantity
+const syncCartDataFromFoodList = () => {
+  const items = cartData.value?.items ?? [];
+  if (!items.length) return;
+  let totalAmount = 0;
+  let totalQuantity = 0;
+  const nextItems = items.map((it) => {
+    const food = findFoodByCartItem(it);
+    const qty = food != null ? (food.quantity || 0) : (it.quantity || 0);
+    const unitPrice = Number(it?.unitPrice ?? it?.price ?? 0) || 0;
+    const subtotalAmount = qty * unitPrice;
+    totalAmount += subtotalAmount;
+    totalQuantity += qty;
+    return { ...it, quantity: qty, subtotalAmount };
+  });
+  cartData.value = { ...cartData.value, items: nextItems, totalAmount, totalQuantity };
+};
+
+// 更新菜品数量:菜品 id 一致则全部同步为同一数量,触发响应式;新增加入购物车时调接口;并同步 cartData
 const updateFoodQuantity = (food, delta) => {
   if (!food) return;
-  const id = food.id;
+  const id = food.id ?? food.cuisineId;
   const nextQty = Math.max(0, (food.quantity || 0) + delta);
   const sameId = (item) =>
-    String(item?.id ?? '') === String(id ?? '');
+    String(item?.id ?? item?.cuisineId ?? '') === String(id ?? '');
   foodList.value = foodList.value.map((item) =>
     sameId(item) ? { ...item, quantity: nextQty } : item
   );
+  // 同步接口格式购物车:若 cartData 中有该项则更新,否则追加(来自菜品列表新加)
+  const items = cartData.value?.items ?? [];
+  const idx = items.findIndex((it) => String(it?.cuisineId ?? it?.id ?? '') === String(id));
+  if (idx >= 0) {
+    const it = items[idx];
+    const unitPrice = Number(it?.unitPrice ?? it?.price ?? 0) || 0;
+    const nextItems = items.slice();
+    nextItems[idx] = { ...it, quantity: nextQty, subtotalAmount: nextQty * unitPrice };
+    const totalAmount = nextItems.reduce((s, i) => s + (Number(i.subtotalAmount) || 0), 0);
+    const totalQuantity = nextItems.reduce((s, i) => s + (Number(i.quantity) || 0), 0);
+    cartData.value = { ...cartData.value, items: nextItems, totalAmount, totalQuantity };
+  } else if (delta > 0) {
+    // 来自菜品列表新加:按接口格式追加到 cartData.items
+    const unitPrice = Number(food?.price ?? food?.unitPrice ?? food?.salePrice ?? 0) || 0;
+    const newItem = {
+      cuisineId: id,
+      cuisineName: food?.name ?? food?.cuisineName ?? '',
+      cuisineImage: food?.image ?? food?.cuisineImage ?? food?.imageUrl ?? '',
+      quantity: nextQty,
+      unitPrice,
+      subtotalAmount: nextQty * unitPrice
+    };
+    const nextItems = [...items, newItem];
+    const totalAmount = nextItems.reduce((s, i) => s + (Number(i.subtotalAmount) || 0), 0);
+    const totalQuantity = nextItems.reduce((s, i) => s + (Number(i.quantity) || 0), 0);
+    cartData.value = { ...cartData.value, items: nextItems, totalAmount, totalQuantity };
+  }
+  // 增加数量且商品数量 >= 1 时调用 /store/order/cart/update
+  if (delta > 0 && nextQty >= 1 && tableId.value) {
+    const params = {
+      cuisineId: id,
+      quantity: nextQty,
+      tableId: tableId.value
+    }
+    PostOrderCartUpdate(params).catch((err) => console.error('更新购物车失败:', err));
+  }
 };
 
-// 增加数量
-const handleIncrease = (food) => {
+// 增加数量:支持传入接口项(cuisineId)或菜品项(id),先解析为 foodList 中的 food
+const handleIncrease = (item) => {
+  const food = item?.id != null && item?.cuisineId == null ? item : findFoodByCartItem(item) ?? item;
   updateFoodQuantity(food, 1);
 };
 
 // 减少数量
-const handleDecrease = (food) => {
-  updateFoodQuantity(food, -1);
+const handleDecrease = (item) => {
+  const food = item?.id != null && item?.cuisineId == null ? item : findFoodByCartItem(item) ?? item;
+  if (food && (food.quantity || 0) > 0) updateFoodQuantity(food, -1);
 };
 
 // 优惠券点击(领取优惠券弹窗)
@@ -255,7 +425,7 @@ const handleCouponClose = () => {
 
 // 购物车点击
 const handleCartClick = () => {
-  if (cartList.value.length === 0) {
+  if (displayCartList.value.length === 0) {
     uni.showToast({
       title: '购物车为空',
       icon: 'none'
@@ -279,9 +449,8 @@ const handleCartClose = () => {
 
 // 清空购物车
 const handleCartClear = () => {
-  foodList.value.forEach(food => {
-    food.quantity = 0;
-  });
+  foodList.value = foodList.value.map((f) => ({ ...f, quantity: 0 }));
+  cartData.value = { items: [], totalAmount: 0, totalQuantity: 0 };
   cartModalOpen.value = false;
   uni.showToast({
     title: '已清空购物车',
@@ -302,6 +471,38 @@ onLoad(async (options) => {
   currentDiners.value = diners;
   console.log('点餐页接收参数 - 桌号(tableid):', tableid, '就餐人数(diners):', diners);
 
+  // 先拉取购物车并缓存,再加载菜品,保证合并时 pendingCartData 已就绪,避免不返显
+  if (tableid) {
+    await fetchAndMergeCart(tableid).catch((err) => console.error('获取购物车失败:', err));
+  }
+
+  // 页面加载时创建订单 SSE 连接(GET /store/order/sse/{tableId})
+  if (tableid) {
+    try {
+      sseRequestTask = createOrderSseConnection(tableid);
+      if (sseRequestTask && typeof sseRequestTask.onChunkReceived === 'function') {
+        sseRequestTask.onChunkReceived((res) => {
+          try {
+            const uint8 = new Uint8Array(res.data);
+            const text = String.fromCharCode.apply(null, uint8);
+            console.log('SSE 收到:', text);
+            // 可在此解析 SSE 事件并更新页面
+          } catch (e) {
+            console.error('SSE 数据解析:', e);
+          }
+        });
+      }
+      if (sseRequestTask && typeof sseRequestTask.onHeadersReceived === 'function') {
+        sseRequestTask.onHeadersReceived(async () => {
+          console.log('SSE 连接已建立');
+          await fetchAndMergeCart(tableid).catch(() => {});
+        });
+      }
+    } catch (e) {
+      console.error('SSE 连接失败:', e);
+    }
+  }
+
   // 调用点餐页接口,入参 dinerCount、tableId
   if (tableid || diners) {
     try {
@@ -331,6 +532,7 @@ onLoad(async (options) => {
                 categoryId: item.categoryId ?? firstCategoryId
               }));
               console.log('默认分类菜品:', cuisinesRes);
+              mergeCartIntoFoodList();
             }
           }
           console.log('菜品种类:', categoriesRes);
@@ -343,6 +545,13 @@ onLoad(async (options) => {
     }
   }
 });
+
+onUnload(() => {
+  if (sseRequestTask && typeof sseRequestTask.abort === 'function') {
+    sseRequestTask.abort();
+    sseRequestTask = null;
+  }
+});
 </script>
 
 <style lang="scss" scoped>