feat(签署文书): 统一签署态文书样式并精简演示种子为单份麻精同意书
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user