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,
);
}