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

890 lines
30 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: 600px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
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;
}
</style>
</head>
<body>
<body>
<div class="container">
<h1>盘点/随行工具</h1>
<div class="form-group">
<label for="countInput">个数</label>
<input type="number" id="countInput" value="21" min="1">
</div>
<div class="form-group">
<label for="specInput">品规</label>
<input type="text" id="specInput" value="0143">
</div>
<div class="switch-container">
<span class="switch-label">循环模式</span>
<label class="switch">
<input type="checkbox" id="loopToggle">
<span class="slider"></span>
</label>
</div>
<div class="button-group">
<button id="btnCheck" class="btn-check">盘点</button>
<button id="btnFollow" class="btn-follow">随行</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>
<script>
const btnCheck = document.getElementById('btnCheck');
const btnFollow = document.getElementById('btnFollow');
const loopToggle = document.getElementById('loopToggle');
const countInput = document.getElementById('countInput');
const specInput = document.getElementById('specInput');
const status = document.getElementById('status');
const progressBar = document.getElementById('progressBar');
const logContainer = document.getElementById('logContainer');
let isExecuting = false;
let shouldStop = false;
let currentTaskId = ''; // 当前任务ID同一轮内保持一致
// 生成新的 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}`;
}
// 写死的接口列表
const checkList = [
{
name: '货物放回',
url: 'http://192.168.100.99:8083/extend',
method: 'GET',
timeout: 0
},
{
name: '品规盘点',
url: 'http://127.0.0.1:48080/admin-api/logistics/StockController/check',
method: 'POST',
params: {
srmNumber: '001',
cmdName: 'E1',
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: 'UR202606',
wmsTrayCode: 'UR202606',
wmsCategory: '',
wmsCount: '',
wmsItemCode: '{spec}',
wmsShelfCode: '',
wmsPltCode: '',
wmsCountNumber: '{count}',
batchNumber: '',
extendInfo: null
},
timeout: 0
},
{
name: '货物拉出',
url: 'http://192.168.100.99:8083/retract',
method: 'GET',
timeout: 0
},
{
name: '个数盘点',
url: 'http://127.0.0.1:48080/admin-api/logistics/StockController/check',
method: 'POST',
params: {
srmNumber: '001',
cmdName: 'E2',
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: '',
extendInfo: null
},
timeout: 0
},
{
name: '货物放回',
url: 'http://192.168.100.99:8083/retract',
method: 'GET',
timeout: 0
}
];
const followList = [
{
name: '货物放回',
url: 'http://192.168.100.99:8083/retract',
method: 'GET',
timeout: 0
},
{
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: '',
extendInfo: null
},
timeout: 0
},
{
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: '',
extendInfo: null
},
timeout: 0
},
{
name: '货物拉出',
url: 'http://192.168.100.99:8083/extend',
method: 'GET',
timeout: 0
},
{
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: '',
extendInfo: null
},
timeout: 0
},
{
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: '',
extendInfo: null
},
timeout: 0
},
{
name: '货物放回',
url: 'http://192.168.100.99:8083/retract',
method: 'GET',
timeout: 0
}
,
{
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: '',
extendInfo: null
},
timeout: 0
},
{
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: '',
extendInfo: null
},
timeout: 0
},
];
// 格式化时间
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 = '';
}
// 替换参数中的占位符
function replaceParams(params, count, spec, taskId) {
if (!params) return {};
const result = {};
for (const key in params) {
const value = params[key];
if (typeof value === 'string') {
result[key] = value
.replace('{count}', count)
.replace('{spec}', spec)
.replace('{taskId}', taskId);
} else {
result[key] = value;
}
}
return result;
}
// 执行单个请求
async function executeRequest(item, taskId) {
let url = item.url;
const name = item.name;
const count = parseInt(countInput.value) || 21;
const spec = specInput.value || '0143';
const method = item.method || 'POST';
const baseParams = item.params || {};
const timeout = item.timeout || 0; // 0 表示不超时
// 替换参数中的占位符(包括 taskId
const params = replaceParams(baseParams, count, spec, taskId);
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) {
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);
// 每个请求完成后延时 0.5 秒
await new Promise(resolve => setTimeout(resolve, 500));
}
}
// 主执行函数
async function execute(type) {
const list = type === 'check' ? checkList : followList;
const isLoop = loopToggle.checked;
clearLog();
isExecuting = true;
shouldStop = false;
btnCheck.disabled = true;
btnFollow.disabled = true;
const activeBtn = type === 'check' ? btnCheck : btnFollow;
activeBtn.textContent = '执行中...';
let loopCount = 0;
try {
do {
loopCount++;
// 每一轮生成新的 taskId
currentTaskId = generateTaskId();
addLog(`<span class="log-time">[${formatTime(new Date())}]</span> ========== 第 ${loopCount} ${type === 'check' ? '盘点' : '随行'} ========== (taskId: ${currentTaskId})`);
await executeList(list, type, currentTaskId);
if (shouldStop) {
addLog(`<span class="log-time">[${formatTime(new Date())}]</span> ========== 用户停止 ==========`);
break;
}
if (isLoop) {
// 循环模式下,等待一小段时间后继续
await new Promise(resolve => setTimeout(resolve, 500));
}
} while (isLoop);
const modeText = isLoop ? ` (循环 ${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;
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> ========== 循环已关闭,将在当前循环结束后停止 ==========`);
}
});
</script>
</body>
</html>