重构管理后台页面 #6

Merged
Mplan merged 8 commits from feat/admin-workspace-redesign into main 2026-07-24 01:03:09 +08:00
Showing only changes of commit 45a4e834dd - Show all commits
+94 -49
View File
@@ -79,13 +79,14 @@ const statusMeta = {
} satisfies Record<CloudStatus, { label: string; chip: string }> } satisfies Record<CloudStatus, { label: string; chip: string }>
const tabs = [ const tabs = [
{ key: 'dashboard', label: '数据看板' }, { key: 'dashboard', label: '数据看板', code: '01', description: '运行概览与内容分布' },
{ key: 'review', label: '内容审核' }, { key: 'review', label: '内容审核', code: '02', description: '处理待审核上传' },
{ key: 'users', label: '用户管理' }, { key: 'users', label: '用户管理', code: '03', description: '角色与账号状态' },
{ key: 'images', label: '图片管理' }, { key: 'images', label: '图片管理', code: '04', description: '内容状态与可见性' },
] satisfies Array<{ key: AdminTab; label: string }> ] satisfies Array<{ key: AdminTab; label: string; code: string; description: string }>
const pendingImages = computed(() => images.value.filter(item => item.status === 'pending')) const pendingImages = computed(() => images.value.filter(item => item.status === 'pending'))
const activeTabMeta = computed(() => tabs.find(tab => tab.key === activeTab.value) ?? tabs[0])
const filteredImages = computed(() => { const filteredImages = computed(() => {
if (imageFilter.value === 'all') return images.value if (imageFilter.value === 'all') return images.value
return images.value.filter(item => item.status === imageFilter.value) return images.value.filter(item => item.status === imageFilter.value)
@@ -408,63 +409,106 @@ onMounted(loadAdminData)
<template> <template>
<div class="min-h-screen bg-[linear-gradient(180deg,#f8fafc_0%,#eef6ff_55%,#f8fafc_100%)]"> <div class="min-h-screen bg-[linear-gradient(180deg,#f8fafc_0%,#eef6ff_55%,#f8fafc_100%)]">
<section class="border-b border-slate-200 bg-white/82 backdrop-blur"> <div class="mx-auto min-h-screen max-w-[1536px] lg:grid lg:grid-cols-[280px_minmax(0,1fr)]">
<div class="mx-auto max-w-7xl px-4 py-10"> <aside class="border-b border-slate-200 bg-white/88 px-4 py-5 backdrop-blur lg:sticky lg:top-16 lg:flex lg:h-[calc(100vh-4rem)] lg:flex-col lg:border-b-0 lg:border-r lg:px-5 lg:py-7">
<div class="flex flex-col gap-5 lg:flex-row lg:items-end lg:justify-between"> <div class="flex items-start justify-between gap-4 lg:block">
<div> <div>
<p class="text-sm font-semibold uppercase tracking-[0.24em] text-sky-700">Admin Console</p> <p class="text-xs font-semibold uppercase tracking-[0.24em] text-sky-700">Admin Console</p>
<h1 class="mt-3 text-4xl font-bold text-slate-950">管理后台</h1> <h1 class="mt-2 text-2xl font-bold text-slate-950">管理工作台</h1>
<!-- <p class="mt-4 max-w-2xl text-sm leading-7 text-slate-600"> <p class="mt-2 hidden text-sm leading-6 text-slate-500 lg:block">社区内容、成员与运行状态的集中控制台。</p>
集中处理社区云图审核、图片可见性、用户角色和运行数据。所有操作都由 FastAPI 服务统一处理。
</p> -->
</div> </div>
<NButton secondary strong type="default" class="oc-panel-button oc-panel-button--neutral" :loading="loading" @click="loadAdminData"> <NButton secondary strong type="default" class="oc-panel-button oc-panel-button--neutral lg:hidden" :loading="loading" @click="loadAdminData">
<template #icon> <template #icon>
<NIcon><Refresh /></NIcon> <NIcon><Refresh /></NIcon>
</template> </template>
刷新数据 刷新
</NButton> </NButton>
</div> </div>
</div>
</section>
<main class="mx-auto max-w-7xl px-4 py-8"> <nav aria-label="后台管理分区" class="mt-5 flex gap-2 overflow-x-auto pb-1 lg:mt-8 lg:block lg:space-y-2 lg:overflow-visible lg:pb-0">
<NAlert
v-if="loadError"
class="mb-6"
type="error"
:show-icon="false"
:bordered="false"
title="后台操作失败"
>
{{ loadError }}
</NAlert>
<section v-if="loading" class="grid gap-4 md:grid-cols-4">
<div v-for="n in 8" :key="n" class="border border-slate-200 bg-white p-5">
<NSkeleton class="h-4 w-1/2" />
<NSkeleton class="mt-4 h-8 w-2/3" />
<NSkeleton class="mt-3 h-3 w-full" />
</div>
</section>
<template v-else>
<section class="grid gap-3 md:grid-cols-4">
<button <button
v-for="tab in tabs" v-for="tab in tabs"
:key="tab.key" :key="tab.key"
type="button" type="button"
class="border px-5 py-4 text-left transition-colors" class="group flex min-w-[168px] items-center gap-3 border px-3 py-3 text-left transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-600 focus-visible:ring-offset-2 lg:w-full lg:min-w-0"
:class="activeTab === tab.key ? 'border-sky-500 bg-sky-50 text-sky-900' : 'border-slate-200 bg-white text-slate-600 hover:border-slate-400'" :class="activeTab === tab.key
? 'border-teal-200 bg-teal-50 text-teal-900 shadow-[3px_3px_0_0_rgba(20,184,166,0.14)]'
: 'border-transparent bg-white text-slate-600 hover:border-slate-200 hover:bg-slate-50 hover:text-slate-950'"
:aria-current="activeTab === tab.key ? 'page' : undefined"
@click="activeTab = tab.key" @click="activeTab = tab.key"
> >
<span class="text-sm font-semibold">{{ tab.label }}</span> <span
<span v-if="tab.key === 'review'" class="ml-2 text-xs text-amber-600">{{ dashboardStats.pending }}</span> class="font-mono text-xs font-semibold"
:class="activeTab === tab.key ? 'text-teal-700' : 'text-slate-400 group-hover:text-slate-600'"
>
{{ tab.code }}
</span>
<span class="min-w-0 flex-1">
<span class="block text-sm font-semibold">{{ tab.label }}</span>
<span class="mt-0.5 hidden truncate text-xs text-slate-500 lg:block">{{ tab.description }}</span>
</span>
<span
v-if="tab.key === 'review' && dashboardStats.pending"
class="inline-flex min-w-6 justify-center border border-amber-200 bg-amber-100 px-1.5 py-0.5 text-xs font-semibold text-amber-800"
>
{{ dashboardStats.pending }}
</span>
</button> </button>
</nav>
<div class="mt-auto hidden lg:block">
<div class="mb-4 border border-sky-100 bg-[linear-gradient(135deg,#f0fdfa_0%,#eff6ff_100%)] p-4">
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">当前待办</p>
<div class="mt-3 flex items-end justify-between">
<span class="text-3xl font-bold text-amber-800">{{ dashboardStats.pending }}</span>
<span class="pb-1 text-xs text-slate-500">张图片待审核</span>
</div>
</div>
<NButton secondary strong type="default" class="oc-panel-button oc-panel-button--neutral w-full" :loading="loading" @click="loadAdminData">
<template #icon>
<NIcon><Refresh /></NIcon>
</template>
刷新后台数据
</NButton>
</div>
</aside>
<main class="min-w-0 px-4 py-7 sm:px-6 lg:px-8 lg:py-9">
<header class="mb-7 border-b border-slate-200 pb-6">
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-sky-700">
{{ activeTabMeta.code }} / {{ activeTabMeta.label }}
</p>
<div class="mt-2 flex flex-col gap-2 xl:flex-row xl:items-end xl:justify-between">
<div>
<h2 class="text-3xl font-bold text-slate-950">{{ activeTabMeta.label }}</h2>
<p class="mt-2 text-sm leading-6 text-slate-500">{{ activeTabMeta.description }}</p>
</div>
<p class="text-xs text-slate-400">数据以最近一次刷新结果为准</p>
</div>
</header>
<NAlert
v-if="loadError"
class="mb-6"
type="error"
:show-icon="false"
:bordered="false"
title="后台操作失败"
>
{{ loadError }}
</NAlert>
<section v-if="loading" class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<div v-for="n in 8" :key="n" class="border border-slate-200 bg-white p-5">
<NSkeleton class="h-4 w-1/2" />
<NSkeleton class="mt-4 h-8 w-2/3" />
<NSkeleton class="mt-3 h-3 w-full" />
</div>
</section> </section>
<section v-if="activeTab === 'dashboard'" class="mt-6 space-y-6"> <template v-else>
<section v-if="activeTab === 'dashboard'" class="space-y-6">
<div class="grid gap-4 md:grid-cols-4"> <div class="grid gap-4 md:grid-cols-4">
<div class="border border-slate-200 bg-white p-5"> <div class="border border-slate-200 bg-white p-5">
<p class="text-sm text-slate-500">总用户数</p> <p class="text-sm text-slate-500">总用户数</p>
@@ -524,7 +568,7 @@ onMounted(loadAdminData)
</div> </div>
</section> </section>
<section v-else-if="activeTab === 'review'" class="mt-6"> <section v-else-if="activeTab === 'review'">
<div class="mb-4 flex flex-col gap-3 border border-slate-200 bg-white p-4 md:flex-row md:items-center md:justify-between"> <div class="mb-4 flex flex-col gap-3 border border-slate-200 bg-white p-4 md:flex-row md:items-center md:justify-between">
<div> <div>
<h2 class="text-xl font-bold text-slate-950">待审核队列</h2> <h2 class="text-xl font-bold text-slate-950">待审核队列</h2>
@@ -599,7 +643,7 @@ onMounted(loadAdminData)
</div> </div>
</section> </section>
<section v-else-if="activeTab === 'users'" class="mt-6"> <section v-else-if="activeTab === 'users'">
<div class="overflow-hidden border border-slate-200 bg-white"> <div class="overflow-hidden border border-slate-200 bg-white">
<div class="grid grid-cols-[1.1fr_0.8fr_0.7fr_0.9fr_1.2fr] border-b border-slate-200 bg-slate-50 px-4 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-slate-500"> <div class="grid grid-cols-[1.1fr_0.8fr_0.7fr_0.9fr_1.2fr] border-b border-slate-200 bg-slate-50 px-4 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-slate-500">
<span>用户</span> <span>用户</span>
@@ -650,7 +694,7 @@ onMounted(loadAdminData)
</div> </div>
</section> </section>
<section v-else class="mt-6"> <section v-else>
<div class="mb-4 flex flex-col gap-3 border border-slate-200 bg-white p-4 md:flex-row md:items-center md:justify-between"> <div class="mb-4 flex flex-col gap-3 border border-slate-200 bg-white p-4 md:flex-row md:items-center md:justify-between">
<div> <div>
<h2 class="text-xl font-bold text-slate-950">图片管理</h2> <h2 class="text-xl font-bold text-slate-950">图片管理</h2>
@@ -776,8 +820,9 @@ onMounted(loadAdminData)
<NEmpty description="没有符合筛选条件的图片" /> <NEmpty description="没有符合筛选条件的图片" />
</div> </div>
</section> </section>
</template> </template>
</main> </main>
</div>
<ImageDetailModal <ImageDetailModal
v-if="selectedImage" v-if="selectedImage"