orderDetail.vue 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905
  1. <template>
  2. <!-- 订单详情页面 -->
  3. <view class="content">
  4. <!-- 菜品清单 -->
  5. <view class="card">
  6. <!-- 店铺信息 -->
  7. <view class="store-info">
  8. <view class="store-info-title">{{ orderDetail.storeName || '店铺名称' }}</view>
  9. <view class="store-info-address">{{ orderDetail.storeAddress || '—' }}</view>
  10. </view>
  11. <view class="card-header">
  12. <view class="card-header-title">菜品详情</view>
  13. </view>
  14. <view class="card-content">
  15. <view class="info-food">
  16. <view v-for="(item, index) in displayFoodList" :key="item.id || index" class="food-item">
  17. <image :src="getItemImage(item)" mode="aspectFill" class="food-item__image"></image>
  18. <!-- 菜品信息 -->
  19. <view class="food-item__info">
  20. <view class="food-item__name">{{ item.name }}</view>
  21. <view class="food-item__desc" v-if="item.tags && item.tags.length > 0">
  22. <text v-for="(tag, tagIndex) in item.tags" :key="tagIndex" class="food-item__tag"
  23. :class="tag.type">{{ tag.text }}<text v-if="tagIndex < item.tags.length - 1">,</text>
  24. </text>
  25. </view>
  26. </view>
  27. <!-- 价格和数量 -->
  28. <view class="food-item__right">
  29. <view class="food-item__price">
  30. <text class="price-main">¥{{ formatPrice(item.price) }}</text>
  31. </view>
  32. <view class="food-item__quantity">{{ item.quantity || 1 }}份</view>
  33. </view>
  34. </view>
  35. </view>
  36. </view>
  37. <view class="card-header">
  38. <view class="card-header-title">价格明细</view>
  39. </view>
  40. <view class="card-content">
  41. <view class="info-item">
  42. <view class="info-item-label">菜品总价</view>
  43. <view class="info-item-value">¥{{ priceDetail.dishTotal }}</view>
  44. </view>
  45. <!-- 本版本不参与服务费
  46. <view class="info-item">
  47. <view class="info-item-label">服务费</view>
  48. <view class="info-item-value">¥{{ priceDetail.serviceFee }}</view>
  49. </view>
  50. -->
  51. <view v-if="showDiscountLine" class="info-item">
  52. <view class="info-item-label">优惠金额</view>
  53. <view class="info-item-value">
  54. <text class="discount-amount-num">{{ discountAmountDisplayText }}</text>
  55. </view>
  56. </view>
  57. <view class="price-line">
  58. <view class="price-line-label">合计</view>
  59. <view class="price-line-value">¥{{ formatPrice(displayPayInPriceSection) }}</view>
  60. </view>
  61. </view>
  62. </view>
  63. <!-- 已支付 / 已完成:支付信息(支付方式、支付时间) -->
  64. <view v-if="showPaidOrderPaymentCard" class="card">
  65. <view class="card-header">
  66. <view class="card-header-title">支付信息</view>
  67. </view>
  68. <view class="card-content">
  69. <view class="info-item">
  70. <view class="info-item-label">支付方式</view>
  71. <view class="info-item-value">微信支付</view>
  72. </view>
  73. <view class="info-item">
  74. <view class="info-item-label">支付时间</view>
  75. <view class="info-item-value">{{ payTimeDisplay }}</view>
  76. </view>
  77. </view>
  78. </view>
  79. <!-- 订单卡片 -->
  80. <view class="card">
  81. <view class="card-header">
  82. <view class="card-header-title">订单信息</view>
  83. </view>
  84. <view class="card-content">
  85. <view class="info-item">
  86. <view class="info-item-label">订单编号</view>
  87. <view class="info-item-value">{{ orderDetail.orderNo || '—' }}</view>
  88. </view>
  89. <view class="info-item">
  90. <view class="info-item-label">就餐桌号</view>
  91. <view class="info-item-value">{{ orderDetail.tableNo || '—' }}</view>
  92. </view>
  93. <view class="info-item">
  94. <view class="info-item-label">用餐人数</view>
  95. <view class="info-item-value">{{ orderDetail.dinerCount != null && orderDetail.dinerCount !== '' ? orderDetail.dinerCount + '人' : '—' }}</view>
  96. </view>
  97. <view class="info-item">
  98. <view class="info-item-label">下单时间</view>
  99. <view class="info-item-value">{{ orderDetail.createTime || '—' }}</view>
  100. </view>
  101. <view class="info-item">
  102. <view class="info-item-label">联系电话</view>
  103. <view class="info-item-value">{{ orderDetail.contactPhone || '—' }}</view>
  104. </view>
  105. <view class="info-item">
  106. <view class="info-item-label">备注信息</view>
  107. <view class="info-item-value">{{ (orderDetail.remark || '').trim() || '无' }}</view>
  108. </view>
  109. </view>
  110. </view>
  111. <!-- 底部按钮:已完成(3)不显示(与 isOrderCompleted 一致用数值比较,避免字符串 "3" 仍显示按钮) -->
  112. <view v-if="Number(orderDetail.orderStatus) !== 3" class="bottom-button">
  113. <view class="bottom-button-text1 " hover-class="hover-active" @click="handleAddFood">去加餐</view>
  114. <view class="bottom-button-text" hover-class="hover-active" @click="handleConfirmPay">去结算</view>
  115. </view>
  116. </view>
  117. </template>
  118. <script setup>
  119. import { onLoad } from "@dcloudio/uni-app";
  120. import { ref, computed } from "vue";
  121. import { getFileUrl } from "@/utils/file.js";
  122. import { navigateToAddFood } from "@/utils/orderAddFoodNavigate.js";
  123. import { GetOrderInfo } from "@/api/dining.js";
  124. const payType = ref('confirmOrder'); // confirmOrder 确认下单 confirmPay 确认支付
  125. /** 当前订单ID,用于「去结算」传参 */
  126. const detailOrderId = ref('');
  127. // 订单详情(店铺、订单信息)
  128. // orderStatus:0 待支付,1 已支付,2 已取消,3 已完成
  129. const orderDetail = ref({
  130. storeName: '',
  131. storeAddress: '',
  132. storeId: '',
  133. orderNo: '',
  134. tableId: '', // 桌台主键,加餐/购物车用
  135. storeTableId: '',
  136. diningTableId: '',
  137. tableNo: '', // 桌号展示(tableNumber)
  138. dinerCount: '',
  139. createTime: '',
  140. contactPhone: '',
  141. remark: '',
  142. orderStatus: null,
  143. payType: '', // 支付方式编码,如 wechatPayMininProgram
  144. /** 支付完成时间 */
  145. payTime: ''
  146. });
  147. // 价格明细(两位小数)
  148. const priceDetail = ref({
  149. dishTotal: '0.00',
  150. serviceFee: '0.00',
  151. serviceFeeAmount: 0,
  152. couponDiscount: '0.00',
  153. discountAmount: 0,
  154. couponName: '',
  155. couponType: null,
  156. nominalValue: null,
  157. discountRate: null,
  158. total: '0.00',
  159. /** 接口实付(元);已完成订单详情「合计」直接绑定此字段 */
  160. payAmount: 0
  161. });
  162. /** 订单详情接口已写入价格区(避免首屏初始 0 误展示优惠行) */
  163. const orderPriceSectionReady = ref(false);
  164. // 菜品列表(接口订单明细)
  165. const foodList = ref([]);
  166. // 菜品详情展示(排除特殊占位 id=-1)
  167. const displayFoodList = computed(() =>
  168. (foodList.value ?? []).filter((it) => Number(it?.id ?? it?.cuisineId) !== -1)
  169. );
  170. // 未支付订单:按明细行累计菜品总价(与 checkout 行小计口径一致)
  171. const calculatedDishTotalFromItems = computed(() => {
  172. const list = displayFoodList.value;
  173. let sum = 0;
  174. for (const it of list) {
  175. const ls = Number(it?.lineSubtotal);
  176. // 行小计为 0 时仍可能单价×数量>0(接口占位 0),不要用 0 覆盖真实金额
  177. if (Number.isFinite(ls) && ls > 0) {
  178. sum += ls;
  179. continue;
  180. }
  181. const q = Number(it?.quantity) || 1;
  182. const u = Number(it?.price) || 0;
  183. sum += u * q;
  184. }
  185. return Math.max(0, Math.round(sum * 100) / 100);
  186. });
  187. // 合计/优惠计算用菜品基数:有明细则按行累计,并与接口菜品总价取较大值(避免行数据为 0 时把优惠上限压成 0 导致不展示)
  188. const dishTotalBaseForOrder = computed(() => {
  189. const fromApi = Number(String(priceDetail.value?.dishTotal ?? '').replace(/,/g, ''));
  190. const apiNum = Number.isFinite(fromApi) && fromApi > 0 ? fromApi : 0;
  191. if (displayFoodList.value.length > 0) {
  192. const fromItems = calculatedDishTotalFromItems.value;
  193. return Math.max(fromItems, apiNum);
  194. }
  195. return apiNum;
  196. });
  197. /** 接口返回后且优惠大于 0 时才展示优惠行(为 0 不展示) */
  198. const showDiscountLine = computed(() => {
  199. if (!orderPriceSectionReady.value) return false;
  200. const n = Number(priceDetail.value?.discountAmount);
  201. return Number.isFinite(n) && n > 0;
  202. });
  203. /** 价格明细「合计」:优先接口 payAmount,缺失时再走计算值 */
  204. const displayPayInPriceSection = computed(() => {
  205. const p = priceDetail.value?.payAmount;
  206. if (p != null && p !== '' && Number.isFinite(Number(p)) && Number(p) >= 0) {
  207. return Math.round(Number(p) * 100) / 100;
  208. }
  209. return actualPayAmount.value;
  210. });
  211. /** 优惠金额文案(仅 showDiscountLine 为 true 时使用,即 n > 0) */
  212. const discountAmountDisplayText = computed(() => {
  213. const n = Number(priceDetail.value?.discountAmount);
  214. if (!Number.isFinite(n) || n <= 0) return '-¥0.00';
  215. return `-¥${formatPrice(n)}`;
  216. });
  217. /** 接口 discountAmount(数值化;0 也保留,供展示与推导) */
  218. const rawDiscountFromApi = computed(() => {
  219. const n = Number(priceDetail.value?.discountAmount);
  220. if (!Number.isFinite(n) || n < 0) return 0;
  221. return Math.round(n * 100) / 100;
  222. });
  223. /** 已支付:orderStatus 1 已支付、3 已完成 */
  224. function isPaidOrderStatus(status) {
  225. const s = Number(status);
  226. return s === 1 || s === 3;
  227. }
  228. /** 订单已完成 orderStatus === 3 */
  229. const isOrderCompleted = computed(() => Number(orderDetail.value?.orderStatus) === 3);
  230. /** 已取消等不参与「待付场景」优惠推导 */
  231. const isCancelledOrder = computed(() => Number(orderDetail.value?.orderStatus) === 2);
  232. /** 非已完成、非取消:需要展示/推导优惠(含 orderStatus 为字符串或缺失导致 Number 为 NaN 的情况) */
  233. const isOpenOrderForDiscount = computed(
  234. () => !isOrderCompleted.value && !isCancelledOrder.value
  235. );
  236. /** 接口未回 discountAmount 时,用「菜品基数 + 服务费 − 应付」反推优惠(与合计 payAmount 一致) */
  237. const impliedDiscountFromPay = computed(() => {
  238. if (!isOpenOrderForDiscount.value) return 0;
  239. const base = dishTotalBaseForOrder.value;
  240. const fee = Number(priceDetail.value?.serviceFeeAmount) || 0;
  241. const pay = Number(priceDetail.value?.payAmount);
  242. if (!Number.isFinite(pay) || pay < 0 || base <= 0) return 0;
  243. const diff = base + fee - pay;
  244. if (diff <= 0.0001) return 0;
  245. return Math.min(Math.max(0, Math.round(diff * 100) / 100), Math.max(0, base));
  246. });
  247. // 待支付 / 已支付等与已支付一致:优先接口 discountAmount,否则按「菜品基数 + 服务费 − 应付」反推,再按菜品基数上限裁剪
  248. const orderDiscountDisplay = computed(() => {
  249. if (!isOpenOrderForDiscount.value) return 0;
  250. const dishBase = dishTotalBaseForOrder.value;
  251. const explicit = rawDiscountFromApi.value;
  252. const fromImplied = explicit > 0 ? 0 : impliedDiscountFromPay.value;
  253. const d = Math.max(explicit, fromImplied);
  254. const capped = Math.min(Math.max(0, d), Math.max(0, dishBase));
  255. return Math.max(0, Math.round(capped * 100) / 100);
  256. });
  257. // 合计:已完成用接口 payAmount/total;待支付/已支付等非完成态与已支付一致:优先 payAmount,否则「菜品基数 − 优惠」
  258. const actualPayAmount = computed(() => {
  259. if (isOrderCompleted.value) {
  260. const pa = Number(priceDetail.value?.payAmount);
  261. if (Number.isFinite(pa) && pa >= 0) return Math.round(pa * 100) / 100;
  262. const t = Number(priceDetail.value?.total);
  263. return Number.isFinite(t) ? Math.max(0, Math.round(t * 100) / 100) : 0;
  264. }
  265. const pa = Number(priceDetail.value?.payAmount);
  266. if (Number.isFinite(pa) && pa >= 0) return Math.round(pa * 100) / 100;
  267. const base = dishTotalBaseForOrder.value;
  268. const d = orderDiscountDisplay.value;
  269. return Math.max(0, Math.round((base - d) * 100) / 100);
  270. });
  271. // 已支付(1) / 已完成(3) 展示支付信息卡片
  272. const showPaidOrderPaymentCard = computed(() => {
  273. const s = Number(orderDetail.value?.orderStatus);
  274. return s === 1 || s === 3;
  275. });
  276. // 支付时间
  277. const payTimeDisplay = computed(() => {
  278. const t = orderDetail.value?.payTime;
  279. if (t != null && String(t).trim() !== '') return String(t).trim();
  280. return '—';
  281. });
  282. // 取第一张图:逗号分隔取首段,数组取首项,对象取 url/path/src(与 orderInfo/index、FoodCard 一致)
  283. function firstImage(val) {
  284. if (val == null || val === '') return '';
  285. if (Array.isArray(val)) {
  286. const first = val[0];
  287. if (first != null && first !== '') {
  288. if (typeof first === 'object' && first !== null) return first.url ?? first.path ?? first.src ?? first.link ?? '';
  289. return String(first).split(/[,,]/)[0].trim();
  290. }
  291. return '';
  292. }
  293. if (typeof val === 'object') return val.url ?? val.path ?? val.src ?? val.link ?? '';
  294. const str = String(val).trim();
  295. return str ? str.split(/[,,]/)[0].trim() : '';
  296. }
  297. function getItemImage(item) {
  298. const raw = item?.image ?? item?.cuisineImage ?? item?.imageUrl ?? item?.pic ?? item?.cover ?? item?.images ?? '';
  299. const url = firstImage(raw) || (typeof raw === 'string' ? raw.split(/[,,]/)[0]?.trim() : '');
  300. if (url && typeof url === 'string' && (url.startsWith('http') || url.startsWith('//'))) return url;
  301. return getFileUrl(url || 'img/icon/shop.png');
  302. }
  303. // 金额保留两位小数
  304. const formatPrice = (price) => {
  305. if (price === '' || price === null || price === undefined) return '0.00';
  306. const num = Number(price);
  307. return Number.isNaN(num) ? '0.00' : num.toFixed(2);
  308. };
  309. // 将 tags 统一为 [{ text, type }] 格式
  310. function normalizeTags(raw) {
  311. if (raw == null) return [];
  312. let arr = [];
  313. if (Array.isArray(raw)) arr = raw;
  314. else if (typeof raw === 'string') {
  315. const t = raw.trim();
  316. if (t.startsWith('[')) { try { arr = JSON.parse(t); if (!Array.isArray(arr)) arr = []; } catch { arr = t ? [t] : []; } }
  317. else arr = t ? t.split(/[,,、\s]+/).map(s => s.trim()).filter(Boolean) : [];
  318. } else if (raw && typeof raw === 'object') arr = Array.isArray(raw.list) ? raw.list : Array.isArray(raw.items) ? raw.items : [];
  319. return arr.map((it) => {
  320. if (it == null) return { text: '', type: '' };
  321. if (typeof it === 'string') return { text: it, type: '' };
  322. if (typeof it === 'number') return { text: String(it), type: '' };
  323. return { text: it.text ?? it.tagName ?? it.name ?? it.label ?? it.title ?? '', type: it.type ?? it.tagType ?? '' };
  324. }).filter((t) => t.text !== '' && t.text != null);
  325. }
  326. // 将接口订单项转为列表项(图片取首张;行小计与 checkout 一致,便于未支付订单合计按明细计算)
  327. function normalizeOrderItem(item) {
  328. const rawTags = item?.tags ?? item?.tagList ?? item?.tagNames ?? item?.labels ?? item?.tag;
  329. const rawImg = item?.images ?? item?.image ?? item?.cuisineImage ?? item?.imageUrl ?? item?.pic ?? item?.cover ?? '';
  330. const imageUrl = firstImage(rawImg) || (typeof rawImg === 'string' ? rawImg.split(/[,,]/)[0]?.trim() : '') || 'img/icon/shop.png';
  331. const qty = Number(item?.quantity ?? 1) || 1;
  332. let lineSubtotal = 0;
  333. if (item?.subtotalAmount != null && item.subtotalAmount !== '') {
  334. lineSubtotal = Number(item.subtotalAmount) || 0;
  335. } else if (item?.totalPrice != null && item.totalPrice !== '') {
  336. lineSubtotal = Number(item.totalPrice) || 0;
  337. } else {
  338. const unit = Number(item?.unitPrice ?? item?.price ?? item?.salePrice ?? 0) || 0;
  339. lineSubtotal = unit * qty;
  340. }
  341. // 接口可能把小计写成 0,但单价/数量有效,回退为单价×数量,与列表展示一致
  342. if (lineSubtotal <= 0) {
  343. const unit = Number(item?.unitPrice ?? item?.price ?? item?.salePrice ?? 0) || 0;
  344. const fallback = unit * qty;
  345. if (fallback > 0) lineSubtotal = fallback;
  346. }
  347. const unitForDisplay =
  348. Number(item?.unitPrice ?? item?.price ?? 0) ||
  349. (qty > 0 ? lineSubtotal / qty : 0);
  350. return {
  351. id: item?.id ?? item?.cuisineId ?? item?.orderItemId ?? item?.skuId,
  352. name:
  353. item?.cuisineName ??
  354. item?.name ??
  355. item?.goodsName ??
  356. item?.skuName ??
  357. item?.productName ??
  358. '',
  359. price: unitForDisplay,
  360. lineSubtotal,
  361. image: imageUrl,
  362. quantity: qty,
  363. tags: normalizeTags(rawTags)
  364. };
  365. }
  366. // 从接口数据填充 orderDetail、priceDetail、foodList
  367. function pickFirstDefined(...vals) {
  368. for (const v of vals) {
  369. if (v != null && v !== '') return v;
  370. }
  371. return undefined;
  372. }
  373. function applyOrderData(data) {
  374. orderPriceSectionReady.value = false;
  375. const root = data?.data ?? data ?? {};
  376. const nested = root?.order && typeof root.order === 'object' ? root.order : {};
  377. const deepData =
  378. root?.data && typeof root.data === 'object' && !Array.isArray(root.data) ? root.data : {};
  379. const raw = root;
  380. const store = raw?.storeInfo ?? raw?.store ?? {};
  381. const tableInfo = raw?.tableInfo ?? nested?.tableInfo ?? deepData?.tableInfo ?? {};
  382. const storeTableIdVal =
  383. raw?.storeTableId ??
  384. nested?.storeTableId ??
  385. deepData?.storeTableId ??
  386. raw?.store_table_id ??
  387. nested?.store_table_id ??
  388. tableInfo?.storeTableId ??
  389. '';
  390. const diningTableIdVal =
  391. raw?.diningTableId ??
  392. nested?.diningTableId ??
  393. deepData?.diningTableId ??
  394. tableInfo?.diningTableId ??
  395. '';
  396. const tableIdVal =
  397. pickFirstDefined(
  398. raw?.tableId,
  399. nested?.tableId,
  400. deepData?.tableId,
  401. storeTableIdVal,
  402. diningTableIdVal,
  403. tableInfo?.tableId,
  404. tableInfo?.id,
  405. raw?.table?.id,
  406. nested?.table?.id
  407. ) ?? '';
  408. orderDetail.value = {
  409. storeName: store?.storeName ?? raw?.storeName ?? '',
  410. storeAddress: store?.storeAddress ?? store?.address ?? raw?.storeAddress ?? raw?.address ?? '',
  411. storeId: store?.storeId ?? store?.id ?? raw?.storeId ?? raw?.store_id ?? nested?.storeId ?? deepData?.storeId ?? '',
  412. orderNo: raw?.orderNo ?? raw?.orderId ?? nested?.orderNo ?? '',
  413. tableId: tableIdVal,
  414. storeTableId: storeTableIdVal,
  415. diningTableId: diningTableIdVal,
  416. tableNo:
  417. raw?.tableNumber ??
  418. raw?.tableNo ??
  419. nested?.tableNumber ??
  420. nested?.tableNo ??
  421. deepData?.tableNumber ??
  422. tableInfo?.tableNumber ??
  423. tableInfo?.tableNo ??
  424. raw?.tableName ??
  425. '',
  426. dinerCount: raw?.dinerCount ?? nested?.dinerCount ?? deepData?.dinerCount ?? '',
  427. createTime: raw?.createdTime ?? raw?.createTime ?? raw?.orderTime ?? '',
  428. contactPhone: raw?.contactPhone ?? raw?.phone ?? '',
  429. remark: raw?.remark ?? '',
  430. orderStatus: raw?.orderStatus ?? nested?.orderStatus ?? raw?.status ?? nested?.status ?? null,
  431. payType: raw?.payType ?? raw?.payMethod ?? '',
  432. payTime:
  433. raw?.payTime ??
  434. raw?.paymentTime ??
  435. raw?.paidTime ??
  436. raw?.payFinishTime ??
  437. raw?.paySuccessTime ??
  438. ''
  439. };
  440. const couponInfo = raw?.couponInfo ?? raw?.coupon ?? nested?.couponInfo ?? nested?.coupon ?? {};
  441. const dishTotal =
  442. raw?.totalAmount ??
  443. nested?.totalAmount ??
  444. deepData?.totalAmount ??
  445. raw?.dishTotal ??
  446. nested?.dishTotal ??
  447. deepData?.dishTotal ??
  448. raw?.orderAmount ??
  449. nested?.orderAmount ??
  450. raw?.foodAmount ??
  451. nested?.foodAmount ??
  452. 0;
  453. const couponDiscount =
  454. raw?.couponAmount ??
  455. nested?.couponAmount ??
  456. deepData?.couponAmount ??
  457. raw?.couponDiscount ??
  458. nested?.couponDiscount ??
  459. 0;
  460. /** 优惠金额:不能用「首个有限数字」合并 couponAmount,否则接口 couponAmount=0 会截断后面的 discountAmount(待支付常见) */
  461. const discountPrimaryPick = pickFirstDefined(
  462. raw.discountAmount,
  463. nested.discountAmount,
  464. deepData.discountAmount,
  465. raw.discount_amount,
  466. nested.discount_amount,
  467. deepData.discount_amount,
  468. raw.data?.discountAmount,
  469. nested.data?.discountAmount,
  470. raw.priceDetail?.discountAmount,
  471. nested.priceDetail?.discountAmount,
  472. deepData.priceDetail?.discountAmount,
  473. raw.totalDiscountAmount,
  474. nested.totalDiscountAmount,
  475. deepData.totalDiscountAmount,
  476. raw.totalDiscount,
  477. nested.totalDiscount,
  478. raw.couponDiscountAmount,
  479. nested.couponDiscountAmount,
  480. raw.couponDiscount,
  481. nested.couponDiscount
  482. );
  483. const discountSecondaryPick = pickFirstDefined(
  484. couponInfo.discountAmount,
  485. couponInfo.amount,
  486. couponInfo.nominalValue,
  487. raw.couponAmount,
  488. nested.couponAmount,
  489. deepData.couponAmount
  490. );
  491. const discountPick = discountPrimaryPick ?? discountSecondaryPick;
  492. const discountAmountVal =
  493. discountPick != null && discountPick !== '' ? Number(discountPick) || 0 : 0;
  494. const totalFallback =
  495. Number(
  496. raw?.totalAmount ??
  497. nested?.totalAmount ??
  498. deepData?.totalAmount ??
  499. raw?.totalPrice ??
  500. nested?.totalPrice ??
  501. raw?.orderAmount ??
  502. nested?.orderAmount ??
  503. raw?.foodAmount ??
  504. nested?.foodAmount ??
  505. 0
  506. ) || 0;
  507. const payPick = pickFirstDefined(
  508. raw.payAmount,
  509. nested.payAmount,
  510. deepData.payAmount,
  511. raw.waitPayAmount,
  512. nested.waitPayAmount,
  513. deepData.waitPayAmount,
  514. raw.needPayAmount,
  515. nested.needPayAmount,
  516. deepData.needPayAmount,
  517. raw.unpaidAmount,
  518. nested.unpaidAmount,
  519. raw.pay_amount,
  520. nested.pay_amount,
  521. deepData.pay_amount,
  522. raw.realPayAmount,
  523. nested.realPayAmount,
  524. deepData.realPayAmount,
  525. raw.real_pay_amount,
  526. nested.real_pay_amount,
  527. raw.paidAmount,
  528. nested.paidAmount,
  529. deepData.paidAmount
  530. );
  531. const payAmountNum =
  532. payPick != null && payPick !== '' ? Number(payPick) || 0 : totalFallback;
  533. const serviceFeeRaw =
  534. Number(raw?.serviceFee ?? raw?.serviceCharge ?? raw?.tablewareFee ?? 0) || 0;
  535. const rawDiscountRate =
  536. raw?.discountRate ??
  537. nested?.discountRate ??
  538. couponInfo?.discountRate ??
  539. raw?.coupon?.discountRate ??
  540. nested?.coupon?.discountRate;
  541. const discountRateVal = rawDiscountRate != null && rawDiscountRate !== '' ? (Number(rawDiscountRate) || 0) / 10 : null;
  542. priceDetail.value = {
  543. dishTotal: formatPrice(dishTotal),
  544. serviceFee: formatPrice(serviceFeeRaw),
  545. serviceFeeAmount: serviceFeeRaw,
  546. couponDiscount: formatPrice(couponDiscount),
  547. discountAmount: discountAmountVal,
  548. couponName:
  549. raw?.couponName ?? nested?.couponName ?? couponInfo?.couponName ?? couponInfo?.name ?? '',
  550. couponType: raw?.couponType ?? nested?.couponType ?? couponInfo?.couponType ?? null,
  551. nominalValue:
  552. raw?.nominalValue ?? nested?.nominalValue ?? couponInfo?.nominalValue ?? couponInfo?.amount ?? null,
  553. discountRate: discountRateVal,
  554. total: formatPrice(payAmountNum),
  555. payAmount: payAmountNum
  556. };
  557. const list =
  558. raw?.orderItemList ??
  559. nested?.orderItemList ??
  560. raw?.orderItems ??
  561. nested?.orderItems ??
  562. raw?.orderLines ??
  563. nested?.orderLines ??
  564. raw?.orderLineList ??
  565. nested?.orderLineList ??
  566. raw?.detailList ??
  567. nested?.detailList ??
  568. raw?.order?.orderItemList ??
  569. raw?.order?.orderItems ??
  570. (Array.isArray(raw?.items) ? raw.items : []);
  571. foodList.value = (Array.isArray(list) ? list : []).map(normalizeOrderItem);
  572. orderPriceSectionReady.value = true;
  573. }
  574. // 去加餐(与订单列表页同一套逻辑,见 utils/orderAddFoodNavigate.js)
  575. const handleAddFood = () => navigateToAddFood(orderDetail.value);
  576. // 去结算:跳转确认订单页,携带订单ID、订单号、桌号、就餐人数、订单金额(用于调起支付)
  577. const handleConfirmPay = () => {
  578. const orderId = detailOrderId.value || '';
  579. const orderNo = orderDetail.value?.orderNo ?? '';
  580. const tableId = orderDetail.value?.tableId || orderDetail.value?.tableNo || '';
  581. const tableNumber = orderDetail.value?.tableNo ?? orderDetail.value?.tableNumber ?? '';
  582. const diners = orderDetail.value?.dinerCount ?? '';
  583. const totalAmount = formatPrice(displayPayInPriceSection.value);
  584. const remark = (orderDetail.value?.remark ?? '').trim().slice(0, 30);
  585. const q = [];
  586. if (orderId !== '') q.push(`orderId=${encodeURIComponent(String(orderId))}`);
  587. if (orderNo !== '') q.push(`orderNo=${encodeURIComponent(String(orderNo))}`);
  588. if (tableId !== '') q.push(`tableId=${encodeURIComponent(String(tableId))}`);
  589. if (tableNumber !== '') q.push(`tableNumber=${encodeURIComponent(String(tableNumber))}`);
  590. if (diners !== '') q.push(`diners=${encodeURIComponent(String(diners))}`);
  591. if (totalAmount !== '' && totalAmount != null) q.push(`totalAmount=${encodeURIComponent(String(totalAmount))}`);
  592. if (remark !== '') q.push(`remark=${encodeURIComponent(remark)}`);
  593. uni.navigateTo({ url: q.length ? `/pages/checkout/index?${q.join('&')}` : '/pages/checkout/index' });
  594. };
  595. /** 列表进详情时 URL 携带的桌台/门店信息(详情接口未回 tableId 时用于加餐) */
  596. function mergeRouteOrderMeta(options) {
  597. if (!options || typeof options !== 'object') return;
  598. const routeTableId = String(options.tableId ?? options.tableid ?? '').trim();
  599. const routeStoreId = String(options.storeId ?? options.storeid ?? '').trim();
  600. const routeDiners = options.dinerCount ?? options.diners ?? '';
  601. const od = orderDetail.value;
  602. if (routeTableId && !String(od.tableId ?? '').trim()) {
  603. od.tableId = routeTableId;
  604. if (!String(od.storeTableId ?? '').trim()) od.storeTableId = routeTableId;
  605. }
  606. if (routeStoreId && !String(od.storeId ?? '').trim()) od.storeId = routeStoreId;
  607. if (routeDiners !== '' && routeDiners != null && !String(od.dinerCount ?? '').trim()) {
  608. od.dinerCount = routeDiners;
  609. }
  610. }
  611. onLoad(async (e) => {
  612. if (e.payType) payType.value = e.payType;
  613. const orderId = e?.orderId ?? e?.id ?? '';
  614. detailOrderId.value = orderId;
  615. if (!orderId) {
  616. uni.showToast({ title: '缺少订单ID', icon: 'none' });
  617. return;
  618. }
  619. try {
  620. orderPriceSectionReady.value = false;
  621. const res = await GetOrderInfo(orderId);
  622. applyOrderData(res);
  623. mergeRouteOrderMeta(e);
  624. } catch (err) {
  625. orderPriceSectionReady.value = false;
  626. console.error('订单详情加载失败:', err);
  627. uni.showToast({ title: '加载失败', icon: 'none' });
  628. }
  629. });
  630. </script>
  631. <style lang="scss" scoped>
  632. .content {
  633. padding: 0 30rpx 300rpx;
  634. }
  635. .card {
  636. background-color: #fff;
  637. border-radius: 24rpx;
  638. padding: 30rpx 0;
  639. margin-top: 20rpx;
  640. .card-header {
  641. display: flex;
  642. justify-content: space-between;
  643. align-items: center;
  644. padding: 0 30rpx;
  645. height: 40rpx;
  646. position: relative;
  647. font-size: 27rpx;
  648. color: #151515;
  649. font-weight: bold;
  650. }
  651. .tag {
  652. width: 10rpx;
  653. height: 42rpx;
  654. background: linear-gradient(35deg, #FCB73F 0%, #FC733D 100%);
  655. border-radius: 0rpx 0rpx 0rpx 0rpx;
  656. position: absolute;
  657. left: 0;
  658. top: 0;
  659. }
  660. .card-content {
  661. padding: 0 30rpx;
  662. }
  663. .info-item {
  664. display: flex;
  665. justify-content: space-between;
  666. align-items: center;
  667. margin-top: 20rpx;
  668. font-size: 27rpx;
  669. .info-item-label {
  670. color: #666666;
  671. }
  672. .info-item-value {
  673. color: #151515;
  674. .discount-amount-num {
  675. color: #e61f19;
  676. }
  677. }
  678. }
  679. .info-item-textarea {
  680. width: 100%;
  681. height: 115rpx;
  682. border-radius: 8rpx;
  683. border: 1rpx solid #F2F2F2;
  684. margin-top: 20rpx;
  685. font-size: 23rpx;
  686. color: #AAAAAA;
  687. box-sizing: border-box;
  688. padding: 20rpx;
  689. }
  690. .info-food {
  691. margin-top: 20rpx;
  692. }
  693. .food-item {
  694. display: flex;
  695. align-items: center;
  696. padding: 20rpx 0;
  697. &:last-child {
  698. border-bottom: none;
  699. }
  700. &__image {
  701. width: 118rpx;
  702. height: 118rpx;
  703. border-radius: 8rpx;
  704. flex-shrink: 0;
  705. background-color: #f5f5f5;
  706. }
  707. &__info {
  708. flex: 1;
  709. margin-left: 20rpx;
  710. display: flex;
  711. flex-direction: column;
  712. justify-content: center;
  713. }
  714. &__name {
  715. font-size: 28rpx;
  716. font-weight: bold;
  717. color: #151515;
  718. margin-bottom: 8rpx;
  719. }
  720. &__desc {
  721. font-size: 24rpx;
  722. color: #666666;
  723. line-height: 1.5;
  724. }
  725. &__tag {
  726. font-size: 24rpx;
  727. color: #666666;
  728. &.signature {
  729. color: #FC793D;
  730. }
  731. &.spicy {
  732. color: #2E2E2E;
  733. }
  734. }
  735. &__right {
  736. display: flex;
  737. flex-direction: column;
  738. align-items: flex-end;
  739. justify-content: center;
  740. margin-left: 20rpx;
  741. }
  742. &__price {
  743. display: flex;
  744. align-items: baseline;
  745. color: #151515;
  746. font-weight: bold;
  747. margin-bottom: 8rpx;
  748. .price-symbol {
  749. font-size: 20rpx;
  750. margin-right: 2rpx;
  751. }
  752. .price-main {
  753. font-size: 28rpx;
  754. }
  755. .price-decimal {
  756. font-size: 24rpx;
  757. }
  758. }
  759. &__quantity {
  760. font-size: 24rpx;
  761. color: #797979;
  762. }
  763. }
  764. .price-line {
  765. display: flex;
  766. justify-content: space-between;
  767. align-items: center;
  768. margin-top: 20rpx;
  769. font-size: 27rpx;
  770. color: #151515;
  771. border-top: 1rpx solid rgba(170, 170, 170, 0.15);
  772. font-weight: bold;
  773. padding-top: 20rpx;
  774. }
  775. }
  776. .bottom-button {
  777. width: 100%;
  778. background-color: #fff;
  779. padding: 20rpx 30rpx;
  780. box-sizing: border-box;
  781. position: fixed;
  782. bottom: 0;
  783. left: 0;
  784. right: 0;
  785. z-index: 999;
  786. padding-bottom: env(safe-area-inset-bottom);
  787. box-shadow: 0rpx -11rpx 46rpx 0rpx rgba(0, 0, 0, 0.05);
  788. display: flex;
  789. justify-content: space-between;
  790. align-items: center;
  791. .bottom-button-text1 {
  792. font-size: 32rpx;
  793. font-weight: bold;
  794. color: #fff;
  795. width: 324rpx;
  796. height: 80rpx;
  797. border-radius: 23rpx 23rpx 23rpx 23rpx;
  798. border: 2rpx solid #F47D1F;
  799. display: flex;
  800. align-items: center;
  801. color: #F47D1F;
  802. justify-content: center;
  803. }
  804. .bottom-button-text {
  805. font-size: 32rpx;
  806. font-weight: bold;
  807. color: #fff;
  808. background: linear-gradient(90deg, #FCB73F 0%, #FC743D 100%);
  809. border-radius: 23rpx 23rpx 23rpx 23rpx;
  810. display: flex;
  811. align-items: center;
  812. justify-content: center;
  813. width: 324rpx;
  814. height: 80rpx;
  815. }
  816. }
  817. .store-info {
  818. border-bottom: 1rpx solid rgba(170, 170, 170, 0.15);
  819. box-sizing: border-box;
  820. margin: 0 30rpx 30rpx;
  821. .store-info-title {
  822. font-size: 27rpx;
  823. color: #151515;
  824. font-weight: bold;
  825. margin-bottom: 10rpx;
  826. }
  827. .store-info-address {
  828. font-size: 23rpx;
  829. color: #AAAAAA;
  830. margin-bottom: 30rpx;
  831. }
  832. }
  833. </style>