用户界面实现指引
本文档是 Reflekt Health 项目的 UI 实现指南,为前端开发提供技术规范、Mock 数据规范、API 联调方式和跨端共享约定。
重要说明:本文档基于
docs/archon/docs/目录下的需求文档编写,不引用reflekt-health-prd仓库的任何目录结构作为实现依据。实际代码仓库的目录结构由对应开发团队自行定义。核心参考文档(均为
docs/archon/docs/下的需求文档):
1. 三端定位与技术栈
详见 软件端划分与特征定义 §2。
1.1 平台概览
| 平台 | 技术栈 | 设备尺寸 | 用户角色 | 技术特点 |
|---|---|---|---|---|
| Family App(Family App) | UniApp + Vue 3 + TypeScript | 390×844px(iPhone 16 Pro) | 子女/家属(35~55 岁) | 一套代码编译 iOS/Android 双端 |
| Luma App(Luma 智能音箱) | 原生 Android(Kotlin) + Web 前端 | 1280×800px(音箱屏幕) | 老年人(65~85 岁) | 语音优先,屏幕辅助,嵌入式固件 |
| Admin(管理后台) | Vue 3 + TypeScript + ElementPlus | PC Web(最小 1024px) | 运营团队 | 响应式 Web |
1.2 目录结构约定
以下目录结构为实现指引,源自 docs/archon/docs/ 需求文档,非实际代码仓库路径。 实际代码仓库的目录由开发团队自行组织。
# Family App
fap-app/
├── src/
│ ├── pages/
│ │ ├── auth/login.vue # 登录页
│ │ ├── auth/register.vue # 注册页
│ │ ├── home/index.vue # 首页多状态(calm/alert/reminder/family/offline/help)
│ │ ├── devices/ # 设备管理
│ │ ├── messages/ # 消息中心
│ │ └── profile/ # 个人设置
│ ├── components/
│ │ ├── HeroCard.vue # 状态 Hero 卡片
│ │ ├── QuickSend.vue # Quick Send 网格
│ │ ├── BottomNav.vue # 底部导航
│ │ └── ElderAvatar.vue # 老人头像(状态环+状态点)
│ ├── styles/
│ │ └── tokens.css # 设计令牌(来自 ui_design_specification.md §1.1)
│ ├── api/
│ │ └── index.ts # 请求封装(JWT Token、响应拦截)
│ └── mocks/
│ ├── browser.ts
│ ├── handlers.ts
│ └── data/
│ ├── family.json # 首页状态 Mock
│ ├── alert.json # 告警 Mock
│ └── device.json # 设备 Mock
├── package.json
└── uni-app.config.js
# Luma App
luma-firmware/
├── app/
│ ├── src/main/kotlin/
│ │ ├── service/
│ │ │ ├── WebSocketService.kt # WebSocket 通信
│ │ │ ├── TTSService.kt # TTS 播报
│ │ │ └── WakeWordService.kt # 语音唤醒
│ │ └── ui/screen/ # 屏幕状态管理
│ └── src/main/assets/
│ └── screens/
│ ├── home_calm.html # 首页-正常状态(CALM)
│ ├── home_alert.html # 首页-预警询问(ALERT)
│ ├── home_offline.html # 首页-离线状态(OFFLINE)
│ ├── home_help.html # 首页-紧急帮助(HELP)
│ └── ... # 更多屏幕见 user_interface_structure.md §2
└── build.gradle
# Admin 管理后台
admin-web/
├── src/
│ ├── views/ # 页面视图
│ ├── api/ # 接口调用层
│ ├── store/ # Pinia 状态管理
│ └── router/ # Vue Router
├── mocks/
│ ├── handlers.ts
│ └── data/
└── package.json
1.3 页面文件命名规范(强制)
所有 UI 页面文件必须使用全小写 + 下划线分隔符,不得出现大写字母。
- ✅ 正确:
home_calm、home_alert、devices - ❌ 错误:
HomeCalm、HomeAlert、Devices
2. Family App 实现规范
2.1 技术栈详解
| 层级 | 技术选型 | 说明 |
|---|---|---|
| 跨端框架 | UniApp + Vue 3 + TypeScript | 一套代码编译至 iOS / Android |
| 样式方案 | CSS Variables + Scoped CSS | 共享设计令牌,不依赖 Tailwind |
| 状态管理 | Pinia | UniApp 官方推荐 |
| HTTP 客户端 | uni.request | API 调用统一出口 |
| 推送 SDK | uni-app-push(APNs + FCM) | iOS APNs / Android FCM 双端 |
| 页面路由 | UniApp Router(pages.json) | 声明式路由,路由守卫鉴权 |
| Mock | MSW(Mock Service Worker) | 拦截请求,本地开发不依赖后端 |
| 构建输出 | iOS .ipa / Android .apk / .aab | App Store / Google Play |
禁止使用:jQuery、Zepto、Bootstrap(不符合适老化要求)。
2.2 设计令牌接入
从 ui_design_specification.md §1.1 提取设计令牌,转换为项目中的 CSS 变量文件(tokens.css):
/* tokens.css — 从 ui_design_specification.md §1.1 提取 */
:root {
/* 墨色系列(文字) */
--ink: #1A1814;
--ink-mid: #4A4540;
--ink-soft: #7A7570;
--ink-dim: #B0AAA5;
/* 纸张系列(背景) */
--paper: #F8F6F2;
--paper2: #F0EDE8;
--paper3: #E8E4DE;
--white: #FFFFFF;
/* 状态色 */
--green: #4A8A5A;
--green-light: #EAF4EF;
--amber: #8A7040;
--amber-light: #FEF3C7;
--red: #9A5040;
--red-light: #FEF2F2;
--blue: #3D6B9A;
--blue-light: #EFF6FF;
/* 字体 */
--font-display: 'Fraunces', Georgia, serif;
--font-body: 'DM Sans', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
/* 动画缓动 */
--ease-spring: cubic-bezier(0.22, 1, 0.36, 1);
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}
始终使用 CSS 变量,禁止硬编码色值:
/* ✅ 正确 */
background-color: var(--green-light);
color: var(--ink);
font-family: var(--font-display);
/* ❌ 错误 */
background-color: #EAF4EF;
color: #1A1814;
font-family: 'Fraunces', Georgia, serif;
2.3 页面路由与跳转
完整路由表见 user_interface_structure.md §4。
// pages.json(UniApp 路由配置)
{
"pages": [
{ "path": "pages/auth/login/index" },
{ "path": "pages/auth/register/index" },
{ "path": "pages/home/index" },
{ "path": "pages/devices/index" },
{ "path": "pages/messages/index" },
{ "path": "pages/profile/index" }
],
"subPackages": [
{ "path": "pages-reminder", "pages": [...] },
{ "path": "pages-memory", "pages": [...] },
{ "path": "pages-privacy", "pages": [...] }
]
}
首页状态路由参数(六状态联动):
| 全局状态 | 路由参数 | 首页 Hero 卡片样式 |
|---|---|---|
| CALM | ?state=calm | --green-light 绿色背景 |
| ALERT | ?state=alert | --red-light 红色背景 + 脉冲动画 |
| HELP | ?state=help | --red 深红背景 + 紧急横幅 |
| REMINDER | ?state=reminder | --amber-light 琥珀色背景 |
| FAMILY | ?state=family | --blue-light 蓝色背景 |
| OFFLINE | ?state=offline | --paper3 灰色背景 |
2.4 Mock 数据规范(MSW)
Family App 前端开发阶段使用 MSW(Mock Service Worker) 拦截所有 API 请求,无需后端即可完成开发和验收。
2.4.1 Mock 初始化
// mocks/browser.ts
import { setupWorker } from 'msw'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)
// main.ts 中
if (import.meta.env.DEV) {
worker.start({
onUnhandledRequest: 'bypass',
quiet: true,
})
}
2.4.2 Mock Handlers(示例)
完整接口列表见 api_specification.yaml。
// mocks/handlers.ts
import { http, HttpResponse } from 'msw'
import type { FamilyStatus, Alert, Device } from '@/types'
// 响应数据结构(来自 api_specification.yaml)
type ApiResponse<T> = {
code: number
data: T
message: string
}
export const handlers = [
// GET /family/status — 首页六状态
http.get('/api/v1/family/status', (): ApiResponse<FamilyStatus> => ({
code: 0,
data: {
elderName: 'Mary',
elderAvatar: null,
state: 'CALM', // CALM | REMINDER | FAMILY | ALERT | HELP | OFFLINE
lastHeartbeat: '2026-04-22T08:30:00Z',
}
})),
// GET /alerts — 告警列表
http.get('/api/v1/alerts', (): ApiResponse<{ total: number; list: Alert[] }> => ({
code: 0,
data: {
total: 1,
list: [{
id: 'ALT_001',
elderId: 'ELDER_001',
level: 'red', // red | amber | normal
status: 'firing', // firing | acknowledged | resolved | false_alarm
firedAt: new Date().toISOString(),
evidence: { heartRate: 58, fallConfidence: 0.92 },
}]
}
})),
// GET /devices — 设备列表
http.get('/api/v1/devices', (): ApiResponse<Device[]> => ({
code: 0,
data: [
{
id: 'DEV_LUMA_001',
deviceType: 'luma',
name: "Mary's Luma",
serialNumber: 'LMA-XXXX-XXXX',
status: 'online', // online | offline
lastHeartbeat: new Date().toISOString(),
},
{
id: 'DEV_RADAR_001',
deviceType: 'radar',
name: 'Bedroom Radar',
serialNumber: 'RDR-XXXX-XXXX',
status: 'online',
lastHeartbeat: new Date().toISOString(),
}
]
})),
// POST /auth/login — 登录
http.post('/api/v1/auth/login', async ({ request }): ApiResponse<{ token: string; userId: string }> => {
const body = await request.json() as { phone: string; password: string }
if (body.phone === '13800000000' && body.password === 'password') {
return {
code: 0,
data: { token: 'mock-jwt-token-' + Date.now(), userId: 'USR_001' }
}
}
return { code: 401, data: null, message: 'Invalid credentials' }
}),
// POST /alerts/{id}/ack — 告警确认(中断升级链路)
http.post('/api/v1/alerts/:id/ack', ({ params }): ApiResponse<{ id: string }> => ({
code: 0,
data: { id: params.id }
})),
// POST /device/bind — 设备绑定
http.post('/api/v1/device/bind', async (): ApiResponse<Device> => ({
code: 0,
data: {
id: 'DEV_NEW_' + Date.now(),
deviceType: 'luma',
status: 'online',
createdAt: new Date().toISOString(),
}
})),
// GET /family/members — 家庭成员
http.get('/api/v1/family/members', (): ApiResponse<FamilyMember[]> => ({
code: 0,
data: [
{ id: 'USR_001', name: 'John', role: 'admin', priority: 1, phone: '13800000001' },
{ id: 'USR_002', name: 'Sarah', role: 'member', priority: 2, phone: '13800000002' },
]
})),
]