feat: 实现技能审核全流程 - 新增审核管理模块、技能状态机、版本审核机制

- 新增审核管理页面:版本审核列表、下架审核列表、审核详情页
- 完善技能状态机:开发中/已上架/下架审核中/已下架四种状态
- 实现版本审核机制:审核中/通过/拒绝/撤销四种状态
- 更新 README:详细记录技能开发流程与审核机制
- 优化技能详情页:根据状态展示不同操作按钮
- 完善我的技能列表:状态筛选与操作限制
- 新增上传新版本页面:分离版本上传与基本信息编辑
- 更新 openspec 规范:技能审核流程与状态定义
This commit is contained in:
2026-03-20 17:54:51 +08:00
parent 9c487f3ed6
commit fb9616a10f
18 changed files with 938 additions and 119 deletions

295
README.md
View File

@@ -88,13 +88,26 @@ grandclaw-archtype/
│ │ │ ├── AdminProjectsPage.jsx # 项目管理
│ │ │ ├── AddProjectPage.jsx # 新增/编辑项目
│ │ │ └── AdminLogsPage.jsx # 全局日志查询
│ │ ├── console/ # 工作台子页面
│ │ │ ├── ChatPage.jsx # 聊天页面
│ │ │ ├── SkillsPage.jsx # 技能市场
│ │ │ ├── SkillDetailPage.jsx # 技能详情
│ │ │ ├── LogsPage.jsx # 日志查询
│ │ │ ├── TasksPage.jsx # 定时任务
│ │ │ ├── TaskDetailPage.jsx # 任务详情
│ │ │ ├── AccountPage.jsx # 账号管理
│ │ │ ├── ProjectsPage.jsx # 项目管理
│ │ │ ├── MemberConfigPage.jsx # 成员配置
│ │ │ ├── AddMemberPage.jsx # 增加成员
│ │ │ ├── ConsoleReviewListPage.jsx # 审核管理列表NEW
│ │ │ └── ConsoleReviewDetailPage.jsx # 审核详情NEW
│ │ └── developer/ # 开发台子页面
│ │ ├── DevOverviewPage.jsx # 开发者总览
│ │ ├── MySkillsPage.jsx # 我的技能(筛选+分页)
│ │ ├── SkillEditorPage.jsx # 技能详情(只读+操作)
│ │ ├── UploadSkillPage.jsx # 创建技能
│ │ ├── UploadSkillPage.jsx # 创建技能(基本信息)
│ │ ├── UpdateSkillInfoPage.jsx # 更新基本信息
│ │ ├── NewVersionPage.jsx # 上传新版本(仅版本信息
│ │ ├── UploadVersionPage.jsx # 上传新版本(NEW
│ │ ├── DevDocsPage.jsx # 开发文档
│ │ └── DevAccountPage.jsx # 开发者设置
│ └── styles/ # SCSS样式模块
@@ -153,7 +166,7 @@ pnpm build
### 3. 工作台Console
- **聊天界面**:支持多种聊天场景(欢迎页、普通对话、技能调用、文件上传)
- **技能市场**:浏览、订阅、查看技能详情
- **技能市场**:浏览、订阅、查看技能详情(仅展示最新版本)
- **日志查询**:支持按用户、类型、状态筛选
- **定时任务**:管理定时任务,支持启用/禁用,查看任务详情
- **项目管理**:成员列表,增加成员
@@ -161,18 +174,19 @@ pnpm build
### 4. 管理台Admin
- **运营总览**:平台运营指标卡片(用户总数、部门数量、项目数量、今日调用)、异常/待办事项提醒、最近操作日志
- **审核管理**版本审核列表与详情、下架审核列表与详情NEW
- **部门管理**:部门列表,支持搜索筛选、新增、编辑、启用/禁用、删除确认
- **用户管理**:用户列表,支持搜索筛选(关键词/部门/状态)、新增、编辑、启用/禁用、删除确认,角色区分(管理员/开发者/成员)
- **项目管理**:项目列表,支持搜索筛选、新增、编辑、启用/禁用、删除确认
- **日志查询**:全局系统日志查询,支持多维度筛选(关键词、用户、部门、类型、状态、时间范围)
### 5. 开发台Developer
- **总览**:开发者指标卡片(我的技能总数、已发布、草稿、待审核)、待审核项目列表、最近动态
- **我的技能**:技能列表,支持关键词搜索、分类筛选、状态筛选、分页,支持上架/下架、删除操作
- **技能详情**:基本信息只读展示、版本历史管理(启用/下载/删除)、审核拒绝原因展示
- **创建技能**:基本信息表单 + 技能图标选择 + 技能包上传
- **总览**:开发者指标卡片(我的技能总数、已上架、开发中、待审核)、待审核项目列表、最近动态
- **我的技能**:技能列表,支持关键词搜索、分类筛选、状态筛选(开发中/已上架/下架审核中/已下架)、分页,支持下架(需要先撤回审核中的版本)、删除(已上架需要先下架)
- **技能详情**:基本信息只读展示、版本历史管理(根据状态展示操作按钮:审核中-撤回审核+下载、审核通过/拒绝/撤销-仅下载、审核拒绝-显示拒绝理由)、技能操作(上传新版本-有审核中版本时禁用、下架技能、删除技能-已上架时禁用)
- **创建技能**:基本信息表单 + 技能图标选择(移除技能包上传
- **更新基本信息**:独立页面编辑技能名称/描述/分类/标签/图标,与版本上传分离
- **上传新版本**:仅包含版本说明和技能包上传,不含基本信息编辑
- **上传新版本**:仅包含版本说明和技能包上传NEW
- **开发文档**:技能开发相关文档
- **开发者设置**:开发者账号信息
@@ -550,8 +564,8 @@ const members = api.members.list();
### 数据文件说明
- `conversations.js`:聊天场景和对话历史
- `skills.js`:技能市场数据,包含技能详情、文件列表、版本历史
- `developerData.js`:开发台数据,包含我的技能(含图标、版本审核状态)、技能分类、开发者总览、开发文档
- `skills.js`:技能市场数据,包含技能详情含状态dev/published/unlisting/unlisted、文件列表、版本历史含状态reviewing/approved/rejected/withdrawn、拒绝理由、审核列表pendingVersionReviews、pendingUnlistReviews
- `developerData.js`:开发台数据,包含我的技能(含图标、版本审核状态、hasPendingReview标识)、技能分类、开发者总览、开发文档
- `logs.js`:操作日志数据(成功/失败/警告状态)
- `tasks.js`:定时任务数据(包含任务配置和执行日志)
- `adminData.js`:管理台数据(部门列表、用户列表、项目列表、总览指标、全局日志、可选项数据)
@@ -631,4 +645,263 @@ export default defineConfig({
3. 组件样式添加到 `_components.scss`
4. 页面特定样式添加到 `global.scss`
*最后更新2026-03-19*
---
## 技能开发流程与审核机制
### 流程概述
技能从创建到上架的完整流程包括:技能创建、版本上传、审核管理、上架销售四个主要环节。
### 状态定义
#### 技能状态
技能有四种状态,表示技能在平台上的可见性和可用性:
| 状态 | 英文 | 描述 | 图标样式 |
|------|------|------|---------|
| 开发中 | `dev` | 技能已创建但尚未有审核通过的版本 | status-stopped (灰色) |
| 已上架 | `published` | 技能有审核通过的版本,可在技能市场展示和订阅 | status-running (绿色) |
| 下架审核中 | `unlisting` | 开发者申请下架,等待管理员审核 | status-warning (黄色) |
| 已下架 | `unlisted` | 技能已下架,不在技能市场展示,可删除 | status-stopped (灰色) |
#### 版本状态
版本有四种状态,表示版本在审核流程中的位置:
| 状态 | 英文 | 描述 | 图标样式 |
|------|------|------|---------|
| 审核中 | `reviewing` | 版本已提交,等待管理员审核 | status-warning (黄色) |
| 审核通过 | `approved` | 版本审核通过,自动生效,技能可上架 | status-running (绿色) |
| 审核拒绝 | `rejected` | 版本审核未通过,需修改后重新提交 | status-error (红色) |
| 已撤销 | `withdrawn` | 开发者主动撤回审核,版本废弃 | status-stopped (灰色) |
### 完整流程
```
┌───────────────────────────────────────────────────────────────────┐
│ 技能开发流程 │
└───────────────────────────────────────────────────────────────────┘
开发者操作 管理员操作 技能市场
──────────── ─────────── ─────────
[1. 创建技能] → 技能: 开发中
↓ (创建后可立即上传版本)
[2. 上传新版本] → 版本: 审核中
│ ──────────────────────────▶ [待审核列表]
↓ │
[3. 提交审核] ↓
│ [4. 审核操作]
├───────────────────────────────────────────────┼───────┐
│ │ │
↓ ↓ ↓
[审核通过] [审核通过] [审核拒绝]
│ │
└───────────────────────────────────────────────┘
技能: 已上架
版本: 审核通过(生效)
───────────────────────────────────────────────────────────▶ [可在技能市场展示]
后续操作:
- [5. 上传新版本] → (需先撤回审核中的版本,一次只能有一个审核中)
- [6. 下架技能] → (需先撤回审核中的版本,下架需管理员审核)
- [7. 删除技能] → (已上架的技能必须先下架)
```
### 关键规则
#### 1. 版本上传规则
- **版本号由后端自动生成**:前端上传时无需填写版本号
- **上传即进入审核**:版本上传后状态自动变为"审核中",没有"草稿"状态
- **一次只能有一个审核中的版本**
- 如果存在审核中的版本,"上传新版本"按钮禁用
- 审核通过或撤回后,才能上传新版本
#### 2. 审核中版本操作
- 可执行操作:撤回审核、下载
- 撤回审核后,版本状态变为"已撤销"(终态),无法继续操作
- 如需重新提交,需上传新版本
#### 3. 技能操作规则
| 技能状态 | 可用操作 | 备注 |
|---------|---------|------|
| 开发中 | 更新基本信息、删除技能 | 未上架,可直接删除 |
| 已上架 | 更新基本信息、下架技能 | 删除按钮禁用(需先下架) |
| 已上架+有审核中版本 | 更新基本信息 | 下架按钮禁用、上传按钮禁用 |
| 下架审核中 | 无(等待管理员审核) | 所有操作按钮禁用 |
| 已下架 | 更新基本信息、删除技能 | 可删除 |
#### 4. 下架流程
- **下架需要管理员审核**
1. 开发者点击"下架技能"
2. 技能状态变为"下架审核中"
3. 管理台"审核管理" -> "下架审核"列表中显示
4. 管理员审核通过/拒绝
5. 通过:技能状态变为"已下架",技能市场移除
6. 拒绝:技能状态回到"已上架"
- **下架期间禁止操作**
- 下架审核中的技能,所有操作按钮禁用
- 不能上传新版本
#### 5. 审核拒绝
- 版本审核拒绝后,在版本说明下方显示拒绝理由
- 被拒绝的版本在技能详情页可见,但无法再次提交审核
- 开发者需上传新版本重新提交
### 数据结构
#### 技能数据skills.js / developerData.js
```javascript
{
id: 1,
name: '代码生成助手',
desc: '根据需求自动生成高质量代码',
tags: ['开发', '代码', 'AI'],
status: 'published', // 技能状态: dev | published | unlisting | unlisted
hasPendingReview: false, // 是否有审核中的版本
category: '开发工具',
versions: [...] // 版本列表
}
```
#### 版本数据
```javascript
{
version: 'v1.3.0',
date: '2026-03-12',
desc: '新增 Python 3.11 支持',
status: 'approved', // 版本状态: reviewing | approved | rejected | withdrawn
rejectionReason: '' // 审核拒绝理由仅status为rejected时
}
```
#### 审核列表数据
```javascript
// 版本审核列表
pendingVersionReviews = [{
id: 1,
skillName: '代码生成助手',
version: 'v1.4.0',
date: '2026-03-20',
developer: '张三'
}]
// 下架审核列表
pendingUnlistReviews = [{
id: 1,
skillName: 'CRM 客户查询',
currentVersion: 'v1.5.0',
date: '2026-03-20',
developer: '赵六'
}]
```
### 页面交互细节
#### 技能详情页SkillEditorPage
**版本历史表格:**
| 版本号 | 版本说明 | 状态 | 更新时间 | 操作 |
|-------|---------|------|---------|------|
| v1.3.0 | 新增功能... | 审核通过 | 2026-03-12 | [下载] |
| v1.2.0 | 优化性能... | 审核拒绝 | 2026-03-08 | [下载] 拒绝理由: 测试用例不完整 |
| v1.1.0 | 新增支持... | 审核中 | 2026-02-20 | [撤回审核] [下载] |
**技能操作按钮:**
- 更新基本信息:始终可用
- 上传新版本:有审核中版本时禁用,提示"存在审核中的版本,请先撤回后再上传新版本"
- 下架技能:技能状态为已上架且无审核中版本时可用
- 删除技能:技能状态为开发中或已下架时可用,已上架时禁用并提示"已上架的技能需要先下架才能删除"
#### 我的技能列表页MySkillsPage
**状态列展示:**
- 开发中:显示"开发中"
- 已上架:显示"已上架"
- 已上架+有审核中版本:显示"已上架 · 审核中"
- 下架审核中:显示"下架审核中"
- 已下架:显示"已下架"
**操作列:**
- 编辑:始终可用
- 下架技能:已上架且无审核中版本时可用
- 删除技能:开发中或已下架时可用
#### 管理台审核管理AdminPage - reviewList
**Tab切换**
- 版本审核:显示待审核的版本列表
- 下架审核:显示待审核的下架申请列表
**版本审核列表:**
| 技能名称 | 版本号 | 提交时间 | 开发者 | 操作 |
|---------|-------|---------|--------|------|
| 代码生成助手 | v1.4.0 | 2026-03-20 | 张三 | [审核] |
**审核详情页reviewDetail**
**版本审核详情:**
- 基本信息:技能名称、开发者、分类、标签
- 版本信息:版本号、提交时间、版本说明
- 文件列表:显示技能包包含的文件
- 操作:[拒绝] [通过]
**下架审核详情:**
- 技能信息:技能名称、开发者、当前版本、订阅数、申请时间
- 操作:[拒绝] [通过]
#### 技能市场详情页SkillDetailPage
**版本展示:**
- 只显示最新审核通过版本的信息
- 不展示完整版本历史
- 示例:
```
当前版本: v1.3.0
更新说明: 新增 Python 3.11 支持
更新时间: 2026-03-12
```
### 状态映射关系
```javascript
// 技能状态映射
const skillStatusMap = {
dev: { text: '开发中', className: 'status-stopped' },
published: { text: '已上架', className: 'status-running' },
unlisting: { text: '下架审核中', className: 'status-warning' },
unlisted: { text: '已下架', className: 'status-stopped' }
};
// 版本状态映射
const versionStatusMap = {
reviewing: { text: '审核中', className: 'status-warning' },
approved: { text: '审核通过', className: 'status-running' },
rejected: { text: '审核拒绝', className: 'status-error' },
withdrawn: { text: '已撤销', className: 'status-stopped' }
};
```
### 注意事项
1. **原型说明**:本页面为静态原型,所有交互均为前端模拟,未连接真实后端
2. **状态持久化**:页面刷新后,状态会重置为初始值
3. **按钮禁用**:使用 HTML 原生 `disabled` 属性,配合 `title` 提示显示禁用原因
4. **Modal 确认**:删除和下架操作需要二次确认,使用 Modal 组件展示确认对话框
5. **Toast 提示**:操作完成后显示 Toast 提示,展示操作结果(成功/失败)
*最后更新2026-03-20*

View File

@@ -1,11 +1,11 @@
## ADDED Requirements
## MODIFIED Requirements
### Requirement: 开发者指标展示
开发台总览页 SHALL 展示开发者维度的核心指标数据,以卡片形式呈现。
#### Scenario: 指标卡片展示
- **WHEN** 用户打开开发台总览页
- **THEN** 页面顶部显示4个指标卡片我的技能总数、已发布数量、草稿数量、待审核版本数量,每个卡片包含数值
- **THEN** 页面顶部显示4个指标卡片我的技能总数、已上架数量、开发中数量、待审核版本数量,每个卡片包含数值
### Requirement: 待审核项目提醒
开发台总览页 SHALL 展示待审核的版本项目列表。
@@ -18,6 +18,10 @@
- **WHEN** 待审核列表中包含被拒绝的版本
- **THEN** 该项显示拒绝状态标签和"查看原因"链接
#### Scenario: 下架审核项展示
- **WHEN** 待审核列表中包含下架审核
- **THEN** 该项显示"下架审核"状态标签
### Requirement: 最近动态展示
开发台总览页 SHALL 展示开发者最近的操作动态记录。

View File

@@ -0,0 +1,42 @@
## ADDED Requirements
### Requirement: 管理台审核列表
AdminPage SHALL 提供版本审核和下架审核的列表展示。
#### Scenario: 审核管理入口
- **WHEN** 用户点击管理台侧边栏"审核管理"导航
- **THEN** 页面显示审核管理列表页面包含版本审核和下架审核Tab
#### Scenario: Tab切换展示
- **WHEN** 用户打开审核管理列表页
- **THEN** 页面顶部显示"版本审核"和"下架审核"两个Tab默认选中版本审核
#### Scenario: 版本审核列表展示
- **WHEN** 用户查看版本审核Tab
- **THEN** 页面显示待审核版本列表,每条包含技能名称、版本号、提交时间、开发者、操作按钮
#### Scenario: 下架审核列表展示
- **WHEN** 用户切换到下架审核Tab
- **THEN** 页面显示待审核下架列表,每条包含技能名称、当前版本、申请时间、开发者、操作按钮
### Requirement: 管理台版本审核详情
AdminPage SHALL 提供版本审核的详情展示和操作。
#### Scenario: 版本信息展示
- **WHEN** 用户点击版本审核列表中的"审核"按钮
- **THEN** 页面显示技能基本信息(名称、开发者、分类、标签)、版本信息(版本号、提交时间、版本说明)、文件列表
#### Scenario: 审核操作按钮
- **WHEN** 用户查看版本审核详情
- **THEN** 页面底部显示"拒绝"和"通过"两个按钮
### Requirement: 管理台下架审核详情
AdminPage SHALL 提供下架审核的详情展示和操作。
#### Scenario: 下架信息展示
- **WHEN** 用户点击下架审核列表中的"审核"按钮
- **THEN** 页面显示技能信息(名称、开发者、当前版本、订阅数、申请时间)
#### Scenario: 下架审核操作
- **WHEN** 用户查看下架审核详情
- **THEN** 页面底部显示"拒绝"和"通过"两个按钮

View File

@@ -0,0 +1,45 @@
## ADDED Requirements
### Requirement: 技能状态标签展示
MySkillsPage SHALL 在技能列表中展示技能状态。
#### Scenario: 技能状态列展示
- **WHEN** 用户查看我的技能列表
- **THEN** 状态列显示技能的当前状态:开发中、已上架、下架审核中、已下架
#### Scenario: 审核中版本提示
- **WHEN** 技能存在审核中的版本
- **THEN** 状态列额外显示版本审核状态(如"已上架 · v1.2 审核中"
### Requirement: 侧边栏技能状态展示
DeveloperPage SHALL 在侧边栏技能列表中展示状态标签。
#### Scenario: 侧边栏状态展示
- **WHEN** 用户查看开发台侧边栏的技能列表
- **THEN** 每个技能项显示对应的状态标签(开发中、已上架、下架审核中、已下架)
### Requirement: 技能操作按钮可用性
MySkillsPage 和 SkillEditorPage SHALL 根据技能状态控制操作按钮的可用性。
#### Scenario: 开发中状态按钮
- **WHEN** 技能状态为开发中
- **THEN** 显示"更新基本信息"、"删除技能"按钮,"下架"按钮不显示
#### Scenario: 已上架状态按钮
- **WHEN** 技能状态为已上架
- **THEN** 显示"更新基本信息"、"下架技能"按钮,"删除技能"按钮禁用并提示"已上架的技能需要先下架才能删除"
#### Scenario: 已下架状态按钮
- **WHEN** 技能状态为已下架
- **THEN** 显示"更新基本信息"、"删除技能"按钮
### Requirement: 上传新版本按钮可用性
SkillEditorPage SHALL 根据版本审核状态控制上传按钮。
#### Scenario: 无审核中版本
- **WHEN** 技能不存在审核中的版本
- **THEN** "上传新版本"按钮可用
#### Scenario: 有审核中版本
- **WHEN** 技能存在审核中的版本
- **THEN** "上传新版本"按钮禁用并提示"存在审核中的版本,请先撤回后再上传新版本"

View File

@@ -0,0 +1,45 @@
## ADDED Requirements
### Requirement: 版本上传表单
UploadVersionPage SHALL 提供版本上传的表单界面。
#### Scenario: 版本信息输入
- **WHEN** 用户打开上传新版本页面
- **THEN** 页面显示版本说明输入框和文件上传区域
#### Scenario: 文件上传展示
- **WHEN** 用户在上传页面看到文件上传区域
- **THEN** 页面展示拖拽上传区域,支持 .zip 格式,显示上传图标和提示文字
### Requirement: 版本历史展示
SkillEditorPage SHALL 展示技能的版本历史列表。
#### Scenario: 版本列表展示
- **WHEN** 用户打开技能详情页
- **THEN** 页面显示版本历史表格,包含版本号、版本说明、状态、更新时间、操作列
#### Scenario: 版本状态展示
- **WHEN** 用户查看版本历史列表
- **THEN** 每个版本显示对应的状态标签审核中warning、审核通过running、审核拒绝error、已撤销stopped
#### Scenario: 审核拒绝理由展示
- **WHEN** 版本状态为审核拒绝
- **THEN** 版本说明下方显示拒绝理由文本
### Requirement: 版本操作按钮
版本历史表格 SHALL 根据版本状态展示不同的操作按钮。
#### Scenario: 审核中版本操作
- **WHEN** 版本状态为审核中
- **THEN** 显示"撤回审核"、"下载"按钮
#### Scenario: 已完结版本操作
- **WHEN** 版本状态为审核通过/审核拒绝/已撤销
- **THEN** 仅显示"下载"按钮
### Requirement: 技能市场版本展示
SkillDetailPage SHALL 展示技能的最新版本信息。
#### Scenario: 最新版本展示
- **WHEN** 用户打开技能市场详情页
- **THEN** 页面显示当前版本号、更新说明、更新时间(仅展示最新版本,不展示完整历史)

View File

@@ -25,6 +25,8 @@ export const ADMIN_PAGES = {
users: { title: '用户管理', icon: 'FiUsers' },
projects: { title: '项目管理', icon: 'FiList' },
adminLogs: { title: '日志查询', icon: 'FiActivity' },
reviewList: { title: '审核管理', icon: 'FiCheckCircle' },
reviewDetail: { title: '审核详情', icon: null },
addDepartment: { title: '新增部门', icon: null },
addUser: { title: '新增用户', icon: null },
addProject: { title: '新增项目', icon: null },

View File

@@ -5,6 +5,7 @@ export const mySkills = [
desc: '根据城市名称查询当前天气和未来预报,支持全国主要城市',
icon: '🌤️',
status: 'published',
hasPendingReview: false,
version: '1.2.0',
category: '信息查询',
tags: ['天气', '查询', '生活'],
@@ -12,22 +13,18 @@ export const mySkills = [
installs: 156,
rating: 4.7,
versions: [
{ version: '1.2.0', date: '2026-03-18', desc: '新增支持未来7天预报', current: true, status: 'approved', enabled: true, rejectionReason: '' },
{ version: '1.1.0', date: '2026-03-10', desc: '优化响应速度', current: false, status: 'approved', enabled: false, rejectionReason: '' },
{ version: '1.0.0', date: '2026-03-01', desc: '初始版本', current: false, status: 'approved', enabled: false, rejectionReason: '' }
],
package: {
name: 'weather-assistant-v1.2.0.zip',
size: '2.4 MB',
uploadDate: '2026-03-18 14:30'
}
{ version: '1.2.0', date: '2026-03-18', desc: '新增支持未来7天预报', status: 'approved' },
{ version: '1.1.0', date: '2026-03-10', desc: '优化响应速度', status: 'approved' },
{ version: '1.0.0', date: '2026-03-01', desc: '初始版本', status: 'approved' }
]
},
{
id: 2,
name: '待办事项管理',
desc: '帮助用户管理日常待办事项,支持添加、完成、删除操作',
icon: '📋',
status: 'draft',
status: 'dev',
hasPendingReview: false,
version: '0.1.0',
category: '效率工具',
tags: ['待办', '管理', '效率'],
@@ -35,13 +32,8 @@ export const mySkills = [
installs: 0,
rating: 0,
versions: [
{ version: '0.1.0', date: '2026-03-17', desc: '开发中版本', current: true, status: 'pending', enabled: false, rejectionReason: '' }
],
package: {
name: 'todo-manager-v0.1.0.zip',
size: '1.8 MB',
uploadDate: '2026-03-17 10:15'
}
{ version: '0.1.0', date: '2026-03-17', desc: '开发中版本', status: 'reviewing' }
]
},
{
id: 3,
@@ -49,6 +41,7 @@ export const mySkills = [
desc: '自动审查代码质量,提供优化建议和潜在问题检测',
icon: '💻',
status: 'published',
hasPendingReview: true,
version: '2.0.1',
category: '开发工具',
tags: ['代码', '审查', '开发'],
@@ -56,16 +49,11 @@ export const mySkills = [
installs: 342,
rating: 4.9,
versions: [
{ version: '2.0.1', date: '2026-03-15', desc: '修复 Python 代码审查问题', current: true, status: 'approved', enabled: true, rejectionReason: '' },
{ version: '2.0.0', date: '2026-03-10', desc: '修复安全问题', current: false, status: 'rejected', enabled: false, rejectionReason: '安全审查未通过,存在潜在的代码注入风险,请修复后重新提交' },
{ version: '2.0.0', date: '2026-03-08', desc: '支持多语言审查', current: false, status: 'approved', enabled: false, rejectionReason: '' },
{ version: '1.0.0', date: '2026-02-20', desc: '初始版本', current: false, status: 'approved', enabled: false, rejectionReason: '' }
],
package: {
name: 'code-reviewer-v2.0.1.zip',
size: '3.2 MB',
uploadDate: '2026-03-15 16:45'
}
{ version: '2.0.1', date: '2026-03-15', desc: '修复 Python 代码审查问题', status: 'approved' },
{ version: '2.0.0', date: '2026-03-10', desc: '修复安全问题', status: 'rejected', rejectionReason: '安全审查未通过,存在潜在的代码注入风险,请修复后重新提交' },
{ version: '2.0.0', date: '2026-03-08', desc: '支持多语言审查', status: 'approved' },
{ version: '1.0.0', date: '2026-02-20', desc: '初始版本', status: 'approved' }
]
}
];

View File

@@ -1,12 +1,12 @@
// skills data
export const skills = [
{ id: 1, name: '代码生成助手', author: 'GrandClaw Team', desc: '根据需求自动生成高质量代码,支持多种编程语言', tags: ['开发', '代码', 'AI'], subs: 1256, rating: 4.8, subscribed: true },
{ id: 2, name: '数据分析专家', author: 'DataLab', desc: '智能分析数据,生成可视化图表和洞察报告', tags: ['数据', '分析', '可视化'], subs: 892, rating: 4.7, subscribed: true },
{ id: 3, name: '文档智能撰写', author: 'DocAI', desc: '帮助撰写各种文档,包括报告、邮件、技术文档等', tags: ['文档', '写作', '办公'], subs: 2103, rating: 4.9, subscribed: true },
{ id: 4, name: 'CRM 客户查询', author: 'Telecom', desc: '对接企业CRM系统快速查询客户信息和订单状态', tags: ['业务', 'CRM', '客户'], subs: 567, rating: 4.5, subscribed: false },
{ id: 5, name: '财务数据同步', author: 'Finance Team', desc: '自动同步财务系统数据,生成费用报表', tags: ['财务', '报表', '同步'], subs: 432, rating: 4.6, subscribed: false },
{ id: 6, name: '网络故障排查', author: 'NetOps', desc: '智能诊断网络问题,提供故障排除方案', tags: ['运维', '网络', '诊断'], subs: 789, rating: 4.8, subscribed: false }
{ id: 1, name: '代码生成助手', author: 'GrandClaw Team', desc: '根据需求自动生成高质量代码,支持多种编程语言', tags: ['开发', '代码', 'AI'], subs: 1256, rating: 4.8, subscribed: true, status: 'published', hasPendingReview: false },
{ id: 2, name: '数据分析专家', author: 'DataLab', desc: '智能分析数据,生成可视化图表和洞察报告', tags: ['数据', '分析', '可视化'], subs: 892, rating: 4.7, subscribed: true, status: 'published', hasPendingReview: true },
{ id: 3, name: '文档智能撰写', author: 'DocAI', desc: '帮助撰写各种文档,包括报告、邮件、技术文档等', tags: ['文档', '写作', '办公'], subs: 2103, rating: 4.9, subscribed: true, status: 'dev', hasPendingReview: false },
{ id: 4, name: 'CRM 客户查询', author: 'Telecom', desc: '对接企业CRM系统快速查询客户信息和订单状态', tags: ['业务', 'CRM', '客户'], subs: 567, rating: 4.5, subscribed: false, status: 'unlisting', hasPendingReview: false },
{ id: 5, name: '财务数据同步', author: 'Finance Team', desc: '自动同步财务系统数据,生成费用报表', tags: ['财务', '报表', '同步'], subs: 432, rating: 4.6, subscribed: false, status: 'unlisted', hasPendingReview: false },
{ id: 6, name: '网络故障排查', author: 'NetOps', desc: '智能诊断网络问题,提供故障排除方案', tags: ['运维', '网络', '诊断'], subs: 789, rating: 4.8, subscribed: false, status: 'published', hasPendingReview: false }
];
export const skillFiles = [
@@ -17,10 +17,21 @@ export const skillFiles = [
];
export const skillVersions = [
{ version: 'v1.3.0', date: '2026-03-12', desc: '新增 Python 3.11 支持', current: true },
{ version: 'v1.2.1', date: '2026-03-08', desc: '修复若干已知问题', current: false },
{ version: 'v1.2.0', date: '2026-03-01', desc: '优化性能,提升响应速度 30%', current: false },
{ version: 'v1.1.0', date: '2026-02-15', desc: '新增 JavaScript 支持', current: false }
{ version: 'v1.3.0', date: '2026-03-12', desc: '新增 Python 3.11 支持', status: 'approved' },
{ version: 'v1.2.1', date: '2026-03-08', desc: '修复若干已知问题', status: 'rejected', rejectionReason: '测试用例覆盖不完整,请补充单元测试' },
{ version: 'v1.2.0', date: '2026-03-01', desc: '优化性能,提升响应速度 30%', status: 'approved' },
{ version: 'v1.1.5', date: '2026-02-20', desc: '紧急修复安全漏洞', status: 'withdrawn' },
{ version: 'v1.1.0', date: '2026-02-15', desc: '新增 JavaScript 支持', status: 'reviewing' }
];
export const pendingVersionReviews = [
{ id: 1, skillName: '代码生成助手', version: 'v1.4.0', date: '2026-03-20', developer: '张三' },
{ id: 2, skillName: '数据分析专家', version: 'v2.0.0', date: '2026-03-19', developer: '李四' },
{ id: 3, skillName: '文档智能撰写', version: 'v1.0.0', date: '2026-03-18', developer: '王五' }
];
export const pendingUnlistReviews = [
{ id: 1, skillName: 'CRM 客户查询', currentVersion: 'v1.5.0', date: '2026-03-20', developer: '赵六' }
];
// 技能图标映射
@@ -28,4 +39,4 @@ const skillIcons = ['💻', '📊', '📝', '👥', '📈', '🔧'];
export function getSkillIcon(id) {
return skillIcons[(id - 1) % skillIcons.length];
}
}

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { FiHome, FiBarChart2, FiUsers, FiList, FiActivity } from 'react-icons/fi';
import { FiHome, FiBarChart2, FiUsers, FiList, FiCheckCircle, FiActivity } from 'react-icons/fi';
import Layout from '../components/Layout.jsx';
import SidebarBrand from '../components/layout/SidebarBrand.jsx';
import SidebarUser from '../components/layout/SidebarUser.jsx';
@@ -16,6 +16,8 @@ import AddDepartmentPage from './admin/AddDepartmentPage.jsx';
import AddUserPage from './admin/AddUserPage.jsx';
import AddProjectPage from './admin/AddProjectPage.jsx';
import AdminLogsPage from './admin/AdminLogsPage.jsx';
import ConsoleReviewListPage from './console/ConsoleReviewListPage.jsx';
import ConsoleReviewDetailPage from './console/ConsoleReviewDetailPage.jsx';
function AdminPage() {
const location = useLocation();
@@ -28,12 +30,26 @@ function AdminPage() {
});
const [editData, setEditData] = useState(null);
const [reviewType, setReviewType] = useState(null);
const [reviewId, setReviewId] = useState(null);
const navigateTo = (page, data) => {
setEditData(data || null);
setCurrentPage(page);
};
const handleReviewClick = (type, id) => {
setReviewType(type);
setReviewId(id);
navigateTo('reviewDetail');
};
const handleReviewBack = () => {
setReviewType(null);
setReviewId(null);
navigateTo('reviewList');
};
const renderPage = () => {
switch (currentPage) {
case 'overview':
@@ -55,6 +71,14 @@ function AdminPage() {
/>;
case 'adminLogs':
return <AdminLogsPage />;
case 'reviewList':
return <ConsoleReviewListPage onReviewClick={handleReviewClick} />;
case 'reviewDetail':
return <ConsoleReviewDetailPage
type={reviewType}
reviewId={reviewId}
onBack={handleReviewBack}
/>;
case 'addDepartment':
return <AddDepartmentPage
onBack={() => navigateTo('departments')}
@@ -81,6 +105,9 @@ function AdminPage() {
const nameMap = { addDepartment: '部门', addUser: '用户', addProject: '项目' };
return prefix + nameMap[currentPage];
}
if (currentPage === 'reviewDetail') {
return reviewType === 'version' ? '版本审核' : '下架审核';
}
return ADMIN_PAGES[currentPage]?.title || '';
};
@@ -99,6 +126,15 @@ function AdminPage() {
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
/>
<SidebarNavItem
icon={<FiCheckCircle />}
label="审核管理"
active={currentPage === 'reviewList' || currentPage === 'reviewDetail'}
onClick={() => navigateTo('reviewList')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
/>
<SidebarNavItem
icon={<FiBarChart2 />}
label="部门管理"

View File

@@ -18,6 +18,14 @@ import DevDocsPage from './developer/DevDocsPage.jsx';
import DevAccountPage from './developer/DevAccountPage.jsx';
import SkillEditorPage from './developer/SkillEditorPage.jsx';
import UpdateSkillInfoPage from './developer/UpdateSkillInfoPage.jsx';
import UploadVersionPage from './developer/UploadVersionPage.jsx';
const skillStatusMap = {
dev: { text: '开发中', className: 'status-stopped' },
published: { text: '已上架', className: 'status-running' },
unlisting: { text: '下架审核中', className: 'status-warning' },
unlisted: { text: '已下架', className: 'status-stopped' }
};
function DeveloperPage() {
const location = useLocation();
@@ -105,7 +113,7 @@ function DeveloperPage() {
onUpdateInfo={openUpdateInfoPage}
/>;
case 'newVersion':
return <NewVersionPage skillName={newVersionSkillName} onBack={handleEditorBack} />;
return <UploadVersionPage skillName={newVersionSkillName} onBack={handleEditorBack} />;
case 'updateInfo':
return <UpdateSkillInfoPage
skill={api.developer.getSkillById(currentSkillId)}
@@ -139,7 +147,11 @@ function DeveloperPage() {
onClick={() => openSkillEditor(skill.id)}
>
<div className="conversation-title">{skill.name}</div>
<div className="conversation-time">{skill.status === 'published' ? '已发布' : '草稿'}</div>
<div className="conversation-time">
<span className={`status ${skillStatusMap[skill.status]?.className || 'status-stopped'}`}>
{skillStatusMap[skill.status]?.text || skill.status}
</span>
</div>
</div>
))}
</div>

View File

@@ -0,0 +1,146 @@
import { useState } from 'react';
import { FiChevronLeft, FiFile } from 'react-icons/fi';
import { pendingVersionReviews, pendingUnlistReviews, skillFiles } from '../../data/skills.js';
import Toast from '../../components/common/Toast.jsx';
function ConsoleReviewDetailPage({ type, reviewId, onBack }) {
const [showToast, setShowToast] = useState(false);
const [toastMessage, setToastMessage] = useState('');
const versionReview = type === 'version' ? pendingVersionReviews.find(r => r.id === reviewId) : null;
const unlistReview = type === 'unlist' ? pendingUnlistReviews.find(r => r.id === reviewId) : null;
const review = versionReview || unlistReview;
if (!review) {
return <div>审核项不存在</div>;
}
const handleApprove = () => {
setToastMessage('审核通过');
setShowToast(true);
setTimeout(() => {
onBack && onBack();
}, 1000);
};
const handleReject = () => {
setToastMessage('已拒绝');
setShowToast(true);
setTimeout(() => {
onBack && onBack();
}, 1000);
};
return (
<>
<div className="console-back-btn" onClick={onBack}>
<FiChevronLeft /> 返回审核列表
</div>
<div className="card">
<div className="card-header">
<div className="card-title">{type === 'version' ? '版本审核' : '下架审核'}</div>
</div>
<div className="card-body">
{type === 'version' && (
<>
<div className="dev-detail-section">
<h3>基本信息</h3>
<div className="dev-info-row">
<span className="dev-info-label">技能名称</span>
<span className="dev-info-value">{review.skillName}</span>
</div>
<div className="dev-info-row">
<span className="dev-info-label">开发者</span>
<span className="dev-info-value">{review.developer}</span>
</div>
<div className="dev-info-row">
<span className="dev-info-label">分类</span>
<span className="dev-info-value">开发工具</span>
</div>
<div className="dev-info-row">
<span className="dev-info-label">标签</span>
<span className="dev-info-value">
<span className="dev-detail-tag" style={{ marginRight: '6px' }}>开发</span>
<span className="dev-detail-tag" style={{ marginRight: '6px' }}>代码</span>
<span className="dev-detail-tag">AI</span>
</span>
</div>
</div>
<div className="dev-detail-section">
<h3>版本信息</h3>
<div className="dev-info-row">
<span className="dev-info-label">版本号</span>
<span className="dev-info-value">{review.version}</span>
</div>
<div className="dev-info-row">
<span className="dev-info-label">提交时间</span>
<span className="dev-info-value">{review.date}</span>
</div>
<div className="dev-info-row">
<span className="dev-info-label">版本说明</span>
<span className="dev-info-value">优化性能提升响应速度 30%修复若干已知问题</span>
</div>
</div>
<div className="dev-detail-section">
<h3>文件列表</h3>
{skillFiles.map((file, index) => (
<div key={index} className="file-list-item">
<div className="file-icon"><FiFile /></div>
<div className="file-info">
<div className="file-name">{file.name}</div>
<div className="file-size">{file.type} · {file.size}</div>
</div>
</div>
))}
</div>
</>
)}
{type === 'unlist' && (
<div className="dev-detail-section">
<h3>技能信息</h3>
<div className="dev-info-row">
<span className="dev-info-label">技能名称</span>
<span className="dev-info-value">{review.skillName}</span>
</div>
<div className="dev-info-row">
<span className="dev-info-label">开发者</span>
<span className="dev-info-value">{review.developer}</span>
</div>
<div className="dev-info-row">
<span className="dev-info-label">当前版本</span>
<span className="dev-info-value">{review.currentVersion}</span>
</div>
<div className="dev-info-row">
<span className="dev-info-label">订阅数</span>
<span className="dev-info-value">567</span>
</div>
<div className="dev-info-row">
<span className="dev-info-label">申请时间</span>
<span className="dev-info-value">{review.date}</span>
</div>
</div>
)}
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px', paddingTop: '16px', borderTop: '1px solid #E2E8F0' }}>
<button className="btn btn-danger" onClick={handleReject}>拒绝</button>
<button className="btn btn-success" onClick={handleApprove}>通过</button>
</div>
</div>
</div>
<Toast
visible={showToast}
type="success"
message={toastMessage}
onClose={() => setShowToast(false)}
/>
</>
);
}
export default ConsoleReviewDetailPage;

View File

@@ -0,0 +1,110 @@
import { useState } from 'react';
import { pendingVersionReviews, pendingUnlistReviews } from '../../data/skills.js';
function ConsoleReviewListPage({ onReviewClick }) {
const [activeTab, setActiveTab] = useState('version');
return (
<>
<div className="card">
<div className="card-header">
<div className="card-title">审核管理</div>
</div>
<div className="card-body">
<div style={{ marginBottom: '16px' }}>
<div className="btn-group">
<button
className={`btn ${activeTab === 'version' ? 'btn-primary' : ''}`}
onClick={() => setActiveTab('version')}
>
版本审核
</button>
<button
className={`btn ${activeTab === 'unlist' ? 'btn-primary' : ''}`}
onClick={() => setActiveTab('unlist')}
>
下架审核
</button>
</div>
</div>
{activeTab === 'version' && (
<div className="table-wrapper">
<table className="table">
<thead>
<tr>
<th>技能名称</th>
<th>版本号</th>
<th>提交时间</th>
<th>开发者</th>
<th style={{ width: '100px' }}>操作</th>
</tr>
</thead>
<tbody>
{pendingVersionReviews.map(review => (
<tr key={review.id}>
<td style={{ fontWeight: 600 }}>{review.skillName}</td>
<td>{review.version}</td>
<td>{review.date}</td>
<td>{review.developer}</td>
<td>
<button
className="text-btn text-btn-primary"
onClick={() => onReviewClick('version', review.id)}
>
审核
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{activeTab === 'unlist' && (
<div className="table-wrapper">
<table className="table">
<thead>
<tr>
<th>技能名称</th>
<th>当前版本</th>
<th>申请时间</th>
<th>开发者</th>
<th style={{ width: '100px' }}>操作</th>
</tr>
</thead>
<tbody>
{pendingUnlistReviews.map(review => (
<tr key={review.id}>
<td style={{ fontWeight: 600 }}>{review.skillName}</td>
<td>{review.currentVersion}</td>
<td>{review.date}</td>
<td>{review.developer}</td>
<td>
<button
className="text-btn text-btn-primary"
onClick={() => onReviewClick('unlist', review.id)}
>
审核
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<div className="pagination">
<div className="pagination-item"></div>
<div className="pagination-item active">1</div>
<div className="pagination-item"></div>
</div>
</div>
</div>
</>
);
}
export default ConsoleReviewListPage;

View File

@@ -79,16 +79,21 @@ function SkillDetailPage({ skillId, onBack }) {
))}
</div>
<div className="skill-detail-section">
<h3>版本历史</h3>
{skillVersions.map(ver => (
<div key={ver.version} className="version-list-item">
<div className="version-info">
<span className={`version-tag ${ver.current ? 'current' : ''}`}>{ver.version}</span>
<span className="version-desc">{ver.desc}</span>
<h3>当前版本</h3>
{(() => {
const approvedVersion = skillVersions.find(v => v.status === 'approved');
return approvedVersion ? (
<div className="version-list-item">
<div className="version-info">
<span className="version-tag current">{approvedVersion.version}</span>
<span className="version-desc">{approvedVersion.desc}</span>
</div>
<div className="version-date">{approvedVersion.date}</div>
</div>
<div className="version-date">{ver.date}</div>
</div>
))}
) : (
<div style={{ color: '#94A3B8' }}>暂无版本信息</div>
);
})()}
</div>
</div>
</div>

View File

@@ -3,6 +3,13 @@ import { api } from '../../services/api.js';
import Modal from '../../components/common/Modal.jsx';
import Toast from '../../components/common/Toast.jsx';
const skillStatusMap = {
dev: { text: '开发中', className: 'status-stopped' },
published: { text: '已上架', className: 'status-running' },
unlisting: { text: '下架审核中', className: 'status-warning' },
unlisted: { text: '已下架', className: 'status-stopped' }
};
function MySkillsPage({ onSkillClick }) {
const sourceData = api.developer.getMySkills();
const categories = api.developer.getCategories();
@@ -25,8 +32,7 @@ function MySkillsPage({ onSkillClick }) {
if (filters.category && skill.category !== filters.category) {
return false;
}
if (filters.status === 'published' && skill.status !== 'published') return false;
if (filters.status === 'draft' && skill.status !== 'draft') return false;
if (filters.status && skill.status !== filters.status) return false;
return true;
});
@@ -81,8 +87,10 @@ function MySkillsPage({ onSkillClick }) {
onChange={e => handleFilterChange('status', e.target.value)}
>
<option value="">全部</option>
<option value="published">已发布</option>
<option value="draft">草稿</option>
<option value="dev">开发中</option>
<option value="published">已上架</option>
<option value="unlisting">下架审核中</option>
<option value="unlisted">已下架</option>
</select>
</div>
</div>
@@ -120,9 +128,12 @@ function MySkillsPage({ onSkillClick }) {
<td>{skill.category}</td>
<td>{skill.version}</td>
<td>
<span className={`status ${skill.status === 'published' ? 'status-running' : 'status-stopped'}`}>
{skill.status === 'published' ? '已发布' : '草稿'}
<span className={`status ${skillStatusMap[skill.status]?.className || 'status-stopped'}`}>
{skillStatusMap[skill.status]?.text || skill.status}
</span>
{skill.hasPendingReview && (
<span className="status status-warning" style={{ marginLeft: '6px' }}>审核中</span>
)}
</td>
<td>{skill.installs}</td>
<td>{skill.rating || '-'}</td>
@@ -132,11 +143,21 @@ function MySkillsPage({ onSkillClick }) {
编辑
</button>
{skill.status === 'published' && (
<button className="text-btn text-btn-danger" onClick={e => handleUnpublish(e, skill)}>
<button
className="text-btn text-btn-danger"
onClick={e => handleUnpublish(e, skill)}
disabled={skill.hasPendingReview}
title={skill.hasPendingReview ? '存在审核中的版本,请先撤回后再下架' : ''}
>
下架
</button>
)}
<button className="text-btn text-btn-danger" onClick={e => handleDelete(e, skill)}>
<button
className="text-btn text-btn-danger"
onClick={e => handleDelete(e, skill)}
disabled={skill.status === 'published'}
title={skill.status === 'published' ? '已上架的技能需要先下架才能删除' : ''}
>
删除
</button>
</div>

View File

@@ -4,6 +4,20 @@ import { api } from '../../services/api.js';
import Modal from '../../components/common/Modal.jsx';
import Toast from '../../components/common/Toast.jsx';
const versionStatusMap = {
reviewing: { text: '审核中', className: 'status-warning' },
approved: { text: '审核通过', className: 'status-running' },
rejected: { text: '审核拒绝', className: 'status-error' },
withdrawn: { text: '已撤销', className: 'status-stopped' }
};
const skillStatusMap = {
dev: { text: '开发中', className: 'status-stopped' },
published: { text: '已上架', className: 'status-running' },
unlisting: { text: '下架审核中', className: 'status-warning' },
unlisted: { text: '已下架', className: 'status-stopped' }
};
function SkillEditorPage({ skillId, onBack, onUploadNewVersion, onUpdateInfo }) {
const skill = api.developer.getSkillById(skillId);
const [deleteSkillModal, setDeleteSkillModal] = useState(false);
@@ -50,9 +64,7 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion, onUpdateInfo })
))}
</div>
<div className="dev-detail-stats">
<span>版本: {skill.version}</span>
<span>安装量: {skill.installs}</span>
<span>评分: {skill.rating || '-'}</span>
<span>当前版本: {skill.version}</span>
</div>
</div>
</div>
@@ -78,13 +90,20 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion, onUpdateInfo })
))}
</span>
</div>
<div style={{ display: 'flex', gap: '12px', marginTop: '20px', paddingTop: '16px', borderTop: '1px solid #E2E8F0' }}>
<button className="btn btn-primary" onClick={() => onUpdateInfo && onUpdateInfo(skill.id)}>更新基本信息</button>
<button className={`btn ${skill.status === 'published' ? 'btn-danger' : 'btn-success'}`} onClick={handleTogglePublish}>
{skill.status === 'published' ? '下架技能' : '上架技能'}
</button>
<button className="btn btn-danger" onClick={() => setDeleteSkillModal(true)}>删除技能</button>
</div>
<div style={{ display: 'flex', gap: '12px', marginBottom: '24px' }}>
<button className="btn btn-primary" onClick={() => onUpdateInfo && onUpdateInfo(skill.id)}>更新基本信息</button>
{skill.status === 'published' && (
<button className="btn btn-danger" onClick={handleTogglePublish}>下架技能</button>
)}
<button
className="btn btn-danger"
onClick={() => setDeleteSkillModal(true)}
disabled={skill.status === 'published'}
title={skill.status === 'published' ? '已上架的技能需要先下架才能删除' : ''}
>
删除技能
</button>
</div>
</div>
</div>
</div>
@@ -94,7 +113,14 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion, onUpdateInfo })
</div>
<div className="card-body">
<div style={{ display: 'flex', gap: '12px', marginBottom: '24px' }}>
<button className="btn btn-primary" onClick={() => onUploadNewVersion(skill.name)}><FiUpload /> 上传新版本</button>
<button
className="btn btn-primary"
onClick={() => onUploadNewVersion(skill.name)}
disabled={skill.hasPendingReview}
title={skill.hasPendingReview ? '存在审核中的版本,请先撤回后再上传新版本' : ''}
>
<FiUpload /> 上传新版本
</button>
</div>
<h4 style={{ marginBottom: '12px' }}>版本历史</h4>
<div className="table-wrapper" style={{ margin: 0, padding: 0 }}>
@@ -104,8 +130,7 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion, onUpdateInfo })
<col />
<col style={{ width: '120px' }} />
<col style={{ width: '120px' }} />
<col style={{ width: '100px' }} />
<col style={{ width: '240px' }} />
<col style={{ width: '180px' }} />
</colgroup>
<thead>
<tr>
@@ -113,7 +138,6 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion, onUpdateInfo })
<th>版本说明</th>
<th>状态</th>
<th>更新时间</th>
<th>是否启用</th>
<th>操作</th>
</tr>
</thead>
@@ -128,30 +152,21 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion, onUpdateInfo })
)}
</td>
<td>
{ver.status === 'pending' ? (
<span className="status status-warning">审核中</span>
) : ver.status === 'rejected' ? (
<span className="status status-error">审核拒绝</span>
) : (
<span className="status status-running">审核通过</span>
)}
<span className={`status ${versionStatusMap[ver.status]?.className || 'status-stopped'}`}>
{versionStatusMap[ver.status]?.text || ver.status}
</span>
</td>
<td>{ver.date}</td>
<td>
{ver.enabled ? (
<span className="status status-running">已启用</span>
) : (
<span className="status status-stopped">未启用</span>
)}
</td>
<td>
<div className="btn-group">
{!ver.enabled && <button className="text-btn text-btn-success">启用</button>}
<button className="text-btn">下载</button>
{!ver.enabled && (
<button className="text-btn text-btn-danger" onClick={() => setDeleteVersionTarget(ver)}>
删除
</button>
{ver.status === 'reviewing' && (
<>
<button className="text-btn text-btn-warning">撤回审核</button>
<button className="text-btn">下载</button>
</>
)}
{(ver.status === 'approved' || ver.status === 'rejected' || ver.status === 'withdrawn') && (
<button className="text-btn">下载</button>
)}
</div>
</td>

View File

@@ -1,4 +1,4 @@
import { FiUpload, FiX } from 'react-icons/fi';
import { FiX } from 'react-icons/fi';
import { useState } from 'react';
import { api } from '../../services/api.js';
import Toast from '../../components/common/Toast.jsx';
@@ -87,14 +87,6 @@ function UploadSkillPage({ onBack }) {
</div>
<div style={{ fontSize: '12px', color: '#94A3B8', marginTop: '6px' }}>当前选择: {icon}</div>
</div>
<div className="form-group">
<label className="form-label required">技能包上传</label>
<div style={{ border: '2px dashed #E2E8F0', borderRadius: '8px', padding: '40px', textAlign: 'center', color: '#94A3B8' }}>
<FiUpload size={48} style={{ marginBottom: '16px' }} />
<div>点击或拖拽文件到此处上传</div>
<div style={{ fontSize: '12px', marginTop: '8px' }}>支持 .zip 格式</div>
</div>
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
<button className="btn" onClick={onBack}>取消</button>
<button className="btn btn-primary" onClick={handleCreate}>创建技能</button>

View File

@@ -0,0 +1,63 @@
import { FiUpload, FiChevronLeft } from 'react-icons/fi';
import { useState } from 'react';
import Toast from '../../components/common/Toast.jsx';
function UploadVersionPage({ skillName, onBack }) {
const [showToast, setShowToast] = useState(false);
const handleSubmit = () => {
setShowToast(true);
setTimeout(() => {
onBack();
}, 1000);
};
return (
<>
<div className="dev-back-btn" onClick={onBack}>
<FiChevronLeft /> 返回技能详情
</div>
<div className="card">
<div className="card-header">
<div className="card-title">上传新版本</div>
</div>
<div className="card-body">
<div style={{ marginBottom: '16px', color: '#64748B' }}>
技能: {skillName}
</div>
<div className="form-group">
<label className="form-label required">版本说明</label>
<textarea
className="form-control"
rows="4"
placeholder="请输入版本更新说明,描述本次更新的内容..."
/>
</div>
<div className="form-group">
<label className="form-label required">技能包上传</label>
<div style={{ border: '2px dashed #E2E8F0', borderRadius: '8px', padding: '40px', textAlign: 'center', color: '#94A3B8', cursor: 'pointer' }}>
<FiUpload size={48} style={{ marginBottom: '16px' }} />
<div>点击或拖拽文件到此处上传</div>
<div style={{ fontSize: '12px', marginTop: '8px' }}>支持 .zip 格式</div>
</div>
</div>
<div style={{ fontSize: '12px', color: '#94A3B8', marginBottom: '16px' }}>
版本号将由系统自动生成
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
<button className="btn" onClick={onBack}>取消</button>
<button className="btn btn-primary" onClick={handleSubmit}>提交审核</button>
</div>
</div>
</div>
<Toast
visible={showToast}
type="success"
message="提交成功,等待审核"
onClose={() => setShowToast(false)}
/>
</>
);
}
export default UploadVersionPage;

View File

@@ -124,11 +124,20 @@ export const developerApi = {
getMySkills: () => mySkills,
/**
* 根据 ID 获取技能详情
* @param {number} id - 技能 ID
* @returns {Object|undefined} 技能对象
*/
getSkillById: (id) => mySkills.find(skill => skill.id === id),
* 根据 ID 获取技能详情
* @param {number} id - 技能 ID
* @returns {Object|undefined} 技能对象
*/
getSkillById: (id) => {
const skill = mySkills.find(skill => skill.id === id);
if (!skill) {
return undefined;
}
return {
...skill,
versions: skill.versions || []
};
},
/**
* 获取技能分类