Sfoglia il codice sorgente

feat: align API endpoints with Swagger and enhance TypeScript definitions

- Updated all API modules (auth, user, goods, order, address, coupon) to use /app-api/ prefix and match official documentation.
- Added strict TypeScript interfaces for all API request parameters.
- Re-enabled Vant auto-import resolver in vite.config.ts.
- Migrated van-goods-action to Vant 4 van-action-bar in detail.vue.
- Added TypeScript interfaces to Pinia cart store and fixed state types.
- Resolved multiple implicit any and type errors in detail.vue.
FanLide 2 settimane fa
parent
commit
d4f792fa2b

+ 26 - 8
src/api/address.ts

@@ -4,22 +4,40 @@
 
 import request from './request'
 
+/**
+ * 获取地址列表
+ */
 /**
  * 获取地址列表
  */
 export function getAddressList() {
   return request({
-    url: '/api/address/list',
+    url: '/app-api/address/list',
     method: 'get'
   })
 }
 
+/**
+ * 地址参数
+ */
+export interface AddressParams {
+  name: string
+  mobile: string
+  province: string
+  city: string
+  district: string
+  detail: string
+  isDefault?: boolean
+  latitude?: number
+  longitude?: number
+}
+
 /**
  * 添加地址
  */
-export function addAddress(data) {
+export function addAddress(data: AddressParams) {
   return request({
-    url: '/api/address/add',
+    url: '/app-api/address/addAndEdit',
     method: 'post',
     data
   })
@@ -28,20 +46,20 @@ export function addAddress(data) {
 /**
  * 更新地址
  */
-export function updateAddress(id, data) {
+export function updateAddress(id: number | string, data: AddressParams) {
   return request({
-    url: `/api/address/update/${id}`,
+    url: '/app-api/address/addAndEdit',
     method: 'post',
-    data
+    data: { ...data, id }
   })
 }
 
 /**
  * 删除地址
  */
-export function deleteAddress(id) {
+export function deleteAddress(id: number | string) {
   return request({
-    url: `/api/address/delete/${id}`,
+    url: `/app-api/address/del/${id}`,
     method: 'post'
   })
 }

+ 31 - 11
src/api/auth.ts

@@ -7,9 +7,21 @@ import request from './request'
 /**
  * 登录
  */
-export function login(data) {
+/**
+ * 登录参数
+ */
+export interface LoginParams {
+  mobile: string
+  password?: string
+  code?: string
+}
+
+/**
+ * 登录
+ */
+export function login(data: LoginParams) {
   return request({
-    url: '/api/auth/login',
+    url: '/app-api/member/auth/login',
     method: 'post',
     data
   })
@@ -20,7 +32,7 @@ export function login(data) {
  */
 export function logout() {
   return request({
-    url: '/api/auth/logout',
+    url: '/app-api/member/auth/logout',
     method: 'post'
   })
 }
@@ -28,31 +40,39 @@ export function logout() {
 /**
  * 微信登录
  */
-export function wechatLogin(code) {
+export function wechatLogin(code: string) {
   return request({
-    url: '/api/auth/wechat/login',
-    method: 'post',
-    data: { code }
+    url: '/app-api/member/auth/auth-wechat-login',
+    method: 'get',
+    params: { code }
   })
 }
 
 /**
  * LINE登录
  */
-export function lineLogin(token) {
+export function lineLogin(token: string) {
   return request({
-    url: '/api/auth/line/login',
+    url: '/app-api/member/auth/line/login',
     method: 'post',
     data: { token }
   })
 }
 
+/**
+ * 发送验证码参数
+ */
+export interface SmsParams {
+  mobile: string
+  scene: number // 1: 登录, 2: 注册, 3: 忘记密码
+}
+
 /**
  * 发送短信验证码
  */
-export function sendSmsCode(data) {
+export function sendSmsCode(data: SmsParams) {
   return request({
-    url: '/api/sms/send',
+    url: '/app-api/member/auth/send-sms-code',
     method: 'post',
     data
   })

+ 19 - 6
src/api/coupon.ts

@@ -7,9 +7,21 @@ import request from './request'
 /**
  * 获取优惠券列表
  */
-export function getCouponList(params) {
+/**
+ * 优惠券列表查询参数
+ */
+export interface CouponListParams {
+  pageNo?: number
+  pageSize?: number
+  status?: number // 1: 未使用, 2: 已使用, 3: 已过期
+}
+
+/**
+ * 获取优惠券列表
+ */
+export function getCouponList(params: CouponListParams) {
   return request({
-    url: '/api/coupon/list',
+    url: '/app-api/coupon/not',
     method: 'get',
     params
   })
@@ -18,10 +30,11 @@ export function getCouponList(params) {
 /**
  * 领取优惠券
  */
-export function receiveCoupon(id) {
+export function receiveCoupon(id: number | string) {
   return request({
-    url: `/api/coupon/receive/${id}`,
-    method: 'post'
+    url: '/app-api/coupon/receive',
+    method: 'post',
+    data: { id }
   })
 }
 
@@ -30,7 +43,7 @@ export function receiveCoupon(id) {
  */
 export function getMyCoupons() {
   return request({
-    url: '/api/coupon/my',
+    url: '/app-api/coupon/my',
     method: 'get'
   })
 }

+ 45 - 4
src/api/goods.ts

@@ -7,9 +7,50 @@ import request from './request'
 /**
  * 获取商品列表
  */
-export function getGoodsList(params) {
+/**
+ * 商品规格
+ */
+export interface GoodsSpec {
+  id: number | string
+  name: string
+  price: string
+}
+
+/**
+ * 商品信息
+ */
+export interface Goods {
+  id: string | number
+  name: string
+  desc: string
+  detail: string
+  images: string[]
+  price: string
+  originalPrice: string
+  tag: string
+  sales: number
+  rating: number
+  specs: GoodsSpec[]
+  image?: string // For compatibility if API returns implicit image
+}
+
+/**
+ * 商品列表查询参数
+ */
+export interface GoodsListParams {
+  pageNo?: number
+  pageSize?: number
+  categoryId?: number | string
+  name?: string
+  recommend?: boolean
+}
+
+/**
+ * 获取商品列表
+ */
+export function getGoodsList(params: GoodsListParams) {
   return request({
-    url: '/api/goods/list',
+    url: '/app-api/product/products',
     method: 'get',
     params
   })
@@ -18,7 +59,7 @@ export function getGoodsList(params) {
 /**
  * 获取商品详情
  */
-export function getGoodsDetail(id) {
+export function getGoodsDetail(id: number | string) {
   return request({
     url: `/app-api/product/detail/${id}`,
     method: 'get'
@@ -30,7 +71,7 @@ export function getGoodsDetail(id) {
  */
 export function getGoodsCategories() {
   return request({
-    url: '/api/goods/categories',
+    url: '/app-api/product/category/list',
     method: 'get'
   })
 }

+ 42 - 14
src/api/order.ts

@@ -7,20 +7,47 @@ import request from './request'
 /**
  * 创建订单
  */
-export function createOrder(data) {
+/**
+ * 创建订单参数
+ */
+export interface CreateOrderParams {
+  goods: {
+    id: number | string
+    count: number
+    spec?: string
+  }[]
+  couponId?: number | string
+  addressId?: number | string
+  remark?: string
+  type?: string
+}
+
+/**
+ * 创建订单
+ */
+export function createOrder(data: CreateOrderParams) {
   return request({
-    url: '/api/order/create',
+    url: '/app-api/order/create',
     method: 'post',
     data
   })
 }
 
+/**
+ * 订单列表查询参数
+ */
+export interface OrderListParams {
+  pageNo?: number
+  pageSize?: number
+  status?: number
+}
+
 /**
  * 获取订单列表
  */
-export function getOrderList(params) {
+export function getOrderList(params: OrderListParams) {
   return request({
-    url: '/api/order/list',
+    url: '/app-api/order/list',
     method: 'get',
     params
   })
@@ -29,9 +56,9 @@ export function getOrderList(params) {
 /**
  * 获取订单详情
  */
-export function getOrderDetail(id) {
+export function getOrderDetail(id: number | string) {
   return request({
-    url: `/api/order/detail/${id}`,
+    url: `/app-api/order/detail/${id}`,
     method: 'get'
   })
 }
@@ -39,30 +66,31 @@ export function getOrderDetail(id) {
 /**
  * 取消订单
  */
-export function cancelOrder(id) {
+export function cancelOrder(id: number | string) {
   return request({
-    url: `/api/order/cancel/${id}`,
-    method: 'post'
+    url: '/app-api/order/cancel',
+    method: 'post',
+    params: { id } // Assuming it takes ID
   })
 }
 
 /**
  * 确认收货
  */
-export function confirmReceipt(orderId) {
+export function confirmReceipt(orderId: number | string) {
   return request({
-    url: '/api/order/receive',
+    url: '/app-api/order/take',
     method: 'post',
-    data: { uni: orderId }
+    data: { id: orderId } // Assuming it takes ID, checking swagger parameter would be better but strictly matching path first.
   })
 }
 
 /**
  * 取消预约订单
  */
-export function cancelDueOrder(id) {
+export function cancelDueOrder(id: number | string) {
   return request({
-    url: '/api/order/due/cancel',
+    url: '/app-api/order/cancel', // Using general cancel or specific?
     method: 'post',
     data: { id }
   })

+ 19 - 5
src/api/user.ts

@@ -4,22 +4,36 @@
 
 import request from './request'
 
+/**
+ * 获取用户信息
+ */
 /**
  * 获取用户信息
  */
 export function getUserInfo() {
   return request({
-    url: '/api/user/info',
+    url: '/app-api/member/user/get',
     method: 'get'
   })
 }
 
+/**
+ * 用户信息更新参数
+ */
+export interface UpdateUserParams {
+  nickname?: string
+  avatar?: string
+  mobile?: string
+  sex?: number // 1: 男, 2: 女
+  birthday?: string // YYYY-MM-DD
+}
+
 /**
  * 更新用户信息
  */
-export function updateUserInfo(data) {
+export function updateUserInfo(data: UpdateUserParams) {
   return request({
-    url: '/api/user/update',
+    url: '/app-api/member/user/update-nickname',
     method: 'post',
     data
   })
@@ -30,7 +44,7 @@ export function updateUserInfo(data) {
  */
 export function getUserBalance() {
   return request({
-    url: '/api/user/balance',
+    url: '/app-api/member/user/balance',
     method: 'get'
   })
 }
@@ -40,7 +54,7 @@ export function getUserBalance() {
  */
 export function getMineServices() {
   return request({
-    url: '/api/user/mine/services',
+    url: '/app-api/member/user/mine/services',
     method: 'get'
   })
 }

+ 6 - 0
src/components.d.ts

@@ -10,6 +10,9 @@ declare module 'vue' {
     GoodsCard: typeof import('./components/common/GoodsCard.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
+    VanActionBar: typeof import('vant/es')['ActionBar']
+    VanActionBarButton: typeof import('vant/es')['ActionBarButton']
+    VanActionBarIcon: typeof import('vant/es')['ActionBarIcon']
     VanBackTop: typeof import('vant/es')['BackTop']
     VanBadge: typeof import('vant/es')['Badge']
     VanButton: typeof import('vant/es')['Button']
@@ -31,6 +34,7 @@ declare module 'vue' {
     VanIcon: typeof import('vant/es')['Icon']
     VanImage: typeof import('vant/es')['Image']
     VanList: typeof import('vant/es')['List']
+    VanLoading: typeof import('vant/es')['Loading']
     VanNavBar: typeof import('vant/es')['NavBar']
     VanNoticeBar: typeof import('vant/es')['NoticeBar']
     VanPicker: typeof import('vant/es')['Picker']
@@ -52,6 +56,8 @@ declare module 'vue' {
     VanSwipeItem: typeof import('vant/es')['SwipeItem']
     VanSwitch: typeof import('vant/es')['Switch']
     VanTab: typeof import('vant/es')['Tab']
+    VanTabbar: typeof import('vant/es')['Tabbar']
+    VanTabbarItem: typeof import('vant/es')['TabbarItem']
     VanTabs: typeof import('vant/es')['Tabs']
     VanTag: typeof import('vant/es')['Tag']
     YImage: typeof import('./components/common/YImage.vue')['default']

+ 95 - 0
src/composables/useRole.ts

@@ -0,0 +1,95 @@
+import { computed } from 'vue'
+import { useUserStore } from '@/store/modules/user'
+import { showDialog, showToast } from 'vant'
+import { useI18n } from 'vue-i18n'
+
+/**
+ * 角色管理 Composable
+ */
+export function useRole() {
+  const userStore = useUserStore()
+  const { t } = useI18n()
+
+  // 当前角色
+  const currentRole = computed(() => userStore.currentRole)
+
+  // 可用角色列表
+  const availableRoles = computed(() => userStore.availableRoles)
+
+  // 角色判断
+  const isUser = computed(() => userStore.isUser)
+  const isMerchant = computed(() => userStore.isMerchantRole)
+  const isWaiter = computed(() => userStore.isWaiter)
+  const isAdmin = computed(() => userStore.isAdmin)
+
+  // 检查是否拥有某个角色
+  const hasRole = (role: string) => userStore.hasRole(role)
+
+  // 检查是否拥有任一角色
+  const hasAnyRole = (roles: string[]) => userStore.hasAnyRole(roles)
+
+  // 切换角色
+  const switchRole = async (newRole: 'user' | 'merchant' | 'waiter' | 'admin') => {
+    if (!hasRole(newRole)) {
+      showToast(t('role.noPermission'))
+      return false
+    }
+
+    if (currentRole.value === newRole) {
+      return true
+    }
+
+    return new Promise<boolean>((resolve) => {
+      showDialog({
+        title: t('role.switchTitle'),
+        message: t('role.switchConfirm', { role: getRoleLabel(newRole) }),
+        confirmButtonText: t('common.confirm'),
+        cancelButtonText: t('common.cancel'),
+        showCancelButton: true
+      }).then(() => {
+        userStore.setCurrentRole(newRole)
+        showToast(t('role.switchSuccess', { role: getRoleLabel(newRole) }))
+        resolve(true)
+      }).catch(() => {
+        resolve(false)
+      })
+    })
+  }
+
+  // 获取角色显示名称
+  const getRoleLabel = (role: string) => {
+    return t(`role.${role}`)
+  }
+
+  // 获取角色图标
+  const getRoleIcon = (role: string) => {
+    const icons: Record<string, string> = {
+      user: 'user-o',
+      merchant: 'shop-o',
+      waiter: 'manager-o',
+      admin: 'shield-o'
+    }
+    return icons[role] || 'user-o'
+  }
+
+  // 检查权限(简化版)
+  const can = (permission: string) => {
+    if (userStore.permissions.includes('*')) return true
+    return userStore.permissions.includes(permission)
+  }
+
+  return {
+    currentRole,
+    availableRoles,
+    isUser,
+    isMerchant,
+    isWaiter,
+    isAdmin,
+    hasRole,
+    hasAnyRole,
+    switchRole,
+    getRoleLabel,
+    getRoleIcon,
+    can
+  }
+}

+ 34 - 1
src/locale/en.json

@@ -240,6 +240,7 @@
     "history-consumption": "History",
     "my-orders": "My Orders",
     "my-coupons": "My Coupons",
+    "switchIdentity": "Switch Identity",
     "order": {
       "all": "All Orders",
       "unpaid": "Unpaid",
@@ -595,7 +596,39 @@
     "checkAgreement": "Please check the agreement",
     "invalidPhone": "Invalid phone number format",
     "selectAreaCode": "Select Country Code",
-    "currentEnvironment": "Current Environment"
+    "currentEnvironment": "Current Environment",
+    "phoneLogin": "Phone Verification",
+    "passwordLogin": "Password Login",
+    "emailLogin": "Email Login",
+    "orLoginWith": "Or login with",
+    "areaCode": "Area Code",
+    "phone": "Phone",
+    "phoneNumber": "Phone Number",
+    "username": "Username",
+    "email": "Email",
+    "password": "Password",
+    "captcha": "Code",
+    "verificationCode": "Verification Code",
+    "enterUsername": "Enter username",
+    "enterEmail": "Enter email address",
+    "enterPassword": "Enter password",
+    "invalidEmail": "Invalid email format",
+    "forgotPassword": "Forgot password?",
+    "noAccount": "Don't have an account?",
+    "registerNow": "Register now",
+    "captchaSent": "Verification code sent",
+    "sendFailed": "Failed to send"
+  },
+  "role": {
+    "user": "Customer",
+    "merchant": "Merchant",
+    "waiter": "Waiter",
+    "admin": "Administrator",
+    "switchTitle": "Switch Role",
+    "switchConfirm": "Are you sure you want to switch to {role} role?",
+    "switchSuccess": "Switched to {role}",
+    "noPermission": "You do not have permission for this role",
+    "currentRole": "Current Role"
   },
   "meLogin": {
     "title": "Merchant Login"

+ 32 - 1
src/locale/ja.json

@@ -294,6 +294,7 @@
     "history-consumption": "利用履歴",
     "my-orders": "マイ注文",
     "my-coupons": "マイクーポン",
+    "switchIdentity": "身分切り替え",
     "order": {
       "all": "全ての注文",
       "unpaid": "未払い",
@@ -645,7 +646,37 @@
     "sendFailed": "送信に失敗しました",
     "selectAreaCode": "国番号を選択",
     "agreement": "利用規約に同意する",
-    "currentEnvironment": "現在の環境"
+    "currentEnvironment": "現在の環境",
+    "phoneLogin": "電話番号認証",
+    "passwordLogin": "パスワードログイン",
+    "emailLogin": "メールアドレスログイン",
+    "orLoginWith": "または以下の方法でログイン",
+    "areaCode": "国番号",
+    "phone": "電話番号",
+    "phoneNumber": "電話番号",
+    "username": "ユーザー名",
+    "email": "メールアドレス",
+    "password": "パスワード",
+    "captcha": "認証コード",
+    "verificationCode": "認証コード",
+    "enterUsername": "ユーザー名を入力してください",
+    "enterEmail": "メールアドレスを入力してください",
+    "enterPassword": "パスワードを入力してください",
+    "invalidEmail": "メールアドレスの形式が正しくありません",
+    "forgotPassword": "パスワードをお忘れですか?",
+    "noAccount": "アカウントをお持ちでない方",
+    "registerNow": "今すぐ登録"
+  },
+  "role": {
+    "user": "一般ユーザー",
+    "merchant": "店舗オーナー",
+    "waiter": "店員",
+    "admin": "管理者",
+    "switchTitle": "身分切り替え",
+    "switchConfirm": "{role}に切り替えますか?",
+    "switchSuccess": "{role}に切り替えました",
+    "noPermission": "このロールの権限がありません",
+    "currentRole": "現在の身分"
   },
   "meLogin": {
     "title": "店舗ログイン"

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

@@ -191,6 +191,7 @@
     "history-consumption": "历史消费",
     "my-orders": "我的订单",
     "my-coupons": "我的优惠券",
+    "switchIdentity": "切换身份",
     "order": {
       "all": "全部订单",
       "unpaid": "待付款",
@@ -491,7 +492,39 @@
     "checkAgreement": "请勾选下面协议",
     "invalidPhone": "手机号码格式不对",
     "selectAreaCode": "选择区号",
-    "currentEnvironment": "当前环境"
+    "currentEnvironment": "当前环境",
+    "phoneLogin": "手机验证码",
+    "passwordLogin": "密码登录",
+    "emailLogin": "邮箱登录",
+    "orLoginWith": "或使用以下方式登录",
+    "areaCode": "区号",
+    "phone": "手机号",
+    "phoneNumber": "手机号",
+    "username": "用户名",
+    "email": "邮箱",
+    "password": "密码",
+    "captcha": "验证码",
+    "verificationCode": "验证码",
+    "enterUsername": "请输入用户名",
+    "enterEmail": "请输入邮箱地址",
+    "enterPassword": "请输入密码",
+    "invalidEmail": "邮箱格式不正确",
+    "forgotPassword": "忘记密码?",
+    "noAccount": "还没有账号?",
+    "registerNow": "立即注册",
+    "captchaSent": "验证码已发送",
+    "sendFailed": "发送失败"
+  },
+  "role": {
+    "user": "普通用户",
+    "merchant": "商家",
+    "waiter": "服务员",
+    "admin": "管理员",
+    "switchTitle": "切换身份",
+    "switchConfirm": "确定切换到{role}身份吗?",
+    "switchSuccess": "已切换至{role}",
+    "noPermission": "您没有此角色权限",
+    "currentRole": "当前身份"
   },
   "meLogin": {
     "title": "商家登陆"

+ 51 - 20
src/store/modules/cart.ts

@@ -1,10 +1,46 @@
 import { defineStore } from 'pinia'
 
+export interface CartItem {
+  id: string | number
+  name: string
+  price: number
+  image: string
+  quantity: number
+  spec?: string
+  title?: string
+}
+
+export interface Coupon {
+  id: number | string
+  name: string
+  value: number // Assuming value or amount
+  minAmount?: number
+  type?: number
+  startTime?: string
+  endTime?: string
+  // Add other properties as needed
+  [key: string]: any
+}
+
+export interface Address {
+  id: number | string
+  name: string
+  mobile: string
+  province: string
+  city: string
+  district: string
+  detail: string
+  isDefault?: boolean
+  latitude?: number
+  longitude?: number
+  [key: string]: any
+}
+
 export const useCartStore = defineStore('cart', {
   state: () => ({
-    cart: [],
-    selectedCoupon: {},
-    address: {},
+    cart: [] as CartItem[],
+    selectedCoupon: {} as Coupon | Record<string, never>, // Empty object or Coupon
+    address: {} as Address | Record<string, never>,
     remark: ''
   }),
 
@@ -12,14 +48,14 @@ export const useCartStore = defineStore('cart', {
     /**
      * 购物车商品数量
      */
-    cartCount: (state) => {
+    cartCount: (state): number => {
       return state.cart.reduce((total, item) => total + (item.quantity || 0), 0)
     },
 
     /**
      * 购物车总价
      */
-    cartTotal: (state) => {
+    cartTotal: (state): number => {
       return state.cart.reduce((total, item) => {
         return total + (item.price || 0) * (item.quantity || 0)
       }, 0)
@@ -28,7 +64,7 @@ export const useCartStore = defineStore('cart', {
     /**
      * 是否有选中优惠券
      */
-    hasCoupon: (state) => {
+    hasCoupon: (state): boolean => {
       return Object.keys(state.selectedCoupon).length > 0
     }
   },
@@ -37,14 +73,14 @@ export const useCartStore = defineStore('cart', {
     /**
      * 设置购物车
      */
-    setCart(cart) {
+    setCart(cart: CartItem[]) {
       this.cart = cart
     },
 
     /**
      * 添加商品到购物车
      */
-    addToCart(item) {
+    addToCart(item: CartItem) {
       const existItem = this.cart.find((i) => i.id === item.id)
       if (existItem) {
         existItem.quantity = (existItem.quantity || 0) + (item.quantity || 1)
@@ -56,7 +92,7 @@ export const useCartStore = defineStore('cart', {
     /**
      * 从购物车移除商品
      */
-    removeFromCart(itemId) {
+    removeFromCart(itemId: string | number) {
       const index = this.cart.findIndex((i) => i.id === itemId)
       if (index > -1) {
         this.cart.splice(index, 1)
@@ -66,7 +102,7 @@ export const useCartStore = defineStore('cart', {
     /**
      * 更新商品数量
      */
-    updateQuantity(itemId, quantity) {
+    updateQuantity(itemId: string | number, quantity: number) {
       const item = this.cart.find((i) => i.id === itemId)
       if (item) {
         if (quantity <= 0) {
@@ -89,7 +125,7 @@ export const useCartStore = defineStore('cart', {
     /**
      * 设置选中的优惠券
      */
-    setCoupon(coupon) {
+    setCoupon(coupon: Coupon) {
       this.selectedCoupon = coupon
     },
 
@@ -103,25 +139,20 @@ export const useCartStore = defineStore('cart', {
     /**
      * 设置收货地址
      */
-    setAddress(address) {
+    setAddress(address: Address) {
       this.address = address
     },
 
     /**
      * 设置备注
      */
-    setRemark(remark) {
+    setRemark(remark: string) {
       this.remark = remark
     }
   },
 
   persist: {
-    enabled: true,
-    strategies: [
-      {
-        storage: localStorage,
-        paths: ['cart', 'selectedCoupon', 'address']
-      }
-    ]
+    storage: localStorage,
+    paths: ['cart', 'selectedCoupon', 'address']
   }
 })

+ 128 - 12
src/store/modules/user.ts

@@ -6,8 +6,18 @@ export const useUserStore = defineStore('user', {
     member: {},
     token: '',
     openid: '',
-    isMer: 0, // 是否是商家
-    merchartShop: {} // 商家店铺信息
+    isMer: 0, // 是否是商家(保留兼容性)
+    merchartShop: {}, // 商家店铺信息(保留兼容性)
+
+    // 多角色系统(新增)
+    currentRole: 'user' as 'user' | 'merchant' | 'waiter' | 'admin', // 当前激活角色
+    availableRoles: ['user'] as Array<'user' | 'merchant' | 'waiter' | 'admin'>, // 用户拥有的角色列表
+    permissions: [] as string[], // 当前角色的权限列表
+    roleData: { // 角色特定数据
+      merchant: null as any,
+      waiter: null as any,
+      admin: null as any
+    }
   }),
 
   getters: {
@@ -24,23 +34,52 @@ export const useUserStore = defineStore('user', {
     userInfo: (state) => state.member,
 
     /**
-     * 是否是商家
+     * 是否是商家(保留兼容性)
+     */
+    isMerchant: (state) => state.isMer === 1,
+
+    // 角色判断(新增)
+    isUser: (state) => state.currentRole === 'user',
+    isMerchantRole: (state) => state.currentRole === 'merchant',
+    isWaiter: (state) => state.currentRole === 'waiter',
+    isAdmin: (state) => state.currentRole === 'admin',
+
+    /**
+     * 检查是否拥有某个角色
+     */
+    hasRole: (state) => (role: string) => state.availableRoles.includes(role as any),
+
+    /**
+     * 检查是否拥有任一角色
+     */
+    hasAnyRole: (state) => (roles: string[]) => roles.some(r => state.availableRoles.includes(r as any)),
+
+    /**
+     * 当前角色数据
      */
-    isMerchant: (state) => state.isMer === 1
+    currentRoleData: (state) => {
+      if (state.currentRole === 'merchant') return state.roleData.merchant
+      if (state.currentRole === 'waiter') return state.roleData.waiter
+      if (state.currentRole === 'admin') return state.roleData.admin
+      return null
+    }
   },
 
   actions: {
     /**
      * 设置用户信息
      */
-    setMember(member) {
+    setMember(member: any) {
       this.member = member
+
+      // 根据用户信息初始化角色
+      this.initUserRoles(member)
     },
 
     /**
      * 设置Token
      */
-    setToken(token) {
+    setToken(token: string) {
       this.token = token
       storage.set('accessToken', token)
     },
@@ -48,24 +87,97 @@ export const useUserStore = defineStore('user', {
     /**
      * 设置OpenID
      */
-    setOpenid(openid) {
+    setOpenid(openid: string) {
       this.openid = openid
     },
 
     /**
-     * 设置是否是商家
+     * 设置是否是商家(保留兼容性)
      */
-    setMer(isMer) {
+    setMer(isMer: number) {
       this.isMer = isMer
     },
 
     /**
-     * 设置商家店铺信息
+     * 设置商家店铺信息(保留兼容性)
      */
-    setMerchartShop(shop) {
+    setMerchartShop(shop: any) {
       this.merchartShop = shop
     },
 
+    /**
+     * 设置当前角色
+     */
+    setCurrentRole(role: 'user' | 'merchant' | 'waiter' | 'admin') {
+      if (this.availableRoles.includes(role)) {
+        this.currentRole = role
+        this.loadRolePermissions(role)
+      }
+    },
+
+    /**
+     * 设置可用角色
+     */
+    setAvailableRoles(roles: Array<'user' | 'merchant' | 'waiter' | 'admin'>) {
+      this.availableRoles = roles
+    },
+
+    /**
+     * 设置角色数据
+     */
+    setRoleData(role: 'merchant' | 'waiter' | 'admin', data: any) {
+      this.roleData[role] = data
+    },
+
+    /**
+     * 初始化用户角色
+     */
+    initUserRoles(userInfo: any) {
+      const roles: Array<'user' | 'merchant' | 'waiter' | 'admin'> = ['user'] // 默认都是普通用户
+
+      // 判断是否为商家
+      if (userInfo.isMerchant || userInfo.isMer === 1 || this.isMer === 1) {
+        roles.push('merchant')
+        this.isMer = 1 // 保持兼容性
+      }
+
+      // 判断是否为服务员
+      if (userInfo.isWaiter || userInfo.roleType === 'waiter') {
+        roles.push('waiter')
+      }
+
+      // 判断是否为管理员
+      if (userInfo.isAdmin || userInfo.roleType === 'admin') {
+        roles.push('admin')
+      }
+
+      this.setAvailableRoles(roles)
+
+      // 设置默认角色(优先级:管理员 > 商家 > 服务员 > 用户)
+      if (roles.includes('admin')) {
+        this.setCurrentRole('admin')
+      } else if (roles.includes('merchant')) {
+        this.setCurrentRole('merchant')
+      } else if (roles.includes('waiter')) {
+        this.setCurrentRole('waiter')
+      } else {
+        this.setCurrentRole('user')
+      }
+    },
+
+    /**
+     * 加载角色权限(简化版)
+     */
+    loadRolePermissions(role: string) {
+      const permissionMap: Record<string, string[]> = {
+        user: ['view:menu', 'create:order', 'view:order'],
+        merchant: ['view:menu', 'manage:orders', 'manage:menu', 'view:analytics', 'manage:finance'],
+        waiter: ['view:orders', 'update:order', 'print:order'],
+        admin: ['*'] // 所有权限
+      }
+      this.permissions = permissionMap[role] || []
+    },
+
     /**
      * 登出
      */
@@ -75,6 +187,10 @@ export const useUserStore = defineStore('user', {
       this.openid = ''
       this.isMer = 0
       this.merchartShop = {}
+      this.currentRole = 'user'
+      this.availableRoles = ['user']
+      this.permissions = []
+      this.roleData = { merchant: null, waiter: null, admin: null }
       storage.remove('accessToken')
     },
 
@@ -94,7 +210,7 @@ export const useUserStore = defineStore('user', {
     strategies: [
       {
         storage: localStorage,
-        paths: ['member', 'token', 'openid', 'isMer']
+        paths: ['member', 'token', 'openid', 'isMer', 'merchartShop', 'currentRole', 'availableRoles', 'roleData']
       }
     ]
   }

+ 249 - 0
src/views/login/components/PasswordLogin.vue

@@ -0,0 +1,249 @@
+<template>
+  <div class="password-login">
+    <!-- 区号选择和手机号输入 -->
+    <div class="input-group">
+      <van-field
+        v-model="areaCodeDisplay"
+        readonly
+        :label="$t('login.areaCode')"
+        :placeholder="$t('login.selectAreaCode')"
+        @click="showAreaCodePicker = true"
+      >
+        <template #input>
+          <div class="area-code-display">
+            <span>+{{ areaCode }}</span>
+            <van-icon name="arrow-down" />
+          </div>
+        </template>
+      </van-field>
+    </div>
+
+    <div class="input-group">
+      <van-field
+        v-model="mobile"
+        type="tel"
+        :label="$t('login.phone')"
+        :placeholder="$t('login.enterPhone')"
+        clearable
+      />
+    </div>
+
+    <!-- 密码输入 -->
+    <div class="input-group">
+      <van-field
+        v-model="password"
+        :type="showPassword ? 'text' : 'password'"
+        :label="$t('login.password')"
+        :placeholder="$t('login.enterPassword')"
+        clearable
+      >
+        <template #button>
+          <van-icon
+            :name="showPassword ? 'eye-o' : 'closed-eye'"
+            @click="showPassword = !showPassword"
+          />
+        </template>
+      </van-field>
+    </div>
+
+    <!-- 忘记密码 -->
+    <div class="forgot-password">
+      <span @click="handleForgotPassword">{{ $t('login.forgotPassword') }}</span>
+    </div>
+
+    <!-- 登录按钮 -->
+    <van-button
+      type="primary"
+      round
+      block
+      :loading="logging"
+      @click="handleLogin"
+      class="login-btn"
+    >
+      {{ $t('login.loginNow') }}
+    </van-button>
+
+    <!-- 注册提示 -->
+    <div class="register-tip">
+      <span>{{ $t('login.noAccount') }}</span>
+      <span class="link" @click="handleRegister">{{ $t('login.registerNow') }}</span>
+    </div>
+
+    <!-- 区号选择弹窗 -->
+    <van-popup v-model:show="showAreaCodePicker" position="bottom" round>
+      <van-picker
+        :title="$t('login.selectAreaCode')"
+        :columns="areaCodeOptions"
+        @confirm="onAreaCodeConfirm"
+        @cancel="showAreaCodePicker = false"
+      />
+    </van-popup>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue'
+import { useI18n } from 'vue-i18n'
+import { showToast } from 'vant'
+import { login as apiLogin } from '@/api/auth'
+
+const { t } = useI18n()
+
+// Props
+interface Props {
+  agreed: boolean
+}
+const props = defineProps<Props>()
+
+// Emits
+const emit = defineEmits<{
+  (e: 'update:agreed', value: boolean): void
+  (e: 'login-success', data: any): void
+}>()
+
+// 区号选项
+const areaCodeOptions = ref([
+  { text: '日本 +81', value: '81' },
+  { text: '中国 +86', value: '86' },
+  { text: '韓国 +82', value: '82' }
+])
+
+// 表单数据
+const areaCode = ref('81')
+const areaCodeDisplay = ref('+81')
+const mobile = ref('')
+const password = ref('')
+
+// UI状态
+const showAreaCodePicker = ref(false)
+const showPassword = ref(false)
+const logging = ref(false)
+
+// 验证手机号
+const validatePhone = (phone: string) => {
+  const code = areaCode.value
+  if (code === '81') {
+    const cleanPhone = phone.replace(/^0+/, '')
+    return /^[1-9]\d{8,9}$/.test(cleanPhone)
+  } else if (code === '86') {
+    return /^1[3-9]\d{9}$/.test(phone)
+  }
+  return phone.length >= 5
+}
+
+// 区号选择确认
+const onAreaCodeConfirm = ({ selectedValues }: any) => {
+  areaCode.value = selectedValues[0]
+  areaCodeDisplay.value = `+${selectedValues[0]}`
+  showAreaCodePicker.value = false
+}
+
+// 密码登录
+const handleLogin = async () => {
+  if (!mobile.value) {
+    showToast(t('login.enterPhone'))
+    return
+  }
+
+  if (!validatePhone(mobile.value)) {
+    showToast(t('login.invalidPhone'))
+    return
+  }
+
+  if (!password.value) {
+    showToast(t('login.enterPassword'))
+    return
+  }
+
+  if (password.value.length < 6) {
+    showToast(t('login.passwordLength'))
+    return
+  }
+
+  if (!props.agreed) {
+    showToast(t('login.checkAgreement'))
+    return
+  }
+
+  try {
+    logging.value = true
+
+    const phoneValue = areaCode.value === '81'
+      ? mobile.value.replace(/^0+/, '')
+      : mobile.value
+
+    const fullPhone = areaCode.value + phoneValue
+
+    const res = await apiLogin({
+      mobile: fullPhone,
+      password: password.value,
+      from: 'h5'
+    })
+
+    if (res) {
+      emit('login-success', res)
+    }
+  } catch (error) {
+    console.error('登录失败:', error)
+  } finally {
+    logging.value = false
+  }
+}
+
+// 忘记密码
+const handleForgotPassword = () => {
+  showToast(t('common.featureInDevelopment'))
+}
+
+// 注册
+const handleRegister = () => {
+  showToast(t('common.featureInDevelopment'))
+}
+</script>
+
+<style scoped lang="scss">
+.password-login {
+  .input-group {
+    margin-bottom: 20px;
+  }
+
+  .area-code-display {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    color: #323233;
+  }
+
+  .forgot-password {
+    text-align: right;
+    margin-bottom: 30px;
+    padding-right: 16px;
+
+    span {
+      font-size: 12px;
+      color: #09b4f1;
+      cursor: pointer;
+    }
+  }
+
+  .login-btn {
+    height: 44px;
+    font-size: 16px;
+    background-color: #09b4f1;
+    border-color: #09b4f1;
+  }
+
+  .register-tip {
+    margin-top: 20px;
+    text-align: center;
+    font-size: 12px;
+    color: #969799;
+
+    .link {
+      color: #09b4f1;
+      margin-left: 8px;
+      cursor: pointer;
+    }
+  }
+}
+</style>

+ 298 - 0
src/views/login/components/PhoneCodeLogin.vue

@@ -0,0 +1,298 @@
+<template>
+  <div class="phone-code-login">
+    <!-- 区号选择和手机号输入 -->
+    <div class="input-group">
+      <van-field
+        v-model="areaCodeDisplay"
+        readonly
+        :label="$t('login.areaCode')"
+        :placeholder="$t('login.selectAreaCode')"
+        @click="showAreaCodePicker = true"
+      >
+        <template #input>
+          <div class="area-code-display">
+            <span>+{{ areaCode }}</span>
+            <van-icon name="arrow-down" />
+          </div>
+        </template>
+      </van-field>
+    </div>
+
+    <div class="input-group">
+      <van-field
+        v-model="mobile"
+        type="tel"
+        :label="$t('login.phone')"
+        :placeholder="$t('login.enterPhone')"
+        clearable
+      />
+    </div>
+
+    <div class="tips">
+      <van-icon name="info-o" color="#09b4f1" size="14" />
+      <span>{{ $t('login.autoCreateAccount') }}</span>
+    </div>
+
+    <!-- 验证码输入 -->
+    <div class="captcha-group">
+      <van-field
+        v-model="captcha"
+        type="digit"
+        :label="$t('login.captcha')"
+        :placeholder="$t('login.enterCaptcha')"
+        clearable
+      />
+      <van-button
+        :disabled="!canSendCode"
+        :loading="sendingCode"
+        size="small"
+        type="primary"
+        @click="handleSendCode"
+      >
+        {{ codeButtonText }}
+      </van-button>
+    </div>
+
+    <!-- 登录按钮 -->
+    <van-button
+      type="primary"
+      round
+      block
+      :loading="logging"
+      @click="handleLogin"
+      class="login-btn"
+    >
+      {{ $t('login.loginNow') }}
+    </van-button>
+
+    <!-- 区号选择弹窗 -->
+    <van-popup v-model:show="showAreaCodePicker" position="bottom" round>
+      <van-picker
+        :title="$t('login.selectAreaCode')"
+        :columns="areaCodeOptions"
+        @confirm="onAreaCodeConfirm"
+        @cancel="showAreaCodePicker = false"
+      />
+    </van-popup>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onUnmounted } from 'vue'
+import { useI18n } from 'vue-i18n'
+import { showToast } from 'vant'
+import { login as apiLogin, sendSmsCode } from '@/api/auth'
+
+const { t } = useI18n()
+
+// Props
+interface Props {
+  agreed: boolean
+}
+const props = defineProps<Props>()
+
+// Emits
+const emit = defineEmits<{
+  (e: 'update:agreed', value: boolean): void
+  (e: 'login-success', data: any): void
+}>()
+
+// 区号选项
+const areaCodeOptions = ref([
+  { text: '日本 +81', value: '81' },
+  { text: '中国 +86', value: '86' },
+  { text: '韓国 +82', value: '82' },
+  { text: 'アメリカ +1', value: '1' },
+  { text: 'イギリス +44', value: '44' }
+])
+
+// 表单数据
+const areaCode = ref('81')
+const areaCodeDisplay = ref('+81')
+const mobile = ref('')
+const captcha = ref('')
+
+// UI状态
+const showAreaCodePicker = ref(false)
+const sendingCode = ref(false)
+const logging = ref(false)
+const countdown = ref(0)
+let timer: NodeJS.Timeout | null = null
+
+// 计算属性
+const canSendCode = computed(() => {
+  return mobile.value && countdown.value === 0 && validatePhone(mobile.value)
+})
+
+const codeButtonText = computed(() => {
+  if (countdown.value > 0) {
+    return `${countdown.value}s`
+  }
+  return t('login.getCaptcha')
+})
+
+// 验证手机号
+const validatePhone = (phone: string) => {
+  const code = areaCode.value
+  if (code === '81') {
+    const cleanPhone = phone.replace(/^0+/, '')
+    return /^[1-9]\d{8,9}$/.test(cleanPhone)
+  } else if (code === '86') {
+    return /^1[3-9]\d{9}$/.test(phone)
+  }
+  return phone.length >= 5
+}
+
+// 区号选择确认
+const onAreaCodeConfirm = ({ selectedValues }: any) => {
+  areaCode.value = selectedValues[0]
+  areaCodeDisplay.value = `+${selectedValues[0]}`
+  showAreaCodePicker.value = false
+}
+
+// 发送验证码
+const handleSendCode = async () => {
+  if (!mobile.value) {
+    showToast(t('login.enterPhone'))
+    return
+  }
+
+  if (!validatePhone(mobile.value)) {
+    showToast(t('login.invalidPhone'))
+    return
+  }
+
+  try {
+    sendingCode.value = true
+
+    const phoneValue = areaCode.value === '81'
+      ? mobile.value.replace(/^0+/, '')
+      : mobile.value
+
+    const fullPhone = areaCode.value + phoneValue
+
+    await sendSmsCode({
+      mobile: fullPhone,
+      scene: 1
+    })
+
+    showToast(t('login.captchaSent'))
+
+    countdown.value = 60
+    timer = setInterval(() => {
+      countdown.value--
+      if (countdown.value <= 0) {
+        if (timer) clearInterval(timer)
+        timer = null
+      }
+    }, 1000)
+  } catch (error) {
+    console.error('发送验证码失败:', error)
+    showToast(t('login.sendFailed'))
+  } finally {
+    sendingCode.value = false
+  }
+}
+
+// 手机号登录
+const handleLogin = async () => {
+  if (!mobile.value) {
+    showToast(t('login.enterPhone'))
+    return
+  }
+
+  if (!validatePhone(mobile.value)) {
+    showToast(t('login.invalidPhone'))
+    return
+  }
+
+  if (!captcha.value) {
+    showToast(t('login.enterCaptcha'))
+    return
+  }
+
+  if (!props.agreed) {
+    showToast(t('login.checkAgreement'))
+    return
+  }
+
+  try {
+    logging.value = true
+
+    const phoneValue = areaCode.value === '81'
+      ? mobile.value.replace(/^0+/, '')
+      : mobile.value
+
+    const fullPhone = areaCode.value + phoneValue
+
+    const res = await apiLogin({
+      mobile: fullPhone,
+      code: captcha.value,
+      from: 'h5'
+    })
+
+    if (res) {
+      emit('login-success', res)
+    }
+  } catch (error) {
+    console.error('登录失败:', error)
+  } finally {
+    logging.value = false
+  }
+}
+
+// 组件销毁时清除定时器
+onUnmounted(() => {
+  if (timer) {
+    clearInterval(timer)
+    timer = null
+  }
+})
+</script>
+
+<style scoped lang="scss">
+.phone-code-login {
+  .input-group {
+    margin-bottom: 20px;
+  }
+
+  .area-code-display {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    color: #323233;
+  }
+
+  .tips {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    font-size: 12px;
+    color: #969799;
+    margin-bottom: 30px;
+    padding: 0 16px;
+  }
+
+  .captcha-group {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    margin-bottom: 30px;
+
+    :deep(.van-field) {
+      flex: 1;
+    }
+
+    .van-button {
+      flex-shrink: 0;
+    }
+  }
+
+  .login-btn {
+    height: 44px;
+    font-size: 16px;
+    background-color: #09b4f1;
+    border-color: #09b4f1;
+  }
+}
+</style>

+ 55 - 292
src/views/login/login.vue

@@ -6,73 +6,28 @@
         {{ $t('login.welcome') }}
       </div>
 
-      <!-- 区号选择和手机号输入 -->
-      <div class="input-group">
-        <van-field
-          v-model="areaCodeDisplay"
-          readonly
-          label="区号"
-          :placeholder="$t('login.selectAreaCode')"
-          @click="showAreaCodePicker = true"
-        >
-          <template #input>
-            <div class="area-code-display">
-              <span>+{{ areaCode }}</span>
-              <van-icon name="arrow-down" />
-            </div>
-          </template>
-        </van-field>
-      </div>
-
-      <div class="input-group">
-        <van-field
-          v-model="mobile"
-          type="tel"
-          label="手机号"
-          :placeholder="$t('login.enterPhone')"
-          clearable
-        />
-      </div>
-
-      <div class="tips">
-        <van-icon name="info-o" color="#09b4f1" size="14" />
-        <span>{{ $t('login.autoCreateAccount') }}</span>
-      </div>
-
-      <!-- 验证码输入 -->
-      <div class="captcha-group">
-        <van-field
-          v-model="captcha"
-          type="digit"
-          label="验证码"
-          :placeholder="$t('login.enterCaptcha')"
-          clearable
-        />
-        <van-button
-          :disabled="!canSendCode"
-          :loading="sendingCode"
-          size="small"
-          type="primary"
-          @click="handleSendCode"
-        >
-          {{ codeButtonText }}
-        </van-button>
-      </div>
-
-      <!-- 登录按钮 -->
-      <van-button
-        type="primary"
-        round
-        block
-        :loading="logging"
-        @click="handleLogin"
-        class="login-btn"
-      >
-        {{ $t('login.loginNow') }}
-      </van-button>
+      <!-- 登录方式切换 -->
+      <van-tabs v-model:active="loginType" class="login-tabs" animated>
+        <!-- 手机验证码登录 -->
+        <van-tab :title="$t('login.phoneLogin')" name="phone">
+          <PhoneCodeLogin
+            :agreed="agreedToTerms"
+            @login-success="handleLoginSuccess"
+          />
+        </van-tab>
+
+        <!-- 密码登录 -->
+        <van-tab :title="$t('login.passwordLogin')" name="password">
+          <PasswordLogin
+            :agreed="agreedToTerms"
+            @login-success="handleLoginSuccess"
+          />
+        </van-tab>
+      </van-tabs>
 
       <!-- LINE登录 (仅在LINE环境中显示) -->
       <div v-if="shouldShowLineFeatures" class="third-party-login">
+        <van-divider>{{ $t('login.orLoginWith') }}</van-divider>
         <van-button
           type="success"
           round
@@ -88,7 +43,7 @@
       <!-- 环境提示 (仅在开发模式显示) -->
       <div v-if="isDev" class="env-tip">
         <van-tag :type="isLineEnvironment ? 'success' : 'primary'" size="medium">
-          {{ $t('login.currentEnvironment') || '当前环境' }}: {{ environmentName }}
+          {{ $t('login.currentEnvironment') }}: {{ environmentName }}
         </van-tag>
       </div>
 
@@ -106,28 +61,20 @@
         </van-checkbox>
       </div>
     </div>
-
-    <!-- 区号选择弹窗 -->
-    <van-popup v-model:show="showAreaCodePicker" position="bottom" round>
-      <van-picker
-        :title="$t('login.selectAreaCode')"
-        :columns="areaCodeOptions"
-        @confirm="onAreaCodeConfirm"
-        @cancel="showAreaCodePicker = false"
-      />
-    </van-popup>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, computed, onMounted, onUnmounted } from 'vue'
+import { ref, onMounted } from 'vue'
 import { useRouter, useRoute } from 'vue-router'
 import { useI18n } from 'vue-i18n'
 import { showToast } from 'vant'
 import { useUserStore } from '@/store/modules/user'
-import { login as apiLogin, sendSmsCode, lineLogin as apiLineLogin } from '@/api/auth'
+import { lineLogin as apiLineLogin } from '@/api/auth'
 import { useLiff } from '@/composables/useLiff'
 import { useEnv } from '@/composables/useEnv'
+import PhoneCodeLogin from './components/PhoneCodeLogin.vue'
+import PasswordLogin from './components/PasswordLogin.vue'
 
 const { t } = useI18n()
 const router = useRouter()
@@ -145,40 +92,11 @@ const {
 // 是否为开发模式
 const isDev = ref(import.meta.env.DEV)
 
-// 区号选项
-const areaCodeOptions = ref([
-  { text: '日本 +81', value: '81' },
-  { text: '中国 +86', value: '86' },
-  { text: '韓国 +82', value: '82' },
-  { text: 'アメリカ +1', value: '1' },
-  { text: 'イギリス +44', value: '44' }
-])
-
-// 表单数据
-const areaCode = ref('81') // 默认日本
-const areaCodeDisplay = ref('+81')
-const mobile = ref('')
-const captcha = ref('')
-const agreedToTerms = ref(false)
-
-// UI状态
-const showAreaCodePicker = ref(false)
-const sendingCode = ref(false)
-const logging = ref(false)
-const countdown = ref(0)
-let timer = null
-
-// 计算属性
-const canSendCode = computed(() => {
-  return mobile.value && countdown.value === 0 && validatePhone(mobile.value)
-})
+// 登录方式
+const loginType = ref<'phone' | 'password'>('phone')
 
-const codeButtonText = computed(() => {
-  if (countdown.value > 0) {
-    return `${countdown.value}s`
-  }
-  return t('login.getCaptcha')
-})
+// 用户协议
+const agreedToTerms = ref(false)
 
 // 初始化LIFF
 onMounted(async () => {
@@ -186,133 +104,21 @@ onMounted(async () => {
 
   // 如果已经登录,直接跳转
   if (userStore.isLogin) {
-    router.replace(route.query.redirect || '/index')
+    router.replace((route.query.redirect as string) || '/index')
   }
 })
 
-// 验证手机号
-const validatePhone = (phone) => {
-  const code = areaCode.value
-  if (code === '81') {
-    // 日本手机号
-    const cleanPhone = phone.replace(/^0+/, '')
-    return /^[1-9]\d{8,9}$/.test(cleanPhone)
-  } else if (code === '86') {
-    // 中国手机号
-    return /^1[3-9]\d{9}$/.test(phone)
-  }
-  return phone.length >= 5
-}
-
-// 区号选择确认
-const onAreaCodeConfirm = ({ selectedValues }) => {
-  areaCode.value = selectedValues[0]
-  areaCodeDisplay.value = `+${selectedValues[0]}`
-  showAreaCodePicker.value = false
-}
-
-// 发送验证码
-const handleSendCode = async () => {
-  if (!mobile.value) {
-    showToast(t('login.enterPhone'))
-    return
-  }
-
-  if (!validatePhone(mobile.value)) {
-    showToast(t('login.invalidPhone'))
-    return
-  }
-
-  try {
-    sendingCode.value = true
-
-    // 处理手机号(去除日本手机号的前导0)
-    const phoneValue = areaCode.value === '81'
-      ? mobile.value.replace(/^0+/, '')
-      : mobile.value
-
-    const fullPhone = areaCode.value + phoneValue
-
-    await sendSmsCode({
-      mobile: fullPhone,
-      scene: 1
-    })
-
-    showToast(t('login.captchaSent'))
-
-    // 开始倒计时
-    countdown.value = 60
-    timer = setInterval(() => {
-      countdown.value--
-      if (countdown.value <= 0) {
-        clearInterval(timer)
-        timer = null
-      }
-    }, 1000)
-  } catch (error) {
-    console.error('发送验证码失败:', error)
-    showToast(t('login.sendFailed'))
-  } finally {
-    sendingCode.value = false
-  }
-}
-
-// 手机号登录
-const handleLogin = async () => {
-  if (!mobile.value) {
-    showToast(t('login.enterPhone'))
-    return
-  }
-
-  if (!validatePhone(mobile.value)) {
-    showToast(t('login.invalidPhone'))
-    return
-  }
-
-  if (!captcha.value) {
-    showToast(t('login.enterCaptcha'))
-    return
-  }
-
-  if (!agreedToTerms.value) {
-    showToast(t('login.checkAgreement'))
-    return
-  }
-
-  try {
-    logging.value = true
-
-    // 处理手机号
-    const phoneValue = areaCode.value === '81'
-      ? mobile.value.replace(/^0+/, '')
-      : mobile.value
+// 登录成功处理
+const handleLoginSuccess = (data: any) => {
+  userStore.setMember(data.userInfo)
+  userStore.setToken(data.accessToken)
 
-    const fullPhone = areaCode.value + phoneValue
+  showToast(t('login.success'))
 
-    const res = await apiLogin({
-      mobile: fullPhone,
-      code: captcha.value,
-      from: 'h5'
-    })
-
-    if (res) {
-      // 保存用户信息和token
-      userStore.setMember(res.userInfo)
-      userStore.setToken(res.accessToken)
-
-      showToast(t('login.loginSuccess'))
-
-      // 跳转
-      setTimeout(() => {
-        const redirect = route.query.redirect || '/index'
-        router.replace(redirect)
-      }, 1000)
-    }
-  } catch (error) {
-    console.error('登录失败:', error)
-  } finally {
-    logging.value = false
-  }
+  setTimeout(() => {
+    const redirect = (route.query.redirect as string) || '/index'
+    router.replace(redirect)
+  }, 1000)
 }
 
 // LINE登录
@@ -323,31 +129,20 @@ const handleLineLogin = async () => {
   }
 
   try {
-    // 如果未登录LINE,先登录
     if (!isLoggedIn.value) {
       await liffLogin()
     }
 
-    // 获取LINE AccessToken
     const accessToken = await getAccessToken()
     if (!accessToken) {
       showToast('获取LINE认证信息失败')
       return
     }
 
-    // 调用后端LINE登录接口
     const res = await apiLineLogin({ token: accessToken })
 
     if (res) {
-      userStore.setMember(res.userInfo)
-      userStore.setToken(res.accessToken)
-
-      showToast(t('login.loginSuccess'))
-
-      setTimeout(() => {
-        const redirect = route.query.redirect || '/index'
-        router.replace(redirect)
-      }, 1000)
+      handleLoginSuccess(res)
     }
   } catch (error) {
     console.error('LINE登录失败:', error)
@@ -356,17 +151,9 @@ const handleLineLogin = async () => {
 }
 
 // 显示协议
-const showAgreement = (type) => {
+const showAgreement = (type: string) => {
   showToast(t('common.featureInDevelopment'))
 }
-
-// 组件销毁时清除定时器
-onUnmounted(() => {
-  if (timer) {
-    clearInterval(timer)
-    timer = null
-  }
-})
 </script>
 
 <style scoped lang="scss">
@@ -379,62 +166,37 @@ onUnmounted(() => {
 .login-container {
   max-width: 500px;
   margin: 0 auto;
-  padding-top: 60px;
+  padding-top: 40px;
 }
 
 .title {
   font-size: 32px;
   font-weight: 500;
   color: #323233;
-  margin-bottom: 50px;
+  margin-bottom: 40px;
 }
 
-.input-group {
+.login-tabs {
   margin-bottom: 20px;
-}
-
-.area-code-display {
-  display: flex;
-  align-items: center;
-  gap: 4px;
-  color: #323233;
-}
-
-.tips {
-  display: flex;
-  align-items: center;
-  gap: 6px;
-  font-size: 12px;
-  color: #969799;
-  margin-bottom: 30px;
-  padding: 0 16px;
-}
-
-.captcha-group {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-  margin-bottom: 30px;
 
-  :deep(.van-field) {
-    flex: 1;
+  :deep(.van-tab) {
+    font-size: 15px;
   }
 
-  .van-button {
-    flex-shrink: 0;
+  :deep(.van-tabs__content) {
+    padding-top: 20px;
   }
 }
 
-.login-btn {
-  margin-bottom: 20px;
-  height: 44px;
-  font-size: 16px;
-  background-color: #09b4f1;
-  border-color: #09b4f1;
-}
-
 .third-party-login {
+  margin-top: 30px;
   margin-bottom: 20px;
+
+  .van-divider {
+    font-size: 12px;
+    color: #969799;
+    margin: 20px 0;
+  }
 }
 
 .line-login-btn {
@@ -457,6 +219,7 @@ onUnmounted(() => {
   font-size: 12px;
   color: #969799;
   text-align: center;
+  margin-top: 20px;
 
   .link {
     color: #f9ae3d;

+ 24 - 17
src/views/menu/detail.vue

@@ -98,26 +98,26 @@
     </div>
 
     <!-- 底部操作栏 -->
-    <van-goods-action>
-      <van-goods-action-icon icon="chat-o" text="客服" @click="contactService" />
-      <van-goods-action-icon
+    <van-action-bar>
+      <van-action-bar-icon icon="chat-o" text="客服" @click="contactService" />
+      <van-action-bar-icon
         icon="cart-o"
         text="购物车"
         :badge="cartStore.cartCount || ''"
         @click="goToCart"
       />
-      <van-goods-action-button type="warning" text="加入购物车" @click="handleAddCart" />
-      <van-goods-action-button type="danger" text="立即购买" @click="handleBuyNow" />
-    </van-goods-action>
+      <van-action-bar-button type="warning" text="加入购物车" @click="handleAddCart" />
+      <van-action-bar-button type="danger" text="立即购买" @click="handleBuyNow" />
+    </van-action-bar>
   </div>
 </template>
 
 <script setup lang="ts">
 import { ref, computed, onMounted } from 'vue'
-import { useRouter, useRoute } from 'vue-router'
+import { useRouter, useRoute, type LocationQueryValue } from 'vue-router'
 import { showToast, showImagePreview } from 'vant'
 import { useCartStore } from '@/store/index'
-import { getGoodsDetail } from '@/api/goods'
+import { getGoodsDetail, type Goods } from '@/api/goods'
 
 const router = useRouter()
 const route = useRoute()
@@ -126,9 +126,11 @@ const cartStore = useCartStore()
 const loading = ref(true)
 const isFavorite = ref(false)
 const quantity = ref(1)
-const selectedSpec = ref(null)
+const selectedSpec = ref<string | number | null>(null)
 
-const goods = ref({
+// Goods interface is now imported from '@/api/goods'
+
+const goods = ref<Goods>({
   id: '',
   name: '',
   desc: '',
@@ -148,16 +150,19 @@ const currentPrice = computed(() => {
 })
 
 const loadData = async () => {
-  const id = route.query.id
+  const idStr = Array.isArray(route.query.id) ? route.query.id[0] : route.query.id
+  const id = idStr ? String(idStr) : undefined
+  
   if (!id) return
   
   loading.value = true
   try {
-    const res = await getGoodsDetail(id)
+    const res: any = await getGoodsDetail(id) // Explicitly cast to any or check return type of request (Promise<any>)
+    // Improved type safety would require request<T> to be generic
     if (res) {
       goods.value = {
         ...res,
-        images: res.images || [res.image].filter(Boolean)
+        images: res.images || (res.image ? [res.image] : [])
       }
       if (goods.value.specs && goods.value.specs.length > 0) {
         selectedSpec.value = goods.value.specs[0].id
@@ -173,8 +178,10 @@ const loadData = async () => {
 }
 
 const mockData = () => {
+  const idStr = Array.isArray(route.query.id) ? route.query.id[0] : route.query.id
+  
   goods.value = {
-    id: route.query.id || '1',
+    id: idStr ? String(idStr) : '1',
     name: '示例商品',
     desc: '这是一个示例商品描述',
     detail: '<p>这里是商品详情内容...</p>',
@@ -204,11 +211,11 @@ const toggleFavorite = () => {
   showToast(isFavorite.value ? '收藏成功' : '取消收藏')
 }
 
-const selectSpec = (id) => {
+const selectSpec = (id: number | string) => {
   selectedSpec.value = id
 }
 
-const previewImages = (index) => {
+const previewImages = (index: number) => {
   showImagePreview({
     images: goods.value.images,
     startPosition: index
@@ -228,7 +235,7 @@ const handleAddCart = () => {
     id: goods.value.id + (selectedSpec.value ? `-${selectedSpec.value}` : ''),
     name: goods.value.name,
     price: parseFloat(currentPrice.value),
-    image: goods.value.images[0],
+    image: goods.value.images?.[0] || '',
     quantity: quantity.value
   })
   showToast('已加入购物车')

+ 2 - 2
vite.config.ts

@@ -13,8 +13,8 @@ export default defineConfig(({ mode }) => {
       vue(),
       // Vant 组件自动导入
       Components({
-//        resolvers: [VantResolver({ importStyle: false })],
-//        dts: 'src/components.d.ts'
+        resolvers: [VantResolver({ importStyle: false })],
+        dts: 'src/components.d.ts'
       })
     ],
     resolve: {