初次提交
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user