@chenchaolong/plugin-trade-compliance-workbench 0.1.48 → 0.1.49

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,8 @@
74
74
  const [busy, setBusy] = React.useState(false)
75
75
  const [query, setQuery] = React.useState('')
76
76
  const [pageByList, setPageByList] = React.useState({})
77
+ const [pageSizeByList, setPageSizeByList] = React.useState({})
78
+ const [pageJumpByList, setPageJumpByList] = React.useState({})
77
79
  const [detailItem, setDetailItem] = React.useState(null)
78
80
  const [formDialog, setFormDialog] = React.useState(null)
79
81
  const [deleteDialog, setDeleteDialog] = React.useState(null)
@@ -84,6 +86,7 @@
84
86
  loading: false,
85
87
  searched: false,
86
88
  error: '',
89
+ jumpPage: '',
87
90
  result: null
88
91
  })
89
92
  const pollingRef = React.useRef(null)
@@ -104,10 +107,11 @@
104
107
  }
105
108
  }, [])
106
109
 
107
- React.useEffect(reportResize, [data, activePage, selectedIds, statusFilter, overviewTab, busy, formDialog, deleteDialog, hsCodeSearch, hsCodeDetailDialog])
110
+ React.useEffect(reportResize, [data, activePage, selectedIds, statusFilter, overviewTab, pageByList, pageSizeByList, busy, formDialog, deleteDialog, hsCodeSearch, hsCodeDetailDialog])
108
111
 
109
112
  const reviewItems = data.reviewItems.filter((item) => !isPlaceholderReviewItem(item))
110
113
  const pendingItems = reviewItems.filter((item) => (item.reviewStatus || 'pending') === 'pending')
114
+ const filteredPendingItems = filterItems(pendingItems, query, overviewSearchKeys)
111
115
  const controlledReviewRows = reviewItems.filter((item) => item.type === 'controlled_goods' && item.reviewStatus !== 'confirmed')
112
116
  const controlledRows = controlledReviewRows.concat(data.controlledGoods.map(toControlledGoodsReviewRow))
113
117
  const controlledReviews = applyStatusFilter(filterItems(controlledRows, query, reviewSearchKeys), statusFilter)
@@ -471,6 +475,7 @@
471
475
  hsCandidates: Array.isArray(merged.hsCodeCandidates) ? merged.hsCodeCandidates : [],
472
476
  hsCandidateKeyword: merged.hsCodeLookupKeyword || buildSupplierHsCandidateKeyword(merged),
473
477
  hsCandidatePage: 1,
478
+ hsCandidateJumpPage: '',
474
479
  hsCandidatePagination: null,
475
480
  hsCandidateStatus: merged.hsCodeLookupStatus || '',
476
481
  hsCandidateError: merged.hsCodeLookupError || '',
@@ -487,6 +492,7 @@
487
492
  hsCandidates: [],
488
493
  hsCandidateKeyword: '',
489
494
  hsCandidatePage: 1,
495
+ hsCandidateJumpPage: '',
490
496
  hsCandidatePagination: null,
491
497
  hsCandidateStatus: '',
492
498
  hsCandidateError: '',
@@ -672,15 +678,20 @@
672
678
  }
673
679
 
674
680
  function renderOverviewPage() {
675
- const pendingOverviewItems = overviewTab === 'all'
676
- ? pendingItems.slice(0, 12)
677
- : pendingItems.filter((item) => item.type === overviewTab).slice(0, 12)
681
+ const filteredPendingControlledItems = filteredPendingItems.filter((item) => item.type === 'controlled_goods')
682
+ const filteredPendingSupplierItems = filteredPendingItems.filter((item) => item.type === 'supplier_product')
683
+ const filteredPendingSalesItems = filteredPendingItems.filter((item) => item.type === 'customs_workbook')
684
+ const overviewRows = overviewTab === 'all'
685
+ ? filteredPendingItems
686
+ : filteredPendingItems.filter((item) => item.type === overviewTab)
687
+ const overviewListKey = `overview-${overviewTab}`
688
+ const overviewPage = paginateRows(overviewListKey, overviewRows)
678
689
  return h('div', { className: 'tcw-page overview-page' },
679
690
  h('section', { className: 'tcw-metrics' },
680
- metric('待审核', pendingItems.length, 'dashboard', 'blue'),
681
- metric('管控识别待审', pendingControlledReviews.length, 'shield', 'green'),
682
- metric('供应商商品待审', pendingSupplierReviews.length, 'box', 'orange'),
683
- metric('销售合同待审', pendingSalesReviews.length, 'invoice', 'violet'),
691
+ metric('待审核', filteredPendingItems.length, 'dashboard', 'blue'),
692
+ metric('管控识别待审', filteredPendingControlledItems.length, 'shield', 'green'),
693
+ metric('供应商商品待审', filteredPendingSupplierItems.length, 'box', 'orange'),
694
+ metric('销售合同待审', filteredPendingSalesItems.length, 'invoice', 'violet'),
684
695
  metric('已入库管控商品', data.controlledGoods.length, 'archive', 'slate'),
685
696
  metric('已入库供应商商品', data.products.length, 'supplier', 'green'),
686
697
  metric('销售发票历史', data.workbookGenerations.length, 'document', 'violet'),
@@ -689,14 +700,15 @@
689
700
  panel('待办事项明细',
690
701
  h('div', { className: 'tcw-overview-panel' },
691
702
  h('div', { className: 'tcw-overview-tabs' }, [
692
- overviewTabButton('all', '全部', pendingItems.length),
693
- overviewTabButton('controlled_goods', '管控商品', pendingItems.filter((item) => item.type === 'controlled_goods').length),
694
- overviewTabButton('supplier_product', '供应商商品', pendingItems.filter((item) => item.type === 'supplier_product').length),
695
- overviewTabButton('customs_workbook', '销售发票', pendingItems.filter((item) => item.type === 'customs_workbook').length)
703
+ overviewTabButton('all', '全部', filteredPendingItems.length),
704
+ overviewTabButton('controlled_goods', '管控商品', filteredPendingControlledItems.length),
705
+ overviewTabButton('supplier_product', '供应商商品', filteredPendingSupplierItems.length),
706
+ overviewTabButton('customs_workbook', '销售发票', filteredPendingSalesItems.length)
696
707
  ]),
697
- pendingOverviewItems.length
698
- ? compactReviewList(pendingOverviewItems)
699
- : empty('暂无待审核记录。智能体识别结果会先出现在这里。')
708
+ overviewPage.rows.length
709
+ ? compactReviewList(overviewPage.rows)
710
+ : empty('暂无待审核记录。智能体识别结果会先出现在这里。'),
711
+ pagination(overviewListKey, overviewRows.length)
700
712
  )
701
713
  )
702
714
  )
@@ -892,20 +904,26 @@
892
904
  function renderHsCodePagination(pagination) {
893
905
  if (!pagination || (!pagination.hasPrevious && !pagination.hasNext && (!pagination.pages || pagination.pages.length <= 1))) return null
894
906
  const pages = Array.isArray(pagination.pages) ? pagination.pages.filter((item) => item.page) : []
895
- const visiblePages = pages.filter((item, index, arr) => arr.findIndex((candidate) => candidate.page === item.page) === index).slice(0, 10)
896
- return h('div', { className: 'tcw-pagination hs-code-pagination' },
897
- h('span', null, `第 ${pagination.currentPage || hsCodeSearch.page} 页`),
898
- h('div', { className: 'tcw-page-buttons' },
899
- h('button', { className: 'tcw-mini-btn', disabled: hsCodeSearch.loading || !pagination.hasPrevious, onClick: () => searchHsCodePage((pagination.currentPage || hsCodeSearch.page) - 1) }, '上一页'),
900
- visiblePages.map((item) => h('button', {
901
- key: item.page,
902
- className: item.current ? 'tcw-mini-btn active' : 'tcw-mini-btn',
903
- disabled: hsCodeSearch.loading || item.current,
904
- onClick: () => searchHsCodePage(item.page)
905
- }, String(item.page))),
906
- h('button', { className: 'tcw-mini-btn', disabled: hsCodeSearch.loading || !pagination.hasNext, onClick: () => searchHsCodePage((pagination.currentPage || hsCodeSearch.page) + 1) }, '下一页')
907
- )
908
- )
907
+ const maxPage = Number(pagination.maxVisiblePage) || Math.max(...pages.map((item) => Number(item.page) || 0), pagination.currentPage || hsCodeSearch.page || 1)
908
+ const currentPage = pagination.currentPage || hsCodeSearch.page || 1
909
+ return renderUnifiedPagination({
910
+ page: currentPage,
911
+ totalPages: Math.max(1, maxPage),
912
+ pageSize: 10,
913
+ fixedPageSize: true,
914
+ loading: hsCodeSearch.loading,
915
+ hasPrevious: pagination.hasPrevious,
916
+ hasNext: pagination.hasNext,
917
+ jumpValue: hsCodeSearch.jumpPage || '',
918
+ onPageChange: searchHsCodePage,
919
+ onJumpInput: (nextValue) => setHsCodeSearch(Object.assign({}, hsCodeSearch, { jumpPage: nextValue })),
920
+ onJump: () => {
921
+ const nextPage = Number(hsCodeSearch.jumpPage)
922
+ if (!Number.isFinite(nextPage) || nextPage <= 0) return
923
+ searchHsCodePage(nextPage)
924
+ setHsCodeSearch((current) => Object.assign({}, current, { jumpPage: '' }))
925
+ }
926
+ })
909
927
  }
910
928
 
911
929
  function compactReviewList(items) {
@@ -1050,7 +1068,7 @@
1050
1068
  }
1051
1069
 
1052
1070
  function paginateRows(listKey, rows) {
1053
- const pageSize = 10
1071
+ const pageSize = getPageSize(listKey)
1054
1072
  const total = rows.length
1055
1073
  const totalPages = Math.max(1, Math.ceil(total / pageSize))
1056
1074
  const current = Math.min(Math.max(pageByList[listKey] || 1, 1), totalPages)
@@ -1058,15 +1076,44 @@
1058
1076
  }
1059
1077
 
1060
1078
  function pagination(listKey, total) {
1061
- if (total <= 10) return null
1079
+ if (total <= 0) return null
1062
1080
  const page = paginateRows(listKey, Array.from({ length: total }))
1063
- return h('div', { className: 'tcw-pagination' },
1064
- h('span', null, `共 ${total} 条,第 ${page.page} / ${page.totalPages} 页`),
1065
- h('div', { className: 'tcw-page-buttons' },
1066
- h('button', { className: 'tcw-mini-btn', disabled: page.page <= 1, onClick: () => setPageByList(Object.assign({}, pageByList, { [listKey]: page.page - 1 })) }, '上一页'),
1067
- h('button', { className: 'tcw-mini-btn', disabled: page.page >= page.totalPages, onClick: () => setPageByList(Object.assign({}, pageByList, { [listKey]: page.page + 1 })) }, '下一页')
1068
- )
1069
- )
1081
+ return renderUnifiedPagination({
1082
+ total,
1083
+ page: page.page,
1084
+ totalPages: page.totalPages,
1085
+ pageSize: page.pageSize,
1086
+ pageSizeOptions: [10, 20, 50],
1087
+ jumpValue: pageJumpByList[listKey] || '',
1088
+ onPageChange: (nextPage) => setLocalPage(listKey, nextPage, page.totalPages),
1089
+ onPageSizeChange: (nextSize) => setLocalPageSize(listKey, nextSize),
1090
+ onJumpInput: (nextValue) => setPageJumpByList(Object.assign({}, pageJumpByList, { [listKey]: nextValue })),
1091
+ onJump: () => jumpLocalPage(listKey, page.totalPages)
1092
+ })
1093
+ }
1094
+
1095
+ function getPageSize(listKey) {
1096
+ const value = Number(pageSizeByList[listKey])
1097
+ return Number.isFinite(value) && value > 0 ? value : 10
1098
+ }
1099
+
1100
+ function setLocalPage(listKey, nextPage, totalPages) {
1101
+ const page = Math.min(Math.max(Number(nextPage) || 1, 1), totalPages || 1)
1102
+ setPageByList(Object.assign({}, pageByList, { [listKey]: page }))
1103
+ }
1104
+
1105
+ function setLocalPageSize(listKey, nextSize) {
1106
+ const pageSize = Number(nextSize) || 10
1107
+ setPageSizeByList(Object.assign({}, pageSizeByList, { [listKey]: pageSize }))
1108
+ setPageByList(Object.assign({}, pageByList, { [listKey]: 1 }))
1109
+ setPageJumpByList(Object.assign({}, pageJumpByList, { [listKey]: '' }))
1110
+ }
1111
+
1112
+ function jumpLocalPage(listKey, totalPages) {
1113
+ const value = Number(pageJumpByList[listKey])
1114
+ if (!Number.isFinite(value) || value <= 0) return
1115
+ setLocalPage(listKey, value, totalPages)
1116
+ setPageJumpByList(Object.assign({}, pageJumpByList, { [listKey]: '' }))
1070
1117
  }
1071
1118
 
1072
1119
  function renderDetailModal() {
@@ -1167,20 +1214,67 @@
1167
1214
  function renderSupplierHsCandidatePagination(pagination) {
1168
1215
  if (!pagination || (!pagination.hasPrevious && !pagination.hasNext && (!pagination.pages || pagination.pages.length <= 1))) return null
1169
1216
  const pages = Array.isArray(pagination.pages) ? pagination.pages.filter((item) => item.page) : []
1170
- const visiblePages = pages.filter((item, index, arr) => arr.findIndex((candidate) => candidate.page === item.page) === index).slice(0, 10)
1171
1217
  const currentPage = pagination.currentPage || formDialog.hsCandidatePage || 1
1172
- return h('div', { className: 'tcw-pagination hs-code-pagination' },
1173
- h('span', null, `第 ${currentPage} 页`),
1174
- h('div', { className: 'tcw-page-buttons' },
1175
- h('button', { className: 'tcw-mini-btn', disabled: formDialog.hsCandidateLoading || !pagination.hasPrevious, onClick: () => searchSupplierHsCandidatesForForm(currentPage - 1) }, '上一页'),
1176
- visiblePages.map((item) => h('button', {
1177
- key: item.page,
1178
- className: item.current ? 'tcw-mini-btn active' : 'tcw-mini-btn',
1179
- disabled: formDialog.hsCandidateLoading || item.current,
1180
- onClick: () => searchSupplierHsCandidatesForForm(item.page)
1181
- }, String(item.page))),
1182
- h('button', { className: 'tcw-mini-btn', disabled: formDialog.hsCandidateLoading || !pagination.hasNext, onClick: () => searchSupplierHsCandidatesForForm(currentPage + 1) }, '下一页')
1183
- )
1218
+ const maxPage = Number(pagination.maxVisiblePage) || Math.max(...pages.map((item) => Number(item.page) || 0), currentPage)
1219
+ return renderUnifiedPagination({
1220
+ page: currentPage,
1221
+ totalPages: Math.max(1, maxPage),
1222
+ pageSize: 10,
1223
+ fixedPageSize: true,
1224
+ loading: formDialog.hsCandidateLoading,
1225
+ hasPrevious: pagination.hasPrevious,
1226
+ hasNext: pagination.hasNext,
1227
+ jumpValue: formDialog.hsCandidateJumpPage || '',
1228
+ onPageChange: searchSupplierHsCandidatesForForm,
1229
+ onJumpInput: (nextValue) => setFormDialog(Object.assign({}, formDialog, { hsCandidateJumpPage: nextValue })),
1230
+ onJump: () => {
1231
+ const nextPage = Number(formDialog.hsCandidateJumpPage)
1232
+ if (!Number.isFinite(nextPage) || nextPage <= 0) return
1233
+ searchSupplierHsCandidatesForForm(nextPage)
1234
+ setFormDialog((current) => current ? Object.assign({}, current, { hsCandidateJumpPage: '' }) : current)
1235
+ }
1236
+ })
1237
+ }
1238
+
1239
+ function renderUnifiedPagination(config) {
1240
+ const totalPages = Math.max(1, Number(config.totalPages) || 1)
1241
+ const page = Math.min(Math.max(Number(config.page) || 1, 1), totalPages)
1242
+ const canPrevious = config.hasPrevious === undefined ? page > 1 : Boolean(config.hasPrevious)
1243
+ const canNext = config.hasNext === undefined ? page < totalPages : Boolean(config.hasNext)
1244
+ const loading = Boolean(config.loading)
1245
+ const jumpValue = config.jumpValue || ''
1246
+ const pageSize = Number(config.pageSize) || 10
1247
+ return h('div', { className: 'tcw-pagination tcw-pagination-unified' },
1248
+ config.total == null ? null : h('span', { className: 'tcw-pagination-total' }, `共 ${config.total} 条`),
1249
+ h('button', { className: 'tcw-page-btn', disabled: loading || !canPrevious, onClick: () => config.onPageChange(1) }, '首页'),
1250
+ h('button', { className: 'tcw-page-btn', disabled: loading || !canPrevious, onClick: () => config.onPageChange(page - 1) }, '上一页'),
1251
+ h('span', { className: 'tcw-page-current' }, `第 ${page} / ${totalPages} 页`),
1252
+ h('button', { className: 'tcw-page-btn', disabled: loading || !canNext, onClick: () => config.onPageChange(page + 1) }, '下一页'),
1253
+ h('button', { className: 'tcw-page-btn', disabled: loading || !canNext, onClick: () => config.onPageChange(totalPages) }, '末页'),
1254
+ h('span', { className: 'tcw-page-size-label' }, '每页'),
1255
+ config.fixedPageSize
1256
+ ? h('span', { className: 'tcw-page-size-fixed' }, `${pageSize} 条/页`)
1257
+ : h('select', {
1258
+ className: 'tcw-page-size-select',
1259
+ value: pageSize,
1260
+ onChange: (event) => config.onPageSizeChange(Number(event.target.value))
1261
+ }, (config.pageSizeOptions || [10, 20, 50]).map((size) => h('option', { key: size, value: size }, `${size} 条/页`))),
1262
+ h('input', {
1263
+ className: 'tcw-page-jump-input',
1264
+ type: 'number',
1265
+ min: 1,
1266
+ max: totalPages,
1267
+ value: jumpValue,
1268
+ placeholder: String(page),
1269
+ onChange: (event) => config.onJumpInput(event.target.value),
1270
+ onKeyDown: (event) => {
1271
+ if (event.key === 'Enter') {
1272
+ event.preventDefault()
1273
+ config.onJump()
1274
+ }
1275
+ }
1276
+ }),
1277
+ h('button', { className: 'tcw-page-btn', disabled: loading, onClick: config.onJump }, '跳转')
1184
1278
  )
1185
1279
  }
1186
1280
 
@@ -1372,6 +1466,27 @@
1372
1466
  }
1373
1467
 
1374
1468
  const reviewSearchKeys = ['title', 'sourceLocation']
1469
+ const overviewSearchKeys = [
1470
+ 'title',
1471
+ 'sourceLocation',
1472
+ 'sourceFileName',
1473
+ 'fileName',
1474
+ 'supplierName',
1475
+ 'supplierCreditCode',
1476
+ 'productName',
1477
+ 'model',
1478
+ 'description',
1479
+ 'hsCode',
1480
+ 'contractHsCode',
1481
+ 'enrichedHsCode',
1482
+ 'englishName',
1483
+ 'invoiceNo',
1484
+ 'contractNo',
1485
+ 'buyerName',
1486
+ 'sellerName',
1487
+ 'controlNote',
1488
+ 'keywords'
1489
+ ]
1375
1490
  const controlledGoodsFormFields = [
1376
1491
  { key: 'productName', label: '商品名称', placeholder: '例如:高性能服务器' },
1377
1492
  { key: 'hsCode', label: '海关编码', placeholder: '例如:8471501010' },
@@ -1977,8 +2092,74 @@ button, input, textarea, select { font: inherit; letter-spacing: 0; }
1977
2092
  .tcw-error { border: 1px solid color-mix(in srgb, var(--tcw-danger) 35%, var(--tcw-border)); border-radius: 8px; background: color-mix(in srgb, var(--tcw-danger) 6%, var(--tcw-card)); color: var(--tcw-danger); padding: 10px 12px; font-size: 13px; font-weight: 800; }
1978
2093
  .row-selection-checkbox { width: 16px; height: 16px; }
1979
2094
  .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; }
1980
- .tcw-pagination { display: flex; align-items: center; justify-content: space-between; gap: 10px; border: 1px solid var(--tcw-border); border-top: 0; border-radius: 0 0 8px 8px; background: var(--tcw-card); padding: 10px 12px; color: var(--tcw-muted); font-size: 12px; }
2095
+ .tcw-pagination {
2096
+ display: flex;
2097
+ flex-wrap: wrap;
2098
+ align-items: center;
2099
+ gap: 6px;
2100
+ border: 1px solid var(--tcw-border);
2101
+ border-radius: 8px;
2102
+ background: var(--tcw-card);
2103
+ padding: 8px 10px;
2104
+ color: var(--tcw-muted);
2105
+ font-size: 12px;
2106
+ }
2107
+ .tcw-pagination-unified { justify-content: flex-start; }
1981
2108
  .tcw-page-buttons { display: flex; gap: 8px; }
2109
+ .tcw-pagination-total {
2110
+ display: inline-flex;
2111
+ align-items: center;
2112
+ min-height: 28px;
2113
+ color: var(--tcw-primary);
2114
+ }
2115
+ .tcw-page-btn {
2116
+ display: inline-flex;
2117
+ align-items: center;
2118
+ justify-content: center;
2119
+ min-width: 48px;
2120
+ min-height: 28px;
2121
+ border: 1px solid var(--tcw-border);
2122
+ border-radius: 6px;
2123
+ background: #fff;
2124
+ color: var(--tcw-primary);
2125
+ padding: 4px 9px;
2126
+ cursor: pointer;
2127
+ font-size: 12px;
2128
+ font-weight: 700;
2129
+ }
2130
+ .tcw-page-btn:disabled {
2131
+ cursor: not-allowed;
2132
+ background: var(--tcw-soft);
2133
+ color: #9aa7b8;
2134
+ }
2135
+ .tcw-page-current {
2136
+ display: inline-flex;
2137
+ align-items: center;
2138
+ min-height: 28px;
2139
+ border-radius: 6px;
2140
+ background: var(--tcw-primary);
2141
+ color: #fff;
2142
+ padding: 4px 12px;
2143
+ font-weight: 800;
2144
+ }
2145
+ .tcw-page-size-label {
2146
+ color: var(--tcw-primary);
2147
+ font-weight: 800;
2148
+ }
2149
+ .tcw-page-size-select,
2150
+ .tcw-page-jump-input,
2151
+ .tcw-page-size-fixed {
2152
+ min-height: 28px;
2153
+ border: 1px solid var(--tcw-border);
2154
+ border-radius: 6px;
2155
+ background: #fff;
2156
+ color: var(--tcw-text);
2157
+ padding: 4px 8px;
2158
+ font-size: 12px;
2159
+ }
2160
+ .tcw-page-size-select { min-width: 92px; }
2161
+ .tcw-page-size-fixed { display: inline-flex; align-items: center; }
2162
+ .tcw-page-jump-input { width: 56px; }
1982
2163
  .tcw-modal-backdrop { position: fixed; inset: 0; z-index: 10; display: grid; place-items: center; background: rgba(15, 23, 42, .32); padding: 20px; }
1983
2164
  .tcw-modal { display: grid; gap: 12px; width: min(900px, 100%); max-height: calc(100vh - 48px); overflow: auto; border: 1px solid var(--tcw-border); border-radius: 8px; background: var(--tcw-card); padding: 16px; box-shadow: 0 18px 48px rgba(15, 23, 42, .18); }
1984
2165
  .tcw-form-modal { width: min(960px, 100%); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chenchaolong/plugin-trade-compliance-workbench",
3
- "version": "0.1.48",
3
+ "version": "0.1.49",
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",