--- doc_id: DDS-202606-003 feature_id: FEAT-202606-001-unified-entry-client type: detailed-design title: 统一入口客户端前端工程启动指南 status: approved owner: 医梦研发团队 created_at: 2026-06-17 updated_at: 2026-06-17 reviewers: [] related_docs: - PRD-202606-001 - DDS-202606-001 - API-202606-001 related_modules: - emoon-terminal-client tags: - 统一入口客户端 - 前端工程 - Vue --- # 统一入口客户端前端工程启动指南 > **目标仓库:** `emoon-terminal-client`(独立仓库,不在 AI 中台后端工程内) > **技术栈建议:** Vue 3 + TypeScript + Vite,复用现有 Demo 前端交互资产 > **后端契约基准:** `docs/initiatives/FEAT-202606-001-unified-entry-client/MVP接口契约.md` + `docs/api/openapi/医梦AI中台开放平台接口-v1.2.openapi.yaml` --- ## 1. 推荐工程结构 ```text emoon-terminal-client ├── apps │ ├── robot-client # 机器人端(Android WebView 壳) │ ├── kiosk-client # 自助机端 │ └── guide-screen-client # 导诊大屏端 │ ├── packages │ ├── api-client # 统一 OpenPlatform API SDK(自动从 openapi.yaml 生成) │ ├── medical-cards # 医疗卡片组件库(department-selection, doctor-selection 等) │ ├── terminal-bridge # JSBridge 抽象层(导航、扫码、读卡、打印) │ ├── terminal-theme # 主题系统(医梦主题 + 医院定制) │ └── terminal-core # 共享 Hooks / Stores / Utils │ ├── package.json ├── turbo.json # Turborepo 编排 └── README.md ``` ## 2. 三端差异速查 | 层次 | 共用 | 差异 | |------|------|------| | API SDK (`api-client`) | ✅ 统一调用 OpenPlatform | 无 | | 卡片组件 (`medical-cards`) | ✅ 科室卡、医生卡、号源卡、路线卡、舌诊卡 | 不同终端的 `allowedCards` 限制不同 | | Bridge (`terminal-bridge`) | ✅ 统一 JSBridge 抽象接口 | 机器人有导航;自助机有读卡/打印;导诊大屏能力最少 | | 动线区 (`journey-panel`) | ✅ 渲染后端 `journeyState` 和下一步服务 | 导诊屏偏公共导流,自助机偏业务办理,机器人偏导航 | | 感知状态 (`perception-status`) | ✅ 展示身份绑定、授权、扫码/NFC/刷卡状态 | 不同终端感知能力来自 `perceptionCapabilities` | | 主题 (`terminal-theme`) | ✅ 医梦主题 + 医院主题 | 屏幕尺寸、字体、按钮密度不同 | | 首页 | ✅ 聊天主控件可复用 | 三类终端首页布局不同 | ## 3. 首页启动流程(必实现) ```text loadConfig(hospitalId, accessKey) → POST /devices/register # 拿到 deviceId + activateStatus → setInterval 30s: POST /devices/{deviceId}/heartbeat → GET /devices/{deviceId}/scene # 拿到 homeTemplate + allowedAgents + allowedCards → initPerception(scene.perceptionCapabilities) → initJourneyPanel(scene.journeyPolicy) → renderHome(homeTemplate) # 根据 sceneCode 渲染首页 ``` **阻塞条件:** - `activateStatus = "pending"` → 展示"等待激活"状态,不进入核心业务 - `activateStatus = "rejected"` → 展示拒绝原因 + 联系运维入口 - `activateStatus = "activated"` → 正常进入 首页推荐布局: ```text 顶部:设备/院区/身份绑定/授权状态 左侧:公共快捷入口、人工服务、无障碍入口 中央:AI 对话、虚拟形象、业务卡片 右侧:当前动线阶段、下一步任务、排队/缴费/检查提醒 底部:通知、异常、扫码/NFC/人工降级入口 ``` `journeyState` 只能由后端返回后展示。前端可以上报扫码、NFC、刷卡、近场等 `perceptionContext` 或设备事件,但不能自行判断“已签到”“可缴费”“可挂号成功”。 ## 4. 卡片组件渲染 MVP(P0 必须支持) | cardKey | 渲染要求 | |---------|---------| | `department-selection` | 科室列表(名称 + 推荐原因)+ 选择按钮 | | `doctor-selection` | 医生列表(姓名 + 职称)+ 选择按钮 | | `time-slot-selection` | 号源列表(时间 + 剩余号数)+ 选择按钮 | | `confirm-appointment` | 挂号信息摘要 + 二次确认弹窗 | | `appointment-success` | 成功页(预约号 + Mock 标识) | | `route-card` | 路线文字 + "开始导航"按钮(机器人端调 JSBridge) | | `tongue-capture` | 拍照/上传引导 + 提交按钮 | **卡片通用交互模式:** ```typescript // 1. 渲染卡片(cardData 来自 GET /cards/{cardInstanceId}) renderCard(cardInstanceId, cardKey, cardData) // 2. 用户操作 → 生成幂等键 → 提交动作 const idempotencyKey = crypto.randomUUID() // 客户端生成,同一逻辑操作复用 const result = await apiClient.cardAction(cardInstanceId, actionName, { idempotencyKey, confirm: action.requiredConfirm ? userConfirmed : true, payload: { /* 用户选择的 departmentId / doctorId / slotId */ } }) // 3. 如有 nextCard → 继续渲染下一卡片 if (result.nextCard) { renderCard(result.nextCard.cardInstanceId, result.nextCard.cardKey, ...) } ``` ## 5. SSE 流式对话处理 ```typescript const eventSource = await apiClient.chatStream({ deviceContext: { deviceId, deviceType }, message: { type: "text", text: userInput, clientMessageId: crypto.randomUUID() } // 注意:不传 agentId,由后端 AgentRouter 自动路由 }) eventSource.on("message_delta", (data) => { appendText(data.text) // 逐字追加到对话气泡 }) eventSource.on("card_created", (data) => { fetchAndRenderCard(data.cardInstanceId) // 拉取卡片详情并渲染 }) eventSource.on("journey_updated", (data) => { renderJourneyPanel(data) // P0 必须处理;前端不得从 task/card 反推动线 }) eventSource.on("completed", (data) => { saveConversationId(data.conversationId) // 记录,用于断线恢复 }) eventSource.on("error", (data) => { showError(data.message) }) ``` ## 6. API SDK 生成建议 使用 `openapi-generator` 或 `openapi-typescript` 从 `docs/api/openapi/医梦AI中台开放平台接口-v1.2.openapi.yaml` 自动生成 TypeScript SDK: ```bash # 示例:openapi-typescript npx openapi-typescript \ https://raw.githubusercontent.com//docs/api/openapi/医梦AI中台开放平台接口-v1.2.openapi.yaml \ -o packages/api-client/src/generated/schema.ts ``` ## 7. 前端禁止事项 | 禁止 | 原因 | |------|------| | 请求中传 `agentId` | 后端 AgentRouter 决定路由 | | 直连 Dify API | 前端不决定流程编排 | | 直连 HIS / 舌诊底层接口 | 必须通过卡片动作闭环 | | 自行构造医疗建议文本 | 只展示后端返回的文本和卡片 | | 自行推导动线阶段并放开能力 | 动线阶段由后端结合 HIS/叫号/任务状态判断 | | 保存患者连续位置轨迹 | P0 只允许服务区域级事件,不保存移动路线 | | 隐藏 Mock 标识 | 后端返回 `"mock":true` 时必须标注"联调演示" | | 不传 `idempotencyKey` | 卡片动作必须带幂等键 | ## 8. 环境配置 ```typescript // packages/api-client/src/config.ts export const apiConfig = { baseUrl: import.meta.env.VITE_API_BASE_URL || "https://api.demo.emoon.local", accessKey: import.meta.env.VITE_ACCESS_KEY || "", } ``` **关键:`VITE_*` 前缀变量会被编译进前端 bundle,浏览器端可直接读取。签名密钥不得放入此类变量。** 签名应由受控终端壳(Android WebView bridge / Electron main process / 自助机原生层)持有并在每次请求前完成 HMAC 签名计算。前端 API SDK 仅接收已签名的请求头,或调用桥接层提供的 `signAndSend()` 方法。 三个 app 通过各自的 `.env` 文件区分: ```text # apps/kiosk-client/.env VITE_API_BASE_URL=https://api.demo.emoon.local VITE_ACCESS_KEY=kiosk_demo_key # 注意:不要在此文件放置签名密钥 ``` ## 9. 参考资产 | 资产 | 位置 | |------|------| | OpenAPI 完整契约 | `docs/api/openapi/医梦AI中台开放平台接口-v1.2.openapi.yaml` | | 终端接口契约(含 curl 示例) | `docs/initiatives/FEAT-202606-001-unified-entry-client/MVP接口契约.md` | | 对外接口设计文档 | `docs/initiatives/FEAT-202606-001-unified-entry-client/对外接口联调基准.md` | | 技术设计文档 | `docs/initiatives/FEAT-202606-001-unified-entry-client/详细设计.md` | | 现有 Demo 前端 | `emoon-ui/`(交互资产可复用,业务逻辑需重构) |