CaiWuXiTong/resources/views/business/pages/courier-payables/reverse-logistics-quotes.blade.php

944 lines
43 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@php
$reverseQuoteSnapshot = app(\App\Support\ReverseLogisticsQuoteSnapshot::class)->load();
$reverseQuotes = $reverseQuoteSnapshot['quotes'];
$reverseRates = $reverseQuoteSnapshot['rates'] ?? [];
$reverseRules = $reverseQuoteSnapshot['rate_groups'] ?? [];
$reverseRateSummary = $reverseQuoteSnapshot['rate_summary'] ?? [
'rate_count' => 0,
'carrier_count' => 0,
'origin_count' => 0,
'destination_count' => 0,
'weight_band_count' => 0,
'available_count' => 0,
'missing_count' => 0,
];
$rateCarriers = collect($reverseRules)->pluck('carrier')->unique()->values()->all();
$rateOrigins = collect($reverseRates)->pluck('origin')->unique()->sort()->values()->all();
$rateDestinations = collect($reverseRates)->pluck('destination')->unique()->sort()->values()->all();
$calcOrigins = $reverseQuoteSnapshot['provinces']['origins'] ?? [];
$calcDestinations = $reverseQuoteSnapshot['provinces']['destinations'] ?? [];
@endphp
<div data-reverse-logistics-quotes>
<style>
.reverse-quote-rules {
display: grid;
gap: 0.75rem;
grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
padding: 1rem;
}
.reverse-quote-rule {
display: grid;
align-content: start;
gap: 0.6rem;
border: 1px solid var(--quote-border);
border-radius: 0.75rem;
padding: 0.85rem 0.9rem;
background: #FFFDF8;
}
.reverse-quote-rule__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.reverse-quote-rule__carrier {
font-weight: 600;
font-size: 0.875rem;
}
.reverse-quote-rule__meta {
display: grid;
gap: 0.3rem;
margin: 0;
}
.reverse-quote-rule__meta > div {
display: grid;
grid-template-columns: 5.5rem 1fr;
gap: 0.5rem;
align-items: start;
}
.reverse-quote-rule__meta dt {
color: var(--quote-muted);
font-size: 0.75rem;
}
.reverse-quote-rule__meta dd {
margin: 0;
word-break: break-word;
}
.reverse-quote-rule__notes {
border-top: 1px dashed var(--quote-border);
padding-top: 0.5rem;
}
.reverse-quote-rule__notes summary {
cursor: pointer;
color: var(--vip-content-accent-dark, #856632);
font-size: 0.75rem;
font-weight: 600;
}
.reverse-quote-rule__notes ul {
display: grid;
gap: 0.3rem;
margin: 0.5rem 0 0;
padding-left: 1.1rem;
color: var(--quote-muted);
}
.reverse-rate-band {
color: var(--quote-muted);
}
.reverse-rate-missing {
color: var(--vip-content-warning, #935F07);
font-weight: 600;
}
.quote-empty[hidden] {
display: none;
}
.reverse-calc-best td {
background: var(--vip-content-accent-soft, #F7F0DE);
font-weight: 600;
}
</style>
<div class="quote-summary" aria-label="逆向物流报价数据概览">
<div class="quote-source">
<span class="quote-source__icon" aria-hidden="true">
<span class="material-icons-outlined">assignment_return</span>
</span>
<div class="quote-source__body">
<div class="quote-source__file" title="{{ $reverseQuoteSnapshot['source_file'] }}">{{ $reverseQuoteSnapshot['source_file'] }}</div>
<div class="quote-source__state">本机私有报价源 · 原始工作簿未修改 · 金额按原表数值直读</div>
</div>
</div>
<div class="quote-metric">
<div class="quote-metric__label">报价版本</div>
<div class="quote-metric__value">{{ $reverseQuoteSnapshot['summary']['quote_count'] }}</div>
</div>
<div class="quote-metric">
<div class="quote-metric__label">运价明细</div>
<div class="quote-metric__value" data-reverse-rate-total>{{ $reverseRateSummary['rate_count'] }}</div>
</div>
<div class="quote-metric">
<div class="quote-metric__label">可计算运价</div>
<div class="quote-metric__value">{{ $reverseRateSummary['available_count'] }}</div>
</div>
<div class="quote-metric">
<div class="quote-metric__label">无报价行</div>
<div class="quote-metric__value">{{ $reverseRateSummary['missing_count'] }}</div>
</div>
<div class="quote-metric">
<div class="quote-metric__label">已标执行日期</div>
<div class="quote-metric__value">{{ $reverseQuoteSnapshot['summary']['dated_count'] }}</div>
</div>
</div>
@if ($reverseQuoteSnapshot['available'])
<div class="quote-workspace">
<div class="quote-results">
<span class="quote-results__count">计费规则</span>
<span class="quote-results__notice">
<span class="material-icons-outlined" aria-hidden="true">rule</span>
<span>每种承运商的原表口径(面单、续重、重量段、限重、计泡)</span>
</span>
</div>
<div class="reverse-quote-rules" data-reverse-rule-list>
@foreach ($reverseRules as $rule)
<article class="reverse-quote-rule" data-reverse-rule="{{ $rule['carrier'] }}">
<header class="reverse-quote-rule__head">
<span class="reverse-quote-rule__carrier">{{ $rule['carrier'] }}</span>
<span class="quote-badge quote-badge--ready">{{ $rule['sheet_name'] }}</span>
</header>
<dl class="reverse-quote-rule__meta">
<div><dt>计费方式</dt><dd>{{ $rule['pricing_label'] }}</dd></div>
<div><dt>生效日期</dt><dd>{{ $rule['effective_date'] ?? '未标注' }}</dd></div>
<div><dt>取价方向</dt><dd>{{ $rule['route_label'] ?? '出发省份 → 到达省份' }}</dd></div>
<div><dt>面单口径</dt><dd>{{ $rule['base_label'] }}({{ $rule['base_unit'] }})</dd></div>
<div><dt>续重口径</dt><dd>{{ $rule['increment_label'] }}({{ $rule['increment_unit'] }})</dd></div>
<div><dt>重量段</dt><dd>{{ $rule['weight_bands'] === [] ? '首重 1kg + 续重' : implode(' / ', $rule['weight_bands']) }}</dd></div>
<div><dt>运价覆盖</dt><dd>{{ $rule['rate_count'] }} 条(可用 {{ $rule['available_count'] }})· 出发 {{ $rule['origin_count'] }} 省 · 到达 {{ $rule['destination_count'] }} 处</dd></div>
</dl>
@if ($rule['notes'] !== [])
<details class="reverse-quote-rule__notes">
<summary>原表备注与限制({{ count($rule['notes']) }} 条)</summary>
<ul>
@foreach ($rule['notes'] as $note)
<li>{{ $note }}</li>
@endforeach
</ul>
</details>
@endif
</article>
@endforeach
</div>
</div>
<div class="quote-workspace" data-reverse-calculator>
<div class="quote-results">
<span class="quote-results__count">运费计算器</span>
<span class="quote-results__notice">
<span class="material-icons-outlined" aria-hidden="true">calculate</span>
<span>计泡 6000;按各承运商原表口径计算,附加费按原表备注</span>
</span>
</div>
<div class="quote-toolbar" aria-label="运费计算条件">
<label class="quote-field">
<span class="quote-field__label">承运商</span>
<select class="quote-control" name="calc_carrier" data-reverse-calc="carrier">
<option value="">自动试算(全部)</option>
@foreach ($rateCarriers as $carrier)
<option value="{{ $carrier }}">{{ $carrier }}</option>
@endforeach
</select>
</label>
<label class="quote-field">
<span class="quote-field__label">出发省份</span>
<select class="quote-control" name="calc_origin" data-reverse-calc="origin">
@foreach ($calcOrigins as $origin)
<option value="{{ $origin }}" @selected($origin === '广东')>{{ $origin }}</option>
@endforeach
</select>
</label>
<label class="quote-field">
<span class="quote-field__label">到达省份</span>
<select class="quote-control" name="calc_destination" data-reverse-calc="destination">
@foreach ($calcDestinations as $destination)
<option value="{{ $destination }}" @selected($destination === '江苏')>{{ $destination }}</option>
@endforeach
</select>
</label>
<label class="quote-field">
<span class="quote-field__label">结算重量(kg)</span>
<input class="quote-control" type="number" min="0" step="0.01" value="10" name="calc_weight" data-reverse-calc="weight">
</label>
<label class="quote-field">
<span class="quote-field__label">长(cm)</span>
<input class="quote-control" type="number" min="0" step="1" placeholder="选填" name="calc_length" data-reverse-calc="length">
</label>
<label class="quote-field">
<span class="quote-field__label">宽(cm)</span>
<input class="quote-control" type="number" min="0" step="1" placeholder="选填" name="calc_width" data-reverse-calc="width">
</label>
<label class="quote-field">
<span class="quote-field__label">高(cm)</span>
<input class="quote-control" type="number" min="0" step="1" placeholder="选填" name="calc_height" data-reverse-calc="height">
</label>
<div class="quote-toolbar__actions">
<button class="quote-button" type="button" data-reverse-calc-reset>
<span class="material-icons-outlined" aria-hidden="true">restart_alt</span>
<span>重置</span>
</button>
</div>
</div>
<div class="quote-results">
<span class="quote-results__count" data-reverse-calc-summary>按条件自动试算</span>
<span class="quote-results__notice">
<span class="material-icons-outlined" aria-hidden="true">straighten</span>
<span data-reverse-calc-volume>未填写长宽高,按结算重量计算</span>
</span>
</div>
<div class="quote-table-wrap">
<table class="quote-table">
<thead>
<tr>
<th scope="col">承运商</th>
<th scope="col">线路</th>
<th scope="col">计费重量(kg)</th>
<th scope="col">计算式</th>
<th scope="col">运费(元)</th>
<th scope="col">加收(元)</th>
<th scope="col">附加费 / 说明</th>
</tr>
</thead>
<tbody data-reverse-calc-body></tbody>
</table>
</div>
</div>
<div class="quote-workspace" data-reverse-rate-panel>
<div class="quote-toolbar" aria-label="运价筛选">
<label class="quote-field">
<span class="quote-field__label">承运商</span>
<select class="quote-control" name="rate_carrier" data-reverse-rate-filter="carrier">
<option value="">全部承运商</option>
@foreach ($rateCarriers as $carrier)
<option value="{{ $carrier }}">{{ $carrier }}</option>
@endforeach
</select>
</label>
<label class="quote-field">
<span class="quote-field__label">出发省份</span>
<select class="quote-control" name="rate_origin" data-reverse-rate-filter="origin">
<option value="">全部出发省</option>
@foreach ($rateOrigins as $origin)
<option value="{{ $origin }}">{{ $origin }}</option>
@endforeach
</select>
</label>
<label class="quote-field">
<span class="quote-field__label">到达省份</span>
<select class="quote-control" name="rate_destination" data-reverse-rate-filter="destination">
<option value="">全部到达省</option>
@foreach ($rateDestinations as $destination)
<option value="{{ $destination }}">{{ $destination }}</option>
@endforeach
</select>
</label>
<label class="quote-field">
<span class="quote-field__label">搜索</span>
<input class="quote-control" type="search" name="rate_keyword" placeholder="省份或来源单元格" autocomplete="off" data-reverse-rate-filter="keyword">
</label>
<div class="quote-toolbar__actions">
<button class="quote-icon-button" type="button" title="重置运价筛选" aria-label="重置运价筛选" data-reverse-rate-reset>
<span class="material-icons-outlined" aria-hidden="true">filter_alt_off</span>
</button>
</div>
</div>
<div class="quote-results">
<span class="quote-results__count" data-reverse-rate-count>共 {{ count($reverseRates) }} 条运价</span>
<span class="quote-results__notice">
<span class="material-icons-outlined" aria-hidden="true">calculate</span>
<span>面单价格与续重/费率直接取自原表单元格,页面不重新折算</span>
</span>
</div>
<div class="quote-table-wrap">
<table class="quote-table">
<thead>
<tr>
<th scope="col">承运商</th>
<th scope="col">出发省份</th>
<th scope="col">到达省份</th>
<th scope="col">计费方式</th>
<th scope="col">面单 / 首重(元/票)</th>
<th scope="col">续重 / 重量段费率(元/kg)</th>
<th scope="col">重量段</th>
<th scope="col">生效日期</th>
<th scope="col">来源</th>
</tr>
</thead>
<tbody data-reverse-rate-body></tbody>
</table>
</div>
<div class="quote-empty" role="status" data-reverse-rate-empty hidden>
<div class="quote-empty__body">
<span class="material-icons-outlined" aria-hidden="true">search_off</span>
<div class="quote-empty__title">没有符合筛选条件的运价</div>
<div class="quote-empty__text">调整承运商、出发省份或到达省份后重试。</div>
</div>
</div>
</div>
<div class="quote-workspace">
<div class="quote-results">
<span class="quote-results__count" data-reverse-result-count>报价来源 · 共 {{ count($reverseQuotes) }} 个版本</span>
<span class="quote-results__notice">
<span class="material-icons-outlined" aria-hidden="true">verified_user</span>
<span>原始报价规则保留,可查看每个工作表的完整单元格</span>
</span>
</div>
<div class="quote-table-wrap">
<table class="quote-table">
<thead>
<tr>
<th scope="col">报价版本</th>
<th scope="col">快递公司</th>
<th scope="col">报价类型</th>
<th scope="col">原始范围</th>
<th scope="col">数据规模</th>
<th scope="col">执行日期</th>
<th scope="col">签章</th>
<th scope="col">资料状态</th>
<th scope="col">操作</th>
</tr>
</thead>
<tbody data-reverse-table-body></tbody>
</table>
</div>
</div>
@else
<div class="quote-workspace">
<div class="quote-empty" role="status">
<div class="quote-empty__body">
<span class="material-icons-outlined" aria-hidden="true">assignment_return</span>
<div class="quote-empty__title">{{ $reverseQuoteSnapshot['message'] }}</div>
<div class="quote-empty__text">导入本机逆向物流报价文件后,会在此显示各承运商的报价版本和运价明细。</div>
</div>
</div>
</div>
@endif
@if ($reverseQuoteSnapshot['available'])
<div class="quote-modal" role="dialog" aria-modal="true" aria-labelledby="reverse-quote-detail-title" data-reverse-modal>
<div class="quote-modal__panel">
<header class="quote-modal__header">
<div class="quote-modal__title-wrap">
<div class="quote-modal__title" id="reverse-quote-detail-title" data-reverse-detail="title"></div>
<div class="quote-modal__subtitle">原工作表内容 · 只读</div>
</div>
<button class="quote-modal__close" type="button" aria-label="关闭报价明细" title="关闭" data-reverse-close>
<span class="material-icons-outlined" aria-hidden="true">close</span>
</button>
</header>
<dl class="quote-detail-meta">
<div><dt>版本号</dt><dd data-reverse-detail="id"></dd></div>
<div><dt>快递公司</dt><dd data-reverse-detail="carrier"></dd></div>
<div><dt>报价类型</dt><dd data-reverse-detail="type"></dd></div>
<div><dt>原始范围</dt><dd data-reverse-detail="range"></dd></div>
<div><dt>执行日期</dt><dd data-reverse-detail="date"></dd></div>
<div><dt>公式单元格</dt><dd data-reverse-detail="formulas"></dd></div>
</dl>
<div class="quote-detail-table-wrap">
<table class="quote-detail-table" data-reverse-detail-table></table>
</div>
</div>
</div>
<script>
(function () {
var root = document.querySelector('[data-reverse-logistics-quotes]');
if (!root || root.dataset.initialized === 'true') {
return;
}
root.dataset.initialized = 'true';
var quotes = @json($reverseQuotes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
var rates = @json($reverseRates, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
var rules = @json($reverseRules, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
var body = root.querySelector('[data-reverse-table-body]');
var rateBody = root.querySelector('[data-reverse-rate-body]');
var rateCount = root.querySelector('[data-reverse-rate-count]');
var rateEmpty = root.querySelector('[data-reverse-rate-empty]');
var rateTableWrap = root.querySelector('[data-reverse-rate-panel] .quote-table-wrap');
var rateFilters = {
carrier: root.querySelector('[data-reverse-rate-filter="carrier"]'),
origin: root.querySelector('[data-reverse-rate-filter="origin"]'),
destination: root.querySelector('[data-reverse-rate-filter="destination"]'),
keyword: root.querySelector('[data-reverse-rate-filter="keyword"]')
};
var ruleByCarrier = {};
var modal = root.querySelector('[data-reverse-modal]');
var lastFocusedElement = null;
rules.forEach(function (rule) {
ruleByCarrier[rule.carrier] = rule;
});
function text(value, fallback) {
if (value === null || value === undefined || value === '') {
return fallback || '';
}
return String(value);
}
function cellText(value) {
if (typeof value === 'boolean') {
return value ? 'TRUE' : 'FALSE';
}
return text(value, '');
}
function amountText(value) {
if (value === null || value === undefined || value === '') {
return '—';
}
var number = Number(value);
if (!isFinite(number)) {
return String(value);
}
return String(Math.round(number * 10000) / 10000);
}
function create(tag, className, value) {
var element = document.createElement(tag);
if (className) {
element.className = className;
}
if (value !== undefined) {
element.textContent = value;
}
return element;
}
function appendCell(row, value, className) {
var cell = create('td', className || '', value);
row.appendChild(cell);
return cell;
}
function renderRateRow(rate) {
var rule = ruleByCarrier[rate.carrier] || {};
var row = create('tr');
appendCell(row, rate.carrier);
appendCell(row, rate.origin);
appendCell(row, rate.destination);
appendCell(row, text(rule.pricing_label, '-'));
appendCell(row, amountText(rate.base_amount));
appendCell(row, amountText(rate.increment_amount), rate.available ? '' : 'reverse-rate-missing');
appendCell(row, text(rate.weight_band, '首重 + 续重'), 'reverse-rate-band');
appendCell(row, text(rule.effective_date, '未标注'));
appendCell(row, text(rate.source, '-'), 'quote-table__secondary');
return row;
}
function applyRateFilters() {
if (!rateBody) {
return;
}
var carrier = rateFilters.carrier.value;
var origin = rateFilters.origin.value;
var destination = rateFilters.destination.value;
var keyword = rateFilters.keyword.value.trim().toLowerCase();
var filtered = rates.filter(function (rate) {
if (carrier && rate.carrier !== carrier) {
return false;
}
if (origin && rate.origin !== origin) {
return false;
}
if (destination && rate.destination !== destination) {
return false;
}
if (keyword) {
var haystack = (rate.origin + ' ' + rate.destination + ' ' + rate.carrier + ' ' + text(rate.source, '')).toLowerCase();
if (haystack.indexOf(keyword) === -1) {
return false;
}
}
return true;
});
rateBody.replaceChildren();
filtered.forEach(function (rate) {
rateBody.appendChild(renderRateRow(rate));
});
if (rateCount) {
rateCount.textContent = '共 ' + filtered.length + ' 条运价(全部 ' + rates.length + ' 条)';
}
if (rateEmpty) {
rateEmpty.hidden = filtered.length > 0;
}
if (rateTableWrap) {
rateTableWrap.hidden = filtered.length === 0;
}
}
function renderRow(quote) {
var row = create('tr');
var nameCell = appendCell(row, '');
nameCell.appendChild(create('span', 'quote-table__name', quote.id));
appendCell(row, quote.company_name);
appendCell(row, quote.quote_type);
appendCell(row, text(quote.source_range, '-'));
appendCell(row, quote.row_count + ' 行 × ' + quote.column_count + ' 列');
appendCell(row, text(quote.effective_date, '-'));
appendCell(row, quote.signature_status, 'quote-table__secondary');
var statusCell = appendCell(row, '');
statusCell.appendChild(create(
'span',
'quote-badge ' + (quote.effective_date ? 'quote-badge--ready' : 'quote-badge--attention'),
quote.status
));
var actionCell = appendCell(row, '');
var action = create('button', 'quote-action', '查看');
action.type = 'button';
action.dataset.reverseOpen = quote.id;
action.setAttribute('aria-label', '查看 ' + quote.sheet_name);
actionCell.appendChild(action);
return row;
}
function setDetail(name, value) {
root.querySelector('[data-reverse-detail="' + name + '"]').textContent = value;
}
function excelColumn(index) {
var label = '';
var value = index + 1;
while (value > 0) {
value--;
label = String.fromCharCode(65 + (value % 26)) + label;
value = Math.floor(value / 26);
}
return label;
}
function openDetail(quote, trigger) {
lastFocusedElement = trigger;
setDetail('title', quote.sheet_name);
setDetail('id', quote.id);
setDetail('carrier', quote.company_name);
setDetail('type', quote.quote_type);
setDetail('range', text(quote.source_range, '-'));
setDetail('date', text(quote.effective_date, 'Excel 未标注'));
setDetail('formulas', String(quote.formula_count));
var table = root.querySelector('[data-reverse-detail-table]');
var head = create('thead');
var headerRow = create('tr');
headerRow.appendChild(create('th', 'quote-detail-table__row-number', '#'));
for (var column = 0; column < quote.column_count; column++) {
headerRow.appendChild(create('th', '', excelColumn(column)));
}
head.appendChild(headerRow);
var tableBody = create('tbody');
quote.values.forEach(function (values, rowIndex) {
var row = create('tr');
row.appendChild(create('th', 'quote-detail-table__row-number', String(rowIndex + 1)));
for (var columnIndex = 0; columnIndex < quote.column_count; columnIndex++) {
var cell = create('td', '', cellText(values[columnIndex]));
var formula = quote.formulas[rowIndex] && quote.formulas[rowIndex][columnIndex];
if (formula) {
cell.appendChild(create('span', 'quote-detail-table__formula', '=' + String(formula).replace(/^=/, '')));
}
row.appendChild(cell);
}
tableBody.appendChild(row);
});
table.replaceChildren(head, tableBody);
modal.classList.add('is-open');
document.body.style.overflow = 'hidden';
root.querySelector('[data-reverse-close]').focus();
}
function closeDetail() {
modal.classList.remove('is-open');
document.body.style.overflow = '';
if (lastFocusedElement) {
lastFocusedElement.focus();
}
}
quotes.forEach(function (quote) {
body.appendChild(renderRow(quote));
});
Object.keys(rateFilters).forEach(function (key) {
if (!rateFilters[key]) {
return;
}
rateFilters[key].addEventListener('input', applyRateFilters);
rateFilters[key].addEventListener('change', applyRateFilters);
});
var rateReset = root.querySelector('[data-reverse-rate-reset]');
if (rateReset) {
rateReset.addEventListener('click', function () {
Object.keys(rateFilters).forEach(function (key) {
if (rateFilters[key]) {
rateFilters[key].value = '';
}
});
applyRateFilters();
});
}
var calcFields = {
carrier: root.querySelector('[data-reverse-calc="carrier"]'),
origin: root.querySelector('[data-reverse-calc="origin"]'),
destination: root.querySelector('[data-reverse-calc="destination"]'),
weight: root.querySelector('[data-reverse-calc="weight"]'),
length: root.querySelector('[data-reverse-calc="length"]'),
width: root.querySelector('[data-reverse-calc="width"]'),
height: root.querySelector('[data-reverse-calc="height"]')
};
var calcBody = root.querySelector('[data-reverse-calc-body]');
var calcSummary = root.querySelector('[data-reverse-calc-summary]');
var calcVolume = root.querySelector('[data-reverse-calc-volume]');
var calcCarriers = rules.map(function (rule) { return rule.carrier; });
var rateIndex = {};
rates.forEach(function (rate) {
rateIndex[rate.carrier] = rateIndex[rate.carrier] || {};
var key = rate.origin_key + '|' + rate.destination_key;
rateIndex[rate.carrier][key] = rateIndex[rate.carrier][key] || [];
rateIndex[rate.carrier][key].push(rate);
});
function calcNumber(field) {
if (!field) {
return 0;
}
var value = parseFloat(field.value);
return isFinite(value) && value > 0 ? value : 0;
}
function round2(value) {
return Math.round(value * 100) / 100;
}
function pickBand(bucket, band) {
for (var index = 0; index < bucket.length; index++) {
if (bucket[index].weight_band === band) {
return bucket[index];
}
}
return null;
}
function computeFreight(carrier, context) {
var rule = ruleByCarrier[carrier] || {};
var bucket = (rateIndex[carrier] || {})[context.key] || [];
var row = {
carrier: carrier,
route: context.origin + ' → ' + context.destination,
weight: '—',
formula: '',
total: null,
surcharge: 0,
notes: []
};
if (!bucket.length) {
row.notes.push('原表未覆盖该线路');
return row;
}
var rate = bucket[0];
var billable = 0;
var total = 0;
if (rule.pricing_mode === 'first_weight_increment') {
billable = Math.max(1, Math.ceil(context.chargeable));
total = Number(rate.base_amount) + Number(rate.increment_amount) * (billable - 1);
row.formula = '面单 ' + rate.base_amount + ' + 续重 ' + rate.increment_amount + ' ×(' + billable + ' − 1)';
row.notes.push('续重按结算重量取整(原表备注 3)');
} else if (rule.weight_bands && rule.weight_bands.length > 0) {
billable = Math.max(15, Math.ceil(context.chargeable));
var band = billable > 30 ? '31-60kg' : '0-30kg';
rate = pickBand(bucket, band) || rate;
if (!rate.available) {
row.notes.push('该线路重量段无报价');
return row;
}
total = Number(rate.base_amount) + Number(rate.increment_amount) * billable;
row.formula = '面单 ' + rate.base_amount + ' + 费率 ' + rate.increment_amount + ' × ' + billable + '(' + band + ')';
row.notes.push('不足 15kg 按 15kg 计费并向上取整(原表备注 2)');
if (billable > 60 || context.maxSide > 200) {
var surcharge = Math.max(20, billable * 1);
row.surcharge = round2(surcharge);
row.notes.push('超长超重附加费 ' + round2(surcharge) + ' 元(最长边>2m 或 计费重量>60kg,1 元/kg 最低 20 元)');
}
} else {
billable = round2(context.chargeable);
total = Number(rate.base_amount) + Number(rate.increment_amount) * billable;
row.formula = '面单 ' + rate.base_amount + ' + 费率 ' + rate.increment_amount + ' × ' + billable;
if (context.maxSide > 160 || context.sideSum > 250) {
row.surcharge = 7;
row.notes.push('单边>1.6m 或 三边>2.5m,加收 7 元/单(原表备注 1)');
}
}
row.weight = billable;
row.total = round2(total);
return row;
}
function calculate() {
if (!calcBody) {
return;
}
var settlement = calcNumber(calcFields.weight);
var length = calcNumber(calcFields.length);
var width = calcNumber(calcFields.width);
var height = calcNumber(calcFields.height);
var hasSize = length > 0 && width > 0 && height > 0;
var volumeWeight = hasSize ? (length * width * height / 6000) : 0;
var chargeable = Math.max(settlement, volumeWeight);
var origin = calcFields.origin.value;
var destination = calcFields.destination.value;
var selected = calcFields.carrier.value;
var context = {
key: origin + '|' + destination,
origin: origin,
destination: destination,
settlement: settlement,
chargeable: chargeable,
maxSide: Math.max(length, width, height),
sideSum: length + width + height
};
var carriers = selected ? [selected] : calcCarriers;
var results = carriers.map(function (carrier) { return computeFreight(carrier, context); });
var bestTotal = null;
var bestCarrier = null;
results.forEach(function (item) {
if (item.total === null) {
return;
}
var itemTotal = round2(item.total + (item.surcharge || 0));
if (bestTotal === null || itemTotal < bestTotal) {
bestTotal = itemTotal;
bestCarrier = item.carrier;
}
});
calcBody.replaceChildren();
results.forEach(function (item) {
var row = create('tr');
if (item.total !== null && round2(item.total + (item.surcharge || 0)) === bestTotal && carriers.length > 1) {
row.className = 'reverse-calc-best';
}
appendCell(row, item.carrier);
appendCell(row, item.route);
appendCell(row, item.weight === '—' ? '—' : String(item.weight));
appendCell(row, item.formula || '—');
appendCell(row, item.total === null ? '—' : item.total.toFixed(2));
appendCell(row, (item.surcharge || 0) > 0 ? Number(item.surcharge).toFixed(2) : '—');
appendCell(row, item.notes.join(';') || '按原表价格计算', 'quote-table__secondary');
calcBody.appendChild(row);
});
if (calcVolume) {
calcVolume.textContent = hasSize
? '体积重 ' + round2(volumeWeight) + ' kg(' + length + '×' + width + '×' + height + '÷6000),结算重量取较大值 ' + round2(chargeable) + ' kg'
: '未填写长宽高,按结算重量计算';
}
if (calcSummary) {
calcSummary.textContent = bestTotal === null
? origin + ' → ' + destination + ':所选承运商无可用报价'
: origin + ' → ' + destination + ' 最低总价 ' + bestCarrier + ' ' + bestTotal.toFixed(2) + ' 元(含加收,结算重量 ' + round2(chargeable) + ' kg)';
}
}
Object.keys(calcFields).forEach(function (key) {
if (!calcFields[key]) {
return;
}
calcFields[key].addEventListener('input', calculate);
calcFields[key].addEventListener('change', calculate);
});
var calcReset = root.querySelector('[data-reverse-calc-reset]');
if (calcReset) {
calcReset.addEventListener('click', function () {
if (calcFields.carrier) {
calcFields.carrier.value = '';
}
if (calcFields.origin) {
calcFields.origin.value = '广东';
}
if (calcFields.destination) {
calcFields.destination.value = '江苏';
}
if (calcFields.weight) {
calcFields.weight.value = '10';
}
['length', 'width', 'height'].forEach(function (key) {
if (calcFields[key]) {
calcFields[key].value = '';
}
});
calculate();
});
}
body.addEventListener('click', function (event) {
var trigger = event.target.closest('[data-reverse-open]');
if (!trigger) {
return;
}
var quote = quotes.find(function (item) { return item.id === trigger.dataset.reverseOpen; });
if (quote) {
openDetail(quote, trigger);
}
});
root.querySelector('[data-reverse-close]').addEventListener('click', closeDetail);
modal.addEventListener('click', function (event) {
if (event.target === modal) {
closeDetail();
}
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && modal.classList.contains('is-open')) {
closeDetail();
}
});
applyRateFilters();
calculate();
}());
</script>
@endif
</div>