@yuku123/z-frontend-common 0.1.2 → 0.1.3

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 (43) hide show
  1. package/dist/z-frontend-common.css +1 -0
  2. package/dist/z-frontend-common.es.js +6153 -300
  3. package/dist/z-frontend-common.umd.js +22 -4
  4. package/package.json +5 -4
  5. package/src/components/Ctc/Layout.jsx +328 -0
  6. package/src/components/Ctc/Layout.module.css +145 -0
  7. package/src/components/Ctc/agentTeam/index.tsx +308 -0
  8. package/src/components/Ctc/login/AuthPage.module.css +26 -0
  9. package/src/components/Ctc/login/AuthPage.tsx +235 -0
  10. package/src/components/Ctc/login/index.less +49 -0
  11. package/src/components/Ctc/login/index.tsx +142 -0
  12. package/src/components/Ctc/userPanel/index.tsx +998 -0
  13. package/src/components/Ctc/webide/index.tsx +272 -0
  14. package/src/components/LowCode/LowCodeModel.jsx +962 -0
  15. package/src/components/LowCode/LowCodePage.jsx +31 -0
  16. package/src/components/LowCode/LowCodeRuntime.jsx +335 -0
  17. package/src/components/LowCode/MaterializePage.jsx +235 -0
  18. package/src/components/LowCode/index.js +1 -0
  19. package/src/components/MockPlatform/CurlImportModal.jsx +362 -0
  20. package/src/components/MockPlatform/EndpointsTab.jsx +509 -0
  21. package/src/components/MockPlatform/EnvironmentsTab.jsx +212 -0
  22. package/src/components/MockPlatform/MockTemplateHelper.jsx +200 -0
  23. package/src/components/MockPlatform/OpenApiImportModal.jsx +305 -0
  24. package/src/components/MockPlatform/RecordingsTab.jsx +397 -0
  25. package/src/components/MockPlatform/RequestLogsTab.jsx +239 -0
  26. package/src/components/MockPlatform/ScenariosTab.jsx +236 -0
  27. package/src/components/MockPlatform/TestCasesTab.jsx +462 -0
  28. package/src/components/MockPlatform/index.jsx +127 -0
  29. package/src/components/Overview.jsx +74 -0
  30. package/src/index.js +26 -27
  31. package/src/services/agentTeam.js +7 -0
  32. package/src/services/api.js +84 -0
  33. package/src/services/ctcAc.js +7 -0
  34. package/src/services/ctcAcDomain.js +7 -0
  35. package/src/services/ctcAuthorization.js +7 -0
  36. package/src/services/ctcSurl.js +7 -0
  37. package/src/services/ctcUser.js +7 -0
  38. package/src/services/job.js +7 -0
  39. package/src/services/metaApp.js +7 -0
  40. package/src/services/privateConfig.js +7 -0
  41. package/src/services/request.js +6 -0
  42. package/src/services/webide.js +6 -0
  43. package/src/services/workspace.js +21 -0
@@ -0,0 +1,962 @@
1
+ import React, {useCallback, useEffect, useMemo, useState} from 'react'
2
+ import {useSearchParams} from 'react-router-dom'
3
+ import {
4
+ Alert,
5
+ Badge,
6
+ Button,
7
+ Card,
8
+ Col,
9
+ Drawer,
10
+ Empty,
11
+ Form,
12
+ Input,
13
+ InputNumber,
14
+ message,
15
+ Modal,
16
+ Popconfirm,
17
+ Row,
18
+ Select,
19
+ Space,
20
+ Spin,
21
+ Switch,
22
+ Table,
23
+ Tabs,
24
+ Tag,
25
+ Tooltip
26
+ } from 'antd'
27
+ import {
28
+ CheckCircleOutlined,
29
+ CodeOutlined,
30
+ CopyOutlined,
31
+ DatabaseOutlined,
32
+ DeleteOutlined,
33
+ EditOutlined,
34
+ FileTextOutlined,
35
+ FormatPainterOutlined,
36
+ HolderOutlined,
37
+ PlusOutlined,
38
+ ReloadOutlined,
39
+ ThunderboltOutlined
40
+ } from '@ant-design/icons'
41
+ import request from '../../utils/request'
42
+
43
+ const {Option} = Select
44
+ const {TextArea} = Input
45
+
46
+ // ===== 字段类型定义 =====
47
+ const FIELD_TYPES = [
48
+ {type: 'STRING', label: '字符串', icon: <FileTextOutlined/>, color: '#1677ff', desc: '短文本 (默认 VARCHAR 255)'},
49
+ {type: 'TEXT', label: '长文本', icon: <FileTextOutlined/>, color: '#13c2c2', desc: 'TEXT 类型, 无长度限制'},
50
+ {type: 'INT', label: '整数', icon: '#', color: '#722ed1', desc: 'INT 整数'},
51
+ {type: 'LONG', label: '长整数', icon: '#', color: '#722ed1', desc: 'BIGINT 长整数'},
52
+ {type: 'DECIMAL', label: '小数', icon: '0.00', color: '#eb2f96', desc: 'DECIMAL 精确小数'},
53
+ {type: 'BOOLEAN', label: '布尔', icon: <CheckCircleOutlined/>, color: '#52c41a', desc: 'TINYINT(1) 是/否'},
54
+ {type: 'DATE', label: '日期', icon: '📅', color: '#fa8c16', desc: 'DATE yyyy-MM-dd'},
55
+ {type: 'DATETIME', label: '日期时间', icon: '🕐', color: '#fa8c16', desc: 'DATETIME yyyy-MM-dd HH:mm:ss'},
56
+ {type: 'JSON', label: 'JSON', icon: '{}', color: '#2f54eb', desc: 'JSON 字符串'},
57
+ {type: 'REF', label: '引用', icon: '🔗', color: '#8c8c8c', desc: '外键引用 BIGINT'},
58
+ ]
59
+
60
+ const FIELD_TYPE_MAP = Object.fromEntries(FIELD_TYPES.map(t => [t.type, t]))
61
+
62
+ const DRAG_TYPE_PALETTE = 'application/x-lc-field-type'
63
+ const DRAG_TYPE_FIELD = 'application/x-lc-field-idx'
64
+
65
+ /**
66
+ * LowCodeModel: 拖拽式实体/字段编辑器 (z-lc Phase 1)
67
+ * <p>
68
+ * 三列布局:
69
+ * <ul>
70
+ * <li>左侧调色板: 10 种字段类型, 可拖到中间列表</li>
71
+ * <li>中间字段列表: 支持拖入新字段 + 字段间拖拽排序</li>
72
+ * <li>右侧详情: 选中字段后编辑详细属性</li>
73
+ * </ul>
74
+ * <p>
75
+ * 所有提交走事件溯源 (POST /api/lc/app/{appCode}/event), schema 通过 GET /schema 回放得到.
76
+ */
77
+ export default function LowCodeModel() {
78
+ const [searchParams] = useSearchParams()
79
+ const [appCode, setAppCode] = useState(searchParams.get('appCode') || 'demo')
80
+ const [tenantCode, setTenantCode] = useState(searchParams.get('tenant') || localStorage.getItem('z_tenant') || 'default')
81
+ const [schema, setSchema] = useState([])
82
+ const [lastEventId, setLastEventId] = useState(null)
83
+ const [loading, setLoading] = useState(false)
84
+ const [editingEntity, setEditingEntity] = useState(null)
85
+ const [entityModalOpen, setEntityModalOpen] = useState(false)
86
+ const [entityForm] = Form.useForm()
87
+
88
+ // ===== 拖拽态 =====
89
+ // palette drag: 拖字段类型时携带 type 字符串
90
+ const [paletteDragType, setPaletteDragType] = useState(null)
91
+ // field list drag: 拖已有字段重排时, 记录源 idx
92
+ const [dragFieldIdx, setDragFieldIdx] = useState(null)
93
+ // 当前 hover 位置 (palette idx / field insert idx / null)
94
+ const [dropHint, setDropHint] = useState(null)
95
+
96
+ // ===== 当前实体编辑态 (右侧 Drawer) =====
97
+ const [activeEntityFields, setActiveEntityFields] = useState([])
98
+ const [selectedFieldIdx, setSelectedFieldIdx] = useState(null)
99
+ const [dirty, setDirty] = useState(false)
100
+ const [rightDrawerOpen, setRightDrawerOpen] = useState(false)
101
+
102
+ const fetchSchema = useCallback(async () => {
103
+ if (!appCode) return
104
+ setLoading(true)
105
+ try {
106
+ const list = await request.get(`/lc/app/${appCode}/schema`, {
107
+ params: {tenantCode}
108
+ })
109
+ setSchema(Array.isArray(list) ? list : [])
110
+ setLastEventId(null)
111
+ } catch (e) {
112
+ message.error('加载 schema 失败: ' + (e?.message || '未知错误'))
113
+ setSchema([])
114
+ } finally {
115
+ setLoading(false)
116
+ }
117
+ }, [appCode, tenantCode])
118
+
119
+ useEffect(() => {
120
+ fetchSchema()
121
+ }, [fetchSchema])
122
+
123
+ // ===== 事件提交 (事件溯源) =====
124
+ const submitEvent = async (eventType, entityCode, eventData) => {
125
+ try {
126
+ const body = {
127
+ tenantCode,
128
+ entityCode,
129
+ eventType,
130
+ eventData: typeof eventData === 'string' ? eventData : JSON.stringify(eventData),
131
+ source: 'USER',
132
+ parentEventId: lastEventId
133
+ }
134
+ const res = await request.post(`/lc/app/${appCode}/event`, body)
135
+ message.success(`事件 ${eventType} 已提交 (eventId=${res?.eventId?.slice(0, 8)}…)`)
136
+ setLastEventId(res?.eventId || null)
137
+ await fetchSchema()
138
+ } catch (e) {
139
+ message.error('提交事件失败: ' + (e?.message || '未知错误'))
140
+ }
141
+ }
142
+
143
+ // ===== Entity CRUD =====
144
+ const handleCreateEntity = () => {
145
+ setEditingEntity(null)
146
+ entityForm.resetFields()
147
+ entityForm.setFieldsValue({
148
+ entityCode: '',
149
+ entityName: '',
150
+ tableName: '',
151
+ description: '',
152
+ fields: []
153
+ })
154
+ setEntityModalOpen(true)
155
+ }
156
+
157
+ const handleEditEntity = (entity) => {
158
+ setEditingEntity(entity)
159
+ entityForm.setFieldsValue({
160
+ entityCode: entity.entityCode,
161
+ entityName: entity.entityName,
162
+ tableName: entity.tableName || entity.entityCode,
163
+ description: entity.description,
164
+ fields: entity.fields || []
165
+ })
166
+ setEntityModalOpen(true)
167
+ }
168
+
169
+ const handleOpenEntityEditor = (entity) => {
170
+ // 打开右侧画布编辑器 (拖拽)
171
+ setActiveEntityFields((entity.fields || []).map((f, i) => ({...f, sortOrder: f.sortOrder ?? i})))
172
+ setSelectedFieldIdx(null)
173
+ setDirty(false)
174
+ setRightDrawerOpen(true)
175
+ // 暂存当前编辑中的 entity 信息
176
+ setEditingEntity(entity)
177
+ }
178
+
179
+ const handleDeleteEntity = (entity) => {
180
+ Modal.confirm({
181
+ title: `删除实体 ${entity.entityName || entity.entityCode}?`,
182
+ content: '此操作会发出 DELETE 事件, 所有字段一起删除.',
183
+ okType: 'danger',
184
+ onOk: () => submitEvent('DELETE', entity.entityCode, {entityCode: entity.entityCode})
185
+ })
186
+ }
187
+
188
+ const handleSubmitEntity = async () => {
189
+ try {
190
+ const v = await entityForm.validateFields()
191
+ const eventType = editingEntity ? 'UPDATE' : 'CREATE'
192
+ const eventData = {
193
+ entityCode: v.entityCode,
194
+ entityName: v.entityName,
195
+ tableName: v.tableName || v.entityCode,
196
+ description: v.description,
197
+ fields: (v.fields || []).map(f => ({
198
+ fieldCode: f.fieldCode,
199
+ fieldName: f.fieldName,
200
+ fieldType: f.fieldType || 'STRING',
201
+ required: !!f.required,
202
+ defaultValue: f.defaultValue || null,
203
+ dictCode: f.dictCode || null,
204
+ refEntity: f.refEntity || null,
205
+ fieldLength: f.fieldLength || null,
206
+ scale: f.scale || null,
207
+ sortOrder: f.sortOrder || 0
208
+ }))
209
+ }
210
+ setEntityModalOpen(false)
211
+ await submitEvent(eventType, v.entityCode, eventData)
212
+ } catch (e) {
213
+ if (e?.errorFields) {
214
+ message.error('请补全必填字段')
215
+ } else {
216
+ message.error('提交失败: ' + (e?.message || '未知错误'))
217
+ }
218
+ }
219
+ }
220
+
221
+ // ===== 拖拽调色板 → 字段列表 =====
222
+ const handlePaletteDragStart = (e, type) => {
223
+ e.dataTransfer.setData(DRAG_TYPE_PALETTE, type)
224
+ e.dataTransfer.setData('text/plain', type) // 兜底
225
+ e.dataTransfer.effectAllowed = 'copy'
226
+ setPaletteDragType(type)
227
+ }
228
+
229
+ const handlePaletteDragEnd = () => {
230
+ setPaletteDragType(null)
231
+ setDropHint(null)
232
+ }
233
+
234
+ // ===== 拖拽已有字段重排 =====
235
+ const handleFieldDragStart = (e, idx) => {
236
+ e.dataTransfer.setData(DRAG_TYPE_FIELD, String(idx))
237
+ e.dataTransfer.effectAllowed = 'move'
238
+ setDragFieldIdx(idx)
239
+ }
240
+
241
+ const handleFieldDragEnd = () => {
242
+ setDragFieldIdx(null)
243
+ setDropHint(null)
244
+ }
245
+
246
+ // ===== 列表接受拖入/拖放 =====
247
+ const handleListDragOver = (e) => {
248
+ e.preventDefault()
249
+ e.dataTransfer.dropEffect = paletteDragType ? 'copy' : 'move'
250
+ }
251
+
252
+ const handleListDragLeave = () => {
253
+ setDropHint(null)
254
+ }
255
+
256
+ const handleListDrop = (e) => {
257
+ e.preventDefault()
258
+ const paletteType = e.dataTransfer.getData(DRAG_TYPE_PALETTE)
259
+ const fieldIdxStr = e.dataTransfer.getData(DRAG_TYPE_FIELD)
260
+ if (paletteType) {
261
+ // 从调色板拖入: 在末尾追加
262
+ addFieldFromPalette(paletteType, activeEntityFields.length)
263
+ } else if (fieldIdxStr) {
264
+ // 拖到列表末尾 (无 idx 提示): 等同拖到末尾
265
+ const srcIdx = Number(fieldIdxStr)
266
+ moveField(srcIdx, activeEntityFields.length)
267
+ }
268
+ setDropHint(null)
269
+ setPaletteDragType(null)
270
+ setDragFieldIdx(null)
271
+ }
272
+
273
+ const handleRowDragOver = (e, insertIdx) => {
274
+ e.preventDefault()
275
+ e.stopPropagation()
276
+ setDropHint(insertIdx)
277
+ }
278
+
279
+ const handleRowDrop = (e, insertIdx) => {
280
+ e.preventDefault()
281
+ e.stopPropagation()
282
+ const paletteType = e.dataTransfer.getData(DRAG_TYPE_PALETTE)
283
+ const fieldIdxStr = e.dataTransfer.getData(DRAG_TYPE_FIELD)
284
+ if (paletteType) {
285
+ addFieldFromPalette(paletteType, insertIdx)
286
+ } else if (fieldIdxStr) {
287
+ moveField(Number(fieldIdxStr), insertIdx)
288
+ }
289
+ setDropHint(null)
290
+ setPaletteDragType(null)
291
+ setDragFieldIdx(null)
292
+ }
293
+
294
+ const addFieldFromPalette = (type, insertIdx) => {
295
+ const meta = FIELD_TYPE_MAP[type]
296
+ if (!meta) return
297
+ const seq = activeEntityFields.length + 1
298
+ const newField = {
299
+ fieldCode: `field_${seq}`,
300
+ fieldName: `新${meta.label}字段`,
301
+ fieldType: type,
302
+ required: false,
303
+ defaultValue: null,
304
+ dictCode: null,
305
+ refEntity: null,
306
+ fieldLength: type === 'STRING' ? 255 : null,
307
+ scale: type === 'DECIMAL' ? 2 : null,
308
+ sortOrder: insertIdx
309
+ }
310
+ const next = [...activeEntityFields]
311
+ next.splice(insertIdx, 0, newField)
312
+ reassignSortOrder(next)
313
+ setActiveEntityFields(next)
314
+ setSelectedFieldIdx(insertIdx)
315
+ setDirty(true)
316
+ }
317
+
318
+ const moveField = (srcIdx, dstIdx) => {
319
+ if (srcIdx === dstIdx || srcIdx < 0 || dstIdx < 0) return
320
+ const next = [...activeEntityFields]
321
+ const [moved] = next.splice(srcIdx, 1)
322
+ // 如果从前往后拖, 目标 idx 要 -1
323
+ const finalIdx = dstIdx > srcIdx ? dstIdx - 1 : dstIdx
324
+ next.splice(finalIdx, 0, moved)
325
+ reassignSortOrder(next)
326
+ setActiveEntityFields(next)
327
+ setDirty(true)
328
+ }
329
+
330
+ const reassignSortOrder = (arr) => {
331
+ arr.forEach((f, i) => { f.sortOrder = i })
332
+ }
333
+
334
+ // ===== 字段详情编辑 =====
335
+ const updateField = (idx, patch) => {
336
+ const next = [...activeEntityFields]
337
+ next[idx] = {...next[idx], ...patch}
338
+ setActiveEntityFields(next)
339
+ setDirty(true)
340
+ }
341
+
342
+ const removeField = (idx) => {
343
+ const next = activeEntityFields.filter((_, i) => i !== idx)
344
+ reassignSortOrder(next)
345
+ setActiveEntityFields(next)
346
+ setSelectedFieldIdx(null)
347
+ setDirty(true)
348
+ }
349
+
350
+ const duplicateField = (idx) => {
351
+ const src = activeEntityFields[idx]
352
+ const copy = {...src, fieldCode: `${src.fieldCode}_copy`, fieldName: `${src.fieldName} (副本)`}
353
+ const next = [...activeEntityFields]
354
+ next.splice(idx + 1, 0, copy)
355
+ reassignSortOrder(next)
356
+ setActiveEntityFields(next)
357
+ setSelectedFieldIdx(idx + 1)
358
+ setDirty(true)
359
+ }
360
+
361
+ // ===== 提交整个 entity (从画布编辑器) =====
362
+ const handleCommitEntity = async () => {
363
+ if (!editingEntity) return
364
+ const eventData = {
365
+ entityCode: editingEntity.entityCode,
366
+ entityName: editingEntity.entityName,
367
+ tableName: editingEntity.tableName,
368
+ description: editingEntity.description,
369
+ fields: activeEntityFields.map(f => ({
370
+ fieldCode: f.fieldCode,
371
+ fieldName: f.fieldName,
372
+ fieldType: f.fieldType || 'STRING',
373
+ required: !!f.required,
374
+ defaultValue: f.defaultValue || null,
375
+ dictCode: f.dictCode || null,
376
+ refEntity: f.refEntity || null,
377
+ fieldLength: f.fieldLength || null,
378
+ scale: f.scale || null,
379
+ sortOrder: f.sortOrder ?? 0
380
+ }))
381
+ }
382
+ await submitEvent('UPDATE', editingEntity.entityCode, eventData)
383
+ setDirty(false)
384
+ }
385
+
386
+ // ===== 自动建表 =====
387
+ const handleProvision = async () => {
388
+ if (!editingEntity) return
389
+ try {
390
+ const ddl = await request.post(
391
+ `/lc/admin/entity/${editingEntity.id}/provision?tenant=${tenantCode}`
392
+ )
393
+ message.success(`已建表 (${activeEntityFields.length} 字段)`)
394
+ console.log('DDL:', ddl)
395
+ } catch (e) {
396
+ message.error('建表失败: ' + (e?.message || '未知错误'))
397
+ }
398
+ }
399
+
400
+ // ===== 渲染 =====
401
+ const entityColumns = [
402
+ {title: '编码', dataIndex: 'entityCode', key: 'entityCode', width: 140},
403
+ {title: '名称', dataIndex: 'entityName', key: 'entityName', width: 140},
404
+ {title: '物理表', dataIndex: 'tableName', key: 'tableName', width: 160},
405
+ {
406
+ title: '字段数',
407
+ key: 'fields',
408
+ width: 80,
409
+ render: (_, r) => <Badge count={r.fields?.length || 0} showZero color="blue"/>
410
+ },
411
+ {title: '描述', dataIndex: 'description', key: 'description', ellipsis: true},
412
+ {
413
+ title: '操作',
414
+ key: 'action',
415
+ width: 260,
416
+ render: (_, r) => (
417
+ <Space>
418
+ <Button type="link" size="small" icon={<FormatPainterOutlined/>}
419
+ onClick={() => handleOpenEntityEditor(r)}>
420
+ 画布编辑
421
+ </Button>
422
+ <Button type="link" size="small" icon={<EditOutlined/>}
423
+ onClick={() => handleEditEntity(r)}>
424
+ 表单编辑
425
+ </Button>
426
+ <Popconfirm title="确定删除?" onConfirm={() => handleDeleteEntity(r)}>
427
+ <Button type="link" size="small" danger icon={<DeleteOutlined/>}>
428
+ 删除
429
+ </Button>
430
+ </Popconfirm>
431
+ </Space>
432
+ )
433
+ }
434
+ ]
435
+
436
+ const selectedField = selectedFieldIdx != null ? activeEntityFields[selectedFieldIdx] : null
437
+
438
+ return (
439
+ <div>
440
+ <Card
441
+ title={<><ThunderboltOutlined/> z-lc 模型编辑器 (拖拽版)</>}
442
+ extra={
443
+ <Space>
444
+ <Input
445
+ addonBefore="appCode"
446
+ value={appCode}
447
+ onChange={e => setAppCode(e.target.value.trim())}
448
+ style={{width: 200}}
449
+ />
450
+ <Input
451
+ addonBefore="tenant"
452
+ value={tenantCode}
453
+ onChange={e => setTenantCode(e.target.value.trim())}
454
+ style={{width: 160}}
455
+ />
456
+ <Button icon={<ReloadOutlined/>} onClick={fetchSchema}>刷新</Button>
457
+ <Button type="primary" icon={<PlusOutlined/>} onClick={handleCreateEntity}>
458
+ 新建实体
459
+ </Button>
460
+ </Space>
461
+ }
462
+ >
463
+ <Alert
464
+ type="info"
465
+ showIcon
466
+ message="提示: 点击实体的「画布编辑」进入拖拽式建模: 从左侧面板拖字段类型到中间, 拖拽字段行可重排, 右侧编辑属性。"
467
+ style={{marginBottom: 16}}
468
+ />
469
+ <Spin spinning={loading}>
470
+ {schema.length === 0 ? (
471
+ <Empty description="暂无实体, 点击右上角 「新建实体」 开始"/>
472
+ ) : (
473
+ <Table
474
+ rowKey="entityCode"
475
+ columns={entityColumns}
476
+ dataSource={schema}
477
+ pagination={false}
478
+ size="small"
479
+ expandable={{
480
+ expandedRowRender: (record) => (
481
+ <FieldPreview entity={record}/>
482
+ )
483
+ }}
484
+ />
485
+ )}
486
+ </Spin>
487
+ </Card>
488
+
489
+ {/* 实体表单编辑 (兼容旧路径) */}
490
+ <Modal
491
+ title={editingEntity ? `编辑实体 ${editingEntity.entityCode}` : '新建实体'}
492
+ open={entityModalOpen}
493
+ onCancel={() => setEntityModalOpen(false)}
494
+ onOk={handleSubmitEntity}
495
+ width={760}
496
+ destroyOnClose
497
+ >
498
+ <EntityForm form={entityForm} editing={editingEntity}/>
499
+ </Modal>
500
+
501
+ {/* 拖拽式画布编辑器 (右侧 Drawer) */}
502
+ <Drawer
503
+ title={
504
+ <Space>
505
+ <FormatPainterOutlined/>
506
+ {editingEntity ? `画布编辑 - ${editingEntity.entityName || editingEntity.entityCode}` : '画布编辑'}
507
+ {dirty && <Tag color="orange">未保存</Tag>}
508
+ </Space>
509
+ }
510
+ open={rightDrawerOpen}
511
+ onClose={() => {
512
+ if (dirty) {
513
+ Modal.confirm({
514
+ title: '有未保存的修改, 确定关闭?',
515
+ onOk: () => {
516
+ setRightDrawerOpen(false)
517
+ setDirty(false)
518
+ }
519
+ })
520
+ } else {
521
+ setRightDrawerOpen(false)
522
+ }
523
+ }}
524
+ width="85%"
525
+ destroyOnClose
526
+ extra={
527
+ <Space>
528
+ <Button onClick={handleProvision} icon={<DatabaseOutlined/>}>
529
+ 自动建表
530
+ </Button>
531
+ <Button
532
+ type="primary"
533
+ icon={<ThunderboltOutlined/>}
534
+ disabled={!dirty}
535
+ onClick={handleCommitEntity}
536
+ >
537
+ 提交 (UPDATE 事件)
538
+ </Button>
539
+ </Space>
540
+ }
541
+ >
542
+ <Row gutter={16} style={{height: 'calc(100vh - 200px)'}}>
543
+ {/* 左侧: 字段类型调色板 */}
544
+ <Col span={5}>
545
+ <Card size="small" title={<><HolderOutlined/> 字段类型面板</>} style={{height: '100%'}}
546
+ bodyStyle={{padding: 12, overflowY: 'auto', height: 'calc(100% - 40px)'}}>
547
+ <div style={{fontSize: 12, color: '#999', marginBottom: 8}}>
548
+ 拖拽字段类型到右侧
549
+ </div>
550
+ {FIELD_TYPES.map(t => (
551
+ <div
552
+ key={t.type}
553
+ draggable
554
+ onDragStart={(e) => handlePaletteDragStart(e, t.type)}
555
+ onDragEnd={handlePaletteDragEnd}
556
+ style={{
557
+ padding: '8px 12px',
558
+ marginBottom: 8,
559
+ background: paletteDragType === t.type ? '#e6f4ff' : '#fafafa',
560
+ border: `1px dashed ${t.color}`,
561
+ borderRadius: 6,
562
+ cursor: 'grab',
563
+ opacity: paletteDragType === t.type ? 0.5 : 1,
564
+ transition: 'all 0.2s'
565
+ }}
566
+ >
567
+ <Space>
568
+ <span style={{
569
+ display: 'inline-block',
570
+ width: 22, height: 22,
571
+ borderRadius: 4,
572
+ background: t.color,
573
+ color: '#fff',
574
+ textAlign: 'center',
575
+ lineHeight: '22px',
576
+ fontSize: 11,
577
+ fontWeight: 'bold'
578
+ }}>{t.type.slice(0, 2)}</span>
579
+ <span style={{fontWeight: 500}}>{t.label}</span>
580
+ </Space>
581
+ <div style={{fontSize: 11, color: '#888', marginTop: 4}}>{t.desc}</div>
582
+ </div>
583
+ ))}
584
+ </Card>
585
+ </Col>
586
+
587
+ {/* 中间: 字段列表 */}
588
+ <Col span={12}>
589
+ <Card
590
+ size="small"
591
+ title={
592
+ <Space>
593
+ <DatabaseOutlined/>
594
+ 字段列表 ({activeEntityFields.length})
595
+ </Space>
596
+ }
597
+ style={{height: '100%'}}
598
+ bodyStyle={{padding: 8, overflowY: 'auto', height: 'calc(100% - 40px)'}}
599
+ >
600
+ <div
601
+ onDragOver={handleListDragOver}
602
+ onDragLeave={handleListDragLeave}
603
+ onDrop={handleListDrop}
604
+ style={{
605
+ minHeight: '100%',
606
+ padding: 4,
607
+ border: '2px dashed transparent',
608
+ borderRadius: 6
609
+ }}
610
+ >
611
+ {activeEntityFields.length === 0 ? (
612
+ <Empty
613
+ description="从左侧拖拽字段类型到这里"
614
+ style={{marginTop: 80}}
615
+ />
616
+ ) : (
617
+ <>
618
+ {activeEntityFields.map((f, idx) => {
619
+ const meta = FIELD_TYPE_MAP[f.fieldType] || FIELD_TYPE_MAP.STRING
620
+ const isSelected = selectedFieldIdx === idx
621
+ const isDragging = dragFieldIdx === idx
622
+ return (
623
+ <React.Fragment key={`${f.fieldCode}-${idx}`}>
624
+ {/* 行间插入提示 */}
625
+ {dropHint === idx && (
626
+ <div style={{
627
+ height: 4,
628
+ background: '#1677ff',
629
+ borderRadius: 2,
630
+ margin: '4px 0'
631
+ }}/>
632
+ )}
633
+ <div
634
+ draggable
635
+ onDragStart={(e) => handleFieldDragStart(e, idx)}
636
+ onDragEnd={handleFieldDragEnd}
637
+ onDragOver={(e) => handleRowDragOver(e, idx)}
638
+ onDrop={(e) => handleRowDrop(e, idx)}
639
+ onClick={() => setSelectedFieldIdx(idx)}
640
+ style={{
641
+ padding: '10px 12px',
642
+ marginBottom: 6,
643
+ background: isSelected ? '#e6f4ff' : '#fff',
644
+ border: `1px solid ${isSelected ? '#1677ff' : '#d9d9d9'}`,
645
+ borderRadius: 6,
646
+ cursor: 'grab',
647
+ opacity: isDragging ? 0.4 : 1,
648
+ transition: 'all 0.15s'
649
+ }}
650
+ >
651
+ <Space style={{width: '100%', justifyContent: 'space-between'}}>
652
+ <Space>
653
+ <HolderOutlined style={{color: '#999'}}/>
654
+ <span style={{
655
+ display: 'inline-block',
656
+ width: 20, height: 20,
657
+ borderRadius: 3,
658
+ background: meta.color,
659
+ color: '#fff',
660
+ textAlign: 'center',
661
+ lineHeight: '20px',
662
+ fontSize: 10
663
+ }}>{f.fieldType.slice(0, 2)}</span>
664
+ <span style={{
665
+ fontFamily: 'monospace',
666
+ fontWeight: 500
667
+ }}>{f.fieldCode}</span>
668
+ <span>{f.fieldName}</span>
669
+ {f.required && <Tag color="red" style={{margin: 0}}>必填</Tag>}
670
+ {f.dictCode && <Tag color="blue" style={{margin: 0}}>dict:{f.dictCode}</Tag>}
671
+ {f.refEntity && <Tag color="purple" style={{margin: 0}}>ref:{f.refEntity}</Tag>}
672
+ </Space>
673
+ <Space size={4} onClick={(e) => e.stopPropagation()}>
674
+ <Tooltip title="复制">
675
+ <Button
676
+ type="text" size="small"
677
+ icon={<CopyOutlined/>}
678
+ onClick={() => duplicateField(idx)}
679
+ />
680
+ </Tooltip>
681
+ <Tooltip title="删除">
682
+ <Button
683
+ type="text" size="small" danger
684
+ icon={<DeleteOutlined/>}
685
+ onClick={() => removeField(idx)}
686
+ />
687
+ </Tooltip>
688
+ </Space>
689
+ </Space>
690
+ </div>
691
+ </React.Fragment>
692
+ )
693
+ })}
694
+ {/* 末尾插入提示 */}
695
+ {dropHint === activeEntityFields.length && (
696
+ <div style={{
697
+ height: 4,
698
+ background: '#1677ff',
699
+ borderRadius: 2,
700
+ margin: '4px 0'
701
+ }}/>
702
+ )}
703
+ </>
704
+ )}
705
+ </div>
706
+ </Card>
707
+ </Col>
708
+
709
+ {/* 右侧: 字段属性面板 */}
710
+ <Col span={7}>
711
+ <Card
712
+ size="small"
713
+ title={<><CodeOutlined/> 字段属性</>}
714
+ style={{height: '100%'}}
715
+ bodyStyle={{padding: 12, overflowY: 'auto', height: 'calc(100% - 40px)'}}
716
+ >
717
+ {selectedField ? (
718
+ <FieldPropertyPanel
719
+ field={selectedField}
720
+ onChange={(patch) => updateField(selectedFieldIdx, patch)}
721
+ />
722
+ ) : (
723
+ <Empty description="点击中间列表中的字段以编辑属性" style={{marginTop: 80}}/>
724
+ )}
725
+ </Card>
726
+ </Col>
727
+ </Row>
728
+ </Drawer>
729
+ </div>
730
+ )
731
+ }
732
+
733
+ // ===== 字段预览 (实体列表展开行) =====
734
+ function FieldPreview({entity}) {
735
+ const fields = entity.fields || []
736
+ if (fields.length === 0) {
737
+ return <span style={{color: '#999'}}>无字段</span>
738
+ }
739
+ return (
740
+ <Space wrap size={[4, 4]}>
741
+ {fields.map(f => (
742
+ <Tag key={f.fieldCode} color={
743
+ (FIELD_TYPE_MAP[f.fieldType] || {}).color || 'default'
744
+ }>
745
+ {f.fieldCode} : {f.fieldType}
746
+ {f.required ? ' *' : ''}
747
+ </Tag>
748
+ ))}
749
+ </Space>
750
+ )
751
+ }
752
+
753
+ // ===== 实体表单 (兼容旧版) =====
754
+ function EntityForm({form, editing}) {
755
+ return (
756
+ <Form form={form} layout="vertical">
757
+ <Space style={{width: '100%'}} size="middle">
758
+ <Form.Item name="entityCode" label="实体编码" rules={[{
759
+ required: true,
760
+ pattern: /^[A-Za-z][A-Za-z0-9_]*$/,
761
+ message: '字母数字下划线'
762
+ }]}>
763
+ <Input disabled={!!editing} placeholder="如 user"/>
764
+ </Form.Item>
765
+ <Form.Item name="entityName" label="实体名称">
766
+ <Input placeholder="如 用户"/>
767
+ </Form.Item>
768
+ <Form.Item name="tableName" label="物理表名" rules={[{
769
+ required: true,
770
+ pattern: /^[A-Za-z][A-Za-z0-9_]*$/,
771
+ message: '字母数字下划线'
772
+ }]}>
773
+ <Input placeholder="默认 = entityCode"/>
774
+ </Form.Item>
775
+ </Space>
776
+ <Form.Item name="description" label="描述">
777
+ <TextArea rows={2}/>
778
+ </Form.Item>
779
+ <Form.List name="fields">
780
+ {(fields, {add, remove}) => (
781
+ <>
782
+ <div style={{marginBottom: 8}}>
783
+ <Button type="dashed" onClick={() => add({fieldType: 'STRING', required: false})}
784
+ icon={<PlusOutlined/>}>
785
+ 添加字段
786
+ </Button>
787
+ </div>
788
+ <Table
789
+ rowKey={(r) => r.fieldCode || Math.random()}
790
+ columns={[
791
+ {
792
+ title: '编码', dataIndex: 'fieldCode',
793
+ render: (_, _r, idx) => (
794
+ <Form.Item name={[idx, 'fieldCode']} noStyle rules={[{
795
+ required: true,
796
+ pattern: /^[A-Za-z][A-Za-z0-9_]*$/,
797
+ message: '字母数字下划线'
798
+ }]}>
799
+ <Input size="small" placeholder="userName"/>
800
+ </Form.Item>
801
+ )
802
+ },
803
+ {
804
+ title: '名称', dataIndex: 'fieldName',
805
+ render: (_, _r, idx) => (
806
+ <Form.Item name={[idx, 'fieldName']} noStyle>
807
+ <Input size="small" placeholder="用户名"/>
808
+ </Form.Item>
809
+ )
810
+ },
811
+ {
812
+ title: '类型', dataIndex: 'fieldType', width: 120,
813
+ render: (_, _r, idx) => (
814
+ <Form.Item name={[idx, 'fieldType']} noStyle>
815
+ <Select size="small">
816
+ {FIELD_TYPES.map(t =>
817
+ <Option key={t.type} value={t.type}>{t.type}</Option>
818
+ )}
819
+ </Select>
820
+ </Form.Item>
821
+ )
822
+ },
823
+ {
824
+ title: '必填', dataIndex: 'required', width: 60,
825
+ render: (_, _r, idx) => (
826
+ <Form.Item name={[idx, 'required']} noStyle valuePropName="checked">
827
+ <Switch size="small"/>
828
+ </Form.Item>
829
+ )
830
+ },
831
+ {
832
+ title: '字典', dataIndex: 'dictCode', width: 110,
833
+ render: (_, _r, idx) => (
834
+ <Form.Item name={[idx, 'dictCode']} noStyle>
835
+ <Input size="small" placeholder="可选"/>
836
+ </Form.Item>
837
+ )
838
+ },
839
+ {
840
+ title: '引用', dataIndex: 'refEntity', width: 110,
841
+ render: (_, _r, idx) => (
842
+ <Form.Item name={[idx, 'refEntity']} noStyle>
843
+ <Input size="small" placeholder="可选"/>
844
+ </Form.Item>
845
+ )
846
+ },
847
+ {
848
+ title: '操作', width: 60,
849
+ render: (_, _r, idx) => (
850
+ <Button size="small" danger type="link"
851
+ onClick={() => remove(idx)}>移除</Button>
852
+ )
853
+ }
854
+ ]}
855
+ dataSource={fields}
856
+ pagination={false}
857
+ size="small"
858
+ />
859
+ </>
860
+ )}
861
+ </Form.List>
862
+ </Form>
863
+ )
864
+ }
865
+
866
+ // ===== 字段属性编辑面板 =====
867
+ function FieldPropertyPanel({field, onChange}) {
868
+ return (
869
+ <div>
870
+ <Form layout="vertical" size="small">
871
+ <Form.Item label="字段编码 (物理列名)" required>
872
+ <Input
873
+ value={field.fieldCode || ''}
874
+ onChange={e => onChange({fieldCode: e.target.value})}
875
+ placeholder="如 userName"
876
+ />
877
+ </Form.Item>
878
+ <Form.Item label="显示名称">
879
+ <Input
880
+ value={field.fieldName || ''}
881
+ onChange={e => onChange({fieldName: e.target.value})}
882
+ placeholder="如 用户名"
883
+ />
884
+ </Form.Item>
885
+ <Form.Item label="字段类型">
886
+ <Select
887
+ value={field.fieldType || 'STRING'}
888
+ onChange={v => onChange({fieldType: v})}
889
+ style={{width: '100%'}}
890
+ >
891
+ {FIELD_TYPES.map(t => (
892
+ <Option key={t.type} value={t.type}>{t.label} ({t.type})</Option>
893
+ ))}
894
+ </Select>
895
+ </Form.Item>
896
+ <Form.Item label="必填">
897
+ <Switch
898
+ checked={!!field.required}
899
+ onChange={v => onChange({required: v})}
900
+ />
901
+ </Form.Item>
902
+ {(field.fieldType === 'STRING' || field.fieldType === 'TEXT') && (
903
+ <Form.Item label="字段长度">
904
+ <InputNumber
905
+ value={field.fieldLength}
906
+ onChange={v => onChange({fieldLength: v})}
907
+ min={1}
908
+ max={65535}
909
+ style={{width: '100%'}}
910
+ />
911
+ </Form.Item>
912
+ )}
913
+ {field.fieldType === 'DECIMAL' && (
914
+ <Form.Item label="小数位数">
915
+ <InputNumber
916
+ value={field.scale}
917
+ onChange={v => onChange({scale: v})}
918
+ min={0}
919
+ max={10}
920
+ style={{width: '100%'}}
921
+ />
922
+ </Form.Item>
923
+ )}
924
+ <Form.Item label="默认值">
925
+ <Input
926
+ value={field.defaultValue || ''}
927
+ onChange={e => onChange({defaultValue: e.target.value})}
928
+ placeholder="可选"
929
+ />
930
+ </Form.Item>
931
+ <Form.Item label="字典编码 (dictCode)">
932
+ <Input
933
+ value={field.dictCode || ''}
934
+ onChange={e => onChange({dictCode: e.target.value})}
935
+ placeholder="如 user_status"
936
+ />
937
+ </Form.Item>
938
+ <Form.Item label="引用实体 (refEntity)">
939
+ <Input
940
+ value={field.refEntity || ''}
941
+ onChange={e => onChange({refEntity: e.target.value})}
942
+ placeholder="如 其他 entity 的 entityCode"
943
+ />
944
+ </Form.Item>
945
+ <Form.Item label="排序号">
946
+ <InputNumber
947
+ value={field.sortOrder ?? 0}
948
+ onChange={v => onChange({sortOrder: v})}
949
+ style={{width: '100%'}}
950
+ />
951
+ </Form.Item>
952
+ <Form.Item label="备注">
953
+ <TextArea
954
+ rows={2}
955
+ value={field.description || ''}
956
+ onChange={e => onChange({description: e.target.value})}
957
+ />
958
+ </Form.Item>
959
+ </Form>
960
+ </div>
961
+ )
962
+ }