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

|

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

|

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

|

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

|

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

|

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

|

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