Explorar el Código

feat(buffet): implement buffet ordering system and update documentation

FanLide hace 2 semanas
padre
commit
825e86bdb7

+ 84 - 0
CLAUDE.md

@@ -0,0 +1,84 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/claude-code) when working with this codebase.
+
+## Project Overview
+
+LINE Order App - A mobile-first web application for restaurant ordering, built with Vue 3 + TypeScript + LIFF (LINE Front-end Framework).
+
+## Tech Stack
+
+- **Framework**: Vue 3 (Composition API) + TypeScript
+- **Build Tool**: Vite 5
+- **State Management**: Pinia 2
+- **Router**: Vue Router 4
+- **UI Components**: Vant 4
+- **HTTP Client**: Axios
+- **WebSocket**: Socket.IO Client
+- **i18n**: vue-i18n 9 (ja, en, zh-Hans, zh-Hant)
+- **LINE SDK**: @line/liff
+
+## Common Commands
+
+```bash
+# Install dependencies
+npm install
+
+# Start development server
+npm run dev
+
+# Build for production
+npm run build
+
+# Preview production build
+npm run preview
+```
+
+## Architecture
+
+The app supports four business modes:
+
+1. **Customer Mode** - C-end ordering interface (LINE/mobile browser)
+2. **POS Mode** - Staff tablet interface for table/order management
+3. **Owner Mode** - Mobile management dashboard for shop owners
+4. **Platform Mode** - Aggregated homepage with shop listings and LBS
+
+## Key Directories
+
+- `src/api/` - API interface definitions
+- `src/components/` - Reusable Vue components
+- `src/composables/` - Vue composition functions (hooks)
+- `src/config/` - Global configuration
+- `src/locale/` - i18n language packs (ja, en, zh-Hans, zh-Hant)
+- `src/router/` - Route definitions
+- `src/store/` - Pinia state management
+- `src/utils/` - Utility functions
+- `src/views/` - Page components organized by feature
+
+## Internationalization (i18n)
+
+All user-facing text must use i18n. Language files are in `src/locale/`:
+- `ja.json` - Japanese (default)
+- `en.json` - English
+- `zh-Hans.json` - Simplified Chinese
+- `zh-Hant.json` - Traditional Chinese
+
+When adding new text:
+1. Add the key-value pair to all four language files
+2. Use `$t('key')` in templates or `t('key')` in setup scripts
+
+## Coding Conventions
+
+- Use Composition API with `<script setup lang="ts">`
+- Use Vant 4 components for UI (auto-imported)
+- Follow existing file naming: kebab-case for files, PascalCase for components
+- Keep components focused and reusable
+- Use TypeScript for type safety
+
+## Environment Variables
+
+Located in `.env.development` and `.env.production`:
+- `VITE_API_URL` - Backend API URL
+- `VITE_WS_URL` - WebSocket URL
+- `VITE_LIFF_ID` - LINE LIFF ID
+- `VITE_TENANT_ID` - Tenant ID

+ 287 - 0
doc/CORE_PROGRESS.md

@@ -0,0 +1,287 @@
+# LINE 订餐系统 - 核心开发进度
+
+**项目**: LINE Order App
+**更新日期**: 2026-01-14
+**总体完成度**: 约 70%
+
+---
+
+## 一、项目概述
+
+将 uniapp 订餐系统重构为基于 LINE LIFF 的 Web 应用,支持多角色、多模式的餐饮点餐场景。
+
+| 项目   | 说明                                                        |
+| ------ | ----------------------------------------------------------- |
+| 原项目 | `/orderApp/online-order-uniapp` (Vue 3 + uniapp)            |
+| 新项目 | `/orderApp/line-order-app` (Vue 3 + LIFF + Vite)            |
+| 技术栈 | Vue 3 + Vite 5 + Pinia 2 + Vue Router 4 + Vant 4 + LIFF SDK |
+
+---
+
+## 二、完成状态总览
+
+```
+██████████████████░░░░░░░░░░░░ 70%
+
+✅ 已完成: 基础架构、顾客端核心页面、国际化
+🔄 进行中: POS/店长/商家/管理后台页面
+⏳ 待开始: 第三方支付、评价系统、消息中心
+```
+
+---
+
+## 三、按模块完成状态
+
+### 3.1 基础架构 ✅ 100% 完成
+
+| 模块           | 状态 | 说明                  |
+| -------------- | ---- | --------------------- |
+| Vite + Vue 3   | ✅   | 项目搭建完成          |
+| Pinia 状态管理 | ✅   | app/user/cart/order   |
+| Vue Router     | ✅   | 路由 + 守卫           |
+| Axios API      | ✅   | 请求封装 + 拦截器     |
+| 国际化 i18n    | ✅   | ja/en/zh-Hans/zh-Hant |
+| LIFF SDK       | ✅   | 环境检测 + 初始化     |
+| Vant 4 UI      | ✅   | 自动导入配置          |
+| Socket.IO      | ✅   | WebSocket 集成        |
+
+### 3.2 顾客端 (C 端) ✅ 95% 完成
+
+| 页面     | 路由            | 状态       | 功能说明                 |
+| -------- | --------------- | ---------- | ------------------------ |
+| 首页     | `/index`        | ✅ 完成    | 轮播图、分类、推荐商品   |
+| 菜单列表 | `/menu`         | ✅ 完成    | 分类侧栏、商品列表、搜索 |
+| 商品详情 | `/menu/detail`  | ✅ 完成    | 图片轮播、规格选择       |
+| 购物车   | `/cart`         | ✅ 完成    | 商品管理、优惠券、提交   |
+| 订单列表 | `/order`        | ✅ 完成    | Tab 切换、状态筛选       |
+| 订单详情 | `/order/detail` | ✅ 完成    | 状态步骤、操作按钮       |
+| 我的     | `/mine`         | ✅ 完成    | 用户信息、VIP、服务入口  |
+| 登录     | `/login`        | ✅ 完成    | 手机号 + LINE 登录       |
+| 地址管理 | `/address`      | ✅ 完成    | 列表、新增、编辑         |
+| 支付     | `/payment`      | 🔄 UI 完成 | 待接入第三方支付         |
+| 扫码入口 | `/scan`         | ✅ 完成    | 解析店铺/桌位码          |
+
+### 3.3 POS 端 (店员) 🔄 60% 完成
+
+| 页面     | 路由                | 状态       | 功能说明            |
+| -------- | ------------------- | ---------- | ------------------- |
+| 欢迎页   | `/pos/welcome`      | ✅ UI 完成 | 店员登录入口        |
+| 桌台管理 | `/pos/tables`       | ✅ UI 完成 | 桌位状态、开台/清台 |
+| 订单管理 | `/pos/orders`       | ✅ UI 完成 | 接单、出餐管理      |
+| 订单详情 | `/pos/order-detail` | ✅ UI 完成 | 订单详情查看        |
+| 收银结账 | -                   | ⏳ 待开发  | 结账功能            |
+| 呼叫服务 | -                   | ⏳ 待开发  | 服务员呼叫响应      |
+
+### 3.4 店长端 (Owner) 🔄 50% 完成
+
+| 页面     | 路由               | 状态       | 功能说明    |
+| -------- | ------------------ | ---------- | ----------- |
+| 仪表盘   | `/owner/dashboard` | ✅ UI 完成 | 销售概览    |
+| 报表     | `/owner/reports`   | ✅ UI 完成 | 销售统计    |
+| 店铺管理 | `/owner/shops`     | ✅ UI 完成 | 多店铺列表  |
+| 菜品管理 | -                  | ⏳ 待开发  | 菜单编辑    |
+| 员工管理 | -                  | ⏳ 待开发  | 员工权限    |
+| 营销活动 | -                  | ⏳ 待开发  | 优惠券/满减 |
+
+### 3.5 商家端 (Merchant) 🔄 40% 完成
+
+| 页面       | 路由                     | 状态       | 功能说明   |
+| ---------- | ------------------------ | ---------- | ---------- |
+| 仪表盘     | `/merchant/dashboard`    | ✅ UI 完成 | 商家概览   |
+| 订单管理   | `/merchant/orders`       | ✅ UI 完成 | 订单处理   |
+| 自助餐计划 | `/merchant/buffet-plans` | ✅ UI 完成 | 套餐管理   |
+| 菜品管理   | -                        | ⏳ 待开发  | 菜单 CRUD  |
+| 店铺设置   | -                        | ⏳ 待开发  | 营业时间等 |
+
+### 3.6 管理后台 (Admin) 🔄 30% 完成
+
+| 页面     | 路由               | 状态       | 功能说明 |
+| -------- | ------------------ | ---------- | -------- |
+| 仪表盘   | `/admin/dashboard` | ✅ UI 完成 | 平台概览 |
+| 商家管理 | `/admin/merchants` | ✅ UI 完成 | 商家列表 |
+| 用户管理 | `/admin/users`     | ✅ UI 完成 | 用户列表 |
+| 系统设置 | -                  | ⏳ 待开发  | 全局配置 |
+| 数据统计 | -                  | ⏳ 待开发  | 平台报表 |
+
+### 3.7 自助餐模块 (Buffet) 🔄 70% 完成
+
+| 页面       | 路由                | 状态       | 功能说明              |
+| ---------- | ------------------- | ---------- | --------------------- |
+| 套餐选择   | `/buffet/select`    | ✅ UI 完成 | 自助餐套餐选择        |
+| 点餐菜单   | `/buffet/menu`      | ✅ UI 完成 | 自助餐点餐界面        |
+| 计时器组件 | `BuffetTimer.vue`   | ✅ 完成    | 放题计时、提醒        |
+| 状态管理   | `buffet.ts` (store) | ✅ 完成    | Pinia 状态管理        |
+| 后端 API   | -                   | ⏳ 待开发  | API 集成(P0 优先级) |
+| 订单追踪   | -                   | ⏳ 待开发  | 上菜状态              |
+
+**已完成功能**:
+
+- ✅ 放题套餐选择 UI(时长、价格、人数)
+- ✅ 放题点餐界面(分类、购物车)
+- ✅ 倒计时功能(暂停/恢复/提醒)
+- ✅ 点单次数限制
+- ✅ 前端状态管理
+
+**待完成功能**:
+
+- ❌ 放题 API 集成(`/api/buffet.ts`)
+- ❌ 服务器时间同步
+- ❌ 真实订单提交
+- ❌ 会话持久化
+
+---
+
+## 四、待完成任务规划
+
+### Phase 1: 核心功能完善 (高优先级)
+
+| 任务                  | 优先级 | 预估工作量 | 说明                              |
+| --------------------- | ------ | ---------- | --------------------------------- |
+| **放题点餐 API 集成** | **P0** | **中**     | **后端 API 集成、服务器时间同步** |
+| 第三方支付集成        | P0     | 中         | LINE Pay / 微信支付               |
+| POS 收银结账          | P0     | 中         | 扫码支付、现金结账                |
+| 菜品管理 CRUD         | P1     | 中         | 商家/店长端                       |
+| 订单状态推送          | P1     | 小         | WebSocket 实时通知                |
+| 优惠券使用优化        | P1     | 小         | 选择和抵扣逻辑                    |
+
+### Phase 2: 运营功能 (中优先级)
+
+| 任务     | 优先级 | 预估工作量 | 说明               |
+| -------- | ------ | ---------- | ------------------ |
+| 评价系统 | P2     | 中         | 订单评价、商品评分 |
+| 员工管理 | P2     | 中         | 权限分配           |
+| 营销活动 | P2     | 中         | 满减/折扣/优惠券   |
+| 数据报表 | P2     | 中         | 销售统计、导出     |
+
+### Phase 3: 体验优化 (低优先级)
+
+| 任务     | 优先级 | 预估工作量 | 说明       |
+| -------- | ------ | ---------- | ---------- |
+| 消息中心 | P3     | 小         | 通知列表   |
+| 会员系统 | P3     | 中         | 积分、等级 |
+| 分享功能 | P3     | 小         | LINE 分享  |
+| 搜索优化 | P3     | 小         | 智能搜索   |
+
+---
+
+## 五、三种业务模式状态
+
+| 模式         | 入口                             | 完成度 | 说明               |
+| ------------ | -------------------------------- | ------ | ------------------ |
+| **平台模式** | `/index`                         | ✅ 90% | 首页浏览、店铺列表 |
+| **店铺模式** | `/menu?shopId=xxx`               | ✅ 90% | 外卖/自提点餐      |
+| **桌位模式** | `/menu?shopId=xxx&tableCode=xxx` | 🔄 70% | 堂食点餐           |
+
+---
+
+## 六、国际化状态
+
+| 语言               | 文件                  | 状态 | 覆盖率 |
+| ------------------ | --------------------- | ---- | ------ |
+| 日语 (ja)          | `locale/ja.json`      | ✅   | 100%   |
+| 英语 (en)          | `locale/en.json`      | ✅   | 100%   |
+| 简体中文 (zh-Hans) | `locale/zh-Hans.json` | ✅   | 100%   |
+| 繁体中文 (zh-Hant) | `locale/zh-Hant.json` | ✅   | 100%   |
+
+---
+
+## 七、技术债务
+
+| 问题          | 优先级 | 说明                         |
+| ------------- | ------ | ---------------------------- |
+| API Mock 数据 | 中     | 部分页面使用模拟数据         |
+| 类型定义      | 低     | 部分文件缺少 TypeScript 类型 |
+| 单元测试      | 低     | 暂无测试覆盖                 |
+| E2E 测试      | 低     | 暂无端到端测试               |
+
+---
+
+## 八、快速命令
+
+```bash
+# 安装依赖
+npm install
+
+# 开发模式
+npm run dev
+
+# 构建生产
+npm run build
+
+# 预览构建
+npm run preview
+```
+
+---
+
+## 九、测试入口
+
+| 角色      | URL                                   | 说明     |
+| --------- | ------------------------------------- | -------- |
+| 顾客-平台 | `/#/index`                            | 首页     |
+| 顾客-外卖 | `/#/menu?shopId=shop_1`               | 店铺点餐 |
+| 顾客-堂食 | `/#/menu?shopId=shop_1&tableCode=A01` | 桌位点餐 |
+| POS       | `/#/pos/welcome`                      | 店员入口 |
+| 店长      | `/#/owner/dashboard`                  | 店长后台 |
+| 商家      | `/#/merchant/dashboard`               | 商家后台 |
+| 管理员    | `/#/admin/dashboard`                  | 管理后台 |
+
+---
+
+## 十、下一步行动建议
+
+### 立即可做
+
+1. **完成放题点餐 API 集成** - 连接后端 API,实现完整放题流程
+2. **完善支付功能** - 接入 LINE Pay 或模拟支付流程
+3. **完善 POS 收银** - 实现结账功能
+4. **测试核心流程** - 浏览 → 点餐 → 下单 → 支付
+
+### 近期规划
+
+1. 放题订单追踪功能
+2. 菜品管理 CRUD 功能
+3. 员工权限管理
+4. 订单实时通知优化
+
+---
+
+**图例**: ✅ 完成 | 🔄 进行中/UI 完成 | ⏳ 待开始
+已完成 ✅
+┌──────────────┬────────┐
+│ 模块 │ 完成度 │
+├──────────────┼────────┤
+│ 基础架构 │ 100% │
+├──────────────┼────────┤
+│ 顾客端 (C 端) │ 95% │
+├──────────────┼────────┤
+│ 国际化 │ 100% │
+└──────────────┴────────┘
+进行中 🔄
+┌──────────┬────────┬─────────────────────────────┐
+│ 模块 │ 完成度 │ 说明 │
+├──────────┼────────┼─────────────────────────────┤
+│ POS 端 │ 60% │ UI 完成,待完善收银结账 │
+├──────────┼────────┼─────────────────────────────┤
+│ 店长端 │ 50% │ UI 完成,待开发菜品/员工管理 │
+├──────────┼────────┼─────────────────────────────┤
+│ 商家端 │ 40% │ UI 完成,待开发菜品管理 │
+├──────────┼────────┼──ß───────────────────────────┤
+│ 管理后台 │ 30% │ UI 完成,待开发系统设置 │
+├──────────┼────────┼─────────────────────────────┤
+│ 自助餐 │ 50% │ UI 完成,待开发订单追踪 │
+└──────────┴────────┴─────────────────────────────┘
+待开发任务规划 ß
+
+Phase 1 (高优先级)
+
+- P0: 第三方支付集成、POS 收银结账
+- P1: 菜品管理 CRUD、订单状态推送
+
+Phase 2 (中优先级)
+
+- P2: 评价系统、员工管理、营销活动、数据报表
+
+Phase 3 (低优先级)
+
+- P3: 消息中心、会员系统、分享功能

+ 342 - 0
src/api/buffet.ts

@@ -0,0 +1,342 @@
+/**
+ * 放题(自助餐)相关API
+ * 目前使用Mock数据,后端API就绪后可快速切换
+ */
+
+import request from './request'
+
+// ==================== 配置 ====================
+const USE_MOCK = true // 切换为false以使用真实API
+
+// ==================== 类型定义 ====================
+export interface BuffetPlan {
+  id: string
+  name: string
+  duration: number // 时长(分钟)
+  price: number
+  description?: string
+  menuCategories?: string[]
+  maxOrders?: number // 最大点单次数(0=不限制)
+  reminderMinutes: number[]
+  status: 'active' | 'inactive'
+}
+
+export interface BuffetSession {
+  id: string
+  planId: string
+  planName: string
+  tableId: string
+  tableName: string
+  startTime: number
+  endTime: number
+  serverTime: number // 服务器当前时间
+  duration: number
+  status: 'active' | 'paused' | 'expired' | 'completed'
+  orderCount: number
+  maxOrders?: number
+}
+
+export interface BuffetOrderItem {
+  dishId: string
+  dishName: string
+  quantity: number
+  spec?: string
+}
+
+export interface CreateBuffetOrderParams {
+  sessionId: string
+  items: BuffetOrderItem[]
+  remark?: string
+}
+
+// ==================== Mock 数据 ====================
+const mockPlans: BuffetPlan[] = [
+  {
+    id: 'plan_1',
+    name: '90分钟畅饮畅食',
+    duration: 90,
+    price: 2980,
+    description: '90分钟内无限点餐,含酒水饮料',
+    reminderMinutes: [30, 10, 5],
+    status: 'active',
+    maxOrders: 0
+  },
+  {
+    id: 'plan_2',
+    name: '120分钟豪华套餐',
+    duration: 120,
+    price: 3980,
+    description: '120分钟豪华放题,包含高级食材',
+    reminderMinutes: [60, 30, 10],
+    status: 'active',
+    maxOrders: 0
+  },
+  {
+    id: 'plan_3',
+    name: '60分钟轻食套餐',
+    duration: 60,
+    price: 1980,
+    description: '60分钟快捷放题,最多点5轮',
+    reminderMinutes: [30, 10, 5],
+    status: 'active',
+    maxOrders: 5
+  }
+]
+
+// 模拟存储当前会话(仅用于Mock)
+let mockCurrentSession: BuffetSession | null = null
+
+// ==================== Mock 函数 ====================
+function mockDelay(ms: number = 500): Promise<void> {
+  return new Promise(resolve => setTimeout(resolve, ms))
+}
+
+function mockResponse<T>(data: T) {
+  return {
+    code: 0,
+    data,
+    message: 'success'
+  }
+}
+
+// ==================== API 接口 ====================
+
+/**
+ * 获取放题方案列表
+ */
+export async function getBuffetPlans(params?: { shopId?: string }) {
+  if (USE_MOCK) {
+    await mockDelay()
+    return mockResponse(mockPlans.filter(p => p.status === 'active'))
+  }
+
+  return request({
+    url: '/app-api/buffet/plan/list',
+    method: 'get',
+    params
+  })
+}
+
+/**
+ * 获取放题方案详情
+ */
+export async function getBuffetPlanDetail(id: string) {
+  if (USE_MOCK) {
+    await mockDelay()
+    const plan = mockPlans.find(p => p.id === id)
+    return mockResponse(plan || null)
+  }
+
+  return request({
+    url: `/app-api/buffet/plan/detail/${id}`,
+    method: 'get'
+  })
+}
+
+/**
+ * 创建放题会话(开始放题)
+ */
+export async function createBuffetSession(data: {
+  planId: string
+  tableId: string
+  tableName: string
+  guestCount: number
+}) {
+  if (USE_MOCK) {
+    await mockDelay(800)
+    const plan = mockPlans.find(p => p.id === data.planId)
+    if (!plan) {
+      throw new Error('方案不存在')
+    }
+
+    const now = Date.now()
+    const durationMs = plan.duration * 60 * 1000
+
+    mockCurrentSession = {
+      id: `session_${Date.now()}`,
+      planId: data.planId,
+      planName: plan.name,
+      tableId: data.tableId,
+      tableName: data.tableName,
+      startTime: now,
+      endTime: now + durationMs,
+      serverTime: now,
+      duration: plan.duration,
+      status: 'active',
+      orderCount: 0,
+      maxOrders: plan.maxOrders
+    }
+
+    return mockResponse(mockCurrentSession)
+  }
+
+  return request({
+    url: '/app-api/buffet/session/create',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 获取当前放题会话
+ */
+export async function getCurrentSession() {
+  if (USE_MOCK) {
+    await mockDelay(300)
+    if (mockCurrentSession) {
+      // 更新服务器时间
+      mockCurrentSession.serverTime = Date.now()
+    }
+    return mockResponse(mockCurrentSession)
+  }
+
+  return request({
+    url: '/app-api/buffet/session/current',
+    method: 'get'
+  })
+}
+
+/**
+ * 暂停放题会话
+ */
+export async function pauseBuffetSession(sessionId: string) {
+  if (USE_MOCK) {
+    await mockDelay()
+    if (mockCurrentSession && mockCurrentSession.id === sessionId) {
+      mockCurrentSession.status = 'paused'
+      mockCurrentSession.serverTime = Date.now()
+    }
+    return mockResponse({ success: true })
+  }
+
+  return request({
+    url: '/app-api/buffet/session/pause',
+    method: 'post',
+    data: { sessionId }
+  })
+}
+
+/**
+ * 恢复放题会话
+ */
+export async function resumeBuffetSession(sessionId: string) {
+  if (USE_MOCK) {
+    await mockDelay()
+    if (mockCurrentSession && mockCurrentSession.id === sessionId) {
+      mockCurrentSession.status = 'active'
+      mockCurrentSession.serverTime = Date.now()
+    }
+    return mockResponse({ success: true })
+  }
+
+  return request({
+    url: '/app-api/buffet/session/resume',
+    method: 'post',
+    data: { sessionId }
+  })
+}
+
+/**
+ * 结束放题会话
+ */
+export async function completeBuffetSession(sessionId: string) {
+  if (USE_MOCK) {
+    await mockDelay()
+    if (mockCurrentSession && mockCurrentSession.id === sessionId) {
+      mockCurrentSession.status = 'completed'
+      mockCurrentSession.serverTime = Date.now()
+    }
+    return mockResponse({ success: true })
+  }
+
+  return request({
+    url: '/app-api/buffet/session/complete',
+    method: 'post',
+    data: { sessionId }
+  })
+}
+
+/**
+ * 提交放题订单
+ */
+export async function createBuffetOrder(data: CreateBuffetOrderParams) {
+  if (USE_MOCK) {
+    await mockDelay(1000)
+    
+    // 增加订单次数
+    if (mockCurrentSession && mockCurrentSession.id === data.sessionId) {
+      mockCurrentSession.orderCount++
+      mockCurrentSession.serverTime = Date.now()
+    }
+
+    const order = {
+      id: `order_${Date.now()}`,
+      sessionId: data.sessionId,
+      items: data.items,
+      remark: data.remark,
+      status: 'pending',
+      createTime: Date.now()
+    }
+
+    return mockResponse(order)
+  }
+
+  return request({
+    url: '/app-api/buffet/order/create',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 获取放题订单列表
+ */
+export async function getBuffetOrderList(params: { sessionId: string }) {
+  if (USE_MOCK) {
+    await mockDelay()
+    // Mock订单列表
+    const orders = [
+      {
+        id: 'order_1',
+        sessionId: params.sessionId,
+        items: [
+          { dishId: 'dish_1', dishName: '宫保鸡丁', quantity: 2 },
+          { dishId: 'dish_2', dishName: '米饭', quantity: 2 }
+        ],
+        status: 'completed',
+        createTime: Date.now() - 30 * 60 * 1000
+      }
+    ]
+    return mockResponse(orders)
+  }
+
+  return request({
+    url: '/app-api/buffet/order/list',
+    method: 'get',
+    params
+  })
+}
+
+/**
+ * 获取服务器时间(用于同步)
+ */
+export async function getServerTime() {
+  if (USE_MOCK) {
+    await mockDelay(100)
+    return mockResponse({ serverTime: Date.now() })
+  }
+
+  return request({
+    url: '/app-api/buffet/server-time',
+    method: 'get'
+  })
+}
+
+/**
+ * 清除Mock会话(仅用于开发测试)
+ */
+export function clearMockSession() {
+  if (USE_MOCK) {
+    mockCurrentSession = null
+  }
+}

+ 20 - 9
src/components/buffet/BuffetTimer.vue

@@ -78,14 +78,22 @@ const formatTime = (timestamp: number) => {
   return `${hours}:${minutes}`
 }
 
-const handlePause = () => {
-  buffetStore.pauseSession()
-  showNotify({ type: 'warning', message: t('buffet.paused') })
+const handlePause = async () => {
+  try {
+    await buffetStore.pauseSession()
+    showNotify({ type: 'warning', message: t('buffet.paused') })
+  } catch (error) {
+    console.error('暂停失败', error)
+  }
 }
 
-const handleResume = () => {
-  buffetStore.resumeSession()
-  showNotify({ type: 'success', message: t('buffet.resumed') })
+const handleResume = async () => {
+  try {
+    await buffetStore.resumeSession()
+    showNotify({ type: 'success', message: t('buffet.resumed') })
+  } catch (error) {
+    console.error('恢复失败', error)
+  }
 }
 
 const handleComplete = async () => {
@@ -94,10 +102,13 @@ const handleComplete = async () => {
       title: t('buffet.confirmEnd'),
       message: t('buffet.confirmEndMessage')
     })
-    buffetStore.completeSession()
+    await buffetStore.completeSession()
     showNotify({ type: 'success', message: t('buffet.ended') })
-  } catch {
-    // 用户取消
+  } catch (error: any) {
+    // 用户取消确认对话框时,error为undefined
+    if (error) {
+      console.error('结束失败', error)
+    }
   }
 }
 

+ 23 - 0
src/locale/en.json

@@ -137,6 +137,29 @@
       "value": "10"
     }
   },
+  "scan": {
+    "loading": "Getting information...",
+    "retry": "Retry",
+    "shop": "Shop",
+    "tableInfo": "Table {code}",
+    "scanToOrder": "Scan to order, no login required",
+    "dineInMode": "Dine-In Mode",
+    "welcome": "Welcome",
+    "shopDescription": "Delivery · Pickup · Reservation",
+    "shopMode": "Shop Mode",
+    "startOrdering": "Start Ordering",
+    "enterShop": "Enter Shop",
+    "hasAccount": "Have an account?",
+    "login": "Login",
+    "earnPoints": "to earn points",
+    "missingShopParam": "Missing shop parameter",
+    "recognitionFailed": "Recognition failed",
+    "mockData": {
+      "restaurantName": "Sample Restaurant",
+      "address": "Shibuya, Tokyo...",
+      "tableName": "Table {code}"
+    }
+  },
   "order": {
     "title": "Orders",
     "detail": "Order Detail",

+ 23 - 0
src/locale/ja.json

@@ -137,6 +137,29 @@
       "value": "10"
     }
   },
+  "scan": {
+    "loading": "情報を取得中...",
+    "retry": "再試行",
+    "shop": "店舗",
+    "tableInfo": "テーブル {code}",
+    "scanToOrder": "スキャンして注文、ログイン不要",
+    "dineInMode": "店内モード",
+    "welcome": "ようこそ",
+    "shopDescription": "配達 · 店舗受取 · 予約",
+    "shopMode": "店舗モード",
+    "startOrdering": "注文を開始",
+    "enterShop": "店舗に入る",
+    "hasAccount": "アカウントをお持ちですか?",
+    "login": "ログイン",
+    "earnPoints": "ポイントを獲得",
+    "missingShopParam": "店舗パラメータが不足しています",
+    "recognitionFailed": "認識に失敗しました",
+    "mockData": {
+      "restaurantName": "サンプルレストラン",
+      "address": "東京都渋谷区...",
+      "tableName": "テーブル{code}番"
+    }
+  },
   "order": {
     "title": "注文履歴",
     "detail": "注文詳細",

+ 38 - 1
src/locale/zh-Hans.json

@@ -137,6 +137,29 @@
       "value": "10"
     }
   },
+  "scan": {
+    "loading": "正在获取信息...",
+    "retry": "重新尝试",
+    "shop": "店铺",
+    "tableInfo": "桌位 {code}",
+    "scanToOrder": "扫码点餐,无需登录",
+    "dineInMode": "堂食模式",
+    "welcome": "欢迎光临",
+    "shopDescription": "外卖配送 · 到店自提 · 预约订座",
+    "shopMode": "店铺模式",
+    "startOrdering": "开始点餐",
+    "enterShop": "进入店铺",
+    "hasAccount": "已有账号?",
+    "login": "登录",
+    "earnPoints": "可获得积分",
+    "missingShopParam": "缺少店铺参数",
+    "recognitionFailed": "识别失败",
+    "mockData": {
+      "restaurantName": "示例餐厅",
+      "address": "东京都渋谷区...",
+      "tableName": "{code}号桌"
+    }
+  },
   "order": {
     "title": "订单",
     "detail": "订单详情",
@@ -518,11 +541,25 @@
       }
     },
     "remaining": "剩余时间",
+    "plan": "方案",
+    "table": "桌位",
+    "orderCount": "已点单次数",
+    "startTime": "开始时间",
+    "endTime": "结束时间",
     "pause": "暂停",
     "resume": "继续",
     "endEarly": "提前结束",
+    "confirmEnd": "确认结束",
+    "confirmEndMessage": "确定要提前结束放题吗?",
+    "paused": "已暂停",
+    "resumed": "已继续",
+    "ended": "放题已结束",
+    "reminder10min": "放题还剩10分钟,请抓紧时间!",
+    "reminder5min": "放题还剩5分钟!",
     "timeUp": "放题时间已到!感谢光临",
-    "orderSuccess": "点餐成功!"
+    "orderSuccess": "点餐成功!",
+    "orderFailed": "点餐失败,请重试",
+    "startFailed": "开始放题失败,请重试"
   },
   "role": {
     "customer": "顾客",

+ 23 - 0
src/locale/zh-Hant.json

@@ -137,6 +137,29 @@
       "value": "10"
     }
   },
+  "scan": {
+    "loading": "正在獲取資訊...",
+    "retry": "重新嘗試",
+    "shop": "店鋪",
+    "tableInfo": "桌位 {code}",
+    "scanToOrder": "掃碼點餐,無需登錄",
+    "dineInMode": "堂食模式",
+    "welcome": "歡迎光臨",
+    "shopDescription": "外賣配送 · 到店自提 · 預約訂座",
+    "shopMode": "店鋪模式",
+    "startOrdering": "開始點餐",
+    "enterShop": "進入店鋪",
+    "hasAccount": "已有帳號?",
+    "login": "登錄",
+    "earnPoints": "可獲得積分",
+    "missingShopParam": "缺少店鋪參數",
+    "recognitionFailed": "識別失敗",
+    "mockData": {
+      "restaurantName": "示例餐廳",
+      "address": "東京都澀谷區...",
+      "tableName": "{code}號桌"
+    }
+  },
   "order": {
     "title": "訂單",
     "detail": "訂單詳情",

+ 93 - 52
src/store/modules/buffet.ts

@@ -1,4 +1,13 @@
 import { defineStore } from 'pinia'
+import {
+  getBuffetPlans,
+  createBuffetSession,
+  getCurrentSession,
+  pauseBuffetSession as pauseSessionAPI,
+  resumeBuffetSession as resumeSessionAPI,
+  completeBuffetSession as completeSessionAPI
+} from '@/api/buffet'
+import type { BuffetPlan as APIBuffetPlan, BuffetSession as APIBuffetSession } from '@/api/buffet'
 
 /**
  * 放题方案
@@ -101,54 +110,45 @@ export const useBuffetStore = defineStore('buffet', {
      * 加载放题方案列表
      */
     async loadPlans() {
-      // TODO: 调用API
-      this.plans = [
-        {
-          id: 'plan_1',
-          name: '90分钟畅饮畅食',
-          duration: 90,
-          price: 2980,
-          description: '90分钟内无限点餐,含酒水',
-          reminderMinutes: [30, 10, 5],
-          status: 'active'
-        },
-        {
-          id: 'plan_2',
-          name: '120分钟豪华套餐',
-          duration: 120,
-          price: 3980,
-          description: '120分钟豪华放题',
-          reminderMinutes: [60, 30, 10],
-          status: 'active'
-        }
-      ]
+      try {
+        const res = await getBuffetPlans()
+        this.plans = res.data || []
+      } catch (error) {
+        console.error('加载放题方案失败', error)
+        throw error
+      }
     },
 
     /**
      * 开始放题会话
      */
-    startSession(plan: BuffetPlan, tableId: string, tableName: string) {
-      const now = Date.now()
-      const durationMs = plan.duration * 60 * 1000
-
-      this.currentSession = {
-        id: `session_${Date.now()}`,
-        planId: plan.id,
-        planName: plan.name,
-        tableId,
-        tableName,
-        startTime: now,
-        endTime: now + durationMs,
-        duration: plan.duration,
-        remainingTime: plan.duration * 60,
-        status: 'active',
-        orderCount: 0,
-        maxOrders: plan.maxOrders,
-        reminders: []
-      }
+    async startSession(plan: BuffetPlan, tableId: string, tableName: string, guestCount: number = 2) {
+      try {
+        const res = await createBuffetSession({
+          planId: plan.id,
+          tableId,
+          tableName,
+          guestCount
+        })
+
+        const session = res.data
+        // 计算剩余时间(秒)
+        const remainingSeconds = Math.floor((session.endTime - session.serverTime) / 1000)
+
+        this.currentSession = {
+          ...session,
+          remainingTime: remainingSeconds,
+          reminders: []
+        }
 
-      this.showTimer = true
-      this.startTimer()
+        this.showTimer = true
+        this.startTimer()
+
+        return session
+      } catch (error) {
+        console.error('创建放题会话失败', error)
+        throw error
+      }
     },
 
     /**
@@ -196,20 +196,32 @@ export const useBuffetStore = defineStore('buffet', {
     /**
      * 暂停会话
      */
-    pauseSession() {
+    async pauseSession() {
       if (this.currentSession && this.currentSession.status === 'active') {
-        this.currentSession.status = 'paused'
-        this.stopTimer()
+        try {
+          await pauseSessionAPI(this.currentSession.id)
+          this.currentSession.status = 'paused'
+          this.stopTimer()
+        } catch (error) {
+          console.error('暂停会话失败', error)
+          throw error
+        }
       }
     },
 
     /**
      * 恢复会话
      */
-    resumeSession() {
+    async resumeSession() {
       if (this.currentSession && this.currentSession.status === 'paused') {
-        this.currentSession.status = 'active'
-        this.startTimer()
+        try {
+          await resumeSessionAPI(this.currentSession.id)
+          this.currentSession.status = 'active'
+          this.startTimer()
+        } catch (error) {
+          console.error('恢复会话失败', error)
+          throw error
+        }
       }
     },
 
@@ -228,11 +240,17 @@ export const useBuffetStore = defineStore('buffet', {
     /**
      * 完成会话(提前结束)
      */
-    completeSession() {
+    async completeSession() {
       if (this.currentSession) {
-        this.currentSession.status = 'completed'
-        this.stopTimer()
-        this.showTimer = false
+        try {
+          await completeSessionAPI(this.currentSession.id)
+          this.currentSession.status = 'completed'
+          this.stopTimer()
+          this.showTimer = false
+        } catch (error) {
+          console.error('结束会话失败', error)
+          throw error
+        }
       }
     },
 
@@ -253,6 +271,29 @@ export const useBuffetStore = defineStore('buffet', {
       console.log(`放题提醒: 还剩 ${remainingMinutes} 分钟`)
     },
 
+    /**
+     * 同步服务器会话状态
+     */
+    async syncWithServer() {
+      if (!this.currentSession) return
+
+      try {
+        const res = await getCurrentSession()
+        if (res.data) {
+          const session = res.data
+          // 更新会话状态
+          this.currentSession.status = session.status
+          this.currentSession.orderCount = session.orderCount
+          
+          // 重新计算剩余时间(基于服务器时间)
+          const remainingSeconds = Math.floor((session.endTime - session.serverTime) / 1000)
+          this.currentSession.remainingTime = remainingSeconds
+        }
+      } catch (error) {
+        console.error('同步会话失败', error)
+      }
+    },
+
     /**
      * 清除会话
      */

+ 40 - 4
src/views/buffet/menu.vue

@@ -74,6 +74,7 @@ import { useRouter } from 'vue-router'
 import { useI18n } from 'vue-i18n'
 import { showToast, showConfirmDialog } from 'vant'
 import { useBuffetStore } from '@/store/modules/buffet'
+import { createBuffetOrder } from '@/api/buffet'
 import BuffetTimer from '@/components/buffet/BuffetTimer.vue'
 
 const { t } = useI18n()
@@ -131,7 +132,7 @@ const selectDish = (dish: any) => {
 }
 
 const submitOrder = async () => {
-  if (!canOrder.value) {
+  if (!canOrder.value || !session.value) {
     showToast(t('buffet.menu.cannotOrder'))
     return
   }
@@ -146,13 +147,48 @@ const submitOrder = async () => {
       message: t('buffet.menu.confirmOrderMessage', { count: cartCount.value }) + orderInfo
     })
 
-    // TODO: 提交订单到后端
+    // 显示加载状态
+    showToast({
+      type: 'loading',
+      message: t('common.loading'),
+      duration: 0,
+      forbidClick: true
+    })
+
+    // 构建订单项
+    const items = Object.entries(cart.value).map(([dishId, quantity]) => {
+      // 从分类中查找菜品
+      let dish: any = null
+      for (const category of categories.value) {
+        dish = category.dishes.find((d: any) => d.id === dishId)
+        if (dish) break
+      }
+      
+      return {
+        dishId,
+        dishName: dish?.name || '',
+        quantity
+      }
+    })
+
+    // 提交订单到后端
+    await createBuffetOrder({
+      sessionId: session.value.id,
+      items
+    })
+
+    // 更新本地点单次数
     buffetStore.incrementOrderCount()
+    
     showToast(t('buffet.orderSuccess'))
     cart.value = {}
 
-  } catch {
-    // 用户取消
+  } catch (error: any) {
+    // 用户取消确认对话框时,error为undefined
+    if (error) {
+      console.error('提交订单失败', error)
+      showToast(t('buffet.orderFailed'))
+    }
   }
 }
 

+ 16 - 4
src/views/buffet/select.vue

@@ -123,12 +123,20 @@ const confirmSelection = async () => {
       })
     })
 
+    // 显示加载状态
+    showToast({
+      type: 'loading',
+      message: t('common.loading'),
+      duration: 0,
+      forbidClick: true
+    })
+
     // 获取桌位信息(从路由或store)
     const tableId = route.query.tableId as string || 'table_1'
     const tableName = route.query.tableName as string || 'A1'
 
-    // 开始放题会话
-    buffetStore.startSession(selectedPlanData.value, tableId, tableName)
+    // 开始放题会话(传递guestCount参数)
+    await buffetStore.startSession(selectedPlanData.value, tableId, tableName, guestCount.value)
 
     showToast(t('buffet.orderSuccess'))
 
@@ -138,8 +146,12 @@ const confirmSelection = async () => {
       query: { buffet: '1' }
     })
 
-  } catch {
-    // 用户取消
+  } catch (error: any) {
+    // 用户取消确认对话框时,error为undefined
+    if (error) {
+      console.error('开始放题失败', error)
+      showToast(t('buffet.startFailed'))
+    }
   }
 }
 

+ 13 - 13
src/views/scan/index.vue

@@ -3,14 +3,14 @@
     <!-- Loading State -->
     <div v-if="loading" class="loading-container">
       <van-loading type="spinner" size="48" color="#1989fa" />
-      <p class="loading-text">正在获取信息...</p>
+      <p class="loading-text">{{ $t('scan.loading') }}</p>
     </div>
 
     <!-- Error State -->
     <div v-else-if="error" class="error-container">
       <van-empty image="error" :description="errorMessage">
         <van-button round type="primary" @click="retry">
-          重新尝试
+          {{ $t('scan.retry') }}
         </van-button>
       </van-empty>
     </div>
@@ -30,7 +30,7 @@
           <van-icon name="shop-o" size="32" />
         </div>
         <div class="shop-detail">
-          <h2>{{ shopInfo?.name || '店铺' }}</h2>
+          <h2>{{ shopInfo?.name || $t('scan.shop') }}</h2>
           <p v-if="shopInfo?.address">{{ shopInfo.address }}</p>
         </div>
       </div>
@@ -38,16 +38,16 @@
       <!-- Mode Display -->
       <div v-if="mode === 'table'" class="table-card table-mode">
         <van-icon name="scan" size="48" color="#1989fa" />
-        <h3>{{ tableInfo?.tableName || `桌位 ${tableInfo?.code}` }}</h3>
-        <p class="hint">扫码点餐,无需登录</p>
-        <van-tag type="primary" size="medium" class="mode-tag">堂食模式</van-tag>
+        <h3>{{ tableInfo?.tableName || $t('scan.tableInfo', { code: tableInfo?.code }) }}</h3>
+        <p class="hint">{{ $t('scan.scanToOrder') }}</p>
+        <van-tag type="primary" size="medium" class="mode-tag">{{ $t('scan.dineInMode') }}</van-tag>
       </div>
 
       <div v-else class="table-card shop-mode">
         <van-icon name="bag-o" size="48" color="#ff976a" />
-        <h3>欢迎光临</h3>
-        <p class="hint">外卖配送 · 到店自提 · 预约订座</p>
-        <van-tag type="warning" size="medium" class="mode-tag">店铺模式</van-tag>
+        <h3>{{ $t('scan.welcome') }}</h3>
+        <p class="hint">{{ $t('scan.shopDescription') }}</p>
+        <van-tag type="warning" size="medium" class="mode-tag">{{ $t('scan.shopMode') }}</van-tag>
       </div>
 
       <van-button
@@ -58,13 +58,13 @@
         class="start-btn"
         @click="startOrdering"
       >
-        {{ mode === 'table' ? '开始点餐' : '进入店铺' }}
+        {{ mode === 'table' ? $t('scan.startOrdering') : $t('scan.enterShop') }}
       </van-button>
 
       <p class="login-hint">
-        已有账号?
-        <span class="link" @click="goToLogin">登录</span>
-        可获得积分
+        {{ $t('scan.hasAccount') }}
+        <span class="link" @click="goToLogin">{{ $t('scan.login') }}</span>
+        {{ $t('scan.earnPoints') }}
       </p>
     </div>
   </div>