chore: initial commit
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Check, 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 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 })
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await store.loadRoles()
|
||||
if (!store.roles.some((role) => role.id === selectedRoleId.value)) selectedRoleId.value = store.roles[0]?.id ?? ''
|
||||
loadRolePermissions()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '角色列表加载失败')
|
||||
}
|
||||
})
|
||||
|
||||
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) 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')
|
||||
try {
|
||||
await store.setRolePermissions(selectedRole.value.id, selectedCodes.value)
|
||||
savedCodes.value = [...selectedCodes.value]
|
||||
ElMessage.success('角色菜单及操作权限已保存')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '保存失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="permissions-page">
|
||||
<aside class="client-card role-selector">
|
||||
<header><div><span class="section-label">ROLES</span><h3>选择角色</h3></div></header>
|
||||
<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-heading client-card">
|
||||
<div><span class="section-label">PERMISSIONS</span><h2>{{ selectedRole?.name }}</h2><p>{{ selectedRole?.description }}</p></div>
|
||||
<div class="permission-actions"><el-button :disabled="!editable || !hasChanges" @click="resetPermissions"><RotateCcw :size="16" />撤销修改</el-button><el-button type="primary" :disabled="!editable || !hasChanges" @click="savePermissions"><Save :size="16" />保存权限</el-button></div>
|
||||
</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">
|
||||
<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">
|
||||
<label v-for="item in group.items" :key="item.code" class="permission-item" :class="{ selected: selectedCodes.includes(item.code) }">
|
||||
<el-checkbox :value="item.code"><span class="permission-check"><Check :size="14" /></span></el-checkbox>
|
||||
<span><strong>{{ item.label }}</strong><small>{{ item.description }}</small><code>{{ item.code }}</code></span>
|
||||
</label>
|
||||
</el-checkbox-group>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.permissions-page { display: grid; grid-template-columns: 250px minmax(0, 1fr); align-items: start; gap: 14px; }
|
||||
.role-selector { position: sticky; top: 84px; padding: 12px; }
|
||||
.role-selector header { padding: 8px 8px 12px; }
|
||||
.role-selector h3 { margin-bottom: 0; }
|
||||
.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.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-heading { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
|
||||
.permission-heading h2 { margin: 5px 0; font-size: 19px; }
|
||||
.permission-heading p { margin: 0; color: var(--muted); font-size: 11px; }
|
||||
.permission-actions { display: flex; gap: 8px; }
|
||||
.permission-notice { display: flex; align-items: center; gap: 9px; padding: 12px 15px; border-left: 3px solid var(--primary); background: #edf6f4; color: #45615d; font-size: 11px; }
|
||||
.permission-notice.warning { border-left-color: #d97706; background: #fff7ed; color: #9a5a09; }
|
||||
.permission-groups { display: grid; gap: 12px; }
|
||||
.permission-group { padding: 0; overflow: hidden; }
|
||||
.permission-group > header { 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-item { display: flex; min-height: 92px; align-items: flex-start; gap: 9px; padding: 16px 18px; border-bottom: 1px solid var(--border); cursor: pointer; }
|
||||
.permission-item:nth-child(odd) { border-right: 1px solid var(--border); }
|
||||
.permission-item.selected { background: #f5faf9; }
|
||||
.permission-item :deep(.el-checkbox__label) { display: none; }
|
||||
.permission-check { display: none; }
|
||||
.permission-item > span { display: flex; min-width: 0; flex-direction: column; gap: 4px; }
|
||||
.permission-item strong { color: var(--navy); font-size: 12px; }
|
||||
.permission-item small { color: var(--muted); font-size: 10px; line-height: 1.5; }
|
||||
.permission-item code { color: #86939a; font-size: 9px; }
|
||||
@media (max-width: 900px) { .permissions-page { grid-template-columns: 1fr; } .role-selector { position: static; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); } .role-selector header { grid-column: 1 / -1; } }
|
||||
@media (max-width: 720px) { .permission-heading { align-items: flex-start; flex-direction: column; } .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; } .role-selector header { grid-column: auto; } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user