diff --git a/src/components/audit/CreateTaskDialog.tsx b/src/components/audit/CreateTaskDialog.tsx index bb5701d..ffa061e 100644 --- a/src/components/audit/CreateTaskDialog.tsx +++ b/src/components/audit/CreateTaskDialog.tsx @@ -24,6 +24,7 @@ import { toast } from "sonner"; import TerminalProgressDialog from "./TerminalProgressDialog"; import { runRepositoryAudit } from "@/features/projects/services/repoScan"; import { scanZipFile, validateZipFile } from "@/features/projects/services/repoZipScan"; +import { loadZipFile } from "@/shared/utils/zipStorage"; interface CreateTaskDialogProps { open: boolean; @@ -40,6 +41,8 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr const [showTerminalDialog, setShowTerminalDialog] = useState(false); const [currentTaskId, setCurrentTaskId] = useState(null); const [zipFile, setZipFile] = useState(null); + const [loadingZipFile, setLoadingZipFile] = useState(false); + const [hasLoadedZip, setHasLoadedZip] = useState(false); const [taskForm, setTaskForm] = useState({ project_id: "", @@ -72,9 +75,40 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr if (preselectedProjectId) { setTaskForm(prev => ({ ...prev, project_id: preselectedProjectId })); } + // 重置ZIP文件状态 + setZipFile(null); + setHasLoadedZip(false); } }, [open, preselectedProjectId]); + // 当项目ID变化时,尝试自动加载保存的ZIP文件 + useEffect(() => { + const autoLoadZipFile = async () => { + if (!taskForm.project_id || hasLoadedZip) return; + + const project = projects.find(p => p.id === taskForm.project_id); + if (!project || project.repository_type !== 'other') return; + + try { + setLoadingZipFile(true); + const savedFile = await loadZipFile(taskForm.project_id); + + if (savedFile) { + setZipFile(savedFile); + setHasLoadedZip(true); + console.log('✓ 已自动加载保存的ZIP文件:', savedFile.name); + toast.success(`已加载保存的ZIP文件: ${savedFile.name}`); + } + } catch (error) { + console.error('自动加载ZIP文件失败:', error); + } finally { + setLoadingZipFile(false); + } + }; + + autoLoadZipFile(); + }, [taskForm.project_id, projects, hasLoadedZip]); + const loadProjects = async () => { try { setLoading(true); @@ -338,61 +372,86 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
-
- -
-

ZIP项目需要上传文件

-

- 该项目是通过ZIP上传创建的,请重新上传ZIP文件进行扫描 -

+ {loadingZipFile ? ( +
+
+

正在加载保存的ZIP文件...

-
- -
- - { - const file = e.target.files?.[0]; - if (file) { - console.log('📁 选择的文件:', { - name: file.name, - size: file.size, - type: file.type, - sizeMB: (file.size / 1024 / 1024).toFixed(2) - }); - - const validation = validateZipFile(file); - if (!validation.valid) { - toast.error(validation.error || "文件无效"); - e.target.value = ''; - return; - } - setZipFile(file); - - const sizeMB = (file.size / 1024 / 1024).toFixed(2); - const sizeKB = (file.size / 1024).toFixed(2); - const sizeText = file.size >= 1024 * 1024 ? `${sizeMB} MB` : `${sizeKB} KB`; - - toast.success(`已选择文件: ${file.name} (${sizeText})`); - } - }} - className="cursor-pointer" - /> - {zipFile && ( -

- ✓ 已选择: {zipFile.name} ( - {zipFile.size >= 1024 * 1024 - ? `${(zipFile.size / 1024 / 1024).toFixed(2)} MB` - : zipFile.size >= 1024 - ? `${(zipFile.size / 1024).toFixed(2)} KB` - : `${zipFile.size} B` - }) -

- )} -
+ ) : zipFile ? ( +
+ +
+

已准备就绪

+

+ 使用保存的ZIP文件: {zipFile.name} ( + {zipFile.size >= 1024 * 1024 + ? `${(zipFile.size / 1024 / 1024).toFixed(2)} MB` + : zipFile.size >= 1024 + ? `${(zipFile.size / 1024).toFixed(2)} KB` + : `${zipFile.size} B` + }) +

+
+ +
+ ) : ( + <> +
+ +
+

需要上传ZIP文件

+

+ 未找到保存的ZIP文件,请上传文件进行扫描 +

+
+
+ +
+ + { + const file = e.target.files?.[0]; + if (file) { + console.log('📁 选择的文件:', { + name: file.name, + size: file.size, + type: file.type, + sizeMB: (file.size / 1024 / 1024).toFixed(2) + }); + + const validation = validateZipFile(file); + if (!validation.valid) { + toast.error(validation.error || "文件无效"); + e.target.value = ''; + return; + } + setZipFile(file); + setHasLoadedZip(true); + + const sizeMB = (file.size / 1024 / 1024).toFixed(2); + const sizeKB = (file.size / 1024).toFixed(2); + const sizeText = file.size >= 1024 * 1024 ? `${sizeMB} MB` : `${sizeKB} KB`; + + toast.success(`已选择文件: ${file.name} (${sizeText})`); + } + }} + className="cursor-pointer" + /> +
+ + )}
diff --git a/src/components/system/SystemConfig.tsx b/src/components/system/SystemConfig.tsx index 6335ea3..9671352 100644 --- a/src/components/system/SystemConfig.tsx +++ b/src/components/system/SystemConfig.tsx @@ -139,7 +139,7 @@ export function SystemConfig() { const parsedConfig = JSON.parse(savedConfig); setConfig(parsedConfig); setConfigSource('runtime'); - toast.success("已加载运行时配置"); + console.log('已加载运行时配置'); } else { // 使用构建时配置 loadFromEnv(); diff --git a/src/pages/ProjectDetail.tsx b/src/pages/ProjectDetail.tsx index 292d51f..1c7a2f0 100644 --- a/src/pages/ProjectDetail.tsx +++ b/src/pages/ProjectDetail.tsx @@ -24,8 +24,9 @@ import { FileText } from "lucide-react"; import { api } from "@/shared/config/database"; -import { runRepositoryAudit } from "@/features/projects/services"; +import { runRepositoryAudit, scanZipFile } from "@/features/projects/services"; import type { Project, AuditTask, CreateProjectForm } from "@/shared/types"; +import { loadZipFile } from "@/shared/utils/zipStorage"; import { toast } from "sonner"; import CreateTaskDialog from "@/components/audit/CreateTaskDialog"; import TerminalProgressDialog from "@/components/audit/TerminalProgressDialog"; @@ -81,34 +82,76 @@ export default function ProjectDetail() { const handleRunAudit = async () => { if (!project || !id) return; - if (!project.repository_url || project.repository_type !== 'github') { - toast.error('请在项目中配置 GitHub 仓库地址'); - return; - } - try { - setScanning(true); - console.log('开始启动审计任务...'); - const taskId = await runRepositoryAudit({ - projectId: id, - repoUrl: project.repository_url, - branch: project.default_branch || 'main', - githubToken: undefined, - createdBy: undefined - }); - - console.log('审计任务创建成功,taskId:', taskId); - - // 显示终端进度窗口 - setCurrentTaskId(taskId); - setShowTerminalDialog(true); - - // 重新加载项目数据 - loadProjectData(); - } catch (e: any) { - console.error('启动审计失败:', e); - toast.error(e?.message || '启动审计失败'); - } finally { - setScanning(false); + + // 如果是GitHub项目且有仓库地址,直接启动审计 + if (project.repository_type === 'github' && project.repository_url) { + try { + setScanning(true); + console.log('开始启动审计任务...'); + const taskId = await runRepositoryAudit({ + projectId: id, + repoUrl: project.repository_url, + branch: project.default_branch || 'main', + githubToken: undefined, + createdBy: undefined + }); + + console.log('审计任务创建成功,taskId:', taskId); + + // 显示终端进度窗口 + setCurrentTaskId(taskId); + setShowTerminalDialog(true); + + // 重新加载项目数据 + loadProjectData(); + } catch (e: any) { + console.error('启动审计失败:', e); + toast.error(e?.message || '启动审计失败'); + } finally { + setScanning(false); + } + } else { + // 对于ZIP项目,尝试从IndexedDB加载保存的文件 + try { + setScanning(true); + const file = await loadZipFile(id); + + if (file) { + console.log('找到保存的ZIP文件,开始启动审计...'); + try { + // 启动ZIP文件审计 + const taskId = await scanZipFile({ + projectId: id, + zipFile: file, + excludePatterns: ['node_modules/**', '.git/**', 'dist/**', 'build/**'], + createdBy: 'local-user' + }); + + console.log('审计任务创建成功,taskId:', taskId); + + // 显示终端进度窗口 + setCurrentTaskId(taskId); + setShowTerminalDialog(true); + + // 重新加载项目数据 + loadProjectData(); + } catch (e: any) { + console.error('启动审计失败:', e); + toast.error(e?.message || '启动审计失败'); + } finally { + setScanning(false); + } + } else { + setScanning(false); + toast.error('未找到保存的ZIP文件,请通过"新建任务"上传'); + setShowCreateTaskDialog(true); + } + } catch (error) { + console.error('启动审计失败:', error); + setScanning(false); + toast.error('读取ZIP文件失败,请通过"新建任务"重新上传'); + setShowCreateTaskDialog(true); + } } }; diff --git a/src/pages/Projects.tsx b/src/pages/Projects.tsx index 589d551..f588737 100644 --- a/src/pages/Projects.tsx +++ b/src/pages/Projects.tsx @@ -29,12 +29,12 @@ import { CheckCircle } from "lucide-react"; import { api } from "@/shared/config/database"; -import { scanZipFile, validateZipFile } from "@/features/projects/services"; +import { validateZipFile } from "@/features/projects/services"; import type { Project, CreateProjectForm } from "@/shared/types"; +import { saveZipFile } from "@/shared/utils/zipStorage"; import { Link } from "react-router-dom"; import { toast } from "sonner"; import CreateTaskDialog from "@/components/audit/CreateTaskDialog"; -import TerminalProgressDialog from "@/components/audit/TerminalProgressDialog"; export default function Projects() { const [projects, setProjects] = useState([]); @@ -46,8 +46,6 @@ export default function Projects() { const [uploadProgress, setUploadProgress] = useState(0); const [uploading, setUploading] = useState(false); const fileInputRef = useRef(null); - const [showTerminalDialog, setShowTerminalDialog] = useState(false); - const [currentTaskId, setCurrentTaskId] = useState(null); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [projectToDelete, setProjectToDelete] = useState(null); const [showEditDialog, setShowEditDialog] = useState(false); @@ -144,30 +142,29 @@ export default function Projects() { setUploading(true); setUploadProgress(0); + // 模拟上传进度 + const progressInterval = setInterval(() => { + setUploadProgress(prev => { + if (prev >= 100) { + clearInterval(progressInterval); + return 100; + } + return prev + 20; + }); + }, 100); + // 创建项目 const project = await api.createProject({ ...createForm, repository_type: "other" } as any); - // 模拟上传进度 - const progressInterval = setInterval(() => { - setUploadProgress(prev => { - if (prev >= 90) { - clearInterval(progressInterval); - return 90; - } - return prev + 10; - }); - }, 200); - - // 扫描ZIP文件 - const taskId = await scanZipFile({ - projectId: project.id, - zipFile: file, - excludePatterns: ['node_modules/**', '.git/**', 'dist/**', 'build/**'], - createdBy: 'local-user' // 使用默认本地用户ID - }); + // 保存ZIP文件到IndexedDB(使用项目ID作为key) + try { + await saveZipFile(project.id, file); + } catch (error) { + console.error('保存ZIP文件失败:', error); + } clearInterval(progressInterval); setUploadProgress(100); @@ -177,9 +174,10 @@ export default function Projects() { resetCreateForm(); loadProjects(); - // 显示终端进度窗口 - setCurrentTaskId(taskId); - setShowTerminalDialog(true); + toast.success(`项目 "${project.name}" 已创建`, { + description: 'ZIP文件已保存,您可以启动代码审计', + duration: 4000 + }); } catch (error: any) { console.error('Upload failed:', error); @@ -520,8 +518,8 @@ export default function Projects() {
  • • 请确保 ZIP 文件包含完整的项目代码
  • • 系统会自动排除 node_modules、.git 等目录
  • -
  • • 上传后将立即开始代码分析
  • -
  • • 分析完成后可在任务详情页查看结果
  • +
  • • ZIP 文件会保存,只需上传一次
  • +
  • • 创建后可在项目详情页启动多次审计
@@ -766,14 +764,6 @@ export default function Projects() { preselectedProjectId={selectedProjectForTask} /> - {/* 终端进度对话框 */} - - {/* 编辑项目对话框 */} diff --git a/src/pages/RecycleBin.tsx b/src/pages/RecycleBin.tsx index 53a474f..4ec1162 100644 --- a/src/pages/RecycleBin.tsx +++ b/src/pages/RecycleBin.tsx @@ -18,6 +18,7 @@ import { import { api } from "@/shared/config/database"; import type { Project } from "@/shared/types"; import { toast } from "sonner"; +import { deleteZipFile } from "@/shared/utils/zipStorage"; export default function RecycleBin() { const [deletedProjects, setDeletedProjects] = useState([]); @@ -73,7 +74,16 @@ export default function RecycleBin() { if (!selectedProject) return; try { + // 删除项目数据 await api.permanentlyDeleteProject(selectedProject.id); + + // 删除保存的ZIP文件(如果有) + try { + await deleteZipFile(selectedProject.id); + } catch (error) { + console.error('删除ZIP文件失败:', error); + } + toast.success(`项目 "${selectedProject.name}" 已永久删除`); setShowPermanentDeleteDialog(false); setSelectedProject(null); diff --git a/src/shared/utils/zipStorage.ts b/src/shared/utils/zipStorage.ts new file mode 100644 index 0000000..e05da79 --- /dev/null +++ b/src/shared/utils/zipStorage.ts @@ -0,0 +1,240 @@ +/** + * ZIP文件存储工具 + * 用于管理保存在IndexedDB中的ZIP文件 + */ + +const DB_NAME = 'xcodereviewer_files'; +const STORE_NAME = 'zipFiles'; + +/** + * 保存ZIP文件到IndexedDB + */ +export async function saveZipFile(projectId: string, file: File): Promise { + // 检查浏览器是否支持IndexedDB + if (!window.indexedDB) { + throw new Error('您的浏览器不支持IndexedDB,无法保存ZIP文件'); + } + + return new Promise((resolve, reject) => { + // 不指定版本号,让IndexedDB使用当前最新版本 + const dbRequest = indexedDB.open(DB_NAME); + + dbRequest.onupgradeneeded = (event) => { + try { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME); + } + } catch (error) { + console.error('创建对象存储失败:', error); + reject(new Error('创建存储结构失败,请检查浏览器设置')); + } + }; + + dbRequest.onsuccess = async (event) => { + const db = (event.target as IDBOpenDBRequest).result; + + // 检查对象存储是否存在,如果不存在则需要升级数据库 + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.close(); + // 增加版本号以触发onupgradeneeded + const upgradeRequest = indexedDB.open(DB_NAME, db.version + 1); + + upgradeRequest.onupgradeneeded = (event) => { + try { + const upgradeDb = (event.target as IDBOpenDBRequest).result; + if (!upgradeDb.objectStoreNames.contains(STORE_NAME)) { + upgradeDb.createObjectStore(STORE_NAME); + } + } catch (error) { + console.error('升级数据库时创建对象存储失败:', error); + } + }; + + upgradeRequest.onsuccess = async (event) => { + const upgradeDb = (event.target as IDBOpenDBRequest).result; + await performSave(upgradeDb, file, projectId, resolve, reject); + }; + + upgradeRequest.onerror = (event) => { + const error = (event.target as IDBOpenDBRequest).error; + console.error('升级数据库失败:', error); + reject(new Error(`升级数据库失败: ${error?.message || '未知错误'}`)); + }; + } else { + await performSave(db, file, projectId, resolve, reject); + } + }; + + dbRequest.onerror = (event) => { + const error = (event.target as IDBOpenDBRequest).error; + console.error('打开IndexedDB失败:', error); + const errorMsg = error?.message || '未知错误'; + reject(new Error(`无法打开本地存储,可能是隐私模式或存储权限问题: ${errorMsg}`)); + }; + + dbRequest.onblocked = () => { + console.warn('数据库被阻塞,可能有其他标签页正在使用'); + reject(new Error('数据库被占用,请关闭其他标签页后重试')); + }; + }); +} + +async function performSave( + db: IDBDatabase, + file: File, + projectId: string, + resolve: () => void, + reject: (error: Error) => void +) { + try { + const arrayBuffer = await file.arrayBuffer(); + const transaction = db.transaction([STORE_NAME], 'readwrite'); + const store = transaction.objectStore(STORE_NAME); + + const putRequest = store.put({ + buffer: arrayBuffer, + fileName: file.name, + fileSize: file.size, + uploadedAt: new Date().toISOString() + }, projectId); + + putRequest.onerror = (event) => { + const error = (event.target as IDBRequest).error; + console.error('写入数据失败:', error); + reject(new Error(`保存ZIP文件失败: ${error?.message || '未知错误'}`)); + }; + + transaction.oncomplete = () => { + console.log(`ZIP文件已保存到项目 ${projectId} (${(file.size / 1024 / 1024).toFixed(2)} MB)`); + db.close(); + resolve(); + }; + + transaction.onerror = (event) => { + const error = (event.target as IDBTransaction).error; + console.error('事务失败:', error); + reject(new Error(`保存事务失败: ${error?.message || '未知错误'}`)); + }; + + transaction.onabort = () => { + console.error('事务被中止'); + reject(new Error('保存操作被中止')); + }; + } catch (error) { + console.error('保存ZIP文件时发生异常:', error); + reject(error as Error); + } +} + +/** + * 从IndexedDB加载ZIP文件 + */ +export async function loadZipFile(projectId: string): Promise { + return new Promise((resolve, reject) => { + // 不指定版本号,让IndexedDB使用当前最新版本 + const dbRequest = indexedDB.open(DB_NAME); + + dbRequest.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME); + } + }; + + dbRequest.onsuccess = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.close(); + resolve(null); + return; + } + + const transaction = db.transaction([STORE_NAME], 'readonly'); + const store = transaction.objectStore(STORE_NAME); + const getRequest = store.get(projectId); + + getRequest.onsuccess = () => { + const savedFile = getRequest.result; + + if (savedFile && savedFile.buffer) { + const blob = new Blob([savedFile.buffer], { type: 'application/zip' }); + const file = new File([blob], savedFile.fileName, { type: 'application/zip' }); + resolve(file); + } else { + resolve(null); + } + }; + + getRequest.onerror = () => { + reject(new Error('读取ZIP文件失败')); + }; + }; + + dbRequest.onerror = () => { + // 数据库打开失败,可能是首次使用,返回null而不是报错 + console.warn('打开ZIP文件数据库失败,可能是首次使用'); + resolve(null); + }; + }); +} + +/** + * 删除ZIP文件 + */ +export async function deleteZipFile(projectId: string): Promise { + return new Promise((resolve, reject) => { + // 不指定版本号,让IndexedDB使用当前最新版本 + const dbRequest = indexedDB.open(DB_NAME); + + dbRequest.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME); + } + }; + + dbRequest.onsuccess = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.close(); + resolve(); + return; + } + + const transaction = db.transaction([STORE_NAME], 'readwrite'); + const store = transaction.objectStore(STORE_NAME); + const deleteRequest = store.delete(projectId); + + deleteRequest.onsuccess = () => { + console.log(`已删除项目 ${projectId} 的ZIP文件`); + resolve(); + }; + + deleteRequest.onerror = () => { + reject(new Error('删除ZIP文件失败')); + }; + }; + + dbRequest.onerror = () => { + // 数据库打开失败,可能文件不存在,直接resolve + console.warn('打开ZIP文件数据库失败,跳过删除操作'); + resolve(); + }; + }); +} + +/** + * 检查是否存在ZIP文件 + */ +export async function hasZipFile(projectId: string): Promise { + try { + const file = await loadZipFile(projectId); + return file !== null; + } catch { + return false; + } +} +