@yuku123/z-agent-frontend-component 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -0
- package/dist/z-agent-frontend-component.css +1 -0
- package/dist/z-agent-frontend-component.es.js +9956 -0
- package/dist/z-agent-frontend-component.umd.js +219 -0
- package/package.json +77 -0
- package/src/api/apiRouter.js +78 -0
- package/src/api/index.js +23 -0
- package/src/api/request.js +59 -0
- package/src/api/routes.js +140 -0
- package/src/dev.jsx +80 -0
- package/src/index.js +86 -0
- package/src/pages/agent/app/index.jsx +2 -0
- package/src/pages/agent/editor/AgentAppEditor.jsx +456 -0
- package/src/pages/agent/editor/WorkflowEditor.jsx +495 -0
- package/src/pages/agent/editor/nodes/index.ts +225 -0
- package/src/pages/agent/index.jsx +1379 -0
- package/src/pages/agent/share.jsx +512 -0
- package/src/pages/ak/AkUsageDrawer.jsx +208 -0
- package/src/pages/ak/index.jsx +496 -0
- package/src/pages/llm/index.jsx +736 -0
- package/src/pages/llm/model/index.jsx +220 -0
- package/src/pages/llm/provider/index.jsx +173 -0
- package/src/pages/mcp/index.jsx +359 -0
- package/src/pages/oss/BucketList.jsx +320 -0
- package/src/pages/oss/ObjectBrowser.jsx +409 -0
- package/src/pages/product/execute.jsx +608 -0
- package/src/pages/product/index.jsx +628 -0
- package/src/pages/product/scene.jsx +746 -0
- package/src/pages/script/ApiBridgeEditor.jsx +255 -0
- package/src/pages/script/CurlImportModal.jsx +263 -0
- package/src/pages/script/FieldMappingEditor.jsx +131 -0
- package/src/pages/script/OpenApiImportModal.jsx +212 -0
- package/src/pages/script/index.jsx +532 -0
- package/src/pages/skill/index.jsx +1595 -0
- package/src/pages/trace/DebugPlayground.jsx +357 -0
- package/src/pages/trace/components/MetricsDashboard.jsx +164 -0
- package/src/pages/trace/components/RagFragments.jsx +134 -0
- package/src/pages/trace/components/Timeline.jsx +142 -0
- package/src/pages/trace/components/ToolCallTree.jsx +116 -0
- package/src/pages/trace/index.jsx +13 -0
- package/src/pages/usage/index.jsx +352 -0
|
@@ -0,0 +1,1595 @@
|
|
|
1
|
+
import {useCallback, useEffect, useRef, useState} from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Card,
|
|
5
|
+
Col,
|
|
6
|
+
Drawer,
|
|
7
|
+
Dropdown,
|
|
8
|
+
Empty,
|
|
9
|
+
Form,
|
|
10
|
+
Input,
|
|
11
|
+
message,
|
|
12
|
+
Modal,
|
|
13
|
+
Pagination,
|
|
14
|
+
Row,
|
|
15
|
+
Select,
|
|
16
|
+
Spin,
|
|
17
|
+
Tabs,
|
|
18
|
+
Tag,
|
|
19
|
+
Timeline,
|
|
20
|
+
Tooltip,
|
|
21
|
+
Tree,
|
|
22
|
+
Typography,
|
|
23
|
+
Upload
|
|
24
|
+
} from 'antd'
|
|
25
|
+
import {
|
|
26
|
+
AppstoreOutlined,
|
|
27
|
+
CheckCircleOutlined,
|
|
28
|
+
ClockCircleOutlined,
|
|
29
|
+
CloudUploadOutlined,
|
|
30
|
+
CodeOutlined,
|
|
31
|
+
CopyOutlined,
|
|
32
|
+
DeleteOutlined,
|
|
33
|
+
DownloadOutlined,
|
|
34
|
+
EditOutlined,
|
|
35
|
+
FileTextOutlined,
|
|
36
|
+
FileZipOutlined,
|
|
37
|
+
FireOutlined,
|
|
38
|
+
FolderOpenOutlined,
|
|
39
|
+
FolderOutlined,
|
|
40
|
+
HistoryOutlined,
|
|
41
|
+
InfoCircleOutlined,
|
|
42
|
+
MoreOutlined,
|
|
43
|
+
PlusOutlined,
|
|
44
|
+
ReloadOutlined,
|
|
45
|
+
SearchOutlined,
|
|
46
|
+
StarOutlined
|
|
47
|
+
} from '@ant-design/icons'
|
|
48
|
+
import ReactMarkdown from 'react-markdown'
|
|
49
|
+
import {skillApi} from '../../api'
|
|
50
|
+
|
|
51
|
+
// 示例数据(后端无 skill 模块时的回退)
|
|
52
|
+
const mockSkills = [
|
|
53
|
+
{
|
|
54
|
+
id: 1,
|
|
55
|
+
skillCode: 'code-review',
|
|
56
|
+
skillName: '代码审查',
|
|
57
|
+
categoryCode: 'backend',
|
|
58
|
+
version: '1.2.0',
|
|
59
|
+
author: 'zifang',
|
|
60
|
+
status: 'PUBLISHED',
|
|
61
|
+
description: '自动审查代码质量,检测潜在 Bug 和安全漏洞,支持 Java/Python/Go 等多种语言。',
|
|
62
|
+
tags: 'java,python,code-quality,lint',
|
|
63
|
+
downloadCount: 128,
|
|
64
|
+
content: '# 代码审查技能\n\n自动分析代码变更并给出审查意见。',
|
|
65
|
+
files: [{
|
|
66
|
+
path: 'SKILL.md',
|
|
67
|
+
content: '# Code Review Skill\n\nAutomated code review with AI-powered analysis.\n\n## Features\n- Static analysis\n- Security vulnerability detection\n- Style enforcement\n'
|
|
68
|
+
}, {
|
|
69
|
+
path: 'scripts/analyze.py',
|
|
70
|
+
content: 'import ast\n\ndef analyze_file(filepath):\n with open(filepath) as f:\n tree = ast.parse(f.read())\n results = []\n for node in ast.walk(tree):\n if isinstance(node, ast.FunctionDef):\n results.append({"type": "function", "name": node.name})\n return results\n'
|
|
71
|
+
}, {
|
|
72
|
+
path: 'rules/java-rules.yaml',
|
|
73
|
+
content: 'rules:\n - id: avoid-system-out\n pattern: System.out.println\n severity: WARNING\n - id: catch-exception\n pattern: catch (Exception e)\n severity: ERROR\n'
|
|
74
|
+
}]
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 2,
|
|
78
|
+
skillCode: 'data-analyzer',
|
|
79
|
+
skillName: '数据分析师',
|
|
80
|
+
categoryCode: 'backend',
|
|
81
|
+
version: '1.0.0',
|
|
82
|
+
author: 'zifang',
|
|
83
|
+
status: 'PUBLISHED',
|
|
84
|
+
description: 'SQL 查询生成与数据分析助手,支持 MySQL/PostgreSQL/ClickHouse。',
|
|
85
|
+
tags: 'sql,data,analysis,dashboard',
|
|
86
|
+
downloadCount: 86,
|
|
87
|
+
content: '# 数据分析师技能\n\n通过自然语言生成SQL查询。',
|
|
88
|
+
files: [{
|
|
89
|
+
path: 'SKILL.md',
|
|
90
|
+
content: '# Data Analyzer Skill\n\nNatural language to SQL query generation.\n## Supported Databases: MySQL, PostgreSQL, ClickHouse\n'
|
|
91
|
+
}, {
|
|
92
|
+
path: 'queries/templates.sql',
|
|
93
|
+
content: '-- User retention\nSELECT DATE(created_at) as day, COUNT(DISTINCT user_id) as active_users\nFROM user_events\nWHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY DATE(created_at) ORDER BY day;\n-- Revenue summary\nSELECT product_category, SUM(amount) as total_revenue\nFROM transactions GROUP BY product_category;\n'
|
|
94
|
+
}]
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 3,
|
|
98
|
+
skillCode: 'ui-gen',
|
|
99
|
+
skillName: 'UI 生成器',
|
|
100
|
+
categoryCode: 'frontend',
|
|
101
|
+
version: '2.1.0',
|
|
102
|
+
author: 'zifang',
|
|
103
|
+
status: 'PUBLISHED',
|
|
104
|
+
description: '从设计稿或描述生成 React 组件代码,支持 Ant Design 组件库。',
|
|
105
|
+
tags: 'react,antd,ui,component',
|
|
106
|
+
downloadCount: 215,
|
|
107
|
+
content: '# UI 生成器技能\n\n从文字描述生成UI代码。',
|
|
108
|
+
files: [{
|
|
109
|
+
path: 'SKILL.md',
|
|
110
|
+
content: '# UI Generator Skill\n\nGenerate React components from natural language.\n## Tech Stack: React 18, Ant Design 5, TypeScript\n'
|
|
111
|
+
}, {
|
|
112
|
+
path: 'templates/component.ejs',
|
|
113
|
+
content: 'import React from "react";\nimport { Card, Button, Space } from "antd";\n\ninterface <%= compName %>Props { title?: string; onAction?: () => void }\n\nconst <%= compName %>: React.FC<<%= compName %>Props> = ({ title, onAction }) => (\n <Card title={title}><Space><Button onClick={onAction}>Action</Button></Space></Card>\n);\nexport default <%= compName %>;\n'
|
|
114
|
+
}, {
|
|
115
|
+
path: 'styles/default.css',
|
|
116
|
+
content: '.ui-gen-container { padding: 24px; background: #fff; border-radius: 8px; }\n.ui-gen-header { display: flex; align-items: center; margin-bottom: 16px; }\n'
|
|
117
|
+
}]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 4,
|
|
121
|
+
skillCode: 'doc-writer',
|
|
122
|
+
skillName: '文档编写',
|
|
123
|
+
categoryCode: 'doc',
|
|
124
|
+
version: '1.0.0',
|
|
125
|
+
author: 'zifang',
|
|
126
|
+
status: 'PUBLISHED',
|
|
127
|
+
description: '自动生成项目文档、API 文档、README。支持 Javadoc/Swagger/Markdown 格式。',
|
|
128
|
+
tags: 'documentation,api,markdown,javadoc',
|
|
129
|
+
downloadCount: 67,
|
|
130
|
+
content: '# 文档编写技能\n\n自动生成各类文档。',
|
|
131
|
+
files: [{
|
|
132
|
+
path: 'SKILL.md',
|
|
133
|
+
content: '# Doc Writer Skill\n\nAutomated documentation generation.\n## Formats: Markdown, Javadoc, OpenAPI/Swagger\n'
|
|
134
|
+
}, {
|
|
135
|
+
path: 'templates/api-doc.md',
|
|
136
|
+
content: '# {{apiName}}\n\n{{description}}\n\n{{#each endpoints}}\n### {{method}} {{path}}\n**Parameters:**\n| Name | Type | Required | Description |\n|------|------|----------|-------------|\n{{/each}}\n'
|
|
137
|
+
}]
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: 5,
|
|
141
|
+
skillCode: 'ci-helper',
|
|
142
|
+
skillName: 'CI/CD 助手',
|
|
143
|
+
categoryCode: 'devops',
|
|
144
|
+
version: '2.0.0',
|
|
145
|
+
author: 'zifang',
|
|
146
|
+
status: 'PUBLISHED',
|
|
147
|
+
description: 'Pipeline 配置生成、Dockerfile 优化、K8s 部署清单自动生成。',
|
|
148
|
+
tags: 'ci/cd,docker,kubernetes,devops',
|
|
149
|
+
downloadCount: 94,
|
|
150
|
+
content: '# CI/CD 助手技能\n\n自动化运维配置生成。',
|
|
151
|
+
files: [{
|
|
152
|
+
path: 'SKILL.md',
|
|
153
|
+
content: '# CI/CD Helper Skill\n\nAutomated CI/CD configuration generation.\n## Platforms: GitHub Actions, GitLab CI, Jenkins\n'
|
|
154
|
+
}, {
|
|
155
|
+
path: 'pipelines/build.yaml',
|
|
156
|
+
content: 'name: build-and-test\non: [push, pull_request]\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - name: Setup JDK 17\n uses: actions/setup-java@v3\n with:\n java-version: "17"\n distribution: "temurin"\n - run: mvn clean verify\n'
|
|
157
|
+
}, {
|
|
158
|
+
path: 'docker/Dockerfile',
|
|
159
|
+
content: 'FROM eclipse-temurin:17-jre-alpine\nWORKDIR /app\nCOPY target/*.jar app.jar\nEXPOSE 8080\nENTRYPOINT ["java", "-jar", "app.jar"]\n'
|
|
160
|
+
}]
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 6,
|
|
164
|
+
skillCode: 'test-gen',
|
|
165
|
+
skillName: '测试生成器',
|
|
166
|
+
categoryCode: 'testing',
|
|
167
|
+
version: '1.3.0',
|
|
168
|
+
author: 'zifang',
|
|
169
|
+
status: 'PUBLISHED',
|
|
170
|
+
description: '基于代码自动生成单元测试和集成测试用例,支持 JUnit/TestNG/Pytest。',
|
|
171
|
+
tags: 'testing,junit,pytest,tdd',
|
|
172
|
+
downloadCount: 156,
|
|
173
|
+
content: '# 测试生成器技能\n\n自动生成测试代码。',
|
|
174
|
+
files: [{
|
|
175
|
+
path: 'SKILL.md',
|
|
176
|
+
content: '# Test Generator Skill\n\nAutomated test generation.\n## Frameworks: JUnit 5, TestNG, Pytest\n'
|
|
177
|
+
}, {
|
|
178
|
+
path: 'configs/junit-config.yaml',
|
|
179
|
+
content: 'generator:\n framework: junit5\n mockLibrary: mockito\n coverage: 0.8\n rules:\n - skipGettersSetters: true\n - generateEdgeCases: true\n - maxAssertionsPerTest: 5\n'
|
|
180
|
+
}]
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: 7,
|
|
184
|
+
skillCode: 'agent-flow',
|
|
185
|
+
skillName: 'Agent 工作流',
|
|
186
|
+
categoryCode: 'ai-agent',
|
|
187
|
+
version: '1.1.0',
|
|
188
|
+
author: 'zifang',
|
|
189
|
+
status: 'PUBLISHED',
|
|
190
|
+
description: '多步骤 Agent 工作流编排引擎,支持条件判断、循环、并行执行。',
|
|
191
|
+
tags: 'agent,workflow,automation,llm',
|
|
192
|
+
downloadCount: 201,
|
|
193
|
+
content: '# Agent 工作流技能\n\n编排多步骤 Agent 工作流。',
|
|
194
|
+
files: [{
|
|
195
|
+
path: 'SKILL.md',
|
|
196
|
+
content: '# Agent Flow Skill\n\nMulti-step agent workflow orchestration.\n## Features: Conditional branching, Loops, Parallel execution, Retry\n'
|
|
197
|
+
}, {
|
|
198
|
+
path: 'workflows/order-flow.yaml',
|
|
199
|
+
content: 'workflow:\n id: order-processing\n steps:\n - id: validate-order\n type: function\n handler: validateOrder\n - id: check-inventory\n type: function\n handler: checkInventory\n retry: 3\n - id: process-payment\n type: function\n handler: processPayment\n - id: send-notification\n type: function\n parallel: true\n'
|
|
200
|
+
}, {
|
|
201
|
+
path: 'nodes/check-condition.js',
|
|
202
|
+
content: 'module.exports = async (context, params) => {\n const { field, operator, value } = params;\n const actual = context.get(field);\n const ops = { eq: (a, b) => a === b, gt: (a, b) => a > b, lt: (a, b) => a < b, in: (a, b) => b.includes(a) };\n if (!ops[operator]) throw new Error("Unknown operator: " + operator);\n return ops[operator](actual, value);\n};\n'
|
|
203
|
+
}]
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: 8,
|
|
207
|
+
skillCode: 'api-mocker',
|
|
208
|
+
skillName: 'API Mock',
|
|
209
|
+
categoryCode: 'frontend',
|
|
210
|
+
version: '1.0.0',
|
|
211
|
+
author: 'zifang',
|
|
212
|
+
status: 'DRAFT',
|
|
213
|
+
description: '快速生成 API Mock 服务,基于 OpenAPI 规范自动生成模拟数据。',
|
|
214
|
+
tags: 'api,mock,prototype',
|
|
215
|
+
downloadCount: 45,
|
|
216
|
+
content: '# API Mock 技能\n\n快速生成API Mock。',
|
|
217
|
+
files: [{
|
|
218
|
+
path: 'SKILL.md',
|
|
219
|
+
content: '# API Mock Skill\n\nRapid API mock server from OpenAPI specs.\n## Features: OpenAPI 3.0, Realistic data, Custom delay\n'
|
|
220
|
+
}, {
|
|
221
|
+
path: 'configs/openapi-spec.yaml',
|
|
222
|
+
content: 'openapi: "3.0.0"\ninfo:\n title: Sample API\n version: "1.0.0"\npaths:\n /users:\n get:\n summary: List users\n responses:\n "200":\n description: User list\n content:\n application/json:\n schema:\n type: array\n items:\n $ref: "#/components/schemas/User"\ncomponents:\n schemas:\n User:\n type: object\n properties:\n id: { type: integer }\n name: { type: string }\n email: { type: string }\n'
|
|
223
|
+
}]
|
|
224
|
+
},
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
const mockCategories = [
|
|
228
|
+
{categoryCode: 'backend', categoryName: '后端开发', parentCode: '', skillCount: 2},
|
|
229
|
+
{categoryCode: 'frontend', categoryName: '前端开发', parentCode: '', skillCount: 2},
|
|
230
|
+
{categoryCode: 'devops', categoryName: 'DevOps', parentCode: '', skillCount: 1},
|
|
231
|
+
{categoryCode: 'testing', categoryName: '测试', parentCode: '', skillCount: 1},
|
|
232
|
+
{categoryCode: 'ai-agent', categoryName: 'AI Agent', parentCode: '', skillCount: 1},
|
|
233
|
+
{categoryCode: 'doc', categoryName: '文档', parentCode: '', skillCount: 1},
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
const mockStats = {total: 8, published: 7}
|
|
237
|
+
|
|
238
|
+
const {TextArea} = Input
|
|
239
|
+
const {Title, Paragraph, Text} = Typography
|
|
240
|
+
|
|
241
|
+
const GRADIENTS = {
|
|
242
|
+
backend: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
243
|
+
frontend: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
|
|
244
|
+
devops: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
|
|
245
|
+
testing: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
|
|
246
|
+
'ai-agent': 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
|
|
247
|
+
doc: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)',
|
|
248
|
+
default: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const getGradient = (code) => GRADIENTS[code] || GRADIENTS.default
|
|
252
|
+
const getContrast = (code) => {
|
|
253
|
+
const map = {
|
|
254
|
+
backend: '#fff',
|
|
255
|
+
frontend: '#fff',
|
|
256
|
+
devops: '#fff',
|
|
257
|
+
testing: '#333',
|
|
258
|
+
'ai-agent': '#333',
|
|
259
|
+
doc: '#333',
|
|
260
|
+
default: '#fff'
|
|
261
|
+
}
|
|
262
|
+
return map[code] || '#fff'
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const STATUS_LABEL = {PUBLISHED: '已发布', DRAFT: '草稿', ARCHIVED: '已归档'}
|
|
266
|
+
|
|
267
|
+
const TAG_COLORS = ['blue', 'green', 'cyan', 'purple', 'orange', 'red']
|
|
268
|
+
|
|
269
|
+
// 扁平列表构建嵌套树
|
|
270
|
+
const buildCategoryTreeData = (flatCats) => {
|
|
271
|
+
const itemMap = {}
|
|
272
|
+
flatCats.forEach(c => {
|
|
273
|
+
itemMap[c.categoryCode] = {
|
|
274
|
+
key: c.categoryCode,
|
|
275
|
+
title: c.categoryName,
|
|
276
|
+
categoryCode: c.categoryCode,
|
|
277
|
+
categoryName: c.categoryName,
|
|
278
|
+
parentCode: c.parentCode || '',
|
|
279
|
+
skillCount: c.skillCount || 0,
|
|
280
|
+
data: c,
|
|
281
|
+
children: [],
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
const roots = []
|
|
285
|
+
flatCats.forEach(c => {
|
|
286
|
+
const node = itemMap[c.categoryCode]
|
|
287
|
+
if (c.parentCode && itemMap[c.parentCode]) {
|
|
288
|
+
itemMap[c.parentCode].children.push(node)
|
|
289
|
+
} else if (!c.parentCode) {
|
|
290
|
+
roots.push(node)
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
return roots
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 树形数据扁平化(从 API 获取树后转为扁平列表用于 CRUD)
|
|
297
|
+
const flattenTree = (nodes) => {
|
|
298
|
+
const result = []
|
|
299
|
+
const walk = (items, parentCode) => {
|
|
300
|
+
items.forEach(n => {
|
|
301
|
+
const code = n.categoryCode || n.key
|
|
302
|
+
result.push({
|
|
303
|
+
categoryCode: code,
|
|
304
|
+
categoryName: n.categoryName || n.title,
|
|
305
|
+
parentCode: parentCode || n.parentCode || '',
|
|
306
|
+
skillCount: n.skillCount || 0,
|
|
307
|
+
})
|
|
308
|
+
if (n.children?.length) walk(n.children, code)
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
if (nodes?.length) walk(nodes, '')
|
|
312
|
+
return result
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 文件路径 → 嵌套树
|
|
316
|
+
function buildFileTree(files) {
|
|
317
|
+
const root = {key: '__root__', children: []}
|
|
318
|
+
const dirMap = {'': root}
|
|
319
|
+
// collect all dirs
|
|
320
|
+
files.forEach(f => {
|
|
321
|
+
const parts = f.path.split('/')
|
|
322
|
+
parts.pop() // leaf filename
|
|
323
|
+
let parentKey = ''
|
|
324
|
+
parts.forEach((dir, i) => {
|
|
325
|
+
const key = parts.slice(0, i + 1).join('/')
|
|
326
|
+
if (!dirMap[key]) {
|
|
327
|
+
dirMap[key] = {key, title: dir, isLeaf: false, children: []}
|
|
328
|
+
}
|
|
329
|
+
if (!dirMap[parentKey].children.some(c => c.key === key)) {
|
|
330
|
+
dirMap[parentKey].children.push(dirMap[key])
|
|
331
|
+
}
|
|
332
|
+
parentKey = key
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
// add leaf files
|
|
336
|
+
files.forEach(f => {
|
|
337
|
+
const parentKey = f.path.includes('/') ? f.path.split('/').slice(0, -1).join('/') : ''
|
|
338
|
+
const fileName = f.path.includes('/') ? f.path.split('/').pop() : f.path
|
|
339
|
+
dirMap[parentKey].children.push({
|
|
340
|
+
key: f.path,
|
|
341
|
+
title: fileName,
|
|
342
|
+
isLeaf: true,
|
|
343
|
+
file: f,
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
return root.children
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export default function SkillMarket() {
|
|
350
|
+
const [skills, setSkills] = useState([])
|
|
351
|
+
const [loading, setLoading] = useState(false)
|
|
352
|
+
const [total, setTotal] = useState(0)
|
|
353
|
+
const [current, setCurrent] = useState(1)
|
|
354
|
+
const pageSizeRef = useRef(12)
|
|
355
|
+
|
|
356
|
+
const [keyword, setKeyword] = useState('')
|
|
357
|
+
const [categoryCode, setCategoryCode] = useState(undefined)
|
|
358
|
+
const [sortBy, setSortBy] = useState('download_count')
|
|
359
|
+
const [categories, setCategories] = useState([])
|
|
360
|
+
const [expandedKeys, setExpandedKeys] = useState([])
|
|
361
|
+
|
|
362
|
+
const [drawerOpen, setDrawerOpen] = useState(false)
|
|
363
|
+
const [detailSkill, setDetailSkill] = useState(null)
|
|
364
|
+
const [detailContent, setDetailContent] = useState('')
|
|
365
|
+
const [versions, setVersions] = useState([])
|
|
366
|
+
const [detailLoading, setDetailLoading] = useState(false)
|
|
367
|
+
const [detailTab, setDetailTab] = useState('overview')
|
|
368
|
+
const [selectedFile, setSelectedFile] = useState(null)
|
|
369
|
+
const [fileTreeKeys, setFileTreeKeys] = useState([])
|
|
370
|
+
|
|
371
|
+
const [pubModalOpen, setPubModalOpen] = useState(false)
|
|
372
|
+
const [pubForm] = Form.useForm()
|
|
373
|
+
const [pubLoading, setPubLoading] = useState(false)
|
|
374
|
+
const [uploadLoading, setUploadLoading] = useState(false)
|
|
375
|
+
|
|
376
|
+
const [stats, setStats] = useState({total: 0, published: 0})
|
|
377
|
+
|
|
378
|
+
// 分类 CRUD
|
|
379
|
+
const [catModalVisible, setCatModalVisible] = useState(false)
|
|
380
|
+
const [editingCategory, setEditingCategory] = useState(null)
|
|
381
|
+
const [parentForCreate, setParentForCreate] = useState(null)
|
|
382
|
+
const [catLoading, setCatLoading] = useState(false)
|
|
383
|
+
const [categoryForm] = Form.useForm()
|
|
384
|
+
|
|
385
|
+
// 拖拽分割
|
|
386
|
+
const [treeWidth, setTreeWidth] = useState(220)
|
|
387
|
+
const treeRef = useRef(null)
|
|
388
|
+
const draggingRef = useRef(false)
|
|
389
|
+
const startXRef = useRef(0)
|
|
390
|
+
const startWRef = useRef(0)
|
|
391
|
+
|
|
392
|
+
const loadStats = async () => {
|
|
393
|
+
try {
|
|
394
|
+
const res = await skillApi.stats()
|
|
395
|
+
if (res && (res.total !== undefined || res.published !== undefined)) {
|
|
396
|
+
setStats(res)
|
|
397
|
+
} else if (res?.data) {
|
|
398
|
+
setStats(res.data)
|
|
399
|
+
} else {
|
|
400
|
+
setStats(mockStats)
|
|
401
|
+
}
|
|
402
|
+
} catch (e) {
|
|
403
|
+
setStats(mockStats)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const loadSkills = useCallback(async (page = 1) => {
|
|
408
|
+
setLoading(true)
|
|
409
|
+
try {
|
|
410
|
+
const params = {current: page, size: pageSizeRef.current, sortBy}
|
|
411
|
+
if (keyword) params.keyword = keyword
|
|
412
|
+
if (categoryCode) params.categoryCode = categoryCode
|
|
413
|
+
if (keyword || categoryCode) params.status = 'PUBLISHED'
|
|
414
|
+
|
|
415
|
+
const res = await skillApi.page(params)
|
|
416
|
+
if (res && res.records) {
|
|
417
|
+
setSkills(res.records)
|
|
418
|
+
setTotal(Number(res.total) || res.records.length)
|
|
419
|
+
} else if (Array.isArray(res)) {
|
|
420
|
+
setSkills(res)
|
|
421
|
+
setTotal(res.length)
|
|
422
|
+
} else if (res?.data?.records) {
|
|
423
|
+
setSkills(res.data.records)
|
|
424
|
+
setTotal(Number(res.data.total) || res.data.records.length)
|
|
425
|
+
} else {
|
|
426
|
+
// 回退到 mock 数据
|
|
427
|
+
const filtered = mockSkills.filter(s => {
|
|
428
|
+
if (keyword && !s.skillName.includes(keyword) && !s.description.includes(keyword)) return false
|
|
429
|
+
if (categoryCode && s.categoryCode !== categoryCode) return false
|
|
430
|
+
return true
|
|
431
|
+
})
|
|
432
|
+
setSkills(filtered)
|
|
433
|
+
setTotal(filtered.length)
|
|
434
|
+
}
|
|
435
|
+
} catch (e) {
|
|
436
|
+
console.error('loadSkills error:', e)
|
|
437
|
+
const filtered = mockSkills.filter(s => {
|
|
438
|
+
if (keyword && !s.skillName.includes(keyword) && !s.description.includes(keyword)) return false
|
|
439
|
+
if (categoryCode && s.categoryCode !== categoryCode) return false
|
|
440
|
+
return true
|
|
441
|
+
})
|
|
442
|
+
setSkills(filtered)
|
|
443
|
+
setTotal(filtered.length)
|
|
444
|
+
} finally {
|
|
445
|
+
setLoading(false)
|
|
446
|
+
}
|
|
447
|
+
}, [keyword, categoryCode, sortBy])
|
|
448
|
+
|
|
449
|
+
const loadCategories = async () => {
|
|
450
|
+
try {
|
|
451
|
+
const res = await skillApi.categoryTree()
|
|
452
|
+
// API 返回 catCode/catName → 映射为 categoryCode/categoryName
|
|
453
|
+
const mapCat = (items) => (items || []).map(c => ({
|
|
454
|
+
categoryCode: c.catCode || c.categoryCode,
|
|
455
|
+
categoryName: c.catName || c.categoryName,
|
|
456
|
+
parentCode: c.parentCode || '',
|
|
457
|
+
skillCount: c.skillCount || 0,
|
|
458
|
+
children: c.children ? mapCat(c.children) : undefined,
|
|
459
|
+
}))
|
|
460
|
+
if (Array.isArray(res) && res.length > 0) {
|
|
461
|
+
const mapped = mapCat(res)
|
|
462
|
+
if (mapped[0]?.children) {
|
|
463
|
+
setCategories(flattenTree(mapped))
|
|
464
|
+
} else {
|
|
465
|
+
setCategories(mapped)
|
|
466
|
+
}
|
|
467
|
+
} else if (res?.data && res.data.length > 0) {
|
|
468
|
+
const mapped = mapCat(res.data)
|
|
469
|
+
if (mapped[0]?.children) {
|
|
470
|
+
setCategories(flattenTree(mapped))
|
|
471
|
+
} else {
|
|
472
|
+
setCategories(mapped)
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
setCategories(mockCategories)
|
|
476
|
+
}
|
|
477
|
+
} catch (e) {
|
|
478
|
+
// API 失败时从 localStorage 恢复,没有则用 mock
|
|
479
|
+
const cached = localStorage.getItem('z_skill_categories')
|
|
480
|
+
if (cached) {
|
|
481
|
+
try {
|
|
482
|
+
setCategories(JSON.parse(cached))
|
|
483
|
+
} catch {
|
|
484
|
+
setCategories(mockCategories)
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
setCategories(mockCategories)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
useEffect(() => {
|
|
493
|
+
loadStats()
|
|
494
|
+
loadCategories()
|
|
495
|
+
}, [])
|
|
496
|
+
|
|
497
|
+
useEffect(() => {
|
|
498
|
+
loadSkills(current)
|
|
499
|
+
}, [loadSkills, current])
|
|
500
|
+
|
|
501
|
+
const handleKeywordSearch = () => {
|
|
502
|
+
setCurrent(1);
|
|
503
|
+
loadSkills(1)
|
|
504
|
+
}
|
|
505
|
+
const handleCategoryChange = (code) => {
|
|
506
|
+
setCategoryCode(prev => prev === code ? undefined : code)
|
|
507
|
+
setCurrent(1)
|
|
508
|
+
}
|
|
509
|
+
const handleSortChange = (sort) => {
|
|
510
|
+
setSortBy(sort);
|
|
511
|
+
setCurrent(1)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const openDetail = async (skill) => {
|
|
515
|
+
setDetailSkill(skill)
|
|
516
|
+
setDrawerOpen(true)
|
|
517
|
+
setDetailContent(skill.content || '')
|
|
518
|
+
setVersions([])
|
|
519
|
+
setSelectedFile(null)
|
|
520
|
+
setFileTreeKeys([])
|
|
521
|
+
setDetailTab('overview')
|
|
522
|
+
setDetailLoading(true)
|
|
523
|
+
try {
|
|
524
|
+
// Load detail and versions in parallel
|
|
525
|
+
const [detailRes, versionsRes] = await Promise.allSettled([
|
|
526
|
+
skillApi.getBySkillCode(skill.skillCode),
|
|
527
|
+
skillApi.versions(skill.skillCode),
|
|
528
|
+
])
|
|
529
|
+
// Process detail
|
|
530
|
+
if (detailRes.status === 'fulfilled' && detailRes.value) {
|
|
531
|
+
const data = detailRes.value?.data || detailRes.value
|
|
532
|
+
if (data && typeof data === 'object') {
|
|
533
|
+
const merged = {...skill, ...data}
|
|
534
|
+
setDetailSkill(merged)
|
|
535
|
+
setDetailContent(data.content || data.readme || skill.content || '')
|
|
536
|
+
if (data.files?.length) setSelectedFile(data.files[0])
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// Process versions
|
|
540
|
+
if (versionsRes.status === 'fulfilled' && versionsRes.value) {
|
|
541
|
+
const vData = versionsRes.value?.data || versionsRes.value
|
|
542
|
+
if (Array.isArray(vData)) setVersions(vData)
|
|
543
|
+
}
|
|
544
|
+
} catch (e) {
|
|
545
|
+
console.warn('openDetail error:', e)
|
|
546
|
+
} finally {
|
|
547
|
+
setDetailLoading(false)
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const handleCopyContent = async (text) => {
|
|
552
|
+
try {
|
|
553
|
+
await navigator.clipboard.writeText(text)
|
|
554
|
+
message.success('已复制到剪贴板')
|
|
555
|
+
} catch {
|
|
556
|
+
message.error('复制失败')
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const handleDownload = (skill) => {
|
|
561
|
+
if (!skill) return
|
|
562
|
+
if (skill.packageType === 'ZIP' && (skill.packagePath || skill.version)) {
|
|
563
|
+
const url = skillApi.downloadPackage(skill.skillCode, skill.version)
|
|
564
|
+
window.open(url, '_blank')
|
|
565
|
+
} else if (skill.content) {
|
|
566
|
+
handleCopyContent(skill.content)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const handleInstall = async (skillCode) => {
|
|
571
|
+
try {
|
|
572
|
+
const res = await skillApi.install({skillCode})
|
|
573
|
+
if (res !== undefined && res !== null) {
|
|
574
|
+
message.success('安装成功');
|
|
575
|
+
loadStats()
|
|
576
|
+
} else message.error('安装失败')
|
|
577
|
+
} catch (e) {
|
|
578
|
+
message.error('安装异常')
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// 上传技能包(ZIP)
|
|
583
|
+
const handleUploadPackage = async (file) => {
|
|
584
|
+
if (!detailSkill) return false
|
|
585
|
+
if (!file.name.toLowerCase().endsWith('.zip')) {
|
|
586
|
+
message.error('仅支持 .zip 格式')
|
|
587
|
+
return Upload.LIST_IGNORE
|
|
588
|
+
}
|
|
589
|
+
setUploadLoading(true)
|
|
590
|
+
try {
|
|
591
|
+
await skillApi.uploadPackage(detailSkill.skillCode, detailSkill.version || '1.0.0', file)
|
|
592
|
+
message.success('上传成功')
|
|
593
|
+
// 刷新当前详情
|
|
594
|
+
setDetailSkill(prev => prev ? {...prev, packageType: 'ZIP'} : prev)
|
|
595
|
+
// 重新拉取服务端最新数据
|
|
596
|
+
try {
|
|
597
|
+
const res = await skillApi.getBySkillCode(detailSkill.skillCode)
|
|
598
|
+
const data = res && typeof res === 'object' && 'data' in res ? res.data : res
|
|
599
|
+
if (data) setDetailSkill(prev => prev ? {...prev, ...data} : prev)
|
|
600
|
+
} catch (_) {
|
|
601
|
+
}
|
|
602
|
+
// 同步刷新列表(packageType 改变)
|
|
603
|
+
loadSkills(current)
|
|
604
|
+
} catch (e) {
|
|
605
|
+
console.error('uploadPackage error:', e)
|
|
606
|
+
message.error('上传失败:' + (e?.message || '未知错误'))
|
|
607
|
+
} finally {
|
|
608
|
+
setUploadLoading(false)
|
|
609
|
+
}
|
|
610
|
+
return false // 阻止 antd Upload 默认上传
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const handlePublish = async () => {
|
|
614
|
+
try {
|
|
615
|
+
const vals = await pubForm.validateFields()
|
|
616
|
+
setPubLoading(true)
|
|
617
|
+
const res = await skillApi.create({...vals, status: 'PUBLISHED'})
|
|
618
|
+
if (res !== undefined && res !== null) {
|
|
619
|
+
message.success('发布成功')
|
|
620
|
+
setPubModalOpen(false)
|
|
621
|
+
pubForm.resetFields()
|
|
622
|
+
loadSkills(1)
|
|
623
|
+
loadStats()
|
|
624
|
+
} else {
|
|
625
|
+
message.error('发布失败')
|
|
626
|
+
}
|
|
627
|
+
} catch (e) {
|
|
628
|
+
if (e?.errorFields) return
|
|
629
|
+
message.error('发布异常')
|
|
630
|
+
} finally {
|
|
631
|
+
setPubLoading(false)
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// 分类 CRUD 操作
|
|
636
|
+
const openAddCategory = (parent) => {
|
|
637
|
+
setEditingCategory(null)
|
|
638
|
+
setParentForCreate(parent)
|
|
639
|
+
categoryForm.resetFields()
|
|
640
|
+
if (parent) {
|
|
641
|
+
categoryForm.setFieldsValue({parentCode: parent.categoryCode})
|
|
642
|
+
}
|
|
643
|
+
setCatModalVisible(true)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const openEditCategory = (cat) => {
|
|
647
|
+
setEditingCategory(cat)
|
|
648
|
+
setParentForCreate(null)
|
|
649
|
+
categoryForm.setFieldsValue({
|
|
650
|
+
categoryCode: cat.categoryCode,
|
|
651
|
+
categoryName: cat.categoryName,
|
|
652
|
+
parentCode: cat.parentCode || '',
|
|
653
|
+
})
|
|
654
|
+
setCatModalVisible(true)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const handleCategorySubmit = async () => {
|
|
658
|
+
try {
|
|
659
|
+
const values = await categoryForm.validateFields()
|
|
660
|
+
setCatLoading(true)
|
|
661
|
+
if (editingCategory) {
|
|
662
|
+
// API 期望 catCode/catName,前端用 categoryCode/categoryName
|
|
663
|
+
await skillApi.createCategory({
|
|
664
|
+
catCode: values.categoryCode,
|
|
665
|
+
catName: values.categoryName,
|
|
666
|
+
parentCode: values.parentCode || '',
|
|
667
|
+
})
|
|
668
|
+
message.success('更新成功')
|
|
669
|
+
} else {
|
|
670
|
+
await skillApi.createCategory({
|
|
671
|
+
catCode: values.categoryCode,
|
|
672
|
+
catName: values.categoryName,
|
|
673
|
+
parentCode: values.parentCode || '',
|
|
674
|
+
})
|
|
675
|
+
message.success('创建成功')
|
|
676
|
+
}
|
|
677
|
+
setCatModalVisible(false)
|
|
678
|
+
loadCategories()
|
|
679
|
+
} catch (e) {
|
|
680
|
+
if (e?.errorFields) return
|
|
681
|
+
// 回退:本地 mock 操作 + localStorage 持久化
|
|
682
|
+
const values = categoryForm.getFieldsValue()
|
|
683
|
+
let updated = [...categories]
|
|
684
|
+
if (editingCategory) {
|
|
685
|
+
const idx = updated.findIndex(c => c.categoryCode === editingCategory.categoryCode)
|
|
686
|
+
if (idx >= 0) {
|
|
687
|
+
updated[idx] = {...updated[idx], ...values}
|
|
688
|
+
}
|
|
689
|
+
} else {
|
|
690
|
+
updated.push({
|
|
691
|
+
categoryCode: values.categoryCode,
|
|
692
|
+
categoryName: values.categoryName,
|
|
693
|
+
parentCode: values.parentCode || '',
|
|
694
|
+
skillCount: 0,
|
|
695
|
+
})
|
|
696
|
+
}
|
|
697
|
+
setCategories(updated)
|
|
698
|
+
localStorage.setItem('z_skill_categories', JSON.stringify(updated))
|
|
699
|
+
setCatModalVisible(false)
|
|
700
|
+
message.success(editingCategory ? '更新成功' : '创建成功')
|
|
701
|
+
} finally {
|
|
702
|
+
setCatLoading(false)
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const handleCategoryDelete = async (catCode) => {
|
|
707
|
+
try {
|
|
708
|
+
const cat = categories.find(c => c.categoryCode === catCode)
|
|
709
|
+
const apiId = cat?.id
|
|
710
|
+
if (apiId) {
|
|
711
|
+
await skillApi.deleteCategory(apiId)
|
|
712
|
+
} else {
|
|
713
|
+
throw new Error('no api id')
|
|
714
|
+
}
|
|
715
|
+
message.success('删除成功')
|
|
716
|
+
loadCategories()
|
|
717
|
+
} catch (e) {
|
|
718
|
+
// 回退:本地 mock
|
|
719
|
+
const updated = categories.filter(c => c.categoryCode !== catCode)
|
|
720
|
+
setCategories(updated)
|
|
721
|
+
localStorage.setItem('z_skill_categories', JSON.stringify(updated))
|
|
722
|
+
message.success('删除成功')
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const treeNodeTitleRender = (node) => {
|
|
727
|
+
const cat = categories.find(c => c.categoryCode === node.categoryCode)
|
|
728
|
+
if (!cat) return <span>{node.title}</span>
|
|
729
|
+
return (
|
|
730
|
+
<span
|
|
731
|
+
style={{display: 'flex', alignItems: 'center', gap: 4, padding: '2px 0', cursor: 'pointer'}}
|
|
732
|
+
onClick={() => handleCategoryChange(node.categoryCode)}
|
|
733
|
+
>
|
|
734
|
+
{categoryCode === node.categoryCode
|
|
735
|
+
? <FolderOpenOutlined style={{fontSize: 13, color: '#667eea'}}/>
|
|
736
|
+
: <FolderOutlined style={{fontSize: 13, color: '#8c8c8c'}}/>
|
|
737
|
+
}
|
|
738
|
+
<span style={{
|
|
739
|
+
flex: 1,
|
|
740
|
+
fontSize: 13,
|
|
741
|
+
color: categoryCode === node.categoryCode ? '#667eea' : undefined,
|
|
742
|
+
fontWeight: categoryCode === node.categoryCode ? 600 : 400
|
|
743
|
+
}}>
|
|
744
|
+
{cat.categoryName}
|
|
745
|
+
</span>
|
|
746
|
+
<Tag style={{fontSize: 10, padding: '0 4px', lineHeight: '16px', marginRight: 0}}>
|
|
747
|
+
{cat.skillCount || 0}
|
|
748
|
+
</Tag>
|
|
749
|
+
<Dropdown
|
|
750
|
+
trigger={['click']}
|
|
751
|
+
menu={{
|
|
752
|
+
items: [
|
|
753
|
+
{key: 'add', icon: <PlusOutlined/>, label: '新增子分类'},
|
|
754
|
+
{key: 'edit', icon: <EditOutlined/>, label: '编辑分类'},
|
|
755
|
+
{type: 'divider'},
|
|
756
|
+
{key: 'delete', icon: <DeleteOutlined/>, label: '删除分类', danger: true},
|
|
757
|
+
],
|
|
758
|
+
onClick: ({key, domEvent}) => {
|
|
759
|
+
domEvent.stopPropagation()
|
|
760
|
+
if (key === 'add') openAddCategory(cat)
|
|
761
|
+
else if (key === 'edit') openEditCategory(cat)
|
|
762
|
+
else if (key === 'delete') {
|
|
763
|
+
Modal.confirm({
|
|
764
|
+
title: `删除分类"${cat.categoryName}"?`,
|
|
765
|
+
content: '子分类也会被删除',
|
|
766
|
+
okText: '确认删除',
|
|
767
|
+
okType: 'danger',
|
|
768
|
+
cancelText: '取消',
|
|
769
|
+
onOk: () => handleCategoryDelete(node.categoryCode),
|
|
770
|
+
})
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
}}
|
|
774
|
+
>
|
|
775
|
+
<Button type="text" size="small"
|
|
776
|
+
icon={<MoreOutlined style={{fontSize: 13}}/>}
|
|
777
|
+
onClick={e => e.stopPropagation()}/>
|
|
778
|
+
</Dropdown>
|
|
779
|
+
</span>
|
|
780
|
+
)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const sortOptions = [
|
|
784
|
+
{value: 'download_count', label: '最热门'},
|
|
785
|
+
{value: 'gmt_create', label: '最新发布'},
|
|
786
|
+
{value: 'skill_name', label: '名称排序'},
|
|
787
|
+
]
|
|
788
|
+
|
|
789
|
+
const onTreeDividerDown = useCallback((e) => {
|
|
790
|
+
e.preventDefault()
|
|
791
|
+
draggingRef.current = true
|
|
792
|
+
startXRef.current = e.clientX
|
|
793
|
+
startWRef.current = treeWidth
|
|
794
|
+
document.body.style.cursor = 'col-resize'
|
|
795
|
+
document.body.style.userSelect = 'none'
|
|
796
|
+
}, [treeWidth])
|
|
797
|
+
|
|
798
|
+
useEffect(() => {
|
|
799
|
+
const onMove = (e) => {
|
|
800
|
+
if (!draggingRef.current) return
|
|
801
|
+
const nw = Math.max(180, Math.min(startWRef.current + e.clientX - startXRef.current, 340))
|
|
802
|
+
setTreeWidth(nw)
|
|
803
|
+
}
|
|
804
|
+
const onUp = () => {
|
|
805
|
+
if (draggingRef.current) {
|
|
806
|
+
draggingRef.current = false
|
|
807
|
+
document.body.style.cursor = ''
|
|
808
|
+
document.body.style.userSelect = ''
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
window.addEventListener('mousemove', onMove)
|
|
812
|
+
window.addEventListener('mouseup', onUp)
|
|
813
|
+
return () => {
|
|
814
|
+
window.removeEventListener('mousemove', onMove)
|
|
815
|
+
window.removeEventListener('mouseup', onUp)
|
|
816
|
+
}
|
|
817
|
+
}, [])
|
|
818
|
+
|
|
819
|
+
return (
|
|
820
|
+
<div style={{display: 'flex', gap: 16, minHeight: 600}}>
|
|
821
|
+
{/* LEFT: Category Tree (可拖拽) */}
|
|
822
|
+
<div ref={treeRef} style={{width: treeWidth, flexShrink: 0}}>
|
|
823
|
+
<Card size="small"
|
|
824
|
+
title={<span style={{fontWeight: 600}}><AppstoreOutlined
|
|
825
|
+
style={{color: '#667eea', marginRight: 6}}/>分类</span>}
|
|
826
|
+
extra={
|
|
827
|
+
<div style={{display: 'flex', gap: 4}}>
|
|
828
|
+
<Button size="small" icon={<ReloadOutlined/>} onClick={loadCategories}/>
|
|
829
|
+
<Button size="small" icon={<PlusOutlined/>} onClick={() => openAddCategory(null)}/>
|
|
830
|
+
</div>
|
|
831
|
+
}
|
|
832
|
+
styles={{
|
|
833
|
+
header: {borderBottom: '1px solid #f0f0f0', padding: '10px 14px'},
|
|
834
|
+
body: {padding: '6px 8px'}
|
|
835
|
+
}}
|
|
836
|
+
>
|
|
837
|
+
<Tree
|
|
838
|
+
showIcon={false}
|
|
839
|
+
treeData={buildCategoryTreeData(categories)}
|
|
840
|
+
defaultExpandAll
|
|
841
|
+
showLine
|
|
842
|
+
titleRender={treeNodeTitleRender}
|
|
843
|
+
selectedKeys={categoryCode ? [categoryCode] : []}
|
|
844
|
+
onSelect={() => {
|
|
845
|
+
}}
|
|
846
|
+
style={{fontSize: 13}}
|
|
847
|
+
/>
|
|
848
|
+
</Card>
|
|
849
|
+
</div>
|
|
850
|
+
{/* 拖拽分隔线 */}
|
|
851
|
+
<div
|
|
852
|
+
onMouseDown={onTreeDividerDown}
|
|
853
|
+
style={{
|
|
854
|
+
width: 6,
|
|
855
|
+
cursor: 'col-resize',
|
|
856
|
+
flexShrink: 0,
|
|
857
|
+
background: '#f0f0f0',
|
|
858
|
+
borderRadius: 3,
|
|
859
|
+
alignSelf: 'stretch'
|
|
860
|
+
}}
|
|
861
|
+
/>
|
|
862
|
+
|
|
863
|
+
{/* RIGHT: Content */}
|
|
864
|
+
<div style={{flex: 1, display: 'flex', flexDirection: 'column', gap: 12, minWidth: 0}}>
|
|
865
|
+
{/* Toolbar */}
|
|
866
|
+
<div style={{
|
|
867
|
+
display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap',
|
|
868
|
+
background: '#fff', padding: '14px 16px', borderRadius: 10,
|
|
869
|
+
boxShadow: '0 1px 4px rgba(0,0,0,0.04)',
|
|
870
|
+
}}>
|
|
871
|
+
<Input
|
|
872
|
+
prefix={<SearchOutlined style={{color: '#bfbfbf'}}/>}
|
|
873
|
+
placeholder="搜索技能名称、描述、标签..."
|
|
874
|
+
value={keyword}
|
|
875
|
+
onChange={e => setKeyword(e.target.value)}
|
|
876
|
+
onPressEnter={handleKeywordSearch}
|
|
877
|
+
allowClear
|
|
878
|
+
style={{width: 260, borderRadius: 8}}
|
|
879
|
+
/>
|
|
880
|
+
<Select
|
|
881
|
+
value={sortBy}
|
|
882
|
+
onChange={handleSortChange}
|
|
883
|
+
options={sortOptions}
|
|
884
|
+
style={{width: 130, borderRadius: 8}}
|
|
885
|
+
/>
|
|
886
|
+
<div style={{flex: 1}}/>
|
|
887
|
+
|
|
888
|
+
<div style={{display: 'flex', gap: 20, alignItems: 'center'}}>
|
|
889
|
+
<div style={{textAlign: 'center'}}>
|
|
890
|
+
<div style={{
|
|
891
|
+
fontSize: 22,
|
|
892
|
+
fontWeight: 700,
|
|
893
|
+
color: '#667eea',
|
|
894
|
+
lineHeight: 1.2
|
|
895
|
+
}}>{stats.total || 0}</div>
|
|
896
|
+
<div style={{fontSize: 11, color: '#999'}}>全部技能</div>
|
|
897
|
+
</div>
|
|
898
|
+
<div style={{width: 1, height: 32, background: '#f0f0f0'}}/>
|
|
899
|
+
<div style={{textAlign: 'center'}}>
|
|
900
|
+
<div style={{
|
|
901
|
+
fontSize: 22,
|
|
902
|
+
fontWeight: 700,
|
|
903
|
+
color: '#52c41a',
|
|
904
|
+
lineHeight: 1.2
|
|
905
|
+
}}>{stats.published || 0}</div>
|
|
906
|
+
<div style={{fontSize: 11, color: '#999'}}>已发布</div>
|
|
907
|
+
</div>
|
|
908
|
+
</div>
|
|
909
|
+
|
|
910
|
+
<Button
|
|
911
|
+
type="primary"
|
|
912
|
+
icon={<CloudUploadOutlined/>}
|
|
913
|
+
onClick={() => setPubModalOpen(true)}
|
|
914
|
+
style={{
|
|
915
|
+
borderRadius: 8, height: 36, paddingInline: 18,
|
|
916
|
+
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
917
|
+
border: 'none', fontWeight: 600,
|
|
918
|
+
boxShadow: '0 4px 12px rgba(102,126,234,0.3)',
|
|
919
|
+
}}
|
|
920
|
+
>
|
|
921
|
+
发布技能
|
|
922
|
+
</Button>
|
|
923
|
+
</div>
|
|
924
|
+
|
|
925
|
+
{/* Card Grid */}
|
|
926
|
+
{loading ? (
|
|
927
|
+
<div style={{textAlign: 'center', padding: 60}}>
|
|
928
|
+
<Spin size="large"/>
|
|
929
|
+
</div>
|
|
930
|
+
) : skills.length === 0 ? (
|
|
931
|
+
<Empty description="暂无技能" style={{padding: 60}}/>
|
|
932
|
+
) : (
|
|
933
|
+
<Row gutter={[16, 16]}>
|
|
934
|
+
{skills.map(skill => {
|
|
935
|
+
const tags = (skill.tags || '').split(',').filter(Boolean)
|
|
936
|
+
return (
|
|
937
|
+
<Col key={skill.id} xs={24} sm={12} md={8} lg={6}>
|
|
938
|
+
<Card
|
|
939
|
+
hoverable
|
|
940
|
+
size="small"
|
|
941
|
+
onClick={() => openDetail(skill)}
|
|
942
|
+
style={{
|
|
943
|
+
borderRadius: 12, border: 'none',
|
|
944
|
+
boxShadow: '0 2px 12px rgba(0,0,0,0.07)',
|
|
945
|
+
overflow: 'hidden', cursor: 'pointer',
|
|
946
|
+
}}
|
|
947
|
+
styles={{body: {padding: 0}}}
|
|
948
|
+
onMouseEnter={e => {
|
|
949
|
+
e.currentTarget.style.boxShadow = '0 8px 24px rgba(102,126,234,0.18)';
|
|
950
|
+
e.currentTarget.style.transform = 'translateY(-2px)'
|
|
951
|
+
}}
|
|
952
|
+
onMouseLeave={e => {
|
|
953
|
+
e.currentTarget.style.boxShadow = '0 2px 12px rgba(0,0,0,0.07)';
|
|
954
|
+
e.currentTarget.style.transform = 'translateY(0)'
|
|
955
|
+
}}
|
|
956
|
+
>
|
|
957
|
+
{/* Gradient Header */}
|
|
958
|
+
<div style={{
|
|
959
|
+
background: getGradient(skill.categoryCode),
|
|
960
|
+
padding: '14px 16px',
|
|
961
|
+
borderRadius: '12px 12px 0 0',
|
|
962
|
+
}}>
|
|
963
|
+
<div style={{display: 'flex', alignItems: 'center', gap: 10}}>
|
|
964
|
+
<div style={{
|
|
965
|
+
width: 40, height: 40, borderRadius: 10,
|
|
966
|
+
background: 'rgba(255,255,255,0.2)',
|
|
967
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
968
|
+
backdropFilter: 'blur(8px)',
|
|
969
|
+
}}>
|
|
970
|
+
<CodeOutlined style={{fontSize: 20, color: '#fff'}}/>
|
|
971
|
+
</div>
|
|
972
|
+
<div style={{flex: 1, minWidth: 0}}>
|
|
973
|
+
<div style={{
|
|
974
|
+
fontSize: 14,
|
|
975
|
+
fontWeight: 600,
|
|
976
|
+
color: '#fff',
|
|
977
|
+
lineHeight: 1.3
|
|
978
|
+
}}>{skill.skillName}</div>
|
|
979
|
+
<div style={{
|
|
980
|
+
fontSize: 11,
|
|
981
|
+
color: 'rgba(255,255,255,0.8)',
|
|
982
|
+
marginTop: 2
|
|
983
|
+
}}>
|
|
984
|
+
v{skill.version} · {skill.author}
|
|
985
|
+
</div>
|
|
986
|
+
</div>
|
|
987
|
+
{skill.status !== 'PUBLISHED' && (
|
|
988
|
+
<Tag style={{
|
|
989
|
+
background: 'rgba(255,255,255,0.25)',
|
|
990
|
+
color: '#fff',
|
|
991
|
+
border: 'none',
|
|
992
|
+
borderRadius: 10,
|
|
993
|
+
fontSize: 11
|
|
994
|
+
}}>
|
|
995
|
+
{STATUS_LABEL[skill.status]}
|
|
996
|
+
</Tag>
|
|
997
|
+
)}
|
|
998
|
+
</div>
|
|
999
|
+
</div>
|
|
1000
|
+
|
|
1001
|
+
{/* Body */}
|
|
1002
|
+
<div style={{padding: '12px 14px 14px'}}>
|
|
1003
|
+
<Paragraph ellipsis={{rows: 2}} style={{
|
|
1004
|
+
fontSize: 12,
|
|
1005
|
+
color: '#666',
|
|
1006
|
+
marginBottom: 10,
|
|
1007
|
+
minHeight: 36
|
|
1008
|
+
}}>
|
|
1009
|
+
{skill.description || '暂无描述'}
|
|
1010
|
+
</Paragraph>
|
|
1011
|
+
<div style={{display: 'flex', gap: 4, flexWrap: 'wrap', marginBottom: 10}}>
|
|
1012
|
+
{tags.slice(0, 3).map((t, i) => (
|
|
1013
|
+
<Tag key={t} color={TAG_COLORS[i % TAG_COLORS.length]}
|
|
1014
|
+
style={{fontSize: 11, borderRadius: 4, marginBottom: 2}}>
|
|
1015
|
+
{t}
|
|
1016
|
+
</Tag>
|
|
1017
|
+
))}
|
|
1018
|
+
{tags.length > 3 &&
|
|
1019
|
+
<Tag style={{fontSize: 11}}>+{tags.length - 3}</Tag>}
|
|
1020
|
+
</div>
|
|
1021
|
+
<div style={{
|
|
1022
|
+
display: 'flex',
|
|
1023
|
+
alignItems: 'center',
|
|
1024
|
+
justifyContent: 'space-between',
|
|
1025
|
+
paddingTop: 10,
|
|
1026
|
+
borderTop: '1px solid #f5f5f5'
|
|
1027
|
+
}}>
|
|
1028
|
+
<Tooltip title="安装次数">
|
|
1029
|
+
<div style={{display: 'flex', alignItems: 'center', gap: 3}}>
|
|
1030
|
+
<FireOutlined style={{color: '#ff4d4f', fontSize: 12}}/>
|
|
1031
|
+
<Text type="secondary"
|
|
1032
|
+
style={{fontSize: 12}}>{skill.downloadCount || 0}</Text>
|
|
1033
|
+
</div>
|
|
1034
|
+
</Tooltip>
|
|
1035
|
+
<Button
|
|
1036
|
+
type="primary"
|
|
1037
|
+
size="small"
|
|
1038
|
+
icon={<DownloadOutlined/>}
|
|
1039
|
+
onClick={e => {
|
|
1040
|
+
e.stopPropagation();
|
|
1041
|
+
handleInstall(skill.skillCode)
|
|
1042
|
+
}}
|
|
1043
|
+
style={{
|
|
1044
|
+
borderRadius: 6,
|
|
1045
|
+
background: getGradient(skill.categoryCode),
|
|
1046
|
+
border: 'none', fontWeight: 500,
|
|
1047
|
+
}}
|
|
1048
|
+
>
|
|
1049
|
+
安装
|
|
1050
|
+
</Button>
|
|
1051
|
+
</div>
|
|
1052
|
+
</div>
|
|
1053
|
+
</Card>
|
|
1054
|
+
</Col>
|
|
1055
|
+
)
|
|
1056
|
+
})}
|
|
1057
|
+
</Row>
|
|
1058
|
+
)}
|
|
1059
|
+
|
|
1060
|
+
{/* Pagination */}
|
|
1061
|
+
{total > pageSizeRef.current && (
|
|
1062
|
+
<div style={{textAlign: 'right', padding: '8px 0'}}>
|
|
1063
|
+
<Pagination
|
|
1064
|
+
current={current}
|
|
1065
|
+
pageSize={pageSizeRef.current}
|
|
1066
|
+
total={total}
|
|
1067
|
+
onChange={p => {
|
|
1068
|
+
setCurrent(p);
|
|
1069
|
+
window.scrollTo({top: 0, behavior: 'smooth'})
|
|
1070
|
+
}}
|
|
1071
|
+
showSizeChanger={false}
|
|
1072
|
+
/>
|
|
1073
|
+
</div>
|
|
1074
|
+
)}
|
|
1075
|
+
</div>
|
|
1076
|
+
|
|
1077
|
+
{/* Detail Drawer */}
|
|
1078
|
+
<Drawer
|
|
1079
|
+
title={
|
|
1080
|
+
<div style={{display: 'flex', alignItems: 'center', gap: 12}}>
|
|
1081
|
+
<div style={{
|
|
1082
|
+
width: 36, height: 36, borderRadius: 8,
|
|
1083
|
+
background: getGradient(detailSkill?.categoryCode),
|
|
1084
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
1085
|
+
}}>
|
|
1086
|
+
<CodeOutlined style={{fontSize: 18, color: '#fff'}}/>
|
|
1087
|
+
</div>
|
|
1088
|
+
<div>
|
|
1089
|
+
<div style={{fontSize: 15, fontWeight: 600}}>{detailSkill?.skillName}</div>
|
|
1090
|
+
<div style={{fontSize: 11, color: '#999', fontWeight: 400}}>
|
|
1091
|
+
v{detailSkill?.version} · {detailSkill?.author} · {STATUS_LABEL[detailSkill?.status] || detailSkill?.status}
|
|
1092
|
+
</div>
|
|
1093
|
+
</div>
|
|
1094
|
+
</div>
|
|
1095
|
+
}
|
|
1096
|
+
open={drawerOpen}
|
|
1097
|
+
onClose={() => setDrawerOpen(false)}
|
|
1098
|
+
width="75%"
|
|
1099
|
+
styles={{header: {padding: '14px 24px', borderBottom: '1px solid #f0f0f0'}, body: {padding: 0}}}
|
|
1100
|
+
>
|
|
1101
|
+
{detailSkill && (
|
|
1102
|
+
<div style={{height: '100%', display: 'flex', flexDirection: 'column'}}>
|
|
1103
|
+
{/* Top action bar */}
|
|
1104
|
+
<div style={{
|
|
1105
|
+
padding: '16px 24px', borderBottom: '1px solid #f0f0f0',
|
|
1106
|
+
display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap',
|
|
1107
|
+
background: '#fafafa',
|
|
1108
|
+
}}>
|
|
1109
|
+
{/* Download / Copy button */}
|
|
1110
|
+
{detailSkill.packageType === 'ZIP' && detailSkill.packagePath ? (
|
|
1111
|
+
<Button type="primary" size="large" icon={<DownloadOutlined/>}
|
|
1112
|
+
onClick={() => handleDownload(detailSkill)}
|
|
1113
|
+
style={{
|
|
1114
|
+
borderRadius: 8,
|
|
1115
|
+
background: getGradient(detailSkill.categoryCode),
|
|
1116
|
+
border: 'none',
|
|
1117
|
+
fontWeight: 600,
|
|
1118
|
+
height: 40,
|
|
1119
|
+
paddingInline: 24
|
|
1120
|
+
}}>
|
|
1121
|
+
下载技能包 (ZIP)
|
|
1122
|
+
</Button>
|
|
1123
|
+
) : detailSkill.content ? (
|
|
1124
|
+
<Button type="primary" size="large" icon={<CopyOutlined/>}
|
|
1125
|
+
onClick={() => handleCopyContent(detailSkill.content)}
|
|
1126
|
+
style={{
|
|
1127
|
+
borderRadius: 8,
|
|
1128
|
+
background: getGradient(detailSkill.categoryCode),
|
|
1129
|
+
border: 'none',
|
|
1130
|
+
fontWeight: 600,
|
|
1131
|
+
height: 40,
|
|
1132
|
+
paddingInline: 24
|
|
1133
|
+
}}>
|
|
1134
|
+
复制技能内容
|
|
1135
|
+
</Button>
|
|
1136
|
+
) : (
|
|
1137
|
+
<Button size="large" disabled icon={<DownloadOutlined/>} style={{height: 40}}>
|
|
1138
|
+
暂无下载包
|
|
1139
|
+
</Button>
|
|
1140
|
+
)}
|
|
1141
|
+
|
|
1142
|
+
{/* Install button */}
|
|
1143
|
+
<Button icon={<DownloadOutlined/>}
|
|
1144
|
+
onClick={() => handleInstall(detailSkill.skillCode)}
|
|
1145
|
+
style={{borderRadius: 8, height: 40}}>
|
|
1146
|
+
安装此技能
|
|
1147
|
+
</Button>
|
|
1148
|
+
|
|
1149
|
+
{/* Info tags */}
|
|
1150
|
+
<div style={{
|
|
1151
|
+
flex: 1,
|
|
1152
|
+
display: 'flex',
|
|
1153
|
+
gap: 8,
|
|
1154
|
+
flexWrap: 'wrap',
|
|
1155
|
+
justifyContent: 'flex-end'
|
|
1156
|
+
}}>
|
|
1157
|
+
<Tag icon={<CheckCircleOutlined/>} color="blue">v{detailSkill.version}</Tag>
|
|
1158
|
+
<Tag icon={<FireOutlined/>} color="red">{detailSkill.downloadCount || 0} 次安装</Tag>
|
|
1159
|
+
<Tag icon={<StarOutlined/>} color="orange">{detailSkill.author}</Tag>
|
|
1160
|
+
{detailSkill.gmtModified && (
|
|
1161
|
+
<Tag icon={<ClockCircleOutlined/>} color="default">
|
|
1162
|
+
{new Date(detailSkill.gmtModified).toLocaleDateString()}
|
|
1163
|
+
</Tag>
|
|
1164
|
+
)}
|
|
1165
|
+
</div>
|
|
1166
|
+
</div>
|
|
1167
|
+
|
|
1168
|
+
{/* Tab content */}
|
|
1169
|
+
<div style={{flex: 1, overflow: 'auto'}}>
|
|
1170
|
+
<Tabs
|
|
1171
|
+
activeKey={detailTab}
|
|
1172
|
+
onChange={setDetailTab}
|
|
1173
|
+
style={{height: '100%'}}
|
|
1174
|
+
tabBarStyle={{padding: '0 24px', marginBottom: 0}}
|
|
1175
|
+
items={[
|
|
1176
|
+
{
|
|
1177
|
+
key: 'overview',
|
|
1178
|
+
label: <span><InfoCircleOutlined/> 概述</span>,
|
|
1179
|
+
children: (
|
|
1180
|
+
<div style={{padding: '20px 24px'}}>
|
|
1181
|
+
{detailLoading ? (
|
|
1182
|
+
<div style={{textAlign: 'center', padding: 40}}><Spin/></div>
|
|
1183
|
+
) : (
|
|
1184
|
+
<>
|
|
1185
|
+
{/* Description */}
|
|
1186
|
+
<div style={{marginBottom: 20}}>
|
|
1187
|
+
<div style={{
|
|
1188
|
+
fontSize: 14,
|
|
1189
|
+
fontWeight: 600,
|
|
1190
|
+
color: '#333',
|
|
1191
|
+
marginBottom: 8
|
|
1192
|
+
}}>简介
|
|
1193
|
+
</div>
|
|
1194
|
+
<div style={{fontSize: 14, color: '#555', lineHeight: 1.8}}>
|
|
1195
|
+
{detailSkill.description || '暂无描述'}
|
|
1196
|
+
</div>
|
|
1197
|
+
</div>
|
|
1198
|
+
|
|
1199
|
+
{/* Tags */}
|
|
1200
|
+
{detailSkill.tags && (
|
|
1201
|
+
<div style={{marginBottom: 20}}>
|
|
1202
|
+
<div style={{
|
|
1203
|
+
fontSize: 14,
|
|
1204
|
+
fontWeight: 600,
|
|
1205
|
+
color: '#333',
|
|
1206
|
+
marginBottom: 8
|
|
1207
|
+
}}>标签
|
|
1208
|
+
</div>
|
|
1209
|
+
<div
|
|
1210
|
+
style={{display: 'flex', gap: 6, flexWrap: 'wrap'}}>
|
|
1211
|
+
{(detailSkill.tags || '').split(',').filter(Boolean).map((t, i) => (
|
|
1212
|
+
<Tag key={t}
|
|
1213
|
+
color={TAG_COLORS[i % TAG_COLORS.length]}
|
|
1214
|
+
style={{
|
|
1215
|
+
fontSize: 12,
|
|
1216
|
+
borderRadius: 4,
|
|
1217
|
+
padding: '2px 8px'
|
|
1218
|
+
}}>
|
|
1219
|
+
{t}
|
|
1220
|
+
</Tag>
|
|
1221
|
+
))}
|
|
1222
|
+
</div>
|
|
1223
|
+
</div>
|
|
1224
|
+
)}
|
|
1225
|
+
|
|
1226
|
+
{/* Content / README */}
|
|
1227
|
+
{detailContent && (
|
|
1228
|
+
<div>
|
|
1229
|
+
<div style={{
|
|
1230
|
+
fontSize: 14,
|
|
1231
|
+
fontWeight: 600,
|
|
1232
|
+
color: '#333',
|
|
1233
|
+
marginBottom: 8
|
|
1234
|
+
}}>技能说明
|
|
1235
|
+
</div>
|
|
1236
|
+
<div style={{
|
|
1237
|
+
background: '#fafafa', border: '1px solid #f0f0f0',
|
|
1238
|
+
padding: 16, borderRadius: 8, lineHeight: 1.8,
|
|
1239
|
+
fontSize: 14, color: '#333',
|
|
1240
|
+
}}>
|
|
1241
|
+
<ReactMarkdown
|
|
1242
|
+
components={{
|
|
1243
|
+
code: ({
|
|
1244
|
+
node,
|
|
1245
|
+
inline,
|
|
1246
|
+
className,
|
|
1247
|
+
children,
|
|
1248
|
+
...props
|
|
1249
|
+
}) =>
|
|
1250
|
+
inline
|
|
1251
|
+
? <code style={{
|
|
1252
|
+
background: '#f0f0f0',
|
|
1253
|
+
padding: '2px 6px',
|
|
1254
|
+
borderRadius: 4,
|
|
1255
|
+
fontSize: 13
|
|
1256
|
+
}} {...props}>{children}</code>
|
|
1257
|
+
: <pre style={{
|
|
1258
|
+
background: '#1e1e1e',
|
|
1259
|
+
color: '#d4d4d4',
|
|
1260
|
+
padding: 14,
|
|
1261
|
+
borderRadius: 8,
|
|
1262
|
+
overflow: 'auto',
|
|
1263
|
+
fontSize: 13
|
|
1264
|
+
}}><code {...props}>{children}</code></pre>,
|
|
1265
|
+
h1: ({children}) => <h1 style={{
|
|
1266
|
+
fontSize: 20,
|
|
1267
|
+
fontWeight: 700,
|
|
1268
|
+
borderBottom: '1px solid #eee',
|
|
1269
|
+
paddingBottom: 8,
|
|
1270
|
+
marginBottom: 12
|
|
1271
|
+
}}>{children}</h1>,
|
|
1272
|
+
h2: ({children}) => <h2 style={{
|
|
1273
|
+
fontSize: 17,
|
|
1274
|
+
fontWeight: 600,
|
|
1275
|
+
marginTop: 20,
|
|
1276
|
+
marginBottom: 8
|
|
1277
|
+
}}>{children}</h2>,
|
|
1278
|
+
h3: ({children}) => <h3 style={{
|
|
1279
|
+
fontSize: 15,
|
|
1280
|
+
fontWeight: 600,
|
|
1281
|
+
marginTop: 16,
|
|
1282
|
+
marginBottom: 6
|
|
1283
|
+
}}>{children}</h3>,
|
|
1284
|
+
ul: ({children}) => <ul style={{
|
|
1285
|
+
paddingLeft: 20,
|
|
1286
|
+
marginBottom: 12
|
|
1287
|
+
}}>{children}</ul>,
|
|
1288
|
+
li: ({children}) => <li
|
|
1289
|
+
style={{marginBottom: 4}}>{children}</li>,
|
|
1290
|
+
p: ({children}) => <p
|
|
1291
|
+
style={{marginBottom: 10}}>{children}</p>,
|
|
1292
|
+
}}
|
|
1293
|
+
>
|
|
1294
|
+
{detailContent}
|
|
1295
|
+
</ReactMarkdown>
|
|
1296
|
+
</div>
|
|
1297
|
+
</div>
|
|
1298
|
+
)}
|
|
1299
|
+
</>
|
|
1300
|
+
)}
|
|
1301
|
+
</div>
|
|
1302
|
+
),
|
|
1303
|
+
},
|
|
1304
|
+
{
|
|
1305
|
+
key: 'files',
|
|
1306
|
+
label: <span><FileTextOutlined/> 文件</span>,
|
|
1307
|
+
children: (
|
|
1308
|
+
<div style={{padding: '20px 24px'}}>
|
|
1309
|
+
{detailSkill.files && detailSkill.files.length > 0 ? (
|
|
1310
|
+
<div style={{display: 'flex', gap: 16, height: 400}}>
|
|
1311
|
+
{/* File tree */}
|
|
1312
|
+
<div style={{
|
|
1313
|
+
width: 220,
|
|
1314
|
+
flexShrink: 0,
|
|
1315
|
+
border: '1px solid #f0f0f0',
|
|
1316
|
+
borderRadius: 8,
|
|
1317
|
+
overflow: 'auto',
|
|
1318
|
+
padding: '4px 0'
|
|
1319
|
+
}}>
|
|
1320
|
+
<Tree
|
|
1321
|
+
treeData={buildFileTree(detailSkill.files)}
|
|
1322
|
+
defaultExpandAll showIcon
|
|
1323
|
+
selectedKeys={selectedFile ? [selectedFile.path] : []}
|
|
1324
|
+
onSelect={(_, info) => {
|
|
1325
|
+
if (info.node.isLeaf && info.node.file) setSelectedFile(info.node.file)
|
|
1326
|
+
}}
|
|
1327
|
+
titleRender={(n) => <span
|
|
1328
|
+
style={{fontSize: 13}}>{n.title}</span>}
|
|
1329
|
+
icon={(n) => !n.isLeaf
|
|
1330
|
+
? <FolderOutlined
|
|
1331
|
+
style={{fontSize: 13, color: '#faad14'}}/>
|
|
1332
|
+
: <FileTextOutlined
|
|
1333
|
+
style={{fontSize: 13, color: '#8c8c8c'}}/>}
|
|
1334
|
+
/>
|
|
1335
|
+
</div>
|
|
1336
|
+
{/* File preview */}
|
|
1337
|
+
<div
|
|
1338
|
+
style={{flex: 1, display: 'flex', flexDirection: 'column'}}>
|
|
1339
|
+
{selectedFile ? (
|
|
1340
|
+
<>
|
|
1341
|
+
<div style={{
|
|
1342
|
+
fontSize: 12,
|
|
1343
|
+
color: '#999',
|
|
1344
|
+
marginBottom: 6
|
|
1345
|
+
}}>{selectedFile.path}</div>
|
|
1346
|
+
<pre style={{
|
|
1347
|
+
flex: 1,
|
|
1348
|
+
background: '#1e1e1e',
|
|
1349
|
+
color: '#d4d4d4',
|
|
1350
|
+
padding: 14,
|
|
1351
|
+
borderRadius: 8,
|
|
1352
|
+
fontSize: 12,
|
|
1353
|
+
lineHeight: 1.6,
|
|
1354
|
+
overflow: 'auto',
|
|
1355
|
+
whiteSpace: 'pre-wrap',
|
|
1356
|
+
fontFamily: '"SF Mono","Fira Code","Consolas",monospace',
|
|
1357
|
+
margin: 0
|
|
1358
|
+
}}>
|
|
1359
|
+
{selectedFile.content || '(empty)'}
|
|
1360
|
+
</pre>
|
|
1361
|
+
</>
|
|
1362
|
+
) : (
|
|
1363
|
+
<div style={{
|
|
1364
|
+
color: '#999',
|
|
1365
|
+
textAlign: 'center',
|
|
1366
|
+
paddingTop: 80
|
|
1367
|
+
}}>选择一个文件查看内容</div>
|
|
1368
|
+
)}
|
|
1369
|
+
</div>
|
|
1370
|
+
</div>
|
|
1371
|
+
) : detailSkill.packageType === 'ZIP' && detailSkill.packagePath ? (
|
|
1372
|
+
<div style={{
|
|
1373
|
+
background: '#f6ffed', border: '1px solid #b7eb8f',
|
|
1374
|
+
borderRadius: 8, padding: 20, textAlign: 'center',
|
|
1375
|
+
}}>
|
|
1376
|
+
<FileZipOutlined
|
|
1377
|
+
style={{fontSize: 36, color: '#52c41a', marginBottom: 12}}/>
|
|
1378
|
+
<div style={{
|
|
1379
|
+
fontSize: 14,
|
|
1380
|
+
fontWeight: 600,
|
|
1381
|
+
color: '#389e0d',
|
|
1382
|
+
marginBottom: 8
|
|
1383
|
+
}}>ZIP 包已上传
|
|
1384
|
+
</div>
|
|
1385
|
+
<div style={{
|
|
1386
|
+
fontSize: 12,
|
|
1387
|
+
color: '#999',
|
|
1388
|
+
wordBreak: 'break-all'
|
|
1389
|
+
}}>{detailSkill.packagePath}</div>
|
|
1390
|
+
<Button type="link" icon={<DownloadOutlined/>}
|
|
1391
|
+
onClick={() => handleDownload(detailSkill)}
|
|
1392
|
+
style={{marginTop: 12}}>
|
|
1393
|
+
下载并解压查看文件
|
|
1394
|
+
</Button>
|
|
1395
|
+
</div>
|
|
1396
|
+
) : (
|
|
1397
|
+
<div style={{textAlign: 'center', padding: 40, color: '#999'}}>
|
|
1398
|
+
<FileTextOutlined
|
|
1399
|
+
style={{fontSize: 36, color: '#d9d9d9', marginBottom: 12}}/>
|
|
1400
|
+
<div>暂无文件,上传 ZIP 包后可查看文件目录</div>
|
|
1401
|
+
</div>
|
|
1402
|
+
)}
|
|
1403
|
+
|
|
1404
|
+
{/* Upload section for skill author */}
|
|
1405
|
+
<div style={{
|
|
1406
|
+
marginTop: 20,
|
|
1407
|
+
borderTop: '1px solid #f0f0f0',
|
|
1408
|
+
paddingTop: 16
|
|
1409
|
+
}}>
|
|
1410
|
+
<div style={{
|
|
1411
|
+
fontSize: 13,
|
|
1412
|
+
fontWeight: 600,
|
|
1413
|
+
color: '#333',
|
|
1414
|
+
marginBottom: 8
|
|
1415
|
+
}}>上传技能包
|
|
1416
|
+
</div>
|
|
1417
|
+
<Upload accept=".zip" showUploadList={false}
|
|
1418
|
+
beforeUpload={handleUploadPackage} disabled={uploadLoading}>
|
|
1419
|
+
<div style={{
|
|
1420
|
+
border: '2px dashed #d9d9d9',
|
|
1421
|
+
borderRadius: 8,
|
|
1422
|
+
padding: 16,
|
|
1423
|
+
textAlign: 'center',
|
|
1424
|
+
cursor: uploadLoading ? 'not-allowed' : 'pointer',
|
|
1425
|
+
opacity: uploadLoading ? 0.6 : 1,
|
|
1426
|
+
}}>
|
|
1427
|
+
{uploadLoading ? <Spin size="small"/> : <CloudUploadOutlined
|
|
1428
|
+
style={{fontSize: 20, color: '#bfbfbf'}}/>}
|
|
1429
|
+
<div style={{fontSize: 12, color: '#999', marginTop: 6}}>
|
|
1430
|
+
{uploadLoading ? '上传中...' : (detailSkill.packageType === 'ZIP' ? '重新上传 ZIP' : '上传 ZIP 包')}
|
|
1431
|
+
</div>
|
|
1432
|
+
</div>
|
|
1433
|
+
</Upload>
|
|
1434
|
+
</div>
|
|
1435
|
+
</div>
|
|
1436
|
+
),
|
|
1437
|
+
},
|
|
1438
|
+
{
|
|
1439
|
+
key: 'versions',
|
|
1440
|
+
label:
|
|
1441
|
+
<span><HistoryOutlined/> 版本历史 {versions.length > 0 && `(${versions.length})`}</span>,
|
|
1442
|
+
children: (
|
|
1443
|
+
<div style={{padding: '20px 24px'}}>
|
|
1444
|
+
{versions.length > 0 ? (
|
|
1445
|
+
<Timeline
|
|
1446
|
+
items={versions.map((v, i) => ({
|
|
1447
|
+
color: i === 0 ? 'green' : 'gray',
|
|
1448
|
+
children: (
|
|
1449
|
+
<div>
|
|
1450
|
+
<div style={{
|
|
1451
|
+
display: 'flex',
|
|
1452
|
+
alignItems: 'center',
|
|
1453
|
+
gap: 8
|
|
1454
|
+
}}>
|
|
1455
|
+
<Tag color={i === 0 ? 'green' : 'default'}
|
|
1456
|
+
style={{fontWeight: 600}}>
|
|
1457
|
+
{v.version || v.versionNumber || `v${i + 1}`}
|
|
1458
|
+
</Tag>
|
|
1459
|
+
{i === 0 && <Tag color="blue">当前版本</Tag>}
|
|
1460
|
+
</div>
|
|
1461
|
+
{v.changeLog && (
|
|
1462
|
+
<div style={{
|
|
1463
|
+
fontSize: 13,
|
|
1464
|
+
color: '#666',
|
|
1465
|
+
marginTop: 4
|
|
1466
|
+
}}>{v.changeLog}</div>
|
|
1467
|
+
)}
|
|
1468
|
+
<div style={{
|
|
1469
|
+
fontSize: 11,
|
|
1470
|
+
color: '#bbb',
|
|
1471
|
+
marginTop: 2
|
|
1472
|
+
}}>
|
|
1473
|
+
{v.gmtCreate ? new Date(v.gmtCreate).toLocaleString() : ''}
|
|
1474
|
+
</div>
|
|
1475
|
+
</div>
|
|
1476
|
+
),
|
|
1477
|
+
}))}
|
|
1478
|
+
/>
|
|
1479
|
+
) : (
|
|
1480
|
+
<div style={{textAlign: 'center', padding: 40, color: '#999'}}>
|
|
1481
|
+
<HistoryOutlined
|
|
1482
|
+
style={{fontSize: 36, color: '#d9d9d9', marginBottom: 12}}/>
|
|
1483
|
+
<div>暂无版本记录</div>
|
|
1484
|
+
<div style={{fontSize: 12, color: '#bbb', marginTop: 4}}>当前版本:
|
|
1485
|
+
v{detailSkill.version}</div>
|
|
1486
|
+
</div>
|
|
1487
|
+
)}
|
|
1488
|
+
</div>
|
|
1489
|
+
),
|
|
1490
|
+
},
|
|
1491
|
+
]}
|
|
1492
|
+
/>
|
|
1493
|
+
</div>
|
|
1494
|
+
</div>
|
|
1495
|
+
)}
|
|
1496
|
+
</Drawer>
|
|
1497
|
+
|
|
1498
|
+
{/* Publish Modal */}
|
|
1499
|
+
<Modal
|
|
1500
|
+
title={
|
|
1501
|
+
<div style={{display: 'flex', alignItems: 'center', gap: 10}}>
|
|
1502
|
+
<div style={{
|
|
1503
|
+
width: 32, height: 32, borderRadius: 8,
|
|
1504
|
+
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
1505
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
1506
|
+
}}>
|
|
1507
|
+
<CloudUploadOutlined style={{fontSize: 16, color: '#fff'}}/>
|
|
1508
|
+
</div>
|
|
1509
|
+
<span style={{fontSize: 16}}>发布新技能</span>
|
|
1510
|
+
</div>
|
|
1511
|
+
}
|
|
1512
|
+
open={pubModalOpen}
|
|
1513
|
+
onCancel={() => {
|
|
1514
|
+
setPubModalOpen(false);
|
|
1515
|
+
pubForm.resetFields()
|
|
1516
|
+
}}
|
|
1517
|
+
onOk={handlePublish}
|
|
1518
|
+
confirmLoading={pubLoading}
|
|
1519
|
+
okText="确认发布"
|
|
1520
|
+
cancelText="取消"
|
|
1521
|
+
width={580}
|
|
1522
|
+
style={{top: 80}}
|
|
1523
|
+
>
|
|
1524
|
+
<Form form={pubForm} layout="vertical" style={{marginTop: 12}}>
|
|
1525
|
+
<div style={{display: 'flex', gap: 12}}>
|
|
1526
|
+
<Form.Item name="skillCode" label="技能编码"
|
|
1527
|
+
rules={[{required: true, message: '请输入技能编码'}]} style={{flex: 1}}>
|
|
1528
|
+
<Input placeholder="如 java-entity-gen" style={{borderRadius: 8}}/>
|
|
1529
|
+
</Form.Item>
|
|
1530
|
+
<Form.Item name="skillName" label="技能名称"
|
|
1531
|
+
rules={[{required: true, message: '请输入技能名称'}]} style={{flex: 1}}>
|
|
1532
|
+
<Input placeholder="如 Java Entity 生成器" style={{borderRadius: 8}}/>
|
|
1533
|
+
</Form.Item>
|
|
1534
|
+
</div>
|
|
1535
|
+
<div style={{display: 'flex', gap: 12}}>
|
|
1536
|
+
<Form.Item name="categoryCode" label="分类" style={{flex: 1}}>
|
|
1537
|
+
<Input placeholder="如 backend" style={{borderRadius: 8}}/>
|
|
1538
|
+
</Form.Item>
|
|
1539
|
+
<Form.Item name="version" label="版本号" initialValue="1.0.0" rules={[{required: true}]}
|
|
1540
|
+
style={{flex: 1}}>
|
|
1541
|
+
<Input placeholder="1.0.0" style={{borderRadius: 8}}/>
|
|
1542
|
+
</Form.Item>
|
|
1543
|
+
</div>
|
|
1544
|
+
<Form.Item name="description" label="描述">
|
|
1545
|
+
<TextArea rows={3} placeholder="简要描述技能功能..." style={{borderRadius: 8}}/>
|
|
1546
|
+
</Form.Item>
|
|
1547
|
+
<Form.Item name="tags" label="标签">
|
|
1548
|
+
<Input placeholder="用逗号分隔,如 java,entity,generator" style={{borderRadius: 8}}/>
|
|
1549
|
+
</Form.Item>
|
|
1550
|
+
<Form.Item name="author" label="作者">
|
|
1551
|
+
<Input placeholder="技能作者" style={{borderRadius: 8}}/>
|
|
1552
|
+
</Form.Item>
|
|
1553
|
+
</Form>
|
|
1554
|
+
</Modal>
|
|
1555
|
+
|
|
1556
|
+
{/* Category Create/Edit Modal */}
|
|
1557
|
+
<Modal
|
|
1558
|
+
title={editingCategory ? '编辑分类' : parentForCreate ? `新建子分类 - ${parentForCreate.categoryName}` : '新建分类'}
|
|
1559
|
+
open={catModalVisible}
|
|
1560
|
+
onCancel={() => setCatModalVisible(false)}
|
|
1561
|
+
onOk={() => categoryForm.submit()}
|
|
1562
|
+
confirmLoading={catLoading}
|
|
1563
|
+
destroyOnClose
|
|
1564
|
+
okText="确认"
|
|
1565
|
+
cancelText="取消"
|
|
1566
|
+
>
|
|
1567
|
+
<Form form={categoryForm} layout="vertical" onFinish={handleCategorySubmit} style={{marginTop: 8}}>
|
|
1568
|
+
<Form.Item name="categoryCode" label="分类编码"
|
|
1569
|
+
rules={[{required: true, message: '请输入分类编码'}]}>
|
|
1570
|
+
<Input placeholder="如 backend" disabled={!!editingCategory} style={{borderRadius: 8}}/>
|
|
1571
|
+
</Form.Item>
|
|
1572
|
+
<Form.Item name="categoryName" label="分类名称"
|
|
1573
|
+
rules={[{required: true, message: '请输入分类名称'}]}>
|
|
1574
|
+
<Input placeholder="如 后端开发" style={{borderRadius: 8}}/>
|
|
1575
|
+
</Form.Item>
|
|
1576
|
+
<Form.Item name="parentCode" label="父分类">
|
|
1577
|
+
<Select
|
|
1578
|
+
allowClear
|
|
1579
|
+
placeholder="留空为顶级分类"
|
|
1580
|
+
popupMatchSelectWidth={false}
|
|
1581
|
+
>
|
|
1582
|
+
{categories
|
|
1583
|
+
.filter(c => editingCategory ? c.categoryCode !== editingCategory.categoryCode : true)
|
|
1584
|
+
.map(c => (
|
|
1585
|
+
<Select.Option key={c.categoryCode} value={c.categoryCode}>
|
|
1586
|
+
{c.categoryName}
|
|
1587
|
+
</Select.Option>
|
|
1588
|
+
))}
|
|
1589
|
+
</Select>
|
|
1590
|
+
</Form.Item>
|
|
1591
|
+
</Form>
|
|
1592
|
+
</Modal>
|
|
1593
|
+
</div>
|
|
1594
|
+
)
|
|
1595
|
+
}
|