diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..8ebf661
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,72 @@
+{
+ "name": "code-vulnerability-scanner-frontend",
+ "version": "1.0.0",
+ "description": "代码漏洞检测系统前端",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "antd": "^5.12.8",
+ "axios": "^1.6.2",
+ "chart.js": "^4.4.0",
+ "react": "^18.2.0",
+ "react-chartjs-2": "^5.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.8.1",
+ "react-scripts": "5.0.1",
+ "typescript": "^4.9.4",
+ "web-vitals": "^2.1.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "proxy": "http://localhost:8000",
+ "devDependencies": {
+ "@types/aria-query": "^5.0.4",
+ "@types/babel__core": "^7.20.5",
+ "@types/babel__generator": "^7.27.0",
+ "@types/babel__template": "^7.4.4",
+ "@types/babel__traverse": "^7.28.0",
+ "@types/bonjour": "^3.5.13",
+ "@types/eslint": "^9.6.1",
+ "@types/estree": "^1.0.8",
+ "@types/graceful-fs": "^4.1.9",
+ "@types/http-errors": "^2.0.5",
+ "@types/http-proxy": "^1.17.16",
+ "@types/jest": "^30.0.0",
+ "@types/node": "^24.6.0",
+ "@types/prettier": "^3.0.0",
+ "@types/qs": "^6.14.0",
+ "@types/react": "^19.1.16",
+ "@types/react-dom": "^19.1.9",
+ "@types/retry": "^0.12.5",
+ "@types/semver": "^7.7.1",
+ "@types/send": "^0.17.5",
+ "@types/serve-static": "^1.15.8",
+ "@types/stack-utils": "^2.0.3",
+ "@types/ws": "^8.18.1",
+ "@types/yargs": "^17.0.33"
+ }
+}
diff --git a/frontend/public/index.html b/frontend/public/index.html
new file mode 100644
index 0000000..7f6fa85
--- /dev/null
+++ b/frontend/public/index.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+ 代码漏洞检测系统
+
+
+
+
+
+
diff --git a/frontend/src/App.css b/frontend/src/App.css
new file mode 100644
index 0000000..7cc6f03
--- /dev/null
+++ b/frontend/src/App.css
@@ -0,0 +1,50 @@
+.App {
+ text-align: center;
+}
+
+.App-logo {
+ height: 40vmin;
+ pointer-events: none;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .App-logo {
+ animation: App-logo-spin infinite 20s linear;
+ }
+}
+
+.App-header {
+ background-color: #282c34;
+ padding: 20px;
+ color: white;
+}
+
+.App-link {
+ color: #61dafb;
+}
+
+@keyframes App-logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+/* 自定义样式 */
+.page-header {
+ margin-bottom: 24px;
+}
+
+.stats-card {
+ text-align: center;
+}
+
+.vulnerability-table {
+ margin-top: 24px;
+}
+
+.chart-container {
+ margin: 24px 0;
+}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 0000000..5b537b8
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
+import { ConfigProvider } from 'antd';
+import zhCN from 'antd/locale/zh_CN';
+import Layout from './components/Layout/Layout';
+import Dashboard from './pages/Dashboard';
+import Projects from './pages/Projects';
+import Scans from './pages/Scans';
+import Reports from './pages/Reports';
+import CodeEditor from './pages/CodeEditor';
+import './App.css';
+
+const App: React.FC = () => {
+ return (
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+ );
+};
+
+export default App;
diff --git a/frontend/src/components/CodeEditor/CodeEditor.css b/frontend/src/components/CodeEditor/CodeEditor.css
new file mode 100644
index 0000000..3ef8d1a
--- /dev/null
+++ b/frontend/src/components/CodeEditor/CodeEditor.css
@@ -0,0 +1,172 @@
+.code-editor-container {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.editor-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+}
+
+.file-path {
+ font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
+ font-size: 14px;
+ color: #666;
+}
+
+.editor-content {
+ position: relative;
+ display: flex;
+ height: 600px;
+ border: 1px solid #d9d9d9;
+ border-radius: 6px;
+ overflow: hidden;
+}
+
+.vulnerability-sidebar {
+ position: relative;
+ width: 20px;
+ background-color: #f5f5f5;
+ border-right: 1px solid #d9d9d9;
+}
+
+.vulnerability-marker {
+ position: absolute;
+ left: 2px;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ cursor: pointer;
+ border: 2px solid white;
+ box-shadow: 0 0 4px rgba(0, 0, 0, 0.3);
+ z-index: 10;
+}
+
+.vulnerability-marker:hover {
+ transform: scale(1.2);
+ transition: transform 0.2s;
+}
+
+.line-numbers {
+ background-color: #f5f5f5;
+ color: #999;
+ font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
+ font-size: 12px;
+ line-height: 20px;
+ padding: 8px 4px;
+ border-right: 1px solid #d9d9d9;
+ user-select: none;
+ min-width: 40px;
+ text-align: right;
+}
+
+.line-number {
+ height: 20px;
+ line-height: 20px;
+}
+
+.editor-main {
+ flex: 1;
+ position: relative;
+}
+
+.code-textarea {
+ width: 100%;
+ height: 100%;
+ border: none;
+ outline: none;
+ resize: none;
+ font-size: 12px;
+ line-height: 20px;
+ padding: 8px;
+ font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
+ background-color: #fff;
+ tab-size: 2;
+}
+
+.code-textarea:focus {
+ outline: none;
+}
+
+.vulnerability-header {
+ display: flex;
+ align-items: center;
+}
+
+.vulnerability-details {
+ font-size: 14px;
+ line-height: 1.6;
+}
+
+.vulnerability-details p {
+ margin-bottom: 8px;
+}
+
+.ai-suggestion {
+ margin-top: 16px;
+ padding: 12px;
+ background-color: #f6ffed;
+ border: 1px solid #b7eb8f;
+ border-radius: 6px;
+}
+
+.suggestion-content {
+ background-color: #fff;
+ padding: 8px;
+ border-radius: 4px;
+ margin: 8px 0;
+ font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
+ font-size: 12px;
+ white-space: pre-wrap;
+ border: 1px solid #d9d9d9;
+}
+
+.confidence {
+ margin-top: 8px;
+ font-size: 12px;
+ color: #666;
+}
+
+/* 语法高亮样式 */
+.keyword { color: #0000ff; }
+.string { color: #008000; }
+.comment { color: #808080; }
+.number { color: #ff0000; }
+.function { color: #800080; }
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .editor-content {
+ height: 400px;
+ }
+
+ .file-path {
+ font-size: 12px;
+ }
+
+ .code-textarea {
+ font-size: 11px;
+ }
+}
+
+/* 滚动条样式 */
+.code-textarea::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+.code-textarea::-webkit-scrollbar-track {
+ background: #f1f1f1;
+}
+
+.code-textarea::-webkit-scrollbar-thumb {
+ background: #c1c1c1;
+ border-radius: 4px;
+}
+
+.code-textarea::-webkit-scrollbar-thumb:hover {
+ background: #a8a8a8;
+}
diff --git a/frontend/src/components/CodeEditor/CodeEditor.tsx b/frontend/src/components/CodeEditor/CodeEditor.tsx
new file mode 100644
index 0000000..17b6ab6
--- /dev/null
+++ b/frontend/src/components/CodeEditor/CodeEditor.tsx
@@ -0,0 +1,265 @@
+import React, { useState, useRef, useEffect } from 'react';
+import { Card, Button, Space, Tag, Tooltip, message } from 'antd';
+import {
+ SaveOutlined,
+ ReloadOutlined,
+ BugOutlined,
+ CheckCircleOutlined,
+ ExclamationCircleOutlined
+} from '@ant-design/icons';
+import { vulnerabilityService } from '../../services/api';
+import './CodeEditor.css';
+
+interface Vulnerability {
+ id: number;
+ rule_id: string;
+ message: string;
+ severity: 'critical' | 'high' | 'medium' | 'low' | 'info';
+ line_number: number;
+ column_number?: number;
+ end_line?: number;
+ end_column?: number;
+ ai_suggestion?: string;
+ ai_confidence?: number;
+}
+
+interface CodeEditorProps {
+ filePath: string;
+ content: string;
+ language: string;
+ vulnerabilities: Vulnerability[];
+ onSave: (content: string) => Promise;
+ onRefresh: () => void;
+}
+
+const CodeEditor: React.FC = ({
+ filePath,
+ content,
+ language,
+ vulnerabilities,
+ onSave,
+ onRefresh
+}) => {
+ const [editedContent, setEditedContent] = useState(content);
+ const [selectedVulnerability, setSelectedVulnerability] = useState(null);
+ const [isSaving, setIsSaving] = useState(false);
+ const textareaRef = useRef(null);
+
+ useEffect(() => {
+ setEditedContent(content);
+ }, [content]);
+
+ const getSeverityColor = (severity: string) => {
+ const colorMap: { [key: string]: string } = {
+ critical: '#ff4d4f',
+ high: '#ff7a45',
+ medium: '#ffa940',
+ low: '#73d13d',
+ info: '#40a9ff',
+ };
+ return colorMap[severity] || '#d9d9d9';
+ };
+
+ const getSeverityIcon = (severity: string) => {
+ const iconMap: { [key: string]: React.ReactNode } = {
+ critical: ,
+ high: ,
+ medium: ,
+ low: ,
+ info: ,
+ };
+ return iconMap[severity] || ;
+ };
+
+ const handleSave = async () => {
+ setIsSaving(true);
+ try {
+ await onSave(editedContent);
+ message.success('代码保存成功');
+ } catch (error) {
+ message.error('代码保存失败');
+ console.error(error);
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const handleVulnerabilityClick = (vulnerability: Vulnerability) => {
+ setSelectedVulnerability(vulnerability);
+ // 滚动到对应行
+ if (textareaRef.current) {
+ const lines = editedContent.split('\n');
+ const targetLine = vulnerability.line_number;
+ if (targetLine <= lines.length) {
+ const lineHeight = 20; // 估算行高
+ const scrollTop = (targetLine - 1) * lineHeight;
+ textareaRef.current.scrollTop = scrollTop;
+ textareaRef.current.focus();
+ }
+ }
+ };
+
+ const applyAIFix = async (vulnerability: Vulnerability) => {
+ if (!vulnerability.ai_suggestion) {
+ message.warning('该漏洞暂无AI修复建议');
+ return;
+ }
+
+ try {
+ // 这里可以实现自动应用AI建议的逻辑
+ // 目前先显示建议内容
+ message.info(`AI修复建议: ${vulnerability.ai_suggestion}`);
+
+ // 标记漏洞为已修复
+ await vulnerabilityService.updateVulnerability(vulnerability.id, {
+ status: 'fixed'
+ });
+
+ message.success('漏洞已标记为已修复');
+ onRefresh(); // 刷新数据
+ } catch (error) {
+ message.error('修复失败');
+ console.error(error);
+ }
+ };
+
+ const renderLineNumbers = () => {
+ const lines = editedContent.split('\n');
+ return lines.map((_, index) => (
+
+ {index + 1}
+
+ ));
+ };
+
+ const renderVulnerabilityMarkers = () => {
+ return vulnerabilities.map((vuln) => (
+ handleVulnerabilityClick(vuln)}
+ title={`${vuln.severity.toUpperCase()}: ${vuln.message}`}
+ />
+ ));
+ };
+
+ return (
+
+
+ {filePath}
+
+ }
+ onClick={onRefresh}
+ size="small"
+ >
+ 刷新
+
+ }
+ onClick={handleSave}
+ loading={isSaving}
+ size="small"
+ >
+ 保存
+
+
+
+ }
+ extra={
+
+ {vulnerabilities.length > 0 && (
+
+ {vulnerabilities.length} 个漏洞
+
+ )}
+
+ }
+ >
+
+ {/* 漏洞标记侧边栏 */}
+
+ {renderVulnerabilityMarkers()}
+
+
+ {/* 行号 */}
+
+ {renderLineNumbers()}
+
+
+ {/* 代码编辑器 */}
+
+
+
+
+ {/* 漏洞详情面板 */}
+ {selectedVulnerability && (
+
+ {getSeverityIcon(selectedVulnerability.severity)}
+
+ 漏洞详情 - 第 {selectedVulnerability.line_number} 行
+
+
+ {selectedVulnerability.severity.toUpperCase()}
+
+
+ }
+ size="small"
+ style={{ marginTop: 16 }}
+ extra={
+
+ }
+ >
+
+
规则ID: {selectedVulnerability.rule_id}
+
描述: {selectedVulnerability.message}
+
+ {selectedVulnerability.ai_suggestion && (
+
+
AI修复建议:
+
+ {selectedVulnerability.ai_suggestion}
+
+ {selectedVulnerability.ai_confidence && (
+
+ 置信度: {(selectedVulnerability.ai_confidence * 100).toFixed(1)}%
+
+ )}
+
+ )}
+
+
+ )}
+
+
+ );
+};
+
+export default CodeEditor;
diff --git a/frontend/src/components/Layout/Layout.tsx b/frontend/src/components/Layout/Layout.tsx
new file mode 100644
index 0000000..39cab46
--- /dev/null
+++ b/frontend/src/components/Layout/Layout.tsx
@@ -0,0 +1,125 @@
+import React, { useState } from 'react';
+import { Layout as AntLayout, Menu, Button } from 'antd';
+import {
+ DashboardOutlined,
+ ProjectOutlined,
+ SearchOutlined,
+ FileTextOutlined,
+ EditOutlined,
+ MenuFoldOutlined,
+ MenuUnfoldOutlined
+} from '@ant-design/icons';
+import { useNavigate, useLocation } from 'react-router-dom';
+
+const { Header, Sider, Content } = AntLayout;
+
+interface LayoutProps {
+ children: React.ReactNode;
+}
+
+const Layout: React.FC = ({ children }) => {
+ const [collapsed, setCollapsed] = useState(false);
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const menuItems = [
+ {
+ key: '/',
+ icon: ,
+ label: '仪表板',
+ },
+ {
+ key: '/projects',
+ icon: ,
+ label: '项目管理',
+ },
+ {
+ key: '/scans',
+ icon: ,
+ label: '扫描管理',
+ },
+ {
+ key: '/reports',
+ icon: ,
+ label: '报告中心',
+ },
+ {
+ key: '/editor',
+ icon: ,
+ label: '代码编辑器',
+ },
+ ];
+
+ const handleMenuClick = ({ key }: { key: string }) => {
+ navigate(key);
+ };
+
+ return (
+
+
+
+ {collapsed ? 'CVS' : '代码漏洞检测'}
+
+
+
+
+
+ : }
+ onClick={() => setCollapsed(!collapsed)}
+ style={{
+ fontSize: '16px',
+ width: 64,
+ height: 64,
+ }}
+ />
+
+
+ 代码漏洞检测系统
+
+
+
+
+ {children}
+
+
+
+ );
+};
+
+export default Layout;
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..291cdb8
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,21 @@
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html, body, #root {
+ height: 100%;
+}
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
new file mode 100644
index 0000000..1fd12b7
--- /dev/null
+++ b/frontend/src/index.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(
+ document.getElementById('root') as HTMLElement
+);
+root.render(
+
+
+
+);
diff --git a/frontend/src/pages/CodeEditor.tsx b/frontend/src/pages/CodeEditor.tsx
new file mode 100644
index 0000000..a57d082
--- /dev/null
+++ b/frontend/src/pages/CodeEditor.tsx
@@ -0,0 +1,298 @@
+import React, { useState, useEffect } from 'react';
+import { Layout, Tree, Card, message, Spin, Empty } from 'antd';
+import { FileOutlined, FolderOutlined, BugOutlined } from '@ant-design/icons';
+import { projectService, vulnerabilityService } from '../services/api';
+import CodeEditor from '../components/CodeEditor/CodeEditor';
+
+const { Sider, Content } = Layout;
+
+interface ProjectFile {
+ name: string;
+ path: string;
+ is_directory: boolean;
+ size: number;
+ modified: number;
+}
+
+interface Vulnerability {
+ id: number;
+ file_path: string;
+ line_number?: number;
+ severity: 'critical' | 'high' | 'medium' | 'low' | 'info';
+ message: string;
+ rule_id: string;
+ ai_suggestion?: string;
+ ai_confidence?: number;
+}
+
+interface CodeEditorPageProps {
+ projectId?: number;
+}
+
+const CodeEditorPage: React.FC = ({ projectId }) => {
+ const [projects, setProjects] = useState([]);
+ const [selectedProject, setSelectedProject] = useState(projectId || null);
+ const [files, setFiles] = useState([]);
+ const [selectedFile, setSelectedFile] = useState(null);
+ const [fileContent, setFileContent] = useState('');
+ const [vulnerabilities, setVulnerabilities] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [fileLoading, setFileLoading] = useState(false);
+
+ useEffect(() => {
+ fetchProjects();
+ }, []);
+
+ useEffect(() => {
+ if (selectedProject) {
+ fetchFiles('');
+ }
+ }, [selectedProject]);
+
+ useEffect(() => {
+ if (selectedFile && selectedProject) {
+ fetchFileContent();
+ fetchFileVulnerabilities();
+ }
+ }, [selectedFile, selectedProject]);
+
+ const fetchProjects = async () => {
+ try {
+ const data = await projectService.getProjects();
+ setProjects(data);
+ if (data.length > 0 && !selectedProject) {
+ setSelectedProject(data[0].id);
+ }
+ } catch (error) {
+ message.error('获取项目列表失败');
+ console.error(error);
+ }
+ };
+
+ const fetchFiles = async (path: string = '') => {
+ if (!selectedProject) return;
+
+ setLoading(true);
+ try {
+ const response = await fetch(`http://localhost:8000/api/projects/${selectedProject}/files?path=${encodeURIComponent(path)}`);
+ const data = await response.json();
+ setFiles(data.files || []);
+ } catch (error) {
+ message.error('获取文件列表失败');
+ console.error(error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchFileContent = async () => {
+ if (!selectedFile || !selectedProject) return;
+
+ setFileLoading(true);
+ try {
+ const response = await fetch(
+ `http://localhost:8000/api/projects/${selectedProject}/files/content?file_path=${encodeURIComponent(selectedFile.path)}`
+ );
+ const data = await response.json();
+ setFileContent(data.content || '');
+ } catch (error) {
+ message.error('读取文件内容失败');
+ console.error(error);
+ } finally {
+ setFileLoading(false);
+ }
+ };
+
+ const fetchFileVulnerabilities = async () => {
+ if (!selectedFile || !selectedProject) return;
+
+ try {
+ const data = await vulnerabilityService.getVulnerabilities({
+ project_id: selectedProject
+ });
+
+ // 过滤出当前文件的漏洞
+ const fileVulns = data.filter((vuln: any) =>
+ vuln.file_path.includes(selectedFile.path) ||
+ selectedFile.path.includes(vuln.file_path)
+ );
+
+ setVulnerabilities(fileVulns);
+ } catch (error) {
+ console.error('获取漏洞信息失败:', error);
+ }
+ };
+
+ const handleSaveFile = async (content: string) => {
+ if (!selectedFile || !selectedProject) return;
+
+ try {
+ const response = await fetch(
+ `http://localhost:8000/api/projects/${selectedProject}/files/content?file_path=${encodeURIComponent(selectedFile.path)}`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ content })
+ }
+ );
+
+ if (response.ok) {
+ message.success('文件保存成功');
+ // 重新获取漏洞信息
+ fetchFileVulnerabilities();
+ } else {
+ throw new Error('保存失败');
+ }
+ } catch (error) {
+ message.error('保存文件失败');
+ throw error;
+ }
+ };
+
+ const handleRefresh = () => {
+ if (selectedProject) {
+ fetchFiles('');
+ }
+ if (selectedFile && selectedProject) {
+ fetchFileContent();
+ fetchFileVulnerabilities();
+ }
+ };
+
+ const getFileIcon = (file: ProjectFile) => {
+ return file.is_directory ? : ;
+ };
+
+ const getLanguageFromExtension = (filename: string): string => {
+ const ext = filename.split('.').pop()?.toLowerCase();
+ const langMap: { [key: string]: string } = {
+ 'py': 'python',
+ 'js': 'javascript',
+ 'jsx': 'javascript',
+ 'ts': 'typescript',
+ 'tsx': 'typescript',
+ 'java': 'java',
+ 'cpp': 'cpp',
+ 'c': 'c',
+ 'h': 'c',
+ 'hpp': 'cpp',
+ 'cs': 'csharp',
+ 'php': 'php',
+ 'rb': 'ruby',
+ 'go': 'go',
+ 'rs': 'rust',
+ 'swift': 'swift',
+ 'kt': 'kotlin',
+ 'scala': 'scala',
+ 'sh': 'bash',
+ 'sql': 'sql',
+ 'html': 'html',
+ 'css': 'css',
+ 'scss': 'scss',
+ 'sass': 'sass',
+ 'less': 'less',
+ 'vue': 'vue',
+ 'json': 'json',
+ 'xml': 'xml',
+ 'yaml': 'yaml',
+ 'yml': 'yaml',
+ 'md': 'markdown',
+ 'txt': 'text'
+ };
+ return langMap[ext || ''] || 'text';
+ };
+
+ const renderTreeData = (files: ProjectFile[]): any[] => {
+ return files.map(file => ({
+ title: (
+
+ {getFileIcon(file)}
+ {file.name}
+ {!file.is_directory && vulnerabilities.some(v => v.file_path.includes(file.path)) && (
+
+ )}
+
+ ),
+ key: file.path,
+ icon: getFileIcon(file),
+ isLeaf: !file.is_directory,
+ data: file
+ }));
+ };
+
+ const handleTreeSelect = (selectedKeys: React.Key[], info: any) => {
+ if (selectedKeys.length > 0 && info.node.data && !info.node.data.is_directory) {
+ setSelectedFile(info.node.data);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+ {loading ? (
+
+ ) : files.length > 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {selectedFile ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ );
+};
+
+export default CodeEditorPage;
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx
new file mode 100644
index 0000000..5ed0e39
--- /dev/null
+++ b/frontend/src/pages/Dashboard.tsx
@@ -0,0 +1,244 @@
+import React, { useState, useEffect } from 'react';
+import { Card, Row, Col, Statistic, Table, Tag, Button } from 'antd';
+import {
+ ProjectOutlined,
+ SearchOutlined,
+ BugOutlined,
+ CheckCircleOutlined
+} from '@ant-design/icons';
+import { Line, Doughnut } from 'react-chartjs-2';
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ Title,
+ Tooltip,
+ Legend,
+ ArcElement,
+} from 'chart.js';
+import { dashboardService } from '../services/api';
+
+// 注册Chart.js组件
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ Title,
+ Tooltip,
+ Legend,
+ ArcElement
+);
+
+const Dashboard: React.FC = () => {
+ const [loading, setLoading] = useState(true);
+ const [summaryData, setSummaryData] = useState(null);
+
+ useEffect(() => {
+ fetchDashboardData();
+ }, []);
+
+ const fetchDashboardData = async () => {
+ try {
+ const data = await dashboardService.getSummary();
+ setSummaryData(data);
+ } catch (error) {
+ console.error('获取仪表板数据失败:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 漏洞趋势图数据
+ const vulnerabilityTrendData = {
+ labels: ['1月', '2月', '3月', '4月', '5月', '6月'],
+ datasets: [
+ {
+ label: '严重漏洞',
+ data: [12, 19, 3, 5, 2, 3],
+ borderColor: 'rgb(255, 99, 132)',
+ backgroundColor: 'rgba(255, 99, 132, 0.2)',
+ },
+ {
+ label: '高危漏洞',
+ data: [2, 3, 20, 5, 1, 4],
+ borderColor: 'rgb(54, 162, 235)',
+ backgroundColor: 'rgba(54, 162, 235, 0.2)',
+ },
+ ],
+ };
+
+ // 漏洞分布饼图数据
+ const vulnerabilityDistributionData = {
+ labels: ['安全', '性能', '可维护性', '可靠性'],
+ datasets: [
+ {
+ data: [30, 25, 35, 10],
+ backgroundColor: [
+ '#FF6384',
+ '#36A2EB',
+ '#FFCE56',
+ '#4BC0C0',
+ ],
+ },
+ ],
+ };
+
+ const recentVulnerabilitiesColumns = [
+ {
+ title: '项目',
+ dataIndex: 'project_name',
+ key: 'project_name',
+ },
+ {
+ title: '漏洞类型',
+ dataIndex: 'category',
+ key: 'category',
+ render: (category: string) => {
+ const colorMap: { [key: string]: string } = {
+ security: 'red',
+ performance: 'blue',
+ maintainability: 'green',
+ reliability: 'orange',
+ };
+ return {category};
+ },
+ },
+ {
+ title: '严重程度',
+ dataIndex: 'severity',
+ key: 'severity',
+ render: (severity: string) => {
+ const colorMap: { [key: string]: string } = {
+ critical: 'red',
+ high: 'orange',
+ medium: 'yellow',
+ low: 'green',
+ info: 'blue',
+ };
+ return {severity};
+ },
+ },
+ {
+ title: '文件路径',
+ dataIndex: 'file_path',
+ key: 'file_path',
+ ellipsis: true,
+ },
+ {
+ title: '操作',
+ key: 'action',
+ render: () => (
+
+ ),
+ },
+ ];
+
+ const chartOptions = {
+ responsive: true,
+ plugins: {
+ legend: {
+ position: 'top' as const,
+ },
+ title: {
+ display: true,
+ text: '漏洞趋势分析',
+ },
+ },
+ };
+
+ const doughnutOptions = {
+ responsive: true,
+ plugins: {
+ legend: {
+ position: 'right' as const,
+ },
+ title: {
+ display: true,
+ text: '漏洞分类分布',
+ },
+ },
+ };
+
+ if (loading) {
+ return 加载中...
;
+ }
+
+ return (
+
+
仪表板
+
+ {/* 统计卡片 */}
+
+
+
+ }
+ />
+
+
+
+
+ }
+ />
+
+
+
+
+ }
+ valueStyle={{ color: '#cf1322' }}
+ />
+
+
+
+
+ }
+ valueStyle={{ color: '#3f8600' }}
+ />
+
+
+
+
+ {/* 图表区域 */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* 最近漏洞 */}
+
+
+
+
+ );
+};
+
+export default Dashboard;
diff --git a/frontend/src/pages/Projects.tsx b/frontend/src/pages/Projects.tsx
new file mode 100644
index 0000000..16aba3d
--- /dev/null
+++ b/frontend/src/pages/Projects.tsx
@@ -0,0 +1,291 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Card,
+ Button,
+ Table,
+ Tag,
+ Space,
+ Modal,
+ Form,
+ Input,
+ Select,
+ message,
+ Popconfirm
+} from 'antd';
+import { PlusOutlined, EditOutlined, DeleteOutlined, PlayCircleOutlined, CodeOutlined } from '@ant-design/icons';
+import { projectService, ProjectDto } from '../services/api';
+import { useNavigate } from 'react-router-dom';
+
+const { TextArea } = Input;
+const { Option } = Select;
+
+interface Project {
+ id: number;
+ name: string;
+ description: string;
+ language: string;
+ repository_url: string;
+ project_path: string;
+ created_at: string;
+ updated_at: string;
+}
+
+const Projects: React.FC = () => {
+ const [projects, setProjects] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [modalVisible, setModalVisible] = useState(false);
+ const [editingProject, setEditingProject] = useState(null);
+ const [form] = Form.useForm();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ fetchProjects();
+ }, []);
+
+ const fetchProjects = async () => {
+ setLoading(true);
+ try {
+ const data = await projectService.getProjects();
+ setProjects(data as unknown as Project[]);
+ } catch (error) {
+ message.error('获取项目列表失败');
+ console.error(error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleCreate = () => {
+ setEditingProject(null);
+ form.resetFields();
+ setModalVisible(true);
+ };
+
+ const handleEdit = (project: Project) => {
+ setEditingProject(project);
+ form.setFieldsValue(project);
+ setModalVisible(true);
+ };
+
+ const handleDelete = async (projectId: number) => {
+ try {
+ await projectService.deleteProject(projectId);
+ message.success('项目删除成功');
+ fetchProjects();
+ } catch (error) {
+ message.error('项目删除失败');
+ console.error(error);
+ }
+ };
+
+ const handleStartScan = async (projectId: number) => {
+ try {
+ // 这里调用扫描API
+ message.success('扫描任务已启动');
+ } catch (error) {
+ message.error('启动扫描失败');
+ console.error(error);
+ }
+ };
+
+ const handleEditCode = (projectId: number) => {
+ navigate(`/editor/${projectId}`);
+ };
+
+ const handleModalOk = async () => {
+ try {
+ const values = await form.validateFields();
+
+ if (editingProject) {
+ await projectService.updateProject(editingProject.id, values);
+ message.success('项目更新成功');
+ } else {
+ await projectService.createProject(values);
+ message.success('项目创建成功');
+ }
+
+ setModalVisible(false);
+ fetchProjects();
+ } catch (error) {
+ message.error('操作失败');
+ console.error(error);
+ }
+ };
+
+ const handleModalCancel = () => {
+ setModalVisible(false);
+ form.resetFields();
+ };
+
+ const columns = [
+ {
+ title: '项目名称',
+ dataIndex: 'name',
+ key: 'name',
+ render: (text: string, record: Project) => (
+ handleEdit(record)}>{text}
+ ),
+ },
+ {
+ title: '编程语言',
+ dataIndex: 'language',
+ key: 'language',
+ render: (language: string) => {
+ const colorMap: { [key: string]: string } = {
+ python: 'green',
+ cpp: 'blue',
+ javascript: 'yellow',
+ java: 'red',
+ go: 'cyan',
+ };
+ return {language};
+ },
+ },
+ {
+ title: '描述',
+ dataIndex: 'description',
+ key: 'description',
+ ellipsis: true,
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'created_at',
+ key: 'created_at',
+ render: (date: string) => new Date(date).toLocaleString(),
+ },
+ {
+ title: '操作',
+ key: 'action',
+ render: (_: unknown, record: Project) => (
+
+ }
+ onClick={() => handleStartScan(record.id)}
+ >
+ 扫描
+
+ }
+ onClick={() => handleEditCode(record.id)}
+ >
+ 编辑代码
+
+ }
+ onClick={() => handleEdit(record)}
+ >
+ 编辑
+
+ handleDelete(record.id)}
+ okText="确定"
+ cancelText="取消"
+ >
+ }
+ >
+ 删除
+
+
+
+ ),
+ },
+ ];
+
+ return (
+
+
} onClick={handleCreate}>
+ 新建项目
+
+ }
+ >
+
+ `第 ${range[0]}-${range[1]} 条/共 ${total} 条`,
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Projects;
diff --git a/frontend/src/pages/Reports.tsx b/frontend/src/pages/Reports.tsx
new file mode 100644
index 0000000..2a45563
--- /dev/null
+++ b/frontend/src/pages/Reports.tsx
@@ -0,0 +1,418 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Card,
+ Table,
+ Tag,
+ Space,
+ Button,
+ Input,
+ Select,
+ Modal,
+ message,
+ Statistic,
+ Row,
+ Col
+} from 'antd';
+import {
+ SearchOutlined,
+ DownloadOutlined,
+ EyeOutlined,
+ BugOutlined,
+ FilterOutlined
+} from '@ant-design/icons';
+import { vulnerabilityService, VulnerabilityDto, VulnerabilityStatsDto } from '../services/api';
+
+const { Option } = Select;
+const { Search } = Input;
+
+interface Vulnerability {
+ id: number;
+ scan_id: number;
+ rule_id: string;
+ message: string;
+ category: string;
+ severity: string;
+ file_path: string;
+ line_number: number;
+ status: string;
+ ai_enhanced: boolean;
+ ai_confidence: number;
+ ai_suggestion: string;
+ created_at: string;
+}
+
+const Reports: React.FC = () => {
+ const [vulnerabilities, setVulnerabilities] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [stats, setStats] = useState(null);
+ const [detailModalVisible, setDetailModalVisible] = useState(false);
+ const [selectedVulnerability, setSelectedVulnerability] = useState(null);
+
+ // 筛选条件
+ const [filters, setFilters] = useState({
+ severity: '',
+ category: '',
+ status: '',
+ search: '',
+ });
+
+ useEffect(() => {
+ fetchVulnerabilities();
+ fetchStats();
+ }, [filters]);
+
+ const fetchVulnerabilities = async () => {
+ setLoading(true);
+ try {
+ const params: any = {};
+ if (filters.severity) params.severity = filters.severity;
+ if (filters.category) params.category = filters.category;
+ if (filters.status) params.status = filters.status;
+ if (filters.search) params.search = filters.search;
+
+ const data = await vulnerabilityService.getVulnerabilities(params);
+ setVulnerabilities(data as unknown as Vulnerability[]);
+ } catch (error) {
+ message.error('获取漏洞列表失败');
+ console.error(error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchStats = async () => {
+ try {
+ const data = await vulnerabilityService.getVulnerabilityStats();
+ setStats(data as unknown as VulnerabilityStatsDto);
+ } catch (error) {
+ console.error('获取统计数据失败:', error);
+ }
+ };
+
+ const handleViewDetail = (vulnerability: Vulnerability) => {
+ setSelectedVulnerability(vulnerability);
+ setDetailModalVisible(true);
+ };
+
+ const handleExport = async (format: string) => {
+ try {
+ const url = `http://localhost:8000/api/vulnerabilities/export?format=${format}`;
+ window.open(url, '_blank');
+ message.success('导出成功');
+ } catch (error) {
+ message.error('导出失败');
+ console.error(error);
+ }
+ };
+
+ const getSeverityColor = (severity: string) => {
+ const colorMap: { [key: string]: string } = {
+ critical: 'red',
+ high: 'orange',
+ medium: 'yellow',
+ low: 'green',
+ info: 'blue',
+ };
+ return colorMap[severity] || 'default';
+ };
+
+ const getCategoryColor = (category: string) => {
+ const colorMap: { [key: string]: string } = {
+ security: 'red',
+ performance: 'blue',
+ maintainability: 'green',
+ reliability: 'orange',
+ usability: 'purple',
+ };
+ return colorMap[category] || 'default';
+ };
+
+ const getStatusColor = (status: string) => {
+ const colorMap: { [key: string]: string } = {
+ open: 'red',
+ fixed: 'green',
+ false_positive: 'gray',
+ wont_fix: 'orange',
+ };
+ return colorMap[status] || 'default';
+ };
+
+ const columns = [
+ {
+ title: 'ID',
+ dataIndex: 'id',
+ key: 'id',
+ width: 60,
+ },
+ {
+ title: '规则ID',
+ dataIndex: 'rule_id',
+ key: 'rule_id',
+ width: 100,
+ },
+ {
+ title: '严重程度',
+ dataIndex: 'severity',
+ key: 'severity',
+ width: 100,
+ render: (severity: string) => (
+
+ {severity.toUpperCase()}
+
+ ),
+ },
+ {
+ title: '分类',
+ dataIndex: 'category',
+ key: 'category',
+ width: 100,
+ render: (category: string) => (
+
+ {category}
+
+ ),
+ },
+ {
+ title: '描述',
+ dataIndex: 'message',
+ key: 'message',
+ ellipsis: true,
+ width: 300,
+ },
+ {
+ title: '文件路径',
+ dataIndex: 'file_path',
+ key: 'file_path',
+ ellipsis: true,
+ width: 200,
+ render: (text: string) => (
+ {text}
+ ),
+ },
+ {
+ title: '行号',
+ dataIndex: 'line_number',
+ key: 'line_number',
+ width: 80,
+ render: (line: number) => line || '-',
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ key: 'status',
+ width: 100,
+ render: (status: string) => (
+
+ {status}
+
+ ),
+ },
+ {
+ title: 'AI增强',
+ dataIndex: 'ai_enhanced',
+ key: 'ai_enhanced',
+ width: 80,
+ render: (enhanced: boolean) => enhanced ? '是' : '否',
+ },
+ {
+ title: '操作',
+ key: 'action',
+ width: 120,
+ render: (_: unknown, record: Vulnerability) => (
+
+ }
+ onClick={() => handleViewDetail(record)}
+ >
+ 详情
+
+
+ ),
+ },
+ ];
+
+ return (
+
+ {/* 统计卡片 */}
+ {stats && (
+
+
+
+ }
+ />
+
+
+ {Object.entries(stats.by_severity || {}).map(([severity, count]) => (
+
+
+
+
+
+ ))}
+
+ )}
+
+
+ }
+ onClick={() => handleExport('excel')}
+ >
+ 导出Excel
+
+ }
+ onClick={() => handleExport('json')}
+ >
+ 导出JSON
+
+
+ }
+ >
+ {/* 筛选器 */}
+
+ setFilters({ ...filters, search: value })}
+ onChange={(e) => {
+ if (!e.target.value) {
+ setFilters({ ...filters, search: '' });
+ }
+ }}
+ />
+
+
+
+
+
+
+ `第 ${range[0]}-${range[1]} 条/共 ${total} 条`,
+ }}
+ />
+
+
+ {/* 漏洞详情模态框 */}
+ setDetailModalVisible(false)}
+ footer={null}
+ width={800}
+ >
+ {selectedVulnerability && (
+
+
+
+ 规则ID: {selectedVulnerability.rule_id}
+ 严重程度:
+
+ {selectedVulnerability.severity.toUpperCase()}
+
+
+ 分类:
+
+ {selectedVulnerability.category}
+
+
+ 状态:
+
+ {selectedVulnerability.status}
+
+
+
+
+ 文件路径: {selectedVulnerability.file_path}
+ 行号: {selectedVulnerability.line_number || 'N/A'}
+ AI增强: {selectedVulnerability.ai_enhanced ? '是' : '否'}
+ {selectedVulnerability.ai_confidence && (
+ AI置信度: {(selectedVulnerability.ai_confidence * 100).toFixed(1)}%
+ )}
+
+
+
+
+
漏洞描述:
+
+ {selectedVulnerability.message}
+
+
+
+ {selectedVulnerability.ai_suggestion && (
+
+
AI修复建议:
+
+ {selectedVulnerability.ai_suggestion}
+
+
+ )}
+
+ )}
+
+
+ );
+};
+
+export default Reports;
diff --git a/frontend/src/pages/Scans.tsx b/frontend/src/pages/Scans.tsx
new file mode 100644
index 0000000..e7f38b8
--- /dev/null
+++ b/frontend/src/pages/Scans.tsx
@@ -0,0 +1,357 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Card,
+ Table,
+ Tag,
+ Space,
+ Button,
+ Progress,
+ message,
+ Modal,
+ Form,
+ Select,
+ Input
+} from 'antd';
+import {
+ PlayCircleOutlined,
+ StopOutlined,
+ EyeOutlined,
+ DownloadOutlined,
+ ReloadOutlined
+} from '@ant-design/icons';
+import { scanService, projectService, ScanDto, ProjectDto } from '../services/api';
+
+const { Option } = Select;
+const { TextArea } = Input;
+
+interface Scan {
+ id: number;
+ project_id: number;
+ project_name?: string;
+ scan_type: string;
+ status: string;
+ total_files: number;
+ scanned_files: number;
+ total_vulnerabilities: number;
+ started_at: string;
+ completed_at: string;
+ created_at: string;
+}
+
+interface Project {
+ id: number;
+ name: string;
+}
+
+const Scans: React.FC = () => {
+ const [scans, setScans] = useState([]);
+ const [projects, setProjects] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [modalVisible, setModalVisible] = useState(false);
+ const [form] = Form.useForm();
+
+ useEffect(() => {
+ fetchScans();
+ fetchProjects();
+ }, []);
+
+ const fetchScans = async () => {
+ setLoading(true);
+ try {
+ const data = await scanService.getScans();
+ setScans(data as unknown as Scan[]);
+ } catch (error) {
+ message.error('获取扫描列表失败');
+ console.error(error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchProjects = async () => {
+ try {
+ const data = await projectService.getProjects();
+ setProjects(data as unknown as Project[]);
+ } catch (error) {
+ console.error('获取项目列表失败:', error);
+ }
+ };
+
+ const handleStartScan = async (values: any) => {
+ try {
+ await scanService.createScan(values);
+ message.success('扫描任务已启动');
+ setModalVisible(false);
+ form.resetFields();
+ fetchScans();
+ } catch (error) {
+ message.error('启动扫描失败');
+ console.error(error);
+ }
+ };
+
+ const handleCancelScan = async (scanId: number) => {
+ try {
+ await scanService.cancelScan(scanId);
+ message.success('扫描已取消');
+ fetchScans();
+ } catch (error) {
+ message.error('取消扫描失败');
+ console.error(error);
+ }
+ };
+
+ const handleViewReport = (scanId: number) => {
+ window.open(`/reports/scan/${scanId}`, '_blank');
+ };
+
+ const handleDownloadReport = (scanId: number, format: string) => {
+ const url = `http://localhost:8000/api/reports/scan/${scanId}?format=${format}`;
+ window.open(url, '_blank');
+ };
+
+ const getStatusColor = (status: string) => {
+ const colorMap: { [key: string]: string } = {
+ pending: 'blue',
+ running: 'orange',
+ completed: 'green',
+ failed: 'red',
+ cancelled: 'gray',
+ };
+ return colorMap[status] || 'default';
+ };
+
+ const getStatusText = (status: string) => {
+ const textMap: { [key: string]: string } = {
+ pending: '等待中',
+ running: '运行中',
+ completed: '已完成',
+ failed: '失败',
+ cancelled: '已取消',
+ };
+ return textMap[status] || status;
+ };
+
+ const columns = [
+ {
+ title: '扫描ID',
+ dataIndex: 'id',
+ key: 'id',
+ width: 80,
+ },
+ {
+ title: '项目',
+ dataIndex: 'project_name',
+ key: 'project_name',
+ render: (text: string, record: Scan) => {
+ const project = projects.find(p => p.id === record.project_id);
+ return project?.name || `项目 ${record.project_id}`;
+ },
+ },
+ {
+ title: '扫描类型',
+ dataIndex: 'scan_type',
+ key: 'scan_type',
+ render: (type: string) => {
+ const typeMap: { [key: string]: string } = {
+ full: '全量扫描',
+ incremental: '增量扫描',
+ custom: '自定义扫描',
+ };
+ return typeMap[type] || type;
+ },
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ key: 'status',
+ render: (status: string) => (
+
+ {getStatusText(status)}
+
+ ),
+ },
+ {
+ title: '进度',
+ key: 'progress',
+ render: (_: unknown, record: Scan) => {
+ if (record.status === 'running') {
+ const progress = record.total_files > 0
+ ? (record.scanned_files / record.total_files) * 100
+ : 0;
+ return (
+
+ );
+ } else if (record.status === 'completed') {
+ return ;
+ } else {
+ return '-';
+ }
+ },
+ },
+ {
+ title: '漏洞数',
+ dataIndex: 'total_vulnerabilities',
+ key: 'total_vulnerabilities',
+ render: (count: number) => count || 0,
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'created_at',
+ key: 'created_at',
+ render: (date: string) => new Date(date).toLocaleString(),
+ },
+ {
+ title: '操作',
+ key: 'action',
+ render: (_: unknown, record: Scan) => (
+
+ {record.status === 'completed' && (
+ <>
+ }
+ onClick={() => handleViewReport(record.id)}
+ >
+ 查看
+
+ }
+ onClick={() => handleDownloadReport(record.id, 'pdf')}
+ >
+ PDF
+
+ >
+ )}
+ {record.status === 'running' && (
+ }
+ onClick={() => handleCancelScan(record.id)}
+ >
+ 取消
+
+ )}
+
+ ),
+ },
+ ];
+
+ return (
+
+
+ }
+ onClick={fetchScans}
+ >
+ 刷新
+
+ }
+ onClick={() => setModalVisible(true)}
+ >
+ 新建扫描
+
+
+ }
+ >
+
+ `第 ${range[0]}-${range[1]} 条/共 ${total} 条`,
+ }}
+ />
+
+
+ {
+ setModalVisible(false);
+ form.resetFields();
+ }}
+ footer={null}
+ width={600}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Scans;
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
new file mode 100644
index 0000000..7dc0009
--- /dev/null
+++ b/frontend/src/services/api.ts
@@ -0,0 +1,145 @@
+import axios, { AxiosResponse } from 'axios';
+
+// 通用类型定义(与后端模型对齐的最小字段)
+export interface ProjectDto {
+ id: number;
+ name: string;
+ description?: string;
+ language: string;
+ repository_url?: string;
+ project_path?: string;
+ created_at: string;
+ updated_at?: string;
+}
+
+export interface ScanDto {
+ id: number;
+ project_id: number;
+ scan_type: string;
+ status: string;
+ total_files: number;
+ scanned_files: number;
+ total_vulnerabilities: number;
+ started_at?: string;
+ completed_at?: string;
+ created_at: string;
+}
+
+export interface VulnerabilityDto {
+ id: number;
+ scan_id: number;
+ rule_id: string;
+ message: string;
+ category: string;
+ severity: string;
+ file_path: string;
+ line_number?: number;
+ status: string;
+ ai_enhanced: boolean;
+ ai_confidence?: number;
+ ai_suggestion?: string;
+ created_at: string;
+}
+
+export interface VulnerabilityStatsDto {
+ total: number;
+ by_severity: Record;
+ by_category: Record;
+}
+
+// 创建axios实例
+const api = axios.create({
+ baseURL: 'http://localhost:8000/api',
+ timeout: 10000,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+// 请求拦截器
+api.interceptors.request.use(
+ (config) => {
+ // 可以在这里添加认证token等
+ return config;
+ },
+ (error) => {
+ return Promise.reject(error);
+ }
+);
+
+// 响应拦截器
+api.interceptors.response.use(
+ (response: AxiosResponse) => {
+ return response.data;
+ },
+ (error) => {
+ console.error('API请求错误:', error);
+ return Promise.reject(error);
+ }
+);
+
+// 项目相关API
+export const projectService = {
+ getProjects: (): Promise => api.get('/projects'),
+ getProject: (id: number): Promise => api.get(`/projects/${id}`),
+ createProject: (data: Partial): Promise => api.post('/projects', data),
+ updateProject: (id: number, data: Partial): Promise => api.put(`/projects/${id}`, data),
+ deleteProject: (id: number): Promise => api.delete(`/projects/${id}`),
+};
+
+// 扫描相关API
+export const scanService = {
+ getScans: (params?: any): Promise => api.get('/scans', { params }),
+ getScan: (id: number): Promise => api.get(`/scans/${id}`),
+ createScan: (data: any): Promise => api.post('/scans', data),
+ getScanStatus: (id: number): Promise => api.get(`/scans/${id}/status`),
+ cancelScan: (id: number): Promise => api.post(`/scans/${id}/cancel`),
+};
+
+// 漏洞相关API
+export const vulnerabilityService = {
+ getVulnerabilities: (params?: any): Promise => api.get('/vulnerabilities', { params }),
+ getVulnerability: (id: number): Promise => api.get(`/vulnerabilities/${id}`),
+ updateVulnerability: (id: number, data: Partial): Promise => api.put(`/vulnerabilities/${id}`, data),
+ getVulnerabilityStats: (params?: any): Promise => api.get('/vulnerabilities/stats/summary', { params }),
+};
+
+// 报告相关API
+export const reportService = {
+ generateScanReport: (scanId: number, format: string = 'html') =>
+ api.get(`/reports/scan/${scanId}?format=${format}`),
+ generateProjectReport: (projectId: number, format: string = 'html') =>
+ api.get(`/reports/project/${projectId}?format=${format}`),
+ downloadReport: (scanId: number, format: string) => {
+ const url = `http://localhost:8000/api/reports/scan/${scanId}?format=${format}`;
+ window.open(url, '_blank');
+ },
+};
+
+// 仪表板相关API
+export const dashboardService = {
+ getSummary: () => api.get('/reports/dashboard/summary'),
+};
+
+export default api;
+
+// 便捷方法:保证返回 Promise,避免 AxiosResponse 类型外溢
+export const http = {
+ get: async (url: string, config?: any): Promise => {
+ const res = await api.get(url, config);
+ // 由于拦截器已返回 data,这里类型上做一次收敛
+ return res as unknown as T;
+ },
+ post: async (url: string, data?: any, config?: any): Promise => {
+ const res = await api.post(url, data, config);
+ return res as unknown as T;
+ },
+ put: async (url: string, data?: any, config?: any): Promise => {
+ const res = await api.put(url, data, config);
+ return res as unknown as T;
+ },
+ del: async (url: string, config?: any): Promise => {
+ const res = await api.delete(url, config);
+ return res as unknown as T;
+ }
+};
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..365b4aa
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,35 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "es6"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "typeRoots": [
+ "./node_modules/@types"
+ ],
+ "types": [
+ "node",
+ "react",
+ "react-dom",
+ "jest"
+ ]
+ },
+ "include": [
+ "src"
+ ]
+}