refactor: 完成技能数据结构重构 - 分离内部信息与发布信息

- 新增技能内部信息与商店发布信息分离的数据结构
- 重构技能详情页为四段式布局(概览+当前生效版本+版本历史+管理)
- 移除历史版本中的下载按钮
- 版本历史改为卡片布局,新增发布信息预览
- 分类与标签合并显示,分类作为第一个标签
- 更新按钮禁用逻辑:下架审核中/已下架状态禁用上传新版本
- 下架技能按钮添加二次确认弹窗
- 补充10个不同状态的技能示例数据
- 同步 delta specs 到主 specs
- 归档变更:refactor-skill-data-structure
This commit is contained in:
2026-03-21 18:09:43 +08:00
parent 8179ff2f95
commit 017a8af2a3
21 changed files with 1452 additions and 646 deletions

View File

@@ -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: '审核通过' }
]
};

View File

@@ -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: '👥'
}
];
// 技能图标映射

View File

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

View File

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

View File

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

View File

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

View File

@@ -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="确认删除"

View File

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

View File

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

View File

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

View File

@@ -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;
},
/**
* 获取技能文件列表

View File

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