--- doc_id: DEV-202606-002 feature_id: FEAT-202606-001-unified-entry-client type: dev-progress title: Terminal Web Demo Implementation Plan status: implemented owner: 医梦研发团队 created_at: 2026-06-02 updated_at: 2026-06-02 reviewers: [] related_docs: - PRD-202606-001 - DDS-202606-003 - API-202606-001 related_modules: - emoon-terminal-client tags: - 统一入口客户端 - Web Demo - Vue --- # Terminal Web Demo 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:** Build a single Vue 3 Web demo for the unified entry terminal client that presents a dialogue-first AI hospital assistant and completes the outpatient registration card flow. **Architecture:** Create an independent frontend app in `emoon-terminal-client`, outside the backend Maven reactor. The app has a three-column desktop layout: left assistant context, center conversation input/stream, right context panel. It talks only to OpenPlatform terminal endpoints through an API client, supports local demo fixtures when backend is unavailable, and uses Playwright to verify desktop visual and interaction behavior. **Tech Stack:** Vue 3, TypeScript, Vite, Pinia, Vue Router only if needed, native `fetch` streaming parser for SSE, CSS variables for theme tokens, Vitest, Vue Test Utils, Playwright, generated/static TypeScript API types from backend contract. --- ## Source Documents Read these before implementation: - `docs/initiatives/FEAT-202606-001-unified-entry-client/design/终端Web演示设计.md` - `docs/initiatives/FEAT-202606-001-unified-entry-client/MVP接口契约.md` - `docs/initiatives/FEAT-202606-001-unified-entry-client/design/前端工程启动指南.md` - `docs/initiatives/FEAT-202606-001-unified-entry-client/前端接入指引.md` - `docs/initiatives/FEAT-202606-001-unified-entry-client/dev-progress/统一入口挂号演示实施计划.md` ## Target Repository Create or work in: ```text /Users/destiny/dev/emoon/emoon-terminal-client ``` This frontend repository is intentionally separate from: ```text /Users/destiny/dev/emoon/emoon-backend ``` Do not add frontend source files to the backend Maven reactor. ## Target File Structure ```text emoon-terminal-client ├── package.json ├── pnpm-lock.yaml ├── vite.config.ts ├── tsconfig.json ├── index.html ├── playwright.config.ts ├── src │ ├── App.vue │ ├── main.ts │ ├── assets │ │ └── doctor-assistant.png │ ├── api │ │ ├── client.ts │ │ ├── demoFixtures.ts │ │ ├── hmac.ts │ │ ├── sse.ts │ │ └── types.ts │ ├── cards │ │ ├── CardRenderer.vue │ │ ├── DepartmentSelectionCard.vue │ │ ├── DoctorSelectionCard.vue │ │ ├── TimeSlotSelectionCard.vue │ │ ├── ConfirmAppointmentCard.vue │ │ ├── PaymentQrCard.vue │ │ ├── AppointmentSuccessCard.vue │ │ └── ErrorCard.vue │ ├── chat │ │ ├── ConversationPanel.vue │ │ ├── MessageBubble.vue │ │ └── ChatInput.vue │ ├── layouts │ │ ├── AppShell.vue │ │ ├── AssistantContextPanel.vue │ │ ├── ContextPanel.vue │ │ ├── DoctorAssistantFigure.vue │ │ └── RegistrationProgress.vue │ ├── state │ │ └── terminalStore.ts │ ├── theme │ │ └── tokens.css │ └── demo │ └── demoMode.ts ├── tests │ ├── unit │ │ ├── cardRenderer.spec.ts │ │ ├── sse.spec.ts │ │ └── terminalStore.spec.ts │ └── e2e │ ├── registration-flow.spec.ts │ └── visual-layout.spec.ts └── README.md ``` ## Task 1: Scaffold The Vue Web Demo App **Files:** - Create: `/Users/destiny/dev/emoon/emoon-terminal-client/package.json` - Create: `/Users/destiny/dev/emoon/emoon-terminal-client/vite.config.ts` - Create: `/Users/destiny/dev/emoon/emoon-terminal-client/tsconfig.json` - Create: `/Users/destiny/dev/emoon/emoon-terminal-client/index.html` - Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/main.ts` - Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/App.vue` - [ ] **Step 1: Create package manifest** Use this dependency baseline: ```json { "name": "emoon-terminal-client", "version": "0.1.0", "private": true, "type": "module", "scripts": { "dev": "vite --host 127.0.0.1", "build": "vue-tsc --noEmit && vite build", "preview": "vite preview --host 127.0.0.1", "test:unit": "vitest run", "test:e2e": "playwright test", "test": "pnpm test:unit && pnpm test:e2e" }, "dependencies": { "@vitejs/plugin-vue": "^5.2.0", "pinia": "^2.3.1", "vue": "^3.5.13" }, "devDependencies": { "@playwright/test": "^1.49.1", "@types/node": "^22.10.2", "@vue/test-utils": "^2.4.6", "jsdom": "^25.0.1", "typescript": "^5.7.2", "vite": "^6.0.5", "vitest": "^2.1.8", "vue-tsc": "^2.2.0" } } ``` - [ ] **Step 2: Install dependencies** Run: ```bash cd /Users/destiny/dev/emoon/emoon-terminal-client pnpm install ``` Expected: `pnpm-lock.yaml` is created and install succeeds. - [ ] **Step 3: Create minimal Vue entry** `src/main.ts`: ```ts import { createPinia } from 'pinia'; import { createApp } from 'vue'; import App from './App.vue'; import './theme/tokens.css'; createApp(App).use(createPinia()).mount('#app'); ``` `src/App.vue`: ```vue ``` - [ ] **Step 4: Run build and verify failure until later layout files exist** Run: ```bash pnpm build ``` Expected: fails with missing `./layouts/AppShell.vue`. This confirms the app entry is wired to the planned layout component. **Commit:** `feat: scaffold terminal web demo` ## Task 2: Add Theme Tokens And Doctor Asset **Files:** - Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/theme/tokens.css` - Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/assets/doctor-assistant.png` - Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/layouts/DoctorAssistantFigure.vue` - Test: `/Users/destiny/dev/emoon/emoon-terminal-client/tests/unit/doctorAssistantFigure.spec.ts` - [ ] **Step 1: Copy generated doctor image** Copy, do not move or delete the original: ```bash cp /Users/destiny/.codex/generated_images/019e8719-cee0-7723-83a4-ac1a9e0a3bd4/ig_037570e350a4f508016a1e882c0c188191b10d3dbe9b5f776a.png /Users/destiny/dev/emoon/emoon-terminal-client/src/assets/doctor-assistant.png ``` Expected: `src/assets/doctor-assistant.png` exists. - [ ] **Step 2: Add CSS token file** `src/theme/tokens.css`: ```css :root { --brand-primary: #2b1f99; --brand-accent: #3ad4d8; --page-bg: #f7fbff; --surface: #ffffff; --soft-tint: #f4fdff; --border: #dfe7f7; --text-primary: #16133a; --text-secondary: #69708a; --warning-bg: #fff8e8; --warning-text: #735b14; --shadow-soft: 0 20px 48px rgba(43, 31, 153, 0.12); --radius-panel: 16px; --radius-card: 14px; font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; } * { box-sizing: border-box; } html, body, #app { width: 100%; min-width: 1280px; min-height: 100%; margin: 0; background: var(--page-bg); color: var(--text-primary); } button, input, textarea { font: inherit; } ``` - [ ] **Step 3: Write failing doctor component test** `tests/unit/doctorAssistantFigure.spec.ts`: ```ts import { mount } from '@vue/test-utils'; import { describe, expect, it } from 'vitest'; import DoctorAssistantFigure from '../../src/layouts/DoctorAssistantFigure.vue'; describe('DoctorAssistantFigure', () => { it('renders the static doctor assistant image and no voice controls', () => { const wrapper = mount(DoctorAssistantFigure); expect(wrapper.find('img[alt="医生助手"]').exists()).toBe(true); expect(wrapper.text()).toContain('医生助手'); expect(wrapper.text()).toContain('静态形象展示'); expect(wrapper.text()).not.toContain('可听'); expect(wrapper.text()).not.toContain('可说'); expect(wrapper.find('button').exists()).toBe(false); }); }); ``` - [ ] **Step 4: Run test and verify failure** Run: ```bash pnpm vitest run tests/unit/doctorAssistantFigure.spec.ts ``` Expected: fails because `DoctorAssistantFigure.vue` does not exist. - [ ] **Step 5: Implement static doctor figure** `src/layouts/DoctorAssistantFigure.vue`: ```vue