build(deps): 新增 CKEditor 与 TinyMCE 依赖

This commit is contained in:
yelan
2026-09-11 15:28:18 +08:00
parent 23d85cf8ce
commit 7035c6f378
13 changed files with 3165 additions and 184 deletions
+2589
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -13,12 +13,16 @@
"format:check": "prettier . --check"
},
"dependencies": {
"@ckeditor/ckeditor5-vue": "^8.2.0",
"@tinymce/tinymce-vue": "^6.3.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.20.0",
"ckeditor5": "^48.5.0",
"element-plus": "^2.14.5",
"mammoth": "^1.12.2",
"pinia": "^4.0.3",
"tinymce": "^8.9.1",
"vue": "^3.5.41",
"vue-router": "^5.2.0"
},
@@ -1,16 +1,16 @@
<script setup lang="ts">
import type { DepartmentTab, DocumentStatus, StatusTab } from '../types'
import type { DepartmentTab, DocumentFilterStatus, StatusTab } from '../types'
defineProps<{
department: string | null
status: DocumentStatus
status: DocumentFilterStatus
departments: DepartmentTab[]
statuses: StatusTab[]
}>()
const emit = defineEmits<{
'update:department': [value: string | null]
'update:status': [value: DocumentStatus]
'update:status': [value: DocumentFilterStatus]
}>()
</script>
@@ -23,6 +23,47 @@ const statusLabel = computed(() => {
return documentStatusLabels[props.template.status]
})
// 预置文书的 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;
}
img {
max-width: 100%;
}
table {
width: 100%;
margin: 8px 0;
border-collapse: collapse;
font-size: 12.5px;
line-height: 1.6;
}
th,
td {
padding: 5px 7px;
border: 1px solid #52646e;
vertical-align: middle;
}
`
const previewSrcDoc = computed(() => {
const contentHtml = props.template?.contentHtml ?? ''
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>`
})
</script>
<template>
@@ -58,7 +99,7 @@ const statusLabel = computed(() => {
<iframe
v-else-if="template.contentHtml"
class="preview-frame"
:srcdoc="template.contentHtml"
:srcdoc="previewSrcDoc"
sandbox=""
title="文档模板内容预览"
/>
@@ -12,6 +12,7 @@ const emit = defineEmits<{
preview: [template: DocumentTemplate]
edit: [template: DocumentTemplate]
initiate: [template: DocumentTemplate]
remove: [template: DocumentTemplate]
}>()
const statusLabel = computed(() => documentStatusLabels[props.template.status])
@@ -49,6 +50,14 @@ const statusLabel = computed(() => documentStatusLabels[props.template.status])
>
发起
</button>
<button
type="button"
class="card-button danger"
title="删除此文书"
@click="emit('remove', template)"
>
删除
</button>
</div>
</div>
</article>
@@ -224,6 +233,15 @@ const statusLabel = computed(() => documentStatusLabels[props.template.status])
background: #f1f5f6;
}
.card-button.danger {
color: #b3442f;
border-color: #cf7c68;
}
.card-button.danger:hover {
background: #fbeee9;
}
@media (max-width: 680px) {
.template-footer {
align-items: flex-start;
@@ -1,12 +1,11 @@
<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 { reactive, ref, watch } from 'vue'
import type { CampusRecord, DepartmentRecord } from '@/api/management/types'
import EditorCkPane from './editors/EditorCkPane.vue'
import EditorTinyPane from './editors/EditorTinyPane.vue'
import EditorWangPane from './editors/EditorWangPane.vue'
import type { DocumentTemplate, TemplateEditorForm } from '../types'
const props = defineProps<{
@@ -24,21 +23,45 @@ const emit = defineEmits<{
// Word 自维护方案:科室把文书从 Word/WPS 直接粘贴进来(保留表格与基本格式),
// 或上传 .docx 由 mammoth 转换;签署时由交互管线识别"勾选"表并转为签署域。
const editorRef = shallowRef<IDomEditor>()
// 富文本引擎做成可切换的三套(对比 Word 粘贴保真度),存储统一为编辑器产出的 HTML。
type EditorEngine = 'wangeditor' | 'ckeditor5' | 'tinymce'
interface EditorPaneHandle {
getText: () => string
insertText: (text: string) => void
}
const engineComponents = {
wangeditor: EditorWangPane,
ckeditor5: EditorCkPane,
tinymce: EditorTinyPane,
}
const engineOptions: Array<{ key: EditorEngine; label: string; note: string }> = [
{
key: 'wangeditor',
label: 'WangEditor',
note: '已接管粘贴,尽量保留内联样式(MIT)',
},
{
key: 'ckeditor5',
label: 'CKEditor 5',
note: 'PasteFromOffice 插件,Word 粘贴保真最强(GPL2)',
},
{
key: 'tinymce',
label: 'TinyMCE',
note: '均衡之选(MIT)',
},
]
const engine = ref<EditorEngine>('wangeditor')
const engineNote = ref(engineOptions[0]?.note ?? '')
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 paneRef = ref<EditorPaneHandle | null>(null)
// 占位符在签署时由系统按患者与就诊信息自动填充;签署交互管线同时会识别"勾选"表格。
const placeholders = [
@@ -102,13 +125,13 @@ function handleCampusChange() {
}
}
function handleCreated(editor: IDomEditor) {
editorRef.value = editor
function selectEngine(next: EditorEngine) {
engine.value = next
engineNote.value = engineOptions.find((option) => option.key === next)?.note ?? ''
}
function insertPlaceholder(text: string) {
// slate 文本节点:{ text };由编辑器在当前光标处插入。
editorRef.value?.insertNode({ text })
paneRef.value?.insertText(text)
}
async function handleDocxChange(event: Event) {
@@ -139,7 +162,7 @@ async function handleDocxChange(event: Event) {
}
function save() {
const text = editorRef.value?.getText().trim()
const text = paneRef.value?.getText() ?? ''
if (!form.name.trim()) {
importMessage.value = '请填写文书名称'
@@ -154,10 +177,6 @@ function save() {
form.contentHtml = valueHtml.value
emit('save', { ...form, contentHtml: valueHtml.value })
}
onBeforeUnmount(() => {
editorRef.value?.destroy()
})
</script>
<template>
@@ -242,6 +261,20 @@ onBeforeUnmount(() => {
</div>
<div class="editor-shell">
<div class="engine-bar">
<span class="engine-label">编辑器引擎</span>
<button
v-for="option in engineOptions"
:key="option.key"
type="button"
class="engine-button"
:class="{ active: engine === option.key }"
@click="selectEngine(option.key)"
>
{{ option.label }}
</button>
<span class="engine-note">{{ engineNote }}</span>
</div>
<div class="docx-bar">
<button
type="button"
@@ -265,18 +298,12 @@ onBeforeUnmount(() => {
</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"
<component
:is="engineComponents[engine]"
:key="engine"
ref="paneRef"
:model-value="valueHtml"
@update:model-value="valueHtml = $event"
/>
</div>
</div>
@@ -478,24 +505,52 @@ onBeforeUnmount(() => {
font-size: 12px;
}
.engine-bar {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
margin-bottom: 8px;
}
.engine-label {
color: var(--ink);
font-size: 12px;
font-weight: 700;
}
.engine-button {
padding: 5px 12px;
color: var(--mut);
font-size: 12px;
font-weight: 700;
background: #fff;
border: 1px solid var(--line);
border-radius: 14px;
}
.engine-button:hover {
border-color: var(--brand);
color: var(--brand);
}
.engine-button.active {
color: #fff;
background: var(--brand);
border-color: var(--brand);
}
.engine-note {
color: var(--mut);
font-size: 11.5px;
}
.editor-area {
display: flex;
flex-direction: column;
flex: 1;
min-height: 380px;
min-height: 420px;
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 {
@@ -0,0 +1,149 @@
<script setup lang="ts">
import { shallowRef } from 'vue'
import { Ckeditor } from '@ckeditor/ckeditor5-vue'
import {
Alignment,
Bold,
ClassicEditor,
Essentials,
FontColor,
FontSize,
GeneralHtmlSupport,
Heading,
Italic,
PasteFromOffice,
Paragraph,
Table,
TableToolbar,
Underline,
} from 'ckeditor5'
import type { Editor } from 'ckeditor5'
import 'ckeditor5/ckeditor5.css'
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const editorRef = shallowRef<Editor>()
// PasteFromOffice + GeneralHtmlSupport:CKEditor 的 Word 粘贴保真核心,
// 开源 GPL 构建即包含(对应 config 中 licenseKey: 'GPL')。
const config = {
licenseKey: 'GPL',
placeholder: '将 Word / WPS 中的文书内容直接粘贴到此处(PasteFromOffice 插件处理 Word 粘贴)。',
plugins: [
Essentials,
Paragraph,
Heading,
Bold,
Italic,
Underline,
Alignment,
FontSize,
FontColor,
Table,
TableToolbar,
GeneralHtmlSupport,
PasteFromOffice,
],
toolbar: [
'heading',
'|',
'bold',
'italic',
'underline',
'|',
'fontSize',
'fontColor',
'|',
'alignment:left',
'alignment:center',
'alignment:right',
'alignment:justify',
'|',
'bulletedList',
'numberedList',
'|',
'insertTable',
'undo',
'redo',
],
heading: {
options: [
{ model: 'paragraph' as const, title: '正文', class: 'ck-heading_paragraph' },
{ model: 'heading1' as const, view: 'h1', title: '标题 1', class: 'ck-heading_heading1' },
{ model: 'heading2' as const, view: 'h2', title: '标题 2', class: 'ck-heading_heading2' },
{ model: 'heading3' as const, view: 'h3', title: '标题 3', class: 'ck-heading_heading3' },
],
},
// 尽量保留粘贴内容中的内联样式与 class(Word 居中等视觉信息)。
htmlSupport: {
allow: [{ name: /[\s\S]+/, classes: /[\s\S]+/, styles: /[\s\S]+/, attributes: /[\s\S]+/ }],
},
table: {
contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells'],
},
}
function onReady(editor: Editor) {
editorRef.value = editor
}
function getText(): string {
const html = editorRef.value?.getData() ?? ''
return html
.replace(/<[^>]*>/g, ' ')
.replace(/&nbsp;/g, ' ')
.trim()
}
function insertText(text: string) {
const editor = editorRef.value
if (!editor) {
return
}
editor.model.change((writer) => {
const focus = editor.model.document.selection.focus
if (focus) {
writer.insertText(text, focus)
}
})
}
defineExpose({ getText, insertText })
</script>
<template>
<div class="ck-pane">
<Ckeditor
class="pane-editor"
:editor="ClassicEditor"
:model-value="props.modelValue"
:config="config"
@update:model-value="emit('update:modelValue', String($event))"
@ready="onReady"
/>
</div>
</template>
<style scoped>
.ck-pane {
height: 100%;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 8px;
}
.ck-pane :deep(.ck-editor__editable_inline) {
min-height: 380px;
max-height: 460px;
}
</style>
@@ -0,0 +1,82 @@
<script setup lang="ts">
import { shallowRef } from 'vue'
// 副作用导入:注册全局 window.tinymce(@tinymce/tinymce-vue 会检测全局实例,从而走本地包而非 CDN)。
import 'tinymce/tinymce'
import 'tinymce/themes/silver'
import 'tinymce/icons/default/icons'
import 'tinymce/models/dom'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/table'
import 'tinymce/skins/ui/oxide/skin.min.css'
import TinyEditor from '@tinymce/tinymce-vue'
// TinyMCE 8(GPL 开源模式需声明 license_key: 'gpl')。
// skin: false / content_css: false —— 皮肤样式由打包引入,iframe 内容样式用 content_style 内联。
const editorRef = shallowRef<TinyMceApi | null>(null)
interface TinyMceApi {
getContent: (options?: { format?: string }) => string
setContent: (content: string) => void
insertContent: (content: string) => void
}
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const init = {
license_key: 'gpl',
menubar: false,
placeholder: '将 Word / WPS 中的文书内容直接粘贴到此处(保留表格与基本格式)。',
plugins: 'lists table paste',
toolbar:
'undo redo | bold italic underline | alignleft aligncenter alignright alignjustify | fontsizeselect forecolor backcolor | bullist numlist | table',
skin: false,
content_css: false,
content_style: `
body { font-family: 'SimSun', 'Songti SC', serif; font-size: 14px; line-height: 1.8; color: #1c2b33; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #52646e; padding: 5px 7px; }
`,
paste_data_images: true,
height: 460,
}
function onInit(_event: unknown, editor: TinyMceApi) {
editorRef.value = editor
}
function getText(): string {
return editorRef.value?.getContent({ format: 'text' }).trim() ?? ''
}
function insertText(text: string) {
editorRef.value?.insertContent(text)
}
defineExpose({ getText, insertText })
</script>
<template>
<div class="tiny-pane">
<TinyEditor
:model-value="props.modelValue"
:init="init"
@update:model-value="emit('update:modelValue', $event)"
@init="onInit"
/>
</div>
</template>
<style scoped>
.tiny-pane {
overflow: hidden;
border: 1px solid var(--line);
border-radius: 8px;
}
</style>
@@ -0,0 +1,107 @@
<script setup lang="ts">
import '@wangeditor/editor/dist/css/style.css'
import { onBeforeUnmount, shallowRef } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import type { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const editorRef = shallowRef<IDomEditor>()
const editorMode = 'default'
// 剪贴板富 HTML 的轻量清理:去掉脚本/样式表/条件注释(WangEditor 默认粘贴会过滤掉
// 大量内联样式导致"粘贴变形",这里接管粘贴以尽量保留 Word 原样式)。
function cleanClipboardHtml(html: string): string {
return html
.replace(/<!--[\s\S]*?-->/g, '')
.replace(/<(script|style|meta|link)\b[^>]*>[\s\S]*?<\/\1>/gi, '')
.replace(/<(script|meta|link)\b[^>]*\/?>/gi, '')
}
const toolbarConfig: Partial<IToolbarConfig> = {
excludeKeys: ['group-video', 'fullScreen'],
}
const editorConfig: Partial<IEditorConfig> = {
placeholder: '将 Word / WPS 中的文书内容直接粘贴到此处(保留表格与基本格式)。',
// 接管粘贴:优先使用剪贴板中的富 HTML 原样插入(保留居中、加粗、表格等内联样式);
// 纯文本粘贴(无富 HTML)时返回 true 走默认处理。
customPaste: (editor, event) => {
const html = event.clipboardData?.getData('text/html')
if (html) {
editor.dangerouslyInsertHtml(cleanClipboardHtml(html))
return false
}
return true
},
}
function handleCreated(editor: IDomEditor) {
editorRef.value = editor
}
function getText(): string {
return editorRef.value?.getText().trim() ?? ''
}
// slate 文本节点:{ text };由编辑器在当前光标处插入。
function insertText(text: string) {
editorRef.value?.insertNode({ text })
}
defineExpose({ getText, insertText })
onBeforeUnmount(() => {
editorRef.value?.destroy()
})
</script>
<template>
<div class="wang-pane">
<Toolbar
class="pane-toolbar"
:editor="editorRef"
:default-config="toolbarConfig"
:mode="editorMode"
/>
<Editor
class="pane-content"
:model-value="props.modelValue"
:default-config="editorConfig"
:mode="editorMode"
@update:model-value="emit('update:modelValue', $event)"
@on-created="handleCreated"
/>
</div>
</template>
<style scoped>
.wang-pane {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 8px;
}
.pane-toolbar {
flex-shrink: 0;
border-bottom: 1px solid var(--line);
}
.pane-content {
flex: 1;
min-height: 340px;
overflow-y: auto;
}
</style>
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import NewSigningTaskDialog from '@/components/signing/NewSigningTaskDialog.vue'
import type { SigningTemplate } from '@/api/workbench/types'
@@ -50,7 +50,7 @@ const departments: DepartmentRecord[] = ['医学影像科', '医学检验科', '
const filters = reactive<DocumentFilterForm>({
keyword: '',
department: null,
status: 'enabled',
status: 'all',
})
const allTemplates = computed<DocumentTemplate[]>(() =>
@@ -76,19 +76,21 @@ const departmentTabs = computed<DepartmentTab[]>(() => [
})),
])
const statusTabs = computed<StatusTab[]>(() =>
documentStatusOptions.map((option) => ({
const statusTabs = computed<StatusTab[]>(() => [
{ key: 'all', label: '全部', count: allTemplates.value.length },
...documentStatusOptions.map((option) => ({
...option,
count: allTemplates.value.filter((template) => template.status === option.key).length,
})),
)
])
const filteredTemplates = computed(() => {
const keyword = filters.keyword.trim().toLowerCase()
return allTemplates.value.filter((template) => {
const matchesDepartment = !filters.department || template.departmentKey === filters.department
const matchesStatus = template.status === filters.status
const matchesStatus =
filters.status === 'all' || template.status === filters.status
const matchesKeyword =
!keyword ||
[template.name, template.code, template.department].join(' ').toLowerCase().includes(keyword)
@@ -169,7 +171,8 @@ function addLocalTemplate(form: TemplateEditorForm) {
name: form.name,
code: form.templateCode,
version: form.versionNo || 'v1',
status: 'draft',
// 原型闭环:新文书直接启用(工作流 UI 待签署端接入后恢复),保证创建后即可预览/发起。
status: 'enabled',
description: form.description,
versionId: `local-version-${timestamp}`,
departmentId: form.departmentId || null,
@@ -200,6 +203,35 @@ function saveTemplate(form: TemplateEditorForm) {
ElMessage.success(form.id ? '文书已更新' : '文书已创建')
}
async function removeTemplate(template: DocumentTemplate) {
const confirmed = await ElMessageBox.confirm(
`确定删除文书「${template.name}」?删除后不可恢复。`,
'删除文书',
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' },
)
.then(() => true)
.catch(() => false)
if (!confirmed) {
return
}
// 移除文书后,顺带清理空分类与空科室分组,避免留下空壳节点。
library.value = library.value
.map((department) => ({
...department,
categories: department.categories
.map((category) => ({
...category,
documents: category.documents.filter((document) => document.id !== template.id),
}))
.filter((category) => category.documents.length > 0),
}))
.filter((department) => department.categories.length > 0)
ElMessage.success('文书已删除')
}
// 版本工作流(提交审核/发布等)随原模板编辑器移除,待签署端接入后按后端工作流接口恢复。
function toSigningTemplate(template: DocumentTemplate): SigningTemplate {
@@ -265,20 +297,26 @@ onMounted(loadLibrary)
@preview="handlePreview"
@edit="showEditMessage"
@initiate="openSigningDialog"
@remove="removeTemplate"
/>
</div>
<div v-else class="empty-state">
<div class="empty-icon" aria-hidden="true">⌕</div>
<p>没有符合条件的模板</p>
<button
v-if="filters.keyword || filters.department"
type="button"
class="reset-button"
@click="clearFilters"
>
清除筛选
</button>
<template v-if="allTemplates.length === 0">
<p>文书库为空,点击右上角「新增文档」用 Word 创建第一份文书</p>
</template>
<template v-else>
<p>没有符合条件的文书</p>
<button
v-if="filters.keyword || filters.department"
type="button"
class="reset-button"
@click="clearFilters"
>
清除筛选
</button>
</template>
</div>
<DocumentPreviewDialog
@@ -1,6 +1,4 @@
import type { DocumentDepartment, DocumentStatus, LibraryDocument } from './types'
import { presetDocumentContentHtml } from './preset-contents'
import type { DocumentDepartment, DocumentStatus } from './types'
const documentStatusLabels: Record<DocumentStatus, string> = {
enabled: '启用中',
@@ -16,90 +14,5 @@ export const documentStatusOptions = Object.entries(documentStatusLabels).map(([
export { documentStatusLabels }
function createDocument(
id: string,
name: string,
code: string,
version: string,
description: string,
status: DocumentStatus = 'enabled',
): LibraryDocument {
return {
id,
name,
code,
version,
description,
status,
contentHtml: presetDocumentContentHtml[id],
}
}
// 预置文书为 2025 年在各科室采集的真实知情同意书原件(HTML 已固化在 preset-contents.ts),
// 也可在 Word 编辑器中继续维护;科室自维护的新文书通过 Word 编辑器录入。
export const documentLibrary: DocumentDepartment[] = [
{
key: 'medical-imaging',
name: '医学影像科',
campus: 'all',
categories: [
{
name: '磁共振检查',
documents: [
createDocument(
'doc-img-mri-001',
'MRI 检查预约单暨知情同意书',
'TPL-IMG-MRI-001',
'v1.0.0',
'含检查预约登记、病人检查准备及注意事项、婴幼儿检查特殊要求,背面为 MRI 安全筛查表(起搏器、金属植入物、孕产史等逐项勾选),需患者或家属签名确认。',
),
],
},
],
},
{
key: 'medical-laboratory',
name: '医学检验科',
campus: 'all',
categories: [
{
name: '基因检测告知',
documents: [
createDocument(
'doc-lab-infection-gene-001',
'感染基因高通量测序自费项目知情同意书',
'TPL-LAB-IGT-001',
'v1.0.0',
'感染基因 mNGS 检测原理、局限性与潜在风险、受检者知情确认,附 3 项自费检测项目勾选表,自费项目需患者签字确认。',
),
createDocument(
'doc-lab-coop-gene-001',
'对外合作或自费基因检测项目知情同意书',
'TPL-LAB-CGT-001',
'v2025.06',
'2025.6.24 版,覆盖医学检验科、生殖医学中心、宫内儿科疾病诊治中心、临床遗传中心、病理科共 60 项对外合作或自费检测项目,含收费金额、收费类别与执行科室,患方勾选后签署。',
),
],
},
],
},
{
key: 'clinical-genetics',
name: '临床遗传中心',
campus: 'all',
categories: [
{
name: '肿瘤基因检测',
documents: [
createDocument(
'doc-gen-onco-gene-001',
'肿瘤基因高通量测序自费项目知情同意书',
'TPL-GEN-OGT-001',
'v1.0.0',
'肿瘤基因 NGS 分子检测原理、局限性与潜在风险、受检者知情确认,附 BRCA1/2 及 NGS 小/中/大 panel 共 4 项检测项目勾选表。',
),
],
},
],
},
]
// Word 自维护方案:文书库初始为空,所有文书由科室通过 Word 编辑器(粘贴 / 导入 .docx)创建维护。
export const documentLibrary: DocumentDepartment[] = []
File diff suppressed because one or more lines are too long
@@ -1,7 +1,5 @@
export type DocumentStatus = 'enabled' | 'draft' | 'archived' | 'review'
export type DocumentFilterStatus = 'all' | DocumentStatus
export type DocumentCampus = 'all' | '本部院区' | '东院区' | '西院区'
export interface LibraryDocument {
@@ -49,10 +47,18 @@ export interface TemplateEditorForm {
contentHtml: string
}
export type DocumentFilterStatus = 'all' | DocumentStatus
export interface StatusTab {
key: DocumentFilterStatus
label: string
count: number
}
export interface DocumentFilterForm {
keyword: string
department: string | null
status: DocumentStatus
status: DocumentFilterStatus
}
export interface DepartmentTab {
@@ -60,9 +66,3 @@ export interface DepartmentTab {
label: string
count: number
}
export interface StatusTab {
key: DocumentStatus
label: string
count: number
}