Initial commit: SQL audit platform v0.1.0

FastAPI backend with auth, user, instance, ticket, query, dashboard APIs.
React + TypeScript frontend with mock data UI (10 pages).
SQLite + async SQLAlchemy, JWT auth, role-based access control.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
loganandClaude Opus 4.7 committed 2026-05-22 11:11:57 +08:00
commit 439155c92e
290 files changed
+61950

No files matched your search

+173
View File
@@ -0,0 +1,173 @@
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import {
LayoutDashboard, Database, Server, Zap, Leaf,
ClipboardList, Search, UserCog, Users, Lock, Shield, GitBranch, Bell,
LogOut, Settings, Clock, Megaphone, Sun, Moon
} from 'lucide-react';
import { menuSections } from '@/lib/mock';
import { useAuth } from '@/hooks/useAuth';
import type { UserRole } from '@/hooks/useAuth';
import { useTheme } from '@/hooks/useTheme';
const iconMap: Record<string, React.ElementType> = {
LayoutDashboard, Database, Server, Zap, Leaf,
ClipboardList, Search, UserCog, Users, Lock, Shield, GitBranch, Bell,
Settings, Clock, Megaphone,
};
const roleBadgeStyle: Record<UserRole, string> = {
admin: 'bg-purple-500/15 text-purple-400',
dba: 'bg-blue-500/15 text-blue-400',
developer: 'bg-green-500/15 text-green-400',
guest: 'bg-gray-500/15 text-gray-400',
};
export default function Layout() {
const { user, logout, switchRole, visibleMenuItems } = useAuth();
const { theme, toggleTheme } = useTheme();
const location = useLocation();
const navigate = useNavigate();
const currentPath = location.pathname.slice(1) || 'dashboard';
const getPageTitle = () => {
const item = visibleMenuItems.find(m => m.id === currentPath);
return item?.label || '仪表盘';
};
const toggleRole = () => {
const roles: UserRole[] = ['admin', 'dba', 'developer', 'guest'];
const idx = roles.indexOf(user.role);
const nextRole = roles[(idx + 1) % roles.length];
switchRole(nextRole);
// 切换角色后如果当前页面无权限,自动跳仪表盘
const allowed = visibleMenuItems.find(m => m.id === currentPath);
if (!allowed && currentPath !== 'dashboard') {
navigate('/dashboard', { replace: true });
}
};
return (
<div className="flex h-screen bg-[var(--dg-bg-primary)] text-[var(--dg-text-primary)] overflow-hidden">
{/* 侧边栏 */}
<aside className="flex flex-col bg-[var(--dg-bg-secondary)] border-r border-[var(--dg-border)] w-[260px] min-w-[260px]">
{/* Logo */}
<div className="h-16 flex items-center px-5 border-b border-[var(--dg-border)]">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg dg-gradient-br flex items-center justify-center flex-shrink-0">
<Shield className="w-5 h-5 text-white" />
</div>
<span className="text-lg font-bold dg-gradient-text whitespace-nowrap">
Web-Lite
</span>
</div>
</div>
{/* 菜单 */}
<nav className="flex-1 overflow-y-auto scrollbar-thin py-3">
{menuSections.map(section => {
const items = visibleMenuItems.filter(item => item.section === section.id);
if (items.length === 0) return null;
return (
<div key={section.id} className="mb-2">
<div className="px-5 py-2 text-[11px] font-semibold uppercase tracking-wider text-[var(--dg-text-muted)]">
{section.label}
</div>
{items.map(item => {
const Icon = iconMap[item.icon] || Database;
const isActive = currentPath === item.id;
return (
<div
key={item.id}
className={`
flex items-center mx-3 px-4 py-2.5 rounded-lg cursor-pointer transition-all duration-200 relative
${isActive
? 'bg-blue-500/15 text-blue-400'
: 'text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] hover:text-[var(--dg-text-primary)]'
}
`}
onClick={() => navigate(`/${item.id}`)}
>
{isActive && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-5 bg-blue-400 rounded-r" />
)}
<Icon className="w-[18px] h-[18px] flex-shrink-0" />
<span className="ml-3 text-sm font-medium">{item.label}</span>
</div>
);
})}
</div>
);
})}
</nav>
{/* 底部 */}
<div className="p-4 border-t border-[var(--dg-border)]">
{/* 角色切换演示 */}
<div className="mb-3 px-3 py-2 rounded-lg bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)]">
<div className="flex items-center justify-between">
<span className="text-xs text-[var(--dg-text-muted)]">当前角色</span>
<span className={`text-xs font-medium px-2 py-0.5 rounded ${roleBadgeStyle[user.role]}`}>
{user.roleName}
</span>
</div>
<button
onClick={toggleRole}
className="mt-2 w-full text-center text-[11px] text-[var(--dg-text-muted)] hover:text-blue-400 transition-colors py-1 rounded hover:bg-[var(--dg-bg-hover)]"
>
[演示] 切换角色
</button>
</div>
<div className="flex items-center gap-3 p-2 rounded-lg hover:bg-[var(--dg-bg-hover)] cursor-pointer transition-colors">
<div className="w-9 h-9 rounded-full dg-gradient-br flex items-center justify-center text-white font-semibold text-sm flex-shrink-0">
{user.avatar}
</div>
<div className="overflow-hidden flex-1">
<div className="text-sm font-medium truncate">{user.username}</div>
<div className="text-xs text-[var(--dg-text-muted)]">{user.roleName}</div>
</div>
</div>
<button
className="mt-2 w-full flex items-center justify-center gap-2 p-2 rounded-lg hover:bg-[var(--dg-bg-hover)] cursor-pointer text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)] transition-colors text-xs"
onClick={() => { logout(); navigate('/login'); }}
>
<LogOut className="w-3.5 h-3.5" /> 退出登录
</button>
</div>
</aside>
{/* 主内容区 */}
<main className="flex-1 flex flex-col min-w-0 overflow-hidden">
{/* 顶部栏 */}
<header className="h-16 bg-[var(--dg-bg-secondary)] border-b border-[var(--dg-border)] flex items-center justify-between px-6 flex-shrink-0">
<div className="flex items-center gap-2 text-sm">
<span className="text-[var(--dg-text-muted)]">Web-Lite</span>
<span className="text-[var(--dg-text-muted)]">/</span>
<span className="text-[var(--dg-text-primary)] font-medium">{getPageTitle()}</span>
</div>
<div className="flex items-center gap-3">
{/* 角色标签 */}
<span className={`px-2.5 py-1 rounded-md text-xs font-medium ${roleBadgeStyle[user.role]}`}>
{user.roleName}
</span>
<button
onClick={toggleTheme}
className="w-9 h-9 rounded-lg flex items-center justify-center text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] hover:text-[var(--dg-text-primary)] transition-colors"
title={theme === 'dark' ? '切换到浅色模式' : '切换到深色模式'}
>
{theme === 'dark' ? <Sun className="w-[18px] h-[18px]" /> : <Moon className="w-[18px] h-[18px]" />}
</button>
<button className="w-9 h-9 rounded-lg flex items-center justify-center text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] hover:text-[var(--dg-text-primary)] transition-colors relative">
<Bell className="w-[18px] h-[18px]" />
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-red-500 rounded-full" />
</button>
</div>
</header>
{/* 页面内容 */}
<div className="flex-1 overflow-y-auto scrollbar-thin p-6">
<Outlet />
</div>
</main>
</div>
);
}