CaiWuXiTong/resources/views/onboarding/courier-form.blade.php
limeng 2a3ea39e72 feat(快递报价): 价格表右侧增加备注列,计费条件随确认单盖章并进入导出电子表 (#1020)
快递入驻填写的计费条件(执行时间、计费方式、是否计泡及抛比、起算重量、报价备注里的限长限重等)
原来在确认单里排在价格表下方、导出的报价电子表格里则完全没有,容易事后扯皮。
- 报价确认单 PDF(推送快递 e签宝盖章)与导出的报价 xlsx:价格表右侧增加「备注」列,逐行列出价格表以外的全部条件
- 新生成的入驻/变价报价补充口径:重量不取整说明、重量区间含上限、未列省份及空白价格不在报价范围、开票类型与税率、结算周期、附加费(除报价表及备注列明外不另收取)
- 入驻表单报价备注下提示:要收的附加费须写在这里,没写视为不收取,并随确认单盖章
涉及:报价确认单与导出展示;已签署的确认单不变;不涉及迁移、权限和上游写入。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VANWYsNUbu2piMtCKymoyd
2026-11-09 18:58:05 +08:00

1019 lines
74 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
/** @var \App\Models\Business\CourierOnboarding $onboarding */
// 后台变价复用本页的报价区:$requote 为后台变价上下文(只显示报价,提交到后台)。
$requote = $requote ?? null;
$editable = $requote !== null || $onboarding->courierCanEdit();
$value = static fn (string $key, $default = null) => old($key, $onboarding->{$key} ?? $default);
$savedRows = collect($onboarding->quoteRowList())->keyBy('region');
$oldRows = old('quote_rows');
$quoteRows = [];
$customRows = $savedRows->except($provinces)->values()->all();
foreach ([...$provinces, null, null, null, null] as $index => $province) {
$row = is_array($oldRows) ? ($oldRows[$index] ?? []) : ($province !== null ? ($savedRows->get($province) ?? []) : ($customRows[$index - count($provinces)] ?? []));
$quoteRows[] = [
'region' => $province ?? ($row['region'] ?? ''),
'fixed' => $province !== null,
'flat' => array_values((array) ($row['flat'] ?? [])),
'base' => $row['base'] ?? null,
'rates' => array_values((array) ($row['rates'] ?? [])),
];
}
$structure = $onboarding->quoteStructure();
$kgList = static fn (array $list): string => implode(',', array_map(static fn ($v) => \App\Support\CourierQuoteStructure::kg((float) $v), $list));
$quoteSheet = $onboarding->files->where('kind', \App\Models\Business\CourierOnboardingFile::QUOTE_SHEET_KIND)->last();
$filesByKind = $onboarding->files->groupBy('kind');
$quoteMode = $value('quote_mode', 'waybill_rate');
$entityChosen = $requote !== null || filled(old('entity_type')) || filled($onboarding->company_full_name) || $onboarding->status !== 'invited' || $onboarding->files->isNotEmpty();
$entityType = $entityChosen ? $value('entity_type', 'company') : null;
$statusTone = match ($onboarding->status) {
'returned' => 'warning',
'activated' => 'success',
'rejected' => 'danger',
default => 'info',
};
$num = static fn ($v) => $v === null || $v === '' ? '' : rtrim(rtrim(number_format((float) $v, 4, '.', ''), '0'), '.');
@endphp
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="robots" content="noindex, nofollow">
<meta name="format-detection" content="telephone=no">
<title>{{ $requote ? '快递变价' : '快递合作入驻' }}</title>
<style>
:root {
--ink: #171717; --muted: #595959; --gold: #856632; --gold-strong: #6A5129; --gold-soft: #F7F0DE;
--gold-line: #D7C39D; --rail: #050506; --rail-gold: #F2E7C5; --bg: #F8F6F1; --surface: #FFFFFF;
--border: #D8D2C7; --success: #53793E; --danger: #B80000; --warning: #935F07; --info: #006395;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body { margin: 0; background: var(--bg); color: var(--ink); font: 15px/1.55 "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; }
.hero { padding: 22px 16px 18px; background: var(--rail); color: var(--rail-gold); }
.hero__inner, .main { max-width: 720px; margin: 0 auto; }
.hero__eyebrow { margin: 0 0 6px; color: #BFAF86; font-size: 12px; letter-spacing: .08em; }
.hero h1 { margin: 0; font-size: 21px; font-weight: 700; letter-spacing: .02em; }
.hero p { margin: 8px 0 0; color: #DDD0AA; font-size: 13px; }
.hero__meta { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 12px; color: #BFAF86; font-size: 12px; font-variant-numeric: tabular-nums; }
.main { padding: 14px 16px 120px; }
.notice { display: flex; gap: 10px; margin-bottom: 12px; padding: 12px 14px; border: 1px solid var(--gold-line); border-radius: 10px; background: #FFFAF0; font-size: 14px; }
.notice strong { display: block; margin-bottom: 2px; }
.notice--success { border-color: #B7CDA8; background: #F1F7EC; color: #2F4A22; }
.notice--danger { border-color: #E3B0B0; background: #FDF1F1; color: #7A1010; }
.notice--warning { border-color: #E2C68C; background: #FFF6E2; color: #6A4505; }
.notice--info { border-color: #B4CFDE; background: #EEF6FA; color: #0A4560; }
.notice ul { margin: 6px 0 0; padding-left: 18px; }
.steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin: 0 0 14px; padding: 0; list-style: none; font-size: 12px; text-align: center; }
.steps li { padding: 7px 4px; border-radius: 8px; background: #EFEAE0; color: var(--muted); }
.steps li.is-done { background: var(--gold-soft); color: var(--gold); font-weight: 600; }
.steps li.is-current { background: var(--gold); color: #fff; font-weight: 600; }
.card { margin-bottom: 12px; overflow: hidden; border: 1px solid var(--border); border-radius: 12px; background: var(--surface); }
.card__head { display: flex; align-items: baseline; gap: 8px; padding: 13px 16px; border-bottom: 1px solid var(--border); }
.card__index { display: inline-grid; flex: none; width: 22px; height: 22px; place-items: center; border-radius: 50%; background: var(--gold-soft); color: var(--gold); font-size: 12px; font-weight: 700; }
.card__title { margin: 0; font-size: 16px; font-weight: 700; }
.card__hint { margin-left: auto; color: var(--muted); font-size: 12px; }
.card__body { display: grid; gap: 14px; padding: 16px; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.field { display: grid; gap: 6px; min-width: 0; }
.field > label, .field > .label { color: var(--ink); font-size: 14px; font-weight: 600; }
.req { color: var(--danger); margin-left: 2px; }
.help { color: var(--muted); font-size: 12px; }
.error { color: var(--danger); font-size: 13px; }
input[type=text], input[type=tel], input[type=number], input[type=date], select, textarea {
width: 100%; min-height: 44px; padding: 10px 12px; border: 1px solid var(--border); border-radius: 8px;
background: #fff; color: var(--ink); font: inherit; font-size: 16px; appearance: none;
}
select { background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(135deg, var(--muted) 50%, transparent 50%); background-position: calc(100% - 18px) 50%, calc(100% - 13px) 50%; background-size: 5px 5px; background-repeat: no-repeat; padding-right: 32px; }
textarea { min-height: 84px; resize: vertical; }
input:focus, select:focus, textarea:focus { border-color: var(--gold); outline: none; box-shadow: 0 0 0 3px rgba(133, 102, 50, .18); }
input:disabled, select:disabled, textarea:disabled { background: #F4F1EA; color: var(--muted); }
.has-error input, .has-error select, .has-error textarea { border-color: var(--danger); }
.choices { display: flex; flex-wrap: wrap; gap: 8px; }
.choice { position: relative; }
.choice input { position: absolute; opacity: 0; pointer-events: none; }
.choice span { display: inline-flex; align-items: center; min-height: 40px; padding: 8px 14px; border: 1px solid var(--border); border-radius: 999px; background: #fff; font-size: 14px; }
.choice input:checked + span { border-color: var(--gold); background: var(--gold-soft); color: var(--gold-strong); font-weight: 600; }
.choice input:focus-visible + span { box-shadow: 0 0 0 3px rgba(133, 102, 50, .25); }
.quote-tools { display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: 8px; align-items: end; padding: 12px; border: 1px dashed var(--gold-line); border-radius: 10px; background: #FFFDF8; }
.quote-tools .field > label { font-size: 12px; font-weight: 500; color: var(--muted); }
.quote-table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: 10px; -webkit-overflow-scrolling: touch; }
.quote-table { width: max-content; min-width: 100%; border-collapse: collapse; font-size: 14px; }
.quote-table td:first-child, .quote-table th:first-child { position: sticky; left: 0; z-index: 1; background: #fff; }
.quote-table th:first-child { background: var(--gold-soft); }
.quote-table th { position: sticky; top: 0; padding: 9px 8px; background: var(--gold-soft); color: var(--gold-strong); font-size: 12px; font-weight: 700; text-align: left; white-space: nowrap; }
.quote-table td { padding: 5px 6px; border-top: 1px solid #ECE8E0; }
.quote-table th:first-child, .quote-table td:first-child { width: 4.6em; padding-left: 10px; font-weight: 600; white-space: nowrap; }
.quote-table th { white-space: normal; line-height: 1.3; }
.quote-table input { min-height: 38px; padding: 6px; font-variant-numeric: tabular-nums; }
.quote-table tr.is-custom td:first-child { padding-left: 6px; }
.quote-table tr.is-custom td:first-child input { font-weight: 600; }
.upload { display: grid; gap: 8px; padding: 12px; border: 1px solid var(--border); border-radius: 10px; background: #FFFDF8; }
.upload__head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.upload__title { font-size: 14px; font-weight: 600; }
.upload input[type=file] { width: 100%; font-size: 14px; }
.upload input[type=file]::file-selector-button { margin-right: 10px; min-height: 36px; padding: 0 14px; border: 1px solid var(--gold); border-radius: 8px; background: #fff; color: var(--gold-strong); font: inherit; font-weight: 600; }
.file-list { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; }
.file-list li { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: 8px; background: #fff; border: 1px solid #ECE8E0; font-size: 13px; }
.file-list .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-list .size { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
.file-list label { display: inline-flex; align-items: center; gap: 4px; color: var(--danger); font-size: 12px; }
.actionbar { position: fixed; right: 0; bottom: 0; left: 0; z-index: 10; padding: 10px 16px calc(10px + env(safe-area-inset-bottom)); border-top: 1px solid var(--border); background: rgba(255, 255, 255, .96); backdrop-filter: blur(8px); }
.actionbar__inner { display: grid; grid-template-columns: 1fr 2fr; gap: 10px; max-width: 720px; margin: 0 auto; }
.btn { display: inline-flex; align-items: center; justify-content: center; min-height: 48px; padding: 0 16px; border: 1px solid var(--border); border-radius: 10px; background: #fff; color: var(--ink); font: inherit; font-size: 16px; font-weight: 600; cursor: pointer; }
.btn--primary { border-color: var(--gold); background: var(--gold); color: #fff; }
.btn--primary:active { background: var(--gold-strong); }
.btn[disabled] { opacity: .6; }
.summary { display: grid; gap: 0; margin: 0; }
.summary div { display: grid; grid-template-columns: 7.5em 1fr; gap: 10px; padding: 9px 0; border-bottom: 1px solid #F0ECE4; font-size: 14px; }
.summary div:last-child { border-bottom: 0; }
.summary dt { color: var(--muted); }
.summary dd { margin: 0; word-break: break-all; }
.footer-note { margin: 18px 0 0; color: var(--muted); font-size: 12px; text-align: center; }
.card__body > *, .structure > *, .quote-tools > * { min-width: 0; }
.quote-table-wrap { max-width: 100%; }
.mode-list { display: grid; gap: 8px; }
.mode-option { position: relative; display: block; cursor: pointer; }
.mode-option input { position: absolute; opacity: 0; pointer-events: none; }
.mode-option > span { display: grid; gap: 3px; padding: 11px 14px; border: 1px solid var(--border); border-radius: 10px; background: #fff; }
.mode-option strong { font-size: 15px; }
.mode-option small { color: var(--muted); font-size: 12px; line-height: 1.5; }
.mode-option input:checked + span { border-color: var(--gold); background: var(--gold-soft); box-shadow: inset 3px 0 0 var(--gold); }
.mode-option input:checked + span strong { color: var(--gold-strong); }
.structure { display: grid; gap: 14px; }
.account { display: grid; gap: 12px; padding: 12px; border: 1px solid var(--border); border-radius: 10px; }
.account__title { margin: 0; font-weight: 700; font-size: 15px; }
.account__tag { margin-left: 6px; padding: 1px 6px; border-radius: 6px; background: #F1EEE6; color: var(--muted); font-size: 12px; font-weight: 500; }
.account__check { font-size: 12px; }
.account__check.is-ok { color: #2F6B2A; }
.account__check.is-bad { color: #A1281F; }
.band-list { display: grid; gap: 8px; }
.band-row { display: grid; grid-template-columns: 44px 40px 92px auto 40px; gap: 8px; align-items: center; font-size: 15px; }
.band-row__no { color: var(--muted); font-size: 13px; }
.band-row input { width: 92px; }
.band-row__del { min-height: 40px; padding: 0; border: 1px solid var(--border); border-radius: 8px; background: #fff; color: var(--muted); font-size: 18px; cursor: pointer; }
.band-add { justify-self: start; min-height: 40px; font-size: 15px; }
[data-mode="none"] .structure, [data-mode="other"] .structure { display: none; }
.quote-table th { min-width: 84px; }
.quote-table td input[data-q-slot] { width: 76px; min-width: 76px; }
.quote-table td input[data-q-region] { width: 78px; }
.quote-table th { white-space: normal; max-width: 96px; }
.upload-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.upload { gap: 6px; padding: 10px; }
.upload__title { font-size: 13px; }
.upload__drop { position: relative; display: block; cursor: pointer; }
.upload__drop input[type=file] { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
.upload__preview { display: grid; place-items: center; align-content: center; gap: 4px; height: 108px; overflow: hidden; border: 1px dashed var(--gold-line); border-radius: 8px; background: #fff; color: var(--muted); font-size: 12px; text-align: center; }
.upload__preview img { width: 100%; height: 100%; object-fit: cover; }
.upload__plus { color: var(--gold); font-size: 28px; line-height: 1; }
.upload__done { color: var(--success); font-weight: 700; }
.upload__name { max-width: 100%; padding: 0 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.upload__remove { display: inline-flex; align-items: center; gap: 4px; color: var(--danger); font-size: 12px; }
.chooser { display: grid; gap: 12px; margin-bottom: 12px; }
.chooser__title { margin: 4px 0 0; font-size: 17px; font-weight: 700; }
.chooser__card { display: grid; grid-template-columns: 48px 1fr; gap: 12px; align-items: center; width: 100%; padding: 18px 16px; border: 1px solid var(--border); border-radius: 12px; background: #fff; color: var(--ink); font: inherit; text-align: left; cursor: pointer; }
.chooser__card:active { background: var(--gold-soft); }
.chooser__icon { display: grid; width: 48px; height: 48px; place-items: center; border-radius: 12px; background: var(--gold-soft); color: var(--gold); font-size: 22px; font-weight: 700; }
.chooser__card strong { display: block; font-size: 16px; }
.chooser__card span span { display: block; margin-top: 3px; color: var(--muted); font-size: 13px; }
[data-entity="none"] .card, [data-entity="none"] ~ .actionbar, [data-entity="none"] .actionbar { display: none !important; }
[data-entity="company"] [data-chooser], [data-entity="person"] [data-chooser] { display: none !important; }
[data-entity="company"] [data-entity-only="person"], [data-entity="person"] [data-entity-only="company"] { display: none !important; }
@media (max-width: 520px) {
.grid-2 { grid-template-columns: 1fr; }
.quote-tools { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.quote-tools .btn { grid-column: span 3; min-height: 42px; }
}
</style>
</head>
<body>
@if ($requote)
<header class="hero">
<div class="hero__inner">
<p class="hero__eyebrow"><a href="{{ $requote['back'] }}" style="color:inherit">← 返回报价管理 · 快递报价</a></p>
<h1>{{ $requote['courier'] }} · 变价</h1>
<p>以当前报价为底稿修改,可以只改价格,也可以改计费方式和重量分档。提交后需另一人审核,审核通过自动生成新报价版本,并推送到快递的 e签宝签署。</p>
<div class="hero__meta">
<span>底稿版本 {{ $requote['version']?->version_code ?? '入驻首版' }}</span>
@if ($requote['change'])<span>修改已退回的变价单 {{ $requote['change']->number }}</span>@endif
<span>签署人 {{ $requote['signer'] }}</span>
</div>
</div>
</header>
@else
<header class="hero">
<div class="hero__inner">
<p class="hero__eyebrow">{{ company()?->name ? company()->name . ' · ' : '' }}快递合作</p>
<h1>快递合作入驻资料</h1>
<p>请如实填写贵司主体、结算账户和报价,提交后我方会在 1–3 个工作日内审核。</p>
<div class="hero__meta">
<span>申请编号 {{ $onboarding->number }}</span>
@if ($onboarding->invite_label)<span>{{ $onboarding->invite_label }}</span>@endif
@if ($editable && $onboarding->expires_at)<span>链接有效期至 {{ $onboarding->expires_at->format('Y-m-d') }}</span>@endif
</div>
</div>
</header>
@endif
<main class="main">
@php
$stepIndex = match ($onboarding->status) {
'invited', 'returned' => 0,
'submitted', 'first_approved' => 1,
'approved' => 2,
'activated' => 3,
default => 1,
};
@endphp
@unless ($requote)
<ol class="steps" aria-label="入驻进度">
@foreach (['填写资料', '我方审核', '签署协议', '开通合作'] as $i => $step)
<li class="{{ $i < $stepIndex ? 'is-done' : ($i === $stepIndex ? 'is-current' : '') }}" @if ($i === $stepIndex) aria-current="step" @endif>{{ $step }}</li>
@endforeach
</ol>
@endunless
@if (session('onboarding_success'))
<div class="notice notice--success" role="status"><div><strong>{{ session('onboarding_success') }}</strong></div></div>
@endif
@if (request()->boolean('too_large'))
<div class="notice notice--danger" role="alert"><div><strong>图片太大,这次没有保存成功</strong>请重新选择照片,或每次少选几张、先点「暂存」再继续上传。</div></div>
@endif
@if ($errors->any())
<div class="notice notice--danger" role="alert">
<div>
<strong>还有 {{ count($errors->all()) }} 项需要处理</strong>
<ul>@foreach (array_slice($errors->all(), 0, 8) as $message)<li>{{ $message }}</li>@endforeach</ul>
</div>
</div>
@endif
@if ($requote)
@if ($requote['change'] && $requote['change']->status === 'returned')
<div class="notice notice--warning" role="status"><div><strong>变价单 {{ $requote['change']->number }} 已被退回</strong>按审核意见修改后重新提交即可。</div></div>
@endif
@elseif ($onboarding->status === 'returned' && $onboarding->return_reason)
<div class="notice notice--warning" role="status"><div><strong>需要补充或修改</strong>{{ $onboarding->return_reason }}</div></div>
@elseif (! $editable)
<div class="notice notice--{{ $statusTone }}" role="status">
<div>
<strong>当前状态:{{ $onboarding->linkExpired() && in_array($onboarding->status, ['invited', 'returned'], true) ? '链接已过期' : $onboarding->statusLabel() }}</strong>
@switch ($onboarding->status)
@case('submitted')
@case('first_approved')
资料已提交,审核中。如需修改,请联系我方对接人退回后再改。
@break
@case('approved')
审核已通过,报价确认单已推送到你填写的 e签宝账号,请留意短信或邮件,登录 e签宝完成签署。
@break
@case('activated')
合作已开通,感谢配合。
@break
@case('rejected')
很抱歉,本次暂未达成合作。
@break
@default
链接已过期,请联系我方对接人重新获取二维码。
@endswitch
</div>
</div>
@endif
<form method="POST" action="{{ $requote ? $requote['action'] : route('courier-onboarding.update', ['token' => $token]) }}" enctype="multipart/form-data" novalidate data-onboarding-form data-entity="{{ $entityType === null ? 'none' : ($entityType === 'person' ? 'person' : 'company') }}">
@csrf
@if ($editable && ! $requote)
<div class="chooser" data-chooser>
<p class="chooser__title">请选择入驻方式</p>
<button type="button" class="chooser__card" data-choose-entity="company">
<span class="chooser__icon" aria-hidden="true">企</span>
<span><strong>公司 / 个体户入驻</strong><span>有营业执照,以公司名义合作和签署</span></span>
</button>
<button type="button" class="chooser__card" data-choose-entity="person">
<span class="chooser__icon" aria-hidden="true">人</span>
<span><strong>个人入驻</strong><span>以个人身份合作,用本人身份证实名签署</span></span>
</button>
</div>
@endif
<fieldset @disabled(! $editable) style="margin:0;padding:0;border:0;min-width:0">
@unless ($requote)
<section class="card" aria-labelledby="sec-company">
<div class="card__head"><span class="card__index">1</span><h2 class="card__title" id="sec-company">入驻主体</h2><span class="card__hint" data-entity-only="company">与营业执照一致</span><span class="card__hint" data-entity-only="person">与身份证一致</span></div>
<div class="card__body">
<div class="field">
<span class="label">入驻类型<span class="req">*</span></span>
<div class="choices" role="radiogroup" aria-label="入驻类型">
@foreach (\App\Models\Business\CourierOnboarding::ENTITY_TYPES as $type => $label)
<label class="choice"><input type="radio" name="entity_type" value="{{ $type }}" @checked($entityType === $type) data-entity-type><span>{{ $label }}</span></label>
@endforeach
</div>
<span class="help" data-entity-only="company">有营业执照的公司或个体工商户选这个,以公司名义签署。</span>
<span class="help" data-entity-only="person">没有营业执照、以个人身份合作选这个,用本人身份证实名签署。</span>
</div>
<div class="field @error('company_full_name') has-error @enderror">
<label for="company_full_name"><span data-entity-only="company">公司全称</span><span data-entity-only="person">姓名</span><span class="req">*</span></label>
<input id="company_full_name" name="company_full_name" type="text" maxlength="255" value="{{ $value('company_full_name') }}" data-placeholder-company="如:常州某某快递服务有限公司" data-placeholder-person="与身份证一致">
@error('company_full_name')<span class="error">{{ $message }}</span>@enderror
</div>
<div class="field @error('id_number') has-error @enderror" data-entity-only="person">
<label for="id_number">身份证号<span class="req">*</span></label>
<input id="id_number" name="id_number" type="text" maxlength="18" autocapitalize="characters" autocomplete="off" value="{{ old('id_number', $onboarding->id_number) }}" placeholder="18 位">
@error('id_number')<span class="error">{{ $message }}</span>@enderror
</div>
<div class="grid-2" data-entity-only="company">
<div class="field @error('unified_social_credit_code') has-error @enderror">
<label for="unified_social_credit_code">统一社会信用代码<span class="req">*</span></label>
<input id="unified_social_credit_code" name="unified_social_credit_code" type="text" maxlength="18" autocapitalize="characters" value="{{ $value('unified_social_credit_code') }}" placeholder="18 位">
@error('unified_social_credit_code')<span class="error">{{ $message }}</span>@enderror
</div>
<div class="field @error('legal_representative') has-error @enderror">
<label for="legal_representative">法定代表人</label>
<input id="legal_representative" name="legal_representative" type="text" maxlength="100" value="{{ $value('legal_representative') }}">
</div>
</div>
<div class="field">
<label for="business_address">经营地址</label>
<input id="business_address" name="business_address" type="text" maxlength="255" autocomplete="street-address" value="{{ $value('business_address') }}">
</div>
<div class="grid-2">
<div class="field @error('carrier_brand') has-error @enderror">
<label for="carrier_brand">快递品牌<span class="req">*</span></label>
<input id="carrier_brand" name="carrier_brand" type="text" maxlength="50" list="carrier-brands" value="{{ $value('carrier_brand') }}" placeholder="如:中通、圆通、德邦">
<datalist id="carrier-brands">@foreach (['中通', '圆通', '申通', '韵达', '极兔', '德邦', '顺丰', '京东', '邮政', '百世快运', '安能', '中通快运', '壹米滴答', '跨越'] as $brand)<option value="{{ $brand }}">@endforeach</datalist>
@error('carrier_brand')<span class="error">{{ $message }}</span>@enderror
</div>
<div class="field @error('network_name') has-error @enderror">
<label for="network_name">网点名称<span class="req">*</span></label>
<input id="network_name" name="network_name" type="text" maxlength="100" value="{{ $value('network_name') }}" placeholder="如:常州新北网点">
@error('network_name')<span class="error">{{ $message }}</span>@enderror
</div>
</div>
<div class="field @error('service_type') has-error @enderror">
<span class="label">业务类型<span class="req">*</span></span>
<div class="choices" role="radiogroup" aria-label="业务类型">
@foreach (\App\Models\Business\CourierOnboarding::SERVICE_TYPES as $type)
<label class="choice"><input type="radio" name="service_type" value="{{ $type }}" @checked($value('service_type') === $type)><span>{{ $type }}</span></label>
@endforeach
</div>
@error('service_type')<span class="error">{{ $message }}</span>@enderror
</div>
<div class="field">
<label for="pickup_warehouses">可上门揽收的仓库 / 区域</label>
<textarea id="pickup_warehouses" name="pickup_warehouses" maxlength="1000" placeholder="如:常州新北区、武进横林仓">{{ $value('pickup_warehouses') }}</textarea>
</div>
</div>
</section>
<section class="card" aria-labelledby="sec-contact">
<div class="card__head"><span class="card__index">2</span><h2 class="card__title" id="sec-contact">联系人</h2></div>
<div class="card__body">
<div class="grid-2">
<div class="field @error('contact_name') has-error @enderror">
<label for="contact_name">业务对接人<span class="req">*</span></label>
<input id="contact_name" name="contact_name" type="text" maxlength="100" autocomplete="name" value="{{ $value('contact_name') }}">
@error('contact_name')<span class="error">{{ $message }}</span>@enderror
</div>
<div class="field @error('contact_phone') has-error @enderror">
<label for="contact_phone">对接人手机<span class="req">*</span></label>
<input id="contact_phone" name="contact_phone" type="tel" inputmode="tel" maxlength="20" autocomplete="tel" value="{{ $value('contact_phone') }}">
<span class="help">用于接收合作协议的电子签署短信</span>
@error('contact_phone')<span class="error">{{ $message }}</span>@enderror
</div>
</div>
<div class="grid-2">
<div class="field">
<label for="finance_contact_name">财务对账人</label>
<input id="finance_contact_name" name="finance_contact_name" type="text" maxlength="100" value="{{ $value('finance_contact_name') }}">
</div>
<div class="field @error('finance_contact_phone') has-error @enderror">
<label for="finance_contact_phone">对账人电话</label>
<input id="finance_contact_phone" name="finance_contact_phone" type="tel" inputmode="tel" maxlength="20" value="{{ $value('finance_contact_phone') }}">
@error('finance_contact_phone')<span class="error">{{ $message }}</span>@enderror
</div>
</div>
</div>
</section>
<section class="card" aria-labelledby="sec-esign">
<div class="card__head"><span class="card__index">3</span><h2 class="card__title" id="sec-esign">e签宝电子签署</h2><span class="card__hint">报价确认单、合作协议都发到这里</span></div>
<div class="card__body">
<p class="help" style="margin:0">审核通过后,系统会按你填写的报价自动生成报价确认单,推送到下面这个 e签宝账号,签署人用该账号登录 e签宝(或点短信链接)即可盖章签署,无需再纸质盖章寄回。</p>
<div class="grid-2">
<div class="field @error('esign_operator_name') has-error @enderror" data-entity-only="company">
<label for="esign_operator_name">签署人姓名<span class="req">*</span></label>
<input id="esign_operator_name" name="esign_operator_name" type="text" maxlength="100" value="{{ $value('esign_operator_name') }}" placeholder="e签宝实名认证的姓名" data-default-from="contact_name">
@error('esign_operator_name')<span class="error">{{ $message }}</span>@enderror
</div>
<div class="field @error('esign_account') has-error @enderror">
<label for="esign_account">e签宝账号<span class="req">*</span></label>
<input id="esign_account" name="esign_account" type="text" inputmode="email" maxlength="255" autocomplete="off" value="{{ $value('esign_account') }}" placeholder="注册 e签宝用的手机号或邮箱" data-default-from="contact_phone">
<span class="help">手机号会收到签署短信,邮箱会收到签署邮件</span>
@error('esign_account')<span class="error">{{ $message }}</span>@enderror
</div>
</div>
<p class="help" style="margin:0" data-entity-only="company">签署主体为上面填写的公司全称,签署人需有该公司在 e签宝的签章权限。</p>
<p class="help" style="margin:0" data-entity-only="person">个人入驻由本人签署,e签宝账号需用本人身份证完成实名认证。</p>
</div>
</section>
<section class="card" aria-labelledby="sec-settle">
<div class="card__head"><span class="card__index">4</span><h2 class="card__title" id="sec-settle">结算与开票</h2><span class="card__hint">运费打款账户(对公 / 对私)</span></div>
<div class="card__body">
<div class="account" data-account="corporate">
<p class="account__title">对公账户<span class="req">*</span></p>
<div class="field @error('bank_account_name') has-error @enderror">
<label for="bank_account_name">对公户名</label>
<input id="bank_account_name" name="bank_account_name" type="text" maxlength="255" value="{{ $value('bank_account_name') }}" data-placeholder-company="须与营业执照公司全称一致" data-placeholder-person="个体户或公司的对公账户户名" data-account-name="corporate">
<span class="account__check" data-account-check="corporate" aria-live="polite"></span>
@error('bank_account_name')<span class="error">{{ $message }}</span>@enderror
</div>
<div class="grid-2">
<div class="field @error('bank_name') has-error @enderror">
<label for="bank_name">开户行</label>
<input id="bank_name" name="bank_name" type="text" maxlength="255" value="{{ $value('bank_name') }}" placeholder="写到支行">
@error('bank_name')<span class="error">{{ $message }}</span>@enderror
</div>
<div class="field @error('bank_account_number') has-error @enderror">
<label for="bank_account_number">对公账号</label>
<input id="bank_account_number" name="bank_account_number" type="text" inputmode="numeric" maxlength="40" autocomplete="off" value="{{ old('bank_account_number', $onboarding->bank_account_number) }}">
@error('bank_account_number')<span class="error">{{ $message }}</span>@enderror
</div>
</div>
</div>
<div class="account" data-account="private">
<p class="account__title">对私账户<span data-entity-only="company"><span class="req">*</span></span><span class="account__tag" data-entity-only="person">选填</span></p>
<div class="field @error('private_account_name') has-error @enderror">
<label for="private_account_name">对私收款人</label>
<input id="private_account_name" name="private_account_name" type="text" maxlength="255" value="{{ $value('private_account_name') }}" data-placeholder-company="须为营业执照上的法定代表人" data-placeholder-person="须为本人" data-account-name="private">
<span class="account__check" data-account-check="private" aria-live="polite"></span>
@error('private_account_name')<span class="error">{{ $message }}</span>@enderror
</div>
<div class="grid-2">
<div class="field @error('private_bank_name') has-error @enderror">
<label for="private_bank_name">开户行</label>
<input id="private_bank_name" name="private_bank_name" type="text" maxlength="255" value="{{ $value('private_bank_name') }}" placeholder="写到支行">
@error('private_bank_name')<span class="error">{{ $message }}</span>@enderror
</div>
<div class="field @error('private_account_number') has-error @enderror">
<label for="private_account_number">银行卡号</label>
<input id="private_account_number" name="private_account_number" type="text" inputmode="numeric" maxlength="40" autocomplete="off" value="{{ old('private_account_number', $onboarding->private_account_number) }}">
@error('private_account_number')<span class="error">{{ $message }}</span>@enderror
</div>
</div>
<span class="help">这张银行卡的正反面请在下方「证件照片」上传。</span>
</div>
<div class="field @error('invoice_type') has-error @enderror">
<span class="label">开票类型<span class="req">*</span></span>
<div class="choices">
@foreach (\App\Models\Business\CourierOnboarding::INVOICE_TYPES as $type)
<label class="choice"><input type="radio" name="invoice_type" value="{{ $type }}" @checked($value('invoice_type') === $type)><span>{{ $type }}</span></label>
@endforeach
</div>
@error('invoice_type')<span class="error">{{ $message }}</span>@enderror
</div>
<div class="grid-2">
<div class="field @error('tax_rate') has-error @enderror">
<label for="tax_rate">税率(%)</label>
<input id="tax_rate" name="tax_rate" type="number" inputmode="decimal" step="0.01" min="0" max="20" value="{{ $num($value('tax_rate')) }}" placeholder="如 6 或 1">
@error('tax_rate')<span class="error">{{ $message }}</span>@enderror
</div>
<div class="field @error('settlement_cycle') has-error @enderror">
<label for="settlement_cycle">结算周期<span class="req">*</span></label>
<select id="settlement_cycle" name="settlement_cycle">
<option value="">请选择</option>
@foreach (\App\Models\Business\CourierOnboarding::SETTLEMENT_CYCLES as $cycle)
<option value="{{ $cycle }}" @selected($value('settlement_cycle') === $cycle)>{{ $cycle }}</option>
@endforeach
</select>
@error('settlement_cycle')<span class="error">{{ $message }}</span>@enderror
</div>
</div>
</div>
</section>
@endunless
<section class="card" aria-labelledby="sec-quote" data-quote-section>
<div class="card__head"><span class="card__index">{{ $requote ? 1 : 5 }}</span><h2 class="card__title" id="sec-quote">报价</h2><span class="card__hint">单位:元</span></div>
<div class="card__body">
<div class="field @error('quote_mode') has-error @enderror">
<span class="label">① 计费方式<span class="req">*</span></span>
<div class="mode-list" role="radiogroup" aria-label="计费方式">
@foreach (\App\Support\CourierQuoteStructure::MODES as $mode => $label)
@continue($requote && $mode === 'other')
<label class="mode-option">
<input type="radio" name="quote_mode" value="{{ $mode }}" @checked($value('quote_mode') === $mode) data-quote-mode>
<span><strong>{{ $label }}</strong><small>{{ \App\Support\CourierQuoteStructure::MODE_HINTS[$mode] }}</small></span>
</label>
@endforeach
</div>
@error('quote_mode')<span class="error">{{ $message }}</span>@enderror
</div>
<div class="structure" data-structure>
<span class="label" data-show-for="waybill_rate first_additional flat_bands flat_then">② 计费结构</span>
<div class="field @error('quote_flat_bands') has-error @enderror" data-show-for="flat_bands flat_then">
<span class="label">一口价重量档<span class="req">*</span></span>
<input id="quote_flat_bands" name="quote_flat_bands" type="hidden" value="{{ old('quote_flat_bands', $kgList($structure['flat_bands'])) }}" data-band-value>
<div class="band-list" data-band-list></div>
<button type="button" class="btn band-add" data-band-add>+ 添加一档</button>
<span class="help">每档填上限重量,含上限。例如依次填 1、2、3,就是 0-1kg(含1)、1-2kg(含2)、2-3kg(含3) 三档。</span>
<span class="help" data-show-for="flat_bands">超过最后一档还要按「面单+费率×重量」或「首重+续重」计费的,请改选上面的「小重量一口价 + 超出另算」。</span>
@error('quote_flat_bands')<span class="error">{{ $message }}</span>@enderror
</div>
<div class="field" data-show-for="flat_then">
<span class="label">超出一口价部分怎么算<span class="req">*</span></span>
<div class="choices">
<label class="choice"><input type="radio" name="quote_then_mode" value="waybill_rate" @checked(old('quote_then_mode', $structure['then_mode'] ?: 'waybill_rate') === 'waybill_rate') data-structure-input><span>面单 + 费率×重量</span></label>
<label class="choice"><input type="radio" name="quote_then_mode" value="first_additional" @checked(old('quote_then_mode', $structure['then_mode']) === 'first_additional') data-structure-input><span>首重 + 续重</span></label>
</div>
</div>
<div class="field @error('quote_first_weight') has-error @enderror" data-show-for="first_additional flat_then:first_additional">
<label for="quote_first_weight">首重公斤数<span class="req">*</span></label>
<input id="quote_first_weight" name="quote_first_weight" type="number" inputmode="decimal" min="0" step="0.01" value="{{ old('quote_first_weight', $structure['first_weight'] !== null ? \App\Support\CourierQuoteStructure::kg($structure['first_weight']) : '') }}" placeholder="如 1、3、5、20" data-structure-input>
@error('quote_first_weight')<span class="error">{{ $message }}</span>@enderror
</div>
<div class="field @error('quote_rate_tiers') has-error @enderror" data-show-for="waybill_rate first_additional flat_then">
<label for="quote_rate_tiers"><span data-rate-name>费率</span>按重量分段(选填,kg)</label>
<input id="quote_rate_tiers" name="quote_rate_tiers" type="text" inputmode="decimal" maxlength="200" value="{{ old('quote_rate_tiers', $kgList($structure['rate_tiers'])) }}" placeholder="不分段就留空;如 35,50" data-structure-input>
<span class="help">「35,50」表示 0-35kg、35-50kg、50kg以上三档,按总重量所在区间取价</span>
@error('quote_rate_tiers')<span class="error">{{ $message }}</span>@enderror
</div>
</div>
<div data-show-for="waybill_rate first_additional flat_bands flat_then">
<span class="label">③ 各省价格</span>
<div class="quote-tools" data-fill-tools></div>
<p class="help" style="margin:6px 0 10px">全国统一价先在上面填好,点「填充到全部省份」(会覆盖已填的价格),个别省份再单独改;不发的省份整行留空。{{ $requote ? '审核通过后会生成新的报价确认单推送给快递签署,请仔细核对。' : '这份报价会直接生成报价确认单发给你签署,请仔细核对。' }}</p>
@error('quote_rows')<span class="error">{{ $message }}</span>@enderror
<p class="help" style="margin:0 0 6px" data-scroll-hint>列较多时左右滑动表格查看,省份一列固定不动</p>
<div class="quote-table-wrap">
<table class="quote-table"><thead data-quote-head></thead><tbody data-quote-body></tbody></table>
</div>
<script type="application/json" data-quote-rows>{!! json_encode($quoteRows, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP) !!}</script>
</div>
<div class="upload @error('files.quote_sheet') has-error @enderror" data-show-for="other" data-upload>
<div class="upload__head"><span class="upload__title">报价表<span class="req">*</span></span></div>
<label class="upload__drop">
<span class="upload__preview" data-upload-preview>
@if ($quoteSheet)
<span class="upload__done">已上传</span><span class="upload__name">{{ $quoteSheet->original_name }}</span>
@else
<span class="upload__plus" aria-hidden="true">+</span><span class="upload__name">上传 Excel、PDF 或照片</span>
@endif
</span>
@if ($editable)<input type="file" name="files[quote_sheet][]" accept="image/*,.pdf,.xls,.xlsx" aria-label="上传报价表" data-upload-input>@endif
</label>
@error('files.quote_sheet')<span class="error">{{ $message }}</span>@enderror
</div>
<span class="label">④ 附加规则</span>
<div class="grid-2">
<div class="field @error('volumetric_rule') has-error @enderror">
<span class="label">是否计泡<span class="req">*</span></span>
<div class="choices">
@foreach (\App\Models\Business\CourierOnboarding::VOLUMETRIC_RULES as $rule)
<label class="choice"><input type="radio" name="volumetric_rule" value="{{ $rule }}" @checked($value('volumetric_rule') === $rule) data-volumetric-rule><span>{{ $rule }}</span></label>
@endforeach
</div>
@error('volumetric_rule')<span class="error">{{ $message }}</span>@enderror
</div>
<div class="field @error('volumetric_divisor') has-error @enderror" data-volumetric-divisor>
<label for="volumetric_divisor">计泡系数</label>
<input id="volumetric_divisor" name="volumetric_divisor" type="number" inputmode="numeric" min="1000" max="20000" step="1" value="{{ $value('volumetric_divisor') }}" placeholder="长×宽×高 ÷ 6000">
@error('volumetric_divisor')<span class="error">{{ $message }}</span>@enderror
</div>
</div>
<div class="grid-2">
<div class="field @error('min_weight') has-error @enderror">
<label for="min_weight">起算重量(kg,选填)</label>
<input id="min_weight" name="min_weight" type="number" inputmode="decimal" min="0" step="0.01" value="{{ $num($value('min_weight')) }}" placeholder="如不足 10kg 按 10kg 算,填 10">
</div>
@if (! empty($requote))
{{-- 入驻表单不再填写重量取整、每票固定加收;后台变价沿用当前规则,避免把已有规则清空。 --}}
<input type="hidden" name="weight_rounding" value="{{ $value('weight_rounding', 'none') }}">
<input type="hidden" name="per_ticket_surcharge" value="{{ $num($value('per_ticket_surcharge')) }}">
@endif
<div class="field @error('quote_effective_date') has-error @enderror">
<label for="quote_effective_date">报价执行日期<span class="req">*</span></label>
<input id="quote_effective_date" name="quote_effective_date" type="date" value="{{ old('quote_effective_date', $onboarding->quote_effective_date?->format('Y-m-d')) }}">
@error('quote_effective_date')<span class="error">{{ $message }}</span>@enderror
</div>
</div>
@if (! empty($requote))
<div class="grid-2">
<div class="field @error('quote_end_date') has-error @enderror">
<label for="quote_end_date">结束日期(选填 · 短期变价)</label>
<input id="quote_end_date" name="quote_end_date" type="date" value="{{ old('quote_end_date', ($requote['change'] ?? null)?->end_date?->format('Y-m-d')) }}">
<span class="help">留空 = 长期执行。填了就是临时价:只在执行日期~结束日期期间用,结束后第二天自动恢复原来的长期报价,快递不用再签。</span>
@error('quote_end_date')<span class="error">{{ $message }}</span>@enderror
</div>
</div>
@endif
<div class="field">
<label for="quote_note">报价备注</label>
<textarea id="quote_note" name="quote_note" maxlength="2000" placeholder="如:限长限重、超长费、上楼费、偏远地区加价、深圳等城市单独价格">{{ $value('quote_note') }}</textarea>
<span class="help">需要收取的附加费(超长超重、偏远地区、上楼、保价等)请在这里写清楚;没写的视为不收取。报价备注会原样写进报价确认单,由你方盖章确认。</span>
</div>
</div>
</section>
@if ($requote)
<section class="card" aria-labelledby="sec-summary">
<div class="card__head"><span class="card__index">2</span><h2 class="card__title" id="sec-summary">变价说明</h2></div>
<div class="card__body">
<div class="field @error('summary') has-error @enderror">
<label for="summary">变价原因 / 依据<span class="req">*</span></label>
<textarea id="summary" name="summary" maxlength="2000" placeholder="如:快递通知 11 月起江浙沪面单上调 0.3 元;改为 0-1、1-2、2-3kg 三档一口价">{{ $requote['summary'] }}</textarea>
@error('summary')<span class="error">{{ $message }}</span>@enderror
</div>
</div>
</section>
@else
<section class="card" aria-labelledby="sec-files">
<div class="card__head"><span class="card__index">6</span><h2 class="card__title" id="sec-files">上传证件照片</h2><span class="card__hint">可直接拍照,系统会自动压缩</span></div>
<div class="card__body">
<div class="upload-grid">
@foreach ($fileKinds as $kind => $label)
@php $existing = $filesByKind->get($kind, collect())->last(); @endphp
<div class="upload @error('files.' . $kind) has-error @enderror" data-upload>
<div class="upload__head">
<span class="upload__title">
<span data-entity-only="company">{{ $label }}@if (in_array($kind, $requiredKinds, true))<span class="req">*</span>@endif</span>
<span data-entity-only="person">{{ $personKinds[$kind] ?? $label }}@if (in_array($kind, $personRequiredKinds, true))<span class="req">*</span>@endif</span>
</span>
</div>
<label class="upload__drop">
<span class="upload__preview" data-upload-preview>
@if ($existing)
<span class="upload__done">已上传</span><span class="upload__name">{{ $existing->original_name }}</span>
@else
<span class="upload__plus" aria-hidden="true">+</span><span class="upload__name">拍照或选择图片</span>
@endif
</span>
@if ($editable)
<input type="file" name="files[{{ $kind }}][]" accept="image/*,.pdf" aria-label="上传{{ $label }}" data-upload-input>
@endif
</label>
@if ($existing && $editable && ! in_array($kind, $requiredKinds, true))
<label class="upload__remove"><input type="checkbox" name="remove_files[]" value="{{ $existing->id }}">删除已上传</label>
@endif
@error('files.' . $kind)<span class="error">{{ $message }}</span>@enderror
</div>
@endforeach
</div>
<p class="help" style="margin:0" data-upload-total></p>
<div class="field">
<label for="remark">其他说明</label>
<textarea id="remark" name="remark" maxlength="2000">{{ $value('remark') }}</textarea>
</div>
</div>
</section>
@endif
</fieldset>
@if ($requote)
<div class="actionbar">
<div class="actionbar__inner">
<a class="btn" href="{{ $requote['back'] }}">返回</a>
<button type="submit" class="btn btn--primary" data-submit-button>提交审核</button>
</div>
</div>
@elseif ($editable)
<div class="actionbar">
<div class="actionbar__inner">
<button type="submit" name="action" value="save" class="btn" data-submit-button>暂存</button>
<button type="submit" name="action" value="submit" class="btn btn--primary" data-submit-button>提交审核</button>
</div>
</div>
@endif
</form>
@unless ($requote)
<p class="footer-note">本页面仅用于快递合作入驻,资料仅用于合作审核与结算。<br>如有疑问请联系我方对接人。</p>
@endunless
</main>
<script>
(function () {
var form = document.querySelector('[data-onboarding-form]');
if (!form) return;
function applyVolumetric() {
var checked = form.querySelector('[data-volumetric-rule]:checked');
var box = form.querySelector('[data-volumetric-divisor]');
if (box) box.style.display = checked && checked.value === '计泡' ? '' : 'none';
}
function applyEntity() {
var checked = form.querySelector('[data-entity-type]:checked');
var type = checked ? checked.value : 'company';
form.setAttribute('data-entity', type);
form.querySelectorAll('[data-entity-only]').forEach(function (el) {
var show = el.getAttribute('data-entity-only') === type;
el.querySelectorAll('input, select, textarea').forEach(function (input) { if (!input.closest('fieldset[disabled]')) input.disabled = !show; });
});
form.querySelectorAll('[data-placeholder-' + type + ']').forEach(function (el) { el.placeholder = el.getAttribute('data-placeholder-' + type); });
}
form.querySelectorAll('[data-entity-type]').forEach(function (el) { el.addEventListener('change', applyEntity); });
form.querySelectorAll('[data-choose-entity]').forEach(function (button) {
button.addEventListener('click', function () {
var radio = form.querySelector('[data-entity-type][value="' + button.getAttribute('data-choose-entity') + '"]');
if (radio) { radio.checked = true; }
applyEntity();
window.scrollTo(0, 0);
});
});
if (form.querySelector('[data-entity-type]:checked')) applyEntity();
// 手机原图往往 3-8MB,上传前在本机压缩到 1600px 左右的 JPEG,避免超过服务器上传上限。
var MAX_TOTAL = 11 * 1024 * 1024;
function compress(file) {
return new Promise(function (resolve) {
if (!/^image\//.test(file.type) || file.size < 600 * 1024) { resolve(file); return; }
var url = URL.createObjectURL(file);
var img = new Image();
img.onload = function () {
var scale = Math.min(1, 1600 / Math.max(img.width, img.height));
var canvas = document.createElement('canvas');
canvas.width = Math.round(img.width * scale);
canvas.height = Math.round(img.height * scale);
canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);
URL.revokeObjectURL(url);
canvas.toBlob(function (blob) {
if (!blob || blob.size >= file.size) { resolve(file); return; }
resolve(new File([blob], file.name.replace(/\.[^.]+$/, '') + '.jpg', { type: 'image/jpeg' }));
}, 'image/jpeg', 0.82);
};
img.onerror = function () { URL.revokeObjectURL(url); resolve(file); };
img.src = url;
});
}
function totalSize() {
var total = 0;
form.querySelectorAll('[data-upload-input]').forEach(function (input) {
Array.prototype.forEach.call(input.files || [], function (f) { total += f.size; });
});
return total;
}
function refreshTotal() {
var note = form.querySelector('[data-upload-total]');
var total = totalSize();
if (note) note.textContent = total > 0 ? '本次待上传 ' + (total / 1048576).toFixed(1) + ' MB' + (total > MAX_TOTAL ? ',超过 11MB,请分两次暂存上传' : '') : '';
}
form.querySelectorAll('[data-upload-input]').forEach(function (input) {
input.addEventListener('change', function () {
var file = input.files && input.files[0];
var preview = input.closest('[data-upload]').querySelector('[data-upload-preview]');
if (!file) { refreshTotal(); return; }
preview.innerHTML = '<span class="upload__name">处理中…</span>';
compress(file).then(function (result) {
if (result !== file && window.DataTransfer) {
try { var dt = new DataTransfer(); dt.items.add(result); input.files = dt.files; } catch (e) { result = file; }
}
if (/^image\//.test(result.type)) {
var img = document.createElement('img');
img.src = URL.createObjectURL(result);
img.alt = '';
preview.innerHTML = '';
preview.appendChild(img);
} else {
preview.innerHTML = '<span class="upload__done">已选择</span><span class="upload__name"></span>';
preview.querySelector('.upload__name').textContent = result.name;
}
refreshTotal();
});
});
});
form.querySelectorAll('[data-default-from]').forEach(function (target) {
var source = form.querySelector('[name="' + target.getAttribute('data-default-from') + '"]');
if (!source) return;
source.addEventListener('change', function () { if (target.value === '') target.value = source.value; });
});
form.querySelectorAll('[data-volumetric-rule]').forEach(function (el) { el.addEventListener('change', applyVolumetric); });
applyVolumetric();
// ---- 打款账户:对公户名 = 公司全称;对私收款人 = 法定代表人(个人为本人) ----
function normName(v) { return String(v || '').replace(/(/g, '(').replace(/)/g, ')').replace(/[\s ]+/g, '').toUpperCase(); }
function checkAccounts() {
var person = form.getAttribute('data-entity') === 'person';
var company = (form.querySelector('[name=company_full_name]') || {}).value || '';
var legal = (form.querySelector('[name=legal_representative]') || {}).value || '';
[['corporate', person ? '' : company, '营业执照公司全称'], ['private', person ? company : legal, person ? '本人姓名' : '营业执照法定代表人']].forEach(function (item) {
var input = form.querySelector('[data-account-name="' + item[0] + '"]');
var note = form.querySelector('[data-account-check="' + item[0] + '"]');
if (!input || !note) return;
note.className = 'account__check';
if (!input.value.trim() || !item[1].trim()) { note.textContent = ''; return; }
var ok = normName(input.value) === normName(item[1]);
note.classList.add(ok ? 'is-ok' : 'is-bad');
note.textContent = ok ? '✓ 与' + item[2] + '一致' : '✗ 与' + item[2] + '「' + item[1].trim() + '」不一致,提交时会被拦下';
});
}
['company_full_name', 'legal_representative', 'bank_account_name', 'private_account_name'].forEach(function (name) {
var el = form.querySelector('[name=' + name + ']');
if (el) el.addEventListener('input', checkAccounts);
});
form.querySelectorAll('[data-entity-type], [data-choose-entity]').forEach(function (el) { el.addEventListener(el.tagName === 'BUTTON' ? 'click' : 'change', checkAccounts); });
checkAccounts();
// ---- 报价表:按计费方式和重量结构动态生成列 ----
var quoteSection = form.querySelector('[data-quote-section]');
var rowsScript = form.querySelector('[data-quote-rows]');
var model = rowsScript ? JSON.parse(rowsScript.textContent || '[]') : [];
var editableForm = !form.querySelector('fieldset[disabled]');
function kg(v) { return String(Math.round(v * 100) / 100); }
function parseList(text) {
var seen = {}, out = [];
String(text || '').split(/[\s,,、;;\/]+/).forEach(function (p) {
p = p.replace(/(kg|KG|公斤)$/, '');
var n = parseFloat(p);
if (!isNaN(n) && n > 0 && !seen[n]) { seen[n] = true; out.push(Math.round(n * 100) / 100); }
});
return out.sort(function (a, b) { return a - b; });
}
function structure() {
var mode = (form.querySelector('[data-quote-mode]:checked') || {}).value || '';
var thenMode = (form.querySelector('[name=quote_then_mode]:checked') || {}).value || 'waybill_rate';
return {
mode: mode,
thenMode: mode === 'flat_then' ? thenMode : '',
bands: (mode === 'flat_bands' || mode === 'flat_then') ? parseList(form.querySelector('[name=quote_flat_bands]').value).slice(0, 12) : [],
firstWeight: parseFloat(form.querySelector('[name=quote_first_weight]').value) || 0,
tiers: (mode === 'waybill_rate' || mode === 'first_additional' || mode === 'flat_then') ? parseList(form.querySelector('[name=quote_rate_tiers]').value).slice(0, 6) : []
};
}
function columns(st) {
var cols = [];
if (!st.mode || st.mode === 'other') return cols;
var lower = 0;
st.bands.forEach(function (upper, i) { cols.push({ key: 'flat', index: i, slot: 'flat:' + lower + '-' + upper, label: kg(lower) + '-' + kg(upper) + 'kg(含' + kg(upper) + ') 一口价' }); lower = upper; });
if (st.mode === 'flat_bands') return cols;
var first = st.mode === 'first_additional' || st.thenMode === 'first_additional';
var prefix = st.mode === 'flat_then' && st.bands.length ? kg(st.bands[st.bands.length - 1]) + 'kg 以上 ' : '';
cols.push({ key: 'base', slot: 'base', label: prefix + (first ? '首重' + kg(st.firstWeight) + 'kg(元)' : '面单') });
var name = first ? '续重' : '费率';
if (!st.tiers.length) { cols.push({ key: 'rates', index: 0, slot: 'rates:0', label: name + '(元/kg)' }); return cols; }
var start = st.mode === 'flat_then' && st.bands.length ? st.bands[st.bands.length - 1] : 0;
st.tiers.forEach(function (edge, i) { cols.push({ key: 'rates', index: i, slot: 'rates:' + i, label: name + ' ' + kg(i === 0 ? start : st.tiers[i - 1]) + '-' + kg(edge) + 'kg' }); });
cols.push({ key: 'rates', index: st.tiers.length, slot: 'rates:' + st.tiers.length, label: name + ' ' + kg(st.tiers[st.tiers.length - 1]) + 'kg以上' });
return cols;
}
// 每个价格按「计费方式 + 列含义」单独存放:换了计费方式,面单不会跑到首重价那一列;切回来原来填的还在。
function slotKey(st, col) { return st.mode + '|' + st.thenMode + '|' + col.slot; }
function readTable() {
if (!quoteSection) return;
quoteSection.querySelectorAll('[data-q-row]').forEach(function (tr) {
var row = model[+tr.getAttribute('data-q-row')];
var region = tr.querySelector('[data-q-region]');
if (region && region.type === 'text') row.region = region.value;
tr.querySelectorAll('[data-q-slot]').forEach(function (input) { row.vals[input.getAttribute('data-q-slot')] = input.value; });
});
}
function esc(t) { return String(t).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); }
function nameFor(i, col) { return 'quote_rows[' + i + '][' + col.key + ']' + (col.key === 'base' ? '' : '[' + col.index + ']'); }
// ---- 一口价重量档:逐档添加 / 删除,写回隐藏字段 quote_flat_bands ----
var bandValue = form.querySelector('[data-band-value]');
var bandList = form.querySelector('[data-band-list]');
var bandAdd = form.querySelector('[data-band-add]');
function bandInputs() { return bandList ? Array.prototype.slice.call(bandList.querySelectorAll('[data-band-input]')) : []; }
function syncBands() {
var inputs = bandInputs(), prev = '0';
bandValue.value = inputs.map(function (el) { return el.value; }).filter(function (v) { return v !== ''; }).join(',');
inputs.forEach(function (el, i) {
var row = el.closest('[data-band-row]');
row.querySelector('[data-band-no]').textContent = '第' + (i + 1) + '档';
row.querySelector('[data-band-lower]').textContent = prev + ' -';
prev = el.value !== '' ? el.value : '?';
});
}
function buildBands(values) {
var dis = editableForm ? '' : ' disabled';
bandList.innerHTML = values.map(function (v) {
return '<div class="band-row" data-band-row><span class="band-row__no" data-band-no></span><span data-band-lower></span>'
+ '<input type="number" inputmode="decimal" min="0" step="0.01" value="' + esc(v) + '" placeholder="上限" aria-label="本档上限重量" data-band-input' + dis + '>'
+ '<span>kg(含)</span>'
+ (editableForm ? '<button type="button" class="band-row__del" aria-label="删除这一档" data-band-del>×</button>' : '<span></span>') + '</div>';
}).join('');
bandInputs().forEach(function (el) { el.addEventListener('input', function () { syncBands(); readTable(); render(); }); });
bandList.querySelectorAll('[data-band-del]').forEach(function (btn) {
btn.addEventListener('click', function () {
var values = bandInputs().map(function (el) { return el.value; });
values.splice(bandInputs().indexOf(btn.closest('[data-band-row]').querySelector('[data-band-input]')), 1);
buildBands(values.length ? values : ['']);
syncBands(); readTable(); render();
});
});
syncBands();
}
if (bandList && bandValue) {
var initialBands = parseList(bandValue.value).map(kg);
buildBands(initialBands.length ? initialBands : ['']);
if (bandAdd) {
if (!editableForm) bandAdd.style.display = 'none';
bandAdd.addEventListener('click', function () {
var values = bandInputs().map(function (el) { return el.value; });
if (values.length >= 12) return;
values.push('');
buildBands(values);
var inputs = bandInputs();
inputs[inputs.length - 1].focus();
});
}
}
// 页面带回的已填报价(服务端按当前结构的 flat[] / base / rates[])转成按列含义存放
(function () {
var st0 = structure(), cols0 = columns(st0);
model.forEach(function (row) {
row.vals = {};
cols0.forEach(function (c) {
var v = c.key === 'base' ? row.base : (row[c.key] || [])[c.index];
if (v !== null && v !== undefined && v !== '') row.vals[slotKey(st0, c)] = String(v);
});
});
})();
function render() {
if (!quoteSection) return;
var st = structure();
quoteSection.setAttribute('data-mode', st.mode || 'none');
quoteSection.querySelectorAll('[data-show-for]').forEach(function (el) {
var tokens = el.getAttribute('data-show-for').split(' ');
var show = tokens.some(function (t) {
var parts = t.split(':');
return parts[0] === st.mode && (!parts[1] || parts[1] === st.thenMode);
});
el.style.display = show ? '' : 'none';
});
var rateName = quoteSection.querySelector('[data-rate-name]');
if (rateName) rateName.textContent = (st.mode === 'first_additional' || st.thenMode === 'first_additional') ? '续重' : '费率';
var cols = columns(st);
var head = quoteSection.querySelector('[data-quote-head]');
var body = quoteSection.querySelector('[data-quote-body]');
var tools = quoteSection.querySelector('[data-fill-tools]');
if (!head || !body) return;
var dis = editableForm ? '' : ' disabled';
head.innerHTML = '<tr><th>省份</th>' + cols.map(function (c) { return '<th>' + esc(c.label) + '</th>'; }).join('') + '</tr>';
body.innerHTML = model.map(function (row, i) {
var regionCell = row.fixed
? esc(row.region) + '<input type="hidden" name="quote_rows[' + i + '][region]" value="' + esc(row.region) + '" data-q-region>'
: '<input type="text" name="quote_rows[' + i + '][region]" value="' + esc(row.region || '') + '" maxlength="50" placeholder="其他地区" aria-label="自定义地区" data-q-region' + dis + '>';
return '<tr data-q-row="' + i + '"' + (row.fixed ? '' : ' class="is-custom"') + '><td>' + regionCell + '</td>' + cols.map(function (c) {
var slot = slotKey(st, c), v = row.vals[slot];
return '<td><input type="number" inputmode="decimal" step="0.01" min="0" name="' + nameFor(i, c) + '" value="' + esc(v === undefined ? '' : v) + '" data-q-slot="' + esc(slot) + '" aria-label="' + esc((row.region || '自定义') + c.label) + '"' + dis + '></td>';
}).join('') + '</tr>';
}).join('');
if (tools) {
tools.style.gridTemplateColumns = 'repeat(' + Math.min(Math.max(cols.length, 1), 3) + ', minmax(0, 1fr))';
tools.innerHTML = cols.map(function (c, i) {
return '<div class="field"><label for="fill-' + i + '">' + esc(c.label) + '</label><input id="fill-' + i + '" type="number" inputmode="decimal" step="0.01" data-fill-col="' + i + '"' + dis + '></div>';
}).join('') + (editableForm && cols.length ? '<button type="button" class="btn" data-fill-all style="grid-column:1/-1">填充到全部省份</button>' : '');
var fill = tools.querySelector('[data-fill-all]');
if (fill) fill.addEventListener('click', function () {
var filled = 0;
tools.querySelectorAll('[data-fill-col]').forEach(function (src) {
if (src.value === '') return;
filled++;
var slot = slotKey(st, cols[+src.getAttribute('data-fill-col')]);
body.querySelectorAll('tr:not(.is-custom) [data-q-slot]').forEach(function (cell) {
if (cell.getAttribute('data-q-slot') === slot) cell.value = src.value;
});
});
readTable();
if (!filled) window.alert('先在上面填好要统一的价格,再点填充。');
});
}
}
if (quoteSection) {
quoteSection.querySelectorAll('[data-quote-mode], [data-structure-input]').forEach(function (el) {
var typing = el.type === 'text' || el.type === 'number';
el.addEventListener(typing ? 'input' : 'change', function () { readTable(); render(); });
});
render();
}
form.addEventListener('submit', function (event) {
var submitter = event.submitter;
if (totalSize() > MAX_TOTAL) {
event.preventDefault();
window.alert('本次上传的图片总共超过 11MB,请先选几张点「暂存」,再继续上传剩下的。');
return;
}
if (submitter && submitter.value === 'submit' && !window.confirm('提交后需等待我方审核,审核期间不能修改。确认提交?')) {
event.preventDefault();
return;
}
if (submitter) {
var hidden = document.createElement('input');
hidden.type = 'hidden'; hidden.name = 'action'; hidden.value = submitter.value;
form.appendChild(hidden);
}
form.querySelectorAll('[data-submit-button]').forEach(function (button) { button.disabled = true; });
});
})();
</script>
</body>
</html>