chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成

包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、
文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
iven
2026-03-29 10:46:26 +08:00
parent 9a5fad2b59
commit 5fdf96c3f5
268 changed files with 22011 additions and 3886 deletions

View File

@@ -0,0 +1,115 @@
// ============================================================
// Skeleton 组件 — 替代全屏 spinner 的骨架屏
// ============================================================
import { cn } from '@/lib/utils'
function SkeletonBase({ className }: { className?: string }) {
return (
<div
className={cn(
'animate-pulse rounded-md bg-muted',
className,
)}
/>
)
}
/** 表格骨架屏 */
export function TableSkeleton({
rows = 5,
cols = 5,
hasToolbar = true,
}: {
rows?: number
cols?: number
hasToolbar?: boolean
}) {
return (
<div className="space-y-4">
{hasToolbar && (
<div className="flex items-center justify-between">
<SkeletonBase className="h-9 w-[200px]" />
<SkeletonBase className="h-9 w-[120px]" />
</div>
)}
<div className="rounded-md border border-border overflow-hidden">
{/* Header */}
<div className="border-b border-border bg-muted/30 px-4 py-3">
<div className="flex gap-4">
{Array.from({ length: cols }).map((_, i) => (
<SkeletonBase
key={i}
className={cn(
'h-4',
i === 0 ? 'w-[120px]' : i === cols - 1 ? 'w-[80px]' : 'w-[100px]',
)}
/>
))}
</div>
</div>
{/* Rows */}
{Array.from({ length: rows }).map((_, rowIdx) => (
<div
key={rowIdx}
className={cn(
'px-4 py-3',
rowIdx < rows - 1 && 'border-b border-border',
)}
>
<div className="flex gap-4">
{Array.from({ length: cols }).map((_, colIdx) => (
<SkeletonBase
key={colIdx}
className={cn(
'h-4',
colIdx === 0 ? 'w-[120px]' : colIdx === cols - 1 ? 'w-[80px]' : 'w-[100px]',
)}
/>
))}
</div>
</div>
))}
</div>
{/* Pagination */}
<div className="flex items-center justify-between">
<SkeletonBase className="h-4 w-[140px]" />
<div className="flex gap-2">
<SkeletonBase className="h-8 w-[80px]" />
<SkeletonBase className="h-8 w-[80px]" />
</div>
</div>
</div>
)
}
/** 统计卡片骨架屏 */
export function StatsSkeleton({ count = 4 }: { count?: number }) {
return (
<div className={`grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-${count}`}>
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="rounded-lg border border-border p-6">
<SkeletonBase className="h-4 w-[80px]" />
<SkeletonBase className="mt-2 h-8 w-[100px]" />
<SkeletonBase className="mt-1 h-3 w-[120px]" />
</div>
))}
</div>
)
}
/** 图表骨架屏 */
export function ChartSkeleton({ height }: { height?: number }) {
return (
<div className="rounded-lg border border-border">
<div className="border-b border-border px-6 py-4">
<SkeletonBase className="h-5 w-[140px]" />
</div>
<div className="p-6">
<SkeletonBase className="w-full" />
</div>
</div>
)
}
export { SkeletonBase as Skeleton }

View File

@@ -0,0 +1,63 @@
'use client'
import { AlertCircle, Inbox } from 'lucide-react'
/** 统一的错误提示横幅 */
export function ErrorBanner({
message,
onDismiss,
}: {
message: string
onDismiss?: () => void
}) {
return (
<div className="rounded-md bg-destructive/10 border border-destructive/20 px-4 py-3 text-sm text-destructive flex items-center gap-2">
<AlertCircle className="h-4 w-4 shrink-0" />
<span className="flex-1">{message}</span>
{onDismiss && (
<button
onClick={onDismiss}
className="underline cursor-pointer shrink-0"
>
</button>
)}
</div>
)
}
/** 统一的空状态占位 */
export function EmptyState({
message = '暂无数据',
}: {
message?: string
}) {
return (
<div className="flex h-64 flex-col items-center justify-center gap-2 text-muted-foreground">
<Inbox className="h-8 w-8" />
<span className="text-sm">{message}</span>
</div>
)
}
/** 统一的加载失败提示 + 重试 */
export function ErrorRetry({
message = '请求失败,请重试',
onRetry,
}: {
message?: string
onRetry: () => void
}) {
return (
<div className="flex h-64 flex-col items-center justify-center gap-3 text-muted-foreground">
<AlertCircle className="h-8 w-8 text-destructive" />
<span className="text-sm">{message}</span>
<button
onClick={onRetry}
className="rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90 transition-colors cursor-pointer"
>
</button>
</div>
)
}