docs(skills): design-handoff 设计稿 + spec + .gitignore 更新

- mp-11-doctor-core 设计交付包(截图 + tokens)
- mp-13/mp-14 新原型 HTML
- design-handoff skill 设计规格文档
- .gitignore 排除 dist-h5/test-results/uploads 等构建产物
This commit is contained in:
iven
2026-05-18 02:13:29 +08:00
parent ded37830fe
commit 3aa71a94d2
9 changed files with 2084 additions and 2 deletions

View File

@@ -9,7 +9,8 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"js-yaml": "^4.1.1"
"js-yaml": "^4.1.1",
"playwright": "^1.58.0"
}
},
"node_modules/argparse": {
@@ -18,6 +19,20 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0"
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
@@ -29,6 +44,36 @@
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/playwright": {
"version": "1.58.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.0.tgz",
"integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz",
"integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}

View File

@@ -11,6 +11,7 @@
"license": "ISC",
"type": "commonjs",
"dependencies": {
"js-yaml": "^4.1.1"
"js-yaml": "^4.1.1",
"playwright": "^1.58.0"
}
}

10
.gitignore vendored
View File

@@ -28,6 +28,16 @@ docker/redis_data/
# Test artifacts
.test_token
test-results/
# Build outputs
apps/miniprogram/dist-h5/
# Runtime uploads
uploads/
# Temp logs
_server_out.txt
*.heapsnapshot
perf-trace-*.json
docs/debug-*.png

View File

@@ -0,0 +1,29 @@
prototype: mp-11-doctor-core.html
source: docs/design/mp-11-doctor-core.html
generated_at: "2026-05-18T00:16:00+08:00"
variant: 医生端(.doctor-mode 靛蓝主色 #3A6B8C
tokens:
matched: 20
unmatched: 2
pending: 15
components:
total: 14
mapped: 8
new: 6
interactions: 5
screens:
- label: 医生工作台
component: DoctorHome
screenshot: screenshots/doctor.png
- label: 待办收件箱
component: ActionInbox
screenshot: screenshots/screen-2.png
- label: 在线咨询
component: ConsultList
screenshot: screenshots/consultation.png
- label: 随访管理
component: FollowUpList
screenshot: screenshots/followupmanage.png
- label: 患者管理
component: PatientList
screenshot: screenshots/patientmanage.png

View File

@@ -0,0 +1,458 @@
# mp-11-doctor-core 设计规格
> 由 design-handoff 自动生成 | 源文件: mp-11-doctor-core.html | 生成时间: 2026-05-18
## 1. 概览
- **原型文件**: mp-11-doctor-core.html
- **设计变体**: 医生端(`.doctor-mode` 靛蓝主色 #3A6B8C 替代患者端赤土橙 #C4623A
- **设计 Token 数**: 20/22 已匹配confirmed15 pending2 unmatched
- **组件数**: 148 已映射 + 6 需新建/扩展)
- **交互行为数**: 5 matched / 3 unmatched
- **屏幕数**: 5
### 屏幕索引
| # | 屏幕 | 组件名 | 截图 |
|---|------|--------|------|
| 1 | 医生工作台 | DoctorHome | screenshots/doctor.png |
| 2 | 待办收件箱 | ActionInbox | screenshots/screen-2.png |
| 3 | 在线咨询 | ConsultList | screenshots/consultation.png |
| 4 | 随访管理 | FollowUpList | screenshots/followupmanage.png |
| 5 | 患者管理 | PatientList | screenshots/patientmanage.png |
---
## 2. 设计 Token
### 2.1 医生端色彩变体
> 医生端核心差异:主色系从赤土橙切换为靛蓝,通过 `.doctor-mode` CSS 变量级联覆盖实现。
| Token | 原型值 | CSS Token | SCSS 变量 | 状态 | 说明 |
|-------|--------|-----------|-----------|------|------|
| T.pri | `#3A6B8C` | `--tk-pri` | `$doc-pri` | confirmed | 医生端靛蓝主色 |
| T.priL | `#D4E5F0` | `--tk-pri-l` | `$doc-pri-l` | confirmed | 靛蓝浅色 |
| T.priD | `#2A4F6A` | `--tk-pri-d` | `$doc-pri-d` | confirmed | 靛蓝深色 |
### 2.2 语义色彩
| Token | 值 | CSS Token / SCSS 变量 | 状态 | 角色 |
|-------|-----|----------------------|------|------|
| T.bg | `#F5F0EB` | `$bg` | pending | 页面背景/温润米底 |
| T.card | `#FFFFFF` | `--tk-card-bg` | confirmed | 卡片白底 |
| T.surface | `#EDE8E2` | `$surface-alt` | pending | 辅助底色 |
| T.tx | `#2D2A26` | `$tx` | pending | 主文字/暖黑 |
| T.tx2 | `#5A554F` | `$tx2` | pending | 次文字/暖灰 |
| T.tx3 | `#78716C` | `--tk-text-secondary` | confirmed | 辅助文字 |
| T.bd | `#E8E2DC` | `$bd` | pending | 边框色 |
| T.bdL | `#F0EBE5` | `$bd-l` | pending | 浅边框色 |
| T.acc | `#5B7A5E` | `$acc` | pending | 鼠尾草绿/成功色 |
| T.accL | `#E8F0E8` | `$acc-l` | pending | 成功浅色 |
| T.wrn | `#C4873A` | `$wrn` | pending | 警告色/暖琥珀 |
| T.wrnL | `#FFF3E0` | `$wrn-l` | pending | 警告浅色 |
| T.dan | `#B54A4A` | `$dan` | pending | 危险色/柔红 |
| T.danL | `#FDEAEA` | `$dan-l` | pending | 危险浅色 |
### 2.3 字号
| 原型值 (px) | CSS Token | 状态 | 使用位置 |
|------------|-----------|------|---------|
| 28 | `--tk-font-h1` | confirmed | 统计数值 |
| 26 | — | unmatched | 工作台标题(介于 h1/h2 之间) |
| 20 | — | unmatched | 头像文字 |
| 18 | `--tk-font-body-lg` | confirmed | NavBar 标题、头像内文字 |
| 16 | `--tk-font-body` | confirmed | 页面标题 |
| 15 | — | unmatched | 列表主文字(介于 body/body-sm |
| 14 | `--tk-font-body-sm` | confirmed | 次要文字、日期 |
| 13 | `--tk-font-cap` | confirmed | section 标题、辅助信息 |
| 12 | — | unmatched | 标签文字、时间、辅助 |
| 11 | `--tk-font-micro` | confirmed | 角标、Tab 文字、Tag |
### 2.4 间距
| 原型值 (px) | CSS Token / SCSS 变量 | 状态 | 使用位置 |
|------------|----------------------|------|---------|
| 16 | `--tk-gap-md` / `$sp-md` | confirmed | 卡片 padding、section 间距 |
| 14 | — | unmatched | 列表项 padding14px 16px |
| 12 | `--tk-gap-sm` / `$sp-sm` | confirmed | 列表项间距、卡片 padding |
| 10 | — | unmatched | 待办项间距、元素间距 |
| 8 | `--tk-gap-xs` / `$sp-xs` | confirmed | Tab 间距、元素 gap |
| 6 | — | unmatched | 小间距 |
| 3 | `--tk-tag-padding-v` | confirmed | Tab 文字与图标间距 |
### 2.5 圆角
| 原型值 (px) | CSS Token / SCSS 变量 | 状态 | 使用位置 |
|------------|----------------------|------|---------|
| 16 | `--tk-card-radius` / `$r` | confirmed | 卡片圆角 |
| 12 | `$r-sm` | pending | 搜索栏、数据条圆角 |
| 8 | `$r-xs` | pending | 小圆角 |
| 6 | — | unmatched | Tag 圆角 |
| 20 | `$r-lg` | pending | 筛选标签 pill 圆角 |
| 22-26 | — | unmatched | 头像圆形borderRadius = width/2 |
### 2.6 字体
| Token | 值 | 状态 | 说明 |
|-------|-----|------|------|
| T.serif | `Georgia, 'Times New Roman', serif` | unmatched | 标题/数值衬线字体 |
| T.sans | `-apple-system, 'PingFang SC', sans-serif` | unmatched | 正文无衬线字体 |
---
## 3. 组件清单
### 3.1 组件树
```
App
├── IosFrame (设备框,不实现)
│ ├── DoctorHome ─── 医生工作台
│ │ ├── PageShell
│ │ ├── SectionTitle (工作台标题)
│ │ ├── ContentCard > StatGrid (今日概览)
│ │ ├── ShortcutButton × 4
│ │ ├── SectionTitle (待办提醒)
│ │ └── TodoAlert × 2
│ ├── ActionInbox ─── 待办收件箱
│ │ ├── NavBar
│ │ ├── TabFilter (筛选标签)
│ │ └── ListItem × 5
│ ├── ConsultList ─── 在线咨询
│ │ ├── NavBar
│ │ ├── StatusTabs (进行中/已结束)
│ │ └── ListItem × 3
│ ├── FollowUpList ─── 随访管理
│ │ ├── NavBar
│ │ ├── TabFilter
│ │ └── FollowUpCard × 3
│ └── PatientList ─── 患者管理
│ ├── NavBar
│ ├── SearchBar
│ └── ListItem × 4
└── BottomTabBar (工作台页专用)
```
### 3.2 已映射组件
#### NavBar已有组件
- **推断规则**: 命中规则 #5(页面级容器)
- **Token 依赖**: `T.bg`, `T.bdL`, `T.tx`, `T.serif`
- **医生端差异**: 标题字体 serif bold 18px背景 T.bg + 底部 1px 分隔线
- **样式属性**:
```
height: 44px
background: T.bg
borderBottom: 1px solid T.bdL
fontFamily: T.serif
fontSize: 18px
fontWeight: 700
color: T.tx
```
#### ContentCard已有组件
- **推断规则**: 命中规则 #1容器 + borderRadius + boxShadow + 标题 + 正文)
- **Token 依赖**: `T.card`, `T.r`, `T.bg`, `T.tx2`, `T.tx3`
- **子组件**: StatGrid医生工作台、数据条随访卡片
- **样式属性**:
```
background: T.card
borderRadius: T.r (16px)
padding: 16px
boxShadow: 0 2px 12px rgba(0,0,0,0.04)
marginBottom: 16px
```
#### Tag / StatusTag已有组件
- **推断规则**: 命中规则 #6小型容器 + 背景色 + 短文字)
- **Token 依赖**: 动态 color/bg props
- **变体**: 类型标签(异常/随访/咨询)、紧急标签、状态标签
- **样式属性**:
```
display: inline-block
padding: 2px 8px
borderRadius: 6px
fontSize: 11px (or dynamic)
fontWeight: 600
lineHeight: 1.6
```
#### ListItem已有组件
- **推断规则**: 命中规则 #7列表行 + 左侧区 + 双行文字 + 右侧箭头)
- **Token 依赖**: `T.card`, `T.r`, `T.tx`, `T.tx2`, `T.tx3`, `T.pri`
- **变体**:
- ActionInbox: Tag + 患者名 + 描述 + 时间 + 箭头
- ConsultList: AvatarCircle + 患者名 + 消息 + 未读角标 + 箭头
- PatientList: AvatarCircle + 姓名 + 诊断 + 最近日期 + 箭头
- **样式属性**:
```
background: T.card
borderRadius: T.r (16px)
padding: 14px 16px
marginBottom: 10px
boxShadow: 0 1px 8px rgba(0,0,0,0.03)
display: flex; align-items: center; gap: 12px
```
#### PageShell已有组件
- **推断规则**: 命中规则 #5最外层容器 + padding + 可滚动)
- **Token 依赖**: `T.bg`
- **样式属性**:
```
height: 100%
background: T.bg
display: flex; flexDirection: column
```
#### BottomTabBar已有组件需 doctor-mode 变体)
- **推断规则**: 页面底部固定
- **Token 依赖**: `T.card`, `T.bdL`, `T.pri`, `T.tx3`
- **Tab 项**: 工作台、患者、消息、我的
- **样式属性**:
```
position: absolute; bottom: 0; left: 0; right: 0
height: 70px
background: T.card
borderTop: 1px solid T.bdL
paddingTop: 8px; paddingBottom: 28px
```
- **激活态**: 颜色 T.pri靛蓝、fontWeight: 600
- **非激活**: 颜色 T.tx3、fontWeight: 400
### 3.3 需新建/扩展组件
#### TabFilter筛选标签 pill
- **推断规则**: 未命中(特殊 pill 形筛选栏)
- **视觉特征**: 横向排列的圆角标签,激活态实色填充(白字),非激活态白底+边框
- **出现位置**: ActionInbox全部/异常/随访/咨询、FollowUpList待随访/已完成/已过期)
- **Token 依赖**: `T.pri`, `T.card`, `T.bd`, `T.tx2`
- **样式属性**:
```
padding: 6px 16px
borderRadius: 20px (pill)
fontSize: 13px
// 激活态: background: T.pri, color: #fff, border: T.pri
// 非激活: background: T.card, color: T.tx2, border: T.bd
cursor: pointer
```
#### StatusTabs状态切换标签栏
- **推断规则**: 未命中(全宽等分标签栏,底部指示线)
- **视觉特征**: 等宽标签,激活态底部 2.5px 主色线,非激活底部 1.5px 浅色线
- **出现位置**: ConsultList进行中/已结束)
- **Token 依赖**: `T.pri`, `T.bdL`, `T.tx3`
- **样式属性**:
```
flex: 1; textAlign: center
padding: 10px 0
fontSize: 14px
// 激活: color: T.pri, borderBottom: 2.5px solid T.pri, fontWeight: 600
// 非激活: color: T.tx3, borderBottom: 1.5px solid T.bdL
```
#### ShortcutButton快捷操作按钮
- **推断规则**: 未命中(圆形图标按钮 + 文字标签)
- **视觉特征**: 52×52 圆形容器(彩色浅底)+ SVG 图标 + 下方文字标签
- **出现位置**: DoctorHome4 个:患者管理/在线咨询/随访管理/透析管理)
- **Token 依赖**: `T.priL`, `T.accL`, `T.wrnL`, `T.danL`, `T.pri`, `T.acc`, `T.wrn`, `T.dan`
- **样式属性**:
```
// 圆形容器
width: 52px; height: 52px
borderRadius: 26px (50%)
background: 动态浅色
display: flex; alignItems: center; justifyContent: center
// 文字标签
fontSize: 12px; color: T.tx2
gap: 8px
```
#### TodoAlert待办提醒卡片
- **推断规则**: 命中规则 #2 的扩展(左侧竖线 + 图标 + 双行文字)
- **视觉特征**: 左侧 4px 彩色竖线 + SVG 图标 + 标题 + 副标题,浅色背景
- **出现位置**: DoctorHome2 个:血压异常/随访报告)
- **Token 依赖**: `T.priL`, `T.pri`, `T.wrnL`, `T.wrn`, `T.tx`, `T.tx3`
- **样式属性**:
```
background: T.priL (or T.wrnL)
borderRadius: T.r (16px)
padding: 14px 16px
borderLeft: 4px solid T.pri (or T.wrn)
display: flex; alignItems: center; gap: 10px
```
#### SearchBar搜索栏
- **推断规则**: 未命中(带搜索图标的输入框容器)
- **视觉特征**: 圆角白底容器 + 搜索图标 + placeholder 文字
- **出现位置**: PatientList
- **Token 依赖**: `T.card`, `T.bd`, `T.tx3`
- **样式属性**:
```
background: T.card
borderRadius: 12px
height: 42px
padding: 0 14px
gap: 10px
border: 1px solid T.bd
```
#### AvatarCircle文字头像圆形
- **推断规则**: 未命中(圆形 + 单字 + 衬线字体)
- **视觉特征**: 正圆形容器彩色浅底居中显示姓氏首字serif bold
- **出现位置**: ConsultList44px、PatientList46px
- **Token 依赖**: `T.priL`, `T.accL`, `T.wrnL`, `T.danL`, `T.pri`, `T.serif`
- **样式属性**:
```
width: 44px (or 46px); height: 同
borderRadius: 50%
background: 动态浅色
fontFamily: T.serif
fontSize: 18px (or 20px)
fontWeight: 700
color: T.pri
```
---
## 4. 交互行为
| # | 元素 | 事件类型 | 行为描述 | 实现建议 |
|---|------|---------|---------|---------|
| 1 | TabFilter 标签 | click | 点击切换筛选类别,激活态视觉变化 | `useState` 管理激活索引,切换时重新过滤列表 |
| 2 | StatusTabs 标签 | click | 点击切换「进行中/已结束」状态 | `useState` 管理激活索引,切换时重新加载列表 |
| 3 | 列表卡片 | click | 点击跳转到详情页 | 每个 ListItem 包裹 `navigator`,传递患者/咨询 ID |
| 4 | 快捷操作按钮 | click | 点击跳转到对应功能页 | ShortcutButton 绑定 `Taro.navigateTo` |
| 5 | BottomTabBar | click | 切换 Tab 页 | 使用 `Taro.switchTab` |
| 6 | 搜索栏 | input | 输入关键词实时搜索患者 | 防抖 300ms + API 调用 |
| 7 | 列表滚动 | scroll | 滚动加载更多数据 | `onScrollToLower` + 分页 API |
### 滚动容器
所有列表页的内容区域均设置了 `overflow: auto`,需要实现为 `ScrollView` 组件,支持上拉加载:
- DoctorHome: `padding: 16px 20px 80px`(底部留出 TabBar 空间)
- ActionInbox: `padding: 0 20px 20px`
- ConsultList: `padding: 12px 20px 20px`
- FollowUpList: `padding: 0 20px 20px`
- PatientList: `padding: 0 20px 20px`
---
## 5. 未匹配项
### 5.1 未匹配 Token
| 值 | 类型 | 出现位置 | 建议 Token 名称 | 备注 |
|----|------|---------|----------------|------|
| `T.serif` = `Georgia, ...` | fontFamily | 标题、数值、头像 | 已有 `$font-serif` SCSS 变量 | 直接使用 `$font-serif` |
| `T.sans` = `-apple-system, ...` | fontFamily | 正文、辅助文字 | 已有 `$font-sans` SCSS 变量 | 直接使用 `$font-sans` |
| 26px | fontSize | 工作台标题 | — | 使用 `--tk-font-h1`(28px) 或保留原值 |
| 20px | fontSize | 头像文字 | — | body-lg(18px) 或保留 |
| 15px | fontSize | 列表主文字 | — | body(16px) 更符合 Token 体系 |
| 12px | fontSize | 辅助文字、时间 | 建议新增 `--tk-font-xs: 12px` | 高频使用 |
| 14px (padding) | padding | 列表项上下 padding | — | gap-md(16px) 近似 |
### 5.2 需新建组件
| 组件名建议 | 视觉特征摘要 | 出现位置 | 优先级 |
|-----------|------------|---------|--------|
| TabFilter | 横向 pill 筛选标签,激活态实色填充 | ActionInbox, FollowUpList | 高 |
| StatusTabs | 全宽等分标签栏,底部指示线 | ConsultList | 高 |
| ShortcutButton | 圆形图标 + 文字标签4 种颜色 | DoctorHome | 中 |
| TodoAlert | 左侧竖线 + 图标 + 双行文字 | DoctorHome | 中 |
| SearchBar | 圆角白底 + 搜索图标 | PatientList | 中 |
| AvatarCircle | 圆形 + 单字衬线头像 | ConsultList, PatientList | 低(可用现有 Avatar 扩展) |
---
## 6. 医生端实现注意事项
### 6.1 .doctor-mode CSS 变量覆盖
本原型是**医生端变体**,核心差异通过 CSS 变量级联覆盖:
```scss
// 患者端默认
:root {
--tk-pri: #C4623A;
--tk-pri-l: #F0DDD4;
--tk-pri-d: #8B3E1F;
}
// 医生端覆盖
.doctor-mode {
--tk-pri: #3A6B8C;
--tk-pri-l: #D4E5F0;
--tk-pri-d: #2A4F6A;
}
```
所有使用 `--tk-pri*` 的组件在 `.doctor-mode` 下自动切换为靛蓝色系,无需单独处理。
### 6.2 页面路由规划
| 页面 | 路径建议 | TabBar |
|------|---------|--------|
| 医生工作台 | `pages/doctor/index` | 工作台 Tab |
| 患者管理 | `pages/doctor/patients` | 患者 Tab |
| 消息/咨询列表 | `pages/doctor/consultations` | 消息 Tab |
| 我的 | `pages/doctor/profile` | 我的 Tab |
| 待办收件箱 | `pages/doctor/inbox` | 非 Tab从工作台跳转 |
| 随访管理 | `pages/doctor/followups` | 非 Tab从快捷入口跳转 |
### 6.3 TabBar 配置
医生端使用独立的 4 Tab 布局:
```typescript
const doctorTabs = [
{ pagePath: 'pages/doctor/index', text: '工作台', icon: 'grid' },
{ pagePath: 'pages/doctor/patients', text: '患者', icon: 'users' },
{ pagePath: 'pages/doctor/consultations', text: '消息', icon: 'message' },
{ pagePath: 'pages/doctor/profile', text: '我的', icon: 'user' },
];
```
### 6.4 数据接口
| 页面 | 需要的 API | 方法 |
|------|-----------|------|
| 医生工作台 | `GET /api/v1/health/doctor/stats` | 统计概览 |
| 医生工作台 | `GET /api/v1/health/doctor/todos` | 待办列表 |
| 待办收件箱 | `GET /api/v1/health/doctor/todos?type={type}` | 筛选待办 |
| 在线咨询 | `GET /api/v1/health/consultations?status=active` | 咨询列表 |
| 随访管理 | `GET /api/v1/health/followups?status={status}` | 随访列表 |
| 患者管理 | `GET /api/v1/health/patients?search={keyword}` | 患者搜索 |
### 6.5 长者模式适配
所有页面需适配长者模式(字号 ≥ 22px。`.elder-mode` CSS 变量覆盖已定义在 tokens.yml 中,实现时注意:
- Tag 字号 elder: 13px → 17px
- 微调间距 elder 值
- TabFilter pill 需同步增大触控区域
### 6.6 与患者端组件复用
以下组件可直接从患者端复用(已实现 .doctor-mode 覆盖):
- `NavBar`
- `ContentCard`
- `Tag`
- `BottomTabBar`(需配置医生端 Tab 项)
需要新建的医生端专用组件:
- `TabFilter` — 患者端无对应(患者端用横向滚动分类)
- `StatusTabs` — 患者端无对应
- `ShortcutButton` — 患者端无对应
- `TodoAlert` — 患者端无对应

View File

@@ -0,0 +1,45 @@
{
"source": "mp-11-doctor-core.html",
"generatedAt": "2026-05-18T00:16:00+08:00",
"matched": {
"T.pri": { "method": "alias", "confidence": "confirmed", "token": "--tk-pri", "prototypeValue": "#3A6B8C", "tokenValue": "#C4623A", "note": "医生端 .doctor-mode 覆盖为靛蓝 #3A6B8C" },
"T.priL": { "method": "alias", "confidence": "confirmed", "token": "--tk-pri-l", "prototypeValue": "#D4E5F0", "tokenValue": "#F0DDD4", "note": "医生端覆盖" },
"T.priD": { "method": "alias", "confidence": "confirmed", "token": "--tk-pri-d", "prototypeValue": "#2A4F6A", "tokenValue": "#8B3E1F", "note": "医生端覆盖" },
"T.bg": { "method": "alias", "confidence": "pending", "token": null, "scssVar": "$bg", "prototypeValue": "#F5F0EB", "note": "tokens.scss 未声明为 CSS 变量" },
"T.card": { "method": "alias", "confidence": "confirmed", "token": "--tk-card-bg", "prototypeValue": "#FFFFFF", "tokenValue": "#FFFFFF" },
"T.surface": { "method": "alias", "confidence": "approximate", "token": "--tk-card-bg", "prototypeValue": "#EDE8E2", "tokenValue": "#FFFFFF" },
"T.tx": { "method": "alias", "confidence": "pending", "token": null, "scssVar": "$tx", "prototypeValue": "#2D2A26" },
"T.tx2": { "method": "alias", "confidence": "pending", "token": null, "scssVar": "$tx2", "prototypeValue": "#5A554F" },
"T.tx3": { "method": "alias", "confidence": "confirmed", "token": "--tk-text-secondary", "prototypeValue": "#78716C" },
"T.bd": { "method": "alias", "confidence": "pending", "token": null, "scssVar": "$bd", "prototypeValue": "#E8E2DC" },
"T.bdL": { "method": "value_exact", "confidence": "pending", "scssVar": "$bd-l", "prototypeValue": "#F0EBE5" },
"T.acc": { "method": "value_exact", "confidence": "pending", "scssVar": "$acc", "prototypeValue": "#5B7A5E" },
"T.accL": { "method": "value_exact", "confidence": "pending", "scssVar": "$acc-l", "prototypeValue": "#E8F0E8" },
"T.wrn": { "method": "value_exact", "confidence": "pending", "scssVar": "$wrn", "prototypeValue": "#C4873A" },
"T.wrnL": { "method": "value_exact", "confidence": "pending", "scssVar": "$wrn-l", "prototypeValue": "#FFF3E0" },
"T.dan": { "method": "value_exact", "confidence": "pending", "scssVar": "$dan", "prototypeValue": "#B54A4A" },
"T.danL": { "method": "value_exact", "confidence": "pending", "scssVar": "$dan-l", "prototypeValue": "#FDEAEA" },
"T.r": { "method": "alias", "confidence": "confirmed", "token": "--tk-card-radius", "prototypeValue": "16" },
"T.rSm": { "method": "alias", "confidence": "pending", "scssVar": "$r-sm", "prototypeValue": "12" },
"T.rXs": { "method": "alias", "confidence": "pending", "scssVar": "$r-xs", "prototypeValue": "8" }
},
"unmatched": ["T.serif", "T.sans"],
"inlineTokenMap": {
"fontSize:28": { "token": "--tk-font-h1", "confidence": "confirmed" },
"fontSize:26": { "token": null, "confidence": "unmatched", "note": "工作台标题 26px介于 h1(28) 和 h2(22) 之间" },
"fontSize:20": { "token": null, "confidence": "unmatched", "note": "头像文字 20px" },
"fontSize:18": { "token": "--tk-font-body-lg", "confidence": "confirmed" },
"fontSize:16": { "token": "--tk-font-body", "confidence": "confirmed" },
"fontSize:15": { "token": null, "confidence": "unmatched", "note": "列表主文字 15px介于 body(16) 和 body-sm(14) 之间" },
"fontSize:14": { "token": "--tk-font-body-sm", "confidence": "confirmed" },
"fontSize:13": { "token": "--tk-font-cap", "confidence": "confirmed" },
"fontSize:12": { "token": null, "confidence": "unmatched", "note": "辅助文字 12px无对应 Token" },
"fontSize:11": { "token": "--tk-font-micro", "confidence": "confirmed" }
},
"summary": {
"confirmed": 20,
"pending": 15,
"approximate": 1,
"unmatched": 2
}
}

View File

@@ -0,0 +1,258 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HMS 小程序 — 就诊人管理 + 建档</title>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #1a1a1a; font-family: -apple-system, 'PingFang SC', sans-serif; display: flex; flex-direction: column; align-items: center; padding: 40px 20px; gap: 24px; }
.page-title { color: #999; font-size: 13px; letter-spacing: 0.15em; text-transform: uppercase; }
.note { color: #666; font-size: 12px; max-width: 900px; text-align: center; line-height: 1.8; }
.screens { display: flex; gap: 36px; flex-wrap: wrap; justify-content: center; align-items: flex-start; }
.screen-wrap { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.screen-label { color: #888; font-size: 12px; font-style: italic; }
::-webkit-scrollbar { width: 0; height: 0; }
</style>
</head>
<body>
<div class="page-title">HMS 小程序 · 就诊人管理 + 建档</div>
<div class="note">温润东方风设计系统 — 就诊人列表(切换/编辑)+ 新建就诊人表单,两屏并排。表单即建档,建档后解锁积分商城等功能。</div>
<div id="root"></div>
<script type="text/babel">
const iosFrameStyles = {
wrapper: { display: 'inline-block', padding: 12, background: '#000', borderRadius: 60, boxShadow: '0 0 0 2px #1f2937, 0 20px 60px rgba(0,0,0,0.3)', position: 'relative' },
screen: { position: 'relative', borderRadius: 48, overflow: 'hidden', background: '#fff' },
statusBar: { position: 'absolute', top: 0, left: 0, right: 0, height: 54, display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 32px', fontSize: 16, fontWeight: 600, fontFamily: '-apple-system, "SF Pro Text", sans-serif', zIndex: 20, pointerEvents: 'none' },
dynamicIsland: { position: 'absolute', top: 12, left: '50%', transform: 'translateX(-50%)', width: 124, height: 36, background: '#000', borderRadius: 999, zIndex: 30 },
content: { position: 'absolute', top: 54, left: 0, right: 0, bottom: 34, overflow: 'auto' },
homeIndicator: { position: 'absolute', bottom: 10, left: '50%', transform: 'translateX(-50%)', width: 140, height: 5, background: 'rgba(0,0,0,0.3)', borderRadius: 999, zIndex: 10 },
};
function IosFrame({ children, width = 370, height = 800, time = '9:41', battery = 85 }) {
return (
<div style={iosFrameStyles.wrapper}>
<div style={{ ...iosFrameStyles.screen, width, height }}>
<div style={{ ...iosFrameStyles.statusBar, color: '#000' }}>
<span>{time}</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<svg width="16" height="12" viewBox="0 0 16 12" fill="none"><path d="M8 11.5a1 1 0 100-2 1 0 000 2z" fill="#000"/><path d="M3 7.5a7 7 0 0110 0" stroke="#000" strokeWidth="1.3" fill="none" strokeLinecap="round"/><path d="M1 4.5a11 11 0 0114 0" stroke="#000" strokeWidth="1.3" fill="none" strokeLinecap="round" opacity="0.7"/></svg>
<div style={{ width: 26, height: 12, border: '1.5px solid #000', borderRadius: 3, padding: 1, position: 'relative' }}>
<div style={{ width: `${battery}%`, height: '100%', background: '#000', borderRadius: 1 }} />
</div>
</div>
</div>
<div style={iosFrameStyles.dynamicIsland} />
<div style={iosFrameStyles.content}>{children}</div>
<div style={iosFrameStyles.homeIndicator} />
</div>
</div>
);
}
const T = {
pri: '#C4623A', priL: '#F0DDD4', priD: '#8B3E1F',
bg: '#F5F0EB', card: '#FFFFFF', surface: '#EDE8E2',
tx: '#2D2A26', tx2: '#5A554F', tx3: '#78716C',
bd: '#E8E2DC', bdL: '#F0EBE5',
acc: '#5B7A5E', accL: '#E8F0E8',
wrn: '#C4873A', wrnL: '#FFF3E0',
dan: '#B54A4A', danL: '#FDEAEA',
serif: "Georgia, 'Times New Roman', serif",
sans: "-apple-system, 'PingFang SC', sans-serif",
r: 16, rSm: 12, rXs: 8,
};
function NavBar({ title }) {
return (
<div style={{
height: 44, display: 'flex', alignItems: 'center', justifyContent: 'center',
borderBottom: `1px solid ${T.bdL}`, position: 'relative', background: T.bg, flexShrink: 0
}}>
<svg style={{ position: 'absolute', left: 16, top: '50%', transform: 'translateY(-50%)' }} width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M15 19l-7-7 7-7" stroke={T.tx} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
<span style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.tx }}>{title}</span>
</div>
);
}
// ─── 就诊人列表 ───
function FamilyList() {
const patients = [
{ name: '张三', relation: '本人', gender: '男', birth: '1985-03-15', active: true },
{ name: '李四', relation: '配偶', gender: '女', birth: '1988-07-22', active: false },
];
const relationColors = { '本人': T.pri, '配偶': T.acc, '父母': T.wrn, '子女': '#8B7ACC', '其他': T.tx3 };
const relationBgs = { '本人': T.priL, '配偶': T.accL, '父母': T.wrnL, '子女': '#EDE8F4', '其他': T.surface };
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="就诊人管理" />
<div style={{ flex: 1, overflow: 'auto', padding: '20px 20px 100px' }}>
{/* 提示 */}
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 16, lineHeight: 1.5 }}>
完善信息后即可使用积分商城签到等功能可添加多位家庭成员
</div>
{patients.map((p, i) => (
<div key={i} style={{
background: T.card, borderRadius: T.r, padding: 16, marginBottom: 12,
boxShadow: '0 1px 4px rgba(45,42,38,0.06)',
border: p.active ? `2px solid ${T.pri}` : '2px solid transparent',
position: 'relative',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
{/* 头像 */}
<div style={{
width: 48, height: 48, borderRadius: 24,
background: `linear-gradient(135deg, ${relationBgs[p.relation] || T.surface} 0%, ${relationColors[p.relation] || T.tx3} 100%)`,
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
}}>
<span style={{ fontFamily: T.serif, fontSize: 20, fontWeight: 700, color: '#fff' }}>
{p.name.charAt(0)}
</span>
</div>
{/* 信息 */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<span style={{ fontSize: 16, fontWeight: 600, color: T.tx }}>{p.name}</span>
{p.active && (
<span style={{ fontSize: 11, padding: '2px 8px', borderRadius: 999, background: T.priL, color: T.pri, fontWeight: 600 }}>
当前
</span>
)}
</div>
<div style={{ display: 'flex', gap: 8, fontSize: 13, color: T.tx3 }}>
<span style={{ padding: '1px 6px', borderRadius: 4, background: relationBgs[p.relation] || T.surface, color: relationColors[p.relation] || T.tx3, fontWeight: 500 }}>
{p.relation}
</span>
<span>{p.gender}</span>
<span>{p.birth.slice(0, 4)}</span>
</div>
</div>
{/* 编辑 */}
<span style={{ fontSize: 14, color: T.pri, fontWeight: 500, flexShrink: 0 }}>编辑</span>
</div>
</div>
))}
{/* 添加按钮 */}
<div style={{
height: 52, borderRadius: T.r, border: `2px dashed ${T.bd}`,
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
color: T.pri, fontSize: 16, fontWeight: 600,
}}>
<span style={{ fontSize: 20 }}>+</span>
<span>添加就诊人</span>
</div>
</div>
</div>
);
}
// ─── 新建就诊人(建档) ───
function FamilyAdd() {
const formItems = [
{ label: '姓名', placeholder: '请输入真实姓名', type: 'text' },
{ label: '关系', placeholder: '本人', type: 'select' },
{ label: '性别', placeholder: '男', type: 'select' },
{ label: '出生日期', placeholder: '请选择', type: 'date' },
{ label: '手机号', placeholder: '选填,用于接收通知', type: 'text' },
{ label: '身份证号', placeholder: '选填,用于医保对接', type: 'text' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="添加就诊人" />
<div style={{ flex: 1, overflow: 'auto', padding: '20px 16px 100px' }}>
{/* 顶部提示卡片 */}
<div style={{
background: T.priL, borderRadius: T.r, padding: '14px 16px', marginBottom: 20,
borderLeft: `4px solid ${T.pri}`,
}}>
<div style={{ fontSize: 14, fontWeight: 600, color: T.pri, marginBottom: 4 }}>完善个人信息</div>
<div style={{ fontSize: 13, color: T.tx2, lineHeight: 1.6 }}>
完善信息后即可使用积分商城签到等功能请填写真实信息
</div>
</div>
{/* 表单 */}
<div style={{
background: T.card, borderRadius: T.r, overflow: 'hidden',
boxShadow: '0 1px 4px rgba(45,42,38,0.06)',
}}>
{formItems.map((item, i) => (
<div key={i} style={{
padding: '14px 16px',
borderBottom: i < formItems.length - 1 ? `1px solid ${T.bdL}` : 'none',
}}>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 8, fontWeight: 500 }}>
{item.label}
{i < 4 && <span style={{ color: T.dan, marginLeft: 2 }}>*</span>}
</div>
{item.type === 'select' || item.type === 'date' ? (
<div style={{
height: 44, background: T.bg, border: `1.5px solid ${T.bd}`, borderRadius: T.rSm,
display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 14px',
}}>
<span style={{ fontSize: 15, color: item.placeholder === '请选择' ? T.tx3 : T.tx }}>
{item.placeholder}
</span>
<span style={{ color: T.tx3, fontSize: 14 }}></span>
</div>
) : (
<div style={{
height: 44, background: T.bg, border: `1.5px solid ${T.bd}`, borderRadius: T.rSm,
display: 'flex', alignItems: 'center', padding: '0 14px',
}}>
<span style={{ fontSize: 15, color: T.tx3 }}>{item.placeholder}</span>
</div>
)}
</div>
))}
</div>
{/* 提交按钮 */}
<div style={{
height: 52, borderRadius: T.r, background: T.pri,
display: 'flex', alignItems: 'center', justifyContent: 'center',
marginTop: 24,
boxShadow: '0 4px 16px rgba(196,98,58,0.3)',
}}>
<span style={{ fontSize: 17, fontWeight: 600, color: '#fff' }}>确认建档</span>
</div>
</div>
</div>
);
}
function App() {
return (
<div className="screens">
<div className="screen-wrap">
<span className="screen-label">就诊人列表</span>
<IosFrame time="9:41" battery={85}>
<FamilyList />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">新建就诊人建档</span>
<IosFrame time="9:42" battery={84}>
<FamilyAdd />
</IosFrame>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>

View File

@@ -0,0 +1,244 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HMS 小程序 — 访客首页</title>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #1a1a1a; font-family: -apple-system, 'PingFang SC', sans-serif; display: flex; flex-direction: column; align-items: center; padding: 40px 20px; gap: 24px; }
.page-title { color: #999; font-size: 13px; letter-spacing: 0.15em; text-transform: uppercase; }
.note { color: #666; font-size: 12px; max-width: 600px; text-align: center; line-height: 1.8; }
.screens { display: flex; gap: 36px; flex-wrap: wrap; justify-content: center; align-items: flex-start; }
.screen-wrap { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.screen-label { color: #888; font-size: 12px; font-style: italic; }
</style>
</head>
<body>
<div class="page-title">HMS 小程序 · 访客首页</div>
<div class="note">温润东方风设计系统 — 未登录用户的首页。品牌 Hero + 服务特色 + 健康资讯 + 底部注册/登录引导。TabBar 仅展示首页/健康/商城/我的四个入口。</div>
<div id="root"></div>
<script type="text/babel">
const iosFrameStyles = {
wrapper: { display: 'inline-block', padding: 12, background: '#000', borderRadius: 60, boxShadow: '0 0 0 2px #1f2937, 0 20px 60px rgba(0,0,0,0.3)', position: 'relative' },
screen: { position: 'relative', borderRadius: 48, overflow: 'hidden', background: '#fff' },
statusBar: { position: 'absolute', top: 0, left: 0, right: 0, height: 54, display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 32px', fontSize: 16, fontWeight: 600, fontFamily: '-apple-system, "SF Pro Text", sans-serif', zIndex: 20, pointerEvents: 'none' },
dynamicIsland: { position: 'absolute', top: 12, left: '50%', transform: 'translateX(-50%)', width: 124, height: 36, background: '#000', borderRadius: 999, zIndex: 30 },
content: { position: 'absolute', top: 54, left: 0, right: 0, bottom: 34, overflow: 'auto' },
homeIndicator: { position: 'absolute', bottom: 10, left: '50%', transform: 'translateX(-50%)', width: 140, height: 5, background: 'rgba(0,0,0,0.3)', borderRadius: 999, zIndex: 10 },
};
function IosFrame({ children, width = 370, height = 800, time = '9:41', battery = 85, darkStatus = false }) {
const statusColor = darkStatus ? '#fff' : '#000';
return (
<div style={iosFrameStyles.wrapper}>
<div style={{ ...iosFrameStyles.screen, width, height }}>
<div style={{ ...iosFrameStyles.statusBar, color: statusColor }}>
<span>{time}</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<svg width="16" height="12" viewBox="0 0 16 12" fill="none"><path d="M8 11.5a1 1 0 100-2 1 0 000 2z" fill={statusColor}/><path d="M3 7.5a7 7 0 0110 0" stroke={statusColor} strokeWidth="1.3" fill="none" strokeLinecap="round"/><path d="M1 4.5a11 11 0 0114 0" stroke={statusColor} strokeWidth="1.3" fill="none" strokeLinecap="round" opacity="0.7"/></svg>
<div style={{ width: 26, height: 12, border: `1.5px solid ${statusColor}`, borderRadius: 3, padding: 1, position: 'relative' }}>
<div style={{ width: `${battery}%`, height: '100%', background: statusColor, borderRadius: 1 }} />
</div>
</div>
</div>
<div style={iosFrameStyles.dynamicIsland} />
<div style={iosFrameStyles.content}>{children}</div>
<div style={iosFrameStyles.homeIndicator} />
</div>
</div>
);
}
const T = {
pri: '#C4623A', priL: '#F0DDD4', priD: '#8B3E1F',
bg: '#F5F0EB', card: '#FFFFFF', surface: '#EDE8E2',
tx: '#2D2A26', tx2: '#5A554F', tx3: '#78716C',
bd: '#E8E2DC', bdL: '#F0EBE5',
acc: '#5B7A5E', accL: '#E8F0E8',
wrn: '#C4873A', wrnL: '#FFF3E0',
dan: '#B54A4A', danL: '#FDEAEA',
serif: "Georgia, 'Times New Roman', serif",
sans: "-apple-system, 'PingFang SC', sans-serif",
r: 16, rSm: 12, rXs: 8,
};
// ─── TabBar ───
function TabBar({ active = 0 }) {
const tabs = [
{ label: '首页', icon: '⌂' },
{ label: '健康', icon: '♡' },
{ label: '商城', icon: '◉' },
{ label: '我的', icon: '◎' },
];
return (
<div style={{
position: 'absolute', bottom: 0, left: 0, right: 0,
background: T.card, borderTop: `1px solid ${T.bdL}`,
display: 'flex', height: 80, paddingBottom: 20, zIndex: 10,
}}>
{tabs.map((tab, i) => (
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 2 }}>
<span style={{ fontSize: 20, color: i === active ? T.pri : T.tx3, lineHeight: 1 }}>{tab.icon}</span>
<span style={{ fontSize: 10, color: i === active ? T.pri : T.tx3, fontWeight: i === active ? 600 : 400 }}>{tab.label}</span>
</div>
))}
</div>
);
}
// ─── 访客首页 ───
function GuestHome() {
const articles = [
{ title: '高血压患者的日常管理指南', date: '2026-05-15' },
{ title: '如何科学控制血糖水平', date: '2026-05-12' },
{ title: '透析患者的饮食注意事项', date: '2026-05-08' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column', position: 'relative' }}>
<div style={{ flex: 1, overflow: 'auto', paddingBottom: 80 }}>
{/* ── 轮播图(模拟 Swiper── */}
<div style={{ position: 'relative', height: 200, overflow: 'hidden' }}>
{/* Slide 1 */}
<div style={{
position: 'absolute', inset: 0,
background: `linear-gradient(135deg, ${T.priD} 0%, ${T.pri} 60%, ${T.priL} 100%)`,
}} />
{/* 装饰圆 */}
<div style={{ position: 'absolute', top: -20, right: -10, width: 120, height: 120, borderRadius: 60, background: 'rgba(255,255,255,0.08)' }} />
<div style={{ position: 'relative', zIndex: 1, height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '0 24px' }}>
<div style={{ fontFamily: T.serif, fontSize: 24, fontWeight: 700, color: '#fff', marginBottom: 6 }}>
专业健康管理
</div>
<div style={{ fontSize: 14, color: 'rgba(255,255,255,0.8)' }}>
AI 驱动个性化健康方案
</div>
</div>
{/* 分页指示器 */}
<div style={{ position: 'absolute', bottom: 12, left: '50%', transform: 'translateX(-50%)', display: 'flex', gap: 6 }}>
<div style={{ width: 18, height: 4, borderRadius: 2, background: '#fff' }} />
<div style={{ width: 6, height: 4, borderRadius: 2, background: 'rgba(255,255,255,0.4)' }} />
<div style={{ width: 6, height: 4, borderRadius: 2, background: 'rgba(255,255,255,0.4)' }} />
</div>
</div>
{/* ── 健康资讯 ── */}
<div style={{ padding: '24px 20px 0' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<div style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.tx }}>健康资讯</div>
<span style={{ fontSize: 13, color: T.tx3 }}>查看全部 </span>
</div>
{articles.map((a, i) => (
<div key={i} style={{
background: T.card, borderRadius: T.r, padding: 16, marginBottom: 10,
boxShadow: '0 1px 4px rgba(45,42,38,0.06)',
display: 'flex', alignItems: 'center', gap: 14,
}}>
<div style={{
width: 64, height: 64, borderRadius: T.rSm,
background: [T.priL, T.accL, T.wrnL][i % 3],
display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>
<span style={{ fontSize: 22, color: [T.pri, T.acc, T.wrn][i % 3] }}>
{['♥', '◇', '✦'][i % 3]}
</span>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 15, fontWeight: 600, color: T.tx, marginBottom: 4, lineHeight: 1.4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{a.title}
</div>
<div style={{ fontSize: 12, color: T.tx3 }}>{a.date}</div>
</div>
</div>
))}
</div>
{/* ── 底部注册引导 ── */}
<div style={{ padding: '24px 20px 24px' }}>
<div style={{
background: T.card, borderRadius: T.r, padding: 20,
boxShadow: '0 1px 4px rgba(45,42,38,0.06)',
textAlign: 'center',
}}>
<div style={{ fontFamily: T.serif, fontSize: 16, fontWeight: 700, color: T.tx, marginBottom: 6 }}>
加入我们
</div>
<div style={{ fontSize: 13, color: T.tx2, marginBottom: 16, lineHeight: 1.5 }}>
注册后即可使用签到积分商城等全部功能
</div>
<div style={{
height: 48, borderRadius: 24, background: T.pri,
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: `0 4px 16px rgba(196,98,58,0.3)`,
}}>
<span style={{ fontSize: 16, fontWeight: 600, color: '#fff' }}>注册 / 登录</span>
</div>
</div>
</div>
</div>
{/* TabBar */}
<TabBar active={0} />
</div>
);
}
// ─── TabBar 其他页面占位(灰色提示) ───
function PlaceholderTab({ title, icon }) {
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column', position: 'relative' }}>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12, padding: 20 }}>
<div style={{ width: 64, height: 64, borderRadius: 32, background: T.surface, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ fontSize: 28, color: T.tx3 }}>{icon}</span>
</div>
<div style={{ fontSize: 16, fontWeight: 600, color: T.tx2 }}>登录后查看{title}</div>
<div style={{ fontSize: 13, color: T.tx3, textAlign: 'center', lineHeight: 1.5 }}>
请先登录以使用完整服务
</div>
<div style={{
marginTop: 8, height: 44, padding: '0 32px',
background: T.pri, borderRadius: 22,
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: `0 4px 16px rgba(196,98,58,0.3)`,
}}>
<span style={{ fontSize: 15, fontWeight: 600, color: '#fff' }}>去登录</span>
</div>
</div>
<TabBar active={1} />
</div>
);
}
function App() {
return (
<div className="screens">
<div className="screen-wrap">
<span className="screen-label">访客首页</span>
<IosFrame time="9:41" battery={85} darkStatus={true}>
<GuestHome />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">未登录 Tab 占位</span>
<IosFrame time="9:42" battery={84}>
<PlaceholderTab title="健康数据" icon="♡" />
</IosFrame>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>

View File

@@ -0,0 +1,992 @@
# design-handoff Skill 设计规格
> 日期: 2026-05-17 | 状态: 草案
> 问题: HTML 原型到实际实现的翻译层缺失,新会话 LLM 无法高保真还原原型设计
> 方案: 结构化交付包(截图 + SPEC.md + Token 映射)
## 目录
1. **背景与问题** — 现状分析、核心矛盾、解决目标
2. **Skill 架构** — 触发方式、输入输出、核心流程
3. **SPEC.md 核心格式** — 页面结构、组件映射、交互规格的文档模板
4. **Token 映射系统** — 混合匹配算法、配置文件、产出格式
5. **截图提取与交互推断** — Playwright 截图、交互规则引擎
6. **多平台支持与集成** — 多平台映射、huashu-design 协作、Prompt 模板
---
## 1. 背景与问题
### 1.1 现状
项目使用 `huashu-design` skill 产出 HTML 原型稿React inline styles + 设计 Token 对象 `T`)。目前已有 **20 个原型文件**`docs/design/mp-*.html`),覆盖小程序访客端、患者端、医生端全部页面。
实施时的工作流是:
1. **会话 A**:调用 huashu-design → 选定设计风格 → 扩展到所有页面原型
2. **会话 B**(新会话):拿 HTML 原型实现 Taro/React 组件代码
### 1.2 核心矛盾
会话 B 的 LLM 与会话 A 完全隔离,丢失了所有设计上下文。三层翻译丢失:
| 丢失层 | 原因 | 表现 |
|--------|------|------|
| **视觉丢失** | LLM 读 HTML 源码,看不到渲染结果 | 颜色、字号、间距与原型不一致 |
| **Token 断裂** | 原型用 `#C4623A`,代码用 `var(--tk-pri)` | 映射关系丢失,硬编码泛滥 |
| **组件错配** | 原型全是 `<div style={...}>`,代码要用组件库 | 应复用 ContentCard/PageShell 但没复用 |
| **交互缺失** | 原型暗示的按压反馈、滑动、状态切换无文档化 | 交互行为被忽略 |
### 1.3 解决目标
创建 `design-handoff` skill在 HTML 原型完成后自动产出**结构化交付包**,让新会话的 LLM 能:
- **看到**原型渲染效果(截图)
- **知道**每个值对应哪个项目 Token映射表
- **选用**正确的现有组件(组件映射)
- **实现**原型暗示的交互(交互规格)
---
## 2. Skill 架构
### 2.1 触发方式
```bash
# 单个原型
/design-handoff docs/design/mp-00-visitor.html
# 批量处理
/design-handoff docs/design/mp-*.html
# 指定平台
/design-handoff docs/design/mp-00-visitor.html --platform web
```
**触发词**`design-handoff``设计交付``handoff`
### 2.2 输入
| 参数 | 必需 | 默认值 | 说明 |
|------|------|--------|------|
| HTML 文件路径 | 是 | — | 原型文件路径,支持 glob |
| `--platform` | 否 | `miniprogram` | 目标平台miniprogram / web / h5 |
| `--tokens` | 否 | `.design/tokens.yml` | Token 映射配置文件路径 |
### 2.3 输出目录结构
```
docs/design/
├── mp-00-visitor.html # 原始原型(不修改)
├── mp-00-visitor/ # 交付包目录
│ ├── SPEC.md # 主规格文件LLM 消费入口)
│ ├── screenshots/ # 截图目录
│ │ ├── home.png
│ │ ├── home-slide-1.png
│ │ ├── home-slide-2.png
│ │ ├── home-slide-3.png
│ │ └── profile.png
│ ├── tokens.json # Token 映射表(机器可校验)
│ └── META.yml # 元数据
├── mp-01-login/
│ └── ...
└── .design/ # 全局配置(项目级)
└── tokens.yml # Token 映射配置
```
### 2.4 核心流程
```
输入: HTML 原型文件
├─ Step 1: 解析 HTML ──────────────────────┐
│ 提取 T 对象Token 引用) │
│ 提取组件树(页面结构) │
│ 识别 IosFrame 实例(截图目标) │
│ │
├─ Step 2: 截图提取 ───────────────────────┤
│ Playwright 打开 HTML │
│ 逐 IosFrame 截图 │
│ 裁剪设备框,保留屏幕内容 │
│ │
├─ Step 3: Token 匹配 ─────────────────────┤
│ 优先级 1: 别名直查aliases
│ 优先级 2: 值精确匹配 │
│ 优先级 3: 色彩模糊匹配ΔE < 3
│ 产出 tokens.json │
│ │
├─ Step 4: 组件映射 ───────────────────────┤
│ 原型元素 → 项目组件库匹配 │
│ 标记需新建的组件 │
│ │
├─ Step 5: 交互推断 ───────────────────────┤
│ DOM 模式 → 交互规则匹配 │
│ 高/中/低置信度分级 │
│ │
└─ Step 6: 组装 SPEC.md ───────────────────┘
截图 + 结构 + Token + 组件 + 交互
→ 单一规格文档
```
### 2.5 Skill 文件清单
```
.claude/skills/design-handoff/
├── SKILL.md # Skill 入口
├── scripts/
│ ├── extract-screenshots.mjs # Playwright 截图
│ ├── parse-prototype.mjs # HTML 解析T 对象 + 组件树)
│ ├── match-tokens.mjs # Token 三层匹配
│ └── infer-interactions.mjs # 交互推断规则引擎
├── templates/
│ └── spec-template.md # SPEC.md 模板
├── defaults/
│ └── tokens.yml # 初始 Token 配置
└── rules/
└── interaction-rules.yml # 交互推断规则表
```
**前置依赖**
- Node.js ≥ 18运行 .mjs 脚本)
- Playwright截图提取huashu-design 已依赖,无需额外安装)
- 项目需包含 `styles/tokens.scss` 或等效的 Token 定义文件
---
## 3. SPEC.md 核心格式
SPEC.md 是 LLM 在新会话里消费的**唯一入口文件**。设计原则:
- **自包含**:截图通过 markdown 图片引用内嵌,一个文件看全貌
- **结构化**六个固定章节LLM 可按需定位
- **可操作**:每个值都映射到项目 Token 或组件,不含模糊描述
### 3.1 文档结构(六个固定章节)
```markdown
# {页面名称} 设计规格
> 来源: {原型文件名} | 平台: {platform} | 页面数: {n}
## 页面索引
(截图缩略图 + 路由映射表)
## 一、Token 映射
(原型值 → 项目 Token含匹配状态标记
## 二、页面结构
(逐页:截图 + 布局层级描述 + 尺寸标注)
## 三、组件映射
(原型元素 → 项目组件库组件,含来源路径)
## 四、交互规格
(元素 × 交互类型 × 触发 × 反馈 × 备注)
## 五、状态变体
(加载中 / 空数据 / 错误 / 已登录等边界状态)
## 六、样式清单
(间距 / 字号 / 圆角 / 阴影的 Token 汇总)
```
### 3.2 页面索引
用表格关联截图与路由LLM 一眼定位:
```markdown
## 页面索引
| 页面 | 截图 | 路由 |
|------|------|------|
| 访客首页 | ![home](./screenshots/home.png) | pages/index/index |
| 访客"我的" | ![profile](./screenshots/profile.png) | pages/profile/index |
```
### 3.3 Token 映射章节
三级状态标记LLM 知道哪些可以直接用:
| 标记 | 含义 | LLM 行为 |
|------|------|---------|
| ✅ confirmed | 已确认映射 | 直接使用对应 Token |
| ⚠️ pending | 模糊匹配,待确认 | 使用但需人工复核 |
| ❌ unmatched | 无匹配 | 硬编码或新建 Token |
```markdown
## 一、Token 映射
| 原型值 | 项目 Token | 状态 |
|--------|-----------|------|
| #C4623A (T.pri) | var(--tk-pri) | ✅ |
| #F5F0EB (T.bg) | var(--tk-bg-base) | ✅ |
| 32px (轮播标题) | var(--tk-font-h1) | ⚠️ 待确认28→32 不匹配) |
| serif 字体 | — | ❌ 无 Token硬编码 font-family |
```
### 3.4 页面结构章节
每个页面一个子节,**以截图开头**,紧跟布局层级:
```markdown
### 2.1 访客首页
![首页全貌](./screenshots/home.png)
布局层级(从上到下):
1. **轮播区域** (height: 280px)
- 容器: `<Swiper>` (Taro)autoplay + circular
- 3 张品牌轮播:渐变背景 + 装饰圆 + 标题文案 + 指示点
- 指示点: 活跃 24×4 白色,非活跃 8×4 半透明白
2. **健康资讯** (padding: 20px → var(--tk-gap-lg))
- 标题行: SectionTitleserif 18px bold + "更多›"
- 文章卡片 ×2: ContentCard variant="default"
- 左侧配图 110px 宽,右侧标题 15px + 摘要 13px
3. **登录引导** (padding: 28px 20px 40px)
- CTA: PrimaryButton size="large",阴影 var(--tk-shadow-btn)
```
每个元素标注:**用什么组件** + **尺寸/间距的 Token 引用** + **视觉特征**
### 3.5 组件映射章节
表格形式,明确"用哪个组件"
```markdown
## 三、组件映射
| 原型元素 | 推荐组件 | 来源 | 备注 |
|----------|---------|------|------|
| 文章卡片 | ContentCard | @components/ui/ContentCard | variant="default" |
| 区块标题 | SectionTitle | @components/ui/SectionTitle | action="更多›" |
| CTA 按钮 | PrimaryButton | @components/ui/PrimaryButton | size="large" |
| 页面容器 | PageShell | @components/ui/PageShell | padding="md" safeBottom |
| 功能卡片 | — | 需新建 | 3 列等宽圆形图标卡片 |
| 轮播 | Swiper | @tarojs/components | autoplay circular |
| TabBar | Taro TabBar | 框架配置 | 原型不实现 |
```
来源列使用 `@components/` 前缀LLM 可直接 `import`。"需新建"标记让 LLM 知道这个需要从头写。
#### 组件映射推断规则H4 修复)
组件映射通过 DOM 结构特征推断,规则定义在 skill 中:
| 原型 DOM 特征 | 推断组件 | 匹配依据 |
|--------------|---------|---------|
| 容器 + borderRadius + boxShadow + 内含标题+正文 | ContentCard | 卡片结构 |
| 容器 + 左侧竖线装饰 + 标题文本 + 可选右侧链接 | SectionTitle | 区块标题结构 |
| 按钮 + 主色背景 + 白色文字 + 高度 44-56px | PrimaryButton | CTA 按钮特征 |
| 按钮 + 边框 + 透明背景 | SecondaryButton | 次按钮特征 |
| 页面最外层容器 + padding + 滚动 | PageShell | 页面壳结构 |
| 圆形小容器 + 内含数字/文字 + 配色背景 | StatusTag | 标签/徽章 |
| 列表行 + 左图标 + 中间双行文字 + 右箭头 | ListItem | 列表项结构 |
| 未匹配上述任何规则 | — (需新建) | 标记为"需新建" |
推断失败时默认标记为"需新建",不强制匹配错误组件。
### 3.6 交互规格章节
五列表格,覆盖触发 → 反馈 → 生命周期:
```markdown
## 四、交互规格
| 元素 | 交互 | 触发 | 反馈 | 备注 |
|------|------|------|------|------|
| 轮播 | 自动播放+滑动 | 页面显示 | 3s 切换 | useDidShow 启动useDidHide 暂停 |
| 文章卡片 | 点击跳转 | onPress | activeFeedback="bg" | 跳转文章详情 |
| CTA 按钮 | 点击登录 | onPress | loading 态 | 跳转登录页 |
| 功能卡片 | 点击跳转 | onPress | opacity 反馈 | 各卡跳不同服务页 |
| 资讯区域 | 下拉刷新 | onPullDownRefresh | — | 无数据时显示功能引导 |
```
### 3.7 状态变体章节
覆盖边界场景,避免实现时遗漏:
```markdown
## 五、状态变体
- **无数据**: 资讯为空 → 显示功能引导卡片3 个固定入口)
- **加载中**: LoadingCard layout="card" count=2
- **已登录**: 自动跳转 HomeDashboard不由本页处理
- **网络错误**: ErrorState + onRetry 重新加载
```
### 3.8 样式清单章节
Token 维度的汇总,便于 LLM 快速查表:
```markdown
## 六、样式清单
间距: 页面 padding 20px (--tk-gap-lg), 卡片 padding 16px (--tk-gap-md)
字号: 标题 18px (--tk-font-nav), 正文 15px (硬编码), 摘要 13px (--tk-font-cap)
圆角: 卡片 16px (--tk-card-radius), 按钮 14px (--tk-radius-lg)
阴影: 卡片 (--tk-shadow-sm), 按钮 (--tk-shadow-btn)
```
---
## 4. Token 映射系统
### 4.1 全局配置文件
项目级配置 `.design/tokens.yml`,所有原型共享。**首次运行自动生成**,从项目代码库扫描:
- `styles/tokens.scss` → CSS 运行时 Token`--tk-*`
- `styles/variables.scss` → SCSS 编译时变量(`$pri`, `$r` 等)
生成后可手动微调,后续运行直接读取。
配置文件结构分为五个 Token 类别 + 一个别名映射区:
```yaml
# .design/tokens.yml
version: 1
updated: 2026-05-17
# 色彩 Token — 从 tokens.scss 中 --tk-pri 等提取
colors:
- token: --tk-pri
value: "#C4623A"
- token: --tk-pri-l
value: "#F0DDD4"
# ...
# 字号 Token — 从 tokens.scss 中 --tk-font-* 提取
typography:
- token: --tk-font-h1
value: "28px"
note: 页面标题 serif bold
# ...
# 间距 Token — 从 tokens.scss 中 --tk-gap-* 提取
spacing:
- token: --tk-gap-md
value: "16px"
# ...
# 圆角 Token — 从 tokens.scss 中 --tk-card-radius 等提取
radius:
- token: --tk-card-radius
value: "16px"
# ...
# 阴影 Token — 从 tokens.scss 中 --tk-shadow-* 提取
shadow:
- token: --tk-shadow-sm
value: "0 1px 4px rgba(45,42,38,0.04)"
# ...
# 原型 Token 别名 — 手动或自动积累的映射
aliases:
prototype_keys:
T.pri: --tk-pri
T.priL: --tk-pri-l
T.bg: --tk-bg-base
T.card: --tk-bg-card
T.r: --tk-card-radius
T.rSm: --tk-radius-sm
# ...
```
### 4.2 三层匹配算法
从原型 HTML 中提取的每个 Token 引用,按以下优先级匹配:
```
优先级 1: 别名直查
原型中 T.pri → 查 aliases.prototype_keys → --tk-pri ✅
预估覆盖率: 最高(已有 aliases 的常用 Token
特点: 零歧义,直接确认
优先级 2: 值精确匹配(按 CSS 属性上下文消歧)
原型中 #C4623A → 仅在 colors 类别中查找 → --tk-pri ✅
原型中 borderRadius: 16px → 仅在 radius 类别中查找 → --tk-card-radius ✅
原型中 padding: 16px → 仅在 spacing 类别中查找 → --tk-gap-md ✅
预估覆盖率: 中等(值恰好一致的 Token
特点: 按 CSS 属性类别过滤,避免同值多 Token 歧义
优先级 3: 色彩模糊匹配(仅颜色类)
原型中 #C4623B → 与 colors 中各值算色差 ΔE
ΔE < 3 → 视为近似色 → ⚠️ pending 待确认
预估覆盖率: 较低(设计迭代中微调的色值)
特点: 仅限色彩,必须人工确认
未匹配: ❌ unmatched
无任何匹配结果
需人工决定: 新建 Token / 硬编码 / 忽略
```
**值归一化规则M4**
- `"16px"``"16"` 视为相同(去除 px 后缀比较数值)
- `"#C4623A"``"c4623a"` 视为相同(大小写不敏感)
- 不支持 `rem`/`em` 转换,遇此单位标记为 pending
### 4.3 首次运行流程
```
/design-handoff docs/design/mp-00-visitor.html
Step 0: 格式校验C3 弹性检查)
- 检查文件是否为合法 HTML
- 检查是否包含 React/Babel 脚本标签huashu-design 特征)
- 检查是否包含 T 对象定义const T = { ... }
- 任何检查失败 → 输出清晰错误信息并终止
- 缺少 T 对象: "未检测到设计 Token 对象 (T),请确认是否为 huashu-design 产物"
- 缺少 React: "未检测到 React 渲染环境,无法解析原型"
Step 1: 检查 .design/tokens.yml
→ 不存在: 自动扫描项目 SCSS 文件生成
- 解析 tokens.scss → 提取所有 --tk-* 变量及值
- 解析 variables.scss → 提取所有 $ 变量及值
- 生成 .design/tokens.yml
→ 存在: 直接使用
Step 2: 解析 HTML 中的 TokenC2 提取方式)
方式: 静态解析 T 对象定义 + 内联 style 值
- 用正则匹配 "const T = { ... }" 提取完整对象定义
- 解析 T 对象的每个 key-value 对(如 T.pri: '#C4623A'
- 扫描内联 style 中的硬编码值: fontSize, padding, borderRadius 等
- 不依赖 React 运行时,纯文本解析
- 值归一化规则: "16px" 和 "16" 视为相同; 不支持 rem/em 转换
Step 3: 三层匹配
- 匹配时按 CSS 属性上下文消歧H1 修复)
- borderRadius / radius 相关属性 → 只匹配 radius 类别 Token
- padding / margin / gap → 只匹配 spacing 类别 Token
- fontSize / fontWeight → 只匹配 typography 类别 Token
- color / background → 匹配 colors 类别 Token
- 如果同值多 Token 且无属性上下文 → 标记为 ⚠️ pending
- 产出 tokens.json
Step 4: 处理未确认项
- pending + unmatched 项在终端列表展示
- 用户确认后更新 aliases
- 后续原型自动复用已确认的映射
```
### 4.3.1 .design/ 目录管理
- `.design/` 目录**提交到 git**tokens.yml 是团队共享的项目级配置)
- `.design/config.yml`(个人偏好配置,如 auto_handoff可加入 `.gitignore`
- 首次生成 tokens.yml 后建议立即提交,作为团队基线
### 4.4 产出文件 `tokens.json`
```json
{
"source": "mp-00-visitor.html",
"platform": "miniprogram",
"generated": "2026-05-17T10:30:00Z",
"tokens": {
"T.pri": {
"value": "#C4623A",
"mapped": "var(--tk-pri)",
"match": "alias",
"status": "confirmed"
},
"fontSize:32": {
"value": "32px",
"closest": "var(--tk-font-h1)",
"closestValue": "28px",
"match": "fuzzy",
"status": "pending",
"note": "原型 32px vs 最近 Token 28px (--tk-font-h1),差 4px"
},
"T.serif": {
"value": "Georgia, 'Times New Roman', serif",
"mapped": null,
"match": "none",
"status": "unmatched",
"note": "项目无字体族 Token需硬编码或新建"
}
},
"summary": {
"total": 18,
"confirmed": 13,
"pending": 2,
"unmatched": 3
}
}
```
**字段说明**
| 字段 | 说明 |
|------|------|
| `value` | 原型中的原始值 |
| `mapped` | 匹配到的项目 Tokennull 表示未匹配) |
| `match` | 匹配方式alias / exact / fuzzy / none |
| `status` | 状态confirmed / pending / unmatched |
| `note` | 补充说明(模糊匹配的差异原因等) |
### 4.5 映射积累效应
每次运行 `/design-handoff` 并确认 pending 项后,映射自动写入 `aliases`
```
第 1 次运行: 13 confirmed + 2 pending + 3 unmatched
→ 确认 2 个 pending → aliases 新增 2 条
第 2 次运行: 15 confirmed + 1 pending + 2 unmatched
→ 确认 1 个 pending → aliases 新增 1 条
第 N 次运行: 18 confirmed + 0 pending + 0 unmatched
→ 全部自动匹配,无需人工干预
```
随着项目推进,手动确认越来越少,最终趋近全自动。
---
## 5. 截图提取与交互推断
### 5.1 截图提取
#### 5.1.1 提取流程
使用 Playwrighthuashu-design 已内置 `verify.py` 依赖 Playwright
```
1. Playwright 启动浏览器,打开 HTML 原型文件
2. 等待 React + Babel 渲染完成(检测 #root 内有子元素)
3. 遍历所有 IosFrame 实例,逐个截图
4. 裁剪设备框12px padding + 54px 状态栏 + 34px 底部指示器)
5. 输出到 screenshots/ 目录
```
#### 5.1.2 IosFrame 定位规则
原型 HTML 中 `screen-label``IosFrame` 是**兄弟节点**,不是父子关系:
```html
<div class="screen-wrap">
<span class="screen-label">访客首页 — 完整页</span> <!-- 兄弟节点 -->
<IosFrame time="9:41" battery={85}> <!-- 兄弟节点 -->
<GuestHome />
</IosFrame>
</div>
```
定位方式:
```
1. 查找所有 class="screen-wrap" 的容器
2. 每个容器内:
- <span class="screen-label"> 的文本 → 截图文件名
- <IosFrame> 实例 → 截图目标
3. 如果容器无 screen-label → 使用序号命名 (screen-1.png, screen-2.png)
```
如果原型未使用 screen-wrap + IosFrame 结构,进入降级方案(见 5.1.4)。
#### 5.1.3 文件命名规则
| 规则 | 示例 |
|------|------|
| 使用 screen-label 文本 | `访客首页 — 完整页``home.png` |
| 轮播编号保留 | `访客首页 — 轮播 1``home-slide-1.png` |
| 中文翻译为英文简写 | `我的``profile`, `登录``login` |
| 同名页面加序号 | `home-2.png`, `home-3.png` |
翻译映射表内置在 skill 中(覆盖项目已有的 20 个原型中出现的所有中文标签)。
#### 5.1.4 降级方案
当原型未使用 IosFrame 时(如早期部分原型或 Web 端原型):
1. **整页截图**:截取完整渲染页面
2. **手动标注**:用户通过 `--crop "x,y,w,h"` 参数指定裁剪区域
3. **CSS 选择器**:通过 `--selector ".screen-content"` 指定截图目标
### 5.2 交互推断
#### 5.2.1 推断策略
从原型 HTML 的 DOM 结构推断交互行为。不做运行时分析,纯静态代码模式匹配。
推断规则存储在 `rules/interaction-rules.yml` 中,按 DOM 模式匹配。
#### 5.2.2 内置规则表
```yaml
# rules/interaction-rules.yml
rules:
- id: swiper-autoplay
name: 自动轮播 + 手动滑动
patterns:
- "连续多个子元素含渐变背景 + 底部指示点(宽窄交替)"
- "Swiper / swiper / carousel 关键词"
infer:
interaction: "自动播放 + 手动滑动"
trigger: "页面显示"
feedback: "3s 间隔自动切换"
lifecycle: "useDidShow 启动useDidHide 暂停"
confidence: high
- id: card-tap
name: 卡片点击跳转
patterns:
- "卡片容器含标题+摘要+配图"
- "onClick / onPress 事件"
infer:
interaction: "点击跳转"
trigger: "onPress"
feedback: "activeFeedback 按组件类型自动选择"
confidence: high
- id: form-submit
name: 表单提交
patterns:
- "输入框 + 按钮在同一容器内"
- "input / textarea + button 组合"
infer:
interaction: "表单提交"
trigger: "onPress按钮"
feedback: "loading 态 + 禁用重复提交 + 错误提示"
confidence: high
- id: list-scroll
name: 列表滚动加载
patterns:
- "列表容器含多个重复结构子元素"
- "overflow: auto / scroll"
- "3 个以上同类卡片连续排列"
infer:
interaction: "下拉刷新 + 上拉加载"
trigger: "onPullDownRefresh / onReachBottom"
feedback: "分页加载"
confidence: medium
- id: tab-switch
name: 标签页切换
patterns:
- "多个平行区域 + 选中态样式差异"
- "tab / segment / filter 关键词"
- "一组按钮样元素,其中一个高亮"
infer:
interaction: "标签页切换"
trigger: "onPresstab 项)"
feedback: "选中态样式切换 + 内容区切换"
confidence: high
- id: static-decoration
name: 静态装饰
patterns:
- "渐变背景 + 装饰圆/波浪/几何图形"
- "无事件绑定"
- "position: absolute + opacity < 1"
infer:
interaction: "无(纯装饰)"
trigger: "—"
feedback: "—"
confidence: high
- id: login-cta
name: 登录/注册 CTA
patterns:
- "按钮含 '登录' / '注册' / '立即' 文案"
- "主色按钮 + 阴影"
infer:
interaction: "点击触发登录流程"
trigger: "onPress"
feedback: "PrimaryButton loading 态"
guard: "GuestGuard 拦截未登录态"
confidence: high
- id: empty-fallback
name: 空数据降级
patterns:
- "条件渲染: data.length > 0 ? ... : ..."
- "占位符文字 '暂无' / '空'"
- "硬编码的固定内容(非 API 数据)"
infer:
interaction: "无数据时显示替代内容"
trigger: "数据加载完成且为空"
feedback: "EmptyState 组件或固定引导卡片"
confidence: medium
```
#### 5.2.3 置信度分级
| 级别 | 阈值 | 处理方式 |
|------|------|---------|
| **high** (≥80%) | 结构明确,模式唯一 | 直接写入 SPEC.md 交互规格表 |
| **medium** (50-80%) | 结构合理但有歧义 | 写入交互规格表,标注 `⚠️ 待确认` |
| **low** (<50%) | 模式模糊,可能是静态 | 不写入交互规格,末尾单独列出 `## 待人工补充的交互` |
#### 5.2.4 推断结果示例
`mp-00-visitor.html` 的推断输出:
| 元素 | 推断交互 | 置信度 | 来源规则 |
|------|---------|--------|---------|
| 轮播区域 | 自动播放 3s + 手动滑动 | high | swiper-autoplay |
| 文章卡片 ×2 | 点击跳转详情 | high | card-tap |
| "更多›" 链接 | 点击跳转列表页 | high | card-tap |
| 功能卡片 ×3 | 点击跳转各服务页 | high | card-tap |
| CTA"立即登录" | 点击触发登录 | high | login-cta |
| TabBar 占位 | 无(小程序原生处理) | high | static-decoration |
| 资讯为空降级 | 显示功能引导卡片 | medium | empty-fallback |
### 5.3 截图与规格的关联机制
SPEC.md 中每个页面结构块都以对应截图开头,形成视觉-规格一一对应:
```markdown
### 2.1 访客首页
![首页全貌](./screenshots/home.png) ← LLM 读到此处时看到截图
布局层级(从上到下): ← 对照截图理解每层
1. 轮播区域 (height: 280px) ... ← 截图中顶部渐变区域
2. 健康资讯 (padding: 20px) ... ← 截图中卡片列表区域
3. 登录引导 ... ← 截图中底部 CTA 按钮
```
LLM 无需在多个文件间跳转。**一个 SPEC.md = 视觉 + 结构 + 映射 + 交互的全景图**。
---
## 6. 多平台支持与集成
### 6.1 多平台 Token 映射
`tokens.yml` 中的每个 Token 支持按平台指定不同变量名和引用方式:
```yaml
# 单平台通用(大多数 Token值和引用方式相同
- token: --tk-pri
value: "#C4623A"
# 无 platforms 字段 → 所有平台统一用 var(--tk-pri)
# 多平台差异映射(值相同,引用方式不同)
- token: --tk-pri
value: "#C4623A"
platforms:
miniprogram: "var(--tk-pri)"
web: "token('color.primary')" # Ant Design Token 系统
h5: "var(--color-primary)" # H5 独立 CSS 变量体系
```
运行时按 `--platform` 参数展开对应平台的映射:
```bash
# 默认展开 miniprogram 映射
/design-handoff mp-00-visitor.html
# 展开 web 映射
/design-handoff mp-00-visitor.html --platform web
```
SPEC.md 和 tokens.json 中仅包含目标平台的映射,不混淆。
### 6.2 多平台组件映射
可选配置文件 `.design/components.yml`,定义各平台的组件对应关系:
```yaml
# .design/components.yml — 自动扫描生成,可手动调整
components:
ContentCard:
miniprogram:
import: "@components/ui/ContentCard"
props: "variant, padding, margin, activeFeedback, onPress, className"
web:
import: "@app/components/ContentCard"
props: "variant, padding, margin, onClick, className"
PrimaryButton:
miniprogram:
import: "@components/ui/PrimaryButton"
props: "children, onClick, disabled, loading, size"
web:
import: "antd/es/button"
props: "children, onClick, disabled, loading, type='primary'"
PageShell:
miniprogram:
import: "@components/ui/PageShell"
props: "padding, safeBottom, scroll, className"
web:
import: "@app/layouts/PageLayout"
props: "padding, children, className"
Swiper:
miniprogram:
import: "@tarojs/components"
tag: "Swiper"
web:
import: "antd/es/carousel"
tag: "Carousel"
TabBar:
miniprogram:
type: "framework-config"
note: "小程序原生 TabBar在 app.config.ts 配置"
web:
import: "@app/components/AppTabBar"
note: "Web 端自定义 TabBar 组件"
```
**扫描生成规则**
- `miniprogram` → 扫描 `apps/miniprogram/src/components/ui/` 目录
- `web` → 扫描 `apps/desktop/src/components/` 或对应 Web 目录
- 只在对应平台目录存在时才生成映射
### 6.3 huashu-design 协作方式
两种集成方式,**不修改 huashu-design 本身**
#### 方式 A串行调用默认
```
会话 1: /huashu-design → 原型 HTML设计阶段
会话 2: /design-handoff mp-00-visitor.html → 交付包(规格化)
会话 3: "按 SPEC.md 实现" → Taro/React 代码(实施阶段)
```
三个阶段各自独立,松耦合。用户按需调用。
#### 方式 B自动追加可选配置
`.design/config.yml` 中开启:
```yaml
# .design/config.yml
auto_handoff: true
```
开启后huashu-design 完成原型写入时skill 检测到 `auto_handoff: true`,自动提示:
```
原型已生成: docs/design/mp-00-visitor.html
检测到 auto_handoff 开启。是否自动生成设计交付包?
→ 是: 立即运行 /design-handoff
→ 否: 跳过,稍后手动调用
```
### 6.4 实施 Prompt 模板
交付包生成完毕后skill 在终端输出推荐 prompt用户直接复制到新会话
```
📋 实施时复制以下 prompt 到新会话:
请按设计规格实现「访客首页」页面。
设计规格: docs/design/mp-00-visitor/SPEC.md
Token 映射: docs/design/mp-00-visitor/tokens.json
目标平台: miniprogram (Taro + React)
组件库: apps/miniprogram/src/components/ui/
样式 Token: apps/miniprogram/src/styles/tokens.scss
要求:
1. 先阅读 SPEC.md 中的截图和规格
2. 按组件映射表使用现有组件,不要用 div 堆砌
3. 按 Token 映射表使用 CSS 变量,不要硬编码颜色值
4. 按交互规格表实现所有交互行为
5. 覆盖所有状态变体(加载/空数据/错误)
```
注意SPEC.md 中的截图路径为相对路径(`./screenshots/home.png`LLM 应使用项目根目录的绝对路径读取(如 `docs/design/mp-00-visitor/screenshots/home.png`)。
#### 批处理索引M3 修复)
批量运行时(`/design-handoff docs/design/mp-*.html`),完成后自动在 `docs/design/` 下生成 `INDEX.md`
```markdown
# 设计交付包索引
> 生成时间: 2026-05-17 | 原型数: 20
| 原型 | 交付包 | 页面数 | 未匹配 Token |
|------|--------|--------|-------------|
| mp-00-visitor | [SPEC](mp-00-visitor/SPEC.md) | 2 | 2 |
| mp-01-login | [SPEC](mp-01-login/SPEC.md) | 2 | 1 |
| ... | ... | ... | ... |
```
LLM 可通过 `docs/design/INDEX.md` 定位所需的交付包。
### 6.5 META.yml 元数据
每个交付包的元数据文件:
```yaml
# docs/design/mp-00-visitor/META.yml
source: mp-00-visitor.html
platform: miniprogram
generated: 2026-05-17T10:30:00Z
generator: design-handoff v1
pages:
- name: 访客首页
screenshot: screenshots/home.png
route: pages/index/index
- name: 访客"我的"
screenshot: screenshots/profile.png
route: pages/profile/index
tokens:
total: 18
confirmed: 13
pending: 2
unmatched: 3
components:
reused: 5 # 已有组件
new: 1 # 需新建组件
interactions:
total: 7
high_confidence: 6
medium_confidence: 1
```
---
## 附录 A. 交互推断规则表
完整内置规则清单(见 §5.2.2),共 8 条:
| 规则 ID | 名称 | 置信度 | 匹配模式 |
|---------|------|--------|---------|
| swiper-autoplay | 自动轮播 + 手动滑动 | high | 渐变背景 + 指示点宽窄交替 |
| card-tap | 卡片点击跳转 | high | 卡片容器 + 标题摘要配图 |
| form-submit | 表单提交 | high | 输入框 + 按钮同容器 |
| list-scroll | 列表滚动加载 | medium | 3+ 同类卡片连续排列 |
| tab-switch | 标签页切换 | high | 平行区域 + 选中态差异 |
| static-decoration | 静态装饰 | high | 渐变 + 装饰圆 + 无事件 |
| login-cta | 登录/注册 CTA | high | 按钮含登录文案 + 主色 |
| empty-fallback | 空数据降级 | medium | 条件渲染 / 占位符文字 |
扩展方式:在 `rules/interaction-rules.yml` 中追加新规则即可。
## 附录 B. Token 配置文件完整模板
见 §4.1 的 `.design/tokens.yml` 结构。首次运行时自动从以下文件扫描生成:
| 源文件 | 提取内容 |
|--------|---------|
| `styles/tokens.scss` | 所有 `--tk-*` CSS 变量 |
| `styles/variables.scss` | 所有 `$` SCSS 变量 |
| `styles/mixins.scss` | mixin 名称和用途(用于注释) |
## 附录 C. SPEC.md 完整示例mp-00-visitor
见 §3.2 - §3.8 的完整格式定义。以 mp-00-visitor 为实际案例,涵盖:
- 5 张截图(首页全貌 + 3 张轮播 + 我的页)
- 15+ Token 映射11 confirmed + 2 pending + 2 unmatched
- 7 个组件映射5 复用 + 1 新建 + 1 框架配置)
- 7 条交互规格6 high + 1 medium
- 4 个状态变体(加载 / 空数据 / 已登录 / 错误)