Test #1
@@ -123,7 +123,7 @@ node tests/verify-prototype-modules.cjs
|
||||
| 客户服务 | `prototype-src/modules/04a-customer-service.html` | `outbound-exception`(异常订单) |
|
||||
| 供应商中心 | `prototype-src/modules/05-suppliers.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/09-sales.html` | 拜访记录新增/详情、销售提成 |
|
||||
| 通用资料 | `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
+923
-119
File diff suppressed because it is too large
Load Diff
Vendored
+692
-82
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@
|
||||
"outbound-bol",
|
||||
"outbound-selfpick",
|
||||
"outbound-exception",
|
||||
"customer-followup",
|
||||
"gen-product",
|
||||
"gen-fee",
|
||||
"gen-exchange-rate",
|
||||
@@ -20,6 +21,7 @@
|
||||
"gen-owned-warehouse",
|
||||
"gen-fba",
|
||||
"gen-partner-warehouse",
|
||||
"gen-private-warehouse",
|
||||
"gen-overseas-quote",
|
||||
"gen-download",
|
||||
"gen-pallet",
|
||||
@@ -177,6 +179,7 @@
|
||||
"legacy-purchase-detail",
|
||||
"overseas-looseboard",
|
||||
"overseas-looseboard-add",
|
||||
"overseas-truck-detail",
|
||||
"overseas-truck-edit",
|
||||
"overseas-looseboard-edit"
|
||||
],
|
||||
@@ -273,7 +276,8 @@
|
||||
"file": "modules/04a-customer-service.html",
|
||||
"title": "客户服务",
|
||||
"pages": [
|
||||
"outbound-exception"
|
||||
"outbound-exception",
|
||||
"customer-followup"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -318,6 +322,7 @@
|
||||
"legacy-purchase-detail",
|
||||
"overseas-looseboard",
|
||||
"overseas-looseboard-add",
|
||||
"overseas-truck-detail",
|
||||
"overseas-truck-edit",
|
||||
"overseas-looseboard-edit"
|
||||
]
|
||||
@@ -378,6 +383,7 @@
|
||||
"gen-owned-warehouse",
|
||||
"gen-fba",
|
||||
"gen-partner-warehouse",
|
||||
"gen-private-warehouse",
|
||||
"gen-overseas-quote",
|
||||
"gen-download",
|
||||
"gen-pallet",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
<button class="btn btn-primary">查询</button>
|
||||
<button class="btn btn-primary">重置</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="showContainerCalcModal()" style="background:#52c41a;border-color:#52c41a">📦 智能装柜</button>
|
||||
</div>
|
||||
@@ -109,11 +110,11 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr data-inbound-order-type="快递订单">
|
||||
<tr data-inbound-order-type="快递订单" data-inbound-code="EXP260814001" data-inbound-status="已入库">
|
||||
<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><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 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-default">收起 ▲</button>
|
||||
<button class="btn btn-default" onclick="openDomesticInboundBatchTransfer()" style="margin-left:auto">批量转仓</button>
|
||||
</div>
|
||||
</div>
|
||||
<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-default">收起 ▲</button>
|
||||
<button class="btn btn-default" onclick="openDomesticInboundBatchTransfer()" style="margin-left:auto">批量转仓</button>
|
||||
</div>
|
||||
</div>
|
||||
<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-default">收起 ▲</button>
|
||||
<button class="btn btn-default" onclick="openDomesticInboundBatchTransfer()" style="margin-left:auto">批量转仓</button>
|
||||
</div>
|
||||
</div>
|
||||
<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-default">收起 ▲</button>
|
||||
<button class="btn btn-default" onclick="openDomesticInboundBatchTransfer()" style="margin-left:auto">批量转仓</button>
|
||||
</div>
|
||||
</div>
|
||||
<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-default">收起 ▲</button>
|
||||
<button class="btn btn-default" onclick="openDomesticInboundBatchTransfer()" style="margin-left:auto">批量转仓</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inbound-stats-bar">
|
||||
|
||||
@@ -229,7 +229,7 @@
|
||||
<button class="btn btn-sm btn-default" onclick="obInReset()">重置</button>
|
||||
</div>
|
||||
<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>
|
||||
@@ -332,7 +332,7 @@
|
||||
<button class="btn btn-sm btn-default" onclick="rlReset()">重置</button>
|
||||
</div>
|
||||
<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:center;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 .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 .lmo-checkbox-option input{width:16px;flex:0 0 16px}
|
||||
.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}}
|
||||
</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">计费体积(m³)</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">FBA预约</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></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">×</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">×</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">×</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">×</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 - 换标单 ==================== -->
|
||||
@@ -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><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="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-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>
|
||||
@@ -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-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}
|
||||
#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}
|
||||
@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-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}}
|
||||
</style>
|
||||
|
||||
@@ -873,6 +879,8 @@ FORM: 采用现有后台的列表加标准详情结构,保持高信息密度
|
||||
<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" 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 class="search-row lmo-search">
|
||||
<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>
|
||||
<input id="lmo-owner" placeholder="货主" style="width:150px"/>
|
||||
<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>
|
||||
<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>
|
||||
@@ -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-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 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"><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 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">×</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">×</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">×</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">×</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">×</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">×</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">×</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>到达状态(FBA状态编号)</th>
|
||||
<th>配送</th>
|
||||
<th>编辑</th>
|
||||
<th>输入人</th>
|
||||
<th>输入时间</th>
|
||||
<th>FBA配送类型</th>
|
||||
@@ -83,7 +82,7 @@
|
||||
</thead>
|
||||
<tbody id="tbody-outbound-dispatch">
|
||||
<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><span class="tag tag-blue">派送中</span></td>
|
||||
<td>海卡</td>
|
||||
@@ -108,7 +107,6 @@
|
||||
<td>D202606001</td>
|
||||
<td>已签收</td>
|
||||
<td>是</td>
|
||||
<td><a href="javascript:void(0)" style="color:#3b82f6;font-size:11px">编辑</a></td>
|
||||
<td>李输入</td>
|
||||
<td>2026-06-15 09:00</td>
|
||||
<td>FBA-标准</td>
|
||||
@@ -120,7 +118,7 @@
|
||||
<td>7260.00</td>
|
||||
</tr>
|
||||
<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><span class="tag tag-green">已签收</span></td>
|
||||
<td>海卡</td>
|
||||
@@ -145,7 +143,6 @@
|
||||
<td>D202606002</td>
|
||||
<td>已签收</td>
|
||||
<td>是</td>
|
||||
<td><a href="javascript:void(0)" style="color:#3b82f6;font-size:11px">编辑</a></td>
|
||||
<td>王输入</td>
|
||||
<td>2026-06-14 07:30</td>
|
||||
<td>FBA-标准</td>
|
||||
@@ -157,7 +154,7 @@
|
||||
<td>5775.00</td>
|
||||
</tr>
|
||||
<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><span class="tag tag-blue">派送中</span></td>
|
||||
<td>海卡</td>
|
||||
@@ -182,7 +179,6 @@
|
||||
<td>D202606003</td>
|
||||
<td>运输中</td>
|
||||
<td>否</td>
|
||||
<td><a href="javascript:void(0)" style="color:#3b82f6;font-size:11px">编辑</a></td>
|
||||
<td>张输入</td>
|
||||
<td>2026-06-15 10:00</td>
|
||||
<td>FBA-加急</td>
|
||||
@@ -194,7 +190,7 @@
|
||||
<td>3520.00</td>
|
||||
</tr>
|
||||
<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><span class="tag tag-green">已签收</span></td>
|
||||
<td>海卡</td>
|
||||
@@ -219,7 +215,6 @@
|
||||
<td>D202606004</td>
|
||||
<td>已签收</td>
|
||||
<td>是</td>
|
||||
<td><a href="javascript:void(0)" style="color:#3b82f6;font-size:11px">编辑</a></td>
|
||||
<td>陈输入</td>
|
||||
<td>2026-06-13 08:10</td>
|
||||
<td>FBA-退货</td>
|
||||
@@ -231,7 +226,7 @@
|
||||
<td>1072.50</td>
|
||||
</tr>
|
||||
<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><span class="tag tag-blue">派送中</span></td>
|
||||
<td>海卡</td>
|
||||
@@ -256,7 +251,6 @@
|
||||
<td>D202606005</td>
|
||||
<td>待处理</td>
|
||||
<td>否</td>
|
||||
<td><a href="javascript:void(0)" style="color:#3b82f6;font-size:11px">编辑</a></td>
|
||||
<td>刘输入</td>
|
||||
<td>2026-06-15 14:00</td>
|
||||
<td>FBA-标准</td>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="page" id="page-outbound-exception">
|
||||
<div class="table-card">
|
||||
<div class="breadcrumb">首页 <span class="sep">></span> 客户服务 <span class="sep">></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="inbound-search-section">
|
||||
<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>
|
||||
|
||||
<div class="page" id="page-customer-followup">
|
||||
<div class="table-card">
|
||||
<div class="breadcrumb">首页 <span class="sep">></span> 客户服务 <span class="sep">></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="search-row" style="flex-wrap:wrap;gap:8px">
|
||||
<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"/>
|
||||
<button class="btn btn-sm btn-primary">查询</button><button class="btn btn-sm btn-default">重置</button>
|
||||
</div>
|
||||
<table style="font-size:12px">
|
||||
<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>
|
||||
<tbody id="tbody-ct-cost"></tbody>
|
||||
</table>
|
||||
|
||||
@@ -309,6 +309,25 @@
|
||||
</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 ==================== -->
|
||||
<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>
|
||||
@@ -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="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 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>
|
||||
|
||||
@@ -35,25 +35,32 @@ FORM: 已建立后台列表页的精确扩展;本次需求已明确,因此
|
||||
<!-- ==================== WAREHOUSE MANAGEMENT ==================== -->
|
||||
<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="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="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="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="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="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="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="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="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="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>
|
||||
#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}
|
||||
|
||||
@@ -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">×</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 ==================== -->
|
||||
<div class="modal-overlay" id="modal-dispatch-schedule">
|
||||
<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">
|
||||
<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">
|
||||
<option value="全美">全部渠道</option><option value="洛杉矶">洛杉矶</option><option value="纽约">纽约</option>
|
||||
</select>
|
||||
@@ -64,7 +85,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<button class="btn btn-default" onclick="hideModal('modal-dispatch-schedule')">取消</button>
|
||||
<button class="btn btn-primary" onclick="preScheduleVehicle()">预排车</button>
|
||||
@@ -584,8 +605,9 @@
|
||||
<div style="padding:20px 24px">
|
||||
<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">费用类型</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 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 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>
|
||||
@@ -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></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()">×</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弹窗 -->
|
||||
<div class="modal-overlay" id="modal-upload-pod">
|
||||
<div class="modal" style="max-width:520px">
|
||||
@@ -1498,6 +1531,20 @@
|
||||
</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">×</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" 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>
|
||||
</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="关闭">×</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" style="max-width:560px">
|
||||
@@ -1809,3 +1871,5 @@
|
||||
</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">×</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.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}
|
||||
.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-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}
|
||||
@@ -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.active{border-bottom-color:#1677ff;color:#1677ff;font-weight:650}
|
||||
.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}
|
||||
.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}
|
||||
@@ -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-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: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 .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}
|
||||
@@ -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>.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只读展示 */
|
||||
.basic-readonly input,.basic-readonly select,.basic-readonly textarea{background:#f5f5f5;color:#666;cursor:not-allowed;pointer-events:none}
|
||||
/* 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="sub-menu">
|
||||
<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 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-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" 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-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-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-vehicleSize"><option value="">请选择</option><option>大车(30托/15托)</option><option>小车(14托/12托)</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><input id="bol-appointmentNo"/></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" onchange="handleOutboundVehicleAppointmentAccountChange()"><option value="">请选择</option><option>4</option><option>8</option><option value="买约">买约</option></select></div>
|
||||
<div class="form-group" id="bol-seller-name-field" hidden><label>卖家姓名</label><input id="bol-sellerName" placeholder="请输入卖家姓名"/></div>
|
||||
</div>
|
||||
<div class="outbound-vehicle-section-title">费用信息</div>
|
||||
<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" 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 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>
|
||||
@@ -66,9 +68,9 @@
|
||||
</div>
|
||||
<!-- 第六行 -->
|
||||
<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-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><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><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><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-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 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 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 overlays = read('prototype-src/overlays.html');
|
||||
const shell = read('prototype-src/shell.html');
|
||||
@@ -16,6 +16,14 @@ const functionSource = name => {
|
||||
const next = app.indexOf('\nfunction ', start + 1);
|
||||
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\];/);
|
||||
assert.ok(staticDispatchRecords, 'app.js 缺少静态派送订单数据');
|
||||
|
||||
|
||||
@@ -96,10 +96,9 @@ assert.equal(
|
||||
overseasDeliveryIndex + 1,
|
||||
'客户服务模块必须紧邻海外派送之后',
|
||||
);
|
||||
assert.deepEqual(
|
||||
customerServiceModule.pages,
|
||||
['outbound-exception'],
|
||||
'客户服务模块必须且只能归属 outbound-exception 页面',
|
||||
assert.ok(
|
||||
customerServiceModule.pages.includes('outbound-exception'),
|
||||
'客户服务模块必须归属 outbound-exception 页面',
|
||||
);
|
||||
assert.ok(
|
||||
!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.deepEqual(
|
||||
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.receivedAmount, 90, '已收合计必须读取所选月份金额');
|
||||
@@ -118,7 +126,15 @@ const missingRow = api.calculateReceivableReportRow({
|
||||
}, '2026-08');
|
||||
assert.deepEqual(
|
||||
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');
|
||||
@@ -150,11 +166,12 @@ assert.deepEqual(invalidRecord, {customer: '保留客户', remark: '保留备注
|
||||
const header = api.buildReceivableReportHeader('2026-07');
|
||||
assert.equal(typeof header, 'string', '单月表头必须生成 HTML 字符串');
|
||||
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.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.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(
|
||||
bolModal,
|
||||
'bol-vehicleSize',
|
||||
['大车(30托/15托)', '小车(14托/12托)'],
|
||||
['大车', '小车'],
|
||||
'车辆大小',
|
||||
);
|
||||
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, /idle|occupied|闲置|占用/, 'pallet list must show idle or occupied status');
|
||||
const consolidationFn = html.match(/function startConsolidation\(\)\{[^\n]+\}/)?.[0] || '';
|
||||
assert.match(consolidationFn, /page\s*=\s*['"]pallet['"]/, 'consolidation must open detail directly');
|
||||
assert.doesNotMatch(consolidationFn, /pallet-list/, 'consolidation must not open pallet list first');
|
||||
assert.match(consolidationFn, /page\s*=\s*['"]pallet-list['"]/, 'consolidation must open the 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.doesNotMatch(html, /injectReservedEntries[\s\S]*t\('stocktake'\)/, 'stocktake 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('海外本土卡派编辑资料字段校验通过');
|
||||
Reference in New Issue
Block a user