@chenchaolong/plugin-trade-compliance-workbench 0.1.47 → 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)
@@ -194,15 +198,17 @@
194
198
  }
195
199
  }
196
200
 
197
- async function searchSupplierHsCandidatesForForm() {
201
+ async function searchSupplierHsCandidatesForForm(page) {
198
202
  if (!formDialog || formDialog.type !== 'supplier_product' || busy) return
199
203
  const keyword = String(formDialog.hsCandidateKeyword || buildSupplierHsCandidateKeyword(formDialog.values) || '').trim()
200
204
  if (!keyword) {
201
205
  setFormDialog(Object.assign({}, formDialog, { hsCandidateError: '请输入商品名称、型号或海关编码后再查询。' }))
202
206
  return
203
207
  }
208
+ const nextPage = Math.max(1, Number(page) || 1)
204
209
  setFormDialog(Object.assign({}, formDialog, {
205
210
  hsCandidateKeyword: keyword,
211
+ hsCandidatePage: nextPage,
206
212
  hsCandidateLoading: true,
207
213
  hsCandidateError: '',
208
214
  hsCandidateStatus: ''
@@ -210,20 +216,22 @@
210
216
  try {
211
217
  const response = await executeAction('search_hs_code', null, {
212
218
  keywords: keyword,
213
- page: 1,
219
+ page: nextPage,
214
220
  filterFailureCode: true,
215
221
  displayChapter: false,
216
222
  displayEnName: true
217
223
  }, {})
218
224
  assertActionSuccess(response)
219
225
  const result = getActionDataPayload(response)
220
- const candidates = Array.isArray(result && result.results) ? result.results.slice(0, 10) : []
226
+ const candidates = Array.isArray(result && result.results) ? result.results : []
221
227
  setFormDialog((current) => current ? Object.assign({}, current, {
222
228
  hsCandidateKeyword: keyword,
229
+ hsCandidatePage: nextPage,
223
230
  hsCandidateLoading: false,
224
231
  hsCandidateError: '',
225
232
  hsCandidateStatus: candidates.length ? 'pending_confirmation' : 'not_found',
226
- hsCandidates: candidates
233
+ hsCandidates: candidates,
234
+ hsCandidatePagination: result && result.pagination ? result.pagination : null
227
235
  }) : current)
228
236
  } catch (error) {
229
237
  setFormDialog((current) => current ? Object.assign({}, current, {
@@ -466,6 +474,9 @@
466
474
  values: valuesFromItem(item.type, item),
467
475
  hsCandidates: Array.isArray(merged.hsCodeCandidates) ? merged.hsCodeCandidates : [],
468
476
  hsCandidateKeyword: merged.hsCodeLookupKeyword || buildSupplierHsCandidateKeyword(merged),
477
+ hsCandidatePage: 1,
478
+ hsCandidateJumpPage: '',
479
+ hsCandidatePagination: null,
469
480
  hsCandidateStatus: merged.hsCodeLookupStatus || '',
470
481
  hsCandidateError: merged.hsCodeLookupError || '',
471
482
  hsCandidateLoading: false
@@ -480,6 +491,9 @@
480
491
  values: emptyFormValues(type),
481
492
  hsCandidates: [],
482
493
  hsCandidateKeyword: '',
494
+ hsCandidatePage: 1,
495
+ hsCandidateJumpPage: '',
496
+ hsCandidatePagination: null,
483
497
  hsCandidateStatus: '',
484
498
  hsCandidateError: '',
485
499
  hsCandidateLoading: false
@@ -664,15 +678,20 @@
664
678
  }
665
679
 
666
680
  function renderOverviewPage() {
667
- const pendingOverviewItems = overviewTab === 'all'
668
- ? pendingItems.slice(0, 12)
669
- : 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)
670
689
  return h('div', { className: 'tcw-page overview-page' },
671
690
  h('section', { className: 'tcw-metrics' },
672
- metric('待审核', pendingItems.length, 'dashboard', 'blue'),
673
- metric('管控识别待审', pendingControlledReviews.length, 'shield', 'green'),
674
- metric('供应商商品待审', pendingSupplierReviews.length, 'box', 'orange'),
675
- 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'),
676
695
  metric('已入库管控商品', data.controlledGoods.length, 'archive', 'slate'),
677
696
  metric('已入库供应商商品', data.products.length, 'supplier', 'green'),
678
697
  metric('销售发票历史', data.workbookGenerations.length, 'document', 'violet'),
@@ -681,14 +700,15 @@
681
700
  panel('待办事项明细',
682
701
  h('div', { className: 'tcw-overview-panel' },
683
702
  h('div', { className: 'tcw-overview-tabs' }, [
684
- overviewTabButton('all', '全部', pendingItems.length),
685
- overviewTabButton('controlled_goods', '管控商品', pendingItems.filter((item) => item.type === 'controlled_goods').length),
686
- overviewTabButton('supplier_product', '供应商商品', pendingItems.filter((item) => item.type === 'supplier_product').length),
687
- 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)
688
707
  ]),
689
- pendingOverviewItems.length
690
- ? compactReviewList(pendingOverviewItems)
691
- : empty('暂无待审核记录。智能体识别结果会先出现在这里。')
708
+ overviewPage.rows.length
709
+ ? compactReviewList(overviewPage.rows)
710
+ : empty('暂无待审核记录。智能体识别结果会先出现在这里。'),
711
+ pagination(overviewListKey, overviewRows.length)
692
712
  )
693
713
  )
694
714
  )
@@ -884,20 +904,26 @@
884
904
  function renderHsCodePagination(pagination) {
885
905
  if (!pagination || (!pagination.hasPrevious && !pagination.hasNext && (!pagination.pages || pagination.pages.length <= 1))) return null
886
906
  const pages = Array.isArray(pagination.pages) ? pagination.pages.filter((item) => item.page) : []
887
- const visiblePages = pages.filter((item, index, arr) => arr.findIndex((candidate) => candidate.page === item.page) === index).slice(0, 10)
888
- return h('div', { className: 'tcw-pagination hs-code-pagination' },
889
- h('span', null, `第 ${pagination.currentPage || hsCodeSearch.page} 页`),
890
- h('div', { className: 'tcw-page-buttons' },
891
- h('button', { className: 'tcw-mini-btn', disabled: hsCodeSearch.loading || !pagination.hasPrevious, onClick: () => searchHsCodePage((pagination.currentPage || hsCodeSearch.page) - 1) }, '上一页'),
892
- visiblePages.map((item) => h('button', {
893
- key: item.page,
894
- className: item.current ? 'tcw-mini-btn active' : 'tcw-mini-btn',
895
- disabled: hsCodeSearch.loading || item.current,
896
- onClick: () => searchHsCodePage(item.page)
897
- }, String(item.page))),
898
- h('button', { className: 'tcw-mini-btn', disabled: hsCodeSearch.loading || !pagination.hasNext, onClick: () => searchHsCodePage((pagination.currentPage || hsCodeSearch.page) + 1) }, '下一页')
899
- )
900
- )
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
+ })
901
927
  }
902
928
 
903
929
  function compactReviewList(items) {
@@ -1042,7 +1068,7 @@
1042
1068
  }
1043
1069
 
1044
1070
  function paginateRows(listKey, rows) {
1045
- const pageSize = 10
1071
+ const pageSize = getPageSize(listKey)
1046
1072
  const total = rows.length
1047
1073
  const totalPages = Math.max(1, Math.ceil(total / pageSize))
1048
1074
  const current = Math.min(Math.max(pageByList[listKey] || 1, 1), totalPages)
@@ -1050,15 +1076,44 @@
1050
1076
  }
1051
1077
 
1052
1078
  function pagination(listKey, total) {
1053
- if (total <= 10) return null
1079
+ if (total <= 0) return null
1054
1080
  const page = paginateRows(listKey, Array.from({ length: total }))
1055
- return h('div', { className: 'tcw-pagination' },
1056
- h('span', null, `共 ${total} 条,第 ${page.page} / ${page.totalPages} 页`),
1057
- h('div', { className: 'tcw-page-buttons' },
1058
- h('button', { className: 'tcw-mini-btn', disabled: page.page <= 1, onClick: () => setPageByList(Object.assign({}, pageByList, { [listKey]: page.page - 1 })) }, '上一页'),
1059
- h('button', { className: 'tcw-mini-btn', disabled: page.page >= page.totalPages, onClick: () => setPageByList(Object.assign({}, pageByList, { [listKey]: page.page + 1 })) }, '下一页')
1060
- )
1061
- )
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]: '' }))
1062
1117
  }
1063
1118
 
1064
1119
  function renderDetailModal() {
@@ -1100,7 +1155,7 @@
1100
1155
  const fields = formDialog.type === 'controlled_goods' ? controlledGoodsFormFields : supplierProductFormFields
1101
1156
  const title = `${formDialog.mode === 'create' ? '新增' : '编辑'}${formDialog.type === 'controlled_goods' ? '管控商品' : '供应商商品'}`
1102
1157
  return h('div', { className: 'tcw-modal-backdrop' },
1103
- h('form', { className: 'tcw-modal tcw-form-modal', onSubmit: saveProductForm },
1158
+ h('form', { className: formDialog.type === 'supplier_product' ? 'tcw-modal tcw-form-modal tcw-form-modal-supplier' : 'tcw-modal tcw-form-modal', onSubmit: saveProductForm },
1104
1159
  h('div', { className: 'tcw-modal-head' },
1105
1160
  h('h2', null, title),
1106
1161
  h('button', { type: 'button', className: 'tcw-icon-btn', onClick: () => setFormDialog(null) }, '×')
@@ -1119,6 +1174,7 @@
1119
1174
 
1120
1175
  function renderSupplierHsCandidatePanel() {
1121
1176
  const candidates = Array.isArray(formDialog.hsCandidates) ? formDialog.hsCandidates : []
1177
+ const pagination = formDialog.hsCandidatePagination || null
1122
1178
  return h('section', { className: 'tcw-hs-candidate-panel' },
1123
1179
  h('div', { className: 'tcw-subsection-head' },
1124
1180
  h('div', null,
@@ -1132,23 +1188,93 @@
1132
1188
  placeholder: '商品名称、型号或海关编码',
1133
1189
  onChange: (event) => setFormDialog(Object.assign({}, formDialog, { hsCandidateKeyword: event.target.value }))
1134
1190
  }),
1135
- h('button', { type: 'button', className: 'tcw-mini-btn', disabled: busy || formDialog.hsCandidateLoading, onClick: searchSupplierHsCandidatesForForm }, formDialog.hsCandidateLoading ? '查询中' : '查询')
1191
+ h('button', { type: 'button', className: 'tcw-mini-btn', disabled: busy || formDialog.hsCandidateLoading, onClick: () => searchSupplierHsCandidatesForForm(1) }, formDialog.hsCandidateLoading ? '查询中' : '查询')
1136
1192
  )
1137
1193
  ),
1138
1194
  formDialog.hsCandidateError ? h('div', { className: 'tcw-error' }, formDialog.hsCandidateError) : null,
1139
- candidates.length ? table(['编码', '商品名称', '英文名称', '单位', '退税率', '监管', '检验检疫', '操作'], candidates, (candidate) => [
1140
- h('strong', { className: 'tcw-hs-code-text' }, value(candidate.code)),
1141
- value(candidate.name),
1142
- value(candidate.englishName),
1143
- value(candidate.unit),
1144
- value(candidate.taxRefundRate),
1145
- value(candidate.regulatoryConditions),
1146
- value(candidate.inspectionQuarantine),
1147
- h('div', { className: 'tcw-row-actions' },
1148
- h('button', { type: 'button', className: 'tcw-mini-btn', onClick: () => selectHsCandidate(candidate) }, '选用'),
1149
- h('button', { type: 'button', className: 'tcw-mini-btn', disabled: !candidate.code && !candidate.detailUrl, onClick: () => loadHsCodeDetail(candidate) }, '详情')
1150
- )
1151
- ], '暂无候选编码。') : empty('暂无候选编码,可调整关键词后查询。')
1195
+ candidates.length ? h('div', { className: 'tcw-hs-candidate-result' },
1196
+ table(['编码', '商品名称', '英文名称', '单位', '退税率', '监管', '检验检疫', '操作'], candidates, (candidate) => [
1197
+ h('strong', { className: 'tcw-hs-code-text' }, value(candidate.code)),
1198
+ value(candidate.name),
1199
+ value(candidate.englishName),
1200
+ value(candidate.unit),
1201
+ value(candidate.taxRefundRate),
1202
+ value(candidate.regulatoryConditions),
1203
+ value(candidate.inspectionQuarantine),
1204
+ h('div', { className: 'tcw-row-actions' },
1205
+ h('button', { type: 'button', className: 'tcw-mini-btn', onClick: () => selectHsCandidate(candidate) }, '选用'),
1206
+ h('button', { type: 'button', className: 'tcw-mini-btn', disabled: !candidate.code && !candidate.detailUrl, onClick: () => loadHsCodeDetail(candidate) }, '详情')
1207
+ )
1208
+ ], '暂无候选编码。'),
1209
+ renderSupplierHsCandidatePagination(pagination)
1210
+ ) : empty('暂无候选编码,可调整关键词后查询。')
1211
+ )
1212
+ }
1213
+
1214
+ function renderSupplierHsCandidatePagination(pagination) {
1215
+ if (!pagination || (!pagination.hasPrevious && !pagination.hasNext && (!pagination.pages || pagination.pages.length <= 1))) return null
1216
+ const pages = Array.isArray(pagination.pages) ? pagination.pages.filter((item) => item.page) : []
1217
+ const currentPage = pagination.currentPage || formDialog.hsCandidatePage || 1
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 }, '跳转')
1152
1278
  )
1153
1279
  }
1154
1280
 
@@ -1340,6 +1466,27 @@
1340
1466
  }
1341
1467
 
1342
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
+ ]
1343
1490
  const controlledGoodsFormFields = [
1344
1491
  { key: 'productName', label: '商品名称', placeholder: '例如:高性能服务器' },
1345
1492
  { key: 'hsCode', label: '海关编码', placeholder: '例如:8471501010' },
@@ -1945,11 +2092,83 @@ button, input, textarea, select { font: inherit; letter-spacing: 0; }
1945
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; }
1946
2093
  .row-selection-checkbox { width: 16px; height: 16px; }
1947
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; }
1948
- .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; }
1949
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; }
1950
2163
  .tcw-modal-backdrop { position: fixed; inset: 0; z-index: 10; display: grid; place-items: center; background: rgba(15, 23, 42, .32); padding: 20px; }
1951
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); }
1952
2165
  .tcw-form-modal { width: min(960px, 100%); }
2166
+ .tcw-form-modal-supplier {
2167
+ width: min(1180px, calc(100vw - 96px));
2168
+ max-height: min(760px, calc(100vh - 56px));
2169
+ gap: 10px;
2170
+ padding: 14px;
2171
+ }
1953
2172
  .tcw-confirm-modal { width: min(480px, 100%); }
1954
2173
  .tcw-hs-detail-modal { width: min(980px, 100%); }
1955
2174
  .tcw-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
@@ -1959,6 +2178,61 @@ button, input, textarea, select { font: inherit; letter-spacing: 0; }
1959
2178
  .tcw-edit-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
1960
2179
  .tcw-edit-grid-two { grid-template-columns: repeat(2, minmax(260px, 1fr)); }
1961
2180
  .tcw-field-wide { grid-column: 1 / -1; }
2181
+ .tcw-form-modal-supplier .tcw-modal-head h2 { font-size: 17px; }
2182
+ .tcw-form-modal-supplier .tcw-edit-grid-two {
2183
+ grid-template-columns: repeat(3, minmax(0, 1fr));
2184
+ gap: 8px 12px;
2185
+ }
2186
+ .tcw-form-modal-supplier .tcw-field { gap: 4px; }
2187
+ .tcw-form-modal-supplier .tcw-field span { font-size: 11px; }
2188
+ .tcw-form-modal-supplier .tcw-field input,
2189
+ .tcw-form-modal-supplier .tcw-field select {
2190
+ min-height: 32px;
2191
+ padding: 6px 9px;
2192
+ }
2193
+ .tcw-form-modal-supplier .tcw-field textarea {
2194
+ min-height: 58px;
2195
+ padding: 7px 9px;
2196
+ }
2197
+ .tcw-form-modal-supplier .tcw-field-wide {
2198
+ grid-column: span 3;
2199
+ }
2200
+ .tcw-form-modal-supplier .tcw-hs-candidate-panel {
2201
+ display: grid;
2202
+ grid-template-columns: minmax(0, 1fr) minmax(320px, .55fr);
2203
+ gap: 10px;
2204
+ align-items: start;
2205
+ border-top: 1px solid var(--tcw-border);
2206
+ padding-top: 10px;
2207
+ }
2208
+ .tcw-form-modal-supplier .tcw-hs-candidate-panel > .tcw-subsection-head {
2209
+ align-items: center;
2210
+ }
2211
+ .tcw-form-modal-supplier .tcw-hs-candidate-panel > .tcw-table-wrap,
2212
+ .tcw-form-modal-supplier .tcw-hs-candidate-panel > .tcw-empty,
2213
+ .tcw-form-modal-supplier .tcw-hs-candidate-panel > .tcw-error {
2214
+ grid-column: 1 / -1;
2215
+ max-height: 210px;
2216
+ overflow: auto;
2217
+ }
2218
+ .tcw-form-modal-supplier .tcw-hs-candidate-result {
2219
+ display: grid;
2220
+ gap: 8px;
2221
+ grid-column: 1 / -1;
2222
+ }
2223
+ .tcw-form-modal-supplier .tcw-hs-candidate-result .tcw-table-wrap {
2224
+ max-height: 210px;
2225
+ }
2226
+ .tcw-form-modal-supplier .tcw-hs-candidate-search {
2227
+ grid-template-columns: minmax(0, 1fr) auto;
2228
+ }
2229
+ .tcw-form-modal-supplier .tcw-modal-actions {
2230
+ position: sticky;
2231
+ bottom: -14px;
2232
+ border-top: 1px solid var(--tcw-border);
2233
+ background: var(--tcw-card);
2234
+ padding-top: 10px;
2235
+ }
1962
2236
  .tcw-confirm-text { margin: 0; color: var(--tcw-text); font-size: 14px; line-height: 1.7; }
1963
2237
  .tcw-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
1964
2238
  .tcw-detail-field { display: grid; grid-template-columns: 108px minmax(0, 1fr); gap: 8px; align-items: start; border: 1px solid var(--tcw-border); border-radius: 6px; background: var(--tcw-soft); padding: 8px 10px; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chenchaolong/plugin-trade-compliance-workbench",
3
- "version": "0.1.47",
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",