feat(documents): 支持全院通用和院区通用归属选择与展示

This commit is contained in:
xy
2026-10-07 19:22:34 +08:00
parent 46bbbfee06
commit 98c94ad3d6
8 changed files with 130 additions and 56 deletions
+7 -1
View File
@@ -107,7 +107,13 @@ function mapDocument(dto: TemplateResponseDto): DocumentRecord {
versionId,
currentVersionId: dto.currentVersionId ?? undefined,
departmentId: dto.departmentId,
departmentName: dto.departmentId ? undefined : '全院通用',
ownershipScope:
dto.ownershipScope ?? (dto.departmentId ? 'DEPARTMENT' : dto.campusId ? 'CAMPUS' : 'GLOBAL'),
departmentName: dto.departmentId
? undefined
: dto.ownershipScope === 'GLOBAL'
? '全院通用'
: '院区通用',
campusId: dto.campusId,
description: dto.description ?? '',
// 后端补齐这两个字段后卡片会自动显示「检查类型 / 关联项目」;
+7 -3
View File
@@ -10,6 +10,7 @@ export interface DocumentQuery extends PageQuery {
}
export interface DocumentRecord {
ownershipScope?: 'GLOBAL' | 'CAMPUS' | 'DEPARTMENT'
id: string
name: string
code?: string
@@ -75,11 +76,12 @@ export type BackendTemplateStatus =
| string
export interface TemplateResponseDto {
ownershipScope?: 'GLOBAL' | 'CAMPUS' | 'DEPARTMENT'
id: string
templateCode: string
name: string
description?: string | null
campusId: string
campusId: string | null
departmentId?: string | null
category?: string | null
/**
@@ -131,20 +133,22 @@ export interface TemplateVersionResponseDto {
}
export interface CreateTemplateRequest {
ownershipScope?: 'GLOBAL' | 'CAMPUS' | 'DEPARTMENT'
templateCode: string
name: string
description?: string
campusId: string
campusId: string | null
departmentId?: string | null
category?: string
}
export interface UpdateTemplateRequest {
ownershipScope?: 'GLOBAL' | 'CAMPUS' | 'DEPARTMENT'
name: string
description?: string
category?: string
/** 修改归属时提供院区;不传则保留原归属。 */
campusId?: string
campusId?: string | null
/** 提供院区时,null 表示该院区通用。 */
departmentId?: string | null
}
+6 -2
View File
@@ -780,9 +780,13 @@ function mapAvailableTemplate(dto: AvailableTemplateVersionResponseDto): Signing
name: dto.templateName,
code: dto.templateCode,
version: dto.versionNo,
department: dto.departmentId ? '指定科室' : '全院通用',
department: dto.departmentId
? '指定科室'
: dto.ownershipScope === 'GLOBAL'
? '全院通用'
: '院区通用',
departmentId: dto.departmentId,
campusId: dto.campusId,
campusId: dto.campusId ?? undefined,
category: dto.category || '未分类',
description: `已发布版本 ${dto.versionNo}`,
// 可用模板接口不返回方式 ACL;四种方式都放行,实际允许范围由后端在创建时校验。
+2 -1
View File
@@ -477,12 +477,13 @@ export interface VisitResponseDto {
}
export interface AvailableTemplateVersionResponseDto {
ownershipScope?: 'GLOBAL' | 'CAMPUS' | 'DEPARTMENT'
templateId: string
templateVersionId: string
templateCode: string
templateName: string
category: string
campusId: string
campusId: string | null
departmentId: string | null
versionNo: string
contentSha256: string
@@ -4,7 +4,14 @@ import { ElMessage } from 'element-plus'
import type { CampusRecord, DepartmentRecord } from '@/api/management/types'
import { DEFAULT_DOCUMENT_TYPE, DOCUMENT_TYPES, GENERAL_OWNERSHIP } from '../library'
import {
DEFAULT_DOCUMENT_TYPE,
DOCUMENT_TYPES,
GENERAL_OWNERSHIP,
campusOwnership,
isGlobalOwnership,
ownershipCampusId,
} from '../library'
import { DOC_TEMPLATE_PERMISSIONS } from '../permissions'
import type { DocumentPropsForm, DocumentTemplate } from '../types'
@@ -17,7 +24,7 @@ const props = defineProps<{
/** 院区字典,与组织架构页同源;用来给上面的科室分组 */
campuses: CampusRecord[]
/**
* 是否可以把文档设为「通用(全院)」。
* 是否可以把文档设为「全院通用」。
*
* 原型 canSetGeneral() 只放给系统管理员 / 医务管理;这里由页面判定后传进来,
* 弹窗不自己去读角色——弹窗只负责表单。
@@ -57,7 +64,7 @@ const form = reactive<DocumentPropsForm>({
})
interface OwnershipOption {
/** 落库值:科室取 DepartmentRecord.id,「通用(全院)」取固定哨兵 */
/** 落库值:科室取 DepartmentRecord.id,「全院通用」取固定哨兵 */
value: string
label: string
}
@@ -74,7 +81,7 @@ interface OwnershipGroup {
* 组织架构里的科室本来就挂在院区下面(同名科室在不同院区是两条记录),
* 只摊平显示名字的话既看不出属于哪个院区、勾了也定位不到唯一的科室 id,
* 所以这里按科室的 campusId 归到对应院区下:显示院区名 + 科室名,取值用科室 id。
* 「通用(全院)」不是科室、没有院区,单独放最后一组。
* 「全院通用」不是科室、没有院区,单独放最后一组。
*/
const ownershipGroups = computed<OwnershipGroup[]>(() => {
const groups: OwnershipGroup[] = []
@@ -84,6 +91,8 @@ const ownershipGroups = computed<OwnershipGroup[]>(() => {
.filter((department) => department.campusId === campus.id)
.map((department) => ({ value: department.id, label: department.name }))
if (props.canSetGeneral)
options.unshift({ value: campusOwnership(campus.id), label: '院区通用' })
if (options.length) {
groups.push({ key: campus.id, title: campus.name, options })
}
@@ -114,14 +123,18 @@ const generalSelected = computed(() => form.ownership.includes(GENERAL_OWNERSHIP
/** option 是勾选值:科室 id,或 GENERAL_OWNERSHIP */
function toggleOwnership(option: string) {
if (option.startsWith('__campus_common__:')) {
form.ownership = form.ownership.includes(option) ? [] : [option]
return
}
if (option === GENERAL_OWNERSHIP) {
// 勾上「通用(全院)」就清掉科室勾选,取消则回到空选择——
// 勾上「全院通用」就清掉科室勾选,取消则回到空选择——
// 两种选择混在一起会让「到底归属哪儿」没有唯一答案
form.ownership = generalSelected.value ? [] : [GENERAL_OWNERSHIP]
return
}
if (generalSelected.value) {
if (generalSelected.value || ownershipCampusId(form.ownership)) {
form.ownership = []
}
@@ -157,7 +170,9 @@ watch(
? template.departmentId
? // 回填主科室用 departmentId 本身:勾选值就是科室 id,不再经名字中转
[template.departmentId]
: [GENERAL_OWNERSHIP]
: isGlobalOwnership(template)
? [GENERAL_OWNERSHIP]
: [campusOwnership(template.campusId ?? '')]
: // 新增时预选当前用户所属科室(对得上就直接用,对不上就空着让科室自己勾)
[],
description: template?.description ?? '',
@@ -251,11 +266,9 @@ function submit() {
</div>
</div>
<p class="field-tip">
<template v-if="isMock">
勾选科室设置可见范围,支持多科室与通用(全院)。
</template>
<template v-if="isMock"> 勾选科室设置可见范围,支持多科室与全院通用。 </template>
<template v-else>
选择一个归属科室,或选择「通用(全院)」供文档所属院区使用。归属仍受账号权限限制。
全院通用可供所有院区使用;院区通用仅供对应院区使用;也可选择一个专用科室。查看和维护仍受账号权限限制。
</template>
</p>
</div>
@@ -448,7 +461,7 @@ function submit() {
/*
* 文档归属:原型 V10.2 的 .cg-dept —— 卡片式复选框网格。
* 容器是浅底带框的 3 列网格,每项一张白卡(原生 checkbox + 科室名),
* 「通用(全院)」独占一行并用浅蓝底区分(原型 grid-column:1/-1)。
* 「全院通用」独占一行并用浅蓝底区分(原型 grid-column:1/-1)。
* 科室名过长时截断:原型同一处就是 nowrap + ellipsis。
*
* 与原型唯一的差别:这里按院区分成若干个 .ownership(一个院区一个),
@@ -49,7 +49,11 @@ const dotColor = computed(() => DOCUMENT_STATUS_DOT[tone.value])
const isGeneral = computed(() => isGeneralOwnership(props.template))
const ownershipLabel = computed(() =>
isGeneral.value ? '🌐 全院通用' : `🏥 ${props.template.departmentName ?? '未知科室'}`,
isGeneral.value
? props.template.ownershipScope === 'GLOBAL' || !props.template.campusId
? '🌐 全院通用'
: `🏥 ${props.template.campusName ?? '所属院区'} · 院区通用`
: `🏥 ${props.template.departmentName ?? '未知科室'}`,
)
/**
@@ -91,9 +95,7 @@ const toggleAction = computed<TemplateVersionAction | null>(() =>
// 按钮文案与权限码的对应关系集中在模板里,避免多处各写一份
const permissions = DOC_TEMPLATE_PERMISSIONS
const togglePermission = computed(() =>
workflowPermission(toggleAction.value ?? 'disable'),
)
const togglePermission = computed(() => workflowPermission(toggleAction.value ?? 'disable'))
</script>
<template>
@@ -110,7 +112,8 @@ const togglePermission = computed(() =>
<div class="tpl-tags">
<span class="exam-tag" :class="isGeneral ? 'is-general' : 'is-department'">
{{ ownershipLabel }}<template v-if="!isGeneral && sharedCount"> ·+{{ sharedCount }} 科室</template>
{{ ownershipLabel
}}<template v-if="!isGeneral && sharedCount"> ·+{{ sharedCount }} 科室</template>
</span>
<span v-if="examType" class="exam-tag is-exam">🔬 检查类型:{{ examType }}</span>
</div>
@@ -42,6 +42,9 @@ import {
canInitiateSigning,
DOCUMENT_LIBRARY_STATUSES,
GENERAL_OWNERSHIP,
ownershipCampusId,
isGlobalOwnership,
CAMPUS_OWNERSHIP_PREFIX,
readLibraryStatus,
resolveOwnershipDepartmentId,
templateWorkflowAction,
@@ -79,6 +82,7 @@ const useMockLibrary = mockFlags.documents
/** 没有绑定科室的文书归到这一组(全院通用),不属于任何一个科室页签 */
const GENERAL_GROUP_KEY = '__all__'
const CAMPUS_GROUP_KEY = '__campus_common__'
/**
* 页面操作的服务端校验兜底。
@@ -211,12 +215,16 @@ const departmentTabs = computed<DepartmentTab[]>(() => {
const knownKeys = new Set(campusDepartments.map((department) => department.id))
const orphans = library.value.filter(
(group) => group.key !== GENERAL_GROUP_KEY && !knownKeys.has(group.key),
(group) =>
group.key !== GENERAL_GROUP_KEY &&
group.key !== CAMPUS_GROUP_KEY &&
!knownKeys.has(group.key),
)
return [
{ key: null, label: '全部', count: allTemplates.value.length },
{ key: GENERAL_GROUP_KEY, label: '全院通用', count: countOf(GENERAL_GROUP_KEY) },
{ key: CAMPUS_GROUP_KEY, label: '院区通用', count: countOf(CAMPUS_GROUP_KEY) },
...campusDepartments.map((department) => ({
key: department.id,
label: allCampusesSelected.value
@@ -282,7 +290,7 @@ function cloneLibrary(source: DocumentDepartment[]): DocumentDepartment[] {
* 把扁平的模板列表组织成页面需要的 科室 → 分类 → 文书 树。
*
* 分组键使用 departmentId,并用组织接口返回的科室字典补齐名称;没有绑定科室的模板
* 归入“全院通用”。这样同一科室的模板无论接口返回顺序如何都会落进同一个分组。
* 按明确归属范围分别归入“全院通用”或“院区通用”。同一科室的模板落入同一分组。
*/
function groupDocuments(
records: DocumentRecord[],
@@ -294,10 +302,15 @@ function groupDocuments(
)
records.forEach((record) => {
const departmentKey = record.departmentId ?? GENERAL_GROUP_KEY
const departmentKey =
record.departmentId ?? (isGlobalOwnership(record) ? GENERAL_GROUP_KEY : CAMPUS_GROUP_KEY)
const departmentName =
record.departmentName ??
(record.departmentId ? (departmentNames.get(record.departmentId) ?? '未知科室') : '全院通用')
(record.departmentId
? (departmentNames.get(record.departmentId) ?? '未知科室')
: isGlobalOwnership(record)
? '全院通用'
: '院区通用')
let department = grouped.get(departmentKey)
@@ -314,7 +327,11 @@ function groupDocuments(
department.categories.push(category)
}
category.documents.push({ ...record, departmentName })
category.documents.push({
...record,
departmentName,
campusName: campuses.value.find((campus) => campus.id === record.campusId)?.name,
})
})
// 全院通用的模板排最前,其余按科室名排序,保证每次进页面分组顺序稳定
@@ -539,7 +556,12 @@ function generateTemplateCode(name: string) {
*/
function sharedDepartmentNames(ownership: string[], primaryDepartmentId: string | null): string[] {
return ownership
.filter((value) => value !== GENERAL_OWNERSHIP && value !== primaryDepartmentId)
.filter(
(value) =>
value !== GENERAL_OWNERSHIP &&
!value.startsWith(CAMPUS_OWNERSHIP_PREFIX) &&
value !== primaryDepartmentId,
)
.map((id) => departments.value.find((department) => department.id === id)?.name)
.filter((name): name is string => Boolean(name))
}
@@ -567,9 +589,11 @@ function locateLocalDocument(id: string) {
/** 演示模式:把新文书追加进本地文库(分组按科室名匹配已有分组) */
function addLocalTemplate(form: DocumentPropsForm, departmentId: string | null): DocumentTemplate {
const department = departments.value.find((item) => item.id === departmentId)
const departmentName = department?.name ?? '全院通用'
const global = form.ownership.includes(GENERAL_OWNERSHIP)
const campusId = global ? null : department?.campusId || ownershipCampusId(form.ownership)
const departmentName = department?.name ?? (global ? '全院通用' : '院区通用')
// 真实模式用 departmentId 当分组键,演示模式没有 UUID,用品名保持同样语义
const departmentKey = department?.id ?? GENERAL_GROUP_KEY
const departmentKey = department?.id ?? (global ? GENERAL_GROUP_KEY : CAMPUS_GROUP_KEY)
let departmentGroup = library.value.find((item) => item.key === departmentKey)
if (!departmentGroup) {
@@ -607,7 +631,9 @@ function addLocalTemplate(form: DocumentPropsForm, departmentId: string | null):
versionId: `local-version-${timestamp}`,
departmentId: departmentId,
departmentName,
campusId: campuses.value[0]?.id ?? '',
campusId,
ownershipScope: global ? 'GLOBAL' : departmentId ? 'DEPARTMENT' : 'CAMPUS',
campusName: campuses.value.find((campus) => campus.id === campusId)?.name,
contentHtml: '',
sharedDepartments: sharedDepartmentNames(form.ownership, departmentId),
}
@@ -626,9 +652,11 @@ function addLocalTemplate(form: DocumentPropsForm, departmentId: string | null):
*/
function updateLocalProps(form: DocumentPropsForm, departmentId: string | null) {
const target = departments.value.find((item) => item.id === departmentId)
const targetName = target?.name ?? '全院通用'
const global = form.ownership.includes(GENERAL_OWNERSHIP)
const campusId = global ? null : target?.campusId || ownershipCampusId(form.ownership)
const targetName = target?.name ?? (global ? '全院通用' : '院区通用')
// 与 addLocalTemplate 同一套分组键口径:演示模式没有 UUID 时落 GENERAL_GROUP_KEY
const targetKey = target?.id ?? GENERAL_GROUP_KEY
const targetKey = target?.id ?? (global ? GENERAL_GROUP_KEY : CAMPUS_GROUP_KEY)
const located = locateLocalDocument(form.id ?? '')
if (!located) {
@@ -644,6 +672,9 @@ function updateLocalProps(form: DocumentPropsForm, departmentId: string | null)
document.updatedAt = new Date().toISOString()
document.departmentId = departmentId
document.departmentName = targetName
document.campusId = campusId
document.campusName = campuses.value.find((campus) => campus.id === campusId)?.name
document.ownershipScope = global ? 'GLOBAL' : departmentId ? 'DEPARTMENT' : 'CAMPUS'
// 主科室进 departmentId,其余勾选科室留在 sharedDepartments(与新建时同一口径)
document.sharedDepartments = sharedDepartmentNames(form.ownership, departmentId)
@@ -705,10 +736,10 @@ async function handlePropsSubmit(form: DocumentPropsForm) {
ElMessage.error('所选科室不存在,请刷新后重试')
return
}
const campusId =
targetDepartment?.campusId ||
(form.id ? propsTemplate.value?.campusId : appStore.selectedCampusId || campuses.value[0]?.id)
if (!campusId) {
const global = form.ownership.includes(GENERAL_OWNERSHIP)
const campusId = global ? null : targetDepartment?.campusId || ownershipCampusId(form.ownership)
const ownershipScope = global ? 'GLOBAL' : departmentId ? 'DEPARTMENT' : 'CAMPUS'
if (!global && !campusId) {
ElMessage.error('无法确定文档所属院区,请选择院区后重试')
return
}
@@ -719,6 +750,7 @@ async function handlePropsSubmit(form: DocumentPropsForm) {
category: form.category,
campusId,
departmentId,
ownershipScope,
})
closePropsDialog()
await loadLibrary()
@@ -732,6 +764,7 @@ async function handlePropsSubmit(form: DocumentPropsForm) {
description: form.description,
campusId,
departmentId,
ownershipScope,
category: form.category,
})
@@ -131,22 +131,32 @@ export function templateWorkflowLabel(
case 'PUBLISHED':
return '停用'
default:
return record.backendStatus
? ''
: readLibraryStatus(record) === '停用'
? '启用'
: '停用'
return record.backendStatus ? '' : readLibraryStatus(record) === '停用' ? '启用' : '停用'
}
}
/**
* 文档归属:原型把「通用(全院)」当作科室清单里的一个选项一起勾选,
* 文档归属:原型把「全院通用」当作科室清单里的一个选项一起勾选,
* 勾中它就等于全院通用,其余科室勾选一并作废(见 docDeptPicks / openDocProps)。
*/
export const GENERAL_OWNERSHIP = '通用(全院)'
export const GENERAL_OWNERSHIP = '全院通用'
export const CAMPUS_OWNERSHIP_PREFIX = '__campus_common__:'
export function campusOwnership(campusId: string): string {
return `${CAMPUS_OWNERSHIP_PREFIX}${campusId}`
}
export function ownershipCampusId(ownership: string[]): string | null {
const value = ownership.find((item) => item.startsWith(CAMPUS_OWNERSHIP_PREFIX))
return value ? value.slice(CAMPUS_OWNERSHIP_PREFIX.length) : null
}
export function isGlobalOwnership(record: DocumentRecord): boolean {
return (
record.ownershipScope === 'GLOBAL' ||
(!record.ownershipScope && !record.campusId && !record.departmentId)
)
}
/**
* 归属勾选 → 后端可落库的科室 id(勾了通用(全院)就没有科室 id)。
* 归属勾选 → 后端可落库的科室 id(勾了全院通用就没有科室 id)。
*
* 勾选值本身就是 DepartmentRecord.id:同名科室在不同院区是两条记录,
* 这里不再拿名字去反查(反查只能取到第一条,会把院区指错)。
@@ -156,13 +166,19 @@ export function resolveOwnershipDepartmentId(ownership: string[]): string | null
return null
}
return ownership.find((value) => value !== GENERAL_OWNERSHIP) ?? null
return (
ownership.find(
(value) => value !== GENERAL_OWNERSHIP && !value.startsWith(CAMPUS_OWNERSHIP_PREFIX),
) ?? null
)
}
/** 归属徽标文案:全院通用用 🌐,科室用 🏥(与原型 tpl-card 的标签一致) */
export function readOwnershipLabel(record: DocumentRecord): string {
if (!record.departmentId) {
return '🌐 全院通用'
return isGlobalOwnership(record)
? '🌐 全院通用'
: `🏥 ${record.campusName ?? '所属院区'} · 院区通用`
}
const name = record.departmentName ?? '未知科室'
@@ -180,12 +196,6 @@ export function isGeneralOwnership(record: DocumentRecord): boolean {
* 与原型 docTypes 同一份取值,属性弹窗用下拉而不是自由输入:
* 类型是分组的键,自由输入会让「知情同意书 / 知情同意书 」这类写法分成两组。
*/
export const DOCUMENT_TYPES = [
'知情同意书',
'风险告知书',
'入院告知',
'检查预约单',
'授权委托',
]
export const DOCUMENT_TYPES = ['知情同意书', '风险告知书', '入院告知', '检查预约单', '授权委托']
export const DEFAULT_DOCUMENT_TYPE = '知情同意书'