962 lines
26 KiB
Vue
962 lines
26 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, ref, watch } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
|
||
import {
|
||
getPermissionPolicies,
|
||
getTemplatePermissionMatrix,
|
||
getTemplatePermissions,
|
||
isPermissionMockEnabled,
|
||
batchSaveTemplatePermissions,
|
||
updateDepartmentPermissionPolicy,
|
||
updateRolePermissionPolicy,
|
||
} from '@/api/management/document-permissions'
|
||
import { getTemplatePermissionConfig } from '@/api/management/documents'
|
||
import { getUsers } from '@/api/management/users'
|
||
import type {
|
||
DepartmentPermissionPolicyRecord,
|
||
DepartmentRecord,
|
||
DocumentRecord,
|
||
PermissionPoliciesRecord,
|
||
PermissionPolicyLevel,
|
||
RolePermissionPolicyRecord,
|
||
RoleRecord,
|
||
TemplatePermissionBatchItemRequest,
|
||
TemplatePermissionRecord,
|
||
UserRecord,
|
||
} from '@/api/management/types'
|
||
import { useAppStore } from '@/stores/app'
|
||
import { useLoginStore } from '@/stores/login'
|
||
|
||
import DepartmentPermissionDrawer from './components/DepartmentPermissionDrawer.vue'
|
||
import DepartmentPermissionTable from './components/DepartmentPermissionTable.vue'
|
||
import RolePermissionMatrix from './components/RolePermissionMatrix.vue'
|
||
import { permissionColumns } from './mock'
|
||
import type {
|
||
DepartmentPermissionRow,
|
||
PermissionKey,
|
||
PermissionSubjectOption,
|
||
RolePermissionRow,
|
||
TemplatePermissionForm,
|
||
} from './types'
|
||
|
||
const policyReadPermissions = [
|
||
'sign:template:permission:query',
|
||
'system:template:permission:query',
|
||
'system:template:permission:update',
|
||
'system:template:permission:manage',
|
||
'system:document:permission:query',
|
||
'system:document:permission:update',
|
||
'system:document:permission:manage',
|
||
'system:document-permission:query',
|
||
'system:document-permission:update',
|
||
'system:document-permission:manage',
|
||
] as const
|
||
|
||
const policyWritePermissions = [
|
||
'sign:template:permission:maintain',
|
||
'system:template:permission:update',
|
||
'system:template:permission:manage',
|
||
'system:document:permission:update',
|
||
'system:document:permission:manage',
|
||
'system:document-permission:update',
|
||
'system:document-permission:manage',
|
||
] as const
|
||
|
||
const appStore = useAppStore()
|
||
const loginStore = useLoginStore()
|
||
const loading = ref(true)
|
||
const policyError = ref('')
|
||
const policies = ref<PermissionPoliciesRecord | null>(null)
|
||
const roleRecords = ref<RoleRecord[]>([])
|
||
const departmentRecords = ref<DepartmentRecord[]>([])
|
||
const userRecords = ref<UserRecord[]>([])
|
||
const roleRows = ref<RolePermissionRow[]>([])
|
||
const departmentRows = ref<DepartmentPermissionRow[]>([])
|
||
const templateCount = ref(0)
|
||
const defaultSavingId = ref('')
|
||
|
||
const departmentDrawerVisible = ref(false)
|
||
const selectedDepartment = ref<DepartmentPermissionRow | null>(null)
|
||
const templates = ref<DocumentRecord[]>([])
|
||
const templatesLoaded = ref(false)
|
||
const templateCatalogLoading = ref(false)
|
||
const templateCatalogError = ref('')
|
||
const selectedTemplateId = ref('')
|
||
const templatePermissions = ref<TemplatePermissionRecord[]>([])
|
||
const templatePermissionLoading = ref(false)
|
||
const templatePermissionSaving = ref(false)
|
||
const templatePermissionError = ref(false)
|
||
const templatePermissionErrorMessage = ref('')
|
||
const permissionPreviewUserId = ref('')
|
||
const subjectsLoaded = ref(false)
|
||
let templatePermissionRequestId = 0
|
||
let templateCatalogRequest: Promise<void> | null = null
|
||
let subjectRequest: Promise<void> | null = null
|
||
|
||
const canViewPolicy = computed(
|
||
() => isPermissionMockEnabled || hasAnyPermission(policyReadPermissions),
|
||
)
|
||
const canEditPolicy = computed(
|
||
() =>
|
||
!loginStore.isReadOnly &&
|
||
(isPermissionMockEnabled ||
|
||
policyWritePermissions.some((code) => loginStore.hasWritePermission(code))),
|
||
)
|
||
const canViewTemplateExceptions = computed(() => canViewPolicy.value)
|
||
const canEditTemplateExceptions = computed(() => canEditPolicy.value)
|
||
|
||
function hasAnyPermission(codes: readonly string[]) {
|
||
return codes.some((code) => loginStore.hasPermission(code))
|
||
}
|
||
|
||
function formatError(error: unknown, fallback: string) {
|
||
return error instanceof Error && error.message ? `${fallback}:${error.message}` : fallback
|
||
}
|
||
|
||
function mapRoleRecord(policy: RolePermissionPolicyRecord, index: number): RoleRecord {
|
||
return {
|
||
id: policy.roleId,
|
||
code: policy.code,
|
||
name: policy.name,
|
||
description: policy.description,
|
||
color: ['#6b5bd2', '#0e6e8c', '#0f9d6c', '#d9821f', '#c65d5d'][index % 5],
|
||
userCount: policy.userCount ?? undefined,
|
||
status: 'ENABLED',
|
||
version: policy.version,
|
||
}
|
||
}
|
||
|
||
function mapRoleRow(policy: RolePermissionPolicyRecord, index: number): RolePermissionRow {
|
||
return {
|
||
id: policy.roleId,
|
||
name: policy.name,
|
||
color: ['#6b5bd2', '#0e6e8c', '#0f9d6c', '#d9821f', '#c65d5d'][index % 5],
|
||
description: policy.description,
|
||
userCount: policy.userCount ?? 0,
|
||
permissions: {
|
||
VIEW: policy.view,
|
||
USE: policy.use,
|
||
MAINTAIN: policy.maintain,
|
||
},
|
||
permissionStates: {
|
||
VIEW: policy.view ? 'direct-allow' : 'none',
|
||
USE: policy.use ? 'direct-allow' : 'none',
|
||
MAINTAIN: policy.maintain ? 'direct-allow' : 'none',
|
||
},
|
||
version: policy.version,
|
||
}
|
||
}
|
||
|
||
function mapDepartmentRow(policy: DepartmentPermissionPolicyRecord): DepartmentPermissionRow {
|
||
const campusName = appStore.campusRecords.find((campus) => campus.id === policy.campusId)?.name
|
||
|
||
return {
|
||
id: policy.departmentId,
|
||
code: policy.code,
|
||
department: policy.name,
|
||
campus:
|
||
campusName || (policy.campusId === policies.value?.campusId ? appStore.selectedCampus : ''),
|
||
campusId: policy.campusId,
|
||
own: policy.own,
|
||
common: policy.common,
|
||
other: policy.other,
|
||
version: policy.version,
|
||
}
|
||
}
|
||
|
||
function syncPolicyRows(policy: PermissionPoliciesRecord) {
|
||
policies.value = policy
|
||
templateCount.value = policy.templateCount
|
||
roleRecords.value = policy.roles.map(mapRoleRecord)
|
||
departmentRecords.value = policy.departments.map((department) => ({
|
||
id: department.departmentId,
|
||
campusId: department.campusId || policy.campusId,
|
||
code: department.code,
|
||
name: department.name,
|
||
status: 'ENABLED',
|
||
sortNo: 0,
|
||
}))
|
||
roleRows.value = policy.roles.map(mapRoleRow)
|
||
departmentRows.value = policy.departments.map(mapDepartmentRow)
|
||
}
|
||
|
||
async function loadPolicies() {
|
||
loading.value = true
|
||
policyError.value = ''
|
||
|
||
try {
|
||
if (!isPermissionMockEnabled) {
|
||
await loginStore.ensureCurrentUser()
|
||
if (!canViewPolicy.value) {
|
||
policyError.value = '当前账号没有文档权限配置的查看权限'
|
||
return
|
||
}
|
||
await appStore.loadCampuses()
|
||
}
|
||
|
||
const policy = await getPermissionPolicies(appStore.selectedCampusId || undefined)
|
||
syncPolicyRows(policy)
|
||
} catch (error: unknown) {
|
||
roleRecords.value = []
|
||
departmentRecords.value = []
|
||
roleRows.value = []
|
||
departmentRows.value = []
|
||
policyError.value = formatError(error, '默认权限策略加载失败,请稍后重试')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function getAllTemplatesForPermission(campusId?: string) {
|
||
const firstPage = await getTemplatePermissionConfig({
|
||
page: 1,
|
||
pageSize: 200,
|
||
status: 'all',
|
||
campusId,
|
||
})
|
||
const pageSize = Math.max(firstPage.pageSize, 1)
|
||
const pageCount = Math.ceil(firstPage.total / pageSize)
|
||
|
||
if (pageCount <= 1) {
|
||
return firstPage.records
|
||
}
|
||
|
||
const remainingPages = await Promise.all(
|
||
Array.from({ length: pageCount - 1 }, (_, index) =>
|
||
getTemplatePermissionConfig({
|
||
page: index + 2,
|
||
pageSize,
|
||
status: 'all',
|
||
campusId,
|
||
}),
|
||
),
|
||
)
|
||
|
||
return [firstPage, ...remainingPages].flatMap((page) => page.records)
|
||
}
|
||
|
||
async function loadTemplateCatalog() {
|
||
if (templateCatalogRequest) {
|
||
return templateCatalogRequest
|
||
}
|
||
|
||
templateCatalogLoading.value = true
|
||
templateCatalogError.value = ''
|
||
const campusId = appStore.selectedCampusId || policies.value?.campusId || undefined
|
||
const request = getAllTemplatesForPermission(campusId)
|
||
.then((records) => {
|
||
templates.value = records
|
||
templatesLoaded.value = true
|
||
if (!records.some((template) => template.id === selectedTemplateId.value)) {
|
||
selectedTemplateId.value = records[0]?.id ?? ''
|
||
templatePermissions.value = []
|
||
}
|
||
})
|
||
.catch((error: unknown) => {
|
||
templates.value = []
|
||
selectedTemplateId.value = ''
|
||
templatesLoaded.value = false
|
||
templateCatalogError.value = formatError(error, '模板例外目录加载失败,请稍后重试')
|
||
})
|
||
.finally(() => {
|
||
templateCatalogLoading.value = false
|
||
templateCatalogRequest = null
|
||
})
|
||
|
||
templateCatalogRequest = request
|
||
return request
|
||
}
|
||
|
||
async function getAllTemplatePermissionMatrix(templateId: string) {
|
||
const firstPage = await getTemplatePermissionMatrix({
|
||
page: 1,
|
||
pageSize: 200,
|
||
templateId,
|
||
})
|
||
const pageSize = Math.max(firstPage.pageSize, 1)
|
||
const pageCount = Math.ceil(firstPage.total / pageSize)
|
||
|
||
if (pageCount <= 1) {
|
||
return firstPage.records
|
||
}
|
||
|
||
const remainingPages = await Promise.all(
|
||
Array.from({ length: pageCount - 1 }, (_, index) =>
|
||
getTemplatePermissionMatrix({
|
||
page: index + 2,
|
||
pageSize,
|
||
templateId,
|
||
}),
|
||
),
|
||
)
|
||
|
||
return [firstPage, ...remainingPages].flatMap((page) => page.records)
|
||
}
|
||
|
||
async function getAllUsersForPermission() {
|
||
const campusId = appStore.selectedCampusId || policies.value?.campusId || undefined
|
||
const firstPage = await getUsers({
|
||
page: 1,
|
||
pageSize: 200,
|
||
status: 'all',
|
||
...(isPermissionMockEnabled || !campusId ? {} : { campusId }),
|
||
})
|
||
const pageSize = Math.max(firstPage.pageSize, 1)
|
||
const pageCount = Math.ceil(firstPage.total / pageSize)
|
||
|
||
if (pageCount <= 1) {
|
||
return firstPage.records
|
||
}
|
||
|
||
const remainingPages = await Promise.all(
|
||
Array.from({ length: pageCount - 1 }, (_, index) =>
|
||
getUsers({
|
||
page: index + 2,
|
||
pageSize,
|
||
status: 'all',
|
||
...(isPermissionMockEnabled || !campusId ? {} : { campusId }),
|
||
}),
|
||
),
|
||
)
|
||
|
||
return [firstPage, ...remainingPages].flatMap((page) => page.records)
|
||
}
|
||
|
||
async function loadTemplateSubjects() {
|
||
if (subjectsLoaded.value) {
|
||
return
|
||
}
|
||
|
||
if (subjectRequest) {
|
||
return subjectRequest
|
||
}
|
||
|
||
const request = getAllUsersForPermission()
|
||
.then((records) => {
|
||
userRecords.value = records
|
||
subjectsLoaded.value = true
|
||
if (!records.some((user) => user.id === permissionPreviewUserId.value)) {
|
||
permissionPreviewUserId.value = ''
|
||
}
|
||
})
|
||
.catch(() => {
|
||
userRecords.value = []
|
||
ElMessage.warning('用户授权对象加载失败,当前只能配置角色和科室例外')
|
||
})
|
||
.finally(() => {
|
||
subjectRequest = null
|
||
})
|
||
|
||
subjectRequest = request
|
||
return request
|
||
}
|
||
|
||
const templateSubjects = computed<PermissionSubjectOption[]>(() => [
|
||
...roleRecords.value.map((role) => ({
|
||
id: role.id,
|
||
type: 'ROLE' as const,
|
||
label: `${role.name}(${role.code})`,
|
||
})),
|
||
...departmentRecords.value.map((department) => ({
|
||
id: department.id,
|
||
type: 'DEPARTMENT' as const,
|
||
label: department.name,
|
||
})),
|
||
...userRecords.value.map((user) => ({
|
||
id: user.id,
|
||
type: 'USER' as const,
|
||
label: `${user.name}(${user.account})`,
|
||
})),
|
||
])
|
||
|
||
function mergeTemplatePermissions(
|
||
matrixRecords: TemplatePermissionRecord[],
|
||
effectiveRecords: TemplatePermissionRecord[],
|
||
) {
|
||
const seen = new Set<string>()
|
||
|
||
return [...matrixRecords, ...effectiveRecords.filter((record) => record.inherited)].filter(
|
||
(record) => {
|
||
const key = [
|
||
record.templateId,
|
||
record.subjectType,
|
||
record.subjectId ?? '',
|
||
record.permissionLevel,
|
||
record.effect,
|
||
record.inherited,
|
||
].join('|')
|
||
|
||
if (seen.has(key)) {
|
||
return false
|
||
}
|
||
|
||
seen.add(key)
|
||
return true
|
||
},
|
||
)
|
||
}
|
||
|
||
async function loadTemplatePermissions() {
|
||
const templateId = selectedTemplateId.value
|
||
const requestId = ++templatePermissionRequestId
|
||
|
||
if (!templateId || !templatesLoaded.value || !canViewTemplateExceptions.value) {
|
||
templatePermissions.value = []
|
||
templatePermissionError.value = false
|
||
templatePermissionErrorMessage.value = ''
|
||
templatePermissionLoading.value = false
|
||
return
|
||
}
|
||
|
||
templatePermissionLoading.value = true
|
||
templatePermissionError.value = false
|
||
templatePermissionErrorMessage.value = ''
|
||
|
||
try {
|
||
const [matrixResult, effectiveResult] = await Promise.allSettled([
|
||
getAllTemplatePermissionMatrix(templateId),
|
||
getTemplatePermissions(templateId, permissionPreviewUserId.value || undefined),
|
||
])
|
||
|
||
if (matrixResult.status === 'rejected') {
|
||
throw matrixResult.reason
|
||
}
|
||
|
||
if (requestId !== templatePermissionRequestId || templateId !== selectedTemplateId.value) {
|
||
return
|
||
}
|
||
|
||
templatePermissions.value = mergeTemplatePermissions(
|
||
matrixResult.value,
|
||
effectiveResult.status === 'fulfilled' ? effectiveResult.value : [],
|
||
)
|
||
|
||
if (effectiveResult.status === 'rejected') {
|
||
ElMessage.warning('继承权限预览加载失败,当前仅展示模板矩阵数据')
|
||
}
|
||
} catch (error: unknown) {
|
||
if (requestId !== templatePermissionRequestId) {
|
||
return
|
||
}
|
||
|
||
templatePermissions.value = []
|
||
templatePermissionError.value = true
|
||
templatePermissionErrorMessage.value = formatError(error, '模板权限加载失败,请稍后重试')
|
||
} finally {
|
||
if (requestId === templatePermissionRequestId) {
|
||
templatePermissionLoading.value = false
|
||
}
|
||
}
|
||
}
|
||
|
||
async function prepareDepartmentDrawer() {
|
||
await Promise.all([loadTemplateCatalog(), loadTemplateSubjects()])
|
||
await loadTemplatePermissions()
|
||
}
|
||
|
||
function openDepartmentDrawer(row: DepartmentPermissionRow) {
|
||
if (!canEditPolicy.value && !canViewPolicy.value) {
|
||
return
|
||
}
|
||
|
||
selectedDepartment.value = row
|
||
departmentDrawerVisible.value = true
|
||
void prepareDepartmentDrawer()
|
||
}
|
||
|
||
const policyLevelRank: Record<PermissionPolicyLevel, number> = {
|
||
NONE: 0,
|
||
VIEW: 1,
|
||
USE: 2,
|
||
MAINTAIN: 3,
|
||
}
|
||
|
||
function policyLevelForPermissions(
|
||
permissions: Record<PermissionKey, boolean>,
|
||
): PermissionPolicyLevel {
|
||
if (permissions.MAINTAIN) return 'MAINTAIN'
|
||
if (permissions.USE) return 'USE'
|
||
if (permissions.VIEW) return 'VIEW'
|
||
return 'NONE'
|
||
}
|
||
|
||
function toggleRolePolicy(roleId: string, permission: PermissionKey) {
|
||
if (!canEditPolicy.value || defaultSavingId.value) {
|
||
return
|
||
}
|
||
|
||
const row = roleRows.value.find((item) => item.id === roleId)
|
||
const source = policies.value?.roles.find((item) => item.roleId === roleId)
|
||
|
||
if (!row || !source) {
|
||
return
|
||
}
|
||
|
||
const nextPermissions = { ...row.permissions }
|
||
const permissionIndex: Record<PermissionKey, number> = { VIEW: 0, USE: 1, MAINTAIN: 2 }
|
||
const nextIndex = permissionIndex[permission]
|
||
const currentLevel = policyLevelForPermissions(nextPermissions)
|
||
const currentIndex = policyLevelRank[currentLevel] - 1
|
||
const nextLevel: PermissionPolicyLevel = row.permissions[permission]
|
||
? nextIndex <= currentIndex
|
||
? (['NONE', 'VIEW', 'USE'] as const)[nextIndex]
|
||
: currentLevel
|
||
: permission
|
||
|
||
Object.assign(nextPermissions, {
|
||
VIEW: policyLevelRank[nextLevel] >= policyLevelRank.VIEW,
|
||
USE: policyLevelRank[nextLevel] >= policyLevelRank.USE,
|
||
MAINTAIN: policyLevelRank[nextLevel] >= policyLevelRank.MAINTAIN,
|
||
})
|
||
const payload = {
|
||
campusId: source.campusId || policies.value?.campusId || appStore.selectedCampusId,
|
||
permissionLevel: nextLevel,
|
||
expectedVersion: source.version,
|
||
}
|
||
|
||
defaultSavingId.value = roleId
|
||
void updateRolePermissionPolicy(roleId, payload, source)
|
||
.then((updated) => {
|
||
const nextRoles = (policies.value?.roles ?? []).map((item) =>
|
||
item.roleId === roleId ? updated : item,
|
||
)
|
||
syncPolicyRows({
|
||
...(policies.value as PermissionPoliciesRecord),
|
||
roles: nextRoles,
|
||
})
|
||
ElMessage.success('角色默认策略已保存')
|
||
})
|
||
.catch((error: unknown) => {
|
||
ElMessage.error(formatError(error, '角色默认策略保存失败,请稍后重试'))
|
||
})
|
||
.finally(() => {
|
||
defaultSavingId.value = ''
|
||
})
|
||
}
|
||
|
||
function mapRowToDepartmentPolicy(row: DepartmentPermissionRow): DepartmentPermissionPolicyRecord {
|
||
return {
|
||
departmentId: row.id,
|
||
code: row.code ?? row.id,
|
||
name: row.department,
|
||
campusId: row.campusId ?? policies.value?.campusId ?? '',
|
||
own: row.own,
|
||
common: row.common,
|
||
other: row.other,
|
||
ownPermission: row.own ? 'USE' : 'NONE',
|
||
commonPermission: row.common ? 'USE' : 'NONE',
|
||
otherPermission: row.other ? 'USE' : 'NONE',
|
||
version: row.version,
|
||
}
|
||
}
|
||
|
||
function saveDepartmentPolicy(payload: {
|
||
own: boolean
|
||
common: boolean
|
||
other: boolean
|
||
expectedVersion?: number
|
||
}) {
|
||
const row = selectedDepartment.value
|
||
|
||
if (!row || !canEditPolicy.value || defaultSavingId.value) {
|
||
return
|
||
}
|
||
|
||
defaultSavingId.value = row.id
|
||
void updateDepartmentPermissionPolicy(
|
||
row.id,
|
||
{
|
||
campusId: row.campusId ?? policies.value?.campusId ?? appStore.selectedCampusId,
|
||
ownPermission: payload.own ? 'USE' : 'NONE',
|
||
commonPermission: payload.common ? 'USE' : 'NONE',
|
||
otherPermission: payload.other ? 'USE' : 'NONE',
|
||
expectedVersion: payload.expectedVersion,
|
||
},
|
||
mapRowToDepartmentPolicy(row),
|
||
)
|
||
.then((updated) => {
|
||
const nextDepartments = (policies.value?.departments ?? []).map((item) =>
|
||
item.departmentId === row.id ? updated : item,
|
||
)
|
||
const nextPolicy = {
|
||
...(policies.value as PermissionPoliciesRecord),
|
||
departments: nextDepartments,
|
||
}
|
||
syncPolicyRows(nextPolicy)
|
||
selectedDepartment.value = departmentRows.value.find((item) => item.id === row.id) ?? null
|
||
ElMessage.success('科室默认策略已保存')
|
||
})
|
||
.catch((error: unknown) => {
|
||
ElMessage.error(formatError(error, '科室默认策略保存失败,请稍后重试'))
|
||
})
|
||
.finally(() => {
|
||
defaultSavingId.value = ''
|
||
})
|
||
}
|
||
|
||
function handleTemplateChange(templateId: string) {
|
||
if (!templates.value.some((template) => template.id === templateId)) {
|
||
return
|
||
}
|
||
|
||
selectedTemplateId.value = templateId
|
||
templatePermissions.value = []
|
||
void loadTemplatePermissions()
|
||
}
|
||
|
||
function handlePreviewUserChange(userId: string) {
|
||
permissionPreviewUserId.value = userId
|
||
void loadTemplatePermissions()
|
||
}
|
||
|
||
function retryTemplatePermissions() {
|
||
if (templateCatalogError.value) {
|
||
templatesLoaded.value = false
|
||
void loadTemplateCatalog().then(loadTemplatePermissions)
|
||
return
|
||
}
|
||
|
||
void loadTemplatePermissions()
|
||
}
|
||
|
||
async function addTemplatePermission(form: TemplatePermissionForm) {
|
||
if (
|
||
!selectedTemplateId.value ||
|
||
templatePermissionSaving.value ||
|
||
templatePermissionError.value ||
|
||
!canEditTemplateExceptions.value
|
||
) {
|
||
return
|
||
}
|
||
|
||
if (isPermissionMockEnabled) {
|
||
ElMessage.info('演示模式不保存模板例外;真实环境会提交权限批量接口')
|
||
return
|
||
}
|
||
|
||
templatePermissionSaving.value = true
|
||
try {
|
||
await batchSaveTemplatePermissions({
|
||
operations: [
|
||
{
|
||
templateId: selectedTemplateId.value,
|
||
action: 'ADD',
|
||
...form,
|
||
},
|
||
],
|
||
})
|
||
await loadTemplatePermissions()
|
||
ElMessage.success('模板直接权限已添加')
|
||
} catch (error: unknown) {
|
||
ElMessage.error(formatError(error, '模板直接权限添加失败,请稍后重试'))
|
||
} finally {
|
||
templatePermissionSaving.value = false
|
||
}
|
||
}
|
||
|
||
async function removeTemplatePermission(permissionId: string) {
|
||
if (
|
||
!selectedTemplateId.value ||
|
||
templatePermissionSaving.value ||
|
||
templatePermissionError.value ||
|
||
!canEditTemplateExceptions.value
|
||
) {
|
||
return
|
||
}
|
||
|
||
const permission = templatePermissions.value.find((item) => item.id === permissionId)
|
||
if (!permission || permission.inherited) {
|
||
ElMessage.warning('继承权限不可直接删除')
|
||
return
|
||
}
|
||
|
||
if (isPermissionMockEnabled) {
|
||
ElMessage.info('演示模式不保存模板例外;真实环境会提交权限批量接口')
|
||
return
|
||
}
|
||
|
||
const operation: TemplatePermissionBatchItemRequest = {
|
||
templateId: selectedTemplateId.value,
|
||
action: 'REVOKE',
|
||
permissionId,
|
||
}
|
||
templatePermissionSaving.value = true
|
||
try {
|
||
await batchSaveTemplatePermissions({ operations: [operation] })
|
||
await loadTemplatePermissions()
|
||
ElMessage.success('模板直接权限已删除')
|
||
} catch (error: unknown) {
|
||
ElMessage.error(formatError(error, '模板直接权限删除失败,请稍后重试'))
|
||
} finally {
|
||
templatePermissionSaving.value = false
|
||
}
|
||
}
|
||
|
||
function closeDepartmentDrawer() {
|
||
departmentDrawerVisible.value = false
|
||
}
|
||
|
||
watch(
|
||
() => appStore.selectedCampusId,
|
||
(campusId, previousCampusId) => {
|
||
if (campusId && previousCampusId && campusId !== previousCampusId) {
|
||
departmentDrawerVisible.value = false
|
||
templatesLoaded.value = false
|
||
templates.value = []
|
||
selectedTemplateId.value = ''
|
||
templatePermissions.value = []
|
||
subjectsLoaded.value = false
|
||
userRecords.value = []
|
||
void loadPolicies()
|
||
}
|
||
},
|
||
)
|
||
|
||
onMounted(() => {
|
||
void loadPolicies()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<section class="permissions-page">
|
||
<section class="page-card intro-card">
|
||
<div class="card-heading">
|
||
<h2>文档库权限矩阵</h2>
|
||
<span>可见 / 可用 / 可维护分级授权,下级继承上级</span>
|
||
</div>
|
||
<p>
|
||
可见=可浏览模板;可用=可发起签署;可维护=可编辑/停用模板。科室签署员默认继承本科室「可用」权限。调整即时生效并记录审计日志。
|
||
</p>
|
||
</section>
|
||
|
||
<section v-if="loading" class="page-card loading-card" aria-label="正在加载权限配置">
|
||
<span class="loading-title" />
|
||
<span class="loading-row" />
|
||
<span class="loading-row" />
|
||
<span class="loading-row short" />
|
||
</section>
|
||
|
||
<section v-else-if="policyError" class="page-card state-card error-state">
|
||
<span>{{ policyError }}</span>
|
||
<button type="button" class="retry-button" @click="loadPolicies">重新加载</button>
|
||
</section>
|
||
|
||
<template v-else>
|
||
<section class="page-card">
|
||
<div class="card-heading section-heading">
|
||
<div>
|
||
<h2>按角色授权</h2>
|
||
<p>配置当前院区的默认角色策略;模板例外在科室“调整”抽屉中维护。</p>
|
||
</div>
|
||
<span class="policy-summary">默认策略 · {{ templateCount }} 个模板例外入口</span>
|
||
</div>
|
||
<RolePermissionMatrix
|
||
:rows="roleRows"
|
||
:columns="permissionColumns"
|
||
:saving="Boolean(defaultSavingId)"
|
||
:can-edit="canEditPolicy"
|
||
@toggle="toggleRolePolicy"
|
||
/>
|
||
<p v-if="!canEditPolicy" class="legend">当前账号仅可查看角色默认策略。</p>
|
||
</section>
|
||
|
||
<section class="page-card department-card">
|
||
<div class="card-heading section-heading">
|
||
<div>
|
||
<h2>按科室授权</h2>
|
||
<p>科室级别的文档可用范围;调整后即时生效。</p>
|
||
</div>
|
||
<span class="policy-summary">当前院区:{{ appStore.selectedCampus || '—' }}</span>
|
||
</div>
|
||
<DepartmentPermissionTable
|
||
:rows="departmentRows"
|
||
:can-edit="canEditPolicy"
|
||
@adjust="openDepartmentDrawer"
|
||
/>
|
||
</section>
|
||
</template>
|
||
|
||
<DepartmentPermissionDrawer
|
||
:visible="departmentDrawerVisible"
|
||
:department="selectedDepartment"
|
||
:templates="templates"
|
||
:selected-template-id="selectedTemplateId"
|
||
:permissions="templatePermissions"
|
||
:subjects="templateSubjects"
|
||
:policy-saving="Boolean(defaultSavingId)"
|
||
:template-catalog-loading="templateCatalogLoading"
|
||
:template-loading="templatePermissionLoading"
|
||
:template-saving="templatePermissionSaving"
|
||
:template-error="templatePermissionError"
|
||
:template-error-message="templatePermissionErrorMessage"
|
||
:template-catalog-error="templateCatalogError"
|
||
:preview-user-id="permissionPreviewUserId"
|
||
:can-edit-policy="canEditPolicy"
|
||
:can-edit-template="canEditTemplateExceptions"
|
||
@close="closeDepartmentDrawer"
|
||
@save-policy="saveDepartmentPolicy"
|
||
@update:selected-template-id="handleTemplateChange"
|
||
@update:preview-user-id="handlePreviewUserChange"
|
||
@add="addTemplatePermission"
|
||
@delete="removeTemplatePermission"
|
||
@retry="retryTemplatePermissions"
|
||
/>
|
||
</section>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.permissions-page {
|
||
width: 100%;
|
||
max-width: 1440px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.page-card {
|
||
padding: 16px 18px;
|
||
margin-bottom: 16px;
|
||
background: var(--card);
|
||
border-radius: var(--r);
|
||
box-shadow: var(--sh);
|
||
}
|
||
|
||
.intro-card {
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.card-heading {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.section-heading {
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.card-heading h2 {
|
||
margin: 0;
|
||
color: var(--ink);
|
||
font-size: 14.5px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.card-heading p {
|
||
margin: 5px 0 0;
|
||
color: var(--mut);
|
||
font-size: 11.5px;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.card-heading > span {
|
||
color: var(--mut);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.intro-card p {
|
||
margin: 0;
|
||
color: var(--mut);
|
||
font-size: 12px;
|
||
line-height: 1.8;
|
||
}
|
||
|
||
.policy-summary {
|
||
flex-shrink: 0;
|
||
padding-top: 4px;
|
||
font-size: 11.5px !important;
|
||
}
|
||
|
||
.department-card {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.loading-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
min-height: 180px;
|
||
}
|
||
|
||
.loading-card span {
|
||
display: block;
|
||
background: linear-gradient(90deg, #edf3f5 25%, #f7fafb 37%, #edf3f5 63%);
|
||
background-size: 400% 100%;
|
||
border-radius: 6px;
|
||
animation: skeleton-shimmer 1.4s ease infinite;
|
||
}
|
||
|
||
.loading-title {
|
||
width: 28%;
|
||
height: 20px;
|
||
}
|
||
|
||
.loading-row {
|
||
width: 100%;
|
||
height: 14px;
|
||
}
|
||
|
||
.loading-row.short {
|
||
width: 70%;
|
||
}
|
||
|
||
.state-card {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.error-state {
|
||
color: var(--err);
|
||
}
|
||
|
||
.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);
|
||
}
|
||
|
||
.legend {
|
||
margin: 10px 0 0;
|
||
color: var(--mut);
|
||
font-size: 11px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
@keyframes skeleton-shimmer {
|
||
0% {
|
||
background-position: 100% 0;
|
||
}
|
||
|
||
100% {
|
||
background-position: -100% 0;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 760px) {
|
||
.section-heading {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.policy-summary {
|
||
padding-top: 0;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
.card-heading {
|
||
align-items: flex-start;
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
</style>
|