unicorn-rpa
Version:
🦄 Unicorn RPA - 强大的跨域iframe自动化控制工具
1,110 lines (978 loc) • 50.7 kB
JavaScript
const simulateHumanTyping = require('./simulateHumanTyping');
const { delay, log, errorLog, safeQuerySelector, safeQuerySelectorAll } = require('../utils');
const actionHandlers = {
async fill(action, frameDoc, results) {
const el = safeQuerySelector(action.selector, frameDoc);
if (!el) throw new Error(`选择器 ${action.selector} 未找到`);
if (action.value) {
log('执行文本输入:', action.selector, action.value);
await simulateHumanTyping(el, action.value, action.speed || 60);
await delay(action.delay !== undefined ? action.delay : 800);
}
},
async select(action, frameDoc, results) {
const sel = safeQuerySelector(action.selector, frameDoc);
if (!sel) throw new Error(`选择器 ${action.selector} 未找到`);
log('执行下拉选择:', action.selector, action.value);
sel.click()
// sel.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
// sel.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
await delay(action.delay !== undefined ? action.delay : 800);
const allElements = [].slice.call(safeQuerySelectorAll("*"));
const options = allElements.filter((el) => {
const text = (el.textContent && el.textContent.trim()) || '';
if (!text || !text.includes(action.value)) return false;
if (el === sel || sel.contains(el)) return false;
return (
el.tagName === "OPTION" ||
el.tagName === "LI" ||
(el.tagName === "DIV" && (el.textContent && el.textContent.trim()) === action.value) ||
(el.tagName === "SPAN" && (el.textContent && el.textContent.trim()) === action.value) ||
el.classList.contains("option") ||
el.classList.contains("item") ||
el.classList.contains("select-item") ||
el.classList.contains("ant-select-item") ||
el.classList.contains("el-select-dropdown__item")
);
});
// 修改查找逻辑,增加对单个选项的特殊处理
let target = options.find(opt => {
const text = (opt.textContent && opt.textContent.trim()) || '';
return text === action.value || text.includes(action.value);
});
// 如果没有找到完全匹配的选项,尝试更宽松的匹配方式
if (!target && options.length > 0) {
target = options[0]; // 使用第一个选项作为备选
log('使用备选方案选择第一个选项:', target.textContent && target.textContent.trim());
}
if (target) {
// 确保元素在视口中并可点击
if (target.scrollIntoViewIfNeeded) {
target.scrollIntoViewIfNeeded();
} else if (target.scrollIntoView) {
target.scrollIntoView();
}
// 使用多种点击方式确保点击生效
target.click();
// 添加额外的事件触发确保表单更新
target.dispatchEvent(new Event('change', { bubbles: true }));
target.dispatchEvent(new Event('input', { bubbles: true }));
log('成功选择选项:', action.value);
// 等待选项选择完成
await delay(500);
// 验证选择是否成功 - 检查选择框的值是否已更新
const selectedValue = sel.textContent || sel.innerText || sel.value || '';
log('选择后的值:', selectedValue);
// 如果选择框的值没有更新,尝试强制更新
if (!selectedValue.includes(action.value)) {
log('选择框值未更新,尝试强制更新...');
// 方法1: 尝试通过属性设置值
if (sel.hasAttribute('data-value')) {
sel.setAttribute('data-value', action.value);
}
// 方法2: 查找选择框内的文本元素并修改
const textElements = sel.querySelectorAll('span, div, p, .ant-select-selection-item');
let textModified = false;
for (const textEl of textElements) {
if (textEl.textContent && textEl.textContent.trim()) {
textEl.textContent = action.value;
textModified = true;
log('直接修改选择框文本为:', action.value);
break;
}
}
if (textModified) {
// 触发必要的事件
sel.dispatchEvent(new Event('change', { bubbles: true }));
sel.dispatchEvent(new Event('input', { bubbles: true }));
sel.dispatchEvent(new Event('blur', { bubbles: true }));
log('强制更新完成');
}
// 再次验证
await delay(200);
const finalValue = sel.textContent || sel.innerText || sel.value || '';
log('强制更新后的值:', finalValue);
if (finalValue.includes(action.value)) {
log('强制更新成功');
} else {
log('警告:强制更新后值仍未正确设置');
}
}
await delay(action.delay !== undefined ? action.delay : 800);
} else {
log('未找到匹配的选项:', action.value);
}
},
async click(action, frameDoc, results) {
let btn = safeQuerySelector(action.selector, frameDoc);
if (action.text) {
const candidates = [].slice.call(safeQuerySelectorAll(action.selector, frameDoc));
btn = candidates.find(el => (el.innerText || el.textContent).trim() === action.text);
}
if (!btn) throw new Error(`选择器 ${action.selector} (text: ${action.text || ''}) 未找到`);
log('执行点击:', action.selector, action.text || '');
btn.click();
await delay(action.delay !== undefined ? action.delay : 800);
},
async doubleClick(action, frameDoc, results) {
let btn = safeQuerySelector(action.selector, frameDoc);
if (action.text) {
const candidates = [].slice.call(safeQuerySelectorAll(action.selector, frameDoc));
btn = candidates.find(el => (el.innerText || el.textContent).trim() === action.text);
}
if (!btn) throw new Error(`选择器 ${action.selector} 未找到`);
log('执行双击:', action.selector);
btn.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }));
await delay(action.delay !== undefined ? action.delay : 800);
},
async rightClick(action, frameDoc, results) {
let btn = safeQuerySelector(action.selector, frameDoc);
if (action.text) {
const candidates = [].slice.call(safeQuerySelectorAll(action.selector, frameDoc));
btn = candidates.find(el => (el.innerText || el.textContent).trim() === action.text);
}
if (!btn) throw new Error(`选择器 ${action.selector} 未找到`);
log('执行右键点击:', action.selector);
btn.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true }));
await delay(action.delay !== undefined ? action.delay : 800);
},
async hover(action, frameDoc, results) {
const el = safeQuerySelector(action.selector, frameDoc);
if (!el) throw new Error(`选择器 ${action.selector} 未找到`);
log('执行悬停:', action.selector);
el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
await delay(action.delay !== undefined ? action.delay : 800);
},
async scroll(action, frameDoc, results) {
const el = safeQuerySelector(action.selector, frameDoc) || window;
log('执行滚动:', action.selector, { x: action.x, y: action.y });
el.scrollTo({
top: action.y || 0,
left: action.x || 0,
behavior: action.behavior || 'smooth'
});
await delay(action.delay !== undefined ? action.delay : 800);
},
async screenshot(action, frameDoc, results) {
if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
try {
log('开始截图...');
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
const track = stream.getVideoTracks()[0];
const imageCapture = new ImageCapture(track);
const bitmap = await imageCapture.grabFrame();
const canvas = document.createElement('canvas');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const context = canvas.getContext('2d');
context.drawImage(bitmap, 0, 0);
const dataUrl = canvas.toDataURL('image/png');
results.push({ type: 'screenshot', data: dataUrl });
track.stop();
log('截图完成');
await delay(action.delay !== undefined ? action.delay : 800);
} catch (error) {
errorLog('截图失败:', error);
}
} else {
errorLog('浏览器不支持截图功能');
}
},
async waitFor(action, frameDoc, results) {
const timeout = action.timeout || 5000;
log('等待元素出现:', action.selector, `超时: ${timeout}ms`);
await new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
observer.disconnect();
reject(new Error(`等待超时: ${action.selector}`));
}, timeout);
const observer = new MutationObserver(() => {
if (safeQuerySelector(action.selector, frameDoc)) {
clearTimeout(timeoutId);
observer.disconnect();
log('元素已出现:', action.selector);
resolve();
}
});
observer.observe(frameDoc, { childList: true, subtree: true });
if (safeQuerySelector(action.selector, frameDoc)) {
clearTimeout(timeoutId);
observer.disconnect();
log('元素已存在:', action.selector);
resolve();
}
});
},
async wait(action, frameDoc, results) {
const duration = action.duration || 1000;
log('等待固定时长:', duration + 'ms');
await delay(duration);
},
async treeSelect(action, frameDoc, results) {
const sel = safeQuerySelector(action.selector, frameDoc);
if (!sel) throw new Error(`选择器 ${action.selector} 未找到`);
log('执行树形选择:', action.selector, action.value);
sel.click();
// sel.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
// sel.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
await delay(action.delay !== undefined ? action.delay : 800);
for (let i = 0; i < action.value.length; i++) {
const element = action.value[i];
const node = safeQuerySelector(`[title="${element}"], [data-label="${element}"]`);
if (!node) throw new Error(`找不到节点: ${element}`);
node.click();
await delay(action.delay !== undefined ? action.delay : 800);
}
const trigger = safeQuerySelector(action.selector, frameDoc);
if (trigger) {
trigger.click();
await delay(action.delay !== undefined ? action.delay : 800);
}
},
async checkAllCheckboxes(action, frameDoc, results) {
const isSpinning = () => {
const selectors = [
'.ant-spin.ant-spin-spinning',
'.el-loading-spinner',
'.van-loading',
'.spinner-border',
'.spinner-grow'
];
const allLoading = [].slice.call(safeQuerySelectorAll(selectors.join(',')))
.filter(el => el.offsetParent !== null);
return allLoading.length > 0;
};
if (isSpinning()) {
log('检测到loading状态,等待结束...');
await new Promise((resolve, reject) => {
const timeout = setTimeout(
() => reject(new Error("等待loading转圈结束超时")),
action.timeout || 10000
);
function check() {
if (!isSpinning()) {
clearTimeout(timeout);
log('loading状态结束');
resolve();
} else {
setTimeout(check, 100);
}
}
check();
});
}
let scope = frameDoc;
if (action.selector) {
const container = safeQuerySelector(action.selector, frameDoc);
if (container) {
scope = container;
}
}
const lis = safeQuerySelectorAll('ul li, .checkbox-item, [class*="checkbox"]', scope);
const count = typeof action.value === "number" ? action.value : lis.length;
log('开始批量勾选checkbox,总数:', lis.length, '目标数量:', count);
let checked = 0;
for (let i = 0; i < lis.length; i++) {
const li = lis[i];
if (checked >= count) break;
const checkbox = li.querySelector(
'.ant-checkbox, input[type="checkbox"], [class*="checkbox"]'
);
if (checkbox) {
checkbox.click();
checked++;
await delay(action.delay !== undefined ? action.delay : 800);
}
}
log('批量勾选完成,已勾选:', checked);
},
async getText(action, frameDoc, results) {
const el = safeQuerySelector(action.selector, frameDoc);
if (!el) throw new Error(`选择器 ${action.selector} 未找到`);
const text = el.innerText || el.textContent || '';
results.push({ type: 'getText', selector: action.selector, value: text });
log('获取文本内容:', action.selector, text);
await delay(action.delay !== undefined ? action.delay : 800);
},
async getValue(action, frameDoc, results) {
const el = safeQuerySelector(action.selector, frameDoc);
if (!el) throw new Error(`选择器 ${action.selector} 未找到`);
const value = el.value || el.getAttribute('value') || '';
results.push({ type: 'getValue', selector: action.selector, value });
log('获取元素值:', action.selector, value);
await delay(action.delay !== undefined ? action.delay : 800);
},
async timeRange(action, frameDoc, results) {
// 支持多种组件库的时间范围选择器
const el = safeQuerySelector(action.selector, frameDoc) ||
safeQuerySelector(`[data-testid="${action.selector}"], [data-cy="${action.selector}"], [data-id="${action.selector}"]`, frameDoc) ||
safeQuerySelector(`.${action.selector}, #${action.selector}`, frameDoc);
if (!el) throw new Error(`选择器 ${action.selector} 未找到`);
log('执行时间范围选择:', action.selector, action.value);
// 确保传入的值是数组格式
if (!Array.isArray(action.value) || action.value.length !== 2) {
throw new Error('时间范围值必须是包含两个日期的数组');
}
const [startDate, endDate] = action.value;
// 点击时间范围选择器打开日期面板 - 支持多种组件库
el.click();
await delay(action.delay !== undefined ? action.delay : 800);
// 查找日期面板 - 支持多种组件库(移除 ant-calendar 相关内容)
const datePanel = safeQuerySelector(`
.ant-picker-dropdown, .ant-picker-panel, .ant-picker-panel-container,
.el-date-picker, .el-date-editor, .el-calendar,
.van-datetime-picker, .van-picker,
.mui-datepicker, .mui-calendar,
.bootstrap-datepicker, .datepicker,
.flatpickr-calendar, .flatpickr-dropdown,
.react-datepicker, .react-datepicker__container,
.date-picker, .calendar-picker, .date-dropdown,
.el-popper, .el-date-picker__popper,
.van-popup, .van-picker-popup,
.mui-popover, .mui-datepicker-popup,
.bootstrap-datepicker-dropdown, .datepicker-dropdown,
.flatpickr-calendar.open, .flatpickr-calendar.arrowTop,
.react-datepicker__popper, .react-datepicker__portal,
.calendar-popup, .date-picker-popup, .picker-popup,
[data-popper-placement], [role="dialog"], [role="menu"]
`, frameDoc);
if (!datePanel) {
throw new Error('未找到日期选择面板');
}
// 辅助函数:查找并点击日期,如果找不到则翻页
const findAndClickDate = async (targetDate, isStartDate = true) => {
const maxAttempts = 24; // 最多翻24页(2年)
let attempts = 0;
while (attempts < maxAttempts) {
// 查找目标日期 - 支持多种组件库的日期单元格(移除 ant-calendar 相关内容)
const dateCell = safeQuerySelector(`
td[title="${targetDate}"], td[data-value="${targetDate}"],
.ant-picker-cell[title="${targetDate}"], .ant-picker-cell[data-value="${targetDate}"],
.el-date-table td[title="${targetDate}"], .el-date-table td[data-value="${targetDate}"],
.van-picker-column__item[title="${targetDate}"], .van-picker-column__item[data-value="${targetDate}"],
.mui-calendar-day[title="${targetDate}"], .mui-calendar-day[data-value="${targetDate}"],
.bootstrap-datepicker td[title="${targetDate}"], .bootstrap-datepicker td[data-value="${targetDate}"],
.flatpickr-day[title="${targetDate}"], .flatpickr-day[data-value="${targetDate}"],
.react-datepicker__day[title="${targetDate}"], .react-datepicker__day[data-value="${targetDate}"],
.calendar-day[title="${targetDate}"], .calendar-day[data-value="${targetDate}"],
[data-date="${targetDate}"], [data-day="${targetDate}"], [data-value="${targetDate}"]
`, datePanel);
if (dateCell) {
dateCell.click();
log(`${isStartDate ? '点击开始日期' : '点击结束日期'}:`, targetDate);
await delay(action.delay !== undefined ? action.delay : 800);
return true;
}
// 如果找不到,获取当前显示的月份信息
const targetDateObj = new Date(targetDate);
// 尝试多种选择器获取当前月份 - 支持多种组件库(移除 ant-calendar 相关内容)
const currentMonthText = safeQuerySelector(`
.ant-picker-header-view .ant-picker-month-panel, .ant-picker-header-view .ant-picker-year-panel, .ant-picker-header-view,
.el-date-picker__header, .el-date-editor__header, .el-calendar__header,
.van-picker__title, .van-datetime-picker__title,
.mui-datepicker-header, .mui-calendar-header,
.bootstrap-datepicker-header, .datepicker-header,
.flatpickr-current-month, .flatpickr-header,
.react-datepicker__header, .react-datepicker__current-month,
.calendar-header, .date-picker-header, .picker-header
`, datePanel);
if (currentMonthText) {
const currentText = currentMonthText.textContent.trim();
log('当前显示月份文本:', currentText);
// 尝试从所有可能的元素中获取月份信息 - 支持多种组件库(移除 ant-calendar 相关内容)
const allHeaderElements = safeQuerySelectorAll(`
.ant-picker-header-view *,
.el-date-picker__header *, .el-date-editor__header *, .el-calendar__header *,
.van-picker__title *, .van-datetime-picker__title *,
.mui-datepicker-header *, .mui-calendar-header *,
.bootstrap-datepicker-header *, .datepicker-header *,
.flatpickr-current-month *, .flatpickr-header *,
.react-datepicker__header *, .react-datepicker__current-month *,
.calendar-header *, .date-picker-header *, .picker-header *
`, datePanel);
log('找到的头部元素数量:', allHeaderElements.length);
for (let i = 0; i < allHeaderElements.length; i++) {
const element = allHeaderElements[i];
const text = (element.textContent && element.textContent.trim()) || '';
if (text && (text.includes('年') || text.includes('月') || text.includes('202') || text.includes('2024') || text.includes('2025') || text.includes('2026'))) {
log(`找到可能的月份元素 ${i}:`, text);
}
}
// 解析当前显示的月份
let currentDate;
if (currentText.includes('年') && currentText.includes('月')) {
// 中文格式:2024年7月
const match = currentText.match(/(\d{4})年(\d{1,2})月/);
if (match) {
currentDate = new Date(parseInt(match[1]), parseInt(match[2]) - 1, 1);
}
} else if (currentText.includes('-')) {
// 英文格式:Jul 2024
currentDate = new Date(currentText + ' 1');
} else {
// 其他格式,尝试直接解析
currentDate = new Date(currentText + ' 1');
}
if (currentDate && !isNaN(currentDate.getTime())) {
// 比较目标日期与当前显示月份
const targetMonth = new Date(targetDateObj.getFullYear(), targetDateObj.getMonth(), 1);
log('目标月份:', targetMonth.toISOString().slice(0, 7));
log('当前显示月份:', currentDate.toISOString().slice(0, 7));
log('目标月份时间戳:', targetMonth.getTime());
log('当前月份时间戳:', currentDate.getTime());
const nextButton = safeQuerySelector(`
.ant-picker-header-next-btn, .ant-picker-next-icon,
.el-date-picker__header-btn--next, .el-date-editor__header-btn--next,
.van-picker__title--next, .van-datetime-picker__title--next,
.mui-datepicker-next, .mui-calendar-next,
.bootstrap-datepicker-next, .datepicker-next,
.flatpickr-next-month, .flatpickr-next,
.react-datepicker__navigation--next, .react-datepicker__next-button,
.calendar-next, .date-picker-next, .picker-next,
[data-action="next"], [data-direction="next"], .next-btn, .next-button
`, datePanel);
const prevButton = safeQuerySelector(`
.ant-picker-header-prev-btn, .ant-picker-prev-icon,
.el-date-picker__header-btn--prev, .el-date-editor__header-btn--prev,
.van-picker__title--prev, .van-datetime-picker__title--prev,
.mui-datepicker-prev, .mui-calendar-prev,
.bootstrap-datepicker-prev, .datepicker-prev,
.flatpickr-prev-month, .flatpickr-prev,
.react-datepicker__navigation--prev, .react-datepicker__previous-button,
.calendar-prev, .date-picker-prev, .picker-prev,
[data-action="prev"], [data-direction="prev"], .prev-btn, .prev-button
`, datePanel);
if (targetMonth.getTime() > currentDate.getTime() && nextButton) {
// 目标月份在当前月份之后,点击下一页
nextButton.click();
log('目标月份更晚,翻到下一页查找日期:', targetDate);
} else if (targetMonth.getTime() < currentDate.getTime() && prevButton) {
// 目标月份在当前月份之前,点击上一页
prevButton.click();
log('目标月份更早,翻到上一页查找日期:', targetDate);
} else {
log(`无法找到${isStartDate ? '开始' : '结束'}日期:`, targetDate);
return false;
}
} else {
log('无法解析当前月份,尝试点击下一页');
const nextButton = safeQuerySelector(`
.ant-picker-header-next-btn, .ant-picker-next-icon,
.el-date-picker__header-btn--next, .el-date-editor__header-btn--next,
.van-picker__title--next, .van-datetime-picker__title--next,
.mui-datepicker-next, .mui-calendar-next,
.bootstrap-datepicker-next, .datepicker-next,
.flatpickr-next-month, .flatpickr-next,
.react-datepicker__navigation--next, .react-datepicker__next-button,
.calendar-next, .date-picker-next, .picker-next,
[data-action="next"], [data-direction="next"], .next-btn, .next-button
`, datePanel);
if (nextButton) {
nextButton.click();
}
}
} else {
log('无法获取当前月份信息,根据目标日期判断翻页方向');
// 根据目标日期判断翻页方向
const targetYear = targetDateObj.getFullYear();
const currentYear = new Date().getFullYear();
log('目标年份:', targetYear, '当前年份:', currentYear);
const nextButton = safeQuerySelector(`
.ant-picker-header-next-btn, .ant-picker-next-icon, .ant-calendar-next-month-btn,
.ant-calendar-panel .ant-calendar-next-month-btn, .ant-calendar-dropdown .ant-calendar-next-month-btn,
.el-date-picker__header-btn--next, .el-date-editor__header-btn--next,
.van-picker__title--next, .van-datetime-picker__title--next,
.mui-datepicker-next, .mui-calendar-next,
.bootstrap-datepicker-next, .datepicker-next,
.flatpickr-next-month, .flatpickr-next,
.react-datepicker__navigation--next, .react-datepicker__next-button,
.calendar-next, .date-picker-next, .picker-next,
[data-action="next"], [data-direction="next"], .next-btn, .next-button
`, datePanel);
const prevButton = safeQuerySelector(`
.ant-picker-header-prev-btn, .ant-picker-prev-icon, .ant-calendar-prev-month-btn,
.ant-calendar-panel .ant-calendar-prev-month-btn, .ant-calendar-dropdown .ant-calendar-prev-month-btn,
.el-date-picker__header-btn--prev, .el-date-editor__header-btn--prev,
.van-picker__title--prev, .van-datetime-picker__title--prev,
.mui-datepicker-prev, .mui-calendar-prev,
.bootstrap-datepicker-prev, .datepicker-prev,
.flatpickr-prev-month, .flatpickr-prev,
.react-datepicker__navigation--prev, .react-datepicker__previous-button,
.calendar-prev, .date-picker-prev, .picker-prev,
[data-action="prev"], [data-direction="prev"], .prev-btn, .prev-button
`, datePanel);
if (targetYear < currentYear && prevButton) {
// 目标年份早于当前年份,向前翻页
prevButton.click();
log('目标年份更早,强制向前翻页查找日期:', targetDate);
} else if (targetYear > currentYear && nextButton) {
// 目标年份晚于当前年份,向后翻页
nextButton.click();
log('目标年份更晚,向后翻页查找日期:', targetDate);
} else if (nextButton) {
// 默认向后翻页
nextButton.click();
log('默认向后翻页查找日期:', targetDate);
}
}
await delay(action.delay !== undefined ? action.delay : 800);
attempts++;
}
log(`查找${isStartDate ? '开始' : '结束'}日期超时:`, targetDate);
return false;
};
// 查找并点击开始日期
await findAndClickDate(startDate, true);
// 查找并点击结束日期
await findAndClickDate(endDate, false);
// 点击确定按钮确认选择 - 支持多种组件库
const okButton = safeQuerySelector(`
.ant-picker-ok, .ant-btn-primary, button[type="button"],
.el-date-picker__link-btn, .el-date-editor__confirm-btn,
.van-picker__confirm, .van-datetime-picker__confirm,
.mui-datepicker-ok, .mui-calendar-ok,
.bootstrap-datepicker-ok, .datepicker-ok,
.flatpickr-confirm, .flatpickr-ok,
.react-datepicker__close-button, .react-datepicker__ok-button,
.calendar-ok, .date-picker-ok, .picker-ok,
[data-action="confirm"], [data-action="ok"], .confirm-btn, .ok-btn, .ok-button
`, datePanel);
if (okButton) {
okButton.click();
log('点击确定按钮确认时间选择');
} else {
// 如果没有找到确定按钮,尝试按回车键
const activeElement = document.activeElement;
if (activeElement) {
activeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
activeElement.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true }));
log('按回车键确认时间选择');
}
}
await delay(action.delay !== undefined ? action.delay : 800);
log('时间范围选择完成');
},
async antCalendar(action, frameDoc, results) {
// 专门处理 Ant Design 日历组件的动作
const el = safeQuerySelector(action.selector, frameDoc);
if (!el) throw new Error(`选择器 ${action.selector} 未找到`);
log('执行 Ant Design 日历操作:', action.selector, action.value);
// 确保传入的值是数组格式
if (!Array.isArray(action.value) || action.value.length !== 2) {
throw new Error('Ant Design 日历值必须是包含两个日期的数组');
}
const [startDate, endDate] = action.value;
// 转换中文日期格式为标准格式
const convertChineseDate = (chineseDate) => {
// 处理 'YYYY年MM月DD日' 格式
const match = chineseDate.match(/(\d{4})年(\d{1,2})月(\d{1,2})日/);
if (match) {
const year = match[1];
const month = match[2].padStart(2, '0');
const day = match[3].padStart(2, '0');
return `${year}-${month}-${day}`;
}
return chineseDate; // 如果不是中文格式,直接返回
};
const startDateStandard = convertChineseDate(startDate);
const endDateStandard = convertChineseDate(endDate);
log('转换后的日期:', { startDate: startDateStandard, endDate: endDateStandard });
// 点击日历组件打开面板
el.click();
await delay(action.delay !== undefined ? action.delay : 800);
// 查找 Ant Design 日历面板
const calendarPanel = safeQuerySelector(`
.ant-calendar-panel, .ant-calendar-dropdown, .ant-calendar-picker-panel,
.ant-picker-dropdown, .ant-picker-panel, .ant-picker-panel-container
`, frameDoc);
if (!calendarPanel) {
throw new Error('未找到 Ant Design 日历面板');
}
log('找到日历面板:', calendarPanel.className);
// 查找日历主体
const calendarBody = safeQuerySelector(`
.ant-calendar-body, .ant-picker-body, .ant-picker-content
`, calendarPanel);
if (!calendarBody) {
throw new Error('未找到日历主体');
}
// 辅助函数:在日历中查找并点击日期
const findAndClickDateInCalendar = async (targetDate, isStartDate = true) => {
const maxAttempts = 240; // 最多翻24页(2年)
let attempts = 0;
// 生成中文格式的 title 属性(不包含前导零)
const targetDateObj = new Date(targetDate);
const year = targetDateObj.getFullYear();
const month = targetDateObj.getMonth() + 1; // 月份从0开始
const day = targetDateObj.getDate();
const chineseTitle = `${year}年${month}月${day}日`;
log(`开始查找${isStartDate ? '开始' : '结束'}日期: ${chineseTitle} (${targetDate})`);
while (attempts < maxAttempts) {
log(`查找日期单元格: ${chineseTitle} (${targetDate}) - 第${attempts + 1}次尝试`);
// 查找日期单元格 - 支持多种 Ant Design 选择器
const dateCell = safeQuerySelector(`
td[title="${chineseTitle}"], td[title="${targetDate}"],
.ant-calendar-cell[title="${chineseTitle}"], .ant-calendar-cell[title="${targetDate}"],
.ant-calendar-date[title="${chineseTitle}"], .ant-calendar-date[title="${targetDate}"],
.ant-picker-cell[title="${chineseTitle}"], .ant-picker-cell[title="${targetDate}"],
[data-value="${targetDate}"], [data-date="${targetDate}"]
`, calendarBody);
if (dateCell) {
// 检查是否被禁用
if (dateCell.classList.contains('ant-calendar-disabled-cell') ||
dateCell.classList.contains('ant-picker-cell-disabled') ||
dateCell.hasAttribute('disabled')) {
log(`日期单元格被禁用: ${chineseTitle}`);
return false;
}
// 点击日期单元格
dateCell.click();
log(`${isStartDate ? '点击开始日期' : '点击结束日期'}: ${chineseTitle}`);
await delay(action.delay !== undefined ? action.delay : 800);
return true;
}
// 如果找不到日期,需要翻页
log(`未找到日期 ${chineseTitle},尝试翻页`);
// 获取当前显示的月份信息
const currentMonthElement = safeQuerySelector(`
.ant-calendar-header .ant-calendar-header-view,
.ant-picker-header-view,
.ant-calendar-header-view
`, calendarPanel);
if (currentMonthElement) {
const currentText = currentMonthElement.textContent.trim();
log('当前显示月份:', currentText);
// 解析当前月份
let currentDate;
if (currentText.includes('年') && currentText.includes('月')) {
// 中文格式:2024年7月
const match = currentText.match(/(\d{4})年(\d{1,2})月/);
if (match) {
currentDate = new Date(parseInt(match[1]), parseInt(match[2]) - 1, 1);
}
} else {
// 英文格式或其他格式
currentDate = new Date(currentText + ' 1');
}
if (currentDate && !isNaN(currentDate.getTime())) {
// 比较目标日期与当前显示月份
const targetMonth = new Date(targetDateObj.getFullYear(), targetDateObj.getMonth(), 1);
log('目标月份:', targetMonth.toISOString().slice(0, 7));
log('当前显示月份:', currentDate.toISOString().slice(0, 7));
// 查找翻页按钮
const nextButton = safeQuerySelector(`
.ant-calendar-next-month-btn, .ant-calendar-next-icon,
.ant-picker-header-next-btn, .ant-picker-next-icon,
.ant-calendar-header-next-btn, .ant-calendar-header-next
`, calendarPanel);
const prevButton = safeQuerySelector(`
.ant-calendar-prev-month-btn, .ant-calendar-prev-icon,
.ant-picker-header-prev-btn, .ant-picker-prev-icon,
.ant-calendar-header-prev-btn, .ant-calendar-header-prev
`, calendarPanel);
log('找到的翻页按钮:', { nextButton: !!nextButton, prevButton: !!prevButton });
if (targetMonth.getTime() > currentDate.getTime() && nextButton) {
// 目标月份在当前月份之后,点击下一页
nextButton.click();
log('目标月份更晚,翻到下一页');
} else if (targetMonth.getTime() < currentDate.getTime() && prevButton) {
// 目标月份在当前月份之前,点击上一页
prevButton.click();
log('目标月份更早,翻到上一页');
} else {
log(`无法找到${isStartDate ? '开始' : '结束'}日期: ${chineseTitle}`);
return false;
}
} else {
log('无法解析当前月份,尝试点击下一页');
const nextButton = safeQuerySelector(`
.ant-calendar-next-month-btn, .ant-calendar-next-icon,
.ant-picker-header-next-btn, .ant-picker-next-icon
`, calendarPanel);
if (nextButton) {
nextButton.click();
}
}
} else {
log('无法获取当前月份信息,根据目标日期判断翻页方向');
// 根据目标日期判断翻页方向
const targetYear = targetDateObj.getFullYear();
const currentYear = new Date().getFullYear();
log('目标年份:', targetYear, '当前年份:', currentYear);
const nextButton = safeQuerySelector(`
.ant-calendar-next-month-btn, .ant-calendar-next-icon,
.ant-picker-header-next-btn, .ant-picker-next-icon
`, calendarPanel);
const prevButton = safeQuerySelector(`
.ant-calendar-prev-month-btn, .ant-calendar-prev-icon,
.ant-picker-header-prev-btn, .ant-picker-prev-icon
`, calendarPanel);
if (targetYear < currentYear && prevButton) {
// 目标年份早于当前年份,向前翻页
prevButton.click();
log('目标年份更早,向前翻页');
} else if (targetYear > currentYear && nextButton) {
// 目标年份晚于当前年份,向后翻页
nextButton.click();
log('目标年份更晚,向后翻页');
} else if (nextButton) {
// 默认向后翻页
nextButton.click();
log('默认向后翻页');
}
}
await delay(500); // 等待页面更新
attempts++;
}
log(`查找${isStartDate ? '开始' : '结束'}日期超时: ${chineseTitle}`);
return false;
};
// 查找并点击开始日期
const startResult = await findAndClickDateInCalendar(startDateStandard, true);
if (!startResult) {
throw new Error(`无法找到开始日期: ${startDate}`);
}
// 对于结束日期,需要查找第二个日历面板(如果是双面板范围选择器)
log('开始查找结束日期,尝试定位第二个日历面板');
// 查找所有日历面板
const allCalendarPanels = safeQuerySelectorAll(`
.ant-calendar-panel, .ant-calendar-dropdown, .ant-calendar-picker-panel,
.ant-picker-dropdown, .ant-picker-panel, .ant-picker-panel-container
`, frameDoc);
log(`找到 ${allCalendarPanels.length} 个日历面板`);
let endResult = false;
if (allCalendarPanels.length >= 2) {
// 双面板范围选择器,在第二个面板中查找结束日期
log('检测到双面板范围选择器,在第二个面板中查找结束日期');
const secondPanel = allCalendarPanels[1];
const secondPanelBody = safeQuerySelector(`
.ant-calendar-body, .ant-picker-body, .ant-picker-content
`, secondPanel);
if (secondPanelBody) {
// 在第二个面板中查找结束日期
const findEndDateInSecondPanel = async () => {
const maxAttempts = 24;
let attempts = 0;
while (attempts < maxAttempts) {
log(`在第二个面板中查找结束日期: ${chineseTitle} - 第${attempts + 1}次尝试`);
// 在第二个面板中查找日期单元格
const dateCell = safeQuerySelector(`
td[title="${chineseTitle}"], td[title="${endDateStandard}"],
.ant-calendar-cell[title="${chineseTitle}"], .ant-calendar-cell[title="${endDateStandard}"],
.ant-calendar-date[title="${chineseTitle}"], .ant-calendar-date[title="${endDateStandard}"],
.ant-picker-cell[title="${chineseTitle}"], .ant-picker-cell[title="${endDateStandard}"],
[data-value="${endDateStandard}"], [data-date="${endDateStandard}"]
`, secondPanelBody);
if (dateCell) {
// 检查是否被禁用
if (dateCell.classList.contains('ant-calendar-disabled-cell') ||
dateCell.classList.contains('ant-picker-cell-disabled') ||
dateCell.hasAttribute('disabled')) {
log(`结束日期单元格被禁用: ${chineseTitle}`);
return false;
}
// 点击日期单元格
dateCell.click();
log(`点击结束日期: ${chineseTitle}`);
await delay(action.delay !== undefined ? action.delay : 800);
return true;
}
// 如果找不到日期,需要在第二个面板中翻页
log(`在第二个面板中未找到结束日期 ${chineseTitle},尝试翻页`);
// 获取第二个面板的当前月份信息
const currentMonthElement = safeQuerySelector(`
.ant-calendar-header .ant-calendar-header-view,
.ant-picker-header-view,
.ant-calendar-header-view
`, secondPanel);
if (currentMonthElement) {
const currentText = currentMonthElement.textContent.trim();
log('第二个面板当前显示月份:', currentText);
// 解析当前月份
let currentDate;
if (currentText.includes('年') && currentText.includes('月')) {
const match = currentText.match(/(\d{4})年(\d{1,2})月/);
if (match) {
currentDate = new Date(parseInt(match[1]), parseInt(match[2]) - 1, 1);
}
} else {
currentDate = new Date(currentText + ' 1');
}
if (currentDate && !isNaN(currentDate.getTime())) {
const targetMonth = new Date(targetDateObj.getFullYear(), targetDateObj.getMonth(), 1);
log('目标月份:', targetMonth.toISOString().slice(0, 7));
log('第二个面板当前显示月份:', currentDate.toISOString().slice(0, 7));
// 查找第二个面板的翻页按钮
const nextButton = safeQuerySelector(`
.ant-calendar-next-month-btn, .ant-calendar-next-icon,
.ant-picker-header-next-btn, .ant-picker-next-icon,
.ant-calendar-header-next-btn, .ant-calendar-header-next
`, secondPanel);
const prevButton = safeQuerySelector(`
.ant-calendar-prev-month-btn, .ant-calendar-prev-icon,
.ant-picker-header-prev-btn, .ant-picker-prev-icon,
.ant-calendar-header-prev-btn, .ant-calendar-header-prev
`, secondPanel);
log('第二个面板找到的翻页按钮:', { nextButton: !!nextButton, prevButton: !!prevButton });
if (targetMonth.getTime() > currentDate.getTime() && nextButton) {
nextButton.click();
log('目标月份更晚,在第二个面板中翻到下一页');
} else if (targetMonth.getTime() < currentDate.getTime() && prevButton) {
prevButton.click();
log('目标月份更早,在第二个面板中翻到上一页');
} else {
log(`无法在第二个面板中找到结束日期: ${chineseTitle}`);
return false;
}
} else {
log('无法解析第二个面板当前月份,尝试点击下一页');
const nextButton = safeQuerySelector(`
.ant-calendar-next-month-btn, .ant-calendar-next-icon,
.ant-picker-header-next-btn, .ant-picker-next-icon
`, secondPanel);
if (nextButton) {
nextButton.click();
}
}
} else {
log('无法获取第二个面板当前月份信息,尝试点击下一页');
const nextButton = safeQuerySelector(`
.ant-calendar-next-month-btn, .ant-calendar-next-icon,
.ant-picker-header-next-btn, .ant-picker-next-icon
`, secondPanel);
if (nextButton) {
nextButton.click();
}
}
await delay(500);
attempts++;
}
log(`在第二个面板中查找结束日期超时: ${chineseTitle}`);
return false;
};
endResult = await findEndDateInSecondPanel();
} else {
log('未找到第二个面板的主体,回退到单面板模式');
endResult = await findAndClickDateInCalendar(endDateStandard, false);
}
} else {
// 单面板模式,使用原来的方法
log('使用单面板模式查找结束日期');
endResult = await findAndClickDateInCalendar(endDateStandard, false);
}
if (!endResult) {
throw new Error(`无法找到结束日期: ${endDate}`);
}
// 点击确定按钮确认选择
const okButton = safeQuerySelector(`
.ant-picker-ok, .ant-btn-primary, button[type="button"],
.ant-calendar-ok-btn, .ant-calendar-footer-ok
`, calendarPanel);
if (okButton) {
okButton.click();
log('点击确定按钮确认日期选择');
} else {
// 如果没有找到确定按钮,尝试按回车键
const activeElement = document.activeElement;
if (activeElement) {
activeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
activeElement.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true }));
log('按回车键确认日期选择');
}
}
await delay(action.delay !== undefined ? action.delay : 800);
log('Ant Design 日历操作完成');
},
async interfaceReturn(action, frameDoc, results) {
const element = document.querySelector(action.selector);
if (element) {
const interfaceFragment = action.interfaceFragment;
// 拦截 fetch 请求
const originalFetch = window.fetch;
console.log('开始拦截 fetch 请求');
window.fetch = function (...args) {
const url = typeof args[0] === "string" ? args[0] : args[0].toString();
if (interfaceFragment && url.includes(interfaceFragment)) {
console.log('拦截到 fetch 请求:', url);
const fetchPromise = originalFetch.apply(this, args);
fetchPromise.then((response) => {
response
.clone()
.text()
.then((result) => {
console.log("fetch 接口返回:", result);
window.parent !== window && window.parent.postMessage({ type: "apiResult", result }, "*");
});
});
return fetchPromise;
}
return originalFetch.apply(this, args);
};
// 拦截 XMLHttpRequest 请求
const originalXHROpen = XMLHttpRequest.prototype.open;
const originalXHRSend = XMLHttpRequest.prototype.send;
console.log('开始拦截 XHR 请求');
XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
this._method = method;
this._url = url;
this._async = async;
this._user = user;
this._password = password;
return originalXHROpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function(data) {
if (interfaceFragment && this._url && this._url.includes(interfaceFragment)) {
console.log('拦截到 XHR 请求:', this._url);
// 监听响应
this.addEventListener('load', function() {
if (this.status >= 200 && this.status < 300) {
console.log("XHR 接口返回:", this.responseText);
window.parent !== window && window.parent.postMessage({ type: "apiResult", result: this.responseText }, "*");
}
});
this.addEventListener('error', function() {
console.log("XHR 请求失败:", this.status, this.statusText);
});
}
return originalXHRSend.apply(this, arguments);
};
element.click();
// 2秒后恢复原始方法
setTimeout(() => {
window.fetch = originalFetch;
XMLHttpRequest.prototype.open = originalXHROpen;
XMLHttpRequest.prototype.send = originalXHRSend;
console.log('恢复原始请求方法');
}, 2000);
} else {
throw new Error(`选择器 ${action.selector} 未找到`);
}
},
async waitForNext(action, frameDoc, results) {
const nextSelector = action.nextSelector;
const maxWaitTime = action.maxWaitTime || 30000; // 默认最大等待30秒
const checkInterval = action.checkInterval || 500; // 默认每秒检查一次
if (!nextSelector) {
throw new Error('waitForNext 动作需要 nextSelector 参数');
}
log('开始等待下一个元素出现:', nextSelector);
const startTime = Date.now();
let elementFound = false;
while (!elementFound && (Date.now() - startTime) < maxWaitTime) {
// 查找下一个步骤的元素
const nextElement = safeQuerySelector(nextSelector, frameDoc);
if (nextElement) {
log('找到下一个元素:', nextSelector);
elementFound = true;
break;
}
// 等待指定间隔后再次检查
await delay(checkInterval);
}
if (!elementFound) {
throw new Error(`等待超时,未找到元素: ${nextSelector}`);
}
log('等待完成,元素已出现:', nextSelector);
}
};
module.exports = actionHandlers;