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

497 lines
12 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 { 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
error: boolean
errorMessage?: string
catalogError?: string
catalogLoading?: boolean
previewUserId: string
canEdit?: boolean
}>()
const emit = defineEmits<{
'update:selectedTemplateId': [value: string]
'update:previewUserId': [value: string]
add: [form: TemplatePermissionForm]
delete: [permissionId: string]
retry: []
}>()
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 previewUsers = computed(() => props.subjects.filter((subject) => subject.type === 'USER'))
const selectedTemplate = computed(() =>
props.templates.find((template) => template.id === props.selectedTemplateId),
)
function handleTemplateChange(event: Event) {
emit('update:selectedTemplateId', (event.target as HTMLSelectElement).value)
}
function handlePreviewUserChange(event: Event) {
emit('update:previewUserId', (event.target as HTMLSelectElement).value)
}
function handleSubjectTypeChange() {
form.subjectId = availableSubjects.value[0]?.id ?? ''
}
function submit() {
if (!form.subjectId) {
return
}
emit('add', { ...form })
}
function deletePermission(permissionId: string | undefined) {
if (permissionId) {
emit('delete', permissionId)
}
}
function formatSubject(subjectType: PermissionSubjectOption['type'], subjectId: string | null) {
if (!subjectId) {
return '系统默认继承规则'
}
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 v-if="templates.length" 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-else class="template-empty-label">当前院区暂无可配置模板</span>
<span v-if="selectedTemplate" class="template-meta">
{{ selectedTemplate.code || selectedTemplate.id }} · {{ selectedTemplate.category }}
</span>
<label class="template-select preview-select">
<span>继承预览用户</span>
<select :value="previewUserId" :disabled="loading" @change="handlePreviewUserChange">
<option value="">当前登录用户</option>
<option v-for="user in previewUsers" :key="user.id" :value="user.id">
{{ user.label }}
</option>
</select>
</label>
</div>
<div v-if="catalogError" class="error-state">
<span>{{ catalogError }}</span>
<button type="button" class="retry-button" @click="emit('retry')">重新加载</button>
</div>
<div v-else-if="catalogLoading" class="loading-state">正在加载模板目录…</div>
<div v-else-if="!templates.length" class="empty-state">
暂无模板例外
<small>当前院区还没有可配置的文档模板。</small>
</div>
<div v-else-if="!selectedTemplateId" class="empty-state">请选择一个模板查看例外权限。</div>
<div v-else-if="loading" class="loading-state">正在加载模板权限…</div>
<div v-else-if="error" class="error-state">
<span>{{ errorMessage || '模板权限加载失败,请稍后重试' }}</span>
<button type="button" class="retry-button" @click="emit('retry')">重新加载</button>
</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 ??
[
permission.subjectType,
permission.subjectId,
permission.permissionLevel,
permission.effect,
].join('-')
"
>
<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 || props.canEdit === false"
:title="
permission.inherited
? '继承权限不可直接删除'
: props.canEdit === false
? '当前账号无直接权限修改权限'
: '删除权限'
"
@click="deletePermission(permission.id)"
>
删除
</button>
</td>
</tr>
</tbody>
</table>
</div>
<p v-else class="empty-state compact">当前模板暂无直接权限配置。</p>
</div>
<form v-if="props.canEdit !== false" 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>
<p v-else class="readonly-tip">当前账号仅可查看模板例外,新增和删除操作已禁用。</p>
</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%);
}
.preview-select {
min-width: min(280px, 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;
}
.template-empty-label {
padding-bottom: 8px;
color: var(--mut);
font-size: 12px;
}
.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,
.error-state {
padding: 32px 16px;
color: var(--mut);
font-size: 12.5px;
text-align: center;
}
.empty-state small {
display: block;
margin-top: 5px;
color: var(--mut);
font-size: 11px;
}
.error-state {
display: flex;
gap: 10px;
align-items: center;
justify-content: center;
color: var(--err);
background: #f8fbfc;
border-radius: 6px;
}
.retry-button {
padding: 4px 10px;
color: var(--brand);
font-size: 12px;
background: #fff;
border: 1px solid var(--brand);
border-radius: 5px;
}
.retry-button:hover {
background: var(--brand-l);
}
.empty-state.compact {
padding: 18px 12px;
margin: 0;
background: #f8fbfc;
}
.readonly-tip {
padding: 10px 12px;
margin: 0;
color: var(--mut);
font-size: 11.5px;
background: #f8fbfc;
border-radius: 6px;
}
@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>