Kakueeen / OA加班申请表单自动填写

// ==UserScript==
// @name         OA加班申请表单自动填写
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  自动填写OA系统加班提前申请表单
// @author       Kakueeen
// @match        https://oa.uniontech.com/spa/workflow/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // ========== 配置模块 ==========
  const Config = {
    // 表单填写数据配置
    formData: {
      // 预计加班开始日期 - 今天
      overtimeStartDate: new Date().toISOString().split('T')[0],
      // 预计加班开始时间
      overtimeStartTime: '19:00',
      // 预计加班结束日期 - 今天
      overtimeEndDate: new Date().toISOString().split('T')[0],
      // 预计加班结束时间
      overtimeEndTime: '22:00',
      // 预计加班地点 - 需要根据实际选项调整
      overtimeLocation: '公司内',
      // 加班类型 - 需要根据实际选项调整
      overtimeType: '工作日加班',
      // 补偿方式 - 需要根据实际选项调整
      compensationType: '调休',
      // 加班事由
      overtimeReason: '修改文管BUG',
      // 工时项目相关配置
      workTimeProject: 'YFXMCY032025080004', // 项目编码,用于搜索工时项目
    },

    // 表单字段配置
    fields: {
      startDate: 'field33271',
      startTime: 'field33272',
      endDate: 'field33273',
      endTime: 'field33274',
      location: 'weaSelect_1',
      type: 'weaSelect_2',
      compensation: 'weaSelect_3',
      workProject: 'field51206',
      reason: 'field32988',
      hours: 'field32982'
    },

    // UI配置
    ui: {
      button: {
        text: '🤖 自动填写',
        position: {
          top: '10px',
          right: '10px'
        },
        colors: {
          gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
          shadow: '0 4px 15px rgba(0, 0, 0, 0.2)'
        }
      },
      toast: {
        duration: 3000,
        position: {
          top: '130px',
          right: '10px'
        },
        colors: {
          info: '#2196F3',
          success: '#4CAF50',
          error: '#f44336'
        }
      }
    },

    // 操作配置
    timing: {
      waitElement: 5000,
      stepDelay: 200,
      inputDelay: 100,
      panelDelay: 100
    }
  };

  // ========== 工具模块 ==========
  const Utils = {
    sleep(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
    },

    waitForElement(selector, timeout = Config.timing.waitElement) {
      return new Promise((resolve, reject) => {
        const startTime = Date.now();
        const checkElement = () => {
          const element = document.querySelector(selector);
          if (element) {
            resolve(element);
          }
          else if (Date.now() - startTime < timeout) {
            setTimeout(checkElement, 100);
          }
          else {
            reject(new Error(`Element ${selector} not found within ${timeout}ms`));
          }
        };
        checkElement();
      });
    },

    triggerEvents(element, events = ['input', 'change', 'blur']) {
      events.forEach(eventType => {
        element.dispatchEvent(new Event(eventType, {
          bubbles: true
        }));
      });
    }
  };

  // ========== DOM操作模块 ==========
  const DomHelper = {
    findFieldContainer(fieldId) {
      return document.querySelector(`[data-fieldmark="${fieldId}"]`);
    },

    findHiddenInput(fieldId) {
      return document.querySelector(`#${fieldId}`);
    },

    findInputElement(container, selectors = ['textarea', '.ant-input', 'input[type="text"]', '[contenteditable="true"]']) {
      for (let selector of selectors) {
        const element = container.querySelector(selector);
        if (element) return element;
      }
      return null;
    },

    setValue(element, value) {
      if (element.tagName.toLowerCase() === 'textarea' || element.type === 'text') {
        element.value = value;
      }
      else if (element.contentEditable === 'true') {
        element.textContent = value;
      }
    },

    getValue(element) {
      if (element.tagName.toLowerCase() === 'textarea' || element.type === 'text') {
        return element.value;
      }
      else if (element.contentEditable === 'true') {
        return element.textContent || element.innerText || '';
      }
      return '';
    }
  };

  // ========== 表单填写策略模块 ==========
  const FormStrategies = {
    async setInputField(fieldId, value) {
      const container = DomHelper.findFieldContainer(fieldId);
      const hiddenInput = DomHelper.findHiddenInput(fieldId);

      if (!container || !hiddenInput) {
        console.warn(`字段 ${fieldId} 不存在`);
        return false;
      }

      const inputElement = DomHelper.findInputElement(container);
      if (!inputElement) {
        console.warn(`字段 ${fieldId} 的输入元素不存在`);
        return false;
      }

      try {
        inputElement.focus();
        await Utils.sleep(Config.timing.inputDelay);

        DomHelper.setValue(inputElement, value);
        Utils.triggerEvents(inputElement);

        await Utils.sleep(Config.timing.inputDelay);
        return DomHelper.getValue(inputElement).trim() === value.trim();
      }
      catch (error) {
        console.error(`设置字段 ${fieldId} 失败:`, error);
        return false;
      }
    },

    async setDateField(fieldId, date) {
      const container = DomHelper.findFieldContainer(fieldId);
      if (!container) return false;

      try {
        const dateIcon = container.querySelector('.icon-coms-New-schedule');
        const clickableIcon = dateIcon?.closest('.cursor-pointer');

        if (clickableIcon) {
          clickableIcon.click();
          await Utils.sleep(Config.timing.inputDelay * 3);

          const activeInput = document.querySelector('.ant-calendar-input:not([readonly])');
          if (activeInput) {
            activeInput.value = date;
            activeInput.focus();
            Utils.triggerEvents(activeInput);

            const enterEvent = new KeyboardEvent('keydown', {
              key: 'Enter',
              code: 'Enter',
              keyCode: 13,
              bubbles: true
            });
            activeInput.dispatchEvent(enterEvent);

            await Utils.sleep(Config.timing.inputDelay * 2);
            activeInput.blur();
            return true;
          }
        }
      }
      catch (error) {
        console.error(`设置日期字段 ${fieldId} 失败:`, error);
      }
      return false;
    },

    async setTimeField(fieldId, time) {
      const container = DomHelper.findFieldContainer(fieldId);
      if (!container) return false;

      try {
        const timeIcon = container.querySelector('.icon-coms-overtime');
        const clickableIcon = timeIcon?.closest('.cursor-pointer');

        if (clickableIcon) {
          clickableIcon.click();
          await Utils.sleep(Config.timing.panelDelay);

          const timePanel = document.querySelector('.ant-time-picker-panel');
          if (timePanel) {
            const panelInput = timePanel.querySelector('.ant-time-picker-panel-input');
            if (panelInput) {
              panelInput.focus();
              panelInput.value = time;
              Utils.triggerEvents(panelInput);

              const enterEvent = new KeyboardEvent('keydown', {
                key: 'Enter',
                code: 'Enter',
                keyCode: 13,
                bubbles: true
              });
              panelInput.dispatchEvent(enterEvent);

              await Utils.sleep(Config.timing.inputDelay * 3);
              return true;
            }
          }
        }
      }
      catch (error) {
        console.error(`设置时间字段 ${fieldId} 失败:`, error);
      }
      return false;
    },

    async setSelectField(selector, value) {
      try {
        const selectContainer = document.querySelector(`#${selector}`);
        if (selectContainer) {
          const trigger = selectContainer.querySelector('.ant-select-selection');
          if (trigger) {
            trigger.click();
            await Utils.sleep(Config.timing.inputDelay * 2);

            const options = document.querySelectorAll('.ant-select-dropdown-menu-item');
            for (let option of options) {
              if (option.textContent.includes(value)) {
                option.click();
                return true;
              }
            }
          }
        }
      }
      catch (error) {
        console.error(`设置选择字段 ${selector} 失败:`, error);
      }
      return false;
    },

    async setWorkProjectField(projectName) {
      // 参考 oa_tmp.js 的 selectWorkTimeProject 实现
      const fieldId = Config.fields.workProject;
      const fieldContainer = document.querySelector(`[data-fieldmark="${fieldId}"]`);
      const hiddenInput = document.querySelector(`#${fieldId}`);
      if (!fieldContainer || !hiddenInput) {
        console.log('未找到工时项目字段');
        return false;
      }
      // 检查是否已经有选中的项目
      const currentValue = hiddenInput.value;
      const selectedItems = fieldContainer.querySelectorAll('.ant-select-selection__choice');
      if (currentValue && selectedItems.length > 0) {
        let hasTargetProject = false;
        selectedItems.forEach(item => {
          const text = item.textContent.trim();
          if (text.includes(projectName) || text.includes('DDE') || text.includes('YFXM')) {
            hasTargetProject = true;
          }
        });
        if (hasTargetProject) {
          console.log('工时项目已经有合适的选择,跳过设置');
          return true;
        }
      }
      // 如果没有合适的项目,尝试添加新项目
      let success = false;
      const searchButton = fieldContainer.querySelector('.ant-btn-ghost.ant-btn-icon-only');
      if (searchButton) {
        searchButton.click();
        await Utils.sleep(1000); // 等待对话框打开
        const modal = await Utils.waitForElement('.ant-modal-content', 3000).catch(() => null);
        if (!modal) {
          console.log('搜索对话框未出现');
          return false;
        }
        let projectInput = document.querySelector('#con51069_value');
        if (!projectInput) {
          // 查找所有包含"项目编号"标签的输入框
          const labels = document.querySelectorAll('.wea-form-item-label');
          for (let label of labels) {
            if (label.textContent.includes('项目编号(技术)')) {
              const wrapper = label.closest('.wea-form-cell, .wea-form-item');
              if (wrapper) {
                projectInput = wrapper.querySelector('input[type="text"]');
                break;
              }
            }
          }
        }
        if (projectInput) {
          projectInput.value = '';
          projectInput.focus();
          await Utils.sleep(200);
          projectInput.value = projectName;
          projectInput.dispatchEvent(new Event('input', {
            bubbles: true
          }));
          projectInput.dispatchEvent(new Event('change', {
            bubbles: true
          }));
          await Utils.sleep(300);
          const searchButtons = document.querySelectorAll('.ant-btn-primary');
          let searchBtn = null;
          for (let btn of searchButtons) {
            if (btn.textContent.includes('搜 索')) {
              searchBtn = btn;
              break;
            }
          }
          if (searchBtn) {
            searchBtn.click();
            await Utils.sleep(1500); // 等待搜索结果
            const resultTable = document.querySelector('.ant-table-tbody');
            if (resultTable) {
              const rows = resultTable.querySelectorAll('tr');
              for (let row of rows) {
                const cells = row.querySelectorAll('td');
                let projectNumber = '';
                if (cells.length >= 1) {
                  projectNumber = cells[0].textContent.trim();
                }
                if (projectNumber === projectName || projectNumber.includes(projectName)) {
                  row.click();
                  await Utils.sleep(300);
                  if (row.classList.contains('ant-table-row-selected') || row.style.backgroundColor) {
                    success = true;
                  }
                  break;
                }
              }
              if (!success && rows.length > 0) {
                rows[0].click();
                await Utils.sleep(300);
                success = true;
              }
            }
            // 关闭对话框 - 点击取消或确认按钮
            setTimeout(async () => {
              const footerButtons = document.querySelectorAll('.ant-modal-footer .ant-btn');
              let confirmButton = null;
              for (let btn of footerButtons) {
                const text = btn.textContent.trim();
                if (text.includes('确') || text.includes('定')) {
                  confirmButton = btn;
                  break;
                }
              }
              if (!confirmButton) {
                for (let btn of footerButtons) {
                  const text = btn.textContent.trim();
                  if (text.includes('取 消')) {
                    confirmButton = btn;
                    break;
                  }
                }
              }
              if (confirmButton) {
                confirmButton.click();
              }
              else {
                const closeBtn = document.querySelector('.ant-modal-close');
                if (closeBtn) {
                  closeBtn.click();
                }
              }
              setTimeout(() => {
                const newSelectedItems = fieldContainer.querySelectorAll('.ant-select-selection__choice');
                if (newSelectedItems.length > selectedItems.length) {
                  success = true;
                }
              }, 500);
            }, 500);
          }
          else {
            console.log('未找到搜索按钮');
          }
        }
        else {
          console.log('未找到项目编号输入框');
        }
      }
      // 方法2: 如果搜索失败,直接保持现有选择
      if (!success && currentValue) {
        success = true;
      }
      return success;
    }
  };

  // ========== UI模块 ==========
  const UI = {
    createButton() {
      const button = document.createElement('button');
      button.innerHTML = Config.ui.button.text;
      button.style.cssText = `
                position: fixed;
                top: 80px;
                right: ${Config.ui.button.position.right};
                z-index: 9999;
                background: ${Config.ui.button.colors.gradient};
                border: none;
                color: white;
                padding: 12px 20px;
                border-radius: 25px;
                font-size: 14px;
                font-weight: bold;
                cursor: pointer;
                box-shadow: ${Config.ui.button.colors.shadow};
                transition: all 0.3s ease;
                font-family: 'Microsoft YaHei', sans-serif;
            `;

      button.onmouseover = function () {
        this.style.transform = 'translateY(-2px)';
        this.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.3)';
      };

      button.onmouseout = function () {
        this.style.transform = 'translateY(0)';
        this.style.boxShadow = Config.ui.button.colors.shadow;
      };

      button.onclick = () => FormManager.autoFillForm();
      document.body.appendChild(button);
    },

    showToast(message, type = 'info') {
      const existingToast = document.querySelector('#auto-fill-toast');
      if (existingToast) existingToast.remove();

      const toast = document.createElement('div');
      toast.id = 'auto-fill-toast';
      toast.innerHTML = message;

      const bgColor = Config.ui.toast.colors[type] || Config.ui.toast.colors.info;

      toast.style.cssText = `
                position: fixed;
                top: ${Config.ui.toast.position.top};
                right: ${Config.ui.toast.position.right};
                z-index: 10000;
                background: ${bgColor};
                color: white;
                padding: 12px 20px;
                border-radius: 6px;
                font-size: 14px;
                box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
                font-family: 'Microsoft YaHei', sans-serif;
                max-width: 300px;
                animation: slideIn 0.3s ease;
            `;

      if (!document.head.querySelector('#toast-styles')) {
        const style = document.createElement('style');
        style.id = 'toast-styles';
        style.textContent = `
                    @keyframes slideIn {
                        from { transform: translateX(100%); opacity: 0; }
                        to { transform: translateX(0); opacity: 1; }
                    }
                `;
        document.head.appendChild(style);
      }

      document.body.appendChild(toast);
      setTimeout(() => toast.remove(), Config.ui.toast.duration);
    }
  };

  // ========== 表单管理器 ==========
  const FormManager = {
    async autoFillForm() {
      console.log('开始自动填写表单...');

      try {
        await Utils.waitForElement('.excelMainTable');
        UI.showToast('正在自动填写表单...');

        const steps = [{
            name: '设置加班开始日期',
            action: () => FormStrategies.setDateField(Config.fields.startDate, Config.formData.overtimeStartDate)
          },
          {
            name: '设置加班开始时间',
            action: () => FormStrategies.setTimeField(Config.fields.startTime, Config.formData.overtimeStartTime)
          },
          {
            name: '设置加班结束日期',
            action: () => FormStrategies.setDateField(Config.fields.endDate, Config.formData.overtimeEndDate)
          },
          {
            name: '设置加班结束时间',
            action: () => FormStrategies.setTimeField(Config.fields.endTime, Config.formData.overtimeEndTime)
          },
          {
            name: '设置加班地点',
            action: () => FormStrategies.setSelectField(Config.fields.location, Config.formData.overtimeLocation)
          },
          {
            name: '设置加班类型',
            action: () => FormStrategies.setSelectField(Config.fields.type, Config.formData.overtimeType)
          },
          {
            name: '设置加班事由',
            action: () => FormStrategies.setInputField(Config.fields.reason, Config.formData.overtimeReason)
          },
          {
            name: '设置工时项目',
            action: () => FormStrategies.setWorkProjectField(Config.formData.workTimeProject)
          }
        ];

        const results = {};
        for (const step of steps) {
          UI.showToast(`正在${step.name}...`);
          results[step.name] = await step.action();
          await Utils.sleep(Config.timing.stepDelay);
        }

        // 显示结果
        const successCount = Object.values(results).filter(Boolean).length;
        const totalCount = Object.keys(results).length;

        if (successCount === totalCount) {
          UI.showToast('表单填写完成!', 'success');
        }
        else {
          UI.showToast(`填写完成 (${successCount}/${totalCount})`, 'info');
        }

        console.log('表单填写结果:', results);

      }
      catch (error) {
        console.error('自动填写失败:', error);
        UI.showToast('自动填写失败,请手动填写', 'error');
      }
    },

    isTargetPage() {
      return window.location.href.includes('workflow') &&
        (document.title.includes('加班') || document.querySelector('span:contains("加班提前申请")'));
    }
  };

  // ========== 初始化模块 ==========
  const App = {
    init() {
      if (document.querySelector('.excelMainTable') || document.title.includes('加班')) {
        console.log('检测到OA加班申请页面,添加自动填写按钮');
        UI.createButton();
      }
    },

    start() {
      if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', this.init);
      }
      else {
        setTimeout(this.init, 1000);
      }
    }
  };

  // 启动应用
  App.start();

})();