refactor: 完成技能数据结构重构 - 分离内部信息与发布信息
- 新增技能内部信息与商店发布信息分离的数据结构 - 重构技能详情页为四段式布局(概览+当前生效版本+版本历史+管理) - 移除历史版本中的下载按钮 - 版本历史改为卡片布局,新增发布信息预览 - 分类与标签合并显示,分类作为第一个标签 - 更新按钮禁用逻辑:下架审核中/已下架状态禁用上传新版本 - 下架技能按钮添加二次确认弹窗 - 补充10个不同状态的技能示例数据 - 同步 delta specs 到主 specs - 归档变更:refactor-skill-data-structure
This commit is contained in:
29
README.md
29
README.md
@@ -182,14 +182,33 @@ pnpm build
|
||||
|
||||
### 5. 开发台(Developer)
|
||||
- **总览**:开发者指标卡片(我的技能总数、已上架、开发中、待审核)、待审核项目列表、最近动态
|
||||
- **我的技能**:技能列表,支持关键词搜索、分类筛选、状态筛选(开发中/已上架/下架审核中/已下架)、分页,支持下架(需要先撤回审核中的版本)、删除(已上架需要先下架)
|
||||
- **技能详情**:基本信息只读展示、版本历史管理(根据状态展示操作按钮:审核中-撤回审核+下载、审核通过/拒绝/撤销-仅下载、审核拒绝-显示拒绝理由)、技能操作(上传新版本-有审核中版本时禁用、下架技能、删除技能-已上架时禁用)
|
||||
- **创建技能**:基本信息表单 + 技能图标选择(移除技能包上传)
|
||||
- **更新基本信息**:独立页面编辑技能名称/描述/分类/标签/图标,与版本上传分离
|
||||
- **上传新版本**:仅包含版本说明和技能包上传(NEW)
|
||||
- **我的技能**:技能列表,支持关键词搜索(内部名称/内部描述)、状态筛选(开发中/已上架/下架审核中/已下架)、分页,支持下架(需要先撤回审核中的版本)、删除(已上架需要先下架),仅展示开发者内部信息
|
||||
- **技能详情**:四段式布局 - 1) 概览卡片(内部信息:内部名称、状态、编辑按钮);2) 当前生效版本卡片(商店展示效果预览,分类作为第一个标签显示);3) 版本历史卡片(普通卡片布局,展示版本号、状态、日期、版本说明、发布信息预览、操作按钮,无下载按钮);4) 管理操作卡片
|
||||
- **创建技能**:简化表单 - 仅内部技能名称、内部技能描述(明确标注仅供开发者管理使用)
|
||||
- **编辑内部信息**:独立页面编辑内部技能名称/内部技能描述(明确标注不影响商店展示)
|
||||
- **上传新版本**:增强表单 - 版本说明区域 + 发布信息区域(技能发布名称、技能发布描述、分类、标签、图标),非首版本默认继承当前生效版本的值
|
||||
- **开发文档**:技能开发相关文档
|
||||
- **开发者设置**:开发者账号信息
|
||||
|
||||
#### 重要数据结构变更说明
|
||||
- **开发者内部信息**:内部名称、内部描述 - 仅供开发者管理,与商店展示完全无关,可随时修改无需审核
|
||||
- **版本发布信息**:发布名称、发布描述、分类、标签、图标 - 存储在版本中,随版本审核通过后生效,修改必须发布新版本
|
||||
- **技能商店展示**:完全从当前生效版本取发布信息,确保任何商店内容变更都经过版本审核
|
||||
- **分类与标签展示**:分类始终作为第一个标签显示,与普通标签一起展示
|
||||
|
||||
#### 按钮禁用规则
|
||||
基于 `hasPendingReview` 标志和技能状态控制操作按钮可用性:
|
||||
- **上传新版本按钮**:`status === 'unlisting' || status === 'unlisted' || hasPendingReview === true` 时禁用
|
||||
- **下架技能按钮**:`hasPendingReview === true` 时禁用
|
||||
- **删除技能按钮**:`status === 'published' || status === 'unlisting' || hasPendingReview === true` 时禁用
|
||||
|
||||
#### 撤回审核按钮样式
|
||||
- **按钮类型**:警告按钮(橙色)
|
||||
- **按钮类名**:`btn btn-warning btn-sm`
|
||||
- **按钮图标**:逆时针旋转图标(FiRotateCcw)
|
||||
- **按钮文案**:"撤回审核"
|
||||
- **展示位置**:版本历史卡片中审核中版本的操作区域
|
||||
|
||||
## 路由结构
|
||||
|
||||
项目使用 **HashRouter**,所有路由基于哈希路径,支持直接打开HTML文件运行。
|
||||
|
||||
121
docs/审核流程.md
121
docs/审核流程.md
@@ -42,30 +42,34 @@ flowchart TD
|
||||
## 二、状态定义
|
||||
|
||||
### 2.1 技能状态
|
||||
| 状态 | 英文标识 | 描述 | 样式 | 说明 |
|
||||
|------|---------|------|------|------|
|
||||
| 开发中 | `dev` | 技能已创建但尚未有审核通过的版本 | 灰色 | 仅开发者可见,不在市场展示 |
|
||||
| 已上架 | `published` | 技能有审核通过的版本,可公开访问 | 绿色 | 在技能市场展示,用户可订阅使用 |
|
||||
| 下架审核中 | `unlisting` | 开发者申请下架,等待管理员审核 | 黄色 | 仅允许撤回下架申请,其他操作禁用 |
|
||||
| 已下架 | `unlisted` | 技能已下架,不在技能市场展示 | 灰色 | 仅允许删除操作 |
|
||||
| 状态 | 英文标识 | 描述 | 说明 |
|
||||
|------|---------|------|------|
|
||||
| 开发中 | `dev` | 技能已创建但尚未有审核通过的版本 | 仅开发者可见,不在市场展示 |
|
||||
| 已上架 | `published` | 技能有审核通过的版本,可公开访问 | 在技能市场展示,用户可订阅使用 |
|
||||
| 下架审核中 | `unlisting` | 开发者申请下架,等待管理员审核 | 仅允许撤回下架申请,其他操作禁用 |
|
||||
| 已下架 | `unlisted` | 技能已下架,不在技能市场展示 | 仅允许删除操作 |
|
||||
|
||||
### 2.2 版本状态
|
||||
| 状态 | 英文标识 | 描述 | 样式 | 说明 |
|
||||
|------|---------|------|------|------|
|
||||
| 审核中 | `reviewing` | 版本已提交,等待管理员审核 | 黄色 | 正在审核流程中 |
|
||||
| 审核通过 | `approved` | 版本审核通过,自动生效 | 绿色 | 成为当前生效版本 |
|
||||
| 审核拒绝 | `rejected` | 版本审核未通过 | 红色 | 需要修改后重新上传新版本 |
|
||||
| 已撤销 | `withdrawn` | 开发者主动撤回审核 | 灰色 | 版本废弃,无法恢复 |
|
||||
| 状态 | 英文标识 | 描述 | 说明 |
|
||||
|------|---------|------|------|
|
||||
| 审核中 | `reviewing` | 版本已提交,等待管理员审核 | 正在审核流程中 |
|
||||
| 审核通过 | `approved` | 版本审核通过,自动生效 | 成为当前生效版本 |
|
||||
| 审核拒绝 | `rejected` | 版本审核未通过 | 需要修改后重新上传新版本 |
|
||||
| 已撤销 | `withdrawn` | 开发者主动撤回审核 | 版本废弃,无法恢复 |
|
||||
|
||||
## 三、版本审核流程(新技能/版本提交流程)
|
||||
|
||||
### 3.1 完整流程
|
||||
1. **开发者创建技能**
|
||||
- 填写基本信息(名称、描述、分类、标签、图标)
|
||||
- 填写内部信息(内部名称、内部描述)- 仅供开发者管理使用
|
||||
- 创建完成后,技能状态为 **开发中**
|
||||
|
||||
2. **上传版本**
|
||||
- 创建完成后可立即上传第一个版本
|
||||
- 版本上传时需填写两部分信息:
|
||||
- **版本说明**:供审核参考的版本更新说明
|
||||
- **发布信息**:技能发布名称、发布描述、分类、标签、图标(审核通过后在商店展示)
|
||||
- 非首版本默认继承当前生效版本的发布信息,可按需修改
|
||||
- 版本上传后自动进入审核,版本状态设置为 **审核中**
|
||||
- 添加到管理台"版本审核"待处理列表
|
||||
- 同一时间只能有一个审核中的版本,存在审核中版本时"上传新版本"按钮禁用
|
||||
@@ -77,7 +81,7 @@ flowchart TD
|
||||
|
||||
4. **管理员审核**
|
||||
- 所有管理员均可查看并处理所有版本审核申请
|
||||
- 审核详情包含:技能基本信息、版本信息、技能包文件列表
|
||||
- 审核详情包含:版本发布信息预览(发布名称、描述、分类、标签、图标)、版本信息、技能包文件列表
|
||||
- 管理员可执行操作:**拒绝**(必填拒绝理由)或 **通过**
|
||||
|
||||
5. **审核通过处理**
|
||||
@@ -132,13 +136,18 @@ flowchart TD
|
||||
## 五、操作规则明细
|
||||
|
||||
### 5.1 技能状态与可用操作对照表
|
||||
| 技能状态 | 是否可更新基本信息 | 是否可上传新版本 | 是否可下架 | 是否可删除 | 说明 |
|
||||
| 技能状态 | 是否可更新内部信息 | 是否可上传新版本 | 是否可下架 | 是否可删除 | 说明 |
|
||||
|---------|-------------------|-----------------|-----------|-----------|------|
|
||||
| 开发中 | ✅ 允许 | ✅ 允许(无审核中版本时) | - | ✅ 允许 | 未上架,可直接删除 |
|
||||
| 已上架(无审核中版本) | ✅ 允许 | ✅ 允许 | ✅ 允许 | ❌ 禁用 | 已上架需先下架才能删除 |
|
||||
| 已上架(有审核中版本) | ✅ 允许 | ❌ 禁用 | ❌ 禁用 | ❌ 禁用 | 等待审核完成后才能操作 |
|
||||
| 下架审核中 | ❌ 禁用 | ❌ 禁用 | ✅ 仅允许撤回下架申请 | ❌ 禁用 | 仅"撤回下架申请"按钮可用 |
|
||||
| 已下架 | ❌ 禁用 | ❌ 不适用 | - | ✅ 允许 | 仅支持删除操作 |
|
||||
| 开发中(无审核中版本) | 允许 | 允许 | - | 允许 | 未上架,可直接删除 |
|
||||
| 开发中(有审核中版本) | 允许 | 禁用 | - | 禁用 | 需先撤回审核中的版本 |
|
||||
| 已上架(无审核中版本) | 允许 | 允许 | 允许 | 禁用 | 已上架需先下架才能删除 |
|
||||
| 已上架(有审核中版本) | 允许 | 禁用 | 禁用 | 禁用 | 等待审核完成后才能操作 |
|
||||
| 下架审核中 | 允许 | 禁用 | 仅允许撤回下架申请 | 禁用 | 仅"撤回下架申请"按钮可用 |
|
||||
| 已下架 | 允许 | 禁用 | - | 允许 | 仅支持编辑内部信息和删除操作 |
|
||||
|
||||
**注:**
|
||||
- 内部信息(内部名称、内部描述)可随时更新,不涉及审核流程
|
||||
- 有审核中版本的判断依据是存在未完成的审核流程,此时除了"撤回审核"外的其他操作按钮均禁用
|
||||
|
||||
### 5.2 版本操作规则
|
||||
| 版本状态 | 开发者可用操作 | 说明 |
|
||||
@@ -177,7 +186,39 @@ flowchart TD
|
||||
9. **删除终态规则**:技能删除后所有数据彻底清除,不可恢复
|
||||
10. **名称规则**:技能名称允许重复,仅技能ID全局唯一
|
||||
|
||||
## 七、常见问题
|
||||
## 附录一、数据结构设计说明
|
||||
|
||||
### 1.1 核心设计思路
|
||||
为解决审核漏洞问题(开发者修改基本信息无需审核即可同步到商店),将技能信息分离为两套独立数据:
|
||||
|
||||
| 数据类型 | 存储位置 | 用途 | 修改方式 | 审核要求 |
|
||||
|---------|---------|------|---------|---------|
|
||||
| **开发者内部信息** | 技能主体 | 开发者管理技能使用 | 直接编辑 | 无需审核 |
|
||||
| **版本发布信息** | 版本记录 | 技能商店展示 | 发布新版本 | 必须审核 |
|
||||
|
||||
### 1.2 开发者内部信息(技能主体)
|
||||
- **可随时修改**,无需审核
|
||||
- **不影响**技能商店展示
|
||||
- **仅供**开发者在"我的技能"列表和详情页中查看
|
||||
|
||||
### 1.3 版本发布信息(版本记录)
|
||||
- **存储在版本中**,随版本一起审核
|
||||
- **审核通过后**自动生效,成为商店展示内容
|
||||
- **修改必须发布新版本**,走完整审核流程
|
||||
|
||||
### 1.4 技能商店展示规则
|
||||
- 仅展示状态为 `published`(已上架)的技能
|
||||
- 展示信息**完全从当前生效版本**的发布信息中读取
|
||||
- 技能主体的内部信息**永不**在商店展示
|
||||
|
||||
### 1.5 审核完整性保障
|
||||
任何影响技能商店展示的内容变更(名称、描述、分类、标签、图标):
|
||||
- 都必须存储在版本中
|
||||
- 都必须经过版本审核流程
|
||||
- 都必须在审核通过后才能生效
|
||||
- 开发者无法绕过审核直接修改商店展示内容
|
||||
|
||||
## 附录二、常见问题
|
||||
|
||||
### 📋 流程相关问题
|
||||
#### 1. 技能已有上架版本,提交新版本审核的过程中,旧版本还可以正常使用吗?
|
||||
@@ -208,40 +249,52 @@ flowchart TD
|
||||
#### 9. 一个技能有多个历史版本被审核拒绝过,上传新版本时会不会有额外限制?
|
||||
不会。所有版本审核流程独立,历史拒绝记录不影响新版本的审核流程,只要提交新版本就会进入正常审核队列。
|
||||
|
||||
#### 10. 开发者在版本审核过程中修改了技能的基本信息(名称、描述、标签等),会不会影响正在审核中的版本?
|
||||
不会。版本审核是针对提交时的技能包内容,技能基本信息的修改独立于版本审核流程,修改后立即生效,不影响正在审核中的版本内容。
|
||||
#### 10. 开发者在版本审核过程中修改了技能的内部信息,会不会影响正在审核中的版本?
|
||||
不会。内部信息修改仅供开发者管理使用,与版本审核流程完全独立,不影响正在审核中的版本,也不影响商店展示内容。
|
||||
|
||||
#### 11. 开发者想要修改技能在商店的展示名称,应该怎么操作?
|
||||
必须发布新版本。在上传新版本时填写新的发布名称,待版本审核通过后,新的发布名称会自动在商店展示。
|
||||
|
||||
#### 12. 开发者可以随时修改内部信息吗?修改后需要审核吗?
|
||||
可以随时修改内部信息,无需审核。内部信息仅供开发者自己管理使用,不会影响技能商店展示。
|
||||
|
||||
#### 13. 第一个版本上传时,发布信息是必填的吗?
|
||||
是的。发布信息(发布名称、发布描述、分类、标签、图标)是上传版本时的必填项,这些信息将在审核通过后展示在技能商店。
|
||||
|
||||
#### 14. 非首版本上传时,发布信息可以不填吗?
|
||||
可以不填,系统会自动继承当前生效版本的发布信息作为默认值,开发者可以在此基础上按需修改。
|
||||
|
||||
### 🔍 审核管理问题
|
||||
#### 11. 版本审核被拒绝后,可以修改内容后用原来的版本重新提交审核吗?
|
||||
#### 15. 版本审核被拒绝后,可以修改内容后用原来的版本重新提交审核吗?
|
||||
不可以,审核拒绝的版本不可重新提交,必须修改内容后上传全新版本,走完整的新版本审核流程。
|
||||
|
||||
#### 12. 审核拒绝的理由填错了,管理员能不能修改拒绝理由?
|
||||
#### 16. 审核拒绝的理由填错了,管理员能不能修改拒绝理由?
|
||||
不可以。审核拒绝理由提交后无法二次修改,管理员提交前需确认内容正确。
|
||||
|
||||
#### 13. 管理员审核版本的时候,能不能下载技能包查看内容?
|
||||
#### 17. 管理员审核版本的时候,能不能下载技能包查看内容?
|
||||
可以。仅管理员在审核版本时可下载技能包查看内容,开发者侧不提供任何版本的下载功能。
|
||||
|
||||
#### 14. 所有管理员都可以处理审核申请吗?有没有分级权限?
|
||||
#### 18. 所有管理员都可以处理审核申请吗?有没有分级权限?
|
||||
所有管理员可见全部审核申请(版本审核+下架审核),均可直接执行审核操作,无分级权限限制。
|
||||
|
||||
### 📤 下架相关问题
|
||||
#### 15. 下架审核期间开发者想取消下架,应该怎么处理?
|
||||
#### 19. 下架审核期间开发者想取消下架,应该怎么处理?
|
||||
可以直接撤回下架申请,撤回后技能立即恢复为已上架状态,所有功能和操作恢复正常。
|
||||
|
||||
#### 16. 已下架的技能想要重新上架,需要走什么流程?
|
||||
#### 20. 已下架的技能想要重新上架,需要走什么流程?
|
||||
已下架技能不支持直接恢复上架,必须创建全新的技能,按新技能完整的创建、上传版本、审核流程重新提交。
|
||||
|
||||
#### 17. 已下架的技能,开发者还能查看之前的版本历史和审核拒绝理由吗?
|
||||
#### 21. 已下架的技能,开发者还能查看之前的版本历史和审核拒绝理由吗?
|
||||
可以。已下架技能仅限制操作(只能删除),但历史数据(版本记录、拒绝理由等)均保留可见,开发者可以正常查阅。
|
||||
|
||||
#### 18. 技能处于开发中状态,上传了第一个版本正在审核中,此时开发者可以删除该技能吗?
|
||||
#### 22. 技能处于开发中状态,上传了第一个版本正在审核中,此时开发者可以删除该技能吗?
|
||||
不可以。存在审核中的版本时,删除操作被禁用,需要先撤回审核中的版本,待状态恢复为开发中无审核中版本后,才可以删除技能。
|
||||
|
||||
#### 19. 已下架的技能被删除之后,有没有办法恢复?
|
||||
#### 23. 已下架的技能被删除之后,有没有办法恢复?
|
||||
不可以。删除操作是终态操作,技能删除后所有数据彻底清除,无法恢复。
|
||||
|
||||
### ❓ 其他问题
|
||||
#### 20. 开发者创建和已删除技能同名的新技能,会不会被限制?
|
||||
#### 24. 开发者创建和已删除技能同名的新技能,会不会被限制?
|
||||
不会。技能名称允许重复,仅技能ID全局唯一不可重复,删除的技能名称可被重新使用。
|
||||
|
||||
*最后更新:2026-03-21*
|
||||
*最后更新:2026-03-21*
|
||||
|
||||
@@ -1,42 +1,34 @@
|
||||
## ADDED Requirements
|
||||
## Purpose
|
||||
开发者基本信息编辑功能用于管理技能的内部信息,这些信息仅供开发者自己使用,不影响技能商店展示。
|
||||
|
||||
### Requirement: 基本信息编辑表单
|
||||
UpdateSkillInfoPage SHALL 提供技能基本信息的编辑表单,预填当前数据。表单功能保持不变,但技能详情页的UI布局已更新。
|
||||
## Requirements
|
||||
|
||||
#### Scenario: 表单预填展示
|
||||
- **WHEN** 用户从技能详情页点击"更新基本信息"进入 UpdateSkillInfoPage
|
||||
- **THEN** 表单字段预填当前技能的名称、描述、分类、标签和图标数据
|
||||
### Requirement: 内部信息编辑表单
|
||||
UpdateSkillInfoPage SHALL 提供开发者内部信息的编辑表单,预填当前数据。
|
||||
|
||||
#### Scenario: 分类动态生成
|
||||
- **WHEN** 用户在基本信息编辑表单中打开分类下拉框
|
||||
- **THEN** 下拉选项从数据源动态生成,包含所有可用分类(信息查询、效率工具、开发工具、数据分析、文档处理、业务系统)
|
||||
#### Scenario: 表单字段简化
|
||||
- **WHEN** 用户从技能详情页点击"编辑内部信息"进入 UpdateSkillInfoPage
|
||||
- **THEN** 表单仅包含两个字段:开发者内部技能名称、开发者内部技能描述
|
||||
|
||||
#### Scenario: 提交基本信息修改
|
||||
- **WHEN** 用户填写完基本信息后点击"保存修改"按钮
|
||||
- **THEN** 页面展示成功提示"保存成功",并返回技能详情页(使用新的UI布局)
|
||||
#### Scenario: 移除字段
|
||||
- **WHEN** 用户在内部信息编辑页面时
|
||||
- **THEN** 不显示分类、标签、图标选择器(这些字段已移至版本发布信息)
|
||||
|
||||
#### Scenario: 提交内部信息修改
|
||||
- **WHEN** 用户填写完内部信息后点击"保存修改"按钮
|
||||
- **THEN** 页面展示成功提示"保存成功",并返回技能详情页
|
||||
|
||||
#### Scenario: 取消编辑
|
||||
- **WHEN** 用户在基本信息编辑页面点击"取消"按钮
|
||||
- **THEN** 返回技能详情页(使用新的UI布局),不保存任何修改
|
||||
- **WHEN** 用户在内部信息编辑页面点击"取消"按钮
|
||||
- **THEN** 返回技能详情页,不保存任何修改
|
||||
|
||||
### Requirement: 技能图标选择
|
||||
UpdateSkillInfoPage 和 UploadSkillPage SHALL 提供技能图标的 emoji 选择器。
|
||||
UploadSkillPage SHALL 提供技能图标的 emoji 选择器(仅用于创建技能时的图标选择)。
|
||||
|
||||
#### Scenario: 图标选择展示
|
||||
- **WHEN** 用户在技能创建或编辑页面看到图标选择区域
|
||||
- **THEN** 页面展示 emoji 网格(🌤️📊📝🔧💻📋🔍📈🎯⚡🌐🤖),当前选中项高亮显示
|
||||
- **WHEN** 用户在创建技能页面看到图标选择区域
|
||||
- **THEN** 页面展示 emoji 网格,当前选中项高亮显示
|
||||
|
||||
#### Scenario: 切换图标
|
||||
- **WHEN** 用户点击 emoji 网格中的某个图标
|
||||
- **THEN** 该图标高亮选中,之前的选中项取消高亮
|
||||
|
||||
### Requirement: 技能图标显示
|
||||
技能详情页 SHALL 在技能概览卡片中展示技能图标,并采用新的UI布局设计。
|
||||
|
||||
#### Scenario: 图标展示
|
||||
- **WHEN** 用户打开技能详情页
|
||||
- **THEN** 技能概览卡片的图标位置显示该技能选择的 emoji 图标,图标尺寸为80x80像素,圆角16像素
|
||||
|
||||
#### Scenario: 图标背景样式
|
||||
- **WHEN** 用户查看技能概览卡片中的图标
|
||||
- **THEN** 图标具有渐变背景(从#8B5CF6到#EC4899),白色文字,与新的设计系统一致
|
||||
|
||||
@@ -2,27 +2,27 @@
|
||||
|
||||
## 功能描述
|
||||
|
||||
展示开发者自己创建的技能列表,支持筛选和操作,提供清晰的表格布局。
|
||||
展示开发者自己创建的技能列表,展示开发者内部名称,支持筛选和操作。
|
||||
|
||||
## 表格列定义
|
||||
## Requirements
|
||||
|
||||
### Requirement: 表格列定义
|
||||
表格必须包含以下列:
|
||||
|
||||
| 列名 | 内容 | 显示要求 |
|
||||
|------|------|----------|
|
||||
| 技能名称 | `skill.name` | 普通字重,不加粗 |
|
||||
| 技能描述 | `skill.desc` | 正常显示 |
|
||||
| 分类 | `skill.category` | 正常显示 |
|
||||
| 技能名称 | `skill.name`(开发者内部名称) | 普通字重,不加粗 |
|
||||
| 技能描述 | `skill.desc`(开发者内部描述) | 正常显示 |
|
||||
| 状态 | `skill.status` | 使用 `skillStatusMap` 映射显示中文状态和对应样式,**只显示技能整体状态** |
|
||||
| 操作 | 编辑/下架/删除按钮 | 保持按钮组布局 |
|
||||
|
||||
**移除以下列:**
|
||||
- 分类 (`category`)
|
||||
- 版本 (`version`)
|
||||
- 安装量 (`installs`)
|
||||
- 评分 (`rating`)
|
||||
|
||||
## 状态显示规则
|
||||
|
||||
### Requirement: 状态显示规则
|
||||
1. **只显示技能的整体状态**,使用 `skillStatusMap` 映射:
|
||||
- `dev` → 开发中
|
||||
- `published` → 已上架
|
||||
@@ -31,32 +31,31 @@
|
||||
|
||||
2. **不再额外显示** `skill.hasPendingReview` 的"审核中"徽章。版本审核状态在技能详情页展示即可。
|
||||
|
||||
## 操作按钮规则
|
||||
### Requirement: 操作按钮规则
|
||||
|
||||
### 下架按钮
|
||||
#### 下架按钮
|
||||
- 只在 `skill.status === 'published'` 时显示
|
||||
- 当 `skill.hasPendingReview === true` 时禁用,提示"存在审核中的版本,请先撤回后再下架"
|
||||
- 点击触发出下架操作
|
||||
|
||||
### 删除按钮
|
||||
- 当 `skill.status === 'published'` 时禁用,提示"已上架的技能需要先下架才能删除"
|
||||
#### 删除按钮
|
||||
- 当 `skill.status === 'published'` 或 `skill.status === 'unlisting'` 或 `skill.hasPendingReview === true` 时禁用
|
||||
- 提示信息根据状态变化
|
||||
- 点击触发确认删除弹框
|
||||
- 确认后执行删除操作
|
||||
|
||||
### 编辑按钮
|
||||
#### 编辑按钮
|
||||
- 始终显示
|
||||
- 点击跳转到技能详情编辑页
|
||||
|
||||
以上规则必须与**技能详情页**的按钮逻辑保持完全一致。
|
||||
|
||||
## 交互行为
|
||||
|
||||
### Requirement: 交互行为
|
||||
- 点击表格行任意位置跳转到技能详情编辑页(保持不变)
|
||||
- 筛选功能保持不变,支持关键词、分类、状态筛选
|
||||
- 筛选功能保持不变,支持关键词、状态筛选(移除分类筛选)
|
||||
- 分页保持不变
|
||||
|
||||
## 样式要求
|
||||
|
||||
### Requirement: 样式要求
|
||||
- 保持现有表格样式体系
|
||||
- 不引入新的样式类名
|
||||
- 技能名称使用正常字重(移除 `fontWeight: 600`)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
## Purpose
|
||||
技能详情页面布局规范定义了技能开发台详情页的三段式布局结构,确保信息层级清晰,用户能够快速定位基本信息和版本历史。
|
||||
技能详情页面布局规范定义了技能开发台详情页的四段式布局结构,确保信息层级清晰,用户能够快速定位内部信息、当前生效版本、版本历史和管理操作。
|
||||
|
||||
## ADDED Requirements
|
||||
## Requirements
|
||||
|
||||
### Requirement: 三段式页面布局
|
||||
技能详情页面 SHALL 采用三段式卡片布局结构,依次展示头部概览、版本历史、管理操作。
|
||||
### Requirement: 四段式页面布局
|
||||
技能详情页面 SHALL 采用四段式卡片布局结构,依次展示头部概览(内部信息)、当前生效版本、版本历史、管理操作。
|
||||
|
||||
#### Scenario: 页面结构展示
|
||||
- **WHEN** 用户打开技能详情页面
|
||||
- **THEN** 页面从上到下依次显示:头部概览卡片、版本历史卡片、管理操作卡片
|
||||
- **THEN** 页面从上到下依次显示:头部概览卡片(内部信息)、当前生效版本卡片、版本历史卡片、管理操作卡片
|
||||
|
||||
### Requirement: 返回按钮
|
||||
技能详情页面 SHALL 在页面顶部显示返回按钮,允许用户返回上一级页面。
|
||||
@@ -17,56 +17,34 @@
|
||||
- **WHEN** 用户查看技能详情页面
|
||||
- **THEN** 页面顶部显示返回按钮,点击后返回"我的技能"页面
|
||||
|
||||
### Requirement: 头部概览卡片
|
||||
技能详情页面 SHALL 在页面顶部显示头部概览卡片,整合展示技能的核心信息。
|
||||
### Requirement: 头部概览卡片(内部信息)
|
||||
技能详情页面 SHALL 在页面顶部显示头部概览卡片,展示开发者内部信息。
|
||||
|
||||
#### Scenario: 概览卡片信息展示
|
||||
- **WHEN** 用户查看技能详情页面
|
||||
- **THEN** 头部概览卡片包含:技能图标(80x80,无背景)、技能名称、状态标签、指标行、标签区、技能描述
|
||||
- **THEN** 头部概览卡片展示开发者内部信息:内部技能名称、状态标签
|
||||
|
||||
#### Scenario: 概览卡片第一行布局
|
||||
- **WHEN** 用户查看头部概览卡片
|
||||
- **THEN** 第一行显示:技能名称、状态标签、右上角"编辑基本信息"按钮
|
||||
- **THEN** 第一行显示:内部技能名称、状态标签、右上角"编辑内部信息"按钮
|
||||
|
||||
#### Scenario: 概览卡片图标样式
|
||||
- **WHEN** 用户查看头部概览卡片
|
||||
- **THEN** 技能图标为 80x80 像素,无渐变背景,直接显示图标或首字母
|
||||
### Requirement: 当前生效版本卡片
|
||||
技能详情页面 SHALL 在头部概览卡片下方显示当前生效版本卡片。
|
||||
|
||||
### Requirement: 指标行布局
|
||||
头部概览卡片 SHALL 在名称下方显示指标行,展示关键数据。
|
||||
#### Scenario: 当前生效版本展示
|
||||
- **WHEN** 用户查看技能详情页面且技能已上架
|
||||
- **THEN** 显示当前生效版本卡片,展示该版本在技能商店的展示效果:技能发布名称、技能发布描述、分类、标签、图标、安装数、评分、版本号
|
||||
|
||||
#### Scenario: 指标行内容
|
||||
- **WHEN** 用户查看头部概览卡片
|
||||
- **THEN** 指标行依次显示:订阅数(图标+数值)、评分(图标+数值)、当前版本号(图标+版本号)
|
||||
|
||||
#### Scenario: 指标行样式
|
||||
- **WHEN** 用户查看头部概览卡片
|
||||
- **THEN** 指标行与名称之间有分隔线,指标项之间用空格分隔
|
||||
|
||||
### Requirement: 标签区布局
|
||||
头部概览卡片 SHALL 在指标行下方显示标签区。
|
||||
|
||||
#### Scenario: 标签区内容
|
||||
- **WHEN** 用户查看头部概览卡片
|
||||
- **THEN** 标签区第一个位置显示分类标签(蓝色背景),后面跟随技能的所有标签
|
||||
|
||||
#### Scenario: 分类标签样式
|
||||
- **WHEN** 用户查看标签区
|
||||
- **THEN** 分类标签使用蓝色背景(#EFF6FF)和蓝色文字(#3B82F6),圆角样式
|
||||
|
||||
### Requirement: 技能描述
|
||||
头部概览卡片 SHALL 在标签区下方显示技能描述。
|
||||
|
||||
#### Scenario: 技能描述展示
|
||||
- **WHEN** 用户查看头部概览卡片
|
||||
- **THEN** 标签区下方显示技能描述文字
|
||||
#### Scenario: 无生效版本时
|
||||
- **WHEN** 用户查看技能详情页面且技能尚未上架
|
||||
- **THEN** 不显示当前生效版本卡片,或显示空状态提示
|
||||
|
||||
### Requirement: 版本历史卡片
|
||||
技能详情页面 SHALL 在头部概览卡片下方显示版本历史卡片。
|
||||
技能详情页面 SHALL 在当前生效版本卡片下方显示版本历史卡片。
|
||||
|
||||
#### Scenario: 版本历史卡片展示
|
||||
- **WHEN** 用户查看技能详情页面
|
||||
- **THEN** 版本历史卡片包含标题"版本历史"和版本历史表格
|
||||
- **THEN** 版本历史卡片包含标题"版本历史"和版本历史卡片列表(非表格)
|
||||
|
||||
#### Scenario: 版本历史操作按钮
|
||||
- **WHEN** 用户查看版本历史卡片
|
||||
|
||||
@@ -1,50 +1,26 @@
|
||||
## Purpose
|
||||
技能概览卡片用于在技能编辑页面顶部集中展示技能核心信息,提供更好的信息组织和视觉体验。
|
||||
技能概览卡片用于在技能编辑页面顶部集中展示开发者内部信息,提供清晰的内部视图与商店发布视图的分离。
|
||||
|
||||
## ADDED Requirements
|
||||
## Requirements
|
||||
|
||||
### Requirement: 技能概览卡片
|
||||
技能编辑页面 SHALL 在页面顶部显示技能概览卡片,集中展示技能核心信息。
|
||||
技能编辑页面 SHALL 在页面顶部显示技能概览卡片,展示开发者内部信息。
|
||||
|
||||
#### Scenario: 卡片布局结构
|
||||
- **WHEN** 用户打开技能编辑页面
|
||||
- **THEN** 页面顶部显示技能概览卡片,包含技能图标、名称、状态标签、指标行、标签区(含分类)、技能描述和右上角操作按钮
|
||||
- **THEN** 页面顶部显示技能概览卡片,包含内部名称、状态标签、右上角"编辑内部信息"按钮
|
||||
|
||||
#### Scenario: 技能图标显示
|
||||
#### Scenario: 内部名称和状态显示
|
||||
- **WHEN** 用户查看技能概览卡片
|
||||
- **THEN** 卡片左侧显示技能图标,图标尺寸为80x80像素,无背景,直接显示图标或首字母
|
||||
|
||||
#### Scenario: 技能名称和状态显示
|
||||
- **WHEN** 用户查看技能概览卡片
|
||||
- **THEN** 卡片右侧第一行显示技能名称、状态标签,名称右侧显示"编辑基本信息"按钮
|
||||
- **THEN** 卡片右侧第一行显示开发者内部技能名称、状态标签,名称右侧显示"编辑内部信息"按钮
|
||||
|
||||
#### Scenario: 状态标签样式
|
||||
- **WHEN** 用户查看技能概览卡片
|
||||
- **THEN** 状态标签为纯文字样式,不使用图标,通过颜色区分状态类型
|
||||
|
||||
#### Scenario: 指标行显示
|
||||
- **WHEN** 用户查看技能概览卡片
|
||||
- **THEN** 名称下方显示指标行,包含订阅数(图标+数值)、评分(图标+数值)、当前版本号(图标+版本号)
|
||||
|
||||
#### Scenario: 指标行样式
|
||||
- **WHEN** 用户查看技能概览卡片
|
||||
- **THEN** 指标行与名称之间有分隔线,指标项不显示文字说明(如"订阅"、"评分"),图标本身已足够表达含义
|
||||
|
||||
#### Scenario: 标签区显示
|
||||
- **WHEN** 用户查看技能概览卡片
|
||||
- **THEN** 指标行下方显示标签区,第一个位置显示分类标签(蓝色背景),后面跟随技能的所有标签
|
||||
|
||||
#### Scenario: 分类标签样式
|
||||
- **WHEN** 用户查看标签区
|
||||
- **THEN** 分类标签使用蓝色背景(#EFF6FF)和蓝色文字(#3B82F6),圆角胶囊样式,与其他标签区分
|
||||
|
||||
#### Scenario: 技能描述显示
|
||||
- **WHEN** 用户查看技能概览卡片
|
||||
- **THEN** 标签区下方显示技能描述
|
||||
|
||||
#### Scenario: 操作按钮显示
|
||||
- **WHEN** 用户查看技能概览卡片
|
||||
- **THEN** 卡片右上角只显示"编辑基本信息"操作按钮
|
||||
- **THEN** 卡片右上角只显示"编辑内部信息"操作按钮
|
||||
|
||||
#### Scenario: 卡片视觉样式
|
||||
- **WHEN** 用户查看技能概览卡片
|
||||
|
||||
37
openspec/specs/skill-publish-info-versioning/spec.md
Normal file
37
openspec/specs/skill-publish-info-versioning/spec.md
Normal file
@@ -0,0 +1,37 @@
|
||||
## Purpose
|
||||
技能发布信息版本化管理,确保所有在技能商店展示的信息(名称、描述、分类、标签、图标)都跟随版本,任何变更都必须经过版本审核流程。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 发布信息跟随版本
|
||||
每个版本 SHALL 包含完整的技能发布信息,这些信息在版本审核通过后成为技能商店展示的内容。
|
||||
|
||||
#### Scenario: 版本发布信息字段
|
||||
- **WHEN** 创建新版本时
|
||||
- **THEN** 版本必须包含以下发布信息字段:技能发布名称、技能发布描述、分类、标签、图标
|
||||
|
||||
#### Scenario: 商店展示内容来源
|
||||
- **WHEN** 用户在技能商店查看技能时
|
||||
- **THEN** 展示内容来自当前生效版本的发布信息字段,而非 Skill 对象
|
||||
|
||||
### Requirement: 内部信息与发布信息分离
|
||||
Skill 对象 SHALL 仅保留开发者内部使用的名称和描述,与商店发布信息完全分离。
|
||||
|
||||
#### Scenario: 内部信息字段
|
||||
- **WHEN** 创建或编辑技能时
|
||||
- **THEN** Skill 对象仅包含:开发者内部技能名称、开发者内部技能描述
|
||||
|
||||
#### Scenario: 内部信息编辑权限
|
||||
- **WHEN** 开发者编辑技能内部信息时
|
||||
- **THEN** 可在任何状态下编辑,无需审核,不影响技能商店展示
|
||||
|
||||
### Requirement: 发布信息修改必须发版
|
||||
修改技能商店展示的发布信息 SHALL 必须通过发布新版本实现。
|
||||
|
||||
#### Scenario: 发布信息修改路径
|
||||
- **WHEN** 开发者需要修改技能商店展示的名称、描述、分类、标签或图标时
|
||||
- **THEN** 必须上传新版本,在新版本中填写新的发布信息,提交审核
|
||||
|
||||
#### Scenario: 版本继承发布信息
|
||||
- **WHEN** 上传非首个版本时
|
||||
- **THEN** 发布信息表单默认继承当前生效版本的发布信息值
|
||||
@@ -1,4 +1,4 @@
|
||||
## ADDED Requirements
|
||||
## Requirements
|
||||
|
||||
### Requirement: 管理台审核列表
|
||||
AdminPage SHALL 提供版本审核和下架审核的列表展示。
|
||||
@@ -13,30 +13,19 @@ AdminPage SHALL 提供版本审核和下架审核的列表展示。
|
||||
|
||||
#### Scenario: 版本审核列表展示
|
||||
- **WHEN** 用户查看版本审核Tab
|
||||
- **THEN** 页面显示待审核版本列表,每条包含技能名称、版本号、提交时间、开发者、操作按钮
|
||||
- **THEN** 页面显示待审核版本列表,每条包含技能发布名称、版本号、提交时间、开发者、操作按钮
|
||||
|
||||
#### Scenario: 下架审核列表展示
|
||||
- **WHEN** 用户切换到下架审核Tab
|
||||
- **THEN** 页面显示待审核下架列表,每条包含技能名称、当前版本、申请时间、开发者、操作按钮
|
||||
- **THEN** 页面显示待审核下架列表,每条包含技能发布名称、当前版本、申请时间、开发者、操作按钮
|
||||
|
||||
### Requirement: 管理台版本审核详情
|
||||
AdminPage SHALL 提供版本审核的详情展示和操作。
|
||||
AdminPage SHALL 提供版本审核的详情展示和操作,展示该版本的发布信息。
|
||||
|
||||
#### Scenario: 版本信息展示
|
||||
- **WHEN** 用户点击版本审核列表中的"审核"按钮
|
||||
- **THEN** 页面显示技能基本信息(名称、开发者、分类、标签)、版本信息(版本号、提交时间、版本说明)、文件列表
|
||||
- **THEN** 页面展示该版本提交的完整发布信息:技能发布名称、技能发布描述、分类、标签、图标、版本号、提交时间、版本说明、文件列表
|
||||
|
||||
#### Scenario: 审核操作按钮
|
||||
- **WHEN** 用户查看版本审核详情
|
||||
- **THEN** 页面底部显示"拒绝"和"通过"两个按钮
|
||||
|
||||
### Requirement: 管理台下架审核详情
|
||||
AdminPage SHALL 提供下架审核的详情展示和操作。
|
||||
|
||||
#### Scenario: 下架信息展示
|
||||
- **WHEN** 用户点击下架审核列表中的"审核"按钮
|
||||
- **THEN** 页面显示技能信息(名称、开发者、当前版本、订阅数、申请时间)
|
||||
|
||||
#### Scenario: 下架审核操作
|
||||
- **WHEN** 用户查看下架审核详情
|
||||
- **THEN** 页面底部显示"拒绝"和"通过"两个按钮
|
||||
|
||||
@@ -1,45 +1,61 @@
|
||||
## ADDED Requirements
|
||||
## Requirements
|
||||
|
||||
### Requirement: 版本上传表单
|
||||
UploadVersionPage SHALL 提供版本上传的表单界面。
|
||||
UploadVersionPage SHALL 提供版本上传的表单界面,包含发布信息字段。
|
||||
|
||||
#### Scenario: 版本信息输入
|
||||
- **WHEN** 用户打开上传新版本页面
|
||||
- **THEN** 页面显示版本说明输入框和文件上传区域
|
||||
- **THEN** 页面显示:版本说明输入框、技能发布名称输入框、技能发布描述输入框、分类下拉框、标签输入、图标选择器、文件上传区域
|
||||
|
||||
#### Scenario: 发布信息字段展示
|
||||
- **WHEN** 用户在上传新版本页面填写表单时
|
||||
- **THEN** 页面清晰区分版本说明(供审核参考)和发布信息(商店展示内容)
|
||||
|
||||
#### Scenario: 非首版本默认继承
|
||||
- **WHEN** 用户上传非首个版本时
|
||||
- **THEN** 技能发布名称、技能发布描述、分类、标签、图标默认继承当前生效版本的值
|
||||
|
||||
#### Scenario: 首版本默认值
|
||||
- **WHEN** 用户上传首个版本时
|
||||
- **THEN** 技能发布名称、技能发布描述、分类、标签、图标默认值为空
|
||||
|
||||
#### Scenario: 文件上传展示
|
||||
- **WHEN** 用户在上传页面看到文件上传区域
|
||||
- **THEN** 页面展示拖拽上传区域,支持 .zip 格式,显示上传图标和提示文字
|
||||
|
||||
### Requirement: 版本历史展示
|
||||
SkillEditorPage SHALL 展示技能的版本历史列表。
|
||||
SkillEditorPage SHALL 展示技能的版本历史列表,包含每个版本的发布信息。
|
||||
|
||||
#### Scenario: 版本列表展示
|
||||
- **WHEN** 用户打开技能详情页
|
||||
- **THEN** 页面显示版本历史表格,包含版本号、版本说明、状态、更新时间、操作列
|
||||
- **THEN** 页面显示版本历史卡片,每个版本使用普通卡片布局(非表格、非折叠),包含版本号、版本说明、发布信息预览、状态、更新时间、操作列
|
||||
|
||||
#### Scenario: 发布信息预览
|
||||
- **WHEN** 用户查看版本历史卡片时
|
||||
- **THEN** 每个版本卡片显示该版本的发布信息预览:图标 + 技能发布名称 + 标签列表
|
||||
|
||||
#### Scenario: 版本状态展示
|
||||
- **WHEN** 用户查看版本历史列表
|
||||
- **WHEN** 用户查看版本历史卡片
|
||||
- **THEN** 每个版本显示对应的状态标签:审核中(warning)、审核通过(running)、审核拒绝(error)、已撤销(stopped)
|
||||
|
||||
#### Scenario: 审核拒绝理由展示
|
||||
- **WHEN** 版本状态为审核拒绝
|
||||
- **THEN** 版本说明下方显示拒绝理由文本
|
||||
- **THEN** 版本卡片内显示拒绝理由文本
|
||||
|
||||
### Requirement: 版本操作按钮
|
||||
版本历史表格 SHALL 根据版本状态展示不同的操作按钮。
|
||||
版本历史卡片 SHALL 根据版本状态展示不同的操作按钮。
|
||||
|
||||
#### Scenario: 审核中版本操作
|
||||
- **WHEN** 版本状态为审核中
|
||||
- **THEN** 显示"撤回审核"、"下载"按钮
|
||||
- **THEN** 显示"撤回审核"按钮
|
||||
|
||||
#### Scenario: 已完结版本操作
|
||||
- **WHEN** 版本状态为审核通过/审核拒绝/已撤销
|
||||
- **THEN** 仅显示"下载"按钮
|
||||
- **THEN** 不显示操作按钮
|
||||
|
||||
### Requirement: 技能市场版本展示
|
||||
SkillDetailPage SHALL 展示技能的最新版本信息。
|
||||
|
||||
#### Scenario: 最新版本展示
|
||||
- **WHEN** 用户打开技能市场详情页
|
||||
- **THEN** 页面显示当前版本号、更新说明、更新时间(仅展示最新版本,不展示完整历史)
|
||||
- **THEN** 页面展示当前生效版本的发布信息,包括技能发布名称、技能发布描述、分类、标签、图标
|
||||
|
||||
@@ -1,58 +1,373 @@
|
||||
export const mySkills = [
|
||||
{
|
||||
id: 1,
|
||||
name: '天气查询助手',
|
||||
desc: '根据城市名称查询当前天气和未来预报,支持全国主要城市',
|
||||
icon: '🌤️',
|
||||
skillId: 'SKL-2026-0001',
|
||||
name: '天气小工具',
|
||||
desc: '我的个人天气查询项目',
|
||||
status: 'published',
|
||||
hasPendingReview: false,
|
||||
version: '1.2.0',
|
||||
category: '信息查询',
|
||||
tags: ['天气', '查询', '生活'],
|
||||
lastModified: '2026-03-18',
|
||||
installs: 156,
|
||||
rating: 4.7,
|
||||
versions: [
|
||||
{ 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' }
|
||||
{
|
||||
version: '1.2.0',
|
||||
date: '2026-03-18',
|
||||
versionDesc: '新增支持未来7天预报',
|
||||
status: 'approved',
|
||||
publicName: '天气查询助手',
|
||||
publicDesc: '根据城市名称查询当前天气和未来预报,支持全国主要城市',
|
||||
category: '信息查询',
|
||||
tags: ['天气', '查询', '生活'],
|
||||
icon: '🌤️',
|
||||
installs: 156,
|
||||
rating: 4.7
|
||||
},
|
||||
{
|
||||
version: '1.1.0',
|
||||
date: '2026-03-10',
|
||||
versionDesc: '优化响应速度',
|
||||
status: 'approved',
|
||||
publicName: '天气查询助手',
|
||||
publicDesc: '根据城市名称查询当前天气',
|
||||
category: '信息查询',
|
||||
tags: ['天气', '查询'],
|
||||
icon: '🌤️',
|
||||
installs: 142,
|
||||
rating: 4.6
|
||||
},
|
||||
{
|
||||
version: '1.0.0',
|
||||
date: '2026-03-01',
|
||||
versionDesc: '初始版本',
|
||||
status: 'approved',
|
||||
publicName: '天气查询',
|
||||
publicDesc: '查询天气的简单工具',
|
||||
category: '信息查询',
|
||||
tags: ['天气'],
|
||||
icon: '🌤️',
|
||||
installs: 98,
|
||||
rating: 4.5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
skillId: 'SKL-2026-0002',
|
||||
name: '待办事项管理',
|
||||
desc: '帮助用户管理日常待办事项,支持添加、完成、删除操作',
|
||||
icon: '📋',
|
||||
desc: '我的个人待办管理工具',
|
||||
status: 'dev',
|
||||
hasPendingReview: false,
|
||||
version: '0.1.0',
|
||||
category: '效率工具',
|
||||
tags: ['待办', '管理', '效率'],
|
||||
hasPendingReview: true,
|
||||
lastModified: '2026-03-17',
|
||||
installs: 0,
|
||||
rating: 0,
|
||||
versions: [
|
||||
{ version: '0.1.0', date: '2026-03-17', desc: '开发中版本', status: 'reviewing' }
|
||||
{
|
||||
version: '0.1.0',
|
||||
date: '2026-03-17',
|
||||
versionDesc: '开发中版本',
|
||||
status: 'reviewing',
|
||||
publicName: '待办事项助手',
|
||||
publicDesc: '帮助用户管理日常待办事项,支持添加、完成、删除操作',
|
||||
category: '效率工具',
|
||||
tags: ['待办', '管理', '效率'],
|
||||
icon: '📋',
|
||||
installs: 0,
|
||||
rating: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '代码审查助手',
|
||||
desc: '自动审查代码质量,提供优化建议和潜在问题检测',
|
||||
icon: '💻',
|
||||
skillId: 'SKL-2026-0003',
|
||||
name: '代码审查项目',
|
||||
desc: '代码质量审查工具',
|
||||
status: 'published',
|
||||
hasPendingReview: false,
|
||||
lastModified: '2026-03-15',
|
||||
versions: [
|
||||
{
|
||||
version: '2.0.1',
|
||||
date: '2026-03-15',
|
||||
versionDesc: '修复 Python 代码审查问题',
|
||||
status: 'approved',
|
||||
publicName: '代码审查助手',
|
||||
publicDesc: '自动审查代码质量,提供优化建议和潜在问题检测',
|
||||
category: '开发工具',
|
||||
tags: ['代码', '审查', '开发'],
|
||||
icon: '💻',
|
||||
installs: 342,
|
||||
rating: 4.9
|
||||
},
|
||||
{
|
||||
version: '2.0.0',
|
||||
date: '2026-03-10',
|
||||
versionDesc: '修复安全问题',
|
||||
status: 'rejected',
|
||||
rejectionReason: '安全审查未通过,存在潜在的代码注入风险,请修复后重新提交',
|
||||
publicName: '代码审查助手 Pro',
|
||||
publicDesc: '自动审查代码质量,提供优化建议和潜在问题检测,支持多语言',
|
||||
category: '开发工具',
|
||||
tags: ['代码', '审查', '开发', '安全'],
|
||||
icon: '🔒',
|
||||
installs: 0,
|
||||
rating: 0
|
||||
},
|
||||
{
|
||||
version: '2.0.0',
|
||||
date: '2026-03-08',
|
||||
versionDesc: '支持多语言审查',
|
||||
status: 'approved',
|
||||
publicName: '代码审查助手',
|
||||
publicDesc: '自动审查代码质量,提供优化建议和潜在问题检测',
|
||||
category: '开发工具',
|
||||
tags: ['代码', '审查', '开发'],
|
||||
icon: '💻',
|
||||
installs: 310,
|
||||
rating: 4.8
|
||||
},
|
||||
{
|
||||
version: '1.0.0',
|
||||
date: '2026-02-20',
|
||||
versionDesc: '初始版本',
|
||||
status: 'approved',
|
||||
publicName: '代码审查',
|
||||
publicDesc: '简单的代码审查工具',
|
||||
category: '开发工具',
|
||||
tags: ['代码', '审查'],
|
||||
icon: '💻',
|
||||
installs: 198,
|
||||
rating: 4.7
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
skillId: 'SKL-2026-0004',
|
||||
name: '数据可视化工具',
|
||||
desc: '快速生成图表的数据可视化工具',
|
||||
status: 'dev',
|
||||
hasPendingReview: false,
|
||||
lastModified: '2026-03-16',
|
||||
versions: []
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
skillId: 'SKL-2026-0005',
|
||||
name: '翻译助手Pro',
|
||||
desc: '多语言智能翻译工具',
|
||||
status: 'published',
|
||||
hasPendingReview: true,
|
||||
version: '2.0.1',
|
||||
category: '开发工具',
|
||||
tags: ['代码', '审查', '开发'],
|
||||
lastModified: '2026-03-15',
|
||||
installs: 342,
|
||||
rating: 4.9,
|
||||
lastModified: '2026-03-19',
|
||||
versions: [
|
||||
{ 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' }
|
||||
{
|
||||
version: '2.1.0',
|
||||
date: '2026-03-19',
|
||||
versionDesc: '新增小语种支持',
|
||||
status: 'reviewing',
|
||||
publicName: '智能翻译助手',
|
||||
publicDesc: '支持50+种语言互译,带上下文理解',
|
||||
category: '效率工具',
|
||||
tags: ['翻译', '语言', 'AI'],
|
||||
icon: '🌐',
|
||||
installs: 0,
|
||||
rating: 0
|
||||
},
|
||||
{
|
||||
version: '2.0.0',
|
||||
date: '2026-03-05',
|
||||
versionDesc: '全新架构升级',
|
||||
status: 'approved',
|
||||
publicName: '翻译助手',
|
||||
publicDesc: '支持中英日韩互译,准确高效',
|
||||
category: '效率工具',
|
||||
tags: ['翻译', '语言'],
|
||||
icon: '🌐',
|
||||
installs: 892,
|
||||
rating: 4.8
|
||||
},
|
||||
{
|
||||
version: '1.0.0',
|
||||
date: '2026-02-01',
|
||||
versionDesc: '初始版本',
|
||||
status: 'approved',
|
||||
publicName: '简单翻译',
|
||||
publicDesc: '基础中英互译工具',
|
||||
category: '效率工具',
|
||||
tags: ['翻译'],
|
||||
icon: '🌐',
|
||||
installs: 456,
|
||||
rating: 4.5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
skillId: 'SKL-2026-0006',
|
||||
name: '智能客服机器人',
|
||||
desc: '自动回复客服咨询的AI助手',
|
||||
status: 'unlisting',
|
||||
hasPendingReview: false,
|
||||
lastModified: '2026-03-20',
|
||||
versions: [
|
||||
{
|
||||
version: '1.5.0',
|
||||
date: '2026-03-10',
|
||||
versionDesc: '新增多轮对话支持',
|
||||
status: 'approved',
|
||||
publicName: '智能客服',
|
||||
publicDesc: '7x24小时自动回复,支持常见问题解答',
|
||||
category: '业务系统',
|
||||
tags: ['客服', 'AI', '自动化'],
|
||||
icon: '🤖',
|
||||
installs: 234,
|
||||
rating: 4.6
|
||||
},
|
||||
{
|
||||
version: '1.0.0',
|
||||
date: '2026-02-15',
|
||||
versionDesc: '初始版本',
|
||||
status: 'approved',
|
||||
publicName: '客服机器人',
|
||||
publicDesc: '简单的自动回复机器人',
|
||||
category: '业务系统',
|
||||
tags: ['客服', '机器人'],
|
||||
icon: '🤖',
|
||||
installs: 120,
|
||||
rating: 4.3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
skillId: 'SKL-2026-0007',
|
||||
name: '旧版数据分析工具',
|
||||
desc: '已废弃的数据分析项目',
|
||||
status: 'unlisted',
|
||||
hasPendingReview: false,
|
||||
lastModified: '2026-02-28',
|
||||
versions: [
|
||||
{
|
||||
version: '1.0.0',
|
||||
date: '2026-01-20',
|
||||
versionDesc: '初始版本',
|
||||
status: 'approved',
|
||||
publicName: '数据分析工具',
|
||||
publicDesc: '基础数据统计和分析',
|
||||
category: '数据分析',
|
||||
tags: ['数据', '分析', '统计'],
|
||||
icon: '📊',
|
||||
installs: 78,
|
||||
rating: 4.2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
skillId: 'SKL-2026-0008',
|
||||
name: '文档格式转换',
|
||||
desc: 'Word/PDF/Excel格式互转工具',
|
||||
status: 'dev',
|
||||
hasPendingReview: false,
|
||||
lastModified: '2026-03-18',
|
||||
versions: [
|
||||
{
|
||||
version: '0.2.0',
|
||||
date: '2026-03-18',
|
||||
versionDesc: '修复转换乱码问题',
|
||||
status: 'withdrawn',
|
||||
publicName: '文档转换器',
|
||||
publicDesc: '支持多种文档格式互相转换',
|
||||
category: '文档处理',
|
||||
tags: ['文档', '转换', '办公'],
|
||||
icon: '📝',
|
||||
installs: 0,
|
||||
rating: 0
|
||||
},
|
||||
{
|
||||
version: '0.1.0',
|
||||
date: '2026-03-15',
|
||||
versionDesc: '初始测试版本',
|
||||
status: 'rejected',
|
||||
rejectionReason: '转换质量有待提升,部分格式存在乱码,请优化后重新提交',
|
||||
publicName: '文档转换工具',
|
||||
publicDesc: '简单的文档格式转换',
|
||||
category: '文档处理',
|
||||
tags: ['文档', '转换'],
|
||||
icon: '📝',
|
||||
installs: 0,
|
||||
rating: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
skillId: 'SKL-2026-0009',
|
||||
name: '智能日程管理',
|
||||
desc: 'AI驱动的日程安排和提醒',
|
||||
status: 'published',
|
||||
hasPendingReview: false,
|
||||
lastModified: '2026-03-17',
|
||||
versions: [
|
||||
{
|
||||
version: '3.0.0',
|
||||
date: '2026-03-17',
|
||||
versionDesc: 'AI智能推荐升级',
|
||||
status: 'approved',
|
||||
publicName: '智能日程助手',
|
||||
publicDesc: 'AI智能分析日程,智能推荐最佳安排时间',
|
||||
category: '效率工具',
|
||||
tags: ['日程', 'AI', '效率', '提醒'],
|
||||
icon: '📅',
|
||||
installs: 567,
|
||||
rating: 4.9
|
||||
},
|
||||
{
|
||||
version: '2.0.0',
|
||||
date: '2026-02-25',
|
||||
versionDesc: '新增团队协作',
|
||||
status: 'approved',
|
||||
publicName: '日程管理',
|
||||
publicDesc: '个人和团队日程管理工具',
|
||||
category: '效率工具',
|
||||
tags: ['日程', '协作', '效率'],
|
||||
icon: '📅',
|
||||
installs: 423,
|
||||
rating: 4.7
|
||||
},
|
||||
{
|
||||
version: '1.0.0',
|
||||
date: '2026-01-30',
|
||||
versionDesc: '初始版本',
|
||||
status: 'approved',
|
||||
publicName: '简单日程',
|
||||
publicDesc: '基础日程管理',
|
||||
category: '效率工具',
|
||||
tags: ['日程'],
|
||||
icon: '📅',
|
||||
installs: 156,
|
||||
rating: 4.4
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
skillId: 'SKL-2026-0010',
|
||||
name: 'API测试工具',
|
||||
desc: 'RESTful API接口测试',
|
||||
status: 'dev',
|
||||
hasPendingReview: true,
|
||||
lastModified: '2026-03-21',
|
||||
versions: [
|
||||
{
|
||||
version: '1.0.0',
|
||||
date: '2026-03-21',
|
||||
versionDesc: '首个正式版本',
|
||||
status: 'reviewing',
|
||||
publicName: 'API测试助手',
|
||||
publicDesc: '支持GET/POST/PUT/DELETE请求,自动生成测试报告',
|
||||
category: '开发工具',
|
||||
tags: ['API', '测试', '开发', '接口'],
|
||||
icon: '🔧',
|
||||
installs: 0,
|
||||
rating: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -71,20 +386,30 @@ export const devDocs = [
|
||||
];
|
||||
|
||||
export const developerOverview = {
|
||||
totalSkills: 3,
|
||||
publishedCount: 2,
|
||||
draftCount: 1,
|
||||
pendingReview: 1,
|
||||
totalInstalls: 498,
|
||||
totalSkills: 10,
|
||||
publishedCount: 4,
|
||||
draftCount: 4,
|
||||
pendingReview: 3,
|
||||
totalInstalls: 2617,
|
||||
pendingItems: [
|
||||
{ skillId: 2, skillName: '待办事项管理', version: '0.1.0', status: 'pending', date: '2026-03-17' },
|
||||
{ skillId: 3, skillName: '代码审查助手', version: '2.0.0', status: 'rejected', date: '2026-03-10', rejectionReason: '安全审查未通过,存在潜在的代码注入风险,请修复后重新提交' }
|
||||
{ skillId: 5, skillName: '翻译助手Pro', version: '2.1.0', status: 'pending', date: '2026-03-19' },
|
||||
{ skillId: 10, skillName: 'API测试工具', version: '1.0.0', status: 'pending', date: '2026-03-21' },
|
||||
{ skillId: 3, skillName: '代码审查项目', version: '2.0.0', status: 'rejected', date: '2026-03-10', rejectionReason: '安全审查未通过,存在潜在的代码注入风险,请修复后重新提交' },
|
||||
{ skillId: 8, skillName: '文档格式转换', version: '0.1.0', status: 'rejected', date: '2026-03-15', rejectionReason: '转换质量有待提升,部分格式存在乱码,请优化后重新提交' }
|
||||
],
|
||||
recentActivity: [
|
||||
{ time: '2026-03-18', action: '发布天气查询助手 v1.2.0', status: '审核中' },
|
||||
{ time: '2026-03-15', action: '更新代码审查助手 v2.0.1', status: '审核通过' },
|
||||
{ time: '2026-03-10', action: '代码审查助手 v2.0.0', status: '审核拒绝' },
|
||||
{ time: '2026-03-08', action: '上传代码审查助手 v2.0.0', status: '审核通过' },
|
||||
{ time: '2026-03-01', action: '发布天气查询助手 v1.0.0', status: '审核通过' }
|
||||
{ time: '2026-03-21', action: '上传 API测试工具 v1.0.0', status: '审核中' },
|
||||
{ time: '2026-03-20', action: '申请下架 智能客服机器人', status: '下架审核中' },
|
||||
{ time: '2026-03-19', action: '上传 翻译助手Pro v2.1.0', status: '审核中' },
|
||||
{ time: '2026-03-18', action: '撤回 文档格式转换 v0.2.0', status: '已撤销' },
|
||||
{ time: '2026-03-18', action: '发布 天气小工具 v1.2.0', status: '审核通过' },
|
||||
{ time: '2026-03-17', action: '更新 智能日程管理 v3.0.0', status: '审核通过' },
|
||||
{ time: '2026-03-17', action: '上传 待办事项管理 v0.1.0', status: '审核中' },
|
||||
{ time: '2026-03-15', action: '更新 代码审查项目 v2.0.1', status: '审核通过' },
|
||||
{ time: '2026-03-15', action: '文档格式转换 v0.1.0', status: '审核拒绝' },
|
||||
{ time: '2026-03-10', action: '代码审查项目 v2.0.0', status: '审核拒绝' },
|
||||
{ time: '2026-03-08', action: '上传 代码审查项目 v2.0.0', status: '审核通过' },
|
||||
{ time: '2026-03-01', action: '发布 天气小工具 v1.0.0', status: '审核通过' }
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,12 +1,96 @@
|
||||
// skills data
|
||||
|
||||
// 技能商店数据 - 展示信息从当前生效版本取
|
||||
export const skills = [
|
||||
{ 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 }
|
||||
{
|
||||
id: 1,
|
||||
author: 'GrandClaw Team',
|
||||
status: 'published',
|
||||
hasPendingReview: false,
|
||||
subs: 1256,
|
||||
subscribed: true,
|
||||
currentVersion: {
|
||||
version: 'v1.3.0',
|
||||
publicName: '代码生成助手',
|
||||
publicDesc: '根据需求自动生成高质量代码,支持多种编程语言',
|
||||
category: '开发工具',
|
||||
tags: ['开发', '代码', 'AI'],
|
||||
icon: '💻'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
author: 'DataLab',
|
||||
status: 'published',
|
||||
hasPendingReview: true,
|
||||
subs: 892,
|
||||
subscribed: true,
|
||||
currentVersion: {
|
||||
version: 'v2.1.0',
|
||||
publicName: '数据分析专家',
|
||||
publicDesc: '智能分析数据,生成可视化图表和洞察报告',
|
||||
category: '数据分析',
|
||||
tags: ['数据', '分析', '可视化'],
|
||||
icon: '📊'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
author: 'DocAI',
|
||||
status: 'dev',
|
||||
hasPendingReview: false,
|
||||
subs: 0,
|
||||
subscribed: false,
|
||||
currentVersion: null
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
author: 'Telecom',
|
||||
status: 'unlisting',
|
||||
hasPendingReview: false,
|
||||
subs: 567,
|
||||
subscribed: false,
|
||||
currentVersion: {
|
||||
version: 'v1.5.0',
|
||||
publicName: 'CRM 客户查询',
|
||||
publicDesc: '对接企业CRM系统,快速查询客户信息和订单状态',
|
||||
category: '业务系统',
|
||||
tags: ['业务', 'CRM', '客户'],
|
||||
icon: '👥'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
author: 'Finance Team',
|
||||
status: 'unlisted',
|
||||
hasPendingReview: false,
|
||||
subs: 0,
|
||||
subscribed: false,
|
||||
currentVersion: {
|
||||
version: 'v1.2.0',
|
||||
publicName: '财务数据同步',
|
||||
publicDesc: '自动同步财务系统数据,生成费用报表',
|
||||
category: '业务系统',
|
||||
tags: ['财务', '报表', '同步'],
|
||||
icon: '📈'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
author: 'NetOps',
|
||||
status: 'published',
|
||||
hasPendingReview: false,
|
||||
subs: 789,
|
||||
subscribed: false,
|
||||
currentVersion: {
|
||||
version: 'v1.1.0',
|
||||
publicName: '网络故障排查',
|
||||
publicDesc: '智能诊断网络问题,提供故障排除方案',
|
||||
category: '开发工具',
|
||||
tags: ['运维', '网络', '诊断'],
|
||||
icon: '🔧'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export const skillFiles = [
|
||||
@@ -17,21 +101,116 @@ export const skillFiles = [
|
||||
];
|
||||
|
||||
export const skillVersions = [
|
||||
{ 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' }
|
||||
{
|
||||
version: 'v1.3.0',
|
||||
date: '2026-03-12',
|
||||
versionDesc: '新增 Python 3.11 支持',
|
||||
status: 'approved',
|
||||
publicName: '代码生成助手',
|
||||
publicDesc: '根据需求自动生成高质量代码,支持多种编程语言',
|
||||
category: '开发工具',
|
||||
tags: ['开发', '代码', 'AI'],
|
||||
icon: '💻'
|
||||
},
|
||||
{
|
||||
version: 'v1.2.1',
|
||||
date: '2026-03-08',
|
||||
versionDesc: '修复若干已知问题',
|
||||
status: 'rejected',
|
||||
rejectionReason: '测试用例覆盖不完整,请补充单元测试',
|
||||
publicName: '代码生成助手',
|
||||
publicDesc: '根据需求自动生成高质量代码',
|
||||
category: '开发工具',
|
||||
tags: ['开发', '代码'],
|
||||
icon: '💻'
|
||||
},
|
||||
{
|
||||
version: 'v1.2.0',
|
||||
date: '2026-03-01',
|
||||
versionDesc: '优化性能,提升响应速度 30%',
|
||||
status: 'approved',
|
||||
publicName: '代码生成助手',
|
||||
publicDesc: '根据需求自动生成高质量代码',
|
||||
category: '开发工具',
|
||||
tags: ['开发', '代码'],
|
||||
icon: '💻'
|
||||
},
|
||||
{
|
||||
version: 'v1.1.5',
|
||||
date: '2026-02-20',
|
||||
versionDesc: '紧急修复安全漏洞',
|
||||
status: 'withdrawn',
|
||||
publicName: '代码生成',
|
||||
publicDesc: '代码生成工具',
|
||||
category: '开发工具',
|
||||
tags: ['代码'],
|
||||
icon: '💻'
|
||||
},
|
||||
{
|
||||
version: 'v1.1.0',
|
||||
date: '2026-02-15',
|
||||
versionDesc: '新增 JavaScript 支持',
|
||||
status: 'reviewing',
|
||||
publicName: '代码生成助手 Pro',
|
||||
publicDesc: '根据需求自动生成高质量代码,支持 JavaScript',
|
||||
category: '开发工具',
|
||||
tags: ['开发', '代码', 'JS'],
|
||||
icon: '🚀'
|
||||
}
|
||||
];
|
||||
|
||||
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: '王五' }
|
||||
{
|
||||
id: 1,
|
||||
skillName: '代码生成助手 Pro',
|
||||
version: 'v1.4.0',
|
||||
date: '2026-03-20',
|
||||
developer: '张三',
|
||||
publicName: '代码生成助手 Pro',
|
||||
publicDesc: '根据需求自动生成高质量代码,支持多种编程语言',
|
||||
category: '开发工具',
|
||||
tags: ['开发', '代码', 'AI'],
|
||||
icon: '💻'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
skillName: '数据分析专家',
|
||||
version: 'v2.0.0',
|
||||
date: '2026-03-19',
|
||||
developer: '李四',
|
||||
publicName: '数据分析专家',
|
||||
publicDesc: '智能分析数据,生成可视化图表和洞察报告',
|
||||
category: '数据分析',
|
||||
tags: ['数据', '分析', '可视化'],
|
||||
icon: '📊'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
skillName: '文档智能撰写',
|
||||
version: 'v1.0.0',
|
||||
date: '2026-03-18',
|
||||
developer: '王五',
|
||||
publicName: '文档智能撰写',
|
||||
publicDesc: '帮助撰写各种文档,包括报告、邮件、技术文档等',
|
||||
category: '文档处理',
|
||||
tags: ['文档', '写作', '办公'],
|
||||
icon: '📝'
|
||||
}
|
||||
];
|
||||
|
||||
export const pendingUnlistReviews = [
|
||||
{ id: 1, skillName: 'CRM 客户查询', currentVersion: 'v1.5.0', date: '2026-03-20', developer: '赵六' }
|
||||
{
|
||||
id: 1,
|
||||
skillName: 'CRM 客户查询',
|
||||
currentVersion: 'v1.5.0',
|
||||
date: '2026-03-20',
|
||||
developer: '赵六',
|
||||
publicName: 'CRM 客户查询',
|
||||
publicDesc: '对接企业CRM系统,快速查询客户信息和订单状态',
|
||||
category: '业务系统',
|
||||
tags: ['业务', 'CRM', '客户'],
|
||||
icon: '👥'
|
||||
}
|
||||
];
|
||||
|
||||
// 技能图标映射
|
||||
|
||||
@@ -45,27 +45,25 @@ function ConsoleReviewDetailPage({ type, reviewId, onBack }) {
|
||||
<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>
|
||||
<h3>发布信息(商店展示)</h3>
|
||||
<div style={{ display: 'flex', gap: '16px', alignItems: 'flex-start', marginBottom: '16px', padding: '16px', background: '#F8FAFC', borderRadius: '8px' }}>
|
||||
<div style={{ fontSize: '48px' }}>{review.icon}</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
|
||||
<h3 style={{ margin: 0, fontSize: '18px', color: '#1E293B' }}>{review.publicName}</h3>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px', marginBottom: '12px' }}>
|
||||
<span className="dev-detail-tag" style={{ background: '#EFF6FF', color: '#1E40AF', fontWeight: '600' }}>{review.category}</span>
|
||||
{review.tags?.map(tag => (
|
||||
<span key={tag} className="dev-detail-tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
<p style={{ margin: 0, color: '#475569', lineHeight: '1.6' }}>
|
||||
{review.publicDesc}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,6 +81,10 @@ function ConsoleReviewDetailPage({ type, reviewId, onBack }) {
|
||||
<span className="dev-info-label">版本说明</span>
|
||||
<span className="dev-info-value">优化性能,提升响应速度 30%,修复若干已知问题</span>
|
||||
</div>
|
||||
<div className="dev-info-row">
|
||||
<span className="dev-info-label">开发者</span>
|
||||
<span className="dev-info-value">{review.developer}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dev-detail-section">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { FiChevronLeft, FiFile } from 'react-icons/fi';
|
||||
import { skills, getSkillIcon, skillFiles, skillVersions } from '../../data/skills.js';
|
||||
import { skills, skillFiles } from '../../data/skills.js';
|
||||
import Modal from '../../components/common/Modal.jsx';
|
||||
|
||||
function SkillDetailPage({ skillId, onBack }) {
|
||||
@@ -29,74 +29,80 @@ function SkillDetailPage({ skillId, onBack }) {
|
||||
setShowUnsubModal(false);
|
||||
};
|
||||
|
||||
const currentVersion = skill.currentVersion;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="skill-back-btn" onClick={onBack}>
|
||||
<FiChevronLeft /> 返回技能市场
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<div className="skill-detail-header">
|
||||
<div className="skill-detail-icon">{getSkillIcon(skill.id)}</div>
|
||||
<div className="skill-detail-main">
|
||||
<h2 style={{ fontSize: '22px', fontWeight: 800, marginBottom: '6px' }}>{skill.name}</h2>
|
||||
<p style={{ color: '#64748B', marginBottom: '8px' }}>by {skill.author}</p>
|
||||
<div className="skill-detail-tags">
|
||||
{skill.tags.map(tag => (
|
||||
<span key={tag} className="skill-detail-tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="skill-detail-stats">
|
||||
<span>👤 {skill.subs.toLocaleString()} 订阅</span>
|
||||
<span>⭐ {skill.rating} 评分</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
<button className={`btn ${subscribed ? '' : 'btn-primary'}`} onClick={handleSubscribeClick}>
|
||||
{subscribed ? '取消订阅' : '立即订阅'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="skill-detail-section">
|
||||
<h3>使用说明</h3>
|
||||
<p style={{ color: '#475569', lineHeight: 1.8 }}>
|
||||
{skill.desc}。安装后,您可以在对话中直接调用该技能。例如,您可以说:
|
||||
</p>
|
||||
<div style={{ marginTop: '12px', padding: '12px 16px', background: '#F8FAFC', borderRadius: '8px', color: '#3B82F6', fontFamily: 'monospace' }}>
|
||||
"帮我用这个技能 查询一下数据"
|
||||
</div>
|
||||
</div>
|
||||
<div className="skill-detail-section">
|
||||
<h3>文件列表</h3>
|
||||
{skillFiles.map(file => (
|
||||
<div key={file.name} 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>
|
||||
{currentVersion ? (
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<div className="skill-detail-header">
|
||||
<div className="skill-detail-icon">{currentVersion.icon}</div>
|
||||
<div className="skill-detail-main">
|
||||
<h2 style={{ fontSize: '22px', fontWeight: 800, marginBottom: '6px' }}>{currentVersion.publicName}</h2>
|
||||
<p style={{ color: '#64748B', marginBottom: '8px' }}>by {skill.author}</p>
|
||||
<div className="skill-detail-tags">
|
||||
<span className="skill-detail-tag" style={{ background: '#EFF6FF', color: '#1E40AF', fontWeight: '600' }}>{currentVersion.category}</span>
|
||||
{currentVersion.tags.map(tag => (
|
||||
<span key={tag} className="skill-detail-tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="skill-detail-stats">
|
||||
<span>👤 {skill.subs.toLocaleString()} 订阅</span>
|
||||
<span>⭐ {currentVersion.rating || 0} 评分</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="skill-detail-section">
|
||||
<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 style={{ flexShrink: 0 }}>
|
||||
<button className={`btn ${subscribed ? '' : 'btn-primary'}`} onClick={handleSubscribeClick}>
|
||||
{subscribed ? '取消订阅' : '立即订阅'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="skill-detail-section">
|
||||
<h3>使用说明</h3>
|
||||
<p style={{ color: '#475569', lineHeight: 1.8 }}>
|
||||
{currentVersion.publicDesc}。安装后,您可以在对话中直接调用该技能。例如,您可以说:
|
||||
</p>
|
||||
<div style={{ marginTop: '12px', padding: '12px 16px', background: '#F8FAFC', borderRadius: '8px', color: '#3B82F6', fontFamily: 'monospace' }}>
|
||||
"帮我用这个技能 查询一下数据"
|
||||
</div>
|
||||
</div>
|
||||
<div className="skill-detail-section">
|
||||
<h3>文件列表</h3>
|
||||
{skillFiles.map(file => (
|
||||
<div key={file.name} 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 className="version-date">{approvedVersion.date}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ color: '#94A3B8' }}>暂无版本信息</div>
|
||||
);
|
||||
})()}
|
||||
))}
|
||||
</div>
|
||||
<div className="skill-detail-section">
|
||||
<h3>当前版本</h3>
|
||||
<div className="version-list-item">
|
||||
<div className="version-info">
|
||||
<span className="version-tag current">v{currentVersion.version}</span>
|
||||
<span className="version-desc">{currentVersion.publicDesc}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-icon">📦</div>
|
||||
<div className="empty-state-text">该技能暂无可用版本</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
visible={showUnsubModal}
|
||||
title="确认取消订阅"
|
||||
@@ -104,10 +110,10 @@ function SkillDetailPage({ skillId, onBack }) {
|
||||
onCancel={cancelUnsubscribe}
|
||||
confirmText="取消订阅"
|
||||
>
|
||||
确定要取消订阅"{skill.name}"吗?取消后将无法使用该技能。
|
||||
确定要取消订阅"{currentVersion?.publicName || skill.name}"吗?取消后将无法使用该技能。
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SkillDetailPage;
|
||||
export default SkillDetailPage;
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import { useState } from 'react';
|
||||
import { FiUser, FiStar, FiSearch } from 'react-icons/fi';
|
||||
import { FaBoxOpen } from 'react-icons/fa';
|
||||
import { skills, getSkillIcon } from '../../data/skills.js';
|
||||
import { skills } from '../../data/skills.js';
|
||||
import EmptyState from '../../components/common/EmptyState.jsx';
|
||||
import Modal from '../../components/common/Modal.jsx';
|
||||
|
||||
function SkillCard({ skill, onClick, onSubscribe }) {
|
||||
const currentVersion = skill.currentVersion;
|
||||
if (!currentVersion) return null;
|
||||
|
||||
return (
|
||||
<div className="skill-card" onClick={onClick}>
|
||||
<div className="skill-header">
|
||||
<div className="skill-icon">{getSkillIcon(skill.id)}</div>
|
||||
<div className="skill-icon">{currentVersion.icon}</div>
|
||||
<div className="skill-info">
|
||||
<div className="skill-name">{skill.name}</div>
|
||||
<div className="skill-name">{currentVersion.publicName}</div>
|
||||
<div className="skill-author">{skill.author}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="skill-desc">{skill.desc}</div>
|
||||
<div className="skill-desc">{currentVersion.publicDesc}</div>
|
||||
<div className="skill-tags">
|
||||
{skill.tags.map(tag => (
|
||||
<span className="skill-tag" style={{ background: '#EFF6FF', color: '#1E40AF', fontWeight: '600' }}>{currentVersion.category}</span>
|
||||
{currentVersion.tags.map(tag => (
|
||||
<span key={tag} className="skill-tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -27,11 +31,11 @@ function SkillCard({ skill, onClick, onSubscribe }) {
|
||||
<FiUser /> {skill.subs}
|
||||
</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '4px', color: '#94A3B8', fontSize: '13px' }}>
|
||||
<FiStar /> {skill.rating}
|
||||
<FiStar /> {currentVersion.rating || 0}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className={`btn ${skill.subscribed ? 'btn-primary' : ''} btn-sm`}
|
||||
<button
|
||||
className={`btn ${skill.subscribed ? 'btn-primary' : ''} btn-sm`}
|
||||
onClick={e => { e.stopPropagation(); onSubscribe(skill); }}
|
||||
>
|
||||
{skill.subscribed ? '已订阅' : '订阅'}
|
||||
@@ -53,18 +57,24 @@ function SkillsPage({ onSkillClick }) {
|
||||
: [...skillsState];
|
||||
|
||||
const searchedSkills = searchQuery
|
||||
? filteredSkills.filter(s =>
|
||||
s.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
s.desc.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
s.tags.some(t => t.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
)
|
||||
? filteredSkills.filter(s => {
|
||||
const cv = s.currentVersion;
|
||||
if (!cv) return false;
|
||||
return cv.publicName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
cv.publicDesc.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
cv.category.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
cv.tags.some(t => t.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||
})
|
||||
: filteredSkills;
|
||||
|
||||
searchedSkills.sort((a, b) => {
|
||||
if (sort === 'subs') return b.subs - a.subs;
|
||||
if (sort === 'rating') return b.rating - a.rating;
|
||||
return 0;
|
||||
});
|
||||
// 只显示有当前生效版本的已上架技能
|
||||
const displaySkills = searchedSkills
|
||||
.filter(s => s.status === 'published' && s.currentVersion)
|
||||
.sort((a, b) => {
|
||||
if (sort === 'subs') return b.subs - a.subs;
|
||||
if (sort === 'rating') return (b.currentVersion?.rating || 0) - (a.currentVersion?.rating || 0);
|
||||
return 0;
|
||||
});
|
||||
|
||||
const handleSubscribeClick = (skill) => {
|
||||
setModalTarget(skill);
|
||||
@@ -72,7 +82,7 @@ function SkillsPage({ onSkillClick }) {
|
||||
|
||||
const confirmSubscribe = () => {
|
||||
if (modalTarget) {
|
||||
setSkillsState(prev => prev.map(s =>
|
||||
setSkillsState(prev => prev.map(s =>
|
||||
s.id === modalTarget.id ? { ...s, subscribed: !s.subscribed } : s
|
||||
));
|
||||
setModalTarget(null);
|
||||
@@ -90,10 +100,10 @@ function SkillsPage({ onSkillClick }) {
|
||||
<div className="search-bar">
|
||||
<div className="search-item" style={{ flex: 1, minWidth: '200px' }}>
|
||||
<label>关键词</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="搜索技能..."
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="搜索技能..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
@@ -129,12 +139,12 @@ function SkillsPage({ onSkillClick }) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{searchedSkills.length > 0 ? (
|
||||
{displaySkills.length > 0 ? (
|
||||
<div className="skill-grid">
|
||||
{searchedSkills.map(skill => (
|
||||
<SkillCard
|
||||
key={skill.id}
|
||||
skill={skill}
|
||||
{displaySkills.map(skill => (
|
||||
<SkillCard
|
||||
key={skill.id}
|
||||
skill={skill}
|
||||
onClick={() => onSkillClick(skill.id)}
|
||||
onSubscribe={handleSubscribeClick}
|
||||
/>
|
||||
@@ -154,13 +164,13 @@ function SkillsPage({ onSkillClick }) {
|
||||
onCancel={cancelSubscribe}
|
||||
confirmText={modalTarget?.subscribed ? '取消订阅' : '订阅'}
|
||||
>
|
||||
{modalTarget?.subscribed
|
||||
? `确定要取消订阅"${modalTarget?.name}"吗?取消后将无法使用该技能。`
|
||||
: `确定要订阅"${modalTarget?.name}"吗?`
|
||||
{modalTarget?.subscribed
|
||||
? `确定要取消订阅"${modalTarget?.currentVersion?.publicName}"吗?取消后将无法使用该技能。`
|
||||
: `确定要订阅"${modalTarget?.currentVersion?.publicName}"吗?`
|
||||
}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SkillsPage;
|
||||
export default SkillsPage;
|
||||
|
||||
@@ -12,8 +12,7 @@ const skillStatusMap = {
|
||||
|
||||
function MySkillsPage({ onSkillClick }) {
|
||||
const sourceData = api.developer.getMySkills();
|
||||
const categories = api.developer.getCategories();
|
||||
const [filters, setFilters] = useState({ keyword: '', category: '', status: '' });
|
||||
const [filters, setFilters] = useState({ keyword: '', status: '' });
|
||||
const [deleteTarget, setDeleteTarget] = useState(null);
|
||||
const [toast, setToast] = useState({ visible: false, type: 'success', message: '' });
|
||||
|
||||
@@ -22,16 +21,13 @@ function MySkillsPage({ onSkillClick }) {
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setFilters({ keyword: '', category: '', status: '' });
|
||||
setFilters({ keyword: '', status: '' });
|
||||
};
|
||||
|
||||
const filteredList = sourceData.filter(skill => {
|
||||
if (filters.keyword && !skill.name.includes(filters.keyword) && !skill.desc.includes(filters.keyword)) {
|
||||
return false;
|
||||
}
|
||||
if (filters.category && skill.category !== filters.category) {
|
||||
return false;
|
||||
}
|
||||
if (filters.status && skill.status !== filters.status) return false;
|
||||
return true;
|
||||
});
|
||||
@@ -61,24 +57,11 @@ function MySkillsPage({ onSkillClick }) {
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="搜索技能名称、描述..."
|
||||
placeholder="搜索内部名称、描述..."
|
||||
value={filters.keyword}
|
||||
onChange={e => handleFilterChange('keyword', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="search-item">
|
||||
<label>分类</label>
|
||||
<select
|
||||
className="form-control"
|
||||
value={filters.category}
|
||||
onChange={e => handleFilterChange('category', e.target.value)}
|
||||
>
|
||||
<option value="">全部分类</option>
|
||||
{categories.map(cat => (
|
||||
<option key={cat} value={cat}>{cat}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="search-item">
|
||||
<label>状态</label>
|
||||
<select
|
||||
@@ -109,9 +92,8 @@ function MySkillsPage({ onSkillClick }) {
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>技能名称</th>
|
||||
<th>技能描述</th>
|
||||
<th>分类</th>
|
||||
<th>内部名称</th>
|
||||
<th>内部描述</th>
|
||||
<th>状态</th>
|
||||
<th style={{ width: '200px' }}>操作</th>
|
||||
</tr>
|
||||
@@ -121,7 +103,6 @@ function MySkillsPage({ onSkillClick }) {
|
||||
<tr key={skill.id} onClick={() => onSkillClick(skill.id)} style={{ cursor: 'pointer' }}>
|
||||
<td>{skill.name}</td>
|
||||
<td>{skill.desc}</td>
|
||||
<td>{skill.category}</td>
|
||||
<td>
|
||||
<span className={`status ${skillStatusMap[skill.status]?.className || 'status-stopped'}`}>
|
||||
{skillStatusMap[skill.status]?.text || skill.status}
|
||||
@@ -133,8 +114,8 @@ function MySkillsPage({ onSkillClick }) {
|
||||
编辑
|
||||
</button>
|
||||
{skill.status === 'published' && (
|
||||
<button
|
||||
className="text-btn text-btn-danger"
|
||||
<button
|
||||
className="text-btn text-btn-danger"
|
||||
onClick={e => handleUnpublish(e, skill)}
|
||||
disabled={skill.hasPendingReview}
|
||||
title={skill.hasPendingReview ? '存在审核中的版本,请先撤回后再下架' : ''}
|
||||
@@ -142,11 +123,11 @@ function MySkillsPage({ onSkillClick }) {
|
||||
下架
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="text-btn text-btn-danger"
|
||||
<button
|
||||
className="text-btn text-btn-danger"
|
||||
onClick={e => handleDelete(e, skill)}
|
||||
disabled={skill.status === 'published'}
|
||||
title={skill.status === 'published' ? '已上架的技能需要先下架才能删除' : ''}
|
||||
disabled={skill.status === 'published' || skill.status === 'unlisting' || skill.hasPendingReview}
|
||||
title={skill.status === 'published' ? '已上架的技能需要先下架才能删除' : (skill.status === 'unlisting' ? '下架审核中的技能不能删除' : (skill.hasPendingReview ? '存在审核中的版本,请先撤回后再删除' : ''))}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { FiChevronLeft, FiUpload, FiUsers, FiPackage, FiStar } from 'react-icons/fi';
|
||||
import { FiChevronLeft, FiUpload, FiUsers, FiPackage, FiStar, FiRotateCcw } from 'react-icons/fi';
|
||||
import { api } from '../../services/api.js';
|
||||
import Modal from '../../components/common/Modal.jsx';
|
||||
import Toast from '../../components/common/Toast.jsx';
|
||||
@@ -21,6 +21,7 @@ const skillStatusMap = {
|
||||
function SkillEditorPage({ skillId, onBack, onUploadNewVersion, onUpdateInfo }) {
|
||||
const skill = api.developer.getSkillById(skillId);
|
||||
const [deleteSkillModal, setDeleteSkillModal] = useState(false);
|
||||
const [unlistSkillModal, setUnlistSkillModal] = useState(false);
|
||||
const [deleteVersionTarget, setDeleteVersionTarget] = useState(null);
|
||||
const [toast, setToast] = useState({ visible: false, type: 'success', message: '' });
|
||||
|
||||
@@ -28,9 +29,9 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion, onUpdateInfo })
|
||||
return <div>Skill not found</div>;
|
||||
}
|
||||
|
||||
const handleTogglePublish = () => {
|
||||
const msg = skill.status === 'published' ? '已下架' : '已上架';
|
||||
setToast({ visible: true, type: 'success', message: msg });
|
||||
const handleUnlistSkill = () => {
|
||||
setUnlistSkillModal(false);
|
||||
setToast({ visible: true, type: 'success', message: '已提交下架申请' });
|
||||
};
|
||||
|
||||
const handleDeleteSkill = () => {
|
||||
@@ -43,8 +44,8 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion, onUpdateInfo })
|
||||
setToast({ visible: true, type: 'success', message: '已删除' });
|
||||
};
|
||||
|
||||
const currentVersion = skill.versions && skill.versions.length > 0
|
||||
? skill.versions.find(v => v.status === 'approved') || skill.versions[0]
|
||||
const currentVersion = skill.versions && skill.versions.length > 0
|
||||
? skill.versions.find(v => v.status === 'approved')
|
||||
: null;
|
||||
|
||||
return (
|
||||
@@ -52,123 +53,142 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion, onUpdateInfo })
|
||||
<div className="dev-back-btn" onClick={onBack}>
|
||||
<FiChevronLeft /> 返回我的技能
|
||||
</div>
|
||||
|
||||
{/* 1. 技能概览卡片(三段式布局第一段) */}
|
||||
|
||||
{/* 1. 开发者内部信息概览卡片 */}
|
||||
<div className="skill-overview-card">
|
||||
<div className="skill-icon">{skill.icon || skill.name.charAt(0)}</div>
|
||||
<div className="skill-header">
|
||||
{/* 第一行:技能名称 + 状态 + 右上角操作按钮 */}
|
||||
<div className="skill-name-row">
|
||||
<h2 className="skill-name">{skill.name}</h2>
|
||||
<span className={`status ${skillStatusMap[skill.status]?.className || 'status-stopped'}`}>
|
||||
{skillStatusMap[skill.status]?.text || skill.status}
|
||||
</span>
|
||||
<div className="skill-actions">
|
||||
<button className="btn btn-primary btn-sm" onClick={() => onUpdateInfo && onUpdateInfo(skill.id)}>编辑基本信息</button>
|
||||
<button className="btn btn-primary btn-sm" onClick={() => onUpdateInfo && onUpdateInfo(skill.id)}>编辑内部信息</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 第二行:指标行(带分隔线) */}
|
||||
<div className="skill-metrics-row">
|
||||
<div className="metric-item">
|
||||
<FiUsers className="metric-icon" />
|
||||
<span className="metric-value">{skill.installs || 0}</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<FiStar className="metric-icon" />
|
||||
<span className="metric-value">{skill.rating || 0}</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<FiPackage className="metric-icon" />
|
||||
<span className="metric-value">{skill.version || 'v1.0.0'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 第三行:标签区 */}
|
||||
<div className="skill-tags-row">
|
||||
<span className="skill-category-tag">{skill.category}</span>
|
||||
{skill.tags.map(tag => (
|
||||
<span key={tag} className="dev-detail-tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 第四行:技能描述 */}
|
||||
<div className="skill-desc-row">
|
||||
<p className="skill-desc-text">{skill.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2. 版本历史卡片(三段式布局第二段) */}
|
||||
{/* 2. 当前生效版本卡片 */}
|
||||
{currentVersion && (
|
||||
<div className="card" style={{ marginTop: '16px' }}>
|
||||
<div className="card-header">
|
||||
<div className="card-title">当前生效版本</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div style={{ display: 'flex', gap: '16px', alignItems: 'flex-start' }}>
|
||||
<div style={{ fontSize: '48px' }}>{currentVersion.icon}</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
|
||||
<h3 style={{ margin: 0, fontSize: '18px', color: '#1E293B' }}>{currentVersion.publicName}</h3>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px', marginBottom: '12px' }}>
|
||||
<span className="dev-detail-tag" style={{ background: '#EFF6FF', color: '#1E40AF', fontWeight: '600' }}>{currentVersion.category}</span>
|
||||
{currentVersion.tags.map(tag => (
|
||||
<span key={tag} className="dev-detail-tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
<p style={{ margin: '0 0 16px 0', color: '#475569', lineHeight: '1.6' }}>
|
||||
{currentVersion.publicDesc}
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '24px', color: '#64748B', fontSize: '14px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<FiUsers />
|
||||
<span>{currentVersion.installs || 0} 安装</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<FiStar />
|
||||
<span>{currentVersion.rating || 0} 评分</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<FiPackage />
|
||||
<span>v{currentVersion.version}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 3. 版本历史卡片(重新设计的布局) */}
|
||||
<div className="card" style={{ marginTop: '16px' }}>
|
||||
<div className="card-header">
|
||||
<div className="card-title">版本历史</div>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => onUploadNewVersion(skill.name)}
|
||||
disabled={skill.hasPendingReview}
|
||||
title={skill.hasPendingReview ? '存在审核中的版本,请先撤回后再上传新版本' : ''}
|
||||
onClick={() => onUploadNewVersion(skill)}
|
||||
disabled={skill.status === 'unlisting' || skill.status === 'unlisted' || skill.hasPendingReview}
|
||||
title={skill.status === 'unlisted' ? '已下架的技能不能上传新版本' : (skill.status === 'unlisting' ? '下架审核中的技能不能上传新版本' : (skill.hasPendingReview ? '存在审核中的版本,请先撤回后再上传新版本' : ''))}
|
||||
>
|
||||
<FiUpload /> 上传新版本
|
||||
</button>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="table-wrapper" style={{ margin: 0, padding: 0 }}>
|
||||
<table className="table" style={{ tableLayout: 'fixed' }}>
|
||||
<colgroup>
|
||||
<col style={{ width: '100px' }} />
|
||||
<col />
|
||||
<col style={{ width: '120px' }} />
|
||||
<col style={{ width: '120px' }} />
|
||||
<col style={{ width: '180px' }} />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>版本号</th>
|
||||
<th>版本说明</th>
|
||||
<th>状态</th>
|
||||
<th>更新时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{skill.versions.map((ver, index) => (
|
||||
<tr key={index}>
|
||||
<td>{ver.version}</td>
|
||||
<td>
|
||||
{ver.desc}
|
||||
{ver.status === 'rejected' && ver.rejectionReason && (
|
||||
<div className="dev-rejection-reason">{ver.rejectionReason}</div>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<span className={`status ${versionStatusMap[ver.status]?.className || 'status-stopped'}`}>
|
||||
{versionStatusMap[ver.status]?.text || ver.status}
|
||||
</span>
|
||||
</td>
|
||||
<td>{ver.date}</td>
|
||||
<td>
|
||||
<div className="btn-group">
|
||||
{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 style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
{skill.versions.map((ver, index) => (
|
||||
<div key={index} className="version-history-card">
|
||||
{/* 头部:版本号、状态、日期、操作 */}
|
||||
<div className="version-card-header">
|
||||
<div>
|
||||
<span className="version-number">v{ver.version}</span>
|
||||
<span className={`status ${versionStatusMap[ver.status]?.className || 'status-stopped'}`}>
|
||||
{versionStatusMap[ver.status]?.text || ver.status}
|
||||
</span>
|
||||
<span style={{ color: '#94A3B8', fontSize: '14px' }}>{ver.date}</span>
|
||||
</div>
|
||||
<div className="btn-group">
|
||||
{ver.status === 'reviewing' && (
|
||||
<button className="btn btn-warning btn-sm">
|
||||
<FiRotateCcw /> 撤回审核
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 版本说明 */}
|
||||
<div className="version-card-body">
|
||||
<div>
|
||||
<span style={{ fontWeight: '600', color: '#64748B', fontSize: '13px', display: 'block', marginBottom: '4px' }}>版本说明</span>
|
||||
<span style={{ color: '#1E293B', fontSize: '14px', lineHeight: '1.6' }}>{ver.versionDesc}</span>
|
||||
</div>
|
||||
|
||||
{/* 拒绝理由 */}
|
||||
{ver.status === 'rejected' && ver.rejectionReason && (
|
||||
<div className="dev-rejection-reason">
|
||||
<strong>拒绝理由:</strong>{ver.rejectionReason}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 发布信息预览 */}
|
||||
{ver.publicName && (
|
||||
<div className="version-public-preview">
|
||||
<div className="version-public-preview-icon">{ver.icon}</div>
|
||||
<div className="version-public-preview-content">
|
||||
<div className="version-public-preview-title">
|
||||
<span className="version-public-preview-name">{ver.publicName}</span>
|
||||
</div>
|
||||
<div className="version-public-preview-tags">
|
||||
<span className="dev-detail-tag" style={{ fontSize: '12px', padding: '2px 8px', background: '#EFF6FF', color: '#1E40AF', fontWeight: '600' }}>{ver.category}</span>
|
||||
{ver.tags && ver.tags.map(tag => (
|
||||
<span key={tag} className="dev-detail-tag" style={{ fontSize: '12px', padding: '2px 8px' }}>{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="version-public-preview-desc">{ver.publicDesc}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3. 管理操作卡片(三段式布局第三段) */}
|
||||
{/* 4. 管理操作卡片 */}
|
||||
<div className="card manage-card" style={{ marginTop: '16px' }}>
|
||||
<div className="card-header">
|
||||
<div className="card-title">管理</div>
|
||||
@@ -176,9 +196,9 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion, onUpdateInfo })
|
||||
<div className="card-body">
|
||||
<div className="manage-actions">
|
||||
{skill.status === 'published' && (
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={handleTogglePublish}
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={() => setUnlistSkillModal(true)}
|
||||
disabled={skill.hasPendingReview}
|
||||
title={skill.hasPendingReview ? '存在审核中的版本,请先撤回后再下架' : ''}
|
||||
>
|
||||
@@ -188,8 +208,8 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion, onUpdateInfo })
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={() => setDeleteSkillModal(true)}
|
||||
disabled={skill.status === 'published'}
|
||||
title={skill.status === 'published' ? '已上架的技能需要先下架才能删除' : ''}
|
||||
disabled={skill.status === 'published' || skill.status === 'unlisting' || skill.hasPendingReview}
|
||||
title={skill.status === 'published' ? '已上架的技能需要先下架才能删除' : (skill.status === 'unlisting' ? '下架审核中的技能不能删除' : (skill.hasPendingReview ? '存在审核中的版本,请先撤回后再删除' : ''))}
|
||||
>
|
||||
删除技能
|
||||
</button>
|
||||
@@ -197,6 +217,15 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion, onUpdateInfo })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
visible={unlistSkillModal}
|
||||
title="确认下架"
|
||||
onConfirm={handleUnlistSkill}
|
||||
onCancel={() => setUnlistSkillModal(false)}
|
||||
confirmText="提交下架申请"
|
||||
>
|
||||
确定要申请下架技能"{skill.name}"吗?下架申请需要管理员审核,审核期间技能仍可正常使用。
|
||||
</Modal>
|
||||
<Modal
|
||||
visible={deleteSkillModal}
|
||||
title="确认删除"
|
||||
|
||||
@@ -1,34 +1,12 @@
|
||||
import { FiX, FiChevronLeft } from 'react-icons/fi';
|
||||
import { FiChevronLeft } from 'react-icons/fi';
|
||||
import { useState } from 'react';
|
||||
import { api } from '../../services/api.js';
|
||||
import Toast from '../../components/common/Toast.jsx';
|
||||
|
||||
const ICON_OPTIONS = ['🌤️', '📊', '📝', '🔧', '💻', '📋', '🔍', '📈', '🎯', '⚡', '🌐', '🤖'];
|
||||
|
||||
function UpdateSkillInfoPage({ skill, onBack }) {
|
||||
const categories = api.developer.getCategories();
|
||||
const [name, setName] = useState(skill?.name || '');
|
||||
const [desc, setDesc] = useState(skill?.desc || '');
|
||||
const [category, setCategory] = useState(skill?.category || categories[0]);
|
||||
const [tags, setTags] = useState(skill?.tags || []);
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
const [icon, setIcon] = useState(skill?.icon || ICON_OPTIONS[0]);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
|
||||
const handleTagKeyDown = (e) => {
|
||||
if (e.key === 'Enter' && tagInput.trim()) {
|
||||
e.preventDefault();
|
||||
if (!tags.includes(tagInput.trim()) && tags.length < 5) {
|
||||
setTags([...tags, tagInput.trim()]);
|
||||
}
|
||||
setTagInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const removeTag = (tagToRemove) => {
|
||||
setTags(tags.filter(tag => tag !== tagToRemove));
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
setShowToast(true);
|
||||
setTimeout(() => {
|
||||
@@ -43,76 +21,32 @@ function UpdateSkillInfoPage({ skill, onBack }) {
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<div className="card-title">更新基本信息</div>
|
||||
<div className="card-title">编辑内部信息</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div style={{ marginBottom: '20px', padding: '12px', background: '#EFF6FF', borderRadius: '8px', color: '#1E40AF', fontSize: '14px' }}>
|
||||
<strong>提示:</strong>此处编辑的信息仅供开发者自己管理使用,不会影响技能商店展示内容。如需修改商店展示内容,请上传新版本。
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label required">技能名称</label>
|
||||
<label className="form-label required">技能名称(开发者内部)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="请输入技能名称"
|
||||
placeholder="请输入开发者内部技能名称"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label required">技能描述</label>
|
||||
<label className="form-label required">技能描述(开发者内部)</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
rows="3"
|
||||
placeholder="请输入技能描述"
|
||||
placeholder="请输入开发者内部技能描述"
|
||||
value={desc}
|
||||
onChange={e => setDesc(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">技能分类</label>
|
||||
<select
|
||||
className="form-control"
|
||||
value={category}
|
||||
onChange={e => setCategory(e.target.value)}
|
||||
>
|
||||
{categories.map(cat => (
|
||||
<option key={cat} value={cat}>{cat}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">标签</label>
|
||||
<div className="tag-input-container">
|
||||
{tags.map(tag => (
|
||||
<span key={tag} className="tag-item">
|
||||
{tag}
|
||||
<span className="tag-remove" onClick={() => removeTag(tag)}><FiX /></span>
|
||||
</span>
|
||||
))}
|
||||
<input
|
||||
type="text"
|
||||
className="tag-input"
|
||||
placeholder={tags.length === 0 ? '输入标签后按回车添加' : ''}
|
||||
value={tagInput}
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
onKeyDown={handleTagKeyDown}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#94A3B8', marginTop: '6px' }}>按回车添加标签,最多5个</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">技能图标</label>
|
||||
<div className="dev-icon-picker">
|
||||
{ICON_OPTIONS.map(emoji => (
|
||||
<div
|
||||
key={emoji}
|
||||
className={`dev-icon-option ${icon === emoji ? 'selected' : ''}`}
|
||||
onClick={() => setIcon(emoji)}
|
||||
>
|
||||
{emoji}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#94A3B8', marginTop: '6px' }}>当前选择: {icon}</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
|
||||
<button className="btn" onClick={onBack}>取消</button>
|
||||
<button className="btn btn-primary" onClick={handleSave}>保存修改</button>
|
||||
|
||||
@@ -1,31 +1,9 @@
|
||||
import { FiX } from 'react-icons/fi';
|
||||
import { useState } from 'react';
|
||||
import { api } from '../../services/api.js';
|
||||
import Toast from '../../components/common/Toast.jsx';
|
||||
|
||||
const ICON_OPTIONS = ['🌤️', '📊', '📝', '🔧', '💻', '📋', '🔍', '📈', '🎯', '⚡', '🌐', '🤖'];
|
||||
|
||||
function UploadSkillPage({ onBack }) {
|
||||
const categories = api.developer.getCategories();
|
||||
const [tags, setTags] = useState([]);
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
const [icon, setIcon] = useState(ICON_OPTIONS[0]);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
|
||||
const handleTagKeyDown = (e) => {
|
||||
if (e.key === 'Enter' && tagInput.trim()) {
|
||||
e.preventDefault();
|
||||
if (!tags.includes(tagInput.trim()) && tags.length < 5) {
|
||||
setTags([...tags, tagInput.trim()]);
|
||||
}
|
||||
setTagInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const removeTag = (tagToRemove) => {
|
||||
setTags(tags.filter(tag => tag !== tagToRemove));
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
setShowToast(true);
|
||||
};
|
||||
@@ -36,56 +14,16 @@ function UploadSkillPage({ onBack }) {
|
||||
<div className="card-title">创建技能</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="form-group">
|
||||
<label className="form-label required">技能名称</label>
|
||||
<input type="text" className="form-control" placeholder="请输入技能名称" />
|
||||
<div style={{ marginBottom: '20px', padding: '12px', background: '#EFF6FF', borderRadius: '8px', color: '#1E40AF', fontSize: '14px' }}>
|
||||
<strong>提示:</strong>此处填写的信息仅供开发者自己管理使用,不会在技能商店展示。商店展示信息需要在上传版本时填写。
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label required">技能描述</label>
|
||||
<textarea className="form-control" rows="3" placeholder="请输入技能描述" />
|
||||
<label className="form-label required">技能名称(开发者内部)</label>
|
||||
<input type="text" className="form-control" placeholder="请输入开发者内部技能名称" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">技能分类</label>
|
||||
<select className="form-control">
|
||||
{categories.map(cat => (
|
||||
<option key={cat} value={cat}>{cat}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">标签</label>
|
||||
<div className="tag-input-container">
|
||||
{tags.map(tag => (
|
||||
<span key={tag} className="tag-item">
|
||||
{tag}
|
||||
<span className="tag-remove" onClick={() => removeTag(tag)}><FiX /></span>
|
||||
</span>
|
||||
))}
|
||||
<input
|
||||
type="text"
|
||||
className="tag-input"
|
||||
placeholder={tags.length === 0 ? '输入标签后按回车添加' : ''}
|
||||
value={tagInput}
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
onKeyDown={handleTagKeyDown}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#94A3B8', marginTop: '6px' }}>按回车添加标签,最多5个</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">技能图标</label>
|
||||
<div className="dev-icon-picker">
|
||||
{ICON_OPTIONS.map(emoji => (
|
||||
<div
|
||||
key={emoji}
|
||||
className={`dev-icon-option ${icon === emoji ? 'selected' : ''}`}
|
||||
onClick={() => setIcon(emoji)}
|
||||
>
|
||||
{emoji}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#94A3B8', marginTop: '6px' }}>当前选择: {icon}</div>
|
||||
<label className="form-label required">技能描述(开发者内部)</label>
|
||||
<textarea className="form-control" rows="3" placeholder="请输入开发者内部技能描述" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
|
||||
<button className="btn" onClick={onBack}>取消</button>
|
||||
|
||||
@@ -1,10 +1,40 @@
|
||||
import { FiUpload, FiChevronLeft } from 'react-icons/fi';
|
||||
import { FiUpload, FiChevronLeft, FiX } from 'react-icons/fi';
|
||||
import { useState } from 'react';
|
||||
import { api } from '../../services/api.js';
|
||||
import Toast from '../../components/common/Toast.jsx';
|
||||
|
||||
function UploadVersionPage({ skillName, onBack }) {
|
||||
const ICON_OPTIONS = ['🌤️', '📊', '📝', '🔧', '💻', '📋', '🔍', '📈', '🎯', '⚡', '🌐', '🤖'];
|
||||
|
||||
function UploadVersionPage({ skill, onBack }) {
|
||||
const categories = api.developer.getCategories();
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
|
||||
// 获取当前生效版本,用于默认继承
|
||||
const currentApprovedVersion = skill?.versions?.find(v => v.status === 'approved');
|
||||
const isFirstVersion = !currentApprovedVersion;
|
||||
|
||||
// 表单状态
|
||||
const [publicName, setPublicName] = useState(isFirstVersion ? '' : (currentApprovedVersion?.publicName || ''));
|
||||
const [publicDesc, setPublicDesc] = useState(isFirstVersion ? '' : (currentApprovedVersion?.publicDesc || ''));
|
||||
const [category, setCategory] = useState(isFirstVersion ? categories[0] : (currentApprovedVersion?.category || categories[0]));
|
||||
const [tags, setTags] = useState(isFirstVersion ? [] : (currentApprovedVersion?.tags || []));
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
const [icon, setIcon] = useState(isFirstVersion ? ICON_OPTIONS[0] : (currentApprovedVersion?.icon || ICON_OPTIONS[0]));
|
||||
|
||||
const handleTagKeyDown = (e) => {
|
||||
if (e.key === 'Enter' && tagInput.trim()) {
|
||||
e.preventDefault();
|
||||
if (!tags.includes(tagInput.trim()) && tags.length < 5) {
|
||||
setTags([...tags, tagInput.trim()]);
|
||||
}
|
||||
setTagInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const removeTag = (tagToRemove) => {
|
||||
setTags(tags.filter(tag => tag !== tagToRemove));
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
setShowToast(true);
|
||||
setTimeout(() => {
|
||||
@@ -23,28 +53,114 @@ function UploadVersionPage({ skillName, onBack }) {
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div style={{ marginBottom: '16px', color: '#64748B' }}>
|
||||
技能: {skillName}
|
||||
技能: {skill?.name}
|
||||
</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 style={{ marginBottom: '24px', paddingBottom: '20px', borderBottom: '1px solid #E2E8F0' }}>
|
||||
<h4 style={{ margin: '0 0 12px 0', fontSize: '15px', color: '#1E293B' }}>版本说明(供审核参考)</h4>
|
||||
<div className="form-group">
|
||||
<label className="form-label required">版本说明</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
rows="3"
|
||||
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' }}>
|
||||
版本号将由系统自动生成
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#94A3B8', marginBottom: '16px' }}>
|
||||
版本号将由系统自动生成
|
||||
|
||||
{/* 发布信息区域 */}
|
||||
<div>
|
||||
<h4 style={{ margin: '0 0 12px 0', fontSize: '15px', color: '#1E293B' }}>发布信息(技能商店展示)</h4>
|
||||
<div style={{ marginBottom: '16px', padding: '12px', background: '#F0FDF4', borderRadius: '8px', color: '#166534', fontSize: '14px' }}>
|
||||
<strong>提示:</strong>此处填写的信息将在版本审核通过后显示在技能商店。如需修改商店展示内容,必须发布新版本。
|
||||
{!isFirstVersion && (
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
已自动继承当前生效版本的信息,您可以按需修改。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label required">技能发布名称</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="请输入技能发布名称"
|
||||
value={publicName}
|
||||
onChange={e => setPublicName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label required">技能发布描述</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
rows="3"
|
||||
placeholder="请输入技能发布描述"
|
||||
value={publicDesc}
|
||||
onChange={e => setPublicDesc(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label required">技能分类</label>
|
||||
<select
|
||||
className="form-control"
|
||||
value={category}
|
||||
onChange={e => setCategory(e.target.value)}
|
||||
>
|
||||
{categories.map(cat => (
|
||||
<option key={cat} value={cat}>{cat}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">标签</label>
|
||||
<div className="tag-input-container">
|
||||
{tags.map(tag => (
|
||||
<span key={tag} className="tag-item">
|
||||
{tag}
|
||||
<span className="tag-remove" onClick={() => removeTag(tag)}><FiX /></span>
|
||||
</span>
|
||||
))}
|
||||
<input
|
||||
type="text"
|
||||
className="tag-input"
|
||||
placeholder={tags.length === 0 ? '输入标签后按回车添加' : ''}
|
||||
value={tagInput}
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
onKeyDown={handleTagKeyDown}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#94A3B8', marginTop: '6px' }}>按回车添加标签,最多5个</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label required">技能图标</label>
|
||||
<div className="dev-icon-picker">
|
||||
{ICON_OPTIONS.map(emoji => (
|
||||
<div
|
||||
key={emoji}
|
||||
className={`dev-icon-option ${icon === emoji ? 'selected' : ''}`}
|
||||
onClick={() => setIcon(emoji)}
|
||||
>
|
||||
{emoji}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#94A3B8', marginTop: '6px' }}>当前选择: {icon}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px' }}>
|
||||
<button className="btn" onClick={onBack}>取消</button>
|
||||
<button className="btn btn-primary" onClick={handleSubmit}>提交审核</button>
|
||||
</div>
|
||||
|
||||
@@ -35,14 +35,42 @@ export const skillsApi = {
|
||||
* 获取所有技能列表
|
||||
* @returns {Array} 技能列表
|
||||
*/
|
||||
list: () => skills,
|
||||
list: () => skills.map(skill => {
|
||||
if (skill.currentVersion) {
|
||||
return {
|
||||
...skill,
|
||||
name: skill.currentVersion.publicName,
|
||||
desc: skill.currentVersion.publicDesc,
|
||||
category: skill.currentVersion.category,
|
||||
tags: skill.currentVersion.tags,
|
||||
icon: skill.currentVersion.icon,
|
||||
rating: 4.8
|
||||
};
|
||||
}
|
||||
return skill;
|
||||
}),
|
||||
|
||||
/**
|
||||
* 根据 ID 获取技能详情
|
||||
* @param {number} id - 技能 ID
|
||||
* @returns {Object|undefined} 技能对象
|
||||
*/
|
||||
getById: (id) => skills.find(skill => skill.id === id),
|
||||
getById: (id) => {
|
||||
const skill = skills.find(skill => skill.id === id);
|
||||
if (!skill) return undefined;
|
||||
if (skill.currentVersion) {
|
||||
return {
|
||||
...skill,
|
||||
name: skill.currentVersion.publicName,
|
||||
desc: skill.currentVersion.publicDesc,
|
||||
category: skill.currentVersion.category,
|
||||
tags: skill.currentVersion.tags,
|
||||
icon: skill.currentVersion.icon,
|
||||
rating: 4.8
|
||||
};
|
||||
}
|
||||
return skill;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取技能文件列表
|
||||
|
||||
@@ -363,6 +363,18 @@
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: var(--color-warning);
|
||||
border-color: var(--color-warning);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: #D97706;
|
||||
border-color: #D97706;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--color-danger);
|
||||
border-color: var(--color-danger);
|
||||
@@ -2160,6 +2172,193 @@ input:checked + .slider:before {
|
||||
.dev-info-label { width: 100px; flex-shrink: 0; color: #64748B; font-size: 14px; font-weight: 500; }
|
||||
.dev-info-value { flex: 1; color: #1E293B; font-size: 14px; }
|
||||
|
||||
/* 技能概览卡片样式 */
|
||||
.skill-overview-card {
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.skill-overview-card .skill-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #FFFFFF;
|
||||
font-size: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.skill-overview-card .skill-header {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.skill-overview-card .skill-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.skill-overview-card .skill-name {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: #1E293B;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.skill-overview-card .skill-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.skill-overview-card .skill-desc-row {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.skill-overview-card .skill-desc-text {
|
||||
margin: 0;
|
||||
color: #64748B;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 技能分类标签 */
|
||||
.skill-category-tag {
|
||||
padding: 4px 10px;
|
||||
background: #EFF6FF;
|
||||
color: #1E40AF;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 版本历史卡片样式 */
|
||||
.version-history-card {
|
||||
background: #F8FAFC;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.version-history-card:hover {
|
||||
background: #FFFFFF;
|
||||
border-color: #CBD5E1;
|
||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.version-card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.version-card-header > div:first-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.version-number {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: #1E293B;
|
||||
background: #FFFFFF;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #E2E8F0;
|
||||
}
|
||||
|
||||
.version-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 发布信息预览区域 */
|
||||
.version-public-preview {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: flex-start;
|
||||
padding: 14px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #E2E8F0;
|
||||
}
|
||||
|
||||
.version-public-preview-icon {
|
||||
font-size: 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.version-public-preview-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.version-public-preview-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.version-public-preview-name {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #1E293B;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.version-public-preview-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.version-public-preview-desc {
|
||||
font-size: 13px;
|
||||
color: #64748B;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 拒绝理由样式 */
|
||||
.dev-rejection-reason {
|
||||
padding: 12px 14px;
|
||||
background: #FEF2F2;
|
||||
border: 1px solid #FECACA;
|
||||
border-radius: 8px;
|
||||
color: #991B1B;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 管理卡片样式 */
|
||||
.manage-card {
|
||||
background: #F8FAFC;
|
||||
}
|
||||
|
||||
.manage-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* ===== Home page inline styles ===== */
|
||||
.home-layout {
|
||||
min-height: 100vh;
|
||||
|
||||
Reference in New Issue
Block a user