// ==UserScript== // @name 需求三点估算自动填写 // @namespace http://tampermonkey.net/ // @version 1.0.6 // @description 自动填写需求三点估算值并提交 // @author You // @match https://devops.chinaunicom.cn/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_setClipboard // @grant GM_xmlhttpRequest // ==/UserScript== // 防止脚本被多次执行 if (window.threePointEstimationScriptLoaded) { console.log('三点估算脚本已加载,跳过执行'); } else { window.threePointEstimationScriptLoaded = true; console.log('三点估算脚本开始执行'); console.log('浏览器信息:', navigator.userAgent); console.log('当前URL:', window.location.href); (function() { 'use strict'; // 已打开过在线表 let win = null; // 定时器和观察者的引用 let interval = null; let observer = null; // 标记是否已经添加了开始估算按钮 let buttonAdded = false; // 设置buttonAdded值并处理资源清理 function setButtonAdded(value) { const oldValue = buttonAdded; buttonAdded = value; // 当buttonAdded从false变为true时,清除定时器和观察者 if (oldValue === false && value === true) { console.log('buttonAdded状态变为true,开始清理资源...'); // 清除定时器 if (interval) { clearInterval(interval); console.log('已清除定期检测定时器'); interval = null; } // 断开DOM观察器 if (observer) { observer.disconnect(); console.log('已断开DOM变化观察器'); observer = null; } } } // 默认配置 - 定义在IIFE顶部,确保所有函数都能访问 const DEFAULT_CONFIG = { floatValue: 2, // 默认浮动值2% autoSubmit: false, // 默认不自动提交 docUrl: 'https://docs.qq.com/sheet/xxxx' // 默认在线文档URL }; // 获取配置 function getConfig() { // 优先使用远程配置 const remoteConfigStr = GM_getValue('remoteConfig', null); if (remoteConfigStr) { try { const remoteConfig = JSON.parse(remoteConfigStr); return { floatValue: remoteConfig.floatValue !== undefined ? remoteConfig.floatValue : GM_getValue('floatValue', DEFAULT_CONFIG.floatValue), autoSubmit: remoteConfig.autoSubmit !== undefined ? remoteConfig.autoSubmit : GM_getValue('autoSubmit', DEFAULT_CONFIG.autoSubmit), docUrl: remoteConfig.docUrl !== undefined ? remoteConfig.docUrl : GM_getValue('docUrl', DEFAULT_CONFIG.docUrl) }; } catch (error) { console.error('解析远程配置失败,使用本地配置:', error); } } // 使用本地配置或默认配置 return { floatValue: GM_getValue('floatValue', DEFAULT_CONFIG.floatValue), autoSubmit: GM_getValue('autoSubmit', DEFAULT_CONFIG.autoSubmit), docUrl: GM_getValue('docUrl', DEFAULT_CONFIG.docUrl) }; } // 保存配置 function saveConfig(config) { GM_setValue('floatValue', config.floatValue); GM_setValue('autoSubmit', config.autoSubmit); GM_setValue('docUrl', config.docUrl); } // 从远程获取配置 async function fetchRemoteConfig() { // 直接在函数内部定义CONFIG_URL,避免变量提升问题 const CONFIG_URL = 'https://app.tg.unicom.local/api/files/config/script/2026-03-09/983bd743e06c2d9c653f0181e5772bb0542918121135c49b1748e6d53d80437f_587cb980af76fdc7e52369fd0b9d926dff266976b6f8ac631e358fecc49ff8cf.json'; // 替换为实际的远程URL // 本地开发测试时可使用相对路径或绝对路径 // const CONFIG_URL = 'config.json'; // 本地开发测试用 return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: CONFIG_URL, responseType: 'json', onload: function(response) { if (response.status === 200 && response.response) { console.log('成功从远程获取配置:', response.response); // 保存配置到GM缓存 GM_setValue('remoteConfig', JSON.stringify(response.response)); GM_setValue('remoteConfigTimestamp', Date.now()); resolve(response.response); } else { console.error('远程配置获取失败,使用本地缓存或默认配置:', response.status); resolve(null); } }, onerror: function(error) { console.error('远程配置请求出错:', error); resolve(null); } }); }); } // 检查并更新配置 async function checkAndUpdateConfig() { const remoteConfig = await fetchRemoteConfig(); if (remoteConfig) { return remoteConfig; } // 使用本地缓存的配置 const cachedConfigStr = GM_getValue('remoteConfig', null); if (cachedConfigStr) { try { const cachedConfig = JSON.parse(cachedConfigStr); console.log('使用本地缓存的配置:', cachedConfig); return cachedConfig; } catch (error) { console.error('解析本地缓存配置失败:', error); GM_setValue('remoteConfig', null); GM_setValue('remoteConfigTimestamp', null); } } return null; } // 显示设置对话框 function showSettingsDialog() { const config = getConfig(); // 获取浮动值配置 const floatValue = prompt('请输入估算填写的浮动值(%):', config.floatValue); if (floatValue === null) return; // 获取自动提交配置,显示当前值 const currentAutoSubmitText = config.autoSubmit ? '是' : '否'; const autoSubmitInput = prompt(`是否自动确定提交?\n当前设置: ${currentAutoSubmitText}\n请输入 是 或 否:`, currentAutoSubmitText); if (autoSubmitInput === null) return; // 获取在线文档URL配置 const docUrl = prompt('请输入在线文档URL:', config.docUrl); if (docUrl === null) return; // 解析自动提交配置 const autoSubmit = autoSubmitInput.trim().toLowerCase() === '是'; // 保存配置 saveConfig({ floatValue: parseFloat(floatValue) || DEFAULT_CONFIG.floatValue, autoSubmit: autoSubmit, docUrl: docUrl.trim() || DEFAULT_CONFIG.docUrl }); // 显示保存结果 const finalFloatValue = parseFloat(floatValue) || DEFAULT_CONFIG.floatValue; alert(`配置已保存!\n\n当前配置:\n浮动值: ${finalFloatValue}%\n自动提交: ${autoSubmit ? '是' : '否'}\n在线文档URL: ${docUrl.trim() || DEFAULT_CONFIG.docUrl}`); } // 显示输入估算值对话框 function showEstimationDialog() { const input = prompt('请输入估算值,格式为:10;2;0\n(多个值用分号分隔,支持中文或英文分号):'); if (input === null) return; // 支持中文分号和英文分号 const estimationValues = input.replace(/;/g, ';').split(';') .map(v => v.trim()) // 去除空格 .filter(v => v !== '') // 过滤空值 .map(v => parseFloat(v) || 0); // 转换为数字,无效值设为0 if (estimationValues.length === 0) { alert('请输入有效的估算值!'); return; } console.log(`解析后的估算值:${estimationValues}`); startEstimation(estimationValues); } // 等待元素出现 function waitForElement(selector, timeout = 5000) { return new Promise((resolve, reject) => { // 立即检查一次 const check = () => { const element = document.querySelector(selector); if (element) { resolve(element); return true; } return false; }; if (check()) return; // 使用requestAnimationFrame代替setInterval,提高性能 const startTime = Date.now(); const timer = setInterval(() => { if (check() || Date.now() - startTime > timeout) { clearInterval(timer); if (Date.now() - startTime > timeout) { reject(new Error(`元素 ${selector} 未找到`)); } } }, 100); }); } // 等待元素消失或不可见 function waitForElementDisappear(selector, timeout = 8000) { return new Promise((resolve) => { // 立即检查一次 const check = () => { const element = document.querySelector(selector); if (!element || !element.offsetParent || window.getComputedStyle(element).display === 'none' || window.getComputedStyle(element).visibility === 'hidden') { // 元素不存在、不可见、或隐藏 return true; } return false; }; if (check()) { resolve(); return; } // 使用requestAnimationFrame代替setInterval,提高性能 const startTime = Date.now(); const timer = setInterval(() => { if (check() || Date.now() - startTime > timeout) { clearInterval(timer); if (Date.now() - startTime > timeout) { console.warn(`元素 ${selector} 在 ${timeout}ms 内未完全消失,但继续执行`); } resolve(); } }, 150); }); } // 计算三点估算值 function calculateThreePointValues(baseValue, floatPercent) { const float = floatPercent / 100; return { optimistic: baseValue * (1 - float), probable: baseValue, pessimistic: baseValue * (1 + float) }; } // 模拟用户输入值 function simulateUserInput(inputElement, value) { if (!inputElement) return; // 确保元素可见 if (!inputElement.offsetParent) { console.warn('输入元素不可见'); return; } // 模拟真实用户输入流程 inputElement.focus(); // 清空现有值(如果有) if (inputElement.value) { inputElement.value = ''; inputElement.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); } // 设置新值 const stringValue = value.toString(); inputElement.setAttribute('value', stringValue); inputElement.value = stringValue; // 触发完整的事件序列 const events = [ new Event('focus', { bubbles: true, cancelable: true }), new Event('input', { bubbles: true, cancelable: true }), new Event('change', { bubbles: true, cancelable: true }), // 对于Ant Design,可能还需要触发compositionend事件 new Event('compositionend', { bubbles: true, cancelable: true }), new Event('blur', { bubbles: true, cancelable: true }) ]; events.forEach(event => { inputElement.dispatchEvent(event); }); console.log(`已填写值 ${value} 到输入框 ${inputElement.id}`); } // 处理单个事项的估算 async function processItem(rowElement, baseValue, config) { console.log(`开始处理事项,基础值:${baseValue}`); // 点击评估按钮 const evaluateButton = rowElement.querySelector('button.ant-btn.ant-btn-primary.ant-btn-sm'); if (!evaluateButton) { console.error('未找到评估按钮'); return; } evaluateButton.click(); console.log('已点击评估按钮'); // 等待模态弹窗出现 await waitForElement('div.ant-modal-content'); console.log('模态弹窗已出现'); // 计算三点估算值 const { optimistic, probable, pessimistic } = calculateThreePointValues(baseValue, config.floatValue); console.log(`计算得到的估算值:乐观=${optimistic},可能=${probable},悲观=${pessimistic}`); // 等待并填写最乐观工作量 const optimisticInput = await waitForElement('#optimisticValue'); simulateUserInput(optimisticInput, optimistic); // 等待并填写最可能工作量 const probableInput = await waitForElement('#probablyValue'); simulateUserInput(probableInput, probable); // 等待并填写最悲观工作量 const pessimisticInput = await waitForElement('#pessimismValue'); simulateUserInput(pessimisticInput, pessimistic); // 等待一小段时间,确保表单验证完成 await new Promise(resolve => setTimeout(resolve, 300)); // 等待并点击确定按钮 const confirmButton = await waitForElement('div.ant-modal-footer button.ant-btn.ant-btn-primary'); if (confirmButton) { console.log('已点击确定按钮'); confirmButton.click(); // 等待一小段时间,确保点击事件被处理 await new Promise(resolve => setTimeout(resolve, 500)); } // 等待模态弹窗消失 await waitForElementDisappear('div.ant-modal-content'); console.log('模态弹窗已关闭'); // 增加延迟,确保页面状态稳定 await new Promise(resolve => setTimeout(resolve, 800)); } // 开始估算流程 async function startEstimation(estimationValues) { try { const config = getConfig(); console.log('开始执行估算流程'); console.log(`配置:浮动值=${config.floatValue}%,自动提交=${config.autoSubmit}`); console.log(`输入的估算值:${estimationValues}`); // 找到所有事项行 const allRows = document.querySelectorAll('tr.ant-table-row.ant-table-row-level-0'); if (allRows.length === 0) { alert('未找到需要评估的事项行!'); return; } // 过滤出有评估按钮的事项行(排除合计行等) const validRows = Array.from(allRows).filter(row => { const button = row.querySelector('button.ant-btn.ant-btn-primary.ant-btn-sm'); return button !== null; }); console.log(`找到 ${allRows.length} 个事项行,其中 ${validRows.length} 个有评估按钮`); // 打印所有有效事项行,便于调试 validRows.forEach((row, index) => { console.log(`有效事项行 ${index + 1}:`, row); const button = row.querySelector('button.ant-btn.ant-btn-primary.ant-btn-sm'); console.log(`有效事项行 ${index + 1} 的评估按钮:`, button); }); // 过滤出非零估算值 const nonZeroValues = estimationValues.filter(v => v !== 0); console.log(`非零估算值:${nonZeroValues}`); // 处理每个非零估算值,按顺序对应到有效事项行 for (let i = 0; i < Math.min(nonZeroValues.length, validRows.length); i++) { const baseValue = nonZeroValues[i]; const row = validRows[i]; console.log(`开始处理有效事项行 ${i + 1},基础值=${baseValue}`); console.log(`使用事项行:`, row); await processItem(row, baseValue, config); // 等待一小段时间,确保页面状态稳定 await new Promise(resolve => setTimeout(resolve, 500)); } console.log(`处理完成,共处理 ${Math.min(nonZeroValues.length, validRows.length)} 个事项`); // 点击提交按钮 if (config.autoSubmit) { console.log('准备点击提交按钮'); // 找到所有符合条件的按钮,然后过滤出包含"提 交"文本的按钮 const buttons = document.querySelectorAll('div.ant-spin-container div button.ant-btn.ant-btn-primary'); const submitButton = Array.from(buttons).find(btn => { const span = btn.querySelector('span'); return span && span.textContent.trim() === '提 交'; }); if (submitButton) { console.log('已点击提交按钮'); submitButton.click(); } else { console.warn('未找到提交按钮'); } } console.log('估算流程执行完成'); } catch (error) { console.error('估算流程执行出错:', error); alert('估算执行出错:' + error.message); } } // 检查并添加开始估算按钮(只添加一次) function checkAndAddEstimationButton() { // 如果已经添加了按钮,直接返回 if (buttonAdded) { console.log('开始估算按钮已添加,无需重复添加'); return; } // 查找需求编码相关元素(只查找第一个) const requirementLabel = document.querySelector('label[title="需求编码"]'); if (!requirementLabel) { return; } // 查找需求编码值元素 const parent = requirementLabel.closest('.ant-form-item'); if (!parent) { return; } const controlInput = parent.querySelector('.ant-form-item-control-input-content'); if (!controlInput) { return; } // 检查需求编码值是否符合要求(字母+数字) const requirementCode = controlInput.textContent.trim(); if (!requirementCode || !/^[A-Za-z0-9]+$/.test(requirementCode)) { return; } console.log('检查并添加开始估算按钮...'); console.log(`找到有效需求编码:${requirementCode}`); // 创建填写估算按钮 const estimateButton = document.createElement('button'); estimateButton.className = 'ant-btn ant-btn-primary ant-btn-sm three-point-estimation-btn'; estimateButton.textContent = '填写估算'; estimateButton.style.marginLeft = '10px'; // 添加填写估算按钮的点击事件 estimateButton.addEventListener('click', () => { console.log('点击填写估算按钮,触发估算弹窗'); showEstimationDialog(); }); // 创建打开文档按钮 const openDocButton = document.createElement('button'); openDocButton.className = 'ant-btn ant-btn-default ant-btn-sm three-point-estimation-doc-btn'; openDocButton.textContent = '打开文档'; openDocButton.style.marginLeft = '5px'; // 添加打开文档按钮的点击事件 openDocButton.addEventListener('click', () => { console.log('点击打开文档按钮'); // 获取配置 const config = getConfig(); // 打开在线文档URL(如果有配置) if (config.docUrl) { try { console.log(`打开在线文档URL:${config.docUrl}`); // 如果窗口已存在,尝试聚焦到该窗口 if (win) { win.focus(); } else { // 使用特定的窗口名称,避免重复打开 // 相同URL会在同一标签页中打开,而不是新建标签页 win = window.open(config.docUrl, 'three_point_estimation_doc'); } } catch (error) { console.error('打开在线文档失败:', error); alert('打开在线文档失败:' + error.message); } } else { alert('未配置在线文档URL'); } }); // 添加两个按钮到控制输入元素的同级后面 controlInput.parentNode.insertBefore(estimateButton, controlInput.nextSibling); controlInput.parentNode.insertBefore(openDocButton, estimateButton.nextSibling); console.log('填写估算和打开文档按钮已添加'); // 复制需求编码到剪贴板(页面加载时自动复制) try { GM_setClipboard(requirementCode); console.log(`需求编码 ${requirementCode} 已自动复制到剪贴板`); } catch (error) { console.error('自动复制需求编码失败:', error); } // 设置标志位,避免重复添加 setButtonAdded(true); } // 初始检测和定期检测 function initDetection() { console.log('开始检测需求编码元素...'); // 初始检测 checkAndAddEstimationButton(); // 定期检测(每1秒),确保动态加载的内容能被检测到 interval = setInterval(() => { // 只在buttonAdded为false时执行检测 if (!buttonAdded) { checkAndAddEstimationButton(); } }, 1000); // 监听DOM变化,确保动态内容加载时能检测到 observer = new MutationObserver(() => { // 只在buttonAdded为false时执行检测 if (!buttonAdded) { checkAndAddEstimationButton(); } }); observer.observe(document.body, { childList: true, subtree: true }); } // 页面加载完成后开始检测 async function startDetection() { console.log('开始执行页面检测逻辑'); console.log('当前document.readyState:', document.readyState); // 检查并更新配置 await checkAndUpdateConfig(); initDetection(); } // 添加多种初始化方式,确保在各种浏览器环境下都能执行 console.log('当前document.readyState:', document.readyState); // 方式1:如果页面已经加载完成,直接执行 if (document.readyState === 'complete') { console.log('页面已经完全加载完成,直接执行检测逻辑'); startDetection(); } // 方式2:如果页面正在加载中,添加load事件监听器 else if (document.readyState === 'loading' || document.readyState === 'interactive') { console.log('页面正在加载中,添加load事件监听器'); window.addEventListener('load', startDetection); // 方式3:添加DOMContentLoaded事件监听器作为备选 console.log('添加DOMContentLoaded事件监听器作为备选'); document.addEventListener('DOMContentLoaded', function() { console.log('DOMContentLoaded事件触发,延迟500ms执行检测逻辑'); // 延迟执行,确保页面内容已经加载完成 setTimeout(startDetection, 500); }); // 方式4:添加一个定时器,作为最后的备选方案 console.log('添加5秒定时器作为最后的备选方案'); setTimeout(function() { console.log('5秒定时器触发,执行检测逻辑'); startDetection(); }, 5000); } // 方式5:直接尝试执行,作为紧急备选 console.log('直接尝试执行检测逻辑,作为紧急备选'); setTimeout(startDetection, 1000); // 注册菜单命令 GM_registerMenuCommand('设置', showSettingsDialog); GM_registerMenuCommand('开始估算', showEstimationDialog); })(); }