NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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();
})();