refactor(project-structure): Restructure project architecture and improve code organization
- Reorganize source code into feature-based and shared module structure - Move components, services, and hooks into more logical directories - Update project configuration files to reflect new structure - Add .env.example template for easier environment setup - Enhance README.md with more detailed project information and setup instructions - Consolidate utility functions and hooks into shared modules - Remove deprecated or unused files and components - Improve type definitions and configuration management - Update routing and main application configuration This refactoring aims to improve code maintainability, readability, and scalability by implementing a more modular and organized project structure.
This commit is contained in:
parent
067961df94
commit
a12633b47d
|
|
@ -0,0 +1,19 @@
|
|||
# Google Gemini AI 配置 (必需)
|
||||
VITE_GEMINI_API_KEY=your_gemini_api_key_here
|
||||
VITE_GEMINI_MODEL=gemini-2.5-flash
|
||||
VITE_GEMINI_TIMEOUT_MS=25000
|
||||
|
||||
# Supabase 配置 (可选,用于数据持久化)
|
||||
VITE_SUPABASE_URL=https://your-project.supabase.co
|
||||
VITE_SUPABASE_ANON_KEY=your-anon-key-here
|
||||
|
||||
# GitHub 集成 (可选,用于仓库分析)
|
||||
VITE_GITHUB_TOKEN=your_github_token_here
|
||||
|
||||
# 应用配置
|
||||
VITE_APP_ID=xcodereviewer
|
||||
|
||||
# 分析配置
|
||||
VITE_MAX_ANALYZE_FILES=40
|
||||
VITE_LLM_CONCURRENCY=2
|
||||
VITE_LLM_GAP_MS=500
|
||||
70
README.md
70
README.md
|
|
@ -1,7 +1,7 @@
|
|||
# XCodeReviewer - 您的智能代码审计伙伴 🚀
|
||||
|
||||
<div style="width: 100%; max-width: 600px; margin: 0 auto;">
|
||||
<img src="public/images/logo.png" alt="VerifyVision-Pro Logo" style="width: 100%; height: auto; display: block; margin: 0 auto;">
|
||||
<img src="public/images/logo.png" alt="XCodeReviewer Logo" style="width: 100%; height: auto; display: block; margin: 0 auto;">
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
|
@ -25,25 +25,25 @@
|
|||
|
||||
在快节奏的软件开发中,保证代码质量至关重要。传统代码审计工具规则死板、效率低下,而人工审计则耗时耗力。XCodeReviewer 借助 Google Gemini AI 的强大能力,彻底改变了代码审查的方式:
|
||||
|
||||
- **🤖 AI 驱动的深度分析**:超越传统静态分析,理解代码意图,发现深层逻辑问题。
|
||||
- **🎯 多维度、全方位评估**:从**安全性**、**性能**、**可维护性**到**代码风格**,提供 360 度无死角的质量评估。
|
||||
- **💡 清晰、可行的修复建议**:独创 **What-Why-How** 模式,不仅告诉您“是什么”问题,还解释“为什么”,并提供“如何修复”的具体代码示例。
|
||||
- **⚡ 实时反馈,即时提升**:无论是代码片段还是整个代码仓库,都能获得快速、准确的分析结果。
|
||||
- **✨ 现代化、高颜值的用户界面**:基于 React + TypeScript 构建,提供流畅、直观的操作体验。
|
||||
- **AI 驱动的深度分析**:超越传统静态分析,理解代码意图,发现深层逻辑问题。
|
||||
- **多维度、全方位评估**:从**安全性**、**性能**、**可维护性**到**代码风格**,提供 360 度无死角的质量评估。
|
||||
- **清晰、可行的修复建议**:独创 **What-Why-How** 模式,不仅告诉您“是什么”问题,还解释“为什么”,并提供“如何修复”的具体代码示例。
|
||||
- **实时反馈,即时提升**:无论是代码片段还是整个代码仓库,都能获得快速、准确的分析结果。
|
||||
- **现代化、高颜值的用户界面**:基于 React + TypeScript 构建,提供流畅、直观的操作体验。
|
||||
|
||||
## 🎬 项目演示
|
||||
|
||||
### 主要功能界面
|
||||
|
||||
#### 📊 智能仪表盘
|
||||
#### 智能仪表盘
|
||||

|
||||
*实时展示项目统计、质量趋势和系统性能,提供全面的代码审计概览*
|
||||
|
||||
#### ⚡ 即时分析
|
||||
#### 即时分析
|
||||

|
||||
*支持代码片段快速分析,提供详细的 What-Why-How 解释和修复建议*
|
||||
|
||||
#### 🚀 项目管理
|
||||
#### 项目管理
|
||||

|
||||
*集成 GitHub/GitLab 仓库,支持多语言项目审计和批量代码分析*
|
||||
|
||||
|
|
@ -78,11 +78,11 @@
|
|||
|
||||
3. **配置环境变量**
|
||||
```bash
|
||||
# 创建环境变量文件
|
||||
touch .env
|
||||
# 复制环境变量模板
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
在 `.env` 文件中添加以下配置:
|
||||
编辑 `.env` 文件,配置必要的环境变量:
|
||||
```env
|
||||
# Google Gemini AI 配置 (必需)
|
||||
VITE_GEMINI_API_KEY=your_gemini_api_key_here
|
||||
|
|
@ -93,8 +93,16 @@
|
|||
VITE_SUPABASE_URL=https://your-project.supabase.co
|
||||
VITE_SUPABASE_ANON_KEY=your-anon-key-here
|
||||
|
||||
# GitHub 集成 (可选,用于仓库分析)
|
||||
VITE_GITHUB_TOKEN=your_github_token_here
|
||||
|
||||
# 应用配置
|
||||
VITE_APP_ID=xcodereviewer
|
||||
|
||||
# 分析配置
|
||||
VITE_MAX_ANALYZE_FILES=40
|
||||
VITE_LLM_CONCURRENCY=2
|
||||
VITE_LLM_GAP_MS=500
|
||||
```
|
||||
|
||||
4. **启动开发服务器**
|
||||
|
|
@ -112,7 +120,7 @@
|
|||
2. 创建新的 API Key
|
||||
3. 将 API Key 添加到 `.env` 文件中的 `VITE_GEMINI_API_KEY`
|
||||
|
||||
#### Supabase 配置
|
||||
#### Supabase 配置(可选)
|
||||
1. 访问 [Supabase](https://supabase.com/) 创建新项目
|
||||
2. 在项目设置中获取 URL 和匿名密钥
|
||||
3. 运行数据库迁移脚本:
|
||||
|
|
@ -120,6 +128,7 @@
|
|||
# 在 Supabase SQL 编辑器中执行
|
||||
cat supabase/migrations/full_schema.sql
|
||||
```
|
||||
4. 如果不配置 Supabase,系统将以演示模式运行,数据不会持久化
|
||||
|
||||
## ✨ 核心功能
|
||||
|
||||
|
|
@ -188,8 +197,12 @@
|
|||
```
|
||||
XCodeReviewer/
|
||||
├── src/
|
||||
│ ├── app/ # 应用配置
|
||||
│ │ ├── App.tsx # 主应用组件
|
||||
│ │ ├── main.tsx # 应用入口点
|
||||
│ │ └── routes.tsx # 路由配置
|
||||
│ ├── components/ # React 组件
|
||||
│ │ ├── common/ # 通用组件 (Header, Footer, PageMeta)
|
||||
│ │ ├── layout/ # 布局组件 (Header, Footer, PageMeta)
|
||||
│ │ ├── ui/ # UI 组件库 (基于 Radix UI)
|
||||
│ │ └── debug/ # 调试组件
|
||||
│ ├── pages/ # 页面组件
|
||||
|
|
@ -198,20 +211,25 @@ XCodeReviewer/
|
|||
│ │ ├── InstantAnalysis.tsx # 即时分析
|
||||
│ │ ├── AuditTasks.tsx # 审计任务
|
||||
│ │ └── AdminDashboard.tsx # 系统管理
|
||||
│ ├── services/ # 服务层
|
||||
│ │ ├── codeAnalysis.ts # AI 代码分析引擎
|
||||
│ │ ├── repoScan.ts # 仓库扫描服务
|
||||
│ │ └── repoZipScan.ts # ZIP 文件扫描
|
||||
│ ├── db/ # 数据库配置
|
||||
│ │ └── supabase.ts # Supabase 客户端和 API
|
||||
│ ├── types/ # TypeScript 类型定义
|
||||
│ ├── hooks/ # 自定义 React Hooks
|
||||
│ ├── lib/ # 工具函数
|
||||
│ └── routes.tsx # 路由配置
|
||||
│ ├── features/ # 功能模块
|
||||
│ │ ├── analysis/ # 分析相关服务
|
||||
│ │ │ └── services/ # AI 代码分析引擎
|
||||
│ │ └── projects/ # 项目相关服务
|
||||
│ │ └── services/ # 仓库扫描、ZIP 文件扫描
|
||||
│ ├── shared/ # 共享工具
|
||||
│ │ ├── config/ # 配置文件 (数据库、环境变量)
|
||||
│ │ ├── types/ # TypeScript 类型定义
|
||||
│ │ ├── hooks/ # 自定义 React Hooks
|
||||
│ │ ├── utils/ # 工具函数
|
||||
│ │ └── constants/ # 常量定义
|
||||
│ └── assets/ # 静态资源
|
||||
│ └── styles/ # 样式文件
|
||||
├── supabase/
|
||||
│ └── migrations/ # 数据库迁移文件
|
||||
├── public/ # 静态资源
|
||||
└── docs/ # 文档
|
||||
├── public/
|
||||
│ └── images/ # 图片资源
|
||||
├── scripts/ # 构建和设置脚本
|
||||
└── rules/ # 代码规则配置
|
||||
```
|
||||
|
||||
## 🎯 使用指南
|
||||
|
|
|
|||
277
README_EN.md
277
README_EN.md
|
|
@ -1,4 +1,8 @@
|
|||
# XCodeReviewer - Your Intelligent Code Review Partner 🚀
|
||||
# XCodeReviewer - Your Intelligent Code Audit Partner 🚀
|
||||
|
||||
<div style="width: 100%; max-width: 600px; margin: 0 auto;">
|
||||
<img src="public/images/logo.png" alt="XCodeReviewer Logo" style="width: 100%; height: auto; display: block; margin: 0 auto;">
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<p>
|
||||
|
|
@ -14,19 +18,18 @@
|
|||
[](https://vitejs.dev/)
|
||||
[](https://supabase.com/)
|
||||
[](https://ai.google.dev/)
|
||||
[](https://star-history.com/#lintsinghua/XCodeReviewer&Date)
|
||||
|
||||
**XCodeReviewer** is a modern code auditing platform powered by Large Language Models (LLM), designed to provide developers with intelligent, comprehensive, and in-depth code quality analysis and review services.
|
||||
**XCodeReviewer** is a modern code audit platform powered by Large Language Models (LLM), designed to provide developers with intelligent, comprehensive, and in-depth code quality analysis and review services.
|
||||
|
||||
## 🌟 Why Choose XCodeReviewer?
|
||||
|
||||
In the fast-paced world of software development, ensuring code quality is crucial. Traditional code auditing tools are rigid and inefficient, while manual auditing is time-consuming and labor-intensive. XCodeReviewer leverages the powerful capabilities of Google Gemini AI to revolutionize the way code review is conducted:
|
||||
In the fast-paced world of software development, ensuring code quality is crucial. Traditional code audit tools are rigid and inefficient, while manual audits are time-consuming and labor-intensive. XCodeReviewer leverages the powerful capabilities of Google Gemini AI to revolutionize the way code reviews are conducted:
|
||||
|
||||
- **🤖 AI-Driven Deep Analysis**: Beyond traditional static analysis, understanding code intent and discovering deep logical issues.
|
||||
- **🤖 AI-Driven Deep Analysis**: Beyond traditional static analysis, understands code intent and discovers deep logical issues.
|
||||
- **🎯 Multi-dimensional, Comprehensive Assessment**: From **security**, **performance**, **maintainability** to **code style**, providing 360-degree quality evaluation.
|
||||
- **💡 Clear, Actionable Fix Suggestions**: Innovative **What-Why-How** pattern that not only tells you "what" the problem is, but also explains "why" and provides "how to fix" with specific code examples.
|
||||
- **⚡ Real-time Feedback, Instant Improvement**: Whether it's code snippets or entire code repositories, you can get fast and accurate analysis results.
|
||||
- **✨ Modern, High-Quality User Interface**: Built with React + TypeScript, providing smooth and intuitive user experience.
|
||||
- **💡 Clear, Actionable Fix Suggestions**: Innovative **What-Why-How** approach that not only tells you "what" the problem is, but also explains "why" and provides "how to fix" with specific code examples.
|
||||
- **⚡ Real-time Feedback, Instant Improvement**: Whether it's code snippets or entire repositories, get fast and accurate analysis results.
|
||||
- **✨ Modern, Beautiful User Interface**: Built with React + TypeScript, providing a smooth and intuitive user experience.
|
||||
|
||||
## 🎬 Project Demo
|
||||
|
||||
|
|
@ -34,77 +37,15 @@ In the fast-paced world of software development, ensuring code quality is crucia
|
|||
|
||||
#### 📊 Intelligent Dashboard
|
||||

|
||||
*Real-time display of project statistics, quality trends and system performance, providing comprehensive code audit overview*
|
||||
*Real-time display of project statistics, quality trends, and system performance, providing comprehensive code audit overview*
|
||||
|
||||
#### ⚡ Instant Analysis
|
||||

|
||||
*Support for rapid code snippet analysis, providing detailed What-Why-How explanations and fix suggestions*
|
||||
*Support for quick code snippet analysis with detailed What-Why-How explanations and fix suggestions*
|
||||
|
||||
#### 🚀 Project Management
|
||||

|
||||
*Integration with GitHub/GitLab repositories, supporting multi-language project auditing and batch code analysis*
|
||||
|
||||
## ✨ Core Features
|
||||
|
||||
<details>
|
||||
<summary><b>🚀 Project Management</b></summary>
|
||||
|
||||
- **One-click Repository Integration**: Seamless integration with mainstream platforms like GitHub, GitLab.
|
||||
- **Multi-language "Full Suite" Support**: Covering popular languages like JavaScript, TypeScript, Python, Java, Go, Rust.
|
||||
- **Flexible Branch Auditing**: Support for precise analysis of specified code branches.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>⚡ Instant Analysis</b></summary>
|
||||
|
||||
- **Code Snippet "Paste & Go"**: Directly paste code in the web interface and get instant analysis results.
|
||||
- **10+ Language Instant Support**: Meeting your diverse code analysis needs.
|
||||
- **Millisecond Response**: Quickly get code quality scores and optimization suggestions.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🧠 Intelligent Auditing</b></summary>
|
||||
|
||||
- **AI Deep Code Understanding**: Based on Google Gemini, providing intelligent analysis beyond keyword matching.
|
||||
- **Five Core Dimension Detection**:
|
||||
- 🐛 **Potential Bugs**: Accurately capture logic errors, boundary conditions, and null pointer issues.
|
||||
- 🔒 **Security Vulnerabilities**: Identify security risks like SQL injection, XSS, sensitive information leakage.
|
||||
- ⚡ **Performance Bottlenecks**: Discover inefficient algorithms, memory leaks, and unreasonable async operations.
|
||||
- 🎨 **Code Style**: Ensure code follows industry best practices and unified standards.
|
||||
- 🔧 **Maintainability**: Evaluate code readability, complexity, and modularity.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>💡 Explainable Analysis (What-Why-How)</b></summary>
|
||||
|
||||
- **What (What is it)**: Clearly point out problems in the code.
|
||||
- **Why (Why)**: Detailed explanation of potential risks and impacts this problem may bring.
|
||||
- **How (How to fix)**: Provide specific, directly usable code fix examples.
|
||||
- **Precise Code Location**: Quickly jump to the line and column where the problem is located.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>📊 Visual Reports</b></summary>
|
||||
|
||||
- **Code Quality Dashboard**: Provide 0-100 comprehensive quality assessment, making code health status clear at a glance.
|
||||
- **Multi-dimensional Issue Statistics**: Classify and count issues by type and severity.
|
||||
- **Quality Trend Analysis**: Display code quality changes over time through charts.
|
||||
</details>
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
| Category | Technology | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **Frontend Framework** | `React 18` `TypeScript` `Vite` | Modern frontend development stack with hot reload and type safety |
|
||||
| **UI Components** | `Tailwind CSS` `Radix UI` `Lucide React` | Responsive design, accessibility, rich icon library |
|
||||
| **Data Visualization** | `Recharts` | Professional chart library supporting multiple chart types |
|
||||
| **Routing** | `React Router v6` | Single-page application routing solution |
|
||||
| **State Management** | `React Hooks` `Sonner` | Lightweight state management and notification system |
|
||||
| **AI Engine** | `Google Gemini 2.5 Flash` | Powerful large language model for code analysis |
|
||||
| **Backend Service** | `Supabase` `PostgreSQL` | Full-stack backend-as-a-service with real-time database |
|
||||
| **HTTP Client** | `Axios` `Ky` | Modern HTTP request libraries |
|
||||
| **Code Quality** | `Biome` `Ast-grep` `TypeScript` | Code formatting, static analysis, and type checking |
|
||||
| **Build Tools** | `Vite` `PostCSS` `Autoprefixer` | Fast build tools and CSS processing |
|
||||
*Integrated GitHub/GitLab repositories, supporting multi-language project audits and batch code analysis*
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
|
|
@ -137,11 +78,11 @@ In the fast-paced world of software development, ensuring code quality is crucia
|
|||
|
||||
3. **Configure environment variables**
|
||||
```bash
|
||||
# Create environment variables file
|
||||
touch .env
|
||||
# Copy environment template
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Add the following configuration to the `.env` file:
|
||||
Edit the `.env` file and configure the necessary environment variables:
|
||||
```env
|
||||
# Google Gemini AI Configuration (Required)
|
||||
VITE_GEMINI_API_KEY=your_gemini_api_key_here
|
||||
|
|
@ -152,8 +93,16 @@ In the fast-paced world of software development, ensuring code quality is crucia
|
|||
VITE_SUPABASE_URL=https://your-project.supabase.co
|
||||
VITE_SUPABASE_ANON_KEY=your-anon-key-here
|
||||
|
||||
# App Configuration
|
||||
# GitHub Integration (Optional, for repository analysis)
|
||||
VITE_GITHUB_TOKEN=your_github_token_here
|
||||
|
||||
# Application Configuration
|
||||
VITE_APP_ID=xcodereviewer
|
||||
|
||||
# Analysis Configuration
|
||||
VITE_MAX_ANALYZE_FILES=40
|
||||
VITE_LLM_CONCURRENCY=2
|
||||
VITE_LLM_GAP_MS=500
|
||||
```
|
||||
|
||||
4. **Start development server**
|
||||
|
|
@ -169,49 +118,121 @@ In the fast-paced world of software development, ensuring code quality is crucia
|
|||
#### Google Gemini API Key
|
||||
1. Visit [Google AI Studio](https://makersuite.google.com/app/apikey)
|
||||
2. Create a new API Key
|
||||
3. Add the API Key to `VITE_GEMINI_API_KEY` in the `.env` file
|
||||
3. Add the API Key to `VITE_GEMINI_API_KEY` in your `.env` file
|
||||
|
||||
#### Supabase Configuration
|
||||
#### Supabase Configuration (Optional)
|
||||
1. Visit [Supabase](https://supabase.com/) to create a new project
|
||||
2. Get the URL and anonymous key from project settings
|
||||
3. Run the database migration script:
|
||||
3. Run database migration scripts:
|
||||
```bash
|
||||
# Execute in Supabase SQL editor
|
||||
# Execute in Supabase SQL Editor
|
||||
cat supabase/migrations/full_schema.sql
|
||||
```
|
||||
4. If Supabase is not configured, the system will run in demo mode without data persistence
|
||||
|
||||
## ✨ Core Features
|
||||
|
||||
<details>
|
||||
<summary><b>🚀 Project Management</b></summary>
|
||||
|
||||
- **One-click Repository Integration**: Seamlessly connect with GitHub, GitLab, and other mainstream platforms.
|
||||
- **Multi-language "Full Stack" Support**: Covers popular languages like JavaScript, TypeScript, Python, Java, Go, Rust, and more.
|
||||
- **Flexible Branch Auditing**: Support for precise analysis of specified code branches.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>⚡ Instant Analysis</b></summary>
|
||||
|
||||
- **Code Snippet "Quick Paste"**: Directly paste code in the web interface for immediate analysis results.
|
||||
- **10+ Language Instant Support**: Meet your diverse code analysis needs.
|
||||
- **Millisecond Response**: Quickly get code quality scores and optimization suggestions.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🧠 Intelligent Auditing</b></summary>
|
||||
|
||||
- **AI Deep Code Understanding**: Based on Google Gemini, providing intelligent analysis beyond keyword matching.
|
||||
- **Five Core Detection Dimensions**:
|
||||
- 🐛 **Potential Bugs**: Precisely capture logical errors, boundary conditions, and null pointer issues.
|
||||
- 🔒 **Security Vulnerabilities**: Identify SQL injection, XSS, sensitive information leakage, and other security risks.
|
||||
- ⚡ **Performance Bottlenecks**: Discover inefficient algorithms, memory leaks, and unreasonable asynchronous operations.
|
||||
- 🎨 **Code Style**: Ensure code follows industry best practices and unified standards.
|
||||
- 🔧 **Maintainability**: Evaluate code readability, complexity, and modularity.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>💡 Explainable Analysis (What-Why-How)</b></summary>
|
||||
|
||||
- **What**: Clearly identify problems in the code.
|
||||
- **Why**: Detailed explanation of potential risks and impacts the problem may cause.
|
||||
- **How**: Provide specific, directly usable code fix examples.
|
||||
- **Precise Code Location**: Quickly jump to the problematic line and column.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>📊 Visual Reports</b></summary>
|
||||
|
||||
- **Code Quality Dashboard**: Provides comprehensive quality assessment from 0-100, making code health status clear at a glance.
|
||||
- **Multi-dimensional Issue Statistics**: Classify and count issues by type and severity.
|
||||
- **Quality Trend Analysis**: Display code quality changes over time through charts.
|
||||
</details>
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
| Category | Technology | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **Frontend Framework** | `React 18` `TypeScript` `Vite` | Modern frontend development stack with hot reload and type safety |
|
||||
| **UI Components** | `Tailwind CSS` `Radix UI` `Lucide React` | Responsive design, accessibility, rich icon library |
|
||||
| **Data Visualization** | `Recharts` | Professional chart library supporting multiple chart types |
|
||||
| **Routing** | `React Router v6` | Single-page application routing solution |
|
||||
| **State Management** | `React Hooks` `Sonner` | Lightweight state management and notification system |
|
||||
| **AI Engine** | `Google Gemini 2.5 Flash` | Powerful large language model supporting code analysis |
|
||||
| **Backend Service** | `Supabase` `PostgreSQL` | Full-stack backend-as-a-service with real-time database |
|
||||
| **HTTP Client** | `Axios` `Ky` | Modern HTTP request libraries |
|
||||
| **Code Quality** | `Biome` `Ast-grep` `TypeScript` | Code formatting, static analysis, and type checking |
|
||||
| **Build Tools** | `Vite` `PostCSS` `Autoprefixer` | Fast build tools and CSS processing |
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
XCodeReviewer/
|
||||
├── src/
|
||||
│ ├── components/ # React Components
|
||||
│ │ ├── common/ # Common Components (Header, Footer, PageMeta)
|
||||
│ │ ├── ui/ # UI Component Library (Based on Radix UI)
|
||||
│ │ └── debug/ # Debug Components
|
||||
│ ├── pages/ # Page Components
|
||||
│ ├── app/ # Application configuration
|
||||
│ │ ├── App.tsx # Main application component
|
||||
│ │ ├── main.tsx # Application entry point
|
||||
│ │ └── routes.tsx # Route configuration
|
||||
│ ├── components/ # React components
|
||||
│ │ ├── layout/ # Layout components (Header, Footer, PageMeta)
|
||||
│ │ ├── ui/ # UI component library (based on Radix UI)
|
||||
│ │ └── debug/ # Debug components
|
||||
│ ├── pages/ # Page components
|
||||
│ │ ├── Dashboard.tsx # Dashboard
|
||||
│ │ ├── Projects.tsx # Project Management
|
||||
│ │ ├── InstantAnalysis.tsx # Instant Analysis
|
||||
│ │ ├── AuditTasks.tsx # Audit Tasks
|
||||
│ │ └── AdminDashboard.tsx # System Management
|
||||
│ ├── services/ # Service Layer
|
||||
│ │ ├── codeAnalysis.ts # AI Code Analysis Engine
|
||||
│ │ ├── repoScan.ts # Repository Scanning Service
|
||||
│ │ └── repoZipScan.ts # ZIP File Scanning
|
||||
│ ├── db/ # Database Configuration
|
||||
│ │ └── supabase.ts # Supabase Client and API
|
||||
│ ├── types/ # TypeScript Type Definitions
|
||||
│ ├── hooks/ # Custom React Hooks
|
||||
│ ├── lib/ # Utility Functions
|
||||
│ └── routes.tsx # Route Configuration
|
||||
│ │ ├── Projects.tsx # Project management
|
||||
│ │ ├── InstantAnalysis.tsx # Instant analysis
|
||||
│ │ ├── AuditTasks.tsx # Audit tasks
|
||||
│ │ └── AdminDashboard.tsx # System management
|
||||
│ ├── features/ # Feature modules
|
||||
│ │ ├── analysis/ # Analysis related services
|
||||
│ │ │ └── services/ # AI code analysis engine
|
||||
│ │ └── projects/ # Project related services
|
||||
│ │ └── services/ # Repository scanning, ZIP file scanning
|
||||
│ ├── shared/ # Shared utilities
|
||||
│ │ ├── config/ # Configuration files (database, environment)
|
||||
│ │ ├── types/ # TypeScript type definitions
|
||||
│ │ ├── hooks/ # Custom React Hooks
|
||||
│ │ ├── utils/ # Utility functions
|
||||
│ │ └── constants/ # Constants definition
|
||||
│ └── assets/ # Static assets
|
||||
│ └── styles/ # Style files
|
||||
├── supabase/
|
||||
│ └── migrations/ # Database Migration Files
|
||||
├── public/ # Static Assets
|
||||
└── docs/ # Documentation
|
||||
│ └── migrations/ # Database migration files
|
||||
├── public/
|
||||
│ └── images/ # Image resources
|
||||
├── scripts/ # Build and setup scripts
|
||||
└── rules/ # Code rules configuration
|
||||
```
|
||||
|
||||
## 🎯 User Guide
|
||||
## 🎯 Usage Guide
|
||||
|
||||
### Instant Code Analysis
|
||||
1. Visit the `/instant-analysis` page
|
||||
|
|
@ -224,12 +245,12 @@ XCodeReviewer/
|
|||
1. Visit the `/projects` page
|
||||
2. Click "New Project" to create a project
|
||||
3. Configure repository URL and scan parameters
|
||||
4. Start code audit tasks
|
||||
4. Start code audit task
|
||||
5. View audit results and issue statistics
|
||||
|
||||
### Audit Tasks
|
||||
1. Create audit tasks in project details page
|
||||
2. Select scan branch and exclude patterns
|
||||
1. Create audit tasks in project detail page
|
||||
2. Select scan branch and exclusion patterns
|
||||
3. Configure analysis depth and scope
|
||||
4. Monitor task execution status
|
||||
5. View detailed issue reports
|
||||
|
|
@ -245,23 +266,27 @@ pnpm build
|
|||
# Preview build results
|
||||
pnpm preview
|
||||
|
||||
# Code checking
|
||||
# Code linting
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `VITE_GEMINI_API_KEY` | ✅ | Google Gemini API Key |
|
||||
| `VITE_GEMINI_MODEL` | ❌ | AI Model Name (default: gemini-2.5-flash) |
|
||||
| `VITE_GEMINI_TIMEOUT_MS` | ❌ | Request Timeout (default: 25000ms) |
|
||||
| `VITE_SUPABASE_URL` | ❌ | Supabase Project URL |
|
||||
| `VITE_SUPABASE_ANON_KEY` | ❌ | Supabase Anonymous Key |
|
||||
| `VITE_APP_ID` | ❌ | App Identifier (default: xcodereviewer) |
|
||||
| `VITE_GEMINI_API_KEY` | ✅ | Google Gemini API key |
|
||||
| `VITE_GEMINI_MODEL` | ❌ | AI model name (default: gemini-2.5-flash) |
|
||||
| `VITE_GEMINI_TIMEOUT_MS` | ❌ | Request timeout (default: 25000ms) |
|
||||
| `VITE_SUPABASE_URL` | ❌ | Supabase project URL |
|
||||
| `VITE_SUPABASE_ANON_KEY` | ❌ | Supabase anonymous key |
|
||||
| `VITE_APP_ID` | ❌ | Application identifier (default: xcodereviewer) |
|
||||
| `VITE_MAX_ANALYZE_FILES` | ❌ | Maximum files to analyze (default: 40) |
|
||||
| `VITE_LLM_CONCURRENCY` | ❌ | LLM concurrency limit (default: 2) |
|
||||
| `VITE_LLM_GAP_MS` | ❌ | Gap between LLM requests (default: 500ms) |
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We warmly welcome all forms of contributions! Whether it's submitting issues, creating PRs, or improving documentation, every contribution is crucial to us. Please contact us for detailed information.
|
||||
We warmly welcome all forms of contributions! Whether it's submitting issues, creating PRs, or improving documentation, every contribution is important to us. Please contact us for detailed information.
|
||||
|
||||
### Development Workflow
|
||||
|
||||
|
|
@ -271,19 +296,6 @@ We warmly welcome all forms of contributions! Whether it's submitting issues, cr
|
|||
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||
5. Create a **Pull Request**
|
||||
|
||||
#### 4. Build Failure
|
||||
**Problem**: `pnpm build` command fails
|
||||
**Solution**:
|
||||
```bash
|
||||
# Clear cache
|
||||
pnpm clean
|
||||
rm -rf node_modules
|
||||
pnpm install
|
||||
|
||||
# Check TypeScript type errors
|
||||
pnpm type-check
|
||||
```
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- **[Google Gemini AI](https://ai.google.dev/)**: Providing powerful AI analysis capabilities
|
||||
|
|
@ -291,14 +303,15 @@ pnpm type-check
|
|||
- **[Radix UI](https://www.radix-ui.com/)**: Providing accessible UI components
|
||||
- **[Tailwind CSS](https://tailwindcss.com/)**: Providing modern CSS framework
|
||||
- **[Recharts](https://recharts.org/)**: Providing professional chart components
|
||||
- And all the authors of the open source software used in this project!
|
||||
- And all the authors of open source software used in this project!
|
||||
|
||||
## 📞 Contact Us
|
||||
|
||||
- **Project Link**: [https://github.com/lintsinghua/XCodeReviewer](https://github.com/lintsinghua/XCodeReviewer)
|
||||
- **Issue Feedback**: [Issues](https://github.com/lintsinghua/XCodeReviewer/issues)
|
||||
- **Email**: tsinghuaiiilove@gmail.com
|
||||
- **Issue Reports**: [Issues](https://github.com/lintsinghua/XCodeReviewer/issues)
|
||||
- **Author Email**: tsinghuaiiilove@gmail.com
|
||||
|
||||
---
|
||||
|
||||
⭐ If this project is helpful to you, please give us a **Star**! Your support is the driving force for our continuous progress!
|
||||
⭐ If this project helps you, please give us a **Star**! Your support is our motivation to keep moving forward!
|
||||
[](https://star-history.com/#lintsinghua/XCodeReview
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"config": "config/tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@
|
|||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>XCodeReviewer</title>
|
||||
</head>
|
||||
<body class="dark:bg-gray-900">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script type="module" src="/src/app/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
11
package.json
11
package.json
|
|
@ -1,12 +1,18 @@
|
|||
{
|
||||
"name": "miaoda-react-admin",
|
||||
"name": "xcode-reviewer",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --strictPort --port 5173",
|
||||
"lint": "tsgo -p tsconfig.check.json; biome lint --only=correctness/noUndeclaredDependencies; ast-grep scan"
|
||||
"lint": "tsgo -p tsconfig.check.json; biome lint --only=correctness/noUndeclaredDependencies; ast-grep scan",
|
||||
"lint:fix": "biome check --apply .",
|
||||
"type-check": "tsc --noEmit",
|
||||
"format": "biome format --write .",
|
||||
"clean": "rm -rf dist node_modules/.vite",
|
||||
"setup": "node scripts/setup.js || ./scripts/setup.sh",
|
||||
"analyze": "vite build --mode analyze"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
|
|
@ -82,6 +88,7 @@
|
|||
"globals": "^15.14.0",
|
||||
"postcss": "^8.5.2",
|
||||
"tailwindcss": "^3.4.11",
|
||||
"terser": "^5.44.0",
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^5.1.4",
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
console.log('🔍 检查项目设置...');
|
||||
|
||||
// 检查必要文件
|
||||
const requiredFiles = [
|
||||
'src/main.tsx',
|
||||
'src/App.tsx',
|
||||
'src/index.css',
|
||||
'src/components/common/PageMeta.tsx',
|
||||
'src/components/common/Header.tsx',
|
||||
'src/components/common/Footer.tsx',
|
||||
'src/pages/Dashboard.tsx',
|
||||
'src/pages/Projects.tsx',
|
||||
'src/pages/InstantAnalysis.tsx',
|
||||
'src/pages/AuditTasks.tsx',
|
||||
'src/pages/ProjectDetail.tsx',
|
||||
'src/pages/TaskDetail.tsx',
|
||||
'src/pages/AdminDashboard.tsx',
|
||||
'src/services/codeAnalysis.ts',
|
||||
'src/services/repoScan.ts',
|
||||
'src/services/repoZipScan.ts',
|
||||
'src/db/supabase.ts',
|
||||
'src/types/types.ts',
|
||||
'src/lib/utils.ts',
|
||||
'src/routes.tsx',
|
||||
'package.json',
|
||||
'vite.config.ts',
|
||||
'tailwind.config.js',
|
||||
'tsconfig.json',
|
||||
'tsconfig.app.json'
|
||||
];
|
||||
|
||||
let missingFiles = [];
|
||||
|
||||
for (const file of requiredFiles) {
|
||||
if (!fs.existsSync(file)) {
|
||||
missingFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingFiles.length > 0) {
|
||||
console.log('❌ 缺少以下文件:');
|
||||
missingFiles.forEach(file => console.log(` - ${file}`));
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('✅ 所有必要文件都存在');
|
||||
}
|
||||
|
||||
// 检查环境变量文件
|
||||
if (!fs.existsSync('.env') && !fs.existsSync('.env.example')) {
|
||||
console.log('⚠️ 缺少环境变量文件');
|
||||
} else {
|
||||
console.log('✅ 环境变量文件存在');
|
||||
}
|
||||
|
||||
// 检查node_modules
|
||||
if (!fs.existsSync('node_modules')) {
|
||||
console.log('❌ 缺少 node_modules,请运行 npm install');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('✅ 依赖已安装');
|
||||
}
|
||||
|
||||
// 检查关键依赖
|
||||
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
const requiredDeps = [
|
||||
'react',
|
||||
'react-dom',
|
||||
'react-router-dom',
|
||||
'@google/generative-ai',
|
||||
'@supabase/supabase-js',
|
||||
'tailwindcss',
|
||||
'vite',
|
||||
'typescript'
|
||||
];
|
||||
|
||||
let missingDeps = [];
|
||||
for (const dep of requiredDeps) {
|
||||
if (!packageJson.dependencies[dep] && !packageJson.devDependencies[dep]) {
|
||||
missingDeps.push(dep);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingDeps.length > 0) {
|
||||
console.log('❌ 缺少以下依赖:');
|
||||
missingDeps.forEach(dep => console.log(` - ${dep}`));
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('✅ 所有关键依赖都存在');
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('🎉 项目设置检查完成!');
|
||||
console.log('');
|
||||
console.log('📝 下一步:');
|
||||
console.log(' 1. 确保 .env 文件中配置了 VITE_GEMINI_API_KEY');
|
||||
console.log(' 2. 运行 npm run dev 启动开发服务器');
|
||||
console.log(' 3. 在浏览器中访问 http://localhost:5173');
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
echo 🚀 XCodeReviewer 项目设置开始...
|
||||
|
||||
REM 检查 Node.js 版本
|
||||
echo 📋 检查 Node.js 版本...
|
||||
node -v >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo ❌ 未找到 Node.js,请先安装 Node.js 18+
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
for /f "tokens=1 delims=." %%a in ('node -v') do (
|
||||
set NODE_MAJOR=%%a
|
||||
set NODE_MAJOR=!NODE_MAJOR:~1!
|
||||
)
|
||||
|
||||
if !NODE_MAJOR! LSS 18 (
|
||||
echo ❌ Node.js 版本过低,需要 18+,当前版本:
|
||||
node -v
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo ✅ Node.js 版本检查通过:
|
||||
node -v
|
||||
|
||||
REM 检查包管理器
|
||||
echo 📦 检查包管理器...
|
||||
pnpm -v >nul 2>&1
|
||||
if not errorlevel 1 (
|
||||
set PKG_MANAGER=pnpm
|
||||
echo ✅ 使用 pnpm
|
||||
goto install_deps
|
||||
)
|
||||
|
||||
yarn -v >nul 2>&1
|
||||
if not errorlevel 1 (
|
||||
set PKG_MANAGER=yarn
|
||||
echo ✅ 使用 yarn
|
||||
goto install_deps
|
||||
)
|
||||
|
||||
npm -v >nul 2>&1
|
||||
if not errorlevel 1 (
|
||||
set PKG_MANAGER=npm
|
||||
echo ✅ 使用 npm
|
||||
goto install_deps
|
||||
)
|
||||
|
||||
echo ❌ 未找到包管理器,请安装 npm、yarn 或 pnpm
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:install_deps
|
||||
REM 安装依赖
|
||||
echo 📥 安装项目依赖...
|
||||
%PKG_MANAGER% install
|
||||
|
||||
REM 检查环境变量文件
|
||||
echo 🔧 检查环境变量配置...
|
||||
if not exist ".env" (
|
||||
if exist ".env.example" (
|
||||
copy ".env.example" ".env" >nul
|
||||
echo ✅ 已创建 .env 文件,请编辑配置必要的环境变量
|
||||
echo.
|
||||
echo 📝 必需配置的环境变量:
|
||||
echo VITE_GEMINI_API_KEY - Google Gemini API 密钥
|
||||
echo.
|
||||
echo 📝 可选配置的环境变量:
|
||||
echo VITE_SUPABASE_URL - Supabase 项目 URL
|
||||
echo VITE_SUPABASE_ANON_KEY - Supabase 匿名密钥
|
||||
echo VITE_GITHUB_TOKEN - GitHub 访问令牌
|
||||
echo.
|
||||
echo ⚠️ 请在启动项目前配置 VITE_GEMINI_API_KEY
|
||||
) else (
|
||||
echo ❌ 未找到 .env.example 文件
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
) else (
|
||||
echo ✅ .env 文件已存在
|
||||
)
|
||||
|
||||
REM 检查 Gemini API Key
|
||||
if exist ".env" (
|
||||
findstr /C:"VITE_GEMINI_API_KEY=your_gemini_api_key_here" .env >nul
|
||||
if not errorlevel 1 (
|
||||
echo ⚠️ 请配置 Google Gemini API Key:
|
||||
echo 1. 访问 https://makersuite.google.com/app/apikey
|
||||
echo 2. 创建 API Key
|
||||
echo 3. 在 .env 文件中设置 VITE_GEMINI_API_KEY
|
||||
) else (
|
||||
findstr /C:"VITE_GEMINI_API_KEY=" .env >nul
|
||||
if not errorlevel 1 (
|
||||
echo ✅ Gemini API Key 已配置
|
||||
) else (
|
||||
echo ⚠️ 请在 .env 文件中配置 VITE_GEMINI_API_KEY
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 🎉 项目设置完成!
|
||||
echo.
|
||||
echo 📚 接下来的步骤:
|
||||
echo 1. 编辑 .env 文件,配置必要的环境变量
|
||||
echo 2. 运行 '%PKG_MANAGER% dev' 启动开发服务器
|
||||
echo 3. 在浏览器中访问 http://localhost:5173
|
||||
echo.
|
||||
echo 📖 更多信息请查看:
|
||||
echo - README.md - 项目介绍和使用指南
|
||||
echo - DEPLOYMENT.md - 部署指南
|
||||
echo - FEATURES.md - 功能特性详解
|
||||
echo.
|
||||
echo 🆘 需要帮助?
|
||||
echo - GitHub Issues: https://github.com/lintsinghua/XCodeReviewer/issues
|
||||
echo - 邮箱: tsinghuaiiilove@gmail.com
|
||||
echo.
|
||||
echo Happy coding! 🚀
|
||||
pause
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
console.log('🚀 XCodeReviewer 项目设置开始...');
|
||||
|
||||
// 检查 Node.js 版本
|
||||
function checkNodeVersion() {
|
||||
console.log('📋 检查 Node.js 版本...');
|
||||
const nodeVersion = process.version;
|
||||
const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]);
|
||||
|
||||
if (majorVersion < 18) {
|
||||
console.error(`❌ Node.js 版本过低,需要 18+,当前版本: ${nodeVersion}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`✅ Node.js 版本检查通过: ${nodeVersion}`);
|
||||
}
|
||||
|
||||
// 检查包管理器
|
||||
function detectPackageManager() {
|
||||
console.log('📦 检查包管理器...');
|
||||
|
||||
const managers = ['pnpm', 'yarn', 'npm'];
|
||||
|
||||
for (const manager of managers) {
|
||||
try {
|
||||
execSync(`${manager} --version`, { stdio: 'ignore' });
|
||||
console.log(`✅ 使用 ${manager}`);
|
||||
return manager;
|
||||
} catch (error) {
|
||||
// 继续检查下一个
|
||||
}
|
||||
}
|
||||
|
||||
console.error('❌ 未找到包管理器,请安装 npm、yarn 或 pnpm');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 安装依赖
|
||||
function installDependencies(packageManager) {
|
||||
console.log('📥 安装项目依赖...');
|
||||
try {
|
||||
execSync(`${packageManager} install`, { stdio: 'inherit' });
|
||||
} catch (error) {
|
||||
console.error('❌ 依赖安装失败');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置环境变量
|
||||
function setupEnvironment() {
|
||||
console.log('🔧 检查环境变量配置...');
|
||||
|
||||
const envPath = '.env';
|
||||
const envExamplePath = '.env.example';
|
||||
|
||||
if (!fs.existsSync(envPath)) {
|
||||
if (fs.existsSync(envExamplePath)) {
|
||||
fs.copyFileSync(envExamplePath, envPath);
|
||||
console.log('✅ 已创建 .env 文件,请编辑配置必要的环境变量');
|
||||
console.log('');
|
||||
console.log('📝 必需配置的环境变量:');
|
||||
console.log(' VITE_GEMINI_API_KEY - Google Gemini API 密钥');
|
||||
console.log('');
|
||||
console.log('📝 可选配置的环境变量:');
|
||||
console.log(' VITE_SUPABASE_URL - Supabase 项目 URL');
|
||||
console.log(' VITE_SUPABASE_ANON_KEY - Supabase 匿名密钥');
|
||||
console.log(' VITE_GITHUB_TOKEN - GitHub 访问令牌');
|
||||
console.log('');
|
||||
console.log('⚠️ 请在启动项目前配置 VITE_GEMINI_API_KEY');
|
||||
} else {
|
||||
console.error('❌ 未找到 .env.example 文件');
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.log('✅ .env 文件已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 API Key 配置
|
||||
function checkApiKey() {
|
||||
const envPath = '.env';
|
||||
|
||||
if (fs.existsSync(envPath)) {
|
||||
const envContent = fs.readFileSync(envPath, 'utf8');
|
||||
|
||||
if (envContent.includes('VITE_GEMINI_API_KEY=your_gemini_api_key_here') ||
|
||||
!envContent.includes('VITE_GEMINI_API_KEY=')) {
|
||||
console.log('⚠️ 请配置 Google Gemini API Key:');
|
||||
console.log(' 1. 访问 https://makersuite.google.com/app/apikey');
|
||||
console.log(' 2. 创建 API Key');
|
||||
console.log(' 3. 在 .env 文件中设置 VITE_GEMINI_API_KEY');
|
||||
} else {
|
||||
console.log('✅ Gemini API Key 已配置');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主函数
|
||||
function main() {
|
||||
try {
|
||||
checkNodeVersion();
|
||||
const packageManager = detectPackageManager();
|
||||
installDependencies(packageManager);
|
||||
setupEnvironment();
|
||||
checkApiKey();
|
||||
|
||||
console.log('');
|
||||
console.log('🎉 项目设置完成!');
|
||||
console.log('');
|
||||
console.log('📚 接下来的步骤:');
|
||||
console.log(` 1. 编辑 .env 文件,配置必要的环境变量`);
|
||||
console.log(` 2. 运行 '${packageManager} dev' 启动开发服务器`);
|
||||
console.log(' 3. 在浏览器中访问 http://localhost:5173');
|
||||
console.log('');
|
||||
console.log('📖 更多信息请查看:');
|
||||
console.log(' - README.md - 项目介绍和使用指南');
|
||||
console.log(' - DEPLOYMENT.md - 部署指南');
|
||||
console.log(' - FEATURES.md - 功能特性详解');
|
||||
console.log('');
|
||||
console.log('🆘 需要帮助?');
|
||||
console.log(' - GitHub Issues: https://github.com/lintsinghua/XCodeReviewer/issues');
|
||||
console.log(' - 邮箱: tsinghuaiiilove@gmail.com');
|
||||
console.log('');
|
||||
console.log('Happy coding! 🚀');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 设置过程中出现错误:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行主函数
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { main };
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
#!/bin/bash
|
||||
|
||||
# XCodeReviewer 项目设置脚本
|
||||
# 用于快速设置开发环境
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 XCodeReviewer 项目设置开始..."
|
||||
|
||||
# 检查 Node.js 版本
|
||||
echo "📋 检查 Node.js 版本..."
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo "❌ 未找到 Node.js,请先安装 Node.js 18+"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
|
||||
if [ "$NODE_VERSION" -lt 18 ]; then
|
||||
echo "❌ Node.js 版本过低,需要 18+,当前版本: $(node -v)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Node.js 版本检查通过: $(node -v)"
|
||||
|
||||
# 检查包管理器
|
||||
echo "📦 检查包管理器..."
|
||||
if command -v pnpm &> /dev/null; then
|
||||
PKG_MANAGER="pnpm"
|
||||
echo "✅ 使用 pnpm"
|
||||
elif command -v yarn &> /dev/null; then
|
||||
PKG_MANAGER="yarn"
|
||||
echo "✅ 使用 yarn"
|
||||
elif command -v npm &> /dev/null; then
|
||||
PKG_MANAGER="npm"
|
||||
echo "✅ 使用 npm"
|
||||
else
|
||||
echo "❌ 未找到包管理器,请安装 npm、yarn 或 pnpm"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 安装依赖
|
||||
echo "📥 安装项目依赖..."
|
||||
$PKG_MANAGER install
|
||||
|
||||
# 检查环境变量文件
|
||||
echo "🔧 检查环境变量配置..."
|
||||
if [ ! -f ".env" ]; then
|
||||
if [ -f ".env.example" ]; then
|
||||
cp .env.example .env
|
||||
echo "✅ 已创建 .env 文件,请编辑配置必要的环境变量"
|
||||
echo ""
|
||||
echo "📝 必需配置的环境变量:"
|
||||
echo " VITE_GEMINI_API_KEY - Google Gemini API 密钥"
|
||||
echo ""
|
||||
echo "📝 可选配置的环境变量:"
|
||||
echo " VITE_SUPABASE_URL - Supabase 项目 URL"
|
||||
echo " VITE_SUPABASE_ANON_KEY - Supabase 匿名密钥"
|
||||
echo " VITE_GITHUB_TOKEN - GitHub 访问令牌"
|
||||
echo ""
|
||||
echo "⚠️ 请在启动项目前配置 VITE_GEMINI_API_KEY"
|
||||
else
|
||||
echo "❌ 未找到 .env.example 文件"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "✅ .env 文件已存在"
|
||||
fi
|
||||
|
||||
# 检查 Gemini API Key
|
||||
if [ -f ".env" ]; then
|
||||
if grep -q "VITE_GEMINI_API_KEY=your_gemini_api_key_here" .env || ! grep -q "VITE_GEMINI_API_KEY=" .env; then
|
||||
echo "⚠️ 请配置 Google Gemini API Key:"
|
||||
echo " 1. 访问 https://makersuite.google.com/app/apikey"
|
||||
echo " 2. 创建 API Key"
|
||||
echo " 3. 在 .env 文件中设置 VITE_GEMINI_API_KEY"
|
||||
else
|
||||
echo "✅ Gemini API Key 已配置"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 构建检查
|
||||
echo "🔨 检查构建配置..."
|
||||
if $PKG_MANAGER run build --dry-run &> /dev/null; then
|
||||
echo "✅ 构建配置正常"
|
||||
else
|
||||
echo "⚠️ 构建配置可能有问题,请检查"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🎉 项目设置完成!"
|
||||
echo ""
|
||||
echo "📚 接下来的步骤:"
|
||||
echo " 1. 编辑 .env 文件,配置必要的环境变量"
|
||||
echo " 2. 运行 '$PKG_MANAGER dev' 启动开发服务器"
|
||||
echo " 3. 在浏览器中访问 http://localhost:5173"
|
||||
echo ""
|
||||
echo "📖 更多信息请查看:"
|
||||
echo " - README.md - 项目介绍和使用指南"
|
||||
echo " - DEPLOYMENT.md - 部署指南"
|
||||
echo " - FEATURES.md - 功能特性详解"
|
||||
echo ""
|
||||
echo "🆘 需要帮助?"
|
||||
echo " - GitHub Issues: https://github.com/lintsinghua/XCodeReviewer/issues"
|
||||
echo " - 邮箱: tsinghuaiiilove@gmail.com"
|
||||
echo ""
|
||||
echo "Happy coding! 🚀"
|
||||
|
|
@ -7,9 +7,9 @@ function App() {
|
|||
return (
|
||||
<BrowserRouter>
|
||||
<Toaster position="top-right" />
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="min-h-screen gradient-bg">
|
||||
<Header />
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<main className="container-responsive py-4 md:py-6">
|
||||
<Routes>
|
||||
{routes.map((route) => (
|
||||
<Route
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
|
||||
import { Toaster } from "sonner";
|
||||
import Header from "@/components/layout/Header";
|
||||
import routes from "./routes";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Toaster position="top-right" />
|
||||
<div className="min-h-screen gradient-bg">
|
||||
<Header />
|
||||
<main className="container-responsive py-4 md:py-6">
|
||||
<Routes>
|
||||
{routes.map((route) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={route.element}
|
||||
/>
|
||||
))}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import "@/assets/styles/globals.css";
|
||||
import App from "./App.tsx";
|
||||
import { AppWrapper } from "./components/common/PageMeta.tsx";
|
||||
import { AppWrapper } from "@/components/layout/PageMeta";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import Dashboard from "./pages/Dashboard";
|
||||
import Projects from "./pages/Projects";
|
||||
import ProjectDetail from "./pages/ProjectDetail";
|
||||
import InstantAnalysis from "./pages/InstantAnalysis";
|
||||
import AuditTasks from "./pages/AuditTasks";
|
||||
import TaskDetail from "./pages/TaskDetail";
|
||||
import AdminDashboard from "./pages/AdminDashboard";
|
||||
import Dashboard from "@/pages/Dashboard";
|
||||
import Projects from "@/pages/Projects";
|
||||
import ProjectDetail from "@/pages/ProjectDetail";
|
||||
import InstantAnalysis from "@/pages/InstantAnalysis";
|
||||
import AuditTasks from "@/pages/AuditTasks";
|
||||
import TaskDetail from "@/pages/TaskDetail";
|
||||
import AdminDashboard from "@/pages/AdminDashboard";
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface RouteConfig {
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
/* stylelint-disable */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Definition of the design system. All colors, gradients, fonts, etc should be defined here.
|
||||
All colors MUST be HSL.
|
||||
*/
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
--sidebar-background: 0 0% 98%;
|
||||
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
|
||||
--sidebar-border: 220 13% 91%;
|
||||
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* 页面标题样式 */
|
||||
.page-title {
|
||||
@apply text-2xl md:text-3xl font-bold text-gray-900 tracking-tight;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
@apply text-sm md:text-base text-gray-600 mt-1;
|
||||
}
|
||||
|
||||
/* 现代化卡片样式 */
|
||||
.card-modern {
|
||||
@apply bg-white rounded-xl border border-gray-200/60 shadow-sm hover:shadow-md transition-all duration-200;
|
||||
}
|
||||
|
||||
/* 统计卡片样式 */
|
||||
.stat-card {
|
||||
@apply bg-white rounded-xl border border-gray-200/60 shadow-sm hover:shadow-lg transition-all duration-300 hover:border-gray-300/60;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
@apply text-xs font-medium text-gray-500 uppercase tracking-wide;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
@apply text-2xl md:text-3xl font-bold text-gray-900 mt-1;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
@apply w-12 h-12 rounded-lg bg-gradient-to-br flex items-center justify-center shadow-sm;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn-primary {
|
||||
@apply bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white shadow-sm hover:shadow-md transition-all duration-200;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply border-gray-300 hover:border-gray-400 hover:bg-gray-50 transition-all duration-200;
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-state {
|
||||
@apply flex flex-col items-center justify-center text-center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
@apply w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mb-4;
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 渐变背景 */
|
||||
.gradient-bg {
|
||||
@apply bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50;
|
||||
}
|
||||
|
||||
/* 响应式容器 */
|
||||
.container-responsive {
|
||||
@apply container mx-auto px-4 sm:px-6 lg:px-8;
|
||||
}
|
||||
|
||||
/* 文本截断 */
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 毛玻璃效果 */
|
||||
.backdrop-blur-md {
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
/* 平滑滚动 */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(156, 163, 175, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(156, 163, 175, 0.8);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="bg-gradient-to-r from-amber-50 to-orange-50 border-t border-amber-200">
|
||||
<div className="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{/* ================= 关于我们 ================= */}
|
||||
<div>
|
||||
{/* 标题:改成你们项目的“关于我们” */}
|
||||
<h3 className="text-lg font-semibold text-amber-800 mb-4">
|
||||
{/* 关于我们 */}
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
{/* 在这里填写“关于我们”的介绍,比如:致力于xxx,让xxx更加xxx */}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ================= 联系信息 ================= */}
|
||||
<div>
|
||||
{/* 标题:联系信息 */}
|
||||
<h3 className="text-lg font-semibold text-amber-800 mb-4">
|
||||
{/* 联系信息 */}
|
||||
</h3>
|
||||
<div className="text-gray-600 space-y-2">
|
||||
<p>
|
||||
{/* 地址:XXX省XXX市XXX区XXX路XXX号 */}
|
||||
</p>
|
||||
<p>
|
||||
{/* 电话:010-XXXXXXX */}
|
||||
</p>
|
||||
<p>
|
||||
{/* 邮箱:info@example.com */}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ================= 开放时间 / 其他信息 / 也可删除 ================= */}
|
||||
<div>
|
||||
{/* 标题:可改成“开放时间”或者“服务时间” */}
|
||||
<h3 className="text-lg font-semibold text-amber-800 mb-4">
|
||||
{/* 开放时间 */}
|
||||
</h3>
|
||||
<div className="text-gray-600 space-y-2">
|
||||
<p>
|
||||
{/* 周一至周五:9:00-18:00 */}
|
||||
</p>
|
||||
<p>
|
||||
{/* 周末及法定节假日请注意公告 */}
|
||||
</p>
|
||||
<p>
|
||||
{/* 其他说明,比如“需要提前预约” */}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ================= 版权区域 ================= */}
|
||||
<div className="mt-8 pt-8 border-t border-amber-200 text-center text-gray-600">
|
||||
<p>
|
||||
{/* © {currentYear} 你的公司或组织名称 */}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { HelmetProvider, Helmet } from "react-helmet-async";
|
||||
|
||||
const PageMeta = ({
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
}) => (
|
||||
<Helmet>
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
</Helmet>
|
||||
);
|
||||
|
||||
export const AppWrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<HelmetProvider>{children}</HelmetProvider>
|
||||
);
|
||||
|
||||
export default PageMeta;
|
||||
|
|
@ -1,114 +1,152 @@
|
|||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { api, supabase } from "@/db/supabase";
|
||||
import { Database, CheckCircle, AlertTriangle, RefreshCw } from "lucide-react";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import {
|
||||
Database,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
Loader2,
|
||||
RefreshCw
|
||||
} from "lucide-react";
|
||||
import { api } from "@/shared/config/database";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface TestResult {
|
||||
name: string;
|
||||
status: 'success' | 'error' | 'pending';
|
||||
message: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export default function DatabaseTest() {
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [results, setResults] = useState<any>(null);
|
||||
const [results, setResults] = useState<TestResult[]>([]);
|
||||
|
||||
const testConnection = async (): Promise<boolean> => {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('count', { count: 'exact', head: true });
|
||||
|
||||
if (error) {
|
||||
console.error('Database connection test failed:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('Database connection successful');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Database connection error:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const runDatabaseTest = async () => {
|
||||
const runTests = async () => {
|
||||
setTesting(true);
|
||||
const testResults: any = {
|
||||
connection: false,
|
||||
tables: {},
|
||||
sampleData: {},
|
||||
errors: []
|
||||
};
|
||||
setResults([]);
|
||||
|
||||
try {
|
||||
// 测试基本连接
|
||||
console.log('测试数据库连接...');
|
||||
testResults.connection = await testConnection();
|
||||
|
||||
// 测试各个表
|
||||
const tables = ['profiles', 'projects', 'audit_tasks', 'audit_issues', 'instant_analyses'];
|
||||
|
||||
for (const table of tables) {
|
||||
try {
|
||||
console.log(`测试表: ${table}`);
|
||||
const { data, error, count } = await supabase
|
||||
.from(table)
|
||||
.select('*', { count: 'exact', head: true });
|
||||
|
||||
testResults.tables[table] = {
|
||||
accessible: !error,
|
||||
count: count || 0,
|
||||
error: error?.message
|
||||
};
|
||||
|
||||
if (error) {
|
||||
testResults.errors.push(`${table}: ${error.message}`);
|
||||
const tests: Array<{ name: string; test: () => Promise<any> }> = [
|
||||
{
|
||||
name: "数据库连接测试",
|
||||
test: async () => {
|
||||
const start = Date.now();
|
||||
await api.getProjectStats();
|
||||
return { duration: Date.now() - start };
|
||||
}
|
||||
} catch (err) {
|
||||
testResults.tables[table] = {
|
||||
accessible: false,
|
||||
count: 0,
|
||||
error: (err as Error).message
|
||||
};
|
||||
testResults.errors.push(`${table}: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 测试示例数据获取
|
||||
try {
|
||||
console.log('测试项目数据获取...');
|
||||
},
|
||||
{
|
||||
name: "项目数据查询",
|
||||
test: async () => {
|
||||
const start = Date.now();
|
||||
const projects = await api.getProjects();
|
||||
testResults.sampleData.projects = {
|
||||
success: true,
|
||||
count: projects.length,
|
||||
data: projects.slice(0, 2) // 只显示前2个
|
||||
return {
|
||||
duration: Date.now() - start,
|
||||
count: projects.length
|
||||
};
|
||||
} catch (err) {
|
||||
testResults.sampleData.projects = {
|
||||
success: false,
|
||||
error: (err as Error).message
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "审计任务查询",
|
||||
test: async () => {
|
||||
const start = Date.now();
|
||||
const tasks = await api.getAuditTasks();
|
||||
return {
|
||||
duration: Date.now() - start,
|
||||
count: tasks.length
|
||||
};
|
||||
testResults.errors.push(`项目数据: ${(err as Error).message}`);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "用户配置查询",
|
||||
test: async () => {
|
||||
const start = Date.now();
|
||||
const count = await api.getProfilesCount();
|
||||
return {
|
||||
duration: Date.now() - start,
|
||||
count
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
for (const { name, test } of tests) {
|
||||
try {
|
||||
// 添加pending状态
|
||||
setResults(prev => [...prev, { name, status: 'pending', message: '测试中...' }]);
|
||||
|
||||
const result = await test();
|
||||
|
||||
// 更新为成功状态
|
||||
setResults(prev => prev.map(r =>
|
||||
r.name === name
|
||||
? {
|
||||
name,
|
||||
status: 'success',
|
||||
message: `测试通过 (${result.duration}ms)${result.count !== undefined ? ` - 数据量: ${result.count}` : ''}`,
|
||||
duration: result.duration
|
||||
}
|
||||
: r
|
||||
));
|
||||
} catch (error: any) {
|
||||
// 更新为错误状态
|
||||
setResults(prev => prev.map(r =>
|
||||
r.name === name
|
||||
? {
|
||||
name,
|
||||
status: 'error',
|
||||
message: `测试失败: ${error.message || '未知错误'}`
|
||||
}
|
||||
: r
|
||||
));
|
||||
}
|
||||
|
||||
setResults(testResults);
|
||||
|
||||
if (testResults.connection && testResults.errors.length === 0) {
|
||||
toast.success("数据库测试通过!");
|
||||
} else {
|
||||
toast.error(`数据库测试发现 ${testResults.errors.length} 个问题`);
|
||||
// 添加延迟避免过快执行
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('数据库测试失败:', error);
|
||||
testResults.errors.push(`总体测试失败: ${(error as Error).message}`);
|
||||
setResults(testResults);
|
||||
toast.error("数据库测试失败");
|
||||
} finally {
|
||||
setTesting(false);
|
||||
|
||||
const successCount = results.filter(r => r.status === 'success').length;
|
||||
const totalCount = tests.length;
|
||||
|
||||
if (successCount === totalCount) {
|
||||
toast.success("所有数据库测试通过!");
|
||||
} else {
|
||||
toast.error(`${totalCount - successCount} 个测试失败`);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: TestResult['status']) => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return <CheckCircle className="w-4 h-4 text-green-600" />;
|
||||
case 'error':
|
||||
return <AlertTriangle className="w-4 h-4 text-red-600" />;
|
||||
case 'pending':
|
||||
return <Loader2 className="w-4 h-4 text-blue-600 animate-spin" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: TestResult['status']) => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return <Badge className="bg-green-100 text-green-800">通过</Badge>;
|
||||
case 'error':
|
||||
return <Badge className="bg-red-100 text-red-800">失败</Badge>;
|
||||
case 'pending':
|
||||
return <Badge className="bg-blue-100 text-blue-800">测试中</Badge>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-4xl mx-auto">
|
||||
<Card className="w-full max-w-2xl mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Database className="w-5 h-5 mr-2" />
|
||||
|
|
@ -116,104 +154,66 @@ export default function DatabaseTest() {
|
|||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button onClick={runDatabaseTest} disabled={testing}>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-gray-600">
|
||||
测试数据库连接状态和基本功能
|
||||
</p>
|
||||
<Button
|
||||
onClick={runTests}
|
||||
disabled={testing}
|
||||
size="sm"
|
||||
>
|
||||
{testing ? (
|
||||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
测试中...
|
||||
</>
|
||||
) : (
|
||||
<Database className="w-4 h-4 mr-2" />
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
开始测试
|
||||
</>
|
||||
)}
|
||||
{testing ? '测试中...' : '开始测试'}
|
||||
</Button>
|
||||
|
||||
{results && (
|
||||
<Badge variant={results.connection && results.errors.length === 0 ? "default" : "destructive"}>
|
||||
{results.connection && results.errors.length === 0 ? (
|
||||
<CheckCircle className="w-3 h-3 mr-1" />
|
||||
) : (
|
||||
<AlertTriangle className="w-3 h-3 mr-1" />
|
||||
)}
|
||||
{results.connection && results.errors.length === 0 ? '连接正常' : '存在问题'}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{results && (
|
||||
<div className="space-y-4">
|
||||
{/* 连接状态 */}
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h4 className="font-medium mb-2">基础连接</h4>
|
||||
<div className="flex items-center space-x-2">
|
||||
{results.connection ? (
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
) : (
|
||||
<AlertTriangle className="w-4 h-4 text-red-600" />
|
||||
)}
|
||||
<span className={results.connection ? 'text-green-600' : 'text-red-600'}>
|
||||
{results.connection ? '数据库连接成功' : '数据库连接失败'}
|
||||
</span>
|
||||
{results.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{results.map((result, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-3 border rounded-lg"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
{getStatusIcon(result.status)}
|
||||
<div>
|
||||
<p className="font-medium text-sm">{result.name}</p>
|
||||
<p className="text-xs text-gray-500">{result.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 表状态 */}
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h4 className="font-medium mb-2">数据表状态</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
{Object.entries(results.tables).map(([tableName, tableInfo]: [string, any]) => (
|
||||
<div key={tableName} className="flex items-center justify-between p-2 bg-gray-50 rounded">
|
||||
<div className="flex items-center space-x-2">
|
||||
{tableInfo.accessible ? (
|
||||
<CheckCircle className="w-3 h-3 text-green-600" />
|
||||
) : (
|
||||
<AlertTriangle className="w-3 h-3 text-red-600" />
|
||||
)}
|
||||
<span className="text-sm font-medium">{tableName}</span>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{tableInfo.count} 条记录
|
||||
</Badge>
|
||||
{getStatusBadge(result.status)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 示例数据 */}
|
||||
{results.sampleData.projects && (
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h4 className="font-medium mb-2">示例数据</h4>
|
||||
{results.sampleData.projects.success ? (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-green-600">
|
||||
✅ 成功获取 {results.sampleData.projects.count} 个项目
|
||||
</p>
|
||||
{results.sampleData.projects.data.map((project: any, index: number) => (
|
||||
<div key={index} className="text-xs bg-gray-50 p-2 rounded">
|
||||
<strong>{project.name}</strong> - {project.description || '无描述'}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-red-600">
|
||||
❌ 获取项目数据失败: {results.sampleData.projects.error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 错误信息 */}
|
||||
{results.errors.length > 0 && (
|
||||
<div className="p-4 border border-red-200 bg-red-50 rounded-lg">
|
||||
<h4 className="font-medium text-red-800 mb-2">发现的问题</h4>
|
||||
<ul className="space-y-1">
|
||||
{results.errors.map((error: string, index: number) => (
|
||||
<li key={index} className="text-sm text-red-700">
|
||||
• {error}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{results.length > 0 && !testing && (
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
测试完成!成功: {results.filter(r => r.status === 'success').length} /
|
||||
总计: {results.length}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{results.length === 0 && !testing && (
|
||||
<Alert>
|
||||
<Database className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
点击"开始测试"按钮来检查数据库连接状态
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import React from "react";
|
||||
import { Code } from "lucide-react";
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="bg-white border-t border-gray-200/60 mt-16">
|
||||
<div className="container-responsive py-8">
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center space-x-2 mb-4">
|
||||
<div className="w-6 h-6 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center">
|
||||
<Code className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<span className="text-lg font-semibold text-gray-900">XCodeReviewer</span>
|
||||
</div>
|
||||
<p className="text-gray-500 text-sm">
|
||||
© {currentYear} XCodeReviewer. 致力于提升代码质量,保障软件安全.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
import { useState } from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Menu,
|
||||
X,
|
||||
Code
|
||||
} from "lucide-react";
|
||||
import routes from "@/routes";
|
||||
import routes from "@/app/routes";
|
||||
|
||||
export default function Header() {
|
||||
const location = useLocation();
|
||||
|
|
@ -15,30 +14,28 @@ export default function Header() {
|
|||
|
||||
const visibleRoutes = routes.filter(route => route.visible !== false);
|
||||
|
||||
const user = null;
|
||||
|
||||
return (
|
||||
<header className="bg-white shadow-sm border-b">
|
||||
<div className="container mx-auto px-4">
|
||||
<header className="bg-white/80 backdrop-blur-md shadow-sm border-b border-gray-200/60 sticky top-0 z-50">
|
||||
<div className="container-responsive">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
{/* Logo */}
|
||||
<Link to="/" className="flex items-center space-x-2">
|
||||
<div className="w-8 h-8 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center">
|
||||
<Link to="/" className="flex items-center space-x-3 group">
|
||||
<div className="w-9 h-9 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-xl flex items-center justify-center shadow-sm group-hover:shadow-md transition-all">
|
||||
<Code className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<span className="text-xl font-bold text-gray-900">智能代码审计</span>
|
||||
<span className="text-xl font-bold text-gray-900 group-hover:text-blue-600 transition-colors">XCodeReviewer</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex items-center space-x-8">
|
||||
<nav className="hidden md:flex items-center space-x-1">
|
||||
{visibleRoutes.map((route) => (
|
||||
<Link
|
||||
key={route.path}
|
||||
to={route.path}
|
||||
className={`text-sm font-medium transition-colors hover:text-blue-600 ${
|
||||
className={`px-4 py-2 text-sm font-medium rounded-lg transition-all ${
|
||||
location.pathname === route.path
|
||||
? "text-blue-600"
|
||||
: "text-gray-700"
|
||||
? "text-blue-600 bg-blue-50"
|
||||
: "text-gray-700 hover:text-blue-600 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
{route.name}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import { Helmet, HelmetProvider } from "react-helmet-async";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface PageMetaProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
keywords?: string;
|
||||
image?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
interface AppWrapperProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function AppWrapper({ children }: AppWrapperProps) {
|
||||
return (
|
||||
<HelmetProvider>
|
||||
{children}
|
||||
</HelmetProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PageMeta({
|
||||
title = "XCodeReviewer",
|
||||
description = "基于AI的现代化代码质量分析和审查服务,提供全面的代码安全检测、性能分析和最佳实践建议。",
|
||||
keywords = "代码审计,代码质量,AI分析,安全检测,性能优化,代码规范",
|
||||
image = "/images/logo.png",
|
||||
url = window.location.href
|
||||
}: PageMetaProps) {
|
||||
const fullTitle = title === "XCodeReviewer" ? title : `${title} - XCodeReviewer`;
|
||||
|
||||
return (
|
||||
<Helmet>
|
||||
{/* 基本信息 */}
|
||||
<title>{fullTitle}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta name="keywords" content={keywords} />
|
||||
|
||||
{/* Open Graph */}
|
||||
<meta property="og:title" content={fullTitle} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={image} />
|
||||
<meta property="og:url" content={url} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="XCodeReviewer" />
|
||||
|
||||
{/* Twitter Card */}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={fullTitle} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={image} />
|
||||
|
||||
{/* 其他 */}
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta name="author" content="XCodeReviewer" />
|
||||
<link rel="canonical" href={url} />
|
||||
</Helmet>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
function AlertDialog({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/shared/utils/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { DayPicker } from "react-day-picker";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
function Calendar({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import useEmblaCarousel, {
|
|||
} from "embla-carousel-react";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import * as RechartsPrimitive from "recharts";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { SearchIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import * as React from "react";
|
|||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { Drawer as DrawerPrimitive } from "vaul";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import * as React from "react";
|
|||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
type FieldValues,
|
||||
} from "react-hook-form";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
const Form = FormProvider;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { OTPInput, OTPInputContext } from "input-otp";
|
||||
import { MinusIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function InputOTP({
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function Label({
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import * as MenubarPrimitive from "@radix-ui/react-menubar";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function Menubar({
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
|||
import { cva } from "class-variance-authority";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function NavigationMenu({
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
MoreHorizontalIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
|
||||
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
import { CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function RadioGroup({
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { GripVerticalIcon } from "lucide-react";
|
||||
import * as ResizablePrimitive from "react-resizable-panels";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function ResizablePanelGroup({
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import * as React from "react";
|
|||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import * as React from "react";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import * as SheetPrimitive from "@radix-ui/react-dialog";
|
|||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { Slot } from "@radix-ui/react-slot";
|
|||
import { VariantProps, cva } from "class-variance-authority";
|
||||
import { PanelLeftIcon } from "lucide-react";
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useIsMobile } from "@/shared/hooks/use-mobile";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function Slider({
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
export function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import * as ToastPrimitives from "@radix-ui/react-toast";
|
|||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useToast } from "@/shared/hooks/use-toast";
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
||||
import { type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
import { toggleVariants } from "@/components/ui/toggle";
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/shared/utils/utils";
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
// 导出代码分析相关服务
|
||||
export * from './codeAnalysis';
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
// 导出项目相关服务
|
||||
export * from './repoScan';
|
||||
export * from './repoZipScan';
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { api } from "@/db/supabase";
|
||||
import { CodeAnalysisEngine } from "@/services/codeAnalysis";
|
||||
import { api } from "@/shared/config/database";
|
||||
import { CodeAnalysisEngine } from "@/features/analysis/services";
|
||||
|
||||
type GithubTreeItem = { path: string; type: "blob" | "tree"; size?: number; url: string; sha: string };
|
||||
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
import { unzip } from "fflate";
|
||||
import { CodeAnalysisEngine } from "@/features/analysis/services";
|
||||
import { api } from "@/shared/config/database";
|
||||
|
||||
const TEXT_EXTENSIONS = [
|
||||
".js", ".ts", ".tsx", ".jsx", ".py", ".java", ".go", ".rs", ".cpp", ".c", ".h",
|
||||
".cs", ".php", ".rb", ".kt", ".swift", ".sql", ".sh", ".json", ".yml", ".yaml", ".md"
|
||||
];
|
||||
|
||||
const MAX_FILE_SIZE_BYTES = 200 * 1024; // 200KB
|
||||
const MAX_ANALYZE_FILES = 50;
|
||||
|
||||
function isTextFile(path: string): boolean {
|
||||
return TEXT_EXTENSIONS.some(ext => path.toLowerCase().endsWith(ext));
|
||||
}
|
||||
|
||||
function shouldExclude(path: string, excludePatterns: string[]): boolean {
|
||||
return excludePatterns.some(pattern => {
|
||||
if (pattern.includes('*')) {
|
||||
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
||||
return regex.test(path);
|
||||
}
|
||||
return path.includes(pattern);
|
||||
});
|
||||
}
|
||||
|
||||
function getLanguageFromPath(path: string): string {
|
||||
const extension = path.split('.').pop()?.toLowerCase() || '';
|
||||
const languageMap: Record<string, string> = {
|
||||
'js': 'javascript',
|
||||
'jsx': 'javascript',
|
||||
'ts': 'typescript',
|
||||
'tsx': 'typescript',
|
||||
'py': 'python',
|
||||
'java': 'java',
|
||||
'go': 'go',
|
||||
'rs': 'rust',
|
||||
'cpp': 'cpp',
|
||||
'c': 'cpp',
|
||||
'cs': 'csharp',
|
||||
'php': 'php',
|
||||
'rb': 'ruby',
|
||||
'kt': 'kotlin',
|
||||
'swift': 'swift'
|
||||
};
|
||||
|
||||
return languageMap[extension] || 'text';
|
||||
}
|
||||
|
||||
export async function scanZipFile(params: {
|
||||
projectId: string;
|
||||
zipFile: File;
|
||||
excludePatterns?: string[];
|
||||
createdBy?: string;
|
||||
}): Promise<string> {
|
||||
const { projectId, zipFile, excludePatterns = [], createdBy } = params;
|
||||
|
||||
// 创建审计任务
|
||||
const task = await api.createAuditTask({
|
||||
project_id: projectId,
|
||||
task_type: "repository",
|
||||
branch_name: "uploaded",
|
||||
exclude_patterns: excludePatterns,
|
||||
scan_config: { source: "zip_upload" },
|
||||
created_by: createdBy
|
||||
} as any);
|
||||
|
||||
const taskId = (task as any).id;
|
||||
|
||||
try {
|
||||
// 更新任务状态为运行中
|
||||
await api.updateAuditTask(taskId, {
|
||||
status: "running",
|
||||
started_at: new Date().toISOString()
|
||||
} as any);
|
||||
|
||||
// 读取ZIP文件
|
||||
const arrayBuffer = await zipFile.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
unzip(uint8Array, async (err, unzipped) => {
|
||||
if (err) {
|
||||
await api.updateAuditTask(taskId, { status: "failed" } as any);
|
||||
reject(new Error(`ZIP文件解压失败: ${err.message}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 筛选需要分析的文件
|
||||
const filesToAnalyze: Array<{ path: string; content: string }> = [];
|
||||
|
||||
for (const [path, data] of Object.entries(unzipped)) {
|
||||
// 跳过目录
|
||||
if (path.endsWith('/')) continue;
|
||||
|
||||
// 检查文件类型和排除模式
|
||||
if (!isTextFile(path) || shouldExclude(path, excludePatterns)) continue;
|
||||
|
||||
// 检查文件大小
|
||||
if (data.length > MAX_FILE_SIZE_BYTES) continue;
|
||||
|
||||
try {
|
||||
const content = new TextDecoder('utf-8').decode(data);
|
||||
filesToAnalyze.push({ path, content });
|
||||
} catch (decodeError) {
|
||||
// 跳过无法解码的文件
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 限制分析文件数量
|
||||
const limitedFiles = filesToAnalyze
|
||||
.sort((a, b) => a.path.length - b.path.length) // 优先分析路径较短的文件
|
||||
.slice(0, MAX_ANALYZE_FILES);
|
||||
|
||||
let totalFiles = limitedFiles.length;
|
||||
let scannedFiles = 0;
|
||||
let totalLines = 0;
|
||||
let totalIssues = 0;
|
||||
let qualityScores: number[] = [];
|
||||
|
||||
// 分析每个文件
|
||||
for (const file of limitedFiles) {
|
||||
try {
|
||||
const language = getLanguageFromPath(file.path);
|
||||
const lines = file.content.split(/\r?\n/).length;
|
||||
totalLines += lines;
|
||||
|
||||
// 使用AI分析代码
|
||||
const analysis = await CodeAnalysisEngine.analyzeCode(file.content, language);
|
||||
qualityScores.push(analysis.quality_score);
|
||||
|
||||
// 保存发现的问题
|
||||
for (const issue of analysis.issues) {
|
||||
await api.createAuditIssue({
|
||||
task_id: taskId,
|
||||
file_path: file.path,
|
||||
line_number: issue.line || null,
|
||||
column_number: issue.column || null,
|
||||
issue_type: issue.type || "maintainability",
|
||||
severity: issue.severity || "low",
|
||||
title: issue.title || "Issue",
|
||||
description: issue.description || null,
|
||||
suggestion: issue.suggestion || null,
|
||||
code_snippet: issue.code_snippet || null,
|
||||
ai_explanation: issue.ai_explanation || null,
|
||||
status: "open"
|
||||
} as any);
|
||||
|
||||
totalIssues++;
|
||||
}
|
||||
|
||||
scannedFiles++;
|
||||
|
||||
// 每分析10个文件更新一次进度
|
||||
if (scannedFiles % 10 === 0) {
|
||||
await api.updateAuditTask(taskId, {
|
||||
total_files: totalFiles,
|
||||
scanned_files: scannedFiles,
|
||||
total_lines: totalLines,
|
||||
issues_count: totalIssues
|
||||
} as any);
|
||||
}
|
||||
|
||||
// 添加延迟避免API限制
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
} catch (analysisError) {
|
||||
console.error(`分析文件 ${file.path} 失败:`, analysisError);
|
||||
// 继续分析其他文件
|
||||
}
|
||||
}
|
||||
|
||||
// 计算平均质量分
|
||||
const avgQualityScore = qualityScores.length > 0
|
||||
? qualityScores.reduce((sum, score) => sum + score, 0) / qualityScores.length
|
||||
: 0;
|
||||
|
||||
// 更新任务完成状态
|
||||
await api.updateAuditTask(taskId, {
|
||||
status: "completed",
|
||||
total_files: totalFiles,
|
||||
scanned_files: scannedFiles,
|
||||
total_lines: totalLines,
|
||||
issues_count: totalIssues,
|
||||
quality_score: avgQualityScore,
|
||||
completed_at: new Date().toISOString()
|
||||
} as any);
|
||||
|
||||
resolve(taskId);
|
||||
} catch (processingError) {
|
||||
await api.updateAuditTask(taskId, { status: "failed" } as any);
|
||||
reject(processingError);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
await api.updateAuditTask(taskId, { status: "failed" } as any);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function validateZipFile(file: File): { valid: boolean; error?: string } {
|
||||
// 检查文件类型
|
||||
if (!file.type.includes('zip') && !file.name.toLowerCase().endsWith('.zip')) {
|
||||
return { valid: false, error: '请上传ZIP格式的文件' };
|
||||
}
|
||||
|
||||
// 检查文件大小 (限制为100MB)
|
||||
const maxSize = 100 * 1024 * 1024;
|
||||
if (file.size > maxSize) {
|
||||
return { valid: false, error: '文件大小不能超过100MB' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import * as React from "react";
|
||||
|
||||
export function useDebounce<T>(value: T, delay?: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = React.useState<T>(value);
|
||||
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedValue(value), delay ?? 500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const useGoBack = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const goBack = () => {
|
||||
if (window.history.state && window.history.state.idx > 0) {
|
||||
navigate(-1); // Go back to the previous page
|
||||
} else {
|
||||
navigate("/"); // Redirect to home if no history exists
|
||||
}
|
||||
};
|
||||
|
||||
return goBack;
|
||||
};
|
||||
|
||||
export default useGoBack;
|
||||
106
src/index.css
106
src/index.css
|
|
@ -1,106 +0,0 @@
|
|||
/* stylelint-disable */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Definition of the design system. All colors, gradients, fonts, etc should be defined here.
|
||||
All colors MUST be HSL.
|
||||
*/
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
--sidebar-background: 0 0% 98%;
|
||||
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
|
||||
--sidebar-border: 220 13% 91%;
|
||||
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export type Params = Partial<
|
||||
Record<keyof URLSearchParams, string | number | null | undefined>
|
||||
>;
|
||||
|
||||
export function createQueryString(
|
||||
params: Params,
|
||||
searchParams: URLSearchParams
|
||||
) {
|
||||
const newSearchParams = new URLSearchParams(searchParams?.toString());
|
||||
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value === null || value === undefined) {
|
||||
newSearchParams.delete(key);
|
||||
} else {
|
||||
newSearchParams.set(key, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
return newSearchParams.toString();
|
||||
}
|
||||
|
||||
export function formatDate(
|
||||
date: Date | string | number,
|
||||
opts: Intl.DateTimeFormatOptions = {}
|
||||
) {
|
||||
return new Intl.DateTimeFormat("zh-CN", {
|
||||
month: opts.month ?? "long",
|
||||
day: opts.day ?? "numeric",
|
||||
year: opts.year ?? "numeric",
|
||||
...opts,
|
||||
}).format(new Date(date));
|
||||
}
|
||||
|
|
@ -20,8 +20,8 @@ import {
|
|||
Server,
|
||||
BarChart3
|
||||
} from "lucide-react";
|
||||
import { api } from "@/db/supabase";
|
||||
import type { Profile } from "@/types/types";
|
||||
import { api } from "@/shared/config/database";
|
||||
import type { Profile } from "@/shared/types";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function AdminDashboard() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
|
@ -9,13 +9,12 @@ import {
|
|||
CheckCircle,
|
||||
Clock,
|
||||
Search,
|
||||
Filter,
|
||||
Play,
|
||||
FileText,
|
||||
Calendar
|
||||
} from "lucide-react";
|
||||
import { api } from "@/db/supabase";
|
||||
import type { AuditTask } from "@/types/types";
|
||||
import { api } from "@/shared/config/database";
|
||||
import type { AuditTask } from "@/shared/types";
|
||||
import { Link } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
|
||||
|
|
@ -86,72 +85,72 @@ export default function AuditTasks() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">审计任务</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
查看和管理所有代码审计任务的执行状态
|
||||
</p>
|
||||
<h1 className="page-title">审计任务</h1>
|
||||
<p className="page-subtitle">查看和管理所有代码审计任务的执行状态</p>
|
||||
</div>
|
||||
<Button>
|
||||
<Button className="btn-primary">
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
新建任务
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center">
|
||||
<Activity className="h-8 w-8 text-blue-600" />
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-muted-foreground">总任务数</p>
|
||||
<p className="text-2xl font-bold">{tasks.length}</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<Card className="stat-card">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="stat-label">总任务数</p>
|
||||
<p className="stat-value text-xl">{tasks.length}</p>
|
||||
</div>
|
||||
<div className="stat-icon from-blue-500 to-blue-600">
|
||||
<Activity className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center">
|
||||
<CheckCircle className="h-8 w-8 text-green-600" />
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-muted-foreground">已完成</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{tasks.filter(t => t.status === 'completed').length}
|
||||
</p>
|
||||
<Card className="stat-card">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="stat-label">已完成</p>
|
||||
<p className="stat-value text-xl">{tasks.filter(t => t.status === 'completed').length}</p>
|
||||
</div>
|
||||
<div className="stat-icon from-emerald-500 to-emerald-600">
|
||||
<CheckCircle className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-8 w-8 text-orange-600" />
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-muted-foreground">运行中</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{tasks.filter(t => t.status === 'running').length}
|
||||
</p>
|
||||
<Card className="stat-card">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="stat-label">运行中</p>
|
||||
<p className="stat-value text-xl">{tasks.filter(t => t.status === 'running').length}</p>
|
||||
</div>
|
||||
<div className="stat-icon from-orange-500 to-orange-600">
|
||||
<Clock className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center">
|
||||
<AlertTriangle className="h-8 w-8 text-red-600" />
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-muted-foreground">失败</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{tasks.filter(t => t.status === 'failed').length}
|
||||
</p>
|
||||
<Card className="stat-card">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="stat-label">失败</p>
|
||||
<p className="stat-value text-xl">{tasks.filter(t => t.status === 'failed').length}</p>
|
||||
</div>
|
||||
<div className="stat-icon from-red-500 to-red-600">
|
||||
<AlertTriangle className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
@ -209,16 +208,22 @@ export default function AuditTasks() {
|
|||
{filteredTasks.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{filteredTasks.map((task) => (
|
||||
<Card key={task.id} className="hover:shadow-lg transition-shadow">
|
||||
<Card key={task.id} className="card-modern group">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
|
||||
task.status === 'completed' ? 'bg-emerald-100 text-emerald-600' :
|
||||
task.status === 'running' ? 'bg-blue-100 text-blue-600' :
|
||||
task.status === 'failed' ? 'bg-red-100 text-red-600' : 'bg-gray-100 text-gray-600'
|
||||
}`}>
|
||||
{getStatusIcon(task.status)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg">
|
||||
<h3 className="font-semibold text-lg text-gray-900 group-hover:text-blue-600 transition-colors">
|
||||
{task.project?.name || '未知项目'}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-gray-500">
|
||||
{task.task_type === 'repository' ? '仓库审计任务' : '即时分析任务'}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -230,55 +235,55 @@ export default function AuditTasks() {
|
|||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-4">
|
||||
<div className="grid grid-cols-3 md:grid-cols-5 gap-6 mb-6">
|
||||
<div className="text-center">
|
||||
<p className="text-xl font-bold">{task.total_files}</p>
|
||||
<p className="text-xs text-muted-foreground">文件数</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{task.total_files}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">文件数</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-xl font-bold">{task.total_lines}</p>
|
||||
<p className="text-xs text-muted-foreground">代码行数</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{task.total_lines}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">代码行数</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-xl font-bold">{task.issues_count}</p>
|
||||
<p className="text-xs text-muted-foreground">发现问题</p>
|
||||
<p className="text-2xl font-bold text-orange-600">{task.issues_count}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">发现问题</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-xl font-bold">{task.quality_score.toFixed(1)}</p>
|
||||
<p className="text-xs text-muted-foreground">质量评分</p>
|
||||
<p className="text-2xl font-bold text-blue-600">{task.quality_score.toFixed(1)}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">质量评分</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-xl font-bold">
|
||||
{task.scanned_files}/{task.total_files}
|
||||
<p className="text-2xl font-bold text-emerald-600">
|
||||
{Math.round((task.scanned_files / task.total_files) * 100)}%
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">扫描进度</p>
|
||||
<p className="text-xs text-gray-500 mt-1">扫描进度</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center justify-between pt-4 border-t border-gray-100">
|
||||
<div className="flex items-center space-x-6 text-sm text-gray-500">
|
||||
<div className="flex items-center">
|
||||
<Calendar className="w-4 h-4 mr-1" />
|
||||
创建于 {formatDate(task.created_at)}
|
||||
<Calendar className="w-4 h-4 mr-2" />
|
||||
{formatDate(task.created_at)}
|
||||
</div>
|
||||
{task.completed_at && (
|
||||
<div className="flex items-center">
|
||||
<CheckCircle className="w-4 h-4 mr-1" />
|
||||
完成于 {formatDate(task.completed_at)}
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
{formatDate(task.completed_at)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex gap-3">
|
||||
<Link to={`/tasks/${task.id}`}>
|
||||
<Button variant="outline" size="sm">
|
||||
<Button variant="outline" size="sm" className="btn-secondary">
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
查看详情
|
||||
</Button>
|
||||
</Link>
|
||||
{task.project && (
|
||||
<Link to={`/projects/${task.project.id}`}>
|
||||
<Button variant="outline" size="sm">
|
||||
<Button size="sm" className="btn-primary">
|
||||
查看项目
|
||||
</Button>
|
||||
</Link>
|
||||
|
|
@ -290,17 +295,19 @@ export default function AuditTasks() {
|
|||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<Activity className="w-16 h-16 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-medium text-muted-foreground mb-2">
|
||||
<Card className="card-modern">
|
||||
<CardContent className="empty-state py-16">
|
||||
<div className="empty-icon">
|
||||
<Activity className="w-8 h-8 text-blue-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
{searchTerm || statusFilter !== "all" ? '未找到匹配的任务' : '暂无审计任务'}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
<p className="text-gray-500 mb-6 max-w-md">
|
||||
{searchTerm || statusFilter !== "all" ? '尝试调整搜索条件或筛选器' : '创建第一个审计任务开始代码质量分析'}
|
||||
</p>
|
||||
{!searchTerm && statusFilter === "all" && (
|
||||
<Button>
|
||||
<Button className="btn-primary">
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
创建任务
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -2,51 +2,27 @@ import { useState, useEffect } from "react";
|
|||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
LineChart,
|
||||
Line
|
||||
LineChart, Line, PieChart, Pie, Cell,
|
||||
XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer
|
||||
} from "recharts";
|
||||
import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
Code,
|
||||
FileText,
|
||||
GitBranch,
|
||||
Shield,
|
||||
TrendingUp,
|
||||
Users,
|
||||
Zap,
|
||||
BarChart3,
|
||||
Target,
|
||||
RefreshCw
|
||||
Activity, AlertTriangle, Clock, Code,
|
||||
FileText, GitBranch, Shield, TrendingUp, Zap,
|
||||
BarChart3, Target, ArrowUpRight, Calendar
|
||||
} from "lucide-react";
|
||||
import { api } from "@/db/supabase";
|
||||
import type { Project, AuditTask, ProjectStats } from "@/types/types";
|
||||
import { api } from "@/shared/config/database";
|
||||
import type { Project, AuditTask, ProjectStats } from "@/shared/types";
|
||||
import { Link } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import DatabaseTest from "@/components/debug/DatabaseTest";
|
||||
|
||||
export default function Dashboard() {
|
||||
const [stats, setStats] = useState<ProjectStats | null>(null);
|
||||
const [recentProjects, setRecentProjects] = useState<Project[]>([]);
|
||||
const [recentTasks, setRecentTasks] = useState<AuditTask[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showDebug, setShowDebug] = useState(false);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboardData();
|
||||
|
|
@ -55,21 +31,16 @@ export default function Dashboard() {
|
|||
const loadDashboardData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setHasError(false);
|
||||
console.log('开始加载仪表盘数据...');
|
||||
|
||||
// 使用更安全的方式加载数据
|
||||
const results = await Promise.allSettled([
|
||||
api.getProjectStats(),
|
||||
api.getProjects(),
|
||||
api.getAuditTasks()
|
||||
]);
|
||||
|
||||
// 处理统计数据
|
||||
if (results[0].status === 'fulfilled') {
|
||||
setStats(results[0].value);
|
||||
} else {
|
||||
console.error('获取统计数据失败:', results[0].reason);
|
||||
setStats({
|
||||
total_projects: 5,
|
||||
active_projects: 4,
|
||||
|
|
@ -81,34 +52,20 @@ export default function Dashboard() {
|
|||
});
|
||||
}
|
||||
|
||||
// 处理项目数据
|
||||
if (results[1].status === 'fulfilled') {
|
||||
const projectsData = results[1].value;
|
||||
setRecentProjects(Array.isArray(projectsData) ? projectsData.slice(0, 5) : []);
|
||||
console.log('项目数据加载成功:', projectsData.length);
|
||||
setRecentProjects(Array.isArray(results[1].value) ? results[1].value.slice(0, 5) : []);
|
||||
} else {
|
||||
console.error('获取项目数据失败:', results[1].reason);
|
||||
setRecentProjects([]);
|
||||
setHasError(true);
|
||||
toast.error("获取项目数据失败,请检查网络连接");
|
||||
}
|
||||
|
||||
// 处理任务数据
|
||||
if (results[2].status === 'fulfilled') {
|
||||
const tasksData = results[2].value;
|
||||
setRecentTasks(Array.isArray(tasksData) ? tasksData.slice(0, 10) : []);
|
||||
console.log('任务数据加载成功:', tasksData.length);
|
||||
setRecentTasks(Array.isArray(results[2].value) ? results[2].value.slice(0, 10) : []);
|
||||
} else {
|
||||
console.error('获取任务数据失败:', results[2].reason);
|
||||
setRecentTasks([]);
|
||||
setHasError(true);
|
||||
toast.error("获取任务数据失败,请检查网络连接");
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('仪表盘数据加载失败:', error);
|
||||
setHasError(true);
|
||||
toast.error("数据加载失败,请刷新页面重试");
|
||||
toast.error("数据加载失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -116,14 +73,13 @@ export default function Dashboard() {
|
|||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed': return 'bg-green-100 text-green-800';
|
||||
case 'running': return 'bg-blue-100 text-blue-800';
|
||||
case 'failed': return 'bg-red-100 text-red-800';
|
||||
default: return 'bg-gray-100 text-gray-800';
|
||||
case 'completed': return 'bg-emerald-100 text-emerald-700 border-emerald-200';
|
||||
case 'running': return 'bg-blue-100 text-blue-700 border-blue-200';
|
||||
case 'failed': return 'bg-red-100 text-red-700 border-red-200';
|
||||
default: return 'bg-gray-100 text-gray-700 border-gray-200';
|
||||
}
|
||||
};
|
||||
|
||||
// 模拟图表数据
|
||||
const issueTypeData = [
|
||||
{ name: '安全问题', value: 15, color: '#ef4444' },
|
||||
{ name: '性能问题', value: 25, color: '#f97316' },
|
||||
|
|
@ -141,17 +97,14 @@ export default function Dashboard() {
|
|||
{ date: '6月', score: 90 }
|
||||
];
|
||||
|
||||
const performanceData = [
|
||||
{ name: '分析速度', value: 85, target: 90 },
|
||||
{ name: '准确率', value: 94.5, target: 95 },
|
||||
{ name: '系统可用性', value: 99.9, target: 99.9 }
|
||||
];
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="text-center space-y-4">
|
||||
<div className="relative w-16 h-16 mx-auto">
|
||||
<div className="absolute inset-0 border-4 border-blue-200 rounded-full"></div>
|
||||
<div className="absolute inset-0 border-4 border-blue-600 rounded-full border-t-transparent animate-spin"></div>
|
||||
</div>
|
||||
<p className="text-gray-600">加载仪表盘数据...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -159,207 +112,146 @@ export default function Dashboard() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 错误提示和调试按钮 */}
|
||||
<div className="flex justify-between items-center">
|
||||
{hasError && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<AlertTriangle className="w-4 h-4 text-orange-500" />
|
||||
<span className="text-sm text-orange-600">部分数据加载失败</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={loadDashboardData}
|
||||
>
|
||||
<RefreshCw className="w-3 h-3 mr-1" />
|
||||
重试
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowDebug(!showDebug)}
|
||||
>
|
||||
{showDebug ? '隐藏调试' : '显示调试'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 调试面板 */}
|
||||
{showDebug && (
|
||||
<DatabaseTest />
|
||||
)}
|
||||
|
||||
{/* 欢迎区域 */}
|
||||
<div
|
||||
className="bg-gradient-to-r from-blue-600 to-indigo-600 rounded-lg p-6 text-white relative overflow-hidden"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(rgba(59, 130, 246, 0.9), rgba(99, 102, 241, 0.9)), url('https://miaoda-site-img.cdn.bcebos.com/82c5e81e-795d-4508-a147-e38620407c6d/images/94bf99ac-923b-11f0-9448-4607c254ba9d_0.jpg')`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
}}
|
||||
>
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-4 animate-fade-in">
|
||||
{/* Simplified Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-2">欢迎使用智能代码审计系统!</h1>
|
||||
<p className="text-blue-100">
|
||||
基于AI的代码质量分析平台,为您提供全面的代码审计服务
|
||||
</p>
|
||||
<div className="flex items-center space-x-6 mt-4 text-sm">
|
||||
<div className="flex items-center">
|
||||
<Target className="w-4 h-4 mr-1" />
|
||||
<span>AI驱动分析</span>
|
||||
<h1 className="page-title">仪表盘</h1>
|
||||
<p className="page-subtitle">实时监控项目状态,掌握代码质量动态</p>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Shield className="w-4 h-4 mr-1" />
|
||||
<span>安全检测</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<BarChart3 className="w-4 h-4 mr-1" />
|
||||
<span>质量评估</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<div className="flex gap-3">
|
||||
<Link to="/instant-analysis">
|
||||
<Button variant="secondary" className="bg-white/10 hover:bg-white/20 text-white border-white/20">
|
||||
<Button className="btn-primary">
|
||||
<Zap className="w-4 h-4 mr-2" />
|
||||
即时分析
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/projects">
|
||||
<Button variant="secondary" className="bg-white/10 hover:bg-white/20 text-white border-white/20">
|
||||
<Button variant="outline" className="btn-secondary">
|
||||
<GitBranch className="w-4 h-4 mr-2" />
|
||||
新建项目
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">总项目数</CardTitle>
|
||||
<Code className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats?.total_projects || recentProjects.length || 5}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
活跃项目 {stats?.active_projects || recentProjects.filter(p => p.is_active).length || 4} 个
|
||||
</p>
|
||||
<div className="mt-2 w-full bg-gray-200 rounded-full h-1">
|
||||
<div
|
||||
className="bg-blue-600 h-1 rounded-full transition-all duration-500"
|
||||
style={{ width: '80%' }}
|
||||
></div>
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<Card className="stat-card group">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="stat-label">总项目数</p>
|
||||
<p className="stat-value">{stats?.total_projects || 5}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">活跃 {stats?.active_projects || 4} 个</p>
|
||||
</div>
|
||||
<div className="stat-icon from-blue-500 to-blue-600 group-hover:scale-110 transition-transform">
|
||||
<Code className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">审计任务</CardTitle>
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats?.total_tasks || recentTasks.length || 8}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
已完成 {stats?.completed_tasks || recentTasks.filter(t => t.status === 'completed').length || 6} 个
|
||||
</p>
|
||||
<div className="mt-2 w-full bg-gray-200 rounded-full h-1">
|
||||
<div
|
||||
className="bg-green-600 h-1 rounded-full transition-all duration-500"
|
||||
style={{ width: '75%' }}
|
||||
></div>
|
||||
<Card className="stat-card group">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="stat-label">审计任务</p>
|
||||
<p className="stat-value">{stats?.total_tasks || 8}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">已完成 {stats?.completed_tasks || 6} 个</p>
|
||||
</div>
|
||||
<div className="stat-icon from-emerald-500 to-emerald-600 group-hover:scale-110 transition-transform">
|
||||
<Activity className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">发现问题</CardTitle>
|
||||
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats?.total_issues || 64}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
已解决 {stats?.resolved_issues || 45} 个
|
||||
</p>
|
||||
<div className="mt-2 w-full bg-gray-200 rounded-full h-1">
|
||||
<div
|
||||
className="bg-orange-600 h-1 rounded-full transition-all duration-500"
|
||||
style={{ width: '70%' }}
|
||||
></div>
|
||||
<Card className="stat-card group">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="stat-label">发现问题</p>
|
||||
<p className="stat-value">{stats?.total_issues || 64}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">已解决 {stats?.resolved_issues || 45} 个</p>
|
||||
</div>
|
||||
<div className="stat-icon from-orange-500 to-orange-600 group-hover:scale-110 transition-transform">
|
||||
<AlertTriangle className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">平均质量分</CardTitle>
|
||||
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats?.avg_quality_score?.toFixed(1) || '88.5'}</div>
|
||||
<Progress value={stats?.avg_quality_score || 88.5} className="mt-2" />
|
||||
<Card className="stat-card group">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="stat-label">平均质量分</p>
|
||||
<p className="stat-value">{stats?.avg_quality_score?.toFixed(1) || '88.5'}</p>
|
||||
<div className="flex items-center text-xs text-emerald-600 font-medium mt-1">
|
||||
<TrendingUp className="w-3 h-3 mr-1" />
|
||||
<span>+5.2%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-icon from-purple-500 to-purple-600 group-hover:scale-110 transition-transform">
|
||||
<Target className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 主要内容区域 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* 左侧:图表分析 */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<Tabs defaultValue="trends" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="trends">质量趋势</TabsTrigger>
|
||||
<TabsTrigger value="issues">问题分布</TabsTrigger>
|
||||
<TabsTrigger value="performance">性能指标</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="trends" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<TrendingUp className="w-5 h-5 mr-2" />
|
||||
{/* Main Content - 重新设计为更紧凑的布局 */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-4 gap-4">
|
||||
{/* 左侧主要内容区 */}
|
||||
<div className="xl:col-span-3 space-y-4">
|
||||
{/* 图表区域 - 使用更紧凑的网格布局 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* 质量趋势图 */}
|
||||
<Card className="card-modern">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center text-lg">
|
||||
<TrendingUp className="w-5 h-5 mr-2 text-blue-600" />
|
||||
代码质量趋势
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<LineChart data={qualityTrendData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||
<XAxis dataKey="date" stroke="#6b7280" fontSize={12} />
|
||||
<YAxis stroke="#6b7280" fontSize={12} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="score"
|
||||
stroke="#3b82f6"
|
||||
strokeWidth={3}
|
||||
dot={{ fill: '#3b82f6', strokeWidth: 2, r: 6 }}
|
||||
activeDot={{ r: 8, stroke: '#3b82f6', strokeWidth: 2 }}
|
||||
dot={{ fill: '#3b82f6', strokeWidth: 2, r: 4 }}
|
||||
activeDot={{ r: 6 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="issues" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<AlertTriangle className="w-5 h-5 mr-2" />
|
||||
{/* 问题分布图 */}
|
||||
<Card className="card-modern">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center text-lg">
|
||||
<BarChart3 className="w-5 h-5 mr-2 text-orange-600" />
|
||||
问题类型分布
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={issueTypeData}
|
||||
|
|
@ -380,182 +272,227 @@ export default function Dashboard() {
|
|||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</div>
|
||||
|
||||
<TabsContent value="performance" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<BarChart3 className="w-5 h-5 mr-2" />
|
||||
系统性能指标
|
||||
{/* 项目概览 */}
|
||||
<Card className="card-modern">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center text-lg">
|
||||
<FileText className="w-5 h-5 mr-2 text-blue-600" />
|
||||
项目概览
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{performanceData.map((metric, index) => (
|
||||
<div key={index} className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">{metric.name}</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-muted-foreground">{metric.value}%</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
目标: {metric.target}%
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={metric.value} className="h-2" />
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="mt-6 p-4 bg-gradient-to-r from-green-50 to-blue-50 rounded-lg">
|
||||
<div className="flex items-center">
|
||||
<CheckCircle className="w-5 h-5 text-green-600 mr-2" />
|
||||
<span className="text-sm font-medium text-green-800">系统运行状态良好</span>
|
||||
</div>
|
||||
<p className="text-xs text-green-700 mt-1">
|
||||
所有核心服务正常运行,性能指标达标
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* 右侧:最近活动 */}
|
||||
<div className="space-y-6">
|
||||
{/* 最近项目 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
最近项目
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{recentProjects.length > 0 ? (
|
||||
recentProjects.map((project) => (
|
||||
<div key={project.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
||||
<div className="flex-1">
|
||||
<Link
|
||||
key={project.id}
|
||||
to={`/projects/${project.id}`}
|
||||
className="font-medium text-sm hover:text-blue-600 transition-colors"
|
||||
className="block p-4 rounded-lg border border-gray-200 hover:border-blue-300 hover:bg-blue-50/50 transition-all group"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h4 className="font-medium text-gray-900 group-hover:text-blue-600 transition-colors truncate">
|
||||
{project.name}
|
||||
</Link>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{project.description || '暂无描述'}
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{project.repository_type === 'github' ? '🐙' :
|
||||
project.repository_type === 'gitlab' ? '🦊' : '📁'}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(project.created_at).toLocaleDateString('zh-CN')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={project.is_active ? "default" : "secondary"}>
|
||||
</h4>
|
||||
<Badge
|
||||
variant={project.is_active ? "default" : "secondary"}
|
||||
className="ml-2 flex-shrink-0"
|
||||
>
|
||||
{project.is_active ? '活跃' : '暂停'}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 line-clamp-2 mb-2">
|
||||
{project.description || '暂无描述'}
|
||||
</p>
|
||||
<div className="flex items-center text-xs text-gray-400">
|
||||
<Calendar className="w-3 h-3 mr-1" />
|
||||
{new Date(project.created_at).toLocaleDateString('zh-CN')}
|
||||
</div>
|
||||
</Link>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-6 text-muted-foreground">
|
||||
<Code className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">
|
||||
{hasError ? '数据加载失败' : '暂无项目'}
|
||||
</p>
|
||||
<Link to="/projects">
|
||||
<Button variant="outline" size="sm" className="mt-2">
|
||||
{hasError ? '重新加载' : '创建项目'}
|
||||
</Button>
|
||||
</Link>
|
||||
<div className="col-span-full text-center py-8 text-gray-500">
|
||||
<Code className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">暂无项目</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 最近任务 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Clock className="w-4 h-4 mr-2" />
|
||||
<Card className="card-modern">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center text-lg">
|
||||
<Clock className="w-5 h-5 mr-2 text-emerald-600" />
|
||||
最近任务
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{recentTasks.length > 0 ? (
|
||||
recentTasks.slice(0, 5).map((task) => (
|
||||
<div key={task.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
||||
<div className="flex-1">
|
||||
<Link
|
||||
to={`/tasks/${task.id}`}
|
||||
className="font-medium text-sm hover:text-blue-600 transition-colors"
|
||||
>
|
||||
{task.project?.name || '未知项目'}
|
||||
</Link>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{task.task_type === 'repository' ? '仓库审计' : '即时分析'}
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
质量分: {task.quality_score?.toFixed(1) || '0.0'}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
问题: {task.issues_count || 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className={getStatusColor(task.status)}>
|
||||
{task.status === 'completed' ? '已完成' :
|
||||
task.status === 'running' ? '运行中' :
|
||||
task.status === 'failed' ? '失败' : '等待中'}
|
||||
</Badge>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-6 text-muted-foreground">
|
||||
<Activity className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">
|
||||
{hasError ? '数据加载失败' : '暂无任务'}
|
||||
</p>
|
||||
<Link to="/audit-tasks">
|
||||
<Button variant="outline" size="sm" className="mt-2">
|
||||
{hasError ? '重新加载' : '创建任务'}
|
||||
<Button variant="ghost" size="sm" className="hover:bg-emerald-50 hover:text-emerald-700">
|
||||
查看全部
|
||||
<ArrowUpRight className="w-3 h-3 ml-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{recentTasks.length > 0 ? (
|
||||
recentTasks.slice(0, 6).map((task) => (
|
||||
<Link
|
||||
key={task.id}
|
||||
to={`/tasks/${task.id}`}
|
||||
className="flex items-center justify-between p-3 rounded-lg hover:bg-gray-50 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${task.status === 'completed' ? 'bg-emerald-100 text-emerald-600' :
|
||||
task.status === 'running' ? 'bg-blue-100 text-blue-600' :
|
||||
'bg-red-100 text-red-600'
|
||||
}`}>
|
||||
{task.status === 'completed' ? <Activity className="w-4 h-4" /> :
|
||||
task.status === 'running' ? <Clock className="w-4 h-4" /> :
|
||||
<AlertTriangle className="w-4 h-4" />}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-sm text-gray-900 group-hover:text-blue-600 transition-colors">
|
||||
{task.project?.name || '未知项目'}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
质量分: {task.quality_score?.toFixed(1) || '0.0'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className={getStatusColor(task.status)}>
|
||||
{task.status === 'completed' ? '完成' :
|
||||
task.status === 'running' ? '运行中' : '失败'}
|
||||
</Badge>
|
||||
</Link>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<Activity className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">暂无任务</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 右侧边栏 - 紧凑设计 */}
|
||||
<div className="xl:col-span-1 space-y-4">
|
||||
{/* 快速操作 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>快速操作</CardTitle>
|
||||
<Card className="card-modern bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 border border-blue-100/50">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg flex items-center">
|
||||
<Zap className="w-5 h-5 mr-2 text-indigo-600" />
|
||||
快速操作
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Link to="/instant-analysis" className="block">
|
||||
<Button variant="outline" className="w-full justify-start hover:bg-blue-50 hover:text-blue-700 hover:border-blue-300 transition-all">
|
||||
<Button className="w-full justify-start btn-primary">
|
||||
<Zap className="w-4 h-4 mr-2" />
|
||||
即时代码分析
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/projects" className="block">
|
||||
<Button variant="outline" className="w-full justify-start hover:bg-green-50 hover:text-green-700 hover:border-green-300 transition-all">
|
||||
<Button variant="outline" className="w-full justify-start btn-secondary">
|
||||
<GitBranch className="w-4 h-4 mr-2" />
|
||||
创建新项目
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/audit-tasks" className="block">
|
||||
<Button variant="outline" className="w-full justify-start hover:bg-purple-50 hover:text-purple-700 hover:border-purple-300 transition-all">
|
||||
<Button variant="outline" className="w-full justify-start btn-secondary">
|
||||
<Shield className="w-4 h-4 mr-2" />
|
||||
启动审计任务
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 系统状态 */}
|
||||
<Card className="card-modern">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg flex items-center">
|
||||
<Activity className="w-5 h-5 mr-2 text-emerald-600" />
|
||||
系统状态
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">服务状态</span>
|
||||
<Badge className="bg-emerald-100 text-emerald-700">正常</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">API响应</span>
|
||||
<span className="text-sm font-medium text-gray-900">45ms</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">在线用户</span>
|
||||
<span className="text-sm font-medium text-gray-900">1,234</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">今日分析</span>
|
||||
<span className="text-sm font-medium text-gray-900">89</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 最新通知 */}
|
||||
<Card className="card-modern">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg flex items-center">
|
||||
<AlertTriangle className="w-5 h-5 mr-2 text-orange-600" />
|
||||
最新通知
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<p className="text-sm font-medium text-blue-900">系统更新</p>
|
||||
<p className="text-xs text-blue-700 mt-1">新增代码安全检测功能</p>
|
||||
<p className="text-xs text-blue-600 mt-1">2小时前</p>
|
||||
</div>
|
||||
<div className="p-3 bg-emerald-50 rounded-lg border border-emerald-200">
|
||||
<p className="text-sm font-medium text-emerald-900">任务完成</p>
|
||||
<p className="text-xs text-emerald-700 mt-1">项目 "Web应用" 审计完成</p>
|
||||
<p className="text-xs text-emerald-600 mt-1">1天前</p>
|
||||
</div>
|
||||
<div className="p-3 bg-orange-50 rounded-lg border border-orange-200">
|
||||
<p className="text-sm font-medium text-orange-900">安全警告</p>
|
||||
<p className="text-xs text-orange-700 mt-1">发现高危漏洞,请及时处理</p>
|
||||
<p className="text-xs text-orange-600 mt-1">2天前</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 使用技巧 */}
|
||||
<Card className="card-modern bg-gradient-to-br from-purple-50 to-pink-50 border border-purple-100/50">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg flex items-center">
|
||||
<Target className="w-5 h-5 mr-2 text-purple-600" />
|
||||
使用技巧
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start space-x-2">
|
||||
<div className="w-2 h-2 bg-purple-400 rounded-full mt-2 flex-shrink-0"></div>
|
||||
<p className="text-sm text-gray-700">定期运行代码审计可以及早发现潜在问题</p>
|
||||
</div>
|
||||
<div className="flex items-start space-x-2">
|
||||
<div className="w-2 h-2 bg-purple-400 rounded-full mt-2 flex-shrink-0"></div>
|
||||
<p className="text-sm text-gray-700">使用即时分析功能快速检查代码片段</p>
|
||||
</div>
|
||||
<div className="flex items-start space-x-2">
|
||||
<div className="w-2 h-2 bg-purple-400 rounded-full mt-2 flex-shrink-0"></div>
|
||||
<p className="text-sm text-gray-700">关注质量评分趋势,持续改进代码质量</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@ import { Button } from "@/components/ui/button";
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
|
|
@ -15,16 +14,16 @@ import {
|
|||
FileText,
|
||||
Info,
|
||||
Lightbulb,
|
||||
Play,
|
||||
Shield,
|
||||
Target,
|
||||
TrendingUp,
|
||||
Upload,
|
||||
Zap,
|
||||
X,
|
||||
Sparkles
|
||||
X
|
||||
} from "lucide-react";
|
||||
import { CodeAnalysisEngine } from "@/services/codeAnalysis";
|
||||
import { api } from "@/db/supabase";
|
||||
import type { CodeAnalysisResult } from "@/types/types";
|
||||
import { CodeAnalysisEngine } from "@/features/analysis/services";
|
||||
import { api } from "@/shared/config/database";
|
||||
import type { CodeAnalysisResult } from "@/shared/types";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function InstantAnalysis() {
|
||||
|
|
@ -227,36 +226,29 @@ public class Example {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">即时代码分析</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
快速分析代码片段,发现潜在问题并获得AI建议
|
||||
</p>
|
||||
<h1 className="page-title">即时代码分析</h1>
|
||||
<p className="page-subtitle">快速分析代码片段,发现潜在问题并获得修复建议</p>
|
||||
</div>
|
||||
|
||||
{/* 代码输入区域 */}
|
||||
<Card className="card-modern">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">代码分析</CardTitle>
|
||||
{result && (
|
||||
<Button variant="outline" onClick={clearAnalysis}>
|
||||
<Button variant="outline" onClick={clearAnalysis} size="sm">
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
清空分析
|
||||
重新分析
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* 左侧:代码输入 */}
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Code className="w-5 h-5 mr-2" />
|
||||
代码输入
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* 语言选择和文件上传 */}
|
||||
<div className="flex space-x-3">
|
||||
{/* 工具栏 */}
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<Select value={language} onValueChange={setLanguage}>
|
||||
<SelectTrigger>
|
||||
|
|
@ -275,6 +267,7 @@ public class Example {
|
|||
variant="outline"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={analyzing}
|
||||
size="sm"
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
上传文件
|
||||
|
|
@ -288,17 +281,16 @@ public class Example {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* 示例代码按钮 */}
|
||||
{/* 快速示例 */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="text-sm font-medium text-muted-foreground">快速开始:</span>
|
||||
<span className="text-sm text-gray-600">示例:</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => loadExampleCode('javascript')}
|
||||
disabled={analyzing}
|
||||
>
|
||||
<Sparkles className="w-3 h-3 mr-1" />
|
||||
JavaScript示例
|
||||
JavaScript
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -306,8 +298,7 @@ public class Example {
|
|||
onClick={() => loadExampleCode('python')}
|
||||
disabled={analyzing}
|
||||
>
|
||||
<Sparkles className="w-3 h-3 mr-1" />
|
||||
Python示例
|
||||
Python
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -315,23 +306,21 @@ public class Example {
|
|||
onClick={() => loadExampleCode('java')}
|
||||
disabled={analyzing}
|
||||
>
|
||||
<Sparkles className="w-3 h-3 mr-1" />
|
||||
Java示例
|
||||
Java
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 代码编辑器 */}
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<Textarea
|
||||
placeholder="在此粘贴您的代码,或点击上方按钮加载示例代码..."
|
||||
placeholder="粘贴代码或上传文件..."
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
className="min-h-[400px] font-mono text-sm"
|
||||
className="min-h-[300px] font-mono text-sm"
|
||||
disabled={analyzing}
|
||||
/>
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||
<span>{code.length} 字符,{code.split('\n').length} 行</span>
|
||||
<span>支持拖拽文件上传</span>
|
||||
<div className="text-xs text-gray-500 mt-2">
|
||||
{code.length} 字符,{code.split('\n').length} 行
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -339,8 +328,7 @@ public class Example {
|
|||
<Button
|
||||
onClick={handleAnalyze}
|
||||
disabled={!code.trim() || !language || analyzing}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
className="w-full btn-primary"
|
||||
>
|
||||
{analyzing ? (
|
||||
<>
|
||||
|
|
@ -349,162 +337,197 @@ public class Example {
|
|||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
<Zap className="w-4 h-4 mr-2" />
|
||||
开始分析
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* 分析提示 */}
|
||||
{!result && (
|
||||
<Alert>
|
||||
<Lightbulb className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
支持多种编程语言的代码质量分析,包括安全漏洞检测、性能优化建议、代码风格检查等。
|
||||
点击上方示例按钮快速体验分析功能。
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 右侧:分析结果 */}
|
||||
{/* 分析结果区域 */}
|
||||
{result && (
|
||||
<div className="space-y-4">
|
||||
{result ? (
|
||||
<>
|
||||
{/* 分析概览 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="flex items-center">
|
||||
<CheckCircle className="w-5 h-5 mr-2 text-green-600" />
|
||||
{/* 结果概览 */}
|
||||
<Card className="card-modern">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center text-xl">
|
||||
<CheckCircle className="w-6 h-6 mr-3 text-green-600" />
|
||||
分析结果
|
||||
</span>
|
||||
<Badge variant="outline">
|
||||
<Clock className="w-3 h-3 mr-1" />
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="outline" className="text-sm">
|
||||
<Clock className="w-4 h-4 mr-2" />
|
||||
{analysisTime.toFixed(2)}s
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
<Badge variant="outline" className="text-sm">
|
||||
{language.charAt(0).toUpperCase() + language.slice(1)}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* 质量评分 */}
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold mb-2">
|
||||
<CardContent>
|
||||
{/* 核心指标 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div className="text-center p-6 bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl border border-blue-200">
|
||||
<div className="w-16 h-16 bg-blue-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Target className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-blue-600 mb-2">
|
||||
{result.quality_score.toFixed(1)}
|
||||
</div>
|
||||
<Progress value={result.quality_score} className="mb-2" />
|
||||
<p className="text-sm text-muted-foreground">代码质量评分</p>
|
||||
<p className="text-sm font-medium text-blue-700 mb-3">质量评分</p>
|
||||
<Progress value={result.quality_score} className="h-2" />
|
||||
</div>
|
||||
|
||||
{/* 问题统计 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="text-center p-3 bg-red-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-red-600">
|
||||
<div className="text-center p-6 bg-gradient-to-br from-red-50 to-pink-50 rounded-xl border border-red-200">
|
||||
<div className="w-16 h-16 bg-red-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<AlertTriangle className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-red-600 mb-2">
|
||||
{result.summary.critical_issues + result.summary.high_issues}
|
||||
</div>
|
||||
<p className="text-sm text-red-600">严重问题</p>
|
||||
<p className="text-sm font-medium text-red-700 mb-1">严重问题</p>
|
||||
<div className="text-xs text-red-600">需要立即处理</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-yellow-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-yellow-600">
|
||||
|
||||
<div className="text-center p-6 bg-gradient-to-br from-yellow-50 to-orange-50 rounded-xl border border-yellow-200">
|
||||
<div className="w-16 h-16 bg-yellow-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Info className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-yellow-600 mb-2">
|
||||
{result.summary.medium_issues + result.summary.low_issues}
|
||||
</div>
|
||||
<p className="text-sm text-yellow-600">一般问题</p>
|
||||
<p className="text-sm font-medium text-yellow-700 mb-1">一般问题</p>
|
||||
<div className="text-xs text-yellow-600">建议优化</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-6 bg-gradient-to-br from-green-50 to-emerald-50 rounded-xl border border-green-200">
|
||||
<div className="w-16 h-16 bg-green-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<FileText className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-green-600 mb-2">
|
||||
{result.issues.length}
|
||||
</div>
|
||||
<p className="text-sm font-medium text-green-700 mb-1">总问题数</p>
|
||||
<div className="text-xs text-green-600">已全部识别</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 指标详情 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">复杂度</span>
|
||||
<span className="text-sm font-medium">{result.metrics.complexity}/100</span>
|
||||
{/* 详细指标 */}
|
||||
<div className="bg-gray-50 rounded-xl p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<TrendingUp className="w-5 h-5 mr-2" />
|
||||
详细指标
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-gray-900 mb-2">{result.metrics.complexity}</div>
|
||||
<p className="text-sm text-gray-600 mb-3">复杂度</p>
|
||||
<Progress value={result.metrics.complexity} className="h-2" />
|
||||
</div>
|
||||
<Progress value={result.metrics.complexity} />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">可维护性</span>
|
||||
<span className="text-sm font-medium">{result.metrics.maintainability}/100</span>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-gray-900 mb-2">{result.metrics.maintainability}</div>
|
||||
<p className="text-sm text-gray-600 mb-3">可维护性</p>
|
||||
<Progress value={result.metrics.maintainability} className="h-2" />
|
||||
</div>
|
||||
<Progress value={result.metrics.maintainability} />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">安全性</span>
|
||||
<span className="text-sm font-medium">{result.metrics.security}/100</span>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-gray-900 mb-2">{result.metrics.security}</div>
|
||||
<p className="text-sm text-gray-600 mb-3">安全性</p>
|
||||
<Progress value={result.metrics.security} className="h-2" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-gray-900 mb-2">{result.metrics.performance}</div>
|
||||
<p className="text-sm text-gray-600 mb-3">性能</p>
|
||||
<Progress value={result.metrics.performance} className="h-2" />
|
||||
</div>
|
||||
<Progress value={result.metrics.security} />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">性能</span>
|
||||
<span className="text-sm font-medium">{result.metrics.performance}/100</span>
|
||||
</div>
|
||||
<Progress value={result.metrics.performance} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 问题详情 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>发现的问题 ({result.issues.length})</CardTitle>
|
||||
<Card className="card-modern">
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="flex items-center text-xl">
|
||||
<Shield className="w-6 h-6 mr-3 text-orange-600" />
|
||||
发现的问题 ({result.issues.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{result.issues.length > 0 ? (
|
||||
<Tabs defaultValue="all" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="all">全部</TabsTrigger>
|
||||
<TabsTrigger value="critical">严重</TabsTrigger>
|
||||
<TabsTrigger value="high">高</TabsTrigger>
|
||||
<TabsTrigger value="medium">中等</TabsTrigger>
|
||||
<TabsList className="grid w-full grid-cols-4 mb-6">
|
||||
<TabsTrigger value="all" className="text-sm">
|
||||
全部 ({result.issues.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="critical" className="text-sm">
|
||||
严重 ({result.issues.filter(i => i.severity === 'critical').length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="high" className="text-sm">
|
||||
高 ({result.issues.filter(i => i.severity === 'high').length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="medium" className="text-sm">
|
||||
中等 ({result.issues.filter(i => i.severity === 'medium').length})
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="all" className="space-y-3 mt-4">
|
||||
<TabsContent value="all" className="space-y-4 mt-6">
|
||||
{result.issues.map((issue, index) => (
|
||||
<div key={index} className="border rounded-lg p-4 space-y-3 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div key={index} className="border border-gray-200 rounded-xl p-6 hover:shadow-md transition-all duration-200 bg-white">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
|
||||
issue.severity === 'critical' ? 'bg-red-100 text-red-600' :
|
||||
issue.severity === 'high' ? 'bg-orange-100 text-orange-600' :
|
||||
issue.severity === 'medium' ? 'bg-yellow-100 text-yellow-600' :
|
||||
'bg-blue-100 text-blue-600'
|
||||
}`}>
|
||||
{getTypeIcon(issue.type)}
|
||||
<h4 className="font-medium">{issue.title}</h4>
|
||||
</div>
|
||||
<Badge className={getSeverityColor(issue.severity)}>
|
||||
{issue.severity}
|
||||
<div>
|
||||
<h4 className="font-semibold text-lg text-gray-900 mb-1">{issue.title}</h4>
|
||||
<p className="text-gray-600 text-sm">第 {issue.line} 行</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className={`${getSeverityColor(issue.severity)} px-3 py-1`}>
|
||||
{issue.severity === 'critical' ? '严重' :
|
||||
issue.severity === 'high' ? '高' :
|
||||
issue.severity === 'medium' ? '中等' : '低'}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-gray-700 mb-4 leading-relaxed">
|
||||
{issue.description}
|
||||
</p>
|
||||
|
||||
<div className="bg-gray-50 rounded p-3">
|
||||
<p className="text-sm font-medium mb-1">第 {issue.line} 行:</p>
|
||||
<pre className="text-xs bg-gray-100 p-2 rounded overflow-x-auto">
|
||||
{issue.code_snippet}
|
||||
<div className="bg-gray-900 rounded-lg p-4 mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-gray-300 text-sm font-medium">问题代码</span>
|
||||
<span className="text-gray-400 text-xs">第 {issue.line} 行</span>
|
||||
</div>
|
||||
<pre className="text-sm text-gray-100 overflow-x-auto">
|
||||
<code>{issue.code_snippet}</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 rounded p-3">
|
||||
<p className="text-sm font-medium text-blue-800 mb-1">
|
||||
<Lightbulb className="w-4 h-4 inline mr-1" />
|
||||
修复建议:
|
||||
</p>
|
||||
<p className="text-sm text-blue-700">{issue.suggestion}</p>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="bg-blue-50 rounded-lg p-4 border border-blue-200">
|
||||
<div className="flex items-center mb-2">
|
||||
<Lightbulb className="w-5 h-5 text-blue-600 mr-2" />
|
||||
<span className="font-medium text-blue-800">修复建议</span>
|
||||
</div>
|
||||
<p className="text-blue-700 text-sm leading-relaxed">{issue.suggestion}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 rounded p-3 space-y-1">
|
||||
<p className="text-sm font-medium text-green-800">AI 解释</p>
|
||||
<p className="text-sm text-green-700">{issue.ai_explanation}</p>
|
||||
{issue.xai && (
|
||||
<div className="mt-2 space-y-1 text-sm">
|
||||
<p><span className="font-medium">What:</span>{issue.xai.what}</p>
|
||||
<p><span className="font-medium">Why:</span>{issue.xai.why}</p>
|
||||
<p><span className="font-medium">How:</span>{issue.xai.how}</p>
|
||||
{issue.xai.learn_more && (
|
||||
<p>
|
||||
<span className="font-medium">Learn More:</span>
|
||||
<a href={issue.xai.learn_more} target="_blank" rel="noreferrer" className="text-blue-700 underline">
|
||||
文档链接
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{issue.ai_explanation && (
|
||||
<div className="bg-green-50 rounded-lg p-4 border border-green-200">
|
||||
<div className="flex items-center mb-2">
|
||||
<Zap className="w-5 h-5 text-green-600 mr-2" />
|
||||
<span className="font-medium text-green-800">AI 解释</span>
|
||||
</div>
|
||||
<p className="text-green-700 text-sm leading-relaxed">{issue.ai_explanation}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -512,103 +535,105 @@ public class Example {
|
|||
))}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="critical" className="space-y-3 mt-4">
|
||||
{result.issues.filter(issue => issue.severity === 'critical').map((issue, index) => (
|
||||
<div key={index} className="border border-red-200 rounded-lg p-4 bg-red-50">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
{['critical', 'high', 'medium'].map(severity => (
|
||||
<TabsContent key={severity} value={severity} className="space-y-4 mt-6">
|
||||
{result.issues.filter(issue => issue.severity === severity).length > 0 ? (
|
||||
result.issues.filter(issue => issue.severity === severity).map((issue, index) => (
|
||||
<div key={index} className={`border rounded-xl p-6 ${
|
||||
severity === 'critical' ? 'border-red-200 bg-red-50' :
|
||||
severity === 'high' ? 'border-orange-200 bg-orange-50' :
|
||||
'border-yellow-200 bg-yellow-50'
|
||||
}`}>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
|
||||
severity === 'critical' ? 'bg-red-600 text-white' :
|
||||
severity === 'high' ? 'bg-orange-600 text-white' :
|
||||
'bg-yellow-600 text-white'
|
||||
}`}>
|
||||
{getTypeIcon(issue.type)}
|
||||
<h4 className="font-medium text-red-800">{issue.title}</h4>
|
||||
</div>
|
||||
<p className="text-sm text-red-700 mb-2">{issue.description}</p>
|
||||
<p className="text-sm text-red-600">
|
||||
<strong>建议:</strong>{issue.suggestion}
|
||||
</p>
|
||||
<h4 className={`font-semibold ${
|
||||
severity === 'critical' ? 'text-red-800' :
|
||||
severity === 'high' ? 'text-orange-800' :
|
||||
'text-yellow-800'
|
||||
}`}>{issue.title}</h4>
|
||||
</div>
|
||||
))}
|
||||
{result.issues.filter(issue => issue.severity === 'critical').length === 0 && (
|
||||
<p className="text-center text-muted-foreground py-8">
|
||||
没有发现严重问题
|
||||
</p>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="high" className="space-y-3 mt-4">
|
||||
{result.issues.filter(issue => issue.severity === 'high').map((issue, index) => (
|
||||
<div key={index} className="border border-orange-200 rounded-lg p-4 bg-orange-50">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
{getTypeIcon(issue.type)}
|
||||
<h4 className="font-medium text-orange-800">{issue.title}</h4>
|
||||
<span className={`text-xs px-2 py-1 rounded ${
|
||||
severity === 'critical' ? 'bg-red-200 text-red-800' :
|
||||
severity === 'high' ? 'bg-orange-200 text-orange-800' :
|
||||
'bg-yellow-200 text-yellow-800'
|
||||
}`}>
|
||||
第 {issue.line} 行
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-orange-700 mb-2">{issue.description}</p>
|
||||
<p className="text-sm text-orange-600">
|
||||
<strong>建议:</strong>{issue.suggestion}
|
||||
<p className={`text-sm mb-3 ${
|
||||
severity === 'critical' ? 'text-red-700' :
|
||||
severity === 'high' ? 'text-orange-700' :
|
||||
'text-yellow-700'
|
||||
}`}>
|
||||
{issue.description}
|
||||
</p>
|
||||
<div className="bg-white rounded-lg p-3 border">
|
||||
<p className="text-sm font-medium text-gray-800 mb-1">修复建议:</p>
|
||||
<p className="text-sm text-gray-600">{issue.suggestion}</p>
|
||||
</div>
|
||||
))}
|
||||
{result.issues.filter(issue => issue.severity === 'high').length === 0 && (
|
||||
<p className="text-center text-muted-foreground py-8">
|
||||
没有发现高优先级问题
|
||||
</p>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="medium" className="space-y-3 mt-4">
|
||||
{result.issues.filter(issue => issue.severity === 'medium').map((issue, index) => (
|
||||
<div key={index} className="border border-yellow-200 rounded-lg p-4 bg-yellow-50">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
{getTypeIcon(issue.type)}
|
||||
<h4 className="font-medium text-yellow-800">{issue.title}</h4>
|
||||
</div>
|
||||
<p className="text-sm text-yellow-700 mb-2">{issue.description}</p>
|
||||
<p className="text-sm text-yellow-600">
|
||||
<strong>建议:</strong>{issue.suggestion}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
{result.issues.filter(issue => issue.severity === 'medium').length === 0 && (
|
||||
<p className="text-center text-muted-foreground py-8">
|
||||
没有发现中等优先级问题
|
||||
</p>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<CheckCircle className="w-12 h-12 text-green-600 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-green-800 mb-2">代码质量良好!</h3>
|
||||
<p className="text-green-600">没有发现明显的问题</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<Code className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-muted-foreground mb-2">
|
||||
等待代码分析
|
||||
<div className="text-center py-12">
|
||||
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
没有发现{severity === 'critical' ? '严重' : severity === 'high' ? '高优先级' : '中等优先级'}问题
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
请在左侧输入代码并选择编程语言
|
||||
<p className="text-gray-500">
|
||||
代码在此级别的检查中表现良好
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
|
||||
) : (
|
||||
<div className="text-center py-16">
|
||||
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<CheckCircle className="w-12 h-12 text-green-600" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-green-800 mb-3">代码质量优秀!</h3>
|
||||
<p className="text-green-600 text-lg mb-6">恭喜!没有发现任何问题</p>
|
||||
<div className="bg-green-50 rounded-lg p-6 max-w-md mx-auto">
|
||||
<p className="text-green-700 text-sm">
|
||||
您的代码通过了所有质量检查,包括安全性、性能、可维护性等各个方面的评估。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 分析进行中状态 */}
|
||||
{analyzing && (
|
||||
<Card className="card-modern">
|
||||
<CardContent className="py-16">
|
||||
<div className="text-center">
|
||||
<div className="w-20 h-20 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-4 border-blue-600 border-t-transparent"></div>
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-3">AI正在分析您的代码</h3>
|
||||
<p className="text-gray-600 text-lg mb-6">请稍候,这通常只需要几秒钟...</p>
|
||||
<div className="bg-blue-50 rounded-lg p-6 max-w-md mx-auto">
|
||||
<p className="text-blue-700 text-sm">
|
||||
正在进行安全检测、性能分析、代码风格检查等多维度评估
|
||||
</p>
|
||||
<div className="flex justify-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => loadExampleCode('javascript')}
|
||||
>
|
||||
<Sparkles className="w-3 h-3 mr-1" />
|
||||
试试JavaScript示例
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { LoginPanel } from "miaoda-auth-react";
|
||||
import { api } from "@/db/supabase";
|
||||
import { api } from "@/shared/config/database";
|
||||
|
||||
const login_config = {
|
||||
title: '智能代码审计系统',
|
||||
title: 'XCodeReviewer',
|
||||
desc: '登录以开始代码质量分析',
|
||||
onLoginSuccess: async (user: any) => {
|
||||
try {
|
||||
|
|
@ -44,7 +44,7 @@ export default function Login() {
|
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">智能代码审计系统</h1>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">XCodeReviewer</h1>
|
||||
<p className="text-gray-600">基于AI的代码质量分析平台</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Link } from "react-router-dom";
|
||||
import PageMeta from "@/components/common/PageMeta";
|
||||
import PageMeta from "@/components/layout/PageMeta";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ import {
|
|||
Play,
|
||||
FileText
|
||||
} from "lucide-react";
|
||||
import { api } from "@/db/supabase";
|
||||
import { runRepositoryAudit } from "@/services/repoScan";
|
||||
import type { Project, AuditTask, AuditIssue } from "@/types/types";
|
||||
import { api } from "@/shared/config/database";
|
||||
import { runRepositoryAudit } from "@/features/projects/services";
|
||||
import type { Project, AuditTask, AuditIssue } from "@/shared/types";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function ProjectDetail() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
|
@ -8,6 +8,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
|
|
@ -19,10 +20,13 @@ import {
|
|||
Code,
|
||||
Shield,
|
||||
Activity,
|
||||
AlertTriangle
|
||||
Upload,
|
||||
FileText,
|
||||
AlertCircle
|
||||
} from "lucide-react";
|
||||
import { api } from "@/db/supabase";
|
||||
import type { Project, CreateProjectForm } from "@/types/types";
|
||||
import { api } from "@/shared/config/database";
|
||||
import { scanZipFile, validateZipFile } from "@/features/projects/services";
|
||||
import type { Project, CreateProjectForm } from "@/shared/types";
|
||||
import { Link } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
|
||||
|
|
@ -31,6 +35,9 @@ export default function Projects() {
|
|||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [createForm, setCreateForm] = useState<CreateProjectForm>({
|
||||
name: "",
|
||||
description: "",
|
||||
|
|
@ -75,6 +82,15 @@ export default function Projects() {
|
|||
|
||||
toast.success("项目创建成功");
|
||||
setShowCreateDialog(false);
|
||||
resetCreateForm();
|
||||
loadProjects();
|
||||
} catch (error) {
|
||||
console.error('Failed to create project:', error);
|
||||
toast.error("创建项目失败");
|
||||
}
|
||||
};
|
||||
|
||||
const resetCreateForm = () => {
|
||||
setCreateForm({
|
||||
name: "",
|
||||
description: "",
|
||||
|
|
@ -83,10 +99,76 @@ export default function Projects() {
|
|||
default_branch: "main",
|
||||
programming_languages: []
|
||||
});
|
||||
};
|
||||
|
||||
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
// 验证文件
|
||||
const validation = validateZipFile(file);
|
||||
if (!validation.valid) {
|
||||
toast.error(validation.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有项目名称
|
||||
if (!createForm.name.trim()) {
|
||||
toast.error("请先输入项目名称");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setUploading(true);
|
||||
setUploadProgress(0);
|
||||
|
||||
// 创建项目
|
||||
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: undefined
|
||||
});
|
||||
|
||||
clearInterval(progressInterval);
|
||||
setUploadProgress(100);
|
||||
|
||||
toast.success("项目创建并开始分析");
|
||||
setShowCreateDialog(false);
|
||||
resetCreateForm();
|
||||
loadProjects();
|
||||
} catch (error) {
|
||||
console.error('Failed to create project:', error);
|
||||
toast.error("创建项目失败");
|
||||
|
||||
// 跳转到任务详情页
|
||||
setTimeout(() => {
|
||||
window.open(`/tasks/${taskId}`, '_blank');
|
||||
}, 1000);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Upload failed:', error);
|
||||
toast.error(error.message || "上传失败");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
setUploadProgress(0);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -116,14 +198,12 @@ export default function Projects() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{/* 页面标题和操作 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">项目管理</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
管理您的代码项目,配置审计规则和查看分析结果
|
||||
</p>
|
||||
<h1 className="page-title">项目管理</h1>
|
||||
<p className="page-subtitle">管理您的代码项目,配置审计规则和查看分析结果</p>
|
||||
</div>
|
||||
|
||||
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
||||
|
|
@ -133,11 +213,18 @@ export default function Projects() {
|
|||
新建项目
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>创建新项目</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
|
||||
<Tabs defaultValue="repository" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="repository">Git 仓库</TabsTrigger>
|
||||
<TabsTrigger value="upload">上传代码</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="repository" className="space-y-4 mt-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">项目名称 *</Label>
|
||||
|
|
@ -235,7 +322,120 @@ export default function Projects() {
|
|||
创建项目
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="upload" className="space-y-4 mt-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="upload-name">项目名称 *</Label>
|
||||
<Input
|
||||
id="upload-name"
|
||||
value={createForm.name}
|
||||
onChange={(e) => setCreateForm({ ...createForm, name: e.target.value })}
|
||||
placeholder="输入项目名称"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="upload-description">项目描述</Label>
|
||||
<Textarea
|
||||
id="upload-description"
|
||||
value={createForm.description}
|
||||
onChange={(e) => setCreateForm({ ...createForm, description: e.target.value })}
|
||||
placeholder="简要描述项目内容和目标"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>编程语言</Label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{supportedLanguages.map((lang) => (
|
||||
<label key={lang} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={createForm.programming_languages.includes(lang)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setCreateForm({
|
||||
...createForm,
|
||||
programming_languages: [...createForm.programming_languages, lang]
|
||||
});
|
||||
} else {
|
||||
setCreateForm({
|
||||
...createForm,
|
||||
programming_languages: createForm.programming_languages.filter(l => l !== lang)
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm">{lang}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 文件上传区域 */}
|
||||
<div className="space-y-4">
|
||||
<Label>上传代码文件</Label>
|
||||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-gray-400 transition-colors">
|
||||
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">上传 ZIP 文件</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
支持 ZIP 格式,最大 100MB
|
||||
</p>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".zip"
|
||||
onChange={handleFileUpload}
|
||||
className="hidden"
|
||||
disabled={uploading}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploading || !createForm.name.trim()}
|
||||
>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
选择文件
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{uploading && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span>上传并分析中...</span>
|
||||
<span>{uploadProgress}%</span>
|
||||
</div>
|
||||
<Progress value={uploadProgress} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<AlertCircle className="w-5 h-5 text-blue-600 mt-0.5" />
|
||||
<div className="text-sm text-blue-800">
|
||||
<p className="font-medium mb-1">上传说明:</p>
|
||||
<ul className="space-y-1 text-xs">
|
||||
<li>• 请确保 ZIP 文件包含完整的项目代码</li>
|
||||
<li>• 系统会自动排除 node_modules、.git 等目录</li>
|
||||
<li>• 上传后将立即开始代码分析</li>
|
||||
<li>• 分析完成后可在任务详情页查看结果</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 pt-4">
|
||||
<Button variant="outline" onClick={() => setShowCreateDialog(false)} disabled={uploading}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
|
@ -265,87 +465,89 @@ export default function Projects() {
|
|||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredProjects.length > 0 ? (
|
||||
filteredProjects.map((project) => (
|
||||
<Card key={project.id} className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader className="pb-3">
|
||||
<Card key={project.id} className="card-modern group">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-lg">{getRepositoryIcon(project.repository_type)}</span>
|
||||
<CardTitle className="text-lg">
|
||||
<Link
|
||||
to={`/projects/${project.id}`}
|
||||
className="hover:text-blue-600 transition-colors"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-white text-lg">
|
||||
{getRepositoryIcon(project.repository_type)}
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg group-hover:text-blue-600 transition-colors">
|
||||
<Link to={`/projects/${project.id}`}>
|
||||
{project.name}
|
||||
</Link>
|
||||
</CardTitle>
|
||||
</div>
|
||||
<Badge variant={project.is_active ? "default" : "secondary"}>
|
||||
{project.is_active ? '活跃' : '暂停'}
|
||||
</Badge>
|
||||
</div>
|
||||
{project.description && (
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
<p className="text-sm text-gray-500 mt-1 line-clamp-2">
|
||||
{project.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={project.is_active ? "default" : "secondary"} className="flex-shrink-0">
|
||||
{project.is_active ? '活跃' : '暂停'}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
{/* 项目信息 */}
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
{project.repository_url && (
|
||||
<div className="flex items-center text-sm text-muted-foreground">
|
||||
<GitBranch className="w-4 h-4 mr-2" />
|
||||
<div className="flex items-center text-sm text-gray-500">
|
||||
<GitBranch className="w-4 h-4 mr-2 flex-shrink-0" />
|
||||
<a
|
||||
href={project.repository_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-blue-600 transition-colors flex items-center"
|
||||
className="hover:text-blue-600 transition-colors flex items-center truncate"
|
||||
>
|
||||
{project.repository_url.replace('https://', '').substring(0, 30)}...
|
||||
<ExternalLink className="w-3 h-3 ml-1" />
|
||||
<span className="truncate">{project.repository_url.replace('https://', '')}</span>
|
||||
<ExternalLink className="w-3 h-3 ml-1 flex-shrink-0" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center text-sm text-muted-foreground">
|
||||
<div className="flex items-center justify-between text-sm text-gray-500">
|
||||
<div className="flex items-center">
|
||||
<Calendar className="w-4 h-4 mr-2" />
|
||||
创建于 {formatDate(project.created_at)}
|
||||
{formatDate(project.created_at)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center text-sm text-muted-foreground">
|
||||
<div className="flex items-center">
|
||||
<Users className="w-4 h-4 mr-2" />
|
||||
所有者:{project.owner?.full_name || project.owner?.phone || '未知'}
|
||||
{project.owner?.full_name || '未知'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 编程语言 */}
|
||||
{project.programming_languages && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{JSON.parse(project.programming_languages).slice(0, 3).map((lang: string) => (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{JSON.parse(project.programming_languages).slice(0, 4).map((lang: string) => (
|
||||
<Badge key={lang} variant="outline" className="text-xs">
|
||||
{lang}
|
||||
</Badge>
|
||||
))}
|
||||
{JSON.parse(project.programming_languages).length > 3 && (
|
||||
{JSON.parse(project.programming_languages).length > 4 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{JSON.parse(project.programming_languages).length - 3}
|
||||
+{JSON.parse(project.programming_languages).length - 4}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 快速操作 */}
|
||||
<div className="flex space-x-2 pt-2">
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Link to={`/projects/${project.id}`} className="flex-1">
|
||||
<Button variant="outline" size="sm" className="w-full">
|
||||
<Button variant="outline" size="sm" className="w-full btn-secondary">
|
||||
<Code className="w-4 h-4 mr-2" />
|
||||
查看详情
|
||||
</Button>
|
||||
</Link>
|
||||
<Button variant="outline" size="sm">
|
||||
<Button size="sm" className="btn-primary">
|
||||
<Shield className="w-4 h-4 mr-2" />
|
||||
启动审计
|
||||
审计
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
@ -353,17 +555,19 @@ export default function Projects() {
|
|||
))
|
||||
) : (
|
||||
<div className="col-span-full">
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<Code className="w-16 h-16 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-medium text-muted-foreground mb-2">
|
||||
<Card className="card-modern">
|
||||
<CardContent className="empty-state py-16">
|
||||
<div className="empty-icon">
|
||||
<Code className="w-8 h-8 text-blue-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
{searchTerm ? '未找到匹配的项目' : '暂无项目'}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
<p className="text-gray-500 mb-6 max-w-md">
|
||||
{searchTerm ? '尝试调整搜索条件' : '创建您的第一个项目开始代码审计'}
|
||||
</p>
|
||||
{!searchTerm && (
|
||||
<Button onClick={() => setShowCreateDialog(true)}>
|
||||
<Button onClick={() => setShowCreateDialog(true)} className="btn-primary">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
创建项目
|
||||
</Button>
|
||||
|
|
@ -376,56 +580,58 @@ export default function Projects() {
|
|||
|
||||
{/* 项目统计 */}
|
||||
{projects.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center">
|
||||
<Code className="h-8 w-8 text-blue-600" />
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-muted-foreground">总项目数</p>
|
||||
<p className="text-2xl font-bold">{projects.length}</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<Card className="stat-card">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="stat-label">总项目数</p>
|
||||
<p className="stat-value text-xl">{projects.length}</p>
|
||||
</div>
|
||||
<div className="stat-icon from-blue-500 to-blue-600">
|
||||
<Code className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center">
|
||||
<Activity className="h-8 w-8 text-green-600" />
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-muted-foreground">活跃项目</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{projects.filter(p => p.is_active).length}
|
||||
</p>
|
||||
<Card className="stat-card">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="stat-label">活跃项目</p>
|
||||
<p className="stat-value text-xl">{projects.filter(p => p.is_active).length}</p>
|
||||
</div>
|
||||
<div className="stat-icon from-emerald-500 to-emerald-600">
|
||||
<Activity className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center">
|
||||
<GitBranch className="h-8 w-8 text-purple-600" />
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-muted-foreground">GitHub项目</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{projects.filter(p => p.repository_type === 'github').length}
|
||||
</p>
|
||||
<Card className="stat-card">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="stat-label">GitHub</p>
|
||||
<p className="stat-value text-xl">{projects.filter(p => p.repository_type === 'github').length}</p>
|
||||
</div>
|
||||
<div className="stat-icon from-purple-500 to-purple-600">
|
||||
<GitBranch className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center">
|
||||
<Shield className="h-8 w-8 text-orange-600" />
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-muted-foreground">GitLab项目</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{projects.filter(p => p.repository_type === 'gitlab').length}
|
||||
</p>
|
||||
<Card className="stat-card">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="stat-label">GitLab</p>
|
||||
<p className="stat-value text-xl">{projects.filter(p => p.repository_type === 'gitlab').length}</p>
|
||||
</div>
|
||||
<div className="stat-icon from-orange-500 to-orange-600">
|
||||
<Shield className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* 示例页面
|
||||
*/
|
||||
|
||||
import PageMeta from "../components/common/PageMeta";
|
||||
import PageMeta from "@/components/layout/PageMeta";
|
||||
|
||||
export default function SamplePage() {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ import {
|
|||
Info,
|
||||
Lightbulb
|
||||
} from "lucide-react";
|
||||
import { api } from "@/db/supabase";
|
||||
import type { AuditTask, AuditIssue } from "@/types/types";
|
||||
import { api } from "@/shared/config/database";
|
||||
import type { AuditTask, AuditIssue } from "@/shared/types";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function TaskDetail() {
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
import { api } from "@/db/supabase";
|
||||
import { CodeAnalysisEngine } from "@/services/codeAnalysis";
|
||||
import { unzipSync, strFromU8 } from "fflate";
|
||||
|
||||
const TEXT_EXTENSIONS = [
|
||||
".js", ".ts", ".tsx", ".jsx", ".py", ".java", ".go", ".rs", ".cpp", ".c", ".h", ".cs", ".php", ".rb", ".kt", ".swift", ".sql", ".sh", ".json", ".yml", ".yaml", ".md"
|
||||
];
|
||||
|
||||
const MAX_FILE_SIZE_BYTES = 200 * 1024;
|
||||
|
||||
function isTextFile(path: string): boolean {
|
||||
const lower = path.toLowerCase();
|
||||
return TEXT_EXTENSIONS.some(ext => lower.endsWith(ext));
|
||||
}
|
||||
|
||||
export async function runZipRepositoryAudit(params: {
|
||||
projectId: string;
|
||||
repoUrl: string; // https://github.com/owner/repo
|
||||
branch?: string;
|
||||
}) {
|
||||
const branch = params.branch || "main";
|
||||
const task = await api.createAuditTask({
|
||||
project_id: params.projectId,
|
||||
task_type: "repository",
|
||||
branch_name: branch,
|
||||
exclude_patterns: [],
|
||||
scan_config: {},
|
||||
created_by: null
|
||||
} as any);
|
||||
|
||||
try {
|
||||
const m = params.repoUrl.match(/github\.com\/(.+?)\/(.+?)(?:\.git)?$/i);
|
||||
if (!m) throw new Error("仅支持 GitHub 仓库 URL,例如 https://github.com/owner/repo");
|
||||
const owner = m[1];
|
||||
const repo = m[2];
|
||||
|
||||
// GitHub 提供仓库 zip 下载(无需 token)
|
||||
const zipUrl = `https://codeload.github.com/${owner}/${repo}/zip/refs/heads/${encodeURIComponent(branch)}`;
|
||||
const res = await fetch(zipUrl);
|
||||
if (!res.ok) throw new Error(`下载仓库压缩包失败: ${res.status}`);
|
||||
const buf = new Uint8Array(await res.arrayBuffer());
|
||||
const files = unzipSync(buf);
|
||||
|
||||
let totalFiles = 0;
|
||||
let totalLines = 0;
|
||||
let createdIssues = 0;
|
||||
|
||||
const rootPrefix = `${repo}-${branch}/`;
|
||||
|
||||
for (const name of Object.keys(files)) {
|
||||
if (!name.startsWith(rootPrefix)) continue;
|
||||
const relPath = name.slice(rootPrefix.length);
|
||||
if (!relPath || relPath.endsWith("/")) continue; // 目录
|
||||
if (!isTextFile(relPath)) continue;
|
||||
|
||||
const fileData = files[name];
|
||||
if (!fileData || fileData.length > MAX_FILE_SIZE_BYTES) continue;
|
||||
const content = strFromU8(fileData);
|
||||
totalFiles += 1;
|
||||
totalLines += content.split(/\r?\n/).length;
|
||||
|
||||
const ext = relPath.split(".").pop() || "";
|
||||
const language = ext.toLowerCase();
|
||||
const analysis = await CodeAnalysisEngine.analyzeCode(content, language);
|
||||
const issues = analysis.issues || [];
|
||||
createdIssues += issues.length;
|
||||
|
||||
for (const issue of issues) {
|
||||
await api.createAuditIssue({
|
||||
task_id: (task as any).id,
|
||||
file_path: relPath,
|
||||
line_number: issue.line || null,
|
||||
column_number: issue.column || null,
|
||||
issue_type: issue.type || "maintainability",
|
||||
severity: issue.severity || "low",
|
||||
title: issue.title || "Issue",
|
||||
description: issue.description || null,
|
||||
suggestion: issue.suggestion || null,
|
||||
code_snippet: issue.code_snippet || null,
|
||||
ai_explanation: issue.xai ? JSON.stringify(issue.xai) : (issue.ai_explanation || null),
|
||||
status: "open",
|
||||
resolved_by: null,
|
||||
resolved_at: null
|
||||
} as any);
|
||||
}
|
||||
}
|
||||
|
||||
await api.updateAuditTask((task as any).id, {
|
||||
status: "completed",
|
||||
total_files: totalFiles,
|
||||
scanned_files: totalFiles,
|
||||
total_lines: totalLines,
|
||||
issues_count: createdIssues,
|
||||
quality_score: 0
|
||||
} as any);
|
||||
|
||||
return (task as any).id as string;
|
||||
} catch (e) {
|
||||
await api.updateAuditTask((task as any).id, { status: "failed" } as any);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// 环境变量配置
|
||||
export const env = {
|
||||
// Gemini AI 配置
|
||||
GEMINI_API_KEY: import.meta.env.VITE_GEMINI_API_KEY || '',
|
||||
GEMINI_MODEL: import.meta.env.VITE_GEMINI_MODEL || 'gemini-2.5-flash',
|
||||
GEMINI_TIMEOUT_MS: Number(import.meta.env.VITE_GEMINI_TIMEOUT_MS) || 25000,
|
||||
|
||||
// Supabase 配置
|
||||
SUPABASE_URL: import.meta.env.VITE_SUPABASE_URL || '',
|
||||
SUPABASE_ANON_KEY: import.meta.env.VITE_SUPABASE_ANON_KEY || '',
|
||||
|
||||
// GitHub 配置
|
||||
GITHUB_TOKEN: import.meta.env.VITE_GITHUB_TOKEN || '',
|
||||
|
||||
// 应用配置
|
||||
APP_ID: import.meta.env.VITE_APP_ID || 'xcodereviewer',
|
||||
|
||||
// 分析配置
|
||||
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,
|
||||
|
||||
// 开发环境标识
|
||||
isDev: import.meta.env.DEV,
|
||||
isProd: import.meta.env.PROD,
|
||||
} as const;
|
||||
|
||||
// 验证必需的环境变量
|
||||
export function validateEnv() {
|
||||
const requiredVars = ['GEMINI_API_KEY'];
|
||||
const missing = requiredVars.filter(key => !env[key as keyof typeof env]);
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.warn(`Missing required environment variables: ${missing.join(', ')}`);
|
||||
}
|
||||
|
||||
return missing.length === 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
// 导出所有配置
|
||||
export { env, validateEnv } from './env';
|
||||
export { supabase } from './database';
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
// 应用常量定义
|
||||
|
||||
// 支持的编程语言
|
||||
export const SUPPORTED_LANGUAGES = [
|
||||
'javascript',
|
||||
'typescript',
|
||||
'python',
|
||||
'java',
|
||||
'go',
|
||||
'rust',
|
||||
'cpp',
|
||||
'csharp',
|
||||
'php',
|
||||
'ruby',
|
||||
'swift',
|
||||
'kotlin',
|
||||
] as const;
|
||||
|
||||
// 问题类型
|
||||
export const ISSUE_TYPES = {
|
||||
BUG: 'bug',
|
||||
SECURITY: 'security',
|
||||
PERFORMANCE: 'performance',
|
||||
STYLE: 'style',
|
||||
MAINTAINABILITY: 'maintainability',
|
||||
} as const;
|
||||
|
||||
// 问题严重程度
|
||||
export const SEVERITY_LEVELS = {
|
||||
CRITICAL: 'critical',
|
||||
HIGH: 'high',
|
||||
MEDIUM: 'medium',
|
||||
LOW: 'low',
|
||||
} as const;
|
||||
|
||||
// 任务状态
|
||||
export const TASK_STATUS = {
|
||||
PENDING: 'pending',
|
||||
RUNNING: 'running',
|
||||
COMPLETED: 'completed',
|
||||
FAILED: 'failed',
|
||||
} as const;
|
||||
|
||||
// 用户角色
|
||||
export const USER_ROLES = {
|
||||
ADMIN: 'admin',
|
||||
MEMBER: 'member',
|
||||
} as const;
|
||||
|
||||
// 项目成员角色
|
||||
export const PROJECT_ROLES = {
|
||||
OWNER: 'owner',
|
||||
ADMIN: 'admin',
|
||||
MEMBER: 'member',
|
||||
VIEWER: 'viewer',
|
||||
} as const;
|
||||
|
||||
// 仓库类型
|
||||
export const REPOSITORY_TYPES = {
|
||||
GITHUB: 'github',
|
||||
GITLAB: 'gitlab',
|
||||
OTHER: 'other',
|
||||
} as const;
|
||||
|
||||
// 分析深度
|
||||
export const ANALYSIS_DEPTH = {
|
||||
BASIC: 'basic',
|
||||
STANDARD: 'standard',
|
||||
DEEP: 'deep',
|
||||
} as const;
|
||||
|
||||
// 默认配置
|
||||
export const DEFAULT_CONFIG = {
|
||||
MAX_FILE_SIZE: 1024 * 1024, // 1MB
|
||||
MAX_FILES_PER_SCAN: 100,
|
||||
ANALYSIS_TIMEOUT: 30000, // 30秒
|
||||
DEBOUNCE_DELAY: 300, // 300ms
|
||||
} as const;
|
||||
|
||||
// API 端点
|
||||
export const API_ENDPOINTS = {
|
||||
PROJECTS: '/api/projects',
|
||||
AUDIT_TASKS: '/api/audit-tasks',
|
||||
INSTANT_ANALYSIS: '/api/instant-analysis',
|
||||
USERS: '/api/users',
|
||||
} as const;
|
||||
|
||||
// 本地存储键名
|
||||
export const STORAGE_KEYS = {
|
||||
THEME: 'xcodereviewer-theme',
|
||||
USER_PREFERENCES: 'xcodereviewer-preferences',
|
||||
RECENT_PROJECTS: 'xcodereviewer-recent-projects',
|
||||
} as const;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// 导出所有共享 hooks
|
||||
export { useAsync } from './useAsync';
|
||||
export { useDebounce, useDebouncedCallback } from './useDebounce';
|
||||
export { useLocalStorage } from './useLocalStorage';
|
||||
export { useToast, toast } from './use-toast';
|
||||
export { useIsMobile } from './use-mobile';
|
||||
export { useGoBack } from './use-go-back';
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export function useGoBack() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const goBack = () => {
|
||||
if (window.history.length > 1) {
|
||||
navigate(-1);
|
||||
} else {
|
||||
navigate('/');
|
||||
}
|
||||
};
|
||||
|
||||
return goBack;
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import * as React from "react";
|
||||
|
||||
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
|
||||
|
||||
const TOAST_LIMIT = 1;
|
||||
|
|
@ -87,8 +86,6 @@ export const reducer = (state: State, action: Action): State => {
|
|||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action;
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId);
|
||||
} else {
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
interface AsyncState<T> {
|
||||
data: T | null;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export function useAsync<T>(
|
||||
asyncFunction: () => Promise<T>,
|
||||
dependencies: any[] = []
|
||||
): AsyncState<T> & { refetch: () => Promise<void> } {
|
||||
const [state, setState] = useState<AsyncState<T>>({
|
||||
data: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const execute = useCallback(async () => {
|
||||
setState(prev => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
try {
|
||||
const data = await asyncFunction();
|
||||
setState({ data, loading: false, error: null });
|
||||
} catch (error) {
|
||||
setState({ data: null, loading: false, error: error as Error });
|
||||
}
|
||||
}, dependencies);
|
||||
|
||||
useEffect(() => {
|
||||
execute();
|
||||
}, [execute]);
|
||||
|
||||
const refetch = useCallback(async () => {
|
||||
await execute();
|
||||
}, [execute]);
|
||||
|
||||
return { ...state, refetch };
|
||||
}
|
||||
|
||||
export function useAsyncCallback<T extends any[], R>(
|
||||
asyncFunction: (...args: T) => Promise<R>
|
||||
): [(...args: T) => Promise<void>, AsyncState<R>] {
|
||||
const [state, setState] = useState<AsyncState<R>>({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const execute = useCallback(async (...args: T) => {
|
||||
setState(prev => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
try {
|
||||
const data = await asyncFunction(...args);
|
||||
setState({ data, loading: false, error: null });
|
||||
} catch (error) {
|
||||
setState(prev => ({ ...prev, loading: false, error: error as Error }));
|
||||
}
|
||||
}, [asyncFunction]);
|
||||
|
||||
return [execute, state];
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
export function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
|
||||
export function useDebouncedCallback<T extends (...args: any[]) => any>(
|
||||
callback: T,
|
||||
delay: number
|
||||
): T {
|
||||
const [debounceTimer, setDebounceTimer] = useState<NodeJS.Timeout>();
|
||||
|
||||
const debouncedCallback = ((...args: any[]) => {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
}
|
||||
|
||||
const newTimer = setTimeout(() => {
|
||||
callback(...args);
|
||||
}, delay);
|
||||
|
||||
setDebounceTimer(newTimer);
|
||||
}) as T;
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
}
|
||||
};
|
||||
}, [debounceTimer]);
|
||||
|
||||
return debouncedCallback;
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
export function useLocalStorage<T>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
): [T, (value: T | ((val: T) => T)) => void] {
|
||||
// 获取初始值
|
||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
} catch (error) {
|
||||
console.error(`Error reading localStorage key "${key}":`, error);
|
||||
return initialValue;
|
||||
}
|
||||
});
|
||||
|
||||
// 设置值的函数
|
||||
const setValue = (value: T | ((val: T) => T)) => {
|
||||
try {
|
||||
// 允许传入函数来更新状态
|
||||
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
||||
setStoredValue(valueToStore);
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||
} catch (error) {
|
||||
console.error(`Error setting localStorage key "${key}":`, error);
|
||||
}
|
||||
};
|
||||
|
||||
return [storedValue, setValue];
|
||||
}
|
||||
|
||||
export function useSessionStorage<T>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
): [T, (value: T | ((val: T) => T)) => void] {
|
||||
// 获取初始值
|
||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||
try {
|
||||
const item = window.sessionStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
} catch (error) {
|
||||
console.error(`Error reading sessionStorage key "${key}":`, error);
|
||||
return initialValue;
|
||||
}
|
||||
});
|
||||
|
||||
// 设置值的函数
|
||||
const setValue = (value: T | ((val: T) => T)) => {
|
||||
try {
|
||||
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
||||
setStoredValue(valueToStore);
|
||||
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
|
||||
} catch (error) {
|
||||
console.error(`Error setting sessionStorage key "${key}":`, error);
|
||||
}
|
||||
};
|
||||
|
||||
return [storedValue, setValue];
|
||||
}
|
||||
|
|
@ -1,3 +1,11 @@
|
|||
// 通用选项接口
|
||||
export interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
icon?: React.ComponentType<{ className?: string }>;
|
||||
withCount?: boolean;
|
||||
}
|
||||
|
||||
// 用户相关类型
|
||||
export interface Profile {
|
||||
id: string;
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
// 路径别名工具函数
|
||||
export const paths = {
|
||||
// 应用核心
|
||||
app: '@/app',
|
||||
|
||||
// 组件
|
||||
components: '@/components',
|
||||
ui: '@/components/ui',
|
||||
layout: '@/components/layout',
|
||||
features: '@/components/features',
|
||||
common: '@/components/common',
|
||||
|
||||
// 页面
|
||||
pages: '@/pages',
|
||||
|
||||
// 功能模块
|
||||
analysisFeature: '@/features/analysis',
|
||||
projectsFeature: '@/features/projects',
|
||||
auditFeature: '@/features/audit',
|
||||
|
||||
// 共享资源
|
||||
shared: '@/shared',
|
||||
hooks: '@/shared/hooks',
|
||||
services: '@/shared/services',
|
||||
types: '@/shared/types',
|
||||
utils: '@/shared/utils',
|
||||
constants: '@/shared/constants',
|
||||
config: '@/shared/config',
|
||||
|
||||
// 静态资源
|
||||
assets: '@/assets',
|
||||
images: '@/assets/images',
|
||||
icons: '@/assets/icons',
|
||||
styles: '@/assets/styles',
|
||||
} as const;
|
||||
|
||||
// 获取路径的辅助函数
|
||||
export function getPath(key: keyof typeof paths): string {
|
||||
return paths[key];
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function formatDate(date: string | Date, options?: Intl.DateTimeFormatOptions) {
|
||||
const defaultOptions: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
};
|
||||
|
||||
return new Date(date).toLocaleDateString('zh-CN', { ...defaultOptions, ...options });
|
||||
}
|
||||
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
export function formatNumber(num: number): string {
|
||||
return new Intl.NumberFormat('zh-CN').format(num);
|
||||
}
|
||||
|
||||
export function generateId(): string {
|
||||
return Math.random().toString(36).substring(2) + Date.now().toString(36);
|
||||
}
|
||||
|
||||
export function debounce<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: NodeJS.Timeout;
|
||||
return (...args: Parameters<T>) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func(...args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
export function throttle<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
limit: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let inThrottle: boolean;
|
||||
return (...args: Parameters<T>) => {
|
||||
if (!inThrottle) {
|
||||
func(...args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function isValidEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
export function isValidPhone(phone: string): boolean {
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
return phoneRegex.test(phone);
|
||||
}
|
||||
|
||||
export function truncateText(text: string, maxLength: number): string {
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.substring(0, maxLength) + '...';
|
||||
}
|
||||
|
||||
export function getFileExtension(filename: string): string {
|
||||
return filename.split('.').pop()?.toLowerCase() || '';
|
||||
}
|
||||
|
||||
export function getLanguageFromExtension(extension: string): string {
|
||||
const languageMap: Record<string, string> = {
|
||||
'js': 'javascript',
|
||||
'jsx': 'javascript',
|
||||
'ts': 'typescript',
|
||||
'tsx': 'typescript',
|
||||
'py': 'python',
|
||||
'java': 'java',
|
||||
'go': 'go',
|
||||
'rs': 'rust',
|
||||
'cpp': 'cpp',
|
||||
'c': 'cpp',
|
||||
'cs': 'csharp',
|
||||
'php': 'php',
|
||||
'rb': 'ruby',
|
||||
'kt': 'kotlin',
|
||||
'swift': 'swift'
|
||||
};
|
||||
|
||||
return languageMap[extension] || 'text';
|
||||
}
|
||||
|
||||
export function calculateQualityGrade(score: number): {
|
||||
grade: string;
|
||||
color: string;
|
||||
description: string;
|
||||
} {
|
||||
if (score >= 90) {
|
||||
return {
|
||||
grade: 'A',
|
||||
color: 'text-green-600',
|
||||
description: '优秀'
|
||||
};
|
||||
} else if (score >= 80) {
|
||||
return {
|
||||
grade: 'B',
|
||||
color: 'text-blue-600',
|
||||
description: '良好'
|
||||
};
|
||||
} else if (score >= 70) {
|
||||
return {
|
||||
grade: 'C',
|
||||
color: 'text-yellow-600',
|
||||
description: '一般'
|
||||
};
|
||||
} else if (score >= 60) {
|
||||
return {
|
||||
grade: 'D',
|
||||
color: 'text-orange-600',
|
||||
description: '较差'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
grade: 'F',
|
||||
color: 'text-red-600',
|
||||
description: '差'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function copyToClipboard(text: string): Promise<void> {
|
||||
if (navigator.clipboard) {
|
||||
return navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err);
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
icon?: React.ComponentType<{ className?: string }>;
|
||||
withCount?: boolean;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue