@chenchaolong/plugin-trade-compliance-workbench 0.1.7 → 0.1.9

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.
@@ -74,6 +74,7 @@
74
74
  const [query, setQuery] = React.useState('')
75
75
  const [showManualForm, setShowManualForm] = React.useState(false)
76
76
  const [form, setForm] = React.useState({ productName: '', hsCode: '', keywords: '', controlNote: '', enabled: true })
77
+ const pollingRef = React.useRef(null)
77
78
 
78
79
  React.useEffect(() => {
79
80
  window.__tradeComplianceSetContext = (nextContext) => {
@@ -86,6 +87,7 @@
86
87
  return () => {
87
88
  window.__tradeComplianceSetContext = null
88
89
  window.__tradeComplianceHostEvent = null
90
+ stopRecognitionPolling()
89
91
  }
90
92
  }, [])
91
93
 
@@ -123,6 +125,8 @@
123
125
 
124
126
  async function handleFile(actionKey, file) {
125
127
  if (!file || busy) return
128
+ const expectedType = reviewTypeForUploadAction(actionKey)
129
+ const previousCount = countReviewItemsByType(data.reviewItems, expectedType)
126
130
  setBusy(true)
127
131
  try {
128
132
  const response = await executeFileAction(actionKey, file, { name: file.name, fileName: file.name }, {})
@@ -130,6 +134,7 @@
130
134
  const dispatched = await dispatchAssistantCommands(getActionDataPayload(response))
131
135
  showNotice(dispatched ? '文件已发送给智能体解析,识别结果会进入待审核列表。' : '文件已登记,等待识别结果。', 'success')
132
136
  await reload()
137
+ if (dispatched && expectedType) startRecognitionPolling(expectedType, previousCount)
133
138
  } catch (error) {
134
139
  showNotice(getErrorMessage(error), 'error')
135
140
  } finally {
@@ -239,6 +244,30 @@
239
244
  setTimeout(() => setNotice(''), 4500)
240
245
  }
241
246
 
247
+ function startRecognitionPolling(expectedType, previousCount) {
248
+ stopRecognitionPolling()
249
+ let attempts = 0
250
+ pollingRef.current = setInterval(async () => {
251
+ attempts += 1
252
+ try {
253
+ const response = await request('requestData', { query: { page: 1, pageSize: 200, search: query, parameters: {} } })
254
+ const nextData = normalizeData(getResponsePayload(response))
255
+ setData(nextData)
256
+ if (countReviewItemsByType(nextData.reviewItems, expectedType) > previousCount || attempts >= 36) {
257
+ stopRecognitionPolling()
258
+ }
259
+ } catch (_error) {
260
+ if (attempts >= 36) stopRecognitionPolling()
261
+ }
262
+ }, 2500)
263
+ }
264
+
265
+ function stopRecognitionPolling() {
266
+ if (!pollingRef.current) return
267
+ clearInterval(pollingRef.current)
268
+ pollingRef.current = null
269
+ }
270
+
242
271
  return h('div', { className: 'tcw-shell' },
243
272
  h('header', { className: 'tcw-header' },
244
273
  h('div', { className: 'tcw-title-block' },
@@ -292,12 +321,14 @@
292
321
 
293
322
  function renderControlledGoodsPage() {
294
323
  return h('div', { className: 'tcw-page controlled-goods-page' },
295
- pageToolbar('管控商品', '智能体识别结果先进入待审核,确认后写入正式管控商品库。', [
324
+ businessPanel('管控商品', '上传管控目录后,识别结果在列表内完成审核、驳回和入库。', [
296
325
  uploadButton('upload_controlled_goods_file', '上传管控商品文件'),
297
326
  h('button', { className: 'tcw-btn tcw-btn-soft', onClick: () => setShowManualForm(!showManualForm) }, showManualForm ? '收起新增' : '手动新增')
298
- ]),
299
- showManualForm ? renderControlledGoodsForm() : null,
300
- renderListPanel('管控商品列表', 'pending-controlled-goods confirmed-controlled-goods', controlledReviews, ['商品名称', '海关编码', '关键词', '管控说明', '来源'], (item) => {
327
+ ], [
328
+ showManualForm ? renderControlledGoodsForm() : null,
329
+ renderListToolbar('管控商品列表', controlledReviews.length),
330
+ h('div', { className: 'pending-controlled-goods confirmed-controlled-goods tcw-table-section' },
331
+ reviewTable(['商品名称', '海关编码', '关键词', '管控说明', '来源'], controlledReviews, (item) => {
301
332
  const row = readMerged(item)
302
333
  return [
303
334
  value(row.productName || item.title),
@@ -306,7 +337,9 @@
306
337
  value(row.controlNote),
307
338
  value(item.sourceLocation || row.sourceFileName)
308
339
  ]
309
- }, '暂无管控商品识别结果。上传目录并等待智能体调用工具后会显示在这里。')
340
+ }, '暂无管控商品识别结果。上传目录并等待智能体调用工具后会显示在这里。')
341
+ )
342
+ ])
310
343
  )
311
344
  }
312
345
 
@@ -429,7 +462,26 @@
429
462
  }
430
463
 
431
464
  function reviewTable(headers, rows, mapRow, emptyText) {
432
- return table(['', ...headers, '状态', '操作'], rows, (row) => [
465
+ const selectableIds = getSelectableRowIds(rows)
466
+ const selectedVisibleIds = selectableIds.filter((id) => selectedIds.includes(id))
467
+ const allVisibleSelected = selectableIds.length > 0 && selectedVisibleIds.length === selectableIds.length
468
+ const partiallySelected = selectedVisibleIds.length > 0 && selectedVisibleIds.length < selectableIds.length
469
+ return table([
470
+ h('input', {
471
+ className: 'row-selection-checkbox',
472
+ type: 'checkbox',
473
+ 'aria-label': '全选当前列表',
474
+ checked: allVisibleSelected,
475
+ disabled: selectableIds.length === 0,
476
+ ref: (node) => {
477
+ if (node) node.indeterminate = partiallySelected
478
+ },
479
+ onChange: (event) => toggleAllVisibleRows(rows, event.target.checked)
480
+ }),
481
+ ...headers,
482
+ '状态',
483
+ '操作'
484
+ ], rows, (row) => [
433
485
  h('input', {
434
486
  className: 'row-selection-checkbox',
435
487
  type: 'checkbox',
@@ -451,6 +503,21 @@
451
503
  setSelectedIds(checked ? Array.from(new Set([...selectedIds, id])) : selectedIds.filter((item) => item !== id))
452
504
  }
453
505
 
506
+ function toggleAllVisibleRows(rows, checked) {
507
+ const visibleIds = getSelectableRowIds(rows)
508
+ if (checked) {
509
+ setSelectedIds(Array.from(new Set([...selectedIds, ...visibleIds])))
510
+ return
511
+ }
512
+ setSelectedIds(selectedIds.filter((id) => !visibleIds.includes(id)))
513
+ }
514
+
515
+ function getSelectableRowIds(rows) {
516
+ return (rows || [])
517
+ .filter((row) => row.id && !row.materializedOnly && row.reviewStatus !== 'confirmed')
518
+ .map((row) => row.id)
519
+ }
520
+
454
521
  function pageToolbar(title, subtitle, actions) {
455
522
  return h('div', { className: 'tcw-page-head' },
456
523
  h('div', null, h('h2', null, title), h('p', null, subtitle)),
@@ -494,7 +561,7 @@
494
561
  if (!rows || rows.length === 0) return empty(emptyText || '暂无数据。')
495
562
  return h('div', { className: 'tcw-table-wrap' },
496
563
  h('table', { className: 'tcw-table' },
497
- h('thead', null, h('tr', null, headers.map((item) => h('th', { key: item }, item)))),
564
+ h('thead', null, h('tr', null, headers.map((item, index) => h('th', { key: index }, item)))),
498
565
  h('tbody', null, rows.map((row, index) =>
499
566
  h('tr', { key: row.id || index, className: onRowClick ? 'clickable' : '', onClick: onRowClick ? () => onRowClick(row) : undefined },
500
567
  mapRow(row).map((cell, cellIndex) => h('td', { key: cellIndex }, cell))
@@ -626,6 +693,18 @@
626
693
  return items.filter((item) => (item.reviewStatus || 'pending') === statusFilter)
627
694
  }
628
695
 
696
+ function reviewTypeForUploadAction(actionKey) {
697
+ if (actionKey === 'upload_controlled_goods_file') return 'controlled_goods'
698
+ if (actionKey === 'upload_supplier_contract') return 'supplier_product'
699
+ if (actionKey === 'upload_sales_contract') return 'customs_workbook'
700
+ return null
701
+ }
702
+
703
+ function countReviewItemsByType(items, type) {
704
+ if (!type || !Array.isArray(items)) return 0
705
+ return items.filter((item) => item.type === type && !isPlaceholderReviewItem(item)).length
706
+ }
707
+
629
708
  function isPlaceholderReviewItem(item) {
630
709
  const merged = readMerged(item || {})
631
710
  if (!item || !item.type) return true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chenchaolong/plugin-trade-compliance-workbench",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Trade Compliance Workbench app plugin for controlled goods review, supplier product management, and customs workbook generation.",
5
5
  "author": {
6
6
  "name": "XpertAI",