fix: 完善文档权限接口接入

This commit is contained in:
xy
2026-09-03 12:33:16 +08:00
parent 48e5293509
commit 925d8c9202
4 changed files with 193 additions and 39 deletions
@@ -5,7 +5,7 @@
1. **文档库权限矩阵**:说明可见、可用、可维护和继承规则。 1. **文档库权限矩阵**:说明可见、可用、可维护和继承规则。
2. **按角色授权**:选择一个文档模板,调整角色的 VIEW、USE、MAINTAIN。 2. **按角色授权**:选择一个文档模板,调整角色的 VIEW、USE、MAINTAIN。
3. **按科室授权**:沿用当前模板,调整科室的 VIEW、USE、MAINTAIN。 3. **按科室授权**:沿用当前模板,调整科室的 VIEW、USE、MAINTAIN。
4. **模板权限明细与直接授权**:可选择角色、科室或用户新增/删除直接绑定。 4. **模板权限明细与直接授权**:可选择角色、科室或用户新增/删除直接绑定,并选择用户预览继承结果。
## 已接入接口 ## 已接入接口
@@ -36,6 +36,10 @@
- USE:可发起签署 - USE:可发起签署
- MAINTAIN:可维护模板 - MAINTAIN:可维护模板
模板、科室和用户接口都是分页接口,页面按后端返回的 `total` 自动补齐后续页,不会因为接口单页上限 200 而漏掉授权对象。角色优先使用 `/roles/summary` 获取人数;该接口失败时回退 `/roles`,人数按已加载用户的角色关系估算。
查询模板权限时不传 `userId` 表示查看当前登录用户的继承结果;页面也支持选择用户,将其 UUID 作为 `userId` 重新查询,用于预览该用户的科室签署员默认 `USE` 继承。接口返回的直接绑定和继承结果统一展示,但只有直接绑定带有可删除的 `id`。
## 页面保存行为 ## 页面保存行为
当前契约没有更新权限绑定的 PUT 接口,因此开关按以下规则保存: 当前契约没有更新权限绑定的 PUT 接口,因此开关按以下规则保存:
@@ -44,7 +48,7 @@
- 关闭:撤销直接 ALLOW;如果仍有继承的 ALLOW,POST 直接 DENY 覆盖继承。 - 关闭:撤销直接 ALLOW;如果仍有继承的 ALLOW,POST 直接 DENY 覆盖继承。
- 继承 DENY 没有持久化绑定 ID,且契约约定拒绝优先,页面将其显示为不可直接覆盖。 - 继承 DENY 没有持久化绑定 ID,且契约约定拒绝优先,页面将其显示为不可直接覆盖。
页面不会把继承结果当成可删除的直接绑定,也不会伪造全局权限矩阵数据。真实环境接口没有数据时会显示空态。 页面不会把继承结果当成可删除的直接绑定,也不会伪造全局权限矩阵数据。权限查询失败时会显示错误态并禁用新增、删除和矩阵调整,避免把空响应误当成“无权限”。真实环境接口没有数据时会显示空态。
## 当前 OpenAPI 未提供的能力 ## 当前 OpenAPI 未提供的能力
+48 -18
View File
@@ -1,6 +1,6 @@
import { unwrapApiResponse, unwrapNullableApiResponse } from '@/utils/api-response' import { unwrapApiResponse, unwrapNullableApiResponse } from '@/utils/api-response'
import { request } from '@/utils/request' import { request } from '@/utils/request'
import type { ApiResponse } from '@/types/common' import type { ApiResponse, PageResult } from '@/types/common'
import type { import type {
BackendCollection, BackendCollection,
@@ -46,24 +46,54 @@ export function getDepartments(campusId?: string): Promise<DepartmentRecord[]> {
return Promise.resolve([]) return Promise.resolve([])
} }
const params = campusId ? { page: 1, size: 200, campusId } : { page: 1, size: 200 } return getDepartmentPage(campusId).then((page) => page.records)
}
return request async function getDepartmentPage(
.get<ApiResponse<BackendCollection<OrganizationResponseDto>>>('/v1/departments', { campusId: string | undefined,
params, page = 1,
}) pageSize = 200,
.then(unwrapApiResponse) ): Promise<PageResult<DepartmentRecord>> {
.then((data) => if (useMockData) {
getRecords(data).map((item) => ({ return { records: [], total: 0, page, pageSize }
id: item.id, }
campusId: item.campusId ?? '',
parentId: item.parentId, const params: Record<string, string | number> = { page, size: pageSize }
code: item.code ?? item.id,
name: item.name ?? item.departmentName ?? item.id, if (campusId) {
status: item.status ?? 'ENABLED', params.campusId = campusId
sortNo: item.sortNo ?? 0, }
})),
) const response = await request.get<ApiResponse<BackendCollection<OrganizationResponseDto>>>(
'/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<DepartmentRecord[]> {
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 { function mapCampus(data: OrganizationResponseDto): CampusRecord {
@@ -12,12 +12,16 @@ const props = defineProps<{
subjects: PermissionSubjectOption[] subjects: PermissionSubjectOption[]
loading: boolean loading: boolean
saving: boolean saving: boolean
error: boolean
previewUserId: string
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
'update:selectedTemplateId': [value: string] 'update:selectedTemplateId': [value: string]
'update:previewUserId': [value: string]
add: [form: TemplatePermissionForm] add: [form: TemplatePermissionForm]
delete: [permissionId: string] delete: [permissionId: string]
retry: []
}>() }>()
const form = reactive<TemplatePermissionForm>({ const form = reactive<TemplatePermissionForm>({
@@ -48,6 +52,8 @@ const availableSubjects = computed(() =>
props.subjects.filter((subject) => subject.type === form.subjectType), props.subjects.filter((subject) => subject.type === form.subjectType),
) )
const previewUsers = computed(() => props.subjects.filter((subject) => subject.type === 'USER'))
const selectedTemplate = computed(() => const selectedTemplate = computed(() =>
props.templates.find((template) => template.id === props.selectedTemplateId), props.templates.find((template) => template.id === props.selectedTemplateId),
) )
@@ -56,6 +62,10 @@ function handleTemplateChange(event: Event) {
emit('update:selectedTemplateId', (event.target as HTMLSelectElement).value) emit('update:selectedTemplateId', (event.target as HTMLSelectElement).value)
} }
function handlePreviewUserChange(event: Event) {
emit('update:previewUserId', (event.target as HTMLSelectElement).value)
}
function handleSubjectTypeChange() { function handleSubjectTypeChange() {
form.subjectId = availableSubjects.value[0]?.id ?? '' form.subjectId = availableSubjects.value[0]?.id ?? ''
} }
@@ -105,10 +115,23 @@ watch(
<span v-if="selectedTemplate" class="template-meta"> <span v-if="selectedTemplate" class="template-meta">
{{ selectedTemplate.code || selectedTemplate.id }} · {{ selectedTemplate.category }} {{ selectedTemplate.code || selectedTemplate.id }} · {{ selectedTemplate.category }}
</span> </span>
<label class="template-select preview-select">
<span>继承预览用户</span>
<select :value="previewUserId" :disabled="loading" @change="handlePreviewUserChange">
<option value="">当前登录用户</option>
<option v-for="user in previewUsers" :key="user.id" :value="user.id">
{{ user.label }}
</option>
</select>
</label>
</div> </div>
<div v-if="!selectedTemplateId" class="empty-state">暂无模板,请先选择一个模板。</div> <div v-if="!selectedTemplateId" class="empty-state">暂无模板,请先选择一个模板。</div>
<div v-else-if="loading" class="loading-state">正在加载模板权限…</div> <div v-else-if="loading" class="loading-state">正在加载模板权限…</div>
<div v-else-if="error" class="error-state">
<span>模板权限加载失败</span>
<button type="button" class="retry-button" @click="emit('retry')">重新加载</button>
</div>
<template v-else> <template v-else>
<div class="permission-list"> <div class="permission-list">
<div class="list-heading"> <div class="list-heading">
@@ -128,7 +151,18 @@ watch(
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="permission in permissions" :key="permission.id"> <tr
v-for="permission in permissions"
:key="
permission.id ??
[
permission.subjectType,
permission.subjectId,
permission.permissionLevel,
permission.effect,
].join('-')
"
>
<td> <td>
<span class="subject-type">{{ subjectTypeLabels[permission.subjectType] }}</span> <span class="subject-type">{{ subjectTypeLabels[permission.subjectType] }}</span>
{{ formatSubject(permission.subjectType, permission.subjectId) }} {{ formatSubject(permission.subjectType, permission.subjectId) }}
@@ -232,6 +266,10 @@ watch(
min-width: min(420px, 100%); min-width: min(420px, 100%);
} }
.preview-select {
min-width: min(280px, 100%);
}
select { select {
min-width: 0; min-width: 0;
padding: 7px 10px; padding: 7px 10px;
@@ -356,13 +394,37 @@ td {
} }
.empty-state, .empty-state,
.loading-state { .loading-state,
.error-state {
padding: 32px 16px; padding: 32px 16px;
color: var(--mut); color: var(--mut);
font-size: 12.5px; font-size: 12.5px;
text-align: center; text-align: center;
} }
.error-state {
display: flex;
gap: 10px;
align-items: center;
justify-content: center;
color: var(--err);
background: #f8fbfc;
border-radius: 6px;
}
.retry-button {
padding: 4px 10px;
color: var(--brand);
font-size: 12px;
background: #fff;
border: 1px solid var(--brand);
border-radius: 5px;
}
.retry-button:hover {
background: var(--brand-l);
}
.empty-state.compact { .empty-state.compact {
padding: 18px 12px; padding: 18px 12px;
margin: 0; margin: 0;
@@ -2,7 +2,7 @@
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { getDepartments } from '@/api/management/organization' import { getAllDepartments } from '@/api/management/organization'
import { getDocuments } from '@/api/management/documents' import { getDocuments } from '@/api/management/documents'
import { import {
createTemplatePermission, createTemplatePermission,
@@ -11,7 +11,7 @@ import {
isPermissionMockEnabled, isPermissionMockEnabled,
} from '@/api/management/document-permissions' } from '@/api/management/document-permissions'
import { getUsers } from '@/api/management/users' import { getUsers } from '@/api/management/users'
import { getRoleSummaries } from '@/api/management/roles' import { getRoleSummaries, getRoles } from '@/api/management/roles'
import type { import type {
DepartmentRecord, DepartmentRecord,
DocumentRecord, DocumentRecord,
@@ -54,6 +54,7 @@ const templatePermissions = ref<TemplatePermissionRecord[]>([])
const permissionLoading = ref(false) const permissionLoading = ref(false)
const permissionSaving = ref(false) const permissionSaving = ref(false)
const permissionError = ref(false) const permissionError = ref(false)
const permissionPreviewUserId = ref('')
let permissionRequestId = 0 let permissionRequestId = 0
const selectedTemplate = computed(() => const selectedTemplate = computed(() =>
@@ -94,14 +95,14 @@ function resolvePermission(
const hasInheritedDeny = records.some((record) => record.inherited && record.effect === 'DENY') const hasInheritedDeny = records.some((record) => record.inherited && record.effect === 'DENY')
const hasInheritedAllow = records.some((record) => record.inherited && record.effect === 'ALLOW') const hasInheritedAllow = records.some((record) => record.inherited && record.effect === 'ALLOW')
if (hasDirectDeny) {
return { enabled: false, state: 'direct-deny' }
}
if (hasInheritedDeny) { if (hasInheritedDeny) {
return { enabled: false, state: 'inherited-deny' } return { enabled: false, state: 'inherited-deny' }
} }
if (hasDirectDeny) {
return { enabled: false, state: 'direct-deny' }
}
if (hasDirectAllow) { if (hasDirectAllow) {
return { enabled: true, state: 'direct-allow' } return { enabled: true, state: 'direct-allow' }
} }
@@ -132,7 +133,8 @@ function syncPermissionRows() {
name: role.name, name: role.name,
color: role.color ?? ['#6b5bd2', '#0e6e8c', '#0f9d6c', '#d9821f'][index % 4], color: role.color ?? ['#6b5bd2', '#0e6e8c', '#0f9d6c', '#d9821f'][index % 4],
description: role.description, description: role.description,
userCount: role.userCount ?? 0, userCount:
role.userCount ?? userRecords.value.filter((user) => user.roleIds?.includes(role.id)).length,
...buildPermissionMaps('ROLE', role.id), ...buildPermissionMaps('ROLE', role.id),
})) }))
@@ -143,6 +145,41 @@ function syncPermissionRows() {
})) }))
} }
async function getAllTemplatesForPermission() {
const firstPage = await getDocuments({ page: 1, pageSize: 200, status: 'all' })
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) =>
getDocuments({ page: index + 2, pageSize: firstPage.pageSize, status: 'all' }),
),
)
return [firstPage, ...remainingPages].flatMap((page) => page.records)
}
async function getRolesForPermission(): Promise<{
records: RoleRecord[]
usedFallback: boolean
}> {
try {
return { records: await getRoleSummaries(), usedFallback: false }
} catch (summaryError) {
try {
return {
records: await getRoles({ page: 1, pageSize: 200 }),
usedFallback: true,
}
} catch {
throw summaryError
}
}
}
async function getAllUsersForPermission() { async function getAllUsersForPermission() {
const firstPage = await getUsers({ page: 1, pageSize: 200, status: 'all' }) const firstPage = await getUsers({ page: 1, pageSize: 200, status: 'all' })
const pageCount = Math.ceil(firstPage.total / firstPage.pageSize) const pageCount = Math.ceil(firstPage.total / firstPage.pageSize)
@@ -178,9 +215,9 @@ async function loadPermissions() {
const [templatesResult, rolesResult, departmentsResult, usersResult] = await Promise.allSettled( const [templatesResult, rolesResult, departmentsResult, usersResult] = await Promise.allSettled(
[ [
getDocuments({ page: 1, pageSize: 200, status: 'all' }), getAllTemplatesForPermission(),
getRoleSummaries(), getRolesForPermission(),
getDepartments(), getAllDepartments(),
getAllUsersForPermission(), getAllUsersForPermission(),
], ],
) )
@@ -189,8 +226,8 @@ async function loadPermissions() {
throw templatesResult.reason throw templatesResult.reason
} }
templates.value = templatesResult.value.records templates.value = templatesResult.value
roleRecords.value = rolesResult.status === 'fulfilled' ? rolesResult.value : [] roleRecords.value = rolesResult.status === 'fulfilled' ? rolesResult.value.records : []
departmentRecords.value = departmentRecords.value =
departmentsResult.status === 'fulfilled' ? departmentsResult.value : [] departmentsResult.status === 'fulfilled' ? departmentsResult.value : []
userRecords.value = usersResult.status === 'fulfilled' ? usersResult.value : [] userRecords.value = usersResult.status === 'fulfilled' ? usersResult.value : []
@@ -202,9 +239,17 @@ async function loadPermissions() {
usersResult.status === 'rejected' usersResult.status === 'rejected'
) { ) {
ElMessage.warning('角色、科室或用户列表加载不完整,部分授权对象暂不可配置') ElMessage.warning('角色、科室或用户列表加载不完整,部分授权对象暂不可配置')
} else if (rolesResult.value.usedFallback) {
ElMessage.warning('角色人数统计接口暂不可用,已回退到角色列表,人数仅按当前用户列表估算')
} }
selectedTemplateId.value = templates.value[0]?.id ?? '' if (!userRecords.value.some((user) => user.id === permissionPreviewUserId.value)) {
permissionPreviewUserId.value = ''
}
if (!templates.value.some((template) => template.id === selectedTemplateId.value)) {
selectedTemplateId.value = templates.value[0]?.id ?? ''
}
await loadTemplatePermissions() await loadTemplatePermissions()
} catch { } catch {
roleRecords.value = [] roleRecords.value = []
@@ -228,6 +273,7 @@ async function loadTemplatePermissions() {
if (!templateId || isPermissionMockEnabled) { if (!templateId || isPermissionMockEnabled) {
templatePermissions.value = [] templatePermissions.value = []
permissionError.value = false permissionError.value = false
permissionLoading.value = false
return return
} }
@@ -235,7 +281,10 @@ async function loadTemplatePermissions() {
permissionError.value = false permissionError.value = false
try { try {
const records = await getTemplatePermissions(templateId) const records = await getTemplatePermissions(
templateId,
permissionPreviewUserId.value || undefined,
)
if (requestId !== permissionRequestId || templateId !== selectedTemplateId.value) { if (requestId !== permissionRequestId || templateId !== selectedTemplateId.value) {
return return
@@ -349,7 +398,7 @@ async function setSubjectPermission(
subjectId: string, subjectId: string,
permission: PermissionKey, permission: PermissionKey,
) { ) {
if (!selectedTemplateId.value || permissionSaving.value) { if (!selectedTemplateId.value || permissionSaving.value || permissionError.value) {
return return
} }
@@ -381,7 +430,7 @@ async function setSubjectPermission(
} }
async function addTemplatePermission(form: TemplatePermissionForm) { async function addTemplatePermission(form: TemplatePermissionForm) {
if (!selectedTemplateId.value || permissionSaving.value) { if (!selectedTemplateId.value || permissionSaving.value || permissionError.value) {
return return
} }
@@ -399,7 +448,7 @@ async function addTemplatePermission(form: TemplatePermissionForm) {
} }
async function removeTemplatePermission(permissionId: string) { async function removeTemplatePermission(permissionId: string) {
if (!selectedTemplateId.value || permissionSaving.value) { if (!selectedTemplateId.value || permissionSaving.value || permissionError.value) {
return return
} }
@@ -426,6 +475,11 @@ function handlePanelTemplateChange(templateId: string) {
void loadTemplatePermissions() void loadTemplatePermissions()
} }
function handlePermissionPreviewUserChange(userId: string) {
permissionPreviewUserId.value = userId
void loadTemplatePermissions()
}
function retryTemplatePermissions() { function retryTemplatePermissions() {
void loadTemplatePermissions() void loadTemplatePermissions()
} }
@@ -541,9 +595,13 @@ onMounted(loadPermissions)
:subjects="templateSubjects" :subjects="templateSubjects"
:loading="permissionLoading" :loading="permissionLoading"
:saving="permissionSaving" :saving="permissionSaving"
:error="permissionError"
:preview-user-id="permissionPreviewUserId"
@update:selected-template-id="handlePanelTemplateChange" @update:selected-template-id="handlePanelTemplateChange"
@update:preview-user-id="handlePermissionPreviewUserChange"
@add="addTemplatePermission" @add="addTemplatePermission"
@delete="removeTemplatePermission" @delete="removeTemplatePermission"
@retry="retryTemplatePermissions"
/> />
</section> </section>
</template> </template>