// ==UserScript== // @name 安全事件管理 - 新增表单批量填充 (v11) // @namespace http://tampermonkey.net/ // @version 11.0 // @description v8 精确基础上,仅把固定 sleep 换成快速轮询 // @match *://*/incidentDispose/* // @grant none // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const FIELD_ALIAS = { '事件标题': '事件标题', '事件描述': '事件描述', '风险级别': '风险级别', '是否攻击成功': '是否攻击成功', '攻击类型': '攻击类型', '攻击协议': '攻击协议', '告警时间': '告警时间', '告警频次': '告警频次', '源IP类型': '源IP类型', '源国家': '源国家', '源省份': '源省份', '源IP端口': '源IP端口', '目的国家': '目的国家', '目的省份': '目的省份', '目的IP端口': '目的IP端口', '需处置IP类型': '需处置IP类型', '需处置ip': '需处置ip', '是否重保工单': '是否重保工单', '监测设备': '监测设备', '自动拦截': '自动拦截', '载荷': '载荷', }; const log = (...a) => console.log('%c[批量填充]', 'color:#409EFF;font-weight:bold', ...a); const warn = (...a) => console.warn('%c[批量填充]', 'color:#E6A23C;font-weight:bold', ...a); const err = (...a) => console.error('%c[批量填充]', 'color:#F56C6C;font-weight:bold', ...a); const sleep = ms => new Promise(r => setTimeout(r, ms)); const ALL_WS = /[\s\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF\u200B\u200C\u200D]+/g; const EDGE_WS = /^[\s\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF\u200B\u200C\u200D]+|[\s\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF\u200B\u200C\u200D]+$/g; const aggressiveTrim = s => String(s == null ? '' : s).replace(EDGE_WS, ''); const normLabel = s => String(s == null ? '' : s).replace(ALL_WS, '').replace(/[::*]/g, '').toLowerCase(); function parseInput(text) { const r = {}; for (const raw of String(text).split(/\r?\n/)) { const line = aggressiveTrim(raw); if (!line) continue; const m = line.match(/^([^::]+)[::]([\s\S]*)$/); if (!m) continue; const key = aggressiveTrim(m[1]); const value = aggressiveTrim(m[2]); if (key && value) r[key] = value; } return r; } function setNativeValue(el, value) { const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; const setter = Object.getOwnPropertyDescriptor(proto, 'value').set; setter.call(el, value); } function fireEvent(el, type) { const evt = document.createEvent('HTMLEvents'); evt.initEvent(type, true, true); el.dispatchEvent(evt); } function getVueInstance(el) { if (!el) return null; if (el.__vue__) return el.__vue__; if (el.__vueParentComponent) return el.__vueParentComponent; let p = el.parentElement; while (p) { if (p.__vue__) return p.__vue__; if (p.__vueParentComponent) return p.__vueParentComponent; p = p.parentElement; } return null; } function findFormItemByLabel(dialogEl, labelText) { const target = normLabel(labelText); const labels = dialogEl.querySelectorAll('.el-form-item__label'); for (const lb of labels) if (normLabel(lb.textContent) === target) return lb.closest('.el-form-item'); for (const lb of labels) { const t = normLabel(lb.textContent); if (t.includes(target) || target.includes(t)) return lb.closest('.el-form-item'); } const hard = s => String(s||'').replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, ''); const targetH = hard(target); for (const lb of labels) if (hard(lb.textContent) === targetH) return lb.closest('.el-form-item'); return null; } // ============ Vue 通道 ============ function fillSelectByVue(formItem, value) { const selectWrap = formItem.querySelector('.el-select'); if (!selectWrap) return { ok: false, reason: 'no-el-select' }; const vm = getVueInstance(selectWrap); if (!vm) return { ok: false, reason: 'no-vue' }; const opts = vm.options || vm.$props?.options || []; if (!Array.isArray(opts) || !opts.length) return { ok: false, reason: 'no-options' }; const target = normLabel(value); let found = opts.find(o => normLabel(o.label) === target); if (!found) found = opts.find(o => normLabel(o.value) === target); if (!found) found = opts.find(o => normLabel(o.label).includes(target)); if (!found) return { ok: false, reason: 'no-option', options: opts.map(o => o.label) }; try { vm.$emit('input', found.value); vm.$emit('change', found.value); if (vm.selectedLabel !== undefined) vm.selectedLabel = found.label; if (vm.$forceUpdate) vm.$forceUpdate(); } catch (e) { return { ok: false, reason: 'vue-exception', error: e }; } return { ok: true }; } // ============ 点击通道(v8 逻辑 + 快速轮询) ============ async function fillSelectByClick(formItem, value) { const selectWrap = formItem.querySelector('.el-select'); if (!selectWrap) return { ok: false, reason: 'no-el-select' }; const triggerInput = selectWrap.querySelector('.el-input__inner') || selectWrap.querySelector('input'); if (!triggerInput) return { ok: false, reason: 'no-input' }; // v8:关掉其它下拉 document.querySelectorAll('.el-select .el-input__inner').forEach(inp => { if (inp !== triggerInput) try { inp.blur(); } catch (e) {} }); document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); await sleep(30); // v8 是 80,微缩 let dropdownId = triggerInput.getAttribute('aria-controls'); selectWrap.scrollIntoView({ block: 'nearest' }); try { triggerInput.click(); } catch (e) {} triggerInput.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window })); triggerInput.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window })); // ---- 加速 1:把 15×60ms 死循环,换成条件一满足即返回 ---- let dropdown = null; { const start = Date.now(); while (Date.now() - start < 700) { dropdownId = triggerInput.getAttribute('aria-controls') || dropdownId; if (dropdownId) { const el = document.getElementById(dropdownId); if (el) { const s = getComputedStyle(el); if (s.display !== 'none' && s.visibility !== 'hidden') { if (el.querySelectorAll('li.el-select-dropdown__item').length > 0) { dropdown = el; break; } } } } await sleep(20); } } // 兜底 if (!dropdown) { const start = Date.now(); while (Date.now() - start < 500) { const vis = [...document.querySelectorAll('.el-select-dropdown')].filter(d => { const s = getComputedStyle(d); if (s.display === 'none' || s.visibility === 'hidden') return false; const p = d.parentElement; if (p && getComputedStyle(p).display === 'none') return false; return d.querySelectorAll('li.el-select-dropdown__item').length > 0; }); if (vis.length) { dropdown = vis[0]; break; } await sleep(20); } } if (!dropdown) return { ok: false, reason: 'dropdown-not-open' }; const items = [...dropdown.querySelectorAll('li.el-select-dropdown__item:not(.is-disabled)')]; if (!items.length) return { ok: false, reason: 'no-items' }; const target = normLabel(value); const candidates = items.map(li => li.textContent.trim()); log(' [Click] 目标:', value, '候选:', candidates.map(c => JSON.stringify(c))); let li = items.find(x => normLabel(x.textContent) === target); if (!li) li = items.find(x => normLabel(x.textContent).includes(target)); if (!li) li = items.find(x => target.includes(normLabel(x.textContent))); if (!li) { try { document.body.click(); } catch (e) {} return { ok: false, reason: 'no-match', candidates }; } try { li.click(); } catch (e) { warn(' li.click 失败', e); } li.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); li.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); li.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window })); li.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window })); // ---- 加速 2:把 sleep(120) 换成轮询 value 非空 ---- { const start = Date.now(); while (Date.now() - start < 300) { if (triggerInput.value && triggerInput.value.length) break; await sleep(15); } } const shown = triggerInput.value; log(' [Click] 填后 input.value =', JSON.stringify(shown)); if (shown && normLabel(shown) === target) return { ok: true }; if (shown && shown.length) return { ok: true, note: 'value-not-empty' }; return { ok: false, reason: 'clicked-but-empty' }; } // ============ 单字段(与 v8 完全一致) ============ async function fillField(formItem, value) { const hasSelect = !!formItem.querySelector('.el-select'); const hasDate = !!formItem.querySelector('.el-date-editor'); if (hasSelect) { const rv = fillSelectByVue(formItem, value); log(' Vue 结果:', rv); if (rv.ok) return true; const rc = await fillSelectByClick(formItem, value); log(' Click 结果:', rc); if (rc.ok) return true; if (rv.options) warn(' Vue 选项:', rv.options); if (rc.candidates) warn(' DOM 选项:', rc.candidates); return false; } if (hasDate) { const dateWrap = formItem.querySelector('.el-date-editor'); const vm = getVueInstance(dateWrap); if (vm) { const name = vm.$options.name || ''; if (name === 'ElDatePicker' || vm.$props?.type) { const d = new Date(String(value).replace(/-/g, '/')); if (!isNaN(d.getTime())) { try { vm.$emit('input', d); vm.$emit('change', d); if (vm.$forceUpdate) vm.$forceUpdate(); return true; } catch (e) {} } } } const input = formItem.querySelector('input.el-input__inner'); if (!input) return false; input.focus(); setNativeValue(input, ''); fireEvent(input, 'input'); setNativeValue(input, value); fireEvent(input, 'input'); fireEvent(input, 'change'); input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true })); input.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true })); return true; } const input = formItem.querySelector('textarea.el-textarea__inner, input.el-input__inner:not([readonly])'); if (!input) return false; input.focus(); setNativeValue(input, ''); fireEvent(input, 'input'); setNativeValue(input, value); fireEvent(input, 'input'); fireEvent(input, 'change'); input.blur(); return true; } function injectButton(dialogEl) { const titleEl = dialogEl.querySelector('.el-dialog__title'); if (!titleEl || titleEl.parentElement.querySelector('.batch-fill-btn')) return; const btn = document.createElement('button'); btn.textContent = '批量填充'; btn.className = 'batch-fill-btn'; btn.type = 'button'; btn.style.cssText = `margin-left:15px;padding:4px 12px;font-size:12px; color:#fff;background:#409EFF;border:none;border-radius:4px; cursor:pointer;vertical-align:middle;`; btn.onmouseover = () => btn.style.background = '#66b1ff'; btn.onmouseout = () => btn.style.background = '#409EFF'; btn.onclick = e => { e.stopPropagation(); e.preventDefault(); openFillDialog(dialogEl); }; titleEl.parentElement.insertBefore(btn, titleEl.nextSibling); log('按钮已注入'); } function openFillDialog(dialogEl) { log('====== 弹窗内所有 label ======'); dialogEl.querySelectorAll('.el-form-item__label').forEach((lb, i) => { log(` #${i}`, JSON.stringify(lb.textContent), ' → norm:', JSON.stringify(normLabel(lb.textContent))); }); log('==============================='); const overlay = document.createElement('div'); overlay.style.cssText = `position:fixed;inset:0;background:rgba(0,0,0,.5); z-index:99999;display:flex;align-items:center;justify-content:center;`; const box = document.createElement('div'); box.style.cssText = `background:#fff;border-radius:8px;padding:20px;width:640px; max-height:80vh;display:flex;flex-direction:column;font-size:14px; box-shadow:0 4px 20px rgba(0,0,0,.3);`; box.innerHTML = `
字段名: 值