Initial miaojingAI project with image resolution guard

This commit is contained in:
FengLee
2026-05-09 11:32:34 +08:00
commit d499020d4e
264 changed files with 54160 additions and 0 deletions

View File

@@ -0,0 +1,946 @@
'use client';
import type { ReactNode } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import dynamic from 'next/dynamic';
import {
AlertTriangle,
BarChart3,
Bell,
CheckCircle2,
Coins,
CreditCard,
Database,
Eye,
Home,
Key,
LayoutDashboard,
ListChecks,
Logs,
Loader2,
Menu,
PlugZap,
Receipt,
RefreshCw,
Settings,
Shield,
Sparkles,
Users,
X,
type LucideIcon,
} from 'lucide-react';
import { toast } from 'sonner';
import { useAuth } from '@/lib/auth-store';
import { useSiteConfig } from '@/lib/site-config';
import { cn } from '@/lib/utils';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
const ApiManagementTab = dynamic(() => import('@/components/admin/api-management-tab'), { ssr: false });
const UserManagementTab = dynamic(() => import('@/components/admin/user-management-tab'), { ssr: false });
const PricingTab = dynamic(() => import('@/components/admin/pricing-tab'), { ssr: false });
const OrderManagementTab = dynamic(() => import('@/components/admin/order-management-tab'), { ssr: false });
const PaymentTab = dynamic(() => import('@/components/admin/payment-tab'), { ssr: false });
const AnnouncementTab = dynamic(() => import('@/components/admin/announcement-tab'), { ssr: false });
const DataManagementTab = dynamic(() => import('@/components/admin/data-management-tab'), { ssr: false });
const TaskManagementTab = dynamic(() => import('@/components/admin/task-management-tab'), { ssr: false });
const LogManagementTab = dynamic(() => import('@/components/admin/log-management-tab'), { ssr: false });
const SettingsTab = dynamic(() => import('@/components/admin/settings-tab'), { ssr: false });
type ConsoleView =
| 'dashboard'
| 'api'
| 'users'
| 'pricing'
| 'orders'
| 'payment'
| 'announcements'
| 'data'
| 'tasks'
| 'logs'
| 'settings';
type NavItem = {
value: ConsoleView;
label: string;
icon: LucideIcon;
hidden?: boolean;
};
type NavGroup = {
label: string;
items: NavItem[];
};
type DashboardSummary = {
generatedAt: string | null;
platform: {
totalVisits: number;
databaseTime: string | null;
};
users: {
total: number;
active: number;
disabled: number;
admins: number;
members: number;
created7d: number;
};
works: {
total: number;
public: number;
private: number;
completed: number;
failed: number;
withResultUrl: number;
created7d: number;
resultUrlCoverage: number;
byType: {
text2img: number;
img2img: number;
text2video: number;
img2video: number;
};
};
tasks: {
total: number;
queued: number;
running: number;
succeeded: number;
failed: number;
latest: Array<{
id: string;
type: string;
status: string;
error: string | null;
createdAt: string | null;
updatedAt: string | null;
}>;
};
orders: {
total: number;
pending: number;
paid: number;
cancelled: number;
refunded: number;
paidRevenue: number;
paidRevenue7d: number;
latest: Array<{
id: string;
orderNo: string;
productName: string;
amount: number;
status: string;
createdAt: string | null;
}>;
};
providers: {
total: number;
active: number;
inactive: number;
image: number;
video: number;
text: number;
incomplete: number;
recommendationsTotal: number;
recommendationsActive: number;
userApiKeysTotal: number;
userApiKeysActive: number;
};
announcements: {
total: number;
active: number;
scheduled: number;
expired: number;
};
system: {
apiHealth: boolean;
databaseHealth: boolean;
storageHealth?: boolean;
storageDirConfigured?: boolean;
worksPersisted?: number;
worksTotal?: number;
logsTotal?: number;
logsErrors?: number;
logsCreated24h?: number;
};
};
const EMPTY_DASHBOARD_SUMMARY: DashboardSummary = {
generatedAt: null,
platform: { totalVisits: 0, databaseTime: null },
users: { total: 0, active: 0, disabled: 0, admins: 0, members: 0, created7d: 0 },
works: {
total: 0,
public: 0,
private: 0,
completed: 0,
failed: 0,
withResultUrl: 0,
created7d: 0,
resultUrlCoverage: 1,
byType: { text2img: 0, img2img: 0, text2video: 0, img2video: 0 },
},
tasks: { total: 0, queued: 0, running: 0, succeeded: 0, failed: 0, latest: [] },
orders: { total: 0, pending: 0, paid: 0, cancelled: 0, refunded: 0, paidRevenue: 0, paidRevenue7d: 0, latest: [] },
providers: {
total: 0,
active: 0,
inactive: 0,
image: 0,
video: 0,
text: 0,
incomplete: 0,
recommendationsTotal: 0,
recommendationsActive: 0,
userApiKeysTotal: 0,
userApiKeysActive: 0,
},
announcements: { total: 0, active: 0, scheduled: 0, expired: 0 },
system: {
apiHealth: false,
databaseHealth: false,
storageHealth: false,
storageDirConfigured: false,
worksPersisted: 0,
worksTotal: 0,
logsTotal: 0,
logsErrors: 0,
logsCreated24h: 0,
},
};
const VIEW_TITLES: Record<ConsoleView, { title: string; description: string }> = {
dashboard: { title: '仪表盘', description: '查看运营、任务、支付、模型和系统健康概览' },
api: { title: 'API 管理', description: '配置供应商、模型推荐与系统 API' },
users: { title: '用户管理', description: '管理用户、额度、会员与账号状态' },
pricing: { title: '价格设置', description: '维护套餐价格与积分规则' },
orders: { title: '订单管理', description: '查看订单并处理支付状态' },
payment: { title: '支付配置', description: '配置可用支付方式' },
announcements: { title: '公告管理', description: '创建和维护站点弹窗公告' },
data: { title: '数据管理', description: '导出、导入与恢复业务数据' },
tasks: { title: '任务管理', description: '查看生成任务状态并清理任务' },
logs: { title: '系统日志', description: '查看平台运行、登录、安全和管理操作日志' },
settings: { title: '系统设置', description: '维护站点信息、邮箱与通知设置' },
};
function useAdminDashboard(accessToken: string | null) {
const [summary, setSummary] = useState<DashboardSummary>(EMPTY_DASHBOARD_SUMMARY);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const load = useCallback(async ({ silent = false, showSuccess = false }: { silent?: boolean; showSuccess?: boolean } = {}) => {
if (!accessToken) {
setLoading(false);
return;
}
if (silent) {
setRefreshing(true);
} else {
setLoading(true);
}
setError(null);
try {
const res = await fetch('/api/admin/dashboard', {
headers: { Authorization: `Bearer ${accessToken}` },
cache: 'no-store',
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || '仪表盘数据加载失败');
setSummary({ ...EMPTY_DASHBOARD_SUMMARY, ...data });
if (showSuccess) toast.success('仪表盘已刷新');
} catch (err) {
const message = err instanceof Error ? err.message : '仪表盘数据加载失败';
setError(message);
if (!silent || showSuccess) toast.error(message);
} finally {
setLoading(false);
setRefreshing(false);
}
}, [accessToken]);
useEffect(() => {
let cancelled = false;
async function initialLoad() {
if (!accessToken) {
setLoading(false);
return;
}
setLoading(true);
setError(null);
try {
const res = await fetch('/api/admin/dashboard', {
headers: { Authorization: `Bearer ${accessToken}` },
cache: 'no-store',
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || '仪表盘数据加载失败');
if (!cancelled) setSummary({ ...EMPTY_DASHBOARD_SUMMARY, ...data });
} catch (err) {
const message = err instanceof Error ? err.message : '仪表盘数据加载失败';
if (!cancelled) {
setError(message);
toast.error(message);
}
} finally {
if (!cancelled) setLoading(false);
}
}
initialLoad();
const timer = window.setInterval(() => {
if (!cancelled) load({ silent: true });
}, 30000);
return () => {
cancelled = true;
window.clearInterval(timer);
};
}, [accessToken, load]);
return { summary, loading, refreshing, error, refresh: () => load({ silent: true, showSuccess: true }) };
}
export default function ConsoleDashboardPage() {
const { isLoggedIn, isAdmin, user, accessToken, logout } = useAuth();
const { config: siteConfig } = useSiteConfig();
const router = useRouter();
const [mounted, setMounted] = useState(false);
const [activeView, setActiveView] = useState<ConsoleView>('dashboard');
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const membershipEnabled = siteConfig.membershipEnabled !== false;
const navGroups = useMemo<NavGroup[]>(() => {
const groups: NavGroup[] = [
{
label: '总览',
items: [{ value: 'dashboard', label: '仪表盘', icon: LayoutDashboard }],
},
{
label: '运营',
items: [
{ value: 'users', label: '用户管理', icon: Users },
{ value: 'pricing', label: '价格设置', icon: Coins, hidden: !membershipEnabled },
{ value: 'orders', label: '订单管理', icon: Receipt, hidden: !membershipEnabled },
{ value: 'payment', label: '支付配置', icon: CreditCard, hidden: !membershipEnabled },
],
},
{
label: '创作',
items: [
{ value: 'api', label: 'API 管理', icon: Key },
{ value: 'tasks', label: '任务管理', icon: ListChecks },
{ value: 'announcements', label: '公告管理', icon: Bell },
],
},
{
label: '系统',
items: [
{ value: 'data', label: '数据管理', icon: Database },
{ value: 'logs', label: '系统日志', icon: Logs },
{ value: 'settings', label: '系统设置', icon: Settings },
],
},
];
return groups
.map(group => ({ ...group, items: group.items.filter(item => !item.hidden) }))
.filter(group => group.items.length > 0);
}, [membershipEnabled]);
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (!membershipEnabled && ['pricing', 'orders', 'payment'].includes(activeView)) {
setActiveView('dashboard');
}
}, [membershipEnabled, activeView]);
useEffect(() => {
if (!mounted) return;
if (!isLoggedIn || !isAdmin || !accessToken) {
router.replace('/console');
}
}, [mounted, isLoggedIn, isAdmin, accessToken, router]);
if (!mounted) {
return <div className="min-h-screen bg-background" />;
}
if (!isLoggedIn || !isAdmin || !accessToken) {
return <div className="min-h-screen bg-background" />;
}
const title = VIEW_TITLES[activeView];
const handleLogout = () => {
logout();
router.replace('/console');
};
const navigateToView = (view: ConsoleView) => {
setActiveView(view);
setMobileNavOpen(false);
};
return (
<div className="fixed inset-0 flex overflow-hidden bg-background text-foreground">
<aside className="hidden w-[17rem] shrink-0 border-r border-border/70 bg-sidebar/95 lg:flex lg:flex-col">
<ConsoleSidebar
activeView={activeView}
navGroups={navGroups}
onNavigate={navigateToView}
onBackHome={() => router.push('/')}
onLogout={handleLogout}
userName={user?.nickname || '管理员'}
userEmail={user?.email || ''}
/>
</aside>
{mobileNavOpen && (
<div className="fixed inset-0 z-50 lg:hidden">
<button
type="button"
aria-label="关闭导航"
className="absolute inset-0 bg-black/55"
onClick={() => setMobileNavOpen(false)}
/>
<aside className="absolute inset-y-0 left-0 flex w-[min(86vw,21rem)] flex-col border-r border-border bg-sidebar shadow-2xl">
<ConsoleSidebar
activeView={activeView}
navGroups={navGroups}
onNavigate={navigateToView}
onBackHome={() => router.push('/')}
onLogout={handleLogout}
userName={user?.nickname || '管理员'}
userEmail={user?.email || ''}
closeButton={
<Button variant="ghost" size="sm" className="h-9 w-9 p-0" onClick={() => setMobileNavOpen(false)}>
<X className="h-4 w-4" />
</Button>
}
/>
</aside>
</div>
)}
<div className="flex min-w-0 flex-1 flex-col">
<header className="flex h-16 shrink-0 items-center justify-between border-b border-border/70 bg-background/95 px-4 backdrop-blur lg:px-6">
<div className="flex min-w-0 items-center gap-3">
<Button variant="ghost" size="sm" className="h-9 w-9 p-0 lg:hidden" onClick={() => setMobileNavOpen(true)}>
<Menu className="h-5 w-5" />
</Button>
<div className="min-w-0">
<h1 className="truncate text-lg font-semibold sm:text-xl">{title.title}</h1>
<p className="hidden truncate text-xs text-muted-foreground sm:block">{title.description}</p>
</div>
</div>
<div className="flex shrink-0 items-center gap-2">
<Badge variant="secondary" className="hidden sm:inline-flex">
</Badge>
</div>
</header>
<main className="min-h-0 flex-1 overflow-y-auto">
<div className="min-h-full w-full px-4 py-5 sm:px-6 lg:px-8">
<ConsoleContent activeView={activeView} setActiveView={setActiveView} />
</div>
</main>
</div>
</div>
);
}
function ConsoleSidebar({
activeView,
navGroups,
onNavigate,
onBackHome,
onLogout,
userName,
userEmail,
closeButton,
}: {
activeView: ConsoleView;
navGroups: NavGroup[];
onNavigate: (view: ConsoleView) => void;
onBackHome: () => void;
onLogout: () => void;
userName: string;
userEmail: string;
closeButton?: ReactNode;
}) {
return (
<>
<div className="flex h-16 shrink-0 items-center justify-between border-b border-border/70 px-4">
<div className="flex min-w-0 items-center gap-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
<Shield className="h-5 w-5" />
</div>
<div className="min-w-0">
<div className="truncate font-semibold"> Console</div>
<div className="truncate text-xs text-muted-foreground">{userEmail || userName}</div>
</div>
</div>
{closeButton}
</div>
<nav className="min-h-0 flex-1 overflow-y-auto px-3 py-4">
<div className="space-y-5">
{navGroups.map(group => (
<div key={group.label}>
<div className="mb-2 px-3 text-[11px] font-semibold uppercase tracking-normal text-muted-foreground">
{group.label}
</div>
<div className="space-y-1">
{group.items.map(item => {
const Icon = item.icon;
const active = activeView === item.value;
return (
<button
key={item.value}
type="button"
onClick={() => onNavigate(item.value)}
className={cn(
'flex h-10 w-full items-center gap-3 rounded-md px-3 text-left text-sm font-medium transition-colors',
active
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:bg-muted/70 hover:text-foreground',
)}
>
<Icon className="h-4 w-4 shrink-0" />
<span className="truncate">{item.label}</span>
</button>
);
})}
</div>
</div>
))}
</div>
</nav>
<div className="shrink-0 border-t border-border/70 p-3">
<div className="mb-3 rounded-md border border-border/70 bg-background/45 px-3 py-2">
<div className="truncate text-sm font-medium">{userName}</div>
<div className="text-xs text-muted-foreground"></div>
</div>
<div className="grid grid-cols-2 gap-2">
<Button variant="outline" size="sm" className="gap-1.5" onClick={onBackHome}>
<Home className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm" onClick={onLogout}>
退
</Button>
</div>
</div>
</>
);
}
function ConsoleContent({
activeView,
setActiveView,
}: {
activeView: ConsoleView;
setActiveView: (view: ConsoleView) => void;
}) {
switch (activeView) {
case 'dashboard':
return <DashboardView setActiveView={setActiveView} />;
case 'api':
return <ApiManagementTab />;
case 'users':
return <UserManagementTab />;
case 'pricing':
return <PricingTab />;
case 'orders':
return <OrderManagementTab />;
case 'payment':
return <PaymentTab />;
case 'announcements':
return <AnnouncementTab />;
case 'data':
return <DataManagementTab />;
case 'tasks':
return <TaskManagementTab />;
case 'logs':
return <LogManagementTab />;
case 'settings':
return <SettingsTab />;
default:
return <DashboardView setActiveView={setActiveView} />;
}
}
function DashboardView({ setActiveView }: { setActiveView: (view: ConsoleView) => void }) {
const { accessToken } = useAuth();
const { summary, loading, refreshing, error, refresh } = useAdminDashboard(accessToken);
const persistedWorks = summary.system.worksPersisted ?? summary.works.withResultUrl;
const totalWorksForCoverage = summary.system.worksTotal ?? summary.works.total;
const resultUrlCoverage = totalWorksForCoverage > 0 ? persistedWorks / totalWorksForCoverage : 1;
const riskItems = [
{
label: '失败任务',
value: summary.tasks.failed,
detail: summary.tasks.failed > 0 ? '需要检查任务错误并清理' : '任务队列无失败积压',
view: 'tasks' as ConsoleView,
severity: summary.tasks.failed > 0 ? 'warning' : 'ok',
},
{
label: '待处理订单',
value: summary.orders.pending,
detail: summary.orders.pending > 0 ? '存在待处理支付订单' : '订单状态正常',
view: 'orders' as ConsoleView,
severity: summary.orders.pending > 0 ? 'warning' : 'ok',
},
{
label: '未完整供应商',
value: summary.providers.incomplete,
detail: summary.providers.incomplete > 0 ? '供应商缺少地址或默认模型' : '供应商配置完整',
view: 'api' as ConsoleView,
severity: summary.providers.incomplete > 0 ? 'warning' : 'ok',
},
{
label: '作品结果覆盖',
value: `${Math.round(resultUrlCoverage * 100)}%`,
detail: resultUrlCoverage < 1 ? '存在缺少结果链接的作品' : '持久化结果链接完整',
view: 'data' as ConsoleView,
severity: resultUrlCoverage < 1 ? 'warning' : 'ok',
},
];
const statCards = [
{ label: '总访问量', value: summary.platform.totalVisits, sub: '站点累计访问', icon: Eye, tone: 'text-sky-500' },
{ label: '注册用户', value: summary.users.total, sub: `7日新增 ${formatNumber(summary.users.created7d)}`, icon: Users, tone: 'text-emerald-500' },
{ label: '公开作品', value: summary.works.public, sub: `总作品 ${formatNumber(summary.works.total)}`, icon: BarChart3, tone: 'text-amber-500' },
{ label: '任务总数', value: summary.tasks.total, sub: `运行 ${formatNumber(summary.tasks.running)} / 排队 ${formatNumber(summary.tasks.queued)}`, icon: ListChecks, tone: 'text-violet-500' },
{ label: '支付收入', value: formatCurrency(summary.orders.paidRevenue), sub: `7日 ${formatCurrency(summary.orders.paidRevenue7d)}`, icon: Receipt, tone: 'text-rose-500' },
{ label: '启用模型源', value: `${formatNumber(summary.providers.active)}/${formatNumber(summary.providers.total)}`, sub: `推荐模型 ${formatNumber(summary.providers.recommendationsActive)}`, icon: PlugZap, tone: 'text-cyan-500' },
];
const quickActions: Array<{ label: string; description: string; view: ConsoleView; icon: LucideIcon }> = [
{ label: '配置模型 API', description: '维护供应商、推荐模型和默认能力', view: 'api', icon: Key },
{ label: '查看任务队列', description: '排查失败、排队和运行中的生成任务', view: 'tasks', icon: ListChecks },
{ label: '查看系统日志', description: '筛选登录、安全、生成和管理操作日志', view: 'logs', icon: Logs },
{ label: '管理用户额度', description: '调整会员、积分和账号状态', view: 'users', icon: Users },
{ label: '导出数据备份', description: '下载当前业务数据并确认恢复入口', view: 'data', icon: Database },
];
return (
<div className="space-y-6">
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div>
<h2 className="text-2xl font-semibold tracking-normal"></h2>
<p className="text-sm text-muted-foreground">
{summary.generatedAt ? `最后更新 ${formatDateTime(summary.generatedAt)}` : '正在读取系统概览'}
</p>
</div>
<Button
variant="outline"
className="w-full gap-2 md:w-auto"
onClick={refresh}
disabled={refreshing || loading}
aria-busy={refreshing}
>
<RefreshCw className={cn('h-4 w-4', refreshing && 'animate-spin')} />
{refreshing ? '刷新中...' : '刷新'}
</Button>
</div>
{error && (
<div className="flex items-center gap-2 rounded-md border border-destructive/35 bg-destructive/10 px-4 py-3 text-sm text-destructive">
<AlertTriangle className="h-4 w-4 shrink-0" />
<span>{error}</span>
</div>
)}
<div className="grid gap-4 sm:grid-cols-2 2xl:grid-cols-6">
{statCards.map(item => {
const Icon = item.icon;
return (
<Card key={item.label} className="overflow-hidden">
<CardContent className="flex min-h-[7.5rem] items-center gap-4 p-5">
<div className={cn('flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-muted', item.tone)}>
<Icon className="h-5 w-5" />
</div>
<div className="min-w-0">
<p className="text-sm text-muted-foreground">{item.label}</p>
<p className="mt-1 truncate text-2xl font-bold">
{loading ? <Loader2 className="h-6 w-6 animate-spin" /> : item.value}
</p>
<p className="mt-1 truncate text-xs text-muted-foreground">{item.sub}</p>
</div>
</CardContent>
</Card>
);
})}
</div>
<div className="grid gap-4 xl:grid-cols-[1.35fr_0.65fr]">
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<CardDescription>线</CardDescription>
</CardHeader>
<CardContent className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
{riskItems.map(item => (
<button
key={item.label}
type="button"
onClick={() => setActiveView(item.view)}
className="rounded-lg border border-border/70 p-4 text-left transition-colors hover:bg-muted/60"
>
<div className="flex items-center justify-between gap-2">
<span className="text-sm text-muted-foreground">{item.label}</span>
{item.severity === 'warning' ? (
<AlertTriangle className="h-4 w-4 text-amber-500" />
) : (
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
)}
</div>
<div className="mt-2 text-2xl font-bold">{typeof item.value === 'number' ? formatNumber(item.value) : item.value}</div>
<div className="mt-1 text-xs text-muted-foreground">{item.detail}</div>
</button>
))}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<HealthLine label="管理 API" ok={summary.system.apiHealth} />
<HealthLine label="数据库连接" ok={summary.system.databaseHealth} />
<HealthLine label="持久化存储" ok={Boolean(summary.system.storageHealth)} />
<SummaryLine label="作品结果链接" value={`${formatNumber(summary.system.worksPersisted ?? summary.works.withResultUrl)}/${formatNumber(summary.system.worksTotal ?? summary.works.total)}`} />
<SummaryLine label="系统日志" value={`${formatNumber(summary.system.logsCreated24h || 0)} 条/24小时`} />
<SummaryLine label="错误日志" value={formatNumber(summary.system.logsErrors || 0)} />
<SummaryLine label="用户 API Key" value={`${formatNumber(summary.providers.userApiKeysActive)}/${formatNumber(summary.providers.userApiKeysTotal)}`} />
</CardContent>
</Card>
</div>
<div className="grid gap-4 xl:grid-cols-3">
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<SummaryBlock label="排队" value={summary.tasks.queued} />
<SummaryBlock label="运行中" value={summary.tasks.running} />
<SummaryBlock label="已完成" value={summary.tasks.succeeded} />
<SummaryBlock label="失败" value={summary.tasks.failed} />
</div>
<RecentList
emptyText="暂无生成任务"
items={summary.tasks.latest.map(task => ({
key: task.id,
title: `${task.type || 'generation'} · ${statusText(task.status)}`,
meta: task.error || formatDateTime(task.createdAt),
tone: task.status === 'failed' ? 'warning' : undefined,
}))}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<SummaryBlock label="待支付" value={summary.orders.pending} />
<SummaryBlock label="已支付" value={summary.orders.paid} />
<SummaryBlock label="已取消" value={summary.orders.cancelled} />
<SummaryBlock label="已退款" value={summary.orders.refunded} />
</div>
<RecentList
emptyText="暂无订单"
items={summary.orders.latest.map(order => ({
key: order.id,
title: order.productName || order.orderNo || order.id.slice(0, 8),
meta: `${formatCurrency(order.amount)} · ${statusText(order.status)}`,
tone: order.status === 'pending' ? 'warning' : undefined,
}))}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<SummaryLine label="图像供应商" value={summary.providers.image} />
<SummaryLine label="视频供应商" value={summary.providers.video} />
<SummaryLine label="文本供应商" value={summary.providers.text} />
<SummaryLine label="生图作品" value={summary.works.byType.text2img + summary.works.byType.img2img} />
<SummaryLine label="视频作品" value={summary.works.byType.text2video + summary.works.byType.img2video} />
<SummaryLine label="生效公告" value={`${formatNumber(summary.announcements.active)}/${formatNumber(summary.announcements.total)}`} />
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
{quickActions.map(action => {
const Icon = action.icon;
return (
<button
key={action.view}
type="button"
onClick={() => setActiveView(action.view)}
className="rounded-lg border border-border/70 p-4 text-left transition-colors hover:bg-muted/60"
>
<Icon className="mb-3 h-5 w-5 text-primary" />
<div className="font-medium">{action.label}</div>
<div className="mt-1 text-xs text-muted-foreground">{action.description}</div>
</button>
);
})}
</CardContent>
</Card>
<div className="grid gap-4 lg:grid-cols-3">
<InsightCard
icon={<Sparkles className="h-4 w-4" />}
title="AI 平台后台"
description="仪表盘聚焦模型源、用量、任务状态和失败项,便于第一时间定位生成链路问题。"
/>
<InsightCard
icon={<Receipt className="h-4 w-4" />}
title="交易后台"
description="订单状态、待处理支付和营收数据前置,保持支付和会员运营可追踪。"
/>
<InsightCard
icon={<Database className="h-4 w-4" />}
title="云控制台"
description="数据库健康、持久化链接和备份入口放在首屏,支撑上线后的运维检查。"
/>
</div>
</div>
);
}
function SummaryBlock({ label, value }: { label: string; value: number }) {
return (
<div className="rounded-lg border border-border/70 p-3">
<p className="text-xs text-muted-foreground">{label}</p>
<p className="mt-1 text-xl font-bold">{formatNumber(value)}</p>
</div>
);
}
function SummaryLine({ label, value }: { label: string; value: number | string }) {
return (
<div className="flex items-center justify-between rounded-md bg-muted/45 px-3 py-2 text-sm">
<span className="text-muted-foreground">{label}</span>
<span className="font-semibold">{typeof value === 'number' ? formatNumber(value) : value}</span>
</div>
);
}
function HealthLine({ label, ok }: { label: string; ok: boolean }) {
return (
<div className="flex items-center justify-between rounded-md bg-muted/45 px-3 py-2 text-sm">
<span className="text-muted-foreground">{label}</span>
<Badge variant={ok ? 'secondary' : 'destructive'} className="gap-1">
{ok ? <CheckCircle2 className="h-3.5 w-3.5" /> : <AlertTriangle className="h-3.5 w-3.5" />}
{ok ? '正常' : '异常'}
</Badge>
</div>
);
}
function RecentList({
items,
emptyText,
}: {
items: Array<{ key: string; title: string; meta: string; tone?: 'warning' }>;
emptyText: string;
}) {
if (items.length === 0) {
return <div className="rounded-lg border border-dashed border-border/80 px-3 py-5 text-center text-sm text-muted-foreground">{emptyText}</div>;
}
return (
<div className="space-y-2">
{items.slice(0, 4).map(item => (
<div key={item.key} className="flex min-w-0 items-center gap-3 rounded-md border border-border/70 px-3 py-2">
<div className={cn('h-2 w-2 shrink-0 rounded-full bg-emerald-500', item.tone === 'warning' && 'bg-amber-500')} />
<div className="min-w-0">
<div className="truncate text-sm font-medium">{item.title}</div>
<div className="truncate text-xs text-muted-foreground">{item.meta}</div>
</div>
</div>
))}
</div>
);
}
function InsightCard({ icon, title, description }: { icon: ReactNode; title: string; description: string }) {
return (
<div className="rounded-lg border border-border/70 bg-card p-4">
<div className="mb-3 flex h-8 w-8 items-center justify-center rounded-md bg-primary/10 text-primary">{icon}</div>
<div className="font-medium">{title}</div>
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
</div>
);
}
function formatNumber(value: number): string {
return Number(value || 0).toLocaleString('zh-CN');
}
function formatCurrency(value: number): string {
return `¥${Number(value || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}
function formatDateTime(value: string | null): string {
if (!value) return '暂无时间';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '暂无时间';
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
}
function statusText(status: string): string {
const map: Record<string, string> = {
queued: '排队',
running: '运行中',
succeeded: '已完成',
failed: '失败',
pending: '待支付',
paid: '已支付',
cancelled: '已取消',
refunded: '已退款',
completed: '已完成',
};
return map[status] || status || '未知';
}