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">
|
<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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PagePlaceholder
|
<section class="permissions-page">
|
||||||
group="管理"
|
<section class="page-card intro-card">
|
||||||
title="文档权限"
|
<div class="card-heading">
|
||||||
icon="⊞"
|
<h2>文档库权限矩阵</h2>
|
||||||
description="文档权限页面占位,待根据 Web 端需求实现。"
|
<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>
|
||||||
|
|
||||||
|
<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 {
|
export interface PermissionColumn {
|
||||||
keyword: string
|
key: PermissionKey
|
||||||
subjectType: PermissionSubjectFilter
|
label: string
|
||||||
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PermissionTableRow {
|
export interface RolePermissionRow {
|
||||||
id: string
|
id: string
|
||||||
subjectType: PermissionSubjectFilter
|
name: string
|
||||||
subjectName: string
|
color: string
|
||||||
documentName: string
|
description: string
|
||||||
permission: 'view' | 'sign' | 'manage'
|
userCount: number
|
||||||
updatedAt: string
|
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