chore: 入库 9 月积压成果(滇中好运原型评估、小象开票需求与原型、工具配置)

- 原型评估_滇中好运物流云_20260917/:物流云原型评估产物(含核对表与字段对比表)
- requirements/小象开票收款方创建需求.md + prototypes/小象开票原型.html:小象开票建司机/车辆/收款信息需求
- requirements.txt:Execute 战区依赖调整(移除 pytest/allure-pytest/requests,改用 pytest-html,Appium 默认注释)
- .claude/launch.json:新增物流公司管理原型预览服务(wuliu-proto:8138)
- .agents/skills:agent 技能定义
This commit is contained in:
xst
2026-09-29 10:03:43 +08:00
parent 8c61209dd4
commit 5e4e978cd5
14 changed files with 2463 additions and 9 deletions
@@ -0,0 +1,628 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>小象开票 - 收款方创建 - 交互原型</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:"Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei",Arial,sans-serif; font-size:14px; color:#333; background:#f0f2f5; }
.app { display:flex; min-height:100vh; }
.sidebar { width:200px; background:#263445; color:#bfcbd9; flex-shrink:0; min-height:100vh; overflow-y:auto; }
.logo { height:56px; display:flex; align-items:center; justify-content:center; color:#fff; font-size:15px; font-weight:bold; background:#1f2d3d; letter-spacing:1px; }
.menu { list-style:none; padding-bottom:40px; }
.menu > li { line-height:44px; }
.menu .mi { display:block; padding:0 20px; cursor:pointer; white-space:nowrap; }
.menu .mi:hover { background:#1f2d3d; }
.menu .mi .arrow { float:right; font-size:12px; color:#8a9bb0; }
.submenu { list-style:none; background:#1f2d3d; display:none; }
.submenu.open { display:block; }
.submenu li { padding-left:34px; line-height:40px; cursor:pointer; white-space:nowrap; }
.submenu li:hover { color:#fff; }
.submenu li.active { color:#409eff; }
.submenu .sub2 { list-style:none; background:#1a2433; display:none; }
.submenu .sub2.open { display:block; }
.submenu .sub2 li { padding-left:50px; line-height:36px; font-size:13px; }
.main { flex:1; display:flex; flex-direction:column; min-width:0; }
.topbar { height:50px; background:#fff; display:flex; align-items:center; padding:0 16px; border-bottom:1px solid #e6e6e6; box-shadow:0 1px 4px rgba(0,21,41,.08); position:relative; z-index:5; }
.crumb { color:#97a8be; font-size:13px; }
.crumb b { color:#333; font-weight:normal; }
.crumb .sep { margin:0 6px; }
.topbar .right { margin-left:auto; display:flex; align-items:center; gap:16px; color:#5a5e66; }
.avatar { width:30px; height:30px; border-radius:50%; background:#409eff; color:#fff; display:inline-flex; align-items:center; justify-content:center; font-size:12px; }
.tabsbar { background:#fff; padding:6px 12px 0; border-bottom:1px solid #e6e6e6; display:flex; gap:6px; flex-wrap:wrap; }
.chip { background:#fff; border:1px solid #d8dce5; color:#495060; padding:4px 10px; font-size:12px; border-radius:2px; cursor:pointer; margin-bottom:6px; display:inline-flex; align-items:center; gap:6px; }
.chip .x { color:#9aa2b1; font-size:13px; }
.chip.active { background:#67c23a; border-color:#67c23a; color:#fff; }
.chip.active .x { color:#e6f7dd; }
.demo-strip { background:#fdf6ec; color:#b88230; font-size:12px; padding:5px 16px; border-bottom:1px solid #faecd8; }
.content { flex:1; padding:14px; }
.page { display:none; }
.page.active { display:block; }
.card { background:#fff; border-radius:4px; padding:18px; box-shadow:0 1px 2px rgba(0,0,0,.03); margin-bottom:14px; }
.page-title { font-size:15px; font-weight:bold; color:#303133; margin-bottom:14px; padding-left:8px; border-left:3px solid #409eff; }
.btn { display:inline-block; padding:8px 16px; font-size:13px; border-radius:3px; border:1px solid #dcdfe6; background:#fff; color:#606266; cursor:pointer; outline:none; }
.btn.primary { background:#409eff; border-color:#409eff; color:#fff; }
.btn.success { background:#67c23a; border-color:#67c23a; color:#fff; }
.btn.mini { padding:4px 10px; font-size:12px; }
.link { color:#409eff; cursor:pointer; margin-right:10px; font-size:13px; }
.link.danger { color:#f56c6c; }
.btn-row { margin-top:18px; padding-top:14px; border-top:1px solid #ebeef5; }
.btn-row .btn { margin-right:10px; }
.form-grid { display:grid; grid-template-columns:1fr 1fr; gap:2px 30px; }
.form-item { display:flex; align-items:flex-start; padding:8px 0; }
.form-item.full { grid-column:1 / -1; }
.form-item label { width:130px; text-align:right; padding-right:10px; line-height:32px; color:#606266; flex-shrink:0; font-size:13px; }
.req::before { content:"*"; color:#f56c6c; margin-right:3px; }
.form-item .ctrl { flex:1; min-width:0; }
.input, .select { width:100%; height:32px; border:1px solid #dcdfe6; border-radius:3px; padding:0 10px; font-size:13px; color:#606266; outline:none; background:#fff; }
.input:focus, .select:focus { border-color:#409eff; }
.input[readonly] { background:#f5f7fa; color:#909399; }
.tip { color:#909399; font-size:12px; line-height:32px; margin-left:8px; }
.upload-btn { display:inline-block; padding:6px 12px; background:#409eff; color:#fff; border-radius:3px; font-size:12px; cursor:pointer; }
.file-name { margin-left:8px; color:#67c23a; font-size:12px; }
.hint { color:#909399; font-size:12px; margin-top:6px; }
details { border:1px solid #ebeef5; border-radius:3px; padding:8px 12px; margin:10px 0; }
details summary { cursor:pointer; color:#409eff; font-size:13px; }
.btn-bar { display:flex; gap:10px; margin-bottom:14px; }
.table-wrap { overflow-x:auto; }
table.tbl { width:100%; border-collapse:collapse; font-size:13px; }
.tbl th, .tbl td { border-bottom:1px solid #ebeef5; padding:10px 8px; text-align:left; color:#606266; white-space:nowrap; }
.tbl th { background:#f8f8f9; color:#515a6e; font-weight:bold; }
.tbl tbody tr:nth-child(even) { background:#fafbfc; }
.tbl tbody tr:hover { background:#f5f7fa; }
.tbl tr.frow th { padding:6px 4px; }
.frow .input, .frow .select { height:28px; font-size:12px; color:#909399; }
.tag { display:inline-block; padding:2px 8px; font-size:12px; border-radius:3px; line-height:18px; }
.tag.green { background:#f0f9eb; color:#67c23a; border:1px solid #e1f3d8; }
.tag.orange { background:#fdf6ec; color:#e6a23c; border:1px solid #faecd8; }
.tag.red { background:#fef0f0; color:#f56c6c; border:1px solid #fde2e2; }
.tag.blue { background:#ecf5ff; color:#409eff; border:1px solid #d9ecff; }
.pagination { display:flex; align-items:center; justify-content:flex-end; gap:8px; margin-top:14px; font-size:13px; color:#606266; }
.pg-btn { min-width:28px; height:28px; border:1px solid #dcdfe6; border-radius:3px; background:#fff; color:#606266; cursor:pointer; font-size:12px; }
.pg-btn.cur { background:#409eff; border-color:#409eff; color:#fff; }
.empty { text-align:center; color:#909399; padding:30px 0; font-size:13px; }
.mask { display:none; position:fixed; inset:0; background:rgba(0,0,0,.5); z-index:100; align-items:flex-start; justify-content:center; }
.mask.open { display:flex; }
.dialog { background:#fff; border-radius:4px; width:680px; max-width:94vw; margin-top:60px; box-shadow:0 4px 20px rgba(0,0,0,.2); max-height:86vh; display:flex; flex-direction:column; }
.dialog .dlg-head { display:flex; align-items:center; padding:14px 18px; border-bottom:1px solid #ebeef5; font-weight:bold; color:#303133; }
.dialog .dlg-head .close { margin-left:auto; cursor:pointer; color:#909399; font-size:18px; line-height:1; }
.dialog .dlg-body { padding:18px 22px 6px; overflow-y:auto; }
.dialog .dlg-foot { padding:12px 18px 16px; text-align:right; }
.dialog .dlg-foot .btn { margin-left:10px; min-width:70px; }
.section-title { font-size:13px; color:#303133; font-weight:bold; margin:6px 0 4px; padding-left:8px; border-left:3px solid #409eff; }
#toast-wrap { position:fixed; top:60px; left:50%; transform:translateX(-50%); z-index:200; display:flex; flex-direction:column; gap:10px; align-items:center; }
.toast { padding:9px 18px; border-radius:4px; color:#fff; font-size:13px; box-shadow:0 2px 12px rgba(0,0,0,.15); }
.toast.success { background:#67c23a; }
.toast.error { background:#f56c6c; }
.toast.warning { background:#e6a23c; }
.home-cards { display:flex; gap:14px; flex-wrap:wrap; }
.stat-card { background:#fff; border-radius:4px; padding:18px 22px; min-width:190px; box-shadow:0 1px 2px rgba(0,0,0,.03); }
.stat-card .num { font-size:24px; font-weight:bold; color:#303133; margin-top:6px; }
.stat-card .lbl { color:#909399; font-size:13px; }
</style>
</head>
<body>
<div class="app">
<aside class="sidebar">
<div class="logo">Ⓦ 运八网络货运平台系统</div>
<ul class="menu">
<li><span class="mi" onclick="go('home')">✈ 首页</span></li>
<li><span class="mi" onclick="todo()">🎧 客服运维 <span class="arrow">⌃</span></span></li>
<li><span class="mi" onclick="todo()">📋 补单管理 <span class="arrow">⌃</span></span></li>
<li><span class="mi" onclick="todo()">🧾 发票管理 <span class="arrow">⌃</span></span></li>
<li>
<span class="mi" onclick="toggleMenu('m-36',this)">🧮 三十六匠发票 <span class="arrow">⌃</span></span>
<ul class="submenu open" id="m-36">
<li onclick="todo()">轨迹异常运单</li>
<li onclick="todo()">司机上传</li>
<li onclick="todo()">车辆上传</li>
<li onclick="todo()">运单上传</li>
<li onclick="todo()">资金上传</li>
<li onclick="todo()">上传查询</li>
</ul>
</li>
<li><span class="mi" onclick="todo()">📄 满货达发票 <span class="arrow">⌃</span></span></li>
<li><span class="mi" onclick="todo()">🧾 滇运通开票 <span class="arrow">⌃</span></span></li>
<li>
<span class="mi" onclick="toggleMenu('m-xx',this)">🐘 小象开票 <span class="arrow">⌃</span></span>
<ul class="submenu open" id="m-xx">
<li onclick="todo()">承运人管理</li>
<li class="active" onclick="go('driver')">创建司机 <span style="color:#67c23a;font-size:11px;">NEW</span></li>
<li onclick="go('vehicle')">创建车辆 <span style="color:#67c23a;font-size:11px;">NEW</span></li>
<li onclick="go('payee')">新增收款信息 <span style="color:#67c23a;font-size:11px;">NEW</span></li>
<li onclick="todo()">创建运单</li>
<li onclick="todo()">提货</li>
<li onclick="todo()">运抵</li>
<li onclick="todo()">开票结果查询</li>
</ul>
</li>
<li><span class="mi" onclick="todo()">🛠 管理服务 <span class="arrow">⌃</span></span></li>
<li><span class="mi" onclick="todo()">✨ 增值服务 <span class="arrow">⌃</span></span></li>
<li><span class="mi" onclick="todo()">📊 财务统计 <span class="arrow">⌃</span></span></li>
</ul>
</aside>
<div class="main">
<header class="topbar">
<span class="crumb" id="crumb"></span>
<div class="right">
<span class="avatar">管</span><span style="font-size:13px;">超级管理员</span>
</div>
</header>
<nav class="tabsbar" id="tabsbar">
<span class="chip" data-page="home" onclick="go('home')">首页 <span class="x">×</span></span>
<span class="chip" data-page="driver" onclick="go('driver')">创建司机 <span class="x">×</span></span>
<span class="chip" data-page="vehicle" onclick="go('vehicle')">创建车辆 <span class="x">×</span></span>
<span class="chip" data-page="payee" onclick="go('payee')">新增收款信息 <span class="x">×</span></span>
</nav>
<div class="demo-strip">交互原型:数据为演示数据、不落库;对接 xxyb 接口(创建司机 / 创建车辆 / 新增收款信息);筛选样式参照真实页面(表头筛选行)。</div>
<main class="content">
<section class="page" id="page-home">
<div class="home-cards">
<div class="stat-card"><div class="lbl">已创建司机</div><div class="num" id="st-d">0</div></div>
<div class="stat-card"><div class="lbl">已创建车辆</div><div class="num" id="st-v">0</div></div>
<div class="stat-card"><div class="lbl">收款信息</div><div class="num" id="st-p">0</div></div>
<div class="stat-card"><div class="lbl">待审核</div><div class="num" style="color:#e6a23c;" id="st-wait">0</div></div>
</div>
<div class="card" style="margin-top:14px;">
<div class="page-title">快捷入口(小象开票)</div>
<div class="btn-bar" style="margin-bottom:0;">
<button class="btn primary" onclick="go('driver')">创建司机</button>
<button class="btn primary" onclick="go('vehicle')">创建车辆</button>
<button class="btn success" onclick="go('payee')">新增收款信息</button>
</div>
</div>
</section>
<!-- 创建司机 -->
<section class="page" id="page-driver">
<div class="card">
<div class="page-title">创建司机(小象开票)</div>
<div class="btn-bar">
<button class="btn primary" onclick="renderDrivers()">查询</button>
<button class="btn success" onclick="openDriverDlg(-1)">新增司机</button>
<button class="btn" onclick="toast('演示:批量上传(Excel 模板导入,Q1)','warning')">批量上传</button>
<button class="btn" onclick="toast('演示:司机身份证批量上传(Q1)','warning')">司机身份证批量上传</button>
<button class="btn" onclick="toast('演示:导出成功','success')">导出</button>
</div>
<div class="table-wrap">
<table class="tbl">
<thead><tr class="frow">
<th style="width:34px;"></th>
<th><select class="select" id="df-tax" onchange="renderDrivers()"><option value="">请选择税源地</option></select></th>
<th><select class="select" id="df-status" onchange="renderDrivers()"><option value="">请选择审核状态</option><option>WAITING</option><option>SUCCESS</option><option>FAILURE</option></select></th>
<th><input type="date" class="input" id="df-d1" onchange="renderDrivers()" style="width:125px;"><input type="date" class="input" id="df-d2" onchange="renderDrivers()" style="width:125px;"></th>
<th><input class="input" id="df-name" placeholder="请输入司机" oninput="renderDrivers()"></th>
<th><input class="input" id="df-idcard" placeholder="请输入身份证号" oninput="renderDrivers()"></th>
<th></th><th></th><th></th>
</tr><tr>
<th style="width:34px;"><input type="checkbox" onchange="document.querySelectorAll('.ck-d').forEach(c=>c.checked=this.checked)"></th>
<th>税源地</th><th>审核状态</th><th>审核时间</th><th>司机姓名</th><th>身份证号</th><th>身份证有效期</th><th>手机号</th><th style="width:110px;">操作</th>
</tr></thead>
<tbody id="driver-tbody"></tbody>
</table>
</div>
<div class="pagination"><span id="driver-pg"></span><button class="pg-btn">«</button><button class="pg-btn cur">1</button><button class="pg-btn">»</button><select><option>20条/页</option></select></div>
</div>
</section>
<!-- 创建车辆 -->
<section class="page" id="page-vehicle">
<div class="card">
<div class="page-title">创建车辆(小象开票)</div>
<div class="btn-bar">
<button class="btn primary" onclick="renderVehicles()">查询</button>
<button class="btn success" onclick="openVehicleDlg(-1)">新增车辆</button>
<button class="btn" onclick="toast('演示:批量上传(Q1)','warning')">批量上传</button>
<button class="btn" onclick="toast('演示:车牌号批量上传(Q1)','warning')">车牌号批量上传</button>
<button class="btn" onclick="toast('演示:导出成功','success')">导出</button>
</div>
<div class="table-wrap">
<table class="tbl">
<thead><tr class="frow">
<th style="width:34px;"></th>
<th><select class="select" id="vf-tax" onchange="renderVehicles()"><option value="">请选择税源地</option></select></th>
<th><select class="select" id="vf-status" onchange="renderVehicles()"><option value="">请选择审核状态</option><option>WAITING</option><option>SUCCESS</option><option>FAILURE</option></select></th>
<th><input type="date" class="input" id="vf-d1" onchange="renderVehicles()" style="width:125px;"><input type="date" class="input" id="vf-d2" onchange="renderVehicles()" style="width:125px;"></th>
<th><input class="input" id="vf-plate" placeholder="请输入车牌" oninput="renderVehicles()"></th>
<th><input class="input" id="vf-owner" placeholder="请输入车辆所有人" oninput="renderVehicles()"></th>
<th><select class="select" id="vf-type" onchange="renderVehicles()"><option value="">请选择车辆分类</option></select></th>
<th><select class="select" id="vf-color" onchange="renderVehicles()"><option value="">请选择车牌颜色</option></select></th>
<th><select class="select" id="vf-energy" onchange="renderVehicles()"><option value="">请选择能源类型</option></select></th>
</tr><tr>
<th style="width:34px;"><input type="checkbox" onchange="document.querySelectorAll('.ck-v').forEach(c=>c.checked=this.checked)"></th>
<th>税源地</th><th>审核状态</th><th>审核时间</th><th>车牌号</th><th>车辆所有人</th><th>车辆分类</th><th>车牌颜色</th><th>车辆能源类型</th><th style="width:110px;">操作</th>
</tr></thead>
<tbody id="vehicle-tbody"></tbody>
</table>
</div>
<div class="pagination"><span id="vehicle-pg"></span><button class="pg-btn">«</button><button class="pg-btn cur">1</button><button class="pg-btn">»</button><select><option>20条/页</option></select></div>
</div>
</section>
<!-- 新增收款信息 -->
<section class="page" id="page-payee">
<div class="card">
<div class="page-title">新增收款信息(小象开票)</div>
<div class="btn-bar">
<button class="btn primary" onclick="renderPayees()">查询</button>
<button class="btn success" onclick="openPayeeDlg(-1)">新增收款信息</button>
<button class="btn" onclick="toast('演示:导出成功','success')">导出</button>
</div>
<div class="table-wrap">
<table class="tbl">
<thead><tr class="frow">
<th style="width:34px;"></th>
<th><input class="input" id="pf-name" placeholder="请输入收款人姓名" oninput="renderPayees()"></th>
<th><input class="input" id="pf-idcard" placeholder="请输入身份证号" oninput="renderPayees()"></th>
<th><select class="select" id="pf-type" onchange="renderPayees()"><option value="">请选择收款人类型</option><option value="PERSONAL">个人</option><option value="ENTERPRISE">企业</option></select></th>
<th></th><th></th><th></th><th></th>
</tr><tr>
<th style="width:34px;"><input type="checkbox" onchange="document.querySelectorAll('.ck-p').forEach(c=>c.checked=this.checked)"></th>
<th>收款人姓名</th><th>身份证号</th><th>收款银行卡号</th><th>收款银行</th><th>收款手机号</th><th>收款人类型</th><th style="width:130px;">第三方ID / 操作</th>
</tr></thead>
<tbody id="payee-tbody"></tbody>
</table>
</div>
<div class="pagination"><span id="payee-pg"></span><button class="pg-btn">«</button><button class="pg-btn cur">1</button><button class="pg-btn">»</button><select><option>20条/页</option></select></div>
</div>
</section>
</main>
</div>
</div>
<!-- 新增/查看 司机 -->
<div class="mask" id="mask-driver">
<div class="dialog">
<div class="dlg-head"><span id="driver-dlg-title">新增司机</span><span class="close" onclick="closeDialog('mask-driver')">×</span></div>
<div class="dlg-body">
<div class="section-title">基础信息(司机实名)</div>
<div class="form-grid">
<div class="form-item"><label class="req" id="lb-d-phone">司机手机号</label><div class="ctrl"><input class="input" id="d-phone" placeholder="请输入司机手机号" maxlength="11"></div></div>
<div class="form-item"><label class="req" id="lb-d-name">司机姓名</label><div class="ctrl"><input class="input" id="d-name" placeholder="请输入司机姓名"></div></div>
<div class="form-item"><label class="req" id="lb-d-idcard">身份证号</label><div class="ctrl"><input class="input" id="d-idcard" placeholder="请输入18位身份证号" maxlength="18"></div></div>
<div class="form-item"><label id="lb-d-addr">身份证地址</label><div class="ctrl"><input class="input" id="d-addr" placeholder="请输入身份证地址"></div></div>
<div class="form-item"><label id="lb-d-front">身份证人像面</label><div class="ctrl"><span class="upload-btn" onclick="mockFile('d-front')">点击上传</span><span class="file-name" id="d-front"></span></div></div>
<div class="form-item"><label id="lb-d-back">身份证国徽面</label><div class="ctrl"><span class="upload-btn" onclick="mockFile('d-back')">点击上传</span><span class="file-name" id="d-back"></span></div></div>
</div>
<details>
<summary>司机认证信息(选填,driverAuthDomain)</summary>
<div class="form-grid" style="margin-top:8px;">
<div class="form-item"><label id="lb-d-license">驾驶证号</label><div class="ctrl"><input class="input" id="d-license"></div></div>
<div class="form-item"><label id="lb-d-dltype">准驾车型</label><div class="ctrl"><select class="select" id="d-dltype"><option value="">请选择</option></select></div></div>
<div class="form-item"><label id="lb-d-authority">驾驶证发证机关</label><div class="ctrl"><input class="input" id="d-authority"></div></div>
<div class="form-item"><label id="lb-d-lfront">驾驶证正面</label><div class="ctrl"><span class="upload-btn" onclick="mockFile('d-lfront')">点击上传</span><span class="file-name" id="d-lfront"></span></div></div>
<div class="form-item"><label id="lb-d-lback">驾驶证反面</label><div class="ctrl"><span class="upload-btn" onclick="mockFile('d-lback')">点击上传</span><span class="file-name" id="d-lback"></span></div></div>
<div class="form-item"><label id="lb-d-lrange">驾驶证有效期</label><div class="ctrl"><input class="input" id="d-lstart" placeholder="起 yyyy-MM-dd" style="margin-bottom:4px;"><input class="input" id="d-lend" placeholder="止 yyyy-MM-dd 或 长期"></div></div>
<div class="form-item"><label id="lb-d-pqc">从业资格证</label><div class="ctrl"><input class="input" id="d-pqc"></div></div>
<div class="form-item"><label id="lb-d-pqcimg">从业资格证图片</label><div class="ctrl"><span class="upload-btn" onclick="mockFile('d-pqcimg')">点击上传</span><span class="file-name" id="d-pqcimg"></span></div></div>
<div class="form-item"><label id="lb-d-prange">资格证有效期</label><div class="ctrl"><input class="input" id="d-pstart" placeholder="起 yyyy-MM-dd" style="margin-bottom:4px;"><input class="input" id="d-pend" placeholder="止 yyyy-MM-dd 或 长期"></div></div>
</div>
<div class="hint">非必填字段按 xxyb 平台实际配置填写(接口文档注意事项);证件有效期为长期时填"长期"。</div>
</details>
</div>
<div class="dlg-foot">
<button class="btn" onclick="closeDialog('mask-driver')">取 消</button>
<button class="btn primary" id="driver-ok" onclick="saveDriver()">确 定</button>
</div>
</div>
</div>
<!-- 新增/查看 车辆 -->
<div class="mask" id="mask-vehicle">
<div class="dialog">
<div class="dlg-head"><span id="vehicle-dlg-title">新增车辆</span><span class="close" onclick="closeDialog('mask-vehicle')">×</span></div>
<div class="dlg-body">
<div class="section-title">车辆基础信息</div>
<div class="form-grid">
<div class="form-item"><label class="req" id="lb-v-plate">车牌号</label><div class="ctrl"><input class="input" id="v-plate" placeholder="如 云A12345"></div></div>
<div class="form-item"><label class="req" id="lb-v-color">车牌颜色</label><div class="ctrl"><select class="select" id="v-color"><option value="">请选择</option></select></div></div>
</div>
<div class="section-title" style="margin-top:10px;">车辆认证信息</div>
<div class="form-grid">
<div class="form-item"><label class="req" id="lb-v-type">车型</label><div class="ctrl"><select class="select" id="v-type"><option value="">请选择</option></select></div></div>
<div class="form-item"><label class="req" id="lb-v-length">车长(米)</label><div class="ctrl"><input class="input" id="v-length" placeholder="如 9.6"></div></div>
<div class="form-item"><label class="req" id="lb-v-energy">能源类型</label><div class="ctrl"><select class="select" id="v-energy"><option value="">请选择</option></select></div></div>
<div class="form-item"><label id="lb-v-vin">车辆识别代码</label><div class="ctrl"><input class="input" id="v-vin"></div></div>
<div class="form-item"><label class="req" id="lb-v-lfront">行驶证正面</label><div class="ctrl"><span class="upload-btn" onclick="mockFile('v-lfront')">点击上传</span><span class="file-name" id="v-lfront"></span></div></div>
<div class="form-item"><label class="req" id="lb-v-lback">行驶证反面</label><div class="ctrl"><span class="upload-btn" onclick="mockFile('v-lback')">点击上传</span><span class="file-name" id="v-lback"></span></div></div>
<div class="form-item"><label class="req" id="lb-v-lexp">行驶证有效期</label><div class="ctrl"><input class="input" id="v-lexp" placeholder="yyyy-MM-dd 或 长期"></div></div>
<div class="form-item"><label id="lb-v-rtl">道路运输证</label><div class="ctrl"><input class="input" id="v-rtl"></div></div>
<div class="form-item"><label id="lb-v-rbl">道路运输经营许可证</label><div class="ctrl"><input class="input" id="v-rbl"></div></div>
<div class="form-item"><label id="lb-v-owner">车辆所有人</label><div class="ctrl"><input class="input" id="v-owner"></div></div>
</div>
<div class="hint">其余字段(行驶证副页/注册日期/发证机关、道路运输证年检与有效期、挂车信息、总质量/核定载质量/整备质量、外廓尺寸、使用性质、挂靠协议等)见接口文档,正式表单全量保留。</div>
</div>
<div class="dlg-foot">
<button class="btn" onclick="closeDialog('mask-vehicle')">取 消</button>
<button class="btn primary" id="vehicle-ok" onclick="saveVehicle()">确 定</button>
</div>
</div>
</div>
<!-- 新增/查看 收款信息 -->
<div class="mask" id="mask-payee">
<div class="dialog" style="width:620px;">
<div class="dlg-head"><span id="payee-dlg-title">新增收款信息</span><span class="close" onclick="closeDialog('mask-payee')">×</span></div>
<div class="dlg-body">
<div class="form-grid">
<div class="form-item"><label class="req" id="lb-p-type">收款人类型</label><div class="ctrl"><select class="select" id="p-type" onchange="payeeTypeChange()"><option value="">请选择</option><option value="PERSONAL">个人</option><option value="ENTERPRISE">企业</option></select></div></div>
<div class="form-item"><label class="req" id="lb-p-name">收款人姓名</label><div class="ctrl"><input class="input" id="p-name"></div></div>
<div class="form-item"><label class="req" id="lb-p-idcard">身份证号</label><div class="ctrl"><input class="input" id="p-idcard" maxlength="18"></div></div>
<div class="form-item"><label class="req" id="lb-p-bankcard">收款银行卡号</label><div class="ctrl"><input class="input" id="p-bankcard"></div></div>
<div class="form-item"><label id="lb-p-bankimg">银行卡图片</label><div class="ctrl"><span class="upload-btn" onclick="mockFile('p-bankimg')">点击上传</span><span class="file-name" id="p-bankimg"></span></div></div>
<div class="form-item"><label id="lb-p-bankname">收款银行</label><div class="ctrl"><input class="input" id="p-bankname" placeholder="如 中国工商银行"></div></div>
<div class="form-item"><label id="lb-p-phone">收款手机号</label><div class="ctrl"><input class="input" id="p-phone" maxlength="11"><div class="hint" id="p-phone-hint" style="display:none;">收款人类型为个人时必填</div></div></div>
</div>
</div>
<div class="dlg-foot">
<button class="btn" onclick="closeDialog('mask-payee')">取 消</button>
<button class="btn primary" id="payee-ok" onclick="savePayee()">确 定</button>
</div>
</div>
</div>
<div id="toast-wrap"></div>
<script>
/* ============ 枚举(来源:xxyb Code.html) ============ */
const TAX = ['云南运八数据科技有限公司','富源运八数据科技有限公司','曲靖市马龙区呈钢加油站有限公司'];
const STATUS = { WAITING:['待审核','orange'], SUCCESS:['审核通过','green'], FAILURE:['审核不通过','red'] };
const COLORS = [['BLUE','蓝色'],['YELLOW','黄色'],['GREEN','绿色']];
const ETYPES = [['A','汽油'],['B','柴油'],['C','电'],['D','混合油'],['E','天然气'],['F','液化石油气'],['L','甲醇'],['M','乙醇'],['N','太阳能'],['O','混合动力'],['Y','无'],['Z','其他']];
const VTYPES = ['重型半挂牵引车','重型自卸货车','重型仓栅式半挂车','重型普通货车','重型厢式货车','重型箱式货车','轻型仓栅式货车','轻型厢式货车','轻型普通货车','中型自卸货车','小型轿车','小型面包车'];
const DLTYPES = ['A1','A2','A3','B1','B2','B3','C1','C2','C3','A1A2','A2B2','B1B2','B2C1','C1C2'];
const TODAY = '2026-09-28';
let drivers = [
{ uid:'DRV20260927001', tax:0, status:'SUCCESS', auditTime:'2026-09-27 13:39:39', name:'杜锦兵', idCard:'532129198803050731', expire:'2040-12-07', phone:'13800002001' },
{ uid:'DRV20260927002', tax:0, status:'SUCCESS', auditTime:'2026-09-27 10:23:25', name:'郑国金', idCard:'513426198312224017', expire:'2046-06-11', phone:'13800002002' },
{ uid:'DRV20260926003', tax:0, status:'SUCCESS', auditTime:'2026-09-26 21:01:50', name:'龚稳全', idCard:'530325198410111111', expire:'2044-04-12', phone:'13800002003' },
{ uid:'DRV20260926004', tax:1, status:'WAITING', auditTime:'', name:'张必亮', idCard:'532323198509110716', expire:'2037-03-08', phone:'13800002004' },
{ uid:'DRV20260926005', tax:0, status:'FAILURE', auditTime:'2026-09-26 16:07:16', name:'张林', idCard:'532527198812072018', expire:'2036-09-12', phone:'13800002005' },
{ uid:'DRV20260926006', tax:0, status:'SUCCESS', auditTime:'2026-09-26 12:00:04', name:'郭锐', idCard:'21142119831102043X', expire:'2031-05-13', phone:'13800002006' }
];
let vehicles = [
{ uid:'VEH20260927001', tax:0, status:'SUCCESS', auditTime:'2026-09-27 14:38:09', plate:'云A06361D', owner:'安宁驰诺商贸有限公司', vtype:'重型半挂牵引车', color:'GREEN', energy:'C' },
{ uid:'VEH20260927002', tax:0, status:'SUCCESS', auditTime:'2026-09-26 23:51:58', plate:'川D76975', owner:'攀枝花市虹樾贸易有限公司', vtype:'重型半挂牵引车', color:'YELLOW', energy:'E' },
{ uid:'VEH20260926003', tax:0, status:'SUCCESS', auditTime:'2026-09-26 21:13:58', plate:'云D07951D', owner:'南雄建信物流有限公司', vtype:'重型半挂牵引车', color:'GREEN', energy:'C' },
{ uid:'VEH20260926004', tax:0, status:'WAITING', auditTime:'', plate:'云G07125D', owner:'弥勒市浩源物流有限公司', vtype:'重型半挂牵引车', color:'GREEN', energy:'C' },
{ uid:'VEH20260926005', tax:1, status:'SUCCESS', auditTime:'2026-09-26 13:49:41', plate:'云AD6641', owner:'李桂花', vtype:'重型半挂牵引车', color:'YELLOW', energy:'E' },
{ uid:'VEH20260926006', tax:0, status:'FAILURE', auditTime:'2026-09-26 08:24:02', plate:'云AJ9383', owner:'普发美', vtype:'重型自卸货车', color:'YELLOW', energy:'B' }
];
let payees = [
{ uid:'PAY20260928001', name:'杜锦兵', idCard:'532129198803050731', bankCard:'6222021001123456789', bankName:'中国工商银行', phone:'13800002001', type:'PERSONAL', createdAt:'2026-09-28 09:12:00' },
{ uid:'PAY20260928002', name:'郑国金', idCard:'513426198312224017', bankCard:'6228480402564890018', bankName:'中国农业银行', phone:'13800002002', type:'PERSONAL', createdAt:'2026-09-28 09:15:40' },
{ uid:'PAY20260928003', name:'云南顺达物流有限公司', idCard:'91530100MA6N5X2P7A', bankCard:'110901234567890012', bankName:'浦发银行', phone:'', type:'ENTERPRISE', createdAt:'2026-09-28 09:20:11' },
{ uid:'PAY20260928004', name:'龚稳全', idCard:'530325198410111111', bankCard:'6217000010012345678', bankName:'中国建设银行', phone:'13800002003', type:'PERSONAL', createdAt:'2026-09-28 10:02:33' },
{ uid:'PAY20260928005', name:'郭锐', idCard:'21142119831102043X', bankCard:'6216610101234567890', bankName:'中国银行', phone:'13800002006', type:'PERSONAL', createdAt:'2026-09-28 10:31:07' }
];
let driverIdx = -1, vehicleIdx = -1, payeeIdx = -1;
let seqD = 7, seqV = 7, seqP = 6;
/* ============ 工具 ============ */
function esc(x){ return String(x==null?'':x).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function taxOf(i){ return TAX[i] || '-'; }
function statusTag(k){ const st = STATUS[k]||['未知','gray']; return '<span class="tag '+st[1]+'">'+st[0]+'</span>'; }
function mockFile(id){ document.getElementById(id).textContent = 'IMG_' + Math.floor(Math.random()*9000+1000) + '.jpg'; }
function toast(msg, type){
const t = document.createElement('div');
t.className = 'toast ' + (type||'success');
t.textContent = msg;
document.getElementById('toast-wrap').appendChild(t);
setTimeout(()=>t.remove(), 2600);
}
function closeDialog(id){ document.getElementById(id).classList.remove('open'); }
function toggleMenu(id){ document.getElementById(id).classList.toggle('open'); }
function todo(){ toast('原型未实现该菜单','warning'); }
function val(id){ const el = document.getElementById(id); return el ? el.value.trim() : ''; }
function fileType(id){ const el = document.getElementById(id); return el ? el.textContent.trim() : ''; }
const CRUMBS = {
home: ['首页'],
driver: ['首页','小象开票','创建司机'],
vehicle: ['首页','小象开票','创建车辆'],
payee: ['首页','小象开票','新增收款信息']
};
function go(page){
document.querySelectorAll('.page').forEach(x=>x.classList.remove('active'));
document.getElementById('page-'+page).classList.add('active');
document.querySelectorAll('.chip').forEach(c=>c.classList.toggle('active', c.dataset.page===page));
document.getElementById('crumb').innerHTML = CRUMBS[page].map((x,i)=>
i===CRUMBS[page].length-1 ? '<b>'+x+'</b>' : x).join('<span class="sep">/</span>');
if (page==='driver') renderDrivers();
if (page==='vehicle') renderVehicles();
if (page==='payee') renderPayees();
if (page==='home') renderStats();
window.scrollTo(0,0);
}
function renderStats(){
document.getElementById('st-d').textContent = drivers.length;
document.getElementById('st-v').textContent = vehicles.length;
document.getElementById('st-p').textContent = payees.length;
document.getElementById('st-wait').textContent =
drivers.filter(d=>d.status==='WAITING').length + vehicles.filter(v=>v.status==='WAITING').length;
}
/* ============ 创建司机 ============ */
function renderDrivers(){
const tax = val('df-tax'), st = val('df-status');
const d1 = val('df-d1'), d2 = val('df-d2');
const name = val('df-name'), idcard = val('df-idcard');
const rows = drivers.map((d,i)=>({d,i})).filter(({d})=>
(!tax || String(d.tax)===tax) && (!st || d.status===st) &&
(!name || d.name.includes(name)) && (!idcard || d.idCard.includes(idcard)) &&
(!d1 || (d.auditTime && d.auditTime.slice(0,10) >= d1)) &&
(!d2 || (d.auditTime && d.auditTime.slice(0,10) <= d2)));
const tb = document.getElementById('driver-tbody');
tb.innerHTML = rows.length ? rows.map(({d,i})=>`<tr>
<td><input type="checkbox" class="ck-d"></td>
<td>${esc(taxOf(d.tax))}</td>
<td>${statusTag(d.status)}</td>
<td>${esc(d.auditTime||'-')}</td>
<td>${esc(d.name)}</td>
<td>${esc(d.idCard)}</td>
<td>${esc(d.expire||'-')}</td>
<td>${esc(d.phone)}</td>
<td><span class="link" onclick="openDriverDlg(${i})">查看</span>${d.status==='FAILURE'?'<span class="link" onclick="toast(\'演示:已重新上传并提交(Q3)\',\'success\')">重新上传</span>':''}</td>
</tr>`).join('') : '<tr><td colspan="9" class="empty">未找到符合条件的司机</td></tr>';
document.getElementById('driver-pg').textContent = '共 '+rows.length+' 条记录';
}
function openDriverDlg(i){
driverIdx = i;
document.getElementById('driver-dlg-title').textContent = i<0 ? '新增司机' : '司机详情(只读)';
const d = i<0 ? null : drivers[i];
['d-phone','d-name','d-idcard','d-addr'].forEach(k=>{ const el=document.getElementById(k); el.value = d?(d[k.slice(2)]||''):''; el.readOnly = !!d; });
['d-front','d-back','d-lfront','d-lback','d-pqcimg'].forEach(k=>{ document.getElementById(k).textContent=''; });
['d-license','d-authority','d-lstart','d-lend','d-pqc','d-pstart','d-pend'].forEach(k=>{ const el=document.getElementById(k); el.value=''; el.readOnly = !!d; });
document.getElementById('d-dltype').value = '';
document.getElementById('driver-ok').style.display = i<0 ? '' : 'none';
document.getElementById('mask-driver').classList.add('open');
}
function saveDriver(){
const phone = val('d-phone'), name = val('d-name'), idcard = val('d-idcard');
if (!/^1\d{10}$/.test(phone)){ toast('请输入正确的11位司机手机号','error'); return; }
if (!name){ toast('请输入司机姓名','error'); return; }
if (!/^\d{17}[\dXx]$/.test(idcard)){ toast('请输入正确的18位身份证号','error'); return; }
const uid = 'DRV' + TODAY.replace(/-/g,'') + String(seqD++).padStart(3,'0');
drivers.unshift({ uid, tax:0, status:'WAITING', auditTime:'', name, idCard:idcard, expire: val('d-lend') || '-', phone });
closeDialog('mask-driver');
renderDrivers();
toast('创建成功:司机ID ' + uid + '(已提交 xxyb,待审核)','success');
}
/* ============ 创建车辆 ============ */
function renderVehicles(){
const tax = val('vf-tax'), st = val('vf-status');
const d1 = val('vf-d1'), d2 = val('vf-d2');
const plate = val('vf-plate'), owner = val('vf-owner');
const vt = val('vf-type'), color = val('vf-color'), energy = val('vf-energy');
const rows = vehicles.map((v,i)=>({v,i})).filter(({v})=>
(!tax || String(v.tax)===tax) && (!st || v.status===st) &&
(!plate || v.plate.includes(plate)) && (!owner || v.owner.includes(owner)) &&
(!vt || v.vtype===vt) && (!color || v.color===color) && (!energy || v.energy===energy) &&
(!d1 || (v.auditTime && v.auditTime.slice(0,10) >= d1)) &&
(!d2 || (v.auditTime && v.auditTime.slice(0,10) <= d2)));
const tb = document.getElementById('vehicle-tbody');
tb.innerHTML = rows.length ? rows.map(({v,i})=>`<tr>
<td><input type="checkbox" class="ck-v"></td>
<td>${esc(taxOf(v.tax))}</td>
<td>${statusTag(v.status)}</td>
<td>${esc(v.auditTime||'-')}</td>
<td>${esc(v.plate)}</td>
<td>${esc(v.owner||'-')}</td>
<td>${esc(v.vtype)}</td>
<td>${esc(colorName(v.color))}</td>
<td>${esc(energyName(v.energy))}</td>
<td><span class="link" onclick="openVehicleDlg(${i})">查看</span>${v.status==='FAILURE'?'<span class="link" onclick="toast(\'演示:已重新上传并提交(Q3)\',\'success\')">重新上传</span>':''}</td>
</tr>`).join('') : '<tr><td colspan="10" class="empty">未找到符合条件的车辆</td></tr>';
document.getElementById('vehicle-pg').textContent = '共 '+rows.length+' 条记录';
}
function colorName(k){ const c = COLORS.find(x=>x[0]===k); return c ? c[1] : (k||'-'); }
function energyName(k){ const c = ETYPES.find(x=>x[0]===k); return c ? c[1] : (k||'-'); }
function openVehicleDlg(i){
vehicleIdx = i;
document.getElementById('vehicle-dlg-title').textContent = i<0 ? '新增车辆' : '车辆详情(只读)';
const v = i<0 ? null : vehicles[i];
['v-plate','v-length','v-vin','v-lexp','v-rtl','v-rbl','v-owner'].forEach(k=>{ const el=document.getElementById(k); el.value = v?(v[k.slice(2)]||''):''; el.readOnly = !!v; });
['v-lfront','v-lback'].forEach(k=>{ document.getElementById(k).textContent=''; });
document.getElementById('v-color').value = v ? v.color : '';
document.getElementById('v-type').value = v ? v.vtype : '';
document.getElementById('v-energy').value = v ? v.energy : '';
document.getElementById('vehicle-ok').style.display = i<0 ? '' : 'none';
document.getElementById('mask-vehicle').classList.add('open');
}
function saveVehicle(){
const plate = val('v-plate'), color = val('v-color'), vtype = val('v-type');
const length = val('v-length'), energy = val('v-energy'), lexp = val('v-lexp');
if (!/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,5}[挂学警港澳]?$/.test(plate)){
toast('请输入正确的车牌号','error'); return;
}
if (!color){ toast('请选择车牌颜色','error'); return; }
if (!vtype){ toast('请选择车型','error'); return; }
if (!/^\d+(\.\d+)?$/.test(length)){ toast('请输入正确的车长(米)','error'); return; }
if (!energy){ toast('请选择能源类型','error'); return; }
if (!lexp){ toast('请填写行驶证有效期(yyyy-MM-dd 或 长期)','error'); return; }
if (!fileType('v-lfront')){ toast('请上传行驶证正面图片','error'); return; }
if (!fileType('v-lback')){ toast('请上传行驶证反面图片','error'); return; }
const uid = 'VEH' + TODAY.replace(/-/g,'') + String(seqV++).padStart(3,'0');
vehicles.unshift({ uid, tax:0, status:'WAITING', auditTime:'', plate, owner: val('v-owner'),
vtype, color, energy });
closeDialog('mask-vehicle');
renderVehicles();
toast('创建成功:车辆ID ' + uid + '(已提交 xxyb,待审核)','success');
}
/* ============ 新增收款信息 ============ */
function renderPayees(){
const name = val('pf-name'), idcard = val('pf-idcard'), type = val('pf-type');
const rows = payees.map((x,i)=>({x,i})).filter(({x})=>
(!name || x.name.includes(name)) && (!idcard || x.idCard.includes(idcard)) && (!type || x.type===type));
const tb = document.getElementById('payee-tbody');
tb.innerHTML = rows.length ? rows.map(({x,i})=>`<tr>
<td><input type="checkbox" class="ck-p"></td>
<td>${esc(x.name)}</td>
<td>${esc(x.idCard)}</td>
<td>${esc(x.bankCard)}</td>
<td>${esc(x.bankName||'-')}</td>
<td>${esc(x.phone||'-')}</td>
<td><span class="tag ${x.type==='PERSONAL'?'blue':'green'}">${x.type==='PERSONAL'?'个人':'企业'}</span></td>
<td><span class="tip">${esc(x.uid)}</span> <span class="link" onclick="openPayeeDlg(${i})">查看</span></td>
</tr>`).join('') : '<tr><td colspan="8" class="empty">未找到符合条件的收款信息</td></tr>';
document.getElementById('payee-pg').textContent = '共 '+rows.length+' 条记录';
}
function openPayeeDlg(i){
payeeIdx = i;
document.getElementById('payee-dlg-title').textContent = i<0 ? '新增收款信息' : '收款信息详情(只读)';
const x = i<0 ? null : payees[i];
['p-name','p-idcard','p-bankcard','p-bankname','p-phone'].forEach(k=>{ const el=document.getElementById(k); el.value = x?(x[k.slice(2)]||''):''; el.readOnly = !!x; });
document.getElementById('p-bankimg').textContent = '';
document.getElementById('p-type').value = x ? x.type : '';
document.getElementById('payee-ok').style.display = i<0 ? '' : 'none';
payeeTypeChange();
document.getElementById('mask-payee').classList.add('open');
}
function payeeTypeChange(){
const personal = document.getElementById('p-type').value === 'PERSONAL';
document.getElementById('lb-p-phone').className = personal ? 'req' : '';
document.getElementById('p-phone-hint').style.display = personal ? '' : 'none';
}
function savePayee(){
const type = val('p-type'), name = val('p-name'), idcard = val('p-idcard');
const bankCard = val('p-bankcard'), phone = val('p-phone');
if (!type){ toast('请选择收款人类型','error'); return; }
if (!name){ toast('请输入收款人姓名','error'); return; }
const idOk = type==='PERSONAL' ? /^\d{17}[\dXx]$/.test(idcard) : /^[\dA-HJ-NPQRTUWXY]{18}$/.test(idcard);
if (!idOk){ toast(type==='PERSONAL' ? '请输入正确的18位身份证号' : '请输入正确的18位统一社会信用代码','error'); return; }
if (!/^\d{15,19}$/.test(bankCard)){ toast('请输入正确的银行卡号(15-19位数字)','error'); return; }
if (type==='PERSONAL' && !/^1\d{10}$/.test(phone)){ toast('收款人类型为个人时,收款手机号必填(11位)','error'); return; }
const uid = 'PAY' + TODAY.replace(/-/g,'') + String(seqP++).padStart(3,'0');
payees.unshift({ uid, name, idCard:idcard, bankCard, bankName: val('p-bankname'), phone,
type, createdAt: TODAY + ' ' + new Date().toTimeString().slice(0,8) });
closeDialog('mask-payee');
renderPayees();
toast('创建成功:收款人ID ' + uid + '(已提交 xxyb)','success');
}
/* ============ 初始化 ============ */
(function init(){
document.querySelectorAll('#df-tax,#vf-tax').forEach(sel=>{
sel.innerHTML = '<option value="">请选择税源地</option>' + TAX.map((t,i)=>'<option value="'+i+'">'+esc(t)+'</option>').join('');
});
document.getElementById('vf-type').innerHTML = '<option value="">请选择车辆分类</option>' + VTYPES.map(v=>'<option>'+esc(v)+'</option>').join('');
document.getElementById('vf-color').innerHTML = '<option value="">请选择车牌颜色</option>' + COLORS.map(c=>'<option value="'+c[0]+'">'+c[1]+'</option>').join('');
document.getElementById('vf-energy').innerHTML = '<option value="">请选择能源类型</option>' + ETYPES.map(c=>'<option value="'+c[0]+'">'+c[1]+'</option>').join('');
document.getElementById('v-type').innerHTML = '<option value="">请选择</option>' + VTYPES.map(v=>'<option>'+esc(v)+'</option>').join('');
document.getElementById('v-color').innerHTML = '<option value="">请选择</option>' + COLORS.map(c=>'<option value="'+c[0]+'">'+c[1]+'</option>').join('');
document.getElementById('v-energy').innerHTML = '<option value="">请选择</option>' + ETYPES.map(c=>'<option value="'+c[0]+'">'+c[1]+'</option>').join('');
document.getElementById('d-dltype').innerHTML = '<option value="">请选择</option>' + DLTYPES.map(v=>'<option>'+v+'</option>').join('');
go('home');
})();
</script>
</body>
</html>
@@ -0,0 +1,272 @@
# 小象开票 —— 收款方创建需求文档(创建司机 / 创建车辆 / 新增收款信息)
> **版本**: v1.0(初稿)
> **编写日期**: 2026-09-28
> **状态**: 待评审
> **接口文档(权威来源)**:
> - 创建司机:https://v4.xxyb.cn/markdown/link/proxy/DriverCreate.html
> - 创建车辆:https://v4.xxyb.cn/markdown/link/proxy/VehicleCreate.html
> - 新增收款信息:https://v4.xxyb.cn/markdown/link/proxy/PayeeCreate.html
> - 公共枚举:https://v4.xxyb.cn/markdown/link/base/Code.html
> **交互原型**: [prototypes/小象开票原型.html](prototypes/小象开票原型.html)(风格与"物流公司管理"原型一致,数据为演示数据)
> **UI 参考**: 三十六匠发票"司机上传 / 车辆上传"页面截图(表头筛选行 + 批量上传按钮样式)
> **菜单位置**: 小象开票(顶级菜单)下新增:创建司机、创建车辆、新增收款信息
---
## 1. 背景与目标
小象开票(联合开票)业务要求承运方(司机、车辆)及其收款账户先在 xxyb 平台完成建档,才能开票。当前建档在小象平台侧手工操作,与运八系统脱节。本次在小象开票菜单下新增三个功能,直接调用 xxyb 开放接口完成建档:
1. **创建司机**:录入司机信息 → 调用 `createDriver` 在 xxyb 平台创建司机档案;
2. **创建车辆**:录入车辆信息 → 调用 `createVehicle` 创建车辆档案;
3. **新增收款信息**:录入收款人账户 → 调用 `createPayee` 创建收款信息。
建档结果(第三方返回 ID、审核状态)在列表中可见、可追溯。
### 1.1 设计原则
- 三个功能均为**记录/建档性质**:调用成功即在本系统留存记录并展示第三方审核状态,**不阻断**运单、开票等任何既有流程(是否允许开票由小象平台侧控制)。
- 接口文档为字段权威来源,本文档字段表与其一致;页面展示字段为全量字段的子集(必填 + 高频选填),其余字段按接口文档保留扩展。
---
## 2. 名词
| 名词 | 说明 |
|:---|:---|
| xxyb 平台 | 联合开票合作方(小象),接口基地址 `https://www.xxyb.cn/union/api/link/` |
| 税源地 | 开票主体(如云南运八数据科技有限公司),建档时随单传入 |
| 审核状态 | xxyb 平台对建档资料的审核结果:待审核 / 审核通过 / 审核不通过(AuditStatusEnum) |
| 长期 | 证件有效期无固定截止日期时,有效期字段填写字符串"长期" |
---
## 3. 角色与权限
| 角色 | 权限 |
|:---|:---|
| 开票运营人员 | 三个页面的新增、查看、批量上传 |
| 其他角色 | 列表只读(按需授权) |
- 数据按机构隔离(沿用平台多机构规则);操作记行为日志。
- **Header 鉴权**:`token`(应用令牌)、`cid`(合作ID),由平台配置统一注入,前端不接触。
---
## 4. 功能清单
| 编号 | 功能 | 说明 | 优先级 |
|:---|:---|:---|:---:|
| FR-01 | 创建司机 | 司机列表 + 新增司机弹窗,提交调用 `createDriver` | P0 |
| FR-02 | 创建车辆 | 车辆列表 + 新增车辆弹窗,提交调用 `createVehicle` | P0 |
| FR-03 | 新增收款信息 | 收款信息列表 + 新增弹窗,提交调用 `createPayee` | P0 |
| FR-04 | 审核状态展示 | 列表展示 xxyb 审核状态与审核时间 | P0 |
| FR-05 | 批量上传 | 司机身份证批量上传、车牌号批量上传(参照三十六匠页面;范围见 Q1) | P2 |
---
## 5. 接口规范(权威来源:xxyb 接口文档)
### 5.0 公共
- 请求方式:POST(JSON);Header:`token`(应用令牌,必填)、`cid`(合作ID,必填)。
- 返回结构:`{ code, msg, data, requestId }`;`code=200` 成功;成功后 `data.id` 为第三方建档 ID,需落库保存。
- 图片/附件类字段均为 **URL**:文件先上传平台文件服务(OBS)取得 URL 后再随接口提交。
### 5.1 创建司机 `POST /union/api/link/createDriver`
| 字段 | 类型 | 层级 | 说明 | 必填 |
|:---|:---|:---:|:---|:---:|
| driverPhone | String | 1 | 司机手机号 | 是 |
| driverRealAuthDomain | ApiDriverRealAuthDomain | 1 | 司机基础信息 | 是 |
| driverAuthDomain | ApiDriverAuthDomain | 1 | 司机认证信息 | 否 |
**ApiDriverRealAuthDomain 司机实名认证信息**:
| 字段 | 类型 | 说明 | 必填 |
|:---|:---|:---|:---:|
| driverName | String | 司机姓名 | 是 |
| driverIdCard | String | 身份证 | 是 |
| driverIdCardAddress | String | 身份证地址 | 否 |
| idCardFrontImg | String | 身份证人像面图片 | 否 |
| idCardBackImg | String | 身份证国徽面图片 | 否 |
**ApiDriverAuthDomain 司机认证信息(全部选填)**:
| 字段 | 类型 | 说明 |
|:---|:---|:---|
| driverLicense | String | 驾驶证 |
| driverLicenseFrontImg / BackImg | String | 驾驶证正/反面图片 |
| driverLicenseIssuingAuthority | String | 驾驶证发证机关 |
| driverLicenseExpireStartTime / EndTime | String | 驾驶证有效期起/止(长期填"长期") |
| professionalQualificationCertificate | String | 从业资格证 |
| professionalQualificationCertificateImg | String | 从业资格证图片 |
| professionalQualificationCertificateExpireStartTime / EndTime | String | 从业资格证有效期起/止 |
| approvedDrivingType | ApprovedDrivingTypeEnum | 准驾车型(A1/A2/A1A2/B2/C1 等组合值,见 Code.html) |
返回:`data.id` = 司机ID。
### 5.2 创建车辆 `POST /union/api/link/createVehicle`
| 字段 | 类型 | 层级 | 说明 | 必填 |
|:---|:---|:---:|:---|:---:|
| vehicleBasicDomain | ApiVehicleBasicDomain | 1 | 车辆基础信息 | 是 |
| vehicleAuthDomain | ApiVehicleAuthDomain | 1 | 车辆认证信息 | 否 |
**ApiVehicleBasicDomain 车辆基础信息**:
| 字段 | 类型 | 说明 | 必填 |
|:---|:---|:---|:---:|
| licensePlateNumber | String | 车辆牌号 | 是 |
| licensePlateColor | ColorEnum | 车牌颜色(BLUE 蓝 / YELLOW 黄 / GREEN 绿) | 是 |
**ApiVehicleAuthDomain 车辆认证信息**(必填:vehicleType、vehicleLength、vehicleEnergyType、vehicleLicenseFrontImg、vehicleLicenseBackImg、vehicleLicenseExpireTime;其余选填):
| 字段 | 类型 | 说明 |
|:---|:---|:---|
| vehicleType | VehicleTypeEnum | 车型(重型半挂牵引车/重型自卸货车/轻型厢式货车等,见 Code.html) |
| vehicleLength | BigDecimal | 车长 |
| vehicleEnergyType | VehicleEnergyTypeEnum | 能源类型(A汽油/B柴油/C电/E天然气/O混合动力/Y无/Z其他等) |
| vehicleIdentificationNumber | String | 车辆识别代码 |
| vehicleLicense | String | 行驶证 |
| vehicleLicenseFrontImg / BackImg / ClipImg | String | 行驶证正面/反面/副页图片 |
| vehicleLicenseRegisterTime / ExpireTime | String | 行驶证注册日期/有效期 |
| vehicleLicenseIssuingAuthority / IssuingTime | String | 行驶证发证机关/发证日期 |
| roadTransportLicense + Img + AnnualInspectionImg + ExpireTime | String | 道路运输证系列 |
| roadTransportBusinessLicense | String | 道路运输经营许可证 |
| trailerLicenseNumber + Img + BackImg | String | 挂车牌照/副本照片 |
| groupPhotoImg | String | 人车合照 |
| totalMass / approvedLoad / curbWeight | BigDecimal | 总质量/核定载质量/整备质量 |
| overallDimensions / useNature | String | 外廓尺寸/使用性质 |
| creditCode | String | 统一社会信用代码/身份证号 |
| vehicleOwner / vehicleOwnerExpireTime | String | 车辆所有人/有效期 |
| affiliationAgreement | List&lt;String&gt; | 挂靠协议(多附件 URL) |
返回:`data.id` = 车辆ID。
### 5.3 新增收款信息 `POST /union/api/link/createPayee`
| 字段 | 类型 | 层级 | 说明 | 必填 |
|:---|:---|:---:|:---|:---:|
| payeeBasicDomain | ApiPayeeBasicDomain | 1 | 收款信息 | 是 |
**ApiPayeeBasicDomain 收款人基础信息**:
| 字段 | 类型 | 说明 | 必填 | 备注 |
|:---|:---|:---|:---:|:---|
| payeeName | String | 收款人姓名 | 是 | |
| payeeIdCard | String | 收款人身份证 | 是 | |
| payeeBankCard | String | 收款银行卡号 | 是 | |
| payeeBankCardImg | String | 收款银行卡图片 | 否 | |
| payeeBankName | String | 收款银行 | 否 | |
| payeePhone | String | 收款手机号 | 条件 | accountType 为 PERSONAL 时必填 |
| accountType | AccountTypeEnum | 收款人类型 | 是 | PERSONAL 个人 / ENTERPRISE 企业 |
返回:`data.id` = 收款人ID。
---
## 6. 业务规则
### BR-01 必填与格式校验(前端 + 后端双重)
- 手机号 11 位;身份证 18 位国标格式;银行卡号按 Luhn 校验 + 位数 15-19 位。
- 收款人类型 = 个人时,`payeePhone` 必填(接口条件必填,前端联动校验)。
- 车牌号格式校验(省份简称+字母+5/6位,含挂车"挂"字尾);车辆长度数值 ≥ 0。
### BR-02 证件有效期"长期"口径
- 有效期截止类字段(驾驶证/资格证/行驶证/道路运输证/车辆所有人有效期)支持选择"长期",提交时填写字符串 `长期`;起止格式 `yyyy-MM-dd`。
### BR-03 附件
- 页面上传证件图片 → 平台文件服务(OBS)→ 取 URL 随接口提交;单图 ≤ 10MB,jpg/jpeg/png/pdf(挂靠协议支持多文件)。
- 上传失败时阻断提交并提示。
### BR-04 提交与结果落库
- 提交成功:保存 `data.id`(第三方ID)+ 提交内容 + 税源地 + 审核状态=待审核;提示"创建成功"。
- 提交失败:展示接口 `msg`;表单不清空。
- **重复创建**:同一身份证号(司机)/同一车牌号(车辆)在"待审核或审核通过"状态下重复创建时,前端提示确认;后端是否阻断以 xxyb 平台校验为准(见 Q4)。
### BR-05 审核状态同步
- 列表展示审核状态(待审核/审核通过/审核不通过)与审核时间,来源于 xxyb 平台。
- 同步方式见待确认 Q2(回调 / 定时轮询),状态变化仅做展示更新,不影响本系统流程。
### BR-06 批量上传
- 参照三十六匠页面提供"批量上传 / 司机身份证批量上传 / 车牌号批量上传"入口(Excel 模板导入,逐条调接口);范围见 Q1。
---
## 7. 页面详述
### 7.1 创建司机(小象开票 → 创建司机)
- **按钮**:查询、新增司机、批量上传、司机身份证批量上传(后两者见 Q1)。
- **表头筛选行**(参照截图样式,无标签、placeholder 直书):请选择税源地 ▼ / 请选择审核状态 ▼ / 审核时间起-止 / 请输入司机 / 请输入身份证号。
- **列表列**:选择框、税源地、审核状态(待审核-橙 / 审核通过-绿 / 审核不通过-红)、审核时间、司机姓名、身份证号、身份证有效期(驾驶证有效期止)、手机号、操作(查看、重新上传——仅审核不通过时显示)。
- **新增司机弹窗**(字段名对应 5.1):
- 基础信息:司机手机号*、司机姓名*、身份证号*、身份证地址、身份证人像面图片、身份证国徽面图片;
- 认证信息(选填,默认折叠):驾驶证号、驾驶证正/反面图片、发证机关、驾驶证有效期起/止(支持"长期")、从业资格证、资格证图片、资格证有效期起/止、准驾车型(下拉)。
- 提交成功后列表新增一行:审核状态"待审核",第三方ID落库。
### 7.2 创建车辆(小象开票 → 创建车辆)
- **按钮**:查询、新增车辆、批量上传、车牌号批量上传。
- **表头筛选行**:请选择税源地 ▼ / 请选择审核状态 ▼ / 审核时间起-止 / 请输入车牌 / 请输入车辆所有人 / 请选择车辆分类 ▼ / 请选择车牌颜色 ▼ / 请选择能源类型 ▼。
- **列表列**:税源地、审核状态、审核时间、车牌号、车辆所有人、车辆分类(=vehicleType)、车牌颜色、车辆能源类型、操作(查看、重新上传)。
- **新增车辆弹窗**:
- 基础信息:车牌号*、车牌颜色*(蓝/黄/绿);
- 认证信息:车型*(下拉,VehicleTypeEnum)、车长*(米)、能源类型*(下拉)、车辆识别代码、行驶证正面图片*、行驶证反面图片*、行驶证有效期*(支持"长期")、道路运输证、道路运输经营许可证、车辆所有人;
- 其余字段(挂车、质量参数、外廓尺寸、挂靠协议等)按接口文档保留输入(弹窗"更多信息"折叠区),原型中省略展示。
### 7.3 新增收款信息(小象开票 → 新增收款信息)
- **按钮**:查询、新增收款信息、导出。
- **表头筛选行**:请输入收款人姓名 / 请输入身份证号 / 请选择收款人类型 ▼。
- **列表列**:收款人姓名、身份证号、收款银行卡号、收款银行、收款手机号、收款人类型(个人/企业)、第三方ID、创建时间、操作(查看)。
- **新增收款信息弹窗**:收款人类型*(个人/企业,切换联动)、收款人姓名*、身份证号*、收款银行卡号*、银行卡图片、收款银行、收款手机号(**个人必填**联动校验)。
---
## 8. 待确认问题
| 编号 | 问题 | 倾向方案 |
|:---:|:---|:---|
| Q1 | 批量上传是否本期实现(Excel 模板 + 逐条调接口)? | 建议本期先做单笔,批量二期 |
| Q2 | 审核状态如何同步:xxyb 回调(Callback)还是我方定时轮询? | 待与平台确认接口 |
| Q3 | 审核不通过后的"重新上传"是调编辑接口重新提交还是新建一条?接口文档未见编辑接口 | 待平台确认;倾向新建一条并作废旧记录 |
| Q4 | 重复创建(同身份证/同车牌)xxyb 侧是否幂等/报错? | 以平台校验为准,我方先做提示 |
| Q5 | 菜单项命名:创建司机 / 创建车辆 / 新增收款信息(与接口同名)还是 司机管理 / 车辆管理 / 收款信息? | 待产品确认 |
| Q6 | 税源地字段接口未见入参——是否随 `cid` 隐含?列表中税源地列取值来源? | 待与平台确认 |
> ⚠️ 待确认 Q6 补充:接口文档的请求参数中**没有税源地字段**,但参照页面(三十六匠)列表均有"税源地"列。需确认税源地是否由 `cid`(合作ID)隐含绑定,或需要额外参数。
> ⚠️ 待确认 Q7:**企业收款人(accountType=ENTERPRISE)的 payeeIdCard 字段填什么?** 接口字段名为"收款人身份证",企业场景下应填法定代表人身份证还是统一社会信用代码?原型当前按"企业填统一社会信用代码、个人填身份证"处理。
---
## 9. 验收要点(概要)
1. 三个接口的 Header(token/cid)由平台配置注入,前端无感知;请求体字段与接口文档一致(逐字段核对)。
2. 必填校验生效:司机(手机号/姓名/身份证)、车辆(车牌/颜色/车型/车长/能源类型/行驶证正反/有效期)、收款信息(类型/姓名/身份证/银行卡号;个人时手机号必填)。
3. 有效期选择"长期"时提交值为字符串"长期";日期字段格式 yyyy-MM-dd。
4. 提交成功落库 `data.id` 并以"待审核"入列表;失败展示 `msg` 且表单保留。
5. 收款人类型=企业时手机号非必填、个人时必填(联动)。
6. 车牌颜色/车型/能源类型/准驾车型/收款人类型取值与 Code.html 枚举一致。
7. 列表筛选(表头筛选行)与分页正常;审核状态展示正确。
8. 权限:非授权角色只读;跨机构数据不可见。
---
## 10. 原型对照
| 原型页面 | 说明 |
|:---|:---|
| 创建司机(列表 + 新增弹窗) | 对标三十六匠"司机上传"截图:表头筛选行 + 批量上传按钮 |
| 创建车辆(列表 + 新增弹窗) | 对标"车辆上传"截图:车辆分类/车牌颜色/能源类型列 |
| 新增收款信息(列表 + 新增弹窗) | 新增设计,列表含收款人类型 |
| 侧边栏 | 小象开票菜单下新增三个菜单项(NEW 标识) |