// ==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();
}
}
})();