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

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