纪念币自动预约
// ==UserScript== // @name 纪念币预约助手 // @namespace http://tampermonkey.net/ // @version 1.4 // @description 纪念币自动预约 // @author shenfangda // @match https://jnb.icbc.com.cn/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_notification // @require https://code.jquery.com/jquery-3.6.0.min.js // ==/UserScript== (function() { 'use strict'; // 配置参数 const CONFIG = { VERSION: '1.4', FILL_DELAY: 50, // 填充延迟(毫秒) RETRY_TIMES: 3, // 重试次数 AUTO_AMOUNT: 20 // 默认预约数量 }; // 样式注入 GM_addStyle(` .booking-helper { position: fixed; top: 100px; right: 20px; width: 300px; background: white; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,.1); z-index: 9999; padding: 16px; font-family: -apple-system, system-ui, sans-serif; } .booking-helper .panel-header { border-bottom: 1px solid #eee; padding-bottom: 10px; margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center; } .booking-helper .panel-title { font-size: 16px; font-weight: 600; color: #333; } .booking-helper .form-group { margin-bottom: 12px; } .booking-helper .form-group label { display: block; font-size: 13px; color: #606266; margin-bottom: 4px; } .booking-helper input { width: 100%; padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px; font-size: 14px; box-sizing: border-box; } .booking-helper input:focus { border-color: #409eff; outline: none; } .booking-helper .btn { width: 100%; padding: 8px; margin-bottom: 8px; border: none; border-radius: 4px; font-size: 14px; cursor: pointer; transition: opacity 0.3s; } .booking-helper .btn-primary { background: #409eff; color: white; } .booking-helper .btn-primary:hover { opacity: 0.9; } .booking-helper .btn-primary:active { opacity: 0.8; } .booking-helper .status-bar { margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee; font-size: 12px; color: #666; } .booking-helper .toast { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.7); color: white; padding: 10px 20px; border-radius: 4px; font-size: 14px; z-index: 10000; } `); // 默认配置 const defaultConfig = { profiles: [{ name: '', idCard: '', phone: '', province: '', city: '', district: '', targetBranch: '', // 目标网点 autoSubmit: true, enabled: true, version: '1.2' }], activeProfile: 0, settings: { autoRetry: true, notifyOnSuccess: true, autoMode: false, multipleBooking: false } }; // 验证身份证号 function validateIdCard(idCard) { return /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(idCard); } // 验证手机号 function validatePhone(phone) { return /^1[3456789]\d{9}$/.test(phone); } // 验证姓名 function validateName(name) { return name.length >= 2 && /^[\u4e00-\u9fa5]{2,}$/.test(name); } // 延时函数 function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 页面元素选择器定义 const SELECTORS = { // 个人信息部分 personalInfo: { name: 'input[placeholder="请输入客户姓名"]', idCard: 'input[placeholder="请输入证件号码"]', phone: 'input[placeholder="请输入正确的手机号码"]' }, // 按钮和其他元素 buttons: { agreement: '.el-checkbox__original', submit: '.mybutton' } }; // 地区选择处理 async function handleAreaSelect() { // 选择省份 async function selectProvince() { const provinceSelect = document.querySelector('.el-select[placeholder="请选择省份"] input'); if (!provinceSelect) return false; provinceSelect.click(); await sleep(200); // 找到第一个非空的选项(假设是本省)并选中 const options = Array.from(document.querySelectorAll('.el-select-dropdown__item')); const targetOption = options.find(opt => opt.textContent.trim() !== ''); if (targetOption) { targetOption.click(); await sleep(300); return true; } return false; } // 选择城市 async function selectCity(value) { const citySelect = document.querySelector('.el-select[placeholder="请选择城市"] input'); if (!citySelect) return false; citySelect.click(); await sleep(200); const options = Array.from(document.querySelectorAll('.el-select-dropdown__item')); const targetOption = options.find(opt => opt.textContent.includes(value)); if (targetOption) { targetOption.click(); await sleep(300); return true; } return false; } // 选择区县 async function selectDistrict(value) { const districtSelect = document.querySelector('.el-select[placeholder="请选择区县"] input'); if (!districtSelect) return false; districtSelect.click(); await sleep(200); const options = Array.from(document.querySelectorAll('.el-select-dropdown__item')); const targetOption = options.find(opt => opt.textContent.includes(value)); if (targetOption) { targetOption.click(); await sleep(300); return true; } return false; } // 选择网点 async function selectBranch(value) { const branchSelect = document.querySelector('.el-select[placeholder="请选择网点"] input'); if (!branchSelect) return false; branchSelect.click(); await sleep(200); const options = Array.from(document.querySelectorAll('.el-select-dropdown__item')); const targetOption = options.find(opt => opt.textContent.includes(value)); if (targetOption) { targetOption.click(); await sleep(300); return true; } return false; } // 选择兑换日期 - 自动选第一个 async function selectExchangeDate() { const dateSelect = document.querySelector('.el-select[placeholder="请选择兑换时间"] input'); if (!dateSelect) return false; dateSelect.click(); await sleep(200); const options = Array.from(document.querySelectorAll('.el-select-dropdown__item')); if (options.length > 0) { options[0].click(); await sleep(300); return true; } return false; } return { selectProvince, selectCity, selectDistrict, selectBranch, selectExchangeDate }; } // 创建控制面板 function createPanel() { const panel = document.createElement('div'); panel.className = 'booking-helper'; panel.innerHTML = ` <div class="panel-header"> <span class="panel-title">预约助手</span> <span class="version">v${CONFIG.VERSION}</span> </div> <div class="panel-body"> <!-- 简化控制面板元素 --> <div class="form-group"> <label>姓名</label> <input type="text" class="form-input" id="nameInput" placeholder="请输入姓名"> </div> <div class="form-group"> <label>身份证号</label> <input type="text" class="form-input" id="idCardInput" placeholder="请输入身份证号"> </div> <div class="form-group"> <label>手机号</label> <input type="text" class="form-input" id="phoneInput" placeholder="请输入手机号"> </div> <div class="form-group"> <label>目标时间</label> <input type="text" class="form-input" id="targetTimeInput" placeholder="格式:22:00:00"> </div> <div class="form-group switches"> <label class="switch" style="display: flex;"> <input type="checkbox" id="autoModeSwitch" style="width:30px"> <span class="label">自动模式</span> </label> </div> </div> <div class="buttons"> <button id="saveBtn" class="btn primary">保存配置</button> <button id="fillBtn" class="btn success">开始填充</button> </div> <div class="status-bar"> <div class="status">状态: <span id="statusText">就绪</span></div> <div class="countdown">倒计时: <span id="countdownText">--:--:--</span></div> </div> `; document.body.appendChild(panel); } // 使面板可拖动 function makeDraggable(element) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; element.querySelector('.panel-header').onmousedown = dragMouseDown; function dragMouseDown(e) { e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } function elementDrag(e) { e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; element.style.top = (element.offsetTop - pos2) + "px"; element.style.left = (element.offsetLeft - pos1) + "px"; } function closeDragElement() { document.onmouseup = null; document.onmousemove = null; } } // 显示提示信息 function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = 'booking-toast'; if(type === 'error') { toast.style.backgroundColor = 'rgba(245, 108, 108, 0.9)'; } else if(type === 'success') { toast.style.backgroundColor = 'rgba(103, 194, 58, 0.9)'; } toast.textContent = message; document.body.appendChild(toast); setTimeout(() => { toast.remove(); }, 2000); } // 更新状态显示 function updateStatus(message, type = 'info') { const statusText = document.getElementById('statusText'); if (statusText) { statusText.textContent = message; statusText.style.color = type === 'error' ? '#f56c6c' : type === 'success' ? '#67c23a' : type === 'warning' ? '#e6a23c' : '#666'; } } // 更新倒计时 function updateCountdown(targetTime) { if (!targetTime) return; const countdownText = document.getElementById('countdownText'); if (!countdownText) return; const now = new Date(); const target = new Date(); const [hours, minutes, seconds] = targetTime.split(':').map(Number); target.setHours(hours, minutes, seconds, 0); if (target <= now) { target.setDate(target.getDate() + 1); } const diff = target - now; const h = Math.floor(diff / 3600000); const m = Math.floor((diff % 3600000) / 60000); const s = Math.floor((diff % 60000) / 1000); countdownText.textContent = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; } // 自动填充核心函数 async function fillForm(profile, options = {}) { try { updateStatus('开始填充...'); // 填充个人信息 await fillPersonalInfo(profile); // 选择地区 const areaSelector = await handleAreaSelect(); await areaSelector.selectProvince(); await areaSelector.selectCity(profile.city); await areaSelector.selectDistrict(profile.district); await areaSelector.selectBranch(profile.targetBranch); // 选择兑换日期 - 自动选第一个 await areaSelector.selectExchangeDate(); // 勾选协议 await handleAgreement(); // 自动提交 if (profile.autoSubmit && options.autoSubmit) { await submitForm(); } showToast('填充完成'); return true; } catch (error) { console.error('填充失败:', error); showToast('填充失败: ' + error.message); if (options.autoRetry) { return await retryFill(profile, options); } return false; } } // 填充个人信息 async function fillPersonalInfo(profile) { const fields = { name: { selector: SELECTORS.personalInfo.name, value: profile.name }, idCard: { selector: SELECTORS.personalInfo.idCard, value: profile.idCard }, phone: { selector: SELECTORS.personalInfo.phone, value: profile.phone } }; for (const [fieldName, field] of Object.entries(fields)) { const element = document.querySelector(field.selector); if (!element) { throw new Error(`未找到${fieldName}输入框`); } // 聚焦并清空 element.focus(); element.value = ''; await sleep(50); // 模拟输入 for (const char of field.value) { element.value += char; element.dispatchEvent(new Event('input', { bubbles: true })); await sleep(10); } // 触发事件 ['change', 'blur'].forEach(event => { element.dispatchEvent(new Event(event, { bubbles: true })); }); await sleep(50); } } // 处理协议勾选 async function handleAgreement() { const checkbox = document.querySelector(SELECTORS.buttons.agreement); if (checkbox && !checkbox.checked) { checkbox.click(); await sleep(50); } } // 提交表单 async function submitForm() { const submitBtn = document.querySelector(SELECTORS.buttons.submit); if (!submitBtn) { throw new Error('未找到提交按钮'); } submitBtn.click(); } // 重试填充 async function retryFill(profile, options) { for (let i = 0; i < CONFIG.RETRY_TIMES; i++) { updateStatus(`第${i + 1}次重试...`, 'warning'); await sleep(1000); try { if (await fillForm(profile, { ...options, autoRetry: false })) { return true; } } catch (error) { console.error(`重试${i + 1}失败:`, error); } } updateStatus('重试次数已用完', 'error'); return false; } // 验证配置 function validateProfile(profile) { if (!profile) return false; if (!validateName(profile.name)) { showToast('姓名格式不正确', 'error'); return false; } if (!validateIdCard(profile.idCard)) { showToast('身份证号格式不正确', 'error'); return false; } if (!validatePhone(profile.phone)) { showToast('手机号格式不正确', 'error'); return false; } return true; } // 绑定事件 function bindEvents() { // 填充按钮点击事件 document.getElementById('fillBtn')?.addEventListener('click', async () => { const btn = document.getElementById('fillBtn'); try { btn.disabled = true; btn.textContent = '填充中...'; const config = loadConfig(); const profile = { name: document.getElementById('nameInput').value.trim(), idCard: document.getElementById('idCardInput').value.trim(), phone: document.getElementById('phoneInput').value.trim(), targetTime: document.getElementById('targetTimeInput').value.trim(), ...config.profiles[0] }; if (!validateProfile(profile)) { return; } await fillForm(profile, { autoSubmit: profile.autoSubmit, autoRetry: config.settings.autoRetry }); } catch (error) { console.error('填充出错:', error); showToast(error.message || '填充失败', 'error'); } finally { btn.disabled = false; btn.textContent = '开始填充'; } }); // 保存配置按钮事件 document.getElementById('saveBtn')?.addEventListener('click', () => { try { const profile = { name: document.getElementById('nameInput').value.trim(), idCard: document.getElementById('idCardInput').value.trim(), phone: document.getElementById('phoneInput').value.trim(), targetTime: document.getElementById('targetTimeInput').value.trim() }; if (!validateProfile(profile)) { return; } const config = loadConfig(); config.profiles[0] = { ...config.profiles[0], ...profile }; saveConfig(config); showToast('配置已保存', 'success'); } catch (error) { console.error('保存配置失败:', error); showToast('保存配置失败', 'error'); } }); // 自动模式开关事件 document.getElementById('autoModeSwitch')?.addEventListener('change', (e) => { const config = loadConfig(); config.settings.autoMode = e.target.checked; saveConfig(config); updateStatus(e.target.checked ? '自动模式已开启' : '自动模式已关闭'); }); // 定时检查 setInterval(() => { const config = loadConfig(); if (!config.settings.autoMode) return; const profile = config.profiles[0]; if (!profile || !profile.targetTime) return; // 更新倒计时显示 updateCountdown(profile.targetTime); // 检查是否到达目标时间 const now = new Date(); const [hours, minutes, seconds] = profile.targetTime.split(':').map(Number); const targetTime = new Date(); targetTime.setHours(hours, minutes, seconds, 0); if (Math.abs(now - targetTime) < 1000) { // 1秒误差 fillForm(profile, { autoSubmit: true, autoRetry: config.settings.autoRetry }); } }, 500); } // 加载配置 function loadConfig() { return GM_getValue('BOOKING_CONFIG', defaultConfig); } // 保存配置 function saveConfig(config) { GM_setValue('BOOKING_CONFIG', config); } // 初始化函数 async function init() { try { // 创建控制面板 createPanel(); // 使面板可拖动 makeDraggable(document.querySelector('.booking-helper')); // 绑定事件 bindEvents(); // 加载已保存的配置 const profile = loadConfig().profiles[0]; if (profile) { document.getElementById('nameInput').value = profile.name || ''; document.getElementById('idCardInput').value = profile.idCard || ''; document.getElementById('phoneInput').value = profile.phone || ''; document.getElementById('targetTimeInput').value = profile.targetTime || ''; } // 加载配置 const config = loadConfig(); document.getElementById('autoModeSwitch').checked = config.settings.autoMode; showToast('预约助手已启动', 'success'); updateStatus('就绪'); } catch (error) { console.error('初始化失败:', error); showToast('初始化失败', 'error'); } } // 检查是否在预约页面 function isBookingPage() { return window.location.pathname.includes('/ICBCCOINWEBPC/'); } // 启动脚本 if (isBookingPage()) { // 等待页面加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } } })();