refactor(文书预览): 抽出统一的文书纸与预览弹窗,消除五处重复渲染
预览原先在 5 个地方各写一遍(文书库预览、编辑器签署预览、发起签署弹窗预览、 工作台在线预览、工作台文书区),基础排版已经分叉:表格边框 #52646e 与 #c8d7dd 两套色、单元格内边距 5px 7px 与 5px 9px 两个值、只有两处带宋体。 原型本身是「一处渲染(.paper + fillDocHtml)、两种外框」,这次按那个形状收口。 新增 - utils/document-paper.ts:基础排版的**唯一一份** CSS(p / h1-h4 / 表格 / 宋体)。 运行期幂等注入 head —— 因为同一份纸有两条渲染通道:v-html(吃全局样式)与 iframe srcdoc(隔离模板自带 style,只能吃字符串),各写一份必然漂移。 - components/document/DocumentPaper.vue:唯一的纸渲染器。 signing / raw 两模式、interactive、取值还原、☑☐ 就地切换、签名位请求采集、 填写项与勾选上报、getDocumentElement()(供导出 PDF)。 - components/document/DocumentPreviewDialog.vue:唯一的预览外框。 标题/副标题/徽标、可选页签、加载与错误态、纸舞台(浅色卡片 / 深灰 A4)、 #footer 与 #empty 插槽。 - styles/document-preview.css:页脚按钮等**插槽内容**要用的类。 插槽节点带的是父组件 scope id,弹窗的 scoped 样式命中不了 —— 与 signing-document.css 同一类问题,同一套解法。 迁移 - 文书库预览、编辑器签署预览、发起签署弹窗预览、工作台在线预览全部改走统一组件; SigningDocumentInteractive 的纸换成 DocumentPaper,只留任务外壳 (横幅 / 作废章 / 填写项进度)。 - 删除四处基础排版副本;SimSun 与 :deep(table) 现在各只剩一处。 行为保持 - 文书库预览仍是两个页签(文书内容走 iframe 隔离、签署预览走 v-html), 没有正文时仍给占位版式(占位符写成 {患者姓名} 原始形态,不预填示例值)。 - 工作台预览的深灰 A4 舞台、页脚说明与两个出口按钮不变。 - 作废章原先是纸的兄弟节点、DOM 顺序在前,纸改成自带白底后会被盖住 —— 给章补 z-index,并在探针里加命中测试锁死。 验证 - 新增 probe-doc-paper.cjs:31/31 PASS。核心是**计算样式**断言 (边框色 / 内边距 / 宋体 / border-collapse),并断言文书库预览与编辑器预览 两份纸的样式**逐项一致** —— 只看 DOM 存在证明不了"只有一份排版"。 - 回归:probe-new-task 104/104、probe-workbench-lib 46/46(新增作废章命中测试)、 probe-dept-confirm 36/36、probe-method-domain 14/14、probe-report-analytics 92/92。 - vue-tsc 与 eslint 0 错。 净减 888 行(309 增 / 1197 删)。
This commit is contained in:
+170
-583
@@ -1,18 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
/**
|
||||
* 文书库的模板预览。
|
||||
*
|
||||
* 它只是**统一预览弹窗**在文书库这一处的配置:两个页签(文书内容 / 签署预览)+
|
||||
* 「用此模板发起签署」出口。纸的渲染、外框、页脚按钮都在 components/document/ 里,
|
||||
* 这里不再实现任何渲染逻辑 —— 改版前这一处自己抄了一份基础排版(表格边框、字号、宋体),
|
||||
* 与签署工作台那份已经分叉。
|
||||
*
|
||||
* 两个页签的区别只在渲染模式:
|
||||
* - 文书内容:`mode="raw"`,原样展示定稿(iframe 隔离模板自带的 `<style>`);
|
||||
* - 签署预览:`mode="signing"`,按签署时效果渲染,占位符用示例患者填充、☑/☐ 可直接点。
|
||||
*/
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import DocumentPreviewDialog from '@/components/document/DocumentPreviewDialog.vue'
|
||||
import type { SigningAnswers } from '@/api/workbench/types'
|
||||
import type { SigningFieldProgress } from '@/utils/signing-document'
|
||||
|
||||
import {
|
||||
buildSigningDocumentHtml,
|
||||
collectSelectedItemsFromRoot,
|
||||
describeFieldProgress,
|
||||
OPTION_TICK_SELECTOR,
|
||||
readAnswersFromRoot,
|
||||
toggleOptionAt,
|
||||
type SigningFieldProgress,
|
||||
} from '@/utils/signing-document'
|
||||
|
||||
import { canInitiateSigning, readLibraryStatus, readStatusTone } from '../library'
|
||||
import { PREVIEW_PATIENT as previewPatient } from '../sample-patient'
|
||||
canInitiateSigning,
|
||||
readLibraryStatus,
|
||||
readStatusTone,
|
||||
type DocumentStatusTone,
|
||||
} from '../library'
|
||||
import { PREVIEW_PATIENT } from '../sample-patient'
|
||||
import type { DocumentLibraryStatus, DocumentTemplate } from '../types'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -27,45 +38,31 @@ const emit = defineEmits<{
|
||||
use: [template: DocumentTemplate]
|
||||
}>()
|
||||
|
||||
/**
|
||||
* 签署预览用的示例患者快照见 ../sample-patient(编辑器弹窗的「签署预览」共用同一份)。
|
||||
*/
|
||||
|
||||
/** 预览页签:「文书内容」看原始定稿,「签署预览」看占位符填充与勾选交互化后的样子 */
|
||||
const activeTab = ref<'content' | 'signing'>('content')
|
||||
/** 签署预览里科室试勾选的结果,仅用于预览反馈,不作为签署证据 */
|
||||
const signingSelection = ref<string[]>([])
|
||||
/** 签署预览里填写项的完成情况,同样只用于预览反馈 */
|
||||
const signingProgress = ref<SigningFieldProgress>({
|
||||
total: 0,
|
||||
filled: 0,
|
||||
missingRequired: [],
|
||||
abnormal: [],
|
||||
pendingSignatures: [],
|
||||
})
|
||||
const signingBodyRef = ref<HTMLElement | null>(null)
|
||||
const activeTab = ref('content')
|
||||
|
||||
// 切到签署预览时才挂载文书 DOM,挂载后再统计填写项,否则进页面会显示「0 / 0 项」
|
||||
watch(activeTab, async (tab) => {
|
||||
if (tab === 'signing') {
|
||||
await nextTick()
|
||||
syncPreviewFields()
|
||||
}
|
||||
})
|
||||
/**
|
||||
* 签署预览里的试填结果,仅用于预览反馈,不作为签署证据。
|
||||
* 只在页脚展示一行汇总,所以只留计数与清单,不留整份取值。
|
||||
*/
|
||||
const filledCount = ref(0)
|
||||
const totalCount = ref(0)
|
||||
const selectedLabels = ref<string[]>([])
|
||||
|
||||
function handleFieldChange(_answers: SigningAnswers, progress: SigningFieldProgress) {
|
||||
filledCount.value = progress.filled
|
||||
totalCount.value = progress.total
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
// 每次打开都回到「文书内容」,并把上一次的试填结果清掉
|
||||
activeTab.value = 'content'
|
||||
signingSelection.value = []
|
||||
signingProgress.value = {
|
||||
total: 0,
|
||||
filled: 0,
|
||||
missingRequired: [],
|
||||
abnormal: [],
|
||||
pendingSignatures: [],
|
||||
}
|
||||
filledCount.value = 0
|
||||
totalCount.value = 0
|
||||
selectedLabels.value = []
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -75,10 +72,16 @@ const libraryStatus = computed<DocumentLibraryStatus>(() =>
|
||||
props.template ? readLibraryStatus(props.template) : '草稿',
|
||||
)
|
||||
|
||||
const statusLabel = computed(() => libraryStatus.value)
|
||||
/** 文书状态 → 预览弹窗徽标色调(弹窗只认 brand / ok / warn / mut 四档) */
|
||||
const BADGE_TONES: Record<DocumentStatusTone, 'ok' | 'warn' | 'mut'> = {
|
||||
enabled: 'ok',
|
||||
review: 'warn',
|
||||
draft: 'mut',
|
||||
archived: 'mut',
|
||||
stopped: 'mut',
|
||||
}
|
||||
|
||||
/** 徽标配色与文书卡片同口径(见 library.ts 的 STATUS_TONES) */
|
||||
const statusTone = computed(() => readStatusTone(libraryStatus.value))
|
||||
const statusTone = computed(() => BADGE_TONES[readStatusTone(libraryStatus.value)])
|
||||
|
||||
/**
|
||||
* 与文书卡片保持同一口径:只有「启用中」的文书能发起签署。
|
||||
@@ -88,501 +91,144 @@ const canInitiate = computed(
|
||||
() => Boolean(props.template?.contentHtml) && canInitiateSigning(libraryStatus.value),
|
||||
)
|
||||
|
||||
// 预置文书的 contentHtml 是完整 HTML 文档(自带版式样式);
|
||||
// Word 自维护文书存的是编辑器产出的 HTML 片段,需要叠加文书基础渲染样式
|
||||
// (表格边框、正文字体等),使导入路径的"裸表格"等语义结构也有合理的文书外观。
|
||||
const DOCUMENT_BASE_STYLE = `
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 24px 28px 36px;
|
||||
color: #1c2b33;
|
||||
font-family: 'SimSun', 'Songti SC', 'STSong', serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.85;
|
||||
background: #fff;
|
||||
overflow-x: auto;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
margin: 8px 0;
|
||||
border-collapse: collapse;
|
||||
table-layout: auto;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
padding: 5px 7px;
|
||||
border: 1px solid #52646e;
|
||||
vertical-align: middle;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
th p,
|
||||
td p {
|
||||
margin: 0;
|
||||
}
|
||||
`
|
||||
const tabs = computed(() => [
|
||||
{ key: 'content', label: '文书内容' },
|
||||
{
|
||||
key: 'signing',
|
||||
label: '签署预览',
|
||||
disabled: !props.template?.contentHtml,
|
||||
hint: '按签署时的效果渲染:占位符填充、勾选表交互化、签名域',
|
||||
},
|
||||
])
|
||||
|
||||
const previewSrcDoc = computed(() => {
|
||||
const contentHtml = props.template?.contentHtml ?? ''
|
||||
const badges = computed(() => [
|
||||
{ text: '类 Word 版式', tone: 'brand' as const },
|
||||
{ text: libraryStatus.value, tone: statusTone.value },
|
||||
])
|
||||
|
||||
if (/^\s*<!doctype html|^\s*<html[\s>]/i.test(contentHtml)) {
|
||||
return contentHtml
|
||||
}
|
||||
|
||||
return `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8" /><style>${DOCUMENT_BASE_STYLE}</style></head><body>${contentHtml}</body></html>`
|
||||
})
|
||||
|
||||
// 签署预览:复用签署端同一套渲染管线(占位符填充 → 签名域 → 勾选表交互化),
|
||||
// 患者/就诊数据用示例快照。interactive 传 true,科室可以在预览里试勾选、确认交互效果。
|
||||
// 这里不用 iframe:需要读取勾选结果做汇总,iframe 的 sandbox 也不便于表单交互。
|
||||
const signingPreviewHtml = computed(() =>
|
||||
buildSigningDocumentHtml(props.template?.contentHtml ?? '', previewPatient, true),
|
||||
const note = computed(() =>
|
||||
activeTab.value === 'signing'
|
||||
? `按签署时效果渲染:占位符已用示例患者「${PREVIEW_PATIENT.patientName}」填充,空白处是待补录的下划线、☑ / ☐ 可直接点击确认交互效果(预览不保存结果)。`
|
||||
: '',
|
||||
)
|
||||
|
||||
// 填写项与勾选汇总只在预览内展示(切页签、重开弹窗即重置),不作为签署证据。
|
||||
function syncPreviewFields() {
|
||||
const body = signingBodyRef.value
|
||||
|
||||
if (!body) {
|
||||
return
|
||||
}
|
||||
|
||||
const answers = readAnswersFromRoot(body)
|
||||
signingProgress.value = describeFieldProgress(body, answers)
|
||||
signingSelection.value = collectSelectedItemsFromRoot(body)
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览里的 ☑ / ☐ 就地切换。
|
||||
* 选项渲染成文本(原型口径),不会触发 change 事件,必须自己接 click;
|
||||
* 切换判据在 utils/signing-document 的 toggleOptionAt 里,与签署端共用一份。
|
||||
*/
|
||||
function handlePreviewClick(event: MouseEvent) {
|
||||
const body = signingBodyRef.value
|
||||
const target = event.target
|
||||
|
||||
if (!body || !(target instanceof Element)) {
|
||||
return
|
||||
}
|
||||
|
||||
const option = target.closest<HTMLElement>(OPTION_TICK_SELECTOR)
|
||||
|
||||
if (option && toggleOptionAt(body, option)) {
|
||||
syncPreviewFields()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="visible && template"
|
||||
class="dialog-mask"
|
||||
role="presentation"
|
||||
@click.self="emit('close')"
|
||||
>
|
||||
<section
|
||||
class="preview-dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="document-preview-title"
|
||||
<DocumentPreviewDialog
|
||||
:visible="visible && Boolean(template)"
|
||||
:title="template?.name ?? ''"
|
||||
:badges="badges"
|
||||
:tabs="tabs"
|
||||
:active-tab="activeTab"
|
||||
:mode="activeTab === 'signing' ? 'signing' : 'raw'"
|
||||
:content-html="template?.contentHtml ?? ''"
|
||||
:data="PREVIEW_PATIENT"
|
||||
:interactive="activeTab === 'signing'"
|
||||
:note="note"
|
||||
:loading="loading"
|
||||
:error="error ? '模板内容加载失败,请关闭后重试。' : ''"
|
||||
:empty-text="`「${template?.name ?? ''}」暂无结构化正文`"
|
||||
@close="emit('close')"
|
||||
@update:active-tab="activeTab = $event"
|
||||
@field-change="handleFieldChange"
|
||||
@selection-change="selectedLabels = $event"
|
||||
>
|
||||
<!-- 没有正文时的占位版式:这是**占位**,所以占位符写成 {患者姓名} 的原始形态,
|
||||
不预填任何示例值 —— 预填会让人以为这份文书已经有正文了 -->
|
||||
<template #empty>
|
||||
<article class="placeholder-paper">
|
||||
<h3>{{ template?.name }}</h3>
|
||||
<p class="paper-number">
|
||||
模板编号:{{ template?.code || '未编号' }} · 版本 {{ template?.version }}
|
||||
</p>
|
||||
<p>
|
||||
患者:<strong>{患者姓名}</strong> · 性别:<strong>{性别}</strong> · 年龄:<strong
|
||||
>{年龄}</strong
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
门诊/住院号:<strong>{就诊号}</strong> · 科室:<strong>{{
|
||||
template?.department
|
||||
}}</strong>
|
||||
· 日期:<strong>{签署日期}</strong>
|
||||
</p>
|
||||
<p>
|
||||
医务人员已就本项检查、治疗的适应证、禁忌证、医疗风险、替代方案及可能出现的不良后果向本人(或授权委托人)充分说明,本人已完全理解上述内容,自愿接受并签署确认。
|
||||
</p>
|
||||
<p>
|
||||
本文档采用电子方式签署,患者手写签名图片将合成至下方签名域,文档原件与签名原图双留存。
|
||||
</p>
|
||||
<div class="placeholder-signature">
|
||||
<div>
|
||||
<span>患者 / 授权委托人签名:</span>
|
||||
<div class="placeholder-line">签名域(自动合成)</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>日期:</span>
|
||||
<div class="placeholder-line placeholder-line--date">自动填充</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="placeholder-hint">
|
||||
本文书尚无结构化正文,以上为占位版式。请在卡片上点「编辑正文」导入 .docx
|
||||
或编写正文后再预览。
|
||||
</p>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<span v-if="activeTab === 'signing' && totalCount" class="preview-note-footer">
|
||||
填写项 {{ filledCount }} / {{ totalCount }} 项已填<template v-if="selectedLabels.length"
|
||||
>・已勾选 {{ selectedLabels.length }} 项:{{ selectedLabels.join('、') }}</template
|
||||
>
|
||||
</span>
|
||||
<span v-else class="preview-note-footer">
|
||||
文书版式取自科室真实知情同意书原件,{占位符}与勾选项在发起签署时由系统自动填充。
|
||||
</span>
|
||||
<span class="preview-spacer" />
|
||||
<button type="button" class="preview-button preview-button--ghost" @click="emit('close')">
|
||||
关闭
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="preview-button"
|
||||
:disabled="!canInitiate || !template"
|
||||
:title="canInitiate ? '用此模板发起签署' : '仅「启用中」且已有正文的文书可发起签署'"
|
||||
@click="template && emit('use', template)"
|
||||
>
|
||||
<header class="dialog-header">
|
||||
<div class="dialog-title">
|
||||
<h2 id="document-preview-title">{{ template.name }}</h2>
|
||||
<span class="word-badge">类 Word 版式</span>
|
||||
<span class="status-badge" :class="'tone-' + statusTone">{{ statusLabel }}</span>
|
||||
</div>
|
||||
<button type="button" class="close-button" aria-label="关闭预览" @click="emit('close')">
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="preview-tabs" role="tablist" aria-label="预览方式">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="preview-tab"
|
||||
:class="{ 'is-active': activeTab === 'content' }"
|
||||
:aria-selected="activeTab === 'content'"
|
||||
@click="activeTab = 'content'"
|
||||
>
|
||||
文书内容
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="preview-tab"
|
||||
:class="{ 'is-active': activeTab === 'signing' }"
|
||||
:aria-selected="activeTab === 'signing'"
|
||||
:disabled="!template.contentHtml"
|
||||
title="按签署时的效果渲染:占位符填充、勾选表交互化、签名域"
|
||||
@click="activeTab = 'signing'"
|
||||
>
|
||||
签署预览
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="dialog-body">
|
||||
<div v-if="loading" class="preview-state">正在加载模板内容…</div>
|
||||
<div v-else-if="error" class="preview-state preview-state--error">
|
||||
模板内容加载失败,请关闭后重试。
|
||||
</div>
|
||||
<template v-else-if="template.contentHtml && activeTab === 'signing'">
|
||||
<p class="preview-note">
|
||||
按签署时效果渲染:占位符已用示例患者「{{ previewPatient.patientName }}」填充,
|
||||
空白处是待补录的下划线、☑ / ☐ 可直接点击确认交互效果(预览不保存结果)。
|
||||
</p>
|
||||
<!-- eslint-disable vue/no-v-html -- 内容为本系统定稿的文书 HTML(编辑器产出、仅医务人员可改),非任意用户输入 -->
|
||||
<div
|
||||
ref="signingBodyRef"
|
||||
class="signing-preview"
|
||||
@change="syncPreviewFields"
|
||||
@input="syncPreviewFields"
|
||||
@click="handlePreviewClick"
|
||||
v-html="signingPreviewHtml"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<p class="signing-selection">
|
||||
填写项 <strong>{{ signingProgress.filled }}</strong> /
|
||||
{{ signingProgress.total }} 项已填<template v-if="signingSelection.length">
|
||||
・已勾选 <strong>{{ signingSelection.length }}</strong> 项:{{
|
||||
signingSelection.join('、')
|
||||
}}</template
|
||||
><template v-if="signingProgress.abnormal.length">
|
||||
・异常项:<strong class="is-abnormal">{{
|
||||
signingProgress.abnormal.join('、')
|
||||
}}</strong></template
|
||||
>
|
||||
</p>
|
||||
</template>
|
||||
<iframe
|
||||
v-else-if="template.contentHtml"
|
||||
class="preview-frame"
|
||||
:srcdoc="previewSrcDoc"
|
||||
sandbox=""
|
||||
title="文档模板内容预览"
|
||||
/>
|
||||
<article v-else class="preview-paper">
|
||||
<h3>{{ template.name }}</h3>
|
||||
<p class="paper-number">模板编号:{{ template.code }} · 版本 {{ template.version }}</p>
|
||||
<p>
|
||||
患者:<strong>{患者姓名}</strong> · 性别:<strong>{性别}</strong> · 年龄:<strong
|
||||
>{年龄}</strong
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
门诊/住院号:<strong>{就诊号}</strong> · 科室:<strong>{{
|
||||
template.department
|
||||
}}</strong>
|
||||
· 日期:<strong>{签署日期}</strong>
|
||||
</p>
|
||||
<p>
|
||||
医务人员已就本项检查、治疗的适应证、禁忌证、医疗风险、替代方案及可能出现的不良后果向本人(或授权委托人)充分说明,本人已完全理解上述内容,自愿接受并签署确认。
|
||||
</p>
|
||||
<p>
|
||||
本文档采用电子方式签署,患者手写签名图片将合成至下方签名域,文档原件与签名原图双留存。
|
||||
</p>
|
||||
|
||||
<div class="signature-area">
|
||||
<div>
|
||||
<span>患者 / 授权委托人签名:</span>
|
||||
<div class="signature-line">签名域(自动合成)</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>日期:</span>
|
||||
<div class="signature-line date-line">自动填充</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<footer class="dialog-footer">
|
||||
<p>文书版式取自科室真实知情同意书原件,{占位符}与勾选项在发起签署时由系统自动填充。</p>
|
||||
<button type="button" class="footer-button secondary" @click="emit('close')">关闭</button>
|
||||
<button
|
||||
type="button"
|
||||
class="footer-button primary"
|
||||
:disabled="!canInitiate"
|
||||
:title="canInitiate ? '用此模板发起签署' : '仅已发布(审核通过)的模板可发起签署'"
|
||||
@click="emit('use', template)"
|
||||
>
|
||||
用此模板发起签署
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
</Teleport>
|
||||
用此模板发起签署
|
||||
</button>
|
||||
</template>
|
||||
</DocumentPreviewDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dialog-mask {
|
||||
position: fixed;
|
||||
z-index: 20;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
background: rgb(9 40 52 / 45%);
|
||||
}
|
||||
|
||||
.preview-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 720px;
|
||||
max-width: 96vw;
|
||||
max-height: calc(100vh - 32px);
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
box-shadow: var(--sh-modal);
|
||||
animation: dialog-pop 0.18s ease;
|
||||
}
|
||||
|
||||
@keyframes dialog-pop {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.96) translateY(8px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 12px 18px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dialog-title h2 {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: var(--ink);
|
||||
font-size: 15px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.word-badge,
|
||||
.status-badge {
|
||||
padding: 3px 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.word-badge {
|
||||
color: var(--brand);
|
||||
background: var(--brand-l);
|
||||
}
|
||||
|
||||
/* 状态徽标配色:与文书卡片同一套色调(见 library.ts 的 STATUS_TONES) */
|
||||
.status-badge.tone-draft,
|
||||
.status-badge.tone-archived,
|
||||
.status-badge.tone-stopped {
|
||||
color: #8a9aa3;
|
||||
background: #eceef0;
|
||||
}
|
||||
|
||||
.status-badge.tone-review {
|
||||
color: var(--warn);
|
||||
background: var(--warn-l);
|
||||
}
|
||||
|
||||
.status-badge.tone-enabled {
|
||||
color: var(--ok);
|
||||
background: var(--ok-l);
|
||||
}
|
||||
|
||||
.close-button {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
color: var(--mut);
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.preview-tabs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 8px 18px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.preview-tab {
|
||||
padding: 5px 12px;
|
||||
color: var(--mut);
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.preview-tab:hover:not(:disabled) {
|
||||
color: var(--brand);
|
||||
}
|
||||
|
||||
.preview-tab.is-active {
|
||||
color: var(--brand);
|
||||
background: var(--brand-l);
|
||||
}
|
||||
|
||||
.preview-tab:disabled {
|
||||
color: #b6c3c9;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.preview-note {
|
||||
padding: 8px 12px;
|
||||
margin: 0 0 10px;
|
||||
color: var(--brand);
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
background: var(--brand-l);
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
min-height: 0;
|
||||
padding: 12px 18px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.preview-state {
|
||||
display: grid;
|
||||
min-height: 420px;
|
||||
color: var(--mut);
|
||||
font-size: 13px;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.preview-state--error {
|
||||
color: var(--err);
|
||||
}
|
||||
|
||||
.preview-frame {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 560px;
|
||||
background: #fff;
|
||||
border: 1px solid #eef4f6;
|
||||
}
|
||||
|
||||
/* 签署预览用 v-html 渲染(需读取勾选结果,因此不用 sandbox iframe)。
|
||||
下列基础文书样式需与 previewSrcDoc 使用的 DOCUMENT_BASE_STYLE 保持一致;
|
||||
v-html 注入的节点不携带 scoped 属性,统一用 :deep 覆盖。 */
|
||||
.signing-preview {
|
||||
min-height: 560px;
|
||||
padding: 24px 28px 36px;
|
||||
color: #1c2b33;
|
||||
font-family: 'SimSun', 'Songti SC', 'STSong', serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.85;
|
||||
background: #fff;
|
||||
border: 1px solid #eef4f6;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.signing-preview :deep(img) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.signing-preview :deep(table) {
|
||||
width: 100%;
|
||||
margin: 8px 0;
|
||||
border-collapse: collapse;
|
||||
table-layout: auto;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.signing-preview :deep(th),
|
||||
.signing-preview :deep(td) {
|
||||
padding: 5px 7px;
|
||||
border: 1px solid #52646e;
|
||||
vertical-align: middle;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.signing-preview :deep(th p),
|
||||
.signing-preview :deep(td p) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 填写项胶囊、选项 ☑ / ☐、系统页眉的样式在 styles/signing-document.css(全局):
|
||||
同一份签署态文书会在文书库预览、签署工作台与编辑器三处出现,各写一份必然漂移。
|
||||
这里只保留预览容器自己的版式。 */
|
||||
|
||||
.signing-selection {
|
||||
padding: 8px 12px;
|
||||
margin: 10px 0 0;
|
||||
color: var(--mut);
|
||||
font-size: 12px;
|
||||
background: #f8fbfc;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.signing-selection strong {
|
||||
color: var(--brand);
|
||||
}
|
||||
|
||||
.signing-selection .is-abnormal {
|
||||
color: var(--err);
|
||||
}
|
||||
|
||||
.preview-paper {
|
||||
min-height: 420px;
|
||||
/* 占位版式:只在文书还没有正文时出现,是"告诉人这里该有什么"的示意图,
|
||||
不是真实文书纸,所以不复用 DocumentPaper 的排版,样式就地写清楚。
|
||||
真实文书的排版一律走 components/document/DocumentPaper.vue(唯一一份)。 */
|
||||
.placeholder-paper {
|
||||
padding: 26px 34px;
|
||||
color: var(--ink);
|
||||
font-size: 13px;
|
||||
line-height: 1.9;
|
||||
background: #fff;
|
||||
border: 1px solid #eef4f6;
|
||||
box-shadow: 0 1px 5px rgb(13 62 81 / 5%);
|
||||
border-radius: var(--r);
|
||||
box-shadow: var(--sh);
|
||||
}
|
||||
|
||||
.preview-paper h3 {
|
||||
.placeholder-paper h3 {
|
||||
margin: 0 0 6px;
|
||||
color: var(--ink);
|
||||
font-size: 17px;
|
||||
text-align: center;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.preview-paper p {
|
||||
.placeholder-paper p {
|
||||
margin: 0 0 7px;
|
||||
text-indent: 2em;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.preview-paper .paper-number {
|
||||
.placeholder-paper .paper-number {
|
||||
margin-bottom: 18px;
|
||||
color: var(--mut);
|
||||
font-size: 12px;
|
||||
@@ -590,7 +236,7 @@ function handlePreviewClick(event: MouseEvent) {
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
.signature-area {
|
||||
.placeholder-signature {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 40px;
|
||||
@@ -598,14 +244,14 @@ function handlePreviewClick(event: MouseEvent) {
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.signature-area > div > span {
|
||||
.placeholder-signature > div > span {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: var(--mut);
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.signature-line {
|
||||
.placeholder-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -615,77 +261,18 @@ function handlePreviewClick(event: MouseEvent) {
|
||||
border-bottom: 2px dashed var(--line);
|
||||
}
|
||||
|
||||
.signature-line.date-line {
|
||||
.placeholder-line--date {
|
||||
min-width: 130px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 10px 18px;
|
||||
background: #f8fbfc;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.dialog-footer p {
|
||||
max-width: 46%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: var(--mut);
|
||||
.placeholder-hint {
|
||||
padding: 8px 12px;
|
||||
margin: 20px 0 0 !important;
|
||||
color: var(--warn);
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.footer-button {
|
||||
padding: 7px 13px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-align: left !important;
|
||||
text-indent: 0 !important;
|
||||
background: var(--warn-l);
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.footer-button.secondary {
|
||||
margin-left: auto;
|
||||
color: var(--brand);
|
||||
background: #fff;
|
||||
border: 1px solid var(--brand);
|
||||
}
|
||||
|
||||
.footer-button.primary {
|
||||
color: #fff;
|
||||
background: var(--brand);
|
||||
border: 1px solid var(--brand);
|
||||
}
|
||||
|
||||
.footer-button:hover {
|
||||
filter: brightness(0.96);
|
||||
}
|
||||
|
||||
.footer-button:disabled {
|
||||
color: #aebbc0;
|
||||
background: #f1f5f6;
|
||||
border-color: #dce5e8;
|
||||
cursor: not-allowed;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.preview-paper {
|
||||
padding: 22px 18px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dialog-footer p {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.footer-button.secondary {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
SIGNING_FIELD_PRESETS,
|
||||
upgradeFieldTokensToStructured,
|
||||
} from '@/utils/signing-fields'
|
||||
import { buildSigningDocumentHtml } from '@/utils/signing-document'
|
||||
|
||||
import DocumentPaper from '@/components/document/DocumentPaper.vue'
|
||||
|
||||
import EditorWangPane from './editors/EditorWangPane.vue'
|
||||
import { DOC_TEMPLATE_PERMISSIONS } from '../permissions'
|
||||
@@ -338,9 +339,6 @@ const saveHints = computed(() => {
|
||||
|
||||
/** 签署预览:把正文按签署时效果渲染一遍(原型 docTogglePreview) */
|
||||
const previewing = ref(false)
|
||||
const previewHtml = computed(() =>
|
||||
previewing.value ? buildSigningDocumentHtml(valueHtml.value, PREVIEW_PATIENT, false) : '',
|
||||
)
|
||||
|
||||
function togglePreview() {
|
||||
if (!previewing.value) {
|
||||
@@ -543,12 +541,15 @@ function requestClose() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 签署预览:纸的渲染走统一组件,这里只给盒子(.preview-paper)。
|
||||
改版前这里自己抄了一份基础排版,表格边框与工作台那份已经分叉 -->
|
||||
<div v-if="previewing" class="preview-shell">
|
||||
<div class="preview-paper">
|
||||
<!-- eslint-disable vue/no-v-html -- 内容为本系统定稿的文书 HTML(编辑器产出、仅医务人员可改),非任意用户输入 -->
|
||||
<div v-html="previewHtml" />
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</div>
|
||||
<DocumentPaper
|
||||
class="preview-paper"
|
||||
:content-html="valueHtml"
|
||||
:data="PREVIEW_PATIENT"
|
||||
:interactive="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else class="editor-shell">
|
||||
@@ -1008,32 +1009,8 @@ function requestClose() {
|
||||
box-shadow: var(--sh);
|
||||
}
|
||||
|
||||
.preview-paper :deep(p) {
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
.preview-paper :deep(table) {
|
||||
width: 100%;
|
||||
margin: 8px 0 12px;
|
||||
border-collapse: collapse;
|
||||
table-layout: auto;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.preview-paper :deep(th),
|
||||
.preview-paper :deep(td) {
|
||||
padding: 5px 9px;
|
||||
vertical-align: middle;
|
||||
border: 1px solid #c8d7dd;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.preview-paper :deep(th p),
|
||||
.preview-paper :deep(td p) {
|
||||
margin: 0;
|
||||
}
|
||||
/* 基础排版(p / table / th / td / 宋体 …)在 components/document/DocumentPaper.vue
|
||||
的 DOCUMENT_PAPER_CSS 里,全项目唯一一份。这里只留盒子。 */
|
||||
|
||||
/* ---- 底部 ---- */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user