// ==UserScript==
// @name 学习通随堂练习与选人活动自动化增强
// @namespace https://scriptcat.org/
// @version 1.4.2
// @description 自动填充随堂练习设置(时长3天/客观题2分/简答题10分/督促120分钟/存入题库),以及在选人活动(selectPerson)与随堂练习统计页(quizStatisticsPage)支持勾选部分学生进行批量打分
// @author Antigravity
// @match *://mobilelearn.chaoxing.com/page/quiz/addOrUpdateQuiz*
// @match *://mobilelearn.chaoxing.com/page/selectperson/selectPerson*
// @match *://mobilelearn.chaoxing.com/page/quiz/quizStatisticsPage*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
const currentPath = window.location.pathname;
// =========================================================================
// 基础通用组件:气泡提示 (Toast)
// =========================================================================
function showToast(message, type = 'success') {
let toastContainer = document.getElementById('cx-quiz-toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'cx-quiz-toast-container';
document.body.appendChild(toastContainer);
}
const toast = document.createElement('div');
toast.className = `cx-quiz-toast cx-quiz-toast-${type}`;
toast.innerHTML = `${message}`;
toastContainer.appendChild(toast);
setTimeout(() => toast.classList.add('cx-quiz-toast-show'), 10);
setTimeout(() => {
toast.classList.remove('cx-quiz-toast-show');
setTimeout(() => toast.remove(), 400);
}, 4500);
}
// =========================================================================
// 全局通用样式注入
// =========================================================================
function injectGlobalStyles() {
const css = `
/* 全局 Toast */
#cx-quiz-toast-container {
position: fixed;
top: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 999999;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
pointer-events: none;
}
.cx-quiz-toast {
padding: 10px 18px;
border-radius: 8px;
font-size: 13px;
line-height: 1.5;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
opacity: 0;
transform: translateY(-20px);
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
color: #fff;
max-width: 480px;
text-align: center;
pointer-events: auto;
}
.cx-quiz-toast-show {
opacity: 1;
transform: translateY(0);
}
.cx-quiz-toast-success { background: rgba(39, 174, 96, 0.95); }
.cx-quiz-toast-info { background: rgba(24, 144, 255, 0.95); }
.cx-quiz-toast-error { background: rgba(245, 34, 45, 0.95); }
/* 批量评分工具栏通用样式 */
.cx-batch-toolbar {
display: inline-flex;
align-items: center;
gap: 8px;
margin-left: 12px;
padding: 4px 10px;
background: #f0f7ff;
border: 1px solid #bae7ff;
border-radius: 6px;
font-size: 13px;
color: #595959;
vertical-align: middle;
user-select: none;
}
.cx-batch-btn {
padding: 2px 8px;
border-radius: 4px;
border: 1px solid #d9d9d9;
background: #ffffff;
color: #262626;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.cx-batch-btn:hover {
border-color: #1890ff;
color: #1890ff;
}
.cx-batch-btn-primary {
background: #1890ff;
border-color: #1890ff;
color: #ffffff;
font-weight: 500;
}
.cx-batch-btn-primary:hover {
background: #40a9ff;
border-color: #40a9ff;
color: #ffffff;
}
.cx-count-badge {
font-weight: 600;
color: #1890ff;
}
/* 学生勾选框通用样式 */
.cx-student-check-wrap {
position: absolute;
z-index: 10;
display: inline-flex;
align-items: center;
justify-content: center;
user-select: none;
}
.cx-student-check-input {
width: 18px !important;
height: 18px !important;
cursor: pointer !important;
accent-color: #1890ff !important;
display: inline-block !important;
visibility: visible !important;
opacity: 1 !important;
margin: 0 !important;
}
/* 自定义现代滑动开关组件 (解决宿主页面原生 checkbox 被样式重置隐藏的问题) */
.cx-switch {
position: relative;
display: inline-block !important;
width: 38px !important;
height: 20px !important;
vertical-align: middle;
cursor: pointer;
user-select: none;
margin: 0 !important;
flex-shrink: 0;
}
.cx-switch input[type="checkbox"] {
opacity: 0 !important;
width: 0 !important;
height: 0 !important;
position: absolute !important;
margin: 0 !important;
pointer-events: none;
}
.cx-switch .cx-slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: #d9d9d9;
transition: background-color 0.2s, transform 0.2s;
border-radius: 20px;
box-sizing: border-box;
}
.cx-switch .cx-slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background-color: #ffffff;
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
}
.cx-switch input:checked + .cx-slider {
background-color: #1890ff;
}
.cx-switch input:checked + .cx-slider:before {
transform: translateX(18px);
}
/* 预设悬浮按钮:仅在创建随堂练习页出现,在打分界面坚决不出现 */
#cx-quiz-fab-btn {
display: none !important;
}
body.cx-is-quiz-create #cx-quiz-fab-btn {
display: flex !important;
}
/* 选人页面特定高亮与定位 */
.person-list li {
position: relative !important;
cursor: pointer;
}
.person-list li .cx-student-check-wrap {
top: 6px;
left: 6px;
}
.person-list li.cx-card-selected .p-info {
outline: 2px solid #1890ff !important;
background: rgba(24, 144, 255, 0.08) !important;
border-radius: 8px !important;
}
/* 随堂练习题目卡片特定高亮与定位 */
.gap-people-box {
position: relative !important;
padding-left: 42px !important; /* 给复选框预留独立左侧列,避免遮挡 44px 头像 */
cursor: pointer;
transition: background-color 0.2s, border-color 0.2s;
}
.gap-people-box .cx-student-check-wrap {
top: 24px;
left: 12px;
}
.gap-people-box.cx-box-selected {
border-color: #1890ff !important;
background: rgba(24, 144, 255, 0.04) !important;
box-shadow: 0 0 0 1px #1890ff !important;
}
/* 快速批量打分弹窗 */
.cx-modal-mask {
position: fixed;
top: 0; left: 0; width: 100vw; height: 100vh;
background: rgba(0, 0, 0, 0.45);
z-index: 999998;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
backdrop-filter: blur(2px);
}
.cx-modal-mask.cx-show {
opacity: 1;
visibility: visible;
}
.cx-score-modal {
background: #ffffff;
width: 440px;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.cx-score-header {
padding: 14px 18px;
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
justify-content: space-between;
}
.cx-score-title {
font-size: 15px;
font-weight: 600;
color: #262626;
}
.cx-score-close {
font-size: 20px;
color: #8c8c8c;
cursor: pointer;
}
.cx-score-close:hover { color: #ff4d4f; }
.cx-score-body {
padding: 20px;
}
.cx-score-subtitle {
font-size: 13px;
color: #595959;
margin-bottom: 14px;
}
.cx-quick-scores {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
margin-bottom: 16px;
}
.cx-score-pill {
padding: 7px 0;
border-radius: 6px;
border: 1px solid #d9d9d9;
background: #ffffff;
color: #1890ff;
font-size: 14px;
font-weight: 600;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.cx-score-pill:hover {
border-color: #1890ff;
background: #e6f7ff;
}
.cx-score-pill-neg {
color: #f5222d;
}
.cx-score-pill-neg:hover {
border-color: #f5222d;
background: #fff1f0;
}
.cx-custom-row {
display: flex;
gap: 10px;
align-items: center;
}
.cx-custom-ipt {
flex: 1;
height: 34px;
padding: 4px 10px;
border: 1px solid #d9d9d9;
border-radius: 6px;
font-size: 14px;
outline: none;
}
.cx-custom-ipt:focus {
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
`;
GM_addStyle(css);
}
// =========================================================================
// 模块 1:选人活动页面增强 (selectPerson) - 支持勾选部分学生批量打分
// =========================================================================
function initSelectPersonModule() {
console.log('[选人活动-增强] 正在初始化选人页面批量评分增强...');
document.body.classList.remove('cx-is-quiz-create');
document.getElementById('cx-quiz-fab-btn')?.remove();
document.getElementById('cx-quiz-modal-mask')?.remove();
const pageCode = function () {
const TAG = '[选人评分-主世界]';
function getQueryParams() {
const params = new URLSearchParams(window.location.search);
return {
activeId: params.get('activeId') || (window.activeId ? String(window.activeId) : ''),
classId: params.get('classId') || (window.classId ? String(window.classId) : ''),
courseId: params.get('courseId') || (window.courseId ? String(window.courseId) : '')
};
}
// 挂载顶部工具栏
function ensureToolbar() {
const container = document.querySelector('.t-tip');
if (!container || document.getElementById('cx-sp-toolbar')) return;
const toolbar = document.createElement('div');
toolbar.id = 'cx-sp-toolbar';
toolbar.className = 'cx-batch-toolbar';
toolbar.innerHTML = `
已勾选 0 人
`;
container.appendChild(toolbar);
toolbar.querySelector('#cx-sp-select-all').addEventListener('click', () => {
document.querySelectorAll('.cx-sp-chk').forEach(chk => {
chk.checked = true;
chk.closest('li')?.classList.add('cx-card-selected');
});
updateCount();
});
toolbar.querySelector('#cx-sp-select-invert').addEventListener('click', () => {
document.querySelectorAll('.cx-sp-chk').forEach(chk => {
chk.checked = !chk.checked;
chk.closest('li')?.classList.toggle('cx-card-selected', chk.checked);
});
updateCount();
});
toolbar.querySelector('#cx-sp-select-clear').addEventListener('click', () => {
document.querySelectorAll('.cx-sp-chk').forEach(chk => {
chk.checked = false;
chk.closest('li')?.classList.remove('cx-card-selected');
});
updateCount();
});
toolbar.querySelector('#cx-sp-batch-grade').addEventListener('click', () => {
const uids = getSelectedUids();
if (uids.length === 0) {
alert('请先勾选需要打分的学生!');
return;
}
openScoreModal(uids);
});
}
function updateCount() {
const count = document.querySelectorAll('.cx-sp-chk:checked').length;
const badge = document.getElementById('cx-sp-selected-count');
if (badge) badge.textContent = String(count);
// 同步更新原生批量评分弹窗标题(若存在)
const maskTitle = document.querySelector('#gradeMask .preson-info .image-box span');
if (maskTitle && count > 0) {
maskTitle.textContent = `批量评分 (已选 ${count} 人)`;
}
}
function getSelectedUids() {
const checked = Array.from(document.querySelectorAll('.cx-sp-chk:checked'));
return checked.map(chk => chk.getAttribute('data-uid')).filter(Boolean);
}
// 为每个学生卡片注入复选框
function ensureCheckboxes() {
const items = document.querySelectorAll('.person-list li');
items.forEach(li => {
const pInfo = li.querySelector('.p-info');
if (!pInfo) return;
const suid = pInfo.getAttribute('suid');
if (!suid || li.querySelector('.cx-student-check-wrap')) return;
const wrap = document.createElement('span');
wrap.className = 'cx-student-check-wrap';
wrap.title = '勾选学生';
wrap.innerHTML = ``;
li.insertBefore(wrap, li.firstChild);
});
}
// 打开快速打分弹窗
function openScoreModal(uids) {
let modalMask = document.getElementById('cx-sp-modal-mask');
if (!modalMask) {
modalMask = document.createElement('div');
modalMask.id = 'cx-sp-modal-mask';
modalMask.className = 'cx-modal-mask';
modalMask.innerHTML = `
已勾选 0 名学生,点击快捷分值或输入分值:
+1分
+2分
+3分
+4分
+5分
+10分
+0分
-1分
-2分
-3分
`;
document.body.appendChild(modalMask);
modalMask.querySelector('#cx-sp-modal-close').addEventListener('click', () => {
modalMask.classList.remove('cx-show');
});
modalMask.addEventListener('click', (e) => {
if (e.target === modalMask) modalMask.classList.remove('cx-show');
});
// 快捷分值点击
modalMask.querySelectorAll('.cx-score-pill').forEach(btn => {
btn.addEventListener('click', () => {
const val = parseFloat(btn.dataset.score);
submitScores(window.__cxSpTargetUids || [], val);
modalMask.classList.remove('cx-show');
});
});
// 自定义输入确认
modalMask.querySelector('#cx-sp-confirm-btn').addEventListener('click', () => {
const ipt = modalMask.querySelector('#cx-sp-custom-score');
const val = parseFloat(ipt.value);
if (isNaN(val)) {
alert('请输入有效的分值!');
return;
}
submitScores(window.__cxSpTargetUids || [], val);
modalMask.classList.remove('cx-show');
});
}
window.__cxSpTargetUids = uids;
modalMask.querySelector('#cx-sp-modal-count').textContent = String(uids.length);
modalMask.classList.add('cx-show');
}
// 核心执行打分
async function submitScores(uids, score) {
if (!uids || uids.length === 0) return;
const { activeId, classId, courseId } = getQueryParams();
console.log(TAG, `开始为 ${uids.length} 名勾选学生打分: ${score}`, uids);
const widgetUrl = window._CP_widget_ || (window.ServiceDomain && window.ServiceDomain.widgetDomainWithProtocol) || '';
const apiUrl = `${widgetUrl}/widget/pick/pc/updateScore`;
let successCount = 0;
for (const suid of uids) {
try {
await new Promise((resolve) => {
window.$.ajax({
url: apiUrl,
type: 'GET',
data: { uid: suid, activeId, classId, courseId, score },
dataType: 'json',
success: function (res) {
if (res && res.result === 1) successCount++;
resolve(res);
},
error: function () { resolve(null); }
});
});
// 更新 Vue 响应式状态
if (window.vm && window.vm.userChoose) {
for (let s of window.vm.userChoose) {
if (String(s.uid) === String(suid)) {
s.score = score;
}
}
}
// 更新界面卡片分值徽章
const pInfo = document.querySelector(`.p-info[suid="${suid}"]`);
if (pInfo) {
const badge = pInfo.querySelector('.sel-opt');
if (badge) {
badge.textContent = `${score >= 0 ? '+' : ''}${score}`;
badge.className = `sel-opt ${score >= 0 ? 'bg-blue' : 'bg-red'}`;
}
}
} catch (e) {
console.error(TAG, `学生 ${suid} 打分异常:`, e);
}
}
alert(`🎉 批量评分完成!已为勾选的 ${uids.length} 名学生打分 ${score >= 0 ? '+' : ''}${score} 分`);
// 打分完成后清空勾选
document.querySelectorAll('.cx-sp-chk').forEach(chk => {
chk.checked = false;
chk.closest('li')?.classList.remove('cx-card-selected');
});
updateCount();
}
// 拦截原生批量评分弹窗的评分点击:若勾选了学生,则只给勾选的学生打分
document.addEventListener('click', function (e) {
const target = e.target;
const scoreItem = target.closest('#gradeMask .score-list .score-item');
const isBatchMode = document.getElementById('gradeMask')?.classList.contains('is-batch');
if (scoreItem && isBatchMode) {
const uids = getSelectedUids();
if (uids.length > 0) {
e.stopPropagation();
e.stopImmediatePropagation();
const num = parseFloat(scoreItem.textContent.replace('+', ''));
document.getElementById('gradeMask').style.display = 'none';
submitScores(uids, num);
}
}
}, true);
// 全局事件委托:点击复选框或学生卡片均可独立勾选/取消勾选
document.addEventListener('click', function (e) {
const li = e.target.closest('.person-list li');
if (!li) return;
// 排除单人评分徽章,保留原生单人评分面板
if (e.target.closest('.sel-opt')) return;
const chk = li.querySelector('.cx-sp-chk');
if (!chk) return;
if (e.target === chk) {
// 用户直接点击复选框,浏览器已完成勾选状态反转
e.stopPropagation();
} else {
// 用户点击卡片其他区域,程序化切换勾选状态并阻止原生弹窗
e.stopPropagation();
chk.checked = !chk.checked;
}
li.classList.toggle('cx-card-selected', chk.checked);
updateCount();
});
// 循环轮询与变动监听
setInterval(() => {
ensureToolbar();
ensureCheckboxes();
}, 800);
};
const script = document.createElement('script');
script.textContent = `(${pageCode.toString()})();`;
(document.head || document.documentElement).appendChild(script);
script.remove();
}
// =========================================================================
// 模块 2:随堂练习统计页面增强 (quizStatisticsPage) - 支持勾选部分学生批量打分
// =========================================================================
function initQuizStatisticsModule() {
console.log('[随堂练习统计-增强] 正在初始化随堂练习统计页面批量评分增强...');
document.body.classList.remove('cx-is-quiz-create');
document.getElementById('cx-quiz-fab-btn')?.remove();
document.getElementById('cx-quiz-modal-mask')?.remove();
const pageCode = function () {
const TAG = '[随堂练习统计-主世界]';
function getQueryParams() {
const params = new URLSearchParams(window.location.search);
return {
activeId: params.get('activeId') || (window.activeId ? String(window.activeId) : '')
};
}
// 挂载每个题目卡片上方的工具栏
function ensureQuizToolbars() {
const questionItems = document.querySelectorAll('.cql-item');
questionItems.forEach((cql, qIdx) => {
const setTipsLeft = cql.querySelector('.set-tips .set-tips-left');
if (!setTipsLeft || cql.querySelector('.cx-quiz-toolbar')) return;
const toolbar = document.createElement('div');
toolbar.className = 'cx-batch-toolbar cx-quiz-toolbar';
toolbar.setAttribute('data-qidx', String(qIdx));
toolbar.innerHTML = `
已勾选 0 人
`;
setTipsLeft.appendChild(toolbar);
toolbar.querySelector('.cx-btn-select-all').addEventListener('click', () => {
cql.querySelectorAll('.cx-quiz-chk').forEach(chk => {
chk.checked = true;
chk.closest('.gap-people-box')?.classList.add('cx-box-selected');
});
updateQuestionCount(cql);
});
toolbar.querySelector('.cx-btn-select-invert').addEventListener('click', () => {
cql.querySelectorAll('.cx-quiz-chk').forEach(chk => {
chk.checked = !chk.checked;
chk.closest('.gap-people-box')?.classList.toggle('cx-box-selected', chk.checked);
});
updateQuestionCount(cql);
});
toolbar.querySelector('.cx-btn-select-clear').addEventListener('click', () => {
cql.querySelectorAll('.cx-quiz-chk').forEach(chk => {
chk.checked = false;
chk.closest('.gap-people-box')?.classList.remove('cx-box-selected');
});
updateQuestionCount(cql);
});
toolbar.querySelector('.cx-btn-batch-grade').addEventListener('click', () => {
const uids = getQuestionSelectedUids(cql);
if (uids.length === 0) {
alert('请先勾选本题需要打分的学生!');
return;
}
openQuizScoreModal(cql, qIdx, uids);
});
});
}
function updateQuestionCount(cql) {
const count = cql.querySelectorAll('.cx-quiz-chk:checked').length;
const badge = cql.querySelector('.cx-quiz-count');
if (badge) badge.textContent = String(count);
// 更新本题原生下拉框文案提示
const currentScoreSpan = cql.querySelector('.set-tips-left .score-down-content .current-score span');
if (currentScoreSpan && count > 0) {
currentScoreSpan.textContent = `批量评分 (已选${count}人)`;
}
}
function getQuestionSelectedUids(cql) {
const checked = Array.from(cql.querySelectorAll('.cx-quiz-chk:checked'));
return checked.map(chk => chk.getAttribute('data-stuid')).filter(Boolean);
}
// 为每个学生的回答框注入复选框
function ensureQuizCheckboxes() {
const boxes = document.querySelectorAll('.gap-people-box');
boxes.forEach(box => {
if (box.querySelector('.cx-student-check-wrap')) return;
// 从头像链接解析 stuid
const img = box.querySelector('.avatar-img');
let stuid = '';
if (img && img.src) {
const m = img.src.match(/\/p\/(\d+)_/);
if (m) stuid = m[1];
}
if (!stuid && box.__vue__ && box.__vue__.mem) {
stuid = String(box.__vue__.mem.stuid || '');
}
if (!stuid) {
// 兜底:从内部组件或姓名附近寻找
const nameEl = box.querySelector('h5');
if (nameEl) stuid = nameEl.textContent.trim();
}
const wrap = document.createElement('span');
wrap.className = 'cx-student-check-wrap';
wrap.title = '勾选此学生';
wrap.innerHTML = ``;
box.insertBefore(wrap, box.firstChild);
});
}
// 打开随堂练习批量打分弹窗
function openQuizScoreModal(cql, qIdx, uids) {
let modalMask = document.getElementById('cx-quiz-modal-mask');
if (!modalMask) {
modalMask = document.createElement('div');
modalMask.id = 'cx-quiz-modal-mask';
modalMask.className = 'cx-modal-mask';
modalMask.innerHTML = `
本题已勾选 0 名学生,点击快捷分值或输入分值:
+1分
+2分
+3分
+4分
+5分
+10分
+0分
-1分
-2分
-3分
`;
document.body.appendChild(modalMask);
modalMask.querySelector('#cx-quiz-modal-close').addEventListener('click', () => {
modalMask.classList.remove('cx-show');
});
modalMask.addEventListener('click', (e) => {
if (e.target === modalMask) modalMask.classList.remove('cx-show');
});
modalMask.querySelectorAll('.cx-score-pill').forEach(btn => {
btn.addEventListener('click', () => {
const val = parseFloat(btn.dataset.score);
const ctx = window.__cxQuizTargetContext;
if (ctx) submitQuizScores(ctx.cql, ctx.qIdx, ctx.uids, val);
modalMask.classList.remove('cx-show');
});
});
modalMask.querySelector('#cx-quiz-confirm-btn').addEventListener('click', () => {
const ipt = modalMask.querySelector('#cx-quiz-custom-score');
const val = parseFloat(ipt.value);
if (isNaN(val)) {
alert('请输入有效的分值!');
return;
}
const ctx = window.__cxQuizTargetContext;
if (ctx) submitQuizScores(ctx.cql, ctx.qIdx, ctx.uids, val);
modalMask.classList.remove('cx-show');
});
}
window.__cxQuizTargetContext = { cql, qIdx, uids };
modalMask.querySelector('#cx-quiz-modal-title').textContent = `🎯 批量为第 ${qIdx + 1} 题选中学生打分`;
modalMask.querySelector('#cx-quiz-modal-count').textContent = String(uids.length);
modalMask.classList.add('cx-show');
}
// 核心执行随堂练习打分
async function submitQuizScores(cql, qIdx, uids, score) {
if (!uids || uids.length === 0) return;
const { activeId } = getQueryParams();
const vm = window.vm;
const que = (vm && vm.questionList && vm.questionList[qIdx]) ? vm.questionList[qIdx] : null;
const questionId = que ? que.id : (cql.getAttribute('data-question-id') || '');
console.log(TAG, `开始为第 ${qIdx + 1} 题(ID: ${questionId})选中的 ${uids.length} 名学生打分: ${score}`, uids);
const widgetUrl = window._CP_widget_ || (window.ServiceDomain && window.ServiceDomain.widgetDomainWithProtocol) || '';
const apiUrl = `${widgetUrl}/v2/apis/quizScore/setScoreForAnswer`;
let successCount = 0;
for (const uid of uids) {
try {
await new Promise((resolve) => {
window.$.ajax({
url: apiUrl,
type: 'GET',
data: { activeId, questionId, uid, score },
dataType: 'json',
success: function (res) {
if (res && res.result === 1) successCount++;
resolve(res);
},
error: function () { resolve(null); }
});
});
// 更新 Vue 模型
if (que && que.memberList) {
for (let m of que.memberList) {
if (String(m.stuid) === String(uid)) {
m.score = score;
}
}
}
// 更新 DOM 显示
const box = Array.from(cql.querySelectorAll('.gap-people-box')).find(b => {
return b.querySelector(`[data-stuid="${uid}"]`);
});
if (box) {
const curSpan = box.querySelector('.score-down-content .current-score span');
if (curSpan) curSpan.textContent = `${score}分`;
}
} catch (e) {
console.error(TAG, `第 ${qIdx + 1} 题学生 ${uid} 打分异常:`, e);
}
}
alert(`🎉 批量评分完成!已为第 ${qIdx + 1} 题勾选的 ${uids.length} 名学生打分 ${score >= 0 ? '+' : ''}${score} 分`);
// 尝试调用官方刷新接口
if (vm && typeof vm.getStudentQuestion === 'function') {
try {
vm.getStudentQuestion(null, qIdx, questionId, 0);
} catch (e) { }
}
// 清空本题勾选
cql.querySelectorAll('.cx-quiz-chk').forEach(chk => {
chk.checked = false;
chk.closest('.gap-people-box')?.classList.remove('cx-box-selected');
});
updateQuestionCount(cql);
}
// 拦截题目头部原生的批量修改积分下拉框
document.addEventListener('click', function (e) {
const li = e.target.closest('.set-tips-left .score-down-list li');
if (!li) return;
const cql = li.closest('.cql-item');
if (!cql) return;
const uids = getQuestionSelectedUids(cql);
if (uids.length > 0) {
// 勾选了部分学生:拦截全部打分,转为只对勾选学生打分
e.stopPropagation();
e.stopImmediatePropagation();
const numText = li.textContent.trim().replace('+', '');
const num = parseFloat(numText);
const qIdx = parseInt(cql.querySelector('.cx-quiz-toolbar')?.getAttribute('data-qidx') || '0', 10);
// 关闭下拉框
li.closest('.score-down-list').style.display = 'none';
submitQuizScores(cql, qIdx, uids, num);
}
}, true);
// 全局事件委托:点击复选框或学生答题卡片均可独立勾选/取消勾选
document.addEventListener('click', function (e) {
const box = e.target.closest('.gap-people-box');
if (!box) return;
// 排除单人评分下拉、状态指示、反馈面板、大图预览及链接/按钮
if (e.target.closest('.score-down-content, .gap-status-indicator, .gap-feedback-panel, .attach-img-list, .html-content img, a, button')) {
return;
}
const chk = box.querySelector('.cx-quiz-chk');
if (!chk) return;
if (e.target === chk) {
// 用户直接点击复选框,浏览器已完成勾选状态反转
e.stopPropagation();
} else {
// 用户点击卡片空白区、头像、姓名或回答内容:程序化切换勾选
e.stopPropagation();
chk.checked = !chk.checked;
}
box.classList.toggle('cx-box-selected', chk.checked);
const cql = box.closest('.cql-item');
if (cql) updateQuestionCount(cql);
});
setInterval(() => {
ensureQuizToolbars();
ensureQuizCheckboxes();
}, 800);
};
const script = document.createElement('script');
script.textContent = `(${pageCode.toString()})();`;
(document.head || document.documentElement).appendChild(script);
script.remove();
}
// =========================================================================
// 模块 3:添加/更新随堂练习页面 (addOrUpdateQuiz) - 保持原有自动预设功能
// =========================================================================
function initAddOrUpdateQuizModule() {
console.log('[随堂练习预设] 启动随堂练习预设自动填充模块...');
document.body.classList.add('cx-is-quiz-create');
const DEFAULT_CONFIG = {
objectiveScore: 2,
shortAnswerScore: 10,
durationDay: 3,
durationHour: 0,
durationMinute: 0,
enableUrge: true,
urgeBeforeMinutes: 120,
saveToBank: true,
bankFolderName: '随堂练习',
autoApplyOnLoad: true
};
function getConfig() {
const saved = GM_getValue('chaoxing_quiz_preset', null);
if (!saved) return { ...DEFAULT_CONFIG };
try {
return Object.assign({}, DEFAULT_CONFIG, typeof saved === 'string' ? JSON.parse(saved) : saved);
} catch (e) {
return { ...DEFAULT_CONFIG };
}
}
function saveConfig(cfg) {
GM_setValue('chaoxing_quiz_preset', JSON.stringify(cfg));
}
function injectAndRunInPageWorld(cfg) {
const pageCode = function (CONFIG) {
const TAG = '[随堂练习-主世界]';
console.log(TAG, '启动主世界预设注入...', CONFIG);
if (window.__cxIsApplying) {
console.log(TAG, '已有预设注入任务在执行中,忽略重复触发');
return;
}
window.__cxIsApplying = true;
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
function getPageVm() {
if (window.vm) return window.vm;
const app = document.getElementById('app');
if (app && app.__vue__) return app.__vue__;
return null;
}
async function setDuration(day, hour, min) {
const vm = getPageVm();
if (vm) {
vm.timelong_day = String(day);
vm.timelong_hour = String(hour);
vm.timelong_minute = String(min);
vm.handleEnd_temp = false;
}
const dayInp = document.getElementById('day');
const hourInp = document.getElementById('hour');
const minInp = document.getElementById('minute');
if (dayInp) {
dayInp.value = String(day);
dayInp.setAttribute('value', String(day));
dayInp.dispatchEvent(new Event('input', { bubbles: true }));
dayInp.dispatchEvent(new Event('change', { bubbles: true }));
}
if (hourInp) {
hourInp.value = String(hour);
hourInp.setAttribute('value', String(hour));
hourInp.dispatchEvent(new Event('input', { bubbles: true }));
hourInp.dispatchEvent(new Event('change', { bubbles: true }));
}
if (minInp) {
minInp.value = String(min);
minInp.setAttribute('value', String(min));
minInp.dispatchEvent(new Event('input', { bubbles: true }));
minInp.dispatchEvent(new Event('change', { bubbles: true }));
}
if (typeof window.reInitTestSelect === 'function') {
try {
window.reInitTestSelect(day != 0 || hour != 0);
} catch (e) { }
}
console.log(TAG, `活动时长已设置为: ${day}天 ${hour}小时 ${min}分钟`);
}
async function setUrge(enable, minutes) {
const vm = getPageVm();
if (vm) {
vm.isPrompt = !!enable;
vm.promptBeforeTime = minutes;
}
const swCheckbox = document.getElementById('isPrompt_checkbox');
const hiddenPrompt = document.querySelector('input[name="isPrompt"]');
if (swCheckbox) {
swCheckbox.checked = !!enable;
if (enable) {
swCheckbox.classList.add('checked');
} else {
swCheckbox.classList.remove('checked');
}
swCheckbox.dispatchEvent(new Event('change', { bubbles: true }));
}
if (hiddenPrompt) {
hiddenPrompt.value = enable ? '1' : '0';
}
const promptContent = document.getElementById('promptContent');
if (promptContent) {
promptContent.style.display = enable ? 'block' : 'none';
}
if (enable) {
const promptInp = document.getElementById('Prompt-select') || document.querySelector('input[name="promptBeforeTime"]');
if (promptInp) {
promptInp.value = String(minutes);
promptInp.setAttribute('value', String(minutes));
promptInp.dispatchEvent(new Event('input', { bubbles: true }));
promptInp.dispatchEvent(new Event('change', { bubbles: true }));
promptInp.dispatchEvent(new Event('blur', { bubbles: true }));
console.log(TAG, `督促未交学生已开启,提前提醒时间已设置为: ${minutes} 分钟`);
}
}
}
async function setBankFolder(saveToBank, folderName) {
const vm = getPageVm();
if (vm) {
vm.isSaveToReservoir = !!saveToBank;
}
const swBank = document.getElementById('isSaveToReservoir_checkbox');
const hiddenBank = document.querySelector('input[name="isSaveToReservoir"]');
if (swBank) {
swBank.checked = !!saveToBank;
if (saveToBank) {
swBank.classList.add('checked');
} else {
swBank.classList.remove('checked');
}
swBank.dispatchEvent(new Event('change', { bubbles: true }));
}
if (hiddenBank) {
hiddenBank.value = saveToBank ? '1' : '0';
}
const bankContent = document.getElementById('isSaveToReservoirContent');
if (bankContent) {
bankContent.style.display = saveToBank ? 'block' : 'none';
}
if (!saveToBank) return;
const currentText = document.getElementById('selDirectoryName')?.textContent?.trim() || '';
if (currentText.includes(folderName)) {
console.log(TAG, '题库目标目录已是:', currentText);
return;
}
const tkDirectoryBtn = document.getElementById('tkDirectory');
if (tkDirectoryBtn) {
console.log(TAG, '打开题库目录选择弹窗...');
tkDirectoryBtn.click();
await sleep(800);
await handleBankModal(folderName);
}
}
async function handleBankModal(targetName) {
const vm = getPageVm();
const findTargetNode = () => {
const nodes = Array.from(document.querySelectorAll('.popMove_name'));
return nodes.find(n => {
const name = n.dataset.folderName || n.getAttribute('data-folder-name') || n.textContent?.trim();
return name === targetName;
});
};
let target = findTargetNode();
if (!target) {
console.log(TAG, `尚未发现【${targetName}】文件夹,先聚焦选中根目录并点击新建...`);
const rootNode = document.querySelector('.popMove_name[data-folder-id="0"]') || document.querySelector('.popMove_name');
if (rootNode) {
rootNode.click();
await sleep(300);
}
const newFolderBtn = document.querySelector('.popMove_newFolder');
if (newFolderBtn) {
newFolderBtn.click();
await sleep(500);
const renameInp = document.querySelector('.rename_input');
if (renameInp) {
renameInp.value = targetName;
renameInp.dispatchEvent(new Event('input', { bubbles: true }));
renameInp.dispatchEvent(new Event('change', { bubbles: true }));
await sleep(300);
const renameYes = document.querySelector('.rename_yes');
if (renameYes) {
renameYes.click();
console.log(TAG, `已请求创建文件夹【${targetName}】,等待目录树刷新...`);
await sleep(1200);
}
}
}
target = findTargetNode();
}
if (target) {
console.log(TAG, `成功选中目标目录【${targetName}】`);
target.click();
await sleep(300);
if (vm && typeof vm.moveFolderOrQuestion === 'function') {
vm.moveFolderOrQuestion();
} else {
const confirmBtn = document.getElementById('confirm-move-btn');
if (confirmBtn) confirmBtn.click();
}
await sleep(400);
}
const cancelBtn = document.querySelector('.popMoveDele');
if (cancelBtn) cancelBtn.click();
}
function updateQuestionScores(objScore, saScore) {
const vm = getPageVm();
if (vm && vm.latestTopicSet) {
vm.latestTopicSet.latestSingleChoice = vm.latestTopicSet.latestSingleChoice || {};
vm.latestTopicSet.latestSingleChoice.score = objScore;
vm.latestTopicSet.latestMultipleChoice = vm.latestTopicSet.latestMultipleChoice || {};
vm.latestTopicSet.latestMultipleChoice.score = objScore;
vm.latestTopicSet.latestFillBlank = vm.latestTopicSet.latestFillBlank || {};
vm.latestTopicSet.latestFillBlank.score = objScore;
vm.latestTopicSet.latestTrueOrFalse = vm.latestTopicSet.latestTrueOrFalse || {};
vm.latestTopicSet.latestTrueOrFalse.score = objScore;
vm.latestTopicSet.latestShortAnswer = vm.latestTopicSet.latestShortAnswer || {};
vm.latestTopicSet.latestShortAnswer.score = saScore;
}
const cards = Array.from(document.querySelectorAll('.part-one-operate'));
console.log(TAG, `扫描到 ${cards.length} 道题目卡片`);
cards.forEach((card, idx) => {
const typeInput = card.querySelector('input[name^="question_type_"]');
const typeVal = typeInput ? typeInput.value : '';
const selectOne = card.querySelector('input[id^="selectOne"]');
const typeText = selectOne ? selectOne.value : '';
const cardContent = card.textContent || '';
const isShortAnswer = typeVal === '4' || typeText === '简答题' || cardContent.includes('作答获得课程积分') || cardContent.includes('简答');
const targetScore = isShortAnswer ? saScore : objScore;
const scoreInp = card.querySelector('.right-scores input, input[name^="item_score_"]');
if (scoreInp) {
scoreInp.value = String(targetScore);
scoreInp.setAttribute('value', String(targetScore));
scoreInp.dispatchEvent(new Event('input', { bubbles: true }));
scoreInp.dispatchEvent(new Event('change', { bubbles: true }));
if (window.$) {
window.$(scoreInp).val(targetScore).attr('value', targetScore);
}
if (vm && vm.questionList && vm.questionList[idx]) {
vm.questionList[idx].integral = targetScore;
}
try {
if (typeof window.setQueScore === 'function') {
window.setQueScore(scoreInp, idx);
}
} catch (e) { }
console.log(TAG, `题目 ${idx + 1}【${isShortAnswer ? '简答题' : '客观题'}】积分成功设定为: ${targetScore} 分`);
}
});
}
function hookOfficialAddQuestion(objScore, saScore) {
if (typeof window.addQuestion === 'function' && !window.__cxAddQuestionHooked) {
window.__cxAddQuestionHooked = true;
const origAddQuestion = window.addQuestion;
window.addQuestion = function (qtype) {
const res = origAddQuestion.apply(this, arguments);
setTimeout(() => {
updateQuestionScores(objScore, saScore);
}, 250);
return res;
};
console.log(TAG, '已成功挂钩官方 addQuestion 函数');
}
const sortBox = document.querySelector('.sorttable-box');
if (sortBox && !window.__cxSortBoxHooked) {
window.__cxSortBoxHooked = true;
const obs = new MutationObserver((mutations) => {
if (window.__cxIsApplying) return;
const hasNewCard = mutations.some(m => Array.from(m.addedNodes).some(n => n.nodeType === 1 && (n.classList?.contains('part-one-operate') || n.querySelector?.('.part-one-operate'))));
if (hasNewCard) {
if (window.__cxScoreDebounceTimer) clearTimeout(window.__cxScoreDebounceTimer);
window.__cxScoreDebounceTimer = setTimeout(() => {
updateQuestionScores(objScore, saScore);
}, 250);
}
});
obs.observe(sortBox, { childList: true });
}
}
(async () => {
try {
await setDuration(CONFIG.durationDay, CONFIG.durationHour, CONFIG.durationMinute);
await setUrge(CONFIG.enableUrge, CONFIG.urgeBeforeMinutes);
await setBankFolder(CONFIG.saveToBank, CONFIG.bankFolderName);
updateQuestionScores(CONFIG.objectiveScore, CONFIG.shortAnswerScore);
hookOfficialAddQuestion(CONFIG.objectiveScore, CONFIG.shortAnswerScore);
console.log(TAG, '🎉 全流程预设填充顺利完成!');
} catch (err) {
console.error(TAG, '执行过程异常:', err);
} finally {
window.__cxIsApplying = false;
}
})();
};
const script = document.createElement('script');
script.textContent = `(${pageCode.toString()})(${JSON.stringify(cfg)});`;
(document.head || document.documentElement).appendChild(script);
script.remove();
}
function applyAllPresets(cfg = getConfig()) {
showToast('🚀 正在执行随堂练习预设自动填充...', 'info');
injectAndRunInPageWorld(cfg);
showToast(
`🎉 随堂练习预设填充指令已发送!
` +
`⏱️ 时长:${cfg.durationDay}天${cfg.durationHour}小时${cfg.durationMinute}分 | ` +
`💯 客观题:${cfg.objectiveScore}分,简答题:${cfg.shortAnswerScore}分
` +
`🔔 督促:${cfg.enableUrge ? `提前${cfg.urgeBeforeMinutes}分钟` : '已关闭'} | ` +
`📁 题库:${cfg.saveToBank ? `根目录/${cfg.bankFolderName}` : '未启用'}`,
'success'
);
}
// 创建随堂练习设置弹窗与悬浮按钮
function createPresetUI() {
// 严格防护:仅在创建/编辑随堂练习页面注入悬浮设置按钮,打分界面绝不出现
if (!window.location.pathname.includes('/page/quiz/addOrUpdateQuiz')) {
return;
}
if (document.getElementById('cx-quiz-fab-btn')) return;
const fab = document.createElement('div');
fab.id = 'cx-quiz-fab-btn';
fab.title = '随堂练习自动预设设置';
fab.style.cssText = `
position: fixed; right: 24px; bottom: 80px; z-index: 99999;
width: 44px; height: 44px; border-radius: 50%;
background: linear-gradient(135deg, #1890ff, #096dd9);
color: #fff; box-shadow: 0 4px 12px rgba(24, 144, 255, 0.4);
display: flex; align-items: center; justify-content: center;
cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; user-select: none;
`;
fab.innerHTML = `
`;
document.body.appendChild(fab);
const mask = document.createElement('div');
mask.id = 'cx-quiz-modal-mask';
mask.className = 'cx-modal-mask';
const cfg = getConfig();
mask.innerHTML = `
⚙️ 随堂练习自动预设设置
×
`;
document.body.appendChild(mask);
const openModal = () => {
const current = getConfig();
document.getElementById('cx-cfg-obj-score').value = current.objectiveScore;
document.getElementById('cx-cfg-sa-score').value = current.shortAnswerScore;
document.getElementById('cx-cfg-dur-day').value = current.durationDay;
document.getElementById('cx-cfg-dur-hour').value = current.durationHour;
document.getElementById('cx-cfg-dur-min').value = current.durationMinute;
document.getElementById('cx-cfg-enable-urge').checked = current.enableUrge;
document.getElementById('cx-cfg-urge-min').value = current.urgeBeforeMinutes;
document.getElementById('cx-cfg-save-bank').checked = current.saveToBank;
document.getElementById('cx-cfg-bank-folder').value = current.bankFolderName;
document.getElementById('cx-cfg-auto-apply').checked = current.autoApplyOnLoad;
mask.classList.add('cx-show');
};
const closeModal = () => mask.classList.remove('cx-show');
fab.addEventListener('click', openModal);
document.getElementById('cx-modal-close-btn').addEventListener('click', closeModal);
mask.addEventListener('click', (e) => {
if (e.target === mask) closeModal();
});
const getFormData = () => ({
objectiveScore: parseInt(document.getElementById('cx-cfg-obj-score').value, 10) || 2,
shortAnswerScore: parseInt(document.getElementById('cx-cfg-sa-score').value, 10) || 10,
durationDay: parseInt(document.getElementById('cx-cfg-dur-day').value, 10) || 0,
durationHour: parseInt(document.getElementById('cx-cfg-dur-hour').value, 10) || 0,
durationMinute: parseInt(document.getElementById('cx-cfg-dur-min').value, 10) || 0,
enableUrge: document.getElementById('cx-cfg-enable-urge').checked,
urgeBeforeMinutes: parseInt(document.getElementById('cx-cfg-urge-min').value, 10) || 120,
saveToBank: document.getElementById('cx-cfg-save-bank').checked,
bankFolderName: document.getElementById('cx-cfg-bank-folder').value.trim() || '随堂练习',
autoApplyOnLoad: document.getElementById('cx-cfg-auto-apply').checked
});
document.getElementById('cx-btn-save').addEventListener('click', () => {
const newCfg = getFormData();
saveConfig(newCfg);
showToast('✅ 预设已成功保存!', 'success');
closeModal();
});
document.getElementById('cx-btn-apply-now').addEventListener('click', () => {
const newCfg = getFormData();
saveConfig(newCfg);
closeModal();
applyAllPresets(newCfg);
});
document.getElementById('cx-btn-reset').addEventListener('click', () => {
saveConfig(DEFAULT_CONFIG);
openModal();
showToast('已重置为系统默认配置', 'info');
});
GM_registerMenuCommand('⚙️ 打开随堂练习预设设置', openModal);
GM_registerMenuCommand('⚡ 立即执行预设填充', () => applyAllPresets(getConfig()));
}
createPresetUI();
const cfg = getConfig();
if (cfg.autoApplyOnLoad) {
let retry = 0;
const checkReady = setInterval(() => {
retry++;
const hasForm = !!document.getElementById('app') &&
(!!document.getElementById('day') || !!document.getElementById('isPrompt_checkbox'));
if (hasForm || retry > 20) {
clearInterval(checkReady);
setTimeout(() => {
applyAllPresets(cfg);
}, 800);
}
}, 300);
}
}
// =========================================================================
// 入口调度:依据当前访问的页面路由分别加载对应模块
// =========================================================================
injectGlobalStyles();
if (currentPath.includes('/page/selectperson/selectPerson')) {
initSelectPersonModule();
} else if (currentPath.includes('/page/quiz/quizStatisticsPage')) {
initQuizStatisticsModule();
} else if (currentPath.includes('/page/quiz/addOrUpdateQuiz')) {
initAddOrUpdateQuizModule();
}
})();