CaiWuXiTong/resources/views/business/pages/courier-payables/courier-onboarding.blade.php
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

220 lines
19 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
$ready = (bool) ($onboardingReady ?? false);
$rows = $onboardingRows ?? null;
$group = $onboardingGroup ?? 'pending';
$groups = $onboardingGroups ?? [];
$counts = $onboardingCounts ?? [];
$search = $onboardingSearch ?? '';
$groupUrl = static fn (string $key): string => route('business.pages.show', array_filter([
'section' => 'courier-payables',
'page' => 'courier-onboarding',
'group' => $key,
'search' => $search !== '' ? $search : null,
]));
$badge = static fn (string $status): string => match ($status) {
'submitted', 'first_approved', 'approved' => 'is-pending',
'returned' => 'is-warning',
'activated' => 'is-success',
'rejected', 'cancelled' => 'is-muted',
default => 'is-info',
};
@endphp
<section class="co-page" aria-label="快递入驻">
<style>
.co-page { --co-ink:var(--vip-content-ink,#171717); --co-muted:var(--vip-content-muted,#595959); --co-accent:var(--vip-content-accent-dark,#856632); --co-soft:var(--vip-content-accent-soft,#F7F0DE); --co-border:var(--vip-border,#D8D2C7); display:grid; gap:1rem; color:var(--co-ink); font-family:JetBrains Mono,"Microsoft YaHei","PingFang SC",monospace; font-size:.8125rem; }
.co-page *, .co-page *::before, .co-page *::after { box-sizing:border-box; }
.co-notice { display:flex; align-items:flex-start; gap:.6rem; padding:.75rem 1rem; border:1px solid #d7c39d; border-radius:.65rem; background:#fffaf0; color:#6a5129; line-height:1.55; }
.co-notice .material-icons-outlined { color:var(--co-accent); font-size:1.15rem; }
.co-notice.is-error { border-color:#e3b0b0; background:#fdf1f1; color:#7a1010; } .co-notice.is-error .material-icons-outlined { color:#b80000; }
.co-hero { display:grid; grid-template-columns:minmax(0,1.4fr) minmax(18rem,1fr); gap:1rem; align-items:start; }
.co-card { overflow:hidden; border:1px solid var(--co-border); border-radius:.75rem; background:#fff; }
.co-card__head { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:.5rem 1rem; padding:.85rem 1.15rem; border-bottom:1px solid var(--co-border); }
.co-card__title { display:flex; align-items:center; gap:.45rem; margin:0; font-size:.9rem; font-weight:700; } .co-card__title .material-icons-outlined { color:var(--co-accent); font-size:1.1rem; }
.co-card__hint { color:var(--co-muted); font-size:.72rem; }
.co-flow { display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:0; margin:0; padding:1rem 1.15rem 1.15rem; list-style:none; counter-reset:step; }
.co-flow li { position:relative; display:grid; align-content:start; gap:.25rem; padding-right:.75rem; counter-increment:step; }
.co-flow li::before { display:grid; width:1.6rem; height:1.6rem; place-items:center; border-radius:50%; background:var(--co-soft); color:var(--co-accent); font-weight:700; font-size:.72rem; content:counter(step); }
.co-flow li:not(:last-child)::after { position:absolute; top:.8rem; left:2rem; right:.5rem; height:1px; background:var(--co-border); content:''; }
.co-flow strong { font-size:.78rem; } .co-flow span { color:var(--co-muted); font-size:.7rem; line-height:1.5; }
.co-invite { display:grid; gap:.7rem; padding:1rem 1.15rem 1.15rem; }
.co-field { display:grid; gap:.3rem; min-width:0; } .co-field label { color:var(--co-muted); font-size:.72rem; font-weight:600; }
.co-field input, .co-field select { width:100%; height:2.2rem; padding:0 .6rem; border:1px solid var(--co-border); border-radius:.45rem; background:#fff; color:var(--co-ink); font:inherit; }
.co-field input:focus, .co-field select:focus { border-color:var(--co-accent); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.18); }
.co-help { color:var(--co-muted); font-size:.68rem; line-height:1.5; } .co-error { color:#b80000; font-size:.72rem; }
.co-invite__row { display:grid; grid-template-columns:minmax(0,1fr) 7rem; gap:.6rem; }
.co-hero__main { display:grid; gap:1rem; align-content:start; min-width:0; }
.co-profile-grid { display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:.7rem .8rem; padding:1rem 1.15rem 1.15rem; }
@media (max-width: 1100px) { .co-profile-grid { grid-template-columns:repeat(2, minmax(0,1fr)); } }
@media (max-width: 560px) { .co-profile-grid { grid-template-columns:1fr; } }
.co-button { display:inline-flex; align-items:center; justify-content:center; gap:.35rem; height:2.2rem; padding:0 .8rem; border:1px solid var(--co-border); border-radius:.45rem; background:#fff; color:var(--co-ink); font:inherit; font-weight:600; text-decoration:none; white-space:nowrap; cursor:pointer; }
.co-button .material-icons-outlined { font-size:1rem; } .co-button--primary { border-color:var(--co-accent); background:var(--co-accent); color:#fff; } .co-button--primary:hover { background:#6a5129; }
.co-toolbar { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:.75rem; padding:.75rem 1.15rem; border-bottom:1px solid var(--co-border); background:#fffdf8; }
.co-tabs { display:flex; flex-wrap:wrap; gap:1.1rem; }
.co-tab { display:inline-flex; align-items:center; gap:.35rem; min-height:2.1rem; padding:0 .15rem; border-bottom:2px solid transparent; color:var(--co-muted); font-weight:600; text-decoration:none; }
.co-tab.is-active { border-color:var(--co-accent); color:var(--co-accent); }
.co-tab__count { min-width:1.4rem; padding:.05rem .4rem; border-radius:999px; background:#f1ede4; color:var(--co-muted); font-size:.68rem; text-align:center; font-variant-numeric:tabular-nums; }
.co-tab.is-active .co-tab__count { background:var(--co-soft); color:var(--co-accent); }
.co-search { display:flex; gap:.45rem; } .co-search input { width:16rem; height:2.2rem; padding:0 .6rem; border:1px solid var(--co-border); border-radius:.45rem; font:inherit; }
.co-table-wrap { overflow:auto; }
.co-table { width:100%; min-width:62rem; border-collapse:separate; border-spacing:0; font-size:.75rem; }
.co-table th, .co-table td { padding:.62rem .7rem; border-bottom:1px solid var(--co-border); text-align:left; vertical-align:middle; white-space:nowrap; }
.co-table th { background:var(--co-soft); font-weight:700; }
.co-table tbody tr:hover td { background:#fcfaf4; }
.co-table .is-code { font-variant-numeric:tabular-nums; }
.co-table .is-clamp { max-width:16rem; overflow:hidden; text-overflow:ellipsis; }
.co-table a { color:var(--co-accent); font-weight:600; text-decoration:none; }
.co-badge { display:inline-flex; align-items:center; padding:.15rem .5rem; border-radius:999px; font-size:.7rem; font-weight:700; }
.co-badge.is-pending { background:#fff4df; color:#935f07; } .co-badge.is-warning { background:#fde9d4; color:#8a4a06; } .co-badge.is-success { background:#edf5e8; color:#53793e; } .co-badge.is-muted { background:#f1efea; color:#595959; } .co-badge.is-info { background:#e8f2f8; color:#006395; }
.co-empty { padding:3rem 1rem; text-align:center; color:var(--co-muted); }
.co-footer { display:flex; align-items:center; justify-content:space-between; padding:.65rem 1.15rem; color:var(--co-muted); font-size:.72rem; }
.co-footer nav { display:flex; gap:.35rem; } .co-footer a { display:inline-grid; width:2rem; height:2rem; place-items:center; border:1px solid var(--co-border); border-radius:.4rem; color:var(--co-ink); } .co-footer a.is-disabled { pointer-events:none; opacity:.4; }
@media (max-width: 1100px) { .co-hero { grid-template-columns:1fr; } .co-flow { grid-template-columns:repeat(2,minmax(0,1fr)); row-gap:1rem; } .co-flow li::after { display:none; } }
</style>
@if (! $ready)
<div class="co-notice is-error" role="alert">
<span class="material-icons-outlined" aria-hidden="true">error_outline</span>
<span>快递入驻数据表尚未完成迁移,请联系管理员执行数据库迁移后再使用。</span>
</div>
@else
@if (session('success'))
<div class="co-notice" role="status"><span class="material-icons-outlined" aria-hidden="true">check_circle</span><span>{{ session('success') }}</span></div>
@endif
<div class="co-hero">
<div class="co-hero__main">
<div class="co-card">
<div class="co-card__head">
<h2 class="co-card__title"><span class="material-icons-outlined" aria-hidden="true">route</span>入驻流程</h2>
<span class="co-card__hint">快递方无需账号,扫码即可填写</span>
</div>
<ol class="co-flow">
<li><strong>生成二维码</strong><span>填快递公司代码、系统快递名称和快递资料,生成后发给快递方</span></li>
<li><strong>快递扫码填写</strong><span>主体、联系人、结算账户、报价、资质照片</span></li>
<li><strong>对接人初审</strong><span>资料不全可退回,快递用原码补件</span></li>
<li><strong>财务复审</strong><span>确认系统快递名称,首版报价进报价管理</span></li>
<li><strong>签署并开通</strong><span>协议签署后开通,信息对齐到快递资料</span></li>
</ol>
</div>
<section class="co-card" aria-labelledby="co-profile-title">
<div class="co-card__head">
<h2 class="co-card__title" id="co-profile-title"><span class="material-icons-outlined" aria-hidden="true">local_shipping</span>快递资料</h2>
<span class="co-card__hint">全部必填 · 我方填写,快递方看不到;生成后对齐到快递资料,入驻详情里也能改</span>
</div>
<div class="co-profile-grid">
@foreach (\App\Models\Business\CourierOnboarding::PROFILE_FIELDS as $field => [$label, $options])
<div class="co-field">
<label for="co-{{ $field }}">{{ $label }} *</label>
@if ($options !== null)
<select id="co-{{ $field }}" name="{{ $field }}" form="invite" required>
<option value=""></option>
@foreach ($options as $option)
<option value="{{ $option }}" @selected(old($field) === $option)>{{ $option }}</option>
@endforeach
</select>
@else
<input id="co-{{ $field }}" name="{{ $field }}" form="invite" required type="{{ $field === 'courier_email' ? 'email' : 'text' }}" maxlength="{{ $field === 'network_code' ? 64 : 191 }}" value="{{ old($field) }}" placeholder="{{ ['courier_email' => '如:kefu@example.com', 'network_code' => '如:519001', 'network_region' => '如:江苏省常州市天宁区'][$field] ?? '' }}" autocomplete="off">
@endif
@error($field)<span class="co-error">{{ $message }}</span>@enderror
</div>
@endforeach
</div>
</section>
</div>
<form class="co-card" id="invite" method="POST" action="{{ route('business.courier-onboardings.store') }}">
@csrf
<div class="co-card__head">
<h2 class="co-card__title"><span class="material-icons-outlined" aria-hidden="true">qr_code_2</span>生成入驻二维码</h2>
</div>
<div class="co-invite">
<div class="co-field">
<label for="co-expcode">快递公司代码 *</label>
<input id="co-expcode" name="workplatform_expcode" type="text" maxlength="64" required value="{{ old('workplatform_expcode', request('expcode')) }}" placeholder="如:SQZTKY" autocomplete="off">
<span class="co-help">与快递资料「快递公司代码」一致,入驻信息按这个代码对齐到快递资料。</span>
@error('workplatform_expcode')<span class="co-error">{{ $message }}</span>@enderror
</div>
<div class="co-field">
<label for="co-courier-name">系统快递名称 *</label>
<input id="co-courier-name" name="courier_name" type="text" maxlength="100" required value="{{ old('courier_name', request('courier_name')) }}" placeholder="如:新北中通、小件宿迁圆通">
<span class="co-help">我方规定的名称,需与报价名、发货数据里的快递名称完全一致;快递方看不到。</span>
@error('courier_name')<span class="co-error">{{ $message }}</span>@enderror
</div>
<div class="co-invite__row">
<div class="co-field">
<label for="co-invite-label">备注(快递方也能看到)</label>
<input id="co-invite-label" name="invite_label" type="text" maxlength="100" value="{{ old('invite_label') }}" placeholder="如:新北中通 · 张经理">
</div>
<div class="co-field">
<label for="co-valid-days">有效天数</label>
<select id="co-valid-days" name="valid_days">
@foreach ([7, 15, 30, 60, 90] as $days)
<option value="{{ $days }}" @selected((int) old('valid_days', 30) === $days)>{{ $days }} 天</option>
@endforeach
</select>
</div>
</div>
<button type="submit" class="co-button co-button--primary"><span class="material-icons-outlined" aria-hidden="true">qr_code_2</span>生成二维码</button>
</div>
</form>
</div>
<div class="co-card">
<div class="co-toolbar">
<nav class="co-tabs" aria-label="入驻状态">
@foreach ($groups as $key => $config)
<a class="co-tab {{ $group === $key ? 'is-active' : '' }}" href="{{ $groupUrl($key) }}" @if ($group === $key) aria-current="page" @endif>
{{ $config['label'] }}<span class="co-tab__count">{{ number_format($counts[$key] ?? 0) }}</span>
</a>
@endforeach
</nav>
<form class="co-search" method="GET" role="search">
<input type="hidden" name="group" value="{{ $group }}">
<input type="search" name="search" value="{{ $search }}" placeholder="单号 / 公司 / 网点 / 对接人 / 电话" aria-label="搜索入驻申请">
<button type="submit" class="co-button"><span class="material-icons-outlined" aria-hidden="true">search</span>查询</button>
</form>
</div>
<div class="co-table-wrap">
<table class="co-table" aria-label="入驻申请">
<thead>
<tr><th>入驻单号</th><th>状态</th><th>备注</th><th>类型</th><th>公司 / 姓名</th><th>网点 / 品牌</th><th>快递公司代码</th>@foreach (\App\Models\Business\CourierOnboarding::PROFILE_FIELDS as [$profileLabel])<th>{{ $profileLabel }}</th>@endforeach<th>对接人</th><th>系统快递名称</th><th>附件</th><th>提交时间</th><th>最近更新</th><th>操作</th></tr>
</thead>
<tbody>
@forelse ($rows ?? [] as $row)
<tr>
<td class="is-code"><a href="{{ route('business.courier-onboardings.show', $row) }}">{{ $row->number }}</a></td>
<td><span class="co-badge {{ $badge($row->status) }}">{{ $row->linkExpired() && in_array($row->status, ['invited', 'returned'], true) ? '链接已过期' : $row->statusLabel() }}</span></td>
<td class="is-clamp" title="{{ $row->invite_label }}">{{ $row->invite_label ?: '—' }}</td>
<td>{{ $row->company_full_name ? $row->entityLabel() : '—' }}</td>
<td class="is-clamp" title="{{ $row->company_full_name }}">{{ $row->company_full_name ?: '—' }}</td>
<td>{{ $row->network_name ?: '—' }}{{ $row->carrier_brand ? ' · ' . $row->carrier_brand : '' }}</td>
<td class="is-code">{{ $row->workplatform_expcode ?: '—' }}</td>
@foreach (array_keys(\App\Models\Business\CourierOnboarding::PROFILE_FIELDS) as $profileField)
<td class="is-clamp" title="{{ $row->{$profileField} }}">{{ $row->{$profileField} ?: '—' }}</td>
@endforeach
<td>{{ $row->contact_name ?: '—' }}{{ $row->contact_phone ? ' ' . $row->contact_phone : '' }}</td>
<td>{{ $row->courier_name ?: '—' }}</td>
<td class="is-code">{{ $row->files_count }}</td>
<td class="is-code">{{ $row->submitted_at?->format('Y-m-d H:i') ?? '—' }}</td>
<td class="is-code">{{ $row->updated_at?->format('Y-m-d H:i') ?? '—' }}</td>
<td><a href="{{ route('business.courier-onboardings.show', $row) }}">{{ in_array($row->status, ['submitted', 'first_approved', 'approved'], true) ? '去处理' : '查看' }}</a></td>
</tr>
@empty
<tr><td colspan="24" class="co-empty">{{ $search !== '' ? '没有符合条件的入驻申请。' : '这里还没有入驻申请,先在上方生成一个二维码发给快递方。' }}</td></tr>
@endforelse
</tbody>
</table>
</div>
@if ($rows && $rows->total() > 0)
<div class="co-footer">
<span>第 {{ $rows->currentPage() }} / {{ max($rows->lastPage(), 1) }} 页,共 {{ number_format($rows->total()) }} 条</span>
<nav aria-label="分页">
<a class="{{ $rows->onFirstPage() ? 'is-disabled' : '' }}" href="{{ $rows->previousPageUrl() ?: '#' }}" aria-label="上一页"><span class="material-icons-outlined" aria-hidden="true">chevron_left</span></a>
<a class="{{ $rows->hasMorePages() ? '' : 'is-disabled' }}" href="{{ $rows->nextPageUrl() ?: '#' }}" aria-label="下一页"><span class="material-icons-outlined" aria-hidden="true">chevron_right</span></a>
</nav>
</div>
@endif
</div>
@endif
</section>