优化构架
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { LockKeyhole, RotateCcw, Save, ShieldCheck } from '@lucide/vue'
|
||||
import { TENANT_PERMISSION_GROUPS, useTenantAdministrationStore } from '@/stores/tenantAdministration'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const store = useTenantAdministrationStore()
|
||||
const auth = useAuthStore()
|
||||
const route = useRoute()
|
||||
const initialRoleId = typeof route.query.role === 'string' && store.roles.some((role) => role.id === route.query.role) ? route.query.role : store.roles[0]?.id ?? ''
|
||||
const selectedRoleId = ref(initialRoleId)
|
||||
const selectedCodes = ref<string[]>([])
|
||||
const savedCodes = ref<string[]>([])
|
||||
const loading = ref(false)
|
||||
const loadError = ref('')
|
||||
const saving = ref(false)
|
||||
|
||||
const selectedRole = computed(() => store.roles.find((role) => role.id === selectedRoleId.value))
|
||||
const editable = computed(() => Boolean(auth.hasPermission('company:permission:assign') && selectedRole.value && !selectedRole.value.builtin && selectedRole.value.status === 'ACTIVE'))
|
||||
const hasChanges = computed(() => [...selectedCodes.value].sort().join('|') !== [...savedCodes.value].sort().join('|'))
|
||||
|
||||
function loadRolePermissions() {
|
||||
const codes = selectedRole.value?.permissionCodes ?? []
|
||||
selectedCodes.value = [...codes]
|
||||
savedCodes.value = [...codes]
|
||||
}
|
||||
|
||||
watch(selectedRoleId, loadRolePermissions, { immediate: true })
|
||||
|
||||
async function loadRoles() {
|
||||
loading.value = true
|
||||
loadError.value = ''
|
||||
try {
|
||||
await store.loadRoles()
|
||||
if (!store.roles.some((role) => role.id === selectedRoleId.value)) selectedRoleId.value = store.roles[0]?.id ?? ''
|
||||
loadRolePermissions()
|
||||
} catch (error) {
|
||||
loadError.value = error instanceof Error ? error.message : '角色列表加载失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => void loadRoles())
|
||||
|
||||
function groupCodes(groupKey: string) {
|
||||
return TENANT_PERMISSION_GROUPS.find((group) => group.key === groupKey)?.items.map((item) => item.code) ?? []
|
||||
}
|
||||
|
||||
function groupChecked(groupKey: string) {
|
||||
const codes = groupCodes(groupKey)
|
||||
return codes.length > 0 && codes.every((code) => selectedCodes.value.includes(code))
|
||||
}
|
||||
|
||||
function groupIndeterminate(groupKey: string) {
|
||||
const codes = groupCodes(groupKey)
|
||||
const count = codes.filter((code) => selectedCodes.value.includes(code)).length
|
||||
return count > 0 && count < codes.length
|
||||
}
|
||||
|
||||
function toggleGroup(groupKey: string, checked: boolean) {
|
||||
const codes = groupCodes(groupKey)
|
||||
if (checked) selectedCodes.value = [...new Set([...selectedCodes.value, ...codes])]
|
||||
else selectedCodes.value = selectedCodes.value.filter((code) => !codes.includes(code))
|
||||
}
|
||||
|
||||
function resetPermissions() {
|
||||
selectedCodes.value = [...savedCodes.value]
|
||||
}
|
||||
|
||||
async function savePermissions() {
|
||||
if (!selectedRole.value || !editable.value || saving.value) return
|
||||
if (!selectedCodes.value.includes('account:profile:view')) selectedCodes.value.unshift('account:profile:view')
|
||||
if (!selectedCodes.value.includes('dashboard:view')) selectedCodes.value.unshift('dashboard:view')
|
||||
saving.value = true
|
||||
try {
|
||||
await store.setRolePermissions(selectedRole.value.id, selectedCodes.value)
|
||||
savedCodes.value = [...selectedCodes.value]
|
||||
ElMessage.success('角色菜单及操作权限已保存')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-loading="loading" class="permissions-page">
|
||||
<div v-if="loadError" class="client-card permission-load-error" role="alert">
|
||||
<div><strong>角色权限加载失败</strong><span>{{ loadError }}</span></div>
|
||||
<el-button :loading="loading" @click="loadRoles">重新加载</el-button>
|
||||
</div>
|
||||
<template v-else-if="store.roles.length">
|
||||
<aside class="client-card role-selector" aria-label="选择角色">
|
||||
<button v-for="role in store.roles" :key="role.id" type="button" :class="{ active: role.id === selectedRoleId }" @click="selectedRoleId = role.id">
|
||||
<span class="role-select-icon"><LockKeyhole v-if="role.builtin" :size="17" /><ShieldCheck v-else :size="17" /></span>
|
||||
<span><strong>{{ role.name }}</strong><small>{{ role.code }}</small></span>
|
||||
<el-tag :type="role.status === 'ACTIVE' ? 'success' : 'info'" effect="plain" size="small">{{ role.status === 'ACTIVE' ? '启用' : '停用' }}</el-tag>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<div class="permission-content">
|
||||
<div class="permission-actions" aria-label="权限操作"><el-button :disabled="!editable || !hasChanges || saving" @click="resetPermissions"><RotateCcw :size="16" />撤销修改</el-button><el-button type="primary" :loading="saving" :disabled="!editable || !hasChanges || saving" @click="savePermissions"><Save :size="16" />保存权限</el-button></div>
|
||||
|
||||
<div v-if="selectedRole?.builtin" class="permission-notice"><LockKeyhole :size="18" /><span>当前角色为系统内置角色,权限由平台统一维护,不允许修改。</span></div>
|
||||
<div v-else-if="selectedRole?.status === 'INACTIVE'" class="permission-notice warning"><span>该角色已停用,请先启用角色后再调整权限。</span></div>
|
||||
<div v-else class="permission-notice"><ShieldCheck :size="18" /><span>保存时会全量替换该角色的权限,请确认所有需要保留的菜单和操作均已勾选。</span></div>
|
||||
|
||||
<div class="permission-groups">
|
||||
<article v-for="group in TENANT_PERMISSION_GROUPS" :key="group.key" class="client-card permission-group" :class="{ 'permission-group--single': group.items.length === 1 }">
|
||||
<header>
|
||||
<el-checkbox :model-value="groupChecked(group.key)" :indeterminate="groupIndeterminate(group.key)" :disabled="!editable" @change="toggleGroup(group.key, Boolean($event))"><strong>{{ group.label }}</strong></el-checkbox>
|
||||
<span>{{ group.items.filter((item) => selectedCodes.includes(item.code)).length }} / {{ group.items.length }}</span>
|
||||
</header>
|
||||
<el-checkbox-group v-model="selectedCodes" :disabled="!editable">
|
||||
<el-checkbox v-for="item in group.items" :key="item.code" :value="item.code" class="permission-item" :class="{ selected: selectedCodes.includes(item.code) }">
|
||||
<span class="permission-copy"><strong>{{ item.label }}</strong><small>{{ item.description }}</small><code>{{ item.code }}</code></span>
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="client-card permission-empty">当前企业暂无可用角色,创建角色后才能查看和分配权限。</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.permissions-page { display: grid; grid-template-columns: 250px minmax(0, 1fr); align-items: start; gap: 14px; }
|
||||
.permission-load-error, .permission-empty { grid-column: 1 / -1; min-height: 170px; }
|
||||
.permission-load-error { display: flex; align-items: center; justify-content: center; gap: 20px; }
|
||||
.permission-load-error > div { display: flex; max-width: 520px; flex-direction: column; gap: 6px; }
|
||||
.permission-load-error span { color: var(--muted); font-size: 12px; line-height: 1.6; }
|
||||
.permission-empty { display: grid; place-items: center; color: var(--muted); font-size: 12px; text-align: center; }
|
||||
.role-selector { position: sticky; top: 84px; padding: 12px; }
|
||||
.role-selector > button { display: flex; width: 100%; min-height: 58px; align-items: center; gap: 9px; margin: 3px 0; padding: 9px; border: 0; border-radius: 8px; background: transparent; color: var(--navy); cursor: pointer; text-align: left; }
|
||||
.role-selector > button:hover, .role-selector > button.active { background: #edf6f4; }
|
||||
.role-selector > button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
|
||||
.role-selector > button.active { color: var(--primary); }
|
||||
.role-select-icon { display: grid; width: 32px; height: 32px; flex: 0 0 32px; place-items: center; background: #f0f5f4; }
|
||||
.role-selector > button > span:nth-child(2) { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 3px; }
|
||||
.role-selector strong { overflow: hidden; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.role-selector small { color: var(--muted); font-size: 9px; }
|
||||
.permission-content { display: flex; min-width: 0; flex-direction: column; gap: 12px; }
|
||||
.permission-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
.permission-notice { display: flex; align-items: center; gap: 9px; padding: 12px 15px; border: 1px solid #cfe1dc; background: #edf6f4; color: #45615d; font-size: 11px; }
|
||||
.permission-notice.warning { border-color: #f1d4a8; background: #fff7ed; color: #9a5a09; }
|
||||
.permission-groups { display: grid; gap: 12px; }
|
||||
.permission-group { padding: 0; overflow: hidden; }
|
||||
.permission-group > header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 18px; border-bottom: 1px solid var(--border); background: #fafcfc; }
|
||||
.permission-group > header > span { color: var(--muted); font-size: 11px; }
|
||||
.permission-group :deep(.el-checkbox-group) { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.permission-group--single :deep(.el-checkbox-group) { grid-template-columns: minmax(0, 1fr); }
|
||||
.permission-item { display: flex; width: 100%; min-height: 104px; align-items: flex-start; margin: 0; padding: 16px 18px; border-bottom: 1px solid var(--border); cursor: pointer; }
|
||||
.permission-item:nth-child(odd) { border-right: 1px solid var(--border); }
|
||||
.permission-item:last-child:nth-child(odd) { grid-column: 1 / -1; border-right: 0; }
|
||||
.permission-item.selected { background: #f5faf9; }
|
||||
.permission-item :deep(.el-checkbox__input) { flex: 0 0 auto; margin-top: 3px; }
|
||||
.permission-item :deep(.el-checkbox__label) { display: flex; min-width: 0; flex: 1; padding-left: 10px; white-space: normal; }
|
||||
.permission-copy { display: flex; min-width: 0; flex-direction: column; gap: 5px; }
|
||||
.permission-copy strong { color: var(--navy); font-size: 13px; line-height: 1.35; }
|
||||
.permission-copy small { color: var(--muted); font-size: 11px; line-height: 1.5; }
|
||||
.permission-copy code { overflow-wrap: anywhere; color: #86939a; font-size: 10px; line-height: 1.35; }
|
||||
@media (max-width: 900px) { .permissions-page { grid-template-columns: 1fr; } .role-selector { position: static; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 720px) { .permission-actions { width: 100%; } .permission-actions .el-button { flex: 1; } .permission-group :deep(.el-checkbox-group) { grid-template-columns: 1fr; } .permission-item:nth-child(odd) { border-right: 0; } }
|
||||
@media (max-width: 480px) { .role-selector { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user