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.
- package/lib/app/canvas-compile.js +33 -2
- package/lib/app/publish.js +47 -3
- package/lib/core/sample.js +6 -0
- package/lib/samples/yida-canvas-table-form/table-form-batch-submit.canvas.jsx +511 -0
- package/lib/samples/yida-rechart/trend-combo.canvas.jsx +253 -0
- package/package.json +1 -1
- package/scripts/build-skills-package.js +12 -8
- package/scripts/postinstall.js +4 -4
- package/scripts/validate-skills.js +91 -30
- package/yida-skills/SKILL.md +14 -5
- package/yida-skills/skills/yida-canvas-table-form/SKILL.md +151 -0
- package/yida-skills/skills/yida-dashboard/SKILL.md +143 -147
- package/yida-skills/skills/yida-dashboard/references/interaction-patterns.md +4 -2
- package/yida-skills/skills/yida-dashboard/references/pitfalls.md +2 -0
- package/yida-skills/skills/yida-dashboard/references/structure-and-layout.md +97 -320
- package/yida-skills/skills/yida-dashboard/references/theme-presets.md +1 -1
- package/yida-skills/skills/yida-data-management/SKILL.md +32 -2
- package/yida-skills/skills/yida-data-source-connectors/SKILL.md +106 -64
- package/yida-skills/skills/yida-density/SKILL.md +118 -110
- package/yida-skills/skills/yida-flash-note-to-prd/SKILL.md +3 -1
- package/yida-skills/skills/yida-nav-shell/SKILL.md +125 -74
- package/yida-skills/skills/yida-nav-shell/references/nav-shell-patterns.md +43 -32
- package/yida-skills/skills/yida-page-uiux/SKILL.md +11 -2
- package/yida-skills/skills/yida-page-uiux/references/dashboard/chart-contracts.md +1 -0
- package/yida-skills/skills/yida-page-uiux/references/scenes/dashboard.md +2 -2
- package/yida-skills/skills/yida-page-uiux/references/visual-decision-engine.md +1 -1
- package/yida-skills/skills/yida-page-uiux/workflow/output-decision-block.md +3 -3
- package/yida-skills/skills/yida-page-uiux/workflow/step-0-nav-shape.md +4 -4
- package/yida-skills/skills/yida-page-uiux/workflow/step-5-icon-and-assets.md +2 -2
- package/yida-skills/skills/yida-page-uiux/workflow/step-6-deai-selfcheck.md +1 -1
- package/yida-skills/skills/yida-ppt-slider/SKILL.md +185 -113
- package/yida-skills/skills/yida-ppt-slider/references/dark-tech-theme.md +7 -4
- package/yida-skills/skills/yida-ppt-slider/references/echarts-race-example.md +2 -0
- package/yida-skills/skills/yida-ppt-slider/references/examples.md +29 -7
- package/yida-skills/skills/yida-rechart/SKILL.md +134 -0
- package/yida-skills/skills/yida-tingji/SKILL.md +4 -3
- package/yida-skills/skills-index.json +124 -35
- 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
|
|
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
|
-
|
|
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
|
}
|
package/lib/app/publish.js
CHANGED
|
@@ -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
|
-
|
|
726
|
-
process.exit(1);
|
|
770
|
+
throw buildCanvasCompileCliError(canvasCompileError, sourcePath);
|
|
727
771
|
}
|
|
728
772
|
success(t('publish.canvas_compile_done'));
|
|
729
773
|
} else {
|
package/lib/core/sample.js
CHANGED
|
@@ -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;
|