Files
BYW-VoiceHub/app/components/Admin/PlaylistCardPage.client.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

163 lines
6.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="flex flex-col space-y-8 pb-12 lg:pb-20 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div class="flex flex-col space-y-2">
<h2 class="text-2xl font-bold tracking-tight text-zinc-900">导入酷狗</h2>
<p class="text-sm text-zinc-500">选择排期日期,生成对应的广播排期卡片,用于扫描导入酷狗音乐</p>
</div>
<div class="flex flex-col gap-8">
<!-- 设置面板 -->
<div class="w-full flex flex-col gap-6">
<div class="bg-zinc-50/40 border border-zinc-200 rounded-xl p-6 space-y-6 shadow-lg">
<h3 class="text-lg font-bold flex items-center gap-2 text-zinc-900 shrink-0">
<Layout class="w-4 h-4 text-blue-500" /> 选择日期
</h3>
<div class="flex flex-col gap-5 items-end">
<!-- 日期选择 -->
<div class="space-y-2 flex-1 w-full">
<label class="text-[11px] font-black uppercase text-zinc-600 tracking-wider">选择要生成图片的日期(可多选)</label>
<AdminMultiDatePicker
v-model="selectedDates"
@change="fetchSchedules"
/>
</div>
<button
:disabled="loading || selectedDates.length === 0"
class="w-full py-3 h-[54px] bg-blue-600 hover:bg-blue-500 text-white text-xs font-bold rounded-xl shadow-lg shadow-blue-900/20 transition-colors uppercase tracking-wider disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 shrink-0"
@click="fetchSchedules"
>
<CalendarDays class="w-4 h-4" />
{{ loading ? '加载排期中...' : '读取所选日期排期' }}
</button>
</div>
</div>
</div>
<!-- 预览区 -->
<div class="w-full flex flex-col gap-4">
<template v-if="Object.keys(schedulesByDate).length > 0">
<div v-for="(schedules, date) in schedulesByDate" :key="date" class="bg-zinc-50/40 border border-zinc-200 rounded-xl p-6 shadow-lg mb-4">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-bold text-zinc-900">{{ date }} 排期预览 ({{ schedules.length }} 首)</h3>
<button
:disabled="schedules.length === 0"
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-bold rounded-xl shadow-lg shadow-emerald-900/20 transition-colors flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
@click="openGenerator(date, schedules)"
>
<ImageIcon class="w-4 h-4" />
生成该日图片
</button>
</div>
<div v-if="schedules.length > 0" class="space-y-3">
<div v-for="(schedule, index) in schedules" :key="schedule.id || index" class="flex items-center gap-3 p-3 bg-white/70 border border-zinc-200 rounded-lg">
<span class="text-blue-500 font-bold w-6">{{ index + 1 }}.</span>
<div class="flex-1 overflow-hidden">
<p class="text-sm text-zinc-700 font-medium truncate">{{ schedule.song?.title || schedule.title }}</p>
<p class="text-xs text-zinc-500 truncate">{{ schedule.song?.artist || schedule.artist }}</p>
</div>
</div>
</div>
<div v-else class="text-center py-8 text-zinc-500">
该日期没有排期歌曲
</div>
</div>
</template>
<div v-else-if="!loading && selectedDates.length > 0" class="flex flex-col items-center justify-center py-20 bg-zinc-50/20 border border-zinc-200 rounded-xl border-dashed">
<CalendarDays class="w-12 h-12 text-zinc-700 mb-4" />
<p class="text-zinc-500">所选日期均没有排期歌曲</p>
</div>
</div>
</div>
<AdminPlaylistCardGenerator
:show="showModal"
:songs="currentGeneratorSongs"
:default-title="`${currentGeneratorDate} 校园广播歌单`"
@close="showModal = false"
/>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { Layout, Image as ImageIcon, CalendarDays } from '@lucide/vue'
import AdminMultiDatePicker from './AdminMultiDatePicker.vue'
import AdminPlaylistCardGenerator from './PlaylistCardGenerator.vue'
import { useAuth } from '~/composables/useAuth'
const auth = useAuth()
const todayStr = new Date().toISOString().split('T')[0]
const selectedDates = ref([todayStr])
const schedulesByDate = ref({})
const loading = ref(false)
const showModal = ref(false)
const currentGeneratorDate = ref('')
const currentGeneratorSongs = ref([])
const fetchSchedulesForDate = async (date) => {
try {
const response = await $fetch('/api/admin/schedule/full', {
...auth.getAuthConfig(),
query: {
date: date,
includeDrafts: 'true'
}
})
if (response.success && response.data && response.data.schedules) {
return response.data.schedules
}
} catch (error) {
console.error(`Failed to fetch schedules for ${date}:`, error)
}
return []
}
const fetchSchedules = async () => {
if (selectedDates.value.length === 0) {
schedulesByDate.value = {}
return
}
loading.value = true
const newSchedules = {}
try {
// 按日期顺序获取
const sortedDates = [...selectedDates.value].sort()
for (const date of sortedDates) {
const schedules = await fetchSchedulesForDate(date)
if (schedules.length > 0) {
newSchedules[date] = schedules
}
}
schedulesByDate.value = newSchedules
if (Object.keys(newSchedules).length === 0 && window.$showNotification) {
window.$showNotification('所选日期没有排期歌曲', 'info')
}
} catch (error) {
console.error('Failed to fetch schedules:', error)
if (window.$showNotification) {
window.$showNotification('获取排期失败', 'error')
}
} finally {
loading.value = false
}
}
const openGenerator = (date, songs) => {
currentGeneratorDate.value = date
currentGeneratorSongs.value = songs
showModal.value = true
}
onMounted(() => {
fetchSchedules()
})
</script>