// ==UserScript==
// @name 问卷星自动答题助手
// @namespace https://wjx.panel.local
// @version 0.0.5
// @description 一款功能强大的问卷星辅助工具。支持全题型识别(单选、多选、填空、矩阵、量表、滑块、地区、排序),通过可视化面板自定义各选项权重或次数及设计份数,支持比例/数量模式切换与配置 JSON 导出导入。支持一键破解复制限制,全自动模拟填写与批量提交,内置滑块验证模拟及本地存储清理功能。
// @author x7R2p9,yangwenren
// @match https://*.wjx.top/*
// @match https://*.wjx.cn/*
// @match https://*.wjx.com/*
// @grant none
// @run-at document-end
// @license MIT
// @tag 问卷星
// @tag 问卷星脚本
// @tag 问卷星全自动
// @tag 自动填写
// @tag 批量提交
// @tag 问卷星刷问卷
// @tag 问卷星辅助
// ==/UserScript==
(function() {
'use strict';
const PANEL_ID = 'wjx-commercial-panel';
const STORAGE_PREFIX = 'WJX_CN_PANEL_';
const REMAINING_COUNT_KEY = 'WJX_REMAINING_COUNT';
const TOTAL_COUNT_KEY = 'WJX_TOTAL_COUNT';
const SURVEY_URL_KEY = 'WJX_SURVEY_URL';
const PANEL_COLLAPSED_KEY = 'WJX_PANEL_COLLAPSED_BEFORE_AUTOMATION';
const QUOTA_STATE_KEY = 'WJX_COUNT_QUOTA_STATE';
const CONFIG_EXPORT_VERSION = 1;
const GREASYFORK_URL = 'https://scriptcat.org/zh-CN/script-show-page/6321';
const PROMO_TOAST_KEY = 'WJX_GREASYFORK_PROMO_SHOWN';
const DEFAULT_TEXT_LIBRARY = ['很好', '满意', '支持', '体验不错'];
const TYPE_LABELS = {
radio: '单选题',
checkbox: '多选题',
dropdown: '下拉题',
text: '填空题',
rating: '量表题',
matrix: '矩阵题',
slide: '滑块题',
location: '地区题',
sorting: '排序题',
unknown: '未知题型'
};
const State = {
surveyKey: '',
config: {
targetCount: 1,
distributionMode: 'ratio',
questions: []
}
};
const STYLE_TEXT = `
:root {
--wjx-bg: #f5f9ff;
--wjx-surface: #ffffff;
--wjx-surface-strong: #ffffff;
--wjx-primary: #0064ff;
--wjx-primary-deep: #0052d9;
--wjx-primary-soft: #edf4ff;
--wjx-line: #d7e6ff;
--wjx-text: #1f2d3d;
--wjx-subtle: #5f6f82;
--wjx-shadow: 0 10px 28px rgba(23, 80, 179, 0.10);
}
#${PANEL_ID} {
position: fixed;
top: 16px;
right: 16px;
width: 402px;
max-height: calc(100vh - 32px);
z-index: 2147483647;
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: 10px;
border: 1px solid var(--wjx-line);
background: var(--wjx-surface);
color: var(--wjx-text);
box-shadow: var(--wjx-shadow);
font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
}
#${PANEL_ID}.is-collapsed {
width: 240px;
}
#${PANEL_ID}.is-automation-hidden {
display: none !important;
}
.wjx-head {
padding: 16px;
background: linear-gradient(180deg, #f8fbff, #edf4ff);
color: var(--wjx-text);
border-bottom: 1px solid var(--wjx-line);
border-top: 3px solid var(--wjx-primary);
}
.wjx-head-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.wjx-title {
margin: 0;
font-size: 18px;
font-weight: 700;
letter-spacing: 0;
}
.wjx-toggle {
border: 1px solid #b9d2ff;
border-radius: 6px;
padding: 6px 12px;
font-size: 11px;
font-weight: 700;
color: var(--wjx-primary-deep);
background: #ffffff;
cursor: pointer;
flex-shrink: 0;
}
.wjx-body {
padding: 16px;
overflow-y: auto;
background: var(--wjx-bg);
}
.wjx-section {
margin-bottom: 14px;
padding: 14px;
border-radius: 8px;
background: var(--wjx-surface);
border: 1px solid var(--wjx-line);
}
.wjx-section-title {
margin: 0 0 10px;
font-size: 13px;
font-weight: 700;
color: var(--wjx-primary-deep);
}
.wjx-metrics {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.wjx-metric {
padding: 12px;
border-radius: 8px;
background: var(--wjx-surface-strong);
border: 1px solid var(--wjx-line);
}
.wjx-metric span {
display: block;
}
.wjx-metric-label {
font-size: 11px;
color: var(--wjx-subtle);
margin-bottom: 4px;
}
.wjx-metric-value {
font-size: 18px;
font-weight: 800;
}
.wjx-field {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 10px;
}
.wjx-field label {
font-size: 12px;
font-weight: 700;
}
.wjx-input,
.wjx-textarea {
width: 100%;
box-sizing: border-box;
border: 1px solid #c9dbff;
border-radius: 6px;
padding: 10px 12px;
font-size: 13px;
color: var(--wjx-text);
background: #ffffff;
outline: none;
}
.wjx-input:focus,
.wjx-textarea:focus {
border-color: #7fb0ff;
box-shadow: 0 0 0 3px rgba(0,100,255,0.10);
}
.wjx-textarea {
min-height: 88px;
resize: vertical;
line-height: 1.5;
}
.wjx-mode-switch {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.wjx-mode-btn {
min-height: 36px;
border-radius: 6px;
padding: 0 10px;
font-size: 12px;
font-weight: 700;
cursor: pointer;
color: var(--wjx-text);
background: #ffffff;
border: 1px solid #c9dbff;
}
.wjx-mode-btn.is-active {
color: #ffffff;
background: var(--wjx-primary);
border-color: var(--wjx-primary);
}
.wjx-mode-hint {
margin: 0 0 10px;
font-size: 11px;
line-height: 1.5;
color: var(--wjx-subtle);
}
.wjx-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.wjx-btn {
min-height: 40px;
border-radius: 6px;
padding: 0 12px;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: background-color 0.18s ease, border-color 0.18s ease, color 0.18s ease;
}
.wjx-btn:hover {
filter: none;
}
.wjx-btn-primary {
color: #ffffff;
background: var(--wjx-primary);
border: 1px solid var(--wjx-primary);
}
.wjx-btn-secondary {
color: var(--wjx-text);
background: #ffffff;
border: 1px solid #c9dbff;
}
.wjx-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.wjx-card {
padding: 14px;
border-radius: 8px;
background: var(--wjx-surface-strong);
border: 1px solid var(--wjx-line);
}
.wjx-card-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
margin-bottom: 8px;
}
.wjx-card-title {
font-size: 13px;
font-weight: 800;
line-height: 1.5;
}
.wjx-card-type {
flex-shrink: 0;
padding: 5px 9px;
border-radius: 6px;
background: var(--wjx-primary-soft);
color: var(--wjx-primary-deep);
font-size: 11px;
font-weight: 700;
border: 1px solid #cfe0ff;
}
.wjx-card-meta {
margin-bottom: 10px;
color: var(--wjx-subtle);
font-size: 11px;
line-height: 1.45;
}
.wjx-count-hint {
margin: 0 0 8px;
font-size: 12px;
font-weight: 700;
color: var(--wjx-primary-deep);
line-height: 1.45;
}
.wjx-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.wjx-grid-item {
padding: 8px;
border-radius: 6px;
background: #f7fbff;
border: 1px solid #deebff;
}
.wjx-grid-item span {
display: block;
margin-bottom: 6px;
font-size: 11px;
color: var(--wjx-subtle);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.wjx-grid-item input {
width: 100%;
box-sizing: border-box;
border: 1px solid #c9dbff;
border-radius: 6px;
padding: 8px 10px;
font-size: 13px;
background: #ffffff;
}
.wjx-tools {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-top: 10px;
}
.wjx-chip {
border: 1px solid #cfe0ff;
border-radius: 6px;
padding: 6px 10px;
font-size: 11px;
font-weight: 700;
color: var(--wjx-primary-deep);
background: var(--wjx-primary-soft);
cursor: pointer;
}
.wjx-empty {
text-align: center;
padding: 24px 16px;
color: var(--wjx-subtle);
line-height: 1.7;
}
.wjx-toast {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 2147483647;
max-width: 320px;
padding: 12px 14px;
border-radius: 8px;
color: #ffffff;
background: rgba(0, 82, 217, 0.94);
box-shadow: 0 12px 28px rgba(0, 82, 217, 0.20);
font-size: 13px;
line-height: 1.5;
opacity: 0;
transform: translateY(8px);
transition: opacity 0.18s ease, transform 0.18s ease;
pointer-events: none;
}
.wjx-toast.is-actionable {
max-width: 300px;
padding: 10px 12px;
font-size: 12px;
line-height: 1.6;
background: rgba(19, 50, 104, 0.96);
box-shadow: 0 10px 24px rgba(19, 50, 104, 0.18);
pointer-events: auto;
cursor: pointer;
}
.wjx-toast .wjx-toast-link {
color: #ffd36b;
font-weight: 700;
}
.wjx-toast.is-visible {
opacity: 1;
transform: translateY(0);
}
.wjx-progress-modal {
position: fixed;
top: 16px;
left: 50%;
z-index: 2147483647;
min-width: 240px;
padding: 14px 18px;
border-radius: 10px;
border: 1px solid #b9d2ff;
background: rgba(255, 255, 255, 0.98);
color: var(--wjx-text);
box-shadow: 0 12px 32px rgba(0, 82, 217, 0.18);
font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
text-align: center;
opacity: 0;
transform: translate(-50%, -8px);
transition: opacity 0.2s ease, transform 0.2s ease;
pointer-events: none;
}
.wjx-progress-modal.is-visible {
opacity: 1;
transform: translate(-50%, 0);
pointer-events: auto;
}
.wjx-progress-title {
margin: 0 0 8px;
font-size: 13px;
font-weight: 700;
color: var(--wjx-primary-deep);
}
.wjx-progress-count {
margin: 0 0 10px;
font-size: 16px;
font-weight: 800;
color: var(--wjx-text);
}
.wjx-progress-bar-wrap {
height: 8px;
margin-bottom: 8px;
border-radius: 999px;
background: #e8f1ff;
overflow: hidden;
}
.wjx-progress-bar {
height: 100%;
width: 0;
border-radius: 999px;
background: linear-gradient(90deg, var(--wjx-primary), #3d8bff);
transition: width 0.25s ease;
}
.wjx-progress-percent {
margin: 0;
font-size: 12px;
font-weight: 700;
color: var(--wjx-subtle);
}
.wjx-progress-stop {
margin-top: 12px;
min-height: 34px;
padding: 0 16px;
border-radius: 6px;
border: 1px solid #ffb4b4;
background: #fff5f5;
color: #d4380d;
font-size: 13px;
font-weight: 700;
cursor: pointer;
}
.wjx-progress-stop:hover {
background: #ffece8;
}
.wjx-hidden {
display: none;
}
.wjx-file-input {
display: none;
}
@media (max-width: 640px) {
#${PANEL_ID} {
top: auto;
right: 10px;
left: 10px;
bottom: 10px;
width: auto;
max-height: 82vh;
}
.wjx-metrics,
.wjx-actions,
.wjx-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
`;
function init() {
// 破解右键、选择和复制限制
try {
document.oncontextmenu = () => true;
document.onselectstart = () => true;
document.oncopy = () => true;
document.onpaste = () => true;
if (window.$ && window.$.fn) {
$('body').css('user-select', 'text');
}
} catch (e) {}
if (isCompletionPage()) {
handleCompletion();
return;
}
if (document.getElementById(PANEL_ID)) {
return;
}
State.surveyKey = createSurveyKey();
injectStyles();
renderPanel();
refreshQuestions({ silent: true, preserveInputs: false });
updateAllRelationVisibility();
scheduleRescan();
checkAndContinueAutomation();
schedulePromoToast();
}
function isCompletionPage() {
return /complete\.aspx|join\/complete|done/i.test(location.href);
}
function handleCompletion() {
let remaining = Number(localStorage.getItem(REMAINING_COUNT_KEY) || 0);
if (remaining > 0) {
remaining -= 1;
localStorage.setItem(REMAINING_COUNT_KEY, String(remaining));
if (remaining > 0) {
ensureTotalCountForAutomation();
hidePanelForAutomation();
showAutomationProgress();
const url = localStorage.getItem(SURVEY_URL_KEY);
if (url) {
clearTimeout(automationRedirectTimer);
automationRedirectTimer = setTimeout(() => {
automationRedirectTimer = null;
if (!isAutomationActive()) {
return;
}
location.href = url;
}, 2000);
}
} else {
endAutomationSession('所有自动化提交任务已完成!');
}
updateAutomationProgress();
}
}
function checkAndContinueAutomation() {
const remaining = Number(localStorage.getItem(REMAINING_COUNT_KEY) || 0);
if (remaining > 0) {
ensureTotalCountForAutomation();
ensureQuotaState();
beginAutomationSession();
setTimeout(() => {
executeAutoFillAndSubmit();
}, 2000);
}
}
function createSurveyKey() {
return STORAGE_PREFIX + `${location.host}${location.pathname}`.replace(/[^a-zA-Z0-9_-]/g, '_');
}
function injectStyles() {
const style = document.createElement('style');
style.textContent = STYLE_TEXT;
document.head.appendChild(style);
}
function renderPanel() {
const panel = document.createElement('aside');
panel.id = PANEL_ID;
panel.innerHTML = `
`;
document.body.appendChild(panel);
bindEvents(panel);
}
function bindEvents(panel) {
panel.querySelector('#wjx-toggle-panel').addEventListener('click', () => {
panel.classList.toggle('is-collapsed');
const body = panel.querySelector('#wjx-panel-body');
const button = panel.querySelector('#wjx-toggle-panel');
const collapsed = panel.classList.contains('is-collapsed');
body.classList.toggle('wjx-hidden', collapsed);
button.textContent = collapsed ? '展开' : '收起';
});
panel.querySelector('#wjx-target-count').addEventListener('input', (event) => {
State.config.targetCount = Math.max(1, Number(event.target.value) || 1);
updateCountModeHints();
});
panel.addEventListener('input', (event) => {
if (!event.target.matches('[data-role="weight-input"]')) {
return;
}
if (isCountMode()) {
updateCountModeHints();
}
});
panel.querySelector('#wjx-mode-ratio').addEventListener('click', () => {
setDistributionMode('ratio');
});
panel.querySelector('#wjx-mode-count').addEventListener('click', () => {
setDistributionMode('count');
});
panel.querySelector('#wjx-randomize-all').addEventListener('click', () => {
randomizeAllQuestions();
});
panel.querySelector('#wjx-rescan').addEventListener('click', () => {
refreshQuestions({ silent: false, preserveInputs: true });
});
panel.querySelector('#wjx-export-config').addEventListener('click', () => {
exportCurrentConfig();
});
panel.querySelector('#wjx-import-config').addEventListener('click', () => {
const fileInput = panel.querySelector('#wjx-import-file');
if (fileInput) {
fileInput.click();
}
});
panel.querySelector('#wjx-import-file').addEventListener('change', (event) => {
const file = event.target.files && event.target.files[0];
importConfigFromFile(file);
event.target.value = '';
});
panel.querySelector('#wjx-start-automation').addEventListener('click', () => {
syncStateFromDom();
saveConfig();
startAutomation();
});
panel.addEventListener('click', (event) => {
const trigger = event.target.closest('[data-preset]');
if (!trigger) {
return;
}
applyPreset(trigger.closest('.wjx-card'), trigger.dataset.preset);
});
}
function refreshQuestions(options) {
const settings = Object.assign({ silent: false, preserveInputs: true }, options);
const previousQuestions = settings.preserveInputs ? collectCurrentQuestionsFromDom() : [];
const previousTargetCount = getCurrentTargetCount();
const savedConfig = loadConfig();
const scannedQuestions = scanQuestions();
State.config = mergeConfig(savedConfig, previousQuestions, previousTargetCount, scannedQuestions);
renderStats();
renderQuestionList();
updateCountModeHints();
const targetInput = document.getElementById('wjx-target-count');
if (targetInput) {
targetInput.value = String(State.config.targetCount);
}
updateDistributionModeUI();
if (shouldAutoRandomize(settings, previousQuestions, savedConfig, scannedQuestions)) {
randomizeAllQuestions({ silent: true });
}
updateAllRelationVisibility();
if (!settings.silent) {
const relationCount = scannedQuestions.filter((item) => Array.isArray(item.relations) && item.relations.length).length;
if (scannedQuestions.length) {
const relationTip = relationCount ? `,其中 ${relationCount} 道含题目关联` : '';
showToast(`已重新识别 ${scannedQuestions.length} 道题${relationTip}。`);
} else {
showToast('当前页面没有识别到可配置题目。');
}
}
}
function shouldAutoRandomize(settings, previousQuestions, savedConfig, scannedQuestions) {
if (!scannedQuestions.length) {
return false;
}
if (settings.preserveInputs && previousQuestions.length) {
return false;
}
if (hasConfiguredQuestions(savedConfig && savedConfig.questions, savedConfig && savedConfig.distributionMode)) {
return false;
}
return true;
}
function hasConfiguredQuestions(list, distributionMode) {
if (!Array.isArray(list) || !list.length) {
return false;
}
const countMode = isCountModeFromConfig({ distributionMode });
return list.some((question) => {
if (question.type === 'text') {
return Array.isArray(question.content) && question.content.length > 0;
}
if (question.type === 'slide') {
return clampScore(question.minScore, 1) !== 1 || clampScore(question.maxScore, 100) !== 100;
}
if (countMode) {
return Array.isArray(question.weights) && question.weights.some((weight) => Number(weight) > 0);
}
return Array.isArray(question.weights) && question.weights.some((weight) => Number(weight) > 1);
});
}
function mergeConfig(savedConfig, previousQuestions, previousTargetCount, scannedQuestions) {
const mergedQuestions = scannedQuestions.map((question) => {
const previous = findMatchingQuestion(previousQuestions, question) || findMatchingQuestion(savedConfig && savedConfig.questions, question);
if (!previous) {
return question;
}
const next = Object.assign({}, question);
if (question.type === 'text') {
next.content = Array.isArray(previous.content) && previous.content.length ? previous.content.slice() : question.content.slice();
} else if (question.type === 'slide') {
next.minScore = clampScore(previous.minScore, question.minScore);
next.maxScore = clampScore(previous.maxScore, question.maxScore);
if (next.maxScore < next.minScore) {
next.maxScore = next.minScore;
}
} else if (Array.isArray(previous.weights) && previous.weights.length === question.weights.length) {
next.weights = previous.weights.slice();
}
return next;
});
const mode = normalizeDistributionMode(
(savedConfig && savedConfig.distributionMode) ||
State.config.distributionMode
);
return {
targetCount: Math.max(1, Number(previousTargetCount || (savedConfig && savedConfig.targetCount) || 1) || 1),
distributionMode: mode,
questions: mergedQuestions
};
}
function findMatchingQuestion(list, question) {
if (!Array.isArray(list)) {
return null;
}
return list.find((item) => String(item.id) === String(question.id) && item.type === question.type) || null;
}
function loadConfig() {
try {
return JSON.parse(localStorage.getItem(State.surveyKey) || 'null');
} catch (error) {
return null;
}
}
function saveConfig() {
localStorage.setItem(State.surveyKey, JSON.stringify(buildConfigPayload()));
}
function buildConfigPayload() {
return {
targetCount: State.config.targetCount,
distributionMode: normalizeDistributionMode(State.config.distributionMode),
questions: State.config.questions
};
}
function cloneQuestionForExport(question) {
const next = Object.assign({}, question);
if (Array.isArray(next.optionLabels)) {
next.optionLabels = next.optionLabels.slice();
}
if (Array.isArray(next.weights)) {
next.weights = next.weights.slice();
}
if (Array.isArray(next.content)) {
next.content = next.content.slice();
}
if (Array.isArray(next.relations)) {
next.relations = next.relations.map((relation) => Object.assign({}, relation));
}
return next;
}
function buildConfigExportSnapshot() {
syncStateFromDom();
return {
version: CONFIG_EXPORT_VERSION,
exportedAt: new Date().toISOString(),
surveyKey: State.surveyKey,
targetCount: State.config.targetCount,
distributionMode: normalizeDistributionMode(State.config.distributionMode),
questions: State.config.questions.map((question) => cloneQuestionForExport(question))
};
}
function downloadJsonFile(filename, payload) {
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json;charset=utf-8' });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = filename;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
URL.revokeObjectURL(url);
}
function exportCurrentConfig() {
syncStateFromDom();
if (!State.config.questions.length) {
showToast('当前没有可导出的题目配置。');
return;
}
const payload = buildConfigExportSnapshot();
const stamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
downloadJsonFile(`wjx-config-${stamp}.json`, payload);
showToast('配置已导出为 JSON 文件。');
}
function normalizeImportedConfig(raw) {
if (!raw || typeof raw !== 'object') {
return null;
}
const questions = Array.isArray(raw.questions) ? raw.questions : null;
if (!questions || !questions.length) {
return null;
}
return {
targetCount: Math.max(1, Number(raw.targetCount) || 1),
distributionMode: normalizeDistributionMode(raw.distributionMode),
questions: questions.filter((question) => question && question.id != null && question.type)
};
}
function importConfigFromFile(file) {
if (!file) {
return;
}
const reader = new FileReader();
reader.onload = () => {
try {
const raw = JSON.parse(String(reader.result || ''));
applyImportedConfig(raw);
} catch (error) {
showToast('配置文件解析失败,请检查 JSON 格式。');
}
};
reader.onerror = () => {
showToast('读取配置文件失败。');
};
reader.readAsText(file, 'utf-8');
}
function applyImportedConfig(raw) {
const imported = normalizeImportedConfig(raw);
if (!imported) {
showToast('配置文件格式无效,需包含 questions 数组。');
return;
}
const scannedQuestions = scanQuestions();
if (!scannedQuestions.length) {
showToast('当前页面未识别到题目,请先打开问卷题目页再导入。');
return;
}
State.config = mergeConfig(imported, [], imported.targetCount, scannedQuestions);
State.config.distributionMode = imported.distributionMode;
saveConfig();
renderStats();
renderQuestionList();
updateCountModeHints();
const targetInput = document.getElementById('wjx-target-count');
if (targetInput) {
targetInput.value = String(State.config.targetCount);
}
updateDistributionModeUI();
updateAllRelationVisibility();
const matched = imported.questions.filter((question) => findMatchingQuestion(scannedQuestions, question)).length;
showToast(`已导入配置,匹配 ${matched}/${scannedQuestions.length} 道题。`);
}
function normalizeDistributionMode(mode) {
return mode === 'count' ? 'count' : 'ratio';
}
function isCountModeFromConfig(config) {
return normalizeDistributionMode(config && config.distributionMode) === 'count';
}
function isCountMode() {
return normalizeDistributionMode(State.config.distributionMode) === 'count';
}
function supportsCountDistribution(type) {
return ['radio', 'dropdown', 'rating', 'checkbox', 'matrix'].includes(type);
}
function setDistributionMode(mode) {
syncStateFromDom();
State.config.distributionMode = normalizeDistributionMode(mode);
saveConfig();
updateDistributionModeUI();
renderQuestionList();
updateCountModeHints();
showToast(mode === 'count' ? '已切换为数量模式' : '已切换为比例模式');
}
function updateDistributionModeUI() {
const mode = normalizeDistributionMode(State.config.distributionMode);
State.config.distributionMode = mode;
const ratioBtn = document.getElementById('wjx-mode-ratio');
const countBtn = document.getElementById('wjx-mode-count');
const hint = document.getElementById('wjx-mode-hint');
if (ratioBtn) {
ratioBtn.classList.toggle('is-active', mode === 'ratio');
}
if (countBtn) {
countBtn.classList.toggle('is-active', mode === 'count');
}
if (hint) {
hint.textContent = mode === 'count'
? '数量模式:各选项数值为该选项被选中的次数。无关联题目次数合计应等于设计份数;有关联题目次数合计应等于其父题对应选项的次数。'
: '比例模式:各选项数值为权重比例。';
}
}
function getWeightInputSuffix() {
return isCountMode() ? '次数' : '比例';
}
function getRandomPresetLabel() {
return isCountMode() ? '均衡次数' : '随机比例';
}
function distributeCountsEvenly(optionCount, total) {
const count = Math.max(1, optionCount || 1);
const target = Math.max(0, Math.round(Number(total) || 0));
const base = Math.floor(target / count);
const remainder = target % count;
return Array.from({ length: count }, (_, index) => base + (index < remainder ? 1 : 0));
}
function buildRandomWeightValues(optionCount, question) {
if (isCountMode()) {
const targetQuestion = question || { relations: [] };
return distributeCountsEvenly(optionCount, getExpectedCountTotalForQuestion(targetQuestion));
}
return Array.from({ length: optionCount }, () => Math.floor(Math.random() * 100) + 1);
}
function findConfigQuestionByTopicId(topicId) {
return State.config.questions.find((item) => String(item.id) === String(topicId)) || null;
}
function getRelationOptionCount(relation) {
const optionIndex = Number(relation.optionIndex) - 1;
if (!Number.isFinite(optionIndex) || optionIndex < 0) {
return null;
}
const parentCard = document.querySelector(
`#${PANEL_ID} .wjx-card[data-qid="${String(relation.topicId).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`
);
if (parentCard) {
const inputs = parentCard.querySelectorAll('[data-role="weight-input"]');
if (optionIndex < inputs.length) {
return Math.max(0, Math.round(Number(inputs[optionIndex].value) || 0));
}
}
const parent = findConfigQuestionByTopicId(relation.topicId);
if (!parent || !Array.isArray(parent.weights)) {
return null;
}
if (optionIndex >= parent.weights.length) {
return null;
}
return Math.max(0, Math.round(Number(parent.weights[optionIndex]) || 0));
}
function getExpectedCountTotalForQuestion(question) {
const total = getCurrentTargetCount();
if (!Array.isArray(question.relations) || !question.relations.length) {
return total;
}
const relationCounts = question.relations
.map((relation) => getRelationOptionCount(relation))
.filter((value) => value !== null);
if (!relationCounts.length) {
return total;
}
return Math.min(...relationCounts);
}
function formatCountSumOnlyHint(question) {
if (question.type === 'checkbox') {
return '';
}
const expected = getExpectedCountTotalForQuestion(question);
if (question.type === 'matrix') {
return `每行次数合计应为 ${expected}`;
}
return `次数合计应为 ${expected}`;
}
function findQuestionByCard(card) {
if (!card) {
return null;
}
return State.config.questions.find((item) => {
return String(item.id) === String(card.dataset.qid) && item.type === card.dataset.type;
}) || null;
}
function getQuestionWeightInputCount(card, question) {
const inputCount = card ? card.querySelectorAll('[data-role="weight-input"]').length : 0;
const labelCount = question && Array.isArray(question.optionLabels) ? question.optionLabels.length : 0;
const weightCount = question && Array.isArray(question.weights) ? question.weights.length : 0;
return Math.max(inputCount, labelCount, weightCount, 1);
}
function syncQuestionWeightsFromCard(card) {
const question = findQuestionByCard(card);
if (!question) {
return;
}
question.weights = Array.from(card.querySelectorAll('[data-role="weight-input"]')).map((input) => {
const value = Number(input.value);
return Number.isFinite(value) && value >= 0 ? Math.round(value) : 0;
});
}
function sortCardsParentsFirst(cards) {
const relationDepth = (card, stack) => {
const visited = stack || new Set();
if (visited.has(card)) {
return 0;
}
visited.add(card);
const question = findQuestionByCard(card);
if (!question || !Array.isArray(question.relations) || !question.relations.length) {
return 0;
}
let depth = 0;
question.relations.forEach((relation) => {
const parentCard = cards.find((item) => String(item.dataset.qid) === String(relation.topicId));
if (parentCard) {
depth = Math.max(depth, relationDepth(parentCard, visited) + 1);
}
});
return depth;
};
return cards.slice().sort((left, right) => relationDepth(left) - relationDepth(right));
}
function updateCountModeHints() {
if (!isCountMode()) {
return;
}
State.config.targetCount = getCurrentTargetCount();
document.querySelectorAll(`#${PANEL_ID} [data-role="count-expected-hint"]`).forEach((element) => {
const question = findQuestionByCard(element.closest('.wjx-card'));
if (!question) {
return;
}
const text = formatCountSumOnlyHint(question);
element.textContent = text;
element.classList.toggle('wjx-hidden', !text);
});
document.querySelectorAll(`#${PANEL_ID} .wjx-card`).forEach((card) => {
const meta = card.querySelector('.wjx-card-meta');
const question = findQuestionByCard(card);
if (!meta || !question) {
return;
}
meta.textContent = buildQuestionMeta(question);
});
}
function formatCountModeExpectedHint(question) {
const expected = getExpectedCountTotalForQuestion(question);
const total = getCurrentTargetCount();
if (question.type === 'matrix') {
return `${question.rowCount || 0} 行 ${question.optionLabels.length} 列,每行次数合计应为 ${expected}`;
}
if (question.type === 'checkbox') {
if (Array.isArray(question.relations) && question.relations.length) {
return `${question.optionLabels.length} 个选项,数量为各选项在关联可见的 ${expected} 份答卷中的选中次数`;
}
return `${question.optionLabels.length} 个选项,数量为各选项在 ${total} 份答卷中的选中次数`;
}
if (Array.isArray(question.relations) && question.relations.length && expected !== total) {
return `${question.optionLabels.length} 个选项,次数合计应为 ${expected}(关联 ${formatRelationText(question.relations)})`;
}
return `${question.optionLabels.length} 个选项,次数合计应为 ${expected}`;
}
function validateCountModeBeforeStart() {
if (!isCountMode()) {
return true;
}
const total = getCurrentTargetCount();
const issues = [];
State.config.questions.forEach((question) => {
if (!supportsCountDistribution(question.type)) {
return;
}
const weights = Array.isArray(question.weights) ? question.weights : [];
const sum = weights.reduce((acc, value) => acc + Math.max(0, Math.round(Number(value) || 0)), 0);
if (question.type === 'checkbox') {
return;
}
const expected = getExpectedCountTotalForQuestion(question);
if (sum !== expected) {
const relationTip = Array.isArray(question.relations) && question.relations.length
? `(关联父题选项次数为 ${expected})`
: `(设计份数为 ${total})`;
issues.push(`Q${question.order} 选项次数合计为 ${sum},应为 ${expected}${relationTip}`);
}
});
if (!issues.length) {
return true;
}
showToast(issues[0] + (issues.length > 1 ? ' 等' : ''));
return false;
}
function loadQuotaState() {
try {
const raw = localStorage.getItem(QUOTA_STATE_KEY);
return raw ? JSON.parse(raw) : null;
} catch (error) {
return null;
}
}
function saveQuotaState(state) {
localStorage.setItem(QUOTA_STATE_KEY, JSON.stringify(state));
}
function clearQuotaState() {
localStorage.removeItem(QUOTA_STATE_KEY);
}
function initQuotaState() {
const quotas = {};
State.config.questions.forEach((question) => {
if (!supportsCountDistribution(question.type)) {
return;
}
const weights = (question.weights || []).map((value) => Math.max(0, Math.round(Number(value) || 0)));
if (question.type === 'matrix') {
const rowCount = Math.max(1, Number(question.rowCount) || 1);
quotas[String(question.id)] = {
type: 'matrix',
rows: Array.from({ length: rowCount }, () => weights.slice())
};
return;
}
quotas[String(question.id)] = {
type: 'flat',
values: weights.slice()
};
});
saveQuotaState({
surveyKey: State.surveyKey,
quotas
});
}
function ensureQuotaState() {
if (!isCountMode()) {
clearQuotaState();
return;
}
const existing = loadQuotaState();
if (existing && existing.surveyKey === State.surveyKey && existing.quotas) {
return;
}
initQuotaState();
}
function getQuotaEntry(questionId) {
const state = loadQuotaState();
if (!state || state.surveyKey !== State.surveyKey || !state.quotas) {
return null;
}
return state.quotas[String(questionId)] || null;
}
function getFlatQuotas(questionId) {
const entry = getQuotaEntry(questionId);
if (!entry || entry.type !== 'flat' || !Array.isArray(entry.values)) {
return null;
}
return entry.values;
}
function getMatrixRowQuotas(questionId, rowIndex) {
const entry = getQuotaEntry(questionId);
if (!entry || entry.type !== 'matrix' || !Array.isArray(entry.rows)) {
return null;
}
return entry.rows[rowIndex] || null;
}
function pickQuotaIndexFromValues(quotas) {
const available = quotas
.map((quota, index) => ({ index, quota: Math.max(0, Number(quota) || 0) }))
.filter((item) => item.quota > 0);
if (!available.length) {
return Math.floor(Math.random() * quotas.length);
}
const pick = available[Math.floor(Math.random() * available.length)];
return pick.index;
}
function consumeFlatQuota(questionId, optionIndex) {
const state = loadQuotaState();
const entry = state && state.quotas && state.quotas[String(questionId)];
if (!state || !entry || entry.type !== 'flat' || !Array.isArray(entry.values)) {
return;
}
if (entry.values[optionIndex] > 0) {
entry.values[optionIndex] -= 1;
saveQuotaState(state);
}
}
function consumeMatrixQuota(questionId, rowIndex, optionIndex) {
const state = loadQuotaState();
const entry = getQuotaEntry(questionId);
if (!state || !entry || entry.type !== 'matrix' || !entry.rows[rowIndex]) {
return;
}
if (entry.rows[rowIndex][optionIndex] > 0) {
entry.rows[rowIndex][optionIndex] -= 1;
saveQuotaState(state);
}
}
function pickOptionIndexForQuestion(question) {
if (!isCountMode() || !supportsCountDistribution(question.type)) {
return pickWeightedIndex(question.weights);
}
const quotas = getFlatQuotas(question.id);
if (!quotas) {
return pickWeightedIndex(question.weights);
}
return pickQuotaIndexFromValues(quotas);
}
function pickCheckboxIndicesForQuestion(question) {
if (!isCountMode()) {
return pickMultipleIndices(question.weights, question.minSelections, question.maxSelections);
}
const quotas = getFlatQuotas(question.id);
if (!quotas) {
return pickMultipleIndices(question.weights, question.minSelections, question.maxSelections);
}
const totalOptions = quotas.length;
const available = quotas
.map((quota, index) => ({ index, quota: Math.max(0, Number(quota) || 0) }))
.filter((item) => item.quota > 0);
if (!available.length) {
return pickMultipleIndices(question.weights, question.minSelections, question.maxSelections);
}
const minCount = Math.max(0, Math.min(Number(question.minSelections) || 0, totalOptions));
const defaultMax = Math.min(totalOptions, Math.max(minCount || 1, Math.min(3, totalOptions)));
const maxCount = Math.max(minCount, Math.min(Number(question.maxSelections) || defaultMax, totalOptions));
const desiredCount = minCount >= maxCount
? Math.max(1, minCount || 1)
: randomInt(Math.max(1, minCount || 1), maxCount);
const count = Math.min(desiredCount, available.length);
const pool = available.slice();
const results = [];
while (results.length < count && pool.length > 0) {
const pick = pool[Math.floor(Math.random() * pool.length)];
results.push(pick.index);
pool.splice(pool.indexOf(pick), 1);
}
return results;
}
function pickMatrixRowIndex(question, rowIndex) {
if (!isCountMode()) {
return pickWeightedIndex(question.weights);
}
const quotas = getMatrixRowQuotas(question.id, rowIndex);
if (!quotas) {
return pickWeightedIndex(question.weights);
}
return pickQuotaIndexFromValues(quotas);
}
function renderStats() {
document.getElementById('wjx-stat-total').textContent = String(State.config.questions.length);
document.getElementById('wjx-stat-types').textContent = String(new Set(State.config.questions.map((item) => item.type)).size);
}
function renderQuestionList() {
const container = document.getElementById('wjx-question-list');
if (!container) {
return;
}
if (!State.config.questions.length) {
container.innerHTML = `
未识别到题目
`;
return;
}
container.innerHTML = State.config.questions.map((question) => renderQuestionCard(question)).join('');
}
function renderQuestionCard(question) {
const title = escapeHtml(question.title || `题目 ${question.order}`);
const typeLabel = TYPE_LABELS[question.type] || TYPE_LABELS.unknown;
const meta = buildQuestionMeta(question);
const editor = question.type === 'text'
? `
`
: question.type === 'slide'
? `
`
: question.type === 'unknown'
? ''
: `
${question.optionLabels.map((label, index) => `
${escapeHtml(label)}(${getWeightInputSuffix()})
`).join('')}
`;
const tools = question.type === 'text'
? ``
: question.type === 'slide'
? ``
: question.type === 'unknown'
? ''
: `
`;
const countHint = isCountMode() && supportsCountDistribution(question.type) && question.type !== 'checkbox'
? `${escapeHtml(formatCountSumOnlyHint(question))}
`
: '';
return `
Q${question.order}. ${title}
${typeLabel}
${meta}
${editor}
${countHint}
${tools}
`;
}
function buildQuestionMeta(question) {
const relationText = formatRelationText(question.relations);
if (relationText) {
return `关联:${relationText}`;
}
if (question.type === 'matrix') {
if (isCountMode() && supportsCountDistribution(question.type)) {
return formatCountModeExpectedHint(question);
}
return `${question.rowCount || 0} 行 ${question.optionLabels.length} 列`;
}
if (question.type === 'text') {
return '填空题';
}
if (question.type === 'slide') {
return `分值范围 ${clampScore(question.minScore, 1)} - ${clampScore(question.maxScore, 100)}`;
}
if (question.type === 'unknown') {
return '未知题型';
}
if (question.type === 'checkbox' && question.minSelections) {
return `${question.optionLabels.length} 个选项,最少选 ${question.minSelections} 项`;
}
if (isCountMode() && supportsCountDistribution(question.type)) {
return formatCountModeExpectedHint(question);
}
return `${question.optionLabels.length} 个选项`;
}
function syncStateFromDom() {
State.config.targetCount = getCurrentTargetCount();
State.config.questions = collectCurrentQuestionsFromDom();
}
function getCurrentTargetCount() {
const input = document.getElementById('wjx-target-count');
return Math.max(1, Number(input && input.value) || 1);
}
function collectCurrentQuestionsFromDom() {
return Array.from(document.querySelectorAll(`#${PANEL_ID} .wjx-card`)).map((card, index) => {
const id = card.dataset.qid;
const type = card.dataset.type;
const original = State.config.questions.find((item) => String(item.id) === String(id) && item.type === type) || {};
const next = {
id,
order: original.order || index + 1,
title: original.title || '',
type,
optionLabels: Array.isArray(original.optionLabels) ? original.optionLabels.slice() : [],
relations: Array.isArray(original.relations) ? original.relations.map((relation) => Object.assign({}, relation)) : [],
rowCount: original.rowCount || 0,
minSelections: original.minSelections || 0,
maxSelections: original.maxSelections || 0,
minScore: clampScore(original.minScore, 1),
maxScore: clampScore(original.maxScore, 100)
};
if (type === 'text') {
const area = card.querySelector('[data-role="content-editor"]');
next.content = area
? area.value.split('\n').map((line) => line.trim()).filter(Boolean)
: DEFAULT_TEXT_LIBRARY.slice();
} else if (type === 'slide') {
next.minScore = clampScore(card.querySelector('[data-role="slide-min"]') && card.querySelector('[data-role="slide-min"]').value, original.minScore || 1);
next.maxScore = clampScore(card.querySelector('[data-role="slide-max"]') && card.querySelector('[data-role="slide-max"]').value, original.maxScore || 100);
if (next.maxScore < next.minScore) {
next.maxScore = next.minScore;
}
} else if (type !== 'unknown') {
next.weights = Array.from(card.querySelectorAll('[data-role="weight-input"]')).map((input) => {
const value = Number(input.value);
return Number.isFinite(value) && value >= 0 ? value : 0;
});
}
return next;
});
}
function applyPreset(card, preset) {
if (!card) {
return;
}
if (preset === 'text-default') {
const area = card.querySelector('[data-role="content-editor"]');
if (area) {
area.value = DEFAULT_TEXT_LIBRARY.join('\n');
showToast('已填入默认词库。');
}
return;
}
if (preset === 'slide-default') {
const minInput = card.querySelector('[data-role="slide-min"]');
const maxInput = card.querySelector('[data-role="slide-max"]');
if (minInput && maxInput) {
const minScore = randomInt(20, 70);
const maxScore = randomInt(minScore, Math.min(100, minScore + 30));
minInput.value = String(minScore);
maxInput.value = String(maxScore);
showToast('已生成随机分值区间。');
}
return;
}
const inputs = Array.from(card.querySelectorAll('[data-role="weight-input"]'));
if (!inputs.length) {
return;
}
syncStateFromDom();
const original = findQuestionByCard(card) || { relations: [] };
const values = buildRandomWeightValues(getQuestionWeightInputCount(card, original), original);
inputs.forEach((input, index) => {
input.value = String(values[index] || 0);
});
syncQuestionWeightsFromCard(card);
updateCountModeHints();
showToast(isCountMode() ? '已按关联次数均衡分配。' : '已生成随机比例。');
}
function randomizeAllQuestions(options) {
const settings = Object.assign({ silent: false }, options);
syncStateFromDom();
const cards = sortCardsParentsFirst(Array.from(document.querySelectorAll(`#${PANEL_ID} .wjx-card`)));
cards.forEach((card) => {
const type = card.dataset.type;
if (type === 'text') {
const area = card.querySelector('[data-role="content-editor"]');
if (area) {
area.value = DEFAULT_TEXT_LIBRARY.join('\n');
}
return;
}
if (type === 'slide') {
const minInput = card.querySelector('[data-role="slide-min"]');
const maxInput = card.querySelector('[data-role="slide-max"]');
if (minInput && maxInput) {
const minScore = randomInt(20, 70);
const maxScore = randomInt(minScore, Math.min(100, minScore + 30));
minInput.value = String(minScore);
maxInput.value = String(maxScore);
}
return;
}
if (type === 'unknown') {
return;
}
const original = findQuestionByCard(card) || { relations: [] };
const values = buildRandomWeightValues(getQuestionWeightInputCount(card, original), original);
card.querySelectorAll('[data-role="weight-input"]').forEach((input, index) => {
input.value = String(values[index] || 0);
});
syncQuestionWeightsFromCard(card);
});
syncStateFromDom();
saveConfig();
updateCountModeHints();
if (!settings.silent) {
showToast(isCountMode() ? '已按各题关联次数均衡分配。' : '已随机全部题目。');
}
}
function scanQuestions() {
return findQuestionNodes().map((node, index) => buildQuestionConfig(node, index + 1)).filter(Boolean);
}
function findQuestionNodes() {
const selectors = [
'.field.ui-field-contain',
'.div_question',
'.div_table_radio_question',
'.ui-field-contain'
];
const nodes = Array.from(document.querySelectorAll(selectors.join(',')));
return nodes.filter((node, index, list) => {
if (!(node instanceof HTMLElement)) {
return false;
}
if (!extractTitle(node)) {
return false;
}
return !list.some((other, otherIndex) => otherIndex !== index && other.contains(node));
});
}
function parseRelations(node) {
const raw = String(node.getAttribute('relation') || '').trim();
if (!raw) {
return [];
}
return raw.split(';').map((part) => {
const pieces = part.split(',').map((item) => item.trim()).filter(Boolean);
if (pieces.length < 2) {
return null;
}
const topicId = pieces[0].replace(/\D/g, '') || pieces[0];
const optionIndex = Number(pieces[1]);
if (!topicId || !Number.isFinite(optionIndex) || optionIndex < 1) {
return null;
}
return { topicId: String(topicId), optionIndex: Math.floor(optionIndex) };
}).filter(Boolean);
}
function findQuestionNodeById(topicId) {
return findQuestionNodes().find((node) => String(extractQuestionId(node, 0)) === String(topicId)) || null;
}
function getRelationOptionLabel(topicId, optionIndex) {
const parentNode = findQuestionNodeById(topicId);
if (!parentNode) {
return `选项${optionIndex}`;
}
const parentType = detectType(parentNode);
const detail = extractDetail(parentNode, parentType);
return detail.optionLabels[optionIndex - 1] || `选项${optionIndex}`;
}
function formatRelationText(relations) {
if (!Array.isArray(relations) || !relations.length) {
return '';
}
return relations.map((relation) => {
const label = getRelationOptionLabel(relation.topicId, relation.optionIndex);
return `第${relation.topicId}题·${label}`;
}).join(';');
}
function isRelationSatisfied(relations) {
if (!Array.isArray(relations) || !relations.length) {
return true;
}
return relations.every((relation) => isOptionSelectedByIndex(relation.topicId, relation.optionIndex));
}
function isOptionSelectedByIndex(topicId, optionIndex) {
const parentNode = findQuestionNodeById(topicId);
if (!parentNode) {
return false;
}
return isOptionSelected(parentNode, optionIndex);
}
function isOptionSelected(node, optionIndex) {
const type = detectType(node);
if (type === 'dropdown') {
const select = node.querySelector('select');
if (!select || select.selectedIndex < 0) {
return false;
}
const selected = select.options[select.selectedIndex];
if (!selected) {
return false;
}
if (Number(selected.value) === optionIndex) {
return true;
}
let realIndex = select.selectedIndex;
if (select.options[0] && /请选择/.test(cleanText(select.options[0].textContent))) {
realIndex = select.selectedIndex;
}
return realIndex === optionIndex;
}
const valueInput = node.querySelector(`input[type="radio"][value="${optionIndex}"], input[type="checkbox"][value="${optionIndex}"]`);
if (valueInput) {
return !!valueInput.checked;
}
const choiceType = type === 'checkbox' ? 'checkbox' : 'radio';
const options = getChoiceElements(node, type === 'checkbox' ? 'checkbox' : (type === 'rating' ? 'rating' : 'radio'));
const option = options[optionIndex - 1];
return !!(option && isChoiceSelected(option, choiceType));
}
function updateAllRelationVisibility() {
findQuestionNodes().forEach((node) => {
const relations = parseRelations(node);
if (!relations.length) {
return;
}
node.style.display = isRelationSatisfied(relations) ? '' : 'none';
});
}
function buildQuestionConfig(node, order) {
const title = extractTitle(node);
if (!title) {
return null;
}
const type = detectType(node);
const base = {
id: extractQuestionId(node, order),
order,
title,
type,
rawType: String(node.getAttribute('type') || ''),
relations: parseRelations(node),
optionLabels: [],
rowCount: 0,
minSelections: 0,
maxSelections: 0,
minScore: 1,
maxScore: 100
};
if (type === 'text') {
return Object.assign(base, { content: DEFAULT_TEXT_LIBRARY.slice() });
}
if (type === 'slide') {
return Object.assign(base, {
minScore: 1,
maxScore: 100
});
}
if (type === 'unknown') {
return base;
}
const detail = extractDetail(node, type);
const limits = extractSelectionLimits(node, type, detail.optionLabels.length);
return Object.assign(base, {
optionLabels: detail.optionLabels,
rowCount: detail.rowCount || 0,
minSelections: limits.minSelections,
maxSelections: limits.maxSelections,
weights: Array.from({ length: detail.optionLabels.length }, () => 1)
});
}
function extractQuestionId(node, fallbackOrder) {
const raw = node.id || node.getAttribute('topic') || '';
const match = raw.match(/(\d+)/);
return match ? match[1] : String(fallbackOrder);
}
function extractTitle(node) {
const titleNode = node.querySelector('.div_title_question, .ui-controlgroup-label, .field-label, .legend, .title');
return cleanText(titleNode ? titleNode.textContent : '').replace(/^\d+\s*[\.、)]\s*/, '');
}
function detectType(node) {
const wjxType = String(node.getAttribute('type') || '');
if (wjxType === '6') {
return 'matrix';
}
if (wjxType === '5') {
return 'rating';
}
if (wjxType === '4') {
return 'checkbox';
}
if (wjxType === '3') {
return 'radio';
}
if (wjxType === '7') {
return 'dropdown';
}
if (wjxType === '11') {
return 'sorting';
}
if (wjxType === '1' || wjxType === '2') {
return 'text';
}
if (wjxType === '8') {
return 'slide';
}
if (node.querySelector('.div_table_radio_question, .div_table_clear_top')) {
return 'matrix';
}
const table = node.querySelector('table');
if (table && table.querySelector('input[type="radio"], input[type="checkbox"], .ui-radio, .ui-checkbox, a[dval], .rate-off, .rate-on')) {
return 'matrix';
}
if (node.querySelector('.city-container, .divProvince, .divCity')) {
return 'location';
}
if (node.querySelector('.reorder-list, .ui-sortable')) {
return 'sorting';
}
if (node.querySelector('.scale-div, .rating-star, .onscore, .starlevel')) {
return 'rating';
}
if (node.querySelector('.ui-checkbox, .jqCheckbox, input[type="checkbox"]')) {
return 'checkbox';
}
if (node.querySelector('.ui-radio, .jqRadio, input[type="radio"]')) {
return 'radio';
}
if (node.querySelector('select')) {
return 'dropdown';
}
if (node.querySelector('textarea, input[type="text"]:not([style*="display:none"]), input[type="tel"], input[type="email"], input[type="number"]')) {
return 'text';
}
return 'unknown';
}
function extractSelectionLimits(node, type, optionCount) {
const rawMin = Number(node.getAttribute('minvalue') || 0);
const rawMax = Number(node.getAttribute('maxvalue') || 0);
let minSelections = Number.isFinite(rawMin) && rawMin > 0 ? rawMin : 0;
let maxSelections = Number.isFinite(rawMax) && rawMax > 0 ? rawMax : 0;
if (!minSelections && (type === 'checkbox' || type === 'radio') && node.getAttribute('req') === '1') {
minSelections = 1;
}
minSelections = Math.min(Math.max(minSelections, 0), optionCount || 0);
maxSelections = Math.min(Math.max(maxSelections, 0), optionCount || 0);
if (maxSelections && maxSelections < minSelections) {
maxSelections = minSelections;
}
return { minSelections, maxSelections };
}
function extractDetail(node, type) {
if (type === 'dropdown') {
const select = node.querySelector('select');
const optionLabels = Array.from(select ? select.options : [])
.map((item) => trimOptionPrefix(cleanText(item.textContent)))
.filter((text) => text && !/^请选择/.test(text));
return {
optionLabels: optionLabels.length ? optionLabels : ['选项1', '选项2'],
rowCount: 0
};
}
if (type === 'matrix') {
return extractMatrixDetail(node);
}
const optionLabels = extractChoiceLabels(node);
return {
optionLabels: optionLabels.length ? optionLabels : ['选项1', '选项2'],
rowCount: 0
};
}
function extractChoiceLabels(node) {
const selectors = [
'.ulradiocheck li',
'.ui-controlgroup .ui-radio',
'.ui-controlgroup .ui-checkbox',
'.label',
'.option-item',
'.wjx-options li'
];
const rawTexts = [];
selectors.forEach((selector) => {
node.querySelectorAll(selector).forEach((item) => {
const text = trimOptionPrefix(cleanText(item.textContent));
if (text) {
rawTexts.push(text);
}
});
});
return dedupeTexts(rawTexts).slice(0, 20);
}
function extractMatrixDetail(node) {
const table = node.querySelector('table');
if (!table) {
return { optionLabels: ['选项1', '选项2'], rowCount: 0 };
}
let headerCells = [];
const theadCells = table.querySelectorAll('thead tr:first-child th, thead tr:first-child td');
if (theadCells.length > 1) {
headerCells = Array.from(theadCells).slice(1);
} else {
const firstRow = table.querySelector('tbody tr, tr');
const bodyCells = firstRow ? firstRow.querySelectorAll('td, th') : [];
if (bodyCells.length > 1) {
headerCells = Array.from(bodyCells).slice(1);
}
}
const optionLabels = dedupeTexts(headerCells.map((cell) => trimOptionPrefix(cleanText(cell.textContent)))).filter(Boolean);
const rowCount = table.querySelectorAll('tbody tr').length || Math.max(0, table.querySelectorAll('tr').length - 1);
return {
optionLabels: optionLabels.length ? optionLabels : ['选项1', '选项2'],
rowCount
};
}
function dedupeTexts(list) {
const result = [];
const seen = new Set();
list.forEach((item) => {
const normalized = cleanText(item);
if (!normalized || seen.has(normalized)) {
return;
}
seen.add(normalized);
result.push(normalized);
});
return result;
}
function trimOptionPrefix(text) {
return String(text || '').replace(/^[A-ZA-Za-za-z0-90-9]+[\.\、\)]\s*/, '').trim();
}
function cleanText(text) {
return String(text || '').replace(/\s+/g, ' ').trim();
}
function clampScore(value, fallback) {
const score = Number(value);
if (!Number.isFinite(score)) {
return Number(fallback);
}
return Math.max(0, Math.min(100, Math.round(score)));
}
function escapeHtml(value) {
return String(value || '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
let progressStyleInjected = false;
let automationRedirectTimer = null;
function injectProgressStyles() {
if (progressStyleInjected || document.querySelector('style[data-wjx-progress]')) {
progressStyleInjected = true;
return;
}
const style = document.createElement('style');
style.setAttribute('data-wjx-progress', '1');
style.textContent = `
:root {
--wjx-primary: #0064ff;
--wjx-primary-deep: #0052d9;
--wjx-text: #1f2d3d;
--wjx-subtle: #5f6f82;
}
.wjx-progress-modal {
position: fixed;
top: 16px;
left: 50%;
z-index: 2147483647;
min-width: 240px;
padding: 14px 18px;
border-radius: 10px;
border: 1px solid #b9d2ff;
background: rgba(255, 255, 255, 0.98);
color: #1f2d3d;
box-shadow: 0 12px 32px rgba(0, 82, 217, 0.18);
font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
text-align: center;
opacity: 0;
transform: translate(-50%, -8px);
transition: opacity 0.2s ease, transform 0.2s ease;
pointer-events: none;
}
.wjx-progress-modal.is-visible {
opacity: 1;
transform: translate(-50%, 0);
pointer-events: auto;
}
.wjx-progress-title {
margin: 0 0 8px;
font-size: 13px;
font-weight: 700;
color: #0052d9;
}
.wjx-progress-count {
margin: 0 0 10px;
font-size: 16px;
font-weight: 800;
}
.wjx-progress-bar-wrap {
height: 8px;
margin-bottom: 8px;
border-radius: 999px;
background: #e8f1ff;
overflow: hidden;
}
.wjx-progress-bar {
height: 100%;
width: 0;
border-radius: 999px;
background: linear-gradient(90deg, #0064ff, #3d8bff);
transition: width 0.25s ease;
}
.wjx-progress-percent {
margin: 0;
font-size: 12px;
font-weight: 700;
color: #5f6f82;
}
.wjx-progress-stop {
margin-top: 12px;
min-height: 34px;
padding: 0 16px;
border-radius: 6px;
border: 1px solid #ffb4b4;
background: #fff5f5;
color: #d4380d;
font-size: 13px;
font-weight: 700;
cursor: pointer;
}
.wjx-progress-stop:hover {
background: #ffece8;
}
`;
document.head.appendChild(style);
progressStyleInjected = true;
}
function hidePanelForAutomation() {
const panel = document.getElementById(PANEL_ID);
if (!panel) {
return;
}
try {
sessionStorage.setItem(
PANEL_COLLAPSED_KEY,
panel.classList.contains('is-collapsed') ? '1' : '0'
);
} catch (error) {}
panel.classList.add('is-automation-hidden');
}
function showPanelAfterAutomation() {
const panel = document.getElementById(PANEL_ID);
if (!panel) {
return;
}
panel.classList.remove('is-automation-hidden');
let collapsed = false;
try {
const saved = sessionStorage.getItem(PANEL_COLLAPSED_KEY);
collapsed = saved === '1';
sessionStorage.removeItem(PANEL_COLLAPSED_KEY);
} catch (error) {}
const body = panel.querySelector('#wjx-panel-body');
const button = panel.querySelector('#wjx-toggle-panel');
panel.classList.toggle('is-collapsed', collapsed);
if (body) {
body.classList.toggle('wjx-hidden', collapsed);
}
if (button) {
button.textContent = collapsed ? '展开' : '收起';
}
}
function clearAutomationStorage() {
localStorage.removeItem(REMAINING_COUNT_KEY);
localStorage.removeItem(TOTAL_COUNT_KEY);
localStorage.removeItem(SURVEY_URL_KEY);
clearQuotaState();
}
function endAutomationSession(message) {
clearTimeout(automationRedirectTimer);
automationRedirectTimer = null;
clearAutomationStorage();
hideAutomationProgress();
showPanelAfterAutomation();
if (message) {
showToast(message);
}
}
function beginAutomationSession() {
hidePanelForAutomation();
showAutomationProgress();
}
function stopAutomation() {
endAutomationSession('已停止自动化提交');
}
function isAutomationActive() {
return Number(localStorage.getItem(REMAINING_COUNT_KEY) || 0) > 0;
}
function ensureTotalCountForAutomation() {
const remaining = Number(localStorage.getItem(REMAINING_COUNT_KEY) || 0);
const total = Number(localStorage.getItem(TOTAL_COUNT_KEY) || 0);
if (remaining > 0 && total <= 0) {
localStorage.setItem(TOTAL_COUNT_KEY, String(remaining));
}
}
function getAutomationProgress() {
const remaining = Number(localStorage.getItem(REMAINING_COUNT_KEY) || 0);
let total = Number(localStorage.getItem(TOTAL_COUNT_KEY) || 0);
if (total <= 0) {
total = remaining;
}
if (remaining <= 0 || total <= 0) {
return null;
}
const completed = Math.max(0, total - remaining);
const current = Math.min(total, completed + 1);
const percent = Math.min(100, Math.round((completed / total) * 100));
return { current, total, percent };
}
function ensureProgressModal() {
injectProgressStyles();
let modal = document.querySelector('.wjx-progress-modal');
if (!modal) {
modal = document.createElement('div');
modal.className = 'wjx-progress-modal';
modal.innerHTML = `
自动化提交中
第1份/共1份
进度 0%
`;
document.body.appendChild(modal);
modal.querySelector('#wjx-progress-stop').addEventListener('click', (event) => {
event.stopPropagation();
stopAutomation();
});
}
return modal;
}
function updateAutomationProgressUI(progress) {
const countEl = document.getElementById('wjx-progress-count');
const percentEl = document.getElementById('wjx-progress-percent');
const barEl = document.getElementById('wjx-progress-bar');
if (countEl) {
countEl.textContent = `第${progress.current}份/共${progress.total}份`;
}
if (percentEl) {
percentEl.textContent = `进度 ${progress.percent}%`;
}
if (barEl) {
barEl.style.width = `${progress.percent}%`;
}
}
function showAutomationProgress() {
const progress = getAutomationProgress();
if (!progress) {
return;
}
const modal = ensureProgressModal();
updateAutomationProgressUI(progress);
modal.classList.add('is-visible');
}
function updateAutomationProgress() {
const progress = getAutomationProgress();
if (!progress) {
return;
}
const modal = document.querySelector('.wjx-progress-modal') || ensureProgressModal();
updateAutomationProgressUI(progress);
modal.classList.add('is-visible');
}
function hideAutomationProgress() {
const modal = document.querySelector('.wjx-progress-modal');
if (modal) {
modal.classList.remove('is-visible');
}
}
let toastTimer = null;
function showToast(message, options) {
const settings = Object.assign({ html: false, duration: 2200, actionable: false, href: '' }, options);
let toast = document.querySelector('.wjx-toast');
if (!toast) {
toast = document.createElement('div');
toast.className = 'wjx-toast';
document.body.appendChild(toast);
}
toast.classList.toggle('is-actionable', settings.actionable);
toast.onclick = null;
if (settings.html) {
toast.innerHTML = message;
} else {
toast.textContent = message;
}
if (settings.actionable && settings.href) {
toast.onclick = () => {
window.open(settings.href, '_blank', 'noopener,noreferrer');
};
}
toast.classList.add('is-visible');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
toast.classList.remove('is-visible');
}, settings.duration);
}
function schedulePromoToast() {
try {
const key = `${PROMO_TOAST_KEY}_${location.host}${location.pathname}`;
if (sessionStorage.getItem(key)) {
return;
}
sessionStorage.setItem(key, '1');
setTimeout(() => {
showToast('脚本已生效!如果觉得好用,可以点击这里在 scriptcat 点赞一下支持作者吗?', {
html: true,
duration: 5200,
actionable: true,
href: GREASYFORK_URL
});
}, 900);
} catch (error) {}
}
function scheduleRescan() {
setTimeout(() => {
if (document.getElementById(PANEL_ID)) {
refreshQuestions({ silent: true, preserveInputs: true });
}
}, 1500);
}
function startAutomation() {
const count = State.config.targetCount;
if (count <= 0) {
showToast('请输入有效的设计份数。');
return;
}
if (!validateCountModeBeforeStart()) {
return;
}
localStorage.setItem(REMAINING_COUNT_KEY, String(count));
localStorage.setItem(TOTAL_COUNT_KEY, String(count));
localStorage.setItem(SURVEY_URL_KEY, location.href);
if (isCountMode()) {
initQuotaState();
} else {
clearQuotaState();
}
beginAutomationSession();
executeAutoFillAndSubmit();
}
async function executeAutoFillAndSubmit() {
try {
if (!isAutomationActive()) {
return;
}
showAutomationProgress();
const started = await ensureSurveyStarted();
if (!isAutomationActive()) {
return;
}
if (!started) {
throw new Error('未识别到题目页或封面开始按钮');
}
const result = await fillAllQuestions();
if (!isAutomationActive()) {
return;
}
if (result && result.submitted) {
return;
}
// Wait a bit to look more natural
await new Promise(resolve => setTimeout(resolve, 1500));
if (!isAutomationActive()) {
return;
}
const submitted = await submitSurvey();
if (!isAutomationActive()) {
return;
}
if (!submitted) {
throw new Error('未找到提交按钮');
}
} catch (error) {
console.error('Automation error:', error);
endAutomationSession(`提交出错: ${error.message || '未知错误'}`);
}
}
async function fillAllQuestions() {
const filledQuestionIds = new Set();
for (let pageIndex = 0; pageIndex < 12; pageIndex++) {
updateAllRelationVisibility();
const visibleNodes = getVisibleQuestionNodes();
if (!visibleNodes.length) {
throw new Error('当前页面未识别到可填写题目');
}
const pageSignature = visibleNodes.map((node) => extractQuestionId(node, 0)).join('|');
let filledOnPage = false;
for (let pass = 0; pass < 8; pass++) {
updateAllRelationVisibility();
let filledInPass = false;
for (const node of getVisibleQuestionNodes()) {
const questionId = extractQuestionId(node, 0);
if (filledQuestionIds.has(questionId)) {
continue;
}
const relations = parseRelations(node);
if (relations.length && !isRelationSatisfied(relations)) {
continue;
}
const question = State.config.questions.find((item) => String(item.id) === String(questionId));
if (!question) {
continue;
}
await fillQuestion(question, node);
filledQuestionIds.add(questionId);
updateAllRelationVisibility();
await sleep(200 + Math.random() * 300);
filledInPass = true;
filledOnPage = true;
}
if (!filledInPass) {
break;
}
}
if (!filledOnPage) {
const pendingVisible = getVisibleQuestionNodes().filter((node) => {
const questionId = extractQuestionId(node, 0);
if (filledQuestionIds.has(questionId)) {
return false;
}
const relations = parseRelations(node);
return !relations.length || isRelationSatisfied(relations);
});
if (pendingVisible.length) {
throw new Error('存在未填写的关联题目,请检查父题选项是否已选中');
}
}
const action = await advanceSurvey(pageSignature);
if (action === 'next') {
continue;
}
if (action === 'submit') {
await sleep(800 + Math.random() * 700);
const submitted = await submitSurvey();
if (!submitted) {
throw new Error('进入提交分支后仍未找到提交按钮');
}
return { submitted: true };
}
if (action === 'stalled') {
throw new Error('检测到下一页按钮,但页面未成功翻页');
}
return { submitted: false };
}
return { submitted: false };
}
async function fillQuestion(question, node) {
switch (question.type) {
case 'radio':
case 'dropdown':
case 'rating':
const index = pickOptionIndexForQuestion(question);
clickOption(node, index, question.type);
if (isCountMode()) {
consumeFlatQuota(question.id, index);
}
updateAllRelationVisibility();
break;
case 'checkbox':
const indices = pickCheckboxIndicesForQuestion(question);
indices.forEach((idx) => clickOption(node, idx, question.type));
if (isCountMode()) {
indices.forEach((idx) => consumeFlatQuota(question.id, idx));
}
updateAllRelationVisibility();
break;
case 'text':
const text = pickRandomText(question.content);
fillText(node, text);
break;
case 'slide':
fillSlide(node, question.minScore, question.maxScore);
break;
case 'matrix':
fillMatrix(node, question, question.weights);
break;
case 'location':
await fillLocation(node);
break;
case 'sorting':
await fillSorting(node);
break;
}
}
async function fillLocation(node) {
const trigger = node.querySelector('.city-container, .divProvince, .ui-select, textarea, input[type="text"]');
if (trigger) {
trigger.click();
await new Promise(resolve => setTimeout(resolve, 1000));
const clickAndPick = async (selector) => {
const btn = document.querySelector(selector);
if (btn) {
btn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
await new Promise(r => setTimeout(r, 500));
const options = document.querySelectorAll('[id$=-results] > li:not(:first-child)');
if (options.length > 0) {
const randomOpt = options[Math.floor(Math.random() * options.length)];
randomOpt.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
await new Promise(r => setTimeout(r, 500));
return true;
}
}
return false;
};
const hasProvince = await clickAndPick('.divProvince, [id^=select2-province]');
if (hasProvince) {
await clickAndPick('.divCity, [id^=select2-city]');
await clickAndPick('.divArea, [id^=select2-area]');
const saveBtn = document.querySelector('.layer_save_btn a, .ui-dialog .save_btn');
if (saveBtn) saveBtn.click();
}
}
}
async function fillSorting(node) {
const items = Array.from(node.querySelectorAll('.reorder-list > li, .ui-sortable > li, .ui-sortable-handle, .reorder-item, ul > li'))
.filter((item) => cleanText(item.textContent));
if (items.length > 0) {
const shuffled = items.sort(() => Math.random() - 0.5);
for (const item of shuffled) {
item.click();
await new Promise(r => setTimeout(r, 300));
}
}
}
function pickWeightedIndex(weights) {
const total = weights.reduce((a, b) => a + b, 0);
if (total <= 0) return Math.floor(Math.random() * weights.length);
let random = Math.random() * total;
for (let i = 0; i < weights.length; i++) {
if (random < weights[i]) return i;
random -= weights[i];
}
return weights.length - 1;
}
function pickMultipleIndices(weights, minSelections, maxSelections) {
const totalOptions = weights.length;
if (!totalOptions) {
return [];
}
const minCount = Math.max(0, Math.min(Number(minSelections) || 0, totalOptions));
const defaultMax = Math.min(totalOptions, Math.max(minCount || 1, Math.min(3, totalOptions)));
const maxCount = Math.max(minCount, Math.min(Number(maxSelections) || defaultMax, totalOptions));
const count = minCount >= maxCount ? Math.max(1, minCount || 1) : randomInt(minCount || 1, maxCount);
const pool = weights.map((weight, index) => ({
index,
weight: Math.max(1, Number(weight) || 1)
}));
const results = [];
while (results.length < count && pool.length > 0) {
const total = pool.reduce((sum, item) => sum + item.weight, 0);
let random = Math.random() * total;
let selectedIndex = 0;
for (let i = 0; i < pool.length; i++) {
random -= pool[i].weight;
if (random <= 0) {
selectedIndex = i;
break;
}
}
results.push(pool[selectedIndex].index);
pool.splice(selectedIndex, 1);
}
return results;
}
function pickRandomText(texts) {
if (!texts || texts.length === 0) return DEFAULT_TEXT_LIBRARY[Math.floor(Math.random() * DEFAULT_TEXT_LIBRARY.length)];
return texts[Math.floor(Math.random() * texts.length)];
}
function randomInt(min, max) {
return min + Math.floor(Math.random() * (max - min + 1));
}
function isNodeVisible(node) {
if (!(node instanceof HTMLElement)) {
return false;
}
const style = window.getComputedStyle(node);
return style.display !== 'none' && style.visibility !== 'hidden' && node.getClientRects().length > 0;
}
function getVisibleQuestionNodes() {
return findQuestionNodes().filter(isNodeVisible);
}
function getButtonText(button) {
if (!(button instanceof HTMLElement)) {
return '';
}
return cleanText(button.textContent || button.value || button.getAttribute('aria-label') || '');
}
function getVisibleActionButtons() {
return Array.from(document.querySelectorAll(
'#divNext, #ctlNext, #divStart, #submit_button, .submitbutton, .btn-submit, a.button, button, .button.mainBgColor, input[type="button"], input[type="submit"]'
)).filter(isNodeVisible);
}
function getCoverStartButton() {
return getVisibleActionButtons().find((button) => {
const text = getButtonText(button);
if (button.matches('#divStart')) {
return true;
}
if (button.matches('#ctlNext, #divNext') && /开始|进入|继续|下一页|下一步/.test(text)) {
return true;
}
return /开始答题|开始填写|进入问卷|继续答题|继续填写|参与答题|立即开始|马上开始|点击开始/.test(text);
}) || null;
}
function getNextPageButton() {
return getVisibleActionButtons().find((button) => {
if (button.matches('#ctlNext')) {
return false;
}
const text = getButtonText(button);
return button.id === 'divNext' || /下一页|下一步/.test(text);
}) || null;
}
function getSubmitButton() {
return getVisibleActionButtons().find((button) => {
const text = getButtonText(button);
if (button.matches('#submit_button, .submitbutton, .btn-submit')) {
return true;
}
if (button.matches('#ctlNext')) {
return !/下一页|下一步|开始答题|开始填写|进入问卷|继续答题|继续填写|立即开始|马上开始/.test(text);
}
return /提交|交卷|完成|发送|确认提交|确认/.test(text);
}) || null;
}
async function waitForNextPage(previousSignature) {
const start = Date.now();
while (Date.now() - start < 5000) {
await sleep(250);
const currentNodes = getVisibleQuestionNodes();
const currentSignature = currentNodes.map((node) => extractQuestionId(node, 0)).join('|');
if (currentSignature && currentSignature !== previousSignature) {
return true;
}
}
return false;
}
async function waitForQuestionPage() {
const start = Date.now();
while (Date.now() - start < 8000) {
await sleep(250);
if (getVisibleQuestionNodes().length > 0) {
return true;
}
}
return false;
}
async function ensureSurveyStarted() {
if (getVisibleQuestionNodes().length > 0) {
return true;
}
const startButton = getCoverStartButton();
if (!startButton) {
return false;
}
startButton.click();
return waitForQuestionPage();
}
async function advanceSurvey(previousSignature) {
const nextPageButton = getNextPageButton();
if (nextPageButton) {
nextPageButton.click();
const pageChanged = await waitForNextPage(previousSignature);
return pageChanged ? 'next' : 'stalled';
}
return getSubmitButton() ? 'submit' : null;
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function getChoiceElements(node, type) {
const controlGroup = node.querySelector('.ui-controlgroup');
if (type === 'checkbox' && controlGroup) {
const directOptions = Array.from(controlGroup.children).filter((item) => item.matches('.ui-checkbox, li, .option-item'));
if (directOptions.length) {
return directOptions;
}
}
if ((type === 'radio' || type === 'rating') && controlGroup) {
const directOptions = Array.from(controlGroup.children).filter((item) => item.matches('.ui-radio, li, .option-item'));
if (directOptions.length) {
return directOptions;
}
}
if (type === 'checkbox') {
const options = Array.from(node.querySelectorAll('.ui-controlgroup .ui-checkbox, .ulradiocheck > li, .option-item'));
return options.length ? options : Array.from(node.querySelectorAll('input[type="checkbox"]'));
}
if (type === 'radio' || type === 'rating') {
const options = Array.from(node.querySelectorAll('.ui-controlgroup .ui-radio, .ulradiocheck > li, .scale-div li, .rating-star, .onscore, .starlevel, .option-item'));
return options.length ? options : Array.from(node.querySelectorAll('input[type="radio"]'));
}
return [];
}
function isChoiceSelected(option, type) {
if (!option) {
return false;
}
const inputSelector = type === 'checkbox' ? 'input[type="checkbox"]' : 'input[type="radio"]';
const input = option.matches(inputSelector) ? option : option.querySelector(inputSelector);
if (input) {
return !!input.checked;
}
return option.classList.contains('checked') ||
option.classList.contains('active') ||
option.classList.contains('on') ||
option.querySelector('.jqchecked, .checked, .active, a.checked, a.jqchecked') !== null;
}
function clickChoiceElement(option, type) {
if (!option) {
return;
}
const targets = [
option,
option.querySelector('.label'),
option.querySelector('.jqcheck, .jqradio, a.jqcheck, a.jqradio'),
option.querySelector('label'),
option.querySelector('input')
].filter(Boolean);
for (const target of targets) {
['mousedown', 'mouseup'].forEach((eventName) => {
target.dispatchEvent(new MouseEvent(eventName, { bubbles: true, cancelable: true }));
});
if (typeof target.click === 'function') {
target.click();
}
if (isChoiceSelected(option, type)) {
break;
}
}
const otherInput = option.querySelector('input.OtherText, input.OtherRadioText, .OtherText, .OtherRadioText, .ui-text input[type="text"]');
if (otherInput && !otherInput.value) {
otherInput.value = '其他';
otherInput.dispatchEvent(new Event('input', { bubbles: true }));
otherInput.dispatchEvent(new Event('change', { bubbles: true }));
}
}
function clickOption(node, index, type) {
if (type === 'dropdown') {
const select = node.querySelector('select');
if (select && select.options.length > index) {
let realIndex = index;
// If the first option is "Please select", we skip it
if (select.options[0].text.includes('请选择')) {
realIndex += 1;
}
if (select.options[realIndex]) {
select.selectedIndex = realIndex;
select.dispatchEvent(new Event('change', { bubbles: true }));
}
}
return;
}
const options = getChoiceElements(node, type);
if (options[index]) {
const option = options[index];
clickChoiceElement(option, type);
if (!isChoiceSelected(option, type)) {
const fallback = option.querySelector(type === 'checkbox' ? 'input[type="checkbox"]' : 'input[type="radio"]');
if (fallback && typeof fallback.click === 'function') {
fallback.click();
}
}
}
}
function fillText(node, text) {
const inputs = Array.from(node.querySelectorAll('textarea, input[type="text"], input[type="tel"], input[type="email"], input[type="number"]'))
.filter((input) => input.offsetParent !== null && !input.classList.contains('OtherText') && !input.classList.contains('OtherRadioText'));
inputs.forEach((input) => {
input.value = text;
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
});
}
function fillSlide(node, minScore, maxScore) {
const lower = clampScore(minScore, 1);
const upper = Math.max(lower, clampScore(maxScore, 100));
const value = randomInt(lower, upper);
const input = node.querySelector('input[type="range"], input[type="hidden"], input[type="number"], input[type="text"][id^="q"], input[name^="q"]');
if (!input) {
return;
}
input.value = String(value);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
const visibleDisplay = node.querySelector('.slider-value, .scale-value, .slider_num, .slider-value-text');
if (visibleDisplay) {
visibleDisplay.textContent = String(value);
}
}
function getMatrixRowOptions(row) {
const directControls = Array.from(row.querySelectorAll('.ui-radio, .ui-checkbox, .jqRadio, .jqCheckbox'));
if (directControls.length) {
return directControls;
}
const rateAnchors = Array.from(row.querySelectorAll('a[dval], .rate-off, .rate-on, .rate-offlarge, .rate-onlarge'));
if (rateAnchors.length) {
return rateAnchors;
}
const cells = Array.from(row.querySelectorAll('td, th')).slice(1);
return cells.filter((cell) =>
cell.querySelector('input[type="radio"], input[type="checkbox"], .ui-radio, .ui-checkbox, a[dval], .rate-off, .rate-on') ||
cleanText(cell.textContent)
);
}
function fillMatrix(node, question, weights) {
const rows = node.querySelectorAll('tr[rowindex], tbody tr');
let rowIndex = 0;
rows.forEach((row) => {
if (!cleanText(row.textContent)) {
return;
}
const index = pickMatrixRowIndex(question, rowIndex);
const options = getMatrixRowOptions(row);
if (options[index]) {
const option = options[index];
if (option.matches('a[dval], .rate-off, .rate-on, .rate-offlarge, .rate-onlarge')) {
option.click();
} else if (option.matches('td, th')) {
const clickable = option.querySelector('a[dval], .rate-off, .rate-on, .rate-offlarge, .rate-onlarge, .ui-radio, .ui-checkbox, input[type="radio"], input[type="checkbox"]');
if (clickable) {
clickable.click();
} else {
option.click();
}
} else {
clickChoiceElement(option, option.querySelector('input[type="checkbox"]') ? 'checkbox' : 'radio');
}
if (isCountMode()) {
consumeMatrixQuota(question.id, rowIndex, index);
}
}
rowIndex += 1;
});
}
function isElementVisible(element) {
if (!(element instanceof HTMLElement)) {
return false;
}
const style = window.getComputedStyle(element);
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) {
return false;
}
return element.getClientRects().length > 0;
}
async function waitForCondition(checker, timeoutMs, intervalMs) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
if (checker()) {
return true;
}
await sleep(intervalMs);
}
return false;
}
const SECURITY_DIALOG_POLL_MS = 40;
const SECURITY_DIALOG_CONFIRM_DELAY_MIN_MS = 800;
const SECURITY_DIALOG_CONFIRM_DELAY_MAX_MS = 2000;
const SECURITY_DIALOG_POST_CLICK_MS = 350;
function getSecurityCheckDialog() {
const layers = document.querySelectorAll('.layui-layer-dialog');
for (let i = 0; i < layers.length; i++) {
const layer = layers[i];
if (!isElementVisible(layer)) {
continue;
}
const content = layer.querySelector('.layui-layer-content');
if (content && /需要安全校验/.test(cleanText(content.textContent))) {
return layer;
}
}
return null;
}
function getSecurityCheckConfirmButton(layer) {
const root = layer || getSecurityCheckDialog();
if (!root) {
return null;
}
const directBtn = root.querySelector('.layui-layer-btn0');
if (directBtn && isElementVisible(directBtn)) {
return directBtn;
}
return Array.from(root.querySelectorAll('.layui-layer-btn a, .layui-layer-btn button')).find((btn) => {
return isElementVisible(btn) && /确认|确定|知道了/.test(getButtonText(btn));
}) || null;
}
function clickConfirmFast(element) {
if (!(element instanceof HTMLElement)) {
return false;
}
const rect = element.getBoundingClientRect();
const clientX = rect.left + rect.width / 2;
const clientY = rect.top + rect.height / 2;
dispatchMouseLikeEvent(element, 'pointerdown', clientX, clientY, { buttons: 1, pressure: 0.5 });
dispatchMouseLikeEvent(element, 'mousedown', clientX, clientY, { buttons: 1 });
dispatchMouseLikeEvent(element, 'pointerup', clientX, clientY, { buttons: 0, pressure: 0 });
dispatchMouseLikeEvent(element, 'mouseup', clientX, clientY, { buttons: 0 });
dispatchMouseLikeEvent(element, 'click', clientX, clientY, { buttons: 0 });
if (typeof element.click === 'function') {
element.click();
}
return true;
}
function getAliyunCaptchaPopup() {
const popup = document.querySelector('#aliyunCaptcha-window-popup');
return popup && isElementVisible(popup) ? popup : null;
}
function getAliyunCaptchaCheckboxIcon() {
const icon = document.querySelector('#aliyunCaptcha-checkbox-icon.aliyunCaptcha-checkbox-icon') ||
document.getElementById('aliyunCaptcha-checkbox-icon');
return icon && isElementVisible(icon) ? icon : null;
}
function randomFloat(min, max) {
return min + Math.random() * (max - min);
}
function getRandomPointInElement(element, paddingRatio) {
const ratio = Number.isFinite(paddingRatio) ? paddingRatio : 0.22;
const rect = element.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) {
return { clientX: rect.left, clientY: rect.top };
}
const padX = Math.min(rect.width * ratio, rect.width * 0.42);
const padY = Math.min(rect.height * ratio, rect.height * 0.42);
const minX = rect.left + padX;
const maxX = Math.max(minX, rect.right - padX);
const minY = rect.top + padY;
const maxY = Math.max(minY, rect.bottom - padY);
return {
clientX: randomFloat(minX, maxX),
clientY: randomFloat(minY, maxY)
};
}
function buildPointerInit(clientX, clientY, extra) {
return Object.assign({
bubbles: true,
cancelable: true,
view: window,
clientX,
clientY,
screenX: window.screenX + clientX,
screenY: window.screenY + clientY,
button: 0,
buttons: 0,
detail: 1,
pointerId: 1,
pointerType: 'mouse',
isPrimary: true,
width: 1,
height: 1,
pressure: 0.5
}, extra || {});
}
function dispatchMouseLikeEvent(target, type, clientX, clientY, extra) {
if (!(target instanceof HTMLElement)) {
return;
}
const mouseInit = buildPointerInit(clientX, clientY, extra);
if (type.startsWith('pointer') && typeof PointerEvent !== 'undefined') {
const pointerInit = Object.assign({}, mouseInit, {
pressure: type === 'pointerdown' ? 0.5 : 0
});
target.dispatchEvent(new PointerEvent(type, pointerInit));
}
if (/^mouse|click|dblclick$/.test(type)) {
target.dispatchEvent(new MouseEvent(type, mouseInit));
}
}
function getEventTargetAt(clientX, clientY, fallback) {
const hit = document.elementFromPoint(clientX, clientY);
return hit instanceof HTMLElement ? hit : fallback;
}
function collectEventTargets(hitTarget, rootElement) {
const chain = [];
let node = hitTarget;
while (node instanceof HTMLElement) {
chain.push(node);
if (node === rootElement) {
break;
}
node = node.parentElement;
}
if (rootElement instanceof HTMLElement && !chain.includes(rootElement)) {
chain.push(rootElement);
}
return chain;
}
async function simulateMouseMovePath(endX, endY, startX, startY) {
const steps = randomInt(10, 22);
for (let i = 1; i <= steps; i++) {
const progress = i / steps;
const eased = progress * progress * (3 - 2 * progress);
const clientX = startX + (endX - startX) * eased + randomFloat(-1.8, 1.8);
const clientY = startY + (endY - startY) * eased + randomFloat(-1.8, 1.8);
const target = getEventTargetAt(clientX, clientY, document.body);
dispatchMouseLikeEvent(target, 'mousemove', clientX, clientY, { buttons: 0 });
if (typeof PointerEvent !== 'undefined') {
dispatchMouseLikeEvent(target, 'pointermove', clientX, clientY, { buttons: 0 });
}
await sleep(randomInt(14, 42));
}
}
async function simulateHumanClick(element, options) {
if (!(element instanceof HTMLElement)) {
return false;
}
const settings = Object.assign({
paddingRatio: 0.22,
preDelay: [120, 380],
hoverDelay: [60, 180],
pressDelay: [55, 140],
postDelay: [80, 200],
approachDistance: [45, 110]
}, options || {});
await sleep(randomInt(settings.preDelay[0], settings.preDelay[1]));
const point = getRandomPointInElement(element, settings.paddingRatio);
const clientX = point.clientX;
const clientY = point.clientY;
const approach = settings.approachDistance;
const startX = clientX + randomFloat(-approach[1], approach[1]);
const startY = clientY + randomFloat(-approach[0], approach[0]);
await simulateMouseMovePath(clientX, clientY, startX, startY);
await sleep(randomInt(settings.hoverDelay[0], settings.hoverDelay[1]));
const hitTarget = getEventTargetAt(clientX, clientY, element);
const eventTargets = collectEventTargets(hitTarget, element);
eventTargets.forEach((target) => {
dispatchMouseLikeEvent(target, 'mouseover', clientX, clientY, { buttons: 0 });
dispatchMouseLikeEvent(target, 'mouseenter', clientX, clientY, { buttons: 0 });
if (typeof PointerEvent !== 'undefined') {
dispatchMouseLikeEvent(target, 'pointerover', clientX, clientY, { buttons: 0 });
dispatchMouseLikeEvent(target, 'pointerenter', clientX, clientY, { buttons: 0 });
}
});
await sleep(randomInt(settings.pressDelay[0], settings.pressDelay[1]));
const clickTarget = hitTarget.closest('#aliyunCaptcha-checkbox-icon, #aliyunCaptcha-checkbox-body, #aliyunCaptcha-checkbox-left, #aliyunCaptcha-checkbox-wrapper') || element;
dispatchMouseLikeEvent(clickTarget, 'pointerdown', clientX, clientY, { buttons: 1, pressure: 0.5 });
dispatchMouseLikeEvent(clickTarget, 'mousedown', clientX, clientY, { buttons: 1 });
await sleep(randomInt(45, 125));
dispatchMouseLikeEvent(clickTarget, 'pointerup', clientX, clientY, { buttons: 0, pressure: 0 });
dispatchMouseLikeEvent(clickTarget, 'mouseup', clientX, clientY, { buttons: 0 });
dispatchMouseLikeEvent(clickTarget, 'click', clientX, clientY, { buttons: 0 });
await sleep(randomInt(settings.postDelay[0], settings.postDelay[1]));
return true;
}
async function handleAliyunCaptchaCheckbox(timeoutMs) {
const appeared = await waitForCondition(() => getAliyunCaptchaPopup(), timeoutMs, SECURITY_DIALOG_POLL_MS);
if (!appeared) {
return false;
}
return performAliyunCaptchaClick(timeoutMs);
}
async function performAliyunCaptchaClick(timeoutMs) {
const icon = getAliyunCaptchaCheckboxIcon() ||
document.querySelector('#aliyunCaptcha-checkbox-body') ||
document.querySelector('#aliyunCaptcha-checkbox-left');
if (!icon) {
return false;
}
await sleep(randomInt(450, 1100));
await simulateHumanClick(icon, {
paddingRatio: 0.18,
preDelay: [180, 520],
hoverDelay: [90, 240],
pressDelay: [70, 160],
postDelay: [120, 280],
approachDistance: [55, 130]
});
await sleep(randomInt(600, 1200));
const loading = document.querySelector('#aliyunCaptcha-loading');
if (loading && isElementVisible(loading)) {
await waitForCondition(() => {
return !isElementVisible(loading) || !!document.querySelector('.aliyunCaptcha-checkbox-icon-checked');
}, Math.min(timeoutMs, 12000), 400);
}
await waitForCondition(() => !getAliyunCaptchaPopup(), Math.min(timeoutMs, 15000), 400);
return true;
}
async function handleVerification(timeoutMs) {
const deadline = Date.now() + (timeoutMs || 12000);
let handled = false;
let securityConfirmed = false;
let aliyunClicked = false;
let sliderHandled = false;
let idleSince = Date.now();
while (Date.now() < deadline) {
let activeThisTick = false;
if (!securityConfirmed) {
const confirmBtn = getSecurityCheckConfirmButton(getSecurityCheckDialog());
if (confirmBtn) {
await sleep(randomInt(SECURITY_DIALOG_CONFIRM_DELAY_MIN_MS, SECURITY_DIALOG_CONFIRM_DELAY_MAX_MS));
clickConfirmFast(confirmBtn);
await sleep(SECURITY_DIALOG_POST_CLICK_MS);
securityConfirmed = true;
handled = true;
activeThisTick = true;
idleSince = Date.now();
}
}
if (!aliyunClicked && getAliyunCaptchaPopup()) {
if (await performAliyunCaptchaClick(deadline - Date.now())) {
aliyunClicked = true;
handled = true;
activeThisTick = true;
idleSince = Date.now();
}
}
if (!sliderHandled) {
const rectMask = document.querySelector('#rectMask');
if (rectMask && isElementVisible(rectMask)) {
rectMask.click();
await sleep(2000);
await simulateSlider();
sliderHandled = true;
handled = true;
activeThisTick = true;
idleSince = Date.now();
}
}
if (activeThisTick) {
await sleep(SECURITY_DIALOG_POLL_MS);
continue;
}
if (Date.now() - idleSince >= 1200) {
break;
}
await sleep(SECURITY_DIALOG_POLL_MS);
}
return handled;
}
async function submitSurvey() {
const submitBtn = getSubmitButton();
if (!submitBtn) {
return false;
}
submitBtn.click();
const verificationHandled = await handleVerification();
if (verificationHandled) {
await sleep(600);
const retrySubmit = getSubmitButton();
if (retrySubmit) {
retrySubmit.click();
await handleVerification();
}
} else {
await sleep(800);
}
return true;
}
async function simulateSlider() {
const slider = document.querySelector('#nc_1__scale_text > span') || document.querySelector('.nc_iconfont.btn_slide');
if (slider) {
const rect = slider.getBoundingClientRect();
const startX = rect.left + rect.width / 2;
const startY = rect.top + rect.height / 2;
const mouseDown = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
clientX: startX,
clientY: startY
});
slider.dispatchEvent(mouseDown);
const steps = 15;
const totalWidth = 300;
for (let i = 0; i <= steps; i++) {
await new Promise(r => setTimeout(r, 50 + Math.random() * 100));
const mouseMove = new MouseEvent('mousemove', {
bubbles: true,
cancelable: true,
clientX: startX + (totalWidth / steps) * i + (Math.random() * 6 - 3),
clientY: startY + (Math.random() * 6 - 3)
});
window.dispatchEvent(mouseMove);
}
const mouseUp = new MouseEvent('mouseup', {
bubbles: true,
cancelable: true
});
window.dispatchEvent(mouseUp);
}
}
init();
})();