Greasy Fork is available in English.
支持保存/填充各类表单数据(输入框、文本域、下拉框、单选框、复选框),支持预设规则与自定义标题,完美适配移动端。
// ==UserScript==
// @name 表单填充助手
// @namespace https://github.com/yourname/enhanced-autofill
// @version 1.0.2
// @description 支持保存/填充各类表单数据(输入框、文本域、下拉框、单选框、复选框),支持预设规则与自定义标题,完美适配移动端。
// @author kk小志
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @run-at document-end
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 防止在 iframe 中重复运行(解决页面中页面卡顿问题)
if (window.self !== window.top) {
// 在 iframe 中不执行,避免重复创建UI导致卡顿
return;
}
// ---------- 样式注入 (简洁美观、易用设计) ----------
GM_addStyle(`
:root {
--eaf-primary: #4a90d9;
--eaf-primary-hover: #3a7bc8;
--eaf-bg: #ffffff;
--eaf-surface: #f5f7fa;
--eaf-border: #e1e4e8;
--eaf-text: #24292e;
--eaf-text-secondary: #586069;
--eaf-success: #28a745;
--eaf-danger: #dc3545;
--eaf-shadow: 0 2px 8px rgba(0,0,0,0.08);
--eaf-shadow-lg: 0 4px 16px rgba(0,0,0,0.12);
}
/* 主按钮 */
#enhanced-autofill-widget {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 2147483647;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.eaf-btn {
width: 50px;
height: 50px;
border-radius: 50%;
background: #4a90d9;
color: white;
border: none;
box-shadow: 0 4px 12px rgba(74, 144, 217, 0.35);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.25s ease;
outline: none;
}
.eaf-btn:hover {
background: #3a7bc8;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(74, 144, 217, 0.45);
}
.eaf-btn:active {
transform: translateY(0);
}
.eaf-btn svg {
width: 24px;
height: 24px;
fill: none;
stroke: white;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
/* 菜单面板 */
.eaf-panel {
position: fixed;
bottom: 86px;
right: 20px;
background: var(--eaf-bg);
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
padding: 8px 0;
min-width: 200px;
z-index: 2147483647;
border: 1px solid var(--eaf-border);
font-size: 14px;
color: var(--eaf-text);
}
/* 菜单项 */
.eaf-menu-item {
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
transition: all 0.15s;
color: var(--eaf-text);
}
.eaf-menu-item:hover {
background: var(--eaf-surface);
}
.eaf-menu-item:active {
background: #e8ecf1;
}
.eaf-icon {
font-size: 18px;
width: 24px;
text-align: center;
}
/* 侧边栏 */
.eaf-sidebar {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 380px;
max-width: 100vw;
background: var(--eaf-bg);
box-shadow: -4px 0 20px rgba(0,0,0,0.1);
z-index: 2147483647;
transform: translateX(100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
font-size: 14px;
}
.eaf-sidebar.visible {
transform: translateX(0);
}
/* 侧边栏头部 */
.eaf-sidebar-header {
padding: 16px 20px;
border-bottom: 1px solid var(--eaf-border);
display: flex;
justify-content: space-between;
align-items: center;
background: #4a90d9;
color: white;
flex-shrink: 0;
}
.eaf-sidebar-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: white;
}
.eaf-sidebar-close {
background: rgba(255,255,255,0.15);
border: none;
width: 32px;
height: 32px;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
color: white;
font-size: 18px;
}
.eaf-sidebar-close:hover {
background: rgba(255,255,255,0.25);
}
/* 侧边栏内容 */
.eaf-sidebar-body {
padding: 16px 20px;
overflow-y: auto;
flex: 1;
}
.eaf-sidebar-body::-webkit-scrollbar {
width: 6px;
}
.eaf-sidebar-body::-webkit-scrollbar-track {
background: transparent;
}
.eaf-sidebar-body::-webkit-scrollbar-thumb {
background: #d1d5da;
border-radius: 3px;
}
/* 输入框 */
.eaf-input, .eaf-textarea {
width: 100%;
padding: 10px 14px;
margin: 8px 0;
background: var(--eaf-bg);
border: 1px solid var(--eaf-border);
border-radius: 8px;
color: var(--eaf-text);
font-size: 14px;
box-sizing: border-box;
transition: border-color 0.2s, box-shadow 0.2s;
}
.eaf-input:focus, .eaf-textarea:focus {
outline: none;
border-color: var(--eaf-primary);
box-shadow: 0 0 0 3px rgba(74,144,217,0.15);
}
.eaf-input::placeholder {
color: #a0a0a0;
}
/* 按钮 */
.eaf-button-row {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 16px;
}
.eaf-button {
padding: 8px 16px;
border-radius: 6px;
border: 1px solid var(--eaf-border);
background: var(--eaf-bg);
color: var(--eaf-text);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
}
.eaf-button:hover {
background: var(--eaf-surface);
border-color: #c6cbd1;
}
.eaf-button-primary {
background: var(--eaf-primary);
border-color: var(--eaf-primary);
color: white;
}
.eaf-button-primary:hover {
background: var(--eaf-primary-hover);
border-color: var(--eaf-primary-hover);
}
.eaf-button-danger {
color: var(--eaf-danger);
border-color: #f5c6cb;
}
.eaf-button-danger:hover {
background: #f8d7da;
border-color: #f1b0b7;
}
/* 列表项 */
.eaf-list-item {
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
transition: background 0.15s;
border-radius: 8px;
margin: 4px 0;
color: var(--eaf-text);
}
.eaf-list-item:hover {
background: var(--eaf-surface);
}
.eaf-title {
font-weight: 500;
flex: 1;
}
.eaf-badge {
background: #e1e4e8;
color: var(--eaf-text-secondary);
border-radius: 12px;
padding: 2px 8px;
font-size: 11px;
font-weight: 500;
}
/* 字段卡片 */
.eaf-field-row {
background: var(--eaf-surface);
border-radius: 8px;
margin: 8px 0;
padding: 12px;
border: 1px solid var(--eaf-border);
}
.eaf-field-row-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.eaf-field-row-title {
font-weight: 500;
color: var(--eaf-text);
}
.eaf-small-btn {
background: var(--eaf-bg);
border: 1px solid var(--eaf-border);
color: var(--eaf-text-secondary);
border-radius: 4px;
padding: 4px 10px;
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
}
.eaf-small-btn:hover {
background: #f0f0f0;
color: var(--eaf-text);
}
.eaf-small-btn.danger:hover {
color: var(--eaf-danger);
border-color: #f5c6cb;
background: #f8d7da;
}
/* 添加字段按钮 */
.eaf-add-field-btn {
width: 100%;
padding: 10px;
border: 2px dashed var(--eaf-border);
border-radius: 8px;
background: transparent;
color: var(--eaf-text-secondary);
cursor: pointer;
font-size: 13px;
transition: all 0.15s;
margin-top: 8px;
}
.eaf-add-field-btn:hover {
border-color: var(--eaf-primary);
color: var(--eaf-primary);
background: rgba(74,144,217,0.05);
}
/* 滚动区域 */
.eaf-scroll {
max-height: 50vh;
overflow-y: auto;
}
.eaf-scroll::-webkit-scrollbar {
width: 6px;
}
.eaf-scroll::-webkit-scrollbar-track {
background: transparent;
}
.eaf-scroll::-webkit-scrollbar-thumb {
background: #d1d5da;
border-radius: 3px;
}
/* 空状态 */
.eaf-empty {
text-align: center;
padding: 40px 20px;
color: var(--eaf-text-secondary);
}
.eaf-empty-icon {
font-size: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
/* Toast 提示 */
.eaf-toast {
position: fixed;
bottom: 90px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: #333;
color: white;
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
z-index: 2147483647;
opacity: 0;
transition: all 0.3s ease;
pointer-events: none;
}
.eaf-toast.visible {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.eaf-toast.success {
background: var(--eaf-success);
}
.eaf-toast.error {
background: var(--eaf-danger);
}
/* 分组标题 */
.eaf-group-title {
padding: 8px 16px 4px;
font-size: 12px;
font-weight: 600;
color: var(--eaf-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* 移动端适配 */
@media (max-width: 640px) {
#enhanced-autofill-widget {
bottom: 16px;
right: 16px;
}
.eaf-btn {
width: 48px;
height: 48px;
font-size: 20px;
}
.eaf-panel {
right: 12px;
bottom: 76px;
min-width: 180px;
}
.eaf-modal {
width: 92vw;
}
}
`);
// ---------- 存储 Keys ----------
const STORAGE_SAVED_FORMS = 'eaf_saved_forms';
const STORAGE_PRESETS = 'eaf_presets';
const STORAGE_DRAFTS = 'eaf_drafts';
const STORAGE_STATS = 'eaf_stats';
const STORAGE_TAGS = 'eaf_tags';
// ---------- 全局数据 ----------
let savedForms = []; // { id, title, fields, url, tags, createdAt }
let presets = []; // { id, name, fields, urlPattern }
let fillHistory = []; // 撤销用:保存填充前的状态
let currentDraft = null;
let stats = { fillCount: 0, exportCount: 0, lastUsed: null };
// ---------- 辅助函数 ----------
function loadData() {
savedForms = GM_getValue(STORAGE_SAVED_FORMS, []);
presets = GM_getValue(STORAGE_PRESETS, []);
stats = GM_getValue(STORAGE_STATS, { fillCount: 0, exportCount: 0, lastUsed: null });
}
function saveForms() {
GM_setValue(STORAGE_SAVED_FORMS, savedForms);
}
function savePresets() {
GM_setValue(STORAGE_PRESETS, presets);
}
function saveStats() {
GM_setValue(STORAGE_STATS, stats);
}
// 生成简单ID
function generateId() {
return Date.now() + '-' + Math.random().toString(36).substr(2, 6);
}
// ---------- 1. 数据导出/导入 ----------
function exportData() {
const data = {
version: '1.0',
exportTime: Date.now(),
savedForms: savedForms,
presets: presets
};
const json = JSON.stringify(data, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `表单数据_${new Date().toLocaleDateString().replace(/\//g, '-')}.json`;
a.click();
URL.revokeObjectURL(url);
stats.exportCount++;
saveStats();
showToast('导出成功', 'success');
}
function importData(file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
if (data.savedForms) {
const existingIds = new Set(savedForms.map(f => f.id));
const newForms = data.savedForms.filter(f => !existingIds.has(f.id));
savedForms.push(...newForms);
saveForms();
}
if (data.presets) {
const existingIds = new Set(presets.map(p => p.id));
const newPresets = data.presets.filter(p => !existingIds.has(p.id));
presets.push(...newPresets);
savePresets();
}
showToast(`导入成功:${data.savedForms?.length || 0}个表单,${data.presets?.length || 0}个预设`, 'success');
} catch (err) {
showToast('导入失败:文件格式错误', 'error');
}
};
reader.readAsText(file);
}
// ---------- 2. 一键撤销填充 ----------
function saveFillHistory() {
fillHistory.push(collectFormFields());
if (fillHistory.length > 10) fillHistory.shift();
}
function undoLastFill() {
if (fillHistory.length === 0) {
showToast('没有可撤销的操作', 'error');
return;
}
const previousState = fillHistory.pop();
// 使用填充函数恢复之前状态(不保存历史,允许恢复空值)
fillFormWithFields(previousState, false, true);
showToast('已撤销填充', 'success');
}
function findElementByIdentifier(identifier) {
const all = document.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]), select, textarea');
for (let el of all) {
if (getElementIdentifier(el) === identifier) return el;
}
return null;
}
// ---------- 3. 快捷键操作 ----------
function setupShortcuts() {
document.addEventListener('keydown', (e) => {
// Ctrl+Shift+F 快速填充最近一条
if (e.ctrlKey && e.shiftKey && e.key === 'F') {
e.preventDefault();
if (savedForms.length > 0) {
saveFillHistory();
fillFormWithFields(savedForms[0].fields);
stats.fillCount++;
stats.lastUsed = Date.now();
saveStats();
} else {
showToast('没有保存的表单', 'error');
}
}
// Ctrl+Shift+Z 撤销
if (e.ctrlKey && e.shiftKey && e.key === 'Z') {
e.preventDefault();
undoLastFill();
}
// Ctrl+Shift+S 快速保存
if (e.ctrlKey && e.shiftKey && e.key === 'S') {
e.preventDefault();
saveCurrentForm();
}
});
}
// ---------- 自动保存草稿(静默保存,不弹窗) ----------
let autoSaveTimer = null;
function startAutoSave() {
// 静默保存草稿到本地存储,不弹窗打扰用户
autoSaveTimer = setInterval(() => {
const fields = collectFormFields();
const hasContent = fields.some(f => f.value && String(f.value).trim() !== '');
if (hasContent) {
currentDraft = {
url: location.href,
title: document.title,
fields: fields,
savedAt: Date.now()
};
GM_setValue('draft_' + location.pathname, JSON.stringify(currentDraft));
}
}, 30000);
}
// ---------- 5. 智能推荐填充 ----------
function getRecommendedForms() {
const currentUrl = location.href;
const currentDomain = location.hostname;
return savedForms.filter(form => {
if (form.url) {
if (currentUrl.includes(form.url) || form.url.includes(currentDomain)) return true;
}
// 域名匹配
try {
const formDomain = new URL(form.url || '').hostname;
if (formDomain === currentDomain) return true;
} catch (e) {}
return false;
}).slice(0, 3);
}
// ---------- 6. 搜索功能 ----------
function searchForms(keyword) {
if (!keyword) return savedForms;
const k = keyword.toLowerCase();
return savedForms.filter(form =>
form.title.toLowerCase().includes(k) ||
form.fields.some(f => f.identifier.toLowerCase().includes(k))
);
}
function searchPresets(keyword) {
if (!keyword) return presets;
const k = keyword.toLowerCase();
return presets.filter(preset =>
preset.name.toLowerCase().includes(k) ||
preset.fields.some(f => f.identifier.toLowerCase().includes(k))
);
}
// ---------- 7. 使用统计 ----------
function getStats() {
return {
totalForms: savedForms.length,
totalPresets: presets.length,
fillCount: stats.fillCount,
exportCount: stats.exportCount,
lastUsed: stats.lastUsed ? new Date(stats.lastUsed).toLocaleString() : '从未'
};
}
// ========== 增强模块:异步等待机制 ==========
function waitForElement(selector, timeout = 5000, container = document) {
return new Promise((resolve, reject) => {
const element = container.querySelector(selector);
if (element) {
resolve(element);
return;
}
const observer = new MutationObserver((mutations, obs) => {
const el = container.querySelector(selector);
if (el) {
obs.disconnect();
resolve(el);
}
});
observer.observe(container === document ? document.body : container, {
childList: true,
subtree: true
});
setTimeout(() => {
observer.disconnect();
reject(new Error(`waitForElement timeout: ${selector}`));
}, timeout);
});
}
function waitForFieldUpdate(field, condition, timeout = 3000) {
return new Promise((resolve, reject) => {
if (condition(field)) {
resolve(field);
return;
}
const observer = new MutationObserver((mutations, obs) => {
if (condition(field)) {
obs.disconnect();
resolve(field);
}
});
observer.observe(field, {
attributes: true,
childList: true,
subtree: true,
characterData: true
});
setTimeout(() => {
observer.disconnect();
reject(new Error('waitForFieldUpdate timeout'));
}, timeout);
});
}
function waitFor(timeout) {
return new Promise(resolve => setTimeout(resolve, timeout));
}
// ========== 增强模块:组件类型定义 ==========
const COMPONENT_TYPES = {
STANDARD_INPUT: 'standard-input',
STANDARD_SELECT: 'standard-select',
STANDARD_TEXTAREA: 'standard-textarea',
STANDARD_RADIO: 'standard-radio',
STANDARD_CHECKBOX: 'standard-checkbox',
CONTENTEDITABLE: 'contenteditable',
ARIA_TEXTBOX: 'aria-textbox',
ARIA_COMBOBOX: 'aria-combobox',
ARIA_RADIO: 'aria-radio',
ARIA_CHECKBOX: 'aria-checkbox',
CUSTOM_INPUT: 'custom-input',
CUSTOM_RADIO: 'custom-radio',
CUSTOM_CHECKBOX: 'custom-checkbox',
ELEMENT_INPUT: 'element-input',
ELEMENT_SELECT: 'element-select',
ELEMENT_RADIO: 'element-radio',
ELEMENT_CHECKBOX: 'element-checkbox',
ELEMENT_CASCADER: 'element-cascader',
ELEMENT_DATE: 'element-date',
ANT_INPUT: 'ant-input',
ANT_SELECT: 'ant-select',
ANT_RADIO: 'ant-radio',
ANT_CHECKBOX: 'ant-checkbox',
ANT_CASCADER: 'ant-cascader',
ANT_DATE: 'ant-date',
IVIEW_INPUT: 'iview-input',
IVIEW_SELECT: 'iview-select',
IVIEW_RADIO: 'iview-radio',
IVIEW_CHECKBOX: 'iview-checkbox',
IVIEW_CASCADER: 'iview-cascader',
ADDRESS_CASCADE_SELECTS: 'address-cascade-selects',
VANT_INPUT: 'vant-input',
VANT_SELECT: 'vant-select',
VANT_RADIO: 'vant-radio',
VANT_CHECKBOX: 'vant-checkbox',
NATIVE_DATE: 'native-date',
NATIVE_TIME: 'native-time'
};
// ========== 增强模块:组件类型检测 ==========
function detectComponentType(el) {
if (!el || !el.tagName) return null;
const tag = el.tagName.toLowerCase();
const type = (el.type || '').toLowerCase();
if (tag === 'input') {
if (type === 'radio') return COMPONENT_TYPES.STANDARD_RADIO;
if (type === 'checkbox') return COMPONENT_TYPES.STANDARD_CHECKBOX;
if (type === 'date' || type === 'datetime-local') return COMPONENT_TYPES.NATIVE_DATE;
if (type === 'time') return COMPONENT_TYPES.NATIVE_TIME;
return COMPONENT_TYPES.STANDARD_INPUT;
}
if (tag === 'select') return COMPONENT_TYPES.STANDARD_SELECT;
if (tag === 'textarea') return COMPONENT_TYPES.STANDARD_TEXTAREA;
if (el.getAttribute && el.getAttribute('contenteditable') === 'true') {
return COMPONENT_TYPES.CONTENTEDITABLE;
}
const role = el.getAttribute ? el.getAttribute('role') : null;
if (role === 'textbox') return COMPONENT_TYPES.ARIA_TEXTBOX;
if (role === 'combobox') return COMPONENT_TYPES.ARIA_COMBOBOX;
if (role === 'radio') return COMPONENT_TYPES.ARIA_RADIO;
if (role === 'checkbox') return COMPONENT_TYPES.ARIA_CHECKBOX;
if (el.closest) {
if (el.closest('.el-input') && !el.closest('.el-select') && !el.closest('.el-cascader')) {
return COMPONENT_TYPES.ELEMENT_INPUT;
}
if (el.closest('.el-select')) return COMPONENT_TYPES.ELEMENT_SELECT;
if (el.closest('.el-radio')) return COMPONENT_TYPES.ELEMENT_RADIO;
if (el.closest('.el-checkbox')) return COMPONENT_TYPES.ELEMENT_CHECKBOX;
if (el.closest('.el-cascader')) return COMPONENT_TYPES.ELEMENT_CASCADER;
if (el.closest('.el-date-editor, .el-date-picker')) return COMPONENT_TYPES.ELEMENT_DATE;
if (el.closest('.ant-input') && !el.closest('.ant-select') && !el.closest('.ant-cascader')) {
return COMPONENT_TYPES.ANT_INPUT;
}
if (el.closest('.ant-select')) return COMPONENT_TYPES.ANT_SELECT;
if (el.closest('.ant-radio')) return COMPONENT_TYPES.ANT_RADIO;
if (el.closest('.ant-checkbox')) return COMPONENT_TYPES.ANT_CHECKBOX;
if (el.closest('.ant-cascader')) return COMPONENT_TYPES.ANT_CASCADER;
if (el.closest('.ant-picker')) return COMPONENT_TYPES.ANT_DATE;
if (el.closest('.ivu-input') && !el.closest('.ivu-select') && !el.closest('.ivu-cascader')) {
return COMPONENT_TYPES.IVIEW_INPUT;
}
if (el.closest('.ivu-select')) return COMPONENT_TYPES.IVIEW_SELECT;
if (el.closest('.ivu-radio')) return COMPONENT_TYPES.IVIEW_RADIO;
if (el.closest('.ivu-checkbox')) return COMPONENT_TYPES.IVIEW_CHECKBOX;
if (el.closest('.ivu-cascader')) return COMPONENT_TYPES.IVIEW_CASCADER;
if (el.closest('.van-field') && !el.closest('.van-picker')) {
return COMPONENT_TYPES.VANT_INPUT;
}
if (el.closest('.van-picker')) return COMPONENT_TYPES.VANT_SELECT;
if (el.closest('.van-radio')) return COMPONENT_TYPES.VANT_RADIO;
if (el.closest('.van-checkbox')) return COMPONENT_TYPES.VANT_CHECKBOX;
}
if (tag === 'div' || tag === 'span') {
if (el.classList && (
el.classList.contains('input') ||
el.classList.contains('textbox') ||
el.getAttribute('contenteditable') === 'true'
)) {
return COMPONENT_TYPES.CUSTOM_INPUT;
}
}
return null;
}
// ========== 增强模块:统一操作接口 ==========
const ComponentHandlers = {
[COMPONENT_TYPES.STANDARD_INPUT]: {
getValue: (el) => el.value,
setValue: (el, value) => {
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeSetter.call(el, value);
triggerEvents(el, ['input', 'change']);
}
},
[COMPONENT_TYPES.STANDARD_TEXTAREA]: {
getValue: (el) => el.value,
setValue: (el, value) => {
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
nativeSetter.call(el, value);
triggerEvents(el, ['input', 'change']);
}
},
[COMPONENT_TYPES.STANDARD_SELECT]: {
getValue: (el) => el.multiple ? Array.from(el.selectedOptions).map(o => o.value) : el.value,
setValue: (el, value) => {
if (el.multiple && Array.isArray(value)) {
Array.from(el.options).forEach(opt => {
opt.selected = value.includes(opt.value) || value.includes(opt.text);
});
} else {
el.value = value;
for (let i = 0; i < el.options.length; i++) {
if (el.options[i].value === value || el.options[i].text === value) {
el.selectedIndex = i;
break;
}
}
}
triggerEvents(el, ['change', 'input']);
}
},
[COMPONENT_TYPES.STANDARD_RADIO]: {
getValue: (el) => el.checked ? el.value : '',
setValue: (el, value) => {
if (el.value === value) {
el.checked = true;
triggerEvents(el, ['change', 'click']);
}
}
},
[COMPONENT_TYPES.STANDARD_CHECKBOX]: {
getValue: (el) => el.checked,
setValue: (el, value) => {
const shouldCheck = value === true || value === 'true' || value === el.value;
if (el.checked !== shouldCheck) {
el.checked = shouldCheck;
triggerEvents(el, ['change', 'click']);
}
}
},
[COMPONENT_TYPES.CONTENTEDITABLE]: {
getValue: (el) => el.innerText || el.textContent,
setValue: (el, value) => {
el.innerText = value;
triggerEvents(el, ['input', 'change', 'blur']);
}
},
[COMPONENT_TYPES.ARIA_TEXTBOX]: {
getValue: (el) => el.innerText || el.textContent || el.getAttribute('aria-valuenow') || '',
setValue: (el, value) => {
if (el.isContentEditable || el.getAttribute('contenteditable') === 'true') {
el.innerText = value;
} else {
el.setAttribute('aria-valuenow', value);
const input = el.querySelector('input');
if (input) input.value = value;
}
triggerEvents(el, ['input', 'change']);
}
},
[COMPONENT_TYPES.ARIA_COMBOBOX]: {
getValue: (el) => {
const input = el.querySelector('input');
return input ? input.value : el.getAttribute('aria-valuenow') || '';
},
setValue: async (el, value) => {
const input = el.querySelector('input');
if (input) {
input.value = value;
triggerEvents(input, ['input', 'change']);
}
}
},
[COMPONENT_TYPES.ARIA_RADIO]: {
getValue: (el) => {
const isChecked = el.getAttribute('aria-checked') === 'true' ||
el.classList.contains('checked') ||
el.classList.contains('selected');
return isChecked ? (el.getAttribute('data-value') || el.getAttribute('aria-label') || el.innerText) : '';
},
setValue: (el, value) => {
const itemValue = el.getAttribute('data-value') || el.getAttribute('aria-label') || el.innerText?.trim();
if (itemValue === value || fuzzyMatch(String(itemValue), String(value))) {
el.click();
el.setAttribute('aria-checked', 'true');
el.classList.add('checked', 'selected');
triggerEvents(el, ['change', 'click']);
}
}
},
[COMPONENT_TYPES.ARIA_CHECKBOX]: {
getValue: (el) => el.getAttribute('aria-checked') === 'true',
setValue: (el, value) => {
const shouldCheck = value === true || value === 'true';
if (el.getAttribute('aria-checked') !== String(shouldCheck)) {
el.click();
el.setAttribute('aria-checked', String(shouldCheck));
el.classList.toggle('checked', shouldCheck);
triggerEvents(el, ['change', 'click']);
}
}
},
[COMPONENT_TYPES.CUSTOM_INPUT]: {
getValue: (el) => el.innerText || el.textContent || el.getAttribute('data-value') || '',
setValue: (el, value) => {
if (el.isContentEditable || el.getAttribute('contenteditable') === 'true') {
el.innerText = value;
} else {
el.textContent = value;
el.setAttribute('data-value', value);
}
triggerEvents(el, ['input', 'change']);
}
},
[COMPONENT_TYPES.ELEMENT_INPUT]: {
getValue: (el) => el.value,
setValue: (el, value) => {
const container = el.closest('.el-input');
if (container) {
const vueInstance = container.__vue__ || container._vnode?.component?.proxy || container.__vueParentComponent?.proxy;
if (vueInstance) {
try {
vueInstance.$emit('update:modelValue', value);
vueInstance.$emit('input', value);
vueInstance.$emit('change', value);
} catch (e) {}
}
}
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeSetter.call(el, value);
triggerEvents(el, ['input', 'change']);
}
},
[COMPONENT_TYPES.ELEMENT_SELECT]: {
getValue: (el) => {
const container = el.closest('.el-select');
if (container) {
const vueInstance = container.__vue__ || container._vnode?.component?.proxy || container.__vueParentComponent?.proxy;
if (vueInstance && vueInstance.modelValue !== undefined) return vueInstance.modelValue;
if (vueInstance && vueInstance.value !== undefined) return vueInstance.value;
}
return el.value;
},
setValue: async (el, value) => {
const container = el.closest('.el-select');
if (!container) return;
const vueInstance = container.__vue__ || container._vnode?.component?.proxy || container.__vueParentComponent?.proxy;
if (vueInstance) {
try {
vueInstance.$emit('update:modelValue', value);
vueInstance.$emit('input', value);
vueInstance.$emit('change', value);
} catch (e) {}
}
const wrapper = container.querySelector('.el-select__wrapper, .el-input');
if (wrapper) {
wrapper.click();
await waitFor(200);
try {
const dropdown = await waitForElement('.el-select__popper, .el-select-dropdown', 2000);
const items = dropdown.querySelectorAll('.el-select-dropdown__item');
for (const item of items) {
const itemText = item.innerText?.trim();
const itemValue = item.getAttribute('data-value');
if (itemText === value || itemValue === value || itemText === String(value)) {
item.click();
await waitFor(300);
break;
}
}
} catch (e) {}
}
}
},
[COMPONENT_TYPES.ELEMENT_RADIO]: {
getValue: (el) => {
const wrapper = el.closest('.el-radio');
if (wrapper && wrapper.classList.contains('is-checked')) {
return el.value;
}
return el.checked ? el.value : '';
},
setValue: (el, value) => {
const wrapper = el.closest('.el-radio');
if (el.value === value || fuzzyMatch(el.value, String(value))) {
if (wrapper) {
wrapper.click();
} else {
el.checked = true;
triggerEvents(el, ['change', 'click']);
}
}
}
},
[COMPONENT_TYPES.ELEMENT_CHECKBOX]: {
getValue: (el) => {
const wrapper = el.closest('.el-checkbox');
return wrapper && wrapper.classList.contains('is-checked') ? true : el.checked;
},
setValue: (el, value) => {
const wrapper = el.closest('.el-checkbox');
const shouldCheck = value === true || value === 'true' || value === el.value;
if (wrapper) {
const isChecked = wrapper.classList.contains('is-checked');
if (isChecked !== shouldCheck) {
wrapper.click();
}
} else {
el.checked = shouldCheck;
triggerEvents(el, ['change', 'click']);
}
}
},
[COMPONENT_TYPES.ELEMENT_CASCADER]: {
getValue: (el) => {
const container = el.closest('.el-cascader');
if (container) {
const vueInstance = container.__vue__ || container._vnode?.component?.proxy || container.__vueParentComponent?.proxy;
if (vueInstance && vueInstance.modelValue !== undefined) return vueInstance.modelValue;
}
return el.value;
},
setValue: async (el, values) => {
const container = el.closest('.el-cascader');
if (!container) return;
const valueArray = Array.isArray(values) ? values : [values];
const vueInstance = container.__vue__ || container._vnode?.component?.proxy || container.__vueParentComponent?.proxy;
if (vueInstance) {
try {
vueInstance.$emit('update:modelValue', valueArray);
vueInstance.$emit('change', valueArray);
} catch (e) {}
}
const input = container.querySelector('.el-input__inner, input');
if (input) {
input.click();
await waitFor(200);
}
for (let level = 0; level < valueArray.length; level++) {
try {
const dropdown = await waitForElement('.el-cascader__dropdown, .el-cascader-menus', 2000);
const menus = dropdown.querySelectorAll('.el-cascader-menu');
const currentMenu = menus[level];
if (currentMenu) {
const items = currentMenu.querySelectorAll('.el-cascader-node');
for (const item of items) {
const itemValue = item.getAttribute('data-value') || item.innerText?.trim();
if (itemValue === valueArray[level] || fuzzyMatch(itemValue, String(valueArray[level]))) {
item.click();
await waitFor(300);
break;
}
}
}
} catch (e) {
console.debug('[Autofill] Cascader level error:', level, e);
}
}
}
},
[COMPONENT_TYPES.ANT_INPUT]: {
getValue: (el) => el.value,
setValue: (el, value) => {
const container = el.closest('.ant-input');
if (container) {
const vueInstance = container.__vue__ || container._vnode?.component?.proxy || container.__vueParentComponent?.proxy;
if (vueInstance) {
try {
vueInstance.$emit('update:value', value);
vueInstance.$emit('change', { target: { value } });
} catch (e) {}
}
}
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeSetter.call(el, value);
triggerEvents(el, ['input', 'change']);
}
},
[COMPONENT_TYPES.ANT_SELECT]: {
getValue: (el) => {
const container = el.closest('.ant-select');
if (container) {
const vueInstance = container.__vue__ || container._vnode?.component?.proxy;
if (vueInstance && vueInstance.value !== undefined) return vueInstance.value;
const selectionItem = container.querySelector('.ant-select-selection-item');
if (selectionItem) return selectionItem.innerText.trim();
}
return el.value;
},
setValue: async (el, value) => {
const container = el.closest('.ant-select');
if (!container) return;
const vueInstance = container.__vue__ || container._vnode?.component?.proxy;
if (vueInstance) {
try {
vueInstance.$emit('update:value', value);
vueInstance.$emit('change', value);
} catch (e) {}
}
const selector = container.querySelector('.ant-select-selector');
if (selector) {
selector.click();
await waitFor(200);
try {
const dropdown = await waitForElement('.ant-select-dropdown:not(.ant-select-dropdown-hidden)', 2000);
const items = dropdown.querySelectorAll('.ant-select-item-option');
for (const item of items) {
if (item.innerText.trim() === value || item.getAttribute('data-value') === value) {
item.click();
await waitFor(300);
break;
}
}
} catch (e) {}
}
}
},
[COMPONENT_TYPES.ANT_RADIO]: {
getValue: (el) => {
const wrapper = el.closest('.ant-radio-wrapper');
if (wrapper && wrapper.classList.contains('ant-radio-wrapper-checked')) {
return el.value;
}
const radio = el.closest('.ant-radio');
if (radio && radio.classList.contains('ant-radio-checked')) {
return el.value;
}
return el.checked ? el.value : '';
},
setValue: (el, value) => {
const wrapper = el.closest('.ant-radio-wrapper') || el.closest('.ant-radio');
if (el.value === value || fuzzyMatch(el.value, String(value))) {
if (wrapper) {
wrapper.click();
} else {
el.checked = true;
triggerEvents(el, ['change', 'click']);
}
}
}
},
[COMPONENT_TYPES.ANT_CHECKBOX]: {
getValue: (el) => {
const wrapper = el.closest('.ant-checkbox-wrapper');
const checkbox = el.closest('.ant-checkbox');
return (wrapper && wrapper.classList.contains('ant-checkbox-wrapper-checked')) ||
(checkbox && checkbox.classList.contains('ant-checkbox-checked')) ||
el.checked;
},
setValue: (el, value) => {
const wrapper = el.closest('.ant-checkbox-wrapper') || el.closest('.ant-checkbox');
const shouldCheck = value === true || value === 'true' || value === el.value;
if (wrapper) {
const isChecked = wrapper.classList.contains('ant-checkbox-wrapper-checked') ||
wrapper.classList.contains('ant-checkbox-checked');
if (isChecked !== shouldCheck) {
wrapper.click();
}
} else {
el.checked = shouldCheck;
triggerEvents(el, ['change', 'click']);
}
}
},
[COMPONENT_TYPES.ANT_CASCADER]: {
getValue: (el) => {
const container = el.closest('.ant-cascader');
if (container) {
const vueInstance = container.__vue__ || container._vnode?.component?.proxy;
if (vueInstance && vueInstance.value !== undefined) return vueInstance.value;
}
return el.value;
},
setValue: async (el, values) => {
const container = el.closest('.ant-cascader');
if (!container) return;
const valueArray = Array.isArray(values) ? values : [values];
const vueInstance = container.__vue__ || container._vnode?.component?.proxy;
if (vueInstance) {
try {
vueInstance.$emit('update:value', valueArray);
vueInstance.$emit('change', valueArray);
} catch (e) {}
}
const input = container.querySelector('.ant-cascader-input, input');
if (input) {
input.click();
await waitFor(200);
}
for (let level = 0; level < valueArray.length; level++) {
try {
const dropdown = await waitForElement('.ant-cascader-dropdown:not(.ant-cascader-dropdown-hidden)', 2000);
const menus = dropdown.querySelectorAll('.ant-cascader-menu');
const currentMenu = menus[level];
if (currentMenu) {
const items = currentMenu.querySelectorAll('.ant-cascader-menu-item');
for (const item of items) {
const itemValue = item.getAttribute('data-value') || item.innerText?.trim();
if (itemValue === valueArray[level] || fuzzyMatch(itemValue, String(valueArray[level]))) {
item.click();
await waitFor(300);
break;
}
}
}
} catch (e) {
console.debug('[Autofill] Ant Cascader level error:', level, e);
}
}
}
},
[COMPONENT_TYPES.NATIVE_DATE]: {
getValue: (el) => el.value,
setValue: (el, value) => {
el.value = value;
triggerEvents(el, ['input', 'change']);
}
},
[COMPONENT_TYPES.NATIVE_TIME]: {
getValue: (el) => el.value,
setValue: (el, value) => {
el.value = value;
triggerEvents(el, ['input', 'change']);
}
},
[COMPONENT_TYPES.IVIEW_INPUT]: {
getValue: (el) => el.value,
setValue: (el, value) => {
const container = el.closest('.ivu-input');
if (container) {
const vueInstance = container.__vue__;
if (vueInstance) {
try {
vueInstance.$emit('input', value);
vueInstance.$emit('on-change', value);
} catch (e) {}
}
}
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeSetter.call(el, value);
triggerEvents(el, ['input', 'change']);
}
},
[COMPONENT_TYPES.IVIEW_SELECT]: {
getValue: (el) => {
const container = el.closest('.ivu-select');
if (container) {
const vueInstance = container.__vue__;
if (vueInstance && vueInstance.value !== undefined) return vueInstance.value;
const selected = container.querySelector('.ivu-select-selected-value');
if (selected) return selected.innerText.trim();
}
return el.value;
},
setValue: async (el, value) => {
const container = el.closest('.ivu-select');
if (!container) return;
const vueInstance = container.__vue__;
if (vueInstance) {
try {
vueInstance.$emit('input', value);
vueInstance.$emit('on-change', value);
} catch (e) {}
}
const selection = container.querySelector('.ivu-select-selection');
if (selection) {
selection.click();
await waitFor(200);
try {
const dropdown = await waitForElement('.ivu-select-dropdown:not(.ivu-select-dropdown-hidden)', 2000);
const items = dropdown.querySelectorAll('.ivu-select-item');
for (const item of items) {
if (item.innerText.trim() === value || item.getAttribute('data-value') === value) {
item.click();
await waitFor(300);
break;
}
}
} catch (e) {}
}
}
},
[COMPONENT_TYPES.IVIEW_RADIO]: {
getValue: (el) => {
const wrapper = el.closest('.ivu-radio-wrapper');
if (wrapper && wrapper.classList.contains('ivu-radio-checked')) {
return el.value;
}
return el.checked ? el.value : '';
},
setValue: (el, value) => {
const wrapper = el.closest('.ivu-radio-wrapper');
if (el.value === value || fuzzyMatch(el.value, String(value))) {
if (wrapper) {
wrapper.click();
} else {
el.checked = true;
triggerEvents(el, ['change', 'click']);
}
}
}
},
[COMPONENT_TYPES.IVIEW_CHECKBOX]: {
getValue: (el) => {
const wrapper = el.closest('.ivu-checkbox-wrapper');
return wrapper && wrapper.classList.contains('ivu-checkbox-checked') ? true : el.checked;
},
setValue: (el, value) => {
const wrapper = el.closest('.ivu-checkbox-wrapper');
const shouldCheck = value === true || value === 'true' || value === el.value;
if (wrapper) {
const isChecked = wrapper.classList.contains('ivu-checkbox-checked');
if (isChecked !== shouldCheck) {
wrapper.click();
}
} else {
el.checked = shouldCheck;
triggerEvents(el, ['change', 'click']);
}
}
},
[COMPONENT_TYPES.IVIEW_CASCADER]: {
getValue: (el) => {
const container = el.closest('.ivu-cascader');
if (container) {
const vueInstance = container.__vue__;
if (vueInstance && vueInstance.value !== undefined) return vueInstance.value;
}
return el.value;
},
setValue: async (el, values) => {
const container = el.closest('.ivu-cascader');
if (!container) return;
const valueArray = Array.isArray(values) ? values : [values];
const vueInstance = container.__vue__;
if (vueInstance) {
try {
vueInstance.$emit('input', valueArray);
vueInstance.$emit('on-change', valueArray);
} catch (e) {}
}
const input = container.querySelector('.ivu-input');
if (input) {
input.click();
await waitFor(200);
}
for (let level = 0; level < valueArray.length; level++) {
try {
const dropdown = await waitForElement('.ivu-cascader-dropdown:not(.ivu-cascader-dropdown-hidden)', 2000);
const menus = dropdown.querySelectorAll('.ivu-cascader-menu');
const currentMenu = menus[level];
if (currentMenu) {
const items = currentMenu.querySelectorAll('.ivu-cascader-menu-item');
for (const item of items) {
const itemValue = item.getAttribute('data-value') || item.innerText?.trim();
if (itemValue === valueArray[level] || fuzzyMatch(itemValue, String(valueArray[level]))) {
item.click();
await waitFor(300);
break;
}
}
}
} catch (e) {
console.debug('[Autofill] iView Cascader level error:', level, e);
}
}
}
},
[COMPONENT_TYPES.VANT_INPUT]: {
getValue: (el) => el.value,
setValue: (el, value) => {
const container = el.closest('.van-field');
if (container) {
const vueInstance = container.__vue__ || container.__vueParentComponent?.proxy;
if (vueInstance) {
try {
vueInstance.$emit('update:modelValue', value);
vueInstance.$emit('input', value);
} catch (e) {}
}
}
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeSetter.call(el, value);
triggerEvents(el, ['input', 'change']);
}
},
[COMPONENT_TYPES.VANT_SELECT]: {
getValue: (el) => {
const container = el.closest('.van-field');
if (container) {
const input = container.querySelector('.van-field__control');
return input ? input.value : '';
}
return el.value;
},
setValue: async (el, value) => {
const container = el.closest('.van-field');
if (!container) return;
const input = container.querySelector('.van-field__control');
if (input) {
input.click();
await waitFor(300);
try {
const picker = await waitForElement('.van-picker', 2000);
const columns = picker.querySelectorAll('.van-picker-column');
if (columns.length > 0) {
const items = columns[0].querySelectorAll('.van-picker-column__item');
for (const item of items) {
if (item.innerText.trim() === value || item.getAttribute('data-value') === value) {
item.click();
await waitFor(200);
break;
}
}
}
const confirmBtn = picker.querySelector('.van-picker__confirm, .van-picker__toolbar button:last-child');
if (confirmBtn) {
confirmBtn.click();
await waitFor(200);
}
} catch (e) {}
}
}
},
[COMPONENT_TYPES.VANT_RADIO]: {
getValue: (el) => {
const wrapper = el.closest('.van-radio');
if (wrapper && wrapper.classList.contains('van-radio--checked')) {
return el.value;
}
return el.checked ? el.value : '';
},
setValue: (el, value) => {
const wrapper = el.closest('.van-radio');
if (el.value === value || fuzzyMatch(el.value, String(value))) {
if (wrapper) {
wrapper.click();
} else {
el.checked = true;
triggerEvents(el, ['change', 'click']);
}
}
}
},
[COMPONENT_TYPES.VANT_CHECKBOX]: {
getValue: (el) => {
const wrapper = el.closest('.van-checkbox');
return wrapper && wrapper.classList.contains('van-checkbox--checked') ? true : el.checked;
},
setValue: (el, value) => {
const wrapper = el.closest('.van-checkbox');
const shouldCheck = value === true || value === 'true' || value === el.value;
if (wrapper) {
const isChecked = wrapper.classList.contains('van-checkbox--checked');
if (isChecked !== shouldCheck) {
wrapper.click();
}
} else {
el.checked = shouldCheck;
triggerEvents(el, ['change', 'click']);
}
}
}
};
function triggerEvents(el, events) {
events.forEach(eventType => {
const event = new Event(eventType, { bubbles: true, cancelable: true });
el.dispatchEvent(event);
});
el.dispatchEvent(new FocusEvent('focus'));
el.dispatchEvent(new FocusEvent('blur'));
}
function getValue(element, type = null) {
const componentType = type || detectComponentType(element);
const handler = ComponentHandlers[componentType];
if (handler && handler.getValue) {
try {
return handler.getValue(element);
} catch (e) {
console.debug('[Autofill] getValue error:', e);
}
}
return element.value || element.innerText || '';
}
async function setValue(element, value, type = null) {
const componentType = type || detectComponentType(element);
const handler = ComponentHandlers[componentType];
if (handler && handler.setValue) {
try {
await handler.setValue(element, value);
return true;
} catch (e) {
console.debug('[Autofill] setValue error:', e);
}
}
return false;
}
// ========== 增强模块:字段发现增强 ==========
function discoverAllFields() {
const fields = [];
const processed = new Set();
const selectors = [
'input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="image"])',
'select',
'textarea',
'[contenteditable="true"]',
'[role="textbox"]',
'[role="combobox"]',
'[role="radio"]',
'[role="checkbox"]',
'.el-input input',
'.el-select',
'.el-radio',
'.el-checkbox',
'.el-cascader',
'.ant-input input',
'.ant-select',
'.ant-radio',
'.ant-checkbox',
'.ant-cascader',
'.ivu-input input',
'.ivu-select',
'.ivu-radio',
'.ivu-checkbox',
'.ivu-cascader',
'.van-field input',
'.van-radio',
'.van-checkbox'
];
const selector = selectors.join(', ');
const elements = document.querySelectorAll(selector);
elements.forEach(el => {
const componentType = detectComponentType(el);
if (!componentType) return;
const container = getComponentContainer(el, componentType);
if (container && processed.has(container)) return;
if (container) processed.add(container);
const identifier = getEnhancedIdentifier(el, componentType);
const value = getValue(el, componentType);
fields.push({
element: el,
identifier,
value,
type: componentType,
container
});
});
discoverRadioGroups(fields, processed);
discoverCheckboxGroups(fields, processed);
return fields;
}
function getComponentContainer(el, type) {
if (type.startsWith('element-') || type.startsWith('ant-')) {
return el.closest('.el-input, .el-select, .el-radio, .el-checkbox, .el-cascader, .el-date-editor, .ant-input, .ant-select, .ant-radio, .ant-checkbox, .ant-cascader, .ant-picker');
}
if (type.startsWith('iview-')) {
return el.closest('.ivu-input, .ivu-select, .ivu-radio, .ivu-checkbox, .ivu-cascader');
}
if (type.startsWith('vant-')) {
return el.closest('.van-field, .van-radio, .van-checkbox, .van-picker');
}
if (type === COMPONENT_TYPES.ARIA_RADIO || type === COMPONENT_TYPES.ARIA_CHECKBOX) {
return el.closest('[role="radiogroup"], [role="group"]') || el;
}
return el;
}
function getEnhancedIdentifier(el, type) {
const existingId = getElementIdentifier(el);
if (existingId && !existingId.startsWith('radio_group_') && !existingId.startsWith('checkbox_group_')) {
return existingId;
}
if (el.getAttribute) {
const ariaLabel = el.getAttribute('aria-label');
if (ariaLabel) return ariaLabel;
const dataName = el.getAttribute('data-name') || el.getAttribute('data-field') || el.getAttribute('data-key');
if (dataName) return dataName;
}
if (type && (type.startsWith('element-') || type.startsWith('ant-') || type.startsWith('iview-') || type.startsWith('vant-'))) {
const container = getComponentContainer(el, type);
if (container) {
const formItem = container.closest('.el-form-item, .ant-form-item, .ivu-form-item, .van-field, [class*="form-item"]');
if (formItem) {
const label = formItem.querySelector('.el-form-item__label, .ant-form-item-label label, .ivu-form-item-label, .van-field__label, label');
if (label && label.innerText.trim()) return label.innerText.trim();
}
}
}
return existingId;
}
function discoverRadioGroups(fields, processed) {
const radioGroups = new Map();
document.querySelectorAll('input[type="radio"]').forEach(radio => {
const name = radio.name;
if (!name) return;
if (!radioGroups.has(name)) {
radioGroups.set(name, []);
}
radioGroups.get(name).push(radio);
});
radioGroups.forEach((radios, name) => {
const firstRadio = radios[0];
const container = firstRadio.closest('.el-radio-group, .ant-radio-group, [role="radiogroup"]') || firstRadio.parentElement;
if (processed.has(container)) return;
processed.add(container);
const groupInfo = getRadioGroupValueAndIdentifier(radios);
const allGroups = getAllRadioGroups();
const groupIndex = allGroups.findIndex(g => g.length > 0 && g[0] === radios[0]);
fields.push({
element: firstRadio,
identifier: groupInfo.identifier,
value: groupInfo.value,
type: 'radio-group',
groupName: name,
baseIdentifier: groupInfo.baseIdentifier,
checkedIndex: groupInfo.checkedIndex,
groupIndex: groupIndex,
container
});
});
document.querySelectorAll('[role="radio"]:not(input)').forEach(radio => {
if (processed.has(radio)) return;
const group = radio.closest('[role="radiogroup"]') || radio.parentElement;
if (processed.has(group)) return;
processed.add(group);
const radios = group.querySelectorAll('[role="radio"]');
let selectedValue = '';
radios.forEach(r => {
if (r.getAttribute('aria-checked') === 'true' || r.classList.contains('checked')) {
selectedValue = r.getAttribute('data-value') || r.innerText?.trim();
}
});
const identifier = group.getAttribute('aria-label') ||
group.getAttribute('data-name') ||
getElementIdentifier(radio);
fields.push({
element: radio,
identifier,
value: selectedValue,
type: COMPONENT_TYPES.ARIA_RADIO,
container: group
});
});
}
function discoverCheckboxGroups(fields, processed) {
const checkboxGroups = new Map();
document.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
const name = checkbox.name;
if (!name) return;
const count = document.querySelectorAll(`input[type="checkbox"][name="${name}"]`).length;
if (count <= 1) return;
if (!checkboxGroups.has(name)) {
checkboxGroups.set(name, []);
}
checkboxGroups.get(name).push(checkbox);
});
checkboxGroups.forEach((checkboxes, name) => {
const firstCb = checkboxes[0];
const container = firstCb.closest('.el-checkbox-group, .ant-checkbox-group, [role="group"]') || firstCb.parentElement;
if (processed.has(container)) return;
processed.add(container);
const groupInfo = getCheckboxGroupValueAndIdentifier(checkboxes, name);
fields.push({
element: firstCb,
identifier: groupInfo.identifier,
value: groupInfo.value,
type: 'checkbox-group',
groupName: name,
container
});
});
document.querySelectorAll('[role="checkbox"]:not(input)').forEach(checkbox => {
if (processed.has(checkbox)) return;
processed.add(checkbox);
const isChecked = checkbox.getAttribute('aria-checked') === 'true' || checkbox.classList.contains('checked');
const value = checkbox.getAttribute('data-value') || checkbox.innerText?.trim();
const identifier = checkbox.getAttribute('aria-label') || getElementIdentifier(checkbox);
fields.push({
element: checkbox,
identifier,
value: isChecked ? value : '',
type: COMPONENT_TYPES.ARIA_CHECKBOX,
container: checkbox
});
});
}
// ---------- 智能提取字段标识符 (label文本/placeholder/name/id) ----------
function getElementIdentifier(el) {
// 1. 从父级form-item获取label (Element Plus/Ant Design等框架优先)
let formItem = el.closest('.el-form-item, .ant-form-item, .ivu-form-item, .van-field');
// 如果匹配到的是 __content,继续向上查找
if (formItem && formItem.className.includes('__content')) {
formItem = formItem.parentElement?.closest('.el-form-item, .ant-form-item, .ivu-form-item, .van-field');
}
if (formItem) {
const label = formItem.querySelector('.el-form-item__label, .ant-form-item-label label, .ivu-form-item-label, .van-field__label');
if (label && label.innerText.trim()) {
return label.innerText.trim();
}
}
// 2. 关联label (for 或者父级label)
let labelText = '';
if (el.id && !isDynamicId(el.id)) {
const label = document.querySelector(`label[for="${el.id}"]`);
if (label) labelText = label.innerText.trim();
}
if (!labelText && el.closest('label')) {
const parentLabel = el.closest('label');
if (parentLabel) {
const clone = parentLabel.cloneNode(true);
if (clone.querySelector('input, select, textarea')) {
clone.querySelector('input, select, textarea').remove();
}
labelText = clone.innerText.trim();
}
}
if (labelText) return labelText;
// 3. placeholder
if (el.placeholder) return el.placeholder;
// 4. aria-label
if (el.getAttribute('aria-label')) return el.getAttribute('aria-label');
// 5. name
if (el.name) return el.name;
// 6. id (排除动态生成的ID)
if (el.id && !isDynamicId(el.id)) return el.id;
// 7. 兜底
return `${el.tagName}_${el.placeholder || el.name || el.className}_${Array.from(el.parentElement?.children || []).indexOf(el)}`;
}
// 判断是否为动态生成的ID
function isDynamicId(id) {
if (!id) return false;
const dynamicPatterns = [
/^el-id-\d+-\d+$/, // Element Plus: el-id-8575-140
/^:r[0-9a-z]+:$/, // React: :r1:, :r2:
/^radix-/, // Radix UI
/-\d{4,}$/, // 以长数字结尾
/_[a-f0-9]{6,}$/i, // 以随机字符串结尾
];
return dynamicPatterns.some(p => p.test(id));
}
// 精确匹配函数(用于预设规则填充)
function fuzzyMatch(text, pattern) {
if (!text || !pattern) return false;
const t = text.toLowerCase().trim();
const p = pattern.toLowerCase().trim();
// 精确匹配
if (t === p) return true;
// 移除常见标点符号后匹配(如冒号、空格等)
const clean = (s) => s.replace(/[*::\s\n,,.。;;!!??'""''()()\[\]【】]/g, '');
const cleanT = clean(t);
const cleanP = clean(p);
if (cleanT === cleanP) return true;
return false;
}
// 获取radio组的选中值和统一标识符
function getRadioGroupValueAndIdentifier(radioGroup) {
// 优先使用原生 checked 属性
let checkedRadio = radioGroup.find(r => r.checked);
let checkedIndex = radioGroup.findIndex(r => r.checked);
// 如果没有找到 checked,检查框架特定的选中状态(Ant Design, Element UI 等)
if (!checkedRadio) {
for (let i = 0; i < radioGroup.length; i++) {
const radio = radioGroup[i];
// 方法1: 检查 input 元素的 aria-checked 属性
if (radio.getAttribute('aria-checked') === 'true') {
checkedRadio = radio;
checkedIndex = i;
break;
}
// 方法2: 检查 Ant Design 的 wrapper 类
const antWrapper = radio.closest('.ant-radio');
if (antWrapper) {
if (antWrapper.classList.contains('ant-radio-checked')) {
checkedRadio = radio;
checkedIndex = i;
break;
}
}
// 方法3: 检查 Ant Design 的 label wrapper
const antLabelWrapper = radio.closest('.ant-radio-wrapper');
if (antLabelWrapper) {
if (antLabelWrapper.classList.contains('ant-radio-wrapper-checked')) {
checkedRadio = radio;
checkedIndex = i;
break;
}
}
// 方法4: 检查 Element UI
const elWrapper = radio.closest('.el-radio');
if (elWrapper) {
const elInput = elWrapper.querySelector('.el-radio__input');
if (elInput && elInput.classList.contains('is-checked')) {
checkedRadio = radio;
checkedIndex = i;
break;
}
}
// 方法5: 检查 iView
const ivuWrapper = radio.closest('.ivu-radio-wrapper');
if (ivuWrapper) {
if (ivuWrapper.classList.contains('ivu-radio-checked')) {
checkedRadio = radio;
checkedIndex = i;
break;
}
}
// 方法6: 通用 checked 类
const genericWrapper = radio.closest('[class*="radio"]');
if (genericWrapper) {
if (genericWrapper.classList.contains('checked') ||
genericWrapper.getAttribute('aria-checked') === 'true') {
checkedRadio = radio;
checkedIndex = i;
break;
}
}
}
}
const value = checkedRadio ? checkedRadio.value : '';
let identifier = '';
let contextPath = '';
const firstRadio = radioGroup[0];
if (firstRadio) {
// 1. 优先从 fieldset 获取 legend
const fieldset = firstRadio.closest('fieldset');
if (fieldset) {
const legend = fieldset.querySelector('legend');
if (legend) identifier = legend.innerText.trim();
}
// 2. 从表单项获取 label
if (!identifier) {
const formItem = firstRadio.closest('.ant-form-item, .el-form-item, .ivu-form-item, .van-field, [class*="form-item"], [class*="form-group"]');
if (formItem) {
const labelEl = formItem.querySelector('.ant-form-item-label label, .el-form-item__label, .ivu-form-item-label, .van-field__label, label');
if (labelEl) {
identifier = labelEl.innerText.trim();
}
if (!identifier) {
const labelContainer = formItem.querySelector('.ant-form-item-label');
if (labelContainer) {
identifier = labelContainer.innerText.trim();
}
}
}
}
// 3. 从 radio group 容器获取关联 label
if (!identifier) {
const radioGroupContainer = firstRadio.closest('.ant-radio-group, .el-radio-group, .ivu-radio-group, [class*="radio-group"]');
if (radioGroupContainer && radioGroupContainer.id) {
const labelForId = document.querySelector(`label[for="${radioGroupContainer.id}"]`);
if (labelForId) identifier = labelForId.innerText.trim();
}
}
// 4. 向上查找标题/问题文本(用于问卷调查类页面)
if (!identifier) {
let parent = firstRadio.parentElement;
while (parent && parent !== document.body) {
// 查找标题元素(h1-h6、带序号的文本等)
const titleEl = parent.querySelector('h1, h2, h3, h4, h5, h6, .title, .question, [class*="title"], [class*="question"]');
if (titleEl) {
const titleText = titleEl.innerText?.trim();
if (titleText && titleText.length > 0 && titleText.length < 200) {
identifier = titleText;
break;
}
}
// 查找父元素的前置文本节点(可能是问题描述)
const prevSibling = parent.previousElementSibling;
if (prevSibling) {
const siblingText = prevSibling.innerText?.trim();
if (siblingText && siblingText.length > 0 && siblingText.length < 200 &&
(siblingText.match(/^\d+[、..]/) || siblingText.match(/^[一二三四五六七八九十]+[、..]/))) {
identifier = siblingText;
break;
}
}
parent = parent.parentElement;
}
}
// 5. 兜底:获取父容器的第一行文本
if (!identifier) {
let parent = firstRadio.parentElement;
while (parent && parent !== document.body) {
const prevText = parent.innerText.split('\n')[0]?.trim();
if (prevText && prevText.length > 0 && prevText.length < 50) {
identifier = prevText;
break;
}
parent = parent.parentElement;
}
}
if (!identifier) identifier = `radio_group_${firstRadio.name || 'unnamed'}`;
// 添加上下文索引以确保唯一性
const allRadioGroups = getAllRadioGroups();
const groupIndex = allRadioGroups.findIndex(group => group.includes(firstRadio));
if (groupIndex >= 0) {
contextPath = `_q${groupIndex}`;
}
}
const finalIdentifier = identifier + contextPath;
return { identifier: finalIdentifier, value, baseIdentifier: identifier, checkedIndex };
}
// 缓存单选框组,避免重复计算
let cachedRadioGroups = null;
let cachedRadioGroupsTime = 0;
const CACHE_DURATION = 1000; // 缓存1秒
// 获取页面中所有单选框组(带缓存)
function getAllRadioGroups() {
const now = Date.now();
if (cachedRadioGroups && (now - cachedRadioGroupsTime) < CACHE_DURATION) {
return cachedRadioGroups;
}
const groups = [];
const processedNames = new Set();
const allRadios = document.querySelectorAll('input[type="radio"]');
allRadios.forEach(radio => {
const name = radio.name;
if (!name || processedNames.has(name)) return;
processedNames.add(name);
const group = Array.from(document.querySelectorAll(`input[type="radio"][name="${name}"]`));
if (group.length > 0) {
groups.push(group);
}
});
// 处理无 name 的单选框
const noNameRadios = document.querySelectorAll('input[type="radio"]:not([name])');
const processedContainers = new Set();
noNameRadios.forEach(radio => {
const container = radio.closest('[class*="radio-group"], [role="radiogroup"]') || radio.parentElement;
if (processedContainers.has(container)) return;
processedContainers.add(container);
const group = Array.from(container.querySelectorAll('input[type="radio"]:not([name])'));
if (group.length > 0) {
groups.push(group);
}
});
cachedRadioGroups = groups;
cachedRadioGroupsTime = now;
return groups;
}
// 清除缓存(在页面变化时调用)
function clearRadioGroupsCache() {
cachedRadioGroups = null;
cachedRadioGroupsTime = 0;
}
// 获取表单项在表单中的索引位置
function getFormItemIndex(formItem) {
const allFormItems = document.querySelectorAll('.ant-form-item, .el-form-item, .ivu-form-item, .van-field, [class*="form-item"], [class*="form-group"]');
for (let i = 0; i < allFormItems.length; i++) {
if (allFormItems[i] === formItem) {
return i;
}
}
return -1;
}
// 获取单选框组在页面中的索引
function getRadioGroupIndex(radioEl) {
const name = radioEl.name;
if (!name) return -1;
const processedContainers = new Set();
const allRadios = document.querySelectorAll(`input[type="radio"][name="${name}"]`);
let groupIndex = 0;
for (const radio of allRadios) {
const container = radio.closest('[class*="radio-group"], [role="radiogroup"]') || radio.parentElement;
if (!processedContainers.has(container)) {
if (container.contains(radioEl)) {
return groupIndex;
}
processedContainers.add(container);
groupIndex++;
}
}
return -1;
}
// 获取checkbox组(同名多选)选中值数组 & 标识符
function getCheckboxGroupValueAndIdentifier(checkboxGroup, name) {
const checkedValues = checkboxGroup.filter(cb => cb.checked).map(cb => cb.value);
let identifier = '';
const firstCb = checkboxGroup[0];
if (firstCb) {
const parentLabel = firstCb.closest('label');
if (parentLabel) identifier = parentLabel.innerText.replace(/checkbox|□/i, '').trim();
if (!identifier && firstCb.closest('fieldset')) {
const legend = firstCb.closest('fieldset')?.querySelector('legend');
if (legend) identifier = legend.innerText.trim();
}
if (!identifier) {
const formItem = firstCb.closest('.ant-form-item, .el-form-item, .ivu-form-item, .van-field, [class*="form-item"], [class*="form-group"]');
if (formItem) {
const labelEl = formItem.querySelector('.ant-form-item-label label, .el-form-item__label, .ivu-form-item-label, .van-field__label, label');
if (labelEl) identifier = labelEl.innerText.trim();
}
}
if (!identifier && firstCb.id) {
const labelFor = document.querySelector(`label[for="${firstCb.id}"]`);
if (labelFor) identifier = labelFor.innerText.trim();
}
if (!identifier) {
const form = firstCb.closest('form');
if (form) {
const formLabels = [];
checkboxGroup.forEach(cb => {
if (cb.id) {
const lbl = document.querySelector(`label[for="${cb.id}"]`);
if (lbl) formLabels.push(lbl.innerText.trim());
}
});
if (formLabels.length > 0) {
identifier = formLabels[0].replace(/[我有一辆艘架台部个只]/g, '').trim() || formLabels[0];
}
}
}
if (!identifier) identifier = `checkbox_group_${name}`;
}
return { identifier, value: checkedValues };
}
// 查找单选框组的容器标识(用于没有 name 属性的自定义组件)
function findRadioGroupContainer(radioEl) {
const container = radioEl.closest('[class*="radio-group"], [role="radiogroup"], .ant-radio-group, .el-radio-group, .ivu-radio-group');
if (container) {
return container.className || container.id || `radio_container_${Array.from(container.querySelectorAll('input[type="radio"]')).map(r => r.value).join('_')}`;
}
const parent = radioEl.parentElement;
if (parent) {
const grandParent = parent.parentElement;
if (grandParent) {
const radios = grandParent.querySelectorAll('input[type="radio"]');
if (radios.length > 1) {
return `parent_${grandParent.className || grandParent.tagName}_${radios.length}`;
}
}
}
return null;
}
// 根据容器标识查找单选框组
function findRadioGroupByContainer(radioEl, containerKey) {
const container = radioEl.closest('[class*="radio-group"], [role="radiogroup"], .ant-radio-group, .el-radio-group, .ivu-radio-group');
if (container) {
return Array.from(container.querySelectorAll('input[type="radio"]'));
}
const parent = radioEl.parentElement;
if (parent) {
const grandParent = parent.parentElement;
if (grandParent) {
return Array.from(grandParent.querySelectorAll('input[type="radio"]'));
}
}
return [radioEl];
}
// 查找复选框组的容器标识(用于没有 name 属性的自定义组件)
function findCheckboxGroupContainer(checkboxEl) {
const container = checkboxEl.closest('[class*="checkbox-group"], [role="group"], .ant-checkbox-group, .el-checkbox-group, .ivu-checkbox-group');
if (container) {
return container.className || container.id || `checkbox_container_${Array.from(container.querySelectorAll('input[type="checkbox"]')).map(c => c.value).join('_')}`;
}
const parent = checkboxEl.parentElement;
if (parent) {
const grandParent = parent.parentElement;
if (grandParent) {
const checkboxes = grandParent.querySelectorAll('input[type="checkbox"]');
if (checkboxes.length > 1) {
return `parent_${grandParent.className || grandParent.tagName}_${checkboxes.length}`;
}
}
}
return null;
}
// 根据容器标识查找复选框组
function findCheckboxGroupByContainer(checkboxEl, containerKey) {
const container = checkboxEl.closest('[class*="checkbox-group"], [role="group"], .ant-checkbox-group, .el-checkbox-group, .ivu-checkbox-group');
if (container) {
return Array.from(container.querySelectorAll('input[type="checkbox"]'));
}
const parent = checkboxEl.parentElement;
if (parent) {
const grandParent = parent.parentElement;
if (grandParent) {
return Array.from(grandParent.querySelectorAll('input[type="checkbox"]'));
}
}
return [checkboxEl];
}
// ---------- 通用下拉选择组件检测与处理 ----------
// 检测是否为自定义下拉组件(Element Plus, Ant Design, iView, 等)
function detectCustomSelect(el) {
// Element Plus el-select
const elSelect = el.closest('.el-select');
if (elSelect) {
return { type: 'element-plus', container: elSelect };
}
// Ant Design Select
const antSelect = el.closest('.ant-select');
if (antSelect) {
return { type: 'ant-design', container: antSelect };
}
// iView / View UI
const iviewSelect = el.closest('.ivu-select');
if (iviewSelect) {
return { type: 'iview', container: iviewSelect };
}
// Element UI (Vue 2)
const elUISelect = el.closest('.el-select');
if (elUISelect && !elSelect) {
return { type: 'element-ui', container: elUISelect };
}
// Vant
const vantPicker = el.closest('.van-picker') || el.closest('.van-field');
if (vantPicker && el.closest('.van-field')) {
return { type: 'vant', container: vantPicker };
}
// 通用检测:有 readonly 的 input 且父级有 select/picker/dropdown 类名
if (el.tagName === 'INPUT' && el.readOnly) {
const parent = el.parentElement;
if (parent && /select|picker|dropdown|combo/i.test(parent.className)) {
return { type: 'generic-readonly', container: parent };
}
}
return null;
}
// 从自定义下拉组件获取当前值
function getCustomSelectValue(selectInfo) {
const { type, container } = selectInfo;
try {
// Element Plus / Element UI
if (type === 'element-plus' || type === 'element-ui') {
// 尝试从 Vue 实例获取
const vueInstance = container.__vue__ || container._vnode?.component?.proxy || container.__vueParentComponent?.proxy;
if (vueInstance) {
if (vueInstance.modelValue !== undefined) return vueInstance.modelValue;
if (vueInstance.value !== undefined) return vueInstance.value;
if (vueInstance.$props?.modelValue !== undefined) return vueInstance.$props.modelValue;
if (vueInstance.$props?.value !== undefined) return vueInstance.$props.value;
}
// 从 DOM 获取显示文本
const selectedText = container.querySelector('.el-select__selected-item:not(.is-hidden) span, .el-select__tags-text, .el-input__inner');
if (selectedText && selectedText.innerText) {
return selectedText.innerText.trim();
}
// 获取 input 的值
const input = container.querySelector('input.el-input__inner, input.el-select__input');
if (input && input.value) return input.value;
return '';
}
// Ant Design
if (type === 'ant-design') {
const vueInstance = container.__vue__ || container._vnode?.component?.proxy;
if (vueInstance) {
if (vueInstance.value !== undefined) return vueInstance.value;
if (vueInstance.$props?.value !== undefined) return vueInstance.$props.value;
}
const selectionItem = container.querySelector('.ant-select-selection-item, .ant-select-selection-placeholder');
if (selectionItem) return selectionItem.innerText.trim();
return '';
}
// iView
if (type === 'iview') {
const vueInstance = container.__vue__;
if (vueInstance && vueInstance.value !== undefined) return vueInstance.value;
const selected = container.querySelector('.ivu-select-selected-value');
if (selected) return selected.innerText.trim();
return '';
}
// Vant
if (type === 'vant') {
const vueInstance = container.__vue__ || container.__vueParentComponent?.proxy;
if (vueInstance && vueInstance.value !== undefined) return vueInstance.value;
const input = container.querySelector('.van-field__control');
if (input) return input.value;
return '';
}
// 通用 readonly input
if (type === 'generic-readonly') {
const input = container.querySelector('input[readonly]');
return input ? input.value : '';
}
} catch (e) {
console.debug('[Autofill] getCustomSelectValue error:', e);
}
return '';
}
// 获取自定义下拉组件的标识符
function getCustomSelectIdentifier(selectInfo) {
const { container } = selectInfo;
// 先找关联的 label
const formItem = container.closest('.el-form-item, .ant-form-item, .ivu-form-item, .van-field');
if (formItem) {
const label = formItem.querySelector('.el-form-item__label, .ant-form-item-label label, .ivu-form-item-label, .van-field__label');
if (label && label.innerText.trim()) return label.innerText.trim();
}
// 尝试 aria-label
const ariaLabel = container.getAttribute('aria-label');
if (ariaLabel) return ariaLabel;
// 尝试 placeholder
const input = container.querySelector('input[placeholder]');
if (input && input.placeholder) return input.placeholder;
// 尝试当前显示的值或 placeholder 文本 (用于区分级联选择器)
const placeholderSpan = container.querySelector('.el-select__placeholder span, .el-select__selected-item:not(.is-hidden) span');
if (placeholderSpan && placeholderSpan.innerText) {
const text = placeholderSpan.innerText.trim();
// 如果是"请选择xxx"格式的placeholder,直接使用
if (text.startsWith('请选择') || text.includes('省') || text.includes('市') || text.includes('区') || text.includes('县')) {
return text;
}
}
// 尝试 id 关联的 label
const inputEl = container.querySelector('input[id]');
if (inputEl && inputEl.id) {
const labelEl = document.querySelector(`label[for="${inputEl.id}"]`);
if (labelEl) return labelEl.innerText.trim();
}
// 最后尝试使用父容器的类名 + 索引来区分
const parentContainer = container.closest('.address-select-container, [class*="address"], [class*="region"], [class*="area"]');
if (parentContainer) {
const selects = parentContainer.querySelectorAll('.el-select, .ant-select, .ivu-select');
const index = Array.from(selects).indexOf(container);
if (index >= 0) {
const classHint = parentContainer.className.match(/address|region|area/i);
return `${classHint ? classHint[0] : 'select'}_${index + 1}`;
}
}
return `select_${Date.now()}`;
}
// 获取自定义下拉组件的所有选项
function getCustomSelectOptions(selectInfo) {
const { type, container } = selectInfo;
const options = [];
try {
// Element Plus / Element UI - 需要打开下拉菜单才能获取选项
if (type === 'element-plus' || type === 'element-ui') {
// 尝试从 Vue 实例获取选项列表
const vueInstance = container.__vue__ || container._vnode?.component?.proxy || container.__vueParentComponent?.proxy;
if (vueInstance) {
// Element Plus
if (vueInstance.options) {
vueInstance.options.forEach(opt => {
if (typeof opt === 'object') {
options.push({ label: opt.label || opt.text, value: opt.value });
} else {
options.push({ label: String(opt), value: opt });
}
});
}
// 通过 $parent 查找
let parent = vueInstance.$parent;
while (parent && options.length === 0) {
if (parent.options) {
parent.options.forEach(opt => {
if (typeof opt === 'object') {
options.push({ label: opt.label || opt.text, value: opt.value });
} else {
options.push({ label: String(opt), value: opt });
}
});
}
parent = parent.$parent;
}
}
}
} catch (e) {
console.debug('[Autofill] getCustomSelectOptions error:', e);
}
return options;
}
// ---------- 时间选择器检测与处理 ----------
function detectDatePicker(el) {
const elDatePicker = el.closest('.el-date-editor, .el-date-picker');
if (elDatePicker) {
const input = elDatePicker.querySelector('input');
const hasTimeClass = elDatePicker.classList.contains('el-date-editor--datetime') ||
elDatePicker.querySelector('.el-input__inner')?.placeholder?.includes('时间');
return {
type: hasTimeClass ? 'element-datetime' : 'element-date',
container: elDatePicker,
input: input
};
}
const antDatePicker = el.closest('.ant-picker');
if (antDatePicker && (antDatePicker.classList.contains('ant-picker-date') || antDatePicker.classList.contains('ant-picker-datetime') || antDatePicker.classList.contains('ant-picker-time'))) {
return {
type: antDatePicker.classList.contains('ant-picker-time') ? 'ant-time' : 'ant-date',
container: antDatePicker,
input: antDatePicker.querySelector('input')
};
}
if (el.tagName === 'INPUT' && (el.type === 'date' || el.type === 'time' || el.type === 'datetime-local')) {
return {
type: 'native-' + el.type,
container: el,
input: el
};
}
return null;
}
function getDatePickerValue(pickerInfo) {
const { type, container, input } = pickerInfo;
try {
if (type.startsWith('native-')) {
return input.value;
}
if (type.startsWith('element-')) {
const vueInstance = container.__vue__ || container._vnode?.component?.proxy || container.__vueParentComponent?.proxy;
if (vueInstance) {
if (vueInstance.modelValue !== undefined) return vueInstance.modelValue;
if (vueInstance.value !== undefined) return vueInstance.value;
if (vueInstance.$props?.modelValue !== undefined) return vueInstance.$props.modelValue;
}
return input ? input.value : '';
}
if (type.startsWith('ant-')) {
const vueInstance = container.__vue__ || container._vnode?.component?.proxy;
if (vueInstance) {
if (vueInstance.value !== undefined) return vueInstance.value;
if (vueInstance.$props?.value !== undefined) return vueInstance.$props.value;
}
return input ? input.value : '';
}
} catch (e) {
console.debug('[Autofill] getDatePickerValue error:', e);
}
return '';
}
function getDatePickerIdentifier(pickerInfo) {
const { container } = pickerInfo;
const formItem = container.closest('.el-form-item, .ant-form-item, .ivu-form-item, .van-field, [class*="form-item"]');
if (formItem) {
const label = formItem.querySelector('.el-form-item__label, .ant-form-item-label label, .ivu-form-item-label, .van-field__label, label');
if (label) return label.innerText.trim();
}
const input = pickerInfo.input;
if (input) {
if (input.placeholder) return input.placeholder;
if (input.id) {
const labelEl = document.querySelector(`label[for="${input.id}"]`);
if (labelEl) return labelEl.innerText.trim();
}
}
return `datepicker_${Date.now()}`;
}
function fillDatePicker(pickerInfo, value) {
const { type, container, input } = pickerInfo;
if (!value) return false;
try {
if (type.startsWith('native-')) {
input.value = value;
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
return true;
}
if (type.startsWith('element-')) {
const vueInstance = container.__vue__ || container._vnode?.component?.proxy || container.__vueParentComponent?.proxy;
if (vueInstance) {
if (typeof vueInstance.$emit === 'function') {
vueInstance.$emit('update:modelValue', value);
vueInstance.$emit('input', value);
vueInstance.$emit('change', value);
}
if (vueInstance.$ && vueInstance.$.emit) {
vueInstance.$.emit('update:modelValue', value);
vueInstance.$.emit('change', value);
}
}
if (input) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeInputValueSetter.call(input, value);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}
return true;
}
if (type.startsWith('ant-')) {
const vueInstance = container.__vue__ || container._vnode?.component?.proxy;
if (vueInstance) {
if (typeof vueInstance.$emit === 'function') {
vueInstance.$emit('update:value', value);
vueInstance.$emit('change', value);
}
}
if (input) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeInputValueSetter.call(input, value);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}
return true;
}
} catch (e) {
console.debug('[Autofill] fillDatePicker error:', e);
}
return false;
}
// 查找页面所有时间选择器
function findAllDatePickers() {
const pickers = [];
const processed = new Set();
const selectors = [
'.el-date-editor',
'.el-date-picker',
'.ant-picker-date',
'.ant-picker-datetime',
'.ant-picker-time',
'input[type="date"]',
'input[type="time"]',
'input[type="datetime-local"]'
];
selectors.forEach(selector => {
document.querySelectorAll(selector).forEach(el => {
if (processed.has(el)) return;
const pickerInfo = detectDatePicker(el);
if (pickerInfo) {
processed.add(pickerInfo.container);
pickers.push(pickerInfo);
}
});
});
return pickers;
}
// 填充自定义下拉组件
function fillCustomSelect(selectInfo, value) {
const { type, container } = selectInfo;
if (!value && value !== 0) return Promise.resolve(false);
return new Promise((resolve) => {
try {
// Element Plus / Element UI
if (type === 'element-plus' || type === 'element-ui') {
const vueInstance = container.__vue__ || container._vnode?.component?.proxy || container.__vueParentComponent?.proxy;
if (vueInstance) {
if (typeof vueInstance.$emit === 'function') {
vueInstance.$emit('update:modelValue', value);
vueInstance.$emit('input', value);
vueInstance.$emit('change', value);
}
if (vueInstance.$ && vueInstance.$.emit) {
vueInstance.$.emit('update:modelValue', value);
vueInstance.$.emit('change', value);
}
if (vueInstance.modelValue !== undefined) {
vueInstance.modelValue = value;
}
if (vueInstance.value !== undefined) {
vueInstance.value = value;
}
}
// 模拟点击打开下拉菜单,然后点击选项
const wrapper = container.querySelector('.el-select__wrapper, .el-input');
if (wrapper) {
wrapper.click();
setTimeout(() => {
const dropdowns = document.querySelectorAll('.el-select__popper, .el-select-dropdown');
let foundAndClicked = false;
dropdowns.forEach(dropdown => {
const items = dropdown.querySelectorAll('.el-select-dropdown__item');
items.forEach(item => {
const itemText = item.innerText?.trim();
const itemValue = item.getAttribute('data-value');
if (itemText === value || itemValue === value || itemText === String(value)) {
item.click();
foundAndClicked = true;
}
});
});
// 等待选择完成后 resolve
setTimeout(() => {
resolve(true);
}, foundAndClicked ? 300 : 100);
}, 200);
} else {
const input = container.querySelector('input');
if (input) {
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}
setTimeout(() => resolve(true), 100);
}
return;
}
// Ant Design
if (type === 'ant-design') {
const vueInstance = container.__vue__ || container._vnode?.component?.proxy;
if (vueInstance) {
if (typeof vueInstance.$emit === 'function') {
vueInstance.$emit('update:value', value);
vueInstance.$emit('change', value);
}
}
const selector = container.querySelector('.ant-select-selector');
if (selector) {
selector.click();
setTimeout(() => {
const dropdowns = document.querySelectorAll('.ant-select-dropdown:not(.ant-select-dropdown-hidden)');
dropdowns.forEach(dropdown => {
const items = dropdown.querySelectorAll('.ant-select-item-option');
items.forEach(item => {
if (item.innerText.trim() === value) {
item.click();
}
});
});
setTimeout(() => resolve(true), 300);
}, 200);
} else {
setTimeout(() => resolve(true), 100);
}
return;
}
// iView
if (type === 'iview') {
const vueInstance = container.__vue__;
if (vueInstance) {
vueInstance.$emit('input', value);
vueInstance.$emit('on-change', value);
}
setTimeout(() => resolve(true), 100);
return;
}
// Vant
if (type === 'vant') {
const vueInstance = container.__vue__ || container.__vueParentComponent?.proxy;
if (vueInstance) {
if (typeof vueInstance.$emit === 'function') {
vueInstance.$emit('update:modelValue', value);
vueInstance.$emit('input', value);
}
}
setTimeout(() => resolve(true), 100);
return;
}
// 通用:直接设置 input 值
if (type === 'generic-readonly') {
const input = container.querySelector('input[readonly]');
if (input) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeInputValueSetter.call(input, value);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
setTimeout(() => resolve(true), 100);
return;
}
}
resolve(false);
} catch (e) {
console.debug('[Autofill] fillCustomSelect error:', e);
resolve(false);
}
});
}
// 判断字段是否为验证码字段
function isCaptchaField(identifier, element) {
if (!identifier) return false;
const captchaKeywords = ['验证码', 'captcha', 'code', '验证', '校验码', '图形码', '安全码', '随机码', 'verify', 'auth', 'security'];
const lowerId = identifier.toLowerCase();
const isCaptcha = captchaKeywords.some(keyword => lowerId.includes(keyword.toLowerCase()));
// 额外检查:检查元素是否有验证码相关属性
if (element && !isCaptcha) {
const placeholder = (element.placeholder || '').toLowerCase();
const name = (element.name || '').toLowerCase();
const id = (element.id || '').toLowerCase();
const className = (element.className || '').toLowerCase();
const allText = placeholder + ' ' + name + ' ' + id + ' ' + className;
return captchaKeywords.some(keyword => allText.includes(keyword.toLowerCase()));
}
return isCaptcha;
}
// 过滤掉验证码字段
function filterCaptchaFields(fields) {
return fields.filter(field => {
const isCaptcha = isCaptchaField(field.identifier, field.element);
if (isCaptcha) {
console.debug('[Autofill] 过滤验证码字段:', field.identifier);
}
return !isCaptcha;
});
}
// 收集当前页面所有表单数据 (返回fields数组)
function collectContentEditablesAndAria(fields, processedContentEditables, processedAriaComponents) {
document.querySelectorAll('[contenteditable="true"]').forEach(el => {
if (processedContentEditables.has(el)) return;
processedContentEditables.add(el);
if (el.closest('.el-input, .ant-input, .el-select, .ant-select')) return;
const identifier = getEnhancedIdentifier(el, COMPONENT_TYPES.CONTENTEDITABLE);
const value = el.innerText || el.textContent || '';
fields.push({ identifier, value, type: COMPONENT_TYPES.CONTENTEDITABLE });
});
document.querySelectorAll('[role="textbox"]:not(input):not(textarea)').forEach(el => {
if (processedAriaComponents.has(el)) return;
processedAriaComponents.add(el);
const identifier = el.getAttribute('aria-label') ||
el.getAttribute('data-name') ||
getElementIdentifier(el);
const value = el.innerText || el.textContent || el.getAttribute('aria-valuenow') || '';
fields.push({ identifier, value, type: COMPONENT_TYPES.ARIA_TEXTBOX });
});
document.querySelectorAll('[role="combobox"]:not(select)').forEach(el => {
if (processedAriaComponents.has(el)) return;
if (el.closest('.el-select, .ant-select, .el-cascader, .ant-cascader')) return;
processedAriaComponents.add(el);
const identifier = el.getAttribute('aria-label') ||
el.getAttribute('data-name') ||
getElementIdentifier(el);
const input = el.querySelector('input');
const value = input ? input.value : el.getAttribute('aria-valuenow') || '';
fields.push({ identifier, value, type: COMPONENT_TYPES.ARIA_COMBOBOX, container: el });
});
}
function collectCascaderComponents(fields, processedCascaders) {
document.querySelectorAll('.el-cascader').forEach(container => {
if (processedCascaders.has(container)) return;
processedCascaders.add(container);
const vueInstance = container.__vue__ || container._vnode?.component?.proxy || container.__vueParentComponent?.proxy;
let value = [];
if (vueInstance && vueInstance.modelValue !== undefined) {
value = Array.isArray(vueInstance.modelValue) ? vueInstance.modelValue : [vueInstance.modelValue];
}
const formItem = container.closest('.el-form-item, .ant-form-item, [class*="form-item"]');
let identifier = '级联选择器';
if (formItem) {
const label = formItem.querySelector('.el-form-item__label, .ant-form-item-label label, label');
if (label) identifier = label.innerText.trim();
}
if (container.getAttribute('aria-label')) {
identifier = container.getAttribute('aria-label');
}
fields.push({
identifier,
value,
type: COMPONENT_TYPES.ELEMENT_CASCADER,
container
});
});
document.querySelectorAll('.ant-cascader').forEach(container => {
if (processedCascaders.has(container)) return;
processedCascaders.add(container);
const vueInstance = container.__vue__ || container._vnode?.component?.proxy;
let value = [];
if (vueInstance && vueInstance.value !== undefined) {
value = Array.isArray(vueInstance.value) ? vueInstance.value : [vueInstance.value];
}
const formItem = container.closest('.ant-form-item, .el-form-item, [class*="form-item"]');
let identifier = '级联选择器';
if (formItem) {
const label = formItem.querySelector('.ant-form-item-label label, .el-form-item__label, label');
if (label) identifier = label.innerText.trim();
}
if (container.getAttribute('aria-label')) {
identifier = container.getAttribute('aria-label');
}
fields.push({
identifier,
value,
type: COMPONENT_TYPES.ANT_CASCADER,
container
});
});
document.querySelectorAll('.ivu-cascader').forEach(container => {
if (processedCascaders.has(container)) return;
processedCascaders.add(container);
const vueInstance = container.__vue__;
let value = [];
if (vueInstance && vueInstance.value !== undefined) {
value = Array.isArray(vueInstance.value) ? vueInstance.value : [vueInstance.value];
}
const formItem = container.closest('.ivu-form-item, [class*="form-item"]');
let identifier = '级联选择器';
if (formItem) {
const label = formItem.querySelector('.ivu-form-item-label, label');
if (label) identifier = label.innerText.trim();
}
if (container.getAttribute('aria-label')) {
identifier = container.getAttribute('aria-label');
}
fields.push({
identifier,
value,
type: COMPONENT_TYPES.IVIEW_CASCADER,
container
});
});
}
function collectAddressCascadeSelects(fields, processed) {
const addressContainers = document.querySelectorAll('.address-select-container, [class*="address"], [class*="region"], [class*="area"]');
addressContainers.forEach(container => {
if (processed.has(container)) return;
const selects = container.querySelectorAll('.el-select, .ant-select, .ivu-select');
if (selects.length < 2) return;
const isAddressSelect = Array.from(selects).some((select, index) => {
const placeholder = select.querySelector('.el-select__placeholder span, .ant-select-selection-placeholder, .ivu-select-placeholder');
if (!placeholder) return false;
const text = placeholder.innerText || placeholder.textContent || '';
return /省|市|区|县|城市|区县/.test(text);
});
if (!isAddressSelect) return;
processed.add(container);
const formItem = container.closest('.el-form-item, .ant-form-item, .ivu-form-item, [class*="form-item"]');
let identifier = '地址选择';
if (formItem) {
const label = formItem.querySelector('.el-form-item__label, .ant-form-item-label label, .ivu-form-item-label, label');
if (label) identifier = label.innerText.trim();
}
if (container.getAttribute('aria-label')) {
identifier = container.getAttribute('aria-label');
}
const values = [];
const selectInfos = [];
selects.forEach((select, index) => {
let currentValue = '';
let placeholder = '';
const vueInstance = select.__vue__ || select._vnode?.component?.proxy || select.__vueParentComponent?.proxy;
if (vueInstance) {
if (vueInstance.modelValue !== undefined) currentValue = vueInstance.modelValue;
else if (vueInstance.value !== undefined) currentValue = vueInstance.value;
}
const placeholderEl = select.querySelector('.el-select__placeholder span, .ant-select-selection-placeholder, .ivu-select-placeholder');
if (placeholderEl) {
placeholder = placeholderEl.innerText || placeholderEl.textContent || '';
}
const isDisabled = select.querySelector('.is-disabled, .ant-select-disabled, .ivu-select-disabled') !== null;
values.push(currentValue);
selectInfos.push({
element: select,
value: currentValue,
placeholder,
index,
isDisabled
});
});
fields.push({
identifier,
value: values,
type: COMPONENT_TYPES.ADDRESS_CASCADE_SELECTS,
container,
selectInfos,
selectCount: selects.length
});
});
}
async function fillAddressCascadeSelects(container, values) {
if (!Array.isArray(values) || values.length === 0) {
return;
}
const selects = container.querySelectorAll('.el-select, .ant-select, .ivu-select');
if (selects.length === 0) {
return;
}
for (let i = 0; i < Math.min(values.length, selects.length); i++) {
const select = selects[i];
const value = values[i];
if (!value) {
continue;
}
const wrapper = select.querySelector('.el-select__wrapper, .ant-select-selector, .ivu-select-selection');
if (!wrapper) {
continue;
}
const isDisabled = select.querySelector('.is-disabled, .ant-select-disabled, .ivu-select-disabled') !== null;
if (isDisabled) {
let waited = 0;
const maxWait = 5000;
while (waited < maxWait) {
await waitFor(200);
waited += 200;
const stillDisabled = select.querySelector('.is-disabled, .ant-select-disabled, .ivu-select-disabled') !== null;
if (!stillDisabled) {
break;
}
}
if (waited >= maxWait) {
continue;
}
}
const vueInstance = select.__vue__ || select._vnode?.component?.proxy || select.__vueParentComponent?.proxy;
if (vueInstance) {
try {
vueInstance.$emit('update:modelValue', value);
vueInstance.$emit('input', value);
vueInstance.$emit('change', value);
} catch (e) {}
}
wrapper.click();
await waitFor(300);
try {
const dropdown = await waitForElement('.el-select__popper, .el-select-dropdown, .ant-select-dropdown:not(.ant-select-dropdown-hidden), .ivu-select-dropdown', 2000);
const items = dropdown.querySelectorAll('.el-select-dropdown__item, .ant-select-item-option, .ivu-select-item');
let found = false;
for (const item of items) {
const itemText = item.innerText?.trim();
const itemValue = item.getAttribute('data-value');
if (itemText === value || itemValue === value || fuzzyMatch(itemText, String(value))) {
item.click();
found = true;
break;
}
}
if (!found) {
document.body.click();
}
await waitFor(500);
} catch (e) {
document.body.click();
}
}
}
function collectFormFields() {
const fields = [];
const processedRadios = new Set();
const processedCheckboxes = new Set();
const processedCustomSelects = new Set();
const processedDatePickers = new Set();
const processedContentEditables = new Set();
const processedAriaComponents = new Set();
const processedCascaders = new Set();
collectContentEditablesAndAria(fields, processedContentEditables, processedAriaComponents);
collectCascaderComponents(fields, processedCascaders);
collectAddressCascadeSelects(fields, processedCascaders);
const inputs = document.querySelectorAll('input:not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="image"]), select, textarea');
for (let el of inputs) {
const tag = el.tagName.toLowerCase();
const type = el.type ? el.type.toLowerCase() : '';
// ----- 时间选择器检测 (优先处理) -----
const datePickerInfo = detectDatePicker(el);
if (datePickerInfo) {
if (processedDatePickers.has(datePickerInfo.container)) continue;
processedDatePickers.add(datePickerInfo.container);
const identifier = getDatePickerIdentifier(datePickerInfo);
const value = getDatePickerValue(datePickerInfo);
fields.push({ identifier, value, type: 'date-picker', pickerType: datePickerInfo.type });
continue;
}
// ----- 自定义下拉组件检测 (优先处理) -----
const customSelectInfo = detectCustomSelect(el);
if (customSelectInfo) {
if (processedCustomSelects.has(customSelectInfo.container)) continue;
processedCustomSelects.add(customSelectInfo.container);
const identifier = getCustomSelectIdentifier(customSelectInfo);
const value = getCustomSelectValue(customSelectInfo);
fields.push({ identifier, value, type: 'custom-select', selectType: customSelectInfo.type });
continue;
}
// ----- 隐藏域 -----
if (type === 'hidden') {
const identifier = getElementIdentifier(el);
fields.push({ identifier, value: el.value, type: 'hidden' });
continue;
}
// ----- 滑块 -----
if (type === 'range') {
const identifier = getElementIdentifier(el);
fields.push({ identifier, value: el.value, type: 'range' });
continue;
}
// ----- 颜色选择器 -----
if (type === 'color') {
const identifier = getElementIdentifier(el);
fields.push({ identifier, value: el.value, type: 'color' });
continue;
}
// ----- 单选框组 -----
if (type === 'radio') {
const name = el.name;
if (name) {
if (processedRadios.has(name)) continue;
processedRadios.add(name);
const radioGroup = Array.from(document.querySelectorAll(`input[type="radio"][name="${name}"]`));
const groupInfo = getRadioGroupValueAndIdentifier(radioGroup);
// 计算组索引(在页面中的位置)
const allGroups = getAllRadioGroups();
const groupIndex = allGroups.findIndex(g =>
g.length > 0 && g[0] === radioGroup[0]
);
fields.push({
identifier: groupInfo.identifier,
value: groupInfo.value,
type: 'radio-group',
groupName: name,
baseIdentifier: groupInfo.baseIdentifier,
checkedIndex: groupInfo.checkedIndex,
groupIndex: groupIndex
});
continue;
}
const containerKey = findRadioGroupContainer(el);
if (containerKey) {
if (processedRadios.has(containerKey)) continue;
processedRadios.add(containerKey);
const radioGroup = findRadioGroupByContainer(el, containerKey);
if (radioGroup.length > 1) {
const groupInfo = getRadioGroupValueAndIdentifier(radioGroup);
// 计算组索引
const allGroups = getAllRadioGroups();
const groupIndex = allGroups.findIndex(g =>
g.length > 0 && g[0] === radioGroup[0]
);
fields.push({
identifier: groupInfo.identifier,
value: groupInfo.value,
type: 'radio-group',
containerKey: containerKey,
baseIdentifier: groupInfo.baseIdentifier,
checkedIndex: groupInfo.checkedIndex,
groupIndex: groupIndex
});
continue;
}
}
const identifier = getElementIdentifier(el);
fields.push({ identifier, value: el.checked ? el.value : '', type: 'radio' });
continue;
}
// ----- 复选框组 -----
if (type === 'checkbox') {
const name = el.name;
if (name && document.querySelectorAll(`input[type="checkbox"][name="${name}"]`).length > 1) {
if (processedCheckboxes.has(name)) continue;
processedCheckboxes.add(name);
const checkboxGroup = Array.from(document.querySelectorAll(`input[type="checkbox"][name="${name}"]`));
const { identifier, value } = getCheckboxGroupValueAndIdentifier(checkboxGroup, name);
fields.push({ identifier, value, type: 'checkbox-group', groupName: name });
continue;
}
const containerKey = findCheckboxGroupContainer(el);
if (containerKey) {
if (processedCheckboxes.has(containerKey)) continue;
processedCheckboxes.add(containerKey);
const checkboxGroup = findCheckboxGroupByContainer(el, containerKey);
if (checkboxGroup.length > 1) {
const { identifier, value } = getCheckboxGroupValueAndIdentifier(checkboxGroup, containerKey);
fields.push({ identifier, value, type: 'checkbox-group', containerKey: containerKey });
continue;
}
}
const identifier = getElementIdentifier(el);
fields.push({ identifier, value: el.checked, type: 'checkbox' });
continue;
}
// ----- 下拉框(支持多选) -----
if (tag === 'select') {
const identifier = getElementIdentifier(el);
if (el.multiple) {
// 多选下拉框:保存所有选中的值
const selectedValues = Array.from(el.selectedOptions).map(opt => opt.value);
fields.push({ identifier, value: selectedValues, type: 'select-multiple' });
} else {
fields.push({ identifier, value: el.value, type: 'select' });
}
continue;
}
// ----- 其他输入框(text, password, email, tel, url, search, number, date, time 等) -----
const identifier = getElementIdentifier(el);
let value = el.value;
fields.push({ identifier, value, type: type || 'input' });
}
// ----- 收集纯 div/span 自定义表单组件 -----
collectCustomDivComponents(fields, processedRadios, processedCheckboxes);
return fields;
}
// 收集纯 div/span 自定义表单组件(无原生 input 元素)
function collectCustomDivComponents(fields, processedRadios, processedCheckboxes) {
// 1. 收集自定义单选组件 (role=radio 或 class 包含 radio)
const customRadios = document.querySelectorAll('[role="radio"], [class*="radio"]:not(input):not(.ant-radio-input):not(.el-radio__original):not(.ivu-radio-input)');
const customRadioGroups = new Map();
customRadios.forEach(radio => {
if (radio.tagName === 'INPUT') return;
const groupContainer = radio.closest('[role="radiogroup"], [class*="radio-group"]') || radio.parentElement;
const groupKey = groupContainer.className || groupContainer.id || `custom_radio_group_${customRadioGroups.size}`;
if (!customRadioGroups.has(groupKey)) {
customRadioGroups.set(groupKey, { container: groupContainer, items: [] });
}
customRadioGroups.get(groupKey).items.push(radio);
});
customRadioGroups.forEach((group, groupKey) => {
if (processedRadios.has(groupKey)) return;
processedRadios.add(groupKey);
const identifier = getCustomRadioGroupIdentifier(group.container, group.items);
const value = getCustomRadioGroupValue(group.items);
if (group.items.length > 0) {
fields.push({
identifier,
value,
type: 'custom-radio-group',
containerKey: groupKey,
selector: generateElementSelector(group.container)
});
}
});
// 2. 收集自定义复选组件 (role=checkbox 或 class 包含 checkbox)
const customCheckboxes = document.querySelectorAll('[role="checkbox"], [class*="checkbox"]:not(input):not(.ant-checkbox-input):not(.el-checkbox__original):not(.ivu-checkbox-input)');
const customCheckboxGroups = new Map();
customCheckboxes.forEach(checkbox => {
if (checkbox.tagName === 'INPUT') return;
const groupContainer = checkbox.closest('[role="group"], [class*="checkbox-group"]') || checkbox.parentElement;
const groupKey = groupContainer.className || groupContainer.id || `custom_checkbox_group_${customCheckboxGroups.size}`;
if (!customCheckboxGroups.has(groupKey)) {
customCheckboxGroups.set(groupKey, { container: groupContainer, items: [] });
}
customCheckboxGroups.get(groupKey).items.push(checkbox);
});
customCheckboxGroups.forEach((group, groupKey) => {
if (processedCheckboxes.has(groupKey)) return;
processedCheckboxes.add(groupKey);
const identifier = getCustomCheckboxGroupIdentifier(group.container, group.items);
const value = getCustomCheckboxGroupValue(group.items);
if (group.items.length > 0) {
fields.push({
identifier,
value,
type: 'custom-checkbox-group',
containerKey: groupKey,
selector: generateElementSelector(group.container)
});
}
});
// 3. 收集自定义可点击选项 (如投票选项等)
const customOptions = document.querySelectorAll('[class*="option"][class*="active"], [class*="item"][class*="selected"], [class*="choice"][class*="active"]');
customOptions.forEach(option => {
const container = option.closest('[class*="options"], [class*="choices"], [class*="list"]');
if (container) {
const identifier = container.getAttribute('aria-label') || container.className || 'custom_options';
const value = option.getAttribute('data-value') || option.getAttribute('data-id') || option.innerText?.trim();
if (value) {
fields.push({
identifier,
value,
type: 'custom-option',
selector: generateElementSelector(option)
});
}
}
});
}
// 获取自定义单选组的标识符
function getCustomRadioGroupIdentifier(container, items) {
// 尝试从容器获取
const ariaLabel = container.getAttribute('aria-label');
if (ariaLabel) return ariaLabel;
// 尝试从表单项获取 label
const formItem = container.closest('.ant-form-item, .el-form-item, .ivu-form-item, [class*="form-item"]');
if (formItem) {
const label = formItem.querySelector('.ant-form-item-label label, .el-form-item__label, .ivu-form-item-label, label');
if (label) return label.innerText.trim();
}
// 尝试从父元素获取标题
let parent = container.parentElement;
while (parent && parent !== document.body) {
const titleEl = parent.querySelector('.title, .label, [class*="title"], [class*="label"]');
if (titleEl) {
const text = titleEl.innerText?.trim();
if (text && text.length < 50) return text;
}
parent = parent.parentElement;
}
return 'custom_radio_group';
}
// 获取自定义单选组的值
function getCustomRadioGroupValue(items) {
for (const item of items) {
const isSelected = item.classList.contains('checked') ||
item.classList.contains('selected') ||
item.classList.contains('active') ||
item.getAttribute('aria-checked') === 'true' ||
item.getAttribute('data-checked') === 'true';
if (isSelected) {
return item.getAttribute('data-value') ||
item.getAttribute('data-id') ||
item.innerText?.trim();
}
}
return '';
}
// 获取自定义复选组的标识符
function getCustomCheckboxGroupIdentifier(container, items) {
const ariaLabel = container.getAttribute('aria-label');
if (ariaLabel) return ariaLabel;
const formItem = container.closest('.ant-form-item, .el-form-item, .ivu-form-item, [class*="form-item"]');
if (formItem) {
const label = formItem.querySelector('.ant-form-item-label label, .el-form-item__label, .ivu-form-item-label, label');
if (label) return label.innerText.trim();
}
return 'custom_checkbox_group';
}
// 获取自定义复选组的值
function getCustomCheckboxGroupValue(items) {
const values = [];
for (const item of items) {
const isSelected = item.classList.contains('checked') ||
item.classList.contains('selected') ||
item.classList.contains('active') ||
item.getAttribute('aria-checked') === 'true' ||
item.getAttribute('data-checked') === 'true';
if (isSelected) {
values.push(item.getAttribute('data-value') ||
item.getAttribute('data-id') ||
item.innerText?.trim());
}
}
return values;
}
// 生成元素选择器(用于后续定位)
function generateElementSelector(el) {
if (el.id) return `#${el.id}`;
const path = [];
let current = el;
while (current && current !== document.body) {
let selector = current.tagName.toLowerCase();
if (current.className && typeof current.className === 'string') {
const classes = current.className.split(' ').filter(c => c && c.length < 30);
if (classes.length > 0) {
selector += '.' + classes.slice(0, 2).join('.');
}
}
path.unshift(selector);
current = current.parentElement;
}
return path.join(' > ');
}
// 核心填充函数: 根据fields数组填充表单
async function fillFormWithFields(fields, saveHistory = true, allowEmpty = false) {
// 保存填充前的状态(用于撤销)
if (saveHistory) {
saveFillHistory();
}
// 追踪已填充的元素,避免重复填充
const filledElements = new Set();
for (let field of fields) {
const { identifier, value, type, selectType, pickerType, groupName, containerKey, selector, baseIdentifier, checkedIndex, groupIndex } = field;
if (!identifier) continue;
// ----- 单选框组填充 (使用 groupIndex 精确匹配) -----
if (type === 'radio-group') {
fillRadioGroupPrecise(identifier, value, groupName, containerKey, baseIdentifier, checkedIndex, groupIndex);
continue;
}
// ----- 自定义 div/span 单选组填充 -----
if (type === 'custom-radio-group') {
fillCustomRadioGroup(identifier, value, containerKey, selector);
continue;
}
// ----- 自定义 div/span 复选组填充 -----
if (type === 'custom-checkbox-group') {
fillCustomCheckboxGroup(identifier, value, containerKey, selector);
continue;
}
// ----- 自定义选项填充 -----
if (type === 'custom-option') {
fillCustomOption(identifier, value, selector);
continue;
}
// ----- 复选框组填充 (优先处理,避免被单个匹配) -----
if (type === 'checkbox-group' && Array.isArray(value)) {
let targetCheckboxes = [];
if (groupName) {
targetCheckboxes = Array.from(document.querySelectorAll(`input[type="checkbox"][name="${groupName}"]`));
} else if (containerKey) {
const allCheckboxes = document.querySelectorAll('input[type="checkbox"]');
for (const cb of allCheckboxes) {
const cbContainerKey = findCheckboxGroupContainer(cb);
if (cbContainerKey === containerKey) {
targetCheckboxes.push(cb);
}
}
}
if (targetCheckboxes.length > 0) {
targetCheckboxes.forEach(cb => {
if (filledElements.has(cb)) return;
fillCheckboxElement(cb, value.includes(cb.value));
filledElements.add(cb);
});
continue;
}
}
// ----- 时间选择器填充 -----
if (type === 'date-picker') {
const datePickers = findAllDatePickers();
for (const pickerInfo of datePickers) {
const pickerIdentifier = getDatePickerIdentifier(pickerInfo);
if (fuzzyMatch(pickerIdentifier, identifier)) {
fillDatePicker(pickerInfo, value);
}
}
continue;
}
// ----- 自定义下拉组件填充 (异步等待,支持级联选择器) -----
if (type === 'custom-select') {
const customSelects = findAllCustomSelects();
for (const selectInfo of customSelects) {
// 跳过已填充的元素
if (filledElements.has(selectInfo.container)) continue;
const selectIdentifier = getCustomSelectIdentifier(selectInfo);
if (fuzzyMatch(selectIdentifier, identifier)) {
await fillCustomSelect(selectInfo, value);
filledElements.add(selectInfo.container);
break;
}
}
continue;
}
// ----- 级联选择器填充 (Element UI / Ant Design) -----
if (type === COMPONENT_TYPES.ELEMENT_CASCADER || type === COMPONENT_TYPES.ANT_CASCADER) {
const cascaderSelector = type === COMPONENT_TYPES.ELEMENT_CASCADER ? '.el-cascader' : '.ant-cascader';
const cascaders = document.querySelectorAll(cascaderSelector);
for (const cascader of cascaders) {
if (filledElements.has(cascader)) continue;
const formItem = cascader.closest('.el-form-item, .ant-form-item, [class*="form-item"]');
let cascaderIdentifier = '级联选择器';
if (formItem) {
const label = formItem.querySelector('.el-form-item__label, .ant-form-item-label label, label');
if (label) cascaderIdentifier = label.innerText.trim();
}
if (cascader.getAttribute('aria-label')) {
cascaderIdentifier = cascader.getAttribute('aria-label');
}
if (fuzzyMatch(cascaderIdentifier, identifier)) {
const handler = ComponentHandlers[type];
if (handler) {
const input = cascader.querySelector('input');
if (input) {
await handler.setValue(input, value);
filledElements.add(cascader);
break;
}
}
}
}
continue;
}
// ----- 地址级联选择器填充 (多个独立Select组合) -----
if (type === COMPONENT_TYPES.ADDRESS_CASCADE_SELECTS) {
const addressContainers = document.querySelectorAll('.address-select-container, [class*="address"], [class*="region"], [class*="area"]');
for (const container of addressContainers) {
if (filledElements.has(container)) continue;
const formItem = container.closest('.el-form-item, .ant-form-item, .ivu-form-item, [class*="form-item"]');
let containerIdentifier = '地址选择';
if (formItem) {
const label = formItem.querySelector('.el-form-item__label, .ant-form-item-label label, .ivu-form-item-label, label');
if (label) containerIdentifier = label.innerText.trim();
}
if (container.getAttribute('aria-label')) {
containerIdentifier = container.getAttribute('aria-label');
}
if (fuzzyMatch(containerIdentifier, identifier)) {
await fillAddressCascadeSelects(container, value);
filledElements.add(container);
break;
}
}
continue;
}
// ----- 地址级联选择器填充 (多个独立Select组合) -----
if (type === COMPONENT_TYPES.ADDRESS_CASCADE_SELECTS) {
const addressContainers = document.querySelectorAll('.address-select-container, [class*="address"], [class*="region"], [class*="area"]');
for (const container of addressContainers) {
if (filledElements.has(container)) continue;
const formItem = container.closest('.el-form-item, .ant-form-item, .ivu-form-item, [class*="form-item"]');
let containerIdentifier = '地址选择';
if (formItem) {
const label = formItem.querySelector('.el-form-item__label, .ant-form-item-label label, .ivu-form-item-label, label');
if (label) containerIdentifier = label.innerText.trim();
}
if (container.getAttribute('aria-label')) {
containerIdentifier = container.getAttribute('aria-label');
}
if (fuzzyMatch(containerIdentifier, identifier)) {
await fillAddressCascadeSelects(container, value);
filledElements.add(container);
break;
}
}
continue;
}
// ----- contenteditable 填充 -----
if (type === COMPONENT_TYPES.CONTENTEDITABLE || type === COMPONENT_TYPES.ARIA_TEXTBOX) {
const contentEditables = document.querySelectorAll('[contenteditable="true"], [role="textbox"]:not(input):not(textarea)');
for (const el of contentEditables) {
if (filledElements.has(el)) continue;
if (el.closest('.el-input, .ant-input, .el-select, .ant-select')) continue;
const elIdentifier = el.getAttribute('aria-label') ||
el.getAttribute('data-name') ||
getElementIdentifier(el);
if (fuzzyMatch(elIdentifier, identifier)) {
const handler = ComponentHandlers[type];
if (handler) {
await handler.setValue(el, value);
filledElements.add(el);
break;
}
}
}
continue;
}
// ----- ARIA combobox 填充 -----
if (type === COMPONENT_TYPES.ARIA_COMBOBOX) {
const comboboxes = document.querySelectorAll('[role="combobox"]:not(select)');
for (const el of comboboxes) {
if (filledElements.has(el)) continue;
if (el.closest('.el-select, .ant-select, .el-cascader, .ant-cascader')) continue;
const elIdentifier = el.getAttribute('aria-label') ||
el.getAttribute('data-name') ||
getElementIdentifier(el);
if (fuzzyMatch(elIdentifier, identifier)) {
const handler = ComponentHandlers[COMPONENT_TYPES.ARIA_COMBOBOX];
if (handler) {
await handler.setValue(el, value);
filledElements.add(el);
break;
}
}
}
continue;
}
let elements = [];
const allCandidates = Array.from(document.querySelectorAll('input:not([type="hidden"]), select, textarea'));
// 跳过空值字段(避免覆盖有效值)- 但撤销时需要恢复空值
if (!allowEmpty && (value === '' || value === null || value === undefined)) {
continue;
}
const matches = (el) => {
const elIdentifier = getElementIdentifier(el);
if (fuzzyMatch(elIdentifier, identifier)) return true;
if (el.id && !isDynamicId(el.id)) {
const labelFor = document.querySelector(`label[for="${el.id}"]`);
if (labelFor && fuzzyMatch(labelFor.innerText.trim(), identifier)) return true;
}
if (el.closest('label')) {
const labelText = el.closest('label').innerText.replace(/[*\n]/g, '').trim();
if (fuzzyMatch(labelText, identifier)) return true;
}
if (el.placeholder && fuzzyMatch(el.placeholder, identifier)) return true;
if (el.getAttribute('aria-label') && fuzzyMatch(el.getAttribute('aria-label'), identifier)) return true;
if (el.name && fuzzyMatch(el.name, identifier)) return true;
const formItem = el.closest('.el-form-item, .ant-form-item, .ivu-form-item, .van-field, [class*="form-item"], [class*="form-group"]');
if (formItem) {
const label = formItem.querySelector('.el-form-item__label, .ant-form-item-label label, .ivu-form-item-label, .van-field__label, label');
if (label && fuzzyMatch(label.innerText.trim(), identifier)) return true;
}
const customSelectInfo = detectCustomSelect(el);
if (customSelectInfo) {
const customIdentifier = getCustomSelectIdentifier(customSelectInfo);
if (fuzzyMatch(customIdentifier, identifier)) return true;
}
const datePickerInfo = detectDatePicker(el);
if (datePickerInfo) {
const pickerIdentifier = getDatePickerIdentifier(datePickerInfo);
if (fuzzyMatch(pickerIdentifier, identifier)) return true;
}
return false;
};
elements = allCandidates.filter(matches);
if (elements.length === 0) continue;
for (let el of elements) {
const datePickerInfo = detectDatePicker(el);
if (datePickerInfo) {
if (filledElements.has(datePickerInfo.container)) continue;
fillDatePicker(datePickerInfo, value);
filledElements.add(datePickerInfo.container);
continue;
}
const customSelectInfo = detectCustomSelect(el);
if (customSelectInfo) {
if (filledElements.has(customSelectInfo.container)) continue;
await fillCustomSelect(customSelectInfo, value);
filledElements.add(customSelectInfo.container);
continue;
}
// 跳过已填充的元素
if (filledElements.has(el)) continue;
filledElements.add(el);
const tag = el.tagName.toLowerCase();
const inputType = el.type ? el.type.toLowerCase() : '';
if (inputType === 'radio') {
fillRadioElement(el, value);
} else if (inputType === 'checkbox') {
if (type === 'checkbox-group' && Array.isArray(value)) {
const name = el.name;
if (name) {
const allCheckboxes = document.querySelectorAll(`input[type="checkbox"][name="${name}"]`);
allCheckboxes.forEach(cb => {
fillCheckboxElement(cb, value.includes(cb.value));
});
} else if (containerKey) {
const allCheckboxes = findCheckboxGroupByContainer(el, containerKey);
allCheckboxes.forEach(cb => {
fillCheckboxElement(cb, value.includes(cb.value));
});
} else {
fillCheckboxElement(el, value.includes(el.value));
}
} else if (type === 'checkbox') {
fillCheckboxElement(el, value === true || value === 'true');
} else if (!type || type === 'input') {
fillCheckboxElement(el, value === true || value === 'true' || value === el.value);
}
} else if (tag === 'select') {
fillNativeSelect(el, value);
} else {
fillInputElement(el, value);
}
}
}
showToast('✓ 填充完成', 'success');
}
// 精确填充单选框组
function fillRadioGroupPrecise(identifier, value, groupName, containerKey, baseIdentifier, checkedIndex, groupIndex) {
clearRadioGroupsCache();
let targetRadios = [];
const allRadioGroups = getAllRadioGroups();
// 1. 最优先:使用 groupIndex 精确匹配(最可靠)
if (groupIndex !== undefined && groupIndex >= 0 && groupIndex < allRadioGroups.length) {
targetRadios = allRadioGroups[groupIndex];
}
// 2. 使用完整标识符精确匹配
if (targetRadios.length === 0) {
for (const group of allRadioGroups) {
const groupInfo = getRadioGroupValueAndIdentifier(group);
if (groupInfo.identifier === identifier) {
targetRadios = group;
break;
}
}
}
// 3. 使用 groupName 匹配(但需要验证索引)
if (targetRadios.length === 0 && groupName) {
const groupsWithSameName = allRadioGroups.filter(group =>
group[0] && group[0].name === groupName
);
if (groupsWithSameName.length === 1) {
targetRadios = groupsWithSameName[0];
} else if (groupsWithSameName.length > 1 && checkedIndex >= 0) {
for (const group of groupsWithSameName) {
const groupInfo = getRadioGroupValueAndIdentifier(group);
if (groupInfo.checkedIndex === checkedIndex) {
targetRadios = group;
break;
}
}
}
}
// 4. 使用 containerKey 匹配
if (targetRadios.length === 0 && containerKey) {
for (const group of allRadioGroups) {
const firstRadio = group[0];
if (!firstRadio) continue;
const radioContainerKey = findRadioGroupContainer(firstRadio);
if (radioContainerKey === containerKey) {
targetRadios = group;
break;
}
}
}
// 5. 模糊匹配(最后手段)
if (targetRadios.length === 0 && baseIdentifier) {
for (const group of allRadioGroups) {
const groupInfo = getRadioGroupValueAndIdentifier(group);
if (fuzzyMatch(groupInfo.baseIdentifier, baseIdentifier)) {
targetRadios = group;
break;
}
}
}
// 填充找到的单选框
if (targetRadios.length > 0) {
// 优先使用 checkedIndex 填充(最可靠)
if (checkedIndex >= 0 && checkedIndex < targetRadios.length) {
fillRadioElementFast(targetRadios[checkedIndex]);
return;
}
// 按值匹配
for (const radio of targetRadios) {
if (radio.value === value) {
fillRadioElementFast(radio);
return;
}
}
// 模糊匹配值
for (const radio of targetRadios) {
if (fuzzyMatch(radio.value, value)) {
fillRadioElementFast(radio);
return;
}
}
}
}
// 快速填充单选框(无延迟)
function fillRadioElementFast(radioEl) {
// 对于 Ant Design 等框架,需要点击 label 或 wrapper 才能正确触发
const antWrapper = radioEl.closest('.ant-radio-wrapper');
if (antWrapper) {
antWrapper.click();
return;
}
const elWrapper = radioEl.closest('.el-radio');
if (elWrapper) {
elWrapper.click();
return;
}
const ivuWrapper = radioEl.closest('.ivu-radio-wrapper');
if (ivuWrapper) {
ivuWrapper.click();
return;
}
// 原生单选框:直接设置 checked 并触发事件
radioEl.checked = true;
radioEl.dispatchEvent(new Event('change', { bubbles: true }));
radioEl.dispatchEvent(new Event('input', { bubbles: true }));
radioEl.dispatchEvent(new Event('click', { bubbles: true }));
}
// 填充自定义 div/span 单选组 - 优化版本
function fillCustomRadioGroup(identifier, value, containerKey, selector) {
let container = null;
if (selector) {
try {
container = document.querySelector(selector);
} catch (e) {}
}
if (!container) {
const customRadios = document.querySelectorAll('[role="radio"], [class*="radio"]:not(input)');
for (const radio of customRadios) {
const groupContainer = radio.closest('[role="radiogroup"], [class*="radio-group"]') || radio.parentElement;
const groupIdentifier = getCustomRadioGroupIdentifier(groupContainer, [radio]);
if (fuzzyMatch(groupIdentifier, identifier)) {
container = groupContainer;
break;
}
}
}
if (!container) return;
const items = container.querySelectorAll('[role="radio"], [class*="radio"]:not(input)');
for (const item of items) {
const itemValue = item.getAttribute('data-value') ||
item.getAttribute('data-id') ||
item.innerText?.trim();
if (itemValue === value || fuzzyMatch(String(itemValue), String(value))) {
item.click();
item.classList.add('checked', 'selected', 'active');
item.setAttribute('aria-checked', 'true');
break;
}
}
}
// 填充自定义 div/span 复选组 - 优化版本
function fillCustomCheckboxGroup(identifier, value, containerKey, selector) {
let container = null;
if (selector) {
try {
container = document.querySelector(selector);
} catch (e) {}
}
if (!container) {
const customCheckboxes = document.querySelectorAll('[role="checkbox"], [class*="checkbox"]:not(input)');
for (const checkbox of customCheckboxes) {
const groupContainer = checkbox.closest('[role="group"], [class*="checkbox-group"]') || checkbox.parentElement;
const groupIdentifier = getCustomCheckboxGroupIdentifier(groupContainer, [checkbox]);
if (fuzzyMatch(groupIdentifier, identifier)) {
container = groupContainer;
break;
}
}
}
if (!container) return;
const items = container.querySelectorAll('[role="checkbox"], [class*="checkbox"]:not(input)');
const valueArray = Array.isArray(value) ? value : [value];
items.forEach(item => {
const itemValue = item.getAttribute('data-value') ||
item.getAttribute('data-id') ||
item.innerText?.trim();
const shouldCheck = valueArray.some(v => v === itemValue || fuzzyMatch(String(v), String(itemValue)));
const isCurrentlyChecked = item.classList.contains('checked') ||
item.classList.contains('selected') ||
item.classList.contains('active') ||
item.getAttribute('aria-checked') === 'true';
if (shouldCheck !== isCurrentlyChecked) {
item.click();
if (shouldCheck) {
item.classList.add('checked', 'selected', 'active');
item.setAttribute('aria-checked', 'true');
} else {
item.classList.remove('checked', 'selected', 'active');
item.setAttribute('aria-checked', 'false');
}
}
});
}
// 填充自定义选项 - 优化版本
function fillCustomOption(identifier, value, selector) {
const containers = document.querySelectorAll('[class*="options"], [class*="choices"], [class*="list"]');
for (const container of containers) {
const containerIdentifier = container.getAttribute('aria-label') || container.className;
if (!fuzzyMatch(containerIdentifier, identifier)) continue;
const options = container.querySelectorAll('[class*="option"], [class*="item"], [class*="choice"]');
for (const option of options) {
const optionValue = option.getAttribute('data-value') ||
option.getAttribute('data-id') ||
option.innerText?.trim();
if (optionValue === value || fuzzyMatch(String(optionValue), String(value))) {
option.click();
option.classList.add('active', 'selected');
break;
}
}
}
}
// 查找页面所有自定义下拉组件
function findAllCustomSelects() {
const selects = [];
const processed = new Set();
const selectors = [
'.el-select',
'.ant-select',
'.ivu-select',
'.van-field',
'[class*="select"]:not([class*="input"])',
'[class*="picker"]:not([class*="input"])',
'[class*="dropdown"]'
];
selectors.forEach(selector => {
document.querySelectorAll(selector).forEach(container => {
if (processed.has(container)) return;
processed.add(container);
const input = container.querySelector('input');
if (input) {
const selectInfo = detectCustomSelect(input);
if (selectInfo) {
selects.push(selectInfo);
}
}
});
});
return selects;
}
// 填充单选框 (支持各种自定义框架组件) - 优化版本,无延迟
function fillRadioElement(radioEl, value) {
// 处理空值情况(撤销时需要取消选中)
if (value === '' || value === null || value === undefined) {
if (radioEl.checked) {
radioEl.checked = false;
radioEl.dispatchEvent(new Event('change', { bubbles: true }));
radioEl.dispatchEvent(new Event('input', { bubbles: true }));
}
return;
}
if (radioEl.value !== value) return;
const customWrapper = radioEl.closest('[class*="radio"], [role="radio"]');
if (customWrapper && customWrapper !== radioEl) {
customWrapper.click();
}
radioEl.checked = true;
radioEl.dispatchEvent(new Event('change', { bubbles: true }));
radioEl.dispatchEvent(new Event('input', { bubbles: true }));
radioEl.dispatchEvent(new Event('click', { bubbles: true }));
}
// 填充复选框 (支持各种自定义框架组件) - 优化版本,无延迟
function fillCheckboxElement(checkboxEl, shouldCheck) {
// 确保 shouldCheck 是布尔值
const checkValue = shouldCheck === true || shouldCheck === 'true' || shouldCheck === true;
if (checkValue === checkboxEl.checked) return;
const customWrapper = checkboxEl.closest('[class*="checkbox"], [role="checkbox"]');
if (customWrapper && customWrapper !== checkboxEl) {
customWrapper.click();
}
checkboxEl.checked = checkValue;
checkboxEl.dispatchEvent(new Event('change', { bubbles: true }));
checkboxEl.dispatchEvent(new Event('input', { bubbles: true }));
checkboxEl.dispatchEvent(new Event('click', { bubbles: true }));
}
// 填充原生 select
function fillNativeSelect(selectEl, value) {
// 处理空值情况(撤销时需要清除选择)
if (value === '' || value === null || value === undefined) {
selectEl.selectedIndex = -1;
selectEl.value = '';
selectEl.dispatchEvent(new Event('change', { bubbles: true }));
selectEl.dispatchEvent(new Event('input', { bubbles: true }));
return;
}
selectEl.value = value;
selectEl.dispatchEvent(new Event('change', { bubbles: true }));
selectEl.dispatchEvent(new Event('input', { bubbles: true }));
const options = selectEl.options;
for (let i = 0; i < options.length; i++) {
if (options[i].value === value || options[i].text === value) {
selectEl.selectedIndex = i;
break;
}
}
}
// 填充普通输入框 (支持 React/Vue 等框架)
function fillInputElement(inputEl, value) {
// 尝试获取 Vue 实例并更新 (Vue 3 + Element Plus)
const elInput = inputEl.closest('.el-input');
if (elInput) {
// 尝试多种方式获取 Vue 实例
let vueInstance = elInput.__vue__ || elInput._vnode?.component?.proxy || elInput.__vueParentComponent?.proxy;
// 尝试从父元素获取
if (!vueInstance) {
const parent = elInput.parentElement;
if (parent) {
vueInstance = parent.__vue__ || parent._vnode?.component?.proxy || parent.__vueParentComponent?.proxy;
}
}
// 尝试从 el-form-item 获取
if (!vueInstance) {
const formItem = elInput.closest('.el-form-item');
if (formItem) {
vueInstance = formItem.__vue__ || formItem._vnode?.component?.proxy || formItem.__vueParentComponent?.proxy;
}
}
if (vueInstance) {
try {
if (typeof vueInstance.$emit === 'function') {
vueInstance.$emit('update:modelValue', value);
vueInstance.$emit('input', value);
vueInstance.$emit('change', value);
}
if (vueInstance.$ && vueInstance.$.emit) {
vueInstance.$.emit('update:modelValue', value);
vueInstance.$.emit('change', value);
}
} catch (e) {
console.debug('[Autofill] Vue emit error:', e);
}
}
}
// 尝试获取 Ant Design Vue 实例
const antInput = inputEl.closest('.ant-input');
if (antInput) {
const vueInstance = antInput.__vue__ || antInput._vnode?.component?.proxy || antInput.__vueParentComponent?.proxy;
if (vueInstance) {
try {
if (typeof vueInstance.$emit === 'function') {
vueInstance.$emit('update:value', value);
vueInstance.$emit('update:modelValue', value);
vueInstance.$emit('change', { target: { value: value } });
}
} catch (e) {
console.debug('[Autofill] Ant Design Vue emit error:', e);
}
}
}
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
if (inputEl.tagName === 'TEXTAREA' && nativeTextAreaValueSetter) {
nativeTextAreaValueSetter.call(inputEl, value);
} else if (nativeInputValueSetter) {
nativeInputValueSetter.call(inputEl, value);
} else {
inputEl.value = value;
}
inputEl.dispatchEvent(new Event('input', { bubbles: true }));
inputEl.dispatchEvent(new Event('change', { bubbles: true }));
inputEl.dispatchEvent(new FocusEvent('focus'));
inputEl.dispatchEvent(new FocusEvent('blur'));
}
// Toast 提示
function showToast(msg, type = 'info', duration = 2000) {
let toast = document.querySelector('.eaf-toast');
if (!toast) {
toast = document.createElement('div');
toast.className = 'eaf-toast';
document.body.appendChild(toast);
}
toast.textContent = msg;
toast.className = 'eaf-toast ' + type;
toast.offsetHeight;
requestAnimationFrame(() => toast.classList.add('visible'));
setTimeout(() => toast.classList.remove('visible'), duration);
}
// 保存当前表单数据(使用Element UI风格对话框)
function saveCurrentForm() {
let fields = collectFormFields();
// 过滤掉验证码字段
fields = filterCaptchaFields(fields);
if (fields.length === 0) {
showToast('未检测到表单字段(已过滤验证码)', 'error');
return;
}
const defaultTitle = `表单_${new Date().toLocaleString()}`;
// 创建遮罩层
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
z-index: 2147483646;
opacity: 0;
transition: opacity 0.3s;
`;
document.body.appendChild(overlay);
// 创建对话框
const dialog = document.createElement('div');
dialog.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.9);
width: 420px;
max-width: 90vw;
background: white;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 2147483647;
opacity: 0;
transition: all 0.3s;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
dialog.innerHTML = `
<div style="padding: 20px 20px 10px;">
<div style="font-size: 18px; font-weight: 500; color: #303133; margin-bottom: 8px;">保存表单数据</div>
<div style="font-size: 14px; color: #606266; line-height: 1.5;">
请输入此组数据的标题,方便后续识别和使用。
</div>
</div>
<div style="padding: 10px 20px;">
<input id="save-form-title" style="
width: 100%;
padding: 10px 15px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
color: #606266;
box-sizing: border-box;
outline: none;
transition: border-color 0.2s;
" placeholder="例如:登录信息" value="${defaultTitle}">
<div style="margin-top: 8px; font-size: 12px; color: #909399;">
共检测到 ${fields.length} 个字段
</div>
</div>
<div style="padding: 10px 20px 20px; text-align: right;">
<button id="cancel-save-btn" style="
padding: 9px 20px;
font-size: 14px;
border-radius: 4px;
border: 1px solid #dcdfe6;
background: white;
color: #606266;
cursor: pointer;
margin-right: 10px;
transition: all 0.2s;
">取消</button>
<button id="confirm-save-btn" style="
padding: 9px 20px;
font-size: 14px;
border-radius: 4px;
border: 1px solid #409eff;
background: #409eff;
color: white;
cursor: pointer;
transition: all 0.2s;
">保存</button>
</div>
`;
document.body.appendChild(dialog);
// 显示动画
requestAnimationFrame(() => {
overlay.style.opacity = '1';
dialog.style.opacity = '1';
dialog.style.transform = 'translate(-50%, -50%) scale(1)';
});
const titleInput = dialog.querySelector('#save-form-title');
const cancelBtn = dialog.querySelector('#cancel-save-btn');
const saveBtn = dialog.querySelector('#confirm-save-btn');
// 输入框焦点样式
titleInput.addEventListener('focus', () => {
titleInput.style.borderColor = '#409eff';
});
titleInput.addEventListener('blur', () => {
titleInput.style.borderColor = '#dcdfe6';
});
// 按钮悬停效果
cancelBtn.addEventListener('mouseenter', () => {
cancelBtn.style.color = '#409eff';
cancelBtn.style.borderColor = '#c6e2ff';
cancelBtn.style.background = '#ecf5ff';
});
cancelBtn.addEventListener('mouseleave', () => {
cancelBtn.style.color = '#606266';
cancelBtn.style.borderColor = '#dcdfe6';
cancelBtn.style.background = 'white';
});
saveBtn.addEventListener('mouseenter', () => {
saveBtn.style.background = '#66b1ff';
saveBtn.style.borderColor = '#66b1ff';
});
saveBtn.addEventListener('mouseleave', () => {
saveBtn.style.background = '#409eff';
saveBtn.style.borderColor = '#409eff';
});
const closeDialog = () => {
overlay.style.opacity = '0';
dialog.style.opacity = '0';
dialog.style.transform = 'translate(-50%, -50%) scale(0.9)';
setTimeout(() => {
overlay.remove();
dialog.remove();
}, 300);
};
cancelBtn.onclick = closeDialog;
overlay.onclick = closeDialog;
saveBtn.onclick = () => {
const title = titleInput.value.trim();
if (!title) {
titleInput.style.borderColor = '#f56c6c';
titleInput.style.boxShadow = '0 0 0 2px rgba(245,108,108,0.2)';
showToast('请输入标题', 'error');
return;
}
const newForm = {
id: generateId(),
title: title,
fields: fields,
createdAt: Date.now()
};
savedForms.unshift(newForm);
saveForms();
closeDialog();
showToast(`已保存: ${title}`, 'success');
};
titleInput.focus();
titleInput.select();
titleInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
saveBtn.click();
} else if (e.key === 'Escape') {
closeDialog();
}
});
}
// 展示填充列表 (savedForms + presets)
function showFillList() {
let content = '<div id="eaf-fill-list-container"></div>';
const sidebar = openSidebar('选择填充项', content);
const container = sidebar.querySelector('#eaf-fill-list-container');
function renderItems() {
container.innerHTML = '';
if (savedForms.length === 0 && presets.length === 0) {
container.innerHTML = `
<div class="eaf-empty">
<div class="eaf-empty-icon">📋</div>
<p>暂无填充数据</p>
<span style="font-size:13px;">请先保存表单或添加预设规则</span>
</div>`;
return;
}
if (savedForms.length) {
const groupTitle = document.createElement('div');
groupTitle.className = 'eaf-group-title';
groupTitle.innerText = '保存的表单';
container.appendChild(groupTitle);
// 按创建时间降序排序(最新的在前)
const sortedForms = [...savedForms].sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
sortedForms.forEach(form => {
const item = document.createElement('div');
item.className = 'eaf-list-item';
item.innerHTML = `
<span class="eaf-icon">📋</span>
<span class="eaf-title">${escapeHtml(form.title)}</span>
<span class="eaf-badge">${form.fields.length}项</span>
`;
item.onclick = () => {
fillFormWithFields(form.fields);
showToast('填充完成', 'success');
closeSidebar();
};
container.appendChild(item);
});
}
if (presets.length) {
const groupTitle = document.createElement('div');
groupTitle.className = 'eaf-group-title';
groupTitle.innerText = '预设规则';
container.appendChild(groupTitle);
// 按创建时间降序排序(最新的在前)
const sortedPresets = [...presets].sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
sortedPresets.forEach(preset => {
const item = document.createElement('div');
item.className = 'eaf-list-item';
item.innerHTML = `
<span class="eaf-icon">⚡</span>
<span class="eaf-title">${escapeHtml(preset.name)}</span>
<span class="eaf-badge">${preset.fields.length}项</span>
`;
item.onclick = () => {
fillFormWithFields(preset.fields);
showToast('填充完成', 'success');
closeSidebar();
};
container.appendChild(item);
});
}
}
renderItems();
}
// ---------- 预设规则管理UI ----------
function managePresets() {
let content = `
<div id="eaf-preset-list" class="eaf-scroll" style="max-height:45vh;"></div>
<button id="eaf-add-preset" class="eaf-add-field-btn" style="margin-top:12px;">+ 新增预设规则</button>
`;
const sidebar = openSidebar('预设规则', content);
const container = sidebar.querySelector('#eaf-preset-list');
const addBtn = sidebar.querySelector('#eaf-add-preset');
function renderPresetList() {
if (!container) return;
container.innerHTML = '';
if (presets.length === 0) {
container.innerHTML = `
<div class="eaf-empty">
<div class="eaf-empty-icon">⚡</div>
<p>暂无预设规则</p>
<span style="font-size:13px;">点击下方按钮添加</span>
</div>`;
return;
}
presets.forEach((preset) => {
const card = document.createElement('div');
card.className = 'eaf-field-row';
card.innerHTML = `
<div class="eaf-field-row-header">
<span class="eaf-field-row-title">${escapeHtml(preset.name)}</span>
<div>
<button class="eaf-small-btn edit-preset" data-id="${preset.id}">编辑</button>
<button class="eaf-small-btn danger del-preset" data-id="${preset.id}">删除</button>
</div>
</div>
<div style="font-size:12px; color:var(--eaf-text-secondary);">包含 ${preset.fields.length} 个字段映射</div>
`;
container.appendChild(card);
});
container.querySelectorAll('.edit-preset').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-id');
const preset = presets.find(p => p.id === id);
if (preset) editPreset(preset);
});
});
container.querySelectorAll('.del-preset').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-id');
if (confirm('确定删除此预设规则?')) {
presets = presets.filter(p => p.id !== id);
savePresets();
renderPresetList();
showToast('已删除', 'success');
}
});
});
}
function editPreset(preset = null) {
const isNew = !preset;
let fieldsArray = preset ? [...preset.fields] : [];
let content = `
<div class="eaf-field">
<label>规则名称</label>
<input id="preset-name" class="eaf-input" placeholder="例如:登录信息" value="${preset ? escapeHtml(preset.name) : ''}">
</div>
<div class="eaf-field" style="margin-top:12px;">
<label>字段映射</label>
<div id="fields-editor"></div>
<button id="add-field-btn" class="eaf-add-field-btn">+ 添加字段</button>
</div>
<div class="eaf-button-row" style="margin-top:20px;">
<button id="cancel-preset-btn" class="eaf-button">取消</button>
<button id="save-preset-btn" class="eaf-button eaf-button-primary">保存</button>
</div>
`;
const editSidebar = openSidebar(isNew ? '新建预设规则' : '编辑预设', content);
const nameInput = editSidebar.querySelector('#preset-name');
const fieldsContainer = editSidebar.querySelector('#fields-editor');
const addFieldBtn = editSidebar.querySelector('#add-field-btn');
const cancelBtn = editSidebar.querySelector('#cancel-preset-btn');
const saveBtn = editSidebar.querySelector('#save-preset-btn');
function renderFields() {
fieldsContainer.innerHTML = '';
if (fieldsArray.length === 0) {
fieldsContainer.innerHTML = '<div style="color:var(--eaf-text-secondary);font-size:13px;padding:8px 0;">暂无字段,请添加</div>';
return;
}
fieldsArray.forEach((field, idx) => {
const div = document.createElement('div');
div.className = 'eaf-field-row';
div.style.marginBottom = '8px';
div.innerHTML = `
<input class="eaf-input field-identifier" placeholder="字段标识(如:用户名)" value="${escapeHtml(field.identifier)}">
<input class="eaf-input field-value" placeholder="填充值" value="${escapeHtml(field.value)}" style="margin-top:6px;">
<button class="eaf-small-btn danger remove-field" data-idx="${idx}" style="margin-top:6px;">删除</button>
`;
fieldsContainer.appendChild(div);
});
fieldsContainer.querySelectorAll('.remove-field').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.getAttribute('data-idx'));
fieldsArray.splice(idx, 1);
renderFields();
});
});
const rows = fieldsContainer.querySelectorAll('.eaf-field-row');
rows.forEach((row, i) => {
const identInput = row.querySelector('.field-identifier');
const valInput = row.querySelector('.field-value');
identInput.addEventListener('input', () => {
if (fieldsArray[i]) fieldsArray[i].identifier = identInput.value;
});
valInput.addEventListener('input', () => {
if (fieldsArray[i]) fieldsArray[i].value = valInput.value;
});
});
}
renderFields();
addFieldBtn.onclick = () => {
fieldsArray.push({ identifier: '', value: '' });
renderFields();
};
cancelBtn.onclick = () => {
managePresets();
};
saveBtn.onclick = () => {
const name = nameInput.value.trim();
if (!name) {
showToast('请输入规则名称', 'error');
return;
}
const validFields = fieldsArray.filter(f => f.identifier.trim() !== '');
if (validFields.length === 0) {
showToast('至少添加一个有效字段', 'error');
return;
}
if (isNew) {
const newPreset = { id: generateId(), name, fields: validFields, createdAt: Date.now() };
presets.unshift(newPreset);
} else {
preset.name = name;
preset.fields = validFields;
preset.updatedAt = Date.now();
}
savePresets();
managePresets();
showToast(isNew ? '预设已添加' : '已更新', 'success');
};
}
renderPresetList();
addBtn.onclick = () => editPreset(null);
}
// 数据管理(保存的表单 + 预设规则)
function manageSavedForms() {
let content = `
<div style="display:flex;gap:8px;margin-bottom:12px;align-items:center;">
<input type="text" id="search-manage" class="eaf-input" placeholder="搜索..." style="flex:1;">
<button id="select-all-btn" class="eaf-small-btn" style="white-space:nowrap;">全选</button>
<button id="delete-selected-btn" class="eaf-small-btn danger" style="white-space:nowrap;">删除选中</button>
</div>
<div id="eaf-saved-list" class="eaf-scroll" style="max-height:60vh;"></div>
`;
const sidebar = openSidebar('数据管理', content);
const container = sidebar.querySelector('#eaf-saved-list');
const searchInput = sidebar.querySelector('#search-manage');
const selectAllBtn = sidebar.querySelector('#select-all-btn');
const deleteSelectedBtn = sidebar.querySelector('#delete-selected-btn');
let selectedForms = new Set();
let selectedPresets = new Set();
function updateSelectAllBtn() {
const totalItems = savedForms.length + presets.length;
const selectedItems = selectedForms.size + selectedPresets.size;
if (selectedItems === 0) {
selectAllBtn.textContent = '全选';
selectAllBtn.classList.remove('active');
} else if (selectedItems === totalItems && totalItems > 0) {
selectAllBtn.textContent = '取消全选';
selectAllBtn.classList.add('active');
} else {
selectAllBtn.textContent = '全选';
selectAllBtn.classList.remove('active');
}
}
function updateDeleteBtn() {
const hasSelection = selectedForms.size > 0 || selectedPresets.size > 0;
deleteSelectedBtn.style.opacity = hasSelection ? '1' : '0.5';
deleteSelectedBtn.disabled = !hasSelection;
}
function renderSaved(formsList = savedForms, presetsList = presets) {
container.innerHTML = '';
selectedForms.clear();
selectedPresets.clear();
updateSelectAllBtn();
updateDeleteBtn();
if (formsList.length === 0 && presetsList.length === 0) {
container.innerHTML = `
<div class="eaf-empty">
<div class="eaf-empty-icon">📋</div>
<p>暂无数据</p>
<span style="font-size:13px;">保存表单或添加预设规则</span>
</div>`;
return;
}
// 保存的表单
if (formsList.length) {
const groupTitle = document.createElement('div');
groupTitle.className = 'eaf-group-title';
groupTitle.innerHTML = `<input type="checkbox" class="select-group-forms" style="margin-right:8px;">保存的表单 (${formsList.length})`;
container.appendChild(groupTitle);
const sortedForms = [...formsList].sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
sortedForms.forEach((form) => {
const card = document.createElement('div');
card.className = 'eaf-field-row';
card.innerHTML = `
<div class="eaf-field-row-header">
<label style="display:flex;align-items:center;gap:8px;flex:1;cursor:pointer;">
<input type="checkbox" class="select-form" data-id="${form.id}">
<span class="eaf-field-row-title">${escapeHtml(form.title)}</span>
</label>
<div style="display:flex;gap:6px;">
<button class="eaf-small-btn danger del-form" data-id="${form.id}">删除</button>
<button class="eaf-small-btn use-form" data-id="${form.id}">填充</button>
</div>
</div>
<div style="font-size:12px; color:var(--eaf-text-secondary);margin-left:24px;">${form.fields.length} 个字段</div>
`;
container.appendChild(card);
});
}
// 预设规则
if (presetsList.length) {
const groupTitle = document.createElement('div');
groupTitle.className = 'eaf-group-title';
groupTitle.innerHTML = `<input type="checkbox" class="select-group-presets" style="margin-right:8px;">预设规则 (${presetsList.length})`;
container.appendChild(groupTitle);
const sortedPresets = [...presetsList].sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
sortedPresets.forEach((preset) => {
const card = document.createElement('div');
card.className = 'eaf-field-row';
card.innerHTML = `
<div class="eaf-field-row-header">
<label style="display:flex;align-items:center;gap:8px;flex:1;cursor:pointer;">
<input type="checkbox" class="select-preset" data-id="${preset.id}">
<span class="eaf-field-row-title">${escapeHtml(preset.name)}</span>
</label>
<div style="display:flex;gap:6px;">
<button class="eaf-small-btn danger del-preset" data-id="${preset.id}">删除</button>
<button class="eaf-small-btn use-preset" data-id="${preset.id}">填充</button>
</div>
</div>
<div style="font-size:12px; color:var(--eaf-text-secondary);margin-left:24px;">${preset.fields.length} 个字段</div>
`;
container.appendChild(card);
});
}
// 绑定复选框事件
container.querySelectorAll('.select-form').forEach(cb => {
cb.addEventListener('change', (e) => {
const id = cb.getAttribute('data-id');
if (cb.checked) {
selectedForms.add(id);
} else {
selectedForms.delete(id);
}
updateSelectAllBtn();
updateDeleteBtn();
});
});
container.querySelectorAll('.select-preset').forEach(cb => {
cb.addEventListener('change', (e) => {
const id = cb.getAttribute('data-id');
if (cb.checked) {
selectedPresets.add(id);
} else {
selectedPresets.delete(id);
}
updateSelectAllBtn();
updateDeleteBtn();
});
});
// 分组全选
container.querySelector('.select-group-forms')?.addEventListener('change', (e) => {
const checked = e.target.checked;
container.querySelectorAll('.select-form').forEach(cb => {
cb.checked = checked;
const id = cb.getAttribute('data-id');
if (checked) {
selectedForms.add(id);
} else {
selectedForms.delete(id);
}
});
updateSelectAllBtn();
updateDeleteBtn();
});
container.querySelector('.select-group-presets')?.addEventListener('change', (e) => {
const checked = e.target.checked;
container.querySelectorAll('.select-preset').forEach(cb => {
cb.checked = checked;
const id = cb.getAttribute('data-id');
if (checked) {
selectedPresets.add(id);
} else {
selectedPresets.delete(id);
}
});
updateSelectAllBtn();
updateDeleteBtn();
});
// 绑定事件
container.querySelectorAll('.use-form').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const id = btn.getAttribute('data-id');
const form = savedForms.find(f => f.id === id);
if (form) {
fillFormWithFields(form.fields);
showToast('填充完成', 'success');
}
});
});
container.querySelectorAll('.use-preset').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const id = btn.getAttribute('data-id');
const preset = presets.find(p => p.id === id);
if (preset) {
fillFormWithFields(preset.fields);
showToast('填充完成', 'success');
}
});
});
container.querySelectorAll('.del-form').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const id = btn.getAttribute('data-id');
if (confirm('确定删除此表单?')) {
savedForms = savedForms.filter(f => f.id !== id);
saveForms();
renderSaved();
showToast('已删除', 'success');
}
});
});
container.querySelectorAll('.del-preset').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const id = btn.getAttribute('data-id');
if (confirm('确定删除此预设?')) {
presets = presets.filter(p => p.id !== id);
savePresets();
renderSaved();
showToast('已删除', 'success');
}
});
});
}
// 全选/取消全选
selectAllBtn.addEventListener('click', () => {
const totalItems = savedForms.length + presets.length;
const selectedItems = selectedForms.size + selectedPresets.size;
const selectAll = selectedItems < totalItems;
selectedForms.clear();
selectedPresets.clear();
if (selectAll) {
savedForms.forEach(f => selectedForms.add(f.id));
presets.forEach(p => selectedPresets.add(p.id));
}
container.querySelectorAll('.select-form').forEach(cb => {
cb.checked = selectAll;
});
container.querySelectorAll('.select-preset').forEach(cb => {
cb.checked = selectAll;
});
container.querySelector('.select-group-forms').checked = selectAll && savedForms.length > 0;
container.querySelector('.select-group-presets').checked = selectAll && presets.length > 0;
updateSelectAllBtn();
updateDeleteBtn();
});
// 删除选中项
deleteSelectedBtn.addEventListener('click', () => {
const totalSelected = selectedForms.size + selectedPresets.size;
if (totalSelected === 0) return;
if (confirm(`确定删除选中的 ${totalSelected} 项?`)) {
if (selectedForms.size > 0) {
savedForms = savedForms.filter(f => !selectedForms.has(f.id));
saveForms();
}
if (selectedPresets.size > 0) {
presets = presets.filter(p => !selectedPresets.has(p.id));
savePresets();
}
renderSaved();
showToast(`已删除 ${totalSelected} 项`, 'success');
}
});
// 搜索功能
searchInput.addEventListener('input', (e) => {
const keyword = e.target.value.toLowerCase();
if (!keyword) {
renderSaved();
return;
}
const filteredForms = savedForms.filter(f => f.title.toLowerCase().includes(keyword));
const filteredPresets = presets.filter(p => p.name.toLowerCase().includes(keyword));
renderSaved(filteredForms, filteredPresets);
});
renderSaved();
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&';
if (m === '<') return '<';
if (m === '>') return '>';
return m;
});
}
// ---------- 侧边栏管理 ----------
let currentSidebar = null;
function openSidebar(title, content) {
if (currentSidebar) {
currentSidebar.remove();
}
const sidebar = document.createElement('div');
sidebar.className = 'eaf-sidebar';
sidebar.innerHTML = `
<div class="eaf-sidebar-header">
<h3>${title}</h3>
<button class="eaf-sidebar-close">✕</button>
</div>
<div class="eaf-sidebar-body">${content}</div>
`;
document.body.appendChild(sidebar);
currentSidebar = sidebar;
requestAnimationFrame(() => {
sidebar.classList.add('visible');
});
const closeBtn = sidebar.querySelector('.eaf-sidebar-close');
closeBtn.onclick = closeSidebar;
document.addEventListener('keydown', function onEsc(e) {
if (e.key === 'Escape') {
closeSidebar();
document.removeEventListener('keydown', onEsc);
}
});
return sidebar;
}
function closeSidebar() {
if (currentSidebar) {
currentSidebar.classList.remove('visible');
setTimeout(() => {
if (currentSidebar) {
currentSidebar.remove();
currentSidebar = null;
}
}, 300);
}
}
// ---------- 构建悬浮UI ----------
function buildWidget() {
const container = document.createElement('div');
container.id = 'enhanced-autofill-widget';
const btn = document.createElement('button');
btn.className = 'eaf-btn';
btn.innerHTML = `<svg viewBox="0 0 24 24"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>`;
btn.setAttribute('aria-label', '智能填充助手');
container.appendChild(btn);
document.body.appendChild(container);
let menuPanel = null;
function closeMenu() {
if (menuPanel) menuPanel.remove();
menuPanel = null;
}
btn.addEventListener('click', (e) => {
e.stopPropagation();
// 检查用户是否已同意
if (!checkUserAgreement()) {
return;
}
if (menuPanel) {
closeMenu();
return;
}
// 检查是否有智能推荐
const recommended = getRecommendedForms();
menuPanel = document.createElement('div');
menuPanel.className = 'eaf-panel';
let menuHTML = '';
// 智能推荐(如果有)
if (recommended.length > 0) {
menuHTML += `<div class="eaf-group-title">智能推荐</div>`;
recommended.forEach(form => {
menuHTML += `<div class="eaf-menu-item eaf-recommended" data-action="quick-fill" data-id="${form.id}">⚡ ${escapeHtml(form.title)}</div>`;
});
}
// 主菜单
menuHTML += `
<div class="eaf-menu-item" data-action="save">💾 保存当前数据</div>
<div class="eaf-menu-item" data-action="fill">📂 填充数据</div>
<div class="eaf-menu-item" data-action="undo">↩️ 撤销填充</div>
<div class="eaf-menu-item" data-action="preset">⚙️ 预设规则</div>
<div class="eaf-menu-item" data-action="manage">🗄️ 数据管理</div>
<div class="eaf-menu-item" data-action="donate">❤️ 赞赏作者</div>
<div class="eaf-menu-item" data-action="more">📦 更多功能</div>
<div class="eaf-menu-item" data-action="disclaimer" style="color:#e74c3c;">⚠️ 重要说明</div>
`;
menuPanel.innerHTML = menuHTML;
container.appendChild(menuPanel);
const handleAction = (action, id) => {
closeMenu();
if (action === 'save') saveCurrentForm();
if (action === 'fill') showFillList();
if (action === 'preset') managePresets();
if (action === 'manage') manageSavedForms();
if (action === 'undo') undoLastFill();
if (action === 'donate') showDonatePanel();
if (action === 'disclaimer') showDisclaimerPanel();
if (action === 'quick-fill' && id) {
const form = savedForms.find(f => f.id === id);
if (form) {
saveFillHistory();
fillFormWithFields(form.fields);
stats.fillCount++;
stats.lastUsed = Date.now();
saveStats();
}
}
if (action === 'more') showMorePanel();
};
menuPanel.querySelectorAll('.eaf-menu-item').forEach(item => {
item.addEventListener('click', (e) => {
const action = item.getAttribute('data-action');
const id = item.getAttribute('data-id');
handleAction(action, id);
});
});
document.addEventListener('click', function onDocClick(ev) {
if (!menuPanel || menuPanel.contains(ev.target) || ev.target === btn) return;
closeMenu();
document.removeEventListener('click', onDocClick);
});
});
}
// ---------- 更多功能面板 ----------
function showMorePanel() {
const s = getStats();
let content = `
<div style="margin-bottom:16px;">
<div style="font-size:13px;color:var(--eaf-text-secondary);margin-bottom:8px;">数据操作</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<button id="export-btn" class="eaf-button" style="flex:1;">📤 导出数据</button>
<button id="import-btn" class="eaf-button" style="flex:1;">📥 导入数据</button>
</div>
<input type="file" id="import-file" accept=".json" style="display:none;">
</div>
<div style="margin-bottom:16px;">
<div style="font-size:13px;color:var(--eaf-text-secondary);margin-bottom:8px;">快捷键说明</div>
<div style="font-size:12px;color:var(--eaf-text);line-height:1.8;">
<div><kbd style="background:#f0f0f0;padding:2px 6px;border-radius:3px;">Ctrl+Shift+F</kbd> 快速填充</div>
<div><kbd style="background:#f0f0f0;padding:2px 6px;border-radius:3px;">Ctrl+Shift+S</kbd> 快速保存</div>
<div><kbd style="background:#f0f0f0;padding:2px 6px;border-radius:3px;">Ctrl+Shift+Z</kbd> 撤销填充</div>
</div>
</div>
<div style="padding:12px;background:var(--eaf-surface);border-radius:8px;">
<div style="font-size:13px;color:var(--eaf-text-secondary);margin-bottom:8px;">使用统计</div>
<div style="font-size:12px;color:var(--eaf-text);line-height:1.8;">
<div>保存的表单: <strong>${s.totalForms}</strong> 个</div>
<div>预设规则: <strong>${s.totalPresets}</strong> 个</div>
<div>累计填充: <strong>${s.fillCount}</strong> 次</div>
<div>上次使用: ${s.lastUsed}</div>
</div>
</div>
`;
const sidebar = openSidebar('更多功能', content);
// 导出按钮
sidebar.querySelector('#export-btn').onclick = exportData;
// 导入按钮
const importBtn = sidebar.querySelector('#import-btn');
const importFile = sidebar.querySelector('#import-file');
importBtn.onclick = () => importFile.click();
importFile.onchange = (e) => {
if (e.target.files[0]) {
importData(e.target.files[0]);
}
};
}
// ---------- 重要说明与免责声明面板 ----------
function showDisclaimerPanel() {
let content = `
<div style="padding:16px 0;">
<div style="background:#fff3cd;border:1px solid #ffc107;border-radius:8px;padding:16px;margin-bottom:16px;">
<div style="font-size:15px;font-weight:600;color:#856404;margin-bottom:12px;">⚠️ 重要警告</div>
<div style="font-size:13px;color:#856404;line-height:1.8;">
<p style="margin:0 0 8px 0;"><strong>请勿在重要场合使用本插件进行自动填充!</strong></p>
<p style="margin:0 0 8px 0;">• 考试、银行、医疗、法律等重要数据填写请手动操作</p>
<p style="margin:0 0 8px 0;">• 提交前请务必仔细检查填充的数据是否正确</p>
<p style="margin:0;">• 因使用本插件导致的数据错误,开发者不承担任何责任</p>
</div>
</div>
<div style="background:#f8d7da;border:1px solid #f5c6cb;border-radius:8px;padding:16px;margin-bottom:16px;">
<div style="font-size:15px;font-weight:600;color:#721c24;margin-bottom:12px;">📋 免责声明</div>
<div style="font-size:12px;color:#721c24;line-height:1.8;">
<p style="margin:0 0 8px 0;">1. 本插件为免费开源工具,按"现状"提供,不提供任何明示或暗示的保证。</p>
<p style="margin:0 0 8px 0;">2. 开发者不对因使用或无法使用本插件而产生的任何直接或间接损失负责。</p>
<p style="margin:0 0 8px 0;">3. 用户需自行承担使用本插件的风险,开发者不承担任何法律责任。</p>
<p style="margin:0;">4. 继续使用本插件即表示您已阅读并同意以上条款。</p>
</div>
</div>
<div style="background:var(--eaf-surface);border-radius:8px;padding:16px;">
<div style="font-size:15px;font-weight:600;color:var(--eaf-text);margin-bottom:12px;">💡 使用建议</div>
<div style="font-size:12px;color:var(--eaf-text);line-height:1.8;">
<p style="margin:0 0 8px 0;">1. 首次使用时,先保存数据,然后测试填充是否正确</p>
<p style="margin:0 0 8px 0;">2. 填充后请逐一检查每个字段是否正确</p>
<p style="margin:0 0 8px 0;">3. 重要表单建议手动填写,确保数据准确</p>
<p style="margin:0;">4. 如发现问题,请及时反馈以便改进</p>
</div>
</div>
</div>
`;
openSidebar('重要说明', content);
}
// ---------- 赞赏作者面板 ----------
function showDonatePanel() {
let content = `
<div style="text-align:center;padding:20px 0;">
<div style="font-size:16px;font-weight:500;margin-bottom:16px;color:var(--eaf-text);">感谢您的支持!</div>
<div style="font-size:13px;color:var(--eaf-text-secondary);margin-bottom:20px;">
如果这个工具对您有帮助,欢迎赞赏支持开发者继续维护
</div>
<div style="background:#f5f5f5;border-radius:12px;padding:16px;display:inline-block;">
<img src="data:image/png;base64,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"
alt="微信收款码"
style="width:200px;height:auto;border-radius:8px;display:block;"
onerror="this.onerror=null;this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 200 200%22><rect fill=%22%23f0f0f0%22 width=%22200%22 height=%22200%22/><text x=%22100%22 y=%22100%22 text-anchor=%22middle%22 dy=%22.3em%22 fill=%22%23999%22 font-size=%2214%22>图片加载失败</text></svg>'">
</div>
<div style="margin-top:16px;font-size:12px;color:var(--eaf-text-secondary);">
微信扫码赞赏
</div>
</div>
<div style="margin-top:20px;padding:16px;background:var(--eaf-surface);border-radius:8px;">
<div style="font-size:13px;color:var(--eaf-text-secondary);margin-bottom:8px;">其他支持方式</div>
<div style="font-size:12px;color:var(--eaf-text);line-height:1.8;">
<div>⭐ 给项目点个 Star</div>
<div>🐛 反馈 Bug 和建议</div>
<div>📢 分享给更多人使用</div>
</div>
</div>
`;
openSidebar('赞赏作者', content);
}
// ---------- 首次使用确认对话框 ----------
function checkUserAgreement() {
// 尝试从 GM 存储读取
let agreed = false;
try {
agreed = GM_getValue('eaf_user_agreed', false);
} catch (e) {
// GM 存储不可用,尝试 localStorage
try {
agreed = localStorage.getItem('eaf_user_agreed') === 'true';
} catch (e2) {
agreed = false;
}
}
if (agreed === true || agreed === 'true') {
return true;
}
showUserAgreementDialog();
return false;
}
function showUserAgreementDialog() {
const overlay = document.createElement('div');
overlay.id = 'eaf-agreement-overlay';
overlay.style.cssText = `
position: fixed;
inset: 0;
background: rgba(0,0,0,0.7);
z-index: 2147483647;
display: flex;
align-items: center;
justify-content: center;
`;
const dialog = document.createElement('div');
dialog.style.cssText = `
background: white;
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0,0,0,0.3);
max-width: 500px;
width: 90%;
max-height: 85vh;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
dialog.innerHTML = `
<div style="padding:20px;border-bottom:1px solid #eee;background:#f8f9fa;">
<div style="font-size:18px;font-weight:600;color:#333;display:flex;align-items:center;gap:8px;">
<span style="color:#e74c3c;">⚠️</span> 使用前请仔细阅读
</div>
</div>
<div style="padding:20px;max-height:400px;overflow-y:auto;">
<div style="background:#f8d7da;border:1px solid #f5c6cb;border-radius:8px;padding:16px;margin-bottom:16px;">
<div style="font-size:15px;font-weight:600;color:#721c24;margin-bottom:12px;">🚨 重要警告</div>
<div style="font-size:13px;color:#721c24;line-height:1.8;">
<p style="margin:0 0 8px 0;"><strong>请勿在重要场合使用本插件进行自动填充!</strong></p>
<p style="margin:0 0 8px 0;">• 考试、银行、医疗、法律等重要数据填写请手动操作</p>
<p style="margin:0 0 8px 0;">• 提交前请务必仔细检查填充的数据是否正确</p>
<p style="margin:0;">• 因使用本插件导致的数据错误,开发者不承担任何责任</p>
</div>
</div>
<div style="background:#fff3cd;border:1px solid #ffc107;border-radius:8px;padding:16px;margin-bottom:16px;">
<div style="font-size:15px;font-weight:600;color:#856404;margin-bottom:12px;">📋 免责声明</div>
<div style="font-size:12px;color:#856404;line-height:1.8;">
<p style="margin:0 0 8px 0;">1. 本插件为免费开源工具,按"现状"提供,不提供任何明示或暗示的保证。</p>
<p style="margin:0 0 8px 0;">2. 开发者不对因使用或无法使用本插件而产生的任何直接或间接损失负责。</p>
<p style="margin:0 0 8px 0;">3. 用户需自行承担使用本插件的风险,开发者不承担任何法律责任。</p>
<p style="margin:0;">4. 继续使用本插件即表示您已阅读并同意以上条款。</p>
</div>
</div>
<div style="background:#f8f9fa;border-radius:8px;padding:16px;">
<div style="font-size:15px;font-weight:600;color:#333;margin-bottom:12px;">💡 使用建议</div>
<div style="font-size:12px;color:#333;line-height:1.8;">
<p style="margin:0 0 8px 0;">1. 首次使用时,先保存数据,然后测试填充是否正确</p>
<p style="margin:0 0 8px 0;">2. 填充后请逐一检查每个字段是否正确</p>
<p style="margin:0 0 8px 0;">3. 重要表单建议手动填写,确保数据准确</p>
<p style="margin:0;">4. 如发现问题,请及时反馈以便改进</p>
</div>
</div>
</div>
<div style="padding:16px;display:flex;gap:12px;justify-content:center;border-top:1px solid #eee;background:#f8f9fa;">
<button id="eaf-disagree-btn" style="padding:12px 30px;border:1px solid #ddd;background:white;color:#666;border-radius:8px;cursor:pointer;font-size:14px;">
不同意
</button>
<button id="eaf-agree-btn" disabled style="padding:12px 40px;border:none;background:#ccc;color:white;border-radius:8px;cursor:not-allowed;font-size:14px;font-weight:500;transition:all 0.3s;">
请阅读 (<span id="eaf-countdown">10</span>秒)
</button>
</div>
`;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
// 10秒倒计时
let countdown = 10;
const countdownEl = dialog.querySelector('#eaf-countdown');
const agreeBtn = dialog.querySelector('#eaf-agree-btn');
const disagreeBtn = dialog.querySelector('#eaf-disagree-btn');
const timer = setInterval(() => {
countdown--;
countdownEl.textContent = countdown;
if (countdown <= 0) {
clearInterval(timer);
agreeBtn.disabled = false;
agreeBtn.style.background = '#4a90d9';
agreeBtn.style.cursor = 'pointer';
agreeBtn.innerHTML = '我已阅读并同意';
}
}, 1000);
// 同意按钮
agreeBtn.onclick = () => {
if (agreeBtn.disabled) return;
// 使用 GM 存储优先
try {
GM_setValue('eaf_user_agreed', true);
} catch (e) {
// GM 存储不可用,使用 localStorage
try {
localStorage.setItem('eaf_user_agreed', 'true');
} catch (e2) {
// 都不可用,使用内存标记
window._eaf_user_agreed = true;
}
}
overlay.remove();
};
// 不同意按钮
disagreeBtn.onclick = () => {
overlay.remove();
showToast('您已拒绝使用本插件', 'error');
};
}
// 自动填充功能:页面加载后自动检测并提示
function initAutoFill() {
// 检查是否有匹配的保存表单
const recommended = getRecommendedForms();
if (recommended.length === 0) return;
// 检查当前页面是否有表单
const currentFields = collectFormFields();
if (currentFields.length === 0) return;
// 过滤验证码字段
const filteredFields = filterCaptchaFields(currentFields);
if (filteredFields.length === 0) return;
// 延迟显示自动填充提示
setTimeout(() => {
showAutoFillPrompt(recommended[0]);
}, 1500);
}
// 显示自动填充提示
function showAutoFillPrompt(form) {
// 检查是否已经显示过本次会话的提示
if (window._eaf_autofill_shown) return;
window._eaf_autofill_shown = true;
const prompt = document.createElement('div');
prompt.id = 'eaf-autofill-prompt';
prompt.style.cssText = `
position: fixed;
bottom: 90px;
right: 24px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
padding: 16px 20px;
z-index: 2147483646;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
color: #333;
max-width: 280px;
border: 1px solid #e1e4e8;
animation: eaf-slide-in 0.3s ease;
`;
// 添加动画样式
if (!document.getElementById('eaf-autofill-anim')) {
const style = document.createElement('style');
style.id = 'eaf-autofill-anim';
style.textContent = `
@keyframes eaf-slide-in {
from { transform: translateX(100px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
`;
document.head.appendChild(style);
}
prompt.innerHTML = `
<div style="font-weight: 600; margin-bottom: 8px; color: #4a90d9;">
💡 发现保存的表单
</div>
<div style="margin-bottom: 12px; color: #586069; font-size: 13px;">
检测到 "${escapeHtml(form.title)}" 可填充此页面
</div>
<div style="display: flex; gap: 8px;">
<button id="eaf-autofill-yes" style="
flex: 1;
padding: 8px 16px;
background: #4a90d9;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
">立即填充</button>
<button id="eaf-autofill-no" style="
flex: 1;
padding: 8px 16px;
background: #f5f7fa;
color: #586069;
border: 1px solid #e1e4e8;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
">取消</button>
</div>
`;
document.body.appendChild(prompt);
// 立即填充
prompt.querySelector('#eaf-autofill-yes').onclick = () => {
saveFillHistory();
fillFormWithFields(form.fields);
stats.fillCount++;
stats.lastUsed = Date.now();
saveStats();
prompt.remove();
};
// 取消
prompt.querySelector('#eaf-autofill-no').onclick = () => {
prompt.remove();
};
// 5秒后自动消失
setTimeout(() => {
if (prompt.parentNode) {
prompt.style.opacity = '0';
prompt.style.transform = 'translateX(100px)';
prompt.style.transition = 'all 0.3s ease';
setTimeout(() => prompt.remove(), 300);
}
}, 5000);
}
// 初始化加载数据 & 注入界面
loadData();
buildWidget();
setupShortcuts();
startAutoSave();
// 页面加载完成后尝试自动填充
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAutoFill);
} else {
initAutoFill();
}
})();