feat(management): 接入文档用户权限与报表

This commit is contained in:
yelan
2026-09-01 10:13:11 +08:00
parent beeb98d18f
commit e467f03f23
12 changed files with 2361 additions and 41 deletions
@@ -0,0 +1,381 @@
<script setup lang="ts">
import { computed, reactive, watch } from 'vue'
import type { DocumentRecord, TemplatePermissionRecord } from '@/api/management/types'
import type { PermissionSubjectOption, TemplatePermissionForm } from '../types'
const props = defineProps<{
templates: DocumentRecord[]
selectedTemplateId: string
permissions: TemplatePermissionRecord[]
subjects: PermissionSubjectOption[]
loading: boolean
saving: boolean
}>()
const emit = defineEmits<{
'update:selectedTemplateId': [value: string]
add: [form: TemplatePermissionForm]
delete: [permissionId: string]
}>()
const form = reactive<TemplatePermissionForm>({
subjectType: 'ROLE',
subjectId: '',
permissionLevel: 'VIEW',
effect: 'ALLOW',
})
const subjectTypeLabels: Record<PermissionSubjectOption['type'], string> = {
ROLE: '角色',
DEPARTMENT: '科室',
USER: '用户',
}
const permissionLabels: Record<TemplatePermissionForm['permissionLevel'], string> = {
VIEW: '可见(查看)',
USE: '可用(发起签署)',
MAINTAIN: '可维护(编辑/停用)',
}
const effectLabels: Record<TemplatePermissionForm['effect'], string> = {
ALLOW: '允许',
DENY: '拒绝',
}
const availableSubjects = computed(() =>
props.subjects.filter((subject) => subject.type === form.subjectType),
)
const selectedTemplate = computed(() =>
props.templates.find((template) => template.id === props.selectedTemplateId),
)
function handleTemplateChange(event: Event) {
emit('update:selectedTemplateId', (event.target as HTMLSelectElement).value)
}
function handleSubjectTypeChange() {
form.subjectId = availableSubjects.value[0]?.id ?? ''
}
function submit() {
if (!form.subjectId) {
return
}
emit('add', { ...form })
}
function formatSubject(subjectType: PermissionSubjectOption['type'], subjectId: string) {
const subject = props.subjects.find((item) => item.type === subjectType && item.id === subjectId)
return subject?.label ?? subjectId
}
watch(
() => props.subjects,
() => {
if (!availableSubjects.value.some((subject) => subject.id === form.subjectId)) {
form.subjectId = availableSubjects.value[0]?.id ?? ''
}
},
{ immediate: true },
)
</script>
<template>
<section class="permission-panel">
<div class="panel-toolbar">
<label class="template-select">
<span>选择模板</span>
<select :value="selectedTemplateId" @change="handleTemplateChange">
<option value="" disabled>请选择模板</option>
<option v-for="template in templates" :key="template.id" :value="template.id">
{{ template.name }} · {{ template.version }}
</option>
</select>
</label>
<span v-if="selectedTemplate" class="template-meta">
{{ selectedTemplate.code || selectedTemplate.id }} · {{ selectedTemplate.category }}
</span>
</div>
<div v-if="!selectedTemplateId" class="empty-state">暂无模板,请先选择一个模板。</div>
<div v-else-if="loading" class="loading-state">正在加载模板权限…</div>
<template v-else>
<div class="permission-list">
<div class="list-heading">
<span>已配置权限</span>
<span>{{ permissions.length }} 条</span>
</div>
<div v-if="permissions.length" class="table-scroll">
<table>
<thead>
<tr>
<th>授权对象</th>
<th>权限</th>
<th>效果</th>
<th>来源</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="permission in permissions" :key="permission.id">
<td>
<span class="subject-type">{{ subjectTypeLabels[permission.subjectType] }}</span>
{{ formatSubject(permission.subjectType, permission.subjectId) }}
</td>
<td>{{ permissionLabels[permission.permissionLevel] }}</td>
<td>{{ effectLabels[permission.effect] }}</td>
<td>{{ permission.inherited ? '继承' : '直接' }}</td>
<td>{{ permission.createdAt }}</td>
<td>
<button
type="button"
class="delete-button"
:disabled="permission.inherited || saving"
:title="permission.inherited ? '继承权限不可直接删除' : '删除权限'"
@click="emit('delete', permission.id)"
>
删除
</button>
</td>
</tr>
</tbody>
</table>
</div>
<p v-else class="empty-state compact">当前模板暂无直接权限配置。</p>
</div>
<form class="add-form" @submit.prevent="submit">
<div class="list-heading">
<span>新增权限</span>
<span>服务端会再次校验授权范围</span>
</div>
<div class="form-grid">
<label>
<span>对象类型</span>
<select v-model="form.subjectType" @change="handleSubjectTypeChange">
<option v-for="(label, value) in subjectTypeLabels" :key="value" :value="value">
{{ label }}
</option>
</select>
</label>
<label>
<span>授权对象</span>
<select v-model="form.subjectId" :disabled="!availableSubjects.length">
<option value="" disabled>请选择对象</option>
<option v-for="subject in availableSubjects" :key="subject.id" :value="subject.id">
{{ subject.label }}
</option>
</select>
</label>
<label>
<span>权限级别</span>
<select v-model="form.permissionLevel">
<option v-for="(label, value) in permissionLabels" :key="value" :value="value">
{{ label }}
</option>
</select>
</label>
<label>
<span>授权效果</span>
<select v-model="form.effect">
<option v-for="(label, value) in effectLabels" :key="value" :value="value">
{{ label }}
</option>
</select>
</label>
<button type="submit" class="add-button" :disabled="saving || !form.subjectId">
{{ saving ? '保存中…' : '添加权限' }}
</button>
</div>
</form>
</template>
</section>
</template>
<style scoped>
.permission-panel {
display: flex;
flex-direction: column;
gap: 14px;
}
.panel-toolbar {
display: flex;
gap: 12px;
align-items: flex-end;
flex-wrap: wrap;
padding-bottom: 12px;
border-bottom: 1px solid var(--line);
}
.template-select,
.form-grid label {
display: flex;
flex-direction: column;
gap: 6px;
color: var(--mut);
font-size: 12px;
}
.template-select {
min-width: min(420px, 100%);
}
select {
min-width: 0;
padding: 7px 10px;
color: var(--ink);
font: inherit;
background: #fff;
border: 1px solid var(--line);
border-radius: 6px;
}
select:focus {
outline: none;
border-color: var(--brand);
}
select:disabled {
background: #f5f8f9;
}
.template-meta {
padding-bottom: 8px;
color: var(--mut);
font-size: 11.5px;
}
.list-heading {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
color: var(--ink);
font-size: 12.5px;
font-weight: 700;
}
.list-heading span:last-child {
color: var(--mut);
font-size: 11.5px;
font-weight: 400;
}
.table-scroll {
width: 100%;
overflow-x: auto;
}
table {
width: 100%;
min-width: 760px;
font-size: 12px;
border-collapse: collapse;
}
th {
padding: 8px 10px;
color: var(--mut);
font-weight: 700;
text-align: left;
white-space: nowrap;
background: #f5f9fa;
border-bottom: 1px solid var(--line);
}
td {
padding: 8px 10px;
color: var(--ink);
border-bottom: 1px solid #eef4f6;
}
.subject-type {
display: inline-block;
padding: 2px 6px;
margin-right: 5px;
color: var(--brand);
font-size: 10.5px;
background: var(--brand-l);
border-radius: 10px;
}
.delete-button {
padding: 3px 8px;
color: var(--err);
font-size: 11.5px;
background: #fff;
border: 1px solid rgb(194 73 73 / 32%);
border-radius: 5px;
}
.delete-button:disabled {
color: #aebbc0;
border-color: #dce5e8;
cursor: not-allowed;
}
.add-form {
padding-top: 14px;
border-top: 1px solid var(--line);
}
.form-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)) auto;
gap: 10px;
align-items: end;
}
.add-button {
padding: 7px 13px;
color: #fff;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
background: var(--brand);
border: 1px solid var(--brand);
border-radius: 6px;
}
.add-button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.empty-state,
.loading-state {
padding: 32px 16px;
color: var(--mut);
font-size: 12.5px;
text-align: center;
}
.empty-state.compact {
padding: 18px 12px;
margin: 0;
background: #f8fbfc;
}
@media (max-width: 960px) {
.form-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.add-button {
width: 100%;
}
}
@media (max-width: 600px) {
.form-grid {
grid-template-columns: 1fr;
}
}
</style>