@chenchaolong/plugin-trade-compliance-workbench 1.0.5 → 1.0.7

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 (196) hide show
  1. package/dist/index.d.ts +18 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +20 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/adapters/hsbianma.provider.d.ts +6 -8
  6. package/dist/lib/adapters/hsbianma.provider.d.ts.map +1 -1
  7. package/dist/lib/adapters/hsbianma.provider.js +159 -53
  8. package/dist/lib/adapters/hsbianma.provider.js.map +1 -1
  9. package/dist/lib/adapters/translation.provider.d.ts +7 -1
  10. package/dist/lib/adapters/translation.provider.d.ts.map +1 -1
  11. package/dist/lib/adapters/translation.provider.js +15 -5
  12. package/dist/lib/adapters/translation.provider.js.map +1 -1
  13. package/dist/lib/domain/catalog-localization.d.ts +14 -0
  14. package/dist/lib/domain/catalog-localization.d.ts.map +1 -0
  15. package/dist/lib/domain/catalog-localization.js +19 -0
  16. package/dist/lib/domain/catalog-localization.js.map +1 -0
  17. package/dist/lib/domain/file-name.d.ts +8 -0
  18. package/dist/lib/domain/file-name.d.ts.map +1 -0
  19. package/dist/lib/domain/file-name.js +111 -0
  20. package/dist/lib/domain/file-name.js.map +1 -0
  21. package/dist/lib/domain/import-issues.d.ts +4 -0
  22. package/dist/lib/domain/import-issues.d.ts.map +1 -1
  23. package/dist/lib/domain/import-issues.js +14 -3
  24. package/dist/lib/domain/import-issues.js.map +1 -1
  25. package/dist/lib/entities/catalog.entity.d.ts +5 -1
  26. package/dist/lib/entities/catalog.entity.d.ts.map +1 -1
  27. package/dist/lib/entities/catalog.entity.js +16 -0
  28. package/dist/lib/entities/catalog.entity.js.map +1 -1
  29. package/dist/lib/entities/customer.entity.d.ts +3 -0
  30. package/dist/lib/entities/customer.entity.d.ts.map +1 -1
  31. package/dist/lib/entities/customer.entity.js +12 -0
  32. package/dist/lib/entities/customer.entity.js.map +1 -1
  33. package/dist/lib/entities/shared.entity.d.ts +9 -0
  34. package/dist/lib/entities/shared.entity.d.ts.map +1 -1
  35. package/dist/lib/entities/shared.entity.js +33 -0
  36. package/dist/lib/entities/shared.entity.js.map +1 -1
  37. package/dist/lib/import-jobs/agent-handoff.dispatcher.d.ts +15 -0
  38. package/dist/lib/import-jobs/agent-handoff.dispatcher.d.ts.map +1 -0
  39. package/dist/lib/import-jobs/agent-handoff.dispatcher.js +114 -0
  40. package/dist/lib/import-jobs/agent-handoff.dispatcher.js.map +1 -0
  41. package/dist/lib/import-jobs/contracts.d.ts +76 -0
  42. package/dist/lib/import-jobs/contracts.d.ts.map +1 -0
  43. package/dist/lib/import-jobs/contracts.js +31 -0
  44. package/dist/lib/import-jobs/contracts.js.map +1 -0
  45. package/dist/lib/import-jobs/import-command.builder.d.ts +30 -0
  46. package/dist/lib/import-jobs/import-command.builder.d.ts.map +1 -0
  47. package/dist/lib/import-jobs/import-command.builder.js +97 -0
  48. package/dist/lib/import-jobs/import-command.builder.js.map +1 -0
  49. package/dist/lib/import-jobs/import-job-callback.processor.d.ts +15 -0
  50. package/dist/lib/import-jobs/import-job-callback.processor.d.ts.map +1 -0
  51. package/dist/lib/import-jobs/import-job-callback.processor.js +107 -0
  52. package/dist/lib/import-jobs/import-job-callback.processor.js.map +1 -0
  53. package/dist/lib/import-jobs/import-job.coordinator.d.ts +17 -0
  54. package/dist/lib/import-jobs/import-job.coordinator.d.ts.map +1 -0
  55. package/dist/lib/import-jobs/import-job.coordinator.js +256 -0
  56. package/dist/lib/import-jobs/import-job.coordinator.js.map +1 -0
  57. package/dist/lib/import-jobs/import-job.processor.d.ts +24 -0
  58. package/dist/lib/import-jobs/import-job.processor.d.ts.map +1 -0
  59. package/dist/lib/import-jobs/import-job.processor.js +213 -0
  60. package/dist/lib/import-jobs/import-job.processor.js.map +1 -0
  61. package/dist/lib/import-jobs/import-timeout.processor.d.ts +12 -0
  62. package/dist/lib/import-jobs/import-timeout.processor.d.ts.map +1 -0
  63. package/dist/lib/import-jobs/import-timeout.processor.js +37 -0
  64. package/dist/lib/import-jobs/import-timeout.processor.js.map +1 -0
  65. package/dist/lib/import-jobs/organization-lease.d.ts +39 -0
  66. package/dist/lib/import-jobs/organization-lease.d.ts.map +1 -0
  67. package/dist/lib/import-jobs/organization-lease.js +136 -0
  68. package/dist/lib/import-jobs/organization-lease.js.map +1 -0
  69. package/dist/lib/plugin-context.token.d.ts +4 -0
  70. package/dist/lib/plugin-context.token.d.ts.map +1 -0
  71. package/dist/lib/plugin-context.token.js +2 -0
  72. package/dist/lib/plugin-context.token.js.map +1 -0
  73. package/dist/lib/public-dto.d.ts +6 -0
  74. package/dist/lib/public-dto.d.ts.map +1 -0
  75. package/dist/lib/public-dto.js +33 -0
  76. package/dist/lib/public-dto.js.map +1 -0
  77. package/dist/lib/remote-components/trade-compliance-workbench/app.css +1 -1
  78. package/dist/lib/remote-components/trade-compliance-workbench/app.js +5 -5
  79. package/dist/lib/remote-ui/action-execution.d.ts +26 -0
  80. package/dist/lib/remote-ui/action-execution.d.ts.map +1 -0
  81. package/dist/lib/remote-ui/action-execution.js +85 -0
  82. package/dist/lib/remote-ui/action-execution.js.map +1 -0
  83. package/dist/lib/remote-ui/analytics-contracts.d.ts +3 -2
  84. package/dist/lib/remote-ui/analytics-contracts.d.ts.map +1 -1
  85. package/dist/lib/remote-ui/analytics-contracts.js.map +1 -1
  86. package/dist/lib/remote-ui/app-source.d.ts.map +1 -1
  87. package/dist/lib/remote-ui/app-source.js +426 -169
  88. package/dist/lib/remote-ui/app-source.js.map +1 -1
  89. package/dist/lib/remote-ui/company-logo-lifecycle.d.ts +14 -0
  90. package/dist/lib/remote-ui/company-logo-lifecycle.d.ts.map +1 -0
  91. package/dist/lib/remote-ui/company-logo-lifecycle.js +52 -0
  92. package/dist/lib/remote-ui/company-logo-lifecycle.js.map +1 -0
  93. package/dist/lib/remote-ui/components/data-grid.d.ts +9 -4
  94. package/dist/lib/remote-ui/components/data-grid.d.ts.map +1 -1
  95. package/dist/lib/remote-ui/components/data-grid.js +41 -9
  96. package/dist/lib/remote-ui/components/data-grid.js.map +1 -1
  97. package/dist/lib/remote-ui/components/forms.d.ts +6 -1
  98. package/dist/lib/remote-ui/components/forms.d.ts.map +1 -1
  99. package/dist/lib/remote-ui/components/forms.js +6 -1
  100. package/dist/lib/remote-ui/components/forms.js.map +1 -1
  101. package/dist/lib/remote-ui/components/layout.d.ts.map +1 -1
  102. package/dist/lib/remote-ui/components/layout.js +3 -3
  103. package/dist/lib/remote-ui/components/layout.js.map +1 -1
  104. package/dist/lib/remote-ui/contract-import.d.ts +37 -32
  105. package/dist/lib/remote-ui/contract-import.d.ts.map +1 -1
  106. package/dist/lib/remote-ui/contract-import.js +51 -59
  107. package/dist/lib/remote-ui/contract-import.js.map +1 -1
  108. package/dist/lib/remote-ui/contracts.d.ts +20 -27
  109. package/dist/lib/remote-ui/contracts.d.ts.map +1 -1
  110. package/dist/lib/remote-ui/contracts.js +8 -0
  111. package/dist/lib/remote-ui/contracts.js.map +1 -1
  112. package/dist/lib/remote-ui/customer-contract-contracts.d.ts +5 -11
  113. package/dist/lib/remote-ui/customer-contract-contracts.d.ts.map +1 -1
  114. package/dist/lib/remote-ui/customer-contract-contracts.js.map +1 -1
  115. package/dist/lib/remote-ui/date-time.d.ts +3 -0
  116. package/dist/lib/remote-ui/date-time.d.ts.map +1 -0
  117. package/dist/lib/remote-ui/date-time.js +58 -0
  118. package/dist/lib/remote-ui/date-time.js.map +1 -0
  119. package/dist/lib/remote-ui/model.d.ts +26 -1
  120. package/dist/lib/remote-ui/model.d.ts.map +1 -1
  121. package/dist/lib/remote-ui/model.js +151 -15
  122. package/dist/lib/remote-ui/model.js.map +1 -1
  123. package/dist/lib/remote-ui/pages/analytics.d.ts +2 -0
  124. package/dist/lib/remote-ui/pages/analytics.d.ts.map +1 -1
  125. package/dist/lib/remote-ui/pages/analytics.js +11 -6
  126. package/dist/lib/remote-ui/pages/analytics.js.map +1 -1
  127. package/dist/lib/remote-ui/pages/catalogs.d.ts +37 -14
  128. package/dist/lib/remote-ui/pages/catalogs.d.ts.map +1 -1
  129. package/dist/lib/remote-ui/pages/catalogs.js +149 -91
  130. package/dist/lib/remote-ui/pages/catalogs.js.map +1 -1
  131. package/dist/lib/remote-ui/pages/contracts.d.ts +11 -6
  132. package/dist/lib/remote-ui/pages/contracts.d.ts.map +1 -1
  133. package/dist/lib/remote-ui/pages/contracts.js +45 -33
  134. package/dist/lib/remote-ui/pages/contracts.js.map +1 -1
  135. package/dist/lib/remote-ui/pages/files.d.ts +29 -3
  136. package/dist/lib/remote-ui/pages/files.d.ts.map +1 -1
  137. package/dist/lib/remote-ui/pages/files.js +60 -16
  138. package/dist/lib/remote-ui/pages/files.js.map +1 -1
  139. package/dist/lib/remote-ui/pages/home.d.ts.map +1 -1
  140. package/dist/lib/remote-ui/pages/home.js +18 -18
  141. package/dist/lib/remote-ui/pages/home.js.map +1 -1
  142. package/dist/lib/remote-ui/pages/procurement.d.ts +29 -4
  143. package/dist/lib/remote-ui/pages/procurement.d.ts.map +1 -1
  144. package/dist/lib/remote-ui/pages/procurement.js +58 -63
  145. package/dist/lib/remote-ui/pages/procurement.js.map +1 -1
  146. package/dist/lib/remote-ui/pages/sales.d.ts +47 -1
  147. package/dist/lib/remote-ui/pages/sales.d.ts.map +1 -1
  148. package/dist/lib/remote-ui/pages/sales.js +60 -20
  149. package/dist/lib/remote-ui/pages/sales.js.map +1 -1
  150. package/dist/lib/remote-ui/pages/settings.d.ts +34 -1
  151. package/dist/lib/remote-ui/pages/settings.d.ts.map +1 -1
  152. package/dist/lib/remote-ui/pages/settings.js +120 -94
  153. package/dist/lib/remote-ui/pages/settings.js.map +1 -1
  154. package/dist/lib/remote-ui/polling.d.ts +41 -0
  155. package/dist/lib/remote-ui/polling.d.ts.map +1 -0
  156. package/dist/lib/remote-ui/polling.js +148 -0
  157. package/dist/lib/remote-ui/polling.js.map +1 -0
  158. package/dist/lib/remote-ui/procurement-contracts.d.ts +4 -9
  159. package/dist/lib/remote-ui/procurement-contracts.d.ts.map +1 -1
  160. package/dist/lib/remote-ui/sales-contracts.d.ts +2 -2
  161. package/dist/lib/remote-ui/sales-contracts.d.ts.map +1 -1
  162. package/dist/lib/remote-ui/settings-contracts.d.ts +9 -1
  163. package/dist/lib/remote-ui/settings-contracts.d.ts.map +1 -1
  164. package/dist/lib/remote-ui/settings-contracts.js.map +1 -1
  165. package/dist/lib/remote-ui/toast-queue.d.ts +29 -0
  166. package/dist/lib/remote-ui/toast-queue.d.ts.map +1 -0
  167. package/dist/lib/remote-ui/toast-queue.js +71 -0
  168. package/dist/lib/remote-ui/toast-queue.js.map +1 -0
  169. package/dist/lib/sales-file.service.d.ts.map +1 -1
  170. package/dist/lib/sales-file.service.js +5 -5
  171. package/dist/lib/sales-file.service.js.map +1 -1
  172. package/dist/lib/types.d.ts +5 -0
  173. package/dist/lib/types.d.ts.map +1 -1
  174. package/dist/lib/types.js +3 -0
  175. package/dist/lib/types.js.map +1 -1
  176. package/dist/lib/workbench-action-contract.d.ts +4 -0
  177. package/dist/lib/workbench-action-contract.d.ts.map +1 -0
  178. package/dist/lib/workbench-action-contract.js +43 -0
  179. package/dist/lib/workbench-action-contract.js.map +1 -0
  180. package/dist/lib/workbench-view.provider.d.ts +57 -78
  181. package/dist/lib/workbench-view.provider.d.ts.map +1 -1
  182. package/dist/lib/workbench-view.provider.js +89 -91
  183. package/dist/lib/workbench-view.provider.js.map +1 -1
  184. package/dist/lib/workbench.middleware.d.ts +129 -72
  185. package/dist/lib/workbench.middleware.d.ts.map +1 -1
  186. package/dist/lib/workbench.middleware.js +16 -3
  187. package/dist/lib/workbench.middleware.js.map +1 -1
  188. package/dist/lib/workbench.plugin.d.ts +3 -1
  189. package/dist/lib/workbench.plugin.d.ts.map +1 -1
  190. package/dist/lib/workbench.plugin.js +13 -4
  191. package/dist/lib/workbench.plugin.js.map +1 -1
  192. package/dist/lib/workbench.service.d.ts +136 -40
  193. package/dist/lib/workbench.service.d.ts.map +1 -1
  194. package/dist/lib/workbench.service.js +985 -71
  195. package/dist/lib/workbench.service.js.map +1 -1
  196. package/package.json +6 -5
@@ -2,12 +2,14 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import React from 'react';
3
3
  import { createRoot } from 'react-dom/client';
4
4
  import { createRemoteBridge } from './bridge.js';
5
- import { createBlobUrlOwner, createLatestCommittedRequest, createLatestRequestGate, normalizeWorkbenchData, routeDataRequest, unwrapResponse } from './model.js';
5
+ import { resolvePaginationPageSize } from './contracts.js';
6
+ import { clampPageAfterSingleDeletion, createBlobUrlOwner, createKeyedLatestRequestGate, createLatestCommittedRequest, createLatestRequestGate, generatedFileHistoryKey, normalizeWorkbenchData, routeDataRequest, runKeyedLatestRequest, unwrapResponse } from './model.js';
6
7
  import { encodeViewFilters } from '../view-parameters.js';
7
8
  import { InfoNotice } from './components/data-grid.js';
8
- import { LoadingBlocker, Toast } from './components/forms.js';
9
+ import { LoadingBlocker, ToastStack } from './components/forms.js';
10
+ import { addToast, createToastId, createToastTimerRegistry, removeToast } from './toast-queue.js';
9
11
  import { buildBreadcrumbs, WorkbenchShell } from './components/layout.js';
10
- import { CatalogCommands, CatalogImportError, ControlledGoodsPage, findActiveCatalogImportTask, findLatestFailedCatalogImportTask, runCatalogImportOrchestration, SanctionedCompaniesPage } from './pages/catalogs.js';
12
+ import { CatalogCommands, CatalogImportError, ControlledGoodsPage, findActiveCatalogImportTasks, findLatestFailedCatalogImportTask, runCatalogImportOrchestration, SanctionedCompaniesPage } from './pages/catalogs.js';
11
13
  import { HomePage } from './pages/home.js';
12
14
  import { ProcurementPage } from './pages/procurement.js';
13
15
  import { CustomerContractsPage, adaptCustomerContractReview } from './pages/contracts.js';
@@ -15,7 +17,9 @@ import { SalesPage } from './pages/sales.js';
15
17
  import { SalesFilesPage } from './pages/files.js';
16
18
  import { AnalyticsPage } from './pages/analytics.js';
17
19
  import { SettingsPage } from './pages/settings.js';
18
- import { ContractImportError, findRecoverableContractImport, findResumableContractImport, runContractImportOrchestration, runCustomerContractReanalysis } from './contract-import.js';
20
+ import { ContractImportError, findRecoverableContractImport, findResumableContractImports, runContractImportOrchestration, runCustomerContractReanalysis, taskTransition } from './contract-import.js';
21
+ import { compareTaskBusinessOrder, createPendingResultQueue, createPollingTaskTracker, isAbortError, isPollingObservationTimeout, submitAndRefreshTaskState } from './polling.js';
22
+ import { executeWorkbenchAction } from './action-execution.js';
19
23
  import './styles.css';
20
24
  const pageMeta = {
21
25
  home: { title: '首页', subtitle: '查看系统风险操作提示与外贸合规业务流程。' },
@@ -30,24 +34,48 @@ const pageMeta = {
30
34
  };
31
35
  const bridge = createRemoteBridge();
32
36
  const initialAnalyticsQuery = { mode: 'PURCHASE', dimension: 'SUPPLIER', filters: {}, page: 1, pageSize: 20 };
37
+ function comparePendingReviewBusinessOrder(left, right) {
38
+ const leftTask = left.kind === 'CONTRACT_IMPORT' ? left.completion.task : left.result.run;
39
+ const rightTask = right.kind === 'CONTRACT_IMPORT' ? right.completion.task : right.result.run;
40
+ return compareTaskBusinessOrder(leftTask, rightTask);
41
+ }
42
+ class ActionResponseError extends Error {
43
+ constructor(message, code) {
44
+ super(message);
45
+ this.code = code;
46
+ this.name = 'ActionResponseError';
47
+ }
48
+ }
33
49
  let excelEditorRuntimePromise = null;
34
50
  function App() {
35
51
  const [route, setRoute] = React.useState({ type: 'page', page: 'home' });
36
52
  const routeRef = React.useRef(route);
37
53
  const contextRef = React.useRef(null);
38
54
  const requestGateRef = React.useRef(createLatestRequestGate());
55
+ const taskStateRequestGateRef = React.useRef(createLatestRequestGate());
56
+ const taskRecoveryTimerRef = React.useRef(null);
39
57
  const routeRequestGateRef = React.useRef(createLatestRequestGate());
58
+ const settingsRatesRequestGateRef = React.useRef(createLatestRequestGate());
59
+ const generatedFilesRequestGateRef = React.useRef(createKeyedLatestRequestGate());
60
+ const activeGeneratedFilesKeyRef = React.useRef(null);
61
+ const settingsRatesQueryRef = React.useRef({ page: 1, pageSize: 20 });
40
62
  const analyticsRequestsRef = React.useRef(createLatestCommittedRequest(initialAnalyticsQuery));
41
63
  const [analyticsCommittedQuery, setAnalyticsCommittedQuery] = React.useState(initialAnalyticsQuery);
42
64
  const [data, setData] = React.useState(() => normalizeWorkbenchData(null));
43
65
  const [busy, setBusy] = React.useState(false);
44
66
  const [actionBusy, setActionBusy] = React.useState(false);
45
67
  const [catalogCommand, setCatalogCommand] = React.useState(null);
46
- const [importCompletion, setImportCompletion] = React.useState(null);
68
+ const [importCompletions, setImportCompletions] = React.useState({ CONTROLLED_CATALOG: null, SANCTION_CATALOG: null });
47
69
  const [importFailures, setImportFailures] = React.useState({ CONTROLLED_CATALOG: null, SANCTION_CATALOG: null });
48
- const runningImportTaskRef = React.useRef(null);
49
- const runningContractTasksRef = React.useRef(new Set());
50
- const handledContractTasksRef = React.useRef({ PURCHASE_CONTRACT: null, CUSTOMER_CONTRACT: null });
70
+ const catalogTaskTrackerRef = React.useRef(createPollingTaskTracker());
71
+ const contractTaskTrackerRef = React.useRef(createPollingTaskTracker());
72
+ const reanalysisTaskTrackerRef = React.useRef(createPollingTaskTracker());
73
+ const handledContractTasksRef = React.useRef(new Set());
74
+ const handledReanalysisResultsRef = React.useRef(new Set());
75
+ const pendingReviewResultsRef = React.useRef(createPendingResultQueue(item => item.kind === 'CONTRACT_IMPORT' ? item.completion.task.id : item.result.run.id, comparePendingReviewBusinessOrder));
76
+ const activeReviewResultRef = React.useRef(null);
77
+ const presentNextReviewRef = React.useRef(() => undefined);
78
+ const reviewPresentationTimerRef = React.useRef(null);
51
79
  const blobUrlOwnerRef = React.useRef(createBlobUrlOwner());
52
80
  const changeRouteRef = React.useRef(() => undefined);
53
81
  const [procurementContext, setProcurementContext] = React.useState({ supplier: null, order: null, metrics: null });
@@ -57,15 +85,24 @@ function App() {
57
85
  const [customerReanalysisState, setCustomerReanalysisState] = React.useState({ status: 'IDLE' });
58
86
  const [activeSalesFileOrder, setActiveSalesFileOrder] = React.useState(null);
59
87
  const [editingSalesFile, setEditingSalesFile] = React.useState(null);
60
- const [toast, setToast] = React.useState(null);
88
+ const [toasts, setToasts] = React.useState([]);
89
+ const [toastTimers] = React.useState(() => createToastTimerRegistry((id) => setToasts((items) => removeToast(items, id))));
61
90
  const [analyticsError, setAnalyticsError] = React.useState(null);
91
+ const setToast = React.useCallback((toast) => {
92
+ setToasts((items) => addToast(items, toast, createToastId));
93
+ }, []);
94
+ const closeToast = React.useCallback((id) => {
95
+ toastTimers.dismiss(id);
96
+ setToasts((items) => removeToast(items, id));
97
+ }, [toastTimers]);
62
98
  const loadRoute = React.useCallback(async (targetRoute, page = 1, pageSize = 20, filters = {}) => {
63
- const request = routeDataRequest(targetRoute, page, pageSize, filters);
99
+ const requestedPageSize = resolvePaginationPageSize(pageSize);
100
+ const request = routeDataRequest(targetRoute, page, requestedPageSize, filters);
64
101
  if (!request)
65
102
  return;
66
103
  const collection = String(asRecord(request.parameters).collection);
67
104
  const requests = collection === 'analytics' ? analyticsRequestsRef.current : routeRequestGateRef.current;
68
- const analyticsCandidate = collection === 'analytics' ? analyticsQueryFromRoute(page, pageSize, filters) : null;
105
+ const analyticsCandidate = collection === 'analytics' ? analyticsQueryFromRoute(page, requestedPageSize, filters) : null;
69
106
  const requestId = analyticsCandidate ? analyticsRequestsRef.current.begin(analyticsCandidate) : routeRequestGateRef.current.begin();
70
107
  setBusy(true);
71
108
  try {
@@ -78,15 +115,18 @@ function App() {
78
115
  setAnalyticsCommittedQuery(analyticsRequestsRef.current.committed());
79
116
  setData(current => ({ ...current, analytics: normalizeWorkbenchData({ summary: { analytics: payload } }).analytics }));
80
117
  }
81
- else if (collection === 'settings')
82
- setData(current => ({ ...current, settings: normalizeWorkbenchData({ summary: { settings: payload } }).settings }));
118
+ else if (collection === 'settings') {
119
+ const settings = normalizeWorkbenchData({ summary: { settings: payload } }).settings;
120
+ settingsRatesQueryRef.current = { page: settings.rates.page, pageSize: settings.rates.pageSize };
121
+ setData(current => ({ ...current, settings }));
122
+ }
83
123
  else
84
124
  setData(current => ({
85
125
  ...current,
86
126
  [collection]: {
87
127
  ...payload,
88
128
  items: Array.isArray(payload.items) ? payload.items : [],
89
- total: numberValue(payload.total, 0), page: numberValue(payload.page, page), pageSize: numberValue(payload.pageSize, pageSize)
129
+ total: numberValue(payload.total, 0), page: numberValue(payload.page, page), pageSize: requestedPageSize
90
130
  }
91
131
  }));
92
132
  if (targetRoute.type === 'procurement')
@@ -116,7 +156,7 @@ function App() {
116
156
  const requestId = gate.begin();
117
157
  setBusy(true);
118
158
  try {
119
- const response = await bridge.requestData({ page: 1, pageSize: 200, parameters: active.initialQuery?.parameters ?? {} });
159
+ const response = await bridge.requestData({ page: 1, pageSize: 100, parameters: active.initialQuery?.parameters ?? {} });
120
160
  if (gate.isLatest(requestId)) {
121
161
  const normalized = normalizeWorkbenchData(response);
122
162
  bridge.configure({ uploadPolicy: normalized.uploadPolicy });
@@ -133,6 +173,64 @@ function App() {
133
173
  setBusy(gate.activeCount() + routeRequestGateRef.current.activeCount() + analyticsRequestsRef.current.activeCount() > 0);
134
174
  }
135
175
  }, [loadRoute]);
176
+ const loadTaskState = React.useCallback(async () => {
177
+ const active = contextRef.current;
178
+ if (!active)
179
+ return false;
180
+ const gate = taskStateRequestGateRef.current;
181
+ const requestId = gate.begin();
182
+ try {
183
+ const response = await bridge.requestData({ page: 1, pageSize: 100, parameters: active.initialQuery?.parameters ?? {} });
184
+ if (!gate.commit(requestId))
185
+ return false;
186
+ const normalized = normalizeWorkbenchData(response);
187
+ setData(current => ({
188
+ ...current,
189
+ controlledGoods: normalized.controlledGoods,
190
+ controlledBatches: normalized.controlledBatches,
191
+ sanctionedCompanies: normalized.sanctionedCompanies,
192
+ sanctionBatches: normalized.sanctionBatches,
193
+ importTasks: normalized.importTasks,
194
+ customerContractAnalyses: normalized.customerContractAnalyses
195
+ }));
196
+ return true;
197
+ }
198
+ catch (error) {
199
+ if (gate.isLatest(requestId))
200
+ setToast({ message: error instanceof Error ? `任务状态刷新失败:${error.message}` : '任务状态刷新失败', tone: 'error' });
201
+ return false;
202
+ }
203
+ finally {
204
+ gate.finish(requestId);
205
+ }
206
+ }, []);
207
+ const scheduleTaskStateRecovery = React.useCallback(() => {
208
+ if (taskRecoveryTimerRef.current != null)
209
+ return;
210
+ taskRecoveryTimerRef.current = window.setInterval(() => {
211
+ void loadTaskState().then(refreshed => {
212
+ if (!refreshed || taskRecoveryTimerRef.current == null)
213
+ return;
214
+ window.clearInterval(taskRecoveryTimerRef.current);
215
+ taskRecoveryTimerRef.current = null;
216
+ });
217
+ }, 2000);
218
+ }, [loadTaskState]);
219
+ React.useEffect(() => () => {
220
+ if (taskRecoveryTimerRef.current != null)
221
+ window.clearInterval(taskRecoveryTimerRef.current);
222
+ if (reviewPresentationTimerRef.current != null)
223
+ window.clearTimeout(reviewPresentationTimerRef.current);
224
+ }, []);
225
+ React.useEffect(() => {
226
+ return () => catalogTaskTrackerRef.current.abortAll();
227
+ }, []);
228
+ React.useEffect(() => {
229
+ return () => contractTaskTrackerRef.current.abortAll();
230
+ }, []);
231
+ React.useEffect(() => {
232
+ return () => reanalysisTaskTrackerRef.current.abortAll();
233
+ }, []);
136
234
  React.useEffect(() => {
137
235
  bridge.start({
138
236
  onInit(nextContext) {
@@ -148,84 +246,87 @@ function App() {
148
246
  });
149
247
  return () => { blobUrlOwnerRef.current.dispose(); bridge.destroy(); };
150
248
  }, [load]);
249
+ React.useEffect(() => () => toastTimers.dispose(), [toastTimers]);
151
250
  React.useEffect(() => {
152
251
  const timer = window.setTimeout(() => bridge.reportResize(), 0);
153
252
  return () => window.clearTimeout(timer);
154
253
  }, [route, data, busy]);
155
254
  React.useEffect(() => {
156
- if (!toast)
157
- return;
158
- const timer = window.setTimeout(() => setToast(null), 4200);
159
- return () => window.clearTimeout(timer);
160
- }, [toast]);
255
+ toastTimers.sync(toasts);
256
+ }, [toastTimers, toasts]);
161
257
  const executeAction = React.useCallback(async (actionKey, input) => {
162
258
  setActionBusy(true);
163
259
  try {
164
- const result = asActionResult(unwrapResponse(await bridge.executeAction(actionKey, input)));
165
- if (result.success === false)
166
- throw new Error(resolveActionMessage(result) || '操作失败');
167
- if (actionKey === 'download_import_issues')
168
- downloadActionFile(result.data);
169
- if (result.refresh && shouldRefreshAfterAction(actionKey))
170
- await load();
171
- setToast({ message: resolveActionMessage(result) || '操作成功', tone: 'success' });
172
- return result;
173
- }
174
- catch (error) {
175
- setToast({ message: error instanceof Error ? error.message : '操作失败', tone: 'error' });
176
- throw error;
260
+ return await executeWorkbenchAction({
261
+ actionKey,
262
+ mutate: async () => asActionResult(unwrapResponse(await bridge.executeAction(actionKey, input))),
263
+ refresh: load,
264
+ notify: setToast,
265
+ resolveMessage: resolveActionMessage,
266
+ onResult: result => { if (actionKey === 'download_import_issues' || actionKey === 'download_sales_file')
267
+ downloadActionFile(result.data); }
268
+ });
177
269
  }
178
270
  finally {
179
271
  setActionBusy(false);
180
272
  }
181
273
  }, [load]);
182
274
  const executeSalesFileAction = React.useCallback((actionKey, input) => executeAction(actionKey, input), [executeAction]);
183
- const orchestrateImport = React.useCallback(async (start) => {
275
+ const schedulePendingReviewPresentation = React.useCallback(() => {
276
+ if (reviewPresentationTimerRef.current != null)
277
+ return;
278
+ reviewPresentationTimerRef.current = window.setTimeout(() => {
279
+ reviewPresentationTimerRef.current = null;
280
+ presentNextReviewRef.current();
281
+ }, 0);
282
+ }, []);
283
+ const enqueuePendingReview = React.useCallback((result) => {
284
+ pendingReviewResultsRef.current.enqueue(result);
285
+ schedulePendingReviewPresentation();
286
+ }, [schedulePendingReviewPresentation]);
287
+ const orchestrateImport = React.useCallback(async (start, signal) => {
184
288
  const taskId = start.data?.task.id;
185
289
  if (!taskId)
186
290
  throw new Error('解析响应缺少任务标识');
187
- if (runningImportTaskRef.current === taskId)
188
- throw new Error('该目录任务正在解析中');
189
- runningImportTaskRef.current = taskId;
190
- setActionBusy(true);
191
291
  const documentType = start.data?.task.documentType;
192
292
  if (documentType)
193
293
  setImportFailures((current) => ({ ...current, [documentType]: null }));
194
294
  try {
195
295
  const completion = await runCatalogImportOrchestration({
196
296
  start,
197
- invokeClientCommand: (commandKey, payload) => bridge.invokeClientCommand(commandKey, payload),
198
- markCommandDispatched: async (taskId) => {
199
- const result = asActionResult(unwrapResponse(await bridge.executeAction('mark_import_command_dispatched', { taskId })));
200
- if (result.success === false)
201
- throw new Error(resolveActionMessage(result) || '记录解析命令状态失败');
202
- },
203
297
  pollTask: async (taskId) => asImportCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId } }))),
204
- refresh: load
298
+ refresh: loadTaskState,
299
+ signal,
300
+ onTransition: (previous, next) => {
301
+ const notice = taskTransition(previous, next);
302
+ if (notice)
303
+ setToast(notice);
304
+ },
305
+ onPollError: error => setToast({ message: `任务状态查询失败:${error.message}`, tone: 'error' })
205
306
  });
206
- setImportCompletion(completion);
207
- setToast({ message: '目录导入完成', tone: 'success' });
307
+ setImportCompletions(current => ({ ...current, [completion.task.documentType]: completion }));
208
308
  return completion;
209
309
  }
210
310
  catch (error) {
211
- if (error instanceof CatalogImportError)
311
+ if (isAbortError(error))
312
+ throw error;
313
+ if (isPollingObservationTimeout(error)) {
314
+ setToast({ message: '任务仍在后台处理中,将继续跟踪', tone: 'info' });
315
+ scheduleTaskStateRecovery();
316
+ throw error;
317
+ }
318
+ if (error instanceof CatalogImportError) {
212
319
  setImportFailures((current) => ({ ...current, [error.task.documentType]: error.task }));
213
- setToast({ message: error instanceof Error ? error.message : '目录解析失败', tone: 'error' });
320
+ }
321
+ else
322
+ setToast({ message: error instanceof Error ? error.message : '目录解析失败', tone: 'error' });
214
323
  throw error;
215
324
  }
216
- finally {
217
- runningImportTaskRef.current = null;
218
- setActionBusy(false);
219
- }
220
- }, [load]);
221
- const orchestrateContractImport = React.useCallback(async (start) => {
325
+ }, [loadTaskState, scheduleTaskStateRecovery]);
326
+ const orchestrateContractImport = React.useCallback(async (start, signal) => {
222
327
  const task = start.data?.task;
223
328
  if (!task)
224
329
  throw new Error('解析响应缺少合同任务标识');
225
- if (runningContractTasksRef.current.has(task.id))
226
- throw new Error('该合同任务正在解析中');
227
- runningContractTasksRef.current.add(task.id);
228
- setActionBusy(true);
229
330
  if (task.documentType === 'PURCHASE_CONTRACT') {
230
331
  blobUrlOwnerRef.current.clear('purchase');
231
332
  setPurchaseImportTask(task);
@@ -238,29 +339,28 @@ function App() {
238
339
  try {
239
340
  const completion = await runContractImportOrchestration({
240
341
  start,
241
- invokeClientCommand: (commandKey, payload) => bridge.invokeClientCommand(commandKey, payload),
242
- markCommandDispatched: async (taskId) => {
243
- const result = asActionResult(unwrapResponse(await bridge.executeAction('mark_import_command_dispatched', { taskId })));
244
- if (result.success === false)
245
- throw new Error(resolveActionMessage(result) || '记录解析命令状态失败');
342
+ signal,
343
+ pollTask: async (taskId) => asContractCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId } }))),
344
+ onTransition: (previous, next) => {
345
+ const notice = taskTransition(previous, next);
346
+ if (notice)
347
+ setToast(notice);
246
348
  },
247
- pollTask: async (taskId) => asContractCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId } })))
349
+ onPollError: error => setToast({ message: `任务状态查询失败:${error.message}`, tone: 'error' })
248
350
  });
249
- handledContractTasksRef.current[task.documentType] = task.id;
250
351
  if (completion.task.status === 'AWAITING_REVIEW') {
251
- if (completion.task.documentType === 'PURCHASE_CONTRACT') {
252
- const draft = adaptPurchaseReview(completion.task);
253
- setPurchaseImportTask(completion.task);
254
- setPurchaseReviewDraft(await attachPurchaseSource(draft, completion.task, blobUrlOwnerRef.current));
255
- }
256
- else {
257
- const review = adaptCustomerContractReview(completion.task.resultPayload, { reviewSource: 'IMPORT_DRAFT', importTaskId: completion.task.id, sourceFileId: completion.task.sourceFileId ?? '', sourceFileName: completion.task.sourceFileName ?? '' });
258
- setCustomerImportState({ status: 'AWAITING_REVIEW', importTaskId: completion.task.id, review: await attachCustomerSource(review, blobUrlOwnerRef.current) });
259
- }
352
+ enqueuePendingReview({ kind: 'CONTRACT_IMPORT', completion });
260
353
  }
261
354
  return completion;
262
355
  }
263
356
  catch (error) {
357
+ if (isAbortError(error))
358
+ throw error;
359
+ if (isPollingObservationTimeout(error)) {
360
+ setToast({ message: '任务仍在后台处理中,将继续跟踪', tone: 'info' });
361
+ scheduleTaskStateRecovery();
362
+ throw error;
363
+ }
264
364
  const failed = error instanceof ContractImportError ? error.task : task;
265
365
  if (task.documentType === 'PURCHASE_CONTRACT') {
266
366
  setPurchaseImportTask({ ...failed, status: failed.status === 'TIMED_OUT' ? 'TIMED_OUT' : 'FAILED', errorMessage: error instanceof Error ? error.message : '采购合同解析失败' });
@@ -268,62 +368,58 @@ function App() {
268
368
  }
269
369
  else
270
370
  setCustomerImportState({ status: 'FAILED', importTaskId: failed.id, message: error instanceof Error ? error.message : '客户合同解析失败' });
271
- setToast({ message: error instanceof Error ? error.message : '合同解析失败', tone: 'error' });
371
+ if (!(error instanceof ContractImportError))
372
+ setToast({ message: error instanceof Error ? error.message : '合同解析失败', tone: 'error' });
272
373
  throw error;
273
374
  }
274
- finally {
275
- runningContractTasksRef.current.delete(task.id);
276
- setActionBusy(false);
277
- }
278
- }, []);
279
- const executeFileAction = React.useCallback(async (actionKey, file, input) => {
375
+ }, [enqueuePendingReview, scheduleTaskStateRecovery]);
376
+ const executeFileAction = React.useCallback(async (actionKey, file, input, notifyError = true) => {
280
377
  try {
281
- const start = asActionResult(unwrapResponse(await bridge.executeFileAction(actionKey, file, input)));
378
+ const raw = await submitAndRefreshTaskState(() => bridge.executeFileAction(actionKey, file, input), loadTaskState);
379
+ const start = asActionResult(unwrapResponse(raw));
282
380
  if (start.success === false)
283
- throw new Error(resolveActionMessage(start) || '上传失败');
284
- if (actionKey === 'upload_purchase_contract' || actionKey === 'upload_customer_contract') {
285
- const completion = await orchestrateContractImport(start);
286
- return { success: true, data: completion, message: '合同解析完成,等待审核', refresh: false };
287
- }
288
- const completion = await orchestrateImport(start);
289
- return { success: true, data: completion, message: '目录导入完成', refresh: true };
381
+ throw actionResponseError(start, '上传失败');
382
+ setToast({ message: resolveActionMessage(start) || '解析任务已提交', tone: 'info' });
383
+ return start;
290
384
  }
291
385
  catch (error) {
292
- if (!(error instanceof CatalogImportError))
386
+ if (notifyError && !(error instanceof CatalogImportError))
293
387
  setToast({ message: error instanceof Error ? error.message : '上传失败', tone: 'error' });
294
388
  throw error;
295
389
  }
296
- }, [orchestrateContractImport, orchestrateImport]);
390
+ }, [loadTaskState]);
391
+ const executeCatalogFileAction = React.useCallback((actionKey, file, input) => executeFileAction(actionKey, file, input, false), [executeFileAction]);
297
392
  React.useEffect(() => {
298
393
  const tasks = data.importTasks.items;
299
394
  setImportFailures({
300
395
  CONTROLLED_CATALOG: findLatestFailedCatalogImportTask(tasks, 'CONTROLLED_CATALOG'),
301
396
  SANCTION_CATALOG: findLatestFailedCatalogImportTask(tasks, 'SANCTION_CATALOG')
302
397
  });
303
- const active = findActiveCatalogImportTask(tasks);
304
- if (active && runningImportTaskRef.current !== active.id && active.executionCommand) {
305
- void orchestrateImport({ success: true, data: { task: active, commands: [active.executionCommand], polling: { intervalMs: 2000, timeoutMs: 1200000 } } }).catch(() => undefined);
306
- return;
398
+ for (const active of findActiveCatalogImportTasks(tasks)) {
399
+ const running = catalogTaskTrackerRef.current.run(active.id, signal => orchestrateImport({ success: true, data: { task: active } }, signal));
400
+ if (running)
401
+ void running.catch(() => undefined);
307
402
  }
308
403
  }, [data.importTasks.items, orchestrateImport]);
309
404
  React.useEffect(() => {
310
405
  const tasks = data.importTasks.items;
406
+ for (const resumable of findResumableContractImports(tasks)) {
407
+ if (handledContractTasksRef.current.has(resumable.id))
408
+ continue;
409
+ const running = contractTaskTrackerRef.current.run(resumable.id, signal => orchestrateContractImport({ success: true, data: { task: resumable } }, signal));
410
+ if (running)
411
+ void running.catch(() => undefined);
412
+ }
311
413
  for (const documentType of ['PURCHASE_CONTRACT', 'CUSTOMER_CONTRACT']) {
312
- const resumable = findResumableContractImport(tasks, documentType);
313
414
  const failed = findRecoverableContractImport(tasks, documentType);
314
- if (resumable && handledContractTasksRef.current[documentType] !== resumable.id && !runningContractTasksRef.current.has(resumable.id)) {
315
- if (resumable.status === 'AWAITING_REVIEW' || resumable.executionCommand) {
316
- void orchestrateContractImport({ success: true, data: { task: resumable, commands: resumable.executionCommand ? [resumable.executionCommand] : [], polling: { intervalMs: 2000, timeoutMs: 1200000 } } }).catch(() => undefined);
317
- }
318
- continue;
319
- }
320
- if (!resumable && failed) {
415
+ const hasResumable = tasks.some(task => task.documentType === documentType && (task.status === 'AWAITING_REVIEW' || ['QUEUED', 'EXTRACTING', 'AI_PARSING', 'VALIDATING', 'PERSISTING'].includes(task.status)));
416
+ if (!hasResumable && failed) {
321
417
  if (documentType === 'PURCHASE_CONTRACT')
322
418
  setPurchaseImportTask(failed);
323
419
  else
324
420
  setCustomerImportState({ status: 'FAILED', importTaskId: failed.id, message: failed.errorMessage || '客户合同解析失败,请重新解析' });
325
421
  }
326
- if (!resumable && !failed && tasks.some(item => item.documentType === documentType && item.status === 'SUCCEEDED')) {
422
+ if (!hasResumable && !failed && tasks.some(item => item.documentType === documentType && item.status === 'SUCCEEDED')) {
327
423
  if (documentType === 'PURCHASE_CONTRACT') {
328
424
  setPurchaseImportTask(null);
329
425
  setPurchaseReviewDraft(null);
@@ -335,65 +431,173 @@ function App() {
335
431
  }, [data.importTasks.items, orchestrateContractImport]);
336
432
  const retryImport = React.useCallback(async (taskId) => {
337
433
  try {
338
- const start = asActionResult(unwrapResponse(await bridge.executeAction('retry_import_task', { taskId })));
434
+ const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState);
435
+ const start = asActionResult(unwrapResponse(raw));
339
436
  if (start.success === false)
340
- throw new Error(resolveActionMessage(start) || '重新解析失败');
341
- await orchestrateImport(start);
437
+ throw actionResponseError(start, '重新解析失败');
438
+ setToast({ message: resolveActionMessage(start) || '重新解析任务已提交', tone: 'info' });
342
439
  }
343
440
  catch (error) {
344
441
  if (!(error instanceof CatalogImportError))
345
442
  setToast({ message: error instanceof Error ? error.message : '重新解析失败', tone: 'error' });
346
443
  throw error;
347
444
  }
348
- }, [orchestrateImport]);
445
+ }, [loadTaskState]);
349
446
  const retryContractImport = React.useCallback(async (taskId) => {
350
- const start = asActionResult(unwrapResponse(await bridge.executeAction('retry_import_task', { taskId })));
351
- if (start.success === false)
352
- throw new Error(resolveActionMessage(start) || '重新解析失败');
353
- const documentType = start.data?.task.documentType;
354
- if (documentType === 'PURCHASE_CONTRACT' || documentType === 'CUSTOMER_CONTRACT')
355
- handledContractTasksRef.current[documentType] = null;
356
- await orchestrateContractImport(start);
357
- }, [orchestrateContractImport]);
447
+ try {
448
+ const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState);
449
+ const start = asActionResult(unwrapResponse(raw));
450
+ if (start.success === false)
451
+ throw actionResponseError(start, '重新解析失败');
452
+ setToast({ message: resolveActionMessage(start) || '重新解析任务已提交', tone: 'info' });
453
+ }
454
+ catch (error) {
455
+ setToast({ message: error instanceof Error ? error.message : '重新解析失败', tone: 'error' });
456
+ throw error;
457
+ }
458
+ }, [loadTaskState]);
459
+ const applyCustomerContractReanalysisResult = React.useCallback(async (result) => {
460
+ let review;
461
+ if (result.review) {
462
+ review = adaptCustomerContractReview(result.review, { reviewSource: 'EXISTING_CONTRACT' });
463
+ }
464
+ else if (result.task) {
465
+ review = adaptCustomerContractReview(result.task.resultPayload, { reviewSource: 'IMPORT_DRAFT', importTaskId: result.task.id, sourceFileId: result.task.sourceFileId ?? '', sourceFileName: result.task.sourceFileName ?? '' });
466
+ }
467
+ else {
468
+ throw new Error('重新分析结果缺少合同审核数据');
469
+ }
470
+ const attached = await attachCustomerSource(review, blobUrlOwnerRef.current);
471
+ setCustomerImportState({ status: 'AWAITING_REVIEW', importTaskId: result.task?.id ?? result.run.importTaskId ?? '', review: attached });
472
+ changeRouteRef.current({ type: 'page', page: 'customer-contract-review' });
473
+ return attached;
474
+ }, []);
475
+ const consumeCustomerContractReanalysisResult = React.useCallback(async (runId) => {
476
+ const consumed = asActionResult(unwrapResponse(await bridge.executeAction('consume_customer_contract_reanalysis_result', { runId })));
477
+ if (consumed.success === false)
478
+ throw new Error(resolveActionMessage(consumed) || '确认重新分析结果失败');
479
+ }, []);
358
480
  const reanalyzeCustomerContract = React.useCallback(async (input) => {
359
481
  setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析中' });
360
482
  try {
361
- const response = asActionResult(unwrapResponse(await bridge.executeAction('reanalyze_customer_contract', { reviewSource: input.reviewSource, ...(input.customerContractId ? { id: input.customerContractId } : {}), ...(input.importTaskId ? { importTaskId: input.importTaskId } : {}) })));
483
+ const raw = await submitAndRefreshTaskState(() => bridge.executeAction('reanalyze_customer_contract', { reviewSource: input.reviewSource, ...(input.customerContractId ? { id: input.customerContractId } : {}), ...(input.importTaskId ? { importTaskId: input.importTaskId } : {}) }), loadTaskState);
484
+ const response = asActionResult(unwrapResponse(raw));
362
485
  if (response.success === false || !response.data)
363
- throw new Error(resolveActionMessage(response) || 'AI 重新分析启动失败');
364
- const result = await runCustomerContractReanalysis({
486
+ throw actionResponseError(response, 'AI 重新分析启动失败');
487
+ setToast({ message: resolveActionMessage(response) || 'AI 重新分析任务已提交', tone: 'info' });
488
+ const running = reanalysisTaskTrackerRef.current.run(response.data.run.id, signal => runCustomerContractReanalysis({
365
489
  start: response.data,
366
- invokeClientCommand: (commandKey, payload) => bridge.invokeClientCommand(commandKey, payload),
367
- markCommandDispatched: async (runId) => {
368
- const marked = asActionResult(unwrapResponse(await bridge.executeAction('mark_customer_contract_reanalysis_dispatched', { runId })));
369
- if (marked.success === false)
370
- throw new Error(resolveActionMessage(marked) || '记录重新分析命令状态失败');
371
- },
372
- markFailed: async (runId, failure) => {
373
- const marked = asActionResult(unwrapResponse(await bridge.executeAction('mark_customer_contract_reanalysis_failed', { runId, ...failure })));
374
- if (marked.success === false)
375
- throw new Error(resolveActionMessage(marked) || '记录重新分析失败状态失败');
376
- },
377
- poll: async (runId) => asCustomerContractReanalysisResult(unwrapResponse(await bridge.requestData({ parameters: { collection: 'customerContractAnalysis', runId } })))
378
- });
379
- let review;
380
- if (result.review) {
381
- review = adaptCustomerContractReview(result.review, { reviewSource: 'EXISTING_CONTRACT' });
382
- }
383
- else if (result.task) {
384
- review = adaptCustomerContractReview(result.task.resultPayload, { reviewSource: 'IMPORT_DRAFT', importTaskId: result.task.id, sourceFileId: result.task.sourceFileId ?? '', sourceFileName: result.task.sourceFileName ?? '' });
385
- }
386
- else {
387
- throw new Error('重新分析结果缺少合同审核数据');
388
- }
490
+ signal,
491
+ poll: async (runId) => asCustomerContractReanalysisResult(unwrapResponse(await bridge.requestData({ parameters: { collection: 'customerContractAnalysis', runId } }))),
492
+ onPollError: error => setToast({ message: `任务状态查询失败:${error.message}`, tone: 'error' })
493
+ }));
494
+ if (!running)
495
+ throw new Error('该 AI 重新分析任务正在跟踪中');
496
+ const result = await running;
497
+ const review = await applyCustomerContractReanalysisResult(result);
498
+ await consumeCustomerContractReanalysisResult(result.run.id);
499
+ handledReanalysisResultsRef.current.add(result.run.id);
389
500
  setCustomerReanalysisState({ status: 'IDLE' });
501
+ setToast({ message: 'AI 重新分析完成', tone: 'success' });
502
+ await loadTaskState();
390
503
  return review;
391
504
  }
392
505
  catch (error) {
506
+ if (isAbortError(error))
507
+ throw error;
508
+ if (error instanceof ActionResponseError && error.code === 'IMPORT_QUEUE_UNAVAILABLE') {
509
+ setCustomerReanalysisState({ status: 'RUNNING', message: '任务已保存,等待队列服务恢复' });
510
+ setToast({ message: error.message, tone: 'info' });
511
+ scheduleTaskStateRecovery();
512
+ throw error;
513
+ }
514
+ if (isPollingObservationTimeout(error)) {
515
+ setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析仍在后台处理中' });
516
+ setToast({ message: '任务仍在后台处理中,将继续跟踪', tone: 'info' });
517
+ scheduleTaskStateRecovery();
518
+ throw error;
519
+ }
393
520
  setCustomerReanalysisState({ status: 'FAILED', message: error instanceof Error ? error.message : 'AI 重新分析失败' });
521
+ setToast({ message: error instanceof Error ? error.message : 'AI 重新分析失败', tone: 'error' });
394
522
  throw error;
395
523
  }
396
- }, []);
524
+ }, [applyCustomerContractReanalysisResult, consumeCustomerContractReanalysisResult, loadTaskState, scheduleTaskStateRecovery]);
525
+ React.useEffect(() => {
526
+ for (const recovery of data.customerContractAnalyses.items) {
527
+ const recoverable = recovery.recoveryType === 'ACTIVE_POLL' || recovery.recoveryType === 'SUCCEEDED_RESULT';
528
+ if (!recoverable)
529
+ continue;
530
+ if (handledReanalysisResultsRef.current.has(recovery.run.id))
531
+ continue;
532
+ const running = reanalysisTaskTrackerRef.current.run(recovery.run.id, signal => runCustomerContractReanalysis({
533
+ start: { run: recovery.run },
534
+ signal,
535
+ poll: async (runId) => asCustomerContractReanalysisResult(unwrapResponse(await bridge.requestData({ parameters: { collection: 'customerContractAnalysis', runId } }))),
536
+ onPollError: error => setToast({ message: `任务状态查询失败:${error.message}`, tone: 'error' })
537
+ }));
538
+ if (!running)
539
+ continue;
540
+ setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析中' });
541
+ setToast({ message: '已恢复 AI 重新分析任务', tone: 'info' });
542
+ void running.then(result => {
543
+ enqueuePendingReview({ kind: 'REANALYSIS', result });
544
+ }).catch(error => {
545
+ if (isAbortError(error))
546
+ return;
547
+ if (isPollingObservationTimeout(error)) {
548
+ setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析仍在后台处理中' });
549
+ setToast({ message: '任务仍在后台处理中,将继续跟踪', tone: 'info' });
550
+ scheduleTaskStateRecovery();
551
+ return;
552
+ }
553
+ setCustomerReanalysisState({ status: 'FAILED', message: error instanceof Error ? error.message : 'AI 重新分析失败' });
554
+ setToast({ message: error instanceof Error ? error.message : 'AI 重新分析失败', tone: 'error' });
555
+ });
556
+ }
557
+ }, [data.customerContractAnalyses.items, enqueuePendingReview, scheduleTaskStateRecovery]);
558
+ const presentNextPendingReview = React.useCallback(async () => {
559
+ if (activeReviewResultRef.current)
560
+ return;
561
+ const pending = pendingReviewResultsRef.current.peek();
562
+ if (!pending)
563
+ return;
564
+ const resultId = pending.kind === 'CONTRACT_IMPORT' ? pending.completion.task.id : pending.result.run.id;
565
+ activeReviewResultRef.current = resultId;
566
+ try {
567
+ if (pending.kind === 'CONTRACT_IMPORT') {
568
+ const task = pending.completion.task;
569
+ if (task.documentType === 'PURCHASE_CONTRACT') {
570
+ blobUrlOwnerRef.current.clear('purchase');
571
+ const draft = adaptPurchaseReview(task);
572
+ setPurchaseImportTask(task);
573
+ setPurchaseReviewDraft(await attachPurchaseSource(draft, task, blobUrlOwnerRef.current));
574
+ changeRouteRef.current({ type: 'procurement', level: 'suppliers' });
575
+ }
576
+ else {
577
+ blobUrlOwnerRef.current.clear('customer');
578
+ const review = adaptCustomerContractReview(task.resultPayload, { reviewSource: 'IMPORT_DRAFT', importTaskId: task.id, sourceFileId: task.sourceFileId ?? '', sourceFileName: task.sourceFileName ?? '' });
579
+ setCustomerImportState({ status: 'AWAITING_REVIEW', importTaskId: task.id, review: await attachCustomerSource(review, blobUrlOwnerRef.current) });
580
+ changeRouteRef.current({ type: 'page', page: 'customer-contract-review' });
581
+ }
582
+ pendingReviewResultsRef.current.ack(resultId);
583
+ handledContractTasksRef.current.add(resultId);
584
+ return;
585
+ }
586
+ await applyCustomerContractReanalysisResult(pending.result);
587
+ await consumeCustomerContractReanalysisResult(pending.result.run.id);
588
+ pendingReviewResultsRef.current.ack(resultId);
589
+ handledReanalysisResultsRef.current.add(pending.result.run.id);
590
+ setCustomerReanalysisState({ status: 'IDLE' });
591
+ setToast({ message: 'AI 重新分析完成', tone: 'success' });
592
+ await loadTaskState();
593
+ }
594
+ catch (error) {
595
+ activeReviewResultRef.current = null;
596
+ setToast({ message: error instanceof Error ? error.message : '审核结果恢复失败', tone: 'error' });
597
+ scheduleTaskStateRecovery();
598
+ }
599
+ }, [applyCustomerContractReanalysisResult, consumeCustomerContractReanalysisResult, loadTaskState, scheduleTaskStateRecovery]);
600
+ presentNextReviewRef.current = () => { void presentNextPendingReview(); };
397
601
  const loadCustomerContractReview = React.useCallback(async (customerContractId) => {
398
602
  const result = asActionResult(unwrapResponse(await bridge.executeAction('get_customer_contract_review', { id: customerContractId })));
399
603
  if (result.success === false || !result.data)
@@ -405,21 +609,27 @@ function App() {
405
609
  const closePurchaseReview = React.useCallback(() => {
406
610
  blobUrlOwnerRef.current.clear('purchase');
407
611
  setPurchaseReviewDraft(null);
408
- }, []);
612
+ activeReviewResultRef.current = null;
613
+ schedulePendingReviewPresentation();
614
+ }, [schedulePendingReviewPresentation]);
409
615
  const closeCustomerReview = React.useCallback(() => {
410
616
  blobUrlOwnerRef.current.clear('customer');
411
617
  setCustomerImportState({ status: 'IDLE' });
412
- }, []);
618
+ activeReviewResultRef.current = null;
619
+ schedulePendingReviewPresentation();
620
+ }, [schedulePendingReviewPresentation]);
413
621
  const queryImportIssues = React.useCallback(async (query) => {
414
- const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: query.pageSize, parameters: { collection: 'importIssues', documentType: query.documentType, batchId: query.batchId, issueType: query.issueType } })));
415
- return { items: Array.isArray(payload.items) ? payload.items : [], total: numberValue(payload.total, 0), page: numberValue(payload.page, query.page), pageSize: numberValue(payload.pageSize, query.pageSize) };
622
+ const requestedPageSize = resolvePaginationPageSize(query.pageSize);
623
+ const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: requestedPageSize, parameters: { collection: 'importIssues', documentType: query.documentType, batchId: query.batchId, issueType: query.issueType } })));
624
+ return { items: Array.isArray(payload.items) ? payload.items : [], total: numberValue(payload.total, 0), page: numberValue(payload.page, query.page), pageSize: requestedPageSize };
416
625
  }, []);
417
626
  const queryCatalog = React.useCallback(async (collection, query) => {
627
+ const requestedPageSize = resolvePaginationPageSize(query.pageSize);
418
628
  const gate = requestGateRef.current;
419
629
  const requestId = gate.begin();
420
630
  setBusy(true);
421
631
  try {
422
- const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: query.pageSize, parameters: { collection, filters: encodeViewFilters(query.filters) } })));
632
+ const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: requestedPageSize, parameters: { collection, filters: encodeViewFilters(query.filters) } })));
423
633
  if (!gate.isLatest(requestId))
424
634
  return;
425
635
  setData((current) => ({
@@ -429,7 +639,7 @@ function App() {
429
639
  items: Array.isArray(payload.items) ? payload.items : [],
430
640
  total: numberValue(payload.total, 0),
431
641
  page: numberValue(payload.page, query.page),
432
- pageSize: numberValue(payload.pageSize, query.pageSize)
642
+ pageSize: requestedPageSize
433
643
  }
434
644
  }));
435
645
  }
@@ -442,6 +652,27 @@ function App() {
442
652
  setBusy(gate.activeCount() > 0);
443
653
  }
444
654
  }, []);
655
+ const queryMonthlyRates = React.useCallback(async (query) => {
656
+ const requestedPageSize = resolvePaginationPageSize(query.pageSize);
657
+ const gate = settingsRatesRequestGateRef.current;
658
+ const requestId = gate.begin();
659
+ setBusy(true);
660
+ try {
661
+ const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: requestedPageSize, parameters: { collection: 'monthlyRates' } })));
662
+ if (!gate.commit(requestId))
663
+ return;
664
+ settingsRatesQueryRef.current = { page: numberValue(payload.page, query.page), pageSize: requestedPageSize };
665
+ setData(current => ({ ...current, settings: normalizeWorkbenchData({ summary: { settings: { ...current.settings, rates: { ...payload, pageSize: requestedPageSize } } } }).settings }));
666
+ }
667
+ catch (error) {
668
+ if (gate.isLatest(requestId))
669
+ setToast({ message: error instanceof Error ? `月度汇率加载失败:${error.message}` : '月度汇率加载失败', tone: 'error' });
670
+ }
671
+ finally {
672
+ gate.finish(requestId);
673
+ setBusy(gate.activeCount() + requestGateRef.current.activeCount() + routeRequestGateRef.current.activeCount() > 0);
674
+ }
675
+ }, []);
445
676
  const queryProcurement = React.useCallback(async (_collection, query) => {
446
677
  await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
447
678
  }, [loadRoute]);
@@ -452,20 +683,36 @@ function App() {
452
683
  await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
453
684
  }, [loadRoute]);
454
685
  const querySalesFiles = React.useCallback(async (collection, query) => {
686
+ const requestedPageSize = resolvePaginationPageSize(query.pageSize);
455
687
  if (collection === 'salesFileOrders') {
456
- await loadRoute({ type: 'page', page: 'sales-files' }, query.page, query.pageSize, query.filters);
688
+ await loadRoute({ type: 'page', page: 'sales-files' }, query.page, requestedPageSize, query.filters);
457
689
  return;
458
690
  }
459
- const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: query.pageSize, parameters: { collection, ...query.parameters, filters: encodeViewFilters(query.filters) } })));
460
- setData(current => ({ ...current, generatedFiles: { ...payload, items: Array.isArray(payload.items) ? payload.items : [], total: numberValue(payload.total, 0), page: numberValue(payload.page, query.page), pageSize: numberValue(payload.pageSize, query.pageSize) } }));
691
+ const key = generatedFileHistoryKey(query.parameters);
692
+ if (!key) {
693
+ setToast({ message: '历史生成文件查询缺少合同或采购订单标识', tone: 'error' });
694
+ return;
695
+ }
696
+ activeGeneratedFilesKeyRef.current = key;
697
+ await runKeyedLatestRequest({
698
+ gate: generatedFilesRequestGateRef.current,
699
+ key,
700
+ request: async () => asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: requestedPageSize, parameters: { collection, ...query.parameters, filters: encodeViewFilters(query.filters) } }))),
701
+ isCurrent: () => activeGeneratedFilesKeyRef.current === key,
702
+ commit: payload => setData(current => ({ ...current, generatedFiles: { ...payload, items: Array.isArray(payload.items) ? payload.items : [], total: numberValue(payload.total, 0), page: numberValue(payload.page, query.page), pageSize: requestedPageSize } })),
703
+ onError: error => setToast({ message: error instanceof Error ? `历史生成文件加载失败:${error.message}` : '历史生成文件加载失败', tone: 'error' }),
704
+ onStart: () => setBusy(true),
705
+ onFinish: () => setBusy(generatedFilesRequestGateRef.current.activeCount() + requestGateRef.current.activeCount() + routeRequestGateRef.current.activeCount() + analyticsRequestsRef.current.activeCount() > 0)
706
+ });
461
707
  }, [loadRoute]);
462
708
  const queryAnalytics = React.useCallback(async (query) => {
709
+ const requestedQuery = { ...query, pageSize: resolvePaginationPageSize(query.pageSize) };
463
710
  const requests = analyticsRequestsRef.current;
464
- const requestId = requests.begin(query);
711
+ const requestId = requests.begin(requestedQuery);
465
712
  setBusy(true);
466
713
  setAnalyticsError(null);
467
714
  try {
468
- const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: query.pageSize, parameters: { collection: 'analytics', mode: query.mode, dimension: query.dimension, filters: encodeViewFilters(query.filters) } })));
715
+ const payload = asRecord(unwrapResponse(await bridge.requestData({ page: requestedQuery.page, pageSize: requestedQuery.pageSize, parameters: { collection: 'analytics', mode: requestedQuery.mode, dimension: requestedQuery.dimension, filters: encodeViewFilters(requestedQuery.filters) } })));
469
716
  if (!requests.commit(requestId))
470
717
  return;
471
718
  setAnalyticsCommittedQuery(requests.committed());
@@ -509,25 +756,26 @@ function App() {
509
756
  const result = asActionResult(unwrapResponse(await bridge.executeAction('save_settings', request)));
510
757
  if (result.success === false)
511
758
  throw new Error(resolveActionMessage(result) || '设置保存失败');
512
- await loadRoute({ type: 'page', page: 'settings' });
759
+ await loadRoute({ type: 'page', page: 'settings' }, settingsRatesQueryRef.current.page, settingsRatesQueryRef.current.pageSize);
513
760
  }, [loadRoute]);
514
761
  const restoreSettings = React.useCallback(async (request) => {
515
762
  const result = asActionResult(unwrapResponse(await bridge.executeAction('restore_settings_default', request)));
516
763
  if (result.success === false)
517
764
  throw new Error(resolveActionMessage(result) || '恢复系统默认失败');
518
- await loadRoute({ type: 'page', page: 'settings' });
765
+ await loadRoute({ type: 'page', page: 'settings' }, settingsRatesQueryRef.current.page, settingsRatesQueryRef.current.pageSize);
519
766
  }, [loadRoute]);
520
767
  const deleteSettingsRate = React.useCallback(async (request) => {
521
768
  const result = asActionResult(unwrapResponse(await bridge.executeAction('delete_settings_rate', request)));
522
769
  if (result.success === false)
523
770
  throw new Error(resolveActionMessage(result) || '月度汇率删除失败');
524
- await loadRoute({ type: 'page', page: 'settings' });
525
- }, [loadRoute]);
771
+ const { page, pageSize } = settingsRatesQueryRef.current;
772
+ const clampedPage = clampPageAfterSingleDeletion(page, pageSize, data.settings.rates.total);
773
+ await loadRoute({ type: 'page', page: 'settings' }, clampedPage, pageSize);
774
+ }, [data.settings.rates.total, loadRoute]);
526
775
  const uploadCompanyLogo = React.useCallback(async (file) => {
527
776
  const result = asActionResult(unwrapResponse(await bridge.executeFileAction('upload_company_logo', file, {})));
528
777
  if (result.success === false || !result.data?.logoFileId)
529
778
  throw new Error(resolveActionMessage(result) || '公司 Logo 上传失败');
530
- setToast({ message: '公司 Logo 上传成功', tone: 'success' });
531
779
  return result.data.logoFileId;
532
780
  }, []);
533
781
  const discardCompanyLogo = React.useCallback(async (logoFileId) => {
@@ -540,6 +788,13 @@ function App() {
540
788
  if (order)
541
789
  changeRouteRef.current({ type: 'page', page: 'sales-files' });
542
790
  }, []);
791
+ const selectSalesFileOrder = React.useCallback((order) => {
792
+ const previousKey = activeGeneratedFilesKeyRef.current;
793
+ if (previousKey)
794
+ generatedFilesRequestGateRef.current.invalidate(previousKey);
795
+ activeGeneratedFilesKeyRef.current = order ? generatedFileHistoryKey(order) : null;
796
+ setActiveSalesFileOrder(order);
797
+ }, []);
543
798
  const changeRoute = React.useCallback((nextRoute) => {
544
799
  routeRef.current = nextRoute;
545
800
  setRoute(nextRoute);
@@ -553,7 +808,7 @@ function App() {
553
808
  : page === 'customer-contract-review'
554
809
  ? _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u5BA2\u6237\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_customer_contract', file, { documentType: 'CUSTOMER_CONTRACT' }) })
555
810
  : undefined;
556
- return (_jsxs(_Fragment, { children: [_jsxs(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, subtitle: meta.subtitle, commands: (page === 'controlled-goods' || page === 'sanctioned-companies') ? _jsx(CatalogCommands, { busy: busy || actionBusy, onUpload: () => setCatalogCommand('upload'), onCreate: () => setCatalogCommand('create') }) : contractCommands, busy: busy || actionBusy, onRouteChange: changeRoute, onRefresh: () => void load(), children: [page === 'home' && _jsx(HomePage, {}), page === 'controlled-goods' && _jsx(ControlledGoodsPage, { collection: data.controlledGoods, batches: data.controlledBatches.items, busy: busy || actionBusy, onAction: executeAction, onFileAction: executeFileAction, onRefresh: load, onQuery: (query) => queryCatalog('controlledGoods', query), onIssueQuery: queryImportIssues, importCompletion: importCompletion, importFailure: importFailures.CONTROLLED_CATALOG, onRetryImport: retryImport, command: catalogCommand, onCommandHandled: () => setCatalogCommand(null) }), page === 'sanctioned-companies' && _jsx(SanctionedCompaniesPage, { collection: data.sanctionedCompanies, batches: data.sanctionBatches.items, busy: busy || actionBusy, onAction: executeAction, onFileAction: executeFileAction, onRefresh: load, onQuery: (query) => queryCatalog('sanctionedCompanies', query), onIssueQuery: queryImportIssues, importCompletion: importCompletion, importFailure: importFailures.SANCTION_CATALOG, onRetryImport: retryImport, command: catalogCommand, onCommandHandled: () => setCatalogCommand(null) }), page === 'procurement' && route.type === 'procurement' && _jsx(ProcurementPage, { route: route, suppliers: data.suppliers, purchaseOrders: data.purchaseOrders, purchaseLines: data.purchaseLines, supplier: procurementContext.supplier, order: procurementContext.order, metrics: procurementContext.metrics, importTask: purchaseImportTask, reviewDraft: purchaseReviewDraft, busy: busy || actionBusy, onQuery: queryProcurement, onAction: executeAction, onFileAction: executeFileAction, onRouteChange: changeRoute, onRetryImport: retryContractImport, onReviewClose: closePurchaseReview }), page === 'customer-contract-review' && _jsx(CustomerContractsPage, { collection: data.customerContracts, busy: busy || actionBusy, importReviewState: customerImportState, onQuery: queryCustomerContracts, onAction: executeAction, onFileAction: executeFileAction, onReanalyze: reanalyzeCustomerContract, onLoadReview: loadCustomerContractReview, onReviewClose: closeCustomerReview, reanalysisState: customerReanalysisState, onRetryImport: retryContractImport }), page === 'sales' && route.type === 'sales' && _jsx(SalesPage, { route: route, customers: data.salesCustomers, orders: data.salesOrders, lines: data.salesLines, busy: busy || actionBusy, onQuery: querySales, onAction: executeAction, onRouteChange: changeRoute, onOpenFiles: openSalesFiles }), page === 'sales-files' && _jsx(SalesFilesPage, { orders: data.salesFileOrders, history: data.generatedFiles, activeOrder: activeSalesFileOrder, banks: data.settings.banks, busy: busy || actionBusy, onQuery: querySalesFiles, onAction: executeSalesFileAction, onOpenSalesOrder: setActiveSalesFileOrder, onDownload: async (file) => { const result = await executeAction('download_sales_file', { id: file.id }); downloadActionFile(result.data); }, onEdit: async (file) => setEditingSalesFile(file) }), page === 'analytics' && _jsx(AnalyticsPage, { data: data.analytics, committedQuery: analyticsCommittedQuery, busy: busy || actionBusy, error: analyticsError, onQuery: queryAnalytics, onExportXlsx: exportAnalytics }), page === 'settings' && _jsx(SettingsPage, { data: data.settings, busy: busy || actionBusy, onSave: saveSettings, onRestoreDefault: restoreSettings, onDeleteRate: deleteSettingsRate, onUploadLogo: uploadCompanyLogo, onDiscardLogo: discardCompanyLogo }), !['home', 'controlled-goods', 'sanctioned-companies', 'procurement', 'customer-contract-review', 'sales', 'sales-files', 'analytics', 'settings'].includes(page) && _jsxs("section", { className: "tcw-page-placeholder", "data-page": page, children: [_jsx(InfoNotice, { children: "\u9875\u9762\u57FA\u7840\u7ED3\u6784\u5DF2\u5C31\u7EEA\uFF0C\u4E1A\u52A1\u5185\u5BB9\u5C06\u5728\u5BF9\u5E94\u9875\u9762\u6A21\u5757\u4E2D\u52A0\u8F7D\u3002" }), _jsx("div", { className: "tcw-placeholder-body", "aria-label": `${meta.title}页面内容` })] })] }), _jsx(LoadingBlocker, { visible: busy || actionBusy, label: actionBusy ? '操作处理中' : '数据加载中' }), toast && _jsx(Toast, { message: toast.message, tone: toast.tone, onClose: () => setToast(null) }), editingSalesFile && _jsx(SalesFileEditor, { file: editingSalesFile, onClose: () => setEditingSalesFile(null), onSaved: () => { setEditingSalesFile(null); if (activeSalesFileOrder)
811
+ return (_jsxs(_Fragment, { children: [_jsxs(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, subtitle: meta.subtitle, commands: (page === 'controlled-goods' || page === 'sanctioned-companies') ? _jsx(CatalogCommands, { busy: busy || actionBusy, onUpload: () => setCatalogCommand('upload'), onCreate: () => setCatalogCommand('create') }) : contractCommands, busy: busy || actionBusy, onRouteChange: changeRoute, onRefresh: () => void load(), children: [page === 'home' && _jsx(HomePage, {}), page === 'controlled-goods' && _jsx(ControlledGoodsPage, { collection: data.controlledGoods, batches: data.controlledBatches.items, busy: busy || actionBusy, onAction: executeAction, onFileAction: executeCatalogFileAction, onRefresh: load, onQuery: (query) => queryCatalog('controlledGoods', query), onIssueQuery: queryImportIssues, importCompletion: importCompletions.CONTROLLED_CATALOG, importFailure: importFailures.CONTROLLED_CATALOG, onRetryImport: retryImport, command: catalogCommand, onCommandHandled: () => setCatalogCommand(null), onNotify: setToast }), page === 'sanctioned-companies' && _jsx(SanctionedCompaniesPage, { collection: data.sanctionedCompanies, batches: data.sanctionBatches.items, busy: busy || actionBusy, onAction: executeAction, onFileAction: executeCatalogFileAction, onRefresh: load, onQuery: (query) => queryCatalog('sanctionedCompanies', query), onIssueQuery: queryImportIssues, importCompletion: importCompletions.SANCTION_CATALOG, importFailure: importFailures.SANCTION_CATALOG, onRetryImport: retryImport, command: catalogCommand, onCommandHandled: () => setCatalogCommand(null), onNotify: setToast }), page === 'procurement' && route.type === 'procurement' && _jsx(ProcurementPage, { route: route, suppliers: data.suppliers, purchaseOrders: data.purchaseOrders, purchaseLines: data.purchaseLines, supplier: procurementContext.supplier, order: procurementContext.order, metrics: procurementContext.metrics, importTask: purchaseImportTask, reviewDraft: purchaseReviewDraft, busy: busy || actionBusy, onQuery: queryProcurement, onAction: executeAction, onFileAction: executeFileAction, onRouteChange: changeRoute, onRetryImport: retryContractImport, onReviewClose: closePurchaseReview, onNotify: setToast }), page === 'customer-contract-review' && _jsx(CustomerContractsPage, { collection: data.customerContracts, busy: busy || actionBusy, importReviewState: customerImportState, onQuery: queryCustomerContracts, onAction: executeAction, onFileAction: executeFileAction, onReanalyze: reanalyzeCustomerContract, onLoadReview: loadCustomerContractReview, onReviewClose: closeCustomerReview, reanalysisState: customerReanalysisState, onRetryImport: retryContractImport, onNotify: setToast }), page === 'sales' && route.type === 'sales' && _jsx(SalesPage, { route: route, customers: data.salesCustomers, orders: data.salesOrders, lines: data.salesLines, busy: busy || actionBusy, onQuery: querySales, onAction: executeAction, onRouteChange: changeRoute, onOpenFiles: openSalesFiles }), page === 'sales-files' && _jsx(SalesFilesPage, { orders: data.salesFileOrders, history: data.generatedFiles, activeOrder: activeSalesFileOrder, banks: data.settings.banks, busy: busy || actionBusy, onQuery: querySalesFiles, onAction: executeSalesFileAction, onOpenSalesOrder: selectSalesFileOrder, onDownload: file => executeAction('download_sales_file', { id: file.id }).then(() => undefined), onEdit: async (file) => setEditingSalesFile(file), onNotify: setToast }), page === 'analytics' && _jsx(AnalyticsPage, { data: data.analytics, committedQuery: analyticsCommittedQuery, busy: busy || actionBusy, error: analyticsError, onQuery: queryAnalytics, onExportXlsx: exportAnalytics }), page === 'settings' && _jsx(SettingsPage, { data: data.settings, busy: busy || actionBusy, onSave: saveSettings, onRestoreDefault: restoreSettings, onDeleteRate: deleteSettingsRate, onRateQuery: queryMonthlyRates, onUploadLogo: uploadCompanyLogo, onDiscardLogo: discardCompanyLogo, onNotify: setToast }), !['home', 'controlled-goods', 'sanctioned-companies', 'procurement', 'customer-contract-review', 'sales', 'sales-files', 'analytics', 'settings'].includes(page) && _jsxs("section", { className: "tcw-page-placeholder", "data-page": page, children: [_jsx(InfoNotice, { children: "\u9875\u9762\u57FA\u7840\u7ED3\u6784\u5DF2\u5C31\u7EEA\uFF0C\u4E1A\u52A1\u5185\u5BB9\u5C06\u5728\u5BF9\u5E94\u9875\u9762\u6A21\u5757\u4E2D\u52A0\u8F7D\u3002" }), _jsx("div", { className: "tcw-placeholder-body", "aria-label": `${meta.title}页面内容` })] })] }), _jsx(LoadingBlocker, { visible: busy || actionBusy, label: actionBusy ? '操作处理中' : '数据加载中' }), _jsx(ToastStack, { items: toasts, onClose: closeToast }), editingSalesFile && _jsx(SalesFileEditor, { file: editingSalesFile, onNotify: setToast, onClose: () => setEditingSalesFile(null), onSaved: () => { setEditingSalesFile(null); if (activeSalesFileOrder)
557
812
  void querySalesFiles('generatedFiles', { page: 1, pageSize: 20, filters: {}, parameters: { customerContractId: activeSalesFileOrder.customerContractId, purchaseOrderId: activeSalesFileOrder.purchaseOrderId } }); } })] }));
558
813
  }
559
814
  function routePage(route) {
@@ -564,6 +819,7 @@ function routePage(route) {
564
819
  return route.page;
565
820
  }
566
821
  function analyticsQueryFromRoute(page, pageSize, values) {
822
+ const requestedPageSize = resolvePaginationPageSize(pageSize);
567
823
  const dateValue = values.orderDate;
568
824
  const date = dateValue && typeof dateValue === 'object' && !Array.isArray(dateValue) ? dateValue : null;
569
825
  const orderDate = date ? { from: optionalText(date.from), to: optionalText(date.to) } : undefined;
@@ -574,11 +830,11 @@ function analyticsQueryFromRoute(page, pageSize, values) {
574
830
  };
575
831
  if (values.mode === 'SALES')
576
832
  return {
577
- mode: 'SALES', dimension: values.dimension === 'PRODUCT' ? 'PRODUCT' : 'CUSTOMER', page, pageSize,
833
+ mode: 'SALES', dimension: values.dimension === 'PRODUCT' ? 'PRODUCT' : 'CUSTOMER', page, pageSize: requestedPageSize,
578
834
  filters: { ...common, ...(optionalText(values.customer) ? { customer: optionalText(values.customer) } : {}), ...(enabled(values.unreceivedOnly) ? { unreceivedOnly: true } : {}) }
579
835
  };
580
836
  return {
581
- mode: 'PURCHASE', dimension: values.dimension === 'PRODUCT' ? 'PRODUCT' : 'SUPPLIER', page, pageSize,
837
+ mode: 'PURCHASE', dimension: values.dimension === 'PRODUCT' ? 'PRODUCT' : 'SUPPLIER', page, pageSize: requestedPageSize,
582
838
  filters: { ...common, ...(optionalText(values.supplier) ? { supplier: optionalText(values.supplier) } : {}), ...(enabled(values.unpaidOnly) ? { unpaidOnly: true } : {}), ...(enabled(values.unrefundedOnly) ? { unrefundedOnly: true } : {}) }
583
839
  };
584
840
  }
@@ -596,6 +852,9 @@ function resolveActionMessage(result) {
596
852
  }
597
853
  return '';
598
854
  }
855
+ function actionResponseError(result, fallback) {
856
+ return new ActionResponseError(resolveActionMessage(result) || fallback, result.error?.code);
857
+ }
599
858
  function asRecord(value) {
600
859
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
601
860
  }
@@ -694,9 +953,6 @@ function asCustomerContractReanalysisResult(value) {
694
953
  throw new Error('轮询响应缺少重新分析任务');
695
954
  return payload;
696
955
  }
697
- function shouldRefreshAfterAction(actionKey) {
698
- return new Set(['save_controlled_goods', 'update_controlled_goods', 'save_sanctioned_company', 'update_sanctioned_company', 'save_purchase_review', 'save_customer_contract_review', 'update_supplier', 'update_purchase_order', 'add_purchase_line', 'update_purchase_line', 'update_customer_contract_review', 'delete_entity']).has(actionKey);
699
- }
700
956
  async function ensureExcelEditorRuntime() {
701
957
  const runtimeWindow = window;
702
958
  if (runtimeWindow.TradeComplianceExcelEditor?.open)
@@ -737,7 +993,7 @@ async function ensureExcelEditorRuntime() {
737
993
  });
738
994
  return excelEditorRuntimePromise;
739
995
  }
740
- function SalesFileEditor({ file, onClose, onSaved }) {
996
+ function SalesFileEditor({ file, onClose, onSaved, onNotify }) {
741
997
  const containerRef = React.useRef(null);
742
998
  const editorRef = React.useRef(null);
743
999
  const [error, setError] = React.useState('');
@@ -778,12 +1034,13 @@ function SalesFileEditor({ file, onClose, onSaved }) {
778
1034
  return () => { cancelled = true; editorRef.current?.dispose(); editorRef.current = null; };
779
1035
  }, [file.id]);
780
1036
  const save = async () => { if (!editorRef.current)
781
- return; setSaving(true); setError(''); try {
1037
+ return; setSaving(true); try {
782
1038
  await editorRef.current.save();
1039
+ onNotify({ message: '工作簿保存成功', tone: 'success' });
783
1040
  onSaved();
784
1041
  }
785
1042
  catch (cause) {
786
- setError(cause instanceof Error ? cause.message : '工作簿保存失败');
1043
+ onNotify({ message: cause instanceof Error ? cause.message : '工作簿保存失败', tone: 'error' });
787
1044
  }
788
1045
  finally {
789
1046
  setSaving(false);