@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,359 @@
1
+ import {useEffect, useState} from 'react'
2
+ import {
3
+ Button,
4
+ Card,
5
+ Descriptions,
6
+ Form,
7
+ Input,
8
+ InputNumber,
9
+ message,
10
+ Modal,
11
+ Popconfirm,
12
+ Select,
13
+ Space,
14
+ Table,
15
+ Tabs,
16
+ Tag,
17
+ Typography
18
+ } from 'antd'
19
+ import {
20
+ ApiOutlined,
21
+ DeleteOutlined,
22
+ LinkOutlined,
23
+ PlayCircleOutlined,
24
+ PlusOutlined,
25
+ ReloadOutlined
26
+ } from '@ant-design/icons'
27
+ import {mcpApi} from '../../api'
28
+
29
+ const {TextArea} = Input
30
+ const {Text} = Typography
31
+
32
+ export default function McpPage() {
33
+ const [activeTab, setActiveTab] = useState('servers')
34
+ const [servers, setServers] = useState([])
35
+ const [loading, setLoading] = useState(false)
36
+ const [modalOpen, setModalOpen] = useState(false)
37
+ const [editing, setEditing] = useState(null)
38
+ const [form] = Form.useForm()
39
+
40
+ // Tool test state
41
+ const [selectedServer, setSelectedServer] = useState(null)
42
+ const [tools, setTools] = useState([])
43
+ const [selectedTool, setSelectedTool] = useState(null)
44
+ const [toolArgs, setToolArgs] = useState('{}')
45
+ const [toolResult, setToolResult] = useState('')
46
+ const [toolRunning, setToolRunning] = useState(false)
47
+ const [testResult, setTestResult] = useState(null)
48
+
49
+ useEffect(() => {
50
+ loadServers()
51
+ }, [])
52
+
53
+ const loadServers = async () => {
54
+ setLoading(true)
55
+ try {
56
+ const tenant = localStorage.getItem('z_tenant') || 'default'
57
+ const res = await mcpApi.list(tenant)
58
+ // 兼容:API 可能返回 404 或空数据
59
+ setServers(Array.isArray(res) ? res : (res?.data || res || []))
60
+ } catch (e) {
61
+ // API 不存在时显示空状态,不报错
62
+ console.warn('MCP API 不可用:', e?.message)
63
+ setServers([])
64
+ } finally {
65
+ setLoading(false)
66
+ }
67
+ }
68
+
69
+ const handleSave = async () => {
70
+ try {
71
+ const values = await form.validateFields()
72
+ const data = {...values}
73
+ if (editing) {
74
+ data.id = editing.id
75
+ await mcpApi.update(data)
76
+ message.success('更新成功')
77
+ } else {
78
+ await mcpApi.create(data)
79
+ message.success('创建成功')
80
+ }
81
+ setModalOpen(false)
82
+ setEditing(null)
83
+ form.resetFields()
84
+ loadServers()
85
+ } catch (e) { /* validation error */
86
+ }
87
+ }
88
+
89
+ const handleEdit = (record) => {
90
+ setEditing(record)
91
+ form.setFieldsValue(record)
92
+ setModalOpen(true)
93
+ }
94
+
95
+ const handleDelete = async (id) => {
96
+ await mcpApi.delete(id)
97
+ message.success('已删除')
98
+ loadServers()
99
+ }
100
+
101
+ const handleTest = async (record) => {
102
+ setTestResult({loading: true})
103
+ try {
104
+ const res = await mcpApi.test(record.id)
105
+ setTestResult(res?.data || res)
106
+ } catch (e) {
107
+ setTestResult({connected: false, error: e.message})
108
+ }
109
+ }
110
+
111
+ const handleSelectServer = async (record) => {
112
+ setSelectedServer(record)
113
+ setSelectedTool(null)
114
+ setToolResult('')
115
+ try {
116
+ const res = await mcpApi.listTools(record.id)
117
+ const data = res?.data || res
118
+ setTools(data?.tools || [])
119
+ } catch (e) {
120
+ setTools([])
121
+ message.error('获取工具列表失败')
122
+ }
123
+ }
124
+
125
+ const handleSelectTool = (tool) => {
126
+ setSelectedTool(tool)
127
+ // Build default args from schema
128
+ const schema = tool.inputSchema
129
+ if (schema?.properties) {
130
+ const defaults = {}
131
+ Object.keys(schema.properties).forEach(k => {
132
+ defaults[k] = ''
133
+ })
134
+ setToolArgs(JSON.stringify(defaults, null, 2))
135
+ } else {
136
+ setToolArgs('{}')
137
+ }
138
+ setToolResult('')
139
+ }
140
+
141
+ const handleRunTool = async () => {
142
+ if (!selectedServer || !selectedTool) return
143
+ setToolRunning(true)
144
+ setToolResult('Running...')
145
+ try {
146
+ let args = {}
147
+ try {
148
+ args = JSON.parse(toolArgs)
149
+ } catch (e) {
150
+ }
151
+ const res = await mcpApi.callTool(selectedServer.id, selectedTool.name, args)
152
+ const data = res?.data || res
153
+ if (data?.content && data.content.length > 0) {
154
+ setToolResult(data.content[0].text || JSON.stringify(data.content, null, 2))
155
+ } else {
156
+ setToolResult(JSON.stringify(data, null, 2))
157
+ }
158
+ } catch (e) {
159
+ setToolResult('Error: ' + e.message)
160
+ } finally {
161
+ setToolRunning(false)
162
+ }
163
+ }
164
+
165
+ // ===== Server columns =====
166
+ const columns = [
167
+ {title: '名称', dataIndex: 'serverName', key: 'serverName', width: 140},
168
+ {
169
+ title: '类型', dataIndex: 'transportType', key: 'transportType', width: 80,
170
+ render: (v) => <Tag color={v === 'STDIO' ? 'purple' : 'blue'}>{v || 'HTTP'}</Tag>
171
+ },
172
+ {title: 'URL', dataIndex: 'url', key: 'url', ellipsis: true},
173
+ {
174
+ title: '状态', dataIndex: 'status', key: 'status', width: 80,
175
+ render: (v) => <Tag color={v === 'active' ? 'green' : 'default'}>{v || 'active'}</Tag>
176
+ },
177
+ {
178
+ title: '操作', key: 'actions', width: 260, render: (_, r) => (
179
+ <Space size="small">
180
+ <Button size="small" icon={<ApiOutlined/>} onClick={() => handleSelectServer(r)}>工具</Button>
181
+ <Button size="small" icon={<LinkOutlined/>} onClick={() => handleTest(r)}>测试</Button>
182
+ <Button size="small" onClick={() => handleEdit(r)}>编辑</Button>
183
+ <Popconfirm title="确认删除?" onConfirm={() => handleDelete(r.id)}>
184
+ <Button size="small" danger icon={<DeleteOutlined/>}/>
185
+ </Popconfirm>
186
+ </Space>
187
+ )
188
+ },
189
+ ]
190
+
191
+ return (
192
+ <div style={{height: 'calc(100vh - 180px)', display: 'flex', flexDirection: 'column'}}>
193
+ <Tabs activeKey={activeTab} onChange={setActiveTab} tabBarExtraContent={
194
+ <Button type="primary" icon={<PlusOutlined/>} onClick={() => {
195
+ setEditing(null);
196
+ form.resetFields();
197
+ setModalOpen(true)
198
+ }}>
199
+ 添加服务
200
+ </Button>
201
+ }>
202
+ <Tabs.TabPane tab="服务管理" key="servers"/>
203
+ <Tabs.TabPane tab="工具测试" key="test" disabled={!selectedServer}/>
204
+ </Tabs>
205
+
206
+ {activeTab === 'servers' && (
207
+ <div style={{flex: 1, display: 'flex', gap: 16, overflow: 'hidden'}}>
208
+ {/* Server List */}
209
+ <div style={{flex: selectedServer ? '0 0 50%' : 1, overflow: 'auto'}}>
210
+ <Table columns={columns} dataSource={servers} rowKey="id" loading={loading}
211
+ size="small" pagination={false}
212
+ onRow={(r) => ({
213
+ onClick: () => handleSelectServer(r),
214
+ style: {
215
+ background: selectedServer?.id === r.id ? '#e6f7ff' : undefined,
216
+ cursor: 'pointer'
217
+ }
218
+ })}/>
219
+ </div>
220
+
221
+ {/* Tool Explorer */}
222
+ {selectedServer && (
223
+ <div style={{
224
+ flex: '0 0 50%',
225
+ borderLeft: '1px solid #f0f0f0',
226
+ paddingLeft: 16,
227
+ overflow: 'auto'
228
+ }}>
229
+ <div style={{
230
+ marginBottom: 12,
231
+ display: 'flex',
232
+ justifyContent: 'space-between',
233
+ alignItems: 'center'
234
+ }}>
235
+ <Text strong style={{fontSize: 15}}>{selectedServer.serverName} — 工具列表</Text>
236
+ <Button size="small" icon={<ReloadOutlined/>}
237
+ onClick={() => handleSelectServer(selectedServer)}>刷新</Button>
238
+ </div>
239
+ {tools.length === 0 ? (
240
+ <div style={{textAlign: 'center', padding: 40, color: '#999'}}>暂无工具</div>
241
+ ) : (
242
+ tools.map(t => (
243
+ <Card key={t.name} size="small" hoverable
244
+ style={{
245
+ marginBottom: 8,
246
+ borderColor: selectedTool?.name === t.name ? '#1890ff' : undefined
247
+ }}
248
+ onClick={() => {
249
+ handleSelectTool(t);
250
+ setActiveTab('test')
251
+ }}>
252
+ <div style={{
253
+ display: 'flex',
254
+ justifyContent: 'space-between',
255
+ alignItems: 'center'
256
+ }}>
257
+ <div>
258
+ <Text strong>{t.name}</Text>
259
+ <br/><Text type="secondary"
260
+ style={{fontSize: 12}}>{t.description}</Text>
261
+ </div>
262
+ <Tag color="blue" style={{cursor: 'pointer'}}
263
+ onClick={(e) => {
264
+ e.stopPropagation();
265
+ handleSelectTool(t);
266
+ setActiveTab('test')
267
+ }}>
268
+ 测试 →
269
+ </Tag>
270
+ </div>
271
+ </Card>
272
+ ))
273
+ )}
274
+ </div>
275
+ )}
276
+ </div>
277
+ )}
278
+
279
+ {activeTab === 'test' && selectedTool && (
280
+ <div style={{flex: 1, display: 'flex', gap: 16, overflow: 'hidden'}}>
281
+ {/* Input */}
282
+ <div style={{flex: '0 0 40%', display: 'flex', flexDirection: 'column'}}>
283
+ <div style={{marginBottom: 8}}>
284
+ <Text strong>{selectedTool.name}</Text>
285
+ <Text type="secondary" style={{marginLeft: 8}}>{selectedTool.description}</Text>
286
+ </div>
287
+ <Text type="secondary" style={{marginBottom: 4}}>参数 (JSON):</Text>
288
+ <TextArea rows={8} value={toolArgs} onChange={e => setToolArgs(e.target.value)}
289
+ style={{fontFamily: 'monospace', fontSize: 13}}/>
290
+ <Button type="primary" icon={<PlayCircleOutlined/>} onClick={handleRunTool}
291
+ loading={toolRunning} style={{marginTop: 12}}>执行</Button>
292
+ </div>
293
+ {/* Output */}
294
+ <div style={{flex: '0 0 60%', display: 'flex', flexDirection: 'column'}}>
295
+ <Text type="secondary" style={{marginBottom: 4}}>结果:</Text>
296
+ <pre style={{
297
+ flex: 1, overflow: 'auto', background: '#f5f5f5', padding: 12, borderRadius: 4,
298
+ fontFamily: 'monospace', fontSize: 13, margin: 0, whiteSpace: 'pre-wrap'
299
+ }}>
300
+ {toolResult || '点击"执行"查看结果'}
301
+ </pre>
302
+ </div>
303
+ </div>
304
+ )}
305
+
306
+ {/* Test Result Modal */}
307
+ <Modal title={`测试连接 - ${testResult ? '' : ''}`} open={!!testResult} onCancel={() => setTestResult(null)}
308
+ footer={<Button onClick={() => setTestResult(null)}>关闭</Button>}>
309
+ {testResult?.loading ? <Text>测试中...</Text> : testResult ? (
310
+ <Descriptions column={1} size="small">
311
+ <Descriptions.Item label="连接状态">
312
+ <Tag
313
+ color={testResult.connected ? 'green' : 'red'}>{testResult.connected ? '成功' : '失败'}</Tag>
314
+ </Descriptions.Item>
315
+ {testResult.serverInfo && <Descriptions.Item label="服务信息">
316
+ {testResult.serverInfo?.name} v{testResult.serverInfo?.version}
317
+ </Descriptions.Item>}
318
+ {testResult.toolCount !== undefined &&
319
+ <Descriptions.Item label="工具数量">{testResult.toolCount}</Descriptions.Item>}
320
+ {testResult.error && <Descriptions.Item label="错误">{testResult.error}</Descriptions.Item>}
321
+ </Descriptions>
322
+ ) : null}
323
+ </Modal>
324
+
325
+ {/* Add/Edit Modal */}
326
+ <Modal title={editing ? '编辑 MCP 服务' : '添加 MCP 服务'} open={modalOpen}
327
+ onOk={handleSave} onCancel={() => {
328
+ setModalOpen(false);
329
+ setEditing(null);
330
+ form.resetFields()
331
+ }}>
332
+ <Form form={form} layout="vertical"
333
+ initialValues={{transportType: 'HTTP', timeout: 60, status: 'active'}}>
334
+ <Form.Item name="serverName" label="服务名称" rules={[{required: true}]}>
335
+ <Input placeholder="如: z-agent-mcp-impl-db"/>
336
+ </Form.Item>
337
+ <Form.Item name="transportType" label="传输类型">
338
+ <Select options={[{label: 'HTTP', value: 'HTTP'}, {label: 'STDIO', value: 'STDIO'}]}/>
339
+ </Form.Item>
340
+ <Form.Item name="url" label="URL" rules={[{required: true}]}>
341
+ <Input placeholder="http://localhost:8095/mcp"/>
342
+ </Form.Item>
343
+ <Form.Item name="authToken" label="Auth Token">
344
+ <Input.Password placeholder="Bearer token (可选)"/>
345
+ </Form.Item>
346
+ <Form.Item name="timeout" label="超时(秒)">
347
+ <InputNumber min={5} max={300} style={{width: '100%'}}/>
348
+ </Form.Item>
349
+ <Form.Item name="status" label="状态">
350
+ <Select options={[{label: '启用', value: 'active'}, {label: '停用', value: 'inactive'}]}/>
351
+ </Form.Item>
352
+ <Form.Item name="remark" label="备注">
353
+ <TextArea rows={2}/>
354
+ </Form.Item>
355
+ </Form>
356
+ </Modal>
357
+ </div>
358
+ )
359
+ }
@@ -0,0 +1,320 @@
1
+ import {useCallback, useEffect, useState} from 'react'
2
+ import {
3
+ Button,
4
+ Card,
5
+ Col,
6
+ Dropdown,
7
+ Empty,
8
+ Form,
9
+ Input,
10
+ message,
11
+ Modal,
12
+ Popconfirm,
13
+ Row,
14
+ Select,
15
+ Space,
16
+ Spin,
17
+ Statistic,
18
+ Tag,
19
+ Tooltip
20
+ } from 'antd'
21
+ import {
22
+ ApartmentOutlined,
23
+ AppstoreOutlined,
24
+ CloudOutlined,
25
+ DatabaseOutlined,
26
+ DeleteOutlined,
27
+ EyeOutlined,
28
+ FolderOutlined,
29
+ GlobalOutlined,
30
+ LockOutlined,
31
+ MoreOutlined,
32
+ PlusOutlined,
33
+ ReloadOutlined,
34
+ TeamOutlined
35
+ } from '@ant-design/icons'
36
+ import {ossApi} from '../../api'
37
+ import {useNavigate} from 'react-router-dom'
38
+
39
+ const ACL_LABELS = {
40
+ 'private': {label: '私有', color: 'default', icon: <LockOutlined/>},
41
+ 'public-read': {label: '公共读', color: 'blue', icon: <EyeOutlined/>},
42
+ 'public-read-write': {label: '公共读写', color: 'orange', icon: <GlobalOutlined/>},
43
+ 'authenticated-read': {label: '认证读', color: 'purple', icon: <TeamOutlined/>},
44
+ }
45
+
46
+ export default function BucketList() {
47
+ const [buckets, setBuckets] = useState([])
48
+ const [loading, setLoading] = useState(false)
49
+ const [createOpen, setCreateOpen] = useState(false)
50
+ const [createForm] = Form.useForm()
51
+ const [creating, setCreating] = useState(false)
52
+ const navigate = useNavigate()
53
+
54
+ const loadBuckets = useCallback(async () => {
55
+ setLoading(true)
56
+ try {
57
+ const res = await ossApi.listBuckets()
58
+ const list = Array.isArray(res) ? res : (res?.data || [])
59
+ setBuckets(list)
60
+ } catch (e) {
61
+ console.error('loadBuckets error:', e)
62
+ message.error('加载桶列表失败:' + (e?.message || '未知错误'))
63
+ } finally {
64
+ setLoading(false)
65
+ }
66
+ }, [])
67
+
68
+ useEffect(() => {
69
+ loadBuckets()
70
+ }, [loadBuckets])
71
+
72
+ const handleCreate = async () => {
73
+ try {
74
+ const vals = await createForm.validateFields()
75
+ setCreating(true)
76
+ await ossApi.createBucket(vals)
77
+ message.success('桶创建成功')
78
+ setCreateOpen(false)
79
+ createForm.resetFields()
80
+ loadBuckets()
81
+ } catch (e) {
82
+ if (e?.errorFields) return
83
+ message.error('创建失败:' + (e?.message || '未知错误'))
84
+ } finally {
85
+ setCreating(false)
86
+ }
87
+ }
88
+
89
+ const handleDelete = async (name) => {
90
+ try {
91
+ await ossApi.deleteBucket(name)
92
+ message.success('已删除')
93
+ loadBuckets()
94
+ } catch (e) {
95
+ message.error('删除失败:' + (e?.message || '未知错误'))
96
+ }
97
+ }
98
+
99
+ const handleSetAcl = async (name, acl) => {
100
+ try {
101
+ await ossApi.setBucketAcl(name, acl)
102
+ message.success('ACL 已更新')
103
+ loadBuckets()
104
+ } catch (e) {
105
+ message.error('设置 ACL 失败:' + (e?.message || '未知错误'))
106
+ }
107
+ }
108
+
109
+ const aclMenu = (name) => ({
110
+ items: Object.keys(ACL_LABELS).map(acl => ({
111
+ key: acl,
112
+ label: ACL_LABELS[acl].label,
113
+ icon: ACL_LABELS[acl].icon,
114
+ })),
115
+ onClick: ({key}) => handleSetAcl(name, key),
116
+ })
117
+
118
+ return (
119
+ <div>
120
+ {/* 顶部工具栏 */}
121
+ <div style={{
122
+ display: 'flex', alignItems: 'center', gap: 12,
123
+ background: '#fff', padding: '14px 16px', borderRadius: 10,
124
+ boxShadow: '0 1px 4px rgba(0,0,0,0.04)', marginBottom: 16,
125
+ }}>
126
+ <DatabaseOutlined style={{fontSize: 18, color: '#1890ff'}}/>
127
+ <span style={{fontSize: 16, fontWeight: 600}}>对象存储</span>
128
+ <Tag color="blue">OSS Buckets</Tag>
129
+ <div style={{flex: 1}}/>
130
+ <Button icon={<ReloadOutlined/>} onClick={loadBuckets}>刷新</Button>
131
+ <Button
132
+ type="primary"
133
+ icon={<PlusOutlined/>}
134
+ onClick={() => setCreateOpen(true)}
135
+ >
136
+ 创建桶
137
+ </Button>
138
+ </div>
139
+
140
+ {/* 统计卡片 */}
141
+ <Row gutter={16} style={{marginBottom: 16}}>
142
+ <Col span={8}>
143
+ <Card>
144
+ <Statistic title="桶总数" value={buckets.length}
145
+ prefix={<ApartmentOutlined style={{color: '#1890ff'}}/>}/>
146
+ </Card>
147
+ </Col>
148
+ <Col span={8}>
149
+ <Card>
150
+ <Statistic title="公共读" value={buckets.filter(b => b.acl === 'public-read').length}
151
+ valueStyle={{color: '#1890ff'}} prefix={<EyeOutlined/>}/>
152
+ </Card>
153
+ </Col>
154
+ <Col span={8}>
155
+ <Card>
156
+ <Statistic title="私有" value={buckets.filter(b => !b.acl || b.acl === 'private').length}
157
+ valueStyle={{color: '#999'}} prefix={<LockOutlined/>}/>
158
+ </Card>
159
+ </Col>
160
+ </Row>
161
+
162
+ {loading ? (
163
+ <div style={{textAlign: 'center', padding: 80}}><Spin size="large"/></div>
164
+ ) : buckets.length === 0 ? (
165
+ <Empty
166
+ image={<CloudOutlined style={{fontSize: 64, color: '#bfbfbf'}}/>}
167
+ description={
168
+ <div>
169
+ <div style={{fontSize: 16, marginBottom: 8}}>还没有桶</div>
170
+ <div style={{color: '#999', fontSize: 13, marginBottom: 16}}>
171
+ 创建一个桶来开始使用对象存储
172
+ </div>
173
+ <Button type="primary" icon={<PlusOutlined/>} onClick={() => setCreateOpen(true)}>
174
+ 创建第一个桶
175
+ </Button>
176
+ </div>
177
+ }
178
+ style={{padding: 80, background: '#fff', borderRadius: 10}}
179
+ />
180
+ ) : (
181
+ <Row gutter={[16, 16]}>
182
+ {buckets.map(bucket => {
183
+ const acl = ACL_LABELS[bucket.acl] || ACL_LABELS.private
184
+ return (
185
+ <Col key={bucket.id || bucket.name} xs={24} sm={12} md={8} lg={6}>
186
+ <Card
187
+ hoverable
188
+ onClick={() => navigate(`/ai/oss/bucket/${encodeURIComponent(bucket.name)}`)}
189
+ style={{
190
+ borderRadius: 12, border: 'none',
191
+ boxShadow: '0 2px 12px rgba(0,0,0,0.07)',
192
+ }}
193
+ styles={{body: {padding: 18}}}
194
+ >
195
+ <div style={{display: 'flex', alignItems: 'flex-start', gap: 12}}>
196
+ <div style={{
197
+ width: 48, height: 48, borderRadius: 10,
198
+ background: 'linear-gradient(135deg, #1890ff 0%, #36cfc9 100%)',
199
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
200
+ }}>
201
+ <FolderOutlined style={{fontSize: 22, color: '#fff'}}/>
202
+ </div>
203
+ <div style={{flex: 1, minWidth: 0}}>
204
+ <div style={{
205
+ fontSize: 15, fontWeight: 600, color: '#262626',
206
+ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
207
+ }}>
208
+ {bucket.name}
209
+ </div>
210
+ <div style={{fontSize: 12, color: '#999', marginTop: 4}}>
211
+ {bucket.region || 'default'} · {bucket.createTime?.slice(0, 10) || '-'}
212
+ </div>
213
+ </div>
214
+ <Dropdown
215
+ trigger={['click']}
216
+ menu={aclMenu(bucket.name)}
217
+ onClick={e => e.stopPropagation()}
218
+ >
219
+ <Button
220
+ type="text" size="small" icon={<MoreOutlined/>}
221
+ onClick={e => e.stopPropagation()}
222
+ />
223
+ </Dropdown>
224
+ </div>
225
+
226
+ <div style={{marginTop: 14, display: 'flex', gap: 6, flexWrap: 'wrap'}}>
227
+ <Tag color={acl.color} icon={acl.icon}>{acl.label}</Tag>
228
+ </div>
229
+
230
+ <div style={{
231
+ marginTop: 14, paddingTop: 12, borderTop: '1px solid #f5f5f5',
232
+ display: 'flex', gap: 8, justifyContent: 'space-between',
233
+ }}>
234
+ <Tooltip title="进入桶">
235
+ <Button
236
+ type="text" size="small" icon={<AppstoreOutlined/>}
237
+ onClick={e => {
238
+ e.stopPropagation()
239
+ navigate(`/ai/oss/bucket/${encodeURIComponent(bucket.name)}`)
240
+ }}
241
+ >
242
+ 浏览对象
243
+ </Button>
244
+ </Tooltip>
245
+ <Popconfirm
246
+ title={`确定删除桶 "${bucket.name}"?`}
247
+ description="桶必须为空才能删除"
248
+ onConfirm={e => {
249
+ e?.stopPropagation();
250
+ handleDelete(bucket.name)
251
+ }}
252
+ onCancel={e => e?.stopPropagation()}
253
+ okText="确认"
254
+ cancelText="取消"
255
+ okType="danger"
256
+ >
257
+ <Button
258
+ type="text" size="small" danger icon={<DeleteOutlined/>}
259
+ onClick={e => e.stopPropagation()}
260
+ >
261
+ 删除
262
+ </Button>
263
+ </Popconfirm>
264
+ </div>
265
+ </Card>
266
+ </Col>
267
+ )
268
+ })}
269
+ </Row>
270
+ )}
271
+
272
+ {/* 创建桶弹窗 */}
273
+ <Modal
274
+ title={
275
+ <Space>
276
+ <CloudOutlined style={{color: '#1890ff'}}/>
277
+ <span>创建存储桶</span>
278
+ </Space>
279
+ }
280
+ open={createOpen}
281
+ onCancel={() => {
282
+ setCreateOpen(false);
283
+ createForm.resetFields()
284
+ }}
285
+ onOk={handleCreate}
286
+ confirmLoading={creating}
287
+ okText="创建"
288
+ cancelText="取消"
289
+ destroyOnClose
290
+ >
291
+ <Form form={createForm} layout="vertical" initialValues={{acl: 'private'}} style={{marginTop: 16}}>
292
+ <Form.Item
293
+ name="name"
294
+ label="桶名称"
295
+ rules={[
296
+ {required: true, message: '请输入桶名称'},
297
+ {
298
+ pattern: /^[a-z0-9][a-z0-9-]{2,62}[a-z0-9]$/,
299
+ message: '只能包含小写字母、数字、连字符,长度 3-63'
300
+ },
301
+ ]}
302
+ extra="仅支持小写字母、数字、连字符,长度 3-63 字符"
303
+ >
304
+ <Input placeholder="my-bucket" prefix={<FolderOutlined/>}/>
305
+ </Form.Item>
306
+ <Form.Item name="region" label="区域" initialValue="default">
307
+ <Input placeholder="default"/>
308
+ </Form.Item>
309
+ <Form.Item name="acl" label="访问权限" initialValue="private">
310
+ <Select
311
+ options={Object.entries(ACL_LABELS).map(([k, v]) => ({
312
+ value: k, label: <Space>{v.icon}{v.label}</Space>,
313
+ }))}
314
+ />
315
+ </Form.Item>
316
+ </Form>
317
+ </Modal>
318
+ </div>
319
+ )
320
+ }