feat(文书管理): 增加版本工作流与版本历史
- 模板状态按后端原始状态展示与筛选,待审核与已驳回分开展示 - 仅已发布模板可发起签署,新增提交审核/审核通过/驳回等动作 - 新增版本历史弹窗,复用统一的模板时间格式化方法 - 演示数据补齐已签署任务的签名图,并提供获取签名 dataURL 的接口 - 修复编辑器插入占位符时未定位光标导致内容落到文首的问题
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user