@chenchaolong/plugin-trade-compliance-workbench 1.0.6 → 1.0.8

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 +462 -176
  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 +4 -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 +6 -0
  124. package/dist/lib/remote-ui/pages/analytics.d.ts.map +1 -1
  125. package/dist/lib/remote-ui/pages/analytics.js +56 -6
  126. package/dist/lib/remote-ui/pages/analytics.js.map +1 -1
  127. package/dist/lib/remote-ui/pages/catalogs.d.ts +35 -14
  128. package/dist/lib/remote-ui/pages/catalogs.d.ts.map +1 -1
  129. package/dist/lib/remote-ui/pages/catalogs.js +139 -89
  130. package/dist/lib/remote-ui/pages/catalogs.js.map +1 -1
  131. package/dist/lib/remote-ui/pages/contracts.d.ts +10 -5
  132. package/dist/lib/remote-ui/pages/contracts.d.ts.map +1 -1
  133. package/dist/lib/remote-ui/pages/contracts.js +29 -26
  134. package/dist/lib/remote-ui/pages/contracts.js.map +1 -1
  135. package/dist/lib/remote-ui/pages/files.d.ts +55 -4
  136. package/dist/lib/remote-ui/pages/files.d.ts.map +1 -1
  137. package/dist/lib/remote-ui/pages/files.js +121 -14
  138. package/dist/lib/remote-ui/pages/files.js.map +1 -1
  139. package/dist/lib/remote-ui/pages/home.js +2 -2
  140. package/dist/lib/remote-ui/pages/home.js.map +1 -1
  141. package/dist/lib/remote-ui/pages/procurement.d.ts +6 -4
  142. package/dist/lib/remote-ui/pages/procurement.d.ts.map +1 -1
  143. package/dist/lib/remote-ui/pages/procurement.js +69 -66
  144. package/dist/lib/remote-ui/pages/procurement.js.map +1 -1
  145. package/dist/lib/remote-ui/pages/sales.d.ts +51 -2
  146. package/dist/lib/remote-ui/pages/sales.d.ts.map +1 -1
  147. package/dist/lib/remote-ui/pages/sales.js +106 -19
  148. package/dist/lib/remote-ui/pages/sales.js.map +1 -1
  149. package/dist/lib/remote-ui/pages/settings.d.ts +29 -14
  150. package/dist/lib/remote-ui/pages/settings.d.ts.map +1 -1
  151. package/dist/lib/remote-ui/pages/settings.js +76 -69
  152. package/dist/lib/remote-ui/pages/settings.js.map +1 -1
  153. package/dist/lib/remote-ui/polling.d.ts +41 -0
  154. package/dist/lib/remote-ui/polling.d.ts.map +1 -0
  155. package/dist/lib/remote-ui/polling.js +148 -0
  156. package/dist/lib/remote-ui/polling.js.map +1 -0
  157. package/dist/lib/remote-ui/procurement-contracts.d.ts +4 -9
  158. package/dist/lib/remote-ui/procurement-contracts.d.ts.map +1 -1
  159. package/dist/lib/remote-ui/sales-contracts.d.ts +5 -3
  160. package/dist/lib/remote-ui/sales-contracts.d.ts.map +1 -1
  161. package/dist/lib/remote-ui/settings-contracts.d.ts +9 -1
  162. package/dist/lib/remote-ui/settings-contracts.d.ts.map +1 -1
  163. package/dist/lib/remote-ui/settings-contracts.js.map +1 -1
  164. package/dist/lib/remote-ui/toast-queue.d.ts +29 -0
  165. package/dist/lib/remote-ui/toast-queue.d.ts.map +1 -0
  166. package/dist/lib/remote-ui/toast-queue.js +71 -0
  167. package/dist/lib/remote-ui/toast-queue.js.map +1 -0
  168. package/dist/lib/sales-file.service.d.ts +13 -35
  169. package/dist/lib/sales-file.service.d.ts.map +1 -1
  170. package/dist/lib/sales-file.service.js +39 -21
  171. package/dist/lib/sales-file.service.js.map +1 -1
  172. package/dist/lib/types.d.ts +22 -3
  173. package/dist/lib/types.d.ts.map +1 -1
  174. package/dist/lib/types.js +5 -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 +104 -94
  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,8 +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';
6
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';
7
12
  const PAGE_SIZE = 20;
8
13
  const ACCEPTED_CATALOG_FILES = '.pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.webp';
9
14
  const formatNotice = () => `支持 PDF、DOC、DOCX、XLS、XLSX、PNG、JPEG、WEBP,单文件最大 ${getConfiguredUploadPolicy().maxUploadMb} MB。`;
@@ -45,7 +50,6 @@ export function createCatalogActionApi({ onAction, onFileAction, onRefresh }) {
45
50
  const mutate = async (actionKey, input) => {
46
51
  const response = await onAction(actionKey, input);
47
52
  assertActionSucceeded(response);
48
- await onRefresh();
49
53
  return response;
50
54
  };
51
55
  const mutateFile = async (actionKey, file, input) => {
@@ -80,26 +84,22 @@ export class CatalogImportError extends Error {
80
84
  this.name = 'CatalogImportError';
81
85
  }
82
86
  }
83
- 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 }) {
84
88
  assertActionSucceeded(start);
85
89
  if (!isCatalogImportStartData(start.data))
86
90
  throw new Error('上传响应缺少解析任务信息');
87
- if (!start.data.task.commandDispatchedAt) {
88
- let marked = false;
89
- for (const command of start.data.commands) {
90
- await invokeClientCommand(command.commandKey, command.payload);
91
- if (!marked) {
92
- await markCommandDispatched(start.data.task.id);
93
- marked = true;
94
- }
95
- }
96
- }
97
- const timeoutMs = Math.min(MAX_IMPORT_TIMEOUT_MS, Math.max(1, start.data.polling.timeoutMs));
98
- const intervalMs = Math.max(1, start.data.polling.intervalMs);
99
- const deadline = now() + timeoutMs;
100
- while (now() < deadline) {
101
- await wait(intervalMs);
102
- 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;
103
103
  const status = completion.task.status;
104
104
  if (status === 'SUCCEEDED') {
105
105
  if (!completion.batch)
@@ -112,13 +112,16 @@ export async function runCatalogImportOrchestration({ start, invokeClientCommand
112
112
  if (!ACTIVE_IMPORT_STATUSES.has(status))
113
113
  throw new Error(`不支持的导入任务状态:${status}`);
114
114
  }
115
- throw new CatalogImportError('目录解析超过 20 分钟,请重新解析', { ...start.data.task, status: 'TIMED_OUT', errorMessage: '目录解析超过 20 分钟,请重新解析' });
115
+ throw observation.expired('目录任务观察窗口已结束', previous);
116
116
  }
117
117
  export function findActiveCatalogImportTask(items) {
118
118
  return items
119
- .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))
120
120
  .sort((left, right) => String(right.updatedAt ?? '').localeCompare(String(left.updatedAt ?? '')))[0] ?? null;
121
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
+ }
122
125
  export function findLatestFailedCatalogImportTask(items, documentType) {
123
126
  const retriedTaskIds = new Set(items.map((task) => task.retryOfTaskId).filter((id) => Boolean(id)));
124
127
  return items
@@ -129,17 +132,19 @@ export function createCatalogQueryScheduler(query, debounceMs = 300) {
129
132
  let timer = null;
130
133
  const cancel = () => { if (timer)
131
134
  clearTimeout(timer); timer = null; };
132
- const flush = (filters) => { cancel(); query(filters); };
135
+ const run = (page, filters, pageSize = PAGE_SIZE) => { cancel(); query(page, filters, pageSize); };
133
136
  return {
134
- schedule(type, filters) {
137
+ schedule(type, filters, pageSize = PAGE_SIZE) {
135
138
  cancel();
136
139
  if (type !== 'text') {
137
- query(filters);
140
+ run(1, filters, pageSize);
138
141
  return;
139
142
  }
140
- timer = setTimeout(() => { timer = null; query(filters); }, debounceMs);
143
+ timer = setTimeout(() => { timer = null; query(1, filters, pageSize); }, debounceMs);
141
144
  },
142
- 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),
143
148
  cancel
144
149
  };
145
150
  }
@@ -217,41 +222,47 @@ export function ControlledGoodsPage(props) {
217
222
  if (props.command)
218
223
  props.onCommandHandled?.();
219
224
  }, [props.command, props.onCommandHandled]);
220
- const query = React.useCallback((page, nextFilters) => props.onQuery({ page, pageSize: PAGE_SIZE, filters: nextFilters }), [props.onQuery]);
221
- 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]);
222
227
  React.useEffect(() => () => scheduler.cancel(), [scheduler]);
223
228
  React.useEffect(() => { issueControllerRef.current?.mount(); return () => issueControllerRef.current?.dispose(); }, []);
224
229
  const changeFilter = (key, value) => {
225
230
  const nextFilters = { ...filters, [key]: value };
226
231
  setFilters(nextFilters);
227
- 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);
228
233
  };
229
- const queryIssues = async (batchId, issueType, page = 1) => {
234
+ const queryIssues = async (batchId, issueType, page = 1, pageSize = PAGE_SIZE) => {
230
235
  if (!props.onIssueQuery)
231
236
  return;
232
- 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 });
233
238
  };
234
239
  const displayedSummary = summary ?? (props.importCompletion?.task.documentType === 'CONTROLLED_CATALOG' ? props.importCompletion.batch ?? null : null);
235
240
  const columns = [
236
- { 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 || '—' }) },
237
- { 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 }) : '—' },
238
- { 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, { iconClass: "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, { iconClass: "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) },
239
244
  { type: 'ellipsis', key: 'controlDescription', header: '管控说明', width: 220, maxWidth: 220 },
240
- { 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) },
241
246
  { type: 'ellipsis', key: 'sourceFileName', header: '来源文件', width: 190, maxWidth: 190 },
242
- { type: 'text', key: 'updatedAt', header: '更新时间', width: 170 }
247
+ { type: 'text', key: 'updatedAt', header: '更新时间', width: 170, truncate: false, format: formatBeijingDateTime }
243
248
  ];
244
- 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), 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", 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.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) => {
245
250
  const response = await api.uploadControlledCatalog(file, input);
246
251
  const completion = asImportCompletion(response.data);
247
252
  if (completion.batch)
248
253
  setSummary(completion.batch);
249
254
  setUploadOpen(false);
250
- } }), _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)
251
- 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)
252
262
  void queryIssues(issueState.batchId, issueType); }, onPageChange: (page) => { if (issueState)
253
- void queryIssues(issueState.batchId, issueState.issueType, page); }, onRetry: () => { if (issueState)
254
- 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); } })] }));
255
266
  }
256
267
  export function SanctionedCompaniesPage(props) {
257
268
  const [filters, setFilters] = React.useState({});
@@ -274,47 +285,53 @@ export function SanctionedCompaniesPage(props) {
274
285
  if (props.command)
275
286
  props.onCommandHandled?.();
276
287
  }, [props.command, props.onCommandHandled]);
277
- const query = React.useCallback((page, nextFilters) => props.onQuery({ page, pageSize: PAGE_SIZE, filters: nextFilters }), [props.onQuery]);
278
- 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]);
279
290
  React.useEffect(() => () => scheduler.cancel(), [scheduler]);
280
291
  React.useEffect(() => { issueControllerRef.current?.mount(); return () => issueControllerRef.current?.dispose(); }, []);
281
292
  const changeFilter = (key, value) => {
282
293
  const nextFilters = { ...filters, [key]: value };
283
294
  setFilters(nextFilters);
284
- 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);
285
296
  };
286
- const queryIssues = async (batchId, issueType, page = 1) => {
297
+ const queryIssues = async (batchId, issueType, page = 1, pageSize = PAGE_SIZE) => {
287
298
  if (!props.onIssueQuery)
288
299
  return;
289
- 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 });
290
301
  };
291
302
  const displayedSummary = summary ?? (props.importCompletion?.task.documentType === 'SANCTION_CATALOG' ? props.importCompletion.batch ?? null : null);
292
303
  const columns = [
293
- { 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, { iconClass: "ri-eye-line", title: row.companyName || '—', ariaLabel: `查看制裁公司详情:${row.companyName || '—'}`, onClick: () => setForm({ mode: 'detail', item: row }), children: row.companyName || '—' }) },
294
305
  { type: 'text', key: 'countryOrRegion', header: '国家或地区', width: 120 },
295
306
  { type: 'ellipsis', key: 'sanctionListSource', header: '制裁名单来源', width: 190, maxWidth: 190 },
296
- { type: 'text', key: 'effectiveDate', header: '生效日期', width: 120 },
307
+ { type: 'text', key: 'effectiveDate', header: '生效日期', width: 120, truncate: false, format: formatBusinessDate },
297
308
  { type: 'ellipsis', key: 'sanctionReason', header: '制裁原因', width: 220, maxWidth: 220 },
298
- { 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) },
299
310
  { type: 'ellipsis', key: 'sourceFileName', header: '来源文件', width: 190, maxWidth: 190 },
300
- { type: 'text', key: 'updatedAt', header: '更新时间', width: 170 }
311
+ { type: 'text', key: 'updatedAt', header: '更新时间', width: 170, truncate: false, format: formatBeijingDateTime }
301
312
  ];
302
- 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), 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", 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.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) => {
303
314
  const response = await api.uploadSanctionCatalog(file, input);
304
315
  const completion = asImportCompletion(response.data);
305
316
  if (completion.batch)
306
317
  setSummary(completion.batch);
307
318
  setUploadOpen(false);
308
- } }), _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)
309
- 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)
310
326
  void queryIssues(issueState.batchId, issueType); }, onPageChange: (page) => { if (issueState)
311
- void queryIssues(issueState.batchId, issueState.issueType, page); }, onRetry: () => { if (issueState)
312
- 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); } })] }));
313
330
  }
314
331
  export function CatalogCommands({ busy, onUpload, onCreate }) {
315
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"] })] });
316
333
  }
317
- export function CatalogUploadDialog({ open, kind, batches, busy = false, onClose, onUpload }) {
334
+ export function CatalogUploadDialog({ open, kind, batches, busy = false, onClose, onUpload, onNotify }) {
318
335
  const [state, dispatch] = React.useReducer(catalogUploadReducer, initialCatalogUploadState);
319
336
  const disabled = busy || state.submitting;
320
337
  React.useEffect(() => {
@@ -335,7 +352,7 @@ export function CatalogUploadDialog({ open, kind, batches, busy = false, onClose
335
352
  dispatch({ type: 'reset' });
336
353
  }
337
354
  catch (cause) {
338
- dispatch({ type: 'set-error', error: errorMessage(cause, '目录上传失败') });
355
+ onNotify?.({ message: errorMessage(cause, '目录上传失败'), tone: 'error' });
339
356
  dispatch({ type: 'cancel-replace' });
340
357
  }
341
358
  finally {
@@ -361,7 +378,10 @@ export function CatalogUploadDialog({ open, kind, batches, busy = false, onClose
361
378
  }
362
379
  export function CatalogReplaceConfirmDialog({ open, kind, batch, fileName, busy = false, onCancel, onConfirm }) {
363
380
  const typeLabel = batch?.catalogType ? catalogTypeLabels[batch.catalogType] : '公司制裁目录';
364
- 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: batch?.updatedAt || batch?.createdAt || '—' }), _jsx("dt", { children: "\u65B0\u4E0A\u4F20\u6587\u4EF6\uFF1A" }), _jsx("dd", { children: fileName || '—' })] })] }) }));
381
+ const warningMessage = kind === 'controlled'
382
+ ? '覆盖后,新文件只有在解析成功并通过基础校验后,旧批次及其文件导入商品才会被逻辑删除。'
383
+ : '只有新文件解析成功并通过基础校验后,旧批次及其公司记录才会被逻辑删除。';
384
+ 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: [_jsx("div", { className: "tcw-catalog-replace-warning tcw-notice--risk", role: "alert", children: _jsx("span", { children: warningMessage }) }), _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 || '—' })] })] }) }));
365
385
  }
366
386
  export function ControlledGoodsFormDialog({ open, mode, item, busy = false, onClose, onSave }) {
367
387
  const [form, setForm] = React.useState(() => controlledForm(item));
@@ -396,7 +416,7 @@ export function SanctionedCompanyDialog({ open, mode, item, busy = false, onClos
396
416
  setForm(sanctionedForm(item));
397
417
  setError('');
398
418
  } }, [open, item]);
399
- const editor = _jsx(SanctionedCompanyFields, { value: form, disabled: mode === 'detail', onChange: setForm });
419
+ const editor = _jsx(SanctionedCompanyFields, { value: form, disabled: mode === 'detail', singleLineReason: mode === 'create', onChange: setForm });
400
420
  const save = async () => {
401
421
  const validationError = validateSanctionedCompanyInput(form);
402
422
  if (validationError) {
@@ -415,7 +435,7 @@ export function SanctionedCompanyDialog({ open, mode, item, busy = false, onClos
415
435
  ? _jsxs("button", { type: "button", className: "tcw-button tcw-button--primary", onClick: onClose, children: [_jsx("i", { className: "ri-close-line", "aria-hidden": "true" }), "\u5173\u95ED"] })
416
436
  : _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"] })] });
417
437
  const withEvidence = mode !== 'create' && item?.sourceType === 'FILE_IMPORT';
418
- 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 })] }) }) }));
438
+ 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 }), editorTitle: "\u7F16\u8F91\u4FE1\u606F", sourceTitle: "\u6765\u6E90\u8BC1\u636E\uFF08\u53EA\u8BFB\uFF09" }) : _jsxs(_Fragment, { children: [mode === 'create' && _jsx(CatalogManualSource, {}), editor, error && _jsx("p", { role: "alert", children: error })] }) }) }));
419
439
  }
420
440
  export function validateSanctionedCompanyInput(input) {
421
441
  if (!input.companyName.trim() || !input.sanctionListSource?.trim() || !input.effectiveDate?.trim() || !input.sanctionReason?.trim()) {
@@ -423,43 +443,50 @@ export function validateSanctionedCompanyInput(input) {
423
443
  }
424
444
  return '';
425
445
  }
426
- function SanctionedCompanyFields({ value, disabled, onChange }) {
427
- 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"] })] }) })] });
446
+ function SanctionedCompanyFields({ value, disabled, singleLineReason, onChange }) {
447
+ 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: singleLineReason ? _jsx("input", { disabled: disabled, maxLength: 500, placeholder: "\u8BF7\u8F93\u5165\u5236\u88C1\u539F\u56E0", value: value.sanctionReason ?? '', onChange: (event) => onChange({ ...value, sanctionReason: event.target.value }) }) : _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"] })] }) })] });
428
448
  }
429
449
  function CatalogManualSource() {
430
450
  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" })] });
431
451
  }
432
452
  function SourceEvidence({ item }) {
433
- 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 ?? '—' }) })] });
453
+ 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: 5, value: item?.sourceContent ?? '—' }) })] });
434
454
  }
435
455
  export function CatalogImportSummaryNotice({ documentType, batch, busy, onDownload, onView }) {
436
456
  const issueCount = (batch.duplicateCount ?? 0) + (batch.invalidCount ?? 0);
457
+ if (issueCount === 0)
458
+ return null;
437
459
  const initialIssueType = (batch.duplicateCount ?? 0) > 0 ? 'DUPLICATE' : 'INVALID';
438
- 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"] })] })] });
460
+ 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"] })] });
439
461
  }
440
462
  export function CatalogImportStatusNotice({ task, busy, onRetry }) {
441
463
  if (task.status !== 'FAILED' && task.status !== 'TIMED_OUT')
442
464
  return null;
443
465
  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"] })] });
444
466
  }
445
- export function CatalogImportIssuesDialog({ open, documentType, batchId, issueType, collection, loading, error, busy, onClose, onIssueTypeChange, onPageChange, onRetry }) {
467
+ export function CatalogImportIssuesDialog({ open, documentType, batchId, issueType, collection, loading, error, busy, onClose, onIssueTypeChange, onPageChange, onPageSizeChange, onRetry }) {
446
468
  const columns = [
447
- { type: 'text', key: 'issueType', header: '问题类型', width: 100, format: (value) => value === 'DUPLICATE' ? '重复' : '无效' },
448
- { type: 'text', key: 'sourceSequence', header: '来源序号', width: 100 },
469
+ { type: 'text', key: 'issueType', header: '问题类型', width: 100, truncate: false, format: (value) => value === 'DUPLICATE' ? '重复' : '无效' },
470
+ { type: 'text', key: 'sourceSequence', header: '来源序号', width: 100, truncate: false },
449
471
  { type: 'ellipsis', key: 'sourceLocation', header: '来源位置', width: 160, maxWidth: 160 },
450
- { type: 'ellipsis', key: 'sourceContent', header: '来源原文', width: 260, maxWidth: 260 },
451
472
  { type: 'ellipsis', key: 'message', header: '问题说明', width: 220, maxWidth: 220 }
452
473
  ];
453
- 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' ? '暂无重复数据' : '暂无无效数据' })] });
474
+ 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' ? '暂无重复数据' : '暂无无效数据' })] });
475
+ }
476
+ export function hsSearchPageSizeRequest(keywords, pageSize) {
477
+ return { keywords: keywords.trim(), page: 1, pageSize };
478
+ }
479
+ function emptyHsSearchData(page = 1, pageSize = 20) {
480
+ return { items: [], total: 0, page, pageSize, pageCount: 1 };
454
481
  }
455
482
  export function HsCodeLookupDialog({ open, initialQuery, onAction, onClose }) {
456
483
  const [query, setQuery] = React.useState(initialQuery);
457
- const [page, setPage] = React.useState(1);
458
- const [results, setResults] = React.useState([]);
484
+ const [collection, setCollection] = React.useState(() => emptyHsSearchData());
459
485
  const [detail, setDetail] = React.useState(null);
460
486
  const [error, setError] = React.useState('');
461
487
  const [loading, setLoading] = React.useState(false);
462
- const search = React.useCallback(async (keywords, nextPage = 1) => {
488
+ const hsRequestGateRef = React.useRef(createLatestRequestGate());
489
+ const search = React.useCallback(async (keywords, nextPage = 1, pageSize = 20) => {
463
490
  const normalized = keywords.trim();
464
491
  if (!normalized) {
465
492
  setError('请输入商品名称或海关编码');
@@ -468,56 +495,73 @@ export function HsCodeLookupDialog({ open, initialQuery, onAction, onClose }) {
468
495
  setLoading(true);
469
496
  setError('');
470
497
  setDetail(null);
498
+ const requestId = hsRequestGateRef.current.begin();
471
499
  try {
472
- const response = await onAction('search_hs_code', { keywords: normalized, page: nextPage });
500
+ const response = await onAction('search_hs_code', { keywords: normalized, page: nextPage, pageSize });
473
501
  if (response.success === false)
474
502
  throw new Error(response.message || '海关编码查询失败');
475
- const data = isHsSearchData(response.data) ? response.data : { results: [] };
476
- setResults(data.results);
477
- setPage(nextPage);
503
+ if (!hsRequestGateRef.current.commit(requestId))
504
+ return;
505
+ setCollection(isHsSearchData(response.data) ? response.data : emptyHsSearchData(nextPage, pageSize));
478
506
  }
479
507
  catch (cause) {
508
+ if (!hsRequestGateRef.current.isLatest(requestId))
509
+ return;
480
510
  setError(errorMessage(cause, '海关编码查询失败'));
481
- setResults([]);
511
+ setCollection(emptyHsSearchData(nextPage, pageSize));
482
512
  }
483
513
  finally {
484
- setLoading(false);
514
+ hsRequestGateRef.current.finish(requestId);
515
+ if (hsRequestGateRef.current.isLatest(requestId))
516
+ setLoading(false);
485
517
  }
486
518
  }, [onAction]);
487
519
  React.useEffect(() => {
488
- if (!open)
520
+ if (!open) {
521
+ hsRequestGateRef.current.invalidate();
522
+ setLoading(false);
489
523
  return;
524
+ }
490
525
  setQuery(initialQuery);
491
- setPage(1);
526
+ setCollection(emptyHsSearchData());
492
527
  void search(initialQuery, 1);
528
+ return () => hsRequestGateRef.current.invalidate();
493
529
  }, [open, initialQuery, search]);
494
530
  const showDetail = async (result) => {
495
531
  setLoading(true);
496
532
  setError('');
533
+ const requestId = hsRequestGateRef.current.begin();
497
534
  try {
498
535
  const response = await onAction('get_hs_code_detail', { code: result.code, ...(result.detailUrl ? { detailUrl: result.detailUrl } : {}) });
499
536
  if (response.success === false)
500
537
  throw new Error(response.message || '海关编码详情查询失败');
538
+ if (!hsRequestGateRef.current.commit(requestId))
539
+ return;
501
540
  setDetail(isRecord(response.data) ? response.data : {});
502
541
  }
503
542
  catch (cause) {
543
+ if (!hsRequestGateRef.current.isLatest(requestId))
544
+ return;
504
545
  setError(errorMessage(cause, '海关编码详情查询失败'));
505
546
  }
506
547
  finally {
507
- setLoading(false);
548
+ hsRequestGateRef.current.finish(requestId);
549
+ if (hsRequestGateRef.current.isLatest(requestId))
550
+ setLoading(false);
508
551
  }
509
552
  };
553
+ const close = () => { hsRequestGateRef.current.invalidate(); setLoading(false); onClose(); };
510
554
  const columns = [
511
555
  { type: 'text', key: 'code', header: '海关编码', width: 130 },
512
556
  { type: 'ellipsis', key: 'name', header: '商品名称', width: 220, maxWidth: 220 },
513
557
  { type: 'ellipsis', key: 'englishName', header: '英文名称', width: 220, maxWidth: 220 },
514
- { type: 'text', key: 'unit', header: '单位', width: 90 },
515
- { type: 'text', key: 'taxRefundRate', header: '退税率', width: 90 }
558
+ { type: 'text', key: 'unit', header: '单位', width: 90, truncate: false },
559
+ { type: 'text', key: 'taxRefundRate', header: '退税率', width: 90, truncate: false }
516
560
  ];
517
- 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"] })] })] })] }));
561
+ 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 ? '查询中' : '暂无海关编码结果' }) })] }));
518
562
  }
519
563
  function HsCodeDetail({ detail, onBack }) {
520
- 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}`))] });
564
+ 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}`))] });
521
565
  }
522
566
  export function hsLookupQuery(row, field) {
523
567
  return String(row[field] ?? '').trim();
@@ -540,8 +584,8 @@ function sanctionedForm(item) {
540
584
  remarks: item?.remarks ?? ''
541
585
  };
542
586
  }
543
- function LinkButton({ children, icon, title, onClick }) {
544
- return _jsx("span", { className: "tcw-row-actions tcw-catalog-table-link", children: _jsxs("button", { type: "button", title: title, onClick: onClick, children: [_jsx("i", { className: icon, "aria-hidden": "true" }), children] }) });
587
+ function LinkButton({ children, iconClass, title, ariaLabel, onClick }) {
588
+ 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: iconClass, "aria-hidden": "true" }), _jsx("span", { children: children })] }) });
545
589
  }
546
590
  function options(labels) {
547
591
  return Object.entries(labels).map(([value, label]) => ({ value, label: String(label) }));
@@ -550,7 +594,13 @@ function labelFor(value, labels) {
550
594
  return typeof value === 'string' && value in labels ? labels[value] : value ? String(value) : '—';
551
595
  }
552
596
  function isHsSearchData(value) {
553
- return isRecord(value) && Array.isArray(value.results) && value.results.every((item) => isRecord(item) && typeof item.code === 'string');
597
+ return isRecord(value)
598
+ && Array.isArray(value.items)
599
+ && value.items.every((item) => isRecord(item) && typeof item.code === 'string')
600
+ && Number.isInteger(value.total) && Number(value.total) >= 0
601
+ && Number.isInteger(value.page) && Number(value.page) >= 1
602
+ && isPaginationPageSize(Number(value.pageSize))
603
+ && Number.isInteger(value.pageCount) && Number(value.pageCount) >= 1;
554
604
  }
555
605
  function isRecord(value) {
556
606
  return Boolean(value && typeof value === 'object' && !Array.isArray(value));
@@ -563,7 +613,7 @@ function assertActionSucceeded(response) {
563
613
  throw new Error(typeof response.message === 'string' && response.message ? response.message : '操作失败');
564
614
  }
565
615
  function collectionPagination(collection) {
566
- const pageSize = Number(collection.pageSize) > 0 ? Number(collection.pageSize) : PAGE_SIZE;
616
+ const pageSize = collection.pageSize ?? PAGE_SIZE;
567
617
  const total = Number(collection.total) >= 0 ? Number(collection.total) : collection.items.length;
568
618
  const pageCount = Math.max(1, Math.ceil(total / pageSize));
569
619
  const page = Math.min(pageCount, Math.max(1, Number(collection.page) || 1));
@@ -575,7 +625,7 @@ function asImportCompletion(value) {
575
625
  return value;
576
626
  }
577
627
  function isCatalogImportStartData(value) {
578
- 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);
628
+ return isRecord(value) && isRecord(value.task) && typeof value.task.id === 'string' && typeof value.task.documentType === 'string' && typeof value.task.status === 'string';
579
629
  }
580
630
  function delay(milliseconds) {
581
631
  return new Promise((resolve) => setTimeout(resolve, milliseconds));