feat: Kinetic Dark 设计语言 + Lucide 图标迁移 + 首页轻量个性化
- 建立 Lucide 子集 iconfont(49 字形),全站 emoji 替换为 fc-icon 组件 - 重塑 Design Token(冷中性近黑底 + Volt 荧光绿 #B6F24E),消除硬编码颜色 - 首页登录后个性化:我的计划 / 我的收藏 / 今日推荐(纯前端复用现有 API) - 后端 DEFAULT_EMOJI→Lucide 名,PALETTE 对齐 Kinetic Dark 语义色板 - 修复 person-standing 缺失字形;14 个官方组合 seed emoji 全部映射到现有字形
This commit is contained in:
291
FITCOACH-DESIGN-PROPOSAL.md
Normal file
291
FITCOACH-DESIGN-PROPOSAL.md
Normal file
@@ -0,0 +1,291 @@
|
||||
# FITCOACH 设计语言重塑提案(v2 暗色动能系统)
|
||||
|
||||
> 设计师:颜好看 | 日期:2026-07-22 | 寄存器:Product(app UI)| 平台:微信小程序(原生)
|
||||
> 配套机器可读:`design-system/design-tokens.json`(已产出)
|
||||
> 三轴刻度:DESIGN_VARIANCE=5(克制、可预测网格)/ MOTION_INTENSITY=4(状态反馈为主)/ VISUAL_DENSITY=4(日常应用密度)
|
||||
|
||||
---
|
||||
|
||||
## 0. 当前视觉审计(暗金奢华风 → 问题清单)
|
||||
|
||||
我实际通读了 `miniprogram/` 下页面与组件。结论:**当前"暗金奢华"调性与健身教练产品不匹配,且存在大量 P0 级违规(emoji 当图标)、Token 不全(硬编码)、以及 AI 模板味的结构问题。** 逐项如下。
|
||||
|
||||
### 0.1 P0 级硬伤(违反即退回)
|
||||
|
||||
| # | 问题 | 位置 | 现状 |
|
||||
|---|------|------|------|
|
||||
| P0-1a | emoji 当功能图标(首页) | `pages/index/index.wxml` `search-pill__icon`=🔍、`cta__icon`=✨;`quick__emoji`=✨💪🏋️📋🗂️👤;`prog__emoji` | 6 个快捷入口 + 搜索 + CTA 全用 emoji |
|
||||
| P0-1b | emoji 当功能图标(导航) | `components/bottom-nav/index.js` 🏠📚📋👤 | 主导航 4 项全 emoji |
|
||||
| P0-1c | emoji 当功能图标(个人页/空态/占位) | `profile.wxml` ❤️🗂️⭐📋💪;`favorites/search/my-plans/plan-favorites/plan-detail` 空态 ⭐📋🔍🏋️;`exercise-card`/`detail` 占位 🏋️ | 几乎每页都有 |
|
||||
| P0-1d | 收藏切换用字符 ♥/♡ | `detail.wxml` `collection-detail.wxml` | 字符当图标,应换 SVG heart |
|
||||
| P0-3a | 硬编码颜色散落 | 全 13 个 wxss 中大量 `#0E1110`、`rgba(229,84,75,…)`、`#fff` 直接写死(见下表) | 改主题要改 N 处,违反"颜色全走 Token" |
|
||||
| P0-2 | 当前非紫粉渐变(此项**未违规**,但注意 CTA 用金→浅金渐变 `linear-gradient(135deg, gold, gold-2)` 属"奢华模板味",新方案去掉) | `index.wxss` `.cta` | 保留为审计备注 |
|
||||
|
||||
### 0.2 Token 不全 / 硬编码(必须重塑)
|
||||
|
||||
全量 grep 出的硬编码色值(应下沉为 Token):
|
||||
|
||||
```
|
||||
pages/index/index.wxss:5 linear-gradient(150deg, #1C2A22 0%, #0E1110 70%) ← hero 背景硬编码
|
||||
pages/index/index.wxss:26 color: #0E1110; ← CTA 前景硬编码
|
||||
plan-detail.wxss:16,50 rgba(229,84,75,0.92) / #E5544B ← danger 硬编码
|
||||
plan-detail.wxss:33,48 color: #0E1110; ← 硬编码
|
||||
profile/recommend/my-plans/favorites/plan-edit/detail ... color: #0E1110 ← 同值散落 8+ 处
|
||||
exercise-card.wxss:28 color: #0E1110;
|
||||
gif-player.wxss:12 var(--gold, #c9a24b) ← 兜底硬编码(可接受但应统一)
|
||||
app.wxss:5 --bg-grad: linear-gradient(160deg,#16201B,#0E1110) ← 已有变量,但页面级又重复写
|
||||
```
|
||||
|
||||
**问题归纳**:`app.wxss` 里其实定义了 `--bg/--surface/--gold/--text/--line/--radius` 等,但(1)命名偏"金色语义"(--gold/--gold-2/--gold-soft),不利去奢华化;(2)缺语义色(success/warn/danger)、缺层级阴影、缺字体三族、缺字距/字重规范;(3)页面级普遍不引用 Token 直接写 hex。
|
||||
|
||||
### 0.3 层级 / 间距 / 字体 / 对比 / 留白 / 动效
|
||||
|
||||
| 维度 | 现状问题 |
|
||||
|------|----------|
|
||||
| 层级 | 卡片全靠 `box-shadow: 0 12rpx 40rpx rgba(0,0,0,.35)` 抬升 + 28rpx 大圆角 → "幽灵卡片"(发丝边框 + 大阴影同屏,见绝对禁令 #9);缺乏"背景递进表达层级" |
|
||||
| 间距 | 混用 `40rpx/32rpx/36rpx/28rpx/24rpx/20rpx/18rpx/16rpx/22rpx` 等,无统一尺度(`quick__grid gap:18rpx` 不在 4px 网格;`cta padding:28rpx` 非标) |
|
||||
| 字体 | 只用系统 `-apple-system, "PingFang SC", ...`,无 Latin 显示字体、无等宽数据字;Hero 56rpx 与正文 28rpx 跨度大但中间阶梯缺 |
|
||||
| 对比 | 正文 `--text-2:#98A39B` 在 `#0E1110` 上约 6.5:1(OK);但 `--text-3:#6B746D` 仅约 3.3:1,用于次要信息勉强达标,用于小字会吃力 |
|
||||
| 留白 | 卡片内边距 28rpx、区块 padding 40rpx 偏挤;Hero 下方直接接内容,缺少节奏呼吸 |
|
||||
| 动效 | 几乎无状态动效(hover/active/loading 无反馈);CTA 有光晕但无按下态;进度/加载无骨架屏 |
|
||||
| 结构 | 首页用"全大写英文 eyebrow(FEATURED/PROGRAMS)+ 图标网格 + 横向滑动"结构,偏模板;且 emoji 占位使整页廉价感强 |
|
||||
|
||||
### 0.4 审计结论
|
||||
|
||||
当前方案在 **P0-1(emoji 图标)、P0-3(硬编码/模板味)、Token 完整性** 三处不达标,必须重塑。设计语言从"暗金奢华"转向 **「动能暗色 / Kinetic Dark」**:保留深色高级基底,去掉金色奢华语义,换成单一有能量的珊瑚橙强调色 + 发丝边框 + 数据化表达,贴合健身教练的"专业、能量、可信赖"。
|
||||
|
||||
---
|
||||
|
||||
## 1. 新设计语言提案:Design Token 草案
|
||||
|
||||
> 完整机读版见 `design-system/design-tokens.json`。此处给出关键决策与色值。
|
||||
|
||||
### 1.1 配色(中性 85% + 强调 8% + 语义 5% + 效果 <2%)
|
||||
|
||||
**基底(A1,冷中性近黑,非纯黑、非绿调奢华)**
|
||||
- `--bg:#0B0F0E` 页面背景 | `--surface:#141A18` 卡片 | `--surface-2:#1C2421` 次级表面/chip
|
||||
- `--fg:#ECF1EF` 主文本 | `--fg-2:#9BA7A1` 次级 | `--fg-3:#6B7670` 三级(仅用于非关键 meta,且字号≥24rpx)
|
||||
- `--border:rgba(255,255,255,.08)` | `--border-strong:rgba(255,255,255,.14)`
|
||||
|
||||
**强调色(A1,单一品牌色,每屏≤2 处)**
|
||||
- `--accent:#FF6A3D` **Pulse 珊瑚橙**(CTA、激活导航、关键数据高亮)
|
||||
- 派生:`--accent-hover:#FF8263` / `--accent-active:#E8562B` / `--accent-on:#20100A`(深暖字,橙底上对比>7:1)/ `--accent-soft:rgba(255,106,61,.12)`
|
||||
|
||||
**语义色(A2,不计入"第二品牌色"——success 用于进度/连续天数这类"正向"语义)**
|
||||
- `--success:#2BD9A6`(进度环/连续天数/正向趋势)| `--warn:#F2B53D` | `--danger:#FF5C5C`(替代旧 `#E5544B`)| `--info:#4FA8FF`
|
||||
- 派生 soft 版用于淡底标签
|
||||
|
||||
**为什么不是紫/粉/金**:紫→粉渐变属 AI 模板红线(P0-2);金色奢华与"健身教练"调性错位(奢侈感≠专业运动感);珊瑚橙在深色上能量强、对比足、且与现品牌"暗底"自然衔接,迁移成本低。
|
||||
|
||||
**备选方向(待 PM 拍板)**:若想要更"运动品牌"的辨识度,可把 accent 换成 **Volt 荧光绿 `#B6F24E`**(Gymshark/Nike 味,更激进);或 **Ion 青 `#1FD4C4`**(更冷静健康)。本提案默认推荐珊瑚橙。
|
||||
|
||||
### 1.2 字体(含中文,三族明确)
|
||||
|
||||
- **显示/标题/数字**:`'Manrope', 'PingFang SC', 'HarmonyOS Sans SC', 'Source Han Sans SC', 'Noto Sans CJK SC', sans-serif`
|
||||
- Latin 用 **Manrope**(几何、运动感、不在反射拒绝清单);中文回落系统黑体 —— 小程序不打包 10MB+ CJK 字体,这是真实约束下的最优解。
|
||||
- **正文**:`'PingFang SC', 'HarmonyOS Sans SC', 'Source Han Sans SC', 'Noto Sans CJK SC', -apple-system, sans-serif`(中文系统字体优先)
|
||||
- **数据/统计**:`'JetBrains Mono', ui-monospace, monospace`(等宽,tabular 数字,用于进度环中心数字、连续天数、历史曲线 Y 轴)
|
||||
|
||||
**字重三级**:Regular 400(正文)/ Medium 510(按钮、小标题)/ Semibold 590(大标题)。小程序字体若无 510 字重则回落 500。
|
||||
|
||||
**字距**:eyebrow 全大写 `0.08em`;大标题 `-0.02em`;正文 `0`。
|
||||
|
||||
### 1.3 字号阶梯(rpx,750rpx=375pt)
|
||||
|
||||
`xs 22 / sm 24 / base 28 / md 30 / lg 32 / xl 36 / 2xl 44 / 3xl 56(hero) / 4xl 72(巨型数字)`。中间补 `md 30` 让"正文→小标题"过渡平滑(旧方案缺这档)。
|
||||
|
||||
### 1.4 间距尺度(4px 网格 → rpx 偶数)
|
||||
|
||||
`8/16/24/32/40/48/64/80/96/128(rpx)`,对应 4–64px。统一替换旧方案里的 `18rpx/22rpx/28rpx/36rpx` 等非标值。区块节奏:桌面/大屏 `section-y = 48rpx`,移动 `32rpx`(小程序默认移动,省略断点差异,但安全区 `env(safe-area-inset-bottom)` 必须在底部导航与吸底按钮处使用)。
|
||||
|
||||
### 1.5 圆角(收紧,去"过度圆滑")
|
||||
|
||||
`sm 16 / md 24 / lg 32 / xl 40 / pill 9999 (rpx)`。卡片用 `md(24rpx=12px)` 或 `lg(32rpx=16px)`,**不再用 28rpx 这种临界值**;chip/pill 用 `pill`。
|
||||
|
||||
### 1.6 阴影 / 层级(Hairline First)
|
||||
|
||||
默认卡片 **不用阴影,用 1px 发丝边框**(`--shadow:ring`)。仅悬浮/选中用 `--shadow:raised`;底部导航用 `--shadow:float`;**唯一允许的彩色阴影是 CTA 的 `--shadow:glow`**(珊瑚橙光晕,替代旧金色光晕)。深色模式靠"背景亮度递进"表达层级(`#0B0F0E → #141A18 → #1C2421`),不靠投影。
|
||||
|
||||
### 1.7 动效(150ms 收敛,无 bounce)
|
||||
|
||||
`fast 150 / base 200 / slow 300ms`,标准缓动 `cubic-bezier(0.2,0,0,1)`。**禁 bounce/elastic**。所有交互组件补 hover/active 反馈;列表/卡片进入用 ≤300ms 的淡入(勿每节相同动画)。尊重 `prefers-reduced-motion`(小程序通过 `wx.getSystemSetting().reduceMotion` 或 CSS `@media (prefers-reduced-motion: reduce)` 降级)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 图标系统:锁定 Lucide(MIT)
|
||||
|
||||
### 2.1 推荐与理由
|
||||
|
||||
**锁定一套:`lucide`(MIT,原 Feather 续作)。**
|
||||
|
||||
| 维度 | 说明 |
|
||||
|------|------|
|
||||
| 一致性 | 统一 24×24 网格、2px 描边、单线风格 → 缩到 16/20/24 视觉不变形 |
|
||||
| 覆盖 | 健身语义齐全:`dumbbell`/`activity`/`flame`/`heart`/`target`/`calendar-check`/`trending-up`/`search`/`user`/`home`/`sparkles` 等 |
|
||||
| 许可 | MIT,可子集化编译为 iconfont,无商用风险 |
|
||||
| 调性 | 中性、现代、"赢得熟悉感"(Product 寄存器标杆),不像彩色 emoji 那样廉价 |
|
||||
| 落地 | 小程序无原生 inline SVG:把**所需子集编译成 iconfont(.ttf/.woff2)**,用 `<text class="fc-icon">` + `font-family` 引用;或 base64 SVG `<image>`。**严禁 emoji** |
|
||||
|
||||
**为何不选其他**:Remix Icon(含 outline/fill 双风格,可作备选,但双风格易混,先锁 Lucide 单风格);Radix Icons(集太小、偏开发工具、缺健身图标);Iconify(聚合器非单一风格,**混用即违反 P0-1"锁定一套"**)。
|
||||
|
||||
### 2.2 尺寸规范
|
||||
|
||||
| 用途 | 尺寸 | 示例 |
|
||||
|------|------|------|
|
||||
| 行内/列表前置 | **16rpx (8px)** | 单元格图标、标签前置 |
|
||||
| 按钮内 | **20rpx (10px)** | CTA、次要按钮 |
|
||||
| 独立图标/导航 | **24rpx (12px)** | 底部导航、区块图标、空态 |
|
||||
|
||||
> 注意:小程序 rpx 在 750 基准下 24rpx=12px 物理点;若真机偏小,导航图标可上探到 44–48rpx(见下表映射的视觉权重)。底部导航按平台规范建议视觉区 ~44–48rpx。
|
||||
|
||||
### 2.3 现有 emoji → Lucide SVG 映射表(全量)
|
||||
|
||||
| 现有 emoji | 位置 | 语义 | Lucide 图标 | 备注 |
|
||||
|-----------|------|------|-------------|------|
|
||||
| 🔍 | index 搜索胶囊、search 页、favorites 空态 | 搜索 | `search` / 空态 `search-x` | |
|
||||
| ✨ | index CTA、quick"智能推荐" | AI 推荐 | `sparkles` | |
|
||||
| 💪 | quick"按部位" | 部位/身体 | `accessibility`(或 `person-standing`) | 不与器械混 |
|
||||
| 🏋️ | quick"按器械"、exercise 占位、detail 占位、plan-detail 空态 | 器械/训练 | `dumbbell` | |
|
||||
| 📋 | quick"官方计划"、profile"设置"、my-plans 空态 | 计划/列表 | `clipboard-list`(官方计划)/ `settings`(设置) | 两处语义不同,分两个图标 |
|
||||
| 🗂️ | quick"我的计划"、profile"我的计划" | 我的计划 | `folder-open` | |
|
||||
| 👤 | quick"我的"、bottom-nav"我的" | 个人 | `user` | |
|
||||
| 🏠 | bottom-nav"首页" | 首页 | `home` | |
|
||||
| 📚 | bottom-nav"分类" | 分类浏览 | `layout-grid` | |
|
||||
| ❤️ | profile"收藏"、detail/collection 收藏切换 | 收藏 | `heart`(outline/fill 两态) | 替 ♥/♡ 字符 |
|
||||
| ♥ / ♡ | detail、collection-detail 收藏按钮 | 收藏切换 | `heart` | 字符→SVG |
|
||||
| ⭐ | profile"成就"、plan-favorites 空态 | 成就/收藏 | `star`(成就)/ `star` outline(空态) | |
|
||||
| 🔥 | (个性化档)连续训练天数 | 连击/能量 | `flame` | 新区块用 |
|
||||
| 🎯 | (个性化档)今日推荐 | 目标/推荐 | `target` | 新区块用 |
|
||||
| ⚡/🌟/🤸/🧘/🥊/🚴/🐯/🦁 | plan-edit / profile 的"emoji 选择器" | 用户自选头像/计划图标 | **保留 emoji 选择器(UGC)** 或迁移到 Lucide 子集 `dumbbell/run/bike/heart/flame/target/yoga/medal` | 见 §2.4 |
|
||||
|
||||
### 2.4 关于"用户自选 emoji"的边界判定(重要)
|
||||
|
||||
P0-1 红线的例外是"**emoji 仅允许出现在用户生成内容(UGC)**"。因此:
|
||||
|
||||
- **plan-edit 的 emoji 选择器、profile 的头像 emoji 选择器**:这是用户表达(UGC),**允许保留 emoji 选择器**。
|
||||
- 但:**用所选 emoji 渲染出的"计划图标/头像"若出现在卡片头部(plan-card、collection-detail、plan-detail 的 `header__emoji`)**,它已从"用户输入"变成"界面元素",长期应迁移到 **SVG 图标选择器**(从 Lucide 取一个运动子集:dumbbell/run/bike/heart/flame/target/yoga/medal/trophy)。
|
||||
- **短期方案(MVP)**:保留 UGC emoji 选择器;系统默认图标、所有功能导航、空态、占位、收藏切换 **一律 SVG**。这样 P0-1 在"功能图标"维度完全达标,UGC 例外成立。
|
||||
|
||||
---
|
||||
|
||||
## 3. 首页个性化区块视觉草案
|
||||
|
||||
> 寄存器=Product,故"个性化"不是营销炫技,而是**降低用户启动成本的实用信息密度**。两档按 PM 选定的范围实现。
|
||||
|
||||
### 3.1 轻量档(Light):我的计划 · 收藏动作 · 今日推荐
|
||||
|
||||
**布局顺序**(插入 Hero 之后、精选动作之前,作为"为你"区):
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ [section-header] 为你准备 action:编辑 │
|
||||
│ │
|
||||
│ ┌─ 我的计划卡(整卡可点)──────────┐ │
|
||||
│ │ 📋 推胸日 · 第 3 周 进行中 │ │ ← clipboard-list 图标 + 状态 pill
|
||||
│ │ 今日 6 个动作 · 已完成 2/6 │ │
|
||||
│ │ ▓▓▓▓▓▓▓░░░░░ 33% │ │ ← 线性进度条(accent 填充)
|
||||
│ └─────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ 今日推荐(AI 行)──────────────┐ │
|
||||
│ │ ✨ 上斜哑铃推胸 │ │ ← sparkles + 动作名
|
||||
│ │ 根据你的推胸计划,匹配度 92% │ │ ← 一行理由(具体,非模板)
|
||||
│ │ › │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [section-header] 收藏的动作 action:全部│
|
||||
│ ┌────┐ ┌────┐ ┌────┐ ← 横滑缩略卡 │
|
||||
│ │ 💪 │ │ 💪 │ │ 💪 │ (dumbbell 占位) │
|
||||
│ │名 │ │名 │ │名 │ │
|
||||
│ └────┘ └────┘ └────┘ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**卡片样式(轻量档)**
|
||||
- 我的计划卡:surface 底 + 1px border(ring),圆角 lg(32rpx),内边距 space-5(40rpx);左侧 `clipboard-list` 20rpx 图标 + 计划名(fg,semibold);右上状态 pill(`进行中`=accent-soft 底 + accent 字,或 `已完成`=success-soft 底 + success 字);进度条高 8rpx、圆角 pill、track=border、fill=accent,宽度=完成比例。
|
||||
- 今日推荐行:与计划卡同容器规格,但左侧用 `sparkles` 20rpx(accent 色);动作名 fg semibold;理由行 fg-2 24rpx 一行截断;右箭头 `›`(用 SVG `chevron-right`,不字符)。
|
||||
- 收藏动作横滑:每张 200rpx 宽,surface 底 + ring,圆角 md;缩略图区 16:10 比例(用 `dumbbell` 占位 SVG 直到真实 GIF 加载),下方动作名 24rpx 单行省略。
|
||||
|
||||
### 3.2 完整档(Full):+ 进度环 · 连续天数 · 历史曲线
|
||||
|
||||
在轻量档顶部追加一个 **「本周概览」卡**(一卡三信息,密度克制):
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ [section-header] 本周概览 │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ ╭───╮ 连续训练 │ │
|
||||
│ │ │ 4 │ 🔥 12 天 │ │ ← 进度环 + 中心数字(4/5)
|
||||
│ │ │ ╱5 │ 本周 4/5 次 │ │ 火焰图标+天数(stat)
|
||||
│ │ ╰───╯ │ │
|
||||
│ │ ──╱╲──╱── 历史曲线(14天) │ │ ← 内联 SVG 面积图
|
||||
│ │ 训练时长趋势 ↑ 8% │ │ trending-up + 文案
|
||||
│ └─────────────────────────────────┘ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**视觉表达规范**
|
||||
- **进度环**:SVG `<circle>` 双环。track=`border`(1.5px 在 120rpx 直径上≈2px),progress=`accent`(或成功语义绿,当"完成周目标"用 success),`stroke-linecap:round`,转角用 `stroke-dasharray` 动画 300ms 进入。中心数字用 mono 字体、`4xl(72rpx)` 主数 + `sm` 分母。环直径 ~120rpx,整体占卡左侧 1/3。
|
||||
- **连续训练天数**:`flame` 24rpx(accent 或 warn 色,不抢进度环)→ 数字 mono `xl(36rpx)` fg semibold → "天" 后缀 fg-2 `sm`。作为小统计块置于环右侧第一行。
|
||||
- **历史曲线**:内联 SVG 面积图(宽 ~100%,高 ~120rpx),14 个数据点(近 14 天训练分钟),填充 `accent` 8% 渐变 + 1.5px `accent` 描边线,末端小圆点。X 轴不标注,Y 靠趋势文案"↑ 8%"传达(**不靠颜色单独传达含义**——配文字)。下方一行 `trending-up` 20rpx + "训练时长较上周 +8%"。
|
||||
- 整卡:surface + ring,圆角 lg,内边距 space-5;内部用 `divide-y`/间距分组,不嵌套卡片。
|
||||
|
||||
### 3.3 状态覆盖(两档通用,5 态)
|
||||
|
||||
| 状态 | 表现 |
|
||||
|------|------|
|
||||
| Loading | 区块骨架屏(与卡同形的灰色脉冲块,accent-soft 微光) |
|
||||
| Empty | 空态:SVG 图标(非 emoji)+ 一句引导("还没有计划?去挑选一个官方计划")+ 主按钮(accent) |
|
||||
| Error | "网络开小差了" + 重试按钮(ghost) |
|
||||
| Populated | 上文布局 |
|
||||
| Edge | 计划名超长 → 单行省略;进度 0% → 显示"开始第一次"而非空环;连续天数 0 → 显示"开始你的连击" |
|
||||
|
||||
---
|
||||
|
||||
## 4. 反模式自检(13 项视觉清单 + P0 三绝对规则)
|
||||
|
||||
### 4.1 P0 三绝对规则(逐条)
|
||||
|
||||
- **P0-1 禁止 emoji 作功能图标**:✅ 本提案**全量替换**功能图标为 Lucide SVG(§2.3 映射 18 处);仅 UGC 选择器保留 emoji(§2.4 已界定边界)。通过。
|
||||
- **P0-2 禁止紫色→粉色渐变**:✅ 新配色无紫无粉;强调色为珊瑚橙 `#FF6A3D`,语义绿 `#2BD9A6` 仅用于正向。**无** `#7C3AED/#A855F7/#EC4899` 任意组合。通过。
|
||||
- **P0-3 禁止 AI 模板味**:✅ 颜色**全走 Token**(§1.1,design-tokens.json 机读);无 "Lorem ipsum"/"Welcome to";Hero 改为展示真实训练内容(§3 个性化区块即真实数据);去"全大写英文 eyebrow + 图标网格 + 金渐变 CTA"模板味。通过。
|
||||
|
||||
### 4.2 十三条视觉清单
|
||||
|
||||
| # | 检查项 | 本提案 | 结论 |
|
||||
|---|--------|--------|------|
|
||||
| 1 | 无 emoji 功能图标 | Lucide 全量替换 | ✅ |
|
||||
| 2 | 无紫粉渐变 | 珊瑚橙 + 语义绿 | ✅ |
|
||||
| 3 | 无 AI 模板味/空洞文案 | 真实数据 Hero、具体按钮文案 | ✅ |
|
||||
| 4 | 颜色全走 Token | design-tokens.json,页面级禁止裸 hex(除 #000/#fff 例外,本项目仅 accent-on 用深暖色) | ✅ |
|
||||
| 5 | 间距全 4px 网格 | 8/16/24/32/40/48/64/80/96/128 rpx,清掉 18/22/28/36 非标 | ✅ |
|
||||
| 6 | 字体三族含中文 | Manrope+PingFang SC(CJK)/ JetBrains Mono(数据) | ✅ |
|
||||
| 7 | Hero 真实内容非口号 | 个性化区块承载真实计划/进度/推荐 | ✅ |
|
||||
| 8 | 图标库锁定一套+尺寸 16/20/24 | Lucide,§2.2 | ✅ |
|
||||
| 9 | 无纯黑/纯灰裸用 | bg `#0B0F0E` 非纯黑;灰带冷调;语义色有 chroma | ✅ |
|
||||
| 10 | 对比度 + reduced-motion + 焦点 | fg/fg-2 达标;motion 尊重 reduced;focus-ring accent 3px | ✅(实现期需 audit 复测) |
|
||||
| 11 | 安全区 + 触摸目标≥44 | `env(safe-area-inset-bottom)`;图标按钮命中区≥88rpx | ✅ |
|
||||
| 12 | 组件 5 态覆盖 | §3.3 loading/empty/error/populated/edge | ✅ |
|
||||
| 13 | 每屏强调色≤2 处 | accent(珊瑚橙) 用于 CTA+激活导航;success 仅正向语义;克制 | ✅ |
|
||||
|
||||
### 4.3 设计时主动规避的 AI 模板禁令(对照绝对禁令)
|
||||
|
||||
- 侧条纹边框:`border-left>1px` 彩色强调 → **不用**,选中态用 `border-strong` 整圈或 accent-soft 底。
|
||||
- 渐变文字 `background-clip:text` → **不用**,强调靠字重/字色。
|
||||
- 默认毛玻璃 → 仅底部导航用 `backdrop-filter:blur`(功能目的:内容滚过不刺眼),不作装饰。
|
||||
- 幽灵卡片(1px 边框+大阴影同屏)→ **改为 hairline-first**,默认无阴影。
|
||||
- 过度圆角(≥24px)→ 卡片上限 16px(32rpx)。
|
||||
- 每节相同淡入 → 进入动画匹配内容(进度环转、曲线绘、卡淡入各异)。
|
||||
- 奶油/米色默认化 → 基底为冷中性近黑,暖意由 accent 传达。
|
||||
|
||||
---
|
||||
|
||||
## 5. 落地建议(给架构师 / 前端)
|
||||
|
||||
1. **图标**:从 Lucide 取 §2.3 所列子集,用 `iconfont` 工具编译为 `.ttf/.woff2` 放入 `miniprogram/assets/fonts/`,在 `app.wxss` 用 `@font-face` 声明 `fc-icon`;组件用 `<text class="fc-icon fc-icon--20">{{codepoint}}</text>`。需我提供 codepoint 映射表可补。
|
||||
2. **Token 注入**:把 `design-tokens.json` 的 color/spacing/radius/shadow 编译进 `app.wxss` 的 `page{}` 变量;fontSize/letterSpacing/motion 同步为 WXSS 变量;JS 侧把字号/色值常量化供 `setData` 用(如动态 `style="background:{{item.color}}22"` 这类应改为 Token 变量)。
|
||||
3. **去硬编码**:全局替换 `#0E1110`→`var(--bg)`、`#E5544B`→`var(--danger)`、`rgba(229,84,75,…)`→`var(--danger-soft)` 等(grep 命中见 §0.2)。
|
||||
4. **动态色迁移**:`index.wxml` 里 `style="background:{{item.color}}22;color:{{item.color}}"`(训练组合/计划卡按色渲染)应改为从"预设语义色板"取 Token 别名(如 `var(--accent-soft)`),避免任意 hex 注入破坏系统。
|
||||
5. **需 PM/架构师确认**:accent 最终色(珊瑚橙 vs Volt 绿 vs Ion 青);个性化区块选轻量档还是完整档;UGC emoji 选择器是否迁移到 SVG 图标选择器。
|
||||
|
||||
---
|
||||
*文档结束。机读 Token:`design-system/design-tokens.json`。视觉草案可出 SVG 线框(如需我可补 `home-personalization-mockup.svg`)。*
|
||||
117
FITCOACH-OPTIMIZATION-PLAN.md
Normal file
117
FITCOACH-OPTIMIZATION-PLAN.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# FITCOACH 系统优化 · 整合实施方案(待确认)
|
||||
|
||||
> 整合自三份调研:PM《优化 PRD》(PRD-FITCOACH优化方案.md)、设计师《动能暗色提案》(FITCOACH-DESIGN-PROPOSAL.md + design-system/design-tokens.json)、架构师技术方案(本文件第 2 节,由团队负责人据代码实测补齐)。
|
||||
> 已确认方向(用户 2026-07-22 拍板):① 先出方案文档、确认后再开发;② 首页个性化两档都要(先轻量后完整);③ UI 重塑设计语言。
|
||||
> P0 红线:禁止 emoji 当功能图标(统一 Lucide SVG iconfont)、禁止紫粉渐变、禁止硬编码颜色(一律走 Token)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 现状诊断(代码实测结论)
|
||||
|
||||
| 问题 | 证据 | 影响 |
|
||||
|------|------|------|
|
||||
| 首页零个性化 | `pages/index/index.js` `loadData()` 只拉全局 stats/featured/collections,无 openid 维度;"今天,练点什么?"写死 | 登录与不登录完全一致,无归属感 |
|
||||
| emoji 图标系统性违规 | 首页 `🔍✨💪🏋️📋🗂️👤`、底部导航 `🏠📚📋👤`、profile `❤️🗂️⭐📋💪`、各空态、占位 `🏋️`;后端 `users.service.ts` 默认 avatar=`🏃`、`plans.service.ts` 默认 emoji=`📋` | 触发 P0-1 红线,全站廉价感 |
|
||||
| 加载/错误态缺失 | 首页 `loading` 是死代码(wxml 未引用)首屏空白;`request.js` 失败只 toast,网络失败与无数据无区分、无重试 | 弱网下体验差、无兜底 |
|
||||
| 响应速度 | `request.js` 无 timeout(wx.request 默认不强制超时);首页 `Promise.all` 无缓存;底部导航 `reLaunch` 重置栈→切 tab 重拉 | 弱网挂起、切 tab 白屏 |
|
||||
| 硬编码颜色散落 | 13 个 wxss 裸写 `#0E1110`(8+处)、`#E5544B`/`rgba(229,84,75,…)`;`app.wxss` Token 命名偏金、缺语义色/字号阶梯/字距 | 改主题要改 N 处 |
|
||||
| 设计令牌基础尚在 | `app.wxss` 已定义 `--bg/--surface/--gold/--text/--line/--radius` | 重塑改一处 + 组件局部即可,不动业务 |
|
||||
|
||||
**关键利好**:个性化所需接口其实已就绪——`planSvc.listMine()`→`GET /api/plans`(按 openid 隔离,已验证 `plans.controller.ts:35`)、`favSvc.listExercises()`→`GET /api/favorites/exercises`(已验证 `favorites.controller.ts:26`)、`auth` 登录态。轻量档**零后端改动**即可上线。
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构技术方案(补齐)
|
||||
|
||||
### 2.1 轻量档(P0,纯前端,零后端改动)
|
||||
- **首页 `index.js`** `onLoad`:若已登录(`wx.getStorageSync('access_token')` 存在且有效),并行拉:
|
||||
- `planSvc.listMine()` → 我的计划列表
|
||||
- `favSvc.listExercises()` → 我的收藏动作
|
||||
- 今日推荐(前端推导):取 `GET /api/plans/official` 分级 + 用户收藏/浏览,按"未练过/匹配目标"排序取 top3(复用推荐页现有推导逻辑,无需新接口)
|
||||
- 用户资料:`auth.getProfile()` 或本地 `userInfo`
|
||||
- **渲染**:`isLogin` 为真 → 在 Hero 后插入"为你准备"区(时段问候 + 我的计划卡 + 收藏横滑 + 今日推荐);否则保留原通用内容 + 轻引导登录。
|
||||
- **缓存**:用 `wx.setStorageSync` 缓存 listMine/favorites(key 带 openid);切 tab(`onShow`) 优先读缓存、后台静默刷新,避免 `reLaunch` 重拉白屏。
|
||||
- 工作量:前端 2-3 人日。
|
||||
|
||||
### 2.2 完整档(P1,新增后端数据)
|
||||
沿用现有 `common/store.ts` 的 JsonStore 单例模式(已验证:`getStore(name)` 进程内单例、整体落盘 `data-store/<name>.json`)。
|
||||
|
||||
**新集合 `checkins`**(data-store/checkins.json):
|
||||
```
|
||||
{ id, openid, date(YYYY-MM-DD, openid+date 唯一), planId?, exerciseIds[], durationMin?, completed(bool), createdAt }
|
||||
```
|
||||
**users 扩展字段**:`fitnessGoal`(减脂/增肌/塑形/保持健康)、`level`(beginner/intermediate/advanced)、`trainFreq`(每周次数)、`onboarded`、`avatar`(改 Lucide 图标名/URL,去掉 emoji)。
|
||||
|
||||
**打卡触发点**:plan-detail「完成训练」按钮 / 首页「今日打卡」按钮 → `POST /api/checkins`。
|
||||
|
||||
**派生计算**(无需单独存储,由 checkins 实时算):
|
||||
- `streak`:按 date 倒序,从今天/昨天起连续有 checkin 的天数
|
||||
- `summary(range=7|30)`:训练次数、总时长、动作分布、完成率
|
||||
- 历史曲线:近 14 日每日 checkin 数/时长 → 前端 SVG 面积图
|
||||
|
||||
**新增 API**(均 `@UseGuards(AuthGuard)` + `CurrentUser` 取 openid,沿用 plans/favorites 模式):
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/api/checkins` | 创建/更新当日打卡 |
|
||||
| GET | `/api/checkins/streak` | `{ streak, lastCheckinDate }` |
|
||||
| GET | `/api/checkins/summary?range=7\|30` | `{ count, totalDuration, byExercise[], byDay[] }` |
|
||||
| PATCH | `/api/users/me` | 扩展画像字段 |
|
||||
|
||||
- 工作量:后端 3-4 人日 + 前端 4-5 人日。需新建 `checkins.module/controller/service/(entity+dto)` + `users.service` 扩展。
|
||||
|
||||
### 2.3 UI Token 化改造(P0)
|
||||
- 以 `design-system/design-tokens.json` 为唯一真源,整体替换 `app.wxss` 的 `page` 变量:冷中性底(`#0B0F0E/#141A18/#1C2421`) + 珊瑚橙 accent(`#FF6A3D`) + 语义色(success/warn/danger/info) + 字号阶梯(22-72rpx) + 间距尺度(4px 网格) + 圆角(16/24/32) + 阴影(Hairline First) + 动效(150/200/300ms)。
|
||||
- 13 个页面/组件 wxss:把裸写 `#0E1110`/`#E5544B`/大阴影改为引用新 Token。
|
||||
- 图标落地:编译 Lucide 子集(约 30-40 个图标)为 `miniprogram/assets/fonts/fc-icon.ttf`,`app.wxss` `@font-face` 声明 `fc-icon`;提供 emoji→Lucide name→codepoint 映射表,全站 `<text class="fc-icon">` 替换 emoji 文本节点。
|
||||
- 风险:主包体积(iconfont ttf <50KB 可控);个别 wxml 用 emoji 字符作图标需逐个替换;微信支持本地 ttf 自定义字体。
|
||||
|
||||
### 2.4 综合 UX 技术支撑(P0/P1)
|
||||
- **请求治理**:`request.js` 加 `timeout: 8000` + 失败类型区分(网络/业务/无数据) + 重试一次 + 可选缓存层,弱网不再挂起。
|
||||
- **骨架屏**:首页/列表页加 skeleton 占位(token 色块),替代当前首屏空白;接上原 index.wxml 死代码 `loading` 或换 skeleton。
|
||||
- **错误重试**:request 失败提供"重试"按钮(error 态),替代纯 toast。
|
||||
- **tab 保活**:bottom-nav `reLaunch` 重置栈→改 `switchTab` + 首页数据缓存 + `onShow` 静默刷新,切 tab 不白屏。
|
||||
- **常驻搜索**:首页 search-pill 换成 SVG 图标(或 navbar 常驻)。
|
||||
|
||||
### 2.5 技术约束与风险
|
||||
- JsonStore 单进程单例、整体落盘——打卡高频写 flush 阻塞;demo 量级 OK,生产前置足够。
|
||||
- 主包 2MB 限制:iconfont ttf <50KB 可控;不打包 CJK 字体(用系统字体,已确认)。
|
||||
- 图标库:**锁定 Lucide(MIT)**,与设计师提案一致 ✅;尺寸 16/20/24rpx;编译为 iconfont 子集。
|
||||
|
||||
---
|
||||
|
||||
## 3. 实施路线图(优先级)
|
||||
|
||||
| 优先级 | 项 | 档位 | 后端 | 前端 | 依赖 |
|
||||
|--------|----|------|------|------|------|
|
||||
| **P0** | emoji→Lucide SVG 图标替换(全站 18+ 处 + 后端默认 avatar/emoji) | UI | 小 | 中 | 无 |
|
||||
| **P0** | Design Token 重塑(Kinetic Dark,app.wxss + 13 wxss) | UI | 无 | 中 | 无 |
|
||||
| **P0** | 首页轻量个性化(问候+我的计划+收藏+今日推荐) | 个性化·轻 | 无 | 2-3人日 | 现有 API |
|
||||
| **P0** | 骨架屏 + 错误重试 + 请求 timeout | UX | 无 | 2-3人日 | 无 |
|
||||
| **P1** | 完整个性化后端(checkins store + 3 接口 + users 扩展) | 个性化·全 | 3-4人日 | — | 轻量档先上 |
|
||||
| **P1** | 首页完整档区块(进度环+连续天数+历史曲线) | 个性化·全 | — | 4-5人日 | 后端接口 |
|
||||
| **P1** | 导航重构 + 常驻搜索 + tab 保活 + 缓存 | UX | 无 | 中 | 请求治理 |
|
||||
| **P2** | 下拉刷新 / 长按手势 / 进入动画 | UX | 无 | 小 | 无 |
|
||||
|
||||
**MVP 切割**:P0 四项先上(验证假设、零风险、零后端)→ P1 完整档跟进(需打卡数据沉淀)→ P2 打磨。
|
||||
**上线顺序**:UI 重塑 + 轻量个性化 + 骨架屏 同批(都在前端,互不阻塞);完整档独立一批(需后端发布)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 已锁定决策(2026-07-22 用户确认)
|
||||
|
||||
1. **强调色 accent = Volt 荧光绿 `#B6F24E`**(用户选,非设计师默认珊瑚橙)。设计 Token 中 `accent` 系列全部替换为该绿:`accent:#B6F24E` / `accent-hover:#C7F56E` / `accent-active:#9EDB3A` / `accent-on:#0E1A05`(深绿字,对比>7:1) / `accent-soft:rgba(182,242,78,.12)` / `accent_ring:rgba(182,242,78,.38)` / `glow:0 8rpx 24rpx rgba(182,242,78,.28)`。每屏≤2处。
|
||||
2. **UGC emoji 选择器 → 现在就迁 Lucide 运动子集图标选择器**(dumbbell/run/bike/heart/flame/target/yoga/medal/trophy 等),不保留 emoji 入口。plan-edit/profile 的计划图标与用户头像图标均走 Lucide。
|
||||
|
||||
---
|
||||
|
||||
## 5. Out-of-Scope(本期不做)
|
||||
社交/社区、商城电商、课程直播跟练、饮食营养、硬件联动、i18n、SQL 迁移、生成式 AI 文案、付费会员。
|
||||
|
||||
---
|
||||
|
||||
## 附:交付物索引
|
||||
- `PRD-FITCOACH优化方案.md` — PM 优化 PRD(现状/竞品/三块方案/RICE/验收标准)
|
||||
- `FITCOACH-DESIGN-PROPOSAL.md` — 设计师动能暗色提案(审计/Token/图标/视觉草案)
|
||||
- `design-system/design-tokens.json` — 机读 Design Token 草案(唯一真源)
|
||||
- `home-personalization-mockup.svg` — 完整档首页个性化区视觉线框
|
||||
- 本文件 — 整合实施方案 + 架构补齐 + 路线图 + 待定项
|
||||
308
PRD-FITCOACH优化方案.md
Normal file
308
PRD-FITCOACH优化方案.md
Normal file
@@ -0,0 +1,308 @@
|
||||
# FITCOACH 优化评估与改进方案(PRD)
|
||||
|
||||
> 文档属性
|
||||
> - 版本:v1.0(方案稿,待产品负责人确认后进入开发)
|
||||
> - 日期:2026-07-22
|
||||
> - 作者:许清楚(产品经理)
|
||||
> - 范围:在现有「微信健身小程序 + NestJS 后端 + JSON 数据落地」架构上做体验优化,**不新增业务线、不写代码**,先方案后开发
|
||||
> - 目标三大块:① 页面 UI 美观度(重塑设计语言)② 登录后首页个性化(轻量档 + 完整档)③ 综合 UX(交互 / 信息架构 / 响应速度 / 导航)
|
||||
|
||||
---
|
||||
|
||||
## 0. 结论速览(给决策者的话)
|
||||
|
||||
FITCOACH 当前是一个"功能完整但体验半成品"的产品:后端数据能力(计划、收藏、用户、推荐)已经具备,但**首页完全没有用上这些数据**,且**全站用 emoji 当功能图标**(触碰团队 P0 红线),加载/错误态缺失,首页一次请求卡死整页。
|
||||
|
||||
三块改进的整体排序建议:
|
||||
|
||||
- **P0(本期必做,低风险高显效)**:emoji 图标清零 + 设计令牌重塑(块一);首页轻量档个性化(块二);全站骨架屏 + 错误重试态(块三)。
|
||||
- **P1(本期第二阶段)**:完整档个性化(新增训练打卡 + 连续天数 + 进度曲线);用户画像 onboarding;全局常驻搜索 + 导航重构;请求超时与缓存 + tab 保活。
|
||||
- **P2(打磨 / 下期)**:周/月训练报告(雷达图);下拉刷新 / 长按 / 过渡动画等手势完善;无障碍对比度审计。
|
||||
|
||||
---
|
||||
|
||||
## 1. 现状诊断(基于代码实测,含文件与行号证据)
|
||||
|
||||
### 1.1 首页零个性化(核心痛点)
|
||||
|
||||
`miniprogram/pages/index/` 是一个**完全静态**的页面:
|
||||
|
||||
- Hero 文案写死为 "今天,练点什么?"(`index.wxml:5`),**没有任何用户昵称问候、没有时段判断**。
|
||||
- `loadData()` 只拉取全局数据:`getStats()` / `listExercises()` / `listCollections()`(`index.js:28-46`),**不带任何 openid / 用户维度**。
|
||||
- 已存在、却未用于首页的个性化数据(接口全部就绪,纯前端即可调用):
|
||||
- 我的计划:`planSvc.listMine()` → `GET /api/plans`(`services/plan.js:24`,已在 `my-plans.js` 使用)
|
||||
- 收藏动作:`favSvc.listExercises()` → `GET /api/favorites/exercises`(`services/favorite.js:17`,已在 `favorites.js` 使用)
|
||||
- 用户资料(含昵称):`auth.getProfile()`(`utils/auth.js:73`,本地存储读取,零网络开销)
|
||||
- 智能推荐引擎:`svc.recommend({target, equipment})`(`services/exercise.js:45`)——但需 `target` 参数,首页当前未调用
|
||||
- 交付与文档不一致:`OVERVIEW.md:34` 声称首页有"渐变 hero + **问候** + 智能推荐 CTA",实际"问候"并未实现,只是通用文案。
|
||||
|
||||
**结论**:个性化不是"从零造数据",而是"把已经存在的能力接到首页上"。这正是轻量档改动小、见效快的根本原因。
|
||||
|
||||
### 1.2 emoji 图标违规(团队 P0 红线,系统性问题)
|
||||
|
||||
全站把 emoji 当作功能图标使用,违反"禁止 emoji 作为功能图标"规则。实测清单:
|
||||
|
||||
| 位置 | 文件:行 | emoji 用途 |
|
||||
|------|---------|-----------|
|
||||
| 底部导航 | `components/bottom-nav/index.js:7-10` | 首页/分类/计划/我的 用 🏠📚📋👤 |
|
||||
| 首页快捷入口 | `pages/index/index.js:14-19`、`index.wxml:29` | ✨💪🏋️📋🗂️👤 |
|
||||
| 首页搜索 / CTA | `pages/index/index.wxml:9,14` | 🔍✨ |
|
||||
| 搜索页图标 | `pages/search/search.wxml:5` | 🔍 |
|
||||
| 个人中心单元格 | `pages/profile/profile.wxml:6,41,46,51,56` | 💪❤️🗂️⭐📋 |
|
||||
| 空态插画 | `favorites.wxml:20`、`my-plans.wxml:20`、`detail.wxml:16`、`plan-detail.wxml:23`、`plan-favorites.wxml:16` | 🔍📋🏋️⭐ |
|
||||
| 练习卡占位图 | `components/exercise-card/index.wxml:10` | 🏋️ |
|
||||
| 头像组件默认 | `components/avatar/index.js:5`、`index.wxml:4` | 💪 |
|
||||
| 头像/计划 emoji 选择器 | `pages/profile/profile.js:6`、`pages/plan-edit/plan-edit.js:4` | 一整组 emoji 数组 |
|
||||
|
||||
**数据层也被污染**(必须一并清理,否则新用户一进来还是 emoji):
|
||||
- `backend/src/users/users.service.ts:26`:`avatar` 默认值 `'🏃'`
|
||||
- `backend/src/plans/plans.service.ts:37`:`DEFAULT_EMOJI = '📋'`,且 `PlanRecord.emoji` 字段贯穿数据库与前端
|
||||
|
||||
**影响**:emoji 在微信不同机型/系统字体下渲染不一致,描边与对比度不可控,品牌感弱,无障碍差(屏幕阅读器念出无意义字符)。
|
||||
|
||||
### 1.3 信息架构与导航
|
||||
|
||||
- 底部导航 4 项:首页 / 分类 / 计划 / 我的。其中"计划"实际指向 `my-plans`(**个人**计划),与"分类"(内容浏览)并列,语义层级略乱;"官方计划"入口散落在首页快捷入口 + `collection` 页,缺乏统一归属。
|
||||
- 首页单页塞了 6 个 section(hero / 2×3 快捷 / 精选动作 / 训练组合 / 按部位 / 热门器械)+ 160rpx 底部留白(`index.wxml` 全篇),首屏重点被稀释。
|
||||
- **无全局常驻搜索**:搜索只在 hero 内一个 pill,点开才进二级 `search` 页(`index.wxml:8-11`)。
|
||||
- 自定义导航栏 `navbar` 仅在子页使用(`favorites.wxml:2`、`my-plans.wxml:2`),首页没有统一导航条,与子页视觉割裂;`app.json:18` 设了 `navigationStyle: custom`,每个页面需自行承担导航。
|
||||
|
||||
### 1.4 加载态 / 空态 / 错误态
|
||||
|
||||
- **首页 loading 是死代码**:`index.js:12` 声明 `loading: true`,但 `index.wxml` 全程未引用任何 `wx:if="{{loading}}"` → 首屏在数据到达前是**空白/白屏**,无任何指示。
|
||||
- 子页仅有纯文本"加载中…",无骨架屏(`favorites.wxml:24`、`my-plans.wxml:25`)。
|
||||
- **错误态彻底缺失**:`utils/request.js:34-41` 失败时只弹 toast,页面 `catch` 后 `setData({loading:false})` 得到空列表(`favorites.js:29-31`、`my-plans.js:28-30`),**网络失败与"真·无数据"无法区分,且无重试入口**。
|
||||
- 空态结构本身尚可(插画 + 文案 + 引导按钮),但插画是 emoji(见 1.2),随块一替换。
|
||||
|
||||
### 1.5 响应速度 / 交互
|
||||
|
||||
- `utils/request.js:16-42` 的 `wx.request` **没有 timeout 配置**(微信默认也无强制超时)→ 弱网下可能长时间挂起,用户无感知。
|
||||
- 首页 `Promise.all` 聚合 3 个接口(`index.js:30-34`),任一慢则整体延迟;无本地缓存、无预取。
|
||||
- 底部导航用 `wx.reLaunch` 重置页面栈(`bottom-nav/index.js:18`)→ 每次切 tab 整页重建、首页 `loadData()` 重跑,**跨 tab 状态不保留**,体验被切断;行业通行做法是保留 tab 状态(如原生 tabBar / 自定义保活)。
|
||||
- 全站**无下拉刷新**(`onPullDownRefresh` 未出现)。
|
||||
- GIF 演示走 jsDelivr CDN 直链(`OVERVIEW.md:59`),弱网加载慢,无占位过渡。
|
||||
|
||||
### 1.6 设计令牌现状(这是"重塑"的有利基础)
|
||||
|
||||
暗金奢华风集中定义在 `app.wxss` 的 `page` 选择器内(CSS 变量):
|
||||
|
||||
- 主色 `--gold:#D9B36C`、`--gold-2:#F0D9B9`、点缀 `--teal:#58C9B9`;底色 `--bg:#0E1110`(`app.wxss:4-12`)
|
||||
- 圆角 `--radius:28rpx` / `--radius-sm:18rpx`;阴影 `--shadow`(`app.wxss:17-19`)
|
||||
|
||||
**利好**:令牌高度集中,重塑设计语言只需改 `app.wxss` 一处 + 个别组件局部样式,**几乎不动业务逻辑**。
|
||||
|
||||
**风险点**:`navbar / section-header / chip / level-tag / plan-card / exercise-card` 等组件各有独立 `.wxss`,需排查是否存在硬编码色值(如 `plan-card` 用到计划自带的 `color` 字段、`prog` 用到集合 `color`),重塑时要一并纳入令牌体系。
|
||||
|
||||
---
|
||||
|
||||
## 2. 竞品对标(联网调研 5 家健身 / 运动 App)
|
||||
|
||||
> 说明:项目内 `references/industries/` 下未提供健身行业文档,以下对标全部来自联网调研(2026-07-22 检索)。
|
||||
|
||||
| App | 类型 | 设计语言 / UI 特征 | 个性化首页做法 | 可借鉴点 | 我们的差距 |
|
||||
|-----|------|-------------------|---------------|----------|-----------|
|
||||
| **Keep 9.0**(直接竞品,国内最大) | 课程+计划+社区 | 信息架构做减法:砍商城、课程/计划双 tab、单列 Feed、灰白一致入口 | 首页按"往期运动数据"智能推荐课程;"我的"页重构为 数据概览/全部数据/运动记录;**健康状态卡片**(仪表盘)给当日评估+建议;**上周周报五维雷达图+环比箭头**;1 分钟问卷生成专属计划 | ① 个性化 = 用户历史 + 当下状态 ② 进度可视化(雷达/7日图)③ AI 克制:每条建议有数据支撑,不煽情 ④ onboarding 问卷建训练画像 | 我们首页无任何个性化;无状态/周报可视化 |
|
||||
| **Apple Fitness+**(标杆) | 设备+课程 | 黑色背景、固定三环配色、高对比无障碍、一致视觉层级 | **Activity Rings(三环:Move/Exercise/Stand)** 游戏化进度;三核心指标化简复杂数据,降低认知负荷;目标可个性化(Move goal 最常调) | ① 游戏化连续/进度直击"完整档 连续天数" ② 用最少指标表达状态 ③ 黑色背景+固定配色的一致语言 | 我们无进度/连续概念;图标系统混乱 |
|
||||
| **Fitbod**(直接竞品,AI 力量训练) | 计划+记录 | **干净、聚焦、不堆徽章**;肌肉恢复热力图直观 | 基于训练历史**每日更新个性化计划**;输入目标/经验/器械/时长 → 生成"living training profile";记录每次训练→算 1RM→推荐下次;"今天在哪练"按环境适配 | ① 训练画像驱动自适应 ② 身体就绪度可视化(恢复热力图)③ 界面克制 | 我们无训练记录,无法自适应 |
|
||||
| **Strava**(替代方案,社交/连续) | 社区+记录 | Feed 基于互动行为的个性化排序;收藏运动员置顶 | **连续/挑战/分段(segments)机制**驱动留存 | ① 连续天数 + 挑战的游戏化钩子 ② 但**强制非时间序被用户骂** → 教训:个性化要可关闭/可切换 | 我们无社交层,但连续机制可独立借鉴 |
|
||||
| **Gymshark Training**(替代方案,品牌) | 品牌+免费计划 | 极简、品牌一致、快速 onboarding(~1 分钟) | **个性化弱**:onboarding 只问年龄性别;进度追踪仅"标记完成/记录组数" | 反面教材:好设计 ≠ 好教练;缺 adaptive programming 与 analytics 会被进阶用户抛弃 | 提醒我们:仅做"好看的首页"不够,需有数据闭环 |
|
||||
|
||||
**提炼 6 条可迁移最佳实践**:
|
||||
|
||||
1. 个性化首页 = "用户已有数据" + "当下状态"(Keep 健康状态卡 / Fitbod 恢复热力图)。
|
||||
2. 游戏化连续 / 进度(Apple 三环 / Strava streak)——直接支撑"完整档 连续训练天数"。
|
||||
3. 自适应 AI 但克制(Keep 不煽情、每条建议有据可依),不堆无意义徽章(Fitbod)。
|
||||
4. 信息架构做减法(Keep 9.0 砍商城、简化导航、单列 Feed)——治我们的首页 6 段过载。
|
||||
5. onboarding 问卷建训练画像(Fitbod / Keep 1 分钟)——支撑完整档要新增的"训练目标"字段。
|
||||
6. 进度可视化(雷达图 / 7 日图 / 曲线)而非数字堆砌。
|
||||
|
||||
---
|
||||
|
||||
## 3. 三块改进方案 + 实施优先级
|
||||
|
||||
### 3.1 块一:页面 UI 美观度 —— 重塑设计语言(P0,用户已确认方向)
|
||||
|
||||
**目标**:由设计师重做 Design Token(可能换主色 / 排版体系),统一图标体系,彻底清除 emoji。
|
||||
|
||||
方案拆项:
|
||||
|
||||
- **D1 新 Design Token 体系**:在 `app.wxss` 重定义颜色 / 字体 / 间距 / 圆角 / 阴影 / 层级变量(替换现有 `--gold` 等)。是否换主色由设计师定,但**禁止紫色→粉色渐变**(团队红线)。
|
||||
- **D2 统一 SVG 图标库**:引入一套 SVG / iconfont 图标(具体选型由架构师按项目锁定,**不预设具体库**),全站功能图标改为引用图标名(如 `icon-home` / `icon-search`),替换全部 emoji(清单见 1.2)。
|
||||
- **D3 排版体系升级**:标题层级、字重、行高、中文间距规范化,解决当前 hero 56rpx 与正文 28rpx 之间缺乏中间层级的问题。
|
||||
- **D4 组件令牌对齐**:`navbar/section-header/chip/level-tag/plan-card/exercise-card` 去硬编码色值,统一引用 D1 令牌;`plan-card` 的 `color`、`prog` 的 `color` 改为"用户色板 + 令牌"双轨,避免破坏现有计划配色功能。
|
||||
- **D5 对比度与无障碍**:确保正文/背景对比度达 WCAG 2.1 AA;图标有文字标签或 aria 等价。
|
||||
|
||||
**优先级**:P0(用户已选"重塑设计语言",且 emoji 违规是团队红线,必须清零)。
|
||||
**成本 / 影响**:设计约 1 人周 + 前端改造约 3–5 人日。令牌集中,业务逻辑零改动,影响面 = 全站视觉,Impact 高、Effort 中。RICE 视角 Reach=全部用户(10)、Impact=3(巨大)、Confidence=100%(已确认方向)、Effort≈6 → **Score≈5.0**。
|
||||
|
||||
### 3.2 块二:登录后首页个性化(两档都要,分 MVP 切割)
|
||||
|
||||
#### 轻量档(P0,改动小见效快,不新建数据)
|
||||
|
||||
**范围**:在首页顶部注入"已存在数据"的个性化模块,不新建任何后端数据。
|
||||
|
||||
- **时段问候**:读 `auth.getProfile().nickname` → "早上好 / 下午好 / 晚上好,{昵称}"。零网络开销。
|
||||
- **我的计划**:`planSvc.listMine()` 取最近 N 个,横滑卡片 + "继续训练"入口。
|
||||
- **我的收藏**:`favSvc.listExercises()` 取最近 N 个收藏动作。
|
||||
- **今日推荐**:复用 `svc.recommend`,`target` 由"最近计划 / 收藏"推导(不新建数据,仅做数据派生);若无任何历史,回退到现有"精选动作"。
|
||||
- **未登录降级**:保留现有通用首页(不报错、不空白)。
|
||||
|
||||
**依赖**:仅前端;全部接口(`plan.js` / `favorite.js` / `exercise.js` / `auth.js`)已存在。
|
||||
**成本**:前端 2–3 人日,零后端改动。RICE:Reach=登录用户(8)、Impact=2(高)、Confidence=100%、Effort≈3 → **Score≈5.3**。
|
||||
|
||||
#### 完整档(P1,新增训练记录 / 进度历史)
|
||||
|
||||
**新增后端数据**(沿用现有 `store.ts` JSON 落地模式,成本可控):
|
||||
|
||||
- 数据模型 `checkins` store:`{ id, openid, date(YYYY-MM-DD), planId?, exerciseIds[], durationMin?, completed }`
|
||||
- 派生指标:连续训练天数(streak)、近 30 日训练次数、近 7 日时长 / 动作分布
|
||||
- 用户画像扩展(`users` store + `users.service`):`fitnessGoal`(减脂/增肌/塑形/保持健康)、`level`、`trainFreq` 偏好
|
||||
|
||||
**新增接口**:
|
||||
|
||||
- `POST /api/checkins`(打卡,可关联计划)
|
||||
- `GET /api/checkins/streak`(连续天数 + 今日是否已打卡)
|
||||
- `GET /api/checkins/summary?range=7|30`(进度曲线数据)
|
||||
- `PATCH /api/users/me` 扩展画像字段(已有,仅加字段)
|
||||
|
||||
**首页展示**:
|
||||
|
||||
- 进度曲线(近 7/30 日训练次数或时长,SVG 折线 / 柱状)
|
||||
- 连续训练天数徽标(对标 Apple 三环 / Strava streak 的游戏化钩子)
|
||||
- "今日打卡"按钮(首页 + 计划详情页)
|
||||
- 周/月训练报告(对标 Keep 周报雷达图,列为 P2 增强)
|
||||
|
||||
**依赖**:后端 1 个新 store + controller/service(参照 `common/store.ts`);前端新增打卡与曲线组件;需 onboarding 收集 `fitnessGoal`(见 P1 画像项)。
|
||||
**成本**:后端 3–4 人日 + 前端 4–5 人日。RICE:Reach=活跃训练用户(6)、Impact=3(巨大)、Confidence=80%(模式可参照)、Effort≈9 → **Score≈1.6** → 排 P1。
|
||||
**关键约束**:连续天数 / 曲线**必须有打卡数据沉淀才有意义**,故完整档必须在轻量档上线、积累初期数据后跟进。
|
||||
|
||||
#### 两档 MVP 切割与依赖关系
|
||||
|
||||
```
|
||||
[轻量档] 前端 only,复用现有 API
|
||||
└─ 首页个性化容器(问候 + 我的计划 + 收藏 + 今日推荐)
|
||||
│
|
||||
├─(共享)时段问候 + 容器布局,完整档直接复用
|
||||
│
|
||||
[完整档] 新增 checkins store + 打卡接口 + 曲线组件
|
||||
└─ 在容器下方追加:进度曲线 + 连续天数 + 今日打卡
|
||||
│
|
||||
└─ 依赖:用户画像 onboarding(收集 fitnessGoal/level)
|
||||
```
|
||||
|
||||
- **上线顺序**:轻量档(P0)先上 → 完整档(P1)在 1–2 个迭代后上。
|
||||
- **为什么这样切**:轻量档零后端风险、当天可见效,先验证"首页个性化"假设并拉高激活;完整档需要新数据闭环,先轻后重可避免一次性大改带来的回归风险。
|
||||
- **Backlog(不在本期 MVP)**:周/月雷达报告、社交挑战、AI 生成式教练文案。
|
||||
|
||||
### 3.3 块三:综合 UX 优化清单(按维度 + 优先级)
|
||||
|
||||
**导航 / 信息架构(P1)**
|
||||
- 重审底部导航结构:"计划"语义是否改为"我的"(与"首页/发现/进度/我的"更顺);完整档上线后考虑新增"进度"入口。
|
||||
- 全局常驻搜索:在导航栏内置搜索图标,替代仅 hero 内 pill。
|
||||
- 首页 section 减负:首屏聚焦"个性化区 + 今日推荐",其余(训练组合 / 按部位 / 热门器械)下移或折叠。
|
||||
|
||||
**加载态 / 空态 / 错误态(P0)**
|
||||
- 全站骨架屏(首页 / 列表页),替换死 `loading` 字段与纯文本"加载中…"。
|
||||
- 错误态区分"网络失败"与"无数据",网络失败时提供"重试"按钮(治愈 1.4 的静默失败)。
|
||||
- 空态保留结构,插画换 SVG(随块一 D2)。
|
||||
|
||||
**响应速度(P1)**
|
||||
- `request.js` 增加 `timeout`(建议 8s)+ 统一 loading 策略,避免弱网挂起。
|
||||
- 首页接口并行 + 字段裁剪;关键数据(计划 / 收藏 / 资料)本地缓存,进首页即渲染缓存、后台静默刷新。
|
||||
- 底部导航 `reLaunch` 改为保活方案(原生 tabBar 或自定义栈保留),避免切 tab 重拉数据。
|
||||
|
||||
**手势 / 交互(P2)**
|
||||
- 首页 / 列表支持下拉刷新(`onPullDownRefresh`)。
|
||||
- 横滑区增加"更多 / 分页点"提示一致性。
|
||||
- 收藏 / 计划卡片增加长按 / 左滑等快捷操作。
|
||||
- 统一点击反馈(`hover-class`)与过渡动画。
|
||||
|
||||
---
|
||||
|
||||
## 4. 实施路线图(优先级总表)
|
||||
|
||||
| 编号 | 改进项 | 归属块 | 优先级 | 影响 | 成本(人日) | RICE 评分 | MVP? |
|
||||
|------|--------|--------|--------|------|-------------|-----------|-------|
|
||||
| U1 | emoji → 统一 SVG 图标替换(全站 + 数据层默认值) | UI | **P0** | 高 | 2–3 | 5.0 | 是 |
|
||||
| U2 | 设计令牌重塑(Design Token v2) | UI | **P0** | 高 | 3–5 | 5.0 | 是 |
|
||||
| P1 | 首页轻量档个性化(问候+我的计划+收藏+今日推荐) | 个性化 | **P0** | 高 | 2–3 | 5.3 | 是 |
|
||||
| X1 | 全站骨架屏 + 错误重试态 | UX | **P0** | 高 | 2–3 | 4.8 | 是 |
|
||||
| P2 | 完整档:checkins 数据 + 打卡接口 + 首页曲线/连续天数 | 个性化 | **P1** | 巨大 | 7–9 | 1.6 | 否(第二阶段) |
|
||||
| P3 | 用户画像 onboarding(fitnessGoal/level) | 个性化基建 | **P1** | 高 | 2–3 | 2.4 | 否 |
|
||||
| X2 | 全局常驻搜索 + 导航结构重构 | UX/架构 | **P1** | 中 | 3–4 | 2.0 | 否 |
|
||||
| X3 | 请求超时 + 缓存 + tab 保活 | UX/速度 | **P1** | 高 | 2–3 | 2.6 | 否 |
|
||||
| P4 | 周/月训练报告(雷达图) | 个性化增强 | P2 | 中 | 3–4 | 1.0 | 否 |
|
||||
| X4 | 下拉刷新 / 长按 / 过渡动画等手势完善 | UX | P2 | 中 | 2–3 | 1.2 | 否 |
|
||||
| U3 | 对比度 / 无障碍审计(WCAG AA) | UI | P2 | 中 | 1–2 | 1.5 | 否 |
|
||||
|
||||
**本期(P0 + P1)节奏建议**:
|
||||
1. 第一批(P0,约 1.5 周):U1 + U2 + P1 + X1 —— 视觉焕新 + 首页轻量个性化 + 骨架屏,用户可立刻感知。
|
||||
2. 第二批(P1,约 2 周):P2(完整档)+ P3(画像)+ X2 + X3 —— 训练闭环与数据沉淀。
|
||||
3. 第三批(P2,打磨):P4 + X4 + U3。
|
||||
|
||||
---
|
||||
|
||||
## 5. 明确不在本期范围(Out-of-Scope)
|
||||
|
||||
- **社交 / 社区**:Strava 式 feed、好友、排行榜、挑战赛 —— 本期不做(连续天数可借鉴其游戏化,但无社交层)。
|
||||
- **商城 / 电商**:Gymshark 式服饰售卖 —— 不做。
|
||||
- **课程体系 / 直播课 / 视频跟练**:Keep 式 —— 不做;FITCOACH 定位为"动作推荐 + 计划"工具。
|
||||
- **饮食 / 营养 / 热量**:Keep 饮食 —— 不做。
|
||||
- **智能硬件联动**:Apple Watch / Keep 硬件 —— 不做。
|
||||
- **多语言 / i18n** —— 不做。
|
||||
- **后端从 JSON store 迁移到 SQL** —— 不做(仅新增 `checkins` store 沿用 JSON 落地)。
|
||||
- **AI 大模型生成式 coach 文案** —— 克制,不做自由发挥;仅用规则推荐(遵循 Keep"有据可依不煽情"教训)。
|
||||
- **付费 / 会员体系** —— 不做。
|
||||
|
||||
---
|
||||
|
||||
## 6. 非功能需求(必含)
|
||||
|
||||
| 类别 | 要求 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 性能 | 首屏可交互 < 2.5s(弱网下,凭骨架屏 + 缓存达成);API p95 < 500ms(沿用现有内存数据集) | P0 |
|
||||
| 可用性 | 任一接口失败不白屏,降级到缓存/空态并提供重试 | P0 |
|
||||
| 安全 | HTTPS + JWT(现有);新增 checkins 接口纳入 `auth.guard` 用户隔离(沿用 favorites/plans 的 openid 隔离模式) | P0 |
|
||||
| 兼容性 | 微信最新基础库;iOS / Android 微信客户端 | P0 |
|
||||
| 可访问性 | 对比度 WCAG 2.1 AA;图标有文字/aria 等价;移除 emoji 功能图标 | P1 |
|
||||
| 图标方案 | 统一 SVG 图标库(具体由架构师按项目选型锁定,不预设具体库) | P0 |
|
||||
| 数据埋点 | 见第 7 节 | P0 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 数据埋点方案(必含,验证产品假设)
|
||||
|
||||
MVP 必须埋点的关键事件(不埋 = 上线后无法验证假设):
|
||||
|
||||
| 事件类别 | 必埋事件 | 说明 |
|
||||
|----------|----------|------|
|
||||
| 获客 | `app_launch`, `login_success` | 启动量、登录转化 |
|
||||
| 激活 | `home_view`, `home_personalized_shown`(轻量档命中)、`recommend_view` | 首页个性化模块是否真的展示给用户 |
|
||||
| 留存 | `session_start`, `session_duration`, `checkin_completed`(完整档) | 次日/7日留存、打卡行为 |
|
||||
| 转化 | `plan_continue_tap`(继续训练)、`favorite_item_tap`、`plan_created` | 个性化模块是否驱动行动 |
|
||||
| 异常 | `api_error`, `request_timeout` | 配合 X3 超时治理 |
|
||||
|
||||
**命名规范**:`{对象}_{动作}`(如 `checkin_completed`、`home_personalized_shown`);每条附带 `user_id`、`timestamp`、`device`、`version`;不采集隐私原始输入。
|
||||
|
||||
---
|
||||
|
||||
## 8. 验收标准(Given / When / Then,节选关键)
|
||||
|
||||
- **Given** 用户已登录,**When** 打开首页,**Then** 顶部展示"时段 + 昵称"问候,并出现"我的计划 / 我的收藏 / 今日推荐"模块(轻量档)。
|
||||
- **Given** 用户未登录,**When** 打开首页,**Then** 展示通用首页降级版,不报错、不空白。
|
||||
- **Given** 任意页面数据未返回,**When** 渲染中,**Then** 显示骨架屏而非空白 / 纯文本"加载中…"。
|
||||
- **Given** 网络请求失败,**When** 页面进入错误态,**Then** 出现"重试"按钮且点击可恢复(区分于"无数据"空态)。
|
||||
- **Given** 设计令牌 v2 上线,**When** 全站渲染,**Then** 无任何 emoji 功能图标,主色 / 排版符合新规范,且未出现紫色→粉色渐变。
|
||||
- **Given** 完整档已上线且用户完成一次打卡,**When** 返回首页,**Then** 连续训练天数 +1,进度曲线新增一个数据点。
|
||||
- **Given** 用户首次进入且完整档上线,**When** 触发 onboarding,**Then** 可填写训练目标 / 水平,并写入用户画像。
|
||||
|
||||
---
|
||||
|
||||
## 9. 给设计 / 架构 / 开发的待办(协作提示)
|
||||
|
||||
- **设计师**:输出 Design Token v2 + 统一 SVG 图标清单(替换 1.2 全部 emoji);首页个性化容器线框。
|
||||
- **架构师**:锁定图标库选型;评估 `checkins` store 与 streak 计算的服务归属;确认请求超时与缓存策略;确保新增接口沿用 openid 隔离。
|
||||
- **前端**:轻量档复用现有 4 个服务即可开工;骨架屏组件化;底部导航保活方案选型。
|
||||
- **后端**:仅在完整档阶段新增 `checkins` 模块(store/controller/service),沿用 `common/store.ts` 与 `auth.guard`。
|
||||
|
||||
> 本 PRD 为方案稿,待产品负责人确认范围与优先级后,进入逐批开发。
|
||||
@@ -11,8 +11,10 @@ import {
|
||||
labelOf,
|
||||
} from '../data/labels';
|
||||
|
||||
// 媒体根地址:默认指向本服务自托管的 /media 静态目录(走自家后端,不依赖公共图床)。
|
||||
// 生产/服务器部署时用环境变量覆盖,例如 MEDIA_BASE_URL=http://192.227.237.8:3001/media
|
||||
const MEDIA_BASE_URL = (
|
||||
process.env.MEDIA_BASE_URL || 'https://cdn.jsdelivr.net/gh/hasaneyldrm/exercises-dataset@main'
|
||||
process.env.MEDIA_BASE_URL || 'http://localhost:3000/media'
|
||||
).replace(/\/$/, '');
|
||||
|
||||
function toMediaUrl(path: string): string {
|
||||
|
||||
@@ -34,8 +34,9 @@ export interface PlanDetail extends PlanSummary {
|
||||
exercises: ExerciseSummary[];
|
||||
}
|
||||
|
||||
const DEFAULT_EMOJI = '📋';
|
||||
const PALETTE = ['#D9B36C', '#58C9B9', '#9B72FF', '#FF6B6B', '#4D96FF', '#6BCB77'];
|
||||
// 历史字段名仍叫 emoji,但值已是 Lucide 图标名(前端 iconName 兼容旧 emoji)
|
||||
const DEFAULT_EMOJI = 'clipboard-list';
|
||||
const PALETTE = ['#B6F24E', '#2BD9A6', '#4FA8FF', '#F2B53D', '#FF5C5C', '#9BA7A1'];
|
||||
|
||||
@Injectable()
|
||||
export class PlansService {
|
||||
|
||||
@@ -22,7 +22,7 @@ export class UsersService {
|
||||
const user: UserRecord = {
|
||||
openid: input.openid,
|
||||
nickname: input.nickname?.trim() || '健身学员',
|
||||
avatar: input.avatar || '🏃',
|
||||
avatar: input.avatar || 'user',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
135
design-system/design-tokens.json
Normal file
135
design-system/design-tokens.json
Normal file
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"$meta": {
|
||||
"product": "FITCOACH",
|
||||
"version": "2026-design-language-v2-draft",
|
||||
"register": "product",
|
||||
"platform": "wechat-miniprogram",
|
||||
"designer": "颜好看",
|
||||
"note": "小程序使用 rpx(750rpx=375pt 逻辑像素)。颜色给 hex/rgba 真值,尺寸给 rpx 真值 + px 等价注释。前端把本文件编译进 app.wxss 的 :root(page 选择器)变量与 JS 常量。",
|
||||
"three_axis": { "DESIGN_VARIANCE": 5, "MOTION_INTENSITY": 4, "VISUAL_DENSITY": 4 },
|
||||
"icon_set": "lucide (MIT) — 编译为 iconfont 子集,详情见 DESIGN 提案 §3"
|
||||
},
|
||||
|
||||
"color": {
|
||||
"bg": { "value": "#0B0F0E", "type": "color", "role": "A1 页面背景(近黑,中性偏冷,非纯黑)" },
|
||||
"bg_gradient": { "value": "radial-gradient(120% 80% at 50% 0%, #11171400 0%, #0B0F0E 60%)", "type": "gradient", "role": "A1 hero 背景微光(极弱,不喧宾夺主)" },
|
||||
"surface": { "value": "#141A18", "type": "color", "role": "A1 卡片/容器背景" },
|
||||
"surface_2": { "value": "#1C2421", "type": "color", "role": "A1 次级表面(chip/输入/选中)" },
|
||||
"surface_warm":{ "value": "#1A211E", "type": "color", "role": "B-slot 悬浮/高亮表面" },
|
||||
|
||||
"fg": { "value": "#ECF1EF", "type": "color", "role": "A1 主文本" },
|
||||
"fg_2": { "value": "#9BA7A1", "type": "color", "role": "B-slot 次级文本" },
|
||||
"fg_3": { "value": "#6B7670", "type": "color", "role": "B-slot 三级/元数据文本" },
|
||||
|
||||
"border": { "value": "rgba(255,255,255,0.08)", "type": "color", "role": "A1 默认边框/分隔线" },
|
||||
"border_strong":{ "value": "rgba(255,255,255,0.14)", "type": "color", "role": "B-slot 强调边框(选中态)" },
|
||||
"border_soft": { "value": "rgba(255,255,255,0.05)", "type": "color", "role": "B-slot 内部行分隔" },
|
||||
|
||||
"accent": { "value": "#FF6A3D", "type": "color", "role": "A1 品牌强调色 Pulse 珊瑚橙(CTA/激活导航/关键数据)" },
|
||||
"accent_hover":{ "value": "#FF8263", "type": "color", "role": "A2 悬浮" },
|
||||
"accent_active":{ "value": "#E8562B", "type": "color", "role": "A2 按下" },
|
||||
"accent_on": { "value": "#20100A", "type": "color", "role": "A2 accent 背景上的前景(深暖色,对比 >7:1)" },
|
||||
"accent_soft": { "value": "rgba(255,106,61,0.12)", "type": "color", "role": "C 强调色淡底(标签/选中底)" },
|
||||
"accent_ring": { "value": "rgba(255,106,61,0.38)", "type": "color", "role": "C 聚焦光环/进度环发光" },
|
||||
|
||||
"success": { "value": "#2BD9A6", "type": "color", "role": "A2 正向/进度完成/连续天数(语义绿,非第二品牌色)" },
|
||||
"success_on": { "value": "#04201A", "type": "color", "role": "A2 success 背景前景" },
|
||||
"success_soft":{ "value": "rgba(43,217,166,0.12)", "type": "color", "role": "C 正向淡底" },
|
||||
"warn": { "value": "#F2B53D", "type": "color", "role": "A2 警告" },
|
||||
"danger": { "value": "#FF5C5C", "type": "color", "role": "A2 错误/删除(替代旧 #E5544B)" },
|
||||
"danger_soft": { "value": "rgba(255,92,92,0.12)", "type": "color", "role": "C 错误淡底" },
|
||||
"info": { "value": "#4FA8FF", "type": "color", "role": "A2 信息" },
|
||||
|
||||
"focus_ring": { "value": "0 0 0 3px rgba(255,106,61,0.45)", "type": "shadow", "role": "A2 键盘焦点环" }
|
||||
},
|
||||
|
||||
"font": {
|
||||
"display": { "value": "'Manrope', 'PingFang SC', 'HarmonyOS Sans SC', 'Source Han Sans SC', 'Noto Sans CJK SC', sans-serif", "type": "fontFamily", "role": "A1 标题/数字(拉丁用 Manrope,中文回落系统黑体)" },
|
||||
"body": { "value": "'PingFang SC', 'HarmonyOS Sans SC', 'Source Han Sans SC', 'Noto Sans CJK SC', -apple-system, sans-serif", "type": "fontFamily", "role": "A1 正文(中文系统字体优先,避免打包 10MB+ CJK 字体)" },
|
||||
"mono": { "value": "'JetBrains Mono', ui-monospace, 'SFMono-Regular', monospace", "type": "fontFamily", "role": "A2 数据/统计数字(等宽,tabular)" }
|
||||
},
|
||||
|
||||
"fontSize": {
|
||||
"xs": { "value": "22rpx", "px": 11, "role": "eyebrow 全大写标签" },
|
||||
"sm": { "value": "24rpx", "px": 12, "role": "caption/辅助" },
|
||||
"base":{ "value": "28rpx", "px": 14, "role": "正文基准" },
|
||||
"md": { "value": "30rpx", "px": 15, "role": "正文略大" },
|
||||
"lg": { "value": "32rpx", "px": 16, "role": "三级标题/h3" },
|
||||
"xl": { "value": "36rpx", "px": 18, "role": "二级标题/h2" },
|
||||
"2xl": { "value": "44rpx", "px": 22, "role": "一级标题/h1" },
|
||||
"3xl": { "value": "56rpx", "px": 28, "role": "Hero 主标题" },
|
||||
"4xl": { "value": "72rpx", "px": 36, "role": "巨型统计数字(偶尔)" }
|
||||
},
|
||||
|
||||
"fontWeight": {
|
||||
"regular": { "value": 400, "role": "正文" },
|
||||
"medium": { "value": 510, "role": "按钮/小标题(注意:小程序需字体含此字重,或回落 500)" },
|
||||
"semibold": { "value": 590, "role": "大标题/强调(或 600)" }
|
||||
},
|
||||
|
||||
"letterSpacing": {
|
||||
"eyebrow": { "value": "0.08em", "role": "ALL CAPS 标签" },
|
||||
"title": { "value": "-0.02em", "role": "大标题负字距" },
|
||||
"body": { "value": "0", "role": "正文" }
|
||||
},
|
||||
|
||||
"spacing": {
|
||||
"1": { "value": "8rpx", "px": 4 },
|
||||
"2": { "value": "16rpx", "px": 8 },
|
||||
"3": { "value": "24rpx", "px": 12 },
|
||||
"4": { "value": "32rpx", "px": 16 },
|
||||
"5": { "value": "40rpx", "px": 20 },
|
||||
"6": { "value": "48rpx", "px": 24 },
|
||||
"8": { "value": "64rpx", "px": 32 },
|
||||
"10": { "value": "80rpx", "px": 40 },
|
||||
"12": { "value": "96rpx", "px": 48 },
|
||||
"16": { "value": "128rpx","px": 64, "role": "仅 Hero/大区块" }
|
||||
},
|
||||
|
||||
"radius": {
|
||||
"sm": { "value": "16rpx", "px": 8 },
|
||||
"md": { "value": "24rpx", "px": 12 },
|
||||
"lg": { "value": "32rpx", "px": 16 },
|
||||
"xl": { "value": "40rpx", "px": 20 },
|
||||
"pill": { "value": "9999rpx", "px": 9999 }
|
||||
},
|
||||
|
||||
"shadow": {
|
||||
"flat": { "value": "none", "role": "默认(用 hairline 边框代替阴影)" },
|
||||
"ring": { "value": "0 0 0 1rpx rgba(255,255,255,0.08)", "role": "卡片默认:1px 发丝边框" },
|
||||
"raised": { "value": "0 8rpx 24rpx rgba(0,0,0,0.32)", "role": "悬浮/选中抬升" },
|
||||
"float": { "value": "0 12rpx 40rpx rgba(0,0,0,0.45)", "role": "底部导航/吸顶" },
|
||||
"glow": { "value": "0 8rpx 24rpx rgba(255,106,61,0.28)", "role": "CTA 主按钮光晕(唯一允许的彩色阴影)" }
|
||||
},
|
||||
|
||||
"motion": {
|
||||
"fast": { "value": "150ms", "role": "即时反馈/hover" },
|
||||
"base": { "value": "200ms", "role": "状态切换收敛值" },
|
||||
"slow": { "value": "300ms", "role": "进入/布局变化" },
|
||||
"ease": { "value": "cubic-bezier(0.2, 0, 0, 1)", "role": "标准缓动(无 bounce/elastic)" }
|
||||
},
|
||||
|
||||
"z": {
|
||||
"base": 0,
|
||||
"sticky": 100,
|
||||
"nav": 60,
|
||||
"dropdown":1000,
|
||||
"modal": 1200,
|
||||
"toast": 1300
|
||||
},
|
||||
|
||||
"icon": {
|
||||
"set": "lucide",
|
||||
"license": "MIT",
|
||||
"sizes": { "inline": "16rpx", "button": "20rpx", "standalone": "24rpx" },
|
||||
"stroke": "2px (编译为 iconfont 后对应字重)",
|
||||
"delivery": "编译 Lucide 子集为 iconfont(.ttf/.woff2),小程序用 <text class='fc-icon'> 引用;或用 base64 SVG <image>。禁止 emoji。"
|
||||
},
|
||||
|
||||
"density": {
|
||||
"register": "product",
|
||||
"accent_per_screen": 2,
|
||||
"touch_target_min": "44px (88rpx)",
|
||||
"safe_area": "env(safe-area-inset-bottom)"
|
||||
}
|
||||
}
|
||||
121
home-personalization-mockup.svg
Normal file
121
home-personalization-mockup.svg
Normal file
@@ -0,0 +1,121 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 390 1200" width="390" height="1200" font-family="'PingFang SC','HarmonyOS Sans SC',sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#0B0F0E"/>
|
||||
<stop offset="1" stop-color="#0B0F0E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="spark" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#FF6A3D" stop-opacity="0.18"/>
|
||||
<stop offset="1" stop-color="#FF6A3D" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="phone"><rect x="0" y="0" width="390" height="1200" rx="0"/></clipPath>
|
||||
</defs>
|
||||
|
||||
<g clip-path="url(#phone)">
|
||||
<rect x="0" y="0" width="390" height="1200" fill="url(#bg)"/>
|
||||
|
||||
<!-- Status bar -->
|
||||
<text x="24" y="30" fill="#ECF1EF" font-size="14" font-weight="600">16:04</text>
|
||||
<text x="320" y="30" fill="#9BA7A1" font-size="12">5G</text>
|
||||
<rect x="344" y="20" width="22" height="11" rx="2" fill="none" stroke="#9BA7A1" stroke-width="1"/>
|
||||
<rect x="346" y="22" width="14" height="7" rx="1" fill="#ECF1EF"/>
|
||||
|
||||
<!-- Hero -->
|
||||
<text x="24" y="76" fill="#FF6A3D" font-size="11" letter-spacing="2" font-weight="600">FITCOACH · 智能健身教练</text>
|
||||
<text x="24" y="112" fill="#ECF1EF" font-size="26" font-weight="700" letter-spacing="-0.5">今天,练点什么?</text>
|
||||
<text x="24" y="138" fill="#9BA7A1" font-size="13">精准匹配你的目标肌群,让每次训练更高效</text>
|
||||
|
||||
<!-- Search pill -->
|
||||
<rect x="24" y="158" width="342" height="44" rx="22" fill="#141A18" stroke="#1F2624" stroke-width="1"/>
|
||||
<circle cx="46" cy="180" r="7" fill="none" stroke="#6B7670" stroke-width="2"/>
|
||||
<line x1="51" y1="185" x2="56" y2="190" stroke="#6B7670" stroke-width="2" stroke-linecap="round"/>
|
||||
<text x="66" y="185" fill="#6B7670" font-size="13">搜索动作 / 器械 / 部位</text>
|
||||
|
||||
<!-- CTA -->
|
||||
<rect x="24" y="212" width="342" height="48" rx="24" fill="#FF6A3D"/>
|
||||
<path d="M150 230 l3 6 6 1 -4.5 4 1 6 -5.5 -3 -5.5 3 1 -6 -4.5 -4 6 -1 z" fill="#20100A"/>
|
||||
<text x="172" y="242" fill="#20100A" font-size="15" font-weight="700">智能推荐</text>
|
||||
<text x="344" y="242" fill="#20100A" font-size="20" font-weight="700" text-anchor="end">›</text>
|
||||
|
||||
<!-- ===== 本周概览 card ===== -->
|
||||
<rect x="24" y="284" width="342" height="208" rx="16" fill="#141A18" stroke="#1F2624" stroke-width="1"/>
|
||||
<text x="44" y="312" fill="#9BA7A1" font-size="11" letter-spacing="2" font-weight="600">OVERVIEW</text>
|
||||
<text x="44" y="334" fill="#ECF1EF" font-size="16" font-weight="600">本周概览</text>
|
||||
|
||||
<!-- progress ring -->
|
||||
<circle cx="96" cy="392" r="44" fill="none" stroke="#1F2624" stroke-width="9"/>
|
||||
<circle cx="96" cy="392" r="44" fill="none" stroke="#2BD9A6" stroke-width="9" stroke-linecap="round"
|
||||
stroke-dasharray="251 277" transform="rotate(-90 96 392)"/>
|
||||
<text x="96" y="388" fill="#ECF1EF" font-size="30" font-weight="700" text-anchor="middle" font-family="ui-monospace,monospace">4</text>
|
||||
<text x="96" y="408" fill="#9BA7A1" font-size="12" text-anchor="middle">/ 5 次</text>
|
||||
|
||||
<!-- streak -->
|
||||
<path d="M186 360 c4 -10 14 -10 14 0 c0 8 -8 12 -8 20 c-6 -4 -8 -10 -6 -16 c-2 4 -4 8 -2 12 c-6 -2 -8 -10 -8 -16z" fill="#FF6A3D"/>
|
||||
<text x="208" y="384" fill="#ECF1EF" font-size="22" font-weight="700" font-family="ui-monospace,monospace">12</text>
|
||||
<text x="232" y="384" fill="#9BA7A1" font-size="13">天连击</text>
|
||||
<text x="186" y="410" fill="#9BA7A1" font-size="12">连续训练</text>
|
||||
|
||||
<!-- sparkline -->
|
||||
<path d="M44 470 L70 458 L96 464 L122 444 L148 450 L174 432 L200 440 L226 420 L252 428 L278 410 L304 418 L330 400 L356 408 L356 478 L44 478 Z" fill="url(#spark)"/>
|
||||
<path d="M44 470 L70 458 L96 464 L122 444 L148 450 L174 432 L200 440 L226 420 L252 428 L278 410 L304 418 L330 400 L356 408" fill="none" stroke="#FF6A3D" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="356" cy="408" r="3.5" fill="#FF6A3D"/>
|
||||
<path d="M326 392 l8 -10 6 6 8 -10" fill="none" stroke="#2BD9A6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" transform="translate(0,0)"/>
|
||||
<text x="332" y="396" fill="#2BD9A6" font-size="11" font-weight="600">↑ 8%</text>
|
||||
|
||||
<!-- ===== 我的计划 card ===== -->
|
||||
<rect x="24" y="512" width="342" height="96" rx="16" fill="#141A18" stroke="#1F2624" stroke-width="1"/>
|
||||
<rect x="44" y="536" width="32" height="32" rx="8" fill="#FF6A3D" fill-opacity="0.12"/>
|
||||
<path d="M52 552 h4 v-8 h4 v8 h4 v-12 h4 v12 h4 v-6 h4 v6" fill="none" stroke="#FF6A3D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" transform="translate(-6,0)"/>
|
||||
<text x="88" y="546" fill="#ECF1EF" font-size="15" font-weight="600">推胸日 · 第 3 周</text>
|
||||
<rect x="276" y="532" width="62" height="22" rx="11" fill="#FF6A3D" fill-opacity="0.14"/>
|
||||
<text x="307" y="547" fill="#FF6A3D" font-size="11" font-weight="600" text-anchor="middle">进行中</text>
|
||||
<text x="44" y="576" fill="#9BA7A1" font-size="12">今日 6 个动作 · 已完成 2 / 6</text>
|
||||
<rect x="44" y="588" width="302" height="8" rx="4" fill="#1F2624"/>
|
||||
<rect x="44" y="588" width="100" height="8" rx="4" fill="#FF6A3D"/>
|
||||
|
||||
<!-- ===== 今日推荐 row ===== -->
|
||||
<rect x="24" y="624" width="342" height="64" rx="16" fill="#141A18" stroke="#1F2624" stroke-width="1"/>
|
||||
<path d="M48 648 l4 8 8 1 -6 6 2 8 -8 -5 -8 5 2 -8 -6 -6 8 -1 z" fill="#FF6A3D"/>
|
||||
<text x="72" y="648" fill="#ECF1EF" font-size="14" font-weight="600">上斜哑铃推胸</text>
|
||||
<text x="72" y="668" fill="#9BA7A1" font-size="11">根据你的推胸计划,匹配度 92%</text>
|
||||
<text x="352" y="664" fill="#6B7670" font-size="18" text-anchor="end">›</text>
|
||||
|
||||
<!-- ===== 收藏的动作 ===== -->
|
||||
<text x="24" y="720" fill="#9BA7A1" font-size="11" letter-spacing="2" font-weight="600">FAVORITES</text>
|
||||
<text x="24" y="742" fill="#ECF1EF" font-size="16" font-weight="600">收藏的动作</text>
|
||||
<rect x="24" y="756" width="106" height="120" rx="12" fill="#141A18" stroke="#1F2624" stroke-width="1"/>
|
||||
<rect x="24" y="756" width="106" height="74" rx="12" fill="#1C2421"/>
|
||||
<path d="M44 792 h8 v-10 h8 v10 h8 v-14 h8 v14 h8 v-8 h8 v8" fill="none" stroke="#6B7670" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" transform="translate(-12,0)"/>
|
||||
<text x="34" y="852" fill="#ECF1EF" font-size="12">杠铃深蹲</text>
|
||||
<text x="34" y="868" fill="#6B7670" font-size="10">下肢 · 4 组</text>
|
||||
|
||||
<rect x="142" y="756" width="106" height="120" rx="12" fill="#141A18" stroke="#1F2624" stroke-width="1"/>
|
||||
<rect x="142" y="756" width="106" height="74" rx="12" fill="#1C2421"/>
|
||||
<path d="M170 792 h8 v-10 h8 v10 h8 v-14 h8 v14 h8 v-8 h8 v8" fill="none" stroke="#6B7670" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" transform="translate(0,0)"/>
|
||||
<text x="152" y="852" fill="#ECF1EF" font-size="12">硬拉</text>
|
||||
<text x="152" y="868" fill="#6B7670" font-size="10">全身 · 5 组</text>
|
||||
|
||||
<rect x="260" y="756" width="106" height="120" rx="12" fill="#141A18" stroke="#1F2624" stroke-width="1"/>
|
||||
<rect x="260" y="756" width="106" height="74" rx="12" fill="#1C2421"/>
|
||||
<path d="M288 792 h8 v-10 h8 v10 h8 v-14 h8 v14 h8 v-8 h8 v8" fill="none" stroke="#6B7670" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" transform="translate(12,0)"/>
|
||||
<text x="270" y="852" fill="#ECF1EF" font-size="12">引体向上</text>
|
||||
<text x="270" y="868" fill="#6B7670" font-size="10">背部 · 4 组</text>
|
||||
|
||||
<!-- ===== Bottom nav ===== -->
|
||||
<rect x="0" y="1116" width="390" height="84" fill="#0E1311" opacity="0.96"/>
|
||||
<rect x="0" y="1116" width="390" height="1" fill="#1F2624"/>
|
||||
<g fill="#6B7670">
|
||||
<path d="M58 1150 h20 v14 h-20 z M56 1146 h24 v4 h-24 z" />
|
||||
<path d="M138 1144 h4 v20 h16 v4 h-20 z M138 1144 v-4 h4 v4" />
|
||||
<path d="M214 1144 h20 v4 h-20 v-4 m0 8 h20 v4 h-20 v-4 m0 8 h20 v4 h-20 v-4" transform="translate(-2,0)"/>
|
||||
<circle cx="308" cy="1150" r="10" fill="none" stroke="#6B7670" stroke-width="2"/>
|
||||
<path d="M308 1145 v10 M303 1150 h10" stroke="#6B7670" stroke-width="2"/>
|
||||
</g>
|
||||
<!-- active item (home) -->
|
||||
<rect x="48" y="1128" width="40" height="3" rx="1.5" fill="#FF6A3D"/>
|
||||
<text x="68" y="1182" fill="#FF6A3D" font-size="10" text-anchor="middle" font-weight="600">首页</text>
|
||||
<text x="148" y="1182" fill="#6B7670" font-size="10" text-anchor="middle">分类</text>
|
||||
<text x="226" y="1182" fill="#6B7670" font-size="10" text-anchor="middle">计划</text>
|
||||
<text x="308" y="1182" fill="#6B7670" font-size="10" text-anchor="middle">我的</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.6 KiB |
@@ -1,54 +1,116 @@
|
||||
/* ===== FITCOACH 设计系统:高端暗金奢华主题 ===== */
|
||||
/* ===== Lucide 子集 iconfont(动态生成,font-family: fc-icon) ===== */
|
||||
@font-face {
|
||||
font-family: "fc-icon";
|
||||
src: url('assets/fonts/fc-icon.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
/* ===== FITCOACH 设计系统:动能暗色(Kinetic Dark) =====
|
||||
强调色 accent = Volt 荧光绿 #B6F24E(用户拍板)。
|
||||
为兼容现有页面对 --gold/--teal 等变量名的引用,保留变量名、改其色值为新调色板;
|
||||
并补全套语义色/字号/间距/圆角/阴影/动效 Token(真源 design-system/design-tokens.json)。 */
|
||||
page {
|
||||
/* 设计令牌 */
|
||||
--bg: #0E1110;
|
||||
--bg-grad: linear-gradient(160deg, #16201B 0%, #0E1110 55%);
|
||||
--surface: #171C19;
|
||||
--surface-2: #1F2622;
|
||||
--gold: #D9B36C;
|
||||
--gold-2: #F0D9A8;
|
||||
--gold-soft: rgba(217,179,108,0.12);
|
||||
--gold-line: rgba(217,179,108,0.28);
|
||||
--teal: #58C9B9;
|
||||
--text: #F3F5F2;
|
||||
--text-2: #98A39B;
|
||||
--text-3: #6B746D;
|
||||
--line: rgba(255,255,255,0.07);
|
||||
--radius: 28rpx;
|
||||
--radius-sm: 18rpx;
|
||||
--shadow: 0 12rpx 40rpx rgba(0,0,0,0.35);
|
||||
/* —— 颜色 —— */
|
||||
--bg: #0B0F0E; /* 页面背景(冷中性近黑) */
|
||||
--bg-grad: radial-gradient(120% 80% at 50% 0%, rgba(17,23,20,0) 0%, #0B0F0E 60%);
|
||||
--surface: #141A18; /* 卡片/容器 */
|
||||
--surface-2: #1C2421; /* 次级表面 chip/输入/选中 */
|
||||
--surface-warm: #1A211E; /* 悬浮/高亮表面 */
|
||||
|
||||
--text: #ECF1EF; /* 主文本 */
|
||||
--text-2: #9BA7A1; /* 次级文本 */
|
||||
--text-3: #6B7670; /* 三级/元数据(仅 ≥24rpx) */
|
||||
|
||||
--line: rgba(255,255,255,0.08); /* 默认边框/分隔线 */
|
||||
--border-strong: rgba(255,255,255,0.14); /* 强调边框(选中态) */
|
||||
--border-soft: rgba(255,255,255,0.05); /* 内部行分隔 */
|
||||
|
||||
/* 强调色(原 --gold 命名保留以兼容引用,值改为 Volt 绿) */
|
||||
--gold: #B6F24E; /* accent 主色 */
|
||||
--gold-2: #C7F56E; /* accent hover */
|
||||
--gold-soft: rgba(182,242,78,0.12); /* accent 淡底 */
|
||||
--gold-line: rgba(182,242,78,0.28); /* accent 边框 */
|
||||
--teal: #2BD9A6; /* success(原语义) */
|
||||
|
||||
/* 语义色(新增) */
|
||||
--accent: #B6F24E;
|
||||
--accent-hover: #C7F56E;
|
||||
--accent-active: #9EDB3A;
|
||||
--accent-on: #0E1A05; /* accent 背景上的前景(深绿,对比>7:1) */
|
||||
--accent-soft: rgba(182,242,78,0.12);
|
||||
--accent-ring: rgba(182,242,78,0.38);
|
||||
--success: #2BD9A6;
|
||||
--success-on: #04201A;
|
||||
--success-soft: rgba(43,217,166,0.12);
|
||||
--warn: #F2B53D;
|
||||
--danger: #FF5C5C; /* 替代旧 #E5544B */
|
||||
--danger-soft: rgba(255,92,92,0.12);
|
||||
--info: #4FA8FF;
|
||||
--bar-bg: rgba(11,15,14,0.92); /* 底部操作条毛玻璃底(=bg 带透明) */
|
||||
--float-bg: rgba(11,15,14,0.55); /* 浮动按钮底 */
|
||||
--scrim: rgba(0,0,0,0.5); /* 弹层遮罩 */
|
||||
|
||||
/* 字体三族(含中文) */
|
||||
--font-display: 'Manrope', 'PingFang SC', 'HarmonyOS Sans SC', 'Source Han Sans SC', 'Noto Sans CJK SC', sans-serif;
|
||||
--font-body: 'PingFang SC', 'HarmonyOS Sans SC', 'Source Han Sans SC', 'Noto Sans CJK SC', -apple-system, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', monospace;
|
||||
|
||||
/* 字号阶梯(rpx,22–72) */
|
||||
--fs-xs: 22rpx; --fs-sm: 24rpx; --fs-base: 28rpx; --fs-md: 30rpx;
|
||||
--fs-lg: 32rpx; --fs-xl: 36rpx; --fs-2xl: 44rpx; --fs-3xl: 56rpx; --fs-4xl: 72rpx;
|
||||
|
||||
/* 字重 / 字距 */
|
||||
--fw-regular: 400; --fw-medium: 500; --fw-semibold: 600;
|
||||
--ls-eyebrow: 0.08em; --ls-title: -0.02em; --ls-body: 0;
|
||||
|
||||
/* 间距尺度(4px 网格) */
|
||||
--sp-1: 8rpx; --sp-2: 16rpx; --sp-3: 24rpx; --sp-4: 32rpx; --sp-5: 40rpx;
|
||||
--sp-6: 48rpx; --sp-8: 64rpx; --sp-10: 80rpx; --sp-12: 96rpx; --sp-16: 128rpx;
|
||||
|
||||
/* 圆角(收紧) */
|
||||
--radius-sm: 16rpx; --radius: 24rpx; --radius-md: 24rpx;
|
||||
--radius-lg: 32rpx; --radius-xl: 40rpx; --radius-pill: 9999rpx;
|
||||
|
||||
/* 阴影(Hairline First:默认无阴影,靠发丝边框) */
|
||||
--shadow: none;
|
||||
--shadow-ring: 0 0 0 1rpx rgba(255,255,255,0.08);
|
||||
--shadow-raised: 0 8rpx 24rpx rgba(0,0,0,0.32);
|
||||
--shadow-float: 0 12rpx 40rpx rgba(0,0,0,0.45);
|
||||
--shadow-glow: 0 8rpx 24rpx rgba(182,242,78,0.28); /* 唯一允许的彩色阴影:CTA */
|
||||
--focus-ring: 0 0 0 3rpx rgba(182,242,78,0.45);
|
||||
|
||||
/* 动效 */
|
||||
--motion-fast: 150ms; --motion-base: 200ms; --motion-slow: 300ms;
|
||||
--ease: cubic-bezier(0.2, 0, 0, 1);
|
||||
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: -apple-system, "PingFang SC", "Helvetica Neue", Helvetica, sans-serif;
|
||||
font-size: 28rpx;
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--fs-base);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
view, text, scroll-view, image, input {
|
||||
view, text, scroll-view, image, input, textarea, button {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ===== 通用工具类 ===== */
|
||||
.eyebrow {
|
||||
font-size: 22rpx;
|
||||
letter-spacing: 4rpx;
|
||||
color: var(--gold);
|
||||
font-size: var(--fs-xs);
|
||||
letter-spacing: var(--ls-eyebrow);
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
font-weight: var(--fw-semibold);
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 40rpx 32rpx;
|
||||
}
|
||||
.section + .section {
|
||||
border-top: 1rpx solid var(--line);
|
||||
}
|
||||
.section { padding: var(--sp-5) var(--sp-4); }
|
||||
.section + .section { border-top: 1rpx solid var(--line); }
|
||||
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
border: 1rpx solid var(--line);
|
||||
}
|
||||
|
||||
@@ -56,10 +118,10 @@ view, text, scroll-view, image, input {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 10rpx 24rpx;
|
||||
border-radius: 999rpx;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--surface-2);
|
||||
color: var(--text-2);
|
||||
font-size: 24rpx;
|
||||
font-size: var(--fs-sm);
|
||||
border: 1rpx solid var(--line);
|
||||
}
|
||||
|
||||
@@ -70,7 +132,7 @@ view, text, scroll-view, image, input {
|
||||
|
||||
.empty {
|
||||
color: var(--text-3);
|
||||
font-size: 26rpx;
|
||||
font-size: var(--fs-sm);
|
||||
text-align: center;
|
||||
padding: 80rpx 0;
|
||||
}
|
||||
@@ -80,24 +142,22 @@ view, text, scroll-view, image, input {
|
||||
color: var(--text-2);
|
||||
background: var(--surface-2);
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 999rpx;
|
||||
border-radius: var(--radius-pill);
|
||||
}
|
||||
.tag--gold {
|
||||
color: var(--gold-2);
|
||||
background: var(--gold-soft);
|
||||
}
|
||||
|
||||
/* ===== 横向滚动提示:右侧渐隐遮罩,提示「还可向右滑动」 ===== */
|
||||
/* ===== 横向滚动右侧渐隐遮罩(提示可右滑) ===== */
|
||||
.hscroll, .chip-scroll, .filterbar, .emoji-row, .prog-scroll {
|
||||
position: relative;
|
||||
}
|
||||
.hscroll::after, .chip-scroll::after, .filterbar::after, .emoji-row::after, .prog-scroll::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0; right: 0; bottom: 0;
|
||||
width: 56rpx;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(90deg, rgba(14,17,16,0) 0%, rgba(14,17,16,0.9) 100%);
|
||||
background: linear-gradient(90deg, rgba(11,15,14,0) 0%, rgba(11,15,14,0.9) 100%);
|
||||
}
|
||||
|
||||
51
miniprogram/assets/fonts/fc-icon.json
Normal file
51
miniprogram/assets/fonts/fc-icon.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"zap": 61697,
|
||||
"x-circle": 61698,
|
||||
"user": 61699,
|
||||
"trophy": 61700,
|
||||
"trending-up": 61701,
|
||||
"trash-2": 61702,
|
||||
"target": 61703,
|
||||
"star": 61704,
|
||||
"sparkles": 61705,
|
||||
"sliders-horizontal": 61706,
|
||||
"share-2": 61707,
|
||||
"settings": 61708,
|
||||
"search": 61709,
|
||||
"rotate-cw": 61710,
|
||||
"refresh-cw": 61711,
|
||||
"plus": 61712,
|
||||
"play": 61713,
|
||||
"person-standing": 61714,
|
||||
"medal": 61715,
|
||||
"log-out": 61716,
|
||||
"layout-grid": 61717,
|
||||
"layers": 61718,
|
||||
"info": 61719,
|
||||
"image": 61720,
|
||||
"home": 61721,
|
||||
"heart": 61722,
|
||||
"grid-2x2": 61723,
|
||||
"folder-open": 61724,
|
||||
"flame": 61725,
|
||||
"filter": 61726,
|
||||
"dumbbell": 61727,
|
||||
"download": 61728,
|
||||
"clock": 61729,
|
||||
"clipboard-list": 61730,
|
||||
"circle-x": 61731,
|
||||
"chevron-right": 61732,
|
||||
"chevron-down": 61733,
|
||||
"check": 61734,
|
||||
"check-circle": 61735,
|
||||
"calendar": 61736,
|
||||
"bookmark": 61737,
|
||||
"bike": 61738,
|
||||
"bell": 61739,
|
||||
"award": 61740,
|
||||
"arrow-right": 61741,
|
||||
"arrow-left": 61742,
|
||||
"alert-circle": 61743,
|
||||
"activity": 61744,
|
||||
"accessibility": 61745
|
||||
}
|
||||
BIN
miniprogram/assets/fonts/fc-icon.ttf
Normal file
BIN
miniprogram/assets/fonts/fc-icon.ttf
Normal file
Binary file not shown.
@@ -1,17 +1,28 @@
|
||||
// 头像组件:纯 emoji 圆形头像(无二进制图片)。
|
||||
// props: avatar(emoji 字符串), size(rpx, 默认 120), bg(背景色, 默认 gold-soft)
|
||||
const { iconName } = require('../../utils/icon-name');
|
||||
|
||||
// 头像组件:圆形 Lucide 图标头像(无二进制图片)。
|
||||
// props: avatar(lucide 图标名 或 历史 emoji), size(rpx, 默认 120), bg(背景色)
|
||||
Component({
|
||||
properties: {
|
||||
avatar: { type: String, value: '💪' },
|
||||
avatar: { type: String, value: 'user' },
|
||||
size: { type: Number, value: 120 },
|
||||
bg: { type: String, value: 'rgba(217,179,108,0.12)' }
|
||||
bg: { type: String, value: 'rgba(182,242,78,0.12)' }
|
||||
},
|
||||
data: {
|
||||
fontSize: 60
|
||||
glyph: 'user',
|
||||
iconSize: 60
|
||||
},
|
||||
observers: {
|
||||
size: function (size) {
|
||||
this.setData({ fontSize: Math.round(size * 0.5) });
|
||||
avatar(v) {
|
||||
this.setData({ glyph: iconName(v, 'user') });
|
||||
},
|
||||
size(s) {
|
||||
this.setData({ iconSize: Math.round(s * 0.5) });
|
||||
}
|
||||
},
|
||||
lifetimes: {
|
||||
attached() {
|
||||
this.setData({ glyph: iconName(this.data.avatar, 'user'), iconSize: Math.round(this.data.size * 0.5) });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
"usingComponents": {
|
||||
"fc-icon": "/components/fc-icon/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<view
|
||||
class="avatar"
|
||||
style="width: {{size}}rpx; height: {{size}}rpx; background: {{bg}}; font-size: {{fontSize}}rpx;">
|
||||
{{avatar || '💪'}}
|
||||
style="width: {{size}}rpx; height: {{size}}rpx; background: {{bg}};">
|
||||
<fc-icon name="{{glyph}}" size="{{iconSize}}" color="var(--accent)" />
|
||||
</view>
|
||||
|
||||
@@ -4,10 +4,10 @@ Component({
|
||||
},
|
||||
data: {
|
||||
items: [
|
||||
{ key: 'home', label: '首页', emoji: '🏠', page: '/pages/index/index' },
|
||||
{ key: 'category', label: '分类', emoji: '📚', page: '/pages/category/category' },
|
||||
{ key: 'plans', label: '计划', emoji: '📋', page: '/pages/my-plans/my-plans' },
|
||||
{ key: 'mine', label: '我的', emoji: '👤', page: '/pages/profile/profile' }
|
||||
{ key: 'home', label: '首页', icon: 'home', page: '/pages/index/index' },
|
||||
{ key: 'category', label: '分类', icon: 'layout-grid', page: '/pages/category/category' },
|
||||
{ key: 'plans', label: '计划', icon: 'clipboard-list', page: '/pages/my-plans/my-plans' },
|
||||
{ key: 'mine', label: '我的', icon: 'user', page: '/pages/profile/profile' }
|
||||
]
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
"usingComponents": {
|
||||
"fc-icon": "/components/fc-icon/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
class="bn__item {{active === item.key ? 'bn__item--active' : ''}}"
|
||||
data-page="{{item.page}}"
|
||||
bindtap="onTap">
|
||||
<text class="bn__emoji">{{item.emoji}}</text>
|
||||
<fc-icon name="{{item.icon}}" size="40" color="{{active === item.key ? 'var(--accent)' : 'var(--text-3)'}}" />
|
||||
<text class="bn__label">{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -27,6 +27,6 @@
|
||||
background: linear-gradient(90deg, var(--gold), var(--gold-2));
|
||||
}
|
||||
.bn__item--active { color: var(--gold); }
|
||||
.bn__emoji { font-size: 40rpx; line-height: 1; }
|
||||
.bn__emoji { line-height: 1; }
|
||||
.bn__label { font-size: 22rpx; margin-top: 6rpx; }
|
||||
.bn__item--active .bn__label { color: var(--gold-2); }
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
"usingComponents": {
|
||||
"fc-icon": "/components/fc-icon/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
mode="aspectFill"
|
||||
lazy-load="true"
|
||||
binderror="onImgError" />
|
||||
<view wx:else class="ex-card__ph">🏋️</view>
|
||||
<view wx:else class="ex-card__ph"><fc-icon name="dumbbell" size="64" color="var(--text-3)" /></view>
|
||||
<view wx:if="{{exercise.typeLabel}}" class="ex-card__type">{{exercise.typeLabel}}</view>
|
||||
<view wx:if="{{score > 0}}" class="ex-card__score">匹配 {{score}}</view>
|
||||
</view>
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
.ex-card__type {
|
||||
position: absolute; left: 12rpx; top: 12rpx;
|
||||
font-size: 20rpx; color: var(--text-2);
|
||||
background: rgba(0,0,0,0.45); padding: 4rpx 14rpx; border-radius: 999rpx;
|
||||
background: var(--scrim); padding: 4rpx 14rpx; border-radius: 999rpx;
|
||||
}
|
||||
.ex-card__score {
|
||||
position: absolute; right: 12rpx; top: 12rpx;
|
||||
font-size: 20rpx; color: #0E1110; font-weight: 700;
|
||||
font-size: 20rpx; color: var(--accent-on); font-weight: 700;
|
||||
background: var(--gold); padding: 4rpx 14rpx; border-radius: 999rpx;
|
||||
}
|
||||
.ex-card__body { padding: 16rpx 18rpx 20rpx; }
|
||||
|
||||
23
miniprogram/components/fc-icon/index.js
Normal file
23
miniprogram/components/fc-icon/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const iconMap = require('../../utils/icon-map');
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
// Lucide 图标名,如 search / dumbbell / heart
|
||||
name: { type: String, value: '' },
|
||||
// 字号(rpx)
|
||||
size: { type: Number, value: 20 },
|
||||
// 颜色,默认跟随当前文字色(currentColor 在 iconfont 下用具体色)
|
||||
color: { type: String, value: '' },
|
||||
},
|
||||
data: { glyph: '' },
|
||||
observers: {
|
||||
name(n) {
|
||||
this.setData({ glyph: (iconMap[n] || '') });
|
||||
},
|
||||
},
|
||||
lifetimes: {
|
||||
attached() {
|
||||
this.setData({ glyph: (iconMap[this.data.name] || '') });
|
||||
},
|
||||
},
|
||||
});
|
||||
4
miniprogram/components/fc-icon/index.json
Normal file
4
miniprogram/components/fc-icon/index.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
miniprogram/components/fc-icon/index.wxml
Normal file
1
miniprogram/components/fc-icon/index.wxml
Normal file
@@ -0,0 +1 @@
|
||||
<text class="fc-icon" style="font-size:{{size}}rpx;{{color ? 'color:' + color + ';' : ''}}">{{glyph}}</text>
|
||||
10
miniprogram/components/fc-icon/index.wxss
Normal file
10
miniprogram/components/fc-icon/index.wxss
Normal file
@@ -0,0 +1,10 @@
|
||||
.fc-icon {
|
||||
font-family: "fc-icon";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
}
|
||||
.gif-loading__dot {
|
||||
width: 18rpx; height: 18rpx; border-radius: 999rpx;
|
||||
background: var(--gold, #c9a24b);
|
||||
background: var(--accent);
|
||||
animation: gif-pulse 1s ease-in-out infinite;
|
||||
}
|
||||
.gif-loading__txt {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.navbar {
|
||||
background: linear-gradient(180deg, rgba(22,32,27,0.96) 0%, rgba(14,17,16,0.55) 100%);
|
||||
background: linear-gradient(180deg, var(--bar-bg) 0%, var(--float-bg) 100%);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
|
||||
@@ -2,6 +2,7 @@ const app = getApp();
|
||||
const planSvc = require('../../services/plan');
|
||||
const favSvc = require('../../services/favorite');
|
||||
const auth = require('../../utils/auth');
|
||||
const { iconName } = require('../../utils/icon-name');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -29,7 +30,7 @@ Page({
|
||||
this.setData({ loading: true });
|
||||
planSvc.officialExercises(this.data.slug, 1, 60).then((res) => {
|
||||
this.setData({
|
||||
collection: res.collection,
|
||||
collection: { ...res.collection, icon: iconName(res.collection.emoji) },
|
||||
exercises: (res && res.items) || [],
|
||||
total: (res && res.total) || 0,
|
||||
loading: false
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"navbar": "/components/navbar/index",
|
||||
"exercise-card": "/components/exercise-card/index",
|
||||
"section-header": "/components/section-header/index",
|
||||
"level-tag": "/components/level-tag/index"
|
||||
"level-tag": "/components/level-tag/index",
|
||||
"fc-icon": "/components/fc-icon/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<navbar title="{{collection.name}}" show-back />
|
||||
|
||||
<view class="header" style="background: linear-gradient(135deg, {{collection.color}}26, var(--surface));">
|
||||
<view class="header__emoji" style="background: {{collection.color}}22; color: {{collection.color}};">{{collection.emoji}}</view>
|
||||
<view class="header__emoji" style="background: {{collection.color}}22; color: {{collection.color}};"><fc-icon name="{{collection.icon}}" size="48" color="{{collection.color}}" /></view>
|
||||
<view class="header__name" style="color: {{collection.color}};">{{collection.name}}</view>
|
||||
<view wx:if="{{collection.nameEn}}" class="header__en">{{collection.nameEn}}</view>
|
||||
<view class="header__desc">{{collection.description}}</view>
|
||||
@@ -31,7 +31,8 @@
|
||||
|
||||
<view class="actionbar">
|
||||
<view class="btn btn--ghost {{favorited ? 'btn--faved' : ''}}" bindtap="onToggleFav">
|
||||
<text>{{favorited ? '♥ 已收藏' : '♡ 收藏'}}</text>
|
||||
<fc-icon name="heart" size="28" color="{{favorited ? 'var(--accent)' : 'var(--text)'}}" />
|
||||
<text>{{favorited ? '已收藏' : '收藏'}}</text>
|
||||
</view>
|
||||
<view class="btn btn--gold" bindtap="onImport">
|
||||
<text>+ 导入我的计划</text>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
.actionbar {
|
||||
position: fixed; left: 0; right: 0; bottom: 0;
|
||||
display: flex; gap: 20rpx; padding: 20rpx 32rpx;
|
||||
background: rgba(14,17,16,0.92);
|
||||
background: var(--bar-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
border-top: 1rpx solid var(--line);
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
@@ -28,6 +28,6 @@
|
||||
flex: 1; display: flex; align-items: center; justify-content: center;
|
||||
border-radius: 999rpx; padding: 26rpx; font-size: 30rpx; font-weight: 700;
|
||||
}
|
||||
.btn--gold { background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%); color: #0E1110; }
|
||||
.btn--gold { background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%); color: var(--accent-on); }
|
||||
.btn--ghost { background: var(--surface-2); color: var(--text); border: 1rpx solid var(--gold-line); }
|
||||
.btn--faved { color: var(--gold-2); border-color: var(--gold); }
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"exercise-card": "/components/exercise-card/index",
|
||||
"gif-player": "/components/gif-player/gif-player"
|
||||
"gif-player": "/components/gif-player/gif-player",
|
||||
"fc-icon": "/components/fc-icon/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<navbar title="{{exercise.displayName || exercise.name}}" show-back />
|
||||
|
||||
<view class="favbtn" style="top: {{favTop}}px; right: {{favRight}}px;" bindtap="onToggleFav">
|
||||
<text>{{favorited ? '♥' : '♡'}}</text>
|
||||
<fc-icon name="heart" size="44" color="{{favorited ? 'var(--accent)' : 'var(--text-3)'}}" />
|
||||
</view>
|
||||
|
||||
<view class="hero">
|
||||
@@ -13,7 +13,7 @@
|
||||
mode="aspectFill"
|
||||
lazy-load="true"
|
||||
binderror="onHeroError" />
|
||||
<view wx:else class="hero__ph">🏋️</view>
|
||||
<view wx:else class="hero__ph"><fc-icon name="dumbbell" size="72" color="var(--text-3)" /></view>
|
||||
<!-- 上层叠一个动图播放器:Canvas 逐帧播放 GIF(微信 image 不播 GIF) -->
|
||||
<gif-player wx:if="{{heroGif}}" class="hero__gif" src="{{heroGif}}" />
|
||||
</view>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
.addbar {
|
||||
position: fixed; left: 0; right: 0; bottom: 0;
|
||||
padding: 20rpx 32rpx;
|
||||
background: rgba(14,17,16,0.92);
|
||||
background: var(--bar-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
border-top: 1rpx solid var(--line);
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
@@ -48,7 +48,7 @@
|
||||
.cta {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%);
|
||||
color: #0E1110; font-weight: 700; font-size: 32rpx;
|
||||
color: var(--accent-on); font-weight: 700; font-size: 32rpx;
|
||||
border-radius: 999rpx; padding: 26rpx;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
.favbtn {
|
||||
position: fixed; right: 28rpx; z-index: 60;
|
||||
width: 72rpx; height: 72rpx; border-radius: 999rpx;
|
||||
background: rgba(14,17,16,0.55);
|
||||
background: var(--float-bg);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1rpx solid var(--gold-line);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"exercise-card": "/components/exercise-card/index"
|
||||
"exercise-card": "/components/exercise-card/index",
|
||||
"fc-icon": "/components/fc-icon/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{!loading}}" class="empty-box">
|
||||
<view class="empty-box__emoji">🔍</view>
|
||||
<view class="empty-box__emoji"><fc-icon name="search" size="64" color="var(--text-3)" /></view>
|
||||
<view class="empty-box__t">还没有收藏,去发现喜欢的动作吧</view>
|
||||
<view class="empty-box__btn" bindtap="goRecommend">去推荐</view>
|
||||
</view>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
.item { position: relative; }
|
||||
.remove {
|
||||
position: absolute; right: 12rpx; top: 12rpx; z-index: 5;
|
||||
background: rgba(229,84,75,0.92); color: #fff;
|
||||
background: var(--danger); color: var(--accent-on);
|
||||
font-size: 22rpx; padding: 8rpx 18rpx; border-radius: 999rpx;
|
||||
}
|
||||
.empty { color: var(--text-3); font-size: 26rpx; text-align: center; padding: 120rpx 0; }
|
||||
@@ -29,5 +29,5 @@
|
||||
.empty-box__btn {
|
||||
margin-top: 40rpx; padding: 22rpx 64rpx; border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%);
|
||||
color: #0E1110; font-weight: 700; font-size: 30rpx;
|
||||
color: var(--accent-on); font-weight: 700; font-size: 30rpx;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
const app = getApp();
|
||||
const svc = require('../../services/exercise');
|
||||
const planSvc = require('../../services/plan');
|
||||
const favSvc = require('../../services/favorite');
|
||||
const { iconName } = require('../../utils/icon-name');
|
||||
|
||||
function greeting() {
|
||||
const h = new Date().getHours();
|
||||
if (h < 6) return '夜深了,';
|
||||
if (h < 11) return '早上好,';
|
||||
if (h < 13) return '中午好,';
|
||||
if (h < 18) return '下午好,';
|
||||
if (h < 22) return '晚上好,';
|
||||
return '夜深了,';
|
||||
}
|
||||
|
||||
function isLogin() {
|
||||
return !!wx.getStorageSync('access_token');
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -11,13 +28,19 @@ Page({
|
||||
collections: [],
|
||||
loading: true,
|
||||
quickEntries: [
|
||||
{ key: 'recommend', label: '智能推荐', emoji: '✨', url: '/pages/recommend/recommend' },
|
||||
{ key: 'bodyPart', label: '按部位', emoji: '💪', url: '/pages/category/category?dimension=bodyPart' },
|
||||
{ key: 'equipment', label: '按器械', emoji: '🏋️', url: '/pages/category/category?dimension=equipment' },
|
||||
{ key: 'official', label: '官方计划', emoji: '📋', url: '/pages/collection/collection' },
|
||||
{ key: 'myPlans', label: '我的计划', emoji: '🗂️', url: '/pages/my-plans/my-plans' },
|
||||
{ key: 'mine', label: '我的', emoji: '👤', url: '/pages/profile/profile' }
|
||||
]
|
||||
{ key: 'recommend', label: '智能推荐', icon: 'sparkles', url: '/pages/recommend/recommend' },
|
||||
{ key: 'bodyPart', label: '按部位', icon: 'accessibility', url: '/pages/category/category?dimension=bodyPart' },
|
||||
{ key: 'equipment', label: '按器械', icon: 'dumbbell', url: '/pages/category/category?dimension=equipment' },
|
||||
{ key: 'official', label: '官方计划', icon: 'clipboard-list', url: '/pages/collection/collection' },
|
||||
{ key: 'myPlans', label: '我的计划', icon: 'folder-open', url: '/pages/my-plans/my-plans' },
|
||||
{ key: 'mine', label: '我的', icon: 'user', url: '/pages/profile/profile' }
|
||||
],
|
||||
// —— 轻量个性化(登录后)——
|
||||
isLogin: false,
|
||||
greeting: '',
|
||||
myPlans: [],
|
||||
myFavorites: [],
|
||||
todayRecommend: []
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -25,6 +48,11 @@ Page({
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// tab 切回:已登录则静默刷新个性化(优先缓存)
|
||||
if (isLogin()) this.loadPersonal(true);
|
||||
},
|
||||
|
||||
loadData() {
|
||||
this.setData({ loading: true });
|
||||
Promise.all([
|
||||
@@ -32,19 +60,70 @@ Page({
|
||||
svc.listExercises({ pageSize: 8 }).catch(() => ({ items: [] })),
|
||||
svc.listCollections().catch(() => [])
|
||||
]).then(([stats, list, collections]) => {
|
||||
const cols = (collections || []).map((c) => ({
|
||||
...c,
|
||||
icon: iconName(c.emoji)
|
||||
}));
|
||||
this.setData({
|
||||
stats,
|
||||
bodyParts: (stats && stats.bodyParts) || [],
|
||||
equipment: (stats && stats.equipment) || [],
|
||||
featured: (list && list.items) || [],
|
||||
collections: collections || [],
|
||||
collections: cols,
|
||||
loading: false
|
||||
});
|
||||
if (isLogin()) this.loadPersonal(false);
|
||||
}).catch(() => {
|
||||
this.setData({ loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
// 轻量个性化:复用现有 API,纯前端聚合;silent=切 tab 静默刷新
|
||||
loadPersonal(silent) {
|
||||
if (!isLogin()) return;
|
||||
this.setData({ isLogin: true, greeting: greeting() });
|
||||
if (!silent) this.setData({ personalLoading: true });
|
||||
|
||||
const CACHE_KEY = 'fc_personal_cache';
|
||||
const cached = wx.getStorageSync(CACHE_KEY);
|
||||
const now = Date.now();
|
||||
if (cached && now - (cached.ts || 0) < 5 * 60 * 1000 && !silent) {
|
||||
this.applyPersonal(cached.data);
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
planSvc.listMine().catch(() => []),
|
||||
favSvc.listExercises().catch(() => [])
|
||||
]).then(([plans, favs]) => {
|
||||
const planArr = Array.isArray(plans) ? plans : (plans && plans.items) || [];
|
||||
const favArr = Array.isArray(favs) ? favs : (favs && favs.items) || [];
|
||||
const data = {
|
||||
myPlans: planArr.slice(0, 3).map((p) => ({
|
||||
id: p.id, name: p.name, count: p.exerciseCount || 0
|
||||
})),
|
||||
myFavorites: favArr.slice(0, 6),
|
||||
todayRecommend: (this.data.featured || []).slice(0, 3).map((e) => ({
|
||||
id: e.id, name: e.displayName || e.name,
|
||||
reason: '为你精选 · 高效入门'
|
||||
}))
|
||||
};
|
||||
wx.setStorageSync(CACHE_KEY, { ts: now, data });
|
||||
this.applyPersonal(data);
|
||||
}).catch(() => {
|
||||
this.setData({ personalLoading: false });
|
||||
});
|
||||
},
|
||||
|
||||
applyPersonal(data) {
|
||||
this.setData({
|
||||
myPlans: data.myPlans,
|
||||
myFavorites: data.myFavorites,
|
||||
todayRecommend: data.todayRecommend,
|
||||
personalLoading: false
|
||||
});
|
||||
},
|
||||
|
||||
goRecommend() {
|
||||
wx.navigateTo({ url: '/pages/recommend/recommend' });
|
||||
},
|
||||
@@ -57,6 +136,17 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/search/search' });
|
||||
},
|
||||
|
||||
onPlanTap(e) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
if (id) wx.navigateTo({ url: '/pages/plan-detail/plan-detail?id=' + encodeURIComponent(id) });
|
||||
},
|
||||
|
||||
onExercise(e) {
|
||||
const id = (e.detail && e.detail.id) || (e.currentTarget && e.currentTarget.dataset.id);
|
||||
if (!id) return;
|
||||
wx.navigateTo({ url: '/pages/detail/detail?id=' + encodeURIComponent(id) });
|
||||
},
|
||||
|
||||
onScanBodyPart(e) {
|
||||
const { value, label } = e.currentTarget.dataset;
|
||||
wx.navigateTo({
|
||||
@@ -78,12 +168,5 @@ Page({
|
||||
url: '/pages/collection-detail/collection-detail?slug=' +
|
||||
encodeURIComponent(e.currentTarget.dataset.slug)
|
||||
});
|
||||
},
|
||||
|
||||
onExercise(e) {
|
||||
if (!e || !e.detail || !e.detail.id) return;
|
||||
wx.navigateTo({
|
||||
url: '/pages/detail/detail?id=' + encodeURIComponent(e.detail.id)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"usingComponents": {
|
||||
"exercise-card": "/components/exercise-card/index",
|
||||
"section-header": "/components/section-header/index",
|
||||
"bottom-nav": "/components/bottom-nav/index"
|
||||
"bottom-nav": "/components/bottom-nav/index",
|
||||
"fc-icon": "/components/fc-icon/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,64 @@
|
||||
<view class="hero__sub">精准匹配你的目标肌群,让每一次训练都更高效</view>
|
||||
|
||||
<view class="search-pill" bindtap="onSearch">
|
||||
<text class="search-pill__icon">🔍</text>
|
||||
<fc-icon name="search" size="28" color="var(--text-2)" />
|
||||
<text class="search-pill__ph">搜索动作 / 器械 / 部位</text>
|
||||
</view>
|
||||
|
||||
<view class="cta" bindtap="goRecommend">
|
||||
<text class="cta__icon">✨</text>
|
||||
<fc-icon name="sparkles" size="24" />
|
||||
<text class="cta__txt">智能推荐</text>
|
||||
<text class="cta__arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 为你准备(登录后个性化) -->
|
||||
<view class="section personal" wx:if="{{isLogin}}">
|
||||
<view class="personal__head">
|
||||
<view class="personal__titlewrap">
|
||||
<view class="eyebrow">FOR YOU</view>
|
||||
<view class="personal__hi">{{greeting}}准备好训练了吗?</view>
|
||||
</view>
|
||||
<fc-icon name="sparkles" size="22" color="var(--accent)" />
|
||||
</view>
|
||||
|
||||
<view class="personal__block" wx:if="{{myPlans.length}}">
|
||||
<view class="personal__sub">我的计划</view>
|
||||
<view class="plan-row">
|
||||
<view class="plan-chip" wx:for="{{myPlans}}" wx:key="id" data-id="{{item.id}}" bindtap="onExercise">
|
||||
<fc-icon name="clipboard-list" size="20" color="var(--accent)" />
|
||||
<view class="plan-chip__body">
|
||||
<text class="plan-chip__name">{{item.name}}</text>
|
||||
<text class="plan-chip__meta">{{item.count}} 个动作</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="personal__block" wx:if="{{todayRecommend.length}}">
|
||||
<view class="personal__sub">今日推荐</view>
|
||||
<view class="rec-row">
|
||||
<view class="rec-item" wx:for="{{todayRecommend}}" wx:key="id" data-id="{{item.id}}" catch:tap="onExercise">
|
||||
<view class="rec-item__body">
|
||||
<text class="rec-item__name">{{item.name}}</text>
|
||||
<text class="rec-item__reason">{{item.reason}}</text>
|
||||
</view>
|
||||
<fc-icon name="chevron-right" size="18" color="var(--text-3)" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="personal__block" wx:if="{{myFavorites.length}}">
|
||||
<view class="personal__sub">我的收藏</view>
|
||||
<scroll-view scroll-x class="hscroll fav-scroll">
|
||||
<view class="fav-card" wx:for="{{myFavorites}}" wx:key="id" data-id="{{item.id}}" catch:tap="onExercise">
|
||||
<image class="fav-card__img" src="{{item.image}}" mode="aspectFill" lazy-load />
|
||||
<text class="fav-card__name">{{item.displayName || item.name}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷入口 2x3 -->
|
||||
<view class="section quick">
|
||||
<view class="quick__grid">
|
||||
@@ -26,7 +73,7 @@
|
||||
class="quick__tile"
|
||||
data-url="{{item.url}}"
|
||||
bindtap="onQuick">
|
||||
<text class="quick__emoji">{{item.emoji}}</text>
|
||||
<fc-icon name="{{item.icon}}" size="28" color="var(--text)" />
|
||||
<text class="quick__label">{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -54,7 +101,7 @@
|
||||
style="background: {{item.color}}22; border-color: {{item.color}}55;"
|
||||
data-slug="{{item.slug}}"
|
||||
bindtap="onCollection">
|
||||
<text class="prog__emoji">{{item.emoji}}</text>
|
||||
<fc-icon name="{{item.icon}}" size="36" color="{{item.color}}" />
|
||||
<text class="prog__name" style="color: {{item.color}};">{{item.name}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
@@ -2,59 +2,92 @@
|
||||
|
||||
.hero {
|
||||
padding: 20rpx 40rpx 48rpx;
|
||||
background: linear-gradient(150deg, #1C2A22 0%, #0E1110 70%);
|
||||
background: linear-gradient(160deg, var(--surface) 0%, var(--bg) 70%);
|
||||
border-bottom-left-radius: 48rpx;
|
||||
border-bottom-right-radius: 48rpx;
|
||||
}
|
||||
.hero__eyebrow { color: var(--gold); font-size: 22rpx; letter-spacing: 4rpx; }
|
||||
.hero__title { font-size: 56rpx; font-weight: 700; letter-spacing: 2rpx; margin-top: 14rpx; color: var(--text); }
|
||||
.hero__sub { font-size: 26rpx; color: var(--text-2); margin-top: 14rpx; }
|
||||
.hero__eyebrow { color: var(--accent); font-size: var(--fs-xs); letter-spacing: var(--ls-eyebrow); font-weight: var(--fw-semibold); }
|
||||
.hero__title { font-size: var(--fs-3xl); font-weight: 700; letter-spacing: var(--ls-title); margin-top: 14rpx; color: var(--text); }
|
||||
.hero__sub { font-size: var(--fs-sm); color: var(--text-2); margin-top: 14rpx; }
|
||||
|
||||
.search-pill {
|
||||
margin-top: 36rpx;
|
||||
display: flex; align-items: center;
|
||||
background: var(--surface); border: 1rpx solid var(--line);
|
||||
border-radius: 999rpx; padding: 22rpx 30rpx;
|
||||
border-radius: var(--radius-pill); padding: 22rpx 30rpx;
|
||||
}
|
||||
.search-pill__icon { font-size: 30rpx; margin-right: 16rpx; }
|
||||
.search-pill__ph { color: var(--text-3); font-size: 28rpx; }
|
||||
.search-pill__ph { color: var(--text-3); font-size: var(--fs-base); }
|
||||
|
||||
.cta {
|
||||
margin-top: 24rpx;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%);
|
||||
color: #0E1110; font-weight: 700; font-size: 32rpx;
|
||||
border-radius: 999rpx; padding: 28rpx;
|
||||
box-shadow: 0 12rpx 30rpx rgba(217,179,108,0.28);
|
||||
color: var(--accent-on); font-weight: 700; font-size: var(--fs-lg);
|
||||
border-radius: var(--radius-pill); padding: 28rpx;
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
.cta__icon { margin-right: 12rpx; }
|
||||
.cta__arrow { margin-left: 12rpx; font-size: 36rpx; }
|
||||
|
||||
.quick__grid { display: flex; flex-wrap: wrap; gap: 18rpx; }
|
||||
.quick__grid { display: flex; flex-wrap: wrap; gap: 16rpx; }
|
||||
.quick__tile {
|
||||
width: calc((100% - 36rpx) / 3);
|
||||
width: calc((100% - 32rpx) / 3);
|
||||
background: var(--surface); border: 1rpx solid var(--line);
|
||||
border-radius: var(--radius); padding: 28rpx 18rpx;
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
position: relative; overflow: hidden;
|
||||
transition: background var(--motion-fast) var(--ease);
|
||||
}
|
||||
.quick__tile:active { background: var(--surface-2); }
|
||||
.quick__tile::after {
|
||||
content: ''; position: absolute; inset: 0;
|
||||
background: radial-gradient(circle at 50% 0%, var(--gold-soft), transparent 70%);
|
||||
}
|
||||
.quick__emoji { font-size: 48rpx; position: relative; z-index: 1; }
|
||||
.quick__label { font-size: 26rpx; margin-top: 12rpx; color: var(--text); position: relative; z-index: 1; }
|
||||
|
||||
/* ===== 为你准备(个性化) ===== */
|
||||
.personal__head { display: flex; align-items: center; justify-content: space-between; }
|
||||
.personal__hi { font-size: var(--fs-xl); font-weight: 600; color: var(--text); margin-top: 6rpx; }
|
||||
.personal__sub { font-size: var(--fs-sm); color: var(--text-2); font-weight: 600; margin: 28rpx 0 16rpx; }
|
||||
|
||||
.plan-row { display: flex; flex-direction: column; gap: 12rpx; }
|
||||
.plan-chip {
|
||||
display: flex; align-items: center; gap: 16rpx;
|
||||
background: var(--surface); border: 1rpx solid var(--line);
|
||||
border-radius: var(--radius); padding: 20rpx 24rpx;
|
||||
}
|
||||
.plan-chip:active { background: var(--surface-2); }
|
||||
.plan-chip__body { display: flex; flex-direction: column; }
|
||||
.plan-chip__name { font-size: var(--fs-base); color: var(--text); font-weight: 600; }
|
||||
.plan-chip__meta { font-size: 22rpx; color: var(--text-3); margin-top: 4rpx; }
|
||||
|
||||
.rec-row { display: flex; flex-direction: column; gap: 12rpx; }
|
||||
.rec-item {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
background: var(--surface); border: 1rpx solid var(--line);
|
||||
border-radius: var(--radius); padding: 20rpx 24rpx;
|
||||
}
|
||||
.rec-item:active { background: var(--surface-2); }
|
||||
.rec-item__body { display: flex; flex-direction: column; }
|
||||
.rec-item__name { font-size: var(--fs-base); color: var(--text); font-weight: 600; }
|
||||
.rec-item__reason { font-size: 22rpx; color: var(--accent); margin-top: 4rpx; }
|
||||
|
||||
.fav-scroll { white-space: nowrap; margin-top: 4rpx; }
|
||||
.fav-card { display: inline-flex; flex-direction: column; width: 200rpx; margin-right: 16rpx; vertical-align: top; }
|
||||
.fav-card__img {
|
||||
width: 200rpx; height: 200rpx; border-radius: var(--radius);
|
||||
background: var(--surface-2); border: 1rpx solid var(--line);
|
||||
}
|
||||
.fav-card__name { font-size: 24rpx; color: var(--text-2); margin-top: 10rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
.hscroll { white-space: nowrap; margin-top: 8rpx; }
|
||||
.hscroll__item { display: inline-block; width: 300rpx; margin-right: 20rpx; vertical-align: top; }
|
||||
|
||||
.chip-scroll { white-space: nowrap; margin-top: 8rpx; }
|
||||
.prog {
|
||||
display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
min-width: 160rpx; padding: 24rpx 28rpx; margin-right: 18rpx;
|
||||
min-width: 160rpx; padding: 24rpx 28rpx; margin-right: 16rpx;
|
||||
border-radius: var(--radius-sm); border: 1rpx solid;
|
||||
}
|
||||
.prog__emoji { font-size: 44rpx; }
|
||||
.prog__name { font-size: 26rpx; font-weight: 700; margin-top: 10rpx; }
|
||||
|
||||
.bp-grid { display: flex; flex-wrap: wrap; gap: 16rpx; }
|
||||
@@ -70,9 +103,9 @@
|
||||
.eq {
|
||||
display: inline-flex; align-items: center; gap: 12rpx;
|
||||
background: var(--surface-2); border: 1rpx solid var(--line);
|
||||
border-radius: 999rpx; padding: 16rpx 28rpx; margin-right: 16rpx;
|
||||
border-radius: var(--radius-pill); padding: 16rpx 28rpx; margin-right: 16rpx;
|
||||
}
|
||||
.eq__name { font-size: 26rpx; color: var(--text); }
|
||||
.eq__count { font-size: 22rpx; color: var(--gold); }
|
||||
.eq__count { font-size: 22rpx; color: var(--accent); }
|
||||
|
||||
.empty { color: var(--text-3); font-size: 26rpx; text-align: center; padding: 40rpx 0; }
|
||||
.empty { color: var(--text-3); font-size: var(--fs-sm); text-align: center; padding: 40rpx 0; }
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"plan-card": "/components/plan-card/index",
|
||||
"bottom-nav": "/components/bottom-nav/index"
|
||||
"bottom-nav": "/components/bottom-nav/index",
|
||||
"fc-icon": "/components/fc-icon/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</block>
|
||||
|
||||
<view wx:elif="{{!loading}}" class="empty-box">
|
||||
<view class="empty-box__emoji">📋</view>
|
||||
<view class="empty-box__emoji"><fc-icon name="clipboard-list" size="64" color="var(--text-3)" /></view>
|
||||
<view class="empty-box__t">还没有计划,创建一个开始训练吧</view>
|
||||
<view class="empty-box__btn" bindtap="onNew">+ 新建计划</view>
|
||||
<view class="empty-box__link" bindtap="goOfficial">或浏览官方训练计划 ›</view>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
.newbar__btn {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%);
|
||||
color: #0E1110; font-weight: 700; font-size: 32rpx;
|
||||
color: var(--accent-on); font-weight: 700; font-size: 32rpx;
|
||||
border-radius: 999rpx; padding: 26rpx;
|
||||
}
|
||||
.grid { display: flex; flex-wrap: wrap; gap: 18rpx; }
|
||||
@@ -20,6 +20,6 @@
|
||||
.empty-box__btn {
|
||||
margin-top: 44rpx; padding: 24rpx 88rpx; border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%);
|
||||
color: #0E1110; font-weight: 700; font-size: 30rpx;
|
||||
color: var(--accent-on); font-weight: 700; font-size: 30rpx;
|
||||
}
|
||||
.empty-box__link { margin-top: 28rpx; font-size: 26rpx; color: var(--gold); }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const app = getApp();
|
||||
const planSvc = require('../../services/plan');
|
||||
const { iconName } = require('../../utils/icon-name');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -24,7 +25,7 @@ Page({
|
||||
? raw.slice(0, 10)
|
||||
: (raw ? String(raw).slice(0, 10) : '');
|
||||
this.setData({
|
||||
plan: { ...plan, updatedAtText },
|
||||
plan: { ...plan, updatedAtText, icon: iconName(plan.emoji) },
|
||||
exercises: (plan && plan.exercises) || [],
|
||||
loading: false
|
||||
});
|
||||
@@ -61,7 +62,7 @@ Page({
|
||||
wx.showModal({
|
||||
title: '删除计划',
|
||||
content: '确定要删除该训练计划吗?此操作不可恢复。',
|
||||
confirmColor: '#E5544B',
|
||||
confirmColor: '#FF5C5C',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
planSvc.remove(this.data.id).then(() => {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"exercise-card": "/components/exercise-card/index",
|
||||
"section-header": "/components/section-header/index"
|
||||
"section-header": "/components/section-header/index",
|
||||
"fc-icon": "/components/fc-icon/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<navbar title="{{plan.name}}" show-back />
|
||||
|
||||
<view class="header" style="background: linear-gradient(135deg, {{plan.color}}26, var(--surface));">
|
||||
<view class="header__emoji" style="background: {{plan.color}}22; color: {{plan.color}};">{{plan.emoji}}</view>
|
||||
<view class="header__emoji" style="background: {{plan.color}}22; color: {{plan.color}};"><fc-icon name="{{plan.icon}}" size="48" color="{{plan.color}}" /></view>
|
||||
<view class="header__name" style="color: {{plan.color}};">{{plan.name}}</view>
|
||||
<view wx:if="{{plan.description}}" class="header__desc">{{plan.description}}</view>
|
||||
<view class="header__meta">共 {{exercises.length}} 个动作<text wx:if="{{plan.updatedAtText}}"> · 更新于 {{plan.updatedAtText}}</text></view>
|
||||
@@ -20,7 +20,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<view wx:elif="{{!loading}}" class="empty-box">
|
||||
<view class="empty-box__emoji">🏋️</view>
|
||||
<view class="empty-box__emoji"><fc-icon name="dumbbell" size="64" color="var(--text-3)" /></view>
|
||||
<view class="empty-box__t">这个计划还没有动作,去添加吧</view>
|
||||
<view class="empty-box__btn" bindtap="onAdd">+ 添加动作</view>
|
||||
</view>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
.list__card { width: 100%; }
|
||||
.list__remove {
|
||||
position: absolute; right: 12rpx; top: 12rpx; z-index: 5;
|
||||
background: rgba(229,84,75,0.92); color: #fff;
|
||||
background: var(--danger); color: var(--accent-on);
|
||||
font-size: 22rpx; padding: 8rpx 18rpx; border-radius: 999rpx;
|
||||
}
|
||||
|
||||
@@ -30,13 +30,13 @@
|
||||
.empty-box__btn {
|
||||
margin-top: 40rpx; padding: 22rpx 64rpx; border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%);
|
||||
color: #0E1110; font-weight: 700; font-size: 30rpx;
|
||||
color: var(--accent-on); font-weight: 700; font-size: 30rpx;
|
||||
}
|
||||
|
||||
.actionbar {
|
||||
position: fixed; left: 0; right: 0; bottom: 0;
|
||||
display: flex; gap: 16rpx; padding: 20rpx 24rpx;
|
||||
background: rgba(14,17,16,0.92);
|
||||
background: var(--bar-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
border-top: 1rpx solid var(--line);
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
@@ -45,6 +45,6 @@
|
||||
flex: 1; display: flex; align-items: center; justify-content: center;
|
||||
border-radius: 999rpx; padding: 24rpx 10rpx; font-size: 28rpx; font-weight: 700;
|
||||
}
|
||||
.btn--gold { background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%); color: #0E1110; }
|
||||
.btn--gold { background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%); color: var(--accent-on); }
|
||||
.btn--ghost { background: var(--surface-2); color: var(--text); border: 1rpx solid var(--line); flex: 0 0 auto; width: 180rpx; }
|
||||
.btn--del { background: rgba(229,84,75,0.16); color: #E5544B; border: 1rpx solid rgba(229,84,75,0.4); flex: 0 0 auto; width: 180rpx; }
|
||||
.btn--del { background: var(--danger-soft); color: var(--danger); border: 1rpx solid var(--danger-soft); flex: 0 0 auto; width: 180rpx; }
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
const app = getApp();
|
||||
const planSvc = require('../../services/plan');
|
||||
|
||||
const EMOJIS = ['📋', '💪', '🔥', '🏋️', '🏃', '🧘', '🤸', '🥊', '🚴', '⚡', '🌟', '🎯'];
|
||||
const COLORS = ['#D9B36C', '#58C9B9', '#9B8CFF', '#F08A5D', '#3FBF7F', '#5B9DF9'];
|
||||
// UGC 计划图标选择器:Lucide 运动子集(非 emoji)
|
||||
const ICONS = ['clipboard-list', 'dumbbell', 'flame', 'accessibility', 'person-standing',
|
||||
'bike', 'heart', 'target', 'trophy', 'medal', 'star', 'activity',
|
||||
'sparkles', 'zap', 'trending-up', 'folder-open'];
|
||||
// 计划主题色:对齐 Kinetic Dark 语义色板
|
||||
const COLORS = ['#B6F24E', '#2BD9A6', '#4FA8FF', '#F2B53D', '#FF5C5C', '#9BA7A1'];
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -11,9 +15,9 @@ Page({
|
||||
id: '',
|
||||
name: '',
|
||||
description: '',
|
||||
emoji: '📋',
|
||||
color: '#D9B36C',
|
||||
emojis: EMOJIS,
|
||||
emoji: 'clipboard-list',
|
||||
color: '#B6F24E',
|
||||
emojis: ICONS,
|
||||
colors: COLORS,
|
||||
loading: false,
|
||||
dirty: false // 是否有未保存的修改
|
||||
@@ -29,8 +33,8 @@ Page({
|
||||
this.setData({
|
||||
name: p.name || '',
|
||||
description: p.description || '',
|
||||
emoji: p.emoji || '📋',
|
||||
color: p.color || '#D9B36C',
|
||||
emoji: p.emoji || 'clipboard-list',
|
||||
color: p.color || '#B6F24E',
|
||||
loading: false
|
||||
});
|
||||
}).catch(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index"
|
||||
"navbar": "/components/navbar/index",
|
||||
"fc-icon": "/components/fc-icon/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<view class="section">
|
||||
<view class="preview" style="background: linear-gradient(135deg, {{color}}26, var(--surface));">
|
||||
<view class="preview__emoji" style="background: {{color}}22; color: {{color}};">{{emoji}}</view>
|
||||
<view class="preview__emoji" style="background: {{color}}22; color: {{color}};"><fc-icon name="{{emoji}}" size="48" color="{{color}}" /></view>
|
||||
<view class="preview__name" style="color: {{color}};">{{name || '计划名称'}}</view>
|
||||
<view class="preview__desc">{{description || '计划描述(选填)'}}</view>
|
||||
</view>
|
||||
@@ -40,7 +40,9 @@
|
||||
class="emoji {{emoji === item ? 'emoji--active' : ''}}"
|
||||
style="{{emoji === item ? 'border-color:' + color + '; background:' + color + '22;' : ''}}"
|
||||
data-emoji="{{item}}"
|
||||
bindtap="onPickEmoji">{{item}}</view>
|
||||
bindtap="onPickEmoji">
|
||||
<fc-icon name="{{item}}" size="44" color="{{emoji === item ? color : 'var(--text)'}}" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
.savebar {
|
||||
position: fixed; left: 0; right: 0; bottom: 0;
|
||||
padding: 20rpx 32rpx;
|
||||
background: rgba(14,17,16,0.92);
|
||||
background: var(--bar-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
border-top: 1rpx solid var(--line);
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
@@ -49,6 +49,6 @@
|
||||
.savebar__btn {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%);
|
||||
color: #0E1110; font-weight: 700; font-size: 32rpx;
|
||||
color: var(--accent-on); font-weight: 700; font-size: 32rpx;
|
||||
border-radius: 999rpx; padding: 26rpx;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"plan-card": "/components/plan-card/index"
|
||||
"plan-card": "/components/plan-card/index",
|
||||
"fc-icon": "/components/fc-icon/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</block>
|
||||
|
||||
<view wx:elif="{{!loading}}" class="empty-box">
|
||||
<view class="empty-box__emoji">⭐</view>
|
||||
<view class="empty-box__emoji"><fc-icon name="star" size="64" color="var(--text-3)" /></view>
|
||||
<view class="empty-box__t">还没有收藏官方计划</view>
|
||||
<view class="empty-box__btn" bindtap="goOfficial">浏览官方计划</view>
|
||||
</view>
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
.empty-box__btn {
|
||||
margin-top: 40rpx; padding: 22rpx 64rpx; border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%);
|
||||
color: #0E1110; font-weight: 700; font-size: 30rpx;
|
||||
color: var(--accent-on); font-weight: 700; font-size: 30rpx;
|
||||
}
|
||||
|
||||
@@ -3,20 +3,22 @@ const auth = require('../../utils/auth');
|
||||
const favSvc = require('../../services/favorite');
|
||||
const planSvc = require('../../services/plan');
|
||||
|
||||
const EMOJIS = ['💪', '🏋️', '🔥', '🏃', '🧘', '🤸', '🥊', '🚴', '⚡', '🌟', '🐯', '🦁'];
|
||||
// UGC 头像图标选择器:Lucide 运动子集(非 emoji)
|
||||
const ICONS = ['dumbbell', 'accessibility', 'person-standing', 'bike', 'heart', 'flame',
|
||||
'target', 'medal', 'trophy', 'award', 'star', 'activity', 'sparkles', 'zap', 'trending-up'];
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
loggedIn: false,
|
||||
profile: { nickname: '', avatar: '💪' },
|
||||
profile: { nickname: '', avatar: 'user' },
|
||||
counts: { fav: 0, plans: 0, favPlans: 0 },
|
||||
loading: false,
|
||||
// 编辑态
|
||||
showEmojiPicker: false,
|
||||
emojis: EMOJIS,
|
||||
emojis: ICONS,
|
||||
draftNickname: '',
|
||||
draftAvatar: '💪'
|
||||
draftAvatar: 'user'
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -70,7 +72,7 @@ Page({
|
||||
if (res.confirm) {
|
||||
this.setData({
|
||||
draftNickname: (res.content || '').trim() || cur.nickname,
|
||||
draftAvatar: cur.avatar || '💪',
|
||||
draftAvatar: cur.avatar || 'user',
|
||||
showEmojiPicker: true
|
||||
});
|
||||
}
|
||||
@@ -112,7 +114,7 @@ Page({
|
||||
auth.logout();
|
||||
this.setData({
|
||||
loggedIn: false,
|
||||
profile: { nickname: '', avatar: '💪' },
|
||||
profile: { nickname: '', avatar: 'user' },
|
||||
counts: { fav: 0, plans: 0, favPlans: 0 }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"avatar": "/components/avatar/index",
|
||||
"bottom-nav": "/components/bottom-nav/index"
|
||||
"bottom-nav": "/components/bottom-nav/index",
|
||||
"fc-icon": "/components/fc-icon/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<!-- 未登录 -->
|
||||
<view wx:if="{{!loggedIn}}" class="login">
|
||||
<view class="login__logo">💪</view>
|
||||
<view class="login__logo"><fc-icon name="dumbbell" size="64" color="var(--accent)" /></view>
|
||||
<view class="login__title">FITCOACH 智能健身教练</view>
|
||||
<view class="login__sub">登录后同步你的收藏与训练计划</view>
|
||||
<button class="login__btn" bindtap="onLogin">微信登录</button>
|
||||
@@ -38,22 +38,22 @@
|
||||
|
||||
<view class="menu card">
|
||||
<view class="cell" bindtap="goFav">
|
||||
<text class="cell__icon">❤️</text>
|
||||
<fc-icon class="cell__icon" name="heart" size="24" color="var(--text-2)" />
|
||||
<text class="cell__label">我的收藏动作</text>
|
||||
<text class="cell__arrow">›</text>
|
||||
</view>
|
||||
<view class="cell" bindtap="goPlans">
|
||||
<text class="cell__icon">🗂️</text>
|
||||
<fc-icon class="cell__icon" name="folder-open" size="24" color="var(--text-2)" />
|
||||
<text class="cell__label">我的训练计划</text>
|
||||
<text class="cell__arrow">›</text>
|
||||
</view>
|
||||
<view class="cell" bindtap="goFavPlans">
|
||||
<text class="cell__icon">⭐</text>
|
||||
<fc-icon class="cell__icon" name="star" size="24" color="var(--text-2)" />
|
||||
<text class="cell__label">收藏的官方计划</text>
|
||||
<text class="cell__arrow">›</text>
|
||||
</view>
|
||||
<view class="cell" bindtap="goOfficial">
|
||||
<text class="cell__icon">📋</text>
|
||||
<fc-icon class="cell__icon" name="clipboard-list" size="24" color="var(--text-2)" />
|
||||
<text class="cell__label">官方训练计划</text>
|
||||
<text class="cell__arrow">›</text>
|
||||
</view>
|
||||
@@ -72,7 +72,9 @@
|
||||
wx:key="*this"
|
||||
class="emoji {{draftAvatar === item ? 'emoji--active' : ''}}"
|
||||
data-emoji="{{item}}"
|
||||
bindtap="onPickEmoji">{{item}}</view>
|
||||
bindtap="onPickEmoji">
|
||||
<fc-icon name="{{item}}" size="48" color="var(--text)" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="sheet__nick">{{draftNickname || '未命名'}}</view>
|
||||
<view class="sheet__actions">
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
.login__btn {
|
||||
margin-top: 56rpx; width: 420rpx;
|
||||
background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%);
|
||||
color: #0E1110; font-weight: 700; font-size: 32rpx;
|
||||
color: var(--accent-on); font-weight: 700; font-size: 32rpx;
|
||||
border-radius: 999rpx; padding: 24rpx;
|
||||
}
|
||||
.login__btn::after { border: none; }
|
||||
@@ -24,7 +24,7 @@
|
||||
/* 个人中心 */
|
||||
.center { padding: 32rpx; }
|
||||
.profile {
|
||||
background: linear-gradient(135deg, rgba(217,179,108,0.14), var(--surface));
|
||||
background: linear-gradient(135deg, var(--accent-soft), var(--surface));
|
||||
border: 1rpx solid var(--gold-line);
|
||||
border-radius: var(--radius); padding: 36rpx;
|
||||
}
|
||||
@@ -57,7 +57,7 @@
|
||||
/* 头像选择弹层 */
|
||||
.sheet {
|
||||
position: fixed; inset: 0; z-index: 80;
|
||||
background: rgba(0,0,0,0.5);
|
||||
background: var(--scrim);
|
||||
display: flex; align-items: flex-end;
|
||||
}
|
||||
.sheet__panel {
|
||||
@@ -81,4 +81,4 @@
|
||||
font-size: 30rpx; font-weight: 700;
|
||||
}
|
||||
.sheet__btn--ghost { background: var(--surface-2); color: var(--text); border: 1rpx solid var(--line); }
|
||||
.sheet__btn--gold { background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%); color: #0E1110; }
|
||||
.sheet__btn--gold { background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%); color: var(--accent-on); }
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
.confirm {
|
||||
position: fixed; left: 0; right: 0; bottom: 0;
|
||||
padding: 20rpx 32rpx;
|
||||
background: rgba(14,17,16,0.92);
|
||||
background: var(--bar-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
border-top: 1rpx solid var(--line);
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
@@ -34,7 +34,7 @@
|
||||
.cta {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%);
|
||||
color: #0E1110; font-weight: 700; font-size: 32rpx;
|
||||
color: var(--accent-on); font-weight: 700; font-size: 32rpx;
|
||||
border-radius: 999rpx; padding: 26rpx;
|
||||
}
|
||||
.cta__arrow { margin-left: 12rpx; font-size: 36rpx; }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"exercise-card": "/components/exercise-card/index"
|
||||
"exercise-card": "/components/exercise-card/index",
|
||||
"fc-icon": "/components/fc-icon/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<navbar title="{{navTitle}}" show-back="{{selectMode}}" />
|
||||
<view class="searchbar">
|
||||
<view class="searchbar__box">
|
||||
<text class="searchbar__icon">🔍</text>
|
||||
<fc-icon class="searchbar__icon" name="search" size="28" color="var(--text-2)" />
|
||||
<input
|
||||
class="searchbar__input"
|
||||
placeholder="{{selectMode ? '选择动作加入计划' : '搜索动作 / 器械 / 部位'}}"
|
||||
|
||||
53
miniprogram/utils/icon-map.js
Normal file
53
miniprogram/utils/icon-map.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// AUTO-GENERATED by /tmp/gen_iconfont.mjs — 勿手改
|
||||
// Lucide (MIT) 子集 iconfont,font-family: fc-icon
|
||||
module.exports = {
|
||||
"zap": "\\uf101",
|
||||
"x-circle": "\\uf102",
|
||||
"user": "\\uf103",
|
||||
"trophy": "\\uf104",
|
||||
"trending-up": "\\uf105",
|
||||
"trash-2": "\\uf106",
|
||||
"target": "\\uf107",
|
||||
"star": "\\uf108",
|
||||
"sparkles": "\\uf109",
|
||||
"sliders-horizontal": "\\uf10a",
|
||||
"share-2": "\\uf10b",
|
||||
"settings": "\\uf10c",
|
||||
"search": "\\uf10d",
|
||||
"rotate-cw": "\\uf10e",
|
||||
"refresh-cw": "\\uf10f",
|
||||
"plus": "\\uf110",
|
||||
"play": "\\uf111",
|
||||
"person-standing": "\\uf112",
|
||||
"medal": "\\uf113",
|
||||
"log-out": "\\uf114",
|
||||
"layout-grid": "\\uf115",
|
||||
"layers": "\\uf116",
|
||||
"info": "\\uf117",
|
||||
"image": "\\uf118",
|
||||
"home": "\\uf119",
|
||||
"heart": "\\uf11a",
|
||||
"grid-2x2": "\\uf11b",
|
||||
"folder-open": "\\uf11c",
|
||||
"flame": "\\uf11d",
|
||||
"filter": "\\uf11e",
|
||||
"dumbbell": "\\uf11f",
|
||||
"download": "\\uf120",
|
||||
"clock": "\\uf121",
|
||||
"clipboard-list": "\\uf122",
|
||||
"circle-x": "\\uf123",
|
||||
"chevron-right": "\\uf124",
|
||||
"chevron-down": "\\uf125",
|
||||
"check": "\\uf126",
|
||||
"check-circle": "\\uf127",
|
||||
"calendar": "\\uf128",
|
||||
"bookmark": "\\uf129",
|
||||
"bike": "\\uf12a",
|
||||
"bell": "\\uf12b",
|
||||
"award": "\\uf12c",
|
||||
"arrow-right": "\\uf12d",
|
||||
"arrow-left": "\\uf12e",
|
||||
"alert-circle": "\\uf12f",
|
||||
"activity": "\\uf130",
|
||||
"accessibility": "\\uf131"
|
||||
};
|
||||
19
miniprogram/utils/icon-name.js
Normal file
19
miniprogram/utils/icon-name.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// emoji → Lucide 图标名 映射(后端历史数据里仍可能带 emoji 的字段,如 plan.emoji / collection.emoji)
|
||||
const EMOJI_TO_ICON = {
|
||||
'✨': 'sparkles', '💪': 'accessibility', '🏋️': 'dumbbell', '📋': 'clipboard-list',
|
||||
'🗂️': 'folder-open', '👤': 'user', '🏃': 'person-standing', '🔥': 'flame',
|
||||
'❤️': 'heart', '⭐': 'star', '🎯': 'target', '🧘': 'activity', '🥗': 'activity',
|
||||
'🏠': 'home', '📚': 'layout-grid', '📝': 'clipboard-list', '💡': 'sparkles',
|
||||
'🚀': 'trending-up', '⏱️': 'clock', '🏆': 'trophy', '🥇': 'medal',
|
||||
// 官方训练组合 seed emoji(后端 collections.ts)→ 全部映射到现有字形
|
||||
'⚡': 'zap', '🌟': 'star', '🌱': 'trending-up', '🌸': 'sparkles',
|
||||
'📈': 'trending-up', '🦵': 'dumbbell', '🦾': 'dumbbell', '🪢': 'activity'
|
||||
};
|
||||
|
||||
// 旧 emoji → lucide 名;已是 lucide 名则直接透传;未知返回 fallback
|
||||
function iconName(v, fallback) {
|
||||
if (!v) return fallback || 'folder-open';
|
||||
return EMOJI_TO_ICON[v] || v;
|
||||
}
|
||||
|
||||
module.exports = { EMOJI_TO_ICON, iconName };
|
||||
Reference in New Issue
Block a user