Merge branch 'test' of http://8.163.30.128:10082/admin/docs into dev
This commit is contained in:
@@ -1,2 +1,6 @@
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
.playwright-cli
|
||||
.superpowers
|
||||
output
|
||||
docs
|
||||
File diff suppressed because one or more lines are too long
+615
-217
File diff suppressed because it is too large
Load Diff
Vendored
+461
-170
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="240" viewBox="0 0 240 240" role="img" aria-labelledby="title desc">
|
||||
<title id="title">示例收款二维码</title>
|
||||
<desc id="desc">仅用于原型展示,不包含真实收款信息</desc>
|
||||
<rect width="240" height="240" fill="#fff"/>
|
||||
<g fill="#111827">
|
||||
<path d="M16 16h64v64H16zm10 10v44h44V26zm10 10h24v24H36zM160 16h64v64h-64zm10 10v44h44V26zm10 10h24v24h-24zM16 160h64v64H16zm10 10v44h44v-44zm10 10h24v24H36z"/>
|
||||
<path d="M96 16h12v12H96zm24 0h12v24h-12zm16 8h12v12h-12zM88 44h20v12H88zm28 8h12v20h-12zm20-8h12v28h-12zM88 84h12v20H88zm24 0h28v12h-28zm36 4h12v20h-12zm20 0h20v12h-20zm28 0h28v12h-28zM16 96h20v12H16zm32-8h12v28H48zm20 8h20v12H68zm96 16h20v12h-20zm32-8h28v12h-28zM16 124h12v20H16zm20-8h20v12H36zm32 4h20v20H68zm92 16h12v16h-12zm20-4h20v12h-20zm28 4h16v20h-16zM92 160h20v12H92zm28-8h12v28h-12zm16 8h20v12h-20zm28 0h20v20h-20zm32 4h28v12h-28zM88 188h12v20H88zm20-4h20v12h-20zm28 4h16v20h-16zm28 0h12v12h-12zm20 0h20v12h-20zm28 4h12v28h-12zM96 216h24v8H96zm36-4h28v12h-28zm48 4h20v8h-20z"/>
|
||||
</g>
|
||||
<rect x="88" y="104" width="64" height="34" rx="4" fill="#fff" stroke="#111827" stroke-width="3"/>
|
||||
<text x="120" y="127" text-anchor="middle" font-family="Arial, sans-serif" font-size="17" font-weight="700" fill="#111827">示例</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -641,6 +641,7 @@
|
||||
<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:right;white-space:nowrap">容积(m³)</th>
|
||||
<th style="padding:9px 10px;text-align:right;white-space:nowrap">托盘重量(kg)</th>
|
||||
<th style="padding:9px 10px;text-align:right;white-space:nowrap">采购价格</th>
|
||||
<th style="padding:9px 10px;text-align:left;white-space:nowrap">所属FBA仓</th>
|
||||
<th style="padding:9px 10px;text-align:center;white-space:nowrap">状态</th>
|
||||
@@ -651,7 +652,7 @@
|
||||
<div class="pagination"><span id="fbp-total">共 0 条</span><button class="page-btn active">1</button></div>
|
||||
</div></div>
|
||||
|
||||
<div class="modal-overlay" id="modal-fba-pallet-form"><div class="modal" style="max-width:760px"><div class="modal-header"><span id="fbp-form-title">新增托盘</span><span onclick="hideModal('modal-fba-pallet-form')" style="cursor:pointer;font-size:20px">×</span></div><div class="modal-body"><div class="form-row"><div class="form-group"><label>托盘编码 *</label><input id="fbp-form-code" maxlength="64" placeholder="请输入托盘编码"></div><div class="form-group"><label>托盘类型 *</label><select id="fbp-form-type"><option>木托盘</option><option>塑料托盘</option><option>金属托盘</option><option>纸托盘</option><option>免熏蒸</option></select></div></div><div class="form-row"><div class="form-group"><label>容积(m³) *</label><input id="fbp-form-volume" type="number" min="0.01" step="0.01" placeholder="请输入容积"></div><div class="form-group"><label>采购价格(USD) *</label><input id="fbp-form-price" type="number" min="0" step="0.01" placeholder="请输入采购价格"></div></div><div class="form-row"><div class="form-group"><label>仓库 *</label><select id="fbp-form-overseas-warehouse"><option>洛杉矶长滩海外仓</option><option>纽约海外仓</option><option>汉堡海外仓</option></select></div><div class="form-group"><label>所属 FBA 仓 *</label><select id="fbp-form-warehouse"><option>FBA ONT8</option><option>FBA CLT2</option><option>FBA MEM1</option><option>DE Frankfurt</option><option>FBA IND9</option></select></div></div><div class="form-row"><div class="form-group"><label>状态 *</label><select id="fbp-form-status"><option>闲置</option><option>占用</option></select></div><div class="form-group" style="visibility:hidden"></div></div><div class="form-group"><label>备注</label><textarea id="fbp-form-remark" placeholder="请输入备注"></textarea></div></div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-fba-pallet-form')">取消</button><button class="btn btn-primary" onclick="saveFbaPalletForm()">保存</button></div></div></div>
|
||||
<div class="modal-overlay" id="modal-fba-pallet-form"><div class="modal" style="max-width:760px"><div class="modal-header"><span id="fbp-form-title">新增托盘</span><span onclick="hideModal('modal-fba-pallet-form')" style="cursor:pointer;font-size:20px">×</span></div><div class="modal-body"><div class="form-row"><div class="form-group"><label>托盘编码 *</label><input id="fbp-form-code" maxlength="64" placeholder="请输入托盘编码"></div><div class="form-group"><label>托盘类型 *</label><select id="fbp-form-type"><option>木托盘</option><option>塑料托盘</option><option>金属托盘</option><option>纸托盘</option><option>免熏蒸</option></select></div></div><div class="form-row"><div class="form-group"><label>容积(m³) *</label><input id="fbp-form-volume" type="number" min="0.01" step="0.01" placeholder="请输入容积"></div><div class="form-group"><label>托盘重量(kg) *</label><input id="fbp-form-weight" type="number" min="0.01" step="0.01" required aria-required="true" placeholder="请输入托盘重量"></div></div><div class="form-row"><div class="form-group"><label>采购价格(USD) *</label><input id="fbp-form-price" type="number" min="0" step="0.01" placeholder="请输入采购价格"></div><div class="form-group" style="visibility:hidden"></div></div><div class="form-row"><div class="form-group"><label>仓库 *</label><select id="fbp-form-overseas-warehouse"><option>洛杉矶长滩海外仓</option><option>纽约海外仓</option><option>汉堡海外仓</option></select></div><div class="form-group"><label>所属 FBA 仓 *</label><select id="fbp-form-warehouse"><option>FBA ONT8</option><option>FBA CLT2</option><option>FBA MEM1</option><option>DE Frankfurt</option><option>FBA IND9</option></select></div></div><div class="form-row"><div class="form-group"><label>状态 *</label><select id="fbp-form-status"><option>闲置</option><option>占用</option></select></div><div class="form-group" style="visibility:hidden"></div></div><div class="form-group"><label>备注</label><textarea id="fbp-form-remark" placeholder="请输入备注"></textarea></div></div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-fba-pallet-form')">取消</button><button class="btn btn-primary" onclick="saveFbaPalletForm()">保存</button></div></div></div>
|
||||
|
||||
<!-- ==================== FBA RETURN - 箱号管理 ==================== -->
|
||||
<div class="page" id="page-fba-carton">
|
||||
|
||||
@@ -969,7 +969,7 @@
|
||||
|
||||
<!-- ==================== CONTAINER LIST ==================== -->
|
||||
<div class="page" id="page-ct-list">
|
||||
<div class="table-card"><div class="table-toolbar"><span class="title">货柜列表</span><div class="toolbar-actions"><button class="btn btn-primary" onclick="openCtForm()">+ 新增</button><button class="btn btn-outline-primary">↓导出柜号费用</button><button class="btn btn-outline-primary">↓导出柜号费用明细</button><button class="btn btn-outline-primary">▼高级查询</button></div></div>
|
||||
<div class="table-card"><div class="table-toolbar"><span class="title">货柜列表</span><div class="toolbar-actions"><button class="btn btn-primary" onclick="openCtForm()">+ 新增</button><button class="btn btn-primary" onclick="openCtPalletLabelPreview()">生成托盘标</button><button class="btn btn-outline-primary">↓导出柜号费用</button><button class="btn btn-outline-primary">↓导出柜号费用明细</button><button class="btn btn-outline-primary">▼高级查询</button></div></div>
|
||||
<div class="search-row" style="flex-wrap:wrap;gap:8px">
|
||||
<input placeholder="请输入集装箱号" style="width:180px"/>
|
||||
<input placeholder="请输入柜号" style="width:180px"/>
|
||||
@@ -987,7 +987,7 @@
|
||||
<span><a href="#" style="color:#666;text-decoration:none;font-size:12px;margin-right:10px">⟳ 刷新</a><a href="#" style="color:#666;text-decoration:none;font-size:12px">⚙ 设置</a></span>
|
||||
</div>
|
||||
<div style="overflow-x:auto;-webkit-overflow-scrolling:touch">
|
||||
<table style="min-width:2800px"><thead><tr><th width="36"><input type="checkbox" onclick="toggleCtAll(this)"/></th><th style="width:300px;white-space:nowrap;text-align:center">操作</th><th>报空预警</th><th>集装箱号</th><th>装柜清单号</th><th>货柜状态</th><th style="white-space:nowrap">是否异常</th><th>柜号</th><th>目的港清关资料</th><th>DO资料</th><th>PU资料</th><th style="white-space:nowrap">码头车队</th><th>仓库</th><th>提单号</th><th>船名航次</th><th>箱型(货柜类型)</th><th>船公司</th><th>始发港</th><th>目的港</th><th>货柜容量(m³)</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-list"></tbody></table>
|
||||
<table style="min-width:2800px"><thead><tr><th width="36"><input type="checkbox" onclick="toggleCtAll(this)"/></th><th style="width:300px;white-space:nowrap;text-align:center">操作</th><th>报空预警</th><th>集装箱号</th><th>装柜清单号</th><th>货柜状态</th><th style="white-space:nowrap">是否异常</th><th>柜号</th><th>目的港清关资料</th><th>DO资料</th><th>PU资料</th><th style="white-space:nowrap">码头车队</th><th>仓库</th><th>提单号</th><th>船名航次</th><th>箱型(货柜类型)</th><th>船公司</th><th>始发港</th><th>目的港</th><th>货柜容量(m³)</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-list"></tbody></table>
|
||||
</div>
|
||||
<div class="pagination"><span>共 685 条</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">137</button></div></div>
|
||||
|
||||
@@ -1013,6 +1013,7 @@
|
||||
<!-- ==================== CONTAINER MANIFEST ==================== -->
|
||||
<div class="page" id="page-ct-manifest">
|
||||
<div class="table-card">
|
||||
<div class="table-toolbar"><span class="title">装柜清单</span></div>
|
||||
<!-- 搜索栏 -->
|
||||
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:12px">
|
||||
<label style="font-size:13px;color:#333;font-weight:500;white-space:nowrap">仓库:</label>
|
||||
@@ -1023,8 +1024,6 @@
|
||||
<button class="btn btn-sm btn-default" style="padding:7px 18px;border-radius:6px" onclick="mfReset()">🔄 重置</button>
|
||||
<a href="javascript:void(0)" style="color:#1890ff;text-decoration:none;font-size:13px;white-space:nowrap" onclick="this.textContent=this.textContent.includes('展开')?'收起 ▲':'展开 ▼'">展开 ▼</a>
|
||||
</div>
|
||||
<!-- 查看查询按钮 -->
|
||||
<div style="margin-bottom:14px"><button class="btn btn-primary" style="padding:7px 20px;border-radius:6px;font-size:13px" onclick="showModal('modal-mf-query')">▼ 查看查询</button></div>
|
||||
<!-- 选择条 -->
|
||||
<div style="background:#e6f4ff;border:1px solid #91caff;border-radius:6px;padding:8px 14px;display:flex;align-items:center;justify-content:space-between;margin-bottom:14px">
|
||||
<span style="font-size:13px;color:#1890ff">已选择 <b id="mf-sel-count">0</b> 项 <a href="javascript:void(0)" style="color:#1890ff;text-decoration:none" onclick="mfClearSel()">清空</a></span>
|
||||
@@ -1037,7 +1036,7 @@
|
||||
<div style="overflow-x:auto">
|
||||
<table style="min-width:1600px;font-size:12px"><thead><tr style="background:#fafafa;color:#666">
|
||||
<th style="padding:9px 10px;text-align:center;width:36px"><input type="checkbox" onclick="mfToggleAll(this)"/></th>
|
||||
<th style="padding:9px 10px;text-align:center;white-space:nowrap;width:130px">操作</th>
|
||||
<th style="padding:9px 10px;text-align:center;white-space:nowrap;width:180px">操作</th>
|
||||
<th style="padding:9px 10px;text-align:left;white-space:nowrap">仓库</th>
|
||||
<th style="padding:9px 10px;text-align:left;white-space:nowrap">装柜清单号</th>
|
||||
<th style="padding:9px 10px;text-align:left;white-space:nowrap">集装箱号</th>
|
||||
|
||||
@@ -198,11 +198,11 @@
|
||||
|
||||
<!-- ==================== OVERSEAS PURCHASE DETAIL ==================== -->
|
||||
<div class="page" id="page-domestic-purchase-detail">
|
||||
<div class="purchase-edit-shell"><div class="purchase-detail-head"><div class="purchase-detail-title">国内采购订单详情</div><button class="btn btn-default" onclick="switchPage('overseas-purchase');switchPurchaseOrderTab('domestic')">返回</button></div><div class="purchase-detail-body"><div class="purchase-detail-grid"><section class="purchase-detail-section"><h3>商品信息</h3><div id="domestic-detail-product" class="purchase-detail-fields"></div></section><section class="purchase-detail-section"><h3>库存信息</h3><div id="domestic-detail-stock" class="purchase-detail-fields"></div></section><section class="purchase-detail-section"><h3>订单信息</h3><div id="domestic-detail-order" class="purchase-detail-fields"></div></section><section class="purchase-detail-section"><h3>快递信息</h3><div id="domestic-detail-courier" class="purchase-detail-fields"></div></section><section class="purchase-detail-section full"><h3>进销存明细</h3><div id="domestic-detail-ledger" class="purchase-ledger-wrap"></div></section></div></div></div>
|
||||
<div class="purchase-edit-shell"><div class="purchase-detail-head"><div class="purchase-detail-title">国内采购订单详情</div><button class="btn btn-default" onclick="switchPage('overseas-purchase');switchPurchaseOrderTab('domestic')">返回</button></div><div class="purchase-detail-body"><div class="purchase-detail-grid"><section class="purchase-detail-section"><h3>商品信息</h3><div id="domestic-detail-product" class="purchase-detail-fields"></div></section><section class="purchase-detail-section"><h3>库存信息</h3><div id="domestic-detail-stock" class="purchase-detail-fields"></div></section><section class="purchase-detail-section"><h3>订单信息</h3><div id="domestic-detail-order" class="purchase-detail-fields"></div></section><section class="purchase-detail-section"><h3>快递信息</h3><div id="domestic-detail-courier" class="purchase-detail-fields"></div></section><section class="purchase-detail-section full"><h3>操作记录</h3><div id="domestic-detail-operation-records" class="purchase-ledger-wrap"></div></section></div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="page" id="page-overseas-purchase-detail">
|
||||
<div class="purchase-edit-shell"><div class="purchase-detail-head"><div class="purchase-detail-title">海外采购订单详情</div><button class="btn btn-default" onclick="switchPage('overseas-purchase');switchPurchaseOrderTab('overseas')">返回</button></div><div class="purchase-detail-body"><div class="purchase-detail-grid"><section class="purchase-detail-section"><h3>商品信息</h3><div id="overseas-detail-product" class="purchase-detail-fields"></div></section><section class="purchase-detail-section"><h3>库存信息</h3><div id="overseas-detail-stock" class="purchase-detail-fields"></div></section><section class="purchase-detail-section"><h3>订单信息</h3><div id="overseas-detail-order" class="purchase-detail-fields"></div></section><section class="purchase-detail-section"><h3>快递信息</h3><div id="overseas-detail-courier" class="purchase-detail-fields"></div></section><section class="purchase-detail-section full"><h3>进销存明细</h3><div id="overseas-detail-ledger" class="purchase-ledger-wrap"></div></section></div></div></div>
|
||||
<div class="purchase-edit-shell"><div class="purchase-detail-head"><div class="purchase-detail-title">海外采购订单详情</div><button class="btn btn-default" onclick="switchPage('overseas-purchase');switchPurchaseOrderTab('overseas')">返回</button></div><div class="purchase-detail-body"><div class="purchase-detail-grid"><section class="purchase-detail-section"><h3>商品信息</h3><div id="overseas-detail-product" class="purchase-detail-fields"></div></section><section class="purchase-detail-section"><h3>库存信息</h3><div id="overseas-detail-stock" class="purchase-detail-fields"></div></section><section class="purchase-detail-section"><h3>订单信息</h3><div id="overseas-detail-order" class="purchase-detail-fields"></div></section><section class="purchase-detail-section"><h3>快递信息</h3><div id="overseas-detail-courier" class="purchase-detail-fields"></div></section><section class="purchase-detail-section full"><h3>操作记录</h3><div id="overseas-detail-operation-records" class="purchase-ledger-wrap"></div></section></div></div></div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== LEGACY PURCHASE DETAIL (UNLINKED) ==================== -->
|
||||
@@ -428,4 +428,3 @@
|
||||
<button class="btn btn-primary" style="padding:8px 28px;border-radius:6px" onclick="saveLooseboardOrderEdit()">保存修改</button>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
|
||||
@@ -639,16 +639,15 @@
|
||||
<div class="stat-row" style="grid-template-columns:repeat(4,1fr)"><div class="stat-card"><div class="label">客户账户数</div><div class="value" id="ca-stat-count">0<span>个</span></div></div><div class="stat-card"><div class="label">账户实际余额</div><div class="value" id="ca-stat-balance">¥0</div></div><div class="stat-card"><div class="label">冻结金额</div><div class="value" id="ca-stat-frozen" style="color:#f59e0b">¥0</div></div><div class="stat-card"><div class="label">本月累计扣款</div><div class="value" id="ca-stat-deduction" style="color:#ef4444">¥0</div></div></div>
|
||||
<div class="table-card"><div class="table-toolbar"><span class="title">客户账户</span><div class="toolbar-actions"><button class="btn btn-default" onclick="showToast('客户账户数据已导出','success')">导出</button></div></div>
|
||||
<div class="search-row"><input id="ca-filter-keyword" placeholder="客户名称/客户编号" style="width:210px"><select id="ca-filter-status" style="width:140px"><option value="">全部状态</option><option>正常</option><option>冻结</option><option>停用</option></select><select id="ca-filter-balance" style="width:150px"><option value="">全部余额</option><option value="positive">余额大于0</option><option value="zero">余额为0</option><option value="negative">余额小于0</option></select><button class="btn btn-sm btn-primary" onclick="renderCustomerAccounts()">查询</button><button class="btn btn-sm btn-default" onclick="resetCustomerAccountFilters()">重置</button></div>
|
||||
<div class="inbound-table-container"><table><thead><tr><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-customer-account"></tbody></table></div><div class="pagination"><span id="ca-total-count">共 0 条</span><button class="page-btn active">1</button></div></div></div>
|
||||
<div class="inbound-table-container"><table><thead><tr><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-customer-account"></tbody></table></div><div class="pagination"><span id="ca-total-count">共 0 条</span><button class="page-btn active">1</button></div></div>
|
||||
<div class="table-card" style="margin-top:16px"><div class="table-toolbar"><span class="title">充值申请记录</span></div><div class="search-row"><input id="customer-recharge-application-keyword" placeholder="申请单号/客户名称" style="width:210px"><select id="customer-recharge-application-status" style="width:140px"><option value="">全部状态</option><option>待审核</option><option>已通过</option><option>已驳回</option></select><button class="btn btn-sm btn-primary" onclick="renderCustomerRechargeApplications()">查询</button><button class="btn btn-sm btn-default" onclick="resetCustomerRechargeApplicationFilters()">重置</button></div><div class="inbound-table-container"><table><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></tr></thead><tbody id="tbody-customer-recharge-application"></tbody></table></div><div class="pagination"><span id="customer-recharge-application-total">共 0 条</span></div></div></div>
|
||||
|
||||
<div class="page" id="page-customer-account-detail">
|
||||
<div class="page-nav"><span class="nav-item" onclick="switchPage('customer-account',this)">财务管理</span><span class="nav-sep">></span><span class="nav-item" onclick="switchPage('customer-account',this)">客户账户</span><span class="nav-sep">></span><span class="nav-item active">账户详情</span></div>
|
||||
<div class="table-card" style="margin-bottom:16px"><div class="table-toolbar"><div><span class="title" id="ca-detail-name">客户账户详情</span><span class="tag tag-green" id="ca-detail-status" style="margin-left:10px">正常</span></div><div class="toolbar-actions"><button class="btn btn-default" onclick="switchPage('customer-account',this)">返回列表</button><button class="btn btn-primary" id="ca-detail-recharge-btn">手动充值</button><button class="btn btn-default" id="ca-detail-deduction-btn">手动扣款</button></div></div>
|
||||
<div class="table-card" style="margin-bottom:16px"><div class="table-toolbar"><div><span class="title" id="ca-detail-name">客户账户详情</span><span class="tag tag-green" id="ca-detail-status" style="margin-left:10px">正常</span></div><div class="toolbar-actions"><button class="btn btn-default" onclick="switchPage('customer-account',this)">返回列表</button><button class="btn btn-primary" id="ca-detail-recharge-btn" onclick="showCustomerRechargeApplication(currentCustomerAccountId)">充值申请</button><button class="btn btn-default" id="ca-detail-deduction-btn">手动扣款</button></div></div>
|
||||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:0 20px 18px"><div style="padding:18px;border-radius:12px;background:#f0fdf4;border:1px solid #d7f4df"><div style="font-size:12px;color:#64748b">实际余额</div><div id="ca-detail-balance" style="font-size:26px;font-weight:800;color:#16a34a;margin-top:8px">¥0.00</div></div><div style="padding:18px;border-radius:12px;background:#fffaf0;border:1px solid #ffebc0"><div style="font-size:12px;color:#64748b">冻结金额</div><div id="ca-detail-frozen" style="font-size:26px;font-weight:800;color:#f59e0b;margin-top:8px">¥0.00</div></div><div style="padding:18px;border-radius:12px;background:#f6f7ff;border:1px solid #e4e7ff"><div style="font-size:12px;color:#64748b">可用余额</div><div id="ca-detail-available" style="font-size:26px;font-weight:800;color:#667eea;margin-top:8px">¥0.00</div></div></div><div class="ca-info-grid" id="ca-detail-info" style="margin:0 20px 20px"></div></div>
|
||||
<div class="table-card"><div class="table-toolbar"><span class="title">账户明细(充值/扣款)</span><div class="toolbar-actions"><button class="btn btn-default" onclick="showToast('账户明细已导出','success')">导出明细</button></div></div><div class="search-row"><select id="ca-detail-type" style="width:150px"><option value="">全部扣款类型</option><option>充值</option><option>贴牌</option><option>出库</option><option>仓储费用</option><option>自提</option><option>暂扣</option><option>弃货</option><option>其它</option></select><input id="ca-detail-start" type="date" style="width:150px"><span style="color:#94a3b8">至</span><input id="ca-detail-end" type="date" style="width:150px"><button class="btn btn-sm btn-primary" onclick="renderCustomerAccountTransactions()">查询</button><button class="btn btn-sm btn-default" onclick="resetCustomerAccountTransactions()">重置</button></div><div class="inbound-table-container"><table><thead><tr><th>流水号</th><th>发生时间</th><th>扣款类型</th><th>关联单据</th><th>收入</th><th>支出</th><th>变动后余额</th><th>操作人</th><th>备注</th></tr></thead><tbody id="tbody-customer-account-detail"></tbody></table></div><div class="pagination"><span id="ca-detail-total">共 0 条</span><button class="page-btn active">1</button></div></div></div>
|
||||
|
||||
<div class="modal-overlay" id="modal-customer-recharge"><div class="modal" style="max-width:620px"><div class="modal-header"><span>手动充值</span><span onclick="hideModal('modal-customer-recharge')" style="cursor:pointer;font-size:20px">×</span></div><div class="modal-body" style="padding:22px 26px"><div style="padding:12px 14px;border-radius:8px;background:#f6f7ff;color:#475569;font-size:13px;margin-bottom:18px">充值后金额将立即计入客户实际余额,并自动生成一条“充值”账户明细。</div><input type="hidden" id="ca-recharge-id"><div class="form-row"><div class="form-group"><label>客户名称</label><input id="ca-recharge-customer" readonly style="background:#f5f6f8"></div><div class="form-group"><label>当前实际余额</label><input id="ca-recharge-current" readonly style="background:#f5f6f8;color:#16a34a;font-weight:700"></div></div><div class="form-row"><div class="form-group"><label>充值金额 <span style="color:#ef4444">*</span></label><input id="ca-recharge-amount" type="number" min="0.01" step="0.01" placeholder="请输入充值金额"></div><div class="form-group"><label>充值方式 <span style="color:#ef4444">*</span></label><select id="ca-recharge-method"><option>银行转账</option><option>支付宝</option><option>微信支付</option><option>现金</option><option>其它</option></select></div></div><div class="form-row"><div class="form-group"><label>到账日期 <span style="color:#ef4444">*</span></label><input id="ca-recharge-date" type="date"></div><div class="form-group"><label>收款凭证号</label><input id="ca-recharge-ref" placeholder="请输入银行流水/凭证号"></div></div><div class="form-row"><div class="form-group" style="width:100%"><label>充值备注</label><textarea id="ca-recharge-remark" rows="3" placeholder="请输入充值说明" style="resize:vertical"></textarea></div></div></div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-customer-recharge')">取消</button><button class="btn btn-primary" onclick="saveCustomerRecharge()">确认充值</button></div></div></div>
|
||||
|
||||
<!-- ==================== FINANCE COST ANALYSIS ==================== -->
|
||||
<div class="page" id="page-finance-cost">
|
||||
<div class="chart-row equal" style="margin-bottom:16px">
|
||||
@@ -720,7 +719,7 @@
|
||||
<!-- 数据表格 -->
|
||||
<div class="arap-table-wrap">
|
||||
<table class="arap-table">
|
||||
<colgroup><col style="width:40px"/><col style="width:255px"/><col style="width:520px"/><col style="width:120px"/><col style="width:80px"/><col style="width:96px"/><col style="width:84px"/><col style="width:160px"/><col style="width:110px"/><col style="width:140px"/><col style="width:90px"/><col style="width:130px"/><col style="width:180px"/></colgroup>
|
||||
<colgroup><col style="width:40px"/><col style="width:220px"/><col style="width:240px"/><col style="width:120px"/><col style="width:80px"/><col style="width:96px"/><col style="width:84px"/><col style="width:160px"/><col style="width:110px"/><col style="width:140px"/><col style="width:90px"/><col style="width:110px"/><col style="width:130px"/><col style="width:180px"/></colgroup>
|
||||
<thead><tr style="background:#fafafa;border-bottom:2px solid #eee">
|
||||
<th style="padding:10px 5px;text-align:left;font-weight:600;color:#333;font-size:12px"><input type="checkbox"/></th>
|
||||
<th style="padding:10px 5px;text-align:left;font-weight:600;color:#333;font-size:12px">操作</th>
|
||||
@@ -733,6 +732,7 @@
|
||||
<th style="padding:10px 5px;text-align:left;font-weight:600;color:#333;font-size:12px">订单状态</th>
|
||||
<th style="padding:10px 5px;text-align:left;font-weight:600;color:#333;font-size:12px">派送单号</th>
|
||||
<th style="padding:10px 5px;text-align:left;font-weight:600;color:#333;font-size:12px">费用类型</th>
|
||||
<th style="padding:10px 5px;text-align:left;font-weight:600;color:#333;font-size:12px">金额</th>
|
||||
<th style="padding:10px 5px;text-align:left;font-weight:600;color:#333;font-size:12px">集装箱号</th>
|
||||
<th style="padding:10px 5px;text-align:left;font-weight:600;color:#333;font-size:12px">柜号</th>
|
||||
</tr></thead>
|
||||
@@ -972,7 +972,6 @@
|
||||
<th>操作</th>
|
||||
<th>结算单号</th>
|
||||
<th>客户公司名</th>
|
||||
<th>费用类型</th>
|
||||
<th>币种</th>
|
||||
<th>应收金额</th>
|
||||
<th>已收</th>
|
||||
@@ -1032,7 +1031,6 @@
|
||||
<th>操作</th>
|
||||
<th>结算单号</th>
|
||||
<th>供应商</th>
|
||||
<th>费用类型</th>
|
||||
<th>币种</th>
|
||||
<th>应付金额</th>
|
||||
<th>已付</th>
|
||||
@@ -1112,13 +1110,12 @@
|
||||
<div class="table-toolbar"><span class="title">财务利润表</span><div class="toolbar-actions"><button class="btn btn-default">导出Excel</button></div></div>
|
||||
<div id="profit-business-tabs" class="profit-business-tabs">
|
||||
<button type="button" class="profit-business-tab active" data-profit-business="firstMile" onclick="switchProfitBusiness('firstMile',this)">头程</button>
|
||||
<button type="button" class="profit-business-tab" data-profit-business="warehouseRent" onclick="switchProfitBusiness('warehouseRent',this)">仓租</button>
|
||||
<button type="button" class="profit-business-tab" data-profit-business="relabel" onclick="switchProfitBusiness('relabel',this)">贴标</button>
|
||||
<button type="button" class="profit-business-tab" data-profit-business="warehouseTransfer" onclick="switchProfitBusiness('warehouseTransfer',this)">仓储转运</button>
|
||||
<button type="button" class="profit-business-tab" data-profit-business="lastMile" onclick="switchProfitBusiness('lastMile',this)">尾程</button>
|
||||
</div>
|
||||
<div class="search-row" style="flex-wrap:wrap;gap:8px">
|
||||
<span style="color:#666;align-self:center">查询维度</span>
|
||||
<select id="profit-scope" style="width:130px" onchange="profitScopeChange()"><option value="customer">客户</option><option value="container">货柜</option><option value="ticket">单票</option><option value="month">单月</option></select>
|
||||
<select id="profit-order-type" style="width:150px"><option value="">全部订单类型</option><option value="暂扣">暂扣</option><option value="退货">退货</option><option value="仓储转运">仓储转运</option><option value="换标">换标</option></select>
|
||||
<input id="profit-kw" placeholder="请输入客户名称" style="width:230px"/>
|
||||
<span style="color:#999;align-self:center">时间</span>
|
||||
<input id="profit-start" type="month" style="width:150px" title="开始月份"/>
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
<button class="btn btn-sm btn-primary" onclick="renderSalesCommission()">查询</button>
|
||||
<button class="btn btn-sm btn-default" onclick="scReset()">重置</button>
|
||||
</div>
|
||||
<table><thead><tr><th>销售编号</th><th>销售姓名</th><th>区域/团队</th><th>客户数</th><th>订单数</th><th>销售额(元)</th><th>综合提成率</th><th>提成金额(元)</th><th>操作</th></tr></thead><tbody id="tbody-sales-commission"></tbody><tfoot id="tfoot-sales-commission"></tfoot></table>
|
||||
<table><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></tr></thead><tbody id="tbody-sales-commission"></tbody><tfoot id="tfoot-sales-commission"></tfoot></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -290,8 +290,8 @@ FORM: 通用模块中的管理清单加分区编辑弹窗,作为既有后台
|
||||
<div class="cost-field"><label>参考时效</label><input id="cost-time" value="船开后约35天"/></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cost-table-wrap"><table class="cost-table"><thead><tr><th>操作</th><th>亚马逊仓</th><th>AMS+换单费</th><th>内陆</th><th>清关/报关费</th><th>关税</th><th>提柜费</th><th>拆柜费</th><th>托盘费</th><th>派送费</th><th>费用小计</th><th>海运费成本</th><th>总成本(USD)</th><th>汇率</th><th>折人民币</th><th id="cost-base-header">重量(kg)</th><th id="cost-unit-header">成本单价(元/kg)</th><th>建议报价</th></tr></thead><tbody id="tbody-voyage-cost"></tbody></table></div>
|
||||
<div class="cost-footnote">计算公式:费用小计 = AMS/换单 + 内陆 + 清关/报关 + 关税 + 提柜 + 拆柜 + 托盘 + 派送;总成本 = 小计 + 海运费;成本单价 = 总成本 × 汇率 ÷ 计费基数。</div>
|
||||
<div class="cost-table-wrap"><table class="cost-table"><thead><tr><th>操作</th><th>亚马逊仓</th><th>AMS+换单费</th><th>内陆</th><th>清关/报关费</th><th>关税</th><th>提柜费</th><th>拆柜费</th><th>托盘费</th><th>派送费</th><th>费用小计</th><th>海运费成本</th><th>总成本(USD)</th><th>汇率</th><th>折人民币</th><th id="cost-base-header">重量(kg)</th><th id="cost-unit-header">成本单价(元/kg)</th><th>销售底价</th><th>建议报价</th></tr></thead><tbody id="tbody-voyage-cost"></tbody></table></div>
|
||||
<div class="cost-footnote">计算公式:费用小计 = AMS/换单 + 内陆 + 清关/报关 + 关税 + 提柜 + 拆柜 + 托盘 + 派送;总成本 = 小计 + 海运费;成本单价 = 总成本 × 汇率 ÷ 计费基数;销售底价 = 建议报价 - 0.6。</div>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
@@ -324,6 +324,7 @@ FORM: 通用模块中的管理清单加分区编辑弹窗,作为既有后台
|
||||
<th style="padding:9px 10px;text-align:right;white-space:nowrap">当前公里码(km)</th>
|
||||
<th style="padding:9px 10px;text-align:left;white-space:nowrap">司机</th>
|
||||
<th style="padding:9px 10px;text-align:left;white-space:nowrap">联系电话</th>
|
||||
<th class="vehicle-overseas-only" style="display:none;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;width:160px">操作</th>
|
||||
@@ -339,6 +340,7 @@ FORM: 通用模块中的管理清单加分区编辑弹窗,作为既有后台
|
||||
<th style="padding:9px 10px;text-align:right;white-space:nowrap">当前公里码(km)</th>
|
||||
<th style="padding:9px 10px;text-align:left;white-space:nowrap">外派单位</th>
|
||||
<th style="padding:9px 10px;text-align:left;white-space:nowrap">联系电话</th>
|
||||
<th class="vehicle-overseas-only" style="display:none;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;width:180px">最近更换原因</th>
|
||||
<th style="padding:9px 10px;text-align:center;white-space:nowrap">状态</th>
|
||||
@@ -369,7 +371,11 @@ FORM: 通用模块中的管理清单加分区编辑弹窗,作为既有后台
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>司机 / 外派单位</label><input id="vehicle-m-driver" placeholder="司机姓名 或 外派单位"/></div>
|
||||
<div class="form-group"><label>区域</label><select id="vehicle-m-region" style="height:40px"><option value="domestic">国内</option><option value="overseas">海外</option></select></div>
|
||||
<div class="form-group"><label>区域</label><select id="vehicle-m-region" style="height:40px" onchange="syncVehicleEmailFieldVisibility()"><option value="domestic">国内</option><option value="overseas">海外</option></select></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group" id="vehicle-email-group" style="display:none"><label>邮箱</label><input id="vehicle-m-email" type="email" placeholder="海外车队联系邮箱"/></div>
|
||||
<div class="form-group"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>状态</label><select id="vehicle-m-status" style="height:40px"><option>在役</option><option>外派中</option><option>维修中</option><option>停用</option><option>已归还</option></select></div>
|
||||
|
||||
@@ -562,7 +562,7 @@
|
||||
<div class="form-row single"><div class="form-group"><label>到港时间:</label><input id="ct-add-eta" type="date" placeholder="选择到港时间"/></div></div>
|
||||
<div class="form-row single"><div class="form-group"><label>到库时间:</label><input id="ct-add-etw" type="date" placeholder="选择到库时间"/></div></div>
|
||||
<div class="form-row single"><div class="form-group"><label>预计提柜时间:</label><input id="ct-add-pickup-expected" type="datetime-local"/></div></div>
|
||||
<div class="form-row single"><div class="form-group"><label>预计报空时间:</label><input id="ct-add-empty-expected" type="datetime-local"/></div></div>
|
||||
<div class="form-row single"><div class="form-group"><label>约车时间:</label><input id="ct-add-appointment-at" type="datetime-local"/></div></div>
|
||||
<div class="form-row single"><div class="form-group"><label>报空时间:</label><input id="ct-add-emptied-at" type="datetime-local"/></div></div>
|
||||
<div class="form-row single"><div class="form-group"><label>还柜时间:</label><input id="ct-add-returned-at" type="datetime-local"/></div></div>
|
||||
<div class="form-row single"><div class="form-group"><label>清关申报金额(USD):</label><input id="ct-add-declare" type="number" placeholder="请输入清关申报金额(USD)"/></div></div>
|
||||
@@ -1595,8 +1595,8 @@
|
||||
<div class="form-row"><div class="form-group"><label>客户公司名 *</label><input id="arap-form-customer" maxlength="120" placeholder="请输入客户公司名"/></div><div class="form-group"><label>参与结算 *</label><select id="arap-form-join-settle"><option value="">请选择</option><option>是</option><option>否</option></select></div></div>
|
||||
<div class="form-row"><div class="form-group"><label>应收/应付 *</label><select id="arap-form-type"><option value="">请选择</option><option>应收</option><option>应付</option></select></div><div class="form-group"><label>入仓码 *</label><input id="arap-form-wh-code" maxlength="64" placeholder="请输入入仓码"/></div></div>
|
||||
<div class="form-row"><div class="form-group"><label>订单状态 *</label><input id="arap-form-order-status" maxlength="40" placeholder="请输入订单状态"/></div><div class="form-group"><label>派送单号</label><input id="arap-form-dispatch-no" maxlength="64" placeholder="选填"/></div></div>
|
||||
<div class="form-row"><div class="form-group"><label>费用类型 *</label><input id="arap-form-fee-type" maxlength="40" placeholder="请输入费用类型"/></div><div class="form-group"><label>集装箱号</label><input id="arap-form-container-no" maxlength="32" placeholder="选填"/></div></div>
|
||||
<div class="form-row single"><div class="form-group"><label>柜号</label><input id="arap-form-cabinet-no" maxlength="80" placeholder="选填"/></div></div>
|
||||
<div class="form-row"><div class="form-group"><label>费用类型 *</label><input id="arap-form-fee-type" maxlength="40" placeholder="请输入费用类型"/></div><div class="form-group"><label>金额 *</label><input id="arap-form-amount" type="number" min="0" step="0.01" placeholder="请输入金额"/></div></div>
|
||||
<div class="form-row"><div class="form-group"><label>集装箱号</label><input id="arap-form-container-no" maxlength="32" placeholder="选填"/></div><div class="form-group"><label>柜号</label><input id="arap-form-cabinet-no" maxlength="80" placeholder="选填"/></div></div>
|
||||
<div style="font-size:12px;color:#64748b;line-height:1.6">保存后记录将进入“待审核”,审核通过后不可编辑。</div>
|
||||
</div>
|
||||
<div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-arap-form')">取消</button><button class="btn btn-primary" onclick="saveArapForm()">保存并提交审核</button></div>
|
||||
@@ -1609,7 +1609,7 @@
|
||||
<div class="modal-header"><span>审核客户应收应付</span><span onclick="hideModal('modal-arap-audit')" style="cursor:pointer;font-size:20px" aria-label="关闭">×</span></div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="arap-audit-index" value="-1"/>
|
||||
<div style="display:grid;grid-template-columns:90px 1fr;gap:10px 14px;padding:14px;background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;margin-bottom:16px;font-size:13px"><span style="color:#64748b">费用单号</span><strong id="arap-audit-doc-no">—</strong><span style="color:#64748b">客户公司</span><strong id="arap-audit-customer">—</strong><span style="color:#64748b">费用类型</span><strong id="arap-audit-fee-type">—</strong><span style="color:#64748b">当前状态</span><strong id="arap-audit-status">—</strong></div>
|
||||
<div style="display:grid;grid-template-columns:90px 1fr;gap:10px 14px;padding:14px;background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;margin-bottom:16px;font-size:13px"><span style="color:#64748b">费用单号</span><strong id="arap-audit-doc-no">—</strong><span style="color:#64748b">客户公司</span><strong id="arap-audit-customer">—</strong><span style="color:#64748b">费用类型</span><strong id="arap-audit-fee-type">—</strong><span style="color:#64748b">金额</span><strong id="arap-audit-amount">—</strong><span style="color:#64748b">当前状态</span><strong id="arap-audit-status">—</strong></div>
|
||||
<div class="form-group"><label>审核备注</label><textarea id="arap-audit-remark" rows="3" maxlength="300" placeholder="审核通过时可填写备注"></textarea></div>
|
||||
<div class="form-group"><label>驳回原因(驳回时必填)</label><textarea id="arap-audit-reject-reason" rows="3" maxlength="300" placeholder="选择审核驳回时必填"></textarea></div>
|
||||
</div>
|
||||
@@ -1678,6 +1678,25 @@
|
||||
<div class="form-group"><label>取消原因 *</label><textarea id="fboo-cancel-reason" rows="3" maxlength="200" placeholder="请输入取消原因"></textarea></div>
|
||||
</div><div class="modal-footer"><button class="btn btn-default" onclick="hideModal('modal-fboo-cancel')">返回</button><button class="btn btn-danger" onclick="confirmFbaOutboundCancellation()">确认取消</button></div></div></div>
|
||||
|
||||
<!-- 客户账户:充值申请 -->
|
||||
<style>
|
||||
.customer-recharge-layout{display:grid;grid-template-columns:minmax(0,1fr) 260px;gap:22px}.customer-recharge-layout>*{min-width:0}.customer-recharge-qr{padding:16px;border:1px solid #e5e7eb;border-radius:8px;text-align:center;background:#fafafa}.customer-recharge-qr img{display:block;width:min(100%,220px);height:auto;margin:0 auto}.customer-recharge-voucher-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 10px;border-bottom:1px solid #eef0f4}.customer-recharge-voucher-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.customer-recharge-review-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 20px}.customer-recharge-review-item{min-width:0}.customer-recharge-review-item label{display:block;font-size:12px;color:#94a3b8;margin-bottom:4px}.customer-recharge-review-item div{font-size:13px;color:#334155;word-break:break-word}@media(max-width:700px){.customer-recharge-layout,.customer-recharge-review-grid{grid-template-columns:minmax(0,1fr)}.customer-recharge-layout{gap:16px}.customer-recharge-layout .form-row{display:block}.customer-recharge-layout .form-group{width:100%;min-width:0}.customer-recharge-layout input,.customer-recharge-layout select,.customer-recharge-layout textarea{box-sizing:border-box;max-width:100%}}
|
||||
</style>
|
||||
<div class="modal-overlay" id="modal-customer-recharge-application">
|
||||
<div class="modal" style="max-width:900px">
|
||||
<div class="modal-header"><span>提交充值申请</span><span onclick="closeCustomerRechargeApplication()" style="cursor:pointer;font-size:20px" aria-label="关闭">×</span></div>
|
||||
<div class="modal-body" style="padding:22px 26px"><input id="customer-recharge-application-id" type="hidden"><div class="customer-recharge-layout"><div>
|
||||
<div class="form-row"><div class="form-group"><label>客户名称</label><input id="customer-recharge-customer" readonly style="background:#f5f6f8"></div><div class="form-group"><label>当前余额</label><input id="customer-recharge-current" readonly style="background:#f5f6f8;color:#16a34a;font-weight:700"></div></div>
|
||||
<div class="form-row"><div class="form-group"><label>充值金额 <span style="color:#ef4444">*</span></label><input id="customer-recharge-amount" type="number" min="0.01" step="0.01" placeholder="请输入充值金额"></div><div class="form-group"><label>充值方式 <span style="color:#ef4444">*</span></label><select id="customer-recharge-method"><option>银行转账</option><option>支付宝</option><option>微信支付</option><option>其它</option></select></div></div>
|
||||
<div class="form-row"><div class="form-group"><label>到账日期 <span style="color:#ef4444">*</span></label><input id="customer-recharge-date" type="date"></div><div class="form-group"><label>收款凭证号</label><input id="customer-recharge-ref" maxlength="80" placeholder="请输入银行流水/凭证号"></div></div>
|
||||
<div class="form-group"><label>充值备注</label><textarea id="customer-recharge-remark" rows="3" maxlength="300" placeholder="请输入充值说明" style="resize:vertical"></textarea></div>
|
||||
<div class="form-group"><label>上传充值凭证 <span style="color:#ef4444">*</span></label><input id="customer-recharge-vouchers" type="file" multiple accept=".pdf,.jpg,.jpeg,.png" onchange="handleCustomerRechargeVouchers(this.files)"><div style="font-size:12px;color:#94a3b8;margin-top:6px">支持 PDF、JPG、JPEG、PNG,单个文件不超过 10 MB,可多选。</div><div id="customer-recharge-voucher-list" style="margin-top:8px;border:1px solid #eef0f4;border-radius:6px"></div></div>
|
||||
</div><div class="customer-recharge-qr"><img id="customer-recharge-qr" src="prototype-src/assets/customer-recharge-qr.svg" alt="示例收款二维码"><div style="font-weight:700;color:#334155;margin-top:10px">柏悦物流(示例)</div><div style="font-size:12px;line-height:1.6;color:#ef4444;margin-top:5px">示例收款二维码,仅用于原型展示</div></div></div></div>
|
||||
<div class="modal-footer"><button class="btn btn-default" onclick="closeCustomerRechargeApplication()">取消</button><button class="btn btn-primary" onclick="submitCustomerRechargeApplication()">提交申请</button></div></div></div>
|
||||
|
||||
<!-- 客户账户:充值申请审核 -->
|
||||
<div class="modal-overlay" id="modal-customer-recharge-review"><div class="modal" style="max-width:720px"><div class="modal-header"><span id="customer-recharge-review-title">充值申请审核</span><span onclick="hideModal('modal-customer-recharge-review')" style="cursor:pointer;font-size:20px" aria-label="关闭">×</span></div><div class="modal-body" style="padding:22px 26px"><div id="customer-recharge-review-summary" class="customer-recharge-review-grid"></div><div class="form-group" style="margin-top:18px"><label>充值凭证</label><div id="customer-recharge-review-vouchers" style="border:1px solid #eef0f4;border-radius:6px"></div></div><div class="form-group" style="margin-top:18px"><label>审核备注 <span id="customer-recharge-review-required" style="color:#ef4444">*</span></label><textarea id="customer-recharge-review-remark" rows="3" maxlength="300" placeholder="驳回时必须填写原因" style="resize:vertical"></textarea></div></div><div class="modal-footer" id="customer-recharge-review-actions"><button class="btn btn-default" onclick="hideModal('modal-customer-recharge-review')">取消</button><button class="btn btn-danger" onclick="handleCustomerRechargeReview('驳回')">审核驳回</button><button class="btn btn-primary" onclick="handleCustomerRechargeReview('通过')">审核通过</button></div></div></div>
|
||||
|
||||
<!-- 客户账户:手动扣款 -->
|
||||
<div class="modal-overlay" id="modal-customer-deduction">
|
||||
<div class="modal" style="max-width:620px">
|
||||
@@ -1754,3 +1773,39 @@
|
||||
<div class="form-row single"><div class="form-group"><label>内部编号:</label><input id="fba-add-inner-code" placeholder="请输入内部编号"/></div></div>
|
||||
</div>
|
||||
<div class="modal-footer"><button class="btn btn-primary" onclick="saveWarehouseForm()">确定</button><button class="btn btn-default" onclick="hideModal('modal-fba-add')">关闭</button></div></div></div>
|
||||
|
||||
<!-- THESIS:批量标签直入打印。 OWN-WORLD:继承后台控件,深色工作台托白纸。 STORY:选柜、调字号、打印。 FIRST VIEWPORT:稳定工具栏与横向A4纸。 FORM:参考深色全屏预览的Operate局部扩展。 -->
|
||||
<div class="modal-overlay ct-pallet-preview-modal" id="modal-ct-pallet-label-preview" role="dialog" aria-modal="true" aria-labelledby="ct-pallet-preview-title">
|
||||
<div class="modal ct-pallet-preview-panel">
|
||||
<div class="ct-pallet-preview-toolbar">
|
||||
<div class="ct-pallet-preview-meta">
|
||||
<span>打印数据</span>
|
||||
<strong id="ct-pallet-preview-title"><span id="ct-pallet-preview-count" aria-live="polite">0</span> 张托盘标</strong>
|
||||
</div>
|
||||
<div class="ct-pallet-preview-controls">
|
||||
<div class="ct-pallet-font-control">
|
||||
<span>托盘标签字号</span>
|
||||
<div class="ct-pallet-font-stepper">
|
||||
<button type="button" aria-label="减小托盘标签字号" title="减小托盘标签字号" onclick="adjustCtPalletLabelFont('code',-10)">-</button>
|
||||
<output id="ct-pallet-code-size" aria-live="polite">120</output>
|
||||
<button type="button" aria-label="增大托盘标签字号" title="增大托盘标签字号" onclick="adjustCtPalletLabelFont('code',10)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ct-pallet-font-control">
|
||||
<span>目的仓字号</span>
|
||||
<div class="ct-pallet-font-stepper">
|
||||
<button type="button" aria-label="减小目的仓字号" title="减小目的仓字号" onclick="adjustCtPalletLabelFont('destination',-10)">-</button>
|
||||
<output id="ct-pallet-destination-size" aria-live="polite">180</output>
|
||||
<button type="button" aria-label="增大目的仓字号" title="增大目的仓字号" onclick="adjustCtPalletLabelFont('destination',10)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-default" onclick="closeCtPalletLabelPreview()">取消</button>
|
||||
<button type="button" class="btn btn-primary ct-pallet-print-action" onclick="printCtPalletLabels()">打印</button>
|
||||
<button type="button" class="ct-pallet-preview-close" id="ct-pallet-preview-close" aria-label="关闭托盘标预览" title="关闭" onclick="closeCtPalletLabelPreview()">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ct-pallet-preview-scroll">
|
||||
<div id="ct-pallet-label-list" aria-live="polite"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -147,7 +147,7 @@ canvas{max-height:280px}
|
||||
#rsq-wrap.rsq-collapsed .rsq-toggle svg{transform:rotate(180deg)}
|
||||
.recon-main-table-wrap{overflow-x:auto}
|
||||
#page-customer-arap .arap-table-wrap{width:100%;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch}
|
||||
#page-customer-arap .arap-table{width:2005px;min-width:2005px;table-layout:fixed;border-collapse:collapse;font-size:12.5px}
|
||||
#page-customer-arap .arap-table{width:1800px;min-width:1800px;table-layout:fixed;border-collapse:collapse;font-size:12.5px}
|
||||
#page-customer-arap .arap-table th{height:44px;padding:10px 8px;background:#f5f6fb;color:#333;font-size:12px;font-weight:600;text-align:left;white-space:nowrap}
|
||||
#page-customer-arap .arap-table td{height:55px;padding:10px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
#page-customer-arap .arap-table th:nth-child(1),#page-customer-arap .arap-table td:nth-child(1){padding-left:5px;padding-right:5px}
|
||||
@@ -1119,6 +1119,64 @@ body.dashboard-fullscreen #page-dashboard table:has(>thead)>tbody>tr>td{backgrou
|
||||
#modal-fba-carton-form .form-group{min-width:0}
|
||||
#modal-fba-carton-form input,#modal-fba-carton-form select,#modal-fba-carton-form textarea{box-sizing:border-box;max-width:100%}
|
||||
@media(max-width:600px){#modal-fba-carton-form .modal{display:flex;flex-direction:column;width:calc(100vw - 24px)!important;max-height:calc(100vh - 24px);overflow:hidden}#modal-fba-carton-form .modal-body{overflow-x:hidden;overflow-y:auto;padding:18px}#modal-fba-carton-form .form-row{grid-template-columns:minmax(0,1fr);gap:14px;margin-bottom:14px}#modal-fba-carton-form .modal-footer{flex:0 0 auto;padding:12px 18px}#modal-fba-carton-form .modal-footer .btn{min-width:0}}
|
||||
|
||||
/* ===== 货柜托盘标全屏预览 ===== */
|
||||
.ct-pallet-preview-modal{--ct-pallet-code-size:120px;--ct-pallet-destination-size:180px;inset:0;padding:0;background:#1b1f22;backdrop-filter:none;align-items:stretch;justify-content:stretch}
|
||||
.ct-pallet-preview-panel{width:100vw;max-width:none;height:100vh;max-height:none;border:0;border-radius:0;box-shadow:none;overflow:hidden;display:flex;flex-direction:column;background:#1b1f22;animation:none}
|
||||
.ct-pallet-preview-toolbar{min-height:72px;flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;gap:20px;padding:12px 18px;border-bottom:1px solid #343a3f;background:#252a2e;color:#fff;box-sizing:border-box}
|
||||
.ct-pallet-preview-meta{flex:0 0 176px;display:grid;gap:3px;min-width:176px}
|
||||
.ct-pallet-preview-meta>span{font-size:12px;color:#b9c0c5}
|
||||
.ct-pallet-preview-meta strong{font-size:15px;color:#fff;white-space:nowrap}
|
||||
.ct-pallet-preview-controls{display:flex;align-items:center;justify-content:flex-end;gap:10px;min-width:0;flex-wrap:wrap}
|
||||
.ct-pallet-font-control{display:flex;align-items:center;gap:8px;white-space:nowrap;font-size:12px;color:#d9dee2}
|
||||
.ct-pallet-font-stepper{height:34px;display:grid;grid-template-columns:34px 48px 34px;align-items:stretch;border:1px solid #535b61;border-radius:6px;overflow:hidden;background:#1b1f22}
|
||||
.ct-pallet-font-stepper button{border:0;background:#343a3f;color:#fff;font-size:18px;line-height:1;cursor:pointer}
|
||||
.ct-pallet-font-stepper button:hover{background:#454d53}
|
||||
.ct-pallet-font-stepper button:focus-visible,.ct-pallet-preview-close:focus-visible,.ct-pallet-preview-toolbar .btn:focus-visible{outline:2px solid #69b1ff;outline-offset:2px}
|
||||
.ct-pallet-font-stepper output{display:flex;align-items:center;justify-content:center;border-inline:1px solid #535b61;color:#fff;font-size:12px;font-variant-numeric:tabular-nums}
|
||||
.ct-pallet-preview-toolbar .btn{height:34px;padding:0 14px;border-radius:6px;font-size:12px;box-shadow:none;transform:none}
|
||||
.ct-pallet-preview-toolbar .btn-default{background:#fff;color:#374151;border:1px solid #d1d5db}
|
||||
.ct-pallet-preview-toolbar .btn-primary{background:#1677ff;color:#fff;border:1px solid #1677ff}
|
||||
.ct-pallet-print-action{background:#0958d9!important;border:1px solid #0958d9!important;color:#fff}
|
||||
.ct-pallet-print-action:hover,.ct-pallet-print-action:focus{background:#003eb3!important;border-color:#003eb3!important;color:#fff}
|
||||
.ct-pallet-preview-toolbar .btn:hover{transform:none;box-shadow:none}
|
||||
.ct-pallet-preview-close{width:34px;height:34px;display:inline-flex;align-items:center;justify-content:center;border:0;border-radius:6px;background:transparent;color:#d9dee2;font-size:24px;line-height:1;cursor:pointer}
|
||||
.ct-pallet-preview-close:hover{background:#343a3f;color:#fff}
|
||||
.ct-pallet-preview-scroll{flex:1;min-width:0;min-height:0;overflow:auto;padding:28px;background:#1b1f22;box-sizing:border-box}
|
||||
#ct-pallet-label-list{width:max-content;min-width:100%;display:grid;justify-items:center;gap:28px}
|
||||
.ct-pallet-label-sheet{page:ctPalletLabel;position:relative;width:297mm;height:210mm;box-sizing:border-box;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12mm;padding:14mm;background:#fff;color:#000;box-shadow:0 8px 28px rgba(0,0,0,.34);overflow:hidden;page-break-after:always;break-after:page;font-family:"Microsoft YaHei","SimHei",sans-serif}
|
||||
.ct-pallet-label-sheet:last-child{page-break-after:auto;break-after:auto}
|
||||
.ct-pallet-label-code,.ct-pallet-label-destination{width:100%;max-width:100%;margin:0;text-align:center;font-weight:900;line-height:.95;letter-spacing:0;overflow-wrap:anywhere;word-break:break-word}
|
||||
.ct-pallet-label-code{font-size:var(--ct-pallet-code-size)}
|
||||
.ct-pallet-label-destination{font-size:var(--ct-pallet-destination-size)}
|
||||
.ct-pallet-label-empty{width:min(520px,calc(100vw - 48px));padding:28px;text-align:center;color:#c8cdd1;border:1px solid #454b50;border-radius:6px;background:#252a2e;box-sizing:border-box}
|
||||
@media(max-width:900px){
|
||||
.ct-pallet-preview-toolbar{align-items:flex-start;gap:10px;padding:10px 12px;flex-wrap:wrap}
|
||||
.ct-pallet-preview-meta{flex-basis:100%;min-width:0}
|
||||
.ct-pallet-preview-controls{width:100%;justify-content:flex-start}
|
||||
.ct-pallet-preview-scroll{padding:16px 12px}
|
||||
}
|
||||
@media(max-width:760px){
|
||||
.ct-pallet-preview-controls{gap:8px}
|
||||
.ct-pallet-preview-controls .btn,.ct-pallet-preview-controls .booking-print-close,.ct-pallet-preview-controls .ct-pallet-preview-close,.ct-pallet-font-control button{min-height:40px}
|
||||
.ct-pallet-preview-controls .ct-pallet-preview-close{min-width:40px}
|
||||
.ct-pallet-font-stepper{height:40px;grid-template-columns:40px 48px 40px}
|
||||
.ct-pallet-font-stepper button{width:40px;height:40px}
|
||||
}
|
||||
@page ctPalletLabel{size:A4 landscape;margin:0}
|
||||
@media print{
|
||||
html:has(body.ct-pallet-printing),body.ct-pallet-printing{width:297mm!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.ct-pallet-printing>*:not(#modal-ct-pallet-label-preview){display:none!important}
|
||||
body.ct-pallet-printing #modal-booking-print-preview{display:none!important}
|
||||
body.ct-pallet-printing #modal-ct-pallet-label-preview{display:block!important;position:static!important;inset:auto!important;width:297mm!important;height:auto!important;padding:0!important;background:#fff!important}
|
||||
body.ct-pallet-printing #modal-ct-pallet-label-preview,body.ct-pallet-printing #modal-ct-pallet-label-preview *{visibility:visible!important}
|
||||
body.ct-pallet-printing .ct-pallet-preview-panel{display:block!important;position:static!important;width:297mm!important;height:auto!important;max-width:none!important;max-height:none!important;margin:0!important;background:#fff!important;overflow:visible!important}
|
||||
body.ct-pallet-printing .ct-pallet-preview-toolbar{display:none!important}
|
||||
body.ct-pallet-printing .ct-pallet-preview-scroll{display:block!important;position:static!important;width:297mm!important;height:auto!important;padding:0!important;background:#fff!important;overflow:visible!important}
|
||||
body.ct-pallet-printing #ct-pallet-label-list{display:block!important;width:297mm!important;min-width:0!important;margin:0!important}
|
||||
body.ct-pallet-printing .ct-pallet-label-sheet{display:flex!important;width:297mm!important;height:210mm!important;margin:0!important;box-shadow:none!important;page-break-after:always;break-after:page}
|
||||
body.ct-pallet-printing .ct-pallet-label-sheet:last-child{page-break-after:auto;break-after:auto}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1189,8 +1247,6 @@ body.dashboard-fullscreen #page-dashboard table:has(>thead)>tbody>tr>td{backgrou
|
||||
<div class="sub-menu-item" onclick="switchPage('fba-outbound-order',this)">出库单</div>
|
||||
<div class="sub-menu-item" onclick="switchPage('fba-relabel-order',this)">换标单</div>
|
||||
<div class="sub-menu-item" onclick="switchPage('fba-pallet',this)">托盘管理</div>
|
||||
<div class="sub-menu-item" onclick="switchPage('overseas-location',this)">库位管理</div>
|
||||
<div class="sub-menu-item" onclick="switchPage('fba-carton',this)">箱号管理</div>
|
||||
<div class="sub-menu-item" onclick="switchPage('fba-product',this)">产品库</div>
|
||||
<div class="sub-menu-item" onclick="switchPage('fba-stock-management',this)">库存管理</div>
|
||||
</div>
|
||||
|
||||
@@ -8,25 +8,21 @@
|
||||
<div class="outbound-vehicle-form-grid">
|
||||
<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></select></div>
|
||||
<div class="form-group"><label>派送方式 *</label><select id="bol-deliveryMethod" required onchange="handleOutboundVehicleDeliveryMethodChange()"><option value="">请选择</option><option>FBA自提</option><option>客户自提</option><option>卡派打托</option><option>卡派地板</option><option>快递</option><option>外配</option></select></div>
|
||||
<div class="form-group"><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><input id="bol-driverEmail" type="email"/></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><input id="bol-bookAccount"/></div>
|
||||
<div class="form-group"><label>ISA号码</label><input id="bol-isaNo"/></div>
|
||||
<div class="form-group"><label>车辆大小</label><input id="bol-vehicleSize"/></div>
|
||||
<div class="form-group"><label>是否发送派送单</label><select id="bol-sendDispatch"><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>
|
||||
<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" required/></div>
|
||||
<div class="form-group"><label>预估派送单费用(USD) *</label><input id="bol-estimatedDispatchFee" type="number" min="0" step="0.01" required/></div>
|
||||
<div class="form-group"><label>派送费用(USD) *</label><input id="bol-dispatchFee" type="number" min="0" step="0.01" placeholder="请输入派送费用" required/></div>
|
||||
<div class="form-group" id="bol-loading-fee-field" hidden><label>装车费(USD) *</label><input id="bol-loadingFee" type="number" min="0" step="0.01"/></div>
|
||||
<div class="form-group" id="bol-courier-company-field" hidden><label>快递公司 *</label><select id="bol-courierCompanyId"><option value="">请选择快递公司</option></select></div>
|
||||
<div class="form-group" id="bol-courier-fee-field" hidden><label>快递费用(USD) *</label><input id="bol-courierFee" type="number" min="0" step="0.01"/></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>
|
||||
@@ -57,24 +53,22 @@
|
||||
<!-- 第四行 -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr auto;gap:0 16px;margin-bottom:14px;align-items:end">
|
||||
<div><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">预派送日期 <span style="color:#f5222d">*</span></label><input id="bol-deliveryDate" type="date" 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">派送方式 <span style="color:#f5222d">*</span></label><select id="bol-deliveryMethod" 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><option>第三方物流</option></select></div>
|
||||
<div><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">派送方式 <span style="color:#f5222d">*</span></label><select id="bol-deliveryMethod" 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>FBA自提</option><option>客户自提</option><option>卡派打托</option><option>卡派地板</option><option>快递</option><option>外配</option></select></div>
|
||||
<div><button class="btn btn-primary" style="padding:9px 24px;border-radius:6px;font-size:13px" onclick="bolSearch()">🔍 搜索</button></div>
|
||||
</div>
|
||||
<!-- 分隔线 -->
|
||||
<div style="border-top:1px solid #e8e8e8;margin:18px 0"></div>
|
||||
<!-- 第五行 -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:0 16px;margin-bottom:14px">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:0 16px;margin-bottom:14px">
|
||||
<div><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">车队/司机名</label><input id="bol-driverName" placeholder="请输入车队/司机名" style="width:100%;padding:9px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;box-sizing:border-box"/></div>
|
||||
<div><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">车队联系电话</label><input id="bol-driverPhone" placeholder="请输入车队联系电话" style="width:100%;padding:9px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;box-sizing:border-box"/></div>
|
||||
<div><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">车队邮箱</label><input id="bol-driverEmail" 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-vehicleType" 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><option>冷藏车</option><option>集装箱卡车</option></select></div>
|
||||
</div>
|
||||
<!-- 第六行 -->
|
||||
<div style="display:grid;grid-template-columns:1fr 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><input id="bol-bookAccount" 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">ISA号码</label><input id="bol-isaNo" placeholder="请输入ISA号码" style="width:100%;padding:9px 12px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;box-sizing:border-box"/></div>
|
||||
<div><label style="font-size:12px;font-weight:600;color:#333;display:block;margin-bottom:4px">车辆大小</label><input id="bol-vehicleSize" 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-sendDispatch" 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 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>
|
||||
<!-- 订单明细 -->
|
||||
<div style="border-top:1px solid #e8e8e8;margin:18px 0 12px;padding-top:6px">
|
||||
|
||||
@@ -12,19 +12,34 @@ assert.ok(moduleHtml.includes('table style="min-width:2800px"'), '货柜列表
|
||||
assert.ok(moduleHtml.includes('<th style="width:300px;white-space:nowrap;text-align:center">操作</th><th>报空预警</th><th>集装箱号</th>'), '报空预警应紧跟操作列');
|
||||
assert.ok(appJs.includes("['_cb','_act','_emptyWarning','no'"), '报空预警渲染键应紧跟操作列');
|
||||
|
||||
['预计提柜时间', '预计报空时间', '报空时间', '还柜时间', '报空预警'].forEach(label => {
|
||||
['预计提柜时间', '约车时间', '报空时间', '还柜时间', '报空预警'].forEach(label => {
|
||||
assert.ok(moduleHtml.includes(label), `货柜列表缺少表头:${label}`);
|
||||
});
|
||||
['ct-add-pickup-expected', 'ct-add-empty-expected', 'ct-add-emptied-at', 'ct-add-returned-at'].forEach(id => {
|
||||
assert.ok(!moduleHtml.includes('<th>预计报空时间</th>'), '货柜列表不应显示预计报空时间');
|
||||
assert.ok(
|
||||
moduleHtml.includes('<th>预计提柜时间</th><th>约车时间</th><th>报空时间</th>'),
|
||||
'约车时间应位于预计提柜时间和报空时间之间',
|
||||
);
|
||||
['ct-add-pickup-expected', 'ct-add-appointment-at', 'ct-add-emptied-at', 'ct-add-returned-at'].forEach(id => {
|
||||
assert.ok(overlays.includes(`id="${id}"`), `货柜表单缺少字段:${id}`);
|
||||
});
|
||||
['pickupExpectedAt', 'emptyExpectedAt', 'emptiedAt', 'returnedAt'].forEach(key => {
|
||||
assert.ok(!overlays.includes('id="ct-add-empty-expected"'), '货柜表单不应显示预计报空时间字段');
|
||||
assert.ok(
|
||||
/预计提柜时间:[\s\S]{0,220}ct-add-pickup-expected[\s\S]{0,220}约车时间:[\s\S]{0,220}ct-add-appointment-at/.test(overlays),
|
||||
'约车时间应紧跟预计提柜时间',
|
||||
);
|
||||
['pickupExpectedAt', 'appointmentAt', 'emptyExpectedAt', 'emptiedAt', 'returnedAt'].forEach(key => {
|
||||
assert.ok(appJs.includes(key), `货柜数据缺少字段:${key}`);
|
||||
});
|
||||
['ct-add-pickup-expected', 'ct-add-empty-expected', 'ct-add-emptied-at', 'ct-add-returned-at'].forEach(id => {
|
||||
['ct-add-pickup-expected', 'ct-add-appointment-at', 'ct-add-emptied-at', 'ct-add-returned-at'].forEach(id => {
|
||||
const uses = appJs.match(new RegExp(id, 'g')) || [];
|
||||
assert.ok(uses.length >= 3, `货柜表单字段未完整接通重置、编辑和保存:${id}`);
|
||||
});
|
||||
assert.ok(!appJs.includes("document.getElementById('ct-add-empty-expected')"), '预计报空时间不应继续接入货柜表单');
|
||||
assert.ok(
|
||||
appJs.includes("'pickupExpectedAt','appointmentAt','emptiedAt','returnedAt'"),
|
||||
'货柜列表时间列映射应包含约车时间并移除预计报空时间',
|
||||
);
|
||||
|
||||
const start = appJs.indexOf('function getCtEmptyWarningState');
|
||||
const end = appJs.indexOf('\nfunction renderCtEmptyWarning', start);
|
||||
|
||||
@@ -19,22 +19,12 @@ const renderCtListSource = extractBetween(
|
||||
'货柜列表渲染片段',
|
||||
);
|
||||
|
||||
assert.ok(/class="ct-fleet-region-sel"/.test(renderCtListSource), '码头车队区域选择框缺失');
|
||||
assert.ok(/const regionOpts=\['国内','海外'\]/.test(renderCtListSource), '国内和海外区域选项缺失');
|
||||
assert.ok(/regionOpts\.map\(region=>/.test(renderCtListSource), '区域选项渲染逻辑缺失');
|
||||
assert.ok(/d\.fleetRegion===region/.test(renderCtListSource), '区域选中态绑定缺失');
|
||||
assert.ok(
|
||||
/onchange="ctSetFleetRegion\('\$\{d\.no\}',this\.value\)"/.test(renderCtListSource),
|
||||
'区域选择变更事件缺失',
|
||||
);
|
||||
assert.ok(/class="ct-fleet-control"/.test(renderCtListSource), '码头车队控件容器缺失');
|
||||
assert.ok(/class="ct-fleet-sel"/.test(renderCtListSource), '码头车队选择框缺失');
|
||||
assert.ok(!/class="ct-fleet-region-sel"/.test(renderCtListSource), '码头车队不应保留国内/海外选择框');
|
||||
assert.ok(!/const regionOpts=/.test(renderCtListSource), '码头车队不应保留国内/海外选项');
|
||||
assert.ok(!/ctSetFleetRegion/.test(renderCtListSource), '码头车队渲染不应绑定区域变更事件');
|
||||
|
||||
const ctSetFleetRegionSource = extractBetween(
|
||||
'function ctSetFleetRegion(no,val)',
|
||||
'function ctSetFleet(no,val)',
|
||||
'码头车队区域 setter 片段',
|
||||
);
|
||||
assert.ok(!appJs.includes('function ctSetFleetRegion('), '码头车队区域 setter 应移除');
|
||||
|
||||
assert.ok(/d\.fleetRegion=val/.test(ctSetFleetRegionSource), '区域 setter 赋值缺失');
|
||||
|
||||
console.log('货柜列表码头车队区域选择框验证通过');
|
||||
console.log('货柜列表码头车队单选择框验证通过');
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const {spawnSync} = require('node:child_process');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const appJs = fs.readFileSync(path.join(root, 'prototype-src', 'app.js'), 'utf8');
|
||||
const moduleHtml = fs.readFileSync(path.join(root, 'prototype-src', 'modules', '06-containers.html'), 'utf8');
|
||||
const overlaysHtml = fs.readFileSync(path.join(root, 'prototype-src', 'overlays.html'), 'utf8');
|
||||
const shellHtml = fs.readFileSync(path.join(root, 'prototype-src', 'shell.html'), 'utf8');
|
||||
const pureFunctionsStart = '// ===== 货柜托盘标纯函数 =====';
|
||||
const captureMarker = '// ===== 货柜托盘标交互 =====';
|
||||
|
||||
function loadCtPalletLabelApi(source) {
|
||||
if (!source.includes(captureMarker)) return null;
|
||||
|
||||
const sentinel = {};
|
||||
const context = {
|
||||
console,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
__ctPalletLabelSentinel: sentinel,
|
||||
};
|
||||
const capture = [
|
||||
captureMarker,
|
||||
';globalThis.__ctPalletLabelApi = {',
|
||||
" normalizeCtPalletCabinet: typeof normalizeCtPalletCabinet === 'function' ? normalizeCtPalletCabinet : null,",
|
||||
" buildCtPalletLabels: typeof buildCtPalletLabels === 'function' ? buildCtPalletLabels : null,",
|
||||
'};',
|
||||
'throw globalThis.__ctPalletLabelSentinel;',
|
||||
].join('\n');
|
||||
const instrumented = source.split(captureMarker).join(capture);
|
||||
|
||||
vm.createContext(context);
|
||||
try {
|
||||
vm.runInContext(instrumented, context, {timeout: 2000});
|
||||
} catch (error) {
|
||||
if (error === sentinel) return context.__ctPalletLabelApi || null;
|
||||
if (error && error.name === 'ReferenceError') return null;
|
||||
throw error;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const loaderFixture = [
|
||||
pureFunctionsStart,
|
||||
'function normalizeCtPalletCabinet(value) { return value; }',
|
||||
'function buildCtPalletLabels() { return {labels: []}; }',
|
||||
captureMarker,
|
||||
].join('\n');
|
||||
const executableApi = loadCtPalletLabelApi(loaderFixture);
|
||||
assert.equal(typeof executableApi.normalizeCtPalletCabinet, 'function', 'loader 应捕获真实可执行函数');
|
||||
assert.equal(typeof executableApi.buildCtPalletLabels, 'function', 'loader 应捕获真实可执行函数');
|
||||
assert.equal(
|
||||
loadCtPalletLabelApi(['/*', loaderFixture, '*/'].join('\n')),
|
||||
null,
|
||||
'loader 不应捕获块注释中的伪函数',
|
||||
);
|
||||
assert.equal(
|
||||
loadCtPalletLabelApi(['const ignored = `', loaderFixture, '`;'].join('\n')),
|
||||
null,
|
||||
'loader 不应捕获多行模板字符串中的伪函数',
|
||||
);
|
||||
|
||||
const sandbox = loadCtPalletLabelApi(appJs);
|
||||
assert.ok(
|
||||
sandbox && typeof sandbox.normalizeCtPalletCabinet === 'function',
|
||||
'缺少 normalizeCtPalletCabinet 函数',
|
||||
);
|
||||
assert.ok(
|
||||
typeof sandbox.buildCtPalletLabels === 'function',
|
||||
'缺少 buildCtPalletLabels 函数',
|
||||
);
|
||||
|
||||
assert.equal(sandbox.normalizeCtPalletCabinet(' D2253(AJC) '), 'D2253');
|
||||
assert.equal(sandbox.normalizeCtPalletCabinet(' D2254(TC) '), 'D2254');
|
||||
|
||||
const floatingBoundaryResult = sandbox.buildCtPalletLabels(
|
||||
[{no: 'CONT-FLOAT', ctNo: 'D2001(A)', manifest: 'MF-FLOAT'}],
|
||||
[{no: 'MF-FLOAT'}],
|
||||
{'MF-FLOAT': Array.from({length: 20}, () => ({destWh: 'ONT8', businessTypeCode: 'DF', vol: 0.1}))},
|
||||
);
|
||||
assert.equal(floatingBoundaryResult.labels.length, 1, '20 个 0.1 方归一后只应生成 1 张标签');
|
||||
assert.equal(floatingBoundaryResult.labels[0].volume, 2);
|
||||
|
||||
const fractionalRemainderResult = sandbox.buildCtPalletLabels(
|
||||
[{no: 'CONT-FRACTION', ctNo: 'D2002(A)', manifest: 'MF-FRACTION'}],
|
||||
[{no: 'MF-FRACTION'}],
|
||||
{'MF-FRACTION': [{destWh: 'ONT8', businessTypeCode: 'OTHER', vol: 2.0000004}]},
|
||||
);
|
||||
assert.equal(fractionalRemainderResult.labels.length, 2, '超过 2 方的微小余量仍应生成第 2 张标签');
|
||||
assert.equal(fractionalRemainderResult.labels[0].volume, 2.0000004);
|
||||
assert.equal(fractionalRemainderResult.labels[1].volume, 2.0000004);
|
||||
|
||||
const containers = [
|
||||
{no: 'CONT-1', ctNo: 'D2253(AJC)', manifest: 'MF-1'},
|
||||
{no: 'CONT-2', ctNo: '', manifest: 'MF-2'},
|
||||
];
|
||||
const manifests = [
|
||||
{no: 'MF-1'},
|
||||
{no: 'MF-2'},
|
||||
];
|
||||
const detailMap = {
|
||||
'MF-1': [
|
||||
{destWh: 'ONT8', businessTypeCode: 'DF', vol: 2.1},
|
||||
{destWh: 'ONT8', businessTypeCode: 'DF', vol: 2.2},
|
||||
{destWh: 'LAX9', businessTypeCode: 'DF', vol: 2},
|
||||
{destWh: 'LAX9', businessTypeCode: 'TC', vol: 0.1},
|
||||
{destWh: '', businessTypeCode: 'DF', vol: 1},
|
||||
{destWh: 'ONT8', businessTypeCode: '', vol: 1},
|
||||
{destWh: 'ONT8', businessTypeCode: 'DF', vol: 0},
|
||||
],
|
||||
'MF-2': [
|
||||
{destWh: 'ONT8', businessTypeCode: 'DF', vol: 2},
|
||||
],
|
||||
};
|
||||
|
||||
const result = JSON.parse(JSON.stringify(
|
||||
sandbox.buildCtPalletLabels(containers, manifests, detailMap),
|
||||
));
|
||||
|
||||
assert.deepEqual(result.labels, [
|
||||
{code: 'D2253-DF-1', cabinetNo: 'D2253', destWh: 'LAX9', businessTypeCode: 'DF', volume: 2},
|
||||
{code: 'D2253-DF-2', cabinetNo: 'D2253', destWh: 'ONT8', businessTypeCode: 'DF', volume: 4.3},
|
||||
{code: 'D2253-DF-3', cabinetNo: 'D2253', destWh: 'ONT8', businessTypeCode: 'DF', volume: 4.3},
|
||||
{code: 'D2253-DF-4', cabinetNo: 'D2253', destWh: 'ONT8', businessTypeCode: 'DF', volume: 4.3},
|
||||
{code: 'D2253-TC-1', cabinetNo: 'D2253', destWh: 'LAX9', businessTypeCode: 'TC', volume: 0.1},
|
||||
]);
|
||||
assert.equal(result.skippedContainers, 1);
|
||||
assert.equal(result.skippedDetails, 3);
|
||||
|
||||
const perContainerResult = JSON.parse(JSON.stringify(sandbox.buildCtPalletLabels(
|
||||
[
|
||||
{no: 'CONT-A', ctNo: 'D3001(AJC)', manifest: 'MF-A'},
|
||||
{no: 'CONT-B', ctNo: 'D3002(TC)', manifest: 'MF-B'},
|
||||
],
|
||||
[{no: 'MF-A'}, {no: 'MF-B'}],
|
||||
{
|
||||
'MF-A': [{destWh: 'ONT8', businessTypeCode: 'DF', vol: 2.1}],
|
||||
'MF-B': [{destWh: 'ONT8', businessTypeCode: 'DF', vol: 2.1}],
|
||||
},
|
||||
)));
|
||||
assert.deepEqual(perContainerResult.labels, [
|
||||
{code: 'D3001-DF-1', cabinetNo: 'D3001', destWh: 'ONT8', businessTypeCode: 'DF', volume: 2.1},
|
||||
{code: 'D3001-DF-2', cabinetNo: 'D3001', destWh: 'ONT8', businessTypeCode: 'DF', volume: 2.1},
|
||||
{code: 'D3002-DF-1', cabinetNo: 'D3002', destWh: 'ONT8', businessTypeCode: 'DF', volume: 2.1},
|
||||
{code: 'D3002-DF-2', cabinetNo: 'D3002', destWh: 'ONT8', businessTypeCode: 'DF', volume: 2.1},
|
||||
]);
|
||||
|
||||
const duplicateCabinetResult = sandbox.buildCtPalletLabels(
|
||||
[
|
||||
{no: 'CONT-DUP-A', ctNo: 'D1(A)', manifest: 'MF-DUP-A'},
|
||||
{no: 'CONT-DUP-B', ctNo: 'D1(B)', manifest: 'MF-DUP-B'},
|
||||
],
|
||||
[{no: 'MF-DUP-A'}, {no: 'MF-DUP-B'}],
|
||||
{
|
||||
'MF-DUP-A': [{destWh: 'LAX9', businessTypeCode: 'DF', vol: 1}],
|
||||
'MF-DUP-B': [{destWh: 'ONT8', businessTypeCode: 'DF', vol: 1}],
|
||||
},
|
||||
);
|
||||
assert.deepEqual(
|
||||
Array.from(duplicateCabinetResult.labels, item => item.code),
|
||||
['D1-DF-1', 'D1-DF-2'],
|
||||
);
|
||||
|
||||
const stableDetails = [
|
||||
{destWh: 'ONT8', businessTypeCode: 'TC', vol: 1},
|
||||
{destWh: 'SBD1', businessTypeCode: 'DF', vol: 1},
|
||||
{destWh: 'LAX9', businessTypeCode: 'DF', vol: 1},
|
||||
];
|
||||
function buildStableResult(details) {
|
||||
return JSON.parse(JSON.stringify(sandbox.buildCtPalletLabels(
|
||||
[{no: 'CONT-STABLE', ctNo: 'D6001(A)', manifest: 'MF-STABLE'}],
|
||||
[{no: 'MF-STABLE'}],
|
||||
{'MF-STABLE': details},
|
||||
)));
|
||||
}
|
||||
assert.deepEqual(
|
||||
buildStableResult(stableDetails),
|
||||
buildStableResult(stableDetails.slice().reverse()),
|
||||
'分组输出不应受明细输入顺序影响',
|
||||
);
|
||||
|
||||
const validDetail = [{destWh: 'ONT8', businessTypeCode: 'DF', vol: 2}];
|
||||
const missingManifestResult = sandbox.buildCtPalletLabels(
|
||||
[{no: 'CONT-MISSING-MF', ctNo: 'D4001', manifest: 'MF-MISSING'}],
|
||||
[],
|
||||
{},
|
||||
);
|
||||
assert.equal(missingManifestResult.skippedContainers, 1);
|
||||
assert.equal(missingManifestResult.labels.length, 0);
|
||||
|
||||
const missingDetailsResult = sandbox.buildCtPalletLabels(
|
||||
[{no: 'CONT-MISSING-DETAILS', ctNo: 'D4002', manifest: 'MF-NO-DETAILS'}],
|
||||
[{no: 'MF-NO-DETAILS'}],
|
||||
{},
|
||||
);
|
||||
assert.equal(missingDetailsResult.skippedContainers, 1);
|
||||
assert.equal(missingDetailsResult.labels.length, 0);
|
||||
|
||||
const emptyCabinetResult = sandbox.buildCtPalletLabels(
|
||||
[{no: 'CONT-EMPTY-CABINET', ctNo: ' ', manifest: 'MF-VALID'}],
|
||||
[{no: 'MF-VALID'}],
|
||||
{'MF-VALID': validDetail},
|
||||
);
|
||||
assert.equal(emptyCabinetResult.skippedContainers, 1);
|
||||
assert.equal(emptyCabinetResult.labels.length, 0);
|
||||
|
||||
const invalidVolumeResult = sandbox.buildCtPalletLabels(
|
||||
[{no: 'CONT-INVALID-VOL', ctNo: 'D5001(AJC)', manifest: 'MF-INVALID-VOL'}],
|
||||
[{no: 'MF-INVALID-VOL'}],
|
||||
{
|
||||
'MF-INVALID-VOL': [
|
||||
{destWh: 'ONT8', businessTypeCode: 'DF', vol: -1},
|
||||
{destWh: 'ONT8', businessTypeCode: 'DF', vol: 'not-a-number'},
|
||||
],
|
||||
},
|
||||
);
|
||||
assert.equal(invalidVolumeResult.skippedContainers, 0);
|
||||
assert.equal(invalidVolumeResult.skippedDetails, 2);
|
||||
assert.equal(invalidVolumeResult.labels.length, 0);
|
||||
|
||||
function buildOverflowResult(details) {
|
||||
const script = [
|
||||
'const MAX_CT_PALLET_LABELS_PER_GROUP=500;',
|
||||
'const CT_PALLET_VOLUME_SCALE=1000000000;',
|
||||
sandbox.normalizeCtPalletCabinet.toString(),
|
||||
sandbox.buildCtPalletLabels.toString(),
|
||||
`process.stdout.write(JSON.stringify(buildCtPalletLabels(
|
||||
[{no:'CONT-OVERFLOW',ctNo:'D7001(A)',manifest:'MF-OVERFLOW'}],
|
||||
[{no:'MF-OVERFLOW'}],
|
||||
{'MF-OVERFLOW':${JSON.stringify(details)}}
|
||||
)));`,
|
||||
].join('\n');
|
||||
const child = spawnSync(process.execPath, ['-e', script], {encoding: 'utf8', timeout: 1000});
|
||||
assert.equal(child.error, undefined, '溢出分组必须在 1 秒内返回');
|
||||
assert.equal(child.status, 0, child.stderr || '溢出分组子进程异常退出');
|
||||
return JSON.parse(child.stdout);
|
||||
}
|
||||
|
||||
const maxValueResult = buildOverflowResult([
|
||||
{destWh: 'ONT8', businessTypeCode: 'DF', vol: Number.MAX_VALUE},
|
||||
]);
|
||||
assert.equal(maxValueResult.labels.length, 0);
|
||||
assert.equal(maxValueResult.skippedDetails, 1);
|
||||
|
||||
const summedInfinityResult = buildOverflowResult([
|
||||
{destWh: 'ONT8', businessTypeCode: 'DF', vol: Number.MAX_VALUE},
|
||||
{destWh: 'ONT8', businessTypeCode: 'DF', vol: Number.MAX_VALUE},
|
||||
]);
|
||||
assert.equal(summedInfinityResult.labels.length, 0);
|
||||
assert.equal(summedInfinityResult.skippedDetails, 2);
|
||||
|
||||
assert.match(moduleHtml, /生成托盘标/, '货柜列表缺少生成托盘标按钮');
|
||||
assert.match(moduleHtml, /openCtPalletLabelPreview\(\)/, '货柜列表按钮未绑定托盘标预览');
|
||||
|
||||
[
|
||||
'modal-ct-pallet-label-preview',
|
||||
'ct-pallet-label-list',
|
||||
'ct-pallet-preview-count',
|
||||
'托盘标签字号',
|
||||
'目的仓字号',
|
||||
'printCtPalletLabels()',
|
||||
].forEach(token => assert.ok(overlaysHtml.includes(token), `托盘标预览缺少 ${token}`));
|
||||
assert.match(overlaysHtml, /id="ct-pallet-preview-close"/, '托盘标预览关闭按钮缺少稳定 ID');
|
||||
assert.match(
|
||||
overlaysHtml,
|
||||
/class="[^"]*\bct-pallet-print-action\b[^"]*"[^>]*onclick="printCtPalletLabels\(\)"/,
|
||||
'托盘标打印按钮缺少专用主操作 class',
|
||||
);
|
||||
|
||||
assert.match(shellHtml, /\.ct-pallet-label-sheet\b/, '缺少托盘标纸张样式');
|
||||
assert.match(shellHtml, /page-break-after\s*:\s*always/, '托盘标未配置逐页打印');
|
||||
assert.match(shellHtml, /body\.ct-pallet-printing/, '托盘标打印样式缺少 body 作用域');
|
||||
assert.match(
|
||||
shellHtml,
|
||||
/\.ct-pallet-preview-modal\{[^}]*--ct-pallet-code-size\s*:\s*120px[^}]*--ct-pallet-destination-size\s*:\s*180px/,
|
||||
'字号变量默认值必须与脚本更新保持在预览 modal 同一作用域',
|
||||
);
|
||||
assert.match(
|
||||
shellHtml,
|
||||
/\.ct-pallet-print-action\{[^}]*background\s*:\s*#0958d9\s*!important[^}]*border\s*:\s*1px solid #0958d9\s*!important/,
|
||||
'托盘标打印按钮缺少专用高对比主操作样式',
|
||||
);
|
||||
assert.match(
|
||||
shellHtml,
|
||||
/@media\(max-width:760px\)\{[\s\S]*?\.ct-pallet-preview-controls \.btn,[^}]*?\.ct-pallet-preview-controls \.booking-print-close,[^}]*?\.ct-pallet-font-control button\{[^}]*min-height\s*:\s*40px/,
|
||||
'窄屏操作控件高度未达到 40px',
|
||||
);
|
||||
assert.match(
|
||||
shellHtml,
|
||||
/@media\(max-width:760px\)\{[\s\S]*?\.ct-pallet-font-stepper button\{[^}]*width\s*:\s*40px[^}]*height\s*:\s*40px/,
|
||||
'窄屏字号步进器未达到 40px 触控尺寸',
|
||||
);
|
||||
|
||||
[
|
||||
'openCtPalletLabelPreview',
|
||||
'renderCtPalletLabelPreview',
|
||||
'adjustCtPalletLabelFont',
|
||||
'closeCtPalletLabelPreview',
|
||||
'printCtPalletLabels',
|
||||
].forEach(name => assert.match(appJs, new RegExp(`function\\s+${name}\\s*\\(`), `缺少 ${name} 函数`));
|
||||
|
||||
assert.match(appJs, /var\s+ctPalletLabelReturnFocus\s*=\s*null/, '缺少托盘标预览返回焦点状态');
|
||||
assert.match(appJs, /ctPalletLabelReturnFocus\s*=\s*document\.activeElement/, '打开预览前未保存当前焦点');
|
||||
assert.match(appJs, /requestAnimationFrame\s*\([\s\S]*?ct-pallet-preview-close[\s\S]*?\.focus\s*\(/, '打开预览后未聚焦关闭按钮');
|
||||
assert.match(appJs, /ctPalletLabelReturnFocus\.isConnected[\s\S]*?ctPalletLabelReturnFocus\.focus\s*\(/, '关闭预览后未恢复原焦点');
|
||||
assert.match(appJs, /event\.key\s*(?:===|!==)\s*'Tab'/, '托盘标预览未处理 Tab 键');
|
||||
assert.match(appJs, /button:not\(\[disabled\]\)[\s\S]*?\[tabindex\]:not\(\[tabindex="-1"\]\)/, '托盘标预览缺少可聚焦控件查询');
|
||||
assert.match(appJs, /event\.shiftKey[\s\S]*?event\.preventDefault\s*\(/, '托盘标预览缺少反向 Tab 循环');
|
||||
|
||||
console.log('CONTAINER_PALLET_LABEL_CHECK_PASS');
|
||||
@@ -0,0 +1,58 @@
|
||||
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/08-finance.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');
|
||||
|
||||
const pageStart = moduleHtml.indexOf('id="page-customer-arap"');
|
||||
const pageEnd = moduleHtml.indexOf('id="page-finance-ar-detail-report"', pageStart);
|
||||
const pageHtml = moduleHtml.slice(pageStart, pageEnd);
|
||||
const feeHeaderIndex = pageHtml.indexOf('>费用类型</th>');
|
||||
const amountHeaderIndex = pageHtml.indexOf('>金额</th>');
|
||||
assert.ok(feeHeaderIndex >= 0, '客户应收应付列表缺少费用类型表头');
|
||||
assert.ok(amountHeaderIndex > feeHeaderIndex, '客户应收应付列表应在费用类型后显示金额');
|
||||
|
||||
assert.ok(overlays.includes('id="arap-form-amount"'), '新增/编辑弹框缺少金额字段');
|
||||
assert.ok(overlays.includes('id="arap-audit-amount"'), '审核摘要缺少金额字段');
|
||||
const formStart = overlays.indexOf('id="modal-arap-form"');
|
||||
const formEnd = overlays.indexOf('id="modal-arap-audit"', formStart);
|
||||
const formHtml = overlays.slice(formStart, formEnd);
|
||||
assert.ok(formHtml.indexOf('>金额 *</label>') > formHtml.indexOf('>费用类型 *</label>'), '新增/编辑弹框金额应位于费用类型之后');
|
||||
const auditStart = overlays.indexOf('id="modal-arap-audit"');
|
||||
const auditEnd = overlays.indexOf('id="modal-settlement-ap-voucher"', auditStart);
|
||||
const auditHtml = overlays.slice(auditStart, auditEnd);
|
||||
assert.ok(auditHtml.indexOf('>金额</span>') > auditHtml.indexOf('>费用类型</span>'), '审核摘要金额应位于费用类型之后');
|
||||
|
||||
function loadFunction(name, nextName) {
|
||||
const start = appJs.indexOf(`function ${name}`);
|
||||
const end = appJs.indexOf(`\nfunction ${nextName}`, start);
|
||||
assert.ok(start >= 0 && end > start, `缺少可测试函数 ${name}`);
|
||||
const sandbox = {};
|
||||
vm.runInNewContext(`${appJs.slice(start, end)}\nresult=${name};`, sandbox);
|
||||
return sandbox.result;
|
||||
}
|
||||
|
||||
const normalizeAmount = loadFunction('normalizeArapAmount', 'appendArapLog');
|
||||
assert.equal(normalizeAmount('1200'), '1200.00');
|
||||
assert.equal(normalizeAmount('1200.5'), '1200.50');
|
||||
assert.equal(normalizeAmount('0.01'), '0.01');
|
||||
['', '-1', 'abc', '1.234'].forEach(value => assert.equal(normalizeAmount(value), ''));
|
||||
|
||||
assert.ok(appJs.includes("['arap-form-amount',record.amount]"), '编辑弹框未回填金额');
|
||||
assert.ok(appJs.includes("getArapFormValue('arap-form-amount')"), '保存函数未读取金额');
|
||||
assert.ok(appJs.includes("document.getElementById('arap-audit-amount').textContent=record.amount||'—'"), '审核摘要未将金额写入可见文本');
|
||||
assert.ok(appJs.includes("['费用类型',d.feeType],['金额',d.amount]"), '详情页金额未紧跟费用类型');
|
||||
assert.ok(appJs.includes("existing.auditStatus=getArapStatusAfterSave('edit')"), '编辑保存后未重新进入审核');
|
||||
|
||||
const renderStart = appJs.indexOf('function renderArap()');
|
||||
const renderEnd = appJs.indexOf('// ============ 应收款统计表', renderStart);
|
||||
const renderSource = appJs.slice(renderStart, renderEnd);
|
||||
assert.ok(renderSource.includes("+d.feeType+'</td>'\n +'<td"), '列表金额未紧跟费用类型渲染');
|
||||
assert.ok(!renderSource.includes('>删除</a>'), '客户应收应付列表仍包含删除操作');
|
||||
assert.ok(renderSource.includes('colspan="14"'), '列表空态列数未更新为 14');
|
||||
|
||||
console.log('客户应收应付金额与审核回归校验通过');
|
||||
@@ -0,0 +1,83 @@
|
||||
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/08-finance.html'), 'utf8');
|
||||
const overlaysSource = fs.readFileSync(path.join(root, 'prototype-src/overlays.html'), 'utf8');
|
||||
const appSource = fs.readFileSync(path.join(root, 'prototype-src/app.js'), 'utf8');
|
||||
|
||||
assert.match(moduleSource, /充值申请记录/);
|
||||
assert.match(moduleSource, /showCustomerRechargeApplication\(/);
|
||||
assert.match(overlaysSource, /id="modal-customer-recharge-application"/);
|
||||
assert.match(overlaysSource, /id="modal-customer-recharge-review"/);
|
||||
assert.match(overlaysSource, /customer-recharge-qr/);
|
||||
assert.match(overlaysSource, /id="customer-recharge-vouchers"[^>]*multiple[^>]*accept="\.pdf,\.jpg,\.jpeg,\.png"/);
|
||||
assert.match(appSource, /customerRechargeApplications/);
|
||||
assert.match(appSource, /function submitCustomerRechargeApplication\(/);
|
||||
assert.match(appSource, /function reviewCustomerRechargeApplication\(/);
|
||||
assert.match(appSource, /function approveCustomerRechargeApplication\(/);
|
||||
assert.doesNotMatch(moduleSource, /id="modal-customer-recharge"/);
|
||||
assert.doesNotMatch(appSource, /function showCustomerRecharge\(/);
|
||||
assert.doesNotMatch(appSource, /function saveCustomerRecharge\(/);
|
||||
|
||||
const rechargeDataStart = appSource.indexOf('const customerRechargeApplications=');
|
||||
const rechargeDataEnd = appSource.indexOf('const integrationData', rechargeDataStart);
|
||||
const rechargeSource = appSource.slice(rechargeDataStart, rechargeDataEnd);
|
||||
assert.match(rechargeSource, /referenceNo:/);
|
||||
assert.match(rechargeSource, /app\.referenceNo/);
|
||||
assert.doesNotMatch(rechargeSource, /\breference:/);
|
||||
assert.doesNotMatch(rechargeSource, /app\.reference\b/);
|
||||
|
||||
const closeSource = extract('closeCustomerRechargeApplication');
|
||||
for (const id of ['customer-recharge-application-id', 'customer-recharge-customer', 'customer-recharge-current', 'customer-recharge-amount', 'customer-recharge-date', 'customer-recharge-ref', 'customer-recharge-remark', 'customer-recharge-vouchers']) {
|
||||
assert.match(closeSource, new RegExp(id), `close must clear ${id}`);
|
||||
}
|
||||
|
||||
const reviewSource = extract('reviewCustomerRechargeApplication');
|
||||
assert.match(reviewSource, /未找到充值申请/);
|
||||
const handleSource = extract('handleCustomerRechargeReview');
|
||||
assert.match(handleSource, /未找到充值申请/);
|
||||
assert.match(handleSource, /未找到客户账户/);
|
||||
assert.ok(handleSource.indexOf("customerAccountData.find") < handleSource.indexOf("customerAccountTransactions[app.accountId]"), 'account must be validated before transaction storage access');
|
||||
|
||||
function extract(name) {
|
||||
const match = appSource.match(new RegExp(`function ${name}\\([^]*?\\n\\}`));
|
||||
assert.ok(match, `missing ${name}`);
|
||||
return match[0];
|
||||
}
|
||||
|
||||
const normalizeSource = extract('normalizeCustomerRechargeAmount');
|
||||
const normalize = new Function(`${normalizeSource}; return normalizeCustomerRechargeAmount;`)();
|
||||
assert.equal(normalize('12'), 12);
|
||||
assert.equal(normalize('12.3'), 12.3);
|
||||
assert.equal(normalize('12.345'), null);
|
||||
assert.equal(normalize('-1'), null);
|
||||
assert.equal(normalize(''), null);
|
||||
|
||||
const approveSource = extract('approveCustomerRechargeApplication');
|
||||
const approve = new Function(`${approveSource}; return approveCustomerRechargeApplication;`)();
|
||||
const account = { id: 'CA-X', balance: 100, frozen: 0, status: '正常', lastChange: '' };
|
||||
const application = { id: 'a1', accountId: 'CA-X', amount: 25.5, arrivalDate: '2026-08-20', status: '待审核', vouchers: [] };
|
||||
const transactions = [];
|
||||
const first = approve(application, account, transactions, { decision: '通过', now: '2026/8/20 10:00', operator: '财务' });
|
||||
assert.equal(first.credited, true);
|
||||
assert.equal(first.processed, true);
|
||||
assert.match(first.transaction.no, /^AC20260820\d{8}$/);
|
||||
assert.equal(account.balance, 125.5);
|
||||
assert.equal(transactions.length, 1);
|
||||
const second = approve(application, account, transactions, { decision: '通过', now: '2026-08-20 10:01', operator: '财务' });
|
||||
assert.equal(second.credited, false);
|
||||
assert.equal(second.processed, false);
|
||||
assert.equal(account.balance, 125.5);
|
||||
assert.equal(transactions.length, 1);
|
||||
|
||||
const rejected = { id: 'a2', accountId: 'CA-X', amount: 10, status: '待审核', vouchers: [] };
|
||||
const firstReject = approve(rejected, account, transactions, { decision: '驳回', remark: '金额不符', now: '2026-08-20 10:02', operator: '财务' });
|
||||
assert.equal(firstReject.processed, true);
|
||||
assert.equal(firstReject.credited, false);
|
||||
const repeatedReject = approve(rejected, account, transactions, { decision: '驳回', remark: '再次驳回', now: '2026-08-20 10:03', operator: '财务' });
|
||||
assert.equal(repeatedReject.processed, false);
|
||||
assert.equal(transactions.length, 1);
|
||||
|
||||
console.log('客户充值申请与审核回归校验通过');
|
||||
@@ -22,4 +22,16 @@ assert.match(appJs, /d\.warehouse/, '托盘列表应渲染仓库字段');
|
||||
assert.match(appJs, /fbp-form-overseas-warehouse/, '托盘新增编辑应读写仓库字段');
|
||||
assert.match(appJs, /id === 'fbp-wh'|fbp-wh/, '托盘列表应支持按仓库筛选');
|
||||
|
||||
assert.match(moduleHtml, /<th[^>]*>容积\(m³\)<\/th>\s*<th[^>]*>托盘重量\(kg\)<\/th>/, '托盘重量应在容积后展示');
|
||||
assert.match(moduleHtml, /<label>托盘重量\(kg\) \*<\/label><input id="fbp-form-weight" type="number" min="0\.01" step="0\.01" required aria-required="true"/, '新增编辑表单应提供必填托盘重量');
|
||||
const palletDataBlock = appJs.match(/const fbaPalletData = \[([\s\S]*?)\];/);
|
||||
assert.ok(palletDataBlock, '应定义托盘示例数据');
|
||||
assert.equal((palletDataBlock[1].match(/weight:\s*\d+(?:\.\d+)?/g) || []).length, 5, '每条托盘示例数据都应包含重量');
|
||||
assert.match(appJs, /\$\{d\.weight\}/, '托盘列表应渲染重量');
|
||||
assert.match(appJs, /set\('fbp-form-weight',d\?d\.weight:''\)/, '编辑托盘时应回填重量');
|
||||
assert.match(appJs, /const weightInput=get\('fbp-form-weight'\)/, '保存时应读取重量');
|
||||
assert.match(appJs, /!weightInput\|\|!Number\.isFinite\(weight\)\|\|weight<=0/, '重量为空或不大于0时应阻止保存');
|
||||
assert.match(appJs, /请输入大于0的托盘重量/, '重量校验应提供明确提示');
|
||||
assert.match(appJs, /const next=\{code,type,warehouse,vol:volume,weight,purchasePrice,fbaWh,status,remark\}/, '新增编辑保存结果应包含重量');
|
||||
|
||||
console.log('FBA pallet create/edit checks passed');
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const file = path.join(root, '流程图', '费用流转流程图.html');
|
||||
assert.ok(fs.existsSync(file), '缺少费用流转流程图 HTML');
|
||||
const html = fs.readFileSync(file, 'utf8');
|
||||
|
||||
assert.match(html, /data-flow-direction="vertical"/, '流程方向必须为由上向下');
|
||||
assert.match(html, /data-department-layout="columns"/, '部门必须横向排列为四列');
|
||||
const departmentColumns = [...html.matchAll(/<div class="lane" data-department="([^"]+)">/g)].map(match => match[1]);
|
||||
assert.deepEqual(departmentColumns, ['业务订单', '系统', '财务', '客户与供应商'], '部门列顺序不正确');
|
||||
const canvasSize = html.match(/data-canvas-width="(\d+)" data-canvas-height="(\d+)"/);
|
||||
assert.ok(canvasSize, '缺少竖向画布尺寸标记');
|
||||
assert.ok(Number(canvasSize[2]) > Number(canvasSize[1]), '竖向画布高度必须大于宽度');
|
||||
|
||||
for (const label of ['业务订单', '系统', '财务', '客户与供应商']) {
|
||||
assert.match(html, new RegExp(label), `缺少泳道:${label}`);
|
||||
}
|
||||
for (const label of ['应收', '应付', '待审核', '审核通过', '审核驳回', '待确认', '已确认', '待审批', '生效', '部分收款', '部分付款', '已结清', '禁止结算', '原型未定义']) {
|
||||
assert.match(html, new RegExp(label), `缺少流程节点或图例:${label}`);
|
||||
}
|
||||
for (const marker of ['data-node-id=', 'contenteditable', 'localStorage', 'download', 'Blob', '打印', '恢复默认']) {
|
||||
assert.match(html, new RegExp(marker), `缺少可编辑能力:${marker}`);
|
||||
}
|
||||
assert.doesNotMatch(html, /<script[^>]+src=|<link[^>]+href=/i, '流程图不应依赖外部脚本或样式');
|
||||
|
||||
const expectedNodeIds = [
|
||||
'order-fee-source', 'fee-generated', 'fee-pending-audit', 'fee-audit-decision',
|
||||
'fee-rejected', 'fee-approved', 'join-settlement-decision', 'fee-archived',
|
||||
'ar-bill', 'ar-bill-audit', 'ar-push', 'customer-confirm', 'ar-confirm-wait',
|
||||
'ar-settlement', 'ar-approve', 'ar-payment', 'ar-paid', 'ar-undefined',
|
||||
'ap-bill', 'ap-settlement', 'ap-approval', 'ap-rejected', 'ap-effective',
|
||||
'ap-voucher', 'ap-payment', 'ap-paid', 'supplier-undefined'
|
||||
];
|
||||
const nodeIds = [...html.matchAll(/data-node-id="([^"]+)"/g)].map(match => match[1]);
|
||||
assert.equal(nodeIds.length, expectedNodeIds.length, '费用节点数量不正确');
|
||||
assert.equal(new Set(nodeIds).size, nodeIds.length, '费用节点 ID 必须唯一');
|
||||
assert.deepEqual(new Set(nodeIds), new Set(expectedNodeIds), '费用节点 ID 清单不完整');
|
||||
assert.match(html, /function readNodeText\(textEl\)/, '缺少保留换行的纯文本读取函数');
|
||||
assert.match(html, /const safeStorage\s*=\s*\{/, '缺少 localStorage 安全访问封装');
|
||||
assert.match(html, /addEventListener\('paste'/, '缺少纯文本粘贴处理');
|
||||
assert.match(html, /height:74px[^}]*overflow:hidden/, '节点必须保持固定高度,避免覆盖相邻节点和连线');
|
||||
assert.match(html, /textEl\.textContent\s*=\s*exportedTexts\[/, '下载文件必须使用清洗后的纯文本节点内容');
|
||||
console.log('验证通过:费用流转流程图 HTML 结构完整且为离线自包含文件。');
|
||||
@@ -74,12 +74,14 @@ assert.match(
|
||||
);
|
||||
assert.match(tail, /id="bol-courier-company-field"[^>]*hidden/, '缺少默认隐藏的快递公司字段');
|
||||
assert.match(tail, /id="bol-courierCompanyId"/, '缺少快递公司选择框');
|
||||
assert.match(tail, /id="bol-courier-fee-field"[^>]*hidden/, '缺少默认隐藏的快递费用字段');
|
||||
assert.match(
|
||||
tail,
|
||||
/id="bol-courierFee"[^>]*type="number"[^>]*min="0"[^>]*step="0\.01"/,
|
||||
'快递费用应支持非负 USD 小数',
|
||||
/<label>派送费用(USD) \*<\/label><input id="bol-dispatchFee"[^>]*type="number"[^>]*min="0"[^>]*step="0\.01"[^>]*required/,
|
||||
'转派送弹框应只保留必填的派送费用',
|
||||
);
|
||||
assert.doesNotMatch(tail, /id="bol-estimatedDispatchFee"/, '不应保留预估派送费用输入');
|
||||
assert.doesNotMatch(tail, /id="bol-courierFee"/, '不应保留独立快递费用输入');
|
||||
assert.doesNotMatch(tail, /id="bol-courier-fee-field"/, '不应保留独立快递费用字段容器');
|
||||
assert.doesNotMatch(
|
||||
tail,
|
||||
/<\/template>\s*<\/div>\s*<\/div>\s*<div class="modal-footer"/,
|
||||
@@ -99,32 +101,31 @@ const base = {
|
||||
pickupMethod: '一提一卸',
|
||||
loadType: '托盘',
|
||||
dispatchFee: 200,
|
||||
estimatedDispatchFee: 180,
|
||||
loadingFee: 0,
|
||||
};
|
||||
assert.equal(validate({...base, deliveryMethod: '客户自提'}, companies), '');
|
||||
assert.match(
|
||||
validate({...base, deliveryMethod: '快递', courierCompanyId: '', courierFee: '10'}, companies),
|
||||
validate({...base, deliveryMethod: '客户自提', dispatchFee: ''}, companies),
|
||||
/派送费用/,
|
||||
);
|
||||
assert.match(
|
||||
validate({...base, deliveryMethod: '客户自提', dispatchFee: '-1'}, companies),
|
||||
/派送费用/,
|
||||
);
|
||||
assert.match(
|
||||
validate({...base, deliveryMethod: '客户自提', dispatchFee: 'not-a-number'}, companies),
|
||||
/派送费用/,
|
||||
);
|
||||
assert.match(
|
||||
validate({...base, deliveryMethod: '快递', courierCompanyId: ''}, companies),
|
||||
/快递公司/,
|
||||
);
|
||||
assert.match(
|
||||
validate({...base, deliveryMethod: '快递', courierCompanyId: 'E002', courierFee: '10'}, companies),
|
||||
validate({...base, deliveryMethod: '快递', courierCompanyId: 'E002'}, companies),
|
||||
/快递公司/,
|
||||
);
|
||||
assert.match(
|
||||
validate({...base, deliveryMethod: '快递', courierCompanyId: 'E001', courierFee: ''}, companies),
|
||||
/快递费用/,
|
||||
);
|
||||
assert.match(
|
||||
validate({...base, deliveryMethod: '快递', courierCompanyId: 'E001', courierFee: '-1'}, companies),
|
||||
/快递费用/,
|
||||
);
|
||||
assert.match(
|
||||
validate({...base, deliveryMethod: '快递', courierCompanyId: 'E001', courierFee: 'not-a-number'}, companies),
|
||||
/快递费用/,
|
||||
);
|
||||
assert.equal(
|
||||
validate({...base, deliveryMethod: '快递', courierCompanyId: 'E001', courierFee: '0'}, companies),
|
||||
validate({...base, deliveryMethod: '快递', courierCompanyId: 'E001'}, companies),
|
||||
'',
|
||||
);
|
||||
|
||||
@@ -136,24 +137,26 @@ assert.deepEqual(
|
||||
source: 'outboundVehicle',
|
||||
deliveryMethod: '快递',
|
||||
courierCompanyName: 'DHL',
|
||||
courierFee: 12.5,
|
||||
dispatchFee: 12.5,
|
||||
appointmentDate: '2026-08-14',
|
||||
},
|
||||
{dispatchNo: 'PSD002', source: 'outboundVehicle', deliveryMethod: '客户自提', courierFee: 99},
|
||||
{dispatchNo: 'PSD003', source: 'outboundVehicle', deliveryMethod: '快递', courierFee: -1},
|
||||
{dispatchNo: 'PSD002', source: 'outboundVehicle', deliveryMethod: '客户自提', dispatchFee: 99},
|
||||
{dispatchNo: 'PSD003', source: 'outboundVehicle', deliveryMethod: '快递', dispatchFee: -1},
|
||||
]),
|
||||
[{dispatchNo: 'PSD001', company: 'DHL', businessDate: '2026-08-14', currency: 'USD', amount: 12.5}],
|
||||
);
|
||||
|
||||
const deliveryMethodChange = extractFunction('handleOutboundVehicleDeliveryMethodChange', app);
|
||||
assert.match(deliveryMethodChange, /status\s*===\s*'合作中'/, '快递公司下拉应只显示合作中公司');
|
||||
assert.match(deliveryMethodChange, /feeInput\.value\s*=\s*'0'/, '切换非快递方式时应重置快递费用');
|
||||
assert.doesNotMatch(deliveryMethodChange, /bol-courierFee|bol-courier-fee-field/, '派送方式联动不应再控制独立快递费用');
|
||||
|
||||
const saveBol = extractFunction('saveBol', app);
|
||||
assert.match(saveBol, /validateOutboundVehicleDeliveryDraft\(draft\s*,\s*genExpressData\)/, '保存时应使用快递公司主数据校验');
|
||||
assert.match(saveBol, /courierCompanyId/, '派送单应保存快递公司 ID 快照');
|
||||
assert.match(saveBol, /courierCompanyName/, '派送单应保存快递公司名称快照');
|
||||
assert.match(saveBol, /courierFee/, '派送单和车辆派送快照应保存快递费用');
|
||||
assert.doesNotMatch(saveBol, /value\('bol-estimatedDispatchFee'\)/, '保存时不应读取预估派送费用输入');
|
||||
assert.doesNotMatch(saveBol, /value\('bol-courierFee'\)/, '保存时不应读取独立快递费用输入');
|
||||
assert.match(saveBol, /estimatedDispatchFee\s*:\s*deliveryFee/, '现有预估费用展示应复用唯一派送费用');
|
||||
|
||||
const renderFeeRows = extractFunction('renderOutboundVehicleCourierFeeRows', app);
|
||||
assert.match(renderFeeRows, /data-generated-courier-fee/, '动态费用行应支持幂等清理');
|
||||
|
||||
@@ -245,12 +245,42 @@ expect(Boolean(bolModal), '缺少独立转派送弹框');
|
||||
expectExactOptionSet(
|
||||
bolModal,
|
||||
'bol-deliveryMethod',
|
||||
['FBA自提', '客户自提', '卡派打托', '卡派地板', '快递'],
|
||||
['FBA自提', '客户自提', '卡派打托', '卡派地板', '快递', '外配'],
|
||||
'派送方式',
|
||||
);
|
||||
expectExactOptionSet(bolModal, 'bol-pickupMethod', ['一提一卸', '一提多卸'], '提货方式');
|
||||
expectExactOptionSet(bolModal, 'bol-loadType', ['托盘', '地板', '混装'], '装车类型');
|
||||
expectExactOptionSet(bolModal, 'bol-bookAccount', ['4', '8'], '预约账号');
|
||||
expectExactOptionSet(
|
||||
bolModal,
|
||||
'bol-vehicleSize',
|
||||
['大车(30托/15托)', '小车(14托/12托)'],
|
||||
'车辆大小',
|
||||
);
|
||||
expect(!bolModal.includes('卡派地版'), '转派送弹框不应保留错误文案“卡派地版”');
|
||||
expect(bolModal.includes('<label>预约号</label>'), 'ISA号码应改为预约号');
|
||||
expect(bolModal.includes('id="bol-appointmentNo"'), '预约号应使用独立字段');
|
||||
expect(bolModal.includes('id="bol-courierCompanyId"'), '快递方式应保留快递公司选择');
|
||||
expect(!bolModal.includes('车队邮箱'), '转派送弹框应移除车队邮箱');
|
||||
expect(!bolModal.includes('ISA号码'), '转派送弹框不应保留 ISA号码 文案');
|
||||
expect(!bolModal.includes('是否发送派送单'), '转派送弹框应移除是否发送派送单');
|
||||
expect(!bolModal.includes('预估派送单费用'), '费用区应移除预估派送单费用');
|
||||
expect(!bolModal.includes('快递费用'), '费用区应移除快递费用');
|
||||
expect(!bolModal.includes('id="bol-driverEmail"'), '转派送弹框不应保留车队邮箱字段');
|
||||
expect(!bolModal.includes('id="bol-sendDispatch"'), '转派送弹框不应保留发送派送单字段');
|
||||
expect(!bolModal.includes('id="bol-estimatedDispatchFee"'), '转派送弹框不应保留预估派送费用字段');
|
||||
expect(!bolModal.includes('id="bol-courierFee"'), '转派送弹框不应保留快递费字段');
|
||||
expect(
|
||||
/id="bol-dispatchFee"[^>]*placeholder="请输入派送费用"/.test(bolModal),
|
||||
'派送费用输入框应提示“请输入派送费用”',
|
||||
);
|
||||
const vehicleTypeIndex = bolModal.indexOf('id="bol-vehicleType"');
|
||||
const vehicleSizeIndex = bolModal.indexOf('id="bol-vehicleSize"');
|
||||
const appointmentAccountIndex = bolModal.indexOf('id="bol-bookAccount"');
|
||||
expect(
|
||||
vehicleTypeIndex >= 0 && vehicleSizeIndex > vehicleTypeIndex && appointmentAccountIndex > vehicleSizeIndex,
|
||||
'车辆大小应排在车辆类型后、预约账号前',
|
||||
);
|
||||
|
||||
const functionNames = [
|
||||
'editVehicle',
|
||||
@@ -268,6 +298,7 @@ const functionNames = [
|
||||
'nextOutboundVehicleDeliveryNo',
|
||||
'saveBol',
|
||||
'openOutboundVehicleDelivery',
|
||||
'getOutboundVehicleCourierFeeDetails',
|
||||
];
|
||||
const scriptSource = `${app}\n${tail}`;
|
||||
const scriptCode = codeOnly(scriptSource);
|
||||
@@ -278,6 +309,14 @@ functionNames.forEach((name) => {
|
||||
expect(Boolean(functionSources[name]), `缺少 ${name} 函数`);
|
||||
});
|
||||
|
||||
const saveBolSource = functionSources.saveBol || '';
|
||||
expect(saveBolSource.includes("appointmentAccount:value('bol-bookAccount')"), '保存时应读取预约账号');
|
||||
expect(saveBolSource.includes("appointmentNo:value('bol-appointmentNo')"), '保存时应读取预约号');
|
||||
expect(saveBolSource.includes("vehicleSize:value('bol-vehicleSize')"), '保存时应读取车辆大小');
|
||||
expect(saveBolSource.includes('appointmentAccount:draft.appointmentAccount'), '派送记录应保存预约账号');
|
||||
expect(saveBolSource.includes('appointmentNo:draft.appointmentNo'), '派送记录应保存预约号');
|
||||
expect(saveBolSource.includes('vehicleSize:draft.vehicleSize'), '派送记录应保存车辆大小');
|
||||
|
||||
function runPureFunctionCheck(name, callback) {
|
||||
if (!functionSources[name]) return;
|
||||
try {
|
||||
@@ -313,7 +352,6 @@ runPureFunctionCheck('validateOutboundVehicleDeliveryDraft', (validateOutboundVe
|
||||
pickupMethod: '一提一卸',
|
||||
loadType: '地板',
|
||||
dispatchFee: 200,
|
||||
estimatedDispatchFee: 180,
|
||||
loadingFee: 150,
|
||||
}),
|
||||
'',
|
||||
@@ -325,10 +363,53 @@ runPureFunctionCheck('validateOutboundVehicleDeliveryDraft', (validateOutboundVe
|
||||
pickupMethod: '一提一卸',
|
||||
loadType: '地板',
|
||||
dispatchFee: -1,
|
||||
estimatedDispatchFee: 180,
|
||||
loadingFee: 150,
|
||||
}),
|
||||
/派送单费用/,
|
||||
/派送费用/,
|
||||
);
|
||||
assert.match(
|
||||
validateOutboundVehicleDeliveryDraft({
|
||||
deliveryDate: '2026-08-13',
|
||||
deliveryMethod: '快递',
|
||||
pickupMethod: '一提一卸',
|
||||
loadType: '托盘',
|
||||
dispatchFee: 200,
|
||||
courierCompanyId: '',
|
||||
}, [{id: 'EXP-1', status: '合作中'}]),
|
||||
/快递公司/,
|
||||
);
|
||||
assert.equal(
|
||||
validateOutboundVehicleDeliveryDraft({
|
||||
deliveryDate: '2026-08-13',
|
||||
deliveryMethod: '快递',
|
||||
pickupMethod: '一提一卸',
|
||||
loadType: '托盘',
|
||||
dispatchFee: 200,
|
||||
courierCompanyId: 'EXP-1',
|
||||
}, [{id: 'EXP-1', status: '合作中'}]),
|
||||
'',
|
||||
);
|
||||
});
|
||||
|
||||
runPureFunctionCheck('getOutboundVehicleCourierFeeDetails', (getOutboundVehicleCourierFeeDetails) => {
|
||||
assert.deepEqual(
|
||||
getOutboundVehicleCourierFeeDetails([
|
||||
{
|
||||
source: 'outboundVehicle',
|
||||
deliveryMethod: '快递',
|
||||
dispatchNo: 'PSD20260818001',
|
||||
courierCompanyName: 'UPS',
|
||||
appointmentDate: '2026-08-18',
|
||||
dispatchFee: 88,
|
||||
},
|
||||
]),
|
||||
[{
|
||||
dispatchNo: 'PSD20260818001',
|
||||
company: 'UPS',
|
||||
businessDate: '2026-08-18',
|
||||
currency: 'USD',
|
||||
amount: 88,
|
||||
}],
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
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 htmlPath = path.join(root, 'PDA(海外)', 'index.html');
|
||||
assert.ok(fs.existsSync(htmlPath), 'PDA(海外)/index.html must exist');
|
||||
|
||||
const html = fs.readFileSync(htmlPath, 'utf8');
|
||||
assert.match(html, /const I18N\s*=\s*\{/, 'three-language dictionary must exist');
|
||||
assert.match(html, /zh\s*:\s*\{/, 'Chinese dictionary must exist');
|
||||
assert.match(html, /en\s*:\s*\{/, 'English dictionary must exist');
|
||||
assert.match(html, /es\s*:\s*\{/, 'Spanish dictionary must exist');
|
||||
|
||||
for (const metric of ['待卸货集装箱', '今日已入库', '今日待出库']) {
|
||||
assert.ok(html.includes(metric), `workbench metric missing: ${metric}`);
|
||||
}
|
||||
|
||||
assert.ok(!html.includes('入库上架'), 'overseas PDA must not expose inbound shelving');
|
||||
assert.ok(!html.includes('批次号'), 'overseas PDA must not expose batch fields');
|
||||
|
||||
for (const photoKey of ['licensePlate', 'containerSeal', 'doorOpen', 'unloading', 'unloadComplete']) {
|
||||
assert.ok(html.includes(photoKey), `unload photo step missing: ${photoKey}`);
|
||||
}
|
||||
assert.match(html, /capture=["']environment["']/, 'unload photos must support the rear camera');
|
||||
assert.match(html, /mergeFlag|isMerged/, 'pallet lines must expose a merge marker');
|
||||
assert.ok(html.includes('托数'), 'inbound plans must use pallet count rather than carton count');
|
||||
assert.match(html, /startPlanPallet|开始打托/, 'inbound plan must start palletizing');
|
||||
assert.match(html, /startConsolidation|合托/, 'consolidation must have a separate entry');
|
||||
assert.match(html, /targetPallets/, 'palletizing must carry the inbound plan pallet target');
|
||||
assert.match(html, /palletMode\s*===\s*['"]consolidate['"]/, 'palletizing and consolidation modes must be distinct');
|
||||
assert.match(html, /expectedPallets/, 'palletizing progress must use planned pallet count');
|
||||
assert.match(html, /palletTargetReached/, 'submitting more pallets than the plan target must be blocked');
|
||||
assert.match(html, /receiveListView/, 'unload receiving must open a pending-container list first');
|
||||
assert.match(html, /page==='receive-list'/, 'pending-container list must be a separate page');
|
||||
assert.ok(html.includes("container:'D2222'"), 'container codes must demonstrate the D2222 format');
|
||||
assert.match(html, /-TC-/, 'pallet codes must follow the container-TC-sequence format');
|
||||
assert.ok(!html.includes('打托 / 合托'), 'palletize and consolidation must not use a combined visible label');
|
||||
assert.match(html, /inboundPallet|入库打托/, 'inbound palletizing must be the single inbound pallet entry');
|
||||
assert.doesNotMatch(html, /<h3>\$\{t\('plan'\)\}<\/h3>[\s\S]*?<h3>\$\{t\('pallet'\)\}/, 'workbench must not expose duplicate plan and pallet entries');
|
||||
assert.match(html, /palletCount|托盘数量/, 'outbound orders must expose pallet quantity');
|
||||
assert.match(html, /orderInfo|关联订单/, 'outbound orders must expose linked order information');
|
||||
for (const outboundField of ['inboundCode','plateNo','driver','outboundTime','totalPallets']) {
|
||||
assert.ok(html.includes(outboundField), `outbound picking field missing: ${outboundField}`);
|
||||
}
|
||||
assert.match(html, /totalPalletBoxes|总托数\s*\/\s*总箱数/, 'outbound picking must combine total pallets and total cartons');
|
||||
assert.match(html, /injectInboundPalletUnits[\s\S]*entries\?\.\[1\]/, 'unload receiving must remain immediately after inbound palletizing');
|
||||
assert.match(html, /injectInboundPalletUnits[\s\S]*insertBefore/, 'workbench inbound entries must order unload before palletizing');
|
||||
for (const unloadField of ['tractorDriver','tractorPlate']) {
|
||||
assert.ok(html.includes(unloadField), `unload list field missing: ${unloadField}`);
|
||||
}
|
||||
assert.match(html, /receiveListView[\s\S]*inboundCode/, 'unload list must label ticket as inbound code');
|
||||
assert.match(html, /destinationWarehouse/, 'unload list must use destination warehouse label');
|
||||
assert.match(html, /destinationWarehouses|slice\(0,3\)/, 'unload list must support up to three destination warehouse codes');
|
||||
assert.match(html, /navigate\('receive-list'\)/, 'workbench must expose a separate unload receiving entry');
|
||||
assert.match(html, /page\s*=\s*['"]pallet-list['"]|palletListView/, 'inbound palletizing must open a pallet list before pallet detail');
|
||||
assert.match(html, /selectPallet|openPalletDetail|pallet-list/, 'pallet list must provide a pallet detail selection entry');
|
||||
assert.match(html, /viewPalletDetails|选择查看明细/, 'inbound order list must use the view details label');
|
||||
assert.match(html, /startPalletWork|t\('startPallet'\)/, 'inbound order list must provide a palletizing action');
|
||||
assert.match(html, /btn ghost[\s\S]*viewPalletDetails|viewPalletDetails[\s\S]*btn ghost/, 'view details must be the secondary left action');
|
||||
assert.match(html, /directEnter|直接进入/, 'pallet list must directly enter pallet detail');
|
||||
assert.match(html, /palletLabelSelector|selectPalletLabel|选择托盘标/, 'pallet detail must provide a pallet label selector');
|
||||
assert.match(html, /palletLabelOption[\s\S]*idle|occupied/, 'pallet label options must show pallet number and status');
|
||||
assert.match(html, /t\('boxMark'\)|箱唛/, 'pallet detail must show box mark');
|
||||
assert.match(html, /t\('inboundCode'\)|入仓码/, 'pallet detail must show inbound code');
|
||||
assert.match(html, /t\('productName'\)|产品名称/, 'pallet detail must show product name');
|
||||
assert.match(html, /removePalletItem/, 'pallet detail must keep a delete action');
|
||||
const orderListView = html.match(/function plansPalletView\(\)\{[^\n]+\}/)?.[0] || '';
|
||||
assert.equal((orderListView.match(/t\('container'\)/g) || []).length, 1, 'inbound order card must show container number only once');
|
||||
assert.match(orderListView, /esc\(p\.container\)/, 'inbound order cards must render each plan container number');
|
||||
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(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');
|
||||
for (const handoverPhoto of ['licensePlatePhoto','vehicleCabPhoto','driverLicense','loadingSealPhoto','lockSealPhoto','bolDriverSigned','amazonAppointment']) {
|
||||
assert.ok(html.includes(handoverPhoto), `handover photo step missing: ${handoverPhoto}`);
|
||||
}
|
||||
assert.match(html, /HANDOVER_PHOTO_STEPS/, 'handover must require a seven-photo checklist');
|
||||
assert.match(html, /completed===7|completed<7|7 \/ 7/, 'handover completion must require seven photos');
|
||||
assert.match(html, /function relabelView|page==='relabel'/, 'relabel operation must be implemented');
|
||||
assert.match(html, /function exceptionView|page==='exception'/, 'exception operation must be implemented');
|
||||
|
||||
const fnMatch = html.match(/function canMergePallet\(pallet,\s*cargo\)\s*\{[\s\S]*?\n\s*\}/);
|
||||
assert.ok(fnMatch, 'canMergePallet(pallet, cargo) must exist');
|
||||
const canMergePallet = vm.runInNewContext(`(${fnMatch[0].replace('function canMergePallet', 'function')})`);
|
||||
|
||||
const emptyPallet = { destinationCode: '', destinationAddress: '' };
|
||||
const laxA = { customer: 'A', planNo: 'IN-A', destinationCode: 'ONT8', destinationAddress: '24300 Nandina Ave, Moreno Valley, CA' };
|
||||
const laxB = { customer: 'B', planNo: 'IN-B', destinationCode: 'ONT8', destinationAddress: '24300 Nandina Ave, Moreno Valley, CA' };
|
||||
const conflict = { customer: 'B', planNo: 'IN-B', destinationCode: 'LGB8', destinationAddress: '1568 N Linden Ave, Rialto, CA' };
|
||||
|
||||
assert.equal(canMergePallet(emptyPallet, laxA), true, 'first cargo must lock an empty pallet');
|
||||
assert.equal(canMergePallet({ ...laxA }, laxB), true, 'same destination must merge across customers and plans');
|
||||
assert.equal(canMergePallet({ ...laxA }, conflict), false, 'different destination must be blocked');
|
||||
|
||||
console.log('verify-overseas-pda: all assertions passed');
|
||||
@@ -85,9 +85,7 @@ function functionSource(source, name) {
|
||||
|
||||
const businessTabs = {
|
||||
firstMile: '头程',
|
||||
warehouseRent: '仓租',
|
||||
relabel: '贴标',
|
||||
warehouseTransfer: '仓储转运',
|
||||
lastMile: '尾程',
|
||||
};
|
||||
|
||||
const businessTabsSource = elementSourceById(financeHtml, 'profit-business-tabs');
|
||||
@@ -100,6 +98,18 @@ Object.entries(businessTabs).forEach(([key, label]) => {
|
||||
);
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
!/data-profit-business=["'](warehouseRent|relabel|warehouseTransfer)["']/.test(businessTabsSource),
|
||||
'利润表不应继续展示仓租、贴标或仓储转运业务页签',
|
||||
);
|
||||
|
||||
const orderTypeMatch = /<select\b[^>]*\bid=["']profit-order-type["'][^>]*>[\s\S]*?<\/select>/i.exec(financeHtml);
|
||||
const orderTypeSource = orderTypeMatch ? orderTypeMatch[0] : '';
|
||||
assert.ok(orderTypeSource, '利润表缺少订单类型查询下拉');
|
||||
['全部订单类型', '暂扣', '退货', '仓储转运', '换标'].forEach((label) => {
|
||||
assertPattern(orderTypeSource, new RegExp('>\\s*' + label + '\\s*</option>'), `订单类型下拉缺少“${label}”`);
|
||||
});
|
||||
|
||||
const profitBusinessDataSource = constObjectSource(appJs, 'profitBusinessData');
|
||||
assert.ok(profitBusinessDataSource, '缺少利润表业务数据对象');
|
||||
assertPattern(appJs, /let\s+profitBusiness\s*=\s*["']firstMile["']/, '利润表默认业务类型应为头程');
|
||||
@@ -110,6 +120,9 @@ Object.keys(businessTabs).forEach((key) => {
|
||||
assertPattern(profitBusinessDataSource, new RegExp(`(?:["']${key}["']|\\b${key})\\s*:\\s*\\[`), `利润表业务数据缺少 ${key} 数组`);
|
||||
});
|
||||
|
||||
assertPattern(appJs, /profit-order-type/, '利润表脚本未读取订单类型筛选');
|
||||
assertPattern(appJs, /orderType/, '利润表数据或过滤逻辑缺少订单类型字段');
|
||||
|
||||
const profitBusinessLabelsSource = constObjectSource(appJs, 'profitBusinessLabels');
|
||||
assert.ok(profitBusinessLabelsSource, '缺少利润表业务名称对象');
|
||||
Object.entries(businessTabs).forEach(([key, label]) => {
|
||||
@@ -130,7 +143,7 @@ Object.keys(businessTabs).forEach((key) => {
|
||||
);
|
||||
});
|
||||
|
||||
for (const feeLabel of ['头程运费收入', '仓租收入', '贴标服务收入', '仓储转运收入']) {
|
||||
for (const feeLabel of ['头程运费收入', '尾程服务收入']) {
|
||||
assertPattern(
|
||||
profitFeeTemplatesSource,
|
||||
new RegExp("[\"']" + feeLabel + "[\"']"),
|
||||
@@ -153,5 +166,10 @@ assertPattern(
|
||||
/profitBusiness\s*=\s*["']firstMile["']/,
|
||||
'利润表重置时应恢复头程业务类型',
|
||||
);
|
||||
assertPattern(
|
||||
profitResetSource,
|
||||
/profit-order-type[\s\S]*value\s*=\s*["']?["']?/,
|
||||
'利润表重置时应清空订单类型',
|
||||
);
|
||||
|
||||
console.log('利润表业务类型页签回归校验通过');
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
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');
|
||||
|
||||
for (const pageId of ['page-domestic-purchase-detail', 'page-overseas-purchase-detail']) {
|
||||
const start = moduleSource.indexOf(`id="${pageId}"`);
|
||||
assert.ok(start >= 0, `${pageId} is missing`);
|
||||
const end = moduleSource.indexOf('<div class="page"', start + 1);
|
||||
const page = moduleSource.slice(start, end < 0 ? moduleSource.length : end);
|
||||
assert.ok(page.includes('<h3>操作记录</h3>'), `${pageId} is missing 操作记录`);
|
||||
assert.ok(!page.includes('进销存明细'), `${pageId} still exposes 进销存明细`);
|
||||
assert.ok(!page.includes('-detail-ledger'), `${pageId} still exposes the ledger DOM id`);
|
||||
}
|
||||
|
||||
assert.ok(!appSource.includes('inventoryTransactions:'), 'purchase data still defines inventoryTransactions');
|
||||
assert.ok(!appSource.includes('domestic-detail-ledger'), 'domestic purchase renderer still targets the ledger DOM');
|
||||
assert.ok(!appSource.includes('overseas-detail-ledger'), 'overseas purchase renderer still targets the ledger DOM');
|
||||
assert.match(appSource, /operationRecords:/, 'purchase data does not define operationRecords');
|
||||
assert.match(appSource, /r\.operationRecords/, 'domestic or overseas detail does not read operationRecords');
|
||||
assert.match(appSource, /domestic-detail-operation-records/, 'domestic operation-record renderer is missing');
|
||||
assert.match(appSource, /overseas-detail-operation-records/, 'overseas operation-record renderer is missing');
|
||||
const expectedHeaderRow = '<th>操作时间</th><th>操作内容</th><th>操作人</th><th>操作结果</th><th>备注</th>';
|
||||
assert.ok(appSource.split(expectedHeaderRow).length - 1 >= 2, 'both detail renderers must output the five operation-record headers');
|
||||
|
||||
console.log('PURCHASE_ORDER_OPERATION_RECORDS_CHECK_PASS');
|
||||
@@ -0,0 +1,65 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const salesModule = fs.readFileSync(path.join(root, 'prototype-src/modules/09-sales.html'), 'utf8');
|
||||
const appJs = fs.readFileSync(path.join(root, 'prototype-src/app.js'), 'utf8');
|
||||
const salesDataBlock = appJs.slice(appJs.indexOf('const salesCommissionData='), appJs.indexOf('const sysTenantData'));
|
||||
|
||||
function assertInOrder(source, labels, message) {
|
||||
let previous = -1;
|
||||
labels.forEach((label) => {
|
||||
const position = source.indexOf(label, previous + 1);
|
||||
assert.ok(position > previous, `${message}: 缺少或顺序错误 “${label}”`);
|
||||
previous = position;
|
||||
});
|
||||
}
|
||||
|
||||
assertInOrder(
|
||||
salesModule,
|
||||
['综合提成率', '固定提成(元)', '溢价提成(元)', '垫款利息(元)', '提成金额(元)'],
|
||||
'销售提成列表表头'
|
||||
);
|
||||
|
||||
['fixedCommission', 'premiumCommission', 'advanceInterest'].forEach((field) => {
|
||||
assert.ok(new RegExp(`${field}:-?\\d`).test(salesDataBlock), `销售人员示例数据缺少 ${field}`);
|
||||
});
|
||||
const advanceInterestValues = [...salesDataBlock.matchAll(/advanceInterest:(-?\d+)/g)].map((match) => Number(match[1]));
|
||||
assert.equal(advanceInterestValues.length, 5, '销售人员垫款利息示例数据数量不正确');
|
||||
assert.ok(advanceInterestValues.every((value) => value < 0), '垫款利息必须全部为负值');
|
||||
assert.ok(
|
||||
appJs.includes("return (value<0?'-':'')+'¥'+Math.abs(value).toLocaleString"),
|
||||
'负数金额必须显示为 -¥金额'
|
||||
);
|
||||
const salesOrderCount = (salesDataBlock.match(/{no:'SO/g) || []).length;
|
||||
assert.equal((salesDataBlock.match(/floorPrice:\d/g) || []).length, salesOrderCount, '并非每条订单都有 floorPrice');
|
||||
assert.equal((salesDataBlock.match(/settlementStatus:/g) || []).length, salesOrderCount, '并非每条订单都有独立结算状态');
|
||||
assert.ok(/settlementStatus:'未结算'/.test(salesDataBlock), '订单示例数据缺少未结算状态');
|
||||
assert.ok(/settlementStatus:'已结算'/.test(salesDataBlock), '订单示例数据缺少已结算状态');
|
||||
|
||||
assert.ok(/comm:fixedCommission\+premiumCommission\+advanceInterest/.test(appJs), '提成金额没有按三类提成求和');
|
||||
assert.ok(/calculatedComm:total\*rate\/100/.test(appJs), '原订单比例提成结果没有保留给明细使用');
|
||||
assert.ok(appJs.includes('scMoney(c.calculatedComm)'), '明细汇总没有继续使用原订单比例提成口径');
|
||||
['c.fixedCommission', 'c.premiumCommission', 'c.advanceInterest'].forEach((field) => {
|
||||
assert.ok(appJs.includes(field), `列表没有渲染 ${field}`);
|
||||
});
|
||||
assert.ok(/colspan="12"[^>]*>暂无匹配数据/.test(appJs), '列表空态列数不是 12');
|
||||
|
||||
assertInOrder(
|
||||
appJs,
|
||||
['收款时间', '状态', '客户名称'],
|
||||
'订单提成明细状态表头'
|
||||
);
|
||||
assertInOrder(
|
||||
appJs,
|
||||
['单价(元/kg)', '销售底价(元/kg)', '件数'],
|
||||
'订单提成明细销售底价表头'
|
||||
);
|
||||
assert.ok(/o\.floorPrice\.toFixed\(2\)/.test(appJs), '订单提成明细没有渲染销售底价');
|
||||
assert.ok(/o\.settlementStatus/.test(appJs), '订单提成明细没有渲染独立结算状态');
|
||||
assert.ok(appJs.includes("o.settlementStatus==='已结算'?'tag-green':'tag-orange'"), '结算状态没有使用约定标签颜色');
|
||||
assert.ok(/colspan="10"[^>]*>合计/.test(appJs), '明细合计行列数不是 10');
|
||||
assert.ok(/overflow-x:auto/.test(appJs), '13 列明细表缺少横向滚动容器');
|
||||
|
||||
console.log('销售提成字段验证通过');
|
||||
@@ -91,10 +91,27 @@ assert.match(
|
||||
/<div class="breadcrumb">首页 <span class="sep">><\/span> 通用/,
|
||||
'车辆列表面包屑应归属通用模块',
|
||||
);
|
||||
assert.equal(
|
||||
(vehicleListPage.match(/class="vehicle-overseas-only"[^>]*>邮箱<\/th>/g) || []).length,
|
||||
2,
|
||||
'自有和外派车辆列表都应提供海外专属邮箱列',
|
||||
);
|
||||
assert.match(
|
||||
vehicleListPage,
|
||||
/class="vehicle-overseas-only" style="display:none;/,
|
||||
'国内车辆默认视图应隐藏邮箱列',
|
||||
);
|
||||
assert.match(vehicleListPage, /id="vehicle-m-email"/, '车辆新增/编辑表单缺少邮箱字段');
|
||||
assert.match(
|
||||
vehicleListPage,
|
||||
/id="vehicle-m-region"[^>]*onchange="syncVehicleEmailFieldVisibility\(\)"/,
|
||||
'切换车辆区域时应同步邮箱字段可见性',
|
||||
);
|
||||
|
||||
assert.match(vehicleDetailPage, /id="vehicle-detail-plate"/, '详情页缺少车牌标题节点');
|
||||
assert.match(vehicleDetailPage, /id="vehicle-detail-status"/, '详情页缺少状态节点');
|
||||
assert.match(vehicleDetailPage, /id="vehicle-detail-info"/, '详情页缺少车辆信息区域');
|
||||
assert.match(app, /\['邮箱',vehicle\.email\|\|'—'\]/, '车辆详情缺少邮箱字段');
|
||||
assert.match(vehicleDetailPage, /id="vehicle-detail-orders-body"/, '详情页缺少派送订单明细表体');
|
||||
assert.match(vehicleDetailPage, /id="vehicle-detail-history-body"/, '详情页缺少派送更换记录表体');
|
||||
assert.match(vehicleDetailPage, /onclick="backToVehicleList\(\)"/, '详情页缺少返回列表入口');
|
||||
@@ -109,10 +126,13 @@ assert.match(shell, /\.vehicle-detail-grid/, '缺少车辆详情信息栅格样
|
||||
assert.match(shell, /@media[^}]*max-width:\s*900px/s, '缺少车辆详情窄屏适配');
|
||||
|
||||
const aggregateSource = extractFunction(app, 'getVehicleDispatchOrders');
|
||||
const emailVisibilitySource = extractFunction(app, 'syncVehicleEmailFieldVisibility');
|
||||
const detailRenderSource = extractFunction(app, 'renderVehicleDetail');
|
||||
const openDetailSource = extractFunction(app, 'openVehicleDetail');
|
||||
const backToListSource = extractFunction(app, 'backToVehicleList');
|
||||
assert.doesNotMatch(detailRenderSource, /\bd\./, '详情渲染仍引用旧弹窗局部变量 d');
|
||||
assert.match(emailVisibilitySource, /region==='overseas'/, '邮箱表单应仅对海外车辆显示');
|
||||
assert.match(app, /const email=region==='overseas'/, '保存车辆时应仅为海外车辆读取邮箱');
|
||||
assert.match(
|
||||
openDetailSource,
|
||||
/switchPage\('vehicle-detail',vehicleNav\)/,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const generalHtml = fs.readFileSync(path.join(root, 'prototype-src/modules/10-general.html'), 'utf8');
|
||||
const appJs = fs.readFileSync(path.join(root, 'prototype-src/app.js'), 'utf8');
|
||||
|
||||
function extractPage(source, pageId) {
|
||||
const marker = `<div class="page" id="page-${pageId}">`;
|
||||
const start = source.indexOf(marker);
|
||||
if (start < 0) return '';
|
||||
const next = source.indexOf('<div class="page" id="page-', start + marker.length);
|
||||
return source.slice(start, next < 0 ? source.length : next);
|
||||
}
|
||||
|
||||
function extractFunction(source, name, nextName) {
|
||||
const start = source.indexOf(`function ${name}(`);
|
||||
if (start < 0) return '';
|
||||
const end = source.indexOf(`function ${nextName}(`, start);
|
||||
return source.slice(start, end < 0 ? source.length : end);
|
||||
}
|
||||
|
||||
const voyagePage = extractPage(generalHtml, 'gen-voyage');
|
||||
assert.ok(voyagePage, '通用资料模块缺少 page-gen-voyage');
|
||||
assert.match(
|
||||
voyagePage,
|
||||
/<th id="cost-unit-header">成本单价\(元\/kg\)<\/th><th>销售底价<\/th><th>建议报价<\/th><\/tr><\/thead><tbody id="tbody-voyage-cost">/,
|
||||
'成本测算表尾列顺序必须为“成本单价、销售底价、建议报价”',
|
||||
);
|
||||
assert.match(
|
||||
voyagePage,
|
||||
/销售底价\s*=\s*建议报价\s*-\s*0\.6/,
|
||||
'成本测算公式说明缺少“销售底价 = 建议报价 - 0.6”',
|
||||
);
|
||||
|
||||
const calculateCostRowSource = extractFunction(appJs, 'calculateCostRow', 'renderCostCalcRows');
|
||||
assert.ok(calculateCostRowSource, 'app.js 缺少 calculateCostRow 函数');
|
||||
assert.match(
|
||||
calculateCostRowSource,
|
||||
/salesFloor\s*:\s*quote\s*-\s*0\.6/,
|
||||
'calculateCostRow 必须返回固定派生值 salesFloor: quote - 0.6',
|
||||
);
|
||||
|
||||
const renderCostCalcRowsSource = extractFunction(appJs, 'renderCostCalcRows', 'calculateVoyageCosts');
|
||||
assert.ok(renderCostCalcRowsSource, 'app.js 缺少 renderCostCalcRows 函数');
|
||||
assert.match(
|
||||
renderCostCalcRowsSource,
|
||||
/<td class="cost-sales-floor">¥'\+c\.salesFloor\.toFixed\(2\)\+'<\/td><td class="cost-quote">¥'\+c\.quote\.toFixed\(2\)/,
|
||||
'renderCostCalcRows 必须在建议报价前渲染 cost-sales-floor 与 salesFloor.toFixed(2)',
|
||||
);
|
||||
|
||||
console.log('VOYAGE_SALES_FLOOR_CHECK_PASS');
|
||||
@@ -0,0 +1,296 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>费用流转流程图</title>
|
||||
<style>
|
||||
:root{--ink:#1f2937;--muted:#64748b;--line:#d8e1eb;--blue:#2563eb;--blue-soft:#eff6ff;--orange:#d97706;--orange-soft:#fff7ed;--green:#15803d;--green-soft:#f0fdf4;--red:#dc2626;--red-soft:#fef2f2;--gray:#64748b;--gray-soft:#f8fafc;--lane:#f7f9fc;--lane-alt:#fbfdff}
|
||||
*{box-sizing:border-box}
|
||||
html,body{margin:0;min-height:100%;font-family:"Microsoft YaHei","PingFang SC",Arial,sans-serif;color:var(--ink);background:#eef2f6}
|
||||
body{padding:24px}
|
||||
.page{max-width:1800px;margin:0 auto}
|
||||
.toolbar{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;margin-bottom:16px;padding:20px 22px;background:#fff;border:1px solid #dbe3ed;border-radius:10px;box-shadow:0 6px 18px rgba(30,41,59,.06)}
|
||||
.title-wrap h1{margin:0;font-size:24px;letter-spacing:0;font-weight:760;color:#172033}
|
||||
.title-wrap p{margin:8px 0 0;color:var(--muted);font-size:13px;line-height:1.55}
|
||||
.toolbar-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px}
|
||||
button{border:1px solid #cbd5e1;border-radius:6px;background:#fff;color:#334155;padding:9px 14px;font:inherit;font-size:13px;cursor:pointer;transition:background .15s,border-color .15s,color .15s}
|
||||
button:hover{border-color:#94a3b8;background:#f8fafc}
|
||||
button.primary{border-color:#2563eb;background:#2563eb;color:#fff}
|
||||
button.primary:hover{background:#1d4ed8;border-color:#1d4ed8}
|
||||
button.danger{color:#b91c1c;border-color:#fecaca;background:#fffafa}
|
||||
.legend{display:flex;align-items:center;flex-wrap:wrap;gap:10px 18px;margin-bottom:14px;padding:12px 16px;background:#fff;border:1px solid #dbe3ed;border-radius:8px;color:#475569;font-size:12px}
|
||||
.legend-title{font-weight:700;color:#334155;margin-right:2px}
|
||||
.legend-item{display:inline-flex;align-items:center;gap:7px;white-space:nowrap}
|
||||
.legend-dot{width:12px;height:12px;border-radius:4px;border:1px solid currentColor;display:inline-block}
|
||||
.legend-dot.blue{background:var(--blue-soft);color:var(--blue)}
|
||||
.legend-dot.orange{background:var(--orange-soft);color:var(--orange)}
|
||||
.legend-dot.green{background:var(--green-soft);color:var(--green)}
|
||||
.legend-dot.red{background:var(--red-soft);color:var(--red)}
|
||||
.legend-dot.gray{background:var(--gray-soft);color:var(--gray);border-style:dashed}
|
||||
.legend-line{width:30px;border-top:2px solid #64748b;display:inline-block}
|
||||
.legend-line.red{border-top-color:var(--red);border-top-style:dashed}
|
||||
.legend-line.gray{border-top-color:#94a3b8;border-top-style:dashed}
|
||||
.scroll-frame{max-width:1602px;margin:0 auto;overflow:auto;border:1px solid #cfd9e5;border-radius:10px;background:#fff;box-shadow:0 8px 22px rgba(30,41,59,.06)}
|
||||
.canvas{position:relative;width:1600px;height:3300px;background:#fff}
|
||||
.lane{position:absolute;top:0;bottom:0;width:400px;border-left:1px solid #dbe4ee;border-right:1px solid #dbe4ee}
|
||||
.lane:nth-of-type(1){left:0;background:var(--lane-alt)}
|
||||
.lane:nth-of-type(2){left:400px;background:var(--lane)}
|
||||
.lane:nth-of-type(3){left:800px;background:var(--lane-alt)}
|
||||
.lane:nth-of-type(4){left:1200px;background:var(--lane)}
|
||||
.lane-label{position:absolute;left:0;right:0;top:0;height:76px;display:flex;align-items:center;justify-content:center;border-bottom:1px solid #d6e0eb;background:rgba(241,245,249,.88);font-size:16px;font-weight:760;color:#334155;writing-mode:horizontal-tb;letter-spacing:1px}
|
||||
.lane-note{position:absolute;left:24px;right:24px;top:48px;text-align:center;font-size:10px;line-height:1.4;color:#94a3b8;white-space:nowrap;letter-spacing:0}
|
||||
.node{position:absolute;width:220px;height:74px;overflow:hidden;padding:12px 13px;display:flex;align-items:center;justify-content:center;text-align:center;border:1px solid #cbd5e1;border-radius:8px;background:#fff;box-shadow:0 3px 8px rgba(15,23,42,.07);font-size:13px;line-height:1.45;z-index:3}
|
||||
.node .node-text{display:block;width:100%;max-height:50px;overflow:auto;outline:0;white-space:pre-line;overflow-wrap:anywhere;scrollbar-width:thin}
|
||||
.node.system{border-color:#93c5fd;background:var(--blue-soft);color:#1e40af}
|
||||
.node.pending{border-color:#f6c778;background:var(--orange-soft);color:#92400e}
|
||||
.node.success{border-color:#86d39d;background:var(--green-soft);color:#166534}
|
||||
.node.reject{border-color:#f3a4a4;background:var(--red-soft);color:#991b1b}
|
||||
.node.decision{border-style:dashed;border-color:#f59e0b;background:#fffdf5;color:#854d0e;border-radius:8px}
|
||||
.node.note{border-style:dashed;border-color:#b8c3d1;background:var(--gray-soft);color:#64748b;font-size:12px}
|
||||
.node.editing{box-shadow:0 0 0 3px rgba(37,99,235,.16),0 3px 8px rgba(15,23,42,.07)}
|
||||
.node.editing .node-text{cursor:text;background:rgba(255,255,255,.6);border-radius:4px;padding:2px 3px}
|
||||
.branch-tag{position:absolute;z-index:4;padding:6px 12px;border-radius:4px;font-size:11px;font-weight:700;letter-spacing:.2px;background:#fff}
|
||||
.branch-tag.ar{left:24px;top:1000px;color:#1d4ed8;border-left:3px solid #2563eb}
|
||||
.branch-tag.ap{left:24px;top:2130px;color:#7c3aed;border-left:3px solid #7c3aed}
|
||||
.canvas-caption{display:flex;justify-content:space-between;gap:12px;padding:11px 16px;border-top:1px solid #dbe4ee;background:#fff;color:#64748b;font-size:12px}
|
||||
.canvas-caption strong{color:#334155}
|
||||
.status{min-height:18px;color:#64748b;font-size:12px}
|
||||
svg.connections{position:absolute;inset:0;width:100%;height:100%;z-index:2;pointer-events:none}
|
||||
.connections path{fill:none;stroke:#64748b;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}
|
||||
.connections path.red{stroke:var(--red);stroke-dasharray:7 6}
|
||||
.connections path.gray{stroke:#94a3b8;stroke-dasharray:5 6}
|
||||
.connections text{font-family:"Microsoft YaHei","PingFang SC",Arial,sans-serif;font-size:12px;fill:#64748b}
|
||||
.connections text.red{fill:var(--red)}
|
||||
.connections text.gray{fill:#64748b}
|
||||
@media (max-width:900px){body{padding:12px}.toolbar{display:block}.toolbar-actions{justify-content:flex-start;margin-top:14px}.title-wrap h1{font-size:20px}}
|
||||
@media print{body{padding:0;background:#fff}.toolbar,.legend,.canvas-caption{display:none}.page{max-width:none}.scroll-frame{border:0;overflow:visible;box-shadow:none}.canvas{transform-origin:top left;transform:scale(.48);margin-bottom:-1716px}}
|
||||
</style>
|
||||
</head>
|
||||
<body data-storage-key="fee-flow-node-texts-v1">
|
||||
<main class="page">
|
||||
<header class="toolbar">
|
||||
<div class="title-wrap">
|
||||
<h1>费用流转流程图</h1>
|
||||
<p>依据后台订单与财务原型整理:费用生成 → 财务审核 → 应收 / 应付结算 → 收付款结清。</p>
|
||||
</div>
|
||||
<div class="toolbar-actions" aria-label="流程图操作">
|
||||
<button type="button" class="primary" id="edit-toggle">编辑文字</button>
|
||||
<button type="button" id="reset-flow">恢复默认</button>
|
||||
<button type="button" id="download-flow">下载 HTML</button>
|
||||
<button type="button" id="print-flow">打印</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="legend" aria-label="图例">
|
||||
<span class="legend-title">图例</span>
|
||||
<span class="legend-item"><i class="legend-dot blue"></i>系统动作 / 单据</span>
|
||||
<span class="legend-item"><i class="legend-dot orange"></i>待处理 / 待确认</span>
|
||||
<span class="legend-item"><i class="legend-dot green"></i>完成 / 结清</span>
|
||||
<span class="legend-item"><i class="legend-dot red"></i>驳回 / 异常</span>
|
||||
<span class="legend-item"><i class="legend-dot gray"></i>原型未定义 / 留档</span>
|
||||
<span class="legend-item"><i class="legend-line"></i>正常流</span>
|
||||
<span class="legend-item"><i class="legend-line red"></i>驳回回路</span>
|
||||
<span class="legend-item"><i class="legend-line gray"></i>旁路</span>
|
||||
</section>
|
||||
|
||||
<div class="scroll-frame">
|
||||
<section class="canvas" aria-label="费用流转竖向泳道图" data-flow-direction="vertical" data-department-layout="columns" data-canvas-width="1600" data-canvas-height="3300">
|
||||
<div class="lane" data-department="业务订单"><div class="lane-label">业务订单</div><div class="lane-note">国内仓 / 海外仓 / 派送 / 货柜等业务单据</div></div>
|
||||
<div class="lane" data-department="系统"><div class="lane-label">系统</div><div class="lane-note">记录费用、账单、结算单和余额状态</div></div>
|
||||
<div class="lane" data-department="财务"><div class="lane-label">财务</div><div class="lane-note">审核、对账、审批、收付款登记</div></div>
|
||||
<div class="lane" data-department="客户与供应商"><div class="lane-label">客户与供应商</div><div class="lane-note">客户确认已实现;供应商确认未在原型定义</div></div>
|
||||
|
||||
<span class="branch-tag ar">应收分支 · 客户结算</span>
|
||||
<span class="branch-tag ap">应付分支 · 供应商结算</span>
|
||||
|
||||
<svg class="connections" viewBox="0 0 1600 3300" role="img" aria-label="费用流转连线">
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 Z" fill="#64748b"></path></marker>
|
||||
<marker id="arrow-red" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 Z" fill="#dc2626"></path></marker>
|
||||
<marker id="arrow-gray" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 Z" fill="#94a3b8"></path></marker>
|
||||
</defs>
|
||||
|
||||
<path d="M200 194 V224 H600 V250" marker-end="url(#arrow)"></path>
|
||||
<path d="M600 324 V352 H1000 V380" marker-end="url(#arrow)"></path>
|
||||
<path d="M1000 454 V510" marker-end="url(#arrow)"></path>
|
||||
<path d="M890 547 H600 V650" marker-end="url(#arrow)"></path>
|
||||
<text x="730" y="535">通过</text>
|
||||
<path d="M1000 584 V610 H200 V650" class="red" marker-end="url(#arrow-red)"></path>
|
||||
<text x="925" y="603" class="red">驳回</text>
|
||||
<path d="M90 687 H40 V287 H490" class="red" marker-end="url(#arrow-red)"></path>
|
||||
<text x="48" y="472" class="red">编辑后重新提交</text>
|
||||
<path d="M600 724 V790" marker-end="url(#arrow)"></path>
|
||||
<path d="M710 827 H1000 V900" class="gray" marker-end="url(#arrow-gray)"></path>
|
||||
<text x="822" y="815" class="gray">否:留档</text>
|
||||
<path d="M600 864 V1040" marker-end="url(#arrow)"></path>
|
||||
<text x="612" y="963">是:应收</text>
|
||||
<path d="M490 827 H430 V2217 H490" marker-end="url(#arrow)"></path>
|
||||
<text x="440" y="2106">是:应付</text>
|
||||
|
||||
<path d="M600 1114 V1142 H1000 V1170" marker-end="url(#arrow)"></path>
|
||||
<path d="M1000 1244 V1300" marker-end="url(#arrow)"></path>
|
||||
<path d="M1000 1374 V1402 H1400 V1430" marker-end="url(#arrow)"></path>
|
||||
<path d="M1400 1504 V1560" class="red" marker-end="url(#arrow-red)"></path>
|
||||
<text x="1412" y="1540" class="red">否</text>
|
||||
<path d="M1290 1467 H1180 V1518 H600 V1560" marker-end="url(#arrow)"></path>
|
||||
<text x="1200" y="1454">是:已确认</text>
|
||||
<path d="M1510 1597 H1560 V1337 H1110" class="red" marker-end="url(#arrow-red)"></path>
|
||||
<text x="1470" y="1392" class="red">重新推送</text>
|
||||
<path d="M600 1634 V1662 H1000 V1690" marker-end="url(#arrow)"></path>
|
||||
<path d="M1000 1764 V1820" marker-end="url(#arrow)"></path>
|
||||
<path d="M1000 1894 V1950" marker-end="url(#arrow)"></path>
|
||||
<path d="M600 1634 V1820" class="gray" marker-end="url(#arrow-gray)"></path>
|
||||
<text x="612" y="1754" class="gray">原型未定义</text>
|
||||
|
||||
<path d="M600 2254 V2310" marker-end="url(#arrow)"></path>
|
||||
<path d="M600 2384 V2412 H1000 V2440" marker-end="url(#arrow)"></path>
|
||||
<path d="M1000 2514 V2570" class="red" marker-end="url(#arrow-red)"></path>
|
||||
<text x="1012" y="2552" class="red">驳回</text>
|
||||
<path d="M1110 2607 H1150 V2477 H1110" class="red" marker-end="url(#arrow-red)"></path>
|
||||
<text x="1160" y="2548" class="red">重新审批</text>
|
||||
<path d="M890 2477 H600 V2700" marker-end="url(#arrow)"></path>
|
||||
<text x="730" y="2465">通过</text>
|
||||
<path d="M710 2737 H1290" class="gray" marker-end="url(#arrow-gray)"></path>
|
||||
<text x="1008" y="2724" class="gray">供应商确认未定义</text>
|
||||
<path d="M600 2774 V2802 H1000 V2830" marker-end="url(#arrow)"></path>
|
||||
<path d="M1000 2904 V2960" marker-end="url(#arrow)"></path>
|
||||
<path d="M1000 3034 V3090" marker-end="url(#arrow)"></path>
|
||||
</svg>
|
||||
|
||||
<div class="node" style="left:90px;top:120px" data-node-id="order-fee-source" data-default-text="业务订单产生费用"><span class="node-text" contenteditable="false">业务订单产生费用</span></div>
|
||||
<div class="node system" style="left:490px;top:250px" data-node-id="fee-generated" data-default-text="系统生成应收/应付费用记录"><span class="node-text" contenteditable="false">系统生成应收/应付费用记录</span></div>
|
||||
<div class="node pending" style="left:890px;top:380px" data-node-id="fee-pending-audit" data-default-text="费用待审核"><span class="node-text" contenteditable="false">费用待审核</span></div>
|
||||
<div class="node decision" style="left:890px;top:510px" data-node-id="fee-audit-decision" data-default-text="财务审核通过?"><span class="node-text" contenteditable="false">财务审核通过?</span></div>
|
||||
<div class="node reject" style="left:90px;top:650px" data-node-id="fee-rejected" data-default-text="审核驳回:费用可编辑"><span class="node-text" contenteditable="false">审核驳回:费用可编辑</span></div>
|
||||
<div class="node success" style="left:490px;top:650px" data-node-id="fee-approved" data-default-text="审核通过:记录锁定"><span class="node-text" contenteditable="false">审核通过:记录锁定</span></div>
|
||||
<div class="node decision" style="left:490px;top:790px" data-node-id="join-settlement-decision" data-default-text="参与结算?"><span class="node-text" contenteditable="false">参与结算?</span></div>
|
||||
<div class="node note" style="left:890px;top:900px" data-node-id="fee-archived" data-default-text="不参与结算 / 禁止结算\n留档,不生成账单"><span class="node-text" contenteditable="false">不参与结算 / 禁止结算<br>留档,不生成账单</span></div>
|
||||
|
||||
<div class="node system" style="left:490px;top:1040px" data-node-id="ar-bill" data-default-text="客户对账单\n按费用明细汇总"><span class="node-text" contenteditable="false">客户对账单<br>按费用明细汇总</span></div>
|
||||
<div class="node pending" style="left:890px;top:1170px" data-node-id="ar-bill-audit" data-default-text="对账单审核通过"><span class="node-text" contenteditable="false">对账单审核通过</span></div>
|
||||
<div class="node" style="left:890px;top:1300px" data-node-id="ar-push" data-default-text="推送客户"><span class="node-text" contenteditable="false">推送客户</span></div>
|
||||
<div class="node decision" style="left:1290px;top:1430px" data-node-id="customer-confirm" data-default-text="客户确认?"><span class="node-text" contenteditable="false">客户确认?</span></div>
|
||||
<div class="node pending" style="left:1290px;top:1560px" data-node-id="ar-confirm-wait" data-default-text="待确认 / 可重新推送"><span class="node-text" contenteditable="false">待确认 / 可重新推送</span></div>
|
||||
<div class="node system" style="left:490px;top:1560px" data-node-id="ar-settlement" data-default-text="生成应收结算单"><span class="node-text" contenteditable="false">生成应收结算单</span></div>
|
||||
<div class="node pending" style="left:890px;top:1690px" data-node-id="ar-approve" data-default-text="应收结算单\n草稿 → 待审批 → 生效"><span class="node-text" contenteditable="false">应收结算单<br>草稿 → 待审批 → 生效</span></div>
|
||||
<div class="node pending" style="left:890px;top:1820px" data-node-id="ar-payment" data-default-text="收款登记\n部分收款可重复登记"><span class="node-text" contenteditable="false">收款登记<br>部分收款可重复登记</span></div>
|
||||
<div class="node success" style="left:890px;top:1950px" data-node-id="ar-paid" data-default-text="已结清"><span class="node-text" contenteditable="false">已结清</span></div>
|
||||
<div class="node note" style="left:490px;top:1820px" data-node-id="ar-undefined" data-default-text="自动开票 / 自动核销:原型未定义"><span class="node-text" contenteditable="false">自动开票 / 自动核销:原型未定义</span></div>
|
||||
|
||||
<div class="node system" style="left:490px;top:2180px" data-node-id="ap-bill" data-default-text="供应商应付对账\n按费用明细汇总"><span class="node-text" contenteditable="false">供应商应付对账<br>按费用明细汇总</span></div>
|
||||
<div class="node system" style="left:490px;top:2310px" data-node-id="ap-settlement" data-default-text="生成应付结算单"><span class="node-text" contenteditable="false">生成应付结算单</span></div>
|
||||
<div class="node pending" style="left:890px;top:2440px" data-node-id="ap-approval" data-default-text="应付结算审批\n草稿 → 待审批"><span class="node-text" contenteditable="false">应付结算审批<br>草稿 → 待审批</span></div>
|
||||
<div class="node reject" style="left:890px;top:2570px" data-node-id="ap-rejected" data-default-text="审批驳回\n可重新审批"><span class="node-text" contenteditable="false">审批驳回<br>可重新审批</span></div>
|
||||
<div class="node success" style="left:490px;top:2700px" data-node-id="ap-effective" data-default-text="审批通过:生效"><span class="node-text" contenteditable="false">审批通过:生效</span></div>
|
||||
<div class="node note" style="left:1290px;top:2700px" data-node-id="supplier-undefined" data-default-text="供应商确认:原型未定义"><span class="node-text" contenteditable="false">供应商确认:原型未定义</span></div>
|
||||
<div class="node" style="left:890px;top:2830px" data-node-id="ap-voucher" data-default-text="上传付款凭证"><span class="node-text" contenteditable="false">上传付款凭证</span></div>
|
||||
<div class="node pending" style="left:890px;top:2960px" data-node-id="ap-payment" data-default-text="付款登记\n部分付款可重复登记"><span class="node-text" contenteditable="false">付款登记<br>部分付款可重复登记</span></div>
|
||||
<div class="node success" style="left:890px;top:3090px" data-node-id="ap-paid" data-default-text="已结清"><span class="node-text" contenteditable="false">已结清</span></div>
|
||||
</section>
|
||||
<div class="canvas-caption"><span><strong>原型已确认:</strong>费用审核、客户确认门槛、结算审批、部分收付款与结清。</span><span><strong>原型未定义:</strong>供应商确认、自动开票、自动核销。</span></div>
|
||||
</div>
|
||||
<p class="status" id="status" role="status" aria-live="polite">当前为只读模式。点击“编辑文字”后可修改节点内容。</p>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const body = document.body;
|
||||
const storageKey = body.dataset.storageKey || 'fee-flow-node-texts-v1';
|
||||
const nodes = [...document.querySelectorAll('[data-node-id]')];
|
||||
const textEls = nodes.map(node => node.querySelector('.node-text')).filter(Boolean);
|
||||
const defaults = Object.fromEntries(nodes.map(node => [node.dataset.nodeId, node.dataset.defaultText.replace(/\\n/g, '\n')]));
|
||||
const editToggle = document.getElementById('edit-toggle');
|
||||
const resetButton = document.getElementById('reset-flow');
|
||||
const downloadButton = document.getElementById('download-flow');
|
||||
const printButton = document.getElementById('print-flow');
|
||||
const status = document.getElementById('status');
|
||||
const safeStorage = {
|
||||
get() { try { return localStorage.getItem(storageKey); } catch { return null; } },
|
||||
set(value) { try { localStorage.setItem(storageKey, value); return true; } catch { return false; } },
|
||||
remove() { try { localStorage.removeItem(storageKey); return true; } catch { return false; } }
|
||||
};
|
||||
|
||||
function setStatus(message) { status.textContent = message; }
|
||||
function readNodeText(textEl) {
|
||||
return String(textEl.innerText || textEl.textContent || '').replace(/\r\n/g, '\n').replace(/\u00a0/g, ' ');
|
||||
}
|
||||
function applyTexts(values) {
|
||||
nodes.forEach(node => {
|
||||
const value = values[node.dataset.nodeId];
|
||||
if (value !== undefined) node.querySelector('.node-text').textContent = String(value).replace(/\\n/g, '\n');
|
||||
});
|
||||
}
|
||||
function currentTexts() {
|
||||
return Object.fromEntries(nodes.map(node => [node.dataset.nodeId, readNodeText(node.querySelector('.node-text'))]));
|
||||
}
|
||||
function persist() {
|
||||
if (!safeStorage.set(JSON.stringify(currentTexts()))) setStatus('浏览器未允许本地保存,仍可下载当前 HTML。');
|
||||
}
|
||||
function setEditing(enabled) {
|
||||
body.classList.toggle('editing', enabled);
|
||||
textEls.forEach(textEl => {
|
||||
textEl.contentEditable = enabled ? 'true' : 'false';
|
||||
textEl.parentElement.classList.toggle('editing', enabled);
|
||||
textEl.spellcheck = false;
|
||||
});
|
||||
editToggle.textContent = enabled ? '完成编辑' : '编辑文字';
|
||||
editToggle.classList.toggle('primary', !enabled);
|
||||
setStatus(enabled ? '编辑模式:点击任意节点文字进行修改,修改会自动保存。' : '当前为只读模式。点击“编辑文字”后可修改节点内容。');
|
||||
if (enabled && textEls[0]) textEls[0].focus();
|
||||
}
|
||||
function loadSaved() {
|
||||
try {
|
||||
const saved = JSON.parse(safeStorage.get() || '{}');
|
||||
applyTexts(saved && typeof saved === 'object' ? saved : {});
|
||||
} catch { safeStorage.remove(); }
|
||||
}
|
||||
function resetFlow() {
|
||||
if (!window.confirm('确定恢复流程图默认文字吗?')) return;
|
||||
safeStorage.remove();
|
||||
applyTexts(defaults);
|
||||
setEditing(false);
|
||||
setStatus('已恢复默认文字。');
|
||||
}
|
||||
function downloadHtml() {
|
||||
const exportedTexts = currentTexts();
|
||||
const clone = document.documentElement.cloneNode(true);
|
||||
const cloneBody = clone.querySelector('body');
|
||||
cloneBody.classList.remove('editing');
|
||||
cloneBody.dataset.storageKey = 'fee-flow-node-texts-export-' + Date.now();
|
||||
clone.querySelectorAll('[data-node-id]').forEach(node => {
|
||||
const textEl = node.querySelector('.node-text');
|
||||
node.dataset.defaultText = exportedTexts[node.dataset.nodeId];
|
||||
textEl.textContent = exportedTexts[node.dataset.nodeId];
|
||||
textEl.setAttribute('contenteditable', 'false');
|
||||
textEl.removeAttribute('spellcheck');
|
||||
node.classList.remove('editing');
|
||||
});
|
||||
const blob = new Blob(['<!doctype html>\n' + clone.outerHTML], { type: 'text/html;charset=utf-8' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = '费用流转流程图-已编辑.html';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(link.href), 0);
|
||||
setStatus('已下载当前编辑版本 HTML。');
|
||||
}
|
||||
|
||||
loadSaved();
|
||||
setEditing(false);
|
||||
editToggle.addEventListener('click', () => setEditing(!body.classList.contains('editing')));
|
||||
resetButton.addEventListener('click', resetFlow);
|
||||
downloadButton.addEventListener('click', downloadHtml);
|
||||
printButton.addEventListener('click', () => window.print());
|
||||
textEls.forEach(textEl => {
|
||||
textEl.addEventListener('input', persist);
|
||||
textEl.addEventListener('paste', event => {
|
||||
event.preventDefault();
|
||||
document.execCommand('insertText', false, event.clipboardData.getData('text/plain'));
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user