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