This commit is contained in:
2026-08-22 09:50:50 +08:00
parent 984eb641ff
commit e8cb9e1588
35 changed files with 2937 additions and 271 deletions
+1 -1
View File
@@ -123,7 +123,7 @@ node tests/verify-prototype-modules.cjs
| 客户服务 | `prototype-src/modules/04a-customer-service.html` | `outbound-exception`(异常订单) | | 客户服务 | `prototype-src/modules/04a-customer-service.html` | `outbound-exception`(异常订单) |
| 供应商中心 | `prototype-src/modules/05-suppliers.html` | 供应商列表/编辑、快递公司、往来单位、海运渠道 | | 供应商中心 | `prototype-src/modules/05-suppliers.html` | 供应商列表/编辑、快递公司、往来单位、海运渠道 |
| 货柜管理 | `prototype-src/modules/06-containers.html` | 订舱、拖车、货柜、装柜清单、预装清单、装柜看板 | | 货柜管理 | `prototype-src/modules/06-containers.html` | 订舱、拖车、货柜、装柜清单、预装清单、装柜看板 |
| 海外业务 | `prototype-src/modules/07-overseas-business.html` | 采购订单、本土卡车、本土散板及新增/编辑/详情 | | 海外业务 | `prototype-src/modules/07-overseas-business.html` | 采购订单、本土卡车及详情、本土散板及新增/编辑/详情 |
| 财务管理 | `prototype-src/modules/08-finance.html` | 客户账户、应收应付、对账单、结算单、费用分析、利润表及历史财务报表页 | | 财务管理 | `prototype-src/modules/08-finance.html` | 客户账户、应收应付、对账单、结算单、费用分析、利润表及历史财务报表页 |
| 销售管理 | `prototype-src/modules/09-sales.html` | 拜访记录新增/详情、销售提成 | | 销售管理 | `prototype-src/modules/09-sales.html` | 拜访记录新增/详情、销售提成 |
| 通用资料 | `prototype-src/modules/10-general.html` | 产品、费用项目、汇率管理、仓库(`gen-owned-warehouse`、`gen-fba`、`gen-partner-warehouse`)、下载、船期价格、车辆及详情、派送车队报价表、历史库位/托盘/路线页 | | 通用资料 | `prototype-src/modules/10-general.html` | 产品、费用项目、汇率管理、仓库(`gen-owned-warehouse`、`gen-fba`、`gen-partner-warehouse`)、下载、船期价格、车辆及详情、派送车队报价表、历史库位/托盘/路线页 |
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -13,6 +13,7 @@
"outbound-bol", "outbound-bol",
"outbound-selfpick", "outbound-selfpick",
"outbound-exception", "outbound-exception",
"customer-followup",
"gen-product", "gen-product",
"gen-fee", "gen-fee",
"gen-exchange-rate", "gen-exchange-rate",
@@ -20,6 +21,7 @@
"gen-owned-warehouse", "gen-owned-warehouse",
"gen-fba", "gen-fba",
"gen-partner-warehouse", "gen-partner-warehouse",
"gen-private-warehouse",
"gen-overseas-quote", "gen-overseas-quote",
"gen-download", "gen-download",
"gen-pallet", "gen-pallet",
@@ -177,6 +179,7 @@
"legacy-purchase-detail", "legacy-purchase-detail",
"overseas-looseboard", "overseas-looseboard",
"overseas-looseboard-add", "overseas-looseboard-add",
"overseas-truck-detail",
"overseas-truck-edit", "overseas-truck-edit",
"overseas-looseboard-edit" "overseas-looseboard-edit"
], ],
@@ -273,7 +276,8 @@
"file": "modules/04a-customer-service.html", "file": "modules/04a-customer-service.html",
"title": "客户服务", "title": "客户服务",
"pages": [ "pages": [
"outbound-exception" "outbound-exception",
"customer-followup"
] ]
}, },
{ {
@@ -318,6 +322,7 @@
"legacy-purchase-detail", "legacy-purchase-detail",
"overseas-looseboard", "overseas-looseboard",
"overseas-looseboard-add", "overseas-looseboard-add",
"overseas-truck-detail",
"overseas-truck-edit", "overseas-truck-edit",
"overseas-looseboard-edit" "overseas-looseboard-edit"
] ]
@@ -378,6 +383,7 @@
"gen-owned-warehouse", "gen-owned-warehouse",
"gen-fba", "gen-fba",
"gen-partner-warehouse", "gen-partner-warehouse",
"gen-private-warehouse",
"gen-overseas-quote", "gen-overseas-quote",
"gen-download", "gen-download",
"gen-pallet", "gen-pallet",
@@ -40,6 +40,7 @@
<button class="btn btn-primary">查询</button> <button class="btn btn-primary">查询</button>
<button class="btn btn-primary">重置</button> <button class="btn btn-primary">重置</button>
<button class="btn btn-default">收起 ▲</button> <button class="btn btn-default">收起 ▲</button>
<button class="btn btn-default" onclick="openDomesticInboundBatchTransfer()">批量转仓</button>
<button class="btn btn-primary" onclick="mergeCustomsDeclaration()" style="margin-left:auto;background:#fa8c16;border-color:#fa8c16">🛃 合关报关</button> <button class="btn btn-primary" onclick="mergeCustomsDeclaration()" style="margin-left:auto;background:#fa8c16;border-color:#fa8c16">🛃 合关报关</button>
<button class="btn btn-primary" onclick="showContainerCalcModal()" style="background:#52c41a;border-color:#52c41a">📦 智能装柜</button> <button class="btn btn-primary" onclick="showContainerCalcModal()" style="background:#52c41a;border-color:#52c41a">📦 智能装柜</button>
</div> </div>
@@ -109,11 +110,11 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr data-inbound-order-type="快递订单"> <tr data-inbound-order-type="快递订单" data-inbound-code="EXP260814001" data-inbound-status="已入库">
<td><input type="checkbox"/></td> <td><input type="checkbox"/></td>
<td><a href="javascript:void(0)" onclick="openDomesticInboundDetail('EXP260814001')">详情</a> | <div class="more-dropdown"><span class="more-dropdown-btn" onclick="toggleMoreDropdown(this)">更多 ∨</span><div class="more-dropdown-menu"><a href="javascript:void(0)" class="more-dropdown-item" onclick="openDomesticInboundFeeModal('courier','EXP260814001',this)">填写快递费</a><a href="javascript:void(0)" class="more-dropdown-item" onclick="showToast('备注','info')">备注</a></div></div></td> <td><a href="javascript:void(0)" onclick="openDomesticInboundDetail('EXP260814001')">详情</a> | <div class="more-dropdown"><span class="more-dropdown-btn" onclick="toggleMoreDropdown(this)">更多 ∨</span><div class="more-dropdown-menu"><a href="javascript:void(0)" class="more-dropdown-item" onclick="openDomesticInboundFeeModal('courier','EXP260814001',this)">填写快递费</a><a href="javascript:void(0)" class="more-dropdown-item" onclick="openDomesticInboundTransfer(this)">转仓</a><a href="javascript:void(0)" class="more-dropdown-item" onclick="showToast('备注','info')">备注</a></div></div></td>
<td>EXP260814001</td> <td>EXP260814001</td>
<td><span class="tag tag-green">审核通过</span></td> <td><span class="tag tag-green">已入库</span></td>
<td><span class="tag tag-green">审核通过</span></td> <td><span class="tag tag-green">审核通过</span></td>
<td>2026-08-14</td> <td>2026-08-14</td>
<td data-inbound-fee-status="EXP260814001"><span class="tag tag-gray">未录入费用</span></td> <td data-inbound-fee-status="EXP260814001"><span class="tag tag-gray">未录入费用</span></td>
@@ -164,6 +165,7 @@
<button class="btn btn-primary">查询</button> <button class="btn btn-primary">查询</button>
<button class="btn btn-primary">重置</button> <button class="btn btn-primary">重置</button>
<button class="btn btn-default">收起 ▲</button> <button class="btn btn-default">收起 ▲</button>
<button class="btn btn-default" onclick="openDomesticInboundBatchTransfer()" style="margin-left:auto">批量转仓</button>
</div> </div>
</div> </div>
<div class="inbound-stats-bar"> <div class="inbound-stats-bar">
@@ -276,6 +278,7 @@
<button class="btn btn-primary">查询</button> <button class="btn btn-primary">查询</button>
<button class="btn btn-primary">重置</button> <button class="btn btn-primary">重置</button>
<button class="btn btn-default">收起 ▲</button> <button class="btn btn-default">收起 ▲</button>
<button class="btn btn-default" onclick="openDomesticInboundBatchTransfer()" style="margin-left:auto">批量转仓</button>
</div> </div>
</div> </div>
<div class="inbound-stats-bar"> <div class="inbound-stats-bar">
@@ -388,6 +391,7 @@
<button class="btn btn-primary">查询</button> <button class="btn btn-primary">查询</button>
<button class="btn btn-primary">重置</button> <button class="btn btn-primary">重置</button>
<button class="btn btn-default">收起 ▲</button> <button class="btn btn-default">收起 ▲</button>
<button class="btn btn-default" onclick="openDomesticInboundBatchTransfer()" style="margin-left:auto">批量转仓</button>
</div> </div>
</div> </div>
<div class="inbound-stats-bar"> <div class="inbound-stats-bar">
@@ -500,6 +504,7 @@
<button class="btn btn-primary">查询</button> <button class="btn btn-primary">查询</button>
<button class="btn btn-primary">重置</button> <button class="btn btn-primary">重置</button>
<button class="btn btn-default">收起 ▲</button> <button class="btn btn-default">收起 ▲</button>
<button class="btn btn-default" onclick="openDomesticInboundBatchTransfer()" style="margin-left:auto">批量转仓</button>
</div> </div>
</div> </div>
<div class="inbound-stats-bar"> <div class="inbound-stats-bar">
@@ -612,6 +617,7 @@
<button class="btn btn-primary">查询</button> <button class="btn btn-primary">查询</button>
<button class="btn btn-primary">重置</button> <button class="btn btn-primary">重置</button>
<button class="btn btn-default">收起 ▲</button> <button class="btn btn-default">收起 ▲</button>
<button class="btn btn-default" onclick="openDomesticInboundBatchTransfer()" style="margin-left:auto">批量转仓</button>
</div> </div>
</div> </div>
<div class="inbound-stats-bar"> <div class="inbound-stats-bar">
@@ -229,7 +229,7 @@
<button class="btn btn-sm btn-default" onclick="obInReset()">重置</button> <button class="btn btn-sm btn-default" onclick="obInReset()">重置</button>
</div> </div>
<div style="overflow-x:auto"> <div style="overflow-x:auto">
<table style="min-width:1180px;font-size:12px"><thead><tr style="background:#fafafa;color:#666"> <table style="min-width:1260px;font-size:12px"><thead><tr style="background:#fafafa;color:#666">
<th style="padding:9px 10px;text-align:left;white-space:nowrap">入库单号</th> <th style="padding:9px 10px;text-align:left;white-space:nowrap">入库单号</th>
<th style="padding:9px 10px;text-align:left;white-space:nowrap">海外仓</th> <th style="padding:9px 10px;text-align:left;white-space:nowrap">海外仓</th>
<th style="padding:9px 10px;text-align:left;white-space:nowrap">货主</th> <th style="padding:9px 10px;text-align:left;white-space:nowrap">货主</th>
@@ -332,7 +332,7 @@
<button class="btn btn-sm btn-default" onclick="rlReset()">重置</button> <button class="btn btn-sm btn-default" onclick="rlReset()">重置</button>
</div> </div>
<div style="overflow-x:auto"> <div style="overflow-x:auto">
<table style="min-width:1180px;font-size:12px"><thead><tr style="background:#fafafa;color:#666"> <table style="min-width:1340px;font-size:12px"><thead><tr style="background:#fafafa;color:#666">
<th style="padding:9px 10px;text-align:left;white-space:nowrap">商品SKU</th> <th style="padding:9px 10px;text-align:left;white-space:nowrap">商品SKU</th>
<th style="padding:9px 10px;text-align:center;white-space:nowrap">商品图片</th> <th style="padding:9px 10px;text-align:center;white-space:nowrap">商品图片</th>
<th style="padding:9px 10px;text-align:left;white-space:nowrap">中文品名</th> <th style="padding:9px 10px;text-align:left;white-space:nowrap">中文品名</th>
@@ -485,6 +485,7 @@
.fbio-edit-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px 20px} .fbio-edit-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px 20px}
.fbio-edit-grid .form-group{min-width:0;margin:0} .fbio-edit-grid .form-group{min-width:0;margin:0}
.fbio-edit-grid .form-group input,.fbio-edit-grid .form-group select{width:100%;box-sizing:border-box} .fbio-edit-grid .form-group input,.fbio-edit-grid .form-group select{width:100%;box-sizing:border-box}
.fbio-edit-grid .lmo-checkbox-option input{width:16px;flex:0 0 16px}
.fbio-readonly-input{background:#f5f7fa!important;color:#64748b!important;cursor:not-allowed} .fbio-readonly-input{background:#f5f7fa!important;color:#64748b!important;cursor:not-allowed}
@media(max-width:720px){#page-fba-inbound-order .fbio-list-actions{align-items:flex-start;flex-direction:column;padding:10px 14px}#page-fba-inbound-order .fbio-list-action-buttons{width:100%;justify-content:flex-start}.fbio-edit-grid{grid-template-columns:1fr}} @media(max-width:720px){#page-fba-inbound-order .fbio-list-actions{align-items:flex-start;flex-direction:column;padding:10px 14px}#page-fba-inbound-order .fbio-list-action-buttons{width:100%;justify-content:flex-start}.fbio-edit-grid{grid-template-columns:1fr}}
</style> </style>
@@ -515,6 +516,9 @@
<th style="padding:9px 10px;text-align:right;white-space:nowrap">总箱数</th> <th style="padding:9px 10px;text-align:right;white-space:nowrap">总箱数</th>
<th style="padding:9px 10px;text-align:right;white-space:nowrap">计费体积(m³)</th> <th style="padding:9px 10px;text-align:right;white-space:nowrap">计费体积(m³)</th>
<th style="padding:9px 10px;text-align:right;white-space:nowrap">总托盘数</th> <th style="padding:9px 10px;text-align:right;white-space:nowrap">总托盘数</th>
<th style="padding:9px 10px;text-align:left;white-space:nowrap">托盘类型</th>
<th style="padding:9px 10px;text-align:center;white-space:nowrap">是否外配</th>
<th style="padding:9px 10px;text-align:center;white-space:nowrap">是否加急</th>
<th style="padding:9px 10px;text-align:center;white-space:nowrap">状态</th> <th style="padding:9px 10px;text-align:center;white-space:nowrap">状态</th>
<th style="padding:9px 10px;text-align:center;white-space:nowrap">FBA预约</th> <th style="padding:9px 10px;text-align:center;white-space:nowrap">FBA预约</th>
<th style="padding:9px 10px;text-align:center;white-space:nowrap">排车状态</th> <th style="padding:9px 10px;text-align:center;white-space:nowrap">排车状态</th>
@@ -524,7 +528,7 @@
<div class="pagination"><span id="fbio-total">共 0 条</span><button class="page-btn active">1</button></div> <div class="pagination"><span id="fbio-total">共 0 条</span><button class="page-btn active">1</button></div>
</div></div> </div></div>
<div class="modal-overlay" id="modal-fbio-appointment"><div class="modal" style="max-width:720px"><div class="modal-header"><span>预约</span><span onclick="hideModal('modal-fbio-appointment')" style="cursor:pointer;font-size:20px">&times;</span></div><div class="modal-body"><div id="fbio-appointment-summary" class="lmo-modal-note">请选择入库单</div><div class="form-row"><div class="form-group"><label>预约号 *</label><input id="fbio-appointment-no" placeholder="请输入预约号"></div><div class="form-group"><label>预约送仓时间 *</label><input id="fbio-appointment-date" type="datetime-local" step="60"></div></div><div class="form-row"><div class="form-group"><label>预计装车时间 *</label><input id="fbio-appointment-loading-time" type="datetime-local" step="60"></div><div class="form-group"><label>预约状态 *</label><select id="fbio-appointment-status"><option>预约成功</option><option>预约中</option></select></div></div><div class="form-row"><div class="form-group" style="flex:1"><label>预约备注</label><input id="fbio-appointment-remark" placeholder="请输入预约备注"></div></div></div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-fbio-appointment')">取消</button><button class="btn btn-primary" onclick="saveFbioBatchAppointment()">保存预约</button></div></div></div> <div class="modal-overlay" id="modal-fbio-appointment"><div class="modal" style="max-width:720px"><div class="modal-header"><span>预约</span><span onclick="hideModal('modal-fbio-appointment')" style="cursor:pointer;font-size:20px">&times;</span></div><div class="modal-body"><div id="fbio-appointment-summary" class="lmo-modal-note">请选择入库单</div><div class="form-row"><div class="form-group"><label>预约号 *</label><input id="fbio-appointment-no" placeholder="请输入预约号"></div><div class="form-group"><label>预约送仓时间 *</label><input id="fbio-appointment-date" type="datetime-local" step="60"></div></div><div class="form-row"><div class="form-group"><label>预计装车时间 *</label><input id="fbio-appointment-loading-time" type="datetime-local" step="60"></div><div class="form-group"><label>预约状态 *</label><select id="fbio-appointment-status"><option>预约成功</option><option>预约中</option></select></div></div><div class="form-row"><div class="form-group" style="flex:1"><label>预约备注</label><input id="fbio-appointment-remark" placeholder="请输入预约备注"></div></div></div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-fbio-appointment')">取消</button><button class="btn btn-primary" onclick="saveFbioBatchAppointment()">保存预约</button></div></div></div>
<div class="modal-overlay" id="modal-fbio-edit"><div class="modal" style="max-width:880px;width:94%"><div class="modal-header"><span>编辑入库单</span><span onclick="hideModal('modal-fbio-edit')" style="cursor:pointer;font-size:20px">&times;</span></div><div class="modal-body"><div class="fbio-edit-grid"><div class="form-group"><label>入库单号</label><input id="fbio-edit-inbound-no" class="fbio-readonly-input" readonly aria-readonly="true"></div><div class="form-group"><label>入仓码 *</label><input id="fbio-edit-warehouse-code" maxlength="64" placeholder="请输入入仓码"></div><div class="form-group"><label>类型 *</label><select id="fbio-edit-type"><option>国内订单</option><option>海外订单</option></select></div><div class="form-group"><label>海外仓 *</label><select id="fbio-edit-wh"><option>洛杉矶长滩海外仓</option><option>纽约海外仓</option><option>汉堡海外仓</option></select></div><div class="form-group"><label>FBA 仓库 *</label><select id="fbio-edit-fba-warehouse"><option>FBA ONT8</option><option>FBA ABE8</option><option>DE Frankfurt</option><option>FBA LGB8</option><option>FBA JFK8</option></select></div><div class="form-group"><label>货主 *</label><input id="fbio-edit-owner" maxlength="100" placeholder="请输入货主"></div><div class="form-group"><label>总箱数 *</label><input id="fbio-edit-total-boxes" type="number" min="0" step="1"></div><div class="form-group"><label>计费体积(m³) *</label><input id="fbio-edit-charge-vol" type="number" min="0" step="0.01"></div><div class="form-group"><label>总托盘数 *</label><input id="fbio-edit-pallet-total" type="number" min="0" step="1"></div><div class="form-group"><label>状态 *</label><select id="fbio-edit-status"><option>待审核</option><option>已审核</option><option>已入库</option></select></div><div class="form-group"><label>预约送仓时间</label><input id="fbio-edit-appointment-date" type="datetime-local" step="60"></div><div class="form-group"><label>预计装车时间</label><input id="fbio-edit-loading-time" type="datetime-local" step="60"></div></div></div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-fbio-edit')">取消</button><button class="btn btn-primary" onclick="saveFbioEdit()">保存</button></div></div></div> <div class="modal-overlay" id="modal-fbio-edit"><div class="modal" style="max-width:880px;width:94%"><div class="modal-header"><span>编辑入库单</span><span onclick="hideModal('modal-fbio-edit')" style="cursor:pointer;font-size:20px">&times;</span></div><div class="modal-body"><div class="fbio-edit-grid"><div class="form-group"><label>入库单号</label><input id="fbio-edit-inbound-no" class="fbio-readonly-input" readonly aria-readonly="true"></div><div class="form-group"><label>入仓码 *</label><input id="fbio-edit-warehouse-code" maxlength="64" placeholder="请输入入仓码"></div><div class="form-group"><label>类型 *</label><select id="fbio-edit-type"><option>国内订单</option><option>海外订单</option></select></div><div class="form-group"><label>海外仓 *</label><select id="fbio-edit-wh"><option>洛杉矶长滩海外仓</option><option>纽约海外仓</option><option>汉堡海外仓</option></select></div><div class="form-group"><label>FBA 仓库 *</label><select id="fbio-edit-fba-warehouse"><option>FBA ONT8</option><option>FBA ABE8</option><option>DE Frankfurt</option><option>FBA LGB8</option><option>FBA JFK8</option></select></div><div class="form-group"><label>货主 *</label><input id="fbio-edit-owner" maxlength="100" placeholder="请输入货主"></div><div class="form-group"><label>总箱数 *</label><input id="fbio-edit-total-boxes" type="number" min="0" step="1"></div><div class="form-group"><label>计费体积(m³) *</label><input id="fbio-edit-charge-vol" type="number" min="0" step="0.01"></div><div class="form-group"><label>总托盘数 *</label><input id="fbio-edit-pallet-total" type="number" min="0" step="1"></div><div class="form-group"><label for="fbio-edit-pallet-type">托盘类型</label><select id="fbio-edit-pallet-type"><option value="标准托盘">标准托盘</option><option value="木托盘">木托盘</option><option value="纸托盘">纸托盘</option></select></div><div class="form-group"><label for="fbio-edit-outsourced">是否外配</label><label class="lmo-checkbox-option"><input id="fbio-edit-outsourced" type="checkbox"> 是</label></div><div class="form-group"><label for="fbio-edit-urgent">是否加急</label><label class="lmo-checkbox-option"><input id="fbio-edit-urgent" type="checkbox"> 是</label></div><div class="form-group"><label>状态 *</label><select id="fbio-edit-status"><option>待审核</option><option>已审核</option><option>已入库</option></select></div><div class="form-group"><label>预约送仓时间</label><input id="fbio-edit-appointment-date" type="datetime-local" step="60"></div><div class="form-group"><label>预计装车时间</label><input id="fbio-edit-loading-time" type="datetime-local" step="60"></div></div></div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-fbio-edit')">取消</button><button class="btn btn-primary" onclick="saveFbioEdit()">保存</button></div></div></div>
<div class="modal-overlay" id="modal-fbio-schedule"><div class="modal" style="max-width:720px"><div class="modal-header"><span>批量排车</span><span onclick="hideModal('modal-fbio-schedule')" style="cursor:pointer;font-size:20px">&times;</span></div><div class="modal-body"><div id="fbio-schedule-summary" class="lmo-modal-note">排车前必须完成 FBA 预约</div><div class="form-row"><div class="form-group"><label>车次号 *</label><input id="fbio-schedule-vehicle-no" maxlength="64" placeholder="请输入车次号"></div><div class="form-group"><label>车牌号 *</label><input id="fbio-schedule-plate" placeholder="请输入车牌号"></div></div><div class="form-row"><div class="form-group"><label>司机</label><input id="fbio-schedule-driver" placeholder="请输入司机姓名"></div><div class="form-group"><label>计划发车时间 *</label><input id="fbio-schedule-depart" type="datetime-local"></div></div></div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-fbio-schedule')">取消</button><button class="btn btn-primary" onclick="saveFbioBatchSchedule()">保存排车</button></div></div></div> <div class="modal-overlay" id="modal-fbio-schedule"><div class="modal" style="max-width:720px"><div class="modal-header"><span>批量排车</span><span onclick="hideModal('modal-fbio-schedule')" style="cursor:pointer;font-size:20px">&times;</span></div><div class="modal-body"><div id="fbio-schedule-summary" class="lmo-modal-note">排车前必须完成 FBA 预约</div><div class="form-row"><div class="form-group"><label>车次号 *</label><input id="fbio-schedule-vehicle-no" maxlength="64" placeholder="请输入车次号"></div><div class="form-group"><label>车牌号 *</label><input id="fbio-schedule-plate" placeholder="请输入车牌号"></div></div><div class="form-row"><div class="form-group"><label>司机</label><input id="fbio-schedule-driver" placeholder="请输入司机姓名"></div><div class="form-group"><label>计划发车时间 *</label><input id="fbio-schedule-depart" type="datetime-local"></div></div></div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-fbio-schedule')">取消</button><button class="btn btn-primary" onclick="saveFbioBatchSchedule()">保存排车</button></div></div></div>
<!-- ==================== FBA RETURN - 换标单 ==================== --> <!-- ==================== FBA RETURN - 换标单 ==================== -->
@@ -560,7 +564,7 @@
<div class="form-row"><div class="form-group"><label>箱数 *</label><input id="fbro-form-boxes" type="number" min="1" step="1" placeholder="请输入箱数"></div><div class="form-group"><label>预算总价(USD) *</label><input id="fbro-form-budget-total" type="number" min="0" step="0.01" placeholder="请输入预算总价"></div></div> <div class="form-row"><div class="form-group"><label>箱数 *</label><input id="fbro-form-boxes" type="number" min="1" step="1" placeholder="请输入箱数"></div><div class="form-group"><label>预算总价(USD) *</label><input id="fbro-form-budget-total" type="number" min="0" step="0.01" placeholder="请输入预算总价"></div></div>
<div class="form-row"><div class="form-group"><label>状态</label><select id="fbro-form-status"><option>待处理</option><option>处理中</option><option>已完成</option></select></div><div class="form-group"></div></div> <div class="form-row"><div class="form-group"><label>状态</label><select id="fbro-form-status"><option>待处理</option><option>处理中</option><option>已完成</option></select></div><div class="form-group"></div></div>
<div class="section-title">作业要求</div> <div class="section-title">作业要求</div>
<div class="form-row"><div class="form-group"><label>贴标要求 *</label><select id="fbro-form-label-req"><option value="">请选择贴标要求</option><option>全覆盖换标</option><option>加贴新标</option><option>移除原标+换标</option></select></div><div class="form-group"><label>上传文件</label><input id="fbro-form-file" type="file" accept=".pdf,.jpg,.jpeg,.png,.xls,.xlsx"></div></div> <div class="form-row"><div class="form-group"><label>换标类型 *</label><select id="fbro-form-label-req"><option value="">请选择换标类型</option><option>换箱唛</option><option>换SKU标</option><option>换箱唛和SKU标</option></select></div><div class="form-group"><label>上传文件</label><input id="fbro-form-file" type="file" accept=".pdf,.jpg,.jpeg,.png,.xls,.xlsx"></div></div>
<div class="form-row" id="fbro-form-cost-row" style="display:none"><div class="form-group"><label>预算报价(USD)</label><input id="fbro-form-relabel-cost" type="number" min="0" step="0.01" placeholder="请输入预算报价"></div><div class="form-group"></div></div> <div class="form-row" id="fbro-form-cost-row" style="display:none"><div class="form-group"><label>预算报价(USD)</label><input id="fbro-form-relabel-cost" type="number" min="0" step="0.01" placeholder="请输入预算报价"></div><div class="form-group"></div></div>
<div class="form-group"><label>备注</label><textarea id="fbro-form-remark" rows="3" maxlength="500" placeholder="请输入备注"></textarea></div> <div class="form-group"><label>备注</label><textarea id="fbro-form-remark" rows="3" maxlength="500" placeholder="请输入备注"></textarea></div>
</div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-fba-relabel-form')">取消</button><button class="btn btn-primary" id="fbro-form-submit" onclick="saveFbaRelabelOrder()">保存换标单</button></div></div></div> </div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-fba-relabel-form')">取消</button><button class="btn btn-primary" id="fbro-form-submit" onclick="saveFbaRelabelOrder()">保存换标单</button></div></div></div>
@@ -859,9 +863,11 @@ FORM: 采用现有后台的列表加标准详情结构,保持高信息密度
.lmo-detail-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin-top:14px}.lmo-section{background:#fff;border:1px solid #e5eaf1;border-radius:8px;overflow:hidden}.lmo-section.wide{grid-column:1/-1}.lmo-section-title{min-height:44px;padding:12px 16px;border-bottom:1px solid #e8edf3;box-sizing:border-box;font-size:14px;font-weight:700;color:#1f2937}.lmo-info-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));padding:4px 16px 12px}.lmo-info-item{display:grid;grid-template-columns:118px minmax(0,1fr);gap:10px;padding:10px 4px;border-bottom:1px solid #f0f2f5;font-size:13px}.lmo-info-item .label{color:#8491a3}.lmo-info-item .value{color:#273548;font-weight:500;word-break:break-word}.lmo-empty{padding:28px;text-align:center;color:#94a3b8;font-size:13px} .lmo-detail-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin-top:14px}.lmo-section{background:#fff;border:1px solid #e5eaf1;border-radius:8px;overflow:hidden}.lmo-section.wide{grid-column:1/-1}.lmo-section-title{min-height:44px;padding:12px 16px;border-bottom:1px solid #e8edf3;box-sizing:border-box;font-size:14px;font-weight:700;color:#1f2937}.lmo-info-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));padding:4px 16px 12px}.lmo-info-item{display:grid;grid-template-columns:118px minmax(0,1fr);gap:10px;padding:10px 4px;border-bottom:1px solid #f0f2f5;font-size:13px}.lmo-info-item .label{color:#8491a3}.lmo-info-item .value{color:#273548;font-weight:500;word-break:break-word}.lmo-empty{padding:28px;text-align:center;color:#94a3b8;font-size:13px}
.lmo-departure-fee-head{display:flex;align-items:center;justify-content:space-between;gap:10px}.lmo-departure-fee-hint{font-size:12px;font-weight:400;color:#8491a3}.lmo-departure-fee-scroll{overflow-x:auto}.lmo-departure-fee-table{width:100%;min-width:620px;border-collapse:collapse;font-size:12px}.lmo-departure-fee-table th{padding:10px 12px;background:#f8fafc;border-bottom:1px solid #e8edf3;color:#526071;font-weight:650;text-align:left;white-space:nowrap}.lmo-departure-fee-table td{padding:10px 12px;border-bottom:1px solid #f0f2f5;color:#334155;white-space:nowrap;vertical-align:middle}.lmo-departure-fee-table tbody tr:last-child td{border-bottom:0}.lmo-departure-fee-table .supplier-cell{color:#475569}.lmo-departure-fee-table .amount-cell{text-align:right;color:#1f5fbf;font-weight:700}.lmo-departure-fee-table tfoot td{border-top:1px solid #dce3ec;border-bottom:0;background:#f8fafc;font-weight:700}.lmo-departure-fee-table .total-label{text-align:right;color:#526071}.lmo-departure-fee-table .total-amount{color:#111827}.lmo-departure-fee-empty{padding:28px!important;text-align:center;color:#94a3b8!important} .lmo-departure-fee-head{display:flex;align-items:center;justify-content:space-between;gap:10px}.lmo-departure-fee-hint{font-size:12px;font-weight:400;color:#8491a3}.lmo-departure-fee-scroll{overflow-x:auto}.lmo-departure-fee-table{width:100%;min-width:620px;border-collapse:collapse;font-size:12px}.lmo-departure-fee-table th{padding:10px 12px;background:#f8fafc;border-bottom:1px solid #e8edf3;color:#526071;font-weight:650;text-align:left;white-space:nowrap}.lmo-departure-fee-table td{padding:10px 12px;border-bottom:1px solid #f0f2f5;color:#334155;white-space:nowrap;vertical-align:middle}.lmo-departure-fee-table tbody tr:last-child td{border-bottom:0}.lmo-departure-fee-table .supplier-cell{color:#475569}.lmo-departure-fee-table .amount-cell{text-align:right;color:#1f5fbf;font-weight:700}.lmo-departure-fee-table tfoot td{border-top:1px solid #dce3ec;border-bottom:0;background:#f8fafc;font-weight:700}.lmo-departure-fee-table .total-label{text-align:right;color:#526071}.lmo-departure-fee-table .total-amount{color:#111827}.lmo-departure-fee-empty{padding:28px!important;text-align:center;color:#94a3b8!important}
.lmo-modal-note{margin-bottom:14px;padding:10px 12px;border:1px solid #dbeafe;border-radius:6px;background:#f7faff;color:#526071;font-size:12px}.lmo-modal-note.warning{border-color:#fde2b8;background:#fffaf0;color:#9a5b13} .lmo-modal-note{margin-bottom:14px;padding:10px 12px;border:1px solid #dbeafe;border-radius:6px;background:#f7faff;color:#526071;font-size:12px}.lmo-modal-note.warning{border-color:#fde2b8;background:#fffaf0;color:#9a5b13}
#modal-lmo-manual-nodes .modal{display:flex;max-height:calc(100vh - 48px);overflow:hidden;flex-direction:column}#modal-lmo-manual-nodes .modal-body{min-height:0;overflow:hidden}#modal-lmo-manual-nodes .modal-header,#modal-lmo-manual-nodes .modal-footer{flex:0 0 auto}.lmo-manual-node-order{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:14px;padding:11px 13px;border:1px solid #e5eaf1;border-radius:6px;background:#f8fafc;color:#64748b;font-size:12px}.lmo-manual-node-order strong{min-width:0;color:#1f2937;font-size:13px;overflow-wrap:anywhere}.lmo-manual-node-wrap{max-height:430px;overflow:auto;border:1px solid #e5eaf1;border-radius:7px}.lmo-manual-node-table{width:100%;min-width:560px;border-collapse:collapse}.lmo-manual-node-table th{position:sticky;top:0;z-index:1;padding:10px 14px;background:#f8fafc;border-bottom:1px solid #e5eaf1;color:#526071;font-size:12px;font-weight:650;text-align:left}.lmo-manual-node-table td{padding:9px 14px;border-bottom:1px solid #edf0f4;color:#334155;font-size:13px}.lmo-manual-node-table tr:last-child td{border-bottom:0}.lmo-manual-node-table input{width:100%;min-width:220px;height:36px;padding:0 10px;border:1px solid #d7dee8;border-radius:6px;background:#fff;color:#273548;font:13px inherit;box-sizing:border-box;outline:none}.lmo-manual-node-table input:focus{border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.1)}
.lmo-checkbox-option{display:flex;align-items:center;gap:8px;min-height:36px;color:#334155;font-size:13px;cursor:pointer}.lmo-checkbox-option input{width:16px;height:16px;margin:0;accent-color:#667eea} .lmo-checkbox-option{display:flex;align-items:center;gap:8px;min-height:36px;color:#334155;font-size:13px;cursor:pointer}.lmo-checkbox-option input{width:16px;height:16px;margin:0;accent-color:#667eea}
@media(max-width:900px){.lmo-detail-grid{grid-template-columns:1fr}.lmo-section.wide{grid-column:auto}.lmo-detail-head{align-items:flex-start;flex-wrap:wrap}.lmo-detail-head .lmo-detail-actions{width:100%;margin-left:0}.lmo-info-grid{grid-template-columns:1fr}.lmo-field-selector-grid{grid-template-columns:repeat(2,minmax(0,1fr))}} @media(max-width:900px){.lmo-detail-grid{grid-template-columns:1fr}.lmo-section.wide{grid-column:auto}.lmo-detail-head{align-items:flex-start;flex-wrap:wrap}.lmo-detail-head .lmo-detail-actions{width:100%;margin-left:0}.lmo-info-grid{grid-template-columns:1fr}.lmo-field-selector-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:600px){#modal-lmo-inbound-plan .modal{width:calc(100% - 24px);max-width:none}.modal-overlay#modal-lmo-inbound-plan .modal-body{padding:20px}.modal-overlay#modal-lmo-inbound-plan .form-row{grid-template-columns:minmax(0,1fr);gap:12px;margin-bottom:12px}.modal-overlay#modal-lmo-inbound-plan .form-group{min-width:0}.modal-overlay#modal-lmo-inbound-plan .modal-footer{padding:12px 16px;flex-wrap:wrap}} @media(max-width:600px){#modal-lmo-inbound-plan .modal{width:calc(100% - 24px);max-width:none}.modal-overlay#modal-lmo-inbound-plan .modal-body{padding:20px}.modal-overlay#modal-lmo-inbound-plan .form-row{grid-template-columns:minmax(0,1fr);gap:12px;margin-bottom:12px}.modal-overlay#modal-lmo-inbound-plan .form-group{min-width:0}.modal-overlay#modal-lmo-inbound-plan .modal-footer{padding:12px 16px;flex-wrap:wrap}}
@media(max-width:600px){#modal-lmo-manual-nodes .modal{width:calc(100% - 24px);max-width:none}#modal-lmo-manual-nodes .modal-body{padding:16px}#modal-lmo-manual-nodes .modal-footer{padding:12px 16px;flex-wrap:wrap}.lmo-manual-node-order{align-items:flex-start;flex-direction:column;gap:4px}.lmo-manual-node-table{min-width:0;table-layout:fixed}.lmo-manual-node-table th:first-child{width:42%!important}.lmo-manual-node-table th,.lmo-manual-node-table td{padding-inline:10px}.lmo-manual-node-table input{min-width:0;padding-inline:7px;font-size:12px}}
@media(max-width:600px){#modal-fba-relabel-form .modal{width:calc(100% - 24px)!important;max-width:none!important}#modal-fba-relabel-form .modal-body{padding:20px}#modal-fba-relabel-form .form-row{grid-template-columns:minmax(0,1fr);gap:12px;margin-bottom:12px}#modal-fba-relabel-form .form-group{min-width:0}#modal-fba-relabel-form .modal-footer{padding:12px 16px;flex-wrap:wrap}} @media(max-width:600px){#modal-fba-relabel-form .modal{width:calc(100% - 24px)!important;max-width:none!important}#modal-fba-relabel-form .modal-body{padding:20px}#modal-fba-relabel-form .form-row{grid-template-columns:minmax(0,1fr);gap:12px;margin-bottom:12px}#modal-fba-relabel-form .form-group{min-width:0}#modal-fba-relabel-form .modal-footer{padding:12px 16px;flex-wrap:wrap}}
</style> </style>
@@ -873,6 +879,8 @@ FORM: 采用现有后台的列表加标准详情结构,保持高信息密度
<div class="lmo-status-tabs" role="tablist" aria-label="尾程订单状态"> <div class="lmo-status-tabs" role="tablist" aria-label="尾程订单状态">
<button type="button" class="lmo-status-tab active" data-lmo-tab="全部" role="tab" aria-selected="true" onclick="switchLastMileStatusTab('全部',this)">全部</button> <button type="button" class="lmo-status-tab active" data-lmo-tab="全部" role="tab" aria-selected="true" onclick="switchLastMileStatusTab('全部',this)">全部</button>
<button type="button" class="lmo-status-tab" data-lmo-tab="待分配" role="tab" aria-selected="false" onclick="switchLastMileStatusTab('待分配',this)">待分配</button> <button type="button" class="lmo-status-tab" data-lmo-tab="待分配" role="tab" aria-selected="false" onclick="switchLastMileStatusTab('待分配',this)">待分配</button>
<button type="button" class="lmo-status-tab" data-lmo-tab="待结算" role="tab" aria-selected="false" onclick="switchLastMileStatusTab('待结算',this)">待结算</button>
<button type="button" class="lmo-status-tab" data-lmo-tab="已完成" role="tab" aria-selected="false" onclick="switchLastMileStatusTab('已完成',this)">已完成</button>
</div> </div>
<div class="search-row lmo-search"> <div class="search-row lmo-search">
<input id="lmo-no" placeholder="尾程单号" style="width:170px"/> <input id="lmo-no" placeholder="尾程单号" style="width:170px"/>
@@ -882,7 +890,7 @@ FORM: 采用现有后台的列表加标准详情结构,保持高信息密度
<select id="lmo-order-type" style="width:140px"><option value="">全部订单类型</option><option>一件代发</option><option>头程</option><option>拒收订单</option><option>快递订单</option></select> <select id="lmo-order-type" style="width:140px"><option value="">全部订单类型</option><option>一件代发</option><option>头程</option><option>拒收订单</option><option>快递订单</option></select>
<input id="lmo-owner" placeholder="货主" style="width:150px"/> <input id="lmo-owner" placeholder="货主" style="width:150px"/>
<input id="lmo-container" placeholder="集装箱号" style="width:160px"/> <input id="lmo-container" placeholder="集装箱号" style="width:160px"/>
<select id="lmo-status" style="width:140px"><option value="">全部状态</option><option>待入库计划</option><option>待清关</option><option>清关完成</option><option>已合托</option><option>待预约</option><option>预约中</option><option>预约成功</option></select> <select id="lmo-status" style="width:140px"><option value="">全部状态</option><option>待入库计划</option><option>待清关</option><option>清关完成</option><option>已合托</option><option>待预约</option><option>预约中</option><option>预约成功</option><option value="待结算">待结算</option><option value="已完成">已完成</option></select>
<select id="lmo-consolidated-filter" style="width:130px"><option value="">是否合托</option><option value="yes">是</option><option value="no">否</option></select> <select id="lmo-consolidated-filter" style="width:130px"><option value="">是否合托</option><option value="yes">是</option><option value="no">否</option></select>
<div class="lmo-search-date-range"><input id="lmo-port-eta-start" type="date" aria-label="预计到港开始时间"/><span>–</span><input id="lmo-port-eta-end" type="date" aria-label="预计到港结束时间"/></div> <div class="lmo-search-date-range"><input id="lmo-port-eta-start" type="date" aria-label="预计到港开始时间"/><span>–</span><input id="lmo-port-eta-end" type="date" aria-label="预计到港结束时间"/></div>
<button class="btn btn-sm btn-primary" onclick="lastMileSearch()">查询</button><button class="btn btn-sm btn-default" onclick="lastMileReset()">重置</button> <button class="btn btn-sm btn-primary" onclick="lastMileSearch()">查询</button><button class="btn btn-sm btn-default" onclick="lastMileReset()">重置</button>
@@ -905,12 +913,16 @@ FORM: 采用现有后台的列表加标准详情结构,保持高信息密度
<section class="lmo-section"><div class="lmo-section-title">订单明细</div><div class="lmo-info-grid" id="lmo-detail-order-details"></div></section> <section class="lmo-section"><div class="lmo-section-title">订单明细</div><div class="lmo-info-grid" id="lmo-detail-order-details"></div></section>
<section class="lmo-section"><div class="lmo-section-title">计划信息</div><div class="lmo-info-grid" id="lmo-detail-plan-info"></div></section> <section class="lmo-section"><div class="lmo-section-title">计划信息</div><div class="lmo-info-grid" id="lmo-detail-plan-info"></div></section>
<section class="lmo-section"><div class="lmo-section-title">关联单据</div><div style="overflow-x:auto"><table><thead><tr><th>单据类型</th><th>单据编号</th><th>生成时间</th><th>报价</th></tr></thead><tbody id="tbody-lmo-detail-documents"></tbody></table></div></section> <section class="lmo-section"><div class="lmo-section-title">关联单据</div><div style="overflow-x:auto"><table><thead><tr><th>单据类型</th><th>单据编号</th><th>生成时间</th><th>报价</th></tr></thead><tbody id="tbody-lmo-detail-documents"></tbody></table></div></section>
<section class="lmo-section"><div class="lmo-section-title">结算信息</div><div class="lmo-info-grid" id="lmo-detail-settlement-info"></div></section>
<section class="lmo-section wide"><div class="lmo-section-title">货物信息</div><div style="overflow-x:auto"><table style="min-width:980px"><thead><tr><th>SKU</th><th>中文品名</th><th style="text-align:right">托数</th><th style="text-align:right">箱数</th><th style="text-align:right">件数</th><th style="text-align:right">重量(kg)</th><th style="text-align:right">体积(m³)</th></tr></thead><tbody id="tbody-lmo-detail-cargo"></tbody></table></div></section> <section class="lmo-section wide"><div class="lmo-section-title">货物信息</div><div style="overflow-x:auto"><table style="min-width:980px"><thead><tr><th>SKU</th><th>中文品名</th><th style="text-align:right">托数</th><th style="text-align:right">箱数</th><th style="text-align:right">件数</th><th style="text-align:right">重量(kg)</th><th style="text-align:right">体积(m³)</th></tr></thead><tbody id="tbody-lmo-detail-cargo"></tbody></table></div></section>
<section class="lmo-section"><div class="lmo-section-title">操作记录</div><div style="overflow-x:auto"><table><thead><tr><th>时间</th><th>操作节点</th><th>操作人</th><th>处理结果</th></tr></thead><tbody id="tbody-lmo-detail-logs"></tbody></table></div></section> <section class="lmo-section"><div class="lmo-section-title">操作记录</div><div style="overflow-x:auto"><table><thead><tr><th>时间</th><th>操作节点</th><th>操作人</th><th>处理结果</th></tr></thead><tbody id="tbody-lmo-detail-logs"></tbody></table></div></section>
</div></div> </div></div>
<!-- 尾程订单 - 人工修改客户端节点 -->
<div class="modal-overlay" id="modal-lmo-manual-nodes" role="dialog" aria-modal="true" aria-labelledby="lmo-manual-nodes-title"><div class="modal" style="max-width:700px"><div class="modal-header"><span id="lmo-manual-nodes-title">人工修改节点</span><button type="button" aria-label="关闭" onclick="hideModal('modal-lmo-manual-nodes')" style="display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:0;border-radius:6px;background:transparent;color:#64748b;font-size:22px;cursor:pointer">&times;</button></div><div class="modal-body"><div class="lmo-manual-node-order"><span>尾程单号</span><strong id="lmo-manual-nodes-order">—</strong></div><div class="lmo-manual-node-wrap"><table class="lmo-manual-node-table"><thead><tr><th style="width:42%">节点</th><th>节点时间</th></tr></thead><tbody id="tbody-lmo-manual-nodes"></tbody></table></div></div><div class="modal-footer"><button type="button" class="btn btn-default" onclick="hideModal('modal-lmo-manual-nodes')">取消</button><button type="button" class="btn btn-primary" onclick="saveLastMileManualNodes()">保存</button></div></div></div>
<!-- 尾程订单 - 入库计划 --> <!-- 尾程订单 - 入库计划 -->
<div class="modal-overlay" id="modal-lmo-inbound-plan"><div class="modal" style="max-width:720px"><div class="modal-header"><span>入库计划</span><span onclick="hideModal('modal-lmo-inbound-plan')" style="cursor:pointer;font-size:20px">&times;</span></div><div class="modal-body"><div class="lmo-modal-note">保存后生成入库单;清关完成后入库计划不可更改。</div><div class="form-row"><div class="form-group"><label>计划入库仓 *</label><select id="lmo-inbound-warehouse"><option>洛杉矶长滩海外仓</option><option>纽约海外仓</option><option>汉堡海外仓</option></select></div><div class="form-group"><label>预计到仓日期 *</label><input id="lmo-inbound-eta" type="date"/></div></div><div class="form-row"><div class="form-group"><label>入仓码 *</label><input id="lmo-inbound-code" placeholder="请输入入仓码"/></div><div class="form-group"><label>托数 *</label><input id="lmo-inbound-pallets" type="number" min="1" step="1" placeholder="请输入托数"/></div></div><div class="form-row single"><div class="form-group"><label for="lmo-inbound-consolidated">是否合托</label><label class="lmo-checkbox-option"><input id="lmo-inbound-consolidated" type="checkbox"/> 是</label></div></div><div class="form-row single"><div class="form-group"><label>备注</label><textarea id="lmo-inbound-remark" rows="3" placeholder="请输入备注"></textarea></div></div></div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-lmo-inbound-plan')">取消</button><button class="btn btn-primary" onclick="saveLastMileInboundPlan()">保存并生成入库单</button></div></div></div> <div class="modal-overlay" id="modal-lmo-inbound-plan"><div class="modal" style="max-width:720px"><div class="modal-header"><span>入库计划</span><span onclick="hideModal('modal-lmo-inbound-plan')" style="cursor:pointer;font-size:20px">&times;</span></div><div class="modal-body"><div class="lmo-modal-note">保存后生成入库单;清关完成后入库计划不可更改。</div><div class="form-row"><div class="form-group"><label>计划入库仓 *</label><select id="lmo-inbound-warehouse"><option>洛杉矶长滩海外仓</option><option>纽约海外仓</option><option>汉堡海外仓</option></select></div><div class="form-group"><label>预计到仓日期 *</label><input id="lmo-inbound-eta" type="date"/></div></div><div class="form-row"><div class="form-group"><label>入仓码 *</label><input id="lmo-inbound-code" placeholder="请输入入仓码"/></div><div class="form-group"><label>托数 *</label><input id="lmo-inbound-pallets" type="number" min="1" step="1" placeholder="请输入托数"/></div></div><div class="form-row"><div class="form-group"><label for="lmo-inbound-pallet-type">托盘类型</label><select id="lmo-inbound-pallet-type"><option value="标准托盘" selected>标准托盘</option><option value="木托盘">木托盘</option><option value="纸托盘">纸托盘</option></select></div><div class="form-group"><label for="lmo-inbound-outsourced">是否外配</label><label class="lmo-checkbox-option"><input id="lmo-inbound-outsourced" type="checkbox"/> 是</label></div></div><div class="form-row"><div class="form-group"><label for="lmo-inbound-consolidated">是否合托</label><label class="lmo-checkbox-option"><input id="lmo-inbound-consolidated" type="checkbox"/> 是</label></div><div class="form-group"><label for="lmo-inbound-urgent">是否加急</label><label class="lmo-checkbox-option"><input id="lmo-inbound-urgent" type="checkbox"/> 是</label></div></div><div class="form-row single"><div class="form-group"><label>备注</label><textarea id="lmo-inbound-remark" rows="3" placeholder="请输入备注"></textarea></div></div></div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-lmo-inbound-plan')">取消</button><button class="btn btn-primary" onclick="saveLastMileInboundPlan()">保存并生成入库单</button></div></div></div>
<!-- 尾程订单 - 合托 --> <!-- 尾程订单 - 合托 -->
<div class="modal-overlay" id="modal-lmo-consolidate"><div class="modal" style="max-width:680px"><div class="modal-header"><span>合托</span><span onclick="hideModal('modal-lmo-consolidate')" style="cursor:pointer;font-size:20px">&times;</span></div><div class="modal-body"><div class="form-row"><div class="form-group"><label>合托批次 *</label><input id="lmo-consolidate-batch" placeholder="如 CT20260810001"/></div><div class="form-group"><label>合托后托数 *</label><input id="lmo-consolidate-pallets" type="number" min="1"/></div></div><div class="form-row single"><div class="form-group"><label>备注</label><textarea id="lmo-consolidate-remark" rows="3" placeholder="请输入合托说明"></textarea></div></div></div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-lmo-consolidate')">取消</button><button class="btn btn-primary" onclick="saveLastMileConsolidate()">保存</button></div></div></div> <div class="modal-overlay" id="modal-lmo-consolidate"><div class="modal" style="max-width:680px"><div class="modal-header"><span>合托</span><span onclick="hideModal('modal-lmo-consolidate')" style="cursor:pointer;font-size:20px">&times;</span></div><div class="modal-body"><div class="form-row"><div class="form-group"><label>合托批次 *</label><input id="lmo-consolidate-batch" placeholder="如 CT20260810001"/></div><div class="form-group"><label>合托后托数 *</label><input id="lmo-consolidate-pallets" type="number" min="1"/></div></div><div class="form-row single"><div class="form-group"><label>备注</label><textarea id="lmo-consolidate-remark" rows="3" placeholder="请输入合托说明"></textarea></div></div></div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-lmo-consolidate')">取消</button><button class="btn btn-primary" onclick="saveLastMileConsolidate()">保存</button></div></div></div>
@@ -930,6 +942,10 @@ FORM: 采用现有后台的列表加标准详情结构,保持高信息密度
<!-- 尾程订单 - 转仓 --> <!-- 尾程订单 - 转仓 -->
<div class="modal-overlay" id="modal-lmo-transfer"><div class="modal" style="max-width:660px"><div class="modal-header"><span>转仓</span><span onclick="hideModal('modal-lmo-transfer')" style="cursor:pointer;font-size:20px">&times;</span></div><div class="modal-body"><div class="lmo-modal-note">保存后仅更新目的仓,不改变当前所在海外仓。</div><div class="form-row"><div class="form-group"><label>当前仓</label><input id="lmo-transfer-current" readonly/></div><div class="form-group"><label>目的仓 *</label><select id="lmo-transfer-destination"><option value="">请选择目的仓</option><option>洛杉矶长滩海外仓</option><option>纽约海外仓</option><option>汉堡海外仓</option></select></div></div><div class="form-row single"><div class="form-group"><label>备注</label><textarea id="lmo-transfer-remark" rows="3" placeholder="请输入转仓备注"></textarea></div></div></div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-lmo-transfer')">取消</button><button class="btn btn-primary" onclick="saveLastMileTransfer()">保存</button></div></div></div> <div class="modal-overlay" id="modal-lmo-transfer"><div class="modal" style="max-width:660px"><div class="modal-header"><span>转仓</span><span onclick="hideModal('modal-lmo-transfer')" style="cursor:pointer;font-size:20px">&times;</span></div><div class="modal-body"><div class="lmo-modal-note">保存后仅更新目的仓,不改变当前所在海外仓。</div><div class="form-row"><div class="form-group"><label>当前仓</label><input id="lmo-transfer-current" readonly/></div><div class="form-group"><label>目的仓 *</label><select id="lmo-transfer-destination"><option value="">请选择目的仓</option><option>洛杉矶长滩海外仓</option><option>纽约海外仓</option><option>汉堡海外仓</option></select></div></div><div class="form-row single"><div class="form-group"><label>备注</label><textarea id="lmo-transfer-remark" rows="3" placeholder="请输入转仓备注"></textarea></div></div></div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-lmo-transfer')">取消</button><button class="btn btn-primary" onclick="saveLastMileTransfer()">保存</button></div></div></div>
<!-- 尾程订单 - 导入 -->
<!-- 尾程订单 - 暂扣 -->
<div class="modal-overlay" id="modal-lmo-detention"><div class="modal" style="max-width:620px"><div class="modal-header"><span id="lmo-detention-title">暂扣订单</span><span onclick="hideModal('modal-lmo-detention')" style="cursor:pointer;font-size:20px">&times;</span></div><div class="modal-body"><div class="lmo-modal-note warning">暂扣后不可创建出库计划;解除暂扣后恢复出库计划操作。</div><div class="form-row"><div class="form-group"><label>暂扣状态 *</label><div style="display:flex;gap:18px;align-items:center;height:36px"><label class="lmo-checkbox-option"><input type="radio" name="lmo-detention-active" value="1" checked onchange="toggleLastMileDetentionRelabelFields()"/> 暂扣</label><label class="lmo-checkbox-option"><input type="radio" name="lmo-detention-active" value="0" onchange="toggleLastMileDetentionRelabelFields()"/> 解除暂扣</label></div></div><div class="form-group"><label>是否计费 *</label><div style="display:flex;gap:18px;align-items:center;height:36px"><label class="lmo-checkbox-option"><input type="radio" name="lmo-detention-chargeable" value="1" checked/> 是</label><label class="lmo-checkbox-option"><input type="radio" name="lmo-detention-chargeable" value="0"/> 否</label></div></div></div><div class="form-row single"><div class="form-group"><label for="lmo-detention-reason">变更原因 *</label><textarea id="lmo-detention-reason" rows="4" maxlength="200" placeholder="请输入暂扣或解除暂扣原因"></textarea></div></div><div id="lmo-detention-relabel-fields" style="display:none"><div class="form-row"><div class="form-group"><label>是否换标 *</label><div style="display:flex;gap:18px;align-items:center;height:36px"><label class="lmo-checkbox-option"><input type="radio" name="lmo-detention-relabel" value="1" onchange="toggleLastMileDetentionRelabelFields()"/> 是</label><label class="lmo-checkbox-option"><input type="radio" name="lmo-detention-relabel" value="0" checked onchange="toggleLastMileDetentionRelabelFields()"/> 否</label></div></div><div class="form-group"></div></div><div id="lmo-detention-relabel-inputs" style="display:none"><div class="form-row"><div class="form-group"><label for="lmo-detention-relabel-type">换标类型 *</label><select id="lmo-detention-relabel-type"><option value="">请选择换标类型</option><option>换箱唛</option><option>换SKU标</option><option>换箱唛和SKU标</option></select></div><div class="form-group"><label for="lmo-detention-relabel-file">上传换标资料 *</label><input id="lmo-detention-relabel-file" type="file" accept=".pdf,.jpg,.jpeg,.png,.xls,.xlsx"/></div></div></div></div></div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-lmo-detention')">取消</button><button class="btn btn-primary" onclick="saveLastMileDetention()">保存</button></div></div></div>
<!-- 尾程订单 - 导入 --> <!-- 尾程订单 - 导入 -->
<div class="modal-overlay" id="modal-lmo-import"><div class="modal" style="max-width:980px"><div class="modal-header"><span>导入尾程订单</span><span onclick="hideModal('modal-lmo-import')" style="cursor:pointer;font-size:20px">&times;</span></div><div class="modal-body"><div class="lmo-import-picker"><div><div style="font-size:13px;font-weight:650;color:#334155;margin-bottom:5px">选择 CSV 文件</div><div style="font-size:12px;color:#8491a3">请使用模板填写;Excel 文件可另存为 UTF-8 CSV 后导入。</div></div><div style="display:flex;align-items:center;gap:8px"><button class="btn btn-default btn-sm" onclick="downloadLastMileImportTemplate()">下载模板</button><input id="lmo-import-file" type="file" accept=".csv" onchange="handleLastMileImportFile(this)"/></div></div><div id="lmo-import-summary" class="lmo-import-summary">尚未选择文件</div><div class="lmo-import-preview"><table><thead id="thead-lmo-import-preview"></thead><tbody id="tbody-lmo-import-preview"></tbody></table></div></div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-lmo-import')">取消</button><button id="lmo-import-confirm" class="btn btn-primary" onclick="confirmLastMileImport()" disabled>确认导入</button></div></div></div> <div class="modal-overlay" id="modal-lmo-import"><div class="modal" style="max-width:980px"><div class="modal-header"><span>导入尾程订单</span><span onclick="hideModal('modal-lmo-import')" style="cursor:pointer;font-size:20px">&times;</span></div><div class="modal-body"><div class="lmo-import-picker"><div><div style="font-size:13px;font-weight:650;color:#334155;margin-bottom:5px">选择 CSV 文件</div><div style="font-size:12px;color:#8491a3">请使用模板填写;Excel 文件可另存为 UTF-8 CSV 后导入。</div></div><div style="display:flex;align-items:center;gap:8px"><button class="btn btn-default btn-sm" onclick="downloadLastMileImportTemplate()">下载模板</button><input id="lmo-import-file" type="file" accept=".csv" onchange="handleLastMileImportFile(this)"/></div></div><div id="lmo-import-summary" class="lmo-import-summary">尚未选择文件</div><div class="lmo-import-preview"><table><thead id="thead-lmo-import-preview"></thead><tbody id="tbody-lmo-import-preview"></tbody></table></div></div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-lmo-import')">取消</button><button id="lmo-import-confirm" class="btn btn-primary" onclick="confirmLastMileImport()" disabled>确认导入</button></div></div></div>
<!-- 尾程订单 - 导入/导出字段选择 --> <!-- 尾程订单 - 导入/导出字段选择 -->
@@ -69,7 +69,6 @@
<th>单号</th> <th>单号</th>
<th>到达状态(FBA状态编号)</th> <th>到达状态(FBA状态编号)</th>
<th>配送</th> <th>配送</th>
<th>编辑</th>
<th>输入人</th> <th>输入人</th>
<th>输入时间</th> <th>输入时间</th>
<th>FBA配送类型</th> <th>FBA配送类型</th>
@@ -83,7 +82,7 @@
</thead> </thead>
<tbody id="tbody-outbound-dispatch"> <tbody id="tbody-outbound-dispatch">
<tr data-dispatch-no="YWBHK200615001"> <tr data-dispatch-no="YWBHK200615001">
<td><div class="dispatch-actions"><a href="javascript:void(0)" style="color:#3b82f6;font-size:12px">详情</a><a href="javascript:void(0)" onclick="showEditFleet('BOL20260615001')">编辑车队</a><a href="javascript:void(0)" onclick="showSendConfirm('BOL20260615001')">发送完成消息</a></div></td> <td><div class="dispatch-actions"><a href="javascript:void(0)" style="color:#3b82f6;font-size:12px">详情</a><a class="dispatch-bol-print-action" href="javascript:void(0)" onclick="openDispatchBolPrint('BOL20260615001')">打印 BOL</a><a href="javascript:void(0)" onclick="showEditFleet('BOL20260615001')">编辑车队</a><a href="javascript:void(0)" onclick="showSendConfirm('BOL20260615001')">发送完成消息</a></div></td>
<td><div class="dispatch-doc-cell" data-dispatch-docs="YWBHK200615001">资料加载中…</div></td> <td><div class="dispatch-doc-cell" data-dispatch-docs="YWBHK200615001">资料加载中…</div></td>
<td><span class="tag tag-blue">派送中</span></td> <td><span class="tag tag-blue">派送中</span></td>
<td>海卡</td> <td>海卡</td>
@@ -108,7 +107,6 @@
<td>D202606001</td> <td>D202606001</td>
<td>已签收</td> <td>已签收</td>
<td>是</td> <td>是</td>
<td><a href="javascript:void(0)" style="color:#3b82f6;font-size:11px">编辑</a></td>
<td>李输入</td> <td>李输入</td>
<td>2026-06-15 09:00</td> <td>2026-06-15 09:00</td>
<td>FBA-标准</td> <td>FBA-标准</td>
@@ -120,7 +118,7 @@
<td>7260.00</td> <td>7260.00</td>
</tr> </tr>
<tr data-dispatch-no="YWBHK200615002"> <tr data-dispatch-no="YWBHK200615002">
<td><div class="dispatch-actions"><a href="javascript:void(0)" style="color:#3b82f6;font-size:12px">详情</a><a href="javascript:void(0)" onclick="showEditFleet('BOL20260615002')">编辑车队</a><a href="javascript:void(0)" onclick="showSendConfirm('BOL20260615002')">发送完成消息</a></div></td> <td><div class="dispatch-actions"><a href="javascript:void(0)" style="color:#3b82f6;font-size:12px">详情</a><a class="dispatch-bol-print-action" href="javascript:void(0)" onclick="openDispatchBolPrint('BOL20260615002')">打印 BOL</a><a href="javascript:void(0)" onclick="showEditFleet('BOL20260615002')">编辑车队</a><a href="javascript:void(0)" onclick="showSendConfirm('BOL20260615002')">发送完成消息</a></div></td>
<td><div class="dispatch-doc-cell" data-dispatch-docs="YWBHK200615002">资料加载中…</div></td> <td><div class="dispatch-doc-cell" data-dispatch-docs="YWBHK200615002">资料加载中…</div></td>
<td><span class="tag tag-green">已签收</span></td> <td><span class="tag tag-green">已签收</span></td>
<td>海卡</td> <td>海卡</td>
@@ -145,7 +143,6 @@
<td>D202606002</td> <td>D202606002</td>
<td>已签收</td> <td>已签收</td>
<td>是</td> <td>是</td>
<td><a href="javascript:void(0)" style="color:#3b82f6;font-size:11px">编辑</a></td>
<td>王输入</td> <td>王输入</td>
<td>2026-06-14 07:30</td> <td>2026-06-14 07:30</td>
<td>FBA-标准</td> <td>FBA-标准</td>
@@ -157,7 +154,7 @@
<td>5775.00</td> <td>5775.00</td>
</tr> </tr>
<tr data-dispatch-no="YWBHK200615003"> <tr data-dispatch-no="YWBHK200615003">
<td><div class="dispatch-actions"><a href="javascript:void(0)" style="color:#3b82f6;font-size:12px">详情</a><a href="javascript:void(0)" onclick="showEditFleet('BOL20260615003')">编辑车队</a><a href="javascript:void(0)" onclick="showSendConfirm('BOL20260615003')">发送完成消息</a></div></td> <td><div class="dispatch-actions"><a href="javascript:void(0)" style="color:#3b82f6;font-size:12px">详情</a><a class="dispatch-bol-print-action" href="javascript:void(0)" onclick="openDispatchBolPrint('BOL20260615003')">打印 BOL</a><a href="javascript:void(0)" onclick="showEditFleet('BOL20260615003')">编辑车队</a><a href="javascript:void(0)" onclick="showSendConfirm('BOL20260615003')">发送完成消息</a></div></td>
<td><div class="dispatch-doc-cell" data-dispatch-docs="YWBHK200615003">资料加载中…</div></td> <td><div class="dispatch-doc-cell" data-dispatch-docs="YWBHK200615003">资料加载中…</div></td>
<td><span class="tag tag-blue">派送中</span></td> <td><span class="tag tag-blue">派送中</span></td>
<td>海卡</td> <td>海卡</td>
@@ -182,7 +179,6 @@
<td>D202606003</td> <td>D202606003</td>
<td>运输中</td> <td>运输中</td>
<td>否</td> <td>否</td>
<td><a href="javascript:void(0)" style="color:#3b82f6;font-size:11px">编辑</a></td>
<td>张输入</td> <td>张输入</td>
<td>2026-06-15 10:00</td> <td>2026-06-15 10:00</td>
<td>FBA-加急</td> <td>FBA-加急</td>
@@ -194,7 +190,7 @@
<td>3520.00</td> <td>3520.00</td>
</tr> </tr>
<tr data-dispatch-no="YWBHK200615004"> <tr data-dispatch-no="YWBHK200615004">
<td><div class="dispatch-actions"><a href="javascript:void(0)" style="color:#3b82f6;font-size:12px">详情</a><a href="javascript:void(0)" onclick="showEditFleet('BOL20260615004')">编辑车队</a><a href="javascript:void(0)" onclick="showSendConfirm('BOL20260615004')">发送完成消息</a></div></td> <td><div class="dispatch-actions"><a href="javascript:void(0)" style="color:#3b82f6;font-size:12px">详情</a><a class="dispatch-bol-print-action" href="javascript:void(0)" onclick="openDispatchBolPrint('BOL20260615004')">打印 BOL</a><a href="javascript:void(0)" onclick="showEditFleet('BOL20260615004')">编辑车队</a><a href="javascript:void(0)" onclick="showSendConfirm('BOL20260615004')">发送完成消息</a></div></td>
<td><div class="dispatch-doc-cell" data-dispatch-docs="YWBHK200615004">资料加载中…</div></td> <td><div class="dispatch-doc-cell" data-dispatch-docs="YWBHK200615004">资料加载中…</div></td>
<td><span class="tag tag-green">已签收</span></td> <td><span class="tag tag-green">已签收</span></td>
<td>海卡</td> <td>海卡</td>
@@ -219,7 +215,6 @@
<td>D202606004</td> <td>D202606004</td>
<td>已签收</td> <td>已签收</td>
<td>是</td> <td>是</td>
<td><a href="javascript:void(0)" style="color:#3b82f6;font-size:11px">编辑</a></td>
<td>陈输入</td> <td>陈输入</td>
<td>2026-06-13 08:10</td> <td>2026-06-13 08:10</td>
<td>FBA-退货</td> <td>FBA-退货</td>
@@ -231,7 +226,7 @@
<td>1072.50</td> <td>1072.50</td>
</tr> </tr>
<tr data-dispatch-no="YWBHK200615005"> <tr data-dispatch-no="YWBHK200615005">
<td><div class="dispatch-actions"><a href="javascript:void(0)" style="color:#3b82f6;font-size:12px">详情</a><a href="javascript:void(0)" onclick="showEditFleet('BOL20260615005')">编辑车队</a><a href="javascript:void(0)" onclick="showSendConfirm('BOL20260615005')">发送完成消息</a></div></td> <td><div class="dispatch-actions"><a href="javascript:void(0)" style="color:#3b82f6;font-size:12px">详情</a><a class="dispatch-bol-print-action" href="javascript:void(0)" onclick="openDispatchBolPrint('BOL20260615005')">打印 BOL</a><a href="javascript:void(0)" onclick="showEditFleet('BOL20260615005')">编辑车队</a><a href="javascript:void(0)" onclick="showSendConfirm('BOL20260615005')">发送完成消息</a></div></td>
<td><div class="dispatch-doc-cell" data-dispatch-docs="YWBHK200615005">资料加载中…</div></td> <td><div class="dispatch-doc-cell" data-dispatch-docs="YWBHK200615005">资料加载中…</div></td>
<td><span class="tag tag-blue">派送中</span></td> <td><span class="tag tag-blue">派送中</span></td>
<td>海卡</td> <td>海卡</td>
@@ -256,7 +251,6 @@
<td>D202606005</td> <td>D202606005</td>
<td>待处理</td> <td>待处理</td>
<td>否</td> <td>否</td>
<td><a href="javascript:void(0)" style="color:#3b82f6;font-size:11px">编辑</a></td>
<td>刘输入</td> <td>刘输入</td>
<td>2026-06-15 14:00</td> <td>2026-06-15 14:00</td>
<td>FBA-标准</td> <td>FBA-标准</td>
@@ -1,7 +1,7 @@
<div class="page" id="page-outbound-exception"> <div class="page" id="page-outbound-exception">
<div class="table-card"> <div class="table-card">
<div class="breadcrumb">首页 <span class="sep">&gt;</span> 客户服务 <span class="sep">&gt;</span> <strong>异常订单</strong></div> <div class="breadcrumb">首页 <span class="sep">&gt;</span> 客户服务 <span class="sep">&gt;</span> <strong>异常订单</strong></div>
<div class="table-toolbar"><span class="title">异常订单</span><div class="toolbar-actions"><button class="btn btn-default btn-sm" onclick="switchPage('outbound-dispatch',this)">返回派送单</button></div></div> <div class="table-toolbar"><span class="title">异常订单</span><div class="toolbar-actions"><button class="btn btn-primary btn-sm" onclick="openDispatchExceptionCreate()">新增异常订单</button><button class="btn btn-default btn-sm" onclick="switchPage('outbound-dispatch',this)">返回派送单</button></div></div>
<div class="dispatch-exception-summary"><div class="dispatch-exception-kpi"><strong id="dispatch-exception-total">0</strong><span>异常订单</span></div><div class="dispatch-exception-kpi"><strong id="dispatch-exception-pending">0</strong><span>待处理</span></div><div class="dispatch-exception-kpi"><strong id="dispatch-exception-processing">0</strong><span>处理中</span></div></div> <div class="dispatch-exception-summary"><div class="dispatch-exception-kpi"><strong id="dispatch-exception-total">0</strong><span>异常订单</span></div><div class="dispatch-exception-kpi"><strong id="dispatch-exception-pending">0</strong><span>待处理</span></div><div class="dispatch-exception-kpi"><strong id="dispatch-exception-processing">0</strong><span>处理中</span></div></div>
<div class="inbound-search-section"> <div class="inbound-search-section">
<div class="search-row" style="margin-bottom:0;display:flex;align-items:center;gap:10px;flex-wrap:wrap"> <div class="search-row" style="margin-bottom:0;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
@@ -17,3 +17,23 @@
</div><div class="pagination"><span id="dispatch-exception-page-total">共 0 条</span><button class="page-btn active">1</button></div> </div><div class="pagination"><span id="dispatch-exception-page-total">共 0 条</span><button class="page-btn active">1</button></div>
</div> </div>
</div> </div>
<div class="page" id="page-customer-followup">
<div class="table-card">
<div class="breadcrumb">首页 <span class="sep">&gt;</span> 客户服务 <span class="sep">&gt;</span> <strong>客户回访</strong></div>
<div class="table-toolbar"><span class="title">客户回访</span><div class="toolbar-actions"><button class="btn btn-default btn-sm" onclick="switchPage('outbound-dispatch',this)">返回派送单</button></div></div>
<div style="display:flex;align-items:center;gap:8px;padding:10px 12px;margin-bottom:12px;border:1px solid #dbeafe;border-radius:6px;background:#eff6ff;color:#1e40af;font-size:12px;line-height:1.5"><span style="font-size:16px">i</span><span>页面展示最近 7 天内已完成派送的订单,完成时间以签收/派送完成时间为准。</span></div>
<div class="dispatch-exception-summary" style="margin-bottom:12px"><div class="dispatch-exception-kpi"><strong id="customer-followup-total">0</strong><span>可回访订单</span></div><div class="dispatch-exception-kpi"><strong id="customer-followup-pending">0</strong><span>待回访</span></div><div class="dispatch-exception-kpi"><strong id="customer-followup-completed">0</strong><span>已完成回访</span></div></div>
<div class="inbound-search-section">
<div class="search-row" style="margin-bottom:0;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<input id="customer-followup-keyword" placeholder="派送单号 / 客户 / 联系人" style="width:280px"/>
<select id="customer-followup-status" style="width:140px"><option value="">回访状态</option><option value="未回访">未回访</option><option value="已回访">已回访</option></select>
<button class="btn btn-sm btn-primary" onclick="searchCustomerFollowups()">查询</button><button class="btn btn-sm btn-default" onclick="resetCustomerFollowups()">重置</button>
</div>
</div>
<div class="inbound-table-container">
<table style="min-width:1250px;font-size:12px"><thead><tr><th style="width:100px">操作</th><th>派送单号</th><th>客户</th><th>联系人</th><th>完成时间</th><th>目的地</th><th>回访状态</th><th>回访时间</th><th>回访结果</th><th>资料</th></tr></thead>
<tbody id="tbody-customer-followup"></tbody></table>
</div><div class="pagination"><span id="customer-followup-page-total">共 0 条</span><button class="page-btn active">1</button></div>
</div>
</div>
@@ -996,13 +996,13 @@
<div class="table-toolbar"><span class="title">货柜费用</span><div class="toolbar-actions"><button class="btn btn-primary" onclick="openCtCostAdd()">+ 新增费用</button><button class="btn btn-default">导出</button></div></div> <div class="table-toolbar"><span class="title">货柜费用</span><div class="toolbar-actions"><button class="btn btn-primary" onclick="openCtCostAdd()">+ 新增费用</button><button class="btn btn-default">导出</button></div></div>
<div class="search-row" style="flex-wrap:wrap;gap:8px"> <div class="search-row" style="flex-wrap:wrap;gap:8px">
<input placeholder="集装箱号" style="width:160px"/> <input placeholder="集装箱号" style="width:160px"/>
<select style="width:130px"><option value="">全部费用类型</option><option>港口费</option><option>堆存费</option><option>修箱费</option><option>吊机费</option><option>拖车费</option><option>报关费</option><option>其他</option></select> <select style="width:130px"><option value="">全部费用项目</option><option>港口费</option><option>堆存费</option><option>修箱费</option><option>吊机费</option><option>拖车费</option><option>报关费</option><option>其他</option></select>
<input type="date" style="width:150px"/> <input type="date" style="width:150px"/>
<button class="btn btn-sm btn-primary">查询</button><button class="btn btn-sm btn-default">重置</button> <button class="btn btn-sm btn-primary">查询</button><button class="btn btn-sm btn-default">重置</button>
</div> </div>
<table style="font-size:12px"> <table style="font-size:12px">
<thead><tr> <thead><tr>
<th>费用编号</th><th>集装箱号</th><th>费用类型</th><th>金额</th><th>币种</th><th>费用日期</th><th>备注</th><th>操作</th> <th>费用编号</th><th>集装箱号</th><th>费用项目</th><th>费用类型</th><th>金额</th><th>币种</th><th>费用日期</th><th>备注</th><th>操作</th>
</tr></thead> </tr></thead>
<tbody id="tbody-ct-cost"></tbody> <tbody id="tbody-ct-cost"></tbody>
</table> </table>
@@ -309,6 +309,25 @@
</div> </div>
</div></div> </div></div>
<!-- ==================== OVERSEAS TRUCK DETAIL ==================== -->
<div class="page" id="page-overseas-truck-detail">
<div class="purchase-edit-shell">
<div class="purchase-detail-head">
<div><div class="purchase-detail-title">本土卡车订单详情</div><div id="truck-detail-no" style="font-size:13px;color:#64748b;margin-top:5px">-</div></div>
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap"><span id="truck-detail-status" class="tag tag-gray">-</span><button class="btn btn-primary" onclick="editTruckOrderFromDetail()">编辑</button><button class="btn btn-default" onclick="switchPage('overseas-truck')">返回列表</button></div>
</div>
<div class="purchase-detail-body">
<div id="truck-detail-fields" class="purchase-detail-grid">
<section class="purchase-detail-section"><h3>运输信息</h3><div id="truck-detail-transport" class="purchase-detail-fields"></div></section>
<section class="purchase-detail-section"><h3>联系人信息</h3><div id="truck-detail-contact" class="purchase-detail-fields"></div></section>
<section class="purchase-detail-section"><h3>货物信息</h3><div id="truck-detail-cargo" class="purchase-detail-fields"></div></section>
<section class="purchase-detail-section"><h3>时间与费用</h3><div id="truck-detail-schedule" class="purchase-detail-fields"></div></section>
<section class="purchase-detail-section full"><h3>上传资料</h3><div id="truck-detail-attachments"></div></section>
</div>
</div>
</div>
</div>
<!-- ==================== OVERSEAS TRUCK EDIT ==================== --> <!-- ==================== OVERSEAS TRUCK EDIT ==================== -->
<div class="page" id="page-overseas-truck-edit"> <div class="page" id="page-overseas-truck-edit">
<div style="font-size:14px;color:#666;margin-bottom:12px"><a href="javascript:void(0)" onclick="switchPage('overseas-truck')" style="color:#1890ff;text-decoration:none">← 返回列表</a></div> <div style="font-size:14px;color:#666;margin-bottom:12px"><a href="javascript:void(0)" onclick="switchPage('overseas-truck')" style="color:#1890ff;text-decoration:none">← 返回列表</a></div>
@@ -354,6 +373,12 @@
<div style="margin-bottom:20px"><label style="font-size:13px;font-weight:600;color:#333;margin-bottom:8px;display:block">要求时间</label><input id="tke-required-date" type="date" style="width:100%;max-width:300px;padding:11px 14px;border-radius:8px;border:1px solid #e0e0e0;background:#fff;font-size:13px;outline:none;box-sizing:border-box"/></div> <div style="margin-bottom:20px"><label style="font-size:13px;font-weight:600;color:#333;margin-bottom:8px;display:block">要求时间</label><input id="tke-required-date" type="date" style="width:100%;max-width:300px;padding:11px 14px;border-radius:8px;border:1px solid #e0e0e0;background:#fff;font-size:13px;outline:none;box-sizing:border-box"/></div>
<!-- 状态 --> <!-- 状态 -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0 24px;margin-bottom:0"><div><label style="font-size:13px;font-weight:600;color:#333;margin-bottom:8px;display:block">状态</label><select id="tke-status" style="width:100%;padding:11px 14px;border-radius:8px;border:1px solid #e0e0e0;background:#fff;font-size:13px;outline:none;box-sizing:border-box"><option>待接单</option><option>已派车</option><option>运输中</option><option>已完成</option></select></div><div></div></div> <div style="display:grid;grid-template-columns:1fr 1fr;gap:0 24px;margin-bottom:0"><div><label style="font-size:13px;font-weight:600;color:#333;margin-bottom:8px;display:block">状态</label><select id="tke-status" style="width:100%;padding:11px 14px;border-radius:8px;border:1px solid #e0e0e0;background:#fff;font-size:13px;outline:none;box-sizing:border-box"><option>待接单</option><option>已派车</option><option>运输中</option><option>已完成</option></select></div><div></div></div>
<!-- 上传资料 -->
<div style="margin-top:20px"><label style="font-size:13px;font-weight:600;color:#333;margin-bottom:8px;display:block">上传资料</label>
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap"><input id="tke-attachments-input" type="file" multiple style="font-size:13px;max-width:100%"/><button type="button" class="btn btn-default" style="padding:7px 14px" onclick="addTruckEditAttachments()">添加所选资料</button></div>
<div style="color:#999;font-size:12px;margin-top:6px">支持多文件上传,保存后可再次编辑删除。</div>
<div id="tke-attachments-list" style="margin-top:10px"></div>
</div>
</div> </div>
<div style="display:flex;justify-content:flex-end;gap:12px;margin-top:20px;padding-top:16px;border-top:1px solid #eee"> <div style="display:flex;justify-content:flex-end;gap:12px;margin-top:20px;padding-top:16px;border-top:1px solid #eee">
<button class="btn btn-default" style="padding:8px 28px;border-radius:6px" onclick="switchPage('overseas-truck')">取消</button> <button class="btn btn-default" style="padding:8px 28px;border-radius:6px" onclick="switchPage('overseas-truck')">取消</button>
@@ -35,25 +35,32 @@ FORM: 已建立后台列表页的精确扩展;本次需求已明确,因此
<!-- ==================== WAREHOUSE MANAGEMENT ==================== --> <!-- ==================== WAREHOUSE MANAGEMENT ==================== -->
<div class="page" id="page-gen-owned-warehouse"> <div class="page" id="page-gen-owned-warehouse">
<div class="table-card"><div class="table-toolbar"><span class="title">仓库管理</span><div class="toolbar-actions"><button class="btn btn-primary" onclick="openWarehouseAdd('owned')">+ 新增</button><button class="btn btn-default">导入</button><button class="btn btn-default">导出</button></div></div> <div class="table-card"><div class="table-toolbar"><span class="title">仓库管理</span><div class="toolbar-actions"><button class="btn btn-primary" onclick="openWarehouseAdd('owned')">+ 新增</button><button class="btn btn-default">导入</button><button class="btn btn-default">导出</button></div></div>
<div class="warehouse-page-tabs" role="tablist" aria-label="仓库分类"><button type="button" class="warehouse-page-tab active" role="tab" aria-selected="true" onclick="switchWarehousePage('gen-owned-warehouse',this)">自有仓</button><button type="button" class="warehouse-page-tab" role="tab" aria-selected="false" onclick="switchWarehousePage('gen-fba',this)">FBA仓库</button><button type="button" class="warehouse-page-tab" role="tab" aria-selected="false" onclick="switchWarehousePage('gen-partner-warehouse',this)">合作仓</button></div> <div class="warehouse-page-tabs" role="tablist" aria-label="仓库分类"><button type="button" class="warehouse-page-tab active" role="tab" aria-selected="true" onclick="switchWarehousePage('gen-owned-warehouse',this)">自有仓</button><button type="button" class="warehouse-page-tab" role="tab" aria-selected="false" onclick="switchWarehousePage('gen-fba',this)">FBA仓库</button><button type="button" class="warehouse-page-tab" role="tab" aria-selected="false" onclick="switchWarehousePage('gen-partner-warehouse',this)">合作仓</button><button type="button" class="warehouse-page-tab" role="tab" aria-selected="false" onclick="switchWarehousePage('gen-private-warehouse',this)">私人仓</button></div>
<div class="search-row"><input id="gen-owned-warehouse-keyword" placeholder="自有仓名称/地址" style="width:200px"/><select id="gen-owned-warehouse-country"><option value="">全部国家</option><option>美国</option><option>英国</option><option>德国</option><option>日本</option></select><button class="btn btn-sm btn-primary" onclick="renderOwnedWarehouse()">查询</button></div> <div class="search-row"><input id="gen-owned-warehouse-keyword" placeholder="自有仓名称/地址" style="width:200px"/><select id="gen-owned-warehouse-country"><option value="">全部国家</option><option>美国</option><option>英国</option><option>德国</option><option>日本</option></select><button class="btn btn-sm btn-primary" onclick="renderOwnedWarehouse()">查询</button></div>
<div class="warehouse-table-wrap"><table><thead><tr><th>自有仓名称</th><th>仓库地址</th><th>城市</th><th>州/省</th><th>邮编</th><th>国家</th><th>状态</th><th>操作</th></tr></thead><tbody id="tbody-gen-owned-warehouse"></tbody></table></div> <div class="warehouse-table-wrap"><table><thead><tr><th>自有仓名称</th><th>仓库地址</th><th>城市</th><th>州/省</th><th>邮编</th><th>国家</th><th>状态</th><th>操作</th></tr></thead><tbody id="tbody-gen-owned-warehouse"></tbody></table></div>
<div class="pagination"><span>共 2 条</span><button class="page-btn active">1</button></div></div></div> <div class="pagination"><span>共 2 条</span><button class="page-btn active">1</button></div></div></div>
<div class="page" id="page-gen-fba"> <div class="page" id="page-gen-fba">
<div class="table-card"><div class="table-toolbar"><span class="title">仓库管理</span><div class="toolbar-actions"><button class="btn btn-primary" onclick="openWarehouseAdd('fba')">+ 新增</button><button class="btn btn-default">导入</button><button class="btn btn-default">导出</button></div></div> <div class="table-card"><div class="table-toolbar"><span class="title">仓库管理</span><div class="toolbar-actions"><button class="btn btn-primary" onclick="openWarehouseAdd('fba')">+ 新增</button><button class="btn btn-default">导入</button><button class="btn btn-default">导出</button></div></div>
<div class="warehouse-page-tabs" role="tablist" aria-label="仓库分类"><button type="button" class="warehouse-page-tab" role="tab" aria-selected="false" onclick="switchWarehousePage('gen-owned-warehouse',this)">自有仓</button><button type="button" class="warehouse-page-tab active" role="tab" aria-selected="true" onclick="switchWarehousePage('gen-fba',this)">FBA仓库</button><button type="button" class="warehouse-page-tab" role="tab" aria-selected="false" onclick="switchWarehousePage('gen-partner-warehouse',this)">合作仓</button></div> <div class="warehouse-page-tabs" role="tablist" aria-label="仓库分类"><button type="button" class="warehouse-page-tab" role="tab" aria-selected="false" onclick="switchWarehousePage('gen-owned-warehouse',this)">自有仓</button><button type="button" class="warehouse-page-tab active" role="tab" aria-selected="true" onclick="switchWarehousePage('gen-fba',this)">FBA仓库</button><button type="button" class="warehouse-page-tab" role="tab" aria-selected="false" onclick="switchWarehousePage('gen-partner-warehouse',this)">合作仓</button><button type="button" class="warehouse-page-tab" role="tab" aria-selected="false" onclick="switchWarehousePage('gen-private-warehouse',this)">私人仓</button></div>
<div class="search-row"><span style="font-size:12px;color:#475569;font-weight:600">自有仓</span><select id="gen-fba-owned-warehouse" data-owned-warehouse-select aria-label="自有仓"></select><input id="gen-fba-keyword" placeholder="仓库代码/地址" style="width:200px"/><select id="gen-fba-country"><option value="">全部国家</option><option>美国</option><option>英国</option><option>德国</option><option>日本</option></select><button class="btn btn-sm btn-primary" onclick="renderGenFba()">查询</button></div> <div class="search-row"><span style="font-size:12px;color:#475569;font-weight:600">自有仓</span><select id="gen-fba-owned-warehouse" data-owned-warehouse-select aria-label="自有仓"></select><input id="gen-fba-keyword" placeholder="仓库代码/地址" style="width:200px"/><select id="gen-fba-country"><option value="">全部国家</option><option>美国</option><option>英国</option><option>德国</option><option>日本</option></select><button class="btn btn-sm btn-primary" onclick="renderGenFba()">查询</button></div>
<div class="warehouse-table-wrap"><table><thead><tr><th>仓库代码</th><th>仓库地址</th><th>城市</th><th>州/省</th><th>邮编</th><th>国家</th><th>距离(MILES)</th><th>状态</th><th>操作</th></tr></thead><tbody id="tbody-gen-fba"></tbody></table></div> <div class="warehouse-table-wrap"><table><thead><tr><th>仓库代码</th><th>仓库地址</th><th>城市</th><th>州/省</th><th>邮编</th><th>国家</th><th>距离(MILES)</th><th>状态</th><th>操作</th></tr></thead><tbody id="tbody-gen-fba"></tbody></table></div>
<div class="pagination"><span>共 56 条</span><button class="page-btn active">1</button><button class="page-btn">2</button><button class="page-btn">3</button><button class="page-btn">...</button><button class="page-btn">6</button></div></div></div> <div class="pagination"><span>共 56 条</span><button class="page-btn active">1</button><button class="page-btn">2</button><button class="page-btn">3</button><button class="page-btn">...</button><button class="page-btn">6</button></div></div></div>
<div class="page" id="page-gen-partner-warehouse"> <div class="page" id="page-gen-partner-warehouse">
<div class="table-card"><div class="table-toolbar"><span class="title">仓库管理</span><div class="toolbar-actions"><button class="btn btn-primary" onclick="openWarehouseAdd('partner')">+ 新增</button><button class="btn btn-default">导入</button><button class="btn btn-default">导出</button></div></div> <div class="table-card"><div class="table-toolbar"><span class="title">仓库管理</span><div class="toolbar-actions"><button class="btn btn-primary" onclick="openWarehouseAdd('partner')">+ 新增</button><button class="btn btn-default">导入</button><button class="btn btn-default">导出</button></div></div>
<div class="warehouse-page-tabs" role="tablist" aria-label="仓库分类"><button type="button" class="warehouse-page-tab" role="tab" aria-selected="false" onclick="switchWarehousePage('gen-owned-warehouse',this)">自有仓</button><button type="button" class="warehouse-page-tab" role="tab" aria-selected="false" onclick="switchWarehousePage('gen-fba',this)">FBA仓库</button><button type="button" class="warehouse-page-tab active" role="tab" aria-selected="true" onclick="switchWarehousePage('gen-partner-warehouse',this)">合作仓</button></div> <div class="warehouse-page-tabs" role="tablist" aria-label="仓库分类"><button type="button" class="warehouse-page-tab" role="tab" aria-selected="false" onclick="switchWarehousePage('gen-owned-warehouse',this)">自有仓</button><button type="button" class="warehouse-page-tab" role="tab" aria-selected="false" onclick="switchWarehousePage('gen-fba',this)">FBA仓库</button><button type="button" class="warehouse-page-tab active" role="tab" aria-selected="true" onclick="switchWarehousePage('gen-partner-warehouse',this)">合作仓</button><button type="button" class="warehouse-page-tab" role="tab" aria-selected="false" onclick="switchWarehousePage('gen-private-warehouse',this)">私人仓</button></div>
<div class="search-row"><span style="font-size:12px;color:#475569;font-weight:600">自有仓</span><select id="gen-partner-warehouse-owned-warehouse" data-owned-warehouse-select aria-label="自有仓"></select><input id="gen-partner-warehouse-keyword" placeholder="仓库代码/地址" style="width:200px"/><select id="gen-partner-warehouse-country"><option value="">全部国家</option><option>美国</option><option>英国</option><option>德国</option><option>日本</option></select><button class="btn btn-sm btn-primary" onclick="renderPartnerWarehouse()">查询</button></div> <div class="search-row"><span style="font-size:12px;color:#475569;font-weight:600">自有仓</span><select id="gen-partner-warehouse-owned-warehouse" data-owned-warehouse-select aria-label="自有仓"></select><input id="gen-partner-warehouse-keyword" placeholder="仓库代码/地址" style="width:200px"/><select id="gen-partner-warehouse-country"><option value="">全部国家</option><option>美国</option><option>英国</option><option>德国</option><option>日本</option></select><button class="btn btn-sm btn-primary" onclick="renderPartnerWarehouse()">查询</button></div>
<div class="warehouse-table-wrap"><table><thead><tr><th>仓库代码</th><th>仓库地址</th><th>城市</th><th>州/省</th><th>邮编</th><th>国家</th><th>距离(MILES)</th><th>状态</th><th>操作</th></tr></thead><tbody id="tbody-gen-partner-warehouse"></tbody></table></div> <div class="warehouse-table-wrap"><table><thead><tr><th>仓库代码</th><th>仓库地址</th><th>城市</th><th>州/省</th><th>邮编</th><th>国家</th><th>距离(MILES)</th><th>状态</th><th>操作</th></tr></thead><tbody id="tbody-gen-partner-warehouse"></tbody></table></div>
<div class="pagination"><span>共 1 条</span><button class="page-btn active">1</button></div></div></div> <div class="pagination"><span>共 1 条</span><button class="page-btn active">1</button></div></div></div>
<div class="page" id="page-gen-private-warehouse">
<div class="table-card"><div class="table-toolbar"><span class="title">仓库管理</span><div class="toolbar-actions"><button class="btn btn-primary" onclick="openWarehouseAdd('private')">+ 新增</button><button class="btn btn-default">导入</button><button class="btn btn-default">导出</button></div></div>
<div class="warehouse-page-tabs" role="tablist" aria-label="仓库分类"><button type="button" class="warehouse-page-tab" role="tab" aria-selected="false" onclick="switchWarehousePage('gen-owned-warehouse',this)">自有仓</button><button type="button" class="warehouse-page-tab" role="tab" aria-selected="false" onclick="switchWarehousePage('gen-fba',this)">FBA仓库</button><button type="button" class="warehouse-page-tab" role="tab" aria-selected="false" onclick="switchWarehousePage('gen-partner-warehouse',this)">合作仓</button><button type="button" class="warehouse-page-tab active" role="tab" aria-selected="true" onclick="switchWarehousePage('gen-private-warehouse',this)">私人仓</button></div>
<div class="search-row"><span style="font-size:12px;color:#475569;font-weight:600">自有仓</span><select id="gen-private-warehouse-owned-warehouse" data-owned-warehouse-select aria-label="自有仓"></select><input id="gen-private-warehouse-keyword" placeholder="仓库代码/地址" style="width:200px"/><select id="gen-private-warehouse-country"><option value="">全部国家</option><option>美国</option><option>英国</option><option>德国</option><option>日本</option></select><button class="btn btn-sm btn-primary" onclick="renderPrivateWarehouse()">查询</button></div>
<div class="warehouse-table-wrap"><table><thead><tr><th>仓库代码</th><th>仓库地址</th><th>城市</th><th>州/省</th><th>邮编</th><th>国家</th><th>距离(MILES)</th><th>状态</th><th>操作</th></tr></thead><tbody id="tbody-gen-private-warehouse"></tbody></table></div>
<div class="pagination"><span>共 1 条</span><button class="page-btn active">1</button></div></div></div>
<style> <style>
#page-gen-overseas-quote .goq-table-wrap{overflow-x:auto;border-top:1px solid #edf0f4;border-bottom:1px solid #edf0f4} #page-gen-overseas-quote .goq-table-wrap{overflow-x:auto;border-top:1px solid #edf0f4;border-bottom:1px solid #edf0f4}
#page-gen-overseas-quote .goq-table{min-width:1480px;table-layout:fixed;font-size:12px} #page-gen-overseas-quote .goq-table{min-width:1480px;table-layout:fixed;font-size:12px}
@@ -1,5 +1,25 @@
<!-- ==================== CUSTOMER FOLLOW-UP MODAL ==================== -->
<div class="modal-overlay" id="modal-customer-followup">
<div class="modal" style="max-width:720px;max-height:90vh;overflow-y:auto">
<div class="modal-header"><span>客户回访</span><span onclick="hideModal('modal-customer-followup')" style="cursor:pointer;font-size:20px">&times;</span></div>
<div class="modal-body">
<div class="customer-followup-modal-meta" style="display:grid;grid-template-columns:1fr 1fr;gap:10px 16px;padding:12px 14px;margin-bottom:16px;border:1px solid #e5e7eb;border-radius:6px;background:#f9fafb">
<div><span style="display:block;color:#6b7280;font-size:12px;margin-bottom:3px">派送单号</span><strong id="customer-followup-dispatch-no" style="font-size:13px;color:#1f2937">—</strong></div>
<div><span style="display:block;color:#6b7280;font-size:12px;margin-bottom:3px">客户</span><strong id="customer-followup-customer" style="font-size:13px;color:#1f2937">—</strong></div>
<div><span style="display:block;color:#6b7280;font-size:12px;margin-bottom:3px">联系人</span><span id="customer-followup-contact" style="font-size:13px;color:#374151">—</span></div>
<div><span style="display:block;color:#6b7280;font-size:12px;margin-bottom:3px">完成时间</span><span id="customer-followup-completed-at" style="font-size:13px;color:#374151">—</span></div>
<div style="grid-column:1/-1"><span style="display:block;color:#6b7280;font-size:12px;margin-bottom:3px">目的地</span><span id="customer-followup-destination" style="font-size:13px;color:#374151">—</span></div>
</div>
<div class="form-group"><label>回访结果 <span style="color:#ef4444">*</span></label><select id="customer-followup-result"><option value="">请选择回访结果</option><option value="满意">满意</option><option value="一般">一般</option><option value="不满意">不满意</option><option value="未接通">未接通</option></select></div>
<div class="form-group"><label>客户反馈</label><textarea id="customer-followup-feedback" rows="3" maxlength="500" placeholder="记录客户对派送服务的反馈"></textarea></div>
<div class="form-group"><label>备注</label><textarea id="customer-followup-note" rows="2" maxlength="300" placeholder="补充后续跟进事项"></textarea></div>
<div class="form-group"><label>回访资料</label><div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap"><label class="dispatch-doc-upload-label" style="font-size:12px">选择文件<input id="customer-followup-files" type="file" multiple accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.webp" style="display:none" onchange="handleCustomerFollowupFiles(this)"/></label><span style="font-size:12px;color:#9ca3af">支持 PDF、Office、JPG、PNG、WEBP</span></div><div id="customer-followup-file-list" style="display:flex;flex-direction:column;gap:6px;margin-top:8px"></div></div>
</div>
<div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-customer-followup')">取消</button><button class="btn btn-primary" onclick="saveCustomerFollowup()">保存回访</button></div>
</div></div>
<!-- ==================== DISPATCH SCHEDULE MODAL ==================== --> <!-- ==================== DISPATCH SCHEDULE MODAL ==================== -->
<div class="modal-overlay" id="modal-dispatch-schedule"> <div class="modal-overlay" id="modal-dispatch-schedule">
<div class="modal" style="max-width:1440px;width:96%"> <div class="modal" style="max-width:1440px;width:96%">
@@ -8,6 +28,7 @@
<!-- 查询项 --> <!-- 查询项 -->
<div class="ds-search" style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:14px;padding:12px 14px;background:#f7f8fa;border:1px solid #eef0f3;border-radius:8px"> <div class="ds-search" style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:14px;padding:12px 14px;background:#f7f8fa;border:1px solid #eef0f3;border-radius:8px">
<input id="ds-q-ono" placeholder="订单号" style="width:170px;height:32px;padding:0 10px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px"/> <input id="ds-q-ono" placeholder="订单号" style="width:170px;height:32px;padding:0 10px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px"/>
<input id="ds-q-appointment-no" placeholder="预约号" style="width:150px;height:32px;padding:0 10px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px"/>
<select id="ds-q-ch" style="width:130px;height:32px;padding:0 8px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px"> <select id="ds-q-ch" style="width:130px;height:32px;padding:0 8px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px">
<option value="全美">全部渠道</option><option value="洛杉矶">洛杉矶</option><option value="纽约">纽约</option> <option value="全美">全部渠道</option><option value="洛杉矶">洛杉矶</option><option value="纽约">纽约</option>
</select> </select>
@@ -64,7 +85,7 @@
</div> </div>
</div> </div>
<div class="modal-footer" style="display:flex;justify-content:space-between;align-items:center;padding:14px 20px;border-top:1px solid #f0f0f0"> <div class="modal-footer" style="display:flex;justify-content:space-between;align-items:center;padding:14px 20px;border-top:1px solid #f0f0f0">
<span style="font-size:12px;color:#999">提示:选择订单后,将中间托盘明细拖入右侧装托盘类型,托数/立方自动汇总。</span> <span style="font-size:12px;color:#999">提示:选择订单后,将中间托盘明细拖入右侧装托盘类型,托数/立方/重量自动汇总。</span>
<div style="display:flex;gap:10px"> <div style="display:flex;gap:10px">
<button class="btn btn-default" onclick="hideModal('modal-dispatch-schedule')">取消</button> <button class="btn btn-default" onclick="hideModal('modal-dispatch-schedule')">取消</button>
<button class="btn btn-primary" onclick="preScheduleVehicle()">预排车</button> <button class="btn btn-primary" onclick="preScheduleVehicle()">预排车</button>
@@ -584,8 +605,9 @@
<div style="padding:20px 24px"> <div style="padding:20px 24px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0 16px;margin-bottom:14px"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:0 16px;margin-bottom:14px">
<div><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">集装箱号 <span style="color:#f5222d">*</span></label><input id="cost-ctNo" style="width:100%;padding:9px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;box-sizing:border-box"/></div> <div><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">集装箱号 <span style="color:#f5222d">*</span></label><input id="cost-ctNo" style="width:100%;padding:9px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;box-sizing:border-box"/></div>
<div><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">费用类型</label><select id="cost-type" style="width:100%;padding:9px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;box-sizing:border-box"><option>港口费</option><option>堆存费</option><option>修箱费</option><option>吊机费</option><option>拖车费</option><option>报关费</option><option>其他</option></select></div> <div><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">费用类型</label><select id="cost-category" style="width:100%;padding:9px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;box-sizing:border-box"><option value="应收" selected>应收</option><option value="应付">应付</option><option value="其他">其他</option></select></div>
</div> </div>
<div style="margin-bottom:14px"><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">费用项目</label><select id="cost-type" style="width:100%;padding:9px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;box-sizing:border-box"><option>港口费</option><option>堆存费</option><option>修箱费</option><option>吊机费</option><option>拖车费</option><option>报关费</option><option>其他</option></select></div>
<div id="cost-supplier-field" style="margin-bottom:14px"><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">供应商 <span style="color:#f5222d">*</span></label><select id="cost-supplier" required style="width:100%;padding:9px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;box-sizing:border-box"><option value="">请选择供应商</option></select></div> <div id="cost-supplier-field" style="margin-bottom:14px"><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">供应商 <span style="color:#f5222d">*</span></label><select id="cost-supplier" required style="width:100%;padding:9px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;box-sizing:border-box"><option value="">请选择供应商</option></select></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0 16px;margin-bottom:14px"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:0 16px;margin-bottom:14px">
<div><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">金额 <span style="color:#f5222d">*</span></label><input id="cost-amount" type="number" step="0.01" min="0" placeholder="0.00" style="width:100%;padding:9px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;box-sizing:border-box"/></div> <div><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">金额 <span style="color:#f5222d">*</span></label><input id="cost-amount" type="number" step="0.01" min="0" placeholder="0.00" style="width:100%;padding:9px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;box-sizing:border-box"/></div>
@@ -1301,6 +1323,17 @@
<div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-dispatch-docs')">关闭</button><button class="btn btn-primary" onclick="saveDispatchDocs()">保存资料</button></div> <div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-dispatch-docs')">关闭</button><button class="btn btn-primary" onclick="saveDispatchDocs()">保存资料</button></div>
</div></div> </div></div>
<!-- 派送单 BOL 打印预览 -->
<div class="modal-overlay dispatch-bol-print-modal" id="modal-dispatch-bol-print">
<div class="dispatch-bol-print-panel" role="dialog" aria-modal="true" aria-labelledby="dispatch-bol-print-title">
<div class="dispatch-bol-print-toolbar">
<div><span>BOL 打印预览</span><strong id="dispatch-bol-print-title">—</strong></div>
<div class="dispatch-bol-print-actions"><button type="button" class="btn btn-primary" onclick="printDispatchBol()">打印</button><button type="button" class="dispatch-bol-print-close" aria-label="关闭 BOL 打印预览" onclick="closeDispatchBolPrint()">&times;</button></div>
</div>
<div class="dispatch-bol-print-scroll"><div id="dispatch-bol-print-stage"><div id="dispatch-bol-print-sheet"></div></div></div>
</div>
</div>
<!-- 上传POD弹窗 --> <!-- 上传POD弹窗 -->
<div class="modal-overlay" id="modal-upload-pod"> <div class="modal-overlay" id="modal-upload-pod">
<div class="modal" style="max-width:520px"> <div class="modal" style="max-width:520px">
@@ -1498,6 +1531,20 @@
</div> </div>
</div> </div>
<!-- 异常订单处理弹窗 -->
<div class="modal-overlay" id="modal-dispatch-exception-create">
<div class="modal" style="max-width:620px">
<div class="modal-header"><span>新增异常订单</span><button type="button" aria-label="关闭" onclick="hideModal('modal-dispatch-exception-create')" class="dispatch-reject-close" style="border:0;background:transparent">&times;</button></div>
<div class="modal-body">
<div class="dispatch-exception-process-note">入仓码和尾程订单号至少填写一个,系统会自动带出关联订单信息。</div>
<div class="form-row"><div class="form-group"><label>入仓码</label><input id="dispatch-exception-create-inbound-code" placeholder="请输入入仓码" /></div><div class="form-group"><label>尾程订单号</label><input id="dispatch-exception-create-last-mile-no" placeholder="请输入尾程订单号" /></div></div>
<div class="form-row"><div class="form-group"><label>异常类型 <span style="color:#ef4444">*</span></label><select id="dispatch-exception-create-type"><option value="">请选择异常类型</option><option value="退货">退货</option><option value="拒收">拒收</option></select></div><div class="form-group"><label>处理状态</label><input value="待处理" readonly style="background:#f8fafc;color:#64748b" /></div></div>
<div class="form-row single"><div class="form-group"><label>异常原因 <span style="color:#ef4444">*</span></label><textarea id="dispatch-exception-create-reason" rows="3" placeholder="请输入异常原因"></textarea></div></div>
</div>
<div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-dispatch-exception-create')">取消</button><button class="btn btn-primary" onclick="saveDispatchExceptionCreate()">保存</button></div>
</div>
</div>
<!-- 异常订单处理弹窗 --> <!-- 异常订单处理弹窗 -->
<div class="modal-overlay" id="modal-dispatch-exception-process"> <div class="modal-overlay" id="modal-dispatch-exception-process">
<div class="modal" style="max-width:620px"> <div class="modal" style="max-width:620px">
@@ -1746,6 +1793,21 @@
<div class="modal-footer"><button type="button" class="btn btn-default" onclick="hideModal('modal-domestic-inbound-fee')">取消</button><button type="button" class="btn btn-primary" onclick="saveDomesticInboundFee()">保存</button></div> <div class="modal-footer"><button type="button" class="btn btn-default" onclick="hideModal('modal-domestic-inbound-fee')">取消</button><button type="button" class="btn btn-primary" onclick="saveDomesticInboundFee()">保存</button></div>
</div> </div>
</div> </div>
<!-- 国内仓订单转仓 -->
<div class="modal-overlay" id="modal-domestic-inbound-transfer" role="dialog" aria-modal="true" aria-labelledby="domestic-inbound-transfer-title">
<div class="modal" style="max-width:620px;width:94%">
<div class="modal-header"><span id="domestic-inbound-transfer-title">转仓</span><button type="button" class="modal-close" onclick="hideModal('modal-domestic-inbound-transfer')" aria-label="关闭">&times;</button></div>
<div class="modal-body">
<div class="form-group"><label for="domestic-inbound-transfer-orders">已选订单</label><textarea id="domestic-inbound-transfer-orders" rows="3" readonly></textarea></div>
<div class="form-row">
<div class="form-group"><label for="domestic-inbound-transfer-current">当前仓库</label><input id="domestic-inbound-transfer-current" readonly/></div>
<div class="form-group"><label for="domestic-inbound-transfer-destination">新仓库 <span style="color:#ef4444">*</span></label><select id="domestic-inbound-transfer-destination"><option value="">请选择新仓库</option><option>义乌仓</option><option>青岛仓库</option></select></div>
</div>
<div class="form-group"><label for="domestic-inbound-transfer-remark">转仓备注</label><textarea id="domestic-inbound-transfer-remark" rows="3" maxlength="200" placeholder="请输入转仓备注(选填)"></textarea></div>
</div>
<div class="modal-footer"><button type="button" class="btn btn-default" onclick="hideModal('modal-domestic-inbound-transfer')">取消</button><button type="button" class="btn btn-primary" onclick="saveDomesticInboundTransfer()">确认转仓</button></div>
</div>
</div>
<!-- 仓库管理新增/编辑弹窗 --> <!-- 仓库管理新增/编辑弹窗 -->
<div class="modal-overlay" id="modal-fba-add"> <div class="modal-overlay" id="modal-fba-add">
<div class="modal" style="max-width:560px"> <div class="modal" style="max-width:560px">
@@ -1809,3 +1871,5 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 海外仓尾程订单 - 结算 -->
<div class="modal-overlay" id="modal-lmo-settlement"><div class="modal" style="max-width:920px"><div class="modal-header"><span>尾程订单结算</span><span onclick="hideModal('modal-lmo-settlement')" style="cursor:pointer;font-size:20px">&times;</span></div><div class="modal-body"><div id="lmo-settlement-summary" class="lmo-modal-note">—</div><div style="overflow-x:auto"><table style="width:100%;min-width:760px"><thead><tr><th>费用项目</th><th>费用说明</th><th>币种</th><th style="text-align:right">金额</th><th>来源</th><th>操作</th></tr></thead><tbody id="tbody-lmo-settlement-fees"></tbody></table></div><div style="margin-top:12px"><button type="button" class="btn btn-default btn-sm" id="lmo-settlement-add-fee" onclick="addLastMileSettlementFeeRow()">+ 新增额外费用</button></div><div class="form-row single" style="margin-top:14px"><div class="form-group"><label>结算状态 *</label><select id="lmo-settlement-status"><option value="待结算">待结算</option><option value="已完成">已完成</option></select></div></div></div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-lmo-settlement')">关闭</button><button id="lmo-settlement-save" class="btn btn-primary" onclick="saveLastMileSettlement()">保存结算</button></div></div></div>
@@ -598,6 +598,10 @@ body.dashboard-fullscreen #page-dashboard>.table-card .btn{padding:4px 10px;font
.dispatch-exception-actions a:hover{text-decoration:underline} .dispatch-exception-actions a:hover{text-decoration:underline}
.dispatch-exception-actions a.danger{color:#dc2626} .dispatch-exception-actions a.danger{color:#dc2626}
.dispatch-exception-process-note{padding:10px 12px;border:1px solid #fde68a;border-radius:6px;background:#fffbeb;color:#92400e;font-size:12px;line-height:1.5;margin-bottom:14px} .dispatch-exception-process-note{padding:10px 12px;border:1px solid #fde68a;border-radius:6px;background:#fffbeb;color:#92400e;font-size:12px;line-height:1.5;margin-bottom:14px}
.customer-followup-file{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 10px;border:1px solid #e5e7eb;border-radius:6px;background:#fff}
.customer-followup-file-name{min-width:0;overflow:hidden;color:#374151;font-size:12px;text-overflow:ellipsis;white-space:nowrap}
.customer-followup-file-meta{display:block;margin-top:2px;color:#9ca3af;font-size:11px}
.customer-followup-file-delete{flex:0 0 auto;padding:3px 7px;border:1px solid #fecaca;border-radius:4px;background:#fff;color:#b91c1c;font-size:11px;cursor:pointer}
.dispatch-doc-cell{width:312px;min-width:312px;padding:2px 0} .dispatch-doc-cell{width:312px;min-width:312px;padding:2px 0}
.dispatch-doc-summary{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:7px} .dispatch-doc-summary{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:7px}
.dispatch-doc-count{font-size:11px;font-weight:600;color:#374151;white-space:nowrap} .dispatch-doc-count{font-size:11px;font-weight:600;color:#374151;white-space:nowrap}
@@ -651,7 +655,7 @@ body.dashboard-fullscreen #page-dashboard>.table-card .btn{padding:4px 10px;font
.warehouse-page-tab:hover{color:#1677ff;background:#f7faff} .warehouse-page-tab:hover{color:#1677ff;background:#f7faff}
.warehouse-page-tab.active{border-bottom-color:#1677ff;color:#1677ff;font-weight:650} .warehouse-page-tab.active{border-bottom-color:#1677ff;color:#1677ff;font-weight:650}
.warehouse-page-tab:focus-visible{outline:2px solid #1677ff;outline-offset:2px} .warehouse-page-tab:focus-visible{outline:2px solid #1677ff;outline-offset:2px}
#page-gen-owned-warehouse .warehouse-table-wrap,#page-gen-fba .warehouse-table-wrap,#page-gen-partner-warehouse .warehouse-table-wrap{width:100%;overflow-x:auto} #page-gen-owned-warehouse .warehouse-table-wrap,#page-gen-fba .warehouse-table-wrap,#page-gen-partner-warehouse .warehouse-table-wrap,#page-gen-private-warehouse .warehouse-table-wrap{width:100%;overflow-x:auto}
.warehouse-table-wrap table{min-width:1040px} .warehouse-table-wrap table{min-width:1040px}
.vehicle-owner-tabs{display:inline-flex;align-items:center;margin:14px 0 12px;padding:3px;border:1px solid #dfe5ee;border-radius:6px;background:#f4f6f9} .vehicle-owner-tabs{display:inline-flex;align-items:center;margin:14px 0 12px;padding:3px;border:1px solid #dfe5ee;border-radius:6px;background:#f4f6f9}
.vehicle-owner-tab{min-width:104px;height:32px;padding:0 16px;border:0;border-radius:4px;background:transparent;color:#5f6b7a;font-size:13px;cursor:pointer} .vehicle-owner-tab{min-width:104px;height:32px;padding:0 16px;border:0;border-radius:4px;background:transparent;color:#5f6b7a;font-size:13px;cursor:pointer}
@@ -682,7 +686,7 @@ body.dashboard-fullscreen #page-dashboard>.table-card .btn{padding:4px 10px;font
.vehicle-detail-relation.is-current{color:#1677ff;background:#eaf3ff} .vehicle-detail-relation.is-current{color:#1677ff;background:#eaf3ff}
.vehicle-detail-relation.is-history{color:#64748b;background:#f1f5f9} .vehicle-detail-relation.is-history{color:#64748b;background:#f1f5f9}
@media(max-width:900px){.vehicle-detail-head{align-items:flex-start;flex-direction:column;padding:16px 18px}.vehicle-detail-head .btn{align-self:flex-end}.vehicle-detail-body{padding:16px 18px 20px}.vehicle-detail-grid{grid-template-columns:1fr}.vehicle-region-tabs{overflow-x:auto}.vehicle-region-tab,.warehouse-page-tab{flex:1;min-width:104px;padding:0 14px}} @media(max-width:900px){.vehicle-detail-head{align-items:flex-start;flex-direction:column;padding:16px 18px}.vehicle-detail-head .btn{align-self:flex-end}.vehicle-detail-body{padding:16px 18px 20px}.vehicle-detail-grid{grid-template-columns:1fr}.vehicle-region-tabs{overflow-x:auto}.vehicle-region-tab,.warehouse-page-tab{flex:1;min-width:104px;padding:0 14px}}
@media(max-width:700px){.dispatch-doc-modal-meta,.dispatch-reject-meta{grid-template-columns:1fr}.dispatch-reject-meta>div:first-child{grid-column:auto}.dispatch-doc-row{grid-template-columns:1fr}.dispatch-doc-controls{justify-content:flex-start}.vehicle-detail-field{grid-template-columns:96px minmax(0,1fr)}.vehicle-detail-empty{position:sticky;left:0;background:#fff;text-align:left!important}} @media(max-width:700px){.dispatch-doc-modal-meta,.dispatch-reject-meta,.customer-followup-modal-meta{grid-template-columns:1fr!important}.dispatch-reject-meta>div:first-child{grid-column:auto}.customer-followup-modal-meta>div{grid-column:auto!important}.dispatch-doc-row{grid-template-columns:1fr}.dispatch-doc-controls{justify-content:flex-start}.vehicle-detail-field{grid-template-columns:96px minmax(0,1fr)}.vehicle-detail-empty{position:sticky;left:0;background:#fff;text-align:left!important}}
.inbound-action-row{display:flex;gap:8px;margin-top:12px;padding-top:12px;border-top:1px solid #e5e7eb} .inbound-action-row{display:flex;gap:8px;margin-top:12px;padding-top:12px;border-top:1px solid #e5e7eb}
.inbound-action-row .btn{padding:7px 14px;font-size:12px;border-radius:4px;font-weight:600} .inbound-action-row .btn{padding:7px 14px;font-size:12px;border-radius:4px;font-weight:600}
.inbound-button-group{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap} .inbound-button-group{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap}
@@ -981,6 +985,61 @@ body.dashboard-fullscreen #page-dashboard>.table-card .btn{padding:4px 10px;font
#booking-print-sheet{display:block!important;position:absolute!important;left:0!important;top:0!important;width:210mm!important;height:297mm!important;margin:0!important} #booking-print-sheet{display:block!important;position:absolute!important;left:0!important;top:0!important;width:210mm!important;height:297mm!important;margin:0!important}
#booking-print-sheet>.booking-print-sheet{display:block!important;width:210mm!important;min-height:297mm!important;margin:0!important;transform:none!important;box-shadow:none!important} #booking-print-sheet>.booking-print-sheet{display:block!important;width:210mm!important;min-height:297mm!important;margin:0!important;transform:none!important;box-shadow:none!important}
} }
/* ===== 派送单 BOL 模板打印 ===== */
.dispatch-bol-print-modal{padding:20px;box-sizing:border-box;background:rgba(15,23,42,.58)}
.dispatch-bol-print-panel{width:min(1120px,96vw);height:min(94vh,980px);max-width:1120px;max-height:94vh;display:flex;flex-direction:column;overflow:hidden;background:#fff;border-radius:10px;box-shadow:0 18px 48px rgba(15,23,42,.24)}
.dispatch-bol-print-toolbar{min-height:64px;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:12px 18px;border-bottom:1px solid #e5e7eb;background:#fff;box-sizing:border-box}
.dispatch-bol-print-toolbar>div:first-child{display:grid;gap:3px;min-width:0}
.dispatch-bol-print-toolbar span{font-size:11px;color:#64748b}
#dispatch-bol-print-title{font-size:15px;color:#1f2937;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.dispatch-bol-print-actions{display:flex;align-items:center;gap:8px;flex:0 0 auto}
.dispatch-bol-print-close{width:34px;height:34px;display:inline-flex;align-items:center;justify-content:center;border:0;border-radius:6px;background:transparent;color:#64748b;font-size:24px;line-height:1;cursor:pointer}
.dispatch-bol-print-close:hover{background:#f1f5f9;color:#1f2937}
.dispatch-bol-print-close:focus-visible{outline:2px solid #1677ff;outline-offset:2px}
.dispatch-bol-print-scroll{flex:1;min-height:0;overflow:auto;padding:24px;background:#eef1f5;box-sizing:border-box}
#dispatch-bol-print-stage{position:relative;margin:0 auto}
#dispatch-bol-print-sheet>.dispatch-bol-sheet{width:210mm;min-height:297mm;padding:10mm 12mm 18mm;box-sizing:border-box;background:#fff;color:#000;transform-origin:top left;box-shadow:0 4px 20px rgba(15,23,42,.14);font-family:Arial,"Microsoft YaHei",sans-serif;font-size:10.5pt;line-height:1.24;page:dispatchBol}
.dispatch-bol-sheet *{box-sizing:border-box}
.dispatch-bol-document-title{height:15mm;display:flex;align-items:center;justify-content:center;border:1px solid #000;background:#000;color:#fff;font-family:"Times New Roman","SimSun",serif;font-size:24pt;font-weight:700;line-height:1;letter-spacing:.01em}
.dispatch-bol-summary{display:grid;grid-template-columns:52.5% 47.5%;border-left:1px solid #000;border-right:1px solid #000}
.dispatch-bol-summary>section{min-width:0;border-bottom:1px solid #000}
.dispatch-bol-summary>section+section{border-left:1px solid #000}
.dispatch-bol-section-title{height:9mm;margin:0;padding:0 1mm;display:flex;align-items:center;background:#000;color:#fff;font-family:"Times New Roman","SimSun",serif;font-size:16pt;font-weight:400;line-height:1}
.dispatch-bol-section-body{padding:2mm 1.5mm}
.dispatch-bol-top .dispatch-bol-section-body{min-height:36mm;display:flex;flex-direction:column;justify-content:center}
.dispatch-bol-shipper strong{display:block;margin-bottom:1mm;font-size:16pt;line-height:1.15}
.dispatch-bol-address{white-space:pre-line;font-size:12pt;line-height:1.28}
.dispatch-bol-fields{display:grid;grid-template-columns:max-content minmax(0,1fr);align-content:center;gap:.8mm 1.5mm;font-size:11pt}
.dispatch-bol-fields dt{font-weight:400}
.dispatch-bol-fields dd{margin:0;min-width:0;overflow-wrap:anywhere}
.dispatch-bol-fields .is-emphasis{font-weight:700}
.dispatch-bol-middle .dispatch-bol-section-body{min-height:27mm;padding-top:1.5mm}
.dispatch-bol-destination-title{font-size:12pt;margin-bottom:.8mm}
.dispatch-bol-destination-line{font-size:11.5pt;line-height:1.38}
.dispatch-bol-lines{width:100%;border-collapse:collapse;table-layout:fixed;font-family:"Times New Roman","SimSun",serif;font-size:9.5pt}
.dispatch-bol-lines th,.dispatch-bol-lines td{border:1px solid #000;padding:1.8mm 1mm;text-align:center;vertical-align:middle;overflow-wrap:anywhere}
.dispatch-bol-lines th{height:13mm;background:#fff;color:#000;font-family:Arial,"Microsoft YaHei",sans-serif;font-size:9pt;line-height:1.15;white-space:normal}
.dispatch-bol-lines td{height:9mm;font-size:8.5pt;white-space:nowrap;overflow-wrap:normal}
.dispatch-bol-lines th:nth-child(1){width:9%}.dispatch-bol-lines th:nth-child(2){width:14%}.dispatch-bol-lines th:nth-child(3){width:12%}.dispatch-bol-lines th:nth-child(4){width:18%}.dispatch-bol-lines th:nth-child(5){width:22%}.dispatch-bol-lines th:nth-child(6){width:18%}.dispatch-bol-lines th:nth-child(7){width:7%}
.dispatch-bol-lines thead{display:table-header-group}
.dispatch-bol-lines tr{break-inside:avoid;page-break-inside:avoid}
.dispatch-bol-empty{height:24mm!important;color:#4b5563;font-family:Arial,"Microsoft YaHei",sans-serif!important}
.dispatch-bol-total{display:grid;grid-template-columns:1fr auto;align-items:center;gap:12mm;margin:12mm 18mm 0;font-family:"Times New Roman","SimSun",serif;font-size:12pt;break-inside:avoid;page-break-inside:avoid}
.dispatch-bol-total strong{font-family:Arial,"Microsoft YaHei",sans-serif;font-size:18pt;font-weight:400;line-height:1}
@media(max-width:680px){.dispatch-bol-print-modal{padding:8px}.dispatch-bol-print-panel{width:calc(100vw - 16px);height:calc(100vh - 16px);max-height:none}.dispatch-bol-print-toolbar{align-items:flex-start;gap:10px;padding:10px 12px;flex-wrap:wrap}.dispatch-bol-print-actions{width:100%;justify-content:flex-end}.dispatch-bol-print-scroll{padding:12px}}
@page dispatchBol{size:A4 portrait;margin:0}
@media print{
html:has(body.dispatch-bol-printing),body.dispatch-bol-printing{width:210mm!important;min-width:0!important;height:auto!important;min-height:0!important;margin:0!important;padding:0!important;background:#fff!important;overflow:visible!important}
body.dispatch-bol-printing>*:not(#modal-dispatch-bol-print){display:none!important}
body.dispatch-bol-printing #modal-booking-print-preview,body.dispatch-bol-printing #modal-ct-pallet-label-preview{display:none!important}
body.dispatch-bol-printing #modal-dispatch-bol-print{display:block!important;position:static!important;inset:auto!important;width:210mm!important;height:auto!important;padding:0!important;background:#fff!important}
body.dispatch-bol-printing #modal-dispatch-bol-print,body.dispatch-bol-printing #modal-dispatch-bol-print *{visibility:visible!important}
body.dispatch-bol-printing .dispatch-bol-print-panel{display:block!important;position:static!important;width:210mm!important;height:auto!important;max-width:none!important;max-height:none!important;border:0!important;border-radius:0!important;box-shadow:none!important;overflow:visible!important}
body.dispatch-bol-printing .dispatch-bol-print-toolbar{display:none!important}
body.dispatch-bol-printing .dispatch-bol-print-scroll{display:block!important;position:static!important;width:210mm!important;height:auto!important;padding:0!important;background:#fff!important;overflow:visible!important}
body.dispatch-bol-printing #dispatch-bol-print-stage,body.dispatch-bol-printing #dispatch-bol-print-sheet{display:block!important;position:static!important;width:210mm!important;height:auto!important;margin:0!important}
body.dispatch-bol-printing #dispatch-bol-print-sheet>.dispatch-bol-sheet{display:block!important;width:210mm!important;min-height:297mm!important;margin:0!important;transform:none!important;box-shadow:none!important}
}
/* 基本Tab只读展示 */ /* 基本Tab只读展示 */
.basic-readonly input,.basic-readonly select,.basic-readonly textarea{background:#f5f5f5;color:#666;cursor:not-allowed;pointer-events:none} .basic-readonly input,.basic-readonly select,.basic-readonly textarea{background:#f5f5f5;color:#666;cursor:not-allowed;pointer-events:none}
/* MB/L 逐行字段 */ /* MB/L 逐行字段 */
@@ -1261,6 +1320,7 @@ body.dashboard-fullscreen #page-dashboard table:has(>thead)>tbody>tr>td{backgrou
<div class="menu-label" onclick="toggleMenu(this)"><span class="icon"><svg viewBox="0 0 24 24" fill="none"><path d="M4 13v-2a8 8 0 0116 0v2" stroke="#13c2c2" stroke-width="2" stroke-linecap="round"/><path d="M4 13a2 2 0 012-2h1v6H6a2 2 0 01-2-2v-2zM20 13a2 2 0 00-2-2h-1v6h1a2 2 0 002-2v-2z" stroke="#13c2c2" stroke-width="2" stroke-linejoin="round"/><path d="M17 17c0 2-2 3-5 3" stroke="#13c2c2" stroke-width="2" stroke-linecap="round"/></svg></span>客户服务<span class="arrow">▶</span></div> <div class="menu-label" onclick="toggleMenu(this)"><span class="icon"><svg viewBox="0 0 24 24" fill="none"><path d="M4 13v-2a8 8 0 0116 0v2" stroke="#13c2c2" stroke-width="2" stroke-linecap="round"/><path d="M4 13a2 2 0 012-2h1v6H6a2 2 0 01-2-2v-2zM20 13a2 2 0 00-2-2h-1v6h1a2 2 0 002-2v-2z" stroke="#13c2c2" stroke-width="2" stroke-linejoin="round"/><path d="M17 17c0 2-2 3-5 3" stroke="#13c2c2" stroke-width="2" stroke-linecap="round"/></svg></span>客户服务<span class="arrow">▶</span></div>
<div class="sub-menu"> <div class="sub-menu">
<div class="sub-menu-item" onclick="switchPage('outbound-exception',this)">异常订单</div> <div class="sub-menu-item" onclick="switchPage('outbound-exception',this)">异常订单</div>
<div class="sub-menu-item" onclick="switchPage('customer-followup',this)">客户回访</div>
</div> </div>
</div> </div>
<div class="sidebar-item" data-menu="supplier"> <div class="sidebar-item" data-menu="supplier">
@@ -9,20 +9,22 @@
<div class="form-group"><label>排车单号</label><input id="bol-vehicle-no" readonly/></div> <div class="form-group"><label>排车单号</label><input id="bol-vehicle-no" readonly/></div>
<div class="form-group"><label>预派送日期 *</label><input id="bol-deliveryDate" type="date" required/></div> <div class="form-group"><label>预派送日期 *</label><input id="bol-deliveryDate" type="date" required/></div>
<div class="form-group"><label>派送方式 *</label><select id="bol-deliveryMethod" required onchange="handleOutboundVehicleDeliveryMethodChange()"><option value="">请选择</option><option>FBA自提</option><option>客户自提</option><option>卡派打托</option><option>卡派地板</option><option>快递</option><option>外配</option></select></div> <div class="form-group"><label>派送方式 *</label><select id="bol-deliveryMethod" required onchange="handleOutboundVehicleDeliveryMethodChange()"><option value="">请选择</option><option>FBA自提</option><option>客户自提</option><option>卡派打托</option><option>卡派地板</option><option>快递</option><option>外配</option></select></div>
<div class="form-group" id="bol-courier-company-field" hidden><label>快递公司 *</label><select id="bol-courierCompanyId"><option value="">请选择快递公司</option></select></div>
<div class="form-group"><label>提货方式 *</label><select id="bol-pickupMethod" required><option value="">请选择</option><option>一提一卸</option><option>一提多卸</option></select></div> <div class="form-group"><label>提货方式 *</label><select id="bol-pickupMethod" required><option value="">请选择</option><option>一提一卸</option><option>一提多卸</option></select></div>
<div class="form-group"><label>装车类型 *</label><select id="bol-loadType" required onchange="handleOutboundVehicleLoadTypeChange()"><option>托盘</option><option>地板</option><option>混装</option></select></div> <div class="form-group"><label>装车类型 *</label><select id="bol-loadType" required onchange="handleOutboundVehicleLoadTypeChange()"><option>托盘</option><option>地板</option><option>混装</option></select></div>
<div class="form-group"><label>车队/司机名</label><input id="bol-driverName"/></div> <div class="form-group"><label>车队/司机名</label><input id="bol-driverName"/></div>
<div class="form-group"><label>车队联系电话</label><input id="bol-driverPhone"/></div> <div class="form-group"><label>车队联系电话</label><input id="bol-driverPhone"/></div>
<div class="form-group"><label>车辆类型</label><select id="bol-vehicleType"><option value="">请选择</option><option>厢式货车</option><option>平板车</option><option>集装箱卡车</option></select></div> <div class="form-group"><label>车辆类型</label><select id="bol-vehicleType"><option value="">请选择</option><option>厢式货车</option><option>平板车</option><option>集装箱卡车</option></select></div>
<div class="form-group"><label>车辆大小</label><select id="bol-vehicleSize"><option value="">请选择</option><option>大车(30托/15托)</option><option>小车(14托/12托)</option></select></div> <div class="form-group"><label>车辆大小</label><select id="bol-vehicleSize"><option value="">请选择</option><option>大车</option><option>小车</option></select></div>
<div class="form-group"><label>预约账号</label><select id="bol-bookAccount"><option value="">请选择</option><option>4</option><option>8</option></select></div> <div class="form-group"><label>预约账号</label><select id="bol-bookAccount" onchange="handleOutboundVehicleAppointmentAccountChange()"><option value="">请选择</option><option>4</option><option>8</option><option value="买约">买约</option></select></div>
<div class="form-group"><label>预约号</label><input id="bol-appointmentNo"/></div> <div class="form-group" id="bol-seller-name-field" hidden><label>卖家姓名</label><input id="bol-sellerName" placeholder="请输入卖家姓名"/></div>
</div> </div>
<div class="outbound-vehicle-section-title">费用信息</div> <div class="outbound-vehicle-section-title">费用信息</div>
<div class="outbound-vehicle-form-grid"> <div class="outbound-vehicle-form-grid">
<div class="form-group"><label>派送费用(USD) *</label><input id="bol-dispatchFee" type="number" min="0" step="0.01" placeholder="请输入派送费用" required/></div> <div class="form-group"><label>派送费用(USD) *</label><input id="bol-dispatchFee" type="number" min="0" step="0.01" placeholder="请输入派送费用" required/></div>
<div class="form-group" id="bol-loading-fee-field" hidden><label>装车费(USD) *</label><input id="bol-loadingFee" type="number" min="0" step="0.01"/></div> <div class="form-group" id="bol-loading-fee-field" hidden><label>装车费(USD) *</label><input id="bol-loadingFee" type="number" min="0" step="0.01"/></div>
<div class="form-group" id="bol-courier-company-field" hidden><label>快递公司 *</label><select id="bol-courierCompanyId"><option value="">请选择快递公司</option></select></div> <div class="form-group" id="bol-buy-appointment-fee-field" hidden><label>买约费用(USD)</label><input id="bol-buyAppointmentFee" type="number" min="0" step="0.01" placeholder="请输入买约费用"/></div>
<div class="form-group" id="bol-buy-appointment-voucher-field" hidden><label>上传付款凭证</label><input id="bol-buyAppointmentVoucher" type="file" accept="image/*,.pdf" onchange="handleOutboundVehicleBuyAppointmentVoucher(this)"/><span id="bol-buyAppointmentVoucherName" style="display:block;margin-top:4px;color:#94a3b8;font-size:12px">未选择文件</span></div>
</div> </div>
<div class="outbound-vehicle-section-title">订单明细 <span id="bol-detail-count"></span></div> <div class="outbound-vehicle-section-title">订单明细 <span id="bol-detail-count"></span></div>
<div class="outbound-vehicle-table-wrap"><table><thead><tr><th>出库单号</th><th>重量(kg)</th><th>托盘数量</th><th>目的地FBA</th><th>体积(m³)</th><th>货柜号</th></tr></thead><tbody id="bol-detail-body"></tbody></table></div> <div class="outbound-vehicle-table-wrap"><table><thead><tr><th>出库单号</th><th>重量(kg)</th><th>托盘数量</th><th>目的地FBA</th><th>体积(m³)</th><th>货柜号</th></tr></thead><tbody id="bol-detail-body"></tbody></table></div>
@@ -66,9 +68,9 @@
</div> </div>
<!-- 第六行 --> <!-- 第六行 -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:0 16px;margin-bottom:14px"> <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:0 16px;margin-bottom:14px">
<div><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">车辆大小</label><select id="bol-vehicleSize" style="width:100%;padding:9px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;box-sizing:border-box;color:#999"><option value="">请选择</option><option>大车(30托/15托)</option><option>小车(14托/12托)</option></select></div> <div><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">车辆大小</label><select id="bol-vehicleSize" style="width:100%;padding:9px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;box-sizing:border-box;color:#999"><option value="">请选择</option><option>大车</option><option>小车</option></select></div>
<div><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">预约账号</label><select id="bol-bookAccount" style="width:100%;padding:9px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;box-sizing:border-box;color:#999"><option value="">请选择</option><option>4</option><option>8</option></select></div> <div><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">预约账号</label><select id="bol-bookAccount" style="width:100%;padding:9px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;box-sizing:border-box;color:#999"><option value="">请选择</option><option>4</option><option>8</option><option value="买约">买约</option></select></div>
<div><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">预约号</label><input id="bol-appointmentNo" placeholder="请输入预约号" style="width:100%;padding:9px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;box-sizing:border-box"/></div> <div><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">卖家姓名</label><input id="bol-sellerName" placeholder="请输入卖家姓名" style="width:100%;padding:9px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;box-sizing:border-box"/></div>
</div> </div>
<!-- 订单明细 --> <!-- 订单明细 -->
<div style="border-top:1px solid #e8e8e8;margin:18px 0 12px;padding-top:6px"> <div style="border-top:1px solid #e8e8e8;margin:18px 0 12px;padding-top:6px">
@@ -0,0 +1,55 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const root = path.resolve(__dirname, '..');
const sourceRoot = path.join(root, 'prototype-src');
const manifest = JSON.parse(fs.readFileSync(path.join(sourceRoot, 'manifest.json'), 'utf8'));
const customerServiceModule = fs.readFileSync(path.join(sourceRoot, 'modules/04a-customer-service.html'), 'utf8');
const shellSource = fs.readFileSync(path.join(sourceRoot, 'shell.html'), 'utf8');
const overlaysSource = fs.readFileSync(path.join(sourceRoot, 'overlays.html'), 'utf8');
const appSource = fs.readFileSync(path.join(sourceRoot, 'app.js'), 'utf8');
const customerServiceManifest = manifest.modules.find(module => module.file === 'modules/04a-customer-service.html');
assert.ok(customerServiceManifest, 'manifest 缺少客户服务模块');
assert.ok(customerServiceManifest.pages.includes('customer-followup'), 'manifest 未登记 customer-followup');
assert.equal((customerServiceModule.match(/id="page-customer-followup"/g) || []).length, 1, '客户回访页面必须且只能出现一次');
assert.match(customerServiceModule, /id="tbody-customer-followup"/, '客户回访页面缺少列表容器');
assert.match(shellSource, /switchPage\('customer-followup',this\)[^>]*>客户回访</, '客户服务侧栏缺少客户回访入口');
assert.match(overlaysSource, /id="modal-customer-followup"/, '缺少客户回访弹窗');
assert.match(appSource, /function renderCustomerFollowups\(/, '缺少客户回访列表渲染函数');
assert.match(appSource, /function saveCustomerFollowup\(/, '缺少客户回访保存函数');
assert.match(appSource, /#modal-customer-followup \.modal[\s\S]{0,160}scrollTop=0/, '客户回访弹窗再次打开时未回到顶部');
const startMarker = '// ==================== 客户回访 ====================';
const endMarker = '// ==================== 派送单 BOL 资料 ====================';
const start = appSource.indexOf(startMarker);
const end = appSource.indexOf(endMarker, start + startMarker.length);
assert.ok(start >= 0 && end > start, '客户回访逻辑区块缺失');
const records = [
{ dispatchNo: 'D-NEW', status: '已签收', deliveredAt: '2026-08-21 09:00' },
{ dispatchNo: 'D-EDGE', status: '已签收', signedAt: '2026-08-15 00:01' },
{ dispatchNo: 'D-OLD', status: '已签收', completedAt: '2026-08-14 23:59' },
{ dispatchNo: 'D-MOVING', status: '派送中', deliveredAt: '2026-08-21 10:00' },
{ dispatchNo: 'D-FALLBACK', status: '已签收', outboundAt: '2026-08-20 08:00' },
{ dispatchNo: 'D-NEW', status: '已签收', deliveredAt: '2026-08-20 09:00' },
];
const context = {
console,
Date,
normalizeDispatchStatus: status => String(status || ''),
staticDispatchRecords: [],
lastMileDispatchData: [],
__records: records,
};
vm.createContext(context);
vm.runInContext(
`${appSource.slice(start, end)}\n` +
`globalThis.__result = getRecentCompletedDispatches(new Date('2026-08-21T12:00:00'), __records).map(item => item.dispatchNo);`,
context,
);
assert.deepEqual(Array.from(context.__result), ['D-NEW', 'D-FALLBACK', 'D-EDGE'], '最近 7 天已完成派送筛选结果不正确');
console.log('验证通过:客户回访页面契约与最近 7 天筛选逻辑。');
@@ -4,7 +4,7 @@ const path = require('node:path');
const root = path.resolve(__dirname, '..'); const root = path.resolve(__dirname, '..');
const read = relativePath => fs.readFileSync(path.join(root, relativePath), 'utf8'); const read = relativePath => fs.readFileSync(path.join(root, relativePath), 'utf8');
const page = read('prototype-src/modules/04a-customer-service.html'); const customerService = read('prototype-src/modules/04a-customer-service.html');
const app = read('prototype-src/app.js'); const app = read('prototype-src/app.js');
const overlays = read('prototype-src/overlays.html'); const overlays = read('prototype-src/overlays.html');
const shell = read('prototype-src/shell.html'); const shell = read('prototype-src/shell.html');
@@ -16,6 +16,14 @@ const functionSource = name => {
const next = app.indexOf('\nfunction ', start + 1); const next = app.indexOf('\nfunction ', start + 1);
return app.slice(start, next === -1 ? app.length : next); return app.slice(start, next === -1 ? app.length : next);
}; };
const pageSource = (source, pageId) => {
const start = source.search(new RegExp(`<div\\b[^>]*\\bid=["']page-${pageId}["'][^>]*>`));
assert.notEqual(start, -1, `缺少 page-${pageId}`);
const rest = source.slice(start + 1);
const nextOffset = rest.search(/<div\b[^>]*\bid=["']page-/);
return source.slice(start, nextOffset === -1 ? source.length : start + 1 + nextOffset);
};
const page = pageSource(customerService, 'outbound-exception');
const staticDispatchRecords = app.match(/const staticDispatchRecords\s*=\s*\[([\s\S]*?)\n\];/); const staticDispatchRecords = app.match(/const staticDispatchRecords\s*=\s*\[([\s\S]*?)\n\];/);
assert.ok(staticDispatchRecords, 'app.js 缺少静态派送订单数据'); assert.ok(staticDispatchRecords, 'app.js 缺少静态派送订单数据');
@@ -96,10 +96,9 @@ assert.equal(
overseasDeliveryIndex + 1, overseasDeliveryIndex + 1,
'客户服务模块必须紧邻海外派送之后', '客户服务模块必须紧邻海外派送之后',
); );
assert.deepEqual( assert.ok(
customerServiceModule.pages, customerServiceModule.pages.includes('outbound-exception'),
['outbound-exception'], '客户服务模块必须归属 outbound-exception 页面',
'客户服务模块必须且只能归属 outbound-exception 页面',
); );
assert.ok( assert.ok(
!manifest.modules[overseasDeliveryIndex].pages.includes('outbound-exception'), !manifest.modules[overseasDeliveryIndex].pages.includes('outbound-exception'),
@@ -0,0 +1,27 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const root = path.resolve(__dirname, '..');
const read = relativePath => fs.readFileSync(path.join(root, relativePath), 'utf8');
const page = read('prototype-src/modules/04a-customer-service.html');
const app = read('prototype-src/app.js');
const overlays = read('prototype-src/overlays.html');
assert.match(page, /新增异常订单/, '异常订单页缺少新增异常订单按钮');
assert.match(page, /onclick="openDispatchExceptionCreate\(\)"/, '新增按钮未打开新增异常订单弹窗');
assert.match(overlays, /id="modal-dispatch-exception-create"/, '缺少新增异常订单弹窗');
assert.match(overlays, /id="dispatch-exception-create-inbound-code"/, '弹窗缺少入仓码字段');
assert.match(overlays, /id="dispatch-exception-create-last-mile-no"/, '弹窗缺少尾程订单号字段');
assert.match(overlays, /id="dispatch-exception-create-type"/, '弹窗缺少异常类型字段');
assert.match(overlays, /id="dispatch-exception-create-reason"/, '弹窗缺少异常原因字段');
assert.match(app, /function openDispatchExceptionCreate\(/, '缺少新增异常订单打开函数');
assert.match(app, /function saveDispatchExceptionCreate\(/, '缺少新增异常订单保存函数');
assert.match(app, /入仓码或尾程订单号至少填写一个/, '缺少关联编号至少填写一个校验');
assert.match(app, /未找到对应订单/, '缺少关联订单不存在提示');
assert.match(app, /manualDispatchExceptionRecords/, '缺少手动异常订单数据集合');
assert.match(app, /record\.source===['"]manual['"]\?record\.exceptionNo/, '手动异常订单未按异常订单号独立去重');
assert.match(app, /record\.source===['"]manual['"]\?record\.exceptionNo:\(record\.dispatchNo\|\|record\.exceptionNo\)/, '手动异常订单处理入口未使用异常订单号');
assert.match(app, /record\.createdAt/, '手动异常订单未显示创建时间');
console.log('CUSTOMER_SERVICE_MANUAL_EXCEPTION_CHECK_PASS');
@@ -0,0 +1,97 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const root = path.resolve(__dirname, '..');
const moduleSource = fs.readFileSync(path.join(root, 'prototype-src/modules/04-overseas-delivery.html'), 'utf8');
const overlaySource = fs.readFileSync(path.join(root, 'prototype-src/overlays.html'), 'utf8');
const shellSource = fs.readFileSync(path.join(root, 'prototype-src/shell.html'), 'utf8');
const appSource = fs.readFileSync(path.join(root, 'prototype-src/app.js'), 'utf8');
const completeSource = fs.readFileSync(path.join(root, 'index.html'), 'utf8');
const dispatchTable = moduleSource.match(/id="dispatch-table-scroll"[\s\S]*?<tbody id="tbody-outbound-dispatch">([\s\S]*?)<\/tbody>/);
assert.ok(dispatchTable, '缺少派送单列表主表');
const staticRows = [...dispatchTable[1].matchAll(/<tr data-dispatch-no="(YWBHK20061500[1-5])">([\s\S]*?)<\/tr>/g)];
assert.equal(staticRows.length, 5, '派送单静态示例行数量异常');
staticRows.forEach((row, index) => {
const bolNo = `BOL2026061500${index + 1}`;
assert.match(row[2], new RegExp(`openDispatchBolPrint\\('${bolNo}'\\)`), `${row[1]} 缺少对应的打印 BOL 调用`);
assert.match(row[2], /class="dispatch-bol-print-action"[^>]*>打印 BOL<\/a>/, `${row[1]} 的“打印 BOL”入口缺少防重复标识`);
assert.match(row[2], />编辑车队<\/a>/, `${row[1]} 的“编辑车队”操作被移除`);
});
assert.match(overlaySource, /id="modal-dispatch-bol-print"/, '缺少 BOL 打印预览弹窗');
assert.match(overlaySource, /id="dispatch-bol-print-title"/, 'BOL 打印预览缺少标题');
assert.match(overlaySource, /id="dispatch-bol-print-sheet"/, 'BOL 打印预览缺少单据容器');
assert.match(overlaySource, /onclick="printDispatchBol\(\)"/, 'BOL 打印预览缺少打印按钮');
const printDataBlock = appSource.match(/const dispatchBolPrintData\s*=\s*\[([\s\S]*?)\n\];/);
assert.ok(printDataBlock, '缺少派送单 BOL 打印数据');
const dispatchBolPrintData = vm.runInNewContext(`[${printDataBlock[1]}]`);
assert.equal(dispatchBolPrintData.length, 5, 'BOL 打印示例数据不是 5 条');
dispatchBolPrintData.forEach((record, index) => {
const dispatchNo = `YWBHK20061500${index + 1}`;
const bolNo = `BOL2026061500${index + 1}`;
assert.equal(record.dispatchNo, dispatchNo, `${dispatchNo} BOL 数据缺少派送单号`);
assert.equal(record.bolNo, bolNo, `${dispatchNo} BOL 数据缺少对应内部 bolNo`);
['trailerNo', 'sealNo', 'displayBolNo', 'shipmentId', 'poNo', 'fbaAddress'].forEach((field) => {
assert.equal(record[field], '', `${dispatchNo} BOL 数据 ${field} 必须为空白`);
});
assert.ok(Array.isArray(record.lines) && record.lines.length > 0, `${dispatchNo} BOL 数据必须保留明细`);
record.lines.forEach((line) => {
assert.ok(typeof line.poNo === 'string' && line.poNo.length > 0, `${dispatchNo} 每条 BOL 明细必须保留非空 poNo`);
});
});
assert.match(appSource, /function dispatchBolPrintBlankText\(value\)/, '缺少 BOL 空白字段格式化函数');
const blankTextBlock = appSource.match(/function dispatchBolPrintBlankText\(value\)\{([\s\S]*?)\n\}/);
assert.ok(blankTextBlock, '无法读取 BOL 空白字段格式化函数');
assert.ok(blankTextBlock[1].includes("value===undefined||value===null||String(value).trim()===''?'':String(value)"), 'BOL 空白字段必须保持为空字符串');
assert.match(blankTextBlock[1], /return lmoEsc\(text\)/, 'BOL 空白字段仍需经过 HTML 转义');
const renderDispatchBolPrintBlock = appSource.match(/function renderDispatchBolPrint\(data\)\{([\s\S]*?)\n\}\nfunction openDispatchBolPrint\(bolNo\)/);
assert.ok(renderDispatchBolPrintBlock, '缺少 BOL 模板渲染函数');
const renderDispatchBolPrintSource = renderDispatchBolPrintBlock[1];
assert.match(renderDispatchBolPrintSource, /const blank=dispatchBolPrintBlankText;/, 'BOL 模板未绑定空白字段格式化函数');
['trailerNo', 'sealNo', 'displayBolNo', 'shipmentId', 'poNo', 'fbaAddress'].forEach((field) => {
assert.match(renderDispatchBolPrintSource, new RegExp(`blank\\(data\\.${field}\\)`), `BOL 模板 ${field} 未使用 blank(...)`);
});
assert.match(renderDispatchBolPrintSource, /<td>\$\{t\(line\.poNo\)\}<\/td>/, 'BOL 明细 poNo 必须继续使用 t(line.poNo)');
assert.doesNotMatch(renderDispatchBolPrintSource, /t\(data\.bolNo\)/, '模板不得显示内部 bolNo');
['Container #', 'Pallet #', 'FBA Warehouse #', 'Warehouse No #', 'FBA #', 'PO #', 'CTNS'].forEach((header) => {
assert.ok(appSource.includes(header), `BOL 模板缺少表头 ${header}`);
});
['BILL OF LADING', 'SHIP FROM', 'CARRIER', 'SHIP TO', 'REFERENCE'].forEach((heading) => {
assert.ok(appSource.includes(heading), `BOL 模板缺少标题 ${heading}`);
});
const lineCount = (printDataBlock[1].match(/containerNo:/g) || []).length;
const ctnsCount = (printDataBlock[1].match(/ctns:\d+/g) || []).length;
assert.ok(lineCount > 0, 'BOL 打印示例数据缺少明细');
assert.equal(ctnsCount, lineCount, '每条 BOL 明细必须独立维护非负整数 ctns');
assert.match(printDataBlock[1], /palletNo:'MSKU-01'[\s\S]*?palletNo:'MSKU-01'/, '示例数据缺少连续相同托盘,无法验收纵向合并');
assert.match(appSource, /function getDispatchBolPalletRowspans\(lines\)/, '缺少连续托盘分组函数');
assert.match(appSource, /rowspan="\$\{palletRowspans\[index\]\}"/, '连续相同托盘未通过 rowspan 纵向合并');
assert.match(appSource, /function normalizeDispatchBolCtns\(value\)[\s\S]*?Number\.isFinite\(Number\(value\)\)/, 'CTNS 未按独立数值字段规范化');
assert.match(appSource, /reduce\(\(sum,line\)=>sum\+normalizeDispatchBolCtns\(line\.ctns\),0\)/, '总箱数未对全部明细 CTNS 求和');
assert.match(appSource, /总箱数 \/ Total CTNS:/, 'BOL 模板缺少总箱数标签');
assert.match(appSource, /colspan="7" class="dispatch-bol-empty"/, '空明细未适配七列表格');
assert.match(appSource, /function openDispatchBolPrint\(bolNo\)/, '缺少打开 BOL 打印预览函数');
assert.match(appSource, /未找到该派送单的 BOL 打印数据/, '缺少 BOL 打印数据错误提示');
assert.match(appSource, /class="dispatch-bol-print-action"/, '动态派送单未增加打印 BOL 入口');
assert.match(appSource, /document\.body\.classList\.add\('dispatch-bol-printing'\)/, '打印前未启用 BOL 打印隔离状态');
assert.match(appSource, /window\.print\(\)/, 'BOL 打印未调用浏览器打印');
assert.match(shellSource, /\.dispatch-bol-sheet/, '缺少 BOL 单据样式');
assert.match(shellSource, /@page dispatchBol\{size:A4 portrait/, '缺少 BOL A4 纵向页面规则');
assert.match(shellSource, /body\.dispatch-bol-printing/, '缺少 BOL 打印隔离样式');
assert.match(shellSource, /\.dispatch-bol-document-title\{[^}]*background:#000[^}]*color:#fff/, 'BOL 主标题未使用黑底白字');
assert.match(shellSource, /\.dispatch-bol-section-title\{[^}]*background:#000[^}]*color:#fff/, 'BOL 分区标题未使用黑底白字');
assert.match(shellSource, /\.dispatch-bol-lines th\{[^}]*background:#fff[^}]*color:#000/, 'BOL 明细表头未保持模板白底黑字');
assert.match(shellSource, /\.dispatch-bol-lines td\{[^}]*white-space:nowrap/, 'BOL 明细值未保持单行排版');
assert.match(shellSource, /\.dispatch-bol-lines th:nth-child\(7\)\{width:7%\}/, 'BOL 明细未设置 CTNS 列宽');
assert.match(shellSource, /\.dispatch-bol-total\{[^}]*display:grid[^}]*grid-template-columns:1fr auto/, '总箱数未按左标签、右数字布局');
assert.match(completeSource, /id="modal-dispatch-bol-print"/, '完整原型未包含 BOL 打印预览弹窗,请重新构建');
assert.match(completeSource, /function openDispatchBolPrint\(bolNo\)/, '完整原型未包含 BOL 打印逻辑,请重新构建');
console.log('验证通过:派送单列表已接入基于模板的 BOL 预览与 A4 打印。');
@@ -0,0 +1,39 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const root = path.resolve(__dirname, '..');
const targets = [
['维护源码', path.join(root, 'prototype-src/modules/04-overseas-delivery.html')],
['完整入口', path.join(root, 'index.html')],
];
function verifyDispatchTable(label, filePath) {
const html = fs.readFileSync(filePath, 'utf8');
const tableMatch = html.match(/<div class="inbound-table-container" id="dispatch-table-scroll"[\s\S]*?<table[\s\S]*?<\/table>/);
assert.ok(tableMatch, `${label}缺少派送单主表`);
const table = tableMatch[0];
const headMatch = table.match(/<thead>[\s\S]*?<tr>([\s\S]*?)<\/tr>[\s\S]*?<\/thead>/);
assert.ok(headMatch, `${label}缺少派送单表头`);
const headers = [...headMatch[1].matchAll(/<th(?:\s[^>]*)?>([\s\S]*?)<\/th>/g)]
.map((match) => match[1].replace(/<[^>]+>/g, '').trim());
assert.ok(!headers.includes('编辑'), `${label}的派送单表头仍包含独立“编辑”字段`);
const bodyMatch = table.match(/<tbody id="tbody-outbound-dispatch">([\s\S]*?)<\/tbody>/);
assert.ok(bodyMatch, `${label}缺少派送单表体`);
const rows = [...bodyMatch[1].matchAll(/<tr data-dispatch-no="[^"]+">([\s\S]*?)<\/tr>/g)];
assert.equal(rows.length, 5, `${label}的派送单示例行数量异常`);
for (const row of rows) {
const cells = [...row[1].matchAll(/<td(?:\s[^>]*)?>[\s\S]*?<\/td>/g)];
assert.equal(cells.length, headers.length, `${label}的派送单表头与数据列数量不一致`);
assert.match(row[1], />编辑车队<\/a>/, `${label}的派送单行缺少“编辑车队”操作`);
assert.doesNotMatch(row[1], /<td><a[^>]*>编辑<\/a><\/td>/, `${label}的派送单行仍包含独立“编辑”字段`);
}
}
for (const [label, filePath] of targets) verifyDispatchTable(label, filePath);
console.log('验证通过:派送单列表已移除独立“编辑”字段,并保留 5 个“编辑车队”操作。');
@@ -0,0 +1,147 @@
#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const root = path.resolve(__dirname, '..');
const moduleHtml = fs.readFileSync(path.join(root, 'prototype-src', 'modules', '02-domestic-warehouse.html'), 'utf8');
const overlays = fs.readFileSync(path.join(root, 'prototype-src', 'overlays.html'), 'utf8');
const app = fs.readFileSync(path.join(root, 'prototype-src', 'app.js'), 'utf8');
const failures = [];
function expect(condition, message) {
if (!condition) failures.push(message);
}
function functionSource(name) {
const match = new RegExp(`function\\s+${name}\\s*\\([^)]*\\)\\s*\\{`).exec(app);
if (!match) return '';
const braceStart = app.indexOf('{', match.index);
let depth = 0;
let quote = '';
let escaped = false;
for (let index = braceStart; index < app.length; index += 1) {
const char = app[index];
if (quote) {
if (escaped) escaped = false;
else if (char === '\\') escaped = true;
else if (char === quote) quote = '';
continue;
}
if (char === "'" || char === '"' || char === '`') {
quote = char;
continue;
}
if (char === '{') depth += 1;
if (char === '}' && --depth === 0) return app.slice(match.index, index + 1);
}
return '';
}
const batchEntries = moduleHtml.match(/onclick="openDomesticInboundBatchTransfer\(\)"/g) || [];
expect(batchEntries.length === 6, '国内仓订单列表的 6 个状态视图都应提供批量转仓入口');
expect(/data-inbound-code="EXP260814001"[\s\S]*?data-inbound-status="已入库"/.test(moduleHtml), '应提供可验收的已入库示例订单');
expect(/openDomesticInboundTransfer\(this\)[^>]*>转仓<\/a>/.test(moduleHtml), '已入库订单的更多菜单应提供转仓入口');
expect(/id="modal-domestic-inbound-transfer"/.test(overlays), '应提供国内仓转仓弹窗');
expect(/id="domestic-inbound-transfer-orders"/.test(overlays), '转仓弹窗应展示已选订单');
expect(/id="domestic-inbound-transfer-current"/.test(overlays), '转仓弹窗应展示当前仓库');
expect(/id="domestic-inbound-transfer-destination"/.test(overlays), '转仓弹窗应提供新仓库选择');
expect(/id="domestic-inbound-transfer-remark"/.test(overlays), '转仓弹窗应提供转仓备注');
const functionNames = [
'getDomesticInboundRowData',
'getSelectedDomesticInboundRows',
'openDomesticInboundTransferModal',
'openDomesticInboundTransfer',
'openDomesticInboundBatchTransfer',
'saveDomesticInboundTransfer',
];
const sources = functionNames.map(functionSource);
functionNames.forEach((name, index) => expect(Boolean(sources[index]), `缺少 ${name} 函数`));
function makeRow({ code, status, warehouse, checked = false }) {
const checkbox = { checked, closest(selector) { return selector === 'tr' ? row : null; } };
const cells = Array.from({ length: 9 }, () => ({ textContent: '' }));
cells[2].textContent = code;
cells[3].textContent = status;
cells[8].textContent = warehouse;
const row = {
cells,
dataset: { inboundCode: code, inboundStatus: status },
querySelector(selector) { return selector === 'input[type="checkbox"]' ? checkbox : null; },
};
return { row, checkbox, cells, code, status, warehouse };
}
if (sources.every(Boolean)) {
try {
const inboundRow = makeRow({ code: 'EXP260814001', status: '已入库', warehouse: '义乌仓' });
const pendingRow = makeRow({ code: 'PCKHK2606151047', status: '待接收审核', warehouse: '义乌仓' });
const rows = [inboundRow, pendingRow];
const fields = {
'domestic-inbound-transfer-orders': { value: '' },
'domestic-inbound-transfer-current': { value: '' },
'domestic-inbound-transfer-destination': { value: '' },
'domestic-inbound-transfer-remark': { value: '' },
};
const calls = { modal: 0, hidden: 0, selectionUpdates: 0, toasts: [] };
const panel = {
querySelectorAll(selector) {
if (selector === 'table tbody input[type="checkbox"]:checked') {
return rows.filter(item => item.checkbox.checked).map(item => item.checkbox);
}
return [];
},
};
const context = {
document: { getElementById: id => fields[id] },
getVisibleInboundPanel: () => panel,
showModal() { calls.modal += 1; },
hideModal() { calls.hidden += 1; },
updateInboundSelectionCount() { calls.selectionUpdates += 1; },
showToast(message, type) { calls.toasts.push({ message, type }); },
};
vm.createContext(context);
vm.runInContext(`let domesticInboundTransferRows=[];\n${sources.join('\n')}`, context, { timeout: 1000 });
context.openDomesticInboundBatchTransfer();
expect(calls.toasts.at(-1)?.message === '请先选择需要转仓的订单', '空选择应被拦截');
inboundRow.checkbox.checked = true;
pendingRow.checkbox.checked = true;
context.openDomesticInboundBatchTransfer();
expect(calls.toasts.at(-1)?.message.includes(pendingRow.code), '混入非已入库订单时应提示入仓码');
expect(calls.modal === 0, '混入非已入库订单时不得打开弹窗');
pendingRow.checkbox.checked = false;
context.openDomesticInboundBatchTransfer();
expect(calls.modal === 1, '只选择已入库订单时应打开转仓弹窗');
expect(fields['domestic-inbound-transfer-orders'].value.includes(inboundRow.code), '弹窗应展示已选入仓码');
expect(fields['domestic-inbound-transfer-current'].value === inboundRow.warehouse, '弹窗应展示当前仓库');
fields['domestic-inbound-transfer-destination'].value = inboundRow.warehouse;
context.saveDomesticInboundTransfer();
expect(calls.toasts.at(-1)?.message === '新仓库不能与当前仓库相同', '同仓转仓应被拦截');
fields['domestic-inbound-transfer-destination'].value = '青岛仓库';
fields['domestic-inbound-transfer-remark'].value = '转至青岛作业';
context.saveDomesticInboundTransfer();
expect(inboundRow.cells[8].textContent === '青岛仓库', '保存后应更新仓库列');
expect(inboundRow.row.dataset.transferRemark === '转至青岛作业', '保存后应保留转仓备注');
expect(inboundRow.checkbox.checked === false, '保存后应清空选择');
expect(calls.hidden === 1, '保存后应关闭转仓弹窗');
expect(calls.selectionUpdates === 1, '保存后应刷新选择计数');
expect(calls.toasts.at(-1)?.type === 'success', '保存后应显示成功提示');
} catch (error) {
failures.push(`国内仓转仓行为执行失败:${error.message}`);
}
}
if (failures.length) {
console.error('DOMESTIC_INBOUND_WAREHOUSE_TRANSFER_CHECK_FAIL');
failures.forEach(message => console.error(`- ${message}`));
process.exitCode = 1;
} else {
console.log('DOMESTIC_INBOUND_WAREHOUSE_TRANSFER_CHECK_OK');
}
@@ -0,0 +1,17 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const root = path.resolve(__dirname, '..');
const overlays = fs.readFileSync(path.join(root, 'prototype-src', 'overlays.html'), 'utf8');
const app = fs.readFileSync(path.join(root, 'prototype-src', 'app.js'), 'utf8');
assert.match(overlays, /id="ds-q-appointment-no"[^>]*placeholder="预约号"/, '智能排车缺少预约号搜索输入框');
assert.match(app, /let dsAppointmentNo='';/, '智能排车缺少预约号筛选状态');
assert.match(app, /dsAppointmentNo='';[\s\S]{0,200}document\.getElementById\('ds-q-appointment-no'\)/, '打开智能排车时未重置预约号');
assert.match(app, /dsAppointmentNo=\(document\.getElementById\('ds-q-appointment-no'\)\.value\|\|''\)\.trim\(\)/, '查询时未读取预约号');
assert.match(app, /document\.getElementById\('ds-q-appointment-no'\)\.value='';/, '重置时未清空预约号');
assert.match(app, /!dsAppointmentNo\|\|String\(o\.appointmentNo\|\|''\)\.toLowerCase\(\)\.includes\(dsAppointmentNo\.toLowerCase\(\)\)/, '订单列表未按预约号筛选');
assert.match(app, /dispatch\.appointmentNo=no;dispatch\.appointmentDate=date;/, '入库单预约保存未同步预约号到智能排车订单');
console.log('验证通过:智能排车支持预约号搜索。');
@@ -0,0 +1,23 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const root = path.resolve(__dirname, '..');
const appJs = fs.readFileSync(path.join(root, 'prototype-src', 'app.js'), 'utf8');
const renderOrders = appJs.match(/function\s+renderDispatchOrders\s*\(\s*\)\s*\{([\s\S]*?)\n\}/);
assert.ok(renderOrders, 'app.js 缺少 renderDispatchOrders()');
assert.match(renderOrders[1], /体积<\/th>[\s\S]*?重量<\/th>/, '智能排车列表应在体积后展示重量');
assert.match(renderOrders[1], /o\.vol[\s\S]*?o\.weight/, '智能排车列表重量应读取订单重量字段');
assert.match(appJs, /formatDispatchWeight\(o\.weight\)/, '智能排车列表重量应显示 LB 单位');
const renderTrucks = appJs.match(/function\s+renderDispatchTrucks\s*\(\s*\)\s*\{([\s\S]*?)\n\}/);
assert.ok(renderTrucks, 'app.js 缺少 renderDispatchTrucks()');
assert.match(renderTrucks[1], /立方 <b>[\s\S]*?重量 <b>/, '装托类型应在立方后展示重量汇总');
assert.match(renderTrucks[1], /formatDispatchWeight\(t\.weightCapacity\|\|34000\)/, '装托类型应持续展示 34000LB 重量容量');
assert.match(renderTrucks[1], /item\.weight/, '装托类型重量应按托盘重量汇总');
assert.match(appJs, /weightCapacity\s*:\s*34000/, '默认装托类型应显示 34000LB 重量容量');
assert.match(appJs, /function\s+getDispatchPalletByKey[\s\S]*?weight\s*:/, '托盘数据应携带重量用于装托汇总');
console.log('FBA inbound smart schedule weight verification passed.');
@@ -103,8 +103,16 @@ const julyRow = api.calculateReceivableReportRow({
assert.equal(julyRow.monthKey, '2026-07', '行数据必须记录所选月份'); assert.equal(julyRow.monthKey, '2026-07', '行数据必须记录所选月份');
assert.deepEqual( assert.deepEqual(
JSON.parse(JSON.stringify(julyRow.month)), JSON.parse(JSON.stringify(julyRow.month)),
{direct: 100, special: 20}, {
'行数据只能读取所选月份的 FBA 金额', firstLeg: 100,
relabel: 20,
warehouseTransit: 0,
goodsDisposal: 0,
returnRelabel: 0,
usDomesticTruck: 0,
usDomesticLooseBoard: 0,
},
'行数据只能读取所选月份的 7 项业务金额',
); );
assert.equal(julyRow.receivableAmount, 120, '应收总金额必须为所选月份两项业务金额之和'); assert.equal(julyRow.receivableAmount, 120, '应收总金额必须为所选月份两项业务金额之和');
assert.equal(julyRow.receivedAmount, 90, '已收合计必须读取所选月份金额'); assert.equal(julyRow.receivedAmount, 90, '已收合计必须读取所选月份金额');
@@ -118,7 +126,15 @@ const missingRow = api.calculateReceivableReportRow({
}, '2026-08'); }, '2026-08');
assert.deepEqual( assert.deepEqual(
JSON.parse(JSON.stringify(missingRow.month)), JSON.parse(JSON.stringify(missingRow.month)),
{direct: null, special: null}, {
firstLeg: null,
relabel: null,
warehouseTransit: null,
goodsDisposal: null,
returnRelabel: null,
usDomesticTruck: null,
usDomesticLooseBoard: null,
},
'缺失月份必须返回空业务金额', '缺失月份必须返回空业务金额',
); );
assert.equal(missingRow.receivableAmount, 0, '缺失月份应收金额必须为 0'); assert.equal(missingRow.receivableAmount, 0, '缺失月份应收金额必须为 0');
@@ -150,11 +166,12 @@ assert.deepEqual(invalidRecord, {customer: '保留客户', remark: '保留备注
const header = api.buildReceivableReportHeader('2026-07'); const header = api.buildReceivableReportHeader('2026-07');
assert.equal(typeof header, 'string', '单月表头必须生成 HTML 字符串'); assert.equal(typeof header, 'string', '单月表头必须生成 HTML 字符串');
assert.equal(countMatches(header, /<tr>/g), 1, '标准列表必须使用单层表头'); assert.equal(countMatches(header, /<tr>/g), 1, '标准列表必须使用单层表头');
assert.equal(countMatches(header, /<th\b/g), 12, '标准列表必须固定 12 列'); assert.equal(countMatches(header, /<th\b/g), 17, '标准列表必须固定 17 列');
assert.match(header, /<th>操作<\/th><\/tr>$/, '操作必须是最后一列'); assert.match(header, /<th>操作<\/th><\/tr>$/, '操作必须是最后一列');
assert.equal(countMatches(header, /ardr-month-group/g), 0, '表头不应生成月份分组'); assert.equal(countMatches(header, /ardr-month-group/g), 0, '表头不应生成月份分组');
assert.equal(countMatches(header, /FBA直发/g), 1, '表头只能生成一个 FBA直发'); ['头程', '换标', '仓储转运', '货物废弃', '退货换标', '美国本土卡', '美本土散板']
assert.equal(countMatches(header, /FBA专线/g), 1, '表头只能生成一个 FBA专线'); .forEach(label => assert.equal(countMatches(header, new RegExp(`<th>${label}<\/th>`, 'g')), 1, `表头必须包含一个 ${label}`));
assert.doesNotMatch(header, /FBA直发|FBA专线/, '表头不应继续使用旧 FBA 业务列');
assert.ok(!header.includes('2026.07'), '表头不应显示所选月份'); assert.ok(!header.includes('2026.07'), '表头不应显示所选月份');
assert.match(appJs, /if\(page==='finance-ar-detail-report'\)renderReceivableDetailReport\(\)/, 'switchPage 未初始化应收款统计表'); assert.match(appJs, /if\(page==='finance-ar-detail-report'\)renderReceivableDetailReport\(\)/, 'switchPage 未初始化应收款统计表');
@@ -0,0 +1,119 @@
#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const root = path.resolve(__dirname, '..');
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'prototype-src', 'manifest.json'), 'utf8'));
const moduleHtml = fs.readFileSync(path.join(root, 'prototype-src', 'modules', '03-overseas-warehouse.html'), 'utf8');
const appJs = fs.readFileSync(path.join(root, 'prototype-src', 'app.js'), 'utf8');
const failures = [];
function requireMatch(source, pattern, label) {
if (!pattern.test(source)) failures.push(label);
}
function functionSource(name) {
const start = appJs.indexOf(`function ${name}(`);
if (start < 0) return '';
const braceStart = appJs.indexOf('{', start);
let depth = 0; let quote = ''; let escaped = false;
for (let index = braceStart; index < appJs.length; index += 1) {
const char = appJs[index];
if (quote) {
if (escaped) escaped = false;
else if (char === '\\') escaped = true;
else if (char === quote) quote = '';
continue;
}
if (char === "'" || char === '"' || char === '`') { quote = char; continue; }
if (char === '{') depth += 1;
if (char === '}' && --depth === 0) return appJs.slice(start, index + 1);
}
return '';
}
requireMatch(moduleHtml, /modal-lmo-detention/, 'missing detention modal');
requireMatch(moduleHtml, /name="lmo-detention-active"[\s\S]*value="1"[\s\S]*value="0"/, 'missing editable detention status');
requireMatch(moduleHtml, /lmo-detention-chargeable/, 'missing chargeable field');
requireMatch(moduleHtml, /lmo-detention-reason/, 'missing detention reason field');
requireMatch(moduleHtml, /lmo-detention-relabel/, 'missing release relabel switch');
requireMatch(moduleHtml, /lmo-detention-relabel-type/, 'missing release relabel type');
requireMatch(moduleHtml, /lmo-detention-relabel-file/, 'missing release relabel file');
for (const label of ['换箱唛', '换SKU标', '换箱唛和SKU标']) requireMatch(moduleHtml, new RegExp(label), `missing relabel type ${label}`);
requireMatch(appJs, /function openLastMileDetention\(/, 'missing openLastMileDetention');
requireMatch(appJs, /function saveLastMileDetention\(/, 'missing saveLastMileDetention');
requireMatch(appJs, /function createRelabelOrderFromLastMile\(/, 'missing release relabel helper');
requireMatch(appJs, /createRelabelOrderFromLastMile\(/, 'release does not generate relabel order');
requireMatch(appJs, /detention\s*:/, 'missing detention data');
requireMatch(appJs, /暂扣.*操作|openLastMileDetention/, 'missing detention action');
requireMatch(appJs, /function renderLastMileDirectActions[\s\S]*openLastMileDetention/, 'missing visible detention action');
if (/批量暂扣/.test(moduleHtml)) failures.push('batch detention action should not be present');
if (/更改暂扣/.test(appJs)||/更改暂扣/.test(moduleHtml)) failures.push('detention action should use release wording');
const getFirstLeg = functionSource('getLastMileFirstLegOrder');
const isDetained = functionSource('isLastMileOrderDetained');
const saveDetention = functionSource('saveLastMileDetention');
const createRelabel = functionSource('createRelabelOrderFromLastMile');
if (getFirstLeg && isDetained && saveDetention) {
try {
const order = {no:'LMO-STATUS',firstLegNo:'FL-HOLD',detention:{active:true,chargeable:true,reason:'待核查'},logs:[]};
let activeValue = '0';
let chargeableValue = '0';
let reasonValue = '问题已处理';
const calls = {hidden:0,refresh:0,toasts:[]};
const context = {
lastMileFirstLegSamples:[{firstLegNo:'FL-HOLD',isDetained:true}],
lastMileOrderData:[order],currentLastMileActionNo:order.no,
document:{
getElementById(id){return id==='lmo-detention-reason'?{value:reasonValue}:null;},
querySelector(selector){
if (selector==='input[name="lmo-detention-active"]:checked') return {value:activeValue};
if (selector==='input[name="lmo-detention-chargeable"]:checked') return {value:chargeableValue};
return null;
}
},
lmoNow:()=> '2026-08-21 10:00',hideModal(){calls.hidden++;},
refreshLastMileViews(){calls.refresh++;},showToast(message,type){calls.toasts.push({message,type});}
};
vm.createContext(context);
vm.runInContext(`${getFirstLeg}\n${isDetained}\n${saveDetention}`, context, {timeout:1000});
context.saveLastMileDetention();
requireMatch(JSON.stringify(order), /"active":false/, 'release should save inactive detention status');
if (context.isLastMileOrderDetained(order) !== false) failures.push('explicit release should override inherited first-leg detention');
if (!order.logs.some(log=>log.node==='解除暂扣')) failures.push('release should write a release log');
activeValue = '1'; chargeableValue = '1'; reasonValue = '等待客户确认';
context.saveLastMileDetention();
if (context.isLastMileOrderDetained(order) !== true) failures.push('detention status should be changeable back to active');
if (!order.logs.some(log=>log.node==='暂扣')) failures.push('detention should write a detention log');
} catch (error) {
failures.push(`detention status behavior failed: ${error.message}`);
}
}
if (createRelabel) {
try {
const order={no:'LMO-RELABEL',owner:'测试货主',boxes:4,cargo:[{sku:'SKU-1',cnName:'测试品'}],logs:[]};
const relabels=[];
const context={order,fbaRelabelOrderData:relabels,nextFbaRelabelOrderNo:()=> 'FBRO20260821001',lmoEsc:v=>String(v),fbroNowParts:()=>({time:'2026-08-21 10:00'})};
vm.createContext(context);
vm.runInContext(`${createRelabel}\ncreateRelabelOrderFromLastMile(order,'换SKU标','docs.pdf','2026-08-21 10:00');`, context, {timeout:1000});
if(relabels.length!==1||relabels[0].status!=='待处理'||relabels[0].labelReq!=='换SKU标'||relabels[0].relatedOrderNo!=='LMO-RELABEL') failures.push('release relabel helper should create pending linked order');
} catch(error) { failures.push(`release relabel behavior failed: ${error.message}`); }
}
if (saveDetention) {
try {
const order={no:'LMO-SAVE-RELABEL',boxes:2,cargo:[{sku:'SKU-2',cnName:'换标品'}],detention:{active:true,chargeable:true,reason:'待处理'},logs:[]};
const relabels=[];const context={lastMileOrderData:[order],currentLastMileActionNo:order.no,fbaRelabelOrderData:relabels,nextFbaRelabelOrderNo:()=> 'FBRO20260821002',lastMileFirstLegSamples:[],document:{getElementById(id){if(id==='lmo-detention-reason')return {value:'解除原因'};if(id==='lmo-detention-relabel-type')return {value:'换箱唛'};if(id==='lmo-detention-relabel-file')return {files:[{name:'label.pdf'}]};return null;},querySelector(selector){if(selector.includes('active'))return {value:'0'};if(selector.includes('chargeable'))return {value:'1'};if(selector.includes('relabel'))return {value:'1'};return null;}},lmoNow:()=> '2026-08-21 10:00',hideModal(){},refreshLastMileViews(){},showToast(){}};
vm.createContext(context);vm.runInContext(`${isDetained}\n${createRelabel}\n${saveDetention}\nsaveLastMileDetention();`,context,{timeout:1000});
if(relabels.length!==1||order.detention.active!==false||order.relabelOrderNo!=='FBRO20260821002')failures.push('release with relabel should create and link relabel order');
} catch(error) { failures.push(`release relabel save failed: ${error.message}`); }
}
if (failures.length) {
console.error('LAST_MILE_DETENTION_FIELDS_CHECK_FAIL');
failures.forEach(item => console.error(`- ${item}`));
process.exit(1);
}
console.log('LAST_MILE_DETENTION_FIELDS_CHECK_PASS');
@@ -0,0 +1,55 @@
#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
const root = path.resolve(__dirname, '..');
const html = fs.readFileSync(path.join(root, 'prototype-src/modules/03-overseas-warehouse.html'), 'utf8');
const app = fs.readFileSync(path.join(root, 'prototype-src/app.js'), 'utf8');
const failures = [];
const expect = (condition, message) => { if (!condition) failures.push(message); };
const functionSource = name => {
const start = app.indexOf(`function ${name}(`);
if (start < 0) return '';
const next = app.slice(start + 1).match(/\nfunction\s+[A-Za-z_$][\w$]*\s*\(/);
return app.slice(start, next ? start + 1 + next.index : app.length);
};
const inboundPlanModal = html.slice(
html.indexOf('id="modal-lmo-inbound-plan"'),
html.indexOf('<!-- 尾程订单 - 合托 -->')
);
expect(/id="lmo-inbound-pallet-type"/.test(inboundPlanModal), '入库计划缺少托盘类型');
expect(/<option[^>]*value="标准托盘"[^>]*selected[^>]*>标准托盘<\/option>/.test(inboundPlanModal), '托盘类型默认值不是标准托盘');
for (const type of ['标准托盘', '木托盘', '纸托盘']) {
expect(inboundPlanModal.includes(`>${type}</option>`), `缺少${type}选项`);
}
expect(/id="lmo-inbound-outsourced"[^>]*type="checkbox"/.test(inboundPlanModal), '入库计划缺少是否外配复选框');
expect(!/id="lmo-inbound-(?:pallet-type|outsourced)"[^>]*required/.test(inboundPlanModal), '新增字段不应必填');
const inboundTable = html.slice(html.indexOf('id="page-fba-inbound-order"'), html.indexOf('id="modal-fbio-appointment"'));
expect(/>托盘类型<\/th>/.test(inboundTable), '入库单列表缺少托盘类型列');
expect(/>是否外配<\/th>/.test(inboundTable), '入库单列表缺少是否外配列');
expect(/id="fbio-edit-pallet-type"/.test(html), '入库单编辑缺少托盘类型');
expect(/id="fbio-edit-outsourced"/.test(html), '入库单编辑缺少是否外配');
for (const name of ['openLastMileInboundPlan', 'saveLastMileInboundPlan', 'renderFbaInboundOrder', 'openFbioEditModal', 'saveFbioEdit', 'openFbaInboundOrderDetail']) {
const source = functionSource(name);
expect(/palletType|getInboundPalletType/.test(source), `${name} 未读写托盘类型`);
expect(/isOutsourced|isInboundOutsourced|formatInboundOutsourced/.test(source), `${name} 未读写是否外配`);
}
const savePlan = functionSource('saveLastMileInboundPlan');
expect(/palletType,isOutsourced/.test(savePlan), '入库计划未显式保存并同步两个字段');
const detail = functionSource('renderLastMileOrderDetail');
expect(/lmoInfo\('托盘类型'/.test(detail), '尾程详情未展示托盘类型');
expect(/lmoInfo\('是否外配'/.test(detail), '尾程详情未展示是否外配');
if (failures.length) {
console.error('LAST_MILE_INBOUND_PALLET_OUTSOURCING_CHECK_FAIL');
failures.forEach(message => console.error(`- ${message}`));
process.exitCode = 1;
} else {
console.log('LAST_MILE_INBOUND_PALLET_OUTSOURCING_CHECK_PASS');
}
@@ -0,0 +1,114 @@
#!/usr/bin/env node
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const root = path.resolve(__dirname, '..');
const html = fs.readFileSync(path.join(root, 'prototype-src/modules/03-overseas-warehouse.html'), 'utf8');
const app = fs.readFileSync(path.join(root, 'prototype-src/app.js'), 'utf8');
function functionSource(name) {
const start = app.indexOf(`function ${name}(`);
assert.ok(start >= 0, `缺少函数 ${name}`);
const braceStart = app.indexOf('{', start);
let depth = 0;
let quote = '';
let escaped = false;
for (let index = braceStart; index < app.length; index += 1) {
const char = app[index];
if (quote) {
if (escaped) escaped = false;
else if (char === '\\') escaped = true;
else if (char === quote) quote = '';
continue;
}
if (char === "'" || char === '"' || char === '`') { quote = char; continue; }
if (char === '{') depth += 1;
if (char === '}' && --depth === 0) return app.slice(start, index + 1);
}
throw new Error(`无法解析函数 ${name}`);
}
const inboundPlanModal = html.slice(html.indexOf('id="modal-lmo-inbound-plan"'), html.indexOf('<!-- 尾程订单 - 合托 -->'));
const inboundPage = html.slice(html.indexOf('id="page-fba-inbound-order"'), html.indexOf('<!-- ==================== FBA RETURN - 换标单'));
assert.match(inboundPlanModal, /id="lmo-inbound-urgent"[^>]*type="checkbox"/, '入库计划缺少是否加急复选框');
assert.doesNotMatch(inboundPlanModal, /id="lmo-inbound-urgent"[^>]*required/, '是否加急不应必填');
assert.match(inboundPage, />是否加急<\/th>/, '入库单列表缺少是否加急列');
assert.match(inboundPage, /id="fbio-edit-urgent"[^>]*type="checkbox"/, '入库单编辑缺少是否加急复选框');
assert.doesNotMatch(inboundPage, /id="fbio-edit-urgent"[^>]*required/, '入库单编辑是否加急不应必填');
assert.match(html, /\.fbio-edit-grid \.lmo-checkbox-option input\{width:16px;/, '入库单编辑复选框不应被通用输入框宽度撑开');
const urgentSource = functionSource('isInboundUrgent');
const formatSource = functionSource('formatInboundUrgent');
const helperContext = {};
vm.createContext(helperContext);
vm.runInContext(`${urgentSource}\n${formatSource}`, helperContext);
assert.equal(helperContext.isInboundUrgent(undefined), false, '旧数据应默认非加急');
assert.equal(helperContext.isInboundUrgent({}), false, '缺少字段应默认非加急');
assert.equal(helperContext.isInboundUrgent({isUrgent: true}), true, '加急值应正确读取');
assert.equal(helperContext.formatInboundUrgent({isUrgent: true}), '是');
assert.equal(helperContext.formatInboundUrgent({isUrgent: false}), '否');
const openPlanSource = functionSource('openLastMileInboundPlan');
function verifyPlanOpen(isUrgent) {
const order = {no:'LMO-OPEN',warehouse:'测试仓',eta:'2026-08-25',pallets:2,inboundPlan:isUrgent===undefined?null:{isUrgent}};
const elements = {
'lmo-inbound-warehouse': {value:''}, 'lmo-inbound-eta': {value:''}, 'lmo-inbound-code': {value:''},
'lmo-inbound-pallets': {value:''}, 'lmo-inbound-pallet-type': {value:''}, 'lmo-inbound-outsourced': {checked:true},
'lmo-inbound-consolidated': {checked:true}, 'lmo-inbound-urgent': {checked:true}, 'lmo-inbound-remark': {value:''}
};
const context = {
lmoActionOrder: () => order, isLastMileInboundLocked: () => false, getInboundPalletType: () => '标准托盘',
isInboundOutsourced: () => false, isInboundUrgent: record => !!(record&&record.isUrgent), isLastMileConsolidated: () => false,
document: {getElementById: id => elements[id]}, showModal() {}, showToast() {}
};
vm.runInNewContext(`${openPlanSource}\nopenLastMileInboundPlan('LMO-OPEN');`, context, {timeout:1000});
return elements['lmo-inbound-urgent'].checked;
}
assert.equal(verifyPlanOpen(undefined), false, '新建入库计划应默认否');
assert.equal(verifyPlanOpen(true), true, '已有加急计划应回显为是');
const savePlanSource = functionSource('saveLastMileInboundPlan');
function verifyPlanSave(isUrgent) {
const order = {no:'LMO-SAVE',inboundNo:'FBI-SAVE',inboundPlan:null,pallets:1,boxes:2,volume:3,firstLegNo:'HC-SAVE',owner:'测试货主',containerNo:'CONT-SAVE',logs:[]};
const linked = {inboundNo:'FBI-SAVE'};
const elements = {
'lmo-inbound-warehouse': {value:'测试仓'}, 'lmo-inbound-eta': {value:'2026-08-25'}, 'lmo-inbound-code': {value:'IN-SAVE'},
'lmo-inbound-pallets': {value:'2'}, 'lmo-inbound-pallet-type': {value:'标准托盘'}, 'lmo-inbound-outsourced': {checked:false},
'lmo-inbound-consolidated': {checked:false}, 'lmo-inbound-urgent': {checked:isUrgent}, 'lmo-inbound-remark': {value:''}
};
const context = {
currentLastMileActionNo:order.no,lastMileOrderData:[order],fbaInboundOrderData:[linked],isLastMileInboundLocked:()=>false,
document:{getElementById:id=>elements[id]},lmoNow:()=> '2026-08-21 17:00',hideModal(){},showToast(){},refreshLastMileViews(){}
};
vm.runInNewContext(`${savePlanSource}\nsaveLastMileInboundPlan();`,context,{timeout:1000});
return {order,linked};
}
for (const value of [false,true]) {
const saved=verifyPlanSave(value);
assert.equal(saved.order.inboundPlan.isUrgent,value,'入库计划未保存是否加急');
assert.equal(saved.linked.isUrgent,value,'关联入库单未同步是否加急');
}
const openEditSource = functionSource('openFbioEditModal');
const saveEditSource = functionSource('saveFbioEdit');
const record = {inboundNo:'FBI-EDIT',warehouseCode:'IN-EDIT',type:'海外订单',wh:'测试仓',fbaWarehouse:'FBA ONT8',owner:'测试货主',totalBoxes:2,chargeVol:3,palletTotal:1,palletType:'标准托盘',isOutsourced:false,isUrgent:true,status:'待审核',appointmentDate:'',estimatedLoadingTime:''};
const editIds = ['fbio-edit-inbound-no','fbio-edit-warehouse-code','fbio-edit-type','fbio-edit-wh','fbio-edit-fba-warehouse','fbio-edit-owner','fbio-edit-total-boxes','fbio-edit-charge-vol','fbio-edit-pallet-total','fbio-edit-pallet-type','fbio-edit-outsourced','fbio-edit-urgent','fbio-edit-status','fbio-edit-appointment-date','fbio-edit-loading-time'];
const editElements = Object.fromEntries(editIds.map(id=>[id,{value:'',checked:false}]));
const editContext = {fbaInboundOrderData:[record],dispatchOrders:[],getFbioFbaWarehouse:r=>r.fbaWarehouse,getInboundPalletType:r=>r.palletType||'标准托盘',isInboundOutsourced:r=>!!r.isOutsourced,isInboundUrgent:r=>!!r.isUrgent,document:{getElementById:id=>editElements[id]},showModal(){},showToast(){},hideModal(){},renderFbaInboundOrder(){},lmoNow:()=> '2026-08-21 17:10'};
vm.createContext(editContext);
vm.runInContext(`${functionSource('getFbioEditElement')}\n${functionSource('setFbioEditValue')}\n${openEditSource}\n${saveEditSource}`,editContext);
editContext.openFbioEditModal(record.inboundNo);
assert.equal(editElements['fbio-edit-urgent'].checked,true,'入库单编辑未回显加急状态');
editElements['fbio-edit-urgent'].checked=false;
editContext.saveFbioEdit();
assert.equal(record.isUrgent,false,'入库单编辑未独立保存加急状态');
assert.match(functionSource('renderLastMileOrderDetail'), /lmoInfo\('是否加急'/, '尾程详情未展示是否加急');
assert.match(functionSource('renderFbaInboundOrder'), /formatInboundUrgent/, '入库单列表未渲染是否加急');
assert.match(functionSource('openFbaInboundOrderDetail'), /formatInboundUrgent/, '入库单详情未展示是否加急');
console.log('LAST_MILE_INBOUND_URGENT_CHECK_PASS');
@@ -0,0 +1,127 @@
#!/usr/bin/env node
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const root = path.resolve(__dirname, '..');
const moduleHtml = fs.readFileSync(path.join(root, 'prototype-src/modules/03-overseas-warehouse.html'), 'utf8');
const appJs = fs.readFileSync(path.join(root, 'prototype-src/app.js'), 'utf8');
function functionSource(name) {
const start = appJs.indexOf(`function ${name}(`);
assert.ok(start >= 0, `缺少函数 ${name}`);
const braceStart = appJs.indexOf('{', start);
let depth = 0;
let quote = '';
let escaped = false;
for (let index = braceStart; index < appJs.length; index += 1) {
const char = appJs[index];
if (quote) {
if (escaped) escaped = false;
else if (char === '\\') escaped = true;
else if (char === quote) quote = '';
continue;
}
if (char === "'" || char === '"' || char === '`') {
quote = char;
continue;
}
if (char === '{') depth += 1;
if (char === '}' && --depth === 0) return appJs.slice(start, index + 1);
}
throw new Error(`无法解析函数 ${name}`);
}
function constantSource(name) {
const start = appJs.indexOf(`const ${name} =`);
assert.ok(start >= 0, `缺少数据 ${name}`);
const end = appJs.indexOf('\n];', start);
assert.ok(end > start, `无法解析数据 ${name}`);
return appJs.slice(start, end + 3);
}
assert.ok(moduleHtml.includes('id="modal-lmo-manual-nodes"'), '缺少人工修改节点弹窗');
assert.ok(moduleHtml.includes('id="tbody-lmo-manual-nodes"'), '弹窗缺少节点时间表格');
assert.ok(moduleHtml.includes('onclick="saveLastMileManualNodes()"'), '弹窗缺少保存操作');
assert.ok(appJs.includes('>人工修改节点</a>'), '尾程订单操作缺少人工修改节点');
assert.ok(appJs.includes('const lastMileClientNodeData = []'), '缺少独立客户端节点数据表');
const expectedLabels = [
'国内报关完成',
'预计开船',
'实际开船',
'预计到港',
'国外清关完成',
'实际到港等待卸船',
'预约提柜',
'卸柜'
];
for (const label of expectedLabels) assert.ok(appJs.includes(`label:'${label}'`), `缺少节点 ${label}`);
const definitionsSource = constantSource('lastMileClientNodeDefinitions');
const updateSource = functionSource('updateLastMileClientNodeRecords');
const saveSource = functionSource('saveLastMileManualNodes');
const nodeData = [];
const values = {
domestic_customs_completed: '',
estimated_departure: '2026-08-22T09:30',
actual_departure: '',
estimated_arrival: '',
overseas_customs_completed: '',
actual_arrival_waiting_unload: '',
pickup_appointment: '',
unloaded: ''
};
const order = { no: 'LMO-TEST-001', logs: [] };
const calls = { hidden: 0, refreshed: 0, toasts: [] };
const context = {
seedNodeData: nodeData,
values,
String,
Array,
Object,
lastMileOrderData: [order],
currentLastMileActionNo: order.no,
document: {
getElementById(id) {
const prefix = 'lmo-manual-node-';
if (!id.startsWith(prefix)) return null;
const code = id.slice(prefix.length).replaceAll('-', '_');
return { value: values[code] || '' };
}
},
lmoNow: () => '2026-08-21 16:30',
hideModal: () => { calls.hidden += 1; },
refreshLastMileViews: () => { calls.refreshed += 1; },
showToast: (message, type) => { calls.toasts.push({ message, type }); }
};
vm.createContext(context);
vm.runInContext(`${definitionsSource}\nconst lastMileClientNodeData = globalThis.seedNodeData;\n${updateSource}\n${saveSource}`, context);
context.saveLastMileManualNodes();
assert.equal(nodeData.length, 1, '保存后应写入一条独立客户端节点数据');
assert.equal(nodeData[0].orderNo, order.no);
assert.equal(nodeData[0].nodeCode, 'estimated_departure');
assert.equal(nodeData[0].nodeTime, values.estimated_departure);
assert.equal(nodeData[0].updatedBy, '张经理');
assert.equal(order.logs.length, 1, '一次保存只能新增一条后台操作记录');
assert.equal(order.logs[0].node, '人工修改节点');
assert.match(order.logs[0].result, /预计开船/);
assert.equal(calls.hidden, 1, '保存成功后应关闭弹窗');
context.saveLastMileManualNodes();
assert.equal(order.logs.length, 1, '节点未变化时不应新增操作记录');
assert.equal(calls.hidden, 1, '节点未变化时应保留弹窗');
assert.ok(calls.toasts.some(item => item.type === 'warning' && item.message.includes('未发生变化')), '节点未变化时应给出明确提示');
values.estimated_departure = '';
context.saveLastMileManualNodes();
assert.equal(nodeData.length, 0, '清空节点时间后应删除对应客户端节点记录');
assert.equal(order.logs.length, 2, '清空节点时间应新增一条操作记录');
assert.ok(!moduleHtml.includes('tbody-lmo-client-node-detail'), '后台详情不应展示客户端节点数据表');
console.log('LAST_MILE_MANUAL_CLIENT_NODES_CHECK_PASS');
@@ -0,0 +1,79 @@
#!/usr/bin/env node
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const root = path.resolve(__dirname, '..');
const moduleHtml = fs.readFileSync(path.join(root, 'prototype-src/modules/03-overseas-warehouse.html'), 'utf8');
const overlays = fs.readFileSync(path.join(root, 'prototype-src/overlays.html'), 'utf8');
const appJs = fs.readFileSync(path.join(root, 'prototype-src/app.js'), 'utf8');
function functionSource(name, nextName) {
const start = appJs.indexOf(`function ${name}(`);
assert.ok(start >= 0, `缺少函数 ${name}`);
const end = nextName ? appJs.indexOf(`\nfunction ${nextName}`, start) : appJs.length;
const source = appJs.slice(start, end > start ? end : appJs.length);
const braceStart = source.indexOf('{');
let depth = 0;
let quote = '';
let escaped = false;
for (let index = braceStart; index < source.length; index += 1) {
const char = source[index];
if (quote) {
if (escaped) escaped = false;
else if (char === '\\') escaped = true;
else if (char === quote) quote = '';
continue;
}
if (char === "'" || char === '"' || char === '`') {
quote = char;
continue;
}
if (char === '{') depth += 1;
if (char === '}' && --depth === 0) return source.slice(0, index + 1);
}
return source;
}
assert.ok(moduleHtml.includes('value="待结算"'), '尾程订单状态筛选缺少待结算');
assert.ok(moduleHtml.includes('value="已完成"'), '尾程订单状态筛选缺少已完成');
assert.ok(moduleHtml.includes('data-lmo-tab="待结算"'), '尾程订单缺少待结算页签');
assert.ok(moduleHtml.includes('data-lmo-tab="已完成"'), '尾程订单缺少已完成页签');
assert.ok(overlays.includes('id="modal-lmo-settlement"'), '缺少尾程订单结算弹框');
assert.ok(overlays.includes('id="tbody-lmo-settlement-fees"'), '结算弹框缺少费用明细容器');
assert.ok(overlays.includes('onclick="addLastMileSettlementFeeRow()"'), '结算弹框缺少新增额外费用操作');
assert.ok(appJs.includes('function openLastMileSettlement('), '缺少打开结算弹框函数');
assert.ok(appJs.includes('function saveLastMileSettlement('), '缺少保存结算函数');
assert.ok(appJs.includes('sourceLastMileNo'), '财务记录缺少尾程来源字段');
assert.ok(appJs.includes('deliveryStatus'), '尾程订单缺少派送完成状态字段');
const statusSource = functionSource('getLastMileSettlementStatus', 'getLastMileDisplayStatus');
const deliverySource = functionSource('isLastMileDeliveryCompleted', 'getLastMileSettlementStatus');
const feeSource = functionSource('collectLastMileSettlementFees', 'getLastMileStatusClass');
const pushSource = functionSource('pushLastMileSettlementToArap', 'validateLastMileSettlementFees');
const context = { Number, String, Object, Array, Date, arapData: [] };
vm.createContext(context);
vm.runInContext(`${deliverySource}\n${statusSource}\n${feeSource}\n${pushSource}`, context);
assert.equal(context.isLastMileDeliveryCompleted({ deliveryStatus: '派送完成' }), true);
assert.equal(context.getLastMileSettlementStatus({ deliveryStatus: '派送完成' }), '待结算');
assert.equal(context.getLastMileSettlementStatus({ deliveryStatus: '派送中' }), '');
const fees = context.collectLastMileSettlementFees({ feeDetails: [
{ feeType: '派送费', currency: 'USD', amount: 180, status: '已产生' },
{ feeType: '仓储费', currency: 'USD', amount: 0, status: '待产生' }
] }, [{ feeType: '偏远附加费', currency: 'USD', amount: '25.00', source: '额外费用' }]);
assert.equal(fees.length, 2);
assert.equal(fees[0].amount, '180.00');
assert.equal(fees[1].source, '额外费用');
const order = { no: 'LMO-SETTLE-001', firstLegNo: 'HC-001', owner: '测试客户', dispatchNo: 'DSP-001', containerNo: 'CONT-001', settlement: { pushed: false } };
assert.equal(context.pushLastMileSettlementToArap(order, fees), 2);
assert.equal(context.arapData.length, 2);
assert.equal(context.arapData[0].sourceLastMileNo, order.no);
assert.equal(context.pushLastMileSettlementToArap(order, fees), 0);
assert.equal(context.arapData.length, 2);
console.log('LAST_MILE_SETTLEMENT_CHECK_PASS');
@@ -0,0 +1,20 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const root = path.resolve(__dirname, '..');
const modal = fs.readFileSync(path.join(root, 'prototype-src/tail.html'), 'utf8');
const app = fs.readFileSync(path.join(root, 'prototype-src/app.js'), 'utf8');
assert.match(modal, /<option value="买约">买约<\/option>/, '预约账号缺少买约选项');
assert.match(modal, /id="bol-sellerName"/, '缺少买约卖家姓名字段');
assert.match(modal, /id="bol-buyAppointmentFee"/, '缺少买约费用字段');
assert.match(modal, /id="bol-buyAppointmentVoucher"/, '缺少买约付款凭证字段');
assert.match(modal, /id="bol-courier-company-field"[\s\S]*id="bol-courierCompanyId"/, '快递公司字段缺失');
const activeModal = modal.split('<template id="legacy-bol-fields">')[0];
assert.doesNotMatch(activeModal, /<div class="form-group"><label>预约号<\/label><input id="bol-appointmentNo"/, '预约号仍在转派送表单中可见');
assert.match(app, /handleOutboundVehicleAppointmentAccountChange\(\)/, '缺少预约账号联动处理');
assert.match(app, /buyAppointmentFee|buyAppointmentVoucher/, '买约字段未接入保存数据');
assert.match(app, /sellerName/, '卖家姓名未接入保存数据');
console.log('验证通过:排车转派送买约字段已接入。');
@@ -254,7 +254,7 @@ expectExactOptionSet(bolModal, 'bol-bookAccount', ['4', '8'], '预约账号');
expectExactOptionSet( expectExactOptionSet(
bolModal, bolModal,
'bol-vehicleSize', 'bol-vehicleSize',
['大车(30托/15托)', '小车(14托/12托)'], ['大车', '小车'],
'车辆大小', '车辆大小',
); );
expect(!bolModal.includes('卡派地版'), '转派送弹框不应保留错误文案“卡派地版”'); expect(!bolModal.includes('卡派地版'), '转派送弹框不应保留错误文案“卡派地版”');
@@ -72,8 +72,14 @@ assert.match(orderListView, /esc\(p\.container\)/, 'inbound order cards must ren
assert.match(html, /cartonCount|箱数/, 'pallet list must show carton count'); assert.match(html, /cartonCount|箱数/, 'pallet list must show carton count');
assert.match(html, /idle|occupied|闲置|占用/, 'pallet list must show idle or occupied status'); assert.match(html, /idle|occupied|闲置|占用/, 'pallet list must show idle or occupied status');
const consolidationFn = html.match(/function startConsolidation\(\)\{[^\n]+\}/)?.[0] || ''; const consolidationFn = html.match(/function startConsolidation\(\)\{[^\n]+\}/)?.[0] || '';
assert.match(consolidationFn, /page\s*=\s*['"]pallet['"]/, 'consolidation must open detail directly'); assert.match(consolidationFn, /page\s*=\s*['"]pallet-list['"]/, 'consolidation must open the pallet list first');
assert.doesNotMatch(consolidationFn, /pallet-list/, 'consolidation must not open pallet list first'); assert.match(html, /palletListView[\s\S]*consolidating/, 'consolidation must reuse the inbound pallet list');
assert.match(html, /consolidating\s*\?\s*state\.pallets\.filter\([^\n]+mode\s*===\s*['"]consolidate['"]/, 'consolidation list must show only created consolidation pallets');
assert.match(html, /startNewConsolidation|newConsolidation/, 'consolidation list must provide a create action');
assert.match(html, /demoConsolidatedPallets[\s\S]*mode\s*:\s*['"]consolidate['"]/, 'prototype must demonstrate a completed consolidation pallet');
assert.match(html, /reconsolidatePallet|reconsolidate/, 'created consolidation pallets must support reconsolidation');
assert.match(html, /submitConsolidation|提交合托/, 'consolidation detail must provide a submit action');
assert.match(html, /originalPalletId|findIndex\([^\n]+originalPalletId/, 'reconsolidation submit must update the original pallet');
assert.match(html, /injectReservedEntries[\s\S]*more\.style\.display='none'/, 'more operations must be hidden'); assert.match(html, /injectReservedEntries[\s\S]*more\.style\.display='none'/, 'more operations must be hidden');
assert.doesNotMatch(html, /injectReservedEntries[\s\S]*t\('stocktake'\)/, 'stocktake must be hidden'); assert.doesNotMatch(html, /injectReservedEntries[\s\S]*t\('stocktake'\)/, 'stocktake must be hidden');
assert.doesNotMatch(html, /injectReservedEntries[\s\S]*t\('transfer'\)/, 'transfer must be hidden'); assert.doesNotMatch(html, /injectReservedEntries[\s\S]*t\('transfer'\)/, 'transfer must be hidden');
@@ -0,0 +1,45 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const root = path.resolve(__dirname, '..');
const sourceRoot = path.join(root, 'prototype-src');
const moduleSource = fs.readFileSync(path.join(sourceRoot, 'modules/07-overseas-business.html'), 'utf8');
const appSource = fs.readFileSync(path.join(sourceRoot, 'app.js'), 'utf8');
const manifest = JSON.parse(fs.readFileSync(path.join(sourceRoot, 'manifest.json'), 'utf8'));
const moduleEntry = manifest.modules.find(item => item.file === 'modules/07-overseas-business.html');
assert.ok(moduleEntry, 'manifest 缺少海外业务模块');
assert.ok(moduleEntry.pages.includes('overseas-truck-detail'), '海外业务模块未登记本土卡派详情页');
assert.ok(manifest.pageOrder.includes('overseas-truck-detail'), 'pageOrder 未登记本土卡派详情页');
const moduleAddIndex = moduleEntry.pages.indexOf('overseas-truck-add');
const moduleDetailIndex = moduleEntry.pages.indexOf('overseas-truck-detail');
const moduleEditIndex = moduleEntry.pages.indexOf('overseas-truck-edit');
assert.ok(moduleAddIndex < moduleDetailIndex && moduleDetailIndex < moduleEditIndex, '海外业务模块中的详情页顺序应为新增页之后、编辑页之前');
const orderAddIndex = manifest.pageOrder.indexOf('overseas-truck-add');
const orderDetailIndex = manifest.pageOrder.indexOf('overseas-truck-detail');
const orderEditIndex = manifest.pageOrder.indexOf('overseas-truck-edit');
assert.ok(orderAddIndex < orderDetailIndex && orderDetailIndex < orderEditIndex, 'pageOrder 中的详情页顺序应为新增页之后、编辑页之前');
const detailStart = moduleSource.indexOf('id="page-overseas-truck-detail"');
const editStart = moduleSource.indexOf('id="page-overseas-truck-edit"');
assert.ok(detailStart >= 0, '缺少本土卡派详情页');
assert.ok(editStart > detailStart, '本土卡派详情页应位于新增页和编辑页之间');
const detailPage = moduleSource.slice(detailStart, editStart);
assert.match(detailPage, /id="truck-detail-no"/, '详情页缺少订单编号');
assert.match(detailPage, /id="truck-detail-status"/, '详情页缺少状态');
assert.match(detailPage, /id="truck-detail-fields"/, '详情页缺少订单字段容器');
assert.match(detailPage, /id="truck-detail-attachments"/, '详情页缺少上传资料容器');
assert.match(detailPage, /onclick="editTruckOrderFromDetail\(\)"/, '详情页缺少编辑入口');
assert.match(detailPage, /switchPage\('overseas-truck'\)/, '详情页缺少返回列表入口');
assert.match(appSource, /onclick="showTruckOrderDetail\('\+idx\+'\)"/, '列表详情未接通详情页');
assert.match(appSource, /function showTruckOrderDetail\(index\)/, '缺少详情渲染函数');
assert.match(appSource, /function editTruckOrderFromDetail\(\)/, '缺少详情页编辑跳转函数');
assert.match(appSource, /showToast\('订单不存在','error'\)/, '缺少无效订单处理');
assert.match(appSource, /暂无上传资料/, '缺少上传资料空态');
assert.match(appSource, /switchPage\('overseas-truck-detail'\)/, '详情渲染未跳转到详情页');
assert.match(appSource, /page==='overseas-truck-detail'[\s\S]*?switchPage\(\\'overseas-truck\\',this\)/, '本土卡派详情页未映射到列表侧栏导航');
console.log('海外本土卡派详情页校验通过');
@@ -0,0 +1,20 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const root = path.resolve(__dirname, '..');
const moduleSource = fs.readFileSync(path.join(root, 'prototype-src/modules/07-overseas-business.html'), 'utf8');
const appSource = fs.readFileSync(path.join(root, 'prototype-src/app.js'), 'utf8');
const editPageStart = moduleSource.indexOf('id="page-overseas-truck-edit"');
const editPageEnd = moduleSource.indexOf('id="page-overseas-looseboard-edit"');
assert.ok(editPageStart >= 0 && editPageEnd > editPageStart, '缺少美国本土卡派编辑页面');
const editPage = moduleSource.slice(editPageStart, editPageEnd);
assert.match(editPage, /id="tke-attachments-input"/, '编辑页缺少资料上传控件');
assert.match(editPage, /id="tke-attachments-list"/, '编辑页缺少资料回显列表');
assert.match(appSource, /function renderTruckEditAttachments\(\)/, '缺少编辑页资料列表渲染函数');
assert.match(appSource, /r\.attachments\s*=\s*truckEditAttachmentsDraft\.slice\(\)/, '保存编辑时未写回订单资料');
assert.match(appSource, /function setBtnGroup\(parentEl,val\)\{\s*if\(!parentEl\)return;\s*var hidden=parentEl\.querySelector\('input\[type="hidden"\]'\);\s*if\(hidden\)hidden\.value=val;/, '编辑页按钮组未回填隐藏字段值');
console.log('海外本土卡派编辑资料字段校验通过');