This commit is contained in:
2026-08-24 19:10:22 +08:00
parent b50e46ed43
commit bb2171ac28
22 changed files with 2362 additions and 31 deletions
@@ -0,0 +1,7 @@
const http=require('http');
const fs=require('fs');
const path=require('path');
http.createServer((req,res)=>{
const file=path.join(process.cwd(), req.url==='/'?'index.html':req.url);
fs.createReadStream(file).on('error',()=>{res.statusCode=404;res.end('Not found')}).pipe(res);
}).listen(3000,'127.0.0.1');
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
$ErrorActionPreference = 'Stop'
$html = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot '..\index.html')
$required = @(
'totalWeightLb:203.0',
'totalWeightLb:99.9',
"totalBoxes:'总箱数'",
"totalWeight:'总重量'",
"totalBoxes:'Total cartons'",
"totalWeight:'Total weight'",
"totalBoxes:'Total de cajas'",
"totalWeight:'Peso total'",
'pickTasks.reduce((sum,task)=>sum+Number(task.palletCount||0),0)',
'pickTasks.reduce((sum,task)=>sum+Number(task.boxes||0),0)',
'pickTasks.reduce((sum,task)=>sum+Number(task.totalWeightLb||0),0)',
'${totalWeight.toFixed(1)} lb'
)
$missing = $required | Where-Object { -not $html.Contains($_) }
if ($missing) { throw "Missing handover summary contract: $($missing -join ', ')" }
$handoverSource = [regex]::Match($html, '(?s)function handoverView\(\).*?(?=function handleHandoverPhoto)').Value
if ($handoverSource.Contains('<span>${t(''boxes'')}</span>')) {
throw 'Handover still uses the generic cartons label'
}
Write-Output 'PASS: handover summary source contract'
@@ -0,0 +1,149 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
const match = html.match(/<script>([\s\S]*?)<\/script>/);
assert.ok(match, 'inline application script is missing');
const applicationSource = match[1].replace(/\s*render\(\);\s*$/, '');
const tests = [];
const runtimeTests = String.raw`
render=()=>{};
showModal=(message,onConfirm)=>{globalThis.__lastModal={message,onConfirm}};
closeModal=()=>{};
toast=()=>{};
function test(name,fn){tests.push({name,fn})}
test('consolidation ignores dropship pallet lookup and clears type inputs',()=>{
state.palletOrderType='dropship';
state.fbaPalletLabels=['D4444-TC-001'];
state.dropshipPalletLabel='D2222-TC-001';
state.palletTypeMessage={type:'error',text:'stale'};
state.fbaPalletMessage={type:'error',text:'stale'};
state.dropshipScanMessage={type:'error',text:'stale'};
startConsolidation();
assert.deepEqual(state.fbaPalletLabels,[]);
assert.equal(state.dropshipPalletLabel,'');
assert.equal(state.palletTypeMessage,null);
assert.equal(state.fbaPalletMessage,null);
assert.equal(state.dropshipScanMessage,null);
state.pallet={id:'DRAFT',mode:'consolidate',planId:null,targetPallets:null,destinationCode:'',destinationAddress:'',items:[]};
document.__input='D2222-TC-001';
handlePalletScan();
assert.equal(state.pallet.items.length,0,'consolidation must not import an existing pallet');
assert.equal(state.dropshipPalletLabel,'');
});
test('starting and selecting jobs reset type-specific inputs',()=>{
state.palletMode='palletize';
state.selectedPlanId='INB-260819-026';
state.fbaPalletLabels=['D4444-TC-001'];
state.dropshipPalletLabel='D2222-TC-001';
state.fbaPalletMessage={type:'error',text:'stale'};
state.dropshipScanMessage={type:'error',text:'stale'};
startPallet();
assert.deepEqual(state.fbaPalletLabels,[]);
assert.equal(state.dropshipPalletLabel,'');
assert.equal(state.fbaPalletMessage,null);
assert.equal(state.dropshipScanMessage,null);
state.fbaPalletLabels=['D4444-TC-002'];
state.dropshipPalletLabel='D2222-TC-001';
selectPallet('D4444-TC-003','INB-260819-026');
assert.deepEqual(state.fbaPalletLabels,[]);
assert.equal(state.dropshipPalletLabel,'');
state.fbaPalletLabels=['D4444-TC-004'];
state.dropshipPalletLabel='D2222-TC-001';
document.__input='BX-88401-01';
handlePalletTopScan();
assert.deepEqual(state.fbaPalletLabels,[]);
assert.equal(state.dropshipPalletLabel,'');
state.fbaPalletLabels=['D4444-TC-005'];
state.dropshipPalletLabel='D2222-TC-001';
startNewConsolidation();
assert.deepEqual(state.fbaPalletLabels,[]);
assert.equal(state.dropshipPalletLabel,'');
state.fbaPalletLabels=['D4444-TC-006'];
state.dropshipPalletLabel='D2222-TC-001';
reconsolidatePallet('D2222-TC-019');
assert.deepEqual(state.fbaPalletLabels,[]);
assert.equal(state.dropshipPalletLabel,'');
});
test('FBA selector disables occupied labels and every label for persisted pallets',()=>{
state.palletOrderType='fba';
state.pallet={id:'',mode:'palletize',planId:'INB-260819-018',items:[],persisted:false};
let markup=fbaPalletLabelSelector(plans[0]);
assert.match(markup,/D2222-TC-001[\s\S]*?<\/button>/);
const occupiedButton=markup.match(/<button[^>]*>[\s\S]*?D2222-TC-001[\s\S]*?<\/button>/)?.[0]||'';
assert.match(occupiedButton,/disabled/);
state.pallet.persisted=true;
markup=fbaPalletLabelSelector(plans[0]);
const buttons=[...markup.matchAll(/<button[^>]*class="fba-pallet-choice[^>]*>/g)].map(match=>match[0]);
assert.ok(buttons.length>1);
assert.ok(buttons.every(button=>button.includes('disabled')));
});
test('FBA and dropship validation messages render in their owning modules',()=>{
state.palletMode='palletize';
state.selectedPlanId='INB-260819-026';
state.pallet={id:'',mode:'palletize',planId:'INB-260819-026',destinationCode:'',destinationAddress:'',items:[]};
state.fbaPalletMessage={type:'error',text:'FBA_MESSAGE'};
const selector=fbaPalletLabelSelector(plans[2]);
assert.match(selector,/fba-pallet-selector[\s\S]*FBA_MESSAGE/);
state.palletOrderType='dropship';
state.fbaPalletMessage=null;
state.dropshipScanMessage={type:'error',text:'DROPSHIP_MESSAGE'};
const view=palletView();
const scanStart=view.indexOf('pallet-type-scan');
const scanEnd=view.indexOf('</section>',scanStart);
assert.ok(scanStart>=0 && view.slice(scanStart,scanEnd).includes('DROPSHIP_MESSAGE'));
});
test('FBA submission materializes every available selected label exactly once',()=>{
const plan=plans[2];
const originalPallets=state.pallets.map(pallet=>JSON.parse(JSON.stringify(pallet)));
const originalReceived=plan.receivedPallets;
const source=cargo.find(item=>item.id==='C-004');
source.palletNo='';
state.pallets=originalPallets;
state.selectedPlanId=plan.id;
state.palletMode='palletize';
state.palletOrderType='fba';
state.fbaPalletLabels=['D4444-TC-001','D4444-TC-002'];
state.pallet={id:'',mode:'palletize',planId:plan.id,targetPallets:plan.expectedPallets,destinationCode:source.destinationCode,destinationAddress:source.destinationAddress,items:[{...source}]};
submitPallet();
const created=state.pallets.filter(pallet=>['D4444-TC-001','D4444-TC-002'].includes(pallet.id));
assert.equal(created.length,2);
assert.equal(new Set(created.map(pallet=>pallet.id)).size,2);
assert.equal(created.reduce((count,pallet)=>count+pallet.items.length,0),1,'cargo must be linked once across the selected pallets');
assert.equal(source.palletNo,'D4444-TC-001');
assert.equal(plan.receivedPallets,Math.min(plan.expectedPallets,originalReceived+2));
});
for(const entry of tests){
try{entry.fn();process.stdout.write('PASS: '+entry.name+'\n')}
catch(error){process.stderr.write('FAIL: '+entry.name+'\n'+error.stack+'\n');globalThis.__failed=(globalThis.__failed||0)+1}
}
if(globalThis.__failed)throw new Error(globalThis.__failed+' runtime behavior test(s) failed');
`;
const documentStub = {
__input: '',
getElementById(id) {
if (id === 'pallet-scan' || id === 'pallet-top-scan') return { value: this.__input };
return null;
}
};
const run = new Function('assert', 'tests', 'localStorage', 'document', 'window', applicationSource + '\n' + runtimeTests);
run(assert, tests, { getItem: () => null, setItem: () => {} }, documentStub, { scrollTo: () => {} });
@@ -0,0 +1,87 @@
$ErrorActionPreference = 'Stop'
$html = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot '..\index.html')
$required = @(
'id="pallet-top-scan"',
'function handlePalletTopScan()',
"state.page='pallet'",
"t('palletLabel')",
"t('destination')",
"t('cartonCount')",
"t('palletStatus')",
"unknownPalletScan"
)
$missing = $required | Where-Object { -not $html.Contains($_) }
if ($missing) { throw "Missing pallet scan/list contract: $($missing -join ', ')" }
$requiredTypeContracts = @(
"palletOrderType:'fba'",
'fbaPalletLabels:[]',
"dropshipPalletLabel:''",
'function setPalletOrderType(type)',
'class="pallet-order-tabs"',
"setPalletOrderType('fba')",
"setPalletOrderType('overseas')",
"setPalletOrderType('dropship')"
)
$missingTypeContracts = $requiredTypeContracts | Where-Object { -not $html.Contains($_) }
if ($missingTypeContracts) { throw "Missing pallet order tabs: $($missingTypeContracts -join ', ')" }
$plansView = [regex]::Match($html, '(?s)function plansPalletView\(\).*?(?=function receivePhotoView)').Value
if (-not $plansView.Contains('pallet-top-scan')) { throw 'Pallet list does not expose a top scan field' }
if ($plansView.Contains("t('orderList')")) { throw 'Pallet list still presents the order-list label' }
if (-not $plansView.Contains("t('palletBoxSummary')")) { throw 'Pallet list summary label is missing' }
if (-not $plansView.Contains('const totalBoxes=pallets.length*5')) { throw 'Pallet list total carton count is missing' }
if (-not $plansView.Contains('${t(''palletBoxSummary'')}:${pallets.length}/${totalBoxes}')) { throw 'Pallet summary is missing the colon separator' }
if (-not $plansView.Contains("t('scanBoxSku')")) { throw 'Top scan does not use carton mark/SKU copy' }
if (-not $html.Contains('function handlePalletTopScan()')) { throw 'Top scan handler is missing' }
if (-not $html.Contains("c.boxNo===value||c.sku===value")) { throw 'Top scan does not resolve carton mark/SKU' }
$scanHandler = [regex]::Match($html, '(?s)function handlePalletTopScan\(\).*?(?=function receivePhotoView)').Value
if (-not $scanHandler.Contains("if(!value){state.palletTopMessage=null;state.palletMode='palletize';startPallet();return}")) { throw 'Empty scan-button click does not enter palletizing' }
if (-not $plansView.Contains('<table class="table pallet-list-table">')) { throw 'Pallet list is not rendered as a table' }
if ($plansView.Contains("t('viewPalletDetails')") -or $plansView.Contains("t('palletizeAction')")) { throw 'Pallet list still exposes a detail/action button' }
if ($plansView.Contains('${esc(pallet.plan.id)}') -or $plansView.Contains('${esc(pallet.container)}')) { throw 'Pallet list still exposes order/container information' }
$css = [regex]::Match($html, '(?s)<style id="pallet-list-toolbar">.*?</style>').Value
if (-not $css.Contains('.pallet-list-table{min-width:0')) { throw 'Pallet table still requires horizontal scrolling' }
if (-not $css.Contains('.pallet-list-table th,.pallet-list-table td{padding:8px 6px')) { throw 'Pallet table spacing is not compact' }
if (-not $css.Contains('.pallet-list-table tbody td:nth-child(-n+3){font-size:12px}')) { throw 'Primary pallet columns are not enlarged' }
if (-not $css.Contains('.pallet-list-table th:nth-child(1),.pallet-list-table td:nth-child(1){width:35%}')) { throw 'Pallet label column width is incorrect' }
if (-not $css.Contains('.pallet-list-table th:nth-child(2),.pallet-list-table td:nth-child(2){width:25%}')) { throw 'Destination column width is incorrect' }
if (-not $css.Contains('.pallet-list-table th:nth-child(3),.pallet-list-table td:nth-child(3){width:15%;text-align:left}')) { throw 'Carton count column width is incorrect' }
if (-not $css.Contains('.pallet-list-table th:nth-child(4),.pallet-list-table td:nth-child(4){width:25%}')) { throw 'Status column width is incorrect' }
if ($html.Contains("if(state.page==='plans'||state.page==='relabel')")) { throw 'Pallet page still injects an order count' }
$palletView = [regex]::Match($html, '(?s)function palletView\(\).*?(?=function startPallet\()').Value
$tabsAt = $palletView.IndexOf('${palletOrderTabs()}')
$progressAt = $palletView.IndexOf('pallet-progress-card')
$cargoAt = $palletView.IndexOf('${cargoInfo}')
if ($tabsAt -lt 0 -or $progressAt -lt $tabsAt -or $cargoAt -lt $progressAt) { throw 'Pallet detail module order is incorrect' }
if (-not $palletView.Contains("const showDetails=state.palletOrderType==='overseas'")) { throw 'Detail visibility is not type-driven' }
if (-not $palletView.Contains("const scanHint=state.palletOrderType==='dropship'?t('scanPalletLabel'):t('scanBoxSku')")) { throw 'Scan hint is not type-driven' }
if (-not $palletView.Contains("t('cargoInfo')")) { throw 'Pallet detail title is not cargo information' }
if (-not $palletView.Contains("t('inboundCode')")) { throw 'Pallet detail does not show inbound code' }
if (-not $palletView.Contains("t('cartonCount')")) { throw 'Pallet detail does not show carton count' }
if ($palletView.Contains("t('palletNo')")) { throw 'Pallet detail still shows pallet number' }
if ($palletView.Contains("t('destinationAddress')")) { throw 'Pallet detail still shows destination address' }
if ($palletView.Contains("t('pieces')")) { throw 'Pallet detail still shows pieces' }
$fbaContracts = @(
'function fbaPalletLabelSelector(plan)',
'function toggleFbaPalletLabel(palletId)',
'state.fbaPalletLabels.includes(palletId)',
"state.palletOrderType==='fba'&&!state.fbaPalletLabels.length"
)
$missingFba = $fbaContracts | Where-Object { -not $html.Contains($_) }
if ($missingFba) { throw "Missing FBA multi-select behavior: $($missingFba -join ', ')" }
$dropshipContracts = @(
"state.palletOrderType==='dropship'",
'state.dropshipPalletLabel=pallet.id',
"state.palletOrderType==='dropship'&&!state.dropshipPalletLabel",
"state.palletOrderType!=='dropship'"
)
$missingDropship = $dropshipContracts | Where-Object { -not $html.Contains($_) }
if ($missingDropship) { throw "Missing dropship scan behavior: $($missingDropship -join ', ')" }
Write-Output 'PASS: pallet scan/list source contract'
@@ -0,0 +1,27 @@
$ErrorActionPreference = 'Stop'
$html = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot '..\index.html')
$required = @(
'selectedRelabelId:null',
'relabelPhotos:{}',
"state.page='relabel-photo'",
'function relabelPhotoView()',
'function openRelabelPhoto(planId)',
'function handleRelabelPhoto(input)',
'function submitRelabelPhoto()',
'capture="environment"',
"state.page='relabel'"
)
$missing = $required | Where-Object { -not $html.Contains($_) }
if ($missing) { throw "Missing relabel photo page contract: $($missing -join ', ')" }
$relabelView = [regex]::Match($html, '(?s)function relabelView\(\).*?(?=function openRelabelPhoto)').Value
if (-not $relabelView.Contains("onclick=`"openRelabelPhoto('")) { throw 'Relabel list does not open the photo page' }
if ($relabelView.Contains('capture="environment"')) { throw 'Relabel list still contains an inline camera input' }
$photoView = [regex]::Match($html, '(?s)function relabelPhotoView\(\).*?(?=function handleRelabelPhoto)').Value
if (-not $photoView.Contains("!photo?'disabled':''")) { throw 'Photo submit button is not gated by a captured photo' }
if (-not $photoView.Contains("'relabel'")) { throw 'Photo page does not return to the relabel list' }
Write-Output 'PASS: relabel photo page source contract'
+334 -14
View File
@@ -6,6 +6,8 @@
<title>货代SaaS平台 - 虫虫出海</title>
<link rel="icon" href="data:,"/>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js"></script>
<script src="prototype-src/vendor/xlsx.full.min.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
@keyframes gradientShift{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
@@ -89,6 +91,25 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Helv
#page-gen-exchange-rate .table-toolbar{gap:12px;flex-wrap:wrap}
@media(max-width:1200px){#page-gen-exchange-rate .exchange-rate-summary{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:640px){#page-gen-exchange-rate .exchange-rate-summary{grid-template-columns:1fr}}
#page-gen-tax-rate .table-toolbar{gap:12px;flex-wrap:wrap}
#page-gen-tax-rate .tax-rate-table th,#page-gen-tax-rate .tax-rate-table td{white-space:nowrap;vertical-align:middle}
#page-gen-tax-rate .tax-rate-table th:nth-child(1){width:150px}
#page-gen-tax-rate .tax-rate-table th:nth-child(2){width:220px}
#page-gen-tax-rate .tax-rate-table th:nth-child(3){width:210px}
#page-gen-tax-rate .tax-rate-table th:nth-child(6){width:110px}
#page-gen-tax-rate .tax-rate-thumb{width:72px;height:48px;object-fit:cover;border:1px solid #e2e8f0;border-radius:6px;background:#f8fafc;display:block}
#page-gen-tax-rate .tax-rate-actions{display:flex;gap:6px;align-items:center;white-space:nowrap}
#page-gen-tax-rate .tax-rate-actions .btn{padding:6px 10px;font-size:12px}
#modal-tax-rate-edit .tax-rate-form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px 18px}
#modal-tax-rate-edit .tax-rate-form-grid .full{grid-column:1/-1}
#modal-tax-rate-edit .tax-rate-form-grid input,#modal-tax-rate-edit .tax-rate-form-grid textarea{width:100%;box-sizing:border-box}
#modal-tax-rate-edit .tax-rate-image-preview{width:110px;height:72px;object-fit:cover;border:1px solid #e2e8f0;border-radius:6px;background:#f8fafc;display:block;margin-top:8px}
#modal-tax-rate-import .tax-rate-import-summary{padding:10px 12px;margin:14px 0;background:#f8fafc;border:1px solid #e2e8f0;border-radius:7px;color:#475569;font-size:13px}
#modal-tax-rate-import .tax-rate-import-preview{overflow:auto;border:1px solid #e2e8f0;border-radius:7px;max-height:330px}
#modal-tax-rate-import .tax-rate-import-preview table{min-width:1000px;font-size:12px}
#modal-tax-rate-import .tax-rate-import-preview td,#modal-tax-rate-import .tax-rate-import-preview th{padding:8px 10px}
#modal-tax-rate-import .tax-rate-import-error{color:#dc2626}
@media(max-width:640px){#modal-tax-rate-edit .tax-rate-form-grid{grid-template-columns:1fr}#modal-tax-rate-edit .tax-rate-form-grid .full{grid-column:auto}}
/* CHART */
.chart-row{display:grid;grid-template-columns:2fr 1fr;gap:20px;margin-bottom:20px}
.chart-row.equal{grid-template-columns:1fr 1fr}
@@ -1208,6 +1229,10 @@ body.dashboard-fullscreen #page-dashboard table:has(>thead)>tbody>tr>td{backgrou
.ct-pallet-label-code,.ct-pallet-label-destination{width:100%;max-width:100%;margin:0;text-align:center;font-weight:900;line-height:.95;letter-spacing:0;overflow-wrap:anywhere;word-break:break-word}
.ct-pallet-label-code{font-size:var(--ct-pallet-code-size)}
.ct-pallet-label-destination{font-size:var(--ct-pallet-destination-size)}
.ct-pallet-label-barcode-wrap{width:100%;height:30mm;display:flex;align-items:center;justify-content:center;flex:0 0 30mm}
.ct-pallet-label-barcode{display:block;max-width:250mm;width:auto;height:28mm}
.ct-pallet-label-barcode-error{font-size:18px;font-weight:600;color:#b42318;line-height:30mm}
.ct-pallet-label-barcode[hidden],.ct-pallet-label-barcode-error[hidden]{display:none}
.ct-pallet-label-empty{width:min(520px,calc(100vw - 48px));padding:28px;text-align:center;color:#c8cdd1;border:1px solid #454b50;border-radius:6px;background:#252a2e;box-sizing:border-box}
@media(max-width:900px){
.ct-pallet-preview-toolbar{align-items:flex-start;gap:10px;padding:10px 12px;flex-wrap:wrap}
@@ -1375,6 +1400,7 @@ body.dashboard-fullscreen #page-dashboard table:has(>thead)>tbody>tr>td{backgrou
<div class="sub-menu-item" onclick="switchPage('gen-product',this)">产品信息</div>
<div class="sub-menu-item" onclick="switchPage('gen-fee',this)">费用项目</div>
<div class="sub-menu-item" onclick="switchPage('gen-exchange-rate',this)">汇率管理</div>
<div class="sub-menu-item" onclick="switchPage('gen-tax-rate',this)">税率查询表</div>
<div class="sub-menu-item" onclick="switchPage('gen-owned-warehouse',this)">仓库管理</div>
<div class="sub-menu-item" onclick="switchPage('gen-overseas-quote',this)">海外仓报价</div>
<div class="sub-menu-item" onclick="switchPage('gen-voyage',this)">船期价格表</div>
@@ -4249,6 +4275,14 @@ FORM: 已建立后台列表页的精确扩展;本次需求已明确,因此
</div>
</div>
<div class="page" id="page-gen-tax-rate">
<div class="table-card">
<div class="table-toolbar"><div><span class="title">税率查询表</span><span style="font-size:12px;color:#94a3b8;margin-left:10px">按 HS 与品名查询基础关税、附加税</span></div><div class="toolbar-actions"><button class="btn btn-primary" onclick="openTaxRateModal()">+ 新增</button><button class="btn btn-default" onclick="openTaxRateImportModal()">批量导入</button><button class="btn btn-default" onclick="exportTaxRates()">导出</button></div></div>
<div class="search-row" aria-label="税率查询条件"><input id="tax-rate-hs-filter" placeholder="HS 编码" style="width:180px"/><input id="tax-rate-cn-filter" placeholder="中文品名" style="width:180px"/><input id="tax-rate-en-filter" placeholder="英文品名" style="width:180px"/><button class="btn btn-sm btn-primary" onclick="renderTaxRates()">查询</button><button class="btn btn-sm btn-default" onclick="resetTaxRateFilters()">重置</button></div>
<div style="overflow-x:auto"><table class="tax-rate-table" style="min-width:1280px"><thead><tr><th>HS</th><th>中文品名</th><th>英文品名</th><th>价格</th><th>材质</th><th>产品图片</th><th>基础关税</th><th>附加税</th><th>操作</th></tr></thead><tbody id="tbody-gen-tax-rate"></tbody></table></div>
</div>
</div>
<!-- ==================== WAREHOUSE MANAGEMENT ==================== -->
<div class="page" id="page-outbound-vehicle">
<div class="breadcrumb">首页 <span class="sep">&gt;</span> 出库 <span class="sep">&gt;</span> <strong>排车列表</strong></div>
@@ -8795,12 +8829,28 @@ FORM: 通用模块中的管理清单加分区编辑弹窗,作为既有后台
#page-fba-inbound-order .fbio-list-action-buttons .btn{min-width:76px}
.fbd-inbound-fee-table th,.fbd-inbound-fee-table td{text-align:center!important}
.fbd-receivable-fee-table th,.fbd-receivable-fee-table td{text-align:center!important}
.fbd-inbound-ops-grid{margin-top:16px;align-items:start}
#page-fba-inbound-order-detail .fbd-body{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px 20px}
#page-fba-inbound-order-detail #fbd-inbound-top-content{grid-column:1/-1}
#page-fba-inbound-order-detail .fbd-inbound-pallet-grid{grid-column:1;margin-top:0}
#page-fba-inbound-order-detail #fbd-inbound-appointment-content{grid-column:2;margin-top:0}
#page-fba-inbound-order-detail .fbd-inbound-pallet-grid{grid-template-columns:1fr}
#page-fba-inbound-order-detail #fbd-inbound-appointment-content{grid-template-columns:1fr}
#page-fba-inbound-order-detail .fbd-section-wide{grid-column:1/-1;margin-top:0}
.fbd-inbound-pallet-table{min-width:540px;table-layout:fixed}
.fbd-inbound-pallet-table th,.fbd-inbound-pallet-table td{white-space:normal;overflow-wrap:anywhere;word-break:break-word;vertical-align:middle}
.fbd-inbound-pallet-table th:nth-child(1),.fbd-inbound-pallet-table td:nth-child(1){width:140px}
.fbd-inbound-pallet-table th:nth-child(2),.fbd-inbound-pallet-table td:nth-child(2){width:130px}
.fbd-inbound-pallet-table th:nth-child(3),.fbd-inbound-pallet-table td:nth-child(3){width:62px}
.fbd-inbound-pallet-table td:nth-child(3){text-align:left!important}
.fbd-inbound-pallet-table th:nth-child(4),.fbd-inbound-pallet-table td:nth-child(4){width:220px;white-space:nowrap;overflow-wrap:normal;word-break:normal}
.fbd-photo-link{display:inline-block;max-width:none;padding:0;border:0;background:transparent;color:#1677ff;text-align:left;text-decoration:underline;cursor:pointer;white-space:nowrap;overflow-wrap:normal;word-break:normal}
.fbio-edit-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px 20px}
.fbio-edit-grid .form-group{min-width:0;margin:0}
.fbio-edit-grid .form-group input,.fbio-edit-grid .form-group select{width:100%;box-sizing:border-box}
.fbio-edit-grid .lmo-checkbox-option input{width:16px;flex:0 0 16px}
.fbio-readonly-input{background:#f5f7fa!important;color:#64748b!important;cursor:not-allowed}
@media(max-width:720px){#page-fba-inbound-order .fbio-list-actions{align-items:flex-start;flex-direction:column;padding:10px 14px}#page-fba-inbound-order .fbio-list-action-buttons{width:100%;justify-content:flex-start}.fbio-edit-grid{grid-template-columns:1fr}}
@media(max-width:720px){#page-fba-inbound-order .fbio-list-actions{align-items:flex-start;flex-direction:column;padding:10px 14px}#page-fba-inbound-order .fbio-list-action-buttons{width:100%;justify-content:flex-start}.fbio-edit-grid{grid-template-columns:1fr}#page-fba-inbound-order-detail .fbd-body{grid-template-columns:1fr}#page-fba-inbound-order-detail .fbd-inbound-pallet-grid,#page-fba-inbound-order-detail #fbd-inbound-appointment-content{grid-column:1}}
</style>
<div class="page" id="page-fba-inbound-order">
<div class="breadcrumb">首页 <span class="sep">&gt;</span> 海外仓 <span class="sep">&gt;</span> <strong>入库单</strong></div>
@@ -9001,9 +9051,11 @@ FORM: 通用模块中的管理清单加分区编辑弹窗,作为既有后台
<!-- ==================== OVERSEAS WAREHOUSE - 入库单详情 ==================== -->
<div class="page" id="page-fba-inbound-order-detail">
<div class="breadcrumb">首页 <span class="sep">&gt;</span> 海外仓 <span class="sep">&gt;</span> 入库单 <span class="sep">&gt;</span> <strong>详情</strong></div>
<div class="table-card fbd-card"><div class="fbd-head"><div><div class="fbd-title">入库单详情</div><div class="fbd-subtitle">入库单号 <strong id="fbd-inbound-no">—</strong></div></div><div class="fbd-actions"><span id="fbd-inbound-status" class="tag tag-gray">—</span><button class="btn btn-default" onclick="backToFbaInboundOrders()">返回</button></div></div><div class="fbd-body"><div class="fbd-grid" id="fbd-inbound-content"></div><section class="fbd-section fbd-section-wide" aria-labelledby="fbd-inbound-fee-title"><div class="fbd-section-title" id="fbd-inbound-fee-title">应收费用明细</div><div class="fbd-table-wrap"><table class="fbd-detail-table fbd-inbound-fee-table"><thead><tr><th style="width:70px">序号</th><th>费用项目</th><th style="width:100px">币种</th><th style="width:140px">金额</th><th>备注</th></tr></thead><tbody id="fbd-inbound-fees"><tr><td colspan="5" class="fbd-empty">暂无应收费用明细</td></tr></tbody><tfoot><tr><td colspan="3" style="padding:12px;font-weight:700;color:#374151">应收费用合计(USD)</td><td id="fbd-inbound-fee-total" style="padding:12px;font-weight:700;color:#111827">$0.00</td><td></td></tr></tfoot></table></div></section></div></div>
<div class="table-card fbd-card"><div class="fbd-head"><div><div class="fbd-title">入库单详情</div><div class="fbd-subtitle">入库单号 <strong id="fbd-inbound-no">—</strong></div></div><div class="fbd-actions"><span id="fbd-inbound-status" class="tag tag-gray">—</span><button class="btn btn-default" onclick="backToFbaInboundOrders()">返回</button></div></div><div class="fbd-body"><div class="fbd-grid" id="fbd-inbound-top-content"></div><div class="fbd-grid fbd-inbound-pallet-grid"><section class="fbd-section" aria-labelledby="fbd-inbound-pallet-title"><div class="fbd-section-title" id="fbd-inbound-pallet-title">托盘明细</div><div class="fbd-table-wrap"><table class="fbd-detail-table fbd-inbound-pallet-table"><thead><tr><th>托盘号</th><th>产品名称</th><th style="width:100px">箱数</th><th style="width:190px">附件名称(打托照片)</th></tr></thead><tbody id="fbd-inbound-pallet-details"><tr><td colspan="4" class="fbd-empty">暂无托盘明细</td></tr></tbody></table></div></section></div><div class="fbd-grid" id="fbd-inbound-appointment-content"></div><section class="fbd-section fbd-section-wide" aria-labelledby="fbd-inbound-fee-title"><div class="fbd-section-title" id="fbd-inbound-fee-title">应收费用明细</div><div class="fbd-table-wrap"><table class="fbd-detail-table fbd-inbound-fee-table"><thead><tr><th style="width:70px">序号</th><th>费用项目</th><th style="width:100px">币种</th><th style="width:140px">金额</th><th>备注</th></tr></thead><tbody id="fbd-inbound-fees"><tr><td colspan="5" class="fbd-empty">暂无应收费用明细</td></tr></tbody><tfoot><tr><td colspan="3" style="padding:12px;font-weight:700;color:#374151">应收费用合计(USD)</td><td id="fbd-inbound-fee-total" style="padding:12px;font-weight:700;color:#111827">$0.00</td><td></td></tr></tfoot></table></div></section></div></div>
</div>
<div class="modal-overlay" id="modal-fbd-inbound-photo-preview" role="dialog" aria-modal="true" aria-labelledby="fbd-inbound-photo-preview-caption"><div class="modal" style="max-width:900px;width:94%;padding:0;overflow:hidden"><div class="modal-header"><span id="fbd-inbound-photo-preview-caption">打托照片</span><button type="button" aria-label="关闭照片预览" onclick="closeFbaInboundPalletPhoto()" style="border:0;background:transparent;font-size:22px;line-height:1;cursor:pointer;color:#64748b">&times;</button></div><div class="modal-body" style="display:flex;align-items:center;justify-content:center;min-height:360px;padding:18px;background:#f8fafc"><img id="fbd-inbound-photo-preview-image" alt="打托照片预览" style="display:block;max-width:100%;max-height:68vh;object-fit:contain;border-radius:6px;box-shadow:0 8px 24px rgba(15,23,42,.16)"/></div><div class="modal-footer"><button type="button" class="btn btn-default" onclick="closeFbaInboundPalletPhoto()">关闭</button></div></div></div>
<!-- ==================== OVERSEAS WAREHOUSE - 出库单详情 ==================== -->
<div class="page" id="page-fba-outbound-order-detail">
<div class="breadcrumb">首页 <span class="sep">&gt;</span> 海外仓 <span class="sep">&gt;</span> 出库单 <span class="sep">&gt;</span> <strong>详情</strong></div>
@@ -10425,7 +10477,7 @@ FORM: 采用现有后台的列表加标准详情结构,保持高信息密度
<div><span style="display:block;color:#6b7280;font-size:12px;margin-bottom:3px">完成时间</span><span id="customer-followup-completed-at" style="font-size:13px;color:#374151">—</span></div>
<div style="grid-column:1/-1"><span style="display:block;color:#6b7280;font-size:12px;margin-bottom:3px">目的地</span><span id="customer-followup-destination" style="font-size:13px;color:#374151">—</span></div>
</div>
<div class="form-group"><label>回访结果 <span style="color:#ef4444">*</span></label><select id="customer-followup-result"><option value="">请选择回访结果</option><option value="满意">满意</option><option value="一般">一般</option><option value="不满意">不满意</option><option value="未接通">未接通</option></select></div>
<div class="form-group"><label>回访结果 <span style="color:#ef4444">*</span></label><select id="customer-followup-result"><option value="">请选择回访结果</option><option value="已接收">已接收</option><option value="未接收">未接收</option><option value="不清楚">不清楚</option></select></div>
<div class="form-group"><label>客户反馈</label><textarea id="customer-followup-feedback" rows="3" maxlength="500" placeholder="记录客户对派送服务的反馈"></textarea></div>
<div class="form-group"><label>备注</label><textarea id="customer-followup-note" rows="2" maxlength="300" placeholder="补充后续跟进事项"></textarea></div>
<div class="form-group"><label>回访资料</label><div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap"><label class="dispatch-doc-upload-label" style="font-size:12px">选择文件<input id="customer-followup-files" type="file" multiple accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.webp" style="display:none" onchange="handleCustomerFollowupFiles(this)"/></label><span style="font-size:12px;color:#9ca3af">支持 PDF、Office、JPG、PNG、WEBP</span></div><div id="customer-followup-file-list" style="display:flex;flex-direction:column;gap:6px;margin-top:8px"></div></div>
@@ -11012,6 +11064,43 @@ FORM: 采用现有后台的列表加标准详情结构,保持高信息密度
</div>
<div class="modal-footer"><button type="button" class="btn btn-default" onclick="hideModal('modal-ct-abnormal')">取消</button><button type="button" class="btn btn-danger" id="ct-abnormal-submit" onclick="saveCtAbnormalState()">确认标记</button></div></div></div>
<!-- ==================== 货柜更多操作 MODAL ==================== -->
<div class="modal-overlay" id="modal-ct-more">
<div class="modal" style="max-width:420px">
<div class="modal-header"><span>更多操作</span><button type="button" aria-label="关闭" onclick="hideModal('modal-ct-more')" style="display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:0;border-radius:6px;background:transparent;color:#64748b;font-size:22px;cursor:pointer">&times;</button></div>
<div class="modal-body">
<div style="padding:12px 14px;margin-bottom:14px;border:1px solid #e8edf3;border-radius:8px;background:#f8fafc;font-size:13px;color:#64748b">当前货柜:<strong id="ct-more-container" style="color:#1f2937">—</strong></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<button type="button" class="btn btn-default" onclick="showToast('货柜详情功能开发中','info')">详情</button>
<button type="button" class="btn btn-default" onclick="showToast('货柜跟踪功能开发中','info')">跟踪</button>
<button type="button" class="btn btn-default" onclick="showToast('货柜日志功能开发中','info')">日志</button>
</div>
</div>
<div class="modal-footer"><button type="button" class="btn btn-default" onclick="hideModal('modal-ct-more')">关闭</button></div>
</div>
</div>
<!-- ==================== 货柜管理资料 MODAL ==================== -->
<div class="modal-overlay" id="modal-ct-materials">
<div class="modal" style="width:760px;max-width:94vw">
<div class="modal-header"><span id="ct-materials-title">管理资料</span><button type="button" aria-label="关闭" onclick="hideModal('modal-ct-materials')" style="display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:0;border-radius:6px;background:transparent;color:#64748b;font-size:22px;cursor:pointer">&times;</button></div>
<div class="modal-body">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;margin-bottom:16px;border:1px solid #dbeafe;border-radius:8px;background:#eff6ff">
<div><div style="font-size:12px;color:#64748b;margin-bottom:3px">集装箱号</div><strong id="ct-materials-container" style="font-size:15px;color:#1e3a8a">—</strong></div>
<div id="ct-materials-summary" style="font-size:13px;color:#2563eb">已接收 0 / 3 类</div>
</div>
<div style="border:1px solid #e5e7eb;border-radius:8px;overflow:hidden">
<table style="width:100%;border-collapse:collapse;font-size:12px">
<thead style="background:#f8fafc"><tr><th style="padding:10px;text-align:left">资料类型</th><th style="padding:10px;text-align:left">接收状态</th><th style="padding:10px;text-align:left">文件</th><th style="padding:10px;text-align:left">接收时间</th><th style="padding:10px;text-align:left">来源</th><th style="padding:10px;text-align:center;width:150px">操作</th></tr></thead>
<tbody id="ct-materials-body"></tbody>
</table>
</div>
<div style="margin-top:12px;font-size:12px;color:#94a3b8">PDA 推送资料会自动归档到当前货柜;照片仅用于现场留档与后续核验。</div>
</div>
<div class="modal-footer"><button type="button" class="btn btn-default" onclick="hideModal('modal-ct-materials')">关闭</button></div>
</div>
</div>
<!-- ==================== 新增货柜费用 MODAL ==================== -->
<div class="modal-overlay" id="modal-ct-cost-add">
<div class="modal" style="max-width:520px"><div class="modal-header">新增货柜费用<span onclick="hideModal('modal-ct-cost-add')" style="cursor:pointer;font-size:20px">&times;</span></div>
@@ -12118,6 +12207,31 @@ FORM: 采用现有后台的列表加标准详情结构,保持高信息密度
</div>
</div>
<!-- 通用:税率新增/编辑 -->
<div class="modal-overlay" id="modal-tax-rate-edit">
<div class="modal" style="width:720px;max-width:calc(100vw - 32px)">
<div class="modal-header"><span id="tax-rate-modal-title">新增税率</span><span onclick="hideModal('modal-tax-rate-edit')" style="cursor:pointer;font-size:20px" aria-label="关闭">&times;</span></div>
<div class="modal-body"><div class="tax-rate-form-grid">
<div class="form-group"><label><span style="color:#ff4d4f">*</span> HS</label><input id="tax-rate-form-hs" placeholder="请输入 HS 编码"/></div>
<div class="form-group"><label><span style="color:#ff4d4f">*</span> 中文品名</label><input id="tax-rate-form-cn" placeholder="请输入中文品名"/></div>
<div class="form-group"><label><span style="color:#ff4d4f">*</span> 英文品名</label><input id="tax-rate-form-en" placeholder="请输入英文品名"/></div>
<div class="form-group"><label><span style="color:#ff4d4f">*</span> 价格</label><input id="tax-rate-form-price" type="number" min="0" step="0.01" placeholder="请输入价格"/></div>
<div class="form-group"><label><span style="color:#ff4d4f">*</span> 材质</label><input id="tax-rate-form-material" placeholder="请输入材质"/></div>
<div class="form-group"><label><span style="color:#ff4d4f">*</span> 基础关税</label><input id="tax-rate-form-basic-tax" placeholder="如 Free、28.2%"/></div>
<div class="form-group"><label>附加税</label><input id="tax-rate-form-additional-tax" placeholder="选填,如 7.5%"/></div>
<div class="form-group"><label>产品图片 URL</label><input id="tax-rate-form-image" placeholder="https://example.com/image.jpg" oninput="updateTaxRateImagePreview()"/><img id="tax-rate-form-image-preview" class="tax-rate-image-preview" alt="产品图片预览"/></div>
</div></div>
<div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-tax-rate-edit')">取消</button><button class="btn btn-primary" onclick="saveTaxRate()">保存</button></div>
</div></div>
<!-- 通用:税率批量导入 -->
<div class="modal-overlay" id="modal-tax-rate-import">
<div class="modal" style="width:980px;max-width:calc(100vw - 32px)">
<div class="modal-header"><span>批量导入税率</span><span onclick="hideModal('modal-tax-rate-import')" style="cursor:pointer;font-size:20px" aria-label="关闭">&times;</span></div>
<div class="modal-body"><div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap"><div><div style="font-size:13px;font-weight:650;color:#334155">选择 Excel 文件</div><div style="font-size:12px;color:#8491a3;margin-top:4px">仅支持 .xlsx;任一行校验失败时整批拒绝。</div></div><div style="display:flex;align-items:center;gap:8px"><button class="btn btn-default btn-sm" onclick="downloadTaxRateTemplate()">下载模板</button><input id="tax-rate-import-file" type="file" accept=".xlsx" onchange="handleTaxRateImportFile(this)"/></div></div><div id="tax-rate-import-summary" class="tax-rate-import-summary">尚未选择文件</div><div class="tax-rate-import-preview"><table><thead><tr><th>行号</th><th>校验</th><th>HS</th><th>中文品名</th><th>英文品名</th><th>价格</th><th>材质</th><th>产品图片</th><th>基础关税</th><th>附加税</th></tr></thead><tbody id="tbody-tax-rate-import-preview"><tr><td colspan="10" style="padding:32px;text-align:center;color:#94a3b8">选择文件后显示前 10 条数据</td></tr></tbody></table></div></div>
<div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-tax-rate-import')">取消</button><button id="tax-rate-import-confirm" class="btn btn-primary" onclick="confirmTaxRateImport()" disabled>确认导入</button></div>
</div></div>
<!-- 应收款统计表:编辑明细 -->
<div class="modal-overlay" id="modal-receivable-report-edit">
<div class="modal" style="width:560px;max-width:calc(100vw - 32px)">
@@ -12417,6 +12531,19 @@ const exchangeRateData=[
{month:'2026-05',averageRate:7.2095,updatedBy:'李芳',updatedAt:'2026-06-03 14:20',remark:'2026年5月平均汇率'},
{month:'2025-12',averageRate:7.1023,updatedBy:'张经理',updatedAt:'2026-01-05 09:05',remark:'2025年12月平均汇率'},
];
const taxRateData=[
{id:'TR001',hs:'9503000090',cnName:'塑料玩具/塑料充气装饰品',enName:'TOY',price:'0.85',material:'塑胶',imageUrl:'',imageLabel:'TOY',imageColor:'#f59e0b',basicTax:'Free',additionalTax:''},
{id:'TR002',hs:'9503000090',cnName:'塑料玩具',enName:'TOY',price:'0.85',material:'塑料',imageUrl:'',imageLabel:'TOY',imageColor:'#22c55e',basicTax:'Free',additionalTax:''},
{id:'TR003',hs:'6104632011',cnName:'瑜伽裤',enName:'Yoga pants',price:'0.40',material:'涤纶',imageUrl:'',imageLabel:'YOGA',imageColor:'#64748b',basicTax:'28.2%',additionalTax:'7.5%'},
{id:'TR004',hs:'9404904000',cnName:'抱枕套',enName:'PILLOW COVER',price:'0.30',material:'涤纶',imageUrl:'',imageLabel:'HOME',imageColor:'#ec4899',basicTax:'12%',additionalTax:'5%'},
{id:'TR005',hs:'4421999090',cnName:'DIY木制工艺品',enName:'CRAFTS',price:'0.56',material:'杉木',imageUrl:'',imageLabel:'WOOD',imageColor:'#a16207',basicTax:'Free',additionalTax:''},
{id:'TR006',hs:'7323990000',cnName:'锅架',enName:'PAN ORGANIZER',price:'1.00',material:'不锈钢',imageUrl:'',imageLabel:'PAN',imageColor:'#0f766e',basicTax:'2.6%',additionalTax:''},
{id:'TR007',hs:'3924900000',cnName:'刮铲',enName:'SILICONE SPATULA',price:'1.00',material:'硅胶',imageUrl:'',imageLabel:'KITCHEN',imageColor:'#8b5cf6',basicTax:'3.4%',additionalTax:''},
{id:'TR008',hs:'6304920000',cnName:'三合板挂饰',enName:'HANGING DECORATION',price:'0.60',material:'杉木+塑料+麻布',imageUrl:'',imageLabel:'DECOR',imageColor:'#ef4444',basicTax:'6.4%',additionalTax:''},
];
let taxRateSeq=9;
let taxRateEditingId='';
let taxRateImportRows=[];
const genOwnedWarehouseData=[
{id:'OWN001',name:'洛杉矶仓',address:'原型未定义',city:'',state:'',zip:'',country:'美国',status:'启用'},
{id:'OWN002',name:'纽约仓',address:'原型未定义',city:'',state:'',zip:'',country:'美国',status:'启用'},
@@ -13267,7 +13394,18 @@ ctListData.forEach(function(item){
if(typeof item.isAbnormal!=='boolean')item.isAbnormal=false;
if(!Array.isArray(item.abnormalLogs))item.abnormalLogs=[];
if(typeof item.abnormalReason!=='string')item.abnormalReason='';
if(!item.pdaMaterials){
item.pdaMaterials=[
{key:'licensePlate',type:'车牌照片',status:'待接收',file:'',receivedAt:'',source:''},
{key:'containerSeal',type:'集装箱封签',status:'待接收',file:'',receivedAt:'',source:''},
{key:'openingPhoto',type:'开柜照片',status:'待接收',file:'',receivedAt:'',source:''}
];
}
});
if(ctListData[0]&&ctListData[0].pdaMaterials){
Object.assign(ctListData[0].pdaMaterials[0],{status:'已接收',file:'PDA_车牌照片_FFAU6685194.jpg',receivedAt:'2026-08-13 10:25',source:'PDA'});
Object.assign(ctListData[0].pdaMaterials[1],{status:'已接收',file:'PDA_集装箱封签_FFAU6685194.jpg',receivedAt:'2026-08-13 10:26',source:'PDA'});
}
const ctCostData=[
{id:'COST001',ctNo:'FFAU6685194',type:'港口费',amount:850,currency:'USD',date:'2026-07-16',remark:'洛杉矶港港口操作费'},
{id:'COST002',ctNo:'CSNU9332868',type:'堆存费',amount:320,currency:'USD',date:'2026-07-17',remark:'堆存3天'},
@@ -13379,11 +13517,11 @@ const fbaReturnOutboundData = [
/* FBA退货 - 入库单数据 */
const fbaInboundOrderData = [
{inboundNo:'FBIO20260720001',bolNo:'BOL260720001',type:'国内订单',wh:'洛杉矶长滩海外仓',owner:'杭州积乐',totalBoxes:120,chargeVol:24.5,palletTotal:4,palletType:'标准托盘',isOutsourced:false,status:'待审核',fees:[{name:'打托费',currency:'USD',amount:120,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:280,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:350,remark:'码头提柜'}]},
{inboundNo:'FBIO20260721002',bolNo:'BOL260720002',type:'海外订单',wh:'纽约海外仓',owner:'岩盛、欧业',totalBoxes:200,chargeVol:38.0,palletTotal:6,palletType:'木托盘',isOutsourced:true,status:'已审核',fees:[{name:'打托费',currency:'USD',amount:180,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:320,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:420,remark:'码头提柜'}]},
{inboundNo:'FBIO20260722003',bolNo:'BOL260220003',type:'海外订单',wh:'汉堡海外仓',owner:'上海粤讯',totalBoxes:80,chargeVol:15.2,palletTotal:3,palletType:'纸托盘',isOutsourced:false,status:'已入库',fees:[{name:'打托费',currency:'USD',amount:95,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:240,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:310,remark:'码头提柜'}]},
{inboundNo:'FBIO20260723004',bolNo:'BOL260240004',type:'国内订单',wh:'洛杉矶长滩海外仓',owner:'义乌卓越',totalBoxes:150,chargeVol:29.8,palletTotal:5,palletType:'标准托盘',isOutsourced:true,status:'待审核',fees:[{name:'打托费',currency:'USD',amount:150,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:300,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:380,remark:'码头提柜'}]},
{inboundNo:'FBIO20260725005',bolNo:'BOL260250005',type:'国内订单',wh:'纽约海外仓',owner:'杭州积乐',totalBoxes:90,chargeVol:18.6,palletTotal:3,palletType:'木托盘',isOutsourced:false,status:'已审核',fees:[{name:'打托费',currency:'USD',amount:105,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:260,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:330,remark:'码头提柜'}]},
{inboundNo:'FBIO20260720001',bolNo:'BOL260720001',type:'国内订单',wh:'洛杉矶长滩海外仓',owner:'杭州积乐',productName:'蓝牙耳机套件',totalBoxes:120,chargeVol:24.5,palletTotal:4,palletType:'标准托盘',isOutsourced:false,status:'待审核',palletDetails:[{palletNo:'PAL-A1001-01',productName:'蓝牙耳机套件',boxes:32,photoName:'打托照片_PAL-A1001-01.jpg',photo:'https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?auto=format&fit=crop&w=900&q=80'},{palletNo:'PAL-A1001-02',productName:'蓝牙耳机套件',boxes:30,photoName:'打托照片_PAL-A1001-02.jpg',photo:'https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?auto=format&fit=crop&w=900&q=80'},{palletNo:'PAL-A1001-03',productName:'蓝牙耳机套件',boxes:30,photo:''},{palletNo:'PAL-A1001-04',productName:'蓝牙耳机套件',boxes:28,photo:''}],fees:[{name:'打托费',currency:'USD',amount:120,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:280,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:350,remark:'码头提柜'}]},
{inboundNo:'FBIO20260721002',bolNo:'BOL260720002',type:'海外订单',wh:'纽约海外仓',owner:'岩盛、欧业',productName:'手机保护壳',totalBoxes:200,chargeVol:38.0,palletTotal:6,palletType:'木托盘',isOutsourced:true,status:'已审核',palletDetails:[{palletNo:'PAL-B2002-01',productName:'手机保护壳',boxes:34,photoName:'打托照片_PAL-B2002-01.jpg',photo:'https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?auto=format&fit=crop&w=900&q=80'},{palletNo:'PAL-B2002-02',productName:'手机保护壳',boxes:34,photo:''},{palletNo:'PAL-B2002-03',productName:'手机保护壳',boxes:34,photo:''},{palletNo:'PAL-B2002-04',productName:'手机保护壳',boxes:34,photo:''},{palletNo:'PAL-B2002-05',productName:'手机保护壳',boxes:32,photo:''},{palletNo:'PAL-B2002-06',productName:'手机保护壳',boxes:32,photo:''}],fees:[{name:'打托费',currency:'USD',amount:180,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:320,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:420,remark:'码头提柜'}]},
{inboundNo:'FBIO20260722003',bolNo:'BOL260220003',type:'海外订单',wh:'汉堡海外仓',owner:'上海粤讯',productName:'充电器套装',totalBoxes:80,chargeVol:15.2,palletTotal:3,palletType:'纸托盘',isOutsourced:false,status:'已入库',fees:[{name:'打托费',currency:'USD',amount:95,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:240,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:310,remark:'码头提柜'}]},
{inboundNo:'FBIO20260723004',bolNo:'BOL260240004',type:'国内订单',wh:'洛杉矶长滩海外仓',owner:'义乌卓越',productName:'LED台灯',totalBoxes:150,chargeVol:29.8,palletTotal:5,palletType:'标准托盘',isOutsourced:true,status:'待审核',fees:[{name:'打托费',currency:'USD',amount:150,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:300,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:380,remark:'码头提柜'}]},
{inboundNo:'FBIO20260725005',bolNo:'BOL260250005',type:'国内订单',wh:'纽约海外仓',owner:'杭州积乐',productName:'收纳盒组合',totalBoxes:90,chargeVol:18.6,palletTotal:3,palletType:'木托盘',isOutsourced:false,status:'已审核',fees:[{name:'打托费',currency:'USD',amount:105,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:260,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:330,remark:'码头提柜'}]},
];
const fbioQueryMeta=[
{deliveryWh:'宁波仓',warehouseCode:'RK26072001',eta:'2026-07-28',feeAudit:'已审核',containerNo:'MSCU7283910',cabinetNo:'40HQ-001',channel:'洛杉矶普船海卡',sailingDate:'2026-07-22',loaded:'是',fullContainer:'是',measureAudit:'已审核',fbaCode:'ONT8-A1001',createdDate:'2026-07-20'},
@@ -13489,7 +13627,7 @@ const dispatchBolPrintData = [
];
const staticDispatchRecords = [
{dispatchNo:'YWBHK260820001',bolNo:'BOL20260820001',status:'已签收',pickingStatus:'拣货完成',customer:'义乌市赛恩进出口有限公司',contact:'陈女士 13800138101',warehouse:'洛杉矶长滩海外仓',destFba:'FBA ONT8',expressNo:'UPS20260820001',outboundAt:'2026-08-18 09:00:00',deliveredAt:'2026-08-20 15:36',courierNodes:[{time:'2026-08-20 15:36',status:'已签收'}]},
{dispatchNo:'YWBHK260818002',bolNo:'BOL20260818002',status:'已签收',pickingStatus:'拣货完成',customer:'上海粤讯供应链有限公司',contact:'刘先生 13900139102',warehouse:'纽约海外仓',destFba:'FBA CLT2',expressNo:'FEDEX20260818002',outboundAt:'2026-08-16 13:20:00',signedAt:'2026-08-18 11:08',courierNodes:[{time:'2026-08-18 11:08',status:'已签收'}],customerFollowup:{status:'已回访',followedUpAt:'2026-08-19 10:20',operator:'王客服',result:'满意',feedback:'客户确认货物完好,派送时效符合预期。',note:'',attachments:[{fileName:'客户回访记录_0819.pdf',uploadedAt:'2026-08-19 10:18',size:186320}]}},
{dispatchNo:'YWBHK260818002',bolNo:'BOL20260818002',status:'已签收',pickingStatus:'拣货完成',customer:'上海粤讯供应链有限公司',contact:'刘先生 13900139102',warehouse:'纽约海外仓',destFba:'FBA CLT2',expressNo:'FEDEX20260818002',outboundAt:'2026-08-16 13:20:00',signedAt:'2026-08-18 11:08',courierNodes:[{time:'2026-08-18 11:08',status:'已签收'}],customerFollowup:{status:'已回访',followedUpAt:'2026-08-19 10:20',operator:'王客服',result:'已接收',feedback:'客户确认货物完好,派送时效符合预期。',note:'',attachments:[{fileName:'客户回访记录_0819.pdf',uploadedAt:'2026-08-19 10:18',size:186320}]}},
{dispatchNo:'YWBHK260815003',bolNo:'BOL20260815003',status:'已签收',pickingStatus:'拣货完成',customer:'宁波远洋供应链有限公司',contact:'孙女士 13700137103',warehouse:'汉堡海外仓',destFba:'DE Frankfurt',expressNo:'DHL20260815003',outboundAt:'2026-08-14 08:45:00',completedAt:'2026-08-15 17:42',courierNodes:[{time:'2026-08-15 17:42',status:'已签收'}]},
{dispatchNo:'YWBHK200615001',bolNo:'BOL20260615001',status:'待派送',pickingStatus:'已拣货',customer:'义乌欧业进出口有限公司',contact:'张三 13800138000',expressNo:'SF1234567890',outboundAt:'2026-06-15 14:00:00',courierNodes:[],fleetVehicleId:'VD4'},
{dispatchNo:'YWBHK200615002',bolNo:'BOL20260615002',status:'已签收',pickingStatus:'拣货完成',customer:'青岛远航贸易有限公司',contact:'李四 13900139000',expressNo:'UPS20260615002',outboundAt:'2026-06-14 16:30:00',courierNodes:[]},
@@ -13692,6 +13830,7 @@ const menuPermTree = [
{key:'gen-product',label:'产品信息'},
{key:'gen-fee',label:'费用项目'},
{key:'gen-exchange-rate',label:'汇率管理'},
{key:'gen-tax-rate',label:'税率查询表'},
{key:'gen-fba',label:'仓库管理'},
{key:'gen-download',label:'下载中心'},
{key:'gen-pallet',label:'托盘管理'},
@@ -13723,6 +13862,7 @@ roleData.forEach(role=>{
role.perms=role.perms.filter(key=>key!=='sys-tenant'&&key!=='sys-brand');
if(role.perms.includes('gen-fba')&&!role.perms.includes('gen-download'))role.perms.push('gen-download');
if(role.perms.includes('gen-fee')&&!role.perms.includes('gen-exchange-rate'))role.perms.push('gen-exchange-rate');
if(role.perms.includes('gen-exchange-rate')&&!role.perms.includes('gen-tax-rate'))role.perms.push('gen-tax-rate');
if(role.code==='SUPER_ADMIN'&&!role.perms.includes('sys-menu-permissions'))role.perms.push('sys-menu-permissions');
});
@@ -15980,6 +16120,62 @@ document.getElementById('exchange-rate-month-filter').value='';
renderExchangeRates();
}
function taxRateImageSrc(label,color){
const svg='<svg xmlns="http://www.w3.org/2000/svg" width="144" height="96" viewBox="0 0 144 96"><rect width="144" height="96" rx="10" fill="'+color+'"/><circle cx="112" cy="24" r="18" fill="rgba(255,255,255,.25)"/><path d="M20 72 52 40l18 16 16-18 38 34H20Z" fill="rgba(255,255,255,.28)"/><text x="12" y="27" fill="#fff" font-family="Arial,sans-serif" font-size="14" font-weight="700">'+label+'</text></svg>';
return 'data:image/svg+xml;charset=UTF-8,'+encodeURIComponent(svg);
}
function taxRateEscape(value){return String(value??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
function taxRateUniqueKey(item){return [item.hs,item.cnName,item.enName].map(value=>String(value||'').trim().toLowerCase()).join('|');}
function taxRateImage(item){const fallback=taxRateImageSrc(item.imageLabel||'IMAGE',item.imageColor||'#64748b');return item.imageUrl?'<img class="tax-rate-thumb" src="'+taxRateEscape(item.imageUrl)+'" data-tax-fallback="'+fallback+'" onerror="this.onerror=null;this.src=this.dataset.taxFallback" alt="'+taxRateEscape(item.cnName)+'"/>':'<img class="tax-rate-thumb" src="'+fallback+'" alt="'+taxRateEscape(item.cnName)+'"/>';}
function validateTaxRate(item,ignoreId,existing){
const required=[['HS',item.hs],['中文品名',item.cnName],['英文品名',item.enName],['材质',item.material],['基础关税',item.basicTax]];
for(const entry of required)if(!String(entry[1]||'').trim())return entry[0]+'不能为空';
if(item.price===''||item.price===null||item.price===undefined||!Number.isFinite(Number(item.price))||Number(item.price)<0)return '价格必须为大于等于 0 的数字';
if(item.imageUrl){try{new URL(item.imageUrl);}catch(error){return '产品图片必须是有效 URL';}}
const key=taxRateUniqueKey(item);if(existing.some(row=>row.id!==ignoreId&&taxRateUniqueKey(row)===key))return 'HS、中文品名、英文品名组合已存在';
return '';
}
function getFilteredTaxRates(){const filters=getTaxRateFilters();return taxRateData.filter(item=>(!filters.hs||item.hs.toLowerCase().includes(filters.hs))&&(!filters.cn||item.cnName.toLowerCase().includes(filters.cn))&&(!filters.en||item.enName.toLowerCase().includes(filters.en)));}
function getTaxRateFilters(){
return {
hs:(document.getElementById('tax-rate-hs-filter')||{}).value?.trim().toLowerCase()||'',
cn:(document.getElementById('tax-rate-cn-filter')||{}).value?.trim().toLowerCase()||'',
en:(document.getElementById('tax-rate-en-filter')||{}).value?.trim().toLowerCase()||''
};
}
function renderTaxRates(){
const tbody=document.getElementById('tbody-gen-tax-rate');
if(!tbody)return;
const rows=getFilteredTaxRates();
tbody.innerHTML=rows.length?rows.map(item=>'<tr><td><strong>'+taxRateEscape(item.hs)+'</strong></td><td>'+taxRateEscape(item.cnName)+'</td><td>'+taxRateEscape(item.enName)+'</td><td>'+taxRateEscape(item.price)+'</td><td>'+taxRateEscape(item.material)+'</td><td>'+taxRateImage(item)+'</td><td>'+taxRateEscape(item.basicTax)+'</td><td>'+taxRateEscape(item.additionalTax||'-')+'</td><td><div class="tax-rate-actions"><button class="btn btn-sm btn-default" onclick="openTaxRateModal(\''+taxRateEscape(item.id)+'\')">编辑</button><button class="btn btn-sm btn-default" style="color:#ff4d4f;border-color:#fecaca" onclick="deleteTaxRate(\''+taxRateEscape(item.id)+'\')">删除</button></div></td></tr>').join(''):'<tr><td colspan="9" style="padding:48px;text-align:center;color:#8c8c8c">暂无匹配的税率记录</td></tr>';
}
function resetTaxRateFilters(){
['tax-rate-hs-filter','tax-rate-cn-filter','tax-rate-en-filter'].forEach(id=>{const input=document.getElementById(id);if(input)input.value='';});
renderTaxRates();
}
function updateTaxRateImagePreview(){const input=document.getElementById('tax-rate-form-image');const preview=document.getElementById('tax-rate-form-image-preview');if(!preview)return;const url=input&&input.value.trim();preview.src=url||taxRateImageSrc('IMAGE','#64748b');preview.onerror=function(){this.onerror=null;this.src=taxRateImageSrc('IMAGE','#64748b');};}
function clearTaxRateForm(){['hs','cn','en','price','material','image','basic-tax','additional-tax'].forEach(key=>{const el=document.getElementById('tax-rate-form-'+key);if(el)el.value='';});updateTaxRateImagePreview();}
function openTaxRateModal(id){taxRateEditingId=id||'';const record=taxRateData.find(item=>item.id===taxRateEditingId);document.getElementById('tax-rate-modal-title').textContent=record?'编辑税率':'新增税率';clearTaxRateForm();if(record){document.getElementById('tax-rate-form-hs').value=record.hs;document.getElementById('tax-rate-form-cn').value=record.cnName;document.getElementById('tax-rate-form-en').value=record.enName;document.getElementById('tax-rate-form-price').value=record.price;document.getElementById('tax-rate-form-material').value=record.material;document.getElementById('tax-rate-form-image').value=record.imageUrl||'';document.getElementById('tax-rate-form-basic-tax').value=record.basicTax;document.getElementById('tax-rate-form-additional-tax').value=record.additionalTax||'';updateTaxRateImagePreview();}showModal('modal-tax-rate-edit');}
function readTaxRateForm(){return {hs:document.getElementById('tax-rate-form-hs').value.trim(),cnName:document.getElementById('tax-rate-form-cn').value.trim(),enName:document.getElementById('tax-rate-form-en').value.trim(),price:document.getElementById('tax-rate-form-price').value.trim(),material:document.getElementById('tax-rate-form-material').value.trim(),imageUrl:document.getElementById('tax-rate-form-image').value.trim(),basicTax:document.getElementById('tax-rate-form-basic-tax').value.trim(),additionalTax:document.getElementById('tax-rate-form-additional-tax').value.trim()};}
function saveTaxRate(){const value=readTaxRateForm();const error=validateTaxRate(value,taxRateEditingId,taxRateData);if(error){showToast(error,'warning');return;}const index=taxRateData.findIndex(item=>item.id===taxRateEditingId);const id=taxRateEditingId||'TR'+String(taxRateSeq++).padStart(3,'0');const record=Object.assign({id:id,imageLabel:'IMAGE',imageColor:'#64748b'},value);if(index>-1)taxRateData.splice(index,1,record);else taxRateData.push(record);hideModal('modal-tax-rate-edit');renderTaxRates();showToast(index>-1?'税率记录已更新':'税率记录已新增','success');}
function deleteTaxRate(id){const index=taxRateData.findIndex(item=>item.id===id);if(index<0)return;const item=taxRateData[index];if(!window.confirm('确定删除 '+item.cnName+' 的税率记录吗?'))return;taxRateData.splice(index,1);renderTaxRates();showToast('税率记录已删除','success');}
const taxRateExcelFields=[{header:'HS',key:'hs',required:true},{header:'中文品名',key:'cnName',required:true},{header:'英文品名',key:'enName',required:true},{header:'价格',key:'price',required:true},{header:'材质',key:'material',required:true},{header:'产品图片',key:'imageUrl',required:false},{header:'基础关税',key:'basicTax',required:true},{header:'附加税',key:'additionalTax',required:false}];
function taxRateExcelRows(rows){return rows.map(item=>taxRateExcelFields.reduce((row,field)=>{row[field.header]=item[field.key]||'';return row;},{}));}
function taxRateWorkbookDownload(filename,rows){if(typeof XLSX==='undefined'){showToast('Excel 组件未加载','error');return;}const sheet=XLSX.utils.json_to_sheet(rows);const book=XLSX.utils.book_new();XLSX.utils.book_append_sheet(book,sheet,'税率查询表');XLSX.writeFile(book,filename);}
function downloadTaxRateTemplate(){taxRateWorkbookDownload('税率查询表导入模板.xlsx',[{hs:'9503000090',cnName:'示例塑料玩具',enName:'TOY',price:'0.85',material:'塑料',imageUrl:'https://example.com/toy.jpg',basicTax:'Free',additionalTax:''}].map(item=>taxRateExcelRows([item])[0]));}
function exportTaxRates(){const rows=getFilteredTaxRates();if(!rows.length){showToast('当前查询结果为空','warning');return;}const date=new Date();const stamp=date.getFullYear()+String(date.getMonth()+1).padStart(2,'0')+String(date.getDate()).padStart(2,'0');taxRateWorkbookDownload('税率查询表-'+stamp+'.xlsx',taxRateExcelRows(rows));showToast('税率查询结果已导出','success');}
function openTaxRateImportModal(){taxRateImportRows=[];const file=document.getElementById('tax-rate-import-file');if(file)file.value='';document.getElementById('tax-rate-import-summary').textContent='尚未选择文件';document.getElementById('tax-rate-import-confirm').disabled=true;document.getElementById('tbody-tax-rate-import-preview').innerHTML='<tr><td colspan="10" style="padding:32px;text-align:center;color:#94a3b8">选择文件后显示前 10 条数据</td></tr>';showModal('modal-tax-rate-import');}
function renderTaxRateImportPreview(){const summary=document.getElementById('tax-rate-import-summary');const body=document.getElementById('tbody-tax-rate-import-preview');const confirm=document.getElementById('tax-rate-import-confirm');const valid=taxRateImportRows.filter(row=>!row.error).length;summary.innerHTML=taxRateImportRows.length?'共读取 <strong>'+taxRateImportRows.length+'</strong> 条,可导入 <strong>'+valid+'</strong> 条,失败 <strong>'+(taxRateImportRows.length-valid)+'</strong> 条':'尚未选择文件';body.innerHTML=taxRateImportRows.length?taxRateImportRows.slice(0,10).map(row=>'<tr><td>'+row.rowNumber+'</td><td class="'+(row.error?'tax-rate-import-error':'')+'">'+(row.error||'通过')+'</td><td>'+taxRateEscape(row.data.hs)+'</td><td>'+taxRateEscape(row.data.cnName)+'</td><td>'+taxRateEscape(row.data.enName)+'</td><td>'+taxRateEscape(row.data.price)+'</td><td>'+taxRateEscape(row.data.material)+'</td><td>'+taxRateEscape(row.data.imageUrl)+'</td><td>'+taxRateEscape(row.data.basicTax)+'</td><td>'+taxRateEscape(row.data.additionalTax)+'</td></tr>').join(''):'<tr><td colspan="10" style="padding:32px;text-align:center;color:#94a3b8">选择文件后显示前 10 条数据</td></tr>';confirm.disabled=!taxRateImportRows.length||valid!==taxRateImportRows.length;}
function handleTaxRateImportFile(input){const file=input&&input.files&&input.files[0];if(!file)return;if(!/\.xlsx$/i.test(file.name)){showToast('仅支持 .xlsx 文件','warning');input.value='';return;}if(typeof XLSX==='undefined'){showToast('Excel 组件未加载','error');return;}const reader=new FileReader();reader.onload=function(event){try{const workbook=XLSX.read(event.target.result,{type:'array'});const sheet=workbook.Sheets[workbook.SheetNames[0]];const matrix=XLSX.utils.sheet_to_json(sheet,{header:1,defval:''});if(!matrix.length)throw new Error('文件没有数据');const headers=matrix[0].map(value=>String(value).trim());if(taxRateExcelFields.some(field=>headers.indexOf(field.header)<0))throw new Error('表头必须包含:'+taxRateExcelFields.map(field=>field.header).join('、'));const rows=[];const keys=new Set();for(let index=1;index<matrix.length;index++){const cells=matrix[index];if(cells.every(value=>String(value).trim()===''))continue;const data={};taxRateExcelFields.forEach(field=>{const cell=cells[headers.indexOf(field.header)];data[field.key]=cell===undefined?'':String(cell).trim();});let error=validateTaxRate(data,'',taxRateData);const key=taxRateUniqueKey(data);if(!error&&keys.has(key))error='导入文件内存在重复组合';if(!error)keys.add(key);rows.push({rowNumber:index+1,data:data,error:error});}taxRateImportRows=rows;renderTaxRateImportPreview();}catch(error){taxRateImportRows=[];renderTaxRateImportPreview();showToast(error.message||'文件解析失败','error');}};reader.readAsArrayBuffer(file);}
function confirmTaxRateImport(){if(!taxRateImportRows.length||taxRateImportRows.some(row=>row.error)){showToast('导入文件存在错误,无法整批导入','warning');return;}taxRateImportRows.forEach(row=>taxRateData.push(Object.assign({id:'TR'+String(taxRateSeq++).padStart(3,'0'),imageLabel:'IMAGE',imageColor:'#64748b'},row.data)));const count=taxRateImportRows.length;hideModal('modal-tax-rate-import');renderTaxRates();showToast('导入完成:成功 '+count+' 条','success');}
function updateExchangeRateOverwriteHint(){
const month=document.getElementById('exchange-rate-month').value;
const exists=exchangeRateData.some(item=>item.month===month);
@@ -18821,6 +19017,27 @@ function buildCtPalletLabels(containers,manifests,detailMap){
let currentCtPalletLabels=[];
const ctPalletLabelFontSizes={code:120,destination:180};
var ctPalletLabelReturnFocus=null;
function renderCtPalletLabelBarcodes(){
const nodes=Array.from(document.querySelectorAll('.ct-pallet-label-barcode'));
nodes.forEach(function(svg){
const value=svg.dataset.barcodeValue||'';
const fallback=svg.nextElementSibling;
if(!value||typeof window.JsBarcode!=='function'){
svg.hidden=true;
if(fallback)fallback.hidden=false;
return;
}
try{
window.JsBarcode(svg,value,{format:'CODE128',displayValue:false,width:3,height:96,margin:0});
svg.hidden=false;
if(fallback)fallback.hidden=true;
}catch(error){
svg.hidden=true;
if(fallback)fallback.hidden=false;
console.error('托盘标条形码生成失败',value,error);
}
});
}
function renderCtPalletLabelPreview(){
const modal=document.getElementById('modal-ct-pallet-label-preview');
const list=document.getElementById('ct-pallet-label-list');
@@ -18842,8 +19059,9 @@ function renderCtPalletLabelPreview(){
list.innerHTML=currentCtPalletLabels.map(function(label,index){
const code=escapeMfAttr(label&&label.code);
const destination=escapeMfAttr(label&&label.destWh);
return `<article class="ct-pallet-label-sheet" aria-label="托盘标 ${index+1}:${code},目的仓 ${destination}"><div class="ct-pallet-label-code">${code}</div><div class="ct-pallet-label-destination">${destination}</div></article>`;
return `<article class="ct-pallet-label-sheet" aria-label="托盘标 ${index+1}:${code},目的仓 ${destination}"><div class="ct-pallet-label-code">${code}</div><div class="ct-pallet-label-destination">${destination}</div><div class="ct-pallet-label-barcode-wrap"><svg class="ct-pallet-label-barcode" data-barcode-value="${code}" role="img" aria-label="条形码:${code}"></svg><span class="ct-pallet-label-barcode-error" hidden>条形码生成失败</span></div></article>`;
}).join('');
renderCtPalletLabelBarcodes();
}
function openCtPalletLabelPreview(){
const checked=Array.from(document.querySelectorAll('.ct-row-cb:checked'));
@@ -18910,7 +19128,7 @@ document.addEventListener('keydown',function(event){
});
function renderCtList(){renderTable('tbody-ct-list',ctListData,['_cb','_act','_emptyWarning','no','manifest','status','isAbnormal','ctNo','clearDoc','releaseDoc','puDoc','fleet','wh','bl','vessel','type','carrier','pol','pod','vol','time','etd','atd','eta','ata','pickupExpectedAt','appointmentAt','emptiedAt','returnedAt'],(d,c,i)=>{
if(c==='_cb')return `<input type="checkbox" class="ct-row-cb" data-ct-index="${i}" onchange="updateCtSelectCount()"/>`;
if(c==='_act')return `<a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();openCtEdit(${i})">编辑</a> &nbsp; <a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();ctClearance('${d.no}')">清关</a> &nbsp; <a href="#" style="color:${d.isAbnormal?'#1890ff':'#dc2626'};text-decoration:none" onclick="event.preventDefault();event.stopPropagation();openCtAbnormalModal('${d.no}')">${d.isAbnormal?'取消异常':'标记异常'}</a> &nbsp; <a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();showCtMore(${i})">更多 ∨</a>`;
if(c==='_act')return `<a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();openCtEdit(${i})">编辑</a> &nbsp; <a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();ctClearance('${d.no}')">清关</a> &nbsp; <a href="#" style="color:${d.isAbnormal?'#1890ff':'#dc2626'};text-decoration:none" onclick="event.preventDefault();event.stopPropagation();openCtAbnormalModal('${d.no}')">${d.isAbnormal?'取消异常':'标记异常'}</a> &nbsp; <a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();showCtMore(${i})">更多 ∨</a> &nbsp; <a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();openCtMaterials(${i})">管理资料</a>`;
if(c==='status')return renderContainerStatusTag(d.status);
if(c==='isAbnormal')return renderAbnormalTag(getContainerAbnormalState(d.no));
if(c==='fleet'){
@@ -18958,7 +19176,67 @@ function mfUploadDoc(no,key,el){
};
inp.click();
}
function showCtMore(i){showToast('更多操作:详情 / 跟踪 / 日志');}
let currentCtMaterialsIndex=-1;
function showCtMore(i){
const d=ctListData[i];
if(!d){showToast('未找到该货柜','warning');return;}
currentCtMaterialsIndex=i;
const el=document.getElementById('ct-more-container');
if(el)el.textContent=d.no||d.ctNo||'—';
showModal('modal-ct-more');
}
function openCtMaterials(index){
const d=ctListData[index];
if(!d){showToast('未找到该货柜','warning');return;}
currentCtMaterialsIndex=index;
const container=d.ctNo||d.no||'—';
const title=document.getElementById('ct-materials-title');
const noEl=document.getElementById('ct-materials-container');
if(title)title.textContent='管理资料 - '+container;
if(noEl)noEl.textContent=container;
renderCtMaterials(d);
showModal('modal-ct-materials');
}
function renderCtMaterials(d){
const body=document.getElementById('ct-materials-body');
if(!body)return;
const materials=Array.isArray(d.pdaMaterials)?d.pdaMaterials:[];
const received=materials.filter(m=>m.status==='已接收').length;
const summary=document.getElementById('ct-materials-summary');
if(summary)summary.textContent='已接收 '+received+' / 3 类';
body.innerHTML=materials.map(function(m,idx){
const ok=m.status==='已接收';
const file=m.file?'<span style="color:#1677ff" title="'+m.file+'">📎 '+m.file+'</span>':'<span style="color:#94a3b8">暂无文件</span>';
const view=ok?'<a href="javascript:void(0)" style="color:#1677ff;text-decoration:none;white-space:nowrap" onclick="showToast(\'预览 '+m.type+':'+m.file+'\',\'info\')">查看照片</a>':'';
const upload='<a href="javascript:void(0)" style="color:#1677ff;text-decoration:none;white-space:nowrap" onclick="uploadCtMaterial('+idx+')">上传</a>';
const action='<span style="display:inline-flex;align-items:center;justify-content:center;gap:12px;white-space:nowrap">'+view+upload+'</span>';
return '<tr style="border-top:1px solid #f0f0f0"><td style="padding:11px 10px;font-weight:600;color:#1f2937">'+m.type+'</td><td style="padding:11px 10px"><span class="tag '+(ok?'tag-green':'tag-gray')+'">'+(m.status||'待接收')+'</span></td><td style="padding:11px 10px">'+file+'</td><td style="padding:11px 10px;color:#64748b">'+(m.receivedAt||'—')+'</td><td style="padding:11px 10px;color:#64748b">'+(m.source||'—')+'</td><td style="padding:11px 10px;text-align:center">'+action+'</td></tr>';
}).join('');
}
function uploadCtMaterial(index){
const d=ctListData[currentCtMaterialsIndex];
if(!d||!d.pdaMaterials||!d.pdaMaterials[index])return;
const m=d.pdaMaterials[index];
const inp=document.createElement('input');
inp.type='file';
inp.accept='.jpg,.jpeg,.png,.webp';
inp.style.display='none';
const cleanup=function(){if(inp.parentNode)inp.parentNode.removeChild(inp);};
inp.onchange=function(){
const file=inp.files&&inp.files[0];
if(!file){cleanup();return;}
m.status='已接收';
m.file=file.name;
m.receivedAt=new Date().toISOString().slice(0,16).replace('T',' ');
m.source='手动上传';
renderCtMaterials(d);
showToast(m.type+'上传成功','success');
cleanup();
};
inp.oncancel=cleanup;
document.body.appendChild(inp);
inp.click();
}
function ctClearance(no){
var d=ctListData.find(function(x){return x.no===no;});
if(!d){showToast('未找到该货柜','warning');return;}
@@ -21504,13 +21782,54 @@ function renderFbaReceivableFees(bodyId,totalId,fees){
totalNode.textContent='$'+total.toFixed(2);
}
function renderFbaInboundFees(fees){renderFbaReceivableFees('fbd-inbound-fees','fbd-inbound-fee-total',fees);}
function getFbaInboundPalletDetails(record){
const explicit=Array.isArray(record&&record.palletDetails)?record.palletDetails.filter(Boolean):[];
if(explicit.length)return explicit;
const count=Math.max(0,Math.floor(Number(record&&record.palletTotal)||0));
if(!count)return [];
const totalBoxes=Math.max(0,Math.floor(Number(record&&record.totalBoxes)||0));
const base=Math.floor(totalBoxes/count),remainder=totalBoxes%count;
return Array.from({length:count},(_,index)=>({
palletNo:(record&&record.inboundNo?record.inboundNo:'PAL')+'-P'+String(index+1).padStart(2,'0'),
productName:record&&record.productName||'—',
boxes:base+(index<remainder?1:0),
photo:''
}));
}
function renderFbaInboundPalletDetails(record){
const body=document.getElementById('fbd-inbound-pallet-details');
if(!body)return;
const rows=getFbaInboundPalletDetails(record);
body.innerHTML=rows.length?rows.map(item=>{
const palletNo=lmoEsc(fbaDetailDisplay(item.palletNo)),productName=lmoEsc(fbaDetailDisplay(item.productName)),boxes=lmoEsc(fbaDetailDisplay(item.boxes)),photo=String(item.photo||'').trim(),photoName=lmoEsc(fbaDetailDisplay(item.photoName||'打托照片_'+String(item.palletNo||'托盘')+'.jpg'));
const photoCell=photo?'<button type="button" class="fbd-photo-link" title="点击预览打托照片" aria-label="预览 '+photoName+'" onclick="openFbaInboundPalletPhoto(\''+lmoEsc(photo)+'\',\''+palletNo+'\')">'+photoName+'</button>':'<span style="color:#94a3b8">暂无附件</span>';
return '<tr><td>'+palletNo+'</td><td>'+productName+'</td><td style="text-align:right">'+boxes+'</td><td>'+photoCell+'</td></tr>';
}).join(''):'<tr><td colspan="4" class="fbd-empty">暂无托盘明细</td></tr>';
}
function openFbaInboundPalletPhoto(photo,palletNo){
const image=document.getElementById('fbd-inbound-photo-preview-image'),caption=document.getElementById('fbd-inbound-photo-preview-caption');
if(!image||!photo)return;
image.src=photo;image.alt=fbaDetailDisplay(palletNo)+' 打托照片';
if(caption)caption.textContent=fbaDetailDisplay(palletNo)+' · 打托照片';
image.onerror=function(){this.onerror=null;this.alt='打托照片加载失败';showToast('打托照片加载失败,请检查附件地址','error');};
showModal('modal-fbd-inbound-photo-preview');
}
function closeFbaInboundPalletPhoto(){
const image=document.getElementById('fbd-inbound-photo-preview-image'),caption=document.getElementById('fbd-inbound-photo-preview-caption');
if(image){image.src='';image.alt='打托照片预览';image.onerror=null;}
if(caption)caption.textContent='打托照片';
hideModal('modal-fbd-inbound-photo-preview');
}
function renderFbaOutboundFees(fees){renderFbaReceivableFees('fbd-outbound-fees','fbd-outbound-fee-total',fees);}
function openFbaInboundOrderDetail(no){
const d=fbaInboundOrderData.find(item=>item.inboundNo===no);
if(!d){showToast('未找到入库单记录','error');return;}
showFbaReadOnlyDetail('fba-inbound-order-detail','fbd-inbound-no','fbd-inbound-status',d.inboundNo,d.status,'fbd-inbound-content',[
showFbaReadOnlyDetail('fba-inbound-order-detail','fbd-inbound-no','fbd-inbound-status',d.inboundNo,d.status,'fbd-inbound-top-content',[
{title:'单据信息',rows:[['入库单号',d.inboundNo],['入仓码',d.warehouseCode],['类型',d.type],['货主',d.owner],['关联订单',d.relatedOrderNo]]},
{title:'仓储信息',rows:[['海外仓',d.wh],['FBA仓库',getFbioFbaWarehouse(d)],['总箱数',d.totalBoxes],['计费体积(m³)',d.chargeVol],['总托盘数',d.palletTotal],['托盘类型',getInboundPalletType(d)],['是否外配',formatInboundOutsourced(d)],['是否加急',formatInboundUrgent(d)]]},
{title:'仓储信息',rows:[['海外仓',d.wh],['FBA仓库',getFbioFbaWarehouse(d)],['总箱数',d.totalBoxes],['计费体积(m³)',d.chargeVol],['总托盘数',d.palletTotal],['托盘类型',getInboundPalletType(d)],['是否外配',formatInboundOutsourced(d)],['是否加急',formatInboundUrgent(d)]]}
]);
renderFbaInboundPalletDetails(d);
renderFbaReadOnlyDetail('fbd-inbound-appointment-content',[
{title:'预约与排车',rows:[['状态',d.status],['预约状态',d.appointmentStatus||'未预约'],['预约送仓时间',d.appointmentDate||'—'],['预计装车时间',d.estimatedLoadingTime||'—'],['排车状态',d.scheduleStatus||'未排车'],['创建时间',d.createdAt],['更新时间',d.updatedAt],['备注',d.remark]]}
]);
renderFbaInboundFees(d.fees);
@@ -23123,6 +23442,7 @@ if(page==='rpt-business')renderRptBusiness();
if(page==='gen-product')renderGenProduct();
if(page==='gen-fee')renderGenFee();
if(page==='gen-exchange-rate')renderExchangeRates();
if(page==='gen-tax-rate')renderTaxRates();
if(page==='gen-owned-warehouse')renderOwnedWarehouse();
if(page==='gen-fba')renderGenFba();
if(page==='gen-partner-warehouse')renderPartnerWarehouse();
+217 -11
View File
@@ -129,6 +129,19 @@ const exchangeRateData=[
{month:'2026-05',averageRate:7.2095,updatedBy:'李芳',updatedAt:'2026-06-03 14:20',remark:'2026年5月平均汇率'},
{month:'2025-12',averageRate:7.1023,updatedBy:'张经理',updatedAt:'2026-01-05 09:05',remark:'2025年12月平均汇率'},
];
const taxRateData=[
{id:'TR001',hs:'9503000090',cnName:'塑料玩具/塑料充气装饰品',enName:'TOY',price:'0.85',material:'塑胶',imageUrl:'',imageLabel:'TOY',imageColor:'#f59e0b',basicTax:'Free',additionalTax:''},
{id:'TR002',hs:'9503000090',cnName:'塑料玩具',enName:'TOY',price:'0.85',material:'塑料',imageUrl:'',imageLabel:'TOY',imageColor:'#22c55e',basicTax:'Free',additionalTax:''},
{id:'TR003',hs:'6104632011',cnName:'瑜伽裤',enName:'Yoga pants',price:'0.40',material:'涤纶',imageUrl:'',imageLabel:'YOGA',imageColor:'#64748b',basicTax:'28.2%',additionalTax:'7.5%'},
{id:'TR004',hs:'9404904000',cnName:'抱枕套',enName:'PILLOW COVER',price:'0.30',material:'涤纶',imageUrl:'',imageLabel:'HOME',imageColor:'#ec4899',basicTax:'12%',additionalTax:'5%'},
{id:'TR005',hs:'4421999090',cnName:'DIY木制工艺品',enName:'CRAFTS',price:'0.56',material:'杉木',imageUrl:'',imageLabel:'WOOD',imageColor:'#a16207',basicTax:'Free',additionalTax:''},
{id:'TR006',hs:'7323990000',cnName:'锅架',enName:'PAN ORGANIZER',price:'1.00',material:'不锈钢',imageUrl:'',imageLabel:'PAN',imageColor:'#0f766e',basicTax:'2.6%',additionalTax:''},
{id:'TR007',hs:'3924900000',cnName:'刮铲',enName:'SILICONE SPATULA',price:'1.00',material:'硅胶',imageUrl:'',imageLabel:'KITCHEN',imageColor:'#8b5cf6',basicTax:'3.4%',additionalTax:''},
{id:'TR008',hs:'6304920000',cnName:'三合板挂饰',enName:'HANGING DECORATION',price:'0.60',material:'杉木+塑料+麻布',imageUrl:'',imageLabel:'DECOR',imageColor:'#ef4444',basicTax:'6.4%',additionalTax:''},
];
let taxRateSeq=9;
let taxRateEditingId='';
let taxRateImportRows=[];
const genOwnedWarehouseData=[
{id:'OWN001',name:'洛杉矶仓',address:'原型未定义',city:'',state:'',zip:'',country:'美国',status:'启用'},
{id:'OWN002',name:'纽约仓',address:'原型未定义',city:'',state:'',zip:'',country:'美国',status:'启用'},
@@ -979,7 +992,18 @@ ctListData.forEach(function(item){
if(typeof item.isAbnormal!=='boolean')item.isAbnormal=false;
if(!Array.isArray(item.abnormalLogs))item.abnormalLogs=[];
if(typeof item.abnormalReason!=='string')item.abnormalReason='';
if(!item.pdaMaterials){
item.pdaMaterials=[
{key:'licensePlate',type:'车牌照片',status:'待接收',file:'',receivedAt:'',source:''},
{key:'containerSeal',type:'集装箱封签',status:'待接收',file:'',receivedAt:'',source:''},
{key:'openingPhoto',type:'开柜照片',status:'待接收',file:'',receivedAt:'',source:''}
];
}
});
if(ctListData[0]&&ctListData[0].pdaMaterials){
Object.assign(ctListData[0].pdaMaterials[0],{status:'已接收',file:'PDA_车牌照片_FFAU6685194.jpg',receivedAt:'2026-08-13 10:25',source:'PDA'});
Object.assign(ctListData[0].pdaMaterials[1],{status:'已接收',file:'PDA_集装箱封签_FFAU6685194.jpg',receivedAt:'2026-08-13 10:26',source:'PDA'});
}
const ctCostData=[
{id:'COST001',ctNo:'FFAU6685194',type:'港口费',amount:850,currency:'USD',date:'2026-07-16',remark:'洛杉矶港港口操作费'},
{id:'COST002',ctNo:'CSNU9332868',type:'堆存费',amount:320,currency:'USD',date:'2026-07-17',remark:'堆存3天'},
@@ -1091,11 +1115,11 @@ const fbaReturnOutboundData = [
/* FBA退货 - 入库单数据 */
const fbaInboundOrderData = [
{inboundNo:'FBIO20260720001',bolNo:'BOL260720001',type:'国内订单',wh:'洛杉矶长滩海外仓',owner:'杭州积乐',totalBoxes:120,chargeVol:24.5,palletTotal:4,palletType:'标准托盘',isOutsourced:false,status:'待审核',fees:[{name:'打托费',currency:'USD',amount:120,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:280,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:350,remark:'码头提柜'}]},
{inboundNo:'FBIO20260721002',bolNo:'BOL260720002',type:'海外订单',wh:'纽约海外仓',owner:'岩盛、欧业',totalBoxes:200,chargeVol:38.0,palletTotal:6,palletType:'木托盘',isOutsourced:true,status:'已审核',fees:[{name:'打托费',currency:'USD',amount:180,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:320,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:420,remark:'码头提柜'}]},
{inboundNo:'FBIO20260722003',bolNo:'BOL260220003',type:'海外订单',wh:'汉堡海外仓',owner:'上海粤讯',totalBoxes:80,chargeVol:15.2,palletTotal:3,palletType:'纸托盘',isOutsourced:false,status:'已入库',fees:[{name:'打托费',currency:'USD',amount:95,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:240,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:310,remark:'码头提柜'}]},
{inboundNo:'FBIO20260723004',bolNo:'BOL260240004',type:'国内订单',wh:'洛杉矶长滩海外仓',owner:'义乌卓越',totalBoxes:150,chargeVol:29.8,palletTotal:5,palletType:'标准托盘',isOutsourced:true,status:'待审核',fees:[{name:'打托费',currency:'USD',amount:150,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:300,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:380,remark:'码头提柜'}]},
{inboundNo:'FBIO20260725005',bolNo:'BOL260250005',type:'国内订单',wh:'纽约海外仓',owner:'杭州积乐',totalBoxes:90,chargeVol:18.6,palletTotal:3,palletType:'木托盘',isOutsourced:false,status:'已审核',fees:[{name:'打托费',currency:'USD',amount:105,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:260,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:330,remark:'码头提柜'}]},
{inboundNo:'FBIO20260720001',bolNo:'BOL260720001',type:'国内订单',wh:'洛杉矶长滩海外仓',owner:'杭州积乐',productName:'蓝牙耳机套件',totalBoxes:120,chargeVol:24.5,palletTotal:4,palletType:'标准托盘',isOutsourced:false,status:'待审核',palletDetails:[{palletNo:'PAL-A1001-01',productName:'蓝牙耳机套件',boxes:32,photoName:'打托照片_PAL-A1001-01.jpg',photo:'https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?auto=format&fit=crop&w=900&q=80'},{palletNo:'PAL-A1001-02',productName:'蓝牙耳机套件',boxes:30,photoName:'打托照片_PAL-A1001-02.jpg',photo:'https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?auto=format&fit=crop&w=900&q=80'},{palletNo:'PAL-A1001-03',productName:'蓝牙耳机套件',boxes:30,photo:''},{palletNo:'PAL-A1001-04',productName:'蓝牙耳机套件',boxes:28,photo:''}],fees:[{name:'打托费',currency:'USD',amount:120,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:280,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:350,remark:'码头提柜'}]},
{inboundNo:'FBIO20260721002',bolNo:'BOL260720002',type:'海外订单',wh:'纽约海外仓',owner:'岩盛、欧业',productName:'手机保护壳',totalBoxes:200,chargeVol:38.0,palletTotal:6,palletType:'木托盘',isOutsourced:true,status:'已审核',palletDetails:[{palletNo:'PAL-B2002-01',productName:'手机保护壳',boxes:34,photoName:'打托照片_PAL-B2002-01.jpg',photo:'https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?auto=format&fit=crop&w=900&q=80'},{palletNo:'PAL-B2002-02',productName:'手机保护壳',boxes:34,photo:''},{palletNo:'PAL-B2002-03',productName:'手机保护壳',boxes:34,photo:''},{palletNo:'PAL-B2002-04',productName:'手机保护壳',boxes:34,photo:''},{palletNo:'PAL-B2002-05',productName:'手机保护壳',boxes:32,photo:''},{palletNo:'PAL-B2002-06',productName:'手机保护壳',boxes:32,photo:''}],fees:[{name:'打托费',currency:'USD',amount:180,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:320,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:420,remark:'码头提柜'}]},
{inboundNo:'FBIO20260722003',bolNo:'BOL260220003',type:'海外订单',wh:'汉堡海外仓',owner:'上海粤讯',productName:'充电器套装',totalBoxes:80,chargeVol:15.2,palletTotal:3,palletType:'纸托盘',isOutsourced:false,status:'已入库',fees:[{name:'打托费',currency:'USD',amount:95,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:240,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:310,remark:'码头提柜'}]},
{inboundNo:'FBIO20260723004',bolNo:'BOL260240004',type:'国内订单',wh:'洛杉矶长滩海外仓',owner:'义乌卓越',productName:'LED台灯',totalBoxes:150,chargeVol:29.8,palletTotal:5,palletType:'标准托盘',isOutsourced:true,status:'待审核',fees:[{name:'打托费',currency:'USD',amount:150,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:300,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:380,remark:'码头提柜'}]},
{inboundNo:'FBIO20260725005',bolNo:'BOL260250005',type:'国内订单',wh:'纽约海外仓',owner:'杭州积乐',productName:'收纳盒组合',totalBoxes:90,chargeVol:18.6,palletTotal:3,palletType:'木托盘',isOutsourced:false,status:'已审核',fees:[{name:'打托费',currency:'USD',amount:105,remark:'入库打托作业'},{name:'拆柜费',currency:'USD',amount:260,remark:'集装箱拆柜'},{name:'提柜费',currency:'USD',amount:330,remark:'码头提柜'}]},
];
const fbioQueryMeta=[
{deliveryWh:'宁波仓',warehouseCode:'RK26072001',eta:'2026-07-28',feeAudit:'已审核',containerNo:'MSCU7283910',cabinetNo:'40HQ-001',channel:'洛杉矶普船海卡',sailingDate:'2026-07-22',loaded:'是',fullContainer:'是',measureAudit:'已审核',fbaCode:'ONT8-A1001',createdDate:'2026-07-20'},
@@ -1201,7 +1225,7 @@ const dispatchBolPrintData = [
];
const staticDispatchRecords = [
{dispatchNo:'YWBHK260820001',bolNo:'BOL20260820001',status:'已签收',pickingStatus:'拣货完成',customer:'义乌市赛恩进出口有限公司',contact:'陈女士 13800138101',warehouse:'洛杉矶长滩海外仓',destFba:'FBA ONT8',expressNo:'UPS20260820001',outboundAt:'2026-08-18 09:00:00',deliveredAt:'2026-08-20 15:36',courierNodes:[{time:'2026-08-20 15:36',status:'已签收'}]},
{dispatchNo:'YWBHK260818002',bolNo:'BOL20260818002',status:'已签收',pickingStatus:'拣货完成',customer:'上海粤讯供应链有限公司',contact:'刘先生 13900139102',warehouse:'纽约海外仓',destFba:'FBA CLT2',expressNo:'FEDEX20260818002',outboundAt:'2026-08-16 13:20:00',signedAt:'2026-08-18 11:08',courierNodes:[{time:'2026-08-18 11:08',status:'已签收'}],customerFollowup:{status:'已回访',followedUpAt:'2026-08-19 10:20',operator:'王客服',result:'满意',feedback:'客户确认货物完好,派送时效符合预期。',note:'',attachments:[{fileName:'客户回访记录_0819.pdf',uploadedAt:'2026-08-19 10:18',size:186320}]}},
{dispatchNo:'YWBHK260818002',bolNo:'BOL20260818002',status:'已签收',pickingStatus:'拣货完成',customer:'上海粤讯供应链有限公司',contact:'刘先生 13900139102',warehouse:'纽约海外仓',destFba:'FBA CLT2',expressNo:'FEDEX20260818002',outboundAt:'2026-08-16 13:20:00',signedAt:'2026-08-18 11:08',courierNodes:[{time:'2026-08-18 11:08',status:'已签收'}],customerFollowup:{status:'已回访',followedUpAt:'2026-08-19 10:20',operator:'王客服',result:'已接收',feedback:'客户确认货物完好,派送时效符合预期。',note:'',attachments:[{fileName:'客户回访记录_0819.pdf',uploadedAt:'2026-08-19 10:18',size:186320}]}},
{dispatchNo:'YWBHK260815003',bolNo:'BOL20260815003',status:'已签收',pickingStatus:'拣货完成',customer:'宁波远洋供应链有限公司',contact:'孙女士 13700137103',warehouse:'汉堡海外仓',destFba:'DE Frankfurt',expressNo:'DHL20260815003',outboundAt:'2026-08-14 08:45:00',completedAt:'2026-08-15 17:42',courierNodes:[{time:'2026-08-15 17:42',status:'已签收'}]},
{dispatchNo:'YWBHK200615001',bolNo:'BOL20260615001',status:'待派送',pickingStatus:'已拣货',customer:'义乌欧业进出口有限公司',contact:'张三 13800138000',expressNo:'SF1234567890',outboundAt:'2026-06-15 14:00:00',courierNodes:[],fleetVehicleId:'VD4'},
{dispatchNo:'YWBHK200615002',bolNo:'BOL20260615002',status:'已签收',pickingStatus:'拣货完成',customer:'青岛远航贸易有限公司',contact:'李四 13900139000',expressNo:'UPS20260615002',outboundAt:'2026-06-14 16:30:00',courierNodes:[]},
@@ -1404,6 +1428,7 @@ const menuPermTree = [
{key:'gen-product',label:'产品信息'},
{key:'gen-fee',label:'费用项目'},
{key:'gen-exchange-rate',label:'汇率管理'},
{key:'gen-tax-rate',label:'税率查询表'},
{key:'gen-fba',label:'仓库管理'},
{key:'gen-download',label:'下载中心'},
{key:'gen-pallet',label:'托盘管理'},
@@ -1435,6 +1460,7 @@ roleData.forEach(role=>{
role.perms=role.perms.filter(key=>key!=='sys-tenant'&&key!=='sys-brand');
if(role.perms.includes('gen-fba')&&!role.perms.includes('gen-download'))role.perms.push('gen-download');
if(role.perms.includes('gen-fee')&&!role.perms.includes('gen-exchange-rate'))role.perms.push('gen-exchange-rate');
if(role.perms.includes('gen-exchange-rate')&&!role.perms.includes('gen-tax-rate'))role.perms.push('gen-tax-rate');
if(role.code==='SUPER_ADMIN'&&!role.perms.includes('sys-menu-permissions'))role.perms.push('sys-menu-permissions');
});
@@ -3692,6 +3718,62 @@ document.getElementById('exchange-rate-month-filter').value='';
renderExchangeRates();
}
function taxRateImageSrc(label,color){
const svg='<svg xmlns="http://www.w3.org/2000/svg" width="144" height="96" viewBox="0 0 144 96"><rect width="144" height="96" rx="10" fill="'+color+'"/><circle cx="112" cy="24" r="18" fill="rgba(255,255,255,.25)"/><path d="M20 72 52 40l18 16 16-18 38 34H20Z" fill="rgba(255,255,255,.28)"/><text x="12" y="27" fill="#fff" font-family="Arial,sans-serif" font-size="14" font-weight="700">'+label+'</text></svg>';
return 'data:image/svg+xml;charset=UTF-8,'+encodeURIComponent(svg);
}
function taxRateEscape(value){return String(value??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
function taxRateUniqueKey(item){return [item.hs,item.cnName,item.enName].map(value=>String(value||'').trim().toLowerCase()).join('|');}
function taxRateImage(item){const fallback=taxRateImageSrc(item.imageLabel||'IMAGE',item.imageColor||'#64748b');return item.imageUrl?'<img class="tax-rate-thumb" src="'+taxRateEscape(item.imageUrl)+'" data-tax-fallback="'+fallback+'" onerror="this.onerror=null;this.src=this.dataset.taxFallback" alt="'+taxRateEscape(item.cnName)+'"/>':'<img class="tax-rate-thumb" src="'+fallback+'" alt="'+taxRateEscape(item.cnName)+'"/>';}
function validateTaxRate(item,ignoreId,existing){
const required=[['HS',item.hs],['中文品名',item.cnName],['英文品名',item.enName],['材质',item.material],['基础关税',item.basicTax]];
for(const entry of required)if(!String(entry[1]||'').trim())return entry[0]+'不能为空';
if(item.price===''||item.price===null||item.price===undefined||!Number.isFinite(Number(item.price))||Number(item.price)<0)return '价格必须为大于等于 0 的数字';
if(item.imageUrl){try{new URL(item.imageUrl);}catch(error){return '产品图片必须是有效 URL';}}
const key=taxRateUniqueKey(item);if(existing.some(row=>row.id!==ignoreId&&taxRateUniqueKey(row)===key))return 'HS、中文品名、英文品名组合已存在';
return '';
}
function getFilteredTaxRates(){const filters=getTaxRateFilters();return taxRateData.filter(item=>(!filters.hs||item.hs.toLowerCase().includes(filters.hs))&&(!filters.cn||item.cnName.toLowerCase().includes(filters.cn))&&(!filters.en||item.enName.toLowerCase().includes(filters.en)));}
function getTaxRateFilters(){
return {
hs:(document.getElementById('tax-rate-hs-filter')||{}).value?.trim().toLowerCase()||'',
cn:(document.getElementById('tax-rate-cn-filter')||{}).value?.trim().toLowerCase()||'',
en:(document.getElementById('tax-rate-en-filter')||{}).value?.trim().toLowerCase()||''
};
}
function renderTaxRates(){
const tbody=document.getElementById('tbody-gen-tax-rate');
if(!tbody)return;
const rows=getFilteredTaxRates();
tbody.innerHTML=rows.length?rows.map(item=>'<tr><td><strong>'+taxRateEscape(item.hs)+'</strong></td><td>'+taxRateEscape(item.cnName)+'</td><td>'+taxRateEscape(item.enName)+'</td><td>'+taxRateEscape(item.price)+'</td><td>'+taxRateEscape(item.material)+'</td><td>'+taxRateImage(item)+'</td><td>'+taxRateEscape(item.basicTax)+'</td><td>'+taxRateEscape(item.additionalTax||'-')+'</td><td><div class="tax-rate-actions"><button class="btn btn-sm btn-default" onclick="openTaxRateModal(\''+taxRateEscape(item.id)+'\')">编辑</button><button class="btn btn-sm btn-default" style="color:#ff4d4f;border-color:#fecaca" onclick="deleteTaxRate(\''+taxRateEscape(item.id)+'\')">删除</button></div></td></tr>').join(''):'<tr><td colspan="9" style="padding:48px;text-align:center;color:#8c8c8c">暂无匹配的税率记录</td></tr>';
}
function resetTaxRateFilters(){
['tax-rate-hs-filter','tax-rate-cn-filter','tax-rate-en-filter'].forEach(id=>{const input=document.getElementById(id);if(input)input.value='';});
renderTaxRates();
}
function updateTaxRateImagePreview(){const input=document.getElementById('tax-rate-form-image');const preview=document.getElementById('tax-rate-form-image-preview');if(!preview)return;const url=input&&input.value.trim();preview.src=url||taxRateImageSrc('IMAGE','#64748b');preview.onerror=function(){this.onerror=null;this.src=taxRateImageSrc('IMAGE','#64748b');};}
function clearTaxRateForm(){['hs','cn','en','price','material','image','basic-tax','additional-tax'].forEach(key=>{const el=document.getElementById('tax-rate-form-'+key);if(el)el.value='';});updateTaxRateImagePreview();}
function openTaxRateModal(id){taxRateEditingId=id||'';const record=taxRateData.find(item=>item.id===taxRateEditingId);document.getElementById('tax-rate-modal-title').textContent=record?'编辑税率':'新增税率';clearTaxRateForm();if(record){document.getElementById('tax-rate-form-hs').value=record.hs;document.getElementById('tax-rate-form-cn').value=record.cnName;document.getElementById('tax-rate-form-en').value=record.enName;document.getElementById('tax-rate-form-price').value=record.price;document.getElementById('tax-rate-form-material').value=record.material;document.getElementById('tax-rate-form-image').value=record.imageUrl||'';document.getElementById('tax-rate-form-basic-tax').value=record.basicTax;document.getElementById('tax-rate-form-additional-tax').value=record.additionalTax||'';updateTaxRateImagePreview();}showModal('modal-tax-rate-edit');}
function readTaxRateForm(){return {hs:document.getElementById('tax-rate-form-hs').value.trim(),cnName:document.getElementById('tax-rate-form-cn').value.trim(),enName:document.getElementById('tax-rate-form-en').value.trim(),price:document.getElementById('tax-rate-form-price').value.trim(),material:document.getElementById('tax-rate-form-material').value.trim(),imageUrl:document.getElementById('tax-rate-form-image').value.trim(),basicTax:document.getElementById('tax-rate-form-basic-tax').value.trim(),additionalTax:document.getElementById('tax-rate-form-additional-tax').value.trim()};}
function saveTaxRate(){const value=readTaxRateForm();const error=validateTaxRate(value,taxRateEditingId,taxRateData);if(error){showToast(error,'warning');return;}const index=taxRateData.findIndex(item=>item.id===taxRateEditingId);const id=taxRateEditingId||'TR'+String(taxRateSeq++).padStart(3,'0');const record=Object.assign({id:id,imageLabel:'IMAGE',imageColor:'#64748b'},value);if(index>-1)taxRateData.splice(index,1,record);else taxRateData.push(record);hideModal('modal-tax-rate-edit');renderTaxRates();showToast(index>-1?'税率记录已更新':'税率记录已新增','success');}
function deleteTaxRate(id){const index=taxRateData.findIndex(item=>item.id===id);if(index<0)return;const item=taxRateData[index];if(!window.confirm('确定删除 '+item.cnName+' 的税率记录吗?'))return;taxRateData.splice(index,1);renderTaxRates();showToast('税率记录已删除','success');}
const taxRateExcelFields=[{header:'HS',key:'hs',required:true},{header:'中文品名',key:'cnName',required:true},{header:'英文品名',key:'enName',required:true},{header:'价格',key:'price',required:true},{header:'材质',key:'material',required:true},{header:'产品图片',key:'imageUrl',required:false},{header:'基础关税',key:'basicTax',required:true},{header:'附加税',key:'additionalTax',required:false}];
function taxRateExcelRows(rows){return rows.map(item=>taxRateExcelFields.reduce((row,field)=>{row[field.header]=item[field.key]||'';return row;},{}));}
function taxRateWorkbookDownload(filename,rows){if(typeof XLSX==='undefined'){showToast('Excel 组件未加载','error');return;}const sheet=XLSX.utils.json_to_sheet(rows);const book=XLSX.utils.book_new();XLSX.utils.book_append_sheet(book,sheet,'税率查询表');XLSX.writeFile(book,filename);}
function downloadTaxRateTemplate(){taxRateWorkbookDownload('税率查询表导入模板.xlsx',[{hs:'9503000090',cnName:'示例塑料玩具',enName:'TOY',price:'0.85',material:'塑料',imageUrl:'https://example.com/toy.jpg',basicTax:'Free',additionalTax:''}].map(item=>taxRateExcelRows([item])[0]));}
function exportTaxRates(){const rows=getFilteredTaxRates();if(!rows.length){showToast('当前查询结果为空','warning');return;}const date=new Date();const stamp=date.getFullYear()+String(date.getMonth()+1).padStart(2,'0')+String(date.getDate()).padStart(2,'0');taxRateWorkbookDownload('税率查询表-'+stamp+'.xlsx',taxRateExcelRows(rows));showToast('税率查询结果已导出','success');}
function openTaxRateImportModal(){taxRateImportRows=[];const file=document.getElementById('tax-rate-import-file');if(file)file.value='';document.getElementById('tax-rate-import-summary').textContent='尚未选择文件';document.getElementById('tax-rate-import-confirm').disabled=true;document.getElementById('tbody-tax-rate-import-preview').innerHTML='<tr><td colspan="10" style="padding:32px;text-align:center;color:#94a3b8">选择文件后显示前 10 条数据</td></tr>';showModal('modal-tax-rate-import');}
function renderTaxRateImportPreview(){const summary=document.getElementById('tax-rate-import-summary');const body=document.getElementById('tbody-tax-rate-import-preview');const confirm=document.getElementById('tax-rate-import-confirm');const valid=taxRateImportRows.filter(row=>!row.error).length;summary.innerHTML=taxRateImportRows.length?'共读取 <strong>'+taxRateImportRows.length+'</strong> 条,可导入 <strong>'+valid+'</strong> 条,失败 <strong>'+(taxRateImportRows.length-valid)+'</strong> 条':'尚未选择文件';body.innerHTML=taxRateImportRows.length?taxRateImportRows.slice(0,10).map(row=>'<tr><td>'+row.rowNumber+'</td><td class="'+(row.error?'tax-rate-import-error':'')+'">'+(row.error||'通过')+'</td><td>'+taxRateEscape(row.data.hs)+'</td><td>'+taxRateEscape(row.data.cnName)+'</td><td>'+taxRateEscape(row.data.enName)+'</td><td>'+taxRateEscape(row.data.price)+'</td><td>'+taxRateEscape(row.data.material)+'</td><td>'+taxRateEscape(row.data.imageUrl)+'</td><td>'+taxRateEscape(row.data.basicTax)+'</td><td>'+taxRateEscape(row.data.additionalTax)+'</td></tr>').join(''):'<tr><td colspan="10" style="padding:32px;text-align:center;color:#94a3b8">选择文件后显示前 10 条数据</td></tr>';confirm.disabled=!taxRateImportRows.length||valid!==taxRateImportRows.length;}
function handleTaxRateImportFile(input){const file=input&&input.files&&input.files[0];if(!file)return;if(!/\.xlsx$/i.test(file.name)){showToast('仅支持 .xlsx 文件','warning');input.value='';return;}if(typeof XLSX==='undefined'){showToast('Excel 组件未加载','error');return;}const reader=new FileReader();reader.onload=function(event){try{const workbook=XLSX.read(event.target.result,{type:'array'});const sheet=workbook.Sheets[workbook.SheetNames[0]];const matrix=XLSX.utils.sheet_to_json(sheet,{header:1,defval:''});if(!matrix.length)throw new Error('文件没有数据');const headers=matrix[0].map(value=>String(value).trim());if(taxRateExcelFields.some(field=>headers.indexOf(field.header)<0))throw new Error('表头必须包含:'+taxRateExcelFields.map(field=>field.header).join('、'));const rows=[];const keys=new Set();for(let index=1;index<matrix.length;index++){const cells=matrix[index];if(cells.every(value=>String(value).trim()===''))continue;const data={};taxRateExcelFields.forEach(field=>{const cell=cells[headers.indexOf(field.header)];data[field.key]=cell===undefined?'':String(cell).trim();});let error=validateTaxRate(data,'',taxRateData);const key=taxRateUniqueKey(data);if(!error&&keys.has(key))error='导入文件内存在重复组合';if(!error)keys.add(key);rows.push({rowNumber:index+1,data:data,error:error});}taxRateImportRows=rows;renderTaxRateImportPreview();}catch(error){taxRateImportRows=[];renderTaxRateImportPreview();showToast(error.message||'文件解析失败','error');}};reader.readAsArrayBuffer(file);}
function confirmTaxRateImport(){if(!taxRateImportRows.length||taxRateImportRows.some(row=>row.error)){showToast('导入文件存在错误,无法整批导入','warning');return;}taxRateImportRows.forEach(row=>taxRateData.push(Object.assign({id:'TR'+String(taxRateSeq++).padStart(3,'0'),imageLabel:'IMAGE',imageColor:'#64748b'},row.data)));const count=taxRateImportRows.length;hideModal('modal-tax-rate-import');renderTaxRates();showToast('导入完成:成功 '+count+' 条','success');}
function updateExchangeRateOverwriteHint(){
const month=document.getElementById('exchange-rate-month').value;
const exists=exchangeRateData.some(item=>item.month===month);
@@ -6533,6 +6615,27 @@ function buildCtPalletLabels(containers,manifests,detailMap){
let currentCtPalletLabels=[];
const ctPalletLabelFontSizes={code:120,destination:180};
var ctPalletLabelReturnFocus=null;
function renderCtPalletLabelBarcodes(){
const nodes=Array.from(document.querySelectorAll('.ct-pallet-label-barcode'));
nodes.forEach(function(svg){
const value=svg.dataset.barcodeValue||'';
const fallback=svg.nextElementSibling;
if(!value||typeof window.JsBarcode!=='function'){
svg.hidden=true;
if(fallback)fallback.hidden=false;
return;
}
try{
window.JsBarcode(svg,value,{format:'CODE128',displayValue:false,width:3,height:96,margin:0});
svg.hidden=false;
if(fallback)fallback.hidden=true;
}catch(error){
svg.hidden=true;
if(fallback)fallback.hidden=false;
console.error('托盘标条形码生成失败',value,error);
}
});
}
function renderCtPalletLabelPreview(){
const modal=document.getElementById('modal-ct-pallet-label-preview');
const list=document.getElementById('ct-pallet-label-list');
@@ -6554,8 +6657,9 @@ function renderCtPalletLabelPreview(){
list.innerHTML=currentCtPalletLabels.map(function(label,index){
const code=escapeMfAttr(label&&label.code);
const destination=escapeMfAttr(label&&label.destWh);
return `<article class="ct-pallet-label-sheet" aria-label="托盘标 ${index+1}:${code},目的仓 ${destination}"><div class="ct-pallet-label-code">${code}</div><div class="ct-pallet-label-destination">${destination}</div></article>`;
return `<article class="ct-pallet-label-sheet" aria-label="托盘标 ${index+1}:${code},目的仓 ${destination}"><div class="ct-pallet-label-code">${code}</div><div class="ct-pallet-label-destination">${destination}</div><div class="ct-pallet-label-barcode-wrap"><svg class="ct-pallet-label-barcode" data-barcode-value="${code}" role="img" aria-label="条形码:${code}"></svg><span class="ct-pallet-label-barcode-error" hidden>条形码生成失败</span></div></article>`;
}).join('');
renderCtPalletLabelBarcodes();
}
function openCtPalletLabelPreview(){
const checked=Array.from(document.querySelectorAll('.ct-row-cb:checked'));
@@ -6622,7 +6726,7 @@ document.addEventListener('keydown',function(event){
});
function renderCtList(){renderTable('tbody-ct-list',ctListData,['_cb','_act','_emptyWarning','no','manifest','status','isAbnormal','ctNo','clearDoc','releaseDoc','puDoc','fleet','wh','bl','vessel','type','carrier','pol','pod','vol','time','etd','atd','eta','ata','pickupExpectedAt','appointmentAt','emptiedAt','returnedAt'],(d,c,i)=>{
if(c==='_cb')return `<input type="checkbox" class="ct-row-cb" data-ct-index="${i}" onchange="updateCtSelectCount()"/>`;
if(c==='_act')return `<a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();openCtEdit(${i})">编辑</a> &nbsp; <a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();ctClearance('${d.no}')">清关</a> &nbsp; <a href="#" style="color:${d.isAbnormal?'#1890ff':'#dc2626'};text-decoration:none" onclick="event.preventDefault();event.stopPropagation();openCtAbnormalModal('${d.no}')">${d.isAbnormal?'取消异常':'标记异常'}</a> &nbsp; <a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();showCtMore(${i})">更多 ∨</a>`;
if(c==='_act')return `<a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();openCtEdit(${i})">编辑</a> &nbsp; <a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();ctClearance('${d.no}')">清关</a> &nbsp; <a href="#" style="color:${d.isAbnormal?'#1890ff':'#dc2626'};text-decoration:none" onclick="event.preventDefault();event.stopPropagation();openCtAbnormalModal('${d.no}')">${d.isAbnormal?'取消异常':'标记异常'}</a> &nbsp; <a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();showCtMore(${i})">更多 ∨</a> &nbsp; <a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();openCtMaterials(${i})">管理资料</a>`;
if(c==='status')return renderContainerStatusTag(d.status);
if(c==='isAbnormal')return renderAbnormalTag(getContainerAbnormalState(d.no));
if(c==='fleet'){
@@ -6670,7 +6774,67 @@ function mfUploadDoc(no,key,el){
};
inp.click();
}
function showCtMore(i){showToast('更多操作:详情 / 跟踪 / 日志');}
let currentCtMaterialsIndex=-1;
function showCtMore(i){
const d=ctListData[i];
if(!d){showToast('未找到该货柜','warning');return;}
currentCtMaterialsIndex=i;
const el=document.getElementById('ct-more-container');
if(el)el.textContent=d.no||d.ctNo||'—';
showModal('modal-ct-more');
}
function openCtMaterials(index){
const d=ctListData[index];
if(!d){showToast('未找到该货柜','warning');return;}
currentCtMaterialsIndex=index;
const container=d.ctNo||d.no||'—';
const title=document.getElementById('ct-materials-title');
const noEl=document.getElementById('ct-materials-container');
if(title)title.textContent='管理资料 - '+container;
if(noEl)noEl.textContent=container;
renderCtMaterials(d);
showModal('modal-ct-materials');
}
function renderCtMaterials(d){
const body=document.getElementById('ct-materials-body');
if(!body)return;
const materials=Array.isArray(d.pdaMaterials)?d.pdaMaterials:[];
const received=materials.filter(m=>m.status==='已接收').length;
const summary=document.getElementById('ct-materials-summary');
if(summary)summary.textContent='已接收 '+received+' / 3 类';
body.innerHTML=materials.map(function(m,idx){
const ok=m.status==='已接收';
const file=m.file?'<span style="color:#1677ff" title="'+m.file+'">📎 '+m.file+'</span>':'<span style="color:#94a3b8">暂无文件</span>';
const view=ok?'<a href="javascript:void(0)" style="color:#1677ff;text-decoration:none;white-space:nowrap" onclick="showToast(\'预览 '+m.type+':'+m.file+'\',\'info\')">查看照片</a>':'';
const upload='<a href="javascript:void(0)" style="color:#1677ff;text-decoration:none;white-space:nowrap" onclick="uploadCtMaterial('+idx+')">上传</a>';
const action='<span style="display:inline-flex;align-items:center;justify-content:center;gap:12px;white-space:nowrap">'+view+upload+'</span>';
return '<tr style="border-top:1px solid #f0f0f0"><td style="padding:11px 10px;font-weight:600;color:#1f2937">'+m.type+'</td><td style="padding:11px 10px"><span class="tag '+(ok?'tag-green':'tag-gray')+'">'+(m.status||'待接收')+'</span></td><td style="padding:11px 10px">'+file+'</td><td style="padding:11px 10px;color:#64748b">'+(m.receivedAt||'—')+'</td><td style="padding:11px 10px;color:#64748b">'+(m.source||'—')+'</td><td style="padding:11px 10px;text-align:center">'+action+'</td></tr>';
}).join('');
}
function uploadCtMaterial(index){
const d=ctListData[currentCtMaterialsIndex];
if(!d||!d.pdaMaterials||!d.pdaMaterials[index])return;
const m=d.pdaMaterials[index];
const inp=document.createElement('input');
inp.type='file';
inp.accept='.jpg,.jpeg,.png,.webp';
inp.style.display='none';
const cleanup=function(){if(inp.parentNode)inp.parentNode.removeChild(inp);};
inp.onchange=function(){
const file=inp.files&&inp.files[0];
if(!file){cleanup();return;}
m.status='已接收';
m.file=file.name;
m.receivedAt=new Date().toISOString().slice(0,16).replace('T',' ');
m.source='手动上传';
renderCtMaterials(d);
showToast(m.type+'上传成功','success');
cleanup();
};
inp.oncancel=cleanup;
document.body.appendChild(inp);
inp.click();
}
function ctClearance(no){
var d=ctListData.find(function(x){return x.no===no;});
if(!d){showToast('未找到该货柜','warning');return;}
@@ -9216,13 +9380,54 @@ function renderFbaReceivableFees(bodyId,totalId,fees){
totalNode.textContent='$'+total.toFixed(2);
}
function renderFbaInboundFees(fees){renderFbaReceivableFees('fbd-inbound-fees','fbd-inbound-fee-total',fees);}
function getFbaInboundPalletDetails(record){
const explicit=Array.isArray(record&&record.palletDetails)?record.palletDetails.filter(Boolean):[];
if(explicit.length)return explicit;
const count=Math.max(0,Math.floor(Number(record&&record.palletTotal)||0));
if(!count)return [];
const totalBoxes=Math.max(0,Math.floor(Number(record&&record.totalBoxes)||0));
const base=Math.floor(totalBoxes/count),remainder=totalBoxes%count;
return Array.from({length:count},(_,index)=>({
palletNo:(record&&record.inboundNo?record.inboundNo:'PAL')+'-P'+String(index+1).padStart(2,'0'),
productName:record&&record.productName||'—',
boxes:base+(index<remainder?1:0),
photo:''
}));
}
function renderFbaInboundPalletDetails(record){
const body=document.getElementById('fbd-inbound-pallet-details');
if(!body)return;
const rows=getFbaInboundPalletDetails(record);
body.innerHTML=rows.length?rows.map(item=>{
const palletNo=lmoEsc(fbaDetailDisplay(item.palletNo)),productName=lmoEsc(fbaDetailDisplay(item.productName)),boxes=lmoEsc(fbaDetailDisplay(item.boxes)),photo=String(item.photo||'').trim(),photoName=lmoEsc(fbaDetailDisplay(item.photoName||'打托照片_'+String(item.palletNo||'托盘')+'.jpg'));
const photoCell=photo?'<button type="button" class="fbd-photo-link" title="点击预览打托照片" aria-label="预览 '+photoName+'" onclick="openFbaInboundPalletPhoto(\''+lmoEsc(photo)+'\',\''+palletNo+'\')">'+photoName+'</button>':'<span style="color:#94a3b8">暂无附件</span>';
return '<tr><td>'+palletNo+'</td><td>'+productName+'</td><td style="text-align:right">'+boxes+'</td><td>'+photoCell+'</td></tr>';
}).join(''):'<tr><td colspan="4" class="fbd-empty">暂无托盘明细</td></tr>';
}
function openFbaInboundPalletPhoto(photo,palletNo){
const image=document.getElementById('fbd-inbound-photo-preview-image'),caption=document.getElementById('fbd-inbound-photo-preview-caption');
if(!image||!photo)return;
image.src=photo;image.alt=fbaDetailDisplay(palletNo)+' 打托照片';
if(caption)caption.textContent=fbaDetailDisplay(palletNo)+' · 打托照片';
image.onerror=function(){this.onerror=null;this.alt='打托照片加载失败';showToast('打托照片加载失败,请检查附件地址','error');};
showModal('modal-fbd-inbound-photo-preview');
}
function closeFbaInboundPalletPhoto(){
const image=document.getElementById('fbd-inbound-photo-preview-image'),caption=document.getElementById('fbd-inbound-photo-preview-caption');
if(image){image.src='';image.alt='打托照片预览';image.onerror=null;}
if(caption)caption.textContent='打托照片';
hideModal('modal-fbd-inbound-photo-preview');
}
function renderFbaOutboundFees(fees){renderFbaReceivableFees('fbd-outbound-fees','fbd-outbound-fee-total',fees);}
function openFbaInboundOrderDetail(no){
const d=fbaInboundOrderData.find(item=>item.inboundNo===no);
if(!d){showToast('未找到入库单记录','error');return;}
showFbaReadOnlyDetail('fba-inbound-order-detail','fbd-inbound-no','fbd-inbound-status',d.inboundNo,d.status,'fbd-inbound-content',[
showFbaReadOnlyDetail('fba-inbound-order-detail','fbd-inbound-no','fbd-inbound-status',d.inboundNo,d.status,'fbd-inbound-top-content',[
{title:'单据信息',rows:[['入库单号',d.inboundNo],['入仓码',d.warehouseCode],['类型',d.type],['货主',d.owner],['关联订单',d.relatedOrderNo]]},
{title:'仓储信息',rows:[['海外仓',d.wh],['FBA仓库',getFbioFbaWarehouse(d)],['总箱数',d.totalBoxes],['计费体积(m³)',d.chargeVol],['总托盘数',d.palletTotal],['托盘类型',getInboundPalletType(d)],['是否外配',formatInboundOutsourced(d)],['是否加急',formatInboundUrgent(d)]]},
{title:'仓储信息',rows:[['海外仓',d.wh],['FBA仓库',getFbioFbaWarehouse(d)],['总箱数',d.totalBoxes],['计费体积(m³)',d.chargeVol],['总托盘数',d.palletTotal],['托盘类型',getInboundPalletType(d)],['是否外配',formatInboundOutsourced(d)],['是否加急',formatInboundUrgent(d)]]}
]);
renderFbaInboundPalletDetails(d);
renderFbaReadOnlyDetail('fbd-inbound-appointment-content',[
{title:'预约与排车',rows:[['状态',d.status],['预约状态',d.appointmentStatus||'未预约'],['预约送仓时间',d.appointmentDate||'—'],['预计装车时间',d.estimatedLoadingTime||'—'],['排车状态',d.scheduleStatus||'未排车'],['创建时间',d.createdAt],['更新时间',d.updatedAt],['备注',d.remark]]}
]);
renderFbaInboundFees(d.fees);
@@ -10835,6 +11040,7 @@ if(page==='rpt-business')renderRptBusiness();
if(page==='gen-product')renderGenProduct();
if(page==='gen-fee')renderGenFee();
if(page==='gen-exchange-rate')renderExchangeRates();
if(page==='gen-tax-rate')renderTaxRates();
if(page==='gen-owned-warehouse')renderOwnedWarehouse();
if(page==='gen-fba')renderGenFba();
if(page==='gen-partner-warehouse')renderPartnerWarehouse();
@@ -17,6 +17,7 @@
"gen-product",
"gen-fee",
"gen-exchange-rate",
"gen-tax-rate",
"outbound-vehicle",
"gen-owned-warehouse",
"gen-fba",
@@ -380,6 +381,7 @@
"gen-product",
"gen-fee",
"gen-exchange-rate",
"gen-tax-rate",
"gen-owned-warehouse",
"gen-fba",
"gen-partner-warehouse",
@@ -482,12 +482,28 @@
#page-fba-inbound-order .fbio-list-action-buttons .btn{min-width:76px}
.fbd-inbound-fee-table th,.fbd-inbound-fee-table td{text-align:center!important}
.fbd-receivable-fee-table th,.fbd-receivable-fee-table td{text-align:center!important}
.fbd-inbound-ops-grid{margin-top:16px;align-items:start}
#page-fba-inbound-order-detail .fbd-body{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px 20px}
#page-fba-inbound-order-detail #fbd-inbound-top-content{grid-column:1/-1}
#page-fba-inbound-order-detail .fbd-inbound-pallet-grid{grid-column:1;margin-top:0}
#page-fba-inbound-order-detail #fbd-inbound-appointment-content{grid-column:2;margin-top:0}
#page-fba-inbound-order-detail .fbd-inbound-pallet-grid{grid-template-columns:1fr}
#page-fba-inbound-order-detail #fbd-inbound-appointment-content{grid-template-columns:1fr}
#page-fba-inbound-order-detail .fbd-section-wide{grid-column:1/-1;margin-top:0}
.fbd-inbound-pallet-table{min-width:540px;table-layout:fixed}
.fbd-inbound-pallet-table th,.fbd-inbound-pallet-table td{white-space:normal;overflow-wrap:anywhere;word-break:break-word;vertical-align:middle}
.fbd-inbound-pallet-table th:nth-child(1),.fbd-inbound-pallet-table td:nth-child(1){width:140px}
.fbd-inbound-pallet-table th:nth-child(2),.fbd-inbound-pallet-table td:nth-child(2){width:130px}
.fbd-inbound-pallet-table th:nth-child(3),.fbd-inbound-pallet-table td:nth-child(3){width:62px}
.fbd-inbound-pallet-table td:nth-child(3){text-align:left!important}
.fbd-inbound-pallet-table th:nth-child(4),.fbd-inbound-pallet-table td:nth-child(4){width:220px;white-space:nowrap;overflow-wrap:normal;word-break:normal}
.fbd-photo-link{display:inline-block;max-width:none;padding:0;border:0;background:transparent;color:#1677ff;text-align:left;text-decoration:underline;cursor:pointer;white-space:nowrap;overflow-wrap:normal;word-break:normal}
.fbio-edit-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px 20px}
.fbio-edit-grid .form-group{min-width:0;margin:0}
.fbio-edit-grid .form-group input,.fbio-edit-grid .form-group select{width:100%;box-sizing:border-box}
.fbio-edit-grid .lmo-checkbox-option input{width:16px;flex:0 0 16px}
.fbio-readonly-input{background:#f5f7fa!important;color:#64748b!important;cursor:not-allowed}
@media(max-width:720px){#page-fba-inbound-order .fbio-list-actions{align-items:flex-start;flex-direction:column;padding:10px 14px}#page-fba-inbound-order .fbio-list-action-buttons{width:100%;justify-content:flex-start}.fbio-edit-grid{grid-template-columns:1fr}}
@media(max-width:720px){#page-fba-inbound-order .fbio-list-actions{align-items:flex-start;flex-direction:column;padding:10px 14px}#page-fba-inbound-order .fbio-list-action-buttons{width:100%;justify-content:flex-start}.fbio-edit-grid{grid-template-columns:1fr}#page-fba-inbound-order-detail .fbd-body{grid-template-columns:1fr}#page-fba-inbound-order-detail .fbd-inbound-pallet-grid,#page-fba-inbound-order-detail #fbd-inbound-appointment-content{grid-column:1}}
</style>
<div class="page" id="page-fba-inbound-order">
<div class="breadcrumb">首页 <span class="sep">&gt;</span> 海外仓 <span class="sep">&gt;</span> <strong>入库单</strong></div>
@@ -688,9 +704,11 @@
<!-- ==================== OVERSEAS WAREHOUSE - 入库单详情 ==================== -->
<div class="page" id="page-fba-inbound-order-detail">
<div class="breadcrumb">首页 <span class="sep">&gt;</span> 海外仓 <span class="sep">&gt;</span> 入库单 <span class="sep">&gt;</span> <strong>详情</strong></div>
<div class="table-card fbd-card"><div class="fbd-head"><div><div class="fbd-title">入库单详情</div><div class="fbd-subtitle">入库单号 <strong id="fbd-inbound-no">—</strong></div></div><div class="fbd-actions"><span id="fbd-inbound-status" class="tag tag-gray">—</span><button class="btn btn-default" onclick="backToFbaInboundOrders()">返回</button></div></div><div class="fbd-body"><div class="fbd-grid" id="fbd-inbound-content"></div><section class="fbd-section fbd-section-wide" aria-labelledby="fbd-inbound-fee-title"><div class="fbd-section-title" id="fbd-inbound-fee-title">应收费用明细</div><div class="fbd-table-wrap"><table class="fbd-detail-table fbd-inbound-fee-table"><thead><tr><th style="width:70px">序号</th><th>费用项目</th><th style="width:100px">币种</th><th style="width:140px">金额</th><th>备注</th></tr></thead><tbody id="fbd-inbound-fees"><tr><td colspan="5" class="fbd-empty">暂无应收费用明细</td></tr></tbody><tfoot><tr><td colspan="3" style="padding:12px;font-weight:700;color:#374151">应收费用合计(USD)</td><td id="fbd-inbound-fee-total" style="padding:12px;font-weight:700;color:#111827">$0.00</td><td></td></tr></tfoot></table></div></section></div></div>
<div class="table-card fbd-card"><div class="fbd-head"><div><div class="fbd-title">入库单详情</div><div class="fbd-subtitle">入库单号 <strong id="fbd-inbound-no">—</strong></div></div><div class="fbd-actions"><span id="fbd-inbound-status" class="tag tag-gray">—</span><button class="btn btn-default" onclick="backToFbaInboundOrders()">返回</button></div></div><div class="fbd-body"><div class="fbd-grid" id="fbd-inbound-top-content"></div><div class="fbd-grid fbd-inbound-pallet-grid"><section class="fbd-section" aria-labelledby="fbd-inbound-pallet-title"><div class="fbd-section-title" id="fbd-inbound-pallet-title">托盘明细</div><div class="fbd-table-wrap"><table class="fbd-detail-table fbd-inbound-pallet-table"><thead><tr><th>托盘号</th><th>产品名称</th><th style="width:100px">箱数</th><th style="width:190px">附件名称(打托照片)</th></tr></thead><tbody id="fbd-inbound-pallet-details"><tr><td colspan="4" class="fbd-empty">暂无托盘明细</td></tr></tbody></table></div></section></div><div class="fbd-grid" id="fbd-inbound-appointment-content"></div><section class="fbd-section fbd-section-wide" aria-labelledby="fbd-inbound-fee-title"><div class="fbd-section-title" id="fbd-inbound-fee-title">应收费用明细</div><div class="fbd-table-wrap"><table class="fbd-detail-table fbd-inbound-fee-table"><thead><tr><th style="width:70px">序号</th><th>费用项目</th><th style="width:100px">币种</th><th style="width:140px">金额</th><th>备注</th></tr></thead><tbody id="fbd-inbound-fees"><tr><td colspan="5" class="fbd-empty">暂无应收费用明细</td></tr></tbody><tfoot><tr><td colspan="3" style="padding:12px;font-weight:700;color:#374151">应收费用合计(USD)</td><td id="fbd-inbound-fee-total" style="padding:12px;font-weight:700;color:#111827">$0.00</td><td></td></tr></tfoot></table></div></section></div></div>
</div>
<div class="modal-overlay" id="modal-fbd-inbound-photo-preview" role="dialog" aria-modal="true" aria-labelledby="fbd-inbound-photo-preview-caption"><div class="modal" style="max-width:900px;width:94%;padding:0;overflow:hidden"><div class="modal-header"><span id="fbd-inbound-photo-preview-caption">打托照片</span><button type="button" aria-label="关闭照片预览" onclick="closeFbaInboundPalletPhoto()" style="border:0;background:transparent;font-size:22px;line-height:1;cursor:pointer;color:#64748b">&times;</button></div><div class="modal-body" style="display:flex;align-items:center;justify-content:center;min-height:360px;padding:18px;background:#f8fafc"><img id="fbd-inbound-photo-preview-image" alt="打托照片预览" style="display:block;max-width:100%;max-height:68vh;object-fit:contain;border-radius:6px;box-shadow:0 8px 24px rgba(15,23,42,.16)"/></div><div class="modal-footer"><button type="button" class="btn btn-default" onclick="closeFbaInboundPalletPhoto()">关闭</button></div></div></div>
<!-- ==================== OVERSEAS WAREHOUSE - 出库单详情 ==================== -->
<div class="page" id="page-fba-outbound-order-detail">
<div class="breadcrumb">首页 <span class="sep">&gt;</span> 海外仓 <span class="sep">&gt;</span> 出库单 <span class="sep">&gt;</span> <strong>详情</strong></div>
@@ -32,6 +32,14 @@ FORM: 已建立后台列表页的精确扩展;本次需求已明确,因此
</div>
</div>
<div class="page" id="page-gen-tax-rate">
<div class="table-card">
<div class="table-toolbar"><div><span class="title">税率查询表</span><span style="font-size:12px;color:#94a3b8;margin-left:10px">按 HS 与品名查询基础关税、附加税</span></div><div class="toolbar-actions"><button class="btn btn-primary" onclick="openTaxRateModal()">+ 新增</button><button class="btn btn-default" onclick="openTaxRateImportModal()">批量导入</button><button class="btn btn-default" onclick="exportTaxRates()">导出</button></div></div>
<div class="search-row" aria-label="税率查询条件"><input id="tax-rate-hs-filter" placeholder="HS 编码" style="width:180px"/><input id="tax-rate-cn-filter" placeholder="中文品名" style="width:180px"/><input id="tax-rate-en-filter" placeholder="英文品名" style="width:180px"/><button class="btn btn-sm btn-primary" onclick="renderTaxRates()">查询</button><button class="btn btn-sm btn-default" onclick="resetTaxRateFilters()">重置</button></div>
<div style="overflow-x:auto"><table class="tax-rate-table" style="min-width:1280px"><thead><tr><th>HS</th><th>中文品名</th><th>英文品名</th><th>价格</th><th>材质</th><th>产品图片</th><th>基础关税</th><th>附加税</th><th>操作</th></tr></thead><tbody id="tbody-gen-tax-rate"></tbody></table></div>
</div>
</div>
<!-- ==================== WAREHOUSE MANAGEMENT ==================== -->
<div class="page" id="page-gen-owned-warehouse">
<div class="table-card"><div class="table-toolbar"><span class="title">仓库管理</span><div class="toolbar-actions"><button class="btn btn-primary" onclick="openWarehouseAdd('owned')">+ 新增</button><button class="btn btn-default">导入</button><button class="btn btn-default">导出</button></div></div>
@@ -12,7 +12,7 @@
<div><span style="display:block;color:#6b7280;font-size:12px;margin-bottom:3px">完成时间</span><span id="customer-followup-completed-at" style="font-size:13px;color:#374151">—</span></div>
<div style="grid-column:1/-1"><span style="display:block;color:#6b7280;font-size:12px;margin-bottom:3px">目的地</span><span id="customer-followup-destination" style="font-size:13px;color:#374151">—</span></div>
</div>
<div class="form-group"><label>回访结果 <span style="color:#ef4444">*</span></label><select id="customer-followup-result"><option value="">请选择回访结果</option><option value="满意">满意</option><option value="一般">一般</option><option value="不满意">不满意</option><option value="未接通">未接通</option></select></div>
<div class="form-group"><label>回访结果 <span style="color:#ef4444">*</span></label><select id="customer-followup-result"><option value="">请选择回访结果</option><option value="已接收">已接收</option><option value="未接收">未接收</option><option value="不清楚">不清楚</option></select></div>
<div class="form-group"><label>客户反馈</label><textarea id="customer-followup-feedback" rows="3" maxlength="500" placeholder="记录客户对派送服务的反馈"></textarea></div>
<div class="form-group"><label>备注</label><textarea id="customer-followup-note" rows="2" maxlength="300" placeholder="补充后续跟进事项"></textarea></div>
<div class="form-group"><label>回访资料</label><div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap"><label class="dispatch-doc-upload-label" style="font-size:12px">选择文件<input id="customer-followup-files" type="file" multiple accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.webp" style="display:none" onchange="handleCustomerFollowupFiles(this)"/></label><span style="font-size:12px;color:#9ca3af">支持 PDF、Office、JPG、PNG、WEBP</span></div><div id="customer-followup-file-list" style="display:flex;flex-direction:column;gap:6px;margin-top:8px"></div></div>
@@ -599,6 +599,43 @@
</div>
<div class="modal-footer"><button type="button" class="btn btn-default" onclick="hideModal('modal-ct-abnormal')">取消</button><button type="button" class="btn btn-danger" id="ct-abnormal-submit" onclick="saveCtAbnormalState()">确认标记</button></div></div></div>
<!-- ==================== 货柜更多操作 MODAL ==================== -->
<div class="modal-overlay" id="modal-ct-more">
<div class="modal" style="max-width:420px">
<div class="modal-header"><span>更多操作</span><button type="button" aria-label="关闭" onclick="hideModal('modal-ct-more')" style="display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:0;border-radius:6px;background:transparent;color:#64748b;font-size:22px;cursor:pointer">&times;</button></div>
<div class="modal-body">
<div style="padding:12px 14px;margin-bottom:14px;border:1px solid #e8edf3;border-radius:8px;background:#f8fafc;font-size:13px;color:#64748b">当前货柜:<strong id="ct-more-container" style="color:#1f2937">—</strong></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<button type="button" class="btn btn-default" onclick="showToast('货柜详情功能开发中','info')">详情</button>
<button type="button" class="btn btn-default" onclick="showToast('货柜跟踪功能开发中','info')">跟踪</button>
<button type="button" class="btn btn-default" onclick="showToast('货柜日志功能开发中','info')">日志</button>
</div>
</div>
<div class="modal-footer"><button type="button" class="btn btn-default" onclick="hideModal('modal-ct-more')">关闭</button></div>
</div>
</div>
<!-- ==================== 货柜管理资料 MODAL ==================== -->
<div class="modal-overlay" id="modal-ct-materials">
<div class="modal" style="width:760px;max-width:94vw">
<div class="modal-header"><span id="ct-materials-title">管理资料</span><button type="button" aria-label="关闭" onclick="hideModal('modal-ct-materials')" style="display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:0;border-radius:6px;background:transparent;color:#64748b;font-size:22px;cursor:pointer">&times;</button></div>
<div class="modal-body">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;margin-bottom:16px;border:1px solid #dbeafe;border-radius:8px;background:#eff6ff">
<div><div style="font-size:12px;color:#64748b;margin-bottom:3px">集装箱号</div><strong id="ct-materials-container" style="font-size:15px;color:#1e3a8a">—</strong></div>
<div id="ct-materials-summary" style="font-size:13px;color:#2563eb">已接收 0 / 3 类</div>
</div>
<div style="border:1px solid #e5e7eb;border-radius:8px;overflow:hidden">
<table style="width:100%;border-collapse:collapse;font-size:12px">
<thead style="background:#f8fafc"><tr><th style="padding:10px;text-align:left">资料类型</th><th style="padding:10px;text-align:left">接收状态</th><th style="padding:10px;text-align:left">文件</th><th style="padding:10px;text-align:left">接收时间</th><th style="padding:10px;text-align:left">来源</th><th style="padding:10px;text-align:center;width:150px">操作</th></tr></thead>
<tbody id="ct-materials-body"></tbody>
</table>
</div>
<div style="margin-top:12px;font-size:12px;color:#94a3b8">PDA 推送资料会自动归档到当前货柜;照片仅用于现场留档与后续核验。</div>
</div>
<div class="modal-footer"><button type="button" class="btn btn-default" onclick="hideModal('modal-ct-materials')">关闭</button></div>
</div>
</div>
<!-- ==================== 新增货柜费用 MODAL ==================== -->
<div class="modal-overlay" id="modal-ct-cost-add">
<div class="modal" style="max-width:520px"><div class="modal-header">新增货柜费用<span onclick="hideModal('modal-ct-cost-add')" style="cursor:pointer;font-size:20px">&times;</span></div>
@@ -1705,6 +1742,31 @@
</div>
</div>
<!-- 通用:税率新增/编辑 -->
<div class="modal-overlay" id="modal-tax-rate-edit">
<div class="modal" style="width:720px;max-width:calc(100vw - 32px)">
<div class="modal-header"><span id="tax-rate-modal-title">新增税率</span><span onclick="hideModal('modal-tax-rate-edit')" style="cursor:pointer;font-size:20px" aria-label="关闭">&times;</span></div>
<div class="modal-body"><div class="tax-rate-form-grid">
<div class="form-group"><label><span style="color:#ff4d4f">*</span> HS</label><input id="tax-rate-form-hs" placeholder="请输入 HS 编码"/></div>
<div class="form-group"><label><span style="color:#ff4d4f">*</span> 中文品名</label><input id="tax-rate-form-cn" placeholder="请输入中文品名"/></div>
<div class="form-group"><label><span style="color:#ff4d4f">*</span> 英文品名</label><input id="tax-rate-form-en" placeholder="请输入英文品名"/></div>
<div class="form-group"><label><span style="color:#ff4d4f">*</span> 价格</label><input id="tax-rate-form-price" type="number" min="0" step="0.01" placeholder="请输入价格"/></div>
<div class="form-group"><label><span style="color:#ff4d4f">*</span> 材质</label><input id="tax-rate-form-material" placeholder="请输入材质"/></div>
<div class="form-group"><label><span style="color:#ff4d4f">*</span> 基础关税</label><input id="tax-rate-form-basic-tax" placeholder="如 Free、28.2%"/></div>
<div class="form-group"><label>附加税</label><input id="tax-rate-form-additional-tax" placeholder="选填,如 7.5%"/></div>
<div class="form-group"><label>产品图片 URL</label><input id="tax-rate-form-image" placeholder="https://example.com/image.jpg" oninput="updateTaxRateImagePreview()"/><img id="tax-rate-form-image-preview" class="tax-rate-image-preview" alt="产品图片预览"/></div>
</div></div>
<div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-tax-rate-edit')">取消</button><button class="btn btn-primary" onclick="saveTaxRate()">保存</button></div>
</div></div>
<!-- 通用:税率批量导入 -->
<div class="modal-overlay" id="modal-tax-rate-import">
<div class="modal" style="width:980px;max-width:calc(100vw - 32px)">
<div class="modal-header"><span>批量导入税率</span><span onclick="hideModal('modal-tax-rate-import')" style="cursor:pointer;font-size:20px" aria-label="关闭">&times;</span></div>
<div class="modal-body"><div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap"><div><div style="font-size:13px;font-weight:650;color:#334155">选择 Excel 文件</div><div style="font-size:12px;color:#8491a3;margin-top:4px">仅支持 .xlsx;任一行校验失败时整批拒绝。</div></div><div style="display:flex;align-items:center;gap:8px"><button class="btn btn-default btn-sm" onclick="downloadTaxRateTemplate()">下载模板</button><input id="tax-rate-import-file" type="file" accept=".xlsx" onchange="handleTaxRateImportFile(this)"/></div></div><div id="tax-rate-import-summary" class="tax-rate-import-summary">尚未选择文件</div><div class="tax-rate-import-preview"><table><thead><tr><th>行号</th><th>校验</th><th>HS</th><th>中文品名</th><th>英文品名</th><th>价格</th><th>材质</th><th>产品图片</th><th>基础关税</th><th>附加税</th></tr></thead><tbody id="tbody-tax-rate-import-preview"><tr><td colspan="10" style="padding:32px;text-align:center;color:#94a3b8">选择文件后显示前 10 条数据</td></tr></tbody></table></div></div>
<div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-tax-rate-import')">取消</button><button id="tax-rate-import-confirm" class="btn btn-primary" onclick="confirmTaxRateImport()" disabled>确认导入</button></div>
</div></div>
<!-- 应收款统计表:编辑明细 -->
<div class="modal-overlay" id="modal-receivable-report-edit">
<div class="modal" style="width:560px;max-width:calc(100vw - 32px)">
@@ -6,6 +6,8 @@
<title>货代SaaS平台 - 虫虫出海</title>
<link rel="icon" href="data:,"/>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js"></script>
<script src="prototype-src/vendor/xlsx.full.min.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
@keyframes gradientShift{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
@@ -89,6 +91,25 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Helv
#page-gen-exchange-rate .table-toolbar{gap:12px;flex-wrap:wrap}
@media(max-width:1200px){#page-gen-exchange-rate .exchange-rate-summary{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:640px){#page-gen-exchange-rate .exchange-rate-summary{grid-template-columns:1fr}}
#page-gen-tax-rate .table-toolbar{gap:12px;flex-wrap:wrap}
#page-gen-tax-rate .tax-rate-table th,#page-gen-tax-rate .tax-rate-table td{white-space:nowrap;vertical-align:middle}
#page-gen-tax-rate .tax-rate-table th:nth-child(1){width:150px}
#page-gen-tax-rate .tax-rate-table th:nth-child(2){width:220px}
#page-gen-tax-rate .tax-rate-table th:nth-child(3){width:210px}
#page-gen-tax-rate .tax-rate-table th:nth-child(6){width:110px}
#page-gen-tax-rate .tax-rate-thumb{width:72px;height:48px;object-fit:cover;border:1px solid #e2e8f0;border-radius:6px;background:#f8fafc;display:block}
#page-gen-tax-rate .tax-rate-actions{display:flex;gap:6px;align-items:center;white-space:nowrap}
#page-gen-tax-rate .tax-rate-actions .btn{padding:6px 10px;font-size:12px}
#modal-tax-rate-edit .tax-rate-form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px 18px}
#modal-tax-rate-edit .tax-rate-form-grid .full{grid-column:1/-1}
#modal-tax-rate-edit .tax-rate-form-grid input,#modal-tax-rate-edit .tax-rate-form-grid textarea{width:100%;box-sizing:border-box}
#modal-tax-rate-edit .tax-rate-image-preview{width:110px;height:72px;object-fit:cover;border:1px solid #e2e8f0;border-radius:6px;background:#f8fafc;display:block;margin-top:8px}
#modal-tax-rate-import .tax-rate-import-summary{padding:10px 12px;margin:14px 0;background:#f8fafc;border:1px solid #e2e8f0;border-radius:7px;color:#475569;font-size:13px}
#modal-tax-rate-import .tax-rate-import-preview{overflow:auto;border:1px solid #e2e8f0;border-radius:7px;max-height:330px}
#modal-tax-rate-import .tax-rate-import-preview table{min-width:1000px;font-size:12px}
#modal-tax-rate-import .tax-rate-import-preview td,#modal-tax-rate-import .tax-rate-import-preview th{padding:8px 10px}
#modal-tax-rate-import .tax-rate-import-error{color:#dc2626}
@media(max-width:640px){#modal-tax-rate-edit .tax-rate-form-grid{grid-template-columns:1fr}#modal-tax-rate-edit .tax-rate-form-grid .full{grid-column:auto}}
/* CHART */
.chart-row{display:grid;grid-template-columns:2fr 1fr;gap:20px;margin-bottom:20px}
.chart-row.equal{grid-template-columns:1fr 1fr}
@@ -1208,6 +1229,10 @@ body.dashboard-fullscreen #page-dashboard table:has(>thead)>tbody>tr>td{backgrou
.ct-pallet-label-code,.ct-pallet-label-destination{width:100%;max-width:100%;margin:0;text-align:center;font-weight:900;line-height:.95;letter-spacing:0;overflow-wrap:anywhere;word-break:break-word}
.ct-pallet-label-code{font-size:var(--ct-pallet-code-size)}
.ct-pallet-label-destination{font-size:var(--ct-pallet-destination-size)}
.ct-pallet-label-barcode-wrap{width:100%;height:30mm;display:flex;align-items:center;justify-content:center;flex:0 0 30mm}
.ct-pallet-label-barcode{display:block;max-width:250mm;width:auto;height:28mm}
.ct-pallet-label-barcode-error{font-size:18px;font-weight:600;color:#b42318;line-height:30mm}
.ct-pallet-label-barcode[hidden],.ct-pallet-label-barcode-error[hidden]{display:none}
.ct-pallet-label-empty{width:min(520px,calc(100vw - 48px));padding:28px;text-align:center;color:#c8cdd1;border:1px solid #454b50;border-radius:6px;background:#252a2e;box-sizing:border-box}
@media(max-width:900px){
.ct-pallet-preview-toolbar{align-items:flex-start;gap:10px;padding:10px 12px;flex-wrap:wrap}
@@ -1375,6 +1400,7 @@ body.dashboard-fullscreen #page-dashboard table:has(>thead)>tbody>tr>td{backgrou
<div class="sub-menu-item" onclick="switchPage('gen-product',this)">产品信息</div>
<div class="sub-menu-item" onclick="switchPage('gen-fee',this)">费用项目</div>
<div class="sub-menu-item" onclick="switchPage('gen-exchange-rate',this)">汇率管理</div>
<div class="sub-menu-item" onclick="switchPage('gen-tax-rate',this)">税率查询表</div>
<div class="sub-menu-item" onclick="switchPage('gen-owned-warehouse',this)">仓库管理</div>
<div class="sub-menu-item" onclick="switchPage('gen-overseas-quote',this)">海外仓报价</div>
<div class="sub-menu-item" onclick="switchPage('gen-voyage',this)">船期价格表</div>
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (C) 2012-present SheetJS LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,31 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const root = path.resolve(__dirname, '..', '..');
const moduleSource = fs.readFileSync(path.join(root, 'prototype-src/modules/06-containers.html'), 'utf8');
const overlaySource = fs.readFileSync(path.join(root, 'prototype-src/overlays.html'), 'utf8');
const appSource = fs.readFileSync(path.join(root, 'prototype-src/app.js'), 'utf8');
assert.match(moduleSource, /id="page-ct-list"/, '缺少货柜列表页面');
assert.match(appSource, /showCtMore\(\$\{i\}\)/, '货柜列表“更多”入口缺少行索引');
const actionIndex = appSource.indexOf("showCtMore(${i})");
const materialsIndex = appSource.indexOf("openCtMaterials(${i})", actionIndex);
assert.ok(actionIndex >= 0 && materialsIndex > actionIndex && materialsIndex - actionIndex < 500, '管理资料按钮未紧跟在更多按钮之后');
assert.match(overlaySource, /id="modal-ct-materials"/, '缺少货柜管理资料弹窗');
assert.doesNotMatch(overlaySource, /openCtMaterialsFromMore\(\)/, '管理资料不应重复出现在更多操作弹窗');
assert.match(appSource, /type:'车牌照片'/, '缺少车牌照片资料类型');
assert.match(appSource, /type:'集装箱封签'/, '缺少集装箱封签资料类型');
assert.match(appSource, /type:'开柜照片'/, '缺少开柜照片资料类型');
assert.match(appSource, /function openCtMaterials\(index\)/, '缺少打开货柜管理资料函数');
assert.match(appSource, /pdaMaterials/, '货柜数据未声明 PDA 资料字段');
assert.match(appSource, /查看照片[\s\S]*上传/, '已接收资料缺少上传按钮');
assert.doesNotMatch(appSource, /const action=ok\?[^;]*接收此项/, '资料行不应显示接收此项按钮');
assert.doesNotMatch(overlaySource, /onclick="receiveCtMaterials\(\)"/, '管理资料弹框不应显示接收 PDA 按钮');
assert.match(appSource, /function uploadCtMaterial\(index\)/, '缺少资料上传函数');
const materialsRender = appSource.slice(appSource.indexOf('function renderCtMaterials'), appSource.indexOf('function uploadCtMaterial'));
assert.doesNotMatch(materialsRender, /<button[^>]*>查看照片/, '查看照片不应使用带背景按钮');
assert.doesNotMatch(materialsRender, /<button[^>]*>上传/, '上传不应使用带背景按钮');
assert.match(materialsRender, /color:#1677ff;text-decoration:none[^>]*>上传/, '上传应显示为蓝色文字链接');
console.log('货柜管理资料契约验证通过');
@@ -22,6 +22,8 @@ function loadCtPalletLabelApi(source) {
console,
setTimeout,
clearTimeout,
window: {addEventListener() {}},
document: {addEventListener() {}},
__ctPalletLabelSentinel: sentinel,
};
const capture = [
@@ -312,4 +314,35 @@ assert.match(appJs, /event\.key\s*(?:===|!==)\s*'Tab'/, '托盘标预览未处
assert.match(appJs, /button:not\(\[disabled\]\)[\s\S]*?\[tabindex\]:not\(\[tabindex="-1"\]\)/, '托盘标预览缺少可聚焦控件查询');
assert.match(appJs, /event\.shiftKey[\s\S]*?event\.preventDefault\s*\(/, '托盘标预览缺少反向 Tab 循环');
assert.match(
shellHtml,
/jsbarcode@3\.11\.6\/dist\/JsBarcode\.all\.min\.js/,
'托盘标缺少 JsBarcode 依赖',
);
assert.match(shellHtml, /\.ct-pallet-label-barcode-wrap\b/, '缺少托盘标条形码容器样式');
assert.match(shellHtml, /\.ct-pallet-label-barcode\b/, '缺少托盘标条形码 SVG 样式');
assert.match(shellHtml, /\.ct-pallet-label-barcode-error\b/, '缺少条形码失败提示样式');
const previewStart = appJs.indexOf('function renderCtPalletLabelPreview()');
const previewEnd = appJs.indexOf('function openCtPalletLabelPreview()', previewStart);
const previewSource = appJs.slice(previewStart, previewEnd);
assert.match(previewSource, /<svg class="ct-pallet-label-barcode"/, '预览未为每张标签输出条形码 SVG');
assert.match(previewSource, /data-barcode-value="\$\{code\}"/, '条形码未编码托盘标签编号');
assert.match(previewSource, /ct-pallet-label-barcode-error/, '预览缺少条形码失败提示');
assert.ok(
previewSource.indexOf('ct-pallet-label-destination') < previewSource.indexOf('ct-pallet-label-barcode-wrap'),
'条形码必须位于目的仓下方',
);
assert.match(previewSource, /renderCtPalletLabelBarcodes\(\)/, '预览完成后未触发条形码渲染');
const barcodeStart = appJs.indexOf('function renderCtPalletLabelBarcodes()');
const barcodeEnd = appJs.indexOf('function renderCtPalletLabelPreview()', barcodeStart);
const barcodeSource = appJs.slice(barcodeStart, barcodeEnd);
assert.ok(barcodeStart >= 0 && barcodeEnd > barcodeStart, '缺少条形码渲染函数');
assert.match(barcodeSource, /format:'CODE128'/, '条形码格式必须为 Code 128');
assert.match(barcodeSource, /displayValue:false/, '条形码不应重复显示编号文字');
assert.match(barcodeSource, /svg\.dataset\.barcodeValue/, '条形码渲染未读取对应标签编号');
assert.match(barcodeSource, /typeof window\.JsBarcode!==['"]function['"]/, '缺少 JsBarcode 加载失败处理');
assert.match(barcodeSource, /catch\(error\)/, '缺少单张条形码渲染失败处理');
console.log('CONTAINER_PALLET_LABEL_CHECK_PASS');
@@ -18,6 +18,13 @@ assert.equal((customerServiceModule.match(/id="page-customer-followup"/g) || [])
assert.match(customerServiceModule, /id="tbody-customer-followup"/, '客户回访页面缺少列表容器');
assert.match(shellSource, /switchPage\('customer-followup',this\)[^>]*>客户回访</, '客户服务侧栏缺少客户回访入口');
assert.match(overlaysSource, /id="modal-customer-followup"/, '缺少客户回访弹窗');
const followupResultSelect = overlaysSource.match(/<select id="customer-followup-result">[\s\S]*?<\/select>/)?.[0];
assert.ok(followupResultSelect, '缺少回访结果下拉框');
assert.deepEqual(
[...followupResultSelect.matchAll(/<option value="([^"]*)">/g)].map(match => match[1]),
['', '已接收', '未接收', '不清楚'],
'回访结果选项必须为已接收、未接收、不清楚',
);
assert.match(appSource, /function renderCustomerFollowups\(/, '缺少客户回访列表渲染函数');
assert.match(appSource, /function saveCustomerFollowup\(/, '缺少客户回访保存函数');
assert.match(appSource, /#modal-customer-followup \.modal[\s\S]{0,160}scrollTop=0/, '客户回访弹窗再次打开时未回到顶部');
@@ -0,0 +1,41 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const root = path.resolve(__dirname, '..');
const moduleHtml = fs.readFileSync(path.join(root, 'prototype-src', 'modules/03-overseas-warehouse.html'), 'utf8');
const app = fs.readFileSync(path.join(root, 'prototype-src', 'app.js'), 'utf8');
assert.match(moduleHtml, /id="fbd-inbound-pallet-details"/, '入库单详情缺少托盘明细表格容器');
assert.match(moduleHtml, /<th[^>]*>托盘号<\/th>/, '托盘明细缺少托盘号表头');
assert.match(moduleHtml, /<th[^>]*>产品名称<\/th>/, '托盘明细缺少产品名称表头');
assert.match(moduleHtml, /<th[^>]*>箱数<\/th>/, '托盘明细缺少箱数表头');
assert.match(moduleHtml, /<th[^>]*>附件名称(打托照片)<\/th>/, '托盘明细缺少附件名称表头');
assert.match(moduleHtml, /id="modal-fbd-inbound-photo-preview"/, '缺少打托照片预览弹层');
assert.match(moduleHtml, /id="fbd-inbound-photo-preview-image"/, '照片预览弹层缺少图片节点');
assert.match(app, /function\s+renderFbaInboundPalletDetails\s*\(/, '缺少托盘明细渲染函数');
assert.match(app, /function\s+openFbaInboundPalletPhoto\s*\(/, '缺少打托照片预览函数');
assert.match(app, /renderFbaInboundPalletDetails\(d\)/, '入库单详情未调用托盘明细渲染');
assert.match(app, /palletDetails/, '入库单数据未包含托盘明细字段');
assert.match(app, /photoName/, '入库单托盘明细未包含附件名称字段');
assert.match(app, /class="fbd-photo-link"/, '附件名称未提供可点击预览入口');
const documentInfoIndex = moduleHtml.indexOf('id="fbd-inbound-top-content"');
const palletDetailIndex = moduleHtml.indexOf('id="fbd-inbound-pallet-details"');
const appointmentInfoIndex = moduleHtml.indexOf('id="fbd-inbound-appointment-content"');
assert.ok(documentInfoIndex >= 0, '单据信息缺少独立详情容器');
assert.ok(appointmentInfoIndex >= 0, '预约与排车缺少独立详情容器');
assert.ok(documentInfoIndex < palletDetailIndex && palletDetailIndex < appointmentInfoIndex, '详情顺序应为单据信息/仓储信息、托盘明细、预约与排车');
assert.match(moduleHtml, /#page-fba-inbound-order-detail \.fbd-inbound-pallet-grid\{grid-column:1;margin-top:0\}/, '托盘明细未定位到左侧操作区');
assert.match(moduleHtml, /#page-fba-inbound-order-detail #fbd-inbound-appointment-content\{grid-column:2;margin-top:0\}/, '预约与排车未定位到托盘明细右侧');
assert.match(moduleHtml, /class="fbd-grid fbd-inbound-pallet-grid"[\s\S]*?id="fbd-inbound-pallet-details"/, '托盘明细应使用与单据信息相同的两列详情网格宽度');
assert.match(moduleHtml, /#page-fba-inbound-order-detail \.fbd-body\{display:grid;grid-template-columns:repeat\(2,minmax\(0,1fr\)\);gap:16px 20px\}/, '预约与排车模块缺少统一的上方间距');
assert.match(moduleHtml, /fbd-inbound-pallet-table th:nth-child\(3\),\.fbd-inbound-pallet-table td:nth-child\(3\)\{width:62px\}/, '箱数列宽度未收窄');
assert.match(moduleHtml, /fbd-inbound-pallet-table th:nth-child\(4\),\.fbd-inbound-pallet-table td:nth-child\(4\)\{width:220px;white-space:nowrap/, '附件名称列未设置单行显示');
assert.match(moduleHtml, /#page-fba-inbound-order-detail \.fbd-body\{display:grid;grid-template-columns:repeat\(2,minmax\(0,1fr\)\);gap:16px 20px\}/, '详情操作区缺少统一的两列间距布局');
assert.match(moduleHtml, /fbd-inbound-pallet-table td:nth-child\(3\)\{[^}]*text-align:left!important/, '箱数列未设置左对齐');
assert.match(moduleHtml, /\.fbd-inbound-pallet-table\{min-width:540px;table-layout:fixed\}/, '托盘明细表缺少可读的最小宽度');
assert.match(moduleHtml, /fbd-inbound-pallet-table th:nth-child\(1\),\.fbd-inbound-pallet-table td:nth-child\(1\)\{width:140px/, '托盘号列缺少固定宽度');
assert.match(moduleHtml, /fbd-inbound-pallet-table th:nth-child\(2\),\.fbd-inbound-pallet-table td:nth-child\(2\)\{width:130px/, '产品名称列缺少固定宽度');
console.log('FBA inbound pallet detail verification passed.');
@@ -0,0 +1,101 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const root = path.resolve(__dirname, '..');
const finalPath = process.env.OVERSEAS_PDA_FINAL || path.resolve(root, '..', 'PDA(海外)', 'index.html');
assert.ok(fs.existsSync(finalPath), `final PDA file must exist: ${finalPath}`);
const html = fs.readFileSync(finalPath, 'utf8');
assert.match(html, /<style id="ios-final-report">/, 'final iOS report design layer must exist');
assert.match(html, /--ios-blue:#155eef/, 'approved system blue token must exist');
assert.doesNotMatch(html, /box-shadow:[^;}]*rgba\(21,94,239/, 'final surfaces must use neutral elevation rather than blue glow shadows');
assert.match(html, /min-height:44px/, 'interactive controls must preserve 44px touch targets');
assert.doesNotMatch(html, /\.lang-pill button\{[^}]*min-height:40px/, 'language controls must not fall below 44px');
assert.match(html, /env\(safe-area-inset-bottom/, 'bottom actions must support the iPhone safe area');
assert.match(html, /@media\s*\(prefers-reduced-motion:reduce\)/, 'reduced motion must be supported');
assert.match(html, /dataset\.page\s*=\s*state\.page/, 'the application shell must expose current page identity');
assert.match(html, /aria-pressed="\$\{state\.lang===v\}"/, 'language controls must expose selected state');
assert.match(html, /role="dialog"/, 'confirmation overlays must expose dialog semantics');
assert.match(html, /aria-modal="true"/, 'confirmation overlays must be modal to assistive technology');
assert.match(html, /role="status"/, 'toast feedback must expose status semantics');
assert.match(html, /aria-live="polite"/, 'toast feedback must be announced politely');
assert.match(html, /function settlePageScroll\(/, 'page changes must reset inherited scroll position');
assert.match(html, /@media\(max-width:480px\)[\s\S]*:has\(\.back-btn\) \.avatar\{display:none\}/, 'subpages must reserve mobile header space for the full title');
assert.match(html, /palletUnit/, 'numeric pallet values must use a natural localized unit');
assert.match(html, /pieceUnit/, 'numeric piece values must use a natural localized unit');
assert.match(html, /orderCount/, 'order-list counts must use natural localized copy');
assert.match(html, /function compactPageTitle\(/, 'subpages must use compact localized navigation titles');
assert.match(html, /textContent\.split\(label\)\.join\(unit\)/, 'units must be normalized inside compound values');
assert.match(html, /class="inventory-results"/, 'inventory must use responsive result cards');
assert.match(html, /class="inventory-card"/, 'inventory result cards must exist');
const inventoryView = html.match(/function inventoryView\(\)\{[^\n]+\}/)?.[0] || '';
assert.ok(inventoryView, 'inventory view must exist');
assert.doesNotMatch(inventoryView, /<table/, 'inventory view must not rely on a mobile wide table');
for (const page of [
'login', 'workbench', 'receive-list', 'receive', 'plans', 'pallet-list',
'pallet', 'photo-detail', 'pick', 'handover', 'inventory', 'relabel',
'exception', 'profile'
]) {
assert.ok(html.includes(`'${page}'`) || html.includes(`\"${page}\"`), `missing confirmed view: ${page}`);
}
for (const lang of ['zh', 'en', 'es']) {
assert.match(html, new RegExp(`${lang}\\s*:\\s*\\{`), `missing language dictionary: ${lang}`);
}
for (const flow of [
'UNLOAD_PHOTO_STEPS', 'HANDOVER_PHOTO_STEPS', 'startConsolidation',
'submitConsolidation', 'skipPick', 'submitRelabel', 'submitException'
]) {
assert.ok(html.includes(flow), `missing confirmed flow: ${flow}`);
}
const workbenchView = html.match(/function workbenchView\(\)\{[^\n]+\}/)?.[0] || '';
assert.ok(workbenchView, 'workbench view must exist');
for (const marker of [
"t('outbound')", 'receive-entry', 'pallet-entry', 'merge-entry', 'pick-entry', 'handover-entry'
]) {
assert.notEqual(workbenchView.indexOf(marker), -1, `workbench must include ${marker}`);
}
assert.ok(
workbenchView.indexOf("t('outbound')") < workbenchView.indexOf('merge-entry')
&& workbenchView.indexOf('merge-entry') < workbenchView.indexOf('pick-entry'),
'workbench must place merge entry after outbound heading and before pick entry'
);
assert.ok(
workbenchView.indexOf('receive-entry') < workbenchView.indexOf('pallet-entry'),
'workbench must place receive entry before pallet entry'
);
assert.ok(
workbenchView.indexOf('pick-entry') < workbenchView.indexOf('handover-entry'),
'workbench must place pick entry before handover entry'
);
const consolidationDetailView = html.match(/function consolidationDetailView\(\)\{[^\n]+\}/)?.[0] || '';
assert.ok(consolidationDetailView, 'consolidation detail view must exist');
for (const marker of ['pallet-label-selector', 'current-pallet-card']) {
assert.notEqual(consolidationDetailView.indexOf(marker), -1, `consolidation detail must include ${marker}`);
}
assert.ok(
consolidationDetailView.indexOf('pallet-label-selector') < consolidationDetailView.indexOf('current-pallet-card'),
'consolidation detail must place pallet selector before the current pallet card'
);
assert.match(consolidationDetailView, /t\('mergeOnly'\)/, 'consolidation detail must use merge-only copy');
assert.match(consolidationDetailView, /t\('submitConsolidation'\)/, 'consolidation detail must include a submit action');
assert.match(
consolidationDetailView,
/submitConsolidation\(\)/,
'consolidation detail must reference the submitConsolidation handler'
);
assert.match(
consolidationDetailView,
/onclick\s*=\s*(?:"(?:\$\{submitAction\}|submitConsolidation\(\))"|'(?:\$\{submitAction\}|submitConsolidation\(\))')/,
'consolidation detail must wire its submit action to onclick'
);
console.log('verify-overseas-pda-final: all assertions passed');
@@ -4,7 +4,7 @@ const path = require('node:path');
const vm = require('node:vm');
const root = path.resolve(__dirname, '..');
const htmlPath = path.join(root, 'PDA(海外)', 'index.html');
const htmlPath = process.env.OVERSEAS_PDA_FINAL || path.resolve(root, '..', 'PDA(海外)', 'index.html');
assert.ok(fs.existsSync(htmlPath), 'PDA(海外)/index.html must exist');
const html = fs.readFileSync(htmlPath, 'utf8');
@@ -45,8 +45,12 @@ for (const outboundField of ['inboundCode','plateNo','driver','outboundTime','to
assert.ok(html.includes(outboundField), `outbound picking field missing: ${outboundField}`);
}
assert.match(html, /totalPalletBoxes|总托数\s*\/\s*总箱数/, 'outbound picking must combine total pallets and total cartons');
assert.match(html, /injectInboundPalletUnits[\s\S]*entries\?\.\[1\]/, 'unload receiving must remain immediately after inbound palletizing');
assert.match(html, /injectInboundPalletUnits[\s\S]*insertBefore/, 'workbench inbound entries must order unload before palletizing');
const injectInboundPalletUnits = html.match(/function injectInboundPalletUnits\(\)\{[^\n]+\}/)?.[0] || '';
assert.ok(injectInboundPalletUnits, 'injectInboundPalletUnits must exist');
assert.match(injectInboundPalletUnits, /\.task-strip \.task-main p/, 'inbound pallet unit normalization must target only task-strip task details');
assert.doesNotMatch(injectInboundPalletUnits, /entries\?\.\[1\]/, 'inbound pallet unit normalization must not inspect entry elements');
assert.doesNotMatch(injectInboundPalletUnits, /insertBefore/, 'inbound pallet unit normalization must not reorder workbench entries');
assert.doesNotMatch(injectInboundPalletUnits, /receive-entry/, 'inbound pallet unit normalization must not modify entry classes');
for (const unloadField of ['tractorDriver','tractorPlate']) {
assert.ok(html.includes(unloadField), `unload list field missing: ${unloadField}`);
}
@@ -0,0 +1,159 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
function stripHtmlComments(source) {
return source.replace(/<!--[\s\S]*?-->/g, '');
}
function stripJavaScriptCommentsAndStrings(source) {
let result = '';
let state = 'code';
for (let index = 0; index < source.length; index += 1) {
const char = source[index];
const next = source[index + 1];
if (state === 'line-comment') {
if (char === '\n' || char === '\r') {
result += char;
state = 'code';
}
continue;
}
if (state === 'block-comment') {
if (char === '*' && next === '/') {
index += 1;
state = 'code';
} else if (char === '\n' || char === '\r') {
result += char;
}
continue;
}
if (state !== 'code') {
result += char === '\n' || char === '\r' ? char : ' ';
if (char === '\\') {
if (next !== undefined) {
result += next === '\n' || next === '\r' ? next : ' ';
index += 1;
}
} else if (
(state === 'single-quote' && char === "'") ||
(state === 'double-quote' && char === '"') ||
(state === 'template' && char === '`')
) {
state = 'code';
}
continue;
}
if (char === '/' && next === '/') {
state = 'line-comment';
index += 1;
} else if (char === '/' && next === '*') {
state = 'block-comment';
index += 1;
} else {
result += char;
if (char === "'") state = 'single-quote';
else if (char === '"') state = 'double-quote';
else if (char === '`') state = 'template';
}
}
return result;
}
const root = path.resolve(__dirname, '..');
const sourceRoot = path.join(root, 'prototype-src');
const moduleSource = stripHtmlComments(
fs.readFileSync(path.join(sourceRoot, 'modules', '10-general.html'), 'utf8')
);
const overlaysSource = stripHtmlComments(fs.readFileSync(path.join(sourceRoot, 'overlays.html'), 'utf8'));
const shellSource = stripHtmlComments(fs.readFileSync(path.join(sourceRoot, 'shell.html'), 'utf8'));
const appSource = stripJavaScriptCommentsAndStrings(fs.readFileSync(path.join(sourceRoot, 'app.js'), 'utf8'));
const taxRatePageMatch = moduleSource.match(
/<div\b(?=[^>]*\bclass\s*=\s*["'][^"']*\bpage\b[^"']*["'])(?=[^>]*\bid\s*=\s*["']page-gen-tax-rate["'])[^>]*>[\s\S]*?(?=<div\b(?=[^>]*\bclass\s*=\s*["'][^"']*\bpage\b[^"']*["'])(?=[^>]*\bid\s*=\s*["']page-[^"']+["'])|$)/
);
assert.ok(taxRatePageMatch, '未在 10-general.html 中找到 page-gen-tax-rate 页面片段');
const taxRatePage = taxRatePageMatch[0];
const toolbarMatch = taxRatePage.match(
/<div\b(?=[^>]*\bclass\s*=\s*["'][^"']*\btable-toolbar\b[^"']*["'])[^>]*>[\s\S]*?(?=<div\b(?=[^>]*\bclass\s*=\s*["'][^"']*\bsearch-row\b[^"']*["']))/
);
assert.ok(toolbarMatch, '税率查询表页面缺少 table-toolbar 工具栏');
const toolbar = toolbarMatch[0];
assert.match(
toolbar,
/^\s*<div\b(?=[^>]*\bclass\s*=\s*["'][^"']*\btable-toolbar\b[^"']*["'])[^>]*>/,
'工具栏片段未从实际 table-toolbar 元素开始'
);
const toolbarButtons = [
{
label: '新增',
pattern: /<button\b(?=[^>]*\bclass\s*=\s*["'][^"']*\bbtn\b[^"']*["'])(?=[^>]*\bclass\s*=\s*["'][^"']*\bbtn-primary\b[^"']*["'])(?=[^>]*\bonclick\s*=\s*["']openTaxRateModal\(\);?["'])[^>]*>\s*\+\s*新增\s*<\/button>/
},
{
label: '批量导入',
pattern: /<button\b(?=[^>]*\bclass\s*=\s*["'][^"']*\bbtn\b[^"']*["'])(?=[^>]*\bclass\s*=\s*["'][^"']*\bbtn-default\b[^"']*["'])(?=[^>]*\bonclick\s*=\s*["']openTaxRateImportModal\(\);?["'])[^>]*>\s*批量导入\s*<\/button>/
},
{
label: '导出',
pattern: /<button\b(?=[^>]*\bclass\s*=\s*["'][^"']*\bbtn\b[^"']*["'])(?=[^>]*\bclass\s*=\s*["'][^"']*\bbtn-default\b[^"']*["'])(?=[^>]*\bonclick\s*=\s*["']exportTaxRates\(\);?["'])[^>]*>\s*导出\s*<\/button>/
}
];
const buttonIndexes = toolbarButtons.map(({label, pattern}) => {
const match = toolbar.match(pattern);
assert.ok(match, `税率查询表 table-toolbar 缺少“${label}”按钮或其契约已变更`);
return match.index;
});
assert.ok(
buttonIndexes[0] < buttonIndexes[1] && buttonIndexes[1] < buttonIndexes[2],
'税率查询表工具栏按钮顺序必须为“新增、批量导入、导出”'
);
assert.match(
taxRatePage,
/<th\b[^>]*>\s*基础关税\s*<\/th>\s*<th\b[^>]*>\s*附加税\s*<\/th>\s*<th\b[^>]*>\s*操作\s*<\/th>/,
'税率查询表表头必须以“基础关税、附加税、操作”结尾'
);
assert.ok(!taxRatePage.includes('附件税'), '税率查询表页面仍包含旧文案“附件税”');
assert.match(
overlaysSource,
/<div\b(?=[^>]*\bclass\s*=\s*["'][^"']*\bmodal-overlay\b[^"']*["'])(?=[^>]*\bid\s*=\s*["']modal-tax-rate-edit["'])[^>]*>/,
'缺少税率新增/编辑弹窗 modal-tax-rate-edit'
);
assert.match(
overlaysSource,
/<div\b(?=[^>]*\bclass\s*=\s*["'][^"']*\bmodal-overlay\b[^"']*["'])(?=[^>]*\bid\s*=\s*["']modal-tax-rate-import["'])[^>]*>/,
'缺少税率批量导入弹窗 modal-tax-rate-import'
);
assert.match(
shellSource,
/<script\b(?=[^>]*\bsrc\s*=\s*["']prototype-src\/vendor\/xlsx\.full\.min\.js["'])[^>]*>/,
'shell.html 未通过 script src 引用 prototype-src/vendor/xlsx.full.min.js'
);
const xlsxVendorPath = path.join(sourceRoot, 'vendor', 'xlsx.full.min.js');
assert.ok(fs.existsSync(xlsxVendorPath), '缺少 prototype-src/vendor/xlsx.full.min.js 文件');
const requiredFunctions = [
'openTaxRateModal',
'saveTaxRate',
'deleteTaxRate',
'openTaxRateImportModal',
'handleTaxRateImportFile',
'confirmTaxRateImport',
'downloadTaxRateTemplate',
'exportTaxRates'
];
for (const functionName of requiredFunctions) {
const declaration = new RegExp(`^\\s*function\\s+${functionName}\\s*\\(`, 'm');
assert.match(appSource, declaration, `app.js 缺少函数定义 ${functionName}`);
}
console.log('税率查询表维护功能源码验证通过');