Files
BYW-VoiceHub/app/components/Admin/RolesManager.vue
T
Henry_Du 573e168702 feat(admin-ui): 管理后台整体切换为浅色主题(ModernLight) + 新增 lightbulb 图标
1. layout(管理控制台 dashboard.vue): main bg-[#09090b] text-zinc-100 → bg-[#f8fafc] text-zinc-900; header bg-zinc-950/60 border-zinc-800 → bg-white/80 border-zinc-200; 移动菜单按钮/新手引导按钮 text-zinc-300/400 hover:bg-zinc-800 → text-zinc-600 hover:bg-zinc-100 border-zinc-200

2. Admin/Sidebar.vue: bg-[#09090b] border-zinc-800 → bg-white border-zinc-200 + shadow-sm; 标题 text-zinc-100 → 900; activeTab bg-blue-600/10 text-blue-400 → bg-blue-50 text-blue-700 border-blue-200; 未选中 text-zinc-500 hover:text-zinc-200 hover:bg-zinc-800/40 → text-zinc-600 hover:text-zinc-900 hover:bg-zinc-100; 用户卡 avatar/首字母 bg-white/70 border-zinc-200 + 退出按钮 hover:bg-red-50 text-red-600

3. 批量 33 个 Admin 组件硬编码深色原子类 1414 处替换(bg-zinc-9xx / border-zinc-8xx / text-zinc-100,200,300 / shadow-black/ -> 对应 light 版本) + style 块原生 CSS rgba(255,255,255,x) 16 处改为 rgba(0,0,0,x) / var 浅色主题 + 硬编码 bg-[#0c0c0e] / border-white/10 / color:white / surface-primary fallback 对应浅色修正

4. 二轮批量追加: bg-zinc-950 + opacity 变体(80/60/50/40/30/20/10) 共 431 处 → bg-white 对应 opacity 版本;placeholder:text-zinc-800 → 500;text-blue/red/emerald/amber/rose/purple 400 版 → 700/600 深浅一致版;border color /500 /30 → 200;focus:ring /20 → /30;shadow color 阴影降低强度

5. hover:text-white 扫尾: 13 处 DataAnalysisPanel/DatabaseManager/NotificationSender/SemesterManager/SongManagement/UserSongsModal hover 文本从 white → zinc-900;按钮三态从 hover:bg-*600 白字 → hover:bg-*-100/200 对应深字色,和前台 indigo/浅色体系保持一致

6. BlacklistManager 前台可见提示 💡→ <Icon lightbulb amber-600 size=13>;ChangelogManager 徽章色从 emerald-300/blue-300/zinc-400 → emerald-700/blue-700/zinc-600 可读;Icon.vue 新增 lightbulb 分支 stroke 图标

7. 受影响文件: dashboard.vue + Sidebar.vue + Icon.vue + Admin/* (33个组件共 1874 处批量替换 + 20 处手工精细化修正)
2026-09-19 08:30:59 +08:00

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-900 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-100 rounded-lg hover:bg-zinc-200" @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-50/50 border border-zinc-200 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-100 flex items-center justify-center text-zinc-400">
<Shield class="w-5 h-5" />
</div>
<div>
<h3 class="font-bold text-zinc-700">{{ 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-100 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-100/60 text-zinc-400 border border-zinc-200">
已启用 {{ 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-100/60 border border-zinc-200 text-zinc-600 hover:bg-zinc-100 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-100/60 border border-zinc-200 text-zinc-600 hover:bg-zinc-100 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-100/30 border-zinc-200 text-zinc-500 hover:bg-zinc-100/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-200 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>