feat(permissions): implement document permission matrix

This commit is contained in:
yelan
2026-08-27 16:47:55 +08:00
parent e7aa4843b7
commit 94a0d88f8b
5 changed files with 578 additions and 17 deletions
@@ -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>