[JavaScript] 纯文本查看 复制代码
// ==UserScript==
// [url=home.php?mod=space&uid=170990]@name[/url] 网站登录配置管理工具
// [url=home.php?mod=space&uid=467642]@namespace[/url] http://tampermonkey.net/
// [url=home.php?mod=space&uid=1248337]@version[/url] 3.1
// @description 紧凑模式的多网站登录配置管理工具,支持搜索、导入导出、选择器验证、先手点击
// [url=home.php?mod=space&uid=686208]@AuThor[/url] DeepSeek
// [url=home.php?mod=space&uid=195849]@match[/url] *://*/*
// [url=home.php?mod=space&uid=609072]@grant[/url] GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_deleteValue
// @grant GM_addStyle
// @grant GM_listValues
// @grant GM_setClipboard
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// ===================== 执行保护机制 =====================
// 1. 检查是否在iframe中
if (window.top !== window.self) {
console.log('登录配置管理工具:在iframe中,跳过执行');
return;
}
// 2. 检查是否已经执行过(防止重复执行)
if (window.loginManagerInitialized) {
console.log('登录配置管理工具:已经初始化,跳过重复执行');
return;
}
// 3. 检查是否在特殊页面中(如浏览器设置页面)
if (window.location.protocol === 'chrome:' ||
window.location.protocol === 'chrome-extension:' ||
window.location.protocol === 'moz-extension:' ||
window.location.protocol === 'edge:' ||
window.location.protocol === 'about:' ||
window.location.protocol === 'data:' ||
window.location.protocol === 'file:') {
console.log('登录配置管理工具:在特殊页面中,跳过执行');
return;
}
// 4. 标记已初始化
window.loginManagerInitialized = true;
console.log('ℹ️ 登录配置管理工具:开始初始化');
// ===================== 常量定义 =====================
const CONSTANTS = {
// 配置相关
CONFIG_PREFIX: 'loginConfig_',
DEFAULT_SELECTORS: {
username: "input[placeholder*='用户']",
password: "input[type='password']",
loginButton: "button[type='submit']",
preClickButtons: [] // 先手点击按钮选择器数组
},
// 加密相关
ENCRYPTION: {
// 使用固定的盐值,增加安全性(实际应用中可以考虑用户特定的盐值)
SALT: 'login_manager_2024',
// 主密码(建议修改为你自己的强密码,例如:'MyStr0ng!P@ssw0rd#2024')
MASTER_PASSWORD: 'MyStr0ng!P@ssw0rd#2024',
// 加密算法标识
ALGORITHM: 'AES-GCM',
// 密钥派生函数参数
KEY_DERIVATION: {
iterations: 100000,
keyLength: 256
},
// 加密标识前缀
ENCRYPTED_PREFIX: 'ENCRYPTED:'
},
// UI相关
PANEL_DIMENSIONS: {
width: '480px',
height: {
collapsed: '428px',
expanded: '100%'
}
},
// 动画相关
ANIMATION: {
duration: '0.2s',
debounceDelay: 400,
highlightDuration: 2000,
autoLoginDelay: 800,
pageLoadDelay: 1500,
preClickDelay: 500 // 先手点击后的等待时间
},
// 透明度相关
OPACITY: {
dragging: '0.7',
normal: '1'
},
// 弹窗检测相关
POPUP: {
minZIndex: 1000,
selectors: [
'[role="dialog"]',
'[role="alertdialog"]',
'.modal',
'.popup',
'.dialog',
'.overlay',
'.lightbox',
'.tooltip',
'.dropdown',
'.menu',
'.notification',
'.alert',
'.toast',
'#login-manager-compact',
'#import-dialog'
],
keywords: [
'modal', 'popup', 'dialog', 'overlay', 'lightbox', 'tooltip',
'dropdown', 'menu', 'notification', 'alert', 'toast', 'float',
'fixed', 'absolute', 'z-index'
]
},
// 选择器生成相关
SELECTOR: {
userKeywords: ['user', 'name', 'account', 'login'],
loginKeywords: ['登录', 'login', 'sign in'],
genericClasses: ['btn', 'button', 'input', 'form-control', 'form-group']
},
// 消息相关
MESSAGES: {
errors: {
urlRequired: '网站地址不能为空',
nicknameRequired: '昵称不能为空',
configRequired: '请先选择一个配置',
urlInvalid: '网址格式不正确',
importRequired: '请输入要导入的JSON',
importFormat: '格式应为数组',
importFailed: '导入失败: ',
exportFailed: '导出失败',
readFailed: '读取配置失败',
saveFailed: '保存配置失败',
deleteFailed: '删除配置失败',
popupElement: '不能选择弹窗元素,请选择页面主体内容',
passwordDecryptFailed: '密码解密失败,可能是主密码不同',
newPasswordRequired: '请输入新密码',
cryptoUnsupported: '当前浏览器环境不支持加密功能,请更换浏览器或禁用加密选项'
},
success: {
configSaved: '配置已保存',
configDeleted: '配置已删除',
exportSuccess: '所有配置已复制到剪贴板!',
importSuccess: '成功导入 ',
configs: ' 条配置',
passwordHandled: '密码处理完成'
},
confirm: {
deleteConfig: '确定要删除此配置吗?此操作不可恢复!'
},
info: {
encryptedPasswordDetected: '检测到加密密码,请选择处理方式',
manualPasswordRequired: '请手动输入密码',
exportPasswordOption: '请选择导出时的密码处理方式'
}
}
};
// ===================== 样式 =====================
GM_addStyle(`
#login-manager-compact { position: fixed; top: 20px; left: 20px; width: 480px; background: #fff; border-radius: 6px; box-shadow: 0 4px 20px rgba(0,0,0,0.25); z-index: 9999999; font-family: 'Segoe UI', Arial, sans-serif; overflow: hidden; border: 1px solid #ddd; font-size: 13px; display: none; }
#login-manager-compact.dragging { opacity: 0.8; box-shadow: 0 8px 25px rgba(0,0,0,0.6); }
#login-manager-compact .header { background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%); color: white; padding: 10px 12px; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; }
#login-manager-compact .title { font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
#login-manager-compact .close-btn { background: none; border: none; color: white; font-size: 18px; cursor: pointer; padding: 3px 8px; line-height: 1; }
#login-manager-compact .close-btn:hover { transform: scale(1.1); }
#login-manager-compact .body { display: flex; height: 100%; min-height: 0; }
#login-manager-compact .config-list-area { width: 180px; border-right: 1px solid #eee; background: #f9fafc; display: flex; flex-direction: column; height: 100%;max-height: 737px; min-height: 0; }
#login-manager-compact .list-actions { display: flex; gap: 8px; padding: 10px 10px 12px 10px; border-bottom: 1px solid #eee; background: #f5f5f5; }
#login-manager-compact .search-box { padding: 10px; border-bottom: 1px solid #eee; }
#login-manager-compact .search-input { width: 100%; min-width: 0; box-sizing: border-box; padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; }
#login-manager-compact .config-list { flex: 1; min-height: 0; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #b3b3b3 #f1f1f1; }
#login-manager-compact .config-list::-webkit-scrollbar { width: 6px; background: #f1f1f1; }
#login-manager-compact .config-list::-webkit-scrollbar-thumb { background: #b3b3b3; border-radius: 4px; }
#login-manager-compact .config-list::-webkit-scrollbar-thumb:hover { background: #888; }
#login-manager-compact .config-item { padding: 10px 12px; border-bottom: 1px solid #f0f0f0; cursor: pointer; transition: all 0.2s; }
#login-manager-compact .config-item.active { background: #e1e8ff; border-left: 3px solid #4d7cfe; }
#login-manager-compact .config-item.current-page { background: #f0f8ff; border-left: 3px solid #28a745; }
#login-manager-compact .config-item.current-page .nickname { color: #28a745; }
#login-manager-compact .config-item .nickname { font-weight: 600; font-size: 13px; }
#login-manager-compact .config-item .url { font-size: 11px; color: #888; }
#login-manager-compact .list-btn { flex: 1; padding: 8px 0; border: none; border-radius: 6px; background: #4d7cfe; color: white; font-size: 13px; font-weight: 600; cursor: pointer; transition: all 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.04); margin: 0; height: 36px; display: flex; align-items: center; justify-content: center; gap: 6px; }
#login-manager-compact .list-btn.export { background: linear-gradient(to right, #00c9a7, #00b09b); }
#login-manager-compact .list-btn.import { background: linear-gradient(to right, #e74c3c, #c0392b); }
#login-manager-compact .list-btn:hover { opacity: 0.92; box-shadow: 0 2px 6px rgba(0,0,0,0.08); }
#login-manager-compact .form-area { flex: 1; padding: 18px 18px 0 18px; display: flex; flex-direction: column; }
#login-manager-compact .form-group { margin-bottom: 12px; }
#login-manager-compact .form-group label { display: block; margin-bottom: 5px; font-weight: 500; color: #555; font-size: 12px; }
#login-manager-compact .compact-input { width: 100%; padding: 8px 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; box-sizing: border-box; }
#login-manager-compact .compact-input:focus { border-color: #3498db; outline: none; box-shadow: 0 0 0 2px rgba(52,152,219,0.2); }
#login-manager-compact .input-group { display: flex; position: relative; }
#login-manager-compact .input-group input { flex: 1; border-radius: 4px 0 0 4px !important; border-right: none !important; }
#login-manager-compact .input-group .icon-btn { background: #f0f0f0; border: 1px solid #ddd; border-left: none; border-radius: 0 4px 4px 0; padding: 0 10px; cursor: pointer; display: flex; align-items: center; justify-content: center; min-width: 36px; font-size: 14px; }
#login-manager-compact .input-group .icon-btn:hover { background: #e0e0e0; }
#login-manager-compact .selector-status { min-width: 36px; font-size: 14px; }
#login-manager-compact .checkbox-group { display: flex; align-items: center; margin-top: 8px; }
#login-manager-compact .checkbox-group input { margin-right: 6px; }
#login-manager-compact .form-actions { display: flex; gap: 8px; padding: 12px 0 18px 0; }
#login-manager-compact .form-btn { flex: 1; padding: 8px 10px; border: none; border-radius: 4px; font-weight: 600; cursor: pointer; font-size: 13px; }
#login-manager-compact .form-btn.save { background: linear-gradient(to right, #3498db, #1d6fa5); color: white; }
#login-manager-compact .form-btn.delete { background: linear-gradient(to right, #e74c3c, #c0392b); color: white; }
#login-manager-compact .form-btn.new { background: linear-gradient(to right, #00b09b, #96c93d); color: white; }
#login-manager-compact .form-group.selector-toggle-row { background: #f0f0f0; padding: 8px 12px; border-bottom: 1px solid #eee; display: flex; align-items: center; justify-content: space-between; cursor: pointer; }
#login-manager-compact .form-group.selector-toggle-row label { font-weight: 600; color: #333; }
#login-manager-compact #selectors-section { padding: 0 12px 12px 12px; border-bottom: 1px solid #eee; }
#login-manager-compact #selectors-section .form-group { margin-bottom: 10px; }
#login-manager-compact #selectors-section .form-group label { font-weight: 500; color: #555; }
#import-textarea { width: 100%; height: 180px; padding: 8px; border: 2px solid #3498db; border-radius: 6px; font-family: monospace; margin-bottom: 12px; resize: vertical; font-size: 13px; box-sizing: border-box; outline: none; transition: border-color 0.2s; }
#import-textarea:focus { border-color: #2575fc; box-shadow: 0 0 0 2px rgba(52,152,219,0.15); }
#login-manager-compact input[type="checkbox"] { width: 16px !important; height: 16px !important; margin: 0 6px 0 0 !important; vertical-align: middle !important; accent-color: #4d7cfe !important; box-shadow: none !important; border: 1px solid #bbb !important; background: #fff !important; appearance: checkbox !important; -webkit-appearance: checkbox !important; outline: none !important; display: inline-block !important; position: relative; }
#login-manager-compact label { font-size: 13px !important; color: #333 !important; font-weight: 400 !important; margin: 0 4px 0 0 !important; vertical-align: middle !important; user-select: none; }
#login-manager-compact button, #login-manager-compact input, #login-manager-compact .form-group { box-sizing: border-box !important; }
#login-manager-compact .checkbox-group { display: flex !important; align-items: center !important; gap: 8px !important; margin-bottom: 0 !important; }
#login-manager-compact .checkbox-group label { margin: 0 12px 0 0 !important; }
#import-dialog { z-index: 99999999 !important; }
@media (max-width: 600px) { #login-manager-compact { width: 98%; top: 20px; font-size: 12px; } }
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } }
`);
// ===================== 主面板HTML =====================
const managerHTML = `
<div id="login-manager-compact">
<div class="header" id="panel-header">
<button id="toggle-list-btn" style="background:none;border:none;color:white;font-size:16px;cursor:pointer;padding:0 8px;margin-right:8px;">📋</button>
<div class="title"><span>⚙</span> 登录配置管理</div>
<button class="close-btn" id="close-panel">×</button>
</div>
<div class="body">
<div class="config-list-area" id="config-list-area">
<div class="list-actions">
<button class="list-btn export" id="export-all-btn">全部导出</button>
<button class="list-btn import" id="import-all-btn">导入</button>
</div>
<div class="search-box">
<input type="text" class="search-input" id="config-search" placeholder="搜索昵称或URL...">
</div>
<div class="config-list" id="config-list"></div>
</div>
<div class="form-area">
<div class="form-group">
<label>网站地址</label>
<div style="display:flex;align-items:center;gap:8px;">
<input type="text" id="edit-url" class="compact-input" style="flex:1;">
<button id="goto-url-btn" style="padding:6px 12px;border:none;border-radius:4px;background:#4d7cfe;color:white;font-size:13px;cursor:pointer;">跳转</button>
</div>
</div>
<div class="form-group">
<label>昵称</label>
<input type="text" id="edit-nickname" class="compact-input">
</div>
<div class="form-group">
<label>用户名</label>
<input type="text" id="edit-username" class="compact-input">
</div>
<div class="form-group">
<label>密码</label>
<div class="input-group">
<input type="password" id="edit-password" class="compact-input">
<div class="icon-btn" id="toggle-edit-password">👁</div>
</div>
</div>
<div class="checkbox-group" style="gap:18px;">
<input type="checkbox" id="edit-autoLogin">
<label for="edit-autoLogin">自动登录</label>
<input type="checkbox" id="edit-autoFill" style="margin-left:18px;">
<label for="edit-autoFill">自动填充</label>
</div>
<br/>
<div class="form-group selector-toggle-row" style="display:flex;align-items:center;justify-content:space-between;margin-bottom:2px;">
<label style="font-weight:600;">选择器</label>
<button id="toggle-selectors-btn" style="background:none;border:none;font-size:15px;cursor:pointer;outline:none;">▼</button>
</div>
<div id="selectors-section">
<div class="form-group">
<label>用户名选择器</label>
<div class="input-group">
<input type="text" id="edit-username-selector" class="compact-input">
<div class="icon-btn selector-status" id="check-username-selector">🔄</div>
<div class="icon-btn" id="pick-username-selector" title="选择元素">🎯</div>
</div>
</div>
<div class="form-group">
<label>密码选择器</label>
<div class="input-group">
<input type="text" id="edit-password-selector" class="compact-input">
<div class="icon-btn selector-status" id="check-password-selector">🔄</div>
<div class="icon-btn" id="pick-password-selector" title="选择元素">🎯</div>
</div>
</div>
<div class="form-group">
<label>登录按钮选择器</label>
<div class="input-group">
<input type="text" id="edit-login-button-selector" class="compact-input">
<div class="icon-btn selector-status" id="check-login-button-selector">🔄</div>
<div class="icon-btn" id="pick-login-button-selector" title="选择元素">🎯</div>
</div>
</div>
<div class="form-group">
<label>先手点击按钮选择器</label>
<div style="margin-bottom:8px;font-size:12px;color:#666;">
用于在登录前先点击某些按钮(如切换到账号密码登录模式)
</div>
<div class="input-group">
<input type="text" id="edit-pre-click-selector" class="compact-input" placeholder="多个选择器用逗号分隔">
<div class="icon-btn selector-status" id="check-pre-click-selector">🔄</div>
<div class="icon-btn" id="pick-pre-click-selector" title="选择元素">🎯</div>
</div>
</div>
</div>
<div class="form-actions">
<button class="form-btn save" id="save-config-btn">保存</button>
<button class="form-btn delete" id="delete-config-btn">删除</button>
<button class="form-btn new" id="new-config-btn">新建</button>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', managerHTML);
const manager = document.getElementById('login-manager-compact');
// ===================== 模块化骨架 =====================
// 数据管理模块
const DataManager = {
CONFIG_PREFIX: CONSTANTS.CONFIG_PREFIX,
// 配置版本管理
CONFIG_VERSION: '1.0',
/**
* 升级配置到最新版本
* @param {Object} config 配置对象
* @returns {Object} 升级后的配置对象
*/
upgradeConfig(config) {
// 检查配置是否有效
if (!config || typeof config !== 'object') {
console.warn('upgradeConfig: 配置对象无效', config);
return null;
}
if (!config.version) {
// 从旧版本升级到1.0
config.version = this.CONFIG_VERSION;
config.createdAt = new Date().toISOString();
config.updatedAt = new Date().toISOString();
}
return config;
},
/**
* 获取配置模板
* @param {string} templateName 模板名称
* @returns {Object} 配置模板
*/
getConfigTemplate(templateName) {
const templates = {
'default': {
currentUrl: '',
nickname: '新配置',
username: '',
password: '',
autoLogin: false,
autoFill: false,
selectors: {
...CONSTANTS.DEFAULT_SELECTORS,
preClickButtons: [] // 先手点击按钮选择器数组
},
version: this.CONFIG_VERSION,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
};
return templates[templateName] || templates['default'];
},
/**
* 验证配置数据
* @param {Object} config 配置对象
* @returns {Object} 验证结果 {valid: boolean, errors: Array}
*/
validateConfig(config) {
const errors = [];
if (!config.currentUrl || !config.currentUrl.trim()) {
errors.push('网站地址不能为空');
} else {
try {
new URL(config.currentUrl.startsWith('http') ? config.currentUrl : 'https://' + config.currentUrl);
} catch (e) {
errors.push('网站地址格式不正确');
}
}
if (!config.nickname || !config.nickname.trim()) {
errors.push('昵称不能为空');
}
if (config.username && config.username.length > 100) {
errors.push('用户名长度不能超过100个字符');
}
if (config.password && config.password.length > 100) {
errors.push('密码长度不能超过100个字符');
}
return {
valid: errors.length === 0,
errors: errors
};
},
/**
* 获取所有配置
* @returns {Array} 配置数组
*/
getAllConfigs() {
try {
const keys = GM_listValues();
return keys.filter(k => k.startsWith(this.CONFIG_PREFIX)).map(k => {
const data = GM_getValue(k);
// 检查数据是否有效
if (!data || typeof data !== 'object') {
console.warn('getAllConfigs: 跳过无效配置', k, data);
return null;
}
// 升级配置到最新版本
const upgradedData = this.upgradeConfig(data);
if (!upgradedData) {
console.warn('getAllConfigs: 配置升级失败,跳过', k);
return null;
}
if (upgradedData !== data) {
GM_setValue(k, upgradedData);
}
return { key: k, data: upgradedData };
}).filter(item => item !== null); // 过滤掉无效的配置
} catch (e) {
Utils.showError(CONSTANTS.MESSAGES.errors.readFailed, e);
return [];
}
},
/**
* 获取指定配置
* @param {string} key 配置键
* @returns {Object} 配置对象
*/
getConfig(key) {
try {
const config = GM_getValue(key);
// 检查配置是否存在
if (!config) {
return null;
}
// 升级配置到最新版本
const upgradedConfig = this.upgradeConfig(config);
if (!upgradedConfig) {
console.warn('getConfig: 配置升级失败', key);
return null;
}
return upgradedConfig;
} catch (e) {
Utils.showError(CONSTANTS.MESSAGES.errors.readFailed, e);
return null;
}
},
/**
* 保存配置
* @param {Object} config 配置对象
* @param {string} masterPassword 主密码(可选)
* @returns {string} 配置键
*/
async saveConfig(config, masterPassword = null) {
// 验证配置
const validation = this.validateConfig(config);
if (!validation.valid) {
Utils.showError(validation.errors.join('\n'));
return null;
}
// 如果提供了主密码且密码未加密,则加密密码
if (masterPassword && config.password && !Utils.isPasswordEncrypted(config.password)) {
try {
config.password = await Utils.encryptPassword(config.password, masterPassword);
} catch (error) {
console.warn('密码加密失败,使用明文存储:', error);
}
}
// 检查是否为新建配置
const isNewConfig = !config.createdAt || !config.updatedAt;
// 添加版本信息
config.version = this.CONFIG_VERSION;
// 时间戳处理
if (isNewConfig) {
// 新建配置:设置创建时间和更新时间
config.createdAt = new Date().toISOString();
config.updatedAt = new Date().toISOString();
} else {
// 更新配置:只更新修改时间,保持创建时间不变
config.updatedAt = new Date().toISOString();
}
try {
const key = this.CONFIG_PREFIX + config.currentUrl;
GM_setValue(key, config);
return key;
} catch (e) {
Utils.showError(CONSTANTS.MESSAGES.errors.saveFailed, e);
return null;
}
},
/**
* 删除配置
* @param {string} key 配置键
*/
deleteConfig(key) {
try {
GM_deleteValue(key);
} catch (e) {
Utils.showError(CONSTANTS.MESSAGES.errors.deleteFailed, e);
}
},
/**
* 获取配置(带解密)
* @param {string} key 配置键
* @param {string} masterPassword 主密码(可选)
* @returns {Object} 配置对象
*/
async getConfigDecrypted(key, masterPassword = null) {
try {
const config = GM_getValue(key);
if (!config) return null;
// 检查配置是否为有效对象
if (typeof config !== 'object') {
console.warn('getConfigDecrypted: 配置不是有效对象', key, config);
return null;
}
// 升级配置到最新版本
const upgradedConfig = this.upgradeConfig(config);
if (!upgradedConfig) {
console.warn('getConfigDecrypted: 配置升级失败', key);
return null;
}
// 如果提供了主密码且密码已加密,则解密密码
if (masterPassword && upgradedConfig.password && Utils.isPasswordEncrypted(upgradedConfig.password)) {
try {
upgradedConfig.password = await Utils.decryptPassword(upgradedConfig.password, masterPassword);
} catch (error) {
console.warn('密码解密失败:', error);
// 解密失败时保持加密状态
}
}
return upgradedConfig;
} catch (e) {
Utils.showError(CONSTANTS.MESSAGES.errors.readFailed, e);
return null;
}
}
};
// UI渲染模块
const UIRenderer = {
/**
* 渲染配置列表
* @param {Array} configs 配置数组
* @param {string} activeKey 当前选中的配置键
* @param {string} filter 搜索过滤条件
*/
renderConfigList(configs, activeKey, filter = '') {
const list = document.getElementById('config-list');
list.innerHTML = '';
let filtered = configs;
if (filter) {
const kw = filter.trim().toLowerCase();
filtered = configs.filter(c =>
(c.data.nickname || '').toLowerCase().includes(kw) ||
(c.data.currentUrl || '').toLowerCase().includes(kw)
);
}
if (filtered.length === 0) {
list.innerHTML = '<div style="color:#aaa;padding:20px 0;text-align:center;">无匹配配置</div>';
return;
}
// 获取当前网页URL(不含查询参数和hash)
const currentUrl = window.location.origin + window.location.pathname;
// 对配置进行排序:匹配当前网页的配置优先显示
filtered.sort((a, b) => {
const aMatchesCurrent = a.data.currentUrl === currentUrl;
const bMatchesCurrent = b.data.currentUrl === currentUrl;
// 如果a匹配当前网页而b不匹配,a排在前面
if (aMatchesCurrent && !bMatchesCurrent) return -1;
// 如果b匹配当前网页而a不匹配,b排在前面
if (!aMatchesCurrent && bMatchesCurrent) return 1;
// 如果都匹配或都不匹配,按更新时间排序(新的在前)
const aUpdated = a.data.updatedAt ? new Date(a.data.updatedAt).getTime() : 0;
const bUpdated = b.data.updatedAt ? new Date(b.data.updatedAt).getTime() : 0;
return bUpdated - aUpdated;
});
filtered.forEach(c => {
const div = document.createElement('div');
let className = 'config-item';
if (c.key === activeKey) className += ' active';
// 检查是否匹配当前网页
const matchesCurrent = c.data.currentUrl === currentUrl;
if (matchesCurrent) className += ' current-page';
div.className = className;
div.dataset.key = c.key;
// 只显示到网址部分(不显示路径后的内容)
let urlShow = '';
try {
const u = new URL(c.data.currentUrl);
urlShow = u.origin;
} catch (e) {
urlShow = c.data.currentUrl || '';
}
// 为匹配当前网页的配置添加特殊样式
const currentIndicator = matchesCurrent ? '<span style="color:#4d7cfe;font-weight:600;margin-right:4px;">📍</span>' : '';
const nicknameStyle = matchesCurrent ? 'style="color:#4d7cfe;font-weight:600;"' : '';
div.innerHTML = `
<div class="nickname" ${nicknameStyle}>
${currentIndicator}${Utils.escapeHTML(c.data.nickname || '未命名')}
</div>
<div class="url">${Utils.escapeHTML(urlShow)}</div>
`;
list.appendChild(div);
});
},
/**
* 将配置加载到表单
* @param {Object} config 配置对象
*/
async loadConfigToForm(config) {
document.getElementById('edit-url').value = config.currentUrl || '';
document.getElementById('edit-nickname').value = config.nickname || '';
document.getElementById('edit-username').value = config.username || '';
// 处理密码解密
let displayPassword = config.password || '';
if (Utils.isPasswordEncrypted(config.password)) {
try {
displayPassword = await Utils.decryptPassword(config.password, CONSTANTS.ENCRYPTION.MASTER_PASSWORD);
} catch (error) {
console.warn('密码解密失败,显示加密状态:', error);
displayPassword = config.password; // 解密失败时显示加密的密码
}
}
document.getElementById('edit-password').value = displayPassword;
document.getElementById('edit-username-selector').value = config.selectors?.username || '';
document.getElementById('edit-password-selector').value = config.selectors?.password || '';
document.getElementById('edit-login-button-selector').value = config.selectors?.loginButton || '';
// 处理先手点击按钮选择器(数组转字符串)
const preClickButtons = config.selectors?.preClickButtons || [];
document.getElementById('edit-pre-click-selector').value = Array.isArray(preClickButtons) ? preClickButtons.join(', ') : preClickButtons;
document.getElementById('edit-autoLogin').checked = !!config.autoLogin;
document.getElementById('edit-autoFill').checked = !!config.autoFill;
// 显示密码加密状态
const passwordInput = document.getElementById('edit-password');
if (passwordInput && Utils.isPasswordEncrypted(config.password)) {
passwordInput.style.borderColor = '#28a745';
passwordInput.style.backgroundColor = '#f8fff9';
passwordInput.title = '密码已加密存储';
} else {
passwordInput.style.borderColor = '';
passwordInput.style.backgroundColor = '';
passwordInput.title = '';
}
// 切换配置时不自动展开选择器区域,保持当前折叠状态
const section = document.getElementById('selectors-section');
const btn = document.getElementById('toggle-selectors-btn');
const listArea = document.querySelector('.config-list-area');
if (section && btn) {
section.style.display = selectorsCollapsed ? 'none' : '';
btn.textContent = selectorsCollapsed ? '▲' : '▼';
// 同步调整左侧列表高度
if (listArea) {
listArea.style.height = selectorsCollapsed ? CONSTANTS.PANEL_DIMENSIONS.height.collapsed : CONSTANTS.PANEL_DIMENSIONS.height.expanded;
}
}
// 更新跳转按钮显示状态
updateGotoBtnVisibility();
},
/**
* 从表单获取配置数据
* @returns {Object} 配置对象
*/
getConfigFromForm() {
// 处理先手点击按钮选择器(字符串转数组)
const preClickSelectorStr = document.getElementById('edit-pre-click-selector').value.trim();
const preClickButtons = preClickSelectorStr ? preClickSelectorStr.split(',').map(s => s.trim()).filter(s => s) : [];
return {
currentUrl: document.getElementById('edit-url').value.trim(),
nickname: document.getElementById('edit-nickname').value.trim(),
username: document.getElementById('edit-username').value.trim(),
password: document.getElementById('edit-password').value.trim(),
autoLogin: document.getElementById('edit-autoLogin').checked,
autoFill: document.getElementById('edit-autoFill').checked,
selectors: {
username: document.getElementById('edit-username-selector').value.trim(),
password: document.getElementById('edit-password-selector').value.trim(),
loginButton: document.getElementById('edit-login-button-selector').value.trim(),
preClickButtons: preClickButtons
}
};
}
};
// 事件绑定模块
const EventBinder = {
currentKey: null,
allConfigs: [],
/**
* 绑定所有事件监听器
*/
bindAll() {
// 搜索
document.getElementById('config-search').addEventListener('input', e => {
UIRenderer.renderConfigList(this.allConfigs, this.currentKey, e.target.value);
});
// 列表点击
document.getElementById('config-list').addEventListener('click', async (e) => {
const item = e.target.closest('.config-item');
if (!item) return;
this.currentKey = item.dataset.key;
// 使用CONSTANTS中的主密码
const masterPassword = CONSTANTS.ENCRYPTION.MASTER_PASSWORD;
// 获取配置(带解密)
const config = await DataManager.getConfigDecrypted(this.currentKey, masterPassword);
if (config) await UIRenderer.loadConfigToForm(config);
UIRenderer.renderConfigList(this.allConfigs, this.currentKey, document.getElementById('config-search').value);
});
// 新建
document.getElementById('new-config-btn').addEventListener('click', async () => {
const template = DataManager.getConfigTemplate('default');
template.currentUrl = window.location.origin + window.location.pathname;
template.nickname = document.title || '新配置';
await UIRenderer.loadConfigToForm(template);
this.currentKey = null;
UIRenderer.renderConfigList(this.allConfigs, this.currentKey, document.getElementById('config-search').value);
// 新建时自动展开选择器区域
selectorsCollapsed = false;
const section = document.getElementById('selectors-section');
const btn = document.getElementById('toggle-selectors-btn');
const listArea = document.querySelector('.config-list-area');
if (section && btn) {
section.style.display = '';
btn.textContent = '▼';
// 同步调整左侧列表高度
if (listArea) {
listArea.style.height = CONSTANTS.PANEL_DIMENSIONS.height.expanded;
}
}
// 更新跳转按钮显示状态
updateGotoBtnVisibility();
});
// 保存
document.getElementById('save-config-btn').addEventListener('click', async () => {
const config = UIRenderer.getConfigFromForm();
if (!config.currentUrl) return Utils.showError(CONSTANTS.MESSAGES.errors.urlRequired);
if (!config.nickname) return Utils.showError(CONSTANTS.MESSAGES.errors.nicknameRequired);
// 如果是更新已有配置,保留原有的时间戳信息
if (this.currentKey) {
try {
const existingConfig = DataManager.getConfig(this.currentKey);
if (existingConfig) {
config.createdAt = existingConfig.createdAt;
config.updatedAt = existingConfig.updatedAt;
config.version = existingConfig.version;
}
} catch (e) {
console.warn('获取原有配置失败,将作为新配置处理:', e);
}
}
// 使用CONSTANTS中的主密码
const masterPassword = CONSTANTS.ENCRYPTION.MASTER_PASSWORD;
// 保存配置(带加密)
const key = await DataManager.saveConfig(config, masterPassword);
this.currentKey = key;
this.allConfigs = DataManager.getAllConfigs();
UIRenderer.renderConfigList(this.allConfigs, this.currentKey, document.getElementById('config-search').value);
Utils.showMsg(CONSTANTS.MESSAGES.success.configSaved);
});
// 删除
document.getElementById('delete-config-btn').addEventListener('click', async () => {
if (!this.currentKey) return Utils.showError(CONSTANTS.MESSAGES.errors.configRequired);
Utils.showConfirm(CONSTANTS.MESSAGES.confirm.deleteConfig, async () => {
DataManager.deleteConfig(this.currentKey);
this.currentKey = null;
this.allConfigs = DataManager.getAllConfigs();
UIRenderer.renderConfigList(this.allConfigs, this.currentKey, document.getElementById('config-search').value);
await UIRenderer.loadConfigToForm({});
Utils.showMsg(CONSTANTS.MESSAGES.success.configDeleted);
});
});
},
/**
* 初始化事件绑定器
*/
async init() {
this.allConfigs = DataManager.getAllConfigs();
// 优先根据当前网址自动选中配置
const currentUrl = window.location.origin + window.location.pathname;
const matchKey = this.allConfigs.find(c => c.data.currentUrl === currentUrl)?.key;
this.currentKey = matchKey || null;
UIRenderer.renderConfigList(this.allConfigs, this.currentKey, '');
if (this.currentKey) {
// 使用CONSTANTS中的主密码
const masterPassword = CONSTANTS.ENCRYPTION.MASTER_PASSWORD;
// 获取配置(带解密)
const config = await DataManager.getConfigDecrypted(this.currentKey, masterPassword);
if (config) await UIRenderer.loadConfigToForm(config);
} else {
// 如果当前网页没有配置信息,自动触发新建功能
const template = DataManager.getConfigTemplate('default');
template.currentUrl = currentUrl;
template.nickname = document.title || '新配置';
await UIRenderer.loadConfigToForm(template);
// 新建时自动展开选择器区域
selectorsCollapsed = false;
const section = document.getElementById('selectors-section');
const btn = document.getElementById('toggle-selectors-btn');
const listArea = document.querySelector('.config-list-area');
if (section && btn) {
section.style.display = '';
btn.textContent = '▼';
// 同步调整左侧列表高度
if (listArea) {
listArea.style.height = CONSTANTS.PANEL_DIMENSIONS.height.expanded;
}
}
// 更新跳转按钮显示状态
updateGotoBtnVisibility();
}
this.bindAll();
}
};
// 工具函数模块
const Utils = {
/**
* 显示错误提示弹窗
* @param {string} msg 错误消息
* @param {Error} err 错误对象
*/
showError(msg, err) {
// 创建错误提示弹窗
const errorDialog = document.createElement('div');
errorDialog.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
z-index: 99999999;
max-width: 400px;
border: 2px solid #e74c3c;
`;
errorDialog.innerHTML = `
<div style="display:flex;align-items:center;gap:10px;margin-bottom:15px;">
<span style="font-size:20px;">❌</span>
<h3 style="margin:0;color:#e74c3c;">错误</h3>
</div>
<p style="margin:0 0 15px 0;color:#333;">${msg}</p>
<button onclick="this.parentElement.remove()" style="
background:#e74c3c;
color:white;
border:none;
padding:8px 16px;
border-radius:4px;
cursor:pointer;
">确定</button>
`;
document.body.appendChild(errorDialog);
if (err) console.error(msg, err);
},
/**
* 显示消息提示
* @param {string} msg 消息内容
* @param {string} type 消息类型
*/
showMsg(msg, type = 'success') {
// 创建成功提示弹窗
const msgDialog = document.createElement('div');
const isSuccess = type === 'success';
const color = isSuccess ? '#2ecc71' : '#3498db';
const icon = isSuccess ? '✅' : 'ℹ️';
msgDialog.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #fff;
padding: 15px 20px;
border-radius: 6px;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
z-index: 99999999;
border-left: 4px solid ${color};
animation: slideIn 0.3s ease;
`;
msgDialog.innerHTML = `
<div style="display:flex;align-items:center;gap:8px;">
<span style="font-size:16px;">${icon}</span>
<span style="color:#333;">${msg}</span>
</div>
`;
document.body.appendChild(msgDialog);
// 3秒后自动消失
setTimeout(() => {
if (msgDialog.parentElement) {
msgDialog.style.animation = 'slideOut 0.3s ease';
msgDialog.addEventListener('animationend', () => {
if (msgDialog.parentElement) {
msgDialog.remove();
}
}, { once: true });
}
}, 3000);
},
/**
* 显示确认对话框
* @param {string} msg 确认消息
* @param {Function} onConfirm 确认回调
* @param {Function} onCancel 取消回调
*/
showConfirm(msg, onConfirm, onCancel) {
// 创建确认对话框
const confirmDialog = document.createElement('div');
confirmDialog.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 99999999;
`;
confirmDialog.innerHTML = `
<div style="
background: #fff;
padding: 25px;
border-radius: 8px;
max-width: 400px;
text-align: center;
">
<div style="font-size:24px;margin-bottom:15px;">⚠️</div>
<p style="margin:0 0 20px 0;color:#333;">${msg}</p>
<div style="display:flex;gap:10px;justify-content:center;">
<button id="confirm-yes" style="
background: #e74c3c;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
">确定</button>
<button id="confirm-no" style="
background: #95a5a6;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
">取消</button>
</div>
</div>
`;
document.body.appendChild(confirmDialog);
// 绑定事件
confirmDialog.querySelector('#confirm-yes').onclick = () => {
confirmDialog.remove();
if (onConfirm) onConfirm();
};
confirmDialog.querySelector('#confirm-no').onclick = () => {
confirmDialog.remove();
if (onCancel) onCancel();
};
// 将焦点设置到确认按钮上
setTimeout(() => {
const confirmBtn = confirmDialog.querySelector('#confirm-yes');
if (confirmBtn) {
confirmBtn.focus();
}
}, 300);
},
/**
* HTML转义
* @param {string} str 原始字符串
* @returns {string} 转义后的字符串
*/
escapeHTML(str) {
return (str || '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
},
/**
* 防抖函数
* @param {Function} fn 要防抖的函数
* @param {number} delay 延迟时间
* @returns {Function} 防抖后的函数
*/
debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
},
/**
* 高亮匹配的元素
* @param {string} selector CSS选择器
* @param {boolean} checkPopup 是否检测弹窗元素
* @returns {number} 匹配的元素数量
*/
highlightElements(selector, checkPopup = true) {
let elements;
try {
elements = document.querySelectorAll(selector);
} catch (e) {
return 0;
}
if (!elements.length) return 0;
// 如果需要检测弹窗元素,过滤掉弹窗内的元素
if (checkPopup) {
elements = Array.from(elements).filter(el => !this.isPopupElement(el));
if (elements.length === 0) return 0;
}
const original = [];
elements.forEach(el => {
original.push({ el, bg: el.style.backgroundColor, outline: el.style.outline });
el.style.backgroundColor = 'rgba(255,255,0,0.3)';
el.style.outline = '2px solid #ff9800';
});
setTimeout(() => {
original.forEach(({ el, bg, outline }) => {
el.style.backgroundColor = bg;
el.style.outline = outline;
});
}, CONSTANTS.ANIMATION.highlightDuration);
return elements.length;
},
/**
* 检查元素是否为弹窗
* @param {Element} element DOM元素
* @returns {boolean} 是否为弹窗元素
*/
isPopupElement(element) {
// 辅助函数:检查元素是否匹配选择器
const matchesSelector = (el, selectors) => {
return selectors.some(selector => el.matches(selector));
};
// 辅助函数:检查文本是否包含关键词
const containsKeywords = (text, keywords) => {
return keywords.some(keyword => text.toLowerCase().includes(keyword));
};
// 辅助函数:检查元素样式是否为弹窗
const isPopupStyle = (el) => {
const style = window.getComputedStyle(el);
const position = style.position;
const zIndex = parseInt(style.zIndex) || 0;
return position === 'fixed' && zIndex > CONSTANTS.POPUP.minZIndex;
};
// 检查元素本身
if (matchesSelector(element, CONSTANTS.POPUP.selectors)) {
return true;
}
// 检查元素的class和id
const className = element.className || '';
const id = element.id || '';
if (containsKeywords(className, CONSTANTS.POPUP.keywords) || containsKeywords(id, CONSTANTS.POPUP.keywords)) {
return true;
}
// 检查元素的样式
if (isPopupStyle(element)) {
return true;
}
// 检查父元素是否为弹窗
let parent = element.parentElement;
while (parent && parent !== document.body) {
if (matchesSelector(parent, CONSTANTS.POPUP.selectors) || isPopupStyle(parent)) {
return true;
}
parent = parent.parentElement;
}
return false;
},
/**
* 生成加密密钥
* @param {string} password 用户密码
* @returns {Promise<CryptoKey>} 加密密钥
*/
async generateEncryptionKey(password) {
try {
// 将密码和盐值组合
const passwordWithSalt = password + CONSTANTS.ENCRYPTION.SALT;
// 将字符串转换为ArrayBuffer
// 检查 crypto.subtle 是否可用
if (!window.crypto || !window.crypto.subtle) {
throw new Error('当前环境不支持 Web Crypto API,无法生成加密密钥');
}
const encoder = new TextEncoder();
const passwordBuffer = encoder.encode(passwordWithSalt);
// 使用PBKDF2派生密钥
const baseKey = await crypto.subtle.importKey(
'raw',
passwordBuffer,
{ name: 'PBKDF2' },
false,
['deriveBits', 'deriveKey']
);
// 派生加密密钥
const encryptionKey = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: encoder.encode(CONSTANTS.ENCRYPTION.SALT),
iterations: CONSTANTS.ENCRYPTION.KEY_DERIVATION.iterations,
hash: 'SHA-256'
},
baseKey,
{ name: CONSTANTS.ENCRYPTION.ALGORITHM, length: CONSTANTS.ENCRYPTION.KEY_DERIVATION.keyLength },
false,
['encrypt', 'decrypt']
);
return encryptionKey;
} catch (error) {
console.error('生成加密密钥失败:', error);
throw new Error('加密密钥生成失败');
}
},
/**
* 加密密码
* @param {string} password 原始密码
* @param {string} masterPassword 主密码
* @returns {Promise<string>} 加密后的密码
*/
async encryptPassword(password, masterPassword) {
try {
if (!password || !masterPassword) {
return password; // 如果没有密码或主密码,返回原密码
}
const key = await this.generateEncryptionKey(masterPassword);
const encoder = new TextEncoder();
const passwordBuffer = encoder.encode(password);
// 生成随机IV
const iv = crypto.getRandomValues(new Uint8Array(12));
// 加密数据
const encryptedData = await crypto.subtle.encrypt(
{
name: CONSTANTS.ENCRYPTION.ALGORITHM,
iv: iv
},
key,
passwordBuffer
);
// 将IV和加密数据组合并转换为Base64
const combined = new Uint8Array(iv.length + encryptedData.byteLength);
combined.set(iv);
combined.set(new Uint8Array(encryptedData), iv.length);
const encryptedString = btoa(String.fromCharCode(...combined));
return CONSTANTS.ENCRYPTION.ENCRYPTED_PREFIX + encryptedString;
} catch (error) {
console.error('密码加密失败:', error);
return password; // 加密失败时返回原密码
}
},
/**
* 解密密码
* @param {string} encryptedPassword 加密的密码
* @param {string} masterPassword 主密码
* @returns {Promise<string>} 解密后的密码
*/
async decryptPassword(encryptedPassword, masterPassword) {
try {
if (!encryptedPassword || !masterPassword || !encryptedPassword.startsWith(CONSTANTS.ENCRYPTION.ENCRYPTED_PREFIX)) {
return encryptedPassword; // 如果不是加密密码,返回原密码
}
let key;
try {
key = await this.generateEncryptionKey(masterPassword);
} catch (error) {
console.warn(CONSTANTS.MESSAGES.errors.cryptoUnsupported);
return encryptedPassword; // 返回原始加密字符串
}
const encryptedString = encryptedPassword.substring(CONSTANTS.ENCRYPTION.ENCRYPTED_PREFIX.length);
// 从Base64解码
const combined = new Uint8Array(atob(encryptedString).split('').map(char => char.charCodeAt(0)));
// 提取IV和加密数据
const iv = combined.slice(0, 12);
const encryptedData = combined.slice(12);
// 解密数据
const decryptedData = await crypto.subtle.decrypt(
{
name: CONSTANTS.ENCRYPTION.ALGORITHM,
iv: iv
},
key,
encryptedData
);
const decoder = new TextDecoder();
return decoder.decode(decryptedData);
} catch (error) {
console.error('密码解密失败:', error);
throw error; // 解密失败时抛出错误
}
},
/**
* 检查密码是否已加密
* @param {string} password 密码
* @returns {boolean} 是否已加密
*/
isPasswordEncrypted(password) {
return password && password.startsWith(CONSTANTS.ENCRYPTION.ENCRYPTED_PREFIX);
},
/**
* 安全地添加事件监听器,并记录到清理数组
* @param {Element} element 目标元素
* @param {string} type 事件类型
* @param {Function} handler 事件处理函数
* @param {Object} options 事件选项
*/
addSafeEventListener(element, type, handler, options = {}) {
element.addEventListener(type, handler, options);
// 记录到清理数组
if (window.loginManagerEventListeners) {
window.loginManagerEventListeners.push({
element: element,
type: type,
handler: handler,
options: options
});
}
},
/**
* 安全地添加文档级别的事件监听器
* @param {string} type 事件类型
* @param {Function} handler 事件处理函数
* @param {Object} options 事件选项
*/
addDocumentEventListener(type, handler, options = {}) {
document.addEventListener(type, handler, options);
// 记录到清理数组
if (window.loginManagerEventListeners) {
window.loginManagerEventListeners.push({
element: document,
type: type,
handler: handler,
options: options
});
}
}
};
// ========== 先手点击执行函数 ==========
/**
* 执行先手点击操作
* @param {Object} config 配置对象
*/
async function executePreClickActions(config) {
const preClickButtons = config.selectors?.preClickButtons || [];
if (!Array.isArray(preClickButtons) || preClickButtons.length === 0) {
console.log('🔍 无需执行先手点击操作');
return;
}
console.log(`🎯 开始执行先手点击操作,共 ${preClickButtons.length} 个按钮`);
for (let i = 0; i < preClickButtons.length; i++) {
const selector = preClickButtons[i].trim();
if (!selector) continue;
try {
const button = document.querySelector(selector);
if (button) {
console.log(`✅ 找到先手点击按钮 [${i + 1}/${preClickButtons.length}]: ${selector}`);
// 高亮显示要点击的按钮
const originalStyle = button.style.outline;
button.style.outline = '2px solid #ff6b6b';
button.style.outlineOffset = '2px';
// 点击按钮
button.click();
console.log(`🖱️ 已点击按钮: ${selector}`);
// 恢复按钮样式
setTimeout(() => {
button.style.outline = originalStyle;
button.style.outlineOffset = '';
}, 1000);
// 等待一段时间让页面响应
if (i < preClickButtons.length - 1) {
await new Promise(resolve => setTimeout(resolve, CONSTANTS.ANIMATION.preClickDelay));
}
} else {
console.warn(`⚠️ 未找到先手点击按钮 [${i + 1}/${preClickButtons.length}]: ${selector}`);
}
} catch (error) {
console.error(`❌ 先手点击按钮失败 [${i + 1}/${preClickButtons.length}]: ${selector}`, error);
}
}
console.log('✅ 先手点击操作执行完成');
}
// ========== 选择器验证与实时预览 ==========
/**
* 设置选择器验证功能
*/
function setupSelectorValidation() {
const selectorFields = [
{ input: 'edit-username-selector', btn: 'check-username-selector' },
{ input: 'edit-password-selector', btn: 'check-password-selector' },
{ input: 'edit-login-button-selector', btn: 'check-login-button-selector' },
{ input: 'edit-pre-click-selector', btn: 'check-pre-click-selector' }
];
selectorFields.forEach(({ input, btn }) => {
const inputEl = document.getElementById(input);
const btnEl = document.getElementById(btn);
// 点击按钮验证
btnEl.addEventListener('click', () => {
const selector = inputEl.value.trim();
if (!selector) {
btnEl.textContent = '❌';
btnEl.style.color = 'red';
return;
}
// 特殊处理先手点击选择器(支持多个选择器)
if (input === 'edit-pre-click-selector') {
const selectors = selector.split(',').map(s => s.trim()).filter(s => s);
let totalCount = 0;
let foundCount = 0;
selectors.forEach((sel, index) => {
try {
const count = Utils.highlightElements(sel, false);
totalCount += count;
if (count > 0) foundCount++;
console.log(`先手点击选择器 [${index + 1}]: ${sel} - 找到 ${count} 个元素`);
} catch (e) {
console.warn(`先手点击选择器 [${index + 1}] 验证失败: ${sel}`, e);
}
});
if (foundCount === selectors.length && totalCount > 0) {
btnEl.textContent = '✅';
btnEl.style.color = 'green';
} else if (foundCount > 0) {
btnEl.textContent = '⚠️';
btnEl.style.color = 'orange';
} else {
btnEl.textContent = '❌';
btnEl.style.color = 'red';
}
return;
}
// 普通选择器验证
let count = 0;
try {
// 验证时不检测弹窗元素,允许验证弹窗内的元素
count = Utils.highlightElements(selector, false);
} catch (e) {
count = 0;
}
if (count > 0) {
btnEl.textContent = '✅';
btnEl.style.color = 'green';
} else {
btnEl.textContent = '❌';
btnEl.style.color = 'red';
}
});
// 输入时实时预览(防抖)
inputEl.addEventListener('input', Utils.debounce(function () {
btnEl.textContent = '🔄';
btnEl.style.color = '';
const selector = inputEl.value.trim();
if (!selector) return;
// 特殊处理先手点击选择器(支持多个选择器)
if (input === 'edit-pre-click-selector') {
const selectors = selector.split(',').map(s => s.trim()).filter(s => s);
selectors.forEach(sel => {
try {
Utils.highlightElements(sel, false);
} catch (e) {
// 忽略预览错误
}
});
return;
}
// 普通选择器预览
// 预览时不检测弹窗元素,允许预览弹窗内的元素
Utils.highlightElements(selector, false);
}, CONSTANTS.ANIMATION.debounceDelay));
});
}
// ========== 选择器折叠功能 ==========
let selectorsCollapsed = true; // 折叠状态全局变量,默认闭合
/**
* 设置选择器区域折叠功能
*/
function setupSelectorCollapse() {
const btn = document.getElementById('toggle-selectors-btn');
const section = document.getElementById('selectors-section');
const row = document.querySelector('.selector-toggle-row');
const listArea = document.querySelector('.config-list-area');
// 初始化状态(每次都强制刷新)
section.style.display = selectorsCollapsed ? 'none' : '';
btn.textContent = selectorsCollapsed ? '▲' : '▼';
// 设置初始高度
if (listArea) {
listArea.style.height = selectorsCollapsed ? CONSTANTS.PANEL_DIMENSIONS.height.collapsed : CONSTANTS.PANEL_DIMENSIONS.height.expanded;
}
// 绑定整行点击
if (row) {
// 移除所有现有事件监听器
const newRow = row.cloneNode(true);
row.parentNode.replaceChild(newRow, row);
// 重新绑定事件
newRow.addEventListener('click', function (e) {
// 避免label选中文字时误触
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
selectorsCollapsed = !selectorsCollapsed;
section.style.display = selectorsCollapsed ? 'none' : '';
btn.textContent = selectorsCollapsed ? '▲' : '▼';
// 动态调整左侧列表高度
if (listArea) {
listArea.style.height = selectorsCollapsed ? CONSTANTS.PANEL_DIMENSIONS.height.collapsed : CONSTANTS.PANEL_DIMENSIONS.height.expanded;
}
});
}
}
// ========== 密码显示/隐藏切换 ==========
/**
* 设置密码显示/隐藏切换功能
*/
function setupPasswordToggle() {
const pwdInput = document.getElementById('edit-password');
const toggleBtn = document.getElementById('toggle-edit-password');
if (!pwdInput || !toggleBtn) return;
// 克隆按钮并替换原有按钮,确保事件监听器被完全清除
const newToggleBtn = toggleBtn.cloneNode(true);
toggleBtn.parentNode.replaceChild(newToggleBtn, toggleBtn);
newToggleBtn.addEventListener('click', function () {
if (pwdInput.type === 'password') {
pwdInput.type = 'text';
newToggleBtn.textContent = '🙈';
} else {
pwdInput.type = 'password';
newToggleBtn.textContent = '👁';
}
});
}
// ========== 导入/导出全部配置 ==========
/**
* 设置导入导出功能
* @param {Object} EventBinder 事件绑定器对象
*/
function setupImportExport(EventBinder) {
// 创建导入对话框
let importDialog = document.getElementById('import-dialog');
if (!importDialog) {
importDialog = document.createElement('div');
importDialog.id = 'import-dialog';
importDialog.style.display = 'none';
importDialog.style.position = 'fixed';
importDialog.style.top = '0';
importDialog.style.left = '0';
importDialog.style.width = '100vw';
importDialog.style.height = '100vh';
importDialog.style.background = 'rgba(0,0,0,0.4)';
importDialog.style.zIndex = '100000';
importDialog.innerHTML = `
<div style="
background:#fff;
max-width:450px;
margin:10vh auto;
padding:25px;
border-radius:10px;
box-shadow:0 8px 30px rgba(0,0,0,0.3);
border:1px solid #e0e0e0;
">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:20px;">
<span style="font-size:20px;">📥</span>
<h3 style="margin:0;color:#333;font-size:18px;">导入配置</h3>
</div>
<p style="margin:0 0 15px 0;color:#666;font-size:14px;">请粘贴导出的配置JSON数据:</p>
<textarea id="import-textarea" placeholder="在此粘贴JSON配置数据..." style="
width:100%;
height:180px;
padding:12px;
border:2px solid #3498db;
border-radius:6px;
font-family:'Courier New',monospace;
font-size:13px;
resize:vertical;
box-sizing:border-box;
outline:none;
"></textarea>
<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:20px;">
<button id="confirm-import" style="
background:linear-gradient(to right,#2ecc71,#27ae60);
color:white;
border:none;
padding:10px 20px;
border-radius:6px;
cursor:pointer;
font-weight:600;
transition:all 0.2s;
">确认导入</button>
<button id="cancel-import" style="
background:linear-gradient(to right,#e74c3c,#c0392b);
color:white;
border:none;
padding:10px 20px;
border-radius:6px;
cursor:pointer;
font-weight:600;
transition:all 0.2s;
">取消</button>
</div>
</div>
`;
document.body.appendChild(importDialog);
}
// 创建导出密码选择对话框
let exportPasswordDialog = document.getElementById('export-password-dialog');
if (!exportPasswordDialog) {
exportPasswordDialog = document.createElement('div');
exportPasswordDialog.id = 'export-password-dialog';
exportPasswordDialog.style.display = 'none';
exportPasswordDialog.style.position = 'fixed';
exportPasswordDialog.style.top = '0';
exportPasswordDialog.style.left = '0';
exportPasswordDialog.style.width = '100vw';
exportPasswordDialog.style.height = '100vh';
exportPasswordDialog.style.background = 'rgba(0,0,0,0.4)';
exportPasswordDialog.style.zIndex = '100001';
exportPasswordDialog.innerHTML = `
<div style="
background:#fff;
max-width:450px;
margin:20vh auto;
padding:25px;
border-radius:10px;
box-shadow:0 8px 30px rgba(0,0,0,0.3);
border:1px solid #e0e0e0;
">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:20px;">
<span style="font-size:20px;">📤</span>
<h3 style="margin:0;color:#333;font-size:18px;">导出配置</h3>
</div>
<p style="margin:0 0 15px 0;color:#666;font-size:14px;">请选择导出时的密码处理方式:</p>
<div style="margin-bottom:20px;">
<div style="margin-bottom:10px;">
<input type="radio" id="export-option-current" name="export-password-option" value="current" checked>
<label for="export-option-current" style="margin-left:8px;">使用当前主密码(保持加密状态)</label>
</div>
<div style="margin-bottom:10px;">
<input type="radio" id="export-option-new" name="export-password-option" value="new">
<label for="export-option-new" style="margin-left:8px;">使用新密码重新加密</label>
</div>
<div style="margin-bottom:10px;">
<input type="radio" id="export-option-none" name="export-password-option" value="none">
<label for="export-option-none" style="margin-left:8px;">无密码导出(明文)</label>
</div>
</div>
<div id="new-password-input" style="display:none;margin-bottom:15px;">
<label style="display:block;margin-bottom:5px;color:#555;">新密码:</label>
<input type="password" id="new-password-field" placeholder="输入新的主密码" style="
width:100%;
padding:8px;
border:1px solid #ddd;
border-radius:4px;
box-sizing:border-box;
">
<div style="font-size:12px;color:#666;margin-top:5px;">
💡 新密码将用于加密导出的配置数据
</div>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button id="confirm-export-option" style="
background:linear-gradient(to right,#2ecc71,#27ae60);
color:white;
border:none;
padding:8px 16px;
border-radius:4px;
cursor:pointer;
font-weight:600;
">确定</button>
<button id="cancel-export-option" style="
background:linear-gradient(to right,#e74c3c,#c0392b);
color:white;
border:none;
padding:8px 16px;
border-radius:4px;
cursor:pointer;
font-weight:600;
">取消</button>
</div>
</div>
`;
document.body.appendChild(exportPasswordDialog);
}
// 创建密码输入对话框
let passwordDialog = document.getElementById('password-input-dialog');
if (!passwordDialog) {
passwordDialog = document.createElement('div');
passwordDialog.id = 'password-input-dialog';
passwordDialog.style.display = 'none';
passwordDialog.style.position = 'fixed';
passwordDialog.style.top = '0';
passwordDialog.style.left = '0';
passwordDialog.style.width = '100vw';
passwordDialog.style.height = '100vh';
passwordDialog.style.background = 'rgba(0,0,0,0.4)';
passwordDialog.style.zIndex = '100001';
passwordDialog.innerHTML = `
<div style="
background:#fff;
max-width:400px;
margin:20vh auto;
padding:25px;
border-radius:10px;
box-shadow:0 8px 30px rgba(0,0,0,0.3);
border:1px solid #e0e0e0;
">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:20px;">
<span style="font-size:20px;">🔐</span>
<h3 style="margin:0;color:#333;font-size:18px;">密码解密失败</h3>
</div>
<p style="margin:0 0 15px 0;color:#666;font-size:14px;">检测到加密的密码无法解密,可能是主密码不同。请选择处理方式:</p>
<div style="margin-bottom:20px;">
<div style="margin-bottom:10px;">
<input type="radio" id="option-keep-encrypted" name="password-option" value="keep" checked>
<label for="option-keep-encrypted" style="margin-left:8px;">保持加密状态(需要手动输入密码)</label>
</div>
<div style="margin-bottom:10px;">
<input type="radio" id="option-clear-password" name="password-option" value="clear">
<label for="option-clear-password" style="margin-left:8px;">清空密码字段</label>
</div>
<div style="margin-bottom:10px;">
<input type="radio" id="option-manual-input" name="password-option" value="manual">
<label for="option-manual-input" style="margin-left:8px;">手动输入主密码</label>
</div>
</div>
<div id="manual-password-input" style="display:none;margin-bottom:15px;">
<label style="display:block;margin-bottom:5px;color:#555;">手动输入密码:</label>
<input type="password" id="manual-password-field" placeholder="输入密码" style="
width:100%;
padding:8px;
border:1px solid #ddd;
border-radius:4px;
box-sizing:border-box;
">
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button id="confirm-password-option" style="
background:linear-gradient(to right,#2ecc71,#27ae60);
color:white;
border:none;
padding:8px 16px;
border-radius:4px;
cursor:pointer;
font-weight:600;
">确定</button>
<button id="cancel-password-option" style="
background:linear-gradient(to right,#e74c3c,#c0392b);
color:white;
border:none;
padding:8px 16px;
border-radius:4px;
cursor:pointer;
font-weight:600;
">取消</button>
</div>
</div>
`;
document.body.appendChild(passwordDialog);
}
// 导出全部
document.getElementById('export-all-btn').addEventListener('click', function () {
// 显示导出密码选择对话框
showExportPasswordDialog();
});
// 导入弹窗
document.getElementById('import-all-btn').addEventListener('click', function () {
importDialog.style.display = 'block';
const textarea = document.getElementById('import-textarea');
textarea.value = '';
textarea.focus();
});
// 确认导入
importDialog.addEventListener('click', async function (e) {
if (e.target.id === 'cancel-import') {
importDialog.style.display = 'none';
}
if (e.target.id === 'confirm-import') {
const val = document.getElementById('import-textarea').value;
if (!val) return Utils.showError(CONSTANTS.MESSAGES.errors.importRequired);
try {
const arr = JSON.parse(val);
if (!Array.isArray(arr)) throw new Error(CONSTANTS.MESSAGES.errors.importFormat);
// 处理导入的配置
await processImportConfigs(arr, EventBinder);
importDialog.style.display = 'none';
} catch (e) {
Utils.showError(CONSTANTS.MESSAGES.errors.importFailed + e.message);
}
}
});
// 点击遮罩关闭
importDialog.addEventListener('click', function (e) {
if (e.target === importDialog) importDialog.style.display = 'none';
});
// 密码选项对话框事件处理
passwordDialog.addEventListener('click', function (e) {
// 取消按钮
if (e.target.id === 'cancel-password-option') {
console.log('❌ 用户取消密码选项');
passwordDialog.style.display = 'none';
window.pendingImportConfigs = null;
window.currentPasswordOption = null;
return;
}
// 确认按钮
if (e.target.id === 'confirm-password-option') {
const selectedOption = document.querySelector('input[name="password-option"]:checked');
if (!selectedOption) {
console.log('❌ 未选择密码选项');
return;
}
const manualPassword = document.getElementById('manual-password-field').value;
// 处理密码选项
window.currentPasswordOption = {
option: selectedOption.value,
manualPassword: manualPassword
};
console.log('🔧 密码选项:', window.currentPasswordOption);
passwordDialog.style.display = 'none';
// 继续处理导入
if (window.pendingImportConfigs) {
console.log('🔄 继续处理导入配置...');
processImportConfigs(window.pendingImportConfigs, EventBinder);
window.pendingImportConfigs = null;
} else {
console.log('❌ 没有待处理的导入配置');
}
return;
}
// 点击遮罩关闭
if (e.target === passwordDialog) {
// console.log('❌ 用户点击遮罩关闭');
passwordDialog.style.display = 'none';
window.pendingImportConfigs = null;
window.currentPasswordOption = null;
}
});
// 密码选项单选按钮事件
passwordDialog.addEventListener('change', function (e) {
if (e.target.name === 'password-option') {
console.log('🔄 密码选项变更:', e.target.value);
const manualInput = document.getElementById('manual-password-input');
if (e.target.value === 'manual') {
manualInput.style.display = 'block';
} else {
manualInput.style.display = 'none';
}
}
});
// 导出密码选择对话框事件
exportPasswordDialog.addEventListener('click', function (e) {
// 取消按钮
if (e.target.id === 'cancel-export-option') {
exportPasswordDialog.style.display = 'none';
return;
}
// 确认按钮
if (e.target.id === 'confirm-export-option') {
const selectedOption = document.querySelector('input[name="export-password-option"]:checked');
if (!selectedOption) {
console.log('❌ 未选择导出选项');
return;
}
const newPassword = document.getElementById('new-password-field').value;
// 验证新密码
if (selectedOption.value === 'new' && !newPassword.trim()) {
console.log('❌ 新密码为空');
Utils.showError(CONSTANTS.MESSAGES.errors.newPasswordRequired);
return;
}
// 处理导出选项
const exportOption = {
option: selectedOption.value,
newPassword: newPassword.trim()
};
console.log('🔧 导出选项:', exportOption);
exportPasswordDialog.style.display = 'none';
// 执行导出
processExportConfigs(exportOption);
return;
}
// 点击遮罩关闭
if (e.target === exportPasswordDialog) {
console.log('❌ 用户点击遮罩关闭');
exportPasswordDialog.style.display = 'none';
}
});
// 导出密码选项单选按钮事件
exportPasswordDialog.addEventListener('change', function (e) {
if (e.target.name === 'export-password-option') {
console.log('🔄 导出选项变更:', e.target.value);
const newPasswordInput = document.getElementById('new-password-input');
if (e.target.value === 'new') {
newPasswordInput.style.display = 'block';
} else {
newPasswordInput.style.display = 'none';
}
}
});
}
/**
* 显示导出密码选择对话框
*/
function showExportPasswordDialog() {
// console.log('📤 显示导出密码选择对话框');
const dialog = document.getElementById('export-password-dialog');
if (dialog) {
// 重置选项
document.getElementById('export-option-current').checked = true;
document.getElementById('new-password-field').value = '';
document.getElementById('new-password-input').style.display = 'none';
dialog.style.display = 'block';
}
}
/**
* 处理导出配置
* @param {Object} exportOption 导出选项
*/
async function processExportConfigs(exportOption) {
// console.log('🔄 开始处理导出配置...');
// console.log('🔧 导出选项:', exportOption);
try {
// 获取所有配置
const allConfigs = DataManager.getAllConfigs().map(c => ({ ...c.data }));
console.log('📊 获取到配置数量:', allConfigs.length);
// 根据选项处理密码
for (const cfg of allConfigs) {
if (cfg && cfg.password && Utils.isPasswordEncrypted(cfg.password)) {
console.log('🔐 处理加密密码:', cfg.currentUrl);
switch (exportOption.option) {
case 'current':
console.log('🔒 保持当前加密状态');
// 保持当前加密状态,不做任何处理
break;
case 'new':
console.log('🔄 使用新密码重新加密');
if (exportOption.newPassword) {
try {
// 先用当前主密码解密
const decryptedPassword = await Utils.decryptPassword(cfg.password, CONSTANTS.ENCRYPTION.MASTER_PASSWORD);
// 然后用新密码重新加密
cfg.password = await Utils.encryptPassword(decryptedPassword, exportOption.newPassword);
console.log('✅ 重新加密成功');
} catch (error) {
console.warn('⚠️ 重新加密失败:', error);
// 加密失败时保持原样
}
} else {
console.log('❌ 新密码为空,保持原样');
}
break;
case 'none':
console.log('🔓 解密为明文');
try {
// 解密为明文
cfg.password = await Utils.decryptPassword(cfg.password, CONSTANTS.ENCRYPTION.MASTER_PASSWORD);
console.log('✅ 解密成功');
} catch (error) {
console.warn('⚠️ 解密失败:', error);
// 解密失败时保持原样
}
break;
}
} else {
console.log('📝 密码无需处理(明文或为空):', cfg.currentUrl);
}
}
// 导出为JSON
const json = JSON.stringify(allConfigs, null, 2);
GM_setClipboard(json);
// 显示成功消息
let successMsg = CONSTANTS.MESSAGES.success.exportSuccess;
if (exportOption.option === 'new') {
successMsg += '(使用新密码重新加密)';
} else if (exportOption.option === 'none') {
successMsg += '(明文导出)';
}
Utils.showMsg(successMsg);
console.log('✅ 导出完成');
} catch (error) {
console.error('❌ 导出失败:', error);
Utils.showError(CONSTANTS.MESSAGES.errors.exportFailed, error);
}
}
/**
* 处理导入的配置
* @param {Array} configs 配置数组
* @param {Object} EventBinder 事件绑定器对象
*/
async function processImportConfigs(configs, EventBinder) {
let count = 0;
let hasEncryptedPassword = false;
let hasDecryptFailedPassword = false;
// 检查是否有加密的密码
// console.log('🔍 开始检查导入配置中的密码...');
// 如果已经有密码选项,说明用户已经选择了处理方式,跳过解密检查
if (window.currentPasswordOption) {
// console.log('🔧 检测到已有密码选项,跳过解密检查');
// console.log('📝 密码选项:', window.currentPasswordOption);
hasEncryptedPassword = false;
hasDecryptFailedPassword = false;
} else {
// 首次检查,尝试解密
for (const cfg of configs) {
if (cfg && cfg.password && Utils.isPasswordEncrypted(cfg.password)) {
// console.log('🔐 发现加密密码:', cfg.currentUrl);
hasEncryptedPassword = true;
// 尝试解密
try {
const decryptedResult = await Utils.decryptPassword(cfg.password, CONSTANTS.ENCRYPTION.MASTER_PASSWORD);
console.log('✅ 密码解密成功:', cfg.currentUrl, '解密结果长度:', decryptedResult.length);
// 检查解密结果是否还是加密状态
if (decryptedResult.startsWith('ENCRYPTED:')) {
console.log('⚠️ 解密结果仍然是加密状态,可能是解密失败:', cfg.currentUrl);
hasDecryptFailedPassword = true;
break;
}
} catch (error) {
console.log('❌ 密码解密失败:', cfg.currentUrl, error.message);
hasDecryptFailedPassword = true;
break;
}
}
}
}
// console.log('📊 检查结果:', {
// hasEncryptedPassword,
// hasDecryptFailedPassword,
// totalConfigs: configs.length
// });
// 如果有无法解密的密码,显示密码选项对话框
if (hasEncryptedPassword && hasDecryptFailedPassword) {
// console.log('🚨 检测到无法解密的密码,显示密码选项对话框');
window.pendingImportConfigs = configs;
document.getElementById('password-input-dialog').style.display = 'block';
return;
} else {
// console.log('✅ 所有密码都可以正常解密,直接导入');
}
// 处理配置导入
// console.log('🔄 开始处理配置导入...');
for (const cfg of configs) {
if (cfg && cfg.currentUrl) {
// console.log('📝 处理配置:', cfg.currentUrl);
// 处理密码
if (cfg.password && Utils.isPasswordEncrypted(cfg.password)) {
// console.log('🔐 发现加密密码,应用密码选项');
const passwordOption = window.currentPasswordOption || { option: 'keep' };
// console.log('🔧 密码选项:', passwordOption);
switch (passwordOption.option) {
case 'clear':
// console.log('🗑️ 清空密码');
cfg.password = '';
break;
case 'manual':
// console.log('✏️ 使用手动输入的主密码解密');
const manualMasterPassword = passwordOption.manualPassword || '';
if (manualMasterPassword && Utils.isPasswordEncrypted(cfg.password)) {
try {
// 使用手动输入的主密码解密原始密码
const decryptedPassword = await Utils.decryptPassword(cfg.password, manualMasterPassword);
// 然后用当前主密码重新加密
cfg.password = await Utils.encryptPassword(decryptedPassword, CONSTANTS.ENCRYPTION.MASTER_PASSWORD);
// console.log('🔐 使用手动主密码解密成功,已重新加密');
} catch (error) {
console.warn('⚠️ 手动主密码解密失败:', error);
// 解密失败时保持原样
console.log('🔒 保持原始加密状态');
}
} else {
console.log('❌ 手动主密码为空或原始密码未加密');
cfg.password = '';
}
break;
case 'keep':
default:
// console.log('🔒 保持加密状态');
// 保持加密状态
break;
}
} else {
// console.log('📝 密码无需处理(明文或为空)');
}
// 保存配置
// console.log('💾 保存配置...');
await DataManager.saveConfig(cfg, CONSTANTS.ENCRYPTION.MASTER_PASSWORD);
count++;
console.log('✅ 配置保存成功');
}
}
// 清理临时变量
window.currentPasswordOption = null;
// 更新界面
EventBinder.allConfigs = DataManager.getAllConfigs();
EventBinder.currentKey = null;
UIRenderer.renderConfigList(EventBinder.allConfigs, null, document.getElementById('config-search').value);
UIRenderer.loadConfigToForm({}).then(() => {
// 导入后自动展开选择器区域
selectorsCollapsed = false;
const section = document.getElementById('selectors-section');
const btn = document.getElementById('toggle-selectors-btn');
const listArea = document.querySelector('.config-list-area');
if (section && btn) {
section.style.display = '';
btn.textContent = '▼';
// 同步调整左侧列表高度
if (listArea) {
listArea.style.height = CONSTANTS.PANEL_DIMENSIONS.height.expanded;
}
}
Utils.showMsg(CONSTANTS.MESSAGES.success.importSuccess + count + CONSTANTS.MESSAGES.success.configs);
});
}
// ========== 自动登录/自动填充功能 ==========
/**
* 执行自动登录或自动填充
*/
async function doAutoLoginOrFill() {
// 获取当前URL(不含查询参数和hash)
const currentUrl = window.location.origin + window.location.pathname;
let configKey = DataManager.CONFIG_PREFIX + currentUrl;
let config = DataManager.getConfig(configKey);
// 检查配置是否有效
if (!config) {
console.log('自动登录:未找到精确匹配的配置,尝试域名匹配');
}
// 没有精确匹配,尝试域名匹配
if (!config || (!config.autoLogin && !config.autoFill)) {
const domain = window.location.hostname;
const all = DataManager.getAllConfigs();
for (const c of all) {
const cfg = c.data;
// 检查配置是否有效
if (!cfg || typeof cfg !== 'object') {
console.warn('自动登录:跳过无效配置', c.key);
continue;
}
if ((cfg.autoLogin || cfg.autoFill) && cfg.currentUrl) {
try {
const urlObj = new URL(cfg.currentUrl);
if (urlObj.hostname === domain) {
config = cfg;
break;
}
} catch (e) { }
}
}
}
// 最终检查配置是否有效
if (!config || typeof config !== 'object') {
console.log('自动登录:未找到有效的配置');
return;
}
if (config && (config.autoLogin || config.autoFill)) {
// 解密密码
let decryptedPassword = config.password;
if (Utils.isPasswordEncrypted(config.password)) {
try {
decryptedPassword = await Utils.decryptPassword(config.password, CONSTANTS.ENCRYPTION.MASTER_PASSWORD);
} catch (error) {
console.warn('自动填充时密码解密失败:', error);
return; // 解密失败时不执行自动填充
}
}
// 执行先手点击操作
await executePreClickActions(config);
setTimeout(() => {
// 支持更多类型的用户名/密码输入框
let usernameInput = null, passwordInput = null;
try {
usernameInput = document.querySelector(config.selectors?.username) ||
document.querySelector("input[type='text'], input[name*='user'], input[name*='account'], input[placeholder*='户名'], input[placeholder*='账号']");
passwordInput = document.querySelector(config.selectors?.password) ||
document.querySelector("input[type='password']");
} catch (e) { }
if (usernameInput && passwordInput) {
usernameInput.value = config.username;
passwordInput.value = decryptedPassword;
['input', 'change'].forEach(eventType => {
const event = new Event(eventType, { bubbles: true });
usernameInput.dispatchEvent(event);
passwordInput.dispatchEvent(event);
});
// 输出自动填充完成信息
console.log(`✅ 自动填充完成 - 网站: ${config.currentUrl}, 用户: ${config.username}`);
// 自动聚焦到登录按钮
const loginButton = document.querySelector(config.selectors?.loginButton) || document.querySelector("button[type='submit'], input[type='submit'], button.login, .login-btn");
if (loginButton) loginButton.focus();
if (config.autoLogin) {
if (loginButton) {
setTimeout(() => {
loginButton.click();
console.log(`🚀 自动登录已触发 - 网站: ${config.currentUrl}`);
}, CONSTANTS.ANIMATION.autoLoginDelay);
}
}
} else {
console.warn(`⚠️ 自动填充失败 - 未找到输入框 - 网站: ${config.currentUrl}`);
}
}, CONSTANTS.ANIMATION.pageLoadDelay);
}
}
window.addEventListener('load', doAutoLoginOrFill);
// ========== 跳转页面按钮功能 ==========
/**
* 设置跳转按钮功能
*/
function setupGotoUrlBtn() {
const btn = document.getElementById('goto-url-btn');
const input = document.getElementById('edit-url');
// 防止重复绑定
btn.onclick = null;
btn.addEventListener('click', function () {
let url = input.value.trim();
if (!url) return Utils.showError(CONSTANTS.MESSAGES.errors.urlRequired);
// 补全协议
if (!/^https?:\/\//i.test(url)) {
url = 'https://' + url;
}
try {
// 再次校验
const realUrl = new URL(url).href;
window.open(realUrl, '_blank');
} catch (e) {
Utils.showError(CONSTANTS.MESSAGES.errors.urlInvalid);
}
});
}
// ========== 跳转按钮显示/隐藏逻辑 ==========
/**
* 更新跳转按钮的显示状态
*/
function updateGotoBtnVisibility() {
const btn = document.getElementById('goto-url-btn');
const input = document.getElementById('edit-url');
const currentUrl = window.location.origin + window.location.pathname;
const configUrl = input.value.trim();
if (!configUrl) {
btn.style.display = 'inline-block';
return;
}
// 比较URL(忽略协议、查询参数、哈希等)
let configUrlNormalized = configUrl;
let currentUrlNormalized = currentUrl;
try {
const configUrlObj = new URL(configUrl.startsWith('http') ? configUrl : 'https://' + configUrl);
const currentUrlObj = new URL(currentUrl);
configUrlNormalized = configUrlObj.hostname + configUrlObj.pathname;
currentUrlNormalized = currentUrlObj.hostname + currentUrlObj.pathname;
} catch (e) {
// 如果URL解析失败,使用原始值比较
}
// 如果URL基本相同,隐藏按钮
if (configUrlNormalized === currentUrlNormalized) {
btn.style.display = 'none';
} else {
btn.style.display = 'inline-block';
}
}
// ========== 拖拽功能实现 ==========
/**
* 设置面板拖拽功能
* @param {Element} panel 面板元素
*/
function setupPanelDrag(panel) {
const header = document.getElementById('panel-header');
let isDragging = false, startX = 0, startY = 0, origX = 0, origY = 0;
header.style.cursor = 'move';
header.onmousedown = function (e) {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
origX = panel.offsetLeft;
origY = panel.offsetTop;
document.body.style.userSelect = 'none';
// 拖动时增加透明度
panel.style.opacity = CONSTANTS.OPACITY.dragging;
panel.style.transition = 'opacity ' + CONSTANTS.ANIMATION.duration + ' ease';
};
document.onmousemove = function (e) {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
panel.style.left = (origX + dx) + 'px';
panel.style.top = (origY + dy) + 'px';
panel.style.right = '';
panel.style.bottom = '';
panel.style.transform = '';
};
document.onmouseup = function () {
if (isDragging) {
isDragging = false;
document.body.style.userSelect = '';
// 拖动结束时恢复透明度
panel.style.opacity = CONSTANTS.OPACITY.normal;
panel.style.transition = 'opacity ' + CONSTANTS.ANIMATION.duration + ' ease';
}
};
}
// ========== 关闭按钮和ESC关闭功能 ==========
/**
* 设置关闭按钮和ESC关闭功能
* @param {Element} panel 面板元素
*/
function setupCloseBtnAndEsc(panel) {
const closeBtn = document.getElementById('close-panel');
// 防止重复绑定
closeBtn.onclick = null;
closeBtn.addEventListener('click', function () {
panel.style.display = 'none';
// 重置所有临时状态
resetPanelState();
});
// ESC关闭
document.addEventListener('keydown', function (e) {
if (panel.style.display !== 'none' && (e.key === 'Escape' || e.key === 'Esc')) {
panel.style.display = 'none';
// 重置所有临时状态
resetPanelState();
}
});
}
/**
* 重置面板状态(不删除保存的数据)
*/
function resetPanelState() {
// 重置选择器状态
isPickingElement = false;
currentPickingInput = null;
currentPickingIsMulti = false;
if (originalCursor) {
document.body.style.cursor = originalCursor;
originalCursor = null;
}
// 移除选择器样式
const style = document.getElementById('element-picker-style');
if (style) style.remove();
// 重置UI状态
const template = DataManager.getConfigTemplate('default');
template.currentUrl = window.location.origin + window.location.pathname;
template.nickname = document.title || '新配置';
UIRenderer.loadConfigToForm(template);
// 重置选择器区域状态
selectorsCollapsed = true;
const section = document.getElementById('selectors-section');
const btn = document.getElementById('toggle-selectors-btn');
const listArea = document.querySelector('.config-list-area');
if (section && btn) {
section.style.display = 'none';
btn.textContent = '▲';
if (listArea) {
listArea.style.height = CONSTANTS.PANEL_DIMENSIONS.height.collapsed;
}
}
// 重置当前选中配置
EventBinder.currentKey = null;
// 移除多选提示框
removeMultiSelectHint();
// 清理删除按钮事件监听器
const deleteBtn = document.getElementById('delete-config-btn');
if (deleteBtn) {
const newDeleteBtn = deleteBtn.cloneNode(true);
deleteBtn.parentNode.replaceChild(newDeleteBtn, deleteBtn);
// 重新绑定事件
newDeleteBtn.addEventListener('click', function () {
if (!EventBinder.currentKey) return Utils.showError(CONSTANTS.MESSAGES.errors.configRequired);
// 清理现有的确认框
const existingDialog = document.getElementById('confirm-dialog');
if (existingDialog) existingDialog.remove();
Utils.showConfirm(CONSTANTS.MESSAGES.confirm.deleteConfig, async () => {
DataManager.deleteConfig(EventBinder.currentKey);
EventBinder.currentKey = null;
EventBinder.allConfigs = DataManager.getAllConfigs();
UIRenderer.renderConfigList(EventBinder.allConfigs, null, document.getElementById('config-search').value);
await UIRenderer.loadConfigToForm({});
Utils.showMsg(CONSTANTS.MESSAGES.success.configDeleted);
});
});
}
console.log('✅ 面板状态已重置');
}
// ========== 左侧列表隐藏/显示功能 ==========
let listHidden = true;
/**
* 设置左侧列表隐藏/显示功能
*/
function setupListToggle() {
const toggleBtn = document.getElementById('toggle-list-btn');
const listArea = document.getElementById('config-list-area');
const formArea = document.querySelector('.form-area');
// 初始化状态
if (listHidden) {
listArea.style.display = 'none';
toggleBtn.textContent = '📋';
formArea.style.width = '100%';
} else {
listArea.style.display = 'flex';
toggleBtn.textContent = '📋';
formArea.style.width = '';
}
// 移除所有现有事件监听器
const newToggleBtn = toggleBtn.cloneNode(true);
toggleBtn.parentNode.replaceChild(newToggleBtn, toggleBtn);
// 重新绑定事件
newToggleBtn.addEventListener('click', function () {
listHidden = !listHidden;
if (listHidden) {
listArea.style.display = 'none';
newToggleBtn.textContent = '📋';
formArea.style.width = '100%';
} else {
listArea.style.display = 'flex';
newToggleBtn.textContent = '📋';
formArea.style.width = '';
}
});
}
// ========== 元素选择器功能 ==========
let isPickingElement = false;
let currentPickingInput = null;
let currentPickingIsMulti = false;
let originalCursor = null;
/**
* 设置元素选择器功能
*/
function setupElementPicker() {
const pickButtons = [
{ btn: 'pick-username-selector', input: 'edit-username-selector' },
{ btn: 'pick-password-selector', input: 'edit-password-selector' },
{ btn: 'pick-login-button-selector', input: 'edit-login-button-selector' },
{ btn: 'pick-pre-click-selector', input: 'edit-pre-click-selector', isMulti: true }
];
pickButtons.forEach(({ btn, input, isMulti }) => {
const pickBtn = document.getElementById(btn);
const inputEl = document.getElementById(input);
// 移除所有现有事件监听器
const newPickBtn = pickBtn.cloneNode(true);
pickBtn.parentNode.replaceChild(newPickBtn, pickBtn);
// 重新绑定事件
newPickBtn.addEventListener('click', function () {
// 如果已经在选择模式中,点击按钮停止选择
if (isPickingElement && currentPickingInput === inputEl) {
stopElementPicking();
return;
}
// 否则开始选择模式
startElementPicking(inputEl, newPickBtn, isMulti);
});
});
}
/**
* 开始元素选择模式
* @param {Element} inputEl 输入框元素
* @param {Element} pickBtn 选择按钮元素
* @param {boolean} isMulti 是否支持多选
*/
function startElementPicking(inputEl, pickBtn, isMulti = false) {
if (isPickingElement) return;
isPickingElement = true;
currentPickingInput = inputEl;
currentPickingIsMulti = isMulti;
originalCursor = document.body.style.cursor;
// 改变按钮状态
pickBtn.textContent = isMulti ? '⏹️' : '⏹️';
pickBtn.style.background = '#e74c3c';
pickBtn.style.color = 'white';
pickBtn.title = isMulti ? '点击停止多选模式' : '点击停止选择';
// 为多选模式添加特殊指示
if (isMulti) {
pickBtn.style.border = '2px solid #ff6b6b';
pickBtn.style.borderRadius = '4px';
}
// 改变页面光标
document.body.style.cursor = 'crosshair';
// 添加选择模式样式
const style = document.createElement('style');
style.id = 'element-picker-style';
style.textContent = `
*:hover {
outline: 2px solid #3498db !important;
outline-offset: 1px !important;
}
.element-picker-active {
outline: 2px solid #e74c3c !important;
outline-offset: 1px !important;
}
`;
document.head.appendChild(style);
// 绑定页面点击事件
document.addEventListener('click', handleElementClick, true);
document.addEventListener('keydown', handleEnterKey);
// 显示提示
if (isMulti) {
Utils.showMsg('点击页面元素进行选择(多选模式,按ESC结束选择,或点击停止按钮)', 'info');
// 为多选模式创建固定提示框
createMultiSelectHint();
} else {
Utils.showMsg('点击页面元素进行选择', 'info');
}
}
/**
* 处理元素点击事件
* @param {Event} e 点击事件
* @returns {boolean} 是否阻止默认行为
*/
function handleElementClick(e) {
if (!isPickingElement) return;
e.preventDefault();
e.stopPropagation();
const element = e.target;
// 检查是否为弹窗元素,避免选择到弹窗、模态框等
if (Utils.isPopupElement(element)) {
Utils.showMsg(CONSTANTS.MESSAGES.errors.popupElement, 'error');
return false;
}
const selector = generateSelector(element);
// 填入选择器
if (currentPickingInput) {
if (currentPickingIsMulti) {
// 多选模式:追加到现有选择器
const currentValue = currentPickingInput.value.trim();
const newValue = currentValue ? `${currentValue}, ${selector}` : selector;
currentPickingInput.value = newValue;
// 显示提示信息
Utils.showMsg(`已添加选择器: ${selector}(继续点击添加更多,按ESC或点击停止按钮结束)`, 'info');
// 多选模式下不退出选择模式,允许继续选择
return false;
} else {
// 单选模式:直接替换
currentPickingInput.value = selector;
// 退出选择模式
stopElementPicking();
}
}
return false;
}
/**
* 处理回车键退出选择模式
* @param {Event} e 键盘事件
*/
function handleEnterKey(e) {
if (e.key === 'Enter' && isPickingElement) {
// 阻止事件冒泡,避免触发面板的ESC关闭功能
e.preventDefault();
e.stopPropagation();
stopElementPicking();
}
}
/**
* 创建多选模式提示框
*/
function createMultiSelectHint() {
// 移除已存在的提示框
removeMultiSelectHint();
const hint = document.createElement('div');
hint.id = 'multi-select-hint';
hint.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #ff6b6b;
color: white;
padding: 12px 16px;
border-radius: 6px;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
z-index: 99999999;
font-size: 14px;
font-weight: 600;
border: 2px solid #ff4757;
animation: slideIn 0.3s ease;
`;
hint.innerHTML = `
<div style="display:flex;align-items:center;gap:8px;">
<span>🎯</span>
<span>多选模式 - 点击元素添加,按ESC结束</span>
<button onclick="document.getElementById('multi-select-hint')?.remove()"
style="background:none;border:none;color:white;cursor:pointer;font-size:16px;padding:0;margin-left:8px;">×</button>
</div>
`;
document.body.appendChild(hint);
}
/**
* 移除多选模式提示框
*/
function removeMultiSelectHint() {
const hint = document.getElementById('multi-select-hint');
if (hint) {
hint.remove();
}
}
/**
* 停止元素选择模式
*/
function stopElementPicking() {
if (!isPickingElement) return;
isPickingElement = false;
currentPickingInput = null;
currentPickingIsMulti = false;
// 恢复按钮状态
const pickButtons = ['pick-username-selector', 'pick-password-selector', 'pick-login-button-selector', 'pick-pre-click-selector'];
pickButtons.forEach(btnId => {
const btn = document.getElementById(btnId);
if (btn) {
btn.textContent = '🎯';
btn.style.background = '';
btn.style.color = '';
btn.style.border = '';
btn.style.borderRadius = '';
btn.title = '选择元素';
}
});
// 恢复页面光标
document.body.style.cursor = originalCursor || '';
// 移除选择模式样式
const style = document.getElementById('element-picker-style');
if (style) style.remove();
// 移除事件监听器
document.removeEventListener('click', handleElementClick, true);
document.removeEventListener('keydown', handleEnterKey);
// 移除多选提示框
removeMultiSelectHint();
// 显示停止提示
Utils.showMsg('元素选择模式已停止', 'success');
}
/**
* 根据元素生成CSS选择器
* @param {Element} element DOM元素
* @returns {string} CSS选择器
*/
function generateSelector(element) {
// 辅助函数:检查文本是否包含关键词
const containsKeywords = (text, keywords) => {
return keywords.some(keyword => text.toLowerCase().includes(keyword));
};
// 辅助函数:从class列表中查找特定class
const findClassByKeywords = (classes, keywords) => {
return classes.find(c => containsKeywords(c, keywords));
};
// 优先使用ID
if (element.id) {
return `#${element.id}`;
}
const tagName = element.tagName.toLowerCase();
// 根据元素类型生成特定选择器
if (tagName === 'input') {
// 密码输入框
if (element.type === 'password') {
return "input[type='password']";
}
// 用户名输入框
if (element.type === 'text') {
// 优先使用name属性
if (element.name) {
if (containsKeywords(element.name, CONSTANTS.SELECTOR.userKeywords)) {
return `input[name="${element.name}"]`;
}
}
// 使用placeholder
if (element.placeholder) {
if (containsKeywords(element.placeholder, CONSTANTS.SELECTOR.userKeywords)) {
return `input[placeholder*="${element.placeholder}"]`;
}
}
// 使用class
if (element.className && typeof element.className === 'string') {
const classes = element.className.split(' ').filter(c => c.trim());
const userClass = findClassByKeywords(classes, CONSTANTS.SELECTOR.userKeywords);
if (userClass) {
return `.${userClass}`;
}
}
return "input[type='text']";
}
// 其他输入框类型
if (element.type) {
return `${tagName}[type="${element.type}"]`;
}
}
// 按钮元素
if (tagName === 'button') {
// 提交按钮
if (element.type === 'submit') {
return "button[type='submit']";
}
// 登录按钮
if (element.textContent) {
if (containsKeywords(element.textContent, CONSTANTS.SELECTOR.loginKeywords)) {
return `button:contains("${element.textContent.trim()}")`;
}
}
// 使用class
if (element.className && typeof element.className === 'string') {
const classes = element.className.split(' ').filter(c => c.trim());
const loginClass = findClassByKeywords(classes, ['login', 'submit', 'btn']);
if (loginClass) {
return `.${loginClass}`;
}
}
}
// 输入框元素
if (tagName === 'input') {
// 使用name属性
if (element.name) {
return `${tagName}[name="${element.name}"]`;
}
// 使用placeholder
if (element.placeholder) {
return `${tagName}[placeholder*="${element.placeholder}"]`;
}
}
// 使用class(选择最具体的class)
if (element.className && typeof element.className === 'string') {
const classes = element.className.split(' ').filter(c => c.trim());
if (classes.length > 0) {
// 选择第一个非通用class
const specificClass = classes.find(c =>
!CONSTANTS.SELECTOR.genericClasses.includes(c.toLowerCase())
) || classes[0];
return `.${specificClass}`;
}
}
// 使用标签名
return tagName;
}
// ===================== 菜单命令与快捷键 =====================
/**
* 初始化配置管理面板
* 统一处理面板的显示和所有功能的初始化
*/
async function initializePanel() {
// 显示管理面板
manager.style.display = 'block';
// 初始化事件绑定器,加载配置数据并绑定事件
await EventBinder.init();
// 设置选择器验证功能,包括点击验证和实时预览
setupSelectorValidation();
// 设置导入导出功能,支持批量导入导出配置
setupImportExport(EventBinder);
// 设置选择器区域为折叠状态(默认隐藏)
selectorsCollapsed = true;
// 设置选择器区域折叠/展开功能
setupSelectorCollapse();
// 设置面板拖拽功能,允许用户拖动面板位置
setupPanelDrag(manager);
// 设置跳转按钮功能,点击可跳转到配置的网址
setupGotoUrlBtn();
// 设置关闭按钮和ESC键关闭功能
setupCloseBtnAndEsc(manager);
// 设置密码显示/隐藏切换功能
setupPasswordToggle();
// 设置左侧列表隐藏/显示功能
setupListToggle();
// 更新跳转按钮的显示状态(根据当前页面地址智能显示/隐藏)
updateGotoBtnVisibility();
// 设置元素选择器功能,支持点击页面元素自动生成选择器
setupElementPicker();
}
/**
* 注册Tampermonkey菜单命令
* 在Tampermonkey扩展的菜单中添加"打开登录配置管理工具"选项
* 用户可以通过点击菜单来打开配置管理面板
*/
GM_registerMenuCommand('打开登录配置管理工具', initializePanel);
/**
* 注册键盘快捷键监听器
* 监听Ctrl+Shift+L组合键,快速打开配置管理面板
* 提供比菜单更便捷的打开方式
*/
document.addEventListener('keydown', function (e) {
// 检测是否按下Ctrl+Shift+L组合键
if (e.ctrlKey && e.shiftKey && e.key === 'L') {
initializePanel();
// 阻止默认的浏览器快捷键行为
e.preventDefault();
}
});
// ===================== 清理机制 =====================
/**
* 页面卸载时清理标记
*/
window.addEventListener('beforeunload', function () {
// 清理初始化标记
delete window.loginManagerInitialized;
// 清理可能的事件监听器
if (window.loginManagerEventListeners) {
window.loginManagerEventListeners.forEach(listener => {
try {
listener.element.removeEventListener(listener.type, listener.handler, listener.options);
} catch (e) {
// 忽略清理错误
}
});
delete window.loginManagerEventListeners;
}
console.log('✅ 登录配置管理工具:页面卸载,清理完成');
});
// 初始化事件监听器数组
window.loginManagerEventListeners = [];
console.log('✅ 登录配置管理工具:初始化完成');
})();