// ==UserScript==
// @name SteamPY 批量上架激活码助手 (正式版)
// @copyright 2025, Yuxiang ZHANG (https://github.com/jamespaulzhang)
// @license MIT
// @namespace http://tampermonkey.net/
// @version 1.1
// @description 自动化批量上架Steam激活码到SteamPY平台,支持精确元素定位和错误恢复
// @author Yuxiang ZHANG
// @icon https://steampy.com/img/logo.63413a4f.png
// @match https://steampy.com/pyUserInfo/sellerCDKey*
// @match https://steampy.com/pyUserInfo/sellerCDKey/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_setClipboard
// @grant GM_notification
// @require https://cdn.jsdelivr.net/npm/sweetalert2@11
// ==/UserScript==
(function() {
'use strict';
// 配置
const config = {
defaultPrice: 10,
defaultRegion: 'china',
delayBetweenItems: 2000,
batchSize: 5,
maxWaitTime: 15000,
retryTimes: 3
};
// 添加自定义样式
function addCustomStyles() {
const style = document.createElement('style');
style.textContent = `
.centered-swal .swal2-popup {
width: 300px !important;
max-height: 220px !important;
padding: 15px !important;
font-size: 13px !important;
}
.centered-swal .swal2-title {
font-size: 16px !important;
margin: 0 0 8px 0 !important;
padding-right: 20px;
}
.centered-swal .swal2-html-container {
margin: 0 0 12px 0 !important;
max-height: 100px !important;
overflow-y: auto !important;
font-size: 13px !important;
}
.centered-swal .swal2-actions {
margin-top: 8px !important;
padding: 0 !important;
}
.centered-swal .swal2-styled {
margin: 4px !important;
padding: 5px 10px !important;
font-size: 13px !important;
}
.centered-swal .swal2-icon {
width: 36px !important;
height: 36px !important;
margin: 5px auto 8px !important;
}
.centered-swal .swal2-icon .swal2-icon-content {
font-size: 24px !important;
line-height: 36px !important;
}
.centered-swal .swal2-success-circular-line-left,
.centered-swal .swal2-success-circular-line-right,
.centered-swal .swal2-success-fix {
display: none !important;
}
.centered-swal .swal2-success .swal2-success-ring {
width: 36px !important;
height: 36px !important;
border-width: 2px !important;
}
.centered-swal .swal2-success [class^="swal2-success-line"] {
height: 3px !important;
background-color: #a5dc86 !important;
}
.centered-swal .swal2-success .swal2-success-line-tip {
width: 12px !important;
left: 6px !important;
top: 21px !important;
}
.centered-swal .swal2-success .swal2-success-line-long {
width: 20px !important;
right: 6px !important;
top: 17px !important;
}
.centered-swal .swal2-error .swal2-x-mark {
position: relative;
width: 36px;
height: 36px;
}
.centered-swal .swal2-error [class^="swal2-x-mark-line"] {
height: 3px !important;
background-color: #f27474 !important;
}
.centered-swal .swal2-error .swal2-x-mark-line-left {
width: 24px !important;
top: 17px !important;
left: 6px !important;
transform: rotate(45deg) !important;
}
.centered-swal .swal2-error .swal2-x-mark-line-right {
width: 24px !important;
top: 17px !important;
right: 6px !important;
transform: rotate(-45deg) !important;
}
.centered-swal .swal2-warning .swal2-icon-content {
font-size: 24px !important;
line-height: 36px !important;
}
.centered-swal .swal2-close {
top: 10px !important;
right: 10px !important;
font-size: 20px !important;
width: 24px !important;
height: 24px !important;
}
.compact-notification .swal2-popup {
max-height: 280px !important;
}
.compact-notification .swal2-html-container {
max-height: 180px !important;
}
`;
document.head.appendChild(style);
}
// 主初始化函数
async function init() {
try {
addCustomStyles(); // 添加自定义样式
await waitForVueApp();
createUI();
} catch (err) {
console.error('初始化失败:', err);
showError('加载失败', `无法检测到SteamPY页面元素: ${err.message}`);
}
}
// 等待Vue应用加载
function waitForVueApp() {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const interval = setInterval(() => {
const addButton = findElementByText('button.wh150-rem.ht50-rem.button-detail-bg', '添加CDKey');
if (addButton) {
clearInterval(interval);
resolve();
} else if (Date.now() - startTime > config.maxWaitTime) {
clearInterval(interval);
reject(new Error('等待Vue应用加载超时'));
}
}, 300);
});
}
// 创建UI界面
function createUI() {
if (document.getElementById('steampyBatchHelperUI')) return;
// 创建主容器
const container = document.createElement('div');
container.id = 'steampyBatchHelperUI';
Object.assign(container.style, {
position: 'fixed',
top: '20px',
right: '20px',
zIndex: '9999',
backgroundColor: '#2a475e',
color: 'white',
fontFamily: 'Arial, sans-serif',
width: '350px',
borderRadius: '5px',
boxShadow: '0 0 10px rgba(0,0,0,0.5)',
overflow: 'hidden'
});
// 创建标题栏
const header = document.createElement('div');
header.id = 'helperHeader';
Object.assign(header.style, {
background: '#1a365d',
padding: '12px 15px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'move',
borderBottom: '1px solid #66c0f4'
});
container.appendChild(header);
// 创建标题
const title = document.createElement('h3');
title.className = 'ui-title';
title.textContent = '批量上架助手';
Object.assign(title.style, {
color: '#66c0f4',
fontWeight: 'bold',
fontSize: '0.3rem',
margin: '0'
});
header.appendChild(title);
// 创建控制按钮容器
const controls = document.createElement('div');
controls.className = 'ui-controls';
Object.assign(controls.style, {
display: 'flex',
gap: '10px'
});
header.appendChild(controls);
// 最小化按钮
const minimizeBtn = document.createElement('div');
minimizeBtn.id = 'helperMinimizeBtn';
minimizeBtn.textContent = '−';
Object.assign(minimizeBtn.style, {
width: '24px',
height: '24px',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
fontWeight: 'bold',
fontSize: '14px',
background: '#5cba47',
transition: 'all 0.2s ease'
});
controls.appendChild(minimizeBtn);
// 关闭按钮
const closeBtn = document.createElement('div');
closeBtn.id = 'helperCloseBtn';
closeBtn.textContent = '×';
Object.assign(closeBtn.style, {
width: '24px',
height: '24px',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
fontWeight: 'bold',
fontSize: '14px',
background: '#ff4d4d',
transition: 'all 0.2s ease'
});
controls.appendChild(closeBtn);
// 创建内容区域
const content = document.createElement('div');
content.id = 'helperContent';
Object.assign(content.style, {
padding: '15px',
color: '#c7d5e0'
});
container.appendChild(content);
// 创建文本区域
const textarea = document.createElement('textarea');
textarea.id = 'batchDataInput';
textarea.placeholder = "每行格式: Steam链接|激活码|价格(可选)\n例如:\nhttps://store.steampowered.com/app/730/CSGO/|ABCDE-FGHIJ-KLMNO|50";
Object.assign(textarea.style, {
width: '100%',
height: '150px',
marginBottom: '10px',
padding: '8px',
background: '#1a2a4c',
border: '1px solid #66c0f4',
borderRadius: '4px',
color: '#e0e0e0',
resize: 'vertical'
});
content.appendChild(textarea);
// 创建按钮组
const btnGroup = document.createElement('div');
Object.assign(btnGroup.style, {
display: 'flex',
gap: '10px',
marginBottom: '10px'
});
content.appendChild(btnGroup);
// 粘贴按钮
const pasteBtn = document.createElement('button');
pasteBtn.id = 'pasteBtn';
pasteBtn.textContent = '从剪贴板粘贴';
Object.assign(pasteBtn.style, {
flex: '1',
padding: '8px',
border: 'none',
borderRadius: '3px',
cursor: 'pointer',
fontWeight: 'bold',
background: '#1a9fff',
color: 'white',
transition: 'all 0.2s ease'
});
btnGroup.appendChild(pasteBtn);
// 清空按钮
const clearBtn = document.createElement('button');
clearBtn.id = 'clearBtn';
clearBtn.textContent = '清空';
Object.assign(clearBtn.style, {
flex: '1',
padding: '8px',
border: 'none',
borderRadius: '3px',
cursor: 'pointer',
fontWeight: 'bold',
background: '#ff4d4d',
color: 'white',
transition: 'all 0.2s ease'
});
btnGroup.appendChild(clearBtn);
// 全球同步复选框
const globalGroup = document.createElement('div');
Object.assign(globalGroup.style, {
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '15px'
});
content.appendChild(globalGroup);
const globalCheckbox = document.createElement('input');
globalCheckbox.type = 'checkbox';
globalCheckbox.id = 'globalRegionCheckbox';
globalGroup.appendChild(globalCheckbox);
const globalLabel = document.createElement('label');
globalLabel.htmlFor = 'globalRegionCheckbox';
globalLabel.textContent = '在全球区同步进行上架';
globalGroup.appendChild(globalLabel);
// 默认价格
const defaultPriceLabel = document.createElement('label');
defaultPriceLabel.textContent = '默认价格(如果数据中未指定):';
Object.assign(defaultPriceLabel.style, {
display: 'block',
marginBottom: '5px'
});
content.appendChild(defaultPriceLabel);
const defaultPriceInput = document.createElement('input');
defaultPriceInput.type = 'number';
defaultPriceInput.id = 'defaultPrice';
defaultPriceInput.value = config.defaultPrice;
Object.assign(defaultPriceInput.style, {
width: '100%',
padding: '8px',
marginBottom: '15px',
background: '#1a2a4c',
border: '1px solid #66c0f4',
borderRadius: '4px',
color: '#e0e0e0'
});
content.appendChild(defaultPriceInput);
// 操作延迟
const delayLabel = document.createElement('label');
delayLabel.textContent = '操作延迟(毫秒):';
Object.assign(delayLabel.style, {
display: 'block',
marginBottom: '5px'
});
content.appendChild(delayLabel);
const delayInput = document.createElement('input');
delayInput.type = 'number';
delayInput.id = 'delayBetweenItems';
delayInput.value = config.delayBetweenItems;
Object.assign(delayInput.style, {
width: '100%',
padding: '8px',
marginBottom: '15px',
background: '#1a2a4c',
border: '1px solid #66c0f4',
borderRadius: '4px',
color: '#e0e0e0'
});
content.appendChild(delayInput);
// 批量大小
const batchLabel = document.createElement('label');
batchLabel.textContent = '每批处理数量:';
Object.assign(batchLabel.style, {
display: 'block',
marginBottom: '5px'
});
content.appendChild(batchLabel);
const batchInput = document.createElement('input');
batchInput.type = 'number';
batchInput.id = 'batchSize';
batchInput.value = config.batchSize;
Object.assign(batchInput.style, {
width: '100%',
padding: '8px',
marginBottom: '20px',
background: '#1a2a4c',
border: '1px solid #66c0f4',
borderRadius: '4px',
color: '#e0e0e0'
});
content.appendChild(batchInput);
// 开始按钮
const startBtn = document.createElement('button');
startBtn.id = 'startBtn';
startBtn.textContent = '开始批量上架';
Object.assign(startBtn.style, {
display: 'block',
width: '100%',
background: '#5cba47',
color: 'white',
border: 'none',
padding: '10px',
borderRadius: '3px',
cursor: 'pointer',
fontWeight: 'bold',
fontSize: '16px',
transition: 'all 0.3s ease',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
});
content.appendChild(startBtn);
// 状态区域
const statusBox = document.createElement('div');
statusBox.id = 'batchStatus';
Object.assign(statusBox.style, {
marginTop: '15px',
fontSize: '14px',
padding: '10px',
background: 'rgba(0, 0, 0, 0.2)',
borderRadius: '3px',
minHeight: '60px'
});
content.appendChild(statusBox);
// 创建最小化UI
const minimizedUI = document.createElement('div');
minimizedUI.id = 'minimizedHelperUI';
Object.assign(minimizedUI.style, {
position: 'fixed',
top: '20px',
right: '20px',
width: '50px',
height: '50px',
background: '#5cba47',
borderRadius: '50%',
display: 'none',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
boxShadow: '0 4px 10px rgba(0, 0, 0, 0.3)',
zIndex: '9999',
transition: 'all 0.3s ease'
});
const minimizedText = document.createElement('span');
minimizedText.textContent = 'S';
Object.assign(minimizedText.style, {
color: 'white',
fontWeight: 'bold',
fontSize: '18px'
});
minimizedUI.appendChild(minimizedText);
document.body.appendChild(container);
document.body.appendChild(minimizedUI);
// 添加拖动功能
addDragFunctionality(container, header);
// 添加最小化/展开功能
setupMinimizeFunctionality(container, minimizedUI);
// 事件绑定
pasteBtn.addEventListener('click', pasteFromClipboard);
clearBtn.addEventListener('click', () => {
textarea.value = '';
});
startBtn.addEventListener('click', startBatchProcess);
closeBtn.addEventListener('click', () => {
container.style.display = 'none';
minimizedUI.style.display = 'none';
});
}
// 添加拖动功能
function addDragFunctionality(element, handle) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
handle.onmousedown = dragMouseDown;
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
// 获取鼠标初始位置
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
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 setupMinimizeFunctionality(mainUI, minimizedUI) {
const minimizeBtn = document.getElementById('helperMinimizeBtn');
minimizeBtn.addEventListener('click', () => {
// 保存位置
const top = mainUI.style.top;
const left = mainUI.style.left;
// 隐藏主UI
mainUI.style.display = 'none';
// 显示最小化UI
minimizedUI.style.display = 'flex';
minimizedUI.style.top = top;
minimizedUI.style.left = left;
});
minimizedUI.addEventListener('click', () => {
// 隐藏最小化UI
minimizedUI.style.display = 'none';
// 显示主UI
mainUI.style.display = 'block';
mainUI.style.top = minimizedUI.style.top;
mainUI.style.left = minimizedUI.style.left;
});
// 添加拖动功能到最小化UI
addDragFunctionality(minimizedUI, minimizedUI);
}
// 核心处理函数
async function processItem(item, isGlobal, attempt = 1) {
try {
console.group(`处理物品 [尝试 ${attempt}]:`, item);
// 1. 点击添加按钮
const addButton = await waitForElement('button.wh150-rem.ht50-rem.button-detail-bg', '添加CDKey');
await safeClick(addButton);
console.log('已点击添加按钮');
// 2. 等待模态窗口完全加载
const visibleModal = await waitForModalReady();
console.log('可见模态窗口已定位');
// 3. 在可见模态窗口中输入链接
const input = await waitForElementWithin(visibleModal, 'input.addCdkIpt');
await safeType(input, item.url);
console.log('已输入游戏链接');
// 4. 在可见模态窗口中点击搜索
const searchBtn = await waitForElementWithin(visibleModal, 'button.addCDKBtn:not([disabled])');
await safeClick(searchBtn);
console.log('已点击搜索按钮');
// 5. 处理搜索结果
await processSearchResults(visibleModal, item);
// 6. 等待表单加载
console.log("等待1秒,确保表单加载...");
await new Promise(resolve => setTimeout(resolve, 1000));
// 7. 填写表单(不再需要区域参数)
await fillForm(visibleModal, item, isGlobal);
// 8. 提交
await submitForm(visibleModal);
console.groupEnd();
return { success: true };
} catch (err) {
console.groupEnd();
console.error(`处理失败: ${err.message}`);
if (attempt >= config.retryTimes) {
throw new Error(`处理失败: ${err.message}`);
}
console.warn(`将在 ${config.delayBetweenItems}ms 后重试...`);
await new Promise(resolve => setTimeout(resolve, config.delayBetweenItems));
return processItem(item, isGlobal, attempt + 1);
}
}
// 处理搜索结果
async function processSearchResults(visibleModal, item) {
const gameCardSelector = 'div.flex-row.mt-5.c-point';
// 1. 在可见模态框中等待游戏卡片加载
const gameCard = await waitForElementWithin(visibleModal, gameCardSelector, null, 20000);
if (!gameCard) {
throw new Error('无法找到游戏卡片');
}
// 2. 添加视觉标记以便调试
gameCard.style.outline = '3px solid blue';
gameCard.style.boxShadow = '0 0 10px rgba(0,0,255,0.5)';
console.log('已标记游戏卡片');
// 3. 点击游戏卡片(确保在可见模态框中)
await simulateClick(gameCard);
console.log('已选择游戏');
// 4. 等待表单加载(检测提交按钮)
await waitForElement('button.ivu-btn-error.ivu-btn-long', null, 15000);
console.log('表单已加载');
}
// 填写表单 - 修复版本
async function fillForm(visibleModal, item, isGlobal) {
try {
console.log("开始填写表单...");
// 1. 输入CDKey
const keyTextarea = await waitForElementWithin(visibleModal, 'textarea.ivu-input');
keyTextarea.value = item.key;
keyTextarea.dispatchEvent(new Event('input', { bubbles: true }));
keyTextarea.dispatchEvent(new Event('change', { bubbles: true }));
await new Promise(resolve => setTimeout(resolve, 100));
console.log('已输入CDKey');
// 2. 输入价格
const priceInput = await waitForElementWithin(visibleModal, 'input.ivu-input[type="number"]');
priceInput.value = item.price.toString();
priceInput.dispatchEvent(new Event('input', { bubbles: true }));
priceInput.dispatchEvent(new Event('change', { bubbles: true }));
await new Promise(resolve => setTimeout(resolve, 100));
console.log('已输入价格');
// 3. 修复全球同步复选框定位 - 新的定位方法
if (isGlobal) {
try {
// 等待全球同步区域加载
await new Promise(resolve => setTimeout(resolve, 800));
// 新的定位策略:直接通过文本定位
const globalTextElement = await waitForElementWithin(
visibleModal,
'span',
'在全球区同步进行上架',
5000
);
if (globalTextElement) {
const globalSection = globalTextElement.closest('div.mt-15.f15');
if (globalSection) {
const checkbox = globalSection.querySelector('input[type="checkbox"]');
if (checkbox) {
// 添加视觉反馈
globalSection.style.outline = '3px solid #00ff00';
globalSection.style.backgroundColor = 'rgba(0,255,0,0.1)';
if (!checkbox.checked) {
// 使用更可靠的点击方法
await safeClick(checkbox);
console.log('已勾选全球同步');
// 等待状态更新
await new Promise(resolve => setTimeout(resolve, 500));
}
} else {
console.warn('在全球同步区域内未找到复选框');
}
} else {
console.warn('找不到全球同步区域');
}
} else {
console.warn('找不到包含"在全球区同步进行上架"文本的元素');
}
} catch (err) {
console.warn('设置全球同步选项失败,尝试备用方法:', err);
// 备用方法:直接通过文本定位
const textElement = findElementByText('span', '在全球区同步进行上架', visibleModal);
if (textElement) {
const parentDiv = textElement.closest('div.mt-15.f15');
if (parentDiv) {
const checkbox = parentDiv.querySelector('input[type="checkbox"]');
if (checkbox && !checkbox.checked) {
await safeClick(checkbox);
console.log('(备用方法) 已勾选全球同步');
}
}
}
}
}
console.log('表单填写完成');
} catch (err) {
console.error('填写表单失败:', err);
throw new Error(`填写表单失败: ${err.message}`);
}
}
// 新增辅助函数:在指定元素内查找包含文本的元素
function findElementByText(selector, text, parent = document) {
const elements = parent.querySelectorAll(selector);
for (const element of elements) {
if (element.textContent.includes(text)) {
return element;
}
}
return null;
}
// 提交表单 - 修复版本:处理多模态窗口问题
async function submitForm(visibleModal) {
try {
// 等待提交按钮可点击
const submitBtn = await waitForElementWithin(visibleModal, 'button.ivu-btn-error.ivu-btn-long');
// 添加视觉标记
submitBtn.style.outline = '3px solid yellow';
submitBtn.style.boxShadow = '0 0 10px rgba(255,255,0,0.5)';
// 记录当前模态窗口数量
const currentModalCount = document.querySelectorAll('div.ivu-modal-wrap:not(.ivu-modal-hidden)').length;
// 点击提交按钮
await safeClick(submitBtn);
console.log('已点击提交按钮');
// 等待新模态窗口出现(确认出售弹窗)
console.log('等待确认出售弹窗...');
let confirmModal;
try {
// 等待新模态窗口出现(数量增加)
confirmModal = await waitForNewModal(currentModalCount, 10000);
console.log('确认出售弹窗已显示');
} catch (err) {
console.warn('等待新模态窗口失败,尝试直接查找确认出售弹窗...', err);
// 备用:直接等待一个包含"注意!!"文本的模态框
confirmModal = await waitForVisibleElementWithText('div.ivu-modal-wrap:not(.ivu-modal-hidden)', '注意!!', 5000);
}
// 添加视觉标记
confirmModal.style.outline = '3px solid orange';
confirmModal.style.boxShadow = '0 0 10px rgba(255,165,0,0.5)';
// 在确认弹窗中找到确认出售按钮 - 使用更精确的选择器
const confirmButton = await waitForElementWithin(
confirmModal,
'button.ivu-btn-info.ivu-btn-long.ivu-btn-large',
'确认出售'
);
if (!confirmButton) {
throw new Error('无法找到确认出售按钮');
}
// 添加视觉标记
confirmButton.style.outline = '3px solid red';
confirmButton.style.boxShadow = '0 0 10px rgba(255,0,0,0.5)';
// 点击确认出售按钮
await safeClick(confirmButton);
console.log('已点击确认出售按钮');
// 等待确认弹窗关闭
await waitForElementDisappear('div.ivu-modal-wrap:not(.ivu-modal-hidden)', 10000);
console.log('确认出售弹窗已关闭');
} catch (err) {
console.error('提交失败:', err);
throw new Error(`提交失败: ${err.message}`);
}
}
// 新增:等待包含特定文本的可见元素
function waitForVisibleElementWithText(selector, text, timeout = 10000) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
try {
const elements = document.querySelectorAll(selector);
for (const el of elements) {
const style = window.getComputedStyle(el);
const isVisible = style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0' &&
el.offsetWidth > 0 &&
el.offsetHeight > 0 &&
!el.classList.contains('ivu-modal-hidden');
// 检查元素是否包含目标文本
if (isVisible && el.textContent.includes(text)) {
return resolve(el);
}
}
if (Date.now() - startTime >= timeout) {
reject(new Error(`等待包含文本的可见元素超时: ${text}`));
} else {
setTimeout(check, 100);
}
} catch (err) {
if (Date.now() - startTime >= timeout) {
reject(err);
} else {
setTimeout(check, 100);
}
}
};
check();
});
}
// 新增:等待新模态窗口出现
function waitForNewModal(baseCount, timeout = 10000) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
try {
const modals = document.querySelectorAll('div.ivu-modal-wrap:not(.ivu-modal-hidden)');
if (modals.length > baseCount) {
// 返回最后一个模态窗口(最上层的)
resolve(modals[modals.length - 1]);
} else if (Date.now() - startTime >= timeout) {
reject(new Error(`等待新模态窗口超时,当前数量: ${modals.length}, 期望大于: ${baseCount}`));
} else {
setTimeout(check, 100);
}
} catch (err) {
if (Date.now() - startTime >= timeout) {
reject(err);
} else {
setTimeout(check, 100);
}
}
};
check();
});
}
// 辅助函数:模拟点击
async function simulateClick(selectorOrElement) {
const element = typeof selectorOrElement === 'string'
? await waitForElement(selectorOrElement)
: selectorOrElement;
element.scrollIntoViewIfNeeded();
await new Promise(resolve => setTimeout(resolve, 300));
const rect = element.getBoundingClientRect();
const mouseEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
clientX: rect.left + rect.width/2,
clientY: rect.top + rect.height/2
});
element.dispatchEvent(mouseEvent);
// 手动触发可能的自定义事件
const customEvent = new CustomEvent('customClick', { bubbles: true });
element.dispatchEvent(customEvent);
}
// 改进的输入函数
async function safeType(inputElement, value) {
// 确保input是有效的输入元素
if (!(inputElement instanceof HTMLInputElement) && !(inputElement instanceof HTMLTextAreaElement)) {
throw new Error('safeType: 无效的输入元素');
}
// 清空并输入值
inputElement.value = "";
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
inputElement.dispatchEvent(new Event('change', { bubbles: true }));
await new Promise(resolve => setTimeout(resolve, 300));
// 模拟真实输入
for (const char of value) {
inputElement.value += char;
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
await new Promise(resolve => setTimeout(resolve, 10));
}
// 触发额外事件确保Vue检测
inputElement.dispatchEvent(new Event('blur', { bubbles: true }));
await new Promise(resolve => setTimeout(resolve, 500));
}
// 在父元素内查找子元素 - 修复版本
function waitForElementWithin(parent, selector, text = null, timeout = 10000) {
return new Promise((resolve, reject) => {
// 确保parent是DOM元素
if (!(parent instanceof Element)) {
return reject(new Error('parent必须是DOM元素'));
}
// 确保selector是字符串
if (typeof selector !== 'string') {
return reject(new Error('selector必须是字符串'));
}
const startTime = Date.now();
const check = () => {
try {
const elements = parent.querySelectorAll(selector);
let found = null;
for (const el of elements) {
const style = window.getComputedStyle(el);
const isVisible = style.display !== 'none' &&
style.visibility !== 'hidden' &&
el.offsetWidth > 0 &&
el.offsetHeight > 0;
if (isVisible && (!text || el.textContent.includes(text))) {
found = el;
break;
}
}
if (found) {
resolve(found);
} else if (Date.now() - startTime >= timeout) {
reject(new Error(`在父元素内查找元素超时: ${selector}`));
} else {
setTimeout(check, 100);
}
} catch (err) {
if (Date.now() - startTime >= timeout) {
reject(err);
} else {
setTimeout(check, 100);
}
}
};
check();
});
}
// 改进的点击函数
async function safeClick(element) {
element.scrollIntoViewIfNeeded();
await new Promise(resolve => setTimeout(resolve, 500));
// 触发完整点击事件
element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
await new Promise(resolve => setTimeout(resolve, 50));
element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
await new Promise(resolve => setTimeout(resolve, 50));
element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
// 等待操作执行
await new Promise(resolve => setTimeout(resolve, 1000));
}
// 等待模态窗口完全就绪
async function waitForModalReady() {
console.log("开始等待可见模态窗口...");
// 使用更可靠的方法检测模态窗口
const modal = await waitForVisibleElement('div.ivu-modal-wrap:not(.ivu-modal-hidden)', 10000);
// 添加视觉标记以便调试
modal.style.outline = '3px solid green';
modal.style.boxShadow = '0 0 10px rgba(0,255,0,0.5)';
return modal;
}
// 等待可见元素(非隐藏状态)
function waitForVisibleElement(selector, timeout = 20000) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
try {
const elements = document.querySelectorAll(selector);
for (const el of elements) {
// 增强可见性检查
const style = window.getComputedStyle(el);
const isVisible =
style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0' &&
el.offsetWidth > 0 &&
el.offsetHeight > 0 &&
!el.classList.contains('ivu-modal-hidden');
if (isVisible) {
console.log("找到可见元素!");
return resolve(el);
}
}
if (Date.now() - startTime >= timeout) {
console.error(`等待可见元素超时: ${selector}`);
reject(new Error(`等待可见元素超时: ${selector}`));
} else {
setTimeout(check, 100);
}
} catch (err) {
console.error('可见性检查错误:', err);
if (Date.now() - startTime >= timeout) {
reject(err);
} else {
setTimeout(check, 100);
}
}
};
check();
});
}
// 等待元素出现
function waitForElement(selector, text = null, timeout = 30000) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
try {
let element;
if (text) {
element = findElementByText(selector, text);
} else {
element = document.querySelector(selector);
}
if (element) {
resolve(element);
} else if (Date.now() - startTime >= timeout) {
reject(new Error(`等待元素超时: ${selector}`));
} else {
setTimeout(check, 100);
}
} catch (err) {
console.error('元素查找错误:', err);
if (Date.now() - startTime >= timeout) {
reject(err);
} else {
setTimeout(check, 100);
}
}
};
check();
});
}
// 辅助函数:等待元素消失
function waitForElementDisappear(selector, timeout = 10000) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
const element = document.querySelector(selector);
if (!element || window.getComputedStyle(element).display === 'none') {
resolve();
} else if (Date.now() - startTime >= timeout) {
reject(new Error(`等待元素消失超时: ${selector}`));
} else {
setTimeout(check, 100);
}
};
check();
});
}
// 从剪贴板粘贴
async function pasteFromClipboard() {
try {
const text = await navigator.clipboard.readText();
document.getElementById('batchDataInput').value = text;
showSuccess('已从剪贴板粘贴数据');
} catch (err) {
console.error('剪贴板错误:', err);
showError('无法访问剪贴板', '请手动粘贴数据');
}
}
// 开始批量处理
async function startBatchProcess() {
const ui = {
input: document.getElementById('batchDataInput'),
defaultPrice: document.getElementById('defaultPrice'),
delay: document.getElementById('delayBetweenItems'),
batchSize: document.getElementById('batchSize'),
globalCheck: document.getElementById('globalRegionCheckbox'),
status: document.getElementById('batchStatus')
};
// 验证输入
const items = parseInputData(ui.input.value.trim(), parseFloat(ui.defaultPrice.value));
if (!items.length) {
return showError('输入错误', '没有有效的上架数据');
}
// 确认对话框
const confirmation = await Swal.fire({
title: '确认批量上架?',
html: `<div style="font-size: 14px;">
将上架 <b>${items.length}</b> 个激活码<br>
默认价格: <b>${ui.defaultPrice.value}</b> 元<br>
每批处理: <b>${ui.batchSize.value}</b> 个
</div>`,
icon: 'question',
showCancelButton: true,
customClass: {
popup: 'centered-swal'
}
});
if (!confirmation.isConfirmed) return;
// 开始处理
const settings = {
isGlobal: ui.globalCheck.checked,
delay: parseInt(ui.delay.value) || config.delayBetweenItems,
batchSize: parseInt(ui.batchSize.value) || config.batchSize
};
await runBatchProcessing(items, settings, ui.status);
}
// 解析输入数据
function parseInputData(data, defaultPrice) {
return data.split('\n')
.map(line => line.trim())
.filter(line => line)
.map(line => {
const [url, key, price, region] = line.split('|').map(part => part.trim());
return {
url: url,
key: key || '',
price: price ? parseFloat(price) : defaultPrice,
region: region || config.defaultRegion
};
})
.filter(item => item.url && item.key);
}
// 执行批量处理
async function runBatchProcessing(items, settings, statusElement) {
let processed = 0, successful = 0, failed = 0;
const failedItems = [];
for (let i = 0; i < items.length; i += settings.batchSize) {
const batch = items.slice(i, i + settings.batchSize);
for (const item of batch) {
statusElement.innerHTML = `处理中: ${++processed}/${items.length}<br>
成功: ${successful}, 失败: ${failed}`;
try {
const result = await processItem(item, settings.isGlobal);
if (result.success) successful++;
} catch (err) {
failed++;
failedItems.push(item);
console.error('处理失败:', err);
statusElement.innerHTML += `<br><span style="color:red">失败: ${item.url}</span>`;
}
if (processed < items.length) {
await new Promise(resolve => setTimeout(resolve, settings.delay));
}
}
// 批次间隔
if (i + settings.batchSize < items.length) {
statusElement.innerHTML += `<br>已完成一批 ${settings.batchSize} 个, 暂停 5 秒...`;
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
// 完成处理
statusElement.innerHTML += `<br><b>处理完成! 成功: ${successful}, 失败: ${failed}</b>`;
showCompletion(successful, failed, failedItems);
}
// 显示完成通知
function showCompletion(success, failed, failedItems) {
let html = `<div style="font-size: 14px;">批量上架完成!<br>成功: <b>${success}</b>, 失败: <b>${failed}</b></div>`;
if (failed > 0) {
html += `<div style="margin-top: 10px; max-height: 120px; overflow-y: auto;">`;
html += `<b>失败项:</b><br>`;
html += failedItems.slice(0, 5).map(item =>
`• ${item.url.substring(0, 40)}...`).join('<br>');
if (failedItems.length > 5) html += `<br>...及其他 ${failedItems.length - 5} 项`;
html += `</div>`;
}
Swal.fire({
title: '完成',
html: html,
icon: failed ? 'warning' : 'success',
customClass: {
popup: 'centered-swal compact-notification'
}
});
}
// 工具函数:显示通知
function showSuccess(text) {
Swal.fire({
text,
icon: 'success',
timer: 1500,
customClass: {
popup: 'centered-swal'
}
});
}
function showError(title, text) {
Swal.fire({
title,
text,
icon: 'error',
customClass: {
popup: 'centered-swal'
}
});
}
// 启动脚本
if (document.readyState === 'complete') {
init();
} else {
window.addEventListener('load', init);
}
})();