limeng 8ba7ac8327 feat(快递入驻/快递资料): 生成二维码时必填快递资料并对齐到快递资料;去掉「发起报价变更」按钮;自定义3 改名扣款方式
- 生成入驻二维码新增 11 个必填项(快递方看不到,放在入驻流程下方单独一块):快递类型、快递合作方式、快递属性、扣款方式、单号类型、快递邮箱、
  是否需要核对物流点、快递发货方式、是否已有工作平台账号、快递网点代码、网点所在地区;入驻列表、入驻详情可查看和修改
- 快递资料:扣款方式对齐到「扣款方式」列(工作平台没填时用入驻的),其余字段接在入驻信息列里;网点代码/地区进网点明细;导出同步
- 不再依赖钉钉「新建快递」流程字段,ERP 开通改为手动补录 ERP账户ID
- 快递资料「自定义3」列、筛选、编辑和导出表头改名为「扣款方式」(字段仍是 undefined3)
- 报价管理工具栏、报价历史页去掉「发起报价变更」:它默认拿第一份报价走老的改格子流程,对入驻快递会绕开后台变价;
  变价统一从报价列表每一行的「变价」进入;老报价编辑页新增 …/changes/new 入口,避免套餐限制中间件把 GET …/create 跳到用户页
- 新增迁移 2026_10_14_300000:business_courier_onboardings 加 11 个可空字段

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019gzfsnV3fJ89CoUy1D9V9g
2026-10-14 18:31:25 +08:00

1259 lines
48 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

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

@php
try {
app(\App\Support\CourierQuoteSignatureService::class)->refreshPending();
} catch (\Throwable $exception) {
report($exception);
}
$quoteSnapshot = app(\App\Support\QuoteManagementSnapshot::class)->load();
$quotes = $quoteSnapshot['quotes'];
$quoteCompanies = collect($quotes)
->pluck('company_name')
->filter()
->unique()
->sort()
->values()
->all();
$quoteTypes = collect($quotes)->pluck('quote_type')->unique()->sort()->values();
$quoteView = in_array(request('quote_view'), ['warehouse', 'reverse-logistics'], true)
? request('quote_view')
: 'courier';
@endphp
<section class="quote-management" aria-label="报价管理" data-quote-management>
<style>
.quote-management {
--quote-accent: var(--vip-content-accent-dark, #856632);
--quote-accent-soft: var(--vip-content-accent-soft, #F7F0DE);
--quote-border: var(--vip-border, #D8D2C7);
--quote-ink: var(--vip-content-ink, #171717);
--quote-muted: var(--vip-content-muted, #595959);
display: grid;
width: 100%;
min-width: 0;
max-width: 100%;
gap: 1rem;
margin-top: 1.5rem;
color: var(--quote-ink);
font-family: JetBrains Mono, "Microsoft YaHei", "PingFang SC", monospace;
font-size: 0.8125rem;
letter-spacing: 0;
}
.quote-management *,
.quote-management *::before,
.quote-management *::after {
box-sizing: border-box;
}
.quote-local-tabs {
display: flex;
min-height: 2.75rem;
align-items: center;
gap: 1.5rem;
overflow-x: auto;
border-bottom: 1px solid var(--quote-border);
}
.quote-local-tab {
position: relative;
display: inline-flex;
min-height: 2.75rem;
flex: 0 0 auto;
align-items: center;
gap: .4rem;
border: 0;
background: transparent;
color: var(--quote-muted);
cursor: pointer;
font: inherit;
font-weight: 600;
}
.quote-local-tab .material-icons-outlined { font-size: 17px; }
.quote-local-tab.is-active { color: var(--quote-accent); }
.quote-local-tab.is-active::after { position:absolute; right:0; bottom:0; left:0; height:2px; background:var(--quote-accent); content:''; }
.quote-local-tab:focus-visible { outline:2px solid var(--quote-accent); outline-offset:2px; }
.quote-local-panel { display:grid; min-width:0; gap:1rem; }
.quote-summary,
.quote-workspace {
min-width: 0;
max-width: 100%;
width: 100%;
border: 1px solid var(--quote-border);
border-radius: 0.75rem;
background: #FFFFFF;
}
.quote-summary {
display: grid;
grid-template-columns: minmax(13rem, 1.4fr) repeat(4, minmax(7rem, 0.65fr));
align-items: stretch;
overflow: hidden;
}
.quote-source,
.quote-metric {
min-width: 0;
padding: 0.875rem 1rem;
}
.quote-source {
display: flex;
align-items: center;
gap: 0.75rem;
}
.quote-source__icon {
display: grid;
flex: 0 0 34px;
width: 34px;
height: 34px;
place-items: center;
border-radius: 8px;
background: var(--quote-accent-soft);
color: var(--quote-accent);
}
.quote-source__icon .material-icons-outlined {
font-size: 20px;
}
.quote-source__body {
min-width: 0;
}
.quote-source__file {
overflow: hidden;
font-size: 0.8125rem;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.quote-source__state,
.quote-metric__label,
.quote-table__secondary,
.quote-detail-meta dt {
color: var(--quote-muted);
font-size: 0.75rem;
}
.quote-metric {
border-left: 1px solid var(--quote-border);
}
.quote-metric__value {
margin-top: 0.2rem;
font-size: 1rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.quote-toolbar {
min-width: 0;
display: grid;
grid-template-columns: minmax(13rem, 1.2fr) repeat(3, minmax(9rem, 0.65fr)) auto;
gap: 0.75rem;
align-items: end;
padding: 1rem;
border-bottom: 1px solid var(--quote-border);
}
.quote-field {
display: grid;
gap: 0.35rem;
min-width: 0;
}
.quote-field__label {
color: var(--quote-muted);
font-size: 0.75rem;
font-weight: 500;
}
.quote-control,
.quote-icon-button,
.quote-button {
height: 34px;
border: 1px solid var(--quote-border);
border-radius: 8px;
background: #FFFFFF;
color: var(--quote-ink);
font: inherit;
}
.quote-control {
width: 100%;
min-width: 0;
padding: 0 10px;
}
.quote-control:focus,
.quote-icon-button:focus-visible,
.quote-button:focus-visible,
.quote-action:focus-visible,
.quote-modal__close:focus-visible {
border-color: var(--quote-accent);
outline: none;
box-shadow: 0 0 0 3px rgba(133, 102, 50, 0.20);
}
.quote-toolbar__actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.quote-icon-button,
.quote-button,
.quote-action,
.quote-modal__close {
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.quote-icon-button {
width: 34px;
padding: 0;
}
.quote-icon-button .material-icons-outlined,
.quote-button .material-icons-outlined {
font-size: 18px;
}
.quote-button {
gap: 0.4rem;
padding: 0 14px;
}
.quote-button--primary {
border-color: var(--quote-accent);
background: var(--quote-accent);
color: #FFFFFF;
}
.quote-button--primary:hover {
border-color: #6A5129;
background: #6A5129;
}
.quote-button[disabled] {
cursor: not-allowed;
opacity: 0.58;
}
.quote-results {
min-width: 0;
display: flex;
min-height: 2.75rem;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.65rem 1rem;
border-bottom: 1px solid var(--quote-border);
}
.quote-results__count {
color: var(--quote-muted);
font-size: 0.75rem;
font-variant-numeric: tabular-nums;
}
.quote-results__notice {
display: inline-flex;
align-items: center;
gap: 0.35rem;
color: var(--quote-accent);
font-size: 0.75rem;
}
.quote-results__notice .material-icons-outlined {
font-size: 16px;
}
.quote-table-wrap,
.quote-detail-table-wrap {
min-width: 0;
max-width: 100%;
width: 100%;
overflow: auto;
scrollbar-color: #B9954F #F2EFE8;
scrollbar-width: thin;
}
.quote-table-wrap::-webkit-scrollbar,
.quote-detail-table-wrap::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.quote-table-wrap::-webkit-scrollbar-thumb,
.quote-detail-table-wrap::-webkit-scrollbar-thumb {
border-radius: 4px;
background: #B9954F;
}
.quote-table-wrap::-webkit-scrollbar-track,
.quote-detail-table-wrap::-webkit-scrollbar-track {
background: #F2EFE8;
}
.quote-table,
.quote-detail-table {
width: 100%;
border-collapse: collapse;
font-size: 0.78125rem;
font-variant-numeric: tabular-nums;
}
.quote-table {
min-width: 67rem;
}
.quote-table th,
.quote-table td,
.quote-detail-table th,
.quote-detail-table td {
border-bottom: 1px solid var(--quote-border);
padding: 0.65rem 0.75rem;
text-align: left;
vertical-align: middle;
}
.quote-table th,
.quote-detail-table th {
background: var(--quote-accent-soft);
color: var(--quote-muted);
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
}
.quote-table tbody tr:hover td {
background: #FCFAF5;
}
.quote-table__name {
display: block;
max-width: 17rem;
overflow: hidden;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.quote-badge {
display: inline-flex;
align-items: center;
min-height: 24px;
border: 1px solid var(--quote-border);
border-radius: 999px;
padding: 0.15rem 0.55rem;
background: #FFFFFF;
color: var(--quote-muted);
font-size: 0.75rem;
white-space: nowrap;
}
.quote-badge--ready {
border-color: #A9C49A;
background: #F2F7EF;
color: #53793E;
}
.quote-badge--muted {
border-color: #DDD8CE;
background: #F4F2EE;
color: #8A8478;
}
.quote-badge--attention {
border-color: #D7B77D;
background: #FFF8E9;
color: #935F07;
}
.quote-signed {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
margin: 0.75rem 1rem 0;
padding: 0.6rem 0.8rem;
border: 1px solid #CFE3C8;
border-radius: 0.5rem;
background: #F1F8EE;
color: #2F6B2A;
}
.quote-modal__panel > .quote-detail-table-wrap {
grid-row: 4;
}
.quote-signed[hidden],
.quote-signed [hidden] {
display: none;
}
.quote-signed__text {
margin-right: auto;
font-weight: 600;
}
.quote-action {
min-width: 3.2rem;
border: 0;
padding: 0.25rem 0.4rem;
background: transparent;
color: var(--quote-accent);
font: inherit;
font-weight: 600;
}
.quote-action:hover {
background: var(--quote-accent-soft);
}
.quote-action[disabled] {
cursor: not-allowed;
opacity: 0.48;
}
.quote-action[disabled]:hover {
background: transparent;
}
.quote-empty {
display: grid;
min-height: 15rem;
place-items: center;
padding: 2rem;
text-align: center;
}
.quote-empty__body {
display: grid;
justify-items: center;
gap: 0.45rem;
max-width: 30rem;
}
.quote-empty .material-icons-outlined {
color: var(--quote-accent);
font-size: 32px;
}
.quote-empty__title {
font-weight: 600;
}
.quote-empty__text {
color: var(--quote-muted);
}
.quote-pagination {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
min-height: 3.5rem;
padding: 0.65rem 1rem;
}
.quote-pagination__label {
color: var(--quote-muted);
font-size: 0.75rem;
}
.quote-pagination__buttons {
display: flex;
gap: 0.5rem;
}
.quote-modal {
position: fixed;
z-index: 1200;
inset: 0;
display: none;
align-items: stretch;
justify-content: flex-end;
background: rgba(5, 5, 6, 0.52);
}
.quote-modal.is-open {
display: flex;
}
.quote-modal__panel {
display: grid;
grid-template-rows: auto auto auto minmax(0, 1fr);
width: min(78rem, calc(100vw - 3rem));
height: 100%;
background: #FFFFFF;
box-shadow: 0 4px 8px rgba(23, 23, 23, 0.10);
}
.quote-modal__header {
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--quote-border);
}
.quote-modal__title-wrap {
min-width: 0;
}
.quote-modal__title {
overflow-wrap: anywhere;
font-size: 0.875rem;
font-weight: 600;
}
.quote-modal__subtitle {
margin-top: 0.2rem;
color: var(--quote-muted);
font-size: 0.75rem;
}
.quote-modal__close {
flex: 0 0 34px;
width: 34px;
height: 34px;
border: 1px solid var(--quote-border);
border-radius: 8px;
background: #FFFFFF;
color: var(--quote-ink);
}
.quote-detail-meta {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
margin: 0;
border-bottom: 1px solid var(--quote-border);
}
.quote-detail-meta div {
min-width: 0;
padding: 0.75rem 1rem;
border-right: 1px solid var(--quote-border);
}
.quote-detail-meta div:last-child {
border-right: 0;
}
.quote-detail-meta dd {
margin: 0.2rem 0 0;
overflow-wrap: anywhere;
font-size: 0.78125rem;
font-weight: 600;
}
.quote-detail-table-wrap {
min-height: 0;
padding: 1rem 1.25rem 1.25rem;
}
.quote-detail-table {
min-width: max-content;
border-top: 1px solid var(--quote-border);
border-left: 1px solid var(--quote-border);
}
.quote-detail-table th,
.quote-detail-table td {
min-width: 8rem;
max-width: 24rem;
border-right: 1px solid var(--quote-border);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.quote-detail-table .quote-detail-table__row-number {
min-width: 3.75rem;
width: 3.75rem;
background: #F8F6F1;
color: var(--quote-muted);
text-align: center;
}
.quote-detail-table__formula {
display: block;
margin-top: 0.25rem;
color: var(--quote-accent);
font-size: 0.7rem;
}
[hidden] {
display: none !important;
}
@media (max-width: 1050px) {
.quote-summary {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.quote-source {
grid-column: 1 / -1;
border-bottom: 1px solid var(--quote-border);
}
.quote-metric:first-of-type {
border-left: 0;
}
.quote-toolbar {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.quote-toolbar__actions {
grid-column: 1 / -1;
justify-content: flex-end;
}
.quote-detail-meta {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.quote-detail-meta div:nth-child(3) {
border-right: 0;
}
.quote-detail-meta div:nth-child(-n + 3) {
border-bottom: 1px solid var(--quote-border);
}
}
@media (max-width: 640px) {
.quote-management {
margin-top: 1rem;
}
.quote-summary,
.quote-toolbar,
.quote-detail-meta {
grid-template-columns: 1fr;
}
.quote-source,
.quote-metric,
.quote-detail-meta div,
.quote-detail-meta div:nth-child(3) {
grid-column: auto;
border-right: 0;
border-bottom: 1px solid var(--quote-border);
}
.quote-metric:last-child,
.quote-detail-meta div:last-child {
border-bottom: 0;
}
.quote-toolbar__actions {
grid-column: auto;
justify-content: stretch;
}
.quote-toolbar__actions .quote-button {
flex: 1;
}
.quote-results,
.quote-pagination {
align-items: flex-start;
flex-direction: column;
}
.quote-pagination__buttons {
width: 100%;
}
.quote-pagination__buttons .quote-button {
flex: 1;
}
.quote-modal__panel {
width: 100%;
}
}
</style>
<nav class="quote-local-tabs" role="tablist" aria-label="报价管理分类">
<button type="button" role="tab" @class(['quote-local-tab', 'is-active' => $quoteView === 'courier']) data-quote-view="courier" aria-controls="quote-courier-panel" aria-selected="{{ $quoteView === 'courier' ? 'true' : 'false' }}">
<span class="material-icons-outlined" aria-hidden="true">local_shipping</span><span>快递报价</span>
</button>
<button type="button" role="tab" @class(['quote-local-tab', 'is-active' => $quoteView === 'warehouse']) data-quote-view="warehouse" aria-controls="quote-warehouse-panel" aria-selected="{{ $quoteView === 'warehouse' ? 'true' : 'false' }}">
<span class="material-icons-outlined" aria-hidden="true">warehouse</span><span>仓储报价</span>
</button>
<button type="button" role="tab" @class(['quote-local-tab', 'is-active' => $quoteView === 'reverse-logistics']) data-quote-view="reverse-logistics" aria-controls="quote-reverse-logistics-panel" aria-selected="{{ $quoteView === 'reverse-logistics' ? 'true' : 'false' }}">
<span class="material-icons-outlined" aria-hidden="true">assignment_return</span><span>逆向物流报价</span>
</button>
</nav>
<div class="quote-local-panel" id="quote-courier-panel" role="tabpanel" data-quote-panel="courier" @if ($quoteView !== 'courier') hidden @endif>
<div class="quote-summary" aria-label="报价数据概览">
<div class="quote-source">
<span class="quote-source__icon" aria-hidden="true">
<span class="material-icons-outlined">description</span>
</span>
<div class="quote-source__body">
<div class="quote-source__file" title="{{ $quoteSnapshot['source_file'] }}">{{ $quoteSnapshot['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" data-summary="quotes">{{ $quoteSnapshot['summary']['quote_count'] }}</div>
</div>
<div class="quote-metric">
<div class="quote-metric__label">快递品牌</div>
<div class="quote-metric__value">{{ $quoteSnapshot['summary']['carrier_count'] }}</div>
</div>
<div class="quote-metric">
<div class="quote-metric__label">已标执行日期</div>
<div class="quote-metric__value">{{ $quoteSnapshot['summary']['dated_count'] }}</div>
</div>
<div class="quote-metric">
<div class="quote-metric__label">原表公式单元格</div>
<div class="quote-metric__value">{{ $quoteSnapshot['summary']['formula_count'] }}</div>
</div>
</div>
<div class="quote-workspace">
<div class="quote-toolbar" aria-label="报价筛选">
<label class="quote-field">
<span class="quote-field__label">报价名称</span>
<input class="quote-control" type="search" name="keyword" placeholder="搜索快递公司或版本号" autocomplete="off" data-quote-filter="keyword">
</label>
<label class="quote-field">
<span class="quote-field__label">快递公司</span>
<select class="quote-control" name="carrier" data-quote-filter="carrier">
<option value="">全部公司</option>
@foreach ($quoteCompanies as $companyName)
<option value="{{ $companyName }}">{{ $companyName }}</option>
@endforeach
</select>
</label>
<label class="quote-field">
<span class="quote-field__label">报价类型</span>
<select class="quote-control" name="type" data-quote-filter="type">
<option value="">全部类型</option>
@foreach ($quoteTypes as $type)
<option value="{{ $type }}">{{ $type }}</option>
@endforeach
</select>
</label>
<label class="quote-field">
<span class="quote-field__label">资料状态</span>
<select class="quote-control" name="status" data-quote-filter="status">
<option value="">全部状态</option>
<option value="已标执行日期">已标执行日期</option>
<option value="未标执行日期">未标执行日期</option>
</select>
</label>
<div class="quote-toolbar__actions">
<button class="quote-icon-button" type="button" title="重置筛选" aria-label="重置筛选" data-quote-reset>
<span class="material-icons-outlined" aria-hidden="true">filter_alt_off</span>
</button>
@if ($quoteSnapshot['available'])
<a class="quote-button" href="{{ route('business.courier-quote-signatures.export') }}" title="把当前筛选结果里已签署的回签单 PDF 和签署完成证书打包下载" data-quote-export>
<span class="material-icons-outlined" aria-hidden="true">download</span>
<span data-quote-export-label>导出回签单</span>
</a>
@endif
<a class="quote-button" href="{{ route('business.courier-quote-changes.index') }}" title="查看报价变更草稿、审核状态和新版本">
<span class="material-icons-outlined" aria-hidden="true">history</span>
<span>报价历史</span>
</a>
</div>
</div>
@if ($quoteSnapshot['available'])
<div class="quote-results">
<span class="quote-results__count" data-quote-result-count>共 {{ count($quotes) }} 个报价版本</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-quote-table-body></tbody>
</table>
</div>
<div class="quote-empty" data-quote-filter-empty hidden>
<div class="quote-empty__body">
<span class="material-icons-outlined" aria-hidden="true">filter_alt_off</span>
<div class="quote-empty__title">没有符合当前条件的报价</div>
<div class="quote-empty__text">重置筛选后可查看全部已导入版本。</div>
</div>
</div>
<div class="quote-pagination" data-quote-pagination>
<span class="quote-pagination__label" data-quote-page-label></span>
<span class="quote-pagination__buttons">
<button class="quote-button" type="button" data-quote-page="previous">
<span class="material-icons-outlined" aria-hidden="true">chevron_left</span>
<span>上一页</span>
</button>
<button class="quote-button" type="button" data-quote-page="next">
<span>下一页</span>
<span class="material-icons-outlined" aria-hidden="true">chevron_right</span>
</button>
</span>
</div>
@else
<div class="quote-empty" role="status">
<div class="quote-empty__body">
<span class="material-icons-outlined" aria-hidden="true">file_present</span>
<div class="quote-empty__title">{{ $quoteSnapshot['message'] }}</div>
<div class="quote-empty__text">报价页面仍可访问,重新导入后会显示工作表列表。</div>
</div>
</div>
@endif
</div>
@if ($quoteSnapshot['available'])
<div class="quote-modal" role="dialog" aria-modal="true" aria-labelledby="quote-detail-title" data-quote-modal>
<div class="quote-modal__panel">
<header class="quote-modal__header">
<div class="quote-modal__title-wrap">
<div class="quote-modal__title" id="quote-detail-title" data-quote-detail="title"></div>
<div class="quote-modal__subtitle">原工作表内容 · 只读</div>
</div>
<button class="quote-modal__close" type="button" aria-label="关闭报价明细" title="关闭" data-quote-close>
<span class="material-icons-outlined" aria-hidden="true">close</span>
</button>
</header>
<dl class="quote-detail-meta">
<div><dt>版本号</dt><dd data-quote-detail="id"></dd></div>
<div><dt>快递公司</dt><dd data-quote-detail="carrier"></dd></div>
<div><dt>报价类型</dt><dd data-quote-detail="type"></dd></div>
<div><dt>原始范围</dt><dd data-quote-detail="range"></dd></div>
<div><dt>执行日期</dt><dd data-quote-detail="date"></dd></div>
<div><dt>公式单元格</dt><dd data-quote-detail="formulas"></dd></div>
</dl>
<div class="quote-signed" data-quote-signed hidden>
<span class="material-icons-outlined" aria-hidden="true">task_alt</span>
<span class="quote-signed__text" data-quote-signed-text>已签署回传</span>
<a class="quote-button" data-quote-signed-link="signed"><span class="material-icons-outlined" aria-hidden="true">picture_as_pdf</span><span>回签单 PDF</span></a>
<a class="quote-button" data-quote-signed-link="certificate"><span class="material-icons-outlined" aria-hidden="true">verified</span><span>签署完成证书</span></a>
<a class="quote-button" data-quote-signed-link="export" title="导出报价表(xlsx,WPS / Excel 可打开),只含省份价格"><span class="material-icons-outlined" aria-hidden="true">table_view</span><span>导出电子档</span></a>
</div>
<div class="quote-detail-table-wrap">
<table class="quote-detail-table" data-quote-detail-table></table>
</div>
</div>
</div>
<script>
(function () {
var root = document.querySelector('[data-quote-management]');
if (!root || root.dataset.initialized === 'true') {
return;
}
root.dataset.initialized = 'true';
var quotes = @json($quotes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
var pageSize = 12;
var currentPage = 1;
var filteredQuotes = quotes.slice();
var body = root.querySelector('[data-quote-table-body]');
var empty = root.querySelector('[data-quote-filter-empty]');
var tableWrap = root.querySelector('.quote-table-wrap');
var pagination = root.querySelector('[data-quote-pagination]');
var modal = root.querySelector('[data-quote-modal]');
var exportBase = @json(route('business.courier-quote-signatures.export'));
var exportAnchor = root.querySelector('[data-quote-export]');
if (exportAnchor) {
exportAnchor.addEventListener('click', function (event) {
if (exportAnchor.getAttribute('aria-disabled') === 'true') {
event.preventDefault();
}
});
}
var lastFocusedElement = null;
var fields = {
keyword: root.querySelector('[data-quote-filter="keyword"]'),
carrier: root.querySelector('[data-quote-filter="carrier"]'),
type: root.querySelector('[data-quote-filter="type"]'),
status: root.querySelector('[data-quote-filter="status"]')
};
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 create(tag, className, value) {
var element = document.createElement(tag);
if (className) {
element.className = className;
}
if (value !== undefined) {
element.textContent = value;
}
return element;
}
function updateUrl() {
var url = new URL(window.location.href);
Object.keys(fields).forEach(function (key) {
var value = fields[key].value.trim();
if (value) {
url.searchParams.set(key, value);
} else {
url.searchParams.delete(key);
}
});
if (currentPage > 1) {
url.searchParams.set('quote_page', String(currentPage));
} else {
url.searchParams.delete('quote_page');
}
window.history.replaceState({}, '', url);
}
function restoreUrlFilters() {
var params = new URL(window.location.href).searchParams;
Object.keys(fields).forEach(function (key) {
var value = params.get(key);
var field = fields[key];
if (value !== null && Array.from(field.options || []).some(function (option) { return option.value === value; })) {
field.value = value;
} else if (key === 'keyword' && value !== null) {
field.value = value;
}
});
currentPage = Math.max(parseInt(params.get('quote_page') || '1', 10) || 1, 1);
}
function appendCell(row, value, className) {
var cell = create('td', className || '', value);
row.appendChild(cell);
return cell;
}
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.period_label || quote.effective_date, '-'));
var signCell = appendCell(row, quote.signature_status, 'quote-table__secondary');
if (quote.signed_files && quote.signed_files.signed) {
signCell.appendChild(document.createElement('br'));
var signedLink = create('a', 'quote-action', '回签单');
signedLink.href = quote.signed_files.signed;
signedLink.title = '下载 ' + quote.id + ' 已签署的回签单 PDF' + (quote.signed_at ? '(' + quote.signed_at + ' 签署)' : '');
signedLink.setAttribute('aria-label', '下载回签单 ' + quote.id);
signCell.appendChild(signedLink);
}
var statusCell = appendCell(row, '');
statusCell.appendChild(create(
'span',
'quote-badge ' + (['已失效', '已作废', '已到期', '暂停中'].indexOf(quote.status) !== -1 ? 'quote-badge--muted' : (quote.effective_date ? 'quote-badge--ready' : 'quote-badge--attention')),
quote.status
));
if (quote.void_reason) {
statusCell.appendChild(create('div', 'quote-table__secondary', '作废:' + quote.void_reason));
}
if (quote.superseded_by) {
statusCell.appendChild(create('div', 'quote-table__secondary', '被 ' + quote.superseded_by + ' 替代'));
}
if (quote.status_note) {
statusCell.appendChild(create('div', 'quote-table__secondary', quote.status_note));
}
var actionCell = appendCell(row, '');
var action = create('button', 'quote-action', '查看');
action.type = 'button';
action.dataset.quoteOpen = quote.id;
action.setAttribute('aria-label', '查看 ' + quote.sheet_name);
actionCell.appendChild(action);
var changeAction = create('a', 'quote-action', '变价');
changeAction.href = quote.requote_url || (@json(route('business.courier-quote-changes.start')) + '?source_quote_id=' + encodeURIComponent(quote.id));
changeAction.title = '发起 ' + quote.sheet_name + ' 的变更草稿';
changeAction.setAttribute('aria-label', '变价 ' + quote.sheet_name);
if (quote.status !== '已作废') {
actionCell.appendChild(changeAction);
}
return row;
}
function render() {
var pageCount = Math.max(Math.ceil(filteredQuotes.length / pageSize), 1);
currentPage = Math.min(currentPage, pageCount);
var start = (currentPage - 1) * pageSize;
var visibleQuotes = filteredQuotes.slice(start, start + pageSize);
body.replaceChildren();
visibleQuotes.forEach(function (quote) {
body.appendChild(renderRow(quote));
});
root.querySelector('[data-quote-result-count]').textContent = '共 ' + filteredQuotes.length + ' 个报价版本';
var exportLink = root.querySelector('[data-quote-export]');
if (exportLink) {
var signedCodes = filteredQuotes.filter(function (quote) { return quote.signed_files && quote.signed_files.signed; })
.map(function (quote) { return quote.id; });
exportLink.href = exportBase + (signedCodes.length ? '?codes=' + encodeURIComponent(signedCodes.join(',')) : '');
exportLink.setAttribute('aria-disabled', signedCodes.length ? 'false' : 'true');
exportLink.style.opacity = signedCodes.length ? '' : '0.48';
exportLink.querySelector('[data-quote-export-label]').textContent = '导出回签单' + (signedCodes.length ? '(' + signedCodes.length + ')' : '');
}
root.querySelector('[data-quote-page-label]').textContent = filteredQuotes.length
? '共 ' + filteredQuotes.length + ' 条 · 共 ' + pageCount + ' 页 · 当前第 ' + currentPage + ' 页'
: '共 0 条 · 共 0 页';
root.querySelector('[data-quote-page="previous"]').disabled = currentPage <= 1;
root.querySelector('[data-quote-page="next"]').disabled = currentPage >= pageCount;
tableWrap.hidden = filteredQuotes.length === 0;
empty.hidden = filteredQuotes.length !== 0;
pagination.hidden = false;
updateUrl();
}
function filterQuotes(resetPage) {
var keyword = fields.keyword.value.trim().toLocaleLowerCase('zh-CN');
filteredQuotes = quotes.filter(function (quote) {
var matchesKeyword = !keyword || (quote.company_name + ' ' + quote.sheet_name + ' ' + quote.id + ' ' + quote.carrier)
.toLocaleLowerCase('zh-CN')
.includes(keyword);
return matchesKeyword
&& (!fields.carrier.value || quote.company_name === fields.carrier.value)
&& (!fields.type.value || quote.quote_type === fields.type.value)
&& (!fields.status.value || quote.status === fields.status.value);
});
if (resetPage) {
currentPage = 1;
}
render();
}
function setDetail(name, value) {
root.querySelector('[data-quote-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.period_label || quote.effective_date, 'Excel 未标注'));
setDetail('formulas', String(quote.formula_count));
var signedBox = root.querySelector('[data-quote-signed]');
var files = quote.signed_files || {};
signedBox.hidden = !files.signed;
root.querySelector('[data-quote-signed-text]').textContent = '已签署回传' + (quote.signed_at ? ' · ' + quote.signed_at : '');
signedBox.querySelectorAll('[data-quote-signed-link]').forEach(function (link) {
var url = files[link.getAttribute('data-quote-signed-link')];
link.hidden = !url;
if (url) { link.href = url; } else { link.removeAttribute('href'); }
});
var table = root.querySelector('[data-quote-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-quote-close]').focus();
}
function closeDetail() {
modal.classList.remove('is-open');
document.body.style.overflow = '';
if (lastFocusedElement) {
lastFocusedElement.focus();
}
}
Object.keys(fields).forEach(function (key) {
fields[key].addEventListener(key === 'keyword' ? 'input' : 'change', function () {
filterQuotes(true);
});
});
root.querySelector('[data-quote-reset]').addEventListener('click', function () {
Object.keys(fields).forEach(function (key) { fields[key].value = ''; });
filterQuotes(true);
fields.keyword.focus();
});
root.querySelector('[data-quote-page="previous"]').addEventListener('click', function () {
currentPage = Math.max(currentPage - 1, 1);
render();
});
root.querySelector('[data-quote-page="next"]').addEventListener('click', function () {
currentPage++;
render();
});
body.addEventListener('click', function (event) {
var trigger = event.target.closest('[data-quote-open]');
if (!trigger) {
return;
}
var quote = quotes.find(function (item) { return item.id === trigger.dataset.quoteOpen; });
if (quote) {
openDetail(quote, trigger);
}
});
root.querySelector('[data-quote-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();
}
});
restoreUrlFilters();
filterQuotes(false);
}());
</script>
@endif
</div>
<div class="quote-local-panel" id="quote-warehouse-panel" role="tabpanel" data-quote-panel="warehouse" @if ($quoteView !== 'warehouse') hidden @endif>
<livewire:business.third-party-warehouse-quotes />
</div>
<div class="quote-local-panel" id="quote-reverse-logistics-panel" role="tabpanel" data-quote-panel="reverse-logistics" @if ($quoteView !== 'reverse-logistics') hidden @endif>
@include('business.pages.courier-payables.reverse-logistics-quotes')
</div>
<script>
(() => {
const root = document.currentScript.closest('[data-quote-management]');
if (!root || root.dataset.localTabsReady === 'true') return;
root.dataset.localTabsReady = 'true';
const tabs = [...root.querySelectorAll('[data-quote-view]')];
const panels = [...root.querySelectorAll('[data-quote-panel]')];
const activate = (view, updateUrl = true) => {
tabs.forEach((tab) => {
const active = tab.dataset.quoteView === view;
tab.classList.toggle('is-active', active);
tab.setAttribute('aria-selected', active ? 'true' : 'false');
});
panels.forEach((panel) => { panel.hidden = panel.dataset.quotePanel !== view; });
if (updateUrl) {
const url = new URL(window.location.href);
if (view === 'courier') url.searchParams.delete('quote_view');
else url.searchParams.set('quote_view', view);
history.replaceState({}, '', url);
}
};
tabs.forEach((tab) => tab.addEventListener('click', () => activate(tab.dataset.quoteView)));
})();
</script>
</section>