docs(mp): 新增小程序全页面 HTML 原型 + UI 优化指南

- 新增 12 个核心页面原型(登录/首页/咨询/预约/商城/健康等)
- 新增医生端分包原型(核心 + 临床两个分包)
- 新增 AI 客服对话页原型
- 新增 MP UI 优化指南文档
- 更新 wiki 基础设施和小程序文档
This commit is contained in:
iven
2026-05-17 00:51:07 +08:00
parent 710b2e2423
commit aa27c5174c
93 changed files with 15506 additions and 70 deletions

View File

@@ -0,0 +1,547 @@
# HMS 小程序 UI 优化实施指南
> 基于 `docs/design/mp-*.html` 高保真原型稿,指导全局性 UI 优化。
> 核心原则:**先建组件库,再逐页替换**,避免逐页手写导致风格不一致。
## 1. 原型稿索引
所有原型文件在 `docs/design/` 目录下,双击浏览器即可预览:
| 文件 | 涵盖页面 | 屏幕数 |
|------|---------|--------|
| `mp-00-visitor.html` | **访客端**轮播图×3 + 访客首页 + 访客"我的" | 5 |
| `mp-redesign-home.html` | 首页/健康/消息/我的4个TabBar主页面 | 4 |
| `mp-01-login.html` | 登录页(初始状态 + 填写中) | 2 |
| `mp-02-consultation.html` | 咨询列表 + 聊天详情 | 2 |
| `mp-03-appointment.html` | 预约列表 + 预约创建 + 预约详情 | 3 |
| `mp-04-article-report.html` | 文章列表 + 详情 + AI报告列表 + 详情 | 4 |
| `mp-05-mall.html` | 积分商城主页 | 1 |
| `mp-06-health-pkg.html` | 告警/日常监测/设备同步/体征录入/趋势 | 5 |
| `mp-07-profile-health.html` | 健康记录/诊断/用药/报告列表/报告详情 | 5 |
| `mp-08-profile-dialysis.html` | 透析记录列表+详情/随访列表+详情 | 4 |
| `mp-09-profile-other.html` | 家庭管理/添加/知情同意/事件/长者模式/设置 | 6 |
| `mp-10-mall-pkg.html` | 商品详情/兑换确认/订单列表/咨询详情 | 4 |
| `mp-11-doctor-core.html` | 医生工作台/待办/咨询/随访/患者 | 5 |
| `mp-12-doctor-clinical.html` | 告警列表+详情/透析列表+创建/处方列表+详情 | 6 |
**总计 14 个文件52 个屏幕,覆盖 58+ 页面(含访客端)。**
## 2. 设计 Token全局变量
从原型稿中提取的完整 Token 系统,所有页面必须严格遵循:
### 2.1 色彩体系
```scss
// === 患者端主色(暖陶土) ===
$color-primary: #C4623A;
$color-primary-light: #F0DDD4;
$color-primary-dark: #8B3E1F;
// === 医生端主色(靛蓝) ===
$color-doctor-primary: #3A6B8C;
$color-doctor-primary-light: #D4E5F0;
$color-doctor-primary-dark: #2A4F6A;
// === 中性色 ===
$color-bg: #F5F0EB; // 页面背景
$color-card: #FFFFFF; // 卡片背景
$color-surface: #EDE8E2; // 表面/次要背景
$color-border: #E8E2DC; // 边框
$color-border-light: #F0EBE5; // 浅边框
// === 文字色 ===
$color-text: #2D2A26; // 主文字
$color-text-secondary: #5A554F; // 次要文字
$color-text-tertiary: #78716C; // 辅助文字
// === 语义色 ===
$color-success: #5B7A5E; // 正常/成功
$color-success-light: #E8F0E8;
$color-warning: #C4873A; // 警告/偏高
$color-warning-light: #FFF3E0;
$color-danger: #B54A4A; // 危险/异常
$color-danger-light: #FDEAEA;
// === 功能色 ===
$color-wechat: #07C160; // 微信绿
```
### 2.2 字体系统
```scss
$font-display: "Georgia, 'Times New Roman', serif"; // 标题/数字
$font-body: "-apple-system, 'PingFang SC', sans-serif"; // 正文
// 字号规范11级
$font-size-xs: 10px; // 极小标注
$font-size-sm: 11px; // 标签/状态
$font-size-base-sm: 12px; // 辅助文字
$font-size-base: 13px; // 正文小
$font-size-body: 14px; // 正文
$font-size-md: 15px; // 小标题/按钮
$font-size-lg: 16px; // 卡片标题
$font-size-xl: 18px; // 区块标题
$font-size-2xl: 22px; // 页面子标题
$font-size-3xl: 26px; // 页面主标题
$font-size-4xl: 28px; // 大号数字(积分等)
$font-size-5xl: 30px; // 体征数值
```
### 2.3 圆角与间距
```scss
$radius-lg: 16px; // 卡片/大按钮
$radius-md: 12px; // 输入框/中等容器
$radius-sm: 8px; // 小元素/标签
$radius-full: 999px; // 胶囊标签/圆形
$spacing-xs: 4px;
$spacing-sm: 8px;
$spacing-md: 12px;
$spacing-lg: 16px;
$spacing-xl: 20px;
$spacing-2xl: 24px;
$page-padding: 20px; // 页面左右内边距
$card-padding: 16px; // 卡片内边距
$card-padding-lg: 20px; // 大卡片内边距
$section-gap: 16px; // 区块间距
$item-gap: 8px; // 列表项间距
```
### 2.4 阴影
```scss
$shadow-card: 0 2px 12px rgba(45,42,38,0.06); // 普通卡片
$shadow-card-sm: 0 1px 4px rgba(45,42,38,0.04); // 轻量卡片
$shadow-button: 0 4px 16px rgba(196,98,58,0.3); // 主按钮
$shadow-tab-active: 0 2px 8px rgba(196,98,58,0.25); // 选中Tab
```
## 3. 全局组件库(必须先建立)
> **铁律:以下组件必须先在 `src/components/ui/` 和 `src/components/patterns/` 中建好,
> 然后所有页面只引用组件,不允许在页面文件中硬编码样式。**
### 3.1 基础 UI 组件
#### `NavBar` — 子页面导航栏
```
高度: 44px
左侧: 返回箭头(‹ 字符或图标color: $color-primary
中央: 页面标题font: serif 18px boldcolor: $color-text
右侧: 可选操作按钮
底部: 1px solid $color-border-light
```
**原型参考**: 所有 mp-02~12 文件的导航栏
#### `PageHeader` — TabBar 页面标题
```
font: serif 26px/700
color: $color-text
margin-bottom: 20px
padding: 20px 20px 0
```
**原型参考**: mp-redesign-home.html 的"上午好,张三"/"健康数据"/"消息"/页面标题
#### `ContentCard` — 内容卡片容器
```
background: $color-card
border-radius: $radius-lg
padding: $card-padding-lg
box-shadow: $shadow-card
```
**原型参考**: 所有文件中的白色圆角卡片
#### `StatusTag` — 状态标签
```
fontSize: 11px
padding: 2px 8px
borderRadius: $radius-full
fontWeight: 500
变体:
- success: bg=$color-success-light, color=$color-success
- warning: bg=$color-warning-light, color=$color-warning
- danger: bg=$color-danger-light, color=$color-danger
- neutral: bg=$color-surface, color=$color-text-tertiary
- primary: bg=$color-primary-light, color=$color-primary
```
**原型参考**: 所有文件中的"正常"/"偏高"/"待就诊"/"已完成"等标签
#### `SectionTitle` — 区块标题
```
font: serif 18px/700
color: $color-text
margin-bottom: 12px
```
**原型参考**: mp-redesign-home.html 的"今日体征"/"近 7 天趋势"
#### `FormInput` — 表单输入框
```
height: 56px (血压等大号) 或 48px (普通)
background: $color-card
border: 1.5px solid $color-border
border-radius: $radius-lg
padding: 0 16px
font-size: 16px
focus 状态:
border-color: $color-primary
label:
font-size: 13px, color: $color-text-tertiary, margin-bottom: 6px
```
**原型参考**: mp-01-login.html 的手机号/验证码输入, mp-03-appointment.html 的表单
#### `PrimaryButton` — 主操作按钮
```
height: 54px (登录) 或 52px (普通)
border-radius: 14px
background: $color-primary
color: #fff
font-size: 18px (登录) 或 17px (普通)
font-weight: 600
box-shadow: $shadow-button
医生端变体:
background: $color-doctor-primary
```
**原型参考**: 所有文件的底部主按钮
#### `SecondaryButton` — 次操作按钮
```
同 PrimaryButton 尺寸
background: transparent
border: 2px solid $color-primary
color: $color-primary
```
**原型参考**: mp-redesign-home.html 的"预约挂号"按钮
### 3.2 复合组件
#### `TabFilter` — 筛选/切换标签
```
两种样式:
样式A (填充型 - 如体征类型切换):
- 选中: bg=$color-primary, color=#fff, box-shadow=$shadow-tab-active
- 未选: bg=$color-surface, color=$color-text-secondary
- 高度: 44px, borderRadius: $radius-md
样式B (pill型 - 如文章分类):
- 选中: bg=$color-primary, color=#fff
- 未选: bg=$color-surface, color=$color-text-secondary
- 高度: 32px, borderRadius: $radius-full, padding: 0 16px
样式C (段控型 - 如消息页咨询/通知):
- 外框: bg=$color-surface, borderRadius: $radius-sm, padding: 3px
- 选中: bg=$color-card, box-shadow
- 未选: transparent
- 高度: 40px
```
**原型参考**: mp-redesign-home.html HealthA 的体征Tab, MessagesA 的咨询/通知
#### `ListItem` — 列表项
```
background: $color-card
border-radius: $radius-lg
padding: 16px
box-shadow: $shadow-card-sm
margin-bottom: $item-gap
布局: 左侧头像/图标 + 中间内容(flex:1) + 右侧状态/箭头
分隔线(多个item在一张卡片内时):
border-bottom: 1px solid $color-border-light, 最后一项无
未读态: opacity 1
已读态: opacity 0.7
```
**原型参考**: mp-02-consultation.html, mp-07-profile-health.html 的列表项
#### `InfoRow` — 信息行(左标签右值)
```
display: flex, justify-content: space-between, align-items: center
padding: 12px 0
border-bottom: 1px solid $color-border-light (最后一项无)
标签: font-size: 14px, color: $color-text-secondary
值: font-size: 15px, color: $color-text
```
**原型参考**: mp-03-appointment.html 详情页, mp-12-doctor-clinical.html 处方详情
#### `ProgressRing` — 环形进度
```
尺寸: 64px (小号) / 80px (大号)
SVG: 两个 circle, 底层 $color-border, 上层 $color-primary
lineWidth: 4px
中心: 数字/文字, font: serif bold, color: $color-primary
```
**原型参考**: mp-redesign-home.html 首页"今日已记录 3/4"
#### `AlertCard` — 提醒/提示卡片
```
三种样式:
样式A (渐变型 - 智能提醒):
background: linear-gradient(135deg, $color-primary, $color-primary-dark)
color: #fff, borderRadius: $radius-lg
样式B (左边框型 - AI建议/提示):
background: $color-success-light
borderLeft: 4px solid $color-success
样式C (全边框型 - 温馨提示):
background: $color-warning-light
borderRadius: $radius-md
```
**原型参考**: mp-redesign-home.html 首页智能提醒, HealthA 的AI建议, mp-03-appointment.html 温馨提示
#### `ChatBubble` — 聊天气泡
```
医生/对方消息:
- background: $color-card
- border-radius: $radius-lg (左上 $radius-sm)
- 左对齐, maxWidth: 75%
我的消息:
- background: $color-primary-light
- border-radius: $radius-lg (右上 $radius-sm)
- 右对齐, maxWidth: 75%
```
**原型参考**: mp-02-consultation.html, mp-10-mall-pkg.html 咨询详情
#### `VitalCard` — 体征数据卡片2x2网格用
```
background: $color-card
border-radius: $radius-lg
padding: 14px 16px
布局:
- 标签名: 13px, $color-text-secondary
- 数值: serif 30px/700, $color-text
- 单位: 12px, $color-text-tertiary
- 状态标签: StatusTag
```
**原型参考**: mp-redesign-home.html 首页体征2x2网格
#### `GradientHeader` — 渐变头部卡片
```
background: linear-gradient(135deg, $color-primary 0%, $color-primary-dark 100%)
border-radius: $radius-lg
padding: 18px
color: #fff
```
**原型参考**: mp-05-mall.html 积分卡, mp-redesign-home.html 首页智能提醒
### 3.3 页面骨架组件
#### `PageShell` — 页面外壳(已存在,需更新样式)
```
使用 Token 变量更新现有 PageShell 的背景色、内边距等
```
#### `SubPageLayout` — 子页面布局
```
NavBar (固定顶部)
内容区 (scroll-view, padding: $page-padding)
可选底部操作栏 (fixed bottom)
```
## 4. 实施步骤(严格按顺序执行)
### Phase 0: 建立 SCSS 变量文件30分钟
1. 创建 `src/styles/_tokens.scss`,写入 §2 中的所有设计 Token
2. 创建 `src/styles/_mixins.scss`,封装常用样式组合:
```scss
@mixin card { background: $color-card; border-radius: $radius-lg; box-shadow: $shadow-card; }
@mixin page-padding { padding: $page-padding; }
@mixin nav-bar { height: 44px; display: flex; align-items: center; justify-content: center; border-bottom: 1px solid $color-border-light; }
```
3. 在 `app.scss` 中 import tokens 和 mixins
### Phase 1: 更新/创建全局组件2-3小时
按以下顺序逐一实现 §3 中的组件,**每个组件写完后立即替换 2-3 个引用页面验证效果**
1. `NavBar` → 替换所有子页面的导航栏
2. `StatusTag` → 替换所有状态标签
3. `ContentCard` → 替换所有卡片容器
4. `FormInput` → 替换所有表单输入
5. `PrimaryButton` / `SecondaryButton` → 替换所有按钮
6. `TabFilter` → 替换所有筛选标签
7. `ListItem` → 替换所有列表项
8. `InfoRow` → 替换所有信息行
9. `SectionTitle` / `PageHeader` → 替换所有标题
10. `ProgressRing` → 替换进度环
11. `AlertCard` → 替换提示卡片
12. `ChatBubble` → 替换聊天气泡
13. `VitalCard` → 替换体征卡片
14. `GradientHeader` → 替换渐变头部
### Phase 2: 逐页替换样式(每个页面 15-30 分钟)
按页面重要性排序,逐页将硬编码样式替换为组件引用:
**第一批 — 主 TabBar 页面(已有组件基础):**
1. `pages/index/index` → 参照 mp-redesign-home.html HomeA
2. `pages/health/index` → 参照 mp-redesign-home.html HealthA
3. `pages/messages/index` → 参照 mp-redesign-home.html MessagesA
4. `pages/profile/index` → 参照 mp-redesign-home.html ProfileA
**第二批 — 高频功能页面:**
5. `pages/login/index` → 参照 mp-01-login.html
6. `pages/consultation/index` → 参照 mp-02-consultation.html
7. `pages/appointment/index` → 参照 mp-03-appointment.html 列表
8. `pages/appointment/create/index` → 参照 mp-03-appointment.html 创建
9. `pages/appointment/detail/index` → 参照 mp-03-appointment.html 详情
**第三批 — 内容页面:**
10. `pages/article/index` + `detail` → 参照 mp-04-article-report.html
11. `pages/ai-report/list` + `detail` → 参照 mp-04-article-report.html
12. `pages/mall/index` → 参照 mp-05-mall.html
**第四批 — 健康分包:**
13-17. `pages/pkg-health/*` (5个) → 参照 mp-06-health-pkg.html
**第五批 — 个人中心分包:**
18-33. `pages/pkg-profile/*` (16个) → 参照 mp-07/08/09
**第六批 — 商城/咨询分包:**
34-37. `pages/pkg-mall/*` + `pages/pkg-consultation/detail` → 参照 mp-10-mall-pkg.html
**第七批 — 医生端:**
38-47. `pages/pkg-doctor-core/*` + `pages/pkg-doctor-clinical/*` → 参照 mp-11/12
### Phase 3: 全局验证1小时
1. **一致性检查**: 打开所有页面截图对比原型,确认风格统一
2. **长者模式**: 所有 58 个页面的关怀模式适配(字号 ≥ 22px
3. **暗色/亮色**: Token 通过 CSS 变量支持主题切换
4. **交叉页面导航**: 确保页面间跳转无样式断裂
## 5. 医生端 vs 患者端
医生端页面pkg-doctor-core / pkg-doctor-clinical使用不同的主色
```scss
// 在医生端页面入口设置 CSS 变量覆盖
--color-primary: #3A6B8C; // 替代 #C4623A
--color-primary-light: #D4E5F0; // 替代 #F0DDD4
--color-primary-dark: #2A4F6A; // 替代 #8B3E1F
```
这样所有引用 Token 的组件在医生端自动变色,**无需为医生端写单独的组件**。
## 6. 关键约束
1. **不允许在页面文件中硬编码颜色值** — 全部通过 Token 变量引用
2. **不允许跳过组件直接写样式** — 先建组件再引用
3. **每个组件必须支持 CSS 变量覆盖** — 医生端/关怀模式/主题切换都靠这个
4. **先更新 3-5 个页面验证组件可用** — 再批量替换剩余页面
5. **58 个页面中 66 个已迁移到统一组件库** — 在现有基础上更新 Token 和组件样式即可
## 7. 访客端Guest Mode
> **原型参考**: `mp-00-visitor.html`5 个屏幕)
访客端是用户未登录时看到的界面,核心目标是**引导转化登录**。与已登录页面有以下差异:
### 7.1 访客首页 vs 登录首页对比
| 维度 | 访客首页 | 登录首页 |
|------|---------|---------|
| 顶部 | 全屏 Swiper 轮播(品牌形象/智慧健康/温馨环境) | 智能提醒卡片 + 体征概览 |
| 内容 | 健康文章推荐(公开数据)+ 平台功能介绍卡片 | 今日体征网格 + 趋势图 + 快捷入口 |
| 底部 | 无快捷入口,有"登录享更多"CTA | 预约挂号/查看报告等快捷入口 |
| TabBar | 首页/健康(灰化)/消息(灰化)/我的 | 首页/健康/消息/我的(全部可用)|
### 7.2 访客专属组件
#### `BannerSwiper` — 轮播图组件
```
高度: 180px
borderRadius: $radius-lg
indicatorDots: 底部居中,活跃色 $color-primary
autoplay: true, interval: 4000ms
轮播项3个渐变背景 slides:
- 品牌形象: linear-gradient(135deg, #C4623A, #8B3E1F),大标题+副标题
- 智慧健康: linear-gradient(135deg, #3A6B8C, #2A4F6A),数据卡片展示
- 温馨就医: linear-gradient(135deg, #5B7A5E, #3D5A40),场景描述
数据来源: /api/v1/public/banners公开端点无需认证
```
**原型参考**: mp-00-visitor.html 前 3 个 Swiper slides
#### `FeatureCard` — 功能介绍卡片
```
background: $color-card
borderRadius: $radius-lg
padding: 20px
box-shadow: $shadow-card
textAlign: center
布局: 图标(40px圆形背景) + 标题(16px bold) + 描述(13px tertiary)
```
**原型参考**: mp-00-visitor.html 访客首页底部 3 个功能卡片
#### `LoginCTA` — 登录引导按钮
```
background: $color-primary
color: #fff
borderRadius: $radius-full
padding: 14px 32px
fontSize: 16px
fontWeight: 600
boxShadow: $shadow-button
textAlign: center
文字: "登录享更多健康服务"
点击: navigateTo login 页面
```
**原型参考**: mp-00-visitor.html 访客首页底部 + 访客"我的"页
### 7.3 访客"我的"页
访客态的个人中心与登录态完全不同:
```
头像区域:
- 未登录: 显示 "?" 圆形图标(bg: $color-surface, color: $color-text-tertiary)
- 登录后: 真实头像
登录提示卡:
- 标题: "登录后即可查看"
- 副标题: "健康数据、预约记录、咨询消息等"
- 按钮: "立即登录" (PrimaryButton)
菜单列表:
- 仅显示公开菜单(关于我们/隐私政策/使用条款)
- 隐藏所有需认证菜单(健康记录/家庭管理/设备绑定等)
```
**原型参考**: mp-00-visitor.html 最后一个屏幕
### 7.4 访客端实施要点
1. **`isGuest` 状态判断**: 在 `GuestHome` 页面中通过 `useAuth()` 检查登录态,未登录渲染访客布局
2. **轮播图数据**: 调用 `/api/v1/public/banners` 公开端点获取轮播图数据
3. **灰化 Tab**: 健康页/消息页在访客态显示"请先登录"提示 + 登录按钮
4. **登录转化**: 所有交互触发点(查看详情/预约/咨询)统一拦截跳转登录页
5. **组件复用**: BannerSwiper 组件同时用于访客首页和登录首页,通过 props 控制样式差异
6. **TabBar 页面路由**: 访客态和登录态共享同一路由,通过 `isGuest` 条件渲染不同内容
### 7.5 访客端实施优先级
排在 Phase 2 第一批(主 TabBar 页面)之后、第二批之前:
```
Phase 1.5 — 访客端:
1. 创建 BannerSwiper 组件 → src/components/ui/BannerSwiper/
2. 创建 FeatureCard 组件 → src/components/ui/FeatureCard/
3. 创建 LoginCTA 组件 → src/components/ui/LoginCTA/
4. 更新 GuestHome 页面 → 参照 mp-00-visitor.html 访客首页
5. 更新 Profile 页 isGuest 分支 → 参照 mp-00-visitor.html 访客"我的"
```

View File

@@ -0,0 +1,321 @@
<!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: 48px; 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">访客端设计:未登录用户看到的是品牌展示+功能引导+登录转化。首页轮播+资讯+功能卡片,"我的"页面显示登录引导。</div>
<div id="root"></div>
<script type="text/babel">
// ─── iOS 设备框 ───
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 = 393, height = 852, time = '9:41', battery = 85, darkStatus = false }) {
const sc = darkStatus ? '#fff' : '#000';
return (
<div style={iosFrameStyles.wrapper}>
<div style={{ ...iosFrameStyles.screen, width, height }}>
<div style={{ ...iosFrameStyles.statusBar, color: sc }}>
<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 1 0 000 2z" fill={sc}/><path d="M3 7.5a7 7 0 0110 0" stroke={sc} strokeWidth="1.3" fill="none" strokeLinecap="round"/><path d="M1 4.5a11 11 0 0114 0" stroke={sc} strokeWidth="1.3" fill="none" strokeLinecap="round" opacity="0.7"/></svg>
<div style={{ width: 26, height: 12, border: `1.5px solid ${sc}`, borderRadius: 3, padding: 1 }}><div style={{ width: `${battery}%`, height: '100%', background: sc, borderRadius: 1 }} /></div>
</div>
</div>
<div style={iosFrameStyles.dynamicIsland} />
<div style={iosFrameStyles.content}>{children}</div>
<div style={iosFrameStyles.homeIndicator} />
</div>
</div>
);
}
// ─── 设计 Token ───
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,
};
// ─── 访客首页轮播1 — 品牌形象 ───
function GuestSlide1() {
return (
<div style={{ height: '100%', background: `linear-gradient(135deg, ${T.pri} 0%, ${T.priD} 60%, #5A3A2A 100%)`, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', padding: '0 24px 60px', position: 'relative' }}>
{/* 装饰圆 */}
<div style={{ position: 'absolute', top: 40, right: -20, width: 160, height: 160, borderRadius: 80, background: 'rgba(255,255,255,0.08)' }} />
<div style={{ position: 'absolute', top: 80, right: 20, width: 80, height: 80, borderRadius: 40, background: 'rgba(255,255,255,0.06)' }} />
<div style={{ position: 'absolute', bottom: 120, left: -30, width: 120, height: 120, borderRadius: 60, background: 'rgba(255,255,255,0.05)' }} />
{/* 内容 */}
<div style={{ position: 'relative', zIndex: 2 }}>
<div style={{ fontFamily: T.serif, fontSize: 32, fontWeight: 700, color: '#fff', marginBottom: 8, lineHeight: 1.2 }}>温润守护</div>
<div style={{ fontFamily: T.serif, fontSize: 32, fontWeight: 700, color: '#fff', marginBottom: 16, lineHeight: 1.2 }}>健康同行</div>
<div style={{ fontSize: 14, color: 'rgba(255,255,255,0.8)', lineHeight: 1.6 }}>专业的健康管理平台<br/>让每一位患者享受智能化的关怀服务</div>
</div>
{/* 指示点 */}
<div style={{ display: 'flex', gap: 6, marginTop: 20 }}>
<div style={{ width: 24, height: 4, borderRadius: 2, background: '#fff' }} />
<div style={{ width: 8, height: 4, borderRadius: 2, background: 'rgba(255,255,255,0.4)' }} />
<div style={{ width: 8, height: 4, borderRadius: 2, background: 'rgba(255,255,255,0.4)' }} />
</div>
</div>
);
}
// ─── 访客首页:完整页 ───
function GuestHome() {
return (
<div style={{ height: '100%', background: T.bg, overflowY: 'auto' }}>
{/* 轮播区域 */}
<div style={{ height: 280, position: 'relative' }}>
<GuestSlide1 />
</div>
{/* 健康资讯 */}
<div style={{ padding: '20px 20px 0' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<span style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.tx }}>健康资讯</span>
<span style={{ fontSize: 13, color: T.tx3 }}>更多 </span>
</div>
{/* 带封面的文章 */}
{[
{ title: '高血压患者日常饮食指南', summary: '科学饮食,轻松控制血压水平', hasCover: true },
{ title: '血液透析的日常注意事项', summary: '透析患者必知的生活管理要点', hasCover: true },
].map((article, i) => (
<div key={i} style={{ background: T.card, borderRadius: T.r, overflow: 'hidden', marginBottom: 10, boxShadow: '0 1px 4px rgba(45,42,38,0.04)', display: 'flex' }}>
<div style={{ width: 110, minHeight: 80, background: T.surface, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ fontSize: 11, color: T.tx3 }}>配图</span>
</div>
<div style={{ padding: '12px 14px', flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<div style={{ fontSize: 15, fontWeight: 600, color: T.tx, marginBottom: 4, lineHeight: 1.4 }}>{article.title}</div>
<div style={{ fontSize: 13, color: T.tx2, lineHeight: 1.5 }}>{article.summary}</div>
</div>
</div>
))}
{/* 功能介绍卡片 — 3 个横排 */}
<div style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.tx, marginTop: 20, marginBottom: 12 }}>我们的服务</div>
<div style={{ display: 'flex', gap: 10 }}>
{[
{ icon: '健', title: '健康数据管理', desc: '记录体征', bg: T.accL, color: T.acc },
{ icon: '约', title: '智能预约排班', desc: '在线预约', bg: T.priL, color: T.pri },
{ icon: '智', title: 'AI 健康分析', desc: '趋势解读', bg: T.wrnL, color: T.wrn },
].map((item, i) => (
<div key={i} style={{ flex: 1, background: T.card, borderRadius: T.r, padding: 16, textAlign: 'center', boxShadow: '0 1px 4px rgba(45,42,38,0.04)' }}>
<div style={{ width: 40, height: 40, borderRadius: 20, background: item.bg, display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 8px' }}>
<span style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: item.color }}>{item.icon}</span>
</div>
<div style={{ fontSize: 13, fontWeight: 600, color: T.tx, marginBottom: 2 }}>{item.title}</div>
<div style={{ fontSize: 12, color: T.tx3 }}>{item.desc}</div>
</div>
))}
</div>
</div>
{/* 登录引导 */}
<div style={{ padding: '28px 20px 40px', textAlign: 'center' }}>
<div style={{ fontSize: 14, color: T.tx3, marginBottom: 16 }}>登录后即可使用完整健康管理服务</div>
<div style={{ height: 52, borderRadius: 14, background: T.pri, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: 17, fontWeight: 600, boxShadow: `0 4px 16px rgba(196,98,58,0.3)` }}>立即登录</div>
</div>
{/* TabBar — 无选中态 */}
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 80, background: '#fff', borderTop: `1px solid ${T.bdL}`, display: 'flex', alignItems: 'center', justifyContent: 'space-around', paddingBottom: 10 }}>
{['首页','健康','消息','我的'].map((t, i) => (
<div key={i} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2, color: i === 0 ? T.pri : T.tx3, fontSize: 10 }}>
<div style={{ width: 24, height: 24, borderRadius: 6, background: i === 0 ? T.pri : T.surface, display: 'flex', alignItems: 'center', justifyContent: 'center', color: i === 0 ? '#fff' : T.tx3, fontSize: 12 }} />
<span>{t}</span>
</div>
))}
</div>
</div>
);
}
// ─── 访客首页轮播2 — 智慧健康 ───
function GuestSlide2() {
return (
<div style={{ height: '100%', background: `linear-gradient(135deg, ${T.acc} 0%, #3A5A3E 60%, #2A3A2E 100%)`, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', padding: '0 24px 60px', position: 'relative' }}>
<div style={{ position: 'absolute', top: 50, right: 10, width: 140, height: 140, borderRadius: 70, background: 'rgba(255,255,255,0.08)' }} />
<div style={{ position: 'relative', zIndex: 2 }}>
<div style={{ fontFamily: T.serif, fontSize: 32, fontWeight: 700, color: '#fff', marginBottom: 16, lineHeight: 1.2 }}>智慧健康管理</div>
<div style={{ fontSize: 14, color: 'rgba(255,255,255,0.8)', lineHeight: 1.6 }}>AI 驱动个性化健康方案<br/>智能分析您的健康趋势</div>
</div>
<div style={{ display: 'flex', gap: 6, marginTop: 20 }}>
<div style={{ width: 8, height: 4, borderRadius: 2, background: 'rgba(255,255,255,0.4)' }} />
<div style={{ width: 24, height: 4, borderRadius: 2, background: '#fff' }} />
<div style={{ width: 8, height: 4, borderRadius: 2, background: 'rgba(255,255,255,0.4)' }} />
</div>
</div>
);
}
// ─── 访客首页轮播3 — 就医体验 ───
function GuestSlide3() {
return (
<div style={{ height: '100%', background: `linear-gradient(135deg, #6B8E9B 0%, #3A5A6A 60%, #2A3A4A 100%)`, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', padding: '0 24px 60px', position: 'relative' }}>
<div style={{ position: 'absolute', top: 60, left: 20, width: 100, height: 100, borderRadius: 50, background: 'rgba(255,255,255,0.06)' }} />
<div style={{ position: 'relative', zIndex: 2 }}>
<div style={{ fontFamily: T.serif, fontSize: 32, fontWeight: 700, color: '#fff', marginBottom: 16, lineHeight: 1.2 }}>温馨就医环境</div>
<div style={{ fontSize: 14, color: 'rgba(255,255,255,0.8)', lineHeight: 1.6 }}>舒适安全的治疗体验<br/>专业团队全程陪伴</div>
</div>
<div style={{ display: 'flex', gap: 6, marginTop: 20 }}>
<div style={{ width: 8, height: 4, borderRadius: 2, background: 'rgba(255,255,255,0.4)' }} />
<div style={{ width: 8, height: 4, borderRadius: 2, background: 'rgba(255,255,255,0.4)' }} />
<div style={{ width: 24, height: 4, borderRadius: 2, background: '#fff' }} />
</div>
</div>
);
}
// ─── 访客"我的"页面 ───
function GuestProfile() {
const menuGroups = [
{
title: '健康服务',
items: [
{ label: '健康资讯', icon: '文', bg: T.priL, color: T.pri },
{ label: '关于我们', icon: '关', bg: T.accL, color: T.acc },
],
},
{
title: '支持',
items: [
{ label: '帮助中心', icon: '帮', bg: T.surface, color: T.tx3 },
{ label: '意见反馈', icon: '反', bg: T.surface, color: T.tx3 },
{ label: '设置', icon: '齿', bg: T.surface, color: T.tx3 },
],
},
];
return (
<div style={{ height: '100%', background: T.bg, overflowY: 'auto' }}>
<div style={{ padding: '20px 20px 100px' }}>
{/* 未登录用户卡片 */}
<div style={{ background: T.card, borderRadius: T.r, padding: 20, boxShadow: '0 2px 12px rgba(45,42,38,0.06)', marginBottom: 16, display: 'flex', alignItems: 'center', gap: 16 }}>
<div style={{ width: 60, height: 60, borderRadius: 30, background: T.surface, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ fontFamily: T.serif, fontSize: 28, fontWeight: 700, color: T.tx3 }}>?</span>
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 22, fontWeight: 700, color: T.tx, fontFamily: T.serif, marginBottom: 2 }}>未登录</div>
<div style={{ fontSize: 14, color: T.tx3 }}>点击登录开启健康管理之旅</div>
</div>
<span style={{ color: T.tx3, fontSize: 16 }}></span>
</div>
{/* 登录引导卡 */}
<div style={{ background: `linear-gradient(135deg, ${T.priL} 0%, #E8C8B8 100%)`, borderRadius: T.r, padding: 20, marginBottom: 24, display: 'flex', alignItems: 'center', gap: 16 }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 16, fontWeight: 600, color: T.tx, marginBottom: 4 }}>登录享受完整服务</div>
<div style={{ fontSize: 13, color: T.tx2, lineHeight: 1.5 }}>体征记录 · 智能分析 · 在线咨询 · 预约挂号</div>
</div>
<div style={{ height: 44, padding: '0 20px', borderRadius: T.rSm, background: T.pri, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: 15, fontWeight: 600, flexShrink: 0 }}>登录</div>
</div>
{/* 分组菜单 */}
{menuGroups.map((group, gi) => (
<div key={gi} style={{ marginBottom: 14 }}>
<div style={{ fontSize: 14, fontWeight: 600, color: T.tx2, marginBottom: 8, paddingLeft: 4 }}>{group.title}</div>
<div style={{ background: T.card, borderRadius: T.r, overflow: 'hidden', boxShadow: '0 1px 4px rgba(45,42,38,0.04)' }}>
{group.items.map((item, ii) => (
<div key={ii} style={{
display: 'flex', alignItems: 'center', gap: 14, padding: '14px 16px',
borderBottom: ii < group.items.length - 1 ? `1px solid ${T.bdL}` : 'none',
}}>
<div style={{ width: 36, height: 36, borderRadius: T.rSm, background: item.bg, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ fontFamily: T.serif, fontSize: 16, fontWeight: 700, color: item.color }}>{item.icon}</span>
</div>
<span style={{ flex: 1, fontSize: 15, color: T.tx }}>{item.label}</span>
<span style={{ color: T.tx3, fontSize: 14 }}></span>
</div>
))}
</div>
</div>
))}
</div>
{/* TabBar */}
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 80, background: '#fff', borderTop: `1px solid ${T.bdL}`, display: 'flex', alignItems: 'center', justifyContent: 'space-around', paddingBottom: 10 }}>
{['首页','健康','消息','我的'].map((t, i) => (
<div key={i} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2, color: i === 3 ? T.pri : T.tx3, fontSize: 10 }}>
<div style={{ width: 24, height: 24, borderRadius: 6, background: i === 3 ? T.pri : T.surface, display: 'flex', alignItems: 'center', justifyContent: 'center', color: i === 3 ? '#fff' : T.tx3, fontSize: 12 }} />
<span>{t}</span>
</div>
))}
</div>
</div>
);
}
// ─── 渲染 ───
function App() {
return (
<div className="screens">
<div className="screen-wrap">
<span className="screen-label">访客首页 轮播 1</span>
<IosFrame time="9:41" battery={85} darkStatus={true}>
<GuestSlide1 />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">访客首页 完整页</span>
<IosFrame time="9:41" battery={85}>
<GuestHome />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">访客首页 轮播 2</span>
<IosFrame time="9:41" battery={85} darkStatus={true}>
<GuestSlide2 />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">访客首页 轮播 3</span>
<IosFrame time="9:41" battery={85} darkStatus={true}>
<GuestSlide3 />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">访客"我的"页面</span>
<IosFrame time="9:41" battery={85}>
<GuestProfile />
</IosFrame>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>

View File

@@ -0,0 +1,217 @@
<!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: 48px; 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">温润东方风设计系统 — 登录是用户体验的起点,简洁、温暖、无压力。手机号+验证码+微信一键登录三种方式。</div>
<div id="root"></div>
<script type="text/babel">
// ─── iOS 设备框 ───
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 = 393, height = 852, 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 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>
);
}
// ─── 设计 Token ───
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 LoginInitial() {
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column', padding: '80px 28px 40px' }}>
{/* Logo 区域 */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: 56 }}>
<div style={{ width: 80, height: 80, borderRadius: 40, background: `linear-gradient(135deg, ${T.priL} 0%, ${T.pri} 100%)`, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 20, boxShadow: `0 4px 16px rgba(196,98,58,0.25)` }}>
<span style={{ fontFamily: T.serif, fontSize: 36, fontWeight: 700, color: '#fff' }}>H</span>
</div>
<div style={{ fontFamily: T.serif, fontSize: 28, fontWeight: 700, color: T.tx, marginBottom: 8 }}>HMS 健康</div>
<div style={{ fontSize: 14, color: T.tx3 }}>您的专属健康管家</div>
</div>
{/* 手机号输入 */}
<div style={{ marginBottom: 12 }}>
<div style={{ height: 56, background: T.card, border: `1.5px solid ${T.bd}`, borderRadius: T.r, display: 'flex', alignItems: 'center', padding: '0 16px' }}>
<span style={{ fontSize: 16, color: T.tx, fontWeight: 500, marginRight: 12, minWidth: 36 }}>+86</span>
<div style={{ width: 1, height: 24, background: T.bd, marginRight: 12 }} />
<span style={{ fontSize: 16, color: T.tx3 }}>请输入手机号</span>
</div>
</div>
{/* 验证码输入 */}
<div style={{ marginBottom: 24 }}>
<div style={{ height: 56, background: T.card, border: `1.5px solid ${T.bd}`, borderRadius: T.r, display: 'flex', alignItems: 'center', padding: '0 16px', justifyContent: 'space-between' }}>
<span style={{ fontSize: 16, color: T.tx3 }}>请输入验证码</span>
<span style={{ fontSize: 14, color: T.pri, fontWeight: 600, padding: '6px 0' }}>获取验证码</span>
</div>
</div>
{/* 登录按钮 */}
<div style={{ height: 54, borderRadius: 16, background: T.pri, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: 18, fontWeight: 600, boxShadow: `0 4px 16px rgba(196,98,58,0.3)`, marginBottom: 16 }}>登录</div>
{/* 分隔线 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 16 }}>
<div style={{ flex: 1, height: 1, background: T.bdL }} />
<span style={{ fontSize: 12, color: T.tx3 }}></span>
<div style={{ flex: 1, height: 1, background: T.bdL }} />
</div>
{/* 微信一键登录 */}
<div style={{ height: 54, borderRadius: 16, background: '#07C160', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, color: '#fff', fontSize: 18, fontWeight: 600, marginBottom: 24 }}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="#fff"><path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 01.213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 00.167-.054l1.903-1.114a.864.864 0 01.717-.098 10.16 10.16 0 002.837.403c.276 0 .543-.027.811-.05a6.42 6.42 0 01-.246-1.79c0-3.558 3.39-6.451 7.585-6.451.258 0 .507.022.76.042C16.706 4.882 13.075 2.188 8.691 2.188zm-2.85 4.56a1.1 1.1 0 110 2.2 1.1 1.1 0 010-2.2zm5.7 0a1.1 1.1 0 110 2.2 1.1 1.1 0 010-2.2zm4.3 4.378c-3.652 0-6.615 2.472-6.615 5.517s2.963 5.517 6.615 5.517a7.8 7.8 0 002.222-.323.626.626 0 01.52.072l1.38.809a.236.236 0 00.121.039.213.213 0 00.21-.214c0-.052-.02-.103-.035-.153l-.283-1.073a.427.427 0 01.155-.483C21.855 19.882 22.84 18.2 22.84 16.643c0-3.045-2.963-5.517-6.615-5.517h.006zm-2.41 3.2a.895.895 0 110 1.79.895.895 0 010-1.79zm4.82 0a.895.895 0 110 1.79.895.895 0 010-1.79z"/></svg>
微信一键登录
</div>
{/* 用户协议 */}
<div style={{ fontSize: 12, color: T.tx3, textAlign: 'center', lineHeight: 1.8 }}>
登录即同意 <span style={{ color: T.pri }}>用户协议</span> <span style={{ color: T.pri }}></span>
</div>
{/* 弹性空间 */}
<div style={{ flex: 1 }} />
{/* 开发模式 */}
<div style={{ textAlign: 'center', padding: '12px', border: `1px dashed ${T.bd}`, borderRadius: T.rSm }}>
<span style={{ fontSize: 13, color: T.tx3 }}>开发模式快速登录 </span>
</div>
</div>
);
}
// ─── 登录页:填写中状态 ───
function LoginFilled() {
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column', padding: '80px 28px 40px' }}>
{/* Logo 区域 */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: 56 }}>
<div style={{ width: 80, height: 80, borderRadius: 40, background: `linear-gradient(135deg, ${T.priL} 0%, ${T.pri} 100%)`, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 20, boxShadow: `0 4px 16px rgba(196,98,58,0.25)` }}>
<span style={{ fontFamily: T.serif, fontSize: 36, fontWeight: 700, color: '#fff' }}>H</span>
</div>
<div style={{ fontFamily: T.serif, fontSize: 28, fontWeight: 700, color: T.tx, marginBottom: 8 }}>HMS 健康</div>
<div style={{ fontSize: 14, color: T.tx3 }}>您的专属健康管家</div>
</div>
{/* 手机号 — 已填写 */}
<div style={{ marginBottom: 12 }}>
<div style={{ height: 56, background: T.card, border: `1.5px solid ${T.pri}`, borderRadius: T.r, display: 'flex', alignItems: 'center', padding: '0 16px' }}>
<span style={{ fontSize: 16, color: T.tx, fontWeight: 500, marginRight: 12, minWidth: 36 }}>+86</span>
<div style={{ width: 1, height: 24, background: T.bd, marginRight: 12 }} />
<span style={{ fontSize: 16, color: T.tx, letterSpacing: 2 }}>138 1234 5678</span>
</div>
</div>
{/* 验证码 — 已填写 */}
<div style={{ marginBottom: 24 }}>
<div style={{ height: 56, background: T.card, border: `1.5px solid ${T.pri}`, borderRadius: T.r, display: 'flex', alignItems: 'center', padding: '0 16px', justifyContent: 'space-between' }}>
<span style={{ fontSize: 16, color: T.tx, letterSpacing: 6 }}>6285</span>
<span style={{ fontSize: 14, color: T.tx3 }}>58s 后重发</span>
</div>
</div>
{/* 登录按钮 */}
<div style={{ height: 54, borderRadius: 16, background: T.pri, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: 18, fontWeight: 600, boxShadow: `0 4px 16px rgba(196,98,58,0.3)`, marginBottom: 16 }}>登录</div>
{/* 分隔线 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 16 }}>
<div style={{ flex: 1, height: 1, background: T.bdL }} />
<span style={{ fontSize: 12, color: T.tx3 }}></span>
<div style={{ flex: 1, height: 1, background: T.bdL }} />
</div>
{/* 微信一键登录 */}
<div style={{ height: 54, borderRadius: 16, background: '#07C160', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, color: '#fff', fontSize: 18, fontWeight: 600, marginBottom: 24 }}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="#fff"><path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 01.213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 00.167-.054l1.903-1.114a.864.864 0 01.717-.098 10.16 10.16 0 002.837.403c.276 0 .543-.027.811-.05a6.42 6.42 0 01-.246-1.79c0-3.558 3.39-6.451 7.585-6.451.258 0 .507.022.76.042C16.706 4.882 13.075 2.188 8.691 2.188zm-2.85 4.56a1.1 1.1 0 110 2.2 1.1 1.1 0 010-2.2zm5.7 0a1.1 1.1 0 110 2.2 1.1 1.1 0 010-2.2zm4.3 4.378c-3.652 0-6.615 2.472-6.615 5.517s2.963 5.517 6.615 5.517a7.8 7.8 0 002.222-.323.626.626 0 01.52.072l1.38.809a.236.236 0 00.121.039.213.213 0 00.21-.214c0-.052-.02-.103-.035-.153l-.283-1.073a.427.427 0 01.155-.483C21.855 19.882 22.84 18.2 22.84 16.643c0-3.045-2.963-5.517-6.615-5.517h.006zm-2.41 3.2a.895.895 0 110 1.79.895.895 0 010-1.79zm4.82 0a.895.895 0 110 1.79.895.895 0 010-1.79z"/></svg>
微信一键登录
</div>
{/* 用户协议 */}
<div style={{ fontSize: 12, color: T.tx3, textAlign: 'center', lineHeight: 1.8 }}>
登录即同意 <span style={{ color: T.pri }}>用户协议</span> <span style={{ color: T.pri }}></span>
</div>
<div style={{ flex: 1 }} />
<div style={{ textAlign: 'center', padding: '12px', border: `1px dashed ${T.bd}`, borderRadius: T.rSm }}>
<span style={{ fontSize: 13, color: T.tx3 }}>开发模式快速登录 </span>
</div>
</div>
);
}
// ─── 渲染 ───
function App() {
return (
<div className="screens">
<div className="screen-wrap">
<span className="screen-label">登录 初始状态</span>
<IosFrame time="9:41" battery={85}>
<LoginInitial />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">登录 填写中</span>
<IosFrame time="9:41" battery={84}>
<LoginFilled />
</IosFrame>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>

View File

@@ -0,0 +1,329 @@
<!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: 48px; 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">温润东方风设计系统 — 咨询列表页 + 咨询详情(聊天)页。左侧为咨询列表,右侧为与王医生的聊天详情。</div>
<div id="root"></div>
<script type="text/babel">
// ─── iOS 设备框 ───
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 = 393, height = 852, 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 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>
);
}
// ─── 设计 Token ───
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 ConsultationList() {
const doctors = [
{ name: '王医生', dept: '心内科', avatar: '王', avatarBg: '#C4623A', lastMsg: '您的检查报告已出,建议...', time: '10分钟前', unread: 2, opacity: 1 },
{ name: '李医生', dept: '肾内科', avatar: '李', avatarBg: '#5B7A5E', lastMsg: '复查结果整体平稳', time: '昨天', unread: 0, opacity: 0.7 },
{ name: '张医生', dept: '全科', avatar: '张', avatarBg: '#C4873A', lastMsg: '门诊随访已完成', time: '3天前', unread: 0, opacity: 0.7 },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
{/* 导航栏 */}
<div style={{ padding: '16px 20px 12px', background: T.card, borderBottom: `1px solid ${T.bdL}` }}>
<div style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.tx, textAlign: 'center' }}>在线咨询</div>
</div>
{/* 搜索栏 */}
<div style={{ padding: '12px 20px 4px' }}>
<div style={{ height: 44, background: T.surface, borderRadius: T.r, display: 'flex', alignItems: 'center', padding: '0 14px', gap: 8 }}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={T.tx3} strokeWidth="2" strokeLinecap="round"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
<span style={{ fontSize: 14, color: T.tx3 }}>搜索医生或科室</span>
</div>
</div>
{/* 咨询列表 */}
<div style={{ flex: 1, padding: '8px 20px 20px', display: 'flex', flexDirection: 'column', gap: 8 }}>
{doctors.map((doc, i) => (
<div key={i} style={{
background: T.card,
borderRadius: T.r,
padding: 16,
opacity: doc.opacity,
boxShadow: '0 1px 4px rgba(0,0,0,0.04)',
display: 'flex',
alignItems: 'center',
gap: 14,
position: 'relative',
}}>
{/* 头像 */}
<div style={{
width: 48, height: 48, borderRadius: 24,
background: doc.avatarBg,
display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>
<span style={{ color: '#fff', fontSize: 20, fontWeight: 600, fontFamily: T.sans }}>{doc.avatar}</span>
</div>
{/* 信息 */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
<span style={{ fontSize: 16, fontWeight: 600, color: T.tx }}>{doc.name}<span style={{ color: T.tx3, fontWeight: 400, fontSize: 13, marginLeft: 6 }}>· {doc.dept}</span></span>
<span style={{ fontSize: 12, color: T.tx3 }}>{doc.time}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style={{ fontSize: 14, color: T.tx2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 200 }}>{doc.lastMsg}</span>
{doc.unread > 0 && (
<div style={{
minWidth: 20, height: 20, borderRadius: 10,
background: T.dan, display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: '0 6px', flexShrink: 0,
}}>
<span style={{ color: '#fff', fontSize: 12, fontWeight: 600 }}>{doc.unread}</span>
</div>
)}
</div>
</div>
</div>
))}
{/* 底部提示 */}
<div style={{ textAlign: 'center', padding: '20px 0 8px' }}>
<span style={{ fontSize: 12, color: T.tx3 }}> 3 位咨询医生</span>
</div>
</div>
</div>
);
}
// ─── 咨询详情(聊天页) ───
function ChatDetail() {
const messages = [
{ from: 'doctor', text: '张先生您好,您的血常规报告已出。' },
{ from: 'me', text: '好的,有什么需要注意的吗?' },
{ from: 'doctor', text: '血红蛋白略有下降,建议补充铁剂,下月复查。' },
{ from: 'doctor', text: '我帮您开个处方,注意按时服用。' },
{ from: 'me', text: '谢谢医生!' },
];
const timeMarkers = ['今天 09:32', null, null, '09:45', null];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
{/* 导航栏 */}
<div style={{
padding: '16px 20px 12px',
background: T.card,
borderBottom: `1px solid ${T.bdL}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}>
{/* 返回箭头 */}
<div style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke={T.tx} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
</div>
{/* 标题 */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<span style={{ fontFamily: T.serif, fontSize: 17, fontWeight: 700, color: T.tx }}>王医生·心内科</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 2 }}>
<div style={{ width: 6, height: 6, borderRadius: 3, background: T.acc }} />
<span style={{ fontSize: 11, color: T.acc }}>在线</span>
</div>
</div>
{/* 更多 */}
<div style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={T.tx3} strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg>
</div>
</div>
{/* 聊天区域 */}
<div style={{ flex: 1, padding: '16px 20px', display: 'flex', flexDirection: 'column', gap: 4, overflow: 'auto' }}>
{/* 日期标记 */}
<div style={{ textAlign: 'center', margin: '4px 0 12px' }}>
<span style={{ fontSize: 11, color: T.tx3, background: T.surface, padding: '3px 12px', borderRadius: 10 }}>今天 09:30</span>
</div>
{messages.map((msg, i) => (
<React.Fragment key={i}>
{/* 时间标记 */}
{timeMarkers[i] && i > 0 && (
<div style={{ textAlign: 'center', margin: '8px 0' }}>
<span style={{ fontSize: 11, color: T.tx3, background: T.surface, padding: '3px 12px', borderRadius: 10 }}>{timeMarkers[i]}</span>
</div>
)}
{msg.from === 'doctor' ? (
/* 医生消息 — 左对齐 */
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, marginBottom: 4 }}>
<div style={{
width: 36, height: 36, borderRadius: 18,
background: T.pri, display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>
<span style={{ color: '#fff', fontSize: 15, fontWeight: 600 }}></span>
</div>
<div style={{
background: T.card,
borderRadius: '4px 16px 16px 16px',
padding: '10px 14px',
maxWidth: '72%',
boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
}}>
<span style={{ fontSize: 15, color: T.tx, lineHeight: 1.6 }}>{msg.text}</span>
</div>
</div>
) : (
/* 我的消息 — 右对齐 */
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'flex-end', gap: 10, marginBottom: 4 }}>
<div style={{
background: T.priL,
borderRadius: '16px 4px 16px 16px',
padding: '10px 14px',
maxWidth: '72%',
}}>
<span style={{ fontSize: 15, color: T.tx, lineHeight: 1.6 }}>{msg.text}</span>
</div>
</div>
)}
</React.Fragment>
))}
{/* 正在输入提示 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 4 }}>
<div style={{
width: 36, height: 36, borderRadius: 18,
background: T.pri, display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>
<span style={{ color: '#fff', fontSize: 15, fontWeight: 600 }}></span>
</div>
<div style={{
background: T.card, borderRadius: '4px 16px 16px 16px',
padding: '10px 16px', display: 'flex', gap: 4, alignItems: 'center',
}}>
<div style={{ width: 6, height: 6, borderRadius: 3, background: T.tx3, animation: 'pulse 1.4s infinite' }} />
<div style={{ width: 6, height: 6, borderRadius: 3, background: T.tx3, animation: 'pulse 1.4s infinite 0.2s' }} />
<div style={{ width: 6, height: 6, borderRadius: 3, background: T.tx3, animation: 'pulse 1.4s infinite 0.4s' }} />
</div>
</div>
</div>
{/* 底部输入栏 */}
<div style={{
background: T.card,
borderTop: `1px solid ${T.bdL}`,
padding: '10px 16px',
display: 'flex',
alignItems: 'center',
gap: 10,
}}>
{/* 附加按钮 */}
<div style={{ width: 36, height: 36, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke={T.tx3} strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v8M8 12h8"/></svg>
</div>
{/* 输入框 */}
<div style={{
flex: 1, height: 40, background: T.surface, borderRadius: 20,
display: 'flex', alignItems: 'center', padding: '0 14px',
}}>
<span style={{ fontSize: 14, color: T.tx3 }}>输入消息...</span>
</div>
{/* 发送按钮 */}
<div style={{
width: 40, height: 40, borderRadius: 20,
background: T.pri, display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M22 2L11 13"/><path d="M22 2L15 22L11 13L2 9L22 2Z"/></svg>
</div>
</div>
</div>
);
}
// ─── 渲染 ───
function App() {
return (
<div className="screens">
<div className="screen-wrap">
<span className="screen-label">咨询列表</span>
<IosFrame time="9:41" battery={85}>
<ConsultationList />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">咨询详情 聊天</span>
<IosFrame time="9:45" battery={84}>
<ChatDetail />
</IosFrame>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
<style>
@keyframes pulse {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
</style>
</body>
</html>

View File

@@ -0,0 +1,398 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HMS 小程序 — AI 客服咨询</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: 700px; text-align: center; line-height: 1.8; }
.screens { display: flex; gap: 48px; 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; }
@keyframes pulse { 0%,80%,100% { opacity: 0.3; transform: scale(0.8); } 40% { opacity: 1; transform: scale(1); } }
</style>
</head>
<body>
<div class="page-title">HMS 小程序 · AI 客服咨询</div>
<div class="note">
温润东方风设计系统 — 消息 Tab 改为 AI 客服单窗口(无列表),通知迁移至「我的」页面。<br/>
左:欢迎状态(首次进入)· 中:对话进行中 · 右:个人中心通知入口
</div>
<div id="root"></div>
<script type="text/babel">
// ─── iOS 设备框 ───
const F = {
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' },
status: { 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' },
island: { position: 'absolute', top: 12, left: '50%', transform: 'translateX(-50%)', width: 124, height: 36, background: '#000', borderRadius: 999, zIndex: 30 },
body: { position: 'absolute', top: 54, left: 0, right: 0, bottom: 34, overflow: 'auto' },
home: { 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 Phone({ children, w = 393, h = 852, time = '9:41', battery = 85, dark = false }) {
const c = dark ? '#fff' : '#000';
return (
<div style={F.wrapper}>
<div style={{ ...F.screen, width: w, height: h }}>
<div style={{ ...F.status, color: c }}>
<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 1 0 000 2z" fill={c}/><path d="M3 7.5a7 7 0 0110 0" stroke={c} strokeWidth="1.3" fill="none" strokeLinecap="round"/><path d="M1 4.5a11 11 0 0114 0" stroke={c} strokeWidth="1.3" fill="none" strokeLinecap="round" opacity="0.7"/></svg>
<div style={{ width: 26, height: 12, border: `1.5px solid ${c}`, borderRadius: 3, padding: 1, position: 'relative' }}>
<div style={{ width: `${battery}%`, height: '100%', background: c, borderRadius: 1 }} />
</div>
</div>
</div>
<div style={F.island} />
<div style={F.body}>{children}</div>
<div style={F.home} />
</div>
</div>
);
}
// ─── 设计 Token ───
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',
dan: '#B54A4A', danL: '#FDEAEA',
white: '#FFFFFF',
serif: "Georgia, 'Times New Roman', serif",
sans: "-apple-system, 'PingFang SC', sans-serif",
r: 16, rSm: 12, rXs: 8,
};
// ─── 占位:三屏容器 ───
function App() {
return (
<div className="screens">
{/* 左屏:欢迎状态 */}
<div className="screen-wrap">
<Phone><WelcomeChat /></Phone>
<div className="screen-label">首次进入 · 欢迎状态</div>
</div>
{/* 中屏:对话进行中 */}
<div className="screen-wrap">
<Phone><ActiveChat /></Phone>
<div className="screen-label">对话进行中</div>
</div>
{/* 右屏:个人中心通知入口 */}
<div className="screen-wrap">
<Phone><ProfileWithNotify /></Phone>
<div className="screen-label">个人中心 · 通知入口</div>
</div>
</div>
);
}
// ─── 共享:聊天导航栏 ───
function ChatNav() {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '16px 20px 12px', background: T.card, borderBottom: `1px solid ${T.bdL}`, flexShrink: 0 }}>
{/* 返回 */}
<div style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ fontSize: 24, fontWeight: 300, color: T.tx }}></span>
</div>
{/* 标题 + 在线状态 */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<span style={{ fontFamily: T.serif, fontSize: 17, fontWeight: 700, color: T.tx }}>健康助手 · 小华</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 2 }}>
<div style={{ width: 6, height: 6, borderRadius: 3, background: T.acc }} />
<span style={{ fontSize: 11, color: T.acc }}>24小时在线</span>
</div>
</div>
{/* 占位 */}
<div style={{ width: 32 }} />
</div>
);
}
// ─── 共享:底部输入栏 ───
function ChatBar() {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 16px', paddingBottom: 24, background: T.card, borderTop: `1px solid ${T.bdL}`, flexShrink: 0 }}>
<div style={{ width: 36, height: 36, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ fontSize: 22, color: T.tx3, fontWeight: 300 }}>+</span>
</div>
<div style={{ flex: 1, height: 40, background: T.surface, borderRadius: 20, padding: '0 14px', display: 'flex', alignItems: 'center' }}>
<span style={{ color: T.tx3, fontSize: 14 }}>输入您的问题...</span>
</div>
<div style={{ width: 40, height: 40, borderRadius: 20, background: T.pri, display: 'flex', alignItems: 'center', justifyContent: 'center', opacity: 0.5 }}>
<span style={{ color: T.white, fontSize: 20, fontWeight: 700 }}></span>
</div>
</div>
);
}
// ─── 共享AI 头像 ───
function AiAvatar({ size = 36 }) {
return (
<div style={{ width: size, height: size, borderRadius: size / 2, background: `linear-gradient(135deg, ${T.pri}, ${T.priD})`, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ color: T.white, fontSize: size * 0.42, fontWeight: 600 }}></span>
</div>
);
}
// ─── 左屏:欢迎状态 ───
function WelcomeChat() {
const quickActions = [
{ icon: '📋', label: '查看报告' },
{ icon: '💊', label: '用药咨询' },
{ icon: '📅', label: '预约挂号' },
{ icon: '🔔', label: '健康提醒' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<ChatNav />
<div style={{ flex: 1, padding: '24px 20px', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 0 }}>
{/* AI 大头像 */}
<div style={{ width: 72, height: 72, borderRadius: 36, background: `linear-gradient(135deg, ${T.pri}, ${T.priD})`, display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: `0 8px 24px rgba(196, 98, 58, 0.25)` }}>
<span style={{ color: T.white, fontSize: 32, fontWeight: 600, fontFamily: T.serif }}></span>
</div>
{/* 问候语 */}
<div style={{ marginTop: 16, textAlign: 'center' }}>
<div style={{ fontSize: 17, fontWeight: 600, color: T.tx }}>您好我是小华</div>
<div style={{ fontSize: 13, color: T.tx3, marginTop: 6, lineHeight: 1.6 }}>您的专属健康助手随时为您解答<br/>健康问题预约服务报告解读等</div>
</div>
{/* 分割线 */}
<div style={{ width: 32, height: 1, background: T.bd, margin: '20px 0 16px' }} />
{/* 快捷问题 */}
<div style={{ fontSize: 12, color: T.tx3, marginBottom: 12 }}>您可能想问</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, justifyContent: 'center', maxWidth: 320 }}>
{quickActions.map((a, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 14px', background: T.card, borderRadius: T.r, border: `1px solid ${T.bdL}`, fontSize: 13, color: T.tx2, cursor: 'pointer' }}>
<span style={{ fontSize: 15 }}>{a.icon}</span>
<span>{a.label}</span>
</div>
))}
</div>
</div>
<ChatBar />
</div>
);
}
// ─── 占位组件(后续步骤填充)───
// ─── 中屏:对话进行中 ───
function ActiveChat() {
const messages = [
{ from: 'ai', text: '您好!我是健康助手小华,请问有什么可以帮助您的?' },
{ from: 'user', text: '我最近体检报告出来了,想了解一下指标情况' },
{ from: 'ai', text: '好的,我来帮您解读最近的体检报告。请稍等,正在为您查询…', card: true },
{ from: 'user', text: '好的' },
{ from: 'ai', text: '已为您找到最近的体检报告2026-05-10。整体来看各项指标基本正常有几个需要关注的点\n\n1. 空腹血糖略偏高6.2 mmol/L建议关注饮食\n2. 维生素D偏低可适当补充\n3. 其余指标均在正常范围内\n\n如需详细解读某项指标请告诉我。' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<ChatNav />
<div style={{ flex: 1, padding: '16px 20px', display: 'flex', flexDirection: 'column', gap: 4, overflow: 'auto' }}>
{/* 日期标记 */}
<div style={{ textAlign: 'center', margin: '4px 0 12px' }}>
<span style={{ fontSize: 11, color: T.tx3, background: T.surface, padding: '3px 12px', borderRadius: 10 }}>今天 09:30</span>
</div>
{messages.map((msg, i) => (
<React.Fragment key={i}>
{/* 时间间隔 */}
{i === 3 && (
<div style={{ textAlign: 'center', margin: '8px 0' }}>
<span style={{ fontSize: 11, color: T.tx3, background: T.surface, padding: '3px 12px', borderRadius: 10 }}>09:35</span>
</div>
)}
{msg.from === 'ai' ? (
/* AI 消息 — 左对齐 */
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, marginBottom: 4 }}>
<AiAvatar />
<div style={{ maxWidth: 260 }}>
<div style={{
background: T.card,
borderRadius: '4px 16px 16px 16px',
padding: '10px 14px',
boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
}}>
{msg.card ? (
/* 带卡片的内容 */
<div>
<span style={{ fontSize: 15, color: T.tx, lineHeight: 1.6 }}>{msg.text}</span>
{/* 报告卡片 */}
<div style={{ marginTop: 8, padding: '10px 12px', background: T.surface, borderRadius: T.rSm, borderLeft: `3px solid ${T.pri}` }}>
<div style={{ fontSize: 13, fontWeight: 600, color: T.tx }}>2026年度健康体检</div>
<div style={{ fontSize: 11, color: T.tx3, marginTop: 2 }}>2026-05-10 · 综合</div>
</div>
</div>
) : (
<span style={{ fontSize: 15, color: T.tx, lineHeight: 1.6, whiteSpace: 'pre-line' }}>{msg.text}</span>
)}
</div>
</div>
</div>
) : (
/* 用户消息 — 右对齐 */
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 4 }}>
<div style={{ maxWidth: 260, background: T.priL, borderRadius: '16px 4px 16px 16px', padding: '10px 14px' }}>
<span style={{ fontSize: 15, color: T.tx, lineHeight: 1.6 }}>{msg.text}</span>
</div>
</div>
)}
</React.Fragment>
))}
{/* AI 正在输入 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 4 }}>
<AiAvatar />
<div style={{ background: T.card, borderRadius: '4px 16px 16px 16px', padding: '10px 16px', boxShadow: '0 1px 3px rgba(0,0,0,0.04)' }}>
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<div style={{ width: 6, height: 6, borderRadius: 3, background: T.tx3, animation: 'pulse 1.4s infinite' }} />
<div style={{ width: 6, height: 6, borderRadius: 3, background: T.tx3, animation: 'pulse 1.4s infinite 0.2s' }} />
<div style={{ width: 6, height: 6, borderRadius: 3, background: T.tx3, animation: 'pulse 1.4s infinite 0.4s' }} />
</div>
</div>
</div>
</div>
<ChatBar />
</div>
);
}
// ─── 右屏:个人中心 · 通知入口 ───
function ProfileWithNotify() {
const user = { name: '张三', phone: '138****6789' };
const menuGroups = [
{
label: null,
items: [
{ icon: '🔔', label: '消息通知', badge: 3, color: T.pri },
],
},
{
label: '健康管理',
items: [
{ icon: '📊', label: '健康记录', color: '#C4623A' },
{ icon: '📄', label: '我的报告', color: '#5B7A5E' },
{ icon: '🤖', label: 'AI 分析', color: '#C4873A' },
],
},
{
label: '就诊服务',
items: [
{ icon: '📅', label: '我的预约', color: '#3A6B8C' },
{ icon: '🔄', label: '我的随访', color: '#7A5B7A' },
],
},
{
label: '生活服务',
items: [
{ icon: '🎁', label: '积分商城', color: '#C4873A' },
{ icon: '🏃', label: '线下活动', color: '#5B7A5E' },
],
},
{
label: '账号',
items: [
{ icon: '👤', label: '就诊人管理', color: T.tx2 },
{ icon: '⚙️', label: '设置', color: T.tx2 },
],
},
];
return (
<div style={{ height: '100%', background: T.bg }}>
{/* 用户卡片 */}
<div style={{ padding: '20px 20px 16px', display: 'flex', alignItems: 'center', gap: 14 }}>
<div style={{ width: 56, height: 56, borderRadius: 28, background: `linear-gradient(135deg, ${T.pri}, ${T.priD})`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ color: T.white, fontSize: 24, fontWeight: 600 }}>{user.name.charAt(0)}</span>
</div>
<div>
<div style={{ fontSize: 18, fontWeight: 600, color: T.tx }}>{user.name}</div>
<div style={{ fontSize: 13, color: T.tx3, marginTop: 2 }}>{user.phone}</div>
</div>
<div style={{ marginLeft: 'auto', color: T.tx3, fontSize: 20 }}></div>
</div>
{/* 统计行 */}
<div style={{ display: 'flex', gap: 10, padding: '0 20px 16px' }}>
{[
{ label: '健康积分', value: '1,280' },
{ label: '连续打卡', value: '15天' },
].map((s, i) => (
<div key={i} style={{ flex: 1, background: T.card, borderRadius: T.r, padding: '14px 16px' }}>
<div style={{ fontFamily: T.serif, fontSize: 22, fontWeight: 700, color: T.pri }}>{s.value}</div>
<div style={{ fontSize: 12, color: T.tx3, marginTop: 2 }}>{s.label}</div>
</div>
))}
</div>
{/* 菜单列表 */}
<div style={{ padding: '0 20px' }}>
{menuGroups.map((group, gi) => (
<div key={gi} style={{ marginBottom: 12 }}>
{group.label && <div style={{ fontSize: 12, color: T.tx3, marginBottom: 6, paddingLeft: 4 }}>{group.label}</div>}
<div style={{ background: T.card, borderRadius: T.r, overflow: 'hidden' }}>
{group.items.map((item, ii) => (
<div key={ii} style={{
display: 'flex', alignItems: 'center', padding: '14px 16px',
borderBottom: ii < group.items.length - 1 ? `1px solid ${T.bdL}` : 'none',
cursor: 'pointer',
}}>
{/* 图标 */}
<div style={{
width: 32, height: 32, borderRadius: T.rXs,
background: `${item.color}15`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 16, marginRight: 12,
}}>
{item.icon}
</div>
{/* 标签 */}
<span style={{ flex: 1, fontSize: 15, color: T.tx }}>{item.label}</span>
{/* 角标 */}
{item.badge && (
<div style={{
minWidth: 18, height: 18, borderRadius: 9,
background: T.dan, display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: '0 5px', marginRight: 8,
}}>
<span style={{ fontSize: 11, color: T.white, fontWeight: 600 }}>{item.badge}</span>
</div>
)}
{/* 箭头 */}
<span style={{ color: T.tx3, fontSize: 16 }}></span>
</div>
))}
</div>
</div>
))}
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>

View File

@@ -0,0 +1,462 @@
<!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; }
.note { color: #666; font-size: 12px; max-width: 900px; text-align: center; line-height: 1.8; }
.screens { display: flex; gap: 40px; 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">预约列表 + 预约创建 + 预约详情 — 温润东方风设计系统,三屏并排展示完整挂号流程</div>
<div id="root"></div>
<script type="text/babel">
// ─── iOS 设备框 ───
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 = 393, height = 852, 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 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>
);
}
// ─── 设计 Token ───
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}`, background: T.bg, position: 'relative',
}}>
<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 StatusTag({ label, variant }) {
const styles = {
pending: { bg: T.accL, color: T.acc },
done: { bg: T.surface, color: T.tx3 },
cancel: { bg: T.danL, color: T.dan },
};
const s = styles[variant] || styles.pending;
return (
<span style={{
display: 'inline-block', padding: '3px 10px', borderRadius: 20,
background: s.bg, color: s.color, fontSize: 12, fontWeight: 600,
}}>
{label}
</span>
);
}
// ─── 屏幕一:预约列表页 ───
function AppointmentList() {
const tabs = ['全部', '待就诊', '已完成', '已取消'];
const activeTab = 0;
const appointments = [
{ doctor: '李医生', dept: '肾内科', status: 'pending', statusLabel: '待就诊',
date: '5月10日 周六', time: '09:00 - 09:30', location: '门诊楼3层 心内科诊室' },
{ doctor: '王医生', dept: '心内科', status: 'done', statusLabel: '已完成',
date: '5月6日 周二', time: '14:00 - 14:30', location: '门诊楼2层 心内科诊室' },
{ doctor: '张医生', dept: '全科', status: 'cancel', statusLabel: '已取消',
date: '4月28日 周一', time: '10:00 - 10:30', location: '' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="我的预约" />
{/* 状态筛选 Tab */}
<div style={{
display: 'flex', gap: 8, padding: '14px 20px', background: T.bg,
}}>
{tabs.map((tab, i) => (
<div key={i} style={{
flex: 1, height: 36, borderRadius: 20, display: 'flex', alignItems: 'center',
justifyContent: 'center', fontSize: 14, fontWeight: 600,
background: i === activeTab ? T.pri : T.surface,
color: i === activeTab ? '#fff' : T.tx2,
cursor: 'pointer',
}}>
{tab}
</div>
))}
</div>
{/* 卡片列表 */}
<div style={{ flex: 1, overflow: 'auto', padding: '4px 20px 100px' }}>
{appointments.map((a, i) => (
<div key={i} style={{
background: T.card, borderRadius: T.r, padding: '18px 20px',
marginBottom: 12, boxShadow: '0 2px 12px rgba(45,42,38,0.06)',
}}>
{/* 头部:医生+科室 + 状态 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{
width: 40, height: 40, borderRadius: 20,
background: `linear-gradient(135deg, ${T.priL} 0%, ${T.pri} 100%)`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<span style={{ fontFamily: T.serif, fontSize: 16, fontWeight: 700, color: '#fff' }}>
{a.doctor[0]}
</span>
</div>
<div>
<div style={{ fontSize: 16, fontWeight: 600, color: T.tx, fontFamily: T.sans }}>
{a.doctor}
</div>
<div style={{ fontSize: 13, color: T.tx3, marginTop: 2 }}>{a.dept}</div>
</div>
</div>
<StatusTag label={a.statusLabel} variant={a.status} />
</div>
{/* 时间 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<rect x="3" y="4" width="18" height="18" rx="2" stroke={T.tx3} strokeWidth="1.5"/>
<path d="M3 10h18M16 2v4M8 2v4" stroke={T.tx3} strokeWidth="1.5" strokeLinecap="round"/>
</svg>
<span style={{ fontSize: 14, color: T.tx2 }}>{a.date} {a.time}</span>
</div>
{/* 地点 */}
{a.location && (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M12 13a3 3 0 100-6 3 3 0 000 6z" stroke={T.tx3} strokeWidth="1.5"/>
<path d="M12 22s-8-5.5-8-11a8 8 0 1116 0c0 5.5-8 11-8 11z" stroke={T.tx3} strokeWidth="1.5"/>
</svg>
<span style={{ fontSize: 14, color: T.tx2 }}>{a.location}</span>
</div>
)}
</div>
))}
</div>
{/* 浮动按钮 */}
<div style={{
position: 'absolute', bottom: 50, right: 24, width: 56, height: 56,
borderRadius: 28, background: T.pri, display: 'flex', alignItems: 'center',
justifyContent: 'center', boxShadow: `0 4px 20px rgba(196,98,58,0.4)`, zIndex: 5,
}}>
<svg width="26" height="26" viewBox="0 0 24 24" fill="none">
<path d="M12 5v14M5 12h14" stroke="#fff" strokeWidth="2.5" strokeLinecap="round"/>
</svg>
</div>
</div>
);
}
// ─── 屏幕二:预约创建页 ───
function AppointmentCreate() {
const timeSlots = {
morning: [
{ time: '09:00', selected: true },
{ time: '09:30', selected: false },
{ time: '10:00', selected: false },
],
afternoon: [
{ time: '14:00', selected: false },
{ time: '14:30', selected: false, disabled: true },
{ time: '15:00', selected: false, disabled: true },
],
};
function FormField({ label, placeholder, value, showArrow = true }) {
return (
<div style={{ marginBottom: 18 }}>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 6, fontWeight: 500 }}>{label}</div>
<div style={{
height: 56, background: T.card, border: `1.5px solid ${T.bd}`, borderRadius: T.r,
display: 'flex', alignItems: 'center', padding: '0 16px', justifyContent: 'space-between',
}}>
<span style={{ fontSize: 16, color: value ? T.tx : T.tx3 }}>{value || placeholder}</span>
{showArrow && (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M6 9l6 6 6-6" stroke={T.tx3} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)}
</div>
</div>
);
}
function TimeSlot({ time, selected, disabled }) {
return (
<div style={{
flex: 1, height: 44, borderRadius: T.rSm,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 15, fontWeight: 600,
background: disabled ? T.surface : selected ? T.pri : T.card,
color: disabled ? T.tx3 + '88' : selected ? '#fff' : T.tx2,
border: disabled ? 'none' : selected ? 'none' : `1.5px solid ${T.bd}`,
opacity: disabled ? 0.5 : 1,
}}>
{time}
</div>
);
}
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="预约挂号" />
<div style={{ flex: 1, overflow: 'auto', padding: '20px 20px 120px' }}>
<FormField label="选择科室" placeholder="请选择科室" />
<FormField label="选择医生" placeholder="请选择医生" />
<FormField label="选择日期" placeholder="请选择日期" />
{/* 时段选择 */}
<div style={{ marginBottom: 18 }}>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 6, fontWeight: 500 }}>选择时段</div>
{/* 上午 */}
<div style={{ marginBottom: 10 }}>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 8 }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" style={{ verticalAlign: 'middle', marginRight: 4 }}>
<circle cx="12" cy="12" r="5" stroke={T.wrn} strokeWidth="1.5"/>
<path d="M12 2v2M12 20v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M2 12h2M20 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" stroke={T.wrn} strokeWidth="1.5" strokeLinecap="round"/>
</svg>
上午
</div>
<div style={{ display: 'flex', gap: 10 }}>
{timeSlots.morning.map((s, i) => <TimeSlot key={i} {...s} />)}
</div>
</div>
{/* 下午 */}
<div>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 8 }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" style={{ verticalAlign: 'middle', marginRight: 4 }}>
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" stroke={T.tx3} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
下午
</div>
<div style={{ display: 'flex', gap: 10 }}>
{timeSlots.afternoon.map((s, i) => <TimeSlot key={i} {...s} />)}
</div>
</div>
</div>
<FormField label="就诊人" placeholder="请选择就诊人" value="张三" />
{/* 备注 */}
<div style={{ marginBottom: 18 }}>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 6, fontWeight: 500 }}>备注</div>
<div style={{
minHeight: 100, background: T.card, border: `1.5px solid ${T.bd}`, borderRadius: T.r,
padding: '14px 16px', fontSize: 16, color: T.tx3, lineHeight: 1.6,
}}>
请输入病情描述选填
</div>
</div>
</div>
{/* 底部按钮 */}
<div style={{
position: 'absolute', bottom: 34, left: 0, right: 0,
padding: '12px 20px', background: T.bg,
borderTop: `1px solid ${T.bdL}`,
}}>
<div style={{
height: 54, borderRadius: 16, background: T.pri,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', fontSize: 18, fontWeight: 600,
boxShadow: `0 4px 16px rgba(196,98,58,0.3)`,
}}>
确认预约
</div>
</div>
</div>
);
}
// ─── 屏幕三:预约详情页 ───
function AppointmentDetail() {
const infoItems = [
{ label: '科室', value: '肾内科' },
{ label: '医生', value: '李明华 主任医师' },
{ label: '日期', value: '2025年5月10日周六' },
{ label: '时间', value: '09:00 - 09:30' },
{ label: '地点', value: '门诊楼3层 心内科诊室305' },
{ label: '就诊人', value: '张三' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="预约详情" />
<div style={{ flex: 1, overflow: 'auto', padding: '16px 20px 100px' }}>
{/* 状态大卡片 */}
<div style={{
background: T.card, borderRadius: T.r, padding: '24px 20px',
boxShadow: '0 2px 12px rgba(45,42,38,0.06)', marginBottom: 16,
display: 'flex', flexDirection: 'column', alignItems: 'center',
}}>
{/* 状态图标 */}
<div style={{
width: 56, height: 56, borderRadius: 28, background: T.accL,
display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 12,
}}>
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
<rect x="3" y="4" width="18" height="18" rx="2" stroke={T.acc} strokeWidth="1.8"/>
<path d="M3 10h18M16 2v4M8 2v4" stroke={T.acc} strokeWidth="1.8" strokeLinecap="round"/>
<path d="M8 14l2 2 4-4" stroke={T.acc} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
<StatusTag label="待就诊" variant="pending" />
</div>
{/* 信息列表 */}
<div style={{
background: T.card, borderRadius: T.r, padding: '6px 20px',
boxShadow: '0 2px 12px rgba(45,42,38,0.06)', marginBottom: 16,
}}>
{infoItems.map((item, i) => (
<div key={i} style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '15px 0',
borderBottom: i < infoItems.length - 1 ? `1px solid ${T.bdL}` : 'none',
}}>
<span style={{ fontSize: 14, color: T.tx3, minWidth: 56 }}>{item.label}</span>
<span style={{ fontSize: 15, color: T.tx, fontWeight: 500, textAlign: 'right' }}>{item.value}</span>
</div>
))}
</div>
{/* 温馨提示 */}
<div style={{
background: T.accL, borderRadius: T.r, padding: '16px 18px',
display: 'flex', gap: 12,
}}>
<div style={{ flexShrink: 0, marginTop: 1 }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke={T.acc} strokeWidth="1.5"/>
<path d="M12 16v-4M12 8h.01" stroke={T.acc} strokeWidth="2" strokeLinecap="round"/>
</svg>
</div>
<div>
<div style={{ fontSize: 14, fontWeight: 600, color: T.acc, marginBottom: 4 }}>温馨提示</div>
<div style={{ fontSize: 13, color: T.acc, lineHeight: 1.7, opacity: 0.85 }}>
请提前15分钟到达携带医保卡和既往病历
</div>
</div>
</div>
</div>
{/* 底部操作 */}
<div style={{
position: 'absolute', bottom: 34, left: 0, right: 0,
padding: '12px 20px', background: T.bg,
borderTop: `1px solid ${T.bdL}`,
display: 'flex', gap: 12,
}}>
<div style={{
flex: 1, height: 54, borderRadius: 16,
border: `1.5px solid ${T.dan}`, background: 'transparent',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: T.dan, fontSize: 16, fontWeight: 600,
}}>
取消预约
</div>
<div style={{
flex: 1, height: 54, borderRadius: 16, background: T.pri,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', fontSize: 16, fontWeight: 600,
boxShadow: `0 4px 16px rgba(196,98,58,0.3)`,
}}>
导航到科室
</div>
</div>
</div>
);
}
// ─── 渲染 ───
function App() {
return (
<div className="screens">
<div className="screen-wrap">
<span className="screen-label">预约列表</span>
<IosFrame time="9:41" battery={85}>
<AppointmentList />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">预约创建</span>
<IosFrame time="9:42" battery={84}>
<AppointmentCreate />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">预约详情</span>
<IosFrame time="9:43" battery={83}>
<AppointmentDetail />
</IosFrame>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>

View File

@@ -0,0 +1,430 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HMS 小程序 — 文章与AI报告</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 小程序 · 文章与 AI 报告</div>
<div class="note">温润东方风设计系统 — 文章列表/详情 + AI 报告列表/详情,四屏并排展示。内容为王,阅读体验优先。</div>
<div id="root"></div>
<script type="text/babel">
// ─── iOS 设备框 ───
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 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>
);
}
// ─── 设计 Token ───
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>
);
}
// ─── 屏幕1文章列表 ───
function ArticleList() {
const categories = ['全部', '高血压', '糖尿病', '肾病', '透析'];
const articles = [
{ title: '高血压患者日常饮食指南', source: '健康时报', date: '5月8日', summary: '科学饮食是控制血压的关键,低盐、低脂、高纤维...' },
{ title: '血液透析的日常注意事项', source: '肾病频道', date: '5月7日', summary: '透析患者需要特别注意水分控制与营养补充...' },
{ title: '糖尿病患者如何科学运动', source: '医学前沿', date: '5月5日', summary: '规律运动有助控制血糖,但需注意运动强度...' },
{ title: '肾脏健康的十个信号', source: '健康管理', date: '5月3日', summary: '早期发现肾脏问题至关重要,关注这些预警信号...' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="健康资讯" />
{/* 分类标签 */}
<div style={{
display: 'flex', gap: 8, padding: '12px 16px', flexShrink: 0,
overflowX: 'auto', background: T.bg
}}>
{categories.map((cat, i) => (
<div key={cat} style={{
padding: '8px 18px', borderRadius: 20, flexShrink: 0, fontSize: 14,
fontWeight: i === 0 ? 600 : 400,
background: i === 0 ? T.pri : T.surface,
color: i === 0 ? '#fff' : T.tx2,
fontFamily: T.sans,
boxShadow: i === 0 ? `0 2px 8px rgba(196,98,58,0.2)` : 'none',
transition: 'all 0.2s',
}}>{cat}</div>
))}
</div>
{/* 文章列表 */}
<div style={{ flex: 1, overflow: 'auto', padding: '0 16px 16px' }}>
{articles.map((a, i) => (
<div key={i} style={{
background: T.card, borderRadius: T.r, padding: 14, marginBottom: 12,
display: 'flex', gap: 14,
boxShadow: '0 1px 4px rgba(0,0,0,0.04)',
}}>
{/* 缩略图占位 */}
<div style={{
width: 80, height: 80, borderRadius: T.rXs, flexShrink: 0,
background: T.surface, display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<span style={{ fontSize: 12, color: T.tx3 }}>配图</span>
</div>
{/* 文字区 */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', minWidth: 0 }}>
<div>
<div style={{
fontFamily: T.serif, fontSize: 16, fontWeight: 700, color: T.tx,
lineHeight: 1.35, marginBottom: 4,
display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden',
}}>{a.title}</div>
<div style={{
fontSize: 13, color: T.tx2, lineHeight: 1.4,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
}}>{a.summary}</div>
</div>
<div style={{ display: 'flex', gap: 12, fontSize: 12, color: T.tx3 }}>
<span>{a.source}</span>
<span>{a.date}</span>
</div>
</div>
</div>
))}
</div>
</div>
);
}
// ─── 屏幕2文章详情 ───
function ArticleDetail() {
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column', position: 'relative' }}>
<NavBar title="文章详情" />
<div style={{ flex: 1, overflow: 'auto', padding: '20px 16px 80px' }}>
{/* 文章标题 */}
<div style={{
fontFamily: T.serif, fontSize: 22, fontWeight: 700, color: T.tx,
lineHeight: 1.4, marginBottom: 12,
}}>高血压患者日常饮食指南</div>
{/* 元信息 */}
<div style={{ display: 'flex', gap: 16, fontSize: 13, color: T.tx3, marginBottom: 16 }}>
<span>健康时报</span>
<span>2026年5月8日</span>
<span>1.2k 阅读</span>
</div>
{/* 分隔线 */}
<div style={{ height: 1, background: T.bdL, marginBottom: 20 }} />
{/* 正文 */}
<div style={{ fontSize: 15, color: T.tx2, lineHeight: 1.8, fontFamily: T.sans }}>
<p style={{ marginBottom: 16, textIndent: '2em' }}>
高血压是一种常见的慢性疾病合理的饮食习惯对于控制血压至关重要研究表明低盐低脂高纤维的饮食模式能够有效降低血压水平减少心血管疾病的发生风险
</p>
<p style={{ marginBottom: 16, textIndent: '2em' }}>
建议每日食盐摄入量控制在5克以内多食用新鲜蔬果全谷物和优质蛋白避免高钠加工食品如腌制品方便面等同时适量补充富含钾钙的食物如香蕉菠菜和牛奶
</p>
<p style={{ textIndent: '2em' }}>
除了饮食调节还应注意控制总热量摄入维持健康体重每周进行至少150分钟的中等强度有氧运动如快走游泳等定期监测血压遵医嘱服药...
</p>
</div>
</div>
{/* 底部浮动栏 */}
<div style={{
position: 'absolute', bottom: 34, left: 0, right: 0, height: 60,
background: T.card, borderTop: `1px solid ${T.bdL}`,
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 32,
padding: '0 16px',
}}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" stroke={T.tx3} strokeWidth="1.8" fill="none"/>
</svg>
<span style={{ fontSize: 11, color: T.tx3 }}>收藏</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
<path d="M4 12v8a2 2 0 002 2h12a2 2 0 002-2v-8" stroke={T.tx3} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
<polyline points="16,6 12,2 8,6" stroke={T.tx3} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
<line x1="12" y1="2" x2="12" y2="15" stroke={T.tx3} strokeWidth="1.8" strokeLinecap="round"/>
</svg>
<span style={{ fontSize: 11, color: T.tx3 }}>分享</span>
</div>
</div>
</div>
);
}
// ─── 屏幕3AI报告列表 ───
function AIReportList() {
const reports = [
{ type: '综合评估', typeColor: T.acc, typeBg: T.accL, title: '5月健康综合评估', date: '5月8日', summary: '整体健康状况良好,血压需持续关注,建议保持低盐饮食...' },
{ type: '化验解读', typeColor: T.pri, typeBg: T.priL, title: '血常规化验解读', date: '5月6日', summary: '血红蛋白略低,建议补充铁剂,注意营养均衡...' },
{ type: '趋势分析', typeColor: T.wrn, typeBg: T.wrnL, title: '近30天血压趋势', date: '5月1日', summary: '收缩压呈下降趋势,控制效果良好,请继续保持...' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="AI 健康报告" />
<div style={{ flex: 1, overflow: 'auto', padding: '16px' }}>
{reports.map((r, i) => (
<div key={i} style={{
background: T.card, borderRadius: T.r, padding: 16, marginBottom: 12,
boxShadow: '0 1px 4px rgba(0,0,0,0.04)',
position: 'relative',
}}>
{/* 类型标签 */}
<div style={{
display: 'inline-block', padding: '3px 10px', borderRadius: 6,
background: r.typeBg, color: r.typeColor, fontSize: 12, fontWeight: 600,
marginBottom: 10,
}}>{r.type}</div>
{/* 标题 */}
<div style={{
fontFamily: T.serif, fontSize: 16, fontWeight: 700, color: T.tx,
marginBottom: 6, lineHeight: 1.35,
}}>{r.title}</div>
{/* 日期 */}
<div style={{ fontSize: 12, color: T.tx3, marginBottom: 8 }}>{r.date}</div>
{/* 摘要 */}
<div style={{
fontSize: 13, color: T.tx2, lineHeight: 1.5,
display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden',
}}>{r.summary}</div>
{/* 右箭头 */}
<svg style={{ position: 'absolute', right: 16, top: '50%', transform: 'translateY(-50%)' }} width="18" height="18" viewBox="0 0 24 24" fill="none">
<path d="M9 5l7 7-7 7" stroke={T.tx3} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
))}
</div>
</div>
);
}
// ─── 屏幕4AI报告详情 ───
function AIReportDetail() {
const score = 85;
const circumference = 2 * Math.PI * 52;
const offset = circumference - (score / 100) * circumference;
const indicators = [
{ label: '血压', value: '130/85 mmHg', status: '偏高', statusType: 'warn' },
{ label: '血糖', value: '5.6 mmol/L', status: '正常', statusType: 'good' },
{ label: '血红蛋白', value: '110 g/L', status: '偏低', statusType: 'warn' },
{ label: '心率', value: '72 bpm', status: '正常', statusType: 'good' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column', position: 'relative' }}>
<NavBar title="报告详情" />
<div style={{ flex: 1, overflow: 'auto', padding: '16px 16px 80px' }}>
{/* 报告头部 */}
<div style={{
background: T.card, borderRadius: T.r, padding: 20, marginBottom: 12,
boxShadow: '0 1px 4px rgba(0,0,0,0.04)', textAlign: 'center',
}}>
<div style={{
display: 'inline-block', padding: '4px 14px', borderRadius: 6,
background: T.accL, color: T.acc, fontSize: 13, fontWeight: 600,
marginBottom: 12,
}}>综合评估</div>
<div style={{
fontFamily: T.serif, fontSize: 20, fontWeight: 700, color: T.tx,
marginBottom: 4,
}}>5月健康综合评估</div>
<div style={{ fontSize: 13, color: T.tx3 }}>2026年5月8日</div>
</div>
{/* 综合评分环 */}
<div style={{
background: T.card, borderRadius: T.r, padding: '24px 20px', marginBottom: 12,
boxShadow: '0 1px 4px rgba(0,0,0,0.04)', display: 'flex', flexDirection: 'column',
alignItems: 'center',
}}>
<svg width="140" height="140" viewBox="0 0 140 140" style={{ marginBottom: 12 }}>
{/* 背景圆 */}
<circle cx="70" cy="70" r="52" fill="none" stroke={T.bdL} strokeWidth="10" />
{/* 进度圆 */}
<circle cx="70" cy="70" r="52" fill="none"
stroke={T.acc} strokeWidth="10" strokeLinecap="round"
strokeDasharray={circumference} strokeDashoffset={offset}
transform="rotate(-90 70 70)"
style={{ transition: 'stroke-dashoffset 1s ease' }}
/>
{/* 中心文字 */}
<text x="70" y="64" textAnchor="middle" fontFamily={T.serif} fontSize="36" fontWeight="700" fill={T.tx}>{score}</text>
<text x="70" y="86" textAnchor="middle" fontFamily={T.sans} fontSize="14" fill={T.tx3}>/100</text>
</svg>
<div style={{
fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.acc,
}}>良好</div>
</div>
{/* 分析结论 */}
<div style={{
background: T.accL, borderRadius: T.r, padding: '14px 16px', marginBottom: 12,
borderLeft: `4px solid ${T.acc}`,
}}>
<div style={{ fontFamily: T.serif, fontSize: 14, fontWeight: 700, color: T.acc, marginBottom: 8 }}>分析结论</div>
<div style={{ fontSize: 14, color: T.tx2, lineHeight: 1.7 }}>
<div style={{ marginBottom: 6, display: 'flex', gap: 6 }}>
<span style={{ color: T.acc, fontWeight: 700 }}>&bull;</span>
<span>血压控制良好建议继续保持低盐饮食</span>
</div>
<div style={{ display: 'flex', gap: 6 }}>
<span style={{ color: T.acc, fontWeight: 700 }}>&bull;</span>
<span>血红蛋白略有下降建议补充铁剂</span>
</div>
</div>
</div>
{/* 详细指标 */}
<div style={{
background: T.card, borderRadius: T.r, padding: 16, marginBottom: 12,
boxShadow: '0 1px 4px rgba(0,0,0,0.04)',
}}>
<div style={{ fontFamily: T.serif, fontSize: 16, fontWeight: 700, color: T.tx, marginBottom: 14 }}>详细指标</div>
{indicators.map((ind, i) => (
<div key={i} style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '12px 0',
borderBottom: i < indicators.length - 1 ? `1px solid ${T.bdL}` : 'none',
}}>
<span style={{ fontSize: 15, color: T.tx, fontWeight: 500, minWidth: 72 }}>{ind.label}</span>
<span style={{ fontSize: 14, color: T.tx2, flex: 1, textAlign: 'center' }}>{ind.value}</span>
<div style={{
padding: '3px 10px', borderRadius: 6, fontSize: 12, fontWeight: 600,
background: ind.statusType === 'good' ? T.accL : T.wrnL,
color: ind.statusType === 'good' ? T.acc : T.wrn,
}}>{ind.status}</div>
</div>
))}
</div>
</div>
{/* 底部按钮 */}
<div style={{
position: 'absolute', bottom: 34, left: 0, right: 0,
padding: '12px 16px', background: T.bg,
}}>
<div style={{
height: 50, borderRadius: T.r, background: T.pri,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', fontSize: 16, fontWeight: 600, fontFamily: T.sans,
boxShadow: `0 4px 16px rgba(196,98,58,0.3)`,
gap: 8,
}}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<path d="M4 12v8a2 2 0 002 2h12a2 2 0 002-2v-8" stroke="#fff" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
<polyline points="16,6 12,2 8,6" stroke="#fff" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
<line x1="12" y1="2" x2="12" y2="15" stroke="#fff" strokeWidth="1.8" strokeLinecap="round"/>
</svg>
分享给医生
</div>
</div>
</div>
);
}
// ─── 渲染 ───
function App() {
return (
<div className="screens">
<div className="screen-wrap">
<span className="screen-label">文章列表</span>
<IosFrame time="9:41" battery={85}>
<ArticleList />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">文章详情</span>
<IosFrame time="9:42" battery={84}>
<ArticleDetail />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">AI 报告列表</span>
<IosFrame time="9:43" battery={83}>
<AIReportList />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">AI 报告详情</span>
<IosFrame time="9:44" battery={82}>
<AIReportDetail />
</IosFrame>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>

328
docs/design/mp-05-mall.html Normal file
View File

@@ -0,0 +1,328 @@
<!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: 48px; 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">温润东方风设计系统 — 积分商城主页。从"我的"页面进入的子页面,展示积分余额、快捷操作、分类筛选和商品兑换网格。</div>
<div id="root"></div>
<script type="text/babel">
// ─── iOS 设备框 ───
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 = 393, height = 852, 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 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>
);
}
// ─── 设计 Token ───
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 IconCheckin({ size = 24, color = '#fff' }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"/>
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
);
}
function IconTask({ size = 24, color = '#fff' }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<path d="M9 12l2 2 4-4"/>
</svg>
);
}
function IconHistory({ size = 24, color = '#fff' }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"/>
<polyline points="12 6 12 12 16 14"/>
</svg>
);
}
// ─── 商品占位图 SVG ───
function ProductPlaceholder({ label }) {
const iconMap = {
'健康体检套餐': (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#C4623A" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
</svg>
),
'血压计': (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#C4623A" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
</svg>
),
'维生素D3': (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#C4623A" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="8" y="2" width="8" height="4" rx="1"/>
<path d="M6 6h12v14a2 2 0 01-2 2H8a2 2 0 01-2-2V6z"/>
<path d="M10 11h4"/>
</svg>
),
'健康食谱书': (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#C4623A" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 19.5A2.5 2.5 0 016.5 17H20"/>
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/>
<path d="M8 7h8M8 11h6"/>
</svg>
),
'运动手环': (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#C4623A" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="7" y="2" width="10" height="20" rx="5"/>
<path d="M12 8v4l2 2"/>
</svg>
),
'保温杯': (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#C4623A" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M8 2h8v3H8z"/>
<path d="M7 5h10l-1 15H8L7 5z"/>
<path d="M10 9h4"/>
</svg>
),
};
return (
<div style={{ width: '100%', aspectRatio: '1', background: T.surface, borderRadius: T.rSm, display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative' }}>
{iconMap[label] || (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#C4623A" strokeWidth="1.5"><rect x="3" y="3" width="18" height="18" rx="3"/></svg>
)}
<div style={{ position: 'absolute', bottom: 6, left: 0, right: 0, textAlign: 'center', fontSize: 9, color: T.tx3, fontWeight: 500 }}>{label}</div>
</div>
);
}
// ─── 积分商城主页 ───
function MallPage() {
const categories = ['全部', '健康', '生活', '食品'];
const [activeCategory, setActiveCategory] = React.useState(0);
const quickActions = [
{ icon: <IconCheckin size={22} color="#fff" />, label: '签到打卡', bg: T.acc, shadow: 'rgba(91,122,94,0.3)' },
{ icon: <IconTask size={22} color="#fff" />, label: '积分任务', bg: T.pri, shadow: 'rgba(196,98,58,0.3)' },
{ icon: <IconHistory size={22} color="#fff" />, label: '兑换记录', bg: T.wrn, shadow: 'rgba(196,135,58,0.3)' },
];
const products = [
{ name: '健康体检套餐', points: 800, price: 299, tag: '热门' },
{ name: '血压计', points: 1200, price: 199 },
{ name: '维生素D3', points: 300, price: 89 },
{ name: '健康食谱书', points: 500, price: 68, tag: '新品' },
{ name: '运动手环', points: 2000, price: 399 },
{ name: '保温杯', points: 600, price: 128, tag: '新品' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
{/* ── 导航栏 ── */}
<div style={{ padding: '12px 20px 8px', display: 'flex', alignItems: 'center', gap: 8, background: T.bg }}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke={T.tx} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M15 18l-6-6 6-6"/>
</svg>
<div style={{ flex: 1, textAlign: 'center', fontFamily: T.serif, fontSize: 26, fontWeight: 700, color: T.tx, paddingRight: 24 }}>积分商城</div>
</div>
{/* ── 可滚动内容区 ── */}
<div style={{ flex: 1, overflow: 'auto', padding: '8px 20px 24px' }}>
{/* ── 积分卡片 ── */}
<div style={{
background: `linear-gradient(135deg, ${T.pri} 0%, ${T.priD} 100%)`,
borderRadius: T.r,
padding: '24px 24px 20px',
marginBottom: 20,
boxShadow: `0 8px 24px rgba(196,98,58,0.25)`,
position: 'relative',
overflow: 'hidden',
}}>
{/* 装饰圆 */}
<div style={{ position: 'absolute', top: -20, right: -20, width: 100, height: 100, borderRadius: 50, background: 'rgba(255,255,255,0.08)' }} />
<div style={{ position: 'absolute', bottom: -30, right: 40, width: 80, height: 80, borderRadius: 40, background: 'rgba(255,255,255,0.05)' }} />
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', position: 'relative', zIndex: 1 }}>
<div>
<div style={{ fontSize: 13, color: 'rgba(255,255,255,0.7)', marginBottom: 8, letterSpacing: 1 }}>我的积分</div>
<div style={{ fontFamily: T.serif, fontSize: 42, fontWeight: 700, color: '#fff', lineHeight: 1 }}>1,280</div>
</div>
<div style={{
display: 'flex', alignItems: 'center', gap: 4,
background: 'rgba(255,255,255,0.2)', borderRadius: 20,
padding: '6px 14px', fontSize: 13, color: '#fff', fontWeight: 500,
}}>
积分明细
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 18l6-6-6-6"/>
</svg>
</div>
</div>
</div>
{/* ── 快捷操作 ── */}
<div style={{ display: 'flex', justifyContent: 'space-around', marginBottom: 24 }}>
{quickActions.map((action, i) => (
<div key={i} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
<div style={{
width: 52, height: 52, borderRadius: 26,
background: action.bg,
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: `0 4px 12px ${action.shadow}`,
}}>
{action.icon}
</div>
<span style={{ fontSize: 12, color: T.tx2, fontWeight: 500 }}>{action.label}</span>
</div>
))}
</div>
{/* ── 分类标签 ── */}
<div style={{ display: 'flex', gap: 10, marginBottom: 20, overflowX: 'auto' }}>
{categories.map((cat, i) => (
<div
key={i}
onClick={() => setActiveCategory(i)}
style={{
padding: '7px 18px',
borderRadius: 20,
fontSize: 14,
fontWeight: activeCategory === i ? 600 : 400,
background: activeCategory === i ? T.pri : T.surface,
color: activeCategory === i ? '#fff' : T.tx2,
whiteSpace: 'nowrap',
transition: 'all 0.2s',
cursor: 'pointer',
}}
>
{cat}
</div>
))}
</div>
{/* ── 商品网格 ── */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
{products.map((product, i) => (
<div key={i} style={{
background: T.card,
borderRadius: T.rSm,
overflow: 'hidden',
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
}}>
{/* 商品图 */}
<div style={{ position: 'relative' }}>
<ProductPlaceholder label={product.name} />
{product.tag && (
<div style={{
position: 'absolute', top: 8, left: 8,
background: product.tag === '热门' ? T.dan : T.acc,
color: '#fff', fontSize: 10, fontWeight: 600,
padding: '2px 8px', borderRadius: 6,
}}>
{product.tag}
</div>
)}
</div>
{/* 商品信息 */}
<div style={{ padding: '10px 12px 14px' }}>
<div style={{
fontSize: 14, fontWeight: 600, color: T.tx,
lineHeight: 1.4, marginBottom: 8,
display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden',
height: 40,
}}>
{product.name}
</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
<span style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.pri }}>
{product.points}
</span>
<span style={{ fontSize: 11, color: T.pri, fontWeight: 500 }}>积分</span>
</div>
<div style={{ fontSize: 12, color: T.tx3, textDecoration: 'line-through', marginTop: 2 }}>
¥{product.price}
</div>
</div>
</div>
))}
</div>
{/* ── 底部留白 ── */}
<div style={{ height: 16 }} />
</div>
</div>
);
}
// ─── 渲染 ───
function App() {
return (
<div className="screens">
<div className="screen-wrap">
<span className="screen-label">积分商城 主页</span>
<IosFrame time="9:41" battery={85}>
<MallPage />
</IosFrame>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>

View File

@@ -0,0 +1,664 @@
<!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; }
.note { color: #666; font-size: 12px; max-width: 1200px; text-align: center; line-height: 1.8; }
.screens { display: flex; gap: 32px; 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">告警列表 + 日常监测 + 设备同步 + 体征录入 + 趋势分析 — 五屏并排展示健康分包核心功能</div>
<div id="root"></div>
<script type="text/babel">
// ─── iOS 设备框 ───
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 = 360, height = 780, 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 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>
);
}
// ─── 设计 Token ───
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}`, background: T.bg, position: 'relative',
}}>
<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 AlertList() {
const alerts = [
{ level: 'HIGH', desc: '收缩压持续偏高(>140mmHg', time: '今天 08:30', bg: T.danL, color: T.dan, label: '高' },
{ level: 'MEDIUM', desc: '血糖餐后值偏高8.2mmol/L', time: '昨天 19:00', bg: T.wrnL, color: T.wrn, label: '中' },
{ level: 'LOW', desc: '体重较上周增加 0.5kg', time: '3天前', bg: T.accL, color: T.acc, label: '低' },
{ level: 'LOW', desc: '心率偏低55bpm', time: '5天前', bg: T.accL, color: T.acc, label: '低' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="健康告警" />
<div style={{ flex: 1, overflow: 'auto', padding: '16px 16px 100px' }}>
{/* 统计概览 */}
<div style={{
display: 'flex', gap: 10, marginBottom: 16,
}}>
<div style={{ flex: 1, background: T.danL, borderRadius: T.rSm, padding: '12px 14px', textAlign: 'center' }}>
<div style={{ fontFamily: T.serif, fontSize: 22, fontWeight: 700, color: T.dan }}>1</div>
<div style={{ fontSize: 12, color: T.dan, marginTop: 2 }}>高级</div>
</div>
<div style={{ flex: 1, background: T.wrnL, borderRadius: T.rSm, padding: '12px 14px', textAlign: 'center' }}>
<div style={{ fontFamily: T.serif, fontSize: 22, fontWeight: 700, color: T.wrn }}>1</div>
<div style={{ fontSize: 12, color: T.wrn, marginTop: 2 }}>中级</div>
</div>
<div style={{ flex: 1, background: T.accL, borderRadius: T.rSm, padding: '12px 14px', textAlign: 'center' }}>
<div style={{ fontFamily: T.serif, fontSize: 22, fontWeight: 700, color: T.acc }}>2</div>
<div style={{ fontSize: 12, color: T.acc, marginTop: 2 }}>低级</div>
</div>
</div>
{/* 告警卡片列表 */}
{alerts.map((a, i) => (
<div key={i} style={{
background: T.card, borderRadius: T.r, padding: '16px 18px',
marginBottom: 10, boxShadow: '0 2px 12px rgba(45,42,38,0.06)',
display: 'flex', alignItems: 'center', gap: 14,
}}>
{/* 严重程度标签 */}
<div style={{
minWidth: 40, height: 40, borderRadius: T.rSm,
background: a.bg, display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>
<span style={{ fontSize: 14, fontWeight: 700, color: a.color }}>{a.label}</span>
</div>
{/* 内容 */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 15, fontWeight: 500, color: T.tx, lineHeight: 1.4, marginBottom: 4 }}>
{a.desc}
</div>
<div style={{ fontSize: 12, color: T.tx3 }}>{a.time}</div>
</div>
{/* 箭头 */}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
<path d="M9 5l7 7-7 7" stroke={T.tx3} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
))}
</div>
</div>
);
}
// ─── 屏幕二:日常监测 ───
function DailyMonitoring() {
const vitals = [
{ label: '血压', value: '130/85', unit: 'mmHg', status: '偏高', statusType: 'wrn' },
{ label: '心率', value: '72', unit: 'bpm', status: '正常', statusType: 'acc' },
{ label: '血糖', value: '5.6', unit: 'mmol/L', status: '正常', statusType: 'acc' },
{ label: '体重', value: '—', unit: 'kg', status: '未记录', statusType: 'empty' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="日常监测" />
<div style={{ flex: 1, overflow: 'auto', padding: '16px 16px 100px' }}>
{/* 日期选择横条 */}
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
background: T.card, borderRadius: T.r, padding: '12px 16px',
marginBottom: 16, boxShadow: '0 2px 12px rgba(45,42,38,0.06)',
}}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<path d="M15 19l-7-7 7-7" stroke={T.tx3} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: 17, fontWeight: 600, color: T.tx, fontFamily: T.serif }}>5月8日</div>
<div style={{ fontSize: 13, color: T.tx3, marginTop: 2 }}>周四</div>
</div>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<path d="M9 5l7 7-7 7" stroke={T.tx3} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
{/* 今日概览卡片 */}
<div style={{
background: T.card, borderRadius: T.r, padding: 20,
boxShadow: '0 2px 12px rgba(45,42,38,0.06)', marginBottom: 16,
display: 'flex', alignItems: 'center', gap: 18,
}}>
{/* 进度环 */}
<div style={{ width: 68, height: 68, position: 'relative', flexShrink: 0 }}>
<svg width="68" height="68" viewBox="0 0 68 68">
<circle cx="34" cy="34" r="29" fill="none" stroke={T.bd} strokeWidth="4.5" />
<circle cx="34" cy="34" r="29" fill="none" stroke={T.pri} strokeWidth="4.5"
strokeDasharray={`${0.75 * 2 * Math.PI * 29} ${0.25 * 2 * Math.PI * 29}`}
strokeDashoffset="0" strokeLinecap="round" transform="rotate(-90 34 34)" />
</svg>
<div style={{
position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
fontFamily: T.serif, fontSize: 17, fontWeight: 700, color: T.pri,
}}>3/4</div>
</div>
<div>
<div style={{ fontSize: 16, fontWeight: 600, color: T.tx, marginBottom: 6 }}>今日已记录 3 项体征</div>
<div style={{ display: 'flex', gap: 5, flexWrap: 'wrap' }}>
{['血压 ✓','心率 ✓','血糖 ✓','体重'].map((t, i) => (
<span key={i} style={{
fontSize: 11, padding: '3px 8px', borderRadius: 999,
background: i < 3 ? T.accL : T.surface,
color: i < 3 ? T.acc : T.tx3, fontWeight: 500,
}}>{t}</span>
))}
</div>
</div>
</div>
{/* 体征 2x2 网格 */}
<div style={{ fontFamily: T.serif, fontSize: 17, fontWeight: 700, color: T.tx, marginBottom: 10 }}>今日体征</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
{vitals.map((v, i) => (
<div key={i} style={{
background: T.card, borderRadius: T.r, padding: '14px 16px',
boxShadow: '0 1px 4px rgba(45,42,38,0.04)',
}}>
<div style={{ fontSize: 13, color: T.tx2, marginBottom: 6 }}>{v.label}</div>
<div style={{ display: 'flex', alignItems: 'baseline', marginBottom: 6 }}>
<span style={{
fontFamily: T.serif, fontSize: 28, fontWeight: 700, color: T.tx, lineHeight: 1,
opacity: v.statusType === 'empty' ? 0.3 : 1,
}}>{v.value}</span>
<span style={{ fontSize: 12, color: T.tx3, marginLeft: 3 }}>{v.unit}</span>
</div>
<span style={{
fontSize: 11, padding: '2px 8px', borderRadius: 999, fontWeight: 500,
background: v.statusType === 'acc' ? T.accL : v.statusType === 'wrn' ? T.wrnL : T.surface,
color: v.statusType === 'acc' ? T.acc : v.statusType === 'wrn' ? T.wrn : T.tx3,
}}>{v.status}</span>
</div>
))}
</div>
</div>
</div>
);
}
// ─── 屏幕三:设备同步 ───
function DeviceSync() {
const devices = [
{ name: 'BCM 血压计', connected: true, lastSync: '5分钟前' },
{ name: '欧姆龙 血糖仪', connected: false, lastSync: null },
];
// 生成心率波形 SVG path
const wavePoints = [];
for (let x = 0; x <= 300; x += 2) {
let y = 40;
// 模拟心电图 QRS 波形
const cycle = x % 80;
if (cycle >= 30 && cycle < 35) y = 40 - 8;
else if (cycle >= 35 && cycle < 40) y = 40 + 28;
else if (cycle >= 40 && cycle < 45) y = 40 - 18;
else if (cycle >= 45 && cycle < 50) y = 40 + 5;
else y = 40 + Math.sin(x * 0.05) * 2;
wavePoints.push(`${x === 0 ? 'M' : 'L'}${x},${y}`);
}
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="设备同步" />
<div style={{ flex: 1, overflow: 'auto', padding: '16px 16px 100px' }}>
{/* 已连接设备卡片 */}
<div style={{
background: T.card, borderRadius: T.r, padding: 18,
boxShadow: '0 2px 12px rgba(45,42,38,0.06)', marginBottom: 16,
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<span style={{ fontSize: 15, fontWeight: 600, color: T.tx }}>当前设备</span>
<span style={{
display: 'inline-block', padding: '3px 10px', borderRadius: 20,
background: T.accL, color: T.acc, fontSize: 12, fontWeight: 600,
}}>已连接</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{
width: 48, height: 48, borderRadius: T.rSm, background: T.accL,
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
}}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<rect x="2" y="6" width="20" height="12" rx="2" stroke={T.acc} strokeWidth="1.5"/>
<path d="M6 10h2M10 10h2M6 14h8" stroke={T.acc} strokeWidth="1.5" strokeLinecap="round"/>
</svg>
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 16, fontWeight: 600, color: T.tx }}>BCM 血压计</div>
<div style={{ fontSize: 13, color: T.tx3, marginTop: 2 }}>最后同步5分钟前</div>
</div>
</div>
</div>
{/* 可用设备列表 */}
<div style={{ fontSize: 14, fontWeight: 600, color: T.tx2, marginBottom: 8, paddingLeft: 2 }}>可用设备</div>
<div style={{ background: T.card, borderRadius: T.r, overflow: 'hidden', boxShadow: '0 1px 4px rgba(45,42,38,0.04)', marginBottom: 20 }}>
{devices.map((d, i) => (
<div key={i} style={{
display: 'flex', alignItems: 'center', gap: 12, padding: '14px 16px',
borderBottom: i < devices.length - 1 ? `1px solid ${T.bdL}` : 'none',
}}>
<div style={{
width: 40, height: 40, borderRadius: T.rSm,
background: d.connected ? T.accL : T.surface,
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
}}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<rect x="2" y="6" width="20" height="12" rx="2" stroke={d.connected ? T.acc : T.tx3} strokeWidth="1.5"/>
<path d="M6 10h2M10 10h2M6 14h8" stroke={d.connected ? T.acc : T.tx3} strokeWidth="1.5" strokeLinecap="round"/>
</svg>
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 15, fontWeight: 500, color: T.tx }}>{d.name}</div>
</div>
<span style={{
fontSize: 13, color: d.connected ? T.acc : T.tx3, fontWeight: 500,
}}>{d.connected ? '已连接' : '未连接'}</span>
</div>
))}
</div>
{/* 实时数据区 — 心率波形 */}
<div style={{ fontSize: 14, fontWeight: 600, color: T.tx2, marginBottom: 8, paddingLeft: 2 }}>实时数据</div>
<div style={{
background: T.card, borderRadius: T.r, padding: 18,
boxShadow: '0 1px 4px rgba(45,42,38,0.04)', marginBottom: 20,
}}>
<div style={{
background: T.bg, borderRadius: T.rSm, padding: '12px 8px',
height: 90, display: 'flex', alignItems: 'center', justifyContent: 'center',
marginBottom: 12, overflow: 'hidden',
}}>
<svg width="100%" height="80" viewBox="0 0 300 80" preserveAspectRatio="none" style={{ overflow: 'visible' }}>
<path d={wavePoints.join(' ')} fill="none" stroke={T.pri} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<div style={{ textAlign: 'center' }}>
<span style={{ fontFamily: T.serif, fontSize: 36, fontWeight: 700, color: T.tx }}>72</span>
<span style={{ fontSize: 14, color: T.tx3, marginLeft: 4 }}>bpm</span>
</div>
</div>
{/* 同步数据按钮 */}
<div style={{
height: 52, borderRadius: 16, background: T.pri,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', fontSize: 17, fontWeight: 600,
boxShadow: `0 4px 16px rgba(196,98,58,0.3)`,
}}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" style={{ marginRight: 8 }}>
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0118.8-4.3M22 12.5a10 10 0 01-18.8 4.3"
stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
同步数据
</div>
</div>
</div>
);
}
// ─── 屏幕四:体征录入 ───
function VitalInput() {
const types = ['血压', '心率', '血糖', '体重'];
const activeType = 0;
const periods = ['早晨', '晚上'];
const activePeriod = 0;
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="记录体征" />
<div style={{ flex: 1, overflow: 'auto', padding: '16px 16px 120px' }}>
{/* 体征类型选择 */}
<div style={{ display: 'flex', gap: 8, marginBottom: 20 }}>
{types.map((t, i) => (
<div key={i} style={{
flex: 1, height: 40, borderRadius: T.rSm,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 14, fontWeight: 600,
background: i === activeType ? T.pri : T.surface,
color: i === activeType ? '#fff' : T.tx2,
}}>{t}</div>
))}
</div>
{/* 血压录入表单 */}
<div style={{
background: T.card, borderRadius: T.r, padding: 20,
boxShadow: '0 2px 12px rgba(45,42,38,0.06)', marginBottom: 16,
}}>
{/* 收缩压 */}
<div style={{ marginBottom: 18 }}>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 6, fontWeight: 500 }}>收缩压高压</div>
<div style={{
height: 60, background: T.bg, border: `2px solid ${T.bd}`, borderRadius: T.rSm,
display: 'flex', alignItems: 'center', padding: '0 16px',
}}>
<span style={{ fontFamily: T.serif, fontSize: 30, fontWeight: 700, color: T.tx }}>130</span>
<span style={{ fontSize: 13, color: T.tx3, marginLeft: 6 }}>mmHg</span>
</div>
</div>
{/* 舒张压 */}
<div style={{ marginBottom: 18 }}>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 6, fontWeight: 500 }}>舒张压低压</div>
<div style={{
height: 60, background: T.bg, border: `2px solid ${T.bd}`, borderRadius: T.rSm,
display: 'flex', alignItems: 'center', padding: '0 16px',
}}>
<span style={{ fontFamily: T.serif, fontSize: 30, fontWeight: 700, color: T.tx }}>85</span>
<span style={{ fontSize: 13, color: T.tx3, marginLeft: 6 }}>mmHg</span>
</div>
</div>
{/* 测量时段 */}
<div style={{ marginBottom: 18 }}>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 6, fontWeight: 500 }}>测量时段</div>
<div style={{ display: 'flex', gap: 10 }}>
{periods.map((p, i) => (
<div key={i} style={{
flex: 1, height: 44, borderRadius: T.rSm,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 15, fontWeight: 600,
background: i === activePeriod ? T.pri : T.surface,
color: i === activePeriod ? '#fff' : T.tx2,
}}>{p}</div>
))}
</div>
</div>
{/* 备注 */}
<div>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 6, fontWeight: 500 }}>备注</div>
<div style={{
minHeight: 72, background: T.bg, border: `2px solid ${T.bd}`, borderRadius: T.rSm,
padding: '12px 16px', fontSize: 15, color: T.tx3, lineHeight: 1.6,
}}>
添加备注选填
</div>
</div>
</div>
{/* 参考范围提示 */}
<div style={{
background: T.surface, borderRadius: T.rSm, padding: '12px 16px',
display: 'flex', alignItems: 'flex-start', gap: 8,
}}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0, marginTop: 1 }}>
<circle cx="12" cy="12" r="10" stroke={T.tx3} strokeWidth="1.5"/>
<path d="M12 16v-4M12 8h.01" stroke={T.tx3} strokeWidth="2" strokeLinecap="round"/>
</svg>
<div style={{ fontSize: 12, color: T.tx3, lineHeight: 1.7 }}>
参考范围收缩压 90-140 / 舒张压 60-90 mmHg
</div>
</div>
</div>
{/* 底部保存按钮 */}
<div style={{
position: 'absolute', bottom: 34, left: 0, right: 0,
padding: '12px 16px', background: T.bg,
borderTop: `1px solid ${T.bdL}`,
}}>
<div style={{
height: 52, borderRadius: 16, background: T.pri,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', fontSize: 17, fontWeight: 600,
boxShadow: `0 4px 16px rgba(196,98,58,0.3)`,
}}>
保存
</div>
</div>
</div>
);
}
// ─── 屏幕五:趋势分析 ───
function TrendAnalysis() {
const timeRanges = ['7天', '30天', '90天'];
const activeRange = 0;
const trendData = [132, 128, 135, 130, 138, 126, 130];
const days = ['一','二','三','四','五','六','日'];
const maxV = Math.max(...trendData);
const threshold = 140;
const stats = [
{ label: '平均值', value: '130', unit: 'mmHg' },
{ label: '最高值', value: '138', unit: 'mmHg' },
{ label: '最低值', value: '126', unit: 'mmHg' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="趋势分析" />
<div style={{ flex: 1, overflow: 'auto', padding: '16px 16px 100px' }}>
{/* 时间段选择 */}
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
{timeRanges.map((t, i) => (
<div key={i} style={{
flex: 1, height: 40, borderRadius: T.rSm,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 14, fontWeight: 600,
background: i === activeRange ? T.pri : T.surface,
color: i === activeRange ? '#fff' : T.tx2,
}}>{t}</div>
))}
</div>
{/* 趋势图 */}
<div style={{
background: T.card, borderRadius: T.r, padding: 18,
boxShadow: '0 2px 12px rgba(45,42,38,0.06)', marginBottom: 16,
}}>
<div style={{ fontSize: 14, fontWeight: 600, color: T.tx, marginBottom: 14 }}>收缩压趋势</div>
{/* 柱状图 */}
<div style={{
position: 'relative', height: 140, background: T.bg, borderRadius: T.rSm,
padding: '12px 8px', display: 'flex', alignItems: 'flex-end',
}}>
{/* 阈值标线 */}
<div style={{
position: 'absolute', left: 8, right: 8,
bottom: `${12 + (threshold / maxV) * 100}px`,
borderTop: `1.5px dashed ${T.wrn}`, opacity: 0.6,
}} />
<div style={{
position: 'absolute', right: 12,
bottom: `${18 + (threshold / maxV) * 100}px`,
fontSize: 10, color: T.wrn, opacity: 0.7,
}}>140</div>
{trendData.map((v, i) => {
const hPct = Math.max(10, (v / maxV) * 100);
const isWarn = v >= threshold;
return (
<div key={i} style={{
flex: 1, display: 'flex', flexDirection: 'column',
alignItems: 'center', height: '100%', justifyContent: 'flex-end',
}}>
<div style={{
width: 26, borderRadius: '6px 6px 0 0', minHeight: 8,
height: `${hPct}%`, background: isWarn ? T.wrn : T.pri,
opacity: isWarn ? 1 : 0.7, transition: 'height 0.3s',
}} />
<span style={{ fontSize: 11, color: T.tx3, marginTop: 6 }}>{days[i]}</span>
</div>
);
})}
</div>
{/* 图例 */}
<div style={{ display: 'flex', justifyContent: 'center', gap: 16, marginTop: 12 }}>
<span style={{ fontSize: 11, color: T.tx3 }}>
<span style={{ display: 'inline-block', width: 10, height: 10, borderRadius: 2, background: T.pri, marginRight: 4, verticalAlign: 'middle' }} />正常
</span>
<span style={{ fontSize: 11, color: T.tx3 }}>
<span style={{ display: 'inline-block', width: 10, height: 10, borderRadius: 2, background: T.wrn, marginRight: 4, verticalAlign: 'middle' }} />偏高
</span>
<span style={{ fontSize: 11, color: T.tx3 }}>
<span style={{ display: 'inline-block', width: 10, height: 0, borderTop: '1.5px dashed ' + T.wrn, marginRight: 4, verticalAlign: 'middle' }} />阈值
</span>
</div>
</div>
{/* 统计摘要卡片 */}
<div style={{
display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10, marginBottom: 16,
}}>
{stats.map((s, i) => (
<div key={i} style={{
background: T.card, borderRadius: T.rSm, padding: '14px 12px',
textAlign: 'center', boxShadow: '0 1px 4px rgba(45,42,38,0.04)',
}}>
<div style={{ fontSize: 12, color: T.tx3, marginBottom: 4 }}>{s.label}</div>
<div>
<span style={{ fontFamily: T.serif, fontSize: 22, fontWeight: 700, color: T.tx }}>{s.value}</span>
<span style={{ fontSize: 11, color: T.tx3, marginLeft: 2 }}>{s.unit}</span>
</div>
</div>
))}
</div>
{/* 趋势指示 */}
<div style={{
background: T.accL, borderRadius: T.r, padding: '16px 18px',
display: 'flex', alignItems: 'center', gap: 12,
}}>
<div style={{
width: 40, height: 40, borderRadius: 20, background: T.acc,
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
}}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<path d="M12 19V5M5 12l7-7 7 7" stroke="#fff" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
<div>
<div style={{ fontSize: 15, fontWeight: 600, color: T.acc, marginBottom: 2 }}>
趋势下降
</div>
<div style={{ fontSize: 13, color: T.acc, opacity: 0.8 }}>
7 日收缩压呈下降趋势保持良好
</div>
</div>
</div>
</div>
</div>
);
}
// ─── 渲染 ───
function App() {
return (
<div className="screens">
<div className="screen-wrap">
<span className="screen-label">告警列表</span>
<IosFrame time="9:41" battery={85}>
<AlertList />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">日常监测</span>
<IosFrame time="9:42" battery={84}>
<DailyMonitoring />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">设备同步</span>
<IosFrame time="9:43" battery={83}>
<DeviceSync />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">体征录入</span>
<IosFrame time="9:44" battery={82}>
<VitalInput />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">趋势分析</span>
<IosFrame time="9:45" battery={81}>
<TrendAnalysis />
</IosFrame>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>

View File

@@ -0,0 +1,440 @@
<!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: 800px; 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">温润东方风设计系统 — 个人中心健康记录模块。5 个屏幕:健康记录列表、诊断记录、用药记录、检查报告列表、报告详情。</div>
<div id="root"></div>
<script type="text/babel">
// ─── iOS 设备框 ───
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 = 360, height = 780, 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 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>
);
}
// ─── 设计 Token ───
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', position: 'relative', background: T.bg, borderBottom: `1px solid ${T.bdL}`, flexShrink: 0 }}>
<svg style={{ position: 'absolute', left: 16 }} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke={T.tx} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M15 18l-6-6 6-6"/>
</svg>
<span style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.tx }}>{title}</span>
</div>
);
}
function Tag({ label, color = T.acc, bg = T.accL }) {
return (
<span style={{ display: 'inline-block', fontSize: 11, color, background: bg, padding: '2px 8px', borderRadius: 6, fontWeight: 500 }}>{label}</span>
);
}
// ─── 屏幕1健康记录列表 ───
function HealthRecordList() {
const records = [
{ date: '5月8日', time: '09:30', type: '体征记录', tagColor: T.acc, tagBg: T.accL, summary: '血压 130/85, 心率 72, 血糖 5.6', icon: '♥' },
{ date: '5月6日', time: '14:00', type: '化验单', tagColor: T.pri, tagBg: T.priL, summary: '血常规:血红蛋白 110g/L', icon: '📋' },
{ date: '5月1日', time: '10:00', type: '体征记录', tagColor: T.acc, tagBg: T.accL, summary: '血压 128/82, 心率 70', icon: '♥' },
{ date: '4月28日', time: '16:00', type: '体检报告', tagColor: T.wrn, tagBg: T.wrnL, summary: '年度体检(综合)', icon: '📄' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="健康记录" />
<div style={{ flex: 1, overflow: 'auto', padding: '20px 20px 24px' }}>
{/* 顶部摘要 */}
<div style={{ background: T.card, borderRadius: T.r, padding: '18px 20px', marginBottom: 24, boxShadow: '0 2px 12px rgba(0,0,0,0.04)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 4 }}>本月记录</div>
<div style={{ fontFamily: T.serif, fontSize: 28, fontWeight: 700, color: T.pri }}>12</div>
</div>
<div style={{ display: 'flex', gap: 16 }}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: 11, color: T.tx3, marginBottom: 2 }}>体征</div>
<div style={{ fontSize: 16, fontWeight: 600, color: T.tx }}>8</div>
</div>
<div style={{ width: 1, background: T.bdL }} />
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: 11, color: T.tx3, marginBottom: 2 }}>化验</div>
<div style={{ fontSize: 16, fontWeight: 600, color: T.tx }}>3</div>
</div>
<div style={{ width: 1, background: T.bdL }} />
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: 11, color: T.tx3, marginBottom: 2 }}>报告</div>
<div style={{ fontSize: 16, fontWeight: 600, color: T.tx }}>1</div>
</div>
</div>
</div>
</div>
{/* 时间线 */}
<div style={{ position: 'relative', paddingLeft: 24 }}>
{/* 竖线 */}
<div style={{ position: 'absolute', left: 7, top: 6, bottom: 6, width: 2, background: `linear-gradient(to bottom, ${T.pri} 0%, ${T.bd} 100%)`, borderRadius: 1 }} />
{records.map((rec, i) => (
<div key={i} style={{ position: 'relative', marginBottom: i < records.length - 1 ? 20 : 0 }}>
{/* 圆点 */}
<div style={{ position: 'absolute', left: -24, top: 4, width: 16, height: 16, borderRadius: 8, background: T.card, border: `3px solid ${T.pri}`, zIndex: 1 }} />
{/* 日期行 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<span style={{ fontSize: 13, fontWeight: 600, color: T.tx }}>{rec.date}</span>
<span style={{ fontSize: 12, color: T.tx3 }}>{rec.time}</span>
<Tag label={rec.type} color={rec.tagColor} bg={rec.tagBg} />
</div>
{/* 卡片 */}
<div style={{ background: T.card, borderRadius: T.rSm, padding: '14px 16px', boxShadow: '0 1px 8px rgba(0,0,0,0.04)' }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10 }}>
<span style={{ fontSize: 18, lineHeight: 1 }}>{rec.icon}</span>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 14, fontWeight: 500, color: T.tx, marginBottom: 4 }}>{rec.type}</div>
<div style={{ fontSize: 13, color: T.tx2, lineHeight: 1.5 }}>{rec.summary}</div>
</div>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={T.tx3} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 18l6-6-6-6"/>
</svg>
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}
// ─── 屏幕2诊断记录 ───
function DiagnosisRecords() {
const diagnoses = [
{ name: '高血压 II 级', date: '2024-03-15', doctor: '王医生', dept: '心内科', status: '进行中', statusColor: T.wrn, statusBg: T.wrnL },
{ name: '2型糖尿病', date: '2023-08-20', doctor: '李医生', dept: '内分泌科', status: '已控制', statusColor: T.acc, statusBg: T.accL },
{ name: '慢性肾病 III 期', date: '2023-05-10', doctor: '张医生', dept: '肾内科', status: '进行中', statusColor: T.wrn, statusBg: T.wrnL },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="诊断记录" />
<div style={{ flex: 1, overflow: 'auto', padding: '20px 20px 24px' }}>
{/* 统计栏 */}
<div style={{ display: 'flex', gap: 12, marginBottom: 20 }}>
<div style={{ flex: 1, background: T.card, borderRadius: T.rSm, padding: '14px 16px', textAlign: 'center', boxShadow: '0 1px 8px rgba(0,0,0,0.04)' }}>
<div style={{ fontFamily: T.serif, fontSize: 22, fontWeight: 700, color: T.wrn }}>2</div>
<div style={{ fontSize: 12, color: T.tx3, marginTop: 2 }}>进行中</div>
</div>
<div style={{ flex: 1, background: T.card, borderRadius: T.rSm, padding: '14px 16px', textAlign: 'center', boxShadow: '0 1px 8px rgba(0,0,0,0.04)' }}>
<div style={{ fontFamily: T.serif, fontSize: 22, fontWeight: 700, color: T.acc }}>1</div>
<div style={{ fontSize: 12, color: T.tx3, marginTop: 2 }}>已控制</div>
</div>
</div>
{diagnoses.map((d, i) => (
<div key={i} style={{ background: T.card, borderRadius: T.r, padding: '18px 20px', marginBottom: 14, boxShadow: '0 2px 12px rgba(0,0,0,0.04)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 10 }}>
<div style={{ fontFamily: T.serif, fontSize: 16, fontWeight: 700, color: T.tx, lineHeight: 1.3 }}>{d.name}</div>
<Tag label={d.status} color={d.statusColor} bg={d.statusBg} />
</div>
<div style={{ fontSize: 12, color: T.tx3, marginBottom: 8 }}>诊断日期{d.date}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={T.tx3} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
<span style={{ fontSize: 13, color: T.tx2 }}>{d.doctor} · {d.dept}</span>
</div>
</div>
))}
</div>
</div>
);
}
// ─── 屏幕3用药记录 ───
function MedicationRecords() {
const currentMeds = [
{ name: '缬沙坦', dose: '80mg', freq: '每日一次', note: '早餐后', forDisease: '高血压', active: true },
{ name: '二甲双胍', dose: '500mg', freq: '每日两次', note: '', forDisease: '糖尿病', active: true },
];
const historyMeds = [
{ name: '阿莫西林', dose: '250mg', freq: '每日三次', note: '7天', forDisease: '呼吸道感染', active: false },
];
function MedCard({ med }) {
return (
<div style={{ background: T.card, borderRadius: T.rSm, padding: '16px 18px', marginBottom: 10, boxShadow: '0 1px 8px rgba(0,0,0,0.04)', borderLeft: `3px solid ${med.active ? T.acc : T.bd}` }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 8 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
<span style={{ fontFamily: T.serif, fontSize: 16, fontWeight: 700, color: T.tx }}>{med.name}</span>
<span style={{ fontSize: 14, color: T.tx2 }}>{med.dose}</span>
</div>
<Tag label={med.active ? '用药中' : '已停药'} color={med.active ? T.acc : T.tx3} bg={med.active ? T.accL : T.surface} />
</div>
<div style={{ fontSize: 13, color: T.tx2, marginBottom: 6 }}>
{med.freq}{med.note ? ` · ${med.note}` : ''}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke={T.tx3} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"/>
</svg>
<span style={{ fontSize: 12, color: T.tx3 }}>{med.forDisease}</span>
</div>
</div>
);
}
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="用药记录" />
<div style={{ flex: 1, overflow: 'auto', padding: '20px 20px 24px' }}>
{/* 当前用药 */}
<div style={{ fontSize: 13, fontWeight: 600, color: T.tx3, marginBottom: 12, letterSpacing: '0.05em' }}>当前用药</div>
{currentMeds.map((med, i) => <MedCard key={i} med={med} />)}
{/* 分隔 */}
<div style={{ height: 24 }} />
{/* 历史用药 */}
<div style={{ fontSize: 13, fontWeight: 600, color: T.tx3, marginBottom: 12, letterSpacing: '0.05em' }}>历史用药</div>
{historyMeds.map((med, i) => <MedCard key={i} med={med} />)}
</div>
</div>
);
}
// ─── 屏幕4检查报告列表 ───
function ExamReportList() {
const reports = [
{ type: '血常规', date: '5月6日', abnormal: 2, tagBg: T.wrnL, tagColor: T.wrn, tagLabel: '2项异常' },
{ type: '肝功能', date: '5月6日', abnormal: 0, tagBg: T.accL, tagColor: T.acc, tagLabel: '全部正常' },
{ type: '肾功能', date: '5月6日', abnormal: 1, tagBg: T.wrnL, tagColor: T.wrn, tagLabel: '1项异常' },
{ type: '尿常规', date: '4月20日', abnormal: 0, tagBg: T.accL, tagColor: T.acc, tagLabel: '全部正常' },
];
const typeColors = {
'血常规': { bg: '#FDE8E8', color: '#C53030' },
'肝功能': { bg: '#E8F0E8', color: '#5B7A5E' },
'肾功能': { bg: '#FFF3E0', color: '#C4873A' },
'尿常规': { bg: '#E8E0F0', color: '#7A5B8A' },
};
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="检查报告" />
<div style={{ flex: 1, overflow: 'auto', padding: '20px 20px 24px' }}>
{/* 筛选标签 */}
<div style={{ display: 'flex', gap: 8, marginBottom: 18, overflowX: 'auto' }}>
{['全部', '异常', '正常'].map((f, i) => (
<div key={i} style={{ padding: '6px 16px', borderRadius: 999, fontSize: 13, fontWeight: 500, background: i === 0 ? T.pri : T.card, color: i === 0 ? '#fff' : T.tx2, whiteSpace: 'nowrap', boxShadow: i === 0 ? 'none' : '0 1px 4px rgba(0,0,0,0.06)' }}>
{f}
</div>
))}
</div>
{reports.map((r, i) => {
const tc = typeColors[r.type] || { bg: T.surface, color: T.tx2 };
return (
<div key={i} style={{ background: T.card, borderRadius: T.r, padding: '16px 18px', marginBottom: 12, boxShadow: '0 2px 12px rgba(0,0,0,0.04)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
{/* 类型标签 */}
<div style={{ width: 48, height: 48, borderRadius: T.rSm, background: tc.bg, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ fontSize: 13, fontWeight: 600, color: tc.color }}>{r.type.slice(0, 1)}</span>
</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
<span style={{ fontSize: 15, fontWeight: 600, color: T.tx }}>{r.type}</span>
<Tag label={r.tagLabel} color={r.tagColor} bg={r.tagBg} />
</div>
<div style={{ fontSize: 12, color: T.tx3 }}>检查日期{r.date}</div>
</div>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={T.tx3} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 18l6-6-6-6"/>
</svg>
</div>
</div>
);
})}
</div>
</div>
);
}
// ─── 屏幕5报告详情血常规 ───
function ReportDetail() {
const header = {
type: '血常规',
date: '2026年5月6日 14:00',
org: 'HMS 健康管理中心',
};
const items = [
{ name: '白细胞', value: '6.8', unit: '×10⁹/L', range: '3.5-9.5', status: 'normal' },
{ name: '红细胞', value: '4.2', unit: '×10¹²/L', range: '4.3-5.8', status: 'low' },
{ name: '血红蛋白', value: '110', unit: 'g/L', range: '130-175', status: 'low' },
{ name: '血小板', value: '210', unit: '×10⁹/L', range: '125-350', status: 'normal' },
{ name: '中性粒细胞', value: '65', unit: '%', range: '40-75', status: 'normal' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="报告详情" />
<div style={{ flex: 1, overflow: 'auto', padding: '20px 20px 24px' }}>
{/* 报告头部 */}
<div style={{ background: `linear-gradient(135deg, ${T.pri} 0%, ${T.priD} 100%)`, borderRadius: T.r, padding: '20px 22px', marginBottom: 20, color: '#fff', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: -20, right: -20, width: 80, height: 80, borderRadius: 40, background: 'rgba(255,255,255,0.08)' }} />
<div style={{ fontFamily: T.serif, fontSize: 20, fontWeight: 700, marginBottom: 8 }}>{header.type}</div>
<div style={{ fontSize: 13, opacity: 0.85, marginBottom: 4 }}>{header.date}</div>
<div style={{ fontSize: 12, opacity: 0.7 }}>{header.org}</div>
</div>
{/* 异常提示 */}
<div style={{ background: T.wrnL, borderRadius: T.rSm, padding: '12px 16px', marginBottom: 18, display: 'flex', alignItems: 'center', gap: 8 }}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={T.wrn} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
<span style={{ fontSize: 13, color: T.wrn, fontWeight: 500 }}>2 项指标偏低请关注</span>
</div>
{/* 指标列表 */}
<div style={{ background: T.card, borderRadius: T.r, overflow: 'hidden', boxShadow: '0 2px 12px rgba(0,0,0,0.04)' }}>
{/* 表头 */}
<div style={{ display: 'flex', padding: '12px 18px', borderBottom: `1px solid ${T.bdL}`, fontSize: 11, color: T.tx3, fontWeight: 600 }}>
<div style={{ flex: 1 }}>指标</div>
<div style={{ width: 52, textAlign: 'center' }}>结果</div>
<div style={{ width: 56, textAlign: 'center' }}>单位</div>
<div style={{ width: 64, textAlign: 'center' }}>参考范围</div>
<div style={{ width: 48, textAlign: 'center' }}>状态</div>
</div>
{items.map((item, i) => {
const isAbnormal = item.status !== 'normal';
return (
<div key={i} style={{
display: 'flex',
padding: '14px 18px',
alignItems: 'center',
borderBottom: i < items.length - 1 ? `1px solid ${T.bdL}` : 'none',
background: isAbnormal ? T.wrnL : 'transparent',
borderLeft: isAbnormal ? `3px solid ${T.wrn}` : '3px solid transparent',
}}>
<div style={{ flex: 1, fontSize: 13, fontWeight: 500, color: T.tx }}>{item.name}</div>
<div style={{ width: 52, textAlign: 'center', fontSize: 14, fontWeight: 600, color: isAbnormal ? T.wrn : T.tx }}>{item.value}</div>
<div style={{ width: 56, textAlign: 'center', fontSize: 11, color: T.tx3 }}>{item.unit}</div>
<div style={{ width: 64, textAlign: 'center', fontSize: 11, color: T.tx3 }}>{item.range}</div>
<div style={{ width: 48, textAlign: 'center' }}>
{isAbnormal ? (
<span style={{ fontSize: 11, color: T.wrn, fontWeight: 600 }}>偏低</span>
) : (
<span style={{ fontSize: 11, color: T.acc }}>正常</span>
)}
</div>
</div>
);
})}
</div>
{/* 底部按钮 */}
<div style={{ marginTop: 24 }}>
<div style={{ background: T.pri, borderRadius: T.rSm, padding: '14px 0', textAlign: 'center', color: '#fff', fontSize: 15, fontWeight: 600, boxShadow: `0 4px 16px rgba(196,98,58,0.3)` }}>
咨询医生
</div>
</div>
</div>
</div>
);
}
// ─── 组装 ───
function App() {
const screens = [
{ label: '1 · 健康记录列表', component: <HealthRecordList /> },
{ label: '2 · 诊断记录', component: <DiagnosisRecords /> },
{ label: '3 · 用药记录', component: <MedicationRecords /> },
{ label: '4 · 检查报告', component: <ExamReportList /> },
{ label: '5 · 报告详情', component: <ReportDetail /> },
];
return (
<div class="screens">
{screens.map((s, i) => (
<div class="screen-wrap" key={i}>
<IosFrame width={360} height={780}>{s.component}</IosFrame>
<div class="screen-label">{s.label}</div>
</div>
))}
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>

View File

@@ -0,0 +1,313 @@
<!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: 32px; flex-wrap: wrap; justify-content: center; align-items: flex-start; }
.screen-wrap { display: flex; flex-direction: column; align-items: center; gap: 10px; }
.screen-label { color: #888; font-size: 11px; font-style: italic; }
</style>
</head>
<body>
<div class="page-title">HMS 小程序 · 透析管理 + 随访管理</div>
<div class="note">温润东方风设计系统 — 个人中心透析与随访模块。4 个屏幕:透析记录列表、透析详情、随访列表、随访详情。</div>
<div id="root"></div>
<script type="text/babel">
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,
};
// ─── iOS 设备框 ───
const FS = {
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={FS.wrapper}>
<div style={{ ...FS.screen, width, height }}>
<div style={{ ...FS.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 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={FS.dynamicIsland} />
<div style={FS.content}>{children}</div>
<div style={FS.homeIndicator} />
</div>
</div>
);
}
// ─── 通用组件 ───
function NavBar({ title }) {
return (
<div style={{ height: 44, display: 'flex', alignItems: 'center', justifyContent: 'center', borderBottom: `1px solid ${T.bdL}`, position: 'relative', padding: '0 16px', flexShrink: 0 }}>
<span style={{ position: 'absolute', left: 16, color: T.pri, fontSize: 22, fontFamily: T.serif, lineHeight: 1 }}></span>
<span style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.tx }}>{title}</span>
</div>
);
}
function Tag({ text, bg, color }) {
return <span style={{ display: 'inline-block', padding: '2px 10px', borderRadius: T.rXs, fontSize: 11, fontWeight: 600, background: bg, color }}>{text}</span>;
}
function InfoRow({ label, value }) {
return (
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '10px 0', borderBottom: `1px solid ${T.bdL}` }}>
<span style={{ fontSize: 13, color: T.tx3 }}>{label}</span>
<span style={{ fontSize: 13, color: T.tx, fontWeight: 500 }}>{value}</span>
</div>
);
}
function Card({ children, style }) {
return <div style={{ background: T.card, borderRadius: T.r, padding: 16, ...style }}>{children}</div>;
}
// ─── 屏幕1: 透析记录列表 ───
function DialysisList() {
const records = [
{ date: '5/8', day: '周四', mode: 'HD', time: '08:00-12:00', ultra: '2.3L', status: '已完成', sc: T.acc },
{ date: '5/5', day: '周一', mode: 'HD', time: '08:00-12:00', ultra: '2.1L', status: '已完成', sc: T.acc },
{ date: '5/3', day: '周六', mode: 'HDF', time: '08:00-12:30', ultra: '2.5L', status: '已完成', sc: T.acc },
];
return (
<div style={{ background: T.bg, minHeight: '100%', padding: '0 16px 16px' }}>
<NavBar title="透析记录" />
<div style={{ height: 12 }} />
{/* 月度筛选 */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 24, padding: '10px 0' }}>
<span style={{ fontSize: 16, color: T.tx3 }}></span>
<span style={{ fontFamily: T.serif, fontSize: 17, fontWeight: 700, color: T.tx }}>2026年5月</span>
<span style={{ fontSize: 16, color: T.tx3 }}></span>
</div>
<div style={{ height: 8 }} />
{/* 统计条 */}
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
{[['本月透析', '3次', T.priL, T.priD], ['累计透析', '156次', T.accL, T.acc], ['本月超滤', '6.9L', T.wrnL, T.wrn]].map(([l, v, bg, c]) => (
<div key={l} style={{ flex: 1, background: T.card, borderRadius: T.rSm, padding: '10px 0', textAlign: 'center' }}>
<div style={{ fontSize: 18, fontWeight: 700, color: c, fontFamily: T.serif }}>{v}</div>
<div style={{ fontSize: 10, color: T.tx3, marginTop: 2 }}>{l}</div>
</div>
))}
</div>
{records.map((r, i) => (
<Card key={i} style={{ marginBottom: 10 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ width: 44, height: 44, borderRadius: T.rSm, background: T.priL, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ fontSize: 14, fontWeight: 700, color: T.priD, fontFamily: T.serif, lineHeight: 1 }}>{r.date.split('/')[1]}</span>
<span style={{ fontSize: 9, color: T.priD }}>5</span>
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 14, fontWeight: 600, color: T.tx }}>{r.day}</span>
<Tag text={r.mode} bg={r.mode === 'HDF' ? T.wrnL : T.priL} color={r.mode === 'HDF' ? T.wrn : T.priD} />
</div>
<div style={{ fontSize: 12, color: T.tx3, marginTop: 3 }}>{r.time}</div>
</div>
</div>
<div style={{ textAlign: 'right' }}>
<Tag text={r.status} bg={T.accL} color={r.sc} />
<div style={{ fontSize: 11, color: T.tx3, marginTop: 4 }}>超滤 {r.ultra}</div>
</div>
</div>
</Card>
))}
</div>
);
}
// ─── 屏幕2: 透析详情 ───
function DialysisDetail() {
const grid = [
['透析时长', '4h'], ['血流量', '250ml/min'], ['超滤量', '2.3L'],
['透前体重', '67.3kg'], ['透后体重', '65.0kg'], ['静脉压', '150mmHg'],
];
return (
<div style={{ background: T.bg, minHeight: '100%', padding: '0 16px 16px' }}>
<NavBar title="透析详情" />
<div style={{ height: 12 }} />
{/* 状态卡片 */}
<Card style={{ marginBottom: 10 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontFamily: T.serif, fontSize: 20, fontWeight: 700, color: T.tx }}>5月8日 周四</div>
<div style={{ fontSize: 12, color: T.tx3, marginTop: 2 }}>HD 模式 · 08:00 - 12:00</div>
</div>
<Tag text="已完成" bg={T.accL} color={T.acc} />
</div>
</Card>
{/* 数据网格 */}
<Card style={{ marginBottom: 10 }}>
<div style={{ fontFamily: T.serif, fontSize: 14, fontWeight: 700, color: T.tx, marginBottom: 10 }}>透析参数</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 8 }}>
{grid.map(([l, v]) => (
<div key={l} style={{ background: T.bg, borderRadius: T.rSm, padding: '10px 8px', textAlign: 'center' }}>
<div style={{ fontSize: 15, fontWeight: 700, color: T.pri, fontFamily: T.serif }}>{v}</div>
<div style={{ fontSize: 10, color: T.tx3, marginTop: 2 }}>{l}</div>
</div>
))}
</div>
</Card>
{/* 生命体征 */}
<Card style={{ marginBottom: 10 }}>
<div style={{ fontFamily: T.serif, fontSize: 14, fontWeight: 700, color: T.tx, marginBottom: 8 }}>生命体征</div>
{[['透前', '145/90', '82', T.wrnL, T.wrn], ['透后', '128/78', '75', T.accL, T.acc]].map(([label, bp, hr, bg, c]) => (
<div key={label} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 0', borderBottom: `1px solid ${T.bdL}` }}>
<span style={{ fontSize: 11, fontWeight: 600, color: c, background: bg, padding: '2px 8px', borderRadius: T.rXs }}>{label}</span>
<span style={{ fontSize: 13, color: T.tx2 }}>血压 <b style={{ color: T.tx }}>{bp}</b> mmHg</span>
<span style={{ fontSize: 13, color: T.tx2 }}>心率 <b style={{ color: T.tx }}>{hr}</b> bpm</span>
</div>
))}
</Card>
{/* 护士备注 */}
<Card>
<div style={{ fontFamily: T.serif, fontSize: 14, fontWeight: 700, color: T.tx, marginBottom: 6 }}>护士备注</div>
<div style={{ background: T.surface, borderRadius: T.rSm, padding: 12, fontSize: 13, color: T.tx2, lineHeight: 1.6 }}>
透析过程顺利患者无不适主诉超滤达标生命体征平稳
</div>
</Card>
</div>
);
}
// ─── 屏幕3: 随访列表 ───
function FollowUpList() {
const items = [
{ type: '门诊', typeBg: T.priL, typeC: T.priD, title: '肾功能复查', date: '5/12', status: '待完成', stBg: T.wrnL, stC: T.wrn },
{ type: '电话', typeBg: T.accL, typeC: T.acc, title: '血压控制随访', date: '5/8', status: '已完成', stBg: T.accL, stC: T.acc },
{ type: '门诊', typeBg: T.priL, typeC: T.priD, title: '血常规复查', date: '4/25', status: '已完成', stBg: T.accL, stC: T.acc },
];
return (
<div style={{ background: T.bg, minHeight: '100%', padding: '0 16px 16px' }}>
<NavBar title="我的随访" />
<div style={{ height: 12 }} />
{/* 状态筛选 */}
<div style={{ display: 'flex', gap: 8, marginBottom: 14 }}>
{['全部', '待完成', '已完成'].map((t, i) => (
<div key={t} style={{ padding: '6px 16px', borderRadius: 20, fontSize: 12, fontWeight: 600, background: i === 0 ? T.pri : T.card, color: i === 0 ? '#fff' : T.tx2, border: `1px solid ${i === 0 ? T.pri : T.bd}` }}>{t}</div>
))}
</div>
{items.map((it, i) => (
<Card key={i} style={{ marginBottom: 10 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}>
<Tag text={it.type + '随访'} bg={it.typeBg} color={it.typeC} />
<Tag text={it.status} bg={it.stBg} color={it.stC} />
</div>
<div style={{ fontSize: 15, fontWeight: 600, color: T.tx, marginBottom: 4 }}>{it.title}</div>
<div style={{ fontSize: 12, color: T.tx3 }}>计划日期{it.date}</div>
</div>
<span style={{ fontSize: 16, color: T.bd }}></span>
</div>
</Card>
))}
</div>
);
}
// ─── 屏幕4: 随访详情 ───
function FollowUpDetail() {
const checklist = [
{ done: true, text: '携带医保卡' },
{ done: false, text: '空腹8小时' },
{ done: false, text: '携带上次化验单' },
];
return (
<div style={{ background: T.bg, minHeight: '100%', padding: '0 16px 16px', display: 'flex', flexDirection: 'column' }}>
<NavBar title="随访详情" />
<div style={{ height: 12 }} />
{/* 状态 */}
<Card style={{ marginBottom: 10 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.tx }}>肾功能复查</div>
<div style={{ fontSize: 12, color: T.tx3, marginTop: 2 }}>门诊随访</div>
</div>
<Tag text="待完成" bg={T.wrnL} color={T.wrn} />
</div>
</Card>
{/* 信息 */}
<Card style={{ marginBottom: 10 }}>
<InfoRow label="随访类型" value="门诊随访" />
<InfoRow label="计划日期" value="2026年5月12日" />
<InfoRow label="负责医生" value="王医生" />
<InfoRow label="随访内容" value="肾功能指标复查,评估透析效果" />
</Card>
{/* 准备清单 */}
<Card style={{ marginBottom: 16 }}>
<div style={{ fontFamily: T.serif, fontSize: 14, fontWeight: 700, color: T.tx, marginBottom: 10 }}>就诊准备清单</div>
{checklist.map((c, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 0', borderBottom: i < checklist.length - 1 ? `1px solid ${T.bdL}` : 'none' }}>
<div style={{ width: 20, height: 20, borderRadius: 6, border: `2px solid ${c.done ? T.acc : T.bd}`, background: c.done ? T.accL : 'transparent', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, color: c.done ? T.acc : 'transparent' }}>
{c.done ? '✓' : ''}
</div>
<span style={{ fontSize: 13, color: c.done ? T.tx2 : T.tx, textDecoration: c.done ? 'line-through' : 'none' }}>{c.text}</span>
</div>
))}
</Card>
{/* 底部按钮 */}
<div style={{ marginTop: 'auto', padding: '12px 0 8px' }}>
<div style={{ background: T.pri, color: '#fff', textAlign: 'center', padding: '14px 0', borderRadius: T.r, fontSize: 15, fontWeight: 600, fontFamily: T.sans, letterSpacing: 1 }}>
确认完成
</div>
</div>
</div>
);
}
// ─── 渲染 ───
function App() {
const screens = [
{ label: '透析记录列表', el: <DialysisList />, w: 370, h: 800 },
{ label: '透析详情', el: <DialysisDetail />, w: 370, h: 800 },
{ label: '随访列表', el: <FollowUpList />, w: 370, h: 800 },
{ label: '随访详情', el: <FollowUpDetail />, w: 370, h: 800 },
];
return (
<div class="screens">
{screens.map((s) => (
<div class="screen-wrap" key={s.label}>
<IosFrame width={s.w} height={s.h}>{s.el}</IosFrame>
<div class="screen-label">{s.label}</div>
</div>
))}
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>

View File

@@ -0,0 +1,481 @@
<!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: 40px; 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">温润东方风设计系统 — 个人中心子页面原型。6 个屏幕:家庭管理 / 添加家庭成员 / 知情同意 / 事件记录 / 长者模式 / 设置。</div>
<div id="root"></div>
<script type="text/babel">
// ─── 设计 Token ───
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,
};
// ─── iOS 设备框 ───
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 = 340, height = 740, 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 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>
);
}
// ─── 导航栏组件 ───
function NavBar({ title, dark = false }) {
const color = dark ? '#fff' : T.tx;
return (
<div style={{ height: 44, display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative', borderBottom: `1px solid ${T.bdL}`, background: dark ? 'transparent' : T.card }}>
<svg style={{ position: 'absolute', left: 16, top: 12 }} width="20" height="20" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
<span style={{ fontFamily: T.serif, fontSize: 17, fontWeight: 700, color }}>{title}</span>
</div>
);
}
// ─── 右箭头图标 ───
function ArrowRight({ color = T.tx3 }) {
return <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 18 15 12 9 6"/></svg>;
}
// ─── 开关组件 ───
function Toggle({ on = false, color = T.pri }) {
return (
<div style={{ width: 48, height: 28, borderRadius: 14, background: on ? color : '#D1D5DB', position: 'relative', transition: 'background 0.2s', flexShrink: 0 }}>
<div style={{ width: 24, height: 24, borderRadius: 12, background: '#fff', position: 'absolute', top: 2, left: on ? 22 : 2, boxShadow: '0 1px 3px rgba(0,0,0,0.15)', transition: 'left 0.2s' }} />
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// 屏幕1家庭管理
// ═══════════════════════════════════════════════════════════════
function FamilyPage() {
const patients = [
{ name: '张三', relation: '本人', phone: '138****1234', isDefault: true, color: T.pri },
{ name: '张父', relation: '父亲', phone: '139****5678', isDefault: false, color: T.acc },
{ name: '张母', relation: '母亲', phone: '136****9012', isDefault: false, color: T.wrn },
];
return (
<div style={{ background: T.bg, minHeight: '100%', fontFamily: T.sans }}>
<NavBar title="就诊人管理" />
<div style={{ padding: '16px' }}>
{patients.map((p, i) => (
<div key={i} style={{ background: T.card, borderRadius: T.r, padding: 16, marginBottom: 12, boxShadow: '0 1px 4px rgba(0,0,0,0.04)', display: 'flex', alignItems: 'center', gap: 12 }}>
{/* 头像 */}
<div style={{ width: 48, height: 48, borderRadius: 24, background: p.isDefault ? T.priL : T.surface, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ fontSize: 18, fontWeight: 700, color: p.isDefault ? T.priD : T.tx2, fontFamily: T.serif }}>{p.name[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>
<span style={{ fontSize: 12, color: T.tx3, background: T.surface, padding: '2px 8px', borderRadius: 6 }}>{p.relation}</span>
{p.isDefault && (
<span style={{ fontSize: 11, color: T.priD, background: T.priL, padding: '2px 8px', borderRadius: 6, fontWeight: 600 }}>默认</span>
)}
</div>
<span style={{ fontSize: 13, color: T.tx3 }}>{p.phone}</span>
</div>
{/* 箭头 */}
<ArrowRight />
</div>
))}
{/* 添加就诊人按钮 */}
<div style={{ border: `2px dashed ${T.bd}`, borderRadius: T.r, padding: '16px 0', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, marginTop: 4, cursor: 'pointer' }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke={T.pri} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
<span style={{ fontSize: 15, color: T.pri, fontWeight: 500 }}>添加就诊人</span>
</div>
</div>
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// 屏幕2添加家庭成员
// ═══════════════════════════════════════════════════════════════
function FamilyAddPage() {
const FieldStyle = { width: '100%', height: 56, background: T.card, border: `1.5px solid ${T.bd}`, borderRadius: T.r, padding: '0 16px', fontSize: 15, color: T.tx, outline: 'none', fontFamily: T.sans };
return (
<div style={{ background: T.bg, minHeight: '100%', fontFamily: T.sans }}>
<NavBar title="添加就诊人" />
<div style={{ padding: 16, display: 'flex', flexDirection: 'column', gap: 14 }}>
{/* 姓名 */}
<div>
<label style={{ display: 'block', fontSize: 13, color: T.tx2, marginBottom: 6, fontWeight: 500 }}>姓名</label>
<input style={FieldStyle} placeholder="请输入真实姓名" />
</div>
{/* 关系 */}
<div>
<label style={{ display: 'block', fontSize: 13, color: T.tx2, marginBottom: 6, fontWeight: 500 }}>关系</label>
<div style={{ ...FieldStyle, display: 'flex', alignItems: 'center', justifyContent: 'space-between', color: T.tx3 }}>
<span>请选择关系</span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={T.tx3} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</div>
</div>
{/* 手机号 */}
<div>
<label style={{ display: 'block', fontSize: 13, color: T.tx2, marginBottom: 6, fontWeight: 500 }}>手机号</label>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ height: 56, background: T.surface, borderRadius: T.r, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0 14px', fontSize: 14, color: T.tx, fontWeight: 600, flexShrink: 0, border: `1.5px solid ${T.bd}` }}>
+86
</div>
<input style={{ ...FieldStyle, flex: 1 }} placeholder="请输入手机号" type="tel" />
</div>
</div>
{/* 身份证号 */}
<div>
<label style={{ display: 'block', fontSize: 13, color: T.tx2, marginBottom: 6, fontWeight: 500 }}>身份证号</label>
<input style={FieldStyle} placeholder="请输入身份证号" />
</div>
{/* 出生日期 */}
<div>
<label style={{ display: 'block', fontSize: 13, color: T.tx2, marginBottom: 6, fontWeight: 500 }}>出生日期</label>
<div style={{ ...FieldStyle, display: 'flex', alignItems: 'center', justifyContent: 'space-between', color: T.tx3 }}>
<span>请选择出生日期</span>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={T.tx3} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
</div>
</div>
{/* 性别 */}
<div>
<label style={{ display: 'block', fontSize: 13, color: T.tx2, marginBottom: 6, fontWeight: 500 }}>性别</label>
<div style={{ display: 'flex', gap: 12 }}>
{['男', '女'].map((g, i) => (
<div key={g} style={{ flex: 1, height: 56, borderRadius: T.r, border: i === 0 ? `2px solid ${T.pri}` : `1.5px solid ${T.bd}`, background: i === 0 ? T.priL : T.card, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, cursor: 'pointer' }}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={i === 0 ? T.pri : T.tx3} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
{i === 0 ? (<><circle cx="10.5" cy="10.5" r="7"/><line x1="16" y1="16" x2="21" y2="21"/></>) : (<><circle cx="12" cy="12" r="7"/><line x1="8" y1="8" x2="16" y2="16"/></>)}
</svg>
<span style={{ fontSize: 15, fontWeight: 600, color: i === 0 ? T.pri : T.tx2 }}>{g}</span>
</div>
))}
</div>
</div>
{/* 保存按钮 */}
<div style={{ marginTop: 10 }}>
<div style={{ height: 56, borderRadius: T.r, background: T.pri, display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: `0 4px 16px rgba(196,98,58,0.3)`, cursor: 'pointer' }}>
<span style={{ fontSize: 17, fontWeight: 600, color: '#fff' }}>保存</span>
</div>
</div>
</div>
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// 屏幕3知情同意
// ═══════════════════════════════════════════════════════════════
function ConsentsPage() {
const consents = [
{ title: '血液透析知情同意书', date: '2025-04-01', status: 'signed', icon: 'blood' },
{ title: '透析血管通路知情同意书', date: '2025-04-01', status: 'signed', icon: 'vein' },
{ title: '数据共享知情同意书', date: '', status: 'pending', icon: 'data' },
];
return (
<div style={{ background: T.bg, minHeight: '100%', fontFamily: T.sans }}>
<NavBar title="知情同意书" />
<div style={{ padding: 16 }}>
{consents.map((c, i) => (
<div key={i} style={{ background: T.card, borderRadius: T.r, padding: 16, marginBottom: 12, boxShadow: '0 1px 4px rgba(0,0,0,0.04)', display: 'flex', alignItems: 'center', gap: 12 }}>
{/* 图标 */}
<div style={{ width: 44, height: 44, borderRadius: T.rSm, background: c.status === 'signed' ? T.accL : T.wrnL, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke={c.status === 'signed' ? T.acc : T.wrn} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>
</svg>
</div>
{/* 信息 */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontFamily: T.serif, fontSize: 14, fontWeight: 600, color: T.tx, marginBottom: 4, lineHeight: 1.4 }}>{c.title}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{c.date && <span style={{ fontSize: 12, color: T.tx3 }}>{c.date}</span>}
<span style={{ fontSize: 11, fontWeight: 600, color: c.status === 'signed' ? T.acc : T.wrn, background: c.status === 'signed' ? T.accL : T.wrnL, padding: '2px 8px', borderRadius: 6 }}>
{c.status === 'signed' ? '已签署' : '待签署'}
</span>
</div>
</div>
<ArrowRight />
</div>
))}
{/* 底部提示 */}
<div style={{ textAlign: 'center', marginTop: 20, padding: '0 20px' }}>
<p style={{ fontSize: 12, color: T.tx3, lineHeight: 1.8 }}>签署后的同意书具有法律效力请仔细阅读后再签署</p>
</div>
</div>
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// 屏幕4事件记录
// ═══════════════════════════════════════════════════════════════
function EventsPage() {
const events = [
{ date: '5月8日', desc: '开始透析治疗', color: T.pri, bg: T.priL, type: '治疗' },
{ date: '4月15日', desc: '调整透析方案', color: T.acc, bg: T.accL, type: '方案' },
{ date: '3月20日', desc: '完成血管通路手术', color: T.wrn, bg: T.wrnL, type: '手术' },
{ date: '2月10日', desc: '首次门诊', color: T.pri, bg: T.priL, type: '门诊' },
{ date: '2024-12-01', desc: '确诊慢性肾病', color: T.dan, bg: T.danL, type: '诊断' },
];
return (
<div style={{ background: T.bg, minHeight: '100%', fontFamily: T.sans }}>
<NavBar title="事件记录" />
<div style={{ padding: '16px 16px 16px 24px' }}>
{/* 时间线 */}
<div style={{ position: 'relative' }}>
{/* 竖线 */}
<div style={{ position: 'absolute', left: 7, top: 12, bottom: 12, width: 2, background: T.bdL, borderRadius: 1 }} />
{events.map((e, i) => (
<div key={i} style={{ position: 'relative', paddingLeft: 28, marginBottom: i < events.length - 1 ? 20 : 0 }}>
{/* 圆点 */}
<div style={{ position: 'absolute', left: 0, top: 6, width: 16, height: 16, borderRadius: 8, background: e.color, border: `3px solid ${e.bg}`, boxShadow: `0 0 0 2px ${e.bg}`, zIndex: 1 }} />
{/* 事件卡片 */}
<div style={{ background: T.card, borderRadius: T.rSm, padding: 14, boxShadow: '0 1px 4px rgba(0,0,0,0.04)' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
<span style={{ fontSize: 13, color: T.tx3, fontWeight: 500 }}>{e.date}</span>
<span style={{ fontSize: 11, color: e.color, background: e.bg, padding: '2px 8px', borderRadius: 6, fontWeight: 600 }}>{e.type}</span>
</div>
<div style={{ fontSize: 15, fontWeight: 600, color: T.tx, lineHeight: 1.4 }}>{e.desc}</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// 屏幕5长者模式
// ═══════════════════════════════════════════════════════════════
function ElderModePage() {
return (
<div style={{ background: T.bg, minHeight: '100%', fontFamily: T.sans }}>
<NavBar title="关怀模式" />
<div style={{ padding: 16 }}>
{/* 说明区域 */}
<div style={{ background: T.accL, borderLeft: `4px solid ${T.acc}`, borderRadius: '0 T.rSm T.rSm 0', padding: '14px 16px', marginBottom: 20 }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10 }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke={T.acc} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0, marginTop: 1 }}>
<circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>
<span style={{ fontSize: 13, color: T.acc, lineHeight: 1.7, fontWeight: 500 }}>开启关怀模式后字体将放大界面更简洁方便阅读</span>
</div>
</div>
{/* 模式开关卡片 */}
<div style={{ background: T.card, borderRadius: T.r, padding: 16, marginBottom: 16, boxShadow: '0 1px 4px rgba(0,0,0,0.04)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 40, height: 40, borderRadius: 12, background: T.priL, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke={T.pri} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
</div>
<div>
<div style={{ fontSize: 16, fontWeight: 600, color: T.tx }}>关怀模式</div>
<div style={{ fontSize: 12, color: T.tx3, marginTop: 2 }}>放大字号简化界面</div>
</div>
</div>
<Toggle on={true} color={T.pri} />
</div>
{/* 字号预览 */}
<div style={{ background: T.card, borderRadius: T.r, padding: 16, marginBottom: 16, boxShadow: '0 1px 4px rgba(0,0,0,0.04)' }}>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 12, fontWeight: 500 }}>字号预览</div>
{/* 标准字号 */}
<div style={{ marginBottom: 14 }}>
<span style={{ fontSize: 11, color: T.tx3, display: 'block', marginBottom: 4 }}>标准</span>
<div style={{ background: T.surface, borderRadius: T.rXs, padding: '10px 14px' }}>
<span style={{ fontSize: 15, color: T.tx }}>血压 130/85 mmHg</span>
</div>
</div>
{/* 放大字号 */}
<div>
<span style={{ fontSize: 11, color: T.pri, display: 'block', marginBottom: 4, fontWeight: 600 }}>关怀模式</span>
<div style={{ background: T.priL, borderRadius: T.rXs, padding: '12px 14px' }}>
<span style={{ fontSize: 22, color: T.priD, fontWeight: 600 }}>血压 130/85 mmHg</span>
</div>
</div>
</div>
{/* 其他设置 */}
<div style={{ background: T.card, borderRadius: T.r, overflow: 'hidden', boxShadow: '0 1px 4px rgba(0,0,0,0.04)' }}>
{/* 高对比度 */}
<div style={{ padding: '14px 16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', borderBottom: `1px solid ${T.bdL}` }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 36, height: 36, borderRadius: 10, background: T.surface, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={T.tx2} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2a10 10 0 000 20z"/></svg>
</div>
<span style={{ fontSize: 15, color: T.tx, fontWeight: 500 }}>高对比度模式</span>
</div>
<Toggle on={false} />
</div>
{/* 语音播报 */}
<div style={{ padding: '14px 16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 36, height: 36, borderRadius: 10, background: T.surface, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={T.tx2} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 010 14.14M15.54 8.46a5 5 0 010 7.07"/></svg>
</div>
<span style={{ fontSize: 15, color: T.tx, fontWeight: 500 }}>语音播报</span>
</div>
<Toggle on={false} />
</div>
</div>
</div>
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// 屏幕6设置
// ═══════════════════════════════════════════════════════════════
function SettingsPage() {
const settingsGroups = [
[
{ icon: 'shield', iconBg: T.acc, label: '账号与安全', value: '' },
{ icon: 'bell', iconBg: T.wrn, label: '消息通知', value: '' },
{ icon: 'eye', iconBg: T.pri, label: '隐私设置', value: '' },
],
[
{ icon: 'globe', iconBg: '#6B8ABF', label: '语言', value: '简体中文' },
{ icon: 'trash', iconBg: T.tx3, label: '清除缓存', value: '12.3 MB' },
{ icon: 'info', iconBg: T.tx2, label: '关于 HMS', value: '' },
],
];
const iconPaths = {
shield: <><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></>,
bell: <><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></>,
eye: <><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></>,
globe: <><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></>,
trash: <><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></>,
info: <><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></>,
};
return (
<div style={{ background: T.bg, minHeight: '100%', fontFamily: T.sans }}>
<NavBar title="设置" />
<div style={{ padding: 16 }}>
{settingsGroups.map((group, gi) => (
<div key={gi} style={{ background: T.card, borderRadius: T.r, overflow: 'hidden', marginBottom: 16, boxShadow: '0 1px 4px rgba(0,0,0,0.04)' }}>
{group.map((item, ii) => (
<div key={ii} style={{ padding: '14px 16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', borderBottom: ii < group.length - 1 ? `1px solid ${T.bdL}` : 'none' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 34, height: 34, borderRadius: 10, background: item.iconBg, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">{iconPaths[item.icon]}</svg>
</div>
<span style={{ fontSize: 15, color: T.tx, fontWeight: 500 }}>{item.label}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
{item.value && <span style={{ fontSize: 13, color: T.tx3 }}>{item.value}</span>}
<ArrowRight />
</div>
</div>
))}
</div>
))}
{/* 版本号 + 退出登录 */}
<div style={{ textAlign: 'center', marginTop: 20 }}>
<div style={{ fontSize: 12, color: T.tx3, marginBottom: 16 }}>v1.0.0</div>
<div style={{ height: 48, borderRadius: T.r, border: `1.5px solid ${T.dan}`, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}>
<span style={{ fontSize: 15, fontWeight: 600, color: T.dan }}>退出登录</span>
</div>
</div>
</div>
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// 主渲染
// ═══════════════════════════════════════════════════════════════
function App() {
const screens = [
{ label: '家庭管理', component: <FamilyPage /> },
{ label: '添加家庭成员', component: <FamilyAddPage /> },
{ label: '知情同意', component: <ConsentsPage /> },
{ label: '事件记录', component: <EventsPage /> },
{ label: '长者模式', component: <ElderModePage /> },
{ label: '设置', component: <SettingsPage /> },
];
return (
<div className="screens">
{screens.map((s, i) => (
<div key={i} className="screen-wrap">
<IosFrame width={340} height={740}>
{s.component}
</IosFrame>
<div className="screen-label">{s.label}</div>
</div>
))}
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>

View File

@@ -0,0 +1,211 @@
<!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:.15em;text-transform:uppercase}
.note{color:#666;font-size:12px;max-width:800px;text-align:center;line-height:1.8}
.screens{display:flex;gap:32px;flex-wrap:wrap;justify-content:center;align-items:flex-start}
.screen-wrap{display:flex;flex-direction:column;align-items:center;gap:10px}
.screen-label{color:#888;font-size:11px;font-style:italic}
</style>
</head>
<body>
<div class="page-title">HMS 小程序 · 积分商城分包</div>
<div class="note">商品详情 + 兑换确认 + 订单列表 + 咨询聊天详情</div>
<div id="root"></div>
<script type="text/babel">
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};
const F={w:{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'},s:{position:'relative',borderRadius:48,overflow:'hidden',background:'#fff'},c:{position:'absolute',top:54,left:0,right:0,bottom:34,overflow:'auto'}};
function Frame({children,w=370,h=800}){
return(<div style={F.w}><div style={{...F.s,width:w,height:h}}>
<div style={{position:'absolute',top:0,left:0,right:0,height:54,display:'flex',alignItems:'center',justifyContent:'space-between',padding:'0 32px',fontSize:16,fontWeight:600,zIndex:20,pointerEvents:'none',color:'#000'}}>
<span>9:41</span>
<div style={{display:'flex',alignItems:'center',gap:6}}><div style={{width:26,height:12,border:'1.5px solid #000',borderRadius:3,padding:1}}><div style={{width:'85%',height:'100%',background:'#000',borderRadius:1}}/></div></div>
</div>
<div style={{position:'absolute',top:12,left:'50%',transform:'translateX(-50%)',width:124,height:36,background:'#000',borderRadius:999,zIndex:30}}/>
<div style={F.c}>{children}</div>
<div style={{position:'absolute',bottom:10,left:'50%',transform:'translateX(-50%)',width:140,height:5,background:'rgba(0,0,0,0.3)',borderRadius:999,zIndex:10}}/>
</div></div>);
}
function Nav({t}){
return(<div style={{height:44,display:'flex',alignItems:'center',justifyContent:'center',borderBottom:`1px solid ${T.bdL}`,position:'relative'}}>
<span style={{position:'absolute',left:16,color:T.pri,fontSize:20}}></span>
<span style={{fontFamily:T.serif,fontSize:18,fontWeight:700,color:T.tx}}>{t}</span>
</div>);
}
function Tag({label,color=T.acc,bg=T.accL}){
return <span style={{display:'inline-block',padding:'2px 8px',borderRadius:6,fontSize:11,fontWeight:600,color,background:bg}}>{label}</span>;
}
// ── 屏幕1: 商品详情 ──
function ProductDetail(){
return(<div style={{background:T.bg,fontFamily:T.sans}}>
<Nav t="商品详情"/>
<div style={{background:T.surface,height:280,display:'flex',alignItems:'center',justifyContent:'center'}}>
<div style={{width:180,height:180,borderRadius:T.r,background:T.bd,display:'flex',alignItems:'center',justifyContent:'center',flexDirection:'column',gap:8}}>
<div style={{fontSize:36,opacity:.3}}>🩺</div>
<span style={{fontSize:12,color:T.tx3}}>智能血压计</span>
</div>
</div>
<div style={{padding:20,background:T.card,borderRadius:'20px 20px 0 0',marginTop:-16,position:'relative'}}>
<div style={{fontFamily:T.serif,fontSize:20,fontWeight:700,color:T.tx,marginBottom:8}}>智能血压计</div>
<div style={{display:'flex',alignItems:'baseline',gap:10,marginBottom:16}}>
<span style={{fontFamily:T.serif,fontSize:24,fontWeight:700,color:T.pri}}>1,200 积分</span>
<span style={{fontSize:14,color:T.tx3,textDecoration:'line-through'}}>¥199</span>
</div>
<div style={{fontSize:13,color:T.tx2,lineHeight:1.8,marginBottom:20}}>医用级精度蓝牙连接手机自动记录数据支持多人使用大屏显示操作简便适合家庭日常监测</div>
<div style={{display:'flex',flexDirection:'column',gap:10,padding:'14px 16px',background:T.bg,borderRadius:T.rSm,marginBottom:16}}>
{[['品牌','乐心'],['规格','臂式'],['库存','36件']].map(([k,v])=>(
<div key={k} style={{display:'flex',justifyContent:'space-between',fontSize:13}}><span style={{color:T.tx3}}>{k}</span><span style={{color:T.tx,fontWeight:500}}>{v}</span></div>
))}
</div>
<div style={{padding:12,background:T.wrnL,borderRadius:T.rSm,borderLeft:`3px solid ${T.wrn}`}}>
<div style={{fontSize:12,color:T.wrn,fontWeight:600,marginBottom:4}}>温馨提示</div>
<div style={{fontSize:11,color:T.tx2,lineHeight:1.6}}>积分兑换商品不支持退换货请确认需求后再兑换商品将在 7 个工作日内寄出</div>
</div>
</div>
<div style={{position:'sticky',bottom:0,background:T.card,borderTop:`1px solid ${T.bdL}`,padding:'12px 20px',display:'flex',alignItems:'center',gap:12}}>
<div style={{width:48,height:48,border:`1px solid ${T.bd}`,borderRadius:T.rSm,display:'flex',alignItems:'center',justifyContent:'center',fontSize:20}}></div>
<div style={{flex:1,height:48,background:T.pri,borderRadius:T.r,display:'flex',alignItems:'center',justifyContent:'center',color:'#fff',fontFamily:T.serif,fontSize:16,fontWeight:700}}>立即兑换</div>
</div>
</div>);
}
// ── 屏幕2: 兑换确认 ──
function ExchangeConfirm(){
return(<div style={{background:T.bg,fontFamily:T.sans}}>
<Nav t="确认兑换"/>
<div style={{padding:16}}>
<div style={{display:'flex',gap:12,padding:14,background:T.card,borderRadius:T.r,marginBottom:16,boxShadow:'0 1px 4px rgba(0,0,0,0.04)'}}>
<div style={{width:72,height:72,borderRadius:T.rSm,background:T.surface,display:'flex',alignItems:'center',justifyContent:'center',fontSize:24}}>🩺</div>
<div style={{flex:1}}>
<div style={{fontFamily:T.serif,fontSize:15,fontWeight:700,color:T.tx,marginBottom:4}}>智能血压计</div>
<div style={{fontSize:13,color:T.pri,fontWeight:600}}>1,200 积分</div>
<div style={{fontSize:11,color:T.tx3,marginTop:2}}>×1</div>
</div>
</div>
<div style={{padding:16,background:T.card,borderRadius:T.r,marginBottom:16}}>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:10}}>
<span style={{fontSize:14,fontWeight:600,color:T.tx}}>收货信息</span>
<span style={{fontSize:12,color:T.pri,fontWeight:500}}>修改地址 </span>
</div>
<div style={{fontSize:13,color:T.tx,fontWeight:500,marginBottom:2}}>张三 · 138****1234</div>
<div style={{fontSize:12,color:T.tx3,lineHeight:1.5}}>北京市朝阳区建国路88号健康大厦12层</div>
</div>
<div style={{padding:16,background:T.card,borderRadius:T.r,marginBottom:24}}>
<div style={{fontSize:14,fontWeight:600,color:T.tx,marginBottom:12}}>兑换明细</div>
{[['商品积分','1,200'],['运费','¥0.00'],['应扣积分','1,200'],['剩余积分','80']].map(([k,v],i)=>(
<div key={k} style={{display:'flex',justifyContent:'space-between',paddingBottom:i<3?8:0,marginBottom:i<3?8:0,borderBottom:i<3?`1px dashed ${T.bdL}`:'none'}}>
<span style={{fontSize:13,color:T.tx3}}>{k}</span>
<span style={{fontSize:13,color:i===2?T.pri:i===3?T.acc:T.tx,fontWeight:i>=2?600:400}}>{v}</span>
</div>
))}
</div>
<div style={{height:50,background:T.pri,borderRadius:T.r,display:'flex',alignItems:'center',justifyContent:'center',color:'#fff',fontFamily:T.serif,fontSize:16,fontWeight:700}}>确认兑换</div>
</div>
</div>);
}
// ── 屏幕3: 订单列表 ──
function OrderList(){
const tabs=['全部','待发货','已发货','已完成'];
const orders=[
{id:'HX20260515001',name:'智能血压计',pts:'1,200',status:'待发货',statusBg:T.wrnL,statusColor:T.wrn,date:'2026-05-15'},
{id:'HX20260510002',name:'维生素D3 60粒',pts:'360',status:'已发货',statusBg:T.accL,statusColor:T.acc,date:'2026-05-10'},
{id:'HX20260428003',name:'健康手册',pts:'150',status:'已完成',statusBg:T.bg,statusColor:T.tx3,date:'2026-04-28'},
];
return(<div style={{background:T.bg,fontFamily:T.sans}}>
<Nav t="兑换记录"/>
<div style={{display:'flex',padding:'0 16px',gap:0,background:T.card,borderBottom:`1px solid ${T.bdL}`}}>
{tabs.map((t,i)=>(
<div key={t} style={{flex:1,textAlign:'center',padding:'12px 0',fontSize:13,fontWeight:i===0?600:400,color:i===0?T.pri:T.tx3,borderBottom:i===0?`2px solid ${T.pri}`:'2px solid transparent'}}>{t}</div>
))}
</div>
<div style={{padding:16,display:'flex',flexDirection:'column',gap:12}}>
{orders.map(o=>(
<div key={o.id} style={{padding:16,background:T.card,borderRadius:T.r,boxShadow:'0 1px 4px rgba(0,0,0,0.04)'}}>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:10}}>
<span style={{fontSize:11,color:T.tx3}}>订单号 {o.id}</span>
<Tag label={o.status} color={o.statusColor} bg={o.statusBg}/>
</div>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center'}}>
<div>
<div style={{fontSize:14,fontWeight:600,color:T.tx,marginBottom:2}}>{o.name}</div>
<div style={{fontSize:12,color:T.tx3}}>{o.date}</div>
</div>
<div style={{fontFamily:T.serif,fontSize:16,fontWeight:700,color:T.pri}}>{o.pts} <span style={{fontSize:11,fontWeight:400}}>积分</span></div>
</div>
</div>
))}
</div>
</div>);
}
// ── 屏幕4: 咨询聊天详情 ──
function ChatDetail(){
const Bubble=({left,children})=>(
<div style={{display:'flex',justifyContent:left?'flex-start':'flex-end',padding:'0 16px',marginBottom:10}}>
<div style={{maxWidth:'75%',padding:'10px 14px',borderRadius:left?'4px 16px 16px 16px':'16px 4px 16px 16px',background:left?T.card:T.priL,color:T.tx,fontSize:13,lineHeight:1.6}}>{children}</div>
</div>
);
return(<div style={{background:T.bg,fontFamily:T.sans,height:'100%',display:'flex',flexDirection:'column'}}>
<div style={{height:44,display:'flex',alignItems:'center',justifyContent:'center',borderBottom:`1px solid ${T.bdL}`,position:'relative',background:T.card}}>
<span style={{position:'absolute',left:16,color:T.pri,fontSize:20}}></span>
<div style={{display:'flex',alignItems:'center',gap:8}}>
<span style={{fontFamily:T.serif,fontSize:16,fontWeight:700,color:T.tx}}>王医生 · 心内科</span>
<div style={{width:8,height:8,background:T.acc,borderRadius:'50%'}}/>
</div>
</div>
<div style={{flex:1,overflow:'auto',padding:'16px 0'}}>
<div style={{textAlign:'center',fontSize:11,color:T.tx3,marginBottom:16}}>今天 10:32</div>
<Bubble left>化验结果已出您的血红蛋白偏低95g/L属于轻度贫血</Bubble>
<Bubble left={false}>需要怎么处理呢严重吗</Bubble>
<Bubble left>不算严重建议补充铁剂配合维生素C促进吸收我给您开个处方</Bubble>
<div style={{padding:'0 16px',marginBottom:10}}>
<div style={{padding:14,background:T.card,borderRadius:T.r,borderLeft:`3px solid ${T.acc}`}}>
<div style={{fontSize:12,color:T.acc,fontWeight:600,marginBottom:6}}>处方建议</div>
<div style={{fontSize:14,fontWeight:600,color:T.tx,marginBottom:4}}>硫酸亚铁片</div>
<div style={{fontSize:12,color:T.tx2,lineHeight:1.5}}>用法每次1片每日3次饭后服用</div>
<div style={{fontSize:11,color:T.tx3,marginTop:6}}>疗程4周后复查血常规</div>
</div>
</div>
<Bubble left={false}>好的谢谢王医生</Bubble>
</div>
<div style={{padding:'10px 16px',background:T.card,borderTop:`1px solid ${T.bdL}`,display:'flex',alignItems:'center',gap:10}}>
<div style={{flex:1,height:40,background:T.bg,borderRadius:20,padding:'0 16px',display:'flex',alignItems:'center',fontSize:13,color:T.tx3}}>输入消息</div>
<div style={{width:40,height:40,background:T.pri,borderRadius:20,display:'flex',alignItems:'center',justifyContent:'center',color:'#fff',fontSize:16}}></div>
</div>
</div>);
}
// ── 渲染 ──
const screens=[
{label:'1 · 商品详情',el:<ProductDetail/>},
{label:'2 · 兑换确认',el:<ExchangeConfirm/>},
{label:'3 · 订单列表',el:<OrderList/>},
{label:'4 · 咨询聊天详情',el:<ChatDetail/>},
];
ReactDOM.createRoot(document.getElementById('root')).render(
<div class="screens">
{screens.map((s,i)=>(
<div class="screen-wrap" key={i}>
<Frame w={370} h={800}>{s.el}</Frame>
<div class="screen-label">{s.label}</div>
</div>
))}
</div>
);
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 KiB

View File

@@ -0,0 +1,481 @@
<!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: 1100px; text-align: center; line-height: 1.8; }
.screens { display: flex; gap: 32px; 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">温润东方风设计系统 — 医生端变体深靛蓝主色替代暖橙。5 个屏幕:医生工作台、待办收件箱、在线咨询、随访管理、患者管理。专业而温暖的医疗工作界面。</div>
<div id="root"></div>
<script type="text/babel">
// ─── iOS 设备框 ───
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 = 360, height = 780, time = '9:41', battery = 85, darkStatus = false, label }) {
const statusColor = darkStatus ? '#fff' : '#000';
return (
<div className="screen-wrap">
<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 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>
{label && <div className="screen-label">{label}</div>}
</div>
);
}
// ─── 设计 Token — 医生端变体 ───
const T = {
pri: '#3A6B8C', priL: '#D4E5F0', priD: '#2A4F6A',
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', position: 'relative', background: T.bg, borderBottom: `1px solid ${T.bdL}`, flexShrink: 0 }}>
<svg style={{ position: 'absolute', left: 16 }} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke={T.tx} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M15 18l-6-6 6-6"/>
</svg>
<span style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.tx }}>{title}</span>
</div>
);
}
function Tag({ children, color, bg, fontSize = 11 }) {
return (
<span style={{ display: 'inline-block', padding: '2px 8px', borderRadius: 6, fontSize, fontWeight: 600, color, background: bg, lineHeight: 1.6 }}>{children}</span>
);
}
function BottomTabBar({ tabs, active }) {
const icons = {
'工作台': (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>
</svg>
),
'患者': (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M16 21v-2a4 4 0 00-4-4H6a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/>
</svg>
),
'消息': (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>
</svg>
),
'我的': (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/>
</svg>
),
};
return (
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 70, background: T.card, borderTop: `1px solid ${T.bdL}`, display: 'flex', alignItems: 'flex-start', justifyContent: 'space-around', paddingTop: 8, paddingBottom: 28, zIndex: 10 }}>
{tabs.map((tab) => {
const isActive = tab === active;
return (
<div key={tab} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3 }}>
<div style={{ color: isActive ? T.pri : T.tx3, display: 'flex' }}>{icons[tab]}</div>
<span style={{ fontSize: 11, fontWeight: isActive ? 600 : 400, color: isActive ? T.pri : T.tx3 }}>{tab}</span>
</div>
);
})}
</div>
);
}
// ─── 图标组件 ───
function IconPatient() {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke={T.pri} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M16 21v-2a4 4 0 00-4-4H6a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 11h-6"/>
</svg>
);
}
function IconConsult() {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke={T.acc} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>
</svg>
);
}
function IconFollowUp() {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke={T.wrn} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>
</svg>
);
}
function IconDialysis() {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke={T.dan} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/>
</svg>
);
}
// ─── 屏幕1医生工作台 ───
function DoctorHome() {
const stats = [
{ label: '待处理', value: '12', color: T.wrn },
{ label: '咨询中', value: '3', color: T.pri },
{ label: '今日患者', value: '8', color: T.acc },
{ label: '随访到期', value: '5', color: T.dan },
];
const shortcuts = [
{ icon: <IconPatient />, label: '患者管理', bg: T.priL },
{ icon: <IconConsult />, label: '在线咨询', bg: T.accL },
{ icon: <IconFollowUp />, label: '随访管理', bg: T.wrnL },
{ icon: <IconDialysis />, label: '透析管理', bg: T.danL },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
{/* 问候区 */}
<div style={{ padding: '16px 20px 0' }}>
<div style={{ fontFamily: T.serif, fontSize: 26, fontWeight: 700, color: T.tx, marginBottom: 4 }}>工作台</div>
<div style={{ fontSize: 14, color: T.tx3 }}>2026年5月16日 星期六</div>
</div>
<div style={{ flex: 1, overflow: 'auto', padding: '16px 20px 80px' }}>
{/* 今日概览 */}
<div style={{ background: T.card, borderRadius: T.r, padding: 16, marginBottom: 16, boxShadow: '0 2px 12px rgba(0,0,0,0.04)' }}>
<div style={{ fontSize: 13, fontWeight: 600, color: T.tx2, marginBottom: 14, fontFamily: T.sans }}>今日概览</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
{stats.map((s) => (
<div key={s.label} style={{ background: T.bg, borderRadius: T.rSm, padding: '14px 12px', textAlign: 'center' }}>
<div style={{ fontFamily: T.serif, fontSize: 28, fontWeight: 700, color: s.color, lineHeight: 1.1 }}>{s.value}</div>
<div style={{ fontSize: 12, color: T.tx3, marginTop: 4 }}>{s.label}</div>
</div>
))}
</div>
</div>
{/* 快捷操作 */}
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
{shortcuts.map((s) => (
<div key={s.label} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
<div style={{ width: 52, height: 52, borderRadius: 26, background: s.bg, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{s.icon}
</div>
<span style={{ fontSize: 12, color: T.tx2 }}>{s.label}</span>
</div>
))}
</div>
{/* 待办提醒 */}
<div style={{ fontSize: 13, fontWeight: 600, color: T.tx2, marginBottom: 10, fontFamily: T.sans }}>待办提醒</div>
<div style={{ background: T.priL, borderRadius: T.r, padding: '14px 16px', marginBottom: 10, borderLeft: `4px solid ${T.pri}` }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<svg width="18" height="18" viewBox="0 0 24 24" fill={T.pri}><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
<div>
<div style={{ fontSize: 14, fontWeight: 500, color: T.tx }}>3 位患者血压异常待处理</div>
<div style={{ fontSize: 12, color: T.tx3, marginTop: 2 }}>需要立即关注</div>
</div>
</div>
</div>
<div style={{ background: T.wrnL, borderRadius: T.r, padding: '14px 16px', borderLeft: `4px solid ${T.wrn}` }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<svg width="18" height="18" viewBox="0 0 24 24" fill={T.wrn}><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>
<div>
<div style={{ fontSize: 14, fontWeight: 500, color: T.tx }}>2 份随访报告待审核</div>
<div style={{ fontSize: 12, color: T.tx3, marginTop: 2 }}>截止今日 18:00</div>
</div>
</div>
</div>
</div>
<BottomTabBar tabs={['工作台', '患者', '消息', '我的']} active="工作台" />
</div>
);
}
// ─── 屏幕2待办收件箱 ───
function ActionInbox() {
const filterTabs = ['全部', '异常', '随访', '咨询'];
const todos = [
{ type: '异常', typeColor: T.dan, typeBg: T.danL, patient: '张三', desc: '收缩压 158mmHg', time: '10分钟前', urgent: true },
{ type: '随访', typeColor: T.acc, typeBg: T.accL, patient: '李四', desc: '肾功能复查到期', time: '昨天', urgent: false },
{ type: '咨询', typeColor: T.pri, typeBg: T.priL, patient: '王五', desc: '咨询透析方案', time: '昨天', urgent: false },
{ type: '异常', typeColor: T.dan, typeBg: T.danL, patient: '赵六', desc: '血糖 15.2mmol/L', time: '2天前', urgent: true },
{ type: '随访', typeColor: T.acc, typeBg: T.accL, patient: '钱七', desc: '透析处方到期', time: '3天前', urgent: false },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="待办事项" />
{/* 筛选标签 */}
<div style={{ display: 'flex', gap: 8, padding: '12px 20px', flexShrink: 0 }}>
{filterTabs.map((tab, i) => (
<div key={tab} style={{
padding: '6px 16px', borderRadius: 20, fontSize: 13, fontWeight: i === 0 ? 600 : 400,
background: i === 0 ? T.pri : T.card, color: i === 0 ? '#fff' : T.tx2,
border: `1px solid ${i === 0 ? T.pri : T.bd}`, cursor: 'pointer',
}}>{tab}</div>
))}
</div>
{/* 待办列表 */}
<div style={{ flex: 1, overflow: 'auto', padding: '0 20px 20px' }}>
{todos.map((todo, i) => (
<div key={i} style={{ background: T.card, borderRadius: T.r, padding: '14px 16px', marginBottom: 10, boxShadow: '0 1px 8px rgba(0,0,0,0.03)', display: 'flex', alignItems: 'center', gap: 12 }}>
{/* 类型标签 */}
<Tag color={todo.typeColor} bg={todo.typeBg} fontSize={11}>{todo.type}</Tag>
{/* 内容 */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 15, fontWeight: 600, color: T.tx }}>{todo.patient}</span>
{todo.urgent && <Tag color="#fff" bg={T.dan} fontSize={10}>紧急</Tag>}
</div>
<div style={{ fontSize: 13, color: T.tx2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{todo.desc}</div>
</div>
{/* 时间 */}
<div style={{ fontSize: 11, color: T.tx3, flexShrink: 0, textAlign: 'right', minWidth: 52 }}>
<div>{todo.time}</div>
</div>
{/* 箭头 */}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={T.tx3} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
<path d="M9 18l6-6-6-6"/>
</svg>
</div>
))}
</div>
</div>
);
}
// ─── 屏幕3在线咨询列表 ───
function ConsultList() {
const statusTabs = ['进行中', '已结束'];
const consults = [
{ name: '张三', msg: '好的,谢谢医生', time: '10分钟前', unread: 1, avatar: '张', avatarBg: T.priL },
{ name: '王五', msg: '透析方案已调整', time: '1小时前', unread: 0, avatar: '王', avatarBg: T.accL },
{ name: '赵六', msg: '血糖控制情况如何', time: '昨天', unread: 3, avatar: '赵', avatarBg: T.wrnL },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="在线咨询" />
{/* 状态 Tab */}
<div style={{ display: 'flex', gap: 0, padding: '12px 20px 0', flexShrink: 0 }}>
{statusTabs.map((tab, i) => (
<div key={tab} style={{
flex: 1, textAlign: 'center', padding: '10px 0', fontSize: 14, fontWeight: i === 0 ? 600 : 400,
color: i === 0 ? T.pri : T.tx3,
borderBottom: i === 0 ? `2.5px solid ${T.pri}` : `1.5px solid ${T.bdL}`,
}}>{tab}</div>
))}
</div>
{/* 咨询列表 */}
<div style={{ flex: 1, overflow: 'auto', padding: '12px 20px 20px' }}>
{consults.map((c, i) => (
<div key={i} style={{ background: T.card, borderRadius: T.r, padding: '14px 16px', marginBottom: 10, boxShadow: '0 1px 8px rgba(0,0,0,0.03)', display: 'flex', alignItems: 'center', gap: 12 }}>
{/* 头像 */}
<div style={{ width: 44, height: 44, borderRadius: 22, background: c.avatarBg, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.pri }}>{c.avatar}</span>
</div>
{/* 内容 */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: 15, fontWeight: 600, color: T.tx }}>{c.name}</span>
<span style={{ fontSize: 12, color: T.tx3 }}>{c.time}</span>
</div>
<div style={{ fontSize: 13, color: T.tx2, marginTop: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.msg}</div>
</div>
{/* 未读 */}
{c.unread > 0 && (
<div style={{ minWidth: 20, height: 20, borderRadius: 10, background: T.dan, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ fontSize: 11, fontWeight: 700, color: '#fff' }}>{c.unread}</span>
</div>
)}
{/* 箭头 */}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={T.tx3} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
<path d="M9 18l6-6-6-6"/>
</svg>
</div>
))}
</div>
</div>
);
}
// ─── 屏幕4随访管理 ───
function FollowUpList() {
const filterTabs = ['待随访', '已完成', '已过期'];
const followUps = [
{ patient: '张三', type: '门诊随访', date: '5月10日', status: '待随访', statusColor: T.pri, statusBg: T.priL, data: '血压 130/85' },
{ patient: '李四', type: '电话随访', date: '5月8日', status: '已完成', statusColor: T.acc, statusBg: T.accL, data: '血压 125/80' },
{ patient: '王五', type: '线上随访', date: '5月5日', status: '已过期', statusColor: T.dan, statusBg: T.danL, data: '血糖 8.2' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="随访管理" />
{/* 筛选标签 */}
<div style={{ display: 'flex', gap: 8, padding: '12px 20px', flexShrink: 0 }}>
{filterTabs.map((tab, i) => (
<div key={tab} style={{
padding: '6px 16px', borderRadius: 20, fontSize: 13, fontWeight: i === 0 ? 600 : 400,
background: i === 0 ? T.pri : T.card, color: i === 0 ? '#fff' : T.tx2,
border: `1px solid ${i === 0 ? T.pri : T.bd}`, cursor: 'pointer',
}}>{tab}</div>
))}
</div>
{/* 随访卡片列表 */}
<div style={{ flex: 1, overflow: 'auto', padding: '0 20px 20px' }}>
{followUps.map((f, i) => (
<div key={i} style={{ background: T.card, borderRadius: T.r, padding: '16px', marginBottom: 10, boxShadow: '0 1px 8px rgba(0,0,0,0.03)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 8 }}>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 16, fontWeight: 600, color: T.tx }}>{f.patient}</span>
<Tag color={f.statusColor} bg={f.statusBg}>{f.status}</Tag>
</div>
<div style={{ fontSize: 13, color: T.tx3, marginTop: 4 }}>{f.type}</div>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: 13, color: T.tx2, fontWeight: 500 }}>{f.date}</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', background: T.bg, borderRadius: T.rSm, padding: '10px 12px' }}>
<div style={{ fontSize: 12, color: T.tx3 }}>最近数据</div>
<div style={{ fontSize: 14, fontWeight: 600, color: T.tx }}>{f.data}</div>
</div>
</div>
))}
</div>
</div>
);
}
// ─── 屏幕5患者管理 ───
function PatientList() {
const patients = [
{ name: '张三', age: 58, gender: '男', diagnosis: '高血压 · 慢性肾病III期', lastVisit: '5月8日', initial: '张', bg: T.priL },
{ name: '李四', age: 65, gender: '男', diagnosis: '2型糖尿病 · 肾功能不全', lastVisit: '5月6日', initial: '李', bg: T.accL },
{ name: '王五', age: 72, gender: '女', diagnosis: '高血压 · 血液透析', lastVisit: '5月5日', initial: '王', bg: T.wrnL },
{ name: '赵六', age: 45, gender: '男', diagnosis: '2型糖尿病', lastVisit: '4月28日', initial: '赵', bg: T.danL },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="患者管理" />
{/* 搜索栏 */}
<div style={{ padding: '12px 20px', flexShrink: 0 }}>
<div style={{ background: T.card, borderRadius: 12, height: 42, display: 'flex', alignItems: 'center', padding: '0 14px', gap: 10, border: `1px solid ${T.bd}` }}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={T.tx3} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/>
</svg>
<span style={{ fontSize: 14, color: T.tx3 }}>搜索患者姓名</span>
</div>
</div>
{/* 患者列表 */}
<div style={{ flex: 1, overflow: 'auto', padding: '0 20px 20px' }}>
{patients.map((p, i) => (
<div key={i} style={{ background: T.card, borderRadius: T.r, padding: '14px 16px', marginBottom: 10, boxShadow: '0 1px 8px rgba(0,0,0,0.03)', display: 'flex', alignItems: 'center', gap: 12 }}>
{/* 头像 */}
<div style={{ width: 46, height: 46, borderRadius: 23, background: p.bg, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ fontFamily: T.serif, fontSize: 20, fontWeight: 700, color: T.pri }}>{p.initial}</span>
</div>
{/* 内容 */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 15, fontWeight: 600, color: T.tx }}>{p.name}</span>
<span style={{ fontSize: 12, color: T.tx3 }}>{p.age} · {p.gender}</span>
</div>
<div style={{ fontSize: 13, color: T.pri, marginTop: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.diagnosis}</div>
<div style={{ fontSize: 12, color: T.tx3, marginTop: 3 }}>最近 {p.lastVisit}</div>
</div>
{/* 箭头 */}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={T.tx3} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
<path d="M9 18l6-6-6-6"/>
</svg>
</div>
))}
</div>
</div>
);
}
// ─── 渲染 ───
function App() {
return (
<div className="screens">
<IosFrame width={360} height={780} label="医生工作台">
<DoctorHome />
</IosFrame>
<IosFrame width={360} height={780} label="待办收件箱">
<ActionInbox />
</IosFrame>
<IosFrame width={360} height={780} label="在线咨询">
<ConsultList />
</IosFrame>
<IosFrame width={360} height={780} label="随访管理">
<FollowUpList />
</IosFrame>
<IosFrame width={360} height={780} label="患者管理">
<PatientList />
</IosFrame>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>

BIN
docs/design/mp-11-top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

View File

@@ -0,0 +1,646 @@
<!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: 1200px; 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; }
/* Scrollbar hide */
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
</style>
</head>
<body>
<div class="page-title">HMS 小程序 · 医生端临床</div>
<div class="note">医生端深靛蓝设计系统 — 临床管理模块。6 个屏幕:告警列表、告警详情、透析列表、透析创建、处方列表、处方详情。</div>
<div id="root"></div>
<script type="text/babel">
// ─── iOS 设备框 ───
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 = 340, height = 740, time = '9:41', battery = 85, darkStatus = false, label }) {
const statusColor = darkStatus ? '#fff' : '#000';
return (
<div class="screen-wrap">
<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 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 className="no-scrollbar" style={{ ...iosFrameStyles.content }}>{children}</div>
<div style={iosFrameStyles.homeIndicator} />
</div>
</div>
{label && <div class="screen-label">{label}</div>}
</div>
);
}
// ─── 设计 Token — 医生端靛蓝变体 ───
const T = {
pri: '#3A6B8C', priL: '#D4E5F0', priD: '#2A4F6A',
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', position: 'relative', background: T.bg, borderBottom: `1px solid ${T.bdL}`, flexShrink: 0 }}>
<svg style={{ position: 'absolute', left: 16 }} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke={T.tx} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M15 18l-6-6 6-6"/>
</svg>
<span style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.tx }}>{title}</span>
</div>
);
}
function Tag({ label, color = T.acc, bg = T.accL }) {
return (
<span style={{ display: 'inline-block', fontSize: 11, color, background: bg, padding: '2px 8px', borderRadius: 6, fontWeight: 500 }}>{label}</span>
);
}
function FilterTabs({ tabs, active = 0 }) {
return (
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
{tabs.map((t, i) => (
<div key={i} style={{
padding: '6px 14px', borderRadius: 20, fontSize: 13, fontWeight: i === active ? 600 : 400,
color: i === active ? '#fff' : T.tx2, background: i === active ? T.pri : T.card,
border: `1px solid ${i === active ? T.pri : T.bd}`, cursor: 'pointer', transition: 'all 0.2s',
}}>{t}</div>
))}
</div>
);
}
function InfoRow({ label, value }) {
return (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 0', borderBottom: `1px solid ${T.bdL}` }}>
<span style={{ fontSize: 13, color: T.tx3 }}>{label}</span>
<span style={{ fontSize: 14, color: T.tx, fontWeight: 500 }}>{value}</span>
</div>
);
}
function FormField({ label, value, placeholder, multiline }) {
return (
<div style={{ marginBottom: 14 }}>
<div style={{ fontSize: 13, color: T.tx2, marginBottom: 6, fontWeight: 500 }}>{label}</div>
<div style={{
height: multiline ? 80 : 52, background: T.card, border: `1.5px solid ${T.bd}`,
borderRadius: T.r, display: 'flex', alignItems: multiline ? 'flex-start' : 'center',
padding: multiline ? '14px 16px' : '0 16px', fontSize: 14, color: value ? T.tx : T.tx3,
fontFamily: T.sans, lineHeight: multiline ? 1.5 : 1,
}}>{value || placeholder}</div>
</div>
);
}
function ActionButton({ label, primary, danger, style: extraStyle }) {
const isDanger = danger;
const isPrimary = primary;
return (
<div style={{
height: 48, borderRadius: T.r, display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 15, fontWeight: 600, cursor: 'pointer',
background: isDanger ? 'transparent' : (isPrimary ? T.pri : 'transparent'),
color: isDanger ? T.dan : (isPrimary ? '#fff' : T.pri),
border: isDanger ? `1.5px solid ${T.dan}` : (isPrimary ? 'none' : `1.5px solid ${T.pri}`),
...extraStyle,
}}>{label}</div>
);
}
// ─── 屏幕1告警列表 ───
function AlertList() {
const alerts = [
{ patient: '张三', type: '血压异常', urgency: '紧急', urgencyColor: T.dan, urgencyBg: T.danL,
value: '158', unit: 'mmHg', ref: '参考 <140', time: '10分钟前', status: '待处理', statusColor: T.dan, statusBg: T.danL },
{ patient: '赵六', type: '血糖异常', urgency: '紧急', urgencyColor: T.dan, urgencyBg: T.danL,
value: '15.2', unit: 'mmol/L', ref: '参考 <7.0', time: '2小时前', status: '待处理', statusColor: T.dan, statusBg: T.danL },
{ patient: '李四', type: '体重异常', urgency: '普通', urgencyColor: T.wrn, urgencyBg: T.wrnL,
value: '+2.3', unit: 'kg', ref: '周变化', time: '昨天', status: '已处理', statusColor: T.acc, statusBg: T.accL },
{ patient: '陈七', type: '血压异常', urgency: '紧急', urgencyColor: T.dan, urgencyBg: T.danL,
value: '162', unit: 'mmHg', ref: '参考 <140', time: '昨天', status: '待处理', statusColor: T.dan, statusBg: T.danL },
{ patient: '周八', type: '心率异常', urgency: '普通', urgencyColor: T.wrn, urgencyBg: T.wrnL,
value: '108', unit: 'bpm', ref: '参考 60-100', time: '2天前', status: '已处理', statusColor: T.acc, statusBg: T.accL },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="异常告警" />
<div style={{ flex: 1, overflow: 'auto', padding: '16px 20px 24px' }}>
{/* 筛选标签 */}
<FilterTabs tabs={['全部', '高优', '中优', '低优']} active={0} />
{/* 统计栏 */}
<div style={{ display: 'flex', gap: 10, marginBottom: 18 }}>
{[
{ label: '今日', value: '8', color: T.pri },
{ label: '待处理', value: '5', color: T.dan },
{ label: '已处理', value: '3', color: T.acc },
].map((s, i) => (
<div key={i} style={{ flex: 1, background: T.card, borderRadius: T.rSm, padding: '12px 14px', textAlign: 'center', boxShadow: '0 1px 8px rgba(0,0,0,0.04)' }}>
<div style={{ fontFamily: T.serif, fontSize: 22, fontWeight: 700, color: s.color }}>{s.value}</div>
<div style={{ fontSize: 11, color: T.tx3, marginTop: 2 }}>{s.label}</div>
</div>
))}
</div>
{/* 告警列表 */}
{alerts.map((a, i) => (
<div key={i} style={{
background: T.card, borderRadius: T.r, padding: '16px 18px', marginBottom: 12,
boxShadow: '0 2px 12px rgba(0,0,0,0.04)', borderLeft: `4px solid ${a.urgencyColor}`,
}}>
{/* 头部:患者 + 类型 + 紧急度 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontFamily: T.serif, fontSize: 16, fontWeight: 700, color: T.tx }}>{a.patient}</span>
<Tag label={a.type} color={T.pri} bg={T.priL} />
</div>
<Tag label={a.urgency} color={a.urgencyColor} bg={a.urgencyBg} />
</div>
{/* 异常值 */}
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6, marginBottom: 10 }}>
<span style={{ fontFamily: T.serif, fontSize: 28, fontWeight: 700, color: a.urgencyColor }}>{a.value}</span>
<span style={{ fontSize: 13, color: T.tx2 }}>{a.unit}</span>
<span style={{ fontSize: 12, color: T.tx3, marginLeft: 4 }}>{a.ref}</span>
</div>
{/* 底部:时间 + 状态 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: 12, color: T.tx3 }}>{a.time}</span>
<Tag label={a.status} color={a.statusColor} bg={a.statusBg} />
</div>
</div>
))}
</div>
</div>
);
}
// ─── 屏幕2告警详情 ───
function AlertDetail() {
const history = [
{ date: '5/8', value: '158/95', note: '偏高', color: T.dan },
{ date: '5/7', value: '145/90', note: '偏高', color: T.wrn },
{ date: '5/6', value: '132/85', note: '正常', color: T.acc },
{ date: '5/5', value: '138/88', note: '偏高', color: T.wrn },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="告警详情" />
<div style={{ flex: 1, overflow: 'auto', padding: '16px 20px 24px' }}>
{/* 患者信息 */}
<div style={{
background: T.card, borderRadius: T.r, padding: '18px 20px', marginBottom: 14,
boxShadow: '0 2px 12px rgba(0,0,0,0.04)', display: 'flex', alignItems: 'center', gap: 14,
}}>
<div style={{
width: 44, height: 44, borderRadius: 22, background: T.priL,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.pri,
}}></div>
<div style={{ flex: 1 }}>
<div style={{ fontFamily: T.serif, fontSize: 17, fontWeight: 700, color: T.tx, marginBottom: 3 }}>张三</div>
<div style={{ fontSize: 13, color: T.tx3 }}>58 · · 高血压 · 慢性肾病III期</div>
</div>
</div>
{/* 告警信息卡片 */}
<div style={{
background: T.card, borderRadius: T.r, padding: '18px 20px', marginBottom: 14,
boxShadow: '0 2px 12px rgba(0,0,0,0.04)', borderLeft: `4px solid ${T.dan}`,
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 14 }}>
<span style={{ fontSize: 14, fontWeight: 600, color: T.tx }}>告警信息</span>
<Tag label="紧急" color={T.dan} bg={T.danL} />
</div>
<InfoRow label="类型" value="血压异常" />
<InfoRow label="触发时间" value="今天 08:30" />
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 0' }}>
<span style={{ fontSize: 13, color: T.tx3 }}>异常值</span>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4 }}>
<span style={{ fontFamily: T.serif, fontSize: 22, fontWeight: 700, color: T.dan }}>158</span>
<span style={{ fontSize: 13, color: T.tx2 }}>mmHg</span>
<span style={{ fontSize: 12, color: T.tx3, marginLeft: 4 }}>参考 &lt;140</span>
</div>
</div>
</div>
{/* 近期数据 */}
<div style={{
background: T.card, borderRadius: T.r, padding: '18px 20px', marginBottom: 14,
boxShadow: '0 2px 12px rgba(0,0,0,0.04)',
}}>
<div style={{ fontSize: 14, fontWeight: 600, color: T.tx, marginBottom: 12 }}>近期数据</div>
{history.map((h, i) => (
<div key={i} style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '10px 0', borderBottom: i < history.length - 1 ? `1px solid ${T.bdL}` : 'none',
}}>
<span style={{ fontSize: 13, color: T.tx2, width: 40 }}>{h.date}</span>
<span style={{ fontFamily: T.serif, fontSize: 15, fontWeight: 600, color: T.tx, flex: 1 }}>{h.value}</span>
<Tag label={h.note} color={h.color} bg={h.color === T.dan ? T.danL : (h.color === T.wrn ? T.wrnL : T.accL)} />
</div>
))}
</div>
{/* 处理意见 */}
<div style={{
background: T.card, borderRadius: T.r, padding: '18px 20px', marginBottom: 14,
boxShadow: '0 2px 12px rgba(0,0,0,0.04)',
}}>
<div style={{ fontSize: 14, fontWeight: 600, color: T.tx, marginBottom: 10 }}>处理意见</div>
<div style={{
height: 72, background: T.bg, borderRadius: T.rSm, border: `1px solid ${T.bd}`,
padding: '12px 14px', fontSize: 13, color: T.tx3, lineHeight: 1.5,
}}>请输入处理意见...</div>
</div>
{/* 操作按钮 */}
<div style={{ display: 'flex', gap: 12 }}>
<ActionButton label="标记已处理" primary style={{ flex: 1 }} />
<ActionButton label="联系患者" style={{ flex: 1 }} />
</div>
</div>
</div>
);
}
// ─── 屏幕3透析列表 ───
function DialysisList() {
const records = [
{ patient: '张三', bed: '3床', mode: 'HD', time: '08:00-12:00', status: '进行中', statusColor: T.wrn, statusBg: T.wrnL,
detail: '超滤 1.8L/2.3L' },
{ patient: '李四', bed: '5床', mode: 'HDF', time: '08:30-12:30', status: '进行中', statusColor: T.wrn, statusBg: T.wrnL,
detail: '超滤 1.2L/2.0L' },
{ patient: '王五', bed: '1床', mode: 'HD', time: '13:00-17:00', status: '待开始', statusColor: T.tx3, statusBg: T.surface,
detail: null },
{ patient: '赵六', bed: '2床', mode: 'HD', time: '08:00-12:00', status: '已完成', statusColor: T.acc, statusBg: T.accL,
detail: '超滤 2.1L' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="透析管理" />
<div style={{ flex: 1, overflow: 'auto', padding: '16px 20px 24px' }}>
{/* 今日统计 */}
<div style={{ display: 'flex', gap: 10, marginBottom: 18 }}>
{[
{ label: '已透析', value: '5', color: T.acc },
{ label: '进行中', value: '2', color: T.wrn },
{ label: '待开始', value: '3', color: T.tx3 },
].map((s, i) => (
<div key={i} style={{
flex: 1, background: T.card, borderRadius: T.rSm, padding: '14px 14px', textAlign: 'center',
boxShadow: '0 1px 8px rgba(0,0,0,0.04)',
}}>
<div style={{ fontFamily: T.serif, fontSize: 24, fontWeight: 700, color: s.color }}>{s.value}</div>
<div style={{ fontSize: 11, color: T.tx3, marginTop: 2 }}>{s.label}</div>
</div>
))}
</div>
{/* 透析记录 */}
{records.map((r, i) => (
<div key={i} style={{
background: T.card, borderRadius: T.r, padding: '16px 18px', marginBottom: 12,
boxShadow: '0 2px 12px rgba(0,0,0,0.04)',
}}>
{/* 头部 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontFamily: T.serif, fontSize: 16, fontWeight: 700, color: T.tx }}>{r.patient}</span>
<Tag label={r.bed} color={T.priD} bg={T.priL} />
</div>
<Tag label={r.status} color={r.statusColor} bg={r.statusBg} />
</div>
{/* 信息行 */}
<div style={{ display: 'flex', gap: 16, marginBottom: r.detail ? 10 : 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<span style={{ fontSize: 12, color: T.tx3 }}>模式</span>
<span style={{ fontSize: 13, fontWeight: 600, color: T.pri }}>{r.mode}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<span style={{ fontSize: 12, color: T.tx3 }}>时段</span>
<span style={{ fontSize: 13, color: T.tx }}>{r.time}</span>
</div>
</div>
{/* 关键指标 */}
{r.detail && (
<div style={{
background: T.bg, borderRadius: T.rXs, padding: '8px 12px',
fontSize: 13, color: T.tx2,
}}>
{r.detail}
</div>
)}
</div>
))}
{/* FAB */}
<div style={{
position: 'sticky', bottom: 10, display: 'flex', justifyContent: 'flex-end', pointerEvents: 'none',
}}>
<div style={{
width: 52, height: 52, borderRadius: 26, background: T.pri,
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: '0 4px 20px rgba(58,107,140,0.4)', pointerEvents: 'auto', cursor: 'pointer',
}}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round">
<path d="M12 5v14M5 12h14"/>
</svg>
</div>
</div>
</div>
</div>
);
}
// ─── 屏幕4透析创建 ───
function DialysisCreate() {
const modes = ['HD', 'HDF', 'HF'];
const activeMode = 0;
const dialysate = ['碳酸氢盐', '醋酸盐'];
const anticoagulant = ['低分子肝素', '普通肝素', '无'];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="新增透析记录" />
<div style={{ flex: 1, overflow: 'auto', padding: '16px 20px 24px' }}>
<FormField label="患者选择" value="张三" placeholder="请选择患者" />
{/* 透析模式 — 按钮横排 */}
<div style={{ marginBottom: 14 }}>
<div style={{ fontSize: 13, color: T.tx2, marginBottom: 6, fontWeight: 500 }}>透析模式</div>
<div style={{ display: 'flex', gap: 10 }}>
{modes.map((m, i) => (
<div key={i} style={{
flex: 1, height: 44, borderRadius: T.rSm, display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 14, fontWeight: i === activeMode ? 600 : 400,
color: i === activeMode ? '#fff' : T.tx2,
background: i === activeMode ? T.pri : T.card,
border: `1.5px solid ${i === activeMode ? T.pri : T.bd}`,
cursor: 'pointer',
}}>{m}</div>
))}
</div>
</div>
<FormField label="床位号" value="3床" placeholder="请输入床位号" />
<FormField label="开始时间" value="08:00" placeholder="请选择时间" />
<FormField label="计划时长" value="4小时" placeholder="请输入时长" />
<FormField label="血流量" value="250 ml/min" placeholder="请输入血流量" />
{/* 透析液选择 */}
<div style={{ marginBottom: 14 }}>
<div style={{ fontSize: 13, color: T.tx2, marginBottom: 6, fontWeight: 500 }}>透析液</div>
<div style={{ display: 'flex', gap: 10 }}>
{dialysate.map((d, i) => (
<div key={i} style={{
flex: 1, height: 44, borderRadius: T.rSm, display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 13, fontWeight: i === 0 ? 600 : 400,
color: i === 0 ? '#fff' : T.tx2,
background: i === 0 ? T.pri : T.card,
border: `1.5px solid ${i === 0 ? T.pri : T.bd}`,
cursor: 'pointer',
}}>{d}</div>
))}
</div>
</div>
{/* 抗凝剂选择 */}
<div style={{ marginBottom: 14 }}>
<div style={{ fontSize: 13, color: T.tx2, marginBottom: 6, fontWeight: 500 }}>抗凝剂</div>
<div style={{ display: 'flex', gap: 8 }}>
{anticoagulant.map((a, i) => (
<div key={i} style={{
flex: 1, height: 44, borderRadius: T.rSm, display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 12, fontWeight: i === 0 ? 600 : 400,
color: i === 0 ? '#fff' : T.tx2,
background: i === 0 ? T.pri : T.card,
border: `1.5px solid ${i === 0 ? T.pri : T.bd}`,
cursor: 'pointer',
}}>{a}</div>
))}
</div>
</div>
<FormField label="目标超滤量" value="2.3 L" placeholder="请输入目标超滤量" />
{/* 底部按钮 */}
<div style={{ marginTop: 8 }}>
<ActionButton label="开始透析" primary />
</div>
</div>
</div>
);
}
// ─── 屏幕5处方列表 ───
function PrescriptionList() {
const prescriptions = [
{ patient: '张三', id: 'PX-20250401', mode: 'HD', freq: '每周3次', date: '4月1日', status: '生效中', statusColor: T.acc, statusBg: T.accL },
{ patient: '李四', id: 'PX-20250315', mode: 'HDF', freq: '每周2次', date: '3月15日', status: '生效中', statusColor: T.acc, statusBg: T.accL },
{ patient: '王五', id: 'PX-20250201', mode: 'HD', freq: '每周3次', date: '2月1日', status: '已停用', statusColor: T.tx3, statusBg: T.surface },
{ patient: '赵六', id: 'PX-20250110', mode: 'HDF', freq: '每周2次', date: '1月10日', status: '生效中', statusColor: T.acc, statusBg: T.accL },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="透析处方" />
<div style={{ flex: 1, overflow: 'auto', padding: '16px 20px 24px' }}>
{/* 筛选 */}
<FilterTabs tabs={['生效中', '全部']} active={0} />
{/* 处方卡片 */}
{prescriptions.map((p, i) => (
<div key={i} style={{
background: T.card, borderRadius: T.r, padding: '16px 18px', marginBottom: 12,
boxShadow: '0 2px 12px rgba(0,0,0,0.04)',
}}>
{/* 头部 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontFamily: T.serif, fontSize: 16, fontWeight: 700, color: T.tx }}>{p.patient}</span>
<span style={{ fontSize: 12, color: T.tx3 }}>{p.id}</span>
</div>
<Tag label={p.status} color={p.statusColor} bg={p.statusBg} />
</div>
{/* 信息行 */}
<div style={{ display: 'flex', gap: 20, marginBottom: 4 }}>
<div>
<div style={{ fontSize: 11, color: T.tx3, marginBottom: 2 }}>透析模式</div>
<div style={{ fontSize: 14, fontWeight: 600, color: T.pri }}>{p.mode}</div>
</div>
<div>
<div style={{ fontSize: 11, color: T.tx3, marginBottom: 2 }}>频率</div>
<div style={{ fontSize: 14, color: T.tx }}>{p.freq}</div>
</div>
<div>
<div style={{ fontSize: 11, color: T.tx3, marginBottom: 2 }}>生效日期</div>
<div style={{ fontSize: 14, color: T.tx }}>{p.date}</div>
</div>
</div>
</div>
))}
</div>
</div>
);
}
// ─── 屏幕6处方详情 ───
function PrescriptionDetail() {
const params = [
{ label: '处方编号', value: 'PX-20250401' },
{ label: '透析模式', value: '血液透析HD' },
{ label: '频率', value: '每周 3 次(周一/三/五)' },
{ label: '单次时长', value: '4 小时' },
{ label: '血流量', value: '250 ml/min' },
{ label: '透析液流速', value: '500 ml/min' },
{ label: '透析液', value: '碳酸氢盐' },
{ label: '抗凝剂', value: '低分子肝素 4000IU' },
{ label: '干体重', value: '65.0 kg' },
{ label: '目标超滤', value: '2.0-2.5 L' },
{ label: 'Kt/V 目标', value: '≥1.2' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="处方详情" />
<div style={{ flex: 1, overflow: 'auto', padding: '16px 20px 24px' }}>
{/* 患者信息 */}
<div style={{
background: T.card, borderRadius: T.r, padding: '18px 20px', marginBottom: 14,
boxShadow: '0 2px 12px rgba(0,0,0,0.04)', display: 'flex', alignItems: 'center', gap: 14,
}}>
<div style={{
width: 44, height: 44, borderRadius: 22, background: T.priL,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.pri,
}}></div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 3 }}>
<span style={{ fontFamily: T.serif, fontSize: 17, fontWeight: 700, color: T.tx }}>张三</span>
<Tag label="生效中" color={T.acc} bg={T.accL} />
</div>
<div style={{ fontSize: 13, color: T.tx3 }}>58 · </div>
</div>
</div>
{/* 详细参数 */}
<div style={{
background: T.card, borderRadius: T.r, padding: '4px 20px', marginBottom: 14,
boxShadow: '0 2px 12px rgba(0,0,0,0.04)',
}}>
<div style={{ fontSize: 14, fontWeight: 600, color: T.tx, padding: '14px 0 4px' }}>处方参数</div>
{params.map((p, i) => (
<div key={i} style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '11px 0', borderBottom: i < params.length - 1 ? `1px solid ${T.bdL}` : 'none',
}}>
<span style={{ fontSize: 13, color: T.tx3 }}>{p.label}</span>
<span style={{ fontSize: 14, color: T.tx, fontWeight: 500, textAlign: 'right', maxWidth: '55%' }}>{p.value}</span>
</div>
))}
</div>
{/* 医生信息 */}
<div style={{
background: T.card, borderRadius: T.r, padding: '16px 20px', marginBottom: 14,
boxShadow: '0 2px 12px rgba(0,0,0,0.04)',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontSize: 12, color: T.tx3, marginBottom: 3 }}>开方医生</div>
<div style={{ fontSize: 14, color: T.tx, fontWeight: 500 }}>李医生 · 肾内科</div>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: 12, color: T.tx3, marginBottom: 3 }}>生效日期</div>
<div style={{ fontSize: 14, color: T.tx, fontWeight: 500 }}>2025年4月1日</div>
</div>
</div>
</div>
{/* 底部操作 */}
<div style={{ display: 'flex', gap: 12 }}>
<ActionButton label="调整处方" primary style={{ flex: 1 }} />
<ActionButton label="停用处方" danger style={{ flex: 1 }} />
</div>
</div>
</div>
);
}
// ─── 渲染 ───
function App() {
return (
<div class="screens">
<IosFrame label="告警列表" time="9:41" battery={85}>
<AlertList />
</IosFrame>
<IosFrame label="告警详情" time="9:41" battery={85}>
<AlertDetail />
</IosFrame>
<IosFrame label="透析列表" time="9:42" battery={84}>
<DialysisList />
</IosFrame>
<IosFrame label="透析创建" time="9:43" battery={84}>
<DialysisCreate />
</IosFrame>
<IosFrame label="处方列表" time="9:43" battery={83}>
<PrescriptionList />
</IosFrame>
<IosFrame label="处方详情" time="9:44" battery={83}>
<PrescriptionDetail />
</IosFrame>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>