feat(签署文书): 统一签署态文书样式并精简演示种子为单份麻精同意书

This commit is contained in:
yelan
2026-09-24 17:08:12 +08:00
parent 9a6ac93e21
commit 5ea79178cb
30 changed files with 3787 additions and 2878 deletions
@@ -3,185 +3,169 @@ import { computed } from 'vue'
import type { TemplateVersionAction } from '@/api/management/documents'
import type { DocumentTemplate } from '../types'
import { readStatusLabel, readStatusTone } from '../mock'
import {
canInitiateSigning,
canToggleStatus,
DOCUMENT_STATUS_DOT,
isGeneralOwnership,
readLibraryStatus,
readStatusTone,
} from '../library'
import { DOC_TEMPLATE_PERMISSIONS, workflowPermission } from '../permissions'
import type { DocumentTemplate } from '../types'
const props = defineProps<{
template: DocumentTemplate
/** 演示模式下「删除」是真删;真实模式下语义是下架(模板无 DELETE 接口,走归档) */
/**
* 演示模式。
*
* 真实模式下卡片只有「停用 / 启用」这一个状态动作:后端菜单资源树里的
* 「发布 / 提交审核」按钮虽在,但原型 V10.2 已经抛弃那条工作流,
* 科室侧不该再看到「提交审核」这一步(草稿的送审改由属性/正文弹窗完成,
* 或直接由医务管理放行)。
*/
isMock: boolean
}>()
const emit = defineEmits<{
preview: [template: DocumentTemplate]
edit: [template: DocumentTemplate]
versions: [template: DocumentTemplate]
'edit-props': [template: DocumentTemplate]
'edit-content': [template: DocumentTemplate]
history: [template: DocumentTemplate]
'toggle-status': [template: DocumentTemplate]
'submit-review': [template: DocumentTemplate]
initiate: [template: DocumentTemplate]
remove: [template: DocumentTemplate]
workflow: [template: DocumentTemplate, action: TemplateVersionAction]
}>()
const statusLabel = computed(() => readStatusLabel(props.template))
const status = computed(() => readLibraryStatus(props.template))
const tone = computed(() => readStatusTone(status.value))
const dotColor = computed(() => DOCUMENT_STATUS_DOT[tone.value])
/** 配色按后端状态分组取(待审核与已驳回不能共用一个颜色) */
const statusTone = computed(() => readStatusTone(props.template))
const isGeneral = computed(() => isGeneralOwnership(props.template))
const ownershipLabel = computed(() =>
isGeneral.value ? '🌐 全院通用' : `🏥 ${props.template.departmentName ?? '未知科室'}`,
)
/** 只有已发布 / 待复审的模板可以发起签署;草稿与已归档不可用 */
const hasInitiableVersion = computed(() => Boolean(props.template.currentVersionId || props.isMock))
/**
* 归属徽标上的「·+N 科室」。
*
* sharedDepartments 只在演示文库里有值(后端 CreateTemplateRequest 只接受单个
* departmentId),所以真实模式下这里自然为 0、标签不显示后缀——
* 不给真实数据编一个后端没存的共享清单。
*/
const sharedCount = computed(() => props.template.sharedDepartments?.length ?? 0)
/** 只有「启用中」能发起签署;其余状态要么没定稿,要么已下线 */
const canInitiate = computed(() => canInitiateSigning(status.value))
const initiateTitle = computed(() =>
hasInitiableVersion.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: '重新启用' }
case 'APPROVED':
return { key: 'publish', label: '发布' }
default:
return null
}
})
const canToggle = computed(() => canToggleStatus(status.value))
const toggleLabel = computed(() => (status.value === '停用' ? '启用' : '停用'))
/**
* 版本工作流次要动作。
*
* 「驳回」不能只做成主动作的反面:审核通过和驳回是审核人的一对选择,
* 必须同时可见,否则待审核的文书就只能通过、不能打回。
*/
const secondaryActions = computed<{ key: TemplateVersionAction; label: string; title: string }[]>(
() => {
if (props.template.backendStatus === 'PENDING_REVIEW') {
return [
{
key: 'reject',
label: '驳回',
title: '驳回后需填写原因,科室按意见修改并重新提交',
},
]
}
/** 草稿的「提交审核」只在演示模式露出,真实模式不给这个入口 */
const showSubmitReview = computed(() => props.isMock && status.value === '草稿')
return []
},
/** 停用/启用走的是后端版本动作:停用 → disable,启用 → publish */
const toggleAction = computed<TemplateVersionAction>(() =>
status.value === '停用' ? 'publish' : 'disable',
)
/**
* 本次交付暂不包含「版本历史」与「删除 / 下架」入口,先隐藏对应按钮。
* 权限码已按 doc:template:hist / doc:template:stop 接好,需要恢复时把开关改回 true 即可。
*/
const showVersionAction = false
const showRemoveAction = false
const removeTitle = computed(() => (props.isMock ? '删除此文书' : '下架此文书'))
// 按钮文案与权限码的对应关系集中在模板里,避免多处各写一份
const permissions = DOC_TEMPLATE_PERMISSIONS
const togglePermission = computed(() => workflowPermission(toggleAction.value))
</script>
<template>
<article class="template-card">
<div class="template-head">
<h3 :title="template.name">{{ template.name }}</h3>
<span class="template-status" :class="'status-' + statusTone">
<i aria-hidden="true" />
{{ statusLabel }}
<article class="tpl-card">
<div class="tpl-hd">
<h3 class="tpl-title" :title="template.name">{{ template.name }}</h3>
<span class="tpl-status" :class="'tone-' + tone">
<i :style="{ background: dotColor }" aria-hidden="true" />
{{ status }}
</span>
</div>
<p class="template-meta">
{{ template.code }} · {{ template.version }} · {{ template.department }}
</p>
<p class="template-description">{{ template.description }}</p>
<p class="tpl-meta">{{ template.code || '未编号' }} · {{ template.version }}</p>
<div class="template-footer">
<span class="department-tag">
<i aria-hidden="true" />
{{ template.department }}
<div class="tpl-tags">
<span class="exam-tag" :class="isGeneral ? 'is-general' : 'is-department'">
{{ ownershipLabel }}<template v-if="!isGeneral && sharedCount"> ·+{{ sharedCount }} 科室</template>
</span>
<div class="template-actions">
<button type="button" class="card-button" @click="emit('preview', template)">预览</button>
<button
v-permission="DOC_TEMPLATE_PERMISSIONS.edit"
type="button"
class="card-button"
@click="emit('edit', template)"
>
编辑
</button>
<button
v-if="showVersionAction"
v-permission="DOC_TEMPLATE_PERMISSIONS.history"
type="button"
class="card-button"
title="查看该文书的版本历史"
@click="emit('versions', template)"
>
版本
</button>
<button
v-if="hasInitiableVersion"
v-permission="DOC_TEMPLATE_PERMISSIONS.initiate"
type="button"
class="card-button primary"
:title="initiateTitle"
@click="emit('initiate', template)"
>
发起
</button>
<!-- 还不能签署时,把「当前该做的下一步」放在同一位置,避免按钮越堆越多 -->
<button
v-if="primaryAction"
v-permission="workflowPermission(primaryAction.key)"
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"
v-permission="workflowPermission(action.key)"
type="button"
class="card-button"
:title="action.title"
@click="emit('workflow', template, action.key)"
>
{{ action.label }}
</button>
<button
v-if="showRemoveAction"
v-permission="DOC_TEMPLATE_PERMISSIONS.stop"
type="button"
class="card-button danger"
:title="removeTitle"
@click="emit('remove', template)"
>
{{ isMock ? '删除' : '下架' }}
</button>
</div>
</div>
<p class="tpl-desc">{{ template.description || '—' }}</p>
<div class="tpl-ft">
<button type="button" class="card-button" @click="emit('preview', template)">预览</button>
<button
v-permission="permissions.props"
type="button"
class="card-button"
@click="emit('edit-props', template)"
>
编辑属性
</button>
<button
v-permission="permissions.edit"
type="button"
class="card-button"
@click="emit('edit-content', template)"
>
编辑正文
</button>
<button
v-permission="permissions.history"
type="button"
class="card-button"
@click="emit('history', template)"
>
历史记录
</button>
<span class="ft-spacer" />
<button
v-if="canToggle"
v-permission="togglePermission"
type="button"
class="card-button"
:title="
toggleLabel === '停用'
? '停用后各科室不可见、不可发起签署'
: '启用后各科室可见并可发起签署'
"
@click="emit('toggle-status', template)"
>
{{ 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"
type="button"
class="card-button primary"
:title="initiateTitle"
@click="emit('initiate', template)"
>
发起
</button>
</div>
</article>
</template>
<style scoped>
.template-card {
.tpl-card {
display: flex;
flex-direction: column;
gap: 10px;
@@ -193,24 +177,22 @@ const removeTitle = computed(() => (props.isMock ? '删除此文书' : '下架
box-shadow: var(--sh);
transition:
border-color 0.15s ease,
box-shadow 0.15s ease,
transform 0.15s ease;
box-shadow 0.15s ease;
}
.template-card:hover {
.tpl-card:hover {
border-color: var(--brand);
box-shadow: var(--sh-hover);
transform: translateY(-1px);
box-shadow: 0 6px 18px rgb(14 110 140 / 10%);
}
.template-head {
.tpl-hd {
display: flex;
gap: 10px;
align-items: flex-start;
justify-content: space-between;
}
.template-head h3 {
.tpl-title {
display: -webkit-box;
flex: 1;
margin: 0;
@@ -222,7 +204,7 @@ const removeTitle = computed(() => (props.isMock ? '删除此文书' : '下架
-webkit-line-clamp: 2;
}
.template-status {
.tpl-status {
display: inline-flex;
flex-shrink: 0;
gap: 5px;
@@ -234,41 +216,31 @@ const removeTitle = computed(() => (props.isMock ? '删除此文书' : '下架
border-radius: 12px;
}
.template-status i {
.tpl-status i {
width: 6px;
height: 6px;
background: currentColor;
border-radius: 50%;
}
.status-published {
/* 徽标底色沿用原型 tg-* 那套色调;圆点颜色由内联样式给出(见 DOCUMENT_STATUS_DOT) */
.tone-enabled {
color: var(--ok);
background: var(--ok-l);
}
.status-draft {
color: #8a9aa3;
background: #eceef0;
}
.status-archived {
color: #8a9aa3;
background: #eceef0;
}
.status-review {
.tone-review {
color: var(--warn);
background: var(--warn-l);
}
/* 已驳回要和「待审核」在颜色上分开:两者归一后都是 review,
只靠文字区分时一眼扫过去仍会混为一谈 */
.status-rejected {
color: var(--err);
background: var(--err-l);
.tone-draft,
.tone-archived,
.tone-stopped {
color: #8a9aa3;
background: #eceef0;
}
.template-meta {
.tpl-meta {
margin: 0;
overflow: hidden;
color: var(--mut);
@@ -277,9 +249,36 @@ const removeTitle = computed(() => (props.isMock ? '删除此文书' : '下架
white-space: nowrap;
}
.template-description {
flex: 1;
.tpl-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
/* 归属徽标:全院通用用蓝、科室用绿,与原型 tpl-card 的取色一致 */
.exam-tag {
display: inline-flex;
align-items: center;
padding: 2px 8px;
font-size: 10.5px;
font-weight: 700;
white-space: nowrap;
border-radius: 10px;
}
.exam-tag.is-general {
color: #3a7bd5;
background: #e7effb;
}
.exam-tag.is-department {
color: #0b7a4b;
background: #e2f5ec;
}
.tpl-desc {
display: -webkit-box;
flex: 1;
min-height: 58px;
margin: 0;
overflow: hidden;
@@ -290,90 +289,50 @@ const removeTitle = computed(() => (props.isMock ? '删除此文书' : '下架
-webkit-line-clamp: 3;
}
.template-footer {
.tpl-ft {
display: flex;
gap: 10px;
align-items: center;
justify-content: space-between;
margin-top: 4px;
}
.department-tag {
display: inline-flex;
flex-shrink: 0;
gap: 5px;
align-items: center;
padding: 3px 9px;
color: var(--brand);
font-size: 11px;
font-weight: 700;
background: var(--brand-l);
border-radius: 12px;
}
.department-tag i {
width: 5px;
height: 5px;
background: currentColor;
border-radius: 50%;
}
/* 按钮数量随状态变化(待审核时多一个「驳回」),允许换行并右对齐,
否则窄屏下最后一个按钮会被挤出卡片 */
.template-actions {
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
gap: 6px;
justify-content: flex-end;
align-items: center;
padding-top: 10px;
margin-top: 4px;
border-top: 1px solid var(--line);
}
/* 把状态动作与发起推到右侧,左侧四个入口(预览/属性/正文/历史)成一组 */
.ft-spacer {
flex: 1 1 auto;
}
.card-button {
padding: 4px 9px;
color: var(--brand);
color: var(--ink);
font-size: 12px;
background: #fff;
border: 1px solid var(--brand);
border: 1px solid var(--line);
border-radius: 5px;
}
.card-button:hover {
background: var(--brand-l);
color: var(--brand);
border-color: var(--brand);
}
.card-button.primary {
color: #fff;
background: var(--brand);
border-color: var(--brand);
}
.card-button.primary:hover {
color: #fff;
background: var(--brand-d);
}
.card-button:disabled {
color: #aebbc0;
background: #f1f5f6;
border-color: #dce5e8;
cursor: not-allowed;
}
.card-button.primary:disabled:hover {
background: #f1f5f6;
}
.card-button.danger {
color: #b3442f;
border-color: #cf7c68;
}
.card-button.danger:hover {
background: #fbeee9;
border-color: var(--brand-d);
}
@media (max-width: 680px) {
.template-footer {
align-items: flex-start;
flex-direction: column;
.ft-spacer {
display: none;
}
}
</style>