@chenchaolong/plugin-trade-compliance-workbench 0.1.74 → 1.0.1

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.
Files changed (180) hide show
  1. package/README.md +41 -22
  2. package/dist/assets/templates/CI.xlsx +0 -0
  3. package/dist/assets/templates/CUSTOMS_DECLARATION.xlsx +0 -0
  4. package/dist/assets/templates/PI.xlsx +0 -0
  5. package/dist/assets/templates/PL.xlsx +0 -0
  6. package/dist/index.d.ts +115 -228
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +19 -108
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/{document-text-extractor.d.ts → adapters/document-text-extractor.d.ts} +2 -2
  11. package/dist/lib/adapters/document-text-extractor.d.ts.map +1 -0
  12. package/dist/lib/{document-text-extractor.js → adapters/document-text-extractor.js} +8 -2
  13. package/dist/lib/adapters/document-text-extractor.js.map +1 -0
  14. package/dist/lib/adapters/file-storage.d.ts +9 -0
  15. package/dist/lib/adapters/file-storage.d.ts.map +1 -0
  16. package/dist/lib/adapters/file-storage.js +37 -0
  17. package/dist/lib/adapters/file-storage.js.map +1 -0
  18. package/dist/lib/{trade-compliance.enrichment.d.ts → adapters/hsbianma.provider.d.ts} +6 -6
  19. package/dist/lib/adapters/hsbianma.provider.d.ts.map +1 -0
  20. package/dist/lib/{trade-compliance.enrichment.js → adapters/hsbianma.provider.js} +1 -1
  21. package/dist/lib/adapters/hsbianma.provider.js.map +1 -0
  22. package/dist/lib/adapters/translation.provider.d.ts +4 -0
  23. package/dist/lib/adapters/translation.provider.d.ts.map +1 -0
  24. package/dist/lib/adapters/translation.provider.js +35 -0
  25. package/dist/lib/adapters/translation.provider.js.map +1 -0
  26. package/dist/lib/constants.d.ts +17 -11
  27. package/dist/lib/constants.d.ts.map +1 -1
  28. package/dist/lib/constants.js +17 -21
  29. package/dist/lib/constants.js.map +1 -1
  30. package/dist/lib/domain/contract-number.d.ts +14 -0
  31. package/dist/lib/domain/contract-number.d.ts.map +1 -0
  32. package/dist/lib/domain/contract-number.js +36 -0
  33. package/dist/lib/domain/contract-number.js.map +1 -0
  34. package/dist/lib/domain/default-settings.d.ts +28 -0
  35. package/dist/lib/domain/default-settings.d.ts.map +1 -0
  36. package/dist/lib/domain/default-settings.js +16 -0
  37. package/dist/lib/domain/default-settings.js.map +1 -0
  38. package/dist/lib/domain/finance.d.ts +37 -0
  39. package/dist/lib/domain/finance.d.ts.map +1 -0
  40. package/dist/lib/domain/finance.js +60 -0
  41. package/dist/lib/domain/finance.js.map +1 -0
  42. package/dist/lib/domain/import-issues.d.ts +10 -0
  43. package/dist/lib/domain/import-issues.d.ts.map +1 -0
  44. package/dist/lib/domain/import-issues.js +23 -0
  45. package/dist/lib/domain/import-issues.js.map +1 -0
  46. package/dist/lib/domain/normalization.d.ts +8 -0
  47. package/dist/lib/domain/normalization.d.ts.map +1 -0
  48. package/dist/lib/domain/normalization.js +50 -0
  49. package/dist/lib/domain/normalization.js.map +1 -0
  50. package/dist/lib/domain/workflow.d.ts +4 -0
  51. package/dist/lib/domain/workflow.d.ts.map +1 -0
  52. package/dist/lib/domain/workflow.js +14 -0
  53. package/dist/lib/domain/workflow.js.map +1 -0
  54. package/dist/lib/entities/base.d.ts +20 -0
  55. package/dist/lib/entities/base.d.ts.map +1 -0
  56. package/dist/lib/entities/base.js +51 -0
  57. package/dist/lib/entities/base.js.map +1 -0
  58. package/dist/lib/entities/catalog.entity.d.ts +80 -0
  59. package/dist/lib/entities/catalog.entity.d.ts.map +1 -0
  60. package/dist/lib/entities/catalog.entity.js +308 -0
  61. package/dist/lib/entities/catalog.entity.js.map +1 -0
  62. package/dist/lib/entities/customer.entity.d.ts +72 -0
  63. package/dist/lib/entities/customer.entity.d.ts.map +1 -0
  64. package/dist/lib/entities/customer.entity.js +276 -0
  65. package/dist/lib/entities/customer.entity.js.map +1 -0
  66. package/dist/lib/entities/index.d.ts +12 -6
  67. package/dist/lib/entities/index.d.ts.map +1 -1
  68. package/dist/lib/entities/index.js +20 -7
  69. package/dist/lib/entities/index.js.map +1 -1
  70. package/dist/lib/entities/procurement.entity.d.ts +99 -0
  71. package/dist/lib/entities/procurement.entity.d.ts.map +1 -0
  72. package/dist/lib/entities/procurement.entity.js +386 -0
  73. package/dist/lib/entities/procurement.entity.js.map +1 -0
  74. package/dist/lib/entities/settings.entity.d.ts +64 -0
  75. package/dist/lib/entities/settings.entity.d.ts.map +1 -0
  76. package/dist/lib/entities/settings.entity.js +243 -0
  77. package/dist/lib/entities/settings.entity.js.map +1 -0
  78. package/dist/lib/entities/shared.entity.d.ts +40 -0
  79. package/dist/lib/entities/shared.entity.d.ts.map +1 -0
  80. package/dist/lib/entities/shared.entity.js +149 -0
  81. package/dist/lib/entities/shared.entity.js.map +1 -0
  82. package/dist/lib/remote-components/trade-compliance-workbench/app.js +245 -2664
  83. package/dist/lib/remote-components/trade-compliance-workbench/editor-source.mjs +53 -0
  84. package/dist/lib/remote-components/trade-compliance-workbench/editor.css +1 -0
  85. package/dist/lib/remote-components/trade-compliance-workbench/editor.js +533 -0
  86. package/dist/lib/sales-file.service.d.ts +29 -0
  87. package/dist/lib/sales-file.service.d.ts.map +1 -0
  88. package/dist/lib/sales-file.service.js +300 -0
  89. package/dist/lib/sales-file.service.js.map +1 -0
  90. package/dist/lib/trade-compliance.config.d.ts +94 -187
  91. package/dist/lib/trade-compliance.config.d.ts.map +1 -1
  92. package/dist/lib/trade-compliance.config.js +12 -96
  93. package/dist/lib/trade-compliance.config.js.map +1 -1
  94. package/dist/lib/types.d.ts +104 -1
  95. package/dist/lib/types.d.ts.map +1 -1
  96. package/dist/lib/types.js +47 -1
  97. package/dist/lib/types.js.map +1 -1
  98. package/dist/lib/workbench-view.provider.d.ts +276 -0
  99. package/dist/lib/workbench-view.provider.d.ts.map +1 -0
  100. package/dist/lib/workbench-view.provider.js +222 -0
  101. package/dist/lib/workbench-view.provider.js.map +1 -0
  102. package/dist/lib/workbench.middleware.d.ts +351 -0
  103. package/dist/lib/workbench.middleware.d.ts.map +1 -0
  104. package/dist/lib/workbench.middleware.js +48 -0
  105. package/dist/lib/workbench.middleware.js.map +1 -0
  106. package/dist/lib/workbench.plugin.d.ts +5 -0
  107. package/dist/lib/workbench.plugin.d.ts.map +1 -0
  108. package/dist/lib/workbench.plugin.js +24 -0
  109. package/dist/lib/workbench.plugin.js.map +1 -0
  110. package/dist/lib/workbench.service.d.ts +257 -0
  111. package/dist/lib/workbench.service.d.ts.map +1 -0
  112. package/dist/lib/workbench.service.js +755 -0
  113. package/dist/lib/workbench.service.js.map +1 -0
  114. package/package.json +19 -25
  115. package/dist/assets/customs-workbook-template.xls +0 -0
  116. package/dist/lib/controlled-goods-extracted-text-store.d.ts +0 -29
  117. package/dist/lib/controlled-goods-extracted-text-store.d.ts.map +0 -1
  118. package/dist/lib/controlled-goods-extracted-text-store.js +0 -42
  119. package/dist/lib/controlled-goods-extracted-text-store.js.map +0 -1
  120. package/dist/lib/controlled-goods-file-parser.d.ts +0 -9
  121. package/dist/lib/controlled-goods-file-parser.d.ts.map +0 -1
  122. package/dist/lib/controlled-goods-file-parser.js +0 -298
  123. package/dist/lib/controlled-goods-file-parser.js.map +0 -1
  124. package/dist/lib/document-text-extractor.d.ts.map +0 -1
  125. package/dist/lib/document-text-extractor.js.map +0 -1
  126. package/dist/lib/entities/controlled-goods-record.entity.d.ts +0 -5
  127. package/dist/lib/entities/controlled-goods-record.entity.d.ts.map +0 -1
  128. package/dist/lib/entities/controlled-goods-record.entity.js +0 -68
  129. package/dist/lib/entities/controlled-goods-record.entity.js.map +0 -1
  130. package/dist/lib/entities/customs-workbook-generation.entity.d.ts +0 -5
  131. package/dist/lib/entities/customs-workbook-generation.entity.d.ts.map +0 -1
  132. package/dist/lib/entities/customs-workbook-generation.entity.js +0 -72
  133. package/dist/lib/entities/customs-workbook-generation.entity.js.map +0 -1
  134. package/dist/lib/entities/trade-compliance-import-batch.entity.d.ts +0 -5
  135. package/dist/lib/entities/trade-compliance-import-batch.entity.d.ts.map +0 -1
  136. package/dist/lib/entities/trade-compliance-import-batch.entity.js +0 -60
  137. package/dist/lib/entities/trade-compliance-import-batch.entity.js.map +0 -1
  138. package/dist/lib/entities/trade-compliance-review-item.entity.d.ts +0 -5
  139. package/dist/lib/entities/trade-compliance-review-item.entity.d.ts.map +0 -1
  140. package/dist/lib/entities/trade-compliance-review-item.entity.js +0 -84
  141. package/dist/lib/entities/trade-compliance-review-item.entity.js.map +0 -1
  142. package/dist/lib/entities/trade-product.entity.d.ts +0 -5
  143. package/dist/lib/entities/trade-product.entity.d.ts.map +0 -1
  144. package/dist/lib/entities/trade-product.entity.js +0 -105
  145. package/dist/lib/entities/trade-product.entity.js.map +0 -1
  146. package/dist/lib/entities/trade-supplier.entity.d.ts +0 -5
  147. package/dist/lib/entities/trade-supplier.entity.d.ts.map +0 -1
  148. package/dist/lib/entities/trade-supplier.entity.js +0 -55
  149. package/dist/lib/entities/trade-supplier.entity.js.map +0 -1
  150. package/dist/lib/trade-compliance-workbench-view.provider.d.ts +0 -129
  151. package/dist/lib/trade-compliance-workbench-view.provider.d.ts.map +0 -1
  152. package/dist/lib/trade-compliance-workbench-view.provider.js +0 -863
  153. package/dist/lib/trade-compliance-workbench-view.provider.js.map +0 -1
  154. package/dist/lib/trade-compliance-workbench.middleware.d.ts +0 -485
  155. package/dist/lib/trade-compliance-workbench.middleware.d.ts.map +0 -1
  156. package/dist/lib/trade-compliance-workbench.middleware.js +0 -494
  157. package/dist/lib/trade-compliance-workbench.middleware.js.map +0 -1
  158. package/dist/lib/trade-compliance-workbench.plugin.d.ts +0 -11
  159. package/dist/lib/trade-compliance-workbench.plugin.d.ts.map +0 -1
  160. package/dist/lib/trade-compliance-workbench.plugin.js +0 -39
  161. package/dist/lib/trade-compliance-workbench.plugin.js.map +0 -1
  162. package/dist/lib/trade-compliance-workbench.service.d.ts +0 -33
  163. package/dist/lib/trade-compliance-workbench.service.d.ts.map +0 -1
  164. package/dist/lib/trade-compliance-workbench.service.js +0 -369
  165. package/dist/lib/trade-compliance-workbench.service.js.map +0 -1
  166. package/dist/lib/trade-compliance-workbench.templates.d.ts +0 -31
  167. package/dist/lib/trade-compliance-workbench.templates.d.ts.map +0 -1
  168. package/dist/lib/trade-compliance-workbench.templates.js +0 -60
  169. package/dist/lib/trade-compliance-workbench.templates.js.map +0 -1
  170. package/dist/lib/trade-compliance-workbook.d.ts +0 -28
  171. package/dist/lib/trade-compliance-workbook.d.ts.map +0 -1
  172. package/dist/lib/trade-compliance-workbook.js +0 -546
  173. package/dist/lib/trade-compliance-workbook.js.map +0 -1
  174. package/dist/lib/trade-compliance.enrichment.d.ts.map +0 -1
  175. package/dist/lib/trade-compliance.enrichment.js.map +0 -1
  176. package/dist/lib/trade-compliance.matching.d.ts +0 -10
  177. package/dist/lib/trade-compliance.matching.d.ts.map +0 -1
  178. package/dist/lib/trade-compliance.matching.js +0 -63
  179. package/dist/lib/trade-compliance.matching.js.map +0 -1
  180. package/dist/xpert-trade-compliance-workbench-assistant.yaml +0 -27
@@ -1,2696 +1,277 @@
1
- ;(function () {
2
- const CHANNEL = 'xpertai.remote_component'
3
- const VERSION = 1
4
- const REQUEST_TIMEOUT_MS = 120000
1
+ (function () {
2
+ 'use strict'
3
+ const React = window.React
4
+ const ReactDOM = window.ReactDOM
5
5
  const h = React.createElement
6
+ const CHANNEL = 'xpert.remote-ui'
7
+ const VERSION = 1
6
8
  let instanceId = null
7
- let requestSequence = 0
9
+ let sequence = 0
8
10
  const pending = new Map()
9
11
 
10
- injectStyles()
11
-
12
- function post(type, body, transfer) {
13
- if (!instanceId && type !== 'ready') return
14
- parent.postMessage(Object.assign({ channel: CHANNEL, protocolVersion: VERSION, instanceId, type }, body || {}), '*', transfer || [])
15
- }
16
-
17
- function request(type, body, transfer) {
18
- const requestId = String(++requestSequence)
19
- return new Promise((resolve, reject) => {
20
- pending.set(requestId, { resolve, reject })
21
- post(type, Object.assign({ requestId }, body || {}), transfer)
22
- setTimeout(() => {
23
- if (!pending.has(requestId)) return
24
- pending.delete(requestId)
25
- reject(new Error('请求超时'))
26
- }, REQUEST_TIMEOUT_MS)
27
- })
28
- }
29
-
30
- function reportResize() {
31
- post('resize', { height: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, 720) })
32
- }
33
-
34
- async function toFilePayload(file) {
35
- const buffer = await file.arrayBuffer()
36
- return {
37
- payload: { name: file.name, type: file.type, size: file.size, lastModified: file.lastModified, buffer },
38
- transfer: [buffer]
39
- }
40
- }
41
-
42
- function executeAction(actionKey, targetId, input, parameters) {
43
- return request('executeAction', { actionKey, targetId, input, parameters })
44
- }
45
-
46
- async function executeFileAction(actionKey, file, input, parameters) {
47
- const filePayload = await toFilePayload(file)
48
- return request('executeFileAction', { actionKey, file: filePayload.payload, input, parameters }, filePayload.transfer)
49
- }
50
-
51
- function notify(level, message) {
52
- post('notify', { level, message })
53
- }
12
+ function post(type, body, transfer) { parent.postMessage(Object.assign({ channel: CHANNEL, protocolVersion: VERSION, instanceId, type }, body || {}), '*', transfer || []) }
13
+ function request(type, body) { return new Promise((resolve, reject) => { const requestId = `tcw-${Date.now()}-${++sequence}`; pending.set(requestId, { resolve, reject }); post(type, Object.assign({ requestId }, body || {})) }) }
14
+ function action(actionKey, input) { return request('executeAction', { actionKey, input: input || {}, parameters: {} }).then(payload) }
15
+ async function fileAction(actionKey, file, input) { const buffer = await file.arrayBuffer(); return request('executeFileAction', { actionKey, input: input || {}, parameters: {}, file: { name: file.name, originalname: file.name, type: file.type, size: file.size, lastModified: file.lastModified, buffer } }, [buffer]).then(payload) }
16
+ function payload(message) { return message && (message.payload || message.data || message.result) || message }
17
+ function dataOf(response) { const value = payload(response); return value && (value.data !== undefined ? value.data : value) }
18
+ function messageOf(value) { const raw = value && (value.error && value.error.message || value.message); return typeof raw === 'object' ? raw.zh_Hans || raw.en_US : raw || '' }
19
+ function reportResize() { post('resize', { height: Math.max(760, document.documentElement.scrollHeight), width: document.documentElement.scrollWidth }) }
20
+
21
+ const pages = [
22
+ ['home', '首页', '⌂'], ['controlled', '管控商品目录', '▣'], ['sanctions', '公司制裁目录', '▤'], ['purchase', '采购合规管理', '◫'],
23
+ ['contract-review', '客户合同审核', '▧'], ['sales', '销售合规管理', '▥'], ['files', '销售文件管理', '□'], ['analytics', '数据分析', '◔'], ['settings', '系统设置', '⚙']
24
+ ]
54
25
 
55
- async function dispatchAssistantCommands(payload) {
56
- const commands = collectAssistantCommands(payload)
57
- let dispatched = 0
58
- for (const command of commands) {
59
- const response = await request('invokeClientCommand', { commandKey: command.commandKey, payload: command.payload })
60
- const result = getResponsePayload(response)
61
- if (result && result.success === false) throw new Error(resolveMessage(result.message) || '智能体消息发送失败')
62
- dispatched += 1
63
- }
64
- return dispatched
65
- }
26
+ function emptyData() { return { dashboard: {}, controlledGoods: { items: [] }, controlledBatches: { items: [] }, sanctionedCompanies: { items: [] }, sanctionBatches: { items: [] }, suppliers: { items: [] }, purchaseOrders: { items: [] }, purchaseLines: { items: [] }, customers: { items: [] }, customerContracts: { items: [] }, derivedSalesOrders: [], generatedFiles: { items: [] }, settings: { banks: [], rates: [] }, analytics: {}, importTasks: { items: [] } } }
27
+ function normalize(value) { const root = payload(value) || {}; return Object.assign(emptyData(), root.summary || root) }
66
28
 
67
29
  function App() {
68
30
  const [context, setContext] = React.useState(null)
69
- const [data, setData] = React.useState(normalizeData(null))
70
- const [activePage, setActivePage] = React.useState('overview-page')
71
- const [selectedIds, setSelectedIds] = React.useState([])
72
- const [statusFilter, setStatusFilter] = React.useState('all')
73
- const [overviewTab, setOverviewTab] = React.useState('all')
31
+ const [data, setData] = React.useState(emptyData())
32
+ const [page, setPage] = React.useState('home')
33
+ const [search, setSearch] = React.useState('')
74
34
  const [busy, setBusy] = React.useState(false)
75
- const [query, setQuery] = React.useState('')
76
- const [pageByList, setPageByList] = React.useState({})
77
- const [pageSizeByList, setPageSizeByList] = React.useState({})
78
- const [pageJumpByList, setPageJumpByList] = React.useState({})
79
- const [detailItem, setDetailItem] = React.useState(null)
80
- const [formDialog, setFormDialog] = React.useState(null)
81
- const [deleteDialog, setDeleteDialog] = React.useState(null)
82
- const [hsCodeDetailDialog, setHsCodeDetailDialog] = React.useState(null)
83
- const [hsCodeSearch, setHsCodeSearch] = React.useState({
84
- keywords: '',
85
- page: 1,
86
- loading: false,
87
- searched: false,
88
- error: '',
89
- jumpPage: '',
90
- result: null
91
- })
92
- const pollingRef = React.useRef(null)
93
- const fileInputsRef = React.useRef({})
35
+ const [notice, setNotice] = React.useState(null)
36
+ const [dialog, setDialog] = React.useState(null)
37
+ const [purchasePath, setPurchasePath] = React.useState({ supplierId: null, orderId: null })
38
+ const [salesPath, setSalesPath] = React.useState({ customerId: null, orderKey: null })
39
+ const searchTimer = React.useRef(null)
94
40
 
95
41
  React.useEffect(() => {
96
- window.__tradeComplianceSetContext = (nextContext) => {
97
- setContext(nextContext)
98
- setData(normalizeData(nextContext && nextContext.payload))
99
- setTimeout(() => reload(nextContext), 0)
100
- }
101
- window.__tradeComplianceHostEvent = () => reload()
42
+ window.__tcwInit = next => { setContext(next); setData(normalize(next.payload)); setTimeout(() => reload(next), 0) }
43
+ window.__tcwHostEvent = () => reload()
102
44
  post('ready')
103
- return () => {
104
- window.__tradeComplianceSetContext = null
105
- window.__tradeComplianceHostEvent = null
106
- stopRecognitionPolling()
107
- }
45
+ return () => { window.__tcwInit = null; window.__tcwHostEvent = null }
108
46
  }, [])
47
+ React.useEffect(reportResize, [data, page, dialog, notice, purchasePath, salesPath])
48
+ React.useEffect(() => { if (!context) return; clearTimeout(searchTimer.current); searchTimer.current = setTimeout(() => reload(null, search), 350); return () => clearTimeout(searchTimer.current) }, [search])
109
49
 
110
- React.useEffect(reportResize, [data, activePage, selectedIds, statusFilter, overviewTab, pageByList, pageSizeByList, busy, formDialog, deleteDialog, hsCodeSearch, hsCodeDetailDialog])
111
-
112
- const reviewItems = data.reviewItems.filter((item) => !isPlaceholderReviewItem(item))
113
- const pendingItems = reviewItems.filter((item) => (item.reviewStatus || 'pending') === 'pending')
114
- const filteredPendingItems = filterItems(pendingItems, query, overviewSearchKeys)
115
- const controlledReviewRows = reviewItems.filter((item) => item.type === 'controlled_goods' && item.reviewStatus !== 'confirmed')
116
- const controlledRows = controlledReviewRows.concat(data.controlledGoods.map(toControlledGoodsReviewRow))
117
- const controlledReviews = applyStatusFilter(filterItems(controlledRows, query, reviewSearchKeys), statusFilter)
118
- const pendingControlledReviews = controlledReviews.filter((item) => (item.reviewStatus || 'pending') === 'pending')
119
- const supplierReviewRows = reviewItems.filter((item) => item.type === 'supplier_product' && item.reviewStatus !== 'confirmed')
120
- const supplierRows = supplierReviewRows.concat(data.products.map(toSupplierProductReviewRow))
121
- const supplierReviews = applyStatusFilter(filterItems(supplierRows, query, reviewSearchKeys), statusFilter)
122
- const pendingSupplierReviews = supplierReviews.filter((item) => (item.reviewStatus || 'pending') === 'pending')
123
- const salesReviews = applyStatusFilter(filterItems(reviewItems.filter((item) => item.type === 'customs_workbook' && !isSalesContractLineReviewItem(item)), query, reviewSearchKeys), statusFilter)
124
- const pendingSalesReviews = salesReviews.filter((item) => (item.reviewStatus || 'pending') === 'pending')
125
- const filteredControlledGoods = filterItems(data.controlledGoods, query, ['productName', 'hsCode', 'controlNote'])
126
- const filteredProducts = filterItems(data.products, query, ['supplierName', 'productName', 'model', 'enrichedHsCode', 'englishName'])
127
- const filteredWorkbooks = filterItems(data.workbookGenerations, query, ['fileName', 'invoiceNo', 'contractNo', 'sourceFileName'])
128
-
129
- async function reload(nextContext) {
130
- const activeContext = nextContext || context
131
- if (!activeContext) return
132
- setBusy(true)
133
- try {
134
- const response = await request('requestData', { query: { page: 1, pageSize: 200, search: query, parameters: {} } })
135
- setData(normalizeData(getResponsePayload(response)))
136
- } catch (error) {
137
- showNotice('刷新失败:' + getErrorMessage(error), 'error')
138
- } finally {
139
- setBusy(false)
140
- }
141
- }
142
-
143
- async function searchHsCodePage(page) {
144
- const keywords = String(hsCodeSearch.keywords || '').trim()
145
- if (!keywords) {
146
- setHsCodeSearch(Object.assign({}, hsCodeSearch, { error: '请输入商品名称或海关编码', searched: false }))
147
- return
148
- }
149
- const nextPage = Math.max(1, Number(page) || 1)
150
- setHsCodeSearch(Object.assign({}, hsCodeSearch, { loading: true, error: '', page: nextPage }))
151
- try {
152
- const response = await executeAction('search_hs_code', null, {
153
- keywords,
154
- page: nextPage,
155
- filterFailureCode: true,
156
- displayChapter: false,
157
- displayEnName: true
158
- }, {})
159
- assertActionSuccess(response)
160
- const result = getActionDataPayload(response)
161
- setHsCodeSearch((current) => Object.assign({}, current, {
162
- loading: false,
163
- searched: true,
164
- page: nextPage,
165
- error: '',
166
- result
167
- }))
168
- } catch (error) {
169
- setHsCodeSearch((current) => Object.assign({}, current, {
170
- loading: false,
171
- searched: true,
172
- error: getErrorMessage(error)
173
- }))
174
- }
175
- }
176
-
177
- function updateHsCodeSearch(patch) {
178
- setHsCodeSearch(Object.assign({}, hsCodeSearch, patch))
179
- }
180
-
181
- function submitHsCodeSearch(event) {
182
- event.preventDefault()
183
- searchHsCodePage(1)
184
- }
185
-
186
- async function loadHsCodeDetail(item) {
187
- if (!item || (!item.code && !item.detailUrl)) return
188
- setHsCodeDetailDialog({ loading: true, item, detail: null, error: '' })
189
- try {
190
- const response = await executeAction('get_hs_code_detail', null, {
191
- code: item.code,
192
- detailUrl: item.detailUrl
193
- }, {})
194
- assertActionSuccess(response)
195
- setHsCodeDetailDialog({ loading: false, item, detail: getActionDataPayload(response), error: '' })
196
- } catch (error) {
197
- setHsCodeDetailDialog({ loading: false, item, detail: null, error: getErrorMessage(error) })
198
- }
199
- }
200
-
201
- async function searchSupplierHsCandidatesForForm(page) {
202
- if (!formDialog || formDialog.type !== 'supplier_product' || busy) return
203
- const keyword = String(formDialog.hsCandidateKeyword || buildSupplierHsCandidateKeyword(formDialog.values) || '').trim()
204
- if (!keyword) {
205
- setFormDialog(Object.assign({}, formDialog, { hsCandidateError: '请输入商品名称、型号或海关编码后再查询。' }))
206
- return
207
- }
208
- const nextPage = Math.max(1, Number(page) || 1)
209
- setFormDialog(Object.assign({}, formDialog, {
210
- hsCandidateKeyword: keyword,
211
- hsCandidatePage: nextPage,
212
- hsCandidateLoading: true,
213
- hsCandidateError: '',
214
- hsCandidateStatus: ''
215
- }))
216
- try {
217
- const response = await executeAction('search_hs_code', null, {
218
- keywords: keyword,
219
- page: nextPage,
220
- filterFailureCode: true,
221
- displayChapter: false,
222
- displayEnName: true
223
- }, {})
224
- assertActionSuccess(response)
225
- const result = getActionDataPayload(response)
226
- const candidates = Array.isArray(result && result.results) ? result.results : []
227
- setFormDialog((current) => current ? Object.assign({}, current, {
228
- hsCandidateKeyword: keyword,
229
- hsCandidatePage: nextPage,
230
- hsCandidateLocalPage: 1,
231
- hsCandidateLoading: false,
232
- hsCandidateError: '',
233
- hsCandidateStatus: candidates.length ? 'pending_confirmation' : 'not_found',
234
- hsCandidates: candidates,
235
- hsCandidatePagination: result && result.pagination ? result.pagination : null
236
- }) : current)
237
- } catch (error) {
238
- setFormDialog((current) => current ? Object.assign({}, current, {
239
- hsCandidateLoading: false,
240
- hsCandidateStatus: 'failed',
241
- hsCandidateError: getErrorMessage(error)
242
- }) : current)
243
- }
244
- }
245
-
246
- function selectHsCandidate(candidate) {
247
- if (!formDialog || formDialog.type !== 'supplier_product' || !candidate) return
248
- setFormDialog(Object.assign({}, formDialog, {
249
- values: Object.assign({}, formDialog.values, {
250
- enrichedHsCode: candidate.code || '',
251
- taxRefundRate: candidate.taxRefundRate || '',
252
- englishName: candidate.englishName || candidate.name || ''
253
- }),
254
- hsCandidateStatus: 'confirmed'
255
- }))
256
- showNotice('已选用海关编码,请检查后保存。', 'success')
50
+ async function reload(nextContext, nextSearch) {
51
+ const active = nextContext || context
52
+ if (!active) return
53
+ try { const response = await request('requestData', { query: { page: 1, pageSize: 200, search: nextSearch === undefined ? search : nextSearch, parameters: {} } }); setData(normalize(response)) }
54
+ catch (error) { show('刷新失败:' + messageOf(error), 'error') }
257
55
  }
258
-
259
- async function handleFile(actionKey, file) {
260
- debugUpload('file change received', { actionKey, hasFile: !!file, busy })
261
- if (!file) {
262
- debugUpload('file selection was empty', { actionKey })
263
- return
264
- }
265
- if (busy) {
266
- debugUpload('file upload ignored because workbench is busy', { actionKey, fileName: file.name })
267
- return
268
- }
269
- const expectedType = reviewTypeForUploadAction(actionKey)
270
- const previousCount = countReviewItemsByType(data.reviewItems, expectedType)
56
+ function show(message, kind) { setNotice({ message, kind: kind || 'success' }); setTimeout(() => setNotice(null), 4200) }
57
+ async function run(actionKey, input, options) {
271
58
  setBusy(true)
272
59
  try {
273
- debugUpload('executing plugin file action with workspace upload path', { actionKey, fileName: file.name, size: file.size, type: file.type })
274
- const response = await executeFileAction(actionKey, file, Object.assign(
275
- { name: file.name, fileName: file.name },
276
- {
277
- originalFileName: file.name,
278
- mimeType: file.type,
279
- size: file.size,
280
- workspaceUploadPath: workspaceUploadPathForAction(actionKey)
281
- }
282
- ), {})
283
- assertActionSuccess(response)
284
- const actionData = getActionDataPayload(response)
285
- debugUpload('dispatching assistant commands', { actionKey, fileName: file.name })
286
- const dispatched = await dispatchAssistantCommands(actionData)
287
- debugUpload('file workflow completed', { actionKey, fileName: file.name, dispatched })
288
- showNotice(resolveFileWorkflowNotice(actionKey, dispatched), 'success')
289
- await reload()
290
- if (dispatched && expectedType) startRecognitionPolling(expectedType, previousCount, readRecognitionPollingOptions(actionData))
291
- } catch (error) {
292
- debugUpload('file workflow failed', { actionKey, fileName: file.name, message: getErrorMessage(error) })
293
- showNotice(getErrorMessage(error), 'error')
294
- } finally {
295
- setBusy(false)
296
- }
297
- }
298
-
299
- async function saveProductForm(event) {
300
- event.preventDefault()
301
- if (!formDialog || busy) return
302
- const payload = normalizeFormPayload(formDialog.type, formDialog.values)
303
- if (formDialog.type === 'controlled_goods' && !payload.productName) {
304
- showNotice('请填写商品名称。', 'error')
305
- return
306
- }
307
- if (formDialog.type === 'supplier_product' && (!payload.supplierName || !payload.productName)) {
308
- showNotice('请填写供应商和商品名称。', 'error')
309
- return
310
- }
311
- const isCreate = formDialog.mode === 'create'
312
- const actionKey = isCreate
313
- ? (formDialog.type === 'controlled_goods' ? 'save_controlled_goods' : 'save_supplier_product')
314
- : formDialog.item.materializedOnly
315
- ? (formDialog.type === 'controlled_goods' ? 'update_controlled_goods' : 'update_supplier_product')
316
- : 'update_review_item'
317
- const input = isCreate
318
- ? payload
319
- : formDialog.item.materializedOnly
320
- ? Object.assign({ id: formDialog.item.materializedId || parseMaterializedId(formDialog.item.id) }, payload)
321
- : { itemId: formDialog.item.id, confirmedData: payload }
60
+ const response = await action(actionKey, input)
61
+ const result = payload(response)
62
+ if (result && result.success === false) throw new Error(messageOf(result))
63
+ if (!(options && options.silent)) show((options && options.message) || messageOf(result) || '操作成功')
64
+ if (!(options && options.noReload)) await reload()
65
+ return dataOf(response)
66
+ } catch (error) { show(messageOf(error) || '操作失败', 'error'); throw error }
67
+ finally { setBusy(false) }
68
+ }
69
+ async function upload(actionKey, file, input) {
322
70
  setBusy(true)
323
71
  try {
324
- const response = await executeAction(actionKey, null, input, {})
325
- assertActionSuccess(response)
326
- setFormDialog(null)
327
- showNotice(isCreate ? '商品已新增。' : '记录已更新。', 'success')
328
- await reload()
329
- } catch (error) {
330
- showNotice(getErrorMessage(error), 'error')
331
- } finally {
332
- setBusy(false)
333
- }
334
- }
335
-
336
- async function confirmReview(item) {
337
- if (!item || !item.id || busy) return
338
- setBusy(true)
339
- try {
340
- const confirmedData = buildConfirmReviewData(item)
341
- const response = await executeAction('confirm_review_item', item.id, { itemId: item.id, confirmedData }, {})
342
- assertActionSuccess(response)
343
- showNotice('审核项已确认并写入业务列表。', 'success')
344
- await reload()
345
- } catch (error) {
346
- showNotice(getErrorMessage(error), 'error')
347
- } finally {
348
- setBusy(false)
349
- }
350
- }
351
-
352
- async function rejectReview(item) {
353
- if (!item || !item.id || busy) return
354
- setBusy(true)
355
- try {
356
- const response = item.materializedOnly
357
- ? await executeAction(materializedDeleteAction(item), null, { id: item.materializedId || parseMaterializedId(item.id) }, {})
358
- : await executeAction('reject_review_item', item.id, { itemId: item.id }, {})
359
- assertActionSuccess(response)
360
- setSelectedIds(selectedIds.filter((id) => id !== item.id))
361
- showNotice(item.materializedOnly ? '记录已驳回并移出正式列表。' : '审核项已驳回。', 'success')
362
- await reload()
363
- } catch (error) {
364
- showNotice(getErrorMessage(error), 'error')
365
- } finally {
366
- setBusy(false)
367
- }
368
- }
369
-
370
- async function batchApproveSelected() {
371
- if (!selectedIds.length || busy) return
372
- const rows = getSelectedRows().filter((row) => !row.materializedOnly && row.reviewStatus !== 'confirmed' && row.reviewStatus !== 'rejected')
373
- if (!rows.length) return
374
- setBusy(true)
375
- try {
376
- const response = await executeAction('confirm_review_items', null, { itemIds: rows.map((row) => row.id) }, {})
377
- assertActionSuccess(response)
378
- setSelectedIds([])
379
- showNotice(`已批量审核 ${rows.length} 条记录。`, 'success')
380
- await reload()
381
- } catch (error) {
382
- showNotice(getErrorMessage(error), 'error')
383
- } finally {
384
- setBusy(false)
385
- }
386
- }
387
-
388
- async function batchRejectSelected() {
389
- const rows = getSelectedRows().filter((row) => row.reviewStatus !== 'rejected')
390
- if (!rows.length || busy) return
391
- setBusy(true)
392
- try {
393
- for (const row of rows) {
394
- if (row.materializedOnly) {
395
- await executeAction(materializedDeleteAction(row), null, { id: row.materializedId || parseMaterializedId(row.id) }, {})
396
- } else {
397
- await executeAction('reject_review_item', row.id, { itemId: row.id }, {})
398
- }
399
- }
400
- setSelectedIds([])
401
- showNotice(`已批量驳回 ${rows.length} 条记录。`, 'success')
402
- await reload()
403
- } catch (error) {
404
- showNotice(getErrorMessage(error), 'error')
405
- } finally {
406
- setBusy(false)
407
- }
408
- }
409
-
410
- async function batchDeleteSelected() {
411
- const rows = getSelectedRows()
412
- if (!rows.length || busy) return
413
- setDeleteDialog({
414
- type: 'batch-rows',
415
- title: '确认批量删除',
416
- message: `确定要删除选中的 ${rows.length} 条记录吗?此操作不可撤销。`,
417
- rows
418
- })
419
- }
420
-
421
- async function performBatchDelete(rows) {
422
- setBusy(true)
423
- try {
424
- for (const row of rows) {
425
- await deleteRowRequest(row)
426
- }
427
- setSelectedIds([])
428
- showNotice(`已批量删除 ${rows.length} 条记录。`, 'success')
429
- await reload()
430
- } catch (error) {
431
- showNotice(getErrorMessage(error), 'error')
432
- } finally {
433
- setBusy(false)
434
- }
435
- }
436
-
437
- async function deleteRow(item) {
438
- if (!item || !item.id || busy) return
439
- setDeleteDialog({
440
- type: 'row',
441
- title: '确认删除',
442
- message: `确定要删除「${readMerged(item).productName || item.title || '当前记录'}」吗?此操作不可撤销。`,
443
- item
444
- })
445
- }
446
-
447
- async function performDeleteRow(item) {
448
- setBusy(true)
449
- try {
450
- await deleteRowRequest(item)
451
- setSelectedIds(selectedIds.filter((id) => id !== item.id))
452
- showNotice('记录已删除。', 'success')
453
- await reload()
454
- } catch (error) {
455
- showNotice(getErrorMessage(error), 'error')
456
- } finally {
457
- setBusy(false)
458
- }
459
- }
460
-
461
- async function deleteRowRequest(item) {
462
- const actionKey = item.materializedOnly ? materializedDeleteAction(item) : 'delete_review_item'
463
- const input = item.materializedOnly ? { id: item.materializedId || parseMaterializedId(item.id) } : { itemId: item.id }
464
- const response = await executeAction(actionKey, null, input, {})
465
- assertActionSuccess(response)
466
- return response
467
- }
468
-
469
- function openEdit(item) {
470
- const merged = readMerged(item || {})
471
- setFormDialog({
472
- mode: 'edit',
473
- type: item.type,
474
- item,
475
- values: valuesFromItem(item.type, item),
476
- hsCandidates: Array.isArray(merged.hsCodeCandidates) ? merged.hsCodeCandidates : [],
477
- hsCandidateKeyword: merged.hsCodeLookupKeyword || buildSupplierHsCandidateKeyword(merged),
478
- hsCandidatePage: 1,
479
- hsCandidateJumpPage: '',
480
- hsCandidateLocalPage: 1,
481
- hsCandidateLocalPageSize: 5,
482
- hsCandidateLocalJumpPage: '',
483
- hsCandidatePagination: null,
484
- hsCandidateStatus: merged.hsCodeLookupStatus || '',
485
- hsCandidateError: merged.hsCodeLookupError || '',
486
- hsCandidateLoading: false
487
- })
488
- }
489
-
490
- function openCreate(type) {
491
- setFormDialog({
492
- mode: 'create',
493
- type,
494
- item: null,
495
- values: emptyFormValues(type),
496
- hsCandidates: [],
497
- hsCandidateKeyword: '',
498
- hsCandidatePage: 1,
499
- hsCandidateJumpPage: '',
500
- hsCandidateLocalPage: 1,
501
- hsCandidateLocalPageSize: 5,
502
- hsCandidateLocalJumpPage: '',
503
- hsCandidatePagination: null,
504
- hsCandidateStatus: '',
505
- hsCandidateError: '',
506
- hsCandidateLoading: false
507
- })
508
- }
509
-
510
- async function deleteWorkbook(item) {
511
- if (!item || !item.id || busy) return
512
- setDeleteDialog({
513
- type: 'workbook',
514
- title: '确认删除销售发票',
515
- message: `确定要删除「${item.fileName || item.invoiceNo || '当前销售发票'}」吗?此操作不可撤销。`,
516
- item
517
- })
518
- }
519
-
520
- async function performDeleteWorkbook(item) {
521
- setBusy(true)
522
- try {
523
- const response = await executeAction('delete_customs_workbook', null, { id: item.id }, {})
524
- assertActionSuccess(response)
525
- showNotice('销售发票记录已删除。', 'success')
526
- await reload()
527
- } catch (error) {
528
- showNotice(getErrorMessage(error), 'error')
529
- } finally {
530
- setBusy(false)
531
- }
532
- }
533
-
534
- async function confirmDeleteDialog() {
535
- if (!deleteDialog || busy) return
536
- const current = deleteDialog
537
- setDeleteDialog(null)
538
- if (current.type === 'batch-rows') {
539
- await performBatchDelete(current.rows)
540
- } else if (current.type === 'workbook') {
541
- await performDeleteWorkbook(current.item)
542
- } else {
543
- await performDeleteRow(current.item)
544
- }
545
- }
546
-
547
- async function downloadWorkbook(item) {
548
- if (!item || !item.id || busy) return
549
- setBusy(true)
550
- try {
551
- const response = await executeAction('download_customs_workbook', null, { id: item.id }, {})
552
- assertActionSuccess(response)
553
- const payload = getResponsePayload(response)
554
- const data = payload && payload.data ? payload.data : payload
555
- const base64 = data && data.base64
556
- if (!base64) throw new Error('未返回 Excel 文件内容')
557
- const bytes = base64ToBytes(base64)
558
- const blob = new Blob([bytes], { type: data.mimeType || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
559
- const url = URL.createObjectURL(blob)
560
- const link = document.createElement('a')
561
- link.href = url
562
- link.download = data.fileName || item.fileName || '销售发票.xlsx'
563
- link.click()
564
- URL.revokeObjectURL(url)
565
- } catch (error) {
566
- showNotice(getErrorMessage(error), 'error')
567
- } finally {
568
- setBusy(false)
569
- }
570
- }
571
-
572
- async function generateWorkbook() {
573
- if (busy) return
574
- const selectedSalesRows = getSelectedSalesRows()
575
- if (selectedSalesRows.length === 0) {
576
- showNotice('请先勾选要生成销售发票的购销合同。', 'error')
577
- return
578
- }
579
- setBusy(true)
580
- try {
581
- for (let index = 0; index < selectedSalesRows.length; index += 1) {
582
- const salesItem = selectedSalesRows[index]
583
- const base = readMerged(salesItem || {})
584
- const invoiceNo = base.invoiceNo || fallbackInvoiceNo(base, index, selectedSalesRows.length)
585
- const response = await executeAction('generate_customs_workbook', null, {
586
- invoiceNo,
587
- contractNo: base.contractNo,
588
- sourceFileName: base.sourceFileName || salesItem.title,
589
- fileName: invoiceNo + '-销售发票.xlsx',
590
- workbookData: base
591
- }, {})
592
- assertActionSuccess(response)
593
- }
594
- showNotice(selectedSalesRows.length === 1 ? '销售发票已生成并写入历史。' : `已生成 ${selectedSalesRows.length} 张销售发票。`, 'success')
595
- await reload()
596
- } catch (error) {
597
- showNotice(getErrorMessage(error), 'error')
598
- } finally {
599
- setBusy(false)
600
- }
601
- }
602
-
603
- function showNotice(message, level) {
604
- notify(level || 'info', message)
605
- }
606
-
607
- function startRecognitionPolling(expectedType, previousCount, options) {
608
- stopRecognitionPolling()
609
- let attempts = 0
610
- let lastCount = previousCount
611
- let stableTicks = 0
612
- const pollingOptions = options || {}
613
- const expectedCount = pollingOptions.expectedCount || 0
614
- const isChunked = pollingOptions.expectedChunkCount > 1
615
- const targetCount = expectedCount > 0 ? previousCount + expectedCount : null
616
- pollingRef.current = setInterval(async () => {
617
- attempts += 1
618
- try {
619
- const response = await request('requestData', { query: { page: 1, pageSize: 1000, search: query, parameters: {} } })
620
- const nextData = normalizeData(getResponsePayload(response))
621
- setData(nextData)
622
- const nextCount = countReviewItemsByType(nextData.reviewItems, expectedType)
623
- stableTicks = nextCount === lastCount ? stableTicks + 1 : 0
624
- lastCount = nextCount
625
- if ((targetCount && nextCount >= targetCount) || (!targetCount && !isChunked && nextCount > previousCount && stableTicks >= 4) || attempts >= 160) {
626
- stopRecognitionPolling()
627
- }
628
- } catch (_error) {
629
- if (attempts >= 160) stopRecognitionPolling()
630
- }
631
- }, 2500)
632
- }
633
-
634
- function stopRecognitionPolling() {
635
- if (!pollingRef.current) return
636
- clearInterval(pollingRef.current)
637
- pollingRef.current = null
638
- }
639
-
72
+ const response = await fileAction(actionKey, file, input)
73
+ const result = payload(response)
74
+ if (result && result.success === false) throw new Error(messageOf(result))
75
+ const commands = dataOf(response) && dataOf(response).commands || []
76
+ for (const command of commands) await request('invokeClientCommand', { commandKey: command.commandKey, payload: command.payload })
77
+ show('文件已进入解析中')
78
+ setDialog(null); await reload()
79
+ beginPolling()
80
+ } catch (error) { show(messageOf(error) || '上传失败', 'error') }
81
+ finally { setBusy(false) }
82
+ }
83
+ function beginPolling() { let count = 0; const timer = setInterval(async () => { count += 1; await reload(); if (count >= 600) clearInterval(timer) }, 2000) }
84
+
85
+ const ctx = { data, search, setSearch, busy, setBusy, show, run, upload, dialog, setDialog, purchasePath, setPurchasePath, salesPath, setSalesPath }
640
86
  return h('div', { className: 'tcw-shell' },
641
- renderSidebar(),
642
- h('section', { className: 'tcw-workspace' },
643
- h('header', { className: 'tcw-header' },
644
- h('div', { className: 'tcw-title-block' },
645
- h('span', { className: 'tcw-kicker' }, pageBreadcrumb(activePage)),
646
- h('h1', null, pageTitle(activePage)),
647
- h('p', null, pageSubtitle(activePage))
648
- ),
649
- h('div', { className: 'tcw-header-actions' },
650
- h('input', { className: 'tcw-search', value: query, placeholder: '搜索商品、供应商、海关编码、文件名', onChange: (event) => setQuery(event.target.value) }),
651
- h('button', { className: 'tcw-btn tcw-btn-soft', disabled: busy, onClick: () => reload() }, busy ? '处理中' : '刷新')
652
- )
653
- ),
654
- h('main', { className: 'tcw-main' },
655
- h('section', { className: 'tcw-content' },
656
- activePage === 'overview-page' ? renderOverviewPage() : null,
657
- activePage === 'controlled-goods-page' ? renderControlledGoodsPage() : null,
658
- activePage === 'products-page' ? renderProductsPage() : null,
659
- activePage === 'workbooks-page' ? renderWorkbooksPage() : null,
660
- activePage === 'hs-code-search-page' ? renderHsCodeSearchPage() : null
661
- )
662
- )
663
- ),
664
- detailItem ? renderDetailModal() : null,
665
- formDialog ? renderFormModal() : null,
666
- deleteDialog ? renderDeleteConfirmModal() : null,
667
- hsCodeDetailDialog ? renderHsCodeDetailModal() : null
668
- )
669
-
670
- function renderSidebar() {
671
- return h('aside', { className: 'tcw-sidebar' },
672
- h('div', { className: 'tcw-brand' },
673
- h('div', { className: 'tcw-brand-mark' }, 'TC'),
674
- h('div', null,
675
- h('strong', null, '外贸合规工作台'),
676
- h('span', null, 'Trade Compliance')
677
- )
678
- ),
679
- h('nav', { className: 'tcw-nav', 'aria-label': '主导航' }, [
680
- tab('overview-page', '总览', 'dashboard', pendingItems.length, 'blue'),
681
- tab('controlled-goods-page', '管控商品', 'shield', controlledReviews.length, 'green'),
682
- tab('products-page', '供应商商品', 'box', supplierReviews.length, 'orange'),
683
- tab('workbooks-page', '销售发票', 'invoice', salesReviews.length + filteredWorkbooks.length, 'violet'),
684
- tab('hs-code-search-page', '工具', 'search', null, 'slate')
685
- ])
686
- )
687
- }
688
-
689
- function tab(key, label, icon, count, tone) {
690
- return h('button', { key, className: key === activePage ? 'tcw-tab active' : 'tcw-tab', onClick: () => setActivePage(key) },
691
- h('span', { className: 'tcw-nav-icon ' + (tone || 'blue') }, iconImage(icon, label, tone)),
692
- h('span', null, label),
693
- count == null ? null : h('em', null, count)
694
- )
695
- }
696
-
697
- function renderOverviewPage() {
698
- const filteredPendingControlledItems = filteredPendingItems.filter((item) => item.type === 'controlled_goods')
699
- const filteredPendingSupplierItems = filteredPendingItems.filter((item) => item.type === 'supplier_product')
700
- const filteredPendingSalesItems = filteredPendingItems.filter((item) => item.type === 'customs_workbook')
701
- const overviewRows = overviewTab === 'all'
702
- ? filteredPendingItems
703
- : filteredPendingItems.filter((item) => item.type === overviewTab)
704
- const overviewListKey = `overview-${overviewTab}`
705
- const overviewPage = paginateRows(overviewListKey, overviewRows)
706
- return h('div', { className: 'tcw-page overview-page' },
707
- h('section', { className: 'tcw-metrics' },
708
- metric('待审核', filteredPendingItems.length, 'dashboard', 'blue'),
709
- metric('管控识别待审', filteredPendingControlledItems.length, 'shield', 'green'),
710
- metric('供应商商品待审', filteredPendingSupplierItems.length, 'box', 'orange'),
711
- metric('销售合同待审', filteredPendingSalesItems.length, 'invoice', 'violet'),
712
- metric('已入库管控商品', data.controlledGoods.length, 'archive', 'slate'),
713
- metric('已入库供应商商品', data.products.length, 'supplier', 'green'),
714
- metric('销售发票历史', data.workbookGenerations.length, 'document', 'violet'),
715
- metric('全部审核记录', reviewItems.length, 'records', 'blue')
716
- ),
717
- panel('待办事项明细',
718
- h('div', { className: 'tcw-overview-panel' },
719
- h('div', { className: 'tcw-overview-tabs' }, [
720
- overviewTabButton('all', '全部', filteredPendingItems.length),
721
- overviewTabButton('controlled_goods', '管控商品', filteredPendingControlledItems.length),
722
- overviewTabButton('supplier_product', '供应商商品', filteredPendingSupplierItems.length),
723
- overviewTabButton('customs_workbook', '销售发票', filteredPendingSalesItems.length)
724
- ]),
725
- overviewPage.rows.length
726
- ? compactReviewList(overviewPage.rows)
727
- : empty('暂无待审核记录。智能体识别结果会先出现在这里。'),
728
- pagination(overviewListKey, overviewRows.length)
729
- )
730
- )
731
- )
732
-
733
- function overviewTabButton(key, label, count) {
734
- return h('button', {
735
- key,
736
- type: 'button',
737
- className: overviewTab === key ? 'tcw-overview-tab active' : 'tcw-overview-tab',
738
- onClick: () => setOverviewTab(key)
739
- }, `${label}${typeof count === 'number' ? ` ${count}` : ''}`)
740
- }
741
- }
742
-
743
- function renderControlledGoodsPage() {
744
- const page = paginateRows('controlled-goods', controlledReviews)
745
- return h('div', { className: 'tcw-page controlled-goods-page' },
746
- businessPanel([
747
- uploadButton('upload_controlled_goods_file', '上传管控商品文件'),
748
- h('button', { className: 'tcw-btn tcw-btn-primary', disabled: busy, onClick: () => openCreate('controlled_goods') }, '新增管控商品')
749
- ], [
750
- renderListToolbar('管控商品列表', controlledReviews.length),
751
- h('div', { className: 'pending-controlled-goods confirmed-controlled-goods tcw-table-section' },
752
- reviewTable(['商品名称/候选', '海关编码', '解析状态', '管控说明', '来源'], page.rows, (item) => {
753
- const row = readMerged(item)
754
- return [
755
- value(row.productName || row.referenceNameCandidate || item.title),
756
- value(row.hsCode),
757
- value(resolveControlledGoodsParseStatus(row)),
758
- value(row.controlNote),
759
- value(item.sourceLocation || row.sourceFileName)
760
- ]
761
- }, '暂无管控商品识别结果。上传目录并等待智能体调用工具后会显示在这里。', renderProductActions)
762
- ),
763
- pagination('controlled-goods', controlledReviews.length)
764
- ])
765
- )
766
- }
767
-
768
- function renderProductsPage() {
769
- const page = paginateRows('supplier-products', supplierReviews)
770
- return h('div', { className: 'tcw-page products-page' },
771
- businessPanel([
772
- uploadButton('upload_supplier_contract', '上传供应商合同'),
773
- h('button', { className: 'tcw-btn tcw-btn-primary', disabled: busy, onClick: () => openCreate('supplier_product') }, '新增供应商商品')
774
- ], [
775
- renderListToolbar('供应商商品列表', supplierReviews.length),
776
- h('div', { className: 'pending-supplier-products tcw-table-section' },
777
- reviewTable(['供应商', '商品', '型号', '海关编码', '编码确认', '退税率', '英文品名', '管控状态'], page.rows, (item) => {
778
- const row = readSupplierEditableMerged(item)
779
- return [
780
- value(row.supplierName),
781
- value(row.productName || item.title),
782
- value(row.model),
783
- value(resolveDisplayHsCode(row)),
784
- status(hsCodeReviewStatusText(row), hsCodeReviewStatusLevel(row)),
785
- value(row.taxRefundRate),
786
- value(row.englishName),
787
- status(controlStatusText(row.controlledStatus), controlStatusLevel(row.controlledStatus))
788
- ]
789
- }, '暂无供应商商品识别结果。上传供应商合同并等待智能体识别后会显示在这里。', renderProductActions)
790
- ),
791
- pagination('supplier-products', supplierReviews.length)
792
- ])
793
- )
794
- }
795
-
796
- function renderWorkbooksPage() {
797
- const salesPage = paginateRows('sales-contracts', salesReviews)
798
- const workbookPage = paginateRows('sales-workbooks', filteredWorkbooks)
799
- const selectedSalesCount = getSelectedSalesRows().length
800
- return h('div', { className: 'tcw-page workbooks-page' },
801
- businessPanel([
802
- uploadButton('upload_sales_contract', '上传购销合同'),
803
- h('button', { className: 'tcw-btn tcw-btn-primary', disabled: busy || selectedSalesCount === 0, onClick: generateWorkbook }, selectedSalesCount > 1 ? `生成销售发票 (${selectedSalesCount})` : '生成销售发票')
804
- ], [
805
- renderListToolbar('购销合同识别列表', salesReviews.length),
806
- h('div', { className: 'pending-sales-contracts tcw-table-section' },
807
- reviewTable(['文件/标题', '发票号', '合同号', '买方', '卖方'], salesPage.rows, (item) => {
808
- const row = readMerged(item)
809
- return [
810
- value(item.title),
811
- value(row.invoiceNo),
812
- value(row.contractNo),
813
- value(row.buyerName),
814
- value(row.sellerName)
815
- ]
816
- }, '暂无购销合同识别结果。', renderSalesReviewActions)
817
- ),
818
- pagination('sales-contracts', salesReviews.length),
819
- h('section', { className: 'tcw-history-section tcw-workbook-history-section' },
820
- h('div', { className: 'tcw-subsection-head' },
821
- h('div', null, h('h3', null, '已生成销售发票'), h('p', null, `${filteredWorkbooks.length} 条生成历史`))
822
- ),
823
- table(['文件名', '发票号', '合同号', '模板工作表', '状态', '操作'], workbookPage.rows, (item) => [
824
- value(item.fileName),
825
- value(item.invoiceNo),
826
- value(item.contractNo),
827
- Array.isArray(item.sheetNames) ? item.sheetNames.join('、') : '-',
828
- status(value(item.status, 'generated'), 'ok'),
829
- h('div', { className: 'tcw-row-actions' },
830
- h('button', { className: 'tcw-mini-btn', onClick: () => downloadWorkbook(item) }, '下载'),
831
- h('button', { className: 'tcw-mini-btn danger', onClick: () => deleteWorkbook(item) }, '删除')
832
- )
833
- ], '暂无销售发票生成历史。'),
834
- pagination('sales-workbooks', filteredWorkbooks.length)
835
- )
836
- ])
837
- )
838
- }
839
-
840
- function renderHsCodeSearchPage() {
841
- const result = hsCodeSearch.result || {}
842
- const rows = Array.isArray(result.results) ? result.results : []
843
- return h('div', { className: 'tcw-page hs-code-search-page' },
844
- businessPanel([], [
845
- h('form', { className: 'tcw-hs-search-form', onSubmit: submitHsCodeSearch },
846
- h('div', { className: 'tcw-hs-search-row' },
847
- h('input', {
848
- className: 'tcw-search hs-code-search-input',
849
- value: hsCodeSearch.keywords,
850
- placeholder: '请输入商品名称或海关编码,例如:帽子、8471499100',
851
- onChange: (event) => updateHsCodeSearch({ keywords: event.target.value })
852
- }),
853
- h('button', { className: 'tcw-btn tcw-btn-primary', disabled: hsCodeSearch.loading }, hsCodeSearch.loading ? '查询中' : '查询')
854
- )
855
- ),
856
- hsCodeSearch.error ? h('div', { className: 'tcw-error' }, hsCodeSearch.error) : null,
857
- !hsCodeSearch.searched && !hsCodeSearch.loading ? empty('请输入商品名称或海关编码后查询。') : null,
858
- hsCodeSearch.loading ? empty('正在查询 HS 编码网,请稍候。') : null,
859
- hsCodeSearch.searched && !hsCodeSearch.loading && !hsCodeSearch.error ? h('section', { className: 'tcw-history-section' },
860
- h('div', { className: 'tcw-subsection-head' },
861
- h('div', null,
862
- h('h3', null, `和「${value(result.keywords || hsCodeSearch.keywords, '')}」有关的 HS 编码`),
863
- h('p', null, `第 ${result.page || hsCodeSearch.page} 页,${rows.length} 条结果`)
864
- )
865
- ),
866
- table(['商品编码', '商品名称', '英文名称', '计量单位', '出口退税率(%)', '监管条件', '检验检疫', '来源', '操作'], rows, (item) => [
867
- h('strong', { className: 'tcw-hs-code-text' }, value(item.code)),
868
- value(item.name),
869
- value(item.englishName),
870
- value(item.unit),
871
- value(item.taxRefundRate),
872
- value(item.regulatoryConditions),
873
- value(item.inspectionQuarantine),
874
- `第 ${item.sourcePage || result.page || hsCodeSearch.page} 页`,
875
- h('div', { className: 'tcw-row-actions' },
876
- h('button', { className: 'tcw-mini-btn', disabled: !item.code && !item.detailUrl, onClick: () => loadHsCodeDetail(item) }, '详情')
877
- )
878
- ], '未查询到相关编码。可换关键词或海关编码重新查询。'),
879
- renderHsCodePagination(result.pagination)
880
- ) : null
881
- ])
882
- )
883
- }
884
-
885
- function renderHsCodeDetailModal() {
886
- const state = hsCodeDetailDialog || {}
887
- const item = state.item || {}
888
- const detail = state.detail || {}
889
- const sections = Array.isArray(detail.sections) ? detail.sections : []
890
- return h('div', { className: 'tcw-modal-backdrop' },
891
- h('section', { className: 'tcw-modal tcw-hs-detail-modal' },
892
- h('div', { className: 'tcw-modal-head' },
893
- h('div', null,
894
- h('h2', null, '海关编码详情'),
895
- h('p', { className: 'tcw-modal-subtitle' }, `${value(detail.code || item.code, '-')} ${value(detail.name || item.name, '')}`)
896
- ),
897
- h('button', { type: 'button', className: 'tcw-icon-btn', onClick: () => setHsCodeDetailDialog(null) }, '×')
898
- ),
899
- state.loading ? empty('正在读取源站详情,请稍候。') : null,
900
- state.error ? h('div', { className: 'tcw-error' }, state.error) : null,
901
- !state.loading && !state.error && sections.length === 0 ? empty('源站详情页暂无可展示内容。') : null,
902
- !state.loading && !state.error && sections.length > 0 ? h('div', { className: 'tcw-hs-detail-sections' },
903
- sections.map((section) => h('section', { className: 'tcw-hs-detail-section', key: section.title },
904
- h('h3', null, section.title),
905
- h('div', { className: 'tcw-detail-grid' },
906
- (Array.isArray(section.rows) ? section.rows : []).map((row) =>
907
- h('div', { className: 'tcw-detail-field', key: `${section.title}:${row.label}` },
908
- h('span', null, row.label),
909
- h('strong', null, value(row.value, '-'))
910
- )
911
- )
912
- )
913
- ))
914
- ) : null,
915
- h('div', { className: 'tcw-modal-actions' },
916
- h('button', { type: 'button', className: 'tcw-btn tcw-btn-soft', onClick: () => setHsCodeDetailDialog(null) }, '关闭')
917
- )
918
- )
919
- )
920
- }
921
-
922
- function renderHsCodePagination(pagination) {
923
- if (!pagination || (!pagination.hasPrevious && !pagination.hasNext && (!pagination.pages || pagination.pages.length <= 1))) return null
924
- const pages = Array.isArray(pagination.pages) ? pagination.pages.filter((item) => item.page) : []
925
- const maxPage = Number(pagination.maxVisiblePage) || Math.max(...pages.map((item) => Number(item.page) || 0), pagination.currentPage || hsCodeSearch.page || 1)
926
- const currentPage = pagination.currentPage || hsCodeSearch.page || 1
927
- return renderUnifiedPagination({
928
- page: currentPage,
929
- totalPages: Math.max(1, maxPage),
930
- pageSize: 10,
931
- fixedPageSize: true,
932
- loading: hsCodeSearch.loading,
933
- hasPrevious: pagination.hasPrevious,
934
- hasNext: pagination.hasNext,
935
- jumpValue: hsCodeSearch.jumpPage || '',
936
- onPageChange: searchHsCodePage,
937
- onJumpInput: (nextValue) => setHsCodeSearch(Object.assign({}, hsCodeSearch, { jumpPage: nextValue })),
938
- onJump: () => {
939
- const nextPage = Number(hsCodeSearch.jumpPage)
940
- if (!Number.isFinite(nextPage) || nextPage <= 0) return
941
- searchHsCodePage(nextPage)
942
- setHsCodeSearch((current) => Object.assign({}, current, { jumpPage: '' }))
943
- }
944
- })
945
- }
946
-
947
- function compactReviewList(items) {
948
- return h('div', { className: 'tcw-compact-list' }, items.map((item) =>
949
- h('div', { className: 'tcw-compact-row', key: item.id || item.title },
950
- h('span', null, reviewTypeText(item.type)),
951
- h('strong', null, item.title),
952
- h('em', null, reviewStatusText(item.reviewStatus))
953
- )
954
- ))
955
- }
956
-
957
- function renderListPanel(title, className, rows, headers, mapRow, emptyText) {
958
- return panel(title, h('div', { className },
959
- renderListToolbar(null, rows.length),
960
- reviewTable(headers, rows, mapRow, emptyText)
961
- ))
962
- }
963
-
964
- function businessPanel(actions, content) {
965
- const hasActions = Array.isArray(actions) && actions.length > 0
966
- return h('section', { className: 'tcw-business-panel' },
967
- hasActions ? h('div', { className: 'tcw-business-actions' },
968
- h('div', { className: 'tcw-button-row' }, actions)
969
- ) : null,
970
- h('div', { className: 'tcw-business-body' }, content)
971
- )
972
- }
973
-
974
- function renderListToolbar(title, total) {
975
- return h('div', { className: 'tcw-list-toolbar' },
976
- h('div', { className: 'tcw-list-title' },
977
- title ? h('h3', null, title) : null,
978
- h('span', null, `${total || 0} 条记录`)
979
- ),
980
- h('div', { className: 'tcw-list-actions' },
981
- h('label', { className: 'tcw-filter' },
982
- h('span', null, '状态'),
983
- h('select', { className: 'status-filter', value: statusFilter, onChange: (event) => { setStatusFilter(event.target.value); setSelectedIds([]) } },
984
- h('option', { value: 'all' }, '全部'),
985
- h('option', { value: 'pending' }, '待审核'),
986
- h('option', { value: 'confirmed' }, '已审核'),
987
- h('option', { value: 'rejected' }, '已驳回')
988
- )
989
- ),
990
- h('button', { className: 'tcw-btn tcw-btn-primary', disabled: busy || selectedIds.length === 0, onClick: batchApproveSelected }, `批量审核${selectedIds.length ? ` (${selectedIds.length})` : ''}`),
991
- h('button', { className: 'tcw-btn tcw-btn-soft', disabled: busy || selectedIds.length === 0, onClick: batchRejectSelected }, '批量驳回'),
992
- h('button', { className: 'tcw-btn tcw-btn-danger', disabled: busy || selectedIds.length === 0, onClick: batchDeleteSelected }, '批量删除')
993
- )
994
- )
995
- }
996
-
997
- function renderProductActions(row) {
998
- return h('div', { className: 'tcw-row-actions' },
999
- h('button', { className: 'tcw-mini-btn', disabled: busy || row.materializedOnly || row.reviewStatus === 'confirmed', onClick: () => confirmReview(row) }, '审核'),
1000
- h('button', { className: 'tcw-mini-btn', disabled: busy, onClick: () => openEdit(row) }, '编辑'),
1001
- h('button', { className: 'tcw-mini-btn danger', disabled: busy || row.reviewStatus === 'rejected', onClick: () => rejectReview(row) }, '驳回'),
1002
- h('button', { className: 'tcw-mini-btn danger', disabled: busy, onClick: () => deleteRow(row) }, '删除')
1003
- )
1004
- }
1005
-
1006
- function renderSalesReviewActions(row) {
1007
- return h('div', { className: 'tcw-row-actions' },
1008
- h('button', { className: 'tcw-mini-btn', disabled: busy || row.reviewStatus === 'confirmed', onClick: () => confirmReview(row) }, '审核'),
1009
- h('button', { className: 'tcw-mini-btn', disabled: busy, onClick: () => setDetailItem(row) }, '详情'),
1010
- h('button', { className: 'tcw-mini-btn danger', disabled: busy || row.reviewStatus === 'rejected', onClick: () => rejectReview(row) }, '驳回'),
1011
- h('button', { className: 'tcw-mini-btn danger', disabled: busy, onClick: () => deleteRow(row) }, '删除')
1012
- )
1013
- }
1014
-
1015
- function reviewTable(headers, rows, mapRow, emptyText, renderActions) {
1016
- const selectableIds = getSelectableRowIds(rows)
1017
- const selectedVisibleIds = selectableIds.filter((id) => selectedIds.includes(id))
1018
- const allVisibleSelected = selectableIds.length > 0 && selectedVisibleIds.length === selectableIds.length
1019
- const partiallySelected = selectedVisibleIds.length > 0 && selectedVisibleIds.length < selectableIds.length
1020
- return table([
1021
- h('input', {
1022
- className: 'row-selection-checkbox',
1023
- type: 'checkbox',
1024
- 'aria-label': '全选当前列表',
1025
- checked: allVisibleSelected,
1026
- disabled: selectableIds.length === 0,
1027
- ref: (node) => {
1028
- if (node) node.indeterminate = partiallySelected
1029
- },
1030
- onChange: (event) => toggleAllVisibleRows(rows, event.target.checked)
1031
- }),
1032
- ...headers,
1033
- '状态',
1034
- '操作'
1035
- ], rows, (row) => [
1036
- h('input', {
1037
- className: 'row-selection-checkbox',
1038
- type: 'checkbox',
1039
- checked: selectedIds.includes(row.id),
1040
- disabled: !row.id,
1041
- onChange: (event) => toggleSelected(row.id, event.target.checked)
1042
- }),
1043
- ...mapRow(row),
1044
- status(reviewStatusText(row.reviewStatus), row.reviewStatus === 'confirmed' ? 'ok' : row.reviewStatus === 'rejected' ? 'danger' : 'warn'),
1045
- renderActions ? renderActions(row) : h('div', { className: 'tcw-row-actions' },
1046
- h('button', { className: 'tcw-mini-btn', disabled: busy || row.materializedOnly || row.reviewStatus === 'confirmed', onClick: () => confirmReview(row) }, '审核'),
1047
- h('button', { className: 'tcw-mini-btn danger', disabled: busy || row.materializedOnly || row.reviewStatus === 'rejected', onClick: () => rejectReview(row) }, '驳回')
1048
- )
1049
- ], emptyText)
1050
- }
1051
-
1052
- function toggleSelected(id, checked) {
1053
- if (!id) return
1054
- setSelectedIds(checked ? Array.from(new Set([...selectedIds, id])) : selectedIds.filter((item) => item !== id))
1055
- }
1056
-
1057
- function toggleAllVisibleRows(rows, checked) {
1058
- const visibleIds = getSelectableRowIds(rows)
1059
- if (checked) {
1060
- setSelectedIds(Array.from(new Set([...selectedIds, ...visibleIds])))
1061
- return
1062
- }
1063
- setSelectedIds(selectedIds.filter((id) => !visibleIds.includes(id)))
1064
- }
1065
-
1066
- function getSelectableRowIds(rows) {
1067
- return (rows || [])
1068
- .filter((row) => row.id)
1069
- .map((row) => row.id)
1070
- }
1071
-
1072
- function getSelectedRows() {
1073
- const rows = activePage === 'controlled-goods-page'
1074
- ? controlledReviews
1075
- : activePage === 'products-page'
1076
- ? supplierReviews
1077
- : activePage === 'workbooks-page'
1078
- ? salesReviews
1079
- : []
1080
- const selected = new Set(selectedIds)
1081
- return rows.filter((row) => selected.has(row.id))
1082
- }
1083
-
1084
- function getSelectedSalesRows() {
1085
- const selected = new Set(selectedIds)
1086
- return salesReviews.filter((row) => row.id && selected.has(row.id))
1087
- }
1088
-
1089
- function fallbackInvoiceNo(base, index, total) {
1090
- const date = new Date().toISOString().slice(0, 10).replace(/-/g, '')
1091
- const contractNo = String(base && base.contractNo || '').replace(/[^0-9A-Za-z_-]/g, '').slice(0, 18)
1092
- if (contractNo) return `INV-${contractNo}`
1093
- return total > 1 ? `INV-${date}-${index + 1}` : `INV-${date}`
1094
- }
1095
-
1096
- function materializedDeleteAction(item) {
1097
- return item.type === 'controlled_goods' ? 'delete_controlled_goods' : 'delete_supplier_product'
1098
- }
1099
-
1100
- function paginateRows(listKey, rows) {
1101
- const pageSize = getPageSize(listKey)
1102
- const total = rows.length
1103
- const totalPages = Math.max(1, Math.ceil(total / pageSize))
1104
- const current = Math.min(Math.max(pageByList[listKey] || 1, 1), totalPages)
1105
- return { rows: rows.slice((current - 1) * pageSize, current * pageSize), page: current, totalPages, pageSize }
1106
- }
1107
-
1108
- function pagination(listKey, total) {
1109
- if (total <= 0) return null
1110
- const page = paginateRows(listKey, Array.from({ length: total }))
1111
- return renderUnifiedPagination({
1112
- total,
1113
- page: page.page,
1114
- totalPages: page.totalPages,
1115
- pageSize: page.pageSize,
1116
- pageSizeOptions: [10, 20, 50],
1117
- jumpValue: pageJumpByList[listKey] || '',
1118
- onPageChange: (nextPage) => setLocalPage(listKey, nextPage, page.totalPages),
1119
- onPageSizeChange: (nextSize) => setLocalPageSize(listKey, nextSize),
1120
- onJumpInput: (nextValue) => setPageJumpByList(Object.assign({}, pageJumpByList, { [listKey]: nextValue })),
1121
- onJump: () => jumpLocalPage(listKey, page.totalPages)
1122
- })
1123
- }
1124
-
1125
- function getPageSize(listKey) {
1126
- const value = Number(pageSizeByList[listKey])
1127
- return Number.isFinite(value) && value > 0 ? value : 10
1128
- }
1129
-
1130
- function setLocalPage(listKey, nextPage, totalPages) {
1131
- const page = Math.min(Math.max(Number(nextPage) || 1, 1), totalPages || 1)
1132
- setPageByList(Object.assign({}, pageByList, { [listKey]: page }))
1133
- }
1134
-
1135
- function setLocalPageSize(listKey, nextSize) {
1136
- const pageSize = Number(nextSize) || 10
1137
- setPageSizeByList(Object.assign({}, pageSizeByList, { [listKey]: pageSize }))
1138
- setPageByList(Object.assign({}, pageByList, { [listKey]: 1 }))
1139
- setPageJumpByList(Object.assign({}, pageJumpByList, { [listKey]: '' }))
1140
- }
1141
-
1142
- function jumpLocalPage(listKey, totalPages) {
1143
- const value = Number(pageJumpByList[listKey])
1144
- if (!Number.isFinite(value) || value <= 0) return
1145
- setLocalPage(listKey, value, totalPages)
1146
- setPageJumpByList(Object.assign({}, pageJumpByList, { [listKey]: '' }))
1147
- }
1148
-
1149
- function renderDetailModal() {
1150
- const data = readMerged(detailItem)
1151
- const items = Array.isArray(data.items) ? data.items : []
1152
- const entries = Object.entries(data).filter(([key]) => key !== 'items')
1153
- return h('div', { className: 'tcw-modal-backdrop' },
1154
- h('section', { className: 'tcw-modal' },
1155
- h('div', { className: 'tcw-modal-head' },
1156
- h('h2', null, '识别详情'),
1157
- h('button', { className: 'tcw-icon-btn', onClick: () => setDetailItem(null) }, '×')
1158
- ),
1159
- h('div', { className: 'tcw-detail-grid' }, entries.map(([key, val]) =>
1160
- h('div', { className: 'tcw-detail-field', key },
1161
- h('span', null, fieldLabel(key)),
1162
- h('strong', null, stringify(val))
1163
- )
1164
- )),
1165
- items.length ? h('section', { className: 'tcw-detail-items' },
1166
- h('h3', null, '商品明细'),
1167
- table(['序号', '商品名称', '型号', '数量', '单价', '金额', '海关编码'], items, (item, index) => [
1168
- index + 1,
1169
- value(item.productName || item.englishName),
1170
- value(item.model),
1171
- value(item.quantity),
1172
- value(item.unitPrice || item.taxInclusiveUnitPrice),
1173
- value(item.amount || item.taxInclusiveTotalAmount),
1174
- value(item.hsCode || item.contractHsCode || item.enrichedHsCode)
1175
- ], '暂无商品明细。')
1176
- ) : null,
1177
- h('div', { className: 'tcw-modal-actions' },
1178
- h('button', { className: 'tcw-btn tcw-btn-soft', onClick: () => setDetailItem(null) }, '关闭')
1179
- )
1180
- )
1181
- )
1182
- }
1183
-
1184
- function renderFormModal() {
1185
- const fields = formDialog.type === 'controlled_goods' ? controlledGoodsFormFields : supplierProductFormFields
1186
- const title = `${formDialog.mode === 'create' ? '新增' : '编辑'}${formDialog.type === 'controlled_goods' ? '管控商品' : '供应商商品'}`
1187
- return h('div', { className: 'tcw-modal-backdrop' },
1188
- h('form', { className: formDialog.type === 'supplier_product' ? 'tcw-modal tcw-form-modal tcw-form-modal-supplier' : 'tcw-modal tcw-form-modal', onSubmit: saveProductForm },
1189
- h('div', { className: 'tcw-modal-head' },
1190
- h('h2', null, title),
1191
- h('button', { type: 'button', className: 'tcw-icon-btn', onClick: () => setFormDialog(null) }, '×')
1192
- ),
1193
- h('div', { className: formDialog.type === 'supplier_product' ? 'tcw-edit-grid tcw-edit-grid-two' : 'tcw-edit-grid' },
1194
- fields.map((config) => formField(config))
1195
- ),
1196
- formDialog.type === 'supplier_product' ? renderSupplierHsCandidatePanel() : null,
1197
- h('div', { className: 'tcw-modal-actions' },
1198
- h('button', { type: 'button', className: 'tcw-btn tcw-btn-soft', onClick: () => setFormDialog(null) }, '取消'),
1199
- h('button', { type: 'submit', className: 'tcw-btn tcw-btn-primary', disabled: busy }, '保存')
1200
- )
1201
- )
1202
- )
1203
- }
1204
-
1205
- function renderSupplierHsCandidatePanel() {
1206
- const candidates = Array.isArray(formDialog.hsCandidates) ? formDialog.hsCandidates : []
1207
- const pagination = formDialog.hsCandidatePagination || null
1208
- const hasSourcePagination = pagination && (pagination.hasPrevious || pagination.hasNext || (pagination.pages && pagination.pages.length > 1))
1209
- const localPageSize = Math.max(1, Number(formDialog.hsCandidateLocalPageSize) || 5)
1210
- const localTotalPages = Math.max(1, Math.ceil(candidates.length / localPageSize))
1211
- const localPage = Math.min(Math.max(Number(formDialog.hsCandidateLocalPage) || 1, 1), localTotalPages)
1212
- const visibleCandidates = hasSourcePagination
1213
- ? candidates
1214
- : candidates.slice((localPage - 1) * localPageSize, localPage * localPageSize)
1215
- return h('section', { className: 'tcw-hs-candidate-panel' },
1216
- h('div', { className: 'tcw-subsection-head' },
1217
- h('div', null,
1218
- h('h3', null, '海关编码查询'),
1219
- h('p', null, hsCodeLookupStatusText(formDialog.hsCandidateStatus, candidates.length))
1220
- ),
1221
- h('div', { className: 'tcw-hs-candidate-search' },
1222
- h('input', {
1223
- className: 'tcw-search',
1224
- value: formDialog.hsCandidateKeyword || '',
1225
- placeholder: '商品名称、型号或海关编码',
1226
- onChange: (event) => setFormDialog(Object.assign({}, formDialog, { hsCandidateKeyword: event.target.value }))
1227
- }),
1228
- h('button', { type: 'button', className: 'tcw-mini-btn', disabled: busy || formDialog.hsCandidateLoading, onClick: () => searchSupplierHsCandidatesForForm(1) }, formDialog.hsCandidateLoading ? '查询中' : '查询')
1229
- )
1230
- ),
1231
- formDialog.hsCandidateError ? h('div', { className: 'tcw-error' }, formDialog.hsCandidateError) : null,
1232
- candidates.length ? h('div', { className: 'tcw-hs-candidate-result' },
1233
- table(['编码', '商品名称', '英文名称', '单位', '退税率', '监管', '检验检疫', '操作'], visibleCandidates, (candidate) => [
1234
- h('strong', { className: 'tcw-hs-code-text' }, value(candidate.code)),
1235
- value(candidate.name),
1236
- value(candidate.englishName),
1237
- value(candidate.unit),
1238
- value(candidate.taxRefundRate),
1239
- value(candidate.regulatoryConditions),
1240
- value(candidate.inspectionQuarantine),
1241
- h('div', { className: 'tcw-row-actions' },
1242
- h('button', { type: 'button', className: 'tcw-mini-btn', onClick: () => selectHsCandidate(candidate) }, '选用'),
1243
- h('button', { type: 'button', className: 'tcw-mini-btn', disabled: !candidate.code && !candidate.detailUrl, onClick: () => loadHsCodeDetail(candidate) }, '详情')
1244
- )
1245
- ], '暂无查询结果。'),
1246
- hasSourcePagination
1247
- ? renderSupplierHsCandidatePagination(pagination)
1248
- : renderSupplierHsCandidateLocalPagination(candidates.length, localPage, localTotalPages, localPageSize)
1249
- ) : empty('暂无查询结果,可调整关键词后查询。')
1250
- )
1251
- }
1252
-
1253
- function renderSupplierHsCandidateLocalPagination(total, page, totalPages, pageSize) {
1254
- if (total <= pageSize) return null
1255
- return renderUnifiedPagination({
1256
- total,
1257
- page,
1258
- totalPages,
1259
- pageSize,
1260
- pageSizeOptions: [5, 10, 20],
1261
- jumpValue: formDialog.hsCandidateLocalJumpPage || '',
1262
- onPageChange: (nextPage) => setFormDialog(Object.assign({}, formDialog, { hsCandidateLocalPage: Math.min(Math.max(Number(nextPage) || 1, 1), totalPages) })),
1263
- onPageSizeChange: (nextSize) => setFormDialog(Object.assign({}, formDialog, { hsCandidateLocalPageSize: Number(nextSize) || 5, hsCandidateLocalPage: 1, hsCandidateLocalJumpPage: '' })),
1264
- onJumpInput: (nextValue) => setFormDialog(Object.assign({}, formDialog, { hsCandidateLocalJumpPage: nextValue })),
1265
- onJump: () => {
1266
- const nextPage = Number(formDialog.hsCandidateLocalJumpPage)
1267
- if (!Number.isFinite(nextPage) || nextPage <= 0) return
1268
- setFormDialog(Object.assign({}, formDialog, {
1269
- hsCandidateLocalPage: Math.min(Math.max(nextPage, 1), totalPages),
1270
- hsCandidateLocalJumpPage: ''
1271
- }))
1272
- }
1273
- })
1274
- }
1275
-
1276
- function renderSupplierHsCandidatePagination(pagination) {
1277
- if (!pagination || (!pagination.hasPrevious && !pagination.hasNext && (!pagination.pages || pagination.pages.length <= 1))) return null
1278
- const pages = Array.isArray(pagination.pages) ? pagination.pages.filter((item) => item.page) : []
1279
- const currentPage = pagination.currentPage || formDialog.hsCandidatePage || 1
1280
- const maxPage = Number(pagination.maxVisiblePage) || Math.max(...pages.map((item) => Number(item.page) || 0), currentPage)
1281
- return renderUnifiedPagination({
1282
- page: currentPage,
1283
- totalPages: Math.max(1, maxPage),
1284
- pageSize: 10,
1285
- fixedPageSize: true,
1286
- loading: formDialog.hsCandidateLoading,
1287
- hasPrevious: pagination.hasPrevious,
1288
- hasNext: pagination.hasNext,
1289
- jumpValue: formDialog.hsCandidateJumpPage || '',
1290
- onPageChange: searchSupplierHsCandidatesForForm,
1291
- onJumpInput: (nextValue) => setFormDialog(Object.assign({}, formDialog, { hsCandidateJumpPage: nextValue })),
1292
- onJump: () => {
1293
- const nextPage = Number(formDialog.hsCandidateJumpPage)
1294
- if (!Number.isFinite(nextPage) || nextPage <= 0) return
1295
- searchSupplierHsCandidatesForForm(nextPage)
1296
- setFormDialog((current) => current ? Object.assign({}, current, { hsCandidateJumpPage: '' }) : current)
1297
- }
1298
- })
1299
- }
1300
-
1301
- function renderUnifiedPagination(config) {
1302
- const totalPages = Math.max(1, Number(config.totalPages) || 1)
1303
- const page = Math.min(Math.max(Number(config.page) || 1, 1), totalPages)
1304
- const canPrevious = config.hasPrevious === undefined ? page > 1 : Boolean(config.hasPrevious)
1305
- const canNext = config.hasNext === undefined ? page < totalPages : Boolean(config.hasNext)
1306
- const loading = Boolean(config.loading)
1307
- const jumpValue = config.jumpValue || ''
1308
- const pageSize = Number(config.pageSize) || 10
1309
- return h('div', { className: 'tcw-pagination tcw-pagination-unified' },
1310
- config.total == null ? null : h('span', { className: 'tcw-pagination-total' }, `共 ${config.total} 条`),
1311
- h('button', { className: 'tcw-page-btn', disabled: loading || !canPrevious, onClick: () => config.onPageChange(1) }, '首页'),
1312
- h('button', { className: 'tcw-page-btn', disabled: loading || !canPrevious, onClick: () => config.onPageChange(page - 1) }, '上一页'),
1313
- h('span', { className: 'tcw-page-current' }, `第 ${page} / ${totalPages} 页`),
1314
- h('button', { className: 'tcw-page-btn', disabled: loading || !canNext, onClick: () => config.onPageChange(page + 1) }, '下一页'),
1315
- h('button', { className: 'tcw-page-btn', disabled: loading || !canNext, onClick: () => config.onPageChange(totalPages) }, '末页'),
1316
- h('span', { className: 'tcw-page-size-label' }, '每页'),
1317
- config.fixedPageSize
1318
- ? h('span', { className: 'tcw-page-size-fixed' }, `${pageSize} 条/页`)
1319
- : h('select', {
1320
- className: 'tcw-page-size-select',
1321
- value: pageSize,
1322
- onChange: (event) => config.onPageSizeChange(Number(event.target.value))
1323
- }, (config.pageSizeOptions || [10, 20, 50]).map((size) => h('option', { key: size, value: size }, `${size} 条/页`))),
1324
- h('input', {
1325
- className: 'tcw-page-jump-input',
1326
- type: 'number',
1327
- min: 1,
1328
- max: totalPages,
1329
- value: jumpValue,
1330
- placeholder: String(page),
1331
- onChange: (event) => config.onJumpInput(event.target.value),
1332
- onKeyDown: (event) => {
1333
- if (event.key === 'Enter') {
1334
- event.preventDefault()
1335
- config.onJump()
1336
- }
1337
- }
1338
- }),
1339
- h('button', { className: 'tcw-page-btn', disabled: loading, onClick: config.onJump }, '跳转')
1340
- )
1341
- }
1342
-
1343
- function renderDeleteConfirmModal() {
1344
- return h('div', { className: 'tcw-modal-backdrop' },
1345
- h('section', { className: 'tcw-modal tcw-confirm-modal' },
1346
- h('div', { className: 'tcw-modal-head' },
1347
- h('h2', null, deleteDialog.title),
1348
- h('button', { type: 'button', className: 'tcw-icon-btn', onClick: () => setDeleteDialog(null) }, '×')
1349
- ),
1350
- h('p', { className: 'tcw-confirm-text' }, deleteDialog.message),
1351
- h('div', { className: 'tcw-modal-actions' },
1352
- h('button', { type: 'button', className: 'tcw-btn tcw-btn-soft', onClick: () => setDeleteDialog(null) }, '取消'),
1353
- h('button', { type: 'button', className: 'tcw-btn tcw-btn-danger', disabled: busy, onClick: confirmDeleteDialog }, '确认删除')
1354
- )
1355
- )
1356
- )
1357
- }
1358
-
1359
- function formField(config) {
1360
- const value = formDialog.values[config.key]
1361
- if (config.type === 'checkbox') {
1362
- return h('label', { className: fieldClass(config, 'tcw-field-check'), key: config.key },
1363
- h('span', null, config.label),
1364
- h('input', { type: 'checkbox', checked: Boolean(value), onChange: (event) => updateFormValue(config.key, event.target.checked) })
1365
- )
1366
- }
1367
- if (config.type === 'select') {
1368
- return h('label', { className: fieldClass(config), key: config.key }, h('span', null, config.label), h('select', {
1369
- value: value || '',
1370
- onChange: (event) => updateFormValue(config.key, event.target.value)
1371
- }, config.options.map((option) => h('option', { key: option.value, value: option.value }, option.label))))
1372
- }
1373
- if (config.type === 'textarea') {
1374
- return h('label', { className: fieldClass(config), key: config.key }, h('span', null, config.label), h('textarea', {
1375
- value: value || '',
1376
- onChange: (event) => updateFormValue(config.key, event.target.value),
1377
- placeholder: config.placeholder || ''
1378
- }))
1379
- }
1380
- return h('label', { className: fieldClass(config), key: config.key }, h('span', null, config.label), h('input', {
1381
- type: config.type === 'number' ? 'number' : 'text',
1382
- value: value || '',
1383
- onChange: (event) => updateFormValue(config.key, event.target.value),
1384
- placeholder: config.placeholder || ''
1385
- }))
1386
- }
1387
-
1388
- function updateFormValue(key, value) {
1389
- setFormDialog(Object.assign({}, formDialog, {
1390
- values: Object.assign({}, formDialog.values, { [key]: value })
1391
- }))
1392
- }
1393
-
1394
- function fieldClass(config, extra) {
1395
- return ['tcw-field', config.wide ? 'tcw-field-wide' : '', extra || ''].filter(Boolean).join(' ')
1396
- }
1397
-
1398
- function pageToolbar(title, subtitle, actions) {
1399
- return h('div', { className: 'tcw-page-head' },
1400
- h('div', null, h('h2', null, title), h('p', null, subtitle)),
1401
- h('div', { className: 'tcw-button-row' }, actions)
1402
- )
1403
- }
1404
-
1405
- function uploadButton(actionKey, label) {
1406
- return h(React.Fragment, null,
1407
- h('input', {
1408
- ref: (node) => {
1409
- if (node) fileInputsRef.current[actionKey] = node
1410
- else delete fileInputsRef.current[actionKey]
1411
- },
1412
- className: 'tcw-upload-input',
1413
- type: 'file',
1414
- disabled: busy,
1415
- onChange: (event) => {
1416
- const file = event.target.files && event.target.files[0]
1417
- event.target.value = ''
1418
- debugUpload('file picker returned', { actionKey, hasFile: !!file, fileName: file && file.name, size: file && file.size })
1419
- handleFile(actionKey, file)
1420
- }
1421
- }),
1422
- h('button', {
1423
- type: 'button',
1424
- className: 'tcw-upload',
1425
- disabled: busy,
1426
- onClick: () => openFilePicker(actionKey)
1427
- }, label)
1428
- )
1429
- }
1430
-
1431
- function openFilePicker(actionKey) {
1432
- debugUpload('upload button clicked', { actionKey, busy })
1433
- if (busy) return
1434
- const input = fileInputsRef.current[actionKey]
1435
- if (!input) {
1436
- debugUpload('file input was not registered', { actionKey })
1437
- showNotice('上传控件未准备好,请刷新页面后重试。', 'error')
1438
- return
1439
- }
1440
- input.click()
1441
- }
1442
-
1443
- function workspaceUploadPathForAction(actionKey) {
1444
- if (actionKey === 'upload_controlled_goods_file') return 'trade-compliance-workbench/uploads/controlled-goods'
1445
- if (actionKey === 'upload_supplier_contract') return 'trade-compliance-workbench/uploads/supplier-contracts'
1446
- if (actionKey === 'upload_sales_contract') return 'trade-compliance-workbench/uploads/sales-contracts'
1447
- return 'trade-compliance-workbench/uploads/source-files'
1448
- }
1449
-
1450
- }
1451
-
1452
- function metric(label, valueText, icon, tone) {
1453
- return h('div', { className: 'tcw-metric' },
1454
- h('span', { className: 'tcw-metric-icon ' + (tone || 'blue') }, iconImage(icon, label, tone)),
1455
- h('div', { className: 'tcw-metric-body' },
1456
- h('span', null, label),
1457
- h('strong', null, valueText)
1458
- )
1459
- )
1460
- }
1461
-
1462
- function iconImage(name, label, tone) {
1463
- return h('img', { src: iconDataUri(name, tone), alt: label || '', 'aria-hidden': label ? undefined : true })
1464
- }
1465
-
1466
- function iconDataUri(name, tone) {
1467
- const colors = {
1468
- blue: '#2563eb',
1469
- green: '#64748b',
1470
- orange: '#64748b',
1471
- violet: '#64748b',
1472
- slate: '#64748b'
1473
- }
1474
- const color = colors[tone] || colors.blue
1475
- const paths = {
1476
- dashboard: '<rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/>',
1477
- shield: '<path d="M12 3l7 3v5c0 4.8-2.9 8.2-7 10-4.1-1.8-7-5.2-7-10V6l7-3z"/><path d="M9 12l2 2 4-5"/>',
1478
- box: '<path d="M4 8l8-4 8 4-8 4-8-4z"/><path d="M4 8v8l8 4 8-4V8"/><path d="M12 12v8"/>',
1479
- invoice: '<path d="M7 3h8l4 4v14l-3-1.5L13 21l-3-1.5L7 21V3z"/><path d="M15 3v5h4"/><path d="M9 11h6"/><path d="M9 15h6"/>',
1480
- search: '<circle cx="11" cy="11" r="6"/><path d="M16 16l4 4"/>',
1481
- archive: '<path d="M4 7h16v13H4V7z"/><path d="M3 4h18v3H3z"/><path d="M9 11h6"/>',
1482
- supplier: '<path d="M4 20V8l8-4 8 4v12"/><path d="M8 20v-7h8v7"/><path d="M10 9h4"/>',
1483
- document: '<path d="M7 3h7l5 5v13H7V3z"/><path d="M14 3v6h5"/><path d="M9 13h6"/><path d="M9 17h6"/>',
1484
- records: '<path d="M6 5h12v16H6V5z"/><path d="M9 3h6v4H9z"/><path d="M9 11h6"/><path d="M9 15h6"/>'
1485
- }
1486
- const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths[name] || paths.dashboard}</svg>`
1487
- return 'data:image/svg+xml;utf8,' + encodeURIComponent(svg)
1488
- }
1489
-
1490
- function panel(title, content) {
1491
- return h('section', { className: 'tcw-panel' }, h('h3', null, title), content)
1492
- }
1493
-
1494
- function field(label, child) {
1495
- return h('label', { className: 'tcw-field' }, h('span', null, label), child)
1496
- }
1497
-
1498
- function table(headers, rows, mapRow, emptyText, onRowClick) {
1499
- if (!rows || rows.length === 0) return empty(emptyText || '暂无数据。')
1500
- return h('div', { className: 'tcw-table-wrap' },
1501
- h('table', { className: 'tcw-table' },
1502
- h('thead', null, h('tr', null, headers.map((item, index) => h('th', { key: index, className: tableHeaderClass(item, index, headers) }, item)))),
1503
- h('tbody', null, rows.map((row, index) =>
1504
- h('tr', { key: row.id || index, className: onRowClick ? 'clickable' : '', onClick: onRowClick ? () => onRowClick(row) : undefined },
1505
- mapRow(row, index).map((cell, cellIndex) => h('td', { key: cellIndex, className: tableCellClass(headers[cellIndex], cellIndex, headers) }, normalizeTableCell(cell)))
1506
- )
1507
- ))
1508
- )
1509
- )
1510
- }
1511
-
1512
- function tableHeaderClass(header, index, headers) {
1513
- return tableCellClass(header, index, headers)
1514
- }
1515
-
1516
- function tableCellClass(header, index, headers) {
1517
- const classes = []
1518
- const headerText = String(header || '')
1519
- if (index === 0 && typeof header !== 'string') classes.push('tcw-select-cell')
1520
- if (headerText === '序号') classes.push('tcw-index-cell')
1521
- if (index === headers.length - 1 && headerText === '操作') classes.push('tcw-actions-cell')
1522
- if (/状态|确认/.test(headerText)) classes.push('tcw-status-cell')
1523
- if (/说明|描述|英文|商品|文件|标题|买方|卖方|来源|型号|模板工作表/.test(headerText)) classes.push('tcw-text-cell')
1524
- return classes.join(' ')
1525
- }
1526
-
1527
- function normalizeTableCell(cell) {
1528
- if (cell === undefined || cell === null) return h('span', { className: 'tcw-cell-text muted', title: '-' }, '-')
1529
- if (typeof cell === 'string' || typeof cell === 'number' || typeof cell === 'boolean') {
1530
- const text = String(cell)
1531
- return h('span', { className: 'tcw-cell-text', title: text }, text)
1532
- }
1533
- return cell
1534
- }
1535
-
1536
- function compareBlock(label, data) {
1537
- const entries = Object.entries(isObject(data) ? data : {})
1538
- return h('section', { className: 'tcw-compare' },
1539
- h('h3', null, label),
1540
- entries.length === 0 ? h('p', null, '暂无') : entries.slice(0, 16).map(([key, val]) =>
1541
- h('div', { className: 'tcw-kv', key }, h('span', null, key), h('strong', null, stringify(val)))
1542
- )
87
+ h('aside', { className: 'tcw-sidebar' }, h('div', { className: 'tcw-brand' }, h('span', { className: 'tcw-shield' }, 'G'), h('strong', null, '外贸合规工作台')), h('nav', null, pages.map(item => h('button', { key: item[0], className: page === item[0] ? 'active' : '', onClick: () => { setPage(item[0]); setPurchasePath({ supplierId: null, orderId: null }); setSalesPath({ customerId: null, orderKey: null }) } }, h('span', { 'aria-hidden': true }, item[2]), item[1])))),
88
+ h('main', { className: 'tcw-main' }, h(Topbar, { page, busy, onRefresh: () => reload() }), pageView(page, ctx)),
89
+ notice && h('div', { className: `tcw-toast ${notice.kind || ''}` }, notice.message),
90
+ busy && h('div', { className: 'tcw-busy' }, h('span', { className: 'tcw-spinner' }), '处理中...'),
91
+ dialog && renderDialog(dialog, ctx)
1543
92
  )
1544
93
  }
1545
94
 
1546
- function status(text, level) {
1547
- return h('span', { className: 'tcw-status ' + (level || 'muted') }, text)
1548
- }
1549
-
1550
- function empty(text) {
1551
- return h('div', { className: 'tcw-empty' }, text)
1552
- }
1553
-
1554
- const reviewSearchKeys = ['title', 'sourceLocation']
1555
- const overviewSearchKeys = [
1556
- 'title',
1557
- 'sourceLocation',
1558
- 'sourceFileName',
1559
- 'fileName',
1560
- 'supplierName',
1561
- 'supplierCreditCode',
1562
- 'productName',
1563
- 'model',
1564
- 'description',
1565
- 'hsCode',
1566
- 'contractHsCode',
1567
- 'enrichedHsCode',
1568
- 'englishName',
1569
- 'invoiceNo',
1570
- 'contractNo',
1571
- 'buyerName',
1572
- 'sellerName',
1573
- 'controlNote',
1574
- 'keywords',
1575
- 'referenceNameCandidate',
1576
- 'parseStatus',
1577
- 'parseWarnings',
1578
- 'rawText'
1579
- ]
1580
- const controlledGoodsFormFields = [
1581
- { key: 'productName', label: '商品名称', placeholder: '例如:高性能服务器' },
1582
- { key: 'referenceNameCandidate', label: '候选商品名' },
1583
- { key: 'parseStatus', label: '解析状态' },
1584
- { key: 'parseWarnings', label: '解析提示' },
1585
- { key: 'hsCode', label: '海关编码', placeholder: '例如:8471501010' },
1586
- { key: 'keywords', label: '关键词', placeholder: '逗号或空格分隔' },
1587
- { key: 'controlNote', label: '管控说明', type: 'textarea', wide: true, placeholder: '许可证、两用物项、禁限说明' },
1588
- { key: 'rawText', label: '原文片段', type: 'textarea', wide: true },
1589
- { key: 'sourceFileName', label: '来源文件' },
1590
- { key: 'sourceLocation', label: '来源位置' },
1591
- { key: 'enabled', label: '启用', type: 'checkbox' }
1592
- ]
1593
- const supplierProductFormFields = [
1594
- { key: 'supplierName', label: '供应商名称', placeholder: '例如:某某科技有限公司' },
1595
- { key: 'supplierCreditCode', label: '供应商统一社会信用代码' },
1596
- { key: 'supplierAddress', label: '供应商地址' },
1597
- { key: 'productName', label: '商品名称', placeholder: '例如:服务器' },
1598
- { key: 'model', label: '型号' },
1599
- { key: 'description', label: '商品描述', type: 'textarea', wide: true },
1600
- { key: 'quantity', label: '数量', type: 'number' },
1601
- { key: 'unit', label: '单位' },
1602
- { key: 'taxInclusiveUnitPrice', label: '含税单价', type: 'number' },
1603
- { key: 'taxInclusiveTotalAmount', label: '含税金额', type: 'number' },
1604
- { key: 'contractHsCode', label: '合同海关编码' },
1605
- { key: 'enrichedHsCode', label: '补全海关编码' },
1606
- { key: 'taxRefundRate', label: '退税率' },
1607
- { key: 'englishName', label: '英文品名' },
1608
- {
1609
- key: 'controlledStatus',
1610
- label: '管控状态',
1611
- type: 'select',
1612
- options: [
1613
- { value: 'unchecked', label: '未检查' },
1614
- { value: 'not_controlled', label: '非管控' },
1615
- { value: 'suspected', label: '疑似管控' },
1616
- { value: 'controlled', label: '管控' }
1617
- ]
1618
- },
1619
- { key: 'controlNote', label: '管控说明', type: 'textarea', wide: true }
1620
- ]
1621
- const fieldLabels = {
1622
- productName: '商品名称',
1623
- hsCode: '海关编码',
1624
- keywords: '关键词',
1625
- controlNote: '管控说明',
1626
- enabled: '启用',
1627
- supplierName: '供应商名称',
1628
- supplierCreditCode: '供应商统一社会信用代码',
1629
- supplierAddress: '供应商地址',
1630
- model: '型号',
1631
- description: '商品描述',
1632
- quantity: '数量',
1633
- unit: '单位',
1634
- taxInclusiveUnitPrice: '含税单价',
1635
- taxInclusiveTotalAmount: '含税金额',
1636
- contractHsCode: '合同海关编码',
1637
- enrichedHsCode: '补全海关编码',
1638
- suggestedHsCode: '建议海关编码',
1639
- suggestedHsCodeName: '建议编码品名',
1640
- suggestedHsCodeEnglishName: '建议英文品名',
1641
- suggestedTaxRefundRate: '建议退税率',
1642
- hsCodeCandidateCount: '查询结果数量',
1643
- taxRefundRate: '退税率',
1644
- englishName: '英文品名',
1645
- controlledStatus: '管控状态',
1646
- date: '日期',
1647
- origin: '起运地',
1648
- freight: '运费',
1649
- currency: '币制',
1650
- buyerName: '买方',
1651
- buyerAddress: '买方地址',
1652
- sellerName: '卖方',
1653
- sellerEnglishName: '卖方英文名',
1654
- invoiceNo: '发票号',
1655
- contractNo: '合同号',
1656
- netWeight: '净重',
1657
- grossWeight: '毛重',
1658
- taxNature: '征免性质',
1659
- taxExemptionNature: '征免性质',
1660
- tradeTerm: '贸易术语',
1661
- paymentTerm: '付款条款',
1662
- destination: '目的地',
1663
- packageType: '包装种类',
1664
- supervisionMode: '监管方式',
1665
- domesticSourceLocation: '境内货源地',
1666
- bankBeneficiary: '收款人',
1667
- bankName: '开户银行',
1668
- bankAddress: '银行地址',
1669
- bankAccountNo: '银行账号',
1670
- cnapsCode: 'CNAPS 代码',
1671
- swiftCode: 'Swift 代码',
1672
- sourceFileName: '来源文件'
1673
- }
1674
-
1675
- function emptyFormValues(type) {
1676
- if (type === 'controlled_goods') {
1677
- return {
1678
- productName: '',
1679
- hsCode: '',
1680
- keywords: '',
1681
- referenceNameCandidate: '',
1682
- parseStatus: '',
1683
- parseWarnings: '',
1684
- rawText: '',
1685
- controlNote: '',
1686
- sourceFileName: '',
1687
- sourceLocation: '',
1688
- enabled: true
1689
- }
1690
- }
1691
- return {
1692
- supplierName: '',
1693
- supplierCreditCode: '',
1694
- supplierAddress: '',
1695
- productName: '',
1696
- model: '',
1697
- description: '',
1698
- quantity: '',
1699
- unit: '',
1700
- taxInclusiveUnitPrice: '',
1701
- taxInclusiveTotalAmount: '',
1702
- contractHsCode: '',
1703
- enrichedHsCode: '',
1704
- taxRefundRate: '',
1705
- englishName: '',
1706
- controlledStatus: 'unchecked',
1707
- controlNote: ''
1708
- }
1709
- }
1710
-
1711
- function valuesFromItem(type, item) {
1712
- const merged = type === 'supplier_product' ? readSupplierEditableMerged(item || {}) : readMerged(item || {})
1713
- const base = emptyFormValues(type)
1714
- for (const key of Object.keys(base)) {
1715
- const value = merged[key]
1716
- if (key === 'keywords' && Array.isArray(value)) {
1717
- base[key] = value.join(',')
1718
- } else if (value !== undefined && value !== null) {
1719
- base[key] = String(value)
1720
- }
1721
- }
1722
- if (type === 'controlled_goods') {
1723
- base.enabled = merged.enabled === undefined ? true : Boolean(merged.enabled)
1724
- }
1725
- return base
1726
- }
1727
-
1728
- function normalizeFormPayload(type, values) {
1729
- if (type === 'controlled_goods') {
1730
- return compactObject({
1731
- productName: normalizeText(values.productName),
1732
- referenceNameCandidate: normalizeText(values.referenceNameCandidate),
1733
- parseStatus: normalizeText(values.parseStatus),
1734
- parseWarnings: splitKeywords(values.parseWarnings),
1735
- rawText: normalizeText(values.rawText),
1736
- hsCode: normalizeText(values.hsCode),
1737
- keywords: splitKeywords(values.keywords),
1738
- controlNote: normalizeText(values.controlNote),
1739
- sourceFileName: normalizeText(values.sourceFileName),
1740
- sourceLocation: normalizeText(values.sourceLocation),
1741
- enabled: Boolean(values.enabled)
1742
- })
1743
- }
1744
- return compactObject({
1745
- supplierName: normalizeText(values.supplierName),
1746
- supplierCreditCode: normalizeText(values.supplierCreditCode),
1747
- supplierAddress: normalizeText(values.supplierAddress),
1748
- productName: normalizeText(values.productName),
1749
- model: normalizeText(values.model),
1750
- description: normalizeText(values.description),
1751
- quantity: normalizeNumber(values.quantity),
1752
- unit: normalizeText(values.unit),
1753
- taxInclusiveUnitPrice: normalizeNumber(values.taxInclusiveUnitPrice),
1754
- taxInclusiveTotalAmount: normalizeNumber(values.taxInclusiveTotalAmount),
1755
- contractHsCode: normalizeText(values.contractHsCode),
1756
- enrichedHsCode: normalizeText(values.enrichedHsCode),
1757
- taxRefundRate: normalizeText(values.taxRefundRate),
1758
- englishName: normalizeText(values.englishName),
1759
- controlledStatus: normalizeText(values.controlledStatus) || 'unchecked',
1760
- controlNote: normalizeText(values.controlNote)
1761
- })
1762
- }
1763
-
1764
- function normalizeText(value) {
1765
- const text = String(value ?? '').trim()
1766
- return text ? text : undefined
1767
- }
1768
-
1769
- function normalizeNumber(value) {
1770
- if (value === '' || value === undefined || value === null) return undefined
1771
- const parsed = Number(value)
1772
- return Number.isFinite(parsed) ? parsed : undefined
1773
- }
1774
-
1775
- function buildSupplierHsCandidateKeyword(values) {
1776
- const contractHsCode = normalizeHsCode(values && values.contractHsCode)
1777
- if (contractHsCode) return contractHsCode
1778
- const keyword = [values && values.productName, values && values.model].map(normalizeText).filter(Boolean).join(' ').trim()
1779
- if (keyword) return keyword
1780
- const description = normalizeText(values && values.description)
1781
- return description ? description.slice(0, 100) : ''
1782
- }
1783
-
1784
- function resolveDisplayHsCode(row) {
1785
- return normalizeHsCode(row && row.enrichedHsCode) || normalizeHsCode(row && row.contractHsCode)
1786
- }
1787
-
1788
- function normalizeHsCode(value) {
1789
- const text = normalizeText(value)
1790
- if (!text) return ''
1791
- const digits = text.replace(/\D/g, '')
1792
- return /^\d{8,10}$/.test(digits) ? digits : ''
1793
- }
1794
-
1795
- function buildConfirmReviewData(item) {
1796
- if (!item || item.type !== 'supplier_product') {
1797
- return Object.assign({}, item && item.defaultData || {}, item && item.extractedData || {}, item && item.confirmedData || {})
1798
- }
1799
- return Object.assign(
1800
- {},
1801
- item.defaultData,
1802
- item.extractedData,
1803
- item.confirmedData || {}
1804
- )
1805
- }
1806
-
1807
- function readSupplierEditableMerged(item) {
1808
- return Object.assign(
1809
- {},
1810
- item && item.defaultData,
1811
- item && item.extractedData,
1812
- item && item.confirmedData
1813
- )
1814
- }
1815
-
1816
- function hsCodeLookupStatusText(statusValue, count) {
1817
- if (statusValue === 'confirmed') return '已选用海关编码,保存后写入供应商商品。'
1818
- if (statusValue === 'pending_confirmation') return count ? `已查询到 ${count} 条结果,可选用一条写入商品信息。` : '可查询海关编码并选用结果。'
1819
- if (statusValue === 'not_found') return '未查询到候选编码,可调整关键词重新查询。'
1820
- if (statusValue === 'failed') return '海关编码查询失败,可稍后重试。'
1821
- if (statusValue === 'not_ready') return '缺少商品名称、型号或编码,可补充信息后查询。'
1822
- return count ? `已查询到 ${count} 条结果。` : '可按商品名称、型号或海关编码查询。'
1823
- }
1824
-
1825
- function hsCodeReviewStatusText(row) {
1826
- if (row.enrichedHsCode) return '已确认'
1827
- if (Array.isArray(row.hsCodeCandidates) && row.hsCodeCandidates.length > 0) return '待确认'
1828
- if (row.hsCodeLookupStatus === 'failed') return '查询失败'
1829
- if (row.hsCodeLookupStatus === 'not_found') return '未匹配'
1830
- if (normalizeHsCode(row.contractHsCode)) return '合同编码'
1831
- return '待查询'
1832
- }
1833
-
1834
- function hsCodeReviewStatusLevel(row) {
1835
- if (row.enrichedHsCode) return 'ok'
1836
- if (Array.isArray(row.hsCodeCandidates) && row.hsCodeCandidates.length > 0) return 'warn'
1837
- if (row.hsCodeLookupStatus === 'failed') return 'danger'
1838
- if (row.hsCodeLookupStatus === 'not_found') return 'muted'
1839
- return 'muted'
1840
- }
1841
-
1842
- function splitKeywords(value) {
1843
- if (Array.isArray(value)) return value.map(normalizeText).filter(Boolean)
1844
- return String(value || '').split(/[,\s,、]+/).map(normalizeText).filter(Boolean)
1845
- }
1846
-
1847
- function compactObject(value) {
1848
- return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined))
1849
- }
1850
-
1851
- function normalizeData(payload) {
1852
- const body = payload && payload.summary ? payload.summary : payload || {}
1853
- return {
1854
- reviewItems: Array.isArray(body.reviewItems) ? body.reviewItems : [],
1855
- controlledGoods: Array.isArray(body.controlledGoods) ? body.controlledGoods : [],
1856
- products: Array.isArray(body.products) ? body.products : [],
1857
- workbookGenerations: Array.isArray(body.workbookGenerations) ? body.workbookGenerations : []
1858
- }
1859
- }
1860
-
1861
- function toControlledGoodsReviewRow(item) {
1862
- return {
1863
- id: `controlled:${item.id || item.productName || item.hsCode}`,
1864
- materializedId: item.id,
1865
- type: 'controlled_goods',
1866
- title: item.productName || item.hsCode || '管控商品',
1867
- reviewStatus: 'confirmed',
1868
- extractedData: item,
1869
- defaultData: {},
1870
- confirmedData: item,
1871
- sourceLocation: item.sourceLocation || item.sourceFileName,
1872
- materializedOnly: true
1873
- }
1874
- }
1875
-
1876
- function toSupplierProductReviewRow(item) {
1877
- return {
1878
- id: `product:${item.id || item.productName || item.model}`,
1879
- materializedId: item.id,
1880
- type: 'supplier_product',
1881
- title: item.productName || item.model || '供应商商品',
1882
- reviewStatus: 'confirmed',
1883
- extractedData: item,
1884
- defaultData: {},
1885
- confirmedData: item,
1886
- sourceLocation: item.sourceFileName,
1887
- materializedOnly: true
1888
- }
1889
- }
1890
-
1891
- function getResponsePayload(response) {
1892
- if (!response) return null
1893
- if (Object.prototype.hasOwnProperty.call(response, 'payload')) return response.payload
1894
- if (Object.prototype.hasOwnProperty.call(response, 'data')) return response.data
1895
- if (response.result) return response.result
1896
- return response
1897
- }
1898
-
1899
- function getActionDataPayload(response) {
1900
- const result = getResponsePayload(response)
1901
- if (result && typeof result === 'object' && Object.prototype.hasOwnProperty.call(result, 'data')) return result.data
1902
- return result
1903
- }
1904
-
1905
- function readRecognitionPollingOptions(payload) {
1906
- if (!payload || typeof payload !== 'object') return { expectedCount: 0, expectedChunkCount: 0 }
1907
- const expectedCount = Number(payload.expectedCount)
1908
- const expectedChunkCount = Number(payload.expectedChunkCount)
1909
- return {
1910
- expectedCount: Number.isFinite(expectedCount) && expectedCount > 0 ? expectedCount : 0,
1911
- expectedChunkCount: Number.isFinite(expectedChunkCount) && expectedChunkCount > 0 ? expectedChunkCount : 0
1912
- }
1913
- }
1914
-
1915
- function resolveFileWorkflowNotice(actionKey, dispatched) {
1916
- if (actionKey === 'upload_controlled_goods_file') {
1917
- return dispatched ? '文件已解析并提交待审核,智能体会总结导入质量。' : '文件已解析并提交待审核。'
1918
- }
1919
- return dispatched ? '文件已发送给智能体解析,识别结果会进入待审核列表。' : '文件已登记,等待识别结果。'
1920
- }
1921
-
1922
- function resolveControlledGoodsParseStatus(row) {
1923
- if (row.aiAuditStatus === 'verified') return 'AI 已核对'
1924
- if (row.aiAuditStatus === 'corrected') return 'AI 已修正'
1925
- if (row.aiAuditStatus === 'needs_human_review') return '仍需人工核对'
1926
- if (row.parseStatus === 'ai_verified') return 'AI 已核对'
1927
- if (row.parseStatus === 'ai_corrected') return 'AI 已修正'
1928
- if (row.parseStatus === 'ai_needs_human_review') return '仍需人工核对'
1929
- const warnings = Array.isArray(row.parseWarnings) ? row.parseWarnings : []
1930
- return warnings.length ? `需核对:${warnings.join('、')}` : '已解析'
1931
- }
1932
-
1933
- function assertActionSuccess(response) {
1934
- const payload = getResponsePayload(response)
1935
- if (payload && payload.success === false) throw new Error(resolveMessage(payload.message) || '操作失败')
1936
- }
1937
-
1938
- function collectAssistantCommands(payload) {
1939
- if (!payload || typeof payload !== 'object') return []
1940
- const commands = []
1941
- if (Array.isArray(payload.messages)) payload.messages.forEach((message) => {
1942
- const command = normalizeAssistantCommand(message)
1943
- if (command) commands.push(command)
1944
- })
1945
- const directCommand = normalizeAssistantCommand(payload)
1946
- if (directCommand) commands.push(directCommand)
1947
- return commands
1948
- }
1949
-
1950
- function normalizeAssistantCommand(value) {
1951
- if (!value || typeof value !== 'object' || value.commandKey !== 'assistant.chat.send_message' || !value.payload) return null
1952
- const text = typeof value.payload.text === 'string' && value.payload.text.trim() ? value.payload.text : ''
1953
- if (!text) return null
1954
- return { commandKey: value.commandKey, payload: Object.assign({}, value.payload, { text }) }
1955
- }
1956
-
1957
- function resolveMessage(message) {
1958
- if (!message) return ''
1959
- if (typeof message === 'string') return message
1960
- if (typeof message.zh_Hans === 'string') return message.zh_Hans
1961
- if (typeof message.en_US === 'string') return message.en_US
1962
- return ''
1963
- }
1964
-
1965
- function getErrorMessage(error) {
1966
- return error && error.message ? error.message : String(error || '操作失败')
1967
- }
1968
-
1969
- function debugUpload(message, details) {
1970
- if (typeof console !== 'undefined' && typeof console.debug === 'function') {
1971
- console.debug('[trade-compliance-workbench:upload] ' + message, details || {})
1972
- }
1973
- }
1974
-
1975
- function filterItems(items, query, keys) {
1976
- const keyword = String(query || '').trim().toLowerCase()
1977
- if (!keyword) return items
1978
- return items.filter((item) => {
1979
- const merged = Object.assign({}, item || {}, readMerged(item || {}))
1980
- return keys.some((key) => String(merged[key] || '').toLowerCase().includes(keyword))
1981
- })
1982
- }
1983
-
1984
- function applyStatusFilter(items, statusFilter) {
1985
- if (!statusFilter || statusFilter === 'all') return items
1986
- return items.filter((item) => (item.reviewStatus || 'pending') === statusFilter)
1987
- }
1988
-
1989
- function reviewTypeForUploadAction(actionKey) {
1990
- if (actionKey === 'upload_controlled_goods_file') return 'controlled_goods'
1991
- if (actionKey === 'upload_supplier_contract') return 'supplier_product'
1992
- if (actionKey === 'upload_sales_contract') return 'customs_workbook'
95
+ function Topbar({ page, busy, onRefresh }) { const label = pages.find(item => item[0] === page)?.[1] || ''; return h('header', { className: 'tcw-topbar' }, h('div', null, h('span', null, '首页'), h('b', null, ' / '), h('span', null, label)), h('button', { className: 'icon-button', title: '刷新', disabled: busy, onClick: onRefresh }, '↻')) }
96
+ function pageView(page, ctx) { if (page === 'home') return h(HomePage, ctx); if (page === 'controlled') return h(ControlledPage, ctx); if (page === 'sanctions') return h(SanctionsPage, ctx); if (page === 'purchase') return h(PurchasePage, ctx); if (page === 'contract-review') return h(ContractReviewPage, ctx); if (page === 'sales') return h(SalesPage, ctx); if (page === 'files') return h(FilesPage, ctx); if (page === 'analytics') return h(AnalyticsPage, ctx); return h(SettingsPage, ctx) }
97
+ function PageTitle({ title, actions, breadcrumb }) { return h('div', { className: 'tcw-page-title' }, h('div', null, breadcrumb && h('div', { className: 'tcw-breadcrumb' }, breadcrumb), h('h1', null, title)), h('div', { className: 'tcw-page-actions' }, actions)) }
98
+ function SearchBar({ value, onChange, fields }) { return h('div', { className: 'tcw-filterbar' }, h('label', null, h('span', null, fields || '动态筛选'), h('input', { value, placeholder: '输入关键字', onChange: event => onChange(event.target.value) })), h('button', { className: 'button muted', onClick: () => onChange('') }, '重置')) }
99
+ function Button({ children, onClick, tone, title, disabled }) { return h('button', { className: `button ${tone || ''}`, onClick, title, disabled }, children) }
100
+ function Status({ children, tone }) { return h('span', { className: `status ${tone || ''}` }, children) }
101
+ function Empty({ label }) { return h('div', { className: 'tcw-empty' }, label || '暂无数据') }
102
+
103
+ function HomePage({ data }) {
104
+ const d = data.dashboard || {}
105
+ return h('section', null, h(PageTitle, { title: '首页' }), h('div', { className: 'tcw-risk-band' }, h('div', { className: 'risk-icon' }, '!'), h('div', { dangerouslySetInnerHTML: { __html: sanitizeRich(d.riskNotice && d.riskNotice.content || '') } })), h('div', { className: 'tcw-metrics' }, metric('商品风险命中', d.controlledRiskCount || 0), metric('公司制裁命中', d.sanctionRiskCount || 0), metric('未关联采购合同', d.unlinkedPurchaseCount || 0), metric('未关联客户合同', d.unlinkedCustomerCount || 0)), h('div', { className: 'tcw-process' }, ['维护风险目录', '上传采购合同', '审核客户合同', '销售合规校验', '生成销售文件'].map((name, index) => h(React.Fragment, { key: name }, h('div', { className: 'process-step' }, h('b', null, index + 1), h('span', null, name)), index < 4 && h('span', { className: 'process-arrow' }, '→')))))
106
+ }
107
+ function metric(label, value) { return h('div', { className: 'metric' }, h('span', null, label), h('strong', null, value)) }
108
+
109
+ function ControlledPage(ctx) {
110
+ const rows = ctx.data.controlledGoods.items || []
111
+ const actions = [h(Button, { key: 'upload', onClick: () => ctx.setDialog({ type: 'catalog-upload', catalog: 'controlled' }) }, '↑ 上传目录文件'), h(Button, { key: 'add', tone: 'muted', onClick: () => editControlled(ctx) }, '+ 手工新增')]
112
+ return h('section', null, h(PageTitle, { title: '管控商品目录', actions }), h(SearchBar, { value: ctx.search, onChange: ctx.setSearch, fields: '目录类型、来源文件、商品名称、海关编码' }), h(DataTable, { columns: [['productName', '商品名称', (value) => h('button', { className: 'cell-link', onClick: () => openHsDialog(ctx, value) }, value || '—')], ['hsCode', '海关编码', (value, row) => h('button', { className: 'cell-link', onClick: () => openHsDialog(ctx, value || row.productName) }, value || '—')], ['catalogType', '合规分类', value => value === 'DUAL_USE' ? '军民两用物资' : '海外制裁'], ['controlDescription', '管控说明', 'ellipsis'], ['sourceType', '来源'], ['updatedAt', '更新时间', dateTime]], rows, actions: row => [h('button', { key: 'edit', onClick: () => editControlled(ctx, row) }, '审核'), h('button', { key: 'delete', className: 'danger-link', onClick: () => confirmDelete(ctx, 'controlledGoods', row.id, row.productName) }, '删除')] }), h(ImportSummary, { tasks: ctx.data.importTasks.items, type: 'CONTROLLED_CATALOG', ctx }))
113
+ }
114
+ function editControlled(ctx, row) { ctx.setDialog({ type: 'form', title: row ? '审核管控商品' : '新增管控商品', actionKey: row ? 'update_controlled_goods' : 'save_controlled_goods', values: Object.assign({ catalogType: 'DUAL_USE', productName: '', hsCode: '', controlDescription: '' }, row || {}), fields: [['catalogType', '目录类型', 'select', [['DUAL_USE', '军民两用物资'], ['OVERSEAS_SANCTION', '海外制裁']]], ['productName', '商品名称', 'text', true], ['hsCode', '海关编码'], ['controlDescription', '管控说明', 'textarea']], source: row && sourcePane(row) }) }
115
+
116
+ function SanctionsPage(ctx) {
117
+ const rows = ctx.data.sanctionedCompanies.items || []
118
+ const actions = [h(Button, { key: 'upload', onClick: () => ctx.setDialog({ type: 'catalog-upload', catalog: 'sanctions' }) }, '↑ 上传目录文件'), h(Button, { key: 'add', tone: 'muted', onClick: () => editSanction(ctx) }, '+ 手工新增')]
119
+ return h('section', null, h(PageTitle, { title: '公司制裁目录', actions }), h(SearchBar, { value: ctx.search, onChange: ctx.setSearch, fields: '公司名称、国家地区、制裁名单来源' }), h(DataTable, { columns: [['companyName', '公司名称', 'link'], ['countryOrRegion', '国家或地区'], ['sanctionListSource', '制裁名单来源'], ['sanctionReason', '制裁原因', 'ellipsis'], ['effectiveDate', '生效日期'], ['updatedAt', '更新时间', dateTime]], rows, actions: row => [h('button', { key: 'edit', onClick: () => editSanction(ctx, row) }, '审核'), h('button', { key: 'delete', className: 'danger-link', onClick: () => confirmDelete(ctx, 'sanctionedCompany', row.id, row.companyName) }, '删除')] }), h(ImportSummary, { tasks: ctx.data.importTasks.items, type: 'SANCTION_CATALOG', ctx }))
120
+ }
121
+ function editSanction(ctx, row) { ctx.setDialog({ type: 'form', title: row ? '审核制裁公司' : '新增制裁公司', actionKey: row ? 'update_sanctioned_company' : 'save_sanctioned_company', values: Object.assign({ companyName: '', countryOrRegion: '', sanctionListSource: '', effectiveDate: '', sanctionReason: '', remarks: '' }, row || {}), fields: [['companyName', '公司名称', 'text', true], ['countryOrRegion', '国家或地区'], ['sanctionListSource', '制裁名单来源'], ['effectiveDate', '发布/生效日期', 'date'], ['sanctionReason', '制裁原因', 'textarea'], ['remarks', '备注', 'textarea']], source: row && sourcePane(row) }) }
122
+
123
+ function PurchasePage(ctx) {
124
+ const path = ctx.purchasePath
125
+ if (path.orderId) return h(PurchaseLines, ctx)
126
+ if (path.supplierId) return h(PurchaseOrders, ctx)
127
+ const rows = ctx.data.suppliers.items || []
128
+ return h('section', null, h(PageTitle, { title: '采购合规管理', actions: [h(Button, { key: 'upload', onClick: () => ctx.setDialog({ type: 'file-upload', actionKey: 'upload_purchase_contract', title: '上传采购合同' }) }, '↑ 上传采购合同')] }), h(SearchBar, { value: ctx.search, onChange: ctx.setSearch, fields: '供应商代码、名称、简称、联系人、经营产品' }), h(DataTable, { columns: [['code', '供应商代码'], ['name', '供应商名称', 'ellipsis'], ['shortName', '简称'], ['contactName', '联系人'], ['phone', '联系电话'], ['businessProducts', '经营产品', 'ellipsis']], rows, actions: row => [h('button', { key: 'detail', onClick: () => ctx.setPurchasePath({ supplierId: row.id, orderId: null }) }, '详情'), h('button', { key: 'edit', onClick: () => editSupplier(ctx, row) }, '审核'), h('button', { key: 'delete', className: 'danger-link', onClick: () => confirmDelete(ctx, 'supplier', row.id, row.name) }, '删除')] }), h(ImportSummary, { tasks: ctx.data.importTasks.items, type: 'PURCHASE_CONTRACT', ctx, review: true }))
129
+ }
130
+ function editSupplier(ctx, row) { ctx.setDialog({ type: 'form', title: '审核供应商信息', actionKey: 'update_supplier', values: row, fields: partyFields('供应商') }) }
131
+ function PurchaseOrders(ctx) {
132
+ const supplier = (ctx.data.suppliers.items || []).find(row => row.id === ctx.purchasePath.supplierId)
133
+ const rows = (ctx.data.purchaseOrders.items || []).filter(row => row.supplierId === ctx.purchasePath.supplierId)
134
+ const total = sum(rows, 'contractAmountCny'); const annual = rows.filter(row => String(row.orderDate || '').startsWith(String(new Date().getFullYear())))
135
+ return h('section', null, h(PageTitle, { title: `${supplier?.name || ''} / 采购订单`, breadcrumb: h('button', { className: 'back-link', onClick: () => ctx.setPurchasePath({ supplierId: null, orderId: null }) }, '← 返回供应商'), actions: [h(Button, { key: 'upload', onClick: () => ctx.setDialog({ type: 'file-upload', actionKey: 'upload_purchase_contract', title: '上传采购合同' }) }, '↑ 上传采购合同')] }), h('div', { className: 'tcw-metrics compact' }, metric('订单总数', rows.length), metric('采购总金额', money(total)), metric('本年订单数量', annual.length), metric('本年采购金额', money(sum(annual, 'contractAmountCny'))), metric('采购商品 TOP5', topLabel(ctx.data.analytics.purchaseTop5))), h(DataTable, { columns: [['contractNo', '订单编码'], ['orderDate', '下单日期'], ['contractAmountCny', '订单总额', money], ['paidAmountCny', '已付款', money], ['sourceFileName', '来源合同', 'ellipsis']], rows, actions: row => [h('button', { key: 'detail', onClick: () => ctx.setPurchasePath({ supplierId: row.supplierId, orderId: row.id }) }, '详情'), h('button', { key: 'edit', onClick: () => editPurchaseOrder(ctx, row) }, '审核'), h('button', { key: 'delete', className: 'danger-link', onClick: () => confirmDelete(ctx, 'purchaseOrder', row.id, row.contractNo) }, '删除')] }))
136
+ }
137
+ function editPurchaseOrder(ctx, row) { ctx.setDialog({ type: 'form', title: '审核采购订单', actionKey: 'update_purchase_order', values: row, fields: [['contractNo', '合同编号', 'readonly'], ['orderDate', '下单日期', 'date'], ['contractAmountCny', '订单总额(人民币)', 'number'], ['paidAmountCny', '已付款(人民币)', 'number'], ['refundedAmountCny', '已退税(人民币)', 'number']] }) }
138
+ function PurchaseLines(ctx) {
139
+ const order = (ctx.data.purchaseOrders.items || []).find(row => row.id === ctx.purchasePath.orderId)
140
+ const rows = (ctx.data.purchaseLines.items || []).filter(row => row.purchaseOrderId === ctx.purchasePath.orderId)
141
+ return h('section', null, h(PageTitle, { title: `${order?.contractNo || ''} / 商品明细`, breadcrumb: h('button', { className: 'back-link', onClick: () => ctx.setPurchasePath({ supplierId: ctx.purchasePath.supplierId, orderId: null }) }, '← 返回订单'), actions: [h(Button, { key: 'add', tone: 'muted', onClick: () => editPurchaseLine(ctx, null, order) }, '+ 新增商品')] }), h(SearchBar, { value: ctx.search, onChange: ctx.setSearch, fields: '商品名称、描述、规格型号、海关编码' }), h(DataTable, { columns: [['lineNumber', '序号'], ['productName', '商品名称', 'ellipsis'], ['modelSpecification', '规格型号', 'ellipsis'], ['quantity', '数量'], ['unit', '单位'], ['unitPriceTaxIncluded', '含税单价', money], ['lineAmountTaxIncluded', '含税总价', money], ['hsCode', '海关编码'], ['refundRate', '退税率', percent]], rows, actions: row => [h('button', { key: 'edit', onClick: () => editPurchaseLine(ctx, row, order) }, '审核'), h('button', { key: 'delete', className: 'danger-link', onClick: () => confirmDelete(ctx, 'purchaseLine', row.id, row.productName) }, '删除')] }))
142
+ }
143
+ function editPurchaseLine(ctx, row, order) { ctx.setDialog({ type: 'form', title: row ? '审核采购商品' : '新增采购商品', actionKey: row ? 'update_purchase_line' : 'add_purchase_line', values: Object.assign({ purchaseOrderId: order.id, productName: '', quantity: 1, unitPriceTaxIncluded: 0, lineAmountTaxIncluded: 0, hsCodeStatus: 'UNCONFIRMED' }, row || {}), fields: [['productName', '商品名称', 'text', true], ['englishName', '英文名称'], ['productDescription', '商品描述', 'textarea'], ['modelSpecification', '规格型号'], ['unit', '单位'], ['quantity', '数量', 'number', true], ['unitPriceTaxIncluded', '含税单价', 'number', true], ['lineAmountTaxIncluded', '含税总价', 'number', true], ['vatRate', '增值税率', 'number'], ['hsCode', '海关编码'], ['hsCodeStatus', '编码状态', 'select', [['UNCONFIRMED', '未确认'], ['AI_CONFIRMED', 'AI确认'], ['MANUALLY_CONFIRMED', '人工确认']]], ['refundRate', '退税率', 'number'], ['refundPolicyReference', '退税政策依据', 'textarea'], ['refundPolicyDate', '退税政策日期', 'date']], source: row && sourcePane(row) }) }
144
+
145
+ function ContractReviewPage(ctx) {
146
+ const rows = ctx.data.customerContracts.items || []; const customers = ctx.data.customers.items || []
147
+ return h('section', null, h(PageTitle, { title: '客户合同审核', actions: [h(Button, { key: 'upload', onClick: () => ctx.setDialog({ type: 'file-upload', actionKey: 'upload_customer_contract', title: '上传客户合同' }) }, '↑ 上传客户合同')] }), h(SearchBar, { value: ctx.search, onChange: ctx.setSearch, fields: '合同编码、客户名称、来源文件' }), h(DataTable, { columns: [['contractNo', '合同编码'], ['customerId', '客户名称', value => customers.find(item => item.id === value)?.name || ''], ['contractAmountReviewCny', '预审合同金额', money], ['signedAt', '签订日期'], ['overallRiskLevel', '条款风险', riskLabel], ['sourceFileName', '来源文件', 'ellipsis']], rows, actions: row => [h('button', { key: 'review', onClick: () => openContractReview(ctx, row.id) }, '审核'), h('button', { key: 'delete', className: 'danger-link', onClick: () => confirmDelete(ctx, 'customerContract', row.id, row.contractNo) }, '删除')] }), h(ImportSummary, { tasks: ctx.data.importTasks.items, type: 'CUSTOMER_CONTRACT', ctx, review: true }))
148
+ }
149
+ async function openContractReview(ctx, id) { const review = await ctx.run('get_customer_contract_review', { id }, { silent: true, noReload: true }); ctx.setDialog({ type: 'contract-detail', review }) }
150
+ async function openHsDialog(ctx, keywords) { const result = await ctx.run('search_hs_code', { keywords, page: 1 }, { silent: true, noReload: true }); ctx.setDialog({ type: 'hs-detail', keywords, result }) }
151
+
152
+ function SalesPage(ctx) {
153
+ const path = ctx.salesPath
154
+ if (path.orderKey) return h(SalesLines, ctx)
155
+ if (path.customerId) return h(SalesOrders, ctx)
156
+ const rows = ctx.data.customers.items || []
157
+ return h('section', null, h(PageTitle, { title: '销售合规管理' }), h(SearchBar, { value: ctx.search, onChange: ctx.setSearch, fields: '客户代码、名称、国别、联系人' }), h(DataTable, { columns: [['code', '客户代码'], ['name', '客户名称', 'ellipsis'], ['shortName', '简称'], ['country', '国别'], ['contactName', '联系人'], ['defaultPricingProfitRate', '默认利润率', percent]], rows, actions: row => [h('button', { key: 'detail', onClick: () => enterCustomer(ctx, row) }, '订单详情'), h('button', { key: 'profit', onClick: () => profitDialog(ctx, row) }, '默认利润率')] }))
158
+ }
159
+ function enterCustomer(ctx, row) { if (row.defaultPricingProfitRate == null) ctx.setDialog({ type: 'confirm', title: '利润率未维护', message: '进入销售订单前必须维护客户默认利润率。', confirmText: '维护利润率', onConfirm: () => profitDialog(ctx, row) }); else ctx.setSalesPath({ customerId: row.id, orderKey: null }) }
160
+ function profitDialog(ctx, customer) { ctx.setDialog({ type: 'form', title: '维护客户默认利润率', actionKey: 'save_customer_profit_rate', values: { customerId: customer.id, rate: customer.defaultPricingProfitRate || 0 }, fields: [['customerId', '客户ID', 'hidden'], ['rate', '默认利润率(小数)', 'number', true]] }) }
161
+ function SalesOrders(ctx) {
162
+ const customer = (ctx.data.customers.items || []).find(row => row.id === ctx.salesPath.customerId)
163
+ const rows = (ctx.data.derivedSalesOrders || []).filter(row => row.customerId === ctx.salesPath.customerId)
164
+ const annual = rows.filter(row => String(row.orderDate || '').startsWith(String(new Date().getFullYear())))
165
+ return h('section', null, h(PageTitle, { title: `${customer?.name || ''} / 销售订单`, breadcrumb: h('button', { className: 'back-link', onClick: () => ctx.setSalesPath({ customerId: null, orderKey: null }) }, '← 返回客户') }), h('div', { className: 'tcw-metrics compact' }, metric('订单总数', rows.length), metric('订单总额', money(sum(rows, 'orderAmountCny'))), metric('本年订单数量', annual.length), metric('本年合同金额', money(sum(annual, 'orderAmountCny'))), metric('销售商品 TOP5', topLabel(ctx.data.analytics.salesTop5))), h(DataTable, { columns: [['salesOrderNo', '销售订单编码'], ['purchaseContractNo', '采购合同编码'], ['orderDate', '下单日期'], ['effectivePricingProfitRate', '有效利润率', percent], ['orderAmountCny', '订单总额', money], ['receivedAmountCny', '已收款', money], ['unreceivedAmountCny', '未收款', money]], rows, actions: row => [h('button', { key: 'detail', onClick: () => ctx.setSalesPath({ customerId: row.customerId, orderKey: orderKey(row) }) }, '商品详情'), h('button', { key: 'profit', onClick: () => salesManualDialog(ctx, row) }, '订单利润率'), h('button', { key: 'receipt', onClick: () => receiptDialog(ctx, row) }, '已收款')] }))
166
+ }
167
+ function SalesLines(ctx) { const order = (ctx.data.derivedSalesOrders || []).find(row => orderKey(row) === ctx.salesPath.orderKey); return h('section', null, h(PageTitle, { title: `${order?.salesOrderNo || ''} / 销售商品`, breadcrumb: h('button', { className: 'back-link', onClick: () => ctx.setSalesPath({ customerId: ctx.salesPath.customerId, orderKey: null }) }, '← 返回订单') }), h(DataTable, { columns: [['productName', '商品名称', 'ellipsis'], ['modelSpecification', '规格型号', 'ellipsis'], ['productDescription', '商品描述', 'ellipsis'], ['quantity', '数量'], ['unit', '单位'], ['salesUnitPriceCny', '销售单价', money], ['salesLineAmountCny', '销售总价', money], ['hsCode', '海关编码'], ['complianceMatches', '合规分类', matchesLabel]], rows: order?.lines || [] })) }
168
+ function salesManualDialog(ctx, row) { ctx.setDialog({ type: 'form', title: '维护订单利润率', actionKey: 'save_derived_manual_data', values: { customerContractId: row.customerContractId, purchaseOrderId: row.purchaseOrderId, pricingProfitRateOverride: row.pricingProfitRateSource === 'ORDER_OVERRIDE' ? row.effectivePricingProfitRate : '' }, fields: [['customerContractId', '', 'hidden'], ['purchaseOrderId', '', 'hidden'], ['pricingProfitRateOverride', '订单利润率(留空继承客户)', 'number']] }) }
169
+ function receiptDialog(ctx, row) { ctx.setDialog({ type: 'form', title: '维护已收款', actionKey: 'save_derived_manual_data', values: { customerContractId: row.customerContractId, purchaseOrderId: row.purchaseOrderId, receivedAmountCny: row.receivedAmountCny }, fields: [['customerContractId', '', 'hidden'], ['purchaseOrderId', '', 'hidden'], ['receivedAmountCny', '已收款(人民币)', 'number', true]] }) }
170
+
171
+ function FilesPage(ctx) {
172
+ const rows = ctx.data.derivedSalesOrders || []
173
+ return h('section', null, h(PageTitle, { title: '销售文件管理' }), h(SearchBar, { value: ctx.search, onChange: ctx.setSearch, fields: '销售订单、采购合同、客户合同' }), h(DataTable, { columns: [['salesOrderNo', '销售订单'], ['purchaseContractNo', '采购合同'], ['customerContractNo', '客户合同'], ['orderDate', '下单日期'], ['orderAmountCny', '销售金额', money], ['effectivePricingProfitRate', '定价利润率', percent]], rows, actionWidth: 330, actions: row => ['PI', 'CI', 'PL', 'CUSTOMS_DECLARATION'].map(type => h('button', { key: type, onClick: () => generationDialog(ctx, row, type) }, `生成${type === 'CUSTOMS_DECLARATION' ? '报关单' : type}`)).concat(h('button', { key: 'history', onClick: () => ctx.setDialog({ type: 'history', row }) }, '历史生成')) }))
174
+ }
175
+ function generationDialog(ctx, row, fileType) { const banks = ctx.data.settings.banks || []; ctx.setDialog({ type: 'form', title: `生成${fileType === 'CUSTOMS_DECLARATION' ? '报关单' : fileType}`, actionKey: 'generate_sales_file', values: { customerContractId: row.customerContractId, purchaseOrderId: row.purchaseOrderId, fileType, bankType: banks[0]?.bankType || 'OVERSEAS_CNY' }, fields: [['customerContractId', '', 'hidden'], ['purchaseOrderId', '', 'hidden'], ['fileType', '', 'hidden']].concat(fileType === 'CUSTOMS_DECLARATION' ? [] : [['bankType', '银行信息', 'select', banks.map(item => [item.bankType, `${bankLabel(item.bankType)} / ${item.bankName || '未维护'}`]), true]]) }) }
176
+
177
+ function AnalyticsPage(ctx) { const a = ctx.data.analytics || {}; return h('section', null, h(PageTitle, { title: '数据分析' }), h(SearchBar, { value: ctx.search, onChange: ctx.setSearch, fields: '下单日期、供应商/客户、商品名称、海关编码、未付款/未收款' }), h('div', { className: 'tcw-tabs' }, h('button', { className: 'active' }, '采购订单报表'), h('button', null, '销售订单报表')), h('div', { className: 'tcw-metrics' }, metric('采购订单总额', money(a.purchase?.totalCny)), metric('采购已付款', money(a.purchase?.paidCny)), metric('采购未付款', money(a.purchase?.unpaidCny)), metric('销售订单总额', money(a.sales?.totalCny)), metric('销售已收款', money(a.sales?.paidCny)), metric('销售未收款', money(a.sales?.unpaidCny))), h('div', { className: 'tcw-analysis-grid' }, h(Ranking, { title: '采购商品 TOP5', rows: a.purchaseTop5 }), h(Ranking, { title: '销售商品 TOP5', rows: a.salesTop5 }))) }
178
+ function Ranking({ title, rows }) { return h('div', { className: 'ranking' }, h('h3', null, title), (rows || []).map((row, index) => h('div', { key: row.name }, h('b', null, index + 1), h('span', { title: row.name }, row.name), h('strong', null, money(row.amountCny))))) }
179
+
180
+ function SettingsPage(ctx) {
181
+ const s = ctx.data.settings || {}; const company = s.company || {}; const profit = s.profit || {}; const risk = s.risk || {}
182
+ return h('section', null, h(PageTitle, { title: '系统设置' }), h('div', { className: 'settings-stack' },
183
+ settingBlock('风险提示', h('div', { className: 'rich-preview', dangerouslySetInnerHTML: { __html: sanitizeRich(risk.content || '') } }), () => ctx.setDialog({ type: 'rich', title: '编辑风险提示', values: { content: risk.content || '' } })),
184
+ settingBlock('企业信息', h(KeyValues, { values: [['公司中文名称', company.companyNameZh], ['公司英文名称', company.companyNameEn], ['统一社会信用代码', company.unifiedSocialCreditCode], ['电话', company.phone], ['邮箱', company.email]] }), () => settingsForm(ctx, 'company', company, [['companyNameZh', '公司中文名称'], ['companyNameEn', '公司英文名称'], ['unifiedSocialCreditCode', '统一社会信用代码'], ['addressZh', '中文地址', 'textarea'], ['addressEn', '英文地址', 'textarea'], ['phone', '公司电话'], ['email', '公司邮箱']])),
185
+ settingBlock('银行信息', h(DataTable, { columns: [['bankType', '银行类型', bankLabel], ['beneficiary', '收款人'], ['bankName', '银行名称'], ['accountNo', '账号', 'ellipsis'], ['swiftCode', 'SWIFT']], rows: s.banks || [], actions: row => [h('button', { key: 'edit', onClick: () => bankForm(ctx, row) }, '编辑')] }), () => bankForm(ctx, { bankType: 'OVERSEAS_CNY' })),
186
+ settingBlock('合规利润规则', h(KeyValues, { values: [['配置状态', profit.status === 'ACTIVE' ? '已启用' : '未配置'], ['最低合规利润率', percent(profit.minimumOrderProfitRate)], ['换汇成本范围', `${profit.exchangeCostMinimum || 5} - ${profit.exchangeCostMaximum || 8}`]] }), () => settingsForm(ctx, 'profit', profit, [['status', '配置状态', 'select', [['UNCONFIGURED', '未配置'], ['ACTIVE', '启用']]], ['minimumOrderProfitRate', '最低合规利润率', 'number'], ['exchangeCostMinimum', '换汇成本下限', 'number'], ['exchangeCostMaximum', '换汇成本上限', 'number']])),
187
+ settingBlock('月度美元汇率', h(DataTable, { columns: [['year', '年份'], ['month', '月份'], ['currencyCode', '币种'], ['usdToCnyRate', '美元兑人民币'], ['sourceReference', '来源说明', 'ellipsis']], rows: s.rates || [], actions: row => [h('button', { key: 'edit', onClick: () => exchangeForm(ctx, row) }, '编辑'), h('button', { key: 'delete', className: 'danger-link', onClick: () => confirmDelete(ctx, 'exchangeRate', row.id, `${row.year}-${row.month}`) }, '删除')] }), () => exchangeForm(ctx, {}))
188
+ ))
189
+ }
190
+ function settingBlock(title, content, onEdit) { return h('div', { className: 'setting-block' }, h('div', { className: 'setting-head' }, h('h2', null, title), h(Button, { tone: 'muted', onClick: onEdit }, '编辑')), content) }
191
+ function settingsForm(ctx, type, values, fields) { ctx.setDialog({ type: 'form', title: '编辑系统设置', actionKey: 'save_settings', values: { settingsType: type, values: Object.assign({}, values) }, nestedValues: true, fields }) }
192
+ function bankForm(ctx, row) { settingsForm(ctx, 'bank', row, [['bankType', '银行类型', 'select', [['OVERSEAS_CNY', '国外收款银行(人民币)'], ['OVERSEAS_USD', '国外收款银行(美元)']]], ['beneficiary', 'Beneficiary'], ['bankName', 'Bank Name'], ['bankAddress', 'Bank Address', 'textarea'], ['cnapsCode', 'CNAPS Code'], ['accountNo', 'Account No.'], ['swiftCode', 'SWIFT Code']]) }
193
+ function exchangeForm(ctx, row) { settingsForm(ctx, 'rate', Object.assign({ year: new Date().getFullYear(), month: new Date().getMonth() + 1 }, row), [['year', '年份', 'number'], ['month', '月份', 'number'], ['usdToCnyRate', '美元兑人民币汇率', 'number', true], ['sourceReference', '来源说明', 'textarea']]) }
194
+
195
+ function ImportSummary({ tasks, type, ctx, review }) {
196
+ const rows = (tasks || []).filter(task => task.documentType === type).slice(0, 5)
197
+ if (!rows.length) return null
198
+ return h('div', { className: 'import-summary' }, h('h3', null, '最近解析任务'), h(DataTable, { columns: [['sourceFileName', '文件名称', 'ellipsis'], ['status', '解析状态', taskStatus], ['errorMessage', '提示', 'ellipsis'], ['createdAt', '上传时间', dateTime]], rows, actions: task => review && task.status === 'AWAITING_REVIEW' ? [h('button', { key: 'review', onClick: () => ctx.setDialog({ type: 'draft-review', task }) }, '审核')] : !review && task.status === 'SUCCEEDED' && task.resultEntityId ? [h('button', { key: 'issues', onClick: () => downloadBase64(ctx, 'download_import_issues', { documentType: type, batchId: task.resultEntityId }) }, '问题明细')] : [] }))
199
+ }
200
+
201
+ function DataTable({ columns, rows, actions, actionWidth }) {
202
+ if (!rows || !rows.length) return h(Empty)
203
+ return h('div', { className: 'table-wrap' }, h('table', null, h('thead', null, h('tr', null, columns.map(column => h('th', { key: column[0] }, column[1])), actions && h('th', { className: 'action-cell', style: { width: actionWidth || 180, minWidth: actionWidth || 180 } }, '操作'))), h('tbody', null, rows.map((row, index) => h('tr', { key: row.id || `${index}` }, columns.map(column => h('td', { key: column[0], className: column[2] === 'ellipsis' ? 'ellipsis' : '', title: titleFor(row[column[0]], column[2]) }, cell(row[column[0]], column[2], row))), actions && h('td', { className: 'action-cell', style: { width: actionWidth || 180, minWidth: actionWidth || 180 } }, h('div', { className: 'row-actions' }, actions(row))))))))
204
+ }
205
+ function cell(value, renderer, row) { if (typeof renderer === 'function') return renderer(value, row); if (renderer === 'link') return h('button', { className: 'cell-link', title: value || '' }, value || '—'); if (renderer === 'ellipsis') return value || '—'; if (Array.isArray(value)) return value.join('、'); return value === null || value === undefined || value === '' ? '—' : String(value) }
206
+ function titleFor(value, renderer) { return renderer === 'ellipsis' ? String(value || '') : undefined }
207
+ function KeyValues({ values }) { return h('div', { className: 'key-values' }, values.map(item => h('div', { key: item[0] }, h('span', null, item[0]), h('strong', null, item[1] || '—')))) }
208
+
209
+ function renderDialog(dialog, ctx) {
210
+ if (dialog.type === 'form') return h(FormDialog, { dialog, ctx })
211
+ if (dialog.type === 'catalog-upload') return h(CatalogUploadDialog, { dialog, ctx })
212
+ if (dialog.type === 'file-upload') return h(FileUploadDialog, { dialog, ctx })
213
+ if (dialog.type === 'confirm') return h(ConfirmDialog, { dialog, ctx })
214
+ if (dialog.type === 'draft-review') return h(DraftReviewDialog, { dialog, ctx })
215
+ if (dialog.type === 'contract-detail') return h(ContractDetailDialog, { dialog, ctx })
216
+ if (dialog.type === 'hs-detail') return h(HsDetailDialog, { dialog, ctx })
217
+ if (dialog.type === 'history') return h(HistoryDialog, { dialog, ctx })
218
+ if (dialog.type === 'excel') return h(ExcelDialog, { dialog, ctx })
219
+ if (dialog.type === 'rich') return h(RichTextDialog, { dialog, ctx })
1993
220
  return null
1994
221
  }
1995
-
1996
- function countReviewItemsByType(items, type) {
1997
- if (!type || !Array.isArray(items)) return 0
1998
- return items.filter((item) => item.type === type && !isPlaceholderReviewItem(item)).length
1999
- }
2000
-
2001
- function parseMaterializedId(id) {
2002
- return String(id || '').split(':').slice(1).join(':') || id
2003
- }
2004
-
2005
- function isPlaceholderReviewItem(item) {
2006
- const merged = readMerged(item || {})
2007
- if (!item || !item.type) return true
2008
- if (item.type === 'controlled_goods' && !merged.productName && !merged.hsCode && String(item.title || '').startsWith('管控商品文件:')) return true
2009
- if (item.type === 'supplier_product' && !merged.productName && !merged.supplierName && String(item.title || '').startsWith('供应商合同:')) return true
2010
- if (item.type === 'customs_workbook' && !merged.invoiceNo && !merged.contractNo && String(item.title || '').startsWith('购销合同:')) return true
2011
- return false
2012
- }
2013
-
2014
- function isSalesContractLineReviewItem(item) {
2015
- if (!item || item.type !== 'customs_workbook') return false
2016
- const merged = readMerged(item)
2017
- if (Array.isArray(merged.items) && merged.items.length > 0) return false
2018
- if (merged.invoiceNo || merged.buyerName || merged.sellerName || merged.currency) return false
2019
- const hasLineFields = merged.productName || merged.description || merged.model || merged.quantity || merged.unitPrice || merged.amount || merged.hsCode
2020
- return Boolean(hasLineFields && merged.contractNo)
2021
- }
2022
-
2023
- function groupBy(items, keyFn) {
2024
- return items.reduce((acc, item) => {
2025
- const key = keyFn(item)
2026
- acc[key] = acc[key] || []
2027
- acc[key].push(item)
2028
- return acc
2029
- }, {})
2030
- }
2031
-
2032
- function readMerged(item) {
2033
- return Object.assign({}, item && item.defaultData, item && item.extractedData, item && item.confirmedData)
2034
- }
2035
-
2036
- function value(input, fallback) {
2037
- if (input === 0) return '0'
2038
- return input == null || input === '' ? (fallback || '-') : String(input)
2039
- }
2040
-
2041
- function stringify(input) {
2042
- if (input == null || input === '') return '-'
2043
- if (typeof input === 'object') return JSON.stringify(input)
2044
- return String(input)
2045
- }
2046
-
2047
- function fieldLabel(key) {
2048
- return fieldLabels[key] || key
2049
- }
2050
-
2051
- function base64ToBytes(base64) {
2052
- const binary = atob(base64)
2053
- const bytes = new Uint8Array(binary.length)
2054
- for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index)
2055
- return bytes
2056
- }
2057
-
2058
- function escapeHtml(input) {
2059
- return String(input ?? '').replace(/[&<>"']/g, (char) => ({
2060
- '&': '&amp;',
2061
- '<': '&lt;',
2062
- '>': '&gt;',
2063
- '"': '&quot;',
2064
- "'": '&#39;'
2065
- }[char]))
2066
- }
2067
-
2068
- function reviewTypeText(type) {
2069
- if (type === 'controlled_goods') return '管控'
2070
- if (type === 'supplier_product') return '商品'
2071
- if (type === 'customs_workbook') return '发票'
2072
- return value(type)
2073
- }
2074
-
2075
- function reviewStatusText(statusText) {
2076
- if (statusText === 'confirmed') return '已确认'
2077
- if (statusText === 'rejected') return '已驳回'
2078
- if (statusText === 'needs_revision') return '需修订'
2079
- return '待审核'
2080
- }
2081
-
2082
- function controlStatusText(statusText) {
2083
- if (statusText === 'controlled') return '管控'
2084
- if (statusText === 'suspected') return '疑似管控'
2085
- if (statusText === 'not_controlled') return '非管控'
2086
- return '未检查'
2087
- }
2088
-
2089
- function controlStatusLevel(statusText) {
2090
- if (statusText === 'controlled') return 'danger'
2091
- if (statusText === 'suspected') return 'warn'
2092
- if (statusText === 'not_controlled') return 'ok'
2093
- return 'muted'
2094
- }
2095
-
2096
- function pageTitle(key) {
2097
- if (key === 'overview-page') return '总览'
2098
- if (key === 'controlled-goods-page') return '管控商品'
2099
- if (key === 'products-page') return '供应商商品'
2100
- if (key === 'hs-code-search-page') return '工具'
2101
- return '销售发票'
2102
- }
2103
-
2104
- function pageSubtitle(key) {
2105
- if (key === 'overview-page') return '按业务顺序聚合待审核事项,先确认基础资料,再生成销售发票。'
2106
- if (key === 'controlled-goods-page') return '上传管控目录、审核识别结果、维护正式管控商品库。'
2107
- if (key === 'products-page') return '上传供应商合同,审核商品、HS Code、退税率和管控状态。'
2108
- if (key === 'hs-code-search-page') return '输入商品名称或海关编码,查询 HS 编码网结果。'
2109
- return '上传购销合同,审核发票字段并生成固定模板 Excel。'
2110
- }
2111
-
2112
- function pageBreadcrumb(key) {
2113
- return 'Workspace'
2114
- }
2115
-
2116
- function isObject(value) {
2117
- return value && typeof value === 'object' && !Array.isArray(value)
2118
- }
2119
-
2120
- function injectStyles() {
2121
- const style = document.createElement('style')
2122
- style.textContent = `
2123
- :root { color-scheme: light; --tcw-bg:#f5f7fa; --tcw-card:#fff; --tcw-soft:#f8fafc; --tcw-text:#152033; --tcw-muted:#66758a; --tcw-border:#dbe3ed; --tcw-primary:#475569; --tcw-primary-dark:#334155; --tcw-success:#475569; --tcw-warning:#64748b; --tcw-danger:#9f3a38; }
2124
- * { box-sizing: border-box; }
2125
- 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; }
2126
- button, input, textarea, select { font: inherit; letter-spacing: 0; }
2127
- .tcw-shell { display: grid; grid-template-rows: auto auto auto 1fr; gap: 10px; min-height: 100vh; padding: 12px 8px 18px; }
2128
- .tcw-frame { width: 100%; max-width: none; margin: 0; justify-self: stretch; }
2129
- .tcw-header { display: grid; grid-template-columns: minmax(260px, 1fr) minmax(340px, 520px); gap: 12px; align-items: center; border: 1px solid var(--tcw-border); border-radius: 8px; background: var(--tcw-card); padding: 12px 14px; }
2130
- .tcw-title-block { display: grid; gap: 3px; }
2131
- .tcw-kicker { color: var(--tcw-primary); font-size: 12px; font-weight: 900; }
2132
- .tcw-title-block h1 { margin: 0; font-size: 20px; line-height: 1.22; }
2133
- .tcw-title-block p, .tcw-page-head p, .tcw-drawer-head p { margin: 0; color: var(--tcw-muted); font-size: 13px; }
2134
- .tcw-header-actions { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; }
2135
- .tcw-search, .tcw-field input, .tcw-field textarea, .tcw-field select { 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; }
2136
- .tcw-field textarea { min-height: 78px; resize: vertical; }
2137
- .tcw-btn, .tcw-upload { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: 36px; border: 1px solid var(--tcw-border); border-radius: 6px; background: var(--tcw-card); color: var(--tcw-text); padding: 8px 12px; cursor: pointer; white-space: nowrap; }
2138
- .tcw-btn:disabled, .tcw-upload:has(input:disabled) { cursor: not-allowed; opacity: .55; }
2139
- .tcw-btn-primary { border-color: var(--tcw-primary); background: var(--tcw-primary); color: #fff; }
2140
- .tcw-btn-primary:hover:not(:disabled) { background: var(--tcw-primary-dark); }
2141
- .tcw-btn-soft { background: var(--tcw-soft); }
2142
- .tcw-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); }
2143
- .tcw-wide { width: 100%; }
2144
- .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; }
2145
- .tcw-upload-input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
2146
- .tcw-tabs { display: flex; gap: 6px; overflow-x: auto; border-bottom: 1px solid var(--tcw-border); padding: 0 2px; }
2147
- .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; }
2148
- .tcw-tab.active { border-color: var(--tcw-primary); color: var(--tcw-primary); }
2149
- .tcw-main { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; align-items: start; }
2150
- .tcw-main.with-drawer { grid-template-columns: minmax(0, 1fr) minmax(340px, 410px); }
2151
- .tcw-content, .tcw-drawer, .tcw-panel, .tcw-supplier-block { border: 1px solid var(--tcw-border); border-radius: 8px; background: var(--tcw-card); }
2152
- .tcw-content { min-width: 0; border: 0; background: transparent; padding: 0; }
2153
- .tcw-page { display: grid; gap: 12px; width: 100%; }
2154
- .tcw-page-head, .tcw-drawer-head, .tcw-supplier-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
2155
- .tcw-page-head h2, .tcw-drawer-head h2, .tcw-panel h3 { margin: 0 0 4px; font-size: 16px; line-height: 1.35; }
2156
- .tcw-button-row { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; }
2157
- .tcw-panel { display: grid; gap: 10px; padding: 12px; }
2158
- .tcw-business-panel { display: grid; width: 100%; gap: 0; border: 1px solid var(--tcw-border); border-radius: 8px; background: var(--tcw-card); overflow: hidden; }
2159
- .tcw-business-actions { display: flex; align-items: center; justify-content: flex-end; gap: 10px; border-bottom: 1px solid var(--tcw-border); background: var(--tcw-card); padding: 16px 22px; }
2160
- .tcw-business-body { display: grid; gap: 12px; padding: 20px 28px 24px; }
2161
- .tcw-table-section { display: grid; gap: 0; }
2162
- .tcw-history-section { display: grid; gap: 10px; border-top: 1px solid var(--tcw-border); padding-top: 14px; }
2163
- .tcw-subsection-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
2164
- .tcw-subsection-head h3, .tcw-list-title h3 { margin: 0; font-size: 15px; line-height: 1.3; }
2165
- .tcw-subsection-head p, .tcw-list-title span { margin: 0; color: var(--tcw-muted); font-size: 12px; }
2166
- .tcw-metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
2167
- .tcw-metric {
2168
- display: grid;
2169
- grid-template-columns: 44px minmax(0, 1fr);
2170
- gap: 10px;
2171
- align-items: center;
2172
- border: 1px solid var(--tcw-border);
2173
- border-radius: 8px;
2174
- background: var(--tcw-card);
2175
- padding: 12px;
2176
- }
2177
- .tcw-metric-icon {
2178
- display: grid;
2179
- place-items: center;
2180
- width: 44px;
2181
- height: 44px;
2182
- border-radius: 10px;
2183
- font-size: 18px;
2184
- font-weight: 900;
2185
- }
2186
- .tcw-metric-icon img {
2187
- width: 22px;
2188
- height: 22px;
2189
- color: currentColor;
2190
- }
2191
- .tcw-metric-icon.blue { background: #eef2f7; color: var(--tcw-primary); }
2192
- .tcw-metric-icon.green { background: #eef2f7; color: var(--tcw-slate); }
2193
- .tcw-metric-icon.orange { background: #eef2f7; color: var(--tcw-slate); }
2194
- .tcw-metric-icon.violet { background: #eef2f7; color: var(--tcw-slate); }
2195
- .tcw-metric-icon.slate { background: #eef2f6; color: var(--tcw-slate); }
2196
- .tcw-metric-body { display: grid; gap: 2px; }
2197
- .tcw-metric-body span { color: var(--tcw-muted); font-size: 12px; }
2198
- .tcw-metric-body strong { font-size: 22px; line-height: 1; }
2199
- .tcw-field { display: grid; gap: 5px; min-width: 0; }
2200
- .tcw-field span { color: var(--tcw-muted); font-size: 12px; font-weight: 800; }
2201
- .tcw-field-check { align-content: end; }
2202
- .tcw-field-check input { width: 18px; height: 18px; min-height: 18px; padding: 0; }
2203
- .tcw-check { display: inline-flex; align-items: center; gap: 6px; min-height: 36px; color: var(--tcw-muted); font-size: 13px; }
2204
- .tcw-table-wrap { overflow: auto; border: 1px solid var(--tcw-border); border-radius: 8px; }
2205
- .tcw-table { width: 100%; min-width: 1120px; table-layout: fixed; border-collapse: collapse; font-size: 12px; }
2206
- .tcw-table th, .tcw-table td { min-width: 96px; border-bottom: 1px solid var(--tcw-border); padding: 10px; text-align: left; vertical-align: top; }
2207
- .tcw-table th { background: var(--tcw-soft); color: var(--tcw-muted); font-size: 11px; font-weight: 900; }
2208
- .tcw-table th.tcw-select-cell, .tcw-table td.tcw-select-cell, .tcw-table th.tcw-index-cell, .tcw-table td.tcw-index-cell { width: 48px; min-width: 48px; }
2209
- .tcw-table th.tcw-status-cell, .tcw-table td.tcw-status-cell { width: 108px; min-width: 108px; }
2210
- .tcw-table th.tcw-actions-cell, .tcw-table td.tcw-actions-cell { position: sticky; right: 0; z-index: 2; width: 220px; min-width: 220px; background: var(--tcw-card); box-shadow: -8px 0 14px rgba(15, 23, 42, .05); }
2211
- .tcw-table th.tcw-actions-cell { z-index: 3; background: var(--tcw-soft); }
2212
- .tcw-workbook-history-section .tcw-table { min-width: 680px; }
2213
- .tcw-workbook-history-section .tcw-table th.tcw-actions-cell, .tcw-workbook-history-section .tcw-table td.tcw-actions-cell { width: 112px; min-width: 112px; }
2214
- .tcw-workbook-history-section .tcw-cell-text { max-width: 180px; }
2215
- .tcw-cell-text { display: block; max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2216
- .tcw-text-cell .tcw-cell-text { max-width: 280px; }
2217
- .tcw-table tr:last-child td { border-bottom: 0; }
2218
- .tcw-table tr.clickable { cursor: pointer; }
2219
- .tcw-table tr.clickable:hover td { background: color-mix(in srgb, var(--tcw-primary) 5%, transparent); }
2220
- .tcw-list-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
2221
- .tcw-list-title { display: grid; gap: 2px; min-width: 160px; }
2222
- .tcw-list-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; }
2223
- .tcw-filter { display: inline-flex; align-items: center; gap: 8px; color: var(--tcw-muted); font-size: 13px; font-weight: 800; }
2224
- .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; }
2225
- .tcw-row-actions { display: flex; flex-wrap: nowrap; align-items: center; gap: 6px; width: max-content; white-space: nowrap; }
2226
- .tcw-mini-btn { display: inline-flex; align-items: center; justify-content: center; width: 46px; min-width: 46px; min-height: 28px; border: 1px solid var(--tcw-border); border-radius: 6px; background: var(--tcw-card); color: var(--tcw-text); padding: 4px 0; cursor: pointer; font-size: 12px; font-weight: 800; white-space: nowrap; }
2227
- .tcw-mini-btn.active { background: var(--tcw-soft); color: var(--tcw-text); }
2228
- .tcw-mini-btn.danger { border-color: color-mix(in srgb, var(--tcw-danger) 28%, var(--tcw-border)); background: var(--tcw-card); color: var(--tcw-danger); }
2229
- .tcw-mini-btn:disabled { cursor: not-allowed; opacity: .5; }
2230
- .tcw-hs-code-text { overflow-wrap: anywhere; font-size: 12px; }
2231
- .tcw-hs-search-form { display: grid; gap: 10px; }
2232
- .tcw-hs-search-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; }
2233
- .tcw-hs-candidate-panel { display: grid; gap: 10px; border-top: 1px solid var(--tcw-border); padding-top: 12px; }
2234
- .tcw-hs-candidate-search { display: grid; grid-template-columns: minmax(220px, 320px) auto; gap: 8px; align-items: center; }
2235
- .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; }
2236
- .row-selection-checkbox { width: 16px; height: 16px; }
2237
- .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; }
2238
- .tcw-pagination {
2239
- display: flex;
2240
- flex-wrap: wrap;
2241
- align-items: center;
2242
- gap: 6px;
2243
- border: 1px solid var(--tcw-border);
2244
- border-radius: 8px;
2245
- background: var(--tcw-card);
2246
- padding: 8px 10px;
2247
- color: var(--tcw-muted);
2248
- font-size: 12px;
2249
- }
2250
- .tcw-pagination-unified { justify-content: flex-start; }
2251
- .tcw-page-buttons { display: flex; gap: 8px; }
2252
- .tcw-pagination-total {
2253
- display: inline-flex;
2254
- align-items: center;
2255
- min-height: 28px;
2256
- color: var(--tcw-primary);
2257
- }
2258
- .tcw-page-btn {
2259
- display: inline-flex;
2260
- align-items: center;
2261
- justify-content: center;
2262
- min-width: 48px;
2263
- min-height: 28px;
2264
- border: 1px solid var(--tcw-border);
2265
- border-radius: 6px;
2266
- background: #fff;
2267
- color: var(--tcw-primary);
2268
- padding: 4px 9px;
2269
- cursor: pointer;
2270
- font-size: 12px;
2271
- font-weight: 700;
2272
- }
2273
- .tcw-page-btn:disabled {
2274
- cursor: not-allowed;
2275
- background: var(--tcw-soft);
2276
- color: #9aa7b8;
2277
- }
2278
- .tcw-page-current {
2279
- display: inline-flex;
2280
- align-items: center;
2281
- min-height: 28px;
2282
- border-radius: 6px;
2283
- background: var(--tcw-primary);
2284
- color: #fff;
2285
- padding: 4px 12px;
2286
- font-weight: 800;
2287
- }
2288
- .tcw-page-size-label {
2289
- color: var(--tcw-primary);
2290
- font-weight: 800;
2291
- }
2292
- .tcw-page-size-select,
2293
- .tcw-page-jump-input,
2294
- .tcw-page-size-fixed {
2295
- min-height: 28px;
2296
- border: 1px solid var(--tcw-border);
2297
- border-radius: 6px;
2298
- background: #fff;
2299
- color: var(--tcw-text);
2300
- padding: 4px 8px;
2301
- font-size: 12px;
2302
- }
2303
- .tcw-page-size-select { min-width: 92px; }
2304
- .tcw-page-size-fixed { display: inline-flex; align-items: center; }
2305
- .tcw-page-jump-input { width: 56px; }
2306
- .tcw-modal-backdrop { position: fixed; inset: 0; z-index: 10; display: grid; place-items: center; background: rgba(15, 23, 42, .32); padding: 20px; }
2307
- .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); }
2308
- .tcw-form-modal { width: min(960px, 100%); }
2309
- .tcw-form-modal-supplier {
2310
- width: min(1180px, calc(100vw - 96px));
2311
- max-height: min(760px, calc(100vh - 56px));
2312
- gap: 10px;
2313
- padding: 14px;
2314
- }
2315
- .tcw-confirm-modal { width: min(480px, 100%); }
2316
- .tcw-hs-detail-modal { width: min(980px, 100%); }
2317
- .tcw-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
2318
- .tcw-modal-head h2 { margin: 0; font-size: 18px; line-height: 1.3; }
2319
- .tcw-modal-subtitle { margin: 3px 0 0; color: var(--tcw-muted); font-size: 13px; font-weight: 800; }
2320
- .tcw-modal-actions { display: flex; justify-content: flex-end; gap: 8px; }
2321
- .tcw-edit-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
2322
- .tcw-edit-grid-two { grid-template-columns: repeat(2, minmax(260px, 1fr)); }
2323
- .tcw-field-wide { grid-column: 1 / -1; }
2324
- .tcw-form-modal-supplier .tcw-modal-head h2 { font-size: 17px; }
2325
- .tcw-form-modal-supplier .tcw-edit-grid-two {
2326
- grid-template-columns: repeat(3, minmax(0, 1fr));
2327
- gap: 8px 12px;
2328
- }
2329
- .tcw-form-modal-supplier .tcw-field { gap: 4px; }
2330
- .tcw-form-modal-supplier .tcw-field span { font-size: 11px; }
2331
- .tcw-form-modal-supplier .tcw-field input,
2332
- .tcw-form-modal-supplier .tcw-field select {
2333
- min-height: 32px;
2334
- padding: 6px 9px;
2335
- }
2336
- .tcw-form-modal-supplier .tcw-field textarea {
2337
- min-height: 58px;
2338
- padding: 7px 9px;
2339
- }
2340
- .tcw-form-modal-supplier .tcw-field-wide {
2341
- grid-column: span 3;
2342
- }
2343
- .tcw-form-modal-supplier .tcw-hs-candidate-panel {
2344
- display: grid;
2345
- grid-template-columns: minmax(0, 1fr) minmax(320px, .55fr);
2346
- gap: 10px;
2347
- align-items: start;
2348
- border-top: 1px solid var(--tcw-border);
2349
- padding-top: 10px;
2350
- }
2351
- .tcw-form-modal-supplier .tcw-hs-candidate-panel > .tcw-subsection-head {
2352
- align-items: center;
2353
- }
2354
- .tcw-form-modal-supplier .tcw-hs-candidate-panel > .tcw-table-wrap,
2355
- .tcw-form-modal-supplier .tcw-hs-candidate-panel > .tcw-empty,
2356
- .tcw-form-modal-supplier .tcw-hs-candidate-panel > .tcw-error {
2357
- grid-column: 1 / -1;
2358
- max-height: 210px;
2359
- overflow: auto;
2360
- }
2361
- .tcw-form-modal-supplier .tcw-hs-candidate-result {
2362
- display: grid;
2363
- gap: 8px;
2364
- grid-column: 1 / -1;
2365
- }
2366
- .tcw-form-modal-supplier .tcw-hs-candidate-result .tcw-table-wrap {
2367
- max-height: 210px;
2368
- }
2369
- .tcw-form-modal-supplier .tcw-hs-candidate-search {
2370
- grid-template-columns: minmax(0, 1fr) auto;
2371
- }
2372
- .tcw-form-modal-supplier .tcw-modal-actions {
2373
- position: sticky;
2374
- bottom: -14px;
2375
- border-top: 1px solid var(--tcw-border);
2376
- background: var(--tcw-card);
2377
- padding-top: 10px;
2378
- }
2379
- .tcw-confirm-text { margin: 0; color: var(--tcw-text); font-size: 14px; line-height: 1.7; }
2380
- .tcw-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
2381
- .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; }
2382
- .tcw-detail-field span { color: var(--tcw-muted); font-size: 12px; font-weight: 800; }
2383
- .tcw-detail-field strong { overflow-wrap: anywhere; font-size: 12px; line-height: 1.5; }
2384
- .tcw-detail-items { display: grid; gap: 8px; }
2385
- .tcw-detail-items h3 { margin: 0; font-size: 14px; }
2386
- .tcw-hs-detail-sections { display: grid; gap: 12px; }
2387
- .tcw-hs-detail-section { display: grid; gap: 8px; }
2388
- .tcw-hs-detail-section h3 { margin: 0; font-size: 15px; line-height: 1.35; }
2389
- .tcw-supplier-block { display: grid; gap: 10px; padding: 12px; }
2390
- .tcw-supplier-head strong { font-size: 15px; }
2391
- .tcw-supplier-head span { color: var(--tcw-muted); font-size: 12px; }
2392
- .tcw-drawer { position: sticky; top: 10px; display: grid; gap: 12px; padding: 14px; max-height: calc(100vh - 24px); overflow: auto; }
2393
- .tcw-icon-btn { display: grid; place-items: center; width: 30px; height: 30px; border: 1px solid var(--tcw-border); border-radius: 6px; background: var(--tcw-card); cursor: pointer; }
2394
- .tcw-compact-list { display: grid; gap: 7px; }
2395
- .tcw-compact-row { display: grid; grid-template-columns: 52px minmax(0, 1fr) auto; gap: 8px; align-items: center; width: 100%; border: 1px solid var(--tcw-border); border-radius: 6px; background: var(--tcw-soft); padding: 9px; cursor: pointer; text-align: left; }
2396
- .tcw-compact-row span { color: var(--tcw-primary); font-size: 12px; font-weight: 900; }
2397
- .tcw-compact-row strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
2398
- .tcw-compact-row em { color: var(--tcw-muted); font-size: 12px; font-style: normal; }
2399
- .tcw-compare { display: grid; gap: 6px; border-top: 1px solid var(--tcw-border); padding-top: 10px; }
2400
- .tcw-compare h3 { margin: 0; font-size: 13px; }
2401
- .tcw-kv { display: grid; grid-template-columns: minmax(92px, .34fr) minmax(0, 1fr); gap: 8px; border: 1px solid var(--tcw-border); border-radius: 6px; background: var(--tcw-soft); padding: 7px 8px; }
2402
- .tcw-kv span { color: var(--tcw-muted); font-size: 11px; }
2403
- .tcw-kv strong { overflow-wrap: anywhere; font-size: 12px; font-weight: 700; }
2404
- .tcw-evidence { display: grid; gap: 4px; 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) 6%, var(--tcw-card)); padding: 10px; }
2405
- .tcw-evidence span { color: var(--tcw-muted); font-size: 11px; font-weight: 900; }
2406
- .tcw-evidence strong { overflow-wrap: anywhere; font-size: 12px; }
2407
- .tcw-status { display: inline-flex; width: fit-content; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 900; }
2408
- .tcw-status.ok { background: #eef2f7; color: var(--tcw-success); }
2409
- .tcw-status.warn { background: #f1f5f9; color: var(--tcw-warning); }
2410
- .tcw-status.danger { background: #f8eeee; color: var(--tcw-danger); }
2411
- .tcw-status.muted { background: #eef2f6; color: var(--tcw-muted); }
2412
- @media (max-width: 1040px) {
2413
- .tcw-header, .tcw-main.with-drawer, .tcw-edit-grid { grid-template-columns: 1fr; }
2414
- .tcw-detail-grid { grid-template-columns: 1fr; }
2415
- .tcw-hs-candidate-search { grid-template-columns: 1fr; }
2416
- .tcw-business-actions { align-items: stretch; flex-direction: column; }
2417
- .tcw-button-row, .tcw-list-actions { justify-content: flex-start; }
2418
- .tcw-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2419
- .tcw-drawer { position: static; max-height: none; }
2420
- }
2421
-
2422
- /* V2 process-oriented shell */
2423
- :root {
2424
- --tcw-bg:#f6f8fb;
2425
- --tcw-card:#ffffff;
2426
- --tcw-soft:#f8fafc;
2427
- --tcw-text:#172033;
2428
- --tcw-muted:#65758c;
2429
- --tcw-border:#dbe5f0;
2430
- --tcw-primary:#475569;
2431
- --tcw-primary-dark:#334155;
2432
- --tcw-success:#475569;
2433
- --tcw-warning:#64748b;
2434
- --tcw-danger:#9f3a38;
2435
- --tcw-violet:#64748b;
2436
- --tcw-slate:#64748b;
2437
- --tcw-shadow:0 14px 42px rgba(20, 35, 55, .08);
2438
- }
2439
- body { background: var(--tcw-bg); letter-spacing: 0; }
2440
- .tcw-shell {
2441
- display: grid;
2442
- grid-template-columns: 248px minmax(0, 1fr);
2443
- grid-template-rows: 1fr;
2444
- gap: 0;
2445
- min-height: 100vh;
2446
- padding: 0;
2447
- background: var(--tcw-bg);
2448
- }
2449
- .tcw-sidebar {
2450
- display: grid;
2451
- grid-template-rows: auto auto auto 1fr;
2452
- align-content: start;
2453
- gap: 18px;
2454
- min-width: 0;
2455
- border-right: 1px solid var(--tcw-border);
2456
- background: var(--tcw-card);
2457
- padding: 20px 14px;
2458
- }
2459
- .tcw-brand {
2460
- display: flex;
2461
- align-items: center;
2462
- gap: 10px;
2463
- padding: 0 6px 6px;
2464
- }
2465
- .tcw-brand-mark {
2466
- display: grid;
2467
- place-items: center;
2468
- width: 34px;
2469
- height: 34px;
2470
- border-radius: 8px;
2471
- background: #eef2f7;
2472
- color: var(--tcw-primary);
2473
- font-size: 12px;
2474
- font-weight: 900;
2475
- }
2476
- .tcw-brand strong { display: block; font-size: 16px; line-height: 1.2; }
2477
- .tcw-brand span { display: block; color: var(--tcw-muted); font-size: 12px; }
2478
- .tcw-nav { display: grid; gap: 6px; }
2479
- .tcw-tab {
2480
- display: grid;
2481
- grid-template-columns: 26px minmax(0, 1fr) auto;
2482
- align-items: center;
2483
- gap: 8px;
2484
- min-height: 38px;
2485
- border: 1px solid transparent;
2486
- border-radius: 8px;
2487
- background: transparent;
2488
- color: var(--tcw-text);
2489
- padding: 7px 8px;
2490
- text-align: left;
2491
- font-weight: 800;
2492
- }
2493
- .tcw-tab.active {
2494
- border-color: #dbe5f0;
2495
- background: #f1f5f9;
2496
- color: var(--tcw-primary);
2497
- }
2498
- .tcw-overview-panel {
2499
- display: grid;
2500
- gap: 12px;
2501
- }
2502
- .tcw-overview-tabs {
2503
- display: inline-flex;
2504
- align-items: center;
2505
- gap: 0;
2506
- width: fit-content;
2507
- border: 1px solid var(--tcw-border);
2508
- border-radius: 12px;
2509
- overflow: hidden;
2510
- background: #fff;
2511
- }
2512
- .tcw-overview-tab {
2513
- min-height: 38px;
2514
- border: 0;
2515
- border-right: 1px solid var(--tcw-border);
2516
- background: #fff;
2517
- color: var(--tcw-muted);
2518
- padding: 8px 16px;
2519
- font-weight: 800;
2520
- }
2521
- .tcw-overview-tab:last-child { border-right: 0; }
2522
- .tcw-overview-tab.active {
2523
- background: #f1f5f9;
2524
- color: var(--tcw-primary);
2525
- }
2526
- .tcw-tab em {
2527
- min-width: 22px;
2528
- border-radius: 999px;
2529
- background: #edf2f7;
2530
- color: var(--tcw-muted);
2531
- padding: 2px 6px;
2532
- font-size: 11px;
2533
- font-style: normal;
2534
- text-align: center;
2535
- }
2536
- .tcw-nav-icon {
2537
- display: grid;
2538
- place-items: center;
2539
- width: 24px;
2540
- height: 24px;
2541
- border-radius: 7px;
2542
- font-size: 11px;
2543
- font-weight: 900;
2544
- }
2545
- .tcw-nav-icon img {
2546
- width: 16px;
2547
- height: 16px;
2548
- color: currentColor;
2549
- }
2550
- .tcw-nav-icon.blue { background: #eef2f7; color: var(--tcw-primary); }
2551
- .tcw-nav-icon.green { background: #eef2f7; color: var(--tcw-slate); }
2552
- .tcw-nav-icon.orange { background: #eef2f7; color: var(--tcw-slate); }
2553
- .tcw-nav-icon.violet { background: #eef2f7; color: var(--tcw-slate); }
2554
- .tcw-nav-icon.slate { background: #eef2f6; color: var(--tcw-slate); }
2555
- .tcw-workspace {
2556
- display: grid;
2557
- grid-template-rows: auto 1fr;
2558
- min-width: 0;
2559
- padding: 18px;
2560
- }
2561
- .tcw-header {
2562
- display: flex;
2563
- align-items: center;
2564
- justify-content: space-between;
2565
- gap: 14px;
2566
- min-height: 64px;
2567
- border: 0;
2568
- border-radius: 0;
2569
- background: transparent;
2570
- padding: 0 0 14px;
2571
- }
2572
- .tcw-kicker {
2573
- margin: 0 0 3px;
2574
- color: var(--tcw-muted);
2575
- font-size: 12px;
2576
- font-weight: 700;
2577
- }
2578
- .tcw-title-block h1 {
2579
- margin: 0;
2580
- font-size: 24px;
2581
- line-height: 1.2;
2582
- }
2583
- .tcw-title-block p {
2584
- margin: 4px 0 0;
2585
- color: var(--tcw-muted);
2586
- font-size: 13px;
2587
- }
2588
- .tcw-header-actions {
2589
- display: grid;
2590
- grid-template-columns: minmax(260px, 380px) auto;
2591
- gap: 8px;
2592
- align-items: center;
2593
- }
2594
- .tcw-main { display: grid; min-width: 0; }
2595
- .tcw-content { min-width: 0; border: 0; background: transparent; padding: 0; }
2596
- .tcw-business-panel, .tcw-panel {
2597
- border: 1px solid var(--tcw-border);
2598
- border-radius: 8px;
2599
- background: var(--tcw-card);
2600
- box-shadow: var(--tcw-shadow);
2601
- }
2602
- .tcw-business-actions { padding: 14px 22px; }
2603
- .tcw-business-body { padding: 18px 22px 22px; }
2604
- .tcw-metrics {
2605
- grid-template-columns: repeat(4, minmax(0, 1fr));
2606
- gap: 10px;
2607
- }
2608
- .tcw-metric {
2609
- border-color: var(--tcw-border);
2610
- background: var(--tcw-card);
2611
- box-shadow: var(--tcw-shadow);
2612
- }
2613
- .tcw-metric strong { color: var(--tcw-text); font-size: 24px; }
2614
- .tcw-btn, .tcw-upload, .tcw-mini-btn, .tcw-icon-btn {
2615
- border-radius: 7px;
2616
- }
2617
- .tcw-btn-primary {
2618
- border-color: var(--tcw-primary);
2619
- background: var(--tcw-primary);
2620
- }
2621
- .tcw-btn-primary:hover:not(:disabled) { background: var(--tcw-primary-dark); }
2622
- .tcw-upload {
2623
- border-color: var(--tcw-border);
2624
- background: var(--tcw-soft);
2625
- color: var(--tcw-primary);
2626
- }
2627
- .tcw-table-wrap {
2628
- border-color: var(--tcw-border);
2629
- border-radius: 8px;
2630
- }
2631
- .tcw-table th {
2632
- background: var(--tcw-soft);
2633
- color: var(--tcw-muted);
2634
- font-size: 11px;
2635
- }
2636
- .tcw-table td { background: var(--tcw-card); }
2637
- .tcw-table tr:hover td { background: #f8fafc; }
2638
- .tcw-table tr:hover td.tcw-actions-cell { background: var(--tcw-card); }
2639
- .tcw-status.ok { background: #eef2f7; color: var(--tcw-success); }
2640
- .tcw-status.warn { background: #f1f5f9; color: var(--tcw-warning); }
2641
- .tcw-status.danger { background: #f8eeee; color: var(--tcw-danger); }
2642
- .tcw-status.muted { background: #eef2f6; color: var(--tcw-muted); }
2643
- @media (max-width: 1280px) {
2644
- .tcw-shell { grid-template-columns: 230px minmax(0, 1fr); }
2645
- }
2646
- @media (max-width: 900px) {
2647
- .tcw-shell { grid-template-columns: 1fr; }
2648
- .tcw-sidebar {
2649
- position: static;
2650
- border-right: 0;
2651
- border-bottom: 1px solid var(--tcw-border);
2652
- }
2653
- .tcw-nav { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2654
- .tcw-workspace { padding: 14px; }
2655
- .tcw-header, .tcw-business-actions, .tcw-list-toolbar {
2656
- align-items: stretch;
2657
- flex-direction: column;
2658
- }
2659
- .tcw-header-actions { grid-template-columns: 1fr; }
2660
- .tcw-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2661
- }
2662
- @media (max-width: 560px) {
2663
- .tcw-nav { grid-template-columns: 1fr; }
2664
- .tcw-metrics, .tcw-edit-grid, .tcw-edit-grid-two { grid-template-columns: 1fr; }
2665
- }
222
+ function Modal({ title, children, footer, wide, full, onClose }) { return h('div', { className: 'modal-backdrop' }, h('div', { className: `modal ${wide ? 'wide' : ''} ${full ? 'full' : ''}` }, h('header', null, h('h2', null, title), h('button', { className: 'icon-button', title: '关闭', onClick: onClose }, '×')), h('div', { className: 'modal-body' }, children), footer && h('footer', null, footer))) }
223
+ function FormDialog({ dialog, ctx }) {
224
+ const initial = dialog.nestedValues ? dialog.values.values : dialog.values
225
+ const [values, setValues] = React.useState(Object.assign({}, initial))
226
+ async function save() { const missing = dialog.fields.find(field => field[3] && !String(values[field[0]] ?? '').trim()); if (missing) return ctx.show(`${missing[1]}不能为空`, 'error'); const input = dialog.nestedValues ? { settingsType: dialog.values.settingsType, values } : values; await ctx.run(dialog.actionKey, input); ctx.setDialog(null) }
227
+ return h(Modal, { title: dialog.title, wide: !!dialog.source, onClose: () => ctx.setDialog(null), footer: [h(Button, { key: 'cancel', tone: 'muted', onClick: () => ctx.setDialog(null) }, '取消'), h(Button, { key: 'save', onClick: save }, '保存')] }, h('div', { className: dialog.source ? 'review-split' : '' }, h('div', { className: 'form-grid' }, dialog.fields.map(field => h(FormField, { key: field[0], field, value: values[field[0]], onChange: value => setValues(Object.assign({}, values, { [field[0]]: value })) }))), dialog.source && h('div', { className: 'source-pane' }, dialog.source)))
228
+ }
229
+ function FormField({ field, value, onChange }) { const [name, label, type, required, options] = field; if (type === 'hidden') return null; return h('label', { className: type === 'textarea' ? 'span-2' : '' }, h('span', null, required && h('i', null, '*'), label), type === 'textarea' ? h('textarea', { value: value || '', onChange: event => onChange(event.target.value), rows: 4 }) : type === 'select' ? h('select', { value: value || '', onChange: event => onChange(event.target.value) }, (options || []).map(option => h('option', { key: option[0], value: option[0] }, option[1]))) : h('input', { type: type === 'number' ? 'number' : type === 'date' ? 'date' : 'text', step: type === 'number' ? 'any' : undefined, value: value ?? '', readOnly: type === 'readonly', onChange: event => onChange(type === 'number' ? event.target.value : event.target.value) })) }
230
+ function CatalogUploadDialog({ dialog, ctx }) { const [type, setType] = React.useState('DUAL_USE'); const [replace, setReplace] = React.useState(''); const input = React.useRef(); const batches = dialog.catalog === 'controlled' ? ctx.data.controlledBatches.items || [] : ctx.data.sanctionBatches.items || []; const actionKey = dialog.catalog === 'controlled' ? 'upload_controlled_catalog' : 'upload_sanction_catalog'; function selected(file) { if (!file) return; const upload = () => ctx.upload(actionKey, file, { catalogType: type, replaceBatchId: replace || undefined }); if (!replace) return upload(); const batch = batches.find(item => item.id === replace); ctx.setDialog({ type: 'confirm', title: '确认覆盖历史目录', message: `本次操作会逻辑删除“${batch?.sourceFileName || '所选历史文件'}”及其全部目录记录,再导入新文件。该操作可能影响现有风险匹配结果。`, danger: true, confirmText: '确认覆盖并上传', onConfirm: upload }) } return h(Modal, { title: '上传目录文件', onClose: () => ctx.setDialog(null), footer: [h(Button, { key: 'cancel', tone: 'muted', onClick: () => ctx.setDialog(null) }, '取消'), h(Button, { key: 'upload', onClick: () => input.current && input.current.click() }, '选择文件并上传')] }, h('div', { className: 'form-grid one' }, dialog.catalog === 'controlled' && h(FormField, { field: ['catalogType', '目录类型', 'select', true, [['DUAL_USE', '军民两用物资'], ['OVERSEAS_SANCTION', '海外制裁']]], value: type, onChange: setType }), h(FormField, { field: ['replaceBatchId', '覆盖历史文件(可选)', 'select', false, [['', '不覆盖,新增目录']].concat(batches.map(item => [item.id, item.sourceFileName]))], value: replace, onChange: setReplace }), replace && h('div', { className: 'danger-note' }, '选择文件后还需要进行二次风险确认。'), h('input', { ref: input, className: 'hidden-input', type: 'file', accept: '.pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.webp', onChange: event => selected(event.target.files[0]) }))) }
231
+ function FileUploadDialog({ dialog, ctx }) { const input = React.useRef(); return h(Modal, { title: dialog.title, onClose: () => ctx.setDialog(null), footer: [h(Button, { key: 'cancel', tone: 'muted', onClick: () => ctx.setDialog(null) }, '取消'), h(Button, { key: 'choose', onClick: () => input.current.click() }, '选择文件并上传')] }, h('div', { className: 'upload-zone', onClick: () => input.current.click() }, h('strong', null, '选择合同文件'), h('span', null, 'PDF、Word、Excel、PNG、JPG、WEBP,最大 20 MB')), h('input', { ref: input, className: 'hidden-input', type: 'file', accept: '.pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.webp', onChange: event => event.target.files[0] && ctx.upload(dialog.actionKey, event.target.files[0], {}) })) }
232
+ function ConfirmDialog({ dialog, ctx }) { return h(Modal, { title: dialog.title, onClose: () => ctx.setDialog(null), footer: [h(Button, { key: 'cancel', tone: 'muted', onClick: () => ctx.setDialog(null) }, '取消'), h(Button, { key: 'confirm', tone: dialog.danger ? 'danger' : '', onClick: async () => { await dialog.onConfirm(); if (ctx.dialog) ctx.setDialog(null) } }, dialog.confirmText || '确认')] }, h('div', { className: dialog.danger ? 'danger-note large' : 'confirm-message' }, dialog.message)) }
233
+ function DraftReviewDialog({ dialog, ctx }) { const task = dialog.task; const draft = task.resultPayload || {}; const isPurchase = task.documentType === 'PURCHASE_CONTRACT'; const [json, setJson] = React.useState(JSON.stringify(draft, null, 2)); async function save() { try { const parsed = JSON.parse(json); await ctx.run(isPurchase ? 'save_purchase_review' : 'save_customer_contract_review', { importTaskId: task.id, draft: parsed }); ctx.setDialog(null) } catch (error) { ctx.show('审核内容不是有效的结构化数据:' + messageOf(error), 'error') } } return h(Modal, { title: isPurchase ? '审核采购合同' : '审核客户合同', wide: true, onClose: () => ctx.setDialog(null), footer: [h(Button, { key: 'cancel', tone: 'muted', onClick: () => ctx.setDialog(null) }, '取消'), h(Button, { key: 'save', onClick: save }, '保存审核结果')] }, h('div', { className: 'review-split' }, h('div', null, h('h3', null, '识别内容'), h('textarea', { className: 'json-editor', value: json, onChange: event => setJson(event.target.value) })), h('div', { className: 'source-pane' }, h('h3', null, '源文件内容'), h('p', null, task.sourceFileName), h('p', null, '来源文件已保留,审核保存后直接入库。')))) }
234
+ function ContractDetailDialog({ dialog, ctx }) { const review = dialog.review || {}; const [customer, setCustomer] = React.useState(Object.assign({}, review.customer)); const [contract, setContract] = React.useState(Object.assign({}, review.contract)); const [clauses, setClauses] = React.useState((review.clauses || []).map(item => Object.assign({}, item))); async function save() { await ctx.run('update_customer_contract_review', { id: contract.id, customer, contract, clauses: clauses.map(item => ({ id: item.id, reviewedText: item.reviewedText })) }); ctx.setDialog(null) } return h(Modal, { title: '审核客户合同关键条款', wide: true, onClose: () => ctx.setDialog(null), footer: [h(Button, { key: 'cancel', tone: 'muted', onClick: () => ctx.setDialog(null) }, '取消'), h(Button, { key: 'save', onClick: save }, '保存审核结果')] }, h('div', { className: 'review-split' }, h('div', null, h('div', { className: 'form-grid' }, h(FormField, { field: ['name', '客户名称', 'text', true], value: customer.name, onChange: value => setCustomer(Object.assign({}, customer, { name: value })) }), h(FormField, { field: ['contractNo', '合同编码', 'readonly'], value: contract.contractNo, onChange: () => {} }), h(FormField, { field: ['contractAmountReviewCny', '合同金额(人民币)', 'number'], value: contract.contractAmountReviewCny, onChange: value => setContract(Object.assign({}, contract, { contractAmountReviewCny: value })) }), h(FormField, { field: ['signedAt', '签订日期', 'date'], value: contract.signedAt, onChange: value => setContract(Object.assign({}, contract, { signedAt: value })) })), h('div', { className: 'clause-list' }, clauses.map((clause, index) => { const analysis = (review.analyses || []).find(item => item.clauseType === clause.clauseType); return h('div', { key: clause.id, className: `clause ${String(analysis?.riskLevel || '').toLowerCase()}` }, h('b', null, clauseLabel(clause.clauseType)), h('textarea', { value: clause.reviewedText || '', rows: 4, onChange: event => { const next = clauses.slice(); next[index] = Object.assign({}, clause, { reviewedText: event.target.value }); setClauses(next) } }), analysis && h(React.Fragment, null, h('strong', null, analysis.riskMessageZh), h('small', null, analysis.suggestionZh))) }))), h('div', { className: 'source-pane' }, h('h3', null, '来源证据'), h(Button, { tone: 'muted', onClick: () => downloadBase64(ctx, 'download_source_file', { sourceFileId: contract.sourceFileId, fileName: contract.sourceFileName }) }, '下载源文件'), clauses.map(clause => h('div', { key: clause.id }, h('b', null, clause.sourceLocation || '未定位'), h('p', null, clause.sourceContent || clause.sourceText || '—')))))) }
235
+ function HsDetailDialog({ dialog, ctx }) { const rows = dialog.result?.items || dialog.result?.records || dialog.result?.data || (Array.isArray(dialog.result) ? dialog.result : []); return h(Modal, { title: `海关编码查询:${dialog.keywords}`, wide: true, onClose: () => ctx.setDialog(null), footer: h(Button, { onClick: () => ctx.setDialog(null) }, '关闭') }, Array.isArray(rows) && rows.length ? h(DataTable, { columns: [['code', '海关编码'], ['name', '商品名称', 'ellipsis'], ['englishName', '英文名称', 'ellipsis'], ['refundRate', '退税率']], rows }) : h('pre', { className: 'hs-result' }, JSON.stringify(dialog.result || {}, null, 2))) }
236
+ function HistoryDialog({ dialog, ctx }) { const row = dialog.row; const files = (ctx.data.generatedFiles.items || []).filter(file => file.customerContractId === row.customerContractId && file.purchaseOrderId === row.purchaseOrderId); return h(Modal, { title: `${row.salesOrderNo} / 历史生成`, wide: true, onClose: () => ctx.setDialog(null) }, h(DataTable, { columns: [['fileType', '文件类型', fileTypeLabel], ['documentNo', '文件编号'], ['fileName', '文件名称', 'ellipsis'], ['complianceProfitRate', '合规利润率', percent], ['exchangeCost', '换汇成本'], ['generatedAt', '生成时间', dateTime]], rows: files, actions: file => [h('button', { key: 'download', onClick: () => downloadFile(ctx, file) }, '下载'), h('button', { key: 'edit', onClick: () => editExcel(ctx, file) }, '审核'), h('button', { key: 'delete', className: 'danger-link', onClick: () => confirmDelete(ctx, 'generatedFile', file.id, file.fileName) }, '删除')] })) }
237
+ async function downloadFile(ctx, file) { const result = await ctx.run('download_sales_file', { id: file.id }, { silent: true, noReload: true }); const link = document.createElement('a'); link.href = `data:${result.mimeType};base64,${result.base64}`; link.download = result.fileName; link.click() }
238
+ async function downloadBase64(ctx, actionKey, input) { const result = await ctx.run(actionKey, input, { silent: true, noReload: true }); const link = document.createElement('a'); link.href = `data:${result.mimeType};base64,${result.base64}`; link.download = result.fileName; link.click() }
239
+ async function editExcel(ctx, file) { const result = await ctx.run('download_sales_file', { id: file.id }, { silent: true, noReload: true }); ctx.setDialog({ type: 'excel', file, base64: result.base64 }) }
240
+ function ExcelDialog({ dialog, ctx }) { const container = React.useRef(); const editor = React.useRef(); const [ready, setReady] = React.useState(false); React.useEffect(() => { let active = true; window.TradeComplianceExcelEditor.open({ container: container.current, base64: dialog.base64, onSave: base64 => ctx.run('save_edited_sales_file', { id: dialog.file.id, base64 }, { message: '工作簿已覆盖保存' }) }).then(instance => { if (active) { editor.current = instance; setReady(true) } else instance.dispose() }).catch(error => ctx.show('工作簿加载失败:' + messageOf(error), 'error')); return () => { active = false; editor.current && editor.current.dispose() } }, []); return h(Modal, { title: dialog.file.fileName, full: true, onClose: () => ctx.setDialog(null), footer: [h(Button, { key: 'close', tone: 'muted', onClick: () => ctx.setDialog(null) }, '关闭'), h(Button, { key: 'save', disabled: !ready, onClick: () => editor.current && editor.current.save() }, '保存并覆盖当前文件')] }, h('div', { ref: container, className: 'univer-container' }, !ready && h('div', { className: 'editor-loading' }, '正在加载工作簿...'))) }
241
+ function RichTextDialog({ dialog, ctx }) { const editor = React.useRef(); async function save() { const content = sanitizeRich(editor.current.innerHTML); await ctx.run('save_settings', { settingsType: 'risk', values: { content } }); ctx.setDialog(null) } function command(name) { document.execCommand(name, false); editor.current.focus() } return h(Modal, { title: dialog.title, wide: true, onClose: () => ctx.setDialog(null), footer: [h(Button, { key: 'cancel', tone: 'muted', onClick: () => ctx.setDialog(null) }, '取消'), h(Button, { key: 'save', onClick: save }, '保存')] }, h('div', { className: 'rich-toolbar' }, h('button', { onClick: () => command('formatBlock') }, '标题'), h('button', { onClick: () => command('bold') }, 'B'), h('button', { onClick: () => command('insertUnorderedList') }, '• 列表')), h('div', { ref: editor, className: 'rich-editor', contentEditable: true, dangerouslySetInnerHTML: { __html: sanitizeRich(dialog.values.content) } })) }
242
+
243
+ function confirmDelete(ctx, entityType, id, name) { ctx.setDialog({ type: 'confirm', title: '确认删除', message: `删除“${name}”后前端不提供恢复入口,后台将永久保留逻辑删除记录。`, danger: true, confirmText: '确认删除', onConfirm: () => ctx.run('delete_entity', { entityType, id }) }) }
244
+ function sourcePane(row) { return h(React.Fragment, null, h('h3', null, '来源证据(只读)'), h(KeyValues, { values: [['来源位置', row.sourceLocation], ['来源原文', row.sourceContent]] })) }
245
+ function partyFields(label) { return [['code', `${label}代码`], ['name', `${label}名称`, 'text', true], ['shortName', '简称'], ['address', '地址', 'textarea'], ['contactName', '联系人'], ['phone', '联系电话'], ['wechat', '微信'], ['email', '邮箱'], ['taxNumber', '税号'], ['paymentAccount', '收款账号'], ['bankName', '开户行'], ['taxpayerRating', '纳税等级'], ['website', '公司网站'], ['businessProducts', '经营产品', 'textarea']] }
246
+ function money(value) { const number = Number(value || 0); return Number.isFinite(number) ? `¥${number.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 })}` : '—' }
247
+ function percent(value) { const number = Number(value); return Number.isFinite(number) ? `${(number * 100).toFixed(2)}%` : '—' }
248
+ function sum(rows, key) { return (rows || []).reduce((total, row) => total + Number(row[key] || 0), 0) }
249
+ function dateTime(value) { return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '—' }
250
+ function orderKey(row) { return `${row.customerContractId}:${row.purchaseOrderId}` }
251
+ function topLabel(rows) { return (rows || []).slice(0, 3).map(row => row.name).join('、') || '—' }
252
+ function bankLabel(value) { return value === 'OVERSEAS_USD' ? '国外收款银行(美元)' : '国外收款银行(人民币)' }
253
+ function fileTypeLabel(value) { return value === 'CUSTOMS_DECLARATION' ? '报关单' : value }
254
+ function riskLabel(value) { const map = { HIGH: ['高风险', 'danger'], MEDIUM: ['中风险', 'warning'], LOW: ['低风险', 'success'] }; const item = map[value] || ['未分析', '']; return h(Status, { tone: item[1] }, item[0]) }
255
+ function taskStatus(value) { const labels = { QUEUED: '排队中', EXTRACTING: '提取中', AI_PARSING: 'AI解析中', VALIDATING: '校验中', AWAITING_REVIEW: '待审核', PERSISTING: '入库中', SUCCEEDED: '已完成', FAILED: '失败', TIMED_OUT: '已超时' }; return h(Status, { tone: value === 'FAILED' || value === 'TIMED_OUT' ? 'danger' : value === 'SUCCEEDED' ? 'success' : 'warning' }, labels[value] || value) }
256
+ function matchesLabel(value) { const labels = Array.from(new Set((value || []).map(item => item.catalogType === 'DUAL_USE' ? '军民两用物资' : '海外制裁'))); return labels.length ? labels.map(label => h(Status, { key: label, tone: 'danger' }, label)) : h(Status, null, '未命中') }
257
+ function clauseLabel(value) { return { CONTRACT_AMOUNT: '合同金额', TAX_RATE: '税率', PRICE_TERMS: '价格条款', TRANSPORT: '运输方式', WARRANTY: '质保条款', BREACH_LIABILITY: '违约责任', PAYMENT_TERMS: '付款条款' }[value] || value }
258
+ function sanitizeRich(value) { const template = document.createElement('template'); template.innerHTML = String(value || ''); template.content.querySelectorAll('*').forEach(node => { if (!['H1', 'H2', 'H3', 'P', 'STRONG', 'B', 'UL', 'OL', 'LI', 'BR'].includes(node.tagName)) node.replaceWith(...node.childNodes); else [...node.attributes].forEach(attribute => node.removeAttribute(attribute.name)) }); return template.innerHTML }
259
+
260
+ const style = document.createElement('style')
261
+ style.textContent = `
262
+ :root{--primary:#1456b8;--primary-soft:#edf4ff;--text:#172033;--muted:#687386;--border:#dfe5ee;--surface:#fff;--soft:#f6f8fb;--danger:#c73737;--warning:#9a6700;--success:#237548;font-family:Inter,"PingFang SC","Microsoft YaHei",Arial,sans-serif;color:var(--text);background:#f7f9fc}*{box-sizing:border-box}body{margin:0;background:#f7f9fc}button,input,select,textarea{font:inherit;letter-spacing:0}.tcw-shell{display:grid;grid-template-columns:248px minmax(0,1fr);min-height:100vh}.tcw-sidebar{position:sticky;top:0;height:100vh;background:#fff;border-right:1px solid var(--border);padding:24px 12px}.tcw-brand{display:flex;align-items:center;gap:10px;padding:0 10px 24px;font-size:19px}.tcw-shield{display:grid;place-items:center;width:30px;height:34px;color:#fff;background:var(--primary);clip-path:polygon(50% 0,100% 15%,90% 72%,50% 100%,10% 72%,0 15%);font-size:13px}.tcw-sidebar nav{display:grid;gap:4px}.tcw-sidebar nav button{display:flex;gap:12px;align-items:center;width:100%;height:46px;border:0;background:transparent;color:#3b4658;padding:0 14px;text-align:left;cursor:pointer}.tcw-sidebar nav button span{width:22px;text-align:center;color:#758195}.tcw-sidebar nav button.active{background:var(--primary-soft);color:var(--primary);font-weight:650;border-left:3px solid var(--primary)}.tcw-main{min-width:0;padding:0 28px 42px}.tcw-topbar{height:64px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border);color:var(--muted);font-size:13px}.tcw-topbar b{margin:0 7px;color:#adb5c2}.icon-button{border:0;background:transparent;color:#526074;font-size:22px;cursor:pointer}.tcw-page-title{display:flex;align-items:end;justify-content:space-between;gap:20px;padding:26px 0 18px}.tcw-page-title h1{margin:0;font-size:27px;line-height:1.2}.tcw-page-actions,.row-actions{display:flex;align-items:center;gap:8px;white-space:nowrap}.button{height:38px;padding:0 16px;border:1px solid var(--primary);background:var(--primary);color:#fff;border-radius:5px;cursor:pointer}.button.muted{background:#fff;border-color:#cbd3df;color:#344054}.button.danger{background:var(--danger);border-color:var(--danger)}.button:disabled{opacity:.5;cursor:not-allowed}.tcw-filterbar{display:flex;align-items:end;gap:12px;padding:16px 18px;background:#fff;border:1px solid var(--border);border-radius:6px;margin-bottom:14px}.tcw-filterbar label{display:grid;gap:7px;min-width:min(620px,75%);font-size:13px;font-weight:600}.tcw-filterbar input,.form-grid input,.form-grid select,.form-grid textarea{width:100%;border:1px solid #cfd7e4;border-radius:4px;padding:9px 11px;background:#fff;outline:none}.tcw-filterbar input:focus,.form-grid input:focus,.form-grid select:focus,.form-grid textarea:focus{border-color:var(--primary);box-shadow:0 0 0 2px #e5efff}.table-wrap{overflow-x:auto;background:#fff;border:1px solid var(--border);border-radius:6px}.table-wrap table{border-collapse:separate;border-spacing:0;width:100%;min-width:960px;font-size:13px}.table-wrap th,.table-wrap td{height:48px;padding:9px 13px;border-bottom:1px solid #edf0f4;text-align:left;vertical-align:middle;white-space:nowrap}.table-wrap th{background:#f6f8fb;color:#475467;font-weight:650;position:sticky;top:0;z-index:1}.table-wrap tr:last-child td{border-bottom:0}.table-wrap tbody tr:hover td{background:#fafcff}.table-wrap .ellipsis{max-width:220px;overflow:hidden;text-overflow:ellipsis}.table-wrap .action-cell{position:sticky;right:0;z-index:2;background:#fff;box-shadow:-5px 0 10px rgba(16,24,40,.04)}.table-wrap th.action-cell{background:#f6f8fb;z-index:3}.row-actions button,.cell-link,.back-link{border:0;background:transparent;color:var(--primary);padding:3px;cursor:pointer}.row-actions .danger-link{color:var(--danger)}.tcw-empty{padding:48px;text-align:center;color:var(--muted);background:#fff;border:1px solid var(--border)}.status{display:inline-flex;align-items:center;min-height:23px;padding:2px 7px;border-radius:4px;background:#f0f2f5;color:#596579;font-size:12px;margin-right:4px}.status.danger{background:#fff0f0;color:var(--danger)}.status.warning{background:#fff7df;color:var(--warning)}.status.success{background:#eaf8f0;color:var(--success)}.tcw-risk-band{display:grid;grid-template-columns:42px 1fr;gap:16px;background:#fff;border:1px solid #f0c8c8;border-left:4px solid var(--danger);padding:20px;margin-bottom:18px}.tcw-risk-band h3{margin:0 0 8px}.tcw-risk-band p,.tcw-risk-band ul{margin:6px 0;color:#4b5565;line-height:1.7}.risk-icon{display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:#fff0f0;color:var(--danger);font-weight:bold}.tcw-metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:14px 0}.tcw-metrics.compact{grid-template-columns:repeat(5,minmax(0,1fr))}.metric{display:grid;gap:8px;background:#fff;border:1px solid var(--border);padding:16px;min-height:82px}.metric span{color:var(--muted);font-size:12px}.metric strong{font-size:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tcw-process{display:flex;align-items:center;justify-content:center;gap:15px;background:#fff;border:1px solid var(--border);padding:28px;margin-top:18px;overflow-x:auto}.process-step{display:flex;align-items:center;gap:9px;min-width:130px}.process-step b{display:grid;place-items:center;width:28px;height:28px;border-radius:50%;background:var(--primary-soft);color:var(--primary)}.process-arrow{color:#98a2b3}.import-summary{margin-top:18px}.import-summary h3{font-size:15px}.tcw-breadcrumb{margin-bottom:8px}.tcw-toast{position:fixed;right:28px;top:24px;z-index:1001;padding:12px 16px;background:#eef8f2;border:1px solid #a8d5ba;color:var(--success);box-shadow:0 8px 24px rgba(16,24,40,.12)}.tcw-toast.error{background:#fff2f2;border-color:#e7b1b1;color:var(--danger)}.tcw-busy{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;gap:10px;background:rgba(255,255,255,.64);font-weight:600}.tcw-spinner{width:22px;height:22px;border:3px solid #cdd9ea;border-top-color:var(--primary);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.modal-backdrop{position:fixed;inset:0;z-index:900;background:rgba(20,30,48,.48);display:grid;place-items:center;padding:24px}.modal{width:min(620px,96vw);max-height:92vh;background:#fff;border-radius:6px;box-shadow:0 24px 80px rgba(15,23,42,.25);display:grid;grid-template-rows:auto minmax(0,1fr) auto;overflow:hidden}.modal.wide{width:min(1120px,96vw)}.modal.full{width:calc(100vw - 24px);height:calc(100vh - 24px);max-height:none}.modal>header,.modal>footer{display:flex;align-items:center;justify-content:space-between;padding:15px 20px;border-bottom:1px solid var(--border)}.modal>footer{justify-content:flex-end;gap:9px;border-top:1px solid var(--border);border-bottom:0}.modal h2{font-size:19px;margin:0}.modal-body{padding:20px;overflow:auto}.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.form-grid.one{grid-template-columns:1fr}.form-grid label{display:grid;gap:6px;font-size:13px;font-weight:600}.form-grid label.span-2{grid-column:1/-1}.form-grid label i{color:var(--danger);font-style:normal;margin-right:3px}.review-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:22px}.source-pane{border-left:1px solid var(--border);padding-left:22px;color:#4b5565}.source-pane p{white-space:pre-wrap;line-height:1.6}.danger-note{padding:12px;border:1px solid #efb1b1;background:#fff4f4;color:var(--danger);grid-column:1/-1}.danger-note.large,.confirm-message{font-size:15px;line-height:1.7}.upload-zone{display:grid;place-items:center;gap:8px;padding:44px;border:1px dashed #aeb9ca;background:#f8faff;cursor:pointer}.upload-zone span{color:var(--muted);font-size:13px}.hidden-input{display:none}.json-editor{width:100%;height:470px;border:1px solid var(--border);padding:12px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}.key-values{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1px;background:var(--border);border:1px solid var(--border)}.key-values>div{display:grid;gap:5px;padding:12px;background:#fff}.key-values span{font-size:12px;color:var(--muted)}.clause-list{display:grid;gap:10px;margin-top:14px}.clause{border:1px solid var(--border);border-left:3px solid #a8b2c1;padding:12px}.clause.high{border-left-color:var(--danger)}.clause.medium{border-left-color:#d79a19}.clause p{white-space:pre-wrap}.clause strong,.clause small{display:block;margin-top:7px}.clause small{color:var(--muted)}.tcw-tabs{display:flex;border-bottom:1px solid var(--border);margin-bottom:14px}.tcw-tabs button{border:0;background:transparent;padding:12px 18px;color:var(--muted)}.tcw-tabs button.active{color:var(--primary);border-bottom:2px solid var(--primary)}.tcw-analysis-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}.ranking{background:#fff;border:1px solid var(--border);padding:18px}.ranking h3{margin:0 0 12px}.ranking>div{display:grid;grid-template-columns:28px 1fr auto;gap:8px;padding:10px 0;border-top:1px solid #edf0f4}.ranking>div span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.settings-stack{display:grid;gap:14px}.setting-block{background:#fff;border:1px solid var(--border);padding:18px}.setting-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}.setting-head h2{font-size:17px;margin:0}.rich-preview{line-height:1.65;color:#475467}.rich-toolbar{display:flex;gap:7px;padding:8px;border:1px solid var(--border);border-bottom:0}.rich-toolbar button{min-width:34px;height:30px;border:1px solid #ccd4df;background:#fff}.rich-editor{min-height:360px;padding:15px;border:1px solid var(--border);outline:none}.univer-container{height:100%;min-height:620px;position:relative}.editor-loading{position:absolute;inset:0;display:grid;place-items:center;color:var(--muted)}
263
+ @media(max-width:1000px){.tcw-shell{grid-template-columns:82px minmax(0,1fr)}.tcw-brand strong,.tcw-sidebar nav button:not(.active){font-size:0}.tcw-sidebar nav button{justify-content:center}.tcw-sidebar nav button span{font-size:17px}.tcw-metrics,.tcw-metrics.compact{grid-template-columns:repeat(2,minmax(0,1fr))}.review-split{grid-template-columns:1fr}.source-pane{border-left:0;border-top:1px solid var(--border);padding:18px 0 0}.tcw-analysis-grid{grid-template-columns:1fr}}@media(max-width:640px){.tcw-shell{display:block}.tcw-sidebar{position:static;width:100%;height:auto}.tcw-sidebar nav{grid-template-columns:repeat(3,1fr)}.tcw-sidebar nav button{font-size:11px!important}.tcw-main{padding:0 12px 30px}.tcw-page-title{align-items:stretch;flex-direction:column}.form-grid,.key-values{grid-template-columns:1fr}.tcw-metrics,.tcw-metrics.compact{grid-template-columns:1fr}.tcw-filterbar{align-items:stretch;flex-direction:column}.tcw-filterbar label{min-width:0}.modal-backdrop{padding:6px}.modal{max-height:98vh}.tcw-process{justify-content:flex-start}}
2666
264
  `
2667
- document.head.appendChild(style)
2668
- }
265
+ document.head.appendChild(style)
266
+ style.textContent += `.clause textarea{width:100%;margin-top:9px;padding:9px 10px;border:1px solid #cfd7e4;border-radius:4px;resize:vertical}.hs-result{max-height:560px;overflow:auto;white-space:pre-wrap;background:var(--soft);border:1px solid var(--border);padding:14px}`
2669
267
 
2670
- window.addEventListener('message', (event) => {
268
+ window.addEventListener('message', event => {
2671
269
  const message = event.data
2672
- if (!isObject(message) || message.channel !== CHANNEL || message.protocolVersion !== VERSION) return
2673
- if (message.type === 'init') {
2674
- instanceId = message.instanceId
2675
- if (window.XpertRemoteUI && typeof window.XpertRemoteUI.applyTheme === 'function') window.XpertRemoteUI.applyTheme(message.theme)
2676
- if (window.__tradeComplianceSetContext) {
2677
- window.__tradeComplianceSetContext({ manifest: message.manifest, payload: message.payload, initialQuery: message.initialQuery || {}, locale: message.locale, theme: message.theme })
2678
- }
2679
- setTimeout(reportResize, 0)
2680
- return
2681
- }
270
+ if (!message || message.channel !== CHANNEL || message.protocolVersion !== VERSION) return
271
+ if (message.type === 'init') { instanceId = message.instanceId; if (window.XpertRemoteUI && window.XpertRemoteUI.applyTheme) window.XpertRemoteUI.applyTheme(message.theme); window.__tcwInit && window.__tcwInit({ manifest: message.manifest, payload: message.payload, initialQuery: message.initialQuery || {}, locale: message.locale, theme: message.theme }); setTimeout(reportResize, 0); return }
2682
272
  if (message.instanceId !== instanceId) return
2683
- if (message.type === 'hostEvent') {
2684
- if (window.__tradeComplianceHostEvent) window.__tradeComplianceHostEvent(message.event)
2685
- return
2686
- }
2687
- if (message.requestId && pending.has(message.requestId)) {
2688
- const item = pending.get(message.requestId)
2689
- pending.delete(message.requestId)
2690
- if (message.type === 'error') item.reject(new Error(message.message || '远程请求失败'))
2691
- else item.resolve(message)
2692
- }
273
+ if (message.type === 'hostEvent') { window.__tcwHostEvent && window.__tcwHostEvent(message.event); return }
274
+ if (message.requestId && pending.has(message.requestId)) { const item = pending.get(message.requestId); pending.delete(message.requestId); if (message.type === 'error') item.reject(new Error(message.message || '远程请求失败')); else item.resolve(message) }
2693
275
  })
2694
-
2695
276
  ReactDOM.render(h(App), document.getElementById('root'))
2696
277
  })()