@chenchaolong/plugin-trade-compliance-workbench 0.1.6 → 0.1.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.
@@ -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' },
@@ -311,12 +340,13 @@
311
340
  }
312
341
 
313
342
  function renderProductsPage() {
314
- const groups = groupBy(filteredProducts, (item) => value(item.supplierName, '未识别供应商'))
315
343
  return h('div', { className: 'tcw-page products-page' },
316
- pageToolbar('供应商商品', '供应商合同识别结果先进入待审核,确认后按供应商归类入库。', [
344
+ businessPanel('供应商商品', '上传供应商合同后,识别结果在列表内完成审核、驳回和入库。', [
317
345
  uploadButton('upload_supplier_contract', '上传供应商合同')
318
- ]),
319
- renderListPanel('供应商商品列表', 'pending-supplier-products', supplierReviews, ['供应商', '商品', '型号', '海关编码', '退税率', '英文品名', '管控状态'], (item) => {
346
+ ], [
347
+ renderListToolbar('供应商商品列表', supplierReviews.length),
348
+ h('div', { className: 'pending-supplier-products tcw-table-section' },
349
+ reviewTable(['供应商', '商品', '型号', '海关编码', '退税率', '英文品名', '管控状态'], supplierReviews, (item) => {
320
350
  const row = readMerged(item)
321
351
  return [
322
352
  value(row.supplierName),
@@ -327,17 +357,21 @@
327
357
  value(row.englishName),
328
358
  status(controlStatusText(row.controlledStatus), controlStatusLevel(row.controlledStatus))
329
359
  ]
330
- }, '暂无供应商商品识别结果。上传供应商合同并等待智能体识别后会显示在这里。')
360
+ }, '暂无供应商商品识别结果。上传供应商合同并等待智能体识别后会显示在这里。')
361
+ )
362
+ ])
331
363
  )
332
364
  }
333
365
 
334
366
  function renderWorkbooksPage() {
335
367
  return h('div', { className: 'tcw-page workbooks-page' },
336
- pageToolbar('销售发票', '上传购销合同后审核发票字段,审核通过后生成固定模板 Excel。', [
368
+ businessPanel('销售发票', '上传购销合同后,先审核发票字段,再生成固定模板 Excel。', [
337
369
  uploadButton('upload_sales_contract', '上传购销合同'),
338
370
  h('button', { className: 'tcw-btn tcw-btn-primary', disabled: busy, onClick: generateWorkbook }, '生成销售发票')
339
- ]),
340
- renderListPanel('购销合同识别列表', 'pending-sales-contracts', salesReviews, ['文件/标题', '发票号', '合同号', '买方', '卖方'], (item) => {
371
+ ], [
372
+ renderListToolbar('购销合同识别列表', salesReviews.length),
373
+ h('div', { className: 'pending-sales-contracts tcw-table-section' },
374
+ reviewTable(['文件/标题', '发票号', '合同号', '买方', '卖方'], salesReviews, (item) => {
341
375
  const row = readMerged(item)
342
376
  return [
343
377
  value(item.title),
@@ -346,14 +380,21 @@
346
380
  value(row.buyerName),
347
381
  value(row.sellerName)
348
382
  ]
349
- }, '暂无购销合同识别结果。'),
350
- panel('已生成销售发票', table(['文件名', '发票号', '合同号', '模板工作表', '状态'], filteredWorkbooks, (item) => [
351
- value(item.fileName),
352
- value(item.invoiceNo),
353
- value(item.contractNo),
354
- Array.isArray(item.sheetNames) ? item.sheetNames.join('、') : '-',
355
- status(value(item.status, 'generated'), 'ok')
356
- ], '暂无销售发票生成历史。'))
383
+ }, '暂无购销合同识别结果。')
384
+ ),
385
+ h('section', { className: 'tcw-history-section' },
386
+ h('div', { className: 'tcw-subsection-head' },
387
+ h('div', null, h('h3', null, '已生成销售发票'), h('p', null, `${filteredWorkbooks.length} 条生成历史`))
388
+ ),
389
+ table(['文件名', '发票号', '合同号', '模板工作表', '状态'], filteredWorkbooks, (item) => [
390
+ value(item.fileName),
391
+ value(item.invoiceNo),
392
+ value(item.contractNo),
393
+ Array.isArray(item.sheetNames) ? item.sheetNames.join('、') : '-',
394
+ status(value(item.status, 'generated'), 'ok')
395
+ ], '暂无销售发票生成历史。')
396
+ )
397
+ ])
357
398
  )
358
399
  }
359
400
 
@@ -380,7 +421,28 @@
380
421
 
381
422
  function renderListPanel(title, className, rows, headers, mapRow, emptyText) {
382
423
  return panel(title, h('div', { className },
383
- h('div', { className: 'tcw-list-toolbar' },
424
+ renderListToolbar(null, rows.length),
425
+ reviewTable(headers, rows, mapRow, emptyText)
426
+ ))
427
+ }
428
+
429
+ function businessPanel(title, subtitle, actions, content) {
430
+ return h('section', { className: 'tcw-business-panel' },
431
+ h('div', { className: 'tcw-business-head' },
432
+ h('div', null, h('h2', null, title), h('p', null, subtitle)),
433
+ h('div', { className: 'tcw-button-row' }, actions)
434
+ ),
435
+ h('div', { className: 'tcw-business-body' }, content)
436
+ )
437
+ }
438
+
439
+ function renderListToolbar(title, total) {
440
+ return h('div', { className: 'tcw-list-toolbar' },
441
+ h('div', { className: 'tcw-list-title' },
442
+ title ? h('h3', null, title) : null,
443
+ h('span', null, `${total || 0} 条记录`)
444
+ ),
445
+ h('div', { className: 'tcw-list-actions' },
384
446
  h('label', { className: 'tcw-filter' },
385
447
  h('span', null, '状态'),
386
448
  h('select', { className: 'status-filter', value: statusFilter, onChange: (event) => { setStatusFilter(event.target.value); setSelectedIds([]) } },
@@ -391,9 +453,8 @@
391
453
  )
392
454
  ),
393
455
  h('button', { className: 'tcw-btn tcw-btn-primary', disabled: busy || selectedIds.length === 0, onClick: batchApproveSelected }, `批量审核${selectedIds.length ? ` (${selectedIds.length})` : ''}`)
394
- ),
395
- reviewTable(headers, rows, mapRow, emptyText)
396
- ))
456
+ )
457
+ )
397
458
  }
398
459
 
399
460
  function reviewTable(headers, rows, mapRow, emptyText) {
@@ -594,6 +655,18 @@
594
655
  return items.filter((item) => (item.reviewStatus || 'pending') === statusFilter)
595
656
  }
596
657
 
658
+ function reviewTypeForUploadAction(actionKey) {
659
+ if (actionKey === 'upload_controlled_goods_file') return 'controlled_goods'
660
+ if (actionKey === 'upload_supplier_contract') return 'supplier_product'
661
+ if (actionKey === 'upload_sales_contract') return 'customs_workbook'
662
+ return null
663
+ }
664
+
665
+ function countReviewItemsByType(items, type) {
666
+ if (!type || !Array.isArray(items)) return 0
667
+ return items.filter((item) => item.type === type && !isPlaceholderReviewItem(item)).length
668
+ }
669
+
597
670
  function isPlaceholderReviewItem(item) {
598
671
  const merged = readMerged(item || {})
599
672
  if (!item || !item.type) return true
@@ -680,11 +753,11 @@
680
753
  * { box-sizing: border-box; }
681
754
  body { margin: 0; background: var(--tcw-bg); color: var(--tcw-text); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
682
755
  button, input { font: inherit; letter-spacing: 0; }
683
- .tcw-shell { display: grid; gap: 12px; min-height: 100vh; padding: 14px 16px 18px; }
684
- .tcw-header { display: grid; grid-template-columns: minmax(0, 1fr) minmax(340px, 520px); gap: 12px; align-items: end; }
756
+ .tcw-shell { display: grid; gap: 10px; min-height: 100vh; padding: 12px 14px 18px; }
757
+ .tcw-header { display: grid; grid-template-columns: minmax(260px, .72fr) minmax(340px, 520px); gap: 12px; align-items: center; border: 1px solid var(--tcw-border); border-radius: 8px; background: var(--tcw-card); padding: 12px 14px; }
685
758
  .tcw-title-block { display: grid; gap: 3px; }
686
759
  .tcw-kicker { color: var(--tcw-primary); font-size: 12px; font-weight: 900; }
687
- .tcw-title-block h1 { margin: 0; font-size: 22px; line-height: 1.22; }
760
+ .tcw-title-block h1 { margin: 0; font-size: 20px; line-height: 1.22; }
688
761
  .tcw-title-block p, .tcw-page-head p, .tcw-drawer-head p { margin: 0; color: var(--tcw-muted); font-size: 13px; }
689
762
  .tcw-header-actions { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; }
690
763
  .tcw-search, .tcw-field input { width: 100%; min-height: 36px; border: 1px solid var(--tcw-border); border-radius: 6px; background: var(--tcw-card); color: var(--tcw-text); padding: 8px 10px; }
@@ -697,18 +770,28 @@ button, input { font: inherit; letter-spacing: 0; }
697
770
  .tcw-upload { border-style: dashed; border-color: color-mix(in srgb, var(--tcw-primary) 42%, var(--tcw-border)); color: var(--tcw-primary); font-weight: 800; }
698
771
  .tcw-upload input { display: none; }
699
772
  .tcw-notice { border: 1px solid color-mix(in srgb, var(--tcw-primary) 24%, var(--tcw-border)); border-radius: 6px; background: color-mix(in srgb, var(--tcw-primary) 7%, var(--tcw-card)); padding: 10px 12px; color: var(--tcw-primary-dark); font-size: 13px; }
700
- .tcw-tabs { display: flex; gap: 6px; overflow-x: auto; border-bottom: 1px solid var(--tcw-border); }
701
- .tcw-tab { min-height: 38px; border: 0; border-bottom: 3px solid transparent; background: transparent; color: var(--tcw-muted); padding: 8px 12px; cursor: pointer; font-weight: 900; }
773
+ .tcw-tabs { display: flex; gap: 6px; overflow-x: auto; border-bottom: 1px solid var(--tcw-border); padding: 0 2px; }
774
+ .tcw-tab { min-height: 34px; border: 0; border-bottom: 3px solid transparent; background: transparent; color: var(--tcw-muted); padding: 7px 12px; cursor: pointer; font-weight: 900; }
702
775
  .tcw-tab.active { border-color: var(--tcw-primary); color: var(--tcw-primary); }
703
776
  .tcw-main { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; align-items: start; }
704
777
  .tcw-main.with-drawer { grid-template-columns: minmax(0, 1fr) minmax(340px, 410px); }
705
778
  .tcw-content, .tcw-drawer, .tcw-panel, .tcw-supplier-block { border: 1px solid var(--tcw-border); border-radius: 8px; background: var(--tcw-card); }
706
- .tcw-content { min-width: 0; padding: 12px; }
779
+ .tcw-content { min-width: 0; border: 0; background: transparent; padding: 0; }
707
780
  .tcw-page { display: grid; gap: 12px; }
708
781
  .tcw-page-head, .tcw-drawer-head, .tcw-supplier-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
709
782
  .tcw-page-head h2, .tcw-drawer-head h2, .tcw-panel h3 { margin: 0 0 4px; font-size: 16px; line-height: 1.35; }
710
783
  .tcw-button-row { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; }
711
784
  .tcw-panel { display: grid; gap: 10px; padding: 12px; }
785
+ .tcw-business-panel { display: grid; gap: 0; border: 1px solid var(--tcw-border); border-radius: 8px; background: var(--tcw-card); overflow: hidden; }
786
+ .tcw-business-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; border-bottom: 1px solid var(--tcw-border); background: var(--tcw-card); padding: 14px 16px; }
787
+ .tcw-business-head h2 { margin: 0 0 4px; font-size: 18px; line-height: 1.25; }
788
+ .tcw-business-head p { margin: 0; color: var(--tcw-muted); font-size: 13px; }
789
+ .tcw-business-body { display: grid; gap: 12px; padding: 14px 16px 16px; }
790
+ .tcw-table-section { display: grid; gap: 0; }
791
+ .tcw-history-section { display: grid; gap: 10px; border-top: 1px solid var(--tcw-border); padding-top: 14px; }
792
+ .tcw-subsection-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
793
+ .tcw-subsection-head h3, .tcw-list-title h3 { margin: 0; font-size: 15px; line-height: 1.3; }
794
+ .tcw-subsection-head p, .tcw-list-title span { margin: 0; color: var(--tcw-muted); font-size: 12px; }
712
795
  .tcw-metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
713
796
  .tcw-metric { display: grid; gap: 4px; border: 1px solid var(--tcw-border); border-radius: 8px; background: var(--tcw-card); padding: 12px; }
714
797
  .tcw-metric span { color: var(--tcw-muted); font-size: 12px; }
@@ -724,7 +807,9 @@ button, input { font: inherit; letter-spacing: 0; }
724
807
  .tcw-table tr:last-child td { border-bottom: 0; }
725
808
  .tcw-table tr.clickable { cursor: pointer; }
726
809
  .tcw-table tr.clickable:hover td { background: color-mix(in srgb, var(--tcw-primary) 5%, transparent); }
727
- .tcw-list-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; }
810
+ .tcw-list-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
811
+ .tcw-list-title { display: grid; gap: 2px; min-width: 160px; }
812
+ .tcw-list-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; }
728
813
  .tcw-filter { display: inline-flex; align-items: center; gap: 8px; color: var(--tcw-muted); font-size: 13px; font-weight: 800; }
729
814
  .tcw-filter select { min-height: 34px; border: 1px solid var(--tcw-border); border-radius: 6px; background: var(--tcw-card); color: var(--tcw-text); padding: 6px 28px 6px 9px; }
730
815
  .tcw-row-actions { display: flex; flex-wrap: wrap; gap: 6px; }
@@ -732,7 +817,7 @@ button, input { font: inherit; letter-spacing: 0; }
732
817
  .tcw-mini-btn.danger { border-color: color-mix(in srgb, var(--tcw-danger) 35%, var(--tcw-border)); background: color-mix(in srgb, var(--tcw-danger) 6%, var(--tcw-card)); color: var(--tcw-danger); }
733
818
  .tcw-mini-btn:disabled { cursor: not-allowed; opacity: .5; }
734
819
  .row-selection-checkbox { width: 16px; height: 16px; }
735
- .tcw-empty { display: grid; place-items: center; min-height: 96px; border: 1px dashed var(--tcw-border); border-radius: 8px; background: var(--tcw-soft); color: var(--tcw-muted); padding: 18px; text-align: center; }
820
+ .tcw-empty { display: grid; place-items: center; min-height: 86px; border: 1px dashed var(--tcw-border); border-radius: 8px; background: var(--tcw-soft); color: var(--tcw-muted); padding: 16px; text-align: center; }
736
821
  .tcw-supplier-block { display: grid; gap: 10px; padding: 12px; }
737
822
  .tcw-supplier-head strong { font-size: 15px; }
738
823
  .tcw-supplier-head span { color: var(--tcw-muted); font-size: 12px; }
@@ -758,6 +843,8 @@ button, input { font: inherit; letter-spacing: 0; }
758
843
  .tcw-status.muted { background: #eef2f6; color: var(--tcw-muted); }
759
844
  @media (max-width: 1040px) {
760
845
  .tcw-header, .tcw-main.with-drawer, .tcw-form { grid-template-columns: 1fr; }
846
+ .tcw-business-head { align-items: stretch; flex-direction: column; }
847
+ .tcw-button-row, .tcw-list-actions { justify-content: flex-start; }
761
848
  .tcw-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
762
849
  .tcw-drawer { position: static; max-height: none; }
763
850
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chenchaolong/plugin-trade-compliance-workbench",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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",