@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,7 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import React from 'react';
3
3
  import { DataGrid, FilterPanel, InfoNotice } from '../components/data-grid.js';
4
4
  import { ConfirmDialog, FieldGrid, FormField, Modal, ReviewSplit, UploadZone } from '../components/forms.js';
5
+ import { isPaginationPageSize } from '../contracts.js';
5
6
  import { getConfiguredUploadPolicy } from '../upload-policy.js';
7
+ import { formatBeijingDateTime, formatBusinessDate } from '../date-time.js';
8
+ import './catalogs.css';
9
+ import { createLatestRequestGate } from '../model.js';
10
+ import { abortablePollingDelay, createPollingObservation, pollWithRetry } from '../polling.js';
11
+ import { runNotifiedOperation } from '../toast-queue.js';
6
12
  const PAGE_SIZE = 20;
7
13
  const ACCEPTED_CATALOG_FILES = '.pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.webp';
8
14
  const formatNotice = () => `支持 PDF、DOC、DOCX、XLS、XLSX、PNG、JPEG、WEBP,单文件最大 ${getConfiguredUploadPolicy().maxUploadMb} MB。`;
@@ -44,7 +50,6 @@ export function createCatalogActionApi({ onAction, onFileAction, onRefresh }) {
44
50
  const mutate = async (actionKey, input) => {
45
51
  const response = await onAction(actionKey, input);
46
52
  assertActionSucceeded(response);
47
- await onRefresh();
48
53
  return response;
49
54
  };
50
55
  const mutateFile = async (actionKey, file, input) => {
@@ -79,26 +84,22 @@ export class CatalogImportError extends Error {
79
84
  this.name = 'CatalogImportError';
80
85
  }
81
86
  }
82
- export async function runCatalogImportOrchestration({ start, invokeClientCommand, markCommandDispatched, pollTask, refresh, wait = delay, now = Date.now }) {
87
+ export async function runCatalogImportOrchestration({ start, pollTask, refresh, wait = abortablePollingDelay, now = Date.now, intervalMs = 2000, timeoutMs = MAX_IMPORT_TIMEOUT_MS, onTransition, onPollError, signal }) {
83
88
  assertActionSucceeded(start);
84
89
  if (!isCatalogImportStartData(start.data))
85
90
  throw new Error('上传响应缺少解析任务信息');
86
- if (!start.data.task.commandDispatchedAt) {
87
- let marked = false;
88
- for (const command of start.data.commands) {
89
- await invokeClientCommand(command.commandKey, command.payload);
90
- if (!marked) {
91
- await markCommandDispatched(start.data.task.id);
92
- marked = true;
93
- }
94
- }
95
- }
96
- const timeoutMs = Math.min(MAX_IMPORT_TIMEOUT_MS, Math.max(1, start.data.polling.timeoutMs));
97
- const intervalMs = Math.max(1, start.data.polling.intervalMs);
98
- const deadline = now() + timeoutMs;
99
- while (now() < deadline) {
100
- await wait(intervalMs);
101
- const completion = await pollTask(start.data.task.id);
91
+ const observation = createPollingObservation({ now, timeoutMs, maxTimeoutMs: MAX_IMPORT_TIMEOUT_MS });
92
+ intervalMs = Math.max(1, intervalMs);
93
+ let previous = start.data.task;
94
+ const taskId = previous.id;
95
+ while (observation.active()) {
96
+ await wait(intervalMs, signal);
97
+ const attempt = await pollWithRetry(requestSignal => requestSignal ? pollTask(taskId, requestSignal) : pollTask(taskId), onPollError, signal);
98
+ if (!attempt.ok)
99
+ continue;
100
+ const completion = attempt.value;
101
+ onTransition?.(previous, completion.task);
102
+ previous = completion.task;
102
103
  const status = completion.task.status;
103
104
  if (status === 'SUCCEEDED') {
104
105
  if (!completion.batch)
@@ -111,13 +112,16 @@ export async function runCatalogImportOrchestration({ start, invokeClientCommand
111
112
  if (!ACTIVE_IMPORT_STATUSES.has(status))
112
113
  throw new Error(`不支持的导入任务状态:${status}`);
113
114
  }
114
- throw new CatalogImportError('目录解析超过 20 分钟,请重新解析', { ...start.data.task, status: 'TIMED_OUT', errorMessage: '目录解析超过 20 分钟,请重新解析' });
115
+ throw observation.expired('目录任务观察窗口已结束', previous);
115
116
  }
116
117
  export function findActiveCatalogImportTask(items) {
117
118
  return items
118
- .filter((task) => (task.documentType === 'CONTROLLED_CATALOG' || task.documentType === 'SANCTION_CATALOG') && ACTIVE_IMPORT_STATUSES.has(task.status) && Boolean(task.executionCommand))
119
+ .filter((task) => (task.documentType === 'CONTROLLED_CATALOG' || task.documentType === 'SANCTION_CATALOG') && ACTIVE_IMPORT_STATUSES.has(task.status))
119
120
  .sort((left, right) => String(right.updatedAt ?? '').localeCompare(String(left.updatedAt ?? '')))[0] ?? null;
120
121
  }
122
+ export function findActiveCatalogImportTasks(items) {
123
+ return items.filter((task) => (task.documentType === 'CONTROLLED_CATALOG' || task.documentType === 'SANCTION_CATALOG') && ACTIVE_IMPORT_STATUSES.has(task.status));
124
+ }
121
125
  export function findLatestFailedCatalogImportTask(items, documentType) {
122
126
  const retriedTaskIds = new Set(items.map((task) => task.retryOfTaskId).filter((id) => Boolean(id)));
123
127
  return items
@@ -128,17 +132,19 @@ export function createCatalogQueryScheduler(query, debounceMs = 300) {
128
132
  let timer = null;
129
133
  const cancel = () => { if (timer)
130
134
  clearTimeout(timer); timer = null; };
131
- const flush = (filters) => { cancel(); query(filters); };
135
+ const run = (page, filters, pageSize = PAGE_SIZE) => { cancel(); query(page, filters, pageSize); };
132
136
  return {
133
- schedule(type, filters) {
137
+ schedule(type, filters, pageSize = PAGE_SIZE) {
134
138
  cancel();
135
139
  if (type !== 'text') {
136
- query(filters);
140
+ run(1, filters, pageSize);
137
141
  return;
138
142
  }
139
- timer = setTimeout(() => { timer = null; query(filters); }, debounceMs);
143
+ timer = setTimeout(() => { timer = null; query(1, filters, pageSize); }, debounceMs);
140
144
  },
141
- flush,
145
+ flush: (filters, pageSize = PAGE_SIZE) => run(1, filters, pageSize),
146
+ page: (page, filters, pageSize = PAGE_SIZE) => run(page, filters, pageSize),
147
+ pageSize: (pageSize, filters) => run(1, filters, pageSize),
142
148
  cancel
143
149
  };
144
150
  }
@@ -216,41 +222,47 @@ export function ControlledGoodsPage(props) {
216
222
  if (props.command)
217
223
  props.onCommandHandled?.();
218
224
  }, [props.command, props.onCommandHandled]);
219
- const query = React.useCallback((page, nextFilters) => props.onQuery({ page, pageSize: PAGE_SIZE, filters: nextFilters }), [props.onQuery]);
220
- const scheduler = React.useMemo(() => createCatalogQueryScheduler((nextFilters) => void query(1, nextFilters)), [query]);
225
+ const query = React.useCallback((page, nextFilters, pageSize) => props.onQuery({ page, pageSize, filters: nextFilters }), [props.onQuery]);
226
+ const scheduler = React.useMemo(() => createCatalogQueryScheduler((page, nextFilters, pageSize) => void query(page, nextFilters, pageSize)), [query]);
221
227
  React.useEffect(() => () => scheduler.cancel(), [scheduler]);
222
228
  React.useEffect(() => { issueControllerRef.current?.mount(); return () => issueControllerRef.current?.dispose(); }, []);
223
229
  const changeFilter = (key, value) => {
224
230
  const nextFilters = { ...filters, [key]: value };
225
231
  setFilters(nextFilters);
226
- scheduler.schedule(controlledGoodsFilters.find((definition) => definition.key === key)?.type ?? 'text', nextFilters);
232
+ scheduler.schedule(controlledGoodsFilters.find((definition) => definition.key === key)?.type ?? 'text', nextFilters, pagination.pageSize);
227
233
  };
228
- const queryIssues = async (batchId, issueType, page = 1) => {
234
+ const queryIssues = async (batchId, issueType, page = 1, pageSize = PAGE_SIZE) => {
229
235
  if (!props.onIssueQuery)
230
236
  return;
231
- await issueControllerRef.current?.load(props.onIssueQuery, { documentType: 'CONTROLLED_CATALOG', batchId, issueType, page, pageSize: PAGE_SIZE });
237
+ await issueControllerRef.current?.load(props.onIssueQuery, { documentType: 'CONTROLLED_CATALOG', batchId, issueType, page, pageSize });
232
238
  };
233
239
  const displayedSummary = summary ?? (props.importCompletion?.task.documentType === 'CONTROLLED_CATALOG' ? props.importCompletion.batch ?? null : null);
234
240
  const columns = [
235
- { type: 'custom', key: 'productName', header: '商品名称', width: 180, render: (_, row) => _jsx(LinkButton, { icon: "ri-search-line", title: "\u6309\u5546\u54C1\u540D\u79F0\u67E5\u8BE2\u6D77\u5173\u7F16\u7801", onClick: () => setHsQuery(hsLookupQuery(row, 'productName')), children: row.productName || '—' }) },
236
- { type: 'custom', key: 'hsCode', header: '海关编码', width: 130, render: (_, row) => row.hsCode ? _jsx(LinkButton, { icon: "ri-search-line", title: "\u6309\u7F16\u7801\u67E5\u8BE2\u6D77\u5173\u7F16\u7801", onClick: () => setHsQuery(hsLookupQuery(row, 'hsCode')), children: row.hsCode }) : '—' },
237
- { type: 'text', key: 'catalogType', header: '目录类型', width: 130, format: (value) => labelFor(value, catalogTypeLabels) },
241
+ { type: 'custom', key: 'productName', header: '商品名称', width: 180, title: (_, row) => row.productName || undefined, render: (_, row) => _jsx(LinkButton, { icon: "ri-search-line", title: row.productName || '—', ariaLabel: `按商品名称查询海关编码:${row.productName || '—'}`, onClick: () => setHsQuery(hsLookupQuery(row, 'productName')), children: row.productName || '—' }) },
242
+ { type: 'custom', key: 'hsCode', header: '海关编码', width: 130, title: (_, row) => row.hsCode || undefined, render: (_, row) => row.hsCode ? _jsx(LinkButton, { icon: "ri-search-line", title: row.hsCode, ariaLabel: `按编码查询海关编码:${row.hsCode}`, onClick: () => setHsQuery(hsLookupQuery(row, 'hsCode')), children: row.hsCode }) : '—' },
243
+ { type: 'text', key: 'catalogType', header: '目录类型', width: 130, truncate: false, format: (value) => labelFor(value, catalogTypeLabels) },
238
244
  { type: 'ellipsis', key: 'controlDescription', header: '管控说明', width: 220, maxWidth: 220 },
239
- { type: 'text', key: 'sourceType', header: '来源类型', width: 110, format: (value) => labelFor(value, sourceTypeLabels) },
245
+ { type: 'text', key: 'sourceType', header: '来源类型', width: 110, truncate: false, format: (value) => labelFor(value, sourceTypeLabels) },
240
246
  { type: 'ellipsis', key: 'sourceFileName', header: '来源文件', width: 190, maxWidth: 190 },
241
- { type: 'text', key: 'updatedAt', header: '更新时间', width: 170 }
247
+ { type: 'text', key: 'updatedAt', header: '更新时间', width: 170, truncate: false, format: formatBeijingDateTime }
242
248
  ];
243
- return (_jsxs("section", { className: "tcw-catalog-page", "aria-label": "\u7BA1\u63A7\u5546\u54C1\u76EE\u5F55", children: [_jsx(FilterPanel, { definitions: controlledGoodsFilters, values: filters, onChange: changeFilter, onQuery: () => scheduler.flush(filters), onReset: () => { setFilters({}); scheduler.flush({}); } }), _jsx(InfoNotice, { children: formatNotice() }), props.importFailure?.documentType === 'CONTROLLED_CATALOG' && _jsx(CatalogImportStatusNotice, { task: props.importFailure, busy: props.busy, onRetry: (taskId) => props.onRetryImport?.(taskId) ?? Promise.resolve() }), displayedSummary && _jsx(CatalogImportSummaryNotice, { documentType: "CONTROLLED_CATALOG", batch: displayedSummary, busy: props.busy, onDownload: (documentType, batchId) => api.downloadImportIssues(documentType, batchId), onView: (batchId, issueType) => queryIssues(batchId, issueType) }), _jsx(DataGrid, { columns: columns, rows: rows, rowKey: "id", actions: (row) => _jsxs(_Fragment, { children: [_jsxs("button", { type: "button", disabled: props.busy, onClick: () => setForm({ mode: 'review', item: row }), children: [_jsx("i", { className: "ri-edit-line", "aria-hidden": "true" }), "\u5BA1\u6838"] }), _jsxs("button", { type: "button", className: "is-danger", disabled: props.busy, onClick: () => setDeleteItem(row), children: [_jsx("i", { className: "ri-delete-bin-line", "aria-hidden": "true" }), "\u5220\u9664"] })] }), pagination: pagination, onPageChange: (nextPage) => { scheduler.cancel(); void query(nextPage, filters); }, emptyText: "\u6682\u65E0\u7BA1\u63A7\u5546\u54C1\u6570\u636E" }), _jsx(CatalogUploadDialog, { open: uploadOpen, kind: "controlled", batches: props.batches, busy: props.busy, onClose: () => setUploadOpen(false), onUpload: async (file, input) => {
249
+ return (_jsxs("section", { className: "tcw-catalog-page tcw-catalog-page--controlled", "aria-label": "\u7BA1\u63A7\u5546\u54C1\u76EE\u5F55", children: [_jsx(FilterPanel, { definitions: controlledGoodsFilters, values: filters, onChange: changeFilter, onQuery: () => scheduler.flush(filters, pagination.pageSize), onReset: () => { setFilters({}); scheduler.flush({}, pagination.pageSize); } }), _jsx(InfoNotice, { children: formatNotice() }), props.importFailure?.documentType === 'CONTROLLED_CATALOG' && _jsx(CatalogImportStatusNotice, { task: props.importFailure, busy: props.busy, onRetry: (taskId) => props.onRetryImport?.(taskId) ?? Promise.resolve() }), displayedSummary && _jsx(CatalogImportSummaryNotice, { documentType: "CONTROLLED_CATALOG", batch: displayedSummary, busy: props.busy, onDownload: (documentType, batchId) => api.downloadImportIssues(documentType, batchId), onView: (batchId, issueType) => queryIssues(batchId, issueType) }), _jsx(DataGrid, { columns: columns, rows: rows, rowKey: "id", actionWidth: 160, actions: (row) => _jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "tcw-catalog-operation-button", disabled: props.busy, onClick: () => setForm({ mode: 'review', item: row }), children: [_jsx("i", { className: "ri-edit-line", "aria-hidden": "true" }), "\u5BA1\u6838"] }), _jsxs("button", { type: "button", className: "tcw-catalog-operation-button is-danger", disabled: props.busy, onClick: () => setDeleteItem(row), children: [_jsx("i", { className: "ri-delete-bin-line", "aria-hidden": "true" }), "\u5220\u9664"] })] }), pagination: pagination, onPageChange: (nextPage) => scheduler.page(nextPage, filters, pagination.pageSize), onPageSizeChange: (pageSize) => scheduler.pageSize(pageSize, filters), emptyText: "\u6682\u65E0\u7BA1\u63A7\u5546\u54C1\u6570\u636E" }), _jsx(CatalogUploadDialog, { open: uploadOpen, kind: "controlled", batches: props.batches, busy: props.busy, onClose: () => setUploadOpen(false), onNotify: props.onNotify, onUpload: async (file, input) => {
244
250
  const response = await api.uploadControlledCatalog(file, input);
245
251
  const completion = asImportCompletion(response.data);
246
252
  if (completion.batch)
247
253
  setSummary(completion.batch);
248
254
  setUploadOpen(false);
249
- } }), _jsx(ControlledGoodsFormDialog, { open: Boolean(form), mode: form?.mode ?? 'create', item: form?.item ?? null, busy: props.busy, onClose: () => setForm(null), onSave: async (input) => { form?.mode === 'review' && form.item ? await api.updateControlledGoods({ ...input, id: form.item.id }) : await api.saveControlledGoods(input); setForm(null); } }), _jsx(ConfirmDialog, { open: Boolean(deleteItem), title: "\u786E\u8BA4\u5220\u9664\u7BA1\u63A7\u5546\u54C1", message: `删除“${deleteItem?.productName ?? ''}”后,该记录将被逻辑删除。`, confirmText: "\u786E\u8BA4\u5220\u9664", danger: true, onCancel: () => setDeleteItem(null), onConfirm: () => { if (deleteItem)
250
- void api.deleteEntity('controlledGoods', deleteItem.id).then(() => setDeleteItem(null)); } }), _jsx(HsCodeLookupDialog, { open: hsQuery !== null, initialQuery: hsQuery ?? '', onAction: props.onAction, onClose: () => setHsQuery(null) }), _jsx(CatalogImportIssuesDialog, { open: Boolean(issueState), documentType: "CONTROLLED_CATALOG", batchId: issueState?.batchId ?? '', issueType: issueState?.issueType ?? 'DUPLICATE', collection: issueState?.collection ?? { items: [] }, loading: issueState?.loading ?? false, error: issueState?.error ?? '', busy: props.busy, onClose: () => { issueControllerRef.current?.close(); setIssueState(null); }, onIssueTypeChange: (issueType) => { if (issueState)
255
+ } }), _jsx(ControlledGoodsFormDialog, { open: Boolean(form), mode: form?.mode ?? 'create', item: form?.item ?? null, busy: props.busy, onClose: () => setForm(null), onSave: async (input) => {
256
+ const saved = await runNotifiedOperation(async () => { form?.mode === 'review' && form.item ? await api.updateControlledGoods({ ...input, id: form.item.id }) : await api.saveControlledGoods(input); }, props.onNotify, { success: form?.mode === 'review' ? '管控商品保存成功' : '管控商品新增成功', error: '管控商品保存失败' });
257
+ if (saved)
258
+ setForm(null);
259
+ } }), _jsx(ConfirmDialog, { open: Boolean(deleteItem), title: "\u786E\u8BA4\u5220\u9664\u7BA1\u63A7\u5546\u54C1", message: `删除“${deleteItem?.productName ?? ''}”后,该记录将被逻辑删除。`, confirmText: "\u786E\u8BA4\u5220\u9664", danger: true, onCancel: () => setDeleteItem(null), onConfirm: () => { if (deleteItem)
260
+ void runNotifiedOperation(() => api.deleteEntity('controlledGoods', deleteItem.id).then(() => undefined), props.onNotify, { success: '管控商品删除成功', error: '管控商品删除失败' }).then((deleted) => { if (deleted)
261
+ setDeleteItem(null); }); } }), _jsx(HsCodeLookupDialog, { open: hsQuery !== null, initialQuery: hsQuery ?? '', onAction: props.onAction, onClose: () => setHsQuery(null) }), _jsx(CatalogImportIssuesDialog, { open: Boolean(issueState), documentType: "CONTROLLED_CATALOG", batchId: issueState?.batchId ?? '', issueType: issueState?.issueType ?? 'DUPLICATE', collection: issueState?.collection ?? { items: [] }, loading: issueState?.loading ?? false, error: issueState?.error ?? '', busy: props.busy, onClose: () => { issueControllerRef.current?.close(); setIssueState(null); }, onIssueTypeChange: (issueType) => { if (issueState)
251
262
  void queryIssues(issueState.batchId, issueType); }, onPageChange: (page) => { if (issueState)
252
- void queryIssues(issueState.batchId, issueState.issueType, page); }, onRetry: () => { if (issueState)
253
- void queryIssues(issueState.batchId, issueState.issueType, Number(issueState.collection.page) || 1); } })] }));
263
+ void queryIssues(issueState.batchId, issueState.issueType, page, collectionPagination(issueState.collection).pageSize); }, onPageSizeChange: (pageSize) => { if (issueState)
264
+ void queryIssues(issueState.batchId, issueState.issueType, 1, pageSize); }, onRetry: () => { if (issueState)
265
+ void queryIssues(issueState.batchId, issueState.issueType, Number(issueState.collection.page) || 1, collectionPagination(issueState.collection).pageSize); } })] }));
254
266
  }
255
267
  export function SanctionedCompaniesPage(props) {
256
268
  const [filters, setFilters] = React.useState({});
@@ -273,47 +285,53 @@ export function SanctionedCompaniesPage(props) {
273
285
  if (props.command)
274
286
  props.onCommandHandled?.();
275
287
  }, [props.command, props.onCommandHandled]);
276
- const query = React.useCallback((page, nextFilters) => props.onQuery({ page, pageSize: PAGE_SIZE, filters: nextFilters }), [props.onQuery]);
277
- const scheduler = React.useMemo(() => createCatalogQueryScheduler((nextFilters) => void query(1, nextFilters)), [query]);
288
+ const query = React.useCallback((page, nextFilters, pageSize) => props.onQuery({ page, pageSize, filters: nextFilters }), [props.onQuery]);
289
+ const scheduler = React.useMemo(() => createCatalogQueryScheduler((page, nextFilters, pageSize) => void query(page, nextFilters, pageSize)), [query]);
278
290
  React.useEffect(() => () => scheduler.cancel(), [scheduler]);
279
291
  React.useEffect(() => { issueControllerRef.current?.mount(); return () => issueControllerRef.current?.dispose(); }, []);
280
292
  const changeFilter = (key, value) => {
281
293
  const nextFilters = { ...filters, [key]: value };
282
294
  setFilters(nextFilters);
283
- scheduler.schedule(sanctionedCompanyFilters.find((definition) => definition.key === key)?.type ?? 'text', nextFilters);
295
+ scheduler.schedule(sanctionedCompanyFilters.find((definition) => definition.key === key)?.type ?? 'text', nextFilters, pagination.pageSize);
284
296
  };
285
- const queryIssues = async (batchId, issueType, page = 1) => {
297
+ const queryIssues = async (batchId, issueType, page = 1, pageSize = PAGE_SIZE) => {
286
298
  if (!props.onIssueQuery)
287
299
  return;
288
- await issueControllerRef.current?.load(props.onIssueQuery, { documentType: 'SANCTION_CATALOG', batchId, issueType, page, pageSize: PAGE_SIZE });
300
+ await issueControllerRef.current?.load(props.onIssueQuery, { documentType: 'SANCTION_CATALOG', batchId, issueType, page, pageSize });
289
301
  };
290
302
  const displayedSummary = summary ?? (props.importCompletion?.task.documentType === 'SANCTION_CATALOG' ? props.importCompletion.batch ?? null : null);
291
303
  const columns = [
292
- { type: 'custom', key: 'companyName', header: '公司名称', width: 210, render: (_, row) => _jsx(LinkButton, { icon: "ri-eye-line", title: "\u67E5\u770B\u5236\u88C1\u516C\u53F8\u8BE6\u60C5", onClick: () => setForm({ mode: 'detail', item: row }), children: row.companyName || '—' }) },
304
+ { type: 'custom', key: 'companyName', header: '公司名称', width: 210, title: (_, row) => row.companyName || undefined, render: (_, row) => _jsx(LinkButton, { icon: "ri-eye-line", title: row.companyName || '—', ariaLabel: `查看制裁公司详情:${row.companyName || '—'}`, onClick: () => setForm({ mode: 'detail', item: row }), children: row.companyName || '—' }) },
293
305
  { type: 'text', key: 'countryOrRegion', header: '国家或地区', width: 120 },
294
306
  { type: 'ellipsis', key: 'sanctionListSource', header: '制裁名单来源', width: 190, maxWidth: 190 },
295
- { type: 'text', key: 'effectiveDate', header: '生效日期', width: 120 },
307
+ { type: 'text', key: 'effectiveDate', header: '生效日期', width: 120, truncate: false, format: formatBusinessDate },
296
308
  { type: 'ellipsis', key: 'sanctionReason', header: '制裁原因', width: 220, maxWidth: 220 },
297
- { type: 'text', key: 'sourceType', header: '来源类型', width: 110, format: (value) => labelFor(value, sourceTypeLabels) },
309
+ { type: 'text', key: 'sourceType', header: '来源类型', width: 110, truncate: false, format: (value) => labelFor(value, sourceTypeLabels) },
298
310
  { type: 'ellipsis', key: 'sourceFileName', header: '来源文件', width: 190, maxWidth: 190 },
299
- { type: 'text', key: 'updatedAt', header: '更新时间', width: 170 }
311
+ { type: 'text', key: 'updatedAt', header: '更新时间', width: 170, truncate: false, format: formatBeijingDateTime }
300
312
  ];
301
- return (_jsxs("section", { className: "tcw-catalog-page", "aria-label": "\u516C\u53F8\u5236\u88C1\u76EE\u5F55", children: [_jsx(FilterPanel, { definitions: sanctionedCompanyFilters, values: filters, onChange: changeFilter, onQuery: () => scheduler.flush(filters), onReset: () => { setFilters({}); scheduler.flush({}); } }), _jsx(InfoNotice, { children: formatNotice() }), props.importFailure?.documentType === 'SANCTION_CATALOG' && _jsx(CatalogImportStatusNotice, { task: props.importFailure, busy: props.busy, onRetry: (taskId) => props.onRetryImport?.(taskId) ?? Promise.resolve() }), displayedSummary && _jsx(CatalogImportSummaryNotice, { documentType: "SANCTION_CATALOG", batch: displayedSummary, busy: props.busy, onDownload: (documentType, batchId) => api.downloadImportIssues(documentType, batchId), onView: (batchId, issueType) => queryIssues(batchId, issueType) }), _jsx(DataGrid, { columns: columns, rows: rows, rowKey: "id", actions: (row) => _jsxs(_Fragment, { children: [_jsxs("button", { type: "button", disabled: props.busy, onClick: () => setForm({ mode: 'review', item: row }), children: [_jsx("i", { className: "ri-edit-line", "aria-hidden": "true" }), "\u5BA1\u6838"] }), _jsxs("button", { type: "button", className: "is-danger", disabled: props.busy, onClick: () => setDeleteItem(row), children: [_jsx("i", { className: "ri-delete-bin-line", "aria-hidden": "true" }), "\u5220\u9664"] })] }), pagination: pagination, onPageChange: (nextPage) => { scheduler.cancel(); void query(nextPage, filters); }, emptyText: "\u6682\u65E0\u5236\u88C1\u516C\u53F8\u6570\u636E" }), _jsx(CatalogUploadDialog, { open: uploadOpen, kind: "sanction", batches: props.batches, busy: props.busy, onClose: () => setUploadOpen(false), onUpload: async (file, input) => {
313
+ return (_jsxs("section", { className: "tcw-catalog-page tcw-catalog-page--sanction", "aria-label": "\u516C\u53F8\u5236\u88C1\u76EE\u5F55", children: [_jsx(FilterPanel, { definitions: sanctionedCompanyFilters, values: filters, onChange: changeFilter, onQuery: () => scheduler.flush(filters, pagination.pageSize), onReset: () => { setFilters({}); scheduler.flush({}, pagination.pageSize); } }), _jsx(InfoNotice, { children: formatNotice() }), props.importFailure?.documentType === 'SANCTION_CATALOG' && _jsx(CatalogImportStatusNotice, { task: props.importFailure, busy: props.busy, onRetry: (taskId) => props.onRetryImport?.(taskId) ?? Promise.resolve() }), displayedSummary && _jsx(CatalogImportSummaryNotice, { documentType: "SANCTION_CATALOG", batch: displayedSummary, busy: props.busy, onDownload: (documentType, batchId) => api.downloadImportIssues(documentType, batchId), onView: (batchId, issueType) => queryIssues(batchId, issueType) }), _jsx(DataGrid, { columns: columns, rows: rows, rowKey: "id", actionWidth: 160, actions: (row) => _jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "tcw-catalog-operation-button", disabled: props.busy, onClick: () => setForm({ mode: 'review', item: row }), children: [_jsx("i", { className: "ri-edit-line", "aria-hidden": "true" }), "\u5BA1\u6838"] }), _jsxs("button", { type: "button", className: "tcw-catalog-operation-button is-danger", disabled: props.busy, onClick: () => setDeleteItem(row), children: [_jsx("i", { className: "ri-delete-bin-line", "aria-hidden": "true" }), "\u5220\u9664"] })] }), pagination: pagination, onPageChange: (nextPage) => scheduler.page(nextPage, filters, pagination.pageSize), onPageSizeChange: (pageSize) => scheduler.pageSize(pageSize, filters), emptyText: "\u6682\u65E0\u5236\u88C1\u516C\u53F8\u6570\u636E" }), _jsx(CatalogUploadDialog, { open: uploadOpen, kind: "sanction", batches: props.batches, busy: props.busy, onClose: () => setUploadOpen(false), onNotify: props.onNotify, onUpload: async (file, input) => {
302
314
  const response = await api.uploadSanctionCatalog(file, input);
303
315
  const completion = asImportCompletion(response.data);
304
316
  if (completion.batch)
305
317
  setSummary(completion.batch);
306
318
  setUploadOpen(false);
307
- } }), _jsx(SanctionedCompanyDialog, { open: Boolean(form), mode: form?.mode ?? 'create', item: form?.item ?? null, busy: props.busy, onClose: () => setForm(null), onSave: async (input) => { form?.mode === 'review' && form.item ? await api.updateSanctionedCompany({ ...input, id: form.item.id }) : await api.saveSanctionedCompany(input); setForm(null); } }), _jsx(ConfirmDialog, { open: Boolean(deleteItem), title: "\u786E\u8BA4\u5220\u9664\u5236\u88C1\u516C\u53F8", message: `删除“${deleteItem?.companyName ?? ''}”后,该记录将被逻辑删除。`, confirmText: "\u786E\u8BA4\u5220\u9664", danger: true, onCancel: () => setDeleteItem(null), onConfirm: () => { if (deleteItem)
308
- void api.deleteEntity('sanctionedCompany', deleteItem.id).then(() => setDeleteItem(null)); } }), _jsx(CatalogImportIssuesDialog, { open: Boolean(issueState), documentType: "SANCTION_CATALOG", batchId: issueState?.batchId ?? '', issueType: issueState?.issueType ?? 'DUPLICATE', collection: issueState?.collection ?? { items: [] }, loading: issueState?.loading ?? false, error: issueState?.error ?? '', busy: props.busy, onClose: () => { issueControllerRef.current?.close(); setIssueState(null); }, onIssueTypeChange: (issueType) => { if (issueState)
319
+ } }), _jsx(SanctionedCompanyDialog, { open: Boolean(form), mode: form?.mode ?? 'create', item: form?.item ?? null, busy: props.busy, onClose: () => setForm(null), onSave: async (input) => {
320
+ const saved = await runNotifiedOperation(async () => { form?.mode === 'review' && form.item ? await api.updateSanctionedCompany({ ...input, id: form.item.id }) : await api.saveSanctionedCompany(input); }, props.onNotify, { success: form?.mode === 'review' ? '制裁公司保存成功' : '制裁公司新增成功', error: '制裁公司保存失败' });
321
+ if (saved)
322
+ setForm(null);
323
+ } }), _jsx(ConfirmDialog, { open: Boolean(deleteItem), title: "\u786E\u8BA4\u5220\u9664\u5236\u88C1\u516C\u53F8", message: `删除“${deleteItem?.companyName ?? ''}”后,该记录将被逻辑删除。`, confirmText: "\u786E\u8BA4\u5220\u9664", danger: true, onCancel: () => setDeleteItem(null), onConfirm: () => { if (deleteItem)
324
+ void runNotifiedOperation(() => api.deleteEntity('sanctionedCompany', deleteItem.id).then(() => undefined), props.onNotify, { success: '制裁公司删除成功', error: '制裁公司删除失败' }).then((deleted) => { if (deleted)
325
+ setDeleteItem(null); }); } }), _jsx(CatalogImportIssuesDialog, { open: Boolean(issueState), documentType: "SANCTION_CATALOG", batchId: issueState?.batchId ?? '', issueType: issueState?.issueType ?? 'DUPLICATE', collection: issueState?.collection ?? { items: [] }, loading: issueState?.loading ?? false, error: issueState?.error ?? '', busy: props.busy, onClose: () => { issueControllerRef.current?.close(); setIssueState(null); }, onIssueTypeChange: (issueType) => { if (issueState)
309
326
  void queryIssues(issueState.batchId, issueType); }, onPageChange: (page) => { if (issueState)
310
- void queryIssues(issueState.batchId, issueState.issueType, page); }, onRetry: () => { if (issueState)
311
- void queryIssues(issueState.batchId, issueState.issueType, Number(issueState.collection.page) || 1); } })] }));
327
+ void queryIssues(issueState.batchId, issueState.issueType, page, collectionPagination(issueState.collection).pageSize); }, onPageSizeChange: (pageSize) => { if (issueState)
328
+ void queryIssues(issueState.batchId, issueState.issueType, 1, pageSize); }, onRetry: () => { if (issueState)
329
+ void queryIssues(issueState.batchId, issueState.issueType, Number(issueState.collection.page) || 1, collectionPagination(issueState.collection).pageSize); } })] }));
312
330
  }
313
331
  export function CatalogCommands({ busy, onUpload, onCreate }) {
314
332
  return _jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "tcw-button tcw-button--primary", disabled: busy, onClick: onUpload, children: [_jsx("i", { className: "ri-upload-cloud-2-line", "aria-hidden": "true" }), "\u4E0A\u4F20\u76EE\u5F55\u6587\u4EF6"] }), _jsxs("button", { type: "button", className: "tcw-button tcw-button--secondary", disabled: busy, onClick: onCreate, children: [_jsx("i", { className: "ri-add-line", "aria-hidden": "true" }), "\u624B\u5DE5\u65B0\u589E"] })] });
315
333
  }
316
- export function CatalogUploadDialog({ open, kind, batches, busy = false, onClose, onUpload }) {
334
+ export function CatalogUploadDialog({ open, kind, batches, busy = false, onClose, onUpload, onNotify }) {
317
335
  const [state, dispatch] = React.useReducer(catalogUploadReducer, initialCatalogUploadState);
318
336
  const disabled = busy || state.submitting;
319
337
  React.useEffect(() => {
@@ -334,7 +352,7 @@ export function CatalogUploadDialog({ open, kind, batches, busy = false, onClose
334
352
  dispatch({ type: 'reset' });
335
353
  }
336
354
  catch (cause) {
337
- dispatch({ type: 'set-error', error: errorMessage(cause, '目录上传失败') });
355
+ onNotify?.({ message: errorMessage(cause, '目录上传失败'), tone: 'error' });
338
356
  dispatch({ type: 'cancel-replace' });
339
357
  }
340
358
  finally {
@@ -360,7 +378,7 @@ export function CatalogUploadDialog({ open, kind, batches, busy = false, onClose
360
378
  }
361
379
  export function CatalogReplaceConfirmDialog({ open, kind, batch, fileName, busy = false, onCancel, onConfirm }) {
362
380
  const typeLabel = batch?.catalogType ? catalogTypeLabels[batch.catalogType] : '公司制裁目录';
363
- return (_jsx(ConfirmDialog, { open: open, title: kind === 'controlled' ? '确认覆盖历史目录?' : '确认覆盖历史制裁目录?', danger: true, confirmText: "\u786E\u8BA4\u4E0A\u4F20\u5E76\u8986\u76D6", confirmDisabled: busy, onCancel: onCancel, onConfirm: onConfirm, message: _jsxs(_Fragment, { children: [_jsx("span", { className: "tcw-notice tcw-notice--risk", children: "\u53EA\u6709\u65B0\u6587\u4EF6\u89E3\u6790\u6210\u529F\u5E76\u901A\u8FC7\u57FA\u7840\u6821\u9A8C\u540E\uFF0C\u65E7\u6279\u6B21\u53CA\u5176\u8BB0\u5F55\u624D\u4F1A\u88AB\u903B\u8F91\u5220\u9664\u3002" }), _jsxs("span", { children: ["\u76EE\u5F55\u7C7B\u578B\uFF1A", typeLabel, _jsx("br", {}), "\u88AB\u8986\u76D6\u6587\u4EF6\uFF1A", batch?.sourceFileName || '—', _jsx("br", {}), "\u73B0\u6709\u8BB0\u5F55\u6570\u91CF\uFF1A", batch?.totalCount ?? 0, _jsx("br", {}), "\u6700\u8FD1\u66F4\u65B0\u65F6\u95F4\uFF1A", batch?.updatedAt || batch?.createdAt || '—', _jsx("br", {}), "\u65B0\u4E0A\u4F20\u6587\u4EF6\uFF1A", fileName || '—'] })] }) }));
381
+ return (_jsx(Modal, { open: open, title: kind === 'controlled' ? '确认覆盖历史目录?' : '确认覆盖历史制裁目录?', onClose: onCancel, footer: _jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "tcw-button tcw-button--secondary", onClick: onCancel, children: [_jsx("i", { className: "ri-close-line", "aria-hidden": "true" }), "\u53D6\u6D88"] }), _jsxs("button", { type: "button", className: "tcw-button tcw-button--danger", disabled: busy, onClick: onConfirm, children: [_jsx("i", { className: "ri-upload-cloud-2-line", "aria-hidden": "true" }), "\u786E\u8BA4\u4E0A\u4F20\u5E76\u8986\u76D6"] })] }), children: _jsxs("div", { className: `tcw-catalog-replace-dialog tcw-catalog-replace-dialog--${kind}`, children: [_jsxs("div", { className: "tcw-catalog-replace-warning tcw-notice--risk", role: "alert", children: [_jsx("i", { className: "ri-error-warning-fill", "aria-hidden": "true" }), _jsxs("span", { children: ["\u53EA\u6709\u65B0\u6587\u4EF6\u89E3\u6790\u6210\u529F\u5E76\u901A\u8FC7\u57FA\u7840\u6821\u9A8C\u540E\uFF0C\u65E7\u6279\u6B21\u53CA\u5176", kind === 'controlled' ? '文件导入商品' : '公司记录', "\u624D\u4F1A\u88AB\u903B\u8F91\u5220\u9664\u3002"] })] }), _jsxs("dl", { className: "tcw-catalog-replace-summary", children: [kind === 'controlled' && _jsxs(_Fragment, { children: [_jsx("dt", { children: "\u76EE\u5F55\u7C7B\u578B\uFF1A" }), _jsx("dd", { children: _jsx("span", { className: "tcw-catalog-type-badge", children: typeLabel }) })] }), _jsx("dt", { children: "\u88AB\u8986\u76D6\u6587\u4EF6\uFF1A" }), _jsx("dd", { children: batch?.sourceFileName || '—' }), _jsx("dt", { children: kind === 'controlled' ? '现有商品数量:' : '现有记录数量:' }), _jsx("dd", { className: "tcw-catalog-replace-count", children: batch?.totalCount ?? 0 }), _jsx("dt", { children: "\u6700\u8FD1\u66F4\u65B0\u65F6\u95F4\uFF1A" }), _jsx("dd", { children: formatBeijingDateTime(batch?.updatedAt || batch?.createdAt) }), _jsx("dt", { children: "\u65B0\u4E0A\u4F20\u6587\u4EF6\uFF1A" }), _jsx("dd", { children: fileName || '—' })] })] }) }));
364
382
  }
365
383
  export function ControlledGoodsFormDialog({ open, mode, item, busy = false, onClose, onSave }) {
366
384
  const [form, setForm] = React.useState(() => controlledForm(item));
@@ -383,10 +401,10 @@ export function ControlledGoodsFormDialog({ open, mode, item, busy = false, onCl
383
401
  }
384
402
  };
385
403
  const withEvidence = mode === 'review' && item?.sourceType === 'FILE_IMPORT';
386
- return (_jsx(Modal, { open: open, title: mode === 'create' ? '新增管控商品' : '审核管控商品', size: withEvidence ? 'wide' : 'normal', onClose: onClose, footer: _jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "tcw-button tcw-button--secondary", onClick: onClose, children: [_jsx("i", { className: "ri-close-line", "aria-hidden": "true" }), "\u53D6\u6D88"] }), _jsxs("button", { type: "button", className: "tcw-button tcw-button--primary", disabled: busy, onClick: () => void save(), children: [_jsx("i", { className: "ri-save-line", "aria-hidden": "true" }), "\u4FDD\u5B58"] })] }), children: withEvidence ? _jsx(ReviewSplit, { editor: _jsxs(_Fragment, { children: [editor, error && _jsx("p", { role: "alert", children: error })] }), source: _jsx(SourceEvidence, { item: item }), sourceTitle: "\u6765\u6E90\u8BC1\u636E" }) : _jsxs(_Fragment, { children: [mode === 'create' && _jsx("p", { children: "\u6765\u6E90\u7C7B\u578B\uFF1A\u624B\u5DE5\u65B0\u589E" }), editor, error && _jsx("p", { role: "alert", children: error })] }) }));
404
+ return (_jsx(Modal, { open: open, title: mode === 'create' ? '新增管控商品' : '审核管控商品', size: withEvidence ? 'wide' : 'normal', onClose: onClose, footer: _jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "tcw-button tcw-button--secondary", onClick: onClose, children: [_jsx("i", { className: "ri-close-line", "aria-hidden": "true" }), "\u53D6\u6D88"] }), _jsxs("button", { type: "button", className: "tcw-button tcw-button--primary", disabled: busy, onClick: () => void save(), children: [_jsx("i", { className: "ri-save-line", "aria-hidden": "true" }), "\u4FDD\u5B58"] })] }), children: _jsx("div", { className: `tcw-catalog-dialog ${withEvidence ? 'tcw-catalog-dialog--review tcw-catalog-dialog--controlled-review' : 'tcw-catalog-dialog--controlled-create'}`, children: withEvidence ? _jsx(ReviewSplit, { editor: _jsxs(_Fragment, { children: [editor, error && _jsx("p", { role: "alert", children: error })] }), source: _jsx(SourceEvidence, { item: item }), sourceTitle: "\u6765\u6E90\u8BC1\u636E\uFF08\u53EA\u8BFB\uFF09" }) : _jsxs(_Fragment, { children: [mode === 'create' && _jsx(CatalogManualSource, {}), editor, error && _jsx("p", { role: "alert", children: error })] }) }) }));
387
405
  }
388
406
  function ControlledGoodsFields({ value, onChange }) {
389
- return _jsxs(FieldGrid, { columns: 1, children: [_jsx(FormField, { label: "\u76EE\u5F55\u7C7B\u578B", required: true, children: _jsx("select", { value: value.catalogType, onChange: (event) => onChange({ ...value, catalogType: event.target.value }), children: options(catalogTypeLabels).map((option) => _jsx("option", { value: option.value, children: option.label }, option.value)) }) }), _jsx(FormField, { label: "\u5546\u54C1\u540D\u79F0", required: true, children: _jsx("input", { value: value.productName, onChange: (event) => onChange({ ...value, productName: event.target.value }) }) }), _jsx(FormField, { label: "\u6D77\u5173\u7F16\u7801", children: _jsx("input", { value: value.hsCode ?? '', onChange: (event) => onChange({ ...value, hsCode: event.target.value }) }) }), _jsx(FormField, { label: "\u7BA1\u63A7\u8BF4\u660E", children: _jsx("textarea", { rows: 5, value: value.controlDescription ?? '', onChange: (event) => onChange({ ...value, controlDescription: event.target.value }) }) })] });
407
+ return _jsxs(FieldGrid, { columns: 1, children: [_jsxs(FormField, { label: "\u76EE\u5F55\u7C7B\u578B", required: true, children: [_jsxs("select", { value: value.catalogType, onChange: (event) => onChange({ ...value, catalogType: event.target.value }), children: [_jsx("option", { value: "", children: "\u8BF7\u9009\u62E9\u76EE\u5F55\u7C7B\u578B" }), options(catalogTypeLabels).map((option) => _jsx("option", { value: option.value, children: option.label }, option.value))] }), _jsx("small", { children: "\u519B\u6C11\u4E24\u7528\u7269\u8D44 / \u6D77\u5916\u5236\u88C1" })] }), _jsx(FormField, { label: "\u5546\u54C1\u540D\u79F0", required: true, children: _jsx("input", { placeholder: "\u8BF7\u8F93\u5165\u5546\u54C1\u540D\u79F0", value: value.productName, onChange: (event) => onChange({ ...value, productName: event.target.value }) }) }), _jsx(FormField, { label: "\u6D77\u5173\u7F16\u7801", children: _jsx("input", { placeholder: "\u8BF7\u8F93\u5165\u6D77\u5173\u7F16\u7801\uFF08\u53EF\u9009\uFF09", value: value.hsCode ?? '', onChange: (event) => onChange({ ...value, hsCode: event.target.value }) }) }), _jsx(FormField, { label: "\u7BA1\u63A7\u8BF4\u660E", children: _jsxs("div", { className: "tcw-counted-textarea", children: [_jsx("textarea", { rows: 5, maxLength: 500, placeholder: "\u8BF7\u8F93\u5165\u7BA1\u63A7\u8BF4\u660E\uFF08\u53EF\u9009\uFF09", value: value.controlDescription ?? '', onChange: (event) => onChange({ ...value, controlDescription: event.target.value }) }), _jsxs("small", { className: "tcw-character-count", children: [value.controlDescription?.length ?? 0, "/500"] })] }) })] });
390
408
  }
391
409
  export function SanctionedCompanyDialog({ open, mode, item, busy = false, onClose, onSave }) {
392
410
  const [form, setForm] = React.useState(() => sanctionedForm(item));
@@ -397,8 +415,9 @@ export function SanctionedCompanyDialog({ open, mode, item, busy = false, onClos
397
415
  } }, [open, item]);
398
416
  const editor = _jsx(SanctionedCompanyFields, { value: form, disabled: mode === 'detail', onChange: setForm });
399
417
  const save = async () => {
400
- if (!form.companyName.trim()) {
401
- setError('公司名称为必填项');
418
+ const validationError = validateSanctionedCompanyInput(form);
419
+ if (validationError) {
420
+ setError(validationError);
402
421
  return;
403
422
  }
404
423
  try {
@@ -413,42 +432,58 @@ export function SanctionedCompanyDialog({ open, mode, item, busy = false, onClos
413
432
  ? _jsxs("button", { type: "button", className: "tcw-button tcw-button--primary", onClick: onClose, children: [_jsx("i", { className: "ri-close-line", "aria-hidden": "true" }), "\u5173\u95ED"] })
414
433
  : _jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "tcw-button tcw-button--secondary", onClick: onClose, children: [_jsx("i", { className: "ri-close-line", "aria-hidden": "true" }), "\u53D6\u6D88"] }), _jsxs("button", { type: "button", className: "tcw-button tcw-button--primary", disabled: busy, onClick: () => void save(), children: [_jsx("i", { className: "ri-save-line", "aria-hidden": "true" }), "\u4FDD\u5B58"] })] });
415
434
  const withEvidence = mode !== 'create' && item?.sourceType === 'FILE_IMPORT';
416
- return (_jsx(Modal, { open: open, title: title, size: withEvidence ? 'wide' : 'normal', onClose: onClose, footer: footer, children: withEvidence ? _jsx(ReviewSplit, { editor: _jsxs(_Fragment, { children: [editor, error && _jsx("p", { role: "alert", children: error })] }), source: _jsx(SourceEvidence, { item: item }), sourceTitle: "\u6765\u6E90\u8BC1\u636E" }) : _jsxs(_Fragment, { children: [mode === 'create' && _jsx("p", { children: "\u6765\u6E90\u7C7B\u578B\uFF1A\u624B\u5DE5\u65B0\u589E" }), editor, error && _jsx("p", { role: "alert", children: error })] }) }));
435
+ return (_jsx(Modal, { open: open, title: title, size: withEvidence ? 'wide' : 'normal', onClose: onClose, footer: footer, children: _jsx("div", { className: `tcw-catalog-dialog ${withEvidence ? 'tcw-catalog-dialog--review tcw-catalog-dialog--sanction-review' : 'tcw-catalog-dialog--sanction-create'}`, children: withEvidence ? _jsx(ReviewSplit, { editor: _jsxs(_Fragment, { children: [editor, error && _jsx("p", { role: "alert", children: error })] }), source: _jsx(SourceEvidence, { item: item }), sourceTitle: "\u6765\u6E90\u8BC1\u636E\uFF08\u53EA\u8BFB\uFF09" }) : _jsxs(_Fragment, { children: [mode === 'create' && _jsx(CatalogManualSource, {}), editor, error && _jsx("p", { role: "alert", children: error })] }) }) }));
436
+ }
437
+ export function validateSanctionedCompanyInput(input) {
438
+ if (!input.companyName.trim() || !input.sanctionListSource?.trim() || !input.effectiveDate?.trim() || !input.sanctionReason?.trim()) {
439
+ return '公司名称、制裁名单来源、发布/生效日期和制裁原因为必填项';
440
+ }
441
+ return '';
417
442
  }
418
443
  function SanctionedCompanyFields({ value, disabled, onChange }) {
419
- return _jsxs(FieldGrid, { columns: 1, children: [_jsx(FormField, { label: "\u516C\u53F8\u540D\u79F0", required: true, children: _jsx("input", { disabled: disabled, value: value.companyName, onChange: (event) => onChange({ ...value, companyName: event.target.value }) }) }), _jsx(FormField, { label: "\u56FD\u5BB6\u6216\u5730\u533A", children: _jsx("input", { disabled: disabled, value: value.countryOrRegion ?? '', onChange: (event) => onChange({ ...value, countryOrRegion: event.target.value }) }) }), _jsx(FormField, { label: "\u5236\u88C1\u540D\u5355\u6765\u6E90", children: _jsx("input", { disabled: disabled, value: value.sanctionListSource ?? '', onChange: (event) => onChange({ ...value, sanctionListSource: event.target.value }) }) }), _jsx(FormField, { label: "\u53D1\u5E03/\u751F\u6548\u65E5\u671F", children: _jsx("input", { type: "date", disabled: disabled, value: value.effectiveDate ?? '', onChange: (event) => onChange({ ...value, effectiveDate: event.target.value }) }) }), _jsx(FormField, { label: "\u5236\u88C1\u539F\u56E0", children: _jsx("textarea", { disabled: disabled, rows: 4, value: value.sanctionReason ?? '', onChange: (event) => onChange({ ...value, sanctionReason: event.target.value }) }) }), _jsx(FormField, { label: "\u5907\u6CE8", children: _jsx("textarea", { disabled: disabled, rows: 3, value: value.remarks ?? '', onChange: (event) => onChange({ ...value, remarks: event.target.value }) }) })] });
444
+ return _jsxs(FieldGrid, { columns: 1, children: [_jsx(FormField, { label: "\u516C\u53F8\u540D\u79F0", required: true, children: _jsx("input", { disabled: disabled, placeholder: "\u8BF7\u8F93\u5165\u516C\u53F8\u540D\u79F0", value: value.companyName, onChange: (event) => onChange({ ...value, companyName: event.target.value }) }) }), _jsx(FormField, { label: "\u56FD\u5BB6\u6216\u5730\u533A", children: _jsx("input", { disabled: disabled, placeholder: "\u8BF7\u8F93\u5165\u56FD\u5BB6\u6216\u5730\u533A", value: value.countryOrRegion ?? '', onChange: (event) => onChange({ ...value, countryOrRegion: event.target.value }) }) }), _jsx(FormField, { label: "\u5236\u88C1\u540D\u5355\u6765\u6E90", required: true, children: _jsx("input", { disabled: disabled, placeholder: "\u8BF7\u8F93\u5165\u5236\u88C1\u540D\u5355\u6765\u6E90", value: value.sanctionListSource ?? '', onChange: (event) => onChange({ ...value, sanctionListSource: event.target.value }) }) }), _jsx(FormField, { label: "\u53D1\u5E03/\u751F\u6548\u65E5\u671F", required: true, children: _jsx("input", { type: "date", disabled: disabled, value: value.effectiveDate ?? '', onChange: (event) => onChange({ ...value, effectiveDate: event.target.value }) }) }), _jsx(FormField, { label: "\u5236\u88C1\u539F\u56E0", required: true, children: _jsxs("div", { className: "tcw-counted-textarea", children: [_jsx("textarea", { disabled: disabled, rows: 4, maxLength: 500, placeholder: "\u8BF7\u8F93\u5165\u5236\u88C1\u539F\u56E0", value: value.sanctionReason ?? '', onChange: (event) => onChange({ ...value, sanctionReason: event.target.value }) }), _jsxs("small", { className: "tcw-character-count", children: [value.sanctionReason?.length ?? 0, "/500"] })] }) }), _jsx(FormField, { label: "\u5907\u6CE8", children: _jsxs("div", { className: "tcw-counted-textarea", children: [_jsx("textarea", { disabled: disabled, rows: 3, maxLength: 500, placeholder: "\u8BF7\u8F93\u5165\u5907\u6CE8\uFF08\u53EF\u9009\uFF09", value: value.remarks ?? '', onChange: (event) => onChange({ ...value, remarks: event.target.value }) }), _jsxs("small", { className: "tcw-character-count", children: [value.remarks?.length ?? 0, "/500"] })] }) })] });
445
+ }
446
+ function CatalogManualSource() {
447
+ return _jsxs("div", { className: "tcw-catalog-manual-source", "aria-label": "\u6765\u6E90\u7C7B\u578B\uFF1A\u624B\u5DE5\u65B0\u589E", children: [_jsx("span", { children: "\u6765\u6E90\u7C7B\u578B\uFF1A" }), _jsx("strong", { className: "tcw-catalog-source-badge", children: "\u624B\u5DE5\u65B0\u589E" })] });
420
448
  }
421
449
  function SourceEvidence({ item }) {
422
- return _jsxs(FieldGrid, { columns: 1, children: [_jsx(FormField, { label: "\u6765\u6E90\u7C7B\u578B", children: _jsx("input", { disabled: true, value: labelFor(item?.sourceType, sourceTypeLabels) }) }), _jsx(FormField, { label: "\u6765\u6E90\u6587\u4EF6", children: _jsx("input", { disabled: true, value: item?.sourceFileName ?? '—' }) }), _jsx(FormField, { label: "\u6765\u6E90\u4F4D\u7F6E", children: _jsx("input", { disabled: true, value: item?.sourceLocation ?? '—' }) }), _jsx(FormField, { label: "\u6765\u6E90\u539F\u6587", children: _jsx("textarea", { disabled: true, rows: 12, value: item?.sourceContent ?? '—' }) })] });
450
+ return _jsxs(FieldGrid, { columns: 1, children: [_jsx(FormField, { label: "\u6765\u6E90\u7C7B\u578B", children: _jsx("input", { disabled: true, value: labelFor(item?.sourceType, sourceTypeLabels) }) }), _jsx(FormField, { label: "\u6765\u6E90\u6587\u4EF6", children: _jsx("input", { disabled: true, value: item?.sourceFileName ?? '—' }) }), _jsx(FormField, { label: "\u6765\u6E90\u4F4D\u7F6E", children: _jsx("input", { disabled: true, value: item?.sourceLocation ?? '—' }) })] });
423
451
  }
424
452
  export function CatalogImportSummaryNotice({ documentType, batch, busy, onDownload, onView }) {
425
453
  const issueCount = (batch.duplicateCount ?? 0) + (batch.invalidCount ?? 0);
454
+ if (issueCount === 0)
455
+ return null;
426
456
  const initialIssueType = (batch.duplicateCount ?? 0) > 0 ? 'DUPLICATE' : 'INVALID';
427
- return _jsxs(InfoNotice, { children: [_jsxs("span", { children: ["\u5BFC\u5165\u5B8C\u6210\uFF1A\u603B\u6570 ", batch.totalCount ?? 0, "\uFF0C\u6210\u529F ", batch.successCount ?? 0, "\uFF0C\u91CD\u590D ", batch.duplicateCount ?? 0, "\uFF0C\u65E0\u6548 ", batch.invalidCount ?? 0, "\u3002"] }), issueCount > 0 && _jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "tcw-button tcw-button--secondary", disabled: busy || !onView, onClick: () => void onView?.(batch.id, initialIssueType), children: [_jsx("i", { className: "ri-list-check-2", "aria-hidden": "true" }), "\u67E5\u770B\u660E\u7EC6"] }), _jsxs("button", { type: "button", className: "tcw-button tcw-button--secondary", disabled: busy, onClick: () => void onDownload(documentType, batch.id), children: [_jsx("i", { className: "ri-download-line", "aria-hidden": "true" }), "\u4E0B\u8F7D Excel"] })] })] });
457
+ return _jsxs(InfoNotice, { children: [_jsxs("span", { children: ["\u5BFC\u5165\u95EE\u9898\uFF1A\u91CD\u590D ", batch.duplicateCount ?? 0, "\uFF0C\u65E0\u6548 ", batch.invalidCount ?? 0, "\u3002"] }), _jsxs("button", { type: "button", className: "tcw-button tcw-button--secondary", disabled: busy || !onView, onClick: () => void onView?.(batch.id, initialIssueType), children: [_jsx("i", { className: "ri-list-check-2", "aria-hidden": "true" }), "\u67E5\u770B\u660E\u7EC6"] }), _jsxs("button", { type: "button", className: "tcw-button tcw-button--secondary", disabled: busy, onClick: () => void onDownload(documentType, batch.id).catch(() => undefined), children: [_jsx("i", { className: "ri-download-line", "aria-hidden": "true" }), "\u4E0B\u8F7D Excel"] })] });
428
458
  }
429
459
  export function CatalogImportStatusNotice({ task, busy, onRetry }) {
430
460
  if (task.status !== 'FAILED' && task.status !== 'TIMED_OUT')
431
461
  return null;
432
462
  return _jsxs("div", { className: "tcw-catalog-import-failure", role: "alert", children: [_jsxs("div", { children: [_jsx("strong", { children: task.status === 'TIMED_OUT' ? '目录解析超时' : '目录解析失败' }), _jsx("p", { children: task.errorMessage || '解析任务未完成,请重新解析。' })] }), _jsxs("button", { type: "button", className: "tcw-button tcw-button--primary", disabled: busy, onClick: () => void onRetry(task.id).catch(() => undefined), children: [_jsx("i", { className: "ri-restart-line", "aria-hidden": "true" }), "\u91CD\u65B0\u89E3\u6790"] })] });
433
463
  }
434
- export function CatalogImportIssuesDialog({ open, documentType, batchId, issueType, collection, loading, error, busy, onClose, onIssueTypeChange, onPageChange, onRetry }) {
464
+ export function CatalogImportIssuesDialog({ open, documentType, batchId, issueType, collection, loading, error, busy, onClose, onIssueTypeChange, onPageChange, onPageSizeChange, onRetry }) {
435
465
  const columns = [
436
- { type: 'text', key: 'issueType', header: '问题类型', width: 100, format: (value) => value === 'DUPLICATE' ? '重复' : '无效' },
437
- { type: 'text', key: 'sourceSequence', header: '来源序号', width: 100 },
466
+ { type: 'text', key: 'issueType', header: '问题类型', width: 100, truncate: false, format: (value) => value === 'DUPLICATE' ? '重复' : '无效' },
467
+ { type: 'text', key: 'sourceSequence', header: '来源序号', width: 100, truncate: false },
438
468
  { type: 'ellipsis', key: 'sourceLocation', header: '来源位置', width: 160, maxWidth: 160 },
439
- { type: 'ellipsis', key: 'sourceContent', header: '来源原文', width: 260, maxWidth: 260 },
440
469
  { type: 'ellipsis', key: 'message', header: '问题说明', width: 220, maxWidth: 220 }
441
470
  ];
442
- return _jsxs(Modal, { open: open, title: "\u5BFC\u5165\u95EE\u9898\u660E\u7EC6", size: "wide", onClose: onClose, footer: _jsxs("button", { type: "button", className: "tcw-button tcw-button--primary", onClick: onClose, children: [_jsx("i", { className: "ri-close-line", "aria-hidden": "true" }), "\u5173\u95ED"] }), children: [_jsxs("div", { className: "tcw-catalog-issue-toolbar", "data-document-type": documentType, "data-batch-id": batchId, children: [_jsxs("button", { type: "button", className: issueType === 'DUPLICATE' ? 'is-active' : '', disabled: busy || loading, onClick: () => onIssueTypeChange('DUPLICATE'), children: [_jsx("i", { className: "ri-file-copy-line", "aria-hidden": "true" }), "\u91CD\u590D"] }), _jsxs("button", { type: "button", className: issueType === 'INVALID' ? 'is-active' : '', disabled: busy || loading, onClick: () => onIssueTypeChange('INVALID'), children: [_jsx("i", { className: "ri-error-warning-line", "aria-hidden": "true" }), "\u65E0\u6548"] })] }), loading && _jsx("p", { className: "tcw-catalog-issue-status", role: "status", children: "\u6B63\u5728\u52A0\u8F7D\u5BFC\u5165\u95EE\u9898\u660E\u7EC6..." }), error && _jsxs("div", { className: "tcw-catalog-issue-error", role: "alert", children: [_jsx("span", { children: error }), _jsxs("button", { type: "button", className: "tcw-button tcw-button--secondary", disabled: busy || loading, onClick: onRetry, children: [_jsx("i", { className: "ri-refresh-line", "aria-hidden": "true" }), "\u91CD\u65B0\u52A0\u8F7D"] })] }), !loading && !error && _jsx(DataGrid, { columns: columns, rows: collection.items, rowKey: "id", pagination: collectionPagination(collection), onPageChange: onPageChange, emptyText: issueType === 'DUPLICATE' ? '暂无重复数据' : '暂无无效数据' })] });
471
+ return _jsxs(Modal, { open: open, title: "\u5BFC\u5165\u95EE\u9898\u660E\u7EC6", size: "wide", onClose: onClose, footer: _jsxs("button", { type: "button", className: "tcw-button tcw-button--primary", onClick: onClose, children: [_jsx("i", { className: "ri-close-line", "aria-hidden": "true" }), "\u5173\u95ED"] }), children: [_jsxs("div", { className: "tcw-catalog-issue-toolbar", "data-document-type": documentType, "data-batch-id": batchId, children: [_jsxs("button", { type: "button", className: issueType === 'DUPLICATE' ? 'is-active' : '', disabled: busy || loading, onClick: () => onIssueTypeChange('DUPLICATE'), children: [_jsx("i", { className: "ri-file-copy-line", "aria-hidden": "true" }), "\u91CD\u590D"] }), _jsxs("button", { type: "button", className: issueType === 'INVALID' ? 'is-active' : '', disabled: busy || loading, onClick: () => onIssueTypeChange('INVALID'), children: [_jsx("i", { className: "ri-error-warning-line", "aria-hidden": "true" }), "\u65E0\u6548"] })] }), loading && _jsx("p", { className: "tcw-catalog-issue-status", role: "status", children: "\u6B63\u5728\u52A0\u8F7D\u5BFC\u5165\u95EE\u9898\u660E\u7EC6..." }), error && _jsxs("div", { className: "tcw-catalog-issue-error", role: "alert", children: [_jsx("span", { children: error }), _jsxs("button", { type: "button", className: "tcw-button tcw-button--secondary", disabled: busy || loading, onClick: onRetry, children: [_jsx("i", { className: "ri-refresh-line", "aria-hidden": "true" }), "\u91CD\u65B0\u52A0\u8F7D"] })] }), !loading && !error && _jsx(DataGrid, { columns: columns, rows: collection.items, rowKey: "id", pagination: collectionPagination(collection), onPageChange: onPageChange, onPageSizeChange: onPageSizeChange, emptyText: issueType === 'DUPLICATE' ? '暂无重复数据' : '暂无无效数据' })] });
472
+ }
473
+ export function hsSearchPageSizeRequest(keywords, pageSize) {
474
+ return { keywords: keywords.trim(), page: 1, pageSize };
475
+ }
476
+ function emptyHsSearchData(page = 1, pageSize = 20) {
477
+ return { items: [], total: 0, page, pageSize, pageCount: 1 };
443
478
  }
444
479
  export function HsCodeLookupDialog({ open, initialQuery, onAction, onClose }) {
445
480
  const [query, setQuery] = React.useState(initialQuery);
446
- const [page, setPage] = React.useState(1);
447
- const [results, setResults] = React.useState([]);
481
+ const [collection, setCollection] = React.useState(() => emptyHsSearchData());
448
482
  const [detail, setDetail] = React.useState(null);
449
483
  const [error, setError] = React.useState('');
450
484
  const [loading, setLoading] = React.useState(false);
451
- const search = React.useCallback(async (keywords, nextPage = 1) => {
485
+ const hsRequestGateRef = React.useRef(createLatestRequestGate());
486
+ const search = React.useCallback(async (keywords, nextPage = 1, pageSize = 20) => {
452
487
  const normalized = keywords.trim();
453
488
  if (!normalized) {
454
489
  setError('请输入商品名称或海关编码');
@@ -457,56 +492,73 @@ export function HsCodeLookupDialog({ open, initialQuery, onAction, onClose }) {
457
492
  setLoading(true);
458
493
  setError('');
459
494
  setDetail(null);
495
+ const requestId = hsRequestGateRef.current.begin();
460
496
  try {
461
- const response = await onAction('search_hs_code', { keywords: normalized, page: nextPage });
497
+ const response = await onAction('search_hs_code', { keywords: normalized, page: nextPage, pageSize });
462
498
  if (response.success === false)
463
499
  throw new Error(response.message || '海关编码查询失败');
464
- const data = isHsSearchData(response.data) ? response.data : { results: [] };
465
- setResults(data.results);
466
- setPage(nextPage);
500
+ if (!hsRequestGateRef.current.commit(requestId))
501
+ return;
502
+ setCollection(isHsSearchData(response.data) ? response.data : emptyHsSearchData(nextPage, pageSize));
467
503
  }
468
504
  catch (cause) {
505
+ if (!hsRequestGateRef.current.isLatest(requestId))
506
+ return;
469
507
  setError(errorMessage(cause, '海关编码查询失败'));
470
- setResults([]);
508
+ setCollection(emptyHsSearchData(nextPage, pageSize));
471
509
  }
472
510
  finally {
473
- setLoading(false);
511
+ hsRequestGateRef.current.finish(requestId);
512
+ if (hsRequestGateRef.current.isLatest(requestId))
513
+ setLoading(false);
474
514
  }
475
515
  }, [onAction]);
476
516
  React.useEffect(() => {
477
- if (!open)
517
+ if (!open) {
518
+ hsRequestGateRef.current.invalidate();
519
+ setLoading(false);
478
520
  return;
521
+ }
479
522
  setQuery(initialQuery);
480
- setPage(1);
523
+ setCollection(emptyHsSearchData());
481
524
  void search(initialQuery, 1);
525
+ return () => hsRequestGateRef.current.invalidate();
482
526
  }, [open, initialQuery, search]);
483
527
  const showDetail = async (result) => {
484
528
  setLoading(true);
485
529
  setError('');
530
+ const requestId = hsRequestGateRef.current.begin();
486
531
  try {
487
532
  const response = await onAction('get_hs_code_detail', { code: result.code, ...(result.detailUrl ? { detailUrl: result.detailUrl } : {}) });
488
533
  if (response.success === false)
489
534
  throw new Error(response.message || '海关编码详情查询失败');
535
+ if (!hsRequestGateRef.current.commit(requestId))
536
+ return;
490
537
  setDetail(isRecord(response.data) ? response.data : {});
491
538
  }
492
539
  catch (cause) {
540
+ if (!hsRequestGateRef.current.isLatest(requestId))
541
+ return;
493
542
  setError(errorMessage(cause, '海关编码详情查询失败'));
494
543
  }
495
544
  finally {
496
- setLoading(false);
545
+ hsRequestGateRef.current.finish(requestId);
546
+ if (hsRequestGateRef.current.isLatest(requestId))
547
+ setLoading(false);
497
548
  }
498
549
  };
550
+ const close = () => { hsRequestGateRef.current.invalidate(); setLoading(false); onClose(); };
499
551
  const columns = [
500
552
  { type: 'text', key: 'code', header: '海关编码', width: 130 },
501
553
  { type: 'ellipsis', key: 'name', header: '商品名称', width: 220, maxWidth: 220 },
502
554
  { type: 'ellipsis', key: 'englishName', header: '英文名称', width: 220, maxWidth: 220 },
503
- { type: 'text', key: 'unit', header: '单位', width: 90 },
504
- { type: 'text', key: 'taxRefundRate', header: '退税率', width: 90 }
555
+ { type: 'text', key: 'unit', header: '单位', width: 90, truncate: false },
556
+ { type: 'text', key: 'taxRefundRate', header: '退税率', width: 90, truncate: false }
505
557
  ];
506
- return (_jsxs(Modal, { open: open, title: "\u6D77\u5173\u7F16\u7801\u67E5\u8BE2", size: "wide", onClose: onClose, footer: _jsxs("button", { type: "button", className: "tcw-button tcw-button--primary", onClick: onClose, children: [_jsx("i", { className: "ri-close-line", "aria-hidden": "true" }), "\u5173\u95ED"] }), children: [_jsx("form", { onSubmit: (event) => { event.preventDefault(); void search(query, 1); }, children: _jsxs(FieldGrid, { columns: 2, children: [_jsx(FormField, { label: "\u5546\u54C1\u540D\u79F0\u6216\u6D77\u5173\u7F16\u7801", required: true, children: _jsx("input", { value: query, onChange: (event) => setQuery(event.target.value) }) }), _jsx("div", { children: _jsxs("button", { type: "submit", className: "tcw-button tcw-button--primary", disabled: loading, children: [_jsx("i", { className: "ri-search-line", "aria-hidden": "true" }), "\u67E5\u8BE2"] }) })] }) }), error && _jsx("p", { role: "alert", children: error }), detail ? _jsx(HsCodeDetail, { detail: detail, onBack: () => setDetail(null) }) : _jsxs(_Fragment, { children: [_jsx(DataGrid, { columns: columns, rows: results, rowKey: (row) => row.code, actions: (row) => _jsxs("button", { type: "button", onClick: () => void showDetail(row), children: [_jsx("i", { className: "ri-eye-line", "aria-hidden": "true" }), "\u8BE6\u60C5"] }), emptyText: loading ? '查询中' : '暂无海关编码结果' }), results.length > 0 && _jsxs("div", { className: "tcw-page-commands", children: [_jsxs("button", { type: "button", className: "tcw-button tcw-button--secondary", disabled: loading || page <= 1, onClick: () => void search(query, page - 1), children: [_jsx("i", { className: "ri-arrow-left-s-line", "aria-hidden": "true" }), "\u4E0A\u4E00\u9875"] }), _jsxs("span", { children: ["\u7B2C ", page, " \u9875"] }), _jsxs("button", { type: "button", className: "tcw-button tcw-button--secondary", disabled: loading, onClick: () => void search(query, page + 1), children: [_jsx("i", { className: "ri-arrow-right-s-line", "aria-hidden": "true" }), "\u4E0B\u4E00\u9875"] })] })] })] }));
558
+ return (_jsxs(Modal, { open: open, title: "\u6D77\u5173\u7F16\u7801\u67E5\u8BE2", size: "wide", onClose: close, footer: _jsxs("button", { type: "button", className: "tcw-button tcw-button--primary", onClick: close, children: [_jsx("i", { className: "ri-close-line", "aria-hidden": "true" }), "\u5173\u95ED"] }), children: [_jsx("form", { onSubmit: (event) => { event.preventDefault(); void search(query, 1); }, children: _jsxs(FieldGrid, { columns: 2, children: [_jsx(FormField, { label: "\u5546\u54C1\u540D\u79F0\u6216\u6D77\u5173\u7F16\u7801", required: true, children: _jsx("input", { value: query, onChange: (event) => setQuery(event.target.value) }) }), _jsx("div", { children: _jsxs("button", { type: "submit", className: "tcw-button tcw-button--primary", disabled: loading, children: [_jsx("i", { className: "ri-search-line", "aria-hidden": "true" }), "\u67E5\u8BE2"] }) })] }) }), error && _jsx("p", { role: "alert", children: error }), detail ? _jsx(HsCodeDetail, { detail: detail, onBack: () => setDetail(null) }) : _jsx(_Fragment, { children: _jsx(DataGrid, { columns: columns, rows: collection.items, rowKey: (row) => row.code, pagination: collection, onPageChange: (page) => void search(query, page, collection.pageSize), onPageSizeChange: (pageSize) => { const request = hsSearchPageSizeRequest(query, pageSize); void search(request.keywords, request.page, request.pageSize); }, actions: (row) => _jsxs("button", { type: "button", onClick: () => void showDetail(row), children: [_jsx("i", { className: "ri-eye-line", "aria-hidden": "true" }), "\u8BE6\u60C5"] }), emptyText: loading ? '查询中' : '暂无海关编码结果' }) })] }));
507
559
  }
508
560
  function HsCodeDetail({ detail, onBack }) {
509
- return _jsxs("section", { children: [_jsxs("button", { type: "button", className: "tcw-button tcw-button--secondary", onClick: onBack, children: [_jsx("i", { className: "ri-arrow-left-line", "aria-hidden": "true" }), "\u8FD4\u56DE\u67E5\u8BE2\u7ED3\u679C"] }), _jsxs("h3", { children: [detail.code || '—', " ", detail.name || detail.title || ''] }), _jsxs("p", { children: [_jsx("strong", { children: "\u67E5\u8BE2\u65F6\u95F4\uFF1A" }), detail.queriedAt || '—'] }), detail.sourceUrl && _jsxs("p", { children: [_jsx("strong", { children: "\u6570\u636E\u6765\u6E90\uFF1A" }), _jsx("a", { href: detail.sourceUrl, target: "_blank", rel: "noreferrer", children: detail.sourceUrl })] }), detail.sections?.map((section, index) => _jsxs("section", { children: [_jsx("h4", { children: section.title || '详情' }), section.rows?.map((row, rowIndex) => _jsxs("p", { children: [_jsxs("strong", { children: [row.label || '字段', "\uFF1A"] }), row.value || '—'] }, `${row.label ?? 'row'}-${rowIndex}`))] }, `${section.title ?? 'detail'}-${index}`))] });
561
+ return _jsxs("section", { children: [_jsxs("button", { type: "button", className: "tcw-button tcw-button--secondary", onClick: onBack, children: [_jsx("i", { className: "ri-arrow-left-line", "aria-hidden": "true" }), "\u8FD4\u56DE\u67E5\u8BE2\u7ED3\u679C"] }), _jsxs("h3", { children: [detail.code || '—', " ", detail.name || detail.title || ''] }), _jsxs("p", { children: [_jsx("strong", { children: "\u67E5\u8BE2\u65F6\u95F4\uFF1A" }), formatBeijingDateTime(detail.queriedAt)] }), detail.sourceUrl && _jsxs("p", { children: [_jsx("strong", { children: "\u6570\u636E\u6765\u6E90\uFF1A" }), _jsx("a", { href: detail.sourceUrl, target: "_blank", rel: "noreferrer", children: detail.sourceUrl })] }), detail.sections?.map((section, index) => _jsxs("section", { children: [_jsx("h4", { children: section.title || '详情' }), section.rows?.map((row, rowIndex) => _jsxs("p", { children: [_jsxs("strong", { children: [row.label || '字段', "\uFF1A"] }), row.value || '—'] }, `${row.label ?? 'row'}-${rowIndex}`))] }, `${section.title ?? 'detail'}-${index}`))] });
510
562
  }
511
563
  export function hsLookupQuery(row, field) {
512
564
  return String(row[field] ?? '').trim();
@@ -529,8 +581,8 @@ function sanctionedForm(item) {
529
581
  remarks: item?.remarks ?? ''
530
582
  };
531
583
  }
532
- function LinkButton({ children, icon, title, onClick }) {
533
- return _jsx("span", { className: "tcw-row-actions", children: _jsxs("button", { type: "button", title: title, onClick: onClick, children: [_jsx("i", { className: icon, "aria-hidden": "true" }), children] }) });
584
+ function LinkButton({ children, icon, title, ariaLabel, onClick }) {
585
+ return _jsx("span", { className: "tcw-row-actions tcw-catalog-table-link", children: _jsxs("button", { type: "button", title: title, "aria-label": ariaLabel, onClick: onClick, children: [_jsx("i", { className: icon, "aria-hidden": "true" }), children] }) });
534
586
  }
535
587
  function options(labels) {
536
588
  return Object.entries(labels).map(([value, label]) => ({ value, label: String(label) }));
@@ -539,7 +591,13 @@ function labelFor(value, labels) {
539
591
  return typeof value === 'string' && value in labels ? labels[value] : value ? String(value) : '—';
540
592
  }
541
593
  function isHsSearchData(value) {
542
- return isRecord(value) && Array.isArray(value.results) && value.results.every((item) => isRecord(item) && typeof item.code === 'string');
594
+ return isRecord(value)
595
+ && Array.isArray(value.items)
596
+ && value.items.every((item) => isRecord(item) && typeof item.code === 'string')
597
+ && Number.isInteger(value.total) && Number(value.total) >= 0
598
+ && Number.isInteger(value.page) && Number(value.page) >= 1
599
+ && isPaginationPageSize(Number(value.pageSize))
600
+ && Number.isInteger(value.pageCount) && Number(value.pageCount) >= 1;
543
601
  }
544
602
  function isRecord(value) {
545
603
  return Boolean(value && typeof value === 'object' && !Array.isArray(value));
@@ -552,7 +610,7 @@ function assertActionSucceeded(response) {
552
610
  throw new Error(typeof response.message === 'string' && response.message ? response.message : '操作失败');
553
611
  }
554
612
  function collectionPagination(collection) {
555
- const pageSize = Number(collection.pageSize) > 0 ? Number(collection.pageSize) : PAGE_SIZE;
613
+ const pageSize = collection.pageSize ?? PAGE_SIZE;
556
614
  const total = Number(collection.total) >= 0 ? Number(collection.total) : collection.items.length;
557
615
  const pageCount = Math.max(1, Math.ceil(total / pageSize));
558
616
  const page = Math.min(pageCount, Math.max(1, Number(collection.page) || 1));
@@ -564,7 +622,7 @@ function asImportCompletion(value) {
564
622
  return value;
565
623
  }
566
624
  function isCatalogImportStartData(value) {
567
- return isRecord(value) && isRecord(value.task) && typeof value.task.id === 'string' && typeof value.task.documentType === 'string' && typeof value.task.status === 'string' && Array.isArray(value.commands) && value.commands.every((command) => isRecord(command) && typeof command.commandKey === 'string' && isRecord(command.payload)) && isRecord(value.polling) && Number.isFinite(value.polling.intervalMs) && Number.isFinite(value.polling.timeoutMs);
625
+ return isRecord(value) && isRecord(value.task) && typeof value.task.id === 'string' && typeof value.task.documentType === 'string' && typeof value.task.status === 'string';
568
626
  }
569
627
  function delay(milliseconds) {
570
628
  return new Promise((resolve) => setTimeout(resolve, milliseconds));