zjk
1 天以前 3e813eb0c5b3314a2799014db813814d9e5fd0dc
feat: 搭建用户端小程序核心功能与页面

新增底部原生tabbar及全套图标资源,配置pages.json路由与页面样式;新增首页、健康方案、我的订单等核心页面,完善地址管理、联系我们、缺货订单等业务页面;新增登录态校验工具、缺货订单业务工具与全局提示组件;修复登录跳转逻辑适配原生tabbar页面,调整本地开发API地址;新增App级tabbar角标同步逻辑。
18个文件已添加
6个文件已修改
3059 ■■■■■ 已修改文件
doc/design/用户端_首页与导航_20261012.md 359 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/App.vue 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/components/stock-order-toast/stock-order-toast.vue 190 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/config/index.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/pages.json 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/pages/address/add.vue 296 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/pages/address/list.vue 245 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/pages/contact/index.vue 194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/pages/health-plan/index.vue 260 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/pages/index/index.vue 246 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/pages/login/index.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/pages/order/index.vue 568 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/pages/profile/index.vue 485 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/pages/stock-order/list.vue 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/static/tabbar/tab-home-active.png 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/static/tabbar/tab-home.png 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/static/tabbar/tab-order-active.png 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/static/tabbar/tab-order.png 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/static/tabbar/tab-plan-active.png 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/static/tabbar/tab-plan.png 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/static/tabbar/tab-profile-active.png 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/static/tabbar/tab-profile.png 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/utils/request.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/utils/stock-order.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/design/用户端_首页与导航_20261012.md
New file
@@ -0,0 +1,359 @@
# 用户端小程序 首页 / 底部导航 / 缺货订单全局提示 开发设计文档
- 模块:`hugray-user-miniapp`(前端 UI 实现,本轮无后端改动)
- 日期:2026-10-12
- 需求来源:「参考图片以及 pen 源文件,实现首页以及底部 tab 的 ui 布局和样式,底部的缺货订单是一个全局的 toast,规则如图二,注意 icon 需要按照设计稿一比一还原」
- 设计稿权威依据:`doc/琥歌瑞0928.pen`(首页主稿 line 4485 起;底部导航示例 line 14259;待支付/缺货提示体系 line 12839、横向通栏实例 line 17545)
- 换算基准:设计稿 375pt 宽 → 750rpx(**pt × 2 = rpx**)
已确认决策(用户逐项确认):
| 序号 | 待澄清项 | 采用方案 |
| --- | --- | --- |
| 1 | 底部 tab 栏如何落地 | **新建 3 个占位页 + 注册路由**(健康方案 / 我的订单 / 个人中心),tab 可真实切换 |
| 2 | 首页顶部沉浸式导航 | **只自绘品牌 Logo**;状态栏、灵动岛、微信原生胶囊由系统与微信渲染,避免重叠 |
| 3 | 首页两张产品大图素材不在仓库 | **用渐变纯色块占位**,素材就绪后替换为 `image` |
| 4 | 缺货订单数量数据来源 | **先本地 mock 数据**(`utils/stock-order.js` 收口) |
---
## 一、功能概述
| 序号 | 功能 | 说明 |
| --- | --- | --- |
| 1 | 首页(`pages/index/index.vue`) | 沉浸式导航 + 明星产品展位(大图 / 轮播指示器 / 产品信息与行动区)+ 次席展位 |
| 2 | 底部导航栏(`pages.json` → `tabBar`) | **原生 tabBar**,4 个 Tab(代谢300 / 健康方案 / 我的订单 / 个人中心),选中态、个人中心角标 |
| 3 | 缺货订单全局提示(`components/stock-order-toast`) | **所有页面**均弹出;点击跳缺货订单列表页;单笔展示倒计时,多笔仅展示数量 |
缺货提示落地方式:小程序 `App.vue` 不支持渲染模板,故采用**全局组件 + 各页面引入**方案(不使用 tabBar 层能力)。
> 底部导航本轮明确采用**原生 tabBar**(在 `pages.json` 配置),不自定义组件;因此跳转统一用 `uni.switchTab`(仅 `reLaunch` 亦可,`navigateTo`/`redirectTo` 不可用)。
---
## 二、现状与关键约束
### 2.1 底部导航采用原生 tabBar
`pages.json` 原本**无 `tabBar` 配置**,本轮新增顶层 `tabBar` 字段:
- 首页 `navigationStyle: "custom"`(沉浸式,自绘品牌区);
- 底部导航为**原生 tabBar**(系统渲染于 webview 之上),页面内容区自动排除 tabBar 高度,页面内**无需为 tab 预留底部留白**;
- 原生 tabBar **仅接受本地图片文件**作为图标(不支持 base64 / SVG / 网络图)→ `static/tabbar/*.png`;
- 原生 tabBar **无 CSS 角标能力** → 用 `uni.setTabBarBadge` 设置。
### 2.2 项目无 UI 库 / 无 lucide 图标库
设计稿使用 lucide 图标库(`house` / `clipboard-check` / `package` / `circle-user-round` / `clipboard-list` / `chevron-right`),项目未引入该库。
- 方案:**内联 base64 SVG**(`background-image: url('data:image/svg+xml;base64,...')`)。
- 例外:**原生 tabBar 图标仅支持本地图片文件**,故 4 个 tab 图标(双态共 8 个)由 lucide SVG 光栅化为 81×81 PNG,落在 `static/tabbar/`。
- 先例:`pages/login/register.vue` 已使用同款 base64 data-URI 图标背景(class `.field-icon`),风格一致。
- 图标源:lucide 官方 SVG(`stroke-width=2`、`stroke-linecap/linejoin=round`),仅替换 `stroke` 颜色以适配双态。
### 2.3 产品大图素材缺失
设计稿引用的产品图(`微信图片_20260916202238_115_8451.png` 等)不在仓库 → 按决策以**同色系渐变块占位**,DOM 结构保留(便于后续替换为 `image`)。
### 2.4 缺货订单数量无后端接口
→ 本轮以本地 mock 收口在 `utils/stock-order.js`,接口就绪后仅替换 `getStockOrderCount()` 实现,调用方零改动。
### 2.5 登录态校验复用
各页面 `onShow` 统一调用 `utils/request.js` 的公共守卫 `ensureLogin()`(无 token 时 `uni.reLaunch` 至登录页)。
---
## 三、页面 / 路由变更(`pages.json`)
| 路径 | 页面 | 导航样式 | 说明 |
| --- | --- | --- | --- |
| `pages/index/index` | 首页 | `navigationStyle: "custom"` | 沉浸式,自绘品牌区 |
| `pages/health-plan/index` | 健康方案 | 原生标题栏「健康方案」 | tab 占位页(含 toast) |
| `pages/order/index` | 我的订单 | 原生标题栏「我的订单」 | 占位页 |
| `pages/profile/index` | 个人中心 | 原生标题栏「个人中心」 | 占位页 |
| `pages/contact/index` | 联系我们 | `navigationStyle: "custom"` | 顶部深蓝品牌区自绘,个人中心「联系我们」菜单跳转的目标页;**不在 tabBar 列表** |
| `pages/address/list` | 选择地址 | 原生标题栏「选择地址」(白底黑字) | 个人中心「收货地址」菜单跳转的目标页;**不在 tabBar 列表** |
| `pages/stock-order/list` | 缺货订单 | 原生标题栏「缺货订单」 | 缺货提示点击跳转的目标页;**不在 tabBar 列表** |
### 3.1 原生 tabBar 配置(`pages.json` → `tabBar`)
```json
{
  "color": "#64748B",
  "selectedColor": "#003E7E",
  "backgroundColor": "#FFFFFF",
  "borderStyle": "white",
  "list": [
    { "pagePath": "pages/index/index",       "text": "代谢300",  "iconPath": "static/tabbar/tab-home.png",    "selectedIconPath": "static/tabbar/tab-home-active.png" },
    { "pagePath": "pages/health-plan/index", "text": "健康方案", "iconPath": "static/tabbar/tab-plan.png",    "selectedIconPath": "static/tabbar/tab-plan-active.png" },
    { "pagePath": "pages/order/index",       "text": "我的订单", "iconPath": "static/tabbar/tab-order.png",   "selectedIconPath": "static/tabbar/tab-order-active.png" },
    { "pagePath": "pages/profile/index",     "text": "个人中心", "iconPath": "static/tabbar/tab-profile.png", "selectedIconPath": "static/tabbar/tab-profile-active.png" }
  ]
}
```
> `borderStyle` 仅支持 `black` / `white`,取 `white` 对应设计稿 `#F1F5F9` 淡描边。`iconPath` 路径不带前导斜杠。
---
## 四、视觉还原规格
### 4.1 首页(375×812pt,fill `#F8FAFC`)
| 元素 | 设计稿规格 | 实现值 |
| --- | --- | --- |
| 页面底色 | `#F8FAFC` | 同左;`.page` 100%×100vh,`overflow: hidden` |
| 品牌区(沉浸导航) | 位于产品大图视口内左上,状态栏下方 | absolute 叠加于大图上方 z-index 2;`padding-top = 状态栏高度`;左右 24rpx |
| 品牌 Logo | `metabolism-300-logo.png`(蓝「代谢」+ 绿「300」+ 绿波浪) | 168×88rpx(`aspectFit`) |
| 明星展位-大图视口 | 375×406pt,fill `#FFFFFF`,mode cover | H812rpx,渐变占位 `180° #D9E7F7 → #E9F1FB 26% → #FFFFFF 54% → #F4F7FB` |
| 轮播指示器(毛玻璃微胶囊) | x165 y376,H16 R8,fill `#FFFFFFB3`,stroke `#FFFFFF` 0.8,shadow `#0F172A12` y2 blur6,gap5 padding 0/7 | H32rpx R16rpx,`rgba(255,255,255,.7)` + 1rpx `#FFF` 描边 + `0 4rpx 12rpx` 阴影;居中、top 752rpx;gap10rpx、padding 0/14rpx |
| 指示条-非激活 | 4×4 R2 `#003E7E30` | 8×8rpx R4rpx `rgba(0,62,126,.19)` |
| 指示条-激活 | 14×4 R2 渐变 90° `#003E7E → #1E5EBC` | W28rpx H8rpx 渐变 90° `#003E7E → #1E5EBC` |
| 产品信息与行动区 | 375×58pt,fill `#FFFFFF`,上下 1 描边 `#F1F5F9`,shadow `#0F172A06` y2 blur8,padding 0/18,space_between | H116rpx,同左(描边 1rpx,padding 0/36rpx) |
| 英文型号编码 | `G&METS-II` 11pt 700 `#003E7E` 字距 1.2 | 22rpx 700 字距 2.4rpx `#003E7E` |
| 产品中文主名 | `超分子基团复合肽` 17.5pt 800 `#0F172A` 字距 -0.2 | 35rpx 800 字距 -0.4rpx `#0F172A` |
| 探索更多(胶囊) | H34 R17,渐变 90° `#82C236 → #639C1F`,shadow `#6AA02735` y3 blur8,gap6 padding 0/8/0/14 | H68rpx R34rpx,同渐变 + `0 6rpx 16rpx rgba(106,160,39,.21)`;文案 12.5pt 600 → 25rpx |
| 探索更多-箭头底托 | 20×20 R10 `#FFFFFF28`,内嵌 lucide `chevron-right` 12×12 `#FFFFFF` | 40×40rpx R20rpx `rgba(255,255,255,.16)`;箭头 24×24rpx base64 |
| 次席展位 | x0 y464,375×265pt,clip cover | H530rpx,渐变占位 `160° #4E3D94 → #6A5AC1 45% → #A99BE0` |
### 4.2 底部导航栏(原生 tabBar,375×83pt)
> 原生 tabBar 由系统渲染,**仅可通过 `pages.json` 配置有限属性**,以下为设计稿规格与原生能力的映射。
| 元素 | 设计稿规格 | 原生实现 | 差异说明 |
| --- | --- | --- | --- |
| 容器 | fill `#FFFFFF` | `backgroundColor: "#FFFFFF"` | 一致 |
| 顶部描边 | 1 描边 `#F1F5F9` | `borderStyle: "white"` | 原生仅 `black`/`white` 两档,取 `white`(系统绘制极淡描边) |
| 容器阴影 | shadow `#0F172A08` y-4 blur16 | **不支持** | 原生 tabBar 无阴影配置项 |
| Tab 组 | H48pt,4 等分 | 由系统按 `list` 数量等分 | 高度由系统控制(约 50px + 安全区) |
| 图标底托 | 26×26pt,选中态 fill `#003E7E14` | **不支持** | 原生 tabBar 无选中底托能力 |
| 图标 | 22×22pt,4 态:`house` / `clipboard-check` / `package` / `circle-user-round` | `static/tabbar/*.png`(81×81 PNG,系统缩放至 22pt 左右) | 未选中 `#94A3B8` / 选中 `#003E7E` |
| Tab 文案 | 11pt,选中 `#003E7E` 700 / 未选中 `#64748B` 500 | `color: "#64748B"`、`selectedColor: "#003E7E"` | 原生**不支持字重区分**,颜色一致 |
| 个人中心角标 | 14×14 R7 `#E53935`,白描边,白字 9/700 | `uni.setTabBarBadge({ index: 3, text })`(`App.vue` `onShow` 统一设置) | 样式由系统绘制,仅可传文本 |
> 冲突取舍:pen「首页 frame」与「组件示例」对选中底托不一致 → 因原生 tabBar 无底托能力,**统一以图标 + 文案变色表达选中态**。个人中心角标数量本轮 mock(默认 1)。
### 4.3 缺货订单全局提示(横向通栏标准态)
| 元素 | 设计稿规格 | 实现值 |
| --- | --- | --- |
| 容器 | 351×40pt,R20,渐变 135° `#FFFFFFF6 → #FFFFFFE0`,stroke `#FFFFFF` 1.2,shadow `#0F294222` y4 blur14 | 左右各 24rpx,H80rpx R40rpx;渐变 135° `rgba(255,255,255,.96) → rgba(255,255,255,.88)`;2rpx `#FFF` 描边;`0 8rpx 28rpx rgba(15,41,66,.13)` |
| 容器定位 | 悬浮于内容之上 | `position: fixed`;`bottom: calc(24rpx + 安全区)`;z-index 85 |
| 前置图标 | lucide `clipboard-list` 15×15 | 30×30rpx base64 `#0F172A` |
| 主文案 | 11pt 700 | 22rpx 700 `#0F172A`,单行省略 |
| 倒计时数字块 | 16×14 R3,红底白字 | 32×28rpx R6rpx `#E53935`,白字 19rpx 700;仅**单笔**缺货时展示 |
| 倒计时冒号 | 红 `#E53935` | 20rpx 700 `#E53935` |
| 前往支付按钮 | H25 R12.5,渐变 90° `#EF4444 → #DC2626` | H50rpx R25rpx,同渐变 + `0 3rpx 10rpx rgba(220,38,38,.21)`;文案 21rpx 700 |
| 按钮内箭头 | lucide `chevron-right` 9×9 `#FFFFFF` | 18×18rpx base64 |
---
## 五、缺货订单提示规则(按图二 / 需求原文)
> 原文:「如果有缺货订单,需要所有页面都能弹出提示。点击跳转到缺货订单列表页。有多个情况,展示具体数量,不显示倒计时」
| 场景 | 展示规则 | 文案 |
| --- | --- | --- |
| 无缺货订单 | 不展示 | — |
| 单笔缺货(count = 1) | 展示提示 + 支付倒计时(`MM:SS`) | 您有一个缺货订单待支付 |
| 多笔缺货(count > 1) | 展示提示 + **具体数量**,**不展示倒计时** | 您有 N 个缺货订单待支付 |
| 点击提示条 | 跳转缺货订单列表页 | `uni.navigateTo('/pages/stock-order/list')` |
实现要点(`components/stock-order-toast`):
- `visible = count > 0`;`titleText` 按 count 分支;
- 倒计时仅在 `count === 1` 时启动(`setInterval` 1s,初始 30 分钟),`onUnmounted` 清理定时器;
- 组件在**每个业务页面**模板引入。
---
## 六、图标清单(lucide 1:1 还原)
### 6.1 原生 tabBar 图标(PNG,`static/tabbar/`)
> 原生 tabBar 仅支持本地图片文件 → 由 lucide SVG 光栅化为 81×81 PNG(`stroke-width=2`、`round` 端点),仅替换 `stroke` 颜色。
| 图标 | 用途 | 文件(未选中 / 选中) | 颜色 |
| --- | --- | --- | --- |
| `house` | 代谢300 | `tab-home.png` / `tab-home-active.png` | `#94A3B8` / `#003E7E` |
| `clipboard-check` | 健康方案 | `tab-plan.png` / `tab-plan-active.png` | `#94A3B8` / `#003E7E` |
| `package` | 我的订单 | `tab-order.png` / `tab-order-active.png` | `#94A3B8` / `#003E7E` |
| `circle-user-round` | 个人中心 | `tab-profile.png` / `tab-profile-active.png` | `#94A3B8` / `#003E7E` |
### 6.2 内联 base64 SVG 图标
| 图标 | 用途 | 颜色 | 尺寸(rpx) |
| --- | --- | --- | --- |
| `clipboard-list` | 缺货提示-前置图标 | `#0F172A` | 30×30 |
| `chevron-right` | 探索更多 / 前往支付 | `#FFFFFF` | 24×24 / 18×18 |
实现:class 背景图 + `background-repeat: no-repeat; background-position: center; background-size: 100% 100%`。
---
## 七、文件清单与影响范围
新增:
- `static/tabbar/*.png`(原生 tabBar 图标,4 图标 × 双态 = 8 个 PNG)
- `components/stock-order-toast/stock-order-toast.vue`(全局缺货提示组件)
- `utils/stock-order.js`(缺货数量 mock 数据源 + 列表页路径常量)
- `pages/health-plan/index.vue`、`pages/order/index.vue`、`pages/profile/index.vue`(三个 tab 页,已按 pen 稿完整实现,详见第九章)
- `pages/contact/index.vue`(联系我们页,个人中心菜单跳转目标,详见 9.6)
- `pages/address/list.vue`(选择地址页,个人中心菜单跳转目标,详见 9.7)
- `pages/stock-order/list.vue`(缺货订单列表页占位)
修改:
- `pages/index/index.vue`(全量重写为沉浸式首页)
- `pages.json`(首页与个人中心 `navigationStyle: "custom"` + 4 条新路由 + 原生 `tabBar` 配置)
- `App.vue`(`onShow` 用 `uni.setTabBarBadge` 设置个人中心角标)
- `utils/request.js`(新增公共守卫 `ensureLogin()`)
移除:
- `components/app-tab-bar/`(自定义 tab 组件,已由原生 tabBar 取代);页面内 `<app-tab-bar>` 引用与底部留白同步删除
影响:登录 / 注册 / 注册成功页不受影响(仍走各自既有布局)。健康方案 / 我的订单页复用 `ensureLogin()`;个人中心需展示未登录态,不强制跳转。跳转 tab 页须用 `uni.switchTab`。
---
## 八、待办 / 遗留
| 序号 | 事项 | 状态 |
| --- | --- | --- |
| 1 | 首页两张产品大图素材入仓后替换渐变占位块 | 待素材 |
| 2 | 缺货订单数量接口 + 支付倒计时截止时间由后端下发 | 待接口(当前 mock) |
| 3 | 个人中心角标数量(未读)由后端下发 | 待接口(当前默认 1) |
| 4 | 缺货订单列表页真实内容 | 占位 |
| 5 | 「探索更多」产品详情页 | 占位(toast 提示) |
| 6 | 三页真实数据接口(订单列表 / 收益数值 / 会员等级与昵称) | 待接口(当前 mock) |
| 7 | 三页图片素材(方案卡、订单缩略图、用户头像)入仓后替换渐变占位 | 待素材 |
---
## 九、三个 Tab 页实现(健康方案 / 我的订单 / 个人中心)
- 需求来源:「后面的健康方案、我的订单以及个人中心都结合图片以及根据 pen 源文件中的内容实现」
- pen 依据:健康方案 `19179–19939`;我的订单 `19946–22330`;个人中心已登录 `22331–23709`、未登录 `23710–24963`
- 图片素材(方案卡 3 张 `微信图片_*.png`、订单缩略图、用户头像)**均不在仓库** → 统一以同色系渐变块占位,DOM 结构保留便于后续替换为 `image`
### 9.1 健康方案页(`pages/health-plan/index.vue`)
| 区块 | 设计规格(pt → rpx) | 实现要点 |
| --- | --- | --- |
| 分类药丸栏 | 375×44,padding 0/16,药丸间隙 8 | `.filter-bar`;3 档 `G&METS-II` / `HBBLF-2` / `NEARCLC-II` |
| 药丸选中态 | h28 R14,fill `#003E7E`,文字 11.5pt 700 `#FFFFFF` | `.pill--active`;未选中 fill `#EDF2F7`,文字 `#475569` |
| 滑动指示条 | 轨道 28×3 `#E2E8F0`,滑块 10×3 `#C5A566` | `.slider` / `.slider-thumb`,按档位步进 18rpx |
| 产品标题行 | 375×48,padding 0/16 | 副标 11pt 700 `#003E7E` letterSpacing 1.2 + 主名 17.5pt 800 `#0F172A` |
| 探索更多按钮 | h32 渐变 90° `#82C236→#639C1F` R16 | `.explore-btn` + 白 chevron 底托(同首页范式) |
| 方案卡片 | 343×160,R14,间隙 12 | 3 张渐变占位卡(`planList`),240rpx 高 |
### 9.2 我的订单页(`pages/order/index.vue`)
| 区块 | 设计规格 | 实现要点 |
| --- | --- | --- |
| 状态 Tab 栏 | 375×44 白底 + `#F1F5F9` 底描边 | 5 项:全部 / 待支付 / 待发货 / 待收货 / 售后;选中为 90° `#1E5EBC→#003E7E` 胶囊 |
| 二级筛选栏 | 375×44,两个 166×32 下拉 | 商品类型「全部」+ 时间区间「选择时间」(calendar 图标),未开发 → toast 占位 |
| 订单卡片 | 343 R12 白底,gap 12 padding 14 | 6 条 mock 覆盖多状态,`filteredOrderList` 按 Tab 过滤 |
| 状态徽章配色 | padding [2,7] R4,11.5pt 700 | 待支付 `#FEF2F2`/`#E05338`;履约中 `#ECFDF5`/`#10B981`;待发货·待收货 `#FFF7ED`/`#EA580C`;已完成·已取消 `#F1F5F9`/`#94A3B8` |
| 缩略图与操作 | 缩略图 40×40 R6,`•••` 更多指示 | 待支付:倒计时(16×14 `#E53935` 白字)+ 去支付按钮;其余:查看订单按钮 |
### 9.3 个人中心页(`pages/profile/index.vue`)
> **导航栏变更**:`pages.json` 中本页由系统导航栏改为 `navigationStyle: "custom"` —— 设计稿顶部无标题且页面底色为渐变,白底原生导航会造成视觉割裂;页面以 `statusBarHeight` + `.nav-holder`(92rpx)留出状态栏与胶囊区。
| 区块 | 设计规格 | 实现要点 |
| --- | --- | --- |
| 页面底色 | 175° 渐变 `#D6E6F8` 0 → `#C4DCF6` .18 → `#E8F1FC` .38 → `#F8FAFC` .58 → `#F8FAFC` 1 | `.page` |
| 头部信息区 | x20 335×72,居中,头像与文本间隙 14 | `.header` |
| 头像 | 70×70 R35 fill `#F1F5F9` + 2.5 白描边 | 已登录内嵌 `camera` 32×32 `#94A3B8`;未登录内嵌 `user` 34×34 `#CBD5E1` |
| 文本信息 | 昵称 18pt 700 `#0F172A` | 已登录「辉煌G891」+ 等级胶囊(`userLevel` 动态展示 L0~L4);未登录「点击登录 / 注册」+ 副标 12pt `#64748B` |
| 会员等级胶囊 | h22 渐变 90° `#FDE68A→#FCD34D→#F59E0B`,V 勋章 18×18 渐变 145° | `.level-pill` / `.level-medal`;等级文字 12.5pt 800 `#78350F` |
| 主按钮 | h32 渐变 90° `#1E5EBC→#003E7E` R16 | 已登录「编辑资料」/ 未登录「立即登录」 |
| 收益玻璃卡 | x16 343×74,180° 半透明白渐变 + blur20,R16 | 三栏等分:**今日 10.00 / 本月 400.00 / 累计 128000.00**;未登录统一 `0.00`;**未登录或 L0 普通用户不展示该卡** |
| 功能菜单卡 | x16 343×408,同款玻璃态,6 行 × 68(未登录 / L0 仅 2 行) | 行图标 20×20 渐变描边 `#3B82F6→#1D4ED8`,右箭头 16×16 `#94A3B8` |
| 菜单项与等级 | — | L1~L4 且已登录时展示 6 项:`share-2` 我的邀请码 / `crown` 会员中心 / `file-text` 仓库订单 / `badge-cent` 收入明细 / `map-pin` 收货地址 / `contact` 联系我们;**未登录或 L0 普通用户仅展示「收货地址」「联系我们」** |
| 仓库订单角标 | 18×18 `#EF4444` R9 文字 1 | **仅已登录态**展示 |
| ICP 区 | x16 343×40 居中 | `京ICP备2026092800号-1X` + 版权行;**内容不足一屏时贴页面底部**(`.page` 为纵向 flex,`.icp` 用 `margin-top: auto` 吸收剩余空间) |
> **用户等级与展示规则(L0~L4)**:由 `userLevel` 控制,`isRestrictedView = !isLoggedIn || userLevel === 'L0'`。**未登录或 L0 普通用户**在个人中心仅保留「收货地址」「联系我们」两行菜单,并隐藏收益数据卡;**已登录且 L1~L4** 展示全部内容。等级接口未联调(当前 mock `L4`),联调后由 `getUserInfo` 下发等级替换。
### 9.4 新增内联 base64 图标
| icon key | 用途 | 颜色 | 尺寸(rpx) |
| --- | --- | --- | --- |
| `chevron-down` | 订单二级筛选下拉 | `#94A3B8` | 26×26 |
| `calendar` | 订单时间区间筛选 | `#94A3B8` | 24×24 |
| `camera` | 个人中心已登录头像 | `#94A3B8` | 64×64 |
| `user` | 个人中心未登录头像 | `#CBD5E1` | 68×68 |
| `share-2` / `crown` / `file-text` / `badge-cent` / `map-pin` / `contact` | 个人中心 6 行菜单 | 渐变 `#3B82F6→#1D4ED8` | 40×40 |
| `chevron-right` | 菜单右箭头 | `#94A3B8` | 32×32 |
### 9.5 三页公共约定
- 三页模板均引入 `<stock-order-toast />`,保证缺货提示全局可见。
- 首页 / 健康方案 / 我的订单 / 个人中心:**均不校验登录态**,未登录可正常浏览(原 `ensureLogin()` 守卫已移除,仅缺货订单列表页保留)。
- 个人中心:`onShow` 读 `getToken()` 仅用于切换「已登录 / 未登录」展示态,不做跳转;未登录时点击头像 / 名称 / 主按钮直接 `uni.reLaunch` 至登录页,**点击菜单项先 `uni.showModal` 提示登录、确认后跳转**。
- 订单列表、收益数值、用户昵称、用户等级均为本地 mock,待 `getOrderList` / `getMemberInfo` / `getUserInfo` 就绪后替换。
- 「探索更多 / 二级筛选 / 去支付 / 查看订单 / 编辑资料」本轮统一为 toast 占位(菜单项仅在已登录态为占位)。
### 9.6 联系我们页(`pages/contact/index.vue`)
- 需求来源:「联系我们的原型如图,参考 ui 设计风格实现」
- 入口:个人中心菜单项「联系我们」(`menu.key === 'contact'`)→ `uni.navigateTo('/pages/contact/index')`;**未登录仍先 `uni.showModal` 提示登录**(沿用个人中心菜单统一拦截规则)
- 导航:`pages.json` 注册 `navigationStyle: "custom"` —— 顶部为深蓝品牌色块,白底原生导航会造成视觉割裂
| 区块 | 设计规格 | 实现要点 |
| --- | --- | --- |
| 顶部深蓝面板 | 纯色 `#0B3F6F`(取自 logo.png 底色) | `.top-panel`(`paddingTop` = `statusBarHeight`);导航栏与 Hero 区共用底色形成连续色块,且与 Logo 图圆角边缘无缝融合 |
| 导航栏 | h44,返回箭头左置 x8,标题居中 | `.nav`;返回箭头为**纯 CSS**(左/下边框旋转 45°)避免额外 base64,白色 11pt 700 |
| 品牌 Hero 区 | h200,居中展示品牌 Logo | `<image class="brand-logo" src="/static/logo.png" mode="widthFix" />`,宽 170pt,高度按原图比例自适应 |
| 联系方式列表 | x20,列表项纵向间距 44 | `.info-list`(`padding: 140rpx 40rpx 0`) |
| 单行信息 | 标签列宽 110pt,右侧数值 | `.info-label` 16pt 600 `#1F2937`;`.info-value` 15pt `#6B7280`;固定标签列宽保证各行数值纵向对齐 |
| 平台客服按钮 | h30 R15,**pen 标准「微信按钮 WeChat」** 渐变(90°)`#07C160→#05A352` + 投影 `#07C16040` | `<button open-type="contact">`(微信客服会话)+ 内嵌 `/static/wechat-icon.png` + 白字 13pt 600;`::after { border: none }` 重置小程序默认边框 |
| 热线文案 | `城市招商热线` / `合伙人招募` 均 `400-451-666` | 客服接口未就绪,先以文件内常量 `CITY_INVEST_HOTLINE` / `PARTNER_RECRUIT_HOTLINE` 维护,接口就绪后替换 |
| 返回行为 | `uni.navigateBack()` | 无上级页面(如直接进入)时兜底 `uni.switchTab('/pages/profile/index')` |
### 9.7 选择地址页(`pages/address/list.vue`)
- 需求来源:「这是收货地址的原型,参考 ui 规范和设计风格实现收货地址的页面」
- 入口:个人中心菜单项「收货地址」(`menu.key === 'address'`)→ `uni.navigateTo('/pages/address/list')`;未登录仍先 `uni.showModal` 提示登录
- 导航:`pages.json` 注册原生标题栏「选择地址」(白底黑字)
| 区块 | 设计规格 | 实现要点 |
| --- | --- | --- |
| 地址条目 | 纵向 `padding: 36rpx 40rpx`,无分割线 | `.addr-item` 横向 flex,文本区 `flex: 1; min-width: 0` 保证长地址可换行 |
| 姓名 + 手机号 | 姓名 16pt 600 `#1F2937`;手机号 15pt `#374151` | `.addr-head` 横向 flex,`gap: 56rpx`;手机号按规范脱敏展示(`185****0110`) |
| 默认标记 | 蓝框小标签(`#1E5EBC`,描边 1pt,圆角 3pt) | `.addr-tag`,仅 `isDefault` 为 true 时渲染 |
| 地址详情 | 14pt `#6B7280`,行高 1.45,最多两行 | `.addr-detail` |
| 编辑入口 | 右上角图标 22×22,`#1E5EBC` | 内联 base64 SVG `square-pen`,点击为 toast 占位(编辑页未实现) |
| 底部操作栏 | 白色底 + 绿色胶囊按钮(H48 → 96rpx,PILL R24 → 48rpx),白字 + 白色加号 | 尺寸/圆角/文字与登录页 `.btn` 完全一致;`.add-bar` 固定底部,左右 `56rpx` 内边距 + `padding-bottom: calc(32rpx + env(safe-area-inset-bottom))`(胶囊不覆盖 Home 指示条);按钮采用 **pen 标准「草绿动能行动」** 渐变(90°)`#82C236→#639C1F` + 投影 `#6AA02735`,与首页 / 健康方案「探索更多」按钮同源 |
| 空态占位 | 无地址时居中展示:灰底圆形(d100 `#F1F5F9`)+ 灰色 `map-pin` 图标(48×48 `#CBD5E1`)+ 主文案「暂无收货地址」15pt 600 `#475569` + 副文案「点击下方按钮添加收货地址」13pt `#94A3B8` | `v-if="addressList.length"` 渲染列表,`v-else` 渲染空态;底部「新增地址」按钮两种状态均保留 |
| 数据来源 | 三条 mock 地址(张三为默认 / 李四 / 王五) | 地址列表接口就绪后替换 `MOCK_ADDRESS_LIST` |
| 新增入口 | 底部「新增地址」按钮 → `uni.navigateTo('/pages/address/add')` | `handleAdd` 由 toast 占位改为跳转新增地址页(见 9.8) |
### 9.8 新增地址页(`pages/address/add.vue`)
- 需求来源:「点击新增地址时跳转地址新增页面,注意我给你的图片只是原型不是真正的ui设计,需要你参考 `琥歌瑞0928.pen` 的设计风格去设计然后实现」
- 入口:选择地址页底部「新增地址」按钮 → `uni.navigateTo('/pages/address/add')`
- 导航:`pages.json` 注册原生标题栏「新增地址」(白底黑字),与选择地址页同规格
- **设计原则**:原型图仅提供字段结构(收件人 / 手机号 / 收件地区 / 详细地址 / 设为默认 / 提交);视觉按 pen「FORM」组件规范落地,并按要求做简化——**保留字段前缀图标**、输入框**只保留下边框**
| 区块 | 设计规格 | 实现要点 |
| --- | --- | --- |
| 表单区 | 左右 `56rpx` 内边距(与底部提交栏对齐),首项距导航 `40rpx`,项间距 `40rpx` | `.form-area` / `.field-block`,复用 `pages/login/register.vue` 表单结构 |
| 字段标签 | 12.5pt → `26rpx`,600 `#334155`,与输入框间距 `20rpx` | `.field-label`(pen「标签」规范) |
| 输入框 | 高度 H44 → `88rpx`;**只保留下边框**:`border-bottom: 2rpx solid #CBD5E1`,去掉左/右/上三边与圆角,白底、左右内边距 `4rpx`,视觉更轻 | `.input-wrap` |
| 前缀图标 | 每个字段左侧 `16pt` → `40rpx` 图标(`#64748B`,内联 base64 SVG,与输入框间距 `20rpx`):收件人 `user` / 手机号 `smartphone` / 收件地区 `map` / 详细地址 `map-pin` | `.field-icon` + `.icon-user` / `.icon-phone` / `.icon-map` / `.icon-pin` |
| 收件地区 | 点击行唤起原生省市区选择器,右侧 `chevron-right`(`#94A3B8`)暗示可展开;未选择时展示占位文案 `#94A3B8` | `<picker mode="region">` 包裹 `.input-wrap`;`.field-input--placeholder` 控制占位色 |
| 设为默认 | pen 无开关组件规范,取品牌蓝 `#003E7E`(与 tabBar 选中色同源) | `<switch :checked color="#003E7E" @change>`,默认关闭 |
| 底部提交栏 | 白色底 + 绿色胶囊按钮(H48 → `96rpx`,PILL R24 → `48rpx`),白字;采用 pen「草绿动能行动」渐变(90°)`#82C236→#639C1F`,**去掉投影**保持扁平简洁 | `.submit-bar` 固定底部(左右 `56rpx` + `padding-bottom: calc(32rpx + env(safe-area-inset-bottom))`),与该页「新增地址」入口按钮、首页/健康方案「探索更多」同源 |
| 表单校验 | 收件人非空 → 手机号非空且匹配 `^1[3-9]\d{9}$` → 地区非空 → 详细地址非空,逐项 toast 提示 | `_validateForm()` 返回失败原因,通过后 `_handleSubmit` 保存并 `uni.navigateBack()` |
| 数据来源 | 地址新增接口未就绪,提交成功后本地 toast「保存成功」并返回列表页 | 接口就绪后替换 `_handleSubmit` 内的占位反馈 |
hugray-user-miniapp/App.vue
@@ -1,15 +1,37 @@
<script>
    export default {
        onLaunch: function() {
            console.log('App Launch')
        },
        onShow: function() {
            console.log('App Show')
        },
        onHide: function() {
            console.log('App Hide')
// 个人中心 tab 在 pages.json → tabBar.list 中的索引(0 起)
const PROFILE_TAB_INDEX = 3
// TODO: 后端「个人中心待处理/未读数量」接口就绪后替换为真实请求
const MOCK_PROFILE_BADGE_COUNT = 1
export default {
    onLaunch: function() {
        console.log('App Launch')
    },
    onShow: function() {
        console.log('App Show')
        this._syncTabBarBadge()
    },
    onHide: function() {
        console.log('App Hide')
    },
    methods: {
        // 同步个人中心 tab 原生角标(原生 tabBar 无法用 CSS 实现角标)
        _syncTabBarBadge() {
            if (MOCK_PROFILE_BADGE_COUNT > 0) {
                uni.setTabBarBadge({
                    index: PROFILE_TAB_INDEX,
                    text: String(MOCK_PROFILE_BADGE_COUNT),
                    fail: (err) => {
                        console.error('[TABBAR_BADGE] set failed', err)
                    }
                })
            } else {
                uni.removeTabBarBadge({ index: PROFILE_TAB_INDEX })
            }
        }
    }
}
</script>
<style>
hugray-user-miniapp/components/stock-order-toast/stock-order-toast.vue
New file
@@ -0,0 +1,190 @@
<template>
    <view v-if="visible" class="stock-toast" @tap="_handleGoList">
        <!-- 左侧:图标 + 文案(单笔缺货额外展示支付倒计时) -->
        <view class="stock-toast-left">
            <view class="stock-toast-icon"></view>
            <text class="stock-toast-title">{{ titleText }}</text>
            <!-- 单笔缺货:展示支付倒计时;多笔缺货:仅展示具体数量,不展示倒计时 -->
            <view v-if="count === 1" class="stock-toast-countdown">
                <view class="countdown-block">{{ minuteText }}</view>
                <text class="countdown-colon">:</text>
                <view class="countdown-block">{{ secondText }}</view>
            </view>
        </view>
        <!-- 右侧:前往支付按钮 -->
        <view class="stock-toast-btn">
            <text class="stock-toast-btn-text">前往支付</text>
            <view class="stock-toast-btn-arrow"></view>
        </view>
    </view>
</template>
<script setup>
/**
 * 全局缺货订单提示(Toast)
 * 规则:存在缺货订单的所有页面均弹出;点击跳转缺货订单列表页;
 *      单笔展示支付倒计时,多笔仅展示具体数量、不展示倒计时。
 * 用法:在各业务页面模板中直接引入 <stock-order-toast /> 即可。
 */
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { getStockOrderCount, STOCK_ORDER_LIST_PAGE } from '@/utils/stock-order.js'
/** 缺货订单数量(Mock 数据源,接口就绪后由数据层替换) */
const count = ref(getStockOrderCount())
/** 是否存在缺货订单 */
const visible = computed(() => count.value > 0)
/** 提示文案:单笔用「一个」,多笔展示具体数量 */
const titleText = computed(() =>
    count.value > 1 ? `您有${count.value}个缺货订单待支付` : '您有一个缺货订单待支付'
)
// ======================== 支付倒计时(仅单笔缺货时展示) ========================
/** 倒计时初始剩余秒数(Mock 30 分钟) */
const COUNTDOWN_SECONDS = 30 * 60
const remainSeconds = ref(COUNTDOWN_SECONDS)
let countdownTimer = null
/** 剩余分钟(补零两位) */
const minuteText = computed(() => String(Math.floor(remainSeconds.value / 60)).padStart(2, '0'))
/** 剩余秒数(补零两位) */
const secondText = computed(() => String(remainSeconds.value % 60).padStart(2, '0'))
/** 启动倒计时(多笔缺货时按规则不展示倒计时,无需启动) */
const _startCountdown = () => {
    if (count.value !== 1) return
    countdownTimer = setInterval(() => {
        if (remainSeconds.value <= 0) {
            clearInterval(countdownTimer)
            countdownTimer = null
            return
        }
        remainSeconds.value -= 1
    }, 1000)
}
onMounted(_startCountdown)
onUnmounted(() => {
    if (countdownTimer) clearInterval(countdownTimer)
})
/** 跳转缺货订单列表页 */
const _handleGoList = () => {
    uni.navigateTo({ url: STOCK_ORDER_LIST_PAGE })
}
</script>
<style scoped lang="scss">
/* 通栏胶囊:距原生 tabBar 24rpx,左右各 24rpx,毛玻璃白底 + 柔和投影 */
.stock-toast {
    position: fixed;
    left: 24rpx;
    right: 24rpx;
    /* 原生 tabBar 由系统渲染在 webview 之外,内容区 bottom 即 tabBar 上沿 */
    bottom: calc(24rpx + constant(safe-area-inset-bottom));
    bottom: calc(24rpx + env(safe-area-inset-bottom));
    z-index: 85;
    height: 80rpx;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 16rpx 0 24rpx;
    border-radius: 40rpx;
    background: linear-gradient(135deg, rgba(255, 255, 255, 0.96) 0%, rgba(255, 255, 255, 0.88) 100%);
    border: 2rpx solid #ffffff;
    box-shadow: 0 8rpx 28rpx rgba(15, 41, 66, 0.13);
}
.stock-toast-left {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
}
/* 前置图标:lucide clipboard-list 15×15pt */
.stock-toast-icon {
    width: 30rpx;
    height: 30rpx;
    margin-right: 13rpx;
    flex-shrink: 0;
    background-repeat: no-repeat;
    background-position: center center;
    background-size: 100% 100%;
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMEYxNzJBIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHJlY3Qgd2lkdGg9IjgiIGhlaWdodD0iNCIgeD0iOCIgeT0iMiIgcng9IjEiIHJ5PSIxIi8+PHBhdGggZD0iTTE2IDRoMmEyIDIgMCAwIDEgMiAydjE0YTIgMiAwIDAgMS0yIDJINmEyIDIgMCAwIDEtMi0yVjZhMiAyIDAgMCAxIDItMmgyIi8+PHBhdGggZD0iTTEyIDExaDQiLz48cGF0aCBkPSJNMTIgMTZoNCIvPjxwYXRoIGQ9Ik04IDExaC4wMSIvPjxwYXRoIGQ9Ik04IDE2aC4wMSIvPjwvc3ZnPg==');
}
.stock-toast-title {
    font-size: 22rpx;
    font-weight: 700;
    line-height: 1;
    color: #0f172a;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* 倒计时组合:红底数字块 + 冒号 */
.stock-toast-countdown {
    display: flex;
    align-items: center;
    margin-left: 13rpx;
    flex-shrink: 0;
}
.countdown-block {
    width: 32rpx;
    height: 28rpx;
    border-radius: 6rpx;
    background: #e53935;
    color: #ffffff;
    font-size: 19rpx;
    font-weight: 700;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
}
.countdown-colon {
    margin: 0 4rpx;
    font-size: 20rpx;
    font-weight: 700;
    line-height: 1;
    color: #e53935;
}
/* 前往支付按钮:红色渐变胶囊 */
.stock-toast-btn {
    flex-shrink: 0;
    height: 50rpx;
    padding: 0 18rpx;
    border-radius: 25rpx;
    display: flex;
    align-items: center;
    background: linear-gradient(90deg, #ef4444 0%, #dc2626 100%);
    box-shadow: 0 3rpx 10rpx rgba(220, 38, 38, 0.21);
}
.stock-toast-btn-text {
    font-size: 21rpx;
    font-weight: 700;
    line-height: 1;
    color: #ffffff;
}
/* 按钮内箭头:lucide chevron-right 9×9pt */
.stock-toast-btn-arrow {
    width: 18rpx;
    height: 18rpx;
    margin-left: 4rpx;
    background-repeat: no-repeat;
    background-position: center center;
    background-size: 100% 100%;
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0ibTkgMTggNi02LTYtNiIvPjwvc3ZnPg==');
}
</style>
hugray-user-miniapp/config/index.js
@@ -9,7 +9,7 @@
// 各环境请求根地址(统一指向 RuoYi 网关,由网关按前缀转发至各微服务)
const BASE_URL_MAP = {
    // 本地联调:网关默认端口 8085
    development: 'http://localhost:8085',
    development: 'http://192.168.110.150:8085',
    // 生产环境地址,上线前替换为正式域名
    production: 'https://api.hugray.com'
}
hugray-user-miniapp/pages.json
@@ -3,7 +3,61 @@
        {
            "path": "pages/index/index",
            "style": {
                "navigationBarTitleText": ""
                "navigationStyle": "custom"
            }
        },
        {
            "path": "pages/health-plan/index",
            "style": {
                "navigationBarTitleText": "健康方案",
                "navigationBarBackgroundColor": "#ffffff",
                "navigationBarTextStyle": "black"
            }
        },
        {
            "path": "pages/order/index",
            "style": {
                "navigationBarTitleText": "我的订单",
                "navigationBarBackgroundColor": "#ffffff",
                "navigationBarTextStyle": "black"
            }
        },
        {
            "path": "pages/profile/index",
            // 个人中心顶部无标题且底色为渐变,与白底原生导航冲突,故使用自定义导航栏
            "style": {
                "navigationStyle": "custom"
            }
        },
        {
            "path": "pages/contact/index",
            // 顶部为深蓝底品牌区,与白底原生导航冲突,故使用自定义导航栏
            "style": {
                "navigationStyle": "custom"
            }
        },
        {
            "path": "pages/address/list",
            "style": {
                "navigationBarTitleText": "选择地址",
                "navigationBarBackgroundColor": "#ffffff",
                "navigationBarTextStyle": "black"
            }
        },
        {
            "path": "pages/address/add",
            "style": {
                "navigationBarTitleText": "新增地址",
                "navigationBarBackgroundColor": "#ffffff",
                "navigationBarTextStyle": "black"
            }
        },
        {
            "path": "pages/stock-order/list",
            "style": {
                "navigationBarTitleText": "缺货订单",
                "navigationBarBackgroundColor": "#ffffff",
                "navigationBarTextStyle": "black"
            }
        },
        {
@@ -33,5 +87,38 @@
        "navigationBarBackgroundColor": "#F8F8F8",
        "backgroundColor": "#F8F8F8"
    },
    // 原生底部导航栏:图标仅支持本地图片文件(不支持 base64 / SVG / 网络图)
    "tabBar": {
        "color": "#64748B", // 未选中文案色
        "selectedColor": "#003E7E", // 选中文案色
        "backgroundColor": "#FFFFFF", // 背景色
        "borderStyle": "white", // 仅支持 black / white,对应设计稿 #F1F5F9 顶描边
        "list": [
            {
                "pagePath": "pages/index/index",
                "text": "代谢300",
                "iconPath": "static/tabbar/tab-home.png",
                "selectedIconPath": "static/tabbar/tab-home-active.png"
            },
            {
                "pagePath": "pages/health-plan/index",
                "text": "健康方案",
                "iconPath": "static/tabbar/tab-plan.png",
                "selectedIconPath": "static/tabbar/tab-plan-active.png"
            },
            {
                "pagePath": "pages/order/index",
                "text": "我的订单",
                "iconPath": "static/tabbar/tab-order.png",
                "selectedIconPath": "static/tabbar/tab-order-active.png"
            },
            {
                "pagePath": "pages/profile/index",
                "text": "个人中心",
                "iconPath": "static/tabbar/tab-profile.png",
                "selectedIconPath": "static/tabbar/tab-profile-active.png"
            }
        ]
    },
    "uniIdRouter": {}
}
hugray-user-miniapp/pages/address/add.vue
New file
@@ -0,0 +1,296 @@
<template>
    <view class="page">
        <!-- 新增地址表单:字段结构对齐原型(收件人 / 手机号 / 收件地区 / 详细地址 / 设为默认 / 提交),
             视觉按 pen「FORM」规范:标签 #334155 600、输入行 H88、前缀图标 #64748B、仅保留下边框 -->
        <view class="form-area">
            <!-- 收件人 -->
            <view class="field-block">
                <text class="field-label">收件人</text>
                <view class="input-wrap">
                    <view class="field-icon icon-user"></view>
                    <input
                        v-model="form.name"
                        class="field-input"
                        maxlength="20"
                        placeholder="请输入收件人姓名"
                        placeholder-style="color:#94A3B8;font-size:28rpx"
                    />
                </view>
            </view>
            <!-- 手机号 -->
            <view class="field-block">
                <text class="field-label">手机号</text>
                <view class="input-wrap">
                    <view class="field-icon icon-phone"></view>
                    <input
                        v-model="form.phone"
                        class="field-input"
                        type="number"
                        maxlength="11"
                        placeholder="请输入收件人手机号"
                        placeholder-style="color:#94A3B8;font-size:28rpx"
                    />
                </view>
            </view>
            <!-- 收件地区:点击唤起原生省市区选择器,右侧箭头暗示可展开 -->
            <view class="field-block">
                <text class="field-label">收件地区</text>
                <picker mode="region" :value="regionValue" @change="_handleRegionChange">
                    <view class="input-wrap">
                        <view class="field-icon icon-map"></view>
                        <text class="field-input field-input--value" :class="{ 'field-input--placeholder': !regionText }">
                            {{ regionText || '请选择收件地区' }}
                        </text>
                        <view class="field-arrow"></view>
                    </view>
                </picker>
            </view>
            <!-- 详细地址 -->
            <view class="field-block">
                <text class="field-label">详细地址</text>
                <view class="input-wrap">
                    <view class="field-icon icon-pin"></view>
                    <input
                        v-model="form.detail"
                        class="field-input"
                        maxlength="60"
                        placeholder="请输入详细收件地址"
                        placeholder-style="color:#94A3B8;font-size:28rpx"
                    />
                </view>
            </view>
            <!-- 设为默认:pen 中无开关组件规范,取品牌蓝 #003E7E(与 tabBar 选中色同源) -->
            <view class="switch-row">
                <text class="switch-label">设为默认</text>
                <switch :checked="form.isDefault" color="#003E7E" @change="_handleDefaultChange" />
            </view>
        </view>
        <!-- 底部固定提交栏:尺寸/圆角与地址列表页「新增地址」按钮一致 -->
        <view class="submit-bar">
            <view class="submit-btn" @tap="_handleSubmit">提交</view>
        </view>
    </view>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
// ======================== 数据定义区域 ========================
// 新增地址表单
const form = reactive({
    name: '',
    phone: '',
    region: [],       // 省 / 市 / 区三级,来源为 region picker
    detail: '',
    isDefault: false
})
// 地区选择器回显值(picker 的 value 需与当前选中的三级数组保持一致)
const regionValue = ref([])
// 地区文本:拼接为「省 市 区」,为空时由模板展示占位文案
const regionText = computed(() => form.region.join(' '))
// ======================== 方法定义区域 ========================
/** 切换「设为默认」 */
const _handleDefaultChange = (e) => {
    form.isDefault = e.detail.value
}
/** 选择收件地区:回写三级数组,用于文本展示与 picker 回显 */
const _handleRegionChange = (e) => {
    const region = e.detail.value || []
    form.region = region
    regionValue.value = region
}
/** 表单校验,返回校验失败原因(通过则返回空串) */
const _validateForm = () => {
    if (!form.name.trim()) return '请输入收件人姓名'
    if (!form.phone.trim()) return '请输入收件人手机号'
    if (!/^1[3-9]\d{9}$/.test(form.phone.trim())) return '请输入正确手机号'
    if (!form.region.length) return '请选择收件地区'
    if (!form.detail.trim()) return '请输入详细收件地址'
    return ''
}
/** 提交:校验通过后保存并返回地址列表页(地址新增接口未就绪,暂以本地成功反馈占位) */
const _handleSubmit = () => {
    const errorMsg = _validateForm()
    if (errorMsg) {
        uni.showToast({ title: errorMsg, icon: 'none' })
        return
    }
    uni.showToast({ title: '保存成功', icon: 'none' })
    setTimeout(() => uni.navigateBack(), 600)
}
</script>
<style scoped lang="scss">
.page {
    min-height: 100vh;
    box-sizing: border-box;
    background-color: #ffffff;
    /* 预留底部固定提交栏高度(16+96+32rpx),避免表单被遮挡 */
    padding-bottom: calc(144rpx + env(safe-area-inset-bottom));
}
/* ==================== 表单区 ==================== */
/* 左右 56rpx 与底部提交栏对齐,保证标签左边缘与按钮左边缘在同一条竖线上 */
.form-area {
    padding: 40rpx 56rpx 0;
    box-sizing: border-box;
}
/* 单个输入项:统一上间距 40rpx,首项与导航栏保持 40rpx */
.field-block {
    margin-top: 40rpx;
    &:first-child {
        margin-top: 0;
    }
}
.field-label {
    display: block;
    font-size: 26rpx;
    font-weight: 600;
    color: #334155;
    margin-bottom: 20rpx;
}
/* 输入行:白底,仅保留下边框,视觉更轻 */
.input-wrap {
    display: flex;
    align-items: center;
    height: 88rpx;
    padding: 0 4rpx;
    border-bottom: 2rpx solid #cbd5e1;
    box-sizing: border-box;
}
.field-icon {
    width: 40rpx;
    height: 40rpx;
    margin-right: 20rpx;
    flex-shrink: 0;
    background-repeat: no-repeat;
    background-position: center center;
    background-size: 100% 100%;
}
.field-input {
    flex: 1;
    height: 100%;
    font-size: 28rpx;
    color: #1e293b;
    min-width: 0;
}
/* 未选择地区时,文本行按占位色展示 */
.field-input--placeholder {
    color: #94a3b8;
}
/* 地区值为 text 节点(非 input),用 flex 垂直居中,避免文字偏下 */
.field-input--value {
    display: flex;
    align-items: center;
}
/* 地区选择右侧箭头(chevron-right) */
.field-arrow {
    width: 32rpx;
    height: 32rpx;
    flex-shrink: 0;
    margin-left: 16rpx;
    background-repeat: no-repeat;
    background-position: center center;
    background-size: 100% 100%;
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjOTRBM0I4IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0ibTkgMTggNi02LTYtNiIvPjwvc3ZnPg==');
}
/* ==================== 设为默认 ==================== */
.switch-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-top: 48rpx;
}
.switch-label {
    font-size: 26rpx;
    font-weight: 600;
    color: #334155;
}
/* ==================== 底部固定提交栏 ==================== */
.submit-bar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    /* 与地址列表页一致:左右 56rpx 内边距 + 安全区留白 */
    padding: 16rpx 56rpx calc(32rpx + env(safe-area-inset-bottom));
    background-color: #ffffff;
}
/* 尺寸/圆角与地址列表页「新增地址」按钮一致:H48 → 96rpx,PILL R24 → 48rpx */
.submit-btn {
    width: 100%;
    height: 96rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 48rpx;
    box-sizing: border-box;
    /* pen 标准「草绿动能行动」渐变(90°) */
    background: linear-gradient(90deg, #82c236 0%, #639c1f 100%);
    font-size: 30rpx;
    font-weight: 600;
    line-height: 1;
    color: #ffffff;
}
/* ==================== 图标(内联 base64 SVG) ==================== */
.icon-user,
.icon-phone,
.icon-map,
.icon-pin {
    background-repeat: no-repeat;
    background-position: center center;
    background-size: 100% 100%;
}
/* 收件人:单人轮廓 */
.icon-user {
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNjQ3NDhCIiBzdHJva2Utd2lkdGg9IjEuOSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMTkgMjF2LTJhNCA0IDAgMCAwLTQtNEg5YTQgNCAwIDAgMC00IDR2MiIvPjxjaXJjbGUgY3g9IjEyIiBjeT0iNyIgcj0iNCIvPjwvc3ZnPg==');
}
/* 手机号:手机外框 + 底部听筒点 */
.icon-phone {
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNjQ3NDhCIiBzdHJva2Utd2lkdGg9IjEuOSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cmVjdCB3aWR0aD0iMTMiIGhlaWdodD0iMjAiIHg9IjUuNSIgeT0iMiIgcng9IjIuNSIvPjxwYXRoIGQ9Ik0xMSAxOGgyIi8+PC9zdmc+');
}
/* 收件地区:地图折页 */
.icon-map {
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNjQ3NDhCIiBzdHJva2Utd2lkdGg9IjEuOSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cG9seWdvbiBwb2ludHM9IjMgNiA5IDMuNSAxNSA2IDIxIDMuNSAyMSAxOCAxNSAyMC41IDkgMTggMyAyMC41Ii8+PGxpbmUgeDE9IjkiIHgyPSI5IiB5MT0iMy41IiB5Mj0iMTgiLz48bGluZSB4MT0iMTUiIHgyPSIxNSIgeTE9IjYiIHkyPSIyMC41Ii8+PC9zdmc+');
}
/* 详细地址:定位图钉 */
.icon-pin {
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNjQ3NDhCIiBzdHJva2Utd2lkdGg9IjEuOSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMjAgMTBjMCA2LTggMTItOCAxMnMtOC02LTgtMTJhOCA4IDAgMCAxIDE2IDBaIi8+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMCIgcj0iMyIvPjwvc3ZnPg==');
}
</style>
hugray-user-miniapp/pages/address/list.vue
New file
@@ -0,0 +1,245 @@
<template>
    <view class="page">
        <!-- 有数据:渲染地址列表,右侧为编辑入口 -->
        <view v-if="addressList.length">
            <view v-for="item in addressList" :key="item.id" class="addr-item">
                <view class="addr-main">
                    <!-- 姓名 + 手机号 -->
                    <view class="addr-head">
                        <text class="addr-name">{{ item.name }}</text>
                        <text class="addr-phone">{{ item.phone }}</text>
                    </view>
                    <!-- 默认地址标记 -->
                    <view v-if="item.isDefault" class="addr-tag">默认</view>
                    <text class="addr-detail">{{ item.detail }}</text>
                </view>
                <view class="icon icon-edit" @tap="handleEdit(item)"></view>
            </view>
        </view>
        <!-- 空态占位:无收货地址时展示,引导用户新增 -->
        <view v-else class="addr-empty">
            <view class="empty-icon-wrap">
                <view class="icon icon-empty"></view>
            </view>
            <text class="empty-title">暂无收货地址</text>
            <text class="empty-desc">点击下方按钮添加收货地址</text>
        </view>
        <!-- 底部固定操作栏:新增地址 -->
        <view class="add-bar">
            <view class="add-btn" @tap="handleAdd">
                <view class="icon icon-plus"></view>
                <text class="add-text">新增地址</text>
            </view>
        </view>
    </view>
</template>
<script setup>
import { ref } from 'vue'
/** 收货地址列表:地址接口未就绪,先本地 mock(联调后由收货地址列表接口下发);手机号按规范脱敏展示 */
const MOCK_ADDRESS_LIST = [
    {
        id: '1',
        name: '张三',
        phone: '185****0110',
        detail: '四川省成都市武侯区府城大道399号天府新谷9号楼2单元1005a',
        isDefault: true
    },
    {
        id: '2',
        name: '李四',
        phone: '185****0110',
        detail: '四川省成都市武侯区府城大道399号天府新谷9号楼2单元1005a'
    },
    {
        id: '3',
        name: '王五',
        phone: '185****0110',
        detail: '四川省成都市武侯区府城大道399号天府新谷9号楼2单元1005a'
    }
]
const addressList = ref(MOCK_ADDRESS_LIST)
/** 编辑地址:编辑页未实现,暂为占位反馈 */
const handleEdit = () => {
    uni.showToast({ title: '敬请期待', icon: 'none' })
}
/** 新增地址:跳转到新增地址页 */
const handleAdd = () => {
    uni.navigateTo({ url: '/pages/address/add' })
}
</script>
<style scoped lang="scss">
.page {
    min-height: 100vh;
    box-sizing: border-box;
    background-color: #ffffff;
    /* 预留底部固定操作栏高度(16+96+32rpx),避免最后一条地址被遮挡 */
    padding-bottom: calc(144rpx + env(safe-area-inset-bottom));
}
/* ==================== 地址列表 ==================== */
.addr-item {
    display: flex;
    align-items: flex-start;
    padding: 36rpx 40rpx;
}
/* 文本区:占满剩余宽度,地址超长自动换行 */
.addr-main {
    flex: 1;
    min-width: 0;
}
.addr-head {
    display: flex;
    align-items: center;
    gap: 56rpx;
}
.addr-name {
    font-size: 32rpx;
    font-weight: 600;
    line-height: 1;
    color: #1f2937;
}
.addr-phone {
    font-size: 30rpx;
    line-height: 1;
    color: #374151;
}
/* 默认标记:蓝色描边小标签 */
.addr-tag {
    display: inline-flex;
    align-items: center;
    height: 32rpx;
    margin: 10rpx 0 12rpx;
    padding: 0 10rpx;
    border: 2rpx solid #1e5ebc;
    border-radius: 6rpx;
    box-sizing: border-box;
    font-size: 20rpx;
    line-height: 1;
    color: #1e5ebc;
}
.addr-detail {
    display: block;
    font-size: 28rpx;
    line-height: 1.45;
    color: #6b7280;
}
/* ==================== 空态占位 ==================== */
/* 无收货地址时居中展示,避免出现空白页 */
.addr-empty {
    padding: 200rpx 0 0;
    display: flex;
    flex-direction: column;
    align-items: center;
}
/* 圆形底衬 + 地址图标 */
.empty-icon-wrap {
    width: 200rpx;
    height: 200rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background-color: #f1f5f9;
}
.empty-title {
    margin-top: 36rpx;
    font-size: 30rpx;
    font-weight: 600;
    line-height: 1;
    color: #475569;
}
.empty-desc {
    margin-top: 16rpx;
    font-size: 26rpx;
    line-height: 1;
    color: #94a3b8;
}
/* ==================== 底部固定操作栏 ==================== */
.add-bar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    /* 与登录页底部操作区一致:左右 56rpx 内边距 + 安全区留白,绿色胶囊不覆盖 Home 指示条 */
    padding: 16rpx 56rpx calc(32rpx + env(safe-area-inset-bottom));
    background-color: #ffffff;
}
/* 尺寸/圆角与登录页 .btn 完全一致:H48 → 96rpx,PILL R24 → 48rpx */
.add-btn {
    width: 100%;
    height: 96rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 16rpx;
    border-radius: 48rpx;
    box-sizing: border-box;
    /* pen 标准「草绿动能行动」渐变(90°)+ 绿色光晕投影 #6AA02735 */
    background: linear-gradient(90deg, #82c236 0%, #639c1f 100%);
    box-shadow: 0 6rpx 16rpx rgba(106, 160, 39, 0.21);
}
.add-text {
    font-size: 30rpx;
    font-weight: 600;
    line-height: 1;
    color: #ffffff;
}
/* ==================== 图标(内联 base64 SVG) ==================== */
.icon-edit,
.icon-plus,
.icon-empty {
    background-repeat: no-repeat;
    background-position: center center;
    background-size: 100% 100%;
}
/* 编辑:方形 + 铅笔(#1E5EBC) */
.icon-edit {
    width: 44rpx;
    height: 44rpx;
    flex-shrink: 0;
    margin-left: 24rpx;
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMUU1RUJDIiBzdHJva2Utd2lkdGg9IjEuOCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMTIgM0g1YTIgMiAwIDAgMC0yIDJ2MTRhMiAyIDAgMCAwIDIgMmgxNGEyIDIgMCAwIDAgMi0ydi03Ii8+PHBhdGggZD0iTTE4LjM3NSAyLjYyNWExIDEgMCAwIDEgMyAzbC05LjAxMyA5LjAxNGEyIDIgMCAwIDEtLjg1My41MDVsLTIuODczLjg0YS41LjUgMCAwIDEtLjYyLS42MmwuODQtMi44NzNhMiAyIDAgMCAxIC41MDYtLjg1MnoiLz48L3N2Zz4=');
}
/* 新增:白色加号 */
.icon-plus {
    width: 34rpx;
    height: 34rpx;
    flex-shrink: 0;
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjIuNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMTIgNXYxNE01IDEyaDE0Ii8+PC9zdmc+');
}
/* 空态:灰色定位图标(#CBD5E1) */
.icon-empty {
    width: 96rpx;
    height: 96rpx;
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjY2JkNWUxIiBzdHJva2Utd2lkdGg9IjEuNiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMjAgMTBjMCA2LTggMTItOCAxMnMtOC02LTgtMTJhOCA4IDAgMCAxIDE2IDBaIi8+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMCIgcj0iMyIvPjwvc3ZnPg==');
}
</style>
hugray-user-miniapp/pages/contact/index.vue
New file
@@ -0,0 +1,194 @@
<template>
    <view class="page">
        <!-- 深蓝顶部面板:自定义导航栏 + 品牌 Hero 区(同一底色,视觉连续) -->
        <view class="top-panel" :style="{ paddingTop: statusBarHeight + 'px' }">
            <!-- 导航栏:左侧返回箭头,居中标题 -->
            <view class="nav">
                <view class="nav-back" @tap="handleBack">
                    <view class="icon-back"></view>
                </view>
                <text class="nav-title">联系我们</text>
            </view>
            <!-- 品牌区:Logo 图(白色 YiOuS 标识) -->
            <view class="hero">
                <image class="brand-logo" src="/static/logo.png" mode="widthFix" />
            </view>
        </view>
        <!-- 联系方式列表:热线为纯展示文案,平台客服为绿色胶囊按钮 -->
        <view class="info-list">
            <view v-for="item in CONTACT_LIST" :key="item.key" class="info-row">
                <text class="info-label">{{ item.label }}</text>
                <!-- 有 value 渲染文案,否则渲染「联系客服」按钮 -->
                <text v-if="item.value" class="info-value">{{ item.value }}</text>
                <button v-else class="service-btn" open-type="contact">
                    <image class="service-icon" src="/static/wechat-icon.png" mode="aspectFit" />
                    <text class="service-text">联系客服</text>
                </button>
            </view>
        </view>
    </view>
</template>
<script setup>
/** 状态栏高度:自定义导航栏下用于顶部留白 */
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
/** 招商 / 合伙人热线:客服接口未就绪,先以常量维护(接口就绪后替换) */
const CITY_INVEST_HOTLINE = '400-451-666'
const PARTNER_RECRUIT_HOTLINE = '400-451-666'
/** 联系方式列表:value 为空表示该项渲染为客服按钮 */
const CONTACT_LIST = [
    { key: 'invest', label: '城市招商热线', value: CITY_INVEST_HOTLINE },
    { key: 'partner', label: '合伙人招募', value: PARTNER_RECRUIT_HOTLINE },
    { key: 'service', label: '平台客服' }
]
/** 返回上一页:无上级页面(如直接进入)时回到个人中心 */
const handleBack = () => {
    const pages = getCurrentPages()
    if (pages.length > 1) {
        uni.navigateBack()
        return
    }
    uni.switchTab({ url: '/pages/profile/index' })
}
</script>
<style scoped lang="scss">
.page {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    background-color: #ffffff;
}
/* ==================== 深蓝顶部面板 ==================== */
.top-panel {
    /* 与 logo.png 底色(#0B3F6F)保持一致,使 Logo 方块的圆角边缘无缝融合 */
    background-color: #0b3f6f;
}
/* 导航栏(44pt 高) */
.nav {
    position: relative;
    height: 88rpx;
    display: flex;
    align-items: center;
    justify-content: center;
}
.nav-back {
    position: absolute;
    left: 16rpx;
    top: 50%;
    transform: translateY(-50%);
    width: 64rpx;
    height: 64rpx;
    display: flex;
    align-items: center;
    justify-content: center;
}
/* 纯 CSS 左向箭头:左 + 下边框旋转 45° */
.icon-back {
    width: 22rpx;
    height: 22rpx;
    border-left: 4rpx solid #ffffff;
    border-bottom: 4rpx solid #ffffff;
    transform: rotate(45deg);
}
.nav-title {
    font-size: 34rpx;
    font-weight: 700;
    line-height: 1;
    letter-spacing: 2rpx;
    color: #ffffff;
}
/* 品牌 Hero 区(约 200pt 高) */
.hero {
    height: 400rpx;
    display: flex;
    align-items: center;
    justify-content: center;
}
/* 品牌 Logo:固定宽度,高度按原图比例自适应 */
.brand-logo {
    width: 340rpx;
}
/* ==================== 联系方式列表 ==================== */
.info-list {
    padding: 140rpx 40rpx 0;
    display: flex;
    flex-direction: column;
    gap: 88rpx;
}
.info-row {
    display: flex;
    align-items: center;
}
/* 固定标签列宽,保证右侧数值纵向对齐 */
.info-label {
    width: 220rpx;
    flex-shrink: 0;
    font-size: 32rpx;
    font-weight: 600;
    line-height: 1;
    letter-spacing: 1rpx;
    color: #1f2937;
}
.info-value {
    font-size: 30rpx;
    line-height: 1;
    letter-spacing: 1rpx;
    color: #6b7280;
}
/* 平台客服:绿色胶囊按钮 */
.service-btn {
    height: 60rpx;
    margin: 0;
    padding: 0 26rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10rpx;
    border: none;
    border-radius: 30rpx;
    box-sizing: border-box;
    line-height: 1;
    /* pen 标准「微信按钮 WeChat」渐变(90°)+ 绿色光晕投影 #07C16040 */
    background: linear-gradient(90deg, #07c160 0%, #05a352 100%);
    box-shadow: 0 12rpx 32rpx rgba(7, 193, 96, 0.25);
}
/* 重置小程序 button 默认边框 */
.service-btn::after {
    border: none;
}
.service-icon {
    width: 30rpx;
    height: 30rpx;
    flex-shrink: 0;
}
.service-text {
    font-size: 26rpx;
    font-weight: 600;
    line-height: 1;
    color: #ffffff;
}
</style>
hugray-user-miniapp/pages/health-plan/index.vue
New file
@@ -0,0 +1,260 @@
<template>
    <view class="page">
        <!-- 分类药丸筛选栏 -->
        <view class="filter-bar">
            <view class="pill-group">
                <view
                    v-for="(code, index) in categoryList"
                    :key="code"
                    class="pill"
                    :class="{ 'pill--active': index === activeIndex }"
                    @tap="handleCategoryTap(index)"
                >
                    <text class="pill-text">{{ code }}</text>
                </view>
            </view>
            <!-- 药丸组滚动位置指示条(滑块随选中项移动) -->
            <view class="slider">
                <view class="slider-thumb" :style="{ marginLeft: sliderOffset }"></view>
            </view>
        </view>
        <!-- 产品名称 + 探索更多 -->
        <view class="product-bar">
            <view class="product-text">
                <text class="product-code">{{ currentCategory }}</text>
                <text class="product-name">{{ productName }}</text>
            </view>
            <view class="explore-btn" @tap="handleExploreMore">
                <text class="explore-btn-text">探索更多</text>
                <view class="explore-btn-arrow-wrap">
                    <view class="explore-btn-arrow"></view>
                </view>
            </view>
        </view>
        <!-- 健康方案卡片列表 -->
        <view class="plan-list">
            <view
                v-for="plan in planList"
                :key="plan.id"
                class="plan-card"
                @tap="handlePlanTap(plan)"
            >
                <!-- 方案图素材未入仓,暂以同色系渐变块占位,素材就绪后替换为 image -->
                <view class="plan-card-visual" :style="{ background: plan.background }"></view>
            </view>
        </view>
        <!-- 全局缺货订单提示(存在缺货订单时所有页面均弹出) -->
        <stock-order-toast />
    </view>
</template>
<script setup>
import { computed, ref } from 'vue'
import StockOrderToast from '@/components/stock-order-toast/stock-order-toast.vue'
/** 分类药丸:与设计稿一致的三档产品系列代号 */
const categoryList = ['G&METS-II', 'HBBLF-2', 'NEARCLC-II']
/** 当前选中的分类下标 */
const activeIndex = ref(0)
/** 产品主名(三档分类当前共用一个产品名,接口就绪后按分类拉取) */
const productName = '超分子基团复合肽'
/** 方案卡片:素材缺失,先以设计稿同色系渐变占位 */
const planList = [
    { id: 1, background: 'linear-gradient(135deg, #16336e 0%, #2b52b8 48%, #4a6fd0 100%)' },
    { id: 2, background: 'linear-gradient(135deg, #3a2d86 0%, #5b4bc0 50%, #7d6bd8 100%)' },
    { id: 3, background: 'linear-gradient(135deg, #c9dcf3 0%, #e6f0fb 52%, #f7fbff 100%)' }
]
/** 当前选中的分类代号 */
const currentCategory = computed(() => categoryList[activeIndex.value])
/** 指示条滑块偏移:轨道 28pt、滑块 10pt,共 3 档 → 每档步进 18rpx */
const sliderOffset = computed(() => `${activeIndex.value * 18}rpx`)
/** 切换分类药丸 */
const handleCategoryTap = (index) => {
    activeIndex.value = index
}
/** 探索更多:产品详情页尚未开发,先给出占位反馈 */
const handleExploreMore = () => {
    uni.showToast({ title: '敬请期待', icon: 'none' })
}
/** 方案卡片:详情页尚未开发,先给出占位反馈 */
const handlePlanTap = () => {
    uni.showToast({ title: '敬请期待', icon: 'none' })
}
</script>
<style scoped lang="scss">
.page {
    min-height: 100vh;
    background: #f8fafc;
}
/* ==================== 分类药丸筛选栏(375×44pt) ==================== */
.filter-bar {
    height: 88rpx;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 32rpx;
    box-sizing: border-box;
}
.pill-group {
    display: flex;
    align-items: center;
    gap: 16rpx;
}
.pill {
    height: 56rpx;
    display: flex;
    align-items: center;
    padding: 0 28rpx;
    border-radius: 28rpx;
    background: #edf2f7;
}
.pill-text {
    font-size: 23rpx;
    font-weight: 600;
    line-height: 1;
    color: #475569;
}
.pill--active {
    background: #003e7e;
}
.pill--active .pill-text {
    font-weight: 700;
    color: #ffffff;
}
/* 滚动指示条:轨道 28×3pt,滑块 10×3pt */
.slider {
    flex-shrink: 0;
    width: 56rpx;
    height: 6rpx;
    margin-left: 16rpx;
    display: flex;
    align-items: center;
    border-radius: 3rpx;
    background: #e2e8f0;
}
.slider-thumb {
    width: 20rpx;
    height: 6rpx;
    border-radius: 3rpx;
    background: #c5a566;
}
/* ==================== 产品名称 + 探索更多(375×48pt) ==================== */
.product-bar {
    height: 96rpx;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 32rpx;
    box-sizing: border-box;
}
.product-text {
    display: flex;
    flex-direction: column;
    gap: 4rpx;
}
.product-code {
    font-size: 22rpx;
    font-weight: 700;
    letter-spacing: 2.4rpx;
    line-height: 1;
    color: #003e7e;
}
.product-name {
    font-size: 35rpx;
    font-weight: 800;
    letter-spacing: -0.4rpx;
    line-height: 1.1;
    color: #0f172a;
}
/* 探索更多:绿色渐变胶囊按钮 */
.explore-btn {
    flex-shrink: 0;
    height: 64rpx;
    display: flex;
    align-items: center;
    gap: 12rpx;
    padding: 0 16rpx 0 28rpx;
    border-radius: 32rpx;
    background: linear-gradient(90deg, #82c236 0%, #639c1f 100%);
    box-shadow: 0 6rpx 16rpx rgba(106, 160, 39, 0.21);
}
.explore-btn-text {
    font-size: 24rpx;
    font-weight: 600;
    letter-spacing: 0.4rpx;
    line-height: 1;
    color: #ffffff;
}
.explore-btn-arrow-wrap {
    width: 36rpx;
    height: 36rpx;
    border-radius: 18rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(255, 255, 255, 0.16);
}
/* 箭头:lucide chevron-right 白色 */
.explore-btn-arrow {
    width: 22rpx;
    height: 22rpx;
    background-repeat: no-repeat;
    background-position: center center;
    background-size: 100% 100%;
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJtOSAxOCA2LTYtNi02Ii8+PC9zdmc+');
}
/* ==================== 方案卡片列表(343×160pt / 张) ==================== */
.plan-list {
    display: flex;
    flex-direction: column;
    gap: 24rpx;
    padding: 8rpx 32rpx 32rpx;
    box-sizing: border-box;
}
.plan-card {
    width: 100%;
    height: 320rpx;
    overflow: hidden;
    border-radius: 28rpx;
    border: 1rpx solid rgba(226, 232, 240, 0.5);
    box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.04);
}
.plan-card-visual {
    width: 100%;
    height: 100%;
}
</style>
hugray-user-miniapp/pages/index/index.vue
@@ -1,24 +1,242 @@
<template>
    <view class="page">
        <!-- 内容区:设计稿内容高度与视口一致,矮屏可滚动 -->
        <scroll-view class="scroll" scroll-y>
            <!-- ==================== 明星产品展位:G&METS-II ==================== -->
            <view class="hero">
                <!-- 产品大图视口(沉浸加高,顶部延伸到状态栏下方) -->
                <view class="hero-visual">
                    <!-- 顶部品牌区(沉浸式导航:状态栏占位 + 品牌 Logo) -->
                    <view class="brand-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
                        <image class="brand-logo" src="/static/metabolism-300-logo.png" mode="aspectFit" />
                    </view>
                    <!-- 产品大图占位:设计稿素材未入仓,暂以同色系渐变块占位,素材就绪后替换为 image -->
                    <view class="hero-visual-placeholder"></view>
                    <!-- 轮播指示器:毛玻璃微胶囊 -->
                    <view class="hero-indicator">
                        <view class="indicator-dot"></view>
                        <view class="indicator-dot indicator-dot--active"></view>
                        <view class="indicator-dot"></view>
                    </view>
                </view>
                <!-- 产品信息与行动区 -->
                <view class="hero-info">
                    <view class="hero-info-text">
                        <text class="hero-code">G&METS-II</text>
                        <text class="hero-name">超分子基团复合肽</text>
                    </view>
                    <view class="hero-explore" @tap="_handleExploreMore">
                        <text class="hero-explore-text">探索更多</text>
                        <view class="hero-explore-arrow-wrap">
                            <view class="hero-explore-arrow"></view>
                        </view>
                    </view>
                </view>
            </view>
            <!-- ==================== 次席展位:HBBLF-2 与 代谢300 ==================== -->
            <view class="sub-visual">
                <!-- 展位图占位:素材未入仓,暂以渐变块占位 -->
                <view class="sub-visual-placeholder"></view>
            </view>
        </scroll-view>
        <!-- 全局缺货订单提示(存在缺货订单时所有页面均弹出) -->
        <stock-order-toast />
    </view>
</template>
<script setup>
import { onShow } from '@dcloudio/uni-app'
import { getToken } from '@/utils/request.js'
import config from '@/config/index.js'
import StockOrderToast from '@/components/stock-order-toast/stock-order-toast.vue'
/**
 * 校验登录态:本地无 token 时重定向至登录页
 */
const _checkLoginStatus = () => {
    if (getToken()) return
    uni.reLaunch({ url: config.loginPage })
/** 状态栏高度(px):沉浸式布局下品牌区需下移避让系统状态栏 */
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
/** 探索更多:产品详情页尚未开发,先给出占位反馈 */
const _handleExploreMore = () => {
    uni.showToast({ title: '敬请期待', icon: 'none' })
}
// 每次页面显示均校验登录态,避免退出登录后仍停留在首页
onShow(() => {
    _checkLoginStatus()
})
</script>
<style scoped lang="scss">
.page {
    width: 100%;
    height: 100vh;
    overflow: hidden;
    background: #f8fafc;
}
.scroll {
    width: 100%;
    height: 100vh;
}
/* ==================== 明星产品展位 ==================== */
.hero {
    width: 100%;
}
/* 产品大图视口:375×406pt */
.hero-visual {
    position: relative;
    width: 100%;
    height: 812rpx;
    overflow: hidden;
    /* 素材占位渐变:上部浅蓝(品牌区底色)向下过渡到白色 */
    background: linear-gradient(180deg, #d9e7f7 0%, #e9f1fb 26%, #ffffff 54%, #f4f7fb 100%);
}
/* 品牌区:状态栏占位 + 导航行,叠加在大图之上 */
.brand-bar {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    z-index: 2;
    display: flex;
    align-items: center;
    padding-left: 24rpx;
    padding-right: 24rpx;
    box-sizing: border-box;
}
.brand-logo {
    width: 168rpx;
    height: 88rpx;
}
/* 产品大图占位块(保留文案提示,便于后续替换真实素材) */
.hero-visual-placeholder {
    width: 100%;
    height: 100%;
}
/* 轮播指示器:毛玻璃微胶囊,水平居中,底部 376pt 处 */
.hero-indicator {
    position: absolute;
    left: 50%;
    top: 752rpx;
    transform: translateX(-50%);
    height: 32rpx;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10rpx;
    padding: 0 14rpx;
    border-radius: 16rpx;
    background: rgba(255, 255, 255, 0.7);
    border: 1rpx solid #ffffff;
    box-shadow: 0 4rpx 12rpx rgba(15, 23, 42, 0.07);
}
.indicator-dot {
    width: 8rpx;
    height: 8rpx;
    border-radius: 4rpx;
    background: rgba(0, 62, 126, 0.19);
}
.indicator-dot--active {
    width: 28rpx;
    background: linear-gradient(90deg, #003e7e 0%, #1e5ebc 100%);
}
/* 产品信息与行动区:375×58pt */
.hero-info {
    height: 116rpx;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 36rpx;
    background: #ffffff;
    border-top: 1rpx solid #f1f5f9;
    border-bottom: 1rpx solid #f1f5f9;
    box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.024);
}
.hero-info-text {
    display: flex;
    flex-direction: column;
    gap: 4rpx;
}
.hero-code {
    font-size: 22rpx;
    font-weight: 700;
    letter-spacing: 2.4rpx;
    line-height: 1;
    color: #003e7e;
}
.hero-name {
    font-size: 35rpx;
    font-weight: 800;
    letter-spacing: -0.4rpx;
    line-height: 1.15;
    color: #0f172a;
}
/* 探索更多:绿色渐变胶囊按钮 */
.hero-explore {
    flex-shrink: 0;
    height: 68rpx;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    gap: 12rpx;
    padding: 0 16rpx 0 28rpx;
    border-radius: 34rpx;
    background: linear-gradient(90deg, #82c236 0%, #639c1f 100%);
    box-shadow: 0 6rpx 16rpx rgba(106, 160, 39, 0.21);
}
.hero-explore-text {
    font-size: 25rpx;
    font-weight: 600;
    letter-spacing: 0.4rpx;
    line-height: 1;
    color: #ffffff;
}
.hero-explore-arrow-wrap {
    width: 40rpx;
    height: 40rpx;
    border-radius: 20rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(255, 255, 255, 0.16);
}
/* 箭头:lucide chevron-right 12×12pt */
.hero-explore-arrow {
    width: 24rpx;
    height: 24rpx;
    background-repeat: no-repeat;
    background-position: center center;
    background-size: 100% 100%;
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJtOSAxOCA2LTYtNi02Ii8+PC9zdmc+');
}
/* ==================== 次席展位 ==================== */
.sub-visual {
    position: relative;
    width: 100%;
    height: 530rpx;
    overflow: hidden;
    /* 素材占位渐变:紫调过渡,素材就绪后替换为 image */
    background: linear-gradient(160deg, #4e3d94 0%, #6a5ac1 45%, #a99be0 100%);
}
.sub-visual-placeholder {
    width: 100%;
    height: 100%;
}
</style>
hugray-user-miniapp/pages/login/index.vue
@@ -313,7 +313,8 @@
    if (!token) throw new Error('登录失败:未获取到登录凭证')
    setToken(token)
    uni.reLaunch({ url: '/pages/index/index' })
    // 首页为原生 tabBar 页面,须用 switchTab 切换
    uni.switchTab({ url: '/pages/index/index' })
}
/** 跳转注册页 */
hugray-user-miniapp/pages/order/index.vue
New file
@@ -0,0 +1,568 @@
<template>
    <view class="page">
        <!-- 订单状态过滤 Tab(375×44pt) -->
        <view class="status-tabs">
            <view
                v-for="tab in statusTabs"
                :key="tab.key"
                class="status-tab"
                :class="{ 'status-tab--active': tab.key === activeStatus }"
                @tap="handleStatusTap(tab.key)"
            >
                <text class="status-tab-text">{{ tab.label }}</text>
            </view>
        </view>
        <!-- 二级复合筛选:商品类型 + 时间区间 -->
        <view class="sub-filter">
            <view class="sub-filter-item" @tap="handleFilterTap">
                <text class="sub-filter-text">全部</text>
                <view class="icon icon-chevron-down"></view>
            </view>
            <view class="sub-filter-item" @tap="handleFilterTap">
                <view class="sub-filter-left">
                    <view class="icon icon-calendar"></view>
                    <text class="sub-filter-text">选择时间</text>
                </view>
                <view class="icon icon-chevron-down"></view>
            </view>
        </view>
        <!-- 订单卡片列表 -->
        <view class="order-list">
            <view v-for="order in filteredOrderList" :key="order.orderId" class="order-card">
                <!-- 状态行:下单时间 + 状态徽章 -->
                <view class="order-card-head">
                    <text class="order-time">{{ order.createTime }}</text>
                    <text class="order-badge" :class="`order-badge--${order.statusKey}`">
                        {{ order.statusText }}
                    </text>
                </view>
                <!-- 摘要行:套餐名 + 件数 + 实付金额 -->
                <view class="order-card-summary">
                    <text class="order-title">{{ order.title }}</text>
                    <view class="order-summary-right">
                        <text class="order-count">共{{ order.quantity }}件</text>
                        <view class="order-amount">
                            <text class="order-amount-label">实付</text>
                            <text class="order-amount-symbol">¥</text>
                            <text class="order-amount-value">{{ order.amount }}</text>
                        </view>
                    </view>
                </view>
                <!-- 缩略图与操作行 -->
                <view class="order-card-foot">
                    <view class="order-thumbs">
                        <view
                            v-for="(thumb, thumbIndex) in order.thumbnails"
                            :key="thumbIndex"
                            class="order-thumb"
                            :style="{ background: thumb }"
                        ></view>
                        <view v-if="order.moreCount > 0" class="order-thumb order-thumb--more">
                            <text class="order-thumb-more-text">•••</text>
                        </view>
                    </view>
                    <view class="order-actions">
                        <!-- 待支付:倒计时 + 去支付 -->
                        <template v-if="order.statusKey === 'pending'">
                            <view class="order-countdown">
                                <view class="order-countdown-num">
                                    <text class="order-countdown-num-text">{{ order.countdownMinute }}</text>
                                </view>
                                <text class="order-countdown-colon">:</text>
                                <view class="order-countdown-num">
                                    <text class="order-countdown-num-text">{{ order.countdownSecond }}</text>
                                </view>
                            </view>
                            <view class="order-pay-btn" @tap="handlePayTap(order)">
                                <text class="order-pay-btn-text">去支付</text>
                            </view>
                        </template>
                        <!-- 其余状态:查看订单 -->
                        <view v-else class="order-view-btn" @tap="handleViewTap(order)">
                            <text class="order-view-btn-text">查看订单</text>
                        </view>
                    </view>
                </view>
            </view>
            <!-- 空态:无该状态订单 -->
            <view v-if="filteredOrderList.length === 0" class="order-empty">
                <text class="order-empty-text">暂无相关订单</text>
            </view>
        </view>
        <!-- 全局缺货订单提示(存在缺货订单时所有页面均弹出) -->
        <stock-order-toast />
    </view>
</template>
<script setup>
import { computed, ref } from 'vue'
import StockOrderToast from '@/components/stock-order-toast/stock-order-toast.vue'
/** 缩略图占位渐变(商品图素材未入仓) */
const THUMB_BLUE = 'linear-gradient(135deg, #173a8f 0%, #2f57bd 100%)'
const THUMB_LIGHT = 'linear-gradient(135deg, #cfe0f5 0%, #eef5fd 100%)'
/** 订单状态过滤 Tab:全部 / 待支付 / 待发货 / 待收货 / 售后 */
const statusTabs = [
    { key: 'all', label: '全部' },
    { key: 'pending', label: '待支付' },
    { key: 'unshipped', label: '待发货' },
    { key: 'unreceived', label: '待收货' },
    { key: 'aftersale', label: '售后' }
]
/** 当前选中的状态(默认「全部」) */
const activeStatus = ref('all')
/** 订单列表:接口未就绪,先本地 mock(联调后替换为 getOrderList) */
const orderList = [
    {
        orderId: '20260902001',
        createTime: '2026-09-02 16:33:52',
        statusKey: 'pending',
        statusText: '待支付',
        title: '超分子基团复合肽套餐',
        quantity: 3,
        amount: '386',
        thumbnails: [THUMB_BLUE, THUMB_BLUE, THUMB_LIGHT],
        moreCount: 1,
        countdownMinute: '29',
        countdownSecond: '59'
    },
    {
        orderId: '20260902002',
        createTime: '2026-09-02 16:33:52',
        statusKey: 'fulfilling',
        statusText: '履约中',
        title: '超分子基团复合肽套餐',
        quantity: 1,
        amount: '386',
        thumbnails: [THUMB_BLUE],
        moreCount: 0
    },
    {
        orderId: '20260902003',
        createTime: '2026-09-02 16:33:52',
        statusKey: 'unshipped',
        statusText: '待发货',
        title: '超分子基团复合肽套餐',
        quantity: 3,
        amount: '386',
        thumbnails: [THUMB_BLUE, THUMB_BLUE, THUMB_LIGHT],
        moreCount: 0
    },
    {
        orderId: '20260902004',
        createTime: '2026-09-02 16:33:52',
        statusKey: 'unreceived',
        statusText: '待收货',
        title: '超分子基团复合肽套餐',
        quantity: 3,
        amount: '386',
        thumbnails: [THUMB_BLUE, THUMB_BLUE, THUMB_LIGHT],
        moreCount: 0
    },
    {
        orderId: '20260902005',
        createTime: '2026-09-02 16:33:52',
        statusKey: 'done',
        statusText: '已完成',
        title: '超分子基团复合肽套餐',
        quantity: 3,
        amount: '386',
        thumbnails: [THUMB_BLUE, THUMB_BLUE, THUMB_LIGHT],
        moreCount: 0
    },
    {
        orderId: '20260902006',
        createTime: '2026-09-02 16:33:52',
        statusKey: 'cancelled',
        statusText: '已取消',
        title: '超分子基团复合肽套餐',
        quantity: 3,
        amount: '386',
        thumbnails: [THUMB_BLUE, THUMB_BLUE, THUMB_LIGHT],
        moreCount: 0
    }
]
/** 按选中状态过滤订单(「全部」不过滤) */
const filteredOrderList = computed(() => {
    if (activeStatus.value === 'all') {
        return orderList
    }
    return orderList.filter((order) => order.statusKey === activeStatus.value)
})
/** 切换订单状态 Tab */
const handleStatusTap = (key) => {
    activeStatus.value = key
}
/** 二级筛选(商品类型 / 时间区间):筛选面板尚未开发,先给出占位反馈 */
const handleFilterTap = () => {
    uni.showToast({ title: '敬请期待', icon: 'none' })
}
/** 去支付:支付流程尚未开发,先给出占位反馈 */
const handlePayTap = () => {
    uni.showToast({ title: '敬请期待', icon: 'none' })
}
/** 查看订单:订单详情页尚未开发,先给出占位反馈 */
const handleViewTap = () => {
    uni.showToast({ title: '敬请期待', icon: 'none' })
}
</script>
<style scoped lang="scss">
.page {
    min-height: 100vh;
    background: #f8fafc;
}
/* ==================== 订单状态过滤 Tab(375×44pt) ==================== */
.status-tabs {
    height: 88rpx;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 32rpx;
    box-sizing: border-box;
    background: #ffffff;
    border-bottom: 1rpx solid #f1f5f9;
}
.status-tab {
    height: 56rpx;
    display: flex;
    align-items: center;
    padding: 0 24rpx;
    border-radius: 28rpx;
    background: #edf2f7;
}
.status-tab-text {
    font-size: 23rpx;
    font-weight: 600;
    line-height: 1;
    color: #475569;
}
/* 选中态:蓝色渐变胶囊 */
.status-tab--active {
    background: linear-gradient(90deg, #1e5ebc 0%, #003e7e 100%);
    box-shadow: 0 4rpx 12rpx rgba(0, 62, 126, 0.15);
}
.status-tab--active .status-tab-text {
    font-weight: 700;
    color: #ffffff;
}
/* ==================== 二级复合筛选栏(375×44pt) ==================== */
.sub-filter {
    height: 88rpx;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 32rpx;
    box-sizing: border-box;
}
.sub-filter-item {
    width: 332rpx;
    height: 64rpx;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 24rpx;
    box-sizing: border-box;
    border-radius: 12rpx;
    background: #ffffff;
    border: 1rpx solid #e2e8f0;
}
.sub-filter-left {
    display: flex;
    align-items: center;
    gap: 10rpx;
}
.sub-filter-text {
    font-size: 24rpx;
    font-weight: 500;
    line-height: 1;
    color: #334155;
}
/* ==================== 图标 ==================== */
.icon {
    flex-shrink: 0;
    background-repeat: no-repeat;
    background-position: center center;
    background-size: 100% 100%;
}
/* lucide chevron-down 13×13pt */
.icon-chevron-down {
    width: 26rpx;
    height: 26rpx;
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjOTRBM0I4IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0ibTYgOSA2IDYgNi02Ii8+PC9zdmc+');
}
/* lucide calendar 12×12pt */
.icon-calendar {
    width: 24rpx;
    height: 24rpx;
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjOTRBM0I4IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHJlY3Qgd2lkdGg9IjE4IiBoZWlnaHQ9IjE4IiB4PSIzIiB5PSI0IiByeD0iMiIvPjxwYXRoIGQ9Ik0xNiAydjQiLz48cGF0aCBkPSJNOCAydjQiLz48cGF0aCBkPSJNMyAxMGgxOCIvPjwvc3ZnPg==');
}
/* ==================== 订单卡片列表 ==================== */
.order-list {
    display: flex;
    flex-direction: column;
    gap: 24rpx;
    padding: 8rpx 32rpx 32rpx;
    box-sizing: border-box;
}
.order-card {
    display: flex;
    flex-direction: column;
    gap: 24rpx;
    padding: 28rpx;
    box-sizing: border-box;
    border-radius: 24rpx;
    background: #ffffff;
    border: 1rpx solid rgba(226, 232, 240, 0.5);
    box-shadow: 0 6rpx 16rpx rgba(15, 23, 42, 0.03);
}
.order-card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
}
.order-time {
    font-size: 22rpx;
    font-weight: 500;
    color: #94a3b8;
}
.order-badge {
    padding: 4rpx 14rpx;
    border-radius: 8rpx;
    font-size: 23rpx;
    font-weight: 700;
    line-height: 1.35;
}
.order-badge--pending {
    background: #fef2f2;
    color: #e05338;
}
.order-badge--fulfilling {
    background: #ecfdf5;
    color: #10b981;
}
.order-badge--unshipped,
.order-badge--unreceived {
    background: #fff7ed;
    color: #ea580c;
}
.order-badge--done,
.order-badge--cancelled {
    background: #f1f5f9;
    color: #94a3b8;
}
/* 摘要行 */
.order-card-summary {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
}
.order-title {
    font-size: 29rpx;
    font-weight: 700;
    color: #0f172a;
}
.order-summary-right {
    display: flex;
    align-items: flex-end;
    gap: 16rpx;
}
.order-count {
    padding: 4rpx 12rpx;
    border-radius: 8rpx;
    background: #f1f5f9;
    font-size: 21rpx;
    font-weight: 600;
    line-height: 1.35;
    color: #475569;
}
.order-amount {
    display: flex;
    align-items: baseline;
    gap: 4rpx;
}
.order-amount-label {
    font-size: 22rpx;
    font-weight: 500;
    color: #64748b;
}
.order-amount-symbol {
    font-size: 23rpx;
    font-weight: 700;
    color: #0f172a;
}
.order-amount-value {
    font-size: 34rpx;
    font-weight: 800;
    line-height: 1;
    color: #0f172a;
}
/* 缩略图与操作行 */
.order-card-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
}
.order-thumbs {
    display: flex;
    align-items: center;
    gap: 12rpx;
}
.order-thumb {
    width: 80rpx;
    height: 80rpx;
    box-sizing: border-box;
    border-radius: 12rpx;
    border: 1rpx solid #f1f5f9;
}
.order-thumb--more {
    display: flex;
    align-items: center;
    justify-content: center;
    background: #f8fafc;
    border-color: #e2e8f0;
}
.order-thumb-more-text {
    font-size: 22rpx;
    font-weight: 700;
    line-height: 1;
    color: #94a3b8;
}
.order-actions {
    display: flex;
    align-items: center;
    gap: 16rpx;
}
/* 倒计时(仅待支付) */
.order-countdown {
    display: flex;
    align-items: center;
    gap: 4rpx;
}
.order-countdown-num {
    width: 32rpx;
    height: 28rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 6rpx;
    background: #e53935;
}
.order-countdown-num-text {
    font-size: 19rpx;
    font-weight: 700;
    line-height: 1;
    color: #ffffff;
}
.order-countdown-colon {
    font-size: 20rpx;
    font-weight: 700;
    color: #e53935;
}
/* 去支付按钮 */
.order-pay-btn {
    height: 56rpx;
    display: flex;
    align-items: center;
    padding: 0 28rpx;
    border-radius: 28rpx;
    background: linear-gradient(90deg, #ef4444 0%, #dc2626 100%);
    box-shadow: 0 4rpx 12rpx rgba(220, 38, 38, 0.21);
}
.order-pay-btn-text {
    font-size: 24rpx;
    font-weight: 700;
    line-height: 1;
    color: #ffffff;
}
/* 查看订单按钮 */
.order-view-btn {
    height: 56rpx;
    display: flex;
    align-items: center;
    padding: 0 28rpx;
    box-sizing: border-box;
    border-radius: 28rpx;
    background: #f8fafc;
    border: 1rpx solid #cbd5e1;
}
.order-view-btn-text {
    font-size: 24rpx;
    font-weight: 500;
    line-height: 1;
    color: #475569;
}
/* 空态 */
.order-empty {
    padding: 120rpx 0;
    display: flex;
    justify-content: center;
}
.order-empty-text {
    font-size: 26rpx;
    color: #94a3b8;
}
</style>
hugray-user-miniapp/pages/profile/index.vue
New file
@@ -0,0 +1,485 @@
<template>
    <view class="page" :style="{ paddingTop: statusBarHeight + 'px' }">
        <!-- 自定义导航栏占位:系统胶囊按钮所在区域 -->
        <view class="nav-holder"></view>
        <!-- 用户头部信息区(335×72pt) -->
        <view class="header">
            <view class="user-main" @tap="handleAccountTap">
                <view class="avatar">
                    <view class="icon" :class="isLoggedIn ? 'icon-camera' : 'icon-user'"></view>
                </view>
                <view class="user-text">
                    <!-- 已登录:昵称 + 会员等级;未登录:登录入口 + 引导文案 -->
                    <text class="user-name">{{ isLoggedIn ? nickname : '点击登录 / 注册' }}</text>
                    <view v-if="isLoggedIn" class="level-pill">
                        <view class="level-medal">
                            <text class="level-medal-text">V</text>
                        </view>
                        <text class="level-text">{{ userLevel }}</text>
                    </view>
                    <text v-else class="user-sub">登录开启代谢300专属健康方案</text>
                </view>
            </view>
            <view class="edit-btn" @tap="handleAccountTap">
                <text class="edit-btn-text">{{ isLoggedIn ? '编辑资料' : '立即登录' }}</text>
            </view>
        </view>
        <!-- 收益数据展示区(343×74pt):未登录或 L0 普通用户不展示 -->
        <view v-if="!isRestrictedView" class="earnings-card">
            <view v-for="item in earnings" :key="item.key" class="earning-item">
                <text class="earning-label">{{ item.label }}</text>
                <text class="earning-value">{{ item.value }}</text>
            </view>
        </view>
        <!-- 功能菜单卡片组(343×408pt,6 行 × 68pt) -->
        <view class="menu-card">
            <view
                v-for="menu in menuList"
                :key="menu.key"
                class="menu-row"
                @tap="handleMenuTap(menu)"
            >
                <view class="menu-left">
                    <view class="icon menu-icon" :class="`icon-${menu.icon}`"></view>
                    <text class="menu-title">{{ menu.title }}</text>
                </view>
                <view class="menu-right">
                    <!-- 仓库订单角标:仅已登录态展示 -->
                    <view v-if="menu.withBadge && isLoggedIn" class="menu-badge">
                        <text class="menu-badge-text">1</text>
                    </view>
                    <view class="icon icon-chevron-right"></view>
                </view>
            </view>
        </view>
        <!-- ICP 备案区 -->
        <view class="icp">
            <text class="icp-no">京ICP备2026092800号-1X</text>
            <text class="icp-copy">代谢300 · 数据基于系统权限版权所有</text>
        </view>
        <!-- 全局缺货订单提示(存在缺货订单时所有页面均弹出) -->
        <!-- <stock-order-toast /> -->
    </view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { getToken } from '@/utils/request.js'
import config from '@/config/index.js'
import StockOrderToast from '@/components/stock-order-toast/stock-order-toast.vue'
/** 状态栏高度:自定义导航栏下用于顶部留白 */
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
/** 登录态:本页需同时兼容「已登录 / 未登录」两种展示,故以本地 token 判定 */
const isLoggedIn = ref(false)
/** 用户昵称:用户信息接口未就绪,先本地 mock(联调后替换为 getUserInfo) */
const nickname = ref('辉煌G891')
/** 用户等级:取值 L0~L4,其中 L0 为普通用户;等级接口未联调,先本地 mock(联调后取 getUserInfo 下发等级) */
const userLevel = ref('L4')
/** 是否受限展示:未登录或 L0 普通用户,个人中心仅展示「收货地址」「联系我们」 */
const isRestrictedView = computed(() => !isLoggedIn.value || userLevel.value === 'L0')
/** 收益三栏:未登录态统一展示 0.00 */
const earnings = computed(() => {
    const zeroValue = '0.00'
    return [
        { key: 'today', label: '今日收益', value: isLoggedIn.value ? '10.00' : zeroValue },
        { key: 'month', label: '本月收益', value: isLoggedIn.value ? '400.00' : zeroValue },
        { key: 'total', label: '累计收益', value: isLoggedIn.value ? '128000.00' : zeroValue }
    ]
})
/** 功能菜单全集:图标 key 对应 SCSS 中的 .icon-xxx 类;restricted 为 true 表示仅 L1 及以上等级可见 */
const MENU_LIST = [
    { key: 'invite', icon: 'share2', title: '我的邀请码', restricted: true },
    { key: 'member', icon: 'crown', title: '会员中心', restricted: true },
    { key: 'warehouse', icon: 'file-text', title: '仓库订单', withBadge: true, restricted: true },
    { key: 'income', icon: 'badge-cent', title: '收入明细', restricted: true },
    { key: 'address', icon: 'map-pin', title: '收货地址' },
    { key: 'contact', icon: 'contact', title: '联系我们' }
]
/** 当前可见菜单:受限展示(未登录 / L0)时仅保留「收货地址」「联系我们」 */
const menuList = computed(() =>
    isRestrictedView.value ? MENU_LIST.filter((menu) => !menu.restricted) : MENU_LIST
)
/** 头像 / 名称 / 按钮点击:未登录跳登录页,已登录进入资料编辑 */
const handleAccountTap = () => {
    if (!isLoggedIn.value) {
        uni.reLaunch({ url: config.loginPage })
        return
    }
    uni.showToast({ title: '敬请期待', icon: 'none' })
}
/** 菜单项点击:未登录先提示登录;已登录进入二级功能页(未接入的暂为占位反馈) */
const handleMenuTap = (menu) => {
    if (!isLoggedIn.value) {
        uni.showModal({
            title: '提示',
            content: '请先登录后再操作',
            confirmText: '去登录',
            cancelText: '取消',
            success: (res) => {
                // 用户确认后跳转登录页
                if (res.confirm) uni.reLaunch({ url: config.loginPage })
            }
        })
        return
    }
    // 联系我们:跳转独立页面
    if (menu.key === 'contact') {
        uni.navigateTo({ url: '/pages/contact/index' })
        return
    }
    // 收货地址:跳转地址列表页
    if (menu.key === 'address') {
        uni.navigateTo({ url: '/pages/address/list' })
        return
    }
    uni.showToast({ title: '敬请期待', icon: 'none' })
}
// 每次页面显示均刷新登录态,保证登录 / 退出后展示态同步
onShow(() => {
    isLoggedIn.value = Boolean(getToken())
})
</script>
<style scoped lang="scss">
.page {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    /* 页面底色:顶部淡蓝渐变过渡至浅灰 */
    background: linear-gradient(
        175deg,
        #d6e6f8 0%,
        #c4dcf6 18%,
        #e8f1fc 38%,
        #f8fafc 58%,
        #f8fafc 100%
    );
}
/* 自定义导航栏占位(46pt 胶囊栏,状态栏由行内 paddingTop 撑开) */
.nav-holder {
    height: 92rpx;
}
/* ==================== 用户头部信息区 ==================== */
.header {
    height: 144rpx;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 40rpx;
    box-sizing: border-box;
}
.user-main {
    display: flex;
    align-items: center;
    gap: 28rpx;
}
.avatar {
    width: 140rpx;
    height: 140rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    border-radius: 70rpx;
    background: #f1f5f9;
    border: 5rpx solid #ffffff;
}
/* 已登录:相机图标;未登录:用户图标 */
.icon-camera {
    width: 64rpx;
    height: 64rpx;
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjOTRBM0I4IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0iTTE0LjUgNGgtNUw3IDdINGEyIDIgMCAwIDAtMiAydjlhMiAyIDAgMCAwIDIgMmgxNmEyIDIgMCAwIDAgMi0yVjlhMiAyIDAgMCAwLTItMmgtM2wtMi41LTN6Ii8+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMyIgcj0iMyIvPjwvc3ZnPg==');
}
.icon-user {
    width: 68rpx;
    height: 68rpx;
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjQ0JENUUxIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0iTTE5IDIxdi0yYTQgNCAwIDAgMC00LTRIOWE0IDQgMCAwIDAtNCA0djIiLz48Y2lyY2xlIGN4PSIxMiIgY3k9IjciIHI9IjQiLz48L3N2Zz4=');
}
.user-text {
    display: flex;
    flex-direction: column;
    /* 左对齐:避免等级胶囊被拉伸到与昵称同宽(pen 中胶囊按内容自适应宽度) */
    align-items: flex-start;
    gap: 10rpx;
}
.user-name {
    font-size: 36rpx;
    font-weight: 700;
    line-height: 1.15;
    color: #0f172a;
}
.user-sub {
    font-size: 24rpx;
    line-height: 1.2;
    color: #64748b;
}
/* 会员等级胶囊(L4) */
.level-pill {
    height: 44rpx;
    display: flex;
    align-items: center;
    gap: 10rpx;
    padding: 0 20rpx 0 4rpx;
    box-sizing: border-box;
    border-radius: 22rpx;
    background: linear-gradient(90deg, #fde68a 0%, #fcd34d 50%, #f59e0b 100%);
    box-shadow: 0 4rpx 12rpx rgba(245, 158, 11, 0.19);
}
.level-medal {
    width: 36rpx;
    height: 36rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    border-radius: 50%;
    background: linear-gradient(145deg, #f59e0b 0%, #d97706 100%);
    border: 3rpx solid #ffffff;
}
.level-medal-text {
    font-size: 23rpx;
    font-weight: 900;
    line-height: 1;
    color: #ffffff;
}
.level-text {
    font-size: 25rpx;
    font-weight: 800;
    line-height: 1;
    letter-spacing: 1rpx;
    color: #78350f;
}
/* 编辑资料 / 立即登录按钮 */
.edit-btn {
    flex-shrink: 0;
    height: 64rpx;
    display: flex;
    align-items: center;
    padding: 0 32rpx;
    border-radius: 32rpx;
    background: linear-gradient(90deg, #1e5ebc 0%, #003e7e 100%);
    box-shadow: 0 6rpx 16rpx rgba(0, 62, 126, 0.21);
}
.edit-btn-text {
    font-size: 25rpx;
    font-weight: 600;
    line-height: 1;
    color: #ffffff;
}
/* ==================== 收益 / 菜单:玻璃态卡片 ==================== */
.earnings-card {
    height: 148rpx;
    margin: 24rpx 32rpx 0;
    display: flex;
    align-items: center;
    padding: 0 24rpx;
    box-sizing: border-box;
    border-radius: 32rpx;
    border: 2rpx solid rgba(255, 255, 255, 0.82);
    background: linear-gradient(
        180deg,
        rgba(255, 255, 255, 0.94) 0%,
        rgba(240, 245, 253, 0.86) 50%,
        rgba(228, 238, 251, 0.82) 100%
    );
    backdrop-filter: blur(20px);
    box-shadow: 0 12rpx 40rpx rgba(15, 23, 42, 0.04);
}
.earning-item {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10rpx;
}
.earning-label {
    font-size: 25rpx;
    font-weight: 500;
    line-height: 1;
    color: #64748b;
}
.earning-value {
    font-size: 38rpx;
    font-weight: 700;
    line-height: 1;
    color: #0f172a;
}
.menu-card {
    margin: 24rpx 32rpx 0;
    border-radius: 32rpx;
    overflow: hidden;
    border: 2rpx solid rgba(255, 255, 255, 0.82);
    background: linear-gradient(
        180deg,
        rgba(255, 255, 255, 0.94) 0%,
        rgba(240, 245, 253, 0.86) 50%,
        rgba(228, 238, 251, 0.82) 100%
    );
    backdrop-filter: blur(20px);
    box-shadow: 0 12rpx 40rpx rgba(15, 23, 42, 0.04);
}
.menu-row {
    height: 136rpx;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 40rpx;
    box-sizing: border-box;
    border-bottom: 2rpx solid rgba(226, 232, 240, 0.5);
}
/* 末行无分隔线 */
.menu-row:last-child {
    border-bottom: none;
}
.menu-left {
    display: flex;
    align-items: center;
    gap: 28rpx;
}
.menu-title {
    font-size: 30rpx;
    font-weight: 500;
    line-height: 1;
    color: #1e293b;
}
.menu-right {
    display: flex;
    align-items: center;
    gap: 16rpx;
}
/* 仓库订单红色角标 */
.menu-badge {
    width: 36rpx;
    height: 36rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: #ef4444;
}
.menu-badge-text {
    font-size: 22rpx;
    font-weight: 700;
    line-height: 1;
    color: #ffffff;
}
/* ==================== 图标(内联 base64 SVG) ==================== */
.icon {
    flex-shrink: 0;
    background-repeat: no-repeat;
    background-position: center center;
    background-size: 100% 100%;
}
/* 统一渐变描边:135° #3B82F6 → #1D4ED8 */
.menu-icon {
    width: 40rpx;
    height: 40rpx;
}
.icon-share2 {
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI2cpIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM0I4MkY2Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMUQ0RUQ4Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PGNpcmNsZSBjeD0iMTgiIGN5PSI1IiByPSIzIi8+PGNpcmNsZSBjeD0iNiIgY3k9IjEyIiByPSIzIi8+PGNpcmNsZSBjeD0iMTgiIGN5PSIxOSIgcj0iMyIvPjxwYXRoIGQ9Im04LjU5IDEzLjUxIDYuODMgMy45OCIvPjxwYXRoIGQ9Im0xNS40MSA2LjUxLTYuODIgMy45OCIvPjwvc3ZnPg==');
}
.icon-crown {
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI2cpIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM0I4MkY2Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMUQ0RUQ4Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTExLjU2MiAzLjI2NmEuNS41IDAgMCAxIC44NzYgMEwxNS4zOSA4Ljg3YTEgMSAwIDAgMCAxLjUxNi4yOTRMMjEuMTgzIDUuNWEuNS41IDAgMCAxIC43OTguNTE5bC0yLjgzNCAxMC4yNDZhMSAxIDAgMCAxLS45NTYuNzM1SDUuODFhMSAxIDAgMCAxLS45NTctLjczNUwyLjAyIDYuMDJhLjUuNSAwIDAgMSAuNzk4LS41MTlsNC4yNzYgMy42NjRhMSAxIDAgMCAwIDEuNTE2LS4yOTR6Ii8+PHBhdGggZD0iTTUgMjFoMTQiLz48L3N2Zz4=');
}
.icon-file-text {
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI2cpIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM0I4MkY2Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMUQ0RUQ4Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTE1IDJINmEyIDIgMCAwIDAtMiAydjE2YTIgMiAwIDAgMCAyIDJoMTJhMiAyIDAgMCAwIDItMlY3WiIvPjxwYXRoIGQ9Ik0xNCAydjRhMiAyIDAgMCAwIDIgMmg0Ii8+PHBhdGggZD0iTTEwIDlIOCIvPjxwYXRoIGQ9Ik0xNiAxM0g4Ii8+PHBhdGggZD0iTTE2IDE3SDgiLz48L3N2Zz4=');
}
.icon-badge-cent {
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI2cpIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM0I4MkY2Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMUQ0RUQ4Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTMuODUgOC42MmE0IDQgMCAwIDEgNC43OC00Ljc3IDQgNCAwIDAgMSA2Ljc0IDAgNCA0IDAgMCAxIDQuNzggNC43OCA0IDQgMCAwIDEgMCA2Ljc0IDQgNCAwIDAgMS00Ljc3IDQuNzggNCA0IDAgMCAxLTYuNzUgMCA0IDQgMCAwIDEtNC43OC00Ljc3IDQgNCAwIDAgMSAwLTYuNzZaIi8+PHBhdGggZD0iTTEyIDd2MTAiLz48cGF0aCBkPSJNMTAgMTBoNCIvPjwvc3ZnPg==');
}
.icon-map-pin {
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI2cpIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM0I4MkY2Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMUQ0RUQ4Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIwIDEwYzAgNC45OTMtNS41MzkgMTAuMTkzLTcuMzk5IDExLjc5OWExIDEgMCAwIDEtMS4yMDIgMEM5LjUzOSAyMC4xOTMgNCAxNC45OTMgNCAxMGE4IDggMCAwIDEgMTYgMCIvPjxjaXJjbGUgY3g9IjEyIiBjeT0iMTAiIHI9IjMiLz48L3N2Zz4=');
}
.icon-contact {
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI2cpIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM0I4MkY2Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMUQ0RUQ4Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTE2IDJ2MiIvPjxwYXRoIGQ9Ik04IDJ2MiIvPjxjaXJjbGUgY3g9IjEyIiBjeT0iMTEiIHI9IjMiLz48cmVjdCB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHg9IjMiIHk9IjQiIHJ4PSIyIi8+PHBhdGggZD0iTTcgMjJ2LTJhMiAyIDAgMCAxIDItMmg2YTIgMiAwIDAgMSAyIDJ2MiIvPjwvc3ZnPg==');
}
/* lucide chevron-right 16×16pt #94A3B8 */
.icon-chevron-right {
    width: 32rpx;
    height: 32rpx;
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjOTRBM0I4IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0ibTkgMTggNi02LTYtNiIvPjwvc3ZnPg==');
}
/* ==================== ICP 备案区 ==================== */
.icp {
    /* 内容不足一屏时,备案区自动贴住页面底部(剩余空间由 margin-top:auto 吸收) */
    margin: auto 32rpx 0;
    padding: 36rpx 0 48rpx;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6rpx;
}
.icp-no {
    font-size: 22rpx;
    font-weight: 500;
    line-height: 1;
    letter-spacing: 0.6rpx;
    color: #94a3b8;
}
.icp-copy {
    font-size: 20rpx;
    line-height: 1;
    color: #a0aec0;
}
</style>
hugray-user-miniapp/pages/stock-order/list.vue
New file
@@ -0,0 +1,44 @@
<template>
    <view class="page">
        <view class="empty">
            <text class="empty-title">暂无缺货订单</text>
            <text class="empty-desc">缺货订单列表页建设中</text>
        </view>
    </view>
</template>
<script setup>
import { onShow } from '@dcloudio/uni-app'
import { ensureLogin } from '@/utils/request.js'
// 每次页面显示均校验登录态
onShow(() => {
    ensureLogin()
})
</script>
<style scoped lang="scss">
.page {
    min-height: 100vh;
    background: #f8fafc;
}
.empty {
    padding-top: 320rpx;
    display: flex;
    flex-direction: column;
    align-items: center;
}
.empty-title {
    font-size: 34rpx;
    font-weight: 700;
    color: #0f172a;
}
.empty-desc {
    margin-top: 16rpx;
    font-size: 26rpx;
    color: #94a3b8;
}
</style>
hugray-user-miniapp/static/tabbar/tab-home-active.png
hugray-user-miniapp/static/tabbar/tab-home.png
hugray-user-miniapp/static/tabbar/tab-order-active.png
hugray-user-miniapp/static/tabbar/tab-order.png
hugray-user-miniapp/static/tabbar/tab-plan-active.png
hugray-user-miniapp/static/tabbar/tab-plan.png
hugray-user-miniapp/static/tabbar/tab-profile-active.png
hugray-user-miniapp/static/tabbar/tab-profile.png
hugray-user-miniapp/utils/request.js
@@ -24,6 +24,17 @@
    uni.removeStorageSync(config.tokenKey)
}
/**
 * 页面登录态守卫:无 token 时重定向至登录页
 * 供各页面 onShow 调用,避免登录态校验逻辑重复散落
 * @returns {Boolean} 已登录返回 true,未登录(已触发跳转)返回 false
 */
export function ensureLogin() {
    if (getToken()) return true
    uni.reLaunch({ url: config.loginPage })
    return false
}
// ======================== 核心请求方法 ========================
/**
hugray-user-miniapp/utils/stock-order.js
New file
@@ -0,0 +1,27 @@
/**
 * 缺货订单数据源(用户端)
 * 说明:后端暂未提供「缺货订单数量」接口,当前使用本地 mock 数据。
 *      接口就绪后仅需替换 getStockOrderCount 内部实现,调用方无需改动。
 */
/** 缺货订单列表页路径 */
export const STOCK_ORDER_LIST_PAGE = '/pages/stock-order/list'
/** mock:缺货订单数量(联调多笔场景时可写入本地缓存覆盖) */
const MOCK_STOCK_ORDER_COUNT = 1
/** 本地缓存键名(仅用于覆盖 mock 值) */
const STOCK_ORDER_COUNT_KEY = 'HUGRAY_STOCK_ORDER_COUNT'
/**
 * 获取缺货订单数量
 * @returns {Number} 缺货订单数量,无缺货时返回 0
 */
export function getStockOrderCount() {
    // TODO: 后端「缺货订单数量」接口就绪后替换为真实请求
    const cached = uni.getStorageSync(STOCK_ORDER_COUNT_KEY)
    if (cached === '' || cached === null || cached === undefined) {
        return MOCK_STOCK_ORDER_COUNT
    }
    return Number(cached) || 0
}