From 6a648f3fa71e3df7973bd1398b8a02612fd995ce Mon Sep 17 00:00:00 2001 From: dreamhunter2333 Date: Fri, 30 Jan 2026 14:11:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=89=8D=E7=AB=AF=20?= =?UTF-8?q?UI=20=E8=AE=BE=E8=AE=A1=E5=92=8C=E7=94=A8=E6=88=B7=E4=BD=93?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构所有 UI 组件,采用现代化设计风格 - 新增 PageContainer 组件统一页面布局 - 优化 Sidebar 支持折叠功能 - 改进 Toast 通知样式和交互 - 统一所有页面的视觉风格和间距 - 提升整体界面的专业性和易用性 Co-Authored-By: Claude Opus 4.5 --- frontend/src/components/layout/AppLayout.tsx | 58 +- frontend/src/components/layout/Sidebar.tsx | 301 +++++--- frontend/src/components/ui/alert.tsx | 4 +- frontend/src/components/ui/badge.tsx | 4 +- frontend/src/components/ui/button.tsx | 15 +- frontend/src/components/ui/card.tsx | 2 +- frontend/src/components/ui/checkbox.tsx | 2 +- frontend/src/components/ui/input.tsx | 2 +- frontend/src/components/ui/page-container.tsx | 33 + frontend/src/components/ui/select.tsx | 2 +- frontend/src/components/ui/table.tsx | 2 +- frontend/src/components/ui/textarea.tsx | 2 +- frontend/src/components/ui/toaster.tsx | 48 +- frontend/src/components/ui/use-toast.ts | 4 +- frontend/src/index.css | 230 +++++-- frontend/src/pages/ApiTest.tsx | 17 +- frontend/src/pages/Channels.tsx | 627 ++++++++++------- frontend/src/pages/Dashboard.tsx | 232 ++++--- frontend/src/pages/Pricing.tsx | 304 +++++---- frontend/src/pages/Tokens.tsx | 644 +++++++++++------- 20 files changed, 1586 insertions(+), 947 deletions(-) create mode 100644 frontend/src/components/ui/page-container.tsx diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx index 5999d7c..1e0dd16 100644 --- a/frontend/src/components/layout/AppLayout.tsx +++ b/frontend/src/components/layout/AppLayout.tsx @@ -14,6 +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' interface AppLayoutProps { children: ReactNode @@ -23,6 +24,8 @@ export function AppLayout({ children }: AppLayoutProps) { const [adminToken, setAdminToken] = useState('') const [authError, setAuthError] = useState('') const [isLoading, setIsLoading] = useState(false) + const [isMobileNavOpen, setIsMobileNavOpen] = useState(false) + const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false) const { login, showAuthModal, closeAuthModal, openAuthModal } = useAuthStore() const { addToast } = useToast() @@ -44,11 +47,58 @@ export function AppLayout({ children }: AppLayoutProps) { return (
- + setIsSidebarCollapsed((prev) => !prev)} + showCollapseToggle + /> -
- {children} -
+
+
+ +
+
+ +
+ Awsl One API +
+
+
+ +
+
+ {children} +
+
+
+ + {isMobileNavOpen && ( +
+
setIsMobileNavOpen(false)} + /> +
+ setIsMobileNavOpen(false)} + onClose={() => setIsMobileNavOpen(false)} + collapsed={false} + showCollapseToggle={false} + /> +
+
+ )} {/* Auth Dialog */} !open && closeAuthModal()}> diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 046bd98..7294d46 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -12,10 +12,13 @@ import { Sun, LogIn, LogOut, + X, + PanelLeftClose, + PanelLeftOpen, + Zap, } from 'lucide-react' import { useAuthStore } from '@/store/auth' import { useState } from 'react' -import { Button } from '../ui/button' interface NavItem { title: string @@ -63,12 +66,25 @@ const navItems: NavItem[] = [ interface SidebarProps { onAuthClick: () => void + className?: string + onNavigate?: () => void + onClose?: () => void + collapsed?: boolean + onToggleCollapse?: () => void + showCollapseToggle?: boolean } -export function Sidebar({ onAuthClick }: SidebarProps) { +export function Sidebar({ + onAuthClick, + className, + onNavigate, + onClose, + collapsed = false, + onToggleCollapse, + showCollapseToggle = false, +}: SidebarProps) { const location = useLocation() const { isAuthenticated, logout } = useAuthStore() - // Initialize theme from localStorage on mount const [theme, setTheme] = useState<'light' | 'dark'>(() => { if (typeof window !== 'undefined') { const savedTheme = localStorage.getItem('theme') @@ -98,147 +114,214 @@ export function Sidebar({ onAuthClick }: SidebarProps) { } else { onAuthClick() } + onNavigate?.() + } + + const NavLink = ({ item }: { item: NavItem }) => { + const isActive = location.pathname === item.href + return ( + 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', + 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' + )} + > + + {!collapsed && {item.title}} + + ) } return ( -
- @@ -164,7 +161,7 @@ export function ApiTest() { {response && (
-
+

响应结果

@@ -174,7 +171,7 @@ export function ApiTest() {
- +
                     {JSON.stringify(response, null, 2)}
                   
@@ -184,6 +181,6 @@ export function ApiTest() { )}
-
+ ) } diff --git a/frontend/src/pages/Channels.tsx b/frontend/src/pages/Channels.tsx index 80a4060..b248652 100644 --- a/frontend/src/pages/Channels.tsx +++ b/frontend/src/pages/Channels.tsx @@ -1,26 +1,43 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { apiClient } from '@/api/client' import { Channel, ChannelConfig } from '@/types' -import { Card, CardContent, CardHeader } 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' import { Select } from '@/components/ui/select' import { Textarea } from '@/components/ui/textarea' import { useToast } from '@/components/ui/use-toast' +import { cn } from '@/lib/utils' import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table' -import { Plus, RefreshCw, Pencil, Trash2, FileJson, FileText } from 'lucide-react' + Plus, + RefreshCw, + Pencil, + Trash2, + FileJson, + FileText, + Link as LinkIcon, + ArrowLeft, + Check, + MoreHorizontal, + Search, + ArrowRight, + Globe, + Cpu, +} from 'lucide-react' +import { PageContainer } from '@/components/ui/page-container' type EditMode = 'form' | 'json' +const channelTypes = [ + { value: 'azure-openai', label: 'Azure OpenAI' }, + { value: 'openai', label: 'OpenAI' }, + { value: 'claude', label: 'Claude' }, + { value: 'openai-responses', label: 'OpenAI Responses' }, + { value: 'azure-openai-responses', label: 'Azure OpenAI Responses' }, +] + export function Channels() { const [view, setView] = useState<'list' | 'form'>('list') const [editMode, setEditMode] = useState('form') @@ -36,11 +53,13 @@ export function Channels() { const [channelKey, setChannelKey] = useState('') const [jsonValue, setJsonValue] = useState('') const [mapperRows, setMapperRows] = useState>([]) + const [searchQuery, setSearchQuery] = useState('') + const [openMenu, setOpenMenu] = useState(null) const { addToast } = useToast() const queryClient = useQueryClient() - const { data, isLoading, refetch } = useQuery({ + const { data, isLoading, isFetching, refetch } = useQuery({ queryKey: ['channels'], queryFn: async () => { const response = await apiClient.getChannels() @@ -48,6 +67,12 @@ export function Channels() { }, }) + useEffect(() => { + const handleClick = () => setOpenMenu(null) + document.addEventListener('click', handleClick) + return () => document.removeEventListener('click', handleClick) + }, []) + const saveMutation = useMutation({ mutationFn: async ({ key, config }: { key: string; config: any }) => { return apiClient.saveChannel(key, config) @@ -104,7 +129,6 @@ export function Channels() { setFormData(config) setJsonValue(JSON.stringify(config, null, 2)) - // Convert deployment_mapper to rows if (config.deployment_mapper) { const rows = Object.entries(config.deployment_mapper).map(([request, deployment]) => ({ request, @@ -112,12 +136,11 @@ export function Channels() { })) setMapperRows(rows) } - setView('form') } const handleDelete = (key: string) => { - if (confirm(`确定要删除频道 "${key}" 吗?`)) { + if (confirm(`确定要删除此频道吗?`)) { deleteMutation.mutate(key) } } @@ -135,7 +158,6 @@ export function Channels() { return } - // Build deployment_mapper from rows const deployment_mapper: Record = {} mapperRows.forEach((row) => { if (row.request && row.deployment) { @@ -143,17 +165,14 @@ export function Channels() { } }) - config = { - ...formData, - deployment_mapper, - } + config = { ...formData, deployment_mapper } if (!formData.api_version) { delete config.api_version } } else { try { config = JSON.parse(jsonValue) - } catch (error) { + } catch { addToast('JSON格式错误', 'error') return } @@ -164,7 +183,6 @@ export function Channels() { const toggleEditMode = () => { if (editMode === 'form') { - // Switch to JSON const deployment_mapper: Record = {} mapperRows.forEach((row) => { if (row.request && row.deployment) { @@ -175,7 +193,6 @@ export function Channels() { setJsonValue(JSON.stringify(config, null, 2)) setEditMode('json') } else { - // Switch to form try { const config = JSON.parse(jsonValue) setFormData(config) @@ -187,7 +204,7 @@ export function Channels() { setMapperRows(rows) } setEditMode('form') - } catch (error) { + } catch { addToast('JSON格式错误', 'error') } } @@ -207,257 +224,381 @@ export function Channels() { setMapperRows(newRows) } + const getTypeLabel = (type: string) => { + return channelTypes.find((t) => t.value === type)?.label || type + } + + const filteredData = data?.filter((channel) => { + if (!searchQuery) return true + const config = typeof channel.value === 'string' ? JSON.parse(channel.value) : channel.value + return ( + config.name?.toLowerCase().includes(searchQuery.toLowerCase()) || + channel.key.toLowerCase().includes(searchQuery.toLowerCase()) + ) + }) + + // List View if (view === 'list') { return ( -
-
-

频道管理

-
+ + + +
+ } + > + {/* Search */} + {data && data.length > 0 && ( +
+
+ + setSearchQuery(e.target.value)} + className="pl-9" + /> +
+
+ )} - - -
- - + + + ) : ( + +
+ {filteredData?.map((channel) => { + const config = typeof channel.value === 'string' ? JSON.parse(channel.value) : channel.value + const modelCount = Object.keys(config.deployment_mapper || {}).length + const isMenuOpen = openMenu === channel.key + + return ( +
+ {/* Mobile Layout */} +
+
+
+
{config.name}
+
{channel.key}
+
+
+ + {isMenuOpen && ( +
+ + +
+ )} +
+
+
+ + {getTypeLabel(config.type)} + + + {modelCount} 个模型 + +
+
+ + {/* Desktop Layout */} +
+
+
{config.name}
+
{channel.key}
+
+
+ + {getTypeLabel(config.type)} + +
+
+ {config.endpoint.replace(/^https?:\/\//, '').split('/')[0]} +
+
+ {modelCount} 模型 +
+
+ + +
+
+
+ ) + })} + {filteredData?.length === 0 && searchQuery && ( +
+ 未找到匹配的频道 +
+ )}
- - - {isLoading ? ( -
加载中...
- ) : !data || data.length === 0 ? ( -
暂无频道数据
- ) : ( - - - - 名称 - 标识 - 类型 - 端点 - 模型数 - 操作 - - - - {data.map((channel) => { - const config = - typeof channel.value === 'string' - ? JSON.parse(channel.value) - : channel.value - return ( - - {config.name} - {channel.key} - {config.type} - {config.endpoint} - - {Object.keys(config.deployment_mapper || {}).length} - - -
- - -
-
-
- ) - })} -
-
- )} -
-
-
+
+ )} + ) } + // Form View return ( -
-
-

- {editingKey ? '编辑频道' : '添加频道'} -

-
- - - -
- - - +
+

{editingKey ? '编辑频道' : '添加频道'}

+
- - -
- +
+ +
+ {/* Channel Key */} + + +

频道标识

+

用于内部识别的唯一标识

setChannelKey(e.target.value)} - placeholder="例如: azure-openai-1" + placeholder="例如:azure-gpt4-east" disabled={!!editingKey} + className="font-mono text-sm" /> -
+
+ - {editMode === 'form' ? ( - <> -
- - setFormData({ ...formData, name: e.target.value })} - placeholder="例如: Azure OpenAI" - /> -
- -
- - -
- -
- - setFormData({ ...formData, endpoint: e.target.value })} - placeholder="https://your-resource.openai.azure.com/" - /> -
- -
- - setFormData({ ...formData, api_key: e.target.value })} - placeholder="your-api-key" - /> -
- - {(formData.type === 'azure-openai' || formData.type === 'claude') && ( -
- - setFormData({ ...formData, api_version: e.target.value })} - placeholder="Claude: 2023-06-01 / Azure: 2024-02-01" - /> + {editMode === 'form' ? ( + <> + {/* Basic Info */} + + +

基本信息

+
+
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="例如:Azure GPT-4 东部" + /> +
+
+ + +
- )} +
+
-
-
- -
+ {mapperRows.length === 0 ? ( -
- 暂无映射,点击上方按钮添加 -
+ ) : ( - - - - 请求模型名 - 部署模型名 - 操作 - - - - {mapperRows.map((row, index) => ( - - - - updateMapperRow(index, 'request', e.target.value) - } - placeholder="例如: gpt-4" - /> - - - - updateMapperRow(index, 'deployment', e.target.value) - } - placeholder="例如: gpt-4-deployment" - /> - - - - - - ))} - -
+
+ {mapperRows.map((row, index) => ( +
+ updateMapperRow(index, 'request', e.target.value)} + placeholder="gpt-4" + className="flex-1 bg-background text-sm" + /> + + updateMapperRow(index, 'deployment', e.target.value)} + placeholder="gpt-4-0613" + className="flex-1 bg-background text-sm" + /> + +
+ ))} +
)} -
- - ) : ( -
- + + + + ) : ( + + +

JSON 配置