Files
xh-medical-sign-web/clinical-web/src/views/management/document-permissions/components/RolePermissionMatrix.vue
T

234 lines
5.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import type {
PermissionCellState,
PermissionColumn,
PermissionKey,
RolePermissionRow,
} from '../types'
const props = defineProps<{
rows: RolePermissionRow[]
columns: PermissionColumn[]
saving?: boolean
canEdit?: boolean
}>()
const emit = defineEmits<{
toggle: [roleId: string, permission: PermissionKey]
}>()
function getState(row: RolePermissionRow, permission: PermissionKey): PermissionCellState {
return (
row.permissionStates?.[permission] ?? (row.permissions[permission] ? 'direct-allow' : 'none')
)
}
function stateLabel(state: PermissionCellState) {
const labels: Record<PermissionCellState, string> = {
none: '未授权',
'direct-allow': '直接允许',
'direct-deny': '直接拒绝',
'inherited-allow': '继承允许',
'inherited-deny': '继承拒绝',
}
return labels[state]
}
function isDisabled(row: RolePermissionRow, permission: PermissionKey) {
return props.saving || props.canEdit === false || getState(row, permission) === 'inherited-deny'
}
</script>
<template>
<div v-if="!rows.length" class="empty-state">暂无角色数据</div>
<div v-else 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" />
<span class="role-info">
<strong>{{ row.name }}</strong>
<small v-if="row.description">{{ row.description }}</small>
<small v-if="row.userCount">{{ row.userCount }} 人</small>
</span>
</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],
inherited: getState(row, column.key).startsWith('inherited'),
denied: getState(row, column.key).endsWith('deny'),
}"
:disabled="isDisabled(row, column.key)"
:aria-pressed="row.permissions[column.key]"
:aria-label="
row.name + ':' + column.label + ',' + stateLabel(getState(row, column.key))
"
:title="
column.label +
':' +
column.description +
'(' +
stateLabel(getState(row, column.key)) +
')'
"
@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: minmax(190px, 1.5fr) 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);
align-items: flex-start;
font-weight: 700;
}
.role-color {
margin-top: 3px;
width: 10px;
height: 10px;
flex-shrink: 0;
border-radius: 3px;
}
.role-info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.role-info small {
overflow: hidden;
color: var(--mut);
font-size: 11px;
font-weight: 400;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.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.inherited.active .switch-track {
background: #4eaa8a;
box-shadow: inset 0 0 0 2px rgb(255 255 255 / 45%);
}
.permission-toggle.denied .switch-track {
background: #cbd8de;
}
.permission-toggle.active .switch-track::after {
left: 18px;
}
.permission-toggle:disabled {
cursor: not-allowed;
opacity: 0.58;
}
.empty-state {
padding: 28px 12px;
color: var(--mut);
font-size: 12.5px;
text-align: center;
background: #f8fbfc;
border-radius: 6px;
}
@media (max-width: 600px) {
.permission-grid {
grid-template-columns: 120px repeat(3, minmax(76px, 1fr));
gap: 6px;
}
}
</style>