统一交互组件 (#7)

Reviewed-on: #7
This commit was merged in pull request #7.
This commit is contained in:
2026-07-28 21:31:38 +08:00
parent 2b82ec7d9c
commit ccf7049ae0
28 changed files with 968 additions and 479 deletions
+52 -31
View File
@@ -519,7 +519,7 @@ onMounted(loadAdminData)
<button
type="button"
class="hidden h-8 w-8 items-center justify-center border border-slate-200 bg-white text-slate-500 shadow-[3px_3px_0_0_rgba(15,23,42,0.06)] transition-colors hover:border-teal-200 hover:bg-teal-50 hover:text-teal-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-600 lg:absolute lg:-right-4 lg:top-6 lg:z-20 lg:flex"
class="oc-icon-button oc-icon-button--small hidden lg:absolute lg:-right-4 lg:top-6 lg:z-20 lg:flex"
:aria-label="sidebarCollapsed ? '展开侧边栏' : '折叠侧边栏'"
:aria-expanded="!sidebarCollapsed"
:title="sidebarCollapsed ? '展开侧边栏' : '折叠侧边栏'"
@@ -538,7 +538,7 @@ onMounted(loadAdminData)
type="button"
class="group relative flex min-w-[150px] items-center gap-3 overflow-hidden border px-3 py-3 text-left transition-[background-color,border-color,color,box-shadow] 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-teal-200 bg-teal-50 text-teal-900 shadow-[3px_3px_0_0_rgba(20,184,166,0.14)]'
? 'border-teal-200 bg-teal-50 text-teal-900 shadow-[3px_3px_0_0_rgba(52,116,93,0.16)]'
: '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"
:aria-label="tab.label"
@@ -580,7 +580,7 @@ onMounted(loadAdminData)
type="button"
class="flex h-14 min-w-0 flex-1 items-center gap-3 overflow-hidden border px-3 py-2 text-left transition-[background-color,border-color,color,box-shadow] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-600 lg:w-full"
:class="accountMenuOpen
? 'border-teal-200 bg-teal-50 text-teal-800 shadow-[3px_3px_0_0_rgba(20,184,166,0.12)]'
? 'border-teal-200 bg-teal-50 text-teal-800 shadow-[3px_3px_0_0_rgba(52,116,93,0.16)]'
: 'border-slate-200 bg-slate-50 text-slate-700 hover:border-teal-200 hover:bg-teal-50 hover:text-teal-800'"
aria-label="打开管理员账户菜单"
aria-haspopup="menu"
@@ -615,9 +615,9 @@ onMounted(loadAdminData)
role="menu"
aria-label="管理员账户"
>
<div class="border-b border-slate-200 bg-[linear-gradient(180deg,#f0fdfa_0%,#ffffff_100%)] px-4 py-4">
<div class="border-b border-slate-200 bg-[linear-gradient(180deg,#f2faf6_0%,#ffffff_100%)] px-4 py-4">
<div class="flex items-center gap-3">
<div class="flex h-11 w-11 shrink-0 items-center justify-center border border-teal-200 bg-[linear-gradient(135deg,#ecfeff_0%,#ccfbf1_100%)] text-lg font-bold text-teal-800">
<div class="flex h-11 w-11 shrink-0 items-center justify-center border border-teal-200 bg-[linear-gradient(135deg,#eef9f3_0%,#d9efe3_100%)] text-lg font-bold text-teal-800">
{{ displayInitial }}
</div>
<div class="min-w-0">
@@ -651,7 +651,7 @@ onMounted(loadAdminData)
<div class="border-t border-slate-200 p-2">
<button
type="button"
class="flex w-full items-center gap-3 px-2 py-2.5 text-left text-sm font-medium text-rose-600 transition-colors hover:bg-rose-50 hover:text-rose-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-rose-500"
class="oc-menu-item oc-menu-item--danger px-2"
role="menuitem"
@click="handleLogout"
>
@@ -715,7 +715,7 @@ onMounted(loadAdminData)
</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">
<div v-for="n in 8" :key="n" class="oc-panel-card 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" />
@@ -728,8 +728,8 @@ onMounted(loadAdminData)
<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)]'"
? 'border-amber-200 bg-[linear-gradient(135deg,#fffbeb_0%,#fff_52%,#f2faf6_100%)] shadow-[5px_5px_0_0_rgba(245,158,11,0.12)]'
: 'border-teal-200 bg-[linear-gradient(135deg,#f2faf6_0%,#fff_55%,#eff6ff_100%)] shadow-[5px_5px_0_0_rgba(52,116,93,0.16)]'"
>
<div class="flex items-center gap-5">
<div
@@ -753,18 +753,18 @@ onMounted(loadAdminData)
</div>
</div>
<NButton
v-if="dashboardStats.pending > 0"
type="default"
secondary
strong
class="oc-panel-button oc-panel-button--amber shrink-0"
:disabled="!dashboardStats.pending"
@click="activeTab = 'review'"
>
进入审核队列
</NButton>
</div>
<div class="grid grid-cols-2 border border-slate-200 bg-white">
<div class="oc-panel-card 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>
@@ -785,7 +785,7 @@ onMounted(loadAdminData)
</div>
<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="oc-panel-card 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>
@@ -823,7 +823,7 @@ onMounted(loadAdminData)
</div>
</div>
<div class="border border-slate-200 bg-white p-5 sm:p-6">
<div class="oc-panel-card 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>
@@ -849,22 +849,29 @@ onMounted(loadAdminData)
</div>
</div>
<div class="border border-slate-200 bg-white">
<div class="oc-panel-card 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'">
<NButton
size="small"
type="default"
secondary
strong
class="oc-panel-button oc-panel-button--neutral"
@click="activeTab = 'images'"
>
查看全部图片
</button>
</NButton>
</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]"
class="oc-interactive-row grid w-full grid-cols-[52px_minmax(0,1fr)_auto] items-center gap-3 px-5 py-3 text-left 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" />
@@ -883,13 +890,20 @@ onMounted(loadAdminData)
</section>
<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="oc-panel-card 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>
<h2 class="text-xl font-bold text-slate-950">待审核队列</h2>
<p class="oc-field-help">共 {{ pendingImages.length }} 张待处理图片。</p>
</div>
<div class="flex flex-wrap gap-2">
<NButton secondary strong type="default" class="oc-panel-button oc-panel-button--neutral" @click="toggleAllPendingSelection">
<NButton
v-if="pendingImages.length > 0"
secondary
strong
type="default"
class="oc-panel-button oc-panel-button--neutral"
@click="toggleAllPendingSelection"
>
{{ selectedReviewCount === pendingImages.length && pendingImages.length ? '取消全选' : '全选待审核' }}
</NButton>
<template v-if="selectedReviewCount">
@@ -918,7 +932,7 @@ onMounted(loadAdminData)
</div>
<div v-if="activeReviewImage" class="grid min-w-0 gap-4 xl:grid-cols-[220px_minmax(0,1fr)]">
<aside class="min-w-0 self-start border border-slate-200 bg-white xl:sticky xl:top-20 xl:max-h-[calc(100vh-6rem)]">
<aside class="oc-panel-card min-w-0 self-start border border-slate-200 bg-white xl:sticky xl:top-20 xl:max-h-[calc(100vh-6rem)]">
<div class="flex items-center justify-between border-b border-slate-200 px-4 py-3">
<div>
<p class="text-sm font-semibold text-slate-900">审核队列</p>
@@ -939,7 +953,7 @@ onMounted(loadAdminData)
>
<button
type="button"
class="flex min-w-0 flex-1 items-center gap-3 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-600"
class="oc-interactive-row flex min-w-0 flex-1 items-center gap-3 text-left"
:aria-pressed="activeReviewImage.id === item.id"
@click="activeReviewId = item.id"
>
@@ -965,7 +979,7 @@ onMounted(loadAdminData)
<div class="grid min-w-0 gap-4 lg:grid-cols-[minmax(0,1fr)_300px]">
<div class="min-w-0 space-y-4">
<div class="border border-slate-200 bg-white">
<div class="oc-panel-card border border-slate-200 bg-white">
<div class="flex items-center justify-between gap-4 border-b border-slate-200 px-4 py-3">
<div class="min-w-0">
<p class="truncate text-sm font-semibold text-slate-900">{{ activeReviewImage.cloudTypeName }}</p>
@@ -995,7 +1009,7 @@ onMounted(loadAdminData)
</div>
</div>
<div class="border border-slate-200 bg-white p-5">
<div class="oc-panel-card border border-slate-200 bg-white p-5">
<p class="oc-section-label">Uploader Notes</p>
<h3 class="mt-2 text-lg font-bold text-slate-950">上传说明</h3>
<p class="mt-3 text-sm leading-7 text-slate-700">
@@ -1004,7 +1018,7 @@ onMounted(loadAdminData)
</div>
</div>
<aside class="self-start border border-slate-200 bg-white lg:sticky lg:top-20">
<aside class="oc-panel-card self-start border border-slate-200 bg-white lg:sticky lg:top-20">
<div class="border-b border-slate-200 bg-[linear-gradient(180deg,#fffbeb_0%,#fff_100%)] p-5">
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-amber-700">Review Decision</p>
<h3 class="mt-2 text-xl font-bold text-slate-950">审核资料</h3>
@@ -1088,13 +1102,13 @@ onMounted(loadAdminData)
</div>
</div>
<div v-else class="border border-dashed border-slate-300 bg-white p-10">
<div v-else class="oc-panel-card border border-dashed border-slate-300 bg-white p-10">
<NEmpty description="当前没有待审核图片" />
</div>
</section>
<section v-else-if="activeTab === 'users'">
<div class="overflow-hidden border border-slate-200 bg-white">
<div class="oc-panel-card 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">
<span>用户</span>
<span>角色</span>
@@ -1145,7 +1159,7 @@ onMounted(loadAdminData)
</section>
<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="oc-panel-card 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>
<h2 class="text-xl font-bold text-slate-950">图片管理</h2>
<p class="oc-field-help">最近 {{ images.length }} 张图片,可调整审核状态、可见性或删除。</p>
@@ -1160,7 +1174,14 @@ onMounted(loadAdminData)
<option value="approved">已通过</option>
<option value="rejected">已拒绝</option>
</select>
<NButton secondary strong type="default" class="oc-panel-button oc-panel-button--neutral" @click="toggleAllFilteredImageSelection">
<NButton
v-if="filteredImages.length > 0"
secondary
strong
type="default"
class="oc-panel-button oc-panel-button--neutral"
@click="toggleAllFilteredImageSelection"
>
{{ selectedManagedCount === filteredImages.length && filteredImages.length ? '取消全选' : '全选当前列表' }}
</NButton>
<template v-if="selectedManagedCount">
@@ -1219,8 +1240,8 @@ onMounted(loadAdminData)
</div>
<div v-if="filteredImages.length" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<article v-for="item in filteredImages" :key="item.id" class="overflow-hidden border border-slate-200 bg-white">
<button type="button" class="block w-full overflow-hidden bg-slate-100" @click="selectedImage = item">
<article v-for="item in filteredImages" :key="item.id" class="oc-panel-card overflow-hidden border border-slate-200 bg-white">
<button type="button" class="oc-interactive-card block w-full overflow-hidden bg-slate-100" @click="selectedImage = item">
<img :src="item.thumbnail_url || item.image_url" :alt="item.cloudTypeName" class="h-52 w-full object-cover transition-transform hover:scale-105" />
</button>
<div class="p-4">
@@ -1263,7 +1284,7 @@ onMounted(loadAdminData)
</article>
</div>
<div v-else class="border border-dashed border-slate-300 bg-white p-10">
<div v-else class="oc-panel-card border border-dashed border-slate-300 bg-white p-10">
<NEmpty description="没有符合筛选条件的图片" />
</div>
</section>