feat(admin): clarify dashboard priorities and content health
This commit is contained in:
+169
-36
@@ -108,6 +108,35 @@ const cloudTypeStats = computed(() => {
|
||||
.slice(0, 8)
|
||||
})
|
||||
|
||||
const reviewedCount = computed(() => dashboardStats.value.approved + dashboardStats.value.rejected)
|
||||
const approvalRate = computed(() => getPercentage(dashboardStats.value.approved, reviewedCount.value))
|
||||
const publicImageCount = computed(() => Math.max(0, dashboardStats.value.images - dashboardStats.value.hidden))
|
||||
const recentImages = computed(() => images.value.slice(0, 5))
|
||||
const cloudTypeMaxCount = computed(() => Math.max(1, ...cloudTypeStats.value.map(item => item.count)))
|
||||
const reviewStatusStats = computed(() => [
|
||||
{
|
||||
key: 'approved',
|
||||
label: '已通过',
|
||||
count: dashboardStats.value.approved,
|
||||
bar: 'bg-emerald-500',
|
||||
text: 'text-emerald-700',
|
||||
},
|
||||
{
|
||||
key: 'pending',
|
||||
label: '待审核',
|
||||
count: dashboardStats.value.pending,
|
||||
bar: 'bg-amber-400',
|
||||
text: 'text-amber-700',
|
||||
},
|
||||
{
|
||||
key: 'rejected',
|
||||
label: '已拒绝',
|
||||
count: dashboardStats.value.rejected,
|
||||
bar: 'bg-rose-400',
|
||||
text: 'text-rose-700',
|
||||
},
|
||||
])
|
||||
|
||||
const selectedReviewCount = computed(() => selectedReviewIds.value.size)
|
||||
|
||||
function toAdminCloud(row: ApiCloud): AdminCloud {
|
||||
@@ -147,6 +176,11 @@ function formatCoordinate(value: number | null) {
|
||||
return value === null ? '未记录' : value.toFixed(2)
|
||||
}
|
||||
|
||||
function getPercentage(value: number, total: number) {
|
||||
if (!total) return 0
|
||||
return Math.round(value / total * 100)
|
||||
}
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string) {
|
||||
return error instanceof Error ? error.message : fallback
|
||||
}
|
||||
@@ -508,57 +542,124 @@ onMounted(loadAdminData)
|
||||
</section>
|
||||
|
||||
<template v-else>
|
||||
<section v-if="activeTab === 'dashboard'" class="space-y-6">
|
||||
<div class="grid gap-4 md:grid-cols-4">
|
||||
<div class="border border-slate-200 bg-white p-5">
|
||||
<p class="text-sm text-slate-500">总用户数</p>
|
||||
<p class="mt-3 text-3xl font-bold text-slate-950">{{ dashboardStats.users }}</p>
|
||||
<section v-if="activeTab === 'dashboard'" class="space-y-5">
|
||||
<div class="grid gap-4 xl:grid-cols-[1.35fr_0.65fr]">
|
||||
<div
|
||||
class="flex flex-col justify-between gap-5 border p-6 sm:flex-row sm:items-center"
|
||||
:class="dashboardStats.pending
|
||||
? 'border-amber-200 bg-[linear-gradient(135deg,#fffbeb_0%,#fff_52%,#f0fdfa_100%)] shadow-[5px_5px_0_0_rgba(245,158,11,0.12)]'
|
||||
: 'border-teal-200 bg-[linear-gradient(135deg,#f0fdfa_0%,#fff_55%,#eff6ff_100%)] shadow-[5px_5px_0_0_rgba(20,184,166,0.10)]'"
|
||||
>
|
||||
<div class="flex items-center gap-5">
|
||||
<div
|
||||
class="flex h-20 w-20 shrink-0 items-center justify-center border font-mono text-3xl font-bold"
|
||||
:class="dashboardStats.pending ? 'border-amber-300 bg-amber-100 text-amber-900' : 'border-teal-200 bg-teal-100 text-teal-900'"
|
||||
>
|
||||
{{ dashboardStats.pending }}
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.2em]" :class="dashboardStats.pending ? 'text-amber-700' : 'text-teal-700'">
|
||||
{{ dashboardStats.pending ? 'Priority Queue' : 'Queue Cleared' }}
|
||||
</p>
|
||||
<h3 class="mt-2 text-xl font-bold text-slate-950">
|
||||
{{ dashboardStats.pending ? '待审核内容需要处理' : '审核队列已经清空' }}
|
||||
</h3>
|
||||
<p class="mt-2 text-sm leading-6 text-slate-600">
|
||||
{{ dashboardStats.pending
|
||||
? `当前占全部内容的 ${getPercentage(dashboardStats.pending, dashboardStats.images)}%,建议优先核对图片与拍摄信息。`
|
||||
: '当前没有待处理上传,可以继续检查隐藏内容与近期提交。' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<NButton
|
||||
type="default"
|
||||
secondary
|
||||
strong
|
||||
class="oc-panel-button oc-panel-button--amber shrink-0"
|
||||
:disabled="!dashboardStats.pending"
|
||||
@click="activeTab = 'review'"
|
||||
>
|
||||
进入审核队列
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="border border-slate-200 bg-white p-5">
|
||||
<p class="text-sm text-slate-500">图片总数</p>
|
||||
<p class="mt-3 text-3xl font-bold text-slate-950">{{ dashboardStats.images }}</p>
|
||||
</div>
|
||||
<div class="border border-slate-200 bg-white p-5">
|
||||
<p class="text-sm text-slate-500">今日上传</p>
|
||||
<p class="mt-3 text-3xl font-bold text-slate-950">{{ dashboardStats.todayUploads }}</p>
|
||||
</div>
|
||||
<div class="border border-amber-200 bg-amber-50 p-5">
|
||||
<p class="text-sm text-amber-700">待审核</p>
|
||||
<p class="mt-3 text-3xl font-bold text-amber-900">{{ dashboardStats.pending }}</p>
|
||||
|
||||
<div class="grid grid-cols-2 border border-slate-200 bg-white">
|
||||
<div class="border-b border-r border-slate-100 p-4">
|
||||
<p class="text-xs font-medium text-slate-500">今日上传</p>
|
||||
<p class="mt-2 text-2xl font-bold text-sky-800">{{ dashboardStats.todayUploads }}</p>
|
||||
</div>
|
||||
<div class="border-b border-slate-100 p-4">
|
||||
<p class="text-xs font-medium text-slate-500">审核通过率</p>
|
||||
<p class="mt-2 text-2xl font-bold text-teal-800">{{ approvalRate }}%</p>
|
||||
</div>
|
||||
<div class="border-r border-slate-100 p-4">
|
||||
<p class="text-xs font-medium text-slate-500">公开图片</p>
|
||||
<p class="mt-2 text-2xl font-bold text-slate-950">{{ publicImageCount }}</p>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<p class="text-xs font-medium text-slate-500">社区用户</p>
|
||||
<p class="mt-2 text-2xl font-bold text-slate-950">{{ dashboardStats.users }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-[1fr_1.2fr]">
|
||||
<div class="border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-xl font-bold text-slate-950">审核状态</h2>
|
||||
<div class="mt-5 grid gap-3">
|
||||
<div class="flex items-center justify-between border border-slate-100 bg-slate-50 px-4 py-3">
|
||||
<span class="text-sm text-slate-600">已通过</span>
|
||||
<span class="font-semibold text-emerald-700">{{ dashboardStats.approved }}</span>
|
||||
<div class="grid gap-5 lg:grid-cols-[1.05fr_0.95fr]">
|
||||
<div class="border border-slate-200 bg-white p-5 sm:p-6">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="oc-section-label">Review Health</p>
|
||||
<h3 class="mt-2 text-xl font-bold text-slate-950">审核健康度</h3>
|
||||
</div>
|
||||
<div class="flex items-center justify-between border border-slate-100 bg-slate-50 px-4 py-3">
|
||||
<span class="text-sm text-slate-600">已拒绝</span>
|
||||
<span class="font-semibold text-rose-700">{{ dashboardStats.rejected }}</span>
|
||||
<div class="text-right">
|
||||
<p class="text-3xl font-bold text-teal-800">{{ approvalRate }}%</p>
|
||||
<p class="mt-1 text-xs text-slate-500">{{ reviewedCount }} 张已完成审核</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-between border border-slate-100 bg-slate-50 px-4 py-3">
|
||||
<span class="text-sm text-slate-600">隐藏图片</span>
|
||||
<span class="font-semibold text-slate-900">{{ dashboardStats.hidden }}</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex h-3 overflow-hidden bg-slate-100" role="img" aria-label="审核状态分布">
|
||||
<div
|
||||
v-for="item in reviewStatusStats"
|
||||
:key="item.key"
|
||||
:class="item.bar"
|
||||
:style="{ width: `${getPercentage(item.count, dashboardStats.images)}%` }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="mt-5 grid gap-3 sm:grid-cols-3">
|
||||
<div v-for="item in reviewStatusStats" :key="item.key" class="border border-slate-100 bg-slate-50 p-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="h-2 w-2" :class="item.bar"></span>
|
||||
<span class="text-xs font-medium text-slate-500">{{ item.label }}</span>
|
||||
</div>
|
||||
<div class="mt-2 flex items-end justify-between gap-2">
|
||||
<span class="text-xl font-bold" :class="item.text">{{ item.count }}</span>
|
||||
<span class="pb-0.5 text-xs text-slate-400">{{ getPercentage(item.count, dashboardStats.images) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 flex items-center justify-between border-t border-slate-100 pt-4 text-sm">
|
||||
<span class="text-slate-500">隐藏内容(与审核状态独立)</span>
|
||||
<span class="font-semibold text-slate-800">{{ dashboardStats.hidden }} 张</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-slate-200 bg-white p-6">
|
||||
<h2 class="text-xl font-bold text-slate-950">云型分布</h2>
|
||||
<div v-if="cloudTypeStats.length" class="mt-5 space-y-3">
|
||||
<div class="border border-slate-200 bg-white p-5 sm:p-6">
|
||||
<div>
|
||||
<p class="oc-section-label">Cloud Type Mix</p>
|
||||
<h3 class="mt-2 text-xl font-bold text-slate-950">云型分布</h3>
|
||||
<p class="mt-2 text-xs text-slate-500">基于最近加载的 {{ images.length }} 张内容,按最高频云型对比。</p>
|
||||
</div>
|
||||
<div v-if="cloudTypeStats.length" class="mt-5 space-y-3.5">
|
||||
<div v-for="item in cloudTypeStats" :key="item.name">
|
||||
<div class="mb-1 flex items-center justify-between text-sm">
|
||||
<span class="text-slate-600">{{ item.name }}</span>
|
||||
<span class="font-medium text-slate-900">{{ item.count }}</span>
|
||||
<div class="mb-1.5 flex items-center justify-between gap-3 text-sm">
|
||||
<span class="truncate text-slate-600">{{ item.name }}</span>
|
||||
<span class="shrink-0 font-mono text-xs font-medium text-slate-700">
|
||||
{{ item.count }} / {{ getPercentage(item.count, images.length) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="h-2 bg-slate-100">
|
||||
<div
|
||||
class="h-full bg-sky-500"
|
||||
:style="{ width: `${Math.max(8, item.count / Math.max(1, dashboardStats.images) * 100)}%` }"
|
||||
:style="{ width: `${item.count / cloudTypeMaxCount * 100}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -566,6 +667,38 @@ onMounted(loadAdminData)
|
||||
<NEmpty v-else class="py-6" description="暂无图片数据" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-slate-200 bg-white">
|
||||
<div class="flex items-end justify-between gap-4 border-b border-slate-200 px-5 py-4">
|
||||
<div>
|
||||
<p class="oc-section-label">Latest Activity</p>
|
||||
<h3 class="mt-2 text-xl font-bold text-slate-950">最近上传</h3>
|
||||
</div>
|
||||
<button type="button" class="text-sm font-medium text-sky-700 hover:text-sky-900" @click="activeTab = 'images'">
|
||||
查看全部图片
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="recentImages.length" class="divide-y divide-slate-100">
|
||||
<button
|
||||
v-for="item in recentImages"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="grid w-full grid-cols-[52px_minmax(0,1fr)_auto] items-center gap-3 px-5 py-3 text-left transition-colors hover:bg-sky-50/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-sky-600 sm:grid-cols-[52px_minmax(0,1fr)_minmax(120px,0.5fr)_auto]"
|
||||
@click="selectedImage = item"
|
||||
>
|
||||
<img :src="item.thumbnail_url || item.image_url" :alt="item.cloudTypeName" class="h-11 w-13 object-cover" />
|
||||
<span class="min-w-0">
|
||||
<span class="block truncate text-sm font-semibold text-slate-900">{{ item.cloudTypeName }}</span>
|
||||
<span class="mt-0.5 block truncate text-xs text-slate-500">{{ item.username }} · {{ item.location_name || '未填写位置' }}</span>
|
||||
</span>
|
||||
<span class="hidden truncate text-xs text-slate-500 sm:block">{{ formatDateTime(item.created_at) }}</span>
|
||||
<span class="border px-2 py-1 text-xs font-medium" :class="statusMeta[item.status].chip">
|
||||
{{ statusMeta[item.status].label }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<NEmpty v-else class="py-8" description="暂无上传记录" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeTab === 'review'">
|
||||
|
||||
Reference in New Issue
Block a user