sunshibo 3 недель назад
Родитель
Сommit
4afeccd58e

+ 71 - 36
pages/coupon/index.vue

@@ -56,9 +56,9 @@
       <scroll-view
         class="content"
         scroll-y
-        :scroll-top="listScrollTop"
-        :scroll-with-animation="false"
         :class="{ 'content--dimmed': typePanelOpen }"
+        :lower-threshold="100"
+        @scrolltolower="handleCouponScrollToLower"
       >
       <view class="coupon-list" v-if="filteredCoupons.length > 0">
         <view v-for="(coupon, index) in filteredCoupons" :key="coupon.id || index"
@@ -91,6 +91,11 @@
         </view>
       </view>
 
+      <view v-if="couponList.length > 0 && (loadMoreLoading || !listHasMore)" class="coupon-page-footer">
+        <text v-if="loadMoreLoading" class="coupon-page-footer__text">加载中...</text>
+        <text v-else class="coupon-page-footer__text">没有更多了</text>
+      </view>
+
       <!-- 空状态 -->
       <view class="empty-state" v-if="filteredCoupons.length === 0">
         <image :src="getFileUrl('img/icon/noCoupon.png')" mode="widthFix" class="empty-icon"></image>
@@ -106,9 +111,9 @@
 
 <script setup>
 import { onShow } from "@dcloudio/uni-app";
-import { ref, computed, nextTick } from "vue";
+import { ref, computed } from "vue";
 import { getFileUrl } from "@/utils/file.js";
-import { normalizeUserCouponListItem, parseCouponListPage } from "@/utils/couponNormalize.js";
+import { normalizeUserCouponListItem, parseCouponListPage, mergeCouponListById } from "@/utils/couponNormalize.js";
 import RulesModal from "./components/RulesModal.vue";
 import * as diningApi from "@/api/dining.js";
 
@@ -144,9 +149,6 @@ function selectTypeFilter(value) {
   typePanelOpen.value = false;
   // 类型为前端筛选;与列表请求共用队列,保证「切换类型」排在进行中的请求之后(后续 Tab/加载更多按序执行)
   enqueueListFetch(() => Promise.resolve());
-  void nextTick(() => {
-    void scrollCouponListToTop();
-  });
 }
 
 // 弹窗控制
@@ -166,21 +168,13 @@ const selectedCoupon = ref({
   verificationCode: ''
 });
 
-// 优惠券数据(单次拉全量,不做分页
+// 优惠券数据(接口分页累加
 const couponList = ref([]);
 const loading = ref(false);
-/** 列表纵向滚动位置:刷新后归零,避免 scroll-view 仍停在旧滚动位置 */
-const listScrollTop = ref(0);
-/** 与后端约定的一次拉取上限(不分页) */
-const COUPON_LIST_SIZE = 999;
-
-/** 小程序 scroll-view 对 scroll-top 需「变化」才生效,先置非 0 再回 0 */
-async function scrollCouponListToTop() {
-  listScrollTop.value = 1;
-  await nextTick();
-  listScrollTop.value = 0;
-  await nextTick();
-}
+const loadMoreLoading = ref(false);
+const COUPON_PAGE_SIZE = 10;
+const listHasMore = ref(true);
+const lastCouponPageLoaded = ref(0);
 
 /** 列表相关请求串行:上一请求结束(成功/失败)后再执行下一请求,避免切换状态/类型时竞态 */
 let listFetchQueue = Promise.resolve();
@@ -204,40 +198,71 @@ const filteredCoupons = computed(() => {
 const handleTabChange = (index) => {
   currentTab.value = index;
   typePanelOpen.value = false;
-  enqueueListFetch(() => fetchCouponList({ tabType: index }));
+  enqueueListFetch(() => fetchCouponList({ reset: true, tabType: index }));
 };
 
-// 拉取优惠券列表(不分页:page=1、size=COUPON_LIST_SIZE)
+// 拉取优惠券列表(分页,每页 COUPON_PAGE_SIZE)
 const fetchCouponList = async (options = {}) => {
-  const { tabType: tabTypeOpt } = options;
+  const { reset = true, tabType: tabTypeOpt } = options;
   const tabForRequest = tabTypeOpt != null ? tabTypeOpt : currentTab.value;
   const storeId = uni.getStorageSync('currentStoreId') || '';
 
-  loading.value = true;
+  if (reset) {
+    loading.value = true;
+  } else {
+    if (loadMoreLoading.value || loading.value || !listHasMore.value || typePanelOpen.value) return;
+    loadMoreLoading.value = true;
+  }
+
+  const page = reset ? 1 : lastCouponPageLoaded.value + 1;
 
   try {
     const res = await diningApi.GetUserCouponList({
       storeId,
       tabType: tabForRequest,
-      page: 1,
-      size: COUPON_LIST_SIZE
+      page,
+      size: COUPON_PAGE_SIZE
     });
-    const { list: rawList } = parseCouponListPage(res);
+    const { list: rawList, total } = parseCouponListPage(res);
     const arr = Array.isArray(rawList) ? rawList : [];
-    couponList.value = arr.map((raw) => normalizeUserCouponListItem(raw, tabForRequest)).filter(Boolean);
-    await nextTick();
-    await scrollCouponListToTop();
+    const normalized = arr.map((raw) => normalizeUserCouponListItem(raw, tabForRequest)).filter(Boolean);
+
+    if (reset) {
+      couponList.value = normalized;
+    } else {
+      couponList.value = mergeCouponListById(couponList.value, normalized);
+    }
+    lastCouponPageLoaded.value = page;
+
+    const mergedLen = couponList.value.length;
+    if (total > 0) {
+      listHasMore.value = mergedLen < total;
+    } else {
+      listHasMore.value = normalized.length >= COUPON_PAGE_SIZE;
+    }
   } catch (err) {
     console.error('获取优惠券列表失败:', err);
-    uni.showToast({ title: '加载失败', icon: 'none' });
-    couponList.value = [];
-    await nextTick();
-    await scrollCouponListToTop();
+    if (reset) {
+      uni.showToast({ title: '加载失败', icon: 'none' });
+      couponList.value = [];
+      listHasMore.value = false;
+    } else {
+      uni.showToast({ title: '加载更多失败', icon: 'none' });
+    }
   } finally {
-    loading.value = false;
+    if (reset) {
+      loading.value = false;
+    } else {
+      loadMoreLoading.value = false;
+    }
   }
 };
 
+function handleCouponScrollToLower() {
+  if (typePanelOpen.value) return;
+  enqueueListFetch(() => fetchCouponList({ reset: false }));
+}
+
 /** longTermValid=1 显示长期有效;=0 显示 expirationTime + 到期 */
 function formatCouponExpireLine(coupon) {
   if (Number(coupon?.longTermValid) === 1) return '长期有效';
@@ -260,7 +285,7 @@ const handleShowRules = (coupon) => {
 
 // 使用 onShow 拉取数据(onLoad 在 uni-app Vue3 组合式 API 下可能不触发,onShow 更可靠)
 onShow(() => {
-  enqueueListFetch(() => fetchCouponList());
+  enqueueListFetch(() => fetchCouponList({ reset: true }));
 });
 </script>
 
@@ -459,6 +484,16 @@ onShow(() => {
   pointer-events: none;
 }
 
+.coupon-page-footer {
+  padding: 24rpx 0 40rpx;
+  text-align: center;
+}
+
+.coupon-page-footer__text {
+  font-size: 24rpx;
+  color: #999999;
+}
+
 .coupon-list {
   position: relative;
 

+ 18 - 28
pages/orderInfo/index.vue

@@ -78,6 +78,7 @@ import { onLoad } from "@dcloudio/uni-app";
 import { ref, computed, watch } from "vue";
 import { getFileUrl } from "@/utils/file.js";
 import { go } from "@/utils/utils.js";
+import { navigateToAddFood } from "@/utils/orderAddFoodNavigate.js";
 import { GetOrderPage } from "@/api/dining.js";
 
 const activeTab = ref('current');
@@ -269,14 +270,27 @@ watch(activeTab, (tab) => {
   loadOrderList(tab, false);
 });
 
-// 处理订单详情(须传 orderId 详情页才会请求接口
+// 处理订单详情:携带 tableId 等,详情接口未回桌台主键时仍可加餐(与列表去加餐一致
 const handleOrderDetail = (order) => {
   const orderId = order?.id ?? order?.orderId ?? '';
   if (!orderId) {
     uni.showToast({ title: '订单ID缺失', icon: 'none' });
     return;
   }
-  go(`/pages/orderInfo/orderDetail?orderId=${encodeURIComponent(String(orderId))}`);
+  const q = [`orderId=${encodeURIComponent(String(orderId))}`];
+  const tableId = order?.tableId ?? order?.storeTableId ?? order?.diningTableId ?? '';
+  if (tableId !== '' && tableId != null) {
+    q.push(`tableId=${encodeURIComponent(String(tableId))}`);
+  }
+  const storeId = order?.storeId ?? '';
+  if (storeId !== '' && storeId != null) {
+    q.push(`storeId=${encodeURIComponent(String(storeId))}`);
+  }
+  const diners = order?.dinerCount ?? order?.diners ?? '';
+  if (diners !== '' && diners != null) {
+    q.push(`dinerCount=${encodeURIComponent(String(diners))}`);
+  }
+  go(`/pages/orderInfo/orderDetail?${q.join('&')}`);
 };
 
 // 切换标签页
@@ -284,32 +298,8 @@ const switchTab = (tab) => {
   activeTab.value = tab;
 };
 
-// 处理加餐:携带订单桌台主键(非展示用桌号),点餐页用其拉购物车;redirectTo 保证重新 onLoad 避免沿用上一桌缓存
-const handleAddFood = (order) => {
-  const tableid =
-    order?.tableId ??
-    order?.storeTableId ??
-    order?.diningTableId ??
-    '';
-  const tid = tableid != null && tableid !== '' ? String(tableid).trim() : '';
-  if (!tid) {
-    uni.showToast({ title: '订单缺少桌台信息,无法加餐', icon: 'none' });
-    return;
-  }
-  const diners = order?.dinerCount ?? order?.diners ?? uni.getStorageSync('currentDiners') ?? '';
-  const storeId = order?.storeId ?? '';
-  if (storeId !== '' && storeId != null) {
-    uni.setStorageSync(STORAGE_STORE_ID, String(storeId));
-  }
-  uni.setStorageSync(STORAGE_TABLE_ID, tid);
-  const q = [
-    `tableid=${encodeURIComponent(tid)}`,
-    `tableId=${encodeURIComponent(tid)}`
-  ];
-  if (diners !== '' && diners != null) q.push(`diners=${encodeURIComponent(String(diners))}`);
-  if (storeId !== '' && storeId != null) q.push(`storeId=${encodeURIComponent(String(storeId))}`);
-  go(`/pages/orderFood/index?${q.join('&')}`, 'redirectTo');
-};
+// 处理加餐(逻辑见 utils/orderAddFoodNavigate.js)
+const handleAddFood = (order) => navigateToAddFood(order);
 
 // 处理结算:带上订单ID、桌号、人数、订单金额、备注,确认订单页用于调起支付
 const handleCheckout = (order) => {

+ 268 - 99
pages/orderInfo/orderDetail.vue

@@ -51,18 +51,15 @@
           <view class="info-item-value">¥{{ priceDetail.serviceFee }}</view>
         </view>
         -->
-        <view
-          v-if="isOrderCompleted && orderCompletedDiscountAmount > 0"
-          class="info-item"
-        >
+        <view v-if="showDiscountLine" class="info-item">
           <view class="info-item-label">优惠金额</view>
           <view class="info-item-value">
-            <text class="discount-amount-num">-¥{{ formatPrice(priceDetail.discountAmount) }}</text>
+            <text class="discount-amount-num">{{ discountAmountDisplayText }}</text>
           </view>
         </view>
         <view class="price-line">
           <view class="price-line-label">合计</view>
-          <view class="price-line-value">¥{{ formatPrice(actualPayAmount) }}</view>
+          <view class="price-line-value">¥{{ formatPrice(displayPayInPriceSection) }}</view>
         </view>
        
       </view>
@@ -117,9 +114,9 @@
       </view>
     </view>
 
-    <!-- 底部按钮:已完成(3)不显示 -->
-    <view v-if="orderDetail.orderStatus !== 3" class="bottom-button">
-      <view class="bottom-button-text1 " hover-class="hover-active" @click="handleConfirmOrder">去加餐</view>
+    <!-- 底部按钮:已完成(3)不显示(与 isOrderCompleted 一致用数值比较,避免字符串 "3" 仍显示按钮) -->
+    <view v-if="Number(orderDetail.orderStatus) !== 3" class="bottom-button">
+      <view class="bottom-button-text1 " hover-class="hover-active" @click="handleAddFood">去加餐</view>
       <view class="bottom-button-text" hover-class="hover-active" @click="handleConfirmPay">去结算</view>
     </view>
 
@@ -130,7 +127,7 @@
 import { onLoad } from "@dcloudio/uni-app";
 import { ref, computed } from "vue";
 import { getFileUrl } from "@/utils/file.js";
-import { go } from "@/utils/utils.js";
+import { navigateToAddFood } from "@/utils/orderAddFoodNavigate.js";
 import { GetOrderInfo } from "@/api/dining.js";
 
 const payType = ref('confirmOrder'); // confirmOrder 确认下单 confirmPay 确认支付
@@ -144,8 +141,10 @@ const orderDetail = ref({
   storeAddress: '',
   storeId: '',
   orderNo: '',
-  tableId: '',   // 桌号ID,用于跳转加餐
-  tableNo: '',    // 桌号展示(tableNumber)
+  tableId: '', // 桌台主键,加餐/购物车用
+  storeTableId: '',
+  diningTableId: '',
+  tableNo: '', // 桌号展示(tableNumber)
   dinerCount: '',
   createTime: '',
   contactPhone: '',
@@ -172,6 +171,9 @@ const priceDetail = ref({
   payAmount: 0
 });
 
+/** 订单详情接口已写入价格区(避免首屏初始 0 误展示优惠行) */
+const orderPriceSectionReady = ref(false);
+
 // 菜品列表(接口订单明细)
 const foodList = ref([]);
 
@@ -198,11 +200,45 @@ const calculatedDishTotalFromItems = computed(() => {
   return Math.max(0, Math.round(sum * 100) / 100);
 });
 
-// 合计/优惠计算用菜品基数:有明细则按行累计,否则用接口菜品总价
+// 合计/优惠计算用菜品基数:有明细则按行累计,并与接口菜品总价取较大值(避免行数据为 0 时把优惠上限压成 0 导致不展示)
 const dishTotalBaseForOrder = computed(() => {
-  if (displayFoodList.value.length > 0) return calculatedDishTotalFromItems.value;
-  const dish = Number(priceDetail.value?.dishTotal);
-  return Number.isFinite(dish) ? Math.max(0, dish) : 0;
+  const fromApi = Number(String(priceDetail.value?.dishTotal ?? '').replace(/,/g, ''));
+  const apiNum = Number.isFinite(fromApi) && fromApi > 0 ? fromApi : 0;
+  if (displayFoodList.value.length > 0) {
+    const fromItems = calculatedDishTotalFromItems.value;
+    return Math.max(fromItems, apiNum);
+  }
+  return apiNum;
+});
+
+/** 接口返回后且优惠大于 0 时才展示优惠行(为 0 不展示) */
+const showDiscountLine = computed(() => {
+  if (!orderPriceSectionReady.value) return false;
+  const n = Number(priceDetail.value?.discountAmount);
+  return Number.isFinite(n) && n > 0;
+});
+
+/** 价格明细「合计」:优先接口 payAmount,缺失时再走计算值 */
+const displayPayInPriceSection = computed(() => {
+  const p = priceDetail.value?.payAmount;
+  if (p != null && p !== '' && Number.isFinite(Number(p)) && Number(p) >= 0) {
+    return Math.round(Number(p) * 100) / 100;
+  }
+  return actualPayAmount.value;
+});
+
+/** 优惠金额文案(仅 showDiscountLine 为 true 时使用,即 n > 0) */
+const discountAmountDisplayText = computed(() => {
+  const n = Number(priceDetail.value?.discountAmount);
+  if (!Number.isFinite(n) || n <= 0) return '-¥0.00';
+  return `-¥${formatPrice(n)}`;
+});
+
+/** 接口 discountAmount(数值化;0 也保留,供展示与推导) */
+const rawDiscountFromApi = computed(() => {
+  const n = Number(priceDetail.value?.discountAmount);
+  if (!Number.isFinite(n) || n < 0) return 0;
+  return Math.round(n * 100) / 100;
 });
 
 /** 已支付:orderStatus 1 已支付、3 已完成 */
@@ -214,43 +250,38 @@ function isPaidOrderStatus(status) {
 /** 订单已完成 orderStatus === 3 */
 const isOrderCompleted = computed(() => Number(orderDetail.value?.orderStatus) === 3);
 
-// 待支付(0) / 已支付(1) 优惠金额(用于非已完成订单的合计计算)
+/** 已取消等不参与「待付场景」优惠推导 */
+const isCancelledOrder = computed(() => Number(orderDetail.value?.orderStatus) === 2);
+
+/** 非已完成、非取消:需要展示/推导优惠(含 orderStatus 为字符串或缺失导致 Number 为 NaN 的情况) */
+const isOpenOrderForDiscount = computed(
+  () => !isOrderCompleted.value && !isCancelledOrder.value
+);
+
+/** 接口未回 discountAmount 时,用「菜品基数 + 服务费 − 应付」反推优惠(与合计 payAmount 一致) */
+const impliedDiscountFromPay = computed(() => {
+  if (!isOpenOrderForDiscount.value) return 0;
+  const base = dishTotalBaseForOrder.value;
+  const fee = Number(priceDetail.value?.serviceFeeAmount) || 0;
+  const pay = Number(priceDetail.value?.payAmount);
+  if (!Number.isFinite(pay) || pay < 0 || base <= 0) return 0;
+  const diff = base + fee - pay;
+  if (diff <= 0.0001) return 0;
+  return Math.min(Math.max(0, Math.round(diff * 100) / 100), Math.max(0, base));
+});
+
+// 待支付 / 已支付等与已支付一致:优先接口 discountAmount,否则按「菜品基数 + 服务费 − 应付」反推,再按菜品基数上限裁剪
 const orderDiscountDisplay = computed(() => {
-  const st = orderDetail.value.orderStatus;
-  if (st !== 0 && st !== 1) return 0;
-  const p = priceDetail.value;
+  if (!isOpenOrderForDiscount.value) return 0;
   const dishBase = dishTotalBaseForOrder.value;
-  const type = Number(p.couponType);
-
-  let raw = 0;
-  if (type === 1) {
-    if (p.nominalValue != null && p.nominalValue !== '') {
-      const nv = Number(p.nominalValue);
-      raw = Number.isNaN(nv) ? 0 : Math.max(0, nv);
-    } else {
-      // 无面额时不用裸 discountAmount,避免无券订单接口噪声把合计减成 0
-      const name = (p.couponName && String(p.couponName).trim()) || '';
-      if (name) raw = Math.max(0, Number(p.discountAmount) || 0);
-    }
-  } else if (type === 2) {
-    if (p.discountRate != null && p.discountRate !== '' && dishBase > 0) {
-      const rate = Number(p.discountRate) || 0;
-      raw = Math.round(dishBase * (1 - rate / 10) * 100) / 100;
-    }
-  }
-  const capped = Math.min(Math.max(0, raw), Math.max(0, dishBase));
+  const explicit = rawDiscountFromApi.value;
+  const fromImplied = explicit > 0 ? 0 : impliedDiscountFromPay.value;
+  const d = Math.max(explicit, fromImplied);
+  const capped = Math.min(Math.max(0, d), Math.max(0, dishBase));
   return Math.max(0, Math.round(capped * 100) / 100);
 });
 
-/** 已完成订单:接口 discountAmount(用于展示「优惠金额」行) */
-const orderCompletedDiscountAmount = computed(() => {
-  if (!isOrderCompleted.value) return 0;
-  const d = Number(priceDetail.value?.discountAmount);
-  if (!Number.isFinite(d) || d <= 0) return 0;
-  return Math.round(d * 100) / 100;
-});
-
-// 合计:已完成绑定接口 payAmount;其余状态按「菜品基数 − 优惠」计算
+// 合计:已完成用接口 payAmount/total;待支付/已支付等非完成态与已支付一致:优先 payAmount,否则「菜品基数 − 优惠」
 const actualPayAmount = computed(() => {
   if (isOrderCompleted.value) {
     const pa = Number(priceDetail.value?.payAmount);
@@ -258,6 +289,8 @@ const actualPayAmount = computed(() => {
     const t = Number(priceDetail.value?.total);
     return Number.isFinite(t) ? Math.max(0, Math.round(t * 100) / 100) : 0;
   }
+  const pa = Number(priceDetail.value?.payAmount);
+  if (Number.isFinite(pa) && pa >= 0) return Math.round(pa * 100) / 100;
   const base = dishTotalBaseForOrder.value;
   const d = orderDiscountDisplay.value;
   return Math.max(0, Math.round((base - d) * 100) / 100);
@@ -265,7 +298,7 @@ const actualPayAmount = computed(() => {
 
 // 已支付(1) / 已完成(3) 展示支付信息卡片
 const showPaidOrderPaymentCard = computed(() => {
-  const s = orderDetail.value?.orderStatus;
+  const s = Number(orderDetail.value?.orderStatus);
   return s === 1 || s === 3;
 });
 
@@ -366,29 +399,71 @@ function normalizeOrderItem(item) {
 }
 
 // 从接口数据填充 orderDetail、priceDetail、foodList
+function pickFirstDefined(...vals) {
+  for (const v of vals) {
+    if (v != null && v !== '') return v;
+  }
+  return undefined;
+}
+
 function applyOrderData(data) {
-  const raw = data?.data ?? data ?? {};
-  const nested = raw?.order && typeof raw.order === 'object' ? raw.order : {};
+  orderPriceSectionReady.value = false;
+  const root = data?.data ?? data ?? {};
+  const nested = root?.order && typeof root.order === 'object' ? root.order : {};
+  const deepData =
+    root?.data && typeof root.data === 'object' && !Array.isArray(root.data) ? root.data : {};
+  const raw = root;
   const store = raw?.storeInfo ?? raw?.store ?? {};
+  const tableInfo = raw?.tableInfo ?? nested?.tableInfo ?? deepData?.tableInfo ?? {};
+  const storeTableIdVal =
+    raw?.storeTableId ??
+    nested?.storeTableId ??
+    deepData?.storeTableId ??
+    raw?.store_table_id ??
+    nested?.store_table_id ??
+    tableInfo?.storeTableId ??
+    '';
+  const diningTableIdVal =
+    raw?.diningTableId ??
+    nested?.diningTableId ??
+    deepData?.diningTableId ??
+    tableInfo?.diningTableId ??
+    '';
+  const tableIdVal =
+    pickFirstDefined(
+      raw?.tableId,
+      nested?.tableId,
+      deepData?.tableId,
+      storeTableIdVal,
+      diningTableIdVal,
+      tableInfo?.tableId,
+      tableInfo?.id,
+      raw?.table?.id,
+      nested?.table?.id
+    ) ?? '';
   orderDetail.value = {
     storeName: store?.storeName ?? raw?.storeName ?? '',
     storeAddress: store?.storeAddress ?? store?.address ?? raw?.storeAddress ?? raw?.address ?? '',
-    storeId: store?.storeId ?? store?.id ?? raw?.storeId ?? raw?.store_id ?? nested?.storeId ?? '',
+    storeId: store?.storeId ?? store?.id ?? raw?.storeId ?? raw?.store_id ?? nested?.storeId ?? deepData?.storeId ?? '',
     orderNo: raw?.orderNo ?? raw?.orderId ?? nested?.orderNo ?? '',
-    tableId:
-      raw?.tableId ??
-      nested?.tableId ??
-      raw?.storeTableId ??
-      nested?.storeTableId ??
-      raw?.diningTableId ??
-      nested?.diningTableId ??
-      '', // 桌台主键,加餐/购物车用
-    tableNo: raw?.tableNumber ?? raw?.tableNo ?? raw?.tableName ?? '',
-    dinerCount: raw?.dinerCount ?? '',
+    tableId: tableIdVal,
+    storeTableId: storeTableIdVal,
+    diningTableId: diningTableIdVal,
+    tableNo:
+      raw?.tableNumber ??
+      raw?.tableNo ??
+      nested?.tableNumber ??
+      nested?.tableNo ??
+      deepData?.tableNumber ??
+      tableInfo?.tableNumber ??
+      tableInfo?.tableNo ??
+      raw?.tableName ??
+      '',
+    dinerCount: raw?.dinerCount ?? nested?.dinerCount ?? deepData?.dinerCount ?? '',
     createTime: raw?.createdTime ?? raw?.createTime ?? raw?.orderTime ?? '',
     contactPhone: raw?.contactPhone ?? raw?.phone ?? '',
     remark: raw?.remark ?? '',
-    orderStatus: raw?.orderStatus ?? raw?.status ?? null,
+    orderStatus: raw?.orderStatus ?? nested?.orderStatus ?? raw?.status ?? nested?.status ?? null,
     payType: raw?.payType ?? raw?.payMethod ?? '',
     payTime:
       raw?.payTime ??
@@ -398,21 +473,107 @@ function applyOrderData(data) {
       raw?.paySuccessTime ??
       ''
   };
-  const couponInfo = raw?.couponInfo ?? raw?.coupon ?? {};
-  const dishTotal = raw?.totalAmount ?? raw?.dishTotal ?? raw?.orderAmount ?? raw?.foodAmount ?? 0;
-  const couponDiscount = raw?.couponAmount ?? raw?.couponDiscount ?? 0;
-  const discountAmountVal = Number(raw?.discountAmount ?? raw?.couponAmount ?? raw?.couponDiscount ?? 0) || 0;
+  const couponInfo = raw?.couponInfo ?? raw?.coupon ?? nested?.couponInfo ?? nested?.coupon ?? {};
+  const dishTotal =
+    raw?.totalAmount ??
+    nested?.totalAmount ??
+    deepData?.totalAmount ??
+    raw?.dishTotal ??
+    nested?.dishTotal ??
+    deepData?.dishTotal ??
+    raw?.orderAmount ??
+    nested?.orderAmount ??
+    raw?.foodAmount ??
+    nested?.foodAmount ??
+    0;
+  const couponDiscount =
+    raw?.couponAmount ??
+    nested?.couponAmount ??
+    deepData?.couponAmount ??
+    raw?.couponDiscount ??
+    nested?.couponDiscount ??
+    0;
+  /** 优惠金额:不能用「首个有限数字」合并 couponAmount,否则接口 couponAmount=0 会截断后面的 discountAmount(待支付常见) */
+  const discountPrimaryPick = pickFirstDefined(
+    raw.discountAmount,
+    nested.discountAmount,
+    deepData.discountAmount,
+    raw.discount_amount,
+    nested.discount_amount,
+    deepData.discount_amount,
+    raw.data?.discountAmount,
+    nested.data?.discountAmount,
+    raw.priceDetail?.discountAmount,
+    nested.priceDetail?.discountAmount,
+    deepData.priceDetail?.discountAmount,
+    raw.totalDiscountAmount,
+    nested.totalDiscountAmount,
+    deepData.totalDiscountAmount,
+    raw.totalDiscount,
+    nested.totalDiscount,
+    raw.couponDiscountAmount,
+    nested.couponDiscountAmount,
+    raw.couponDiscount,
+    nested.couponDiscount
+  );
+  const discountSecondaryPick = pickFirstDefined(
+    couponInfo.discountAmount,
+    couponInfo.amount,
+    couponInfo.nominalValue,
+    raw.couponAmount,
+    nested.couponAmount,
+    deepData.couponAmount
+  );
+  const discountPick = discountPrimaryPick ?? discountSecondaryPick;
+  const discountAmountVal =
+    discountPick != null && discountPick !== '' ? Number(discountPick) || 0 : 0;
   const totalFallback =
-    Number(raw?.totalAmount ?? raw?.totalPrice ?? raw?.orderAmount ?? raw?.foodAmount ?? 0) || 0;
-  const explicitPay = raw?.payAmount ?? raw?.realPayAmount ?? raw?.paidAmount;
+    Number(
+      raw?.totalAmount ??
+        nested?.totalAmount ??
+        deepData?.totalAmount ??
+        raw?.totalPrice ??
+        nested?.totalPrice ??
+        raw?.orderAmount ??
+        nested?.orderAmount ??
+        raw?.foodAmount ??
+        nested?.foodAmount ??
+        0
+    ) || 0;
+  const payPick = pickFirstDefined(
+    raw.payAmount,
+    nested.payAmount,
+    deepData.payAmount,
+    raw.waitPayAmount,
+    nested.waitPayAmount,
+    deepData.waitPayAmount,
+    raw.needPayAmount,
+    nested.needPayAmount,
+    deepData.needPayAmount,
+    raw.unpaidAmount,
+    nested.unpaidAmount,
+    raw.pay_amount,
+    nested.pay_amount,
+    deepData.pay_amount,
+    raw.realPayAmount,
+    nested.realPayAmount,
+    deepData.realPayAmount,
+    raw.real_pay_amount,
+    nested.real_pay_amount,
+    raw.paidAmount,
+    nested.paidAmount,
+    deepData.paidAmount
+  );
   const payAmountNum =
-    explicitPay != null && explicitPay !== ''
-      ? Number(explicitPay) || 0
-      : totalFallback;
+    payPick != null && payPick !== '' ? Number(payPick) || 0 : totalFallback;
   const serviceFeeRaw =
     Number(raw?.serviceFee ?? raw?.serviceCharge ?? raw?.tablewareFee ?? 0) || 0;
   const rawDiscountRate =
-    raw?.discountRate ?? couponInfo?.discountRate ?? raw?.coupon?.discountRate;
+    raw?.discountRate ??
+    nested?.discountRate ??
+    couponInfo?.discountRate ??
+    raw?.coupon?.discountRate ??
+    nested?.coupon?.discountRate;
   const discountRateVal = rawDiscountRate != null && rawDiscountRate !== '' ? (Number(rawDiscountRate) || 0) / 10 : null;
   priceDetail.value = {
     dishTotal: formatPrice(dishTotal),
@@ -420,47 +581,35 @@ function applyOrderData(data) {
     serviceFeeAmount: serviceFeeRaw,
     couponDiscount: formatPrice(couponDiscount),
     discountAmount: discountAmountVal,
-    couponName: raw?.couponName ?? couponInfo?.couponName ?? couponInfo?.name ?? '',
-    couponType: raw?.couponType ?? couponInfo?.couponType ?? null,
-    nominalValue: raw?.nominalValue ?? couponInfo?.nominalValue ?? couponInfo?.amount ?? null,
+    couponName:
+      raw?.couponName ?? nested?.couponName ?? couponInfo?.couponName ?? couponInfo?.name ?? '',
+    couponType: raw?.couponType ?? nested?.couponType ?? couponInfo?.couponType ?? null,
+    nominalValue:
+      raw?.nominalValue ?? nested?.nominalValue ?? couponInfo?.nominalValue ?? couponInfo?.amount ?? null,
     discountRate: discountRateVal,
     total: formatPrice(payAmountNum),
     payAmount: payAmountNum
   };
   const list =
     raw?.orderItemList ??
+    nested?.orderItemList ??
     raw?.orderItems ??
+    nested?.orderItems ??
     raw?.orderLines ??
+    nested?.orderLines ??
     raw?.orderLineList ??
+    nested?.orderLineList ??
     raw?.detailList ??
+    nested?.detailList ??
     raw?.order?.orderItemList ??
     raw?.order?.orderItems ??
     (Array.isArray(raw?.items) ? raw.items : []);
   foodList.value = (Array.isArray(list) ? list : []).map(normalizeOrderItem);
+  orderPriceSectionReady.value = true;
 }
 
-// 去加餐:使用本单桌台主键(与列表页一致),redirectTo 触发点餐页重新 onLoad 拉取该桌购物车
-const handleConfirmOrder = () => {
-  const od = orderDetail.value;
-  const tableid = String(od?.tableId ?? '').trim();
-  const diners = od?.dinerCount ?? '';
-  const storeId = od?.storeId ?? '';
-  if (!tableid) {
-    uni.showToast({ title: '订单缺少桌台信息,无法加餐', icon: 'none' });
-    return;
-  }
-  if (storeId !== '' && storeId != null) {
-    uni.setStorageSync('currentStoreId', String(storeId));
-  }
-  uni.setStorageSync('currentTableId', tableid);
-  const q = [
-    `tableid=${encodeURIComponent(tableid)}`,
-    `tableId=${encodeURIComponent(tableid)}`
-  ];
-  if (diners !== '' && diners != null) q.push(`diners=${encodeURIComponent(String(diners))}`);
-  if (storeId !== '' && storeId != null) q.push(`storeId=${encodeURIComponent(String(storeId))}`);
-  go(`/pages/orderFood/index?${q.join('&')}`, 'redirectTo');
-};
+// 去加餐(与订单列表页同一套逻辑,见 utils/orderAddFoodNavigate.js)
+const handleAddFood = () => navigateToAddFood(orderDetail.value);
 
 // 去结算:跳转确认订单页,携带订单ID、订单号、桌号、就餐人数、订单金额(用于调起支付)
 const handleConfirmPay = () => {
@@ -469,7 +618,7 @@ const handleConfirmPay = () => {
   const tableId = orderDetail.value?.tableId || orderDetail.value?.tableNo || '';
   const tableNumber = orderDetail.value?.tableNo ?? orderDetail.value?.tableNumber ?? '';
   const diners = orderDetail.value?.dinerCount ?? '';
-  const totalAmount = formatPrice(actualPayAmount.value);
+  const totalAmount = formatPrice(displayPayInPriceSection.value);
   const remark = (orderDetail.value?.remark ?? '').trim().slice(0, 30);
   const q = [];
   if (orderId !== '') q.push(`orderId=${encodeURIComponent(String(orderId))}`);
@@ -482,6 +631,23 @@ const handleConfirmPay = () => {
   uni.navigateTo({ url: q.length ? `/pages/checkout/index?${q.join('&')}` : '/pages/checkout/index' });
 };
 
+/** 列表进详情时 URL 携带的桌台/门店信息(详情接口未回 tableId 时用于加餐) */
+function mergeRouteOrderMeta(options) {
+  if (!options || typeof options !== 'object') return;
+  const routeTableId = String(options.tableId ?? options.tableid ?? '').trim();
+  const routeStoreId = String(options.storeId ?? options.storeid ?? '').trim();
+  const routeDiners = options.dinerCount ?? options.diners ?? '';
+  const od = orderDetail.value;
+  if (routeTableId && !String(od.tableId ?? '').trim()) {
+    od.tableId = routeTableId;
+    if (!String(od.storeTableId ?? '').trim()) od.storeTableId = routeTableId;
+  }
+  if (routeStoreId && !String(od.storeId ?? '').trim()) od.storeId = routeStoreId;
+  if (routeDiners !== '' && routeDiners != null && !String(od.dinerCount ?? '').trim()) {
+    od.dinerCount = routeDiners;
+  }
+}
+
 onLoad(async (e) => {
   if (e.payType) payType.value = e.payType;
   const orderId = e?.orderId ?? e?.id ?? '';
@@ -491,9 +657,12 @@ onLoad(async (e) => {
     return;
   }
   try {
+    orderPriceSectionReady.value = false;
     const res = await GetOrderInfo(orderId);
     applyOrderData(res);
+    mergeRouteOrderMeta(e);
   } catch (err) {
+    orderPriceSectionReady.value = false;
     console.error('订单详情加载失败:', err);
     uni.showToast({ title: '加载失败', icon: 'none' });
   }

+ 43 - 0
utils/orderAddFoodNavigate.js

@@ -0,0 +1,43 @@
+/**
+ * 订单「去加餐」:携带桌台主键跳转点餐页,与 pages/orderInfo/index 列表逻辑一致。
+ */
+import { go } from '@/utils/utils.js';
+
+const STORAGE_STORE_ID = 'currentStoreId';
+const STORAGE_TABLE_ID = 'currentTableId';
+
+/**
+ * @param {object} order 订单对象(列表项或详情 orderDetail)
+ */
+export function navigateToAddFood(order) {
+  let tableid =
+    order?.tableId ??
+    order?.storeTableId ??
+    order?.diningTableId ??
+    '';
+  let tid = tableid != null && tableid !== '' ? String(tableid).trim() : '';
+  // 详情接口可能只回展示桌号、不回主键;与当前点餐桌一致时用本地桌台主键兜底
+  if (!tid) {
+    const cached = uni.getStorageSync(STORAGE_TABLE_ID);
+    if (cached != null && String(cached).trim() !== '') {
+      tid = String(cached).trim();
+    }
+  }
+  if (!tid) {
+    uni.showToast({ title: '订单缺少桌台信息,无法加餐', icon: 'none' });
+    return;
+  }
+  const diners = order?.dinerCount ?? order?.diners ?? uni.getStorageSync('currentDiners') ?? '';
+  const storeId = order?.storeId ?? '';
+  if (storeId !== '' && storeId != null) {
+    uni.setStorageSync(STORAGE_STORE_ID, String(storeId));
+  }
+  uni.setStorageSync(STORAGE_TABLE_ID, tid);
+  const q = [
+    `tableid=${encodeURIComponent(tid)}`,
+    `tableId=${encodeURIComponent(tid)}`
+  ];
+  if (diners !== '' && diners != null) q.push(`diners=${encodeURIComponent(String(diners))}`);
+  if (storeId !== '' && storeId != null) q.push(`storeId=${encodeURIComponent(String(storeId))}`);
+  go(`/pages/orderFood/index?${q.join('&')}`, 'redirectTo');
+}

+ 1 - 1
utils/tableDiningLaunch.js

@@ -5,7 +5,7 @@
 import * as diningApi from '@/api/dining.js';
 import { parseSceneToStoreTable, isScanEntryAllowed } from '@/utils/qrScene.js';
 
-function trim(v) {
+export function trim(v) {
   return v == null ? '' : String(v).trim();
 }