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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!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>