944 lines
43 KiB
PHP
944 lines
43 KiB
PHP
@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>
|