@bit-sun/business-component 4.2.0-alpha.3 → 4.2.0-alpha.31

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 (138) hide show
  1. package/.fatherrc.ts +1 -1
  2. package/.umirc.ts +13 -9
  3. package/dist/components/Business/AddSelectBusiness/index.d.ts +3 -4
  4. package/dist/components/Business/BsLayouts/Components/AllFunc/drawContent.d.ts +1 -2
  5. package/dist/components/Business/BsLayouts/Components/ChooseStore/index.d.ts +1 -2
  6. package/dist/components/Business/BsLayouts/Components/CustomerMenu/MenuSetting/index.d.ts +1 -1
  7. package/dist/components/Business/BsLayouts/Components/CustomerMenu/MenuSetting/leftTree.d.ts +1 -1
  8. package/dist/components/Business/BsLayouts/Components/CustomerMenu/MenuSetting/rightTree.d.ts +2 -2
  9. package/dist/components/Business/BsLayouts/Components/CustomerMenu/globalMenu/DrawContent.d.ts +1 -2
  10. package/dist/components/Business/BsLayouts/Components/CustomerMenu/globalMenu/customMenuHeader.d.ts +1 -2
  11. package/dist/components/Business/BsLayouts/Components/CustomerMenu/index.d.ts +1 -1
  12. package/dist/components/Business/BsLayouts/Components/GlobalHeader/index.d.ts +1 -2
  13. package/dist/components/Business/BsLayouts/Components/RightContent/LoginModal.d.ts +1 -2
  14. package/dist/components/Business/BsLayouts/index.d.ts +1 -1
  15. package/dist/components/Business/BsSulaQueryTable/SearchItemSetting.d.ts +5 -5
  16. package/dist/components/Business/BsSulaQueryTable/index.d.ts +1 -2
  17. package/dist/components/Business/BsSulaQueryTable/setting.d.ts +6 -6
  18. package/dist/components/Business/BsSulaQueryTable/utils.d.ts +14 -15
  19. package/dist/components/Business/CommodityEntry/index.d.ts +1 -2
  20. package/dist/components/Business/CommonAlert/index.d.ts +1 -2
  21. package/dist/components/Business/CommonGuideWrapper/index.d.ts +3 -3
  22. package/dist/components/Business/DetailPageWrapper/index.d.ts +12 -12
  23. package/dist/components/Business/HomePageWrapper/index.d.ts +1 -2
  24. package/dist/components/Business/ImportExport/ExportButton/ModalTitle.d.ts +11 -0
  25. package/dist/components/Business/ImportExport/ExportButton/TemplatePicker.d.ts +13 -0
  26. package/dist/components/Business/ImportExport/ExportButton/index.d.ts +15 -0
  27. package/dist/components/Business/ImportExport/ImportWizard/Preview.d.ts +11 -0
  28. package/dist/components/Business/ImportExport/ImportWizard/index.d.ts +15 -0
  29. package/dist/components/Business/ImportExport/TaskDrawer/index.d.ts +15 -0
  30. package/dist/components/Business/ImportExport/TaskProgress/index.d.ts +15 -0
  31. package/dist/components/Business/ImportExport/businessFunction.d.ts +15 -0
  32. package/dist/components/Business/ImportExport/index.d.ts +14 -0
  33. package/dist/components/Business/ImportExport/sceneCapability.d.ts +20 -0
  34. package/dist/components/Business/ImportExport/services/importExport.d.ts +22 -0
  35. package/dist/components/Business/ImportExport/services/routing.d.ts +8 -0
  36. package/dist/components/Business/ImportExport/types.d.ts +134 -0
  37. package/dist/components/Business/ItemPropertySelector/index.d.ts +1 -2
  38. package/dist/components/Business/JsonQueryTable/components/FieldsModifyModal.d.ts +1 -2
  39. package/dist/components/Business/JsonQueryTable/components/FieldsSettingsTable.d.ts +1 -2
  40. package/dist/components/Business/JsonQueryTable/components/Formula.d.ts +1 -2
  41. package/dist/components/Business/JsonQueryTable/components/MaintainOptions.d.ts +1 -2
  42. package/dist/components/Business/JsonQueryTable/drawer/index.d.ts +1 -2
  43. package/dist/components/Business/PropertyModal/index.d.ts +1 -2
  44. package/dist/components/Business/PropertyModal/propertyGroup.d.ts +1 -1
  45. package/dist/components/Business/SearchSelect/index.d.ts +1 -1
  46. package/dist/components/Business/StateFlow/index.d.ts +1 -2
  47. package/dist/components/Business/SystemLog/index.d.ts +78 -0
  48. package/dist/components/Business/TreeSearchSelect/index.d.ts +1 -1
  49. package/dist/components/Business/TrialCalculation/index.d.ts +15 -0
  50. package/dist/components/Business/columnSettingTable/columnSetting.d.ts +6 -6
  51. package/dist/components/Business/columnSettingTable/components/TableSumComponent.d.ts +1 -2
  52. package/dist/components/Business/columnSettingTable/index.d.ts +3 -3
  53. package/dist/components/Business/columnSettingTable/sulaSettingTable.d.ts +3 -3
  54. package/dist/components/Business/columnSettingTable/utils.d.ts +1 -2
  55. package/dist/components/Business/moreTreeTable/FixedScrollBar.d.ts +1 -1
  56. package/dist/components/Common/ParagraphCopier/index.d.ts +1 -1
  57. package/dist/components/Common/Section/index.d.ts +1 -1
  58. package/dist/components/Functional/AddSelect/index.d.ts +1 -2
  59. package/dist/components/Functional/AuthButton/index.d.ts +1 -2
  60. package/dist/components/Functional/DataImport/index.d.ts +3 -3
  61. package/dist/components/Functional/DataValidation/index.d.ts +5 -5
  62. package/dist/components/Functional/ExportFunctions/ExportIcon/index.d.ts +1 -2
  63. package/dist/components/Functional/QueryMutipleInput/index.d.ts +1 -2
  64. package/dist/components/Functional/QueryMutipleSelect/index.d.ts +1 -2
  65. package/dist/components/Functional/SearchSelect/index.d.ts +1 -1
  66. package/dist/components/Functional/SearchSelect/pagination.d.ts +22 -0
  67. package/dist/components/Functional/SearchSelect/utils.d.ts +2 -3
  68. package/dist/components/Functional/TreeSearchSelect/index.d.ts +1 -2
  69. package/dist/components/Solution/RuleComponent/CustomPlugin/CustomSelector/CustomSelectorModal.d.ts +1 -1
  70. package/dist/components/Solution/RuleComponent/CustomPlugin/CustomSelector/index.d.ts +1 -2
  71. package/dist/components/Solution/RuleComponent/Formula.d.ts +1 -2
  72. package/dist/components/Solution/RuleComponent/InnerSelect.d.ts +1 -2
  73. package/dist/components/Solution/RuleComponent/RenderCompItem.d.ts +1 -2
  74. package/dist/components/Solution/RuleSetter/RuleInstance.d.ts +1 -2
  75. package/dist/components/Solution/RuleSetter/baseRule.d.ts +1 -1
  76. package/dist/components/Solution/RuleSetter/index.d.ts +1 -1
  77. package/dist/index.d.ts +5 -0
  78. package/dist/index.esm.js +5397 -1220
  79. package/dist/index.js +5417 -1213
  80. package/dist/plugin/TableColumnSetting/index.d.ts +5 -5
  81. package/dist/utils/TableUtils.d.ts +18 -19
  82. package/dist/utils/auth.d.ts +4 -0
  83. package/dist/utils/utils.d.ts +41 -0
  84. package/docs/error-record.md +52 -0
  85. package/docs/flow-control-readme.md +283 -0
  86. package/docs/task-records/2026-07-31-import-export-extraction.md +81 -0
  87. package/package.json +1 -1
  88. package/src/components/Business/AddSelectBusiness/index.tsx +3 -2
  89. package/src/components/Business/BsSulaQueryTable/index.md +57 -4
  90. package/src/components/Business/BsSulaQueryTable/index.tsx +174 -10
  91. package/src/components/Business/BsSulaQueryTable/setting.tsx +6 -0
  92. package/src/components/Business/DetailPageWrapper/index.tsx +81 -1
  93. package/src/components/Business/DetailPageWrapper/readme.md +89 -0
  94. package/src/components/Business/ImportExport/ExportButton/ModalTitle.tsx +27 -0
  95. package/src/components/Business/ImportExport/ExportButton/TemplatePicker.tsx +424 -0
  96. package/src/components/Business/ImportExport/ExportButton/index.module.less +382 -0
  97. package/src/components/Business/ImportExport/ExportButton/index.tsx +273 -0
  98. package/src/components/Business/ImportExport/ImportWizard/Preview.tsx +129 -0
  99. package/src/components/Business/ImportExport/ImportWizard/index.module.less +360 -0
  100. package/src/components/Business/ImportExport/ImportWizard/index.tsx +679 -0
  101. package/src/components/Business/ImportExport/TaskDrawer/index.tsx +285 -0
  102. package/src/components/Business/ImportExport/TaskProgress/index.tsx +267 -0
  103. package/src/components/Business/ImportExport/businessFunction.ts +29 -0
  104. package/src/components/Business/ImportExport/index.md +104 -0
  105. package/src/components/Business/ImportExport/index.tsx +208 -0
  106. package/src/components/Business/ImportExport/sceneCapability.ts +28 -0
  107. package/src/components/Business/ImportExport/services/importExport.ts +208 -0
  108. package/src/components/Business/ImportExport/services/routing.ts +21 -0
  109. package/src/components/Business/ImportExport/types.ts +180 -0
  110. package/src/components/Business/SearchSelect/BusinessUtils.tsx +704 -48
  111. package/src/components/Business/SearchSelect/index.md +2 -2
  112. package/src/components/Business/SystemLog/index.md +37 -0
  113. package/src/components/Business/SystemLog/index.tsx +87 -0
  114. package/src/components/Business/TrialCalculation/PathDisplay.less +93 -0
  115. package/src/components/Business/TrialCalculation/PathDisplay_README.md +77 -0
  116. package/src/components/Business/TrialCalculation/index.tsx +322 -0
  117. package/src/components/Business/columnSettingTable/index.tsx +7 -6
  118. package/src/components/Business/columnSettingTable/sulaSettingTable.tsx +23 -22
  119. package/src/components/Functional/AddSelect/index.tsx +95 -1
  120. package/src/components/Functional/DataValidation/index.tsx +31 -9
  121. package/src/components/Functional/QueryMutipleSelect/index.tsx +2 -2
  122. package/src/components/Functional/SearchSelect/index.md +109 -0
  123. package/src/components/Functional/SearchSelect/index.tsx +99 -17
  124. package/src/components/Functional/SearchSelect/pagination.ts +57 -0
  125. package/src/components/Functional/SearchSelect/utils.tsx +28 -18
  126. package/src/components/Functional/TreeSearchSelect/index.tsx +2 -1
  127. package/src/components/Solution/RuleComponent/README.md +187 -0
  128. package/src/components/Solution/RuleComponent/RenderCompItem.tsx +9 -1
  129. package/src/components/Solution/RuleComponent/RenderCompItem_README.md +189 -0
  130. package/src/components/Solution/RuleComponent/RuleField.md +63 -0
  131. package/src/components/Solution/RuleComponent/index.js +237 -8
  132. package/src/components/Solution/RuleComponent/ruleFiled.js +46 -6
  133. package/src/components/Solution/RuleSetter/README.md +97 -0
  134. package/src/index.ts +9 -0
  135. package/src/typings/umi.d.ts +9 -0
  136. package/src/utils/LocalstorageUtils.ts +1 -1
  137. package/src/utils/auth.ts +39 -2
  138. package/src/utils/utils.ts +41 -1
@@ -0,0 +1,679 @@
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import {
3
+ Alert,
4
+ Button,
5
+ Descriptions,
6
+ Form,
7
+ Modal,
8
+ Progress,
9
+ Radio,
10
+ Select,
11
+ Space,
12
+ Steps,
13
+ Tooltip,
14
+ Upload,
15
+ message,
16
+ } from 'antd';
17
+ import {
18
+ DownloadOutlined,
19
+ FileExcelOutlined,
20
+ InboxOutlined,
21
+ QuestionCircleOutlined,
22
+ UploadOutlined,
23
+ } from '@ant-design/icons';
24
+ import {
25
+ getItems,
26
+ isConcurrentLimit,
27
+ normalizeTask,
28
+ unwrapResponse,
29
+ } from '../services/importExport';
30
+ import type {
31
+ FileTask,
32
+ ImportExportRequestContext,
33
+ ImportExportServices,
34
+ ImportExportTemplate,
35
+ } from '../types';
36
+ import Preview from './Preview';
37
+ import styles from './index.module.less';
38
+
39
+ const POLL_INTERVAL_MS = 1000;
40
+ const MAX_FILE_SIZE = 10 * 1024 * 1024;
41
+ const PREFERENCE_KEY = 'import-export:last-import-preference';
42
+ const SUPPORTED_ERROR_HANDLING_MODE = 'PARTIAL_SUCCESS';
43
+ const PREVIEW_EXPIRED_MESSAGE = '校验结果已过期,请重新上传并校验';
44
+ const PREVIEW_CONSUMED_MESSAGE = '校验结果已执行或正在执行,请勿重复提交';
45
+ const wait = () =>
46
+ new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
47
+
48
+ const operationErrorMessages = (error: any) =>
49
+ [
50
+ error?.message,
51
+ error?.msg,
52
+ error?.data?.message,
53
+ error?.data?.msg,
54
+ error?.response?.data?.message,
55
+ error?.response?.data?.msg,
56
+ ]
57
+ .filter(Boolean)
58
+ .map(String);
59
+
60
+ const isPreviewExpiredError = (error: any) =>
61
+ operationErrorMessages(error).some((text) =>
62
+ text.includes(PREVIEW_EXPIRED_MESSAGE),
63
+ );
64
+
65
+ const isPreviewConsumedError = (error: any) =>
66
+ operationErrorMessages(error).some(
67
+ (text) =>
68
+ text.includes('已执行或正在执行') || text.includes('请勿重复提交'),
69
+ );
70
+
71
+ export interface ImportWizardProps {
72
+ visible: boolean;
73
+ context: ImportExportRequestContext;
74
+ documentName?: string;
75
+ services: ImportExportServices;
76
+ onClose: () => void;
77
+ onTaskCreated?: (task: FileTask) => void;
78
+ }
79
+
80
+ /**
81
+ * 完成模板选择、文件上传、数据校验和正式导入的两步向导。
82
+ */
83
+ const ImportWizard: React.FC<ImportWizardProps> = ({
84
+ visible,
85
+ context,
86
+ documentName,
87
+ services,
88
+ onClose,
89
+ onTaskCreated,
90
+ }) => {
91
+ const [form] = Form.useForm();
92
+ const [step, setStep] = useState(0);
93
+ const [parsing, setParsing] = useState(false);
94
+ const [preview, setPreview] = useState<any>();
95
+ const [executing, setExecuting] = useState(false);
96
+ const [templates, setTemplates] = useState<ImportExportTemplate[]>([]);
97
+ const [downloading, setDownloading] = useState(false);
98
+ const [downloadingErrorFile, setDownloadingErrorFile] = useState(false);
99
+ const [selectedTemplateCode, setSelectedTemplateCode] = useState<string>();
100
+ const [selectedFile, setSelectedFile] = useState<any>();
101
+ const [fileName, setFileName] = useState('');
102
+
103
+ useEffect(() => {
104
+ if (!visible) return;
105
+ setStep(0);
106
+ setPreview(undefined);
107
+ setSelectedTemplateCode(undefined);
108
+ setSelectedFile(undefined);
109
+ setFileName('');
110
+ setExecuting(false);
111
+ setDownloadingErrorFile(false);
112
+ let preference: Record<string, string> = {};
113
+ try {
114
+ preference = JSON.parse(localStorage.getItem(PREFERENCE_KEY) || '{}');
115
+ } catch (error) {
116
+ preference = {};
117
+ }
118
+ form.setFieldsValue({
119
+ strategy: preference.strategy || 'UPSERT',
120
+ errorHandlingMode: SUPPORTED_ERROR_HANDLING_MODE,
121
+ });
122
+ services
123
+ .queryTemplates({
124
+ operationType: 'IMPORT',
125
+ businessFunctionCode: context.businessFunctionCode,
126
+ enabled: true,
127
+ currentPage: 1,
128
+ pageSize: 100,
129
+ })
130
+ .then((response) => {
131
+ const loaded = getItems<ImportExportTemplate>(response);
132
+ setTemplates(loaded);
133
+ const defaultTemplate = loaded.find(
134
+ (template) => template.defaultFlag && template.enabled !== false,
135
+ );
136
+ if (!defaultTemplate) {
137
+ message.error('当前业务功能未配置启用的默认导入模板');
138
+ return;
139
+ }
140
+ setSelectedTemplateCode(defaultTemplate.templateCode);
141
+ form.setFieldsValue({ templateCode: defaultTemplate.templateCode });
142
+ })
143
+ .catch(() => message.error('导入模板加载失败,请稍后重试'));
144
+ }, [visible, context.businessFunctionCode, form, services]);
145
+
146
+ const expirePreview = useCallback(() => {
147
+ setPreview(undefined);
148
+ setStep(0);
149
+ setSelectedFile(undefined);
150
+ setFileName('');
151
+ setExecuting(false);
152
+ setDownloadingErrorFile(false);
153
+ message.warning(PREVIEW_EXPIRED_MESSAGE);
154
+ }, []);
155
+
156
+ useEffect(() => {
157
+ if (!visible || step !== 1) return undefined;
158
+ const expiresAt = Number(preview?.expiresAt);
159
+ if (!Number.isFinite(expiresAt) || expiresAt <= 0) return undefined;
160
+ const remaining = expiresAt - Date.now();
161
+ if (remaining <= 0) {
162
+ expirePreview();
163
+ return undefined;
164
+ }
165
+ const timer = window.setTimeout(expirePreview, remaining);
166
+ return () => window.clearTimeout(timer);
167
+ }, [visible, step, preview?.expiresAt, expirePreview]);
168
+
169
+ const waitForPreviewTask = async (taskId: string, previewResult: any) => {
170
+ while (true) {
171
+ const task = normalizeTask(
172
+ unwrapResponse<any>(
173
+ await services.queryTask(context.serviceContext, taskId),
174
+ ),
175
+ );
176
+ if (!task.taskStatus) {
177
+ throw new Error(task?.msg || 'preview task status is missing');
178
+ }
179
+ const merged = {
180
+ ...previewResult,
181
+ ...(task.result || {}),
182
+ ...task,
183
+ taskId,
184
+ serviceContext: context.serviceContext,
185
+ };
186
+ setPreview(merged);
187
+ onTaskCreated?.(task);
188
+ if (task.stage === 'PREVIEW_COMPLETED') {
189
+ if (
190
+ [
191
+ 'PASSED',
192
+ 'COMPLETED_WITH_ERRORS',
193
+ 'SUCCESS',
194
+ 'PARTIAL_SUCCESS',
195
+ ].includes(task.taskStatus)
196
+ ) {
197
+ return merged;
198
+ }
199
+ }
200
+ if (task.taskStatus === 'FAILED' || task.taskStatus === 'CANCELLED') {
201
+ throw new Error(task.errorMessage || 'preview task failed');
202
+ }
203
+ await wait();
204
+ }
205
+ };
206
+
207
+ const selectFile = (file: any) => {
208
+ const extension = String(file?.name || '')
209
+ .split('.')
210
+ .pop()
211
+ ?.toLowerCase();
212
+ if (!['xlsx', 'xls', 'csv'].includes(extension || '')) {
213
+ message.warning('仅支持 .xlsx、.xls、.csv 文件');
214
+ setSelectedFile(undefined);
215
+ return false;
216
+ }
217
+ if (Number(file?.size || 0) > MAX_FILE_SIZE) {
218
+ message.warning('文件不能超过 10MB');
219
+ setSelectedFile(undefined);
220
+ return false;
221
+ }
222
+ setSelectedFile(file);
223
+ setFileName(file?.name || '');
224
+ setPreview(undefined);
225
+ return false;
226
+ };
227
+
228
+ const parse = async () => {
229
+ if (!selectedFile) {
230
+ message.warning('请先选择导入文件');
231
+ return;
232
+ }
233
+ setParsing(true);
234
+ try {
235
+ const values = await form.validateFields([
236
+ 'templateCode',
237
+ 'strategy',
238
+ 'errorHandlingMode',
239
+ ]);
240
+ localStorage.setItem(
241
+ PREFERENCE_KEY,
242
+ JSON.stringify({
243
+ strategy: values.strategy,
244
+ errorHandlingMode: SUPPORTED_ERROR_HANDLING_MODE,
245
+ }),
246
+ );
247
+ const uploaded = unwrapResponse<any>(
248
+ await services.uploadFile(selectedFile),
249
+ );
250
+ const objectKey = uploaded?.objectKey || uploaded?.name || uploaded?.url;
251
+ if (!objectKey) {
252
+ throw new Error('uploaded objectKey is missing');
253
+ }
254
+ const response = await services.previewImport(context.serviceContext, {
255
+ businessFunctionCode: context.businessFunctionCode,
256
+ templateCode: values.templateCode,
257
+ importStrategy: values.strategy,
258
+ errorHandlingMode: SUPPORTED_ERROR_HANDLING_MODE,
259
+ objectKey,
260
+ sourceFileName: selectedFile.name,
261
+ });
262
+ if (isConcurrentLimit(response)) {
263
+ message.warning('当前导入导出任务较多,请稍后重试');
264
+ return false;
265
+ }
266
+ const created = unwrapResponse<any>(response);
267
+ if (!created?.taskId) {
268
+ throw new Error(created?.msg || 'preview taskId is missing');
269
+ }
270
+ setPreview(created);
271
+ onTaskCreated?.(
272
+ normalizeTask({
273
+ taskType: 'IMPORT_VALIDATION',
274
+ taskStatus: 'PENDING',
275
+ ...created,
276
+ serviceContext: context.serviceContext,
277
+ }),
278
+ );
279
+ const completed = await waitForPreviewTask(created.taskId, created);
280
+ setPreview(completed);
281
+ setStep(1);
282
+ } catch (error) {
283
+ if (isConcurrentLimit(error)) {
284
+ message.warning('当前导入导出任务较多,请稍后重试');
285
+ return false;
286
+ }
287
+ message.error('文件解析失败,请稍后重试');
288
+ } finally {
289
+ setParsing(false);
290
+ }
291
+ return false;
292
+ };
293
+
294
+ const selectedTemplate = templates.find(
295
+ (item) => item.templateCode === selectedTemplateCode,
296
+ );
297
+ const personalTemplates = templates.filter(
298
+ (item) => item.visibility === 'PERSONAL',
299
+ );
300
+ const publicTemplates = templates.filter(
301
+ (item) => item.visibility !== 'PERSONAL',
302
+ );
303
+ const selectedTemplateFieldCount =
304
+ selectedTemplate?.fieldCount ?? selectedTemplate?.fieldNames?.length;
305
+
306
+ const execute = async () => {
307
+ const expiresAt = Number(preview?.expiresAt);
308
+ if (Number.isFinite(expiresAt) && expiresAt <= Date.now()) {
309
+ expirePreview();
310
+ return;
311
+ }
312
+ if (preview?.canExecute !== true) {
313
+ message.warning('当前校验结果没有可执行的合格业务单据');
314
+ return;
315
+ }
316
+ let submitted = false;
317
+ setExecuting(true);
318
+ try {
319
+ const previewId = preview?.previewId || preview?.result?.previewId;
320
+ if (!previewId) {
321
+ throw new Error('previewId is missing');
322
+ }
323
+ submitted = true;
324
+ const res: any = await services.executeImport(context.serviceContext, {
325
+ previewId,
326
+ });
327
+ if (isConcurrentLimit(res)) {
328
+ message.warning('当前导入导出任务较多,请稍后重试');
329
+ return;
330
+ }
331
+ onTaskCreated?.(
332
+ normalizeTask({
333
+ taskType: 'IMPORT_EXECUTION',
334
+ taskStatus: 'PENDING',
335
+ ...unwrapResponse<any>(res),
336
+ serviceContext: context.serviceContext,
337
+ }),
338
+ );
339
+ message.success('已创建导入任务');
340
+ onClose();
341
+ } catch (error) {
342
+ if (isConcurrentLimit(error)) {
343
+ message.warning('当前导入导出任务较多,请稍后重试');
344
+ return;
345
+ }
346
+ if (isPreviewExpiredError(error)) {
347
+ expirePreview();
348
+ return;
349
+ }
350
+ if (isPreviewConsumedError(error)) {
351
+ setPreview((current: any) =>
352
+ current ? { ...current, canExecute: false } : current,
353
+ );
354
+ message.error(PREVIEW_CONSUMED_MESSAGE);
355
+ return;
356
+ }
357
+ if (submitted) {
358
+ setPreview((current: any) =>
359
+ current ? { ...current, canExecute: false } : current,
360
+ );
361
+ message.error('执行结果待核对,请勿重复提交,请在任务中心核对业务数据');
362
+ return;
363
+ }
364
+ message.error('执行导入失败,请重新上传并校验');
365
+ } finally {
366
+ setExecuting(false);
367
+ }
368
+ };
369
+
370
+ const downloadValidationErrorFile = async () => {
371
+ const expiresAt = Number(preview?.expiresAt);
372
+ if (Number.isFinite(expiresAt) && expiresAt <= Date.now()) {
373
+ expirePreview();
374
+ return;
375
+ }
376
+ if (preview?.errorFileAvailable !== true) {
377
+ message.warning('当前校验结果没有可下载的错误文件');
378
+ return;
379
+ }
380
+ setDownloadingErrorFile(true);
381
+ try {
382
+ const previewId = preview?.previewId || preview?.result?.previewId;
383
+ if (!previewId) {
384
+ throw new Error('previewId is missing');
385
+ }
386
+ const response = await services.downloadValidationErrorFile(
387
+ context.serviceContext,
388
+ { previewId },
389
+ );
390
+ const result = unwrapResponse<any>(response);
391
+ if (!result?.fileUrl) {
392
+ throw new Error('validation error fileUrl is missing');
393
+ }
394
+ const link = document.createElement('a');
395
+ link.href = result.fileUrl;
396
+ link.download = `${fileName || '导入文件'}-错误数据.xlsx`;
397
+ document.body.appendChild(link);
398
+ link.click();
399
+ document.body.removeChild(link);
400
+ message.success('错误文件下载已开始');
401
+ } catch (error) {
402
+ if (isPreviewExpiredError(error)) {
403
+ expirePreview();
404
+ return;
405
+ }
406
+ message.error('错误文件下载失败,请稍后重试');
407
+ } finally {
408
+ setDownloadingErrorFile(false);
409
+ }
410
+ };
411
+
412
+ const downloadSelectedTemplate = async () => {
413
+ const templateCode = form.getFieldValue('templateCode');
414
+ if (!templateCode) {
415
+ message.warning('请先选择模板');
416
+ return;
417
+ }
418
+ setDownloading(true);
419
+ try {
420
+ const file = await services.downloadTemplate(templateCode, context);
421
+ const selected = templates.find(
422
+ (item) => item.templateCode === templateCode,
423
+ );
424
+ const url = URL.createObjectURL(
425
+ file instanceof Blob ? file : new Blob([file]),
426
+ );
427
+ const link = document.createElement('a');
428
+ link.href = url;
429
+ link.download = `${selected?.templateName || templateCode}.xlsx`;
430
+ document.body.appendChild(link);
431
+ link.click();
432
+ document.body.removeChild(link);
433
+ URL.revokeObjectURL(url);
434
+ message.success('模板下载成功');
435
+ } catch (error) {
436
+ message.error('模板下载失败,请稍后重试');
437
+ } finally {
438
+ setDownloading(false);
439
+ }
440
+ };
441
+
442
+ return (
443
+ <Modal
444
+ width={920}
445
+ title={
446
+ <div className={styles.modalTitle}>
447
+ <span className={styles.modalTitleIcon}>
448
+ <FileExcelOutlined />
449
+ </span>
450
+ <span>
451
+ <span className={styles.modalTitleText}>
452
+ {documentName || '业务单据'}导入
453
+ </span>
454
+ <span className={styles.modalTitleDescription}>
455
+ 下载模板、填写数据并上传校验
456
+ </span>
457
+ </span>
458
+ </div>
459
+ }
460
+ visible={visible}
461
+ footer={
462
+ step === 0
463
+ ? [
464
+ <Button key="cancel" onClick={onClose}>
465
+ 取消
466
+ </Button>,
467
+ <Button
468
+ key="parse"
469
+ type="primary"
470
+ icon={<UploadOutlined />}
471
+ loading={parsing}
472
+ disabled={!selectedTemplateCode || !selectedFile}
473
+ onClick={parse}
474
+ >
475
+ 上传并解析
476
+ </Button>,
477
+ ]
478
+ : null
479
+ }
480
+ onCancel={onClose}
481
+ wrapClassName={styles.importModal}
482
+ destroyOnClose
483
+ >
484
+ <div className={styles.stepsPanel}>
485
+ <Steps current={step} size="small">
486
+ <Steps.Step title="配置并上传" />
487
+ <Steps.Step title="校验并导入" />
488
+ </Steps>
489
+ </div>
490
+ {step === 0 && (
491
+ <Form form={form} layout="vertical" className={styles.form}>
492
+ <section className={styles.section}>
493
+ <div className={styles.sectionHeader}>
494
+ <span className={styles.sectionIndex}>1</span>
495
+ <span>
496
+ <span className={styles.sectionTitle}>选择导入模板</span>
497
+ <span className={styles.sectionDescription}>
498
+ 请先下载并使用当前模板整理数据
499
+ </span>
500
+ </span>
501
+ </div>
502
+ <div className={styles.templateRow}>
503
+ <Form.Item
504
+ label="导入模板"
505
+ name="templateCode"
506
+ className={styles.templateField}
507
+ rules={[{ required: true, message: '请选择导入模板' }]}
508
+ >
509
+ <Select
510
+ showSearch
511
+ optionFilterProp="children"
512
+ placeholder="请选择导入模板"
513
+ onChange={setSelectedTemplateCode}
514
+ >
515
+ {personalTemplates.length > 0 && (
516
+ <Select.OptGroup label="个人模板">
517
+ {personalTemplates.map((item) => (
518
+ <Select.Option
519
+ key={item.templateCode}
520
+ value={item.templateCode}
521
+ >
522
+ {item.templateName}
523
+ </Select.Option>
524
+ ))}
525
+ </Select.OptGroup>
526
+ )}
527
+ {publicTemplates.length > 0 && (
528
+ <Select.OptGroup label="公共模板">
529
+ {publicTemplates.map((item) => (
530
+ <Select.Option
531
+ key={item.templateCode}
532
+ value={item.templateCode}
533
+ >
534
+ {item.templateName}
535
+ </Select.Option>
536
+ ))}
537
+ </Select.OptGroup>
538
+ )}
539
+ </Select>
540
+ </Form.Item>
541
+ <Button
542
+ className={styles.downloadButton}
543
+ icon={<DownloadOutlined />}
544
+ loading={downloading}
545
+ onClick={downloadSelectedTemplate}
546
+ >
547
+ 下载模板
548
+ </Button>
549
+ </div>
550
+ {selectedTemplate && (
551
+ <Descriptions
552
+ bordered
553
+ size="small"
554
+ column={{ xs: 1, sm: 3 }}
555
+ className={styles.templateSummary}
556
+ >
557
+ <Descriptions.Item label="模板名称">
558
+ {selectedTemplate.templateName}
559
+ </Descriptions.Item>
560
+ <Descriptions.Item label="业务单据">
561
+ {documentName || selectedTemplate.businessFunctionCode || '-'}
562
+ </Descriptions.Item>
563
+ <Descriptions.Item label="字段映射">
564
+ {selectedTemplateFieldCount == null
565
+ ? '以模板配置为准'
566
+ : `${selectedTemplateFieldCount} 个字段`}
567
+ </Descriptions.Item>
568
+ </Descriptions>
569
+ )}
570
+ </section>
571
+
572
+ <section className={styles.section}>
573
+ <div className={styles.sectionHeader}>
574
+ <span className={styles.sectionIndex}>2</span>
575
+ <span>
576
+ <span className={styles.sectionTitle}>设置导入规则</span>
577
+ <span className={styles.sectionDescription}>
578
+ 选择数据写入方式与异常处理方式
579
+ </span>
580
+ </span>
581
+ </div>
582
+ <div className={styles.settingGrid}>
583
+ <Form.Item
584
+ className={styles.settingItem}
585
+ label={
586
+ <Space>
587
+ 导入策略
588
+ <Tooltip title="仅新增:已有数据报错;仅更新:不存在的数据报错;更新并新增:已有数据更新,不存在的数据新增。">
589
+ <QuestionCircleOutlined />
590
+ </Tooltip>
591
+ </Space>
592
+ }
593
+ name="strategy"
594
+ >
595
+ <Radio.Group className={styles.radioGroup}>
596
+ <Radio value="CREATE_ONLY">仅新增</Radio>
597
+ <Radio value="UPDATE_ONLY">仅更新</Radio>
598
+ <Radio value="UPSERT">更新并新增</Radio>
599
+ </Radio.Group>
600
+ </Form.Item>
601
+ <Form.Item
602
+ className={styles.settingItem}
603
+ label="错误处理"
604
+ name="errorHandlingMode"
605
+ >
606
+ <Radio.Group className={styles.radioGroup}>
607
+ <Radio value="ALL_ROLLBACK" disabled>
608
+ 全部回滚
609
+ </Radio>
610
+ <Radio value="PARTIAL_SUCCESS">
611
+ 部分成功(允许部分失败)
612
+ </Radio>
613
+ </Radio.Group>
614
+ </Form.Item>
615
+ </div>
616
+ </section>
617
+
618
+ <section className={`${styles.section} ${styles.uploadSection}`}>
619
+ <div className={styles.sectionHeader}>
620
+ <span className={styles.sectionIndex}>3</span>
621
+ <span>
622
+ <span className={styles.sectionTitle}>上传数据文件</span>
623
+ <span className={styles.sectionDescription}>
624
+ 上传后系统将先校验数据,不会直接写入业务单据
625
+ </span>
626
+ </span>
627
+ </div>
628
+ <Upload.Dragger
629
+ className={styles.uploadDragger}
630
+ beforeUpload={selectFile}
631
+ fileList={selectedFile ? [selectedFile] : []}
632
+ onRemove={() => {
633
+ setSelectedFile(undefined);
634
+ setFileName('');
635
+ setPreview(undefined);
636
+ }}
637
+ multiple={false}
638
+ accept=".xls,.xlsx,.csv"
639
+ >
640
+ <p className="ant-upload-drag-icon">
641
+ <InboxOutlined />
642
+ </p>
643
+ <p className="ant-upload-text">点击或拖拽文件到此区域上传</p>
644
+ <p className="ant-upload-hint">
645
+ 支持 XLSX、XLS、CSV,单个文件不超过 10 MB
646
+ </p>
647
+ </Upload.Dragger>
648
+ <Alert
649
+ className={styles.uploadAlert}
650
+ type="info"
651
+ showIcon
652
+ message="行数上限由系统配置决定,默认最多 100,000 行;系统繁忙时文件不会进入解析。"
653
+ />
654
+ {parsing && (
655
+ <div className={styles.progressPanel}>
656
+ <div className={styles.progressTitle}>
657
+ {fileName} · {preview?.stageName || '文件解析中'}
658
+ </div>
659
+ <Progress percent={preview?.progress || 0} size="small" />
660
+ </div>
661
+ )}
662
+ </section>
663
+ </Form>
664
+ )}
665
+ {step === 1 && (
666
+ <Preview
667
+ preview={preview}
668
+ executing={executing}
669
+ downloadingErrorFile={downloadingErrorFile}
670
+ onBack={() => setStep(0)}
671
+ onExecute={execute}
672
+ onDownloadErrorFile={downloadValidationErrorFile}
673
+ />
674
+ )}
675
+ </Modal>
676
+ );
677
+ };
678
+
679
+ export default ImportWizard;