@yuku123/z-frontend-common 0.2.9 → 1.0.0

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 (57) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +55 -42
  3. package/dist/index.js +1 -0
  4. package/package.json +49 -44
  5. package/src/components/ErrorBoundary/index.jsx +24 -77
  6. package/src/components/Layout/index.jsx +141 -38
  7. package/src/components/LoginPage/index.jsx +47 -21
  8. package/src/components/StatusTag/index.jsx +14 -12
  9. package/src/index.js +32 -38
  10. package/src/services/request.js +74 -4
  11. package/dist/z-frontend-common.css +0 -1
  12. package/dist/z-frontend-common.es.js +0 -51275
  13. package/dist/z-frontend-common.umd.js +0 -459
  14. package/src/components/Ctc/Layout.jsx +0 -330
  15. package/src/components/Ctc/Layout.module.css +0 -145
  16. package/src/components/Ctc/agentTeam/index.tsx +0 -308
  17. package/src/components/Ctc/login/AuthPage.module.css +0 -26
  18. package/src/components/Ctc/login/AuthPage.tsx +0 -235
  19. package/src/components/Ctc/login/index.less +0 -49
  20. package/src/components/Ctc/login/index.tsx +0 -142
  21. package/src/components/Ctc/uc/UCLogin.tsx +0 -31
  22. package/src/components/Ctc/uc/UCPhoneLogin.tsx +0 -62
  23. package/src/components/Ctc/uc/UCRegister.tsx +0 -37
  24. package/src/components/Ctc/uc/UCResetPassword.tsx +0 -69
  25. package/src/components/Ctc/uc/index.tsx +0 -15
  26. package/src/components/Ctc/userPanel/index.tsx +0 -999
  27. package/src/components/Ctc/webide/index.tsx +0 -272
  28. package/src/components/LowCode/LowCodeModel.jsx +0 -962
  29. package/src/components/LowCode/LowCodePage.jsx +0 -31
  30. package/src/components/LowCode/LowCodeRuntime.jsx +0 -335
  31. package/src/components/LowCode/MaterializePage.jsx +0 -235
  32. package/src/components/LowCode/index.js +0 -1
  33. package/src/components/MockPlatform/CurlImportModal.jsx +0 -362
  34. package/src/components/MockPlatform/EndpointsTab.jsx +0 -509
  35. package/src/components/MockPlatform/EnvironmentsTab.jsx +0 -212
  36. package/src/components/MockPlatform/MockTemplateHelper.jsx +0 -200
  37. package/src/components/MockPlatform/OpenApiImportModal.jsx +0 -305
  38. package/src/components/MockPlatform/RecordingsTab.jsx +0 -397
  39. package/src/components/MockPlatform/RequestLogsTab.jsx +0 -239
  40. package/src/components/MockPlatform/ScenariosTab.jsx +0 -236
  41. package/src/components/MockPlatform/TestCasesTab.jsx +0 -462
  42. package/src/components/MockPlatform/index.jsx +0 -127
  43. package/src/components/Overview.jsx +0 -74
  44. package/src/dev.jsx +0 -28
  45. package/src/services/agentTeam.js +0 -7
  46. package/src/services/api.js +0 -84
  47. package/src/services/ctcAc.js +0 -7
  48. package/src/services/ctcAcDomain.js +0 -7
  49. package/src/services/ctcAuthorization.js +0 -7
  50. package/src/services/ctcSurl.js +0 -7
  51. package/src/services/ctcUser.js +0 -7
  52. package/src/services/job.js +0 -7
  53. package/src/services/metaApp.js +0 -7
  54. package/src/services/privateConfig.js +0 -7
  55. package/src/services/webide.js +0 -6
  56. package/src/services/workspace.js +0 -21
  57. package/src/utils/request.ts +0 -102
@@ -1,999 +0,0 @@
1
- import {useEffect, useRef, useState} from 'react'
2
- import {Avatar, Badge, Button, Empty, Form, Input, Modal, Popconfirm, Space, Spin, Tag, Tooltip, Tree} from 'antd'
3
- import AntApp from 'antd/es/app'
4
- import type {DataNode} from 'antd/es/tree'
5
- import {
6
- CheckCircleOutlined,
7
- DeleteOutlined,
8
- FileOutlined,
9
- FileTextOutlined,
10
- FolderOpenOutlined,
11
- FolderOutlined,
12
- MessageOutlined,
13
- PlusOutlined,
14
- ReloadOutlined,
15
- SendOutlined,
16
- TeamOutlined,
17
- } from '@ant-design/icons'
18
- import {workspaceApi, workItemApi, type Workspace, type FileNode, type ConversationWorkItem} from '@/services/workspace'
19
- import {agentTeamApi} from '@/services/agentTeam'
20
-
21
- /**
22
- * 用户面板 · 工作项 + 工作空间 + IM 群聊 三合一页面
23
- *
24
- * 布局 (WeChat 风格):
25
- * ┌──────────────┬─────────────────┬──────────────────────────────┐
26
- * │ 工作项列表 │ 工作空间文件树 │ IM 群聊面板 │
27
- * │ │ │ │
28
- * │ + 工作项 │ (点工作项展开) │ 头部: 工作项标题 / 状态 │
29
- * │ + 工作空间 │ FileTree │ 消息流: 用户 / Agent 气泡 │
30
- * │ │ │ 输入框 + 发送 │
31
- * └──────────────┴─────────────────┴──────────────────────────────┘
32
- *
33
- * 数据流:
34
- * - 工作项 (Conv) 一对多 工作空间 (Workspace)
35
- * - 工作空间 1:1 映射本地目录 (localPath)
36
- * - IM 消息走 z-agent-team WebSocket (/api/agent/team/ws?convCode=xxx)
37
- */
38
-
39
- interface ChatMessage {
40
- msgCode: string
41
- senderType: 'USER' | 'AGENT' | 'SYSTEM' | string
42
- senderCode: string
43
- senderName: string
44
- content: string
45
- timestamp: number
46
- messageType?: string
47
- }
48
-
49
- const STATUS_COLOR: Record<string, string> = {
50
- OPEN: 'green',
51
- DONE: 'blue',
52
- ARCHIVED: 'default',
53
- }
54
-
55
- export default function UserPanel() {
56
- const {message} = AntApp.useApp()
57
- const userInfoStr = localStorage.getItem('userInfo') || '{}'
58
- let userInfo: any = {}
59
- try {
60
- userInfo = JSON.parse(userInfoStr)
61
- } catch {
62
- }
63
- const currentUserId: string = userInfo?.userId ? String(userInfo.userId) : 'anonymous'
64
- const currentUserName: string = userInfo?.userName || userInfo?.name || '我'
65
-
66
- // ===== 工作项 (Conv) 列表 =====
67
- const [workItems, setWorkItems] = useState<ConversationWorkItem[]>([])
68
- const [loadingItems, setLoadingItems] = useState(false)
69
- const [selectedItem, setSelectedItem] = useState<ConversationWorkItem | null>(null)
70
-
71
- // ===== 工作空间 (Workspace) 列表 =====
72
- const [workspaces, setWorkspaces] = useState<Workspace[]>([])
73
- const [loadingWs, setLoadingWs] = useState(false)
74
-
75
- // ===== 当前工作项的文件树 =====
76
- const [fileTree, setFileTree] = useState<DataNode[]>([])
77
- const [loadingTree, setLoadingTree] = useState(false)
78
-
79
- // ===== 当前选中文件内容预览 =====
80
- const [previewFile, setPreviewFile] = useState<{path: string; content: string} | null>(null)
81
- const [loadingPreview, setLoadingPreview] = useState(false)
82
-
83
- // ===== IM 聊天 =====
84
- const [messages, setMessages] = useState<ChatMessage[]>([])
85
- const [chatInput, setChatInput] = useState('')
86
- const [wsState, setWsState] = useState<'disconnected' | 'connecting' | 'connected' | 'error'>('disconnected')
87
- const wsRef = useRef<WebSocket | null>(null)
88
- const messagesEndRef = useRef<HTMLDivElement | null>(null)
89
- const [streamingText, setStreamingText] = useState('')
90
- const [streamingFrom, setStreamingFrom] = useState('')
91
-
92
- // ===== UI 状态 =====
93
- const [createWsOpen, setCreateWsOpen] = useState(false)
94
- const [createItemOpen, setCreateItemOpen] = useState(false)
95
- const [wsForm] = Form.useForm()
96
- const [itemForm] = Form.useForm()
97
- const [activeWsCode, setActiveWsCode] = useState<string | null>(null) // 当前左侧选中的 workspace (用于切换文件树)
98
-
99
- // ===== 默认 Team code (用于新建工作项) =====
100
- const [defaultTeamCode, setDefaultTeamCode] = useState<string>('z-one-company-default')
101
-
102
- useEffect(() => {
103
- loadTeams()
104
- loadWorkspaces()
105
- loadWorkItems()
106
- }, [])
107
-
108
- useEffect(() => {
109
- // 自动滚动到消息底部
110
- if (messagesEndRef.current) {
111
- messagesEndRef.current.scrollIntoView({behavior: 'smooth'})
112
- }
113
- }, [messages, streamingText])
114
-
115
- // ===== 数据加载 =====
116
-
117
- const loadTeams = async () => {
118
- try {
119
- const list = await agentTeamApi.listTeams()
120
- if (list && list.length > 0) {
121
- setDefaultTeamCode(list[0].teamCode)
122
- }
123
- } catch {
124
- // ignore
125
- }
126
- }
127
-
128
- const loadWorkspaces = async () => {
129
- setLoadingWs(true)
130
- try {
131
- const list = await workspaceApi.list(currentUserId)
132
- setWorkspaces(list || [])
133
- } catch (e: any) {
134
- // 容错: 后端未启动时显示空列表
135
- setWorkspaces([])
136
- } finally {
137
- setLoadingWs(false)
138
- }
139
- }
140
-
141
- const loadWorkItems = async () => {
142
- setLoadingItems(true)
143
- try {
144
- const list = await workItemApi.listAllByOwner(currentUserId, 100)
145
- setWorkItems(list || [])
146
- } catch (e: any) {
147
- setWorkItems([])
148
- } finally {
149
- setLoadingItems(false)
150
- }
151
- }
152
-
153
- const loadFileTree = async (workspaceCode: string) => {
154
- if (!workspaceCode) {
155
- setFileTree([])
156
- return
157
- }
158
- setLoadingTree(true)
159
- try {
160
- const tree = await workspaceApi.fileTree(workspaceCode)
161
- setFileTree(tree ? [buildAntdTreeNode(tree)] : [])
162
- } catch (e: any) {
163
- message.error('加载文件树失败:' + (e?.message || '未知错误'))
164
- setFileTree([])
165
- } finally {
166
- setLoadingTree(false)
167
- }
168
- }
169
-
170
- // ===== IM 历史消息 =====
171
-
172
- const loadMessages = async (convCode: string) => {
173
- try {
174
- const list = await workItemApi.messages(convCode, 200)
175
- const mapped: ChatMessage[] = (list || []).map((m: any) => ({
176
- msgCode: m.msgCode,
177
- senderType: m.senderType,
178
- senderCode: m.senderCode,
179
- senderName: m.senderName,
180
- content: m.content,
181
- messageType: m.messageType,
182
- timestamp: m.gmtCreate ? new Date(m.gmtCreate).getTime() : Date.now(),
183
- }))
184
- setMessages(mapped)
185
- } catch (e: any) {
186
- setMessages([])
187
- }
188
- }
189
-
190
- // ===== WebSocket =====
191
-
192
- const connectWs = (convCode: string) => {
193
- disconnectWs()
194
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
195
- const url = `${protocol}//${window.location.host}/api/agent/team/ws?convCode=${encodeURIComponent(convCode)}&userId=${encodeURIComponent(currentUserId)}&userName=${encodeURIComponent(currentUserName)}`
196
- setWsState('connecting')
197
- try {
198
- const ws = new WebSocket(url)
199
- ws.onopen = () => {
200
- setWsState('connected')
201
- }
202
- ws.onmessage = (ev) => {
203
- try {
204
- const event = JSON.parse(ev.data)
205
- handleStreamEvent(event)
206
- } catch {
207
- // ignore
208
- }
209
- }
210
- ws.onerror = () => {
211
- setWsState('error')
212
- }
213
- ws.onclose = () => {
214
- setWsState('disconnected')
215
- wsRef.current = null
216
- }
217
- wsRef.current = ws
218
- } catch (e: any) {
219
- setWsState('error')
220
- message.error('WS 连接失败:' + (e?.message || '未知错误'))
221
- }
222
- }
223
-
224
- const disconnectWs = () => {
225
- if (wsRef.current) {
226
- try {
227
- wsRef.current.close()
228
- } catch {
229
- }
230
- wsRef.current = null
231
- setWsState('disconnected')
232
- setStreamingText('')
233
- }
234
- }
235
-
236
- const handleStreamEvent = (event: any) => {
237
- const t = event.type
238
- if (t === 'AGENT_RESPONSE_DELTA' || t === 'AGENT_THINKING_DELTA') {
239
- setStreamingFrom(event.senderName || event.senderCode || 'Agent')
240
- setStreamingText((prev) => prev + (event.content || ''))
241
- } else if (t === 'AGENT_RESPONSE' || t === 'AGENT_THINKING') {
242
- setMessages((prev) => [
243
- ...prev,
244
- {
245
- msgCode: event.eventId || `evt-${Date.now()}`,
246
- senderType: event.senderType || 'AGENT',
247
- senderCode: event.senderCode || '',
248
- senderName: event.senderName || 'Agent',
249
- content: event.content || '',
250
- messageType: t,
251
- timestamp: event.timestamp || Date.now(),
252
- },
253
- ])
254
- setStreamingText('')
255
- setStreamingFrom('')
256
- } else if (t === 'SYSTEM' || t === 'PARTICIPANT_JOIN' || t === 'PARTICIPANT_LEAVE') {
257
- // 系统消息折叠进消息流 (可选)
258
- if (event.content && event.content !== 'HEARTBEAT_ACK' && event.content !== 'PONG') {
259
- setMessages((prev) => [
260
- ...prev,
261
- {
262
- msgCode: event.eventId || `evt-${Date.now()}`,
263
- senderType: 'SYSTEM',
264
- senderCode: event.senderCode || 'system',
265
- senderName: '系统',
266
- content: event.content,
267
- messageType: t,
268
- timestamp: event.timestamp || Date.now(),
269
- },
270
- ])
271
- }
272
- } else if (t === 'ERROR') {
273
- setMessages((prev) => [
274
- ...prev,
275
- {
276
- msgCode: event.eventId || `err-${Date.now()}`,
277
- senderType: 'SYSTEM',
278
- senderCode: 'system',
279
- senderName: '错误',
280
- content: event.content || '未知错误',
281
- messageType: t,
282
- timestamp: event.timestamp || Date.now(),
283
- },
284
- ])
285
- setStreamingText('')
286
- setStreamingFrom('')
287
- } else if (t === 'MESSAGE_END') {
288
- setStreamingText('')
289
- setStreamingFrom('')
290
- }
291
- // 其他类型 (TOOL_CALL / DELEGATION_* / TYPING / REACTION / READ_RECEIPT / PRESENCE) 暂不展示
292
- }
293
-
294
- const sendMessage = () => {
295
- const text = chatInput.trim()
296
- if (!text) return
297
- if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
298
- message.warning('IM 未连接')
299
- return
300
- }
301
- if (!selectedItem) {
302
- message.warning('请先选择一个工作项')
303
- return
304
- }
305
- const payload = JSON.stringify({type: 'USER_MESSAGE', content: text})
306
- wsRef.current.send(payload)
307
- // 立即在本地展示
308
- setMessages((prev) => [
309
- ...prev,
310
- {
311
- msgCode: `local-${Date.now()}`,
312
- senderType: 'USER',
313
- senderCode: currentUserId,
314
- senderName: currentUserName,
315
- content: text,
316
- messageType: 'TEXT',
317
- timestamp: Date.now(),
318
- },
319
- ])
320
- setChatInput('')
321
- }
322
-
323
- // ===== 工作项选择 =====
324
-
325
- const handleSelectItem = (item: ConversationWorkItem) => {
326
- setSelectedItem(item)
327
- loadMessages(item.convCode)
328
- connectWs(item.convCode)
329
- // 自动加载绑定的 workspace 文件树
330
- if (item.workspaceCode) {
331
- setActiveWsCode(item.workspaceCode)
332
- loadFileTree(item.workspaceCode)
333
- } else {
334
- setActiveWsCode(null)
335
- setFileTree([])
336
- }
337
- }
338
-
339
- // ===== 创建工作项 =====
340
-
341
- const handleCreateItem = async () => {
342
- try {
343
- const values = await itemForm.validateFields()
344
- const created = await workItemApi.create({
345
- teamCode: defaultTeamCode,
346
- userId: currentUserId,
347
- userName: currentUserName,
348
- title: values.title,
349
- workspaceCode: values.workspaceCode || undefined,
350
- })
351
- message.success('工作项已创建')
352
- setCreateItemOpen(false)
353
- itemForm.resetFields()
354
- await loadWorkItems()
355
- if (created) {
356
- handleSelectItem(created)
357
- }
358
- } catch (e: any) {
359
- message.error('创建失败:' + (e?.message || '未知错误'))
360
- }
361
- }
362
-
363
- // ===== 创建工作空间 =====
364
-
365
- const handleCreateWorkspace = async () => {
366
- try {
367
- const values = await wsForm.validateFields()
368
- await workspaceApi.create({
369
- workspaceName: values.workspaceName,
370
- localPath: values.localPath,
371
- ownerId: currentUserId,
372
- ownerName: currentUserName,
373
- description: values.description,
374
- })
375
- message.success('工作空间已创建')
376
- setCreateWsOpen(false)
377
- wsForm.resetFields()
378
- await loadWorkspaces()
379
- } catch (e: any) {
380
- const errMsg = e?.response?.data?.message || e?.message || '未知错误'
381
- message.error('创建失败:' + errMsg)
382
- }
383
- }
384
-
385
- // ===== 文件点击预览 =====
386
-
387
- const handleFileClick = async (absPath: string) => {
388
- setLoadingPreview(true)
389
- try {
390
- const resp = await workspaceApi.fileContent(absPath)
391
- setPreviewFile({path: absPath, content: resp?.content || ''})
392
- } catch (e: any) {
393
- message.error('读取文件失败:' + (e?.message || '未知错误'))
394
- } finally {
395
- setLoadingPreview(false)
396
- }
397
- }
398
-
399
- // ===== 关闭工作项 =====
400
-
401
- const handleCloseItem = async (convCode: string) => {
402
- try {
403
- await workItemApi.close(convCode, 'DONE')
404
- message.success('已关闭工作项')
405
- await loadWorkItems()
406
- if (selectedItem?.convCode === convCode) {
407
- setSelectedItem(null)
408
- disconnectWs()
409
- setMessages([])
410
- setFileTree([])
411
- }
412
- } catch (e: any) {
413
- message.error('关闭失败:' + (e?.message || '未知错误'))
414
- }
415
- }
416
-
417
- // ===== 删除工作空间 =====
418
-
419
- const handleDeleteWs = async (workspaceCode: string) => {
420
- try {
421
- await workspaceApi.delete(workspaceCode)
422
- message.success('已删除')
423
- await loadWorkspaces()
424
- if (activeWsCode === workspaceCode) {
425
- setActiveWsCode(null)
426
- setFileTree([])
427
- }
428
- } catch (e: any) {
429
- message.error('删除失败:' + (e?.message || '未知错误'))
430
- }
431
- }
432
-
433
- // ===== Tree helpers =====
434
-
435
- const buildAntdTreeNode = (node: FileNode): DataNode => {
436
- return {
437
- key: node.absolutePath,
438
- title: (
439
- <span>
440
- {node.isDirectory ? <FolderOutlined style={{marginRight: 4, color: '#faad14'}}/> :
441
- <FileOutlined style={{marginRight: 4, color: '#8c8c8c'}}/>}
442
- {node.name}
443
- {node.isDirectory && node.totalDescendants > 0 ? (
444
- <span style={{color: '#bfbfbf', marginLeft: 6, fontSize: 11}}>({node.totalDescendants})</span>
445
- ) : null}
446
- </span>
447
- ),
448
- children: node.children?.map(buildAntdTreeNode) || [],
449
- isLeaf: !node.isDirectory,
450
- disabled: false,
451
- }
452
- }
453
-
454
- // ===== Render =====
455
-
456
- return (
457
- <div style={{
458
- display: 'grid',
459
- gridTemplateColumns: '240px 280px 1fr',
460
- height: 'calc(100vh - 130px)',
461
- background: '#fff',
462
- border: '1px solid #f0f0f0',
463
- borderRadius: 8,
464
- overflow: 'hidden',
465
- }}>
466
- {/* ===== 第 1 列: 工作项列表 ===== */}
467
- <div style={{
468
- borderRight: '1px solid #f0f0f0',
469
- display: 'flex',
470
- flexDirection: 'column',
471
- background: '#fafafa',
472
- }}>
473
- <div style={{
474
- padding: 12,
475
- borderBottom: '1px solid #f0f0f0',
476
- display: 'flex',
477
- alignItems: 'center',
478
- justifyContent: 'space-between',
479
- }}>
480
- <Space size={4}>
481
- <MessageOutlined style={{color: '#1677ff'}}/>
482
- <strong>工作项</strong>
483
- </Space>
484
- <Space size={4}>
485
- <Tooltip title="刷新">
486
- <Button size="small" type="text" icon={<ReloadOutlined/>} onClick={loadWorkItems}/>
487
- </Tooltip>
488
- <Tooltip title="新建工作项">
489
- <Button size="small" type="text" icon={<PlusOutlined/>}
490
- onClick={() => setCreateItemOpen(true)}/>
491
- </Tooltip>
492
- </Space>
493
- </div>
494
- <div style={{flex: 1, overflow: 'auto'}}>
495
- {loadingItems ? (
496
- <div style={{padding: 24, textAlign: 'center'}}><Spin size="small"/></div>
497
- ) : workItems.length === 0 ? (
498
- <Empty
499
- image={Empty.PRESENTED_IMAGE_SIMPLE}
500
- description="暂无工作项"
501
- style={{padding: 24, marginTop: 24}}
502
- >
503
- <Button size="small" type="primary" onClick={() => setCreateItemOpen(true)}>
504
- 新建工作项
505
- </Button>
506
- </Empty>
507
- ) : (
508
- workItems.map((item) => (
509
- <div
510
- key={item.convCode}
511
- onClick={() => handleSelectItem(item)}
512
- style={{
513
- padding: '10px 12px',
514
- cursor: 'pointer',
515
- borderBottom: '1px solid #f5f5f5',
516
- background: selectedItem?.convCode === item.convCode ? '#e6f7ff' : 'transparent',
517
- transition: 'background 0.2s',
518
- }}
519
- >
520
- <div style={{
521
- display: 'flex',
522
- alignItems: 'center',
523
- justifyContent: 'space-between',
524
- marginBottom: 4,
525
- }}>
526
- <strong style={{
527
- fontSize: 13,
528
- overflow: 'hidden',
529
- textOverflow: 'ellipsis',
530
- whiteSpace: 'nowrap',
531
- maxWidth: 150,
532
- }}>
533
- {item.title || '未命名'}
534
- </strong>
535
- <Badge count={item.unreadCount || 0} size="small"/>
536
- </div>
537
- <div style={{
538
- fontSize: 11,
539
- color: '#8c8c8c',
540
- overflow: 'hidden',
541
- textOverflow: 'ellipsis',
542
- whiteSpace: 'nowrap',
543
- }}>
544
- {item.lastMsgPreview || '暂无消息'}
545
- </div>
546
- <div style={{marginTop: 4, fontSize: 11, color: '#bfbfbf'}}>
547
- <Tag color={STATUS_COLOR[item.status || 'OPEN']} style={{marginRight: 4, fontSize: 10, padding: '0 4px'}}>
548
- {item.status || 'OPEN'}
549
- </Tag>
550
- {item.workspaceCode ? <Tag color="purple" style={{fontSize: 10, padding: '0 4px'}}>有工作空间</Tag> : null}
551
- </div>
552
- </div>
553
- ))
554
- )}
555
- </div>
556
- </div>
557
-
558
- {/* ===== 第 2 列: 工作空间文件树 ===== */}
559
- <div style={{
560
- borderRight: '1px solid #f0f0f0',
561
- display: 'flex',
562
- flexDirection: 'column',
563
- background: '#fff',
564
- }}>
565
- <div style={{
566
- padding: 12,
567
- borderBottom: '1px solid #f0f0f0',
568
- display: 'flex',
569
- alignItems: 'center',
570
- justifyContent: 'space-between',
571
- }}>
572
- <Space size={4}>
573
- <FolderOpenOutlined style={{color: '#faad14'}}/>
574
- <strong>工作空间</strong>
575
- </Space>
576
- <Space size={4}>
577
- <Tooltip title="新建工作空间">
578
- <Button size="small" type="text" icon={<PlusOutlined/>}
579
- onClick={() => setCreateWsOpen(true)}/>
580
- </Tooltip>
581
- </Space>
582
- </div>
583
-
584
- {/* 当前工作项绑定的 workspace */}
585
- {selectedItem?.workspaceCode ? (
586
- <div style={{
587
- padding: '8px 12px',
588
- background: '#f0f5ff',
589
- borderBottom: '1px solid #f0f0f0',
590
- fontSize: 12,
591
- }}>
592
- <Space size={4}>
593
- <Tag color="blue">当前工作项</Tag>
594
- <span style={{color: '#1677ff'}}>
595
- {workspaces.find(w => w.workspaceCode === selectedItem.workspaceCode)?.workspaceName || selectedItem.workspaceCode}
596
- </span>
597
- <Button
598
- size="small"
599
- type="text"
600
- icon={<ReloadOutlined/>}
601
- onClick={() => loadFileTree(selectedItem.workspaceCode!)}
602
- />
603
- </Space>
604
- </div>
605
- ) : null}
606
-
607
- {/* 全部 workspace 列表 (可点击切换) */}
608
- <div style={{
609
- maxHeight: 200,
610
- overflow: 'auto',
611
- borderBottom: '1px solid #f0f0f0',
612
- }}>
613
- {loadingWs ? (
614
- <div style={{padding: 12, textAlign: 'center'}}><Spin size="small"/></div>
615
- ) : workspaces.length === 0 ? (
616
- <div style={{padding: 12, color: '#8c8c8c', fontSize: 12, textAlign: 'center'}}>
617
- 暂无工作空间
618
- </div>
619
- ) : (
620
- workspaces.map((ws) => (
621
- <div
622
- key={ws.workspaceCode}
623
- onClick={() => {
624
- setActiveWsCode(ws.workspaceCode)
625
- loadFileTree(ws.workspaceCode)
626
- }}
627
- style={{
628
- padding: '6px 12px',
629
- cursor: 'pointer',
630
- fontSize: 12,
631
- background: activeWsCode === ws.workspaceCode ? '#e6f7ff' : 'transparent',
632
- borderLeft: activeWsCode === ws.workspaceCode ? '3px solid #1677ff' : '3px solid transparent',
633
- display: 'flex',
634
- alignItems: 'center',
635
- justifyContent: 'space-between',
636
- }}
637
- >
638
- <Space size={4} style={{overflow: 'hidden'}}>
639
- <FolderOutlined style={{color: '#faad14', fontSize: 12}}/>
640
- <span style={{
641
- overflow: 'hidden',
642
- textOverflow: 'ellipsis',
643
- whiteSpace: 'nowrap',
644
- }}>
645
- {ws.workspaceName}
646
- </span>
647
- </Space>
648
- <Popconfirm
649
- title="删除此工作空间?"
650
- onConfirm={(e) => {
651
- e?.stopPropagation()
652
- handleDeleteWs(ws.workspaceCode)
653
- }}
654
- onCancel={(e) => e?.stopPropagation()}
655
- >
656
- <Button
657
- size="small"
658
- type="text"
659
- danger
660
- icon={<DeleteOutlined/>}
661
- onClick={(e) => e.stopPropagation()}
662
- />
663
- </Popconfirm>
664
- </div>
665
- ))
666
- )}
667
- </div>
668
-
669
- {/* 文件树 */}
670
- <div style={{flex: 1, overflow: 'auto', padding: 8}}>
671
- {loadingTree ? (
672
- <div style={{padding: 24, textAlign: 'center'}}><Spin/></div>
673
- ) : !activeWsCode ? (
674
- <Empty
675
- image={Empty.PRESENTED_IMAGE_SIMPLE}
676
- description="请选择工作空间"
677
- style={{marginTop: 40}}
678
- />
679
- ) : fileTree.length === 0 ? (
680
- <Empty
681
- image={Empty.PRESENTED_IMAGE_SIMPLE}
682
- description="空目录或无文件"
683
- style={{marginTop: 40}}
684
- />
685
- ) : (
686
- <Tree
687
- treeData={fileTree}
688
- defaultExpandAll={false}
689
- showLine
690
- showIcon={false}
691
- onSelect={(keys, info) => {
692
- const node = info.node
693
- if (!node.isLeaf && keys.length > 0) {
694
- // 目录: 仅展开
695
- return
696
- }
697
- if (node.isLeaf && info.node.key) {
698
- handleFileClick(String(info.node.key))
699
- }
700
- }}
701
- />
702
- )}
703
- </div>
704
-
705
- {/* 选中的文件预览 */}
706
- {previewFile ? (
707
- <div style={{
708
- borderTop: '1px solid #f0f0f0',
709
- maxHeight: 240,
710
- overflow: 'hidden',
711
- display: 'flex',
712
- flexDirection: 'column',
713
- }}>
714
- <div style={{
715
- padding: '6px 12px',
716
- background: '#fafafa',
717
- borderBottom: '1px solid #f0f0f0',
718
- display: 'flex',
719
- alignItems: 'center',
720
- justifyContent: 'space-between',
721
- fontSize: 11,
722
- }}>
723
- <Space size={4}>
724
- <FileTextOutlined/>
725
- <span style={{
726
- overflow: 'hidden',
727
- textOverflow: 'ellipsis',
728
- whiteSpace: 'nowrap',
729
- maxWidth: 180,
730
- }}>
731
- {previewFile.path.split('/').pop()}
732
- </span>
733
- </Space>
734
- <Button size="small" type="text" onClick={() => setPreviewFile(null)}>关闭</Button>
735
- </div>
736
- <pre style={{
737
- flex: 1,
738
- margin: 0,
739
- padding: 8,
740
- overflow: 'auto',
741
- fontSize: 11,
742
- background: '#1e1e1e',
743
- color: '#d4d4d4',
744
- fontFamily: 'Menlo, monospace',
745
- whiteSpace: 'pre-wrap',
746
- wordBreak: 'break-all',
747
- }}>
748
- {loadingPreview ? '加载中...' : previewFile.content || '(空)'}
749
- </pre>
750
- </div>
751
- ) : null}
752
- </div>
753
-
754
- {/* ===== 第 3 列: IM 群聊面板 ===== */}
755
- <div style={{display: 'flex', flexDirection: 'column', background: '#f5f5f5'}}>
756
- {/* 头部 */}
757
- <div style={{
758
- padding: '12px 16px',
759
- background: '#fff',
760
- borderBottom: '1px solid #f0f0f0',
761
- display: 'flex',
762
- alignItems: 'center',
763
- justifyContent: 'space-between',
764
- }}>
765
- {selectedItem ? (
766
- <Space size={8}>
767
- <TeamOutlined style={{color: '#1677ff'}}/>
768
- <strong style={{fontSize: 14}}>{selectedItem.title || '未命名'}</strong>
769
- <Tag color={STATUS_COLOR[selectedItem.status || 'OPEN']} style={{marginLeft: 4}}>
770
- {selectedItem.status || 'OPEN'}
771
- </Tag>
772
- {selectedItem.workspaceCode ? (
773
- <Tag color="purple" icon={<FolderOpenOutlined/>}>
774
- {workspaces.find(w => w.workspaceCode === selectedItem.workspaceCode)?.workspaceName || '工作空间'}
775
- </Tag>
776
- ) : null}
777
- <span style={{color: '#8c8c8c', fontSize: 12}}>
778
- {selectedItem.messageCount || 0} 条消息
779
- </span>
780
- </Space>
781
- ) : (
782
- <span style={{color: '#8c8c8c'}}>请从左侧选择一个工作项</span>
783
- )}
784
- {selectedItem ? (
785
- <Space size={4}>
786
- <Tag color={wsState === 'connected' ? 'green' : wsState === 'error' ? 'red' : 'default'}>
787
- {wsState === 'connected' ? '已连接' : wsState === 'connecting' ? '连接中' : wsState === 'error' ? '错误' : '未连接'}
788
- </Tag>
789
- <Popconfirm
790
- title="关闭此工作项?"
791
- onConfirm={() => handleCloseItem(selectedItem.convCode)}
792
- >
793
- <Button size="small" type="text" danger icon={<CheckCircleOutlined/>}>
794
- 完成
795
- </Button>
796
- </Popconfirm>
797
- </Space>
798
- ) : null}
799
- </div>
800
-
801
- {/* 消息流 */}
802
- <div style={{
803
- flex: 1,
804
- overflow: 'auto',
805
- padding: 16,
806
- display: 'flex',
807
- flexDirection: 'column',
808
- gap: 12,
809
- }}>
810
- {!selectedItem ? (
811
- <Empty
812
- image={Empty.PRESENTED_IMAGE_SIMPLE}
813
- description="从左侧选择工作项开始聊天"
814
- style={{marginTop: 80}}
815
- />
816
- ) : messages.length === 0 && !streamingText ? (
817
- <Empty
818
- image={Empty.PRESENTED_IMAGE_SIMPLE}
819
- description="暂无消息,开始聊吧"
820
- style={{marginTop: 80}}
821
- />
822
- ) : (
823
- <>
824
- {messages.map((m) => <MessageBubble key={m.msgCode} msg={m} currentUserId={currentUserId}/>)}
825
- {streamingText ? (
826
- <MessageBubble
827
- msg={{
828
- msgCode: 'streaming',
829
- senderType: 'AGENT',
830
- senderCode: 'agent',
831
- senderName: streamingFrom,
832
- content: streamingText,
833
- timestamp: Date.now(),
834
- messageType: 'STREAMING',
835
- }}
836
- currentUserId={currentUserId}
837
- />
838
- ) : null}
839
- <div ref={messagesEndRef}/>
840
- </>
841
- )}
842
- </div>
843
-
844
- {/* 输入框 */}
845
- <div style={{
846
- padding: 12,
847
- background: '#fff',
848
- borderTop: '1px solid #f0f0f0',
849
- display: 'flex',
850
- gap: 8,
851
- }}>
852
- <Input.TextArea
853
- value={chatInput}
854
- onChange={(e) => setChatInput(e.target.value)}
855
- onPressEnter={(e) => {
856
- if (!e.shiftKey) {
857
- e.preventDefault()
858
- sendMessage()
859
- }
860
- }}
861
- placeholder={selectedItem ? (wsState === 'connected' ? '输入消息... (Enter 发送, Shift+Enter 换行)' : '正在连接 IM...') : '请先选择工作项'}
862
- autoSize={{minRows: 1, maxRows: 4}}
863
- disabled={!selectedItem || wsState !== 'connected'}
864
- />
865
- <Button
866
- type="primary"
867
- icon={<SendOutlined/>}
868
- onClick={sendMessage}
869
- disabled={!selectedItem || wsState !== 'connected' || !chatInput.trim()}
870
- >
871
- 发送
872
- </Button>
873
- </div>
874
- </div>
875
-
876
- {/* ===== Modal: 新建工作空间 ===== */}
877
- <Modal
878
- title="新建工作空间"
879
- open={createWsOpen}
880
- onCancel={() => setCreateWsOpen(false)}
881
- onOk={handleCreateWorkspace}
882
- okText="创建"
883
- cancelText="取消"
884
- >
885
- <Form form={wsForm} layout="vertical">
886
- <Form.Item
887
- label="名称"
888
- name="workspaceName"
889
- rules={[{required: true, message: '请输入工作空间名称'}]}
890
- >
891
- <Input placeholder="例如:z-opc 主项目"/>
892
- </Form.Item>
893
- <Form.Item
894
- label="本地路径"
895
- name="localPath"
896
- rules={[{required: true, message: '请输入本地路径'}]}
897
- extra="服务器侧的文件系统绝对路径 (例: /Users/zifang/workplace)"
898
- >
899
- <Input placeholder="/Users/zifang/workplace"/>
900
- </Form.Item>
901
- <Form.Item label="描述" name="description">
902
- <Input.TextArea rows={2} placeholder="可选:工作空间用途说明"/>
903
- </Form.Item>
904
- </Form>
905
- </Modal>
906
-
907
- {/* ===== Modal: 新建工作项 ===== */}
908
- <Modal
909
- title="新建工作项"
910
- open={createItemOpen}
911
- onCancel={() => setCreateItemOpen(false)}
912
- onOk={handleCreateItem}
913
- okText="创建"
914
- cancelText="取消"
915
- >
916
- <Form form={itemForm} layout="vertical">
917
- <Form.Item
918
- label="标题"
919
- name="title"
920
- rules={[{required: true, message: '请输入工作项标题'}]}
921
- >
922
- <Input placeholder="例如:完成 z-opc 用户面板"/>
923
- </Form.Item>
924
- <Form.Item
925
- label="关联工作空间 (可选)"
926
- name="workspaceCode"
927
- extra="绑定后,工作项右侧会显示对应工作空间的文件树"
928
- >
929
- <select
930
- style={{width: '100%', height: 32, border: '1px solid #d9d9d9', borderRadius: 6, padding: '0 11px'}}
931
- >
932
- <option value="">无 (仅 IM 群聊)</option>
933
- {workspaces.map((w) => (
934
- <option key={w.workspaceCode} value={w.workspaceCode}>
935
- {w.workspaceName} ({w.localPath})
936
- </option>
937
- ))}
938
- </select>
939
- </Form.Item>
940
- </Form>
941
- </Modal>
942
- </div>
943
- )
944
- }
945
-
946
- function MessageBubble({msg, currentUserId}: {msg: ChatMessage; currentUserId: string}) {
947
- const isMe = msg.senderType === 'USER' && msg.senderCode === currentUserId
948
- const isSystem = msg.senderType === 'SYSTEM'
949
-
950
- if (isSystem) {
951
- return (
952
- <div style={{textAlign: 'center', color: '#8c8c8c', fontSize: 11}}>
953
- <Tag color="default">{msg.content}</Tag>
954
- </div>
955
- )
956
- }
957
-
958
- const bubbleColor = isMe ? '#1677ff' : '#fff'
959
- const textColor = isMe ? '#fff' : '#333'
960
- const avatarBg = isMe ? '#1677ff' : '#87d068'
961
-
962
- return (
963
- <div style={{
964
- display: 'flex',
965
- flexDirection: isMe ? 'row-reverse' : 'row',
966
- alignItems: 'flex-start',
967
- gap: 8,
968
- }}>
969
- <Avatar style={{background: avatarBg, flexShrink: 0}}>
970
- {(msg.senderName || '?').charAt(0).toUpperCase()}
971
- </Avatar>
972
- <div style={{maxWidth: '70%'}}>
973
- <div style={{
974
- fontSize: 11,
975
- color: '#8c8c8c',
976
- marginBottom: 2,
977
- textAlign: isMe ? 'right' : 'left',
978
- }}>
979
- {msg.senderName} · {new Date(msg.timestamp).toLocaleTimeString()}
980
- {msg.messageType && msg.messageType !== 'TEXT' ? (
981
- <Tag style={{marginLeft: 4, fontSize: 10}}>{msg.messageType}</Tag>
982
- ) : null}
983
- </div>
984
- <div style={{
985
- padding: '8px 12px',
986
- background: bubbleColor,
987
- color: textColor,
988
- borderRadius: 8,
989
- border: isMe ? 'none' : '1px solid #e8e8e8',
990
- wordBreak: 'break-word',
991
- whiteSpace: 'pre-wrap',
992
- boxShadow: '0 1px 2px rgba(0,0,0,0.04)',
993
- }}>
994
- {msg.content || (msg.messageType === 'STREAMING' ? '...' : '')}
995
- </div>
996
- </div>
997
- </div>
998
- )
999
- }