Greasy Fork is available in English.
优化冒号与Tab键混合解析逻辑,新增T15格式过磅点精准提取功能
// ==UserScript==
// @name 倒运计划智能填表助手
// @namespace http://tampermonkey.net/
// @version 5.1
// @description 优化冒号与Tab键混合解析逻辑,新增T15格式过磅点精准提取功能
// @author XIAOTIAN
// @match <all_urls>
// @include *
// @grant GM_setClipboard
// ==/UserScript==
(function() {
'use strict';
// ================= V5.0 全新渐变悬浮按钮 UI =================
let btn = document.createElement('button');
btn.innerHTML = '🤖<br>智能<br>填表';
btn.style.cssText = `
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
position: fixed;
bottom: 100px;
right: 30px;
z-index: 9999;
width: 70px;
height: 70px;
background: linear-gradient(to bottom, #D9D6F2, #6873E0);
color: #ffffff;
border: 4px solid #ffffff;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.25);
text-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25);
font-size: 13px;
font-weight: bold;
line-height: 1.2;
box-sizing: border-box;
transition: transform 0.2s ease;
user-select: none;
`;
const originalTransition = btn.style.transition;
btn.onmouseover = () => btn.style.transform = 'scale(1.08)';
btn.onmouseout = () => btn.style.transform = 'scale(1)';
document.body.appendChild(btn);
// ================= 弹窗显示/隐藏控制 =================
setInterval(() => {
if (window.location.href.includes('#/dump-dumparchives')) {
if (btn.style.display === 'none') btn.style.display = 'flex';
} else {
if (btn.style.display !== 'none') btn.style.display = 'none';
}
}, 500);
// ================= 拖拽核心逻辑 =================
let isDragging = false;
let isMoved = false;
let startX, startY, initialLeft, initialTop;
btn.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
function dragStart(e) {
if (e.button !== 0) return;
isDragging = true;
isMoved = false;
startX = e.clientX;
startY = e.clientY;
const rect = btn.getBoundingClientRect();
initialLeft = rect.left;
initialTop = rect.top;
btn.style.right = 'auto';
btn.style.bottom = 'auto';
btn.style.left = initialLeft + 'px';
btn.style.top = initialTop + 'px';
btn.style.transition = 'none';
}
function drag(e) {
if (!isDragging) return;
let dx = e.clientX - startX;
let dy = e.clientY - startY;
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
isMoved = true;
}
let newLeft = initialLeft + dx;
let newTop = initialTop + dy;
const maxX = window.innerWidth - btn.offsetWidth;
const maxY = window.innerHeight - btn.offsetHeight;
newLeft = Math.max(0, Math.min(newLeft, maxX));
newTop = Math.max(0, Math.min(newTop, maxY));
btn.style.left = newLeft + 'px';
btn.style.top = newTop + 'px';
}
function dragEnd() {
if (!isDragging) return;
isDragging = false;
btn.style.transition = originalTransition;
}
// ================= 精准获取当前肉眼可见的弹窗 =================
function getActiveDialog(targetAriaLabel) {
let wrappers = Array.from(document.querySelectorAll('.el-dialog__wrapper')).filter(el => {
let style = window.getComputedStyle(el);
return style.display !== 'none' && el.getBoundingClientRect().width > 0;
});
if (targetAriaLabel) {
let targetWrapper = wrappers.find(w => {
let dialog = w.querySelector('.el-dialog');
return dialog && dialog.getAttribute('aria-label') === targetAriaLabel;
});
return targetWrapper || null;
}
return wrappers.length > 0 ? wrappers[wrappers.length - 1] : null;
}
// ================= 主执行逻辑 =================
btn.addEventListener('click', async (e) => {
if (isMoved) {
e.preventDefault();
e.stopPropagation();
return;
}
try {
const rawText = await navigator.clipboard.readText();
if (!rawText) return alert("剪贴板为空!");
const dataObj = parseClipboardText(rawText);
const cleanStr = (str) => (str || '').replace(/\s+/g, '');
const getVal = (keyword) => {
let key = Object.keys(dataObj).find(k => k.includes(keyword));
return key ? dataObj[key] : null;
};
let mainDialog = getActiveDialog('新增倒运计划');
if (!mainDialog) return alert("未找到可见的'新增倒运计划'主弹窗!");
// --- 第一阶段:基础信息 ---
let tabs = Array.from(mainDialog.querySelectorAll('.el-tabs__item'));
let tab1 = tabs.find(tab => tab.innerText.replace(/\s+/g, '').includes('倒运计划基础信息'));
if (tab1 && !tab1.classList.contains('is-active')) {
tab1.click();
await new Promise(r => setTimeout(r, 400));
}
// 发货库房
let sendLocation = cleanStr(getVal('装车地点'));
let sendUnit = cleanStr(getVal('发货单位'));
if (sendLocation) {
let isMatched = false;
if (sendUnit) {
isMatched = await fillSelectByLabel(mainDialog, '发货库房', [sendLocation, sendUnit]);
}
if (!isMatched) {
await fillSelectByLabel(mainDialog, '发货库房', [sendLocation]);
}
}
// 收货库房
let receiveLocation = cleanStr(getVal('卸车地点'));
let receiveUnit = cleanStr(getVal('收货单位'));
if (receiveLocation) {
let isMatched = false;
if (receiveUnit) {
isMatched = await fillSelectByLabel(mainDialog, '收货库房', [receiveLocation, receiveUnit]);
}
if (!isMatched) {
await fillSelectByLabel(mainDialog, '收货库房', [receiveLocation]);
}
}
// 业务类型双重降级匹配
let businessType = getVal('转运类型') || getVal('业务类型') || getVal('运输类型') || getVal('业务类别');
if (businessType) {
let cnMatch = businessType.match(/[\u4e00-\u9fa5]+/g);
let targetBt = cnMatch ? cnMatch.join('') : cleanStr(businessType);
let isMatched = await fillSelectByLabel(mainDialog, '业务类型', cleanStr(businessType));
if (!isMatched && cnMatch) {
console.log(`⚠️ 精确业务类型未找到,降级为中文匹配: [${targetBt}]`);
await fillSelectByLabel(mainDialog, '业务类型', targetBt);
}
}
// 绝对提纯物料名称
let materialName = getVal('物资名称') || getVal('物料名称') || getVal('货物名称') || getVal('物品名称');
let cleanMaterial = cleanStr(materialName);
let isNickelOre = cleanMaterial === '镍矿';
let wbNum = null;
// 【V5.1 核心升级:T格式提取器】
let weightPlace = getVal('过磅地点');
if (weightPlace) {
let rawPlace = cleanStr(weightPlace);
// 先尝试找T后面的数字(如T15),找不到再找普通数字(如15)
let numMatch = rawPlace.match(/T(\d+)/i) || rawPlace.match(/\d+/);
if (numMatch) {
// 如果是正则 /T(\d+)/ 匹配到的,数字在索引 1;如果是 /\d+/ 匹配到的,数字在 0
let extractedNum = numMatch[1] ? numMatch[1] : numMatch[0];
wbNum = parseInt(extractedNum, 10);
await fillSelectByLabel(mainDialog, '磅号', extractedNum + '#磅');
} else {
await fillSelectByLabel(mainDialog, '磅号', rawPlace);
}
}
// 联动逻辑:控制开关
let turnOnWBN = false;
let turnOnPlan = false;
if (isNickelOre && wbNum !== null) {
if ([6, 7, 8, 10, 11, 12, 13, 17].includes(wbNum)) {
turnOnWBN = true;
} else if ([14, 15, 16, 18, 19].includes(wbNum)) {
turnOnPlan = true;
}
}
await toggleSwitchByLabel(mainDialog, '产线WBN镍矿', turnOnWBN);
await toggleSwitchByLabel(mainDialog, '地磅计划所属', turnOnPlan);
let remarkValue = getVal('批次') || getVal('矿名') || getVal('矿号') || getVal('物资名称') || getVal('物料名称');
if (remarkValue) fillInputByLabel(mainDialog, '备注', remarkValue.trim());
let loadPoint = getVal('装点编号');
if (loadPoint) fillInputByLabel(mainDialog, '装点编号', cleanStr(loadPoint));
let contract = getVal('合同号');
if (contract) fillInputByLabel(mainDialog, '合同号', cleanStr(contract));
// 自动选择物料
if (cleanMaterial) {
await handleMaterialSelection(mainDialog, cleanMaterial);
}
// --- 第二阶段:车辆录入与统计 ---
let vehicleKeys = Object.keys(dataObj).filter(k =>
k.includes('车号') ||
k.includes('车牌') ||
k.includes('车辆') ||
k.includes('车量') ||
k.includes('后八轮')
);
let vehicleRawStr = vehicleKeys.map(k => dataObj[k]).join(' ');
let totalVehicles = 0;
let successCount = 0;
let failedVehicles = [];
if (vehicleRawStr) {
let vehicles = vehicleRawStr.split(/[,,\s、\-\/。.]+/)
.filter(v => !/^\d+[台辆车部]?$/.test(v.trim()))
.map(v => v.replace(/[^a-zA-Z0-9]/g, '').toUpperCase())
.filter(v => v !== '');
totalVehicles = vehicles.length;
if (totalVehicles > 0) {
let currentTabs = Array.from(mainDialog.querySelectorAll('.el-tabs__item'));
let tab2 = currentTabs.find(tab => tab.innerText.replace(/\s+/g, '').includes('倒运车辆列表'));
if (tab2) {
tab2.click();
await new Promise(r => setTimeout(r, 600));
for (let vehicle of vehicles) {
let isSuccess = await handleVehicleSelection(mainDialog, vehicle);
if (isSuccess) {
successCount++;
} else {
failedVehicles.push(vehicle);
}
}
}
}
}
// ================= 战报弹窗 UI =================
let msg = document.createElement('div');
let reportHtml = `<div style="font-size: 16px; margin-bottom: 8px;"><b>${failedVehicles.length > 0 ? '⚠️ 填表完成,但有异常' : '✅ 填表与录入完美完成'}</b></div>`;
if (totalVehicles > 0) {
reportHtml += `<hr style="margin:8px 0; border:0; border-top:1px solid rgba(0,0,0,0.1);">`;
reportHtml += `<div style="margin-bottom: 4px;">🚗 剪贴板识别总计:<b>${totalVehicles}</b> 辆</div>`;
reportHtml += `<div style="margin-bottom: 4px;">✅ 实际成功录入:<b style="color:#67c23a; font-size:16px;">${successCount}</b> 辆</div>`;
if (failedVehicles.length > 0) {
reportHtml += `<div style="margin-top: 6px;">❌ 异常车号拦截:<b style="color:#f56c6c; font-size:16px;">${failedVehicles.length}</b> 辆</div>`;
reportHtml += `<div style="font-size:13px; color:#e6a23c; margin-top: 4px; word-break: break-all;"><b>未通过比对:</b>${failedVehicles.join(', ')}</div>`;
}
}
msg.innerHTML = reportHtml;
if (failedVehicles.length > 0) {
msg.style.cssText = 'position:fixed; top:30px; left:50%; transform:translateX(-50%); background:#fdf6ec; color:#e6a23c; padding:15px 25px; border-radius:8px; z-index:99999; border:1px solid #faecd8; box-shadow: 0 4px 15px rgba(0,0,0,0.15); line-height:1.4; font-size:14px; min-width: 280px;';
document.body.appendChild(msg);
setTimeout(() => msg.remove(), 10000);
} else {
msg.style.cssText = 'position:fixed; top:30px; left:50%; transform:translateX(-50%); background:#f0f9eb; color:#67c23a; padding:15px 25px; border-radius:8px; z-index:99999; border:1px solid #e1f3d8; box-shadow: 0 4px 15px rgba(0,0,0,0.15); line-height:1.4; font-size:14px; min-width: 250px;';
document.body.appendChild(msg);
setTimeout(() => msg.remove(), 4000);
}
} catch (err) {
console.error(err);
alert('执行出错,请按 F12 查看控制台!');
}
});
// --- 【V5.1 满级修复:冒号绝对优先防误切】 ---
function parseClipboardText(rawText) {
let cleanText = rawText.replace(/(^["'\s]+)|(["'\s]+$)/g, '');
let data = {};
let lines = cleanText.split(/\r?\n/);
let currentKey = null;
for (let line of lines) {
let splitIndex = -1;
let matchLen = 0;
// 第一步防线:优先寻找任何形式的冒号
let colonMatch = line.match(/[::∶]/);
if (colonMatch && line.indexOf(colonMatch[0]) > 0) {
splitIndex = line.indexOf(colonMatch[0]);
matchLen = colonMatch[0].length;
} else {
// 如果实在没冒号,再退而求其次找 Tab 分割
let tabMatch = line.match(/\t+/);
if (tabMatch && line.indexOf(tabMatch[0]) > 0) {
splitIndex = line.indexOf(tabMatch[0]);
matchLen = tabMatch[0].length;
}
}
if (splitIndex > 0) {
// 此时截取出的 key 虽然包含被废弃的 Tab,但紧接着的 replace(/\s+/g, '') 会把它当成空气抹去
let key = line.substring(0, splitIndex).replace(/\s+/g, '').trim();
let val = line.substring(splitIndex + matchLen).trim();
currentKey = key;
data[currentKey] = val;
} else if (currentKey && line.trim() !== '') {
data[currentKey] += ' ' + line.trim();
}
}
return data;
}
function getInputElementByLabel(dialogScope, labelText) {
if (!dialogScope) return null;
const labels = Array.from(dialogScope.querySelectorAll('.el-form-item__label'));
const targetLabel = labels.find(l => l.innerText.replace(/\s+/g, '').includes(labelText));
return targetLabel ? targetLabel.parentElement.querySelector('input') : null;
}
function fillInputByLabel(dialogScope, labelText, value) {
let inputEle = getInputElementByLabel(dialogScope, labelText);
if (inputEle) {
inputEle.value = value;
inputEle.dispatchEvent(new Event('input', { bubbles: true }));
}
}
async function fillSelectByLabel(dialogScope, labelText, targetData) {
let inputEle = getInputElementByLabel(dialogScope, labelText);
if (!inputEle) return false;
inputEle.click();
await new Promise(resolve => setTimeout(resolve, 400));
let dropdownItems = document.querySelectorAll('.el-select-dropdown:not([style*="display: none"]) .el-select-dropdown__item');
let found = false;
let keywords = Array.isArray(targetData) ? targetData : [targetData];
for (let item of dropdownItems) {
let itemText = item.innerText.replace(/\s+/g, '');
let isAllMatch = keywords.every(kw => itemText.includes(kw));
if (isAllMatch) {
item.click();
found = true;
break;
}
}
if (!found) {
inputEle.click();
await new Promise(resolve => setTimeout(resolve, 300));
return false;
}
await new Promise(resolve => setTimeout(resolve, 300));
return true;
}
// ================= 智能开关控制器 =================
async function toggleSwitchByLabel(dialogScope, labelText, turnOn) {
if (!dialogScope) return;
const labels = Array.from(dialogScope.querySelectorAll('.el-form-item__label'));
const targetLabel = labels.find(l => l.innerText.replace(/\s+/g, '').includes(labelText));
if (targetLabel) {
const switchContainer = targetLabel.parentElement.querySelector('.el-switch');
if (switchContainer) {
const isCurrentlyOn = switchContainer.classList.contains('is-checked');
if ((turnOn && !isCurrentlyOn) || (!turnOn && isCurrentlyOn)) {
switchContainer.click();
await new Promise(r => setTimeout(r, 200));
}
}
}
}
// ================= 物料选择专属弹窗 =================
async function handleMaterialSelection(mainDialogScope, targetMaterial) {
if (!targetMaterial) return;
let openBtns = Array.from(mainDialogScope.querySelectorAll('button')).filter(btn => btn.innerText.replace(/\s+/g, '') === '+选择物料' || btn.innerText.replace(/\s+/g, '') === '选择物料');
if (openBtns.length === 0) return;
openBtns[openBtns.length - 1].click();
await new Promise(r => setTimeout(r, 800));
let dialog = getActiveDialog('选择物料');
if (!dialog) return;
let searchInput = dialog.querySelector('input[placeholder="关键字查询"]');
if (searchInput) {
searchInput.value = targetMaterial;
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
}
let buttons = Array.from(dialog.querySelectorAll('button'));
let queryBtn = buttons.find(btn => btn.innerText.replace(/\s+/g, '') === '查询');
if (queryBtn) queryBtn.click();
await new Promise(r => setTimeout(r, 1200));
let rows = dialog.querySelectorAll('.el-table__body-wrapper .el-table__row');
let foundRow = null;
for (let row of rows) {
let cells = Array.from(row.querySelectorAll('.cell'));
if (cells.some(cell => cell.innerText.trim() === targetMaterial)) {
foundRow = row; break;
}
}
if (!foundRow) {
for (let row of rows) {
if (row.innerText.replace(/\s+/g, '').includes(targetMaterial)) {
foundRow = row; break;
}
}
}
if (!foundRow && rows.length > 0) foundRow = rows[0];
if (foundRow) {
let radio = foundRow.querySelector('.el-radio');
if (radio) radio.click();
else foundRow.click();
} else {
let closeBtn = dialog.querySelector('.el-dialog__headerbtn');
if(closeBtn) closeBtn.click();
return;
}
await new Promise(r => setTimeout(r, 400));
let footer = dialog.querySelector('.el-dialog__footer');
if (footer) {
let confirmBtns = Array.from(footer.querySelectorAll('button')).filter(btn => btn.innerText.replace(/\s+/g, '') === '确定选择');
if (confirmBtns.length > 0) confirmBtns[0].click();
}
await new Promise(r => setTimeout(r, 800));
}
// ================= 绝对车牌核对引擎 =================
async function handleVehicleSelection(mainDialogScope, targetVehicle) {
if (!targetVehicle) return false;
let addBtns = Array.from(mainDialogScope.querySelectorAll('button')).filter(btn =>
btn.innerText.replace(/\s+/g, '') === '添加车辆' &&
btn.getBoundingClientRect().width > 0
);
if (addBtns.length === 0) return false;
addBtns[0].click();
await new Promise(r => setTimeout(r, 800));
let dialog = getActiveDialog('选择车辆');
if (!dialog) return false;
let searchInput = dialog.querySelector('input[placeholder="关键字查询"]');
if (searchInput) {
searchInput.value = targetVehicle;
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
}
let queryBtn = Array.from(dialog.querySelectorAll('button')).find(btn => btn.innerText.replace(/\s+/g, '') === '查询');
if (queryBtn) queryBtn.click();
await new Promise(r => setTimeout(r, 1200));
let rows = dialog.querySelectorAll('.el-table__body-wrapper .el-table__row');
let foundRow = null;
for (let row of rows) {
let cells = Array.from(row.querySelectorAll('.cell'));
let isExactMatch = cells.some(cell => cell.innerText.trim().toUpperCase() === targetVehicle.toUpperCase());
if (isExactMatch) {
foundRow = row;
break;
}
}
if (foundRow) {
let checkbox = foundRow.querySelector('.el-checkbox');
if (checkbox) checkbox.click();
else foundRow.click();
} else {
console.warn(`[核对失败]:剪贴板车号为 ${targetVehicle},但系统查出的结果不一致,执行安全跳过。`);
let closeBtn = dialog.querySelector('.el-dialog__headerbtn');
if(closeBtn) closeBtn.click();
await new Promise(r => setTimeout(r, 600));
return false;
}
await new Promise(r => setTimeout(r, 400));
let footer = dialog.querySelector('.el-dialog__footer');
if (footer) {
let confirmBtns = Array.from(footer.querySelectorAll('button')).filter(btn => btn.innerText.replace(/\s+/g, '') === '确定选择');
if (confirmBtns.length > 0) confirmBtns[0].click();
}
await new Promise(r => setTimeout(r, 1000));
return true;
}
})();