feat(演示): 持久化 Mock 数据并打通文档库发起签署闭环
This commit is contained in:
@@ -86,6 +86,8 @@ TanStack Vue Query、PDF 预览、报表图表、自动化测试和签字板适
|
|||||||
|
|
||||||
已完成 Vite 基础初始化、路由、Pinia、原型主题 CSS、登录页、首页、签署工作台第一版 Mock 和文档库管理页面。登录、首页统计、签署工作台的患者/就诊/模板/任务/审计、签署文件下载、短信重发、模板创建/版本工作流、用户组织查询与用户创建编辑,以及按模板维护文档权限已经接入 MEDISIGN 后端;系统设置仍保留 Mock,真实签字板/线上签名回调和打印服务仍需设备或后端能力。
|
已完成 Vite 基础初始化、路由、Pinia、原型主题 CSS、登录页、首页、签署工作台第一版 Mock 和文档库管理页面。登录、首页统计、签署工作台的患者/就诊/模板/任务/审计、签署文件下载、短信重发、模板创建/版本工作流、用户组织查询与用户创建编辑,以及按模板维护文档权限已经接入 MEDISIGN 后端;系统设置仍保留 Mock,真实签字板/线上签名回调和打印服务仍需设备或后端能力。
|
||||||
|
|
||||||
|
自测与演示:`VITE_USE_MOCK=true` 时,文档库和签署任务的演示数据会镜像到 localStorage(见 `utils/mock-storage.ts`),刷新页面后仍可继续,不必每次重建文书、重发签署;文档库页头的「重置演示数据」可清空这些本地数据回到初始状态。持久化只针对演示数据,不涉及登录态与真实患者信息。
|
||||||
|
|
||||||
## 当前路由结构
|
## 当前路由结构
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import { unwrapApiResponse } from '@/utils/api-response'
|
import { unwrapApiResponse } from '@/utils/api-response'
|
||||||
|
import { readLocalSigningTemplates } from '@/utils/mock-document-library'
|
||||||
|
import { MOCK_STORAGE_KEYS, readMockState, writeMockState } from '@/utils/mock-storage'
|
||||||
import { request } from '@/utils/request'
|
import { request } from '@/utils/request'
|
||||||
import { resendSigningSms as resendSigningSmsDelivery } from './deliveries'
|
import { resendSigningSms as resendSigningSmsDelivery } from './deliveries'
|
||||||
|
|
||||||
@@ -215,7 +217,7 @@ const mockTemplates: SigningTemplate[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const mockRecords: SigningTaskRecord[] = [
|
const defaultMockRecords: SigningTaskRecord[] = [
|
||||||
{
|
{
|
||||||
id: 'task-001',
|
id: 'task-001',
|
||||||
templateVersionId: 'tpl-001-v2',
|
templateVersionId: 'tpl-001-v2',
|
||||||
@@ -325,6 +327,17 @@ const mockRecords: SigningTaskRecord[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 演示任务记录:优先用本地保存的,刷新后仍保留(任务固化的文书 HTML、签名图都在里面)。
|
||||||
|
* 所有写操作都要调用 persistMockRecords 落盘,否则刷新会回到切换前的状态。
|
||||||
|
*/
|
||||||
|
const mockRecords: SigningTaskRecord[] =
|
||||||
|
readMockState<SigningTaskRecord[]>(MOCK_STORAGE_KEYS.signingTasks) ?? defaultMockRecords
|
||||||
|
|
||||||
|
function persistMockRecords() {
|
||||||
|
writeMockState(MOCK_STORAGE_KEYS.signingTasks, mockRecords)
|
||||||
|
}
|
||||||
|
|
||||||
function clonePatient(patient: PatientProfile): PatientProfile {
|
function clonePatient(patient: PatientProfile): PatientProfile {
|
||||||
return {
|
return {
|
||||||
...patient,
|
...patient,
|
||||||
@@ -371,6 +384,11 @@ function addHours(value: string, hours: number) {
|
|||||||
|
|
||||||
export const isSigningMockEnabled = useMockData
|
export const isSigningMockEnabled = useMockData
|
||||||
|
|
||||||
|
/** 演示患者清单(仅 Mock 模式):给签署弹窗做“点一下就能定位”的输入提示 */
|
||||||
|
export function getMockPatientHints(): { id: string; name: string }[] {
|
||||||
|
return useMockData ? mockPatients.map((item) => ({ id: item.id, name: item.name })) : []
|
||||||
|
}
|
||||||
|
|
||||||
function isNotFoundError(error: unknown) {
|
function isNotFoundError(error: unknown) {
|
||||||
return axios.isAxiosError(error) && error.response?.status === 404
|
return axios.isAxiosError(error) && error.response?.status === 404
|
||||||
}
|
}
|
||||||
@@ -852,6 +870,8 @@ export async function getPatientProfile(keyword: string): Promise<PatientProfile
|
|||||||
const patient = mockPatients.find(
|
const patient = mockPatients.find(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.id === normalizedKeyword ||
|
item.id === normalizedKeyword ||
|
||||||
|
// 演示数据里记不住患者 ID 是常态,允许直接按姓名定位
|
||||||
|
item.name === normalizedKeyword ||
|
||||||
item.visits.some((visit) => visit.visitNo === normalizedKeyword),
|
item.visits.some((visit) => visit.visitNo === normalizedKeyword),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -876,7 +896,12 @@ export async function getSigningTemplates(
|
|||||||
return records.map(mapAvailableTemplate)
|
return records.map(mapAvailableTemplate)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(mockTemplates.map((template) => ({ ...template })))
|
// 文档库本地新建的文书只存在 localStorage,必须一起返回;
|
||||||
|
// 否则文档库刚建好的文书在签署侧选不到(内置 Mock 模板是写死的演示数据)。
|
||||||
|
return Promise.resolve([
|
||||||
|
...readLocalSigningTemplates(),
|
||||||
|
...mockTemplates.map((template) => ({ ...template })),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function prepareSigningTask(
|
export async function prepareSigningTask(
|
||||||
@@ -950,6 +975,7 @@ export async function createSigningTask(input: CreateSigningTaskInput): Promise<
|
|||||||
}
|
}
|
||||||
|
|
||||||
mockRecords.unshift(task)
|
mockRecords.unshift(task)
|
||||||
|
persistMockRecords()
|
||||||
return Promise.resolve(cloneTask(task))
|
return Promise.resolve(cloneTask(task))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -969,6 +995,7 @@ export function updateSigningTaskMethod(
|
|||||||
|
|
||||||
task.method = method
|
task.method = method
|
||||||
task.updatedAt = formatMockDate()
|
task.updatedAt = formatMockDate()
|
||||||
|
persistMockRecords()
|
||||||
return Promise.resolve(cloneTask(task))
|
return Promise.resolve(cloneTask(task))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -990,6 +1017,7 @@ export function completeSigningTask(
|
|||||||
task.signatureDataUrl = signatureDataUrl
|
task.signatureDataUrl = signatureDataUrl
|
||||||
task.signedAt = formatMockDate()
|
task.signedAt = formatMockDate()
|
||||||
task.updatedAt = task.signedAt
|
task.updatedAt = task.signedAt
|
||||||
|
persistMockRecords()
|
||||||
return Promise.resolve(cloneTask(task))
|
return Promise.resolve(cloneTask(task))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1015,6 +1043,7 @@ export async function reopenSigningTask(
|
|||||||
task.signatureDataUrl = undefined
|
task.signatureDataUrl = undefined
|
||||||
task.signedAt = undefined
|
task.signedAt = undefined
|
||||||
task.updatedAt = formatMockDate()
|
task.updatedAt = formatMockDate()
|
||||||
|
persistMockRecords()
|
||||||
return Promise.resolve(cloneTask(task))
|
return Promise.resolve(cloneTask(task))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1041,6 +1070,7 @@ export async function voidSigningTask(
|
|||||||
task.voidReason = reason
|
task.voidReason = reason
|
||||||
task.signatureDataUrl = undefined
|
task.signatureDataUrl = undefined
|
||||||
task.updatedAt = formatMockDate()
|
task.updatedAt = formatMockDate()
|
||||||
|
persistMockRecords()
|
||||||
return Promise.resolve(cloneTask(task))
|
return Promise.resolve(cloneTask(task))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1063,6 +1093,7 @@ export async function resendSigningSms(
|
|||||||
task.status = 'pending'
|
task.status = 'pending'
|
||||||
task.expiresAt = addHours('2026-08-27 12:00', 48)
|
task.expiresAt = addHours('2026-08-27 12:00', 48)
|
||||||
task.updatedAt = formatMockDate()
|
task.updatedAt = formatMockDate()
|
||||||
|
persistMockRecords()
|
||||||
return Promise.resolve(cloneTask(task))
|
return Promise.resolve(cloneTask(task))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { computed, nextTick, ref, watch } from 'vue'
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
import { isSigningMockEnabled } from '@/api/workbench/signing'
|
import { getMockPatientHints, isSigningMockEnabled } from '@/api/workbench/signing'
|
||||||
import type { SigningTaskRecord, SigningTemplate } from '@/api/workbench/types'
|
import type { SigningTaskRecord, SigningTemplate } from '@/api/workbench/types'
|
||||||
import { useSigningTaskForm } from '@/composables/useSigningTaskForm'
|
import { useSigningTaskForm } from '@/composables/useSigningTaskForm'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
@@ -20,6 +20,14 @@ const emit = defineEmits<{
|
|||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const patientInput = ref<HTMLInputElement | null>(null)
|
const patientInput = ref<HTMLInputElement | null>(null)
|
||||||
const initialTemplate = computed(() => props.initialTemplate ?? null)
|
const initialTemplate = computed(() => props.initialTemplate ?? null)
|
||||||
|
// 演示模式下列出可用的 Mock 患者,点一下直接定位,省得记患者 ID(真实模式为空数组)
|
||||||
|
const patientHints = getMockPatientHints()
|
||||||
|
|
||||||
|
function usePatientHint(id: string) {
|
||||||
|
patientId.value = id
|
||||||
|
errors.patient = ''
|
||||||
|
void locatePatient()
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
documentSearch,
|
documentSearch,
|
||||||
@@ -135,6 +143,18 @@ watch(
|
|||||||
{{ locating ? '定位中…' : '定位患者' }}
|
{{ locating ? '定位中…' : '定位患者' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="patientHints.length" class="demo-patients">
|
||||||
|
<span class="demo-patients__label">演示患者</span>
|
||||||
|
<button
|
||||||
|
v-for="hint in patientHints"
|
||||||
|
:key="hint.id"
|
||||||
|
type="button"
|
||||||
|
class="demo-patients__chip"
|
||||||
|
@click="usePatientHint(hint.id)"
|
||||||
|
>
|
||||||
|
{{ hint.name }} · {{ hint.id }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div v-if="patient" class="patient-card">
|
<div v-if="patient" class="patient-card">
|
||||||
<span class="patient-avatar" aria-hidden="true">人</span>
|
<span class="patient-avatar" aria-hidden="true">人</span>
|
||||||
<strong>{{ patient.name }}</strong>
|
<strong>{{ patient.name }}</strong>
|
||||||
@@ -962,4 +982,29 @@ watch(
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.demo-patients {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-patients__label {
|
||||||
|
color: var(--mut);
|
||||||
|
font-size: 11.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-patients__chip {
|
||||||
|
padding: 3px 8px;
|
||||||
|
color: var(--brand);
|
||||||
|
font-size: 11.5px;
|
||||||
|
background: var(--brand-l);
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-patients__chip:hover {
|
||||||
|
filter: brightness(0.95);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* 本地演示文书库的读取与转换(仅 Mock 模式)。
|
||||||
|
*
|
||||||
|
* 管理端文档库的数据只存在前端:科室在 Word 编辑器里建好文书后写进 localStorage。
|
||||||
|
* 文档库页面和签署侧的模板列表都要读这份数据,共享同一份读取逻辑,
|
||||||
|
* 否则会出现「文档库刚建好文书,签署工作台却选不到」。
|
||||||
|
*
|
||||||
|
* 这里只声明实际用到的字段,不绑定文档库页面的类型,避免 api 层反向依赖 views。
|
||||||
|
*/
|
||||||
|
import type { SigningMethod, SigningTemplate } from '@/api/workbench/types'
|
||||||
|
|
||||||
|
import { MOCK_STORAGE_KEYS, readMockState } from './mock-storage'
|
||||||
|
|
||||||
|
interface LocalDocument {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
version: string
|
||||||
|
description?: string
|
||||||
|
versionId?: string
|
||||||
|
departmentId?: string | null
|
||||||
|
campusId?: string | null
|
||||||
|
contentHtml?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LocalCategory {
|
||||||
|
name: string
|
||||||
|
documents: LocalDocument[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LocalDepartment {
|
||||||
|
key: string
|
||||||
|
name: string
|
||||||
|
categories: LocalCategory[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把本地保存的文档库摊平成签署模板。
|
||||||
|
* 科室刚建好的文书因此能直接在签署工作台、文档库「发起」里选到。
|
||||||
|
*/
|
||||||
|
export function readLocalSigningTemplates(): SigningTemplate[] {
|
||||||
|
const departments = readMockState<LocalDepartment[]>(MOCK_STORAGE_KEYS.documentLibrary) ?? []
|
||||||
|
|
||||||
|
return departments.flatMap((department) =>
|
||||||
|
(department.categories ?? []).flatMap((category) =>
|
||||||
|
(category.documents ?? []).map((document) => ({
|
||||||
|
id: document.id,
|
||||||
|
versionId: document.versionId ?? document.id,
|
||||||
|
name: document.name,
|
||||||
|
code: document.code,
|
||||||
|
version: document.version,
|
||||||
|
department: department.name,
|
||||||
|
departmentId: document.departmentId ?? null,
|
||||||
|
campusId: document.campusId ?? undefined,
|
||||||
|
category: category.name,
|
||||||
|
description: document.description ?? '',
|
||||||
|
supportedMethods: ['pad', 'sms'] as SigningMethod[],
|
||||||
|
contentHtml: document.contentHtml,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* 演示数据的本地持久化(仅 Mock 模式使用)。
|
||||||
|
*
|
||||||
|
* 文档库与签署任务的演示数据原本只存在模块内存里,刷新页面就回到初始状态,
|
||||||
|
* 自测「文档库建文书 → 发起签署 → 签署 → 查看留痕」每次都要重来一遍。
|
||||||
|
* 这里把这份数据镜像到 localStorage,刷新后可以接着测。
|
||||||
|
*
|
||||||
|
* 只存演示数据,不涉及登录态、权限或真实患者信息;写入失败(容量超限、
|
||||||
|
* 浏览器禁用存储)时静默降级为纯内存数据,不打断正在进行的操作。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const STORAGE_PREFIX = 'medical-sign:mock:'
|
||||||
|
|
||||||
|
/** 演示数据键名:清空演示数据时按这里的清单统一清理 */
|
||||||
|
export const MOCK_STORAGE_KEYS = {
|
||||||
|
documentLibrary: `${STORAGE_PREFIX}document-library`,
|
||||||
|
signingTasks: `${STORAGE_PREFIX}signing-tasks`,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export function readMockState<T>(key: string): T | null {
|
||||||
|
try {
|
||||||
|
const raw = window.localStorage.getItem(key)
|
||||||
|
|
||||||
|
return raw ? (JSON.parse(raw) as T) : null
|
||||||
|
} catch {
|
||||||
|
// 存储被禁用,或存量数据与当前结构不兼容:按「没有本地数据」处理,页面照常初始化
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeMockState(key: string, value: unknown): void {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(value))
|
||||||
|
} catch {
|
||||||
|
// 容量超限或存储被禁用:退化为纯内存演示数据,不打扰使用者
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清空全部演示数据,调用方负责重新加载数据或刷新页面 */
|
||||||
|
export function clearMockState(): void {
|
||||||
|
try {
|
||||||
|
Object.values(MOCK_STORAGE_KEYS).forEach((key) => window.localStorage.removeItem(key))
|
||||||
|
} catch {
|
||||||
|
// 存储不可用时本来就无需清理
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ const emit = defineEmits<{
|
|||||||
'update:modelValue': [value: string]
|
'update:modelValue': [value: string]
|
||||||
add: []
|
add: []
|
||||||
export: []
|
export: []
|
||||||
|
reset: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function handleInput(event: Event) {
|
function handleInput(event: Event) {
|
||||||
@@ -41,6 +42,14 @@ function handleInput(event: Event) {
|
|||||||
<button type="button" class="tool-button secondary" @click="emit('export')">
|
<button type="button" class="tool-button secondary" @click="emit('export')">
|
||||||
导出模板包
|
导出模板包
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tool-button ghost"
|
||||||
|
title="清除本地保存的演示文书与签署任务"
|
||||||
|
@click="emit('reset')"
|
||||||
|
>
|
||||||
|
重置演示数据
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -135,6 +144,17 @@ function handleInput(event: Event) {
|
|||||||
background: var(--brand-l);
|
background: var(--brand-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool-button.ghost {
|
||||||
|
color: var(--mut);
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-button.ghost:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
border-color: var(--mut);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.library-tools {
|
.library-tools {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
import NewSigningTaskDialog from '@/components/signing/NewSigningTaskDialog.vue'
|
import NewSigningTaskDialog from '@/components/signing/NewSigningTaskDialog.vue'
|
||||||
import type { SigningTemplate } from '@/api/workbench/types'
|
import type { SigningTaskRecord, SigningTemplate } from '@/api/workbench/types'
|
||||||
import type { CampusRecord, DepartmentRecord } from '@/api/management/types'
|
import type { CampusRecord, DepartmentRecord } from '@/api/management/types'
|
||||||
|
import { clearMockState } from '@/utils/mock-storage'
|
||||||
|
|
||||||
import DocumentFilterTabs from './components/DocumentFilterTabs.vue'
|
import DocumentFilterTabs from './components/DocumentFilterTabs.vue'
|
||||||
import DocumentLibraryHeader from './components/DocumentLibraryHeader.vue'
|
import DocumentLibraryHeader from './components/DocumentLibraryHeader.vue'
|
||||||
import DocumentPreviewDialog from './components/DocumentPreviewDialog.vue'
|
import DocumentPreviewDialog from './components/DocumentPreviewDialog.vue'
|
||||||
import DocumentTemplateCard from './components/DocumentTemplateCard.vue'
|
import DocumentTemplateCard from './components/DocumentTemplateCard.vue'
|
||||||
import WordEditorDialog from './components/WordEditorDialog.vue'
|
import WordEditorDialog from './components/WordEditorDialog.vue'
|
||||||
import { documentLibrary, documentStatusOptions } from './mock'
|
import { documentStatusOptions, loadDocumentLibrary, saveDocumentLibrary } from './mock'
|
||||||
import type {
|
import type {
|
||||||
DepartmentTab,
|
DepartmentTab,
|
||||||
DocumentDepartment,
|
DocumentDepartment,
|
||||||
@@ -24,6 +26,8 @@ import type {
|
|||||||
// 原型阶段:文档库数据与文书内容全部来自本地真实附件(mock.ts + contents.ts),暂不调用后端接口。
|
// 原型阶段:文档库数据与文书内容全部来自本地真实附件(mock.ts + contents.ts),暂不调用后端接口。
|
||||||
// 后续接入时,仅需将 loadLibrary / saveTemplate / handleWorkflow 中的本地数据操作替换为文档模板接口。
|
// 后续接入时,仅需将 loadLibrary / saveTemplate / handleWorkflow 中的本地数据操作替换为文档模板接口。
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const library = ref<DocumentDepartment[]>([])
|
const library = ref<DocumentDepartment[]>([])
|
||||||
const selectedTemplate = ref<DocumentTemplate | null>(null)
|
const selectedTemplate = ref<DocumentTemplate | null>(null)
|
||||||
const signingTemplate = ref<SigningTemplate | null>(null)
|
const signingTemplate = ref<SigningTemplate | null>(null)
|
||||||
@@ -99,8 +103,8 @@ const filteredTemplates = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function cloneLibrary(): DocumentDepartment[] {
|
function cloneLibrary(source: DocumentDepartment[]): DocumentDepartment[] {
|
||||||
return documentLibrary.map((department) => ({
|
return source.map((department) => ({
|
||||||
...department,
|
...department,
|
||||||
categories: department.categories.map((category) => ({
|
categories: department.categories.map((category) => ({
|
||||||
...category,
|
...category,
|
||||||
@@ -110,9 +114,12 @@ function cloneLibrary(): DocumentDepartment[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadLibrary() {
|
function loadLibrary() {
|
||||||
library.value = cloneLibrary()
|
library.value = cloneLibrary(loadDocumentLibrary())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 演示数据落本地:刷新后文书库还在,不用每次重新建文书
|
||||||
|
watch(library, (value) => saveDocumentLibrary(value), { deep: true })
|
||||||
|
|
||||||
function handlePreview(template: DocumentTemplate) {
|
function handlePreview(template: DocumentTemplate) {
|
||||||
selectedTemplate.value = template
|
selectedTemplate.value = template
|
||||||
}
|
}
|
||||||
@@ -262,6 +269,31 @@ function closeSigningDialog() {
|
|||||||
signingTemplate.value = null
|
signingTemplate.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 闭环:文档库发起签署后跳到工作台并选中该任务,否则任务创建成功但用户看不到后续
|
||||||
|
function handleTaskCreated(task: SigningTaskRecord) {
|
||||||
|
closeSigningDialog()
|
||||||
|
void router.push({ name: 'workbench-signing', query: { taskId: task.id } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自测用:一键清掉本地保存的演示文书与签署任务,回到初始状态。
|
||||||
|
// 签署任务的演示数据在 api 层的内存里,清存储后直接刷新最彻底。
|
||||||
|
async function resetMockData() {
|
||||||
|
const confirmed = await ElMessageBox.confirm(
|
||||||
|
'将清除本地保存的演示文书与签署任务,恢复到初始状态。',
|
||||||
|
'重置演示数据',
|
||||||
|
{ type: 'warning', confirmButtonText: '重置', cancelButtonText: '取消' },
|
||||||
|
)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false)
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clearMockState()
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
function clearFilters() {
|
function clearFilters() {
|
||||||
filters.keyword = ''
|
filters.keyword = ''
|
||||||
filters.department = null
|
filters.department = null
|
||||||
@@ -281,6 +313,7 @@ onMounted(loadLibrary)
|
|||||||
:total="filteredTemplates.length"
|
:total="filteredTemplates.length"
|
||||||
@add="showAddMessage"
|
@add="showAddMessage"
|
||||||
@export="showExportMessage"
|
@export="showExportMessage"
|
||||||
|
@reset="resetMockData"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DocumentFilterTabs
|
<DocumentFilterTabs
|
||||||
@@ -332,6 +365,7 @@ onMounted(loadLibrary)
|
|||||||
:visible="signingDialogVisible"
|
:visible="signingDialogVisible"
|
||||||
:initial-template="signingTemplate"
|
:initial-template="signingTemplate"
|
||||||
@close="closeSigningDialog"
|
@close="closeSigningDialog"
|
||||||
|
@created="handleTaskCreated"
|
||||||
/>
|
/>
|
||||||
<WordEditorDialog
|
<WordEditorDialog
|
||||||
:visible="wordEditorVisible"
|
:visible="wordEditorVisible"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { MOCK_STORAGE_KEYS, readMockState, writeMockState } from '@/utils/mock-storage'
|
||||||
import type { SigningDocumentData } from '@/utils/signing-document'
|
import type { SigningDocumentData } from '@/utils/signing-document'
|
||||||
|
|
||||||
import type { DocumentDepartment, DocumentStatus } from './types'
|
import type { DocumentDepartment, DocumentStatus } from './types'
|
||||||
@@ -19,6 +20,16 @@ export { documentStatusLabels }
|
|||||||
// Word 自维护方案:文书库初始为空,所有文书由科室通过 Word 编辑器(粘贴 / 导入 .docx)创建维护。
|
// Word 自维护方案:文书库初始为空,所有文书由科室通过 Word 编辑器(粘贴 / 导入 .docx)创建维护。
|
||||||
export const documentLibrary: DocumentDepartment[] = []
|
export const documentLibrary: DocumentDepartment[] = []
|
||||||
|
|
||||||
|
/** 读取演示文书库:优先用本地保存的数据,没有则用初始空库(调用方负责再克隆一份使用) */
|
||||||
|
export function loadDocumentLibrary(): DocumentDepartment[] {
|
||||||
|
return readMockState<DocumentDepartment[]>(MOCK_STORAGE_KEYS.documentLibrary) ?? documentLibrary
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存演示文书库,刷新后仍可继续编辑、预览与发起签署 */
|
||||||
|
export function saveDocumentLibrary(library: DocumentDepartment[]): void {
|
||||||
|
writeMockState(MOCK_STORAGE_KEYS.documentLibrary, library)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 签署预览使用的示例患者快照(脱敏演示数据)。
|
* 签署预览使用的示例患者快照(脱敏演示数据)。
|
||||||
* 创建文书时还没有真实签署任务,用固定示例值让科室看到占位符被填充后的版式;
|
* 创建文书时还没有真实签署任务,用固定示例值让科室看到占位符被填充后的版式;
|
||||||
|
|||||||
Reference in New Issue
Block a user