Browse Source

feat: Implement merchant product management (list, create, edit) and add POS checkout functionality, including new routes, store logic, and localization updates.

FanLide 1 tuần trước cách đây
mục cha
commit
39c47a7197

+ 0 - 0
doc/CORE_PROGRESS.md → CORE_PROGRESS.md


+ 0 - 0
doc/UI_DESIGN_BRIEF.md → UI_DESIGN_BRIEF.md


+ 56 - 1
src/locale/en.json

@@ -323,6 +323,47 @@
         "kungPao": "Kung Pao Chicken",
         "rice": "Rice"
       }
+    },
+    "product": {
+      "title": "Product Management",
+      "createTitle": "Create Product",
+      "editTitle": "Edit Product",
+      "add": "Add",
+      "onShelf": "On Shelf",
+      "offShelf": "Off Shelf",
+      "confirmDelete": "Confirm Delete",
+      "confirmDeleteMessage": "Are you sure you want to delete \"{name}\"?",
+      "deleteSuccess": "Deleted Successfully",
+      "updateSuccess": "Updated Successfully",
+      "createSuccess": "Created Successfully",
+      "saveFailed": "Save Failed",
+      "form": {
+        "name": "Product Name",
+        "namePlaceholder": "Enter product name",
+        "nameRequired": "Product name is required",
+        "price": "Price",
+        "pricePlaceholder": "Enter price",
+        "priceRequired": "Price is required",
+        "currency": "JPY",
+        "category": "Category",
+        "categoryPlaceholder": "Select category",
+        "categoryRequired": "Category is required",
+        "spec": "Specification",
+        "specPlaceholder": "e.g., Large/Medium (Optional)",
+        "status": "Status",
+        "description": "Description",
+        "descriptionPlaceholder": "Enter product description",
+        "image": "Product Image",
+        "uploadMock": "Click to Upload (Mock)",
+        "save": "Save",
+        "delete": "Delete"
+      }
+    },
+    "menu": {
+      "products": "Product Management",
+      "buffetPlans": "Buffet Plans",
+      "settings": "Shop Settings",
+      "commonFunctions": "Common Functions"
     }
   },
   "admin": {
@@ -402,7 +443,8 @@
         "clearSuccess": "Table cleared",
         "gotoCheckout": "Go to checkout",
         "addTable": "Add Table"
-      }
+      },
+      "goToCheckout": "Checkout"
     },
     "orders": {
       "title": "Order Management",
@@ -419,6 +461,19 @@
         "accepted": "Accepted",
         "completed": "Completed"
       }
+    },
+    "checkout": {
+      "title": "Checkout",
+      "amountDue": "Amount Due",
+      "orderNo": "Order No: {no}",
+      "paymentMethod": "Select Payment Method",
+      "methods": {
+        "cash": "Cash",
+        "linePay": "LINE Pay",
+        "card": "Credit Card"
+      },
+      "confirm": "Confirm Payment ¥{amount}",
+      "success": "Payment Success"
     }
   },
   "owner": {

+ 56 - 1
src/locale/ja.json

@@ -323,6 +323,47 @@
         "kungPao": "宮保鶏丁",
         "rice": "ご飯"
       }
+    },
+    "product": {
+      "title": "メニュー管理",
+      "createTitle": "新規商品",
+      "editTitle": "商品編集",
+      "add": "追加",
+      "onShelf": "販売中",
+      "offShelf": "停止中",
+      "confirmDelete": "削除確認",
+      "confirmDeleteMessage": "「{name}」を削除してもよろしいですか?",
+      "deleteSuccess": "削除しました",
+      "updateSuccess": "更新しました",
+      "createSuccess": "作成しました",
+      "saveFailed": "保存に失敗しました",
+      "form": {
+        "name": "商品名",
+        "namePlaceholder": "商品名を入力",
+        "nameRequired": "商品名は必須です",
+        "price": "価格",
+        "pricePlaceholder": "価格を入力",
+        "priceRequired": "価格は必須です",
+        "currency": "円",
+        "category": "カテゴリー",
+        "categoryPlaceholder": "カテゴリーを選択",
+        "categoryRequired": "カテゴリーは必須です",
+        "spec": "規格",
+        "specPlaceholder": "例:大盛/中盛(任意)",
+        "status": "販売状態",
+        "description": "説明",
+        "descriptionPlaceholder": "商品の説明を入力",
+        "image": "商品画像",
+        "uploadMock": "クリックしてアップロード (Mock)",
+        "save": "保存",
+        "delete": "削除"
+      }
+    },
+    "menu": {
+      "products": "メニュー管理",
+      "buffetPlans": "食べ放題プラン",
+      "settings": "店舗設定",
+      "commonFunctions": "よく使う機能"
     }
   },
   "admin": {
@@ -402,7 +443,8 @@
         "clearSuccess": "清席が完了しました",
         "gotoCheckout": "会計画面へ移動",
         "addTable": "テーブル追加"
-      }
+      },
+      "goToCheckout": "お会計"
     },
     "orders": {
       "title": "注文管理",
@@ -419,6 +461,19 @@
         "accepted": "受注しました",
         "completed": "注文完了"
       }
+    },
+    "checkout": {
+      "title": "レジ",
+      "amountDue": "請求金額",
+      "orderNo": "注文番号: {no}",
+      "paymentMethod": "支払方法を選択",
+      "methods": {
+        "cash": "現金払い",
+        "linePay": "LINE Pay",
+        "card": "クレジットカード"
+      },
+      "confirm": "支払う ¥{amount}",
+      "success": "支払完了"
     }
   },
   "owner": {

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

@@ -323,6 +323,47 @@
         "kungPao": "宫保鸡丁",
         "rice": "米饭"
       }
+    },
+    "product": {
+      "title": "菜品管理",
+      "createTitle": "新增菜品",
+      "editTitle": "编辑菜品",
+      "add": "新增",
+      "onShelf": "上架中",
+      "offShelf": "已下架",
+      "confirmDelete": "确认删除",
+      "confirmDeleteMessage": "确定要删除菜品\"{name}\"吗?",
+      "deleteSuccess": "删除成功",
+      "updateSuccess": "更新成功",
+      "createSuccess": "创建成功",
+      "saveFailed": "保存失败",
+      "form": {
+        "name": "菜品名称",
+        "namePlaceholder": "请输入菜品名称",
+        "nameRequired": "请填写菜品名称",
+        "price": "价格",
+        "pricePlaceholder": "请输入价格",
+        "priceRequired": "请填写价格",
+        "currency": "日元",
+        "category": "分类",
+        "categoryPlaceholder": "点击选择分类",
+        "categoryRequired": "请选择分类",
+        "spec": "规格",
+        "specPlaceholder": "例如:大碗/中碗(选填)",
+        "status": "上架状态",
+        "description": "描述",
+        "descriptionPlaceholder": "请输入菜品描述",
+        "image": "菜品图片",
+        "uploadMock": "点击上传 (Mock)",
+        "save": "保存",
+        "delete": "删除"
+      }
+    },
+    "menu": {
+      "products": "菜品管理",
+      "buffetPlans": "放题方案",
+      "settings": "店铺设置",
+      "commonFunctions": "常用功能"
     }
   },
   "admin": {
@@ -402,7 +443,8 @@
         "clearSuccess": "清台完成",
         "gotoCheckout": "跳转结账页面",
         "addTable": "添加桌位"
-      }
+      },
+      "goToCheckout": "去结账"
     },
     "orders": {
       "title": "订单管理",
@@ -419,6 +461,19 @@
         "accepted": "已接单",
         "completed": "订单完成"
       }
+    },
+    "checkout": {
+      "title": "收银台",
+      "amountDue": "应收金额",
+      "orderNo": "订单号: {no}",
+      "paymentMethod": "选择支付方式",
+      "methods": {
+        "cash": "现金支付",
+        "linePay": "LINE Pay",
+        "card": "银行卡"
+      },
+      "confirm": "确认收款 ¥{amount}",
+      "success": "收款成功"
     }
   },
   "owner": {

+ 56 - 1
src/locale/zh-Hant.json

@@ -323,6 +323,47 @@
         "kungPao": "宮保雞丁",
         "rice": "米飯"
       }
+    },
+    "product": {
+      "title": "菜品管理",
+      "createTitle": "新增菜品",
+      "editTitle": "編輯菜品",
+      "add": "新增",
+      "onShelf": "上架中",
+      "offShelf": "已下架",
+      "confirmDelete": "確認刪除",
+      "confirmDeleteMessage": "確定要刪除菜品\"{name}\"嗎?",
+      "deleteSuccess": "刪除成功",
+      "updateSuccess": "更新成功",
+      "createSuccess": "創建成功",
+      "saveFailed": "保存失敗",
+      "form": {
+        "name": "菜品名稱",
+        "namePlaceholder": "請輸入菜品名稱",
+        "nameRequired": "請填寫菜品名稱",
+        "price": "價格",
+        "pricePlaceholder": "請輸入價格",
+        "priceRequired": "請填寫價格",
+        "currency": "日元",
+        "category": "分類",
+        "categoryPlaceholder": "點擊選擇分類",
+        "categoryRequired": "請選擇分類",
+        "spec": "規格",
+        "specPlaceholder": "例如:大碗/中碗(選填)",
+        "status": "上架狀態",
+        "description": "描述",
+        "descriptionPlaceholder": "請輸入菜品描述",
+        "image": "菜品圖片",
+        "uploadMock": "點擊上傳 (Mock)",
+        "save": "保存",
+        "delete": "刪除"
+      }
+    },
+    "menu": {
+      "products": "菜品管理",
+      "buffetPlans": "放題方案",
+      "settings": "店舖設置",
+      "commonFunctions": "常用功能"
     }
   },
   "admin": {
@@ -402,7 +443,8 @@
         "clearSuccess": "清台完成",
         "gotoCheckout": "跳轉結賬頁面",
         "addTable": "添加桌位"
-      }
+      },
+      "goToCheckout": "去結賬"
     },
     "orders": {
       "title": "訂單管理",
@@ -419,6 +461,19 @@
         "accepted": "已接單",
         "completed": "訂單完成"
       }
+    },
+    "checkout": {
+      "title": "收銀台",
+      "amountDue": "應收金額",
+      "orderNo": "訂單號: {no}",
+      "paymentMethod": "選擇支付方式",
+      "methods": {
+        "cash": "現金支付",
+        "linePay": "LINE Pay",
+        "card": "銀行卡"
+      },
+      "confirm": "確認收款 ¥{amount}",
+      "success": "收款成功"
     }
   },
   "owner": {

+ 71 - 1
src/router/routes.ts

@@ -195,7 +195,35 @@ export default [
     name: 'MerchantBuffetPlans',
     component: () => import('@/views/merchant/buffet-plans.vue'),
     meta: {
-      title: '放题方案管理',
+      requiresRole: ['owner', 'manager']
+    }
+  },
+  {
+    path: '/merchant/products',
+    name: 'MerchantProducts',
+    component: () => import('@/views/merchant/products/list.vue'),
+    meta: {
+      title: '菜品管理',
+      requiresAuth: true,
+      requiresRole: ['owner', 'manager']
+    }
+  },
+  {
+    path: '/merchant/products/create',
+    name: 'MerchantProductCreate',
+    component: () => import('@/views/merchant/products/edit.vue'),
+    meta: {
+      title: '新增菜品',
+      requiresAuth: true,
+      requiresRole: ['owner', 'manager']
+    }
+  },
+  {
+    path: '/merchant/products/:id/edit',
+    name: 'MerchantProductEdit',
+    component: () => import('@/views/merchant/products/edit.vue'),
+    meta: {
+      title: '编辑菜品',
       requiresAuth: true,
       requiresRole: ['owner', 'manager']
     }
@@ -242,7 +270,49 @@ export default [
       requiresRole: ['staff', 'manager', 'owner', 'admin']
     }
   },
+  // Pos Checkout Route
+  {
+    path: '/pos/checkout/:id',
+    name: 'PosCheckout',
+    component: () => import('@/views/pos/checkout.vue'),
+    meta: {
+      title: 'pos.checkout.title',
+      requiresAuth: true,
+      requiresRole: ['staff', 'manager', 'owner', 'admin']
+    }
+  },
 
+  // Merchant Product Routes
+  {
+    path: '/merchant/products',
+    name: 'MerchantProducts',
+    component: () => import('@/views/merchant/products/list.vue'),
+    meta: {
+      title: 'merchant.product.title',
+      requiresAuth: true,
+      requiresRole: ['owner', 'manager']
+    }
+  },
+  {
+    path: '/merchant/products/create',
+    name: 'MerchantProductCreate',
+    component: () => import('@/views/merchant/products/edit.vue'),
+    meta: {
+      title: 'merchant.product.createTitle',
+      requiresAuth: true,
+      requiresRole: ['owner', 'manager']
+    }
+  },
+  {
+    path: '/merchant/products/:id/edit',
+    name: 'MerchantProductEdit',
+    component: () => import('@/views/merchant/products/edit.vue'),
+    meta: {
+      title: 'merchant.product.editTitle',
+      requiresAuth: true,
+      requiresRole: ['owner', 'manager']
+    }
+  },
   // 放题路由
   {
     path: '/buffet/select',

+ 53 - 3
src/store/modules/merchant.ts

@@ -20,7 +20,8 @@ export const useMerchantStore = defineStore('merchant', {
       completed: 0
     },
     pendingOrderCount: 0,
-    orders: []
+    orders: [] as any[],
+    products: [] as any[]
   }),
 
   getters: {
@@ -65,15 +66,64 @@ export const useMerchantStore = defineStore('merchant', {
         }
       ]
       return { list: this.orders, total: 1 }
+    },
+
+    async getProducts() {
+      // Mock Data
+      if (!this.products || this.products.length === 0) {
+        this.products = [
+          {
+            id: '1',
+            name: '招牌拉面',
+            price: 38,
+            category: '面食',
+            description: '这是招牌拉面',
+            image: '',
+            status: true,
+            spec: '大碗'
+          },
+          {
+            id: '2',
+            name: '炸猪排',
+            price: 28,
+            category: '小吃',
+            description: '酥脆炸猪排',
+            image: '',
+            status: true,
+            spec: ''
+          }
+        ]
+      }
+      return this.products
+    },
+
+    async addProduct(product: any) {
+      const newProduct = {
+        ...product,
+        id: Date.now().toString(),
+        status: true
+      }
+      this.products.unshift(newProduct)
+      return newProduct
+    },
+
+    async updateProduct(id: string, product: any) {
+      const index = this.products.findIndex((p: any) => p.id === id)
+      if (index > -1) {
+        this.products[index] = { ...this.products[index], ...product }
+      }
+    },
+
+    async deleteProduct(id: string) {
+      this.products = this.products.filter((p: any) => p.id !== id)
     }
   },
 
   persist: {
-    enabled: true,
     strategies: [
       {
         storage: localStorage,
-        paths: ['shopInfo']
+        paths: ['shopInfo', 'products']
       }
     ]
   }

+ 16 - 1
src/views/merchant/dashboard.vue

@@ -70,6 +70,16 @@
             </div>
           </div>
         </div>
+
+        <!-- 常用功能 -->
+        <div class="menu-section">
+          <h4>{{ $t('merchant.menu.commonFunctions') }}</h4>
+          <van-grid :column-num="3" clickable>
+             <van-grid-item icon="apps-o" :text="$t('merchant.menu.products')" @click="goToProducts" />
+             <van-grid-item icon="label-o" :text="$t('merchant.menu.buffetPlans')" to="/merchant/buffet-plans" />
+             <van-grid-item icon="setting-o" :text="$t('merchant.menu.settings')" />
+          </van-grid>
+        </div>
       </div>
     </van-pull-refresh>
   </div>
@@ -114,6 +124,10 @@ const goToOrders = (status: string) => {
   showToast(t('merchant.dashboard.orderManagementInDev'))
 }
 
+const goToProducts = () => {
+  router.push('/merchant/products')
+}
+
 onMounted(() => {
   loadData()
 })
@@ -149,7 +163,8 @@ onMounted(() => {
 }
 
 .stats-section,
-.order-stats-section {
+.order-stats-section,
+.menu-section {
   background: #fff;
   border-radius: 12px;
   padding: 15px;

+ 219 - 0
src/views/merchant/products/edit.vue

@@ -0,0 +1,219 @@
+<template>
+  <div class="product-edit">
+    <van-nav-bar
+      :title="isEdit ? $t('merchant.product.editTitle') : $t('merchant.product.createTitle')"
+      left-arrow
+      @click-left="$router.back()"
+      fixed
+      placeholder
+    />
+
+    <van-form @submit="onSubmit">
+      <van-cell-group inset class="mt-2">
+        <van-field
+          v-model="form.name"
+          name="name"
+          :label="$t('merchant.product.form.name')"
+          :placeholder="$t('merchant.product.form.namePlaceholder')"
+          :rules="[{ required: true, message: $t('merchant.product.form.nameRequired') }]"
+        />
+        <van-field
+          v-model="form.price"
+          type="number"
+          name="price"
+          :label="$t('merchant.product.form.price')"
+          :placeholder="$t('merchant.product.form.pricePlaceholder')"
+          :rules="[{ required: true, message: $t('merchant.product.form.priceRequired') }]"
+        >
+          <template #extra>{{ $t('merchant.product.form.currency') }}</template>
+        </van-field>
+        <van-field
+          v-model="form.category"
+          is-link
+          readonly
+          name="category"
+          :label="$t('merchant.product.form.category')"
+          :placeholder="$t('merchant.product.form.categoryPlaceholder')"
+          @click="showCategoryPicker = true"
+          :rules="[{ required: true, message: $t('merchant.product.form.categoryRequired') }]"
+        />
+        <van-popup v-model:show="showCategoryPicker" position="bottom">
+          <van-picker
+            :columns="categories"
+            @confirm="onConfirmCategory"
+            @cancel="showCategoryPicker = false"
+          />
+        </van-popup>
+        
+        <van-field
+          v-model="form.spec"
+          name="spec"
+          :label="$t('merchant.product.form.spec')"
+          :placeholder="$t('merchant.product.form.specPlaceholder')"
+        />
+
+        <van-field name="status" :label="$t('merchant.product.form.status')">
+          <template #input>
+            <van-switch v-model="form.status" />
+          </template>
+        </van-field>
+
+        <van-field
+          v-model="form.description"
+          rows="3"
+          autosize
+          :label="$t('merchant.product.form.description')"
+          type="textarea"
+          :placeholder="$t('merchant.product.form.descriptionPlaceholder')"
+        />
+      </van-cell-group>
+
+      <!-- 图片上传 (Mock) -->
+      <van-cell-group inset class="mt-2">
+        <van-cell :title="$t('merchant.product.form.image')">
+          <template #label>
+             <div class="upload-mock">
+               <van-icon name="photograph" size="30" color="#dcdee0" />
+               <span>{{ $t('merchant.product.form.uploadMock') }}</span>
+             </div>
+          </template>
+        </van-cell>
+      </van-cell-group>
+
+      <div class="submit-btn">
+        <van-button round block type="primary" native-type="submit" :loading="loading">
+          {{ $t('merchant.product.form.save') }}
+        </van-button>
+        <van-button 
+          v-if="isEdit" 
+          round 
+          block 
+          type="danger" 
+          class="mt-2" 
+          @click="handleDelete"
+        >
+          {{ $t('merchant.product.form.delete') }}
+        </van-button>
+      </div>
+    </van-form>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, computed } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { showToast, showDialog } from 'vant'
+import { useMerchantStore } from '@/store/modules/merchant'
+import { useI18n } from 'vue-i18n'
+
+const { t } = useI18n()
+const route = useRoute()
+const router = useRouter()
+const merchantStore = useMerchantStore()
+
+const loading = ref(false)
+const showCategoryPicker = ref(false)
+const categories = [
+  { text: '面食', value: '面食' },
+  { text: '饭类', value: '饭类' },
+  { text: '小吃', value: '小吃' },
+  { text: '饮料', value: '饮料' }
+]
+
+const form = ref({
+  id: '',
+  name: '',
+  price: '',
+  category: '',
+  description: '',
+  status: true,
+  spec: '',
+  image: ''
+})
+
+const isEdit = computed(() => !!route.params.id)
+
+onMounted(async () => {
+  if (isEdit.value) {
+    const products = await merchantStore.getProducts()
+    const product = products.find((p: any) => p.id === route.params.id)
+    if (product) {
+      form.value = { ...product }
+    } else {
+      showToast(t('common.error')) // Using common error as placeholder for "product not found" if not strictly defined
+      router.back()
+    }
+  }
+})
+
+const onConfirmCategory = ({ selectedOptions }: any) => {
+  form.value.category = selectedOptions[0].text
+  showCategoryPicker.value = false
+}
+
+const onSubmit = async () => {
+  loading.value = true
+  try {
+    const data = {
+      ...form.value,
+      price: Number(form.value.price)
+    }
+    
+    if (isEdit.value) {
+      await merchantStore.updateProduct(form.value.id, data)
+      showToast(t('merchant.product.updateSuccess'))
+    } else {
+      await merchantStore.addProduct(data)
+      showToast(t('merchant.product.createSuccess'))
+    }
+    router.back()
+  } catch (error) {
+    showToast(t('merchant.product.saveFailed'))
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleDelete = () => {
+  showDialog({
+    title: t('merchant.product.confirmDelete'),
+    message: t('merchant.product.confirmDeleteMessage', { name: form.value.name }),
+    showCancelButton: true
+  }).then(async (action) => {
+    if (action === 'confirm') {
+      await merchantStore.deleteProduct(form.value.id)
+      showToast(t('merchant.product.deleteSuccess'))
+      router.back()
+    }
+  })
+}
+</script>
+
+<style scoped>
+.product-edit {
+  min-height: 100vh;
+  background: #f5f5f5;
+  padding-bottom: 20px;
+}
+
+.mt-2 {
+  margin-top: 12px;
+}
+
+.submit-btn {
+  margin: 24px 16px;
+}
+
+.upload-mock {
+  width: 80px;
+  height: 80px;
+  background: #f7f8fa;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  border-radius: 8px;
+  font-size: 10px;
+  color: #999;
+}
+</style>

+ 130 - 0
src/views/merchant/products/list.vue

@@ -0,0 +1,130 @@
+<template>
+  <div class="merchant-products">
+    <van-nav-bar
+      :title="$t('merchant.product.title')"
+      left-arrow
+      :right-text="$t('merchant.product.add')"
+      @click-left="$router.back()"
+      @click-right="goToCreate"
+      fixed
+      placeholder
+    />
+
+    <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
+      <van-list
+        v-model:loading="loading"
+        :finished="finished"
+        :finished-text="$t('menu.noMore')"
+        @load="onLoad"
+      >
+        <div class="product-list">
+          <van-swipe-cell v-for="item in list" :key="item.id">
+            <van-card
+              :price="item.price.toFixed(2)"
+              :desc="item.description"
+              :title="item.name"
+              :thumb="item.image || 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg'"
+              @click="goToEdit(item)"
+            >
+              <template #tags>
+                <van-tag plain type="primary" class="mr-1">{{ item.category }}</van-tag>
+                <van-tag :type="item.status ? 'success' : 'danger'">
+                  {{ item.status ? $t('merchant.product.onShelf') : $t('merchant.product.offShelf') }}
+                </van-tag>
+              </template>
+            </van-card>
+            <template #right>
+              <van-button square :text="$t('common.edit')" type="primary" class="action-btn" @click="goToEdit(item)" />
+              <van-button square :text="$t('merchant.product.form.delete')" type="danger" class="action-btn" @click="confirmDelete(item)" />
+            </template>
+          </van-swipe-cell>
+        </div>
+      </van-list>
+    </van-pull-refresh>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { showToast, showDialog } from 'vant'
+import { useMerchantStore } from '@/store/modules/merchant'
+import { useI18n } from 'vue-i18n'
+
+const { t } = useI18n()
+const router = useRouter()
+const merchantStore = useMerchantStore()
+
+const list = ref<any[]>([])
+const loading = ref(false)
+const finished = ref(false)
+const refreshing = ref(false)
+
+const loadData = async () => {
+  try {
+    const products = await merchantStore.getProducts()
+    list.value = products
+    finished.value = true
+  } catch (error) {
+    showToast(t('merchant.dashboard.loadFailed'))
+  } finally {
+    loading.value = false
+    refreshing.value = false
+  }
+}
+
+const onLoad = () => {
+  loadData()
+}
+
+const onRefresh = () => {
+  finished.value = false
+  loading.value = true
+  onLoad()
+}
+
+const goToCreate = () => {
+  router.push('/merchant/products/create')
+}
+
+const goToEdit = (item: any) => {
+  router.push(`/merchant/products/${item.id}/edit`)
+}
+
+const confirmDelete = (item: any) => {
+  showDialog({
+    title: t('merchant.product.confirmDelete'),
+    message: t('merchant.product.confirmDeleteMessage', { name: item.name }),
+    showCancelButton: true
+  }).then(async (action) => {
+    if (action === 'confirm') {
+      await merchantStore.deleteProduct(item.id)
+      showToast(t('merchant.product.deleteSuccess'))
+      onRefresh()
+    }
+  })
+}
+
+onMounted(() => {
+  onLoad()
+})
+</script>
+
+<style scoped>
+.merchant-products {
+  min-height: 100vh;
+  background: #f5f5f5;
+}
+
+.product-list {
+  padding-top: 10px;
+}
+
+.action-btn {
+  height: 100%;
+}
+
+.mr-1 {
+  margin-right: 4px;
+}
+</style>

+ 157 - 0
src/views/pos/checkout.vue

@@ -0,0 +1,157 @@
+<template>
+  <div class="pos-checkout">
+    <van-nav-bar :title="$t('pos.checkout.title')" left-arrow @click-left="$router.back()" fixed placeholder />
+
+    <div class="checkout-content">
+      <!-- 订单金额 -->
+      <div class="amount-card">
+        <div class="label">{{ $t('pos.checkout.amountDue') }}</div>
+        <div class="amount">¥{{ order.amount }}</div>
+        <div class="order-info">{{ $t('pos.checkout.orderNo', { no: order.orderNo }) }}</div>
+      </div>
+
+      <!-- 支付方式 -->
+      <div class="payment-methods">
+        <div class="section-title">{{ $t('pos.checkout.paymentMethod') }}</div>
+        <van-radio-group v-model="paymentMethod">
+          <van-cell-group inset>
+            <van-cell :title="$t('pos.checkout.methods.cash')" clickable @click="paymentMethod = 'cash'">
+              <template #icon>
+                <van-icon name="gold-coin" color="#ff976a" class="method-icon" />
+              </template>
+              <template #right-icon>
+                <van-radio name="cash" />
+              </template>
+            </van-cell>
+            <van-cell :title="$t('pos.checkout.methods.linePay')" clickable @click="paymentMethod = 'line'">
+              <template #icon>
+                <van-icon name="wechat-pay" color="#07c160" class="method-icon" />
+              </template>
+              <template #right-icon>
+                <van-radio name="line" />
+              </template>
+            </van-cell>
+            <van-cell :title="$t('pos.checkout.methods.card')" clickable @click="paymentMethod = 'card'">
+              <template #icon>
+                <van-icon name="card" color="#1989fa" class="method-icon" />
+              </template>
+              <template #right-icon>
+                <van-radio name="card" />
+              </template>
+            </van-cell>
+          </van-cell-group>
+        </van-radio-group>
+      </div>
+
+      <!-- 支付按钮 -->
+      <div class="action-area">
+        <van-button type="primary" block round size="large" @click="handleCheckout" :loading="loading">
+          {{ $t('pos.checkout.confirm', { amount: order.amount }) }}
+        </van-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { showToast, showSuccessToast } from 'vant'
+import { useI18n } from 'vue-i18n'
+
+const { t } = useI18n()
+const route = useRoute()
+const router = useRouter()
+const loading = ref(false)
+const paymentMethod = ref('cash')
+
+// 模拟订单数据
+const order = ref({
+  id: '',
+  orderNo: '',
+  amount: 0
+})
+
+onMounted(() => {
+  const orderId = route.params.id as string
+  // 实际项目中应根据 ID 获取订单详情
+  // 这里使用 Mock 数据
+  order.value = {
+    id: orderId,
+    orderNo: '2024011201',
+    amount: 158
+  }
+})
+
+const handleCheckout = async () => {
+  loading.value = true
+  
+  // 模拟支付过程
+  setTimeout(() => {
+    loading.value = false
+    showSuccessToast(t('pos.checkout.success'))
+    
+    // 支付成功后返回订单列表或详情
+    // 实际项目中应更新订单状态
+    setTimeout(() => {
+      router.push('/pos/orders')
+    }, 1500)
+  }, 1000)
+}
+</script>
+
+<style scoped>
+.pos-checkout {
+  min-height: 100vh;
+  background: #f5f5f5;
+}
+
+.checkout-content {
+  padding: 16px;
+}
+
+.amount-card {
+  background: #fff;
+  border-radius: 12px;
+  padding: 30px 20px;
+  text-align: center;
+  margin-bottom: 20px;
+}
+
+.label {
+  font-size: 14px;
+  color: #666;
+  margin-bottom: 10px;
+}
+
+.amount {
+  font-size: 36px;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 10px;
+}
+
+.order-info {
+  font-size: 13px;
+  color: #999;
+}
+
+.section-title {
+  font-size: 14px;
+  color: #666;
+  margin-bottom: 10px;
+  padding-left: 4px;
+}
+
+.method-icon {
+  font-size: 20px;
+  margin-right: 10px;
+  display: flex;
+  align-items: center;
+}
+
+.action-area {
+  margin-top: 40px;
+  padding: 0 16px;
+}
+</style>

+ 17 - 0
src/views/pos/order-detail.vue

@@ -77,6 +77,15 @@
         >
           打印小票
         </van-button>
+        <van-button
+          v-if="order.status === 'completed'"
+          type="primary"
+          block
+          class="mt-2"
+          @click="goToCheckout"
+        >
+          {{ $t('pos.tables.goToCheckout') }}
+        </van-button>
       </div>
     </div>
   </div>
@@ -173,6 +182,10 @@ const printOrder = () => {
   showToast('打印小票')
 }
 
+const goToCheckout = () => {
+  router.push(`/pos/checkout/${order.value.id}`)
+}
+
 onMounted(() => {
   // 加载订单详情
 })
@@ -273,4 +286,8 @@ onMounted(() => {
 .action-buttons {
   padding: 16px 0;
 }
+
+.mt-2 {
+  margin-top: 8px;
+}
 </style>