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 处手工精细化修正)
278 lines
9.9 KiB
Vue
278 lines
9.9 KiB
Vue
<template>
|
|
<div class="max-w-[1200px] mx-auto space-y-6 pb-24 px-2">
|
|
<div class="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
|
<div>
|
|
<h2 class="text-2xl font-black text-zinc-900 tracking-tight">更新日志</h2>
|
|
<p class="text-xs text-zinc-500 mt-1 font-medium">管理前台“更新日志”页面的发布内容</p>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button
|
|
class="flex items-center gap-2 px-5 py-2 bg-white border border-zinc-200 hover:border-zinc-200 text-zinc-400 text-xs font-bold rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
|
:disabled="loading"
|
|
@click="load"
|
|
>
|
|
<RefreshCw :size="14" /> 刷新
|
|
</button>
|
|
<button
|
|
class="flex items-center gap-2 px-6 py-2 bg-blue-600 hover:bg-blue-500 text-white text-xs font-black rounded-xl shadow-lg shadow-blue-900/20 transition-all active:scale-95"
|
|
@click="openCreate"
|
|
>
|
|
<Plus :size="14" /> 新建
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="loading" class="flex flex-col items-center justify-center py-20">
|
|
<div class="w-8 h-8 border-4 border-zinc-200 border-t-blue-500 rounded-full animate-spin mb-4" />
|
|
<p class="text-zinc-500 text-sm">加载中...</p>
|
|
</div>
|
|
|
|
<div v-else class="space-y-3">
|
|
<div
|
|
v-for="item in entries"
|
|
:key="item.id"
|
|
class="bg-zinc-50/40 border border-zinc-200 rounded-2xl p-5 flex flex-col md:flex-row md:items-center md:justify-between gap-4"
|
|
>
|
|
<div class="min-w-0">
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<span
|
|
class="px-2.5 py-1 rounded-lg text-[10px] font-black uppercase tracking-widest"
|
|
:class="item.published ? 'bg-emerald-500/10 text-emerald-700' : 'bg-zinc-100 text-zinc-600'"
|
|
>
|
|
{{ item.published ? '已发布' : '草稿' }}
|
|
</span>
|
|
<span class="px-2.5 py-1 rounded-lg bg-blue-50 text-blue-700 text-[10px] font-black">
|
|
v{{ item.version }}
|
|
</span>
|
|
<p class="text-sm font-black text-zinc-900 truncate">{{ item.title }}</p>
|
|
</div>
|
|
<p class="text-[10px] text-zinc-500 mt-2">
|
|
{{ formatDate(item.publishedAt || item.createdAt) }}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<button
|
|
class="px-3 py-2 rounded-lg border border-zinc-200 text-zinc-600 hover:bg-white text-xs font-bold"
|
|
@click="openEdit(item)"
|
|
>
|
|
<Pencil :size="14" class="inline-block mr-1" /> 编辑
|
|
</button>
|
|
<button
|
|
class="px-3 py-2 rounded-lg border border-zinc-200 text-zinc-600 hover:bg-white text-xs font-bold"
|
|
:disabled="savingId === item.id"
|
|
@click="togglePublish(item)"
|
|
>
|
|
<UploadCloud :size="14" class="inline-block mr-1" />
|
|
{{ item.published ? '取消发布' : '发布' }}
|
|
</button>
|
|
<button
|
|
class="px-3 py-2 rounded-lg border border-red-200 text-red-300 hover:bg-red-500/10 text-xs font-bold disabled:opacity-50"
|
|
:disabled="savingId === item.id"
|
|
@click="remove(item)"
|
|
>
|
|
<Trash2 :size="14" class="inline-block mr-1" /> 删除
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="entries.length === 0" class="py-20 text-center text-zinc-500">
|
|
暂无更新日志
|
|
</div>
|
|
</div>
|
|
|
|
<Teleport to="body">
|
|
<div v-if="showModal" class="fixed inset-0 z-[80] bg-black/60 backdrop-blur-sm p-4 flex items-end sm:items-center justify-center">
|
|
<div class="w-full max-w-2xl bg-white border border-zinc-200 rounded-2xl overflow-hidden">
|
|
<div class="px-5 py-4 border-b border-zinc-200 flex items-center justify-between">
|
|
<p class="text-sm font-black text-zinc-900">{{ editingId ? '编辑更新日志' : '新建更新日志' }}</p>
|
|
<button
|
|
class="h-9 w-9 rounded-lg border border-zinc-200 text-zinc-400 hover:text-zinc-700 hover:bg-white transition-colors"
|
|
@click="closeModal"
|
|
>
|
|
<X :size="16" class="mx-auto" />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-5 space-y-4">
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1 block mb-2">版本号</label>
|
|
<input v-model="form.version" type="text" class="w-full bg-white border border-zinc-200 rounded-xl px-4 py-2.5 text-sm text-zinc-700 focus:outline-none focus:border-blue-200 transition-all" placeholder="如:2.4.1" >
|
|
</div>
|
|
<div>
|
|
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1 block mb-2">标题</label>
|
|
<input v-model="form.title" type="text" class="w-full bg-white border border-zinc-200 rounded-xl px-4 py-2.5 text-sm text-zinc-700 focus:outline-none focus:border-blue-200 transition-all" placeholder="如:投稿体验优化" >
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1 block mb-2">内容</label>
|
|
<textarea
|
|
v-model="form.content"
|
|
:rows="10"
|
|
class="w-full bg-white border border-zinc-200 rounded-xl px-4 py-3 text-sm text-zinc-700 focus:outline-none focus:border-blue-200 transition-all font-mono"
|
|
placeholder="支持纯文本(建议用分点)。"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between gap-4 p-3 rounded-xl border border-zinc-200 bg-zinc-50/40">
|
|
<div>
|
|
<p class="text-xs font-bold text-zinc-700">立即发布</p>
|
|
<p class="text-[10px] text-zinc-500 mt-0.5">发布后前台更新日志页面可见</p>
|
|
</div>
|
|
<input v-model="form.published" type="checkbox" class="w-5 h-5 rounded border-zinc-200 bg-white accent-blue-600 cursor-pointer" >
|
|
</div>
|
|
</div>
|
|
|
|
<div class="px-5 py-4 border-t border-zinc-200 flex items-center justify-end gap-3">
|
|
<button
|
|
class="px-4 py-2 rounded-lg border border-zinc-200 text-zinc-600 hover:bg-white text-sm font-bold"
|
|
@click="closeModal"
|
|
>
|
|
取消
|
|
</button>
|
|
<button
|
|
class="px-5 py-2 rounded-lg bg-blue-600 hover:bg-blue-500 text-white text-sm font-black disabled:opacity-50"
|
|
:disabled="savingModal"
|
|
@click="save"
|
|
>
|
|
{{ savingModal ? '保存中...' : '保存' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { onMounted, ref } from 'vue'
|
|
import { Plus, RefreshCw, Pencil, UploadCloud, Trash2, X } from '@lucide/vue'
|
|
import { useToast } from '~/composables/useToast'
|
|
|
|
const { showToast } = useToast()
|
|
|
|
const loading = ref(true)
|
|
const entries = ref([])
|
|
const savingId = ref(null)
|
|
|
|
const showModal = ref(false)
|
|
const savingModal = ref(false)
|
|
const editingId = ref(null)
|
|
const form = ref({ version: '', title: '', content: '', published: false })
|
|
|
|
const formatDate = (value) => {
|
|
if (!value) return ''
|
|
const d = new Date(value)
|
|
if (Number.isNaN(d.getTime())) return ''
|
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
}
|
|
|
|
const load = async () => {
|
|
try {
|
|
loading.value = true
|
|
const data = await $fetch('/api/admin/changelog', { credentials: 'include', query: { t: Date.now() } })
|
|
entries.value = Array.isArray(data) ? data : []
|
|
} catch (e) {
|
|
showToast('加载更新日志失败', 'error')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const openCreate = () => {
|
|
editingId.value = null
|
|
form.value = { version: '', title: '', content: '', published: false }
|
|
showModal.value = true
|
|
}
|
|
|
|
const openEdit = (item) => {
|
|
editingId.value = item.id
|
|
form.value = {
|
|
version: item.version || '',
|
|
title: item.title || '',
|
|
content: item.content || '',
|
|
published: !!item.published
|
|
}
|
|
showModal.value = true
|
|
}
|
|
|
|
const closeModal = () => {
|
|
showModal.value = false
|
|
}
|
|
|
|
const save = async () => {
|
|
const payload = {
|
|
version: (form.value.version || '').trim(),
|
|
title: (form.value.title || '').trim(),
|
|
content: (form.value.content || '').trim(),
|
|
published: !!form.value.published
|
|
}
|
|
|
|
if (!payload.version || !payload.title || !payload.content) {
|
|
showToast('请填写版本号、标题与内容', 'error')
|
|
return
|
|
}
|
|
|
|
try {
|
|
savingModal.value = true
|
|
if (editingId.value) {
|
|
await $fetch(`/api/admin/changelog/${editingId.value}`, {
|
|
method: 'PUT',
|
|
credentials: 'include',
|
|
body: payload
|
|
})
|
|
showToast('已保存', 'success')
|
|
} else {
|
|
await $fetch('/api/admin/changelog', {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
body: payload
|
|
})
|
|
showToast('已创建', 'success')
|
|
}
|
|
showModal.value = false
|
|
await load()
|
|
} catch (e) {
|
|
showToast('保存失败', 'error')
|
|
} finally {
|
|
savingModal.value = false
|
|
}
|
|
}
|
|
|
|
const togglePublish = async (item) => {
|
|
try {
|
|
savingId.value = item.id
|
|
await $fetch(`/api/admin/changelog/${item.id}`, {
|
|
method: 'PUT',
|
|
credentials: 'include',
|
|
body: { published: !item.published }
|
|
})
|
|
await load()
|
|
} catch {
|
|
showToast('更新发布状态失败', 'error')
|
|
} finally {
|
|
savingId.value = null
|
|
}
|
|
}
|
|
|
|
const remove = async (item) => {
|
|
try {
|
|
savingId.value = item.id
|
|
await $fetch(`/api/admin/changelog/${item.id}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include'
|
|
})
|
|
await load()
|
|
showToast('已删除', 'success')
|
|
} catch {
|
|
showToast('删除失败', 'error')
|
|
} finally {
|
|
savingId.value = null
|
|
}
|
|
}
|
|
|
|
onMounted(load)
|
|
</script>
|