diff --git a/web/package.json b/web/package.json index 64ab83d..974e488 100644 --- a/web/package.json +++ b/web/package.json @@ -4,7 +4,7 @@ "version": "0.1.0", "description": "GPT Load Balancer Frontend - A modern Vue 3 frontend for GPT load balancing service", "type": "module", - "keywords": ["vue3", "typescript", "vite", "naive-ui", "gpt", "load-balancer", "frontend"], + "keywords": ["vue3", "typescript", "vite", "naive-ui", "gpt-load", "frontend"], "author": "tbphp", "license": "MIT", "repository": { diff --git a/web/src/App.vue b/web/src/App.vue index f0cf0b3..d715f88 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -10,7 +10,34 @@ const isLoggedIn = computed(() => !!authKey.value); + + diff --git a/web/src/assets/style.css b/web/src/assets/style.css index 44d1249..0cfd5e3 100644 --- a/web/src/assets/style.css +++ b/web/src/assets/style.css @@ -1,28 +1,65 @@ :root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + "Helvetica Neue", + Arial, + sans-serif; + line-height: 1.6; font-weight: 400; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + + /* 现代化颜色方案 */ + --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + --success-gradient: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + --warning-gradient: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); + --surface-gradient: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); + --dark-gradient: linear-gradient(135deg, #2c3e50 0%, #34495e 100%); + + /* 阴影效果 */ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + + /* 边框半径 */ + --border-radius-sm: 8px; + --border-radius-md: 12px; + --border-radius-lg: 16px; + --border-radius-xl: 24px; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; } html, body { height: 100%; + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + background-attachment: fixed; } #app { - max-width: 1280px; + max-width: 1400px; width: 100%; - padding: 20px; margin: 0 auto; height: 100%; box-sizing: border-box; + position: relative; } +/* 工具类 */ .flex { display: flex; } @@ -78,3 +115,216 @@ body { .shrink { flex-shrink: 1; } + +/* 动画和过渡 */ +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.3s ease; +} + +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} + +.slide-enter-active, +.slide-leave-active { + transition: transform 0.3s ease; +} + +.slide-enter-from { + transform: translateX(-100%); +} + +.slide-leave-to { + transform: translateX(100%); +} + +/* 美化滚动条 */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.05); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%); +} + +/* 通用卡片样式 */ +.modern-card { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-radius: var(--border-radius-lg); + box-shadow: var(--shadow-lg); + border: 1px solid rgba(255, 255, 255, 0.2); + transition: all 0.3s ease; +} + +.modern-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-xl); +} + +/* 按钮增强 */ +.modern-button { + border-radius: var(--border-radius-md); + transition: all 0.3s ease; + font-weight: 600; + letter-spacing: 0.5px; +} + +.modern-button:hover { + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +/* 输入框增强 */ +.modern-input { + border-radius: var(--border-radius-md); + transition: all 0.3s ease; +} + +.modern-input:focus { + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +/* 选择文本样式 */ +::selection { + background: rgba(102, 126, 234, 0.2); + color: #1e293b; +} + +::-moz-selection { + background: rgba(102, 126, 234, 0.2); + color: #1e293b; +} + +/* Focus样式增强 */ +*:focus { + outline: none; +} + +*:focus-visible { + outline: 2px solid rgba(102, 126, 234, 0.5); + outline-offset: 2px; +} + +/* 加载状态样式 */ +.loading-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +/* 响应式网格 */ +.responsive-grid { + display: grid; + gap: 24px; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); +} + +@media (max-width: 640px) { + .responsive-grid { + grid-template-columns: 1fr; + gap: 16px; + } +} + +/* 文本渐变 */ +.text-gradient { + background: var(--primary-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* 玻璃态效果 */ +.glass-effect { + background: rgba(255, 255, 255, 0.25); + backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.18); +} + +/* 悬停效果增强 */ +.hover-lift { + transition: + transform 0.3s ease, + box-shadow 0.3s ease; +} + +.hover-lift:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-xl); +} + +/* 脉冲动画 */ +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.pulse { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +/* 旋转动画 */ +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.spin { + animation: spin 1s linear infinite; +} + +/* 弹跳动画 */ +@keyframes bounce { + 0%, + 20%, + 53%, + 80%, + 100% { + transform: translate3d(0, 0, 0); + } + 40%, + 43% { + transform: translate3d(0, -30px, 0); + } + 70% { + transform: translate3d(0, -15px, 0); + } + 90% { + transform: translate3d(0, -4px, 0); + } +} + +.bounce { + animation: bounce 1s ease-in-out; +} diff --git a/web/src/components/BaseInfoCard.vue b/web/src/components/BaseInfoCard.vue index db03a4a..4ac70e7 100644 --- a/web/src/components/BaseInfoCard.vue +++ b/web/src/components/BaseInfoCard.vue @@ -1,5 +1,232 @@ + + + + diff --git a/web/src/components/GlobalProviders.vue b/web/src/components/GlobalProviders.vue index 2226fb3..92c6478 100644 --- a/web/src/components/GlobalProviders.vue +++ b/web/src/components/GlobalProviders.vue @@ -1,14 +1,44 @@ diff --git a/web/src/components/Layout.vue b/web/src/components/Layout.vue index 78cce92..887b29a 100644 --- a/web/src/components/Layout.vue +++ b/web/src/components/Layout.vue @@ -4,25 +4,176 @@ import NavBar from "@/components/NavBar.vue"; diff --git a/web/src/components/LineChart.vue b/web/src/components/LineChart.vue index 844ac8a..6166a28 100644 --- a/web/src/components/LineChart.vue +++ b/web/src/components/LineChart.vue @@ -1,5 +1,265 @@ + + + + diff --git a/web/src/components/Logout.vue b/web/src/components/Logout.vue index d5bf1a1..6fba1ce 100644 --- a/web/src/components/Logout.vue +++ b/web/src/components/Logout.vue @@ -12,5 +12,38 @@ const handleLogout = () => { + + diff --git a/web/src/components/NavBar.vue b/web/src/components/NavBar.vue index a653238..5c244b6 100644 --- a/web/src/components/NavBar.vue +++ b/web/src/components/NavBar.vue @@ -4,16 +4,16 @@ import { computed, h } from "vue"; import { RouterLink, useRoute } from "vue-router"; const menuOptions: MenuOption[] = [ - renderMenuItem("dashboard", "仪表盘"), - renderMenuItem("keys", "密钥管理"), - renderMenuItem("logs", "日志"), - renderMenuItem("settings", "系统设置"), + renderMenuItem("dashboard", "仪表盘", "📊"), + renderMenuItem("keys", "密钥管理", "🔑"), + renderMenuItem("logs", "日志", "📋"), + renderMenuItem("settings", "系统设置", "⚙️"), ]; const route = useRoute(); const activeMenu = computed(() => route.name); -function renderMenuItem(key: string, label: string): MenuOption { +function renderMenuItem(key: string, label: string, icon: string): MenuOption { return { label: () => h( @@ -22,8 +22,14 @@ function renderMenuItem(key: string, label: string): MenuOption { to: { name: key, }, + class: "nav-menu-item", }, - { default: () => label } + { + default: () => [ + h("span", { class: "nav-item-icon" }, icon), + h("span", { class: "nav-item-text" }, label), + ], + } ), key, }; @@ -31,5 +37,86 @@ function renderMenuItem(key: string, label: string): MenuOption { + + diff --git a/web/src/views/Dashboard.vue b/web/src/views/Dashboard.vue index 3ca9e95..e1f3960 100644 --- a/web/src/views/Dashboard.vue +++ b/web/src/views/Dashboard.vue @@ -4,12 +4,81 @@ import LineChart from "@/components/LineChart.vue"; diff --git a/web/src/views/Keys.vue b/web/src/views/Keys.vue index a4de1cc..6483bbb 100644 --- a/web/src/views/Keys.vue +++ b/web/src/views/Keys.vue @@ -1,3 +1,118 @@ + + + + diff --git a/web/src/views/Login.vue b/web/src/views/Login.vue index 0bf1f8b..205f580 100644 --- a/web/src/views/Login.vue +++ b/web/src/views/Login.vue @@ -12,7 +12,7 @@ const { login } = useAuthService(); const handleLogin = async () => { if (!authKey.value) { - message.error("Please enter Auth Key"); + message.error("请输入授权密钥"); return; } loading.value = true; @@ -21,42 +21,224 @@ const handleLogin = async () => { if (success) { router.push("/"); } else { - message.error("Login failed, please check your Auth Key"); + message.error("登录失败,请检查您的授权密钥"); } }; diff --git a/web/src/views/Logs.vue b/web/src/views/Logs.vue index d27c933..a8e063c 100644 --- a/web/src/views/Logs.vue +++ b/web/src/views/Logs.vue @@ -1,3 +1,118 @@ + + + + diff --git a/web/src/views/Settings.vue b/web/src/views/Settings.vue index e201694..f5c9a5f 100644 --- a/web/src/views/Settings.vue +++ b/web/src/views/Settings.vue @@ -42,63 +42,286 @@ async function handleSubmit() {