From 36fa15487c05ccef11e30d7f3674a816c8ae8435 Mon Sep 17 00:00:00 2001 From: yelan Date: Thu, 27 Aug 2026 11:27:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E5=8C=BB?= =?UTF-8?q?=E6=8A=A4=E7=AB=AF=E4=B8=8E=E6=82=A3=E8=80=85=E7=AB=AF=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E9=AA=A8=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 完成两个 Vite 应用的基础工程化配置、路由、状态管理及第一版业务页面,当前使用演示数据,尚未接入真实后端接口。 --- README.md | 26 +- clinical-web/README.md | 13 +- clinical-web/src/App.vue | 4 +- clinical-web/src/api/index.ts | 6 + clinical-web/src/layouts/ClinicalLayout.vue | 73 +++ clinical-web/src/main.ts | 11 +- clinical-web/src/router/index.ts | 52 ++ clinical-web/src/stores/app.ts | 9 + clinical-web/src/styles/index.css | 572 ++++++++++++++++++ clinical-web/src/types/consent.ts | 11 + .../views/consent-tasks/ConsentTasksView.vue | 139 +++++ .../src/views/dashboard/DashboardView.vue | 123 ++++ .../src/views/documents/DocumentsView.vue | 60 ++ .../src/views/reports/ReportsView.vue | 38 ++ .../src/views/settings/SettingsView.vue | 45 ++ patient-h5/README.md | 13 +- patient-h5/src/App.vue | 4 +- patient-h5/src/api/index.ts | 6 + patient-h5/src/components/SignatureCanvas.vue | 130 ++++ patient-h5/src/composables/useSignFlow.ts | 21 + patient-h5/src/main.ts | 8 +- patient-h5/src/router/index.ts | 20 + patient-h5/src/styles/index.css | 481 +++++++++++++++ patient-h5/src/types/signing.ts | 21 + patient-h5/src/views/consent/ConsentView.vue | 67 ++ patient-h5/src/views/entry/EntryView.vue | 56 ++ patient-h5/src/views/result/ResultView.vue | 39 ++ patient-h5/src/views/signer/SignerView.vue | 78 +++ 28 files changed, 2089 insertions(+), 37 deletions(-) create mode 100644 clinical-web/src/api/index.ts create mode 100644 clinical-web/src/layouts/ClinicalLayout.vue create mode 100644 clinical-web/src/router/index.ts create mode 100644 clinical-web/src/stores/app.ts create mode 100644 clinical-web/src/styles/index.css create mode 100644 clinical-web/src/types/consent.ts create mode 100644 clinical-web/src/views/consent-tasks/ConsentTasksView.vue create mode 100644 clinical-web/src/views/dashboard/DashboardView.vue create mode 100644 clinical-web/src/views/documents/DocumentsView.vue create mode 100644 clinical-web/src/views/reports/ReportsView.vue create mode 100644 clinical-web/src/views/settings/SettingsView.vue create mode 100644 patient-h5/src/api/index.ts create mode 100644 patient-h5/src/components/SignatureCanvas.vue create mode 100644 patient-h5/src/composables/useSignFlow.ts create mode 100644 patient-h5/src/router/index.ts create mode 100644 patient-h5/src/styles/index.css create mode 100644 patient-h5/src/types/signing.ts create mode 100644 patient-h5/src/views/consent/ConsentView.vue create mode 100644 patient-h5/src/views/entry/EntryView.vue create mode 100644 patient-h5/src/views/result/ResultView.vue create mode 100644 patient-h5/src/views/signer/SignerView.vue diff --git a/README.md b/README.md index c0e5467..e9ad11b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,12 @@ ## 当前状态 -当前仅完成项目目录和说明文档,尚未初始化 Vite,也尚未生成业务代码。 +已完成两个 Vite 应用的基础初始化、ESLint + Prettier 工程化配置,以及第一版业务页面骨架: + +- `clinical-web`:医护端布局、工作台、签署任务、文书库、报表和设置页面; +- `patient-h5`:签署入口、文书确认、签署人信息、手写签名和结果页面; +- 当前页面使用演示数据,尚未接入真实后端接口; +- `packages` 目录暂时保留,等接口和公共类型稳定后再接入共享包。 ## 项目定位 @@ -67,12 +72,11 @@ medical-sign/ ## 技术栈规划 -- 医护端:Vue 3 + TypeScript + Vite + Element Plus; -- 患者端:Vue 3 + TypeScript + Vite + Vant; -- 接口数据:优先使用 OpenAPI 生成类型安全的客户端; -- 服务端数据:TanStack Vue Query; -- 本地状态:Pinia 或组件状态; -- 测试:Vitest + Playwright。 +- 医护端:Vue 3 + TypeScript + Vite + Vue Router + Pinia + Element Plus + Axios; +- 患者端:Vue 3 + TypeScript + Vite + Vue Router + Axios,移动端先使用原生 Canvas 完成签名演示; +- 接口数据:先通过 Axios 封装请求,接口稳定后再评估 OpenAPI 类型生成; +- 服务端数据:医护端后续再评估 TanStack Vue Query; +- 测试:待最小业务闭环稳定后再补充 Vitest + Playwright。 ## 开发原则 @@ -98,8 +102,8 @@ patient-h5/dist/ → 患者手机签署网页 ## 后续工作 -1. 初始化两个 Vite 应用; -2. 配置 workspace 和公共类型包; -3. 完成“医护端发起 → 患者 H5 签署 → 结果回传”的最小闭环; +1. 用真实调研结果校准页面字段、状态和角色权限; +2. 完成“医护端发起 → 患者 H5 签署 → 结果回传”的最小闭环; +3. 再接入 `packages` 中的公共类型和接口客户端; 4. 验证电子病历 iframe、单点登录和签字板调用; -5. 再补充收费拦截、动态补签和急诊例外流程。 +5. 补充收费拦截、动态补签和急诊例外流程。 diff --git a/clinical-web/README.md b/clinical-web/README.md index 1dc7efe..9e0b7f7 100644 --- a/clinical-web/README.md +++ b/clinical-web/README.md @@ -60,7 +60,7 @@ - 急诊抢救紧急例外; - 签署成功但回传失败。 -## 技术栈规划(不一定全用到) +## 当前技术栈 ```text Vue 3 @@ -68,15 +68,12 @@ TypeScript Vite Vue Router Pinia -TanStack Vue Query Element Plus -Axios / OpenAPI Client -pdfjs-dist -ECharts -Vitest -Playwright +Axios ``` +TanStack Vue Query、PDF 预览、报表图表、自动化测试和签字板适配器暂不接入,等真实接口和业务流程稳定后再按需要增加。 + 签字板调用应通过独立的 `signature-adapter` 或本地桥接服务封装,业务页面不直接绑定具体厂商 SDK。 ## 非目标 @@ -87,4 +84,4 @@ Playwright ## 当前状态 -当前仅完成目录和说明文档,尚未初始化 Vite。 +已完成 Vite 基础初始化、路由、Pinia、医护端布局和第一版页面骨架。当前工作台、签署任务、文书库、报表和设置页面均使用演示数据,尚未接入真实后端。 diff --git a/clinical-web/src/App.vue b/clinical-web/src/App.vue index 388b05e..a78fb6f 100644 --- a/clinical-web/src/App.vue +++ b/clinical-web/src/App.vue @@ -1,7 +1,7 @@ diff --git a/clinical-web/src/api/index.ts b/clinical-web/src/api/index.ts new file mode 100644 index 0000000..629b64d --- /dev/null +++ b/clinical-web/src/api/index.ts @@ -0,0 +1,6 @@ +import axios from 'axios' + +export const apiClient = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || '/api', + timeout: 10_000, +}) diff --git a/clinical-web/src/layouts/ClinicalLayout.vue b/clinical-web/src/layouts/ClinicalLayout.vue new file mode 100644 index 0000000..1effb97 --- /dev/null +++ b/clinical-web/src/layouts/ClinicalLayout.vue @@ -0,0 +1,73 @@ + + + diff --git a/clinical-web/src/main.ts b/clinical-web/src/main.ts index 2425c0f..c1df6cc 100644 --- a/clinical-web/src/main.ts +++ b/clinical-web/src/main.ts @@ -1,5 +1,10 @@ +import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' import { createApp } from 'vue' -import './style.css' -import App from './App.vue' -createApp(App).mount('#app') +import App from './App.vue' +import router from './router' +import './styles/index.css' + +createApp(App).use(router).use(createPinia()).use(ElementPlus).mount('#app') diff --git a/clinical-web/src/router/index.ts b/clinical-web/src/router/index.ts new file mode 100644 index 0000000..e18c564 --- /dev/null +++ b/clinical-web/src/router/index.ts @@ -0,0 +1,52 @@ +import { createRouter, createWebHistory } from 'vue-router' + +import ClinicalLayout from '@/layouts/ClinicalLayout.vue' +import ConsentTasksView from '@/views/consent-tasks/ConsentTasksView.vue' +import DashboardView from '@/views/dashboard/DashboardView.vue' +import DocumentsView from '@/views/documents/DocumentsView.vue' +import ReportsView from '@/views/reports/ReportsView.vue' +import SettingsView from '@/views/settings/SettingsView.vue' + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + component: ClinicalLayout, + redirect: '/dashboard', + children: [ + { + path: 'dashboard', + component: DashboardView, + meta: { title: '工作台' }, + }, + { + path: 'consent-tasks', + component: ConsentTasksView, + meta: { title: '签署任务' }, + }, + { + path: 'documents', + component: DocumentsView, + meta: { title: '文书库' }, + }, + { + path: 'reports', + component: ReportsView, + meta: { title: '统计报表' }, + }, + { + path: 'settings', + component: SettingsView, + meta: { title: '系统设置' }, + }, + ], + }, + { + path: '/:pathMatch(.*)*', + redirect: '/dashboard', + }, + ], +}) + +export default router diff --git a/clinical-web/src/stores/app.ts b/clinical-web/src/stores/app.ts new file mode 100644 index 0000000..4e70ec9 --- /dev/null +++ b/clinical-web/src/stores/app.ts @@ -0,0 +1,9 @@ +import { defineStore } from 'pinia' + +export const useAppStore = defineStore('app', { + state: () => ({ + userName: '演示医生', + department: '儿科急诊', + environment: '演示环境', + }), +}) diff --git a/clinical-web/src/styles/index.css b/clinical-web/src/styles/index.css new file mode 100644 index 0000000..5c2f33e --- /dev/null +++ b/clinical-web/src/styles/index.css @@ -0,0 +1,572 @@ +:root { + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + color: #1d2939; + background: #f5f7fa; + font-synthesis: none; + text-rendering: optimizeLegibility; +} + +* { + box-sizing: border-box; +} + +body { + min-width: 1100px; + min-height: 100vh; + margin: 0; + background: #f5f7fa; +} + +button, +input { + font: inherit; +} + +button { + cursor: pointer; +} + +.clinical-layout { + min-height: 100vh; +} + +.clinical-sidebar { + position: relative; + display: flex; + flex-direction: column; + color: #d6e4ff; + background: #11243e; +} + +.brand { + display: flex; + gap: 12px; + align-items: center; + height: 88px; + padding: 0 24px; + border-bottom: 1px solid rgb(255 255 255 / 8%); +} + +.brand-mark { + display: grid; + width: 38px; + height: 38px; + color: #11243e; + font-size: 20px; + font-weight: 800; + place-items: center; + border-radius: 12px; + background: #93d8ca; +} + +.brand strong, +.brand span { + display: block; +} + +.brand strong { + color: #fff; + font-size: 18px; + letter-spacing: 2px; +} + +.brand span { + margin-top: 3px; + color: #8fa5c1; + font-size: 10px; + letter-spacing: 1.2px; + text-transform: uppercase; +} + +.clinical-menu { + flex: 1; + padding: 18px 12px; + border-right: 0; + background: transparent; +} + +.clinical-menu .el-menu-item { + height: 48px; + margin: 4px 0; + color: #a8bad2; + border-radius: 10px; +} + +.clinical-menu .el-menu-item:hover { + color: #fff; + background: rgb(255 255 255 / 8%); +} + +.clinical-menu .el-menu-item.is-active { + color: #fff; + background: #274765; +} + +.menu-dot { + width: 6px; + height: 6px; + margin-right: 12px; + border-radius: 50%; + background: currentcolor; +} + +.sidebar-footer { + display: flex; + gap: 8px; + align-items: center; + padding: 22px 24px; + color: #8fa5c1; + font-size: 12px; +} + +.status-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: #73d5a5; + box-shadow: 0 0 0 4px rgb(115 213 165 / 12%); +} + +.clinical-header { + display: flex; + align-items: center; + justify-content: space-between; + height: 88px; + padding: 0 34px; + background: #fff; + border-bottom: 1px solid #e8edf3; +} + +.header-kicker, +.eyebrow { + margin: 0 0 6px; + color: #7e91a8; + font-size: 11px; + font-weight: 700; + letter-spacing: 1.3px; + text-transform: uppercase; +} + +.clinical-header h1 { + margin: 0; + color: #172b4d; + font-size: 22px; +} + +.user-context { + display: flex; + gap: 10px; + align-items: center; +} + +.user-avatar { + display: grid; + width: 36px; + height: 36px; + color: #176b67; + font-weight: 700; + place-items: center; + border-radius: 50%; + background: #d9f1ec; +} + +.user-context strong, +.user-context span { + display: block; +} + +.user-context strong { + color: #263b59; + font-size: 13px; +} + +.user-context span { + margin-top: 2px; + color: #91a0b3; + font-size: 11px; +} + +.clinical-main { + padding: 32px 34px 48px; +} + +.page-section { + max-width: 1440px; + margin: 0 auto; +} + +.page-heading { + display: flex; + align-items: flex-end; + justify-content: space-between; + margin-bottom: 24px; +} + +.page-heading h2, +.welcome-panel h2 { + margin: 0; + color: #172b4d; + font-size: 28px; + line-height: 1.25; +} + +.page-heading p:not(.eyebrow) { + margin: 8px 0 0; + color: #8090a5; + font-size: 13px; +} + +.welcome-panel { + display: flex; + align-items: flex-end; + justify-content: space-between; + min-height: 190px; + margin-bottom: 20px; + padding: 30px 34px; + overflow: hidden; + color: #e8f3f5; + border-radius: 18px; + background: + radial-gradient(circle at 84% 18%, rgb(147 216 202 / 24%), transparent 28%), + linear-gradient(125deg, #173958, #195d69); +} + +.welcome-panel h2 { + max-width: 570px; + color: #fff; + font-size: 27px; +} + +.welcome-copy { + max-width: 600px; + margin: 14px 0 0; + color: #b9d1d9; + font-size: 14px; + line-height: 1.7; +} + +.primary-button, +.secondary-button { + min-height: 40px; + padding: 0 18px; + color: #123148; + font-size: 13px; + font-weight: 700; + border: 0; + border-radius: 9px; + background: #9bdfd0; +} + +.primary-button:hover { + background: #b3ebde; +} + +.secondary-button { + color: #176b67; + border: 1px solid #bfe2dc; + background: #eefaf7; +} + +.stats-row { + margin-bottom: 16px; +} + +.stat-card, +.content-card { + border: 1px solid #e9eef4; + border-radius: 14px; +} + +.stat-card { + min-height: 132px; +} + +.stat-card .el-card__body { + padding: 20px; +} + +.stat-topline { + display: flex; + align-items: center; + justify-content: space-between; + color: #7f91a8; + font-size: 12px; +} + +.stat-indicator { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.is-orange { + background: #f5b45e; +} + +.is-green { + background: #68c795; +} + +.is-red { + background: #eb8d8d; +} + +.is-blue { + background: #78aee5; +} + +.stat-card strong { + display: block; + margin: 12px 0 4px; + color: #193453; + font-size: 27px; + letter-spacing: -0.5px; +} + +.stat-card small { + color: #91a0b3; + font-size: 11px; +} + +.dashboard-grid { + margin-bottom: 16px; +} + +.content-card .el-card__header { + padding: 20px 22px; + border-bottom: 1px solid #edf1f5; +} + +.content-card .el-card__body { + padding: 0 22px; +} + +.card-heading { + display: flex; + align-items: center; + justify-content: space-between; +} + +.card-heading h3 { + margin: 0; + color: #203957; + font-size: 16px; +} + +.text-button { + color: #267d78; + font-size: 12px; + border: 0; + background: transparent; +} + +.task-item { + display: flex; + gap: 12px; + align-items: center; + min-height: 72px; + border-bottom: 1px solid #f0f3f6; +} + +.task-item:last-child { + border-bottom: 0; +} + +.task-avatar { + display: grid; + width: 34px; + height: 34px; + color: #456786; + font-size: 13px; + place-items: center; + border-radius: 10px; + background: #eaf1f6; +} + +.task-main { + flex: 1; + min-width: 0; +} + +.task-main strong, +.task-main span { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.task-main strong { + color: #304968; + font-size: 13px; +} + +.task-main span { + margin-top: 5px; + color: #91a0b3; + font-size: 11px; +} + +.reminder-list { + margin: 0; + padding: 2px 0 8px; + list-style: none; +} + +.reminder-list li { + display: flex; + gap: 12px; + padding: 16px 0; + border-bottom: 1px solid #f0f3f6; +} + +.reminder-list li:last-child { + border-bottom: 0; +} + +.reminder-number { + color: #62aa9f; + font-size: 12px; + font-weight: 700; +} + +.reminder-list strong, +.reminder-list span { + display: block; +} + +.reminder-list strong { + color: #304968; + font-size: 12px; +} + +.reminder-list div span { + margin-top: 5px; + color: #91a0b3; + font-size: 11px; + line-height: 1.5; +} + +.table-card .el-card__body { + padding: 22px; +} + +.filter-bar { + display: flex; + gap: 10px; + margin-bottom: 18px; +} + +.filter-bar .el-input { + width: 300px; +} + +.filter-bar .el-select { + width: 150px; +} + +.detail-grid { + display: grid; + grid-template-columns: 90px 1fr; + gap: 16px 12px; + align-items: center; + color: #8797aa; + font-size: 13px; +} + +.detail-grid strong { + color: #304968; + font-weight: 600; +} + +.report-card { + min-height: 142px; +} + +.report-card > .el-card__body > span { + color: #7f91a8; + font-size: 12px; +} + +.report-card > .el-card__body > strong { + margin-top: 15px; +} + +.report-placeholder { + min-height: 330px; +} + +.empty-chart { + display: grid; + min-height: 280px; + color: #8797aa; + text-align: center; + place-items: center; + align-content: center; +} + +.chart-icon { + display: grid; + width: 52px; + height: 52px; + margin-bottom: 12px; + color: #2f8d83; + font-size: 24px; + place-items: center; + border-radius: 16px; + background: #e8f7f3; +} + +.empty-chart h3 { + margin: 0; + color: #304968; + font-size: 15px; +} + +.empty-chart p { + margin: 8px 0 0; + font-size: 12px; +} + +.settings-card { + max-width: 780px; +} + +.settings-card .el-card__body { + padding: 8px 24px; +} + +.setting-row { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 86px; + border-bottom: 1px solid #f0f3f6; +} + +.setting-row:last-child { + border-bottom: 0; +} + +.setting-row strong, +.setting-row span { + display: block; +} + +.setting-row strong { + color: #304968; + font-size: 13px; +} + +.setting-row span { + margin-top: 6px; + color: #91a0b3; + font-size: 11px; +} diff --git a/clinical-web/src/types/consent.ts b/clinical-web/src/types/consent.ts new file mode 100644 index 0000000..152f1f4 --- /dev/null +++ b/clinical-web/src/types/consent.ts @@ -0,0 +1,11 @@ +export type ConsentTaskStatus = 'pending' | 'signed' | 'rejected' | 'expired' + +export interface ConsentTask { + id: string + patientName: string + visitNo: string + documentName: string + source: string + status: ConsentTaskStatus + updatedAt: string +} diff --git a/clinical-web/src/views/consent-tasks/ConsentTasksView.vue b/clinical-web/src/views/consent-tasks/ConsentTasksView.vue new file mode 100644 index 0000000..b785415 --- /dev/null +++ b/clinical-web/src/views/consent-tasks/ConsentTasksView.vue @@ -0,0 +1,139 @@ + + + diff --git a/clinical-web/src/views/dashboard/DashboardView.vue b/clinical-web/src/views/dashboard/DashboardView.vue new file mode 100644 index 0000000..6af8cdc --- /dev/null +++ b/clinical-web/src/views/dashboard/DashboardView.vue @@ -0,0 +1,123 @@ + + + diff --git a/clinical-web/src/views/documents/DocumentsView.vue b/clinical-web/src/views/documents/DocumentsView.vue new file mode 100644 index 0000000..3e11894 --- /dev/null +++ b/clinical-web/src/views/documents/DocumentsView.vue @@ -0,0 +1,60 @@ + + + diff --git a/clinical-web/src/views/reports/ReportsView.vue b/clinical-web/src/views/reports/ReportsView.vue new file mode 100644 index 0000000..c7a7453 --- /dev/null +++ b/clinical-web/src/views/reports/ReportsView.vue @@ -0,0 +1,38 @@ + + + diff --git a/clinical-web/src/views/settings/SettingsView.vue b/clinical-web/src/views/settings/SettingsView.vue new file mode 100644 index 0000000..dc308a4 --- /dev/null +++ b/clinical-web/src/views/settings/SettingsView.vue @@ -0,0 +1,45 @@ + + + diff --git a/patient-h5/README.md b/patient-h5/README.md index 9313cfb..60a7e73 100644 --- a/patient-h5/README.md +++ b/patient-h5/README.md @@ -40,21 +40,18 @@ patient-h5 只服务于一次或一组知情同意签署,不是完整的移动 - 所有最终状态以服务端结果为准; - 需要兼容微信、手机浏览器和常见 iOS/Android 浏览器。 -## 技术栈规划(不一定全用到) +## 当前技术栈 ```text Vue 3 TypeScript Vite Vue Router -Vant -Zod -pdfjs-dist -signature_pad -Vitest -Playwright +Axios ``` +Vant、Zod、PDF 预览、第三方签名组件和自动化测试暂不接入;当前签名区域使用原生 Canvas,先验证业务流程和交互。 + 患者端应采用移动优先设计,按钮、文字和签名区域需要适合老年患者使用。 ## 与医护端的关系 @@ -70,4 +67,4 @@ patient-h5 不直接访问医院完整电子病历。 ## 当前状态 -当前仅完成目录和说明文档,尚未初始化 Vite。 +已完成 Vite 基础初始化、路由、签署流程状态管理和第一版移动端页面骨架。当前流程使用演示数据,尚未接入真实 Token 校验、文书接口和签署提交接口。 diff --git a/patient-h5/src/App.vue b/patient-h5/src/App.vue index 388b05e..a78fb6f 100644 --- a/patient-h5/src/App.vue +++ b/patient-h5/src/App.vue @@ -1,7 +1,7 @@ diff --git a/patient-h5/src/api/index.ts b/patient-h5/src/api/index.ts new file mode 100644 index 0000000..629b64d --- /dev/null +++ b/patient-h5/src/api/index.ts @@ -0,0 +1,6 @@ +import axios from 'axios' + +export const apiClient = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || '/api', + timeout: 10_000, +}) diff --git a/patient-h5/src/components/SignatureCanvas.vue b/patient-h5/src/components/SignatureCanvas.vue new file mode 100644 index 0000000..e3adc2e --- /dev/null +++ b/patient-h5/src/components/SignatureCanvas.vue @@ -0,0 +1,130 @@ + + + diff --git a/patient-h5/src/composables/useSignFlow.ts b/patient-h5/src/composables/useSignFlow.ts new file mode 100644 index 0000000..d3e429a --- /dev/null +++ b/patient-h5/src/composables/useSignFlow.ts @@ -0,0 +1,21 @@ +import { reactive } from 'vue' + +import type { SignFlowState } from '@/types/signing' + +const initialState: SignFlowState = { + patientName: '张*', + visitNo: 'MZ20260827018', + documentTitle: '儿科急诊特殊检查知情同意书', + taskNo: 'CT-20260827-001', + consentAccepted: false, + signerName: '', + relation: '', + signatureCaptured: false, + submittedAt: '', +} + +export const signingState = reactive({ ...initialState }) + +export function resetSignFlow() { + Object.assign(signingState, initialState) +} diff --git a/patient-h5/src/main.ts b/patient-h5/src/main.ts index 2425c0f..3be5620 100644 --- a/patient-h5/src/main.ts +++ b/patient-h5/src/main.ts @@ -1,5 +1,7 @@ import { createApp } from 'vue' -import './style.css' -import App from './App.vue' -createApp(App).mount('#app') +import App from './App.vue' +import router from './router' +import './styles/index.css' + +createApp(App).use(router).mount('#app') diff --git a/patient-h5/src/router/index.ts b/patient-h5/src/router/index.ts new file mode 100644 index 0000000..ce95c08 --- /dev/null +++ b/patient-h5/src/router/index.ts @@ -0,0 +1,20 @@ +import { createRouter, createWebHistory } from 'vue-router' + +import ConsentView from '@/views/consent/ConsentView.vue' +import EntryView from '@/views/entry/EntryView.vue' +import ResultView from '@/views/result/ResultView.vue' +import SignerView from '@/views/signer/SignerView.vue' + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { path: '/', redirect: '/entry' }, + { path: '/entry', component: EntryView, meta: { title: '签署入口' } }, + { path: '/consent', component: ConsentView, meta: { title: '阅读文书' } }, + { path: '/signer', component: SignerView, meta: { title: '确认并签署' } }, + { path: '/result', component: ResultView, meta: { title: '签署结果' } }, + { path: '/:pathMatch(.*)*', redirect: '/entry' }, + ], +}) + +export default router diff --git a/patient-h5/src/styles/index.css b/patient-h5/src/styles/index.css new file mode 100644 index 0000000..03362c2 --- /dev/null +++ b/patient-h5/src/styles/index.css @@ -0,0 +1,481 @@ +:root { + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + color: #173b56; + background: #f3f8f8; + font-synthesis: none; + text-rendering: optimizeLegibility; +} + +* { + box-sizing: border-box; +} + +body { + min-width: 320px; + min-height: 100vh; + margin: 0; + background: radial-gradient(circle at 10% 0%, rgb(170 226 215 / 28%), transparent 30%), #f3f8f8; +} + +button, +input, +select { + font: inherit; +} + +button { + cursor: pointer; +} + +.mobile-shell { + width: min(100%, 560px); + min-height: 100vh; + margin: 0 auto; + padding: 30px 20px 42px; +} + +.mobile-brand { + display: flex; + gap: 10px; + align-items: center; + margin-bottom: 44px; +} + +.mobile-brand-mark, +.result-icon { + display: grid; + place-items: center; + color: #17425b; + font-weight: 800; + border-radius: 14px; + background: #9dded1; +} + +.mobile-brand-mark { + width: 38px; + height: 38px; + font-size: 20px; +} + +.mobile-brand strong, +.mobile-brand span { + display: block; +} + +.mobile-brand strong { + color: #173b56; + font-size: 17px; + letter-spacing: 1px; +} + +.mobile-brand span { + margin-top: 2px; + color: #8497a5; + font-size: 10px; +} + +.eyebrow { + margin: 0 0 8px; + color: #4d988d; + font-size: 11px; + font-weight: 800; + letter-spacing: 1.4px; + text-transform: uppercase; +} + +.mobile-hero, +.mobile-heading { + margin-bottom: 24px; +} + +.mobile-hero h1, +.mobile-heading h1, +.result-shell h1 { + margin: 0; + color: #173b56; + font-size: 28px; + line-height: 1.25; +} + +.mobile-hero p:not(.eyebrow), +.mobile-heading p:not(.eyebrow), +.result-copy { + margin: 12px 0 0; + color: #788f9d; + font-size: 14px; + line-height: 1.7; +} + +.mobile-card, +.document-card, +.signature-pad { + border: 1px solid #e0eceb; + border-radius: 18px; + background: rgb(255 255 255 / 86%); + box-shadow: 0 12px 30px rgb(33 79 92 / 5%); +} + +.patient-card { + padding: 20px; +} + +.card-label { + margin-bottom: 16px; + color: #8196a3; + font-size: 11px; + font-weight: 700; + letter-spacing: 1px; +} + +.patient-row { + display: flex; + gap: 12px; + align-items: center; +} + +.patient-avatar { + display: grid; + width: 46px; + height: 46px; + color: #1c736b; + font-size: 18px; + font-weight: 700; + place-items: center; + border-radius: 14px; + background: #dff3ee; +} + +.patient-row strong, +.patient-row span { + display: block; +} + +.patient-row strong { + color: #294d64; + font-size: 16px; +} + +.patient-row span { + margin-top: 5px; + color: #8aa0aa; + font-size: 12px; +} + +.task-number { + margin-top: 18px; + padding-top: 14px; + color: #8aa0aa; + font-size: 11px; + border-top: 1px solid #edf3f2; +} + +.notice-card { + display: flex; + gap: 12px; + align-items: flex-start; + margin-top: 14px; + padding: 16px; +} + +.notice-icon { + display: grid; + flex: 0 0 auto; + width: 24px; + height: 24px; + color: #2a8176; + font-size: 13px; + font-weight: 800; + place-items: center; + border-radius: 50%; + background: #dff3ee; +} + +.notice-card strong, +.notice-card p { + display: block; +} + +.notice-card strong { + color: #31576c; + font-size: 13px; +} + +.notice-card p { + margin: 6px 0 0; + color: #8298a3; + font-size: 12px; + line-height: 1.6; +} + +.mobile-primary-button { + width: 100%; + min-height: 50px; + margin-top: 24px; + padding: 0 20px; + color: #fff; + font-size: 15px; + font-weight: 700; + border: 0; + border-radius: 13px; + background: #277d78; + box-shadow: 0 10px 20px rgb(39 125 120 / 18%); +} + +.mobile-primary-button:hover { + background: #216c68; +} + +.mobile-text-button { + display: block; + margin: 14px auto 0; + padding: 6px 12px; + color: #4d988d; + font-size: 13px; + border: 0; + background: transparent; +} + +.secure-note { + margin: 18px 0 0; + color: #9aabb3; + font-size: 11px; + line-height: 1.6; + text-align: center; +} + +.mobile-stepper { + display: flex; + gap: 9px; + align-items: center; + margin-bottom: 30px; + color: #a4b4bb; + font-size: 11px; +} + +.mobile-stepper span.is-active { + color: #267d77; + font-weight: 700; +} + +.mobile-stepper i { + width: 18px; + height: 1px; + background: #d3e3e1; +} + +.document-card { + max-height: 48vh; + margin-bottom: 18px; + padding: 20px; + overflow: auto; +} + +.document-meta { + display: flex; + justify-content: space-between; + margin-bottom: 22px; + color: #90a4ad; + font-size: 11px; +} + +.document-card h2 { + margin: 20px 0 8px; + color: #31566c; + font-size: 15px; +} + +.document-card h2:first-of-type { + margin-top: 0; +} + +.document-card p { + margin: 0; + color: #718995; + font-size: 13px; + line-height: 1.8; +} + +.document-placeholder { + margin-top: 20px; + padding: 13px; + color: #7c9997; + font-size: 11px; + text-align: center; + border: 1px dashed #b8d8d1; + border-radius: 10px; + background: #f3fbf9; +} + +.check-row { + display: flex; + gap: 9px; + align-items: flex-start; + color: #5d7885; + font-size: 13px; + line-height: 1.6; +} + +.check-row input { + flex: 0 0 auto; + width: 18px; + height: 18px; + margin: 2px 0 0; + accent-color: #277d78; +} + +.form-error { + margin: 10px 0 0; + color: #c65d5d; + font-size: 12px; +} + +.signer-form { + margin-bottom: 18px; + padding: 20px; +} + +.field-label { + display: block; + margin-bottom: 8px; + color: #567281; + font-size: 12px; + font-weight: 700; +} + +.field-label + .field-label { + margin-top: 18px; +} + +.mobile-input { + width: 100%; + min-height: 46px; + padding: 0 13px; + color: #31566c; + font-size: 14px; + border: 1px solid #d8e8e6; + border-radius: 10px; + outline: 0; + background: #fbfefd; +} + +.mobile-input:focus { + border-color: #69b5aa; + box-shadow: 0 0 0 3px rgb(105 181 170 / 14%); +} + +.signature-pad { + padding: 16px; +} + +.signature-pad-heading { + display: flex; + justify-content: space-between; + margin-bottom: 10px; + color: #567281; + font-size: 12px; + font-weight: 700; +} + +.signature-hint { + color: #9aabb3; + font-size: 10px; + font-weight: 400; +} + +.signature-canvas { + display: block; + width: 100%; + height: 180px; + touch-action: none; + border: 1px dashed #b9d8d2; + border-radius: 10px; + background: + linear-gradient(#fff, #fff), + repeating-linear-gradient( + to bottom, + transparent 0, + transparent 43px, + rgb(185 216 210 / 28%) 44px, + transparent 45px + ); + background-blend-mode: multiply; +} + +.clear-button { + margin-top: 9px; + padding: 5px 0; + color: #4d988d; + font-size: 11px; + border: 0; + background: transparent; +} + +.result-shell { + display: flex; + flex-direction: column; + align-items: center; + padding-top: 100px; + text-align: center; +} + +.result-icon { + width: 68px; + height: 68px; + margin-bottom: 22px; + color: #1f776f; + font-size: 32px; + border-radius: 22px; +} + +.result-copy { + max-width: 370px; +} + +.result-card { + width: 100%; + margin-top: 28px; + padding: 6px 20px; + text-align: left; +} + +.result-card > div { + display: flex; + justify-content: space-between; + gap: 20px; + padding: 15px 0; + border-bottom: 1px solid #edf3f2; +} + +.result-card > div:last-child { + border-bottom: 0; +} + +.result-card span { + color: #91a4ad; + font-size: 12px; +} + +.result-card strong { + color: #31566c; + font-size: 12px; +} + +@media (max-width: 380px) { + .mobile-shell { + padding-right: 16px; + padding-left: 16px; + } + + .mobile-hero h1, + .mobile-heading h1, + .result-shell h1 { + font-size: 25px; + } +} diff --git a/patient-h5/src/types/signing.ts b/patient-h5/src/types/signing.ts new file mode 100644 index 0000000..1d7e865 --- /dev/null +++ b/patient-h5/src/types/signing.ts @@ -0,0 +1,21 @@ +export type SignerRelation = '本人' | '父亲' | '母亲' | '其他监护人' | '其他' + +export interface SignFlowState { + patientName: string + visitNo: string + documentTitle: string + taskNo: string + consentAccepted: boolean + signerName: string + relation: SignerRelation | '' + signatureCaptured: boolean + submittedAt: string +} + +export const relationOptions: Array<{ value: SignerRelation; label: string }> = [ + { value: '本人', label: '本人' }, + { value: '父亲', label: '父亲' }, + { value: '母亲', label: '母亲' }, + { value: '其他监护人', label: '其他监护人' }, + { value: '其他', label: '其他' }, +] diff --git a/patient-h5/src/views/consent/ConsentView.vue b/patient-h5/src/views/consent/ConsentView.vue new file mode 100644 index 0000000..66dca7b --- /dev/null +++ b/patient-h5/src/views/consent/ConsentView.vue @@ -0,0 +1,67 @@ + + + diff --git a/patient-h5/src/views/entry/EntryView.vue b/patient-h5/src/views/entry/EntryView.vue new file mode 100644 index 0000000..465aa36 --- /dev/null +++ b/patient-h5/src/views/entry/EntryView.vue @@ -0,0 +1,56 @@ + + + diff --git a/patient-h5/src/views/result/ResultView.vue b/patient-h5/src/views/result/ResultView.vue new file mode 100644 index 0000000..85eea9b --- /dev/null +++ b/patient-h5/src/views/result/ResultView.vue @@ -0,0 +1,39 @@ + + + diff --git a/patient-h5/src/views/signer/SignerView.vue b/patient-h5/src/views/signer/SignerView.vue new file mode 100644 index 0000000..9090dbc --- /dev/null +++ b/patient-h5/src/views/signer/SignerView.vue @@ -0,0 +1,78 @@ + + +