openyida 2026.7.27 → 2026.7.29-beta.1

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 (38) hide show
  1. package/lib/app/canvas-compile.js +33 -2
  2. package/lib/app/publish.js +47 -3
  3. package/lib/core/sample.js +6 -0
  4. package/lib/samples/yida-canvas-table-form/table-form-batch-submit.canvas.jsx +511 -0
  5. package/lib/samples/yida-rechart/trend-combo.canvas.jsx +253 -0
  6. package/package.json +1 -1
  7. package/scripts/build-skills-package.js +12 -8
  8. package/scripts/postinstall.js +4 -4
  9. package/scripts/validate-skills.js +91 -30
  10. package/yida-skills/SKILL.md +14 -5
  11. package/yida-skills/skills/yida-canvas-table-form/SKILL.md +151 -0
  12. package/yida-skills/skills/yida-dashboard/SKILL.md +143 -147
  13. package/yida-skills/skills/yida-dashboard/references/interaction-patterns.md +4 -2
  14. package/yida-skills/skills/yida-dashboard/references/pitfalls.md +2 -0
  15. package/yida-skills/skills/yida-dashboard/references/structure-and-layout.md +97 -320
  16. package/yida-skills/skills/yida-dashboard/references/theme-presets.md +1 -1
  17. package/yida-skills/skills/yida-data-management/SKILL.md +32 -2
  18. package/yida-skills/skills/yida-data-source-connectors/SKILL.md +106 -64
  19. package/yida-skills/skills/yida-density/SKILL.md +118 -110
  20. package/yida-skills/skills/yida-flash-note-to-prd/SKILL.md +3 -1
  21. package/yida-skills/skills/yida-nav-shell/SKILL.md +125 -74
  22. package/yida-skills/skills/yida-nav-shell/references/nav-shell-patterns.md +43 -32
  23. package/yida-skills/skills/yida-page-uiux/SKILL.md +11 -2
  24. package/yida-skills/skills/yida-page-uiux/references/dashboard/chart-contracts.md +1 -0
  25. package/yida-skills/skills/yida-page-uiux/references/scenes/dashboard.md +2 -2
  26. package/yida-skills/skills/yida-page-uiux/references/visual-decision-engine.md +1 -1
  27. package/yida-skills/skills/yida-page-uiux/workflow/output-decision-block.md +3 -3
  28. package/yida-skills/skills/yida-page-uiux/workflow/step-0-nav-shape.md +4 -4
  29. package/yida-skills/skills/yida-page-uiux/workflow/step-5-icon-and-assets.md +2 -2
  30. package/yida-skills/skills/yida-page-uiux/workflow/step-6-deai-selfcheck.md +1 -1
  31. package/yida-skills/skills/yida-ppt-slider/SKILL.md +185 -113
  32. package/yida-skills/skills/yida-ppt-slider/references/dark-tech-theme.md +7 -4
  33. package/yida-skills/skills/yida-ppt-slider/references/echarts-race-example.md +2 -0
  34. package/yida-skills/skills/yida-ppt-slider/references/examples.md +29 -7
  35. package/yida-skills/skills/yida-rechart/SKILL.md +134 -0
  36. package/yida-skills/skills/yida-tingji/SKILL.md +4 -3
  37. package/yida-skills/skills-index.json +124 -35
  38. package/yida-skills/skills/yida-ppt/SKILL.md +0 -46
@@ -33,6 +33,7 @@ const {
33
33
  assertNoEmojiInArtifactName,
34
34
  assertNoEmojiInText,
35
35
  } = require('../core/no-emoji-guard');
36
+ const { CliError, isCliError } = require('../core/cli-error');
36
37
 
37
38
  /**
38
39
  * 依赖 → window 别名白名单。
@@ -382,14 +383,44 @@ function compileCanvasLocal(source, options = {}) {
382
383
  function compileCanvas(source, options = {}) {
383
384
  return new Promise((resolve, reject) => {
384
385
  if (typeof source !== 'string' || source.trim() === '') {
385
- reject(new Error('canvas 编译源码为空'));
386
+ reject(new CliError('canvas 编译源码为空', {
387
+ code: 'OPENYIDA_CANVAS_COMPILE_EMPTY_SOURCE',
388
+ details: {
389
+ stage: 'canvas_compile',
390
+ sourcePath: options.sourcePath,
391
+ },
392
+ }));
386
393
  return;
387
394
  }
388
395
  try {
389
396
  resolve(compileCanvasLocal(source, options));
390
397
  } catch (compileError) {
398
+ if (isCliError(compileError)) {
399
+ reject(compileError);
400
+ return;
401
+ }
391
402
  const detail = compileError && compileError.message ? compileError.message : String(compileError);
392
- reject(new Error(`Code Canvas 本地编译失败: ${detail}`));
403
+ if (
404
+ compileError &&
405
+ /^OPENYIDA_/.test(String(compileError.code || '')) &&
406
+ compileError.details !== undefined
407
+ ) {
408
+ reject(new CliError(`Code Canvas 本地编译失败: ${detail}`, {
409
+ code: compileError.code,
410
+ details: compileError.details,
411
+ }));
412
+ return;
413
+ }
414
+ reject(new CliError(`Code Canvas 本地编译失败: ${detail}`, {
415
+ code: 'OPENYIDA_CANVAS_COMPILE_FAILED',
416
+ details: {
417
+ stage: 'canvas_compile',
418
+ sourcePath: options.sourcePath,
419
+ causeCode: compileError && compileError.code,
420
+ causeName: compileError && compileError.name,
421
+ loc: compileError && compileError.loc,
422
+ },
423
+ }));
393
424
  }
394
425
  });
395
426
  }
@@ -32,6 +32,7 @@ const { createAuthRef, isTokenAuthRef } = require('../core/yida-client');
32
32
  const { requireSchemaServerRevision } = require('../core/server-revision');
33
33
  const { t } = require('../core/i18n');
34
34
  const { banner, step, label, success, fail, warn, info, error, result, usage, hint } = require('../core/chalk');
35
+ const { CliError } = require('../core/cli-error');
35
36
  const { compileSource } = require('./page-compiler');
36
37
  const { compileCanvas } = require('./canvas-compile');
37
38
  const { runLintCheck } = require('./page-linter');
@@ -97,7 +98,7 @@ function parseArgs(argv = process.argv.slice(2)) {
97
98
  const force = args.includes('--force');
98
99
  const canvas = args.includes('--canvas');
99
100
  const help = args.includes('--help') || args.includes('-h');
100
- const filteredArgs = args.filter(arg => arg !== '--skip-lint' && arg !== '--health-check' && arg !== '--check' && arg !== '--compat' && arg !== '--modern' && arg !== '--force' && arg !== '--canvas' && arg !== '--help' && arg !== '-h');
101
+ const filteredArgs = args.filter(arg => arg !== '--skip-lint' && arg !== '--health-check' && arg !== '--check' && arg !== '--compat' && arg !== '--modern' && arg !== '--force' && arg !== '--canvas' && arg !== '--json' && arg !== '--help' && arg !== '-h');
101
102
 
102
103
  if (help) {
103
104
  usage(t('publish.usage'), t('publish.example'));
@@ -122,6 +123,40 @@ function parseArgs(argv = process.argv.slice(2)) {
122
123
  };
123
124
  }
124
125
 
126
+ function buildCanvasCompileCliError(errorObject, sourcePath) {
127
+ const code = errorObject && errorObject.code
128
+ ? errorObject.code
129
+ : 'OPENYIDA_CANVAS_COMPILE_FAILED';
130
+ const detail = errorObject && errorObject.message ? errorObject.message : String(errorObject);
131
+ const baseDetails = errorObject && errorObject.details && typeof errorObject.details === 'object'
132
+ ? { ...errorObject.details }
133
+ : {};
134
+
135
+ return new CliError(`${t('publish.canvas_compile_failed', detail)}\nError Code: ${code}`, {
136
+ code,
137
+ details: {
138
+ ...baseDetails,
139
+ stage: 'canvas_compile',
140
+ sourcePath,
141
+ },
142
+ });
143
+ }
144
+
145
+ function enableQuietForJson(argv = []) {
146
+ if (!Array.isArray(argv) || !argv.includes('--json')) {
147
+ return () => {};
148
+ }
149
+ const previousQuiet = process.env.YIDA_QUIET;
150
+ process.env.YIDA_QUIET = '1';
151
+ return () => {
152
+ if (previousQuiet === undefined) {
153
+ delete process.env.YIDA_QUIET;
154
+ } else {
155
+ process.env.YIDA_QUIET = previousQuiet;
156
+ }
157
+ };
158
+ }
159
+
125
160
  function normalizeSourcePathForHint(sourceFile) {
126
161
  return String(sourceFile || '')
127
162
  .replace(/\\/g, '/')
@@ -686,6 +721,16 @@ function sendHealthCheckRequest(pageUrl, cookies) {
686
721
  // ── 主流程 ────────────────────────────────────────────
687
722
 
688
723
  async function main(argv) {
724
+ const rawArgv = argv || process.argv.slice(2);
725
+ const restoreQuiet = enableQuietForJson(rawArgv);
726
+ try {
727
+ return await runMain(rawArgv);
728
+ } finally {
729
+ restoreQuiet();
730
+ }
731
+ }
732
+
733
+ async function runMain(argv) {
689
734
  const options = parseArgs(argv);
690
735
  if (options.help) {
691
736
  return options;
@@ -722,8 +767,7 @@ async function main(argv) {
722
767
  try {
723
768
  canvasResult = await compileCanvas(initialSourceCode, { sourcePath });
724
769
  } catch (canvasCompileError) {
725
- fail(t('publish.canvas_compile_failed', canvasCompileError.message));
726
- process.exit(1);
770
+ throw buildCanvasCompileCliError(canvasCompileError, sourcePath);
727
771
  }
728
772
  success(t('publish.canvas_compile_done'));
729
773
  } else {
@@ -60,6 +60,9 @@ const SAMPLES = {
60
60
  'todo-mvc': 'todo-mvc.canvas.jsx',
61
61
  'workbench-home': 'workbench-home.canvas.jsx',
62
62
  },
63
+ 'yida-rechart': {
64
+ 'trend-combo': 'trend-combo.canvas.jsx',
65
+ },
63
66
  'yida-create-app': {
64
67
  'ipd-app-template': 'ipd-app-template.js',
65
68
  },
@@ -69,6 +72,9 @@ const SAMPLES = {
69
72
  'yida-density': {
70
73
  'density-switch-page': 'density-switch-page.js',
71
74
  },
75
+ 'yida-canvas-table-form': {
76
+ 'table-form-batch-submit': 'table-form-batch-submit.canvas.jsx',
77
+ },
72
78
  'yida-table-form': {
73
79
  'table-form-batch-submit': 'table-form-batch-submit.js',
74
80
  },
@@ -0,0 +1,511 @@
1
+ /**
2
+ * Code Canvas + antd 表格批量录入样例。
3
+ *
4
+ * 写入契约:
5
+ * - props.writeBridge.verified === true
6
+ * - props.writeBridge.saveRow(payload, context) 是已验证的数据写入函数
7
+ * - props.fieldMap 提供真实 fieldId:name/status/dueDate/quantity
8
+ *
9
+ * 未提供上述契约时,页面只支持本地草稿与校验,提交保持禁用。
10
+ */
11
+
12
+ import React, { useEffect, useMemo, useState } from 'react';
13
+ import {
14
+ Alert,
15
+ Button,
16
+ ConfigProvider,
17
+ DatePicker,
18
+ Input,
19
+ InputNumber,
20
+ Modal,
21
+ Select,
22
+ Space,
23
+ Table,
24
+ Tag,
25
+ Typography,
26
+ } from 'antd';
27
+ import dayjs from 'dayjs';
28
+
29
+ const { Text, Title } = Typography;
30
+ const BATCH_SIZE = 10;
31
+ const STATUS_OPTIONS = [
32
+ { label: '待处理', value: '待处理' },
33
+ { label: '进行中', value: '进行中' },
34
+ { label: '已完成', value: '已完成' },
35
+ ];
36
+
37
+ function createRow(seed) {
38
+ return {
39
+ _rowId: 'row_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8),
40
+ _status: 'draft',
41
+ _errors: {},
42
+ _submitError: '',
43
+ name: '',
44
+ status: '待处理',
45
+ dueDate: '',
46
+ quantity: 1,
47
+ ...(seed || {}),
48
+ };
49
+ }
50
+
51
+ function loadDraft(key) {
52
+ try {
53
+ const value = JSON.parse(localStorage.getItem(key) || '[]');
54
+ return Array.isArray(value) && value.length
55
+ ? value.map((item) => {
56
+ const base = createRow();
57
+ return {
58
+ ...base,
59
+ ...(item || {}),
60
+ _rowId: item && item._rowId ? item._rowId : base._rowId,
61
+ _errors: item && item._errors ? item._errors : {},
62
+ _submitError: item && item._submitError ? item._submitError : '',
63
+ };
64
+ })
65
+ : [createRow()];
66
+ } catch (error) {
67
+ return [createRow()];
68
+ }
69
+ }
70
+
71
+ function validateRow(row) {
72
+ const errors = {};
73
+ if (!String(row.name || '').trim()) errors.name = '请输入事项名称';
74
+ if (!row.status) errors.status = '请选择状态';
75
+ if (!row.dueDate) errors.dueDate = '请选择计划日期';
76
+ if (!Number.isFinite(Number(row.quantity)) || Number(row.quantity) < 1) {
77
+ errors.quantity = '数量必须大于 0';
78
+ }
79
+ return errors;
80
+ }
81
+
82
+ function isFieldMapReady(fieldMap) {
83
+ return Boolean(
84
+ fieldMap &&
85
+ fieldMap.name &&
86
+ fieldMap.status &&
87
+ fieldMap.dueDate &&
88
+ fieldMap.quantity
89
+ );
90
+ }
91
+
92
+ function toPayload(row, fieldMap) {
93
+ return {
94
+ [fieldMap.name]: String(row.name).trim(),
95
+ [fieldMap.status]: row.status,
96
+ [fieldMap.dueDate]: row.dueDate,
97
+ [fieldMap.quantity]: Number(row.quantity),
98
+ };
99
+ }
100
+
101
+ function getBridgeState(props) {
102
+ const writeBridge = props && props.writeBridge;
103
+ const fieldMap = props && props.fieldMap;
104
+ if (!writeBridge || writeBridge.verified !== true || typeof writeBridge.saveRow !== 'function') {
105
+ return { ready: false, reason: '未注入已验证的同源 fetch、连接器或数据写入桥。' };
106
+ }
107
+ if (!isFieldMapReady(fieldMap)) {
108
+ return { ready: false, reason: '未提供由真实 Schema 解析出的 fieldMap。' };
109
+ }
110
+ return { ready: true, reason: '', writeBridge, fieldMap };
111
+ }
112
+
113
+ function YidaComp(props) {
114
+ const formUuid = props && props.formUuid ? props.formUuid : 'sample-unbound';
115
+ const draftKey = 'openyida_canvas_table_form_draft_' + formUuid;
116
+ const [rows, setRows] = useState(() => loadDraft(draftKey));
117
+ const [submitting, setSubmitting] = useState(false);
118
+ const [summary, setSummary] = useState(null);
119
+ const bridgeState = useMemo(
120
+ () => getBridgeState(props),
121
+ [props && props.writeBridge, props && props.fieldMap]
122
+ );
123
+
124
+ useEffect(() => {
125
+ try {
126
+ const editableRows = rows.filter((row) => row._status !== 'submitted');
127
+ if (editableRows.length) {
128
+ localStorage.setItem(draftKey, JSON.stringify(rows));
129
+ } else {
130
+ localStorage.removeItem(draftKey);
131
+ }
132
+ } catch (error) {
133
+ // localStorage 不可用时仍保留当前页面内状态。
134
+ }
135
+ }, [draftKey, rows]);
136
+
137
+ function updateCell(rowId, field, value) {
138
+ setRows((current) => current.map((row) => {
139
+ if (row._rowId !== rowId || row._status === 'submitted' || row._status === 'submitting') {
140
+ return row;
141
+ }
142
+ const nextErrors = { ...(row._errors || {}) };
143
+ delete nextErrors[field];
144
+ return {
145
+ ...row,
146
+ [field]: value,
147
+ _status: 'draft',
148
+ _errors: nextErrors,
149
+ _submitError: '',
150
+ };
151
+ }));
152
+ setSummary(null);
153
+ }
154
+
155
+ function addRow() {
156
+ setRows((current) => [...current, createRow()]);
157
+ }
158
+
159
+ function addDemoRows() {
160
+ setRows((current) => [
161
+ ...current.filter((row) => String(row.name || '').trim()),
162
+ createRow({ name: '华东门店补货确认', status: '待处理', dueDate: '2026-07-30', quantity: 12 }),
163
+ createRow({ name: '重点客户回访', status: '进行中', dueDate: '2026-07-31', quantity: 6 }),
164
+ createRow({ name: '月末库存复核', status: '待处理', dueDate: '2026-08-01', quantity: 18 }),
165
+ ]);
166
+ }
167
+
168
+ function removeRow(rowId) {
169
+ setRows((current) => {
170
+ const next = current.filter((row) => row._rowId !== rowId || row._status === 'submitted');
171
+ return next.length ? next : [createRow()];
172
+ });
173
+ }
174
+
175
+ async function performSubmit(candidates) {
176
+ setSubmitting(true);
177
+ setSummary(null);
178
+ let successCount = 0;
179
+ let failureCount = 0;
180
+
181
+ for (let index = 0; index < candidates.length; index += BATCH_SIZE) {
182
+ const batch = candidates.slice(index, index + BATCH_SIZE);
183
+ const batchIds = new Set(batch.map((row) => row._rowId));
184
+ setRows((current) => current.map((row) => (
185
+ batchIds.has(row._rowId)
186
+ ? { ...row, _status: 'submitting', _submitError: '' }
187
+ : row
188
+ )));
189
+
190
+ const results = await Promise.all(batch.map(async (row) => {
191
+ try {
192
+ const value = await bridgeState.writeBridge.saveRow(
193
+ toPayload(row, bridgeState.fieldMap),
194
+ {
195
+ rowId: row._rowId,
196
+ idempotencyKey: 'canvas-table:' + formUuid + ':' + row._rowId,
197
+ }
198
+ );
199
+ if (!value || (value.success !== true && !value.formInstId)) {
200
+ throw new Error('数据桥未返回可确认写入成功的 success 或 formInstId。');
201
+ }
202
+ return { rowId: row._rowId, ok: true, value };
203
+ } catch (error) {
204
+ return {
205
+ rowId: row._rowId,
206
+ ok: false,
207
+ error: error && error.message ? error.message : '提交失败,请检查数据桥返回值。',
208
+ };
209
+ }
210
+ }));
211
+
212
+ successCount += results.filter((result) => result.ok).length;
213
+ failureCount += results.filter((result) => !result.ok).length;
214
+ const resultMap = new Map(results.map((result) => [result.rowId, result]));
215
+ setRows((current) => current.map((row) => {
216
+ const result = resultMap.get(row._rowId);
217
+ if (!result) return row;
218
+ return result.ok
219
+ ? {
220
+ ...row,
221
+ _status: 'submitted',
222
+ _errors: {},
223
+ _submitError: '',
224
+ _formInstId: result.value && result.value.formInstId,
225
+ }
226
+ : {
227
+ ...row,
228
+ _status: 'failed',
229
+ _submitError: result.error,
230
+ };
231
+ }));
232
+ }
233
+
234
+ setSummary({ successCount, failureCount });
235
+ setSubmitting(false);
236
+ }
237
+
238
+ function confirmSubmit() {
239
+ if (!bridgeState.ready) return;
240
+
241
+ const candidates = rows.filter((row) => row._status !== 'submitted');
242
+ const validated = candidates.map((row) => {
243
+ const errors = validateRow(row);
244
+ return {
245
+ ...row,
246
+ _errors: errors,
247
+ _status: Object.keys(errors).length ? 'invalid' : 'draft',
248
+ };
249
+ });
250
+ const validatedMap = new Map(validated.map((row) => [row._rowId, row]));
251
+ setRows((current) => current.map((row) => validatedMap.get(row._rowId) || row));
252
+
253
+ const invalidCount = validated.filter((row) => Object.keys(row._errors).length).length;
254
+ if (invalidCount) {
255
+ setSummary({ successCount: 0, failureCount: invalidCount, validationOnly: true });
256
+ return;
257
+ }
258
+ if (!validated.length) return;
259
+
260
+ Modal.confirm({
261
+ title: '确认批量提交',
262
+ content: '将提交 ' + validated.length + ' 行数据,首行事项为“' + validated[0].name + '”。提交后成功行不会重复写入。',
263
+ okText: '确认提交',
264
+ cancelText: '返回检查',
265
+ onOk: () => performSubmit(validated),
266
+ });
267
+ }
268
+
269
+ const statusTag = {
270
+ draft: ['default', '草稿'],
271
+ invalid: ['error', '待修正'],
272
+ submitting: ['processing', '提交中'],
273
+ submitted: ['success', '已提交'],
274
+ failed: ['error', '提交失败'],
275
+ };
276
+
277
+ const columns = [
278
+ {
279
+ title: '事项名称',
280
+ dataIndex: 'name',
281
+ width: 250,
282
+ render: (_, row) => (
283
+ <div>
284
+ <Input
285
+ value={row.name}
286
+ status={row._errors.name ? 'error' : ''}
287
+ disabled={row._status === 'submitted' || row._status === 'submitting'}
288
+ placeholder="请输入事项名称"
289
+ onChange={(event) => updateCell(row._rowId, 'name', event.target.value)}
290
+ />
291
+ {row._errors.name ? <Text type="danger">{row._errors.name}</Text> : null}
292
+ </div>
293
+ ),
294
+ },
295
+ {
296
+ title: '状态',
297
+ dataIndex: 'status',
298
+ width: 160,
299
+ render: (_, row) => (
300
+ <div>
301
+ <Select
302
+ value={row.status}
303
+ status={row._errors.status ? 'error' : ''}
304
+ disabled={row._status === 'submitted' || row._status === 'submitting'}
305
+ options={STATUS_OPTIONS}
306
+ style={{ width: '100%' }}
307
+ onChange={(value) => updateCell(row._rowId, 'status', value)}
308
+ />
309
+ {row._errors.status ? <Text type="danger">{row._errors.status}</Text> : null}
310
+ </div>
311
+ ),
312
+ },
313
+ {
314
+ title: '计划日期',
315
+ dataIndex: 'dueDate',
316
+ width: 180,
317
+ render: (_, row) => (
318
+ <div>
319
+ <DatePicker
320
+ value={row.dueDate ? dayjs(row.dueDate) : null}
321
+ status={row._errors.dueDate ? 'error' : ''}
322
+ disabled={row._status === 'submitted' || row._status === 'submitting'}
323
+ style={{ width: '100%' }}
324
+ onChange={(_, dateString) => updateCell(row._rowId, 'dueDate', dateString)}
325
+ />
326
+ {row._errors.dueDate ? <Text type="danger">{row._errors.dueDate}</Text> : null}
327
+ </div>
328
+ ),
329
+ },
330
+ {
331
+ title: '数量',
332
+ dataIndex: 'quantity',
333
+ width: 130,
334
+ render: (_, row) => (
335
+ <div>
336
+ <InputNumber
337
+ min={1}
338
+ value={row.quantity}
339
+ status={row._errors.quantity ? 'error' : ''}
340
+ disabled={row._status === 'submitted' || row._status === 'submitting'}
341
+ style={{ width: '100%' }}
342
+ onChange={(value) => updateCell(row._rowId, 'quantity', value)}
343
+ />
344
+ {row._errors.quantity ? <Text type="danger">{row._errors.quantity}</Text> : null}
345
+ </div>
346
+ ),
347
+ },
348
+ {
349
+ title: '行状态',
350
+ dataIndex: '_status',
351
+ width: 150,
352
+ render: (_, row) => {
353
+ const tag = statusTag[row._status] || statusTag.draft;
354
+ return (
355
+ <div>
356
+ <Tag color={tag[0]}>{tag[1]}</Tag>
357
+ {row._submitError ? <div className="canvas-table-error">{row._submitError}</div> : null}
358
+ </div>
359
+ );
360
+ },
361
+ },
362
+ {
363
+ title: '操作',
364
+ key: 'action',
365
+ width: 100,
366
+ fixed: 'right',
367
+ render: (_, row) => (
368
+ <Button
369
+ type="link"
370
+ danger
371
+ disabled={row._status === 'submitted' || row._status === 'submitting'}
372
+ onClick={() => removeRow(row._rowId)}
373
+ >
374
+ 删除
375
+ </Button>
376
+ ),
377
+ },
378
+ ];
379
+
380
+ const pendingCount = rows.filter((row) => row._status !== 'submitted').length;
381
+
382
+ return (
383
+ <ConfigProvider
384
+ theme={{
385
+ token: {
386
+ colorPrimary: '#0F766E',
387
+ colorInfo: '#0F766E',
388
+ borderRadius: 9,
389
+ colorText: '#23312F',
390
+ colorBgLayout: '#F3F8F7',
391
+ },
392
+ }}
393
+ >
394
+ <style>{`
395
+ .canvas-table-page {
396
+ min-height: 100%;
397
+ box-sizing: border-box;
398
+ padding: 28px;
399
+ background: linear-gradient(145deg, #F0F9F7 0%, #F7FAFC 56%, #EEF5FF 100%);
400
+ }
401
+ .canvas-table-shell { max-width: 1320px; margin: 0 auto; }
402
+ .canvas-table-header {
403
+ display: flex;
404
+ align-items: flex-start;
405
+ justify-content: space-between;
406
+ gap: 20px;
407
+ margin-bottom: 18px;
408
+ }
409
+ .canvas-table-title.ant-typography { margin: 0 0 6px; }
410
+ .canvas-table-panel {
411
+ overflow: hidden;
412
+ border: 1px solid #DDE9E6;
413
+ border-radius: 16px;
414
+ background: #FFFFFF;
415
+ box-shadow: 0 14px 38px rgba(46, 84, 77, .08);
416
+ }
417
+ .canvas-table-toolbar {
418
+ display: flex;
419
+ align-items: center;
420
+ justify-content: space-between;
421
+ gap: 16px;
422
+ padding: 16px 18px;
423
+ border-bottom: 1px solid #E5EFED;
424
+ }
425
+ .canvas-table-body { padding: 0 18px 18px; }
426
+ .canvas-table-error { margin-top: 4px; color: #C2413B; font-size: 12px; line-height: 1.35; }
427
+ .canvas-table-summary { margin-top: 14px; }
428
+ .canvas-table-page .ant-table-cell { vertical-align: top; }
429
+ @media (max-width: 760px) {
430
+ .canvas-table-page { padding: 16px 10px; }
431
+ .canvas-table-header, .canvas-table-toolbar { align-items: stretch; flex-direction: column; }
432
+ }
433
+ `}</style>
434
+
435
+ <div className="canvas-table-page" data-theme-scope="page">
436
+ <div className="canvas-table-shell">
437
+ <div className="canvas-table-header">
438
+ <div>
439
+ <Title level={2} className="canvas-table-title">批量事项录入</Title>
440
+ <Text type="secondary">草稿保存在当前浏览器;每批最多并发提交 {BATCH_SIZE} 行。</Text>
441
+ </div>
442
+ <Space wrap>
443
+ <Tag color={bridgeState.ready ? 'success' : 'warning'}>
444
+ {bridgeState.ready ? '写入桥已验证' : '写入桥未闭环'}
445
+ </Tag>
446
+ <Tag>{pendingCount} 行待处理</Tag>
447
+ </Space>
448
+ </div>
449
+
450
+ {!bridgeState.ready ? (
451
+ <Alert
452
+ type="warning"
453
+ showIcon
454
+ message="当前仅支持本地草稿与校验"
455
+ description={bridgeState.reason + ' 未验证前不会模拟提交成功。'}
456
+ style={{ marginBottom: 16 }}
457
+ />
458
+ ) : null}
459
+
460
+ <div className="canvas-table-panel">
461
+ <div className="canvas-table-toolbar">
462
+ <Space wrap>
463
+ <Button onClick={addRow}>添加一行</Button>
464
+ <Button onClick={addDemoRows}>载入示例行</Button>
465
+ </Space>
466
+ <Button
467
+ type="primary"
468
+ loading={submitting}
469
+ disabled={!bridgeState.ready || submitting || pendingCount === 0}
470
+ onClick={confirmSubmit}
471
+ >
472
+ 校验并批量提交
473
+ </Button>
474
+ </div>
475
+
476
+ <div className="canvas-table-body">
477
+ <Table
478
+ rowKey="_rowId"
479
+ columns={columns}
480
+ dataSource={rows}
481
+ pagination={false}
482
+ scroll={{ x: 1080 }}
483
+ rowClassName={(row) => 'canvas-table-row-' + row._status}
484
+ />
485
+
486
+ {summary ? (
487
+ <Alert
488
+ className="canvas-table-summary"
489
+ type={summary.failureCount ? 'warning' : 'success'}
490
+ showIcon
491
+ message={
492
+ summary.validationOnly
493
+ ? '有 ' + summary.failureCount + ' 行未通过校验,请逐行修正。'
494
+ : '提交完成:成功 ' + summary.successCount + ' 行,失败 ' + summary.failureCount + ' 行。'
495
+ }
496
+ description={
497
+ summary.failureCount
498
+ ? '失败行已保留,可修正后仅重试未成功的行。'
499
+ : '全部成功后本地草稿已清除。'
500
+ }
501
+ />
502
+ ) : null}
503
+ </div>
504
+ </div>
505
+ </div>
506
+ </div>
507
+ </ConfigProvider>
508
+ );
509
+ }
510
+
511
+ export default YidaComp;