363 lines
43 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
$rows = $courierSurchargeRows ?? collect();
$filters = $courierSurchargeFilters ?? [];
$summary = $courierSurchargeSummary ?? [];
$options = $courierSurchargeOptions ?? [];
$ready = (bool) ($courierSurchargeReady ?? false);
$typeUsage = $courierSurchargeTypeUsage ?? [];
$statusClass = static fn (string $status): string => $status === '已启用' ? 'is-active' : 'is-inactive';
$columns = [
['key' => 'id', 'label' => '规则编号', 'fixed' => true, 'exportable' => true],
['key' => 'courier', 'label' => '快递名称', 'exportable' => true],
['key' => 'scope', 'label' => '快递归属', 'exportable' => true],
['key' => 'reconciler', 'label' => '对账人', 'exportable' => true],
['key' => 'type', 'label' => '附加费类型', 'exportable' => true],
['key' => 'mode', 'label' => '结算方式', 'exportable' => true],
['key' => 'standard', 'label' => '计价标准', 'exportable' => true],
['key' => 'condition', 'label' => '生效条件', 'exportable' => true],
['key' => 'effective_at', 'label' => '生效日期', 'exportable' => true],
['key' => 'status', 'label' => '状态', 'exportable' => true],
['key' => 'remark', 'label' => '备注', 'exportable' => true],
['key' => 'actions', 'label' => '操作', 'fixed' => true, 'exportable' => false],
];
$columnStorageKey = sprintf(
'caiwu:table-columns:%d:%d:courier-surcharge:quotes',
(int) auth()->id(),
(int) company_id(),
);
@endphp
<section class="surcharge-maintenance" aria-label="附加费报价维护" data-surcharge-maintenance data-column-storage-key="{{ $columnStorageKey }}">
<style>
.surcharge-maintenance { --sm-ink:var(--vip-content-ink,#171717); --sm-muted:var(--vip-content-muted,#595959); --sm-accent:var(--vip-content-accent-dark,#856632); --sm-soft:var(--vip-content-accent-soft,#F7F0DE); --sm-border:var(--vip-border,#D8D2C7); display:grid; gap:1rem; color:var(--sm-ink); font-family:JetBrains Mono,"Microsoft YaHei","PingFang SC",monospace; font-size:.8125rem; }
.surcharge-maintenance *, .surcharge-maintenance *::before, .surcharge-maintenance *::after { box-sizing:border-box; }
.sm-notice { display:flex; align-items:flex-start; gap:.65rem; padding:.75rem 1rem; border:1px solid #d7c39d; border-radius:.65rem; background:#fffaf0; color:#6a5129; line-height:1.55; }
.sm-notice .material-icons-outlined { color:var(--sm-accent); font-size:1.15rem; }
.sm-notice strong { color:var(--sm-ink); }
.sm-summary { display:grid; grid-template-columns:1.35fr repeat(4,minmax(8rem,1fr)); overflow:hidden; border:1px solid var(--sm-border); border-radius:.75rem; background:#fff; }
.sm-summary__intro, .sm-metric { min-width:0; padding:1rem 1.15rem; }
.sm-summary__intro { display:flex; align-items:center; gap:.75rem; }
.sm-summary__icon { display:grid; flex:none; width:2.4rem; height:2.4rem; place-items:center; border-radius:.6rem; background:var(--sm-soft); color:var(--sm-accent); }
.sm-summary__title { font-size:.95rem; font-weight:700; } .sm-summary__subtitle { margin-top:.2rem; color:var(--sm-muted); font-size:.72rem; }
.sm-metric { border-left:1px solid var(--sm-border); } .sm-metric__label { color:var(--sm-muted); font-size:.72rem; } .sm-metric__value { margin-top:.25rem; font-size:1.2rem; font-weight:700; font-variant-numeric:tabular-nums; } .sm-metric__value.is-active { color:#53793e; } .sm-metric__value.is-inactive { color:#935f07; }
.sm-card { overflow:visible; border:1px solid var(--sm-border); border-radius:.75rem; background:#fff; }
.sm-card__head { display:flex; align-items:flex-start; justify-content:space-between; flex-wrap:wrap; gap:.75rem 1rem; padding:.85rem 1.15rem; border-bottom:1px solid var(--sm-border); }
.sm-card__title { display:flex; align-items:center; gap:.45rem; font-size:.9rem; font-weight:700; } .sm-card__title .material-icons-outlined { color:var(--sm-accent); font-size:1.1rem; } .sm-card__hint { color:var(--sm-muted); font-size:.72rem; }
.sm-toolbar { display:grid; grid-template-columns:repeat(4,minmax(10rem,1fr)); gap:.7rem; align-items:end; padding:1rem 1.15rem; background:#fffdf8; }
.sm-field { display:grid; gap:.3rem; min-width:0; } .sm-field label { color:var(--sm-muted); font-size:.72rem; font-weight:600; } .sm-field input,.sm-field select,.sm-form input,.sm-form select,.sm-form textarea { width:100%; min-width:0; padding:0 .6rem; border:1px solid var(--sm-border); border-radius:.45rem; background:#fff; color:var(--sm-ink); font:inherit; } .sm-field input,.sm-field select { height:2.2rem; }
.sm-field input:focus,.sm-field select:focus,.sm-form input:focus,.sm-form select:focus,.sm-form textarea:focus { border-color:var(--sm-accent); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.18); }
.sm-actions { display:flex; flex-wrap:wrap; gap:.45rem; } .sm-button { display:inline-flex; align-items:center; justify-content:center; gap:.35rem; height:2.2rem; padding:0 .75rem; border:1px solid var(--sm-border); border-radius:.45rem; background:#fff; color:var(--sm-ink); font:inherit; font-weight:600; text-decoration:none; white-space:nowrap; cursor:pointer; } .sm-button .material-icons-outlined { font-size:1rem; } .sm-button--primary { border-color:var(--sm-accent); background:var(--sm-accent); color:#fff; } .sm-button--primary:hover { background:#6a5129; } .sm-button:disabled { cursor:not-allowed; opacity:.55; }
.sm-table-wrap { max-height:min(62vh,42rem); overflow:auto; overscroll-behavior:contain; scrollbar-gutter:stable both-edges; } .sm-table { width:max-content; min-width:100%; border-collapse:separate; border-spacing:0; font-size:.75rem; } .sm-table th,.sm-table td { min-width:7.5rem; padding:.62rem .7rem; border-right:1px solid var(--sm-border); border-bottom:1px solid var(--sm-border); text-align:left; vertical-align:top; white-space:nowrap; } .sm-table th { position:sticky; top:0; z-index:2; background:var(--sm-soft); font-weight:700; } .sm-table [data-column-key="id"] { position:sticky; left:0; z-index:3; background:#fff; } .sm-table th[data-column-key="id"] { z-index:5; background:var(--sm-soft); } .sm-table [data-column-key="actions"] { position:sticky; right:0; z-index:3; background:#fff; box-shadow:-1px 0 0 var(--sm-border); } .sm-table th[data-column-key="actions"] { z-index:5; background:var(--sm-soft); } .sm-table tr:hover [data-column-key="id"],.sm-table tr:hover [data-column-key="actions"] { background:#fcfaf4; } .sm-table th[data-fixed="true"] { cursor:default; } .sm-table th[data-column-header]:not([data-fixed="true"]) { cursor:grab; } .sm-table th[data-column-header]:not([data-fixed="true"]):active { cursor:grabbing; } .sm-table th.is-dragging { opacity:.55; } .sm-table th.is-drop-target { box-shadow:inset 3px 0 0 var(--sm-accent); } .sm-table th .sm-table__header { display:inline-flex; align-items:center; gap:.35rem; } .sm-table th .sm-table__drag { color:var(--sm-muted); font-size:1rem; } .sm-table [data-column-key].is-column-hidden { display:none; } .sm-table tr:hover td { background:#fcfaf4; } .sm-table .wrap { max-width:18rem; white-space:normal; line-height:1.5; } .sm-table .actions { display:flex; gap:.35rem; } .sm-table .actions button { height:1.8rem; padding:0 .5rem; font-size:.7rem; }
.sm-table-meta { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:.6rem 1rem; padding:.65rem 1.15rem; border-bottom:1px solid var(--sm-border); background:#fffdf8; color:var(--sm-muted); font-size:.72rem; }
.sm-column-picker { position:relative; } .sm-column-picker__panel { position:absolute; right:0; z-index:20; display:grid; width:min(22rem,calc(100vw - 2rem)); gap:.65rem; margin-top:.35rem; padding:.8rem; border:1px solid var(--sm-border); border-radius:.55rem; background:#fff; box-shadow:0 4px 8px rgba(23,23,23,.1); } .sm-column-picker__panel[hidden] { display:none; } .sm-column-picker__intro { display:grid; gap:.2rem; color:var(--sm-muted); font-size:.7rem; line-height:1.45; } .sm-column-picker__intro strong { color:var(--sm-ink); font-size:.78rem; } .sm-visually-hidden { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; } .sm-column-picker__list { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.35rem; max-height:15rem; overflow:auto; } .sm-column-picker__option { display:flex; align-items:center; gap:.35rem; min-width:0; padding:.35rem .4rem; border:1px solid transparent; border-radius:.35rem; color:var(--sm-ink); font-size:.7rem; cursor:grab; } .sm-column-picker__option:hover,.sm-column-picker__option.is-drop-target { border-color:var(--sm-border); background:#fffdf8; } .sm-column-picker__option.is-fixed { cursor:default; } .sm-column-picker__option.is-dragging { opacity:.55; } .sm-column-picker__option input { flex:none; accent-color:var(--sm-accent); } .sm-column-picker__option .material-icons-outlined { color:var(--sm-muted); font-size:.95rem; } .sm-column-picker__foot { display:flex; justify-content:flex-end; padding-top:.35rem; border-top:1px solid var(--sm-border); }
.sm-badge { display:inline-flex; align-items:center; padding:.15rem .5rem; border-radius:999px; font-size:.7rem; font-weight:700; } .sm-badge.is-active { background:#edf5e8; color:#53793e; } .sm-badge.is-inactive { background:#fff4df; color:#935f07; }
.sm-empty { padding:3rem 1rem; text-align:center; color:var(--sm-muted); } .sm-rule-note { padding:1rem 1.15rem; color:var(--sm-muted); font-size:.75rem; line-height:1.65; background:#fffdf8; }
.sm-type-manager { display:grid; gap:1rem; padding:1.15rem; } .sm-type-create { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:.7rem; align-items:end; padding-bottom:1rem; border-bottom:1px solid var(--sm-border); } .sm-type-list { display:grid; gap:.55rem; } .sm-type-list__head { display:grid; gap:.2rem; color:var(--sm-muted); font-size:.72rem; line-height:1.5; } .sm-type-list__head strong { color:var(--sm-ink); font-size:.8rem; } .sm-type-row { display:flex; align-items:center; justify-content:space-between; gap:.75rem; min-height:2.6rem; padding:.5rem .65rem; border:1px solid var(--sm-border); border-radius:.45rem; background:#fffdf8; } .sm-type-row > div { display:flex; align-items:center; flex-wrap:wrap; gap:.45rem; min-width:0; } .sm-type-row small { color:var(--sm-muted); font-size:.68rem; } .sm-type-row form { flex:none; }
.sm-modal { position:fixed; inset:0; z-index:70; display:none; align-items:flex-start; justify-content:center; padding:4rem 1rem; overflow-y:auto; background:rgba(23,23,23,.42); } .sm-modal.is-open { display:flex; } .sm-modal__panel { width:min(42rem,100%); overflow:hidden; border:1px solid var(--sm-border); border-radius:.75rem; background:#fff; box-shadow:0 1rem 3rem rgba(23,23,23,.2); } .sm-modal__head { display:flex; align-items:center; justify-content:space-between; padding:1rem 1.15rem; border-bottom:1px solid var(--sm-border); } .sm-modal__head h2 { margin:0; font-size:1rem; } .sm-modal__close { border:0; background:transparent; color:var(--sm-muted); cursor:pointer; } .sm-form { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.8rem; padding:1.15rem; } .sm-form__field { display:grid; gap:.3rem; } .sm-form__field--full { grid-column:span 2; } .sm-form__field label,.sm-form__field legend { color:var(--sm-muted); font-size:.72rem; font-weight:600; } .sm-form__types { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:.45rem; margin:0; padding:0; border:0; } .sm-type-option { display:flex; align-items:center; gap:.35rem; min-height:2.1rem; padding:.35rem .45rem; border:1px solid var(--sm-border); border-radius:.4rem; background:#fffdf8; color:var(--sm-ink); font-size:.72rem; cursor:pointer; } .sm-type-option:has(input:checked) { border-color:var(--sm-accent); background:var(--sm-soft); color:var(--sm-accent); font-weight:700; } .sm-type-option input { width:1rem; height:1rem; accent-color:var(--sm-accent); } .sm-form input,.sm-form select { height:2.25rem; } .sm-form textarea { min-height:4rem; padding:.55rem .6rem; resize:vertical; } .sm-form__foot { grid-column:span 2; display:flex; justify-content:flex-end; gap:.5rem; margin-top:.2rem; }
@media (max-width:1100px) { .sm-summary { grid-template-columns:repeat(3,1fr); } .sm-summary__intro { grid-column:span 3; border-bottom:1px solid var(--sm-border); } .sm-toolbar { grid-template-columns:repeat(3,minmax(0,1fr)); } .sm-toolbar > .sm-actions { grid-column:span 3; } }
@media (max-width:620px) { .sm-summary { grid-template-columns:repeat(2,1fr); } .sm-summary__intro { grid-column:span 2; } .sm-metric { border-left:0; border-top:1px solid var(--sm-border); } .sm-toolbar,.sm-form,.sm-type-create { grid-template-columns:minmax(0,1fr); } .sm-form__types { grid-template-columns:repeat(2,minmax(0,1fr)); } .sm-actions,.sm-form__field--full,.sm-form__foot { grid-column:auto; } .sm-actions .sm-button { flex:1; } }
</style>
<div class="sm-notice" role="note"><span class="material-icons-outlined" aria-hidden="true">{{ $ready ? 'verified' : 'info' }}</span><div><strong>{{ $ready ? '正式维护数据' : '数据表尚未就绪' }}</strong> {{ $ready ? '报价规则已保存到当前公司数据表,刷新页面后仍会保留;后续结算页面将按启用中的规则计算。' : '附加费报价数据表尚未完成迁移,当前不能保存规则。请由管理员在目标数据库完成迁移后再使用本页。' }}</div></div>
<div class="sm-summary" aria-label="报价维护概览">
<div class="sm-summary__intro"><span class="sm-summary__icon material-icons-outlined" aria-hidden="true">local_shipping</span><div><div class="sm-summary__title">快递附加费报价</div><div class="sm-summary__subtitle">统一维护各快递的附加费结算规则</div></div></div>
<div class="sm-metric"><div class="sm-metric__label">当前规则</div><div class="sm-metric__value">{{ $summary['total'] ?? 0 }} <small>条</small></div></div>
<div class="sm-metric"><div class="sm-metric__label">已启用</div><div class="sm-metric__value is-active">{{ $summary['active'] ?? 0 }} <small>条</small></div></div>
<div class="sm-metric"><div class="sm-metric__label">已停用</div><div class="sm-metric__value is-inactive">{{ $summary['inactive'] ?? 0 }} <small>条</small></div></div>
<div class="sm-metric"><div class="sm-metric__label">覆盖快递</div><div class="sm-metric__value">{{ $summary['couriers'] ?? 0 }} <small>家</small></div></div>
</div>
<div class="sm-card">
<div class="sm-card__head"><div class="sm-card__title"><span class="material-icons-outlined" aria-hidden="true">tune</span>筛选报价规则</div><div class="sm-actions"><button class="sm-button" type="button" data-type-manage @disabled(!$ready)><span class="material-icons-outlined" aria-hidden="true">settings</span>管理附加费类型</button><button class="sm-button sm-button--primary" type="button" data-surcharge-open @disabled(!$ready)><span class="material-icons-outlined" aria-hidden="true">add</span>新增报价规则</button></div></div>
<form method="GET" class="sm-toolbar" aria-label="报价规则筛选">
<div class="sm-field"><label for="surcharge-keyword">快递名称 / 标准</label><input id="surcharge-keyword" name="keyword" value="{{ $filters['keyword'] ?? '' }}" placeholder="输入关键字"></div>
<div class="sm-field"><label for="surcharge-courier">快递名称</label><select id="surcharge-courier" name="courier"><option value="">全部快递</option>@foreach (($options['couriers'] ?? []) as $option)<option value="{{ $option }}" @selected(($filters['courier'] ?? '') === $option)>{{ $option }}</option>@endforeach</select></div>
<div class="sm-field"><label for="surcharge-scope">快递归属</label><select id="surcharge-scope" name="scope"><option value="">全部归属</option>@foreach (($options['scopes'] ?? []) as $option)<option value="{{ $option }}" @selected(($filters['scope'] ?? '') === $option)>{{ $option }}</option>@endforeach</select></div>
<div class="sm-field"><label for="surcharge-reconciler">对账人</label><select id="surcharge-reconciler" name="reconciler"><option value="">全部对账人</option>@foreach (($options['reconcilers'] ?? []) as $option)<option value="{{ $option }}" @selected(($filters['reconciler'] ?? '') === $option)>{{ $option }}</option>@endforeach</select></div>
<div class="sm-field"><label for="surcharge-type">附加费类型</label><select id="surcharge-type" name="type"><option value="">全部类型</option>@foreach (($options['filter_types'] ?? $options['types'] ?? []) as $option)<option value="{{ $option }}" @selected(($filters['type'] ?? '') === $option)>{{ $option }}</option>@endforeach</select></div>
<div class="sm-field"><label for="surcharge-mode">结算方式</label><select id="surcharge-mode" name="mode"><option value="">全部方式</option>@foreach (($options['modes'] ?? []) as $option)<option value="{{ $option }}" @selected(($filters['mode'] ?? '') === $option)>{{ $option }}</option>@endforeach</select></div>
<div class="sm-field"><label for="surcharge-status">状态</label><select id="surcharge-status" name="status"><option value="">全部状态</option>@foreach (($options['statuses'] ?? []) as $option)<option value="{{ $option }}" @selected(($filters['status'] ?? '') === $option)>{{ $option }}</option>@endforeach</select></div>
<div class="sm-actions"><button class="sm-button sm-button--primary" type="submit"><span class="material-icons-outlined" aria-hidden="true">search</span>查询</button><a class="sm-button" href="{{ route('business.pages.show', ['section' => $section['slug'], 'page' => $page['slug']]) }}"><span class="material-icons-outlined" aria-hidden="true">refresh</span>重置</a></div>
</form>
</div>
<div class="sm-card">
<div class="sm-card__head"><div class="sm-card__title"><span class="material-icons-outlined" aria-hidden="true">rule</span>报价规则列表</div><div class="sm-actions"><div class="sm-column-picker" data-column-picker><button class="sm-button" type="button" aria-expanded="false" aria-controls="surcharge-column-options" data-column-toggle><span class="material-icons-outlined" aria-hidden="true">view_column</span>列设置</button><div class="sm-column-picker__panel" id="surcharge-column-options" role="group" aria-label="报价规则列设置" title="勾选字段可控制显示,拖动字段可调整顺序" hidden><div class="sm-column-picker__intro"><strong>列设置</strong><span class="sm-visually-hidden">勾选字段可控制显示,拖动字段可调整顺序。</span></div><div class="sm-column-picker__list" data-column-list>
@foreach ($columns as $column)
<label class="sm-column-picker__option {{ !empty($column['fixed']) ? 'is-fixed' : '' }}" draggable="{{ empty($column['fixed']) ? 'true' : 'false' }}" data-column-option data-column-key="{{ $column['key'] }}">
<input type="checkbox" value="{{ $column['key'] }}" data-column-check @checked(!empty($column['fixed']) || ($column['exportable'] ?? false)) @disabled(!empty($column['fixed']))>
@if (empty($column['fixed']))<span class="material-icons-outlined" aria-hidden="true">drag_handle</span>@endif
<span>{{ $column['label'] }}</span>
@if (!empty($column['fixed']))<small>固定</small>@endif
</label>
@endforeach
</div><div class="sm-column-picker__foot"><button class="sm-button" type="button" data-column-reset><span class="material-icons-outlined" aria-hidden="true">restart_alt</span>恢复默认</button></div></div></div><button class="sm-button" type="button" data-surcharge-export-current><span class="material-icons-outlined" aria-hidden="true">download</span>导出当前列</button><button class="sm-button" type="button" data-surcharge-export-all><span class="material-icons-outlined" aria-hidden="true">file_download</span>导出全部字段</button><span class="sm-card__hint">规则按生效时间应用到后续结算页面</span></div></div>
<div class="sm-table-meta"><span>当前筛选结果(全部):<strong data-surcharge-row-count>{{ $rows->count() }}</strong> 条</span><span>规则编号与操作列固定显示;列设置仅保存于当前用户</span></div>
<div class="sm-table-wrap" data-surcharge-table-wrap><table class="sm-table" data-surcharge-table><thead><tr>
@foreach ($columns as $column)
<th scope="col" draggable="{{ empty($column['fixed']) ? 'true' : 'false' }}" data-column-header data-column-key="{{ $column['key'] }}" data-column-label="{{ $column['label'] }}" data-fixed="{{ !empty($column['fixed']) ? 'true' : 'false' }}"><span class="sm-table__header">@if (empty($column['fixed']))<span class="sm-table__drag material-icons-outlined" aria-hidden="true">drag_handle</span>@endif<span>{{ $column['label'] }}</span></span></th>
@endforeach
</tr></thead><tbody>
@forelse ($rows as $row)
<tr data-surcharge-row data-rule='@json($row, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT)' data-update-url="{{ route('business.courier-surcharge.update', ['quote' => $row['db_id']]) }}">
<td data-column-key="id" data-export-value="{{ $row['id'] }}">{{ $row['id'] }}</td><td data-column-key="courier" data-export-value="{{ $row['courier'] }}">{{ $row['courier'] }}</td><td data-column-key="scope" data-export-value="{{ $row['scope'] }}">{{ $row['scope'] }}</td><td data-column-key="reconciler" data-export-value="{{ $row['reconciler'] }}">{{ $row['reconciler'] }}</td><td data-column-key="type" data-export-value="{{ $row['type'] }}">{{ $row['type'] }}</td><td data-column-key="mode" data-export-value="{{ $row['mode'] }}">{{ $row['mode'] }}</td><td data-column-key="standard" data-export-value="{{ $row['standard'] }}" class="wrap">{{ $row['standard'] }}</td><td data-column-key="condition" data-export-value="{{ $row['condition'] }}" class="wrap">{{ $row['condition'] }}</td><td data-column-key="effective_at" data-export-value="{{ $row['effective_at'] }}">{{ $row['effective_at'] }}</td><td data-column-key="status" data-export-value="{{ $row['status'] }}"><span class="sm-badge {{ $statusClass($row['status']) }}">{{ $row['status'] }}</span></td><td data-column-key="remark" data-export-value="{{ $row['remark'] ?: '—' }}" class="wrap">{{ $row['remark'] ?: '—' }}</td><td data-column-key="actions" data-fixed="true"><div class="sm-actions"><button class="sm-button" type="button" data-surcharge-edit @disabled(!$ready)>编辑</button><form method="POST" action="{{ route('business.courier-surcharge.status', ['quote' => $row['db_id']]) }}">@csrf<input type="hidden" name="status" value="{{ $row['status_code'] === 'active' ? 'inactive' : 'active' }}"><button class="sm-button" type="submit" @disabled(!$ready)>{{ $row['status'] === '已启用' ? '停用' : '启用' }}</button></form></div></td>
</tr>
@empty
<tr data-surcharge-empty><td data-empty-cell colspan="12"><div class="sm-empty">没有符合条件的报价规则,请点击“新增报价规则”。</div></td></tr>
@endforelse
</tbody></table></div>
<div class="sm-rule-note"><strong>维护建议:</strong>固定单价、按重量、按体积、按尺寸/最长边等方式直接维护标准;聚工单客服核实、取发出运费和按账单等方式只维护触发条件,具体金额由后续结算链路读取业务凭证。当前筛选结果可导出为 CSV;本页暂未启用批量导入,避免在未确认字段规则前覆盖维护数据。</div>
</div>
<div class="sm-modal" role="dialog" aria-modal="true" aria-labelledby="surcharge-modal-title" data-surcharge-modal>
<div class="sm-modal__panel"><div class="sm-modal__head"><h2 id="surcharge-modal-title">新增报价规则</h2><button class="sm-modal__close" type="button" aria-label="关闭" data-surcharge-close><span class="material-icons-outlined" aria-hidden="true">close</span></button></div>
<form class="sm-form" method="POST" action="{{ route('business.courier-surcharge.store') }}" data-surcharge-form>
@csrf
<input type="hidden" name="_method" value="POST" data-surcharge-method>
<div class="sm-form__field"><label for="surcharge-form-courier">快递名称</label><input id="surcharge-form-courier" name="courier" required placeholder="例如:曹县德邦"></div>
<div class="sm-form__field"><label for="surcharge-form-scope">快递归属</label><select id="surcharge-form-scope" name="scope" required><option value="">请选择</option>@foreach (($options['scopes'] ?? []) as $option)<option value="{{ $option }}">{{ $option }}</option>@endforeach</select></div>
<div class="sm-form__field"><label for="surcharge-form-reconciler">对账人</label><input id="surcharge-form-reconciler" name="reconciler" required placeholder="例如:张会计"></div>
<fieldset class="sm-form__field sm-form__field--full"><legend>附加费类型(可多选)</legend><div class="sm-form__types" id="surcharge-form-types">@foreach (($options['types'] ?? []) as $option)<label class="sm-type-option"><input type="checkbox" name="types[]" value="{{ $option }}"><span>{{ $option }}</span></label>@endforeach</div></fieldset>
<div class="sm-form__field"><label for="surcharge-form-mode">结算方式</label><select id="surcharge-form-mode" name="mode" required><option value="">请选择</option>@foreach (($options['modes'] ?? []) as $option)<option value="{{ $option }}">{{ $option }}</option>@endforeach</select></div>
<div class="sm-form__field"><label for="surcharge-form-standard">计价标准</label><input id="surcharge-form-standard" name="standard" required placeholder="例如:2 元/kg"></div>
<div class="sm-form__field sm-form__field--full"><label for="surcharge-form-condition">生效条件</label><input id="surcharge-form-condition" name="condition" required placeholder="例如:退回件,按结算重量计费"></div>
<div class="sm-form__field"><label for="surcharge-form-date">生效日期</label><input id="surcharge-form-date" type="date" name="effective_at" value="{{ now()->toDateString() }}" required></div>
<div class="sm-form__field"><label for="surcharge-form-remark">备注(可选)</label><input id="surcharge-form-remark" name="remark" placeholder="补充适用范围"></div>
<div class="sm-form__foot"><button class="sm-button" type="button" data-surcharge-close>取消</button><button class="sm-button sm-button--primary" type="submit"><span class="material-icons-outlined" aria-hidden="true">save</span>保存报价规则</button></div>
</form>
</div>
</div>
<div class="sm-modal" role="dialog" aria-modal="true" aria-labelledby="type-modal-title" data-type-modal>
<div class="sm-modal__panel"><div class="sm-modal__head"><h2 id="type-modal-title">管理附加费类型</h2><button class="sm-modal__close" type="button" aria-label="关闭" data-type-close><span class="material-icons-outlined" aria-hidden="true">close</span></button></div>
<div class="sm-type-manager">
<form class="sm-type-create" method="POST" action="{{ route('business.courier-surcharge.types.store') }}">
@csrf
<div class="sm-field"><label for="new-surcharge-type">新增附加费类型</label><input id="new-surcharge-type" name="name" required maxlength="100" placeholder="例如:偏远地区费"></div>
<button class="sm-button sm-button--primary" type="submit"><span class="material-icons-outlined" aria-hidden="true">add</span>新增类型</button>
</form>
<div class="sm-type-list"><div class="sm-type-list__head"><strong>类型列表</strong><span>停用后不会出现在报价规则表单中,历史规则仍保留原值。</span></div>
@forelse (($options['type_records'] ?? []) as $type)
<div class="sm-type-row"><div><strong>{{ $type['name'] }}</strong><span class="sm-badge {{ $type['status'] === 'active' ? 'is-active' : 'is-inactive' }}">{{ $type['status_label'] }}</span>@if (($typeUsage[$type['name']] ?? 0) > 0)<small>已用于 {{ $typeUsage[$type['name']] }} 条报价</small>@endif</div><form method="POST" action="{{ route('business.courier-surcharge.types.status', ['type' => $type['id']]) }}">@csrf<input type="hidden" name="status" value="{{ $type['status'] === 'active' ? 'inactive' : 'active' }}"><button class="sm-button" type="submit">{{ $type['status'] === 'active' ? '停用' : '启用' }}</button></form></div>
@empty
<div class="sm-empty">类型数据表尚未就绪。</div>
@endforelse
</div>
</div>
</div>
</div>
<script>
(() => {
const root = document.querySelector('[data-surcharge-maintenance]');
if (!root) return;
const table = root.querySelector('[data-surcharge-table]');
const tableWrap = root.querySelector('[data-surcharge-table-wrap]');
const picker = root.querySelector('[data-column-picker]');
const columnToggle = picker?.querySelector('[data-column-toggle]');
const columnPanel = picker?.querySelector('[data-column-options]') || picker?.querySelector('.sm-column-picker__panel');
const columnList = picker?.querySelector('[data-column-list]');
const checks = [...(picker?.querySelectorAll('[data-column-check]') || [])];
const headers = [...(table?.querySelectorAll('[data-column-header]') || [])];
const defaultOrder = headers.map((header) => header.dataset.columnKey);
const fixedKeys = new Set(headers.filter((header) => header.dataset.fixed === 'true').map((header) => header.dataset.columnKey));
const mutableKeys = defaultOrder.filter((key) => !fixedKeys.has(key));
const storageKey = root.dataset.columnStorageKey || 'caiwu:table-columns:courier-surcharge:quotes';
const emptyCell = table?.querySelector('[data-empty-cell]');
let state = { order: [...defaultOrder], visible: new Set(defaultOrder) };
const readState = () => {
try {
const saved = JSON.parse(window.localStorage.getItem(storageKey) || 'null');
if (!saved || typeof saved !== 'object') return;
const savedOrder = Array.isArray(saved.order) ? saved.order : [];
const order = [...new Set(savedOrder.filter((key) => defaultOrder.includes(key)))];
defaultOrder.forEach((key) => { if (!order.includes(key)) order.push(key); });
const visible = Array.isArray(saved.visible)
? new Set(saved.visible.filter((key) => defaultOrder.includes(key)))
: new Set(defaultOrder);
fixedKeys.forEach((key) => visible.add(key));
state = { order, visible };
} catch (_) {
state = { order: [...defaultOrder], visible: new Set(defaultOrder) };
}
};
const writeState = () => {
try {
window.localStorage.setItem(storageKey, JSON.stringify({ order: state.order, visible: [...state.visible] }));
} catch (_) {}
};
const orderedKeys = () => {
const movable = state.order.filter((key) => !fixedKeys.has(key) && mutableKeys.includes(key));
const result = [];
let movableIndex = 0;
defaultOrder.forEach((key) => {
result.push(fixedKeys.has(key) ? key : movable[movableIndex++]);
});
return result;
};
const moveChildren = (row, keys) => {
const cells = new Map([...row.children]
.filter((cell) => cell.dataset.columnKey)
.map((cell) => [cell.dataset.columnKey, cell]));
keys.forEach((key) => { if (cells.has(key)) row.appendChild(cells.get(key)); });
};
const applyColumns = (save = true) => {
if (!table) return;
const keys = orderedKeys();
moveChildren(table.tHead.rows[0], keys);
table.querySelectorAll('tbody tr').forEach((row) => moveChildren(row, keys));
table.querySelectorAll('[data-column-key]').forEach((cell) => {
cell.classList.toggle('is-column-hidden', !state.visible.has(cell.dataset.columnKey));
});
if (emptyCell) emptyCell.colSpan = Math.max(1, [...state.visible].filter((key) => defaultOrder.includes(key)).length);
checks.forEach((check) => {
check.checked = state.visible.has(check.value) || fixedKeys.has(check.value);
check.disabled = fixedKeys.has(check.value);
});
if (columnList) {
const options = new Map([...columnList.querySelectorAll('[data-column-option]')].map((option) => [option.dataset.columnKey, option]));
keys.forEach((key) => { if (options.has(key)) columnList.appendChild(options.get(key)); });
}
if (save) writeState();
};
const moveMutableKey = (sourceKey, targetKey, before = true) => {
if (!sourceKey || !targetKey || sourceKey === targetKey || fixedKeys.has(sourceKey) || fixedKeys.has(targetKey)) return;
const order = state.order.filter((key) => key !== sourceKey);
const targetIndex = order.indexOf(targetKey);
order.splice(Math.max(0, targetIndex + (before ? 0 : 1)), 0, sourceKey);
state.order = order;
applyColumns();
};
const csvEscape = (value) => {
const text = String(value ?? '');
const safe = /^[=+\-@]/.test(text) ? `'${text}` : text;
return `"${safe.replaceAll('"', '""')}"`;
};
const downloadCsv = (columns, filename) => {
if (!table) return;
const header = columns.map((column) => csvEscape(column.label)).join(',');
const lines = [...table.querySelectorAll('tbody tr[data-surcharge-row]')].map((row) => columns.map((column) => {
const cell = row.querySelector(`[data-column-key="${CSS.escape(column.key)}"]`);
return csvEscape(cell?.dataset.exportValue ?? cell?.textContent?.trim() ?? '');
}).join(','));
const blob = new Blob([`\ufeff${[header, ...lines].join('\n')}`], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
window.setTimeout(() => URL.revokeObjectURL(url), 0);
};
const columnLabels = new Map(headers.map((header) => [header.dataset.columnKey, header.dataset.columnLabel || header.textContent.trim()]));
const exportableKeys = defaultOrder.filter((key) => key !== 'actions');
const exportColumns = (keys) => keys.map((key) => ({ key, label: columnLabels.get(key) || key }));
columnToggle?.addEventListener('click', () => {
const opening = columnPanel?.hidden !== false;
if (columnPanel) columnPanel.hidden = !opening;
columnToggle.setAttribute('aria-expanded', String(opening));
});
document.addEventListener('click', (event) => {
if (picker && !picker.contains(event.target) && columnPanel?.hidden === false) {
columnPanel.hidden = true;
columnToggle?.setAttribute('aria-expanded', 'false');
}
});
checks.forEach((check) => check.addEventListener('change', () => {
if (fixedKeys.has(check.value)) return;
if (check.checked) state.visible.add(check.value); else state.visible.delete(check.value);
fixedKeys.forEach((key) => state.visible.add(key));
applyColumns();
}));
picker?.querySelector('[data-column-reset]')?.addEventListener('click', () => {
state = { order: [...defaultOrder], visible: new Set(defaultOrder) };
applyColumns();
});
const dragSource = (element, sourceKey) => {
element.addEventListener('dragstart', (event) => {
if (fixedKeys.has(sourceKey)) { event.preventDefault(); return; }
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', sourceKey);
element.classList.add('is-dragging');
});
element.addEventListener('dragend', () => element.classList.remove('is-dragging'));
};
headers.forEach((header) => {
if (header.dataset.fixed === 'true') return;
const key = header.dataset.columnKey;
dragSource(header, key);
header.addEventListener('dragover', (event) => {
if (fixedKeys.has(key)) return;
event.preventDefault();
header.classList.add('is-drop-target');
});
header.addEventListener('dragleave', () => header.classList.remove('is-drop-target'));
header.addEventListener('drop', (event) => {
event.preventDefault();
header.classList.remove('is-drop-target');
moveMutableKey(event.dataTransfer.getData('text/plain'), key, event.offsetX < header.offsetWidth / 2);
});
});
columnList?.querySelectorAll('[data-column-option]').forEach((option) => {
if (option.dataset.columnKey === undefined || fixedKeys.has(option.dataset.columnKey)) return;
const key = option.dataset.columnKey;
dragSource(option, key);
option.addEventListener('dragover', (event) => { event.preventDefault(); option.classList.add('is-drop-target'); });
option.addEventListener('dragleave', () => option.classList.remove('is-drop-target'));
option.addEventListener('drop', (event) => {
event.preventDefault();
option.classList.remove('is-drop-target');
moveMutableKey(event.dataTransfer.getData('text/plain'), key, event.offsetX < option.offsetWidth / 2);
});
});
root.querySelector('[data-surcharge-export-current]')?.addEventListener('click', () => {
downloadCsv(exportColumns(orderedKeys().filter((key) => state.visible.has(key) && exportableKeys.includes(key))), 'courier-surcharge-current.csv');
});
root.querySelector('[data-surcharge-export-all]')?.addEventListener('click', () => {
downloadCsv(exportColumns(exportableKeys), 'courier-surcharge-all-fields.csv');
});
tableWrap?.addEventListener('scroll', () => {
tableWrap.classList.toggle('is-scrolled-x', tableWrap.scrollLeft > 0);
}, { passive: true });
const modal = root.querySelector('[data-surcharge-modal]');
const typeModal = root.querySelector('[data-type-modal]');
const form = root.querySelector('[data-surcharge-form]');
const title = root.querySelector('#surcharge-modal-title');
const methodField = form.querySelector('[data-surcharge-method]');
const createAction = form.action;
const close = () => { modal.classList.remove('is-open'); form.reset(); form.action = createAction; methodField.value = 'POST'; title.textContent = '新增报价规则'; };
const closeType = () => { typeModal.classList.remove('is-open'); };
root.querySelector('[data-surcharge-open]')?.addEventListener('click', () => { close(); modal.classList.add('is-open'); root.querySelector('#surcharge-form-courier').focus(); });
root.querySelector('[data-type-manage]')?.addEventListener('click', () => { typeModal.classList.add('is-open'); root.querySelector('#new-surcharge-type').focus(); });
root.querySelectorAll('[data-surcharge-close]').forEach((button) => button.addEventListener('click', close));
root.querySelectorAll('[data-type-close]').forEach((button) => button.addEventListener('click', closeType));
modal.addEventListener('click', (event) => { if (event.target === modal) close(); });
typeModal.addEventListener('click', (event) => { if (event.target === typeModal) closeType(); });
root.querySelectorAll('[data-surcharge-edit]').forEach((button) => button.addEventListener('click', () => {
const row = button.closest('[data-surcharge-row]'); const data = JSON.parse(row.dataset.rule); form.action = row.dataset.updateUrl; methodField.value = 'PATCH'; title.textContent = '编辑报价规则';
['courier', 'scope', 'mode', 'standard', 'condition', 'effective_at', 'reconciler', 'remark'].forEach((key) => { const field = form.elements.namedItem(key); if (field) field.value = data[key] || ''; });
const selectedTypes = Array.isArray(data.types) ? data.types : (data.type || '').split('、').map((item) => item.trim()).filter(Boolean);
form.querySelectorAll('input[name="types[]"]').forEach((checkbox) => { checkbox.checked = selectedTypes.includes(checkbox.value); });
modal.classList.add('is-open');
}));
form.addEventListener('submit', (event) => {
const selectedTypes = Array.from(form.querySelectorAll('input[name="types[]"]:checked')).map((checkbox) => checkbox.value);
if (!selectedTypes.length) { event.preventDefault(); form.querySelector('#surcharge-form-types').focus(); }
});
document.addEventListener('keydown', (event) => { if (event.key !== 'Escape') return; if (modal.classList.contains('is-open')) close(); if (typeModal.classList.contains('is-open')) closeType(); if (columnPanel?.hidden === false) { columnPanel.hidden = true; columnToggle?.setAttribute('aria-expanded', 'false'); } });
readState();
applyColumns(false);
})();
</script>
</section>