@yuku123/z-subscribe-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.
@@ -0,0 +1,460 @@
1
+ import {useEffect, useState} from 'react'
2
+ import {useNavigate} from 'react-router-dom'
3
+ import {
4
+ Alert,
5
+ Button,
6
+ Card,
7
+ Drawer,
8
+ Form,
9
+ Input,
10
+ message,
11
+ Modal,
12
+ Popconfirm,
13
+ Space,
14
+ Table,
15
+ Tabs,
16
+ Tag,
17
+ Timeline,
18
+ Tooltip
19
+ } from 'antd'
20
+ import {
21
+ ApiOutlined,
22
+ CheckCircleOutlined,
23
+ CloseCircleOutlined,
24
+ DeleteOutlined,
25
+ EditOutlined,
26
+ ExclamationCircleOutlined,
27
+ PauseCircleOutlined,
28
+ PlayCircleOutlined,
29
+ PlusOutlined,
30
+ ProjectOutlined,
31
+ ReloadOutlined,
32
+ ThunderboltOutlined
33
+ } from '@ant-design/icons'
34
+ import request from '../utils/request'
35
+
36
+ // 抽取器类型 -> 中文 + 颜色
37
+ const EXTRACTOR_LABEL = {
38
+ STANDARD_JSON: {label: '标准 JSON', color: 'blue'},
39
+ RSS_XML: {label: 'RSS 2.0', color: 'orange'},
40
+ ATOM_XML: {label: 'Atom 1.0', color: 'gold'},
41
+ CUSTOM_SCRIPT: {label: '自定义脚本', color: 'purple'},
42
+ INTERNAL_BEAN: {label: '内部 Bean', color: 'cyan'},
43
+ }
44
+
45
+ // 状态 -> 颜色
46
+ const STATUS_TAG = {
47
+ PENDING_VERIFY: {color: 'default', text: '待验证'},
48
+ VERIFIED: {color: 'green', text: '已验证'},
49
+ INVALID_AUTH: {color: 'red', text: '鉴权失败'},
50
+ FATAL: {color: 'red', text: '永久失败'},
51
+ DISABLED: {color: 'default', text: '已停用'},
52
+ }
53
+
54
+ const RUN_STATUS_TAG = {
55
+ PENDING: {color: 'default'},
56
+ RUNNING: {color: 'processing'},
57
+ SUCCESS: {color: 'green'},
58
+ EMPTY: {color: 'default'},
59
+ FAILED: {color: 'red'},
60
+ FATAL: {color: 'red'},
61
+ }
62
+
63
+ function SubscriptionList() {
64
+ const navigate = useNavigate()
65
+ const [data, setData] = useState([])
66
+ const [loading, setLoading] = useState(false)
67
+ const [detail, setDetail] = useState(null) // dry-run 详情 Drawer
68
+ const [runs, setRuns] = useState([]) // 选中订阅的 run 列表
69
+ const [runItems, setRunItems] = useState({}) // runId -> items
70
+ const [activeRunId, setActiveRunId] = useState(null)
71
+ const [selectedItemIds, setSelectedItemIds] = useState([])
72
+
73
+ const fetchData = async () => {
74
+ setLoading(true)
75
+ try {
76
+ const res = await request.get('/subscribe/subscription/list')
77
+ if (res && res.success !== false) {
78
+ setData(Array.isArray(res.data) ? res.data : (Array.isArray(res) ? res : []))
79
+ }
80
+ } catch (e) {
81
+ message.error('获取订阅列表失败: ' + (e?.message || ''))
82
+ } finally {
83
+ setLoading(false)
84
+ }
85
+ }
86
+
87
+ useEffect(() => {
88
+ fetchData()
89
+ }, [])
90
+
91
+ const handleDelete = (record) => {
92
+ Modal.confirm({
93
+ title: '确认删除',
94
+ icon: <ExclamationCircleOutlined/>,
95
+ content: `确定要删除订阅 "${record.name}" 吗? 运行历史会保留.`,
96
+ onOk: async () => {
97
+ try {
98
+ const res = await request.delete(`/subscribe/subscription/${record.id}`)
99
+ if (res && res.success !== false) {
100
+ message.success('删除成功')
101
+ fetchData()
102
+ } else {
103
+ message.error(res?.message || '删除失败')
104
+ }
105
+ } catch (e) {
106
+ message.error('删除失败: ' + (e?.message || ''))
107
+ }
108
+ },
109
+ })
110
+ }
111
+
112
+ const handleEnable = async (id) => {
113
+ try {
114
+ const res = await request.post(`/subscribe/subscription/${id}/enable`)
115
+ if (res && res.success !== false) {
116
+ message.success('已启用')
117
+ fetchData()
118
+ } else {
119
+ message.error(res?.message || '启用失败')
120
+ }
121
+ } catch (e) {
122
+ message.error('启用失败: ' + (e?.message || ''))
123
+ }
124
+ }
125
+
126
+ const handleDisable = async (id) => {
127
+ try {
128
+ const res = await request.post(`/subscribe/subscription/${id}/disable`)
129
+ if (res && res.success !== false) {
130
+ message.success('已停用')
131
+ fetchData()
132
+ } else {
133
+ message.error(res?.message || '停用失败')
134
+ }
135
+ } catch (e) {
136
+ message.error('停用失败: ' + (e?.message || ''))
137
+ }
138
+ }
139
+
140
+ const handleTrigger = async (record) => {
141
+ Modal.confirm({
142
+ title: '手动触发',
143
+ content: `将立即执行一次 "${record.name}" 的抽取, 同步返回 runId.`,
144
+ onOk: async () => {
145
+ try {
146
+ const res = await request.post(`/subscribe/subscription/${record.id}/trigger?triggerBy=ceo`)
147
+ if (res && (res.data !== undefined || res.success !== false)) {
148
+ const runId = res.data ?? res
149
+ message.success(`已触发, runId = ${runId}`)
150
+ openRuns(record)
151
+ } else {
152
+ message.error(res?.message || '触发失败')
153
+ }
154
+ } catch (e) {
155
+ message.error('触发失败: ' + (e?.message || ''))
156
+ }
157
+ },
158
+ })
159
+ }
160
+
161
+ const handleDryRun = async (record) => {
162
+ try {
163
+ const res = await request.post(`/subscribe/subscription/${record.id}/dry-run`)
164
+ setDetail({sub: record, result: res?.data ?? res})
165
+ } catch (e) {
166
+ setDetail({sub: record, result: {success: false, errorMessage: e?.message || '请求失败'}})
167
+ }
168
+ }
169
+
170
+ const openRuns = async (record) => {
171
+ try {
172
+ const res = await request.get(`/subscribe/run/list?subscriptionId=${record.id}&limit=20`)
173
+ const list = res?.data ?? res ?? []
174
+ setRuns(Array.isArray(list) ? list : [])
175
+ setActiveRunId(null)
176
+ setRunItems({})
177
+ setSelectedItemIds([])
178
+ setDetail({sub: record, tab: 'runs'})
179
+ } catch (e) {
180
+ message.error('获取运行历史失败')
181
+ }
182
+ }
183
+
184
+ const loadRunItems = async (runId) => {
185
+ if (runItems[runId]) {
186
+ setActiveRunId(runId)
187
+ return
188
+ }
189
+ try {
190
+ const res = await request.get(`/subscribe/run/${runId}/items?limit=200`)
191
+ const list = res?.data ?? res ?? []
192
+ setRunItems(prev => ({...prev, [runId]: Array.isArray(list) ? list : []}))
193
+ setActiveRunId(runId)
194
+ } catch (e) {
195
+ message.error('获取抽取项失败')
196
+ }
197
+ }
198
+
199
+ const columns = [
200
+ {
201
+ title: 'ID',
202
+ dataIndex: 'id',
203
+ width: 70,
204
+ },
205
+ {
206
+ title: '名称',
207
+ dataIndex: 'name',
208
+ render: (name, r) => (
209
+ <Space orientation="vertical" size={0}>
210
+ <span style={{fontWeight: 500}}>{name}</span>
211
+ {r.description && <span style={{color: '#999', fontSize: 12}}>{r.description}</span>}
212
+ </Space>
213
+ ),
214
+ },
215
+ {
216
+ title: '数据源',
217
+ dataIndex: 'sourceType',
218
+ width: 90,
219
+ render: (t) => <Tag>{t}</Tag>,
220
+ },
221
+ {
222
+ title: '抽取器',
223
+ dataIndex: 'extractorType',
224
+ width: 140,
225
+ render: (t) => {
226
+ const meta = EXTRACTOR_LABEL[t] || {label: t, color: 'default'}
227
+ return <Tag color={meta.color}>{meta.label}</Tag>
228
+ },
229
+ },
230
+ {
231
+ title: 'Cron',
232
+ dataIndex: 'cronExpr',
233
+ width: 150,
234
+ render: (c) => <code style={{fontSize: 12}}>{c}</code>,
235
+ },
236
+ {
237
+ title: '状态',
238
+ dataIndex: 'status',
239
+ width: 110,
240
+ render: (s) => {
241
+ const m = STATUS_TAG[s] || {color: 'default', text: s}
242
+ return <Tag color={m.color}>{m.text}</Tag>
243
+ },
244
+ },
245
+ {
246
+ title: '启用',
247
+ dataIndex: 'enabled',
248
+ width: 80,
249
+ render: (e, r) => e ? <Tag color="green">是</Tag> : <Tag>否</Tag>,
250
+ },
251
+ {
252
+ title: '最近 run',
253
+ dataIndex: 'lastStatus',
254
+ width: 110,
255
+ render: (s, r) => s ? (
256
+ <Tooltip title={r.lastError || ''}>
257
+ <Tag color={RUN_STATUS_TAG[s]?.color || 'default'}>{s}</Tag>
258
+ </Tooltip>
259
+ ) : <span style={{color: '#ccc'}}>-</span>,
260
+ },
261
+ {
262
+ title: '累计',
263
+ width: 130,
264
+ render: (_, r) => (
265
+ <Space size={4}>
266
+ <Tag color="green">✓ {r.successCountTotal || 0}</Tag>
267
+ <Tag color="red">✗ {r.failCountTotal || 0}</Tag>
268
+ </Space>
269
+ ),
270
+ },
271
+ {
272
+ title: '操作',
273
+ width: 280,
274
+ fixed: 'right',
275
+ render: (_, r) => (
276
+ <Space size={4} wrap>
277
+ <Button type="link" size="small" icon={<EditOutlined/>}
278
+ onClick={() => navigate(`/subscribe/edit/${r.id}`)}>编辑</Button>
279
+ <Button type="link" size="small" icon={<ThunderboltOutlined/>}
280
+ onClick={() => handleDryRun(r)}>试运行</Button>
281
+ <Button type="link" size="small" icon={<PlayCircleOutlined/>}
282
+ onClick={() => handleTrigger(r)}>触发</Button>
283
+ <Button type="link" size="small"
284
+ onClick={() => openRuns(r)}>历史</Button>
285
+ {r.enabled ? (
286
+ <Button type="link" size="small" icon={<PauseCircleOutlined/>}
287
+ onClick={() => handleDisable(r.id)}>停用</Button>
288
+ ) : (
289
+ <Button type="link" size="small" icon={<CheckCircleOutlined/>}
290
+ onClick={() => handleEnable(r.id)}>启用</Button>
291
+ )}
292
+ <Popconfirm title="确认删除?" onConfirm={() => handleDelete(r)} okText="删除" cancelText="取消">
293
+ <Button type="link" size="small" danger icon={<DeleteOutlined/>}>删除</Button>
294
+ </Popconfirm>
295
+ </Space>
296
+ ),
297
+ },
298
+ ]
299
+
300
+ return (
301
+ <div style={{padding: 24}}>
302
+ <Card
303
+ title={<Space><ApiOutlined/>订阅中心 - 数据源订阅</Space>}
304
+ extra={
305
+ <Space>
306
+ <Button icon={<ReloadOutlined/>} onClick={fetchData}>刷新</Button>
307
+ <Button type="primary" icon={<PlusOutlined/>}
308
+ onClick={() => navigate('/subscribe/new')}>新建订阅</Button>
309
+ </Space>
310
+ }
311
+ >
312
+ <Alert
313
+ type="info"
314
+ showIcon
315
+ style={{marginBottom: 16}}
316
+ message="订阅中心负责把三方 / 内部 / RSS 等数据源按 Cron 定时抽取, 归一化后落到本地, 供 CEO 立项和 Agent 协作使用."
317
+ />
318
+ <Table
319
+ rowKey="id"
320
+ columns={columns}
321
+ dataSource={data}
322
+ loading={loading}
323
+ scroll={{x: 1400}}
324
+ size="middle"
325
+ pagination={{pageSize: 20, showSizeChanger: true, showTotal: t => `共 ${t} 条`}}
326
+ />
327
+ </Card>
328
+
329
+ {/* dry-run 详情 Drawer */}
330
+ <Drawer
331
+ title={detail?.sub ? `试运行 - ${detail.sub.name}` : '运行历史'}
332
+ open={!!detail}
333
+ onClose={() => setDetail(null)}
334
+ width={900}
335
+ >
336
+ {detail?.tab === 'runs' ? (
337
+ <Tabs
338
+ activeKey={activeRunId ? 'items' : 'list'}
339
+ onChange={(k) => k === 'list' && setActiveRunId(null)}
340
+ items={[
341
+ {
342
+ key: 'list',
343
+ label: `运行列表 (${runs.length})`,
344
+ children: (
345
+ <Table
346
+ rowKey="id"
347
+ size="small"
348
+ dataSource={runs}
349
+ pagination={false}
350
+ onRow={(r) => ({onClick: () => loadRunItems(r.id)})}
351
+ columns={[
352
+ {title: 'runId', dataIndex: 'id', width: 70},
353
+ {title: '计划', dataIndex: 'scheduledAt', width: 160,
354
+ render: t => t ? new Date(t).toLocaleString() : '-'},
355
+ {title: '耗时(ms)', dataIndex: 'durationMs', width: 90},
356
+ {title: '状态', dataIndex: 'status', width: 90,
357
+ render: s => <Tag color={RUN_STATUS_TAG[s]?.color}>{s}</Tag>},
358
+ {title: '拉取/成功/重复/死信', width: 200,
359
+ render: (_, r) => `${r.fetchedCount} / ${r.successCount} / ${r.duplicateCount || 0} / ${r.deadLetterCount}`},
360
+ {title: '错误', dataIndex: 'errorMessage',
361
+ render: e => e ? <span style={{color: '#cf1322', fontSize: 12}}>{e}</span> : '-'},
362
+ ]}
363
+ />
364
+ ),
365
+ },
366
+ {
367
+ key: 'items',
368
+ label: activeRunId ? `run#${activeRunId} 抽取项` : '抽取项',
369
+ disabled: !activeRunId,
370
+ children: (
371
+ <Space orientation="vertical" style={{width: '100%'}}>
372
+ <Space>
373
+ <Tag color="blue">已选 {selectedItemIds.length} 条</Tag>
374
+ <Button size="small" type="primary"
375
+ icon={<ProjectOutlined/>}
376
+ disabled={selectedItemIds.length === 0}
377
+ onClick={() => {
378
+ const ids = selectedItemIds.join(',')
379
+ navigate(`/project/new?subscriptionId=${detail.sub.id}&itemIds=${ids}`)
380
+ }}>
381
+ 用所选 {selectedItemIds.length} 条立项
382
+ </Button>
383
+ </Space>
384
+ <Table
385
+ rowKey="id"
386
+ size="small"
387
+ dataSource={runItems[activeRunId] || []}
388
+ pagination={{pageSize: 20}}
389
+ rowSelection={{
390
+ selectedRowKeys: selectedItemIds,
391
+ onChange: setSelectedItemIds,
392
+ }}
393
+ columns={[
394
+ {title: 'id', dataIndex: 'dedupKey', width: 180, ellipsis: true},
395
+ {title: 'title', dataIndex: 'normalized',
396
+ render: (n) => {
397
+ try { const o = JSON.parse(n); return o.title || '(无标题)' }
398
+ catch { return n }
399
+ }},
400
+ {title: 'link', dataIndex: 'normalized',
401
+ render: (n) => {
402
+ try { const o = JSON.parse(n); return o.link ? <a href={o.link} target="_blank">↗</a> : '-' }
403
+ catch { return '-' }
404
+ }},
405
+ {title: '状态', dataIndex: 'status', width: 110,
406
+ render: s => <Tag>{s}</Tag>},
407
+ ]}
408
+ />
409
+ </Space>
410
+ ),
411
+ },
412
+ ]}
413
+ />
414
+ ) : (
415
+ <DryRunResultView result={detail?.result}/>
416
+ )}
417
+ </Drawer>
418
+ </div>
419
+ )
420
+ }
421
+
422
+ function DryRunResultView({result}) {
423
+ if (!result) return null
424
+ if (!result.success) {
425
+ return <Alert type="error" showIcon message="试运行失败" description={result.errorMessage || '未知错误'}/>
426
+ }
427
+ return (
428
+ <Space orientation="vertical" style={{width: '100%'}} size="middle">
429
+ <Card size="small" title="调用结果">
430
+ <Space size="large" wrap>
431
+ <span><b>HTTP:</b> {result.httpStatus || '-'}</span>
432
+ <span><b>耗时:</b> {result.latencyMs}ms</span>
433
+ <span><b>大小:</b> {result.responseSize}B</span>
434
+ <span><b>样本:</b> {result.sampleCount} 条</span>
435
+ </Space>
436
+ </Card>
437
+ {result.firstItem && (
438
+ <Card size="small" title="第一条样本 (归一化后)">
439
+ <pre style={{background: '#f5f5f5', padding: 12, borderRadius: 4, maxHeight: 300, overflow: 'auto'}}>
440
+ {JSON.stringify(result.firstItem, null, 2)}
441
+ </pre>
442
+ </Card>
443
+ )}
444
+ {result.responsePreview && (
445
+ <Card size="small" title="响应预览 (前 4KB)">
446
+ <pre style={{background: '#f5f5f5', padding: 12, borderRadius: 4, maxHeight: 300, overflow: 'auto', fontSize: 12}}>
447
+ {result.responsePreview}
448
+ </pre>
449
+ </Card>
450
+ )}
451
+ {result.fieldErrors && result.fieldErrors.length > 0 && (
452
+ <Alert type="warning" showIcon message="字段问题" description={
453
+ <ul>{result.fieldErrors.map((e, i) => <li key={i}>{e}</li>)}</ul>
454
+ }/>
455
+ )}
456
+ </Space>
457
+ )
458
+ }
459
+
460
+ export default SubscriptionList
@@ -0,0 +1,76 @@
1
+ /**
2
+ * 兼容垫片: 之前在 z-opc-main-starter-frontend/services/ 里的所有 axios 客户端
3
+ * 这里直接 re-export z-frontend-common 的 utils/request, 并提供 makeApi 工具.
4
+ */
5
+ import request, {authRequest, setupInterceptors} from '../utils/request'
6
+ export {request as default, authRequest, setupInterceptors}
7
+
8
+ function makeApi(name) {
9
+ return {
10
+ list: (params) => request.get(`/${name}/list`, {params}).then(r => r.data),
11
+ page: (params) => request.get(`/${name}/page`, {params}).then(r => r.data),
12
+ get: (id) => request.get(`/${name}/${id}`).then(r => r.data),
13
+ create: (data) => request.post(`/${name}`, data).then(r => r.data),
14
+ update: (id, data) => request.put(`/${name}/${id}`, data).then(r => r.data),
15
+ delete: (id) => request.delete(`/${name}/${id}`).then(r => r.data),
16
+ }
17
+ }
18
+
19
+ export const configApi = makeApi('config')
20
+ export const approvalApi = makeApi('approval')
21
+ export const designerApi = makeApi('designer')
22
+ export const mcpApi = makeApi('mcp')
23
+ export const mockPlatformApi = makeApi('mock')
24
+ export const namingApi = makeApi('naming')
25
+ export const opsApi = makeApi('ops')
26
+ export const ossApi = makeApi('oss')
27
+ export const scriptApi = makeApi('script')
28
+ export const skillApi = makeApi('skill')
29
+ export const agentApi = makeApi('agent')
30
+ export const flowApi = makeApi('flow')
31
+ export const traceApi = makeApi('trace')
32
+ export const llmApi = makeApi('llm')
33
+ export const productApi = makeApi('product')
34
+ export const sceneApi = makeApi('scene')
35
+ export const ctcAcAccountApi = makeApi('ctc/ac/accounts')
36
+ export const ctcAcLoginLogApi = makeApi('ctc/ac/login-log')
37
+ export const ctcAcTenantApi = makeApi('ctc/ac/tenants')
38
+ export const ctcAcDomainApi = makeApi('ctc/ac/domains')
39
+ export const ctcAcOrgApi = makeApi('ctc/ac/orgs')
40
+ export const ctcAcDeptApi = makeApi('ctc/ac/depts')
41
+ export const ctcAcGroupApi = makeApi('ctc/ac/groups')
42
+ export const ctcAuthorizationApi = makeApi('ctc/authorization')
43
+ export const metaAppApi = makeApi('meta-app')
44
+ export const jobApi = makeApi('schedule/job')
45
+ export const webideApi = makeApi('webide')
46
+ export const privateConfigApi = makeApi('private-config')
47
+ export const ctcSurlApi = makeApi('ctc/surl')
48
+ export const ctcUserApi = makeApi('ctc/user')
49
+ export const agentTeamApi = makeApi('agent/team')
50
+ export const workspaceApi = makeApi('agent/team/workspace')
51
+
52
+ // === 项目 / 任务 / 鉴权 ===
53
+ export const login = (data) => request.post('/ctc/auth/login', data).then(r => r.data)
54
+ export const getCurrentUser = () => {
55
+ try { return Promise.resolve(JSON.parse(localStorage.getItem('userInfo') || 'null')) }
56
+ catch { return Promise.resolve(null) }
57
+ }
58
+ export const switchTenant = (tenantCode, domainCode) =>
59
+ authRequest.post('/ctc/auth/switch-tenant', {tenantCode: tenantCode || '', domainCode: domainCode || ''}).then(r => r.data)
60
+ export const listMyProjects = () => authRequest.get('/project/user/list').then(r => r.data)
61
+ export const createProject = (data) => request.post('/project', data).then(r => r.data)
62
+ export const updateProject = (id, data) => request.put(`/project/${id}`, data).then(r => r.data)
63
+ export const archiveProject = (id) => request.put(`/project/${id}/archive`).then(r => r.data)
64
+ export const listTasksByProject = (projectId) => request.get('/task/project/list', {params: {projectId}}).then(r => r.data)
65
+ export const createTask = (data) => request.post('/task', data).then(r => r.data)
66
+ export const completeTask = (id) => request.post('/task/complete', null, {params: {taskId: id}}).then(r => r.data)
67
+ export const moveTask = (taskId, targetListId, position) =>
68
+ request.put('/task/move', null, {params: {taskId, targetListId, position}}).then(r => r.data)
69
+ export const listTasksByList = (listId) => request.get('/task/list', {params: {listId}}).then(r => r.data)
70
+ export const getDomainByTenantCode = async () => [{domainCode: 'default', domainName: '默认域'}]
71
+ export const getTenantList = async () => []
72
+ export const getDynamicMenu = async () => []
73
+ export const userOrgRelApi = {
74
+ usersByDept: () => Promise.resolve([]),
75
+ usersByGroup: () => Promise.resolve([]),
76
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 兼容垫片: 从 @yuku123/z-frontend-common re-export
3
+ * 历史上各业务页面的 import 路径是 '../../../utils/request',保留这个 shim.
4
+ */
5
+ export {request as default, authRequest, ctcRequest, createRequest, setupInterceptors} from '@yuku123/z-frontend-common'