diff --git a/.env.example b/.env.example index e3f410c..15b497f 100644 --- a/.env.example +++ b/.env.example @@ -89,8 +89,16 @@ VITE_LLM_PROVIDER=gemini # # 更多模型: https://ollama.com/library -# ==================== Supabase 数据库配置 (可选) ==================== -# 如果不配置,系统将以演示模式运行,数据不会持久化 +# ==================== 数据库配置 ==================== +# 数据库模式选择: +# 1. 本地数据库模式(推荐):设置 VITE_USE_LOCAL_DB=true,数据存储在浏览器 IndexedDB 中 +# 2. Supabase 云端模式:配置 Supabase URL 和 Key,数据存储在云端 +# 3. 演示模式:不配置任何数据库,使用演示数据(数据不持久化) + +# 使用本地数据库(IndexedDB) +# VITE_USE_LOCAL_DB=true + +# Supabase 云端数据库配置 (可选) # 获取配置: https://supabase.com/ # VITE_SUPABASE_URL=https://your-project.supabase.co # VITE_SUPABASE_ANON_KEY=your-anon-key-here @@ -107,3 +115,9 @@ VITE_APP_ID=xcodereviewer VITE_MAX_ANALYZE_FILES=40 VITE_LLM_CONCURRENCY=2 VITE_LLM_GAP_MS=500 + +# ==================== 输出语言配置 ==================== +# 设置 LLM 分析结果的输出语言 +# zh-CN: 简体中文(默认) +# en-US: 英文 +VITE_OUTPUT_LANGUAGE=zh-CN diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..e69de29 diff --git a/Dockerfile b/Dockerfile index 48a4b34..18a2382 100644 --- a/Dockerfile +++ b/Dockerfile @@ -82,11 +82,15 @@ ARG VITE_SUPABASE_ANON_KEY # GitHub 配置 ARG VITE_GITHUB_TOKEN +# 数据库配置 +ARG VITE_USE_LOCAL_DB + # 应用配置 ARG VITE_APP_ID ARG VITE_MAX_ANALYZE_FILES ARG VITE_LLM_CONCURRENCY ARG VITE_LLM_GAP_MS +ARG VITE_OUTPUT_LANGUAGE # 将构建参数转换为环境变量(Vite 构建时会读取这些环境变量) ENV VITE_LLM_PROVIDER=$VITE_LLM_PROVIDER @@ -133,6 +137,7 @@ ENV VITE_OLLAMA_API_KEY=$VITE_OLLAMA_API_KEY ENV VITE_OLLAMA_MODEL=$VITE_OLLAMA_MODEL ENV VITE_OLLAMA_BASE_URL=$VITE_OLLAMA_BASE_URL +ENV VITE_USE_LOCAL_DB=$VITE_USE_LOCAL_DB ENV VITE_SUPABASE_URL=$VITE_SUPABASE_URL ENV VITE_SUPABASE_ANON_KEY=$VITE_SUPABASE_ANON_KEY @@ -142,6 +147,7 @@ ENV VITE_APP_ID=$VITE_APP_ID ENV VITE_MAX_ANALYZE_FILES=$VITE_MAX_ANALYZE_FILES ENV VITE_LLM_CONCURRENCY=$VITE_LLM_CONCURRENCY ENV VITE_LLM_GAP_MS=$VITE_LLM_GAP_MS +ENV VITE_OUTPUT_LANGUAGE=$VITE_OUTPUT_LANGUAGE # 复制依赖文件 COPY package.json pnpm-lock.yaml ./ diff --git a/README.md b/README.md index bd53761..2becccb 100644 --- a/README.md +++ b/README.md @@ -140,9 +140,15 @@ VITE_CLAUDE_API_KEY=your_claude_api_key_here # ... 支持10+主流平台 - # Supabase 配置 (可选,用于数据持久化) - VITE_SUPABASE_URL=https://your-project.supabase.co - VITE_SUPABASE_ANON_KEY=your-anon-key-here + # 数据库配置 (三种模式可选) + # 1. 本地数据库模式(推荐)- 数据存储在浏览器 IndexedDB + VITE_USE_LOCAL_DB=true + + # 2. Supabase 云端模式 - 数据存储在云端 + # VITE_SUPABASE_URL=https://your-project.supabase.co + # VITE_SUPABASE_ANON_KEY=your-anon-key-here + + # 3. 演示模式 - 不配置任何数据库,使用演示数据(数据不持久化) # GitHub 集成 (可选,用于仓库分析) VITE_GITHUB_TOKEN=your_github_token_here @@ -154,6 +160,9 @@ VITE_MAX_ANALYZE_FILES=40 VITE_LLM_CONCURRENCY=2 VITE_LLM_GAP_MS=500 + + # 输出语言配置(zh-CN: 中文 | en-US: 英文) + VITE_OUTPUT_LANGUAGE=zh-CN ``` 4. **启动开发服务器** @@ -241,6 +250,69 @@ VITE_LLM_PROVIDER=baidu VITE_BAIDU_API_KEY=your_api_key:your_secret_key VITE_BAIDU_MODEL=ERNIE-3.5-8K ``` + + +
+Q: 数据库有哪些模式可选?如何选择? + +XCodeReviewer 支持三种数据库模式: + +**1. 本地数据库模式(推荐)** +- 数据存储在浏览器 IndexedDB 中 +- 无需配置云端服务,开箱即用 +- 数据完全本地化,隐私安全 +- 适合个人使用和快速体验 + +```env +VITE_USE_LOCAL_DB=true +``` + +**2. Supabase 云端模式** +- 数据存储在 Supabase 云端 +- 支持多设备同步 +- 需要注册 Supabase 账号并配置 +- 适合团队协作和跨设备使用 + +```env +VITE_SUPABASE_URL=https://your-project.supabase.co +VITE_SUPABASE_ANON_KEY=your-anon-key-here +``` + +**3. 演示模式** +- 不配置任何数据库 +- 使用内置演示数据 +- 数据不会持久化保存 +- 适合快速预览功能 +
+ +
+Q: 本地数据库的数据存储在哪里?如何备份? + +本地数据库使用浏览器的 IndexedDB 存储数据: + +- **存储位置**:浏览器本地存储(不同浏览器位置不同) +- **数据安全**:数据仅存储在本地,不会上传到服务器 +- **清除数据**:清除浏览器数据会删除所有本地数据 +- **备份方法**:可以在管理界面导出数据为 JSON 文件 +- **恢复方法**:通过导入 JSON 文件恢复数据 + +注意:本地数据库数据仅在当前浏览器可用,更换浏览器或设备需要重新导入数据。 +
+ +
+Q: 如何设置分析结果的输出语言? + +在 `.env` 文件中配置 `VITE_OUTPUT_LANGUAGE`: + +```env +# 中文输出(默认) +VITE_OUTPUT_LANGUAGE=zh-CN + +# 英文输出 +VITE_OUTPUT_LANGUAGE=en-US +``` + +重启应用后,所有 LLM 分析结果将使用指定的语言输出。 可在[百度千帆平台](https://console.bce.baidu.com/qianfan/)获取API Key和Secret Key。
@@ -430,6 +502,21 @@ VITE_LLM_BASE_URL=http://localhost:11434/v1 # Ollama API地址(可选) - **质量趋势分析**:通过图表展示代码质量随时间的变化趋势。 +
+💾 本地数据库管理 + +- **三种数据库模式**: + - 🏠 **本地模式**:使用浏览器 IndexedDB,数据完全本地化,隐私安全 + - ☁️ **云端模式**:使用 Supabase,支持多设备同步 + - 🎭 **演示模式**:无需配置,快速体验功能 +- **数据管理功能**: + - 📤 **导出备份**:将数据导出为 JSON 文件 + - 📥 **导入恢复**:从备份文件恢复数据 + - 🗑️ **清空数据**:一键清理所有本地数据 + - 📊 **存储监控**:实时查看存储空间使用情况 +- **智能统计**:项目、任务、问题的完整统计和可视化展示 +
+ ## 🛠️ 技术栈 | 分类 | 技术 | 说明 | @@ -440,7 +527,7 @@ VITE_LLM_BASE_URL=http://localhost:11434/v1 # Ollama API地址(可选) | **路由管理** | `React Router v6` | 单页应用路由解决方案 | | **状态管理** | `React Hooks` `Sonner` | 轻量级状态管理和通知系统 | | **AI 引擎** | `多平台 LLM` | 支持 Gemini、OpenAI、Claude、通义千问、DeepSeek 等 10+ 主流平台 | -| **后端服务** | `Supabase` `PostgreSQL` | 全栈后端即服务,实时数据库 | +| **数据存储** | `IndexedDB` `Supabase` `PostgreSQL` | 本地数据库 + 云端数据库双模式支持 | | **HTTP 客户端** | `Axios` `Ky` | 现代化的 HTTP 请求库 | | **代码质量** | `Biome` `Ast-grep` `TypeScript` | 代码格式化、静态分析和类型检查 | | **构建工具** | `Vite` `PostCSS` `Autoprefixer` | 快速的构建工具和 CSS 处理 | @@ -457,23 +544,28 @@ XCodeReviewer/ │ ├── components/ # React 组件 │ │ ├── layout/ # 布局组件 (Header, Footer, PageMeta) │ │ ├── ui/ # UI 组件库 (基于 Radix UI) +│ │ ├── database/ # 数据库管理组件 │ │ └── debug/ # 调试组件 │ ├── pages/ # 页面组件 │ │ ├── Dashboard.tsx # 仪表盘 │ │ ├── Projects.tsx # 项目管理 │ │ ├── InstantAnalysis.tsx # 即时分析 │ │ ├── AuditTasks.tsx # 审计任务 -│ │ └── AdminDashboard.tsx # 系统管理 +│ │ └── AdminDashboard.tsx # 数据库管理 │ ├── features/ # 功能模块 │ │ ├── analysis/ # 分析相关服务 │ │ │ └── services/ # AI 代码分析引擎 │ │ └── projects/ # 项目相关服务 │ │ └── services/ # 仓库扫描、ZIP 文件扫描 │ ├── shared/ # 共享工具 -│ │ ├── config/ # 配置文件 (数据库、环境变量) +│ │ ├── config/ # 配置文件 +│ │ │ ├── database.ts # 数据库统一接口 +│ │ │ ├── localDatabase.ts # IndexedDB 实现 +│ │ │ └── env.ts # 环境变量配置 │ │ ├── types/ # TypeScript 类型定义 │ │ ├── hooks/ # 自定义 React Hooks │ │ ├── utils/ # 工具函数 +│ │ │ └── initLocalDB.ts # 本地数据库初始化 │ │ └── constants/ # 常量定义 │ └── assets/ # 静态资源 │ └── styles/ # 样式文件 diff --git a/docker-compose.yml b/docker-compose.yml index 0de6b77..446c08a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -62,7 +62,8 @@ services: - VITE_OLLAMA_MODEL=${VITE_OLLAMA_MODEL:-llama3} - VITE_OLLAMA_BASE_URL=${VITE_OLLAMA_BASE_URL:-http://localhost:11434/v1} - # Supabase 配置 + # 数据库配置 + - VITE_USE_LOCAL_DB=${VITE_USE_LOCAL_DB:-true} - VITE_SUPABASE_URL=${VITE_SUPABASE_URL} - VITE_SUPABASE_ANON_KEY=${VITE_SUPABASE_ANON_KEY} @@ -74,6 +75,7 @@ services: - VITE_MAX_ANALYZE_FILES=${VITE_MAX_ANALYZE_FILES:-40} - VITE_LLM_CONCURRENCY=${VITE_LLM_CONCURRENCY:-2} - VITE_LLM_GAP_MS=${VITE_LLM_GAP_MS:-500} + - VITE_OUTPUT_LANGUAGE=${VITE_OUTPUT_LANGUAGE:-zh-CN} container_name: xcodereviewer-app ports: diff --git a/src/app/main.tsx b/src/app/main.tsx index d87b7fe..9b256b0 100644 --- a/src/app/main.tsx +++ b/src/app/main.tsx @@ -3,6 +3,13 @@ import { createRoot } from "react-dom/client"; import "@/assets/styles/globals.css"; import App from "./App.tsx"; import { AppWrapper } from "@/components/layout/PageMeta"; +import { isLocalMode } from "@/shared/config/database"; +import { initLocalDatabase } from "@/shared/utils/initLocalDB"; + +// 初始化本地数据库 +if (isLocalMode) { + initLocalDatabase().catch(console.error); +} createRoot(document.getElementById("root")!).render( diff --git a/src/app/routes.tsx b/src/app/routes.tsx index 4a272b6..cc023d0 100644 --- a/src/app/routes.tsx +++ b/src/app/routes.tsx @@ -52,7 +52,7 @@ const routes: RouteConfig[] = [ visible: false, }, { - name: "系统管理", + name: "数据库管理", path: "/admin", element: , visible: true, diff --git a/src/components/analysis/AnalysisProgressDialog.tsx b/src/components/analysis/AnalysisProgressDialog.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/database/DatabaseManager.tsx b/src/components/database/DatabaseManager.tsx new file mode 100644 index 0000000..fe6898f --- /dev/null +++ b/src/components/database/DatabaseManager.tsx @@ -0,0 +1,241 @@ +/** + * 数据库管理组件 + * 提供本地数据库的导出、导入、清空等功能 + */ + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Download, Upload, Trash2, Database, AlertCircle, CheckCircle2 } from 'lucide-react'; +import { dbMode, isLocalMode } from '@/shared/config/database'; +import { + exportLocalDatabase, + importLocalDatabase, + clearLocalDatabase, + initLocalDatabase +} from '@/shared/utils/initLocalDB'; + +export function DatabaseManager() { + const [loading, setLoading] = useState(false); + const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + // 导出数据 + const handleExport = async () => { + try { + setLoading(true); + setMessage(null); + + const jsonData = await exportLocalDatabase(); + const blob = new Blob([jsonData], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `xcodereviewer-backup-${new Date().toISOString().split('T')[0]}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + setMessage({ type: 'success', text: '数据导出成功!' }); + } catch (error) { + console.error('导出失败:', error); + setMessage({ type: 'error', text: '数据导出失败,请重试' }); + } finally { + setLoading(false); + } + }; + + // 导入数据 + const handleImport = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + try { + setLoading(true); + setMessage(null); + + const text = await file.text(); + await importLocalDatabase(text); + + setMessage({ type: 'success', text: '数据导入成功!页面将刷新...' }); + setTimeout(() => window.location.reload(), 1500); + } catch (error) { + console.error('导入失败:', error); + setMessage({ type: 'error', text: '数据导入失败,请检查文件格式' }); + } finally { + setLoading(false); + } + }; + + // 清空数据 + const handleClear = async () => { + if (!confirm('确定要清空所有本地数据吗?此操作不可恢复!')) { + return; + } + + try { + setLoading(true); + setMessage(null); + + await clearLocalDatabase(); + + setMessage({ type: 'success', text: '数据已清空!页面将刷新...' }); + setTimeout(() => window.location.reload(), 1500); + } catch (error) { + console.error('清空失败:', error); + setMessage({ type: 'error', text: '清空数据失败,请重试' }); + } finally { + setLoading(false); + } + }; + + // 初始化数据库 + const handleInit = async () => { + try { + setLoading(true); + setMessage(null); + + await initLocalDatabase(); + + setMessage({ type: 'success', text: '数据库初始化成功!' }); + } catch (error) { + console.error('初始化失败:', error); + setMessage({ type: 'error', text: '初始化失败,请重试' }); + } finally { + setLoading(false); + } + }; + + if (!isLocalMode) { + return ( + + + + + 数据库管理 + + + 当前使用 {dbMode === 'supabase' ? 'Supabase 云端' : '演示'} 模式 + + + + + + + 数据库管理功能仅在本地数据库模式下可用。 + {dbMode === 'demo' && '请在 .env 文件中配置 VITE_USE_LOCAL_DB=true 启用本地数据库。'} + + + + + ); + } + + return ( + + + + + 本地数据库管理 + + + 管理您的本地数据库,包括导出、导入和清空数据 + + + + {message && ( + + {message.type === 'success' ? ( + + ) : ( + + )} + {message.text} + + )} + +
+
+

导出数据

+

+ 将本地数据导出为 JSON 文件,用于备份或迁移 +

+ +
+ +
+

导入数据

+

+ 从 JSON 文件恢复数据 +

+ + +
+ +
+

初始化数据库

+

+ 创建默认用户和基础数据 +

+ +
+ +
+

清空数据

+

+ 删除所有本地数据(不可恢复) +

+ +
+
+ + + + + 提示:本地数据存储在浏览器中,清除浏览器数据会导致数据丢失。 + 建议定期导出备份。 + + +
+
+ ); +} diff --git a/src/components/database/DatabaseStatus.tsx b/src/components/database/DatabaseStatus.tsx new file mode 100644 index 0000000..0a1eb5e --- /dev/null +++ b/src/components/database/DatabaseStatus.tsx @@ -0,0 +1,115 @@ +/** + * 数据库状态指示器 + * 显示当前使用的数据库模式 + */ + +import { Database, Cloud, Eye } from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; +import { dbMode } from '@/shared/config/database'; + +export function DatabaseStatus() { + const getStatusConfig = () => { + switch (dbMode) { + case 'local': + return { + icon: Database, + label: '本地数据库', + variant: 'default' as const, + description: '数据存储在浏览器本地' + }; + case 'supabase': + return { + icon: Cloud, + label: 'Supabase 云端', + variant: 'secondary' as const, + description: '数据存储在云端' + }; + case 'demo': + return { + icon: Eye, + label: '演示模式', + variant: 'outline' as const, + description: '使用演示数据,不会持久化' + }; + default: + return { + icon: Database, + label: '未知模式', + variant: 'destructive' as const, + description: '' + }; + } + }; + + const config = getStatusConfig(); + const Icon = config.icon; + + return ( + + + {config.label} + + ); +} + +export function DatabaseStatusDetail() { + const getStatusConfig = () => { + switch (dbMode) { + case 'local': + return { + icon: Database, + label: '本地数据库模式', + variant: 'default' as const, + description: '数据存储在浏览器 IndexedDB 中,完全本地化,隐私安全。', + tips: '提示:定期导出数据以防丢失。' + }; + case 'supabase': + return { + icon: Cloud, + label: 'Supabase 云端模式', + variant: 'secondary' as const, + description: '数据存储在 Supabase 云端,支持多设备同步。', + tips: '提示:确保网络连接正常。' + }; + case 'demo': + return { + icon: Eye, + label: '演示模式', + variant: 'outline' as const, + description: '使用内置演示数据,所有操作不会持久化保存。', + tips: '提示:配置数据库以保存您的数据。' + }; + default: + return { + icon: Database, + label: '未知模式', + variant: 'destructive' as const, + description: '数据库配置异常', + tips: '' + }; + } + }; + + const config = getStatusConfig(); + const Icon = config.icon; + + return ( +
+
+ +
+
+
+

{config.label}

+ + {dbMode} + +
+

{config.description}

+ {config.tips && ( +

{config.tips}

+ )} +
+
+ ); +} diff --git a/src/features/analysis/services/codeAnalysis.ts b/src/features/analysis/services/codeAnalysis.ts index 88e9ef5..d80c681 100644 --- a/src/features/analysis/services/codeAnalysis.ts +++ b/src/features/analysis/services/codeAnalysis.ts @@ -1,4 +1,4 @@ -import type { CodeAnalysisResult } from "@/types/types"; +import type { CodeAnalysisResult } from "@/shared/types"; import { LLMService } from '@/shared/services/llm'; import { getCurrentLLMApiKey, getCurrentLLMModel, env } from '@/shared/config/env'; import type { LLMConfig } from '@/shared/services/llm/types'; @@ -37,6 +37,10 @@ export class CodeAnalysisEngine { static async analyzeCode(code: string, language: string): Promise { const llmService = this.createLLMService(); + + // 获取输出语言配置 + const outputLanguage = env.OUTPUT_LANGUAGE || 'zh-CN'; + const isChineseOutput = outputLanguage === 'zh-CN'; const schema = `{ "issues": [ @@ -74,9 +78,66 @@ export class CodeAnalysisEngine { } }`; - const systemPrompt = `请严格使用中文。你是一个专业代码审计助手。请从编码规范、潜在Bug、性能问题、安全漏洞、可维护性、最佳实践等维度分析代码,并严格输出 JSON(仅 JSON)符合以下 schema:\n\n${schema}`; - - const userPrompt = `语言: ${language}\n\n代码:\n\n${code}`; + // 根据配置生成不同语言的提示词 + const systemPrompt = isChineseOutput + ? `你是一个专业的代码审计助手。 + +【重要】请严格遵守以下规则: +1. 所有文本内容(title、description、suggestion、ai_explanation、xai 等)必须使用简体中文 +2. 仅输出 JSON 格式,不要添加任何额外的文字、解释或 markdown 标记 +3. 确保 JSON 格式完全正确,所有字符串值都要正确转义 + +请从以下维度全面分析代码: +- 编码规范和代码风格 +- 潜在的 Bug 和逻辑错误 +- 性能问题和优化建议 +- 安全漏洞和风险 +- 可维护性和可读性 +- 最佳实践和设计模式 + +输出格式必须严格符合以下 JSON Schema: + +${schema} + +注意: +- title: 问题的简短标题(中文) +- description: 详细描述问题(中文) +- suggestion: 具体的修复建议(中文) +- ai_explanation: AI 的深入解释(中文) +- xai.what: 这是什么问题(中文) +- xai.why: 为什么会有这个问题(中文) +- xai.how: 如何修复这个问题(中文)` + : `You are a professional code auditing assistant. + +【IMPORTANT】Please strictly follow these rules: +1. All text content (title, description, suggestion, ai_explanation, xai, etc.) MUST be in English +2. Output ONLY valid JSON format, without any additional text, explanations, or markdown markers +3. Ensure the JSON format is completely correct with all string values properly escaped + +Please comprehensively analyze the code from the following dimensions: +- Coding standards and code style +- Potential bugs and logical errors +- Performance issues and optimization suggestions +- Security vulnerabilities and risks +- Maintainability and readability +- Best practices and design patterns + +The output format MUST strictly conform to the following JSON Schema: + +${schema} + +Note: +- title: Brief title of the issue (in English) +- description: Detailed description of the issue (in English) +- suggestion: Specific fix suggestions (in English) +- ai_explanation: AI's in-depth explanation (in English) +- xai.what: What is this issue (in English) +- xai.why: Why does this issue exist (in English) +- xai.how: How to fix this issue (in English)`; + + const userPrompt = isChineseOutput + ? `编程语言: ${language}\n\n请分析以下代码:\n\n${code}` + : `Programming Language: ${language}\n\nPlease analyze the following code:\n\n${code}`; let text = ''; try { @@ -84,7 +145,7 @@ export class CodeAnalysisEngine { console.log(`📡 提供商: ${env.LLM_PROVIDER}`); console.log(`🤖 模型: ${getCurrentLLMModel()}`); console.log(`🔗 Base URL: ${env.LLM_BASE_URL || '(默认)'}`); - + // 使用新的LLM服务进行分析 const response = await llmService.complete({ messages: [ @@ -94,17 +155,17 @@ export class CodeAnalysisEngine { temperature: 0.2, }); text = response.content; - + console.log('✅ LLM 响应成功'); console.log(`📊 响应长度: ${text.length} 字符`); console.log(`📝 响应内容预览: ${text.substring(0, 200)}...`); } catch (e: any) { console.error('LLM分析失败:', e); - + // 构造更友好的错误消息 const errorMsg = e.message || '未知错误'; const provider = env.LLM_PROVIDER; - + // 抛出详细的错误信息给前端 throw new Error( `${provider} API调用失败\n\n` + @@ -118,15 +179,15 @@ export class CodeAnalysisEngine { ); } const parsed = this.safeParseJson(text); - + // 如果解析失败,抛出错误而不是返回默认值 if (!parsed) { const provider = env.LLM_PROVIDER; const currentModel = getCurrentLLMModel(); - + let suggestions = ''; if (provider === 'ollama') { - suggestions = + suggestions = `建议解决方案:\n` + `1. 升级到更强的模型(推荐):\n` + ` ollama pull codellama\n` + @@ -136,7 +197,7 @@ export class CodeAnalysisEngine { `3. 重启应用后重试\n\n` + `注意:超轻量模型仅适合测试连接,实际使用需要更强的模型。`; } else { - suggestions = + suggestions = `建议解决方案:\n` + `1. 尝试更换更强大的模型(在 .env 中修改 VITE_LLM_MODEL)\n` + `2. 检查当前模型是否支持结构化输出(JSON 格式)\n` + @@ -148,7 +209,7 @@ export class CodeAnalysisEngine { `4. 如果使用代理,检查网络连接是否稳定\n` + `5. 增加超时时间(VITE_LLM_TIMEOUT)`; } - + throw new Error( `LLM 响应解析失败\n\n` + `提供商: ${provider}\n` + @@ -158,7 +219,7 @@ export class CodeAnalysisEngine { suggestions ); } - + console.log('🔍 解析结果:', { hasIssues: Array.isArray(parsed?.issues), issuesCount: parsed?.issues?.length || 0, @@ -169,7 +230,7 @@ export class CodeAnalysisEngine { const issues = Array.isArray(parsed?.issues) ? parsed.issues : []; const metrics = parsed?.metrics ?? this.estimateMetricsFromIssues(issues); const qualityScore = parsed?.quality_score ?? this.calculateQualityScore(metrics, issues); - + console.log(`📋 最终发现 ${issues.length} 个问题`); console.log(`⭐ 质量评分: ${qualityScore}`); @@ -192,7 +253,7 @@ export class CodeAnalysisEngine { const fixJsonFormat = (str: string): string => { // 1. 去除前后空白 str = str.trim(); - + // 2. 将 JavaScript 模板字符串(反引号)替换为双引号,并处理多行内容 // 匹配: "key": `多行内容` => "key": "转义后的内容" str = str.replace(/:\s*`([\s\S]*?)`/g, (match, content) => { @@ -207,7 +268,7 @@ export class CodeAnalysisEngine { .replace(/\b/g, '\\b'); // 退格符 return `: "${escaped}"`; }); - + // 3. 处理字符串中未转义的换行符(防御性处理) // 匹配双引号字符串内的实际换行符 str = str.replace(/"([^"]*?)"/g, (match, content) => { @@ -222,51 +283,140 @@ export class CodeAnalysisEngine { } return match; }); - + // 4. 修复尾部逗号(JSON 不允许) str = str.replace(/,(\s*[}\]])/g, '$1'); - + // 5. 修复缺少逗号的问题(两个连续的 } 或 ]) str = str.replace(/\}(\s*)\{/g, '},\n{'); str = str.replace(/\](\s*)\[/g, '],\n['); - + return str; }; - - try { - // 先尝试修复后直接解析 - const fixed = fixJsonFormat(text); - return JSON.parse(fixed); - } catch (e1) { - // 如果失败,尝试提取 JSON 对象 - try { - const match = text.match(/\{[\s\S]*\}/); - if (match) { - const fixed = fixJsonFormat(match[0]); - return JSON.parse(fixed); + + // 清理和修复 JSON 字符串 + const cleanText = (str: string): string => { + // 移除 BOM 和零宽字符 + let cleaned = str + .replace(/^\uFEFF/, '') + .replace(/[\u200B-\u200D\uFEFF]/g, ''); + + // 修复字符串值中的特殊字符 + // 匹配所有 JSON 字符串值(包括 description, suggestion, code_snippet 等) + cleaned = cleaned.replace(/"([^"]+)":\s*"((?:[^"\\]|\\.)*)"/g, (match, key, value) => { + // 如果值已经正确转义,跳过 + if (!value.includes('\n') && !value.includes('\r') && !value.includes('\t') && !value.match(/[^\x20-\x7E]/)) { + return match; } - } catch (e2) { - console.warn('提取 JSON 对象后解析失败:', e2); - } - - // 尝试去除 markdown 代码块标记 - try { - const codeBlockMatch = text.match(/```(?:json)?\s*(\{[\s\S]*\})\s*```/); + + // 转义特殊字符 + let escaped = value + // 先处理已存在的反斜杠 + .replace(/\\/g, '\\\\') + // 转义换行符 + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + // 转义制表符 + .replace(/\t/g, '\\t') + // 转义引号 + .replace(/"/g, '\\"') + // 移除其他控制字符 + .replace(/[\x00-\x1F\x7F]/g, ''); + + return `"${key}": "${escaped}"`; + }); + + return cleaned; + }; + + // 尝试多种方式解析 + const attempts = [ + // 1. 直接清理和修复 + () => { + const cleaned = cleanText(text); + const fixed = fixJsonFormat(cleaned); + return JSON.parse(fixed); + }, + // 2. 提取 JSON 对象(贪婪匹配,找到第一个完整的 JSON) + () => { + const cleaned = cleanText(text); + // 找到第一个 { 的位置 + const startIdx = cleaned.indexOf('{'); + if (startIdx === -1) throw new Error('No JSON object found'); + + // 从第一个 { 开始,找到匹配的 } + let braceCount = 0; + let endIdx = -1; + for (let i = startIdx; i < cleaned.length; i++) { + if (cleaned[i] === '{') braceCount++; + if (cleaned[i] === '}') { + braceCount--; + if (braceCount === 0) { + endIdx = i + 1; + break; + } + } + } + + if (endIdx === -1) throw new Error('Incomplete JSON object'); + + const jsonStr = cleaned.substring(startIdx, endIdx); + const fixed = fixJsonFormat(jsonStr); + return JSON.parse(fixed); + }, + // 3. 去除 markdown 代码块 + () => { + const cleaned = cleanText(text); + const codeBlockMatch = cleaned.match(/```(?:json)?\s*(\{[\s\S]*\})\s*```/); if (codeBlockMatch) { const fixed = fixJsonFormat(codeBlockMatch[1]); return JSON.parse(fixed); } - } catch (e3) { - console.warn('从代码块提取 JSON 失败:', e3); + throw new Error('No code block found'); + }, + // 4. 尝试修复截断的 JSON + () => { + const cleaned = cleanText(text); + const startIdx = cleaned.indexOf('{'); + if (startIdx === -1) throw new Error('Cannot fix truncated JSON'); + + let json = cleaned.substring(startIdx); + // 尝试补全未闭合的结构 + const openBraces = (json.match(/\{/g) || []).length; + const closeBraces = (json.match(/\}/g) || []).length; + const openBrackets = (json.match(/\[/g) || []).length; + const closeBrackets = (json.match(/\]/g) || []).length; + + // 补全缺失的闭合符号 + json += ']'.repeat(Math.max(0, openBrackets - closeBrackets)); + json += '}'.repeat(Math.max(0, openBraces - closeBraces)); + + const fixed = fixJsonFormat(json); + return JSON.parse(fixed); + } + ]; + + let lastError: any = null; + for (let i = 0; i < attempts.length; i++) { + try { + return attempts[i](); + } catch (e) { + lastError = e; + if (i === 1) { + console.warn('提取 JSON 对象后解析失败:', e); + } else if (i === 2) { + console.warn('从代码块提取 JSON 失败:', e); + } } - - console.error('⚠️ 无法解析 LLM 响应为 JSON'); - console.error('原始内容(前500字符):', text.substring(0, 500)); - console.error('解析错误:', e1); - console.warn('💡 提示: 当前模型可能无法生成有效的 JSON 格式'); - console.warn(' 建议:更换更强大的模型或切换其他 LLM 提供商'); - return null; } + + // 所有尝试都失败 + console.error('⚠️ 无法解析 LLM 响应为 JSON'); + console.error('原始内容(前500字符):', text.substring(0, 500)); + console.error('解析错误:', lastError); + console.warn('💡 提示: 当前模型可能无法生成有效的 JSON 格式'); + console.warn(' 建议:更换更强大的模型或切换其他 LLM 提供商'); + return null; } private static estimateMetricsFromIssues(issues: any[]) { @@ -300,9 +450,9 @@ export class CodeAnalysisEngine { ); const metricsScore = ( - metrics.complexity + - metrics.maintainability + - metrics.security + + metrics.complexity + + metrics.maintainability + + metrics.security + metrics.performance ) / 4; diff --git a/src/features/projects/services/repoZipScan.ts b/src/features/projects/services/repoZipScan.ts index c2fa0c1..9c02cc4 100644 --- a/src/features/projects/services/repoZipScan.ts +++ b/src/features/projects/services/repoZipScan.ts @@ -15,6 +15,63 @@ function isTextFile(path: string): boolean { } function shouldExclude(path: string, excludePatterns: string[]): boolean { + // 排除 Mac 系统文件 + if (path.includes('__MACOSX/') || path.includes('/.DS_Store') || path.match(/\/\._[^/]+$/)) { + return true; + } + + // 排除 IDE 和编辑器配置目录 + const idePatterns = [ + '/.vscode/', + '/.idea/', + '/.vs/', + '/.eclipse/', + '/.settings/' + ]; + if (idePatterns.some(pattern => path.includes(pattern))) { + return true; + } + + // 排除版本控制和依赖目录 + const systemDirs = [ + '/.git/', + '/node_modules/', + '/vendor/', + '/dist/', + '/build/', + '/.next/', + '/.nuxt/', + '/target/', + '/out/', + '/__pycache__/', + '/.pytest_cache/', + '/coverage/', + '/.nyc_output/' + ]; + if (systemDirs.some(dir => path.includes(dir))) { + return true; + } + + // 排除其他隐藏文件(但保留 .gitignore, .env.example 等重要配置) + const allowedHiddenFiles = ['.gitignore', '.env.example', '.editorconfig', '.prettierrc']; + const fileName = path.split('/').pop() || ''; + if (fileName.startsWith('.') && !allowedHiddenFiles.includes(fileName)) { + return true; + } + + // 排除常见的非代码文件 + const excludeExtensions = [ + '.lock', '.log', '.tmp', '.temp', '.cache', + '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', + '.pdf', '.zip', '.tar', '.gz', '.rar', + '.exe', '.dll', '.so', '.dylib', + '.min.js', '.min.css', '.map' + ]; + if (excludeExtensions.some(ext => path.toLowerCase().endsWith(ext))) { + return true; + } + + // 应用用户自定义的排除模式 return excludePatterns.some(pattern => { if (pattern.includes('*')) { const regex = new RegExp(pattern.replace(/\*/g, '.*')); diff --git a/src/pages/AdminDashboard.tsx b/src/pages/AdminDashboard.tsx index a820c57..82aad9b 100644 --- a/src/pages/AdminDashboard.tsx +++ b/src/pages/AdminDashboard.tsx @@ -1,157 +1,159 @@ import { useState, useEffect } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; -import { Input } from "@/components/ui/input"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Progress } from "@/components/ui/progress"; import { - Users, - Settings, - Shield, - Activity, - AlertTriangle, - Search, - Edit, - Trash2, - UserPlus, Database, - Server, - BarChart3 + HardDrive, + RefreshCw, + Info, + CheckCircle2, + AlertCircle, + FolderOpen, + Clock, + AlertTriangle, + TrendingUp, + Package } from "lucide-react"; -import { api } from "@/shared/config/database"; -import type { Profile } from "@/shared/types"; +import { api, dbMode, isLocalMode } from "@/shared/config/database"; +import { DatabaseManager } from "@/components/database/DatabaseManager"; +import { DatabaseStatusDetail } from "@/components/database/DatabaseStatus"; import { toast } from "sonner"; export default function AdminDashboard() { - const user = null as any; - const [profiles, setProfiles] = useState([]); + const [stats, setStats] = useState({ + totalProjects: 0, + activeProjects: 0, + totalTasks: 0, + completedTasks: 0, + totalIssues: 0, + resolvedIssues: 0, + storageUsed: '计算中...', + storageQuota: '未知' + }); const [loading, setLoading] = useState(true); - const [searchTerm, setSearchTerm] = useState(""); - const [editingUser, setEditingUser] = useState(null); - const [showEditDialog, setShowEditDialog] = useState(false); + const [storageDetails, setStorageDetails] = useState<{ + usage: number; + quota: number; + percentage: number; + } | null>(null); useEffect(() => { - loadProfiles(); + loadStats(); }, []); - const loadProfiles = async () => { + const loadStats = async () => { try { setLoading(true); - const data = await api.getAllProfiles(); - setProfiles(data); + const projectStats = await api.getProjectStats(); + + // 获取存储使用量(IndexedDB) + let storageUsed = '未知'; + let storageQuota = '未知'; + let details = null; + + if ('storage' in navigator && 'estimate' in navigator.storage) { + try { + const estimate = await navigator.storage.estimate(); + const usedMB = ((estimate.usage || 0) / 1024 / 1024).toFixed(2); + const quotaMB = ((estimate.quota || 0) / 1024 / 1024).toFixed(2); + const percentage = estimate.quota ? ((estimate.usage || 0) / estimate.quota * 100) : 0; + + storageUsed = `${usedMB} MB`; + storageQuota = `${quotaMB} MB`; + + details = { + usage: estimate.usage || 0, + quota: estimate.quota || 0, + percentage: Math.round(percentage) + }; + } catch (e) { + console.error('Failed to estimate storage:', e); + } + } + + setStats({ + totalProjects: projectStats.total_projects || 0, + activeProjects: projectStats.active_projects || 0, + totalTasks: projectStats.total_tasks || 0, + completedTasks: projectStats.completed_tasks || 0, + totalIssues: projectStats.total_issues || 0, + resolvedIssues: projectStats.resolved_issues || 0, + storageUsed, + storageQuota + }); + + setStorageDetails(details); } catch (error) { - console.error('Failed to load profiles:', error); - toast.error("加载用户数据失败"); + console.error('Failed to load stats:', error); + toast.error("加载统计数据失败"); } finally { setLoading(false); } }; - const handleUpdateUserRole = async (userId: string, newRole: 'admin' | 'member') => { - try { - await api.updateProfile(userId, { role: newRole }); - toast.success("用户角色更新成功"); - loadProfiles(); - } catch (error) { - console.error('Failed to update user role:', error); - toast.error("更新用户角色失败"); - } - }; - - const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleDateString('zh-CN', { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - }; - - const filteredProfiles = profiles.filter(profile => - profile.full_name?.toLowerCase().includes(searchTerm.toLowerCase()) || - profile.phone?.toLowerCase().includes(searchTerm.toLowerCase()) || - profile.email?.toLowerCase().includes(searchTerm.toLowerCase()) - ); - - // 检查当前用户是否为管理员 - const isAdmin = true; - - if (!isAdmin) { - return ( -
-
- -

访问被拒绝

-

您没有权限访问管理员面板

-
-
- ); - } - if (loading) { return (
-
+
+
+

加载数据库信息...

+
); } return ( -
+
{/* 页面标题 */}
-

系统管理

+

+ + 数据库管理 +

- 管理系统用户、配置和监控系统状态 + 管理和监控本地数据库,查看存储使用情况和数据统计

- - - 管理员权限 - +
- {/* 统计卡片 */} -
- - -
- -
-

总用户数

-

{profiles.length}

-
-
-
-
+ {/* 数据库模式提示 */} + {!isLocalMode && ( + + + + 当前使用 {dbMode === 'supabase' ? 'Supabase 云端' : '演示'} 模式。 + 数据库管理功能仅在本地数据库模式下完全可用。 + {dbMode === 'demo' && ' 请在 .env 文件中配置 VITE_USE_LOCAL_DB=true 启用本地数据库。'} + + + )} + {/* 数据库状态卡片 */} + + + {/* 统计概览 */} +
-
- -
-

管理员

-

- {profiles.filter(p => p.role === 'admin').length} +

+
+

项目总数

+

{stats.totalProjects}

+

+ 活跃: {stats.activeProjects}

-
- - - - - -
- -
-

活跃用户

-

- {profiles.filter(p => p.role === 'member').length} -

+
+
@@ -159,206 +161,184 @@ export default function AdminDashboard() { -
- -
-

系统状态

-

正常

+
+
+

审计任务

+

{stats.totalTasks}

+

+ 已完成: {stats.completedTasks} +

+
+
+ +
+
+ + + + + +
+
+

发现问题

+

{stats.totalIssues}

+

+ 已解决: {stats.resolvedIssues} +

+
+
+ +
+
+
+
+ + + +
+
+

存储使用

+

{stats.storageUsed}

+

+ 配额: {stats.storageQuota} +

+
+
+
- {/* 主要内容 */} - + {/* 主要内容标签页 */} + - 用户管理 - 系统监控 - 系统设置 - 操作日志 + 数据概览 + 存储管理 + 数据操作 + 设置 - - {/* 搜索和操作 */} - - -
-
- - setSearchTerm(e.target.value)} - className="pl-10" + {/* 数据概览 */} + +
+ {/* 任务完成率 */} + + + + + 任务完成率 + + 审计任务的完成情况统计 + + +
+
+ 已完成 + + {stats.totalTasks > 0 + ? Math.round((stats.completedTasks / stats.totalTasks) * 100) + : 0}% + +
+ 0 + ? (stats.completedTasks / stats.totalTasks) * 100 + : 0 + } />
- -
- - - - {/* 用户列表 */} - - - 用户列表 ({filteredProfiles.length}) - - -
- {filteredProfiles.map((profile) => ( -
-
-
- {profile.full_name?.charAt(0) || profile.phone?.charAt(0) || 'U'} -
-
-

- {profile.full_name || '未设置姓名'} -

-
- {profile.phone && ( - 📱 {profile.phone} - )} - {profile.email && ( - 📧 {profile.email} - )} -
-

- 注册时间:{formatDate(profile.created_at)} -

-
-
- -
- - {profile.role === 'admin' ? '管理员' : '普通用户'} - - - {profile.id !== user?.id && ( - - )} - - -
+
+
+

总任务数

+

{stats.totalTasks}

- ))} -
- - - - - -
- {/* 系统性能 */} - - - - - 系统性能 - - - -
-
- CPU 使用率 - 15% -
-
-
-
- -
- 内存使用率 - 32% -
-
-
-
- -
- 磁盘使用率 - 58% -
-
-
+
+

已完成

+

{stats.completedTasks}

- {/* 数据库状态 */} + {/* 问题解决率 */} - - - 数据库状态 + + + 问题解决率 + 代码问题的解决情况统计 -
- 连接状态 - 正常 +
+
+ 已解决 + + {stats.totalIssues > 0 + ? Math.round((stats.resolvedIssues / stats.totalIssues) * 100) + : 0}% + +
+ 0 + ? (stats.resolvedIssues / stats.totalIssues) * 100 + : 0 + } + className="bg-orange-100" + />
-
- 活跃连接 - 12 -
-
- 查询响应时间 - 45ms -
-
- 存储使用量 - 2.3GB +
+
+

总问题数

+

{stats.totalIssues}

+
+
+

已解决

+

{stats.resolvedIssues}

+
- {/* 系统日志 */} + {/* 数据库表统计 */} - 系统日志 + + + 数据库表统计 + + 各数据表的记录数量 -
-
-
-
-

系统启动成功

-

2024-01-15 09:00:00

+
+
+
+ +
+

项目

+

{stats.totalProjects}

+
-
-
-
-

新用户注册

-

2024-01-15 08:45:00

+
+
+ +
+

审计任务

+

{stats.totalTasks}

+
-
-
-
-

数据库连接超时

-

2024-01-15 08:30:00

+
+
+ +
+

问题

+

{stats.totalIssues}

+
@@ -366,22 +346,182 @@ export default function AdminDashboard() { + {/* 存储管理 */} + + + + + + 存储空间使用情况 + + + 浏览器 IndexedDB 存储空间的使用详情 + + + + {storageDetails ? ( + <> +
+
+ 已使用空间 + {storageDetails.percentage}% +
+ +
+ {stats.storageUsed} 已使用 + {stats.storageQuota} 总配额 +
+
+ +
+
+

已使用

+

{stats.storageUsed}

+
+
+

总配额

+

{stats.storageQuota}

+
+
+

剩余空间

+

+ {((storageDetails.quota - storageDetails.usage) / 1024 / 1024).toFixed(2)} MB +

+
+
+ + {storageDetails.percentage > 80 && ( + + + + 存储空间使用率已超过 80%,建议清理不需要的数据或导出备份后清空数据库。 + + + )} + + ) : ( + + + + 无法获取存储空间信息。您的浏览器可能不支持 Storage API。 + + + )} +
+
+ + + + 存储优化建议 + + +
+ +
+

定期导出备份

+

+ 建议定期导出数据为 JSON 文件,防止数据丢失 +

+
+
+
+ +
+

清理旧数据

+

+ 删除不再需要的项目和任务可以释放存储空间 +

+
+
+
+ +
+

监控存储使用

+

+ 定期检查存储使用情况,避免超出浏览器限制 +

+
+
+
+
+
+ + {/* 数据操作 */} + + + + + {/* 设置 */} -
- -

系统设置

-

此功能正在开发中

-
-
+ + + 数据库设置 + 配置数据库行为和性能选项 + + + + + + 当前数据库模式: {dbMode === 'local' ? '本地 IndexedDB' : dbMode === 'supabase' ? 'Supabase 云端' : '演示模式'} + + - -
- -

操作日志

-

此功能正在开发中

-
+
+
+
+

自动备份

+

+ 定期自动导出数据备份(开发中) +

+
+ 即将推出 +
+ +
+
+

数据压缩

+

+ 压缩存储数据以节省空间(开发中) +

+
+ 即将推出 +
+ +
+
+

数据同步

+

+ 在多个设备间同步数据(开发中) +

+
+ 即将推出 +
+
+
+
+ + + + 关于本地数据库 + + +

+ 本地数据库使用浏览器的 IndexedDB 技术存储数据,具有以下特点: +

+
    +
  • 数据完全存储在本地,不会上传到服务器
  • +
  • 支持离线访问,无需网络连接
  • +
  • 存储容量取决于浏览器和设备
  • +
  • 清除浏览器数据会删除所有本地数据
  • +
  • 不同浏览器的数据相互独立
  • +
+

+ 建议:定期导出数据备份,以防意外数据丢失。 +

+
+
); -} \ No newline at end of file +} diff --git a/src/pages/Projects.tsx b/src/pages/Projects.tsx index 826871c..8bd4b7a 100644 --- a/src/pages/Projects.tsx +++ b/src/pages/Projects.tsx @@ -147,7 +147,7 @@ export default function Projects() { projectId: project.id, zipFile: file, excludePatterns: ['node_modules/**', '.git/**', 'dist/**', 'build/**'], - createdBy: undefined + createdBy: 'local-user' // 使用默认本地用户ID }); clearInterval(progressInterval); diff --git a/src/shared/config/database.ts b/src/shared/config/database.ts index 51a09e6..d10c3da 100644 --- a/src/shared/config/database.ts +++ b/src/shared/config/database.ts @@ -1,4 +1,5 @@ import { createClient } from "@supabase/supabase-js"; +import { localDB } from "./localDatabase"; import type { Profile, Project, @@ -9,10 +10,11 @@ import type { CreateProjectForm, CreateAuditTaskForm, InstantAnalysisForm -} from "@/types/types"; +} from "../types/index"; const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; +const useLocalDB = import.meta.env.VITE_USE_LOCAL_DB === 'true'; const isValidUuid = (value?: string): boolean => { if (!value) return false; @@ -35,19 +37,21 @@ export const supabase = hasSupabaseConfig ? createClient(finalSupabaseUrl, final } }) : null; -// 演示模式标识 -export const isDemoMode = !hasSupabaseConfig; +// 数据库模式:local(本地IndexedDB)、supabase(云端)、demo(演示模式) +export const dbMode = useLocalDB ? 'local' : (hasSupabaseConfig ? 'supabase' : 'demo'); +export const isDemoMode = dbMode === 'demo'; +export const isLocalMode = dbMode === 'local'; // 演示数据 const demoProfile: Profile = { id: 'demo-user', - phone: null, + phone: undefined, email: 'demo@xcodereviewer.com', full_name: 'Demo User', - avatar_url: null, + avatar_url: undefined, role: 'admin', github_username: 'demo-user', - gitlab_username: null, + gitlab_username: undefined, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; @@ -60,6 +64,10 @@ export const api = { return demoProfile; } + if (isLocalMode) { + return localDB.getProfileById(id); + } + if (!supabase) return null; const { data, error } = await supabase @@ -77,6 +85,10 @@ export const api = { return 1; } + if (isLocalMode) { + return localDB.getProfilesCount(); + } + if (!supabase) return 0; const { count, error } = await supabase @@ -88,6 +100,12 @@ export const api = { }, async createProfiles(profile: Partial): Promise { + if (isLocalMode) { + return localDB.createProfile(profile); + } + + if (!supabase) throw new Error('Database not available'); + const { data, error } = await supabase .from('profiles') .insert([{ @@ -108,6 +126,12 @@ export const api = { }, async updateProfile(id: string, updates: Partial): Promise { + if (isLocalMode) { + return localDB.updateProfile(id, updates); + } + + if (!supabase) throw new Error('Database not available'); + const { data, error } = await supabase .from('profiles') .update(updates) @@ -120,6 +144,12 @@ export const api = { }, async getAllProfiles(): Promise { + if (isLocalMode) { + return localDB.getAllProfiles(); + } + + if (!supabase) return []; + const { data, error } = await supabase .from('profiles') .select('*') @@ -148,6 +178,10 @@ export const api = { }]; } + if (isLocalMode) { + return localDB.getProjects(); + } + if (!supabase) return []; const { data, error } = await supabase @@ -164,6 +198,12 @@ export const api = { }, async getProjectById(id: string): Promise { + if (isLocalMode) { + return localDB.getProjectById(id); + } + + if (!supabase) return null; + const { data, error } = await supabase .from('projects') .select(` @@ -178,6 +218,12 @@ export const api = { }, async createProject(project: CreateProjectForm & { owner_id?: string }): Promise { + if (isLocalMode) { + return localDB.createProject(project); + } + + if (!supabase) throw new Error('Database not available'); + const { data, error } = await supabase .from('projects') .insert([{ @@ -201,6 +247,12 @@ export const api = { }, async updateProject(id: string, updates: Partial): Promise { + if (isLocalMode) { + return localDB.updateProject(id, updates); + } + + if (!supabase) throw new Error('Database not available'); + const updateData: any = { ...updates }; if (updates.programming_languages) { updateData.programming_languages = JSON.stringify(updates.programming_languages); @@ -221,6 +273,12 @@ export const api = { }, async deleteProject(id: string): Promise { + if (isLocalMode) { + return localDB.deleteProject(id); + } + + if (!supabase) throw new Error('Database not available'); + const { error } = await supabase .from('projects') .update({ is_active: false }) @@ -231,6 +289,12 @@ export const api = { // ProjectMember相关 async getProjectMembers(projectId: string): Promise { + if (isLocalMode) { + return localDB.getProjectMembers(projectId); + } + + if (!supabase) return []; + const { data, error } = await supabase .from('project_members') .select(` @@ -246,6 +310,12 @@ export const api = { }, async addProjectMember(projectId: string, userId: string, role: string = 'member'): Promise { + if (isLocalMode) { + return localDB.addProjectMember(projectId, userId, role); + } + + if (!supabase) throw new Error('Database not available'); + const { data, error } = await supabase .from('project_members') .insert([{ @@ -267,6 +337,12 @@ export const api = { // AuditTask相关 async getAuditTasks(projectId?: string): Promise { + if (isLocalMode) { + return localDB.getAuditTasks(projectId); + } + + if (!supabase) return []; + let query = supabase .from('audit_tasks') .select(` @@ -286,6 +362,12 @@ export const api = { }, async getAuditTaskById(id: string): Promise { + if (isLocalMode) { + return localDB.getAuditTaskById(id); + } + + if (!supabase) return null; + const { data, error } = await supabase .from('audit_tasks') .select(` @@ -301,6 +383,12 @@ export const api = { }, async createAuditTask(task: CreateAuditTaskForm & { created_by: string }): Promise { + if (isLocalMode) { + return localDB.createAuditTask(task); + } + + if (!supabase) throw new Error('Database not available'); + const { data, error } = await supabase .from('audit_tasks') .insert([{ @@ -324,6 +412,12 @@ export const api = { }, async updateAuditTask(id: string, updates: Partial): Promise { + if (isLocalMode) { + return localDB.updateAuditTask(id, updates); + } + + if (!supabase) throw new Error('Database not available'); + const { data, error } = await supabase .from('audit_tasks') .update(updates) @@ -341,6 +435,12 @@ export const api = { // AuditIssue相关 async getAuditIssues(taskId: string): Promise { + if (isLocalMode) { + return localDB.getAuditIssues(taskId); + } + + if (!supabase) return []; + const { data, error } = await supabase .from('audit_issues') .select(` @@ -357,6 +457,12 @@ export const api = { }, async createAuditIssue(issue: Omit): Promise { + if (isLocalMode) { + return localDB.createAuditIssue(issue); + } + + if (!supabase) throw new Error('Database not available'); + const { data, error } = await supabase .from('audit_issues') .insert([issue]) @@ -372,6 +478,12 @@ export const api = { }, async updateAuditIssue(id: string, updates: Partial): Promise { + if (isLocalMode) { + return localDB.updateAuditIssue(id, updates); + } + + if (!supabase) throw new Error('Database not available'); + const { data, error } = await supabase .from('audit_issues') .update(updates) @@ -389,6 +501,12 @@ export const api = { // InstantAnalysis相关 async getInstantAnalyses(userId?: string): Promise { + if (isLocalMode) { + return localDB.getInstantAnalyses(userId); + } + + if (!supabase) return []; + let query = supabase .from('instant_analyses') .select(` @@ -413,6 +531,12 @@ export const api = { quality_score?: number; analysis_time?: number; }): Promise { + if (isLocalMode) { + return localDB.createInstantAnalysis(analysis); + } + + if (!supabase) throw new Error('Database not available'); + const { data, error } = await supabase .from('instant_analyses') .insert([{ @@ -448,6 +572,10 @@ export const api = { }; } + if (isLocalMode) { + return localDB.getProjectStats(); + } + if (!supabase) { return { total_projects: 0, diff --git a/src/shared/config/env.ts b/src/shared/config/env.ts index 51cf9b0..4c2ccba 100644 --- a/src/shared/config/env.ts +++ b/src/shared/config/env.ts @@ -77,6 +77,9 @@ export const env = { MAX_ANALYZE_FILES: Number(import.meta.env.VITE_MAX_ANALYZE_FILES) || 40, LLM_CONCURRENCY: Number(import.meta.env.VITE_LLM_CONCURRENCY) || 2, LLM_GAP_MS: Number(import.meta.env.VITE_LLM_GAP_MS) || 500, + + // ==================== 语言配置 ==================== + OUTPUT_LANGUAGE: import.meta.env.VITE_OUTPUT_LANGUAGE || 'zh-CN', // zh-CN | en-US // ==================== 开发环境标识 ==================== isDev: import.meta.env.DEV, diff --git a/src/shared/config/localDatabase.ts b/src/shared/config/localDatabase.ts new file mode 100644 index 0000000..9ce62ca --- /dev/null +++ b/src/shared/config/localDatabase.ts @@ -0,0 +1,591 @@ +/** + * 本地数据库实现 - 使用 IndexedDB + * 提供与 Supabase 相同的 API 接口,但数据存储在浏览器本地 + */ + +import type { + Profile, + Project, + ProjectMember, + AuditTask, + AuditIssue, + InstantAnalysis, + CreateProjectForm, + CreateAuditTaskForm, + InstantAnalysisForm +} from "../types/index"; + +const DB_NAME = 'xcodereviewer_local'; +const DB_VERSION = 1; + +// 数据库表名 +const STORES = { + PROFILES: 'profiles', + PROJECTS: 'projects', + PROJECT_MEMBERS: 'project_members', + AUDIT_TASKS: 'audit_tasks', + AUDIT_ISSUES: 'audit_issues', + INSTANT_ANALYSES: 'instant_analyses', +} as const; + +class LocalDatabase { + private db: IDBDatabase | null = null; + private initPromise: Promise | null = null; + + /** + * 初始化数据库 + */ + async init(): Promise { + if (this.db) return; + if (this.initPromise) return this.initPromise; + + this.initPromise = new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + + request.onerror = () => reject(request.error); + request.onsuccess = () => { + this.db = request.result; + resolve(); + }; + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + + // 创建 profiles 表 + if (!db.objectStoreNames.contains(STORES.PROFILES)) { + const profileStore = db.createObjectStore(STORES.PROFILES, { keyPath: 'id' }); + profileStore.createIndex('email', 'email', { unique: false }); + profileStore.createIndex('role', 'role', { unique: false }); + } + + // 创建 projects 表 + if (!db.objectStoreNames.contains(STORES.PROJECTS)) { + const projectStore = db.createObjectStore(STORES.PROJECTS, { keyPath: 'id' }); + projectStore.createIndex('owner_id', 'owner_id', { unique: false }); + projectStore.createIndex('is_active', 'is_active', { unique: false }); + projectStore.createIndex('created_at', 'created_at', { unique: false }); + } + + // 创建 project_members 表 + if (!db.objectStoreNames.contains(STORES.PROJECT_MEMBERS)) { + const memberStore = db.createObjectStore(STORES.PROJECT_MEMBERS, { keyPath: 'id' }); + memberStore.createIndex('project_id', 'project_id', { unique: false }); + memberStore.createIndex('user_id', 'user_id', { unique: false }); + } + + // 创建 audit_tasks 表 + if (!db.objectStoreNames.contains(STORES.AUDIT_TASKS)) { + const taskStore = db.createObjectStore(STORES.AUDIT_TASKS, { keyPath: 'id' }); + taskStore.createIndex('project_id', 'project_id', { unique: false }); + taskStore.createIndex('created_by', 'created_by', { unique: false }); + taskStore.createIndex('status', 'status', { unique: false }); + taskStore.createIndex('created_at', 'created_at', { unique: false }); + } + + // 创建 audit_issues 表 + if (!db.objectStoreNames.contains(STORES.AUDIT_ISSUES)) { + const issueStore = db.createObjectStore(STORES.AUDIT_ISSUES, { keyPath: 'id' }); + issueStore.createIndex('task_id', 'task_id', { unique: false }); + issueStore.createIndex('severity', 'severity', { unique: false }); + issueStore.createIndex('status', 'status', { unique: false }); + } + + // 创建 instant_analyses 表 + if (!db.objectStoreNames.contains(STORES.INSTANT_ANALYSES)) { + const analysisStore = db.createObjectStore(STORES.INSTANT_ANALYSES, { keyPath: 'id' }); + analysisStore.createIndex('user_id', 'user_id', { unique: false }); + analysisStore.createIndex('created_at', 'created_at', { unique: false }); + } + }; + }); + + return this.initPromise; + } + + /** + * 生成 UUID + */ + private generateId(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + + /** + * 获取对象存储 + */ + private getStore(storeName: string, mode: IDBTransactionMode = 'readonly'): IDBObjectStore { + if (!this.db) throw new Error('Database not initialized'); + const transaction = this.db.transaction(storeName, mode); + return transaction.objectStore(storeName); + } + + /** + * 通用查询方法 + */ + private async getAll(storeName: string): Promise { + await this.init(); + return new Promise((resolve, reject) => { + const store = this.getStore(storeName); + const request = store.getAll(); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + /** + * 通过 ID 获取单条记录 + */ + private async getById(storeName: string, id: string): Promise { + if (!id) return null; + + await this.init(); + return new Promise((resolve, reject) => { + const store = this.getStore(storeName); + const request = store.get(id); + request.onsuccess = () => resolve(request.result || null); + request.onerror = () => reject(request.error); + }); + } + + /** + * 通过索引查询 + */ + private async getByIndex(storeName: string, indexName: string, value: any): Promise { + await this.init(); + return new Promise((resolve, reject) => { + const store = this.getStore(storeName); + const index = store.index(indexName); + const request = index.getAll(value); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + /** + * 插入或更新记录 + */ + private async put(storeName: string, data: T): Promise { + await this.init(); + return new Promise((resolve, reject) => { + const store = this.getStore(storeName, 'readwrite'); + const request = store.put(data); + request.onsuccess = () => resolve(data); + request.onerror = () => reject(request.error); + }); + } + + /** + * 删除记录 + */ + private async deleteRecord(storeName: string, id: string): Promise { + await this.init(); + return new Promise((resolve, reject) => { + const store = this.getStore(storeName, 'readwrite'); + const request = store.delete(id); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } + + /** + * 统计记录数 + */ + private async count(storeName: string): Promise { + await this.init(); + return new Promise((resolve, reject) => { + const store = this.getStore(storeName); + const request = store.count(); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + // ==================== Profile 相关方法 ==================== + + async getProfileById(id: string): Promise { + return this.getById(STORES.PROFILES, id); + } + + async getProfilesCount(): Promise { + return this.count(STORES.PROFILES); + } + + async createProfile(profile: Partial): Promise { + const newProfile: Profile = { + id: profile.id || this.generateId(), + phone: profile.phone || undefined, + email: profile.email || undefined, + full_name: profile.full_name || undefined, + avatar_url: profile.avatar_url || undefined, + role: profile.role || 'member', + github_username: profile.github_username || undefined, + gitlab_username: profile.gitlab_username || undefined, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }; + return this.put(STORES.PROFILES, newProfile); + } + + async updateProfile(id: string, updates: Partial): Promise { + const existing = await this.getProfileById(id); + if (!existing) throw new Error('Profile not found'); + + const updated: Profile = { + ...existing, + ...updates, + id, + updated_at: new Date().toISOString(), + }; + return this.put(STORES.PROFILES, updated); + } + + async getAllProfiles(): Promise { + return this.getAll(STORES.PROFILES); + } + + // ==================== Project 相关方法 ==================== + + async getProjects(): Promise { + const projects = await this.getAll(STORES.PROJECTS); + const activeProjects = projects.filter(p => p.is_active); + + // 关联 owner 信息 + const projectsWithOwner = await Promise.all( + activeProjects.map(async (project) => { + const owner = project.owner_id ? await this.getProfileById(project.owner_id) : null; + return { ...project, owner: owner || undefined }; + }) + ); + + return projectsWithOwner.sort((a, b) => + new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + ); + } + + async getProjectById(id: string): Promise { + if (!id) return null; + + const project = await this.getById(STORES.PROJECTS, id); + if (!project) return null; + + const owner = project.owner_id ? await this.getProfileById(project.owner_id) : null; + return { ...project, owner: owner || undefined }; + } + + async createProject(projectData: CreateProjectForm & { owner_id?: string }): Promise { + const newProject: Project = { + id: this.generateId(), + name: projectData.name, + description: projectData.description || undefined, + repository_url: projectData.repository_url || undefined, + repository_type: projectData.repository_type || 'other', + default_branch: projectData.default_branch || 'main', + programming_languages: JSON.stringify(projectData.programming_languages || []), + owner_id: projectData.owner_id || 'local-user', + is_active: true, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }; + + await this.put(STORES.PROJECTS, newProject); + return this.getProjectById(newProject.id) as Promise; + } + + async updateProject(id: string, updates: Partial): Promise { + const existing = await this.getById(STORES.PROJECTS, id); + if (!existing) throw new Error('Project not found'); + + const updateData: any = { ...updates }; + if (updates.programming_languages) { + updateData.programming_languages = JSON.stringify(updates.programming_languages); + } + + const updated: Project = { + ...existing, + ...updateData, + id, + updated_at: new Date().toISOString(), + }; + + await this.put(STORES.PROJECTS, updated); + return this.getProjectById(id) as Promise; + } + + async deleteProject(id: string): Promise { + const existing = await this.getById(STORES.PROJECTS, id); + if (!existing) throw new Error('Project not found'); + + const updated: Project = { + ...existing, + is_active: false, + updated_at: new Date().toISOString(), + }; + + await this.put(STORES.PROJECTS, updated); + } + + // ==================== ProjectMember 相关方法 ==================== + + async getProjectMembers(projectId: string): Promise { + const members = await this.getByIndex(STORES.PROJECT_MEMBERS, 'project_id', projectId); + + const membersWithRelations = await Promise.all( + members.map(async (member) => { + const user = member.user_id ? await this.getProfileById(member.user_id) : null; + const project = member.project_id ? await this.getProjectById(member.project_id) : null; + return { + ...member, + user: user || undefined, + project: project || undefined + }; + }) + ); + + return membersWithRelations.sort((a, b) => + new Date(b.joined_at).getTime() - new Date(a.joined_at).getTime() + ); + } + + async addProjectMember(projectId: string, userId: string, role: string = 'member'): Promise { + const newMember: ProjectMember = { + id: this.generateId(), + project_id: projectId, + user_id: userId, + role: role as any, + permissions: '{}', + joined_at: new Date().toISOString(), + created_at: new Date().toISOString(), + }; + + await this.put(STORES.PROJECT_MEMBERS, newMember); + + const user = userId ? await this.getProfileById(userId) : null; + const project = projectId ? await this.getProjectById(projectId) : null; + + return { + ...newMember, + user: user || undefined, + project: project || undefined + }; + } + + // ==================== AuditTask 相关方法 ==================== + + async getAuditTasks(projectId?: string): Promise { + let tasks: AuditTask[]; + + if (projectId) { + tasks = await this.getByIndex(STORES.AUDIT_TASKS, 'project_id', projectId); + } else { + tasks = await this.getAll(STORES.AUDIT_TASKS); + } + + const tasksWithRelations = await Promise.all( + tasks.map(async (task) => { + const project = task.project_id ? await this.getProjectById(task.project_id) : null; + const creator = task.created_by ? await this.getProfileById(task.created_by) : null; + return { + ...task, + project: project || undefined, + creator: creator || undefined + }; + }) + ); + + return tasksWithRelations.sort((a, b) => + new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + ); + } + + async getAuditTaskById(id: string): Promise { + if (!id) return null; + + const task = await this.getById(STORES.AUDIT_TASKS, id); + if (!task) return null; + + const project = task.project_id ? await this.getProjectById(task.project_id) : null; + const creator = task.created_by ? await this.getProfileById(task.created_by) : null; + + return { + ...task, + project: project || undefined, + creator: creator || undefined + }; + } + + async createAuditTask(taskData: CreateAuditTaskForm & { created_by: string }): Promise { + const newTask: AuditTask = { + id: this.generateId(), + project_id: taskData.project_id, + task_type: taskData.task_type, + status: 'pending', + branch_name: taskData.branch_name || undefined, + exclude_patterns: JSON.stringify(taskData.exclude_patterns || []), + scan_config: JSON.stringify(taskData.scan_config || {}), + total_files: 0, + scanned_files: 0, + total_lines: 0, + issues_count: 0, + quality_score: 0, + started_at: undefined, + completed_at: undefined, + created_by: taskData.created_by, + created_at: new Date().toISOString(), + }; + + await this.put(STORES.AUDIT_TASKS, newTask); + return this.getAuditTaskById(newTask.id) as Promise; + } + + async updateAuditTask(id: string, updates: Partial): Promise { + const existing = await this.getById(STORES.AUDIT_TASKS, id); + if (!existing) throw new Error('Audit task not found'); + + const updated: AuditTask = { + ...existing, + ...updates, + id, + }; + + await this.put(STORES.AUDIT_TASKS, updated); + return this.getAuditTaskById(id) as Promise; + } + + // ==================== AuditIssue 相关方法 ==================== + + async getAuditIssues(taskId: string): Promise { + const issues = await this.getByIndex(STORES.AUDIT_ISSUES, 'task_id', taskId); + + const issuesWithRelations = await Promise.all( + issues.map(async (issue) => { + const task = issue.task_id ? await this.getAuditTaskById(issue.task_id) : null; + const resolver = issue.resolved_by ? await this.getProfileById(issue.resolved_by) : null; + return { + ...issue, + task: task || undefined, + resolver: resolver || undefined + }; + }) + ); + + // 按严重程度和创建时间排序 + const severityOrder: Record = { critical: 0, high: 1, medium: 2, low: 3 }; + return issuesWithRelations.sort((a, b) => { + const severityDiff = (severityOrder[a.severity] || 999) - (severityOrder[b.severity] || 999); + if (severityDiff !== 0) return severityDiff; + return new Date(b.created_at).getTime() - new Date(a.created_at).getTime(); + }); + } + + async createAuditIssue(issueData: Omit): Promise { + const newIssue: AuditIssue = { + ...issueData, + id: this.generateId(), + created_at: new Date().toISOString(), + }; + + await this.put(STORES.AUDIT_ISSUES, newIssue); + + const task = newIssue.task_id ? await this.getAuditTaskById(newIssue.task_id) : null; + const resolver = newIssue.resolved_by ? await this.getProfileById(newIssue.resolved_by) : null; + + return { + ...newIssue, + task: task || undefined, + resolver: resolver || undefined + }; + } + + async updateAuditIssue(id: string, updates: Partial): Promise { + const existing = await this.getById(STORES.AUDIT_ISSUES, id); + if (!existing) throw new Error('Audit issue not found'); + + const updated: AuditIssue = { + ...existing, + ...updates, + id, + }; + + await this.put(STORES.AUDIT_ISSUES, updated); + + const task = updated.task_id ? await this.getAuditTaskById(updated.task_id) : null; + const resolver = updated.resolved_by ? await this.getProfileById(updated.resolved_by) : null; + + return { + ...updated, + task: task || undefined, + resolver: resolver || undefined + }; + } + + // ==================== InstantAnalysis 相关方法 ==================== + + async getInstantAnalyses(userId?: string): Promise { + let analyses: InstantAnalysis[]; + + if (userId) { + analyses = await this.getByIndex(STORES.INSTANT_ANALYSES, 'user_id', userId); + } else { + analyses = await this.getAll(STORES.INSTANT_ANALYSES); + } + + const analysesWithUser = await Promise.all( + analyses.map(async (analysis) => { + const user = analysis.user_id ? await this.getProfileById(analysis.user_id) : null; + return { ...analysis, user: user || undefined }; + }) + ); + + return analysesWithUser.sort((a, b) => + new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + ); + } + + async createInstantAnalysis(analysisData: InstantAnalysisForm & { + user_id: string; + analysis_result?: string; + issues_count?: number; + quality_score?: number; + analysis_time?: number; + }): Promise { + const newAnalysis: InstantAnalysis = { + id: this.generateId(), + user_id: analysisData.user_id, + language: analysisData.language, + code_content: '', // 不持久化代码内容 + analysis_result: analysisData.analysis_result || '{}', + issues_count: analysisData.issues_count || 0, + quality_score: analysisData.quality_score || 0, + analysis_time: analysisData.analysis_time || 0, + created_at: new Date().toISOString(), + }; + + await this.put(STORES.INSTANT_ANALYSES, newAnalysis); + + const user = newAnalysis.user_id ? await this.getProfileById(newAnalysis.user_id) : null; + return { ...newAnalysis, user: user || undefined }; + } + + // ==================== 统计相关方法 ==================== + + async getProjectStats(): Promise { + const projects = await this.getAll(STORES.PROJECTS); + const tasks = await this.getAll(STORES.AUDIT_TASKS); + const issues = await this.getAll(STORES.AUDIT_ISSUES); + + return { + total_projects: projects.length, + active_projects: projects.filter(p => p.is_active).length, + total_tasks: tasks.length, + completed_tasks: tasks.filter(t => t.status === 'completed').length, + total_issues: issues.length, + resolved_issues: issues.filter(i => i.status === 'resolved').length, + }; + } +} + +// 导出单例 +export const localDB = new LocalDatabase(); diff --git a/src/shared/utils/initLocalDB.ts b/src/shared/utils/initLocalDB.ts new file mode 100644 index 0000000..a9fdb4e --- /dev/null +++ b/src/shared/utils/initLocalDB.ts @@ -0,0 +1,118 @@ +/** + * 本地数据库初始化工具 + * 用于在首次使用时创建默认用户和演示数据 + */ + +import { localDB } from '../config/localDatabase'; +import { api } from '../config/database'; + +/** + * 初始化本地数据库 + * 创建默认用户和基础数据 + */ +export async function initLocalDatabase(): Promise { + try { + // 初始化数据库 + await localDB.init(); + + // 检查是否已有用户 + const profileCount = await localDB.getProfilesCount(); + + if (profileCount === 0) { + // 创建默认本地用户 + await api.createProfiles({ + id: 'local-user', + email: 'local@xcodereviewer.com', + full_name: '本地用户', + role: 'admin', + github_username: 'local-user', + }); + + console.log('✅ 本地数据库初始化成功'); + } + } catch (error) { + console.error('❌ 本地数据库初始化失败:', error); + throw error; + } +} + +/** + * 清空本地数据库 + * 用于重置或清理数据 + */ +export async function clearLocalDatabase(): Promise { + try { + const dbName = 'xcodereviewer_local'; + const request = indexedDB.deleteDatabase(dbName); + + return new Promise((resolve, reject) => { + request.onsuccess = () => { + console.log('✅ 本地数据库已清空'); + resolve(); + }; + request.onerror = () => { + console.error('❌ 清空本地数据库失败'); + reject(request.error); + }; + }); + } catch (error) { + console.error('❌ 清空本地数据库失败:', error); + throw error; + } +} + +/** + * 导出本地数据库数据 + * 用于备份或迁移 + */ +export async function exportLocalDatabase(): Promise { + try { + await localDB.init(); + + const data = { + version: 1, + exportDate: new Date().toISOString(), + profiles: await localDB.getAllProfiles(), + projects: await localDB.getProjects(), + auditTasks: await localDB.getAuditTasks(), + }; + + return JSON.stringify(data, null, 2); + } catch (error) { + console.error('❌ 导出数据失败:', error); + throw error; + } +} + +/** + * 导入数据到本地数据库 + * 用于恢复备份或迁移数据 + */ +export async function importLocalDatabase(jsonData: string): Promise { + try { + const data = JSON.parse(jsonData); + + if (!data.version || !data.profiles) { + throw new Error('无效的数据格式'); + } + + await localDB.init(); + + // 导入用户 + for (const profile of data.profiles) { + await api.createProfiles(profile); + } + + // 导入项目 + if (data.projects) { + for (const project of data.projects) { + await api.createProject(project); + } + } + + console.log('✅ 数据导入成功'); + } catch (error) { + console.error('❌ 导入数据失败:', error); + throw error; + } +}