278 lines
10 KiB
Vue
278 lines
10 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-100 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-zinc-900 border border-zinc-800 hover:border-zinc-700 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-800 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-900/40 border border-zinc-800 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-300' : 'bg-zinc-800/50 text-zinc-400'"
|
|
>
|
|
{{ item.published ? '已发布' : '草稿' }}
|
|
</span>
|
|
<span class="px-2.5 py-1 rounded-lg bg-blue-500/10 text-blue-300 text-[10px] font-black">
|
|
v{{ item.version }}
|
|
</span>
|
|
<p class="text-sm font-black text-zinc-100 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-700 text-zinc-300 hover:bg-zinc-900 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-700 text-zinc-300 hover:bg-zinc-900 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-500/30 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-zinc-950 border border-zinc-800 rounded-2xl overflow-hidden">
|
|
<div class="px-5 py-4 border-b border-zinc-800 flex items-center justify-between">
|
|
<p class="text-sm font-black text-zinc-100">{{ editingId ? '编辑更新日志' : '新建更新日志' }}</p>
|
|
<button
|
|
class="h-9 w-9 rounded-lg border border-zinc-700 text-zinc-400 hover:text-zinc-200 hover:bg-zinc-900 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-zinc-900 border border-zinc-800 rounded-xl px-4 py-2.5 text-sm text-zinc-200 focus:outline-none focus:border-blue-500/30 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-zinc-900 border border-zinc-800 rounded-xl px-4 py-2.5 text-sm text-zinc-200 focus:outline-none focus:border-blue-500/30 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-zinc-900 border border-zinc-800 rounded-xl px-4 py-3 text-sm text-zinc-200 focus:outline-none focus:border-blue-500/30 transition-all font-mono"
|
|
placeholder="支持纯文本(建议用分点)。"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between gap-4 p-3 rounded-xl border border-zinc-800 bg-zinc-900/40">
|
|
<div>
|
|
<p class="text-xs font-bold text-zinc-200">立即发布</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-800 bg-zinc-900 accent-blue-600 cursor-pointer" >
|
|
</div>
|
|
</div>
|
|
|
|
<div class="px-5 py-4 border-t border-zinc-800 flex items-center justify-end gap-3">
|
|
<button
|
|
class="px-4 py-2 rounded-lg border border-zinc-700 text-zinc-300 hover:bg-zinc-900 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>
|