neo-cmp-cli 1.13.8 → 1.13.10

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 (57) hide show
  1. package/dist/neo/neoLogin.js +1 -1
  2. package/dist/package.json.js +1 -1
  3. package/package.json +1 -1
  4. package/template/antd-custom-cmp-template/package.json +4 -1
  5. package/template/asset-manage-template/package.json +4 -8
  6. package/template/echarts-custom-cmp-template/package.json +4 -1
  7. package/template/empty-custom-cmp-template/package.json +4 -1
  8. package/template/map-custom-cmp-template/package.json +4 -1
  9. package/template/neo-bi-cmps/package.json +3 -0
  10. package/template/neo-custom-cmp-template/package.json +1 -1
  11. package/template/neo-h5-cmps/package.json +1 -1
  12. package/template/neo-order-cmps/package.json +4 -1
  13. package/template/neo-web-entity-grid/package.json +1 -1
  14. package/template/neo-web-entity-grid/src/components/createForm__c/index.tsx +9 -4
  15. package/template/neo-web-entity-grid/src/components/createForm__c/model.ts +4 -2
  16. package/template/neo-web-entity-grid/src/components/entityGrid3__c/model.ts +2 -2
  17. package/template/neo-web-entity-grid/src/components/searchForm__c/index.tsx +6 -10
  18. package/template/neo-web-entity-grid/src/components/searchForm__c/model.ts +1 -1
  19. package/template/neo-web-entity-grid/src/components/searchForm__c/style.scss +205 -229
  20. package/template/neo-web-form/.prettierrc.js +12 -0
  21. package/template/neo-web-form/@types/neo-ui-common.d.ts +36 -0
  22. package/template/neo-web-form/README.md +99 -0
  23. package/template/neo-web-form/commitlint.config.js +59 -0
  24. package/template/neo-web-form/neo.config.js +57 -0
  25. package/template/neo-web-form/package.json +66 -0
  26. package/template/neo-web-form/public/css/base.css +283 -0
  27. package/template/neo-web-form/public/scripts/app/bluebird.js +6679 -0
  28. package/template/neo-web-form/public/template.html +13 -0
  29. package/template/neo-web-form/src/assets/css/common.scss +127 -0
  30. package/template/neo-web-form/src/assets/css/mixin.scss +47 -0
  31. package/template/neo-web-form/src/assets/img/AIBtn.gif +0 -0
  32. package/template/neo-web-form/src/assets/img/NeoCRM.jpg +0 -0
  33. package/template/neo-web-form/src/assets/img/aiLogo.png +0 -0
  34. package/template/neo-web-form/src/assets/img/card-list.svg +1 -0
  35. package/template/neo-web-form/src/assets/img/contact-form.svg +1 -0
  36. package/template/neo-web-form/src/assets/img/custom-form.svg +1 -0
  37. package/template/neo-web-form/src/assets/img/custom-widget.svg +1 -0
  38. package/template/neo-web-form/src/assets/img/data-list.svg +1 -0
  39. package/template/neo-web-form/src/assets/img/detail.svg +1 -0
  40. package/template/neo-web-form/src/assets/img/favicon.png +0 -0
  41. package/template/neo-web-form/src/assets/img/map.svg +1 -0
  42. package/template/neo-web-form/src/assets/img/search.svg +1 -0
  43. package/template/neo-web-form/src/assets/img/table.svg +1 -0
  44. package/template/neo-web-form/src/components/batchAddTable__c/index.tsx +1052 -0
  45. package/template/neo-web-form/src/components/batchAddTable__c/model.ts +90 -0
  46. package/template/neo-web-form/src/components/batchAddTable__c/style.scss +21 -0
  47. package/template/neo-web-form/src/components/batchAddTable__c/tableModal.scss +137 -0
  48. package/template/neo-web-form/src/components/listSummary__c/index.tsx +120 -0
  49. package/template/neo-web-form/src/components/listSummary__c/model.ts +69 -0
  50. package/template/neo-web-form/src/components/listSummary__c/style.scss +40 -0
  51. package/template/neo-web-form/src/utils/axiosFetcher.ts +37 -0
  52. package/template/neo-web-form/src/utils/queryObjectData.ts +112 -0
  53. package/template/neo-web-form/src/utils/xobjects.ts +167 -0
  54. package/template/neo-web-form/tsconfig.json +39 -0
  55. package/template/react-custom-cmp-template/package.json +1 -1
  56. package/template/react-ts-custom-cmp-template/package.json +4 -1
  57. package/template/vue2-custom-cmp-template/package.json +1 -1
@@ -0,0 +1,1052 @@
1
+ /**
2
+ * @file 批量新增表格组件
3
+ * @description 基于 XObject 实体数据源的可编辑表格,支持 Excel 导入与提交事件
4
+ */
5
+ import * as React from 'react';
6
+ import {
7
+ Table,
8
+ Button,
9
+ Space,
10
+ Spin,
11
+ Empty,
12
+ message,
13
+ Input,
14
+ Select,
15
+ DatePicker,
16
+ InputNumber,
17
+ Upload,
18
+ Modal,
19
+ Popconfirm,
20
+ } from 'antd';
21
+ import {
22
+ PlusOutlined,
23
+ DownloadOutlined,
24
+ UploadOutlined,
25
+ SaveOutlined,
26
+ CloseOutlined,
27
+ ReloadOutlined,
28
+ CopyOutlined,
29
+ DeleteOutlined,
30
+ } from '@ant-design/icons';
31
+ import * as XLSX from 'xlsx';
32
+ import moment from 'moment';
33
+ // @ts-ignore
34
+ import { xObject } from 'neo-open-api';
35
+ // @ts-ignore
36
+ import isEqual from 'lodash/isEqual';
37
+ // @ts-ignore
38
+ import { NeoEvent } from 'neo-ui-common';
39
+
40
+ import './style.scss';
41
+ import './tableModal.scss';
42
+
43
+ const { Option } = Select;
44
+
45
+ interface FieldInfo {
46
+ name: string;
47
+ label: string;
48
+ apiKey: string;
49
+ type: string;
50
+ itemType?: string;
51
+ checkitem?: any[];
52
+ selectitem?: any[];
53
+ required?: boolean;
54
+ referTo?: { apiKey: string; [key: string]: any };
55
+ }
56
+
57
+ interface BatchAddTableProps {
58
+ xObjectDataApi: {
59
+ xObjectApiKey: string;
60
+ fields?: string[];
61
+ fieldDescList?: any[];
62
+ };
63
+ data?: any;
64
+ tableTitle?: string;
65
+ /** 入口按钮文案,点击后打开批量导入弹窗 */
66
+ batchImportButtonTitle?: string;
67
+ /** 批量导入入口按钮水平对齐:left 靠左、center 居中、right 靠右,默认 right */
68
+ batchImportButtonAlign?: 'left' | 'center' | 'right';
69
+ className?: string;
70
+ }
71
+
72
+ interface RowRecord {
73
+ _rowKey: string;
74
+ [key: string]: any;
75
+ }
76
+
77
+ interface BatchAddTableState {
78
+ title?: string;
79
+ fieldList: FieldInfo[];
80
+ loading: boolean;
81
+ error: string | null;
82
+ rows: RowRecord[];
83
+ entityTypeList: any[];
84
+ submitting: boolean;
85
+ modalVisible: boolean;
86
+ }
87
+
88
+ let rowKeySeed = 0;
89
+ function nextRowKey() {
90
+ rowKeySeed += 1;
91
+ return `batch_row_${Date.now()}_${rowKeySeed}`;
92
+ }
93
+
94
+ function isEntityTypeField(field: FieldInfo) {
95
+ return field.type === 'entityType' || field.type === 'entitytype';
96
+ }
97
+
98
+ /** 关联类字段:统一用数字 ID 输入(不使用弹窗选择) */
99
+ function isRelationIdField(field: FieldInfo) {
100
+ if (isEntityTypeField(field)) return false;
101
+ const t = (field.type || '').toLowerCase();
102
+ if (t === 'lookup' || t === 'reference') return true;
103
+ return !!field.referTo;
104
+ }
105
+
106
+ function emptyRowForFields(fields: FieldInfo[]): RowRecord {
107
+ const row: RowRecord = { _rowKey: nextRowKey() };
108
+ fields.forEach((f) => {
109
+ if (f.apiKey === 'id') return;
110
+ if (f.type === 'multipicklist') {
111
+ row[f.apiKey] = [];
112
+ } else {
113
+ row[f.apiKey] = undefined;
114
+ }
115
+ });
116
+ return row;
117
+ }
118
+
119
+ /** Excel 序列日期(整数)转 moment,兼容未开启 cellDates 的导出文件 */
120
+ function momentFromExcelSerial(raw: number): moment.Moment | null {
121
+ if (!Number.isFinite(raw)) return null;
122
+ const whole = Math.floor(raw);
123
+ if (whole < 20000 || whole > 120000) return null;
124
+ const utcMs = (whole - 25569) * 86400 * 1000;
125
+ const m = moment.utc(utcMs);
126
+ if (!m.isValid()) return null;
127
+ const frac = raw - whole;
128
+ if (frac > 0.000001) {
129
+ m.add(Math.round(frac * 86400 * 1000), 'ms');
130
+ }
131
+ return m.local();
132
+ }
133
+
134
+ export default class BatchAddTable extends React.PureComponent<
135
+ BatchAddTableProps,
136
+ BatchAddTableState
137
+ > {
138
+ constructor(props: BatchAddTableProps) {
139
+ super(props);
140
+ this.state = {
141
+ fieldList: [],
142
+ loading: false,
143
+ error: null,
144
+ rows: [],
145
+ entityTypeList: [],
146
+ submitting: false,
147
+ modalVisible: false,
148
+ };
149
+ this.loadFieldList = this.loadFieldList.bind(this);
150
+ this.getEntityTypeList = this.getEntityTypeList.bind(this);
151
+ this.addRow = this.addRow.bind(this);
152
+ this.handleCellChange = this.handleCellChange.bind(this);
153
+ this.handleDownloadTemplate = this.handleDownloadTemplate.bind(this);
154
+ this.handleImportBeforeUpload = this.handleImportBeforeUpload.bind(this);
155
+ this.handleSubmit = this.handleSubmit.bind(this);
156
+ this.handleCancel = this.handleCancel.bind(this);
157
+ this.handleCopyRow = this.handleCopyRow.bind(this);
158
+ this.handleDeleteRow = this.handleDeleteRow.bind(this);
159
+ this.confirmReset = this.confirmReset.bind(this);
160
+ this.openModal = this.openModal.bind(this);
161
+ this.closeModal = this.closeModal.bind(this);
162
+ }
163
+
164
+ componentDidMount() {
165
+ const { xObjectApiKey } = this.props.xObjectDataApi || {};
166
+ if (xObjectApiKey) {
167
+ this.getEntityTypeList(xObjectApiKey);
168
+ this.loadFieldList();
169
+ }
170
+ }
171
+
172
+ async componentDidUpdate(prevProps: BatchAddTableProps) {
173
+ const { xObjectApiKey, fields } = this.props.xObjectDataApi || {};
174
+ const { xObjectApiKey: prevKey, fields: prevFields } =
175
+ prevProps.xObjectDataApi || {};
176
+ if (xObjectApiKey !== prevKey || !isEqual(fields, prevFields)) {
177
+ if (xObjectApiKey) {
178
+ await this.loadFieldList();
179
+ await this.getEntityTypeList(xObjectApiKey);
180
+ } else {
181
+ this.setState({ fieldList: [], rows: [], error: null });
182
+ }
183
+ }
184
+ }
185
+
186
+ getVisibleFields(): FieldInfo[] {
187
+ const { xObjectDataApi } = this.props;
188
+ const { fields } = xObjectDataApi || {};
189
+ let list = this.state.fieldList;
190
+ if (xObjectDataApi?.fieldDescList?.length) {
191
+ list = xObjectDataApi.fieldDescList;
192
+ }
193
+ if (fields && fields.length > 0) {
194
+ list = list.filter((f) => fields.includes(f.apiKey));
195
+ }
196
+ return list.filter((f) => f.apiKey !== 'id');
197
+ }
198
+
199
+ async getEntityTypeList(xObjectApiKey?: string) {
200
+ const key = xObjectApiKey || this.props.xObjectDataApi?.xObjectApiKey;
201
+ if (!key) return;
202
+ try {
203
+ const result = await xObject.getEntityTypeList(key);
204
+ if (result && result.status) {
205
+ this.setState({ entityTypeList: result.data || [] });
206
+ }
207
+ } catch (e) {
208
+ console.error('获取业务类型列表失败:', e);
209
+ }
210
+ }
211
+
212
+ async loadFieldList() {
213
+ const { xObjectDataApi } = this.props || {};
214
+ if (!xObjectDataApi?.xObjectApiKey) {
215
+ this.setState({ loading: false });
216
+ return;
217
+ }
218
+
219
+ this.setState({ loading: true, error: null });
220
+
221
+ try {
222
+ if (xObjectDataApi.fieldDescList?.length) {
223
+ const visible = this.getVisibleFieldsFromRaw(
224
+ xObjectDataApi.fieldDescList,
225
+ xObjectDataApi,
226
+ );
227
+ this.setState({
228
+ fieldList: xObjectDataApi.fieldDescList,
229
+ loading: false,
230
+ rows: visible.length ? [emptyRowForFields(visible)] : [],
231
+ });
232
+ return;
233
+ }
234
+
235
+ const resultData = await xObject.getDesc(xObjectDataApi.xObjectApiKey);
236
+ if (resultData && resultData.status) {
237
+ const result = resultData.data || {};
238
+ const fieldList = result.fields || [];
239
+ const visible = this.getVisibleFieldsFromRaw(fieldList, xObjectDataApi);
240
+ this.setState({
241
+ fieldList,
242
+ title: result.label,
243
+ loading: false,
244
+ rows: visible.length ? [emptyRowForFields(visible)] : [],
245
+ });
246
+ } else {
247
+ this.setState({
248
+ error: '获取字段列表失败',
249
+ loading: false,
250
+ rows: [],
251
+ });
252
+ }
253
+ } catch (error) {
254
+ console.error('获取字段列表失败:', error);
255
+ message.error('获取字段列表失败');
256
+ this.setState({
257
+ error: '获取字段列表失败',
258
+ loading: false,
259
+ rows: [],
260
+ });
261
+ }
262
+ }
263
+
264
+ getVisibleFieldsFromRaw(
265
+ fieldList: FieldInfo[],
266
+ xObjectDataApi: BatchAddTableProps['xObjectDataApi'],
267
+ ): FieldInfo[] {
268
+ const { fields } = xObjectDataApi || {};
269
+ let list = fieldList;
270
+ if (fields && fields.length > 0) {
271
+ list = list.filter((f) => fields.includes(f.apiKey));
272
+ }
273
+ return list.filter((f) => f.apiKey !== 'id');
274
+ }
275
+
276
+ addRow() {
277
+ const visible = this.getVisibleFields();
278
+ if (!visible.length) {
279
+ message.warning('暂无可用字段');
280
+ return;
281
+ }
282
+ this.setState((prev) => ({
283
+ rows: [...prev.rows, emptyRowForFields(visible)],
284
+ }));
285
+ }
286
+
287
+ handleCellChange(rowKey: string, apiKey: string, value: any) {
288
+ this.setState((prev) => ({
289
+ rows: prev.rows.map((row) =>
290
+ row._rowKey === rowKey ? { ...row, [apiKey]: value } : row,
291
+ ),
292
+ }));
293
+ }
294
+
295
+ resolvePicklistValue(field: FieldInfo, raw: any): any {
296
+ if (raw === undefined || raw === null || raw === '') return undefined;
297
+ const s = String(raw).trim();
298
+ const items = field.selectitem || [];
299
+ const byId = items.find(
300
+ (it) => it.id === raw || String(it.id) === s || it.apiKey === s,
301
+ );
302
+ if (byId) return byId.id;
303
+ const byLabel = items.find(
304
+ (it) =>
305
+ String(it.label).trim() === s ||
306
+ String(it.label).toLowerCase() === s.toLowerCase(),
307
+ );
308
+ return byLabel ? byLabel.id : raw;
309
+ }
310
+
311
+ resolveEntityTypeValue(raw: any, entityTypeList: any[]): any {
312
+ if (raw === undefined || raw === null || raw === '') return undefined;
313
+ const s = String(raw).trim();
314
+ const byId = entityTypeList.find(
315
+ (it) => it.id === raw || String(it.id) === s,
316
+ );
317
+ if (byId) return byId.id;
318
+ const byLabel = entityTypeList.find(
319
+ (it) => String(it.label).trim() === s,
320
+ );
321
+ return byLabel ? byLabel.id : raw;
322
+ }
323
+
324
+ parseImportedCell(
325
+ field: FieldInfo,
326
+ raw: any,
327
+ entityTypeList: any[],
328
+ ): any {
329
+ if (raw === undefined || raw === null || raw === '') return undefined;
330
+ if (raw instanceof Date) {
331
+ if (
332
+ field.type === 'date' ||
333
+ field.type === 'datetime' ||
334
+ field.type === 'time'
335
+ ) {
336
+ return moment(raw);
337
+ }
338
+ }
339
+ if (isRelationIdField(field)) {
340
+ if (typeof raw === 'number' && Number.isFinite(raw)) return raw;
341
+ const n = Number(String(raw).trim());
342
+ return Number.isNaN(n) ? undefined : n;
343
+ }
344
+ if (isEntityTypeField(field)) {
345
+ return this.resolveEntityTypeValue(raw, entityTypeList);
346
+ }
347
+ if (field.type === 'picklist') {
348
+ return this.resolvePicklistValue(field, raw);
349
+ }
350
+ if (field.type === 'multipicklist') {
351
+ const parts = String(raw)
352
+ .split(/[,,;;]/)
353
+ .map((p) => p.trim())
354
+ .filter(Boolean);
355
+ const items = field.checkitem || [];
356
+ return parts.map((p) => {
357
+ const byId = items.find(
358
+ (it) => it.id === p || String(it.id) === p || it.apiKey === p,
359
+ );
360
+ if (byId) return byId.id;
361
+ const byLabel = items.find((it) => String(it.label).trim() === p);
362
+ return byLabel ? byLabel.id : p;
363
+ });
364
+ }
365
+ if (field.type === 'int' || field.type === 'float') {
366
+ const n = Number(raw);
367
+ return Number.isNaN(n) ? undefined : n;
368
+ }
369
+ if (
370
+ field.type === 'date' ||
371
+ field.type === 'datetime' ||
372
+ field.type === 'time'
373
+ ) {
374
+ if (typeof raw === 'number') {
375
+ const fromSerial = momentFromExcelSerial(raw);
376
+ if (fromSerial) return fromSerial;
377
+ }
378
+ const m = moment(raw);
379
+ return m.isValid() ? m : undefined;
380
+ }
381
+ return raw;
382
+ }
383
+
384
+ buildHeaderIndexMap(
385
+ headerRow: any[],
386
+ fields: FieldInfo[],
387
+ ): Record<string, number> {
388
+ const map: Record<string, number> = {};
389
+ const headers = headerRow.map((c) =>
390
+ c === undefined || c === null ? '' : String(c).trim(),
391
+ );
392
+ fields.forEach((f) => {
393
+ const labelIdx = headers.findIndex((h) => h === f.label);
394
+ if (labelIdx >= 0) {
395
+ map[f.apiKey] = labelIdx;
396
+ return;
397
+ }
398
+ const keyIdx = headers.findIndex((h) => h === f.apiKey);
399
+ if (keyIdx >= 0) {
400
+ map[f.apiKey] = keyIdx;
401
+ }
402
+ });
403
+ return map;
404
+ }
405
+
406
+ handleDownloadTemplate() {
407
+ const fields = this.getVisibleFields();
408
+ if (!fields.length) {
409
+ message.warning('请先配置实体数据源字段');
410
+ return;
411
+ }
412
+ const headers = fields.map((f) => f.label);
413
+ const ws = XLSX.utils.aoa_to_sheet([headers]);
414
+ const wb = XLSX.utils.book_new();
415
+ XLSX.utils.book_append_sheet(wb, ws, '导入模板');
416
+ const name = `${this.props.xObjectDataApi?.xObjectApiKey || 'data'}_导入模板.xlsx`;
417
+ XLSX.writeFile(wb, name);
418
+ message.success('模板已下载');
419
+ }
420
+
421
+ handleImportBeforeUpload(file: File) {
422
+ const fields = this.getVisibleFields();
423
+ if (!fields.length) {
424
+ message.warning('暂无可用字段');
425
+ return false;
426
+ }
427
+ const reader = new FileReader();
428
+ reader.onload = (e) => {
429
+ try {
430
+ const data = new Uint8Array(e.target?.result as ArrayBuffer);
431
+ const wb = XLSX.read(data, { type: 'array', cellDates: true });
432
+ const sheetName = wb.SheetNames[0];
433
+ if (!sheetName) {
434
+ message.error('Excel 中未找到工作表');
435
+ return;
436
+ }
437
+ const sheet = wb.Sheets[sheetName];
438
+ const rowsAoA: any[][] = XLSX.utils.sheet_to_json(sheet, {
439
+ header: 1,
440
+ raw: true,
441
+ defval: '',
442
+ }) as any[][];
443
+ if (!rowsAoA.length) {
444
+ message.error('Excel 内容为空');
445
+ return;
446
+ }
447
+ const headerRow = rowsAoA[0] || [];
448
+ const colMap = this.buildHeaderIndexMap(headerRow, fields);
449
+ const missing = fields.filter((f) => colMap[f.apiKey] === undefined);
450
+ if (missing.length) {
451
+ message.error(
452
+ `表头无法匹配字段:${missing.map((m) => m.label).join('、')}`,
453
+ );
454
+ return;
455
+ }
456
+ const { entityTypeList } = this.state;
457
+ const newRows: RowRecord[] = [];
458
+ for (let r = 1; r < rowsAoA.length; r++) {
459
+ const line = rowsAoA[r];
460
+ if (!line || !line.some((c) => c !== '' && c != null)) continue;
461
+ const row: RowRecord = { _rowKey: nextRowKey() };
462
+ fields.forEach((f) => {
463
+ const idx = colMap[f.apiKey];
464
+ const raw = idx !== undefined ? line[idx] : undefined;
465
+ row[f.apiKey] = this.parseImportedCell(f, raw, entityTypeList);
466
+ });
467
+ newRows.push(row);
468
+ }
469
+ if (!newRows.length) {
470
+ message.warning('未解析到有效数据行');
471
+ return;
472
+ }
473
+ this.setState((prev) => ({
474
+ rows: [...prev.rows, ...newRows],
475
+ }));
476
+ message.success(`已导入 ${newRows.length} 行`);
477
+ } catch (err) {
478
+ console.error(err);
479
+ message.error('解析 Excel 失败');
480
+ }
481
+ };
482
+ reader.readAsArrayBuffer(file);
483
+ return false;
484
+ }
485
+
486
+ rowHasAnyValue(row: RowRecord, fields: FieldInfo[]): boolean {
487
+ return fields.some((f) => {
488
+ const v = row[f.apiKey];
489
+ if (v === undefined || v === null || v === '') return false;
490
+ if (Array.isArray(v) && v.length === 0) return false;
491
+ return true;
492
+ });
493
+ }
494
+
495
+ buildRowPayload(row: RowRecord, fields: FieldInfo[]): Record<string, any> {
496
+ const out: Record<string, any> = {};
497
+ fields.forEach((f) => {
498
+ let v = row[f.apiKey];
499
+ if (
500
+ v &&
501
+ (f.type === 'date' || f.type === 'datetime' || f.type === 'time') &&
502
+ moment.isMoment(v)
503
+ ) {
504
+ v = v.valueOf();
505
+ }
506
+ if (v !== undefined) {
507
+ out[f.apiKey] = v;
508
+ }
509
+ });
510
+ return out;
511
+ }
512
+
513
+ @NeoEvent.dispatch
514
+ onSubmit(eventData?: any) {
515
+ console.log('batchAddTable__c onSubmit:', eventData);
516
+ this.closeModal();
517
+ }
518
+
519
+ onCancel(eventData?: any) {
520
+ console.log('batchAddTable__c onCancel:', eventData);
521
+ }
522
+
523
+ /** 深拷贝单元格值(moment / 数组等可编辑类型) */
524
+ cloneRowCellValue(value: any): any {
525
+ if (value === undefined || value === null) {
526
+ return value;
527
+ }
528
+ if (moment.isMoment(value)) {
529
+ return value.clone();
530
+ }
531
+ if (Array.isArray(value)) {
532
+ return value.slice();
533
+ }
534
+ if (typeof value === 'object') {
535
+ try {
536
+ return JSON.parse(JSON.stringify(value));
537
+ } catch {
538
+ return value;
539
+ }
540
+ }
541
+ return value;
542
+ }
543
+
544
+ cloneRowRecord(record: RowRecord, fields: FieldInfo[]): RowRecord {
545
+ const row: RowRecord = { _rowKey: nextRowKey() };
546
+ fields.forEach((f) => {
547
+ row[f.apiKey] = this.cloneRowCellValue(record[f.apiKey]);
548
+ });
549
+ return row;
550
+ }
551
+
552
+ handleCopyRow(record: RowRecord) {
553
+ const fields = this.getVisibleFields();
554
+ const idx = this.state.rows.findIndex((r) => r._rowKey === record._rowKey);
555
+ if (idx < 0) return;
556
+ const cloned = this.cloneRowRecord(record, fields);
557
+ const newRows = [...this.state.rows];
558
+ newRows.splice(idx + 1, 0, cloned);
559
+ this.setState({ rows: newRows });
560
+ message.success('已在本行下方复制一行');
561
+ }
562
+
563
+ handleDeleteRow(record: RowRecord) {
564
+ const fields = this.getVisibleFields();
565
+ const newRows = this.state.rows.filter((r) => r._rowKey !== record._rowKey);
566
+ if (newRows.length === 0 && fields.length) {
567
+ newRows.push(emptyRowForFields(fields));
568
+ }
569
+ this.setState({ rows: newRows });
570
+ message.success('已删除该行');
571
+ }
572
+
573
+ openModal() {
574
+ this.setState({ modalVisible: true });
575
+ }
576
+
577
+ closeModal() {
578
+ this.setState({ modalVisible: false });
579
+ }
580
+
581
+ handleCancel() {
582
+ const { xObjectApiKey } = this.props.xObjectDataApi || {};
583
+ this.onCancel({
584
+ xObjectApiKey,
585
+ rowCount: this.state.rows.length,
586
+ });
587
+ this.closeModal();
588
+ }
589
+
590
+ confirmReset() {
591
+ const fields = this.getVisibleFields();
592
+ if (!fields.length) {
593
+ message.warning('暂无可用字段');
594
+ return;
595
+ }
596
+ this.setState({
597
+ rows: [emptyRowForFields(fields)],
598
+ });
599
+ message.success('已重置');
600
+ }
601
+
602
+ handleSubmit() {
603
+ const { xObjectApiKey } = this.props.xObjectDataApi || {};
604
+ const fields = this.getVisibleFields();
605
+ if (!xObjectApiKey) {
606
+ message.error('请先选择实体数据源');
607
+ return;
608
+ }
609
+ if (!fields.length) {
610
+ message.warning('暂无可用字段');
611
+ return;
612
+ }
613
+
614
+ const { rows } = this.state;
615
+ const dataRows = rows.filter((r) => this.rowHasAnyValue(r, fields));
616
+
617
+ if (!dataRows.length) {
618
+ message.warning('请至少填写一行数据');
619
+ return;
620
+ }
621
+
622
+ for (let i = 0; i < dataRows.length; i++) {
623
+ const row = dataRows[i];
624
+ for (const f of fields) {
625
+ if (!f.required) continue;
626
+ const v = row[f.apiKey];
627
+ const empty =
628
+ v === undefined ||
629
+ v === null ||
630
+ v === '' ||
631
+ (Array.isArray(v) && v.length === 0);
632
+ if (empty) {
633
+ message.error(`第 ${i + 1} 行「${f.label}」为必填项`);
634
+ return;
635
+ }
636
+ }
637
+ }
638
+
639
+ this.setState({ submitting: true });
640
+ const payload = dataRows.map((r) => this.buildRowPayload(r, fields));
641
+
642
+ try {
643
+ this.onSubmit({
644
+ xObjectApiKey,
645
+ rows: payload,
646
+ });
647
+ // message.success('已触发提交事件');
648
+ } finally {
649
+ this.setState({ submitting: false });
650
+ }
651
+ }
652
+
653
+ renderCellEditor(field: FieldInfo, record: RowRecord) {
654
+ const value = record[field.apiKey];
655
+ const onChange = (v: any) =>
656
+ this.handleCellChange(record._rowKey, field.apiKey, v);
657
+ const selectitem = field.selectitem || [];
658
+ const checkitem = field.checkitem || [];
659
+ const { entityTypeList } = this.state;
660
+
661
+ if (isRelationIdField(field)) {
662
+ return (
663
+ <InputNumber
664
+ className="batch-add-cell-editor"
665
+ placeholder="请输入关联记录 ID(数字)"
666
+ value={value}
667
+ onChange={onChange}
668
+ />
669
+ );
670
+ }
671
+
672
+ switch (field.type) {
673
+ case 'entityType':
674
+ case 'entitytype':
675
+ return (
676
+ <Select
677
+ className="batch-add-cell-editor"
678
+ placeholder="业务类型"
679
+ allowClear
680
+ value={value}
681
+ onChange={onChange}
682
+ style={{ minWidth: 140 }}
683
+ >
684
+ {entityTypeList.map((item) => (
685
+ <Option
686
+ key={item.apiKey}
687
+ value={item.id}
688
+ disabled={!item.active}
689
+ >
690
+ {item.label}
691
+ </Option>
692
+ ))}
693
+ </Select>
694
+ );
695
+ case 'picklist':
696
+ return (
697
+ <Select
698
+ className="batch-add-cell-editor"
699
+ placeholder={`请选择`}
700
+ allowClear
701
+ value={value}
702
+ onChange={onChange}
703
+ style={{ minWidth: 140 }}
704
+ >
705
+ {selectitem.map((item) => (
706
+ <Option key={item.apiKey} value={item.id}>
707
+ {item.label}
708
+ </Option>
709
+ ))}
710
+ </Select>
711
+ );
712
+ case 'multipicklist':
713
+ return (
714
+ <Select
715
+ className="batch-add-cell-editor"
716
+ placeholder={`请选择`}
717
+ mode="multiple"
718
+ allowClear
719
+ value={value}
720
+ onChange={onChange}
721
+ style={{ minWidth: 160 }}
722
+ >
723
+ {checkitem.map((item) => (
724
+ <Option key={item.apiKey} value={item.id}>
725
+ {item.label}
726
+ </Option>
727
+ ))}
728
+ </Select>
729
+ );
730
+ case 'textarea':
731
+ return (
732
+ <Input.TextArea
733
+ className="batch-add-cell-editor"
734
+ placeholder={`请输入`}
735
+ rows={2}
736
+ value={value}
737
+ onChange={(e) => onChange(e.target.value)}
738
+ />
739
+ );
740
+ case 'datetime':
741
+ return (
742
+ <DatePicker
743
+ className="batch-add-cell-editor"
744
+ showTime
745
+ format="YYYY/MM/DD HH:mm:ss"
746
+ value={value || null}
747
+ onChange={onChange}
748
+ />
749
+ );
750
+ case 'date':
751
+ return (
752
+ <DatePicker
753
+ className="batch-add-cell-editor"
754
+ format="YYYY/MM/DD"
755
+ value={value || null}
756
+ onChange={onChange}
757
+ />
758
+ );
759
+ case 'time':
760
+ return (
761
+ <DatePicker
762
+ className="batch-add-cell-editor"
763
+ format="HH:mm:ss"
764
+ showTime
765
+ value={value || null}
766
+ onChange={onChange}
767
+ />
768
+ );
769
+ case 'int':
770
+ case 'float':
771
+ return (
772
+ <InputNumber
773
+ className="batch-add-cell-editor"
774
+ placeholder={`请输入数字`}
775
+ value={value}
776
+ onChange={onChange}
777
+ />
778
+ );
779
+ case 'email':
780
+ return (
781
+ <Input
782
+ className="batch-add-cell-editor"
783
+ type="email"
784
+ placeholder={`请输入邮箱`}
785
+ value={value}
786
+ onChange={(e) => onChange(e.target.value)}
787
+ />
788
+ );
789
+ case 'url':
790
+ return (
791
+ <Input
792
+ className="batch-add-cell-editor"
793
+ type="url"
794
+ placeholder={`请输入 URL`}
795
+ value={value}
796
+ onChange={(e) => onChange(e.target.value)}
797
+ />
798
+ );
799
+ case 'phone':
800
+ return (
801
+ <Input
802
+ className="batch-add-cell-editor"
803
+ type="tel"
804
+ placeholder={`请输入电话`}
805
+ value={value}
806
+ onChange={(e) => onChange(e.target.value)}
807
+ />
808
+ );
809
+ default:
810
+ return (
811
+ <Input
812
+ className="batch-add-cell-editor"
813
+ placeholder={`请输入`}
814
+ value={value}
815
+ onChange={(e) => onChange(e.target.value)}
816
+ />
817
+ );
818
+ }
819
+ }
820
+
821
+ render() {
822
+ const { loading, error, rows, submitting, title } = this.state;
823
+ const { tableTitle, className, data } = this.props;
824
+ const curAmisData = data || {};
825
+ const systemInfo = curAmisData.__NeoSystemInfo || {};
826
+ const { xObjectApiKey } = this.props.xObjectDataApi || {};
827
+ const fields = this.getVisibleFields();
828
+
829
+ const columns: any[] = fields.map((field) => ({
830
+ title: field.label,
831
+ dataIndex: field.apiKey,
832
+ key: field.apiKey,
833
+ width: field.type === 'textarea' ? 220 : 160,
834
+ render: (_: any, record: RowRecord) =>
835
+ this.renderCellEditor(field, record),
836
+ }));
837
+
838
+ columns.unshift({
839
+ title: '序号',
840
+ dataIndex: '_index',
841
+ key: '_index',
842
+ width: 56,
843
+ fixed: 'left' as const,
844
+ render: (_: any, __: RowRecord, index: number) => index + 1,
845
+ });
846
+
847
+ columns.push({
848
+ title: '操作',
849
+ key: '_action',
850
+ dataIndex: '_action',
851
+ width: 148,
852
+ fixed: 'right' as const,
853
+ align: 'center' as const,
854
+ render: (_: any, record: RowRecord) => (
855
+ <Space size={4}>
856
+ <Button
857
+ type="link"
858
+ size="small"
859
+ icon={<CopyOutlined />}
860
+ className="batch-add-copy-btn"
861
+ onClick={() => this.handleCopyRow(record)}
862
+ >
863
+ 复制
864
+ </Button>
865
+ <Button
866
+ type="link"
867
+ size="small"
868
+ danger
869
+ icon={<DeleteOutlined />}
870
+ className="batch-add-delete-btn"
871
+ onClick={() => this.handleDeleteRow(record)}
872
+ >
873
+ 删除
874
+ </Button>
875
+ </Space>
876
+ ),
877
+ });
878
+
879
+ const displayTitle =
880
+ tableTitle || title || '批量新增数据';
881
+ const batchImportButtonTitle =
882
+ this.props.batchImportButtonTitle ?? '批量导入';
883
+ const batchImportButtonAlign =
884
+ this.props.batchImportButtonAlign ?? 'right';
885
+
886
+ const showModalFooter = !error && !!xObjectApiKey && fields.length > 0;
887
+
888
+ const modalFooter = showModalFooter ? (
889
+ <div className="batch-add-modal-footer-inner">
890
+ <Space size="middle" wrap>
891
+ <Button
892
+ size="large"
893
+ icon={<CloseOutlined />}
894
+ onClick={this.handleCancel}
895
+ disabled={submitting}
896
+ >
897
+ 取消
898
+ </Button>
899
+ <Popconfirm
900
+ title={
901
+ <span>
902
+ <div>确认重置</div>
903
+ <div
904
+ style={{
905
+ marginTop: 8,
906
+ fontWeight: 400,
907
+ color: 'rgba(0,0,0,0.65)',
908
+ maxWidth: 280,
909
+ }}
910
+ >
911
+ 重置后将清空当前表格中所有已填数据,并恢复为一行空白行。是否继续?
912
+ </div>
913
+ </span>
914
+ }
915
+ onConfirm={this.confirmReset}
916
+ okText="确定重置"
917
+ cancelText="取消"
918
+ okButtonProps={{ danger: true }}
919
+ >
920
+ <span>
921
+ <Button
922
+ size="large"
923
+ icon={<ReloadOutlined />}
924
+ disabled={submitting}
925
+ >
926
+ 重置
927
+ </Button>
928
+ </span>
929
+ </Popconfirm>
930
+ <Button
931
+ type="primary"
932
+ size="large"
933
+ className="batch-add-submit-btn"
934
+ icon={<SaveOutlined />}
935
+ loading={submitting}
936
+ onClick={this.handleSubmit}
937
+ >
938
+ 提交
939
+ </Button>
940
+ </Space>
941
+ </div>
942
+ ) : null;
943
+
944
+ const modalBody = (
945
+ <div className="batch-add-modal-layout">
946
+ <div className="batch-add-modal-toolbar-wrap">
947
+ <Space wrap>
948
+ <Button
949
+ type="primary"
950
+ icon={<PlusOutlined />}
951
+ onClick={this.addRow}
952
+ disabled={!xObjectApiKey || !fields.length}
953
+ >
954
+ 添加一行
955
+ </Button>
956
+ <Button
957
+ icon={<DownloadOutlined />}
958
+ onClick={this.handleDownloadTemplate}
959
+ disabled={!fields.length}
960
+ >
961
+ 下载 Excel 模板
962
+ </Button>
963
+ <Upload
964
+ accept=".xlsx,.xls"
965
+ showUploadList={false}
966
+ beforeUpload={this.handleImportBeforeUpload}
967
+ >
968
+ <Button
969
+ icon={<UploadOutlined />}
970
+ disabled={!xObjectApiKey || !fields.length}
971
+ >
972
+ Excel 导入
973
+ </Button>
974
+ </Upload>
975
+ </Space>
976
+ </div>
977
+
978
+ <div className="batch-add-modal-table-scroll">
979
+ <Spin spinning={loading} tip="加载字段信息...">
980
+ {error ? (
981
+ <Empty
982
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
983
+ description={
984
+ <div>
985
+ <div style={{ color: '#ff4d4f', marginBottom: 8 }}>
986
+ {error}
987
+ </div>
988
+ <Button type="primary" onClick={this.loadFieldList}>
989
+ 重新加载
990
+ </Button>
991
+ </div>
992
+ }
993
+ />
994
+ ) : !xObjectApiKey ? (
995
+ <Empty
996
+ description="请先选择实体数据源"
997
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
998
+ />
999
+ ) : !fields.length ? (
1000
+ <Empty
1001
+ description="暂无可用字段,请在数据源中勾选字段"
1002
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
1003
+ />
1004
+ ) : (
1005
+ <div className="table-wrap">
1006
+ <Table
1007
+ rowKey="_rowKey"
1008
+ columns={columns as any}
1009
+ dataSource={rows}
1010
+ pagination={false}
1011
+ scroll={{ x: 'max-content' }}
1012
+ bordered
1013
+ size="small"
1014
+ />
1015
+ </div>
1016
+ )}
1017
+ </Spin>
1018
+ </div>
1019
+ </div>
1020
+ );
1021
+
1022
+ return (
1023
+ <div className={`batchAddTable__c ${className || ''}`}>
1024
+ <div
1025
+ className={`batch-add-entry-toolbar batch-add-entry-toolbar--${batchImportButtonAlign}`}
1026
+ >
1027
+ <Button type="primary" onClick={this.openModal}>
1028
+ {batchImportButtonTitle}
1029
+ </Button>
1030
+ </div>
1031
+ <Modal
1032
+ title={
1033
+ <>
1034
+ {displayTitle}
1035
+ {systemInfo.tenantName ? `【${systemInfo.tenantName}】` : ''}
1036
+ </>
1037
+ }
1038
+ visible={this.state.modalVisible}
1039
+ onCancel={this.closeModal}
1040
+ footer={modalFooter}
1041
+ width="95%"
1042
+ style={{ maxWidth: 1200 }}
1043
+ className="batch-add-table-modal"
1044
+ destroyOnClose={false}
1045
+ maskClosable={false}
1046
+ >
1047
+ {modalBody}
1048
+ </Modal>
1049
+ </div>
1050
+ );
1051
+ }
1052
+ }