yidaconnector 2026.6.11

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 (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +383 -0
  3. package/bin/yida.js +670 -0
  4. package/lib/app/form-navigation.js +58 -0
  5. package/lib/app/get-schema.js +538 -0
  6. package/lib/auth/auth.js +294 -0
  7. package/lib/auth/cdp-browser-login.js +390 -0
  8. package/lib/auth/codex-login.js +71 -0
  9. package/lib/auth/login.js +475 -0
  10. package/lib/auth/org.js +363 -0
  11. package/lib/auth/qr-login.js +1563 -0
  12. package/lib/core/chalk.js +384 -0
  13. package/lib/core/check-update.js +82 -0
  14. package/lib/core/cli-error.js +39 -0
  15. package/lib/core/command-manifest.js +106 -0
  16. package/lib/core/env-cmd.js +545 -0
  17. package/lib/core/env-manager.js +601 -0
  18. package/lib/core/env.js +287 -0
  19. package/lib/core/i18n.js +177 -0
  20. package/lib/core/locales/ar.js +805 -0
  21. package/lib/core/locales/de.js +805 -0
  22. package/lib/core/locales/en.js +1623 -0
  23. package/lib/core/locales/es.js +805 -0
  24. package/lib/core/locales/fr.js +805 -0
  25. package/lib/core/locales/hi.js +805 -0
  26. package/lib/core/locales/ja.js +1197 -0
  27. package/lib/core/locales/ko.js +807 -0
  28. package/lib/core/locales/pt.js +805 -0
  29. package/lib/core/locales/vi.js +805 -0
  30. package/lib/core/locales/zh-HK.js +1233 -0
  31. package/lib/core/locales/zh.js +1584 -0
  32. package/lib/core/query-data.js +781 -0
  33. package/lib/core/redact.js +100 -0
  34. package/lib/core/utils.js +799 -0
  35. package/lib/core/yida-client.js +117 -0
  36. package/package.json +94 -0
  37. package/project/config.json +4 -0
  38. package/project/pages/src/demo-birthday-game.oyd.jsx +832 -0
  39. package/project/pages/src/demo-chip-insight.oyd.jsx +983 -0
  40. package/project/pages/src/demo-compat-smoke.oyd.jsx +58 -0
  41. package/project/pages/src/demo-crm-batch-entry.oyd.jsx +805 -0
  42. package/project/pages/src/demo-crm-dashboard.oyd.jsx +677 -0
  43. package/project/pages/src/demo-future-vision-2026.oyd.jsx +1102 -0
  44. package/project/pages/src/demo-ppt.oyd.jsx +1192 -0
  45. package/project/pages/src/demo-salary-calculator.oyd.jsx +904 -0
  46. package/project/pages/src/yidaconnector-knowledge-doc.oyd.jsx +1714 -0
  47. package/project/prd/demo-birthday-game.md +39 -0
  48. package/project/prd/demo-crm.md +463 -0
  49. package/project/prd/demo-dingtalk-ai-solution-center.md +425 -0
  50. package/project/prd/demo-future-vision-2026.md +78 -0
  51. package/project/prd/demo-salary-calculator.md +101 -0
  52. package/scripts/build-skills-package.js +406 -0
  53. package/scripts/check-syntax.js +59 -0
  54. package/scripts/demo-dws.sh +106 -0
  55. package/scripts/e2e-real/cleanup.js +67 -0
  56. package/scripts/e2e-real/fixtures/form-fields.json +18 -0
  57. package/scripts/e2e-real/full-runner.js +1566 -0
  58. package/scripts/e2e-real/runner.js +293 -0
  59. package/scripts/e2e-real/skill-coverage.js +115 -0
  60. package/scripts/generate-command-docs.js +109 -0
  61. package/scripts/nightly-smoke.js +134 -0
  62. package/scripts/postinstall.js +545 -0
  63. package/scripts/solution-center-runner.js +368 -0
  64. package/scripts/validate-ci.sh +50 -0
  65. package/scripts/validate-command-manifest.js +119 -0
  66. package/scripts/validate-package-size.js +78 -0
  67. package/scripts/validate-skills.js +247 -0
  68. package/scripts/validate-structure.js +66 -0
  69. package/yida-skills/SKILL.md +163 -0
  70. package/yida-skills/references/yida-api.md +1309 -0
  71. package/yida-skills/skills/large-file-write/SKILL.md +91 -0
  72. package/yida-skills/skills/large-file-write/references/write-patterns.md +149 -0
  73. package/yida-skills/skills/large-file-write/scripts/write.js +157 -0
  74. package/yida-skills/skills/yida-data-management/SKILL.md +252 -0
  75. package/yida-skills/skills/yida-data-management/references/api-matrix.md +49 -0
  76. package/yida-skills/skills/yida-data-management/references/data-format-guide.md +159 -0
  77. package/yida-skills/skills/yida-data-management/references/verified-endpoints.md +62 -0
  78. package/yida-skills/skills/yida-login/SKILL.md +159 -0
  79. package/yida-skills/skills/yida-logout/SKILL.md +67 -0
@@ -0,0 +1,805 @@
1
+ // ============================================================
2
+ // CRM 批量数据录入页面
3
+ // 支持:客户信息、商机管理、客户回访 三种表单的批量录入
4
+ // ============================================================
5
+
6
+ // ── 配置区 ─────────────────────────────────────────────────
7
+
8
+ var APP_TYPE = 'APP_MRNQXYA404IHD2P6JHSD';
9
+ var DRAFT_KEY_PREFIX = 'yida_crm_batch_entry_';
10
+
11
+ var FORM_CONFIG = {
12
+ customer: {
13
+ formUuid: 'FORM-48FB56AFA31E43B09BDE30B7165D3E92D7WT',
14
+ name: '客户信息',
15
+ columns: [
16
+ { label: '客户名称', field: 'textField_l1h42qsqz', type: 'text', required: true },
17
+ { label: '客户类型', field: 'radioField_l1h43yhd1', type: 'select', options: ['企业客户', '个人客户'] },
18
+ { label: '客户级别', field: 'selectField_l1h54iojj', type: 'select', options: ['A级', 'B级', 'C级', 'D级'] },
19
+ { label: '联系人', field: 'textField_l1h55ogqo', type: 'text' },
20
+ { label: '联系电话', field: 'textField_l1h566r7t', type: 'text' },
21
+ { label: '联系邮箱', field: 'textField_l1h57ofjl', type: 'text' }
22
+ ]
23
+ },
24
+ opportunity: {
25
+ formUuid: 'FORM-ECCF67D4CC994968B2675C9CBD0F04A536P0',
26
+ name: '商机管理',
27
+ columns: [
28
+ { label: '商机名称', field: 'textField_ozj624rw0', type: 'text', required: true },
29
+ { label: '商机阶段', field: 'selectField_ozj64pdqc', type: 'select', options: ['初步接触', '需求确认', '方案报价', '谈判', '赢单', '输单'] },
30
+ { label: '预计金额(万)', field: 'numberField_ozj651thb', type: 'number' },
31
+ { label: '成交概率(%)', field: 'numberField_ozj66qh91', type: 'number' },
32
+ { label: '商机来源', field: 'radioField_ozj69jqrp', type: 'select', options: ['新开发', '老客户', '合作伙伴'] }
33
+ ]
34
+ },
35
+ visit: {
36
+ formUuid: 'FORM-21CC57767B184FB389DD0E9E765F028D77OA',
37
+ name: '客户回访',
38
+ columns: [
39
+ { label: '回访方式', field: 'radioField_pjlt40nnj', type: 'select', options: ['电话', '上门', '视频', '邮件'] },
40
+ { label: '满意度(1-5)', field: 'rateField_pjlt5hp2q', type: 'number' },
41
+ { label: '回访记录', field: 'textareaField_pjlt8k3dg', type: 'text' }
42
+ ]
43
+ }
44
+ };
45
+
46
+ var FORM_OPTIONS = [
47
+ { key: 'customer', label: '客户信息' },
48
+ { key: 'opportunity', label: '商机管理' },
49
+ { key: 'visit', label: '客户回访' }
50
+ ];
51
+
52
+ // ── 状态 ───────────────────────────────────────────────────
53
+
54
+ var _customState = {
55
+ selectedForm: 'customer', // 当前选择的表单类型
56
+ rows: [], // 行数据列表
57
+ selectedRows: {}, // 选中的行 { rowId: true }
58
+ submitting: false, // 是否正在提交
59
+ submitProgress: { current: 0, total: 0 }, // 提交进度
60
+ submitResult: null // { success: number, failed: number }
61
+ };
62
+
63
+ // ── 工具函数 ───────────────────────────────────────────────
64
+
65
+ function generateRowId() {
66
+ return 'row_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7);
67
+ }
68
+
69
+ function getCurrentFormConfig() {
70
+ return FORM_CONFIG[_customState.selectedForm];
71
+ }
72
+
73
+ function getDraftKey() {
74
+ return DRAFT_KEY_PREFIX + _customState.selectedForm;
75
+ }
76
+
77
+ function createEmptyRow() {
78
+ var row = { id: generateRowId(), _status: 'valid', _errors: {} };
79
+ var config = getCurrentFormConfig();
80
+ config.columns.forEach(function(col) { row[col.field] = ''; });
81
+ return row;
82
+ }
83
+
84
+ function validateRow(row) {
85
+ var errors = {};
86
+ var config = getCurrentFormConfig();
87
+ config.columns.forEach(function(col) {
88
+ if (col.required && !row[col.field]) {
89
+ errors[col.field] = col.label + '不能为空';
90
+ }
91
+ if (col.type === 'number' && row[col.field]) {
92
+ var num = Number(row[col.field]);
93
+ if (isNaN(num)) {
94
+ errors[col.field] = col.label + '必须是数字';
95
+ }
96
+ }
97
+ });
98
+ return errors;
99
+ }
100
+
101
+ function saveDraft(rows) {
102
+ try {
103
+ localStorage.setItem(getDraftKey(), JSON.stringify(rows));
104
+ } catch (e) { /* ignore */ }
105
+ }
106
+
107
+ function loadDraft() {
108
+ try {
109
+ var saved = localStorage.getItem(getDraftKey());
110
+ return saved ? JSON.parse(saved) : null;
111
+ } catch (e) {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ function clearDraft() {
117
+ try { localStorage.removeItem(getDraftKey()); } catch (e) { /* ignore */ }
118
+ }
119
+
120
+ // ── 状态管理 ───────────────────────────────────────────────
121
+
122
+ export function getCustomState(key) {
123
+ if (key) return _customState[key];
124
+ return _.clone(_customState);
125
+ }
126
+
127
+ export function setCustomState(newState) {
128
+ Object.keys(newState).forEach(function(key) {
129
+ _customState[key] = newState[key];
130
+ });
131
+ this.forceUpdate();
132
+ }
133
+
134
+ export function forceUpdate() {
135
+ this.setState({ timestamp: new Date().getTime() });
136
+ }
137
+
138
+ // ── 生命周期 ───────────────────────────────────────────────
139
+
140
+ export function didMount() {
141
+ // 绑定粘贴事件
142
+ var self = this;
143
+ self._pasteHandler = function(e) { self.handlePaste(e); };
144
+ document.addEventListener('paste', self._pasteHandler);
145
+
146
+ // 恢复草稿
147
+ var draft = loadDraft();
148
+ if (draft && draft.length > 0) {
149
+ _customState.rows = draft;
150
+ } else {
151
+ _customState.rows = [createEmptyRow(), createEmptyRow(), createEmptyRow()];
152
+ }
153
+ this.forceUpdate();
154
+ }
155
+
156
+ export function didUnmount() {
157
+ // 清理粘贴事件监听
158
+ if (this._pasteHandler) {
159
+ document.removeEventListener('paste', this._pasteHandler);
160
+ }
161
+ }
162
+
163
+ // ── 表单切换 ───────────────────────────────────────────────
164
+
165
+ export function handleFormChange(e) {
166
+ var newFormKey = e.target.value;
167
+ // 保存当前表单的草稿
168
+ saveDraft(_customState.rows);
169
+
170
+ // 切换表单
171
+ _customState.selectedForm = newFormKey;
172
+ _customState.selectedRows = {};
173
+ _customState.submitResult = null;
174
+
175
+ // 加载新表单的草稿
176
+ var draft = loadDraft();
177
+ if (draft && draft.length > 0) {
178
+ _customState.rows = draft;
179
+ } else {
180
+ _customState.rows = [createEmptyRow(), createEmptyRow(), createEmptyRow()];
181
+ }
182
+ this.forceUpdate();
183
+ }
184
+
185
+ // ── 行操作 ─────────────────────────────────────────────────
186
+
187
+ export function addRow() {
188
+ _customState.rows.push(createEmptyRow());
189
+ saveDraft(_customState.rows);
190
+ this.forceUpdate();
191
+ }
192
+
193
+ export function deleteRow(rowId) {
194
+ _customState.rows = _customState.rows.filter(function(row) {
195
+ return row.id !== rowId;
196
+ });
197
+ delete _customState.selectedRows[rowId];
198
+ if (_customState.rows.length === 0) {
199
+ _customState.rows.push(createEmptyRow());
200
+ }
201
+ saveDraft(_customState.rows);
202
+ this.forceUpdate();
203
+ }
204
+
205
+ export function deleteSelectedRows() {
206
+ var selectedIds = Object.keys(_customState.selectedRows);
207
+ if (selectedIds.length === 0) {
208
+ this.utils.toast({ title: '请先选择要删除的行', type: 'warning' });
209
+ return;
210
+ }
211
+ _customState.rows = _customState.rows.filter(function(row) {
212
+ return !_customState.selectedRows[row.id];
213
+ });
214
+ _customState.selectedRows = {};
215
+ if (_customState.rows.length === 0) {
216
+ _customState.rows.push(createEmptyRow());
217
+ }
218
+ saveDraft(_customState.rows);
219
+ this.forceUpdate();
220
+ }
221
+
222
+ export function clearAllRows() {
223
+ _customState.rows = [createEmptyRow(), createEmptyRow(), createEmptyRow()];
224
+ _customState.selectedRows = {};
225
+ _customState.submitResult = null;
226
+ clearDraft();
227
+ this.forceUpdate();
228
+ this.utils.toast({ title: '已清空所有数据', type: 'success' });
229
+ }
230
+
231
+ export function toggleRowSelection(rowId) {
232
+ if (_customState.selectedRows[rowId]) {
233
+ delete _customState.selectedRows[rowId];
234
+ } else {
235
+ _customState.selectedRows[rowId] = true;
236
+ }
237
+ this.forceUpdate();
238
+ }
239
+
240
+ export function toggleSelectAll() {
241
+ var allSelected = _customState.rows.length > 0 &&
242
+ _customState.rows.every(function(row) { return _customState.selectedRows[row.id]; });
243
+
244
+ if (allSelected) {
245
+ _customState.selectedRows = {};
246
+ } else {
247
+ _customState.rows.forEach(function(row) {
248
+ _customState.selectedRows[row.id] = true;
249
+ });
250
+ }
251
+ this.forceUpdate();
252
+ }
253
+
254
+ export function updateCell(rowId, field, value) {
255
+ var row = _customState.rows.find(function(r) { return r.id === rowId; });
256
+ if (!row) return;
257
+ row[field] = value;
258
+ // 清除该字段的错误
259
+ if (row._errors[field]) {
260
+ delete row._errors[field];
261
+ row._status = Object.keys(row._errors).length === 0 ? 'valid' : 'invalid';
262
+ }
263
+ saveDraft(_customState.rows);
264
+ // 不触发重渲染,静默更新
265
+ }
266
+
267
+ // ── Excel 粘贴导入 ─────────────────────────────────────────
268
+
269
+ export function handlePaste(event) {
270
+ var clipboardData = event.clipboardData || window.clipboardData;
271
+ if (!clipboardData) return;
272
+ var text = clipboardData.getData('text');
273
+ if (!text) return;
274
+
275
+ var config = getCurrentFormConfig();
276
+ var lines = text.trim().split('\n');
277
+ var newRows = lines.map(function(line) {
278
+ var cells = line.split('\t');
279
+ var row = createEmptyRow();
280
+ config.columns.forEach(function(col, index) {
281
+ if (cells[index] !== undefined) {
282
+ row[col.field] = cells[index].trim();
283
+ }
284
+ });
285
+ return row;
286
+ });
287
+
288
+ // 过滤掉全空行
289
+ newRows = newRows.filter(function(row) {
290
+ return config.columns.some(function(col) { return row[col.field]; });
291
+ });
292
+
293
+ if (newRows.length === 0) return;
294
+
295
+ // 追加到现有行(过滤掉现有的全空行)
296
+ var nonEmptyExisting = _customState.rows.filter(function(row) {
297
+ return config.columns.some(function(col) { return row[col.field]; });
298
+ });
299
+ _customState.rows = nonEmptyExisting.concat(newRows);
300
+ saveDraft(_customState.rows);
301
+ this.forceUpdate();
302
+ this.utils.toast({ title: '已导入 ' + newRows.length + ' 行数据', type: 'success' });
303
+ }
304
+
305
+ // ── 批量提交 ───────────────────────────────────────────────
306
+
307
+ export function submitAll() {
308
+ var self = this;
309
+ var config = getCurrentFormConfig();
310
+
311
+ // 1. 验证所有行
312
+ var hasError = false;
313
+ _customState.rows.forEach(function(row) {
314
+ var errors = validateRow(row);
315
+ row._errors = errors;
316
+ if (Object.keys(errors).length > 0) {
317
+ row._status = 'invalid';
318
+ hasError = true;
319
+ }
320
+ });
321
+
322
+ if (hasError) {
323
+ self.forceUpdate();
324
+ self.utils.toast({ title: '请修正表格中的错误后再提交', type: 'error' });
325
+ return;
326
+ }
327
+
328
+ // 2. 过滤掉全空行
329
+ var rowsToSubmit = _customState.rows.filter(function(row) {
330
+ return config.columns.some(function(col) { return row[col.field]; });
331
+ });
332
+
333
+ if (rowsToSubmit.length === 0) {
334
+ self.utils.toast({ title: '请至少填写一行数据', type: 'error' });
335
+ return;
336
+ }
337
+
338
+ _customState.submitting = true;
339
+ _customState.submitProgress = { current: 0, total: rowsToSubmit.length };
340
+ _customState.submitResult = null;
341
+ self.forceUpdate();
342
+
343
+ // 3. 批量提交(并发)
344
+ var completedCount = 0;
345
+ var promises = rowsToSubmit.map(function(row) {
346
+ var formDataJson = {};
347
+ config.columns.forEach(function(col) {
348
+ var val = row[col.field];
349
+ // 数字类型转换
350
+ if (col.type === 'number' && val) {
351
+ formDataJson[col.field] = Number(val);
352
+ } else {
353
+ formDataJson[col.field] = val;
354
+ }
355
+ });
356
+
357
+ row._status = 'submitting';
358
+
359
+ return self.utils.yida.saveFormData({
360
+ formUuid: config.formUuid,
361
+ appType: APP_TYPE,
362
+ formDataJson: JSON.stringify(formDataJson)
363
+ }).then(function() {
364
+ row._status = 'submitted';
365
+ completedCount++;
366
+ _customState.submitProgress.current = completedCount;
367
+ self.forceUpdate();
368
+ }).catch(function(err) {
369
+ row._status = 'invalid';
370
+ row._errors._submit = err.message || '提交失败';
371
+ completedCount++;
372
+ _customState.submitProgress.current = completedCount;
373
+ self.forceUpdate();
374
+ });
375
+ });
376
+
377
+ Promise.all(promises).then(function() {
378
+ var successCount = rowsToSubmit.filter(function(r) { return r._status === 'submitted'; }).length;
379
+ var failedCount = rowsToSubmit.filter(function(r) { return r._status === 'invalid'; }).length;
380
+
381
+ _customState.submitting = false;
382
+ _customState.submitResult = { success: successCount, failed: failedCount };
383
+
384
+ if (failedCount === 0) {
385
+ clearDraft();
386
+ _customState.rows = [createEmptyRow(), createEmptyRow(), createEmptyRow()];
387
+ self.utils.toast({ title: '全部提交成功,共 ' + successCount + ' 条', type: 'success' });
388
+ } else {
389
+ self.utils.toast({
390
+ title: '提交完成:' + successCount + ' 条成功,' + failedCount + ' 条失败',
391
+ type: 'error'
392
+ });
393
+ }
394
+ self.forceUpdate();
395
+ }).catch(function(err) {
396
+ _customState.submitting = false;
397
+ self.forceUpdate();
398
+ self.utils.toast({ title: '提交异常:' + err.message, type: 'error' });
399
+ });
400
+ }
401
+
402
+ // ── 渲染辅助 ───────────────────────────────────────────────
403
+
404
+ export function renderCellInput(row, col) {
405
+ var self = this;
406
+ var value = row[col.field];
407
+ var hasError = !!row._errors[col.field];
408
+ var isSubmitted = row._status === 'submitted';
409
+
410
+ var baseInputStyle = {
411
+ width: '100%',
412
+ height: '32px',
413
+ padding: '0 8px',
414
+ border: '1px solid ' + (hasError ? '#ff4d4f' : '#d9d9d9'),
415
+ borderRadius: '4px',
416
+ fontSize: '13px',
417
+ outline: 'none',
418
+ background: isSubmitted ? '#f6ffed' : '#fff',
419
+ boxSizing: 'border-box'
420
+ };
421
+
422
+ if (col.type === 'select') {
423
+ return (
424
+ <select
425
+ defaultValue={value}
426
+ disabled={isSubmitted}
427
+ onChange={(e) => { self.updateCell(row.id, col.field, e.target.value); }}
428
+ style={baseInputStyle}
429
+ >
430
+ <option value="">请选择</option>
431
+ {(col.options || []).map(function(opt) {
432
+ return <option key={opt} value={opt}>{opt}</option>;
433
+ })}
434
+ </select>
435
+ );
436
+ }
437
+
438
+ return (
439
+ <input
440
+ type={col.type === 'number' ? 'number' : 'text'}
441
+ defaultValue={value}
442
+ disabled={isSubmitted}
443
+ placeholder={col.required ? col.label + '(必填)' : col.label}
444
+ onChange={(e) => { self.updateCell(row.id, col.field, e.target.value); }}
445
+ style={baseInputStyle}
446
+ />
447
+ );
448
+ }
449
+
450
+ // ── 主渲染 ─────────────────────────────────────────────────
451
+
452
+ export function renderJsx() {
453
+ var self = this;
454
+ var timestamp = this.state.timestamp;
455
+ var config = getCurrentFormConfig();
456
+ var columns = config.columns;
457
+
458
+ var allSelected = _customState.rows.length > 0 &&
459
+ _customState.rows.every(function(row) { return _customState.selectedRows[row.id]; });
460
+ var selectedCount = Object.keys(_customState.selectedRows).length;
461
+
462
+ // ── 样式定义 ─────────────────────────────────────────────
463
+ var styles = {
464
+ container: {
465
+ padding: '16px',
466
+ background: '#f5f7fa',
467
+ minHeight: '100vh',
468
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
469
+ },
470
+ header: {
471
+ display: 'flex',
472
+ justifyContent: 'space-between',
473
+ alignItems: 'center',
474
+ marginBottom: '16px',
475
+ padding: '16px 20px',
476
+ background: '#fff',
477
+ borderRadius: '8px',
478
+ boxShadow: '0 1px 3px rgba(0,0,0,0.08)'
479
+ },
480
+ title: {
481
+ fontSize: '18px',
482
+ fontWeight: 600,
483
+ color: '#1f2937'
484
+ },
485
+ formSelect: {
486
+ height: '36px',
487
+ padding: '0 12px',
488
+ fontSize: '14px',
489
+ border: '1px solid #d9d9d9',
490
+ borderRadius: '6px',
491
+ background: '#fff',
492
+ marginLeft: '12px',
493
+ cursor: 'pointer'
494
+ },
495
+ toolbar: {
496
+ display: 'flex',
497
+ gap: '8px',
498
+ marginBottom: '12px',
499
+ padding: '12px 16px',
500
+ background: '#fff',
501
+ borderRadius: '8px',
502
+ boxShadow: '0 1px 3px rgba(0,0,0,0.08)',
503
+ flexWrap: 'wrap',
504
+ alignItems: 'center'
505
+ },
506
+ btn: {
507
+ height: '32px',
508
+ padding: '0 14px',
509
+ fontSize: '13px',
510
+ border: '1px solid #d9d9d9',
511
+ borderRadius: '6px',
512
+ background: '#fff',
513
+ cursor: 'pointer',
514
+ display: 'flex',
515
+ alignItems: 'center',
516
+ gap: '4px'
517
+ },
518
+ btnPrimary: {
519
+ height: '32px',
520
+ padding: '0 16px',
521
+ fontSize: '13px',
522
+ border: 'none',
523
+ borderRadius: '6px',
524
+ background: '#1677ff',
525
+ color: '#fff',
526
+ cursor: 'pointer',
527
+ display: 'flex',
528
+ alignItems: 'center',
529
+ gap: '4px'
530
+ },
531
+ btnDanger: {
532
+ height: '32px',
533
+ padding: '0 14px',
534
+ fontSize: '13px',
535
+ border: '1px solid #ff4d4f',
536
+ borderRadius: '6px',
537
+ background: '#fff',
538
+ color: '#ff4d4f',
539
+ cursor: 'pointer'
540
+ },
541
+ tableContainer: {
542
+ background: '#fff',
543
+ borderRadius: '8px',
544
+ overflow: 'hidden',
545
+ boxShadow: '0 1px 3px rgba(0,0,0,0.08)'
546
+ },
547
+ tableHeader: {
548
+ display: 'grid',
549
+ gridTemplateColumns: '40px ' + columns.map(function() { return '1fr'; }).join(' ') + ' 60px',
550
+ background: '#fafafa',
551
+ borderBottom: '1px solid #f0f0f0',
552
+ padding: '10px 12px'
553
+ },
554
+ headerCell: {
555
+ fontSize: '13px',
556
+ fontWeight: 600,
557
+ color: '#595959'
558
+ },
559
+ tableRow: {
560
+ display: 'grid',
561
+ gridTemplateColumns: '40px ' + columns.map(function() { return '1fr'; }).join(' ') + ' 60px',
562
+ padding: '8px 12px',
563
+ borderBottom: '1px solid #f0f0f0',
564
+ alignItems: 'start'
565
+ },
566
+ checkbox: {
567
+ width: '16px',
568
+ height: '16px',
569
+ cursor: 'pointer',
570
+ marginTop: '8px'
571
+ },
572
+ cellWrapper: {
573
+ paddingRight: '8px'
574
+ },
575
+ errorText: {
576
+ fontSize: '11px',
577
+ color: '#ff4d4f',
578
+ marginTop: '2px'
579
+ },
580
+ deleteBtn: {
581
+ border: 'none',
582
+ background: 'none',
583
+ color: '#ff4d4f',
584
+ cursor: 'pointer',
585
+ fontSize: '16px',
586
+ padding: '4px 8px'
587
+ },
588
+ progressBar: {
589
+ marginTop: '12px',
590
+ padding: '12px 16px',
591
+ background: '#fff',
592
+ borderRadius: '8px',
593
+ boxShadow: '0 1px 3px rgba(0,0,0,0.08)'
594
+ },
595
+ progressTrack: {
596
+ height: '8px',
597
+ background: '#f0f0f0',
598
+ borderRadius: '4px',
599
+ overflow: 'hidden',
600
+ marginTop: '8px'
601
+ },
602
+ progressFill: {
603
+ height: '100%',
604
+ background: '#1677ff',
605
+ borderRadius: '4px',
606
+ transition: 'width 0.3s ease'
607
+ },
608
+ resultBox: {
609
+ marginTop: '12px',
610
+ padding: '12px 16px',
611
+ borderRadius: '8px',
612
+ fontSize: '14px'
613
+ },
614
+ tip: {
615
+ marginTop: '8px',
616
+ fontSize: '12px',
617
+ color: '#8c8c8c',
618
+ textAlign: 'right'
619
+ }
620
+ };
621
+
622
+ return (
623
+ <div style={styles.container}>
624
+ {/* 隐藏的 timestamp 用于触发重渲染 */}
625
+ <div style={{ display: 'none' }}>{timestamp}</div>
626
+
627
+ {/* 标题栏 */}
628
+ <div style={styles.header}>
629
+ <div style={{ display: 'flex', alignItems: 'center' }}>
630
+ <span style={styles.title}>批量数据录入</span>
631
+ <select
632
+ value={_customState.selectedForm}
633
+ onChange={(e) => { self.handleFormChange(e); }}
634
+ style={styles.formSelect}
635
+ >
636
+ {FORM_OPTIONS.map(function(opt) {
637
+ return <option key={opt.key} value={opt.key}>{opt.label}</option>;
638
+ })}
639
+ </select>
640
+ </div>
641
+ <div style={{ fontSize: '13px', color: '#8c8c8c' }}>
642
+ 当前:{config.name}({_customState.rows.length} 行)
643
+ </div>
644
+ </div>
645
+
646
+ {/* 工具栏 */}
647
+ <div style={styles.toolbar}>
648
+ <button
649
+ onClick={() => { self.addRow(); }}
650
+ style={styles.btn}
651
+ >
652
+ ➕ 添加行
653
+ </button>
654
+ <button
655
+ onClick={() => { self.deleteSelectedRows(); }}
656
+ style={Object.assign({}, styles.btn, selectedCount > 0 ? { borderColor: '#ff4d4f', color: '#ff4d4f' } : {})}
657
+ >
658
+ 🗑 删除选中 {selectedCount > 0 ? '(' + selectedCount + ')' : ''}
659
+ </button>
660
+ <button
661
+ onClick={() => { self.clearAllRows(); }}
662
+ style={styles.btn}
663
+ >
664
+ 🔄 清空
665
+ </button>
666
+ <div style={{ flex: 1 }} />
667
+ <span style={{ fontSize: '12px', color: '#8c8c8c', marginRight: '8px' }}>
668
+ 💡 提示:可直接从 Excel 复制数据粘贴导入
669
+ </span>
670
+ <button
671
+ onClick={() => { self.submitAll(); }}
672
+ disabled={_customState.submitting}
673
+ style={Object.assign({}, styles.btnPrimary, _customState.submitting ? { background: '#bfbfbf', cursor: 'not-allowed' } : {})}
674
+ >
675
+ {_customState.submitting ? '⏳ 提交中...' : '✓ 批量提交'}
676
+ </button>
677
+ </div>
678
+
679
+ {/* 表格 */}
680
+ <div style={styles.tableContainer}>
681
+ {/* 表头 */}
682
+ <div style={styles.tableHeader}>
683
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
684
+ <input
685
+ type="checkbox"
686
+ checked={allSelected}
687
+ onChange={() => { self.toggleSelectAll(); }}
688
+ style={styles.checkbox}
689
+ />
690
+ </div>
691
+ {columns.map(function(col) {
692
+ return (
693
+ <div key={col.field} style={styles.headerCell}>
694
+ {col.required && <span style={{ color: '#ff4d4f', marginRight: '2px' }}>*</span>}
695
+ {col.label}
696
+ </div>
697
+ );
698
+ })}
699
+ <div style={Object.assign({}, styles.headerCell, { textAlign: 'center' })}>操作</div>
700
+ </div>
701
+
702
+ {/* 数据行 */}
703
+ {_customState.rows.map(function(row) {
704
+ var rowBg = row._status === 'submitted' ? '#f6ffed'
705
+ : row._status === 'invalid' ? '#fff2f0'
706
+ : row._status === 'submitting' ? '#e6f7ff'
707
+ : '#fff';
708
+
709
+ return (
710
+ <div key={row.id} style={Object.assign({}, styles.tableRow, { background: rowBg })}>
711
+ {/* 选择框 */}
712
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
713
+ <input
714
+ type="checkbox"
715
+ checked={!!_customState.selectedRows[row.id]}
716
+ disabled={row._status === 'submitted'}
717
+ onChange={() => { self.toggleRowSelection(row.id); }}
718
+ style={styles.checkbox}
719
+ />
720
+ </div>
721
+
722
+ {/* 数据列 */}
723
+ {columns.map(function(col) {
724
+ return (
725
+ <div key={col.field} style={styles.cellWrapper}>
726
+ {self.renderCellInput(row, col)}
727
+ {row._errors[col.field] && (
728
+ <div style={styles.errorText}>{row._errors[col.field]}</div>
729
+ )}
730
+ {row._errors._submit && col === columns[0] && (
731
+ <div style={styles.errorText}>{row._errors._submit}</div>
732
+ )}
733
+ </div>
734
+ );
735
+ })}
736
+
737
+ {/* 操作列 */}
738
+ <div style={{ textAlign: 'center' }}>
739
+ {row._status === 'submitted' && (
740
+ <span style={{ color: '#52c41a', fontSize: '16px' }}>✓</span>
741
+ )}
742
+ {row._status === 'submitting' && (
743
+ <span style={{ color: '#1677ff', fontSize: '12px' }}>提交中</span>
744
+ )}
745
+ {row._status !== 'submitted' && row._status !== 'submitting' && (
746
+ <button
747
+ onClick={() => { self.deleteRow(row.id); }}
748
+ style={styles.deleteBtn}
749
+ title="删除此行"
750
+ >
751
+ 🗑
752
+ </button>
753
+ )}
754
+ </div>
755
+ </div>
756
+ );
757
+ })}
758
+ </div>
759
+
760
+ {/* 提交进度条 */}
761
+ {_customState.submitting && (
762
+ <div style={styles.progressBar}>
763
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
764
+ <span style={{ fontSize: '14px', fontWeight: 500 }}>提交进度</span>
765
+ <span style={{ fontSize: '13px', color: '#1677ff' }}>
766
+ {_customState.submitProgress.current} / {_customState.submitProgress.total}
767
+ </span>
768
+ </div>
769
+ <div style={styles.progressTrack}>
770
+ <div
771
+ style={Object.assign({}, styles.progressFill, {
772
+ width: (_customState.submitProgress.total > 0
773
+ ? Math.round(_customState.submitProgress.current / _customState.submitProgress.total * 100)
774
+ : 0) + '%'
775
+ })}
776
+ />
777
+ </div>
778
+ </div>
779
+ )}
780
+
781
+ {/* 提交结果 */}
782
+ {_customState.submitResult && (
783
+ <div style={Object.assign({}, styles.resultBox, {
784
+ background: _customState.submitResult.failed === 0 ? '#f6ffed' : '#fff2f0',
785
+ border: '1px solid ' + (_customState.submitResult.failed === 0 ? '#b7eb8f' : '#ffccc7')
786
+ })}>
787
+ 提交完成:
788
+ <span style={{ color: '#52c41a', fontWeight: 600 }}>
789
+ {_customState.submitResult.success} 条成功
790
+ </span>
791
+ {_customState.submitResult.failed > 0 && (
792
+ <span style={{ color: '#ff4d4f', fontWeight: 600, marginLeft: '8px' }}>
793
+ {_customState.submitResult.failed} 条失败(请修正红色行后重新提交)
794
+ </span>
795
+ )}
796
+ </div>
797
+ )}
798
+
799
+ {/* 草稿提示 */}
800
+ <div style={styles.tip}>
801
+ 数据已自动保存为草稿,刷新页面后可继续编辑
802
+ </div>
803
+ </div>
804
+ );
805
+ }