@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.
- package/LICENSE +25 -0
- package/README.md +55 -42
- package/dist/index.js +1 -0
- package/package.json +49 -44
- package/src/components/ErrorBoundary/index.jsx +24 -77
- package/src/components/Layout/index.jsx +141 -38
- package/src/components/LoginPage/index.jsx +47 -21
- package/src/components/StatusTag/index.jsx +14 -12
- package/src/index.js +32 -38
- package/src/services/request.js +74 -4
- package/dist/z-frontend-common.css +0 -1
- package/dist/z-frontend-common.es.js +0 -51275
- package/dist/z-frontend-common.umd.js +0 -459
- package/src/components/Ctc/Layout.jsx +0 -330
- package/src/components/Ctc/Layout.module.css +0 -145
- package/src/components/Ctc/agentTeam/index.tsx +0 -308
- package/src/components/Ctc/login/AuthPage.module.css +0 -26
- package/src/components/Ctc/login/AuthPage.tsx +0 -235
- package/src/components/Ctc/login/index.less +0 -49
- package/src/components/Ctc/login/index.tsx +0 -142
- package/src/components/Ctc/uc/UCLogin.tsx +0 -31
- package/src/components/Ctc/uc/UCPhoneLogin.tsx +0 -62
- package/src/components/Ctc/uc/UCRegister.tsx +0 -37
- package/src/components/Ctc/uc/UCResetPassword.tsx +0 -69
- package/src/components/Ctc/uc/index.tsx +0 -15
- package/src/components/Ctc/userPanel/index.tsx +0 -999
- package/src/components/Ctc/webide/index.tsx +0 -272
- package/src/components/LowCode/LowCodeModel.jsx +0 -962
- package/src/components/LowCode/LowCodePage.jsx +0 -31
- package/src/components/LowCode/LowCodeRuntime.jsx +0 -335
- package/src/components/LowCode/MaterializePage.jsx +0 -235
- package/src/components/LowCode/index.js +0 -1
- package/src/components/MockPlatform/CurlImportModal.jsx +0 -362
- package/src/components/MockPlatform/EndpointsTab.jsx +0 -509
- package/src/components/MockPlatform/EnvironmentsTab.jsx +0 -212
- package/src/components/MockPlatform/MockTemplateHelper.jsx +0 -200
- package/src/components/MockPlatform/OpenApiImportModal.jsx +0 -305
- package/src/components/MockPlatform/RecordingsTab.jsx +0 -397
- package/src/components/MockPlatform/RequestLogsTab.jsx +0 -239
- package/src/components/MockPlatform/ScenariosTab.jsx +0 -236
- package/src/components/MockPlatform/TestCasesTab.jsx +0 -462
- package/src/components/MockPlatform/index.jsx +0 -127
- package/src/components/Overview.jsx +0 -74
- package/src/dev.jsx +0 -28
- package/src/services/agentTeam.js +0 -7
- package/src/services/api.js +0 -84
- package/src/services/ctcAc.js +0 -7
- package/src/services/ctcAcDomain.js +0 -7
- package/src/services/ctcAuthorization.js +0 -7
- package/src/services/ctcSurl.js +0 -7
- package/src/services/ctcUser.js +0 -7
- package/src/services/job.js +0 -7
- package/src/services/metaApp.js +0 -7
- package/src/services/privateConfig.js +0 -7
- package/src/services/webide.js +0 -6
- package/src/services/workspace.js +0 -21
- package/src/utils/request.ts +0 -102
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
import {useEffect, useState} from 'react'
|
|
2
|
-
import {
|
|
3
|
-
Alert,
|
|
4
|
-
Button,
|
|
5
|
-
Checkbox,
|
|
6
|
-
Descriptions,
|
|
7
|
-
Drawer,
|
|
8
|
-
Form,
|
|
9
|
-
Input,
|
|
10
|
-
message,
|
|
11
|
-
Modal,
|
|
12
|
-
Popconfirm,
|
|
13
|
-
Space,
|
|
14
|
-
Table,
|
|
15
|
-
Tabs,
|
|
16
|
-
Tag,
|
|
17
|
-
Typography
|
|
18
|
-
} from 'antd'
|
|
19
|
-
import {
|
|
20
|
-
CheckCircleOutlined,
|
|
21
|
-
DeleteOutlined,
|
|
22
|
-
ExportOutlined,
|
|
23
|
-
ImportOutlined,
|
|
24
|
-
PlayCircleOutlined,
|
|
25
|
-
ReloadOutlined,
|
|
26
|
-
StopOutlined,
|
|
27
|
-
VideoCameraOutlined
|
|
28
|
-
} from '@ant-design/icons'
|
|
29
|
-
import {mockPlatformApi} from '@/services/api'
|
|
30
|
-
|
|
31
|
-
const {Text} = Typography
|
|
32
|
-
|
|
33
|
-
const STATUS_COLORS = {
|
|
34
|
-
IDLE: 'default', RECORDING: 'red', STOPPED: 'green', EXPIRED: 'gray',
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* 录制回放 Tab - 录制真实流量,转换为 Mock 集合
|
|
39
|
-
*/
|
|
40
|
-
export default function RecordingsTab() {
|
|
41
|
-
const [recordings, setRecordings] = useState([])
|
|
42
|
-
const [loading, setLoading] = useState(false)
|
|
43
|
-
const [startOpen, setStartOpen] = useState(false)
|
|
44
|
-
const [playbackOpen, setPlaybackOpen] = useState(false)
|
|
45
|
-
const [exportOpen, setExportOpen] = useState(false)
|
|
46
|
-
const [detailOpen, setDetailOpen] = useState(false)
|
|
47
|
-
const [detail, setDetail] = useState(null)
|
|
48
|
-
const [detailRequests, setDetailRequests] = useState([])
|
|
49
|
-
const [form] = Form.useForm()
|
|
50
|
-
const [playbackForm] = Form.useForm()
|
|
51
|
-
const [exportForm] = Form.useForm()
|
|
52
|
-
const [exportDrafts, setExportDrafts] = useState([])
|
|
53
|
-
const [exportLoading, setExportLoading] = useState(false)
|
|
54
|
-
const [selectedDrafts, setSelectedDrafts] = useState([])
|
|
55
|
-
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
loadRecordings()
|
|
58
|
-
}, [])
|
|
59
|
-
|
|
60
|
-
const loadRecordings = async () => {
|
|
61
|
-
setLoading(true)
|
|
62
|
-
try {
|
|
63
|
-
const res = await mockPlatformApi.recordings.list()
|
|
64
|
-
setRecordings(Array.isArray(res) ? res : [])
|
|
65
|
-
} catch (e) {
|
|
66
|
-
message.warning('加载录制列表失败')
|
|
67
|
-
} finally {
|
|
68
|
-
setLoading(false)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const handleStart = async () => {
|
|
73
|
-
try {
|
|
74
|
-
const values = await form.validateFields()
|
|
75
|
-
await mockPlatformApi.recordings.start(values.recordingCode, values.recordingName, values.targetUrl)
|
|
76
|
-
message.success('录制已开启')
|
|
77
|
-
setStartOpen(false)
|
|
78
|
-
form.resetFields()
|
|
79
|
-
loadRecordings()
|
|
80
|
-
} catch (e) {
|
|
81
|
-
if (!e?.errorFields) message.error('开启失败')
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const handleStop = async (code) => {
|
|
86
|
-
try {
|
|
87
|
-
await mockPlatformApi.recordings.stop(code)
|
|
88
|
-
message.success('录制已停止')
|
|
89
|
-
loadRecordings()
|
|
90
|
-
} catch (e) {
|
|
91
|
-
message.error('停止失败')
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const handleDelete = async (code) => {
|
|
96
|
-
try {
|
|
97
|
-
await mockPlatformApi.recordings.delete(code)
|
|
98
|
-
message.success('删除成功')
|
|
99
|
-
loadRecordings()
|
|
100
|
-
} catch (e) {
|
|
101
|
-
message.error('删除失败')
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const handleDetail = async (record) => {
|
|
106
|
-
try {
|
|
107
|
-
const requests = await mockPlatformApi.recordings.requests(record.recordingCode)
|
|
108
|
-
setDetail(record)
|
|
109
|
-
setDetailRequests(Array.isArray(requests) ? requests : [])
|
|
110
|
-
setDetailOpen(true)
|
|
111
|
-
} catch (e) {
|
|
112
|
-
message.error('加载失败')
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const handleImport = async (record) => {
|
|
117
|
-
Modal.confirm({
|
|
118
|
-
title: '导入录制为 Mock',
|
|
119
|
-
content: `将把录制 "${record.recordingCode}" 转换为 Mock endpoints 并入库。`,
|
|
120
|
-
okText: '确认导入',
|
|
121
|
-
onOk: async () => {
|
|
122
|
-
try {
|
|
123
|
-
const res = await mockPlatformApi.recordings.import(record.recordingCode, 'default')
|
|
124
|
-
message.success(`导入成功: ${res?.imported || 0} 个 Mock endpoint`)
|
|
125
|
-
} catch (e) {
|
|
126
|
-
message.error('导入失败')
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const handlePlayback = async () => {
|
|
133
|
-
try {
|
|
134
|
-
const values = await playbackForm.validateFields()
|
|
135
|
-
const res = await mockPlatformApi.recordings.playback(detail.recordingCode, values.targetUrl)
|
|
136
|
-
Modal.info({
|
|
137
|
-
title: '回放结果',
|
|
138
|
-
width: 700,
|
|
139
|
-
content: (
|
|
140
|
-
<div>
|
|
141
|
-
<Text>总数: {res?.[0]?.total || 0} 通过: {res?.[0]?.pass || 0} 失败: {res?.[0]?.fail || 0}</Text>
|
|
142
|
-
<pre style={{
|
|
143
|
-
marginTop: 12,
|
|
144
|
-
background: '#f5f5f5',
|
|
145
|
-
padding: 12,
|
|
146
|
-
maxHeight: 400,
|
|
147
|
-
overflow: 'auto',
|
|
148
|
-
fontSize: 12
|
|
149
|
-
}}>
|
|
150
|
-
{JSON.stringify(res, null, 2)}
|
|
151
|
-
</pre>
|
|
152
|
-
</div>
|
|
153
|
-
)
|
|
154
|
-
})
|
|
155
|
-
setPlaybackOpen(false)
|
|
156
|
-
} catch (e) {
|
|
157
|
-
if (!e?.errorFields) message.error('回放失败')
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const handleOpenExport = async () => {
|
|
162
|
-
try {
|
|
163
|
-
setExportLoading(true)
|
|
164
|
-
// 先取项目编码(可空)
|
|
165
|
-
const values = await exportForm.validateFields().catch(() => ({}))
|
|
166
|
-
const res = await mockPlatformApi.recordings.export(detail.recordingCode, values?.projectCode)
|
|
167
|
-
const drafts = Array.isArray(res?.drafts) ? res.drafts : []
|
|
168
|
-
setExportDrafts(drafts)
|
|
169
|
-
setSelectedDrafts(drafts.map(d => d.mockCode || d.id))
|
|
170
|
-
setExportOpen(true)
|
|
171
|
-
} catch (e) {
|
|
172
|
-
if (!e?.errorFields) message.error('生成草稿失败')
|
|
173
|
-
} finally {
|
|
174
|
-
setExportLoading(false)
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const handleConfirmImport = async () => {
|
|
179
|
-
try {
|
|
180
|
-
const values = await exportForm.validateFields().catch(() => ({}))
|
|
181
|
-
const codes = selectedDrafts
|
|
182
|
-
if (codes.length === 0) {
|
|
183
|
-
message.warning('请至少选择一个 Mock')
|
|
184
|
-
return
|
|
185
|
-
}
|
|
186
|
-
const res = await mockPlatformApi.recordings.import(
|
|
187
|
-
detail.recordingCode,
|
|
188
|
-
values?.envCode,
|
|
189
|
-
values?.projectCode
|
|
190
|
-
)
|
|
191
|
-
message.success(`已导入 ${res?.imported || codes.length} 个 Mock`)
|
|
192
|
-
setExportOpen(false)
|
|
193
|
-
loadRecordings()
|
|
194
|
-
} catch (e) {
|
|
195
|
-
message.error('导入失败:' + (e?.message || '未知错误'))
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const columns = [
|
|
200
|
-
{
|
|
201
|
-
title: '编码', dataIndex: 'recordingCode', key: 'recordingCode', width: 180,
|
|
202
|
-
render: (v) => <Text code>{v}</Text>
|
|
203
|
-
},
|
|
204
|
-
{title: '名称', dataIndex: 'recordingName', key: 'recordingName', width: 200},
|
|
205
|
-
{
|
|
206
|
-
title: '状态', dataIndex: 'recordStatus', key: 'recordStatus', width: 110,
|
|
207
|
-
render: (v) => <Tag color={STATUS_COLORS[v]}>{v}</Tag>
|
|
208
|
-
},
|
|
209
|
-
{title: '请求数', dataIndex: 'totalRequests', key: 'totalRequests', width: 90, align: 'center'},
|
|
210
|
-
{
|
|
211
|
-
title: '目标 URL', dataIndex: 'targetUrl', key: 'targetUrl', ellipsis: true,
|
|
212
|
-
render: (v) => v ? <Text copyable={{text: v}}>{v}</Text> : '-'
|
|
213
|
-
},
|
|
214
|
-
{title: '开始时间', dataIndex: 'startTime', key: 'startTime', width: 170},
|
|
215
|
-
{title: '结束时间', dataIndex: 'endTime', key: 'endTime', width: 170},
|
|
216
|
-
{
|
|
217
|
-
title: '操作', key: 'action', width: 280, fixed: 'right',
|
|
218
|
-
render: (_, record) => (
|
|
219
|
-
<Space size="small">
|
|
220
|
-
<Button size="small" type="link" icon={<VideoCameraOutlined/>}
|
|
221
|
-
onClick={() => handleDetail(record)}>详情</Button>
|
|
222
|
-
{record.recordStatus === 'RECORDING' && (
|
|
223
|
-
<Button size="small" type="link" danger icon={<StopOutlined/>}
|
|
224
|
-
onClick={() => handleStop(record.recordingCode)}>停止</Button>
|
|
225
|
-
)}
|
|
226
|
-
{record.recordStatus === 'STOPPED' && (
|
|
227
|
-
<Button size="small" type="link" icon={<ImportOutlined/>}
|
|
228
|
-
onClick={() => handleImport(record)}>导入为 Mock</Button>
|
|
229
|
-
)}
|
|
230
|
-
<Popconfirm title={`删除录制 ${record.recordingCode}?`}
|
|
231
|
-
onConfirm={() => handleDelete(record.recordingCode)}>
|
|
232
|
-
<Button size="small" type="link" danger icon={<DeleteOutlined/>}/>
|
|
233
|
-
</Popconfirm>
|
|
234
|
-
</Space>
|
|
235
|
-
)
|
|
236
|
-
},
|
|
237
|
-
]
|
|
238
|
-
|
|
239
|
-
return (
|
|
240
|
-
<div>
|
|
241
|
-
<Alert type="info" showIcon style={{marginBottom: 16}}
|
|
242
|
-
title="流量录制与回放"
|
|
243
|
-
description={
|
|
244
|
-
<div>
|
|
245
|
-
<div>• 开启录制后,所有 <Text code>/api/mock/{envCode}/**</Text> 请求会被捕获到数据库
|
|
246
|
-
</div>
|
|
247
|
-
<div>• 停止录制后,可一键转为 Mock endpoints(用于离线测试)</div>
|
|
248
|
-
<div>• 支持回放录制到目标服务(验证录制有效 / 跨环境验证)</div>
|
|
249
|
-
</div>
|
|
250
|
-
}
|
|
251
|
-
/>
|
|
252
|
-
|
|
253
|
-
<Space style={{marginBottom: 16}}>
|
|
254
|
-
<Button icon={<ReloadOutlined/>} onClick={loadRecordings}>刷新</Button>
|
|
255
|
-
<Button type="primary" icon={<VideoCameraOutlined/>}
|
|
256
|
-
onClick={() => setStartOpen(true)}>开启录制</Button>
|
|
257
|
-
</Space>
|
|
258
|
-
|
|
259
|
-
<Table
|
|
260
|
-
dataSource={recordings}
|
|
261
|
-
columns={columns}
|
|
262
|
-
rowKey="id"
|
|
263
|
-
loading={loading}
|
|
264
|
-
pagination={{pageSize: 10}}
|
|
265
|
-
scroll={{x: 1300}}
|
|
266
|
-
size="small"
|
|
267
|
-
/>
|
|
268
|
-
|
|
269
|
-
<Modal
|
|
270
|
-
title="开启录制"
|
|
271
|
-
open={startOpen}
|
|
272
|
-
onCancel={() => setStartOpen(false)}
|
|
273
|
-
onOk={handleStart}
|
|
274
|
-
okText="开启" cancelText="取消"
|
|
275
|
-
>
|
|
276
|
-
<Form form={form} layout="vertical" style={{marginTop: 16}}>
|
|
277
|
-
<Form.Item name="recordingCode" label="录制编码" rules={[{required: true}]}>
|
|
278
|
-
<Input placeholder="prod-trace-2026-06"/>
|
|
279
|
-
</Form.Item>
|
|
280
|
-
<Form.Item name="recordingName" label="录制名称" rules={[{required: true}]}>
|
|
281
|
-
<Input placeholder="生产环境订单流程"/>
|
|
282
|
-
</Form.Item>
|
|
283
|
-
<Form.Item name="targetUrl" label="目标服务地址">
|
|
284
|
-
<Input placeholder="http://prod-api.example.com"/>
|
|
285
|
-
</Form.Item>
|
|
286
|
-
</Form>
|
|
287
|
-
</Modal>
|
|
288
|
-
|
|
289
|
-
<Drawer
|
|
290
|
-
title={<Space><VideoCameraOutlined/>录制详情: {detail?.recordingCode}</Space>}
|
|
291
|
-
open={detailOpen}
|
|
292
|
-
onClose={() => setDetailOpen(false)}
|
|
293
|
-
width={800}
|
|
294
|
-
>
|
|
295
|
-
{detail && (
|
|
296
|
-
<div>
|
|
297
|
-
<Descriptions column={2} size="small" bordered style={{marginBottom: 16}}>
|
|
298
|
-
<Descriptions.Item label="编码">{detail.recordingCode}</Descriptions.Item>
|
|
299
|
-
<Descriptions.Item label="名称">{detail.recordingName}</Descriptions.Item>
|
|
300
|
-
<Descriptions.Item label="状态">
|
|
301
|
-
<Tag color={STATUS_COLORS[detail.recordStatus]}>{detail.recordStatus}</Tag>
|
|
302
|
-
</Descriptions.Item>
|
|
303
|
-
<Descriptions.Item label="请求数">{detail.totalRequests}</Descriptions.Item>
|
|
304
|
-
<Descriptions.Item label="目标 URL" span={2}>
|
|
305
|
-
<Text copyable>{detail.targetUrl}</Text>
|
|
306
|
-
</Descriptions.Item>
|
|
307
|
-
<Descriptions.Item label="开始">{detail.startTime}</Descriptions.Item>
|
|
308
|
-
<Descriptions.Item label="结束">{detail.endTime || '(录制中)'}</Descriptions.Item>
|
|
309
|
-
</Descriptions>
|
|
310
|
-
|
|
311
|
-
<Space style={{marginBottom: 16}}>
|
|
312
|
-
<Button icon={<PlayCircleOutlined/>} onClick={() => setPlaybackOpen(true)}
|
|
313
|
-
disabled={detail.recordStatus !== 'STOPPED'}>回放</Button>
|
|
314
|
-
<Button icon={<ExportOutlined/>} onClick={handleOpenExport} loading={exportLoading}
|
|
315
|
-
disabled={detail.recordStatus !== 'STOPPED'}>导出 Mock 草稿</Button>
|
|
316
|
-
<Button type="primary" icon={<ImportOutlined/>} onClick={() => handleImport(detail)}
|
|
317
|
-
disabled={detail.recordStatus !== 'STOPPED'}>导入为 Mock</Button>
|
|
318
|
-
</Space>
|
|
319
|
-
|
|
320
|
-
<Text strong>请求列表 (共 {detailRequests.length} 条):</Text>
|
|
321
|
-
<Table
|
|
322
|
-
style={{marginTop: 8}}
|
|
323
|
-
dataSource={detailRequests}
|
|
324
|
-
columns={[
|
|
325
|
-
{title: '#', dataIndex: 'sequence', key: 'sequence', width: 50},
|
|
326
|
-
{
|
|
327
|
-
title: '方法', dataIndex: 'requestMethod', key: 'requestMethod', width: 80,
|
|
328
|
-
render: (v) => <Tag color="blue">{v}</Tag>
|
|
329
|
-
},
|
|
330
|
-
{
|
|
331
|
-
title: 'URL', dataIndex: 'requestUrl', key: 'requestUrl', ellipsis: true,
|
|
332
|
-
render: (v) => <Text code>{v}</Text>
|
|
333
|
-
},
|
|
334
|
-
{
|
|
335
|
-
title: '响应', dataIndex: 'responseStatus', key: 'responseStatus', width: 80,
|
|
336
|
-
render: (v) => <Tag color={v >= 400 ? 'red' : 'green'}>{v}</Tag>
|
|
337
|
-
},
|
|
338
|
-
{
|
|
339
|
-
title: '耗时', dataIndex: 'responseTime', key: 'responseTime', width: 80,
|
|
340
|
-
render: (v) => `${v || 0}ms`
|
|
341
|
-
},
|
|
342
|
-
]}
|
|
343
|
-
pagination={{pageSize: 10}} size="small" rowKey="id"
|
|
344
|
-
/>
|
|
345
|
-
</div>
|
|
346
|
-
)}
|
|
347
|
-
</Drawer>
|
|
348
|
-
|
|
349
|
-
<Modal title="回放录制" open={playbackOpen}
|
|
350
|
-
onCancel={() => setPlaybackOpen(false)} onOk={handlePlayback}>
|
|
351
|
-
<Form form={playbackForm} layout="vertical" style={{marginTop: 16}}>
|
|
352
|
-
<Form.Item name="targetUrl" label="目标服务地址" rules={[{required: true}]}>
|
|
353
|
-
<Input placeholder="http://staging-api.example.com"/>
|
|
354
|
-
</Form.Item>
|
|
355
|
-
</Form>
|
|
356
|
-
</Modal>
|
|
357
|
-
|
|
358
|
-
<Modal title="导出 Mock 草稿" open={exportOpen}
|
|
359
|
-
onCancel={() => setExportOpen(false)} onOk={handleConfirmImport}
|
|
360
|
-
okText={`导入 ${selectedDrafts.length} 个`} width={900}>
|
|
361
|
-
<Form form={exportForm} layout="vertical" style={{marginTop: 8}}>
|
|
362
|
-
<Space size={16} style={{width: '100%'}}>
|
|
363
|
-
<Form.Item name="projectCode" label="项目编码" style={{flex: 1, marginBottom: 8}}>
|
|
364
|
-
<Input placeholder="default"/>
|
|
365
|
-
</Form.Item>
|
|
366
|
-
<Form.Item name="envCode" label="目标环境" style={{flex: 1, marginBottom: 8}}>
|
|
367
|
-
<Input placeholder="default"/>
|
|
368
|
-
</Form.Item>
|
|
369
|
-
</Space>
|
|
370
|
-
</Form>
|
|
371
|
-
<Alert type="info" showIcon style={{marginBottom: 12}}
|
|
372
|
-
title={`从录制中识别出 ${exportDrafts.length} 个去重后的 Mock endpoint,已默认全选`}/>
|
|
373
|
-
<Table
|
|
374
|
-
size="small"
|
|
375
|
-
dataSource={exportDrafts}
|
|
376
|
-
rowKey={r => r.mockCode || r.id}
|
|
377
|
-
pagination={{pageSize: 8}}
|
|
378
|
-
scroll={{y: 300}}
|
|
379
|
-
rowSelection={{
|
|
380
|
-
selectedRowKeys: selectedDrafts,
|
|
381
|
-
onChange: (keys) => setSelectedDrafts(keys),
|
|
382
|
-
}}
|
|
383
|
-
columns={[
|
|
384
|
-
{title: '方法', dataIndex: 'method', width: 80,
|
|
385
|
-
render: v => <Tag color="blue">{v}</Tag>},
|
|
386
|
-
{title: 'URL 模式', dataIndex: 'urlPattern', ellipsis: true,
|
|
387
|
-
render: v => <Text code>{v}</Text>},
|
|
388
|
-
{title: '状态码', dataIndex: 'statusCode', width: 80},
|
|
389
|
-
{title: '响应模板', dataIndex: 'responseTemplate', ellipsis: true,
|
|
390
|
-
render: v => <Text type="secondary" style={{fontSize: 11}}>{(v || '').slice(0, 80)}</Text>},
|
|
391
|
-
{title: '请求数', dataIndex: 'requestCount', width: 80, align: 'center'},
|
|
392
|
-
]}
|
|
393
|
-
/>
|
|
394
|
-
</Modal>
|
|
395
|
-
</div>
|
|
396
|
-
)
|
|
397
|
-
}
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import {useEffect, useState} from 'react'
|
|
2
|
-
import {
|
|
3
|
-
Alert,
|
|
4
|
-
Button,
|
|
5
|
-
Card,
|
|
6
|
-
Col,
|
|
7
|
-
Descriptions,
|
|
8
|
-
Drawer,
|
|
9
|
-
Input,
|
|
10
|
-
message,
|
|
11
|
-
Popconfirm,
|
|
12
|
-
Row,
|
|
13
|
-
Select,
|
|
14
|
-
Space,
|
|
15
|
-
Statistic,
|
|
16
|
-
Table,
|
|
17
|
-
Tag,
|
|
18
|
-
Typography
|
|
19
|
-
} from 'antd'
|
|
20
|
-
import {DeleteOutlined, EyeOutlined, ReloadOutlined, SearchOutlined} from '@ant-design/icons'
|
|
21
|
-
import {mockPlatformApi} from '@/services/api'
|
|
22
|
-
|
|
23
|
-
const {Text} = Typography
|
|
24
|
-
|
|
25
|
-
const STATUS_COLORS = {MOCK: 'green', PROXY: 'blue', '404': 'red', '502': 'orange'}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 请求日志 Tab - 查看每一次 Mock 请求/响应
|
|
29
|
-
*/
|
|
30
|
-
export default function RequestLogsTab() {
|
|
31
|
-
const [logs, setLogs] = useState([])
|
|
32
|
-
const [stats, setStats] = useState(null)
|
|
33
|
-
const [loading, setLoading] = useState(false)
|
|
34
|
-
const [detail, setDetail] = useState(null)
|
|
35
|
-
const [detailOpen, setDetailOpen] = useState(false)
|
|
36
|
-
const [filterPath, setFilterPath] = useState('')
|
|
37
|
-
const [filterMatched, setFilterMatched] = useState()
|
|
38
|
-
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
loadData()
|
|
41
|
-
}, [])
|
|
42
|
-
|
|
43
|
-
const loadData = async () => {
|
|
44
|
-
setLoading(true)
|
|
45
|
-
try {
|
|
46
|
-
const [logsRes, statsRes] = await Promise.all([
|
|
47
|
-
mockPlatformApi.requestLogs.list({
|
|
48
|
-
path: filterPath || undefined,
|
|
49
|
-
matched: filterMatched,
|
|
50
|
-
limit: 100,
|
|
51
|
-
}),
|
|
52
|
-
mockPlatformApi.requestLogs.stats().catch(() => null),
|
|
53
|
-
])
|
|
54
|
-
setLogs(Array.isArray(logsRes) ? logsRes : [])
|
|
55
|
-
setStats(statsRes)
|
|
56
|
-
} catch (e) {
|
|
57
|
-
message.warning('加载失败')
|
|
58
|
-
} finally {
|
|
59
|
-
setLoading(false)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const handleDetail = (record) => {
|
|
64
|
-
setDetail(record)
|
|
65
|
-
setDetailOpen(true)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const handleClear = async () => {
|
|
69
|
-
try {
|
|
70
|
-
await mockPlatformApi.requestLogs.clear(1000)
|
|
71
|
-
message.success('已清理')
|
|
72
|
-
loadData()
|
|
73
|
-
} catch (e) {
|
|
74
|
-
message.error('清理失败')
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const columns = [
|
|
79
|
-
{
|
|
80
|
-
title: '时间', dataIndex: 'createTime', key: 'createTime', width: 170,
|
|
81
|
-
render: (v) => v ? new Date(v).toLocaleString() : '-'
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
title: '环境', dataIndex: 'envCode', key: 'envCode', width: 100,
|
|
85
|
-
render: (v) => <Tag>{v}</Tag>
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
title: '方法', dataIndex: 'requestMethod', key: 'requestMethod', width: 80,
|
|
89
|
-
render: (v) => <Tag color="blue">{v}</Tag>
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
title: '路径', dataIndex: 'requestPath', key: 'requestPath', ellipsis: true,
|
|
93
|
-
render: (v) => <Text code>{v}</Text>
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
title: '状态', dataIndex: 'responseStatus', key: 'responseStatus', width: 90, align: 'center',
|
|
97
|
-
render: (v) => v ? <Tag color={v >= 400 ? 'red' : 'green'}>{v}</Tag> : '-'
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
title: '匹配', dataIndex: 'matched', key: 'matched', width: 90, align: 'center',
|
|
101
|
-
render: (v) => v === 1 ? <Tag color="green">命中</Tag> : <Tag color="red">未匹配</Tag>
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
title: '回退', dataIndex: 'fallbackType', key: 'fallbackType', width: 100,
|
|
105
|
-
render: (v) => v ? <Tag color={STATUS_COLORS[v]}>{v}</Tag> : '-'
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
title: '耗时', dataIndex: 'durationMs', key: 'durationMs', width: 80, align: 'center',
|
|
109
|
-
render: (v) => `${v || 0}ms`
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
title: 'Mock', dataIndex: 'mockCode', key: 'mockCode', width: 160, ellipsis: true,
|
|
113
|
-
render: (v) => v ? <Text code>{v}</Text> : '-'
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
title: '操作', key: 'action', width: 80,
|
|
117
|
-
render: (_, record) => (
|
|
118
|
-
<Button size="small" type="link" icon={<EyeOutlined/>}
|
|
119
|
-
onClick={() => handleDetail(record)}>详情</Button>
|
|
120
|
-
)
|
|
121
|
-
},
|
|
122
|
-
]
|
|
123
|
-
|
|
124
|
-
return (
|
|
125
|
-
<div>
|
|
126
|
-
<Alert type="info" showIcon style={{marginBottom: 16}}
|
|
127
|
-
title="Mock 请求日志 - 排查未匹配请求"
|
|
128
|
-
description="每一次请求都会留痕。包括请求/响应体、是否匹配 Mock、回退类型、耗时、场景状态等。便于排查“为什么这个请求没有 Mock 命中”问题。"
|
|
129
|
-
/>
|
|
130
|
-
|
|
131
|
-
{stats && (
|
|
132
|
-
<Row gutter={16} style={{marginBottom: 16}}>
|
|
133
|
-
<Col span={6}><Card><Statistic title="总请求数" value={stats.total || 0}/></Card></Col>
|
|
134
|
-
<Col span={6}><Card><Statistic title="Mock 命中" value={stats.matched || 0}
|
|
135
|
-
styles={{content: {color: '#52c41a'}}}/></Card></Col>
|
|
136
|
-
<Col span={6}><Card><Statistic title="未匹配" value={stats.unmatched || 0}
|
|
137
|
-
styles={{content: {color: '#ff4d4f'}}}/></Card></Col>
|
|
138
|
-
<Col span={6}><Card><Statistic title="命中率"
|
|
139
|
-
value={`${stats.total > 0 ? Math.round((stats.matched / stats.total) * 100) : 0}%`}/></Card></Col>
|
|
140
|
-
</Row>
|
|
141
|
-
)}
|
|
142
|
-
|
|
143
|
-
<Space style={{marginBottom: 16}}>
|
|
144
|
-
<Input
|
|
145
|
-
placeholder="搜索路径" prefix={<SearchOutlined/>} allowClear
|
|
146
|
-
style={{width: 240}} value={filterPath}
|
|
147
|
-
onChange={(e) => setFilterPath(e.target.value)}
|
|
148
|
-
onPressEnter={loadData}
|
|
149
|
-
/>
|
|
150
|
-
<Select
|
|
151
|
-
placeholder="匹配状态" allowClear style={{width: 140}}
|
|
152
|
-
value={filterMatched}
|
|
153
|
-
onChange={setFilterMatched}
|
|
154
|
-
options={[
|
|
155
|
-
{value: 1, label: '已命中'},
|
|
156
|
-
{value: 0, label: '未匹配'},
|
|
157
|
-
]}
|
|
158
|
-
/>
|
|
159
|
-
<Button icon={<ReloadOutlined/>} onClick={loadData}>查询</Button>
|
|
160
|
-
<Popconfirm title="确认清理,保留最近 1000 条?" onConfirm={handleClear}>
|
|
161
|
-
<Button icon={<DeleteOutlined/>} danger>清理</Button>
|
|
162
|
-
</Popconfirm>
|
|
163
|
-
</Space>
|
|
164
|
-
|
|
165
|
-
<Table
|
|
166
|
-
dataSource={logs}
|
|
167
|
-
columns={columns}
|
|
168
|
-
rowKey="id"
|
|
169
|
-
loading={loading}
|
|
170
|
-
pagination={{pageSize: 20}}
|
|
171
|
-
scroll={{x: 1300}}
|
|
172
|
-
size="small"
|
|
173
|
-
/>
|
|
174
|
-
|
|
175
|
-
<Drawer
|
|
176
|
-
title="请求详情"
|
|
177
|
-
open={detailOpen}
|
|
178
|
-
onClose={() => setDetailOpen(false)}
|
|
179
|
-
width={750}
|
|
180
|
-
>
|
|
181
|
-
{detail && (
|
|
182
|
-
<div>
|
|
183
|
-
<Descriptions column={2} size="small" bordered style={{marginBottom: 16}}>
|
|
184
|
-
<Descriptions.Item label="环境">{detail.envCode}</Descriptions.Item>
|
|
185
|
-
<Descriptions.Item label="方法"><Tag
|
|
186
|
-
color="blue">{detail.requestMethod}</Tag></Descriptions.Item>
|
|
187
|
-
<Descriptions.Item label="路径" span={2}><Text
|
|
188
|
-
code>{detail.requestPath}</Text></Descriptions.Item>
|
|
189
|
-
<Descriptions.Item label="状态码">
|
|
190
|
-
<Tag
|
|
191
|
-
color={detail.responseStatus >= 400 ? 'red' : 'green'}>{detail.responseStatus}</Tag>
|
|
192
|
-
</Descriptions.Item>
|
|
193
|
-
<Descriptions.Item label="耗时">{detail.durationMs}ms</Descriptions.Item>
|
|
194
|
-
<Descriptions.Item label="匹配">
|
|
195
|
-
{detail.matched === 1 ? <Tag color="green">命中</Tag> : <Tag color="red">未匹配</Tag>}
|
|
196
|
-
</Descriptions.Item>
|
|
197
|
-
<Descriptions.Item label="回退">{detail.fallbackType || '-'}</Descriptions.Item>
|
|
198
|
-
<Descriptions.Item label="Mock" span={2}>{detail.mockCode || '-'}</Descriptions.Item>
|
|
199
|
-
<Descriptions.Item label="场景状态"
|
|
200
|
-
span={2}>{detail.scenarioState || '-'}</Descriptions.Item>
|
|
201
|
-
<Descriptions.Item label="客户端 IP">{detail.clientIp || '-'}</Descriptions.Item>
|
|
202
|
-
<Descriptions.Item label="User-Agent" ellipsis>{detail.userAgent || '-'}</Descriptions.Item>
|
|
203
|
-
<Descriptions.Item label="时间" span={2}>
|
|
204
|
-
{detail.createTime ? new Date(detail.createTime).toLocaleString() : '-'}
|
|
205
|
-
</Descriptions.Item>
|
|
206
|
-
</Descriptions>
|
|
207
|
-
|
|
208
|
-
<Text strong>请求头:</Text>
|
|
209
|
-
<pre style={{
|
|
210
|
-
background: '#f5f5f5', padding: 12, borderRadius: 4,
|
|
211
|
-
maxHeight: 150, overflow: 'auto', fontSize: 12, fontFamily: 'monospace',
|
|
212
|
-
whiteSpace: 'pre-wrap',
|
|
213
|
-
}}>
|
|
214
|
-
{detail.requestHeaders || '(无)'}
|
|
215
|
-
</pre>
|
|
216
|
-
|
|
217
|
-
<Text strong style={{marginTop: 12, display: 'block'}}>请求体:</Text>
|
|
218
|
-
<pre style={{
|
|
219
|
-
background: '#f5f5f5', padding: 12, borderRadius: 4,
|
|
220
|
-
maxHeight: 200, overflow: 'auto', fontSize: 12, fontFamily: 'monospace',
|
|
221
|
-
whiteSpace: 'pre-wrap', wordBreak: 'break-all',
|
|
222
|
-
}}>
|
|
223
|
-
{detail.requestBody || '(无)'}
|
|
224
|
-
</pre>
|
|
225
|
-
|
|
226
|
-
<Text strong style={{marginTop: 12, display: 'block'}}>响应体:</Text>
|
|
227
|
-
<pre style={{
|
|
228
|
-
background: '#f5f5f5', padding: 12, borderRadius: 4,
|
|
229
|
-
maxHeight: 300, overflow: 'auto', fontSize: 12, fontFamily: 'monospace',
|
|
230
|
-
whiteSpace: 'pre-wrap', wordBreak: 'break-all',
|
|
231
|
-
}}>
|
|
232
|
-
{detail.responseBody || '(无)'}
|
|
233
|
-
</pre>
|
|
234
|
-
</div>
|
|
235
|
-
)}
|
|
236
|
-
</Drawer>
|
|
237
|
-
</div>
|
|
238
|
-
)
|
|
239
|
-
}
|