You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

254 lines
7.5 KiB

import React, { useState, useRef, useEffect } from 'react';
import { Card, Button, Space, Tag, message } from 'antd';
import {
SaveOutlined,
ReloadOutlined,
BugOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons';
import { vulnerabilityService } from '../../services/api';
import { Vulnerability } from '../../types';
import './CodeEditor.css';
interface CodeEditorProps {
filePath: string;
content: string;
language: string;
vulnerabilities: Vulnerability[];
onSave: (content: string) => Promise<void>;
onRefresh: () => void;
}
const CodeEditor: React.FC<CodeEditorProps> = ({
filePath,
content,
language,
vulnerabilities,
onSave,
onRefresh
}) => {
const [editedContent, setEditedContent] = useState(content);
const [selectedVulnerability, setSelectedVulnerability] = useState<Vulnerability | null>(null);
const [isSaving, setIsSaving] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(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: <ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />,
high: <ExclamationCircleOutlined style={{ color: '#ff7a45' }} />,
medium: <BugOutlined style={{ color: '#ffa940' }} />,
low: <CheckCircleOutlined style={{ color: '#73d13d' }} />,
info: <CheckCircleOutlined style={{ color: '#40a9ff' }} />,
};
return iconMap[severity] || <BugOutlined />;
};
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 || 1;
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) => (
<div key={index} className="line-number">
{index + 1}
</div>
));
};
const renderVulnerabilityMarkers = () => {
return vulnerabilities.map((vuln) => (
<div
key={vuln.id}
className="vulnerability-marker"
style={{
top: `${((vuln.line_number || 1) - 1) * 20 + 2}px`,
backgroundColor: getSeverityColor(vuln.severity),
}}
onClick={() => handleVulnerabilityClick(vuln)}
title={`${vuln.severity.toUpperCase()}: ${vuln.message}`}
/>
));
};
return (
<div className="code-editor-container">
<Card
title={
<div className="editor-header">
<span className="file-path">{filePath}</span>
<Space>
<Button
icon={<ReloadOutlined />}
onClick={onRefresh}
size="small"
>
</Button>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={handleSave}
loading={isSaving}
size="small"
>
</Button>
</Space>
</div>
}
extra={
<Space>
{vulnerabilities.length > 0 && (
<Tag color="red">
{vulnerabilities.length}
</Tag>
)}
</Space>
}
>
<div className="editor-content">
{/* 漏洞标记侧边栏 */}
<div className="vulnerability-sidebar">
{renderVulnerabilityMarkers()}
</div>
{/* 行号 */}
<div className="line-numbers">
{renderLineNumbers()}
</div>
{/* 代码编辑器 */}
<div className="editor-main">
<textarea
ref={textareaRef}
className="code-textarea"
value={editedContent}
onChange={(e) => setEditedContent(e.target.value)}
spellCheck={false}
style={{ fontFamily: 'Monaco, Consolas, "Courier New", monospace' }}
/>
</div>
</div>
{/* 漏洞详情面板 */}
{selectedVulnerability && (
<Card
title={
<div className="vulnerability-header">
{getSeverityIcon(selectedVulnerability.severity)}
<span style={{ marginLeft: 8 }}>
- {selectedVulnerability.line_number || 1}
</span>
<Tag
color={getSeverityColor(selectedVulnerability.severity)}
style={{ marginLeft: 8 }}
>
{selectedVulnerability.severity.toUpperCase()}
</Tag>
</div>
}
size="small"
style={{ marginTop: 16 }}
extra={
<Button
type="primary"
size="small"
onClick={() => applyAIFix(selectedVulnerability)}
disabled={!selectedVulnerability.ai_suggestion}
>
AI
</Button>
}
>
<div className="vulnerability-details">
<p><strong>ID:</strong> {selectedVulnerability.rule_id}</p>
<p><strong>:</strong> {selectedVulnerability.message}</p>
{selectedVulnerability.ai_suggestion && (
<div className="ai-suggestion">
<p><strong>AI:</strong></p>
<div className="suggestion-content">
{selectedVulnerability.ai_suggestion}
</div>
{selectedVulnerability.ai_confidence && (
<p className="confidence">
<strong>:</strong> {(selectedVulnerability.ai_confidence * 100).toFixed(1)}%
</p>
)}
</div>
)}
</div>
</Card>
)}
</Card>
</div>
);
};
export default CodeEditor;