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:
lintsinghua 2025-10-22 15:12:59 +08:00
parent 067961df94
commit a12633b47d
104 changed files with 3617 additions and 2229 deletions

19
.env.example Normal file
View File

@ -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

View File

@ -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 构建,提供流畅、直观的操作体验。
## 🎬 项目演示
### 主要功能界面
#### 📊 智能仪表盘
#### 智能仪表盘
![智能仪表盘](public/images/example1.png)
*实时展示项目统计、质量趋势和系统性能,提供全面的代码审计概览*
#### 即时分析
#### 即时分析
![即时分析](public/images/example3.png)
*支持代码片段快速分析,提供详细的 What-Why-How 解释和修复建议*
#### 🚀 项目管理
#### 项目管理
![项目管理](public/images/example2.png)
*集成 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/ # 代码规则配置
```
## 🎯 使用指南

View File

@ -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 @@
[![Vite](https://img.shields.io/badge/Vite-5.1.4-646CFF.svg)](https://vitejs.dev/)
[![Supabase](https://img.shields.io/badge/Supabase-3ECF8E.svg)](https://supabase.com/)
[![Google Gemini](https://img.shields.io/badge/Google%20Gemini-4285F4.svg)](https://ai.google.dev/)
[![Star History](https://api.star-history.com/svg?repos=lintsinghua/XCodeReviewer&type=Date)](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
![Intelligent Dashboard](public/images/example1.png)
*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
![Instant Analysis](public/images/example3.png)
*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
![Project Management](public/images/example2.png)
*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!
[![Star History](https://api.star-history.com/svg?repos=lintsinghua/XCodeReviewer&type=Date)](https://star-history.com/#lintsinghua/XCodeReview

View File

@ -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,

View File

@ -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>

View File

@ -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"

102
scripts/check-setup.js Normal file
View File

@ -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');

124
scripts/setup.bat Normal file
View File

@ -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

142
scripts/setup.js Normal file
View File

@ -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 };

106
scripts/setup.sh Executable file
View File

@ -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! 🚀"

View File

@ -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

29
src/app/App.tsx Normal file
View File

@ -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;

View File

@ -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>

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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}

View File

@ -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>
);
}

View File

@ -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

View File

@ -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({

View File

@ -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",

View File

@ -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,

View File

@ -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",

View File

@ -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} />;

View File

@ -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",

View File

@ -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({

View File

@ -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 (

View File

@ -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]

View File

@ -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;

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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 (

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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">) {

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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;

View File

@ -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,

View File

@ -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;

View File

@ -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";

View File

@ -1,4 +1,4 @@
import { cn } from "@/lib/utils";
import { cn } from "@/shared/utils/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (

View File

@ -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,

View File

@ -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,

View File

@ -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 (

View File

@ -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,

View File

@ -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 (

View File

@ -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;

View File

@ -1,4 +1,4 @@
import { useToast } from "@/hooks/use-toast";
import { useToast } from "@/shared/hooks/use-toast";
import {
Toast,
ToastClose,

View File

@ -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<

View File

@ -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",

View File

@ -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,

View File

@ -0,0 +1,2 @@
// 导出代码分析相关服务
export * from './codeAnalysis';

View File

@ -0,0 +1,3 @@
// 导出项目相关服务
export * from './repoScan';
export * from './repoZipScan';

View File

@ -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 };

View File

@ -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 };
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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));
}

View File

@ -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() {

View File

@ -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>

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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 (

View File

@ -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() {

View File

@ -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>

View File

@ -2,7 +2,7 @@
*
*/
import PageMeta from "../components/common/PageMeta";
import PageMeta from "@/components/layout/PageMeta";
export default function SamplePage() {
return (

View File

@ -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() {

View File

View File

@ -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;
}
}

38
src/shared/config/env.ts Normal file
View File

@ -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;
}

View File

@ -0,0 +1,3 @@
// 导出所有配置
export { env, validateEnv } from './env';
export { supabase } from './database';

View File

@ -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;

View File

@ -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';

View File

@ -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;
}

View File

@ -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 {

View File

@ -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];
}

View File

@ -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;
}

View File

@ -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];
}

View File

@ -1,3 +1,11 @@
// 通用选项接口
export interface Option {
label: string;
value: string;
icon?: React.ComponentType<{ className?: string }>;
withCount?: boolean;
}
// 用户相关类型
export interface Profile {
id: string;

40
src/shared/utils/paths.ts Normal file
View File

@ -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];
}

164
src/shared/utils/utils.ts Normal file
View File

@ -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();
}
}

View File

@ -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