feat: 完善开发台功能 - 新增总览页、技能筛选分页、版本管理操作、基本信息独立编辑

- 新增 DevOverviewPage 开发者总览页(指标卡片、待审核项目、最近动态)
- 新增 UpdateSkillInfoPage 基本信息编辑页(与版本上传分离)
- MySkillsPage 增加关键词/分类/状态筛选和分页
- SkillEditorPage 基本信息只读、增加上架/下架/删除操作、版本拒绝原因展示
- NewVersionPage 简化为仅版本说明和技能包上传
- UploadSkillPage 增加动态分类、图标选择器、移除模型兼容性
- 数据层新增 icon/rejectionReason 字段和 developerOverview 总览数据
- DeveloperPage 侧边栏新增总览导航入口
- 同步更新 openspec 规格文档和 README
This commit is contained in:
2026-03-20 15:07:12 +08:00
parent 0473a68dc2
commit 9c487f3ed6
17 changed files with 869 additions and 137 deletions

View File

@@ -89,12 +89,14 @@ grandclaw-archtype/
│ │ │ ├── AddProjectPage.jsx # 新增/编辑项目
│ │ │ └── AdminLogsPage.jsx # 全局日志查询
│ │ └── developer/ # 开发台子页面
│ │ ├── MySkillsPage.jsx # 我的技能
│ │ ├── DevOverviewPage.jsx # 开发者总览
│ │ ├── MySkillsPage.jsx # 我的技能(筛选+分页)
│ │ ├── SkillEditorPage.jsx # 技能详情(只读+操作)
│ │ ├── UploadSkillPage.jsx # 创建技能
│ │ ├── NewVersionPage.jsx # 上传新版本
│ │ ├── UpdateSkillInfoPage.jsx # 更新基本信息
│ │ ├── NewVersionPage.jsx # 上传新版本(仅版本信息)
│ │ ├── DevDocsPage.jsx # 开发文档
│ │ ── DevAccountPage.jsx # 开发者设置
│ │ └── SkillEditorPage.jsx # 技能详情/编辑
│ │ ── DevAccountPage.jsx # 开发者设置
│ └── styles/ # SCSS样式模块
│ ├── _variables.scss # 设计系统变量
│ ├── _mixins.scss # 可复用混入
@@ -165,10 +167,12 @@ pnpm build
- **日志查询**:全局系统日志查询,支持多维度筛选(关键词、用户、部门、类型、状态、时间范围)
### 5. 开发台Developer
- **我的技能**开发的技能列表
- **创建技能**上传新技能
- **上传新版本**:为已发布的技能上传新版本
- **技能编辑**:编辑技能配置、版本管理
- **总览**:开发者指标卡片(我的技能总数、已发布、草稿、待审核)、待审核项目列表、最近动态
- **我的技能**:技能列表,支持关键词搜索、分类筛选、状态筛选、分页,支持上架/下架、删除操作
- **技能详情**:基本信息只读展示、版本历史管理(启用/下载/删除)、审核拒绝原因展示
- **创建技能**:基本信息表单 + 技能图标选择 + 技能包上传
- **更新基本信息**:独立页面编辑技能名称/描述/分类/标签/图标,与版本上传分离
- **上传新版本**:仅包含版本说明和技能包上传,不含基本信息编辑
- **开发文档**:技能开发相关文档
- **开发者设置**:开发者账号信息
@@ -431,7 +435,7 @@ localStorage.setItem('console_currentScene', 'welcome');
localStorage.setItem('admin_currentPage', 'overview');
// 开发台
localStorage.setItem('developer_currentPage', 'mySkills');
localStorage.setItem('developer_currentPage', 'overview');
localStorage.setItem('developer_currentSkillId', '1');
```
@@ -535,7 +539,7 @@ const members = api.members.list();
- `api.skills` - 技能市场(列表、详情、文件、版本、图标)
- `api.conversations` - 聊天场景和对话历史
- `api.logs` - 操作日志(列表、筛选)
- `api.developer` - 开发台数据(技能、分类、模型、文档)
- `api.developer` - 开发台数据(总览、技能、分类、文档)
- `api.members` - 项目成员
- `api.tasks` - 定时任务
- `api.admin` - 管理台(总览、部门、用户、项目、全局日志)
@@ -547,7 +551,7 @@ const members = api.members.list();
### 数据文件说明
- `conversations.js`:聊天场景和对话历史
- `skills.js`:技能市场数据,包含技能详情、文件列表、版本历史
- `developerData.js`:开发台数据,包含我的技能、技能分类、开发文档
- `developerData.js`:开发台数据,包含我的技能(含图标、版本审核状态)、技能分类、开发者总览、开发文档
- `logs.js`:操作日志数据(成功/失败/警告状态)
- `tasks.js`:定时任务数据(包含任务配置和执行日志)
- `adminData.js`:管理台数据(部门列表、用户列表、项目列表、总览指标、全局日志、可选项数据)

View File

@@ -12,3 +12,4 @@ context: |
- 不做安全防御性编程eval/dangerouslySetInnerHTML等按需使用
- README.md是项目的开发文档记录代码结构和关键开发模式优先读取获取上下文
- 涉及页面/路由/组件/功能模块变更或技术栈调整时同步更新README.md
- Git提交: 仅中文; 格式为"类型: 简短描述",类型可选: feat(新功能)/fix(修复)/refactor(重构)/docs(文档)/style(格式)/test(测试)/chore(构建/工具); 多行描述空行后加详细说明

View File

@@ -84,3 +84,77 @@
#### Scenario: 取消删除
- **WHEN** 用户在确认弹框中点击取消按钮
- **THEN** 弹框关闭,列表不变
### Requirement: 技能列表搜索筛选
我的技能列表 SHALL 支持按关键词、分类和状态筛选技能数据。
#### Scenario: 关键词搜索
- **WHEN** 用户在筛选卡片的关键词输入框中输入文本并点击查询
- **THEN** 列表仅显示技能名称或描述中包含该关键词的记录
#### Scenario: 分类筛选
- **WHEN** 用户在筛选卡片的分类下拉框选择某个分类并点击查询
- **THEN** 列表仅显示该分类的技能记录
#### Scenario: 状态筛选
- **WHEN** 用户在筛选卡片的状态下拉框选择某个状态(已发布/草稿)并点击查询
- **THEN** 列表仅显示该状态的技能记录
#### Scenario: 筛选重置
- **WHEN** 用户在筛选卡片点击重置按钮
- **THEN** 筛选条件清空,列表恢复显示全部技能
### Requirement: 技能列表分页
我的技能列表 SHALL 在表格底部展示分页组件。
#### Scenario: 分页展示
- **WHEN** 用户打开我的技能列表页
- **THEN** 表格底部右侧显示分页组件,包含页码按钮和前后翻页按钮
### Requirement: 技能上架下架
我的技能列表和技能详情页 SHALL 提供技能的上架/下架操作入口。
#### Scenario: 列表页下架操作
- **WHEN** 用户在已发布技能的操作列点击"下架"按钮
- **THEN** 页面展示成功提示"已下架"
#### Scenario: 详情页上架/下架切换
- **WHEN** 用户在技能详情页点击"下架技能"或"上架技能"按钮
- **THEN** 页面展示对应的成功提示
### Requirement: 技能删除确认
我的技能列表和技能详情页 SHALL 提供技能删除操作,需弹框确认。
#### Scenario: 列表页删除确认
- **WHEN** 用户在技能列表的操作列点击"删除"按钮
- **THEN** 弹出确认弹框,显示"确定要删除技能"{技能名称}"吗?此操作不可撤销。"
#### Scenario: 确认删除技能
- **WHEN** 用户在确认弹框中点击"删除"按钮
- **THEN** 弹框关闭,页面展示成功提示"已删除"
#### Scenario: 取消删除技能
- **WHEN** 用户在确认弹框中点击"取消"按钮
- **THEN** 弹框关闭,列表不变
#### Scenario: 详情页删除确认
- **WHEN** 用户在技能详情页点击"删除技能"按钮
- **THEN** 弹出确认弹框,显示"确定要删除技能"{技能名称}"吗?此操作不可撤销。"
### Requirement: 版本删除确认
技能详情页版本历史表格 SHALL 为未启用版本提供删除操作,需弹框确认。
#### Scenario: 版本删除按钮展示
- **WHEN** 用户在技能详情页版本历史表格中查看某个未启用的版本
- **THEN** 该版本操作列显示"删除"按钮
#### Scenario: 确认删除版本
- **WHEN** 用户点击版本的"删除"按钮并在确认弹框中点击"删除"
- **THEN** 弹框关闭,页面展示成功提示"已删除"
### Requirement: 版本审核拒绝原因展示
技能详情页版本历史表格 SHALL 为被拒绝的版本展示拒绝原因。
#### Scenario: 拒绝原因展示
- **WHEN** 用户在版本历史表格中查看状态为"审核拒绝"的版本
- **THEN** 该版本状态标签下方显示红色小字的拒绝原因信息

View File

@@ -0,0 +1,38 @@
## ADDED Requirements
### Requirement: 基本信息编辑表单
UpdateSkillInfoPage SHALL 提供技能基本信息的编辑表单,预填当前数据。
#### Scenario: 表单预填展示
- **WHEN** 用户从技能详情页点击"更新基本信息"进入 UpdateSkillInfoPage
- **THEN** 表单字段预填当前技能的名称、描述、分类、标签和图标数据
#### Scenario: 分类动态生成
- **WHEN** 用户在基本信息编辑表单中打开分类下拉框
- **THEN** 下拉选项从数据源动态生成,包含所有可用分类(信息查询、效率工具、开发工具、数据分析、文档处理、业务系统)
#### Scenario: 提交基本信息修改
- **WHEN** 用户填写完基本信息后点击"保存修改"按钮
- **THEN** 页面展示成功提示"保存成功",并返回技能详情页
#### Scenario: 取消编辑
- **WHEN** 用户在基本信息编辑页面点击"取消"按钮
- **THEN** 返回技能详情页,不保存任何修改
### Requirement: 技能图标选择
UpdateSkillInfoPage 和 UploadSkillPage SHALL 提供技能图标的 emoji 选择器。
#### Scenario: 图标选择展示
- **WHEN** 用户在技能创建或编辑页面看到图标选择区域
- **THEN** 页面展示 emoji 网格(🌤️📊📝🔧💻📋🔍📈🎯⚡🌐🤖),当前选中项高亮显示
#### Scenario: 切换图标
- **WHEN** 用户点击 emoji 网格中的某个图标
- **THEN** 该图标高亮选中,之前的选中项取消高亮
### Requirement: 技能图标显示
技能详情页 SHALL 在头部区域展示技能图标。
#### Scenario: 图标展示
- **WHEN** 用户打开技能详情页
- **THEN** 技能头部区域的图标位置显示该技能选择的 emoji 图标

View File

@@ -0,0 +1,26 @@
## ADDED Requirements
### Requirement: 开发者指标展示
开发台总览页 SHALL 展示开发者维度的核心指标数据,以卡片形式呈现。
#### Scenario: 指标卡片展示
- **WHEN** 用户打开开发台总览页
- **THEN** 页面顶部显示4个指标卡片我的技能总数、已发布数量、草稿数量、待审核版本数量每个卡片包含数值
### Requirement: 待审核项目提醒
开发台总览页 SHALL 展示待审核的版本项目列表。
#### Scenario: 待审核列表展示
- **WHEN** 用户打开开发台总览页
- **THEN** 页面左侧区域显示待审核项目列表,每条包含技能名称、版本号、审核状态标签和日期,点击可跳转到对应技能详情页
#### Scenario: 审核拒绝项展示
- **WHEN** 待审核列表中包含被拒绝的版本
- **THEN** 该项显示拒绝状态标签和"查看原因"链接
### Requirement: 最近动态展示
开发台总览页 SHALL 展示开发者最近的操作动态记录。
#### Scenario: 动态列表展示
- **WHEN** 用户打开开发台总览页
- **THEN** 页面右侧区域显示最近操作动态列表,每条包含时间、操作描述和状态标签

View File

@@ -37,3 +37,44 @@
#### Scenario: 操作失败提示
- **WHEN** 用户执行操作失败
- **THEN** 页面顶部展示红色错误提示"操作失败,请重试"
### Requirement: 技能删除确认弹窗
系统 SHALL 提供 Modal 组件用于技能删除操作的确认。
#### Scenario: 技能列表删除确认
- **WHEN** 用户点击技能列表中某个技能的"删除"按钮
- **THEN** 页面展示确认弹窗,标题为"确认删除",内容为"确定要删除技能"{技能名称}"吗?此操作不可撤销。"
#### Scenario: 技能详情页删除确认
- **WHEN** 用户点击技能详情页的"删除技能"按钮
- **THEN** 页面展示确认弹窗,标题为"确认删除",内容为"确定要删除技能"{技能名称}"吗?此操作不可撤销。"
### Requirement: 版本删除确认弹窗
系统 SHALL 提供 Modal 组件用于版本删除操作的确认。
#### Scenario: 版本删除确认
- **WHEN** 用户点击版本历史表格中某个未启用版本的"删除"按钮
- **THEN** 页面展示确认弹窗,标题为"确认删除",内容为"确定要删除此版本吗?此操作不可撤销。"
### Requirement: 开发台操作结果消息提示
系统 SHALL 提供 Toast 组件用于开发台操作的结果提示。
#### Scenario: 上架/下架成功提示
- **WHEN** 用户执行上架或下架操作
- **THEN** 页面顶部展示绿色成功提示("已上架"或"已下架"
#### Scenario: 删除成功提示
- **WHEN** 用户确认删除技能或版本
- **THEN** 页面顶部展示绿色成功提示"已删除"
#### Scenario: 保存成功提示
- **WHEN** 用户在更新基本信息页面点击"保存修改"按钮
- **THEN** 页面顶部展示绿色成功提示"保存成功"
#### Scenario: 提交审核成功提示
- **WHEN** 用户在上传新版本页面点击"提交审核"按钮
- **THEN** 页面顶部展示绿色成功提示"已提交审核"
#### Scenario: 创建技能成功提示
- **WHEN** 用户在创建技能页面点击"创建技能"按钮
- **THEN** 页面顶部展示绿色成功提示"创建成功"

View File

@@ -34,6 +34,7 @@ export const ADMIN_PAGES = {
* 开发台页面配置
*/
export const DEVELOPER_PAGES = {
overview: { title: '总览', icon: 'FiHome' },
mySkills: { title: '我的技能', icon: 'FaPuzzlePiece' },
uploadSkill: { title: '创建技能', icon: 'FiPlus' },
newVersion: { title: '上传新版本', icon: null },

View File

@@ -3,18 +3,18 @@ export const mySkills = [
id: 1,
name: '天气查询助手',
desc: '根据城市名称查询当前天气和未来预报,支持全国主要城市',
icon: '🌤️',
status: 'published',
version: '1.2.0',
category: '信息查询',
tags: ['天气', '查询', '生活'],
modelSupport: ['Doubao-pro', 'GPT-4', 'Claude-3'],
lastModified: '2026-03-18',
installs: 156,
rating: 4.7,
versions: [
{ version: '1.2.0', date: '2026-03-18', desc: '新增支持未来7天预报', current: true, status: 'approved', enabled: true },
{ version: '1.1.0', date: '2026-03-10', desc: '优化响应速度', current: false, status: 'approved', enabled: false },
{ version: '1.0.0', date: '2026-03-01', desc: '初始版本', current: false, status: 'approved', enabled: false }
{ 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',
@@ -26,16 +26,16 @@ export const mySkills = [
id: 2,
name: '待办事项管理',
desc: '帮助用户管理日常待办事项,支持添加、完成、删除操作',
icon: '📋',
status: 'draft',
version: '0.1.0',
category: '效率工具',
tags: ['待办', '管理', '效率'],
modelSupport: ['Doubao-pro', 'Claude-3'],
lastModified: '2026-03-17',
installs: 0,
rating: 0,
versions: [
{ version: '0.1.0', date: '2026-03-17', desc: '开发中版本', current: true, status: 'pending', enabled: false }
{ version: '0.1.0', date: '2026-03-17', desc: '开发中版本', current: true, status: 'pending', enabled: false, rejectionReason: '' }
],
package: {
name: 'todo-manager-v0.1.0.zip',
@@ -47,19 +47,19 @@ export const mySkills = [
id: 3,
name: '代码审查助手',
desc: '自动审查代码质量,提供优化建议和潜在问题检测',
icon: '💻',
status: 'published',
version: '2.0.1',
category: '开发工具',
tags: ['代码', '审查', '开发'],
modelSupport: ['Claude-3', 'GPT-4'],
lastModified: '2026-03-15',
installs: 342,
rating: 4.9,
versions: [
{ version: '2.0.1', date: '2026-03-15', desc: '修复 Python 代码审查问题', current: true, status: 'approved', enabled: true },
{ version: '2.0.0', date: '2026-03-10', desc: '修复安全问题', current: false, status: 'rejected', enabled: false },
{ version: '2.0.0', date: '2026-03-08', desc: '支持多语言审查', current: false, status: 'approved', enabled: false },
{ version: '1.0.0', date: '2026-02-20', desc: '初始版本', current: false, status: 'approved', enabled: false }
{ 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',
@@ -71,15 +71,6 @@ export const mySkills = [
export const skillCategories = ['信息查询', '效率工具', '开发工具', '数据分析', '文档处理', '业务系统'];
export const supportedModels = [
{ id: 'doubao-pro', name: 'Doubao-pro', provider: '字节跳动' },
{ id: 'doubao-lite', name: 'Doubao-lite', provider: '字节跳动' },
{ id: 'gpt-4', name: 'GPT-4', provider: 'OpenAI' },
{ id: 'gpt-3.5', name: 'GPT-3.5 Turbo', provider: 'OpenAI' },
{ id: 'claude-3', name: 'Claude-3 Opus', provider: 'Anthropic' },
{ id: 'claude-3-haiku', name: 'Claude-3 Haiku', provider: 'Anthropic' }
];
export const devDocs = [
{ id: 1, title: '快速开始', category: '入门指南', content: '介绍如何开发并上传第一个技能...' },
{ id: 2, title: '技能包规范', category: '入门指南', content: '技能包的目录结构和必要文件说明...' },
@@ -89,4 +80,23 @@ export const devDocs = [
{ id: 6, title: '工具调用规范', category: 'API参考', content: '定义和使用工具函数的规范...' },
{ id: 7, title: '版本管理指南', category: '发布管理', content: '版本号规则和升级策略...' },
{ id: 8, title: '发布审核流程', category: '发布管理', content: '技能发布后的审核和上线流程...' }
];
];
export const developerOverview = {
totalSkills: 3,
publishedCount: 2,
draftCount: 1,
pendingReview: 1,
totalInstalls: 498,
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: '安全审查未通过,存在潜在的代码注入风险,请修复后重新提交' }
],
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: '审核通过' }
]
};

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { FiPlus, FiTerminal } from 'react-icons/fi';
import { FiPlus, FiTerminal, FiHome } from 'react-icons/fi';
import { FaPuzzlePiece } from 'react-icons/fa';
import Layout from '../components/Layout.jsx';
import SidebarBrand from '../components/layout/SidebarBrand.jsx';
@@ -10,12 +10,14 @@ import usePageState from '../hooks/usePageState.js';
import { DEVELOPER_PAGES } from '../constants/pages.js';
import { DEVELOPER_KEYS } from '../constants/storageKeys.js';
import api from '../services/api.js';
import DevOverviewPage from './developer/DevOverviewPage.jsx';
import MySkillsPage from './developer/MySkillsPage.jsx';
import UploadSkillPage from './developer/UploadSkillPage.jsx';
import NewVersionPage from './developer/NewVersionPage.jsx';
import DevDocsPage from './developer/DevDocsPage.jsx';
import DevAccountPage from './developer/DevAccountPage.jsx';
import SkillEditorPage from './developer/SkillEditorPage.jsx';
import UpdateSkillInfoPage from './developer/UpdateSkillInfoPage.jsx';
function DeveloperPage() {
const location = useLocation();
@@ -24,7 +26,7 @@ function DeveloperPage() {
// 使用 usePageState 管理页面状态
const { currentPage, setCurrentPage } = usePageState({
storageKey: DEVELOPER_KEYS.CURRENT_PAGE,
defaultPage: 'mySkills',
defaultPage: 'overview',
pageTitles: DEVELOPER_PAGES,
});
@@ -37,7 +39,7 @@ function DeveloperPage() {
useEffect(() => {
if (location.state?.fromHome) {
setCurrentPage('mySkills');
setCurrentPage('overview');
setCurrentSkillId(null);
navigate('.', { replace: true, state: {} });
}
@@ -68,30 +70,47 @@ function DeveloperPage() {
setCurrentPage('newVersion');
};
const openUpdateInfoPage = (skillId) => {
setCurrentSkillId(skillId);
setCurrentPage('updateInfo');
};
const handleBack = () => {
setCurrentPage('mySkills');
setCurrentSkillId(null);
};
const handleNewVersionBack = () => {
const handleEditorBack = () => {
setCurrentPage('skillEditor');
setNewVersionSkillName('');
};
const renderPage = () => {
switch (currentPage) {
case 'overview':
return <DevOverviewPage onSkillClick={openSkillEditor} />;
case 'mySkills':
return <MySkillsPage onSkillClick={openSkillEditor} />;
case 'uploadSkill':
return <UploadSkillPage />;
return <UploadSkillPage onBack={() => switchPage('mySkills')} />;
case 'devDocs':
return <DevDocsPage />;
case 'devAccount':
return <DevAccountPage />;
case 'skillEditor':
return <SkillEditorPage skillId={currentSkillId} onBack={handleBack} onUploadNewVersion={openNewVersionPage} />;
return <SkillEditorPage
skillId={currentSkillId}
onBack={handleBack}
onUploadNewVersion={openNewVersionPage}
onUpdateInfo={openUpdateInfoPage}
/>;
case 'newVersion':
return <NewVersionPage skillName={newVersionSkillName} onBack={handleNewVersionBack} />;
return <NewVersionPage skillName={newVersionSkillName} onBack={handleEditorBack} />;
case 'updateInfo':
return <UpdateSkillInfoPage
skill={api.developer.getSkillById(currentSkillId)}
onBack={handleEditorBack}
/>;
default:
return <div>Page not found</div>;
}
@@ -125,6 +144,12 @@ function DeveloperPage() {
))}
</div>
<div className="chat-sidebar-nav">
<SidebarNavItem
icon={<FiHome />}
label="总览"
active={currentPage === 'overview'}
onClick={() => switchPage('overview')}
/>
<SidebarNavItem
icon={<FaPuzzlePiece />}
label="我的技能"
@@ -153,4 +178,4 @@ function DeveloperPage() {
);
}
export default DeveloperPage;
export default DeveloperPage;

View File

@@ -0,0 +1,91 @@
import { FiAlertTriangle, FiInfo } from 'react-icons/fi';
import { api } from '../../services/api.js';
function DevOverviewPage({ onSkillClick }) {
const data = api.developer.getOverview();
return (
<>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-title">我的技能</div>
<div className="stat-value">{data.totalSkills}</div>
</div>
<div className="stat-card">
<div className="stat-title">已发布</div>
<div className="stat-value">{data.publishedCount}</div>
</div>
<div className="stat-card">
<div className="stat-title">草稿</div>
<div className="stat-value">{data.draftCount}</div>
</div>
<div className="stat-card">
<div className="stat-title">待审核</div>
<div className="stat-value">{data.pendingReview}</div>
</div>
</div>
<div className="overview-bottom-row">
<div className="card overview-anomalies">
<div className="card-header">
<div className="card-title">待审核项目</div>
</div>
<div className="card-body">
{data.pendingItems.map((item, index) => (
<div
key={index}
className={`anomaly-item ${item.status === 'rejected' ? 'anomaly-warning' : 'anomaly-info'}`}
style={{ cursor: 'pointer' }}
onClick={() => onSkillClick && onSkillClick(item.skillId)}
>
<span className="anomaly-icon">
{item.status === 'rejected' ? <FiAlertTriangle /> : <FiInfo />}
</span>
<span className="anomaly-text">
{item.skillName} {item.version}
<span style={{ fontSize: '12px', marginLeft: '8px', opacity: 0.7 }}>{item.date}</span>
</span>
</div>
))}
</div>
</div>
<div className="card overview-recent-logs">
<div className="card-header">
<div className="card-title">最近动态</div>
</div>
<div className="card-body">
<table className="table">
<thead>
<tr>
<th>时间</th>
<th>操作</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{data.recentActivity.map((item, index) => (
<tr key={index}>
<td>{item.time}</td>
<td>{item.action}</td>
<td>
<span className={`status ${
item.status === '审核通过' ? 'status-running' :
item.status === '审核拒绝' ? 'status-error' :
'status-warning'
}`}>
{item.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</>
);
}
export default DevOverviewPage;

View File

@@ -1,50 +1,175 @@
import { mySkills } from '../../data/developerData.js';
import { useState } from 'react';
import { api } from '../../services/api.js';
import Modal from '../../components/common/Modal.jsx';
import Toast from '../../components/common/Toast.jsx';
function MySkillsPage({ onSkillClick }) {
const sourceData = api.developer.getMySkills();
const categories = api.developer.getCategories();
const [filters, setFilters] = useState({ keyword: '', category: '', status: '' });
const [deleteTarget, setDeleteTarget] = useState(null);
const [toast, setToast] = useState({ visible: false, type: 'success', message: '' });
const handleFilterChange = (key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
};
const handleReset = () => {
setFilters({ keyword: '', category: '', 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 === 'published' && skill.status !== 'published') return false;
if (filters.status === 'draft' && skill.status !== 'draft') return false;
return true;
});
const handleUnpublish = (e, skill) => {
e.stopPropagation();
setToast({ visible: true, type: 'success', message: '已下架' });
};
const handleDelete = (e, skill) => {
e.stopPropagation();
setDeleteTarget(skill);
};
const confirmDelete = () => {
setDeleteTarget(null);
setToast({ visible: true, type: 'success', message: '已删除' });
};
return (
<div className="card">
<div className="card-header">
<div className="card-title">我的技能</div>
<>
<div className="card">
<div className="card-body">
<div className="search-bar">
<div className="search-item" style={{ flex: 1, minWidth: '200px' }}>
<label>关键词</label>
<input
type="text"
className="form-control"
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
className="form-control"
value={filters.status}
onChange={e => handleFilterChange('status', e.target.value)}
>
<option value="">全部</option>
<option value="published">已发布</option>
<option value="draft">草稿</option>
</select>
</div>
</div>
<div className="search-actions">
<button className="btn btn-primary" onClick={() => {}}>查询</button>
<button className="btn" onClick={handleReset}>重置</button>
</div>
</div>
</div>
<div className="card-body">
<table className="table">
<thead>
<tr>
<th>技能名称</th>
<th>分类</th>
<th>版本</th>
<th>状态</th>
<th>安装量</th>
<th>评分</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{mySkills.map(skill => (
<tr key={skill.id} onClick={() => onSkillClick(skill.id)} style={{ cursor: 'pointer' }}>
<td>
<div style={{ fontWeight: 600 }}>{skill.name}</div>
<div style={{ fontSize: '12px', color: '#94A3B8' }}>{skill.desc}</div>
</td>
<td>{skill.category}</td>
<td>{skill.version}</td>
<td><span className={`status ${skill.status === 'published' ? 'status-running' : 'status-stopped'}`}>
{skill.status === 'published' ? '已发布' : '草稿'}
</span></td>
<td>{skill.installs}</td>
<td>{skill.rating || '-'}</td>
<td>
<button className="text-btn text-btn-primary" onClick={e => { e.stopPropagation(); onSkillClick(skill.id); }}>
编辑
</button>
</td>
</tr>
))}
</tbody>
</table>
<div className="card">
<div className="card-header">
<div className="card-title">我的技能</div>
</div>
<div className="card-body">
<div className="table-wrapper">
<table className="table">
<thead>
<tr>
<th>技能名称</th>
<th>分类</th>
<th>版本</th>
<th>状态</th>
<th>安装量</th>
<th>评分</th>
<th style={{ width: '200px' }}>操作</th>
</tr>
</thead>
<tbody>
{filteredList.map(skill => (
<tr key={skill.id} onClick={() => onSkillClick(skill.id)} style={{ cursor: 'pointer' }}>
<td>
<div style={{ fontWeight: 600 }}>{skill.name}</div>
<div style={{ fontSize: '12px', color: '#94A3B8' }}>{skill.desc}</div>
</td>
<td>{skill.category}</td>
<td>{skill.version}</td>
<td>
<span className={`status ${skill.status === 'published' ? 'status-running' : 'status-stopped'}`}>
{skill.status === 'published' ? '已发布' : '草稿'}
</span>
</td>
<td>{skill.installs}</td>
<td>{skill.rating || '-'}</td>
<td>
<div style={{ display: 'flex', gap: '8px' }}>
<button className="text-btn text-btn-primary" onClick={e => { e.stopPropagation(); onSkillClick(skill.id); }}>
编辑
</button>
{skill.status === 'published' && (
<button className="text-btn text-btn-danger" onClick={e => handleUnpublish(e, skill)}>
下架
</button>
)}
<button className="text-btn text-btn-danger" onClick={e => handleDelete(e, skill)}>
删除
</button>
</div>
</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>
</div>
<Modal
visible={!!deleteTarget}
title="确认删除"
onConfirm={confirmDelete}
onCancel={() => setDeleteTarget(null)}
confirmText="删除"
>
确定要删除技能"{deleteTarget?.name}"此操作不可撤销
</Modal>
<Toast
visible={toast.visible}
type={toast.type}
message={toast.message}
onClose={() => setToast(prev => ({ ...prev, visible: false }))}
/>
</>
);
}
export default MySkillsPage;
export default MySkillsPage;

View File

@@ -1,42 +1,56 @@
import { FiUpload } from 'react-icons/fi';
import { useState } from 'react';
import { FiUpload, FiChevronLeft } from 'react-icons/fi';
import Toast from '../../components/common/Toast.jsx';
function NewVersionPage({ skillName, onBack }) {
const [showToast, setShowToast] = useState(false);
const handleSubmit = () => {
setShowToast(true);
setTimeout(() => {
onBack();
}, 1000);
};
return (
<div className="card">
<div className="card-header">
<div className="card-title">上传新版本</div>
<>
<div className="dev-back-btn" onClick={onBack}>
<FiChevronLeft /> 返回技能详情
</div>
<div className="card-body">
<div className="form-group">
<label className="form-label">技能名称</label>
<input type="text" className="form-control" defaultValue={skillName} />
<div className="card">
<div className="card-header">
<div className="card-title">上传新版本 {skillName}</div>
</div>
<div className="form-group">
<label className="form-label">技能描述</label>
<textarea className="form-control" rows="3" placeholder="请输入技能描述"></textarea>
</div>
<div className="form-group">
<label className="form-label">技能分类</label>
<select className="form-control">
<option>信息查询</option>
<option>效率工具</option>
<option>开发工具</option>
</select>
</div>
<div className="form-group">
<label className="form-label">技能包上传</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 className="card-body">
<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' }}>
<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={handleSubmit}>提交审核</button>
</div>
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
<button className="btn" onClick={onBack}>取消</button>
<button className="btn btn-primary">提交审核</button>
</div>
</div>
</div>
<Toast
visible={showToast}
type="success"
message="已提交审核"
onClose={() => setShowToast(false)}
/>
</>
);
}

View File

@@ -1,12 +1,34 @@
import { FiChevronLeft, FiUpload, FiDownload } from 'react-icons/fi';
import { mySkills } from '../../data/developerData.js';
import { useState } from 'react';
import { FiChevronLeft, FiUpload } from 'react-icons/fi';
import { api } from '../../services/api.js';
import Modal from '../../components/common/Modal.jsx';
import Toast from '../../components/common/Toast.jsx';
function SkillEditorPage({ skillId, onBack, onUploadNewVersion, onUpdateInfo }) {
const skill = api.developer.getSkillById(skillId);
const [deleteSkillModal, setDeleteSkillModal] = useState(false);
const [deleteVersionTarget, setDeleteVersionTarget] = useState(null);
const [toast, setToast] = useState({ visible: false, type: 'success', message: '' });
function SkillEditorPage({ skillId, onBack, onUploadNewVersion }) {
const skill = mySkills.find(s => s.id === skillId);
if (!skill) {
return <div>Skill not found</div>;
}
const handleTogglePublish = () => {
const msg = skill.status === 'published' ? '已下架' : '已上架';
setToast({ visible: true, type: 'success', message: msg });
};
const handleDeleteSkill = () => {
setDeleteSkillModal(false);
setToast({ visible: true, type: 'success', message: '已删除' });
};
const handleDeleteVersion = () => {
setDeleteVersionTarget(null);
setToast({ visible: true, type: 'success', message: '已删除' });
};
return (
<>
<div className="dev-back-btn" onClick={onBack}>
@@ -18,7 +40,7 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion }) {
</div>
<div className="card-body">
<div className="dev-detail-header">
<div className="dev-detail-icon">{skill.name.charAt(0)}</div>
<div className="dev-detail-icon">{skill.icon || skill.name.charAt(0)}</div>
<div className="dev-detail-main">
<h2 style={{ marginBottom: '8px' }}>{skill.name}</h2>
<div style={{ color: '#64748B', marginBottom: '12px' }}>{skill.category}</div>
@@ -44,6 +66,25 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion }) {
<span className="dev-info-label">技能描述</span>
<span className="dev-info-value">{skill.desc}</span>
</div>
<div className="dev-info-row">
<span className="dev-info-label">技能分类</span>
<span className="dev-info-value">{skill.category}</span>
</div>
<div className="dev-info-row">
<span className="dev-info-label">技能标签</span>
<span className="dev-info-value">
{skill.tags.map(tag => (
<span key={tag} className="dev-detail-tag" style={{ marginRight: '6px' }}>{tag}</span>
))}
</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>
</div>
</div>
@@ -64,7 +105,7 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion }) {
<col style={{ width: '120px' }} />
<col style={{ width: '120px' }} />
<col style={{ width: '100px' }} />
<col style={{ width: '160px' }} />
<col style={{ width: '240px' }} />
</colgroup>
<thead>
<tr>
@@ -77,10 +118,15 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion }) {
</tr>
</thead>
<tbody>
{skill.versions.map(ver => (
<tr key={ver.version}>
{skill.versions.map((ver, index) => (
<tr key={index}>
<td>{ver.version}</td>
<td>{ver.desc}</td>
<td>
{ver.desc}
{ver.status === 'rejected' && ver.rejectionReason && (
<div className="dev-rejection-reason">{ver.rejectionReason}</div>
)}
</td>
<td>
{ver.status === 'pending' ? (
<span className="status status-warning">审核中</span>
@@ -102,6 +148,11 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion }) {
<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>
)}
</div>
</td>
</tr>
@@ -111,8 +162,32 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion }) {
</div>
</div>
</div>
<Modal
visible={deleteSkillModal}
title="确认删除"
onConfirm={handleDeleteSkill}
onCancel={() => setDeleteSkillModal(false)}
confirmText="删除"
>
确定要删除技能"{skill.name}"此操作不可撤销
</Modal>
<Modal
visible={!!deleteVersionTarget}
title="确认删除"
onConfirm={handleDeleteVersion}
onCancel={() => setDeleteVersionTarget(null)}
confirmText="删除"
>
确定要删除此版本吗此操作不可撤销
</Modal>
<Toast
visible={toast.visible}
type={toast.type}
message={toast.message}
onClose={() => setToast(prev => ({ ...prev, visible: false }))}
/>
</>
);
}
export default SkillEditorPage;
export default SkillEditorPage;

View File

@@ -0,0 +1,132 @@
import { FiX, 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(() => {
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 className="form-group">
<label className="form-label required">技能名称</label>
<input
type="text"
className="form-control"
placeholder="请输入技能名称"
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
<div className="form-group">
<label className="form-label required">技能描述</label>
<textarea
className="form-control"
rows="3"
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>
</div>
</div>
</div>
<Toast
visible={showToast}
type="success"
message="保存成功"
onClose={() => setShowToast(false)}
/>
</>
);
}
export default UpdateSkillInfoPage;

View File

@@ -1,14 +1,21 @@
import { FiUpload, FiX } from 'react-icons/fi';
import { useState } from 'react';
import { api } from '../../services/api.js';
import Toast from '../../components/common/Toast.jsx';
function UploadSkillPage() {
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())) {
if (!tags.includes(tagInput.trim()) && tags.length < 5) {
setTags([...tags, tagInput.trim()]);
}
setTagInput('');
@@ -19,6 +26,10 @@ function UploadSkillPage() {
setTags(tags.filter(tag => tag !== tagToRemove));
};
const handleCreate = () => {
setShowToast(true);
};
return (
<div className="card">
<div className="card-header">
@@ -27,18 +38,18 @@ function UploadSkillPage() {
<div className="card-body">
<div className="form-group">
<label className="form-label required">技能名称</label>
<input type="text" className="form-control" placeholder="请输入技能名称" required />
<input type="text" className="form-control" placeholder="请输入技能名称" />
</div>
<div className="form-group">
<label className="form-label required">技能描述</label>
<textarea className="form-control" rows="3" placeholder="请输入技能描述" required></textarea>
<textarea className="form-control" rows="3" placeholder="请输入技能描述" />
</div>
<div className="form-group">
<label className="form-label">技能分类</label>
<select className="form-control">
<option>信息查询</option>
<option>效率工具</option>
<option>开发工具</option>
{categories.map(cat => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
</div>
<div className="form-group">
@@ -61,6 +72,21 @@ function UploadSkillPage() {
</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 className="form-group">
<label className="form-label required">技能包上传</label>
<div style={{ border: '2px dashed #E2E8F0', borderRadius: '8px', padding: '40px', textAlign: 'center', color: '#94A3B8' }}>
@@ -70,12 +96,18 @@ function UploadSkillPage() {
</div>
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
<button className="btn">取消</button>
<button className="btn btn-primary">创建技能</button>
<button className="btn" onClick={onBack}>取消</button>
<button className="btn btn-primary" onClick={handleCreate}>创建技能</button>
</div>
</div>
<Toast
visible={showToast}
type="success"
message="创建成功"
onClose={() => setShowToast(false)}
/>
</div>
);
}
export default UploadSkillPage;
export default UploadSkillPage;

View File

@@ -7,7 +7,7 @@
import { conversations, getChatScenes } from '../data/conversations.js';
import { skills, skillFiles, skillVersions, getSkillIcon } from '../data/skills.js';
import { logs } from '../data/logs.js';
import { mySkills, skillCategories, supportedModels, devDocs } from '../data/developerData.js';
import { mySkills, skillCategories, devDocs, developerOverview } from '../data/developerData.js';
import { projectMembers } from '../data/members.js';
import { scheduledTasks } from '../data/tasks.js';
import { adminDepartments, adminUsers, adminProjects, adminOverview, adminLogs } from '../data/adminData.js';
@@ -137,10 +137,10 @@ export const developerApi = {
getCategories: () => skillCategories,
/**
* 获取支持的模型列表
* @returns {Array} 模型列表
* 获取开发者总览数据
* @returns {Object} 总览数据
*/
getSupportedModels: () => supportedModels,
getOverview: () => developerOverview,
/**
* 获取开发文档列表

View File

@@ -89,3 +89,46 @@
font-weight: 600;
margin-bottom: 16px;
}
/* 技能图标选择器 */
.dev-icon-picker {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 8px;
max-width: 360px;
}
.dev-icon-option {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
border: 2px solid #E2E8F0;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
background: #fff;
&:hover {
border-color: #3B82F6;
background: #EFF6FF;
}
&.selected {
border-color: #3B82F6;
background: #EFF6FF;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
}
/* 版本拒绝原因 */
.dev-rejection-reason {
font-size: 12px;
color: #EF4444;
margin-top: 4px;
padding: 4px 8px;
background: #FEF2F2;
border-radius: 4px;
}