Greasy Fork

来自缓存

Greasy Fork is available in English.

倒运计划智能填表助手

优化冒号与Tab键混合解析逻辑,新增T15格式过磅点精准提取功能

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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;
    }

})();