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 处手工精细化修正)
289 lines
8.7 KiB
Vue
289 lines
8.7 KiB
Vue
<!-- 侧边栏组件 -->
|
|
<template>
|
|
<aside
|
|
:class="[
|
|
'fixed inset-y-0 left-0 z-50 w-64 bg-[#ffffff] border-r border-zinc-200 shadow-sm transform transition-transform duration-300 ease-in-out lg:translate-x-0',
|
|
isOpen ? 'translate-x-0' : '-translate-x-full'
|
|
]"
|
|
>
|
|
<div class="flex flex-col h-full p-4">
|
|
<!-- 品牌标识区域 -->
|
|
<div class="flex items-center px-2 mb-6 mt-2">
|
|
<NuxtLink to="/" class="flex items-center gap-2.5 group">
|
|
<!-- Logo 图标 -->
|
|
<div class="flex-shrink-0 group-hover:scale-110 transition-all duration-300">
|
|
<img
|
|
:src="logo"
|
|
alt="VoiceHub Logo"
|
|
class="w-8 h-8 object-contain"
|
|
width="32"
|
|
height="32"
|
|
decoding="async"
|
|
loading="lazy"
|
|
>
|
|
</div>
|
|
<!-- 品牌文字 -->
|
|
<div class="flex flex-col justify-center">
|
|
<h1 class="font-bold text-lg text-zinc-900 leading-none tracking-tight">VoiceHub</h1>
|
|
<p
|
|
class="text-[10px] text-zinc-500 mt-1.5 uppercase tracking-widest font-bold leading-none"
|
|
>
|
|
管理控制台
|
|
</p>
|
|
</div>
|
|
</NuxtLink>
|
|
</div>
|
|
|
|
<!-- 导航菜单区域 -->
|
|
<nav class="admin-sidebar-nav flex-1 space-y-6 overflow-y-auto pr-2 custom-scrollbar">
|
|
<div v-for="(group, idx) in menuGroups" :key="idx" class="space-y-1">
|
|
<template v-if="shouldShowGroup(group)">
|
|
<!-- 分组标题 -->
|
|
<h3 class="px-3 text-[10px] font-bold text-zinc-600 uppercase tracking-[0.2em] mb-2">
|
|
{{ group.section }}
|
|
</h3>
|
|
<!-- 菜单项列表 -->
|
|
<template v-for="item in group.items" :key="item.id">
|
|
<button
|
|
v-if="permissions.canAccessPage(item.permissionId || item.id)"
|
|
:class="[
|
|
'w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-[13px] font-bold transition-all group border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/80 focus-visible:ring-inset',
|
|
activeTab === item.id
|
|
? 'bg-blue-50 text-blue-700 border-blue-200'
|
|
: 'text-zinc-600 hover:text-zinc-900 hover:bg-zinc-100 border-transparent',
|
|
getTourClass(item.id)
|
|
]"
|
|
@click="onNavigate(item.id)"
|
|
>
|
|
<!-- 菜单图标 -->
|
|
<component
|
|
:is="item.icon"
|
|
:size="18"
|
|
:class="
|
|
activeTab === item.id
|
|
? 'text-blue-700'
|
|
: 'text-zinc-500 group-hover:text-zinc-700'
|
|
"
|
|
/>
|
|
<span class="truncate">{{ item.label }}</span>
|
|
<!-- 选中状态指示器 -->
|
|
<div
|
|
v-if="activeTab === item.id"
|
|
class="ml-auto w-1 h-1 bg-blue-400 rounded-full shadow-[0_0_8px_rgba(96,165,250,0.6)]"
|
|
/>
|
|
</button>
|
|
</template>
|
|
</template>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- 用户信息及退出登录 -->
|
|
<div class="mt-4 pt-4 border-t border-zinc-200">
|
|
<div
|
|
class="flex items-center gap-3 p-3 rounded-lg bg-zinc-50 border border-zinc-200 hover:bg-zinc-100 transition-colors"
|
|
>
|
|
<!-- 用户头像/首字母 -->
|
|
<img
|
|
v-if="currentUser?.avatar && !avatarError"
|
|
:src="currentUser.avatar"
|
|
class="w-10 h-10 rounded-lg object-cover border border-zinc-200 shrink-0"
|
|
@error="avatarError = true"
|
|
>
|
|
<div
|
|
v-else
|
|
class="w-10 h-10 rounded-lg bg-zinc-100 flex items-center justify-center text-zinc-600 font-bold border border-zinc-200 shrink-0"
|
|
>
|
|
{{ (currentUser?.name || '管').charAt(0) }}
|
|
</div>
|
|
<!-- 用户详细信息 -->
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-xs font-black truncate text-zinc-900">
|
|
{{ currentUser?.name || '管理员' }}
|
|
</p>
|
|
<p
|
|
class="text-[10px] text-zinc-500 truncate uppercase tracking-wider font-medium mt-0.5"
|
|
>
|
|
{{ currentUser?.role?.replace('_', ' ') || 'ADMIN' }}
|
|
</p>
|
|
</div>
|
|
<!-- 退出按钮 -->
|
|
<button
|
|
class="p-2 text-zinc-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition-all"
|
|
title="退出登录"
|
|
@click="$emit('logout')"
|
|
>
|
|
<LogOut :size="16" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</template>
|
|
|
|
<script setup>
|
|
/**
|
|
* 后台管理侧边栏组件
|
|
*/
|
|
import {
|
|
LayoutDashboard,
|
|
CalendarDays,
|
|
Printer,
|
|
Music2,
|
|
BarChart3,
|
|
Users,
|
|
Key,
|
|
Bell,
|
|
Mail,
|
|
LogOut,
|
|
Clock,
|
|
FileEdit,
|
|
ListChecks,
|
|
BookOpen,
|
|
Ban,
|
|
Globe,
|
|
Image as ImageIcon,
|
|
ScrollText,
|
|
Database,
|
|
Lock,
|
|
Shield
|
|
} from '@lucide/vue'
|
|
import logo from '~~/public/images/logo.png'
|
|
import { rolesConfig } from '~/composables/usePermissions'
|
|
|
|
const avatarError = ref(false)
|
|
|
|
const props = defineProps({
|
|
// 侧边栏是否打开(移动端)
|
|
isOpen: Boolean,
|
|
// 当前激活的标签页 ID
|
|
activeTab: String,
|
|
// 当前登录用户信息
|
|
currentUser: Object,
|
|
// 权限控制对象
|
|
permissions: Object,
|
|
// 站点标题
|
|
siteTitle: String
|
|
})
|
|
|
|
watch(
|
|
() => props.currentUser?.avatar,
|
|
() => {
|
|
avatarError.value = false
|
|
}
|
|
)
|
|
|
|
const emit = defineEmits(['navigate', 'close', 'logout'])
|
|
|
|
// 菜单分组配置
|
|
const menuGroups = [
|
|
{
|
|
section: '数据中心',
|
|
items: [
|
|
{ icon: LayoutDashboard, label: '数据概览', id: 'overview' },
|
|
{ icon: BookOpen, label: '流程与教程', id: 'workflow-tutorial' },
|
|
{ icon: BarChart3, label: '数据分析', id: 'data-analysis', permissionId: 'data-analysis' }
|
|
]
|
|
},
|
|
{
|
|
section: '播音管理',
|
|
items: [
|
|
{ icon: Music2, label: '歌曲管理', id: 'songs' },
|
|
{ icon: CalendarDays, label: '排期管理', id: 'schedule' },
|
|
{ icon: Printer, label: '打印排期', id: 'print' },
|
|
{ icon: ImageIcon, label: '导入酷狗', id: 'playlist-card' }
|
|
]
|
|
},
|
|
{
|
|
section: '业务设置',
|
|
items: [
|
|
{ icon: CalendarDays, label: '选曲日期', id: 'expected-play-dates', permissionId: 'settings' },
|
|
{ icon: FileEdit, label: '投稿管理', id: 'request-times' },
|
|
{ icon: BookOpen, label: '学期管理', id: 'semesters' },
|
|
{ icon: Clock, label: '播出时段', id: 'playtimes' }
|
|
]
|
|
},
|
|
{
|
|
section: '用户与权限',
|
|
items: [
|
|
{ icon: Users, label: '用户管理', id: 'users' },
|
|
{ icon: Shield, label: '权限组管理', id: 'roles', permissionId: 'roles' },
|
|
{ icon: Ban, label: '黑名单管理', id: 'blacklist' },
|
|
{ icon: Key, label: 'API密钥管理', id: 'api-keys' }
|
|
]
|
|
},
|
|
{
|
|
section: '系统运维',
|
|
items: [
|
|
{ icon: Globe, label: '站点配置', id: 'site-config' },
|
|
{ icon: ScrollText, label: '更新日志', id: 'changelog' },
|
|
{ icon: Bell, label: '通知管理', id: 'notifications' },
|
|
{ icon: Mail, label: '邮件配置', id: 'smtp-config' },
|
|
{ icon: Database, label: '数据库操作', id: 'database' }
|
|
]
|
|
},
|
|
{
|
|
section: '账户管理',
|
|
items: [{ icon: Lock, label: '修改密码', id: 'password' }]
|
|
}
|
|
]
|
|
|
|
/**
|
|
* 判断是否应该显示该菜单组
|
|
* @param {Object} group 菜单组对象
|
|
*/
|
|
const shouldShowGroup = (group) => {
|
|
if (!props.permissions) return true
|
|
return group.items.some((item) => props.permissions.canAccessPage(item.permissionId || item.id))
|
|
}
|
|
|
|
/**
|
|
* 导航点击处理
|
|
* @param {string} id 菜单项 ID
|
|
*/
|
|
const onNavigate = (id) => {
|
|
if (id === 'password') {
|
|
navigateTo('/change-password')
|
|
return
|
|
}
|
|
emit('navigate', id)
|
|
}
|
|
|
|
const getTourClass = (id) => `tour-nav-${id}`
|
|
|
|
/**
|
|
* 获取角色显示名称
|
|
* @param {string} role 角色标识
|
|
*/
|
|
const getRoleDisplayName = (role) => {
|
|
if (rolesConfig.value && rolesConfig.value[role]?.displayName) {
|
|
return rolesConfig.value[role].displayName
|
|
}
|
|
const roleNames = {
|
|
USER: '学生(普通用户)',
|
|
SONG_ADMIN: '工作人员',
|
|
VICE_MINISTER: '副部',
|
|
ADMIN: '部长',
|
|
RESIDENT: '常驻',
|
|
SUPER_ADMIN: '技术运维'
|
|
}
|
|
return roleNames[role] || role
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* 自定义滚动条样式 */
|
|
.custom-scrollbar::-webkit-scrollbar {
|
|
width: 4px;
|
|
}
|
|
.custom-scrollbar::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
background: var(--bg-tertiary, #27272a);
|
|
border-radius: 10px;
|
|
}
|
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
background: #3f3f46;
|
|
}
|
|
</style>
|