index.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703
  1. <template>
  2. <!-- 点餐页 -->
  3. <view class="content">
  4. <view class="top-info">桌号:{{ displayTableNumber }} &nbsp;&nbsp;就餐人数:{{ currentDiners }}人</view>
  5. <!-- 搜索 -->
  6. <input type="text" placeholder="请输入菜品名称" class="search-input" />
  7. <!-- 内容 -->
  8. <view class="content-box">
  9. <!-- 左侧分类列表 -->
  10. <scroll-view class="category-list" scroll-y>
  11. <view v-for="(category, index) in categories" :key="index" class="category-item"
  12. :class="{ active: currentCategoryIndex === index }" @click="selectCategory(index)">
  13. {{ category.categoryName }}
  14. </view>
  15. </scroll-view>
  16. <!-- 右侧菜品列表 -->
  17. <scroll-view class="food-list" scroll-y>
  18. <FoodCard v-for="(food, index) in currentFoodList" :key="food.id || food.cuisineId || index" :food="food"
  19. :table-id="tableId" @increase="handleIncrease" @decrease="handleDecrease"/>
  20. </scroll-view>
  21. </view>
  22. <!-- 底部下单:按接口格式展示 totalQuantity / totalAmount -->
  23. <BottomActionBar :cart-list="displayCartList" :total-quantity="displayTotalQuantity"
  24. :total-amount="displayTotalAmount" @coupon-click="handleCouponClick" @cart-click="handleCartClick"
  25. @order-click="handleOrderClick" />
  26. <!-- 领取优惠券弹窗 -->
  27. <CouponModal v-model:open="couponModalOpen" :coupon-list="couponList" @receive="handleCouponReceive"
  28. @close="handleCouponClose" />
  29. <!-- 购物车弹窗:按接口格式展示 items(cuisineName/cuisineImage/quantity/unitPrice/subtotalAmount/remark) -->
  30. <CartModal v-model:open="cartModalOpen" :cart-list="displayCartList" :discount-amount="discountAmount"
  31. @increase="handleIncrease" @decrease="handleDecrease" @clear="handleCartClear"
  32. @coupon-click="handleSelectCouponClick" @order-click="handleOrderClick" @close="handleCartClose" />
  33. <!-- 选择优惠券弹窗 -->
  34. <SelectCouponModal v-model:open="selectCouponModalOpen" :coupon-list="availableCoupons"
  35. :selected-coupon-id="selectedCouponId" @select="handleCouponSelect" @close="handleSelectCouponClose" />
  36. </view>
  37. </template>
  38. <script setup>
  39. import { onLoad, onUnload } from "@dcloudio/uni-app";
  40. import { ref, computed } from "vue";
  41. import FoodCard from "./components/FoodCard.vue";
  42. import BottomActionBar from "./components/BottomActionBar.vue";
  43. import CouponModal from "./components/CouponModal.vue";
  44. import CartModal from "./components/CartModal.vue";
  45. import SelectCouponModal from "./components/SelectCouponModal.vue";
  46. import { go } from "@/utils/utils.js";
  47. import { DiningOrderFood, GetStoreCategories, GetStoreCuisines, getOrderSseConfig, GetOrderCart, PostOrderCartAdd, PostOrderCartUpdate, PostOrderCartClear } from "@/api/dining.js";
  48. import { createSSEConnection } from "@/utils/sse.js";
  49. const tableId = ref(''); // 桌号ID,来自上一页 url 参数 tableid,用于接口入参
  50. const tableNumber = ref(''); // 桌号展示,来自 dining/page-info 接口返回的 tableNumber
  51. const tableNumberFetched = ref(false); // 是否已请求过桌号(避免未返回前用 tableId 展示导致闪一下 43)
  52. const currentDiners = ref(uni.getStorageSync('currentDiners') || '');
  53. // 桌号展示:优先用接口返回的 tableNumber,未请求完前不显示 tableId,避免闪 43 再变 2
  54. const displayTableNumber = computed(() => {
  55. if (tableNumber.value) return tableNumber.value;
  56. if (tableNumberFetched.value && tableId.value) return tableId.value;
  57. return '—';
  58. });
  59. let sseRequestTask = null; // 订单 SSE 连接(封装后兼容小程序),页面卸载时需 abort()
  60. const currentCategoryIndex = ref(0);
  61. const couponModalOpen = ref(false);
  62. const cartModalOpen = ref(false);
  63. const selectCouponModalOpen = ref(false);
  64. const discountAmount = ref(12); // 优惠金额,示例数据
  65. const selectedCouponId = ref(null); // 选中的优惠券ID
  66. // 分类列表(由接口 /store/info/categories 返回后赋值)
  67. const categories = ref([]);
  68. // 菜品列表(由接口 /store/info/cuisines 按分类拉取并合并,每项含 quantity、categoryId)
  69. const foodList = ref([]);
  70. // SSE 连接建立后拉取的购物车数据,在 foodList 就绪后合并
  71. let pendingCartData = null;
  72. // 购物车接口返回的完整数据:{ items, totalAmount, totalQuantity },用于按接口格式展示
  73. const cartData = ref({ items: [], totalAmount: 0, totalQuantity: 0 });
  74. // 当前分类的菜品列表(按当前选中的分类 id 过滤)
  75. const currentFoodList = computed(() => {
  76. const cat = categories.value[currentCategoryIndex.value];
  77. if (!cat) return [];
  78. const categoryId = cat.id ?? cat.categoryId;
  79. return foodList.value.filter((food) => String(food?.categoryId ?? '') === String(categoryId ?? ''));
  80. });
  81. // 购物车列表:只包含数量>0 的菜品,并按菜品 id 去重(同一菜品只算一条,避免金额叠加)
  82. const cartList = computed(() => {
  83. const withQty = foodList.value.filter((food) => (food.quantity || 0) > 0);
  84. const seen = new Set();
  85. return withQty.filter((f) => {
  86. const id = String(f?.id ?? '');
  87. if (seen.has(id)) return false;
  88. seen.add(id);
  89. return true;
  90. });
  91. });
  92. // 用于展示的购物车列表:优先使用接口返回的 items(含 cuisineName/cuisineImage/unitPrice/subtotalAmount/remark),只展示数量>0
  93. const displayCartList = computed(() => {
  94. const apiItems = cartData.value?.items;
  95. if (Array.isArray(apiItems) && apiItems.length > 0) {
  96. return apiItems.filter((it) => (Number(it?.quantity) || 0) > 0);
  97. }
  98. return cartList.value;
  99. });
  100. // 行小计:接口项用 subtotalAmount,否则 数量×单价(与 BottomActionBar/CartModal 一致)
  101. const getItemLinePrice = (item) => {
  102. if (item?.subtotalAmount != null) return Number(item.subtotalAmount);
  103. const qty = Number(item?.quantity) || 0;
  104. const unitPrice = Number(item?.unitPrice ?? item?.price ?? item?.salePrice ?? item?.totalPrice ?? 0) || 0;
  105. return qty * unitPrice;
  106. };
  107. // 展示用总数量:优先接口/本地 cartData.totalQuantity,否则从展示列表计算
  108. const displayTotalQuantity = computed(() => {
  109. const apiItems = cartData.value?.items;
  110. if (Array.isArray(apiItems) && apiItems.length > 0 && cartData.value?.totalQuantity != null) {
  111. return Number(cartData.value.totalQuantity);
  112. }
  113. return displayCartList.value.reduce((sum, item) => sum + (Number(item?.quantity) || 0), 0);
  114. });
  115. // 展示用总金额:优先接口/本地 cartData.totalAmount,否则从展示列表按行小计累加
  116. const displayTotalAmount = computed(() => {
  117. const apiItems = cartData.value?.items;
  118. if (Array.isArray(apiItems) && apiItems.length > 0 && cartData.value?.totalAmount != null) {
  119. return Number(cartData.value.totalAmount);
  120. }
  121. return displayCartList.value.reduce((sum, item) => sum + getItemLinePrice(item), 0);
  122. });
  123. // 优惠券列表(示例数据)
  124. const couponList = ref([
  125. {
  126. id: 1,
  127. amount: 38,
  128. minAmount: 158,
  129. name: '优惠券名称',
  130. expireDate: '2024/07/28',
  131. isReceived: false
  132. },
  133. {
  134. id: 2,
  135. amount: 8,
  136. minAmount: 158,
  137. name: '优惠券名称',
  138. expireDate: '2024/07/28',
  139. isReceived: false
  140. },
  141. {
  142. id: 3,
  143. amount: 682,
  144. minAmount: 1580,
  145. name: '优惠券名称',
  146. expireDate: '2024/07/28',
  147. isReceived: true
  148. },
  149. {
  150. id: 4,
  151. amount: 1038,
  152. minAmount: 1580,
  153. name: '优惠券名称',
  154. expireDate: '2024/07/28',
  155. isReceived: true
  156. }
  157. ]);
  158. // 可用的优惠券列表(已领取的优惠券)
  159. const availableCoupons = computed(() => {
  160. return couponList.value.filter(coupon => coupon.isReceived);
  161. });
  162. // 分类 id 统一转字符串,避免 number/string 比较导致误判
  163. const sameCategory = (a, b) => String(a ?? '') === String(b ?? '');
  164. // 将购物车数量同步到菜品列表:用购物车接口的 items 按 cuisineId 匹配 foodList 中的菜品并更新 quantity;items 为空时清空所有菜品数量
  165. const mergeCartIntoFoodList = () => {
  166. const cartItems = pendingCartData ?? cartData.value?.items ?? [];
  167. if (!Array.isArray(cartItems)) return;
  168. if (!foodList.value.length) {
  169. if (cartItems.length > 0) console.log('购物车已缓存,等 foodList 加载后再同步到菜品列表');
  170. return;
  171. }
  172. foodList.value = foodList.value.map((item) => {
  173. const itemId = String(item?.id ?? item?.cuisineId ?? '');
  174. const cartItem = cartItems.find((c) => {
  175. const cId = String(c?.id ?? c?.cuisineId ?? c?.foodId ?? c?.dishId ?? '');
  176. return cId && cId === itemId;
  177. });
  178. const qty = cartItem
  179. ? Number(cartItem.quantity ?? cartItem.num ?? cartItem.count ?? 0) || 0
  180. : 0;
  181. return { ...item, quantity: qty };
  182. });
  183. pendingCartData = null;
  184. console.log('购物车数量已同步到菜品列表', cartItems.length ? '' : '(已清空)');
  185. };
  186. // 从接口返回的 data 层中解析出购物车数组(/store/order/cart/{id} 返回在 items 下)
  187. function parseCartListFromResponse(cartRes) {
  188. if (cartRes == null) return [];
  189. if (Array.isArray(cartRes.items)) return cartRes.items;
  190. // 优先 items(当前接口约定),再兼容 list / data / records 等
  191. const firstLevel =
  192. cartRes.items ??
  193. cartRes.list ??
  194. cartRes.data ??
  195. cartRes.records ??
  196. cartRes.cartList ??
  197. cartRes.cartItems ??
  198. cartRes.cart?.list ??
  199. cartRes.cart?.items ??
  200. cartRes.result?.list ??
  201. cartRes.result?.data ??
  202. cartRes.result?.items;
  203. if (Array.isArray(firstLevel)) return firstLevel;
  204. if (typeof cartRes === 'object') {
  205. const arr = Object.values(cartRes).find((v) => Array.isArray(v));
  206. if (arr) return arr;
  207. }
  208. return [];
  209. }
  210. // 调获取购物车接口并合并到 foodList(需在 foodList 有数据后调用才能返显到购物车)
  211. // 注意:request 层在 code=200 时只返回 res.data.data,故 cartRes 已是后端 data 层
  212. const fetchAndMergeCart = async (tableid) => {
  213. if (!tableid) return;
  214. try {
  215. const cartRes = await GetOrderCart(tableid);
  216. const list = parseCartListFromResponse(cartRes);
  217. pendingCartData = list;
  218. // 按接口格式绑定:data.items / totalAmount / totalQuantity
  219. cartData.value = {
  220. items: list,
  221. totalAmount: Number(cartRes?.totalAmount) || 0,
  222. totalQuantity: Number(cartRes?.totalQuantity) || 0
  223. };
  224. mergeCartIntoFoodList();
  225. console.log('购物车接口返回(data 层):', cartRes, '解析条数:', list.length);
  226. } catch (err) {
  227. console.error('获取购物车失败:', err);
  228. throw err;
  229. }
  230. };
  231. // 根据分类 id 拉取菜品并合并到 foodList,切换分类时保留其他分类已选菜品及本分类已选数量
  232. const fetchCuisinesByCategoryId = async (categoryId) => {
  233. if (!categoryId) return;
  234. try {
  235. const res = await GetStoreCuisines({ categoryId });
  236. const list = res?.list ?? res?.data ?? (Array.isArray(res) ? res : []);
  237. const normalized = (Array.isArray(list) ? list : []).map(item => ({
  238. ...item,
  239. quantity: item.quantity ?? 0,
  240. categoryId: item.categoryId ?? categoryId
  241. }));
  242. // 其他分类的菜品原样保留(含已选数量)
  243. const rest = foodList.value.filter(f => !sameCategory(f.categoryId, categoryId));
  244. // 本分类:按菜品 id 从整份列表里取已选数量,id 一致则自动带上数量
  245. const merged = normalized.map((newItem) => {
  246. const existing = foodList.value.find(
  247. (e) => String(e?.id ?? e?.cuisineId ?? '') === String(newItem?.id ?? newItem?.cuisineId ?? '')
  248. );
  249. const quantity = existing ? existing.quantity : (newItem.quantity ?? 0);
  250. return { ...newItem, quantity };
  251. });
  252. foodList.value = [...rest, ...merged];
  253. // 切换分类后,把购物车数量再同步到当前分类的菜品列表
  254. mergeCartIntoFoodList();
  255. } catch (err) {
  256. console.error('获取菜品失败:', err);
  257. }
  258. };
  259. // 选择分类:切换高亮并拉取该分类菜品
  260. const selectCategory = async (index) => {
  261. currentCategoryIndex.value = index;
  262. const cat = categories.value[index];
  263. if (!cat) return;
  264. const categoryId = cat.id ?? cat.categoryId;
  265. await fetchCuisinesByCategoryId(categoryId);
  266. };
  267. // 根据展示项(可能来自接口 cuisineId 或 foodList 的 id)找到 foodList 中的菜品
  268. const findFoodByCartItem = (item) => {
  269. const id = item?.id ?? item?.cuisineId;
  270. if (id == null) return null;
  271. return foodList.value.find((f) => String(f?.id ?? f?.cuisineId ?? '') === String(id));
  272. };
  273. // 同步 cartData:根据 cuisineId 更新 items 中对应项的 quantity、subtotalAmount,并重算 totalAmount、totalQuantity
  274. const syncCartDataFromFoodList = () => {
  275. const items = cartData.value?.items ?? [];
  276. if (!items.length) return;
  277. let totalAmount = 0;
  278. let totalQuantity = 0;
  279. const nextItems = items.map((it) => {
  280. const food = findFoodByCartItem(it);
  281. const qty = food != null ? (food.quantity || 0) : (it.quantity || 0);
  282. const unitPrice = Number(it?.unitPrice ?? it?.price ?? 0) || 0;
  283. const subtotalAmount = qty * unitPrice;
  284. totalAmount += subtotalAmount;
  285. totalQuantity += qty;
  286. return { ...it, quantity: qty, subtotalAmount };
  287. });
  288. cartData.value = { ...cartData.value, items: nextItems, totalAmount, totalQuantity };
  289. };
  290. // 更新菜品数量:菜品 id 一致则全部同步为同一数量,触发响应式;新增加入购物车时调接口;并同步 cartData
  291. // Update 接口返回 400 时不改页面数量和金额(会回滚本地状态)
  292. const updateFoodQuantity = (food, delta) => {
  293. if (!food) return;
  294. const id = food.id ?? food.cuisineId;
  295. const prevQty = food.quantity || 0;
  296. const nextQty = Math.max(0, prevQty + delta);
  297. const sameId = (item) =>
  298. String(item?.id ?? item?.cuisineId ?? '') === String(id ?? '');
  299. const items = cartData.value?.items ?? [];
  300. const idx = items.findIndex((it) => String(it?.cuisineId ?? it?.id ?? '') === String(id));
  301. const applyQuantity = (qty) => {
  302. foodList.value = foodList.value.map((item) =>
  303. sameId(item) ? { ...item, quantity: qty } : item
  304. );
  305. const currentItems = cartData.value?.items ?? [];
  306. const currentIdx = currentItems.findIndex((it) => String(it?.cuisineId ?? it?.id ?? '') === String(id));
  307. if (currentIdx >= 0) {
  308. const it = currentItems[currentIdx];
  309. const unitPrice = Number(it?.unitPrice ?? it?.price ?? 0) || 0;
  310. const nextItems = currentItems.slice();
  311. nextItems[currentIdx] = { ...it, quantity: qty, subtotalAmount: qty * unitPrice };
  312. const totalAmount = nextItems.reduce((s, i) => s + (Number(i.subtotalAmount) || 0), 0);
  313. const totalQuantity = nextItems.reduce((s, i) => s + (Number(i.quantity) || 0), 0);
  314. cartData.value = { ...cartData.value, items: nextItems, totalAmount, totalQuantity };
  315. } else if (qty > 0) {
  316. const unitPrice = Number(food?.price ?? food?.unitPrice ?? food?.salePrice ?? 0) || 0;
  317. const newItem = {
  318. cuisineId: id,
  319. cuisineName: food?.name ?? food?.cuisineName ?? '',
  320. cuisineImage: food?.image ?? food?.cuisineImage ?? food?.imageUrl ?? '',
  321. quantity: qty,
  322. unitPrice,
  323. subtotalAmount: qty * unitPrice
  324. };
  325. const nextItems = [...currentItems, newItem];
  326. const totalAmount = nextItems.reduce((s, i) => s + (Number(i.subtotalAmount) || 0), 0);
  327. const totalQuantity = nextItems.reduce((s, i) => s + (Number(i.quantity) || 0), 0);
  328. cartData.value = { ...cartData.value, items: nextItems, totalAmount, totalQuantity };
  329. }
  330. };
  331. applyQuantity(nextQty);
  332. if (tableId.value && delta !== 0) {
  333. const needAdd = delta > 0 && (idx < 0 || nextQty === 1);
  334. if (needAdd) {
  335. PostOrderCartAdd({
  336. cuisineId: id,
  337. quantity: nextQty,
  338. tableId: tableId.value
  339. }).catch((err) => {
  340. console.error('加入购物车失败:', err);
  341. applyQuantity(prevQty);
  342. });
  343. } else {
  344. PostOrderCartUpdate({
  345. cuisineId: id,
  346. quantity: nextQty,
  347. tableId: tableId.value
  348. }).catch((err) => {
  349. console.error('更新购物车失败:', err);
  350. applyQuantity(prevQty);
  351. });
  352. }
  353. }
  354. };
  355. // 增加数量:支持传入接口项(cuisineId)或菜品项(id),先解析为 foodList 中的 food
  356. const handleIncrease = (item) => {
  357. const food = item?.id != null && item?.cuisineId == null ? item : findFoodByCartItem(item) ?? item;
  358. updateFoodQuantity(food, 1);
  359. };
  360. // 减少数量
  361. const handleDecrease = (item) => {
  362. const food = item?.id != null && item?.cuisineId == null ? item : findFoodByCartItem(item) ?? item;
  363. if (food && (food.quantity || 0) > 0) updateFoodQuantity(food, -1);
  364. };
  365. // 优惠券点击(领取优惠券弹窗)
  366. const handleCouponClick = () => {
  367. // 先关闭其他弹窗
  368. if (cartModalOpen.value) {
  369. cartModalOpen.value = false;
  370. }
  371. if (selectCouponModalOpen.value) {
  372. selectCouponModalOpen.value = false;
  373. }
  374. couponModalOpen.value = true;
  375. };
  376. // 选择优惠券点击(从购物车弹窗中点击)
  377. const handleSelectCouponClick = () => {
  378. // 先关闭其他弹窗
  379. if (cartModalOpen.value) {
  380. cartModalOpen.value = false;
  381. }
  382. if (couponModalOpen.value) {
  383. couponModalOpen.value = false;
  384. }
  385. // 打开选择优惠券弹窗
  386. selectCouponModalOpen.value = true;
  387. };
  388. // 处理优惠券选择
  389. const handleCouponSelect = ({ coupon, index, selectedId }) => {
  390. selectedCouponId.value = selectedId;
  391. // 根据选中的优惠券更新优惠金额
  392. if (selectedId) {
  393. discountAmount.value = coupon.amount;
  394. } else {
  395. discountAmount.value = 0;
  396. }
  397. // 选择后关闭选择优惠券弹窗,打开购物车弹窗
  398. selectCouponModalOpen.value = false;
  399. // 延迟打开购物车弹窗,确保选择优惠券弹窗完全关闭
  400. setTimeout(() => {
  401. cartModalOpen.value = true;
  402. }, 100);
  403. };
  404. // 选择优惠券弹窗关闭
  405. const handleSelectCouponClose = () => {
  406. selectCouponModalOpen.value = false;
  407. };
  408. // 优惠券领取
  409. const handleCouponReceive = ({ coupon, index }) => {
  410. console.log('领取优惠券:', coupon);
  411. // 更新优惠券状态
  412. couponList.value[index].isReceived = true;
  413. uni.showToast({
  414. title: '领取成功',
  415. icon: 'success'
  416. });
  417. // TODO: 调用接口领取优惠券
  418. };
  419. // 优惠券弹窗关闭
  420. const handleCouponClose = () => {
  421. couponModalOpen.value = false;
  422. };
  423. // 购物车点击
  424. const handleCartClick = () => {
  425. if (displayCartList.value.length === 0) {
  426. uni.showToast({
  427. title: '购物车为空',
  428. icon: 'none'
  429. });
  430. return;
  431. }
  432. // 先关闭其他弹窗
  433. if (couponModalOpen.value) {
  434. couponModalOpen.value = false;
  435. }
  436. if (selectCouponModalOpen.value) {
  437. selectCouponModalOpen.value = false;
  438. }
  439. cartModalOpen.value = true;
  440. };
  441. // 购物车弹窗关闭
  442. const handleCartClose = () => {
  443. cartModalOpen.value = false;
  444. };
  445. // 清空购物车:调用 /store/order/cart/clear,入参 tableId,成功后清空本地并关闭弹窗
  446. const handleCartClear = () => {
  447. if (!tableId.value) {
  448. foodList.value = foodList.value.map((f) => ({ ...f, quantity: 0 }));
  449. cartData.value = { items: [], totalAmount: 0, totalQuantity: 0 };
  450. cartModalOpen.value = false;
  451. uni.showToast({ title: '已清空购物车', icon: 'success' });
  452. return;
  453. }
  454. PostOrderCartClear(tableId.value)
  455. .then(() => {
  456. foodList.value = foodList.value.map((f) => ({ ...f, quantity: 0 }));
  457. cartData.value = { items: [], totalAmount: 0, totalQuantity: 0 };
  458. cartModalOpen.value = false;
  459. uni.showToast({ title: '已清空购物车', icon: 'success' });
  460. })
  461. .catch((err) => {
  462. console.error('清空购物车失败:', err);
  463. uni.showToast({ title: '清空失败,请重试', icon: 'none' });
  464. });
  465. };
  466. // 下单点击:先带购物车数据跳转确认订单页,创建订单在确认页点击「确认下单」时再调
  467. const handleOrderClick = () => {
  468. const cartPayload = {
  469. list: displayCartList.value,
  470. totalAmount: displayTotalAmount.value,
  471. totalQuantity: displayTotalQuantity.value,
  472. tableId: tableId.value,
  473. tableNumber: tableNumber.value,
  474. diners: currentDiners.value
  475. };
  476. uni.setStorageSync('placeOrderCart', JSON.stringify(cartPayload));
  477. const query = [];
  478. if (tableId.value) query.push(`tableId=${encodeURIComponent(tableId.value)}`);
  479. if (tableNumber.value) query.push(`tableNumber=${encodeURIComponent(tableNumber.value)}`);
  480. if (currentDiners.value) query.push(`diners=${encodeURIComponent(currentDiners.value)}`);
  481. go(query.length ? `/pages/placeOrder/index?${query.join('&')}` : '/pages/placeOrder/index');
  482. };
  483. onLoad(async (options) => {
  484. // 获取上一页(选择就餐人数页)传来的桌号和就餐人数
  485. const tableid = options.tableid || '';
  486. const diners = options.diners || '';
  487. tableId.value = tableid;
  488. currentDiners.value = diners;
  489. if (tableid) uni.setStorageSync('currentTableId', tableid);
  490. console.log('点餐页接收参数 - 桌号(tableid):', tableid, '就餐人数(diners):', diners);
  491. // 先拉取购物车并缓存,再加载菜品,保证合并时 pendingCartData 已就绪,避免不返显
  492. if (tableid) {
  493. await fetchAndMergeCart(tableid).catch((err) => console.error('获取购物车失败:', err));
  494. }
  495. // 页面加载时创建订单 SSE 连接(仅 SSE 使用 utils/sse 封装,兼容微信小程序)
  496. if (tableid) {
  497. try {
  498. const sseConfig = getOrderSseConfig(tableid);
  499. sseRequestTask = createSSEConnection(sseConfig.url, sseConfig);
  500. sseRequestTask.onMessage((msg) => {
  501. console.log('SSE 收到:', msg);
  502. if (msg.event === 'cart_update' && msg.data) {
  503. try {
  504. const payload = typeof msg.data === 'string' ? JSON.parse(msg.data) : msg.data;
  505. const items = payload?.items ?? [];
  506. cartData.value = {
  507. items: Array.isArray(items) ? items : [],
  508. totalAmount: Number(payload?.totalAmount) || 0,
  509. totalQuantity: Number(payload?.totalQuantity) || 0
  510. };
  511. pendingCartData = null;
  512. mergeCartIntoFoodList();
  513. } catch (e) {
  514. console.error('SSE 购物车数据解析失败:', e);
  515. }
  516. }
  517. });
  518. sseRequestTask.onOpen(async () => {
  519. console.log('SSE 连接已建立');
  520. await fetchAndMergeCart(tableid).catch(() => {});
  521. });
  522. sseRequestTask.onError((err) => console.error('SSE 错误:', err));
  523. } catch (e) {
  524. console.error('SSE 连接失败:', e);
  525. }
  526. }
  527. // 调用点餐页接口,入参 dinerCount、tableId
  528. if (tableid || diners) {
  529. try {
  530. const res = await DiningOrderFood({
  531. tableId: tableid,
  532. dinerCount: diners
  533. });
  534. console.log('点餐页接口返回:', res);
  535. const data = res?.data ?? res ?? {};
  536. tableNumber.value = data?.tableNumber ?? data?.tableNo ?? '';
  537. tableNumberFetched.value = true;
  538. // 成功后调接口获取菜品种类(storeId 取自点餐页接口返回,若无则需从别处获取)
  539. const storeId = res?.storeId ?? data?.storeId ?? '';
  540. if (storeId) uni.setStorageSync('currentStoreId', storeId);
  541. if (storeId) {
  542. try {
  543. const categoriesRes = await GetStoreCategories({ storeId });
  544. const list = categoriesRes?.list ?? categoriesRes?.data ?? categoriesRes;
  545. if (Array.isArray(list) && list.length) {
  546. categories.value = list;
  547. // 默认用第一项的分类 id 拉取菜品
  548. const firstCat = list[0];
  549. const firstCategoryId = firstCat.id ?? firstCat.categoryId;
  550. if (firstCategoryId) {
  551. const cuisinesRes = await GetStoreCuisines({ categoryId: firstCategoryId });
  552. const cuisinesList = cuisinesRes?.list ?? cuisinesRes?.data ?? (Array.isArray(cuisinesRes) ? cuisinesRes : []);
  553. foodList.value = (Array.isArray(cuisinesList) ? cuisinesList : []).map(item => ({
  554. ...item,
  555. quantity: item.quantity ?? 0,
  556. categoryId: item.categoryId ?? firstCategoryId
  557. }));
  558. console.log('默认分类菜品:', cuisinesRes);
  559. mergeCartIntoFoodList();
  560. }
  561. }
  562. console.log('菜品种类:', categoriesRes);
  563. } catch (err) {
  564. console.error('获取菜品种类失败:', err);
  565. }
  566. }
  567. } catch (e) {
  568. console.error('点餐页接口失败:', e);
  569. tableNumberFetched.value = true;
  570. }
  571. } else {
  572. tableNumberFetched.value = true;
  573. }
  574. });
  575. onUnload(() => {
  576. if (sseRequestTask && typeof sseRequestTask.abort === 'function') {
  577. sseRequestTask.abort();
  578. }
  579. sseRequestTask = null;
  580. });
  581. </script>
  582. <style lang="scss" scoped>
  583. .content {
  584. display: flex;
  585. flex-direction: column;
  586. height: 100vh;
  587. background-color: #f7f9fa;
  588. padding-bottom: 100rpx;
  589. box-sizing: border-box;
  590. }
  591. .top-info {
  592. font-size: 28rpx;
  593. color: #888888;
  594. text-align: center;
  595. width: 100%;
  596. padding: 20rpx 0;
  597. background-color: #fff;
  598. }
  599. .search-input {
  600. width: 90%;
  601. margin-left: 5%;
  602. height: 80rpx;
  603. background-color: #fff;
  604. border-radius: 40rpx;
  605. padding: 0 20rpx;
  606. margin-top: 20rpx;
  607. box-sizing: border-box;
  608. font-size: 28rpx;
  609. text-align: center;
  610. }
  611. .content-box {
  612. display: flex;
  613. flex: 1;
  614. overflow: hidden;
  615. margin-top: 20rpx;
  616. }
  617. // 左侧分类列表
  618. .category-list {
  619. width: 180rpx;
  620. background-color: #fff;
  621. height: 100%;
  622. }
  623. .category-item {
  624. height: 100rpx;
  625. display: flex;
  626. align-items: center;
  627. justify-content: center;
  628. font-size: 28rpx;
  629. color: #999;
  630. background-color: #fff;
  631. transition: all 0.3s;
  632. &.active {
  633. background-color: #fff4e6;
  634. color: #333;
  635. font-weight: 600;
  636. }
  637. }
  638. // 右侧菜品列表
  639. .food-list {
  640. flex: 1;
  641. // background-color: #fff;
  642. // padding: 0 20rpx;
  643. margin: 0 20rpx;
  644. }
  645. </style>