@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.
Files changed (41) hide show
  1. package/README.md +66 -0
  2. package/dist/z-agent-frontend-component.css +1 -0
  3. package/dist/z-agent-frontend-component.es.js +9956 -0
  4. package/dist/z-agent-frontend-component.umd.js +219 -0
  5. package/package.json +77 -0
  6. package/src/api/apiRouter.js +78 -0
  7. package/src/api/index.js +23 -0
  8. package/src/api/request.js +59 -0
  9. package/src/api/routes.js +140 -0
  10. package/src/dev.jsx +80 -0
  11. package/src/index.js +86 -0
  12. package/src/pages/agent/app/index.jsx +2 -0
  13. package/src/pages/agent/editor/AgentAppEditor.jsx +456 -0
  14. package/src/pages/agent/editor/WorkflowEditor.jsx +495 -0
  15. package/src/pages/agent/editor/nodes/index.ts +225 -0
  16. package/src/pages/agent/index.jsx +1379 -0
  17. package/src/pages/agent/share.jsx +512 -0
  18. package/src/pages/ak/AkUsageDrawer.jsx +208 -0
  19. package/src/pages/ak/index.jsx +496 -0
  20. package/src/pages/llm/index.jsx +736 -0
  21. package/src/pages/llm/model/index.jsx +220 -0
  22. package/src/pages/llm/provider/index.jsx +173 -0
  23. package/src/pages/mcp/index.jsx +359 -0
  24. package/src/pages/oss/BucketList.jsx +320 -0
  25. package/src/pages/oss/ObjectBrowser.jsx +409 -0
  26. package/src/pages/product/execute.jsx +608 -0
  27. package/src/pages/product/index.jsx +628 -0
  28. package/src/pages/product/scene.jsx +746 -0
  29. package/src/pages/script/ApiBridgeEditor.jsx +255 -0
  30. package/src/pages/script/CurlImportModal.jsx +263 -0
  31. package/src/pages/script/FieldMappingEditor.jsx +131 -0
  32. package/src/pages/script/OpenApiImportModal.jsx +212 -0
  33. package/src/pages/script/index.jsx +532 -0
  34. package/src/pages/skill/index.jsx +1595 -0
  35. package/src/pages/trace/DebugPlayground.jsx +357 -0
  36. package/src/pages/trace/components/MetricsDashboard.jsx +164 -0
  37. package/src/pages/trace/components/RagFragments.jsx +134 -0
  38. package/src/pages/trace/components/Timeline.jsx +142 -0
  39. package/src/pages/trace/components/ToolCallTree.jsx +116 -0
  40. package/src/pages/trace/index.jsx +13 -0
  41. 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
+ }