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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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