diff --git a/clinical-web/docs/document-permissions-api.md b/clinical-web/docs/document-permissions-api.md index 4786622..55fc40f 100644 --- a/clinical-web/docs/document-permissions-api.md +++ b/clinical-web/docs/document-permissions-api.md @@ -5,7 +5,7 @@ 1. **文档库权限矩阵**:说明可见、可用、可维护和继承规则。 2. **按角色授权**:选择一个文档模板,调整角色的 VIEW、USE、MAINTAIN。 3. **按科室授权**:沿用当前模板,调整科室的 VIEW、USE、MAINTAIN。 -4. **模板权限明细与直接授权**:可选择角色、科室或用户新增/删除直接绑定。 +4. **模板权限明细与直接授权**:可选择角色、科室或用户新增/删除直接绑定,并选择用户预览继承结果。 ## 已接入接口 @@ -36,6 +36,10 @@ - USE:可发起签署 - MAINTAIN:可维护模板 +模板、科室和用户接口都是分页接口,页面按后端返回的 `total` 自动补齐后续页,不会因为接口单页上限 200 而漏掉授权对象。角色优先使用 `/roles/summary` 获取人数;该接口失败时回退 `/roles`,人数按已加载用户的角色关系估算。 + +查询模板权限时不传 `userId` 表示查看当前登录用户的继承结果;页面也支持选择用户,将其 UUID 作为 `userId` 重新查询,用于预览该用户的科室签署员默认 `USE` 继承。接口返回的直接绑定和继承结果统一展示,但只有直接绑定带有可删除的 `id`。 + ## 页面保存行为 当前契约没有更新权限绑定的 PUT 接口,因此开关按以下规则保存: @@ -44,7 +48,7 @@ - 关闭:撤销直接 ALLOW;如果仍有继承的 ALLOW,POST 直接 DENY 覆盖继承。 - 继承 DENY 没有持久化绑定 ID,且契约约定拒绝优先,页面将其显示为不可直接覆盖。 -页面不会把继承结果当成可删除的直接绑定,也不会伪造全局权限矩阵数据。真实环境接口没有数据时会显示空态。 +页面不会把继承结果当成可删除的直接绑定,也不会伪造全局权限矩阵数据。权限查询失败时会显示错误态并禁用新增、删除和矩阵调整,避免把空响应误当成“无权限”。真实环境接口没有数据时会显示空态。 ## 当前 OpenAPI 未提供的能力 diff --git a/clinical-web/src/api/management/organization.ts b/clinical-web/src/api/management/organization.ts index 3db0564..6a1b13b 100644 --- a/clinical-web/src/api/management/organization.ts +++ b/clinical-web/src/api/management/organization.ts @@ -1,6 +1,6 @@ import { unwrapApiResponse, unwrapNullableApiResponse } from '@/utils/api-response' import { request } from '@/utils/request' -import type { ApiResponse } from '@/types/common' +import type { ApiResponse, PageResult } from '@/types/common' import type { BackendCollection, @@ -46,24 +46,54 @@ export function getDepartments(campusId?: string): Promise { return Promise.resolve([]) } - const params = campusId ? { page: 1, size: 200, campusId } : { page: 1, size: 200 } + return getDepartmentPage(campusId).then((page) => page.records) +} - return request - .get>>('/v1/departments', { - params, - }) - .then(unwrapApiResponse) - .then((data) => - getRecords(data).map((item) => ({ - id: item.id, - campusId: item.campusId ?? '', - parentId: item.parentId, - code: item.code ?? item.id, - name: item.name ?? item.departmentName ?? item.id, - status: item.status ?? 'ENABLED', - sortNo: item.sortNo ?? 0, - })), - ) +async function getDepartmentPage( + campusId: string | undefined, + page = 1, + pageSize = 200, +): Promise> { + if (useMockData) { + return { records: [], total: 0, page, pageSize } + } + + const params: Record = { page, size: pageSize } + + if (campusId) { + params.campusId = campusId + } + + const response = await request.get>>( + '/v1/departments', + { params }, + ) + const data = unwrapApiResponse(response) + const records = getRecords(data).map(mapDepartment) + + return { + records, + total: Array.isArray(data) ? records.length : (data.total ?? records.length), + page: Array.isArray(data) ? page : (data.page ?? page), + pageSize: Array.isArray(data) ? pageSize : (data.size ?? pageSize), + } +} + +export async function getAllDepartments(campusId?: string): Promise { + const firstPage = await getDepartmentPage(campusId, 1, 200) + const pageCount = Math.ceil(firstPage.total / firstPage.pageSize) + + if (pageCount <= 1) { + return firstPage.records + } + + const remainingPages = await Promise.all( + Array.from({ length: pageCount - 1 }, (_, index) => + getDepartmentPage(campusId, index + 2, firstPage.pageSize), + ), + ) + + return [firstPage, ...remainingPages].flatMap((page) => page.records) } function mapCampus(data: OrganizationResponseDto): CampusRecord { diff --git a/clinical-web/src/views/management/document-permissions/components/TemplatePermissionPanel.vue b/clinical-web/src/views/management/document-permissions/components/TemplatePermissionPanel.vue index 49bb3ce..bd4cefe 100644 --- a/clinical-web/src/views/management/document-permissions/components/TemplatePermissionPanel.vue +++ b/clinical-web/src/views/management/document-permissions/components/TemplatePermissionPanel.vue @@ -12,12 +12,16 @@ const props = defineProps<{ subjects: PermissionSubjectOption[] loading: boolean saving: boolean + error: boolean + previewUserId: string }>() const emit = defineEmits<{ 'update:selectedTemplateId': [value: string] + 'update:previewUserId': [value: string] add: [form: TemplatePermissionForm] delete: [permissionId: string] + retry: [] }>() const form = reactive({ @@ -48,6 +52,8 @@ const availableSubjects = computed(() => props.subjects.filter((subject) => subject.type === form.subjectType), ) +const previewUsers = computed(() => props.subjects.filter((subject) => subject.type === 'USER')) + const selectedTemplate = computed(() => props.templates.find((template) => template.id === props.selectedTemplateId), ) @@ -56,6 +62,10 @@ function handleTemplateChange(event: Event) { emit('update:selectedTemplateId', (event.target as HTMLSelectElement).value) } +function handlePreviewUserChange(event: Event) { + emit('update:previewUserId', (event.target as HTMLSelectElement).value) +} + function handleSubjectTypeChange() { form.subjectId = availableSubjects.value[0]?.id ?? '' } @@ -105,10 +115,23 @@ watch( {{ selectedTemplate.code || selectedTemplate.id }} · {{ selectedTemplate.category }} +
暂无模板,请先选择一个模板。
正在加载模板权限…
+
+ 模板权限加载失败 + +