@chenchaolong/plugin-trade-compliance-workbench 1.0.1 → 1.0.3
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.
- package/README.md +46 -1
- package/dist/assets/branding/yuneec-logo.png +0 -0
- package/dist/assets/templates/CI.xlsx +0 -0
- package/dist/assets/templates/CUSTOMS_DECLARATION.xlsx +0 -0
- package/dist/assets/templates/PI.xlsx +0 -0
- package/dist/assets/templates/PL.xlsx +0 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/adapters/file-storage.d.ts +4 -2
- package/dist/lib/adapters/file-storage.d.ts.map +1 -1
- package/dist/lib/adapters/file-storage.js +25 -10
- package/dist/lib/adapters/file-storage.js.map +1 -1
- package/dist/lib/adapters/hsbianma.provider.d.ts +1 -0
- package/dist/lib/adapters/hsbianma.provider.d.ts.map +1 -1
- package/dist/lib/adapters/hsbianma.provider.js +32 -3
- package/dist/lib/adapters/hsbianma.provider.js.map +1 -1
- package/dist/lib/adapters/translation.provider.d.ts +3 -0
- package/dist/lib/adapters/translation.provider.d.ts.map +1 -1
- package/dist/lib/adapters/translation.provider.js +15 -6
- package/dist/lib/adapters/translation.provider.js.map +1 -1
- package/dist/lib/constants.d.ts +1 -0
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +1 -0
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/demo-data.definitions.d.ts +28 -0
- package/dist/lib/demo-data.definitions.d.ts.map +1 -0
- package/dist/lib/demo-data.definitions.js +236 -0
- package/dist/lib/demo-data.definitions.js.map +1 -0
- package/dist/lib/demo-data.entity.d.ts +9 -0
- package/dist/lib/demo-data.entity.d.ts.map +1 -0
- package/dist/lib/demo-data.entity.js +24 -0
- package/dist/lib/demo-data.entity.js.map +1 -0
- package/dist/lib/demo-data.ids.d.ts +4 -0
- package/dist/lib/demo-data.ids.d.ts.map +1 -0
- package/dist/lib/demo-data.ids.js +19 -0
- package/dist/lib/demo-data.ids.js.map +1 -0
- package/dist/lib/demo-data.service.d.ts +26 -0
- package/dist/lib/demo-data.service.d.ts.map +1 -0
- package/dist/lib/demo-data.service.js +124 -0
- package/dist/lib/demo-data.service.js.map +1 -0
- package/dist/lib/domain/default-settings.d.ts +1 -0
- package/dist/lib/domain/default-settings.d.ts.map +1 -1
- package/dist/lib/domain/default-settings.js +1 -0
- package/dist/lib/domain/default-settings.js.map +1 -1
- package/dist/lib/domain/risk-notice.d.ts +59 -0
- package/dist/lib/domain/risk-notice.d.ts.map +1 -0
- package/dist/lib/domain/risk-notice.js +30 -0
- package/dist/lib/domain/risk-notice.js.map +1 -0
- package/dist/lib/entities/catalog.entity.d.ts.map +1 -1
- package/dist/lib/entities/catalog.entity.js +4 -2
- package/dist/lib/entities/catalog.entity.js.map +1 -1
- package/dist/lib/entities/customer.entity.d.ts +9 -2
- package/dist/lib/entities/customer.entity.d.ts.map +1 -1
- package/dist/lib/entities/customer.entity.js +29 -1
- package/dist/lib/entities/customer.entity.js.map +1 -1
- package/dist/lib/entities/index.d.ts +2 -2
- package/dist/lib/entities/index.d.ts.map +1 -1
- package/dist/lib/entities/index.js +2 -2
- package/dist/lib/entities/index.js.map +1 -1
- package/dist/lib/entities/procurement.entity.d.ts +6 -4
- package/dist/lib/entities/procurement.entity.d.ts.map +1 -1
- package/dist/lib/entities/procurement.entity.js +9 -1
- package/dist/lib/entities/procurement.entity.js.map +1 -1
- package/dist/lib/entities/settings.entity.d.ts +12 -2
- package/dist/lib/entities/settings.entity.d.ts.map +1 -1
- package/dist/lib/entities/settings.entity.js +40 -0
- package/dist/lib/entities/settings.entity.js.map +1 -1
- package/dist/lib/entities/shared.entity.d.ts +10 -5
- package/dist/lib/entities/shared.entity.d.ts.map +1 -1
- package/dist/lib/entities/shared.entity.js +22 -1
- package/dist/lib/entities/shared.entity.js.map +1 -1
- package/dist/lib/remote-components/trade-compliance-workbench/app.css +1 -0
- package/dist/lib/remote-components/trade-compliance-workbench/app.js +63 -277
- package/dist/lib/remote-components/trade-compliance-workbench/editor-source.mjs +2 -1
- package/dist/lib/remote-components/trade-compliance-workbench/editor.js +171 -128
- package/dist/lib/remote-ui/analytics-contracts.d.ts +106 -0
- package/dist/lib/remote-ui/analytics-contracts.d.ts.map +1 -0
- package/dist/lib/remote-ui/analytics-contracts.js +4 -0
- package/dist/lib/remote-ui/analytics-contracts.js.map +1 -0
- package/dist/lib/remote-ui/app-source.d.ts +2 -0
- package/dist/lib/remote-ui/app-source.d.ts.map +1 -0
- package/dist/lib/remote-ui/app-source.js +796 -0
- package/dist/lib/remote-ui/app-source.js.map +1 -0
- package/dist/lib/remote-ui/bridge.d.ts +36 -0
- package/dist/lib/remote-ui/bridge.d.ts.map +1 -0
- package/dist/lib/remote-ui/bridge.js +159 -0
- package/dist/lib/remote-ui/bridge.js.map +1 -0
- package/dist/lib/remote-ui/components/data-grid.d.ts +34 -0
- package/dist/lib/remote-ui/components/data-grid.d.ts.map +1 -0
- package/dist/lib/remote-ui/components/data-grid.js +59 -0
- package/dist/lib/remote-ui/components/data-grid.js.map +1 -0
- package/dist/lib/remote-ui/components/forms.d.ts +63 -0
- package/dist/lib/remote-ui/components/forms.d.ts.map +1 -0
- package/dist/lib/remote-ui/components/forms.js +117 -0
- package/dist/lib/remote-ui/components/forms.js.map +1 -0
- package/dist/lib/remote-ui/components/layout.d.ts +25 -0
- package/dist/lib/remote-ui/components/layout.d.ts.map +1 -0
- package/dist/lib/remote-ui/components/layout.js +66 -0
- package/dist/lib/remote-ui/components/layout.js.map +1 -0
- package/dist/lib/remote-ui/contract-import.d.ts +82 -0
- package/dist/lib/remote-ui/contract-import.d.ts.map +1 -0
- package/dist/lib/remote-ui/contract-import.js +102 -0
- package/dist/lib/remote-ui/contract-import.js.map +1 -0
- package/dist/lib/remote-ui/contracts.d.ts +257 -0
- package/dist/lib/remote-ui/contracts.d.ts.map +1 -0
- package/dist/lib/remote-ui/contracts.js +12 -0
- package/dist/lib/remote-ui/contracts.js.map +1 -0
- package/dist/lib/remote-ui/customer-contract-contracts.d.ts +227 -0
- package/dist/lib/remote-ui/customer-contract-contracts.d.ts.map +1 -0
- package/dist/lib/remote-ui/customer-contract-contracts.js +10 -0
- package/dist/lib/remote-ui/customer-contract-contracts.js.map +1 -0
- package/dist/lib/remote-ui/model.d.ts +31 -0
- package/dist/lib/remote-ui/model.d.ts.map +1 -0
- package/dist/lib/remote-ui/model.js +248 -0
- package/dist/lib/remote-ui/model.js.map +1 -0
- package/dist/lib/remote-ui/pages/analytics.d.ts +12 -0
- package/dist/lib/remote-ui/pages/analytics.d.ts.map +1 -0
- package/dist/lib/remote-ui/pages/analytics.js +123 -0
- package/dist/lib/remote-ui/pages/analytics.js.map +1 -0
- package/dist/lib/remote-ui/pages/catalogs.d.ts +225 -0
- package/dist/lib/remote-ui/pages/catalogs.d.ts.map +1 -0
- package/dist/lib/remote-ui/pages/catalogs.js +572 -0
- package/dist/lib/remote-ui/pages/catalogs.js.map +1 -0
- package/dist/lib/remote-ui/pages/contracts.d.ts +80 -0
- package/dist/lib/remote-ui/pages/contracts.d.ts.map +1 -0
- package/dist/lib/remote-ui/pages/contracts.js +396 -0
- package/dist/lib/remote-ui/pages/contracts.js.map +1 -0
- package/dist/lib/remote-ui/pages/files.d.ts +31 -0
- package/dist/lib/remote-ui/pages/files.d.ts.map +1 -0
- package/dist/lib/remote-ui/pages/files.js +64 -0
- package/dist/lib/remote-ui/pages/files.js.map +1 -0
- package/dist/lib/remote-ui/pages/home.d.ts +71 -0
- package/dist/lib/remote-ui/pages/home.d.ts.map +1 -0
- package/dist/lib/remote-ui/pages/home.js +136 -0
- package/dist/lib/remote-ui/pages/home.js.map +1 -0
- package/dist/lib/remote-ui/pages/procurement.d.ts +36 -0
- package/dist/lib/remote-ui/pages/procurement.d.ts.map +1 -0
- package/dist/lib/remote-ui/pages/procurement.js +370 -0
- package/dist/lib/remote-ui/pages/procurement.js.map +1 -0
- package/dist/lib/remote-ui/pages/sales.d.ts +12 -0
- package/dist/lib/remote-ui/pages/sales.d.ts.map +1 -0
- package/dist/lib/remote-ui/pages/sales.js +106 -0
- package/dist/lib/remote-ui/pages/sales.js.map +1 -0
- package/dist/lib/remote-ui/pages/settings.d.ts +34 -0
- package/dist/lib/remote-ui/pages/settings.d.ts.map +1 -0
- package/dist/lib/remote-ui/pages/settings.js +259 -0
- package/dist/lib/remote-ui/pages/settings.js.map +1 -0
- package/dist/lib/remote-ui/procurement-contracts.d.ts +177 -0
- package/dist/lib/remote-ui/procurement-contracts.d.ts.map +1 -0
- package/dist/lib/remote-ui/procurement-contracts.js +2 -0
- package/dist/lib/remote-ui/procurement-contracts.js.map +1 -0
- package/dist/lib/remote-ui/sales-contracts.d.ts +133 -0
- package/dist/lib/remote-ui/sales-contracts.d.ts.map +1 -0
- package/dist/lib/remote-ui/sales-contracts.js +2 -0
- package/dist/lib/remote-ui/sales-contracts.js.map +1 -0
- package/dist/lib/remote-ui/settings-contracts.d.ts +114 -0
- package/dist/lib/remote-ui/settings-contracts.d.ts.map +1 -0
- package/dist/lib/remote-ui/settings-contracts.js +6 -0
- package/dist/lib/remote-ui/settings-contracts.js.map +1 -0
- package/dist/lib/remote-ui/upload-policy.d.ts +18 -0
- package/dist/lib/remote-ui/upload-policy.d.ts.map +1 -0
- package/dist/lib/remote-ui/upload-policy.js +56 -0
- package/dist/lib/remote-ui/upload-policy.js.map +1 -0
- package/dist/lib/runtime-validation.d.ts +61 -0
- package/dist/lib/runtime-validation.d.ts.map +1 -0
- package/dist/lib/runtime-validation.js +243 -0
- package/dist/lib/runtime-validation.js.map +1 -0
- package/dist/lib/sales-file.service.d.ts +54 -6
- package/dist/lib/sales-file.service.d.ts.map +1 -1
- package/dist/lib/sales-file.service.js +278 -33
- package/dist/lib/sales-file.service.js.map +1 -1
- package/dist/lib/trade-compliance.config.d.ts +32 -0
- package/dist/lib/trade-compliance.config.d.ts.map +1 -1
- package/dist/lib/trade-compliance.config.js +16 -4
- package/dist/lib/trade-compliance.config.js.map +1 -1
- package/dist/lib/types.d.ts +253 -1
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +24 -0
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/workbench-view.provider.d.ts +675 -38
- package/dist/lib/workbench-view.provider.d.ts.map +1 -1
- package/dist/lib/workbench-view.provider.js +418 -42
- package/dist/lib/workbench-view.provider.js.map +1 -1
- package/dist/lib/workbench.middleware.d.ts +70 -19
- package/dist/lib/workbench.middleware.d.ts.map +1 -1
- package/dist/lib/workbench.middleware.js +5 -1
- package/dist/lib/workbench.middleware.js.map +1 -1
- package/dist/lib/workbench.plugin.d.ts.map +1 -1
- package/dist/lib/workbench.plugin.js +6 -4
- package/dist/lib/workbench.plugin.js.map +1 -1
- package/dist/lib/workbench.service.d.ts +629 -48
- package/dist/lib/workbench.service.d.ts.map +1 -1
- package/dist/lib/workbench.service.js +1818 -300
- package/dist/lib/workbench.service.js.map +1 -1
- package/package.json +15 -4
- package/scripts/seed-demo-data.mjs +138 -0
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { createRemoteBridge } from './bridge.js';
|
|
5
|
+
import { createBlobUrlOwner, createLatestCommittedRequest, createLatestRequestGate, normalizeWorkbenchData, routeDataRequest, unwrapResponse } from './model.js';
|
|
6
|
+
import { InfoNotice } from './components/data-grid.js';
|
|
7
|
+
import { LoadingBlocker, Toast } from './components/forms.js';
|
|
8
|
+
import { buildBreadcrumbs, WorkbenchShell } from './components/layout.js';
|
|
9
|
+
import { CatalogCommands, CatalogImportError, ControlledGoodsPage, findActiveCatalogImportTask, findLatestFailedCatalogImportTask, runCatalogImportOrchestration, SanctionedCompaniesPage } from './pages/catalogs.js';
|
|
10
|
+
import { HomePage } from './pages/home.js';
|
|
11
|
+
import { ProcurementPage } from './pages/procurement.js';
|
|
12
|
+
import { CustomerContractsPage, adaptCustomerContractReview } from './pages/contracts.js';
|
|
13
|
+
import { SalesPage } from './pages/sales.js';
|
|
14
|
+
import { SalesFilesPage } from './pages/files.js';
|
|
15
|
+
import { AnalyticsPage } from './pages/analytics.js';
|
|
16
|
+
import { SettingsPage } from './pages/settings.js';
|
|
17
|
+
import { ContractImportError, findRecoverableContractImport, findResumableContractImport, runContractImportOrchestration, runCustomerContractReanalysis } from './contract-import.js';
|
|
18
|
+
import './styles.css';
|
|
19
|
+
const pageMeta = {
|
|
20
|
+
home: { title: '首页', subtitle: '查看系统风险操作提示与外贸合规业务流程。' },
|
|
21
|
+
'controlled-goods': { title: '管控商品目录', subtitle: '维护军民两用物资和海外制裁商品目录。' },
|
|
22
|
+
'sanctioned-companies': { title: '公司制裁目录', subtitle: '维护被制裁公司信息并支持供应商、客户风险校验。' },
|
|
23
|
+
procurement: { title: '采购合规管理', subtitle: '管理供应商、采购订单及采购商品合规信息。' },
|
|
24
|
+
'customer-contract-review': { title: '客户合同审核', subtitle: '审核客户合同关键条款及 AI 风险建议。' },
|
|
25
|
+
sales: { title: '销售合规管理', subtitle: '查看客户、销售订单及派生销售商品。' },
|
|
26
|
+
'sales-files': { title: '销售文件管理', subtitle: '校验并生成销售业务文件,管理历史工作簿。' },
|
|
27
|
+
analytics: { title: '数据分析', subtitle: '按采购和销售维度实时统计订单数据。' },
|
|
28
|
+
settings: { title: '系统设置', subtitle: '维护企业、银行、汇率、风险提示和利润规则。' }
|
|
29
|
+
};
|
|
30
|
+
const bridge = createRemoteBridge();
|
|
31
|
+
const initialAnalyticsQuery = { mode: 'PURCHASE', dimension: 'SUPPLIER', filters: {}, page: 1, pageSize: 20 };
|
|
32
|
+
let excelEditorRuntimePromise = null;
|
|
33
|
+
function App() {
|
|
34
|
+
const [route, setRoute] = React.useState({ type: 'page', page: 'home' });
|
|
35
|
+
const routeRef = React.useRef(route);
|
|
36
|
+
const contextRef = React.useRef(null);
|
|
37
|
+
const requestGateRef = React.useRef(createLatestRequestGate());
|
|
38
|
+
const routeRequestGateRef = React.useRef(createLatestRequestGate());
|
|
39
|
+
const analyticsRequestsRef = React.useRef(createLatestCommittedRequest(initialAnalyticsQuery));
|
|
40
|
+
const [analyticsCommittedQuery, setAnalyticsCommittedQuery] = React.useState(initialAnalyticsQuery);
|
|
41
|
+
const [data, setData] = React.useState(() => normalizeWorkbenchData(null));
|
|
42
|
+
const [busy, setBusy] = React.useState(false);
|
|
43
|
+
const [actionBusy, setActionBusy] = React.useState(false);
|
|
44
|
+
const [catalogCommand, setCatalogCommand] = React.useState(null);
|
|
45
|
+
const [importCompletion, setImportCompletion] = React.useState(null);
|
|
46
|
+
const [importFailures, setImportFailures] = React.useState({ CONTROLLED_CATALOG: null, SANCTION_CATALOG: null });
|
|
47
|
+
const runningImportTaskRef = React.useRef(null);
|
|
48
|
+
const runningContractTasksRef = React.useRef(new Set());
|
|
49
|
+
const handledContractTasksRef = React.useRef({ PURCHASE_CONTRACT: null, CUSTOMER_CONTRACT: null });
|
|
50
|
+
const blobUrlOwnerRef = React.useRef(createBlobUrlOwner());
|
|
51
|
+
const changeRouteRef = React.useRef(() => undefined);
|
|
52
|
+
const [procurementContext, setProcurementContext] = React.useState({ supplier: null, order: null, metrics: null });
|
|
53
|
+
const [purchaseImportTask, setPurchaseImportTask] = React.useState(null);
|
|
54
|
+
const [purchaseReviewDraft, setPurchaseReviewDraft] = React.useState(null);
|
|
55
|
+
const [customerImportState, setCustomerImportState] = React.useState({ status: 'IDLE' });
|
|
56
|
+
const [customerReanalysisState, setCustomerReanalysisState] = React.useState({ status: 'IDLE' });
|
|
57
|
+
const [activeSalesFileOrder, setActiveSalesFileOrder] = React.useState(null);
|
|
58
|
+
const [editingSalesFile, setEditingSalesFile] = React.useState(null);
|
|
59
|
+
const [toast, setToast] = React.useState(null);
|
|
60
|
+
const [analyticsError, setAnalyticsError] = React.useState(null);
|
|
61
|
+
const loadRoute = React.useCallback(async (targetRoute, page = 1, pageSize = 20, filters = {}) => {
|
|
62
|
+
const request = routeDataRequest(targetRoute, page, pageSize, filters);
|
|
63
|
+
if (!request)
|
|
64
|
+
return;
|
|
65
|
+
const collection = String(asRecord(request.parameters).collection);
|
|
66
|
+
const requests = collection === 'analytics' ? analyticsRequestsRef.current : routeRequestGateRef.current;
|
|
67
|
+
const analyticsCandidate = collection === 'analytics' ? analyticsQueryFromRoute(page, pageSize, filters) : null;
|
|
68
|
+
const requestId = analyticsCandidate ? analyticsRequestsRef.current.begin(analyticsCandidate) : routeRequestGateRef.current.begin();
|
|
69
|
+
setBusy(true);
|
|
70
|
+
try {
|
|
71
|
+
const payload = asRecord(unwrapResponse(await bridge.requestData(request)));
|
|
72
|
+
if (!requests.commit(requestId))
|
|
73
|
+
return;
|
|
74
|
+
if (routeRef.current !== targetRoute && JSON.stringify(routeRef.current) !== JSON.stringify(targetRoute))
|
|
75
|
+
return;
|
|
76
|
+
if (collection === 'analytics') {
|
|
77
|
+
setAnalyticsCommittedQuery(analyticsRequestsRef.current.committed());
|
|
78
|
+
setData(current => ({ ...current, analytics: normalizeWorkbenchData({ summary: { analytics: payload } }).analytics }));
|
|
79
|
+
}
|
|
80
|
+
else if (collection === 'settings')
|
|
81
|
+
setData(current => ({ ...current, settings: normalizeWorkbenchData({ summary: { settings: payload } }).settings }));
|
|
82
|
+
else
|
|
83
|
+
setData(current => ({
|
|
84
|
+
...current,
|
|
85
|
+
[collection]: {
|
|
86
|
+
...payload,
|
|
87
|
+
items: Array.isArray(payload.items) ? payload.items : [],
|
|
88
|
+
total: numberValue(payload.total, 0), page: numberValue(payload.page, page), pageSize: numberValue(payload.pageSize, pageSize)
|
|
89
|
+
}
|
|
90
|
+
}));
|
|
91
|
+
if (targetRoute.type === 'procurement')
|
|
92
|
+
setProcurementContext(current => ({
|
|
93
|
+
supplier: payload.supplier && typeof payload.supplier === 'object' ? payload.supplier : targetRoute.level === 'suppliers' ? null : current.supplier,
|
|
94
|
+
order: payload.order && typeof payload.order === 'object' ? payload.order : null,
|
|
95
|
+
metrics: payload.metrics && typeof payload.metrics === 'object' ? payload.metrics : targetRoute.level === 'orders' ? null : current.metrics
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (requests.isLatest(requestId)) {
|
|
100
|
+
if (collection === 'analytics')
|
|
101
|
+
setAnalyticsCommittedQuery({ ...analyticsRequestsRef.current.committed() });
|
|
102
|
+
setToast({ message: error instanceof Error ? `页面加载失败:${error.message}` : '页面加载失败', tone: 'error' });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
requests.finish(requestId);
|
|
107
|
+
setBusy(routeRequestGateRef.current.activeCount() + requestGateRef.current.activeCount() + analyticsRequestsRef.current.activeCount() > 0);
|
|
108
|
+
}
|
|
109
|
+
}, []);
|
|
110
|
+
const load = React.useCallback(async (nextContext) => {
|
|
111
|
+
const active = nextContext ?? contextRef.current;
|
|
112
|
+
if (!active)
|
|
113
|
+
return;
|
|
114
|
+
const gate = requestGateRef.current;
|
|
115
|
+
const requestId = gate.begin();
|
|
116
|
+
setBusy(true);
|
|
117
|
+
try {
|
|
118
|
+
const response = await bridge.requestData({ page: 1, pageSize: 200, parameters: active.initialQuery?.parameters ?? {} });
|
|
119
|
+
if (gate.isLatest(requestId)) {
|
|
120
|
+
const normalized = normalizeWorkbenchData(response);
|
|
121
|
+
bridge.configure({ uploadPolicy: normalized.uploadPolicy });
|
|
122
|
+
setData(normalized);
|
|
123
|
+
await loadRoute(routeRef.current);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (gate.isLatest(requestId))
|
|
128
|
+
setToast({ message: error instanceof Error ? `刷新失败:${error.message}` : '刷新失败', tone: 'error' });
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
gate.finish(requestId);
|
|
132
|
+
setBusy(gate.activeCount() + routeRequestGateRef.current.activeCount() + analyticsRequestsRef.current.activeCount() > 0);
|
|
133
|
+
}
|
|
134
|
+
}, [loadRoute]);
|
|
135
|
+
React.useEffect(() => {
|
|
136
|
+
bridge.start({
|
|
137
|
+
onInit(nextContext) {
|
|
138
|
+
contextRef.current = nextContext;
|
|
139
|
+
const normalized = normalizeWorkbenchData(nextContext.payload);
|
|
140
|
+
bridge.configure({ uploadPolicy: normalized.uploadPolicy });
|
|
141
|
+
setData(normalized);
|
|
142
|
+
void load(nextContext);
|
|
143
|
+
},
|
|
144
|
+
onHostEvent() {
|
|
145
|
+
void load();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
return () => { blobUrlOwnerRef.current.dispose(); bridge.destroy(); };
|
|
149
|
+
}, [load]);
|
|
150
|
+
React.useEffect(() => {
|
|
151
|
+
const timer = window.setTimeout(() => bridge.reportResize(), 0);
|
|
152
|
+
return () => window.clearTimeout(timer);
|
|
153
|
+
}, [route, data, busy]);
|
|
154
|
+
React.useEffect(() => {
|
|
155
|
+
if (!toast)
|
|
156
|
+
return;
|
|
157
|
+
const timer = window.setTimeout(() => setToast(null), 4200);
|
|
158
|
+
return () => window.clearTimeout(timer);
|
|
159
|
+
}, [toast]);
|
|
160
|
+
const executeAction = React.useCallback(async (actionKey, input) => {
|
|
161
|
+
setActionBusy(true);
|
|
162
|
+
try {
|
|
163
|
+
const result = asActionResult(unwrapResponse(await bridge.executeAction(actionKey, input)));
|
|
164
|
+
if (result.success === false)
|
|
165
|
+
throw new Error(resolveActionMessage(result) || '操作失败');
|
|
166
|
+
if (actionKey === 'download_import_issues')
|
|
167
|
+
downloadActionFile(result.data);
|
|
168
|
+
if (result.refresh && shouldRefreshAfterAction(actionKey))
|
|
169
|
+
await load();
|
|
170
|
+
setToast({ message: resolveActionMessage(result) || '操作成功', tone: 'success' });
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
setToast({ message: error instanceof Error ? error.message : '操作失败', tone: 'error' });
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
setActionBusy(false);
|
|
179
|
+
}
|
|
180
|
+
}, [load]);
|
|
181
|
+
const executeSalesFileAction = React.useCallback((actionKey, input) => executeAction(actionKey, input), [executeAction]);
|
|
182
|
+
const orchestrateImport = React.useCallback(async (start) => {
|
|
183
|
+
const taskId = start.data?.task.id;
|
|
184
|
+
if (!taskId)
|
|
185
|
+
throw new Error('解析响应缺少任务标识');
|
|
186
|
+
if (runningImportTaskRef.current === taskId)
|
|
187
|
+
throw new Error('该目录任务正在解析中');
|
|
188
|
+
runningImportTaskRef.current = taskId;
|
|
189
|
+
setActionBusy(true);
|
|
190
|
+
const documentType = start.data?.task.documentType;
|
|
191
|
+
if (documentType)
|
|
192
|
+
setImportFailures((current) => ({ ...current, [documentType]: null }));
|
|
193
|
+
try {
|
|
194
|
+
const completion = await runCatalogImportOrchestration({
|
|
195
|
+
start,
|
|
196
|
+
invokeClientCommand: (commandKey, payload) => bridge.invokeClientCommand(commandKey, payload),
|
|
197
|
+
markCommandDispatched: async (taskId) => {
|
|
198
|
+
const result = asActionResult(unwrapResponse(await bridge.executeAction('mark_import_command_dispatched', { taskId })));
|
|
199
|
+
if (result.success === false)
|
|
200
|
+
throw new Error(resolveActionMessage(result) || '记录解析命令状态失败');
|
|
201
|
+
},
|
|
202
|
+
pollTask: async (taskId) => asImportCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId } }))),
|
|
203
|
+
refresh: load
|
|
204
|
+
});
|
|
205
|
+
setImportCompletion(completion);
|
|
206
|
+
setToast({ message: '目录导入完成', tone: 'success' });
|
|
207
|
+
return completion;
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (error instanceof CatalogImportError)
|
|
211
|
+
setImportFailures((current) => ({ ...current, [error.task.documentType]: error.task }));
|
|
212
|
+
setToast({ message: error instanceof Error ? error.message : '目录解析失败', tone: 'error' });
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
runningImportTaskRef.current = null;
|
|
217
|
+
setActionBusy(false);
|
|
218
|
+
}
|
|
219
|
+
}, [load]);
|
|
220
|
+
const orchestrateContractImport = React.useCallback(async (start) => {
|
|
221
|
+
const task = start.data?.task;
|
|
222
|
+
if (!task)
|
|
223
|
+
throw new Error('解析响应缺少合同任务标识');
|
|
224
|
+
if (runningContractTasksRef.current.has(task.id))
|
|
225
|
+
throw new Error('该合同任务正在解析中');
|
|
226
|
+
runningContractTasksRef.current.add(task.id);
|
|
227
|
+
setActionBusy(true);
|
|
228
|
+
if (task.documentType === 'PURCHASE_CONTRACT') {
|
|
229
|
+
blobUrlOwnerRef.current.clear('purchase');
|
|
230
|
+
setPurchaseImportTask(task);
|
|
231
|
+
setPurchaseReviewDraft(null);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
blobUrlOwnerRef.current.clear('customer');
|
|
235
|
+
setCustomerImportState({ status: 'PROCESSING', importTaskId: task.id, sourceFileName: task.sourceFileName ?? '客户合同' });
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const completion = await runContractImportOrchestration({
|
|
239
|
+
start,
|
|
240
|
+
invokeClientCommand: (commandKey, payload) => bridge.invokeClientCommand(commandKey, payload),
|
|
241
|
+
markCommandDispatched: async (taskId) => {
|
|
242
|
+
const result = asActionResult(unwrapResponse(await bridge.executeAction('mark_import_command_dispatched', { taskId })));
|
|
243
|
+
if (result.success === false)
|
|
244
|
+
throw new Error(resolveActionMessage(result) || '记录解析命令状态失败');
|
|
245
|
+
},
|
|
246
|
+
pollTask: async (taskId) => asContractCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId } })))
|
|
247
|
+
});
|
|
248
|
+
handledContractTasksRef.current[task.documentType] = task.id;
|
|
249
|
+
if (completion.task.status === 'AWAITING_REVIEW') {
|
|
250
|
+
if (completion.task.documentType === 'PURCHASE_CONTRACT') {
|
|
251
|
+
const draft = adaptPurchaseReview(completion.task);
|
|
252
|
+
setPurchaseImportTask(completion.task);
|
|
253
|
+
setPurchaseReviewDraft(await attachPurchaseSource(draft, completion.task, blobUrlOwnerRef.current));
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
const review = adaptCustomerContractReview(completion.task.resultPayload, { reviewSource: 'IMPORT_DRAFT', importTaskId: completion.task.id, sourceFileId: completion.task.sourceFileId ?? '', sourceFileName: completion.task.sourceFileName ?? '' });
|
|
257
|
+
setCustomerImportState({ status: 'AWAITING_REVIEW', importTaskId: completion.task.id, review: await attachCustomerSource(review, blobUrlOwnerRef.current) });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return completion;
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
const failed = error instanceof ContractImportError ? error.task : task;
|
|
264
|
+
if (task.documentType === 'PURCHASE_CONTRACT') {
|
|
265
|
+
setPurchaseImportTask({ ...failed, status: failed.status === 'TIMED_OUT' ? 'TIMED_OUT' : 'FAILED', errorMessage: error instanceof Error ? error.message : '采购合同解析失败' });
|
|
266
|
+
setPurchaseReviewDraft(null);
|
|
267
|
+
}
|
|
268
|
+
else
|
|
269
|
+
setCustomerImportState({ status: 'FAILED', importTaskId: failed.id, message: error instanceof Error ? error.message : '客户合同解析失败' });
|
|
270
|
+
setToast({ message: error instanceof Error ? error.message : '合同解析失败', tone: 'error' });
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
runningContractTasksRef.current.delete(task.id);
|
|
275
|
+
setActionBusy(false);
|
|
276
|
+
}
|
|
277
|
+
}, []);
|
|
278
|
+
const executeFileAction = React.useCallback(async (actionKey, file, input) => {
|
|
279
|
+
try {
|
|
280
|
+
const start = asActionResult(unwrapResponse(await bridge.executeFileAction(actionKey, file, input)));
|
|
281
|
+
if (start.success === false)
|
|
282
|
+
throw new Error(resolveActionMessage(start) || '上传失败');
|
|
283
|
+
if (actionKey === 'upload_purchase_contract' || actionKey === 'upload_customer_contract') {
|
|
284
|
+
const completion = await orchestrateContractImport(start);
|
|
285
|
+
return { success: true, data: completion, message: '合同解析完成,等待审核', refresh: false };
|
|
286
|
+
}
|
|
287
|
+
const completion = await orchestrateImport(start);
|
|
288
|
+
return { success: true, data: completion, message: '目录导入完成', refresh: true };
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
if (!(error instanceof CatalogImportError))
|
|
292
|
+
setToast({ message: error instanceof Error ? error.message : '上传失败', tone: 'error' });
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
}, [orchestrateContractImport, orchestrateImport]);
|
|
296
|
+
React.useEffect(() => {
|
|
297
|
+
const tasks = data.importTasks.items;
|
|
298
|
+
setImportFailures({
|
|
299
|
+
CONTROLLED_CATALOG: findLatestFailedCatalogImportTask(tasks, 'CONTROLLED_CATALOG'),
|
|
300
|
+
SANCTION_CATALOG: findLatestFailedCatalogImportTask(tasks, 'SANCTION_CATALOG')
|
|
301
|
+
});
|
|
302
|
+
const active = findActiveCatalogImportTask(tasks);
|
|
303
|
+
if (active && runningImportTaskRef.current !== active.id && active.executionCommand) {
|
|
304
|
+
void orchestrateImport({ success: true, data: { task: active, commands: [active.executionCommand], polling: { intervalMs: 2000, timeoutMs: 1200000 } } }).catch(() => undefined);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
}, [data.importTasks.items, orchestrateImport]);
|
|
308
|
+
React.useEffect(() => {
|
|
309
|
+
const tasks = data.importTasks.items;
|
|
310
|
+
for (const documentType of ['PURCHASE_CONTRACT', 'CUSTOMER_CONTRACT']) {
|
|
311
|
+
const resumable = findResumableContractImport(tasks, documentType);
|
|
312
|
+
const failed = findRecoverableContractImport(tasks, documentType);
|
|
313
|
+
if (resumable && handledContractTasksRef.current[documentType] !== resumable.id && !runningContractTasksRef.current.has(resumable.id)) {
|
|
314
|
+
if (resumable.status === 'AWAITING_REVIEW' || resumable.executionCommand) {
|
|
315
|
+
void orchestrateContractImport({ success: true, data: { task: resumable, commands: resumable.executionCommand ? [resumable.executionCommand] : [], polling: { intervalMs: 2000, timeoutMs: 1200000 } } }).catch(() => undefined);
|
|
316
|
+
}
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (!resumable && failed) {
|
|
320
|
+
if (documentType === 'PURCHASE_CONTRACT')
|
|
321
|
+
setPurchaseImportTask(failed);
|
|
322
|
+
else
|
|
323
|
+
setCustomerImportState({ status: 'FAILED', importTaskId: failed.id, message: failed.errorMessage || '客户合同解析失败,请重新解析' });
|
|
324
|
+
}
|
|
325
|
+
if (!resumable && !failed && tasks.some(item => item.documentType === documentType && item.status === 'SUCCEEDED')) {
|
|
326
|
+
if (documentType === 'PURCHASE_CONTRACT') {
|
|
327
|
+
setPurchaseImportTask(null);
|
|
328
|
+
setPurchaseReviewDraft(null);
|
|
329
|
+
}
|
|
330
|
+
else
|
|
331
|
+
setCustomerImportState({ status: 'IDLE' });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}, [data.importTasks.items, orchestrateContractImport]);
|
|
335
|
+
const retryImport = React.useCallback(async (taskId) => {
|
|
336
|
+
try {
|
|
337
|
+
const start = asActionResult(unwrapResponse(await bridge.executeAction('retry_import_task', { taskId })));
|
|
338
|
+
if (start.success === false)
|
|
339
|
+
throw new Error(resolveActionMessage(start) || '重新解析失败');
|
|
340
|
+
await orchestrateImport(start);
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
if (!(error instanceof CatalogImportError))
|
|
344
|
+
setToast({ message: error instanceof Error ? error.message : '重新解析失败', tone: 'error' });
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
}, [orchestrateImport]);
|
|
348
|
+
const retryContractImport = React.useCallback(async (taskId) => {
|
|
349
|
+
const start = asActionResult(unwrapResponse(await bridge.executeAction('retry_import_task', { taskId })));
|
|
350
|
+
if (start.success === false)
|
|
351
|
+
throw new Error(resolveActionMessage(start) || '重新解析失败');
|
|
352
|
+
const documentType = start.data?.task.documentType;
|
|
353
|
+
if (documentType === 'PURCHASE_CONTRACT' || documentType === 'CUSTOMER_CONTRACT')
|
|
354
|
+
handledContractTasksRef.current[documentType] = null;
|
|
355
|
+
await orchestrateContractImport(start);
|
|
356
|
+
}, [orchestrateContractImport]);
|
|
357
|
+
const reanalyzeCustomerContract = React.useCallback(async (input) => {
|
|
358
|
+
setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析中' });
|
|
359
|
+
try {
|
|
360
|
+
const response = asActionResult(unwrapResponse(await bridge.executeAction('reanalyze_customer_contract', { reviewSource: input.reviewSource, ...(input.customerContractId ? { id: input.customerContractId } : {}), ...(input.importTaskId ? { importTaskId: input.importTaskId } : {}) })));
|
|
361
|
+
if (response.success === false || !response.data)
|
|
362
|
+
throw new Error(resolveActionMessage(response) || 'AI 重新分析启动失败');
|
|
363
|
+
const result = await runCustomerContractReanalysis({
|
|
364
|
+
start: response.data,
|
|
365
|
+
invokeClientCommand: (commandKey, payload) => bridge.invokeClientCommand(commandKey, payload),
|
|
366
|
+
markCommandDispatched: async (runId) => {
|
|
367
|
+
const marked = asActionResult(unwrapResponse(await bridge.executeAction('mark_customer_contract_reanalysis_dispatched', { runId })));
|
|
368
|
+
if (marked.success === false)
|
|
369
|
+
throw new Error(resolveActionMessage(marked) || '记录重新分析命令状态失败');
|
|
370
|
+
},
|
|
371
|
+
markFailed: async (runId, failure) => {
|
|
372
|
+
const marked = asActionResult(unwrapResponse(await bridge.executeAction('mark_customer_contract_reanalysis_failed', { runId, ...failure })));
|
|
373
|
+
if (marked.success === false)
|
|
374
|
+
throw new Error(resolveActionMessage(marked) || '记录重新分析失败状态失败');
|
|
375
|
+
},
|
|
376
|
+
poll: async (runId) => asCustomerContractReanalysisResult(unwrapResponse(await bridge.requestData({ parameters: { collection: 'customerContractAnalysis', runId } })))
|
|
377
|
+
});
|
|
378
|
+
let review;
|
|
379
|
+
if (result.review) {
|
|
380
|
+
review = adaptCustomerContractReview(result.review, { reviewSource: 'EXISTING_CONTRACT' });
|
|
381
|
+
}
|
|
382
|
+
else if (result.task) {
|
|
383
|
+
review = adaptCustomerContractReview(result.task.resultPayload, { reviewSource: 'IMPORT_DRAFT', importTaskId: result.task.id, sourceFileId: result.task.sourceFileId ?? '', sourceFileName: result.task.sourceFileName ?? '' });
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
throw new Error('重新分析结果缺少合同审核数据');
|
|
387
|
+
}
|
|
388
|
+
setCustomerReanalysisState({ status: 'IDLE' });
|
|
389
|
+
return review;
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
setCustomerReanalysisState({ status: 'FAILED', message: error instanceof Error ? error.message : 'AI 重新分析失败' });
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
395
|
+
}, []);
|
|
396
|
+
const loadCustomerContractReview = React.useCallback(async (customerContractId) => {
|
|
397
|
+
const result = asActionResult(unwrapResponse(await bridge.executeAction('get_customer_contract_review', { id: customerContractId })));
|
|
398
|
+
if (result.success === false || !result.data)
|
|
399
|
+
throw new Error(resolveActionMessage(result) || '客户合同审核加载失败');
|
|
400
|
+
const raw = asRecord(result.data);
|
|
401
|
+
const review = 'reviewSource' in raw ? raw : adaptCustomerContractReview(raw, { reviewSource: 'EXISTING_CONTRACT' });
|
|
402
|
+
return attachCustomerSource(review, blobUrlOwnerRef.current);
|
|
403
|
+
}, []);
|
|
404
|
+
const closePurchaseReview = React.useCallback(() => {
|
|
405
|
+
blobUrlOwnerRef.current.clear('purchase');
|
|
406
|
+
setPurchaseReviewDraft(null);
|
|
407
|
+
}, []);
|
|
408
|
+
const closeCustomerReview = React.useCallback(() => {
|
|
409
|
+
blobUrlOwnerRef.current.clear('customer');
|
|
410
|
+
setCustomerImportState({ status: 'IDLE' });
|
|
411
|
+
}, []);
|
|
412
|
+
const queryImportIssues = React.useCallback(async (query) => {
|
|
413
|
+
const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: query.pageSize, parameters: { collection: 'importIssues', documentType: query.documentType, batchId: query.batchId, issueType: query.issueType } })));
|
|
414
|
+
return { items: Array.isArray(payload.items) ? payload.items : [], total: numberValue(payload.total, 0), page: numberValue(payload.page, query.page), pageSize: numberValue(payload.pageSize, query.pageSize) };
|
|
415
|
+
}, []);
|
|
416
|
+
const queryCatalog = React.useCallback(async (collection, query) => {
|
|
417
|
+
const gate = requestGateRef.current;
|
|
418
|
+
const requestId = gate.begin();
|
|
419
|
+
setBusy(true);
|
|
420
|
+
try {
|
|
421
|
+
const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: query.pageSize, parameters: { collection, filters: query.filters } })));
|
|
422
|
+
if (!gate.isLatest(requestId))
|
|
423
|
+
return;
|
|
424
|
+
setData((current) => ({
|
|
425
|
+
...current,
|
|
426
|
+
[collection]: {
|
|
427
|
+
...current[collection],
|
|
428
|
+
items: Array.isArray(payload.items) ? payload.items : [],
|
|
429
|
+
total: numberValue(payload.total, 0),
|
|
430
|
+
page: numberValue(payload.page, query.page),
|
|
431
|
+
pageSize: numberValue(payload.pageSize, query.pageSize)
|
|
432
|
+
}
|
|
433
|
+
}));
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
if (gate.isLatest(requestId))
|
|
437
|
+
setToast({ message: error instanceof Error ? `查询失败:${error.message}` : '查询失败', tone: 'error' });
|
|
438
|
+
}
|
|
439
|
+
finally {
|
|
440
|
+
gate.finish(requestId);
|
|
441
|
+
setBusy(gate.activeCount() > 0);
|
|
442
|
+
}
|
|
443
|
+
}, []);
|
|
444
|
+
const queryProcurement = React.useCallback(async (_collection, query) => {
|
|
445
|
+
await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
|
|
446
|
+
}, [loadRoute]);
|
|
447
|
+
const queryCustomerContracts = React.useCallback(async (query) => {
|
|
448
|
+
await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
|
|
449
|
+
}, [loadRoute]);
|
|
450
|
+
const querySales = React.useCallback(async (_collection, query) => {
|
|
451
|
+
await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
|
|
452
|
+
}, [loadRoute]);
|
|
453
|
+
const querySalesFiles = React.useCallback(async (collection, query) => {
|
|
454
|
+
if (collection === 'salesFileOrders') {
|
|
455
|
+
await loadRoute({ type: 'page', page: 'sales-files' }, query.page, query.pageSize, query.filters);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: query.pageSize, parameters: { collection, ...query.parameters, filters: query.filters } })));
|
|
459
|
+
setData(current => ({ ...current, generatedFiles: { ...payload, items: Array.isArray(payload.items) ? payload.items : [], total: numberValue(payload.total, 0), page: numberValue(payload.page, query.page), pageSize: numberValue(payload.pageSize, query.pageSize) } }));
|
|
460
|
+
}, [loadRoute]);
|
|
461
|
+
const queryAnalytics = React.useCallback(async (query) => {
|
|
462
|
+
const requests = analyticsRequestsRef.current;
|
|
463
|
+
const requestId = requests.begin(query);
|
|
464
|
+
setBusy(true);
|
|
465
|
+
setAnalyticsError(null);
|
|
466
|
+
try {
|
|
467
|
+
const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: query.pageSize, parameters: { collection: 'analytics', mode: query.mode, dimension: query.dimension, filters: query.filters } })));
|
|
468
|
+
if (!requests.commit(requestId))
|
|
469
|
+
return;
|
|
470
|
+
setAnalyticsCommittedQuery(requests.committed());
|
|
471
|
+
setData(current => ({ ...current, analytics: normalizeWorkbenchData({ summary: { analytics: payload } }).analytics }));
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
if (!requests.isLatest(requestId))
|
|
475
|
+
return;
|
|
476
|
+
const message = error instanceof Error ? error.message : '统计查询失败';
|
|
477
|
+
setAnalyticsCommittedQuery({ ...requests.committed() });
|
|
478
|
+
setAnalyticsError(message);
|
|
479
|
+
setToast({ message: `统计查询失败:${message}`, tone: 'error' });
|
|
480
|
+
}
|
|
481
|
+
finally {
|
|
482
|
+
requests.finish(requestId);
|
|
483
|
+
setBusy(requests.activeCount() + requestGateRef.current.activeCount() + routeRequestGateRef.current.activeCount() > 0);
|
|
484
|
+
}
|
|
485
|
+
}, []);
|
|
486
|
+
const exportAnalytics = React.useCallback(async (_request) => {
|
|
487
|
+
setActionBusy(true);
|
|
488
|
+
try {
|
|
489
|
+
const committed = analyticsRequestsRef.current.committed();
|
|
490
|
+
const request = committed.mode === 'PURCHASE'
|
|
491
|
+
? { mode: committed.mode, dimension: committed.dimension, filters: committed.filters, format: 'XLSX' }
|
|
492
|
+
: { mode: committed.mode, dimension: committed.dimension, filters: committed.filters, format: 'XLSX' };
|
|
493
|
+
const result = asActionResult(unwrapResponse(await bridge.executeAction('export_analytics', request)));
|
|
494
|
+
if (result.success === false)
|
|
495
|
+
throw new Error(resolveActionMessage(result) || '分析导出失败');
|
|
496
|
+
downloadActionFile(result.data);
|
|
497
|
+
setToast({ message: '分析数据导出成功', tone: 'success' });
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
setToast({ message: error instanceof Error ? error.message : '分析导出失败', tone: 'error' });
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
finally {
|
|
504
|
+
setActionBusy(false);
|
|
505
|
+
}
|
|
506
|
+
}, []);
|
|
507
|
+
const saveSettings = React.useCallback(async (request) => {
|
|
508
|
+
const result = asActionResult(unwrapResponse(await bridge.executeAction('save_settings', request)));
|
|
509
|
+
if (result.success === false)
|
|
510
|
+
throw new Error(resolveActionMessage(result) || '设置保存失败');
|
|
511
|
+
await loadRoute({ type: 'page', page: 'settings' });
|
|
512
|
+
}, [loadRoute]);
|
|
513
|
+
const restoreSettings = React.useCallback(async (request) => {
|
|
514
|
+
const result = asActionResult(unwrapResponse(await bridge.executeAction('restore_settings_default', request)));
|
|
515
|
+
if (result.success === false)
|
|
516
|
+
throw new Error(resolveActionMessage(result) || '恢复系统默认失败');
|
|
517
|
+
await loadRoute({ type: 'page', page: 'settings' });
|
|
518
|
+
}, [loadRoute]);
|
|
519
|
+
const deleteSettingsRate = React.useCallback(async (request) => {
|
|
520
|
+
const result = asActionResult(unwrapResponse(await bridge.executeAction('delete_settings_rate', request)));
|
|
521
|
+
if (result.success === false)
|
|
522
|
+
throw new Error(resolveActionMessage(result) || '月度汇率删除失败');
|
|
523
|
+
await loadRoute({ type: 'page', page: 'settings' });
|
|
524
|
+
}, [loadRoute]);
|
|
525
|
+
const uploadCompanyLogo = React.useCallback(async (file) => {
|
|
526
|
+
const result = asActionResult(unwrapResponse(await bridge.executeFileAction('upload_company_logo', file, {})));
|
|
527
|
+
if (result.success === false || !result.data?.logoFileId)
|
|
528
|
+
throw new Error(resolveActionMessage(result) || '公司 Logo 上传失败');
|
|
529
|
+
setToast({ message: '公司 Logo 上传成功', tone: 'success' });
|
|
530
|
+
return result.data.logoFileId;
|
|
531
|
+
}, []);
|
|
532
|
+
const discardCompanyLogo = React.useCallback(async (logoFileId) => {
|
|
533
|
+
const result = asActionResult(unwrapResponse(await bridge.executeAction('discard_company_logo', { logoFileId })));
|
|
534
|
+
if (result.success === false)
|
|
535
|
+
throw new Error(resolveActionMessage(result) || '公司 Logo 草稿清理失败');
|
|
536
|
+
}, []);
|
|
537
|
+
const openSalesFiles = React.useCallback((order) => {
|
|
538
|
+
setActiveSalesFileOrder(order);
|
|
539
|
+
if (order)
|
|
540
|
+
changeRouteRef.current({ type: 'page', page: 'sales-files' });
|
|
541
|
+
}, []);
|
|
542
|
+
const changeRoute = React.useCallback((nextRoute) => {
|
|
543
|
+
routeRef.current = nextRoute;
|
|
544
|
+
setRoute(nextRoute);
|
|
545
|
+
void loadRoute(nextRoute);
|
|
546
|
+
}, [loadRoute]);
|
|
547
|
+
changeRouteRef.current = changeRoute;
|
|
548
|
+
const page = routePage(route);
|
|
549
|
+
const meta = pageMeta[page];
|
|
550
|
+
const contractCommands = page === 'procurement'
|
|
551
|
+
? _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u91C7\u8D2D\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_purchase_contract', file, { documentType: 'PURCHASE_CONTRACT' }) })
|
|
552
|
+
: page === 'customer-contract-review'
|
|
553
|
+
? _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u5BA2\u6237\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_customer_contract', file, { documentType: 'CUSTOMER_CONTRACT' }) })
|
|
554
|
+
: undefined;
|
|
555
|
+
return (_jsxs(_Fragment, { children: [_jsxs(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, subtitle: meta.subtitle, commands: (page === 'controlled-goods' || page === 'sanctioned-companies') ? _jsx(CatalogCommands, { busy: busy || actionBusy, onUpload: () => setCatalogCommand('upload'), onCreate: () => setCatalogCommand('create') }) : contractCommands, busy: busy || actionBusy, onRouteChange: changeRoute, onRefresh: () => void load(), children: [page === 'home' && _jsx(HomePage, { dashboard: data.dashboard }), page === 'controlled-goods' && _jsx(ControlledGoodsPage, { collection: data.controlledGoods, batches: data.controlledBatches.items, busy: busy || actionBusy, onAction: executeAction, onFileAction: executeFileAction, onRefresh: load, onQuery: (query) => queryCatalog('controlledGoods', query), onIssueQuery: queryImportIssues, importCompletion: importCompletion, importFailure: importFailures.CONTROLLED_CATALOG, onRetryImport: retryImport, command: catalogCommand, onCommandHandled: () => setCatalogCommand(null) }), page === 'sanctioned-companies' && _jsx(SanctionedCompaniesPage, { collection: data.sanctionedCompanies, batches: data.sanctionBatches.items, busy: busy || actionBusy, onAction: executeAction, onFileAction: executeFileAction, onRefresh: load, onQuery: (query) => queryCatalog('sanctionedCompanies', query), onIssueQuery: queryImportIssues, importCompletion: importCompletion, importFailure: importFailures.SANCTION_CATALOG, onRetryImport: retryImport, command: catalogCommand, onCommandHandled: () => setCatalogCommand(null) }), page === 'procurement' && route.type === 'procurement' && _jsx(ProcurementPage, { route: route, suppliers: data.suppliers, purchaseOrders: data.purchaseOrders, purchaseLines: data.purchaseLines, supplier: procurementContext.supplier, order: procurementContext.order, metrics: procurementContext.metrics, importTask: purchaseImportTask, reviewDraft: purchaseReviewDraft, busy: busy || actionBusy, onQuery: queryProcurement, onAction: executeAction, onFileAction: executeFileAction, onRouteChange: changeRoute, onRetryImport: retryContractImport, onReviewClose: closePurchaseReview }), page === 'customer-contract-review' && _jsx(CustomerContractsPage, { collection: data.customerContracts, busy: busy || actionBusy, importReviewState: customerImportState, onQuery: queryCustomerContracts, onAction: executeAction, onFileAction: executeFileAction, onReanalyze: reanalyzeCustomerContract, onLoadReview: loadCustomerContractReview, onReviewClose: closeCustomerReview, reanalysisState: customerReanalysisState, onRetryImport: retryContractImport }), page === 'sales' && route.type === 'sales' && _jsx(SalesPage, { route: route, customers: data.salesCustomers, orders: data.salesOrders, lines: data.salesLines, busy: busy || actionBusy, onQuery: querySales, onAction: executeAction, onRouteChange: changeRoute, onOpenFiles: openSalesFiles }), page === 'sales-files' && _jsx(SalesFilesPage, { orders: data.salesFileOrders, history: data.generatedFiles, activeOrder: activeSalesFileOrder, banks: data.settings.banks, busy: busy || actionBusy, onQuery: querySalesFiles, onAction: executeSalesFileAction, onOpenSalesOrder: setActiveSalesFileOrder, onDownload: async (file) => { const result = await executeAction('download_sales_file', { id: file.id }); downloadActionFile(result.data); }, onEdit: async (file) => setEditingSalesFile(file) }), page === 'analytics' && _jsx(AnalyticsPage, { data: data.analytics, committedQuery: analyticsCommittedQuery, busy: busy || actionBusy, error: analyticsError, onQuery: queryAnalytics, onExportXlsx: exportAnalytics }), page === 'settings' && _jsx(SettingsPage, { data: data.settings, busy: busy || actionBusy, onSave: saveSettings, onRestoreDefault: restoreSettings, onDeleteRate: deleteSettingsRate, onUploadLogo: uploadCompanyLogo, onDiscardLogo: discardCompanyLogo }), !['home', 'controlled-goods', 'sanctioned-companies', 'procurement', 'customer-contract-review', 'sales', 'sales-files', 'analytics', 'settings'].includes(page) && _jsxs("section", { className: "tcw-page-placeholder", "data-page": page, children: [_jsx(InfoNotice, { children: "\u9875\u9762\u57FA\u7840\u7ED3\u6784\u5DF2\u5C31\u7EEA\uFF0C\u4E1A\u52A1\u5185\u5BB9\u5C06\u5728\u5BF9\u5E94\u9875\u9762\u6A21\u5757\u4E2D\u52A0\u8F7D\u3002" }), _jsx("div", { className: "tcw-placeholder-body", "aria-label": `${meta.title}页面内容` })] })] }), _jsx(LoadingBlocker, { visible: busy || actionBusy, label: actionBusy ? '操作处理中' : '数据加载中' }), toast && _jsx(Toast, { message: toast.message, tone: toast.tone, onClose: () => setToast(null) }), editingSalesFile && _jsx(SalesFileEditor, { file: editingSalesFile, onClose: () => setEditingSalesFile(null), onSaved: () => { setEditingSalesFile(null); if (activeSalesFileOrder)
|
|
556
|
+
void querySalesFiles('generatedFiles', { page: 1, pageSize: 20, filters: {}, parameters: { customerContractId: activeSalesFileOrder.customerContractId, purchaseOrderId: activeSalesFileOrder.purchaseOrderId } }); } })] }));
|
|
557
|
+
}
|
|
558
|
+
function routePage(route) {
|
|
559
|
+
if (route.type === 'procurement')
|
|
560
|
+
return 'procurement';
|
|
561
|
+
if (route.type === 'sales')
|
|
562
|
+
return 'sales';
|
|
563
|
+
return route.page;
|
|
564
|
+
}
|
|
565
|
+
function analyticsQueryFromRoute(page, pageSize, values) {
|
|
566
|
+
const dateValue = values.orderDate;
|
|
567
|
+
const date = dateValue && typeof dateValue === 'object' && !Array.isArray(dateValue) ? dateValue : null;
|
|
568
|
+
const orderDate = date ? { from: optionalText(date.from), to: optionalText(date.to) } : undefined;
|
|
569
|
+
const common = {
|
|
570
|
+
...(orderDate?.from || orderDate?.to ? { orderDate } : {}),
|
|
571
|
+
...(optionalText(values.productName) ? { productName: optionalText(values.productName) } : {}),
|
|
572
|
+
...(optionalText(values.hsCode) ? { hsCode: optionalText(values.hsCode) } : {})
|
|
573
|
+
};
|
|
574
|
+
if (values.mode === 'SALES')
|
|
575
|
+
return {
|
|
576
|
+
mode: 'SALES', dimension: values.dimension === 'PRODUCT' ? 'PRODUCT' : 'CUSTOMER', page, pageSize,
|
|
577
|
+
filters: { ...common, ...(optionalText(values.customer) ? { customer: optionalText(values.customer) } : {}), ...(enabled(values.unreceivedOnly) ? { unreceivedOnly: true } : {}) }
|
|
578
|
+
};
|
|
579
|
+
return {
|
|
580
|
+
mode: 'PURCHASE', dimension: values.dimension === 'PRODUCT' ? 'PRODUCT' : 'SUPPLIER', page, pageSize,
|
|
581
|
+
filters: { ...common, ...(optionalText(values.supplier) ? { supplier: optionalText(values.supplier) } : {}), ...(enabled(values.unpaidOnly) ? { unpaidOnly: true } : {}), ...(enabled(values.unrefundedOnly) ? { unrefundedOnly: true } : {}) }
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function optionalText(value) { const text = typeof value === 'string' ? value.trim() : ''; return text || undefined; }
|
|
585
|
+
function enabled(value) { return value === true || value === 'true'; }
|
|
586
|
+
function asActionResult(value) {
|
|
587
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : { success: true, data: value };
|
|
588
|
+
}
|
|
589
|
+
function resolveActionMessage(result) {
|
|
590
|
+
if (typeof result.message === 'string')
|
|
591
|
+
return result.message;
|
|
592
|
+
if (result.message && typeof result.message === 'object') {
|
|
593
|
+
const localized = result.message;
|
|
594
|
+
return String(localized.zh_Hans ?? localized.en_US ?? '');
|
|
595
|
+
}
|
|
596
|
+
return '';
|
|
597
|
+
}
|
|
598
|
+
function asRecord(value) {
|
|
599
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
600
|
+
}
|
|
601
|
+
function numberValue(value, fallback) {
|
|
602
|
+
const result = Number(value);
|
|
603
|
+
return Number.isFinite(result) ? result : fallback;
|
|
604
|
+
}
|
|
605
|
+
function asImportCompletion(value) {
|
|
606
|
+
const payload = asRecord(value);
|
|
607
|
+
if (!payload.task || typeof payload.task !== 'object')
|
|
608
|
+
throw new Error('轮询响应缺少导入任务');
|
|
609
|
+
return payload;
|
|
610
|
+
}
|
|
611
|
+
function downloadActionFile(value) {
|
|
612
|
+
const file = asRecord(value);
|
|
613
|
+
if (typeof file.base64 !== 'string' || typeof file.fileName !== 'string')
|
|
614
|
+
throw new Error('下载响应缺少文件内容');
|
|
615
|
+
const binary = atob(file.base64);
|
|
616
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
617
|
+
const url = URL.createObjectURL(new Blob([bytes], { type: typeof file.mimeType === 'string' ? file.mimeType : 'application/octet-stream' }));
|
|
618
|
+
const anchor = document.createElement('a');
|
|
619
|
+
anchor.href = url;
|
|
620
|
+
anchor.download = file.fileName;
|
|
621
|
+
anchor.click();
|
|
622
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
623
|
+
}
|
|
624
|
+
function ContractUploadCommand({ label, busy, onFile }) {
|
|
625
|
+
return _jsxs("label", { className: "tcw-button tcw-button--primary", children: [_jsx("i", { className: "ri-upload-cloud-2-line", "aria-hidden": "true" }), label, _jsx("input", { className: "tcw-procurement-file-input", type: "file", accept: ".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.webp", disabled: busy, onChange: event => { const file = event.target.files?.[0]; event.currentTarget.value = ''; if (file)
|
|
626
|
+
void onFile(file).catch(() => undefined); } })] });
|
|
627
|
+
}
|
|
628
|
+
function adaptPurchaseReview(task) {
|
|
629
|
+
const payload = asRecord(task.resultPayload);
|
|
630
|
+
const supplier = asRecord(payload.supplier);
|
|
631
|
+
const order = asRecord(payload.order);
|
|
632
|
+
const lines = Array.isArray(payload.lines) ? payload.lines.map(value => {
|
|
633
|
+
const line = asRecord(value);
|
|
634
|
+
return { ...line, sourceType: line.sourceType === 'MANUAL_ENTRY' ? 'MANUAL_ENTRY' : 'CONTRACT_EXTRACTED', hsCodeStatus: line.hsCodeStatus === 'AI_CONFIRMED' || line.hsCodeStatus === 'MANUALLY_CONFIRMED' ? line.hsCodeStatus : 'UNCONFIRMED' };
|
|
635
|
+
}) : [];
|
|
636
|
+
return {
|
|
637
|
+
supplier: { ...supplier, name: String(supplier.name ?? '') },
|
|
638
|
+
order: { ...order, supplierId: String(order.supplierId ?? ''), contractNo: String(order.contractNo ?? ''), orderDate: String(order.orderDate ?? ''), contractAmountCny: String(order.contractAmountCny ?? '0') },
|
|
639
|
+
lines,
|
|
640
|
+
sourceFileName: task.sourceFileName ?? null
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
async function attachPurchaseSource(draft, task, owner) {
|
|
644
|
+
if (draft.sourceFileUrl || !task.sourceFileId)
|
|
645
|
+
return draft;
|
|
646
|
+
const preview = await loadSourcePreview(task.sourceFileId, 'purchase', owner);
|
|
647
|
+
if (preview.previewKind === 'URL')
|
|
648
|
+
return { ...draft, sourceFileUrl: preview.url };
|
|
649
|
+
if (preview.previewKind === 'TEXT')
|
|
650
|
+
return { ...draft, sourceFileUrl: textPreviewUrl(task.sourceFileName ?? '采购合同源文件', preview.content) };
|
|
651
|
+
return draft;
|
|
652
|
+
}
|
|
653
|
+
async function attachCustomerSource(review, owner) {
|
|
654
|
+
if (review.sourcePreview.kind === 'URL' || !review.sourceFileId)
|
|
655
|
+
return review;
|
|
656
|
+
const preview = await loadSourcePreview(review.sourceFileId, 'customer', owner);
|
|
657
|
+
if (preview.previewKind === 'URL')
|
|
658
|
+
return { ...review, sourcePreview: { kind: 'URL', url: preview.url } };
|
|
659
|
+
if (preview.previewKind === 'TEXT')
|
|
660
|
+
return { ...review, sourcePreview: { kind: 'TEXT', content: preview.content } };
|
|
661
|
+
return { ...review, sourcePreview: { kind: 'UNAVAILABLE', message: preview.message } };
|
|
662
|
+
}
|
|
663
|
+
async function loadSourcePreview(sourceFileId, slot, owner) {
|
|
664
|
+
const result = asActionResult(unwrapResponse(await bridge.executeAction('preview_source_file', { sourceFileId })));
|
|
665
|
+
if (result.success === false)
|
|
666
|
+
throw new Error(resolveActionMessage(result) || '源文件加载失败');
|
|
667
|
+
const file = asRecord(result.data);
|
|
668
|
+
if (file.previewKind === 'TEXT' && typeof file.content === 'string')
|
|
669
|
+
return { previewKind: 'TEXT', content: file.content };
|
|
670
|
+
if (file.previewKind === 'URL' && typeof file.base64 === 'string' && typeof file.mimeType === 'string') {
|
|
671
|
+
const binary = atob(file.base64);
|
|
672
|
+
const bytes = Uint8Array.from(binary, character => character.charCodeAt(0));
|
|
673
|
+
return { previewKind: 'URL', url: owner.replace(slot, URL.createObjectURL(new Blob([bytes], { type: file.mimeType }))) };
|
|
674
|
+
}
|
|
675
|
+
if (file.previewKind === 'UNAVAILABLE')
|
|
676
|
+
return { previewKind: 'UNAVAILABLE', message: typeof file.message === 'string' ? file.message : '源文件暂无法在线预览' };
|
|
677
|
+
throw new Error('源文件预览响应类型无效');
|
|
678
|
+
}
|
|
679
|
+
function textPreviewUrl(title, content) {
|
|
680
|
+
const html = `<!doctype html><html lang="zh-Hans"><head><meta charset="utf-8"><title>${escapeHtml(title)}</title><style>body{margin:0;padding:24px;font:14px/1.7 system-ui,sans-serif;color:#1f2937;background:#fff}pre{margin:0;white-space:pre-wrap;overflow-wrap:anywhere}</style></head><body><pre>${escapeHtml(content)}</pre></body></html>`;
|
|
681
|
+
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
|
682
|
+
}
|
|
683
|
+
function escapeHtml(value) { return value.replace(/[&<>"']/g, character => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character] ?? character); }
|
|
684
|
+
function asContractCompletion(value) {
|
|
685
|
+
const payload = asRecord(value);
|
|
686
|
+
if (!payload.task || typeof payload.task !== 'object')
|
|
687
|
+
throw new Error('轮询响应缺少合同导入任务');
|
|
688
|
+
return payload;
|
|
689
|
+
}
|
|
690
|
+
function asCustomerContractReanalysisResult(value) {
|
|
691
|
+
const payload = asRecord(value);
|
|
692
|
+
if (!payload.run || typeof payload.run !== 'object')
|
|
693
|
+
throw new Error('轮询响应缺少重新分析任务');
|
|
694
|
+
return payload;
|
|
695
|
+
}
|
|
696
|
+
function shouldRefreshAfterAction(actionKey) {
|
|
697
|
+
return new Set(['save_controlled_goods', 'update_controlled_goods', 'save_sanctioned_company', 'update_sanctioned_company', 'save_purchase_review', 'save_customer_contract_review', 'update_supplier', 'update_purchase_order', 'add_purchase_line', 'update_purchase_line', 'update_customer_contract_review', 'delete_entity']).has(actionKey);
|
|
698
|
+
}
|
|
699
|
+
async function ensureExcelEditorRuntime() {
|
|
700
|
+
const runtimeWindow = window;
|
|
701
|
+
if (runtimeWindow.TradeComplianceExcelEditor?.open)
|
|
702
|
+
return runtimeWindow.TradeComplianceExcelEditor;
|
|
703
|
+
if (excelEditorRuntimePromise)
|
|
704
|
+
return excelEditorRuntimePromise;
|
|
705
|
+
excelEditorRuntimePromise = (async () => {
|
|
706
|
+
const result = asActionResult(unwrapResponse(await bridge.executeAction('load_excel_editor_runtime', {})));
|
|
707
|
+
if (result.success === false)
|
|
708
|
+
throw new Error(resolveActionMessage(result) || 'Excel 编辑器运行时加载失败');
|
|
709
|
+
const script = result.data?.script;
|
|
710
|
+
const css = result.data?.css;
|
|
711
|
+
if (typeof script !== 'string' || !script.trim() || typeof css !== 'string')
|
|
712
|
+
throw new Error('Excel 编辑器运行时响应不完整');
|
|
713
|
+
let styleElement = document.head.querySelector('style[data-trade-compliance-editor-styles]');
|
|
714
|
+
if (!styleElement) {
|
|
715
|
+
styleElement = document.createElement('style');
|
|
716
|
+
styleElement.dataset.tradeComplianceEditorStyles = 'true';
|
|
717
|
+
styleElement.textContent = css;
|
|
718
|
+
document.head.appendChild(styleElement);
|
|
719
|
+
}
|
|
720
|
+
let scriptElement = document.body.querySelector('script[data-trade-compliance-editor-runtime]');
|
|
721
|
+
if (!scriptElement) {
|
|
722
|
+
scriptElement = document.createElement('script');
|
|
723
|
+
scriptElement.dataset.tradeComplianceEditorRuntime = 'true';
|
|
724
|
+
scriptElement.textContent = script;
|
|
725
|
+
document.body.appendChild(scriptElement);
|
|
726
|
+
}
|
|
727
|
+
const runtime = runtimeWindow.TradeComplianceExcelEditor;
|
|
728
|
+
if (!runtime?.open)
|
|
729
|
+
throw new Error('Excel 编辑器运行时初始化失败');
|
|
730
|
+
return runtime;
|
|
731
|
+
})().catch((error) => {
|
|
732
|
+
document.querySelector('script[data-trade-compliance-editor-runtime]')?.remove();
|
|
733
|
+
document.querySelector('style[data-trade-compliance-editor-styles]')?.remove();
|
|
734
|
+
excelEditorRuntimePromise = null;
|
|
735
|
+
throw error;
|
|
736
|
+
});
|
|
737
|
+
return excelEditorRuntimePromise;
|
|
738
|
+
}
|
|
739
|
+
function SalesFileEditor({ file, onClose, onSaved }) {
|
|
740
|
+
const containerRef = React.useRef(null);
|
|
741
|
+
const editorRef = React.useRef(null);
|
|
742
|
+
const [error, setError] = React.useState('');
|
|
743
|
+
const [saving, setSaving] = React.useState(false);
|
|
744
|
+
const [editorReady, setEditorReady] = React.useState(false);
|
|
745
|
+
React.useEffect(() => {
|
|
746
|
+
let cancelled = false;
|
|
747
|
+
setEditorReady(false);
|
|
748
|
+
void (async () => {
|
|
749
|
+
try {
|
|
750
|
+
const result = asActionResult(unwrapResponse(await bridge.executeAction('download_sales_file', { id: file.id })));
|
|
751
|
+
if (result.success === false)
|
|
752
|
+
throw new Error(resolveActionMessage(result) || '销售文件加载失败');
|
|
753
|
+
const payload = asRecord(result.data);
|
|
754
|
+
if (typeof payload.base64 !== 'string' || typeof payload.storageFileId !== 'string' || !Number.isInteger(payload.editVersion) || !containerRef.current)
|
|
755
|
+
throw new Error('销售文件响应缺少工作簿内容或版本信息');
|
|
756
|
+
const runtime = await ensureExcelEditorRuntime();
|
|
757
|
+
const container = containerRef.current;
|
|
758
|
+
if (cancelled || !container)
|
|
759
|
+
return;
|
|
760
|
+
const editor = await runtime.open({ container, base64: payload.base64, onSave: async (base64) => {
|
|
761
|
+
const saved = asActionResult(unwrapResponse(await bridge.executeAction('save_edited_sales_file', { id: file.id, base64, editVersion: payload.editVersion, expectedStorageFileId: payload.storageFileId })));
|
|
762
|
+
if (saved.success === false)
|
|
763
|
+
throw new Error(resolveActionMessage(saved) || '工作簿保存失败');
|
|
764
|
+
} });
|
|
765
|
+
if (cancelled)
|
|
766
|
+
editor.dispose();
|
|
767
|
+
else {
|
|
768
|
+
editorRef.current = editor;
|
|
769
|
+
setEditorReady(true);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
catch (cause) {
|
|
773
|
+
if (!cancelled)
|
|
774
|
+
setError(cause instanceof Error ? cause.message : 'Excel 编辑器加载失败');
|
|
775
|
+
}
|
|
776
|
+
})();
|
|
777
|
+
return () => { cancelled = true; editorRef.current?.dispose(); editorRef.current = null; };
|
|
778
|
+
}, [file.id]);
|
|
779
|
+
const save = async () => { if (!editorRef.current)
|
|
780
|
+
return; setSaving(true); setError(''); try {
|
|
781
|
+
await editorRef.current.save();
|
|
782
|
+
onSaved();
|
|
783
|
+
}
|
|
784
|
+
catch (cause) {
|
|
785
|
+
setError(cause instanceof Error ? cause.message : '工作簿保存失败');
|
|
786
|
+
}
|
|
787
|
+
finally {
|
|
788
|
+
setSaving(false);
|
|
789
|
+
} };
|
|
790
|
+
return _jsxs("section", { className: "tcw-excel-editor", "aria-label": "\u9500\u552E\u6587\u4EF6\u5168\u5C4F\u5BA1\u6838", children: [_jsxs("header", { children: [_jsx("strong", { children: file.fileName }), _jsxs("div", { children: [_jsx("button", { type: "button", className: "tcw-button tcw-button--secondary", onClick: onClose, children: "\u5173\u95ED" }), _jsx("button", { type: "button", className: "tcw-button tcw-button--primary", disabled: saving || !editorReady, onClick: () => void save(), children: "\u4FDD\u5B58\u5F53\u524D\u6587\u4EF6" })] })] }), error && _jsx("div", { className: "tcw-editor-error", role: "alert", children: error }), _jsx("div", { ref: containerRef, className: "tcw-excel-editor-canvas" })] });
|
|
791
|
+
}
|
|
792
|
+
const root = document.getElementById('root');
|
|
793
|
+
if (!root)
|
|
794
|
+
throw new Error('缺少远程组件挂载节点');
|
|
795
|
+
createRoot(root).render(_jsx(App, {}));
|
|
796
|
+
//# sourceMappingURL=app-source.js.map
|