feat(font): 新增字体设置并统一医护端与患者端文书字体

This commit is contained in:
xy
2026-09-30 08:35:02 +08:00
parent fe8cb01d4d
commit c2984bfd98
18 changed files with 511 additions and 5 deletions
+6
View File
@@ -117,3 +117,9 @@ patient-h5/dist/ → 患者手机签署网页
3. 再接入 `packages` 中的公共类型和接口客户端;
4. 验证电子病历 iframe、单点登录和签字板调用;
5. 补充收费拦截、动态补签和急诊例外流程。
## 文书字体
系统设置 → 参数设置 → 文书字体,已接入真实字体接口。支持 TTF/TTC 字面选择、常规体/粗体上传、检测、浏览器/PDF 预览和设置默认字体。使用系统设置查看权限和参数编辑权限。
医护端任务与患者 H5 使用任务固化的字体描述,通过 `packages/document-fonts` 公共加载器加载同一 TTF。模板预览读取当前默认字体;字体加载失败时阻止展示可签署正文。后端需先升级至包含 V33 字体迁移的版本,完整部署及接口说明见后端 `docs/document-fonts.md`。
@@ -0,0 +1,63 @@
import { request } from '@/utils/request'
import { unwrapApiResponse, unwrapNullableApiResponse, isSuccessCode } from '@/utils/api-response'
import type { ApiResponse } from '@/types/common'
import type { DocumentFont } from '../../../../packages/document-fonts'
export type { DocumentFont } from '../../../../packages/document-fonts'
export interface FontRecord extends DocumentFont {
enabled: boolean
createdAt: string
}
export interface FontNodeCheck {
fontId: string
node: string
available: boolean
message: string
checkedAt: string
}
export interface FontSettings {
nodes: FontNodeCheck[]
defaultFontId: string | null
fonts: FontRecord[]
legacyFileReadable: boolean
}
export interface FontFaceInfo {
index: number
name: string
style: string
}
export interface FontCheck {
available: boolean
message: string
node: string
checkedAt: string
}
const base = '/v1/document-fonts'
const longRequest = { timeout: 90000 }
export const getDocumentFontSettings = () =>
request.get<ApiResponse<FontSettings>>(base).then(unwrapApiResponse)
export const getDefaultDocumentFont = () =>
request.get<ApiResponse<DocumentFont | null>>(`${base}/current`).then(unwrapNullableApiResponse)
export const inspectDocumentFont = (file: File) => {
const form = new FormData()
form.append('file', file)
return request
.post<ApiResponse<FontFaceInfo[]>>(`${base}/inspect`, form, longRequest)
.then(unwrapApiResponse)
}
export const uploadDocumentFont = (form: FormData) =>
request.post<ApiResponse<FontRecord>>(base, form, longRequest).then(unwrapApiResponse)
export const checkDocumentFont = (id: string) =>
request
.post<ApiResponse<FontCheck>>(`${base}/${id}/check`, undefined, longRequest)
.then(unwrapApiResponse)
export const previewDocumentFont = (id: string) =>
request.get<Blob>(`${base}/${id}/preview`, { ...longRequest, responseType: 'blob' })
function requireSuccess(response: ApiResponse<unknown>) {
if (!isSuccessCode(response.code)) throw new Error(response.message || '设置失败')
}
export const activateDocumentFont = (id: string) =>
request
.put<ApiResponse<unknown>>(`${base}/${id}/default`, undefined, longRequest)
.then(requireSuccess)
export const setDocumentFontEnabled = (id: string, enabled: boolean) =>
request.put<ApiResponse<unknown>>(`${base}/${id}/status`, { enabled }).then(requireSuccess)
@@ -895,6 +895,7 @@ function mapSigningTask(
createdAt: formatApiDateTime(dto.createdAt),
updatedAt: formatApiDateTime(dto.updatedAt),
rowVersion: dto.rowVersion,
documentFont: dto.documentFont ?? null,
backendStatus: dto.status,
signedAt: formatApiDateTime(dto.signedAt) || undefined,
voidReason: dto.voidReason ?? undefined,
+3
View File
@@ -1,3 +1,4 @@
import type { DocumentFont } from '../../../../packages/document-fonts'
import type { ApiResponse, PageQuery, PageResult } from '@/types/common'
export type WorkbenchCampus = string
@@ -274,6 +275,7 @@ export type SigningFieldValue = string | boolean | string[]
export type SigningAnswers = Record<string, SigningFieldValue>
export interface SigningTaskRecord extends WorkbenchTask {
documentFont?: DocumentFont | null
templateVersionId: string
campus: WorkbenchCampus
campusId?: string
@@ -532,6 +534,7 @@ export interface SignTaskResponseDto {
visitSnapshotSchemaVersion: string
visitSnapshotCapturedAt: string
visitSnapshotHash: string
documentFont?: DocumentFont | null
templateContentSha256: string
templateFileSha256: string
smsMobileMasked: string | null
@@ -14,6 +14,9 @@
*/
import { computed, onMounted, ref, watch } from 'vue'
import { loadDocumentFont, type DocumentFont } from '../../../../packages/document-fonts'
import { getDefaultDocumentFont } from '@/api/management/document-fonts'
import type { SigningAnswers } from '@/api/workbench/types'
import {
applyAnswerToFieldHost,
@@ -36,6 +39,8 @@ const props = withDefaults(
defineProps<{
/** 文书正文(模板定稿 / 任务固化快照) */
contentHtml: string
/** undefined: template preview uses default; null: historical task retains legacy font. */
documentFont?: DocumentFont | null
/** 回填上下文:患者信息 / 签名 / 勾选 / 已填取值 */
data?: SigningDocumentData
/** 填写项可否改动、签名位可否点击采集 */
@@ -62,6 +67,7 @@ const props = withDefaults(
variant?: 'plain' | 'card'
}>(),
{
documentFont: undefined,
data: () => ({}),
interactive: false,
signatureActionable: undefined,
@@ -80,6 +86,26 @@ const emit = defineEmits<{
signatureRequest: [request: SigningSignatureRequest]
}>()
const fontReady = ref(false)
const fontError = ref('')
const fontFamily = ref('')
let fontLoadVersion = 0
async function prepareFont() {
const version = ++fontLoadVersion
fontReady.value = false
fontError.value = ''
try {
const font =
props.documentFont === undefined ? await getDefaultDocumentFont() : props.documentFont
const family = await loadDocumentFont(font, import.meta.env.VITE_API_BASE_URL || '/api')
if (version !== fontLoadVersion) return
fontFamily.value = family
fontReady.value = true
} catch {
if (version === fontLoadVersion) fontError.value = '文书字体加载失败,请重试。'
}
}
watch(() => props.documentFont, prepareFont, { immediate: true })
const containerRef = ref<HTMLElement | null>(null)
/**
@@ -300,6 +326,7 @@ onMounted(() => {
/** 供「导出 PDF」使用:拿到已渲染的文书容器(含固定页眉与签署态取值) */
function getDocumentElement() {
if (!fontReady.value) return null
return containerRef.value
}
@@ -310,7 +337,16 @@ defineExpose({ getDocumentElement })
<!-- eslint-disable vue/no-v-html -- 内容为本系统定稿的文书 HTML(编辑器产出、仅医务人员可改),非任意用户输入 -->
<!-- doc-paper--a4:无论哪种 variant 都是「一页 A4」(比例 + 一般文书页边距),
variant 只决定盒子装饰 —— card 自带圆角阴影,plain 交给调用方 -->
<article class="doc-paper doc-paper--a4" :class="{ 'doc-paper--card': variant === 'card' }">
<p v-if="!fontReady" role="status">
{{ fontError || '正在加载文书字体…' }}
<button v-if="fontError" type="button" @click="prepareFont">重试</button>
</p>
<article
class="doc-paper doc-paper--a4"
:class="{ 'doc-paper--card': variant === 'card', 'managed-font': Boolean(fontFamily) }"
:style="{ '--document-font': fontFamily, visibility: fontReady ? 'visible' : 'hidden' }"
:inert="!fontReady"
>
<div
ref="containerRef"
class="doc-paper__body"
@@ -325,6 +361,12 @@ defineExpose({ getDocumentElement })
</template>
<style scoped>
.managed-font,
.managed-font :deep(*) {
font-family: var(--document-font) !important;
font-synthesis: none;
}
/* 基础排版(p / h2 / h3 / table / th / td / img)在 utils/document-paper.ts 的
DOCUMENT_PAPER_CSS 里,运行期注入 head —— 那里是唯一一份。
这里只保留容器自己的盒子。 */
@@ -20,6 +20,7 @@ import type {
} from '@/utils/signing-document'
import DocumentPaper from './DocumentPaper.vue'
import type { DocumentFont } from '../../../../packages/document-fonts'
const props = withDefaults(
defineProps<{
@@ -35,6 +36,7 @@ const props = withDefaults(
activeTab?: string
/** 正文(模板定稿 / 任务固化快照) */
contentHtml: string
documentFont?: DocumentFont | null
data?: SigningDocumentData
interactive?: boolean
signatureActionable?: boolean
@@ -65,6 +67,7 @@ const props = withDefaults(
stacked?: boolean
}>(),
{
documentFont: undefined,
subtitle: '',
badges: () => [],
tabs: undefined,
@@ -164,6 +167,7 @@ defineExpose({ getDocumentElement })
<DocumentPaper
v-if="hasPaper"
ref="paperRef"
:document-font="documentFont"
:content-html="contentHtml"
:data="data"
:interactive="interactive"
@@ -114,6 +114,7 @@ defineExpose({ getDocumentElement })
<div class="sign-doc-body">
<DocumentPaper
ref="paperRef"
:document-font="task.documentFont ?? null"
:content-html="task.documentContentHtml ?? ''"
:data="documentData"
:interactive="interactive"
@@ -0,0 +1,294 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useLoginStore } from '@/stores/login'
import {
getDocumentFontSettings,
inspectDocumentFont,
uploadDocumentFont,
checkDocumentFont,
previewDocumentFont,
activateDocumentFont,
setDocumentFontEnabled,
type FontSettings,
type FontFaceInfo,
type FontRecord,
type FontCheck,
} from '@/api/management/document-fonts'
import { loadDocumentFont } from '../../../../../../packages/document-fonts'
const canEdit = computed(() => useLoginStore().hasPermission('system:config:edit'))
const settings = ref<FontSettings | null>(null)
const busy = ref(false)
const error = ref('')
const file = ref<File | null>(null)
const boldFile = ref<File | null>(null)
const faces = ref<FontFaceInfo[]>([])
const boldFaces = ref<FontFaceInfo[]>([])
const face = ref(0)
const boldFace = ref(0)
const licensed = ref(false)
const checks = ref<Record<string, FontCheck>>({})
const previewUrl = ref('')
const family = ref('')
const previewName = ref('')
const apiBase = import.meta.env.VITE_API_BASE_URL || '/api'
async function run(action: () => Promise<void>) {
busy.value = true
error.value = ''
try {
await action()
} catch (e) {
error.value = e instanceof Error ? e.message : '字体操作失败,请重试。'
} finally {
busy.value = false
}
}
async function refresh() {
settings.value = await getDocumentFontSettings()
}
async function selectFile(event: Event, bold = false) {
const selected = (event.target as HTMLInputElement).files?.[0] ?? null
if (bold) {
boldFile.value = null
boldFaces.value = []
boldFace.value = 0
} else {
file.value = null
faces.value = []
face.value = 0
}
licensed.value = false
if (!selected) return
await run(async () => {
if (selected.size > 32 * 1024 * 1024) throw new Error('单个字体文件不能超过 32 MiB。')
const result = await inspectDocumentFont(selected)
if (bold) {
boldFile.value = selected
boldFaces.value = result
} else {
file.value = selected
faces.value = result
}
})
}
async function upload() {
if (!file.value || !licensed.value) return
const form = new FormData()
form.append('file', file.value)
form.append('faceIndex', String(face.value))
form.append('licensed', 'true')
if (boldFile.value) {
form.append('boldFile', boldFile.value)
form.append('boldFaceIndex', String(boldFace.value))
}
await run(async () => {
await uploadDocumentFont(form)
await refresh()
ElMessage.success('字体已上传,可检测后设为默认字体')
})
}
async function check(font: FontRecord) {
await run(async () => {
checks.value[font.id] = await checkDocumentFont(font.id)
await refresh()
})
}
async function preview(font: FontRecord) {
await run(async () => {
const nextFamily = await loadDocumentFont(font, apiBase)
const pdf = await previewDocumentFont(font.id)
if (pdf.type !== 'application/pdf') throw new Error('测试 PDF 生成失败。')
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
family.value = nextFamily
previewName.value = font.name
previewUrl.value = URL.createObjectURL(pdf)
})
}
async function activate(font: FontRecord) {
await run(async () => {
await loadDocumentFont(font, apiBase)
await activateDocumentFont(font.id)
await refresh()
ElMessage.success('已设为默认字体,将用于新建签署任务')
})
}
async function toggle(font: FontRecord) {
await run(async () => {
await setDocumentFontEnabled(font.id, !font.enabled)
await refresh()
})
}
onMounted(() => run(refresh))
function closePreview() {
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
previewUrl.value = ''
}
onBeforeUnmount(closePreview)
</script>
<template>
<section class="font-settings" :aria-busy="busy">
<h2>文书字体</h2>
<p>用于医护端、患者端文书和正式 PDF。切换默认字体只影响新建签署任务,已有任务保留原字体。</p>
<el-alert v-if="error" :title="error" type="error" :closable="false" />
<p v-if="settings && !settings.defaultFontId">
当前使用部署字体。文件状态:{{
settings.legacyFileReadable ? '可读取' : '未配置或不可读取'
}}。上传并启用后,新任务将使用统一字体。
</p>
<fieldset v-if="canEdit" :disabled="busy">
<legend>上传字体</legend>
<label
>常规字体(TTF / TTC,最大 32 MiB)<input
type="file"
accept=".ttf,.ttc"
@change="selectFile($event)"
/></label>
<el-select v-if="faces.length" v-model="face" aria-label="常规字体字面" :disabled="busy">
<el-option
v-for="item in faces"
:key="item.index"
:value="item.index"
:label="`${item.name} · ${item.style}`"
/>
</el-select>
<label
>粗体字体(可选)<input type="file" accept=".ttf,.ttc" @change="selectFile($event, true)"
/></label>
<el-select
v-if="boldFaces.length"
v-model="boldFace"
aria-label="粗体字体字面"
:disabled="busy"
>
<el-option
v-for="item in boldFaces"
:key="item.index"
:value="item.index"
:label="`${item.name} · ${item.style}`"
/>
</el-select>
<small>未上传粗体时,粗体位置使用常规字形。</small>
<label
><input v-model="licensed" type="checkbox" />我确认该字体允许 PDF 嵌入、Web
分发及格式转换。</label
>
<el-button type="primary" :disabled="busy || !file || !licensed" @click="upload"
>上传字体</el-button
>
</fieldset>
<details v-if="settings?.nodes.length" class="node-health">
<summary>各服务节点最近检测结果(超过 10 分钟需重新确认)</summary>
<p v-for="node in settings.nodes" :key="`${node.fontId}:${node.node}`">
{{ settings.fonts.find((item) => item.id === node.fontId)?.name }} · {{ node.node }}:
{{ Date.now() - Date.parse(node.checkedAt) > 600000 ? '检测结果已过期' : node.message }}({{
node.checkedAt
}})
</p>
</details>
<el-table :data="settings?.fonts ?? []" empty-text="暂无上传字体">
<el-table-column label="字体" min-width="220"
><template #default="{ row }"
>{{ row.name }} <el-tag v-if="row.id === settings?.defaultFontId">默认</el-tag>
<div class="muted">{{ row.boldSha256 ? '含粗体' : '仅常规字形' }}</div></template
></el-table-column
>
<el-table-column label="状态" width="90"
><template #default="{ row }">{{
row.enabled ? '启用' : '停用'
}}</template></el-table-column
>
<el-table-column label="检测" min-width="230"
><template #default="{ row }"
><span v-if="checks[row.id]" :class="{ failed: !checks[row.id]?.available }"
>{{ checks[row.id]?.message
}}<small class="muted"
>节点 {{ checks[row.id]?.node }} · {{ checks[row.id]?.checkedAt }}</small
></span
><span v-else>尚未检测</span></template
></el-table-column
>
<el-table-column label="操作" min-width="300"
><template #default="{ row }">
<el-button link :disabled="busy" @click="check(row)">检测</el-button>
<el-button link :disabled="busy" @click="preview(row)">预览</el-button>
<el-button
v-if="canEdit"
link
type="primary"
:disabled="busy || !row.enabled || row.id === settings?.defaultFontId"
@click="activate(row)"
>设为默认</el-button
>
<el-button
v-if="canEdit"
link
:disabled="busy || row.id === settings?.defaultFontId"
@click="toggle(row)"
>{{ row.enabled ? '停用' : '启用' }}</el-button
>
</template></el-table-column
>
</el-table>
<el-dialog
:model-value="Boolean(previewUrl)"
title="文书字体预览"
width="900px"
@close="closePreview"
>
<p>{{ previewName }}</p>
<div class="sample" :style="{ fontFamily: family, fontSynthesis: 'none' }">
<h3>知情同意书</h3>
<p>患者姓名:张三 性别:男 年龄:36</p>
<p>签署日期:2026-09-30 医师确认</p>
</div>
<iframe v-if="previewUrl" :src="previewUrl" title="后端生成的测试 PDF" class="pdf-preview" />
</el-dialog>
</section>
</template>
<style scoped>
.font-settings {
padding: 24px;
background: white;
border-radius: 10px;
}
h2 {
margin-top: 0;
font-size: 18px;
}
p,
small {
color: var(--mut);
}
fieldset {
display: grid;
gap: 12px;
margin: 20px 0;
padding: 18px;
border: 1px solid var(--line);
border-radius: 8px;
}
label {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.muted {
display: block;
color: var(--mut);
font-size: 12px;
}
.failed {
color: var(--err);
}
.sample {
padding: 16px;
border: 1px solid var(--line);
}
.pdf-preview {
width: 100%;
height: 480px;
border: 0;
margin-top: 12px;
}
</style>
@@ -6,7 +6,7 @@
* 只显示所选分区(V5.7 加索引 → V5.8 改单选切换 → V6.0 去卡片描述、单列铺满)。
*
* 分区与接口现状:
* - 「菜单设置」是唯一接了真实接口的分区(菜单资源 CRUD,见 MenuResourceSettings),
* - 「菜单设置」和「文书字体」接入真实接口;菜单分区(菜单资源 CRUD,见 MenuResourceSettings),
* 本页只负责把它放进索引,**不改它的实现**;无 `system:config:menu` 权限时整项不出现。
* - 其余分区(签署参数 / 短信模板 / 签署设备管理 / 签署状态推送 / 第三方系统嵌入 /
* 文档类型设置)**暂时都没有后端接口**,全部用页面内演示数据渲染,刷新即回初值。
@@ -17,6 +17,7 @@ import { ElMessage } from 'element-plus'
import { useLoginStore } from '@/stores/login'
import DocumentFontSettings from './components/DocumentFontSettings.vue'
import DeviceSettingsCard from './components/DeviceSettingsCard.vue'
import DocTypeSettingsCard from './components/DocTypeSettingsCard.vue'
import EmbedSettingsCard from './components/EmbedSettingsCard.vue'
@@ -55,11 +56,12 @@ const EMBED_URL =
const loginStore = useLoginStore()
const canManageMenus = computed(() => loginStore.hasPermission('system:config:menu'))
type SectionKey = 'sign' | 'sms' | 'device' | 'push' | 'embed' | 'menus' | 'docType'
type SectionKey = 'sign' | 'sms' | 'device' | 'push' | 'embed' | 'menus' | 'docType' | 'fonts'
const sections = computed<Array<{ key: SectionKey; label: string }>>(() => {
const list: Array<{ key: SectionKey; label: string }> = [
{ key: 'sign', label: '签署参数' },
{ key: 'fonts', label: '文书字体' },
{ key: 'sms', label: '短信模板' },
{ key: 'device', label: '签署设备管理' },
{ key: 'push', label: '签署状态推送' },
@@ -282,6 +284,8 @@ function removeDocType(name: string) {
@toggle="toggleSetting"
/>
<DocumentFontSettings v-else-if="activeSection === 'fonts'" />
<SmsSettingsCard
v-else-if="activeSection === 'sms'"
:channel="smsChannel"
@@ -202,6 +202,7 @@ function clearSignature() {
<!-- 公众号页把**完整文书**摆出来(原型 #mdWxPt 里就是整篇 docBody) -->
<div v-if="isWx" class="mobile-paper">
<DocumentPaper
:document-font="task?.documentFont ?? null"
:content-html="task.documentContentHtml ?? ''"
:data="documentData"
:interactive="false"
@@ -49,6 +49,7 @@ const documentData = computed<SigningDocumentData>(() => ({
<!-- 宽度用 960 而不是 900:舞台内宽 960−48=912,820px 的纸才放得下、两侧留白也舒服。
纸窄了(原 640)版心只剩 446px,用药信息表会被挤坏 -->
<DocumentPreviewDialog
:document-font="task?.documentFont ?? null"
:visible="visible && Boolean(task)"
title="在线预览(PDF)"
:subtitle="subtitle"
+54
View File
@@ -0,0 +1,54 @@
/** Immutable font descriptor shared by clinical and patient document renderers. */
export interface DocumentFont {
id: string;
name: string;
regularSha256: string;
boldSha256: string | null;
}
const loaded = new Map<string, Promise<string>>();
export function loadDocumentFont(
font: DocumentFont | null,
apiBase: string,
): Promise<string> {
if (!font) return Promise.resolve("");
const key = `${apiBase}:${font.id}:${font.regularSha256}:${font.boldSha256 ?? ""}`;
const existing = loaded.get(key);
if (existing) return existing;
const family = `Medisign-${font.regularSha256}-${font.boldSha256 ?? "regular"}`;
const load = async () => {
const descriptors = [
{ sha: font.regularSha256, weight: "400" },
{ sha: font.boldSha256 ?? font.regularSha256, weight: "700" },
];
const faces = await Promise.all(
descriptors.map(async ({ sha, weight }) => {
const url = `${apiBase.replace(/\/$/, "")}/v1/document-fonts/${encodeURIComponent(font.id)}/${sha}.ttf`;
const response = await fetch(url, {
signal: AbortSignal.timeout(30000),
});
if (!response.ok)
throw new Error("文书字体加载失败,请重试或联系管理员。");
const bytes = await response.arrayBuffer();
// The URL names an immutable digest; verify before registering the face.
if (globalThis.crypto?.subtle) {
const digest = await crypto.subtle.digest("SHA-256", bytes);
const actual = Array.from(new Uint8Array(digest), (byte) =>
byte.toString(16).padStart(2, "0"),
).join("");
if (actual !== sha) throw new Error("文书字体校验失败。");
}
return new FontFace(family, bytes, { weight }).load();
}),
);
for (const face of faces) document.fonts.add(face);
return `"${family}"`;
};
const promise = load().catch((error: unknown) => {
loaded.delete(key);
throw error;
});
loaded.set(key, promise);
return promise;
}
+2
View File
@@ -1,3 +1,4 @@
import type { DocumentFont } from '../../../packages/document-fonts'
export interface ApiResponse<T> {
code: number | string
message: string
@@ -92,6 +93,7 @@ export interface SigningContextTemplate {
}
export interface SigningContextDocument {
font?: DocumentFont | null
contentHtml?: string | null
contentSha256?: string | null
}
+15 -1
View File
@@ -6,6 +6,7 @@ const props = withDefaults(
defineProps<{
/** 文书正文 HTML(含占位符),由 context 接口返回 */
contentHtml: string
fontFamily?: string
fields?: SigningFormField[]
patientAnswers?: Record<string, PatientAnswer>
interactive?: boolean
@@ -22,6 +23,7 @@ const props = withDefaults(
signedAt?: string | null
}>(),
{
fontFamily: '',
answers: () => ({}),
fields: () => [],
patientAnswers: () => ({}),
@@ -232,10 +234,22 @@ watch(
正文来自医院自己的文书模板(经后端存储下发),不是用户输入,因此这里用 v-html 渲染。
患者端不解析、不执行其中任何脚本 —— 后端下发前应已做清洗。
-->
<article ref="paperRef" class="patient-paper" v-html="contentHtml" />
<article
ref="paperRef"
class="patient-paper"
:class="{ 'managed-font': Boolean(fontFamily) }"
:style="{ '--document-font': fontFamily }"
v-html="contentHtml"
/>
</template>
<style scoped>
.managed-font,
.managed-font :deep(*) {
font-family: var(--document-font) !important;
font-synthesis: none;
}
.patient-paper {
padding: 18px 16px 22px;
color: #173b56;
+13 -1
View File
@@ -1,3 +1,4 @@
import { loadDocumentFont } from '../../../packages/document-fonts'
import { reactive } from 'vue'
import {
@@ -59,6 +60,8 @@ const initialState: SignFlowState = {
age: '',
visitNo: 'MZ20260827018',
department: '',
documentFontFamily: '',
documentFontReady: false,
documentTitle: '儿科急诊特殊检查知情同意书',
templateVersion: 'v1.0.0',
documentContentHtml: '',
@@ -447,6 +450,12 @@ async function loadSigningContext(): Promise<SigningContextResponse | null> {
try {
const context = await fetchSigningContext(proof)
applyContext(context)
signingState.documentFontReady = false
signingState.documentFontFamily = await loadDocumentFont(
context.document?.font ?? null,
import.meta.env.VITE_API_BASE_URL || '/api',
)
signingState.documentFontReady = true
persistSession()
return context
} catch (error) {
@@ -488,6 +497,7 @@ function applyDemoContext() {
signingState.fields = [
{ id: DEMO_PATIENT_FIELD_ID, label: '患者签名', kind: 'signature', required: true },
]
signingState.documentFontReady = true
signingState.documentLoaded = true
signingState.documentError = ''
signingState.taskStatus = 'WAITING_SIGN'
@@ -583,7 +593,9 @@ export async function submitRealSignature(input: {
!signingState.taskId ||
!signingState.deliveryId ||
!signingState.uploadToken ||
!input.consentAccepted
!input.consentAccepted ||
!signingState.documentLoaded ||
!signingState.documentFontReady
) {
throw new Error('签署凭证或知情确认状态无效,请重新打开签署链接。')
}
+2
View File
@@ -26,6 +26,8 @@ export interface SignFlowState {
visitNo: string
department: string
documentFontFamily: string
documentFontReady: boolean
documentTitle: string
templateVersion: string
/**
@@ -56,6 +56,7 @@ async function continueToSigner() {
</section>
<DocumentPaper
:font-family="signingState.documentFontFamily"
:content-html="signingState.documentContentHtml"
:answers="signingState.staffAnswers"
:fields="signingState.form?.fields"
@@ -117,6 +117,7 @@ async function submitSigning() {
<!-- 签完的字会实时回填到文书里的签名位,患者能立刻看到自己签在哪一格 -->
<DocumentPaper
:font-family="signingState.documentFontFamily"
:content-html="signingState.documentContentHtml"
:answers="signingState.staffAnswers"
:fields="signingState.form?.fields"