feat(permissions): implement document permission matrix
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import type { DepartmentDocumentAccess, DepartmentPermissionRow } from '../types'
|
||||
|
||||
defineProps<{
|
||||
rows: DepartmentPermissionRow[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
adjust: [row: DepartmentPermissionRow]
|
||||
}>()
|
||||
|
||||
const accessLabels: Record<DepartmentDocumentAccess, string> = {
|
||||
available: '可用',
|
||||
invisible: '不可见',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="table-scroll">
|
||||
<table class="permission-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>科室</th>
|
||||
<th>本科室文档</th>
|
||||
<th>全院通用文档</th>
|
||||
<th>其他科室文档</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in rows" :key="row.id">
|
||||
<td>
|
||||
<strong>{{ row.department }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span class="access-tag" :class="'is-' + row.ownDocuments">
|
||||
{{ accessLabels[row.ownDocuments] }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="access-tag" :class="'is-' + row.commonDocuments">
|
||||
{{ accessLabels[row.commonDocuments] }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="access-tag" :class="'is-' + row.otherDocuments">
|
||||
{{ accessLabels[row.otherDocuments] }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="adjust-button" @click="emit('adjust', row)">调整</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.table-scroll {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.permission-table {
|
||||
width: 100%;
|
||||
min-width: 640px;
|
||||
font-size: 13px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.permission-table th {
|
||||
padding: 9px 12px;
|
||||
color: var(--mut);
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
background: #f5f9fa;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.permission-table td {
|
||||
padding: 9px 12px;
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid #eef4f6;
|
||||
}
|
||||
|
||||
.permission-table tbody tr:hover td {
|
||||
background: #f7fbfc;
|
||||
}
|
||||
|
||||
.access-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.access-tag.is-available {
|
||||
color: var(--ok);
|
||||
background: var(--ok-l);
|
||||
}
|
||||
|
||||
.access-tag.is-invisible {
|
||||
color: #8a9aa3;
|
||||
background: #eceef0;
|
||||
}
|
||||
|
||||
.adjust-button {
|
||||
padding: 4px 10px;
|
||||
color: var(--brand);
|
||||
font-size: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--brand);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.adjust-button:hover {
|
||||
background: var(--brand-l);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,144 @@
|
||||
<script setup lang="ts">
|
||||
import type { PermissionColumn, PermissionKey, RolePermissionRow } from '../types'
|
||||
|
||||
defineProps<{
|
||||
rows: RolePermissionRow[]
|
||||
columns: PermissionColumn[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggle: [roleId: string, permission: PermissionKey]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="permission-grid" role="table" aria-label="按角色授权矩阵">
|
||||
<div class="grid-cell grid-header role-header" role="columnheader">角色</div>
|
||||
<div
|
||||
v-for="column in columns"
|
||||
:key="column.key"
|
||||
class="grid-cell grid-header"
|
||||
role="columnheader"
|
||||
:title="column.description"
|
||||
>
|
||||
{{ column.label }}
|
||||
</div>
|
||||
|
||||
<template v-for="row in rows" :key="row.id">
|
||||
<div class="grid-cell role-name" role="rowheader">
|
||||
<span class="role-color" :style="{ backgroundColor: row.color }" aria-hidden="true" />
|
||||
{{ row.name }}
|
||||
</div>
|
||||
<div v-for="column in columns" :key="column.key" class="grid-cell permission-cell">
|
||||
<button
|
||||
type="button"
|
||||
class="permission-toggle"
|
||||
:class="{ active: row.permissions[column.key] }"
|
||||
:aria-pressed="row.permissions[column.key]"
|
||||
:aria-label="`${row.name}:${column.label}${row.permissions[column.key] ? ',已开启' : ',已关闭'}`"
|
||||
:title="`${column.label}:${column.description}`"
|
||||
@click="emit('toggle', row.id, column.key)"
|
||||
>
|
||||
<span class="switch-track" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.permission-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 140px repeat(3, minmax(100px, 1fr));
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.grid-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.grid-header {
|
||||
justify-content: center;
|
||||
color: var(--mut);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.role-header {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.role-name {
|
||||
gap: 7px;
|
||||
color: var(--ink);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.role-color {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.permission-cell {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.permission-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.permission-toggle:focus-visible {
|
||||
outline: 2px solid var(--brand);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.switch-track {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
background: #cbd8de;
|
||||
border-radius: 20px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.switch-track::after {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
content: '';
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
transition: left 0.2s ease;
|
||||
}
|
||||
|
||||
.permission-toggle.active .switch-track {
|
||||
background: var(--ok);
|
||||
}
|
||||
|
||||
.permission-toggle.active .switch-track::after {
|
||||
left: 18px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.permission-grid {
|
||||
grid-template-columns: 120px repeat(3, minmax(76px, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,187 @@
|
||||
<script setup lang="ts">
|
||||
import PagePlaceholder from '@/components/PagePlaceholder.vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import DepartmentPermissionTable from './components/DepartmentPermissionTable.vue'
|
||||
import RolePermissionMatrix from './components/RolePermissionMatrix.vue'
|
||||
import { departmentPermissionRows, permissionColumns, rolePermissionRows } from './mock'
|
||||
import type { DepartmentPermissionRow, PermissionKey, RolePermissionRow } from './types'
|
||||
|
||||
const loading = ref(true)
|
||||
const roles = ref<RolePermissionRow[]>([])
|
||||
const departments = ref<DepartmentPermissionRow[]>([])
|
||||
|
||||
async function loadPermissions() {
|
||||
loading.value = true
|
||||
await Promise.resolve()
|
||||
roles.value = rolePermissionRows.map((role) => ({
|
||||
...role,
|
||||
permissions: { ...role.permissions },
|
||||
}))
|
||||
departments.value = departmentPermissionRows.map((row) => ({ ...row }))
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function toggleRolePermission(roleId: string, permission: PermissionKey) {
|
||||
const role = roles.value.find((item) => item.id === roleId)
|
||||
|
||||
if (!role) {
|
||||
return
|
||||
}
|
||||
|
||||
role.permissions[permission] = !role.permissions[permission]
|
||||
ElMessage.info('权限已调整并记录审计日志')
|
||||
}
|
||||
|
||||
function adjustDepartmentPermission(row: DepartmentPermissionRow) {
|
||||
ElMessage.info(`原型演示:调整${row.department}文档权限`)
|
||||
}
|
||||
|
||||
onMounted(loadPermissions)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PagePlaceholder
|
||||
group="管理"
|
||||
title="文档权限"
|
||||
icon="⊞"
|
||||
description="文档权限页面占位,待根据 Web 端需求实现。"
|
||||
<section class="permissions-page">
|
||||
<section class="page-card intro-card">
|
||||
<div class="card-heading">
|
||||
<h2>文档库权限矩阵</h2>
|
||||
<span>可见 / 可用 / 可维护 分级授权,下级继承上级</span>
|
||||
</div>
|
||||
<p>
|
||||
可见=可浏览模板;可用=可发起签署;可维护=可编辑/停用模板。科室签署员默认继承本科室「可用」权限。调整即时生效并记录审计日志。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<template v-if="loading">
|
||||
<section 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>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<section class="page-card">
|
||||
<div class="card-heading">
|
||||
<h2>按角色授权</h2>
|
||||
</div>
|
||||
<RolePermissionMatrix
|
||||
:rows="roles"
|
||||
:columns="permissionColumns"
|
||||
@toggle="toggleRolePermission"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="page-card department-card">
|
||||
<div class="card-heading">
|
||||
<h2>按科室授权</h2>
|
||||
<span>科室级别的文档可用范围</span>
|
||||
</div>
|
||||
<DepartmentPermissionTable :rows="departments" @adjust="adjustDepartmentPermission" />
|
||||
</section>
|
||||
</template>
|
||||
</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;
|
||||
}
|
||||
|
||||
.card-heading h2 {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-size: 14.5px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.card-heading span {
|
||||
margin-left: auto;
|
||||
color: var(--mut);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.intro-card p {
|
||||
margin: 0;
|
||||
color: var(--mut);
|
||||
font-size: 12px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.department-card {
|
||||
margin-top: 16px;
|
||||
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%;
|
||||
}
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
0% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -100% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.card-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-heading span {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
108
clinical-web/src/views/management/document-permissions/mock.ts
Normal file
108
clinical-web/src/views/management/document-permissions/mock.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import type { DepartmentPermissionRow, PermissionColumn, RolePermissionRow } from './types'
|
||||
|
||||
export const permissionColumns: PermissionColumn[] = [
|
||||
{
|
||||
key: 'visible',
|
||||
label: '可见',
|
||||
description: '可浏览模板',
|
||||
},
|
||||
{
|
||||
key: 'usable',
|
||||
label: '可用',
|
||||
description: '可发起签署',
|
||||
},
|
||||
{
|
||||
key: 'maintainable',
|
||||
label: '可维护',
|
||||
description: '可编辑、停用模板',
|
||||
},
|
||||
]
|
||||
|
||||
export const rolePermissionRows: RolePermissionRow[] = [
|
||||
{
|
||||
id: 'system-admin',
|
||||
name: '系统管理员',
|
||||
color: '#6b5bd2',
|
||||
description: '系统配置、用户与权限管理、审计日志',
|
||||
userCount: 2,
|
||||
permissions: {
|
||||
visible: true,
|
||||
usable: true,
|
||||
maintainable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'medical-management',
|
||||
name: '医务管理',
|
||||
color: '#0e6e8c',
|
||||
description: '全院文档库维护、报表分析、任务监督',
|
||||
userCount: 4,
|
||||
permissions: {
|
||||
visible: true,
|
||||
usable: true,
|
||||
maintainable: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'department-signer',
|
||||
name: '科室签署员',
|
||||
color: '#0f9d6c',
|
||||
description: '本科室文档发起签署、手写板 / 短信操作',
|
||||
userCount: 23,
|
||||
permissions: {
|
||||
visible: false,
|
||||
usable: false,
|
||||
maintainable: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'audit-viewer',
|
||||
name: '查询审计员',
|
||||
color: '#d9821f',
|
||||
description: '只读查询报表与审计留痕,不可发起',
|
||||
userCount: 3,
|
||||
permissions: {
|
||||
visible: false,
|
||||
usable: false,
|
||||
maintainable: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export const departmentPermissionRows: DepartmentPermissionRow[] = [
|
||||
{
|
||||
id: 'radiology',
|
||||
department: '放射科',
|
||||
ownDocuments: 'available',
|
||||
commonDocuments: 'available',
|
||||
otherDocuments: 'invisible',
|
||||
},
|
||||
{
|
||||
id: 'cardiology',
|
||||
department: '心内科',
|
||||
ownDocuments: 'available',
|
||||
commonDocuments: 'available',
|
||||
otherDocuments: 'invisible',
|
||||
},
|
||||
{
|
||||
id: 'orthopedics',
|
||||
department: '骨科',
|
||||
ownDocuments: 'available',
|
||||
commonDocuments: 'available',
|
||||
otherDocuments: 'invisible',
|
||||
},
|
||||
{
|
||||
id: 'health-management',
|
||||
department: '健康管理中心',
|
||||
ownDocuments: 'available',
|
||||
commonDocuments: 'available',
|
||||
otherDocuments: 'invisible',
|
||||
},
|
||||
{
|
||||
id: 'general',
|
||||
department: '全院通用',
|
||||
ownDocuments: 'available',
|
||||
commonDocuments: 'available',
|
||||
otherDocuments: 'invisible',
|
||||
},
|
||||
]
|
||||
@@ -1,15 +1,26 @@
|
||||
export type PermissionSubjectFilter = 'all' | 'user' | 'role'
|
||||
export type PermissionKey = 'visible' | 'usable' | 'maintainable'
|
||||
|
||||
export interface PermissionFilterForm {
|
||||
keyword: string
|
||||
subjectType: PermissionSubjectFilter
|
||||
export interface PermissionColumn {
|
||||
key: PermissionKey
|
||||
label: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface PermissionTableRow {
|
||||
export interface RolePermissionRow {
|
||||
id: string
|
||||
subjectType: PermissionSubjectFilter
|
||||
subjectName: string
|
||||
documentName: string
|
||||
permission: 'view' | 'sign' | 'manage'
|
||||
updatedAt: string
|
||||
name: string
|
||||
color: string
|
||||
description: string
|
||||
userCount: number
|
||||
permissions: Record<PermissionKey, boolean>
|
||||
}
|
||||
|
||||
export type DepartmentDocumentAccess = 'available' | 'invisible'
|
||||
|
||||
export interface DepartmentPermissionRow {
|
||||
id: string
|
||||
department: string
|
||||
ownDocuments: DepartmentDocumentAccess
|
||||
commonDocuments: DepartmentDocumentAccess
|
||||
otherDocuments: DepartmentDocumentAccess
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user