// ==UserScript== // @name 问卷星自动填写|问卷刷题助手|秒刷问卷|智能配比填写|速答精灵 // @namespace https://suda.dev // @version 2.2.0 // @author SparkLab // @description 问卷星自动填写、刷问卷、问卷秒刷、自动答题、选项配比、一键填写、拟人模式、自动循环、问卷助手、刷题工具、批量填问卷。支持单选多选填空量表矩阵排序题,自定义选项比例,自动循环提交,信效度智能模式。 // @match *://www.wjx.cn/* // @match *://v.wjx.cn/* // @match *://ks.wjx.top/* // @match *://*.wjx.cn/* // @match *://*.wjx.top/* // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // @run-at document-end // @charset UTF-8 // ==/UserScript== (function() { 'use strict'; // ========== 核心配置 ========== const SD_CONFIG = { name: '速答精灵', version: '2.2.0', secret: 'xiangsu678', maxFreeLoop: 10, defaultMinTime: 3, defaultMaxTime: 8 }; // ========== 全局状态 ========== let sudaState = { surveyUrl: '', questions: [], ratios: {}, completed: 0, isRunning: false, isUnlocked: false, mode: 'human', minTime: SD_CONFIG.defaultMinTime, maxTime: SD_CONFIG.defaultMaxTime, currentTab: 'fill', validityEnabled: false, validityBias: 'center', validityScores: [], isInIframe: window.self !== window.top, isModalOpen: false }; // ========== 存储封装 ========== const sudaStore = { save: (key, val) => { try { GM_setValue(key, val); } catch(e) {} try { localStorage.setItem('sd_' + key, JSON.stringify(val)); } catch(e) {} }, load: (key, def) => { let val = def; try { val = GM_getValue(key, def); } catch(e) {} if (val === def) { try { const ls = localStorage.getItem('sd_' + key); if (ls) val = JSON.parse(ls); } catch(e) {} } return val; }, clear: (key) => { try { GM_deleteValue(key); } catch(e) {} try { localStorage.removeItem('sd_' + key); } catch(e) {} } }; // ========== 密码验证 ========== function sudaCheckKey(input) { return input.trim().toLowerCase() === SD_CONFIG.secret; } // ========== 工具函数 ========== const sudaUtil = { qs: (sel, ctx) => (ctx || document).querySelector(sel), qsa: (sel, ctx) => Array.from((ctx || document).querySelectorAll(sel)), wait: (ms) => new Promise(r => setTimeout(r, ms)), rand: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min, randFloat: (min, max) => Math.random() * (max - min) + min, log: (...args) => console.log('[速答精灵]', ...args), error: (...args) => console.error('[速答精灵]', ...args), // 正态分布随机数(Box-Muller变换) normalRandom: (mean = 0, std = 1) => { let u = 0, v = 0; while (u === 0) u = Math.random(); while (v === 0) v = Math.random(); const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v); return z * std + mean; }, getIframeDoc: () => { const iframe = sudaUtil.qs('iframe[id^="wjx-"], iframe[name^="wjx"], iframe[id^="join"], iframe[name^="join"]'); if (iframe && iframe.contentDocument) return iframe.contentDocument; return document; }, isSuccessPage: () => { const url = location.href; if (/vm|complete|finish|success|thank/i.test(url)) return true; const txt = document.body ? document.body.innerText : ''; return /感谢|提交成功|答题完成|已收到/i.test(txt); } }; // ========== 配比持久化 ========== const sudaRatioConfig = { save: () => { const config = { ratios: {}, validityEnabled: sudaState.validityEnabled, validityBias: sudaState.validityBias, minTime: sudaState.minTime, maxTime: sudaState.maxTime, mode: sudaState.mode, timestamp: Date.now() }; sudaState.questions.forEach((q, qi) => { config.ratios[qi] = { type: q.type, options: q.options.map((o, oi) => ({ ratio: o.ratio || 0, textAnswers: o.textAnswers || [] })), validityEnabled: q.validityEnabled || false }; }); sudaStore.save('ratioConfig', config); sudaUtil.log('配比配置已保存'); }, load: () => { const config = sudaStore.load('ratioConfig', null); if (!config) return null; sudaState.validityEnabled = config.validityEnabled || false; sudaState.validityBias = config.validityBias || 'center'; sudaState.minTime = config.minTime || SD_CONFIG.defaultMinTime; sudaState.maxTime = config.maxTime || SD_CONFIG.defaultMaxTime; sudaState.mode = config.mode || 'human'; return config; }, apply: (config) => { if (!config || !config.ratios) return; sudaState.questions.forEach((q, qi) => { const saved = config.ratios[qi]; if (saved) { q.options.forEach((o, oi) => { if (saved.options[oi]) { o.ratio = saved.options[oi].ratio || 50; o.textAnswers = saved.options[oi].textAnswers || []; } }); q.validityEnabled = saved.validityEnabled || false; } }); } }; // ========== 信效度模块 ========== const sudaValidity = { // 计算Cronbach's Alpha(简化版) calculateCronbachAlpha: (responses) => { if (responses.length < 2) return 0; const k = responses[0].length; if (k < 2) return 0; let sumVarItem = 0; let sumVarTotal = 0; responses.forEach(response => { const mean = response.reduce((a, b) => a + b, 0) / k; let varItem = 0; response.forEach(v => { varItem += Math.pow(v - mean, 2); }); sumVarItem += varItem / k; sumVarTotal += varItem; }); const varBetween = sumVarTotal / responses.length; if (varBetween === 0) return 0; const alpha = (k / (k - 1)) * (1 - sumVarItem / varBetween); return Math.max(0, Math.min(1, alpha)); }, // 生成符合正态分布的量表答案 generateNormalAnswer: (min, max, bias = 'center') => { const range = max - min; const mean = min + range / 2; const std = range / 6; let value = sudaUtil.normalRandom(mean, std); value = Math.round(value); switch (bias) { case 'left': value = Math.min(max, Math.floor(sudaUtil.normalRandom(min, range / 4))); break; case 'right': value = Math.max(min, Math.ceil(sudaUtil.normalRandom(max, range / 4))); break; case 'center': default: value = Math.max(min, Math.min(max, Math.round(value))); break; } return value; }, // 选择符合分布的选项 selectByDistribution: (options, bias = 'center') => { if (!options || !options.length) return null; if (bias === 'random') { return options[sudaUtil.rand(0, options.length - 1)]; } const n = options.length; let selectedIdx; switch (bias) { case 'left': selectedIdx = sudaUtil.rand(0, Math.floor(n / 2)); break; case 'right': selectedIdx = sudaUtil.rand(Math.ceil(n / 2), n - 1); break; case 'center': default: const centerIdx = Math.floor(n / 2); if (n % 2 === 0) { selectedIdx = Math.random() < 0.5 ? centerIdx - 1 : centerIdx; } else { selectedIdx = centerIdx; } break; } selectedIdx = Math.max(0, Math.min(n - 1, selectedIdx)); return options[selectedIdx]; }, // 应用信效度填写 applyValidityFill: (q) => { if (!sudaState.validityEnabled) return null; switch (q.type) { case 'scale': const scaleMin = 1; const scaleMax = q.options.length; const scaleVal = sudaValidity.generateNormalAnswer(scaleMin, scaleMax, sudaState.validityBias); return q.options[scaleVal - 1] || null; case 'radio': return sudaValidity.selectByDistribution(q.options, sudaState.validityBias); case 'checkbox': const selected = []; const prob = sudaState.validityBias === 'left' ? 0.3 : sudaState.validityBias === 'right' ? 0.7 : 0.5; q.options.forEach((o, i) => { if (Math.random() < prob || i === 0) { selected.push(o); } }); return selected.length ? selected : [q.options[0]]; default: return null; } } }; // ========== 问卷解析 ========== const sudaParser = { detectType: (el) => { const cls = el.className || ''; if (cls.includes('radio')) return 'radio'; if (cls.includes('checkbox')) return 'checkbox'; if (cls.includes('select')) return 'select'; if (cls.includes('scale')) return 'scale'; if (cls.includes('matrix')) return 'matrix'; if (cls.includes('slider')) return 'slider'; if (cls.includes('sort')) return 'sort'; if (cls.includes('textarea') || cls.includes('text')) return 'text'; const inputs = el.querySelectorAll('input[type="radio"]'); if (inputs.length) return 'radio'; const checks = el.querySelectorAll('input[type="checkbox"]'); if (checks.length) return 'checkbox'; const selects = el.querySelectorAll('select'); if (selects.length) return 'select'; const scales = el.querySelectorAll('.scale-cell, .scale-radio'); if (scales.length) return 'scale'; const matrixRows = el.querySelectorAll('.matrix-row, tr'); if (matrixRows.length > 2) return 'matrix'; const sliderEl = el.querySelector('input[type="range"]'); if (sliderEl) return 'slider'; // 排序题检测 const sortHandles = el.querySelectorAll('.drag-handle, .sort-handle, [class*="sort"], [class*="drag"]'); if (sortHandles.length > 1) return 'sort'; const textarea = el.querySelector('textarea, input[type="text"]'); if (textarea) return 'text'; return 'radio'; }, parseQuestion: (el, idx) => { const type = sudaParser.detectType(el); const titleEl = el.querySelector('.question-title, .tm-title, h3, [class*="title"]'); const title = titleEl ? titleEl.innerText.trim() : `题目${idx + 1}`; const required = el.querySelector('.required, [class*="required"], .star') !== null; let options = []; let optionsRaw = []; switch (type) { case 'radio': case 'checkbox': const items = el.querySelectorAll('.option-item, .jq背上, label, .ui-checkbox, .ui-radio'); items.forEach(item => { const txt = item.innerText.trim().replace(/^[A-Z][\.、)]\s*/, ''); const input = item.querySelector('input'); options.push({ text: txt, value: input ? input.value || txt : txt, el: item, ratio: 50 }); optionsRaw.push(txt); }); break; case 'select': const opts = el.querySelectorAll('select option'); opts.forEach(opt => { if (opt.value && opt.value !== '-1') { options.push({ text: opt.innerText.trim(), value: opt.value, el: opt, ratio: 50 }); optionsRaw.push(opt.innerText.trim()); } }); break; case 'scale': const cells = el.querySelectorAll('.scale-cell, .scale-radio, input[type="radio"]'); cells.forEach((cell, i) => { options.push({ text: String(i + 1), value: String(i + 1), el: cell, ratio: 50 }); optionsRaw.push(String(i + 1)); }); break; case 'matrix': const rows = el.querySelectorAll('.matrix-row, tr[class*="row"]'); rows.forEach((row, ri) => { const rowTitle = row.querySelector('.row-title, [class*="row-title"]'); const rowLabel = rowTitle ? rowTitle.innerText.trim() : `行${ri + 1}`; const cells2 = row.querySelectorAll('input[type="radio"], .matrix-cell'); const cellOptions = []; cells2.forEach((cell2, ci) => { cellOptions.push({ text: String(ci + 1), value: String(ci + 1), el: cell2, ratio: 50 }); }); if (cellOptions.length) { options.push({ text: rowLabel, value: rowLabel, children: cellOptions, ratio: 50 }); optionsRaw.push(rowLabel); } }); break; case 'slider': options.push({ text: '0-100滑块', value: 'slider', el: el, ratio: 50 }); optionsRaw.push('滑块'); break; case 'sort': const sortItems = el.querySelectorAll('.option-item, .sort-item, li, [class*="option"]'); sortItems.forEach((item, i) => { const txt = item.innerText.trim().replace(/^[A-Z][\.、)]\s*/, '') || `选项${i + 1}`; options.push({ text: txt, value: txt, el: item, ratio: 100 - i * 10, sortOrder: i }); optionsRaw.push(txt); }); break; case 'text': options.push({ text: '文本输入', value: 'text', el: el, ratio: 50, textAnswers: [] }); optionsRaw.push('文本'); break; } return { idx, type, title, required, options, optionsRaw, el, validityEnabled: false }; }, parseAll: () => { const doc = sudaUtil.getIframeDoc(); const wrappers = doc.querySelectorAll('.question, .question-item, [class*="question"], div[id*="question"], div[id*="div"]'); sudaState.questions = []; wrappers.forEach((w, i) => { if (w.querySelector('input, select, textarea, .option-item, .scale-cell')) { const q = sudaParser.parseQuestion(w, sudaState.questions.length); if (q.options.length) { sudaState.questions.push(q); } } }); // 应用保存的配比 const savedConfig = sudaRatioConfig.load(); if (savedConfig) { sudaRatioConfig.apply(savedConfig); } sudaUtil.log('解析完成,共', sudaState.questions.length, '题'); return sudaState.questions; } }; // ========== 填写引擎 ========== const sudaFiller = { // ==================== 信效度算法 ==================== // 生成正态分布随机数(Box-Muller变换) normalRandom: () => { let u = 0, v = 0; while (u === 0) u = Math.random(); while (v === 0) v = Math.random(); return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v); }, // 将z值映射到类别索引 zToCategoryIndex: (z, m) => { // z: 正态分布值, m: 选项数量 // 使用累积分布函数将z值转换为概率 const cdf = 0.5 * (1 + Math.erf(z / Math.SQRT2)); // 映射到选项索引 [0, m-1] return Math.min(Math.max(Math.floor(cdf * m), 0), m - 1); }, // 根据信效度偏向获取选项索引 psychoSelect: (q, bias) => { const m = q.options.length; let z; switch (bias) { case SUDA_PSYCHO.LEFT: // 左偏:倾向于低分(选项1-3),z值偏负 z = sudaFiller.normalRandom() - 1.0; break; case SUDA_PSYCHO.RIGHT: // 右偏:倾向于高分(选项3-5),z值偏正 z = sudaFiller.normalRandom() + 1.0; break; case SUDA_PSYCHO.CENTER: default: // 中立:标准正态分布 z = sudaFiller.normalRandom(); break; } return sudaFiller.zToCategoryIndex(z, m); }, // 计算克朗巴赫α系数 computeCronbachAlpha: (matrix) => { const n = matrix.length; // 题目数 if (n < 2) return 0; const m = matrix[0] ? matrix[0].length : 0; // 样本数 if (m < 2) return 0; // 计算每个题目的方差 let sumItemVariances = 0; for (let i = 0; i < n; i++) { let mean = 0; for (let j = 0; j < m; j++) mean += matrix[i][j]; mean /= m; let variance = 0; for (let j = 0; j < m; j++) variance += Math.pow(matrix[i][j] - mean, 2); variance /= m; sumItemVariances += variance; } // 计算总分方差 let totalScores = new Array(m).fill(0); for (let i = 0; i < n; i++) { for (let j = 0; j < m; j++) { totalScores[j] += matrix[i][j]; } } let totalMean = 0; for (let j = 0; j < m; j++) totalMean += totalScores[j]; totalMean /= m; let totalVariance = 0; for (let j = 0; j < m; j++) totalVariance += Math.pow(totalScores[j] - totalMean, 2); totalVariance /= m; if (totalVariance === 0) return 0; const alpha = (n / (n - 1)) * (1 - sumItemVariances / totalVariance); return Math.max(0, Math.min(1, alpha)); }, // ==================== 排序题算法 ==================== // 加权随机排列索引 weightedPermutationIndices: (weights) => { const n = weights.length; const indices = Array.from({ length: n }, (_, i) => i); const result = []; // 归一化权重 const totalWeight = weights.reduce((s, w) => s + Math.max(0, w), 0) || n; for (let i = n - 1; i > 0; i--) { const normalizedWeights = weights.map(w => Math.max(0.1, w)); const weightSum = normalizedWeights.reduce((s, w) => s + w, 0); let rand = Math.random() * weightSum; let selectedIdx = 0; for (let j = 0; j <= i; j++) { rand -= normalizedWeights[indices[j]]; if (rand <= 0) { selectedIdx = j; break; } } result.push(indices[selectedIdx]); indices.splice(selectedIdx, 1); weights.splice(selectedIdx, 1); } if (indices.length > 0) result.push(indices[0]); return result; }, selectByWeight: (options) => { const ratioSum = options.reduce((s, o) => s + (parseFloat(o.ratio) || 0), 0); if (ratioSum <= 0) { return options[sudaUtil.rand(0, options.length - 1)]; } let rand = Math.random() * ratioSum; for (const opt of options) { rand -= (parseFloat(opt.ratio) || 0); if (rand <= 0) return opt; } return options[0]; }, clickElement: async (el) => { if (!el) return false; try { const rect = el.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) return false; const events = ['mouseenter', 'mouseover', 'mousemove', 'mousedown', 'mouseup', 'click']; for (const evt of events) { const me = new MouseEvent(evt, { bubbles: true, cancelable: true, view: window, clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2 }); el.dispatchEvent(me); if (evt === 'click') break; await sudaUtil.wait(sudaUtil.rand(30, 80)); } return true; } catch(e) { sudaUtil.error('点击失败', e); try { el.click(); return true; } catch(e2) { return false; } } }, fillRadio: async (q) => { // 信效度模式:使用正态分布算法 const psycho = sudaState.psychoMap[q.idx]; if (psycho && psycho.enabled) { const idx = sudaFiller.psychoSelect(q, psycho.bias); await sudaFiller.clickElement(q.options[idx].el); return; } // 原有信效度逻辑(兼容) if (sudaState.validityEnabled && q.validityEnabled) { const selected = sudaValidity.applyValidityFill(q); if (selected) { await sudaFiller.clickElement(selected.el); return; } } const opts = q.options.filter(o => (parseFloat(o.ratio) || 0) > 0); if (!opts.length) { const opt = q.options[sudaUtil.rand(0, q.options.length - 1)]; await sudaFiller.clickElement(opt.el); } else { const selected = sudaFiller.selectByWeight(opts); await sudaFiller.clickElement(selected.el); } }, fillCheckbox: async (q) => { if (sudaState.validityEnabled && q.validityEnabled) { const selected = sudaValidity.applyValidityFill(q); if (Array.isArray(selected)) { for (const opt of selected) { await sudaFiller.clickElement(opt.el); await sudaUtil.wait(sudaUtil.rand(50, 150)); } return; } } let selected = []; let hasWeight = false; q.options.forEach(o => { if ((parseFloat(o.ratio) || 0) > 0) hasWeight = true; }); if (hasWeight) { q.options.forEach(o => { if (Math.random() * 100 < (parseFloat(o.ratio) || 0)) { selected.push(o); } }); } else { const cnt = sudaUtil.rand(1, Math.min(q.options.length, 3)); const idxs = []; while (idxs.length < cnt) { const ri = sudaUtil.rand(0, q.options.length - 1); if (!idxs.includes(ri)) idxs.push(ri); } selected = idxs.map(i => q.options[i]); } if (!selected.length && q.required) { selected = [q.options[sudaUtil.rand(0, q.options.length - 1)]]; } for (const opt of selected) { await sudaFiller.clickElement(opt.el); await sudaUtil.wait(sudaUtil.rand(50, 150)); } }, fillSelect: async (q) => { const selectEl = q.el.querySelector('select'); if (!selectEl) return; const opts = q.options.filter(o => (parseFloat(o.ratio) || 0) > 0); let selected; if (opts.length) { selected = sudaFiller.selectByWeight(opts); } else { selected = q.options[sudaUtil.rand(0, q.options.length - 1)]; } if (selected && selected.value) { selectEl.value = selected.value; selectEl.dispatchEvent(new Event('change', { bubbles: true })); } }, fillText: async (q) => { let textarea = q.el.querySelector('textarea, input[type="text"], input:not([type])'); if (!textarea) return; let val = ''; const textAnswers = q.options[0]?.textAnswers || []; if (textAnswers.length > 1) { val = textAnswers[sudaUtil.rand(0, textAnswers.length - 1)]; } else if (textAnswers.length === 1) { val = textAnswers[0]; } else { val = sudaUtil.rand(10, 999).toString(); } textarea.value = val; textarea.dispatchEvent(new Event('input', { bubbles: true })); textarea.dispatchEvent(new Event('change', { bubbles: true })); }, fillScale: async (q) => { // 信效度模式:使用正态分布算法 const psycho = sudaState.psychoMap[q.idx]; if (psycho && psycho.enabled) { const idx = sudaFiller.psychoSelect(q, psycho.bias); await sudaFiller.clickElement(q.options[idx].el); return; } // 原有信效度逻辑(兼容) if (sudaState.validityEnabled && q.validityEnabled) { const selected = sudaValidity.applyValidityFill(q); if (selected) { await sudaFiller.clickElement(selected.el); return; } } const opts = q.options.filter(o => (parseFloat(o.ratio) || 0) > 0); let selected; if (opts.length) { selected = sudaFiller.selectByWeight(opts); } else { selected = q.options[sudaUtil.rand(0, q.options.length - 1)]; } await sudaFiller.clickElement(selected.el); }, fillMatrix: async (q) => { for (const row of q.options) { if (!row.children || !row.children.length) continue; const opts = row.children.filter(o => (parseFloat(o.ratio) || 0) > 0); let selected; if (opts.length) { selected = sudaFiller.selectByWeight(opts); } else { selected = row.children[sudaUtil.rand(0, row.children.length - 1)]; } await sudaFiller.clickElement(selected.el); await sudaUtil.wait(sudaUtil.rand(30, 80)); } }, fillSlider: async (q) => { const slider = q.el.querySelector('input[type="range"]'); if (!slider) return; const val = sudaUtil.rand(0, 100); slider.value = val; slider.dispatchEvent(new Event('input', { bubbles: true })); slider.dispatchEvent(new Event('change', { bubbles: true })); }, fillSort: async (q) => { // 排序题:使用加权随机排列 const weights = q.options.map(o => parseFloat(o.ratio) || 50); const sortedIndices = sudaFiller.weightedPermutationIndices(weights); const sortedOptions = sortedIndices.map(i => q.options[i]); // 获取所有可拖拽元素 const sortContainer = q.el.querySelector('.sort-container, .drag-container, [class*="sortable"], ul'); const dragHandles = q.el.querySelectorAll('.drag-handle, .sort-handle, [class*="drag"], [class*="sort"], li'); if (dragHandles.length > 1) { // 方法1:逐个点击拖拽手柄 for (const opt of sortedOptions) { const handle = opt.el.querySelector('.drag-handle, .sort-handle, [class*="drag"], [class*="sort"]') || opt.el; await sudaFiller.clickElement(handle); await sudaUtil.wait(sudaUtil.rand(100, 200)); } // 方法2:模拟拖拽操作 if (sortContainer && sortedOptions.length > 1) { for (let i = 0; i < sortedOptions.length - 1; i++) { const current = sortedOptions[i].el; const nextTarget = sortedOptions[i + 1].el; if (current && nextTarget) { const currRect = current.getBoundingClientRect(); const nextRect = nextTarget.getBoundingClientRect(); // 模拟拖拽到正确位置 sudaFiller.simulateDrag(current, currRect.left + 10, currRect.top + currRect.height / 2, currRect.left + 10, nextRect.top + nextRect.height / 2); await sudaUtil.wait(sudaUtil.rand(150, 300)); } } } } else { // 如果没有拖拽元素,尝试点击选项(某些问卷是点击选择顺序) for (let i = 0; i < sortedOptions.length; i++) { await sudaFiller.clickElement(sortedOptions[i].el); await sudaUtil.wait(sudaUtil.rand(100, 200)); } } }, simulateDrag: async (el, x1, y1, x2, y2) => { const events = [ new MouseEvent('mousedown', { bubbles: true, cancelable: true, clientX: x1, clientY: y1, view: window }), new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: (x1 + x2) / 2, clientY: (y1 + y2) / 2, view: window }), new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: x2, clientY: y2, view: window }), new MouseEvent('mouseup', { bubbles: true, cancelable: true, clientX: x2, clientY: y2, view: window }) ]; for (const evt of events) { el.dispatchEvent(evt); await sudaUtil.wait(50); } }, fillOne: async (q) => { switch (q.type) { case 'radio': await sudaFiller.fillRadio(q); break; case 'checkbox': await sudaFiller.fillCheckbox(q); break; case 'select': await sudaFiller.fillSelect(q); break; case 'text': await sudaFiller.fillText(q); break; case 'scale': await sudaFiller.fillScale(q); break; case 'matrix': await sudaFiller.fillMatrix(q); break; case 'slider': await sudaFiller.fillSlider(q); break; case 'sort': await sudaFiller.fillSort(q); break; } }, fillAll: async () => { const questions = sudaState.questions; if (!questions.length) { sudaUtil.log('无题目,请先解析问卷'); return false; } for (const q of questions) { if (sudaState.mode === 'human') { await sudaFiller.fillOne(q); await sudaUtil.wait(sudaUtil.rand(sudaState.minTime * 1000, sudaState.maxTime * 1000)); } else { await sudaFiller.fillOne(q); } } return true; }, clickCaptcha: () => { const doc = sudaUtil.getIframeDoc(); const btns = doc.querySelectorAll('.nc_iconfont, .btn_slide, [class*="nc-close"], .yidun_icon'); for (const btn of btns) { const rect = btn.getBoundingClientRect(); if (rect.width > 0) { btn.click(); sudaUtil.log('点击验证码按钮'); return true; } } const slider = doc.querySelector('.yidun_slider, .nc_slider, [class*="slider"]'); if (slider) { const rect = slider.getBoundingClientRect(); if (rect.width > 0) { slider.click(); return true; } } return false; }, // 处理安全校验弹窗 handleSecurityDialog: () => { const doc = sudaUtil.getIframeDoc(); const layerDialog = doc.querySelector('.layui-layer, [class*="layer"], [class*="dialog"]'); if (layerDialog) { const confirmBtn = layerDialog.querySelector('.layui-layer-btn0, .btn-confirm, button[class*="confirm"], a[class*="confirm"]'); if (confirmBtn) { confirmBtn.click(); sudaUtil.log('已确认安全校验弹窗'); return true; } } return false; }, // 处理答题恢复弹窗 handleResumeDialog: () => { const doc = sudaUtil.getIframeDoc(); const dialogs = doc.querySelectorAll('[class*="resume"], [class*="continue"], [class*="recover"]'); for (const dialog of dialogs) { const text = dialog.innerText || ''; if (/继续|resume|continue|恢复/i.test(text)) { const btn = dialog.querySelector('button, a, [class*="btn"]'); if (btn) { btn.click(); sudaUtil.log('已点击继续答题'); return true; } } } // 查找确定/继续按钮 const confirmBtns = doc.querySelectorAll('button:has-text("继续"), a:has-text("继续"), button:has-text("确定")'); for (const btn of confirmBtns) { if (btn.offsetParent !== null) { btn.click(); sudaUtil.log('已点击继续/确定按钮'); return true; } } return false; } }; // ========== iFrame弹窗模式 ========== const IFRAME_SUCCESS_TYPE = 'survey_success'; const sudaModal = { isInIframe: () => { try { return window.self !== window.top; } catch(e) { return true; } }, setupIframeMsgListener: () => { window.addEventListener('message', (event) => { if (event.data && event.data.type) { const { type, data } = event.data; sudaUtil.log('收到iframe消息:', type, data); switch (type) { case 'survey_loaded': sudaUI.showMsg('问卷已加载'); break; case IFRAME_SUCCESS_TYPE: sudaState.completed++; sudaStore.save('completed', sudaState.completed); sudaUI.updateStatus(); if (sudaState.isRunning) { sudaLoop.next(); } break; } } }); }, openSurveyModal: (url, options = {}) => { const { width = 1200, height = 800, onSuccess, onClose } = options; // 创建遮罩层 const overlay = document.createElement('div'); overlay.className = 'sd-modal-overlay'; overlay.id = 'sd-modal-overlay'; // 创建关闭按钮 const closeBtn = document.createElement('button'); closeBtn.className = 'sd-modal-close'; closeBtn.innerHTML = '×'; closeBtn.title = '关闭'; // 创建iframe const iframe = document.createElement('iframe'); iframe.className = 'sd-modal-iframe'; iframe.src = url; iframe.id = 'sd-survey-iframe'; overlay.appendChild(iframe); overlay.appendChild(closeBtn); document.body.appendChild(overlay); sudaState.modalOpen = true; sudaUtil.log('已打开问卷弹窗:', url); // 关闭按钮事件 closeBtn.addEventListener('click', () => { sudaModal.closeModal(); if (onClose) onClose(); }); // ESC键关闭 const escHandler = (e) => { if (e.key === 'Escape') { sudaModal.closeModal(); if (onClose) onClose(); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); // 监听iframe成功页 iframe.onload = () => { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; const checkSuccess = setInterval(() => { if (sudaUtil.isSuccessPage() || /vm|complete|finish|success|thank/i.test(iframe.contentWindow.location.href)) { clearInterval(checkSuccess); if (onSuccess) onSuccess(); sudaModal.closeModal(); } }, 500); } catch(e) { // 跨域限制,无法直接访问iframe内容 } }; return { overlay, iframe, closeBtn }; }, closeModal: () => { const overlay = document.getElementById('sd-modal-overlay'); if (overlay) { overlay.remove(); } sudaState.modalOpen = false; sudaUtil.log('弹窗已关闭'); }, open: (url) => { const width = 900; const height = 700; const left = (screen.width - width) / 2; const top = (screen.height - height) / 2; const win = window.open(url, '_blank', `width=${width},height=${height},left=${left},top=${top}`); if (win) { sudaState.isModalOpen = true; sudaUtil.log('已打开弹窗问卷'); // 监听弹窗关闭 const checkClosed = setInterval(() => { if (win.closed) { clearInterval(checkClosed); sudaState.isModalOpen = false; sudaUtil.log('弹窗问卷已关闭'); } }, 1000); return win; } return null; }, // 向父页面发送消息 sendToParent: (type, data) => { if (window.self !== window.top) { window.parent.postMessage({ source: 'suda-sparkle', type, data }, '*'); sudaUtil.log('已发送消息到父页面:', type); } }, // 监听来自父页面的消息 listenFromParent: () => { window.addEventListener('message', (event) => { if (event.data && event.data.source === 'suda-sparkle') { const { type, data } = event.data; sudaUtil.log('收到父页面消息:', type, data); switch (type) { case 'parse': sudaParser.parseAll(); sudaModal.sendToParent('parsed', { count: sudaState.questions.length }); break; case 'fill': sudaFiller.fillAll().then(ok => { sudaModal.sendToParent('filled', { success: ok }); }); break; case 'submit': const doc = document; const subBtn = doc.querySelector('.submit-btn, input[type="submit"]'); if (subBtn) subBtn.click(); break; case 'success': sudaState.completed++; sudaStore.save('completed', sudaState.completed); break; } } }); }, // 检测成功页并通知父页面 notifyParentSuccess: () => { if (sudaModal.isInIframe() && sudaUtil.isSuccessPage()) { sudaModal.sendToParent(IFRAME_SUCCESS_TYPE, { timestamp: Date.now() }); } } }; // ========== 安全校验弹窗自动处理 ========== const sudaSecurity = { watcher: null, MAX_ATTEMPTS: 3, startSecurityDialogWatcher: () => { if (sudaSecurity.watcher) return; sudaSecurity.watcher = setInterval(() => { const doc = sudaUtil.getIframeDoc(); const dialogs = doc.querySelectorAll('.layui-layer-dialog, [class*="security"], [class*="verify"]'); dialogs.forEach(dialog => { const text = dialog.innerText || ''; // 检测"需要安全校验"、"重新提交"等关键词 if (/安全校验|安全验证|重新提交|verify|security/i.test(text)) { if (sudaState.securityResubmitAttempts >= sudaSecurity.MAX_ATTEMPTS) { sudaUtil.log('安全校验重试次数已达上限'); sudaSecurity.stopSecurityDialogWatcher(); return; } const confirmBtn = dialog.querySelector('.layui-layer-btn0, .btn-confirm, button[class*="confirm"], a[class*="btn"]'); if (confirmBtn) { confirmBtn.click(); sudaState.securityResubmitAttempts++; sudaUtil.log(`安全校验弹窗已确认,重试次数: ${sudaState.securityResubmitAttempts}`); // 延迟后重新提交 setTimeout(() => { const subBtn = doc.querySelector('.submit-btn, input[type="submit"]'); if (subBtn) subBtn.click(); }, 1000); } } }); // 检测layui-layer-dialog const layuiDialog = doc.querySelector('.layui-layer, [class*="layui-layer-dialog"]'); if (layuiDialog) { const dialogText = layuiDialog.innerText || ''; if (/安全|校验|验证|重新/i.test(dialogText)) { const btn0 = layuiDialog.querySelector('.layui-layer-btn0'); if (btn0 && sudaState.securityResubmitAttempts < sudaSecurity.MAX_ATTEMPTS) { btn0.click(); sudaState.securityResubmitAttempts++; sudaUtil.log('Layui安全校验弹窗已处理'); } } } }, 600); }, stopSecurityDialogWatcher: () => { if (sudaSecurity.watcher) { clearInterval(sudaSecurity.watcher); sudaSecurity.watcher = null; } } }; // ========== 答题恢复弹窗自动处理 ========== const sudaResume = { watcher: null, startResumeDialogWatcher: () => { if (sudaResume.watcher) return; sudaResume.watcher = setInterval(() => { const doc = sudaUtil.getIframeDoc(); const dialogs = doc.querySelectorAll('.layui-layer-dialog, [class*="resume"], [class*="continue"], [class*="recover"], [class*="dialog"]'); dialogs.forEach(dialog => { const text = dialog.innerText || ''; // 检测"继续答题"、"恢复答题"等关键词 if (/继续答题|恢复答题|上次答题|resume|continue.*answer/i.test(text)) { const continueBtn = dialog.querySelector('.layui-layer-btn0, .btn-primary, button[class*="continue"], a[class*="btn"]:first-child'); if (continueBtn) { continueBtn.click(); sudaUtil.log('答题恢复弹窗已确认'); } } }); // 通用"继续"按钮 const btns = doc.querySelectorAll('button, a'); btns.forEach(btn => { const btnText = btn.innerText.trim(); if (/^(继续|继续答题|确定|好的|我知道了)$/.test(btnText)) { if (btn.offsetParent !== null && !btn.disabled) { btn.click(); sudaUtil.log('点击了继续按钮'); } } }); }, 500); }, stopResumeDialogWatcher: () => { if (sudaResume.watcher) { clearInterval(sudaResume.watcher); sudaResume.watcher = null; } } }; // ========== 自动循环 ========== let sudaLoopTimer = null; const sudaLoop = { checkLoop: () => { if (!sudaState.isRunning) return; if (sudaUtil.isSuccessPage()) { sudaState.completed++; sudaStore.save('completed', sudaState.completed); sudaUI.updateStatus(); // 通知父页面 sudaModal.sendToParent('success', { completed: sudaState.completed }); if (!sudaState.isUnlocked && sudaState.completed >= SD_CONFIG.maxFreeLoop) { sudaLoop.stop(); sudaUI.showMsg('已达免费次数上限,请输入密码解锁'); return; } const target = sudaStore.load('target', 0); if (target > 0 && sudaState.completed >= target) { sudaLoop.stop(); sudaUI.showMsg(`已完成${sudaState.completed}份,停止循环`); return; } sudaLoop.next(); } }, next: async () => { sudaUI.showMsg('准备下一份...'); await sudaUtil.wait(1500); const doc = sudaUtil.getIframeDoc(); const restartBtn = doc.querySelector('a[href*="vm"], a[href*="activity"], .restart-btn, button:has-text("再答一次"), button:has-text("再填一次")'); if (restartBtn) { restartBtn.click(); } else { const link = doc.querySelector('a[href*="join"], a[href*="jq"]'); if (link) link.click(); } await sudaUtil.wait(2000); sudaLoop.startFill(); }, startFill: async () => { if (!sudaState.isRunning) return; sudaParser.parseAll(); await sudaUtil.wait(800); sudaFiller.fillAll().then(ok => { if (ok) { sudaUI.showMsg('填写完成,提交中...'); sudaUtil.wait(1000).then(() => { const doc = sudaUtil.getIframeDoc(); const subBtn = doc.querySelector('.submit-btn, input[type="submit"]'); if (subBtn) subBtn.click(); }); } }); }, start: () => { if (sudaState.isRunning) return; sudaState.isRunning = true; sudaUI.updateStatus(); sudaLoop.startFill(); sudaLoopTimer = setInterval(() => { // 安全校验弹窗 if (sudaFiller.handleSecurityDialog()) { sudaUtil.log('处理安全校验'); } // 答题恢复弹窗 if (sudaFiller.handleResumeDialog()) { sudaUtil.log('处理答题恢复'); } // 验证码处理 if (document.querySelector('.nc_wrapper, .yidun')) { sudaFiller.clickCaptcha(); } // 成功页检测 if (sudaUtil.isSuccessPage()) { sudaLoop.checkLoop(); } }, 500); }, stop: () => { sudaState.isRunning = false; if (sudaLoopTimer) { clearInterval(sudaLoopTimer); sudaLoopTimer = null; } sudaUI.updateStatus(); } }; // ========== UI渲染 ========== const sudaUI = { panel: null, init: () => { sudaUI.createStyle(); sudaUI.createPanel(); sudaUI.bindEvents(); sudaUI.loadSettings(); }, createStyle: () => { const css = ` .sd-panel{position:fixed;top:80px;right:20px;z-index:999999;width:420px;max-height:calc(100vh - 100px);background:linear-gradient(135deg,#1F1F2E 0%,#2D2D44 100%);border-radius:16px;box-shadow:0 20px 60px rgba(124,58,237,0.3);display:flex;overflow:hidden;color:#F8FAFC;font-size:13px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;} .sd-panel.dragging{cursor:move;opacity:0.95;} .sd-sidebar{width:64px;background:rgba(124,58,237,0.2);display:flex;flex-direction:column;padding:12px 0;gap:4px;} .sd-tab-btn{width:100%;padding:12px 8px;border:none;background:transparent;color:rgba(255,255,255,0.6);cursor:pointer;font-size:20px;transition:all 0.3s;display:flex;flex-direction:column;align-items:center;gap:4px;} .sd-tab-btn span{font-size:10px;} .sd-tab-btn:hover{color:#fff;background:rgba(124,58,237,0.3);} .sd-tab-btn.active{color:#fff;background:linear-gradient(135deg,#7C3AED,#A855F7);border-radius:8px;margin:0 8px;} .sd-content{flex:1;padding:16px;overflow-y:auto;display:none;flex-direction:column;gap:12px;} .sd-content.active{display:flex;} .sd-header{text-align:center;padding-bottom:12px;border-bottom:1px solid rgba(255,255,255,0.1);} .sd-header h2{margin:0;font-size:18px;font-weight:600;background:linear-gradient(135deg,#A855F7,#7C3AED);-webkit-background-clip:text;-webkit-text-fill-color:transparent;} .sd-header p{margin:4px 0 0;font-size:11px;opacity:0.7;} .sd-input-group{display:flex;flex-direction:column;gap:6px;} .sd-input-group label{font-size:12px;opacity:0.8;} .sd-input{padding:10px 12px;border-radius:8px;border:1px solid rgba(255,255,255,0.15);background:rgba(255,255,255,0.05);color:#fff;font-size:13px;outline:none;transition:border-color 0.2s;} .sd-input:focus{border-color:#7C3AED;} .sd-input::placeholder{color:rgba(255,255,255,0.4);} .sd-btn{padding:10px 16px;border:none;border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;transition:all 0.3s;display:inline-flex;align-items:center;justify-content:center;gap:6px;} .sd-btn-primary{background:linear-gradient(135deg,#7C3AED,#A855F7);color:#fff;box-shadow:0 4px 12px rgba(124,58,237,0.4);} .sd-btn-primary:hover{transform:translateY(-1px);box-shadow:0 6px 16px rgba(124,58,237,0.5);} .sd-btn-success{background:linear-gradient(135deg,#10B981,#34D399);color:#fff;box-shadow:0 4px 12px rgba(16,185,129,0.4);} .sd-btn-success:hover{transform:translateY(-1px);} .sd-btn-danger{background:linear-gradient(135deg,#EF4444,#F87171);color:#fff;} .sd-btn-warning{background:linear-gradient(135deg,#F59E0B,#FBBF24);color:#1F1F2E;} .sd-btn-sm{padding:6px 10px;font-size:12px;} .sd-btn:disabled{opacity:0.5;cursor:not-allowed;transform:none;} .sd-btn-row{display:flex;gap:8px;flex-wrap:wrap;} .sd-btn-row .sd-btn{flex:1;min-width:80px;} .sd-card{background:rgba(255,255,255,0.05);border-radius:12px;padding:12px;border-left:3px solid #7C3AED;transition:all 0.3s;} .sd-card:hover{background:rgba(255,255,255,0.08);transform:translateX(2px);} .sd-q-item{margin-bottom:10px;} .sd-q-title{font-size:12px;font-weight:500;margin-bottom:6px;display:flex;align-items:center;gap:6px;flex-wrap:wrap;} .sd-q-type{font-size:10px;padding:2px 6px;border-radius:4px;background:rgba(124,58,237,0.3);color:#A855F7;} .sd-q-required{color:#EF4444;} .sd-opt-row{display:flex;align-items:center;gap:8px;margin:4px 0;font-size:12px;} .sd-opt-label{flex:1;opacity:0.8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} .sd-opt-input{width:60px;padding:4px 8px;border-radius:6px;border:1px solid rgba(255,255,255,0.15);background:rgba(255,255,255,0.05);color:#fff;font-size:12px;text-align:center;} .sd-toggle-row{display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-bottom:1px solid rgba(255,255,255,0.05);} .sd-toggle-label{font-size:13px;} .sd-toggle{position:relative;width:44px;height:24px;} .sd-toggle input{opacity:0;width:0;height:0;} .sd-toggle-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:rgba(255,255,255,0.2);border-radius:24px;transition:0.3s;} .sd-toggle-slider:before{position:absolute;content:"";height:18px;width:18px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:0.3s;} .sd-toggle input:checked+.sd-toggle-slider{background:linear-gradient(135deg,#7C3AED,#A855F7);} .sd-toggle input:checked+.sd-toggle-slider:before{transform:translateX(20px);} .sd-unlock-area{background:rgba(124,58,237,0.15);border-radius:12px;padding:16px;border:1px dashed rgba(124,58,237,0.4);} .sd-unlock-area h4{margin:0 0 10px;font-size:13px;color:#A855F7;text-align:center;} .sd-status-bar{padding:10px 12px;border-radius:8px;background:rgba(255,255,255,0.05);display:flex;align-items:center;gap:8px;font-size:12px;} .sd-status-dot{width:8px;height:8px;border-radius:50%;background:#6B7280;} .sd-status-dot.running{background:#10B981;animation:sdpulse 1.5s infinite;} @keyframes sdpulse{0%,100%{opacity:1}50%{opacity:0.5}} .sd-msg{position:fixed;top:20px;right:20px;z-index:9999999;padding:12px 20px;border-radius:10px;background:linear-gradient(135deg,#7C3AED,#A855F7);color:#fff;font-size:13px;box-shadow:0 8px 24px rgba(124,58,237,0.4);animation:sdin 0.3s ease;} @keyframes sdin{from{transform:translateX(100px);opacity:0}to{transform:translateX(0);opacity:1}} .sd-fab{position:fixed;bottom:30px;right:30px;z-index:999998;width:56px;height:56px;border-radius:50%;background:linear-gradient(135deg,#7C3AED,#A855F7);color:#fff;border:none;cursor:pointer;font-size:24px;box-shadow:0 6px 20px rgba(124,58,237,0.5);display:flex;align-items:center;justify-content:center;transition:all 0.3s;} .sd-fab:hover{transform:scale(1.1);} .sd-fab.hidden{display:none;} .sd-hint{font-size:11px;opacity:0.6;padding:8px;background:rgba(255,255,255,0.03);border-radius:8px;line-height:1.6;} .sd-scroll::-webkit-scrollbar{width:4px;} .sd-scroll::-webkit-scrollbar-track{background:transparent;} .sd-scroll::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.2);border-radius:2px;} /* 信效度开关样式 */ .sd-psycho-chip{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:12px;background:rgba(124,58,237,0.2);color:#A855F7;font-size:10px;cursor:pointer;transition:all 0.2s;border:1px solid transparent;} .sd-psycho-chip:hover{background:rgba(124,58,237,0.3);} .sd-psycho-chip.active{background:linear-gradient(135deg,#7C3AED,#A855F7);color:#fff;} .sd-psycho-bias{display:flex;gap:4px;margin-left:8px;} .sd-psycho-bias-btn{padding:2px 6px;border-radius:8px;background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.2);color:rgba(255,255,255,0.7);font-size:10px;cursor:pointer;transition:all 0.2s;} .sd-psycho-bias-btn:hover{background:rgba(255,255,255,0.2);} .sd-psycho-bias-btn.selected{background:#7C3AED;color:#fff;border-color:#7C3AED;} /* iframe弹窗样式 */ .sd-modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);z-index:9999998;display:flex;align-items:center;justify-content:center;} .sd-modal-iframe{width:90%;height:90%;max-width:1200px;max-height:800px;border:none;border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,0.5);} .sd-modal-close{position:absolute;top:20px;right:20px;width:40px;height:40px;border-radius:50%;background:rgba(255,255,255,0.2);border:none;color:#fff;font-size:24px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all 0.2s;} .sd-modal-close:hover{background:rgba(255,255,255,0.3);} /* 排序题样式 */ .sd-sort-item{display:flex;align-items:center;gap:8px;padding:4px 8px;background:rgba(255,255,255,0.05);border-radius:6px;margin:2px 0;} .sd-sort-drag{cursor:grab;color:rgba(255,255,255,0.5);} .sd-sort-drag:active{cursor:grabbing;} .sd-validity-btn{padding:2px 8px;font-size:11px;border-radius:4px;border:none;cursor:pointer;transition:all 0.2s;} .sd-validity-btn.off{background:rgba(255,255,255,0.1);color:rgba(255,255,255,0.5);} .sd-validity-btn.on{background:linear-gradient(135deg,#10B981,#34D399);color:#fff;} .sd-select{width:100%;padding:8px 10px;border-radius:6px;border:1px solid rgba(255,255,255,0.15);background:rgba(255,255,255,0.05);color:#fff;font-size:12px;outline:none;} .sd-select option{background:#2D2D44;color:#fff;} `; const style = document.createElement('style'); style.id = 'sd-style'; style.textContent = css; document.head.appendChild(style); }, createPanel: () => { const html = `

⚡ 速答精灵

v${SD_CONFIG.version} | SparkLab

等待解析
填写前请先设置配比,可提高填写效率
已解析题目: 0

⏱️ 答题时间

🎭 作答模式

逐题拟人模式

关闭后为瞬时秒刷模式

📐 信效度模式

启用信效度智能

启用后量表题使用正态分布生成答案

🔄 自动循环

运行状态
已停止
已完成: 0

🔐 解锁无限循环

未解锁最多循环${SD_CONFIG.maxFreeLoop}份

✨ 功能介绍

  • 智能解析问卷题目
  • 自定义选项配比权重
  • 信效度智能填写模式
  • 支持排序题拖拽
  • 拟人化自动填写
  • 自动循环提交
  • 验证码自动处理
  • 弹窗模式支持

📌 使用提示

1. 粘贴问卷链接并解析
2. 设置各选项的填写百分比
3. 可开启信效度智能模式
4. 选择作答模式并填写
5. 使用自动循环需解锁

📞 联系我们

问题反馈: suda@suda.dev

交流群: 123456789

`; const wrap = document.createElement('div'); wrap.id = 'sd-wrap'; wrap.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;z-index:999997;pointer-events:none;'; wrap.innerHTML = html; document.body.appendChild(wrap); sudaUI.panel = document.getElementById('sd-panel'); }, bindEvents: () => { document.querySelectorAll('.sd-tab-btn').forEach(btn => { btn.addEventListener('click', () => { const tab = btn.dataset.tab; document.querySelectorAll('.sd-tab-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.sd-content').forEach(c => c.classList.remove('active')); btn.classList.add('active'); document.getElementById('sd-' + tab).classList.add('active'); sudaState.currentTab = tab; if (tab === 'ratio') sudaUI.renderRatio(); }); }); document.getElementById('sd-parse').addEventListener('click', () => { const url = document.getElementById('sd-url').value.trim(); if (!url) { sudaUI.showMsg('请输入问卷链接'); return; } sudaState.surveyUrl = url; sudaUI.showMsg('正在解析...', 2000); setTimeout(() => { sudaParser.parseAll(); document.getElementById('sd-parse-status').textContent = `已解析 ${sudaState.questions.length} 道题`; document.getElementById('sd-q-count').textContent = sudaState.questions.length; if (sudaState.questions.length) sudaUI.showMsg('解析成功'); else sudaUI.showMsg('未检测到题目,请确保在问卷页'); }, 500); }); document.getElementById('sd-open').addEventListener('click', () => { const url = document.getElementById('sd-url').value.trim(); if (!url) { sudaUI.showMsg('请输入问卷链接'); return; } window.open(url, '_blank'); }); document.getElementById('sd-open-modal').addEventListener('click', () => { const url = document.getElementById('sd-url').value.trim(); if (!url) { sudaUI.showMsg('请输入问卷链接'); return; } sudaModal.openSurveyModal(url, { onSuccess: () => { sudaState.completed++; sudaStore.save('completed', sudaState.completed); sudaUI.updateStatus(); if (sudaState.isRunning) { sudaLoop.next(); } } }); sudaUI.showMsg('已在弹窗中打开问卷'); }); document.getElementById('sd-fill-one').addEventListener('click', async () => { if (!sudaState.questions.length) { sudaUI.showMsg('请先解析问卷'); return; } sudaUI.showMsg('开始填写...'); await sudaFiller.fillAll(); sudaUI.showMsg('填写完成'); }); document.getElementById('sd-submit').addEventListener('click', () => { const doc = sudaUtil.getIframeDoc(); const subBtn = doc.querySelector('.submit-btn, input[type="submit"]'); if (subBtn) { sudaUI.showMsg('提交中...'); subBtn.click(); } else { sudaUI.showMsg('未找到提交按钮'); } }); document.getElementById('sd-random-ratio').addEventListener('click', () => { sudaState.questions.forEach(q => { q.options.forEach(o => { o.ratio = sudaUtil.rand(0, 100); }); }); sudaUI.renderRatio(); sudaUI.showMsg('已生成随机配比'); }); document.getElementById('sd-save-ratio').addEventListener('click', () => { // 先收集当前UI中的配比值 document.querySelectorAll('.sd-opt-input').forEach(inp => { const qIdx = parseInt(inp.dataset.q); const oKey = inp.dataset.o; const val = parseFloat(inp.value) || 0; if (sudaState.questions[qIdx]) { if (oKey.includes('-')) { const [ri, ci] = oKey.split('-').map(Number); if (sudaState.questions[qIdx].options[ri] && sudaState.questions[qIdx].options[ri].children) { sudaState.questions[qIdx].options[ri].children[ci].ratio = val; } } else { sudaState.questions[qIdx].options[parseInt(oKey)].ratio = val; } } }); // 收集填空题答案 document.querySelectorAll('[id^="sd-text-"]').forEach(inp => { const qi = parseInt(inp.id.replace('sd-text-', '')); if (sudaState.questions[qi]) { sudaState.questions[qi].options[0].textAnswers = inp.value.split(';').filter(Boolean); } }); // 保存信效度开关状态 document.querySelectorAll('.sd-validity-btn.on').forEach(btn => { const qi = parseInt(btn.dataset.q); if (sudaState.questions[qi]) { sudaState.questions[qi].validityEnabled = true; } }); sudaRatioConfig.save(); sudaUI.showMsg('配比已保存'); }); document.getElementById('sd-min-time').addEventListener('change', e => { sudaState.minTime = parseInt(e.target.value) || 3; }); document.getElementById('sd-max-time').addEventListener('change', e => { sudaState.maxTime = parseInt(e.target.value) || 8; }); document.getElementById('sd-mode-toggle').addEventListener('change', e => { sudaState.mode = e.target.checked ? 'human' : 'fast'; }); document.getElementById('sd-validity-toggle').addEventListener('change', e => { sudaState.validityEnabled = e.target.checked; }); document.getElementById('sd-validity-bias').addEventListener('change', e => { sudaState.validityBias = e.target.value; }); document.getElementById('sd-target').addEventListener('change', e => { sudaStore.save('target', parseInt(e.target.value) || 0); }); document.getElementById('sd-reset-count').addEventListener('click', () => { sudaState.completed = 0; sudaStore.save('completed', 0); document.getElementById('sd-completed').textContent = 0; sudaUI.showMsg('计数已重置'); }); document.getElementById('sd-unlock-btn').addEventListener('click', () => { const key = document.getElementById('sd-unlock-key').value; if (sudaCheckKey(key)) { sudaState.isUnlocked = true; sudaStore.save('unlocked', true); sudaUI.showMsg('解锁成功,无限循环已开启'); } else { sudaUI.showMsg('密码错误'); } }); document.getElementById('sd-start-loop').addEventListener('click', () => { if (!sudaState.questions.length) { sudaUI.showMsg('请先解析问卷'); return; } sudaLoop.start(); sudaUI.showMsg('自动循环已开始'); }); document.getElementById('sd-stop-loop').addEventListener('click', () => { sudaLoop.stop(); sudaUI.showMsg('自动循环已停止'); }); sudaUI.initDrag(); }, initDrag: () => { const panel = sudaUI.panel; let isDrag = false, ox, oy; panel.addEventListener('mousedown', e => { if (e.target.closest('.sd-sidebar, .sd-content, .sd-input, .sd-btn, .sd-select')) return; isDrag = true; panel.classList.add('dragging'); ox = e.clientX - panel.offsetLeft; oy = e.clientY - panel.offsetTop; }); document.addEventListener('mousemove', e => { if (!isDrag) return; panel.style.left = (e.clientX - ox) + 'px'; panel.style.top = (e.clientY - oy) + 'px'; panel.style.right = 'auto'; }); document.addEventListener('mouseup', () => { isDrag = false; panel.classList.remove('dragging'); }); }, renderRatio: () => { const container = document.getElementById('sd-ratio-list'); if (!sudaState.questions.length) { container.innerHTML = '
请先解析问卷
'; return; } container.innerHTML = sudaState.questions.map((q, qi) => { const psycho = sudaState.psychoMap[qi] || { enabled: false, bias: SUDA_PSYCHO.CENTER }; let html = `
${qi + 1}. ${q.title.substring(0, 20)}${q.title.length > 20 ? '...' : ''} ${q.type} ${q.required ? '*' : ''}
`; if (q.type === 'text') { const textAns = q.options[0]?.textAnswers?.join(';') || ''; html += ``; } else if (q.type === 'matrix') { q.options.forEach((row, ri) => { html += `
${row.text}
`; row.children.forEach((col, ci) => { const ratio = col.ratio || 50; html += `
${ci + 1}%
`; }); }); } else { q.options.forEach((o, oi) => { const ratio = parseFloat(o.ratio) || 50; html += `
${o.text.substring(0, 20)}${o.text.length > 20 ? '...' : ''}%
`; }); } html += '
'; return html; }).join(''); // 绑定信效度开关事件 document.querySelectorAll('.sd-validity-btn').forEach(btn => { btn.addEventListener('click', () => { const qi = parseInt(btn.dataset.q); if (sudaState.questions[qi]) { sudaState.questions[qi].validityEnabled = !sudaState.questions[qi].validityEnabled; btn.classList.toggle('on'); btn.classList.toggle('off'); } }); }); // 显示信效度开关行(对于radio/scale类型) document.querySelectorAll('.sd-q-type').forEach(typeEl => { const qItem = typeEl.closest('.sd-q-item'); if (qItem) { const type = typeEl.textContent; if (['radio', 'scale'].includes(type)) { const psychoRow = qItem.querySelector('.sd-psycho-row'); if (psychoRow) psychoRow.style.display = 'flex'; } } }); // 绑定信效度开关chip点击事件 document.querySelectorAll('.sd-psycho-chip').forEach(chip => { chip.addEventListener('click', () => { const qi = parseInt(chip.dataset.q); if (!sudaState.psychoMap[qi]) { sudaState.psychoMap[qi] = { enabled: true, bias: SUDA_PSYCHO.CENTER }; } sudaState.psychoMap[qi].enabled = !sudaState.psychoMap[qi].enabled; chip.classList.toggle('active', sudaState.psychoMap[qi].enabled); }); }); // 绑定偏向选择按钮事件 document.querySelectorAll('.sd-psycho-bias-btn').forEach(biasBtn => { biasBtn.addEventListener('click', () => { const qi = parseInt(biasBtn.dataset.q); const bias = biasBtn.dataset.bias; if (!sudaState.psychoMap[qi]) { sudaState.psychoMap[qi] = { enabled: true, bias: bias }; } else { sudaState.psychoMap[qi].bias = bias; } // 更新按钮样式 const biasContainer = document.getElementById(`sd-psycho-bias-${qi}`); if (biasContainer) { biasContainer.querySelectorAll('.sd-psycho-bias-btn').forEach(btn => { btn.classList.toggle('selected', btn.dataset.bias === bias); }); } }); }); // 绑定配比输入事件 document.querySelectorAll('.sd-opt-input').forEach(inp => { inp.addEventListener('change', () => { const qIdx = parseInt(inp.dataset.q); const oKey = inp.dataset.o; const val = parseFloat(inp.value) || 0; if (sudaState.questions[qIdx]) { if (oKey.includes('-')) { const [ri, ci] = oKey.split('-').map(Number); if (sudaState.questions[qIdx].options[ri]?.children?.[ci]) { sudaState.questions[qIdx].options[ri].children[ci].ratio = val; } } else if (sudaState.questions[qIdx].options[parseInt(oKey)]) { sudaState.questions[qIdx].options[parseInt(oKey)].ratio = val; } } }); }); // 绑定填空题答案事件 document.querySelectorAll('[id^="sd-text-"]').forEach(inp => { inp.addEventListener('change', () => { const qi = parseInt(inp.id.replace('sd-text-', '')); if (sudaState.questions[qi]) { sudaState.questions[qi].options[0].textAnswers = inp.value.split(';').filter(Boolean); } }); }); }, loadSettings: () => { sudaState.completed = sudaStore.load('completed', 0); sudaState.isUnlocked = sudaStore.load('unlocked', false); document.getElementById('sd-completed').textContent = sudaState.completed; const savedConfig = sudaRatioConfig.load(); if (savedConfig) { document.getElementById('sd-min-time').value = savedConfig.minTime || SD_CONFIG.defaultMinTime; document.getElementById('sd-max-time').value = savedConfig.maxTime || SD_CONFIG.defaultMaxTime; document.getElementById('sd-mode-toggle').checked = savedConfig.mode !== 'fast'; document.getElementById('sd-validity-toggle').checked = savedConfig.validityEnabled || false; document.getElementById('sd-validity-bias').value = savedConfig.validityBias || 'center'; sudaState.mode = savedConfig.mode || 'human'; sudaState.validityEnabled = savedConfig.validityEnabled || false; sudaState.validityBias = savedConfig.validityBias || 'center'; sudaState.minTime = savedConfig.minTime || SD_CONFIG.defaultMinTime; sudaState.maxTime = savedConfig.maxTime || SD_CONFIG.defaultMaxTime; } }, updateStatus: () => { const dot = document.getElementById('sd-run-dot'); const status = document.getElementById('sd-run-status'); if (sudaState.isRunning) { dot.classList.add('running'); status.textContent = '运行中'; } else { dot.classList.remove('running'); status.textContent = '已停止'; } }, showMsg: (msg, duration = 2500) => { const old = document.querySelector('.sd-msg'); if (old) old.remove(); const div = document.createElement('div'); div.className = 'sd-msg'; div.textContent = msg; document.body.appendChild(div); setTimeout(() => div.remove(), duration); } }; // ========== 初始化 ========== const sudaInit = () => { if (document.getElementById('sd-panel')) return; if (!/wjx/i.test(location.hostname)) return; sudaUI.init(); const fab = document.getElementById('sd-fab'); fab.classList.remove('hidden'); fab.addEventListener('click', () => { const panel = document.getElementById('sd-panel'); panel.style.display = panel.style.display === 'none' ? 'flex' : 'none'; }); const autoParse = () => { const wrappers = document.querySelectorAll('.question, .question-item, [class*="question"]'); if (wrappers.length) { sudaParser.parseAll(); document.getElementById('sd-parse-status').textContent = `已解析 ${sudaState.questions.length} 道题`; document.getElementById('sd-q-count').textContent = sudaState.questions.length; const urlInput = document.getElementById('sd-url'); if (urlInput && !urlInput.value) { urlInput.value = location.href; } } }; if (document.readyState === 'complete') { setTimeout(autoParse, 1500); } else { window.addEventListener('load', () => setTimeout(autoParse, 1500)); } // 弹窗模式消息监听 sudaModal.listenFromParent(); sudaModal.setupIframeMsgListener(); // 启动安全校验弹窗监听 sudaSecurity.startSecurityDialogWatcher(); // 启动答题恢复弹窗监听 sudaResume.startResumeDialogWatcher(); // 如果在iframe中,检测成功页并通知父页面 if (sudaModal.isInIframe()) { sudaModal.notifyParentSuccess(); } // 主循环定时器 setInterval(() => { // 安全校验弹窗检测 sudaFiller.handleSecurityDialog(); // 答题恢复弹窗检测 sudaFiller.handleResumeDialog(); // 验证码检测 if (document.querySelector('.nc_wrapper, .yidun, [class*="captcha"]')) { sudaFiller.clickCaptcha(); } // layui-layer弹窗检测 const layuiDialog = document.querySelector('.layui-layer, [class*="layui-layer"]'); if (layuiDialog && !layuiDialog.style.display) { const confirmBtn = layuiDialog.querySelector('.layui-layer-btn0, .layui-layer-btn a:first-child'); if (confirmBtn) { confirmBtn.click(); sudaUtil.log('已处理layui弹窗'); } } }, 800); // 自动循环成功检测 setInterval(() => { if (sudaState.isRunning && sudaUtil.isSuccessPage()) { sudaLoop.checkLoop(); } }, 500); sudaUtil.log('速答精灵已加载', SD_CONFIG.version); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', sudaInit); } else { sudaInit(); } })();