feat: 前端 UI 重构,提升视觉效果和交互体验

- Dashboard: 全新 hero 区域,gradient mesh 背景,stagger 动画特性卡片
- API 测试: 改为左右分栏布局(请求/响应),增加状态指示和复制按钮
- Database: 使用 PageContainer 统一风格,改善结果展示
- 新增 404 页面和 catch-all 路由
- Sidebar: 更紧凑精致,优化折叠/展开交互
- Dialog: backdrop blur 效果,圆角升级
- CSS: 新增 gradient-mesh、grid-pattern、stagger-children 等动画
- 配色系统升级为更鲜艳的靛蓝主色调

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dreamhunter2333andClaude Opus 4.6 committed 2026-03-12 18:24:57 +08:00
1 parent 6f99358fb5
commit 07a597e41a
9 files changed
+503 -349

No files matched your search

+2
View File
@@ -10,6 +10,7 @@ import { Tokens } from './pages/Tokens'
import { Pricing } from './pages/Pricing'
import { ApiTest } from './pages/ApiTest'
import { Database } from './pages/Database'
import { NotFound } from './pages/NotFound'
const queryClient = new QueryClient({
defaultOptions: {
@@ -80,6 +81,7 @@ function App() {
</ProtectedRoute>
}
/>
<Route path="*" element={<NotFound />} />
</Routes>
</AppLayout>
<Toaster />
+9 -8
View File
@@ -14,7 +14,7 @@ import { Button } from '@/components/ui/button'
import { useAuthStore } from '@/store/auth'
import { useToast } from '@/components/ui/use-toast'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Menu, Sparkles } from 'lucide-react'
import { Menu, Zap } from 'lucide-react'
interface AppLayoutProps {
children: ReactNode
@@ -56,23 +56,24 @@ export function AppLayout({ children }: AppLayoutProps) {
/>
<div className="flex min-w-0 flex-1 flex-col">
<header className="flex items-center justify-between border-b bg-card/80 backdrop-blur-sm px-4 py-3 lg:hidden sticky top-0 z-30">
<header className="flex items-center justify-between border-b bg-card/80 glass px-4 py-2.5 lg:hidden sticky top-0 z-30">
<Button
type="button"
variant="ghost"
size="icon"
className="h-9 w-9"
onClick={() => setIsMobileNavOpen(true)}
aria-label="打开侧边栏"
>
<Menu className="h-5 w-5" />
<Menu className="h-4 w-4" />
</Button>
<div className="flex items-center gap-2.5">
<div className="w-7 h-7 rounded-lg bg-gradient-to-br from-primary to-primary/60 flex items-center justify-center">
<Sparkles className="h-4 w-4 text-white" />
<div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-lg bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center">
<Zap className="h-3.5 w-3.5 text-white" />
</div>
<span className="font-semibold tracking-tight">Awsl One API</span>
<span className="font-semibold text-sm tracking-tight">Awsl One API</span>
</div>
<div className="h-10 w-10" />
<div className="h-9 w-9" />
</header>
<main className="flex-1 overflow-y-auto bg-background">
+53 -84
View File
@@ -28,40 +28,12 @@ interface NavItem {
}
const navItems: NavItem[] = [
{
title: '仪表盘',
href: '/',
icon: LayoutDashboard,
},
{
title: 'API 测试',
href: '/api-test',
icon: TestTube2,
},
{
title: '频道管理',
href: '/channels',
icon: LinkIcon,
adminOnly: true,
},
{
title: '令牌管理',
href: '/tokens',
icon: Key,
adminOnly: true,
},
{
title: '定价管理',
href: '/pricing',
icon: DollarSign,
adminOnly: true,
},
{
title: '数据库',
href: '/database',
icon: Database,
adminOnly: true,
},
{ title: '仪表盘', href: '/', icon: LayoutDashboard },
{ title: 'API 测试', href: '/api-test', icon: TestTube2 },
{ title: '频道管理', href: '/channels', icon: LinkIcon, adminOnly: true },
{ title: '令牌管理', href: '/tokens', icon: Key, adminOnly: true },
{ title: '定价管理', href: '/pricing', icon: DollarSign, adminOnly: true },
{ title: '数据库', href: '/database', icon: Database, adminOnly: true },
]
interface SidebarProps {
@@ -125,16 +97,16 @@ export function Sidebar({
onClick={() => onNavigate?.()}
title={collapsed ? item.title : undefined}
className={cn(
'group relative flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-200',
'group relative flex items-center gap-3 px-3 py-2 rounded-lg text-[13px] font-medium transition-all duration-150',
collapsed && 'justify-center px-0 w-10 h-10 mx-auto',
isActive
? 'bg-primary text-primary-foreground shadow-md'
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
? 'bg-primary text-primary-foreground shadow-sm shadow-primary/20'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/80'
)}
>
<item.icon className={cn(
"h-[18px] w-[18px] flex-shrink-0",
!isActive && "group-hover:scale-110 transition-transform duration-200"
"h-[17px] w-[17px] flex-shrink-0",
!isActive && "group-hover:scale-105 transition-transform duration-150"
)} />
{!collapsed && <span>{item.title}</span>}
</Link>
@@ -144,37 +116,35 @@ export function Sidebar({
return (
<aside
className={cn(
"bg-card border-r flex flex-col h-full transition-all duration-300",
collapsed ? "w-[72px]" : "w-64",
"bg-card border-r flex flex-col h-full transition-all duration-300 ease-out",
collapsed ? "w-[68px]" : "w-60",
className
)}
>
{/* Header */}
<div className={cn(
"h-16 flex items-center border-b px-4",
"h-14 flex items-center border-b px-4",
collapsed ? "justify-center px-0" : "justify-between"
)}>
{collapsed ? (
<div className="flex flex-col items-center gap-1">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center">
<Zap className="h-5 w-5 text-white" />
</div>
<div className="w-9 h-9 rounded-lg bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center">
<Zap className="h-4 w-4 text-white" />
</div>
) : (
<>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center shadow-lg shadow-primary/20">
<Zap className="h-5 w-5 text-white" />
<div className="flex items-center gap-2.5">
<div className="w-9 h-9 rounded-lg bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center shadow-md shadow-primary/15">
<Zap className="h-4 w-4 text-white" />
</div>
<div className="flex flex-col">
<span className="font-semibold text-[15px] tracking-tight">Awsl One API</span>
<span className="text-[11px] text-muted-foreground">统一 AI 接口网关</span>
<span className="font-semibold text-[14px] tracking-tight leading-tight">Awsl One API</span>
<span className="text-[10px] text-muted-foreground leading-tight">AI Gateway</span>
</div>
</div>
{onClose && (
<button
type="button"
className="p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors lg:hidden"
className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors lg:hidden"
onClick={onClose}
>
<X className="h-4 w-4" />
@@ -184,23 +154,23 @@ export function Sidebar({
)}
</div>
{/* Collapse Toggle - PC Only */}
{/* Collapse Toggle */}
{showCollapseToggle && (
<div className={cn("px-3 py-2 hidden lg:flex", collapsed && "px-0 justify-center")}>
<div className={cn("px-3 py-1.5 hidden lg:flex", collapsed && "px-0 justify-center")}>
<button
type="button"
onClick={onToggleCollapse}
className={cn(
"flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors",
collapsed ? "w-10 h-10 p-0 justify-center" : "w-full"
"flex items-center gap-2 px-2.5 py-1.5 rounded-md text-[11px] font-medium text-muted-foreground/70 hover:text-muted-foreground hover:bg-muted/50 transition-colors",
collapsed ? "w-10 h-8 p-0 justify-center" : "w-full"
)}
>
{collapsed ? (
<PanelLeftOpen className="h-4 w-4" />
<PanelLeftOpen className="h-3.5 w-3.5" />
) : (
<>
<PanelLeftClose className="h-4 w-4" />
<span>收起菜单</span>
<PanelLeftClose className="h-3.5 w-3.5" />
<span>收起</span>
</>
)}
</button>
@@ -208,10 +178,10 @@ export function Sidebar({
)}
{/* Navigation */}
<nav className="flex-1 px-3 py-2 space-y-6 overflow-y-auto scrollbar-thin">
<div className="space-y-1">
<nav className="flex-1 px-2.5 py-2 space-y-5 overflow-y-auto scrollbar-thin">
<div className="space-y-0.5">
{!collapsed && (
<div className="px-3 py-2 text-[11px] font-semibold text-muted-foreground/60 uppercase tracking-wider">
<div className="px-3 py-1.5 text-[10px] font-semibold text-muted-foreground/50 uppercase tracking-widest">
概览
</div>
)}
@@ -224,9 +194,9 @@ export function Sidebar({
{isAuthenticated && (
<>
<div className="space-y-1">
<div className="space-y-0.5">
{!collapsed && (
<div className="px-3 py-2 text-[11px] font-semibold text-muted-foreground/60 uppercase tracking-wider">
<div className="px-3 py-1.5 text-[10px] font-semibold text-muted-foreground/50 uppercase tracking-widest">
管理
</div>
)}
@@ -237,9 +207,9 @@ export function Sidebar({
))}
</div>
<div className="space-y-1">
<div className="space-y-0.5">
{!collapsed && (
<div className="px-3 py-2 text-[11px] font-semibold text-muted-foreground/60 uppercase tracking-wider">
<div className="px-3 py-1.5 text-[10px] font-semibold text-muted-foreground/50 uppercase tracking-widest">
系统
</div>
)}
@@ -254,27 +224,27 @@ export function Sidebar({
</nav>
{/* Footer */}
<div className={cn("p-3 border-t", collapsed && "p-2")}>
<div className={cn("p-2.5 border-t", collapsed && "p-2")}>
{/* Action Buttons */}
<div className={cn(
"flex gap-1",
collapsed ? "flex-col items-center" : "mb-3"
collapsed ? "flex-col items-center" : "mb-2"
)}>
<button
type="button"
title={collapsed ? (theme === 'dark' ? '浅色模式' : '深色模式') : undefined}
className={cn(
"flex items-center justify-center rounded-lg transition-all duration-200",
"flex items-center justify-center rounded-md transition-colors duration-150",
collapsed
? "w-10 h-10 hover:bg-muted text-muted-foreground hover:text-foreground"
: "flex-1 h-9 gap-2 px-3 bg-muted/50 hover:bg-muted text-muted-foreground hover:text-foreground text-xs font-medium"
? "w-10 h-9 hover:bg-muted text-muted-foreground hover:text-foreground"
: "flex-1 h-8 gap-1.5 px-2.5 hover:bg-muted/70 text-muted-foreground hover:text-foreground text-[11px] font-medium"
)}
onClick={toggleTheme}
>
{theme === 'dark' ? (
<Sun className="h-4 w-4" />
<Sun className="h-3.5 w-3.5" />
) : (
<Moon className="h-4 w-4" />
<Moon className="h-3.5 w-3.5" />
)}
{!collapsed && <span>{theme === 'dark' ? '浅色' : '深色'}</span>}
</button>
@@ -285,13 +255,13 @@ export function Sidebar({
rel="noopener noreferrer"
title={collapsed ? "GitHub" : undefined}
className={cn(
"flex items-center justify-center rounded-lg transition-all duration-200",
"flex items-center justify-center rounded-md transition-colors duration-150",
collapsed
? "w-10 h-10 hover:bg-muted text-muted-foreground hover:text-foreground"
: "flex-1 h-9 gap-2 px-3 bg-muted/50 hover:bg-muted text-muted-foreground hover:text-foreground text-xs font-medium"
? "w-10 h-9 hover:bg-muted text-muted-foreground hover:text-foreground"
: "flex-1 h-8 gap-1.5 px-2.5 hover:bg-muted/70 text-muted-foreground hover:text-foreground text-[11px] font-medium"
)}
>
<Github className="h-4 w-4" />
<Github className="h-3.5 w-3.5" />
{!collapsed && <span>GitHub</span>}
</a>
</div>
@@ -301,27 +271,26 @@ export function Sidebar({
type="button"
title={collapsed ? (isAuthenticated ? '退出登录' : '管理员登录') : undefined}
className={cn(
'flex items-center justify-center gap-2 w-full rounded-lg text-sm font-medium transition-all duration-200',
collapsed ? 'h-10' : 'h-10 px-3',
'flex items-center justify-center gap-2 w-full rounded-lg text-[13px] font-medium transition-all duration-150',
collapsed ? 'h-9' : 'h-9 px-3',
isAuthenticated
? 'bg-destructive/10 text-destructive hover:bg-destructive/20'
: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm'
? 'bg-destructive/8 text-destructive hover:bg-destructive/15'
: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm shadow-primary/15'
)}
onClick={handleAuthClick}
>
{isAuthenticated ? (
<>
<LogOut className="h-4 w-4" />
<LogOut className="h-3.5 w-3.5" />
{!collapsed && <span>退出登录</span>}
</>
) : (
<>
<LogIn className="h-4 w-4" />
<LogIn className="h-3.5 w-3.5" />
{!collapsed && <span>管理员登录</span>}
</>
)}
</button>
</div>
</aside>
)
+2 -2
View File
@@ -18,7 +18,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"fixed inset-0 z-50 bg-black/60 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
@@ -35,7 +35,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-2xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-xl",
className
)}
{...props}
+132 -95
View File
@@ -1,105 +1,100 @@
@import 'tailwindcss';
@theme {
/* 现代科技简约配色 - 深邃蓝主题 */
--color-background: hsl(210 20% 98%);
--color-foreground: hsl(222 47% 11%);
/* Refined color system - Deep indigo theme */
--color-background: hsl(220 20% 97%);
--color-foreground: hsl(224 71% 4%);
--color-card: hsl(0 0% 100%);
--color-card-foreground: hsl(222 47% 11%);
--color-card-foreground: hsl(224 71% 4%);
--color-popover: hsl(0 0% 100%);
--color-popover-foreground: hsl(222 47% 11%);
--color-popover-foreground: hsl(224 71% 4%);
/* 主色调 - 靛蓝色 */
--color-primary: hsl(226 70% 55%);
/* Primary - Vibrant indigo */
--color-primary: hsl(234 89% 60%);
--color-primary-foreground: hsl(0 0% 100%);
/* 次要色 - 柔和灰蓝 */
/* Secondary */
--color-secondary: hsl(220 14% 96%);
--color-secondary-foreground: hsl(222 47% 11%);
--color-secondary-foreground: hsl(224 71% 4%);
/* 静音色 */
/* Muted */
--color-muted: hsl(220 14% 96%);
--color-muted-foreground: hsl(220 9% 46%);
/* 强调色 */
/* Accent */
--color-accent: hsl(220 14% 96%);
--color-accent-foreground: hsl(222 47% 11%);
--color-accent-foreground: hsl(224 71% 4%);
/* 危险色 - 柔和红 */
--color-destructive: hsl(0 72% 51%);
/* Destructive */
--color-destructive: hsl(0 84% 60%);
--color-destructive-foreground: hsl(0 0% 100%);
/* 成功色 - 翡翠绿 */
--color-success: hsl(160 84% 39%);
/* Success */
--color-success: hsl(142 71% 45%);
--color-success-foreground: hsl(0 0% 100%);
/* 警告色 - 琥珀黄 */
/* Warning */
--color-warning: hsl(38 92% 50%);
--color-warning-foreground: hsl(0 0% 100%);
/* 信息色 - 天蓝 */
/* Info */
--color-info: hsl(199 89% 48%);
--color-info-foreground: hsl(0 0% 100%);
/* 边框和输入框 */
/* Border & Input */
--color-border: hsl(220 13% 91%);
--color-input: hsl(220 13% 91%);
--color-ring: hsl(226 70% 55%);
--color-ring: hsl(234 89% 60%);
/* 侧边栏专用色 */
--color-sidebar: hsl(222 47% 11%);
/* Sidebar */
--color-sidebar: hsl(224 71% 4%);
--color-sidebar-foreground: hsl(210 20% 98%);
--color-sidebar-muted: hsl(220 17% 17%);
--color-sidebar-accent: hsl(226 70% 55%);
--color-sidebar-accent: hsl(234 89% 60%);
--radius: 0.625rem;
}
.dark {
--color-background: hsl(222 47% 6%);
--color-background: hsl(224 71% 4%);
--color-foreground: hsl(210 20% 98%);
--color-card: hsl(222 47% 9%);
--color-card: hsl(224 50% 8%);
--color-card-foreground: hsl(210 20% 98%);
--color-popover: hsl(222 47% 9%);
--color-popover: hsl(224 50% 8%);
--color-popover-foreground: hsl(210 20% 98%);
/* 主色调 - 深色模式下的靛蓝 */
--color-primary: hsl(226 76% 60%);
--color-primary: hsl(234 89% 67%);
--color-primary-foreground: hsl(0 0% 100%);
--color-secondary: hsl(222 30% 14%);
--color-secondary: hsl(224 40% 12%);
--color-secondary-foreground: hsl(210 20% 98%);
--color-muted: hsl(222 30% 14%);
--color-muted-foreground: hsl(220 9% 60%);
--color-muted: hsl(224 40% 12%);
--color-muted-foreground: hsl(220 9% 56%);
--color-accent: hsl(222 30% 14%);
--color-accent: hsl(224 40% 12%);
--color-accent-foreground: hsl(210 20% 98%);
--color-destructive: hsl(0 62% 50%);
--color-destructive: hsl(0 72% 51%);
--color-destructive-foreground: hsl(0 0% 98%);
/* 成功色 - 深色模式 */
--color-success: hsl(160 84% 35%);
--color-success: hsl(142 71% 40%);
--color-success-foreground: hsl(0 0% 98%);
/* 警告色 - 深色模式 */
--color-warning: hsl(38 92% 45%);
--color-warning-foreground: hsl(0 0% 98%);
/* 信息色 - 深色模式 */
--color-info: hsl(199 89% 42%);
--color-info-foreground: hsl(0 0% 98%);
--color-border: hsl(222 30% 18%);
--color-input: hsl(222 30% 18%);
--color-ring: hsl(226 76% 60%);
--color-border: hsl(224 30% 16%);
--color-input: hsl(224 30% 16%);
--color-ring: hsl(234 89% 67%);
/* 侧边栏专用色 - 深色模式 */
--color-sidebar: hsl(222 47% 8%);
--color-sidebar: hsl(224 71% 3%);
--color-sidebar-foreground: hsl(210 20% 98%);
--color-sidebar-muted: hsl(222 30% 12%);
--color-sidebar-accent: hsl(226 76% 60%);
--color-sidebar-muted: hsl(224 30% 10%);
--color-sidebar-accent: hsl(234 89% 67%);
}
@layer base {
@@ -115,7 +110,7 @@
body {
@apply bg-background text-foreground antialiased;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
a,
@@ -125,29 +120,22 @@
cursor: pointer;
}
a:active,
button:active,
[role='button']:active {
opacity: 0.85;
}
/* 优化标题字重 */
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
letter-spacing: -0.02em;
letter-spacing: -0.025em;
}
}
/* 自定义滚动条 */
/* Scrollbar */
@layer utilities {
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: hsl(220 9% 46% / 0.3) transparent;
scrollbar-color: hsl(220 9% 46% / 0.2) transparent;
}
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
height: 6px;
width: 5px;
height: 5px;
}
.scrollbar-thin::-webkit-scrollbar-track {
@@ -155,67 +143,48 @@
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background: hsl(220 9% 46% / 0.3);
background: hsl(220 9% 46% / 0.2);
border-radius: 3px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: hsl(220 9% 46% / 0.5);
background: hsl(220 9% 46% / 0.4);
}
/* 玻璃效果 */
/* Glass morphism */
.glass {
backdrop-filter: blur(12px) saturate(180%);
background: hsl(0 0% 100% / 0.8);
backdrop-filter: blur(16px) saturate(180%);
background: hsl(0 0% 100% / 0.75);
}
.dark .glass {
background: hsl(222 47% 9% / 0.8);
background: hsl(224 50% 8% / 0.75);
}
/* 渐变边框效果 */
.gradient-border {
position: relative;
}
.gradient-border::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1px;
background: linear-gradient(135deg, hsl(226 70% 55% / 0.3), hsl(160 84% 39% / 0.3));
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
/* 柔和阴影 */
/* Soft shadow */
.shadow-soft {
box-shadow:
0 0 0 1px hsl(220 9% 46% / 0.04),
0 1px 2px hsl(220 9% 46% / 0.04),
0 4px 8px hsl(220 9% 46% / 0.04),
0 8px 16px hsl(220 9% 46% / 0.04);
0 4px 8px -2px hsl(220 9% 46% / 0.06);
}
.dark .shadow-soft {
box-shadow:
0 0 0 1px hsl(0 0% 100% / 0.03),
0 1px 2px hsl(0 0% 0% / 0.2),
0 4px 8px hsl(0 0% 0% / 0.2),
0 8px 16px hsl(0 0% 0% / 0.2);
0 4px 8px -2px hsl(0 0% 0% / 0.25);
}
/* 动画效果 */
/* Animate in */
.animate-in {
animation: animate-in 0.2s ease-out;
animation: animate-in 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes animate-in {
from {
opacity: 0;
transform: translateY(4px);
transform: translateY(6px);
}
to {
opacity: 1;
@@ -223,21 +192,89 @@
}
}
/* 悬停提升效果 */
/* Staggered animation children */
.stagger-children > * {
animation: animate-in 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.stagger-children > *:nth-child(1) { animation-delay: 0ms; }
.stagger-children > *:nth-child(2) { animation-delay: 50ms; }
.stagger-children > *:nth-child(3) { animation-delay: 100ms; }
.stagger-children > *:nth-child(4) { animation-delay: 150ms; }
.stagger-children > *:nth-child(5) { animation-delay: 200ms; }
.stagger-children > *:nth-child(6) { animation-delay: 250ms; }
/* Hover lift */
.hover-lift {
transition: transform 0.2s ease, box-shadow 0.2s ease;
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease;
}
.hover-lift:hover {
transform: translateY(-2px);
box-shadow:
0 4px 12px hsl(220 9% 46% / 0.1),
0 8px 24px hsl(220 9% 46% / 0.1);
0 0 0 1px hsl(220 9% 46% / 0.04),
0 4px 12px hsl(220 9% 46% / 0.08),
0 12px 24px -4px hsl(220 9% 46% / 0.08);
}
.dark .hover-lift:hover {
box-shadow:
0 0 0 1px hsl(0 0% 100% / 0.03),
0 4px 12px hsl(0 0% 0% / 0.3),
0 8px 24px hsl(0 0% 0% / 0.3);
0 12px 24px -4px hsl(0 0% 0% / 0.3);
}
/* Gradient mesh background for hero */
.gradient-mesh {
background:
radial-gradient(ellipse at 20% 50%, hsl(234 89% 60% / 0.08) 0%, transparent 50%),
radial-gradient(ellipse at 80% 20%, hsl(280 80% 60% / 0.06) 0%, transparent 50%),
radial-gradient(ellipse at 40% 80%, hsl(142 71% 45% / 0.05) 0%, transparent 50%);
}
.dark .gradient-mesh {
background:
radial-gradient(ellipse at 20% 50%, hsl(234 89% 60% / 0.12) 0%, transparent 50%),
radial-gradient(ellipse at 80% 20%, hsl(280 80% 60% / 0.08) 0%, transparent 50%),
radial-gradient(ellipse at 40% 80%, hsl(142 71% 45% / 0.06) 0%, transparent 50%);
}
/* Shimmer effect */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.shimmer {
background: linear-gradient(
90deg,
transparent 0%,
hsl(234 89% 60% / 0.06) 50%,
transparent 100%
);
background-size: 200% 100%;
animation: shimmer 3s ease-in-out infinite;
}
/* Pulse glow */
@keyframes pulse-glow {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.8; }
}
.pulse-glow {
animation: pulse-glow 3s ease-in-out infinite;
}
/* Grid pattern for backgrounds */
.grid-pattern {
background-image: linear-gradient(hsl(220 9% 46% / 0.03) 1px, transparent 1px),
linear-gradient(90deg, hsl(220 9% 46% / 0.03) 1px, transparent 1px);
background-size: 32px 32px;
}
.dark .grid-pattern {
background-image: linear-gradient(hsl(0 0% 100% / 0.02) 1px, transparent 1px),
linear-gradient(90deg, hsl(0 0% 100% / 0.02) 1px, transparent 1px);
background-size: 32px 32px;
}
}
+112 -61
View File
@@ -1,5 +1,5 @@
import { useState } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
@@ -8,8 +8,9 @@ import { Select } from '@/components/ui/select'
import { Badge } from '@/components/ui/badge'
import { useToast } from '@/components/ui/use-toast'
import { apiClient } from '@/api/client'
import { Send } from 'lucide-react'
import { Send, Clock, CheckCircle, XCircle, Copy } from 'lucide-react'
import { PageContainer } from '@/components/ui/page-container'
import { cn, copyToClipboard } from '@/lib/utils'
const requestTemplates: Record<string, any> = {
'/v1/chat/completions': {
@@ -63,19 +64,30 @@ export function ApiTest() {
setRequestBody(JSON.stringify(requestTemplates[newEndpoint], null, 2))
}
const handleCopyResponse = async () => {
if (response) {
try {
await copyToClipboard(JSON.stringify(response, null, 2))
addToast('已复制到剪贴板', 'success')
} catch {
addToast('复制失败', 'error')
}
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!apiToken) {
addToast('请输入API令牌', 'error')
addToast('请输入 API 令牌', 'error')
return
}
let body: any
try {
body = JSON.parse(requestBody)
} catch (error) {
addToast('请求体JSON格式错误', 'error')
} catch {
addToast('请求体 JSON 格式错误', 'error')
return
}
@@ -91,32 +103,34 @@ export function ApiTest() {
setResponseTime(endTime - startTime)
setResponse(result)
setStatusCode(200)
addToast('请求成功', 'success')
} catch (error: any) {
const endTime = Date.now()
setResponseTime(endTime - startTime)
setStatusCode(error.status || 500)
setResponse({ error: error.message })
addToast('请求失败', 'error')
} finally {
setIsLoading(false)
}
}
return (
<PageContainer title="API 测试">
<Card>
<CardHeader>
<CardTitle>API 测试工具</CardTitle>
<CardDescription>测试您的 API 连接和配置</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<PageContainer
title="API 测试"
description="测试 API 连接和配置"
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Request Panel */}
<Card>
<CardContent className="p-5">
<div className="flex items-center gap-2 mb-5">
<div className="w-2 h-2 rounded-full bg-primary" />
<h3 className="font-semibold text-sm">请求</h3>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="endpoint">API 端点</Label>
<Label className="text-xs font-medium text-muted-foreground uppercase tracking-wider">端点</Label>
<Select
id="endpoint"
value={endpoint}
onChange={(e) => handleEndpointChange(e.target.value)}
>
@@ -128,59 +142,96 @@ export function ApiTest() {
</div>
<div className="space-y-2">
<Label htmlFor="apiToken">API 令牌</Label>
<Label className="text-xs font-medium text-muted-foreground uppercase tracking-wider">API 令牌</Label>
<Input
id="apiToken"
type="password"
value={apiToken}
onChange={(e) => setApiToken(e.target.value)}
placeholder="sk-..."
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="requestBody">请求体 (JSON)</Label>
<Textarea
id="requestBody"
value={requestBody}
onChange={(e) => setRequestBody(e.target.value)}
rows={12}
className="font-mono text-sm"
placeholder='{"model": "gpt-3.5-turbo", "messages": [...]}'
/>
<p className="text-sm text-muted-foreground">直接编辑 JSON 格式的请求体</p>
</div>
<Button type="submit" disabled={isLoading} className="w-full sm:w-auto">
<Send className="h-4 w-4" />
{isLoading ? '发送中...' : '发送请求'}
</Button>
</form>
{response && (
<div className="mt-6 space-y-3">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<h3 className="font-semibold">响应结果</h3>
<div className="flex gap-2">
<Badge variant={statusCode === 200 ? 'success' : 'destructive'}>
{statusCode === 200 ? '成功' : '失败'}
</Badge>
<Badge variant="outline">{responseTime}ms</Badge>
</div>
<div className="space-y-2">
<Label className="text-xs font-medium text-muted-foreground uppercase tracking-wider">请求体</Label>
<Textarea
value={requestBody}
onChange={(e) => setRequestBody(e.target.value)}
rows={14}
className="font-mono text-sm"
/>
</div>
<Card>
<CardContent className="p-4">
<pre className="whitespace-pre-wrap break-words text-sm font-mono max-h-96 overflow-y-auto">
{JSON.stringify(response, null, 2)}
</pre>
</CardContent>
</Card>
<Button type="submit" disabled={isLoading} className="w-full">
{isLoading ? (
<>
<div className="w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin" />
发送中...
</>
) : (
<>
<Send className="h-4 w-4" />
发送请求
</>
)}
</Button>
</form>
</CardContent>
</Card>
{/* Response Panel */}
<Card className={cn(
"transition-all duration-300",
!response && "opacity-60"
)}>
<CardContent className="p-5">
<div className="flex items-center justify-between mb-5">
<div className="flex items-center gap-2">
<div className={cn(
"w-2 h-2 rounded-full",
!statusCode ? "bg-muted-foreground/30" :
statusCode === 200 ? "bg-success" : "bg-destructive"
)} />
<h3 className="font-semibold text-sm">响应</h3>
</div>
{statusCode && (
<div className="flex items-center gap-2">
<Badge variant={statusCode === 200 ? 'success' : 'destructive'} className="text-xs">
{statusCode === 200 ? (
<><CheckCircle className="h-3 w-3 mr-1" />{statusCode}</>
) : (
<><XCircle className="h-3 w-3 mr-1" />{statusCode}</>
)}
</Badge>
<Badge variant="outline" className="text-xs font-mono">
<Clock className="h-3 w-3 mr-1" />
{responseTime}ms
</Badge>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={handleCopyResponse}
title="复制响应"
>
<Copy className="h-3.5 w-3.5" />
</Button>
</div>
)}
</div>
)}
</CardContent>
</Card>
{response ? (
<pre className="whitespace-pre-wrap break-words text-sm font-mono bg-muted/50 rounded-lg p-4 max-h-[500px] overflow-y-auto scrollbar-thin">
{JSON.stringify(response, null, 2)}
</pre>
) : (
<div className="flex flex-col items-center justify-center h-[400px] text-muted-foreground/40">
<Send className="h-10 w-10 mb-3" />
<p className="text-sm">发送请求后在此查看响应</p>
</div>
)}
</CardContent>
</Card>
</div>
</PageContainer>
)
}
+96 -63
View File
@@ -1,6 +1,5 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Globe, Gauge, BarChart3, Shield, Zap, ArrowRight } from 'lucide-react'
import { PageContainer } from '@/components/ui/page-container'
import { Card, CardContent } from '@/components/ui/card'
import { Globe, Gauge, BarChart3, Shield, Zap, ArrowRight, Sparkles } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { useAuthStore } from '@/store/auth'
import { Link } from 'react-router-dom'
@@ -9,100 +8,122 @@ const features = [
{
icon: Globe,
title: '多提供商支持',
description: '支持 OpenAI、Azure OpenAI 和 Claude API,轻松切换不同的 AI 服务。',
color: 'from-blue-500 to-blue-600',
description: '支持 OpenAI、Azure OpenAI、Claude 等多种 AI 服务提供商。',
gradient: 'from-blue-500/10 to-cyan-500/10',
iconBg: 'from-blue-500 to-cyan-500',
delay: '0ms',
},
{
icon: Gauge,
title: '负载均衡',
description: '自动在多个频道间分配请求,确保服务稳定性和高可用性。',
color: 'from-emerald-500 to-emerald-600',
description: '自动在多个频道间分配请求,确保高可用性。',
gradient: 'from-emerald-500/10 to-teal-500/10',
iconBg: 'from-emerald-500 to-teal-500',
delay: '50ms',
},
{
icon: BarChart3,
title: '用量追踪',
description: '实时监控 token 使用情况,精确计算成本,防止超出配额。',
color: 'from-amber-500 to-amber-600',
description: '实时监控 token 使用情况,精确成本核算。',
gradient: 'from-amber-500/10 to-orange-500/10',
iconBg: 'from-amber-500 to-orange-500',
delay: '100ms',
},
{
icon: Shield,
title: '安全管理',
description: '基于令牌的访问控制,为不同用户分配独立的 API 密钥。',
color: 'from-purple-500 to-purple-600',
description: '基于令牌的访问控制与独立 API 密钥分配。',
gradient: 'from-violet-500/10 to-purple-500/10',
iconBg: 'from-violet-500 to-purple-500',
delay: '150ms',
},
]
const steps = [
{ num: '01', title: '管理员登录', desc: '使用管理员令牌登录系统' },
{ num: '02', title: '配置频道', desc: '添加 OpenAI、Azure 或 Claude API 频道' },
{ num: '03', title: '创建令牌', desc: '为您的应用创建 API 令牌' },
{ num: '04', title: '开始使用', desc: '使用生成的令牌调用 API' },
{ num: '01', title: '管理员登录', desc: '使用管理员令牌登录系统', color: 'text-blue-500' },
{ num: '02', title: '配置频道', desc: '添加 AI 服务提供商频道', color: 'text-emerald-500' },
{ num: '03', title: '创建令牌', desc: '为应用创建 API 令牌', color: 'text-amber-500' },
{ num: '04', title: '开始使用', desc: '使用令牌调用 API', color: 'text-violet-500' },
]
export function Dashboard() {
const { isAuthenticated, openAuthModal } = useAuthStore()
return (
<div className="p-4 md:p-6 lg:p-8 animate-in">
<div className="space-y-8">
{/* Hero Section */}
<Card className="overflow-hidden border-0 bg-gradient-to-br from-primary/10 via-background to-background">
<CardContent className="p-8 md:p-12">
<div className="max-w-2xl">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 text-primary text-sm font-medium mb-4">
<Zap className="h-4 w-4" />
统一 AI 接口网关
<div className="animate-in">
{/* Hero Section */}
<div className="relative overflow-hidden">
<div className="gradient-mesh grid-pattern">
<div className="px-4 md:px-6 lg:px-8 py-12 md:py-16 lg:py-20">
<div className="max-w-3xl">
<div className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-primary/8 border border-primary/10 text-primary text-sm font-medium mb-6">
<Sparkles className="h-3.5 w-3.5" />
<span>Unified AI Gateway</span>
</div>
<h1 className="text-3xl md:text-4xl font-bold tracking-tight mb-4">
欢迎使用 Awsl One API
<h1 className="text-4xl md:text-5xl lg:text-[3.5rem] font-bold tracking-tight leading-[1.1] mb-5">
<span className="bg-gradient-to-r from-foreground via-foreground to-foreground/70 bg-clip-text text-transparent">
Awsl One API
</span>
</h1>
<p className="text-lg text-muted-foreground mb-6">
一个强大的 API 代理和管理平台,为您提供统一的 AI 模型接口管理服务。
支持多种 AI 服务提供商,实现智能负载均衡和精确用量追踪。
<p className="text-lg md:text-xl text-muted-foreground leading-relaxed mb-8 max-w-2xl">
统一的 AI 模型接口网关。支持多服务商接入、智能负载均衡与精确用量追踪。
</p>
<div className="flex flex-wrap gap-3">
{isAuthenticated ? (
<>
<Button asChild>
<Button size="lg" asChild className="h-12 px-6 text-[15px] rounded-xl shadow-lg shadow-primary/20">
<Link to="/channels">
管理频道
<ArrowRight className="h-4 w-4" />
<ArrowRight className="h-4 w-4 ml-1" />
</Link>
</Button>
<Button variant="outline" asChild>
<Button variant="outline" size="lg" asChild className="h-12 px-6 text-[15px] rounded-xl">
<Link to="/api-test">测试 API</Link>
</Button>
</>
) : (
<>
<Button onClick={openAuthModal}>
<Button size="lg" onClick={openAuthModal} className="h-12 px-6 text-[15px] rounded-xl shadow-lg shadow-primary/20">
管理员登录
<ArrowRight className="h-4 w-4" />
<ArrowRight className="h-4 w-4 ml-1" />
</Button>
<Button variant="outline" asChild>
<Button variant="outline" size="lg" asChild className="h-12 px-6 text-[15px] rounded-xl">
<Link to="/api-test">体验 API</Link>
</Button>
</>
)}
</div>
</div>
</CardContent>
</Card>
</div>
</div>
{/* Decorative elements */}
<div className="absolute top-12 right-8 w-72 h-72 bg-primary/5 rounded-full blur-3xl pointer-events-none" />
<div className="absolute bottom-0 right-1/4 w-48 h-48 bg-violet-500/5 rounded-full blur-3xl pointer-events-none" />
</div>
<div className="px-4 md:px-6 lg:px-8 pb-8 space-y-10">
{/* Features Grid */}
<div>
<h2 className="text-xl font-semibold mb-4">核心特性</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="flex items-center gap-3 mb-6">
<div className="h-px flex-1 bg-border" />
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">核心特性</h2>
<div className="h-px flex-1 bg-border" />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 stagger-children">
{features.map((feature) => (
<Card key={feature.title} className="group hover-lift">
<CardContent className="p-6">
<div className={`w-10 h-10 rounded-xl bg-gradient-to-br ${feature.color} flex items-center justify-center mb-4 shadow-lg group-hover:scale-110 transition-transform duration-200`}>
<feature.icon className="h-5 w-5 text-white" />
<Card key={feature.title} className="group hover-lift border-0 bg-gradient-to-br from-card to-card overflow-hidden">
<CardContent className="p-6 relative">
<div className={`absolute inset-0 bg-gradient-to-br ${feature.gradient} opacity-0 group-hover:opacity-100 transition-opacity duration-300`} />
<div className="relative">
<div className={`w-11 h-11 rounded-xl bg-gradient-to-br ${feature.iconBg} flex items-center justify-center mb-4 shadow-lg`}>
<feature.icon className="h-5 w-5 text-white" />
</div>
<h3 className="font-semibold mb-1.5 text-[15px]">{feature.title}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
{feature.description}
</p>
</div>
<h3 className="font-semibold mb-2">{feature.title}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
{feature.description}
</p>
</CardContent>
</Card>
))}
@@ -110,25 +131,24 @@ export function Dashboard() {
</div>
{/* Quick Start */}
<Card>
<CardHeader>
<CardTitle>快速开始</CardTitle>
<CardDescription>按照以下步骤开始使用</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{steps.map((step, index) => (
<div key={step.num} className="relative">
{index < steps.length - 1 && (
<div className="hidden lg:block absolute top-6 left-full w-full h-px bg-border -translate-x-4" />
)}
<Card className="overflow-hidden">
<CardContent className="p-0">
<div className="p-6 pb-2">
<h2 className="text-lg font-semibold mb-1">快速开始</h2>
<p className="text-sm text-muted-foreground">四步完成配置,开始使用统一接口</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 divide-y sm:divide-y-0 sm:divide-x divide-border">
{steps.map((step) => (
<div key={step.num} className="p-6 group">
<div className="flex items-start gap-4">
<div className="flex-shrink-0 w-12 h-12 rounded-xl bg-muted flex items-center justify-center">
<span className="text-lg font-bold text-muted-foreground">{step.num}</span>
<div className="flex-shrink-0">
<span className={`text-2xl font-bold ${step.color} opacity-60 group-hover:opacity-100 transition-opacity`}>
{step.num}
</span>
</div>
<div>
<h4 className="font-semibold mb-1">{step.title}</h4>
<p className="text-sm text-muted-foreground">{step.desc}</p>
<h4 className="font-semibold mb-1 text-sm">{step.title}</h4>
<p className="text-xs text-muted-foreground leading-relaxed">{step.desc}</p>
</div>
</div>
</div>
@@ -136,6 +156,19 @@ export function Dashboard() {
</div>
</CardContent>
</Card>
{/* Supported Providers */}
<div className="text-center py-4">
<p className="text-xs text-muted-foreground/60 uppercase tracking-wider mb-4">支持的服务提供商</p>
<div className="flex items-center justify-center gap-6 flex-wrap text-muted-foreground/40">
{['OpenAI', 'Azure OpenAI', 'Claude', 'OpenAI Responses'].map((name) => (
<div key={name} className="flex items-center gap-2 text-sm font-medium hover:text-muted-foreground transition-colors">
<Zap className="h-3.5 w-3.5" />
{name}
</div>
))}
</div>
</div>
</div>
</div>
)
+71 -36
View File
@@ -1,10 +1,11 @@
import { useState } from 'react'
import { useMutation } from '@tanstack/react-query'
import { apiClient } from '@/api/client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { useToast } from '@/components/ui/use-toast'
import { Database as DatabaseIcon, Info } from 'lucide-react'
import { Database as DatabaseIcon, Info, CheckCircle, AlertTriangle, RefreshCw } from 'lucide-react'
import { PageContainer } from '@/components/ui/page-container'
export function Database() {
const [result, setResult] = useState<any>(null)
@@ -31,40 +32,74 @@ export function Database() {
}
return (
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold tracking-tight">数据库管理</h1>
</div>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>数据库初始化</CardTitle>
<Button onClick={handleInit} disabled={initMutation.isPending}>
<DatabaseIcon className="h-4 w-4" />
{initMutation.isPending ? '初始化中...' : '初始化数据库'}
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Info className="h-4 w-4" />
<span>初始化数据库将创建必要的表结构。如果表已存在,操作将被忽略。</span>
</div>
{result && (
<div className="mt-4">
<Card>
<CardContent className="pt-6">
<pre className="whitespace-pre-wrap text-sm font-mono">
{JSON.stringify(result, null, 2)}
</pre>
</CardContent>
</Card>
<PageContainer
title="数据库管理"
description="管理数据库结构和初始化"
>
<div className="space-y-4">
{/* Init Card */}
<Card>
<CardContent className="p-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-start gap-4">
<div className="w-11 h-11 rounded-xl bg-gradient-to-br from-primary/10 to-primary/5 flex items-center justify-center flex-shrink-0">
<DatabaseIcon className="h-5 w-5 text-primary" />
</div>
<div>
<h3 className="font-semibold mb-1">数据库初始化</h3>
<p className="text-sm text-muted-foreground">
创建必要的表结构。如果表已存在,操作将被忽略。
</p>
</div>
</div>
<Button
onClick={handleInit}
disabled={initMutation.isPending}
className="flex-shrink-0"
>
{initMutation.isPending ? (
<>
<RefreshCw className="h-4 w-4 animate-spin" />
初始化中...
</>
) : (
<>
<DatabaseIcon className="h-4 w-4" />
初始化数据库
</>
)}
</Button>
</div>
)}
</CardContent>
</Card>
</div>
</CardContent>
</Card>
{/* Info */}
<div className="px-4 py-3 rounded-xl bg-muted/50 text-sm text-muted-foreground flex items-start gap-2.5">
<Info className="h-4 w-4 mt-0.5 flex-shrink-0 text-info" />
<span>数据库使用 Cloudflare D1,初始化操作幂等安全,可重复执行。</span>
</div>
{/* Result */}
{result && (
<Card>
<CardContent className="p-6">
<div className="flex items-center gap-2 mb-4">
{result.error ? (
<AlertTriangle className="h-4 w-4 text-destructive" />
) : (
<CheckCircle className="h-4 w-4 text-success" />
)}
<h3 className="font-semibold text-sm">
{result.error ? '执行出错' : '执行结果'}
</h3>
</div>
<pre className="whitespace-pre-wrap text-sm font-mono bg-muted/50 rounded-lg p-4 overflow-x-auto">
{JSON.stringify(result, null, 2)}
</pre>
</CardContent>
</Card>
)}
</div>
</PageContainer>
)
}
+26
View File
@@ -0,0 +1,26 @@
import { Button } from '@/components/ui/button'
import { Link } from 'react-router-dom'
import { ArrowLeft, Zap } from 'lucide-react'
export function NotFound() {
return (
<div className="flex flex-col items-center justify-center min-h-[60vh] px-4 animate-in">
<div className="relative mb-8">
<div className="w-24 h-24 rounded-3xl bg-gradient-to-br from-primary/10 to-violet-500/10 flex items-center justify-center">
<Zap className="h-10 w-10 text-primary/40" />
</div>
<div className="absolute -inset-4 bg-primary/5 rounded-full blur-2xl" />
</div>
<h1 className="text-6xl font-bold text-muted-foreground/20 mb-2">404</h1>
<p className="text-lg text-muted-foreground mb-8">
页面不存在
</p>
<Button variant="outline" size="lg" asChild className="rounded-xl">
<Link to="/">
<ArrowLeft className="h-4 w-4 mr-2" />
返回首页
</Link>
</Button>
</div>
)
}