build(clinical-web): 新增富文本编辑与 Word 转换依赖

This commit is contained in:
yelan
2026-09-10 12:03:21 +08:00
parent 7bea008680
commit 23d85cf8ce
11 changed files with 1398 additions and 1063 deletions
+769 -1
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -13,8 +13,11 @@
"format:check": "prettier . --check"
},
"dependencies": {
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.20.0",
"element-plus": "^2.14.5",
"mammoth": "^1.12.2",
"pinia": "^4.0.3",
"vue": "^3.5.41",
"vue-router": "^5.2.0"
+16
View File
@@ -0,0 +1,16 @@
// mammoth 浏览器构建(.docx → HTML)的类型声明,官方包未内置类型。
declare module 'mammoth/mammoth.browser' {
export interface MammothResult {
/** 转换出的 HTML 片段 */
value: string
/** 转换告警(如不支持的 Word 特性) */
messages: Array<{ type: string; message: string }>
}
export function convertToHtml(
input: { arrayBuffer: ArrayBuffer },
options?: Record<string, unknown>,
): Promise<MammothResult>
export function extractRawText(input: { arrayBuffer: ArrayBuffer }): Promise<{ value: string }>
}
@@ -1,497 +0,0 @@
<script setup lang="ts">
import { computed, reactive, watch } from 'vue'
import type { CampusRecord, DepartmentRecord } from '@/api/management/types'
import type { DocumentTemplate, TemplateEditorForm, TemplateVersionAction } from '../types'
const props = defineProps<{
visible: boolean
template: DocumentTemplate | null
campuses: CampusRecord[]
departments: DepartmentRecord[]
saving: boolean
workflowSaving: boolean
}>()
const emit = defineEmits<{
close: []
save: [form: TemplateEditorForm]
workflow: [action: TemplateVersionAction, comment?: string]
}>()
const form = reactive<TemplateEditorForm>(createEmptyForm())
const error = reactive({ message: '' })
const rejectComment = reactive({ value: '' })
const isNew = computed(() => !form.id)
const availableDepartments = computed(() =>
props.departments.filter((department) => !form.campusId || department.campusId === form.campusId),
)
const workflowActions = computed(() => {
if (!props.template || !form.id) {
return []
}
const actions: Array<{ action: TemplateVersionAction; label: string; tone: string }> = []
if (
props.template.backendStatus === 'DRAFT' ||
props.template.backendStatus === 'REJECTED' ||
(!props.template.backendStatus && props.template.status === 'draft')
) {
actions.push({ action: 'submit-review', label: '提交审核', tone: 'primary' })
}
if (
props.template.backendStatus === 'PENDING_REVIEW' ||
(!props.template.backendStatus && props.template.status === 'review')
) {
actions.push({ action: 'approve', label: '审核通过', tone: 'primary' })
actions.push({ action: 'reject', label: '驳回', tone: 'danger' })
}
if (props.template.backendStatus === 'APPROVED') {
actions.push({ action: 'publish', label: '发布版本', tone: 'primary' })
}
if (
props.template.backendStatus === 'PUBLISHED' ||
(!props.template.backendStatus && props.template.status === 'enabled')
) {
actions.push({ action: 'disable', label: '停用版本', tone: 'secondary' })
actions.push({ action: 'archive', label: '归档版本', tone: 'secondary' })
}
return actions
})
function createEmptyForm(): TemplateEditorForm {
return {
templateCode: '',
name: '',
description: '',
campusId: props.campuses[0]?.id ?? '',
departmentId: '',
category: '知情同意书',
versionNo: '',
contentHtml: '',
}
}
function resetForm() {
if (!props.template) {
Object.assign(form, createEmptyForm())
} else {
Object.assign(form, {
id: props.template.id,
templateCode: props.template.code,
name: props.template.name,
description: props.template.description,
campusId: props.template.campusId ?? props.campuses[0]?.id ?? '',
departmentId: props.template.departmentId ?? '',
category: props.template.category,
versionNo: '',
contentHtml: props.template.contentHtml ?? '',
})
}
error.message = ''
rejectComment.value = ''
}
function handleCampusChange() {
if (!availableDepartments.value.some((department) => department.id === form.departmentId)) {
form.departmentId = ''
}
}
function submit() {
error.message = ''
if (!form.templateCode.trim()) {
error.message = '请输入模板编号'
return
}
if (!form.name.trim()) {
error.message = '请输入模板名称'
return
}
if (!form.campusId) {
error.message = '请选择院区'
return
}
if (!form.contentHtml.trim()) {
error.message = '请输入模板 HTML 内容'
return
}
emit('save', {
...form,
templateCode: form.templateCode.trim(),
name: form.name.trim(),
description: form.description.trim(),
category: form.category.trim() || '知情同意书',
versionNo: form.versionNo.trim(),
contentHtml: form.contentHtml.trim(),
})
}
function runWorkflow(action: TemplateVersionAction) {
if (action === 'reject' && !rejectComment.value.trim()) {
error.message = '请输入驳回原因'
return
}
error.message = ''
emit('workflow', action, rejectComment.value.trim() || undefined)
}
watch(
() => props.visible,
(visible) => {
if (visible) {
resetForm()
}
},
)
watch(
() => props.template,
() => {
if (props.visible) {
resetForm()
}
},
)
</script>
<template>
<Teleport to="body">
<div v-if="visible" class="dialog-mask" @click.self="emit('close')">
<section class="editor-dialog" role="dialog" aria-modal="true" aria-labelledby="editor-title">
<header class="dialog-header">
<div>
<strong id="editor-title">{{ isNew ? '新增文档模板' : '编辑模板版本' }}</strong>
<p>
{{
isNew
? '创建模板并同时保存首个版本。'
: '编辑操作会创建新的文书版本,已发布版本不会被直接覆盖。'
}}
</p>
</div>
<button type="button" class="close-button" aria-label="关闭" @click="emit('close')">
×
</button>
</header>
<form class="dialog-body" @submit.prevent="submit">
<div class="form-grid">
<label>
<span>模板编号 <em>*</em></span>
<input
v-model="form.templateCode"
:disabled="!isNew"
placeholder="如:TPL-RAD-MRI-001"
/>
</label>
<label>
<span>模板名称 <em>*</em></span>
<input v-model="form.name" :disabled="!isNew" placeholder="请输入模板名称" />
</label>
<label>
<span>院区 <em>*</em></span>
<select v-model="form.campusId" :disabled="!isNew" @change="handleCampusChange">
<option value="" disabled>请选择院区</option>
<option v-for="campus in campuses" :key="campus.id" :value="campus.id">
{{ campus.name }}
</option>
</select>
</label>
<label>
<span>科室</span>
<select v-model="form.departmentId" :disabled="!isNew">
<option value="">全院通用</option>
<option
v-for="department in availableDepartments"
:key="department.id"
:value="department.id"
>
{{ department.name }}
</option>
</select>
</label>
<label>
<span>文档类别</span>
<input v-model="form.category" :disabled="!isNew" placeholder="如:知情同意书" />
</label>
<label>
<span>版本号</span>
<input v-model="form.versionNo" placeholder="如:V1.0,不填由服务端生成" />
</label>
</div>
<label class="full-field">
<span>模板说明</span>
<textarea
v-model="form.description"
:disabled="!isNew"
rows="2"
placeholder="请输入模板说明"
/>
</label>
<label class="full-field">
<span>模板 HTML <em>*</em></span>
<textarea
v-model="form.contentHtml"
rows="11"
placeholder="请输入服务端可渲染的 HTML 内容,不要包含脚本。"
/>
</label>
<p v-if="error.message" class="form-error">{{ error.message }}</p>
<p class="form-tip">
版本工作流由服务端校验权限和状态;此处不会将本地签名或患者数据写入模板内容。
</p>
</form>
<footer class="dialog-footer">
<div v-if="workflowActions.length" class="workflow-actions">
<span>当前版本:{{ template?.version }}</span>
<textarea
v-if="workflowActions.some((item) => item.action === 'reject')"
v-model="rejectComment.value"
rows="1"
placeholder="驳回原因(驳回时必填)"
/>
<button
v-for="item in workflowActions"
:key="item.action"
type="button"
class="footer-button"
:class="item.tone"
:disabled="saving || workflowSaving"
@click="runWorkflow(item.action)"
>
{{ item.label }}
</button>
</div>
<span class="footer-spacer" />
<button type="button" class="footer-button secondary" @click="emit('close')">取消</button>
<button type="button" class="footer-button primary" :disabled="saving" @click="submit">
{{ saving ? '保存中…' : isNew ? '创建模板' : '创建新版本' }}
</button>
</footer>
</section>
</div>
</Teleport>
</template>
<style scoped>
.dialog-mask {
position: fixed;
z-index: 30;
inset: 0;
display: flex;
padding: 16px;
overflow-y: auto;
background: rgb(9 40 52 / 45%);
}
.editor-dialog {
display: flex;
flex-direction: column;
width: 760px;
max-width: 96vw;
max-height: calc(100vh - 32px);
margin: auto;
overflow: hidden;
background: #fff;
border-radius: 14px;
box-shadow: var(--sh-modal);
}
.dialog-header {
display: flex;
gap: 12px;
align-items: center;
padding: 14px 18px;
border-bottom: 1px solid var(--line);
}
.dialog-header strong {
color: var(--ink);
font-size: 15px;
}
.dialog-header p {
margin: 4px 0 0;
color: var(--mut);
font-size: 12px;
}
.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);
}
.dialog-body {
min-height: 0;
padding: 16px 18px;
overflow-y: auto;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 14px;
}
.form-grid label,
.full-field {
display: flex;
flex-direction: column;
gap: 6px;
color: var(--mut);
font-size: 12px;
}
.full-field {
margin-top: 12px;
}
label span em {
color: var(--err);
font-style: normal;
}
input,
select,
textarea {
width: 100%;
padding: 8px 10px;
color: var(--ink);
font: inherit;
background: #fff;
border: 1px solid var(--line);
border-radius: 6px;
resize: vertical;
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--brand);
box-shadow: 0 0 0 3px rgb(14 110 140 / 10%);
}
input:disabled,
select:disabled,
textarea:disabled {
color: var(--mut);
background: #f5f8f9;
cursor: not-allowed;
}
.form-error {
margin: 12px 0 0;
color: var(--err);
font-size: 12px;
}
.form-tip {
margin: 12px 0 0;
color: var(--mut);
font-size: 11.5px;
line-height: 1.6;
}
.dialog-footer {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
padding: 10px 18px;
background: #f8fbfc;
border-top: 1px solid var(--line);
}
.workflow-actions {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
color: var(--mut);
font-size: 11.5px;
}
.workflow-actions textarea {
width: 170px;
min-height: 30px;
padding: 5px 8px;
}
.footer-spacer {
flex: 1;
}
.footer-button {
padding: 7px 13px;
font-size: 12px;
font-weight: 700;
border: 1px solid transparent;
border-radius: 7px;
}
.footer-button:disabled {
cursor: wait;
opacity: 0.55;
}
.footer-button.primary {
color: #fff;
background: var(--brand);
border-color: var(--brand);
}
.footer-button.secondary {
color: var(--brand);
background: #fff;
border-color: var(--brand);
}
.footer-button.danger {
color: var(--err);
background: var(--err-l);
border-color: rgb(194 73 73 / 25%);
}
.footer-button:hover:not(:disabled) {
filter: brightness(0.96);
}
@media (max-width: 680px) {
.form-grid {
grid-template-columns: 1fr;
}
.footer-spacer {
display: none;
}
}
</style>
@@ -0,0 +1,554 @@
<script setup lang="ts">
import '@wangeditor/editor/dist/css/style.css'
import { onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import type { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'
import type { CampusRecord, DepartmentRecord } from '@/api/management/types'
import type { DocumentTemplate, TemplateEditorForm } from '../types'
const props = defineProps<{
visible: boolean
/** null 表示新增文书 */
template: DocumentTemplate | null
campuses: CampusRecord[]
departments: DepartmentRecord[]
}>()
const emit = defineEmits<{
close: []
save: [form: TemplateEditorForm]
}>()
// Word 自维护方案:科室把文书从 Word/WPS 直接粘贴进来(保留表格与基本格式),
// 或上传 .docx 由 mammoth 转换;签署时由交互管线识别"勾选"表并转为签署域。
const editorRef = shallowRef<IDomEditor>()
const valueHtml = ref('<p><br></p>')
const editorMode = 'default'
const docxInput = ref<HTMLInputElement | null>(null)
const importing = ref(false)
const importMessage = ref('')
const toolbarConfig: Partial<IToolbarConfig> = {
excludeKeys: ['group-video', 'fullScreen'],
}
const editorConfig: Partial<IEditorConfig> = {
placeholder:
'将 Word / WPS 中的文书内容直接粘贴到此处(保留表格与基本格式),或点击上方「导入 .docx」自动转换。',
}
// 占位符在签署时由系统按患者与就诊信息自动填充;签署交互管线同时会识别"勾选"表格。
const placeholders = [
'{患者姓名}',
'{性别}',
'{年龄}',
'{就诊号}',
'{签署日期}',
'{患者签名}',
'{与患者关系}',
]
const form = reactive<TemplateEditorForm>({
templateCode: '',
name: '',
description: '',
campusId: props.campuses[0]?.id ?? '',
departmentId: '',
category: '知情同意书',
versionNo: 'v1.0.0',
contentHtml: '',
})
const availableDepartments = ref<DepartmentRecord[]>([])
function syncDepartmentOptions() {
availableDepartments.value = form.campusId
? props.departments.filter((department) => department.campusId === form.campusId)
: [...props.departments]
}
watch(
() => props.visible,
(visible) => {
if (!visible) {
return
}
const template = props.template
Object.assign(form, {
id: template?.id,
templateCode: template?.code ?? '',
name: template?.name ?? '',
description: template?.description ?? '',
campusId: template?.campusId ?? props.campuses[0]?.id ?? '',
departmentId: template?.departmentId ?? '',
category: template?.category ?? '知情同意书',
versionNo: template?.version ?? 'v1.0.0',
contentHtml: template?.contentHtml ?? '',
})
valueHtml.value = template?.contentHtml || '<p><br></p>'
syncDepartmentOptions()
importMessage.value = ''
},
{ immediate: true },
)
function handleCampusChange() {
if (!availableDepartments.value.some((department) => department.id === form.departmentId)) {
form.departmentId = ''
}
}
function handleCreated(editor: IDomEditor) {
editorRef.value = editor
}
function insertPlaceholder(text: string) {
// slate 文本节点:{ text };由编辑器在当前光标处插入。
editorRef.value?.insertNode({ text })
}
async function handleDocxChange(event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
if (!file) {
return
}
importing.value = true
importMessage.value = ''
try {
const arrayBuffer = await file.arrayBuffer()
const mammoth = await import('mammoth/mammoth.browser')
const result = await mammoth.convertToHtml({ arrayBuffer })
valueHtml.value = result.value || '<p><br></p>'
importMessage.value = result.messages.length
? `已导入,${result.messages.length} 条转换提示(部分 Word 特性可能未还原)`
: '导入成功'
} catch {
importMessage.value = '导入失败:文件无法解析,请确认为 .docx 格式'
} finally {
importing.value = false
input.value = ''
}
}
function save() {
const text = editorRef.value?.getText().trim()
if (!form.name.trim()) {
importMessage.value = '请填写文书名称'
return
}
if (!text) {
importMessage.value = '文书内容为空,请粘贴 Word 内容或导入 .docx'
return
}
form.contentHtml = valueHtml.value
emit('save', { ...form, contentHtml: valueHtml.value })
}
onBeforeUnmount(() => {
editorRef.value?.destroy()
})
</script>
<template>
<Teleport to="body">
<div
v-if="visible"
class="dialog-mask"
role="presentation"
@click.self="emit('close')"
>
<section
class="word-editor"
role="dialog"
aria-modal="true"
aria-labelledby="word-editor-title"
>
<header class="dialog-header">
<div class="dialog-title">
<h2 id="word-editor-title">{{ template ? '编辑 Word 文书' : '新增 Word 文书' }}</h2>
<span class="mode-badge">Word 自维护</span>
</div>
<button type="button" class="close-button" aria-label="关闭编辑" @click="emit('close')">
×
</button>
</header>
<div class="meta-grid">
<label class="meta-field">
<span>文书名称 *</span>
<input v-model="form.name" type="text" placeholder="如:XXX 科知情同意书" />
</label>
<label class="meta-field">
<span>文书编号</span>
<input v-model="form.templateCode" type="text" placeholder="如 TPL-XXX-001" />
</label>
<label class="meta-field">
<span>所属院区</span>
<select v-model="form.campusId" @change="handleCampusChange">
<option v-for="campus in campuses" :key="campus.id" :value="campus.id">
{{ campus.name }}
</option>
</select>
</label>
<label class="meta-field">
<span>所属科室</span>
<select v-model="form.departmentId">
<option value="">全院通用</option>
<option v-for="department in availableDepartments" :key="department.id" :value="department.id">
{{ department.name }}
</option>
</select>
</label>
<label class="meta-field">
<span>分类</span>
<input v-model="form.category" type="text" />
</label>
<label class="meta-field">
<span>版本号</span>
<input v-model="form.versionNo" type="text" />
</label>
</div>
<div class="meta-row">
<label class="meta-field grow">
<span>文书说明</span>
<input v-model="form.description" type="text" placeholder="显示在文书卡片上的简介" />
</label>
<div class="meta-field">
<span>插入签署占位符</span>
<div class="placeholder-row">
<button
v-for="item in placeholders"
:key="item"
type="button"
class="placeholder-chip"
@click="insertPlaceholder(item)"
>
{{ item }}
</button>
</div>
</div>
</div>
<div class="editor-shell">
<div class="docx-bar">
<button
type="button"
class="docx-button"
:disabled="importing"
@click="docxInput?.click()"
>
{{ importing ? '正在导入…' : '导入 .docx 文件' }}
</button>
<input
ref="docxInput"
type="file"
accept=".docx"
class="docx-input"
aria-label="选择 docx 文件"
@change="handleDocxChange"
/>
<span v-if="importMessage" class="docx-message">{{ importMessage }}</span>
<span v-else class="docx-hint">
建议从 Word/WPS 中全选复制文书后直接粘贴到下方编辑区;勾选列表请保持"勾选"列为第一列,签署时会自动转为可交互勾选。
</span>
</div>
<div class="editor-area">
<Toolbar
class="editor-toolbar"
:editor="editorRef"
:default-config="toolbarConfig"
:mode="editorMode"
/>
<Editor
v-model="valueHtml"
class="editor-content"
:default-config="editorConfig"
:mode="editorMode"
@on-created="handleCreated"
/>
</div>
</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" @click="save">保存文书</button>
</footer>
</section>
</div>
</Teleport>
</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%);
}
.word-editor {
display: flex;
flex-direction: column;
width: 1080px;
max-width: 96vw;
max-height: calc(100vh - 32px);
margin: auto;
overflow: hidden;
background: #fff;
border-radius: 14px;
box-shadow: var(--sh-modal);
}
.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;
color: var(--ink);
font-size: 15px;
}
.mode-badge {
padding: 3px 9px;
color: var(--brand);
font-size: 11px;
font-weight: 700;
background: var(--brand-l);
border-radius: 12px;
}
.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);
}
.meta-grid {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 10px;
padding: 12px 18px 0;
}
.meta-row {
display: flex;
gap: 14px;
align-items: flex-end;
padding: 10px 18px 0;
}
.meta-field {
display: block;
min-width: 0;
}
.meta-field.grow {
flex: 1;
}
.meta-field > span {
display: block;
margin-bottom: 4px;
color: var(--ink);
font-size: 11.5px;
font-weight: 700;
}
.meta-field input,
.meta-field select {
width: 100%;
padding: 6px 8px;
color: var(--ink);
font-size: 12.5px;
border: 1px solid var(--line);
border-radius: 6px;
box-sizing: border-box;
}
.meta-field input:focus,
.meta-field select:focus {
border-color: var(--brand);
outline: none;
}
.placeholder-row {
display: flex;
flex-wrap: wrap;
gap: 4px;
max-width: 420px;
}
.placeholder-chip {
padding: 3px 7px;
color: var(--brand);
font-size: 11px;
background: var(--brand-l);
border: none;
border-radius: 10px;
}
.placeholder-chip:hover {
filter: brightness(0.95);
}
.editor-shell {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
padding: 12px 18px;
}
.docx-bar {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
margin-bottom: 8px;
}
.docx-button {
padding: 6px 12px;
color: var(--brand);
font-size: 12px;
font-weight: 700;
background: #fff;
border: 1px solid var(--brand);
border-radius: 6px;
}
.docx-button:hover {
background: var(--brand-l);
}
.docx-button:disabled {
color: #aebbc0;
background: #f1f5f6;
border-color: #dce5e8;
cursor: not-allowed;
}
.docx-input {
display: none;
}
.docx-message {
color: var(--warn);
font-size: 12px;
}
.docx-hint {
color: var(--mut);
font-size: 12px;
}
.editor-area {
display: flex;
flex-direction: column;
flex: 1;
min-height: 380px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 8px;
}
.editor-toolbar {
border-bottom: 1px solid var(--line);
}
.editor-content {
flex: 1;
min-height: 340px;
overflow-y: auto;
}
.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: 52%;
margin: 0;
overflow: hidden;
color: var(--mut);
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.footer-button {
padding: 7px 13px;
font-size: 12px;
font-weight: 700;
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);
}
@media (max-width: 900px) {
.meta-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.meta-row {
flex-direction: column;
align-items: stretch;
}
}
</style>
@@ -1,467 +0,0 @@
// 文档库真实文书内容:按 2025 年在各科室采集的知情同意书原件 1:1 重建。
// {占位符}在发起签署时由系统按患者与就诊信息自动填充;勾选表格由签署端渲染为签署域。
interface GeneProjectItem {
name: string
price: string
chargeType: string
thirdParty: string
}
interface GeneConsentConfig {
title: string
intro: string[]
risks: string[]
acknowledgements: string[]
tableHeader: string
projects: GeneProjectItem[]
conclusion: string
}
const PAGE_STYLE = `
body {
margin: 0;
padding: 26px 32px 40px;
color: #1c2b33;
font-family: 'SimSun', 'Songti SC', 'STSong', serif;
font-size: 13.5px;
line-height: 1.95;
background: #fff;
}
h1 {
margin: 4px 0 14px;
font-size: 18px;
letter-spacing: 2px;
text-align: center;
}
.org-name {
margin: 0 0 2px;
font-size: 14.5px;
font-weight: 700;
text-align: center;
letter-spacing: 1px;
}
.patient-header {
margin: 0 0 4px;
font-size: 13px;
}
.header-note {
margin: 0 0 12px;
color: #6b7c85;
font-size: 12px;
}
.notice-box {
margin: 10px 0 14px;
padding: 8px 14px;
border: 1.5px solid #1c2b33;
font-weight: 700;
text-align: center;
}
.section-title {
margin: 16px 0 6px;
font-size: 14px;
font-weight: 700;
text-align: center;
}
.sub-title {
margin: 14px 0 4px;
font-size: 13.5px;
font-weight: 700;
}
p {
margin: 0 0 6px;
text-align: justify;
}
ol {
margin: 4px 0 10px;
padding-left: 26px;
}
li {
margin-bottom: 3px;
text-align: justify;
}
.em {
font-weight: 700;
}
.underline {
text-decoration: underline;
}
table {
width: 100%;
margin: 8px 0 14px;
border-collapse: collapse;
font-size: 12.5px;
line-height: 1.6;
}
th,
td {
padding: 5px 7px;
border: 1px solid #52646e;
text-align: center;
vertical-align: middle;
}
th {
font-weight: 700;
background: #f2f6f8;
}
td.item-name {
text-align: left;
}
.page-break {
margin: 26px 0;
border-top: 1.5px dashed #90a4ae;
}
.page-break-label {
margin: -14px 0 14px;
color: #6b7c85;
font-size: 11.5px;
text-align: center;
}
.signature-block {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px 32px;
margin-top: 18px;
padding-top: 10px;
}
.signature-block span {
border-bottom: 1px solid #1c2b33;
min-height: 22px;
display: inline-block;
min-width: 140px;
padding: 0 8px;
}
.signature-line {
white-space: nowrap;
}
.contact {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 6px;
margin-top: 18px;
font-size: 12.5px;
}
`
function wrapDocument(bodyHtml: string): string {
return `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8" /><style>${PAGE_STYLE}</style></head><body>${bodyHtml}</body></html>`
}
function patientHeader(visitNoLabel = '住院号/门诊号'): string {
return `
<p class="patient-header">患者姓名:<span>{患者姓名}</span>&#12288;性别:<span>{性别}</span>&#12288;年龄:<span>{年龄}</span>&#12288;${visitNoLabel}:<span>{就诊号}</span></p>
<p class="header-note">住院患者基本信息由电子病历系统以页眉形式自动带入,签署时自动填充。</p>
`
}
function buildProjectTable(headerCells: string[], projects: GeneProjectItem[]): string {
const rows = projects
.map(
(item, index) =>
`<tr><td>☐</td><td>${index + 1}</td><td class="item-name">${item.name}</td><td>${item.price}</td><td>${item.chargeType}</td><td>${item.thirdParty}</td></tr>`,
)
.join('')
return `<table><thead><tr><th style="width:44px">勾选</th><th>序号</th>${headerCells
.map((cell) => `<th>${cell}</th>`)
.join('')}</tr></thead><tbody>${rows}</tbody></table>`
}
function buildSelfPaidGeneConsent(config: GeneConsentConfig): string {
return wrapDocument(`
${patientHeader()}
<h1>${config.title}</h1>
${config.intro.map((paragraph) => `<p>${paragraph}</p>`).join('')}
<p class="sub-title">【局限性与潜在风险】</p>
<ol>${config.risks.map((risk) => `<li>${risk}</li>`).join('')}</ol>
<p class="sub-title">【受检者知情】</p>
<ol>${config.acknowledgements.map((item) => `<li>${item}</li>`).join('')}</ol>
<p>${config.tableHeader}</p>
${buildProjectTable(['检测项目', '金额(元/例)', '收费类别', '是否第三方合作'], config.projects)}
<p>${config.conclusion}</p>
<div class="signature-block">
<div class="signature-line">患者(或委托人/监护人)意见:<span></span></div>
<div class="signature-line">医师签名:<span></span></div>
<div class="signature-line">患者(或委托人/监护人)签字:<span>{患者签名}</span></div>
<div class="signature-line">日期:<span>{签署日期}</span></div>
<div class="signature-line">委托人/监护人与患者关系:<span>{与患者关系}</span></div>
</div>
`)
}
const infectionGeneConsent = buildSelfPaidGeneConsent({
title: '感染基因高通量测序自费项目知情同意书',
intro: [
'感染基因高通量测序是不依赖病原传统分离、无需预设可疑病原体,直接提取临床样本中全部微生物+宿主游离核酸,通过高通量二代测序批量读取所有核酸序列,和已知微生物参考数据库比对,一次性识别样本内细菌、真菌、病毒、寄生虫、人体微生态菌群等广谱病原检测技术,同时可以附带耐药基因、耐药突变位点、毒力基因分析。',
'检测结果有助于医生快速制定调整的治疗方案,缩短确诊时间,降低重症感染病死率,减少广谱抗菌药物使用,助力抗菌药物管控,填补传统检测难培养、未知病原的诊断空白。',
],
risks: [
'本次检测仅对该次送检样本负责,检测结果需结合具体的临床症状以及患者的个体情况,由临床医生提供临床处理及治疗方案。',
'受检样本报告时间为收到检测样本后 1-2 个工作日发报告。如遇检测失败需要进行重复实验可能导致报告周期延迟。奉贤院区因物流原因报告可能会延迟 12-24 小时。',
'受检者样本如没有严格按照送检要求提供,会对检测结果有不良影响;若分析结果提示质控不合格或怀疑标本污染等情况,需要配合重新采样复测。',
'受检样本的病原核酸检测结果受多重因素影响,阴性不能完全排除感染,仅仅作为排除线索,需要结合培养、影像、炎症指标综合判断。阳性无法明确区分污染、定植、感染,中枢、关节、胸腹水等无菌体液参考价值更大。耐药基因检测不能替代药物敏感性试验。',
'本检测所涉及个人身份有关的信息,将在法律规定的范围内确保保密性,本单位承诺不会透露任何与受检者身份相关的资料给第三方。',
],
acknowledgements: [
'我已经充分理解该知情同意书所列局限性与潜在风险。',
'我自愿本次基因检测,并承诺送检申请单提供的个人资料真实可靠。',
'我充分理解感染基因检测受送检样本的条件限制所造成的结果局限性。',
'我同意对我个人信息及资料的保护;并且同意在去掉所有个人信息后,检测数据可供医学研究,授权医院对检测涉及的样本进行处理。',
'该检测项目尚未纳入上海市医保目录,属于自费项目。',
],
tableHeader: '因疾病诊疗所需,我们拟为您检测以下感染基因自费检测项目(选项):',
projects: [
{ name: '病原微生物宏基因组检测', price: '3040', chargeType: '自费', thirdParty: '否' },
{ name: '多种病原微生物核酸测序', price: '1500', chargeType: '自费', thirdParty: '否' },
{ name: '消化道多种病原检测', price: '620', chargeType: '自费', thirdParty: '否' },
],
conclusion: '本人知晓上述所有内容,愿意进行感染基因检测(高通量测序法)。',
})
const tumorGeneConsent = buildSelfPaidGeneConsent({
title: '肿瘤基因高通量测序自费项目知情同意书',
intro: [
'肿瘤基因高通量测序采用二代测序技术对肿瘤基因的变异进行分子检测(肿瘤基因的变异包括突变/融合/重排/扩增)。通过采集受检者蜡块/组织切片/胸水/血液/DNA 等样本,针对采集到的样本对提取的 DNA 或 RNA 等遗传物质进行高通量测序,通过生物信息学分析受检者的测序数据,得出肿瘤基因的变异情况及异常频率。',
'检测结果有助于医生制定相应的治疗方案,从而实现肿瘤个体化治疗,更精准地为患者提供不同用药方案的分子检测依据,可能会有效提高临床的治疗效果,并可能使受检者拥有较好的生活质量。',
],
risks: [
'本检测仅对该次送检样本负责,检测结果需结合具体的临床症状以及患者的个体情况由临床医生提供临床处理及治疗方案。',
'受检样本报告时间为收到检测样本后 10 个工作日发报告。如遇检测失败需要进行重复实验可能导致报告周期延迟。',
'受检者样本如没有严格按照送检要求提供,会对检测结果有不良影响。',
'受检样本的 DNA/RNA 序列分析是为了说明检测基因的变异情况,如未检出特定基因的基因变异情况(即阴性结果)并不能排除其他未知基因或没检测的基因的关系。',
'本检测方法不适于检测染色体数目及结构异常及其他特殊类型基因突变等变异类型。',
'本检测所涉及个人身份有关的信息,将在法律规定的范围内确保保密性,本单位承诺不会透露任何与受检者身份相关的资料给第三方。',
],
acknowledgements: [
'我已经充分理解该知情同意书所列局限性与潜在风险。',
'我自愿本次基因检测,并承诺送检申请单提供的个人资料真实可靠。',
'我充分理解肿瘤基因检测受送检样本的条件限制所造成的结果局限性。',
'我同意对我个人信息及资料的保护;并且同意在去掉所有个人信息后,检测数据可供医学研究,授权医院对检测涉及的样本进行处理。',
'该检测项目尚未纳入上海市医保目录,属于自费项目。',
],
tableHeader: '因疾病诊疗所需,我们拟为您检测以下肿瘤基因检测项目(选项):',
projects: [
{ name: 'BRCA1/2', price: '3250', chargeType: '自费', thirdParty: '否' },
{ name: 'NGS(小 panel)', price: '5950', chargeType: '自费', thirdParty: '否' },
{ name: 'NGS(中 panel)', price: '7300', chargeType: '自费', thirdParty: '否' },
{ name: 'NGS(大 panel)', price: '9900', chargeType: '自费', thirdParty: '否' },
],
conclusion: '本人知晓上述所有内容,愿意进行肿瘤基因检测(高通量测序法)。',
})
// 对外合作或自费基因检测项目知情同意书:60 项检测项目目录(原件 2025.6.24 版)。
const cooperativeProjects: GeneProjectItem[] = [
{ name: 'HCV 亚型扩增', price: '600', chargeType: '医保', thirdParty: '是' },
{ name: 'HBV 病毒基因测序', price: '600', chargeType: '医保', thirdParty: '是' },
{ name: '乳汁 CMV-DNA 扩增', price: '80', chargeType: '医保', thirdParty: '是' },
{ name: 'BRCA 基因突变检测', price: '1800', chargeType: '医保', thirdParty: '是' },
{ name: '金域多种呼吸道病原体核酸检测', price: '960', chargeType: '医保', thirdParty: '是' },
{ name: '乙肝病毒 HBV 耐药突变检测', price: '600', chargeType: '医保', thirdParty: '是' },
{ name: 'Xpert 结核分枝杆菌', price: '450', chargeType: '医保', thirdParty: '是' },
{ name: '白血病残留病灶 PML-RARa', price: '450', chargeType: '医保', thirdParty: '是' },
{ name: '全套白血病基因筛查组合', price: '450', chargeType: '医保', thirdParty: '是' },
{ name: '白血病残留病灶 TEL-AML1', price: '450', chargeType: '医保', thirdParty: '是' },
{ name: '白血病残留病灶 BCR-ABL 190', price: '450', chargeType: '医保', thirdParty: '是' },
{ name: '白血病残留病灶 BCR-ABL 210', price: '450', chargeType: '医保', thirdParty: '是' },
{ name: '脆性 X 综合征基因片段', price: '550', chargeType: '医保', thirdParty: '是' },
{ name: 'JAK2/V617F', price: '450', chargeType: '医保', thirdParty: '是' },
{ name: 'WT1', price: '600', chargeType: '医保', thirdParty: '是' },
{ name: 'FLT3-ITD', price: '600', chargeType: '医保', thirdParty: '是' },
{ name: 'FIP1L1-PDGFRA', price: '450', chargeType: '医保', thirdParty: '是' },
{ name: 'NPM1', price: '600', chargeType: '医保', thirdParty: '是' },
{ name: '地中海贫血基因检测', price: '800', chargeType: '医保', thirdParty: '是' },
{ name: '肿瘤 BRCA1/2 基因', price: '3600', chargeType: '自费', thirdParty: '是' },
{ name: '多种病原微生物核酸', price: '1500', chargeType: '自费', thirdParty: '是' },
{ name: '全外测序-外周血(检验)', price: '3000', chargeType: '自费', thirdParty: '是' },
{ name: '消化道多种病原体检测', price: '620', chargeType: '自费', thirdParty: '是' },
{ name: '肠道菌群多样性检测', price: '1200', chargeType: '自费', thirdParty: '是' },
{ name: '病原微生物宏基因组检测', price: '3040', chargeType: '自费', thirdParty: '是' },
{ name: '胚胎植入前染色体非整倍体检测(PGT-A)', price: '3600', chargeType: '自费', thirdParty: '是' },
{ name: '胚胎植入前染色体结构变异检测(PGT-SR)', price: '4200', chargeType: '自费', thirdParty: '是' },
{ name: 'PGTM-ChromSwift® 单基因病分子遗传检测(预实验)-样本', price: '4200', chargeType: '自费', thirdParty: '是' },
{ name: 'PGTM-ChromSwift® 单基因病分子遗传检测(胚胎检测)', price: '4200', chargeType: '自费', thirdParty: '是' },
{ name: '200+ 种单基因病无创产前检测', price: '3600', chargeType: '自费', thirdParty: '是' },
{ name: '全因 3.0-胎儿染色体异常和 200+ 种单基因病无创产前检测', price: '4800', chargeType: '自费', thirdParty: '是' },
{ name: '单基因遗传病扩展性携带者筛查 10 种', price: '1800', chargeType: '自费', thirdParty: '是' },
{ name: '单基因遗传病扩展性携带者筛查 155 种', price: '2400', chargeType: '自费', thirdParty: '是' },
{ name: '地中海贫血基因检测(α+β)508 Plus', price: '600', chargeType: '自费', thirdParty: '是' },
{ name: '临床全外显子组检测-家系(VIP)', price: '12000', chargeType: '自费', thirdParty: '是' },
{ name: '全基因组检测-家系(VIP)', price: '12600', chargeType: '自费', thirdParty: '是' },
{ name: '康孕-染色体检测-1M', price: '1800', chargeType: '自费', thirdParty: '是' },
{ name: '康孕-染色体检测-100K', price: '2400', chargeType: '自费', thirdParty: '是' },
{ name: 'PMseq-DNA Pro 高敏局灶感染病原微生物高通量基因检测', price: '3600', chargeType: '自费', thirdParty: '是' },
{ name: 'PMseq-RNA 局灶感染病原微生物高通量基因检测', price: '1800', chargeType: '自费', thirdParty: '是' },
{ name: 'PMseq-DR 局灶感染病原微生物高通量基因检测', price: '3600', chargeType: '自费', thirdParty: '是' },
{ name: '数字定量 PCR 监测-现成位点', price: '600', chargeType: '自费', thirdParty: '是' },
{ name: '泛实体肿瘤基因检测(DNA+RNA Panel)', price: '12600', chargeType: '自费', thirdParty: '是' },
{ name: '数字定量 PCR 监测-定制位点', price: '1200', chargeType: '自费', thirdParty: '是' },
{ name: '脑肿瘤分子分型(甲基化版)', price: '6600', chargeType: '自费', thirdParty: '是' },
{ name: '实体瘤全基因芯片检测', price: '6600', chargeType: '自费', thirdParty: '是' },
{ name: '实体肿瘤全转录测序分析', price: '6600', chargeType: '自费', thirdParty: '是' },
{ name: '无创产前 DNA 监测', price: '1250', chargeType: '自费', thirdParty: '是' },
{ name: 'NIPT-plus', price: '3600', chargeType: '自费', thirdParty: '是' },
{ name: '携带者筛查-紫管', price: '2400', chargeType: '自费', thirdParty: '是' },
{ name: '携带者筛查(夫妻双方)', price: '3600', chargeType: '自费', thirdParty: '是' },
{ name: '脆性 X 综合征基因片段分析', price: '550', chargeType: '自费', thirdParty: '是' },
{ name: '线粒体检测(线粒体环基因全长测序)', price: '2400', chargeType: '自费', thirdParty: '是' },
{ name: 'BRCA1/2 基因突变检测', price: '3600', chargeType: '自费', thirdParty: '是' },
{ name: '泛癌种 NGS 检测', price: '16000', chargeType: '自费', thirdParty: '是' },
{ name: '肠癌 NGS 检测', price: '6400', chargeType: '自费', thirdParty: '是' },
{ name: '肺癌 NGS 检测', price: '9600', chargeType: '自费', thirdParty: '是' },
{ name: '组织细胞荧光定量聚合酶链反应检诊断(肺癌多基因突变检测)', price: '6400', chargeType: '自费', thirdParty: '' },
{ name: '组织细胞荧光定量聚合酶链反应检诊断(ALK/ROS1 基因检测)', price: '3200', chargeType: '自费', thirdParty: '' },
{ name: '组织细胞荧光定量聚合酶链反应检诊断(KRAS/NRAS 基因突变检测)', price: '1400', chargeType: '自费', thirdParty: '' },
]
// 各项目执行科室(按原件顺序分段:1-25 检验科、26-29 生殖医学中心、30-41 宫内儿科疾病诊治中心、
// 42-53 临床遗传中心、54-60 病理科),同段执行科室在表格中纵向合并展示。
const cooperativeExecutorGroups = [
{ count: 25, name: '医学检验科' },
{ count: 4, name: '生殖医学中心' },
{ count: 12, name: '宫内儿科疾病诊治中心' },
{ count: 12, name: '临床遗传中心' },
{ count: 7, name: '病理科' },
]
function buildCooperativeConsent(): string {
let projectCursor = 0
const bodyRows = cooperativeProjects.map((item, index) => {
let executorCell = ''
const group = cooperativeExecutorGroups[0]
if (group) {
if (projectCursor === 0) {
executorCell = `<td rowSpan="${group.count}">${group.name}</td>`
}
projectCursor += 1
if (projectCursor >= group.count) {
cooperativeExecutorGroups.shift()
projectCursor = 0
}
}
return `<tr><td>☐</td><td>${index + 1}</td><td class="item-name">${item.name}</td><td>${item.price}</td><td>${item.chargeType}</td><td>${item.thirdParty}</td>${executorCell}</tr>`
})
return wrapDocument(`
${patientHeader('住院号')}
<h1>对外合作或自费基因检测项目知情同意书</h1>
<p>尊敬的{患者姓名}:因疾病诊疗所需,我们拟为您检测以下检验项目(选项):</p>
<table>
<thead>
<tr>
<th style="width:40px">勾选</th>
<th style="width:40px">序号</th>
<th>基因检测项目</th>
<th style="width:70px">收费金额(元/例)</th>
<th style="width:56px">收费类别</th>
<th style="width:64px">是否对外合作</th>
<th style="width:110px">执行科室</th>
</tr>
</thead>
<tbody>${bodyRows}</tbody>
</table>
<p>以上项目或由我院同第三方医学检验机构合作检测,或为自费检测项目。检测结果的准确性、及时性、保密性和可追溯性由本院向您负责,特此向您进行告知。</p>
<div class="signature-block">
<div class="signature-line">告知医师签名:<span></span></div>
<div class="signature-line">签名日期:<span>{签署日期}</span></div>
<div class="signature-line">患方签名:<span>{患者签名}</span></div>
<div class="signature-line">与患者关系:<span>{与患者关系}</span></div>
<div class="signature-line">签名日期:<span>{签署日期}</span></div>
<div class="signature-line">患方意见:<span>同意 / 不同意(请书写)</span></div>
</div>
`)
}
function buildMriConsent(): string {
return wrapDocument(`
<p class="org-name">上海交通大学医学院附属新华医院医学影像科</p>
<h1>MRI 检查预约单暨知情同意书(此单背面)</h1>
${patientHeader('MRI 号')}
<p class="notice-box">请预先填好知情同意书(此单背面)</p>
<p>定于{预约日期}&#12288;时&#12288;&#12288;请到 1. <span class="em underline">医技楼一楼 MRI 吧台登记</span></p>
<p style="text-indent:6.5em">2. <span class="em underline">医疗保健综合楼 B1 护士站登记</span></p>
<p style="text-indent:6.5em">3. <span class="em underline">儿科综合楼 3 楼窗口登记</span></p>
<p class="section-title">病人检查准备及注意事项</p>
<ol>
<li><span class="em">安装心脏起搏器、心脏瓣膜术后、早期妊娠及动脉夹闭术后的病人是不能进行 MRI 检查的</span>,如有上述任何情况必须及时说明;体内有其他金属植入物者,请说明是何种材料,只有 MRI 相容的安全性材料才能进行 MRI 检查。</li>
<li><span class="em">金属物品可能导致危及医务人员人身伤害、磁共振机器故障、金属制品本身损坏等,故</span><span class="em underline">严禁携带</span>如:钥匙、硬币、磁卡、手机、手表、助听器、活动假牙、发夹、金属带扣、眼镜、金银首饰(项链、耳环、手镯、手链、戒指等)、玉石挂件、玉镯子等各种含有金属的物件进入检查机房,若违犯此条例而产生不良后果,将严厉追究相关人员责任。由此产生的电子器件损坏,责任自负。</li>
<li><span class="em underline">纹身</span>在检查过程中可能导致局部皮肤严重灼伤,请有纹身患者及时告知。</li>
<li>检查当日请带磁卡,病人请勿携带贵重物品,请勿穿戴附有任何金属物质的衣物或饰品。</li>
<li>★ 年幼老龄以及需 <span class="em">MRI 增强的患者,须有家属陪同</span>。</li>
<li><span class="em underline">检查前请提前 15 分钟到场(包括门诊及住院病人)。凡预约后不能按时前来者,需提前一天到预约中心办理更改手续,另行预约检查时间。</span></li>
<li>MRI 检查用时较长,请根据预约登记的时间,依次排队等候检查,请耐心配合。</li>
<li><span class="em underline">检查完成后,请在隔二个工作日后(不含检查当日)至医技楼一楼 3 号窗口或自助机取正式报告;周末及节假日休息,取报告时间顺延。</span></li>
</ol>
<p class="notice-box">请按时领取报告,逾期未取者,相关责任/后果将自行承担。</p>
<p class="section-title">婴幼儿患者检查特殊要求</p>
<ol>
<li><span class="em underline">婴幼儿 MRI 检查请于检查当日根据预约时间提前 15 分钟办理登记。</span></li>
<li><span class="em underline">患儿检查前一晚 22:00 以后睡觉,检查当天早晨 6:00 前起床。(检查前不可再让患儿睡觉,因为如果睡眠太充分会影响镇静剂效果!!!)</span></li>
<li><span class="em">患儿登记检查前请禁食、禁水 4 小时</span>,检查当日请携带小毛毯(检查机房内为患儿保暖用)。</li>
<li><span class="em">患儿家属请事先配好镇静剂,并在检查当天带来,交予磁共振成像(MRI)吧台。</span></li>
<li>MRI 检查时间较长,一般需要 10~20 分钟左右,若患儿不合作或者未熟睡则会影响到图像质量。<span class="em">所以要保证患儿熟睡状态才能进入机房进行检查。</span></li>
<li>患儿检查时,身上不能穿带有金属扣的衣物,<span class="em">并且需要一名家属在旁陪护</span>,以防患儿惊醒摔倒。陪护家属也须取走身上的金属物件如钥匙、硬币、磁卡、手机、手表以及发夹等。</li>
<li><span class="em">患儿家属若曾接受心脏起搏器、脑部动脉瘤夹、心脏人工瓣膜、人工电子耳等手术,眼眶内有金属异物,或体内有其金属含铁磁性物质装置装置,</span><span class="em underline">严禁进入磁共振扫描室</span>。</li>
<li><span class="em underline">如患儿有感冒、咳嗽、发热症状,不能使用镇静剂</span>,请提前到预约中心办理更改手续,另行预约检查时间。</li>
</ol>
<div class="contact">
<span>电话:19 号楼 2507 7049 / 28 号楼 2507 7065</span>
<span>预约中心电话:2507 6213 / 2507 6217</span>
<span>新华医院放射科 地址:控江路 1665 号医技楼一楼(电话:2507 7053〔5 号楼〕)</span>
</div>
<div class="page-break"></div>
<p class="page-break-label">以 下 为 单 张 背 面 内 容</p>
<h1>磁共振成像(MRI)检查知情同意书</h1>
<ol>
<li>MRI 为非侵袭性、无辐射损害的检查,所使用的对比剂安全性很高,是目前较先进的优质的影像检查技术。</li>
<li>MRI 增强用对比剂是一种顺磁性对比剂,不良反应极少。个别患者有过敏、喉头水肿、休克等反应,如有严重肾损害、哮喘等及其它过敏史的患者请检查前及时告知检查技师。</li>
<li>如有以下情形,请告知医护人员,再确定是否可以做磁共振成像检查:<span class="em underline">曾接受心脏起搏器、脑部动脉瘤夹、心脏人工瓣膜、人工电子耳等手术,眼眶内有金属异物,或体内有其他含铁磁性物质的装置</span>。</li>
<li>为使检查安全正确,请于检查之前,如实填写以下表格。</li>
</ol>
<table>
<tbody>
<tr>
<td style="width:88px">姓名</td><td>{患者姓名}</td>
<td style="width:56px">性别</td><td>{性别}</td>
<td style="width:56px">年龄</td><td>{年龄}</td>
<td style="width:70px">体重(Kg)</td><td></td>
</tr>
<tr><td>手机号</td><td>{联系电话}</td><td>预约日期</td><td colSpan="5">{预约日期}</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>您是否曾接受下列手术:</th><th>您是否有下列情况:</th></tr>
</thead>
<tbody>
<tr><td class="item-name">1. 安装人工心脏起搏器&#12288;&#12288;☐ 否&#12288;☐ 是</td><td class="item-name">1. 有任何药物过敏记录&#12288;&#12288;☐ 否&#12288;☐ 是</td></tr>
<tr><td class="item-name">2. 安装人工心脏瓣膜&#12288;&#12288;&#12288;☐ 否&#12288;☐ 是</td><td class="item-name">2. 是否有铁屑溅入眼球&#12288;&#12288;☐ 否&#12288;☐ 是</td></tr>
<tr><td class="item-name">3. 植入金属节育器&#12288;&#12288;&#12288;&#12288;☐ 否&#12288;☐ 是</td><td class="item-name">3. 活动性假牙或金属牙套&#12288;☐ 否&#12288;☐ 是</td></tr>
<tr><td class="item-name">4. 化疗泵/胰岛素泵&#12288;&#12288;&#12288; ☐ 否&#12288;☐ 是</td><td class="item-name">4. 肾功能不全或慢性肾病&#12288;☐ 否&#12288;☐ 是</td></tr>
<tr><td class="item-name">5. 脑内动脉瘤手术&#12288;&#12288;&#12288;&#12288;☐ 否&#12288;☐ 是</td><td class="item-name">您是否有下列情况(女性):</td></tr>
<tr><td class="item-name">6. 体内血管夹/内支架&#12288;&#12288; ☐ 否&#12288;☐ 是</td><td class="item-name">1. 怀孕&#12288;&#12288;☐ 否&#12288;☐ 是-孕周:</td></tr>
<tr><td class="item-name">7. 胃肠镜后不锈钢夹植入史 ☐ 否&#12288;☐ 是</td><td class="item-name">2. 末次月经时间:<br />一个月经期时间:</td></tr>
<tr><td class="item-name">8. 电子耳蜗/助听器&#12288;&#12288;&#12288; ☐ 否&#12288;☐ 是</td><td class="item-name">本人完全理解检查方案和风险,愿意承担各项风险带来的后果,并配合医护人员共同完成检查。</td></tr>
<tr><td class="item-name">9. 体内装置金属内固定器&#12288; ☐ 否&#12288;☐ 是</td><td class="item-name">☐ 是&#12288;&#12288;☐ 否</td></tr>
<tr><td class="item-name">10. 人工关节及假体&#12288;&#12288;&#12288;&#12288;☐ 否&#12288;☐ 是</td><td class="item-name">请患者(或家属)签名:<span>{患者签名}</span></td></tr>
<tr><td class="item-name">11. 纹身&#12288;&#12288;&#12288;&#12288;&#12288;&#12288;&#12288;&#12288;&#12288;☐ 否&#12288;☐ 是</td><td class="item-name">日期:<span>{签署日期}</span></td></tr>
<tr><td class="item-name">12. 其他特殊说明:</td><td></td></tr>
</tbody>
</table>
`)
}
export const documentContentHtml: Record<string, string> = {
'doc-img-mri-001': buildMriConsent(),
'doc-lab-infection-gene-001': infectionGeneConsent,
'doc-lab-coop-gene-001': buildCooperativeConsent(),
'doc-gen-onco-gene-001': tumorGeneConsent,
}
@@ -10,12 +10,10 @@ import DocumentFilterTabs from './components/DocumentFilterTabs.vue'
import DocumentLibraryHeader from './components/DocumentLibraryHeader.vue'
import DocumentPreviewDialog from './components/DocumentPreviewDialog.vue'
import DocumentTemplateCard from './components/DocumentTemplateCard.vue'
import TemplateEditorDialog from './components/TemplateEditorDialog.vue'
import WordEditorDialog from './components/WordEditorDialog.vue'
import { documentLibrary, documentStatusOptions } from './mock'
import type { TemplateVersionAction } from './types'
import type {
DepartmentTab,
DocumentStatus,
DocumentDepartment,
DocumentFilterForm,
DocumentTemplate,
@@ -29,11 +27,9 @@ import type {
const library = ref<DocumentDepartment[]>([])
const selectedTemplate = ref<DocumentTemplate | null>(null)
const signingTemplate = ref<SigningTemplate | null>(null)
const editorTemplate = ref<DocumentTemplate | null>(null)
const wordEditingTemplate = ref<DocumentTemplate | null>(null)
const wordEditorVisible = ref(false)
const signingDialogVisible = ref(false)
const editorVisible = ref(false)
const editorSaving = ref(false)
const workflowSaving = ref(false)
const campuses: CampusRecord[] = [
{ id: 'campus-main', code: 'MAIN', name: '本部院区', status: 'ENABLED', sortNo: 1 },
@@ -124,8 +120,9 @@ function closePreview() {
}
function showAddMessage() {
editorTemplate.value = null
editorVisible.value = true
// Word 自维护方案:新增文书直接进入 Word 编辑器(粘贴/导入 .docx)。
wordEditingTemplate.value = null
wordEditorVisible.value = true
}
function showExportMessage() {
@@ -133,13 +130,14 @@ function showExportMessage() {
}
function showEditMessage(template: DocumentTemplate) {
editorTemplate.value = template
editorVisible.value = true
// Word 自维护方案:所有文书统一在 Word 编辑器中维护。
wordEditingTemplate.value = template
wordEditorVisible.value = true
}
function closeEditor() {
editorVisible.value = false
editorTemplate.value = null
function closeWordEditor() {
wordEditorVisible.value = false
wordEditingTemplate.value = null
}
function addLocalTemplate(form: TemplateEditorForm) {
@@ -181,73 +179,29 @@ function addLocalTemplate(form: TemplateEditorForm) {
}
function saveTemplate(form: TemplateEditorForm) {
editorSaving.value = true
try {
if (form.id) {
library.value.forEach((department) =>
department.categories.forEach((category) =>
category.documents.forEach((document) => {
if (document.id === form.id) {
document.version = form.versionNo || document.version
document.contentHtml = form.contentHtml
document.description = form.description
}
}),
),
)
} else {
addLocalTemplate(form)
}
closeEditor()
ElMessage.success(form.id ? '模板新版本已保存' : '模板已创建')
} catch {
ElMessage.error(form.id ? '模板版本保存失败,请稍后重试' : '模板创建失败,请稍后重试')
} finally {
editorSaving.value = false
}
}
function updateLocalTemplateStatus(templateId: string, action: TemplateVersionAction) {
const nextStatus: DocumentStatus =
action === 'submit-review' || action === 'reject'
? 'review'
: action === 'disable' || action === 'archive'
? 'archived'
: 'enabled'
library.value.forEach((department) =>
department.categories.forEach((category) =>
category.documents.forEach((document) => {
if (document.id === templateId) {
document.status = nextStatus
}
}),
),
)
}
function handleWorkflow(action: TemplateVersionAction) {
const template = editorTemplate.value
const versionId = template?.versionId
if (!template || !versionId) {
ElMessage.error('当前模板没有可操作的版本')
return
if (form.id) {
library.value.forEach((department) =>
department.categories.forEach((category) =>
category.documents.forEach((document) => {
if (document.id === form.id) {
document.name = form.name || document.name
document.version = form.versionNo || document.version
document.contentHtml = form.contentHtml
document.description = form.description
}
}),
),
)
} else {
addLocalTemplate(form)
}
workflowSaving.value = true
try {
updateLocalTemplateStatus(template.id, action)
closeEditor()
ElMessage.success('模板版本状态已更新')
} finally {
workflowSaving.value = false
}
closeWordEditor()
ElMessage.success(form.id ? '文书已更新' : '文书已创建')
}
// 版本工作流(提交审核/发布等)随原模板编辑器移除,待签署端接入后按后端工作流接口恢复。
function toSigningTemplate(template: DocumentTemplate): SigningTemplate {
return {
id: template.id,
@@ -340,16 +294,13 @@ onMounted(loadLibrary)
:initial-template="signingTemplate"
@close="closeSigningDialog"
/>
<TemplateEditorDialog
:visible="editorVisible"
:template="editorTemplate"
<WordEditorDialog
:visible="wordEditorVisible"
:template="wordEditingTemplate"
:campuses="campuses"
:departments="departments"
:saving="editorSaving"
:workflow-saving="workflowSaving"
@close="closeEditor"
@close="wordEditorVisible = false"
@save="saveTemplate"
@workflow="handleWorkflow"
/>
</section>
</template>
@@ -1,6 +1,6 @@
import type { DocumentDepartment, DocumentStatus, LibraryDocument } from './types'
import { documentContentHtml } from './contents'
import { presetDocumentContentHtml } from './preset-contents'
const documentStatusLabels: Record<DocumentStatus, string> = {
enabled: '启用中',
@@ -31,12 +31,12 @@ function createDocument(
version,
description,
status,
contentHtml: documentContentHtml[id],
contentHtml: presetDocumentContentHtml[id],
}
}
// 文书内容以 2025 年在各科室采集的真实知情同意书原件为准(见 ./contents.ts)。
// 各科室版式差异较大:勾选项目表、安全筛查问卷、多签署人等,均为真实业务结构。
// 预置文书为 2025 年在各科室采集的真实知情同意书原件(HTML 已固化在 preset-contents.ts),
// 也可在 Word 编辑器中继续维护;科室自维护的新文书通过 Word 编辑器录入。
export const documentLibrary: DocumentDepartment[] = [
{
key: 'medical-imaging',
File diff suppressed because one or more lines are too long
@@ -1,14 +1,5 @@
export type DocumentStatus = 'enabled' | 'draft' | 'archived' | 'review'
// 模板版本工作流动作。原型阶段在页面内本地消化;接入后端后与文档模板版本状态接口对齐。
export type TemplateVersionAction =
| 'submit-review'
| 'reject'
| 'approve'
| 'publish'
| 'disable'
| 'archive'
export type DocumentFilterStatus = 'all' | DocumentStatus
export type DocumentCampus = 'all' | '本部院区' | '东院区' | '西院区'
+2 -1
View File
@@ -5,7 +5,8 @@
"types": ["vite/client"],
"allowArbitraryExtensions": true,
"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"@wangeditor/editor-for-vue": ["./node_modules/@wangeditor/editor-for-vue/dist/src/index.d.ts"]
},
/* Linting */