fix(documents): 保存文档归属并支持所有院区筛选
This commit is contained in:
@@ -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`)的实现:工作台只保留当天的签署操作,历史记录的检索、核对与导出统一放在这里。页面只读——已签署的正式文书不可直接修改,需要追加内容时回签署工作台新建签署任务。
|
||||
|
||||
**接口现状与边界**(后端还没有「按条件检索历史签署记录」的专用接口):
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user