|
|
@@ -0,0 +1,1402 @@
|
|
|
+---
|
|
|
+doc_id: DEV-202607-001
|
|
|
+feature_id: FEAT-202606-001-unified-entry-client
|
|
|
+type: dev-progress
|
|
|
+title: 患者服务台移动端原型实施计划
|
|
|
+status: draft
|
|
|
+owner: 医梦研发团队
|
|
|
+created_at: 2026-07-02
|
|
|
+updated_at: 2026-07-02
|
|
|
+reviewers:
|
|
|
+ - 产品负责人
|
|
|
+ - 终端负责人
|
|
|
+related_docs:
|
|
|
+ - DES-202607-001
|
|
|
+ - ODS-202606-002
|
|
|
+ - ADR-202606-001
|
|
|
+related_modules:
|
|
|
+ - emoon-terminal-client
|
|
|
+tags:
|
|
|
+ - 患者服务台
|
|
|
+ - 移动端原型
|
|
|
+ - Open Design
|
|
|
+ - 事件驱动
|
|
|
+---
|
|
|
+
|
|
|
+# 患者服务台移动端原型 Implementation Plan
|
|
|
+
|
|
|
+> **For agentic workers:** REQUIRED SUB-SKILL: Use
|
|
|
+> superpowers:subagent-driven-development (recommended) or
|
|
|
+> superpowers:executing-plans to implement this plan task-by-task.
|
|
|
+> Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
+
|
|
|
+**Goal:** 在现有 Open Design 项目中建设一个可交互的患者手机端小程序/H5
|
|
|
+方案演示原型,完整演示报告事件、患者授权、AI 解读、自然语言挂号、
|
|
|
+Command 确认、到院签到和候诊更新。
|
|
|
+
|
|
|
+**Architecture:** 使用单页移动端应用承载全部演示状态,纯状态模型保存
|
|
|
+MedicalEvent、TaskState、ResultSnapshot 和 Command;视图只根据状态渲染,
|
|
|
+不使用 Card 类型推进流程。场景控制器向状态模型注入演示事件,使首页、
|
|
|
+AI 助手、就医旅程和消息中心同步变化。
|
|
|
+
|
|
|
+**Tech Stack:** HTML5、CSS3、原生 JavaScript ES Modules、Node.js
|
|
|
+内置 `node:test`、Lucide 图标、Open Design HTML Artifact。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. 实施范围
|
|
|
+
|
|
|
+### 1.1 源码与发布目录
|
|
|
+
|
|
|
+版本化源码目录:
|
|
|
+
|
|
|
+```text
|
|
|
+/Users/destiny/dev/emoon/emoon-backend/
|
|
|
+docs/prototypes/patient-service-desk-v3/
|
|
|
+```
|
|
|
+
|
|
|
+Open Design 发布目录:
|
|
|
+
|
|
|
+```text
|
|
|
+/Users/destiny/Library/Application Support/Open Design/namespaces/
|
|
|
+release-stable/data/projects/b7a08855-87bd-493c-92a4-31655a384570/
|
|
|
+```
|
|
|
+
|
|
|
+### 1.2 文件结构
|
|
|
+
|
|
|
+```text
|
|
|
+docs/prototypes/patient-service-desk-v3/
|
|
|
+├── assets/
|
|
|
+│ └── doctor.png
|
|
|
+├── patient-service-desk-v3.css
|
|
|
+├── patient-service-desk-v3.html
|
|
|
+├── patient-service-desk-v3.js
|
|
|
+├── patient-service-desk-v3.state.mjs
|
|
|
+└── patient-service-desk-v3.test.mjs
|
|
|
+
|
|
|
+Open Design 发布目录/
|
|
|
+├── patient-service-desk-v3.css
|
|
|
+├── patient-service-desk-v3.html
|
|
|
+├── patient-service-desk-v3.html.artifact.json
|
|
|
+├── patient-service-desk-v3.js
|
|
|
+├── patient-service-desk-v3.state.mjs
|
|
|
+└── index.html
|
|
|
+```
|
|
|
+
|
|
|
+文件职责:
|
|
|
+
|
|
|
+| 文件 | 职责 |
|
|
|
+|---|---|
|
|
|
+| `patient-service-desk-v3.html` | 移动端 App Shell、页面容器、对话框和演示控制器容器 |
|
|
|
+| `patient-service-desk-v3.css` | 品牌 Token、移动端布局、组件状态和动效 |
|
|
|
+| `patient-service-desk-v3.state.mjs` | 事件、TaskState、ResultSnapshot、Command 和场景转换 |
|
|
|
+| `patient-service-desk-v3.js` | DOM 渲染、用户交互、导航和无障碍行为 |
|
|
|
+| `patient-service-desk-v3.test.mjs` | 纯状态模型测试 |
|
|
|
+| `.artifact.json` | Open Design Artifact 元数据 |
|
|
|
+| `index.html` | 将 v3 原型加入现有原型索引 |
|
|
|
+
|
|
|
+不修改现有 v1/v2 HTML,以便对比演示和回退。
|
|
|
+
|
|
|
+除 Task 10 发布步骤外,所有命令从版本化源码目录执行。每个任务的
|
|
|
+Git checkpoint 提交 `emoon-backend` 中的原型源码,不提交 Open Design
|
|
|
+应用数据目录。
|
|
|
+
|
|
|
+## 2. 统一演示数据
|
|
|
+
|
|
|
+原型固定使用以下演示人物和业务数据:
|
|
|
+
|
|
|
+```js
|
|
|
+export const demoPatient = {
|
|
|
+ id: "patient-zhang-san",
|
|
|
+ name: "张三",
|
|
|
+ relation: "本人",
|
|
|
+ hospital: "空海医院",
|
|
|
+ campus: "本部院区"
|
|
|
+};
|
|
|
+
|
|
|
+export const demoReport = {
|
|
|
+ id: "report-blood-20260702",
|
|
|
+ type: "血常规",
|
|
|
+ issuedAt: "2026-07-02 10:18",
|
|
|
+ reviewStatus: "FINAL",
|
|
|
+ abnormalCount: 3,
|
|
|
+ totalCount: 18,
|
|
|
+ summary: "3 项指标轻度异常,未发现需要立即急诊处理的信号"
|
|
|
+};
|
|
|
+
|
|
|
+export const demoAppointment = {
|
|
|
+ department: "神经内科",
|
|
|
+ doctor: "李明",
|
|
|
+ title: "主任医师",
|
|
|
+ visitAt: "2026-07-03 09:30",
|
|
|
+ location: "门诊三楼 302",
|
|
|
+ fee: 25
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+## 3. 状态模型
|
|
|
+
|
|
|
+状态模型至少包含:
|
|
|
+
|
|
|
+```js
|
|
|
+export const initialState = {
|
|
|
+ activeTab: "home",
|
|
|
+ activePatientId: "patient-zhang-san",
|
|
|
+ events: [],
|
|
|
+ tasks: [],
|
|
|
+ resultSnapshots: {},
|
|
|
+ commands: {},
|
|
|
+ messages: [],
|
|
|
+ consents: {},
|
|
|
+ overlays: {
|
|
|
+ commandId: null,
|
|
|
+ consentId: null,
|
|
|
+ scenarioPanelOpen: false
|
|
|
+ }
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+任务结构:
|
|
|
+
|
|
|
+```js
|
|
|
+{
|
|
|
+ id: "task-report-001",
|
|
|
+ type: "REPORT_REVIEW",
|
|
|
+ status: "ACTION_REQUIRED",
|
|
|
+ stage: "REPORT_AVAILABLE",
|
|
|
+ priority: 90,
|
|
|
+ title: "血常规报告已出",
|
|
|
+ description: "18 项指标中有 3 项超出参考范围",
|
|
|
+ primaryAction: "INTERPRET_REPORT",
|
|
|
+ secondaryAction: "VIEW_REPORT",
|
|
|
+ expiresAt: null
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+命令结构:
|
|
|
+
|
|
|
+```js
|
|
|
+{
|
|
|
+ id: "command-register-001",
|
|
|
+ type: "REGISTER_APPOINTMENT",
|
|
|
+ riskLevel: "L2",
|
|
|
+ status: "PREPARED",
|
|
|
+ version: 1,
|
|
|
+ expiresAt: "2026-07-02T15:02:00+08:00",
|
|
|
+ summary: {
|
|
|
+ department: "神经内科",
|
|
|
+ doctor: "李明 主任医师",
|
|
|
+ visitAt: "7月3日 09:30",
|
|
|
+ fee: "¥25.00"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 4. 实施任务
|
|
|
+
|
|
|
+### Task 1: 创建状态模型和基础测试
|
|
|
+
|
|
|
+**Files:**
|
|
|
+
|
|
|
+- Create:
|
|
|
+ `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs`
|
|
|
+- Create:
|
|
|
+ `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs`
|
|
|
+
|
|
|
+- [ ] **Step 1: 写报告事件失败测试**
|
|
|
+
|
|
|
+```js
|
|
|
+import test from "node:test";
|
|
|
+import assert from "node:assert/strict";
|
|
|
+import {
|
|
|
+ createInitialState,
|
|
|
+ dispatchMedicalEvent
|
|
|
+} from "./patient-service-desk-v3.state.mjs";
|
|
|
+
|
|
|
+test("REPORT_ISSUED creates one actionable report task", () => {
|
|
|
+ const state = createInitialState();
|
|
|
+ const next = dispatchMedicalEvent(state, {
|
|
|
+ id: "event-report-001",
|
|
|
+ type: "REPORT_ISSUED",
|
|
|
+ occurredAt: "2026-07-02T10:18:00+08:00",
|
|
|
+ payload: { reportId: "report-blood-20260702" }
|
|
|
+ });
|
|
|
+
|
|
|
+ assert.equal(next.tasks.length, 1);
|
|
|
+ assert.equal(next.tasks[0].type, "REPORT_REVIEW");
|
|
|
+ assert.equal(next.tasks[0].status, "ACTION_REQUIRED");
|
|
|
+ assert.equal(next.tasks[0].primaryAction, "INTERPRET_REPORT");
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 运行测试并确认失败**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL,提示状态模块或导出函数不存在。
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现不可变事件转换**
|
|
|
+
|
|
|
+```js
|
|
|
+export function createInitialState() {
|
|
|
+ return {
|
|
|
+ activeTab: "home",
|
|
|
+ activePatientId: "patient-zhang-san",
|
|
|
+ events: [],
|
|
|
+ tasks: [],
|
|
|
+ resultSnapshots: {},
|
|
|
+ commands: {},
|
|
|
+ messages: [],
|
|
|
+ consents: {},
|
|
|
+ overlays: {
|
|
|
+ commandId: null,
|
|
|
+ consentId: null,
|
|
|
+ scenarioPanelOpen: false
|
|
|
+ }
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+export function dispatchMedicalEvent(state, event) {
|
|
|
+ if (state.events.some(item => item.id === event.id)) {
|
|
|
+ return state;
|
|
|
+ }
|
|
|
+
|
|
|
+ const next = {
|
|
|
+ ...state,
|
|
|
+ events: [...state.events, event],
|
|
|
+ tasks: [...state.tasks],
|
|
|
+ messages: [...state.messages]
|
|
|
+ };
|
|
|
+
|
|
|
+ if (event.type === "REPORT_ISSUED") {
|
|
|
+ next.tasks.push({
|
|
|
+ id: "task-report-001",
|
|
|
+ type: "REPORT_REVIEW",
|
|
|
+ status: "ACTION_REQUIRED",
|
|
|
+ stage: "REPORT_AVAILABLE",
|
|
|
+ priority: 90,
|
|
|
+ title: "血常规报告已出",
|
|
|
+ description: "18 项指标中有 3 项超出参考范围",
|
|
|
+ primaryAction: "INTERPRET_REPORT",
|
|
|
+ secondaryAction: "VIEW_REPORT",
|
|
|
+ expiresAt: null
|
|
|
+ });
|
|
|
+ next.messages.push({
|
|
|
+ id: "message-report-001",
|
|
|
+ type: "REPORT",
|
|
|
+ unread: true,
|
|
|
+ title: "血常规报告已出",
|
|
|
+ createdAt: event.occurredAt
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return next;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 增加事件幂等测试**
|
|
|
+
|
|
|
+```js
|
|
|
+test("duplicate event does not duplicate task or message", () => {
|
|
|
+ const event = {
|
|
|
+ id: "event-report-001",
|
|
|
+ type: "REPORT_ISSUED",
|
|
|
+ occurredAt: "2026-07-02T10:18:00+08:00",
|
|
|
+ payload: { reportId: "report-blood-20260702" }
|
|
|
+ };
|
|
|
+ const once = dispatchMedicalEvent(createInitialState(), event);
|
|
|
+ const twice = dispatchMedicalEvent(once, event);
|
|
|
+
|
|
|
+ assert.equal(twice.tasks.length, 1);
|
|
|
+ assert.equal(twice.messages.length, 1);
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: 运行测试并确认通过**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+Expected: 2 tests PASS。
|
|
|
+
|
|
|
+- [ ] **Step 6: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add patient-service-desk-v3.state.mjs patient-service-desk-v3.test.mjs
|
|
|
+git commit -m "feat: add patient desk event state model"
|
|
|
+```
|
|
|
+
|
|
|
+### Task 2: 建立移动端 App Shell 和设计 Token
|
|
|
+
|
|
|
+**Files:**
|
|
|
+
|
|
|
+- Create:
|
|
|
+ `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.html`
|
|
|
+- Create:
|
|
|
+ `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.css`
|
|
|
+- Create:
|
|
|
+ `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js`
|
|
|
+- Copy:
|
|
|
+ `/Users/destiny/Library/Application Support/Open Design/namespaces/release-stable/data/projects/b7a08855-87bd-493c-92a4-31655a384570/doctor.png`
|
|
|
+ to `docs/prototypes/patient-service-desk-v3/assets/doctor.png`
|
|
|
+
|
|
|
+- [ ] **Step 1: 创建语义化 HTML 骨架**
|
|
|
+
|
|
|
+```html
|
|
|
+<body>
|
|
|
+ <div class="prototype-stage">
|
|
|
+ <main class="phone-shell" aria-label="空海医院患者服务台">
|
|
|
+ <header id="appHeader"></header>
|
|
|
+ <section id="appView" aria-live="polite"></section>
|
|
|
+ <nav id="bottomNav" aria-label="主要导航"></nav>
|
|
|
+ </main>
|
|
|
+ <aside id="scenarioPanel" aria-label="演示场景控制"></aside>
|
|
|
+ </div>
|
|
|
+ <div id="overlayRoot"></div>
|
|
|
+ <script
|
|
|
+ src="https://unpkg.com/lucide@0.468.0/dist/umd/lucide.min.js">
|
|
|
+ </script>
|
|
|
+ <script type="module" src="./patient-service-desk-v3.js"></script>
|
|
|
+</body>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 建立移动端 Token**
|
|
|
+
|
|
|
+```css
|
|
|
+:root {
|
|
|
+ --bg: #f6f8fc;
|
|
|
+ --surface: #ffffff;
|
|
|
+ --surface-soft: #f0f5ff;
|
|
|
+ --fg: #1a1835;
|
|
|
+ --muted: #706d89;
|
|
|
+ --border: #e5e8f2;
|
|
|
+ --brand: #2b1f99;
|
|
|
+ --ai: #3ad4d8;
|
|
|
+ --success: #1aa981;
|
|
|
+ --warning: #e58a16;
|
|
|
+ --danger: #d74747;
|
|
|
+ --radius-sm: 10px;
|
|
|
+ --radius-md: 16px;
|
|
|
+ --radius-lg: 24px;
|
|
|
+ --shadow-card: 0 8px 24px rgb(43 31 153 / 8%);
|
|
|
+ --tap-size: 44px;
|
|
|
+}
|
|
|
+
|
|
|
+.phone-shell {
|
|
|
+ width: min(100%, 430px);
|
|
|
+ min-height: 844px;
|
|
|
+ background: var(--bg);
|
|
|
+ border-radius: 32px;
|
|
|
+ overflow: hidden;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+button,
|
|
|
+a {
|
|
|
+ min-height: var(--tap-size);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现五导航定义**
|
|
|
+
|
|
|
+```js
|
|
|
+export const navItems = [
|
|
|
+ { id: "home", label: "首页", icon: "house" },
|
|
|
+ { id: "care", label: "就医", icon: "stethoscope" },
|
|
|
+ { id: "assistant", label: "AI助手", icon: "sparkles" },
|
|
|
+ { id: "records", label: "健康档案", icon: "folder-heart" },
|
|
|
+ { id: "profile", label: "我的", icon: "user-round" }
|
|
|
+];
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 加载页面并检查基础结构**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+open patient-service-desk-v3.html
|
|
|
+```
|
|
|
+
|
|
|
+Expected:
|
|
|
+
|
|
|
+- 430px 手机容器完整显示;
|
|
|
+- 五个底部导航均可见;
|
|
|
+- 页面没有水平滚动;
|
|
|
+- Lucide 图标加载成功;
|
|
|
+- 浏览器控制台无 JavaScript 错误。
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit/checkpoint**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add patient-service-desk-v3.html patient-service-desk-v3.css patient-service-desk-v3.js
|
|
|
+git commit -m "feat: scaffold mobile patient service desk"
|
|
|
+```
|
|
|
+
|
|
|
+### Task 3: 实现首页和任务排序
|
|
|
+
|
|
|
+**Files:**
|
|
|
+
|
|
|
+- Modify: `patient-service-desk-v3.state.mjs`
|
|
|
+- Modify: `patient-service-desk-v3.js`
|
|
|
+- Modify: `patient-service-desk-v3.css`
|
|
|
+- Test: `patient-service-desk-v3.test.mjs`
|
|
|
+
|
|
|
+- [ ] **Step 1: 写任务排序失败测试**
|
|
|
+
|
|
|
+```js
|
|
|
+import { selectHomeTasks } from "./patient-service-desk-v3.state.mjs";
|
|
|
+
|
|
|
+test("home tasks are sorted by priority and completion", () => {
|
|
|
+ const state = createInitialState();
|
|
|
+ state.tasks = [
|
|
|
+ { id: "done", priority: 100, status: "COMPLETED" },
|
|
|
+ { id: "payment", priority: 80, status: "ACTION_REQUIRED" },
|
|
|
+ { id: "report", priority: 90, status: "ACTION_REQUIRED" }
|
|
|
+ ];
|
|
|
+
|
|
|
+ assert.deepEqual(
|
|
|
+ selectHomeTasks(state).map(item => item.id),
|
|
|
+ ["report", "payment", "done"]
|
|
|
+ );
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 运行测试并确认失败**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL,`selectHomeTasks` 未定义。
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现任务选择器**
|
|
|
+
|
|
|
+```js
|
|
|
+const statusRank = {
|
|
|
+ ACTION_REQUIRED: 0,
|
|
|
+ IN_PROGRESS: 1,
|
|
|
+ UPCOMING: 2,
|
|
|
+ COMPLETED: 3
|
|
|
+};
|
|
|
+
|
|
|
+export function selectHomeTasks(state) {
|
|
|
+ return [...state.tasks].sort((left, right) => {
|
|
|
+ const statusDelta =
|
|
|
+ statusRank[left.status] - statusRank[right.status];
|
|
|
+ return statusDelta || right.priority - left.priority;
|
|
|
+ });
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 实现首页组件**
|
|
|
+
|
|
|
+首页按顺序渲染:
|
|
|
+
|
|
|
+```js
|
|
|
+function renderHome(state) {
|
|
|
+ return `
|
|
|
+ ${renderHospitalHeader(state)}
|
|
|
+ ${renderGreeting(state)}
|
|
|
+ ${renderTaskSection(selectHomeTasks(state))}
|
|
|
+ ${renderTodayJourney(state)}
|
|
|
+ ${renderServiceGrid()}
|
|
|
+ ${renderCareSection(state)}
|
|
|
+ `;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+必须实现:
|
|
|
+
|
|
|
+- 医院和就诊人;
|
|
|
+- 未读消息角标;
|
|
|
+- AI 轻量输入入口;
|
|
|
+- “现在要做”任务卡;
|
|
|
+- 今日旅程;
|
|
|
+- 八个常用服务;
|
|
|
+- 主动关怀。
|
|
|
+
|
|
|
+- [ ] **Step 5: 运行测试并检查首页**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+Expected: 全部 PASS。
|
|
|
+
|
|
|
+Manual:
|
|
|
+
|
|
|
+- 空闲场景显示“今天没有待处理事项”;
|
|
|
+- 注入报告事件后报告任务排第一;
|
|
|
+- 已完成任务不抢占第一屏;
|
|
|
+- 第一屏只存在一个主 CTA。
|
|
|
+
|
|
|
+- [ ] **Step 6: Commit/checkpoint**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add patient-service-desk-v3.state.mjs patient-service-desk-v3.js \
|
|
|
+ patient-service-desk-v3.css patient-service-desk-v3.test.mjs
|
|
|
+git commit -m "feat: add task-driven patient desk home"
|
|
|
+```
|
|
|
+
|
|
|
+### Task 4: 实现报告授权、详情和 AI 解读
|
|
|
+
|
|
|
+**Files:**
|
|
|
+
|
|
|
+- Modify: `patient-service-desk-v3.state.mjs`
|
|
|
+- Modify: `patient-service-desk-v3.js`
|
|
|
+- Modify: `patient-service-desk-v3.css`
|
|
|
+- Test: `patient-service-desk-v3.test.mjs`
|
|
|
+
|
|
|
+- [ ] **Step 1: 写未授权不能解读的失败测试**
|
|
|
+
|
|
|
+```js
|
|
|
+import {
|
|
|
+ requestReportInterpretation,
|
|
|
+ grantConsent
|
|
|
+} from "./patient-service-desk-v3.state.mjs";
|
|
|
+
|
|
|
+test("report interpretation requires active consent", () => {
|
|
|
+ const state = dispatchMedicalEvent(createInitialState(), {
|
|
|
+ id: "event-report-001",
|
|
|
+ type: "REPORT_ISSUED",
|
|
|
+ occurredAt: "2026-07-02T10:18:00+08:00",
|
|
|
+ payload: { reportId: "report-blood-20260702" }
|
|
|
+ });
|
|
|
+
|
|
|
+ const blocked = requestReportInterpretation(
|
|
|
+ state,
|
|
|
+ "report-blood-20260702"
|
|
|
+ );
|
|
|
+ assert.equal(blocked.overlays.consentId, "consent-report-ai");
|
|
|
+ assert.equal(blocked.tasks[0].stage, "CONSENT_REQUIRED");
|
|
|
+
|
|
|
+ const allowed = requestReportInterpretation(
|
|
|
+ grantConsent(blocked, "consent-report-ai"),
|
|
|
+ "report-blood-20260702"
|
|
|
+ );
|
|
|
+ assert.equal(allowed.tasks[0].stage, "INTERPRETING");
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 运行测试并确认失败**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL,授权函数未定义。
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现授权和解读状态转换**
|
|
|
+
|
|
|
+授权对象固定包含:
|
|
|
+
|
|
|
+```js
|
|
|
+{
|
|
|
+ id: "consent-report-ai",
|
|
|
+ purpose: "用于本次报告的 AI 辅助解释",
|
|
|
+ scope: "当前血常规报告",
|
|
|
+ expiresAt: "2026-07-09T23:59:59+08:00",
|
|
|
+ revocable: true,
|
|
|
+ status: "GRANTED"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+任务转换:
|
|
|
+
|
|
|
+```text
|
|
|
+REPORT_AVAILABLE
|
|
|
+→ CONSENT_REQUIRED
|
|
|
+→ INTERPRETING
|
|
|
+→ INTERPRETED
|
|
|
+→ FOLLOWUP_RECOMMENDED
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 实现四个报告视图**
|
|
|
+
|
|
|
+1. 报告授权;
|
|
|
+2. 报告详情;
|
|
|
+3. AI 解读加载;
|
|
|
+4. AI 解读结果。
|
|
|
+
|
|
|
+AI 结果必须按以下结构展示:
|
|
|
+
|
|
|
+```js
|
|
|
+const interpretation = {
|
|
|
+ headline: "3 项指标轻度异常",
|
|
|
+ safety:
|
|
|
+ "未发现需要立即急诊处理的信号,请结合症状由医生判断",
|
|
|
+ highlights: [
|
|
|
+ { name: "白细胞计数", value: "11.2", flag: "偏高" },
|
|
|
+ { name: "中性粒细胞比例", value: "78%", flag: "偏高" },
|
|
|
+ { name: "淋巴细胞比例", value: "18%", flag: "偏低" }
|
|
|
+ ],
|
|
|
+ nextActions: [
|
|
|
+ "预约神经内科复诊",
|
|
|
+ "在线咨询医生",
|
|
|
+ "设置复查提醒"
|
|
|
+ ]
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: 验证授权和患者安全**
|
|
|
+
|
|
|
+Manual:
|
|
|
+
|
|
|
+- 未授权点击“AI 解读”只打开授权层;
|
|
|
+- 拒绝授权后仍能查看原始报告;
|
|
|
+- 解读页不出现“确诊”“一定是”等确定性表述;
|
|
|
+- 撤回授权后不能重新打开历史 AI 解读;
|
|
|
+- 返回首页后报告任务状态保持一致。
|
|
|
+
|
|
|
+- [ ] **Step 6: 运行测试并确认通过**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+Expected: 全部 PASS。
|
|
|
+
|
|
|
+- [ ] **Step 7: Commit/checkpoint**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add patient-service-desk-v3.*
|
|
|
+git commit -m "feat: add consented report interpretation flow"
|
|
|
+```
|
|
|
+
|
|
|
+### Task 5: 实现 AI 助手和自然语言挂号
|
|
|
+
|
|
|
+**Files:**
|
|
|
+
|
|
|
+- Modify: `patient-service-desk-v3.state.mjs`
|
|
|
+- Modify: `patient-service-desk-v3.js`
|
|
|
+- Modify: `patient-service-desk-v3.css`
|
|
|
+- Test: `patient-service-desk-v3.test.mjs`
|
|
|
+
|
|
|
+- [ ] **Step 1: 写完整条件不重复补问测试**
|
|
|
+
|
|
|
+```js
|
|
|
+import { submitInteraction } from "./patient-service-desk-v3.state.mjs";
|
|
|
+
|
|
|
+test("complete registration request produces slot candidates directly", () => {
|
|
|
+ const next = submitInteraction(
|
|
|
+ createInitialState(),
|
|
|
+ "我想挂明天神经内科李明主任的号"
|
|
|
+ );
|
|
|
+
|
|
|
+ const task = next.tasks.find(item => item.type === "REGISTRATION");
|
|
|
+ assert.equal(task.stage, "CANDIDATE_SELECTION");
|
|
|
+ assert.equal(task.missingFields.length, 0);
|
|
|
+ assert.ok(task.activeResultRef);
|
|
|
+ assert.equal(
|
|
|
+ next.resultSnapshots[task.activeResultRef].items.length,
|
|
|
+ 3
|
|
|
+ );
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 运行测试并确认失败**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL,interaction 未定义。
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现挂号演示解析器**
|
|
|
+
|
|
|
+原型只识别演示句:
|
|
|
+
|
|
|
+```text
|
|
|
+我想挂明天神经内科李明主任的号
|
|
|
+```
|
|
|
+
|
|
|
+生成 TaskState:
|
|
|
+
|
|
|
+```js
|
|
|
+{
|
|
|
+ type: "REGISTRATION",
|
|
|
+ stage: "CANDIDATE_SELECTION",
|
|
|
+ constraints: {
|
|
|
+ departmentName: "神经内科",
|
|
|
+ doctorName: "李明",
|
|
|
+ doctorTitle: "主任医师",
|
|
|
+ visitDate: "2026-07-03"
|
|
|
+ },
|
|
|
+ missingFields: [],
|
|
|
+ activeResultRef: "result-slot-001"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+生成三个号源候选,必须使用稳定 `candidateId`,不能只依赖序号。
|
|
|
+
|
|
|
+- [ ] **Step 4: 实现 AI 对话页面**
|
|
|
+
|
|
|
+包含:
|
|
|
+
|
|
|
+- 场景建议;
|
|
|
+- 消息列表;
|
|
|
+- 完整文本回答;
|
|
|
+- 号源候选 Presentation;
|
|
|
+- 固定底部输入框;
|
|
|
+- 返回当前任务;
|
|
|
+- 无结果替代推荐状态。
|
|
|
+
|
|
|
+- [ ] **Step 5: 实现点击和文字统一选择**
|
|
|
+
|
|
|
+以下操作必须得到相同结果:
|
|
|
+
|
|
|
+```text
|
|
|
+点击 candidate-slot-0930
|
|
|
+输入“选第一个”
|
|
|
+```
|
|
|
+
|
|
|
+两者都解析为:
|
|
|
+
|
|
|
+```js
|
|
|
+{
|
|
|
+ type: "SELECT_CANDIDATE",
|
|
|
+ resultRef: "result-slot-001",
|
|
|
+ resultVersion: 1,
|
|
|
+ candidateId: "candidate-slot-0930"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 6: 运行测试并手工检查**
|
|
|
+
|
|
|
+Expected:
|
|
|
+
|
|
|
+- 完整条件直接返回候选;
|
|
|
+- 不出现科室卡、医生卡、时间卡链;
|
|
|
+- Presentation 隐藏后文字仍完整;
|
|
|
+- 点击和“选第一个”选择同一 candidateId。
|
|
|
+
|
|
|
+- [ ] **Step 7: Commit/checkpoint**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add patient-service-desk-v3.*
|
|
|
+git commit -m "feat: add task-aware registration assistant"
|
|
|
+```
|
|
|
+
|
|
|
+### Task 6: 实现 Command 确认和异常状态
|
|
|
+
|
|
|
+**Files:**
|
|
|
+
|
|
|
+- Modify: `patient-service-desk-v3.state.mjs`
|
|
|
+- Modify: `patient-service-desk-v3.js`
|
|
|
+- Modify: `patient-service-desk-v3.css`
|
|
|
+- Test: `patient-service-desk-v3.test.mjs`
|
|
|
+
|
|
|
+- [ ] **Step 1: 写重复确认和 UNKNOWN 测试**
|
|
|
+
|
|
|
+```js
|
|
|
+import {
|
|
|
+ prepareRegistrationCommand,
|
|
|
+ confirmCommand,
|
|
|
+ markCommandUnknown
|
|
|
+} from "./patient-service-desk-v3.state.mjs";
|
|
|
+
|
|
|
+test("same command cannot execute twice", () => {
|
|
|
+ const prepared = prepareRegistrationCommand(createInitialState(), {
|
|
|
+ candidateId: "candidate-slot-0930"
|
|
|
+ });
|
|
|
+ const commandId = prepared.overlays.commandId;
|
|
|
+ const completed = confirmCommand(prepared, commandId);
|
|
|
+ const repeated = confirmCommand(completed, commandId);
|
|
|
+
|
|
|
+ assert.equal(repeated.commands[commandId].status, "COMPLETED");
|
|
|
+ assert.equal(repeated.events.filter(
|
|
|
+ item => item.type === "APPOINTMENT_CREATED"
|
|
|
+ ).length, 1);
|
|
|
+});
|
|
|
+
|
|
|
+test("unknown command blocks retry", () => {
|
|
|
+ const prepared = prepareRegistrationCommand(createInitialState(), {
|
|
|
+ candidateId: "candidate-slot-0930"
|
|
|
+ });
|
|
|
+ const commandId = prepared.overlays.commandId;
|
|
|
+ const unknown = markCommandUnknown(prepared, commandId);
|
|
|
+
|
|
|
+ assert.throws(
|
|
|
+ () => confirmCommand(unknown, commandId),
|
|
|
+ /结果确认中/
|
|
|
+ );
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 运行测试并确认失败**
|
|
|
+
|
|
|
+Expected: FAIL,Command 函数未定义。
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现 Command 状态机**
|
|
|
+
|
|
|
+允许的原型转换:
|
|
|
+
|
|
|
+```text
|
|
|
+PREPARED → EXECUTING → COMPLETED
|
|
|
+PREPARED → REJECTED
|
|
|
+PREPARED → EXPIRED
|
|
|
+EXECUTING → FAILED
|
|
|
+EXECUTING → UNKNOWN
|
|
|
+UNKNOWN → COMPLETED
|
|
|
+UNKNOWN → FAILED
|
|
|
+```
|
|
|
+
|
|
|
+任何其他转换抛出明确错误。
|
|
|
+
|
|
|
+- [ ] **Step 4: 实现统一确认层**
|
|
|
+
|
|
|
+确认层显示:
|
|
|
+
|
|
|
+- 操作名称;
|
|
|
+- 就诊人;
|
|
|
+- 科室、医生和时间;
|
|
|
+- 金额;
|
|
|
+- 有效倒计时;
|
|
|
+- 确认和暂不操作。
|
|
|
+
|
|
|
+实现以下视觉状态:
|
|
|
+
|
|
|
+- 待确认;
|
|
|
+- 执行中;
|
|
|
+- 成功;
|
|
|
+- 失败;
|
|
|
+- 已过期;
|
|
|
+- 结果确认中;
|
|
|
+- 已拒绝。
|
|
|
+
|
|
|
+- [ ] **Step 5: 验证 Command 安全表达**
|
|
|
+
|
|
|
+Manual:
|
|
|
+
|
|
|
+- 页面不能编辑金额和号源参数;
|
|
|
+- 重复点击确认只产生一次成功事件;
|
|
|
+- UNKNOWN 显示“请勿重复操作”;
|
|
|
+- 过期后只允许重新查询号源;
|
|
|
+- 成功后首页出现待到院任务。
|
|
|
+
|
|
|
+- [ ] **Step 6: 运行测试并确认通过**
|
|
|
+
|
|
|
+Expected: 全部 PASS。
|
|
|
+
|
|
|
+- [ ] **Step 7: Commit/checkpoint**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add patient-service-desk-v3.*
|
|
|
+git commit -m "feat: add governed command confirmation states"
|
|
|
+```
|
|
|
+
|
|
|
+### Task 7: 实现到院、签到和候诊事件
|
|
|
+
|
|
|
+**Files:**
|
|
|
+
|
|
|
+- Modify: `patient-service-desk-v3.state.mjs`
|
|
|
+- Modify: `patient-service-desk-v3.js`
|
|
|
+- Modify: `patient-service-desk-v3.css`
|
|
|
+- Test: `patient-service-desk-v3.test.mjs`
|
|
|
+
|
|
|
+- [ ] **Step 1: 写位置事件不自动签到测试**
|
|
|
+
|
|
|
+```js
|
|
|
+test("arrival event offers check-in but does not execute it", () => {
|
|
|
+ const next = dispatchMedicalEvent(createInitialState(), {
|
|
|
+ id: "event-arrival-001",
|
|
|
+ type: "CHECKIN_AVAILABLE",
|
|
|
+ occurredAt: "2026-07-03T09:10:00+08:00",
|
|
|
+ payload: { location: "门诊三楼神经内科诊区" }
|
|
|
+ });
|
|
|
+
|
|
|
+ const task = next.tasks.find(item => item.type === "CHECK_IN");
|
|
|
+ assert.equal(task.status, "ACTION_REQUIRED");
|
|
|
+ assert.equal(task.stage, "CONFIRMATION_REQUIRED");
|
|
|
+ assert.equal(
|
|
|
+ next.events.some(item => item.type === "CHECKIN_COMPLETED"),
|
|
|
+ false
|
|
|
+ );
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 运行测试并确认失败**
|
|
|
+
|
|
|
+Expected: FAIL,签到事件未处理。
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现事件转换**
|
|
|
+
|
|
|
+支持:
|
|
|
+
|
|
|
+```text
|
|
|
+APPOINTMENT_CREATED → 待到院
|
|
|
+CHECKIN_AVAILABLE → 待确认签到
|
|
|
+CHECKIN_COMPLETED → 候诊中
|
|
|
+QUEUE_UPDATED → 更新人数和预计时间
|
|
|
+VISIT_STARTED → 就诊中
|
|
|
+VISIT_COMPLETED → 诊后服务
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 实现今日旅程和候诊页**
|
|
|
+
|
|
|
+候诊页展示:
|
|
|
+
|
|
|
+- 当前排队号;
|
|
|
+- 前方人数;
|
|
|
+- 预计时间;
|
|
|
+- 当前诊室;
|
|
|
+- 导航;
|
|
|
+- 队列更新时间;
|
|
|
+- 异常延迟提示。
|
|
|
+
|
|
|
+- [ ] **Step 5: 验证签到表达**
|
|
|
+
|
|
|
+Manual:
|
|
|
+
|
|
|
+- 到达事件只出现“确认签到”;
|
|
|
+- 不使用“系统自动帮您完成签到”文案;
|
|
|
+- 签到成功后旅程节点和首页任务同步更新;
|
|
|
+- 队列事件只更新候诊,不改变挂号和签到事实。
|
|
|
+
|
|
|
+- [ ] **Step 6: 运行测试并确认通过**
|
|
|
+
|
|
|
+- [ ] **Step 7: Commit/checkpoint**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add patient-service-desk-v3.*
|
|
|
+git commit -m "feat: add event-driven check-in journey"
|
|
|
+```
|
|
|
+
|
|
|
+### Task 8: 补齐五导航和系统状态
|
|
|
+
|
|
|
+**Files:**
|
|
|
+
|
|
|
+- Modify: `patient-service-desk-v3.js`
|
|
|
+- Modify: `patient-service-desk-v3.css`
|
|
|
+
|
|
|
+- [ ] **Step 1: 实现就医总览**
|
|
|
+
|
|
|
+分组:
|
|
|
+
|
|
|
+```text
|
|
|
+待办理
|
|
|
+进行中
|
|
|
+已完成
|
|
|
+需要人工处理
|
|
|
+```
|
|
|
+
|
|
|
+每组至少展示一个真实演示任务。
|
|
|
+
|
|
|
+- [ ] **Step 2: 实现健康档案**
|
|
|
+
|
|
|
+包含:
|
|
|
+
|
|
|
+- 检验报告;
|
|
|
+- 检查报告;
|
|
|
+- 门诊记录;
|
|
|
+- 处方与用药;
|
|
|
+- 过敏史;
|
|
|
+- 授权记录。
|
|
|
+
|
|
|
+报告支持“全部、异常、未解读”三个筛选状态。
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现我的**
|
|
|
+
|
|
|
+包含:
|
|
|
+
|
|
|
+- 就诊人管理;
|
|
|
+- 授权中心;
|
|
|
+- 消息设置;
|
|
|
+- 常用院区;
|
|
|
+- 医保和支付;
|
|
|
+- 电子票据;
|
|
|
+- 人工客服;
|
|
|
+- 关于 AI。
|
|
|
+
|
|
|
+- [ ] **Step 4: 实现消息中心**
|
|
|
+
|
|
|
+消息按事件类型区分:
|
|
|
+
|
|
|
+- 报告;
|
|
|
+- 就医;
|
|
|
+- 缴费;
|
|
|
+- 随访;
|
|
|
+- 系统。
|
|
|
+
|
|
|
+点击消息必须定位到对应 TaskState,不直接执行命令。
|
|
|
+
|
|
|
+- [ ] **Step 5: 实现系统状态**
|
|
|
+
|
|
|
+覆盖:
|
|
|
+
|
|
|
+- 初始加载;
|
|
|
+- AI 解读加载;
|
|
|
+- 空状态;
|
|
|
+- 断网;
|
|
|
+- 服务降级;
|
|
|
+- 查询失败;
|
|
|
+- Command 失败;
|
|
|
+- Command UNKNOWN。
|
|
|
+
|
|
|
+- [ ] **Step 6: 手工回归**
|
|
|
+
|
|
|
+Expected:
|
|
|
+
|
|
|
+- 五个导航均可进入和返回;
|
|
|
+- 所有空状态有下一步;
|
|
|
+- 断网不清空本地任务展示;
|
|
|
+- 无患者可见 Mock、BLE、NFC、L0/L1 或 traceId;
|
|
|
+- 技术信息只在演示控制器中可见。
|
|
|
+
|
|
|
+- [ ] **Step 7: Commit/checkpoint**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add patient-service-desk-v3.js patient-service-desk-v3.css
|
|
|
+git commit -m "feat: complete patient desk navigation and states"
|
|
|
+```
|
|
|
+
|
|
|
+### Task 9: 建设方案演示控制器
|
|
|
+
|
|
|
+**Files:**
|
|
|
+
|
|
|
+- Modify: `patient-service-desk-v3.html`
|
|
|
+- Modify: `patient-service-desk-v3.js`
|
|
|
+- Modify: `patient-service-desk-v3.css`
|
|
|
+- Test: `patient-service-desk-v3.test.mjs`
|
|
|
+
|
|
|
+- [ ] **Step 1: 定义八个演示场景**
|
|
|
+
|
|
|
+```js
|
|
|
+export const demoScenarios = [
|
|
|
+ "EMPTY_HOME",
|
|
|
+ "REPORT_ARRIVED",
|
|
|
+ "REPORT_INTERPRETED",
|
|
|
+ "REGISTRATION_QUERY",
|
|
|
+ "COMMAND_PREPARED",
|
|
|
+ "APPOINTMENT_CREATED",
|
|
|
+ "CHECKIN_AVAILABLE",
|
|
|
+ "QUEUE_UPDATED"
|
|
|
+];
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 写场景重置测试**
|
|
|
+
|
|
|
+```js
|
|
|
+import { loadScenario } from "./patient-service-desk-v3.state.mjs";
|
|
|
+
|
|
|
+test("loading scenario resets previous state deterministically", () => {
|
|
|
+ const first = loadScenario(createInitialState(), "REPORT_ARRIVED");
|
|
|
+ const second = loadScenario(first, "EMPTY_HOME");
|
|
|
+
|
|
|
+ assert.equal(second.tasks.length, 0);
|
|
|
+ assert.equal(second.events.length, 0);
|
|
|
+ assert.equal(second.activeTab, "home");
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现场景控制器**
|
|
|
+
|
|
|
+控制器位于手机框外,仅用于方案演示:
|
|
|
+
|
|
|
+- 上一步;
|
|
|
+- 下一步;
|
|
|
+- 场景列表;
|
|
|
+- 重置;
|
|
|
+- 切换 390×844 / 430×932 视口;
|
|
|
+- 显示当前 Event、TaskState 和 Command 摘要;
|
|
|
+- 隐藏控制器进入纯净演示。
|
|
|
+
|
|
|
+- [ ] **Step 4: 实现键盘快捷键**
|
|
|
+
|
|
|
+```text
|
|
|
+← 上一个场景
|
|
|
+→ 下一个场景
|
|
|
+R 重置
|
|
|
+P 隐藏/显示控制器
|
|
|
+```
|
|
|
+
|
|
|
+输入框获得焦点时不得响应场景快捷键。
|
|
|
+
|
|
|
+- [ ] **Step 5: 运行测试并执行主故事线**
|
|
|
+
|
|
|
+主故事线:
|
|
|
+
|
|
|
+```text
|
|
|
+首页
|
|
|
+→ 报告到达
|
|
|
+→ 授权
|
|
|
+→ AI 解读
|
|
|
+→ 推荐复诊
|
|
|
+→ 自然语言查号
|
|
|
+→ Command 确认
|
|
|
+→ 挂号成功
|
|
|
+→ 到院签到
|
|
|
+→ 候诊更新
|
|
|
+```
|
|
|
+
|
|
|
+Expected: 每一步状态可重复、可重置、无残留。
|
|
|
+
|
|
|
+- [ ] **Step 6: Commit/checkpoint**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add patient-service-desk-v3.*
|
|
|
+git commit -m "feat: add patient desk presentation controls"
|
|
|
+```
|
|
|
+
|
|
|
+### Task 10: 接入 Open Design 索引和 Artifact
|
|
|
+
|
|
|
+**Files:**
|
|
|
+
|
|
|
+- Copy: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.html`
|
|
|
+ to Open Design 发布目录
|
|
|
+- Copy: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.css`
|
|
|
+ to Open Design 发布目录
|
|
|
+- Copy: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js`
|
|
|
+ to Open Design 发布目录
|
|
|
+- Copy: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs`
|
|
|
+ to Open Design 发布目录
|
|
|
+- Copy: `docs/prototypes/patient-service-desk-v3/assets/doctor.png`
|
|
|
+ to Open Design 发布目录
|
|
|
+- Create:
|
|
|
+ `/Users/destiny/Library/Application Support/Open Design/namespaces/release-stable/data/projects/b7a08855-87bd-493c-92a4-31655a384570/patient-service-desk-v3.html.artifact.json`
|
|
|
+- Modify:
|
|
|
+ `/Users/destiny/Library/Application Support/Open Design/namespaces/release-stable/data/projects/b7a08855-87bd-493c-92a4-31655a384570/index.html`
|
|
|
+
|
|
|
+- [ ] **Step 1: 创建 Artifact 元数据**
|
|
|
+
|
|
|
+先发布版本化源码:
|
|
|
+
|
|
|
+```bash
|
|
|
+OPEN_DESIGN="/Users/destiny/Library/Application Support/Open Design/namespaces/release-stable/data/projects/b7a08855-87bd-493c-92a4-31655a384570"
|
|
|
+cp patient-service-desk-v3.html "$OPEN_DESIGN/"
|
|
|
+cp patient-service-desk-v3.css "$OPEN_DESIGN/"
|
|
|
+cp patient-service-desk-v3.js "$OPEN_DESIGN/"
|
|
|
+cp patient-service-desk-v3.state.mjs "$OPEN_DESIGN/"
|
|
|
+cp assets/doctor.png "$OPEN_DESIGN/doctor.png"
|
|
|
+```
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "version": 1,
|
|
|
+ "kind": "html",
|
|
|
+ "title": "patient-service-desk-v3.html",
|
|
|
+ "entry": "patient-service-desk-v3.html",
|
|
|
+ "renderer": "html",
|
|
|
+ "status": "complete",
|
|
|
+ "exports": ["html", "pdf", "zip"],
|
|
|
+ "createdAt": "2026-07-02T00:00:00.000Z",
|
|
|
+ "updatedAt": "2026-07-02T00:00:00.000Z",
|
|
|
+ "metadata": {
|
|
|
+ "inferred": false,
|
|
|
+ "reconciled": true
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 更新索引**
|
|
|
+
|
|
|
+在核心页面最前方增加:
|
|
|
+
|
|
|
+```html
|
|
|
+<a href="patient-service-desk-v3.html"
|
|
|
+ class="screen-card patient-desk-card">
|
|
|
+ <div class="screen-card-num">V3 · Mobile</div>
|
|
|
+ <div class="screen-card-title">患者服务台</div>
|
|
|
+ <div class="screen-card-desc">
|
|
|
+ 事件驱动的手机端小程序/H5方案演示:报告解读、自然语言挂号、
|
|
|
+ Command确认、到院签到和候诊更新。
|
|
|
+ </div>
|
|
|
+ <span class="screen-card-link">打开</span>
|
|
|
+</a>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 验证索引入口**
|
|
|
+
|
|
|
+Expected:
|
|
|
+
|
|
|
+- Open Design 能识别新 Artifact;
|
|
|
+- 索引点击可进入 v3;
|
|
|
+- 返回索引可再次打开;
|
|
|
+- 原有 v1/v2 原型不受影响。
|
|
|
+
|
|
|
+- [ ] **Step 4: 记录发布校验**
|
|
|
+
|
|
|
+在实施记录中保存:
|
|
|
+
|
|
|
+```text
|
|
|
+Open Design 项目 ID:
|
|
|
+b7a08855-87bd-493c-92a4-31655a384570
|
|
|
+
|
|
|
+发布入口:
|
|
|
+patient-service-desk-v3.html
|
|
|
+
|
|
|
+索引入口:
|
|
|
+index.html
|
|
|
+```
|
|
|
+
|
|
|
+### Task 11: 视觉、交互和无障碍 QA
|
|
|
+
|
|
|
+**Files:**
|
|
|
+
|
|
|
+- Modify: `patient-service-desk-v3.html`
|
|
|
+- Modify: `patient-service-desk-v3.css`
|
|
|
+- Modify: `patient-service-desk-v3.js`
|
|
|
+
|
|
|
+- [ ] **Step 1: 运行状态测试**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+Expected: 全部 PASS。
|
|
|
+
|
|
|
+- [ ] **Step 2: 检查两个手机视口**
|
|
|
+
|
|
|
+检查:
|
|
|
+
|
|
|
+```text
|
|
|
+390 × 844
|
|
|
+430 × 932
|
|
|
+```
|
|
|
+
|
|
|
+Expected:
|
|
|
+
|
|
|
+- 无横向滚动;
|
|
|
+- 底部导航不遮挡内容;
|
|
|
+- 弹层不超出视口;
|
|
|
+- 主按钮在安全区内;
|
|
|
+- 文本不截断。
|
|
|
+
|
|
|
+- [ ] **Step 3: 检查交互**
|
|
|
+
|
|
|
+逐项验证:
|
|
|
+
|
|
|
+- 就诊人切换;
|
|
|
+- 五导航;
|
|
|
+- 首页任务主次操作;
|
|
|
+- 报告授权同意和拒绝;
|
|
|
+- 报告详情展开;
|
|
|
+- AI 输入;
|
|
|
+- 候选选择;
|
|
|
+- Command 确认和拒绝;
|
|
|
+- UNKNOWN 禁止重复;
|
|
|
+- 消息定位任务;
|
|
|
+- 场景控制器;
|
|
|
+- 断网恢复。
|
|
|
+
|
|
|
+- [ ] **Step 4: 检查视觉**
|
|
|
+
|
|
|
+- 医院品牌信息清晰;
|
|
|
+- 第一屏只有一个最强主操作;
|
|
|
+- 深蓝用于关键操作;
|
|
|
+- 青色用于 AI 和进行中;
|
|
|
+- 红色只用于高风险和失败;
|
|
|
+- 不使用 Emoji;
|
|
|
+- 使用 Lucide 图标;
|
|
|
+- 医生图片不拉伸、不遮挡任务;
|
|
|
+- 圆角、间距和阴影一致。
|
|
|
+
|
|
|
+- [ ] **Step 5: 检查无障碍**
|
|
|
+
|
|
|
+- 所有按钮有可理解名称;
|
|
|
+- 触控区域至少 44×44px;
|
|
|
+- 焦点状态可见;
|
|
|
+- 对话框打开后焦点进入弹层;
|
|
|
+- 对话框关闭后焦点返回触发按钮;
|
|
|
+- 状态不能只依赖颜色;
|
|
|
+- 动画遵守 `prefers-reduced-motion`。
|
|
|
+
|
|
|
+- [ ] **Step 6: 检查患者语言**
|
|
|
+
|
|
|
+仓库全文检索原型文件,必须不存在患者可见的:
|
|
|
+
|
|
|
+```text
|
|
|
+Mock
|
|
|
+traceId
|
|
|
+BLE
|
|
|
+NFC
|
|
|
+L0
|
|
|
+L1
|
|
|
+cardKey
|
|
|
+cardData
|
|
|
+nextCard
|
|
|
+```
|
|
|
+
|
|
|
+演示控制器的技术状态摘要可以包含 Event、TaskState 和 Command。
|
|
|
+
|
|
|
+- [ ] **Step 7: 最终回归**
|
|
|
+
|
|
|
+从空状态完整演示:
|
|
|
+
|
|
|
+```text
|
|
|
+报告到达
|
|
|
+→ 授权
|
|
|
+→ 解读
|
|
|
+→ 复诊建议
|
|
|
+→ 查询号源
|
|
|
+→ 确认挂号
|
|
|
+→ 到院
|
|
|
+→ 确认签到
|
|
|
+→ 候诊
|
|
|
+```
|
|
|
+
|
|
|
+Expected:
|
|
|
+
|
|
|
+- 无断链;
|
|
|
+- 无重复业务动作;
|
|
|
+- 所有状态可重置;
|
|
|
+- Card 隐藏后文本仍能说明结果;
|
|
|
+- 首页、就医、AI 和消息引用同一任务状态。
|
|
|
+
|
|
|
+- [ ] **Step 8: Final checkpoint**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add patient-service-desk-v3.*
|
|
|
+git commit -m "test: verify patient service desk prototype"
|
|
|
+```
|
|
|
+
|
|
|
+## 5. 验证命令汇总
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /Users/destiny/dev/emoon/emoon-backend/docs/prototypes/patient-service-desk-v3
|
|
|
+node --test patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+```bash
|
|
|
+rg -n \
|
|
|
+ "Mock|traceId|BLE|NFC|L0|L1|cardKey|cardData|nextCard" \
|
|
|
+ /Users/destiny/dev/emoon/emoon-backend/docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.*
|
|
|
+```
|
|
|
+
|
|
|
+```bash
|
|
|
+git -C /Users/destiny/dev/emoon/emoon-backend diff --check
|
|
|
+```
|
|
|
+
|
|
|
+## 6. 完成标准
|
|
|
+
|
|
|
+1. Open Design 索引能打开 v3 患者服务台。
|
|
|
+2. 390×844 和 430×932 两个视口均无布局破损。
|
|
|
+3. 八个演示场景可独立加载和重置。
|
|
|
+4. 报告主线、挂号主线和到院主线能够连续演示。
|
|
|
+5. TaskState 不依赖 Card 类型推进。
|
|
|
+6. 报告解读前必须授权。
|
|
|
+7. Command 状态覆盖确认、执行、成功、失败、过期和 UNKNOWN。
|
|
|
+8. 位置事件不能自动完成签到。
|
|
|
+9. Card 隐藏或渲染失败不阻塞文本和任务。
|
|
|
+10. Node 状态测试全部通过。
|
|
|
+11. 原有 v1/v2 原型保持不变。
|
|
|
+12. 设计文档根据最终原型更新为 `reviewing`。
|