// ==UserScript==
// @name 学习通作业考试全能速览
// @namespace https://github.com/lcandy2/user.js/tree/main/websites/chaoxing.com/chaoxing-assignment
// @version 1.0 修复页面空白版
// @author FURUL (fuwawas) + 空白修复
// @description 支持作业、考试列表快速查看 | 修复页面空白问题
// @license AGPL-3.0-or-later
// @copyright fuwawas All Rights Reserved
// @homepage https://greasyfork.org/scripts/495345
// @homepageURL https://greasyfork.org/scripts/495345
// @source https://github.com/lcandy2/user.js/tree/main/websites/chaoxing.com/chaoxing-assignment
// @match *://i.chaoxing.com/base*
// @match *://mooc1-api.chaoxing.com/work/stu-work*
// @match *://mooc1-api.chaoxing.com/exam-ans/exam/phone/examcode*
// @require https://registry.npmmirror.com/vue/3.4.27/files/dist/vue.global.prod.js
// @require data:application/javascript,%3Bwindow.Vue%3DVue%3B
// @require https://registry.npmmirror.com/vuetify/3.6.6/files/dist/vuetify.min.js
// @require data:application/javascript,%3B
// @resource VuetifyStyle https://registry.npmmirror.com/vuetify/3.6.6/files/dist/vuetify.min.css
// @resource MaterialIcons https://fonts.googleapis.com/icon?family=Material+Icons
// @grant GM_addStyle
// @grant GM_getResourceText
// @run-at document-end
// ==/UserScript==
(function (Vuetify, Vue) {
'use strict';
// 核心配置(基于提供的HTML结构)
const CONFIG = {
homeworkUrl: 'https://mooc1-api.chaoxing.com/work/stu-work#chaoxing-assignment',
examUrl: 'https://mooc1-api.chaoxing.com/exam-ans/exam/phone/examcode#chaoxing-assignment',
// 精准导航栏父容器(从HTML中提取的唯一选择器)
menuParentSelector: '.menu-list-ul',
// 按钮配置(模仿现有导航项结构)
buttons: [
{
id: 'first1000001',
name: '全部作业',
iconClass: 'icon-space icon-bj', // 复用现有图标class
dataId: '1000001',
url: 'https://mooc1-api.chaoxing.com/work/stu-work#chaoxing-assignment'
},
{
id: 'first1000002',
name: '全部考试',
iconClass: 'icon-space icon-cj', // 复用现有图标class
dataId: '1000002',
url: 'https://mooc1-api.chaoxing.com/exam-ans/exam/phone/examcode#chaoxing-assignment'
}
]
};
// 加载样式(仅加载Vuetify和图标,不删除原页面样式)
const cssLoader = key => GM_addStyle(GM_getResourceText(key));
cssLoader("VuetifyStyle");
cssLoader("MaterialIcons");
// -------------------------- 修复:导航按钮正常显示(不破坏页面) --------------------------
const createNavButtons = () => {
const menuParent = document.querySelector(CONFIG.menuParentSelector);
if (!menuParent) {
console.error('[适配失败] 未找到导航栏父容器:', CONFIG.menuParentSelector);
return;
}
CONFIG.buttons.forEach(btnConfig => {
if (document.getElementById(btnConfig.id)) return;
// 1. 创建li容器(完全模仿现有结构)
const liElement = document.createElement('li');
liElement.setAttribute('level', '1');
liElement.setAttribute('parent-id', '');
liElement.setAttribute('table-type', '1');
liElement.setAttribute('parent-type', '');
liElement.setAttribute('data-id', btnConfig.dataId);
liElement.style = '';
// 2. 创建按钮核心div(完全模仿现有.label-item结构)
const btnElement = document.createElement('div');
btnElement.id = btnConfig.id;
btnElement.setAttribute('role', 'menuitem');
btnElement.setAttribute('level', '1');
btnElement.setAttribute('focus_element', '0');
btnElement.setAttribute('tabindex', '-1');
btnElement.setAttribute('name', btnConfig.name);
btnElement.setAttribute('imgName', 'icon-home');
btnElement.setAttribute('dataurl', btnConfig.url);
btnElement.setAttribute('class', 'label-item');
btnElement.setAttribute('onclick', `setUrl('${btnConfig.dataId}','${btnConfig.url}',this,'0','${btnConfig.name}')`);
btnElement.setAttribute('aria-label', `${btnConfig.name}菜单项`);
// 3. 按钮内部结构(图标+文字+箭头)
btnElement.innerHTML = `
${btnConfig.name}
`;
// 4. 添加子菜单容器(模仿现有结构)
const subMenuContainer = document.createElement('div');
subMenuContainer.className = 'school-level';
subMenuContainer.innerHTML = '';
// 5. 组装并插入到导航栏最前面
liElement.appendChild(btnElement);
liElement.appendChild(subMenuContainer);
menuParent.prepend(liElement);
console.log(`[适配成功] 已添加${btnConfig.name}按钮`);
});
};
// -------------------------- 修复:任务列表提取(不依赖被隐藏的容器) --------------------------
function extractTasks() {
// 直接从原页面提取作业元素(去掉#chaoxing-assignment-wrapper的依赖)
const taskElements = document.querySelectorAll('ul.nav > li');
return Array.from(taskElements).map(task => {
const option = task.querySelector('div[role="option"]');
const raw = task.getAttribute("data") || "";
const url = raw ? new URL(raw) : null;
return {
title: option?.querySelector("p")?.textContent || "",
status: option?.querySelector("span:nth-of-type(1)")?.textContent || "",
uncommitted: option?.querySelector("span:nth-of-type(1)")?.className.includes("status") || false,
course: option?.querySelector("span:nth-of-type(2)")?.textContent || "",
leftTime: option?.querySelector(".fr")?.textContent || "",
workId: url?.searchParams.get("taskrefId") || "",
courseId: url?.searchParams.get("courseId") || "",
clazzId: url?.searchParams.get("clazzId") || "",
raw
};
});
}
function extractExams() {
// 直接从原页面提取考试元素(去掉#chaoxing-assignment-wrapper的依赖)
const examElements = document.querySelectorAll('ul.ks_list > li');
return Array.from(examElements).map(exam => {
const dl = exam.querySelector("dl");
const img = exam.querySelector("div.ks_pic > img");
const raw = exam.getAttribute("data") || "";
const url = raw ? new URL(window.location.protocol + "//" + window.location.host + raw) : null;
const status = exam.querySelector("span.ks_state")?.textContent || "";
return {
title: dl?.querySelector("dt")?.textContent || "",
status,
timeLeft: dl?.querySelector("dd")?.textContent || "",
expired: img?.getAttribute("src")?.includes("ks_02") || false,
finished: status.includes("已完成") || status.includes("待批阅"),
examId: url?.searchParams.get("taskrefId") || "",
courseId: url?.searchParams.get("courseId") || "",
classId: url?.searchParams.get("classId") || "",
raw
};
});
}
// -------------------------- Vue组件和Vuetify配置(保留) --------------------------
const API_VISIT_COURSE = "https://mooc1.chaoxing.com/visit/stucoursemiddle?ismooc2=1";
const API_OPEN_EXAM = "https://mooc1-api.chaoxing.com/exam-ans/exam/test/examcode/examnotes";
const TasksList = Vue.defineComponent({
__name: "tasks-list",
setup() {
const tasks = extractTasks();
const search = Vue.ref("");
const headers = [
{ key: "title", title: "作业名称" },
{ key: "course", title: "课程" },
{ key: "leftTime", title: "剩余时间" },
{ key: "status", title: "状态" },
{ key: "action", title: "", sortable: false }
];
const getLink = item => {
const url = new URL(API_VISIT_COURSE);
url.searchParams.append("courseid", item.courseId);
url.searchParams.append("clazzid", item.clazzId);
return url.href;
};
return () => Vue.h("v-card", { title: "作业列表", variant: "flat", style: { margin: '20px' } }, [
Vue.h("v-text-field", {
modelValue: search.value,
"onUpdate:modelValue": v => search.value = v,
label: "搜索",
"prepend-inner-icon": "search",
variant: "outlined",
"hide-details": true,
"single-line": true
}),
Vue.h("v-data-table", {
items: tasks,
search: search.value,
hover: true,
headers,
sticky: true,
"items-per-page": -1,
"hide-default-footer": true,
"item.action": ({ item }) => Vue.h("v-btn", {
variant: item.uncommitted ? "tonal" : "plain",
color: "primary",
href: getLink(item),
target: "_blank"
}, item.uncommitted ? "立即完成" : "查看详情")
})
]);
}
});
const ExamsList = Vue.defineComponent({
__name: "exams-list",
setup() {
const exams = extractExams();
const search = Vue.ref("");
const headers = [
{ key: "title", title: "考试名称" },
{ key: "timeLeft", title: "剩余时间" },
{ key: "status", title: "状态" },
{ key: "action", title: "", sortable: false }
];
const getLink = item => {
const url = new URL(API_OPEN_EXAM);
url.searchParams.append("courseId", item.courseId);
url.searchParams.append("classId", item.classId);
url.searchParams.append("examId", item.examId);
return url.href;
};
return () => Vue.h("v-card", { title: "考试列表", variant: "flat", style: { margin: '20px' } }, [
Vue.h("v-text-field", {
modelValue: search.value,
"onUpdate:modelValue": v => search.value = v,
label: "搜索",
"prepend-inner-icon": "search",
variant: "outlined",
"hide-details": true,
"single-line": true
}),
Vue.h("v-data-table", {
items: exams,
search: search.value,
hover: true,
headers,
sticky: true,
"items-per-page": -1,
"hide-default-footer": true,
"item.action": ({ item }) => Vue.h("v-btn", {
variant: item.finished || item.expired ? "plain" : "tonal",
color: "primary",
href: getLink(item),
target: "_blank"
}, item.finished || item.expired ? "查看详情" : "前往考试")
})
]);
}
});
const appendApp = () => {
const vuetify = Vuetify.createVuetify({
icons: {
defaultSet: "md",
sets: { md: { component: props => Vue.h("span", { class: "material-icons" }, props.icon) } }
}
});
// 创建独立容器挂载Vue组件,不影响原页面
const appContainer = document.createElement("div");
appContainer.style = "margin: 20px;";
document.body.appendChild(appContainer);
const app = Vue.createApp(urlDetection() === "homework" ? TasksList : ExamsList);
app.use(vuetify).mount(appContainer);
};
// -------------------------- 修复:主逻辑(不破坏原页面) --------------------------
const urlDetection = () => {
const url = window.location.href;
const hash = window.location.hash;
if (hash.includes("chaoxing-assignment")) {
return url.includes("work/stu-work") ? "homework" : url.includes("examcode") ? "exam" : "unknown";
}
return url.includes("i.chaoxing.com/base") ? "newHome" : "unknown";
};
const init = () => {
const urlType = urlDetection();
console.log(`[精准适配] 当前页面:${urlType}`);
if (urlType === "newHome") {
// 延迟100ms确保导航栏加载完成
setTimeout(() => {
createNavButtons();
}, 100);
} else if (urlType === "homework" || urlType === "exam") {
// 移除破坏页面的操作(wrap/remove),直接挂载Vue组件
appendApp();
}
};
// 启动初始化(监听DOM加载完成)
if (document.readyState === "complete" || document.readyState === "interactive") {
init();
} else {
document.addEventListener("DOMContentLoaded", init);
}
})(Vuetify, Vue);