diff --git a/apps/AGENTS.md b/apps/AGENTS.md index 2e0e21c..61d3ab0 100644 --- a/apps/AGENTS.md +++ b/apps/AGENTS.md @@ -7,11 +7,13 @@ | App | 端口 | X-Client-App | 负责人 | 原型 | |-----|------|--------------|--------|------| | `h5-user` | 5173 | `USER_H5` | jacy-dukang | `pages/user/` | +| `mini-user` | 5177 / weapp | `USER_MINI` | jacy-dukang | `pages/user/`(V3 目标形态) | | `h5-shop` | 5174 | `SHOP_H5` | 刘景尧 | `pages/shop/` | | `h5-partner` | 5175 | `PARTNER_H5` | 刘景尧 | `pages/partner/` | | `admin-web` | — | (Admin JWT) | jacy-dukang | preV1 内部 HQ 替代 | -V2 目标:`mini-user` / `mini-partner` / `mini-hq` 替换对应 H5(除门店仍 H5)。 +V3 目标:C 端交付为微信小程序 `apps/mini-user`(当前与 `h5-user` 并行;功能以 H5 为过渡参考)。 +V2 规划中的 `mini-partner` / `mini-hq` 非 V3 主交付。 ## 前端硬规则 @@ -58,4 +60,6 @@ V2 目标:`mini-user` / `mini-partner` / `mini-hq` 替换对应 H5(除门店 ```bash pnpm dev:user | dev:shop | dev:partner | dev:admin +pnpm dev:mini-user # C 端小程序 H5 预览 :5177 +pnpm dev:mini-user:weapp # 微信开发者工具打开 apps/mini-user/dist ``` diff --git a/apps/mini-user/.swc/plugins/v7_windows_x86_64_0.104.8/a21de3c9b20ee3f62c37754794ecc3b8637569af96a5d8798cbee4ea25679ab2 b/apps/mini-user/.swc/plugins/v7_windows_x86_64_0.104.8/a21de3c9b20ee3f62c37754794ecc3b8637569af96a5d8798cbee4ea25679ab2 new file mode 100644 index 0000000..71d9455 Binary files /dev/null and b/apps/mini-user/.swc/plugins/v7_windows_x86_64_0.104.8/a21de3c9b20ee3f62c37754794ecc3b8637569af96a5d8798cbee4ea25679ab2 differ diff --git a/apps/mini-user/README.md b/apps/mini-user/README.md new file mode 100644 index 0000000..5db4bbf --- /dev/null +++ b/apps/mini-user/README.md @@ -0,0 +1,45 @@ +# @dukang/mini-user + +杜康好客 C 端微信小程序(Taro 4 + React)。 + +## 启动 + +```bash +# H5 预览(联调 API) +pnpm --filter @dukang/mini-user dev +# → http://localhost:5177 + +# 微信小程序 +pnpm --filter @dukang/mini-user dev:weapp +# 用微信开发者工具打开 apps/mini-user(miniprogramRoot = dist/) +``` + +## API 地址 + +| 环节 | 文件 | 说明 | +|------|------|------| +| **编译注入** | `config/index.ts` → `defineConstants.TARO_APP_API_ORIGIN` | 默认 `https://dkapi.runxian.top`;可用环境变量 `VITE_API_TARGET` 覆盖 | +| **运行时拼装** | `src/lib/api.ts` → `resolveApiBase()` | `{origin}/api/v1`,例如 `https://dkapi.runxian.top/api/v1` | + +本地联调 API(`localhost:3000`)时,启动前设置环境变量并重新编译: + +```powershell +$env:VITE_API_TARGET = "http://localhost:3000" +pnpm dev:mini-user:weapp +``` + +微信小程序还需在公众平台配置 request 合法域名:`dkapi.runxian.top`。 + +## 约定 + +- `X-Client-App: USER_MINI`(与 `USER_H5` 共用用户端 API;JWT 与 Header 一致) +- 功能对照源:`apps/h5-user` + `pages/user/` 原型 +- 底栏使用本地 PNG 图标(weapp 无法加载 Google Material 字体) +- Tab 页:`navigationStyle: custom` + `TabMainHeader`,对齐 h5 浅色顶栏 + +## 迁移路线(建议) + +1. 登录(短信 → 小程序 `wx.login` + 手机号组件) +2. 商品详情 / 下单 / 支付 +3. 订单 3 Tab、权益、核销码 +4. 地址管理、门店详情 diff --git a/apps/mini-user/babel.config.js b/apps/mini-user/babel.config.js new file mode 100644 index 0000000..68ab0a9 --- /dev/null +++ b/apps/mini-user/babel.config.js @@ -0,0 +1,5 @@ +module.exports = { + presets: [ + ['taro', { framework: 'react', ts: true, compiler: 'vite' }], + ], +}; diff --git a/apps/mini-user/config/dev.ts b/apps/mini-user/config/dev.ts new file mode 100644 index 0000000..92f3462 --- /dev/null +++ b/apps/mini-user/config/dev.ts @@ -0,0 +1,11 @@ +import type { UserConfigExport } from '@tarojs/cli'; + +/** + * dev:weapp 下 Vite 会用 prod 模式替换 react 引用,可能导致 useState 等 hooks 失效。 + * @see https://docs.taro.zone/docs/config-detail#minidebugreact + */ +export default { + mini: { + debugReact: true, + }, +} satisfies UserConfigExport; diff --git a/apps/mini-user/config/index.ts b/apps/mini-user/config/index.ts new file mode 100644 index 0000000..eb9731b --- /dev/null +++ b/apps/mini-user/config/index.ts @@ -0,0 +1,66 @@ +import path from 'node:path'; +import { defineConfig } from '@tarojs/cli'; + +/** 小程序/H5 请求的后端 origin(不含 /api);本地联调可用 VITE_API_TARGET 覆盖 */ +const API_ORIGIN = process.env.VITE_API_TARGET ?? 'https://dkapi.runxian.top'; + +export default defineConfig(async () => ({ + projectName: 'mini-user', + date: '2026-7-12', + designWidth: 375, + deviceRatio: { + 640: 2.34 / 2, + 750: 1, + 375: 2, + 828: 1.81 / 2, + }, + sourceRoot: 'src', + outputRoot: 'dist', + plugins: ['@tarojs/plugin-framework-react', '@tarojs/plugin-html'], + alias: { + react: path.resolve(__dirname, '../node_modules/react'), + 'react-dom': path.resolve(__dirname, '../node_modules/react-dom'), + }, + defineConstants: { + TARO_APP_API_ORIGIN: JSON.stringify(API_ORIGIN), + }, + copy: { + patterns: [ + { from: 'src/assets/', to: 'assets/' }, + ], + options: {}, + }, + framework: 'react', + compiler: { + type: 'vite', + vitePlugins: [], + }, + cache: { + enable: false, + }, + mini: { + postcss: { + pxtransform: { enable: true, config: {} }, + cssModules: { enable: false }, + }, + }, + h5: { + publicPath: '/', + staticDirectory: 'static', + devServer: { + port: 5177, + host: '0.0.0.0', + proxy: { + '/api': { + target: API_ORIGIN, + changeOrigin: true, + }, + }, + }, + postcss: { + autoprefixer: { enable: true, config: {} }, + pxtransform: { enable: true, config: {} }, + cssModules: { enable: false }, + }, + }, +})); diff --git a/apps/mini-user/package.json b/apps/mini-user/package.json new file mode 100644 index 0000000..accfa60 --- /dev/null +++ b/apps/mini-user/package.json @@ -0,0 +1,56 @@ +{ + "name": "@dukang/mini-user", + "version": "0.1.0", + "private": true, + "description": "杜康好客 · C 端用户微信小程序(Taro)", + "scripts": { + "dev": "taro build --type h5 --watch", + "dev:weapp": "taro build --type weapp --watch", + "build": "taro build --type h5", + "build:weapp": "taro build --type weapp", + "lint": "echo ok" + }, + "dependencies": { + "@babel/runtime": "^7.24.4", + "@dukang/shared-types": "workspace:*", + "@dukang/shared-ui": "workspace:*", + "@dukang/weixin-sdk": "workspace:*", + "@tarojs/components": "4.2.0", + "@tarojs/helper": "4.2.0", + "@tarojs/plugin-framework-react": "4.2.0", + "@tarojs/plugin-html": "4.2.0", + "@tarojs/plugin-platform-h5": "4.2.0", + "@tarojs/plugin-platform-weapp": "4.2.0", + "@tarojs/react": "4.2.0", + "@tarojs/router": "4.2.0", + "@tarojs/runtime": "4.2.0", + "@tarojs/shared": "4.2.0", + "@tarojs/taro": "4.2.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@babel/preset-react": "^7.24.1", + "@tarojs/cli": "4.2.0", + "@tarojs/vite-runner": "4.2.0", + "@types/react": "^18.3.3", + "@vitejs/plugin-react": "^4.3.1", + "babel-preset-taro": "4.2.0", + "typescript": "^5.4.5", + "vite": "^5.4.0" + }, + "browserslist": { + "development": [ + "defaults and fully supports es6-module", + "maintained node versions", + "Android >= 4.1", + "ios >= 8" + ], + "production": [ + "defaults and fully supports es6-module", + "maintained node versions", + "Android >= 4.1", + "ios >= 8" + ] + } +} diff --git a/apps/mini-user/project.config.json b/apps/mini-user/project.config.json new file mode 100644 index 0000000..983c63d --- /dev/null +++ b/apps/mini-user/project.config.json @@ -0,0 +1,14 @@ +{ + "miniprogramRoot": "dist/", + "projectname": "mini-user", + "description": "杜康好客用户端", + "appid": "wxda31c8e8e85051e7", + "setting": { + "urlCheck": true, + "es6": false, + "enhance": false, + "postcss": false, + "minified": false + }, + "compileType": "miniprogram" +} diff --git a/apps/mini-user/src/app.config.ts b/apps/mini-user/src/app.config.ts new file mode 100644 index 0000000..25c1ed6 --- /dev/null +++ b/apps/mini-user/src/app.config.ts @@ -0,0 +1,49 @@ +export default defineAppConfig({ + pages: [ + 'pages/home/index', + 'pages/stores/index', + 'pages/benefit/index', + 'pages/mine/index', + 'pages/login/index', + ], + window: { + backgroundTextStyle: 'light', + navigationBarBackgroundColor: '#FAF9F7', + navigationBarTitleText: '杜康好客', + navigationBarTextStyle: 'black', + backgroundColor: '#FAF9F7', + }, + tabBar: { + custom: false, + color: '#999999', + selectedColor: '#A61D24', + backgroundColor: '#FFFFFF', + borderStyle: 'black', + list: [ + { + pagePath: 'pages/home/index', + text: '首页', + iconPath: 'assets/tabbar/home.png', + selectedIconPath: 'assets/tabbar/home-active.png', + }, + { + pagePath: 'pages/stores/index', + text: '门店', + iconPath: 'assets/tabbar/store.png', + selectedIconPath: 'assets/tabbar/store-active.png', + }, + { + pagePath: 'pages/benefit/index', + text: '好客权益', + iconPath: 'assets/tabbar/benefit.png', + selectedIconPath: 'assets/tabbar/benefit-active.png', + }, + { + pagePath: 'pages/mine/index', + text: '我的', + iconPath: 'assets/tabbar/mine.png', + selectedIconPath: 'assets/tabbar/mine-active.png', + }, + ], + }, +}); diff --git a/apps/mini-user/src/app.css b/apps/mini-user/src/app.css new file mode 100644 index 0000000..66f3c0b --- /dev/null +++ b/apps/mini-user/src/app.css @@ -0,0 +1,102 @@ +@import './styles/tokens.css'; +@import './styles/home.css'; + +page, +body { + background: var(--color-background); + color: var(--color-on-surface); + font-family: var(--font-body); + margin: 0; +} + +.u-page { + min-height: 100vh; + padding-bottom: calc(24px + env(safe-area-inset-bottom, 0px)); + background: var(--color-background); + box-sizing: border-box; +} + +.u-page--tab { + padding-bottom: calc(88px + env(safe-area-inset-bottom, 0px)); +} + +.u-card { + background: var(--color-card); + border-radius: var(--radius-lg); + margin: 12px var(--space-page); + padding: 16px; + box-shadow: var(--shadow-card); + box-sizing: border-box; +} + +.u-muted { + color: var(--color-subtle-gray); + font-size: 13px; +} + +.u-title { + font-family: var(--font-headline); + font-size: 18px; + font-weight: 600; + margin: 0 0 8px; + color: var(--color-on-surface); +} + +.u-btn { + display: flex; + align-items: center; + justify-content: center; + border: none; + border-radius: var(--radius-md); + padding: 12px 18px; + font-size: 15px; + font-weight: 600; + line-height: 1.2; + background: var(--color-heritage-red); + color: #fff; +} + +.u-btn--block { + width: 100%; +} + +.u-btn:active { + opacity: 0.85; +} + +.u-empty { + text-align: center; + padding: 48px 24px; + color: var(--color-subtle-gray); + font-size: 14px; +} + +.u-store-row { + display: flex; + gap: 12px; + align-items: center; + padding: 14px 0; + border-bottom: 1px solid var(--color-surface-container); +} + +.u-store-row:last-child { + border-bottom: none; +} + +.u-store-avatar { + width: 48px; + height: 48px; + border-radius: 12px; + background: rgba(166, 29, 36, 0.08); + flex-shrink: 0; +} + +.u-store-name { + font-size: 15px; + font-weight: 600; + color: var(--color-on-surface); +} + +.u-safe-top { + padding-top: env(safe-area-inset-top, 0px); +} diff --git a/apps/mini-user/src/app.tsx b/apps/mini-user/src/app.tsx new file mode 100644 index 0000000..3537d0a --- /dev/null +++ b/apps/mini-user/src/app.tsx @@ -0,0 +1,8 @@ +import { PropsWithChildren } from 'react'; +import './app.css'; + +function App({ children }: PropsWithChildren) { + return children; +} + +export default App; diff --git a/apps/mini-user/src/assets/tabbar/benefit-active.png b/apps/mini-user/src/assets/tabbar/benefit-active.png new file mode 100644 index 0000000..d14eb8f Binary files /dev/null and b/apps/mini-user/src/assets/tabbar/benefit-active.png differ diff --git a/apps/mini-user/src/assets/tabbar/benefit.png b/apps/mini-user/src/assets/tabbar/benefit.png new file mode 100644 index 0000000..24d8998 Binary files /dev/null and b/apps/mini-user/src/assets/tabbar/benefit.png differ diff --git a/apps/mini-user/src/assets/tabbar/home-active.png b/apps/mini-user/src/assets/tabbar/home-active.png new file mode 100644 index 0000000..cc7b110 Binary files /dev/null and b/apps/mini-user/src/assets/tabbar/home-active.png differ diff --git a/apps/mini-user/src/assets/tabbar/home.png b/apps/mini-user/src/assets/tabbar/home.png new file mode 100644 index 0000000..8460bc5 Binary files /dev/null and b/apps/mini-user/src/assets/tabbar/home.png differ diff --git a/apps/mini-user/src/assets/tabbar/mine-active.png b/apps/mini-user/src/assets/tabbar/mine-active.png new file mode 100644 index 0000000..3c4b3f4 Binary files /dev/null and b/apps/mini-user/src/assets/tabbar/mine-active.png differ diff --git a/apps/mini-user/src/assets/tabbar/mine.png b/apps/mini-user/src/assets/tabbar/mine.png new file mode 100644 index 0000000..bd8d5af Binary files /dev/null and b/apps/mini-user/src/assets/tabbar/mine.png differ diff --git a/apps/mini-user/src/assets/tabbar/store-active.png b/apps/mini-user/src/assets/tabbar/store-active.png new file mode 100644 index 0000000..c0063c9 Binary files /dev/null and b/apps/mini-user/src/assets/tabbar/store-active.png differ diff --git a/apps/mini-user/src/assets/tabbar/store.png b/apps/mini-user/src/assets/tabbar/store.png new file mode 100644 index 0000000..5beecfe Binary files /dev/null and b/apps/mini-user/src/assets/tabbar/store.png differ diff --git a/apps/mini-user/src/components/CouponBadge.tsx b/apps/mini-user/src/components/CouponBadge.tsx new file mode 100644 index 0000000..25ce66c --- /dev/null +++ b/apps/mini-user/src/components/CouponBadge.tsx @@ -0,0 +1,16 @@ +import { Text } from '@tarojs/components'; + +type CouponBadgeProps = { + amount: number | string; + label?: string; +}; + +/** Taro 友好版权益角标(对齐 shared-ui CouponBadge) */ +export default function CouponBadge({ amount, label = '好客权益' }: CouponBadgeProps) { + return ( + + ¥{amount} + {label} + + ); +} diff --git a/apps/mini-user/src/components/TabMainHeader.tsx b/apps/mini-user/src/components/TabMainHeader.tsx new file mode 100644 index 0000000..5c9d35a --- /dev/null +++ b/apps/mini-user/src/components/TabMainHeader.tsx @@ -0,0 +1,16 @@ +import type { ReactNode } from 'react'; +import { View, Text } from '@tarojs/components'; + +type TabMainHeaderProps = { + title: string; + extra?: ReactNode; +}; + +export default function TabMainHeader({ title, extra }: TabMainHeaderProps) { + return ( + + {title} + {extra ? {extra} : null} + + ); +} diff --git a/apps/mini-user/src/components/UserTabBar.css b/apps/mini-user/src/components/UserTabBar.css new file mode 100644 index 0000000..f5bb2e7 --- /dev/null +++ b/apps/mini-user/src/components/UserTabBar.css @@ -0,0 +1,44 @@ +.u-tabbar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 100; + display: flex; + justify-content: space-around; + align-items: center; + padding: 8px 12px calc(8px + env(safe-area-inset-bottom, 0px)); + background: var(--color-card, #fff); + border-top: 1px solid rgba(226, 190, 188, 0.3); + box-shadow: var(--shadow-tabbar, 0 -4px 20px rgba(0, 0, 0, 0.06)); + box-sizing: border-box; +} + +.u-tabbar__item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2px; + padding: 4px; + border-radius: 8px; + color: var(--color-subtle-gray, #999); +} + +.u-tabbar__item--active { + color: var(--color-heritage-red, #a61d24); +} + +.u-tabbar__icon { + width: 24px; + height: 24px; + display: block; +} + +.u-tabbar__label { + font-size: 11px; + font-weight: 500; + line-height: 1.2; + letter-spacing: 0.02em; +} diff --git a/apps/mini-user/src/components/UserTabBar.tsx b/apps/mini-user/src/components/UserTabBar.tsx new file mode 100644 index 0000000..8b25b06 --- /dev/null +++ b/apps/mini-user/src/components/UserTabBar.tsx @@ -0,0 +1,86 @@ +import { View, Text, Image } from '@tarojs/components'; +import Taro from '@tarojs/taro'; +import './UserTabBar.css'; + +import iconHome from '../assets/tabbar/home.png'; +import iconHomeActive from '../assets/tabbar/home-active.png'; +import iconStore from '../assets/tabbar/store.png'; +import iconStoreActive from '../assets/tabbar/store-active.png'; +import iconBenefit from '../assets/tabbar/benefit.png'; +import iconBenefitActive from '../assets/tabbar/benefit-active.png'; +import iconMine from '../assets/tabbar/mine.png'; +import iconMineActive from '../assets/tabbar/mine-active.png'; + +export const USER_TABS = [ + { + pagePath: '/pages/home/index', + text: '首页', + icon: iconHome, + iconActive: iconHomeActive, + }, + { + pagePath: '/pages/stores/index', + text: '门店', + icon: iconStore, + iconActive: iconStoreActive, + }, + { + pagePath: '/pages/benefit/index', + text: '好客权益', + icon: iconBenefit, + iconActive: iconBenefitActive, + }, + { + pagePath: '/pages/mine/index', + text: '我的', + icon: iconMine, + iconActive: iconMineActive, + }, +] as const; + +type UserTabBarProps = { + selected: number; +}; + +/** C 端底栏:本地 PNG 图标(weapp 无法加载 Material 字体) */ +export default function UserTabBar({ selected }: UserTabBarProps) { + return ( + + {USER_TABS.map((tab, index) => { + const active = selected === index; + return ( + { + if (!active) Taro.switchTab({ url: tab.pagePath }); + }} + > + + {tab.text} + + ); + })} + + ); +} + +/** weapp custom-tab-bar 选中态同步;H5 无 getTabBar 时忽略 */ +export function syncTabBarSelected(index: number) { + try { + const page = Taro.getCurrentInstance().page as + | { getTabBar?: () => { setSelected?: (i: number) => void } } + | undefined; + page?.getTabBar?.()?.setSelected?.(index); + } catch { + /* ignore */ + } +} + +export function shouldRenderPageTabBar(): boolean { + return process.env.TARO_ENV === 'h5'; +} diff --git a/apps/mini-user/src/custom-tab-bar/index.tsx b/apps/mini-user/src/custom-tab-bar/index.tsx new file mode 100644 index 0000000..4b4c65d --- /dev/null +++ b/apps/mini-user/src/custom-tab-bar/index.tsx @@ -0,0 +1,15 @@ +import { Component } from 'react'; +import UserTabBar from '../components/UserTabBar'; + +/** 小程序 custom-tab-bar 入口;H5 由各 Tab 页直接渲染 UserTabBar */ +export default class CustomTabBar extends Component { + state = { selected: 0 }; + + setSelected(index: number) { + this.setState({ selected: index }); + } + + render() { + return ; + } +} diff --git a/apps/mini-user/src/index.html b/apps/mini-user/src/index.html new file mode 100644 index 0000000..ff5e931 --- /dev/null +++ b/apps/mini-user/src/index.html @@ -0,0 +1,16 @@ + + + + + + + 杜康好客 + + +
+ + + diff --git a/apps/mini-user/src/lib/api.ts b/apps/mini-user/src/lib/api.ts new file mode 100644 index 0000000..2a7dec1 --- /dev/null +++ b/apps/mini-user/src/lib/api.ts @@ -0,0 +1,122 @@ +import Taro from '@tarojs/taro'; +import { ClientApp } from '@dukang/shared-types'; + +function resolveApiBase(): string { + const origin = + typeof TARO_APP_API_ORIGIN !== 'undefined' && TARO_APP_API_ORIGIN + ? TARO_APP_API_ORIGIN + : process.env.TARO_ENV === 'h5' + ? 'http://localhost:3000' + : ''; + if (origin) { + return `${origin.replace(/\/$/, '')}/api/v1`; + } + return '/api/v1'; +} + +export const API_BASE = resolveApiBase(); +const TOKEN_KEY = 'user_access_token'; +const REFRESH_KEY = 'user_refresh_token'; +export const CLIENT_APP = ClientApp.USER_MINI; + +export function getToken(): string { + try { + return Taro.getStorageSync(TOKEN_KEY) || ''; + } catch { + return ''; + } +} + +export function getRefreshToken(): string { + try { + return Taro.getStorageSync(REFRESH_KEY) || ''; + } catch { + return ''; + } +} + +export function saveAuth(data: { accessToken: string; refreshToken?: string }) { + Taro.setStorageSync(TOKEN_KEY, data.accessToken); + if (data.refreshToken) { + Taro.setStorageSync(REFRESH_KEY, data.refreshToken); + } +} + +export function clearAuth() { + Taro.removeStorageSync(TOKEN_KEY); + Taro.removeStorageSync(REFRESH_KEY); +} + +export function isLoggedIn(): boolean { + return !!getToken(); +} + +export function redirectToLogin() { + Taro.navigateTo({ url: '/pages/login/index' }); +} + +export function logout() { + clearAuth(); + Taro.reLaunch({ url: '/pages/home/index' }); +} + +type ReqOptions = { + method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + data?: Record | unknown; + auth?: boolean; +}; + +function parseBody(data: unknown): { code?: number; message?: string } { + if (data && typeof data === 'object') { + return data as { code?: number; message?: string }; + } + return {}; +} + +/** 统一请求:注入 USER_MINI + Bearer,解包 { code, message, data } */ +export async function request(path: string, options: ReqOptions = {}): Promise { + const header: Record = { + 'Content-Type': 'application/json', + 'X-Client-App': CLIENT_APP, + }; + const token = getToken(); + if (token) header.Authorization = `Bearer ${token}`; + + const res = await Taro.request({ + url: `${API_BASE}${path}`, + method: options.method ?? 'GET', + data: options.data as Record, + header, + }); + + const status = res.statusCode; + const body = parseBody(res.data); + + if (status === 401 || body?.code === 401) { + clearAuth(); + redirectToLogin(); + throw new Error(body?.message || '登录已过期,请重新登录'); + } + if (status === 404 && body.code === undefined) { + throw new Error('接口不可达,请确认 API 服务已启动'); + } + if (status >= 400 || body.code !== 0) { + throw new Error(body?.message || `请求失败(${status})`); + } + return (res.data as { data: T }).data; +} + +export function toast(title: string, icon: 'success' | 'error' | 'none' = 'none') { + Taro.showToast({ title, icon, duration: 1800 }); +} + +export type SessionPayload = { + accessToken: string; + refreshToken?: string; +}; + +export type UserProfile = { + id: string; + phone?: string | null; + nickname?: string | null; +}; diff --git a/apps/mini-user/src/lib/product-images.ts b/apps/mini-user/src/lib/product-images.ts new file mode 100644 index 0000000..c7a5644 --- /dev/null +++ b/apps/mini-user/src/lib/product-images.ts @@ -0,0 +1,33 @@ +/** 商品无图时的占位(小程序端无本地兜底图时用空串由 UI 显示灰底) */ +export const PRODUCT_IMAGE_FALLBACK = ''; + +export type ProductImageSource = { + mainImageUrl?: string | null; + carouselUrls?: string[] | null; + detailImageUrls?: string[] | null; +}; + +function uniqueUrls(urls: Array) { + const seen = new Set(); + const result: string[] = []; + for (const url of urls) { + if (!url || seen.has(url)) continue; + seen.add(url); + result.push(url); + } + return result; +} + +export function getProductMainImage(source?: ProductImageSource | null): string { + return source?.mainImageUrl ?? source?.carouselUrls?.[0] ?? PRODUCT_IMAGE_FALLBACK; +} + +export function getProductImages(source?: ProductImageSource | null): string[] { + const carousel = uniqueUrls(source?.carouselUrls ?? []); + if (carousel.length > 0) return carousel; + const main = source?.mainImageUrl; + if (main) return [main]; + return PRODUCT_IMAGE_FALLBACK ? [PRODUCT_IMAGE_FALLBACK] : []; +} + +export const FALLBACK_CITY_CODE = '410100'; diff --git a/apps/mini-user/src/pages/benefit/index.config.ts b/apps/mini-user/src/pages/benefit/index.config.ts new file mode 100644 index 0000000..a945419 --- /dev/null +++ b/apps/mini-user/src/pages/benefit/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationStyle: 'custom', + navigationBarTitleText: '好客权益', +}); diff --git a/apps/mini-user/src/pages/benefit/index.tsx b/apps/mini-user/src/pages/benefit/index.tsx new file mode 100644 index 0000000..ce84aca --- /dev/null +++ b/apps/mini-user/src/pages/benefit/index.tsx @@ -0,0 +1,35 @@ +import { View, Text, Button } from '@tarojs/components'; +import Taro, { useDidShow } from '@tarojs/taro'; +import TabMainHeader from '../../components/TabMainHeader'; +import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar'; +import { isLoggedIn } from '../../lib/api'; + +export default function BenefitPage() { + const loggedIn = isLoggedIn(); + + useDidShow(() => { + syncTabBarSelected(2); + }); + + return ( + + + + {loggedIn ? ( + 权益列表将在后续迭代接入 + ) : ( + + 登录后查看好客权益余额 + + + )} + + {shouldRenderPageTabBar() ? : null} + + ); +} diff --git a/apps/mini-user/src/pages/home/index.config.ts b/apps/mini-user/src/pages/home/index.config.ts new file mode 100644 index 0000000..cbb7097 --- /dev/null +++ b/apps/mini-user/src/pages/home/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationStyle: 'custom', + navigationBarTitleText: '杜康好客', +}); diff --git a/apps/mini-user/src/pages/home/index.tsx b/apps/mini-user/src/pages/home/index.tsx new file mode 100644 index 0000000..2ccd214 --- /dev/null +++ b/apps/mini-user/src/pages/home/index.tsx @@ -0,0 +1,123 @@ +import { useEffect, useState } from 'react'; +import { View, Text, Image } from '@tarojs/components'; +import { useDidShow } from '@tarojs/taro'; +import TabMainHeader from '../../components/TabMainHeader'; +import CouponBadge from '../../components/CouponBadge'; +import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar'; +import { request, toast } from '../../lib/api'; +import { FALLBACK_CITY_CODE, getProductMainImage } from '../../lib/product-images'; + +type Product = { + id: string; + name: string; + subtitle?: string; + price: number; + benefitDisplay?: number; + mainImageUrl?: string | null; + carouselUrls?: string[] | null; + aromaType: string; +}; + +const AROMA_TABS = [ + { key: 'QINGXIANG', label: '清香型', open: true }, + { key: 'JIANGXIANG', label: '酱香型', open: false }, + { key: 'NONGXIANG', label: '浓香型', open: false }, +] as const; + +export default function HomePage() { + const [tab, setTab] = useState('QINGXIANG'); + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(true); + const cityCode = FALLBACK_CITY_CODE; + + useDidShow(() => { + syncTabBarSelected(0); + }); + + useEffect(() => { + setLoading(true); + request(`/catalog/products?cityCode=${encodeURIComponent(cityCode)}`) + .then((list) => setProducts(Array.isArray(list) ? list : [])) + .catch((e) => toast(e instanceof Error ? e.message : '加载失败')) + .finally(() => setLoading(false)); + }, [cityCode]); + + function onAromaTabClick(key: string, open: boolean) { + if (!open) { + toast('暂未开放'); + return; + } + setTab(key); + } + + const filtered = products.filter((p) => p.aromaType === tab); + const onSale = tab === 'QINGXIANG'; + + return ( + + + + 郑州市 + + )} + /> + + + {AROMA_TABS.map((t) => ( + onAromaTabClick(t.key, t.open)} + > + {t.label} + + ))} + + + + {loading ? 加载中… : null} + {!loading && !onSale ? 该香型暂未上线,敬请期待 : null} + {!loading && onSale && filtered.length === 0 ? ( + 暂无商品 + ) : null} + {!loading && + onSale && + filtered.map((p) => { + const cover = getProductMainImage(p); + return ( + + {cover ? ( + + ) : ( + + )} + + + {p.name} + ¥{Number(p.price).toFixed(2)} + + {p.subtitle ? {p.subtitle} : null} + + + + + + toast('详情页开发中')} + > + 立即购买 + + + + ); + })} + + + {shouldRenderPageTabBar() ? : null} + + ); +} diff --git a/apps/mini-user/src/pages/login/index.config.ts b/apps/mini-user/src/pages/login/index.config.ts new file mode 100644 index 0000000..d8d09bd --- /dev/null +++ b/apps/mini-user/src/pages/login/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '登录', +}); diff --git a/apps/mini-user/src/pages/login/index.css b/apps/mini-user/src/pages/login/index.css new file mode 100644 index 0000000..948d4b4 --- /dev/null +++ b/apps/mini-user/src/pages/login/index.css @@ -0,0 +1,89 @@ +.login-page { + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + padding: calc(64px + env(safe-area-inset-top, 0px)) 24px calc(24px + env(safe-area-inset-bottom, 0px)); + background: linear-gradient(160deg, #7a0f16 0%, #a61d24 42%, #faf9f7 42%, #faf9f7 100%); + box-sizing: border-box; +} + +.login-brand { + display: flex; + flex-direction: column; + align-items: center; + color: #fff; + margin-bottom: 32px; +} + +.login-title { + font-size: 26px; + font-weight: 800; + letter-spacing: 2px; +} + +.login-subtitle { + font-size: 13px; + opacity: 0.85; + margin-top: 4px; + letter-spacing: 4px; +} + +.login-card { + width: 100%; + max-width: 400px; + background: #fff; + border-radius: 20px; + padding: 24px 20px; + box-shadow: 0 8px 30px rgba(93, 64, 55, 0.12); + display: flex; + flex-direction: column; + gap: 14px; + box-sizing: border-box; +} + +.login-card-title { + font-size: 18px; + font-weight: 700; + text-align: center; + margin-bottom: 6px; + color: var(--color-on-surface, #1a1c1b); +} + +.login-input { + width: 100%; + height: 48px; + padding: 0 14px; + border-radius: 12px; + background: #faf9f7; + font-size: 15px; + box-sizing: border-box; +} + +.login-code-row { + display: flex; + gap: 8px; + align-items: center; +} + +.login-input--code { + flex: 1; +} + +.login-send { + flex-shrink: 0; + height: 48px; + padding: 0 12px; + border-radius: 12px; + background: rgba(166, 29, 36, 0.08); + color: #a61d24; + font-size: 13px; + font-weight: 600; + border: none; + line-height: 48px; +} + +.login-msg { + color: #a61d24; + font-size: 13px; +} diff --git a/apps/mini-user/src/pages/login/index.tsx b/apps/mini-user/src/pages/login/index.tsx new file mode 100644 index 0000000..69e8180 --- /dev/null +++ b/apps/mini-user/src/pages/login/index.tsx @@ -0,0 +1,114 @@ +import { useState } from 'react'; +import { View, Text, Input, Button } from '@tarojs/components'; +import Taro from '@tarojs/taro'; +import { SmsScene } from '@dukang/shared-types'; +import { request, saveAuth, toast, type SessionPayload } from '../../lib/api'; +import './index.css'; + +export default function LoginPage() { + const [phone, setPhone] = useState(''); + const [code, setCode] = useState(process.env.TARO_ENV === 'h5' ? '123456' : ''); + const [cooldown, setCooldown] = useState(0); + const [loading, setLoading] = useState(false); + const [msg, setMsg] = useState(''); + + async function sendCode() { + if (cooldown > 0) return; + const normalized = phone.trim(); + if (!/^1[3-9]\d{9}$/.test(normalized)) { + setMsg('请输入正确的手机号'); + return; + } + setMsg(''); + try { + await request('/auth/sms/send', { + method: 'POST', + data: { phone: normalized, scene: SmsScene.USER_LOGIN }, + }); + toast('验证码已发送'); + setCooldown(60); + const timer = setInterval(() => { + setCooldown((c) => { + if (c <= 1) { + clearInterval(timer); + return 0; + } + return c - 1; + }); + }, 1000); + } catch (e) { + setMsg(e instanceof Error ? e.message : '发送失败'); + } + } + + async function login() { + const normalized = phone.trim(); + if (!/^1[3-9]\d{9}$/.test(normalized)) { + setMsg('请输入正确的手机号'); + return; + } + if (!code.trim()) { + setMsg('请输入验证码'); + return; + } + setLoading(true); + setMsg(''); + try { + const data = await request('/auth/login/sms', { + method: 'POST', + data: { phone: normalized, code: code.trim() }, + }); + saveAuth(data); + toast('登录成功', 'success'); + Taro.switchTab({ url: '/pages/home/index' }); + } catch (e) { + setMsg(e instanceof Error ? e.message : '登录失败'); + } finally { + setLoading(false); + } + } + + return ( + + + 杜康好客 + 用户端小程序 + + + 手机号登录 + setPhone(e.detail.value)} + /> + + setCode(e.detail.value)} + /> + + + {msg ? {msg} : null} + + + Mock 环境验证码一般为 123456;微信一键登录后续接入 + + + + ); +} diff --git a/apps/mini-user/src/pages/mine/index.config.ts b/apps/mini-user/src/pages/mine/index.config.ts new file mode 100644 index 0000000..688777d --- /dev/null +++ b/apps/mini-user/src/pages/mine/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationStyle: 'custom', + navigationBarTitleText: '我的', +}); diff --git a/apps/mini-user/src/pages/mine/index.tsx b/apps/mini-user/src/pages/mine/index.tsx new file mode 100644 index 0000000..c379c7d --- /dev/null +++ b/apps/mini-user/src/pages/mine/index.tsx @@ -0,0 +1,57 @@ +import { useEffect, useState } from 'react'; +import { View, Text, Button } from '@tarojs/components'; +import Taro, { useDidShow } from '@tarojs/taro'; +import TabMainHeader from '../../components/TabMainHeader'; +import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar'; +import { isLoggedIn, logout, request, toast, type UserProfile } from '../../lib/api'; + +export default function MinePage() { + const [profile, setProfile] = useState(null); + const loggedIn = isLoggedIn(); + + useDidShow(() => { + syncTabBarSelected(3); + }); + + useEffect(() => { + if (!loggedIn) return; + request('/auth/me') + .then(setProfile) + .catch((e) => toast(e instanceof Error ? e.message : '加载失败')); + }, [loggedIn]); + + return ( + + + + {loggedIn ? ( + + + {profile?.nickname || '用户'} + + + {profile?.phone || '未绑定手机'} + + + 订单 / 地址 / 核销码等页面后续从 h5-user 迁移 + + + + ) : ( + + 登录后管理订单与个人信息 + + + )} + + {shouldRenderPageTabBar() ? : null} + + ); +} diff --git a/apps/mini-user/src/pages/stores/index.config.ts b/apps/mini-user/src/pages/stores/index.config.ts new file mode 100644 index 0000000..da2edd3 --- /dev/null +++ b/apps/mini-user/src/pages/stores/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationStyle: 'custom', + navigationBarTitleText: '门店', +}); diff --git a/apps/mini-user/src/pages/stores/index.tsx b/apps/mini-user/src/pages/stores/index.tsx new file mode 100644 index 0000000..448b49b --- /dev/null +++ b/apps/mini-user/src/pages/stores/index.tsx @@ -0,0 +1,55 @@ +import { useEffect, useState } from 'react'; +import { View, Text } from '@tarojs/components'; +import { useDidShow } from '@tarojs/taro'; +import TabMainHeader from '../../components/TabMainHeader'; +import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar'; +import { request, toast } from '../../lib/api'; + +type Store = { + id: string; + name: string; + address?: string; + status?: string; +}; + +export default function StoresPage() { + const [stores, setStores] = useState([]); + const [loading, setLoading] = useState(true); + + useDidShow(() => { + syncTabBarSelected(1); + }); + + useEffect(() => { + request('/stores') + .then((list) => setStores(Array.isArray(list) ? list : [])) + .catch((e) => toast(e instanceof Error ? e.message : '加载失败')) + .finally(() => setLoading(false)); + }, []); + + return ( + + + + {loading ? ( + 加载中… + ) : stores.length === 0 ? ( + 暂无营业中门店 + ) : ( + stores.map((s) => ( + + + + {s.name} + + {s.address || '地址待完善'} + + + + )) + )} + + {shouldRenderPageTabBar() ? : null} + + ); +} diff --git a/apps/mini-user/src/styles/home.css b/apps/mini-user/src/styles/home.css new file mode 100644 index 0000000..30d434b --- /dev/null +++ b/apps/mini-user/src/styles/home.css @@ -0,0 +1,217 @@ +/* Tab 主页面顶栏 + 首页(对齐 h5-user) */ +.tab-main-header { + position: sticky; + top: 0; + z-index: 50; + display: flex; + align-items: center; + justify-content: center; + height: 56px; + padding: env(safe-area-inset-top, 0px) var(--space-page) 0; + padding-top: calc(env(safe-area-inset-top, 0px)); + min-height: calc(56px + env(safe-area-inset-top, 0px)); + background: var(--color-background); + box-shadow: var(--shadow-card); + box-sizing: border-box; +} + +.tab-main-header__title { + font-family: var(--font-headline); + font-size: 18px; + font-weight: 700; + color: var(--color-heritage-red); + line-height: 56px; +} + +.tab-main-header__extra { + position: absolute; + right: var(--space-page); + top: calc(env(safe-area-inset-top, 0px) + 28px); + transform: translateY(-50%); + display: flex; + align-items: center; + gap: 4px; + color: var(--color-heritage-red); + font-size: 12px; + font-weight: 500; + max-width: 46vw; +} + +.tab-main-city-pin { + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--color-heritage-red); + flex-shrink: 0; + position: relative; +} + +.tab-main-city-pin::after { + content: ''; + position: absolute; + left: 50%; + top: 50%; + width: 5px; + height: 5px; + margin: -2px 0 0 -2px; + border-radius: 50%; + background: #fff; +} + +.tab-main-city-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100px; +} + +.home-page { + background: var(--color-background); +} + +.home-aroma-nav { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 8px var(--space-page); + background: rgba(250, 249, 247, 0.95); + border-bottom: 1px solid var(--color-surface-container); + position: sticky; + top: calc(56px + env(safe-area-inset-top, 0px)); + z-index: 40; +} + +.home-aroma-tab { + border: none; + background: transparent; + padding: 6px 12px; + font-family: var(--font-headline); + font-size: 16px; + font-weight: 600; + line-height: 24px; + color: var(--color-subtle-gray); + border-bottom: 2px solid transparent; + white-space: nowrap; +} + +.home-aroma-tab--active { + color: var(--color-heritage-red); + border-bottom-color: var(--color-heritage-red); +} + +.home-aroma-tab--muted { + opacity: 0.65; +} + +.home-product-list { + display: flex; + flex-direction: column; + gap: 16px; + padding: 16px var(--space-page); +} + +.home-product-card { + background: var(--color-card); + border-radius: var(--radius-lg); + overflow: hidden; + box-shadow: var(--shadow-card); +} + +.home-product-cover { + width: 100%; + height: 200px; + background: var(--color-surface-container); + display: block; +} + +.home-product-cover--empty { + height: 200px; + background: var(--color-surface-container); +} + +.home-product-body { + padding: var(--space-gutter); + display: flex; + flex-direction: column; + gap: 4px; +} + +.home-product-row { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 8px; +} + +.home-product-name { + font-family: var(--font-headline); + font-size: 16px; + font-weight: 600; + line-height: 24px; + color: var(--color-on-surface); + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.home-product-price { + font-size: 16px; + font-weight: 700; + color: var(--color-heritage-red); + flex-shrink: 0; +} + +.home-product-sub { + font-size: 13px; + color: var(--color-subtle-gray); + line-height: 18px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.home-product-footer { + margin-top: 4px; +} + +.home-product-actions { + padding: 0 var(--space-gutter) var(--space-gutter); + display: flex; + justify-content: flex-end; +} + +.home-buy-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 16px; + border-radius: var(--radius-full); + background: var(--color-heritage-red); + color: #fff; + font-size: 13px; + font-weight: 600; + border: none; +} + +.home-empty { + text-align: center; + padding: 48px 24px; + color: var(--color-subtle-gray); + font-size: 14px; +} + +.coupon-badge { + position: relative; + display: inline-flex; + align-items: center; + background: var(--color-aged-amber); + color: var(--color-on-secondary-container); + padding: 4px 12px; + border-radius: 2px; + font-family: var(--font-label); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.05em; +} diff --git a/apps/mini-user/src/styles/tokens.css b/apps/mini-user/src/styles/tokens.css new file mode 100644 index 0000000..2c47d56 --- /dev/null +++ b/apps/mini-user/src/styles/tokens.css @@ -0,0 +1,51 @@ +/* Dukang design tokens — mirrored from packages/shared-ui/src/tokens.css */ +page, +:root { + --color-surface: #faf9f7; + --color-surface-dim: #dadad8; + --color-surface-container-low: #f4f3f1; + --color-surface-container: #efeeec; + --color-surface-container-highest: #e3e2e0; + --color-on-surface: #1a1c1b; + --color-on-surface-variant: #5a413f; + --color-outline: #8d706e; + --color-outline-variant: #e2bebc; + + --color-primary: #820012; + --color-on-primary: #ffffff; + --color-heritage-red: #a61d24; + --color-on-primary-container: #ffb9b4; + + --color-secondary: #735c00; + --color-secondary-container: #fed65b; + --color-on-secondary-container: #745c00; + --color-aged-amber: #ffbf00; + + --color-success-green: #2d6a4f; + --color-status-blue: #2a6ebb; + --color-subtle-gray: #999999; + --color-ink-black: #1a1a1a; + + --color-error: #ba1a1a; + --color-background: #faf9f7; + --color-card: #ffffff; + + --font-headline: 'PingFang SC', 'Noto Sans SC', sans-serif; + --font-body: 'PingFang SC', 'Noto Sans SC', sans-serif; + --font-label: 'PingFang SC', 'Noto Sans SC', sans-serif; + + --radius-sm: 0.25rem; + --radius-md: 0.75rem; + --radius-lg: 1rem; + --radius-xl: 1.5rem; + --radius-full: 9999px; + + --space-page: 20px; + --space-gutter: 12px; + --space-md: 16px; + --space-lg: 32px; + + --shadow-card: 0 4px 20px rgba(166, 29, 36, 0.05); + --shadow-tabbar: 0 -4px 20px rgba(0, 0, 0, 0.06); + --border-card: 1px solid rgba(166, 29, 36, 0.05); +} diff --git a/apps/mini-user/tsconfig.json b/apps/mini-user/tsconfig.json new file mode 100644 index 0000000..27f833e --- /dev/null +++ b/apps/mini-user/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2017", + "module": "ESNext", + "moduleResolution": "node", + "removeComments": false, + "preserveConstEnums": true, + "moduleDetection": "force", + "useDefineForClassFields": true, + "outDir": "lib", + "sourceMap": true, + "baseUrl": ".", + "rootDir": ".", + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "resolveJsonModule": true, + "typeRoots": ["node_modules/@types"], + "paths": { + "@/*": ["./src/*"], + "@dukang/shared-ui": ["../../packages/shared-ui/src"], + "@dukang/shared-ui/*": ["../../packages/shared-ui/src/*"] + } + }, + "include": ["./src", "./types", "./config"], + "compileOnSave": false +} diff --git a/apps/mini-user/types/global.d.ts b/apps/mini-user/types/global.d.ts new file mode 100644 index 0000000..0c14519 --- /dev/null +++ b/apps/mini-user/types/global.d.ts @@ -0,0 +1,22 @@ +/// + +declare module '*.png'; +declare module '*.gif'; +declare module '*.jpg'; +declare module '*.jpeg'; +declare module '*.svg'; +declare module '*.css'; +declare module '*.less'; +declare module '*.scss'; + +declare const defineAppConfig: (config: Record) => Record; +declare const definePageConfig: (config: Record) => Record; + +declare const TARO_APP_API_ORIGIN: string; + +declare namespace NodeJS { + interface ProcessEnv { + TARO_ENV: 'weapp' | 'h5' | string; + VITE_API_TARGET?: string; + } +} diff --git a/package.json b/package.json index f40dc95..8bc8f1f 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "dev:partner": "pnpm --filter @dukang/h5-partner dev", "dev:admin": "pnpm --filter @dukang/admin-web dev", "dev:hq": "pnpm --filter @dukang/mini-hq dev", + "dev:mini-user": "pnpm --filter @dukang/mini-user dev", + "dev:mini-user:weapp": "pnpm --filter @dukang/mini-user dev:weapp", "preview:hq": "pnpm --filter @dukang/mini-hq build && pnpm --filter @dukang/mini-hq preview", "build": "pnpm -r build", "lint": "eslint .", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09619d3..5bb2d51 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -192,6 +192,88 @@ importers: '@dukang/weixin-sdk': specifier: workspace:* version: link:../../packages/weixin-sdk + '@tarojs/components': + specifier: 4.2.0 + version: 4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)) + '@tarojs/helper': + specifier: 4.2.0 + version: 4.2.0 + '@tarojs/plugin-framework-react': + specifier: 4.2.0 + version: 4.2.0(@tarojs/helper@4.2.0)(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)(@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)))(react@18.3.1)(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))(webpack@5.97.1(postcss@8.5.15)) + '@tarojs/plugin-html': + specifier: 4.2.0 + version: 4.2.0(@tarojs/helper@4.2.0)(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0) + '@tarojs/plugin-platform-h5': + specifier: 4.2.0 + version: 4.2.0(@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@types/react@18.3.31)(postcss@8.5.15)(react@18.3.1)(rollup@3.30.0)(solid-js@1.9.14)(webpack@5.97.1(postcss@8.5.15)) + '@tarojs/plugin-platform-weapp': + specifier: 4.2.0 + version: 4.2.0(@tarojs/service@4.2.0)(@tarojs/shared@4.2.0) + '@tarojs/react': + specifier: 4.2.0 + version: 4.2.0(react@18.3.1) + '@tarojs/router': + specifier: 4.2.0 + version: 4.2.0(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)(@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15))) + '@tarojs/runtime': + specifier: 4.2.0 + version: 4.2.0 + '@tarojs/shared': + specifier: 4.2.0 + version: 4.2.0 + '@tarojs/taro': + specifier: 4.2.0 + version: 4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)) + qrcode: + specifier: ^1.5.4 + version: 1.5.4 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@babel/preset-react': + specifier: ^7.24.1 + version: 7.29.7(@babel/core@7.29.7) + '@tarojs/cli': + specifier: 4.2.0 + version: 4.2.0 + '@tarojs/vite-runner': + specifier: 4.2.0 + version: 4.2.0(@tarojs/runtime@4.2.0)(@types/babel__core@7.20.5)(postcss@8.5.15)(rollup@3.30.0)(terser@5.48.0)(typescript@5.9.3)(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)) + '@types/react': + specifier: ^18.3.3 + version: 18.3.31 + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.7.0(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)) + babel-preset-taro: + specifier: 4.2.0 + version: 4.2.0(@babel/core@7.29.7)(@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7))(@babel/preset-react@7.29.7(@babel/core@7.29.7)) + typescript: + specifier: ^5.4.5 + version: 5.9.3 + vite: + specifier: ^5.4.0 + version: 5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0) + + apps/mini-user: + dependencies: + '@babel/runtime': + specifier: ^7.24.4 + version: 7.29.7 + '@dukang/shared-types': + specifier: workspace:* + version: link:../../packages/shared-types + '@dukang/shared-ui': + specifier: workspace:* + version: link:../../packages/shared-ui + '@dukang/weixin-sdk': + specifier: workspace:* + version: link:../../packages/weixin-sdk '@tarojs/components': specifier: 4.2.0 version: 4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(@swc/core@1.3.96)(postcss@8.5.15)) @@ -225,9 +307,6 @@ importers: '@tarojs/taro': specifier: 4.2.0 version: 4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(@swc/core@1.3.96)(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(@swc/core@1.3.96)(postcss@8.5.15)) - qrcode: - specifier: ^1.5.4 - version: 1.5.4 react: specifier: ^18.3.1 version: 18.3.1 @@ -8366,6 +8445,29 @@ snapshots: - webpack-chain - webpack-dev-server + '@tarojs/components-react@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(react@18.3.1)(rollup@3.30.0)(solid-js@1.9.14)(webpack@5.97.1(postcss@8.5.15))': + dependencies: + '@babel/runtime': 7.29.7 + '@tarojs/components': 4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)) + '@tarojs/shared': 4.2.0 + '@tarojs/taro': 4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)) + classnames: 2.5.1 + identity-obj-proxy: 3.0.0 + react: 18.3.1 + solid-js: 1.9.14 + swiper: 11.1.15 + tslib: 2.8.1 + transitivePeerDependencies: + - '@tarojs/helper' + - '@types/react' + - html-webpack-plugin + - postcss + - rollup + - vue + - webpack + - webpack-chain + - webpack-dev-server + '@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(@swc/core@1.3.96)(postcss@8.5.15))': dependencies: '@stencil/core': 2.22.3 @@ -8389,6 +8491,29 @@ snapshots: - webpack-chain - webpack-dev-server + '@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15))': + dependencies: + '@stencil/core': 2.22.3 + '@tarojs/runtime': 4.2.0 + '@tarojs/shared': 4.2.0 + '@tarojs/taro': 4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)) + classnames: 2.5.1 + hammerjs: 2.0.8 + hls.js: 1.6.16 + resolve-pathname: 3.0.0 + swiper: 11.1.15 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.31 + transitivePeerDependencies: + - '@tarojs/helper' + - html-webpack-plugin + - postcss + - rollup + - webpack + - webpack-chain + - webpack-dev-server + '@tarojs/helper@4.2.0': dependencies: '@babel/core': 7.29.7 @@ -8526,6 +8651,21 @@ snapshots: vite: 5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0) webpack: 5.97.1(@swc/core@1.3.96)(postcss@8.5.15) + '@tarojs/plugin-framework-react@4.2.0(@tarojs/helper@4.2.0)(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)(@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)))(react@18.3.1)(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))(webpack@5.97.1(postcss@8.5.15))': + dependencies: + '@tarojs/helper': 4.2.0 + '@tarojs/runtime': 4.2.0 + '@tarojs/shared': 4.2.0 + acorn: 8.17.0 + acorn-walk: 8.3.5 + lodash: 4.18.1 + tslib: 2.8.1 + optionalDependencies: + '@vitejs/plugin-react': 4.7.0(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)) + react: 18.3.1 + vite: 5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0) + webpack: 5.97.1(postcss@8.5.15) + '@tarojs/plugin-html@4.2.0(@tarojs/helper@4.2.0)(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)': dependencies: '@tarojs/helper': 4.2.0 @@ -8561,6 +8701,35 @@ snapshots: - webpack-chain - webpack-dev-server + '@tarojs/plugin-platform-h5@4.2.0(@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@types/react@18.3.31)(postcss@8.5.15)(react@18.3.1)(rollup@3.30.0)(solid-js@1.9.14)(webpack@5.97.1(postcss@8.5.15))': + dependencies: + '@babel/core': 7.29.7 + '@tarojs/components': 4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)) + '@tarojs/components-react': 4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(react@18.3.1)(rollup@3.30.0)(solid-js@1.9.14)(webpack@5.97.1(postcss@8.5.15)) + '@tarojs/helper': 4.2.0 + '@tarojs/runtime': 4.2.0 + '@tarojs/service': 4.2.0 + '@tarojs/shared': 4.2.0 + '@tarojs/taro-h5': 4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15))) + babel-plugin-transform-taroapi: 4.2.0(@babel/core@7.29.7) + change-case: 4.1.2 + lodash-es: 4.17.21 + tslib: 2.8.1 + transitivePeerDependencies: + - '@swc/helpers' + - '@tarojs/taro' + - '@types/react' + - html-webpack-plugin + - postcss + - react + - rollup + - solid-js + - supports-color + - vue + - webpack + - webpack-chain + - webpack-dev-server + '@tarojs/plugin-platform-weapp@4.2.0(@tarojs/service@4.2.0)(@tarojs/shared@4.2.0)': dependencies: '@tarojs/service': 4.2.0 @@ -8585,6 +8754,18 @@ snapshots: tslib: 2.8.1 universal-router: 9.2.1 + '@tarojs/router@4.2.0(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)(@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))': + dependencies: + '@tarojs/runtime': 4.2.0 + '@tarojs/shared': 4.2.0 + '@tarojs/taro': 4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)) + dingtalk-jsapi: 2.15.6 + history: 5.3.0 + mobile-detect: 1.4.5 + query-string: 9.4.1 + tslib: 2.8.1 + universal-router: 9.2.1 + '@tarojs/runner-utils@4.2.0': dependencies: '@tarojs/helper': 4.2.0 @@ -8636,6 +8817,26 @@ snapshots: transitivePeerDependencies: - '@tarojs/taro' + '@tarojs/taro-h5@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))': + dependencies: + '@tarojs/api': 4.2.0(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0) + '@tarojs/components': 4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)) + '@tarojs/router': 4.2.0(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)(@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15))) + '@tarojs/runtime': 4.2.0 + '@tarojs/shared': 4.2.0 + abortcontroller-polyfill: 1.7.8 + base64-js: 1.5.1 + ics: 3.12.0 + is-mobile: 4.0.0 + jsonp-retry: 1.0.3 + lodash-es: 4.17.21 + platform: 1.3.6 + query-string: 9.4.1 + tslib: 2.8.1 + whatwg-fetch: 3.6.20 + transitivePeerDependencies: + - '@tarojs/taro' + '@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(@swc/core@1.3.96)(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(@swc/core@1.3.96)(postcss@8.5.15))': dependencies: '@tarojs/api': 4.2.0(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0) @@ -8650,6 +8851,20 @@ snapshots: rollup: 3.30.0 webpack: 5.97.1(@swc/core@1.3.96)(postcss@8.5.15) + '@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15))': + dependencies: + '@tarojs/api': 4.2.0(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0) + '@tarojs/components': 4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)) + '@tarojs/helper': 4.2.0 + '@tarojs/runtime': 4.2.0 + '@tarojs/shared': 4.2.0 + '@types/postcss-url': 10.0.4 + postcss: 8.5.15 + optionalDependencies: + '@types/react': 18.3.31 + rollup: 3.30.0 + webpack: 5.97.1(postcss@8.5.15) + '@tarojs/vite-runner@4.2.0(@tarojs/runtime@4.2.0)(@types/babel__core@7.20.5)(postcss@8.5.15)(rollup@3.30.0)(terser@5.48.0)(typescript@5.9.3)(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))': dependencies: '@ampproject/remapping': 2.3.0 @@ -12753,6 +12968,17 @@ snapshots: optionalDependencies: '@swc/core': 1.3.96 + terser-webpack-plugin@5.6.1(postcss@8.5.15)(webpack@5.97.1(postcss@8.5.15)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.48.0 + webpack: 5.97.1(postcss@8.5.15) + optionalDependencies: + postcss: 8.5.15 + optional: true + terser@5.48.0: dependencies: '@jridgewell/source-map': 0.3.11 @@ -13187,6 +13413,46 @@ snapshots: - uglify-js optional: true + webpack@5.97.1(postcss@8.5.15): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.9 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.17.0 + browserslist: 4.28.4 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.24.1 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.2 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.3.3 + terser-webpack-plugin: 5.6.1(postcss@8.5.15)(webpack@5.97.1(postcss@8.5.15)) + watchpack: 2.5.2 + webpack-sources: 3.5.0 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + optional: true + whatwg-fetch@3.6.20: {} whatwg-url@5.0.0: diff --git a/server/dukang-api/src/modules/iam/auth.controller.ts b/server/dukang-api/src/modules/iam/auth.controller.ts index 97a6385..fdfa449 100644 --- a/server/dukang-api/src/modules/iam/auth.controller.ts +++ b/server/dukang-api/src/modules/iam/auth.controller.ts @@ -18,18 +18,24 @@ import { CurrentUser } from '../../common/decorators/current-user.decorator'; import { AuthUser } from '../../common/guards/jwt-auth.guard'; import { ClientApp } from '@dukang/shared-types'; +function resolveUserClientApp(req: Request): ClientApp { + const header = String(req.headers['x-client-app'] || '').trim(); + if (header === ClientApp.USER_MINI) return ClientApp.USER_MINI; + return ClientApp.USER_H5; +} + @Controller() export class UserAuthController { constructor(private readonly authService: AuthService) {} @Post('auth/session/bootstrap') - bootstrap(@Body() dto: BootstrapSessionDto) { - return this.authService.bootstrapSession(dto.deviceKey, ClientApp.USER_H5); + bootstrap(@Req() req: Request, @Body() dto: BootstrapSessionDto) { + return this.authService.bootstrapSession(dto.deviceKey, resolveUserClientApp(req)); } @Post('auth/token/refresh') - refresh(@Body() dto: RefreshTokenDto) { - return this.authService.refreshAccessToken(dto.refreshToken, ClientApp.USER_H5); + refresh(@Req() req: Request, @Body() dto: RefreshTokenDto) { + return this.authService.refreshAccessToken(dto.refreshToken, resolveUserClientApp(req)); } @Post('auth/sms/send') @@ -39,7 +45,7 @@ export class UserAuthController { const guestId = guest?.actorType === 'USER' ? guest.actorId : undefined; return this.authService.sendSms(dto.phone, dto.scene, { guestUserId: guestId, - clientApp: ClientApp.USER_H5, + clientApp: resolveUserClientApp(req), }); } @@ -48,13 +54,13 @@ export class UserAuthController { login(@Req() req: Request, @Body() dto: LoginSmsDto) { const guest = (req as Request & { user?: AuthUser }).user; const guestId = guest?.actorType === 'USER' ? guest.actorId : undefined; - return this.authService.loginUser(dto.phone, dto.code, ClientApp.USER_H5, guestId); + return this.authService.loginUser(dto.phone, dto.code, resolveUserClientApp(req), guestId); } @Post('auth/phone/bind') @UseGuards(JwtAuthGuard) - bindPhone(@CurrentUser() user: AuthUser, @Body() dto: BindPhoneDto) { - return this.authService.bindPhone(user.actorId, dto.phone, dto.code, ClientApp.USER_H5); + bindPhone(@Req() req: Request, @CurrentUser() user: AuthUser, @Body() dto: BindPhoneDto) { + return this.authService.bindPhone(user.actorId, dto.phone, dto.code, resolveUserClientApp(req)); } @Post('auth/login/wechat') @@ -62,32 +68,31 @@ export class UserAuthController { wechatLogin(@Req() req: Request, @Body() dto: LoginWechatDto) { const guest = (req as Request & { user?: AuthUser }).user; const guestId = guest?.actorType === 'USER' ? guest.actorId : undefined; - return this.authService.loginUserWechat( - dto.code, - ClientApp.USER_H5, - dto.platform ?? 'h5', - guestId, - ); + const clientApp = resolveUserClientApp(req); + const platform = dto.platform ?? (clientApp === ClientApp.USER_MINI ? 'mini' : 'h5'); + return this.authService.loginUserWechat(dto.code, clientApp, platform, guestId); } @Post('auth/wechat/bind-phone') - bindPhoneLegacy(@Body() dto: BindWechatPhoneDto) { + bindPhoneLegacy(@Req() req: Request, @Body() dto: BindWechatPhoneDto) { return this.authService.bindWechatPhone( dto.wxSessionKey, dto.phone, dto.code, - ClientApp.USER_H5, + resolveUserClientApp(req), ); } @Post('auth/wechat/bind') @UseGuards(JwtAuthGuard) - bindWechat(@CurrentUser() user: AuthUser, @Body() dto: BindWechatDto) { + bindWechat(@Req() req: Request, @CurrentUser() user: AuthUser, @Body() dto: BindWechatDto) { + const clientApp = resolveUserClientApp(req); + const platform = dto.platform ?? (clientApp === ClientApp.USER_MINI ? 'mini' : 'h5'); return this.authService.bindUserWechat( user.actorId, { code: dto.code, wxSessionKey: dto.wxSessionKey }, - ClientApp.USER_H5, - dto.platform ?? 'h5', + clientApp, + platform, ); }