@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.
- package/dist/z-frontend-common.css +1 -0
- package/dist/z-frontend-common.es.js +6153 -300
- package/dist/z-frontend-common.umd.js +22 -4
- package/package.json +5 -4
- package/src/components/Ctc/Layout.jsx +328 -0
- package/src/components/Ctc/Layout.module.css +145 -0
- package/src/components/Ctc/agentTeam/index.tsx +308 -0
- package/src/components/Ctc/login/AuthPage.module.css +26 -0
- package/src/components/Ctc/login/AuthPage.tsx +235 -0
- package/src/components/Ctc/login/index.less +49 -0
- package/src/components/Ctc/login/index.tsx +142 -0
- package/src/components/Ctc/userPanel/index.tsx +998 -0
- package/src/components/Ctc/webide/index.tsx +272 -0
- package/src/components/LowCode/LowCodeModel.jsx +962 -0
- package/src/components/LowCode/LowCodePage.jsx +31 -0
- package/src/components/LowCode/LowCodeRuntime.jsx +335 -0
- package/src/components/LowCode/MaterializePage.jsx +235 -0
- package/src/components/LowCode/index.js +1 -0
- package/src/components/MockPlatform/CurlImportModal.jsx +362 -0
- package/src/components/MockPlatform/EndpointsTab.jsx +509 -0
- package/src/components/MockPlatform/EnvironmentsTab.jsx +212 -0
- package/src/components/MockPlatform/MockTemplateHelper.jsx +200 -0
- package/src/components/MockPlatform/OpenApiImportModal.jsx +305 -0
- package/src/components/MockPlatform/RecordingsTab.jsx +397 -0
- package/src/components/MockPlatform/RequestLogsTab.jsx +239 -0
- package/src/components/MockPlatform/ScenariosTab.jsx +236 -0
- package/src/components/MockPlatform/TestCasesTab.jsx +462 -0
- package/src/components/MockPlatform/index.jsx +127 -0
- package/src/components/Overview.jsx +74 -0
- package/src/index.js +26 -27
- package/src/services/agentTeam.js +7 -0
- package/src/services/api.js +84 -0
- package/src/services/ctcAc.js +7 -0
- package/src/services/ctcAcDomain.js +7 -0
- package/src/services/ctcAuthorization.js +7 -0
- package/src/services/ctcSurl.js +7 -0
- package/src/services/ctcUser.js +7 -0
- package/src/services/job.js +7 -0
- package/src/services/metaApp.js +7 -0
- package/src/services/privateConfig.js +7 -0
- package/src/services/request.js +6 -0
- package/src/services/webide.js +6 -0
- 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
|
+
}
|