339 lines
8.3 KiB
Vue
339 lines
8.3 KiB
Vue
<script setup lang="ts">
|
||
import { computed } from 'vue'
|
||
|
||
import type { TemplateVersionAction } from '@/api/management/documents'
|
||
|
||
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
|
||
/**
|
||
* 演示模式。
|
||
*
|
||
* 真实模式下卡片只有「停用 / 启用」这一个状态动作:后端菜单资源树里的
|
||
* 「发布 / 提交审核」按钮虽在,但原型 V10.2 已经抛弃那条工作流,
|
||
* 科室侧不该再看到「提交审核」这一步(草稿的送审改由属性/正文弹窗完成,
|
||
* 或直接由医务管理放行)。
|
||
*/
|
||
isMock: boolean
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
preview: [template: DocumentTemplate]
|
||
'edit-props': [template: DocumentTemplate]
|
||
'edit-content': [template: DocumentTemplate]
|
||
history: [template: DocumentTemplate]
|
||
'toggle-status': [template: DocumentTemplate]
|
||
'submit-review': [template: DocumentTemplate]
|
||
initiate: [template: DocumentTemplate]
|
||
}>()
|
||
|
||
const status = computed(() => readLibraryStatus(props.template))
|
||
const tone = computed(() => readStatusTone(status.value))
|
||
const dotColor = computed(() => DOCUMENT_STATUS_DOT[tone.value])
|
||
|
||
const isGeneral = computed(() => isGeneralOwnership(props.template))
|
||
const ownershipLabel = computed(() =>
|
||
isGeneral.value ? '🌐 全院通用' : `🏥 ${props.template.departmentName ?? '未知科室'}`,
|
||
)
|
||
|
||
/**
|
||
* 归属徽标上的「·+N 科室」。
|
||
*
|
||
* sharedDepartments 只在演示文库里有值(后端 CreateTemplateRequest 只接受单个
|
||
* departmentId),所以真实模式下这里自然为 0、标签不显示后缀——
|
||
* 不给真实数据编一个后端没存的共享清单。
|
||
*/
|
||
const sharedCount = computed(() => props.template.sharedDepartments?.length ?? 0)
|
||
|
||
/** 只有「启用中」能发起签署;其余状态要么没定稿,要么已下线 */
|
||
const canInitiate = computed(() => canInitiateSigning(status.value))
|
||
const initiateTitle = computed(() =>
|
||
canInitiate.value ? '用此文书发起签署' : '仅「启用中」的文书可发起签署',
|
||
)
|
||
|
||
const canToggle = computed(() => canToggleStatus(status.value))
|
||
const toggleLabel = computed(() => (status.value === '停用' ? '启用' : '停用'))
|
||
|
||
/** 草稿的「提交审核」只在演示模式露出,真实模式不给这个入口 */
|
||
const showSubmitReview = computed(() => props.isMock && status.value === '草稿')
|
||
|
||
/** 停用/启用走的是后端版本动作:停用 → disable,启用 → publish */
|
||
const toggleAction = computed<TemplateVersionAction>(() =>
|
||
status.value === '停用' ? 'publish' : 'disable',
|
||
)
|
||
|
||
// 按钮文案与权限码的对应关系集中在模板里,避免多处各写一份
|
||
const permissions = DOC_TEMPLATE_PERMISSIONS
|
||
const togglePermission = computed(() => workflowPermission(toggleAction.value))
|
||
</script>
|
||
|
||
<template>
|
||
<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="tpl-meta">{{ template.code || '未编号' }} · {{ template.version }}</p>
|
||
|
||
<div class="tpl-tags">
|
||
<span class="exam-tag" :class="isGeneral ? 'is-general' : 'is-department'">
|
||
{{ ownershipLabel }}<template v-if="!isGeneral && sharedCount"> ·+{{ sharedCount }} 科室</template>
|
||
</span>
|
||
</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>
|
||
.tpl-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
min-width: 0;
|
||
padding: 16px;
|
||
background: var(--card);
|
||
border: 1px solid transparent;
|
||
border-radius: var(--r);
|
||
box-shadow: var(--sh);
|
||
transition:
|
||
border-color 0.15s ease,
|
||
box-shadow 0.15s ease;
|
||
}
|
||
|
||
.tpl-card:hover {
|
||
border-color: var(--brand);
|
||
box-shadow: 0 6px 18px rgb(14 110 140 / 10%);
|
||
}
|
||
|
||
.tpl-hd {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.tpl-title {
|
||
display: -webkit-box;
|
||
flex: 1;
|
||
margin: 0;
|
||
overflow: hidden;
|
||
color: var(--ink);
|
||
font-size: 14px;
|
||
line-height: 1.4;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 2;
|
||
}
|
||
|
||
.tpl-status {
|
||
display: inline-flex;
|
||
flex-shrink: 0;
|
||
gap: 5px;
|
||
align-items: center;
|
||
padding: 3px 9px;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
white-space: nowrap;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.tpl-status i {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
/* 徽标底色沿用原型 tg-* 那套色调;圆点颜色由内联样式给出(见 DOCUMENT_STATUS_DOT) */
|
||
.tone-enabled {
|
||
color: var(--ok);
|
||
background: var(--ok-l);
|
||
}
|
||
|
||
.tone-review {
|
||
color: var(--warn);
|
||
background: var(--warn-l);
|
||
}
|
||
|
||
.tone-draft,
|
||
.tone-archived,
|
||
.tone-stopped {
|
||
color: #8a9aa3;
|
||
background: #eceef0;
|
||
}
|
||
|
||
.tpl-meta {
|
||
margin: 0;
|
||
overflow: hidden;
|
||
color: var(--mut);
|
||
font-size: 11.5px;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.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;
|
||
color: var(--mut);
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 3;
|
||
}
|
||
|
||
.tpl-ft {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
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(--ink);
|
||
font-size: 12px;
|
||
background: #fff;
|
||
border: 1px solid var(--line);
|
||
border-radius: 5px;
|
||
}
|
||
|
||
.card-button:hover {
|
||
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);
|
||
border-color: var(--brand-d);
|
||
}
|
||
|
||
@media (max-width: 680px) {
|
||
.ft-spacer {
|
||
display: none;
|
||
}
|
||
}
|
||
</style>
|