feat(documents): 接入文档库真实接口
This commit is contained in:
@@ -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"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user