feat(documents): 接入文档库真实接口

This commit is contained in:
xy
2026-09-17 18:46:09 +08:00
parent 81530b5dd6
commit 8b1cbee16f
8 changed files with 136 additions and 59 deletions
@@ -10,6 +10,7 @@ const props = defineProps<{
template: DocumentTemplate
canEdit: boolean
canInitiate: boolean
canWorkflow: (action: TemplateVersionAction) => boolean
canRemove: boolean
/** 演示模式下「删除」是真删;真实模式下语义是下架(模板无 DELETE 接口,走归档) */
isMock: boolean
@@ -31,9 +32,7 @@ const statusTone = computed(() => readStatusTone(props.template))
/** 只有已发布 / 待复审的模板可以发起签署;草稿与已归档不可用 */
const canInitiateTemplate = computed(
() =>
props.canInitiate &&
(props.template.status === 'published' || props.template.status === 'review'),
() => props.canInitiate && Boolean(props.template.currentVersionId || props.isMock),
)
const initiateTitle = computed(() =>
@@ -64,6 +63,8 @@ const primaryAction = computed<{ key: TemplateVersionAction; label: string } | n
return { key: 'submit-review', label: '重新提交' }
case 'DISABLED':
return { key: 'publish', label: '重新启用' }
case 'APPROVED':
return { key: 'publish', label: '发布' }
default:
return null
}
@@ -75,21 +76,21 @@ const primaryAction = computed<{ key: TemplateVersionAction; label: string } | n
* 「驳回」不能只做成主动作的反面:审核通过和驳回是审核人的一对选择,
* 必须同时可见,否则待审核的文书就只能通过、不能打回。
*/
const secondaryActions = computed<
{ key: TemplateVersionAction; label: string; title: string }[]
>(() => {
if (props.template.backendStatus === 'PENDING_REVIEW') {
return [
{
key: 'reject',
label: '驳回',
title: '驳回后需填写原因,科室按意见修改并重新提交',
},
]
}
const secondaryActions = computed<{ key: TemplateVersionAction; label: string; title: string }[]>(
() => {
if (props.template.backendStatus === 'PENDING_REVIEW' && props.canWorkflow('reject')) {
return [
{
key: 'reject',
label: '驳回',
title: '驳回后需填写原因,科室按意见修改并重新提交',
},
]
}
return []
})
return []
},
)
/**
* 本次交付暂不包含「版本历史」与「删除 / 下架」入口,先隐藏对应按钮。
@@ -136,10 +137,9 @@ const removeTitle = computed(() => (props.isMock ? '删除此文书' : '下架
版本
</button>
<button
v-if="canInitiate"
v-if="canInitiateTemplate"
type="button"
class="card-button primary"
:disabled="!canInitiateTemplate"
:title="initiateTitle"
@click="emit('initiate', template)"
>
@@ -147,7 +147,7 @@ const removeTitle = computed(() => (props.isMock ? '删除此文书' : '下架
</button>
<!-- 还不能签署时,把「当前该做的下一步」放在同一位置,避免按钮越堆越多 -->
<button
v-else-if="primaryAction"
v-if="primaryAction && canWorkflow(primaryAction.key)"
type="button"
class="card-button primary"
:title="`${primaryAction.label}后才能发起签署`"
@@ -19,6 +19,7 @@ const props = defineProps<{
template: DocumentTemplate | null
campuses: CampusRecord[]
departments: DepartmentRecord[]
isMock: boolean
}>()
const emit = defineEmits<{
@@ -52,7 +53,7 @@ const form = reactive<TemplateEditorForm>({
campusId: props.campuses[0]?.id ?? '',
departmentId: '',
category: '知情同意书',
versionNo: 'v1.0.0',
versionNo: 'v1',
contentHtml: '',
})
@@ -80,7 +81,7 @@ watch(
campusId: template?.campusId ?? props.campuses[0]?.id ?? '',
departmentId: template?.departmentId ?? '',
category: template?.category ?? '知情同意书',
versionNo: template?.version ?? 'v1.0.0',
versionNo: template?.version ?? 'v1',
contentHtml: template?.contentHtml ?? '',
})
valueHtml.value = template?.contentHtml || '<p><br></p>'
@@ -224,11 +225,21 @@ function save() {
</label>
<label class="meta-field">
<span>文书编号</span>
<input v-model="form.templateCode" type="text" placeholder="如 TPL-XXX-001" />
<input
v-model="form.templateCode"
type="text"
placeholder="如 TPL-XXX-001"
:disabled="Boolean(template)"
:title="template ? '文书编号创建后不可修改' : undefined"
/>
</label>
<label class="meta-field">
<span>所属院区</span>
<select v-model="form.campusId" @change="handleCampusChange">
<select
v-model="form.campusId"
:disabled="Boolean(template)"
@change="handleCampusChange"
>
<option v-for="campus in campuses" :key="campus.id" :value="campus.id">
{{ campus.name }}
</option>
@@ -236,7 +247,7 @@ function save() {
</label>
<label class="meta-field">
<span>所属科室</span>
<select v-model="form.departmentId">
<select v-model="form.departmentId" :disabled="Boolean(template)">
<option value="">全院通用</option>
<option
v-for="department in availableDepartments"
@@ -253,7 +264,8 @@ function save() {
</label>
<label class="meta-field">
<span>版本号</span>
<input v-model="form.versionNo" type="text" />
<input v-if="isMock" v-model="form.versionNo" type="text" />
<span v-else class="auto-version">由系统按 vN 自动生成</span>
</label>
</div>
@@ -8,7 +8,9 @@ import {
deleteTemplate,
getDocuments,
getTemplateContent,
getTemplateVersionDetail,
getTemplateVersions,
readVersionContentHtml,
updateTemplate,
updateTemplateContent,
updateTemplateVersionStatus,
@@ -18,6 +20,7 @@ import { getCampuses, getDepartments } from '@/api/management/organization'
import type {
CampusRecord,
CreateTemplateRequest,
CreateTemplateVersionRequest,
DocumentRecord,
DepartmentRecord,
TemplateVersionResponseDto,
@@ -56,8 +59,8 @@ import type {
/**
* 文档库数据来源。
*
* - 演示模式(VITE_MOCK_DOCUMENTS 开启,缺省开启):读本地演示文书库,可随意增删改;
* - 真实模式:读模板接口。列表只给模板元数据(含 currentVersionId),
* - 演示模式(显式开启 VITE_MOCK_DOCUMENTS):读本地演示文书库,可随意增删改;
* - 真实模式:读模板接口。列表只给模板元数据(含发布/最新版本指针),
* 正文要再取一次版本详情——这是接入时最容易被忽略的一步,
* 缺了它文档库会显示一堆没有正文、无法预览的"空壳模板"。
*
@@ -78,7 +81,8 @@ const canCreateTemplate = computed(
const canEditTemplate = computed(
() =>
!loginStore.isReadOnly &&
(useMockLibrary || loginStore.hasWritePermission('sign:template:version:create')),
(useMockLibrary ||
loginStore.hasWritePermission(['sign:template:update', 'sign:template:version:create'])),
)
const canArchiveTemplate = computed(
() =>
@@ -89,6 +93,22 @@ const canInitiateSigning = computed(
() =>
!loginStore.isReadOnly && (useMockLibrary || loginStore.hasWritePermission('sign:task:create')),
)
const workflowPermissions: Record<TemplateVersionAction, string> = {
'submit-review': 'sign:template:review:submit',
reject: 'sign:template:review:reject',
approve: 'sign:template:review:approve',
publish: 'sign:template:publish',
disable: 'sign:template:disable',
archive: 'sign:template:archive',
}
function canRunTemplateWorkflow(action: TemplateVersionAction) {
return (
!loginStore.isReadOnly &&
(useMockLibrary || loginStore.hasWritePermission(workflowPermissions[action]))
)
}
/** 真实模式下的加载状态:区分"正在加载"与"确实没有模板" */
const libraryLoading = ref(false)
const libraryError = ref('')
@@ -177,8 +197,7 @@ const filteredTemplates = computed(() => {
return allTemplates.value.filter((template) => {
const matchesDepartment = !filters.department || template.departmentKey === filters.department
const matchesStatus =
filters.status === 'all' || readBackendStatus(template) === filters.status
const matchesStatus = filters.status === 'all' || readBackendStatus(template) === filters.status
const matchesKeyword =
!keyword ||
[template.name, template.code, template.department].join(' ').toLowerCase().includes(keyword)
@@ -364,7 +383,7 @@ async function handlePreview(template: DocumentTemplate) {
previewError.value = false
try {
const contentHtml = await getTemplateContent(template.id)
const contentHtml = await getTemplateContent(template.id, template.versionId)
// 用户在等待期间可能已经切到别的文书,回填前确认仍是同一份
if (selectedTemplate.value?.id === template.id) {
@@ -422,7 +441,7 @@ async function showEditMessage(template: DocumentTemplate) {
wordEditorLoading.value = true
try {
const contentHtml = await getTemplateContent(template.id)
const contentHtml = await getTemplateContent(template.id, template.versionId)
if (!contentHtml) {
ElMessage.error('未能取到该文书的正文内容,请确认模板版本内容接口可访问')
@@ -512,9 +531,11 @@ function toCreatePayload(form: TemplateEditorForm): CreateTemplateRequest {
}
}
function toVersionPayload(form: TemplateEditorForm) {
function toVersionPayload(
form: TemplateEditorForm,
): Omit<CreateTemplateVersionRequest, 'signatureFields'> {
return {
versionNo: form.versionNo,
...(useMockLibrary && form.versionNo ? { versionNo: form.versionNo } : {}),
contentHtml: form.contentHtml,
}
}
@@ -537,6 +558,8 @@ async function saveTemplate(form: TemplateEditorForm) {
return
}
let versionCreated = false
try {
let savedId = form.id
@@ -545,6 +568,7 @@ async function saveTemplate(form: TemplateEditorForm) {
// 新版本的状态由后端决定,前端不在这里「顺带发布」——发布是需要留痕的正式动作,
// 要由人显式触发(见 handleWorkflow),否则「谁把这份文书放行到可签署」无从追溯。
await updateTemplateContent(form.id, toVersionPayload(form))
versionCreated = true
await updateTemplate(form.id, {
name: form.name,
description: form.description,
@@ -573,7 +597,11 @@ async function saveTemplate(form: TemplateEditorForm) {
)
}
} catch {
ElMessage.error(form.id ? '文书更新失败,请稍后重试' : '文书创建失败,请稍后重试')
if (form.id && versionCreated) {
ElMessage.error('新版本已保存,但文书名称、说明或分类更新失败;请刷新后重试基本信息')
} else {
ElMessage.error(form.id ? '文书更新失败,请稍后重试' : '文书创建失败,请稍后重试')
}
}
}
@@ -627,6 +655,11 @@ async function handleWorkflow(template: DocumentTemplate, action: TemplateVersio
return
}
if (!canRunTemplateWorkflow(action)) {
ElMessage.warning(`当前账号没有${workflowActionLabels[action]}权限`)
return
}
const label = workflowActionLabels[action]
let reason: string | undefined
@@ -777,10 +810,23 @@ async function removeTemplate(template: DocumentTemplate) {
* 与模板无关的空文书。因此这里在缺正文时先取回,取不到就拒绝发起。
*/
async function toSigningTemplate(template: DocumentTemplate): Promise<SigningTemplate | null> {
let contentHtml = template.contentHtml
if (!useMockLibrary && !template.currentVersionId) {
ElMessage.error('该文书还没有已发布版本,无法发起签署')
return null
}
if (!useMockLibrary && !contentHtml) {
contentHtml = await getTemplateContent(template.id)
let contentHtml = template.contentHtml
let versionNo = template.version
let contentSha256 = template.contentSha256
const signingVersionId = useMockLibrary
? (template.versionId ?? template.id)
: template.currentVersionId!
if (!useMockLibrary) {
const version = await getTemplateVersionDetail(template.id, signingVersionId)
contentHtml = readVersionContentHtml(version)
versionNo = version?.versionNo ?? versionNo
contentSha256 = version?.contentSha256 ?? contentSha256
if (!contentHtml) {
ElMessage.error('未能取到该文书的正文内容,无法发起签署')
@@ -790,10 +836,10 @@ async function toSigningTemplate(template: DocumentTemplate): Promise<SigningTem
return {
id: template.id,
versionId: template.versionId ?? template.id,
versionId: signingVersionId,
name: template.name,
code: template.code ?? '',
version: template.version,
version: versionNo,
department: template.department,
departmentId: template.departmentId,
campusId: template.campusId ?? undefined,
@@ -801,7 +847,7 @@ async function toSigningTemplate(template: DocumentTemplate): Promise<SigningTem
description: template.description ?? '',
supportedMethods: ['pad', 'sms'],
contentHtml,
contentSha256: template.contentSha256,
contentSha256,
}
}
@@ -930,6 +976,7 @@ onMounted(() => {
:template="template"
:can-edit="canEditTemplate"
:can-initiate="canInitiateSigning"
:can-workflow="canRunTemplateWorkflow"
:can-remove="canArchiveTemplate"
:is-mock="useMockLibrary"
@preview="handlePreview"
@@ -978,6 +1025,7 @@ onMounted(() => {
:template="wordEditingTemplate"
:campuses="campuses"
:departments="departments"
:is-mock="useMockLibrary"
@close="wordEditorVisible = false"
@save="saveTemplate"
/>