You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
yudao-web/inventory-check.html

1276 lines
45 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>盘点/随行工具</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
padding: 40px;
background: #f5f5f5;
min-height: 100vh;
}
.container {
max-width: 900px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
h1 {
text-align: center;
margin-bottom: 25px;
color: #333;
font-size: 24px;
}
.section {
margin-bottom: 25px;
padding: 20px;
background: #fafafa;
border-radius: 8px;
border: 1px solid #e8e8e8;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 15px;
display: flex;
align-items: center;
}
.section-title .icon {
margin-right: 8px;
font-size: 18px;
}
.url-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.url-item {
display: grid;
grid-template-columns: 1fr 1.5fr;
gap: 10px;
}
.url-item input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
outline: none;
}
.url-item input:focus {
border-color: #1890ff;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
}
input {
width: 100%;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 16px;
outline: none;
transition: border-color 0.2s;
}
input:focus {
border-color: #1890ff;
}
.switch-container {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 20px;
padding: 15px;
background: #fff7e6;
border: 1px solid #ffd591;
border-radius: 6px;
}
.switch-label {
font-weight: 500;
color: #333;
}
.switch {
position: relative;
width: 50px;
height: 26px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .3s;
border-radius: 26px;
}
.slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .3s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #1890ff;
}
input:checked + .slider:before {
transform: translateX(24px);
}
.button-group {
display: flex;
gap: 12px;
margin-top: 25px;
}
button {
flex: 1;
padding: 12px 24px;
font-size: 16px;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
font-weight: 500;
}
button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.btn-check {
background: #1890ff;
color: white;
}
.btn-check:hover:not(:disabled) {
background: #40a9ff;
}
.btn-check:disabled {
background: #69c0ff;
}
.btn-follow {
background: #52c41a;
color: white;
}
.btn-follow:hover:not(:disabled) {
background: #73d13d;
}
.btn-follow:disabled {
background: #95de64;
}
.status {
margin-top: 20px;
padding: 12px 16px;
background: #f0f2f5;
border-radius: 6px;
color: #666;
font-size: 14px;
min-height: 40px;
}
.progress {
margin-top: 15px;
height: 6px;
background: #f0f0f0;
border-radius: 3px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: #1890ff;
width: 0%;
transition: width 0.3s;
}
.log-container {
margin-top: 20px;
max-height: 300px;
overflow-y: auto;
background: #1f1f1f;
border-radius: 6px;
padding: 15px;
font-family: 'Courier New', monospace;
font-size: 13px;
}
.log-item {
color: #d4d4d4;
margin-bottom: 4px;
line-height: 1.5;
}
.log-time {
color: #4ec9b0;
}
.log-process {
color: #9cdcfe;
}
.log-start {
color: #4ec9b0;
}
.log-end {
color: #ce9178;
}
.btn-pos {
background: #722ed1;
color: white;
}
.btn-pos:hover:not(:disabled) {
background: #9254de;
}
.btn-pos:disabled {
background: #b37feb;
}
.btn-update-batch {
background: #fa8c16;
color: white;
}
.btn-update-batch:hover:not(:disabled) {
background: #ffa940;
}
.btn-update-batch:disabled {
background: #ffd591;
}
.switch-right {
display: flex;
align-items: center;
gap: 16px;
}
.batch-number {
font-size: 14px;
color: #333;
}
.batch-number b {
color: #fa8c16;
font-family: 'Courier New', monospace;
}
/* 位置设置弹框 */
.pos-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.pos-modal {
background: #fff;
border-radius: 8px;
width: 90%;
max-width: 720px;
max-height: 90vh;
overflow: auto;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
.pos-header {
padding: 15px 20px;
font-size: 18px;
font-weight: 600;
border-bottom: 1px solid #eee;
}
.pos-body {
padding: 20px;
}
.pos-field {
margin-bottom: 15px;
}
.pos-row {
display: flex;
gap: 12px;
}
.pos-row .pos-field {
flex: 1;
margin-bottom: 15px;
}
.pos-field label {
display: block;
margin-bottom: 6px;
color: #555;
}
.pos-field input, .pos-field select {
width: 100%;
padding: 8px 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.pos-grid-tools {
margin: 10px 0;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.pos-grid-tools button {
padding: 6px 14px;
border: none;
background: #1890ff;
color: #fff;
border-radius: 4px;
cursor: pointer;
}
.pos-grid-tools .btn-pos-clear {
background: #fa8c16;
}
.pos-grid-tools .btn-pos-clear:hover {
background: #ffa940;
}
.pos-hint {
color: #999;
font-size: 12px;
}
.grid-wrap {
overflow: auto;
max-height: 360px;
}
.grid-table {
border-collapse: collapse;
}
.grid-table th, .grid-table td {
border: 1px solid #ddd;
text-align: center;
padding: 0;
}
.grid-table .cell {
width: 30px;
height: 30px;
cursor: pointer;
background: #fafafa;
}
.grid-table .cell:hover {
background: #e6f7ff;
}
.grid-table .cell.selected {
background: #1890ff;
}
.grid-table .row-head, .grid-table .col-head {
background: #f0f0f0;
cursor: pointer;
padding: 4px 8px;
font-weight: 500;
user-select: none;
white-space: nowrap;
}
.grid-table .row-head:hover, .grid-table .col-head:hover {
background: #d9d9d9;
}
.grid-table .corner {
background: #f0f0f0;
}
.btn-pos-close {
padding: 8px 20px;
border: none;
background: #52c41a;
color: #fff;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<body>
<div class="container">
<h1>盘点/随行工具</h1>
<div class="form-row">
<div class="form-group">
<label for="countInput">个数</label>
<input type="number" id="countInput" value="13" min="1">
</div>
<div class="form-group">
<label for="specInput">品规</label>
<input type="text" id="specInput" value="0143">
</div>
<div class="form-group">
<label for="trayCodeInput">托盘码</label>
<input type="text" id="trayCodeInput" value="E045EE00020051F2">
</div>
<div class="form-group">
<label for="scanCodeInput">相机扫码</label>
<input type="text" id="scanCodeInput" value="UR202606">
</div>
</div>
<div class="switch-container">
<span class="switch-label">循环模式</span>
<div class="switch-right">
<label class="switch">
<input type="checkbox" id="loopToggle">
<span class="slider"></span>
</label>
<span class="batch-number">批次号:<b id="batchNumberDisplay">-</b></span>
</div>
</div>
<div class="button-group">
<button id="btnCheck" class="btn-check">盘点</button>
<button id="btnFollow" class="btn-follow">随行</button>
<button id="btnPos" class="btn-pos">位置设置</button>
<button id="btnUpdateBatch" class="btn-update-batch">更新批次号</button>
</div>
<div class="status" id="status">准备就绪</div>
<div class="progress">
<div class="progress-bar" id="progressBar"></div>
</div>
<div class="log-container" id="logContainer">
<div class="log-item">等待执行...</div>
</div>
</div>
<!-- 位置设置弹框 -->
<div id="posDialog" class="pos-overlay" style="display: none;">
<div class="pos-modal">
<div class="pos-header">位置设置</div>
<div class="pos-body">
<div class="pos-row">
<div class="pos-field">
<label>巷道号 srmNumber</label>
<input type="text" id="posSrm" value="001">
</div>
<div class="pos-field">
<label>左右</label>
<select id="posSide">
<option value="1">1 - 左</option>
<option value="2">2 - 右</option>
</select>
</div>
<div class="pos-field">
<label>行(默认 7)</label>
<input type="number" id="posRows" value="7" min="1" max="50">
</div>
<div class="pos-field">
<label>列(默认 12)</label>
<input type="number" id="posCols" value="12" min="1" max="50">
</div>
</div>
<div class="pos-grid-tools">
<button type="button" id="posSelectAll">全选</button>
<button type="button" id="posClear" class="btn-pos-clear">清空</button>
<span class="pos-hint">点击单元格切换;点击行/列标题选择整行/整列</span>
</div>
<div id="gridTable" class="grid-wrap"></div>
</div>
<div class="pos-footer">
<button type="button" id="posClose" class="btn-pos-close">关闭</button>
</div>
</div>
</div>
<script>
const btnCheck = document.getElementById('btnCheck');
const btnFollow = document.getElementById('btnFollow');
const btnPos = document.getElementById('btnPos');
const btnUpdateBatch = document.getElementById('btnUpdateBatch');
const batchNumberDisplay = document.getElementById('batchNumberDisplay');
const loopToggle = document.getElementById('loopToggle');
const countInput = document.getElementById('countInput');
const specInput = document.getElementById('specInput');
const trayCodeInput = document.getElementById('trayCodeInput');
const scanCodeInput = document.getElementById('scanCodeInput');
const status = document.getElementById('status');
const progressBar = document.getElementById('progressBar');
const logContainer = document.getElementById('logContainer');
// ===== 位置设置:网格选择(数据持久,关闭弹框后仍保留) =====
let dialogSrm = '001';
let dialogSide = 1;
let gridRows = 7;
let gridCols = 12;
let selectedCells = []; // [{row, col}] 行/列均为 1-based
function cellSelected(r, c) {
return selectedCells.some(x => x.row === r && x.col === c);
}
function toggleCell(r, c) {
const i = selectedCells.findIndex(x => x.row === r && x.col === c);
if (i >= 0) selectedCells.splice(i, 1);
else selectedCells.push({ row: r, col: c });
}
function selectRow(r) {
for (let c = 1; c <= gridCols; c++) {
if (!cellSelected(r, c)) selectedCells.push({ row: r, col: c });
}
}
function selectCol(c) {
for (let r = 1; r <= gridRows; r++) {
if (!cellSelected(r, c)) selectedCells.push({ row: r, col: c });
}
}
function selectAllCells() {
selectedCells = [];
for (let r = 1; r <= gridRows; r++)
for (let c = 1; c <= gridCols; c++) selectedCells.push({ row: r, col: c });
}
function renderGrid() {
const el = document.getElementById('gridTable');
let html = '<table class="grid-table"><thead><tr><th class="corner"></th>';
for (let c = 1; c <= gridCols; c++) {
html += `<th class="col-head" data-col="${c}">${c}</th>`;
}
html += '</tr></thead><tbody>';
for (let r = 1; r <= gridRows; r++) {
html += `<tr><th class="row-head" data-row="${r}">${r}</th>`;
for (let c = 1; c <= gridCols; c++) {
html += `<td class="cell${cellSelected(r, c) ? ' selected' : ''}" data-row="${r}" data-col="${c}"></td>`;
}
html += '</tr>';
}
html += '</tbody></table>';
el.innerHTML = html;
}
function openPosDialog() {
document.getElementById('posDialog').style.display = 'flex';
renderGrid();
}
function closePosDialog() {
document.getElementById('posDialog').style.display = 'none';
}
document.getElementById('posSrm').addEventListener('input', e => { dialogSrm = e.target.value; });
document.getElementById('posSide').addEventListener('change', e => { dialogSide = parseInt(e.target.value) || 1; });
document.getElementById('posRows').addEventListener('input', e => {
gridRows = parseInt(e.target.value) || 7;
selectedCells = selectedCells.filter(x => x.row <= gridRows && x.col <= gridCols);
renderGrid();
});
document.getElementById('posCols').addEventListener('input', e => {
gridCols = parseInt(e.target.value) || 12;
selectedCells = selectedCells.filter(x => x.row <= gridRows && x.col <= gridCols);
renderGrid();
});
document.getElementById('gridTable').addEventListener('click', e => {
const t = e.target;
if (t.classList.contains('cell')) {
toggleCell(parseInt(t.dataset.row), parseInt(t.dataset.col));
renderGrid();
} else if (t.classList.contains('row-head')) {
selectRow(parseInt(t.dataset.row));
renderGrid();
} else if (t.classList.contains('col-head')) {
selectCol(parseInt(t.dataset.col));
renderGrid();
}
});
document.getElementById('posSelectAll').addEventListener('click', () => { selectAllCells(); renderGrid(); });
document.getElementById('posClear').addEventListener('click', () => { selectedCells = []; renderGrid(); });
document.getElementById('btnPos').addEventListener('click', openPosDialog);
document.getElementById('posClose').addEventListener('click', closePosDialog);
let isExecuting = false;
let shouldStop = false;
let currentTaskId = ''; // 当前任务ID,每个选中位置内保持一致
let currentBatchNumber = ''; // 当前批次号,一次执行内保持不变
// 生成新的 taskId(基于时间戳)
function generateTaskId() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const ms = String(now.getMilliseconds()).padStart(3, '0');
// 格式:20260401-143052-123
return `${year}${month}${day}-${hours}${minutes}${seconds}-${ms}`;
}
// 生成批次号(基于时间戳)
function generateBatchNumber() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const ms = String(now.getMilliseconds()).padStart(3, '0');
return `BN${year}${month}${day}${hours}${minutes}${seconds}${ms}`;
}
// 手动更新批次号并刷新显示
function refreshBatchNumber() {
currentBatchNumber = generateBatchNumber();
if (batchNumberDisplay) {
batchNumberDisplay.textContent = currentBatchNumber;
}
return currentBatchNumber;
}
// 写死的接口列表
const checkList = [
{
name: '货物放回',
url: 'http://192.168.1.99:10000/start',
method: 'GET',
timeout: 0,
delay: 10000
},
{
name: '品规盘点',
url: 'http://127.0.0.1:48080/admin-api/logistics/StockController/check',
method: 'POST',
params: {
cmdName: 'E1',
taskId: '{taskId}',
fromColumn: 3,
fromRow: 5,
fromDirection: 2,
fromSide: 2,
srmNumber: '001',
fromSeparation: 1,
lotnum: '{batchNumber}',
wmsExtendInfo: {
code: '{code}',
trayCode: '{trayCode}',
category: '{spec}',
count: '{count}'
}
},
timeout: 0,
delay: 500
},
{
name: '货物拉出',
url: 'http://192.168.1.99:10000/start',
method: 'GET',
timeout: 0,
delay: 10000
},
{
name: '个数盘点',
url: 'http://127.0.0.1:48080/admin-api/logistics/StockController/check',
method: 'POST',
params: {
cmdName: 'E2',
taskId: '{taskId}',
fromColumn: 3,
fromRow: 5,
fromDirection: 2,
fromSide: 2,
srmNumber: '001',
fromSeparation: 1,
lotnum: '{batchNumber}',
wmsExtendInfo: {
code: '{code}',
trayCode: '{trayCode}',
category: '{spec}',
count: '{count}'
}
},
timeout: 0,
delay: 500
},
// {
// name: '货物放回',
// url: 'http://192.168.100.99:8083/retract',
// method: 'GET',
// timeout: 0,
// delay: 500
// }
];
const followList = [
{
name: '货物归位',
url: 'http://192.168.100.99:8083/extend',
method: 'GET',
timeout: 0,
delay: 500
},
{
name: '初始化随行',
url: 'http://127.0.0.1:48080/admin-api/logistics/StockController/action',
method: 'POST',
params: {
srmNumber: '001',
cmdName: 'B1',
taskId: '{taskId}',
fromColumn: '1',
fromRow: '1',
fromDirection: '1',
fromSide: '1',
fromSeparation: 1,
toColumn: '2',
toRow: '2',
toDirection: '2',
toSide: '2',
toSeparation: 1,
warnCode: '',
ackStatus: '',
code: '',
trayCode: '',
category: '', // 品规
count: '',
itemCode: '',
shelfCode: '',
pltCode: '',
countNumber: '', // 个数
wmsCode: '',
wmsTrayCode: '',
wmsCategory: '',
wmsCount: '',
wmsItemCode: '{spec}',
wmsShelfCode: '',
wmsPltCode: '',
wmsCountNumber: '{count}',
batchNumber: '{batchNumber}',
extendInfo: null
},
timeout: 0,
delay: 500
},
{
name: '取货到位',
url: 'http://127.0.0.1:48080/admin-api/logistics/StockController/action',
method: 'POST',
params: {
srmNumber: '001',
cmdName: 'C1',
taskId: '{taskId}',
fromColumn: '1',
fromRow: '1',
fromDirection: '1',
fromSide: '1',
fromSeparation: 1,
toColumn: '2',
toRow: '2',
toDirection: '2',
toSide: '2',
toSeparation: 1,
warnCode: '',
ackStatus: '',
code: '',
trayCode: '',
category: '', // 品规
count: '',
itemCode: '',
shelfCode: '',
pltCode: '',
countNumber: '', // 个数
wmsCode: '',
wmsTrayCode: '',
wmsCategory: '',
wmsCount: '',
wmsItemCode: '{spec}',
wmsShelfCode: '',
wmsPltCode: '',
wmsCountNumber: '{count}',
batchNumber: '{batchNumber}',
extendInfo: null
},
timeout: 0,
delay: 500
},
{
name: '货物拉出',
url: 'http://192.168.100.99:8083/retract',
method: 'GET',
timeout: 0,
delay: 500
},
{
name: '取货完成',
url: 'http://127.0.0.1:48080/admin-api/logistics/StockController/action',
method: 'POST',
params: {
srmNumber: '001',
cmdName: 'C2',
taskId: '{taskId}',
fromColumn: '1',
fromRow: '1',
fromDirection: '1',
fromSide: '1',
fromSeparation: 1,
toColumn: '2',
toRow: '2',
toDirection: '2',
toSide: '2',
toSeparation: 1,
warnCode: '',
ackStatus: '',
code: '',
trayCode: '',
category: '', // 品规
count: '',
itemCode: '',
shelfCode: '',
pltCode: '',
countNumber: '', // 个数
wmsCode: '',
wmsTrayCode: '',
wmsCategory: '',
wmsCount: '',
wmsItemCode: '{spec}',
wmsShelfCode: '',
wmsPltCode: '',
wmsCountNumber: '{count}',
batchNumber: '{batchNumber}',
extendInfo: null
},
timeout: 0,
delay: 500
},
{
name: '送货到位',
url: 'http://127.0.0.1:48080/admin-api/logistics/StockController/action',
method: 'POST',
params: {
srmNumber: '001',
cmdName: 'C3',
taskId: '{taskId}',
fromColumn: '1',
fromRow: '1',
fromDirection: '1',
fromSide: '1',
fromSeparation: 1,
toColumn: '2',
toRow: '2',
toDirection: '2',
toSide: '2',
toSeparation: 1,
warnCode: '',
ackStatus: '',
code: '',
trayCode: '',
category: '', // 品规
count: '',
itemCode: '',
shelfCode: '',
pltCode: '',
countNumber: '', // 个数
wmsCode: '',
wmsTrayCode: '',
wmsCategory: '',
wmsCount: '',
wmsItemCode: '{spec}',
wmsShelfCode: '',
wmsPltCode: '',
wmsCountNumber: '{count}',
batchNumber: '{batchNumber}',
extendInfo: null
},
timeout: 0,
delay: 500
},
{
name: '货物放回',
url: 'http://192.168.100.99:8083/extend',
method: 'GET',
timeout: 0,
delay: 500
}
,
{
name: '送货完成',
url: 'http://127.0.0.1:48080/admin-api/logistics/StockController/action',
method: 'POST',
params: {
srmNumber: '001',
cmdName: 'C4',
taskId: '{taskId}',
fromColumn: '1',
fromRow: '1',
fromDirection: '1',
fromSide: '1',
fromSeparation: 1,
toColumn: '2',
toRow: '2',
toDirection: '2',
toSide: '2',
toSeparation: 1,
warnCode: '',
ackStatus: '',
code: '',
trayCode: '',
category: '', // 品规
count: '',
itemCode: '',
shelfCode: '',
pltCode: '',
countNumber: '', // 个数
wmsCode: '',
wmsTrayCode: '',
wmsCategory: '',
wmsCount: '',
wmsItemCode: '{spec}',
wmsShelfCode: '',
wmsPltCode: '',
wmsCountNumber: '{count}',
batchNumber: '{batchNumber}',
extendInfo: null
},
timeout: 0,
delay: 500
},
{
name: '随行结束',
url: 'http://127.0.0.1:48080/admin-api/logistics/StockController/action',
method: 'POST',
params: {
srmNumber: '001',
cmdName: 'B2',
taskId: '{taskId}',
fromColumn: '1',
fromRow: '1',
fromDirection: '1',
fromSide: '1',
fromSeparation: 1,
toColumn: '2',
toRow: '2',
toDirection: '2',
toSide: '2',
toSeparation: 1,
warnCode: '',
ackStatus: '',
code: '',
trayCode: '',
category: '', // 品规
count: '',
itemCode: '',
shelfCode: '',
pltCode: '',
countNumber: '', // 个数
wmsCode: '',
wmsTrayCode: '',
wmsCategory: '',
wmsCount: '',
wmsItemCode: '{spec}',
wmsShelfCode: '',
wmsPltCode: '',
wmsCountNumber: '{count}',
batchNumber: '{batchNumber}',
extendInfo: null
},
timeout: 0,
delay: 500
},
{
name: '货物放回',
url: 'http://192.168.100.99:8083/retract',
method: 'GET',
timeout: 0,
delay: 500
}
];
// 格式化时间
function formatTime(date) {
return date.toLocaleTimeString('zh-CN', { hour12: false });
}
// 添加日志
function addLog(message) {
const logItem = document.createElement('div');
logItem.className = 'log-item';
logItem.innerHTML = message;
logContainer.appendChild(logItem);
logContainer.scrollTop = logContainer.scrollHeight;
}
// 清空日志
function clearLog() {
logContainer.innerHTML = '';
}
// 替换占位符(支持嵌套对象,如 wmsExtendInfo)
function replaceValue(value, count, spec, code, trayCode, taskId, batchNumber) {
if (typeof value === 'string') {
return value
.replace('{count}', count)
.replace('{spec}', spec)
.replace('{code}', code)
.replace('{trayCode}', trayCode)
.replace('{taskId}', taskId)
.replace('{batchNumber}', batchNumber);
} else if (value && typeof value === 'object') {
const result = {};
for (const k in value) {
result[k] = replaceValue(value[k], count, spec, code, trayCode, taskId, batchNumber);
}
return result;
}
return value;
}
function replaceParams(params, count, spec, code, trayCode, taskId, batchNumber) {
if (!params) return {};
const result = {};
for (const key in params) {
result[key] = replaceValue(params[key], count, spec, code, trayCode, taskId, batchNumber);
}
return result;
}
// 执行单个请求
async function executeRequest(item, taskId, batchNumber, target) {
let url = item.url;
const name = item.name;
const count = parseInt(countInput.value) || 13;
const spec = specInput.value || '0143';
const code = scanCodeInput.value || 'UR202606';
const trayCode = trayCodeInput.value || 'E045EE00020051F2';
const method = item.method || 'POST';
const baseParams = item.params || {};
const timeout = item.timeout || 0; // 0 表示不超时
// 替换参数中的占位符(包括 taskId 和 batchNumber)
const params = replaceParams(baseParams, count, spec, code, trayCode, taskId, batchNumber);
// 按网格选择的位置覆盖 fromColumn/fromRow/fromDirection/fromSide/srmNumber
if (target) {
const mergeKeys = ['fromColumn', 'fromRow', 'fromDirection', 'fromSide', 'srmNumber'];
for (const k of mergeKeys) {
if (target[k] !== undefined && Object.prototype.hasOwnProperty.call(params, k)) {
params[k] = target[k];
}
}
}
const startTime = new Date();
addLog(`<span class="log-time">[${formatTime(startTime)}]</span> <span class="log-process">${name}</span> <span class="log-start">开始</span> [${method}]`);
try {
const fetchOptions = {
method: method,
headers: {
'Content-Type': 'application/json',
}
};
// GET 请求使用 URL 参数
if (method.toUpperCase() === 'GET') {
if (Object.keys(params).length > 0) {
const queryParams = new URLSearchParams(params).toString();
url = `${url}?${queryParams}`;
}
// GET 请求不需要 body,移除 Content-Type
delete fetchOptions.headers['Content-Type'];
} else if (method.toUpperCase() !== 'GET' && Object.keys(params).length > 0) {
// POST/PUT 等使用 body
fetchOptions.body = JSON.stringify({
...params,
timestamp: Date.now()
});
}
// 处理超时
let response;
if (timeout > 0) {
const controller = new AbortController();
fetchOptions.signal = controller.signal;
const timeoutId = setTimeout(() => controller.abort(), timeout);
response = await fetch(url, fetchOptions);
clearTimeout(timeoutId);
} else {
response = await fetch(url, fetchOptions);
}
const endTime = new Date();
const duration = (endTime - startTime) / 1000;
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
// 获取返回内容
let responseText = '';
try {
responseText = await response.text();
} catch (e) {
responseText = '(无法读取返回内容)';
}
addLog(`<span class="log-time">[${formatTime(endTime)}]</span> <span class="log-process">${name}</span> <span class="log-end">结束</span> (耗时: ${duration.toFixed(2)}s) <span style="color: #dcdcaa;">返回: ${responseText}</span>`);
} catch (error) {
const endTime = new Date();
if (error.name === 'AbortError') {
addLog(`<span class="log-time">[${formatTime(endTime)}]</span> <span class="log-process">${name}</span> <span class="log-end">结束 (超时)</span>`);
} else {
addLog(`<span class="log-time">[${formatTime(endTime)}]</span> <span class="log-process">${name}</span> <span class="log-end">结束 (失败: ${error.message})</span>`);
}
}
}
// 执行列表
async function executeList(list, type, taskId, batchNumber, target) {
const totalSteps = list.length;
for (let i = 0; i < totalSteps; i++) {
status.textContent = `${type === 'check' ? '盘点' : '随行'}中: ${i + 1}/${totalSteps} - ${list[i].name}`;
progressBar.style.width = `${((i + 1) / totalSteps) * 100}%`;
await executeRequest(list[i], taskId, batchNumber, target);
// 每个请求完成后延时(取接口自带 delay,未配置则默认 500ms)
const gap = (typeof list[i].delay === 'number') ? list[i].delay : 500;
if (gap > 0) {
addLog(`<span class="log-time">[${formatTime(new Date())}]</span> ⏱ 等待 ${gap}ms 后执行下一步`);
await new Promise(resolve => setTimeout(resolve, gap));
}
}
}
// 主执行函数
async function execute(type) {
const list = type === 'check' ? checkList : followList;
// 根据网格选择构建目标位置列表
let targets;
if (selectedCells.length > 0) {
targets = selectedCells.map(cell => ({
fromColumn: cell.col,
fromRow: cell.row,
fromDirection: dialogSide,
fromSide: dialogSide,
srmNumber: dialogSrm
}));
} else {
// 行/列均为空:单盘点默认参数,且循环模式不可开启
targets = [{
fromColumn: 3,
fromRow: 5,
fromDirection: dialogSide,
fromSide: dialogSide,
srmNumber: dialogSrm
}];
}
// 未选择任何位置时,强制关闭循环(循环模式无法开启)
const loopEnabled = loopToggle.checked && selectedCells.length > 0;
clearLog();
isExecuting = true;
shouldStop = false;
btnCheck.disabled = true;
btnFollow.disabled = true;
btnPos.disabled = true;
const activeBtn = type === 'check' ? btnCheck : btnFollow;
activeBtn.textContent = '执行中...';
let loopCount = 0;
// 批次号 batchNumber:由“更新批次号”按钮手动控制,此处不再自动生成
if (!currentBatchNumber) {
refreshBatchNumber();
}
try {
do {
loopCount++;
addLog(`<span class="log-time">[${formatTime(new Date())}]</span> ========== 第 ${loopCount} ${type === 'check' ? '盘点' : '随行'} ========== (位置数: ${targets.length}, batch: ${currentBatchNumber})`);
for (const target of targets) {
if (shouldStop) break;
// 任务号 taskId:每个选中位置重新生成(该位置的 checkList 内保持一致)
currentTaskId = generateTaskId();
addLog(`<span class="log-time">[${formatTime(new Date())}]</span> ---- 位置 行${target.fromRow}/列${target.fromColumn} (srm:${target.srmNumber}, 左右:${target.fromSide}, taskId: ${currentTaskId}) ----`);
await executeList(list, type, currentTaskId, currentBatchNumber, target);
}
if (shouldStop) {
addLog(`<span class="log-time">[${formatTime(new Date())}]</span> ========== 用户停止 ==========`);
break;
}
if (loopEnabled) {
// 循环模式下,等待一小段时间后继续
await new Promise(resolve => setTimeout(resolve, 500));
}
} while (loopEnabled);
const modeText = loopEnabled ? ` (循环 ${loopCount} 次)` : '';
status.textContent = `${type === 'check' ? '盘点' : '随行'}完成${modeText}!`;
addLog(`<span class="log-time">[${formatTime(new Date())}]</span> ========== 完成${modeText} ==========`);
} catch (error) {
status.textContent = `${type === 'check' ? '盘点' : '随行'}出错: ${error.message}`;
addLog(`<span class="log-time">[${formatTime(new Date())}]</span> ========== 出错: ${error.message} ==========`);
} finally {
isExecuting = false;
btnCheck.disabled = false;
btnFollow.disabled = false;
btnPos.disabled = false;
btnCheck.textContent = '盘点';
btnFollow.textContent = '随行';
// 3秒后重置进度条和状态
setTimeout(() => {
progressBar.style.width = '0%';
status.textContent = '准备就绪';
}, 3000);
}
}
// 监听按钮点击
btnCheck.addEventListener('click', () => {
if (!isExecuting) {
execute('check');
} else if (isExecuting && !shouldStop) {
shouldStop = true;
addLog(`<span class="log-time">[${formatTime(new Date())}]</span> ========== 等待当前循环结束 ==========`);
}
});
btnFollow.addEventListener('click', () => {
if (!isExecuting) {
execute('follow');
} else if (isExecuting && !shouldStop) {
shouldStop = true;
addLog(`<span class="log-time">[${formatTime(new Date())}]</span> ========== 等待当前循环结束 ==========`);
}
});
// 监听循环开关
loopToggle.addEventListener('change', (e) => {
if (e.target.checked && isExecuting) {
addLog(`<span class="log-time">[${formatTime(new Date())}]</span> ========== 循环已开启,将在当前循环结束后继续 ==========`);
} else if (!e.target.checked && isExecuting) {
shouldStop = true;
addLog(`<span class="log-time">[${formatTime(new Date())}]</span> ========== 循环已关闭,将在当前循环结束后停止 ==========`);
}
});
// 更新批次号按钮:手动刷新批次号
btnUpdateBatch.addEventListener('click', () => {
refreshBatchNumber();
addLog(`<span class="log-time">[${formatTime(new Date())}]</span> ========== 批次号已更新: ${currentBatchNumber} ==========`);
});
// 初始化批次号显示
refreshBatchNumber();
</script>
</body>
</html>