fix(frontend): standardize favicon/title, 401 redirect, SPA root route, and theme switching

- Replace default Vite favicon and title with project-specific branding
- Add axios response interceptor to handle 401 by clearing token and redirecting to login
- Move health check endpoint from '/' to '/api/health' so SPA index.html is served on root
- Integrate next-themes ThemeProvider with system preference detection and manual toggle
- Update docker-compose and k8s health check paths accordingly
- Replace hardcoded dark-only colors with semantic CSS variable tokens for theme compatibility
This commit is contained in:
jeffusion
2026-03-09 22:45:47 +08:00
committed by 路遥知码力
parent 2d4f670365
commit 5bb1c3a2d1
12 changed files with 82 additions and 45 deletions

View File

@@ -50,7 +50,7 @@ services:
ports:
- "3334:3000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
interval: 5s
timeout: 3s
retries: 10
@@ -58,7 +58,7 @@ services:
ports:
- "3334:3000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
interval: 5s
timeout: 3s
retries: 10

View File

@@ -19,7 +19,7 @@ services:
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
interval: 30s
timeout: 5s
retries: 3

View File

@@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>Gitea AI Assistant</title>
</head>
<body>
<div id="root"></div>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="hsl(175, 90%, 45%)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>

After

Width:  |  Height:  |  Size: 342 B

View File

@@ -6,6 +6,7 @@ import { RepositoryManager } from './components/RepositoryManager';
import { ConfigManager } from './components/ConfigManager';
import { ReviewConfigPage } from './components/ReviewConfigPage';
import { Toaster } from "@/components/ui/sonner"
import { useTheme } from 'next-themes'
function AuthGuard({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading } = useAuth();
@@ -32,7 +33,9 @@ function AuthGuard({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
function App() {
function AppContent() {
const { resolvedTheme } = useTheme();
return (
<BrowserRouter>
<Routes>
@@ -51,9 +54,13 @@ function App() {
<Route path="*" element={<Navigate to="/repos" replace />} />
</Route>
</Routes>
<Toaster theme="dark" />
<Toaster theme={resolvedTheme === 'dark' ? 'dark' : 'light'} />
</BrowserRouter>
);
}
function App() {
return <AppContent />;
}
export default App;

View File

@@ -72,11 +72,11 @@
}
.bg-grid-pattern {
background-size: 40px 40px;
background-image: linear-gradient(to right, rgba(255, 255, 255, 0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
background-image: linear-gradient(to right, hsl(var(--foreground) / 0.05) 1px, transparent 1px),
linear-gradient(to bottom, hsl(var(--foreground) / 0.05) 1px, transparent 1px);
}
.glass-panel {
@apply bg-zinc-950/50 backdrop-blur-xl border border-white/10 shadow-2xl;
@apply bg-card/80 backdrop-blur-xl border border-border shadow-2xl;
}
.tech-glow {
box-shadow: 0 0 20px -5px hsl(var(--primary) / 0.5);

View File

@@ -18,4 +18,20 @@ api.interceptors.request.use(
}
);
// 添加响应拦截器,处理 401 未授权自动跳转登录页
api.interceptors.response.use(
(response) => response,
(error) => {
if (axios.isAxiosError(error) && error.response?.status === 401) {
localStorage.removeItem('authToken');
// 避免在登录接口本身触发跳转
const isLoginRequest = error.config?.url?.includes('/login');
if (!isLoginRequest) {
window.location.href = '/';
}
}
return Promise.reject(error);
}
);
export default api;

View File

@@ -4,6 +4,7 @@ import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import axios from 'axios'
import { ThemeProvider } from 'next-themes'
const queryClient = new QueryClient({
defaultOptions: {
@@ -21,13 +22,13 @@ const queryClient = new QueryClient({
},
});
// Force dark mode as requested
document.documentElement.classList.add('dark');
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</ThemeProvider>
</React.StrictMode>,
)

View File

@@ -1,7 +1,8 @@
import { useState, useEffect } from 'react';
import { NavLink, Outlet, useLocation } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { LogOut, Bot, FolderGit2, Sliders, Menu, X, PanelLeftClose, PanelLeftOpen, FileSearch } from 'lucide-react';
import { LogOut, Bot, FolderGit2, Sliders, Menu, X, PanelLeftClose, PanelLeftOpen, FileSearch, Sun, Moon } from 'lucide-react';
import { useTheme } from 'next-themes';
const navItems = [
{ path: '/repos', label: '仓库管理', icon: FolderGit2 },
@@ -11,6 +12,7 @@ const navItems = [
export default function DashboardPage() {
const location = useLocation();
const { setTheme, resolvedTheme } = useTheme();
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
@@ -33,24 +35,24 @@ export default function DashboardPage() {
{/* Mobile Overlay */}
{isMobileMenuOpen && (
<div
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm lg:hidden animate-in fade-in"
className="fixed inset-0 z-40 bg-foreground/60 backdrop-blur-sm lg:hidden animate-in fade-in"
onClick={() => setIsMobileMenuOpen(false)}
/>
)}
{/* Sidebar */}
<aside
className={`fixed inset-y-0 left-0 z-50 flex flex-col border-r border-border bg-zinc-950 transition-all duration-300 ease-in-out lg:relative ${
className={`fixed inset-y-0 left-0 z-50 flex flex-col border-r border-border bg-card transition-all duration-300 ease-in-out lg:relative ${
isSidebarCollapsed ? 'w-[72px]' : 'w-64'
} ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}`}
>
<div className="flex h-16 items-center justify-between px-4 border-b border-border/50 bg-zinc-950">
<div className="flex h-16 items-center justify-between px-4 border-b border-border/50 bg-card">
<div className={`flex items-center gap-3 overflow-hidden transition-all duration-300 ${isSidebarCollapsed ? 'w-10 justify-center -ml-1' : 'w-full'}`}>
<div className="flex shrink-0 h-9 w-9 items-center justify-center rounded-xl bg-primary/10 text-primary border border-primary/20 shadow-[0_0_15px_rgba(20,184,166,0.15)] ring-1 ring-primary/10">
<Bot className="h-5 w-5" />
</div>
{!isSidebarCollapsed && (
<span className="truncate font-bold tracking-tight text-zinc-100 whitespace-nowrap">
<span className="truncate font-bold tracking-tight text-foreground whitespace-nowrap">
Gitea AI Assistant
</span>
)}
@@ -58,7 +60,7 @@ export default function DashboardPage() {
<Button
variant="ghost"
size="icon"
className="lg:hidden shrink-0 h-8 w-8 text-zinc-400 hover:text-zinc-100"
className="lg:hidden shrink-0 h-8 w-8 text-muted-foreground hover:text-foreground"
onClick={() => setIsMobileMenuOpen(false)}
>
<X className="h-4 w-4" />
@@ -76,7 +78,7 @@ export default function DashboardPage() {
`group relative flex w-full items-center rounded-xl p-2.5 transition-all duration-200 ${
isActive
? 'bg-primary/10 text-primary'
: 'text-zinc-400 hover:bg-zinc-900 hover:text-zinc-100'
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground'
} ${isSidebarCollapsed ? 'justify-center' : 'justify-start gap-3'}`
}
title={isSidebarCollapsed ? item.label : undefined}
@@ -86,7 +88,7 @@ export default function DashboardPage() {
{isActive && (
<div className="absolute left-0 top-1/2 h-1/2 w-1 -translate-y-1/2 rounded-r-full bg-primary shadow-[0_0_10px_rgba(20,184,166,0.5)]"></div>
)}
<Icon className={`h-5 w-5 shrink-0 transition-transform duration-300 ${isActive ? 'text-primary scale-110' : 'text-zinc-500 group-hover:text-zinc-300'}`} />
<Icon className={`h-5 w-5 shrink-0 transition-transform duration-300 ${isActive ? 'text-primary scale-110' : 'text-muted-foreground group-hover:text-foreground'}`} />
{!isSidebarCollapsed && (
<span className="font-medium tracking-wide text-sm">{item.label}</span>
)}
@@ -97,10 +99,10 @@ export default function DashboardPage() {
})}
</nav>
<div className="border-t border-border/50 p-3 bg-zinc-950">
<div className="border-t border-border/50 p-3 bg-card">
<button
onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
className={`hidden lg:flex w-full items-center rounded-xl p-2.5 text-zinc-500 transition-colors hover:bg-zinc-900 hover:text-zinc-300 ${
className={`hidden lg:flex w-full items-center rounded-xl p-2.5 text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground ${
isSidebarCollapsed ? 'justify-center' : 'justify-start gap-3'
}`}
>
@@ -124,7 +126,7 @@ export default function DashboardPage() {
<Button
variant="ghost"
size="icon"
className="lg:hidden text-zinc-400 hover:text-zinc-100 h-9 w-9 -ml-2"
className="lg:hidden text-muted-foreground hover:text-foreground h-9 w-9 -ml-2"
onClick={() => setIsMobileMenuOpen(true)}
>
<Menu className="h-5 w-5" />
@@ -136,18 +138,28 @@ export default function DashboardPage() {
</div>
<div className="flex items-center gap-4">
<div className="hidden sm:flex items-center gap-2 px-3 py-1.5 rounded-full border border-border/50 bg-zinc-900/50">
<div className="hidden sm:flex items-center gap-2 px-3 py-1.5 rounded-full border border-border/50 bg-muted/50">
<div className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-500 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-success"></span>
</div>
<span className="text-xs font-mono text-zinc-400 uppercase tracking-wider">System Online</span>
<span className="text-xs font-mono text-muted-foreground uppercase tracking-wider">System Online</span>
</div>
<div className="h-6 w-px bg-border/50 hidden sm:block"></div>
<Button
variant="ghost"
size="icon"
className="rounded-full border border-border/50 bg-zinc-900 hover:bg-rose-500/10 hover:text-rose-400 hover:border-rose-500/20 transition-all h-9 w-9"
className="rounded-full border border-border/50 bg-muted hover:bg-accent/50 transition-all h-9 w-9"
onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
title={resolvedTheme === 'dark' ? '切换为浅色主题' : '切换为深色主题'}
>
{resolvedTheme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
<span className="sr-only"></span>
</Button>
<Button
variant="ghost"
size="icon"
className="rounded-full border border-border/50 bg-muted hover:bg-danger/10 hover:text-danger hover:border-danger/20 transition-all h-9 w-9"
onClick={handleLogout}
title="登出"
>

View File

@@ -29,7 +29,7 @@ export function LoginPage() {
};
return (
<div className="relative flex min-h-screen w-full items-center justify-center overflow-hidden bg-zinc-950">
<div className="relative flex min-h-screen w-full items-center justify-center overflow-hidden bg-background">
{/* Background grid and gradient effects */}
<div className="absolute inset-0 bg-grid-pattern opacity-10"></div>
<div className="absolute top-[-20%] left-[-10%] h-[500px] w-[500px] rounded-full bg-primary/20 blur-[120px] pointer-events-none"></div>
@@ -39,17 +39,17 @@ export function LoginPage() {
<div className="glass-panel relative rounded-2xl p-8 sm:p-10 transition-all duration-500 hover:border-primary/20">
{/* Decorative terminal dots */}
<div className="absolute top-4 left-4 flex gap-2">
<div className="h-2.5 w-2.5 rounded-full bg-rose-500/80 shadow-[0_0_5px_rgba(244,63,94,0.5)]"></div>
<div className="h-2.5 w-2.5 rounded-full bg-amber-500/80 shadow-[0_0_5px_rgba(245,158,11,0.5)]"></div>
<div className="h-2.5 w-2.5 rounded-full bg-emerald-500/80 shadow-[0_0_5px_rgba(16,185,129,0.5)]"></div>
<div className="h-2.5 w-2.5 rounded-full bg-danger/80 shadow-[0_0_5px_rgba(244,63,94,0.5)]"></div>
<div className="h-2.5 w-2.5 rounded-full bg-warning/80 shadow-[0_0_5px_rgba(245,158,11,0.5)]"></div>
<div className="h-2.5 w-2.5 rounded-full bg-success/80 shadow-[0_0_5px_rgba(16,185,129,0.5)]"></div>
</div>
<div className="mb-10 mt-6 flex flex-col items-center text-center">
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-2xl bg-zinc-900 border border-primary/20 shadow-[0_0_20px_rgba(var(--primary),0.15)] ring-1 ring-primary/10 relative group">
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-2xl bg-muted border border-primary/20 shadow-[0_0_20px_rgba(var(--primary),0.15)] ring-1 ring-primary/10 relative group">
<div className="absolute inset-0 rounded-2xl bg-primary/20 blur-md opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<Bot className="h-8 w-8 text-primary relative z-10" />
</div>
<h1 className="mb-2 text-2xl font-bold tracking-tight text-white sm:text-3xl">Gitea AI Assistant</h1>
<h1 className="mb-2 text-2xl font-bold tracking-tight text-foreground sm:text-3xl">Gitea AI Assistant</h1>
<div className="flex items-center gap-2 text-xs font-mono text-primary/70 bg-primary/5 px-3 py-1 rounded-full border border-primary/10">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
@@ -62,7 +62,7 @@ export function LoginPage() {
<div className="grid gap-5">
<div className="space-y-2">
<div className="flex items-center justify-between">
<label htmlFor="password" className="text-xs font-mono font-medium text-zinc-400 flex items-center gap-2">
<label htmlFor="password" className="text-xs font-mono font-medium text-muted-foreground flex items-center gap-2">
<span className="text-primary font-bold">&gt;</span> enter_admin_password
</label>
</div>
@@ -75,14 +75,14 @@ export function LoginPage() {
onKeyDown={(e) => e.key === 'Enter' && !isLoading && handleLogin()}
required
placeholder="••••••••"
className="h-12 border-zinc-800 bg-zinc-900/50 font-mono text-zinc-100 placeholder:text-zinc-700 focus-visible:border-primary/50 focus-visible:ring-primary/20 transition-all duration-300"
className="h-12 border-border bg-muted/50 font-mono text-foreground placeholder:text-muted-foreground/50 focus-visible:border-primary/50 focus-visible:ring-primary/20 transition-all duration-300"
/>
<Terminal className="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-600 transition-colors group-focus-within:text-primary/70" />
<Terminal className="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground transition-colors group-focus-within:text-primary/70" />
</div>
</div>
{error && (
<div className="flex items-start gap-2 rounded-lg border border-rose-500/20 bg-rose-500/10 px-3 py-3 text-sm text-rose-400 animate-in fade-in slide-in-from-top-1">
<div className="flex items-start gap-2 rounded-lg border border-danger/20 bg-danger/10 px-3 py-3 text-sm text-danger animate-in fade-in slide-in-from-top-1">
<Activity className="h-4 w-4 mt-0.5 shrink-0" />
<p className="font-mono text-xs leading-relaxed">{error}</p>
</div>
@@ -94,7 +94,7 @@ export function LoginPage() {
className="tech-glow group relative mt-4 h-12 w-full overflow-hidden bg-primary text-primary-foreground transition-all hover:bg-primary/90 disabled:opacity-70 disabled:pointer-events-none"
>
<div className="absolute inset-0 flex h-full w-full justify-center [transform:skew(-12deg)_translateX(-150%)] group-hover:duration-1000 group-hover:[transform:skew(-12deg)_translateX(150%)]">
<div className="relative h-full w-12 bg-white/20"></div>
<div className="relative h-full w-12 bg-foreground/20"></div>
</div>
<span className="relative flex items-center gap-2 font-mono font-semibold tracking-wide">
{isLoading ? (

View File

@@ -56,7 +56,7 @@ spec:
mountPath: /app/data
livenessProbe:
httpGet:
path: /
path: /api/health
port: http
initialDelaySeconds: 10
periodSeconds: 30
@@ -64,7 +64,7 @@ spec:
failureThreshold: 3
readinessProbe:
httpGet:
path: /
path: /api/health
port: http
initialDelaySeconds: 5
periodSeconds: 10

View File

@@ -24,7 +24,7 @@ const app = new Hono();
// --- API 路由 ---
// 健康检查路由
app.get('/', (c) => {
app.get('/api/health', (c) => {
const webhookSecretConfigured = !!config.app.webhookSecret;
return c.json({