feat: 实现技能审核全流程 - 新增审核管理模块、技能状态机、版本审核机制
- 新增审核管理页面:版本审核列表、下架审核列表、审核详情页 - 完善技能状态机:开发中/已上架/下架审核中/已下架四种状态 - 实现版本审核机制:审核中/通过/拒绝/撤销四种状态 - 更新 README:详细记录技能开发流程与审核机制 - 优化技能详情页:根据状态展示不同操作按钮 - 完善我的技能列表:状态筛选与操作限制 - 新增上传新版本页面:分离版本上传与基本信息编辑 - 更新 openspec 规范:技能审核流程与状态定义
This commit is contained in:
295
README.md
295
README.md
@@ -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*
|
||||
|
||||
@@ -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 展示开发者最近的操作动态记录。
|
||||
|
||||
|
||||
42
openspec/specs/skill-review-workflow/spec.md
Normal file
42
openspec/specs/skill-review-workflow/spec.md
Normal 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** 页面底部显示"拒绝"和"通过"两个按钮
|
||||
45
openspec/specs/skill-status-display/spec.md
Normal file
45
openspec/specs/skill-status-display/spec.md
Normal 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** "上传新版本"按钮禁用并提示"存在审核中的版本,请先撤回后再上传新版本"
|
||||
45
openspec/specs/skill-version-management/spec.md
Normal file
45
openspec/specs/skill-version-management/spec.md
Normal 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** 页面显示当前版本号、更新说明、更新时间(仅展示最新版本,不展示完整历史)
|
||||
@@ -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 },
|
||||
|
||||
@@ -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' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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="部门管理"
|
||||
|
||||
@@ -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>
|
||||
|
||||
146
src/pages/console/ConsoleReviewDetailPage.jsx
Normal file
146
src/pages/console/ConsoleReviewDetailPage.jsx
Normal 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;
|
||||
110
src/pages/console/ConsoleReviewListPage.jsx
Normal file
110
src/pages/console/ConsoleReviewListPage.jsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
63
src/pages/developer/UploadVersionPage.jsx
Normal file
63
src/pages/developer/UploadVersionPage.jsx
Normal 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;
|
||||
@@ -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 || []
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取技能分类
|
||||
|
||||
Reference in New Issue
Block a user