feat: Refactor frontend layout with new sidebar and i18n, update backend LLM adapters, and adjust database models.
This commit is contained in:
parent
6ce5b3c6c1
commit
7d1925db66
|
|
@ -13,3 +13,4 @@ COPY . .
|
|||
# Command is overridden by docker-compose for dev
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -101,3 +101,4 @@ formatter = generic
|
|||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -88,3 +88,4 @@ if context.is_offline_mode():
|
|||
else:
|
||||
asyncio.run(run_migrations_online())
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -23,3 +23,4 @@ def upgrade() -> None:
|
|||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -208,3 +208,4 @@ async def remove_project_member(
|
|||
|
||||
return {"message": "成员已移除"}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -64,3 +64,4 @@ async def read_user_me(
|
|||
"""
|
||||
return current_user
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -27,3 +27,4 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|||
def get_password_hash(password: str) -> str:
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,3 +10,4 @@ class Base:
|
|||
def __tablename__(cls) -> str:
|
||||
return cls.__name__.lower() + "s"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,3 +15,4 @@ async def get_db():
|
|||
finally:
|
||||
await session.close()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ from .project import Project, ProjectMember
|
|||
from .audit import AuditTask, AuditIssue
|
||||
from .analysis import InstantAnalysis
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -22,3 +22,4 @@ class InstantAnalysis(Base):
|
|||
# Relationships
|
||||
user = relationship("User", backref="instant_analyses")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -42,3 +42,4 @@ class ProjectMember(Base):
|
|||
project = relationship("Project", back_populates="members")
|
||||
user = relationship("User", backref="project_memberships")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -23,3 +23,4 @@ class User(Base):
|
|||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -28,3 +28,4 @@ class UserConfig(Base):
|
|||
# Relationships
|
||||
user = relationship("User", backref="config")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,3 +8,4 @@ class Token(BaseModel):
|
|||
class TokenPayload(BaseModel):
|
||||
sub: Optional[str] = None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -33,3 +33,4 @@ class UserInDBBase(UserBase):
|
|||
class User(UserInDBBase):
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -135,3 +135,4 @@ class BaiduAdapter(BaseLLMAdapter):
|
|||
def get_model(self) -> str:
|
||||
return self.config.model or "ERNIE-3.5-8K"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -91,3 +91,4 @@ class ClaudeAdapter(BaseLLMAdapter):
|
|||
raise Exception(f"无效的Claude模型: {self.config.model}")
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -79,3 +79,4 @@ class DeepSeekAdapter(BaseLLMAdapter):
|
|||
raise Exception("未指定DeepSeek模型")
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -85,3 +85,4 @@ class DoubaoAdapter(BaseLLMAdapter):
|
|||
def get_model(self) -> str:
|
||||
return self.config.model or "doubao-pro-32k"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -82,3 +82,4 @@ class MinimaxAdapter(BaseLLMAdapter):
|
|||
def get_model(self) -> str:
|
||||
return self.config.model or "abab6.5-chat"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -77,3 +77,4 @@ class MoonshotAdapter(BaseLLMAdapter):
|
|||
raise Exception("未指定Moonshot模型")
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -80,3 +80,4 @@ class OllamaAdapter(BaseLLMAdapter):
|
|||
raise Exception("未指定Ollama模型")
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -90,3 +90,4 @@ class OpenAIAdapter(BaseLLMAdapter):
|
|||
raise Exception("未指定OpenAI模型")
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -77,3 +77,4 @@ class QwenAdapter(BaseLLMAdapter):
|
|||
raise Exception("未指定通义千问模型")
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -77,3 +77,4 @@ class ZhipuAdapter(BaseLLMAdapter):
|
|||
raise Exception("未指定智谱AI模型")
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -132,3 +132,4 @@ class BaseLLMAdapter(ABC):
|
|||
await self._client.aclose()
|
||||
self._client = None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -118,3 +118,4 @@ DEFAULT_BASE_URLS: Dict[LLMProvider, str] = {
|
|||
LLMProvider.CLAUDE: "https://api.anthropic.com/v1",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,3 +10,4 @@ python-jose[cryptography]
|
|||
python-multipart
|
||||
httpx
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,3 +16,4 @@ EXPOSE 5173
|
|||
|
||||
CMD ["npm", "run", "dev", "--", "--host"]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/logo_xcodereviewer.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>XCodeReviewer</title>
|
||||
</head>
|
||||
|
||||
<body class="dark:bg-gray-900">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/app/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -45,6 +45,8 @@
|
|||
"embla-carousel-react": "^8.6.0",
|
||||
"eventsource-parser": "^3.0.6",
|
||||
"fflate": "^0.8.2",
|
||||
"i18next": "^25.6.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"ky": "^1.9.1",
|
||||
"lucide-react": "^0.525.0",
|
||||
|
|
@ -57,6 +59,7 @@
|
|||
"react-dom": "^18.0.0",
|
||||
"react-helmet-async": "^2.0.5",
|
||||
"react-hook-form": "^7.56.1",
|
||||
"react-i18next": "^16.3.5",
|
||||
"react-resizable-panels": "^2.1.8",
|
||||
"react-router": "^7.1.5",
|
||||
"react-router-dom": "^6.30.0",
|
||||
|
|
@ -6391,6 +6394,15 @@
|
|||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-url-attributes": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
|
||||
|
|
@ -6411,6 +6423,46 @@
|
|||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.6.3",
|
||||
"resolved": "https://registry.npmmirror.com/i18next/-/i18next-25.6.3.tgz",
|
||||
"integrity": "sha512-AEQvoPDljhp67a1+NsnG/Wb1Nh6YoSvtrmeEd24sfGn3uujCtXCF3cXpr7ulhMywKNFF7p3TX1u2j7y+caLOJg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-browser-languagedetector": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz",
|
||||
"integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
|
|
@ -8602,6 +8654,33 @@
|
|||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "16.3.5",
|
||||
"resolved": "https://registry.npmmirror.com/react-i18next/-/react-i18next-16.3.5.tgz",
|
||||
"integrity": "sha512-F7Kglc+T0aE6W2rO5eCAFBEuWRpNb5IFmXOYEgztjZEuiuSLTe/xBIEG6Q3S0fbl8GXMNo+Q7gF8bpokFNWJww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"html-parse-stringify": "^3.0.1",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 25.6.2",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
|
||||
|
|
@ -9684,7 +9763,7 @@
|
|||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.7.3.tgz",
|
||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
|
@ -10091,6 +10170,15 @@
|
|||
"vite": ">=2.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-jsonrpc": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@
|
|||
"embla-carousel-react": "^8.6.0",
|
||||
"eventsource-parser": "^3.0.6",
|
||||
"fflate": "^0.8.2",
|
||||
"i18next": "^25.6.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"ky": "^1.9.1",
|
||||
"lucide-react": "^0.525.0",
|
||||
|
|
@ -64,6 +66,7 @@
|
|||
"react-dom": "^18.0.0",
|
||||
"react-helmet-async": "^2.0.5",
|
||||
"react-hook-form": "^7.56.1",
|
||||
"react-i18next": "^16.3.5",
|
||||
"react-resizable-panels": "^2.1.8",
|
||||
"react-router": "^7.1.5",
|
||||
"react-router-dom": "^6.30.0",
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
|
||||
import { Toaster } from "sonner";
|
||||
import Header from "@/components/layout/Header";
|
||||
import routes, { type RouteConfig } from "./app/routes";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Toaster position="top-right" />
|
||||
<div className="min-h-screen gradient-bg">
|
||||
<Header />
|
||||
<main className="container-responsive py-4 md:py-6">
|
||||
<Routes>
|
||||
{routes.map((route: RouteConfig) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={route.element}
|
||||
/>
|
||||
))}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { BrowserRouter, Routes, Route, Navigate, Outlet } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom";
|
||||
import { Toaster } from "sonner";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Sidebar from "@/components/layout/Sidebar";
|
||||
import routes from "./routes";
|
||||
import { AuthProvider } from "@/shared/context/AuthContext";
|
||||
import { ProtectedRoute } from "./ProtectedRoute";
|
||||
|
|
@ -9,11 +10,18 @@ import Register from "@/pages/Register";
|
|||
import NotFound from "@/pages/NotFound";
|
||||
|
||||
function AppLayout() {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen gradient-bg">
|
||||
<Header />
|
||||
<main className="container-responsive py-4 md:py-6">
|
||||
<Sidebar collapsed={collapsed} setCollapsed={setCollapsed} />
|
||||
<main
|
||||
className={`transition-all duration-300 min-h-screen ${collapsed ? "md:ml-20" : "md:ml-64"
|
||||
}`}
|
||||
>
|
||||
<div className="container mx-auto px-4 py-6 md:py-8 pt-16 md:pt-8">
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,3 +16,4 @@ export const ProtectedRoute = () => {
|
|||
return <Outlet />;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,368 +0,0 @@
|
|||
/* stylelint-disable */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Definition of the design system. All colors, gradients, fonts, etc should be defined here.
|
||||
All colors MUST be HSL.
|
||||
|
||||
专业深红色配色方案:
|
||||
- 主色调:深红色 (0 75% 25%) - 体现专业性和权威感
|
||||
- 强调色:暗红色 (0 60% 35%) - 用于交互元素和重点突出
|
||||
- 背景色:浅灰到黑色渐变 - 提供高对比度和现代感
|
||||
- 状态色:保持语义化的绿色(成功)、红色(错误)、黄色(警告)
|
||||
*/
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 98%;
|
||||
--foreground: 0 0% 5%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 5%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 5%;
|
||||
|
||||
--primary: 0 75% 25%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 0 0% 95%;
|
||||
--secondary-foreground: 0 75% 25%;
|
||||
|
||||
--muted: 0 0% 94%;
|
||||
--muted-foreground: 0 0% 40%;
|
||||
|
||||
--accent: 0 60% 35%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 84% 60%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 0 0% 90%;
|
||||
--input: 0 0% 90%;
|
||||
--ring: 0 75% 25%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* 专业渐变效果 */
|
||||
--gradient-primary: linear-gradient(135deg, hsl(0 75% 25%) 0%, hsl(0 60% 35%) 100%);
|
||||
--gradient-card: linear-gradient(145deg, hsl(0 0% 100%) 0%, hsl(0 0% 98%) 100%);
|
||||
--gradient-background: linear-gradient(180deg, hsl(0 0% 98%) 0%, hsl(0 0% 95%) 100%);
|
||||
|
||||
/* 专业阴影效果 */
|
||||
--shadow-card: 0 1px 3px 0 hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
|
||||
--shadow-hover: 0 4px 6px -1px hsl(0 0% 0% / 0.1), 0 2px 4px -2px hsl(0 0% 0% / 0.1);
|
||||
--shadow-focus: 0 0 0 3px hsl(0 75% 25% / 0.1);
|
||||
|
||||
--sidebar-background: 0 0% 8%;
|
||||
--sidebar-foreground: 0 0% 85%;
|
||||
--sidebar-primary: 0 75% 35%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 0 0% 12%;
|
||||
--sidebar-accent-foreground: 0 0% 85%;
|
||||
--sidebar-border: 0 0% 15%;
|
||||
--sidebar-ring: 0 75% 45%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 0 0% 3%;
|
||||
--foreground: 0 0% 95%;
|
||||
|
||||
--card: 0 0% 5%;
|
||||
--card-foreground: 0 0% 95%;
|
||||
|
||||
--popover: 0 0% 5%;
|
||||
--popover-foreground: 0 0% 95%;
|
||||
|
||||
--primary: 0 75% 45%;
|
||||
--primary-foreground: 0 0% 5%;
|
||||
|
||||
--secondary: 0 0% 8%;
|
||||
--secondary-foreground: 0 0% 95%;
|
||||
|
||||
--muted: 0 0% 8%;
|
||||
--muted-foreground: 0 0% 60%;
|
||||
|
||||
--accent: 0 60% 50%;
|
||||
--accent-foreground: 0 0% 5%;
|
||||
|
||||
--destructive: 0 75% 55%;
|
||||
--destructive-foreground: 0 0% 95%;
|
||||
|
||||
--border: 0 0% 12%;
|
||||
--input: 0 0% 12%;
|
||||
--ring: 0 75% 45%;
|
||||
|
||||
/* 暗色模式专业渐变效果 */
|
||||
--gradient-primary: linear-gradient(135deg, hsl(0 75% 45%) 0%, hsl(0 60% 50%) 100%);
|
||||
--gradient-card: linear-gradient(145deg, hsl(0 0% 5%) 0%, hsl(0 0% 3%) 100%);
|
||||
--gradient-background: linear-gradient(180deg, hsl(0 0% 3%) 0%, hsl(0 0% 1%) 100%);
|
||||
|
||||
/* 暗色模式专业阴影效果 */
|
||||
--shadow-card: 0 1px 3px 0 hsl(0 0% 0% / 0.3), 0 1px 2px -1px hsl(0 0% 0% / 0.3);
|
||||
--shadow-hover: 0 4px 6px -1px hsl(0 0% 0% / 0.4), 0 2px 4px -2px hsl(0 0% 0% / 0.4);
|
||||
--shadow-focus: 0 0 0 3px hsl(0 75% 45% / 0.2);
|
||||
|
||||
--sidebar-background: 0 0% 2%;
|
||||
--sidebar-foreground: 0 0% 85%;
|
||||
--sidebar-primary: 0 75% 50%;
|
||||
--sidebar-primary-foreground: 0 0% 5%;
|
||||
--sidebar-accent: 0 0% 6%;
|
||||
--sidebar-accent-foreground: 0 0% 85%;
|
||||
--sidebar-border: 0 0% 8%;
|
||||
--sidebar-ring: 0 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* 页面标题样式 */
|
||||
.page-title {
|
||||
@apply text-2xl md:text-3xl font-bold text-foreground tracking-tight;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
@apply text-sm md:text-base text-muted-foreground mt-1;
|
||||
}
|
||||
|
||||
/* 现代化卡片样式 */
|
||||
.card-modern {
|
||||
@apply bg-card rounded-xl border border-border shadow-sm hover:shadow-md transition-all duration-200;
|
||||
}
|
||||
|
||||
/* 统计卡片样式 */
|
||||
.stat-card {
|
||||
@apply bg-card rounded-xl border border-border shadow-sm hover:shadow-lg transition-all duration-300 hover:border-accent/30;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
@apply text-xs font-medium text-muted-foreground uppercase tracking-wide;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
@apply text-2xl md:text-3xl font-bold text-foreground mt-1;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
@apply w-12 h-12 rounded-lg bg-gradient-to-br from-primary/10 to-accent/10 flex items-center justify-center shadow-sm;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn-primary {
|
||||
@apply bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90 text-primary-foreground shadow-sm hover:shadow-md transition-all duration-200;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply border-border hover:border-accent/50 hover:bg-accent/5 transition-all duration-200;
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-state {
|
||||
@apply flex flex-col items-center justify-center text-center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
@apply w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-4;
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 渐变背景 */
|
||||
.gradient-bg {
|
||||
@apply bg-gradient-to-br from-background via-muted/30 to-accent/5;
|
||||
}
|
||||
|
||||
/* 响应式容器 */
|
||||
.container-responsive {
|
||||
@apply container mx-auto px-4 sm:px-6 lg:px-8;
|
||||
}
|
||||
|
||||
/* 文本截断 */
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 毛玻璃效果 */
|
||||
.backdrop-blur-md {
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
/* 平滑滚动 */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: hsl(var(--muted-foreground) / 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: hsl(var(--accent) / 0.6);
|
||||
}
|
||||
|
||||
/* 专业高级感样式 */
|
||||
.professional-card {
|
||||
@apply bg-gradient-card border border-border/50 shadow-card hover:shadow-hover transition-all duration-300;
|
||||
}
|
||||
|
||||
.professional-button {
|
||||
@apply bg-gradient-primary text-primary-foreground font-semibold px-6 py-3 rounded-lg shadow-sm hover:shadow-md transition-all duration-200 focus:ring-2 focus:ring-primary/20 focus:outline-none;
|
||||
}
|
||||
|
||||
.professional-input {
|
||||
@apply bg-background border border-border rounded-lg px-4 py-2 focus:border-primary focus:ring-2 focus:ring-primary/10 transition-all duration-200;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
@apply bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
@apply bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
@apply bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
@apply bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400;
|
||||
}
|
||||
|
||||
/* 高级导航样式 */
|
||||
.nav-link {
|
||||
@apply flex items-center px-3 py-2 text-sm font-medium rounded-lg transition-all duration-200 hover:bg-accent/10 hover:text-accent-foreground;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
@apply bg-primary/10 text-primary border-r-2 border-primary;
|
||||
}
|
||||
|
||||
/* 专业表格样式 */
|
||||
.professional-table {
|
||||
@apply w-full border-collapse bg-card rounded-lg overflow-hidden shadow-card;
|
||||
}
|
||||
|
||||
.professional-table th {
|
||||
@apply bg-muted/50 px-6 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider border-b border-border;
|
||||
}
|
||||
|
||||
.professional-table td {
|
||||
@apply px-6 py-4 whitespace-nowrap text-sm text-foreground border-b border-border/50;
|
||||
}
|
||||
|
||||
.professional-table tr:hover {
|
||||
@apply bg-accent/5;
|
||||
}
|
||||
|
||||
/* 图表和图标专用样式 */
|
||||
.chart-primary {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.chart-accent {
|
||||
color: hsl(var(--accent));
|
||||
}
|
||||
|
||||
.chart-success {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.chart-warning {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.chart-error {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.icon-primary {
|
||||
@apply text-primary;
|
||||
}
|
||||
|
||||
.icon-accent {
|
||||
@apply text-accent;
|
||||
}
|
||||
|
||||
.icon-muted {
|
||||
@apply text-muted-foreground;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading-spinner {
|
||||
@apply animate-spin rounded-full border-2 border-primary border-t-transparent;
|
||||
}
|
||||
|
||||
/* 状态指示器颜色 */
|
||||
.status-running {
|
||||
@apply bg-red-50 text-red-800 border-red-200;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
@apply bg-green-50 text-green-800 border-green-200;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
@apply bg-red-100 text-red-900 border-red-300;
|
||||
}
|
||||
|
||||
/* 渐变图标背景 */
|
||||
.icon-bg-primary {
|
||||
@apply bg-gradient-to-br from-primary/10 to-accent/10;
|
||||
}
|
||||
|
||||
.icon-bg-success {
|
||||
@apply bg-gradient-to-br from-green-100 to-emerald-100;
|
||||
}
|
||||
|
||||
.icon-bg-warning {
|
||||
@apply bg-gradient-to-br from-yellow-100 to-orange-100;
|
||||
}
|
||||
|
||||
.icon-bg-error {
|
||||
@apply bg-gradient-to-br from-red-100 to-red-200;
|
||||
}
|
||||
}
|
||||
|
|
@ -338,8 +338,7 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
|||
filteredProjects.map((project) => (
|
||||
<Card
|
||||
key={project.id}
|
||||
className={`cursor-pointer transition-all hover:shadow-md ${
|
||||
taskForm.project_id === project.id
|
||||
className={`cursor-pointer transition-all hover:shadow-md ${taskForm.project_id === project.id
|
||||
? 'ring-2 ring-primary bg-primary/5'
|
||||
: 'hover:bg-gray-50'
|
||||
}`}
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
import React from "react";
|
||||
import { Code } from "lucide-react";
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="bg-white border-t border-gray-200/60 mt-16">
|
||||
<div className="container-responsive py-8">
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center space-x-2 mb-4">
|
||||
<div className="w-6 h-6 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center">
|
||||
<Code className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<span className="text-lg font-semibold text-gray-900">XCodeReviewer</span>
|
||||
</div>
|
||||
<p className="text-gray-500 text-sm">
|
||||
© {currentYear} XCodeReviewer. 致力于提升代码质量,保障软件安全.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
import { useState } from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Menu,
|
||||
X
|
||||
} from "lucide-react";
|
||||
import routes from "@/app/routes";
|
||||
|
||||
export default function Header() {
|
||||
const location = useLocation();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
const visibleRoutes = routes.filter(route => route.visible !== false);
|
||||
|
||||
return (
|
||||
<header className="bg-white/80 backdrop-blur-md shadow-sm border-b border-gray-200/60 sticky top-0 z-50">
|
||||
<div className="container-responsive">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
{/* Logo */}
|
||||
<Link to="/" className="flex items-center space-x-3 group">
|
||||
<img
|
||||
src="/logo_xcodereviewer.png"
|
||||
alt="XCodeReviewer Logo"
|
||||
className="w-9 h-9 rounded-xl shadow-sm group-hover:shadow-md transition-all"
|
||||
/>
|
||||
<span className="text-xl font-bold text-gray-900 group-hover:text-primary transition-colors">XCodeReviewer</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex items-center space-x-1">
|
||||
{visibleRoutes.map((route) => (
|
||||
<Link
|
||||
key={route.path}
|
||||
to={route.path}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-lg transition-all ${
|
||||
location.pathname === route.path
|
||||
? "text-primary bg-red-50"
|
||||
: "text-gray-700 hover:text-primary hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
{route.name}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* User Menu */}
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* GitHub Link */}
|
||||
<a
|
||||
href="https://github.com/lintsinghua/XCodeReviewer"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hidden md:flex items-center justify-center w-9 h-9 rounded-lg text-gray-700 hover:text-gray-900 hover:bg-gray-100 transition-all"
|
||||
title="GitHub Repository"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="md:hidden"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
>
|
||||
{mobileMenuOpen ? (
|
||||
<X className="w-5 h-5" />
|
||||
) : (
|
||||
<Menu className="w-5 h-5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="md:hidden border-t bg-white">
|
||||
<div className="px-2 pt-2 pb-3 space-y-1">
|
||||
{visibleRoutes.map((route) => (
|
||||
<Link
|
||||
key={route.path}
|
||||
to={route.path}
|
||||
className={`block px-3 py-2 text-base font-medium rounded-md transition-colors ${
|
||||
location.pathname === route.path
|
||||
? "text-primary bg-red-50"
|
||||
: "text-gray-700 hover:text-primary hover:bg-gray-50"
|
||||
}`}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{route.name}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{/* GitHub Link for Mobile */}
|
||||
<a
|
||||
href="https://github.com/lintsinghua/XCodeReviewer"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center px-3 py-2 text-base font-medium text-gray-700 hover:text-primary hover:bg-gray-50 rounded-md transition-colors"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" />
|
||||
</svg>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
import { useState } from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Menu,
|
||||
X,
|
||||
LayoutDashboard,
|
||||
FolderGit2,
|
||||
Zap,
|
||||
ListTodo,
|
||||
Settings,
|
||||
Trash2,
|
||||
FileText,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Github
|
||||
} from "lucide-react";
|
||||
import routes from "@/app/routes";
|
||||
|
||||
// Icon mapping for routes
|
||||
const routeIcons: Record<string, React.ReactNode> = {
|
||||
"/": <LayoutDashboard className="w-5 h-5" />,
|
||||
"/projects": <FolderGit2 className="w-5 h-5" />,
|
||||
"/instant-analysis": <Zap className="w-5 h-5" />,
|
||||
"/audit-tasks": <ListTodo className="w-5 h-5" />,
|
||||
"/admin": <Settings className="w-5 h-5" />,
|
||||
"/recycle-bin": <Trash2 className="w-5 h-5" />,
|
||||
"/logs": <FileText className="w-5 h-5" />,
|
||||
};
|
||||
|
||||
interface SidebarProps {
|
||||
collapsed: boolean;
|
||||
setCollapsed: (collapsed: boolean) => void;
|
||||
}
|
||||
|
||||
export default function Sidebar({ collapsed, setCollapsed }: SidebarProps) {
|
||||
const location = useLocation();
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
const visibleRoutes = routes.filter(route => route.visible !== false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile Menu Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="fixed top-4 left-4 z-50 md:hidden bg-white shadow-md border border-gray-200 text-gray-700 hover:bg-gray-50"
|
||||
onClick={() => setMobileOpen(!mobileOpen)}
|
||||
>
|
||||
{mobileOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
|
||||
</Button>
|
||||
|
||||
{/* Overlay for mobile */}
|
||||
{mobileOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/20 backdrop-blur-sm z-40 md:hidden transition-opacity"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
className={`
|
||||
fixed top-0 left-0 h-screen bg-white/95 backdrop-blur-xl
|
||||
border-r border-gray-200/80 shadow-[4px_0_24px_-12px_rgba(0,0,0,0.1)]
|
||||
z-40 transition-all duration-300 ease-in-out
|
||||
${collapsed ? "w-20" : "w-64"}
|
||||
${mobileOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"}
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Logo Section */}
|
||||
<div className="relative flex items-center h-[72px] px-4 border-b border-gray-100">
|
||||
<Link
|
||||
to="/"
|
||||
className={`flex items-center space-x-3 group overflow-hidden transition-all duration-300 ${collapsed ? 'justify-center w-full' : ''}`}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
>
|
||||
<div className="relative flex-shrink-0">
|
||||
<div className="absolute inset-0 bg-red-500/20 blur-lg rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
<img
|
||||
src="/logo_xcodereviewer.png"
|
||||
alt="XCodeReviewer Logo"
|
||||
className="w-9 h-9 rounded-xl shadow-sm relative z-10 group-hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
</div>
|
||||
<div className={`transition-all duration-300 ${collapsed ? 'w-0 opacity-0 overflow-hidden' : 'w-auto opacity-100'}`}>
|
||||
<span className="text-lg font-bold bg-clip-text text-transparent bg-gradient-to-r from-gray-900 to-gray-700 group-hover:from-red-700 group-hover:to-red-500 whitespace-nowrap">
|
||||
XCodeReviewer
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Collapse button for desktop - Absolute Positioned */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="hidden md:flex absolute -right-3 top-1/2 -translate-y-1/2 w-6 h-6 rounded-full bg-white text-gray-400 hover:text-gray-900 border border-gray-200 shadow-sm z-50 hover:bg-gray-50"
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
>
|
||||
{collapsed ? (
|
||||
<ChevronRight className="w-3 h-3" />
|
||||
) : (
|
||||
<ChevronLeft className="w-3 h-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 overflow-y-auto py-6 px-3">
|
||||
<div className="space-y-1.5">
|
||||
{visibleRoutes.map((route) => {
|
||||
const isActive = location.pathname === route.path;
|
||||
return (
|
||||
<Link
|
||||
key={route.path}
|
||||
to={route.path}
|
||||
className={`
|
||||
flex items-center space-x-3 px-3 py-2.5 rounded-xl
|
||||
transition-all duration-200 group relative overflow-hidden
|
||||
${isActive
|
||||
? "bg-red-50 text-red-700 font-medium shadow-sm ring-1 ring-red-100"
|
||||
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900"
|
||||
}
|
||||
`}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
title={collapsed ? route.name : undefined}
|
||||
>
|
||||
{/* Active Indicator Bar */}
|
||||
{isActive && (
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-red-600 rounded-r-full opacity-0 md:opacity-100 transition-opacity" />
|
||||
)}
|
||||
|
||||
{/* Icon */}
|
||||
<span className={`
|
||||
flex-shrink-0 transition-colors duration-200
|
||||
${isActive ? "text-red-600" : "text-gray-400 group-hover:text-gray-600"}
|
||||
`}>
|
||||
{routeIcons[route.path] || <LayoutDashboard className="w-5 h-5" />}
|
||||
</span>
|
||||
|
||||
{/* Label */}
|
||||
{!collapsed && (
|
||||
<span className="whitespace-nowrap z-10">
|
||||
{route.name}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Hover Effect Background (Subtle) */}
|
||||
{!isActive && (
|
||||
<div className="absolute inset-0 bg-gray-100 opacity-0 group-hover:opacity-100 transition-opacity duration-200 -z-0 rounded-xl" />
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Footer with GitHub Link */}
|
||||
<div className="p-4 border-t border-gray-100 bg-gray-50/50">
|
||||
<a
|
||||
href="https://github.com/lintsinghua/XCodeReviewer"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`
|
||||
flex items-center space-x-3 px-3 py-2.5 rounded-xl
|
||||
text-gray-500 hover:bg-white hover:text-gray-900 hover:shadow-sm hover:ring-1 hover:ring-gray-200
|
||||
transition-all duration-200 group
|
||||
`}
|
||||
title={collapsed ? "GitHub Repository" : undefined}
|
||||
>
|
||||
<Github className="w-5 h-5 flex-shrink-0 text-gray-400 group-hover:text-gray-900 transition-colors" />
|
||||
{!collapsed && (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-sm">GitHub</span>
|
||||
<span className="text-xs text-gray-400 group-hover:text-gray-500">View Source</span>
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
/**
|
||||
* 示例页面
|
||||
*/
|
||||
|
||||
import PageMeta from "@/components/layout/PageMeta";
|
||||
|
||||
export default function SamplePage() {
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="首页" description="首页介绍" />
|
||||
<div>
|
||||
<h3>这是一个示例页面</h3>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue