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

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.
@@ -194,15 +194,17 @@
194
194
  }
195
195
  }
196
196
 
197
- async function searchSupplierHsCandidatesForForm() {
197
+ async function searchSupplierHsCandidatesForForm(page) {
198
198
  if (!formDialog || formDialog.type !== 'supplier_product' || busy) return
199
199
  const keyword = String(formDialog.hsCandidateKeyword || buildSupplierHsCandidateKeyword(formDialog.values) || '').trim()
200
200
  if (!keyword) {
201
201
  setFormDialog(Object.assign({}, formDialog, { hsCandidateError: '请输入商品名称、型号或海关编码后再查询。' }))
202
202
  return
203
203
  }
204
+ const nextPage = Math.max(1, Number(page) || 1)
204
205
  setFormDialog(Object.assign({}, formDialog, {
205
206
  hsCandidateKeyword: keyword,
207
+ hsCandidatePage: nextPage,
206
208
  hsCandidateLoading: true,
207
209
  hsCandidateError: '',
208
210
  hsCandidateStatus: ''
@@ -210,20 +212,22 @@
210
212
  try {
211
213
  const response = await executeAction('search_hs_code', null, {
212
214
  keywords: keyword,
213
- page: 1,
215
+ page: nextPage,
214
216
  filterFailureCode: true,
215
217
  displayChapter: false,
216
218
  displayEnName: true
217
219
  }, {})
218
220
  assertActionSuccess(response)
219
221
  const result = getActionDataPayload(response)
220
- const candidates = Array.isArray(result && result.results) ? result.results.slice(0, 10) : []
222
+ const candidates = Array.isArray(result && result.results) ? result.results : []
221
223
  setFormDialog((current) => current ? Object.assign({}, current, {
222
224
  hsCandidateKeyword: keyword,
225
+ hsCandidatePage: nextPage,
223
226
  hsCandidateLoading: false,
224
227
  hsCandidateError: '',
225
228
  hsCandidateStatus: candidates.length ? 'pending_confirmation' : 'not_found',
226
- hsCandidates: candidates
229
+ hsCandidates: candidates,
230
+ hsCandidatePagination: result && result.pagination ? result.pagination : null
227
231
  }) : current)
228
232
  } catch (error) {
229
233
  setFormDialog((current) => current ? Object.assign({}, current, {
@@ -466,6 +470,8 @@
466
470
  values: valuesFromItem(item.type, item),
467
471
  hsCandidates: Array.isArray(merged.hsCodeCandidates) ? merged.hsCodeCandidates : [],
468
472
  hsCandidateKeyword: merged.hsCodeLookupKeyword || buildSupplierHsCandidateKeyword(merged),
473
+ hsCandidatePage: 1,
474
+ hsCandidatePagination: null,
469
475
  hsCandidateStatus: merged.hsCodeLookupStatus || '',
470
476
  hsCandidateError: merged.hsCodeLookupError || '',
471
477
  hsCandidateLoading: false
@@ -480,6 +486,8 @@
480
486
  values: emptyFormValues(type),
481
487
  hsCandidates: [],
482
488
  hsCandidateKeyword: '',
489
+ hsCandidatePage: 1,
490
+ hsCandidatePagination: null,
483
491
  hsCandidateStatus: '',
484
492
  hsCandidateError: '',
485
493
  hsCandidateLoading: false
@@ -630,7 +638,6 @@
630
638
  )
631
639
  )
632
640
  ),
633
- renderAssistantPanel(),
634
641
  detailItem ? renderDetailModal() : null,
635
642
  formDialog ? renderFormModal() : null,
636
643
  deleteDialog ? renderDeleteConfirmModal() : null,
@@ -656,34 +663,6 @@
656
663
  )
657
664
  }
658
665
 
659
- function renderAssistantPanel() {
660
- return h('aside', { className: 'tcw-assistant' },
661
- h('div', { className: 'tcw-assistant-head' },
662
- h('span', { className: 'tcw-assistant-mark' }, 'AI'),
663
- h('div', null,
664
- h('strong', null, '外贸合规助手'),
665
- h('p', null, '按流程处理识别结果,减少来回切换。')
666
- )
667
- ),
668
- h('section', { className: 'tcw-assistant-card' },
669
- h('h3', null, '当前建议'),
670
- h('p', null, assistantTip(activePage))
671
- ),
672
- h('section', { className: 'tcw-assistant-card' },
673
- h('h3', null, '快捷入口'),
674
- h('div', { className: 'tcw-assistant-actions' }, [
675
- h('button', { className: 'tcw-mini-btn', onClick: () => setActivePage('controlled-goods-page') }, '管控商品'),
676
- h('button', { className: 'tcw-mini-btn', onClick: () => setActivePage('products-page') }, '商品审核'),
677
- h('button', { className: 'tcw-mini-btn', onClick: () => setActivePage('hs-code-search-page') }, '查 HS 编码')
678
- ])
679
- ),
680
- h('section', { className: 'tcw-assistant-card' },
681
- h('h3', null, '处理顺序'),
682
- h('p', null, '先把管控规则和供应商商品确认入库,再审核购销合同并生成销售发票。')
683
- )
684
- )
685
- }
686
-
687
666
  function tab(key, label, icon, count, tone) {
688
667
  return h('button', { key, className: key === activePage ? 'tcw-tab active' : 'tcw-tab', onClick: () => setActivePage(key) },
689
668
  h('span', { className: 'tcw-nav-icon ' + (tone || 'blue') }, iconImage(icon, label, tone)),
@@ -1129,7 +1108,7 @@
1129
1108
  const fields = formDialog.type === 'controlled_goods' ? controlledGoodsFormFields : supplierProductFormFields
1130
1109
  const title = `${formDialog.mode === 'create' ? '新增' : '编辑'}${formDialog.type === 'controlled_goods' ? '管控商品' : '供应商商品'}`
1131
1110
  return h('div', { className: 'tcw-modal-backdrop' },
1132
- h('form', { className: 'tcw-modal tcw-form-modal', onSubmit: saveProductForm },
1111
+ h('form', { className: formDialog.type === 'supplier_product' ? 'tcw-modal tcw-form-modal tcw-form-modal-supplier' : 'tcw-modal tcw-form-modal', onSubmit: saveProductForm },
1133
1112
  h('div', { className: 'tcw-modal-head' },
1134
1113
  h('h2', null, title),
1135
1114
  h('button', { type: 'button', className: 'tcw-icon-btn', onClick: () => setFormDialog(null) }, '×')
@@ -1148,6 +1127,7 @@
1148
1127
 
1149
1128
  function renderSupplierHsCandidatePanel() {
1150
1129
  const candidates = Array.isArray(formDialog.hsCandidates) ? formDialog.hsCandidates : []
1130
+ const pagination = formDialog.hsCandidatePagination || null
1151
1131
  return h('section', { className: 'tcw-hs-candidate-panel' },
1152
1132
  h('div', { className: 'tcw-subsection-head' },
1153
1133
  h('div', null,
@@ -1161,23 +1141,46 @@
1161
1141
  placeholder: '商品名称、型号或海关编码',
1162
1142
  onChange: (event) => setFormDialog(Object.assign({}, formDialog, { hsCandidateKeyword: event.target.value }))
1163
1143
  }),
1164
- h('button', { type: 'button', className: 'tcw-mini-btn', disabled: busy || formDialog.hsCandidateLoading, onClick: searchSupplierHsCandidatesForForm }, formDialog.hsCandidateLoading ? '查询中' : '查询')
1144
+ h('button', { type: 'button', className: 'tcw-mini-btn', disabled: busy || formDialog.hsCandidateLoading, onClick: () => searchSupplierHsCandidatesForForm(1) }, formDialog.hsCandidateLoading ? '查询中' : '查询')
1165
1145
  )
1166
1146
  ),
1167
1147
  formDialog.hsCandidateError ? h('div', { className: 'tcw-error' }, formDialog.hsCandidateError) : null,
1168
- candidates.length ? table(['编码', '商品名称', '英文名称', '单位', '退税率', '监管', '检验检疫', '操作'], candidates, (candidate) => [
1169
- h('strong', { className: 'tcw-hs-code-text' }, value(candidate.code)),
1170
- value(candidate.name),
1171
- value(candidate.englishName),
1172
- value(candidate.unit),
1173
- value(candidate.taxRefundRate),
1174
- value(candidate.regulatoryConditions),
1175
- value(candidate.inspectionQuarantine),
1176
- h('div', { className: 'tcw-row-actions' },
1177
- h('button', { type: 'button', className: 'tcw-mini-btn', onClick: () => selectHsCandidate(candidate) }, '选用'),
1178
- h('button', { type: 'button', className: 'tcw-mini-btn', disabled: !candidate.code && !candidate.detailUrl, onClick: () => loadHsCodeDetail(candidate) }, '详情')
1179
- )
1180
- ], '暂无候选编码。') : empty('暂无候选编码,可调整关键词后查询。')
1148
+ candidates.length ? h('div', { className: 'tcw-hs-candidate-result' },
1149
+ table(['编码', '商品名称', '英文名称', '单位', '退税率', '监管', '检验检疫', '操作'], candidates, (candidate) => [
1150
+ h('strong', { className: 'tcw-hs-code-text' }, value(candidate.code)),
1151
+ value(candidate.name),
1152
+ value(candidate.englishName),
1153
+ value(candidate.unit),
1154
+ value(candidate.taxRefundRate),
1155
+ value(candidate.regulatoryConditions),
1156
+ value(candidate.inspectionQuarantine),
1157
+ h('div', { className: 'tcw-row-actions' },
1158
+ h('button', { type: 'button', className: 'tcw-mini-btn', onClick: () => selectHsCandidate(candidate) }, '选用'),
1159
+ h('button', { type: 'button', className: 'tcw-mini-btn', disabled: !candidate.code && !candidate.detailUrl, onClick: () => loadHsCodeDetail(candidate) }, '详情')
1160
+ )
1161
+ ], '暂无候选编码。'),
1162
+ renderSupplierHsCandidatePagination(pagination)
1163
+ ) : empty('暂无候选编码,可调整关键词后查询。')
1164
+ )
1165
+ }
1166
+
1167
+ function renderSupplierHsCandidatePagination(pagination) {
1168
+ if (!pagination || (!pagination.hasPrevious && !pagination.hasNext && (!pagination.pages || pagination.pages.length <= 1))) return null
1169
+ 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
+ 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
+ )
1181
1184
  )
1182
1185
  }
1183
1186
 
@@ -1859,14 +1862,6 @@
1859
1862
  return 'Workspace / 工具'
1860
1863
  }
1861
1864
 
1862
- function assistantTip(key) {
1863
- if (key === 'overview-page') return '从总览开始,优先确认管控规则和供应商商品,再推进发票生成。'
1864
- if (key === 'controlled-goods-page') return '维护管控商品时,建议先核对 HS 编码和关键词,再保存入库。'
1865
- if (key === 'products-page') return '供应商商品可先选用候选 HS 编码,再检查英文品名和管控状态。'
1866
- if (key === 'workbooks-page') return '购销合同审核完毕后,再生成销售发票,避免后面重改。'
1867
- return '海关编码查询页用于人工确认候选结果,必要时可回到商品页继续处理。'
1868
- }
1869
-
1870
1865
  function isObject(value) {
1871
1866
  return value && typeof value === 'object' && !Array.isArray(value)
1872
1867
  }
@@ -1987,6 +1982,12 @@ button, input, textarea, select { font: inherit; letter-spacing: 0; }
1987
1982
  .tcw-modal-backdrop { position: fixed; inset: 0; z-index: 10; display: grid; place-items: center; background: rgba(15, 23, 42, .32); padding: 20px; }
1988
1983
  .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); }
1989
1984
  .tcw-form-modal { width: min(960px, 100%); }
1985
+ .tcw-form-modal-supplier {
1986
+ width: min(1180px, calc(100vw - 96px));
1987
+ max-height: min(760px, calc(100vh - 56px));
1988
+ gap: 10px;
1989
+ padding: 14px;
1990
+ }
1990
1991
  .tcw-confirm-modal { width: min(480px, 100%); }
1991
1992
  .tcw-hs-detail-modal { width: min(980px, 100%); }
1992
1993
  .tcw-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
@@ -1996,6 +1997,61 @@ button, input, textarea, select { font: inherit; letter-spacing: 0; }
1996
1997
  .tcw-edit-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
1997
1998
  .tcw-edit-grid-two { grid-template-columns: repeat(2, minmax(260px, 1fr)); }
1998
1999
  .tcw-field-wide { grid-column: 1 / -1; }
2000
+ .tcw-form-modal-supplier .tcw-modal-head h2 { font-size: 17px; }
2001
+ .tcw-form-modal-supplier .tcw-edit-grid-two {
2002
+ grid-template-columns: repeat(3, minmax(0, 1fr));
2003
+ gap: 8px 12px;
2004
+ }
2005
+ .tcw-form-modal-supplier .tcw-field { gap: 4px; }
2006
+ .tcw-form-modal-supplier .tcw-field span { font-size: 11px; }
2007
+ .tcw-form-modal-supplier .tcw-field input,
2008
+ .tcw-form-modal-supplier .tcw-field select {
2009
+ min-height: 32px;
2010
+ padding: 6px 9px;
2011
+ }
2012
+ .tcw-form-modal-supplier .tcw-field textarea {
2013
+ min-height: 58px;
2014
+ padding: 7px 9px;
2015
+ }
2016
+ .tcw-form-modal-supplier .tcw-field-wide {
2017
+ grid-column: span 3;
2018
+ }
2019
+ .tcw-form-modal-supplier .tcw-hs-candidate-panel {
2020
+ display: grid;
2021
+ grid-template-columns: minmax(0, 1fr) minmax(320px, .55fr);
2022
+ gap: 10px;
2023
+ align-items: start;
2024
+ border-top: 1px solid var(--tcw-border);
2025
+ padding-top: 10px;
2026
+ }
2027
+ .tcw-form-modal-supplier .tcw-hs-candidate-panel > .tcw-subsection-head {
2028
+ align-items: center;
2029
+ }
2030
+ .tcw-form-modal-supplier .tcw-hs-candidate-panel > .tcw-table-wrap,
2031
+ .tcw-form-modal-supplier .tcw-hs-candidate-panel > .tcw-empty,
2032
+ .tcw-form-modal-supplier .tcw-hs-candidate-panel > .tcw-error {
2033
+ grid-column: 1 / -1;
2034
+ max-height: 210px;
2035
+ overflow: auto;
2036
+ }
2037
+ .tcw-form-modal-supplier .tcw-hs-candidate-result {
2038
+ display: grid;
2039
+ gap: 8px;
2040
+ grid-column: 1 / -1;
2041
+ }
2042
+ .tcw-form-modal-supplier .tcw-hs-candidate-result .tcw-table-wrap {
2043
+ max-height: 210px;
2044
+ }
2045
+ .tcw-form-modal-supplier .tcw-hs-candidate-search {
2046
+ grid-template-columns: minmax(0, 1fr) auto;
2047
+ }
2048
+ .tcw-form-modal-supplier .tcw-modal-actions {
2049
+ position: sticky;
2050
+ bottom: -14px;
2051
+ border-top: 1px solid var(--tcw-border);
2052
+ background: var(--tcw-card);
2053
+ padding-top: 10px;
2054
+ }
1999
2055
  .tcw-confirm-text { margin: 0; color: var(--tcw-text); font-size: 14px; line-height: 1.7; }
2000
2056
  .tcw-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
2001
2057
  .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; }
@@ -2059,7 +2115,7 @@ button, input, textarea, select { font: inherit; letter-spacing: 0; }
2059
2115
  body { background: var(--tcw-bg); letter-spacing: 0; }
2060
2116
  .tcw-shell {
2061
2117
  display: grid;
2062
- grid-template-columns: 248px minmax(0, 1fr) 300px;
2118
+ grid-template-columns: 248px minmax(0, 1fr);
2063
2119
  grid-template-rows: 1fr;
2064
2120
  gap: 0;
2065
2121
  min-height: 100vh;
@@ -2262,68 +2318,12 @@ body { background: var(--tcw-bg); letter-spacing: 0; }
2262
2318
  }
2263
2319
  .tcw-table td { background: var(--tcw-card); }
2264
2320
  .tcw-table tr:hover td { background: #fbfdff; }
2265
- .tcw-assistant {
2266
- display: grid;
2267
- align-content: start;
2268
- gap: 12px;
2269
- border-left: 1px solid var(--tcw-border);
2270
- background: var(--tcw-card);
2271
- padding: 18px 14px;
2272
- }
2273
- .tcw-assistant-head {
2274
- display: flex;
2275
- align-items: flex-start;
2276
- gap: 10px;
2277
- padding: 2px 4px 8px;
2278
- }
2279
- .tcw-assistant-mark {
2280
- display: grid;
2281
- place-items: center;
2282
- width: 32px;
2283
- height: 32px;
2284
- border-radius: 8px;
2285
- background: #e7f8ef;
2286
- color: var(--tcw-success);
2287
- font-size: 12px;
2288
- font-weight: 900;
2289
- }
2290
- .tcw-assistant-head strong { display: block; font-size: 15px; }
2291
- .tcw-assistant-head p {
2292
- margin: 3px 0 0;
2293
- color: var(--tcw-muted);
2294
- font-size: 12px;
2295
- line-height: 1.5;
2296
- }
2297
- .tcw-assistant-card {
2298
- display: grid;
2299
- gap: 8px;
2300
- border: 1px solid var(--tcw-border);
2301
- border-radius: 8px;
2302
- background: var(--tcw-soft);
2303
- padding: 12px;
2304
- }
2305
- .tcw-assistant-card h3 {
2306
- margin: 0;
2307
- font-size: 13px;
2308
- }
2309
- .tcw-assistant-card p {
2310
- margin: 0;
2311
- color: var(--tcw-muted);
2312
- font-size: 12px;
2313
- line-height: 1.6;
2314
- }
2315
- .tcw-assistant-actions {
2316
- display: flex;
2317
- flex-wrap: wrap;
2318
- gap: 8px;
2319
- }
2320
2321
  .tcw-status.ok { background: #e7f8ef; color: var(--tcw-success); }
2321
2322
  .tcw-status.warn { background: #fff2df; color: #b35b00; }
2322
2323
  .tcw-status.danger { background: #fde8e6; color: var(--tcw-danger); }
2323
2324
  .tcw-status.muted { background: #eef2f6; color: var(--tcw-muted); }
2324
2325
  @media (max-width: 1280px) {
2325
2326
  .tcw-shell { grid-template-columns: 230px minmax(0, 1fr); }
2326
- .tcw-assistant { display: none; }
2327
2327
  }
2328
2328
  @media (max-width: 900px) {
2329
2329
  .tcw-shell { grid-template-columns: 1fr; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chenchaolong/plugin-trade-compliance-workbench",
3
- "version": "0.1.46",
3
+ "version": "0.1.48",
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",