Test #1
@@ -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
@@ -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">></span> 出库 <span class="sep">></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">></span> 海外仓 <span class="sep">></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">></span> 海外仓 <span class="sep">></span> 入库单 <span class="sep">></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">×</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">></span> 海外仓 <span class="sep">></span> 出库单 <span class="sep">></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">×</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">×</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">×</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="关闭">×</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="关闭">×</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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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> <a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();ctClearance('${d.no}')">清关</a> <a href="#" style="color:${d.isAbnormal?'#1890ff':'#dc2626'};text-decoration:none" onclick="event.preventDefault();event.stopPropagation();openCtAbnormalModal('${d.no}')">${d.isAbnormal?'取消异常':'标记异常'}</a> <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> <a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();ctClearance('${d.no}')">清关</a> <a href="#" style="color:${d.isAbnormal?'#1890ff':'#dc2626'};text-decoration:none" onclick="event.preventDefault();event.stopPropagation();openCtAbnormalModal('${d.no}')">${d.isAbnormal?'取消异常':'标记异常'}</a> <a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();showCtMore(${i})">更多 ∨</a> <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();
|
||||
|
||||
Vendored
+217
-11
@@ -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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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> <a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();ctClearance('${d.no}')">清关</a> <a href="#" style="color:${d.isAbnormal?'#1890ff':'#dc2626'};text-decoration:none" onclick="event.preventDefault();event.stopPropagation();openCtAbnormalModal('${d.no}')">${d.isAbnormal?'取消异常':'标记异常'}</a> <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> <a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();ctClearance('${d.no}')">清关</a> <a href="#" style="color:${d.isAbnormal?'#1890ff':'#dc2626'};text-decoration:none" onclick="event.preventDefault();event.stopPropagation();openCtAbnormalModal('${d.no}')">${d.isAbnormal?'取消异常':'标记异常'}</a> <a href="#" style="color:#1890ff;text-decoration:none" onclick="event.preventDefault();event.stopPropagation();showCtMore(${i})">更多 ∨</a> <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">></span> 海外仓 <span class="sep">></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">></span> 海外仓 <span class="sep">></span> 入库单 <span class="sep">></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">×</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">></span> 海外仓 <span class="sep">></span> 出库单 <span class="sep">></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">×</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">×</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">×</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="关闭">×</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="关闭">×</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>
|
||||
|
||||
+201
@@ -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('税率查询表维护功能源码验证通过');
|
||||
Reference in New Issue
Block a user