feat(文书管理): 增加版本工作流与版本历史
- 模板状态按后端原始状态展示与筛选,待审核与已驳回分开展示 - 仅已发布模板可发起签署,新增提交审核/审核通过/驳回等动作 - 新增版本历史弹窗,复用统一的模板时间格式化方法 - 演示数据补齐已签署任务的签名图,并提供获取签名 dataURL 的接口 - 修复编辑器插入占位符时未定位光标导致内容落到文首的问题
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
||||
type SigningFieldProgress,
|
||||
} from '@/utils/signing-document'
|
||||
|
||||
import { documentStatusLabels } from '../mock'
|
||||
import { readStatusLabel, readStatusTone } from '../mock'
|
||||
import type { DocumentTemplate } from '../types'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -74,19 +74,17 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
if (!props.template) {
|
||||
return ''
|
||||
}
|
||||
const statusLabel = computed(() => (props.template ? readStatusLabel(props.template) : ''))
|
||||
|
||||
return documentStatusLabels[props.template.status]
|
||||
})
|
||||
/** 徽标配色与文书卡片同口径:草稿/待审核/已驳回/已发布/已归档各不相同 */
|
||||
const statusTone = computed(() => (props.template ? readStatusTone(props.template) : 'draft'))
|
||||
|
||||
/** 与文书卡片保持同一口径:草稿与已归档不可发起签署 */
|
||||
/**
|
||||
* 与文书卡片保持同一口径:只有已发布(后端 APPROVED / PUBLISHED)才能发起签署。
|
||||
* 不放行 review —— 它同时承载待审核与已驳回,两者都未定稿。
|
||||
*/
|
||||
const canInitiate = computed(
|
||||
() =>
|
||||
Boolean(props.template?.contentHtml) &&
|
||||
(props.template?.status === 'published' || props.template?.status === 'review'),
|
||||
() => Boolean(props.template?.contentHtml) && props.template?.status === 'published',
|
||||
)
|
||||
|
||||
// 预置文书的 contentHtml 是完整 HTML 文档(自带版式样式);
|
||||
@@ -179,7 +177,7 @@ function syncPreviewFields() {
|
||||
<div class="dialog-title">
|
||||
<h2 id="document-preview-title">{{ template.name }}</h2>
|
||||
<span class="word-badge">类 Word 版式</span>
|
||||
<span class="status-badge">{{ statusLabel }}</span>
|
||||
<span class="status-badge" :class="'tone-' + statusTone">{{ statusLabel }}</span>
|
||||
</div>
|
||||
<button type="button" class="close-button" aria-label="关闭预览" @click="emit('close')">
|
||||
×
|
||||
@@ -291,7 +289,7 @@ function syncPreviewFields() {
|
||||
type="button"
|
||||
class="footer-button primary"
|
||||
:disabled="!canInitiate"
|
||||
:title="canInitiate ? '用此模板发起签署' : '仅已发布或待复审的模板可发起签署'"
|
||||
:title="canInitiate ? '用此模板发起签署' : '仅已发布(审核通过)的模板可发起签署'"
|
||||
@click="emit('use', template)"
|
||||
>
|
||||
用此模板发起签署
|
||||
@@ -377,7 +375,24 @@ function syncPreviewFields() {
|
||||
background: var(--brand-l);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
/* 状态徽标配色:与文书卡片同一套色调,草稿/待审核/已驳回不再一律显示成绿色 */
|
||||
.status-badge.tone-draft,
|
||||
.status-badge.tone-archived {
|
||||
color: #8a9aa3;
|
||||
background: #eceef0;
|
||||
}
|
||||
|
||||
.status-badge.tone-review {
|
||||
color: var(--warn);
|
||||
background: var(--warn-l);
|
||||
}
|
||||
|
||||
.status-badge.tone-rejected {
|
||||
color: var(--err);
|
||||
background: var(--err-l);
|
||||
}
|
||||
|
||||
.status-badge.tone-published {
|
||||
color: var(--ok);
|
||||
background: var(--ok-l);
|
||||
}
|
||||
|
||||
@@ -1,37 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { TemplateVersionAction } from '@/api/management/documents'
|
||||
|
||||
import type { DocumentTemplate } from '../types'
|
||||
import { documentStatusLabels } from '../mock'
|
||||
import { readStatusLabel, readStatusTone } from '../mock'
|
||||
|
||||
const props = defineProps<{
|
||||
template: DocumentTemplate
|
||||
/** 演示模式下「删除」是真删;真实模式下语义是下架(模板无 DELETE 接口,走归档) */
|
||||
isMock: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
preview: [template: DocumentTemplate]
|
||||
edit: [template: DocumentTemplate]
|
||||
versions: [template: DocumentTemplate]
|
||||
initiate: [template: DocumentTemplate]
|
||||
remove: [template: DocumentTemplate]
|
||||
workflow: [template: DocumentTemplate, action: TemplateVersionAction]
|
||||
}>()
|
||||
|
||||
const statusLabel = computed(() => documentStatusLabels[props.template.status])
|
||||
const statusLabel = computed(() => readStatusLabel(props.template))
|
||||
|
||||
/** 只有已发布 / 待复审的模板可以发起签署;草稿与已归档不可用 */
|
||||
const canInitiate = computed(
|
||||
() => props.template.status === 'published' || props.template.status === 'review',
|
||||
)
|
||||
/** 配色按后端状态分组取(待审核与已驳回不能共用一个颜色) */
|
||||
const statusTone = computed(() => readStatusTone(props.template))
|
||||
|
||||
/**
|
||||
* 只有已发布(后端 APPROVED / PUBLISHED)的模板可以发起签署。
|
||||
*
|
||||
* 这里不再放行 review:review 同时承载后端的 PENDING_REVIEW(待审核)与
|
||||
* REJECTED(已驳回),两者都还没通过审核。允许它们发起签署,
|
||||
* 等于让未定稿的文书进入签署流程,签出来的文书也无法追溯是哪一版定稿。
|
||||
*/
|
||||
const canInitiate = computed(() => props.template.status === 'published')
|
||||
|
||||
const initiateTitle = computed(() =>
|
||||
canInitiate.value ? '用此模板发起签署' : '仅已发布或待复审的模板可发起签署',
|
||||
canInitiate.value ? '用此模板发起签署' : '仅已发布(审核通过)的模板可发起签署',
|
||||
)
|
||||
|
||||
/**
|
||||
* 版本工作流主动作。
|
||||
*
|
||||
* 模板状态只能由后端工作流推进,前端不自行改写 status,因此这里按后端原始状态
|
||||
* (backendStatus,不是归一后的 status)给出「当前该做的下一步」,把文书从草稿推到可签署。
|
||||
*
|
||||
* 不放在这里的两种状态:
|
||||
* - APPROVED / PUBLISHED 已经可签署,走「发起」按钮;
|
||||
* - ARCHIVED 是终态,没有任何可执行动作。
|
||||
*/
|
||||
const primaryAction = computed<{ key: TemplateVersionAction; label: string } | null>(() => {
|
||||
switch (props.template.backendStatus) {
|
||||
case 'DRAFT':
|
||||
return { key: 'submit-review', label: '提交审核' }
|
||||
case 'PENDING_REVIEW':
|
||||
return { key: 'approve', label: '审核通过' }
|
||||
case 'REJECTED':
|
||||
return { key: 'submit-review', label: '重新提交' }
|
||||
case 'DISABLED':
|
||||
return { key: 'publish', label: '重新启用' }
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 版本工作流次要动作。
|
||||
*
|
||||
* 「驳回」不能只做成主动作的反面:审核通过和驳回是审核人的一对选择,
|
||||
* 必须同时可见,否则待审核的文书就只能通过、不能打回。
|
||||
*/
|
||||
const secondaryActions = computed<
|
||||
{ key: TemplateVersionAction; label: string; title: string }[]
|
||||
>(() => {
|
||||
if (props.template.backendStatus === 'PENDING_REVIEW') {
|
||||
return [
|
||||
{
|
||||
key: 'reject',
|
||||
label: '驳回',
|
||||
title: '驳回后需填写原因,科室按意见修改并重新提交',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return []
|
||||
})
|
||||
|
||||
const removeTitle = computed(() => (props.isMock ? '删除此文书' : '下架此文书'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article class="template-card">
|
||||
<div class="template-head">
|
||||
<h3 :title="template.name">{{ template.name }}</h3>
|
||||
<span class="template-status" :class="'status-' + template.status">
|
||||
<span class="template-status" :class="'status-' + statusTone">
|
||||
<i aria-hidden="true" />
|
||||
{{ statusLabel }}
|
||||
</span>
|
||||
@@ -51,21 +113,49 @@ const initiateTitle = computed(() =>
|
||||
<button type="button" class="card-button" @click="emit('preview', template)">预览</button>
|
||||
<button type="button" class="card-button" @click="emit('edit', template)">编辑</button>
|
||||
<button
|
||||
type="button"
|
||||
class="card-button"
|
||||
title="查看该文书的版本历史"
|
||||
@click="emit('versions', template)"
|
||||
>
|
||||
版本
|
||||
</button>
|
||||
<button
|
||||
v-if="canInitiate"
|
||||
type="button"
|
||||
class="card-button primary"
|
||||
:disabled="!canInitiate"
|
||||
:title="initiateTitle"
|
||||
@click="emit('initiate', template)"
|
||||
>
|
||||
发起
|
||||
</button>
|
||||
<!-- 还不能签署时,把「当前该做的下一步」放在同一位置,避免按钮越堆越多 -->
|
||||
<button
|
||||
v-else-if="primaryAction"
|
||||
type="button"
|
||||
class="card-button primary"
|
||||
:title="`${primaryAction.label}后才能发起签署`"
|
||||
@click="emit('workflow', template, primaryAction.key)"
|
||||
>
|
||||
{{ primaryAction.label }}
|
||||
</button>
|
||||
<button
|
||||
v-for="action in secondaryActions"
|
||||
:key="action.key"
|
||||
type="button"
|
||||
class="card-button"
|
||||
:title="action.title"
|
||||
@click="emit('workflow', template, action.key)"
|
||||
>
|
||||
{{ action.label }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="card-button danger"
|
||||
title="删除此文书"
|
||||
:title="removeTitle"
|
||||
@click="emit('remove', template)"
|
||||
>
|
||||
删除
|
||||
{{ isMock ? '删除' : '下架' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -153,6 +243,13 @@ const initiateTitle = computed(() =>
|
||||
background: var(--warn-l);
|
||||
}
|
||||
|
||||
/* 已驳回要和「待审核」在颜色上分开:两者归一后都是 review,
|
||||
只靠文字区分时一眼扫过去仍会混为一谈 */
|
||||
.status-rejected {
|
||||
color: var(--err);
|
||||
background: var(--err-l);
|
||||
}
|
||||
|
||||
.template-meta {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
@@ -203,10 +300,14 @@ const initiateTitle = computed(() =>
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* 按钮数量随状态变化(待审核时多一个「驳回」),允许换行并右对齐,
|
||||
否则窄屏下最后一个按钮会被挤出卡片 */
|
||||
.template-actions {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex: 1 1 auto;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.card-button {
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { formatTemplateDateTime } from '@/api/management/documents'
|
||||
import type { TemplateVersionResponseDto } from '@/api/management/types'
|
||||
|
||||
import { readBackendStatusLabel } from '../mock'
|
||||
import type { DocumentTemplate } from '../types'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
template: DocumentTemplate | null
|
||||
versions: TemplateVersionResponseDto[]
|
||||
loading: boolean
|
||||
error: boolean
|
||||
/** 演示文库只保存当前版本,弹窗要如实说明,别让科室以为这就是全部历史 */
|
||||
isMock: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
/**
|
||||
* 版本 DTO → 展示行。
|
||||
*
|
||||
* 版本号优先用后端给的 versionNo;只给了自增号时补一个 `v{号}`,
|
||||
* 两者都没有才显示 —,不自己编一个看起来像版本号的值。
|
||||
*/
|
||||
const versionRows = computed(() =>
|
||||
props.versions.map((version) => ({
|
||||
id: version.id,
|
||||
versionNo: version.versionNo || (version.versionNumber ? `v${version.versionNumber}` : '—'),
|
||||
statusLabel: readBackendStatusLabel(version.status),
|
||||
isCurrent: version.id === props.template?.versionId,
|
||||
createdBy: version.createdBy || '—',
|
||||
createdAt: formatTemplateDateTime(version.createdAt),
|
||||
reviewedAt: formatTemplateDateTime(version.reviewedAt),
|
||||
publishedAt: formatTemplateDateTime(version.publishedAt),
|
||||
reviewComment: version.reviewComment ?? '',
|
||||
})),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="visible && template" class="dialog-mask" @click.self="emit('close')">
|
||||
<section
|
||||
class="dialog-card version-dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="version-dialog-title"
|
||||
>
|
||||
<header class="dialog-header">
|
||||
<div class="dialog-heading">
|
||||
<strong id="version-dialog-title">版本历史</strong>
|
||||
<span>{{ template.name }} · {{ template.code }}</span>
|
||||
</div>
|
||||
<button type="button" class="dialog-close" aria-label="关闭" @click="emit('close')">
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="dialog-body">
|
||||
<p v-if="loading" class="version-state">正在加载版本历史…</p>
|
||||
|
||||
<p v-else-if="error" class="version-state version-state--error">
|
||||
版本历史加载失败,请确认后端模板版本接口可访问后重试。
|
||||
</p>
|
||||
|
||||
<template v-else-if="versionRows.length">
|
||||
<ol class="version-list">
|
||||
<li v-for="row in versionRows" :key="row.id" class="version-item">
|
||||
<div class="version-item__head">
|
||||
<strong>{{ row.versionNo }}</strong>
|
||||
<span class="version-status">{{ row.statusLabel }}</span>
|
||||
<span v-if="row.isCurrent" class="version-current">当前版本</span>
|
||||
</div>
|
||||
|
||||
<dl class="version-meta">
|
||||
<div>
|
||||
<dt>创建人</dt>
|
||||
<dd>{{ row.createdBy }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>创建时间</dt>
|
||||
<dd>{{ row.createdAt || '—' }}</dd>
|
||||
</div>
|
||||
<div v-if="row.reviewedAt">
|
||||
<dt>审核时间</dt>
|
||||
<dd>{{ row.reviewedAt }}</dd>
|
||||
</div>
|
||||
<div v-if="row.publishedAt">
|
||||
<dt>发布时间</dt>
|
||||
<dd>{{ row.publishedAt }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<p v-if="row.reviewComment" class="version-comment">
|
||||
审核意见:{{ row.reviewComment }}
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<p v-if="isMock" class="version-hint">
|
||||
演示文库只保留当前版本,因此这里只有一条。真实模式下会列出该文书的全部历史版本。
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<p v-else class="version-state">该文书还没有版本记录。</p>
|
||||
</div>
|
||||
|
||||
<footer class="dialog-footer">
|
||||
<span>已签署的文书引用的是具体版本,历史版本不会被覆盖,可随时追溯。</span>
|
||||
<button type="button" class="action-button" @click="emit('close')">关闭</button>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dialog-mask {
|
||||
position: fixed;
|
||||
z-index: 50;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
background: rgb(9 40 52 / 45%);
|
||||
}
|
||||
|
||||
.dialog-card {
|
||||
display: flex;
|
||||
width: 560px;
|
||||
max-width: 96vw;
|
||||
max-height: calc(100vh - 32px);
|
||||
flex-direction: column;
|
||||
margin: auto;
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
box-shadow: var(--sh-modal);
|
||||
animation: dialog-pop 0.18s ease;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
padding: 12px 18px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.dialog-heading {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.dialog-heading strong {
|
||||
color: var(--ink);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.dialog-heading span {
|
||||
overflow: hidden;
|
||||
color: var(--mut);
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dialog-close {
|
||||
flex-shrink: 0;
|
||||
padding: 0;
|
||||
margin-left: auto;
|
||||
color: var(--mut);
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dialog-close:hover {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
flex: 1;
|
||||
padding: 14px 18px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.version-state {
|
||||
padding: 28px 0;
|
||||
margin: 0;
|
||||
color: var(--mut);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.version-state--error {
|
||||
color: var(--err);
|
||||
}
|
||||
|
||||
.version-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.version-item {
|
||||
padding: 12px 14px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--r);
|
||||
}
|
||||
|
||||
.version-item__head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.version-item__head strong {
|
||||
color: var(--ink);
|
||||
font-size: 13.5px;
|
||||
}
|
||||
|
||||
.version-status {
|
||||
padding: 2px 8px;
|
||||
color: var(--brand);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
background: var(--brand-l);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.version-current {
|
||||
padding: 2px 8px;
|
||||
color: var(--ok);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
background: var(--ok-l);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.version-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 18px;
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
|
||||
.version-meta div {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.version-meta dt {
|
||||
color: var(--mut);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.version-meta dd {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.version-comment {
|
||||
padding: 8px 10px;
|
||||
margin: 9px 0 0;
|
||||
color: #a15c19;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
background: var(--warn-l);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.version-hint {
|
||||
margin: 12px 0 0;
|
||||
color: var(--mut);
|
||||
font-size: 11.5px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 10px 18px;
|
||||
color: var(--mut);
|
||||
font-size: 12px;
|
||||
background: #f8fbfc;
|
||||
border-top: 1px solid var(--line);
|
||||
border-radius: 0 0 14px 14px;
|
||||
}
|
||||
|
||||
.dialog-footer span {
|
||||
flex: 1;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
flex-shrink: 0;
|
||||
padding: 8px 16px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
background: var(--brand);
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background: var(--brand-d);
|
||||
}
|
||||
|
||||
@keyframes dialog-pop {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.96) translateY(8px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -16,8 +16,26 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const editorRef = shallowRef<IDomEditor>()
|
||||
const editableRef = shallowRef<Element | null>(null)
|
||||
const editorMode = 'default'
|
||||
|
||||
/**
|
||||
* 用户是否真的把光标放进过正文。
|
||||
*
|
||||
* 点「插入占位符 / 勾选框」按钮时焦点在按钮上,编辑器处于失焦态;
|
||||
* 直接 insertNode 会落到**文档最开头**——导入 .docx 后补签名位,
|
||||
* 签名域会被插到标题之前,而且当场看不出来,要等到预览或签署才发现。
|
||||
*
|
||||
* 不能用 editor.selection 判断「有没有光标」:导入 .docx 写入内容时它同样会有一个
|
||||
* 位于文首的选区,照样把签名位插到开头。因此只认真实交互——
|
||||
* 在可编辑区域上发生过 pointerdown / keydown 才算用户放好了光标。
|
||||
*/
|
||||
const userPlacedCaret = shallowRef(false)
|
||||
|
||||
function markUserCaret() {
|
||||
userPlacedCaret.value = true
|
||||
}
|
||||
|
||||
// 医疗文书用不到的工具栏能力直接移除,避免科室误用。
|
||||
// 注意菜单 key:分割线是 divider、表情是 emotion(不是 emoji)。
|
||||
const toolbarConfig: Partial<IToolbarConfig> = {
|
||||
@@ -64,6 +82,11 @@ const editorConfig: Partial<IEditorConfig> = {
|
||||
|
||||
function handleCreated(editor: IDomEditor) {
|
||||
editorRef.value = editor
|
||||
|
||||
const editable = editor.getEditableContainer()
|
||||
editableRef.value = editable
|
||||
editable.addEventListener('pointerdown', markUserCaret)
|
||||
editable.addEventListener('keydown', markUserCaret)
|
||||
}
|
||||
|
||||
function getText(): string {
|
||||
@@ -72,12 +95,23 @@ function getText(): string {
|
||||
|
||||
// slate 文本节点:{ text };由编辑器在当前光标处插入。
|
||||
function insertText(text: string) {
|
||||
editorRef.value?.insertNode({ text })
|
||||
const editor = editorRef.value
|
||||
|
||||
if (!editor) {
|
||||
return
|
||||
}
|
||||
|
||||
// 先聚焦并定位光标再插入:用户没点过正文时落到文末,
|
||||
// 点过则保持他自己的光标位置(光标停在表格单元格里时内容就落进该单元格)。
|
||||
editor.focus(!userPlacedCaret.value)
|
||||
editor.insertNode({ text })
|
||||
}
|
||||
|
||||
defineExpose({ getText, insertText })
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
editableRef.value?.removeEventListener('pointerdown', markUserCaret)
|
||||
editableRef.value?.removeEventListener('keydown', markUserCaret)
|
||||
editorRef.value?.destroy()
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user