Ver Fonte

对接订单

sunshibo há 2 meses atrás
pai
commit
d844fc0d54

+ 28 - 0
api/dining.js

@@ -8,12 +8,24 @@ export const DiningUserWechatLogin = (params) => api.post({ url: '/dining/user/w
 // 点餐页数据(入参 dinerCount 就餐人数、tableId 桌号)
 export const DiningOrderFood = (params) => api.get({ url: '/store/dining/page-info', params });
 
+// 门店详情(GET /store/info/detail/{storeId},入参 storeId)
+export const GetStoreDetail = (storeId) =>
+  api.get({ url: `/store/info/detail/${encodeURIComponent(storeId)}` });
+
 // 菜品种类(入参 storeId,GET /store/info/categories?storeId=)
 export const GetStoreCategories = (params) => api.get({ url: '/store/info/categories', params });
 
 // 根据菜品种类获取菜品(入参 categoryId,GET /store/info/cuisines?categoryId=)
 export const GetStoreCuisines = (params) => api.get({ url: '/store/info/cuisines', params });
 
+// 菜品详情(GET /store/dining/cuisine/{cuisineId},入参 cuisineId 菜品ID、tableId 桌号ID 传 query)
+export const GetCuisineDetail = (cuisineId, tableId) => {
+  const id = cuisineId != null ? encodeURIComponent(cuisineId) : '';
+  const url = id ? `/store/dining/cuisine/${id}` : '/store/dining/cuisine/0';
+  const params = tableId != null && tableId !== '' ? { tableId } : {};
+  return api.get({ url, params });
+};
+
 // 获取购物车(GET /store/order/cart/{tableId},建立 SSE 连接之后调用)
 export const GetOrderCart = (tableId) =>
   api.get({ url: `/store/order/cart/${encodeURIComponent(tableId)}` });
@@ -33,6 +45,22 @@ export const PostOrderCartClear = (tableId) => {
   return api.delete({ url, params: id ? { tableId: id } : {} });
 };
 
+// 创建订单(POST /store/order/create,入参 dto:tableId 桌号ID、contactPhone 联系电话、couponId 优惠券ID 默认 null、dinerCount 就餐人数、immediatePay 是否立即支付 默认 0、remark 备注信息 最多30字)
+export const PostOrderCreate = (dto) =>
+  api.post({ url: '/store/order/create', params: dto });
+
+// 订单分页列表(GET /store/order/page,入参 current 页码、size 每页数量、orderStatus 订单状态、storeId 门店ID、tableId 桌号ID)
+export const GetOrderPage = (params) =>
+  api.get({ url: '/store/order/page', params });
+
+// 我的订单(GET /store/order/my-orders,入参 current 页码、size 每页数量、type 订单类型 0未支付 1历史)
+export const GetMyOrders = (params) =>
+  api.get({ url: '/store/order/my-orders', params });
+
+// 订单详情(GET /store/order/info/{orderId},入参 orderId 订单ID)
+export const GetOrderInfo = (orderId) =>
+  api.get({ url: `/store/order/info/${encodeURIComponent(orderId)}` });
+
 /**
  * 订单 SSE 接口配置(GET /store/order/sse/{tableId})
  * 仅提供 URL 与 header,实际连接请使用 utils/sse.js 的 createSSEConnection 封装

+ 11 - 1
components/NavBar/index.vue

@@ -6,7 +6,7 @@
 			<view class="nav-bar flex">
 				<view class="nav-bar_left" v-if="!hideLeft">
 					<slot name='left'>
-						<view class="left-btns" :class="{ 'left-btns_home': !getIsHome}">
+						<view class="left-btns" :class="{ 'left-btns_home': !getIsHome && !onlyBack, 'left-btns_only-back': onlyBack }">
 							<view class="back" hover-class="hover-active" @click="go('back')">
 								<image src="https://cdn.aliyinba.com/UploadFiles/shop2/number/nav_bar/nav-back.png" mode="aspectFill"></image>
 							</view>
@@ -47,6 +47,7 @@
 	const props = defineProps({
 		title: { type: String },
 		hideLeft: { type: Boolean, default: false },
+		onlyBack: { type: Boolean, default: false }, // 仅显示返回键,不显示首页
 		warn: { type: Boolean, default: false }, // 警告
 		shadow: { type: Boolean, default: false }, // 警告
 	})
@@ -131,6 +132,15 @@
 								display: none !important;
 							}
 						}
+						&_only-back{
+							width: 90rpx;
+							.home{
+								display: none !important;
+							}
+							.back::after{
+								display: none;
+							}
+						}
 						
 						.back, .home{
 							flex: 1;

+ 214 - 34
pages/foodDetail/index.vue

@@ -1,42 +1,45 @@
 <template>
   <!-- 菜品详情 -->
   <view class="content">
-    <swiper class="swiper" :circular="true" :autoplay="true" :interval="3000">
-      <swiper-item>
-        <image :src="getFileUrl('/static/demo.png')" mode="aspectFill" class="food-image"></image>
-      </swiper-item>
-      <swiper-item>
-        <image :src="getFileUrl('/static/demo.png')" mode="aspectFill" class="food-image"></image>
-      </swiper-item>
-      <swiper-item>
-        <image :src="getFileUrl('/static/demo.png')" mode="aspectFill" class="food-image"></image>
+    <!-- 自定义返回栏,避免引用 NavBar 组件导致的 mp 依赖分析报错 -->
+    <view class="back-bar">
+      <view class="back-bar-inner">
+        <view class="back-btn" hover-class="hover-active" @click="onBack">
+          <text class="back-arrow">‹</text>
+        </view>
+        <text class="back-title">菜品详情</text>
+      </view>
+    </view>
+    <swiper class="swiper" :circular="true" :autoplay="true" :interval="3000" v-if="swiperImages.length">
+      <swiper-item v-for="(img, i) in swiperImages" :key="i">
+        <image :src="getFileUrl(img)" mode="aspectFill" class="food-image"></image>
       </swiper-item>
     </swiper>
+    <view class="swiper swiper-placeholder" v-else>
+      <image :src="placeholderImage" mode="aspectFill" class="food-image"></image>
+    </view>
     <view class="food-info">
       <view class="food-price">
         <text class="price-symbol">¥</text>
-        <text class="price-main">49.8</text>
-        <text class="price-decimal">.00</text>
+        <text class="price-main">{{ priceMain }}</text>
+        <text class="price-decimal">.{{ priceDecimal }}</text>
       </view>
       <view class="food-sales">
         <image :src="getFileUrl('img/icon/star.png')" mode="aspectFill" class="star-icon"></image>
-        <text class="sales-text">月售:0</text>
+        <text class="sales-text">月售:{{ detail.monthlySales ?? 0 }}</text>
       </view>
     </view>
 
     <view class="food-desc">
-      <view class="food-desc-title">香煎M9和牛粒</view>
-      <view class="food-desc-tags">
-        <view class="food-desc-tag signature">招牌</view>
-        <view class="food-desc-tag spicy">中辣</view>
+      <view class="food-desc-title">{{ detail.name || detail.cuisineName || '—' }}</view>
+      <view class="food-desc-tags" v-if="detailTags.length">
+        <view v-for="(tag, ti) in detailTags" :key="ti" class="food-desc-tag" :class="tag.type || 'normal'">{{ tag.text }}</view>
       </view>
-      <view class="food-desc-content">香煎 M9 和牛粒是将澳洲顶级 M9 和牛切成均匀小粒,以高温快煎方式锁住肉汁。</view>
+      <view class="food-desc-content">{{ detail.description || detail.desc || '暂无描述' }}</view>
     </view>
     <view class="food-desc">
       <view class="food-desc-title-intro">菜品介绍</view>
-      <view class="food-desc-content">
-        描述:甜麻辣。携手周黑鸭,朝鲜族特色碰撞武汉地道风味。精选鸭腿肉,酱香入骨,紧实入味,年糕Q弹软糯,裹满酱汁,醇厚浓郁。口味层次丰富,前调甜,中段辣,尾韵麻。每份含黑鸭块240克,年糕42克,热烫爆香,甜辣带劲。
-      </view>
+      <view class="food-desc-content">{{ detail.detailContent  || '暂无介绍' }}</view>
     </view>
 
     <!-- 底部按钮 -->
@@ -68,39 +71,211 @@
 </template>
 
 <script setup>
-import { ref } from 'vue';
+import { ref, computed } from 'vue';
 import { onLoad } from "@dcloudio/uni-app";
 import { getFileUrl } from "@/utils/file.js";
-import NavBar from '@/components/NavBar/index.vue';
+import { GetCuisineDetail, GetOrderCart, PostOrderCartAdd, PostOrderCartUpdate } from "@/api/dining.js";
 
 const quantity = ref(1);
+const loading = ref(false);
+const cuisineId = ref('');
+const tableId = ref('');
+/** 进入详情页时该菜品在购物车中的数量,用于与列表页一致的 Add/Update 逻辑 */
+const initialCartQuantity = ref(0);
+// 接口返回的菜品详情(兼容多种字段名)
+const detail = ref({});
+
+const placeholderImage = computed(() => getFileUrl('img/icon/star.png') || '');
+// 轮播图列表:接口可能返回 image/cuisineImage/images(逗号分隔或数组)
+const swiperImages = computed(() => {
+  const d = detail.value;
+  const raw = d?.images ?? d?.cuisineImage ?? d?.image ?? d?.imageUrl ?? '';
+  if (Array.isArray(raw)) return raw.filter(Boolean);
+  if (typeof raw === 'string' && raw) return raw.split(',').map((s) => s.trim()).filter(Boolean);
+  return [];
+});
+const priceMain = computed(() => {
+  const p = detail.value?.totalPrice ?? detail.value?.unitPrice ?? detail.value?.price ?? detail.value?.salePrice ?? 0;
+  const num = Number(p);
+  return Number.isNaN(num) ? '0' : String(Math.floor(num));
+});
+const priceDecimal = computed(() => {
+  const p = detail.value?.totalPrice ?? detail.value?.unitPrice ?? detail.value?.price ?? detail.value?.salePrice ?? 0;
+  const num = Number(p);
+  if (Number.isNaN(num)) return '00';
+  const dec = Math.round((num - Math.floor(num)) * 100);
+  return String(dec).padStart(2, '0');
+});
+// 标签:接口可能返回 tags 数组、JSON 字符串或逗号/顿号分隔字符串,解析为 [{ text, type }] 数组
+const detailTags = computed(() => {
+  const t = detail.value?.tags;
+  if (Array.isArray(t)) {
+    return t.map((item) => {
+      if (item == null) return { text: '', type: 'normal' };
+      if (typeof item === 'string') return { text: item, type: 'normal' };
+      return { text: item?.text ?? item?.tagName ?? item?.name ?? '', type: item?.type ?? 'normal' };
+    }).filter((x) => x.text !== '' && x.text != null);
+  }
+  if (t && typeof t === 'string') {
+    const s = t.trim();
+    if (!s) return [];
+    // 尝试 JSON 解析(如 "[{\"text\":\"招牌\",\"type\":\"signature\"}]")
+    if (s.startsWith('[')) {
+      try {
+        const arr = JSON.parse(s);
+        if (Array.isArray(arr)) {
+          return arr.map((item) => {
+            if (item == null) return { text: '', type: 'normal' };
+            if (typeof item === 'string') return { text: item, type: 'normal' };
+            return { text: item?.text ?? item?.tagName ?? item?.name ?? '', type: item?.type ?? 'normal' };
+          }).filter((x) => x.text !== '' && x.text != null);
+        }
+      } catch (_) {}
+    }
+    // 按逗号、顿号、空格等拆成数组
+    return s.split(/[,,、\s]+/).map((part) => ({ text: part.trim(), type: 'normal' })).filter((x) => x.text !== '');
+  }
+  return [];
+});
+
+const onBack = () => {
+  uni.navigateBack();
+};
 
+// 加减号只改本地数量,不调接口;只有点击「加入购物车」才调接口
 const handleIncrease = () => {
   if (quantity.value >= 99) return;
   quantity.value++;
 };
 
 const handleDecrease = () => {
-  if (quantity.value > 0) {
-    quantity.value--;
-  }
+  if (quantity.value > 0) quantity.value--;
 };
 
-const handleAddToCart = () => {
-  // TODO: 实现加入购物车逻辑
-  console.log('加入购物车,数量:', quantity.value);
-  uni.navigateBack();
-  uni.showToast({
-    title: '加入购物车成功',
-    icon: 'none'
-  });
+// 与列表页一致:购物车无该商品或数量从 0 到 1 用 Add,否则用 Update
+const handleAddToCart = async () => {
+  const id = cuisineId.value;
+  const tid = tableId.value;
+  if (!id) {
+    uni.showToast({ title: '缺少菜品信息', icon: 'none' });
+    return;
+  }
+  if (!tid) {
+    uni.showToast({ title: '请先选择桌号', icon: 'none' });
+    return;
+  }
+  const targetQty = quantity.value || 0;
+  if (targetQty <= 0) {
+    uni.showToast({ title: '请选择数量', icon: 'none' });
+    return;
+  }
+  if (loading.value) return;
+  loading.value = true;
+  try {
+    const needAdd = initialCartQuantity.value === 0 && targetQty >= 1;
+    if (needAdd) {
+      await PostOrderCartAdd({
+        cuisineId: id,
+        quantity: targetQty,
+        tableId: tid
+      });
+    } else {
+      await PostOrderCartUpdate({
+        cuisineId: id,
+        quantity: targetQty,
+        tableId: tid
+      });
+    }
+    uni.showToast({ title: '已加入购物车', icon: 'none' });
+    setTimeout(() => uni.navigateBack(), 500);
+  } catch (e) {
+    // 与列表页一致:接口失败(如 code 400)时不保留本次修改,数量回滚为进入页时的购物车数量
+    quantity.value = initialCartQuantity.value;
+    uni.showToast({ title: e?.message || '加入失败', icon: 'none' });
+  } finally {
+    loading.value = false;
+  }
 };
 
-onLoad((e) => {
+onLoad(async (options) => {
+  const id = options?.cuisineId ?? options?.id ?? '';
+  const tid = options?.tableId ?? options?.tableid ?? uni.getStorageSync('currentTableId') ?? '';
+  cuisineId.value = id;
+  tableId.value = tid;
+  // 从列表页带过来的购物车数量,用于底部数量展示和 Add/Update 判断
+  const fromQuery = Math.max(0, Number(options?.quantity) || 0);
+  quantity.value = fromQuery || 1;
+  initialCartQuantity.value = fromQuery;
+
+  if (!id) {
+    uni.showToast({ title: '缺少菜品ID', icon: 'none' });
+    return;
+  }
+  try {
+    const res = await GetCuisineDetail(id, tid);
+    const data = res?.data ?? res;
+    detail.value = data || {};
+  } catch (e) {
+    uni.showToast({ title: e?.message || '加载失败', icon: 'none' });
+  }
+  // 有桌号时拉取购物车,以接口数量为准覆盖展示和 initialCartQuantity(与列表同步)
+  if (tid) {
+    try {
+      const cartRes = await GetOrderCart(tid);
+      const list = cartRes?.items ?? cartRes?.list ?? cartRes?.data?.items ?? [];
+      const arr = Array.isArray(list) ? list : [];
+      const cartItem = arr.find(
+        (it) => String(it?.cuisineId ?? it?.id ?? '') === String(id)
+      );
+      const cartQty = cartItem ? (Number(cartItem.quantity) || 0) : 0;
+      quantity.value = cartQty > 0 ? cartQty : quantity.value;
+      initialCartQuantity.value = cartQty;
+    } catch (_) {}
+  }
 });
 </script>
 
 <style lang="scss" scoped>
+.back-bar {
+  position: sticky;
+  top: 0;
+  z-index: 101;
+  padding-top: constant(safe-area-inset-top);
+  padding-top: env(safe-area-inset-top);
+  background: rgba(0, 0, 0, 0.3);
+}
+.back-bar-inner {
+  height: 88rpx;
+  padding: 0 24rpx;
+  display: flex;
+  align-items: center;
+}
+.back-btn {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 50%;
+  background: rgba(255, 255, 255, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 16rpx;
+  &.hover-active {
+    opacity: 0.8;
+  }
+}
+.back-arrow {
+  font-size: 48rpx;
+  color: #fff;
+  font-weight: 300;
+  line-height: 1;
+  margin-left: -4rpx;
+}
+.back-title {
+  font-size: 32rpx;
+  color: #fff;
+  font-weight: 500;
+}
+
 .swiper {
   width: 100%;
   height: 510rpx;
@@ -191,6 +366,11 @@ onLoad((e) => {
       background: #2E2E2E;
       color: #fff;
     }
+
+    &.normal {
+      background: #f0f0f0;
+      color: #666;
+    }
   }
 
   .food-desc-content {

+ 60 - 36
pages/index/index.vue

@@ -2,19 +2,19 @@
   <!-- 首页 -->
   <view class="content">
     <!-- 顶部 -->
-    <image :src="getFileUrl('/static/demo.png')" mode="aspectFill" class="top-img"></image>
+    <image :src="getFileUrl('img/icon/zptj.png')" mode="aspectFill" class="top-img"></image>
 
     <!-- 内容 -->
     <view class="content-box">
       <view class="shop-info">
         <image :src="getFileUrl('img/icon/shop.png')" mode="aspectFill" class="shop-img"></image>
-        <view class="shop-name">店铺名称</view>
+        <view class="shop-name">{{ storeInfo.storeName || '店铺名称' }}</view>
       </view>
 
       <!-- 登录卡片 -->
       <view class='login-card' v-if="!userStore.getToken">
         <view class="login-card-left">
-          <view class="welcome-title">欢迎来到xxx店铺</view>
+          <view class="welcome-title">欢迎来到{{ storeInfo.storeName || 'xxx' }}店铺</view>
           <view class="welcome-desc">注册登录即可体验更好的服务</view>
           <view class="login-btn" hover-class="hover-active" @click="showLoginModal = true">立即登录</view>
         </view>
@@ -29,7 +29,7 @@
 
       <!-- 列表 -->
       <view class="list-box">
-        <view class="food-card" v-for="(item, index) in foodList" :key="index">
+        <view class="food-card" v-for="(item, index) in foodList" :key="item.id || index" hover-class="hover-active" @click="handleFoodClick(item)">
           <!-- 菜品图片 -->
           <image :src="getFileUrl(item.image)" mode="aspectFill" class="food-image"></image>
 
@@ -86,50 +86,51 @@ import LoginModal from "@/pages/components/LoginModal.vue";
 import { go } from "@/utils/utils.js";
 import { getFileUrl } from '@/utils/file.js';
 import { useUserStore } from '@/store/user.js';
+import { GetStoreDetail } from '@/api/dining.js';
 
 const userStore = useUserStore();
 const showLoginModal = ref(false);
+/** 门店信息(接口返回的 storeInfo,绑定 storeName 等) */
+const storeInfo = ref({});
+
+// 首页菜品列表(绑定接口 homepageCuisines,兼容多种字段名)
+const foodList = ref([]);
+
+function normalizeHomeCuisine(item) {
+  const tags = item?.tags ?? [];
+  const tagArr = Array.isArray(tags) ? tags : [];
+  const images = item?.images;
+  const imageUrl = Array.isArray(images) ? images[0] : images;
+  return {
+    id: item?.id ?? item?.cuisineId ?? '',
+    image: imageUrl ?? item?.cuisineImage ?? item?.image ?? 'img/icon/shop.png',
+    name: item?.cuisineName ?? item?.name ?? '',
+    sales: item?.monthlySales ?? item?.sales ?? 0,
+    tags: tagArr.map((t) => (typeof t === 'string' ? { text: t, type: 'normal' } : { text: t?.text ?? t?.tagName ?? '', type: t?.type ?? 'normal' })),
+    description: item?.description ?? item?.desc ?? '',
+    price: item?.totalPrice ?? item?.unitPrice ?? item?.price ?? ''
+  };
+}
 
-// 示例菜品数据
-const foodList = ref([
-  {
-    image: '/static/demo.png',
-    name: '川派辣卤牛杂川派辣卤牛杂煲川派辣卤牛杂煲川派辣卤牛杂煲煲',
-    sales: 160,
-    tags: [
-      { text: '招牌', type: 'signature' },
-      { text: '中辣', type: 'spicy' }
-    ],
-    description: '香煎 M9 和牛粒是将澳洲顶级 M9 和牛切成均匀小粒,以高温快煎方式锁住肉汁。',
-    price: '49.8'
-  },
-  {
-    image: '/static/demo.png',
-    name: '川派辣卤牛杂煲',
-    sales: 160,
-    tags: [
-      { text: '招牌', type: 'signature' },
-      { text: '中辣', type: 'spicy' }
-    ],
-    description: '香煎 M9 和牛粒是将澳洲顶级 M9 和牛切成均匀小粒,以高温快煎方式锁住肉汁。',
-    price: '49.8'
-  }
-]);
+// 格式化为保留两位小数的价格字符串
+const formatPriceFixed2 = (price) => {
+  if (price === '' || price === null || price === undefined) return '0.00';
+  const num = Number(price);
+  return Number.isNaN(num) ? '0.00' : num.toFixed(2);
+};
 
 // 获取价格整数部分
 const getPriceMain = (price) => {
-  if (!price) return '';
-  const priceStr = String(price);
+  const priceStr = formatPriceFixed2(price);
   const dotIndex = priceStr.indexOf('.');
   return dotIndex > -1 ? priceStr.substring(0, dotIndex) : priceStr;
 };
 
-// 获取价格小数部分
+// 获取价格小数部分(两位)
 const getPriceDecimal = (price) => {
-  if (!price) return '';
-  const priceStr = String(price);
+  const priceStr = formatPriceFixed2(price);
   const dotIndex = priceStr.indexOf('.');
-  return dotIndex > -1 ? priceStr.substring(dotIndex + 1) : '';
+  return dotIndex > -1 ? priceStr.substring(dotIndex + 1) : '00';
 };
 
 // 登录成功回调
@@ -142,7 +143,30 @@ const handleLoginCancel = () => {
   console.log('用户取消登录');
 };
 
-onLoad((e) => { });
+// 点击菜品:跳转菜品详情页,传 cuisineId,有桌号则带 tableId
+const handleFoodClick = (item) => {
+  const cuisineId = item?.id ?? item?.cuisineId ?? '';
+  if (!cuisineId) return;
+  const tableId = uni.getStorageSync('currentTableId') ?? '';
+  const q = [`cuisineId=${encodeURIComponent(String(cuisineId))}`];
+  if (tableId !== '') q.push(`tableId=${encodeURIComponent(String(tableId))}`);
+  go(`/pages/foodDetail/index?${q.join('&')}`);
+};
+
+onLoad(async (options) => {
+  const storeId = options?.storeId ?? uni.getStorageSync('currentStoreId') ?? '';
+  if (storeId) {
+    try {
+      const res = await GetStoreDetail(storeId);
+      const data = res?.data ?? res;
+      storeInfo.value = data?.storeInfo ?? data ?? {};
+      const list = data?.homepageCuisines ?? data?.storeInfo?.homepageCuisines ?? [];
+      foodList.value = (Array.isArray(list) ? list : []).map(normalizeHomeCuisine);
+    } catch (e) {
+      console.error('门店详情加载失败:', e);
+    }
+  }
+});
 </script>
 
 <style scoped lang="scss">

+ 6 - 4
pages/numberOfDiners/index.vue

@@ -29,7 +29,7 @@ import LoginModal from "@/pages/components/LoginModal.vue";
 
 const userStore = useUserStore();
 const diners = ref(12);
-const currentDiners = ref(2);
+const currentDiners = ref(1);
 const showLoginModal = ref(false);
 // 登录成功后要跳转的点餐页参数(手机号授权完成后再跳转)
 const pendingNavigate = ref(null);
@@ -59,6 +59,7 @@ const toOrderFood = () => {
   uni.setStorageSync('currentDiners', currentDiners.value);
   const tableid = 1;
   const dinersVal = currentDiners.value;
+  if (tableid) uni.setStorageSync('currentTableId', String(tableid));
 
   if (!userStore.getToken) {
     // 未登录:弹出手机号授权一键登录弹窗,登录成功后再跳转
@@ -66,9 +67,10 @@ const toOrderFood = () => {
     showLoginModal.value = true;
     return;
   }
-
-  go(`/pages/orderFood/index?tableid=${tableid}&diners=${dinersVal}`);
-};
+uni.reLaunch({
+  url: `/pages/orderFood/index?tableid=${tableid}&diners=${dinersVal}`
+});
+}
 
 onLoad((e) => {
   // let currentDiners = uni.getStorageSync('currentDiners');

+ 14 - 2
pages/orderFood/components/FoodCard.vue

@@ -59,7 +59,9 @@ const props = defineProps({
       monthlySales: 0,
       quantity: 0
     })
-  }
+  },
+  /** 桌号ID,跳转详情页时传入以便加购 */
+  tableId: { type: [String, Number], default: '' }
 });
 
 const emit = defineEmits(['increase', 'decrease']);
@@ -103,7 +105,17 @@ const normalizedTags = computed(() => {
 });
 
 const handleFoodClick = () => {
-  go('/pages/foodDetail/index');
+  const id = props.food?.id ?? props.food?.cuisineId ?? '';
+  if (!id) {
+    uni.showToast({ title: '暂无菜品信息', icon: 'none' });
+    return;
+  }
+  const tableId = props.tableId != null && props.tableId !== '' ? String(props.tableId) : '';
+  const qty = props.food?.quantity ?? 0;
+  const q = [`cuisineId=${encodeURIComponent(id)}`];
+  if (tableId) q.push(`tableId=${encodeURIComponent(tableId)}`);
+  q.push(`quantity=${encodeURIComponent(String(qty))}`);
+  go(`/pages/foodDetail/index?${q.join('&')}`);
 };
 
 const handleIncrease = () => {

+ 63 - 37
pages/orderFood/index.vue

@@ -17,8 +17,8 @@
 
       <!-- 右侧菜品列表 -->
       <scroll-view class="food-list" scroll-y>
-        <FoodCard v-for="(food, index) in currentFoodList" :key="food.id || index" :food="food"
-          @increase="handleIncrease" @decrease="handleDecrease"/>
+        <FoodCard v-for="(food, index) in currentFoodList" :key="food.id || food.cuisineId || index" :food="food"
+          :table-id="tableId" @increase="handleIncrease" @decrease="handleDecrease"/>
       </scroll-view>
     </view>
 
@@ -307,43 +307,50 @@ const syncCartDataFromFoodList = () => {
 };
 
 // 更新菜品数量:菜品 id 一致则全部同步为同一数量,触发响应式;新增加入购物车时调接口;并同步 cartData
+// Update 接口返回 400 时不改页面数量和金额(会回滚本地状态)
 const updateFoodQuantity = (food, delta) => {
   if (!food) return;
   const id = food.id ?? food.cuisineId;
-  const nextQty = Math.max(0, (food.quantity || 0) + delta);
+  const prevQty = food.quantity || 0;
+  const nextQty = Math.max(0, prevQty + delta);
   const sameId = (item) =>
     String(item?.id ?? item?.cuisineId ?? '') === String(id ?? '');
-  foodList.value = foodList.value.map((item) =>
-    sameId(item) ? { ...item, quantity: nextQty } : item
-  );
-  // 同步接口格式购物车:若 cartData 中有该项则更新,否则追加(来自菜品列表新加)
   const items = cartData.value?.items ?? [];
   const idx = items.findIndex((it) => String(it?.cuisineId ?? it?.id ?? '') === String(id));
-  if (idx >= 0) {
-    const it = items[idx];
-    const unitPrice = Number(it?.unitPrice ?? it?.price ?? 0) || 0;
-    const nextItems = items.slice();
-    nextItems[idx] = { ...it, quantity: nextQty, subtotalAmount: nextQty * unitPrice };
-    const totalAmount = nextItems.reduce((s, i) => s + (Number(i.subtotalAmount) || 0), 0);
-    const totalQuantity = nextItems.reduce((s, i) => s + (Number(i.quantity) || 0), 0);
-    cartData.value = { ...cartData.value, items: nextItems, totalAmount, totalQuantity };
-  } else if (delta > 0) {
-    // 来自菜品列表新加:按接口格式追加到 cartData.items
-    const unitPrice = Number(food?.price ?? food?.unitPrice ?? food?.salePrice ?? 0) || 0;
-    const newItem = {
-      cuisineId: id,
-      cuisineName: food?.name ?? food?.cuisineName ?? '',
-      cuisineImage: food?.image ?? food?.cuisineImage ?? food?.imageUrl ?? '',
-      quantity: nextQty,
-      unitPrice,
-      subtotalAmount: nextQty * unitPrice
-    };
-    const nextItems = [...items, newItem];
-    const totalAmount = nextItems.reduce((s, i) => s + (Number(i.subtotalAmount) || 0), 0);
-    const totalQuantity = nextItems.reduce((s, i) => s + (Number(i.quantity) || 0), 0);
-    cartData.value = { ...cartData.value, items: nextItems, totalAmount, totalQuantity };
-  }
-  // 有数量变化且桌号存在时调接口:购物车没有当前商品或数量从 0 增加到 1(刚清空后重加)则 add,否则 update
+
+  const applyQuantity = (qty) => {
+    foodList.value = foodList.value.map((item) =>
+      sameId(item) ? { ...item, quantity: qty } : item
+    );
+    const currentItems = cartData.value?.items ?? [];
+    const currentIdx = currentItems.findIndex((it) => String(it?.cuisineId ?? it?.id ?? '') === String(id));
+    if (currentIdx >= 0) {
+      const it = currentItems[currentIdx];
+      const unitPrice = Number(it?.unitPrice ?? it?.price ?? 0) || 0;
+      const nextItems = currentItems.slice();
+      nextItems[currentIdx] = { ...it, quantity: qty, subtotalAmount: qty * unitPrice };
+      const totalAmount = nextItems.reduce((s, i) => s + (Number(i.subtotalAmount) || 0), 0);
+      const totalQuantity = nextItems.reduce((s, i) => s + (Number(i.quantity) || 0), 0);
+      cartData.value = { ...cartData.value, items: nextItems, totalAmount, totalQuantity };
+    } else if (qty > 0) {
+      const unitPrice = Number(food?.price ?? food?.unitPrice ?? food?.salePrice ?? 0) || 0;
+      const newItem = {
+        cuisineId: id,
+        cuisineName: food?.name ?? food?.cuisineName ?? '',
+        cuisineImage: food?.image ?? food?.cuisineImage ?? food?.imageUrl ?? '',
+        quantity: qty,
+        unitPrice,
+        subtotalAmount: qty * unitPrice
+      };
+      const nextItems = [...currentItems, newItem];
+      const totalAmount = nextItems.reduce((s, i) => s + (Number(i.subtotalAmount) || 0), 0);
+      const totalQuantity = nextItems.reduce((s, i) => s + (Number(i.quantity) || 0), 0);
+      cartData.value = { ...cartData.value, items: nextItems, totalAmount, totalQuantity };
+    }
+  };
+
+  applyQuantity(nextQty);
+
   if (tableId.value && delta !== 0) {
     const needAdd = delta > 0 && (idx < 0 || nextQty === 1);
     if (needAdd) {
@@ -351,13 +358,19 @@ const updateFoodQuantity = (food, delta) => {
         cuisineId: id,
         quantity: nextQty,
         tableId: tableId.value
-      }).catch((err) => console.error('加入购物车失败:', err));
+      }).catch((err) => {
+        console.error('加入购物车失败:', err);
+        applyQuantity(prevQty);
+      });
     } else {
       PostOrderCartUpdate({
         cuisineId: id,
         quantity: nextQty,
         tableId: tableId.value
-      }).catch((err) => console.error('更新购物车失败:', err));
+      }).catch((err) => {
+        console.error('更新购物车失败:', err);
+        applyQuantity(prevQty);
+      });
     }
   }
 };
@@ -484,9 +497,20 @@ const handleCartClear = () => {
     });
 };
 
-// 下单点击
-const handleOrderClick = (data) => {
-  go('/pages/placeOrder/index');
+// 下单点击:先带购物车数据跳转确认订单页,创建订单在确认页点击「确认下单」时再调
+const handleOrderClick = () => {
+  const cartPayload = {
+    list: displayCartList.value,
+    totalAmount: displayTotalAmount.value,
+    totalQuantity: displayTotalQuantity.value,
+    tableId: tableId.value,
+    diners: currentDiners.value
+  };
+  uni.setStorageSync('placeOrderCart', JSON.stringify(cartPayload));
+  const query = [];
+  if (tableId.value) query.push(`tableId=${encodeURIComponent(tableId.value)}`);
+  if (currentDiners.value) query.push(`diners=${encodeURIComponent(currentDiners.value)}`);
+  go(query.length ? `/pages/placeOrder/index?${query.join('&')}` : '/pages/placeOrder/index');
 };
 
 onLoad(async (options) => {
@@ -495,6 +519,7 @@ onLoad(async (options) => {
   const diners = options.diners || '';
   tableId.value = tableid;
   currentDiners.value = diners;
+  if (tableid) uni.setStorageSync('currentTableId', tableid);
   console.log('点餐页接收参数 - 桌号(tableid):', tableid, '就餐人数(diners):', diners);
 
   // 先拉取购物车并缓存,再加载菜品,保证合并时 pendingCartData 已就绪,避免不返显
@@ -546,6 +571,7 @@ onLoad(async (options) => {
 
       // 成功后调接口获取菜品种类(storeId 取自点餐页接口返回,若无则需从别处获取)
       const storeId = res?.storeId ?? res?.data?.storeId ?? '';
+      if (storeId) uni.setStorageSync('currentStoreId', storeId);
       if (storeId) {
         try {
           const categoriesRes = await GetStoreCategories({ storeId });

+ 191 - 38
pages/orderInfo/index.vue

@@ -51,8 +51,8 @@
           </view>
         </view>
 
-        <!-- 操作按钮 -->
-        <view class="order-actions">
+        <!-- 操作按钮:已完成(3)不显示 -->
+        <view class="order-actions" v-if="order.orderStatus !== 3">
           <view class="action-btn outline" @click="handleAddFood(order)">
             去加餐
           </view>
@@ -72,46 +72,179 @@
 
 <script setup>
 import { onLoad } from "@dcloudio/uni-app";
-import { ref, computed } from "vue";
+import { ref, computed, watch } from "vue";
 import { getFileUrl } from "@/utils/file.js";
 import { go } from "@/utils/utils.js";
+import { GetMyOrders } from "@/api/dining.js";
 
 const activeTab = ref('current');
 const searchKeyword = ref('');
 
-// 示例订单数据
-const currentOrders = ref([
-  {
-    id: 1,
-    orderNo: '1929065620709298441',
-    storeName: '正旗手选海鲜烧烤(东港店)',
-    createTime: '2025-06-01 14:40:58',
-    totalPrice: 19.9,
-    goodsCount: 2,
-    statusText: '进行中',
-    statusClass: 'status-active',
-    goodsImages: ['/static/demo.png', '/static/demo.png']
-  }
-]);
+// 分页参数
+const pageSize = 10;
+const currentPage = ref({ current: 1, history: 1 });
+const loading = ref(false);
+const noMore = ref({ current: false, history: false });
+
+// records 每项:order 为订单信息,cuisineItems 为菜品数组;兼容直接平铺的旧结构
+function normalizeOrder(record) {
+  const order = record?.order ?? record;
+  const cuisineItems = record?.cuisineItems ?? [];
+  const items = Array.isArray(cuisineItems) ? cuisineItems : [];
+
+  const orderStatus = order?.orderStatus ?? order?.status;
+  const payStatus = order?.payStatus;
+  const statusText = getDisplayStatusText(payStatus, orderStatus, order?.statusText ?? order?.orderStatusName);
+  const statusClass = getDisplayStatusClass(payStatus, orderStatus);
+
+  const goodsCount = items.reduce((sum, it) => sum + (Number(it?.quantity) || 0), 0) || order?.dinerCount || 0;
+  const goodsImages = items
+    .map((it) => it?.cuisineImage ?? it?.image ?? it?.imageUrl ?? '')
+    .filter((url) => url && String(url).trim());
+
+  return {
+    id: order?.id,
+    orderNo: order?.orderNo ?? order?.orderNumber ?? order?.orderId ?? '',
+    storeId: order?.storeId,
+    storeName: order?.storeName ?? order?.store?.name ?? (order?.storeId != null ? `门店${order.storeId}` : '—'),
+    tableId: order?.tableId,
+    tableNumber: order?.tableNumber ?? order?.tableId ?? '—',
+    contactPhone: order?.contactPhone ?? '',
+    createTime: order?.createdTime ?? order?.createTime ?? order?.orderTime ?? '—',
+    totalPrice: order?.payAmount ?? order?.totalPrice ?? order?.totalAmount ?? 0,
+    goodsCount,
+    statusText,
+    statusClass,
+    orderStatus,
+    payStatus,
+    dinerCount: order?.dinerCount,
+    discountAmount: order?.discountAmount ?? 0,
+    remark: order?.remark,
+    goodsImages,
+    cuisineItems: items
+  };
+}
+
+// orderStatus 订单状态:0 待支付,1 已支付,2 已取消,3 已完成
+function getDisplayStatusText(payStatus, orderStatus, fallback) {
+  if (fallback) return fallback;
+  return getOrderStatusText(orderStatus);
+}
+
+function getOrderStatusText(status) {
+  if (status == null && status !== 0) return '—';
+  const n = Number(status);
+  if (n === 0) return '待支付';
+  if (n === 1) return '已支付';
+  if (n === 2) return '已取消';
+  if (n === 3) return '已完成';
+  return String(status);
+}
+
+function getDisplayStatusClass(payStatus, orderStatus) {
+  return getStatusClass(orderStatus);
+}
 
+function getStatusClass(status) {
+  if (status == null && status !== 0) return '';
+  const n = Number(status);
+  if (n === 0) return 'status-unpaid';   // 待支付
+  if (n === 1) return 'status-paid';      // 已支付
+  if (n === 2) return 'status-cancelled'; // 已取消
+  if (n === 3) return 'status-completed'; // 已完成
+  return 'status-unpaid';
+}
+
+const currentOrders = ref([]);
 const historyOrders = ref([]);
 
-// 当前显示的订单列表
+// 当前显示的订单列表(含搜索过滤)
 const currentOrderList = computed(() => {
   const orders = activeTab.value === 'current' ? currentOrders.value : historyOrders.value;
-  if (!searchKeyword.value) {
-    return orders;
-  }
+  if (!searchKeyword.value) return orders;
   return orders.filter(order =>
-    order.orderNo.includes(searchKeyword.value) ||
-    order.storeName.includes(searchKeyword.value)
+    (order.orderNo && order.orderNo.includes(searchKeyword.value)) ||
+    (order.storeName && order.storeName.includes(searchKeyword.value))
   );
 });
 
-// 处理订单详情
+// type:0 未支付订单,1 历史订单
+const getOrderTypeByTab = (tab) => (tab === 'current' ? 0 : 1);
+
+async function loadOrderList(tab, append = false) {
+  const pageKey = tab === 'current' ? 'current' : 'history';
+  if (loading.value) return;
+  if (append && noMore.value[pageKey]) return;
+  if (!append) {
+    currentPage.value[pageKey] = 1;
+    noMore.value[pageKey] = false;
+  }
+  const page = currentPage.value[pageKey];
+  if (append && page === 1) return;
+
+  loading.value = true;
+  try {
+    const params = {
+      current: page,
+      size: pageSize,
+      type: getOrderTypeByTab(tab)
+    };
+    const res = await GetMyOrders(params);
+    // /store/order/my-orders:返回 data,可能为 { records, total } 或 { data: { records, total } }
+    const raw = res && typeof res === 'object' ? res : {};
+    const list = Array.isArray(raw.records)
+      ? raw.records
+      : Array.isArray(raw.data?.records)
+        ? raw.data.records
+        : Array.isArray(raw.list)
+          ? raw.list
+          : [];
+    const normalized = list.map(normalizeOrder);
+
+    if (tab === 'current') {
+      currentOrders.value = append ? [...currentOrders.value, ...normalized] : normalized;
+    } else {
+      historyOrders.value = append ? [...historyOrders.value, ...normalized] : normalized;
+    }
+
+    const total =
+      raw.total != null ? Number(raw.total) : (raw.data?.total != null ? Number(raw.data.total) : 0);
+    if (normalized.length < pageSize || (page * pageSize >= total)) {
+      noMore.value[pageKey] = true;
+    } else {
+      currentPage.value[pageKey] = page + 1;
+    }
+  } catch (e) {
+    uni.showToast({ title: e?.message || '加载失败', icon: 'none' });
+  } finally {
+    loading.value = false;
+  }
+}
+
+const storeIdRef = ref('');
+const tableIdRef = ref('');
+const STORAGE_STORE_ID = 'currentStoreId';
+const STORAGE_TABLE_ID = 'currentTableId';
+
+function doLoad() {
+  currentPage.value = { current: 1, history: 1 };
+  noMore.value = { current: false, history: false };
+  loadOrderList(activeTab.value, false);
+}
+
+watch(activeTab, (tab) => {
+  // 每次切换标签都重新拉取该 tab 的数据,保证来回切换时列表会更新
+  loadOrderList(tab, false);
+});
+
+// 处理订单详情(须传 orderId 详情页才会请求接口)
 const handleOrderDetail = (order) => {
-  console.log('订单详情');
-  go('/pages/orderInfo/orderDetail');
+  const orderId = order?.id ?? order?.orderId ?? '';
+  if (!orderId) {
+    uni.showToast({ title: '订单ID缺失', icon: 'none' });
+    return;
+  }
+  go(`/pages/orderInfo/orderDetail?orderId=${encodeURIComponent(String(orderId))}`);
 };
 
 // 切换标签页
@@ -125,24 +258,36 @@ const handleStoreClick = (order) => {
   // TODO: 跳转到店铺详情
 };
 
-// 处理加餐
+// 处理加餐:带上该订单的桌号和人数,点餐页依赖 tableid、diners 拉取列表和购物车
 const handleAddFood = (order) => {
-  console.log('去加餐:', order);
-  // TODO: 跳转到点餐页面
-  go('/pages/orderFood/index');
+  const tableid = order?.tableId ?? order?.tableNumber ?? uni.getStorageSync(STORAGE_TABLE_ID) ?? '';
+  const diners = order?.dinerCount ?? order?.diners ?? uni.getStorageSync('currentDiners') ?? '';
+  const q = [];
+  if (tableid !== '' && tableid != null) q.push(`tableid=${encodeURIComponent(String(tableid))}`);
+  if (diners !== '' && diners != null) q.push(`diners=${encodeURIComponent(String(diners))}`);
+  go(q.length ? `/pages/orderFood/index?${q.join('&')}` : '/pages/orderFood/index');
 };
 
-// 处理结算
+// 处理结算:带上桌号、人数、订单金额、备注,确认订单页用于展示和提交
 const handleCheckout = (order) => {
-  console.log('去结算:', order);
-  // TODO: 跳转到结算页面
-  go('/pages/placeOrder/index');
+  const tableId = order?.tableId ?? order?.tableNumber ?? '';
+  const diners = order?.dinerCount ?? order?.diners ?? '';
+  const totalAmount = order?.totalPrice ?? order?.payAmount ?? 0;
+  const remark = (order?.remark ?? '').trim().slice(0, 30);
+  const q = [];
+  if (tableId !== '' && tableId != null) q.push(`tableId=${encodeURIComponent(String(tableId))}`);
+  if (diners !== '' && diners != null) q.push(`diners=${encodeURIComponent(String(diners))}`);
+  if (totalAmount != null && totalAmount !== '') q.push(`totalAmount=${encodeURIComponent(String(totalAmount))}`);
+  if (remark !== '') q.push(`remark=${encodeURIComponent(remark)}`);
+  go(q.length ? `/pages/placeOrder/index?${q.join('&')}` : '/pages/placeOrder/index');
 };
 
-onLoad((e) => {
-  uni.setNavigationBarTitle({
-    title: '我的订单'
-  });
+onLoad((options) => {
+  uni.setNavigationBarTitle({ title: '我的订单' });
+  // 优先用 URL 参数,否则用本地缓存(点餐/下单流程会写入)
+  storeIdRef.value = options?.storeId ?? uni.getStorageSync(STORAGE_STORE_ID) ?? '';
+  tableIdRef.value = options?.tableId ?? uni.getStorageSync(STORAGE_TABLE_ID) ?? '';
+  doLoad();
 });
 </script>
 
@@ -275,6 +420,14 @@ onLoad((e) => {
         color: #008844;
       }
 
+      &.status-unpaid {
+        color: #008844;
+      }
+
+      &.status-paid {
+        color: #008844;
+      }
+
       &.status-completed {
         color: #999;
       }

+ 121 - 42
pages/orderInfo/orderDetail.vue

@@ -5,8 +5,8 @@
     <view class="card">
       <!-- 店铺信息 -->
       <view class="store-info">
-        <view class="store-info-title">正旗手选海鲜烧烤(东港店)</view>
-        <view class="store-info-address">辽宁省大连市甘井子区高新技术产业园区礼贤街39A号一层</view>
+        <view class="store-info-title">{{ orderDetail.storeName || '店铺名称' }}</view>
+        <view class="store-info-address">{{ orderDetail.storeAddress || '—' }}</view>
       </view>
       <view class="card-header">
         <view class="card-header-title">菜品详情</view>
@@ -30,7 +30,7 @@
             <!-- 价格和数量 -->
             <view class="food-item__right">
               <view class="food-item__price">
-                <text class="price-main">¥{{ item.price }}</text>
+                <text class="price-main">¥{{ formatPrice(item.price) }}</text>
               </view>
               <view class="food-item__quantity">{{ item.quantity || 1 }}份</view>
             </view>
@@ -44,19 +44,19 @@
       <view class="card-content">
         <view class="info-item">
           <view class="info-item-label">菜品总价</view>
-          <view class="info-item-value">1234567890</view>
+          <view class="info-item-value">¥{{ priceDetail.dishTotal }}</view>
         </view>
         <view class="info-item">
           <view class="info-item-label">餐具费</view>
-          <view class="info-item-value">1234567890</view>
+          <view class="info-item-value">¥{{ priceDetail.tablewareFee }}</view>
         </view>
         <view class="info-item">
           <view class="info-item-label">优惠券</view>
-          <view class="info-item-value">1234567890</view>
+          <view class="info-item-value">¥{{ priceDetail.couponDiscount }}</view>
         </view>
         <view class="price-line">
           <view class="price-line-label">合计</view>
-          <view class="price-line-value">¥890</view>
+          <view class="price-line-value">¥{{ priceDetail.total }}</view>
         </view>
       </view>
     </view>
@@ -70,27 +70,27 @@
       <view class="card-content">
         <view class="info-item">
           <view class="info-item-label">订单编号</view>
-          <view class="info-item-value">1234567890</view>
+          <view class="info-item-value">{{ orderDetail.orderNo || '—' }}</view>
         </view>
         <view class="info-item">
           <view class="info-item-label">就餐桌号</view>
-          <view class="info-item-value">1234567890</view>
+          <view class="info-item-value">{{ orderDetail.tableNo || '—' }}</view>
         </view>
         <view class="info-item">
           <view class="info-item-label">用餐人数</view>
-          <view class="info-item-value">1234567890</view>
+          <view class="info-item-value">{{ orderDetail.dinerCount ?? '—' }}</view>
         </view>
         <view class="info-item">
           <view class="info-item-label">下单时间</view>
-          <view class="info-item-value">1234567890</view>
+          <view class="info-item-value">{{ orderDetail.createTime || '—' }}</view>
         </view>
         <view class="info-item">
           <view class="info-item-label">联系电话</view>
-          <view class="info-item-value">1234567890</view>
+          <view class="info-item-value">{{ orderDetail.contactPhone || '—' }}</view>
         </view>
         <view class="info-item">
           <view class="info-item-label">备注信息</view>
-          <view class="info-item-value">1234567890</view>
+          <view class="info-item-value">{{ orderDetail.remark || '—' }}</view>
         </view>
       </view>
     </view>
@@ -109,43 +109,122 @@ import { onLoad } from "@dcloudio/uni-app";
 import { ref } from "vue";
 import { go } from "@/utils/utils.js";
 import { getFileUrl } from "@/utils/file.js";
+import { GetOrderInfo } from "@/api/dining.js";
 
 const payType = ref('confirmOrder'); // confirmOrder 确认下单 confirmPay 确认支付
 
-// 菜品列表数据(示例数据,实际应该从购物车或订单数据中获取)
-const foodList = ref([
-  {
-    id: 1,
-    name: '石板肉酱豆腐',
-    price: 19.9,
-    image: '/static/demo.png',
-    tags: [
-      { text: '份', type: 'normal' },
-      { text: '微辣', type: 'spicy' }
-    ],
-    quantity: 1
-  },
-  {
-    id: 2,
-    name: '经典三杯鸡',
-    price: 26.9,
-    image: '/static/demo.png',
-    tags: [
-      { text: '份', type: 'normal' }
-    ],
-    quantity: 1
-  }
-]);
+// 订单详情(店铺、订单信息)
+const orderDetail = ref({
+  storeName: '',
+  storeAddress: '',
+  orderNo: '',
+  tableId: '',   // 桌号ID,用于跳转加餐
+  tableNo: '',    // 桌号展示(tableNumber)
+  dinerCount: '',
+  createTime: '',
+  contactPhone: '',
+  remark: ''
+});
+
+// 价格明细(两位小数)
+const priceDetail = ref({
+  dishTotal: '0.00',
+  tablewareFee: '0.00',
+  couponDiscount: '0.00',
+  total: '0.00'
+});
 
+// 菜品列表(接口订单明细)
+const foodList = ref([]);
+
+// 金额保留两位小数
+const formatPrice = (price) => {
+  if (price === '' || price === null || price === undefined) return '0.00';
+  const num = Number(price);
+  return Number.isNaN(num) ? '0.00' : num.toFixed(2);
+};
+
+// 将接口订单项转为列表项
+function normalizeOrderItem(item) {
+  const tags = item?.tags ?? [];
+  const tagArr = Array.isArray(tags) ? tags : [];
+  const images = item?.images;
+  const imageUrl = Array.isArray(images) ? images[0] : images;
+  return {
+    id: item?.id ?? item?.cuisineId,
+    name: item?.cuisineName ?? item?.name ?? '',
+    price: item?.totalPrice ?? item?.unitPrice ?? item?.price ?? 0,
+    image: imageUrl ?? item?.image ?? item?.cuisineImage ?? 'img/icon/shop.png',
+    quantity: item?.quantity ?? 1,
+    tags: tagArr.map((t) => (typeof t === 'string' ? { text: t } : { text: t?.text ?? t?.tagName ?? '' }))
+  };
+}
+
+// 从接口数据填充 orderDetail、priceDetail、foodList
+function applyOrderData(data) {
+  const raw = data?.data ?? data ?? {};
+  const store = raw?.storeInfo ?? raw?.store ?? {};
+  orderDetail.value = {
+    storeName: store?.storeName ?? raw?.storeName ?? '',
+    storeAddress: store?.storeAddress ?? store?.address ?? raw?.storeAddress ?? raw?.address ?? '',
+    orderNo: raw?.orderNo ?? raw?.orderId ?? '',
+    tableId: raw?.tableId ?? raw?.tableNumber ?? '',  // 加餐跳转用
+    tableNo: raw?.tableNumber ?? raw?.tableNo ?? raw?.tableName ?? '',
+    dinerCount: raw?.dinerCount ?? '',
+    createTime: raw?.createdTime ?? raw?.createTime ?? raw?.orderTime ?? '',
+    contactPhone: raw?.contactPhone ?? raw?.phone ?? '',
+    remark: raw?.remark ?? ''
+  };
+  const dishTotal = raw?.dishTotal ?? raw?.orderAmount ?? raw?.foodAmount ?? 0;
+  const tablewareFee = raw?.tablewareFee ?? raw?.tablewareAmount ?? 0;
+  const couponDiscount = raw?.couponAmount ?? raw?.couponDiscount ?? 0;
+  const total = raw?.totalAmount ?? raw?.totalPrice ?? raw?.payAmount ?? 0;
+  priceDetail.value = {
+    dishTotal: formatPrice(dishTotal),
+    tablewareFee: formatPrice(tablewareFee),
+    couponDiscount: formatPrice(couponDiscount),
+    total: formatPrice(total)
+  };
+  const list = raw?.orderItemList ?? raw?.orderItems ?? raw?.items ?? raw?.detailList ?? [];
+  foodList.value = (Array.isArray(list) ? list : []).map(normalizeOrderItem);
+}
+
+// 去加餐:跳转点餐页,携带桌号与就餐人数
 const handleConfirmOrder = () => {
-  go('/pages/result/index');
+  const tableid = orderDetail.value?.tableId || orderDetail.value?.tableNo || '';
+  const diners = orderDetail.value?.dinerCount ?? '';
+  const q = [];
+  if (tableid !== '') q.push(`tableid=${encodeURIComponent(String(tableid))}`);
+  if (diners !== '') q.push(`diners=${encodeURIComponent(String(diners))}`);
+  go(q.length ? `/pages/orderFood/index?${q.join('&')}` : '/pages/orderFood/index');
 };
 
-onLoad((e) => {
+// 去结算:跳转确认订单页,携带桌号、就餐人数、订单金额
+const handleConfirmPay = () => {
+  const tableId = orderDetail.value?.tableId || orderDetail.value?.tableNo || '';
+  const diners = orderDetail.value?.dinerCount ?? '';
+  const totalAmount = priceDetail.value?.total ?? '';
+  const q = [];
+  if (tableId !== '') q.push(`tableId=${encodeURIComponent(String(tableId))}`);
+  if (diners !== '') q.push(`diners=${encodeURIComponent(String(diners))}`);
+  if (totalAmount !== '' && totalAmount != null) q.push(`totalAmount=${encodeURIComponent(String(totalAmount))}`);
+  go(q.length ? `/pages/placeOrder/index?${q.join('&')}` : '/pages/placeOrder/index');
+};
+
+onLoad(async (e) => {
   if (e.payType) payType.value = e.payType;
-  // TODO: 从存储或接口获取订单菜品数据
-  // const cartList = uni.getStorageSync('cartList') || [];
-  // foodList.value = cartList.filter(item => item.quantity > 0);
+  const orderId = e?.orderId ?? e?.id ?? '';
+  if (!orderId) {
+    uni.showToast({ title: '缺少订单ID', icon: 'none' });
+    return;
+  }
+  try {
+    const res = await GetOrderInfo(orderId);
+    applyOrderData(res);
+  } catch (err) {
+    console.error('订单详情加载失败:', err);
+    uni.showToast({ title: '加载失败', icon: 'none' });
+  }
 });
 </script>
 

+ 153 - 47
pages/placeOrder/index.vue

@@ -10,21 +10,27 @@
       <view class="card-content">
         <view class="info-item">
           <view class="info-item-label">就餐桌号</view>
-          <view class="info-item-value">1234567890</view>
+          <view class="info-item-value">{{ orderInfo.tableId || '—' }}</view>
         </view>
         <view class="info-item">
           <view class="info-item-label">用餐人数</view>
-          <view class="info-item-value">1234567890</view>
+          <view class="info-item-value">{{ orderInfo.diners || '—' }}人</view>
         </view>
         <view class="info-item">
           <view class="info-item-label">联系电话</view>
-          <view class="info-item-value">1234567890</view>
+          <view class="info-item-value">{{ orderInfo.contactPhone || '—' }}</view>
         </view>
         <view class="info-item">
           <view class="info-item-label">备注信息</view>
-          <view class="info-item-value"></view>
         </view>
-        <textarea placeholder="请输入特殊需求,如少辣、不要香菜等" class="info-item-textarea"></textarea>
+        <textarea
+          v-model="orderInfo.remark"
+          placeholder="请输入特殊需求,如少辣、不要香菜等(最多30字)"
+          class="info-item-textarea"
+          maxlength="30"
+          :disabled="fromCheckout"
+          :readonly="fromCheckout"
+        ></textarea>
       </view>
     </view>
 
@@ -36,13 +42,13 @@
       </view>
       <view class="card-content">
         <view class="info-food">
-          <view v-for="(item, index) in foodList" :key="item.id || index" class="food-item">
-            <!-- 菜品图片 -->
-            <image :src="getFileUrl(item.image)" mode="aspectFill" class="food-item__image"></image>
+          <view v-for="(item, index) in foodList" :key="item.id || item.cuisineId || index" class="food-item">
+            <!-- 菜品图片:兼容 cuisineImage/image,逗号分隔取首图 -->
+            <image :src="getItemImage(item)" mode="aspectFill" class="food-item__image"></image>
 
             <!-- 菜品信息 -->
             <view class="food-item__info">
-              <view class="food-item__name">{{ item.name }}</view>
+              <view class="food-item__name">{{ item.name || item.cuisineName }}</view>
               <view class="food-item__desc" v-if="item.tags && item.tags.length > 0">
                 <text v-for="(tag, tagIndex) in item.tags" :key="tagIndex" class="food-item__tag">
                   {{ tag.text }}<text v-if="tagIndex < item.tags.length - 1">,</text>
@@ -53,7 +59,7 @@
             <!-- 价格和数量 -->
             <view class="food-item__right">
               <view class="food-item__price">
-                <text class="price-main">¥{{ item.price }}</text>
+                <text class="price-main">¥{{ formatPrice(getItemPrice(item)) }}</text>
               </view>
               <view class="food-item__quantity">{{ item.quantity || 1 }}份</view>
             </view>
@@ -72,27 +78,27 @@
       <view class="card-content">
         <view class="info-item">
           <view class="info-item-label">菜品总价</view>
-          <view class="info-item-value">1234567890</view>
+          <view class="info-item-value">¥{{ formatPrice(orderInfo.totalAmount) }}</view>
         </view>
         <view class="info-item">
           <view class="info-item-label">餐具费</view>
-          <view class="info-item-value">1234567890</view>
+          <view class="info-item-value">¥{{ formatPrice(orderInfo.utensilFee ?? 0) }}</view>
         </view>
         <view class="info-item">
           <view class="info-item-label">优惠券</view>
-          <view class="info-item-value">1234567890</view>
+          <view class="info-item-value">¥{{ formatPrice(orderInfo.discountAmount ?? 0) }}</view>
         </view>
         <view class="price-line">
           <view class="price-line-label">应付金额</view>
-          <view class="price-line-value">¥890</view>
+          <view class="price-line-value">¥{{ formatPrice(orderInfo.payAmount ?? orderInfo.totalAmount) }}</view>
         </view>
       </view>
     </view>
 
-    <!-- 底部按钮 -->
+    <!-- 底部按钮:带金额(来自去结算)显示「确认支付」且备注不可改;不带金额显示「确认下单」且备注可改 -->
     <view class="bottom-button">
-      <view class="bottom-button-text" hover-class="hover-active" @click="handleConfirmOrder" v-if="payType === 'confirmOrder'">确认下单</view>
-      <view class="bottom-button-text" hover-class="hover-active" @click="handleConfirmPay" v-if="payType === 'confirmPay'">确认支付 ¥890</view>
+      <view class="bottom-button-text" hover-class="hover-active" @click="handleConfirmPay" v-if="fromCheckout">确认支付 ¥{{ formatPrice(orderInfo.payAmount ?? orderInfo.totalAmount) }}</view>
+      <view class="bottom-button-text" hover-class="hover-active" @click="handleConfirmOrder" v-else>确认下单</view>
     </view>
 
   </view>
@@ -103,43 +109,143 @@ import { onLoad } from "@dcloudio/uni-app";
 import { ref } from "vue";
 import { go } from "@/utils/utils.js";
 import { getFileUrl } from "@/utils/file.js";
+import { useUserStore } from "@/store/user.js";
+import { PostOrderCreate } from "@/api/dining.js";
 
 const payType = ref('confirmOrder'); // confirmOrder 确认下单 confirmPay 确认支付
+/** 仅当从订单列表点击「去结算」进入时为 true,底部确认下单按钮才显示金额 */
+const fromCheckout = ref(false);
+
+// 订单信息(从购物车带过来或 URL 参数)
+const orderInfo = ref({
+  tableId: '',
+  diners: '',
+  contactPhone: '',
+  remark: '',
+  totalAmount: 0,
+  utensilFee: 0,
+  discountAmount: 0,
+  payAmount: 0
+});
+
+// 菜品列表(从购物车带过来)
+const foodList = ref([]);
 
-// 菜品列表数据(示例数据,实际应该从购物车或订单数据中获取)
-const foodList = ref([
-  {
-    id: 1,
-    name: '石板肉酱豆腐',
-    price: 19.9,
-    image: '/static/demo.png',
-    tags: [
-      { text: '份', type: 'normal' },
-      { text: '微辣', type: 'spicy' }
-    ],
-    quantity: 1
-  },
-  {
-    id: 2,
-    name: '经典三杯鸡',
-    price: 26.9,
-    image: '/static/demo.png',
-    tags: [
-      { text: '份', type: 'normal' }
-    ],
-    quantity: 1
+// 菜品图片:兼容 cuisineImage/image,逗号分隔取首图
+function getItemImage(item) {
+  const raw = item?.cuisineImage ?? item?.image ?? item?.imageUrl ?? '';
+  const url = typeof raw === 'string' ? raw.split(',')[0].trim() : raw;
+  return url ? getFileUrl(url) : '';
+}
+
+// 单品单价:列表展示用 unitPrice/price
+function getItemPrice(item) {
+  const p = item?.unitPrice ?? item?.price ?? item?.totalPrice ?? item?.salePrice ?? 0;
+  return Number(p) || 0;
+}
+
+function formatPrice(price) {
+  const num = Number(price);
+  return Number.isNaN(num) ? '0.00' : num.toFixed(2);
+}
+
+// 将存储的购物车项转为确认页展示格式(兼容 cuisineName/cuisineImage/unitPrice 等)
+function normalizeCartItem(item) {
+  const image = item?.cuisineImage ?? item?.image ?? item?.imageUrl ?? '';
+  const url = typeof image === 'string' ? image.split(',')[0].trim() : image;
+  return {
+    id: item?.id ?? item?.cuisineId,
+    name: item?.name ?? item?.cuisineName,
+    cuisineName: item?.cuisineName,
+    price: item?.unitPrice ?? item?.price,
+    image: url,
+    cuisineImage: item?.cuisineImage,
+    quantity: Number(item?.quantity) || 0,
+    tags: item?.tags ?? [],
+    subtotalAmount: item?.subtotalAmount,
+    remark: item?.remark
+  };
+}
+
+const handleConfirmOrder = async () => {
+  const tableId = orderInfo.value.tableId;
+  const contactPhone = orderInfo.value.contactPhone ?? '';
+  const dinerCount = Number(orderInfo.value.diners) || 0;
+  if (!tableId) {
+    uni.showToast({ title: '请先选择桌号', icon: 'none' });
+    return;
+  }
+  if (!contactPhone) {
+    uni.showToast({ title: '请先填写联系电话', icon: 'none' });
+    return;
+  }
+  const remark = (orderInfo.value.remark ?? '').trim().slice(0, 30);
+  uni.showLoading({ title: '提交中...' });
+  try {
+    const res = await PostOrderCreate({
+      tableId,
+      contactPhone,
+      couponId: null,
+      dinerCount: dinerCount || undefined,
+      immediatePay: 0,
+      remark: remark || undefined
+    });
+    uni.hideLoading();
+    const orderId = res?.id ?? res?.orderId ?? res?.data?.id ?? res?.data?.orderId;
+    // 结果页「去结算」需带金额和备注,保存本次下单的桌号、人数、金额、备注
+    const total = orderInfo.value.payAmount ?? orderInfo.value.totalAmount ?? 0;
+    uni.setStorageSync('lastPlaceOrderInfo', JSON.stringify({
+      tableId: orderInfo.value.tableId,
+      diners: orderInfo.value.diners,
+      totalAmount: total,
+      remark: (orderInfo.value.remark ?? '').trim().slice(0, 30)
+    }));
+    if (orderId != null) {
+      go(`/pages/result/index?id=${encodeURIComponent(orderId)}`);
+    } else {
+      go('/pages/result/index');
+    }
+  } catch (e) {
+    uni.hideLoading();
+    uni.showToast({ title: e?.message || '下单失败', icon: 'none' });
   }
-]);
+};
 
-const handleConfirmOrder = () => {
-  go('/pages/result/index');
+const handleConfirmPay = () => {
+  go('/pages/result/index?payType=confirmPay');
 };
 
-onLoad((e) => {
-  if(e.payType) payType.value = e.payType;
-  // TODO: 从存储或接口获取订单菜品数据
-  // const cartList = uni.getStorageSync('cartList') || [];
-  // foodList.value = cartList.filter(item => item.quantity > 0);
+onLoad((options) => {
+  if (options?.payType) payType.value = options.payType;
+  const userStore = useUserStore();
+  const contactPhone = userStore.getUserInfo?.phone ?? userStore.getUserInfo?.contactPhone ?? userStore.getUserInfo?.mobile ?? '';
+  orderInfo.value.contactPhone = contactPhone;
+  if (options?.tableId) orderInfo.value.tableId = options.tableId;
+  if (options?.diners) orderInfo.value.diners = options.diners;
+  if (options?.remark != null && options?.remark !== '') orderInfo.value.remark = decodeURIComponent(options.remark);
+  if (options?.totalAmount != null && options?.totalAmount !== '') {
+    fromCheckout.value = true;
+    const total = Number(options.totalAmount) || 0;
+    orderInfo.value.totalAmount = total;
+    orderInfo.value.payAmount = total;
+  }
+  const raw = uni.getStorageSync('placeOrderCart');
+  if (raw) {
+    try {
+      const data = JSON.parse(raw);
+      const list = Array.isArray(data.list) ? data.list : [];
+      foodList.value = list.map(normalizeCartItem).filter((item) => (item.quantity || 0) > 0);
+      if (data.tableId != null) orderInfo.value.tableId = data.tableId;
+      if (data.diners != null) orderInfo.value.diners = data.diners;
+      if (data.remark != null) orderInfo.value.remark = data.remark;
+      const total = Number(data.totalAmount) || 0;
+      orderInfo.value.totalAmount = total;
+      orderInfo.value.payAmount = total;
+    } catch (e) {
+      console.error('解析购物车数据失败:', e);
+    }
+  }
+  if (orderInfo.value.tableId) uni.setStorageSync('currentTableId', String(orderInfo.value.tableId));
 });
 </script>
 

+ 30 - 2
pages/result/index.vue

@@ -17,12 +17,40 @@ import { ref } from "vue";
 import { go } from "@/utils/utils.js";
 import { getFileUrl } from "@/utils/file.js";
 
+// 去加餐:从缓存取桌号、人数并带上,点餐页依赖 tableid、diners 拉取列表和购物车
 const handleGoAddFood = () => {
-  go('/pages/orderFood/index');
+  const tableid = uni.getStorageSync('currentTableId') ?? '';
+  const diners = uni.getStorageSync('currentDiners') ?? '';
+  const q = [];
+  if (tableid !== '' && tableid != null) q.push(`tableid=${encodeURIComponent(String(tableid))}`);
+  if (diners !== '' && diners != null) q.push(`diners=${encodeURIComponent(String(diners))}`);
+  go(q.length ? `/pages/orderFood/index?${q.join('&')}` : '/pages/orderFood/index');
 };
 
+// 去结算:带上桌号、人数、金额、备注,确认订单页才显示金额并走「确认支付」
 const handleGoSettle = () => {
-  go('/pages/placeOrder/index?payType=confirmPay');
+  let tableId = '';
+  let diners = '';
+  let totalAmount = '';
+  let remark = '';
+  try {
+    const raw = uni.getStorageSync('lastPlaceOrderInfo');
+    if (raw) {
+      const data = JSON.parse(raw);
+      tableId = data?.tableId ?? '';
+      diners = data?.diners ?? '';
+      totalAmount = data?.totalAmount != null ? String(data.totalAmount) : '';
+      remark = (data?.remark ?? '').trim().slice(0, 30);
+    }
+  } catch (e) {
+    console.error('读取下单信息失败', e);
+  }
+  const q = [];
+  if (tableId !== '') q.push(`tableId=${encodeURIComponent(tableId)}`);
+  if (diners !== '') q.push(`diners=${encodeURIComponent(diners)}`);
+  if (totalAmount !== '') q.push(`totalAmount=${encodeURIComponent(totalAmount)}`);
+  if (remark !== '') q.push(`remark=${encodeURIComponent(remark)}`);
+  go(q.length ? `/pages/placeOrder/index?${q.join('&')}` : '/pages/placeOrder/index');
 };
 
 onLoad((e) => {

+ 0 - 2
utils/request.js

@@ -103,7 +103,6 @@ export class Request {
 						case 400:
 							uni.showModal({
 								title: data.msg || '系统异常,请联系管理员',
-								cancelText: '回到首页',
 								confirmText: '确定',
 								success: (res) => {
 									if (res.confirm) {
@@ -118,7 +117,6 @@ export class Request {
 							if (hideErrorModal) return;
 							uni.showModal({
 								title: data.msg || '系统异常,请联系管理员',
-								cancelText: '回到首页',
 								confirmText: '确定',
 								success: (res) => {
 									if (res.confirm) {