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:
1 parent
6f99358fb5
commit
07a597e41a
9 files changed
+503
-349
No files matched your search
@@ -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 />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user