快递应付 → 快递报价 下新增「快递入驻」页:生成入驻二维码发给快递方,
对方免登录在手机上填写主体、联系人、e签宝签署人、结算账户、报价并上传资质,
内部对接人初审、管理员财务复审后生成首版报价,并自动生成报价确认单 PDF
推送到快递填写的 e签宝账号,签署归档后开通。
- 新增三张表 business_courier_onboardings / _files / _events(纯新增,不改旧表)
- 免登录填报页 /onboarding/courier/{token}:40 位随机 token,可暂存、提交、退回后补件,
链接有效期可延长、可作废;银行账号加密存储;附件存 uploads 盘
- 新增 COURIER_ONBOARDING_PUBLIC_URL 配置二维码里的站点地址(留空用当前访问地址)
- 内置纯 PHP 二维码生成(App\Support\QrCode,SVG 输出,无新增依赖)
- 生成二维码时必须定好系统快递名称(唯一,快递方不可见);财务复审沿用该名称,快递所填报价写成报价管理的 CourierQuoteVersion,
复用 CourierQuoteSignatureService 生成报价确认单并推送 e签宝(失败可重推/同步状态);初审人默认不能复审(COURIER_ONBOARDING_ALLOW_SAME_REVIEWER=true 可在测试环境放开)
- 支持公司 / 个人两种入驻:个人填姓名+身份证号(加密)、传身份证和银行卡,报价确认单按个人身份在 e签宝签署
- 扫码先选入驻方式;证件改为 营业执照 / 法人身份证正反面 / 银行卡正反面 各一张(个人营业执照选填),手机端上传前自动压缩,超限回到填报页提示
- 报价支持 4 种计费方式(面单+费率 / 首重+续重 / 按重量段一口价 / 小重量一口价+超出另算,费率可分段)+「其他」上传报价表;附加规则:计泡、起算重量、取整、每票加收(CourierQuoteStructure)
- 不再要求上传盖章报价单,至少填写一行在线报价
- 开通:报价确认单签署归档、或匹配到已签署协议、或填写线下签署说明
- 生成二维码前必填「快递公司代码」(自动大写、不可重复),入驻信息按代码对齐快递资料
- 公司入驻对公(营业执照主体)+ 对私(法定代表人)两个打款账户必填并自动核对;个人入驻对私必填、对公选填
- 变价:报价管理里后台改价 → 审核 → 自动推送快递 e签宝 → 回签;回签单导出 xlsx/zip;误发版本可作废
- 短期(临时)变价:变价单/报价版本加选填「结束日期」(end_date);按日期取价,期间用临时价,到期次日自动恢复原长期报价,
不用重签;报价管理显示 生效中(临时)/ 暂停中 / 已到期,确认单写明执行期间和到期后恢复的版本,审核提醒期间重叠
- 快递资料分四类:快递资料 / 网点 / 打款信息 / 操作日志,支持查询、导出、重置、编辑;
财务系统入驻的快递直接作为快递资料行,合同起止取协议签署里已签署的合作合同;
点开一行在下方看该快递的网点 / 打款信息(完整账号)/ 操作日志
- 编辑存财务系统覆盖值(business_courier_profiles,不回写工作平台),改动记操作日志(business_courier_profile_events)
- 打款信息新增 / 变更(business_courier_payment_accounts,账号加密)保存后跳到协议签署,自动预填收款账户证明 / 收款账户变更,
按签署状态显示 待推送协议 / 签署中 / 已生效;入口在快递资料明细和「打款信息」页签(表格每个账户可直接「变更」)
- ERP 开通:入驻开通后标「待 ERP 开通」,可手动补录(钉钉「新建快递」流程同步待接入)
- 新增 CourierOnboardingWorkflowTest(8 个用例),更新快递报价目录顺序断言
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019gzfsnV3fJ89CoUy1D9V9g
1027 lines
74 KiB
PHP
1027 lines
74 KiB
PHP
@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 data-entity-only="company"><span class="req">*</span></span><span class="account__tag" data-entity-only="person">选填</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 class="req">*</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>
|
||
<div class="field">
|
||
<label for="weight_rounding">重量取整</label>
|
||
<select id="weight_rounding" name="weight_rounding">
|
||
@foreach (\App\Support\CourierQuoteStructure::ROUNDING as $key => $label)
|
||
<option value="{{ $key }}" @selected($value('weight_rounding', 'none') === $key)>{{ $label }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="grid-2">
|
||
<div class="field @error('per_ticket_surcharge') has-error @enderror">
|
||
<label for="per_ticket_surcharge">每票固定加收(元,选填)</label>
|
||
<input id="per_ticket_surcharge" name="per_ticket_surcharge" type="number" inputmode="decimal" min="0" step="0.01" value="{{ $num($value('per_ticket_surcharge')) }}" placeholder="如每票 +1 元,填 1">
|
||
</div>
|
||
<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>
|
||
</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 { '&': '&', '<': '<', '>': '>', '"': '"' }[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>
|