|
|
@@ -1,6 +1,8 @@
|
|
|
<template>
|
|
|
- <!-- 点餐页 -->
|
|
|
- <view class="content">
|
|
|
+ <!-- 点餐页:左上角永远返回主页 -->
|
|
|
+ <view class="page-wrap">
|
|
|
+ <NavBar title="点餐" only-home />
|
|
|
+ <view class="content">
|
|
|
<view class="top-info">桌号:{{ displayTableNumber }} 就餐人数:{{ currentDiners }}人</view>
|
|
|
<!-- 搜索 -->
|
|
|
<input type="text" placeholder="请输入菜品名称" class="search-input" />
|
|
|
@@ -28,30 +30,31 @@
|
|
|
@order-click="handleOrderClick" />
|
|
|
|
|
|
<!-- 领取优惠券弹窗 -->
|
|
|
- <CouponModal v-model:open="couponModalOpen" :coupon-list="couponList" @receive="handleCouponReceive"
|
|
|
- @close="handleCouponClose" />
|
|
|
+ <CouponModal v-model:open="couponModalOpen" :coupon-list="couponList" @close="handleCouponClose" />
|
|
|
|
|
|
<!-- 购物车弹窗:按接口格式展示 items(cuisineName/cuisineImage/quantity/unitPrice/subtotalAmount/remark) -->
|
|
|
- <CartModal v-model:open="cartModalOpen" :cart-list="displayCartList" :discount-amount="discountAmount"
|
|
|
+ <CartModal v-model:open="cartModalOpen" :cart-list="displayCartList"
|
|
|
@increase="handleIncrease" @decrease="handleDecrease" @clear="handleCartClear"
|
|
|
- @coupon-click="handleSelectCouponClick" @order-click="handleOrderClick" @close="handleCartClose" />
|
|
|
+ @order-click="handleOrderClick" @close="handleCartClose" />
|
|
|
|
|
|
- <!-- 选择优惠券弹窗 -->
|
|
|
+ <!-- 选择优惠券弹窗:左下角入口为仅查看,购物车内入口为可选 -->
|
|
|
<SelectCouponModal v-model:open="selectCouponModalOpen" :coupon-list="availableCoupons"
|
|
|
- :selected-coupon-id="selectedCouponId" @select="handleCouponSelect" @close="handleSelectCouponClose" />
|
|
|
+ :selected-coupon-id="selectedCouponId" :view-only="selectCouponViewOnly" @select="handleCouponSelect" @close="handleSelectCouponClose" />
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { onLoad, onUnload } from "@dcloudio/uni-app";
|
|
|
-import { ref, computed } from "vue";
|
|
|
+import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
|
|
|
+import NavBar from "@/components/NavBar/index.vue";
|
|
|
+import { ref, computed, nextTick } from "vue";
|
|
|
import FoodCard from "./components/FoodCard.vue";
|
|
|
import BottomActionBar from "./components/BottomActionBar.vue";
|
|
|
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, getOrderSseConfig, GetOrderCart, PostOrderCartAdd, PostOrderCartUpdate, PostOrderCartClear } from "@/api/dining.js";
|
|
|
+import { DiningOrderFood, GetStoreCategories, GetStoreCuisines, getOrderSseConfig, GetOrderCart, PostOrderCartAdd, PostOrderCartUpdate, PostOrderCartClear, PutOrderCartUpdateTableware, GetUserOwnedCouponList } from "@/api/dining.js";
|
|
|
import { createSSEConnection } from "@/utils/sse.js";
|
|
|
|
|
|
const tableId = ref(''); // 桌号ID,来自上一页 url 参数 tableid,用于接口入参
|
|
|
@@ -70,6 +73,7 @@ const currentCategoryIndex = ref(0);
|
|
|
const couponModalOpen = ref(false);
|
|
|
const cartModalOpen = ref(false);
|
|
|
const selectCouponModalOpen = ref(false);
|
|
|
+const selectCouponViewOnly = ref(false); // true=左下角仅查看,false=购物车内可选
|
|
|
const discountAmount = ref(12); // 优惠金额,示例数据
|
|
|
const selectedCouponId = ref(null); // 选中的优惠券ID
|
|
|
|
|
|
@@ -80,8 +84,8 @@ const categories = ref([]);
|
|
|
const foodList = ref([]);
|
|
|
// SSE 连接建立后拉取的购物车数据,在 foodList 就绪后合并
|
|
|
let pendingCartData = null;
|
|
|
-// 购物车接口返回的完整数据:{ items, totalAmount, totalQuantity },用于按接口格式展示
|
|
|
-const cartData = ref({ items: [], totalAmount: 0, totalQuantity: 0 });
|
|
|
+// 购物车接口返回的完整数据:{ items, totalAmount, totalQuantity, tablewareFee },用于按接口格式展示
|
|
|
+const cartData = ref({ items: [], totalAmount: 0, totalQuantity: 0, tablewareFee: 0 });
|
|
|
|
|
|
// 当前分类的菜品列表(按当前选中的分类 id 过滤)
|
|
|
const currentFoodList = computed(() => {
|
|
|
@@ -138,46 +142,42 @@ const displayTotalAmount = computed(() => {
|
|
|
return displayCartList.value.reduce((sum, item) => sum + getItemLinePrice(item), 0);
|
|
|
});
|
|
|
|
|
|
-// 优惠券列表(示例数据)
|
|
|
-const couponList = ref([
|
|
|
- {
|
|
|
- id: 1,
|
|
|
- amount: 38,
|
|
|
- minAmount: 158,
|
|
|
- name: '优惠券名称',
|
|
|
- expireDate: '2024/07/28',
|
|
|
- isReceived: false
|
|
|
- },
|
|
|
- {
|
|
|
- id: 2,
|
|
|
- amount: 8,
|
|
|
- minAmount: 158,
|
|
|
- name: '优惠券名称',
|
|
|
- expireDate: '2024/07/28',
|
|
|
- isReceived: false
|
|
|
- },
|
|
|
- {
|
|
|
- id: 3,
|
|
|
- amount: 682,
|
|
|
- minAmount: 1580,
|
|
|
- name: '优惠券名称',
|
|
|
- expireDate: '2024/07/28',
|
|
|
- isReceived: true
|
|
|
- },
|
|
|
- {
|
|
|
- id: 4,
|
|
|
- amount: 1038,
|
|
|
- minAmount: 1580,
|
|
|
- name: '优惠券名称',
|
|
|
- expireDate: '2024/07/28',
|
|
|
- isReceived: true
|
|
|
+// 优惠券列表(由接口 /dining/coupon/storeUsableList 返回后赋值)
|
|
|
+const couponList = ref([]);
|
|
|
+// 门店可用优惠券列表(选择优惠券弹窗用,与 couponList 同步自同一接口)
|
|
|
+const storeUsableCouponList = ref([]);
|
|
|
+
|
|
|
+// 规范化接口优惠券项为弹窗所需格式(对接 userOwnedByStore 返回的 data 数组:id/couponId/userCouponId、name、couponType、discountRate、minimumSpendingAmount、expirationTime 等)
|
|
|
+// couponType 1=满减券显示 nominalValue 为金额,2=折扣券显示 discountRate 为折扣力度
|
|
|
+function normalizeCouponItem(item) {
|
|
|
+ if (!item || typeof item !== 'object') return null;
|
|
|
+ const raw = item;
|
|
|
+ const couponType = Number(raw.couponType) || 0;
|
|
|
+ const nominalValue = Number(raw.nominalValue ?? raw.amount ?? 0) || 0;
|
|
|
+ const discountRate = Number(raw.discountRate) || 0;
|
|
|
+ const minAmount = Number(raw.minimumSpendingAmount ?? raw.minAmount ?? raw.min_amount ?? 0) || 0;
|
|
|
+ const isReceived = raw.canReceived === false;
|
|
|
+ let amountDisplay = nominalValue + '元';
|
|
|
+ if (couponType === 1) {
|
|
|
+ amountDisplay = nominalValue + '元';
|
|
|
+ } else if (couponType === 2 && discountRate > 0) {
|
|
|
+ amountDisplay = Math.round(discountRate) + '折';
|
|
|
}
|
|
|
-]);
|
|
|
+ return {
|
|
|
+ id: raw.userCouponId ?? raw.id ?? raw.couponId ?? raw.coupon_id ?? '',
|
|
|
+ amount: nominalValue,
|
|
|
+ minAmount,
|
|
|
+ name: raw.name ?? raw.title ?? raw.couponName ?? '',
|
|
|
+ expireDate: raw.expirationTime ?? raw.endGetDate ?? raw.validDate ?? raw.expireDate ?? raw.endTime ?? '',
|
|
|
+ isReceived,
|
|
|
+ couponType,
|
|
|
+ discountRate,
|
|
|
+ amountDisplay
|
|
|
+ };
|
|
|
+}
|
|
|
|
|
|
-// 可用的优惠券列表(已领取的优惠券)
|
|
|
-const availableCoupons = computed(() => {
|
|
|
- return couponList.value.filter(coupon => coupon.isReceived);
|
|
|
-});
|
|
|
+// 可用的优惠券列表(选择优惠券弹窗用,来自 storeUsableCouponList)
|
|
|
+const availableCoupons = computed(() => storeUsableCouponList.value);
|
|
|
|
|
|
// 分类 id 统一转字符串,避免 number/string 比较导致误判
|
|
|
const sameCategory = (a, b) => String(a ?? '') === String(b ?? '');
|
|
|
@@ -238,11 +238,12 @@ const fetchAndMergeCart = async (tableid) => {
|
|
|
const cartRes = await GetOrderCart(tableid);
|
|
|
const list = parseCartListFromResponse(cartRes);
|
|
|
pendingCartData = list;
|
|
|
- // 按接口格式绑定:data.items / totalAmount / totalQuantity
|
|
|
+ // 按接口格式绑定:data.items / totalAmount / totalQuantity / tablewareFee
|
|
|
cartData.value = {
|
|
|
items: list,
|
|
|
totalAmount: Number(cartRes?.totalAmount) || 0,
|
|
|
- totalQuantity: Number(cartRes?.totalQuantity) || 0
|
|
|
+ totalQuantity: Number(cartRes?.totalQuantity) || 0,
|
|
|
+ tablewareFee: Number(cartRes?.tablewareFee ?? cartRes?.tablewareAmount ?? 0) || 0
|
|
|
};
|
|
|
mergeCartIntoFoodList();
|
|
|
console.log('购物车接口返回(data 层):', cartRes, '解析条数:', list.length);
|
|
|
@@ -315,10 +316,21 @@ const syncCartDataFromFoodList = () => {
|
|
|
cartData.value = { ...cartData.value, items: nextItems, totalAmount, totalQuantity };
|
|
|
};
|
|
|
|
|
|
+// 判断是否为餐具(cuisineId 或 id 为 -1),餐具不可修改数量
|
|
|
+const isTableware = (item) => {
|
|
|
+ if (!item) return false;
|
|
|
+ const id = item.id ?? item.cuisineId;
|
|
|
+ return Number(id) === -1;
|
|
|
+};
|
|
|
+
|
|
|
// 更新菜品数量:菜品 id 一致则全部同步为同一数量,触发响应式;新增加入购物车时调接口;并同步 cartData
|
|
|
// Update 接口返回 400 时不改页面数量和金额(会回滚本地状态)
|
|
|
const updateFoodQuantity = (food, delta) => {
|
|
|
if (!food) return;
|
|
|
+ if (isTableware(food)) {
|
|
|
+ uni.showToast({ title: '不允许更改餐具数量', icon: 'none' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
const id = food.id ?? food.cuisineId;
|
|
|
const prevQty = food.quantity || 0;
|
|
|
const nextQty = Math.max(0, prevQty + delta);
|
|
|
@@ -396,37 +408,63 @@ const handleDecrease = (item) => {
|
|
|
if (food && (food.quantity || 0) > 0) updateFoodQuantity(food, -1);
|
|
|
};
|
|
|
|
|
|
-// 优惠券点击(领取优惠券弹窗)
|
|
|
-const handleCouponClick = () => {
|
|
|
- // 先关闭其他弹窗
|
|
|
- if (cartModalOpen.value) {
|
|
|
- cartModalOpen.value = false;
|
|
|
+// 拉取用户已领/可用优惠券(GET /dining/coupon/userOwnedByStore),列表在 SelectCouponModal 中展示,空时弹窗内显示「暂无可用优惠券」
|
|
|
+const fetchUserOwnedCoupons = async () => {
|
|
|
+ const storeId = uni.getStorageSync('currentStoreId') || '';
|
|
|
+ try {
|
|
|
+ const res = await GetUserOwnedCouponList({ storeId });
|
|
|
+ // 接口返回 { code, data: [...], msg, success },请求层可能只返回 data,故 res 可能为数组
|
|
|
+ const list = Array.isArray(res) ? res : (res?.data ?? res?.records ?? res?.list ?? []);
|
|
|
+ const normalized = (Array.isArray(list) ? list : []).map((item) => normalizeCouponItem(item)).filter(Boolean);
|
|
|
+ storeUsableCouponList.value = normalized;
|
|
|
+ return true;
|
|
|
+ } catch (err) {
|
|
|
+ console.error('获取用户优惠券失败:', err);
|
|
|
+ uni.showToast({ title: '获取优惠券失败', icon: 'none' });
|
|
|
+ return false;
|
|
|
}
|
|
|
- if (selectCouponModalOpen.value) {
|
|
|
- selectCouponModalOpen.value = false;
|
|
|
+};
|
|
|
+
|
|
|
+// 打开选择优惠券弹窗。viewOnly=true 仅查看(左下角),false 可选择并默认选中第一张(购物车内)
|
|
|
+const openSelectCouponModal = async (viewOnly = false) => {
|
|
|
+ if (cartModalOpen.value) cartModalOpen.value = false;
|
|
|
+ if (couponModalOpen.value) couponModalOpen.value = false;
|
|
|
+ selectCouponViewOnly.value = viewOnly;
|
|
|
+ const ok = await fetchUserOwnedCoupons();
|
|
|
+ if (ok) {
|
|
|
+ if (!viewOnly) {
|
|
|
+ const list = storeUsableCouponList.value;
|
|
|
+ if (list && list.length > 0) {
|
|
|
+ const first = list[0];
|
|
|
+ const firstId = first.id != null && first.id !== '' ? String(first.id) : null;
|
|
|
+ selectedCouponId.value = firstId;
|
|
|
+ discountAmount.value = first.amount ?? 0;
|
|
|
+ } else {
|
|
|
+ selectedCouponId.value = null;
|
|
|
+ discountAmount.value = 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ await nextTick();
|
|
|
+ selectCouponModalOpen.value = true;
|
|
|
}
|
|
|
- couponModalOpen.value = true;
|
|
|
};
|
|
|
|
|
|
-// 选择优惠券点击(从购物车弹窗中点击)
|
|
|
+// 优惠券点击(左下角):仅查看,不可选择
|
|
|
+const handleCouponClick = () => {
|
|
|
+ openSelectCouponModal(true);
|
|
|
+};
|
|
|
+
|
|
|
+// 选择优惠券点击(购物车内):可选择,默认选中第一张
|
|
|
const handleSelectCouponClick = () => {
|
|
|
- // 先关闭其他弹窗
|
|
|
- if (cartModalOpen.value) {
|
|
|
- cartModalOpen.value = false;
|
|
|
- }
|
|
|
- if (couponModalOpen.value) {
|
|
|
- couponModalOpen.value = false;
|
|
|
- }
|
|
|
- // 打开选择优惠券弹窗
|
|
|
- selectCouponModalOpen.value = true;
|
|
|
+ openSelectCouponModal(false);
|
|
|
};
|
|
|
|
|
|
-// 处理优惠券选择
|
|
|
+// 处理优惠券选择(单选:只保留当前选中的一张)
|
|
|
const handleCouponSelect = ({ coupon, index, selectedId }) => {
|
|
|
- selectedCouponId.value = selectedId;
|
|
|
+ selectedCouponId.value = selectedId != null && selectedId !== '' ? String(selectedId) : null;
|
|
|
// 根据选中的优惠券更新优惠金额
|
|
|
- if (selectedId) {
|
|
|
- discountAmount.value = coupon.amount;
|
|
|
+ if (selectedCouponId.value) {
|
|
|
+ discountAmount.value = coupon.amount ?? 0;
|
|
|
} else {
|
|
|
discountAmount.value = 0;
|
|
|
}
|
|
|
@@ -443,18 +481,6 @@ const handleSelectCouponClose = () => {
|
|
|
selectCouponModalOpen.value = false;
|
|
|
};
|
|
|
|
|
|
-// 优惠券领取
|
|
|
-const handleCouponReceive = ({ coupon, index }) => {
|
|
|
- console.log('领取优惠券:', coupon);
|
|
|
- // 更新优惠券状态
|
|
|
- couponList.value[index].isReceived = true;
|
|
|
- uni.showToast({
|
|
|
- title: '领取成功',
|
|
|
- icon: 'success'
|
|
|
- });
|
|
|
- // TODO: 调用接口领取优惠券
|
|
|
-};
|
|
|
-
|
|
|
// 优惠券弹窗关闭
|
|
|
const handleCouponClose = () => {
|
|
|
couponModalOpen.value = false;
|
|
|
@@ -486,35 +512,49 @@ const handleCartClose = () => {
|
|
|
|
|
|
// 清空购物车:调用 /store/order/cart/clear,入参 tableId,成功后清空本地并关闭弹窗
|
|
|
const handleCartClear = () => {
|
|
|
- if (!tableId.value) {
|
|
|
+ const doClear = () => {
|
|
|
+ pendingCartData = null;
|
|
|
foodList.value = foodList.value.map((f) => ({ ...f, quantity: 0 }));
|
|
|
cartData.value = { items: [], totalAmount: 0, totalQuantity: 0 };
|
|
|
cartModalOpen.value = false;
|
|
|
uni.showToast({ title: '已清空购物车', icon: 'success' });
|
|
|
+ };
|
|
|
+ if (!tableId.value) {
|
|
|
+ doClear();
|
|
|
return;
|
|
|
}
|
|
|
PostOrderCartClear(tableId.value)
|
|
|
- .then(() => {
|
|
|
- foodList.value = foodList.value.map((f) => ({ ...f, quantity: 0 }));
|
|
|
- cartData.value = { items: [], totalAmount: 0, totalQuantity: 0 };
|
|
|
- cartModalOpen.value = false;
|
|
|
- uni.showToast({ title: '已清空购物车', icon: 'success' });
|
|
|
- })
|
|
|
+ .then(doClear)
|
|
|
.catch((err) => {
|
|
|
console.error('清空购物车失败:', err);
|
|
|
uni.showToast({ title: '清空失败,请重试', icon: 'none' });
|
|
|
});
|
|
|
};
|
|
|
|
|
|
+// 从购物车项中计算餐具费:cuisineId/id 为 -1 的项为餐具,其金额合计为餐具费
|
|
|
+const getTablewareFeeFromCart = () => {
|
|
|
+ const items = cartData.value?.items ?? displayCartList.value ?? [];
|
|
|
+ return (Array.isArray(items) ? items : []).reduce((sum, it) => {
|
|
|
+ const id = it?.cuisineId ?? it?.id;
|
|
|
+ if (Number(id) !== -1) return sum;
|
|
|
+ const line = it?.subtotalAmount != null ? Number(it.subtotalAmount) : (Number(it?.quantity) || 0) * (Number(it?.unitPrice ?? it?.price) || 0);
|
|
|
+ return sum + line;
|
|
|
+ }, 0);
|
|
|
+};
|
|
|
+
|
|
|
// 下单点击:先带购物车数据跳转确认订单页,创建订单在确认页点击「确认下单」时再调
|
|
|
const handleOrderClick = () => {
|
|
|
+ const fromApi = Number(cartData.value?.tablewareFee) || 0;
|
|
|
+ const fromItems = getTablewareFeeFromCart();
|
|
|
+ const utensilFee = fromApi > 0 ? fromApi : fromItems;
|
|
|
const cartPayload = {
|
|
|
list: displayCartList.value,
|
|
|
totalAmount: displayTotalAmount.value,
|
|
|
totalQuantity: displayTotalQuantity.value,
|
|
|
tableId: tableId.value,
|
|
|
tableNumber: tableNumber.value,
|
|
|
- diners: currentDiners.value
|
|
|
+ diners: currentDiners.value,
|
|
|
+ utensilFee: Number(utensilFee) || 0
|
|
|
};
|
|
|
uni.setStorageSync('placeOrderCart', JSON.stringify(cartPayload));
|
|
|
const query = [];
|
|
|
@@ -531,8 +571,17 @@ onLoad(async (options) => {
|
|
|
tableId.value = tableid;
|
|
|
currentDiners.value = diners;
|
|
|
if (tableid) uni.setStorageSync('currentTableId', tableid);
|
|
|
+ if (diners) uni.setStorageSync('currentDiners', diners);
|
|
|
console.log('点餐页接收参数 - 桌号(tableid):', tableid, '就餐人数(diners):', diners);
|
|
|
|
|
|
+ // 更新餐具数量:quantity 传用餐人数,tableId 传桌号ID
|
|
|
+ if (tableid && diners !== '' && diners != null) {
|
|
|
+ PutOrderCartUpdateTableware({
|
|
|
+ quantity: Number(diners) || 1,
|
|
|
+ tableId: parseInt(tableid, 10) || 0
|
|
|
+ }).catch((err) => console.warn('更新餐具数量失败:', err));
|
|
|
+ }
|
|
|
+
|
|
|
// 先拉取购物车并缓存,再加载菜品,保证合并时 pendingCartData 已就绪,避免不返显
|
|
|
if (tableid) {
|
|
|
await fetchAndMergeCart(tableid).catch((err) => console.error('获取购物车失败:', err));
|
|
|
@@ -552,7 +601,8 @@ onLoad(async (options) => {
|
|
|
cartData.value = {
|
|
|
items: Array.isArray(items) ? items : [],
|
|
|
totalAmount: Number(payload?.totalAmount) || 0,
|
|
|
- totalQuantity: Number(payload?.totalQuantity) || 0
|
|
|
+ totalQuantity: Number(payload?.totalQuantity) || 0,
|
|
|
+ tablewareFee: Number(payload?.tablewareFee ?? payload?.tablewareAmount ?? 0) || (cartData.value?.tablewareFee ?? 0)
|
|
|
};
|
|
|
pendingCartData = null;
|
|
|
mergeCartIntoFoodList();
|
|
|
@@ -583,6 +633,9 @@ onLoad(async (options) => {
|
|
|
const data = res?.data ?? res ?? {};
|
|
|
tableNumber.value = data?.tableNumber ?? data?.tableNo ?? '';
|
|
|
tableNumberFetched.value = true;
|
|
|
+ // 餐具费:点餐页接口可能返回,若购物车未带则用此处兜底
|
|
|
+ const fee = Number(data?.tablewareFee ?? data?.tablewareAmount ?? 0) || 0;
|
|
|
+ if (fee > 0) cartData.value = { ...cartData.value, tablewareFee: fee };
|
|
|
|
|
|
// 成功后调接口获取菜品种类(storeId 取自点餐页接口返回,若无则需从别处获取)
|
|
|
const storeId = res?.storeId ?? data?.storeId ?? '';
|
|
|
@@ -622,6 +675,14 @@ onLoad(async (options) => {
|
|
|
}
|
|
|
});
|
|
|
|
|
|
+// 回到点餐页时重新获取购物车(从接口拉取最新数据并合并到列表)
|
|
|
+onShow(() => {
|
|
|
+ const tableid = tableId.value || uni.getStorageSync('currentTableId') || '';
|
|
|
+ if (tableid) {
|
|
|
+ fetchAndMergeCart(tableid).catch((err) => console.warn('重新获取购物车失败:', err));
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
onUnload(() => {
|
|
|
if (sseRequestTask && typeof sseRequestTask.abort === 'function') {
|
|
|
sseRequestTask.abort();
|