fix(documents): 保存文档归属并支持所有院区筛选

This commit is contained in:
xy
2026-10-07 17:20:50 +08:00
parent a0baef1dbf
commit 46bbbfee06
5 changed files with 94 additions and 53 deletions
+3 -1
View File
@@ -218,7 +218,7 @@ API 模块与页面按业务域对应。工作台页面使用 `api/workbench`
- `GET /api/v1/sign-artifacts/{artifactId}/download`:原始 PDF、签署后 PDF、签名原图下载;
- `POST /api/v1/sign-deliveries/{taskId}/sms/resend`:短信重发,自动携带幂等键;
- `GET/POST /api/v1/templates`、`GET /api/v1/templates/{id}`:模板列表、详情;
- `PUT /api/v1/templates/{id}`:更新模板元数据;
- `PUT /api/v1/templates/{id}`:更新模板名称、说明、分类及归属。修改归属时提交 `campusId` 与 `departmentId`;科室为空表示该院区通用,不提交院区则保留原归属。真实模式只支持一个归属科室,选择科室时使用该科室的院区;编辑为通用时保留文档原院区。后端校验目标组织状态、院区匹配及数据权限,记录归属变更审计,保留历史签署快照;
- `GET/POST /api/v1/templates/{id}/versions`、`GET /api/v1/templates/{id}/versions/{versionId}`:版本列表、创建与版本详情(**正文 `contentHtml` 只在这个详情接口里返回**);
- `POST /api/v1/templates/{id}/versions/{versionId}/{action}`(submit-review / reject / approve / publish / disable / archive):版本工作流,模板「删除」即走 archive;
- `GET /api/v1/users`、`POST /api/v1/users`、`GET/PUT /api/v1/users/{id}`:员工列表、资料创建和资料更新;
@@ -243,6 +243,8 @@ API 模块与页面按业务域对应。工作台页面使用 `api/workbench`
## 签署记录查询
文档库院区筛选支持「所有院区」:列表查询省略 `campusId`,由后端按当前账号的角色数据范围过滤;科室标签显示院区和科室名称。此选项只影响文档库查询,全局院区及新增通用文档的目标院区仍使用具体院区,编辑通用文档仍保留原院区。
`views/workbench/records` 是「工作台 → 签署记录查询」(`/sign/records`,菜单资源 `sign/record/index`)的实现:工作台只保留当天的签署操作,历史记录的检索、核对与导出统一放在这里。页面只读——已签署的正式文书不可直接修改,需要追加内容时回签署工作台新建签署任务。
**接口现状与边界**(后端还没有「按条件检索历史签署记录」的专用接口):
+4
View File
@@ -143,6 +143,10 @@ export interface UpdateTemplateRequest {
name: string
description?: string
category?: string
/** 修改归属时提供院区;不传则保留原归属。 */
campusId?: string
/** 提供院区时,null 表示该院区通用。 */
departmentId?: string | null
}
export interface CreateTemplateVersionRequest {
@@ -4,8 +4,7 @@
*
* 原型 V10.2 只有「科室 + 状态」两行(#libTabs / #libStatus)。这里把科室那一级按
* 「组织架构」的口径拆成两级:先选院区(同一份院区字典、同一个 sortNo 顺序,
* 与架构树的院区顺序一致),再在该院区的科室里筛。院区不做「全部院区」——
* 架构树里也始终落在某一个院区上,两处口径保持一致。
* 与架构树的院区顺序一致),再按科室筛选。「所有院区」跨院区查询,科室显示院区名。
*
* 类名沿用原型的 lib-tabs / lib-tab / lib-status / lib-st:这套样式是文档库页面独有的
* 筛选语言(胶囊 + 虚线分隔),与通用 Tabs 组件的观感不同,换成通用组件会把
@@ -16,7 +15,7 @@
import type { CampusTab, DepartmentTab, DocumentFilterStatus, StatusTab } from '../types'
defineProps<{
/** 当前院区 id。院区是页面级的取数口径,由页面持有(见 index.vue) */
/** 当前院区 id;空字符串表示所有院区,由页面持有查询状态。 */
campus: string
department: string | null
status: DocumentFilterStatus
@@ -125,6 +125,11 @@ function toggleOwnership(option: string) {
form.ownership = []
}
if (!props.isMock) {
form.ownership = form.ownership.includes(option) ? [] : [option]
return
}
const index = form.ownership.indexOf(option)
if (index >= 0) {
@@ -205,7 +210,11 @@ function submit() {
<div class="form-grid">
<label class="form-field span-2">
<span>文书名称 <i>*</i></span>
<input v-model="form.name" type="text" placeholder="例:麻醉药品、精神药品知情同意书" />
<input
v-model="form.name"
type="text"
placeholder="例:麻醉药品、精神药品知情同意书"
/>
</label>
<label class="form-field">
@@ -242,17 +251,18 @@ function submit() {
</div>
</div>
<p class="field-tip">
勾选科室设置可见范围,支持多科室与通用(全院)。勾选「通用(全院)」后各科室均可见。
<template v-if="isMock">
勾选科室设置可见范围,支持多科室与通用(全院)。
</template>
<template v-else>
选择一个归属科室,或选择「通用(全院)」供文档所属院区使用。归属仍受账号权限限制。
</template>
</p>
</div>
<label class="form-field span-2">
<span>文书说明</span>
<textarea
v-model="form.description"
rows="2"
placeholder="显示在文书卡片上的简介"
/>
<textarea v-model="form.description" rows="2" placeholder="显示在文书卡片上的简介" />
</label>
</div>
</div>
@@ -265,11 +275,11 @@ function submit() {
: '属性与正文分开维护:此处只修改文档基本信息与归属;正文请在文档卡片点「编辑正文」。'
}}
</p>
<button type="button" class="footer-button secondary" @click="emit('close')">
取消
</button>
<button type="button" class="footer-button secondary" @click="emit('close')">取消</button>
<button
v-permission.disable="isCreate ? DOC_TEMPLATE_PERMISSIONS.create : DOC_TEMPLATE_PERMISSIONS.props"
v-permission.disable="
isCreate ? DOC_TEMPLATE_PERMISSIONS.create : DOC_TEMPLATE_PERMISSIONS.props
"
type="button"
class="footer-button primary"
:disabled="saving"
@@ -111,6 +111,8 @@ const libraryError = ref('')
const library = ref<DocumentDepartment[]>([])
const campuses = ref<CampusRecord[]>([])
const departments = ref<DepartmentRecord[]>([])
const allCampusesSelected = ref(false)
const libraryCampusId = computed(() => (allCampusesSelected.value ? '' : appStore.selectedCampusId))
const filters = reactive<DocumentFilterForm>({
keyword: '',
@@ -169,13 +171,16 @@ const allTemplates = computed<DocumentTemplate[]>(() =>
const campusTabs = computed<CampusTab[]>(() =>
useMockLibrary
? []
: [...campuses.value]
.filter((campus) => campus.status !== 'DISABLED')
.sort(
(left, right) =>
left.sortNo - right.sortNo || left.name.localeCompare(right.name, 'zh-CN'),
)
.map((campus) => ({ key: campus.id, label: campus.name })),
: [
{ key: '', label: '所有院区' },
...[...campuses.value]
.filter((campus) => campus.status !== 'DISABLED')
.sort(
(left, right) =>
left.sortNo - right.sortNo || left.name.localeCompare(right.name, 'zh-CN'),
)
.map((campus) => ({ key: campus.id, label: campus.name })),
],
)
/**
@@ -195,7 +200,7 @@ const departmentTabs = computed<DepartmentTab[]>(() => {
const countOf = (key: string) =>
allTemplates.value.filter((template) => template.departmentKey === key).length
const campusId = appStore.selectedCampusId
const campusId = libraryCampusId.value
const campusDepartments = [...departments.value]
.filter((department) => department.status !== 'DISABLED')
// 演示文库没有院区维度(科室字典的 campusId 是演示值,与全局院区对不上),不按院区过滤
@@ -214,7 +219,9 @@ const departmentTabs = computed<DepartmentTab[]>(() => {
{ key: GENERAL_GROUP_KEY, label: '全院通用', count: countOf(GENERAL_GROUP_KEY) },
...campusDepartments.map((department) => ({
key: department.id,
label: department.name,
label: allCampusesSelected.value
? `${campuses.value.find((campus) => campus.id === department.campusId)?.name ?? '未知院区'} · ${department.name}`
: department.name,
count: countOf(department.id),
})),
...orphans.map((group) => ({ key: group.key, label: group.name, count: countOf(group.key) })),
@@ -307,7 +314,7 @@ function groupDocuments(
department.categories.push(category)
}
category.documents.push(record)
category.documents.push({ ...record, departmentName })
})
// 全院通用的模板排最前,其余按科室名排序,保证每次进页面分组顺序稳定
@@ -330,12 +337,13 @@ function groupDocuments(
* 表现为"文档库里少了几份文书",很难排查。
*/
async function fetchAllDocuments(): Promise<DocumentRecord[]> {
const campusId = libraryCampusId.value || undefined
const pageSize = 200
const firstPage = await getDocuments({
page: 1,
pageSize,
status: 'all',
campusId: appStore.selectedCampusId || undefined,
campusId,
})
const effectivePageSize = Math.max(firstPage.pageSize, 1)
const pageCount = Math.ceil(firstPage.total / effectivePageSize)
@@ -350,7 +358,7 @@ async function fetchAllDocuments(): Promise<DocumentRecord[]> {
page: index + 2,
pageSize: effectivePageSize,
status: 'all',
campusId: appStore.selectedCampusId || undefined,
campusId,
}),
),
)
@@ -358,6 +366,8 @@ async function fetchAllDocuments(): Promise<DocumentRecord[]> {
return [firstPage, ...remaining].flatMap((page) => page.records)
}
let libraryRequestId = 0
async function loadLibrary() {
if (useMockLibrary) {
libraryError.value = ''
@@ -367,15 +377,18 @@ async function loadLibrary() {
libraryLoading.value = true
libraryError.value = ''
const requestId = ++libraryRequestId
try {
const records = await fetchAllDocuments()
if (requestId !== libraryRequestId) return
library.value = groupDocuments(records, departments.value)
} catch {
if (requestId !== libraryRequestId) return
library.value = []
libraryError.value = '文书库加载失败,请确认后端模板接口可访问后重试'
} finally {
libraryLoading.value = false
if (requestId === libraryRequestId) libraryLoading.value = false
}
}
@@ -687,11 +700,25 @@ async function handlePropsSubmit(form: DocumentPropsForm) {
propsSaving.value = true
try {
const targetDepartment = departments.value.find((item) => item.id === departmentId)
if (departmentId && !targetDepartment) {
ElMessage.error('所选科室不存在,请刷新后重试')
return
}
const campusId =
targetDepartment?.campusId ||
(form.id ? propsTemplate.value?.campusId : appStore.selectedCampusId || campuses.value[0]?.id)
if (!campusId) {
ElMessage.error('无法确定文档所属院区,请选择院区后重试')
return
}
if (form.id) {
await updateTemplate(form.id, {
name: form.name,
description: form.description,
category: form.category,
campusId,
departmentId,
})
closePropsDialog()
await loadLibrary()
@@ -703,7 +730,7 @@ async function handlePropsSubmit(form: DocumentPropsForm) {
templateCode: generateTemplateCode(form.name),
name: form.name,
description: form.description,
campusId: appStore.selectedCampusId || campuses.value[0]?.id || '',
campusId,
departmentId,
category: form.category,
})
@@ -894,9 +921,8 @@ async function handleToggleStatus(template: DocumentTemplate) {
return
}
const versionId = action === 'disable'
? template.currentVersionId ?? template.versionId
: template.versionId
const versionId =
action === 'disable' ? (template.currentVersionId ?? template.versionId) : template.versionId
if (!versionId) {
ElMessage.error('该文书没有可操作的版本,请刷新后重试')
return
@@ -1140,15 +1166,16 @@ function clearFilters() {
/**
* 切换院区。
*
* 走全局院区状态而不是页面自己存一份:右上角的院区选择器读的就是这个值,
* 两处共用一份状态,就不会出现「右上角写着本部院区、筛选条却选中东院区」。
* 重新取数由下面的 watch 负责,这里只改状态。
* 所有院区仅用于文档库查询;具体院区仍与全局选择器同步。
* 重新取数由下面的 watch 负责。
*/
function handleCampusSelect(campusId: string) {
if (campusId === appStore.selectedCampusId) {
if (!campusId) {
allCampusesSelected.value = true
return
}
allCampusesSelected.value = false
appStore.selectCampus(campusId)
}
@@ -1156,6 +1183,8 @@ function useTemplate(template: DocumentTemplate) {
void openSigningDialog(template)
}
let libraryInitialized = false
async function initializePage() {
if (useMockLibrary) {
await loadOrganizationOptions()
@@ -1164,28 +1193,25 @@ async function initializePage() {
await loadOrganizationOptions()
}
libraryInitialized = true
await loadLibrary()
}
watch(
() => appStore.selectedCampusId,
(campusId, previousCampusId) => {
if (!campusId || campusId === previousCampusId) {
return
}
watch(libraryCampusId, (campusId, previousCampusId) => {
if (campusId === previousCampusId) {
return
}
// 院区变了,原先选中的科室不一定属于新院区,清掉以免筛出一片空结果
filters.department = null
// 院区变了,原先选中的科室不一定属于新院区,清掉以免筛出一片空结果
filters.department = null
// 演示模式没有院区维度;首次进页面时院区从空值变成第一个院区,
// 那一次由 initializePage 负责取数,这里跳过,避免重复拉一遍文库
if (useMockLibrary || !previousCampusId) {
return
}
// 初始化由 initializePage 取数,演示文库没有院区维度。
if (useMockLibrary || !libraryInitialized) {
return
}
void loadLibrary()
},
)
void loadLibrary()
})
onMounted(() => {
void initializePage()
@@ -1206,7 +1232,7 @@ onMounted(() => {
<DocumentFilterTabs
v-model:department="filters.department"
v-model:status="filters.status"
:campus="appStore.selectedCampusId"
:campus="libraryCampusId"
:campuses="campusTabs"
:departments="departmentTabs"
:statuses="statusTabs"