diff --git a/src/App.tsx b/src/App.tsx
index d42b409..97f2e10 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,6 +1,7 @@
-import { useEffect } from 'react';
+import { useCallback, useEffect, useState } from 'react';
import { HashRouter, Navigate, Route, Routes } from 'react-router-dom';
import { LoginPage } from '@/pages/LoginPage';
+import { DashboardPage } from '@/pages/DashboardPage';
import { SettingsPage } from '@/pages/SettingsPage';
import { ApiKeysPage } from '@/pages/ApiKeysPage';
import { AiProvidersPage } from '@/pages/AiProvidersPage';
@@ -11,6 +12,7 @@ import { ConfigPage } from '@/pages/ConfigPage';
import { LogsPage } from '@/pages/LogsPage';
import { SystemPage } from '@/pages/SystemPage';
import { NotificationContainer } from '@/components/common/NotificationContainer';
+import { SplashScreen } from '@/components/common/SplashScreen';
import { MainLayout } from '@/components/layout/MainLayout';
import { ProtectedRoute } from '@/router/ProtectedRoute';
import { useAuthStore, useLanguageStore, useThemeStore } from '@/stores';
@@ -20,6 +22,9 @@ function App() {
const language = useLanguageStore((state) => state.language);
const setLanguage = useLanguageStore((state) => state.setLanguage);
const restoreSession = useAuthStore((state) => state.restoreSession);
+ const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
+
+ const [showSplash, setShowSplash] = useState(true);
useEffect(() => {
initializeTheme();
@@ -31,6 +36,15 @@ function App() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // 仅用于首屏同步 i18n 语言
+ const handleSplashFinish = useCallback(() => {
+ setShowSplash(false);
+ }, []);
+
+ // 仅在已认证时显示闪屏
+ if (showSplash && isAuthenticated) {
+ return ;
+ }
+
return (
@@ -44,7 +58,8 @@ function App() {
}
>
- } />
+ } />
+ } />
} />
} />
} />
@@ -54,7 +69,7 @@ function App() {
} />
} />
} />
- } />
+ } />
diff --git a/src/components/common/SplashScreen.scss b/src/components/common/SplashScreen.scss
new file mode 100644
index 0000000..f66e1b1
--- /dev/null
+++ b/src/components/common/SplashScreen.scss
@@ -0,0 +1,106 @@
+@use 'sass:color';
+@use '../../styles/variables.scss' as *;
+
+.splash-screen {
+ position: fixed;
+ inset: 0;
+ z-index: 9999;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg-primary);
+ opacity: 1;
+ transition: opacity 0.4s ease-out;
+
+ &.fade-out {
+ opacity: 0;
+ pointer-events: none;
+ }
+}
+
+.splash-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: $spacing-md;
+ animation: splash-enter 0.6s ease-out;
+}
+
+@keyframes splash-enter {
+ from {
+ opacity: 0;
+ transform: scale(0.9) translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+
+.splash-logo {
+ width: 80px;
+ height: 80px;
+ border-radius: $radius-lg;
+ box-shadow: $shadow-lg;
+ animation: splash-logo-pulse 1.5s ease-in-out infinite;
+}
+
+@keyframes splash-logo-pulse {
+ 0%, 100% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.05);
+ }
+}
+
+.splash-title {
+ font-size: 28px;
+ font-weight: 800;
+ color: var(--text-primary);
+ margin: 0;
+ letter-spacing: -0.5px;
+}
+
+.splash-subtitle {
+ font-size: 16px;
+ font-weight: 500;
+ color: var(--text-secondary);
+ margin: 0;
+ margin-top: -8px;
+}
+
+.splash-loader {
+ width: 120px;
+ height: 3px;
+ background: var(--border-color);
+ border-radius: $radius-full;
+ overflow: hidden;
+ margin-top: $spacing-md;
+}
+
+.splash-loader-bar {
+ width: 100%;
+ height: 100%;
+ background: var(--primary-color);
+ border-radius: $radius-full;
+ animation: splash-loading 1.2s ease-in-out infinite;
+ transform-origin: left;
+}
+
+@keyframes splash-loading {
+ 0% {
+ transform: scaleX(0);
+ }
+ 50% {
+ transform: scaleX(1);
+ transform-origin: left;
+ }
+ 50.01% {
+ transform-origin: right;
+ }
+ 100% {
+ transform: scaleX(0);
+ transform-origin: right;
+ }
+}
diff --git a/src/components/common/SplashScreen.tsx b/src/components/common/SplashScreen.tsx
new file mode 100644
index 0000000..1233d8e
--- /dev/null
+++ b/src/components/common/SplashScreen.tsx
@@ -0,0 +1,40 @@
+import { useEffect, useState } from 'react';
+import { INLINE_LOGO_JPEG } from '@/assets/logoInline';
+import './SplashScreen.scss';
+
+interface SplashScreenProps {
+ onFinish: () => void;
+ duration?: number;
+}
+
+export function SplashScreen({ onFinish, duration = 1500 }: SplashScreenProps) {
+ const [fadeOut, setFadeOut] = useState(false);
+
+ useEffect(() => {
+ const fadeTimer = setTimeout(() => {
+ setFadeOut(true);
+ }, duration - 400);
+
+ const finishTimer = setTimeout(() => {
+ onFinish();
+ }, duration);
+
+ return () => {
+ clearTimeout(fadeTimer);
+ clearTimeout(finishTimer);
+ };
+ }, [duration, onFinish]);
+
+ return (
+
+
+

+
CLI Proxy API
+
Management Center
+
+
+
+ );
+}
diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx
index 9410ce0..a8474e3 100644
--- a/src/components/layout/MainLayout.tsx
+++ b/src/components/layout/MainLayout.tsx
@@ -8,6 +8,7 @@ import {
IconFileText,
IconInfo,
IconKey,
+ IconLayoutDashboard,
IconScrollText,
IconSettings,
IconShield,
@@ -18,6 +19,7 @@ import { useAuthStore, useConfigStore, useLanguageStore, useNotificationStore, u
import { versionApi } from '@/services/api';
const sidebarIcons: Record = {
+ dashboard: ,
settings: ,
apiKeys: ,
aiProviders: ,
@@ -230,6 +232,7 @@ export function MainLayout() {
: 'muted';
const navItems = [
+ { path: '/', label: t('nav.dashboard'), icon: sidebarIcons.dashboard },
{ path: '/settings', label: t('nav.basic_settings'), icon: sidebarIcons.settings },
{ path: '/api-keys', label: t('nav.api_keys'), icon: sidebarIcons.apiKeys },
{ path: '/ai-providers', label: t('nav.ai_providers'), icon: sidebarIcons.aiProviders },
diff --git a/src/components/ui/icons.tsx b/src/components/ui/icons.tsx
index f762c7e..de658a8 100644
--- a/src/components/ui/icons.tsx
+++ b/src/components/ui/icons.tsx
@@ -303,3 +303,14 @@ export function IconCode({ size = 20, ...props }: IconProps) {
);
}
+
+export function IconLayoutDashboard({ size = 20, ...props }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 6aa8914..51b0159 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -81,6 +81,7 @@
"status": "Connection Status:"
},
"nav": {
+ "dashboard": "Dashboard",
"basic_settings": "Basic Settings",
"api_keys": "API Keys",
"ai_providers": "AI Providers",
@@ -91,6 +92,13 @@
"logs": "Logs Viewer",
"system_info": "Management Center Info"
},
+ "dashboard": {
+ "title": "Dashboard",
+ "subtitle": "Welcome to CLI Proxy API Management Center",
+ "openai_providers": "OpenAI Providers",
+ "quick_actions": "Quick Actions",
+ "current_config": "Current Configuration"
+ },
"basic_settings": {
"title": "Basic Settings",
"debug_title": "Debug Mode",
diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json
index d9e8bb8..17cc218 100644
--- a/src/i18n/locales/zh-CN.json
+++ b/src/i18n/locales/zh-CN.json
@@ -81,6 +81,7 @@
"status": "连接状态:"
},
"nav": {
+ "dashboard": "仪表盘",
"basic_settings": "基础设置",
"api_keys": "API 密钥",
"ai_providers": "AI 提供商",
@@ -91,6 +92,13 @@
"logs": "日志查看",
"system_info": "中心信息"
},
+ "dashboard": {
+ "title": "仪表盘",
+ "subtitle": "欢迎使用 CLI Proxy API 管理中心",
+ "openai_providers": "OpenAI 提供商",
+ "quick_actions": "快捷操作",
+ "current_config": "当前配置"
+ },
"basic_settings": {
"title": "基础设置",
"debug_title": "调试模式",
diff --git a/src/pages/DashboardPage.module.scss b/src/pages/DashboardPage.module.scss
new file mode 100644
index 0000000..b9f6b38
--- /dev/null
+++ b/src/pages/DashboardPage.module.scss
@@ -0,0 +1,223 @@
+@use 'sass:color';
+@use '../styles/variables.scss' as *;
+
+.dashboard {
+ display: flex;
+ flex-direction: column;
+ gap: $spacing-lg;
+ max-width: 1000px;
+ margin: 0 auto;
+}
+
+.header {
+ margin-bottom: $spacing-sm;
+}
+
+.title {
+ font-size: 26px;
+ font-weight: 800;
+ color: var(--text-primary);
+ margin: 0;
+}
+
+.subtitle {
+ font-size: 15px;
+ color: var(--text-secondary);
+ margin: $spacing-xs 0 0 0;
+}
+
+.connectionCard {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: $spacing-md;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: $radius-lg;
+ padding: $spacing-md $spacing-lg;
+ flex-wrap: wrap;
+}
+
+.connectionStatus {
+ display: flex;
+ align-items: center;
+ gap: $spacing-sm;
+}
+
+.statusDot {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: $gray-400;
+
+ &.connected {
+ background: $success-color;
+ box-shadow: 0 0 8px rgba($success-color, 0.5);
+ }
+
+ &.connecting {
+ background: $warning-color;
+ animation: pulse 1s ease-in-out infinite;
+ }
+
+ &.disconnected {
+ background: $error-color;
+ }
+}
+
+@keyframes pulse {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+}
+
+.statusText {
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.connectionInfo {
+ display: flex;
+ align-items: center;
+ gap: $spacing-md;
+ flex-wrap: wrap;
+}
+
+.serverUrl {
+ font-family: $font-mono;
+ font-size: 13px;
+ color: var(--text-secondary);
+ background: var(--bg-primary);
+ padding: 4px 10px;
+ border-radius: $radius-md;
+ border: 1px solid var(--border-color);
+}
+
+.serverVersion {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--primary-color);
+ background: rgba($primary-color, 0.1);
+ padding: 4px 10px;
+ border-radius: $radius-full;
+}
+
+.statsGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: $spacing-md;
+}
+
+.statCard {
+ display: flex;
+ align-items: center;
+ gap: $spacing-md;
+ padding: $spacing-lg;
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: $radius-lg;
+ text-decoration: none;
+ transition: all $transition-fast;
+
+ &:hover {
+ border-color: var(--primary-color);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+ transform: translateY(-2px);
+ }
+}
+
+.statIcon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 48px;
+ height: 48px;
+ border-radius: $radius-md;
+ background: var(--bg-secondary);
+ color: var(--primary-color);
+}
+
+.statContent {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.statValue {
+ font-size: 24px;
+ font-weight: 800;
+ color: var(--text-primary);
+}
+
+.statLabel {
+ font-size: 13px;
+ color: var(--text-secondary);
+}
+
+.section {
+ display: flex;
+ flex-direction: column;
+ gap: $spacing-md;
+}
+
+.sectionTitle {
+ font-size: 16px;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin: 0;
+}
+
+.actionsGrid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: $spacing-sm;
+
+ a {
+ text-decoration: none;
+ }
+}
+
+.actionButton {
+ display: inline-flex;
+ align-items: center;
+ gap: $spacing-sm;
+}
+
+.configGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: $spacing-sm;
+}
+
+.configItem {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: $spacing-sm;
+ padding: $spacing-sm $spacing-md;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: $radius-md;
+}
+
+.configLabel {
+ font-size: 13px;
+ color: var(--text-secondary);
+}
+
+.configValue {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text-primary);
+
+ &.enabled {
+ color: $success-color;
+ }
+
+ &.disabled {
+ color: var(--text-secondary);
+ }
+}
diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx
new file mode 100644
index 0000000..87dad54
--- /dev/null
+++ b/src/pages/DashboardPage.tsx
@@ -0,0 +1,184 @@
+import { useEffect, useState } from 'react';
+import { Link } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { Button } from '@/components/ui/Button';
+import { IconKey, IconBot, IconFileText, IconChartLine, IconSettings, IconShield } from '@/components/ui/icons';
+import { useAuthStore, useConfigStore } from '@/stores';
+import { apiKeysApi, providersApi, authFilesApi } from '@/services/api';
+import styles from './DashboardPage.module.scss';
+
+interface QuickStat {
+ label: string;
+ value: number | string;
+ icon: React.ReactNode;
+ path: string;
+ loading?: boolean;
+}
+
+export function DashboardPage() {
+ const { t } = useTranslation();
+ const connectionStatus = useAuthStore((state) => state.connectionStatus);
+ const serverVersion = useAuthStore((state) => state.serverVersion);
+ const apiBase = useAuthStore((state) => state.apiBase);
+ const config = useConfigStore((state) => state.config);
+
+ const [stats, setStats] = useState<{
+ apiKeys: number | null;
+ providers: number | null;
+ authFiles: number | null;
+ }>({
+ apiKeys: null,
+ providers: null,
+ authFiles: null
+ });
+
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchStats = async () => {
+ setLoading(true);
+ try {
+ const [keysRes, providersRes, filesRes] = await Promise.allSettled([
+ apiKeysApi.list(),
+ providersApi.getOpenAIProviders(),
+ authFilesApi.list()
+ ]);
+
+ setStats({
+ apiKeys: keysRes.status === 'fulfilled' ? keysRes.value.length : null,
+ providers: providersRes.status === 'fulfilled' ? providersRes.value.length : null,
+ authFiles: filesRes.status === 'fulfilled' ? filesRes.value.files.length : null
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (connectionStatus === 'connected') {
+ fetchStats();
+ }
+ }, [connectionStatus]);
+
+ const quickStats: QuickStat[] = [
+ {
+ label: t('nav.api_keys'),
+ value: stats.apiKeys ?? '-',
+ icon: ,
+ path: '/api-keys',
+ loading: loading && stats.apiKeys === null
+ },
+ {
+ label: t('dashboard.openai_providers'),
+ value: stats.providers ?? '-',
+ icon: ,
+ path: '/ai-providers',
+ loading: loading && stats.providers === null
+ },
+ {
+ label: t('nav.auth_files'),
+ value: stats.authFiles ?? '-',
+ icon: ,
+ path: '/auth-files',
+ loading: loading && stats.authFiles === null
+ }
+ ];
+
+ const quickActions = [
+ { label: t('nav.basic_settings'), icon: , path: '/settings' },
+ { label: t('nav.ai_providers'), icon: , path: '/ai-providers' },
+ { label: t('nav.oauth'), icon: , path: '/oauth' },
+ { label: t('nav.usage_stats'), icon: , path: '/usage' }
+ ];
+
+ return (
+
+
+
{t('dashboard.title')}
+
{t('dashboard.subtitle')}
+
+
+
+
+
+
+ {t(
+ connectionStatus === 'connected'
+ ? 'common.connected'
+ : connectionStatus === 'connecting'
+ ? 'common.connecting'
+ : 'common.disconnected'
+ )}
+
+
+
+ {apiBase || '-'}
+ {serverVersion && v{serverVersion}}
+
+
+
+
+ {quickStats.map((stat) => (
+
+
{stat.icon}
+
+ {stat.loading ? '...' : stat.value}
+ {stat.label}
+
+
+ ))}
+
+
+
+
{t('dashboard.quick_actions')}
+
+ {quickActions.map((action) => (
+
+
+
+ ))}
+
+
+
+ {config && (
+
+
{t('dashboard.current_config')}
+
+
+ {t('basic_settings.debug_enable')}
+
+ {config.debug ? t('common.yes') : t('common.no')}
+
+
+
+ {t('basic_settings.usage_statistics_enable')}
+
+ {config.usageStatisticsEnabled ? t('common.yes') : t('common.no')}
+
+
+
+ {t('basic_settings.logging_to_file_enable')}
+
+ {config.loggingToFile ? t('common.yes') : t('common.no')}
+
+
+
+ {t('basic_settings.retry_count_label')}
+ {config.requestRetry ?? 0}
+
+
+
+ )}
+
+ );
+}