瀏覽代碼

docs: 制定患者服务台原型实施计划

WangKang 2 周之前
父節點
當前提交
aaf444eb8b

+ 1402 - 0
docs/initiatives/FEAT-202606-001-unified-entry-client/dev-progress/患者服务台移动端原型实施计划.md

@@ -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`。

+ 1 - 0
docs/initiatives/FEAT-202606-001-unified-entry-client/专题索引.md

@@ -34,6 +34,7 @@ tags:
 | 开发进度 | `DEV-202606-001` | [演示闭环纵向任务拆分](dev-progress/演示闭环纵向任务拆分.md) | implemented |
 | 开发进度 | `DEV-202606-002` | [Terminal Web Demo Implementation Plan](dev-progress/终端Web演示实施计划.md) | implemented |
 | 开发进度 | `DEV-202606-003` | [Unified Entry Registration Demo Implementation Plan](dev-progress/统一入口挂号演示实施计划.md) | implemented |
+| 开发进度 | `DEV-202607-001` | [患者服务台移动端原型实施计划](dev-progress/患者服务台移动端原型实施计划.md) | draft |
 | UI 设计 | `DES-202606-001` | [Terminal Web Demo Design](design/终端Web演示设计.md) | approved |
 | 移动端原型设计 | `DES-202607-001` | [患者服务台移动端原型设计](design/患者服务台移动端原型设计.md) | draft |