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
|
# Command is overridden by docker-compose for dev
|
||||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
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
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
datefmt = %H:%M:%S
|
datefmt = %H:%M:%S
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,3 +88,4 @@ if context.is_offline_mode():
|
||||||
else:
|
else:
|
||||||
asyncio.run(run_migrations_online())
|
asyncio.run(run_migrations_online())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,3 +23,4 @@ def upgrade() -> None:
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
${downgrades if downgrades else "pass"}
|
${downgrades if downgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -208,3 +208,4 @@ async def remove_project_member(
|
||||||
|
|
||||||
return {"message": "成员已移除"}
|
return {"message": "成员已移除"}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,3 +64,4 @@ async def read_user_me(
|
||||||
"""
|
"""
|
||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,3 +27,4 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
def get_password_hash(password: str) -> str:
|
def get_password_hash(password: str) -> str:
|
||||||
return pwd_context.hash(password)
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,4 @@ class Base:
|
||||||
def __tablename__(cls) -> str:
|
def __tablename__(cls) -> str:
|
||||||
return cls.__name__.lower() + "s"
|
return cls.__name__.lower() + "s"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,3 +15,4 @@ async def get_db():
|
||||||
finally:
|
finally:
|
||||||
await session.close()
|
await session.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,4 @@ from .project import Project, ProjectMember
|
||||||
from .audit import AuditTask, AuditIssue
|
from .audit import AuditTask, AuditIssue
|
||||||
from .analysis import InstantAnalysis
|
from .analysis import InstantAnalysis
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,3 +22,4 @@ class InstantAnalysis(Base):
|
||||||
# Relationships
|
# Relationships
|
||||||
user = relationship("User", backref="instant_analyses")
|
user = relationship("User", backref="instant_analyses")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,3 +42,4 @@ class ProjectMember(Base):
|
||||||
project = relationship("Project", back_populates="members")
|
project = relationship("Project", back_populates="members")
|
||||||
user = relationship("User", backref="project_memberships")
|
user = relationship("User", backref="project_memberships")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,3 +23,4 @@ class User(Base):
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,3 +28,4 @@ class UserConfig(Base):
|
||||||
# Relationships
|
# Relationships
|
||||||
user = relationship("User", backref="config")
|
user = relationship("User", backref="config")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,3 +8,4 @@ class Token(BaseModel):
|
||||||
class TokenPayload(BaseModel):
|
class TokenPayload(BaseModel):
|
||||||
sub: Optional[str] = None
|
sub: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,3 +33,4 @@ class UserInDBBase(UserBase):
|
||||||
class User(UserInDBBase):
|
class User(UserInDBBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,3 +135,4 @@ class BaiduAdapter(BaseLLMAdapter):
|
||||||
def get_model(self) -> str:
|
def get_model(self) -> str:
|
||||||
return self.config.model or "ERNIE-3.5-8K"
|
return self.config.model or "ERNIE-3.5-8K"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,3 +91,4 @@ class ClaudeAdapter(BaseLLMAdapter):
|
||||||
raise Exception(f"无效的Claude模型: {self.config.model}")
|
raise Exception(f"无效的Claude模型: {self.config.model}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,3 +79,4 @@ class DeepSeekAdapter(BaseLLMAdapter):
|
||||||
raise Exception("未指定DeepSeek模型")
|
raise Exception("未指定DeepSeek模型")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,3 +85,4 @@ class DoubaoAdapter(BaseLLMAdapter):
|
||||||
def get_model(self) -> str:
|
def get_model(self) -> str:
|
||||||
return self.config.model or "doubao-pro-32k"
|
return self.config.model or "doubao-pro-32k"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,3 +82,4 @@ class MinimaxAdapter(BaseLLMAdapter):
|
||||||
def get_model(self) -> str:
|
def get_model(self) -> str:
|
||||||
return self.config.model or "abab6.5-chat"
|
return self.config.model or "abab6.5-chat"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,3 +77,4 @@ class MoonshotAdapter(BaseLLMAdapter):
|
||||||
raise Exception("未指定Moonshot模型")
|
raise Exception("未指定Moonshot模型")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,3 +80,4 @@ class OllamaAdapter(BaseLLMAdapter):
|
||||||
raise Exception("未指定Ollama模型")
|
raise Exception("未指定Ollama模型")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,3 +90,4 @@ class OpenAIAdapter(BaseLLMAdapter):
|
||||||
raise Exception("未指定OpenAI模型")
|
raise Exception("未指定OpenAI模型")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,3 +77,4 @@ class QwenAdapter(BaseLLMAdapter):
|
||||||
raise Exception("未指定通义千问模型")
|
raise Exception("未指定通义千问模型")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,3 +77,4 @@ class ZhipuAdapter(BaseLLMAdapter):
|
||||||
raise Exception("未指定智谱AI模型")
|
raise Exception("未指定智谱AI模型")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -132,3 +132,4 @@ class BaseLLMAdapter(ABC):
|
||||||
await self._client.aclose()
|
await self._client.aclose()
|
||||||
self._client = None
|
self._client = None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -118,3 +118,4 @@ DEFAULT_BASE_URLS: Dict[LLMProvider, str] = {
|
||||||
LLMProvider.CLAUDE: "https://api.anthropic.com/v1",
|
LLMProvider.CLAUDE: "https://api.anthropic.com/v1",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,4 @@ python-jose[cryptography]
|
||||||
python-multipart
|
python-multipart
|
||||||
httpx
|
httpx
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,4 @@ EXPOSE 5173
|
||||||
|
|
||||||
CMD ["npm", "run", "dev", "--", "--host"]
|
CMD ["npm", "run", "dev", "--", "--host"]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
<head>
|
||||||
<link rel="icon" type="image/png" href="/logo_xcodereviewer.png" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<link rel="icon" type="image/png" href="/logo_xcodereviewer.png" />
|
||||||
<title>XCodeReviewer</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
</head>
|
<title>XCodeReviewer</title>
|
||||||
<body class="dark:bg-gray-900">
|
</head>
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/app/main.tsx"></script>
|
<body class="dark:bg-gray-900">
|
||||||
</body>
|
<div id="root"></div>
|
||||||
</html>
|
<script type="module" src="/src/app/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -45,6 +45,8 @@
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"eventsource-parser": "^3.0.6",
|
"eventsource-parser": "^3.0.6",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
|
"i18next": "^25.6.3",
|
||||||
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"ky": "^1.9.1",
|
"ky": "^1.9.1",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
|
|
@ -57,6 +59,7 @@
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0",
|
||||||
"react-helmet-async": "^2.0.5",
|
"react-helmet-async": "^2.0.5",
|
||||||
"react-hook-form": "^7.56.1",
|
"react-hook-form": "^7.56.1",
|
||||||
|
"react-i18next": "^16.3.5",
|
||||||
"react-resizable-panels": "^2.1.8",
|
"react-resizable-panels": "^2.1.8",
|
||||||
"react-router": "^7.1.5",
|
"react-router": "^7.1.5",
|
||||||
"react-router-dom": "^6.30.0",
|
"react-router-dom": "^6.30.0",
|
||||||
|
|
@ -6391,6 +6394,15 @@
|
||||||
"url": "https://opencollective.com/unified"
|
"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": {
|
"node_modules/html-url-attributes": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
|
"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"
|
"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": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
|
@ -8602,6 +8654,33 @@
|
||||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
"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": {
|
"node_modules/react-is": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
|
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
|
||||||
|
|
@ -9684,7 +9763,7 @@
|
||||||
"version": "5.7.3",
|
"version": "5.7.3",
|
||||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.7.3.tgz",
|
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.7.3.tgz",
|
||||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
|
|
@ -10091,6 +10170,15 @@
|
||||||
"vite": ">=2.6.0"
|
"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": {
|
"node_modules/vscode-jsonrpc": {
|
||||||
"version": "8.2.0",
|
"version": "8.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,8 @@
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"eventsource-parser": "^3.0.6",
|
"eventsource-parser": "^3.0.6",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
|
"i18next": "^25.6.3",
|
||||||
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"ky": "^1.9.1",
|
"ky": "^1.9.1",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
|
|
@ -64,6 +66,7 @@
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0",
|
||||||
"react-helmet-async": "^2.0.5",
|
"react-helmet-async": "^2.0.5",
|
||||||
"react-hook-form": "^7.56.1",
|
"react-hook-form": "^7.56.1",
|
||||||
|
"react-i18next": "^16.3.5",
|
||||||
"react-resizable-panels": "^2.1.8",
|
"react-resizable-panels": "^2.1.8",
|
||||||
"react-router": "^7.1.5",
|
"react-router": "^7.1.5",
|
||||||
"react-router-dom": "^6.30.0",
|
"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 { Toaster } from "sonner";
|
||||||
import Header from "@/components/layout/Header";
|
import Sidebar from "@/components/layout/Sidebar";
|
||||||
import routes from "./routes";
|
import routes from "./routes";
|
||||||
import { AuthProvider } from "@/shared/context/AuthContext";
|
import { AuthProvider } from "@/shared/context/AuthContext";
|
||||||
import { ProtectedRoute } from "./ProtectedRoute";
|
import { ProtectedRoute } from "./ProtectedRoute";
|
||||||
|
|
@ -9,12 +10,19 @@ import Register from "@/pages/Register";
|
||||||
import NotFound from "@/pages/NotFound";
|
import NotFound from "@/pages/NotFound";
|
||||||
|
|
||||||
function AppLayout() {
|
function AppLayout() {
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen gradient-bg">
|
<div className="min-h-screen gradient-bg">
|
||||||
<Header />
|
<Sidebar collapsed={collapsed} setCollapsed={setCollapsed} />
|
||||||
<main className="container-responsive py-4 md:py-6">
|
<main
|
||||||
<Outlet />
|
className={`transition-all duration-300 min-h-screen ${collapsed ? "md:ml-20" : "md:ml-64"
|
||||||
</main>
|
}`}
|
||||||
|
>
|
||||||
|
<div className="container mx-auto px-4 py-6 md:py-8 pt-16 md:pt-8">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -28,17 +36,17 @@ function App() {
|
||||||
{/* Public Routes */}
|
{/* Public Routes */}
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
|
|
||||||
{/* Protected Routes */}
|
{/* Protected Routes */}
|
||||||
<Route element={<ProtectedRoute />}>
|
<Route element={<ProtectedRoute />}>
|
||||||
<Route element={<AppLayout />}>
|
<Route element={<AppLayout />}>
|
||||||
{routes.map((route) => (
|
{routes.map((route) => (
|
||||||
<Route
|
<Route
|
||||||
key={route.path}
|
key={route.path}
|
||||||
path={route.path}
|
path={route.path}
|
||||||
element={route.element}
|
element={route.element}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,4 @@ export const ProtectedRoute = () => {
|
||||||
return <Outlet />;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,11 +8,11 @@ import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import {
|
import {
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Settings,
|
Settings,
|
||||||
FileText,
|
FileText,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Info,
|
Info,
|
||||||
Zap,
|
Zap,
|
||||||
Shield,
|
Shield,
|
||||||
|
|
@ -43,7 +43,7 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
const [zipFile, setZipFile] = useState<File | null>(null);
|
const [zipFile, setZipFile] = useState<File | null>(null);
|
||||||
const [loadingZipFile, setLoadingZipFile] = useState(false);
|
const [loadingZipFile, setLoadingZipFile] = useState(false);
|
||||||
const [hasLoadedZip, setHasLoadedZip] = useState(false);
|
const [hasLoadedZip, setHasLoadedZip] = useState(false);
|
||||||
|
|
||||||
const [taskForm, setTaskForm] = useState<CreateAuditTaskForm>({
|
const [taskForm, setTaskForm] = useState<CreateAuditTaskForm>({
|
||||||
project_id: "",
|
project_id: "",
|
||||||
task_type: "repository",
|
task_type: "repository",
|
||||||
|
|
@ -77,7 +77,7 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
// 后端 MAX_FILE_SIZE_BYTES 是 200 * 1024 = 204800 bytes = 200KB
|
// 后端 MAX_FILE_SIZE_BYTES 是 200 * 1024 = 204800 bytes = 200KB
|
||||||
// 转换为KB用于前端显示
|
// 转换为KB用于前端显示
|
||||||
const maxFileSizeKB = 200; // 后端默认值 200KB
|
const maxFileSizeKB = 200; // 后端默认值 200KB
|
||||||
|
|
||||||
setTaskForm(prev => ({
|
setTaskForm(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
scan_config: {
|
scan_config: {
|
||||||
|
|
@ -111,14 +111,14 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const autoLoadZipFile = async () => {
|
const autoLoadZipFile = async () => {
|
||||||
if (!taskForm.project_id || hasLoadedZip) return;
|
if (!taskForm.project_id || hasLoadedZip) return;
|
||||||
|
|
||||||
const project = projects.find(p => p.id === taskForm.project_id);
|
const project = projects.find(p => p.id === taskForm.project_id);
|
||||||
if (!project || project.repository_type !== 'other') return;
|
if (!project || project.repository_type !== 'other') return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoadingZipFile(true);
|
setLoadingZipFile(true);
|
||||||
const savedFile = await loadZipFile(taskForm.project_id);
|
const savedFile = await loadZipFile(taskForm.project_id);
|
||||||
|
|
||||||
if (savedFile) {
|
if (savedFile) {
|
||||||
setZipFile(savedFile);
|
setZipFile(savedFile);
|
||||||
setHasLoadedZip(true);
|
setHasLoadedZip(true);
|
||||||
|
|
@ -167,11 +167,11 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setCreating(true);
|
setCreating(true);
|
||||||
|
|
||||||
console.log('🎯 开始创建审计任务...', {
|
console.log('🎯 开始创建审计任务...', {
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
repositoryType: project.repository_type
|
repositoryType: project.repository_type
|
||||||
});
|
});
|
||||||
|
|
||||||
let taskId: string;
|
let taskId: string;
|
||||||
|
|
@ -183,7 +183,7 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
toast.error("请上传ZIP文件进行扫描");
|
toast.error("请上传ZIP文件进行扫描");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('📦 调用 scanZipFile...');
|
console.log('📦 调用 scanZipFile...');
|
||||||
taskId = await scanZipFile({
|
taskId = await scanZipFile({
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
|
|
@ -194,7 +194,7 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
} else {
|
} else {
|
||||||
// GitHub/GitLab等远程仓库
|
// GitHub/GitLab等远程仓库
|
||||||
console.log('📡 调用 runRepositoryAudit...');
|
console.log('📡 调用 runRepositoryAudit...');
|
||||||
|
|
||||||
// 后端会从用户配置中读取 GitHub/GitLab Token,前端不需要传递
|
// 后端会从用户配置中读取 GitHub/GitLab Token,前端不需要传递
|
||||||
taskId = await runRepositoryAudit({
|
taskId = await runRepositoryAudit({
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
|
|
@ -204,9 +204,9 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
createdBy: 'local-user'
|
createdBy: 'local-user'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ 任务创建成功:', taskId);
|
console.log('✅ 任务创建成功:', taskId);
|
||||||
|
|
||||||
// 记录用户操作
|
// 记录用户操作
|
||||||
import('@/shared/utils/logger').then(({ logger, LogCategory }) => {
|
import('@/shared/utils/logger').then(({ logger, LogCategory }) => {
|
||||||
logger.logUserAction('创建审计任务', {
|
logger.logUserAction('创建审计任务', {
|
||||||
|
|
@ -218,25 +218,25 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
hasZipFile: !!zipFile,
|
hasZipFile: !!zipFile,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 关闭创建对话框
|
// 关闭创建对话框
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
resetForm();
|
resetForm();
|
||||||
onTaskCreated();
|
onTaskCreated();
|
||||||
|
|
||||||
// 显示终端进度窗口
|
// 显示终端进度窗口
|
||||||
setCurrentTaskId(taskId);
|
setCurrentTaskId(taskId);
|
||||||
setShowTerminalDialog(true);
|
setShowTerminalDialog(true);
|
||||||
|
|
||||||
toast.success("审计任务已创建并启动");
|
toast.success("审计任务已创建并启动");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 创建任务失败:', error);
|
console.error('❌ 创建任务失败:', error);
|
||||||
|
|
||||||
// 记录错误并显示详细信息
|
// 记录错误并显示详细信息
|
||||||
import('@/shared/utils/errorHandler').then(({ handleError }) => {
|
import('@/shared/utils/errorHandler').then(({ handleError }) => {
|
||||||
handleError(error, '创建审计任务失败');
|
handleError(error, '创建审计任务失败');
|
||||||
});
|
});
|
||||||
|
|
||||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||||
toast.error(`创建任务失败: ${errorMessage}`);
|
toast.error(`创建任务失败: ${errorMessage}`);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -336,13 +336,12 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
</div>
|
</div>
|
||||||
) : filteredProjects.length > 0 ? (
|
) : filteredProjects.length > 0 ? (
|
||||||
filteredProjects.map((project) => (
|
filteredProjects.map((project) => (
|
||||||
<Card
|
<Card
|
||||||
key={project.id}
|
key={project.id}
|
||||||
className={`cursor-pointer transition-all hover:shadow-md ${
|
className={`cursor-pointer transition-all hover:shadow-md ${taskForm.project_id === project.id
|
||||||
taskForm.project_id === project.id
|
? 'ring-2 ring-primary bg-primary/5'
|
||||||
? 'ring-2 ring-primary bg-primary/5'
|
|
||||||
: 'hover:bg-gray-50'
|
: 'hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setTaskForm({ ...taskForm, project_id: project.id })}
|
onClick={() => setTaskForm({ ...taskForm, project_id: project.id })}
|
||||||
>
|
>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
|
|
@ -415,11 +414,11 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
<p className="font-medium text-green-900 text-sm">已准备就绪</p>
|
<p className="font-medium text-green-900 text-sm">已准备就绪</p>
|
||||||
<p className="text-xs text-green-700 mt-1">
|
<p className="text-xs text-green-700 mt-1">
|
||||||
使用保存的ZIP文件: {zipFile.name} (
|
使用保存的ZIP文件: {zipFile.name} (
|
||||||
{zipFile.size >= 1024 * 1024
|
{zipFile.size >= 1024 * 1024
|
||||||
? `${(zipFile.size / 1024 / 1024).toFixed(2)} MB`
|
? `${(zipFile.size / 1024 / 1024).toFixed(2)} MB`
|
||||||
: zipFile.size >= 1024
|
: zipFile.size >= 1024
|
||||||
? `${(zipFile.size / 1024).toFixed(2)} KB`
|
? `${(zipFile.size / 1024).toFixed(2)} KB`
|
||||||
: `${zipFile.size} B`
|
: `${zipFile.size} B`
|
||||||
})
|
})
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -445,7 +444,7 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="zipFile">上传ZIP文件</Label>
|
<Label htmlFor="zipFile">上传ZIP文件</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -461,7 +460,7 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
type: file.type,
|
type: file.type,
|
||||||
sizeMB: (file.size / 1024 / 1024).toFixed(2)
|
sizeMB: (file.size / 1024 / 1024).toFixed(2)
|
||||||
});
|
});
|
||||||
|
|
||||||
const validation = validateZipFile(file);
|
const validation = validateZipFile(file);
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
toast.error(validation.error || "文件无效");
|
toast.error(validation.error || "文件无效");
|
||||||
|
|
@ -470,11 +469,11 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
}
|
}
|
||||||
setZipFile(file);
|
setZipFile(file);
|
||||||
setHasLoadedZip(true);
|
setHasLoadedZip(true);
|
||||||
|
|
||||||
const sizeMB = (file.size / 1024 / 1024).toFixed(2);
|
const sizeMB = (file.size / 1024 / 1024).toFixed(2);
|
||||||
const sizeKB = (file.size / 1024).toFixed(2);
|
const sizeKB = (file.size / 1024).toFixed(2);
|
||||||
const sizeText = file.size >= 1024 * 1024 ? `${sizeMB} MB` : `${sizeKB} KB`;
|
const sizeText = file.size >= 1024 * 1024 ? `${sizeMB} MB` : `${sizeKB} KB`;
|
||||||
|
|
||||||
toast.success(`已选择文件: ${file.name} (${sizeText})`);
|
toast.success(`已选择文件: ${file.name} (${sizeText})`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
@ -491,8 +490,8 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="task_type">任务类型</Label>
|
<Label htmlFor="task_type">任务类型</Label>
|
||||||
<Select
|
<Select
|
||||||
value={taskForm.task_type}
|
value={taskForm.task_type}
|
||||||
onValueChange={(value: any) => setTaskForm({ ...taskForm, task_type: value })}
|
onValueChange={(value: any) => setTaskForm({ ...taskForm, task_type: value })}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
|
|
@ -588,8 +587,8 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
const input = e.currentTarget.previousElementSibling as HTMLInputElement;
|
const input = e.currentTarget.previousElementSibling as HTMLInputElement;
|
||||||
|
|
@ -608,9 +607,9 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
<Label>已选择的排除模式</Label>
|
<Label>已选择的排除模式</Label>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{taskForm.exclude_patterns.map((pattern) => (
|
{taskForm.exclude_patterns.map((pattern) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={pattern}
|
key={pattern}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="cursor-pointer hover:bg-red-100 hover:text-red-800"
|
className="cursor-pointer hover:bg-red-100 hover:text-red-800"
|
||||||
onClick={() => removeExcludePattern(pattern)}
|
onClick={() => removeExcludePattern(pattern)}
|
||||||
>
|
>
|
||||||
|
|
@ -637,7 +636,7 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={taskForm.scan_config.include_tests}
|
checked={taskForm.scan_config.include_tests}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
setTaskForm({
|
setTaskForm({
|
||||||
...taskForm,
|
...taskForm,
|
||||||
scan_config: { ...taskForm.scan_config, include_tests: !!checked }
|
scan_config: { ...taskForm.scan_config, include_tests: !!checked }
|
||||||
|
|
@ -653,7 +652,7 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={taskForm.scan_config.include_docs}
|
checked={taskForm.scan_config.include_docs}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
setTaskForm({
|
setTaskForm({
|
||||||
...taskForm,
|
...taskForm,
|
||||||
scan_config: { ...taskForm.scan_config, include_docs: !!checked }
|
scan_config: { ...taskForm.scan_config, include_docs: !!checked }
|
||||||
|
|
@ -674,12 +673,12 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
id="max_file_size"
|
id="max_file_size"
|
||||||
type="number"
|
type="number"
|
||||||
value={taskForm.scan_config.max_file_size}
|
value={taskForm.scan_config.max_file_size}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setTaskForm({
|
setTaskForm({
|
||||||
...taskForm,
|
...taskForm,
|
||||||
scan_config: {
|
scan_config: {
|
||||||
...taskForm.scan_config,
|
...taskForm.scan_config,
|
||||||
max_file_size: parseInt(e.target.value) || 200
|
max_file_size: parseInt(e.target.value) || 200
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -690,9 +689,9 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="analysis_depth">分析深度</Label>
|
<Label htmlFor="analysis_depth">分析深度</Label>
|
||||||
<Select
|
<Select
|
||||||
value={taskForm.scan_config.analysis_depth}
|
value={taskForm.scan_config.analysis_depth}
|
||||||
onValueChange={(value: any) =>
|
onValueChange={(value: any) =>
|
||||||
setTaskForm({
|
setTaskForm({
|
||||||
...taskForm,
|
...taskForm,
|
||||||
scan_config: { ...taskForm.scan_config, analysis_depth: value }
|
scan_config: { ...taskForm.scan_config, analysis_depth: value }
|
||||||
|
|
@ -738,8 +737,8 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={creating}>
|
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={creating}>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleCreateTask}
|
onClick={handleCreateTask}
|
||||||
disabled={!taskForm.project_id || creating}
|
disabled={!taskForm.project_id || creating}
|
||||||
className="btn-primary"
|
className="btn-primary"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,13 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import {
|
import {
|
||||||
Download,
|
Download,
|
||||||
Upload,
|
Upload,
|
||||||
Trash2,
|
Trash2,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Server,
|
Server,
|
||||||
Activity,
|
Activity,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Database,
|
Database,
|
||||||
|
|
@ -95,16 +95,16 @@ export function DatabaseManager() {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setMessage(null);
|
setMessage(null);
|
||||||
|
|
||||||
const exportData = await api.exportDatabase();
|
const exportData = await api.exportDatabase();
|
||||||
|
|
||||||
// 构建完整的导出数据
|
// 构建完整的导出数据
|
||||||
const fullData = {
|
const fullData = {
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
export_date: exportData.export_date,
|
export_date: exportData.export_date,
|
||||||
data: exportData.data
|
data: exportData.data
|
||||||
};
|
};
|
||||||
|
|
||||||
const blob = new Blob([JSON.stringify(fullData, null, 2)], { type: 'application/json' });
|
const blob = new Blob([JSON.stringify(fullData, null, 2)], { type: 'application/json' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
|
|
@ -114,10 +114,10 @@ export function DatabaseManager() {
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
toast.success('数据导出成功!');
|
toast.success('数据导出成功!');
|
||||||
setMessage({ type: 'success', text: '数据导出成功!' });
|
setMessage({ type: 'success', text: '数据导出成功!' });
|
||||||
|
|
||||||
// 刷新统计信息
|
// 刷新统计信息
|
||||||
loadStats();
|
loadStats();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
@ -152,9 +152,9 @@ export function DatabaseManager() {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setMessage(null);
|
setMessage(null);
|
||||||
|
|
||||||
const result = await api.importDatabase(file);
|
const result = await api.importDatabase(file);
|
||||||
|
|
||||||
const imported = result.imported;
|
const imported = result.imported;
|
||||||
const summary = [
|
const summary = [
|
||||||
imported.projects > 0 && `${imported.projects} 个项目`,
|
imported.projects > 0 && `${imported.projects} 个项目`,
|
||||||
|
|
@ -163,17 +163,17 @@ export function DatabaseManager() {
|
||||||
imported.analyses > 0 && `${imported.analyses} 条分析记录`,
|
imported.analyses > 0 && `${imported.analyses} 条分析记录`,
|
||||||
imported.config > 0 && '用户配置',
|
imported.config > 0 && '用户配置',
|
||||||
].filter(Boolean).join('、');
|
].filter(Boolean).join('、');
|
||||||
|
|
||||||
toast.success(`数据导入成功!已导入:${summary}`);
|
toast.success(`数据导入成功!已导入:${summary}`);
|
||||||
setMessage({ type: 'success', text: `数据导入成功!已导入:${summary}` });
|
setMessage({ type: 'success', text: `数据导入成功!已导入:${summary}` });
|
||||||
|
|
||||||
// 清空文件输入
|
// 清空文件输入
|
||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
|
|
||||||
// 刷新统计信息和健康检查
|
// 刷新统计信息和健康检查
|
||||||
loadStats();
|
loadStats();
|
||||||
loadHealth();
|
loadHealth();
|
||||||
|
|
||||||
// 延迟刷新页面
|
// 延迟刷新页面
|
||||||
setTimeout(() => window.location.reload(), 2000);
|
setTimeout(() => window.location.reload(), 2000);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
@ -202,9 +202,9 @@ export function DatabaseManager() {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setMessage(null);
|
setMessage(null);
|
||||||
|
|
||||||
const result = await api.clearDatabase();
|
const result = await api.clearDatabase();
|
||||||
|
|
||||||
const deleted = result.deleted;
|
const deleted = result.deleted;
|
||||||
const summary = [
|
const summary = [
|
||||||
deleted.projects > 0 && `${deleted.projects} 个项目`,
|
deleted.projects > 0 && `${deleted.projects} 个项目`,
|
||||||
|
|
@ -213,14 +213,14 @@ export function DatabaseManager() {
|
||||||
deleted.analyses > 0 && `${deleted.analyses} 条分析记录`,
|
deleted.analyses > 0 && `${deleted.analyses} 条分析记录`,
|
||||||
deleted.config > 0 && '用户配置',
|
deleted.config > 0 && '用户配置',
|
||||||
].filter(Boolean).join('、');
|
].filter(Boolean).join('、');
|
||||||
|
|
||||||
toast.success(`数据已清空!已删除:${summary}`);
|
toast.success(`数据已清空!已删除:${summary}`);
|
||||||
setMessage({ type: 'success', text: `数据已清空!已删除:${summary}` });
|
setMessage({ type: 'success', text: `数据已清空!已删除:${summary}` });
|
||||||
|
|
||||||
// 刷新统计信息和健康检查
|
// 刷新统计信息和健康检查
|
||||||
loadStats();
|
loadStats();
|
||||||
loadHealth();
|
loadHealth();
|
||||||
|
|
||||||
// 延迟刷新页面
|
// 延迟刷新页面
|
||||||
setTimeout(() => window.location.reload(), 2000);
|
setTimeout(() => window.location.reload(), 2000);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
@ -306,7 +306,7 @@ export function DatabaseManager() {
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm mt-1">
|
<div className="text-sm mt-1">
|
||||||
数据库连接:{health.database_connected ? '正常' : '异常'} |
|
数据库连接:{health.database_connected ? '正常' : '异常'} |
|
||||||
总记录数:{health.total_records.toLocaleString()}
|
总记录数:{health.total_records.toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -507,7 +507,7 @@ export function DatabaseManager() {
|
||||||
<Info className="h-4 w-4" />
|
<Info className="h-4 w-4" />
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<strong>提示:</strong>
|
<strong>提示:</strong>
|
||||||
{dbMode === 'api'
|
{dbMode === 'api'
|
||||||
? '数据存储在后端 PostgreSQL 数据库中,支持多用户、多设备同步。建议定期导出备份。'
|
? '数据存储在后端 PostgreSQL 数据库中,支持多用户、多设备同步。建议定期导出备份。'
|
||||||
: '建议定期导出数据备份,以防意外数据丢失。'}
|
: '建议定期导出数据备份,以防意外数据丢失。'}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -36,7 +36,7 @@ export default function PageMeta({
|
||||||
<title>{fullTitle}</title>
|
<title>{fullTitle}</title>
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
<meta name="keywords" content={keywords} />
|
<meta name="keywords" content={keywords} />
|
||||||
|
|
||||||
{/* Open Graph */}
|
{/* Open Graph */}
|
||||||
<meta property="og:title" content={fullTitle} />
|
<meta property="og:title" content={fullTitle} />
|
||||||
<meta property="og:description" content={description} />
|
<meta property="og:description" content={description} />
|
||||||
|
|
@ -44,13 +44,13 @@ export default function PageMeta({
|
||||||
<meta property="og:url" content={url} />
|
<meta property="og:url" content={url} />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:site_name" content="XCodeReviewer" />
|
<meta property="og:site_name" content="XCodeReviewer" />
|
||||||
|
|
||||||
{/* Twitter Card */}
|
{/* Twitter Card */}
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:title" content={fullTitle} />
|
<meta name="twitter:title" content={fullTitle} />
|
||||||
<meta name="twitter:description" content={description} />
|
<meta name="twitter:description" content={description} />
|
||||||
<meta name="twitter:image" content={image} />
|
<meta name="twitter:image" content={image} />
|
||||||
|
|
||||||
{/* 其他 */}
|
{/* 其他 */}
|
||||||
<meta name="robots" content="index, follow" />
|
<meta name="robots" content="index, follow" />
|
||||||
<meta name="author" content="XCodeReviewer" />
|
<meta name="author" content="XCodeReviewer" />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -109,8 +109,8 @@ export default function ExportReportDialog({
|
||||||
<Label
|
<Label
|
||||||
htmlFor={format.value}
|
htmlFor={format.value}
|
||||||
className={`flex items-start space-x-4 p-4 rounded-lg border-2 cursor-pointer transition-all ${isSelected
|
className={`flex items-start space-x-4 p-4 rounded-lg border-2 cursor-pointer transition-all ${isSelected
|
||||||
? `${format.borderColor} ${format.bgColor} shadow-md`
|
? `${format.borderColor} ${format.bgColor} shadow-md`
|
||||||
: "border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm"
|
: "border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ export function SystemConfig() {
|
||||||
doubaoApiKey: config.doubaoApiKey,
|
doubaoApiKey: config.doubaoApiKey,
|
||||||
ollamaBaseUrl: config.ollamaBaseUrl,
|
ollamaBaseUrl: config.ollamaBaseUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
const otherConfig = {
|
const otherConfig = {
|
||||||
githubToken: config.githubToken,
|
githubToken: config.githubToken,
|
||||||
gitlabToken: config.gitlabToken,
|
gitlabToken: config.gitlabToken,
|
||||||
|
|
@ -219,7 +219,7 @@ export function SystemConfig() {
|
||||||
llmConfig,
|
llmConfig,
|
||||||
otherConfig,
|
otherConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
setConfigSource('runtime');
|
setConfigSource('runtime');
|
||||||
|
|
||||||
|
|
@ -232,7 +232,7 @@ export function SystemConfig() {
|
||||||
concurrency: config.llmConcurrency,
|
concurrency: config.llmConcurrency,
|
||||||
language: config.outputLanguage,
|
language: config.outputLanguage,
|
||||||
});
|
});
|
||||||
}).catch(() => {});
|
}).catch(() => { });
|
||||||
|
|
||||||
toast.success("配置已保存到后端!刷新页面后生效");
|
toast.success("配置已保存到后端!刷新页面后生效");
|
||||||
|
|
||||||
|
|
@ -254,7 +254,7 @@ export function SystemConfig() {
|
||||||
try {
|
try {
|
||||||
// 删除后端配置
|
// 删除后端配置
|
||||||
await api.deleteUserConfig();
|
await api.deleteUserConfig();
|
||||||
|
|
||||||
// 重新加载配置(会使用后端默认配置)
|
// 重新加载配置(会使用后端默认配置)
|
||||||
await loadConfig();
|
await loadConfig();
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
|
|
@ -262,7 +262,7 @@ export function SystemConfig() {
|
||||||
// 记录用户操作
|
// 记录用户操作
|
||||||
import('@/shared/utils/logger').then(({ logger }) => {
|
import('@/shared/utils/logger').then(({ logger }) => {
|
||||||
logger.logUserAction('重置系统配置', { action: 'reset_to_default' });
|
logger.logUserAction('重置系统配置', { action: 'reset_to_default' });
|
||||||
}).catch(() => {});
|
}).catch(() => { });
|
||||||
|
|
||||||
toast.success("已重置为默认配置");
|
toast.success("已重置为默认配置");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -384,9 +384,9 @@ export default function AdminDashboard() {
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<strong>当前数据库模式:</strong> {
|
<strong>当前数据库模式:</strong> {
|
||||||
dbMode === 'api' ? '后端 PostgreSQL 数据库' :
|
dbMode === 'api' ? '后端 PostgreSQL 数据库' :
|
||||||
dbMode === 'local' ? '本地 IndexedDB' :
|
dbMode === 'local' ? '本地 IndexedDB' :
|
||||||
dbMode === 'supabase' ? 'Supabase 云端(已废弃)' :
|
dbMode === 'supabase' ? 'Supabase 云端(已废弃)' :
|
||||||
'演示模式'
|
'演示模式'
|
||||||
}
|
}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
|
||||||
|
|
@ -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