fix: 完善文档权限接口接入
This commit is contained in:
@@ -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 未提供的能力
|
||||
|
||||
|
||||
@@ -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<DepartmentRecord[]> {
|
||||
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<ApiResponse<BackendCollection<OrganizationResponseDto>>>('/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<PageResult<DepartmentRecord>> {
|
||||
if (useMockData) {
|
||||
return { records: [], total: 0, page, pageSize }
|
||||
}
|
||||
|
||||
const params: Record<string, string | number> = { page, size: pageSize }
|
||||
|
||||
if (campusId) {
|
||||
params.campusId = campusId
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
+64
-2
@@ -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<TemplatePermissionForm>({
|
||||
@@ -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(
|
||||
<span v-if="selectedTemplate" class="template-meta">
|
||||
{{ selectedTemplate.code || selectedTemplate.id }} · {{ selectedTemplate.category }}
|
||||
</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 v-if="!selectedTemplateId" class="empty-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>
|
||||
<div class="permission-list">
|
||||
<div class="list-heading">
|
||||
@@ -128,7 +151,18 @@ watch(
|
||||
</tr>
|
||||
</thead>
|
||||
<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>
|
||||
<span class="subject-type">{{ subjectTypeLabels[permission.subjectType] }}</span>
|
||||
{{ formatSubject(permission.subjectType, permission.subjectId) }}
|
||||
@@ -232,6 +266,10 @@ watch(
|
||||
min-width: min(420px, 100%);
|
||||
}
|
||||
|
||||
.preview-select {
|
||||
min-width: min(280px, 100%);
|
||||
}
|
||||
|
||||
select {
|
||||
min-width: 0;
|
||||
padding: 7px 10px;
|
||||
@@ -356,13 +394,37 @@ td {
|
||||
}
|
||||
|
||||
.empty-state,
|
||||
.loading-state {
|
||||
.loading-state,
|
||||
.error-state {
|
||||
padding: 32px 16px;
|
||||
color: var(--mut);
|
||||
font-size: 12.5px;
|
||||
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 {
|
||||
padding: 18px 12px;
|
||||
margin: 0;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import { getDepartments } from '@/api/management/organization'
|
||||
import { getAllDepartments } from '@/api/management/organization'
|
||||
import { getDocuments } from '@/api/management/documents'
|
||||
import {
|
||||
createTemplatePermission,
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
isPermissionMockEnabled,
|
||||
} from '@/api/management/document-permissions'
|
||||
import { getUsers } from '@/api/management/users'
|
||||
import { getRoleSummaries } from '@/api/management/roles'
|
||||
import { getRoleSummaries, getRoles } from '@/api/management/roles'
|
||||
import type {
|
||||
DepartmentRecord,
|
||||
DocumentRecord,
|
||||
@@ -54,6 +54,7 @@ const templatePermissions = ref<TemplatePermissionRecord[]>([])
|
||||
const permissionLoading = ref(false)
|
||||
const permissionSaving = ref(false)
|
||||
const permissionError = ref(false)
|
||||
const permissionPreviewUserId = ref('')
|
||||
let permissionRequestId = 0
|
||||
|
||||
const selectedTemplate = computed(() =>
|
||||
@@ -94,14 +95,14 @@ function resolvePermission(
|
||||
const hasInheritedDeny = records.some((record) => record.inherited && record.effect === 'DENY')
|
||||
const hasInheritedAllow = records.some((record) => record.inherited && record.effect === 'ALLOW')
|
||||
|
||||
if (hasDirectDeny) {
|
||||
return { enabled: false, state: 'direct-deny' }
|
||||
}
|
||||
|
||||
if (hasInheritedDeny) {
|
||||
return { enabled: false, state: 'inherited-deny' }
|
||||
}
|
||||
|
||||
if (hasDirectDeny) {
|
||||
return { enabled: false, state: 'direct-deny' }
|
||||
}
|
||||
|
||||
if (hasDirectAllow) {
|
||||
return { enabled: true, state: 'direct-allow' }
|
||||
}
|
||||
@@ -132,7 +133,8 @@ function syncPermissionRows() {
|
||||
name: role.name,
|
||||
color: role.color ?? ['#6b5bd2', '#0e6e8c', '#0f9d6c', '#d9821f'][index % 4],
|
||||
description: role.description,
|
||||
userCount: role.userCount ?? 0,
|
||||
userCount:
|
||||
role.userCount ?? userRecords.value.filter((user) => user.roleIds?.includes(role.id)).length,
|
||||
...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() {
|
||||
const firstPage = await getUsers({ page: 1, pageSize: 200, status: 'all' })
|
||||
const pageCount = Math.ceil(firstPage.total / firstPage.pageSize)
|
||||
@@ -178,9 +215,9 @@ async function loadPermissions() {
|
||||
|
||||
const [templatesResult, rolesResult, departmentsResult, usersResult] = await Promise.allSettled(
|
||||
[
|
||||
getDocuments({ page: 1, pageSize: 200, status: 'all' }),
|
||||
getRoleSummaries(),
|
||||
getDepartments(),
|
||||
getAllTemplatesForPermission(),
|
||||
getRolesForPermission(),
|
||||
getAllDepartments(),
|
||||
getAllUsersForPermission(),
|
||||
],
|
||||
)
|
||||
@@ -189,8 +226,8 @@ async function loadPermissions() {
|
||||
throw templatesResult.reason
|
||||
}
|
||||
|
||||
templates.value = templatesResult.value.records
|
||||
roleRecords.value = rolesResult.status === 'fulfilled' ? rolesResult.value : []
|
||||
templates.value = templatesResult.value
|
||||
roleRecords.value = rolesResult.status === 'fulfilled' ? rolesResult.value.records : []
|
||||
departmentRecords.value =
|
||||
departmentsResult.status === 'fulfilled' ? departmentsResult.value : []
|
||||
userRecords.value = usersResult.status === 'fulfilled' ? usersResult.value : []
|
||||
@@ -202,9 +239,17 @@ async function loadPermissions() {
|
||||
usersResult.status === 'rejected'
|
||||
) {
|
||||
ElMessage.warning('角色、科室或用户列表加载不完整,部分授权对象暂不可配置')
|
||||
} else if (rolesResult.value.usedFallback) {
|
||||
ElMessage.warning('角色人数统计接口暂不可用,已回退到角色列表,人数仅按当前用户列表估算')
|
||||
}
|
||||
|
||||
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()
|
||||
} catch {
|
||||
roleRecords.value = []
|
||||
@@ -228,6 +273,7 @@ async function loadTemplatePermissions() {
|
||||
if (!templateId || isPermissionMockEnabled) {
|
||||
templatePermissions.value = []
|
||||
permissionError.value = false
|
||||
permissionLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
@@ -235,7 +281,10 @@ async function loadTemplatePermissions() {
|
||||
permissionError.value = false
|
||||
|
||||
try {
|
||||
const records = await getTemplatePermissions(templateId)
|
||||
const records = await getTemplatePermissions(
|
||||
templateId,
|
||||
permissionPreviewUserId.value || undefined,
|
||||
)
|
||||
|
||||
if (requestId !== permissionRequestId || templateId !== selectedTemplateId.value) {
|
||||
return
|
||||
@@ -349,7 +398,7 @@ async function setSubjectPermission(
|
||||
subjectId: string,
|
||||
permission: PermissionKey,
|
||||
) {
|
||||
if (!selectedTemplateId.value || permissionSaving.value) {
|
||||
if (!selectedTemplateId.value || permissionSaving.value || permissionError.value) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -381,7 +430,7 @@ async function setSubjectPermission(
|
||||
}
|
||||
|
||||
async function addTemplatePermission(form: TemplatePermissionForm) {
|
||||
if (!selectedTemplateId.value || permissionSaving.value) {
|
||||
if (!selectedTemplateId.value || permissionSaving.value || permissionError.value) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -399,7 +448,7 @@ async function addTemplatePermission(form: TemplatePermissionForm) {
|
||||
}
|
||||
|
||||
async function removeTemplatePermission(permissionId: string) {
|
||||
if (!selectedTemplateId.value || permissionSaving.value) {
|
||||
if (!selectedTemplateId.value || permissionSaving.value || permissionError.value) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -426,6 +475,11 @@ function handlePanelTemplateChange(templateId: string) {
|
||||
void loadTemplatePermissions()
|
||||
}
|
||||
|
||||
function handlePermissionPreviewUserChange(userId: string) {
|
||||
permissionPreviewUserId.value = userId
|
||||
void loadTemplatePermissions()
|
||||
}
|
||||
|
||||
function retryTemplatePermissions() {
|
||||
void loadTemplatePermissions()
|
||||
}
|
||||
@@ -541,9 +595,13 @@ onMounted(loadPermissions)
|
||||
:subjects="templateSubjects"
|
||||
:loading="permissionLoading"
|
||||
:saving="permissionSaving"
|
||||
:error="permissionError"
|
||||
:preview-user-id="permissionPreviewUserId"
|
||||
@update:selected-template-id="handlePanelTemplateChange"
|
||||
@update:preview-user-id="handlePermissionPreviewUserChange"
|
||||
@add="addTemplatePermission"
|
||||
@delete="removeTemplatePermission"
|
||||
@retry="retryTemplatePermissions"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user