294 lines
9.8 KiB
Vue
294 lines
9.8 KiB
Vue
<template>
|
|
<div class="roles-manager">
|
|
<div class="admin-section-header">
|
|
<div>
|
|
<h2 class="text-xl font-black text-zinc-100 flex items-center gap-2">
|
|
<Shield class="w-6 h-6 text-indigo-400" />
|
|
权限组管理
|
|
</h2>
|
|
<p class="text-sm text-zinc-500 mt-1">管理各角色可以访问的后台页面功能</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<button
|
|
class="px-4 py-2 bg-indigo-500/10 hover:bg-indigo-500/20 text-indigo-400 rounded-xl font-bold transition-all disabled:opacity-50 flex items-center gap-2"
|
|
:disabled="loading || saving"
|
|
@click="saveRoles"
|
|
>
|
|
<Save v-if="!saving" class="w-4 h-4" />
|
|
<Loader2 v-else class="w-4 h-4 animate-spin" />
|
|
{{ saving ? '保存中...' : '保存配置' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="loading" class="py-12 flex flex-col items-center justify-center text-zinc-500">
|
|
<Loader2 class="w-8 h-8 animate-spin mb-4" />
|
|
<p>正在加载权限配置...</p>
|
|
</div>
|
|
|
|
<div v-else-if="error" class="py-12 text-center text-red-400">
|
|
{{ error }}
|
|
<button class="mt-4 px-4 py-2 bg-zinc-800 rounded-lg hover:bg-zinc-700" @click="fetchRoles">
|
|
重试
|
|
</button>
|
|
</div>
|
|
|
|
<div v-else class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
|
|
<div
|
|
v-for="(roleConfig, roleKey) in roles"
|
|
:key="roleKey"
|
|
class="role-card p-6 bg-zinc-900/50 border border-zinc-800 rounded-2xl"
|
|
>
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 rounded-xl bg-zinc-800 flex items-center justify-center text-zinc-400">
|
|
<Shield class="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<h3 class="font-bold text-zinc-200">{{ roleConfig.displayName }}</h3>
|
|
<p class="text-xs text-zinc-500 font-mono mt-0.5">{{ roleKey }}</p>
|
|
</div>
|
|
</div>
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
v-model="roleConfig.canAccessAdmin"
|
|
type="checkbox"
|
|
class="hidden"
|
|
:disabled="roleKey === 'SUPER_ADMIN'"
|
|
/>
|
|
<div class="w-10 h-6 bg-zinc-800 rounded-full relative transition-colors" :class="{ 'bg-indigo-500': roleConfig.canAccessAdmin }">
|
|
<div class="absolute w-4 h-4 bg-white rounded-full top-1 transition-transform" :class="roleConfig.canAccessAdmin ? 'left-5' : 'left-1'" />
|
|
</div>
|
|
<span class="text-xs font-bold text-zinc-400">允许进后台</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div v-if="roleConfig.canAccessAdmin" class="mt-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="flex items-center gap-2">
|
|
<h4 class="text-xs font-bold text-zinc-500 uppercase tracking-wider">可访问页面</h4>
|
|
<span class="text-[10px] font-black px-2 py-0.5 rounded-md bg-zinc-800/60 text-zinc-400 border border-zinc-800">
|
|
已启用 {{ roleConfig.pages.length }} 项
|
|
</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<button
|
|
class="px-3 py-1.5 text-[11px] font-black rounded-lg bg-zinc-800/60 border border-zinc-800 text-zinc-300 hover:bg-zinc-800 transition-colors"
|
|
:disabled="roleKey === 'SUPER_ADMIN'"
|
|
@click="selectAll(roleKey)"
|
|
>
|
|
全选
|
|
</button>
|
|
<button
|
|
class="px-3 py-1.5 text-[11px] font-black rounded-lg bg-zinc-800/60 border border-zinc-800 text-zinc-300 hover:bg-zinc-800 transition-colors"
|
|
:disabled="roleKey === 'SUPER_ADMIN'"
|
|
@click="clearAll(roleKey)"
|
|
>
|
|
清空
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-5">
|
|
<div v-for="section in pageSections" :key="section.title" class="space-y-2">
|
|
<div class="flex items-center justify-between">
|
|
<div class="text-[10px] font-black text-zinc-600 uppercase tracking-[0.2em]">
|
|
{{ section.title }}
|
|
</div>
|
|
<button
|
|
class="text-[10px] font-black text-indigo-400 hover:text-indigo-300 transition-colors"
|
|
:disabled="roleKey === 'SUPER_ADMIN'"
|
|
@click="toggleSection(roleKey, section)"
|
|
>
|
|
{{ isSectionAllSelected(roleKey, section) ? '取消全选' : '本组全选' }}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<label
|
|
v-for="page in section.pages"
|
|
:key="page.id"
|
|
class="flex items-center gap-2 p-2 rounded-xl border cursor-pointer transition-all"
|
|
:class="roleConfig.pages.includes(page.id) ? 'bg-indigo-500/10 border-indigo-500/30 text-indigo-200 shadow-[0_0_0_1px_rgba(99,102,241,0.15)]' : 'bg-zinc-800/30 border-zinc-800 text-zinc-500 hover:bg-zinc-800/70'"
|
|
>
|
|
<input
|
|
v-model="roleConfig.pages"
|
|
type="checkbox"
|
|
:value="page.id"
|
|
class="hidden"
|
|
:disabled="roleKey === 'SUPER_ADMIN' && isRequiredForSuperAdmin(page.id)"
|
|
/>
|
|
<component :is="page.icon" class="w-4 h-4" />
|
|
<span class="text-xs font-bold">{{ page.label }}</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="mt-6 py-8 text-center border border-dashed border-zinc-800 rounded-xl">
|
|
<p class="text-sm text-zinc-600">该角色不允许进入后台</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import {
|
|
Shield,
|
|
Save,
|
|
Loader2,
|
|
Calendar,
|
|
Music,
|
|
Users,
|
|
Bell,
|
|
Clock,
|
|
BookOpen,
|
|
Database,
|
|
Settings,
|
|
Ban,
|
|
Key,
|
|
BarChart,
|
|
FileEdit,
|
|
Mail
|
|
} from '@lucide/vue'
|
|
|
|
const roles = ref({})
|
|
const loading = ref(true)
|
|
const saving = ref(false)
|
|
const error = ref('')
|
|
|
|
const pageSections = [
|
|
{
|
|
title: '数据中心',
|
|
pages: [
|
|
{ id: 'overview', label: '数据概览', icon: BarChart },
|
|
{ id: 'workflow-tutorial', label: '流程与教程', icon: BookOpen },
|
|
{ id: 'data-analysis', label: '数据分析', icon: BarChart }
|
|
]
|
|
},
|
|
{
|
|
title: '播音管理',
|
|
pages: [
|
|
{ id: 'songs', label: '歌曲管理', icon: Music },
|
|
{ id: 'schedule', label: '排期管理', icon: Calendar },
|
|
{ id: 'print', label: '打印排期', icon: BookOpen }
|
|
]
|
|
},
|
|
{
|
|
title: '业务设置',
|
|
pages: [
|
|
{ id: 'settings', label: '选曲日期', icon: Calendar },
|
|
{ id: 'request-times', label: '投稿管理', icon: FileEdit },
|
|
{ id: 'semesters', label: '学期管理', icon: BookOpen },
|
|
{ id: 'playtimes', label: '播出时段', icon: Clock }
|
|
]
|
|
},
|
|
{
|
|
title: '用户与权限',
|
|
pages: [
|
|
{ id: 'users', label: '用户管理', icon: Users },
|
|
{ id: 'roles', label: '权限组管理', icon: Shield },
|
|
{ id: 'blacklist', label: '黑名单管理', icon: Ban },
|
|
{ id: 'api-keys', label: 'API密钥管理', icon: Key }
|
|
]
|
|
},
|
|
{
|
|
title: '系统运维',
|
|
pages: [
|
|
{ id: 'site-config', label: '站点配置', icon: Settings },
|
|
{ id: 'notifications', label: '通知管理', icon: Bell },
|
|
{ id: 'smtp-config', label: '邮件配置', icon: Mail },
|
|
{ id: 'database', label: '数据库操作', icon: Database }
|
|
]
|
|
}
|
|
]
|
|
|
|
const allPages = pageSections.flatMap((s) => s.pages)
|
|
|
|
const isRequiredForSuperAdmin = (pageId) => {
|
|
return ['roles', 'users', 'site-config'].includes(pageId)
|
|
}
|
|
|
|
const selectAll = (roleKey) => {
|
|
const role = roles.value?.[roleKey]
|
|
if (!role || roleKey === 'SUPER_ADMIN') return
|
|
role.pages = Array.from(new Set(allPages.map((p) => p.id)))
|
|
}
|
|
|
|
const clearAll = (roleKey) => {
|
|
const role = roles.value?.[roleKey]
|
|
if (!role || roleKey === 'SUPER_ADMIN') return
|
|
role.pages = []
|
|
}
|
|
|
|
const isSectionAllSelected = (roleKey, section) => {
|
|
const role = roles.value?.[roleKey]
|
|
if (!role) return false
|
|
return section.pages.every((p) => role.pages.includes(p.id))
|
|
}
|
|
|
|
const toggleSection = (roleKey, section) => {
|
|
const role = roles.value?.[roleKey]
|
|
if (!role || roleKey === 'SUPER_ADMIN') return
|
|
|
|
const allSelected = section.pages.every((p) => role.pages.includes(p.id))
|
|
if (allSelected) {
|
|
role.pages = role.pages.filter((id) => !section.pages.some((p) => p.id === id))
|
|
return
|
|
}
|
|
|
|
role.pages = Array.from(new Set([...role.pages, ...section.pages.map((p) => p.id)]))
|
|
}
|
|
|
|
const fetchRoles = async () => {
|
|
loading.value = true
|
|
error.value = ''
|
|
try {
|
|
const data = await $fetch('/api/admin/roles')
|
|
roles.value = JSON.parse(JSON.stringify(data))
|
|
} catch (err) {
|
|
console.error('Failed to fetch roles:', err)
|
|
error.value = '加载权限配置失败'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const saveRoles = async () => {
|
|
saving.value = true
|
|
try {
|
|
await $fetch('/api/admin/roles', {
|
|
method: 'PUT',
|
|
body: roles.value
|
|
})
|
|
|
|
if (window.$showNotification) {
|
|
window.$showNotification('权限配置已保存', 'success')
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to save roles:', err)
|
|
if (window.$showNotification) {
|
|
window.$showNotification('保存失败: ' + (err.message || '未知错误'), 'error')
|
|
}
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchRoles()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.role-card {
|
|
transition: all 0.3s ease;
|
|
}
|
|
.role-card:hover {
|
|
border-color: rgba(99, 102, 241, 0.3);
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
|
}
|
|
</style>
|