refactor(文书库): 状态口径收敛为启用中/停用两档并移除审核链路

This commit is contained in:
yelan
2026-09-28 09:28:21 +08:00
parent 2a0681287c
commit d711e03a4e
7 changed files with 82 additions and 173 deletions
@@ -68,23 +68,21 @@ watch(
/** 状态口径统一走 library.ts:卡片、徽标、预览三处必须是同一档 */
const libraryStatus = computed<DocumentLibraryStatus>(() =>
props.template ? readLibraryStatus(props.template) : '草稿',
props.template ? readLibraryStatus(props.template) : '启用中',
)
/** 文书状态 → 预览弹窗徽标色调(弹窗只认 brand / ok / warn / mut 四档) */
const BADGE_TONES: Record<DocumentStatusTone, 'ok' | 'warn' | 'mut'> = {
const BADGE_TONES: Record<DocumentStatusTone, 'ok' | 'mut'> = {
enabled: 'ok',
review: 'warn',
draft: 'mut',
archived: 'mut',
stopped: 'mut',
}
const statusTone = computed(() => BADGE_TONES[readStatusTone(libraryStatus.value)])
/**
* 与文书卡片保持同一口径:只有「启用中」的文书能发起签署。
* 「需复审」不放行——它同时承载待审核与已驳回,两者都未定稿。
* 与文书卡片同一口径:只有「启用中」的文书能发起签署。
* 预览这一侧手里就有正文(列表接口不返回,进预览时按需取回),
* 所以比卡片多判一道正文——空正文签出来的是一份与模板无关的白纸。
*/
const canInitiate = computed(
() => Boolean(props.template?.contentHtml) && canInitiateSigning(libraryStatus.value),
@@ -5,7 +5,6 @@ import type { TemplateVersionAction } from '@/api/management/documents'
import {
canInitiateSigning,
canToggleStatus,
DOCUMENT_STATUS_DOT,
isGeneralOwnership,
readLibraryStatus,
@@ -16,15 +15,6 @@ import type { DocumentTemplate } from '../types'
const props = defineProps<{
template: DocumentTemplate
/**
* 演示模式。
*
* 真实模式下卡片只有「停用 / 启用」这一个状态动作:后端菜单资源树里的
* 「发布 / 提交审核」按钮虽在,但原型 V10.2 已经抛弃那条工作流,
* 科室侧不该再看到「提交审核」这一步(草稿的送审改由属性/正文弹窗完成,
* 或直接由医务管理放行)。
*/
isMock: boolean
}>()
const emit = defineEmits<{
@@ -33,7 +23,6 @@ const emit = defineEmits<{
'edit-content': [template: DocumentTemplate]
history: [template: DocumentTemplate]
'toggle-status': [template: DocumentTemplate]
'submit-review': [template: DocumentTemplate]
initiate: [template: DocumentTemplate]
}>()
@@ -55,13 +44,18 @@ const ownershipLabel = computed(() =>
*/
const sharedCount = computed(() => props.template.sharedDepartments?.length ?? 0)
/** 只有「启用中」能发起签署;其余状态要么没定稿,要么已下线 */
/**
* 只有「启用中」能发起签署;「停用」已下线。
*
* 这里只看状态,不看正文——真实模式的列表接口不返回正文,按正文判断会让所有
* 真实模板的「发起」按钮都消失。缺正文由发起那一步拦下(见 index.vue 的 toSigningTemplate)。
*/
const canInitiate = computed(() => canInitiateSigning(status.value))
const initiateTitle = computed(() =>
canInitiate.value ? '用此文书发起签署' : '仅「启用中」的文书可发起签署',
)
const canToggle = computed(() => canToggleStatus(status.value))
/** 两档之间都能来回切,所以这个按钮没有「不适用」的状态 */
const toggleLabel = computed(() => (status.value === '停用' ? '启用' : '停用'))
/**
@@ -73,9 +67,6 @@ const toggleLabel = computed(() => (status.value === '停用' ? '启用' : '停
const examType = computed(() => props.template.examType?.trim() ?? '')
const projectCodes = computed(() => (props.template.projectCodes ?? []).filter(Boolean))
/** 草稿的「提交审核」只在演示模式露出,真实模式不给这个入口 */
const showSubmitReview = computed(() => props.isMock && status.value === '草稿')
/** 停用/启用走的是后端版本动作:停用 → disable,启用 → publish */
const toggleAction = computed<TemplateVersionAction>(() =>
status.value === '停用' ? 'publish' : 'disable',
@@ -142,7 +133,6 @@ const togglePermission = computed(() => workflowPermission(toggleAction.value))
<span class="ft-spacer" />
<button
v-if="canToggle"
v-permission="togglePermission"
type="button"
class="card-button"
@@ -155,16 +145,6 @@ const togglePermission = computed(() => workflowPermission(toggleAction.value))
>
{{ toggleLabel }}
</button>
<button
v-if="showSubmitReview"
v-permission="permissions.publish"
type="button"
class="card-button"
title="提交审核,审核通过后自动发布"
@click="emit('submit-review', template)"
>
提交审核
</button>
<button
v-if="canInitiate"
v-permission="permissions.initiate"
@@ -243,13 +223,6 @@ const togglePermission = computed(() => workflowPermission(toggleAction.value))
background: var(--ok-l);
}
.tone-review {
color: var(--warn);
background: var(--warn-l);
}
.tone-draft,
.tone-archived,
.tone-stopped {
color: #8a9aa3;
background: #eceef0;
@@ -14,9 +14,14 @@ import type { DocumentHistoryRow } from './types'
/**
* 由版本状态归纳「这一步做了什么」。
*
* 顺序有讲究:先判终态(归档 / 停用),再看发布与审核时间戳。
* 只看 status 是不够的——一个已发布版本后来又被打回时,status 会回到 DRAFT/REJECTED,
* 但它确实发布过,历史记录里应该留下痕迹。
* 顺序有讲究:先判终态(归档 / 停用),再看发布时间。
* 只看 status 是不够的——一个版本被停用后 status 会变,但它确实发布过,
* 历史记录里应该留下痕迹。
*
* **不再归纳审核动作**(提交审核 / 审核通过 / 审核驳回):状态收敛成
* 「启用中 / 停用」两档后审核链路已废弃,DTO 里即使还留着 reviewSubmittedAt /
* reviewedAt,也不再产生新的历史动作。归档保留:它是版本终态而非审核环节,
* api 层的 deleteTemplate 仍靠 archive 动作承担删除语义。
*/
function describeVersionAction(version: TemplateVersionResponseDto): string {
if (version.status === 'ARCHIVED') {
@@ -31,14 +36,6 @@ function describeVersionAction(version: TemplateVersionResponseDto): string {
return '发布生效'
}
if (version.reviewedAt) {
return version.status === 'REJECTED' ? '审核驳回' : '审核通过'
}
if (version.reviewSubmittedAt) {
return '提交审核'
}
return '创建版本'
}
@@ -40,7 +40,6 @@ import WordEditorDialog from './components/WordEditorDialog.vue'
import { toHistoryRows } from './history'
import {
canInitiateSigning,
canToggleStatus,
DOCUMENT_LIBRARY_STATUSES,
readLibraryStatus,
resolveOwnershipDepartmentId,
@@ -164,10 +163,10 @@ const departmentTabs = computed<DepartmentTab[]>(() => [
])
/**
* 状态页签按原型 V10.2 的 libStatuses 铺开(启用中 / 草稿 / 归档 / 需复审 / 停用)。
* 状态页签只有两档(启用中 / 停用),外加「全部」。
*
* 不再按后端模板状态枚举做页签:原型已经抛弃「发布 → 审核」那条工作流,
* 7 个后端状态摆到界面上,科室会看到一堆看不懂也点不动的档位。
* 不再有草稿 / 归档 / 需复审:审核链路已废弃,后端状态也会重写。
* 也不按后端模板状态枚举做页签——枚举摆到界面上,科室会看到一堆看不懂也点不动的档位。
* 计数按**全库**统计(不受科室页签影响),与原型 libCountByStatus 一致。
*/
const statusTabs = computed<StatusTab[]>(() => [
@@ -514,10 +513,11 @@ function addLocalTemplate(form: DocumentPropsForm, departmentId: string | null):
name: form.name,
code: generateTemplateCode(form.name),
version: form.versionNo || 'v1.0.0',
// 新建落在「草稿」:正文还没写,此时不该出现在"可发起签署"里。
// 原型也是这个口径——新建后先写正文,再由卡片上的「提交审核」送审。
status: 'draft',
backendStatus: 'DRAFT',
// 新建即「启用中」:状态只剩两档,没有草稿这一档了。
// 正文还没写时不该发起签署,但这道闸改由正文校验把守(见 toSigningTemplate),
// 不再靠状态去挡——否则新建的文书会先显示成「停用」,科室以为是被人下线的。
status: 'published',
backendStatus: 'PUBLISHED',
updatedBy: '本地演示',
updatedAt: new Date().toISOString(),
category: form.category,
@@ -752,7 +752,13 @@ async function handleContentSubmit(form: DocumentContentForm) {
/* ---------------- 停用 / 启用 ---------------- */
/** 演示模式:就地翻转状态(停用 ↔ 启用) */
/**
* 演示模式:就地翻转状态(停用 ↔ 启用中)。
*
* 停用一侧落 backendStatus = 'DISABLED'(后端真实的停用取值)。
* 归一的 status 只能写 'archived' —— api 层的 DocumentStatus 没有 disabled 这一项,
* 而 readLibraryStatus 优先读 backendStatus,所以显示口径仍然是准确的。
*/
function toggleLocalStatus(template: DocumentTemplate) {
const next: Pick<DocumentRecord, 'status' | 'backendStatus'> =
readLibraryStatus(template) === '停用'
@@ -776,17 +782,12 @@ function toggleLocalStatus(template: DocumentTemplate) {
* 真实模式走后端版本动作(停用 → disable,启用 → publish),前端不自行改写 status:
* 「谁在什么时候把这份文书放行到可签署 / 什么时候下线的」必须由后端留痕。
*
* 原型 V10.2 已经抛弃「发布 → 审核」那条工作流,所以这里只剩这一个状态动作;
* 「提交审核」只在演示模式露出(见 DocumentTemplateCard)。
* 这是页面上**唯一**的状态动作:状态只有两档,两档之间都能来回切,因此没有
* 「某些档位不能停用 / 启用」这种分支了。
*/
async function handleToggleStatus(template: DocumentTemplate) {
const status = readLibraryStatus(template)
if (!canToggleStatus(status)) {
ElMessage.info(`「${status}」的文书没有停用 / 启用这一档,请用「提交审核」或「编辑正文」`)
return
}
if (!canToggleTemplate.value) {
ElMessage.warning('当前账号没有停用 / 启用文书的权限')
return
@@ -829,34 +830,6 @@ async function handleToggleStatus(template: DocumentTemplate) {
}
}
/**
* 提交审核。
*
* 只在演示模式露出:后端当前没有独立的「提交审核」按钮码,
* 真实模式下这一步属于审核工作流,原型 V10.2 已经把它从科室侧拿掉了。
* 演示库里把它落成「需复审」,让这条状态在页签上真的能看见。
*/
function handleSubmitReview(template: DocumentTemplate) {
if (!canWriteDocument(DOC_TEMPLATE_PERMISSIONS.publish)) {
ElMessage.warning('当前账号没有提交审核的权限')
return
}
library.value.forEach((department) =>
department.categories.forEach((category) =>
category.documents.forEach((document) => {
if (document.id === template.id) {
document.status = 'review'
document.backendStatus = 'PENDING_REVIEW'
document.updatedAt = new Date().toISOString()
}
}),
),
)
ElMessage.success('已提交审核,审核通过后自动发布')
}
/* ---------------- 历史记录 ---------------- */
/**
@@ -959,7 +932,8 @@ async function viewHistoryVersion(versionId: string) {
* 把文书转成签署模板。
*
* 转之前要确保有正文:真实模式的列表接口不返回正文,缺正文时发起签署会签出一份
* 与模板无关的空文书。因此这里在缺正文时先取回,取不到就拒绝发起。
* 与模板无关的空文书。因此这里在缺正文时先取回,取不到就拒绝发起;
* 演示模式没有「取回」这一步,但同样在缺正文时拒绝——新建的文书正是这种。
*/
async function toSigningTemplate(template: DocumentTemplate): Promise<SigningTemplate | null> {
if (!useMockLibrary && !template.currentVersionId) {
@@ -984,6 +958,11 @@ async function toSigningTemplate(template: DocumentTemplate): Promise<SigningTem
ElMessage.error('未能取到该文书的正文内容,无法发起签署')
return null
}
} else if (!contentHtml) {
// 演示模式:状态只剩两档,新建的文书直接落在「启用中」,
// 于是「有没有正文」成了唯一那道闸——空正文签出来的是一份与模板无关的白纸。
ElMessage.error('该文书还没有正文,请先点「编辑正文」编写内容后再发起签署')
return null
}
return {
@@ -1132,13 +1111,11 @@ onMounted(() => {
v-for="template in filteredTemplates"
:key="template.id"
:template="template"
:is-mock="useMockLibrary"
@preview="handlePreview"
@edit-props="openPropsDialog"
@edit-content="openContentEditor"
@history="openHistory"
@toggle-status="handleToggleStatus"
@submit-review="handleSubmitReview"
@initiate="openSigningDialog"
/>
</div>
@@ -1,93 +1,68 @@
/**
* 文档库的展示口径:状态、归属、文档类型。
*
* 状态口径取自原型 V10.2 的 libStatuses(启用中 / 草稿 / 归档 / 需复审 / 停用),
* **不是**后端模板状态枚举(DRAFT / PENDING_REVIEW / APPROVED / PUBLISHED /
* DISABLED / ARCHIVED)。原型已经彻底抛弃「发布 → 审核」那条工作流,
* 页签若继续按后端枚举铺开,界面上会多出一堆科室看不懂也点不动的状态。
* 状态只有**两档**:启用中 / 停用(2026-09-28 收敛)。
* 原先沿用原型 V10.2 的 libStatuses(启用中 / 草稿 / 归档 / 需复审 / 停用),
* 那套带着「发布 → 审核」的工作流;后端已确认不要审核链路、状态会重写,
* 因此前端先收成两个页签,不再有草稿 / 归档 / 需复审。
*
* 因此这里只做一件事:把后端原始状态**折叠**成原型口径。
* 后端模板状态枚举(DRAFT / PENDING_REVIEW / APPROVED / PUBLISHED / DISABLED /
* ARCHIVED)在接口重写前仍会出现,因此这里保留一次**折叠**:把原始状态压成两档。
* 反向映射(口径 → 后端)只保留「按状态过滤」这一处需要,见 toBackendStatusFilter。
*/
import type { DocumentRecord, DocumentStatus } from '@/api/management/types'
/** 文档库页签与卡片上的状态,取值即显示文案 */
export type DocumentLibraryStatus = '启用中' | '草稿' | '归档' | '需复审' | '停用'
export type DocumentLibraryStatus = '启用中' | '停用'
/** 页签固定顺序,与原型 libStatuses 一致:启用中排最前,停用排最后 */
export const DOCUMENT_LIBRARY_STATUSES: DocumentLibraryStatus[] = [
'启用中',
'草稿',
'归档',
'需复审',
'停用',
]
/** 页签固定顺序:启用中排最前,停用排最后 */
export const DOCUMENT_LIBRARY_STATUSES: DocumentLibraryStatus[] = ['启用中', '停用']
/** 状态色调:决定徽标配色(与原型 stDot 的取色一致) */
export type DocumentStatusTone = 'enabled' | 'draft' | 'archived' | 'review' | 'stopped'
/** 状态色调:决定徽标配色 */
export type DocumentStatusTone = 'enabled' | 'stopped'
const STATUS_TONES: Record<DocumentLibraryStatus, DocumentStatusTone> = {
启用中: 'enabled',
草稿: 'draft',
归档: 'archived',
需复审: 'review',
停用: 'stopped',
}
/** 原型 stDot:状态徽标前的圆点颜色 */
/** 状态徽标前的圆点颜色 */
export const DOCUMENT_STATUS_DOT: Record<DocumentStatusTone, string> = {
enabled: '#0f9d6c',
draft: '#8a9aa3',
archived: '#8a9aa3',
review: '#d9821f',
stopped: '#c0c8cd',
}
/**
* 后端原始状态 → 原型口径。
* 后端原始状态 → 页面两档口径。
*
* 折叠是有损的,且是刻意的:
* - APPROVED 与 PUBLISHED 都算「启用中」——原型没有「审核通过但未发布」这一档;
* - PENDING_REVIEW 与 REJECTED 都算「需复审」——原型只给一个「需复审」页签,
* 至于「还等着审」还是「已被打回」,由卡片上的操作按钮与历史记录去区分;
* - DISABLED 算「停用」而不是「归档」:停用是可逆的(还能启用),归档不是。
* 折叠规则只有一条:**能确定已下线的算「停用」,其余一律「启用中」**。
* - DISABLED / ARCHIVED 都是下线语义(前者可再启用,后者是终态),一并算「停用」;
* - DRAFT / PENDING_REVIEW / REJECTED / APPROVED / PUBLISHED 与未知取值都算「启用中」:
* 审核链路已废弃,新建的文书也直接按「启用中」处理(不再有草稿这一档)。
*
* 未知状态按「草稿」兜底而不是原样显示:口径是页签的键,多出一个键就多出一个
* 原型上没有的页签;草稿是最安全的一档(既不显示为可发起,也不谎称已停用)。
* 兜底取「启用中」而不是「停用」:口径只有两档时,把不认识的状态显示成「停用」
* 会让科室以为文书被下线了,而它其实只是状态取值没对齐——那是更坏的误报。
*/
export function readLibraryStatus(record: {
status: DocumentStatus
backendStatus?: string
}): DocumentLibraryStatus {
switch (record.backendStatus) {
case 'PUBLISHED':
case 'APPROVED':
return '启用中'
case 'PENDING_REVIEW':
case 'REJECTED':
return '需复审'
case 'ARCHIVED':
return '归档'
case 'DISABLED':
case 'ARCHIVED':
return '停用'
case 'DRAFT':
return '草稿'
case 'PENDING_REVIEW':
case 'REJECTED':
case 'APPROVED':
case 'PUBLISHED':
return '启用中'
default:
break
}
// 没有 backendStatus 的历史数据(演示文库、早期本地缓存)按归一后的状态反推
switch (record.status) {
case 'published':
return '启用中'
case 'review':
return '需复审'
case 'archived':
return '归档'
case 'draft':
default:
return '草稿'
}
return record.status === 'archived' ? '停用' : '启用中'
}
export function readStatusTone(status: DocumentLibraryStatus): DocumentStatusTone {
@@ -99,35 +74,25 @@ export function readStatusTone(status: DocumentLibraryStatus): DocumentStatusTon
*
* 只用于「真实模式按状态向服务端过滤」;页面上默认仍是一次拉全量再本地过滤
* (口径折叠后一个页签对应多个后端状态,服务端做不到一次查全)。
* 映射不上的(需复审要同时含 PENDING_REVIEW 与 REJECTED)返回 undefined,
* 后端接口重写后这里的取值要跟着改;改不动就返回 undefined,
* 调用方据此退回本地过滤,而不是拿一个错的状态去查。
*/
export function toBackendStatusFilter(status: DocumentLibraryStatus): string | undefined {
switch (status) {
case '启用中':
return 'PUBLISHED'
case '草稿':
return 'DRAFT'
case '归档':
return 'ARCHIVED'
case '停用':
return 'DISABLED'
case '需复审':
default:
return undefined
}
}
/** 只有「启用中」的文书能发起签署:草稿没定稿,停用/归档已下线 */
/** 只有「启用中」的文书能发起签署:停用已下线 */
export function canInitiateSigning(status: DocumentLibraryStatus): boolean {
return status === '启用中'
}
/** 停用 / 启用只在这两档之间来回(原型 setDocStatus 的取值口径) */
export function canToggleStatus(status: DocumentLibraryStatus): boolean {
return status === '启用中' || status === '停用'
}
/**
* 文档归属:原型把「通用(全院)」当作科室清单里的一个选项一起勾选,
* 勾中它就等于全院通用,其余科室勾选一并作废(见 docDeptPicks / openDocProps)。
@@ -30,16 +30,15 @@ export const DOC_TEMPLATE_PERMISSIONS = {
/**
* 版本工作流动作的权限码映射。
*
* 原型 V10.2 已经抛弃「发布 → 审核」那条工作流,页面上只剩三类动作:
* - 停用 / 启用(停用 → disable,启用 → publish);
* - 草稿提交审核(仅在演示模式露出,真实模式不给这个入口);
* - 归档(后端没有 DELETE,删除语义靠它承担,页面暂不暴露)。
* 页面上只剩**一个**状态动作:停用 / 启用(停用 → disable,启用 → publish)。
* 审核链路(提交审核 / 审核通过 / 驳回)已废弃,页面不再有对应入口;
* 归档(archive)页面暂不暴露,但 api 层的 deleteTemplate 仍用它承担删除语义。
*
* 后端当前没有独立的「提交审核 / 审核通过 / 驳回」按钮码,因此按职责归并:
* - 提交审核 / 重新提交归入编辑权限:改稿并送审是编辑流程的一步;
* 后端没有为每个动作单列按钮码,因此按职责归并:
* - 发布 / 重新启用归入发布权限:决定文书能否上线;
* - 停用 / 归档归入停用权限。
* 后端补齐独立按钮码后只需改这张表,页面不需要调整。
* 这张表的类型是 Record<TemplateVersionAction, string>,必须覆盖全部动作取值,
* 页面用不到的几项也留着——后端补齐独立按钮码后只需改这张表,页面不需要调整。
*/
export const TEMPLATE_WORKFLOW_PERMISSIONS: Record<TemplateVersionAction, string> = {
'submit-review': DOC_TEMPLATE_PERMISSIONS.edit,
@@ -1,7 +1,7 @@
/**
* 文档库页面模型。
*
* 状态口径见 ./library:页面用原型 V10.2 的「启用中 / 草稿 / 归档 / 需复审 / 停用」,
* 状态口径见 ./library:页面只有「启用中 / 停用」两档,
* 后端模板状态枚举只在 API 边界出现一次(readLibraryStatus 折叠)。
*
* 「属性」与「正文」是两份表单、两个弹窗:原型 V10.2 刻意把「修改文档」拆成
@@ -65,9 +65,9 @@ export interface DocumentContentForm {
}
/**
* 状态筛选的取值:按原型口径,不再按后端原始状态。
* 折叠后「需复审」同时含 PENDING_REVIEW 与 REJECTED,按后端枚举做页签会把
* 「还等着审」和「已被打回」拆成两个页签,而原型只有一个。
* 状态筛选的取值:页面口径,不是后端原始状态。
* 折叠后一个页签可能对应多个后端状态(「停用」同时含 DISABLED 与 ARCHIVED),
* 按后端枚举做页签会多出科室看不懂也点不动的档位。
*/
export type DocumentFilterStatus = 'all' | DocumentLibraryStatus