// ==UserScript== // @name 学习通作业/考试/任务列表(优化版) // @namespace https://github.com/Cooanyh // @version 2.0.0 // @author 甜檸Cirtron (lcandy2); Modified by Coren // @description 【优化版】支持作业、考试、课程任务列表快速查看。基于原版脚本修改:1. 新增支持在 https://i.chaoxing.com/ 空间页面显示;2. 优化考试与作业列表 UI;3. 新增"任务"/"课程任务"标签,汇总所有课程的待办任务;4. 新增待办即将过期任务提醒;5. 整合学习仪表盘,UI 极简优化,支持板块全屏查看;6. v2.0.0 UI 重构升级:全新设计风格、欢迎区域、状态胶囊。 // @license AGPL-3.0-or-later // @copyright lcandy2 All Rights Reserved // @copyright 2025, Coren (Modified based on original work) // @source https://github.com/lcandy2/user.js/tree/main/websites/chaoxing.com/chaoxing-assignment // @match *://mooc1-api.chaoxing.com/work/stu-work* // @match *://i.chaoxing.com/* // @match *://i.chaoxing.com/base* // @match *://i.mooc.chaoxing.com/space/index* // @match *://i.mooc.chaoxing.com/settings* // @match *://mooc1-api.chaoxing.com/exam-ans/exam/phone/examcode* // @match *://mooc1.chaoxing.com/exam-ans/exam/test/examcode/examlist* // @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 material-design-icons-iconfont/dist/material-design-icons.css https://fonts.googlefonts.cn/css?family=Material+Icons // @grant GM_addStyle // @grant GM_getResourceText // @grant GM_xmlhttpRequest // @connect mooc1-api.chaoxing.com // @connect mobilelearn.chaoxing.com // @connect stat2-ans.chaoxing.com // @connect mooc2-ans.chaoxing.com // @connect mooc1.chaoxing.com // @connect i.chaoxing.com // @match *://mooc2-ans.chaoxing.com/* // @run-at document-end // ==/UserScript== (function (vuetify, vue) { 'use strict'; // --- 核心工具函数 --- const wrapElements = () => { const wrapper = document.createElement("body"); wrapper.id = "chaoxing-assignment-wrapper"; while (document.body.firstChild) { wrapper.appendChild(document.body.firstChild); } document.body.appendChild(wrapper); wrapper.style.display = "none"; }; const removeStyles = () => { removeHtmlStyle(); const styles = document.querySelectorAll("link[rel=stylesheet]"); styles.forEach((style) => { var _a; if ((_a = style.getAttribute("href")) == null ? void 0 : _a.includes("chaoxing")) { style.remove(); } }); }; const removeHtmlStyle = () => { const html = document.querySelector("html"); html == null ? void 0 : html.removeAttribute("style"); }; const keepRemoveHtmlStyle = () => { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === "attributes" && mutation.attributeName === "style") { removeHtmlStyle(); } }); }); const html = document.querySelector("html"); html && observer.observe(html, { attributes: true }); }; const removeScripts = () => { const scripts = document.querySelectorAll("script"); scripts.forEach((script) => { var _a; if ((_a = script.getAttribute("src")) == null ? void 0 : _a.includes("chaoxing")) { script.remove(); } }); }; const urlDetection = () => { const url = window.location.href; const hash = window.location.hash; // 新版仪表盘检测 if (hash.includes("chaoxing-dashboard")) { return "dashboard"; } if (hash.includes("chaoxing-assignment-activities")) { return "activities"; } if (hash.includes("chaoxing-assignment-todo")) { return "todo"; } if (hash.includes("chaoxing-assignment")) { if (url.includes("mooc1-api.chaoxing.com/work/stu-work")) { return "homework"; } if (url.includes("mooc1-api.chaoxing.com/exam-ans/exam/phone/examcode")) { return "exam"; } } if (url.includes("mooc1.chaoxing.com/exam-ans/exam/test/examcode/examlist")) { return "exam"; } if (url.includes("i.chaoxing.com")) { return "home"; } if (url.includes("i.mooc.chaoxing.com/space/index") || url.includes("i.mooc.chaoxing.com/settings")) { return "legacyHome"; } // 课程章节页面检测 if (url.includes("mooc2-ans.chaoxing.com/mooc2-ans/mycourse/studentcourse") || url.includes("mooc2-ans.chaoxing.com/mooc2-ans/mycourse/stu")) { return "course_chapter"; } }; const fixCssConflict = () => { const style = document.createElement('style'); style.textContent = ` .menu-list .label-item h3 { font-size: 14px !important; margin: 0 !important; line-height: 24px !important; font-weight: normal !important; } div.menubar a h5 { font-size: 14px !important; margin: 0 !important; line-height: normal !important; font-weight: bold !important; white-space: nowrap !important; } .leftnav h3, .left_nav h3, .funclistul h3, .user-info h3, div[class*="menu"] h3, div[class*="nav"] h3, #space_left h3, .space-left h3 { font-size: 14px !important; } .space_opt .manageBtn { font-size: 12px !important; line-height: 24px !important; height: auto !important; width: auto !important; padding: 0 10px !important; margin: 0 5px !important; display: inline-block !important; box-sizing: content-box !important; text-align: center !important; white-space: nowrap !important; border-radius: 4px !important; } .space_opt a.manageBtn { text-decoration: none !important; color: #333 !important; } `; document.head.appendChild(style); }; const createMenuItem = (id, text, iconClass, targetUrl, insertFunc) => { const url = targetUrl; const menubarElement = document.querySelector('div.menubar[role="menubar"]'); if (menubarElement) { const a = document.createElement("a"); a.setAttribute("role", "menuitem"); a.setAttribute("tabindex", "-1"); a.id = `first${id}`; a.setAttribute("onclick", `setUrl('${id}','${url}',this,'0','${text}')`); a.setAttribute("dataurl", url); const icon = document.createElement("span"); icon.className = `icon-space ${iconClass}`; a.appendChild(icon); const h5 = document.createElement("h5"); h5.title = text; h5.innerHTML = `${text}`; a.appendChild(h5); const arrow = document.createElement("span"); arrow.className = "arrow icon-uniE900"; a.appendChild(arrow); if (insertFunc) insertFunc(menubarElement, a); else menubarElement.prepend(a); } }; const createMenuItemNew = (id, text, iconClass, targetUrl, insertFunc) => { const menuListElement = document.querySelector("ul.menu-list-ul"); if (menuListElement) { const li = document.createElement("li"); li.setAttribute("level", "1"); li.setAttribute("table-type", "1"); li.setAttribute("data-id", `chaoxing-assignment-${id}`); const div = document.createElement("div"); div.className = "label-item"; div.setAttribute("role", "menuitem"); div.setAttribute("level", "1"); div.setAttribute("tabindex", "-1"); div.setAttribute("name", text); div.setAttribute("id", `first_chaoxing_assignment_${id}`); div.setAttribute("onclick", `setUrl('chaoxing-assignment-${id}','${targetUrl}',this,'0','${text}')`); div.setAttribute("dataurl", targetUrl); const icon = document.createElement("span"); icon.className = `icon-space ${iconClass}`; div.appendChild(icon); const h3 = document.createElement("h3"); h3.title = text; h3.textContent = text; div.appendChild(h3); const arrow = document.createElement("span"); arrow.className = "slide-arrow icon-h-arrow-l hide"; div.appendChild(arrow); li.appendChild(div); li.appendChild(Object.assign(document.createElement("div"), { className: "school-level" })).appendChild(document.createElement("ul")); if (insertFunc) insertFunc(menuListElement, li); else menuListElement.prepend(li); } }; const createMenuItemLegacy = (id, text, iconClass, targetUrl) => { const list = document.querySelector("ul.funclistul"); if (list && !document.querySelector(`#li_chaoxing-assignment-${id}`)) { const li = document.createElement("li"); li.id = `li_chaoxing-assignment-${id}`; li.className = ''; li.innerHTML = `${text}`; list.prepend(li); } }; // URL 常量 const URL_HOMEWORK = 'https://mooc1-api.chaoxing.com/work/stu-work#chaoxing-assignment'; const URL_EXAM = 'https://mooc1-api.chaoxing.com/exam-ans/exam/phone/examcode#chaoxing-assignment'; const URL_TODO = 'https://mooc1-api.chaoxing.com/work/stu-work#chaoxing-assignment-todo'; const URL_ACTIVITIES = 'https://mooc1-api.chaoxing.com/work/stu-work#chaoxing-assignment-activities'; const URL_DASHBOARD = 'https://mooc1-api.chaoxing.com/work/stu-work#chaoxing-dashboard'; const API_COURSE_LIST = 'https://mooc1-api.chaoxing.com/mycourse/backclazzdata?view=json&mcode='; const initMenus = () => { // 只创建单个"学习仪表盘"入口 if (document.querySelector('div.menubar[role="menubar"]')) { if (!document.querySelector('#first1000000')) { createMenuItem('1000000', '📊 学习仪表盘', 'icon-bj', URL_DASHBOARD); } } else if (document.querySelector("ul.menu-list-ul")) { if (!document.querySelector('#first_chaoxing_assignment_dashboard')) { createMenuItemNew('dashboard', '📊 学习仪表盘', 'icon-bj', URL_DASHBOARD); } } else if (document.querySelector("ul.funclistul")) { createMenuItemLegacy('dashboard', '📊 学习仪表盘', 'zne_bj_icon', URL_DASHBOARD); } }; // 新标签页打开的菜单项创建函数 const createMenuItemNewTab = (id, text, iconClass, targetUrl) => { const menubarElement = document.querySelector('div.menubar[role="menubar"]'); if (menubarElement) { const a = document.createElement("a"); a.setAttribute("role", "menuitem"); a.setAttribute("tabindex", "-1"); a.id = `first${id}`; a.href = targetUrl; a.target = "_blank"; a.style.cursor = "pointer"; const icon = document.createElement("span"); icon.className = `icon-space ${iconClass}`; a.appendChild(icon); const h5 = document.createElement("h5"); h5.title = text; h5.innerHTML = `${text}`; a.appendChild(h5); const arrow = document.createElement("span"); arrow.className = "arrow icon-uniE900"; a.appendChild(arrow); menubarElement.prepend(a); } }; const createMenuItemNewTabNew = (id, text, iconClass, targetUrl) => { const menuListElement = document.querySelector("ul.menu-list-ul"); if (menuListElement) { const li = document.createElement("li"); li.setAttribute("level", "1"); li.setAttribute("table-type", "1"); li.setAttribute("data-id", `chaoxing-assignment-${id}`); const div = document.createElement("div"); div.className = "label-item"; div.setAttribute("role", "menuitem"); div.setAttribute("level", "1"); div.setAttribute("tabindex", "-1"); div.setAttribute("name", text); div.setAttribute("id", `first_chaoxing_assignment_${id}`); div.style.cursor = "pointer"; div.onclick = () => window.open(targetUrl, '_blank'); const icon = document.createElement("span"); icon.className = `icon-space ${iconClass}`; div.appendChild(icon); const h3 = document.createElement("h3"); h3.title = text; h3.textContent = text; div.appendChild(h3); const arrow = document.createElement("span"); arrow.className = "slide-arrow icon-h-arrow-l hide"; div.appendChild(arrow); li.appendChild(div); li.appendChild(Object.assign(document.createElement("div"), { className: "school-level" })).appendChild(document.createElement("ul")); menuListElement.prepend(li); } }; const createMenuItemLegacyNewTab = (id, name, iconClass, url) => { const list = document.querySelector("ul.funclistul"); if (list && !document.querySelector(`#first_chaoxing_assignment_${id}`)) { const existingItem = list.querySelector('li a'); if (existingItem) { const li = document.createElement("li"); li.id = `first_chaoxing_assignment_${id}`; const a = document.createElement("a"); a.href = url; a.target = "_blank"; a.title = name; const span = document.createElement("span"); span.className = iconClass; a.appendChild(span); a.appendChild(document.createTextNode(name)); li.appendChild(a); list.prepend(li); } } }; // --- 课程任务汇总功能 --- // GM_xmlhttpRequest 封装为 Promise const gmFetch = (url, options = {}) => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: options.method || 'GET', url: url, headers: options.headers || {}, responseType: options.responseType || 'text', onload: (response) => { if (response.status >= 200 && response.status < 400) { resolve(response); } else { reject(new Error(`Request failed: ${response.status}`)); } }, onerror: (error) => reject(error), ontimeout: () => reject(new Error('Request timeout')) }); }); }; // 获取所有课程列表 const fetchCourseList = async () => { try { console.log('[课程任务] 正在获取课程列表:', API_COURSE_LIST); const response = await gmFetch(API_COURSE_LIST); console.log('[课程任务] 课程列表原始响应:', response.responseText.substring(0, 1000)); const data = JSON.parse(response.responseText); console.log('[课程任务] 解析后数据:', data); if (!data.channelList) { console.log('[课程任务] 没有 channelList'); return []; } const courses = []; for (const channel of data.channelList) { const content = channel.content; if (!content) continue; // 检查是否是课程(有 course 对象) if (content.course && content.course.data && content.course.data.length > 0) { const courseInfo = content.course.data[0]; // 尝试多种方式获取 clazzId let clazzId = ''; if (content.clazz && content.clazz.data && content.clazz.data.length > 0) { clazzId = String(content.clazz.data[0].id); } else if (content.id) { clazzId = String(content.id); } else if (channel.key) { clazzId = String(channel.key); } // 只添加有 clazzId 的课程(API 需要此参数) if (courseInfo && clazzId) { courses.push({ courseId: String(courseInfo.id), courseName: courseInfo.name || '未知课程', clazzId: clazzId, cpi: String(content.cpi || ''), teacherName: courseInfo.teacherfactor || '' }); console.log(`[课程任务] 解析课程: ${courseInfo.name}, clazzId=${clazzId}`); } else if (courseInfo) { console.log(`[课程任务] 跳过无 clazzId 的课程: ${courseInfo.name}`); } } } console.log('[课程任务] 最终解析到课程:', courses.length, '个'); return courses; } catch (error) { console.error('[课程任务] 获取课程列表失败:', error); return []; } }; // 获取单个课程的活动/任务列表 (使用 JSON API) const fetchCourseActivities = async (course) => { try { // 使用正确的 JSON API 接口 const timestamp = Date.now(); const url = `https://mobilelearn.chaoxing.com/v2/apis/active/student/activelist?fid=0&courseId=${course.courseId}&classId=${course.clazzId}&showNotStartedActive=0&_=${timestamp}`; console.log(`[课程任务] 获取课程任务 ${course.courseName}:`, url); const response = await gmFetch(url); console.log(`[课程任务] ${course.courseName} 原始响应:`, response.responseText.substring(0, 300)); const data = JSON.parse(response.responseText); console.log(`[课程任务] ${course.courseName} 解析后:`, data); // 尝试多种可能的数据结构 let activeList = null; if (data.data && data.data.activeList) { activeList = data.data.activeList; } else if (data.activeList) { activeList = data.activeList; } else if (Array.isArray(data.data)) { activeList = data.data; } else if (Array.isArray(data)) { activeList = data; } if (!activeList || activeList.length === 0) { console.log(`[课程任务] ${course.courseName} 没有找到任务列表`); return []; } console.log(`[课程任务] ${course.courseName} 找到 ${activeList.length} 个任务`); const activities = activeList.map((item) => { // 活动类型映射 const typeMap = { 0: '签到', 2: '签到', 4: '抢答', 5: '主题讨论', 6: '投票', 14: '问卷', 17: '直播', 23: '随堂练习', 35: '分组任务', 42: '随堂练习', 43: '评分', 45: '拍照', 47: '作业', 64: '笔记' }; // 状态判断:status=1 进行中,status=2 已结束 const isOngoing = item.status === 1; const isEnded = item.status === 2; return { activeId: item.id || item.activeId || '', title: item.nameOne || item.name || item.title || '未知任务', type: typeMap[item.activeType] || typeMap[item.type] || `类型${item.activeType || item.type}`, status: isOngoing ? '进行中' : (isEnded ? '已结束' : '未开始'), time: item.startTime ? new Date(item.startTime).toLocaleString() : '', endTime: item.endTime ? new Date(item.endTime).toLocaleString() : '', courseName: course.courseName, courseId: course.courseId, clazzId: course.clazzId, cpi: course.cpi, finished: isEnded, ongoing: isOngoing, activeType: item.activeType || item.type }; }); return activities; } catch (error) { console.error(`[课程任务] 获取课程 ${course.courseName} 的任务失败:`, error); return []; } }; // 获取所有课程的任务汇总 const fetchAllActivities = async () => { const courses = await fetchCourseList(); console.log(`[课程任务] 找到 ${courses.length} 个课程`); if (courses.length === 0) { return []; } // 并发获取所有课程的任务(限制并发数防止请求过多) const batchSize = 5; const allActivities = []; for (let i = 0; i < courses.length; i += batchSize) { const batch = courses.slice(i, i + batchSize); const batchResults = await Promise.all( batch.map(course => fetchCourseActivities(course)) ); allActivities.push(...batchResults.flat()); } console.log(`[课程任务] 共获取 ${allActivities.length} 个任务`); return allActivities; }; function extractTasks(doc = document) { let taskElements = doc.querySelectorAll("#chaoxing-assignment-wrapper ul.nav > li"); if (taskElements.length === 0) taskElements = doc.querySelectorAll("ul.nav > li"); const tasks = Array.from(taskElements).map((task) => { var _a, _b, _c; const optionElement = task.querySelector('div[role="option"]'); let title = ""; let status = ""; let uncommitted = false; let course = ""; let leftTime = ""; if (optionElement) { title = ((_a = optionElement.querySelector("p")) == null ? void 0 : _a.textContent) || ""; const statusElement = optionElement.querySelector("span:nth-of-type(1)"); status = (statusElement == null ? void 0 : statusElement.textContent) || ""; uncommitted = (statusElement == null ? void 0 : statusElement.className.includes("status")) || false; course = ((_b = optionElement.querySelector("span:nth-of-type(2)")) == null ? void 0 : _b.textContent) || ""; leftTime = ((_c = optionElement.querySelector(".fr")) == null ? void 0 : _c.textContent) || ""; } const raw = task.getAttribute("data") || ""; let workId = ""; let courseId = ""; let clazzId = ""; if (raw) { const rawUrl = new URL(raw); const searchParams = rawUrl.searchParams; workId = searchParams.get("taskrefId") || ""; courseId = searchParams.get("courseId") || ""; clazzId = searchParams.get("clazzId") || ""; } return { type: "作业", title, status, uncommitted, course, leftTime, workId, courseId, clazzId, raw }; }); return tasks; } function extractExams(doc = document) { let examElements = doc.querySelectorAll("#chaoxing-assignment-wrapper ul.ks_list > li"); if (examElements.length === 0) examElements = doc.querySelectorAll("ul.ks_list > li"); const exams = Array.from(examElements).map((exam) => { var _a, _b, _c, _d; const dlElement = exam.querySelector("dl"); const imgElement = exam.querySelector("div.ks_pic > img"); let title = ""; let timeLeft = ""; let status = ""; let expired = false; let examId = ""; let courseId = ""; let classId = ""; if (dlElement) { title = ((_a = dlElement.querySelector("dt")) == null ? void 0 : _a.textContent) || ""; timeLeft = ((_b = dlElement.querySelector("dd")) == null ? void 0 : _b.textContent) || ""; } if (imgElement) { expired = ((_c = imgElement.getAttribute("src")) == null ? void 0 : _c.includes("ks_02")) || false; } status = ((_d = exam.querySelector("span.ks_state")) == null ? void 0 : _d.textContent) || ""; const raw = exam.getAttribute("data") || ""; if (raw) { let fullRaw = raw; if (raw.startsWith('/')) fullRaw = window.location.protocol + "//" + window.location.host + raw; try { const rawUrl = new URL(fullRaw); const searchParams = rawUrl.searchParams; examId = searchParams.get("taskrefId") || ""; courseId = searchParams.get("courseId") || ""; classId = searchParams.get("classId") || ""; } catch (e) { } } const finished = status.includes("已完成") || status.includes("待批阅"); return { type: "考试", title, status, timeLeft, expired, finished, examId, courseId, classId, raw }; }); return exams; } function extractExamsFromTable(doc = document) { let examRows = doc.querySelectorAll("table.dataTable tr.dataTr"); if (examRows.length === 0) return []; const exams = Array.from(examRows).map((row) => { const cells = row.querySelectorAll("td"); if (cells.length < 9) return null; const index = cells[0]?.textContent?.trim() || ""; const title = cells[1]?.textContent?.trim() || ""; const timeRange = cells[2]?.textContent?.trim() || ""; const duration = cells[3]?.textContent?.trim() || ""; const examStatus = cells[4]?.textContent?.trim() || ""; const answerStatus = cells[5]?.textContent?.trim() || ""; const score = cells[6]?.textContent?.trim() || "---"; const examMethod = cells[7]?.textContent?.trim() || ""; // 从操作链接中提取参数 const actionLink = cells[8]?.querySelector("a"); const actionText = actionLink?.textContent?.trim() || ""; const onclickAttr = actionLink?.getAttribute("onclick") || ""; let courseId = ""; let classId = ""; let examId = ""; let raw = ""; const goMatch = onclickAttr.match(/go\(['"]([^'"]+)['"]\)/); if (goMatch) { raw = goMatch[1]; try { const fullUrl = new URL(raw, window.location.origin); const refer = fullUrl.searchParams.get("refer") || ""; const referDecoded = decodeURIComponent(refer); const referUrl = new URL(referDecoded, window.location.origin); courseId = referUrl.searchParams.get("courseId") || fullUrl.searchParams.get("moocId") || ""; classId = referUrl.searchParams.get("classId") || fullUrl.searchParams.get("clazzid") || ""; examId = referUrl.searchParams.get("examId") || ""; } catch (e) { const moocMatch = onclickAttr.match(/moocId=(\d+)/); const clazzMatch = onclickAttr.match(/clazzid=(\d+)/); const examIdMatch = onclickAttr.match(/examId=(\d+)/); if (moocMatch) courseId = moocMatch[1]; if (clazzMatch) classId = clazzMatch[1]; if (examIdMatch) examId = examIdMatch[1]; } } const expired = examStatus.includes("已结束"); const finished = answerStatus.includes("已完成") || answerStatus.includes("待批阅"); const status = answerStatus || examStatus; const timeLeft = expired ? "已结束" : timeRange; return { type: "考试", title, status, timeLeft, timeRange, duration, examStatus, answerStatus, score, examMethod, expired, finished, examId, courseId, classId, raw }; }).filter(e => e !== null); return exams; } const API_VISIT_COURSE = "https://mooc1.chaoxing.com/visit/stucoursemiddle?ismooc2=1"; const API_EXAM_LIST = "https://mooc1.chaoxing.com/exam-ans/exam/test/examcode/examlist?edition=1&nohead=0&fid="; const API_OPEN_EXAM = "https://mooc1-api.chaoxing.com/exam-ans/exam/test/examcode/examnotes"; const cssLoader = (e) => { const t = GM_getResourceText(e); return GM_addStyle(t), t; }; cssLoader("VuetifyStyle"); // --- Vue Components --- const _sfc_main$2 = /* @__PURE__ */ vue.defineComponent({ __name: "tasks-list", setup(__props) { const extractedData = extractTasks(); const headers = [ { key: "title", title: "作业名称" }, { key: "course", title: "课程" }, { key: "leftTime", title: "剩余时间" }, { key: "status", title: "状态" }, { key: "action", title: "", sortable: false } ]; const search = vue.ref(""); const getCourseLinkHref = (item) => { const courseId = item.courseId; const clazzId = item.clazzId; const requestUrl = new URL(API_VISIT_COURSE); requestUrl.searchParams.append("courseid", courseId); requestUrl.searchParams.append("clazzid", clazzId); requestUrl.searchParams.append("pageHeader", "8"); return requestUrl.href; }; return (_ctx, _cache) => { const _component_v_text_field = vue.resolveComponent("v-text-field"); const _component_v_btn = vue.resolveComponent("v-btn"); const _component_v_data_table = vue.resolveComponent("v-data-table"); const _component_v_card = vue.resolveComponent("v-card"); return vue.openBlock(), vue.createBlock(_component_v_card, { title: "作业列表", variant: "flat" }, { text: vue.withCtx(() => [ vue.createVNode(_component_v_text_field, { modelValue: search.value, "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => search.value = $event), label: "搜索", "prepend-inner-icon": "search", variant: "outlined", "hide-details": "", "single-line": "" }) ]), default: vue.withCtx(() => [ vue.createVNode(_component_v_data_table, { items: vue.unref(extractedData), search: search.value, hover: "", headers, sticky: "", "items-per-page": "-1", "hide-default-footer": "" }, { "item.action": vue.withCtx(({ item }) => [ vue.createVNode(_component_v_btn, { variant: item.uncommitted ? "tonal" : "plain", color: "primary", href: getCourseLinkHref(item), target: "_blank" }, { default: vue.withCtx(() => [vue.createTextVNode(vue.toDisplayString(item.uncommitted ? "立即完成" : "查看详情"), 1)]) }, 1032, ["variant", "href"]) ]) }, 8, ["items", "search"]) ]) }); }; } }); const _sfc_main$1 = /* @__PURE__ */ vue.defineComponent({ __name: "App", setup(__props) { return (_ctx, _cache) => { return vue.openBlock(), vue.createBlock(_sfc_main$2); }; } }); cssLoader("material-design-icons-iconfont/dist/material-design-icons.css"); // --- Vuetify Helper Functions (恢复原版代码) --- // 这些是原脚本为了适配图标组件而手写的一堆辅助函数,之前被误删 function isObject(obj) { return obj !== null && typeof obj === "object" && !Array.isArray(obj); } function pick(obj, paths) { const found = {}; const keys = new Set(Object.keys(obj)); for (const path of paths) { if (keys.has(path)) { found[path] = obj[path]; } } return found; } function mergeDeep() { let source = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}; let target = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {}; let arrayFn = arguments.length > 2 ? arguments[2] : void 0; const out = {}; for (const key in source) { out[key] = source[key]; } for (const key in target) { const sourceProperty = source[key]; const targetProperty = target[key]; if (isObject(sourceProperty) && isObject(targetProperty)) { out[key] = mergeDeep(sourceProperty, targetProperty, arrayFn); continue; } if (Array.isArray(sourceProperty) && Array.isArray(targetProperty) && arrayFn) { out[key] = arrayFn(sourceProperty, targetProperty); continue; } out[key] = targetProperty; } return out; } function toKebabCase() { let str = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : ""; if (toKebabCase.cache.has(str)) return toKebabCase.cache.get(str); const kebab = str.replace(/[^a-z]/gi, "-").replace(/\B([A-Z])/g, "-$1").toLowerCase(); toKebabCase.cache.set(str, kebab); return kebab; } toKebabCase.cache = /* @__PURE__ */ new Map(); function consoleWarn(message) { vue.warn(`Vuetify: ${message}`); } function propsFactory(props, source) { return (defaults) => { return Object.keys(props).reduce((obj, prop) => { const isObjectDefinition = typeof props[prop] === "object" && props[prop] != null && !Array.isArray(props[prop]); const definition = isObjectDefinition ? props[prop] : { type: props[prop] }; if (defaults && prop in defaults) { obj[prop] = { ...definition, default: defaults[prop] }; } else { obj[prop] = definition; } if (source && !obj[prop].source) { obj[prop].source = source; } return obj; }, {}); }; } const DefaultsSymbol = Symbol.for("vuetify:defaults"); function injectDefaults() { const defaults = vue.inject(DefaultsSymbol); if (!defaults) throw new Error("[Vuetify] Could not find defaults instance"); return defaults; } function propIsDefined(vnode, prop) { var _a, _b; return typeof ((_a = vnode.props) == null ? void 0 : _a[prop]) !== "undefined" || typeof ((_b = vnode.props) == null ? void 0 : _b[toKebabCase(prop)]) !== "undefined"; } function internalUseDefaults() { let props = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}; let name = arguments.length > 1 ? arguments[1] : void 0; let defaults = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : injectDefaults(); const vm = getCurrentInstance("useDefaults"); name = name ?? vm.type.name ?? vm.type.__name; if (!name) { throw new Error("[Vuetify] Could not determine component name"); } const componentDefaults = vue.computed(() => { var _a; return (_a = defaults.value) == null ? void 0 : _a[props._as ?? name]; }); const _props = new Proxy(props, { get(target, prop) { var _a, _b, _c, _d; const propValue = Reflect.get(target, prop); if (prop === "class" || prop === "style") { return [(_a = componentDefaults.value) == null ? void 0 : _a[prop], propValue].filter((v) => v != null); } else if (typeof prop === "string" && !propIsDefined(vm.vnode, prop)) { return ((_b = componentDefaults.value) == null ? void 0 : _b[prop]) ?? ((_d = (_c = defaults.value) == null ? void 0 : _c.global) == null ? void 0 : _d[prop]) ?? propValue; } return propValue; } }); const _subcomponentDefaults = vue.shallowRef(); vue.watchEffect(() => { if (componentDefaults.value) { const subComponents = Object.entries(componentDefaults.value).filter((_ref) => { let [key] = _ref; return key.startsWith(key[0].toUpperCase()); }); _subcomponentDefaults.value = subComponents.length ? Object.fromEntries(subComponents) : void 0; } else { _subcomponentDefaults.value = void 0; } }); function provideSubDefaults() { const injected = injectSelf(DefaultsSymbol, vm); vue.provide(DefaultsSymbol, vue.computed(() => { return _subcomponentDefaults.value ? mergeDeep((injected == null ? void 0 : injected.value) ?? {}, _subcomponentDefaults.value) : injected == null ? void 0 : injected.value; })); } return { props: _props, provideSubDefaults }; } function defineComponent(options) { options._setup = options._setup ?? options.setup; if (!options.name) { consoleWarn("The component is missing an explicit name, unable to generate default prop value"); return options; } if (options._setup) { options.props = propsFactory(options.props ?? {}, options.name)(); const propKeys = Object.keys(options.props).filter((key) => key !== "class" && key !== "style"); options.filterProps = function filterProps(props) { return pick(props, propKeys); }; options.props._as = String; options.setup = function setup(props, ctx) { const defaults = injectDefaults(); if (!defaults.value) return options._setup(props, ctx); const { props: _props, provideSubDefaults } = internalUseDefaults(props, props._as ?? options.name, defaults); const setupBindings = options._setup(_props, ctx); provideSubDefaults(); return setupBindings; }; } return options; } function genericComponent() { let exposeDefaults = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : true; return (options) => (exposeDefaults ? defineComponent : vue.defineComponent)(options); } function getCurrentInstance(name, message) { const vm = vue.getCurrentInstance(); if (!vm) { throw new Error(`[Vuetify] ${name} ${"must be called from inside a setup function"}`); } return vm; } function injectSelf(key) { let vm = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : getCurrentInstance("injectSelf"); const { provides } = vm; if (provides && key in provides) { return provides[key]; } return void 0; } const IconValue = [String, Function, Object, Array]; const makeIconProps = propsFactory({ icon: { type: IconValue }, tag: { type: String, required: true } }, "icon"); genericComponent()({ name: "VComponentIcon", props: makeIconProps(), setup(props, _ref) { let { slots } = _ref; return () => { const Icon = props.icon; return vue.createVNode(props.tag, null, { default: () => { var _a; return [props.icon ? vue.createVNode(Icon, null, null) : (_a = slots.default) == null ? void 0 : _a.call(slots)]; } }); }; } }); defineComponent({ name: "VSvgIcon", inheritAttrs: false, props: makeIconProps(), setup(props, _ref2) { let { attrs } = _ref2; return () => { return vue.createVNode(props.tag, vue.mergeProps(attrs, { "style": null }), { default: () => [vue.createVNode("svg", { "class": "v-icon__svg", "xmlns": "http://www.w3.org/2000/svg", "viewBox": "0 0 24 24", "role": "img", "aria-hidden": "true" }, [Array.isArray(props.icon) ? props.icon.map((path) => Array.isArray(path) ? vue.createVNode("path", { "d": path[0], "fill-opacity": path[1] }, null) : vue.createVNode("path", { "d": path }, null)) : vue.createVNode("path", { "d": props.icon }, null)])] }); }; } }); const VLigatureIcon = defineComponent({ name: "VLigatureIcon", props: makeIconProps(), setup(props) { return () => { return vue.createVNode(props.tag, null, { default: () => [props.icon] }); }; } }); defineComponent({ name: "VClassIcon", props: makeIconProps(), setup(props) { return () => { return vue.createVNode(props.tag, { "class": props.icon }, null); }; } }); const aliases = { collapse: "keyboard_arrow_up", complete: "check", cancel: "cancel", close: "close", delete: "cancel", clear: "cancel", success: "check_circle", info: "info", warning: "priority_high", error: "warning", prev: "chevron_left", next: "chevron_right", checkboxOn: "check_box", checkboxOff: "check_box_outline_blank", checkboxIndeterminate: "indeterminate_check_box", delimiter: "fiber_manual_record", sortAsc: "arrow_upward", sortDesc: "arrow_downward", expand: "keyboard_arrow_down", menu: "menu", subgroup: "arrow_drop_down", dropdown: "arrow_drop_down", radioOn: "radio_button_checked", radioOff: "radio_button_unchecked", edit: "edit", ratingEmpty: "star_border", ratingFull: "star", ratingHalf: "star_half", loading: "cached", first: "first_page", last: "last_page", unfold: "unfold_more", file: "attach_file", plus: "add", minus: "remove", calendar: "event", treeviewCollapse: "arrow_drop_down", treeviewExpand: "arrow_right", eyeDropper: "colorize" }; const md = { component: (props) => vue.h(VLigatureIcon, { ...props, class: "material-icons" }) }; const _sfc_main = /* @__PURE__ */ vue.defineComponent({ __name: "exams-list", setup(__props) { const extractedData = extractExams(); const headers = [ { key: "title", title: "考试名称" }, { key: "timeLeft", title: "剩余时间" }, { key: "status", title: "状态" }, { key: "action", title: "", sortable: false } ]; const search = vue.ref(""); const getCourseLinkHref = (item) => { const requestUrl = new URL(API_OPEN_EXAM); requestUrl.searchParams.append("courseId", item.courseId); requestUrl.searchParams.append("classId", item.classId); requestUrl.searchParams.append("examId", item.examId); return requestUrl.href; }; return (_ctx, _cache) => { const _component_v_text_field = vue.resolveComponent("v-text-field"); const _component_v_btn = vue.resolveComponent("v-btn"); const _component_v_data_table = vue.resolveComponent("v-data-table"); const _component_v_card = vue.resolveComponent("v-card"); return vue.openBlock(), vue.createBlock(_component_v_card, { title: "考试列表", variant: "flat" }, { text: vue.withCtx(() => [ vue.createVNode(_component_v_text_field, { modelValue: search.value, "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => search.value = $event), label: "搜索", "prepend-inner-icon": "search", variant: "outlined", "hide-details": "", "single-line": "" }) ]), default: vue.withCtx(() => [ vue.createVNode(_component_v_data_table, { items: vue.unref(extractedData), search: search.value, hover: "", headers, sticky: "", "items-per-page": "-1", "hide-default-footer": "" }, { "item.action": vue.withCtx(({ item }) => [ vue.createVNode(_component_v_btn, { variant: item.finished || item.expired ? "plain" : "tonal", color: "primary", href: getCourseLinkHref(item), target: "_blank" }, { default: vue.withCtx(() => [vue.createTextVNode(vue.toDisplayString(item.finished || item.expired ? "查看详情" : "前往考试"), 1)]) }, 1032, ["variant", "href"]) ]) }, 8, ["items", "search"]) ]) }); }; } }); const _sfc_todo = /* @__PURE__ */ vue.defineComponent({ __name: "todo-list", setup(__props) { const allTodoItems = vue.ref([]); // 所有待办项 const loading = vue.ref(true); const search = vue.ref(""); const showActivities = vue.ref(true); // 是否显示课程任务(默认开启) const showUrgentOnly = vue.ref(false); // 是否只显示紧急任务 // 计算属性:根据开关过滤显示的列表 const todoList = vue.computed(() => { let list = allTodoItems.value; // 如果启用紧急模式,只显示紧急任务 if (showUrgentOnly.value) { return urgentTasks.value; } // 如果关闭课程任务显示,过滤掉 isActivity 项 if (!showActivities.value) { list = list.filter(item => !item.isActivity); } return list; }); const headers = [ { key: "type", title: "类型" }, { key: "title", title: "任务名称" }, { key: "course", title: "课程" }, { key: "info", title: "截止/剩余时间" }, { key: "status", title: "状态" }, { key: "action", title: "", sortable: false } ]; // 检测24小时内截止的紧急任务 const urgentTasks = vue.computed(() => { return allTodoItems.value.filter(item => { // 获取剩余时间字符串(作业用 leftTime,考试用 timeLeft) const timeStr = item.leftTime || item.timeLeft || item.info || ''; // 尝试解析截止时间 if (timeStr.includes('小时')) { const hours = parseInt(timeStr); return !isNaN(hours) && hours <= 24; } if (timeStr.includes('天')) { const days = parseInt(timeStr); return !isNaN(days) && days < 1; } if (timeStr.includes('分钟') || timeStr.includes('分')) { return true; // 还剩分钟肯定是紧急的 } // 进行中的课程任务也算紧急 if (item.isActivity && item.status === '进行中') { return true; } return false; }); }); const getLink = (item) => { if (item.isActivity) { // 课程活动跳转到课程页面 const requestUrl = new URL(API_VISIT_COURSE); requestUrl.searchParams.append("courseid", item.courseId); requestUrl.searchParams.append("clazzid", item.clazzId); requestUrl.searchParams.append("pageHeader", "0"); // 任务页面 return requestUrl.href; } else if (item.type === "作业") { const requestUrl = new URL(API_VISIT_COURSE); requestUrl.searchParams.append("courseid", item.courseId); requestUrl.searchParams.append("clazzid", item.clazzId); requestUrl.searchParams.append("pageHeader", "8"); return requestUrl.href; } else { const requestUrl = new URL(API_OPEN_EXAM); requestUrl.searchParams.append("courseId", item.courseId); requestUrl.searchParams.append("classId", item.classId); requestUrl.searchParams.append("examId", item.examId); return requestUrl.href; } }; vue.onMounted(async () => { const currentTasks = extractTasks(); const pendingTasks = currentTasks.filter(t => t.uncommitted).map(t => ({ ...t, info: t.leftTime })); let pendingExams = []; const seenExamIds = new Set(); try { const res1 = await fetch('https://mooc1-api.chaoxing.com/exam-ans/exam/phone/examcode'); const text1 = await res1.text(); const parser1 = new DOMParser(); const doc1 = parser1.parseFromString(text1, 'text/html'); const exams1 = extractExams(doc1); exams1.filter(e => !e.finished && !e.expired).forEach(e => { const key = e.examId || e.title; if (!seenExamIds.has(key)) { seenExamIds.add(key); pendingExams.push({ ...e, course: "考试课程", info: e.timeLeft }); } }); } catch (e) { console.error("Fetch exams from old API failed", e); } try { const res2 = await fetch(API_EXAM_LIST); const text2 = await res2.text(); const parser2 = new DOMParser(); const doc2 = parser2.parseFromString(text2, 'text/html'); const exams2 = extractExamsFromTable(doc2); exams2.filter(e => !e.finished && !e.expired).forEach(e => { const key = e.examId || e.title; if (!seenExamIds.has(key)) { seenExamIds.add(key); pendingExams.push({ ...e, course: "考试课程", info: e.timeLeft }); } }); } catch (e) { console.error("Fetch exams from new API failed", e); } // 获取进行中的课程任务(签到、讨论等) let ongoingActivities = []; try { const courses = await fetchCourseList(); console.log('[待办任务] 获取到课程:', courses.length, '个'); // 限制并发数 const batchSize = 5; for (let i = 0; i < courses.length; i += batchSize) { const batch = courses.slice(i, i + batchSize); const batchResults = await Promise.all( batch.map(course => fetchCourseActivities(course)) ); batchResults.flat() .filter(activity => activity.ongoing) .forEach(activity => { ongoingActivities.push({ type: activity.type, title: activity.title, course: activity.courseName, info: activity.endTime || '进行中', status: '进行中', courseId: activity.courseId, clazzId: activity.clazzId, isActivity: true }); }); } console.log('[待办任务] 进行中任务:', ongoingActivities.length, '个'); } catch (e) { console.error('[待办任务] 获取课程任务失败:', e); } // 排序:作业和考试在前,课程任务在后 allTodoItems.value = [...pendingTasks, ...pendingExams, ...ongoingActivities]; loading.value = false; }); return (_ctx, _cache) => { const _component_v_text_field = vue.resolveComponent("v-text-field"); const _component_v_switch = vue.resolveComponent("v-switch"); const _component_v_btn = vue.resolveComponent("v-btn"); const _component_v_data_table = vue.resolveComponent("v-data-table"); const _component_v_card = vue.resolveComponent("v-card"); const _component_v_chip = vue.resolveComponent("v-chip"); const _component_v_row = vue.resolveComponent("v-row"); const _component_v_col = vue.resolveComponent("v-col"); const _component_v_alert = vue.resolveComponent("v-alert"); return vue.openBlock(), vue.createBlock(_component_v_card, { title: showUrgentOnly.value ? "🚨 紧急任务" : "待办任务", variant: "flat" }, { text: vue.withCtx(() => [ // 紧急模式下显示"返回全部"按钮 showUrgentOnly.value ? vue.createVNode(_component_v_alert, { type: "info", variant: "tonal", class: "mb-4" }, { default: () => [ vue.createVNode("div", { class: "d-flex align-center justify-space-between" }, [ vue.createVNode("span", {}, `正在查看 ${urgentTasks.value.length} 个紧急任务`), vue.createVNode(_component_v_btn, { variant: "outlined", size: "small", onClick: () => { showUrgentOnly.value = false; } }, { default: () => [vue.createTextVNode("← 返回全部待办")] }) ]) ] }) : ( // 非紧急模式下显示紧急任务提醒 urgentTasks.value.length > 0 ? vue.createVNode(_component_v_alert, { type: "warning", variant: "tonal", class: "mb-4", prominent: true, icon: "warning" }, { default: () => [ vue.createVNode("div", { class: "d-flex align-center justify-space-between" }, [ vue.createVNode("div", {}, [ vue.createVNode("div", { class: "font-weight-bold" }, `⚠️ 有 ${urgentTasks.value.length} 个任务即将到期!`), vue.createVNode("div", { class: "text-caption" }, urgentTasks.value.slice(0, 2).map(t => t.title).join('、') + (urgentTasks.value.length > 2 ? ` 等${urgentTasks.value.length}个任务` : '') ) ]), vue.createVNode(_component_v_btn, { variant: "elevated", color: "warning", size: "small", onClick: () => { showUrgentOnly.value = true; } }, { default: () => [vue.createTextVNode("去查看 →")] }) ]) ] }) : null ), vue.createVNode(_component_v_row, { align: "center", class: "mb-2" }, { default: () => [ vue.createVNode(_component_v_col, { cols: "8" }, { default: () => [ vue.createVNode(_component_v_text_field, { modelValue: search.value, "onUpdate:modelValue": ($event) => search.value = $event, label: "搜索待办", "prepend-inner-icon": "search", variant: "outlined", "hide-details": "", "single-line": "", density: "compact" }) ] }), vue.createVNode(_component_v_col, { cols: "4" }, { default: () => [ vue.createVNode(_component_v_switch, { modelValue: showActivities.value, "onUpdate:modelValue": ($event) => showActivities.value = $event, label: "显示课程任务", color: "primary", "hide-details": "", density: "compact" }) ] }) ] }) ]), default: vue.withCtx(() => [ vue.createVNode(_component_v_data_table, { items: todoList.value, loading: loading.value, search: search.value, hover: "", headers, sticky: "", "items-per-page": "-1", "hide-default-footer": "" }, { "item.type": vue.withCtx(({ item }) => [ vue.createVNode(_component_v_chip, { color: item.isActivity ? 'orange' : (item.type === '作业' ? 'blue' : 'purple'), size: 'small', label: '' }, { default: () => [vue.createTextVNode(item.type)] }) ]), "item.action": vue.withCtx(({ item }) => [ vue.createVNode(_component_v_btn, { variant: "tonal", color: "error", href: getLink(item), target: "_blank" }, { default: vue.withCtx(() => [vue.createTextVNode("立即去办")]) }, 8, ["href"]) ]) }, 8, ["items", "loading", "search"]) ]) }); }; } }); // 课程任务列表组件 const _sfc_activities = /* @__PURE__ */ vue.defineComponent({ __name: "activities-list", setup(__props) { const activitiesList = vue.ref([]); const loading = vue.ref(true); const search = vue.ref(""); const progress = vue.ref(""); const headers = [ { key: "courseName", title: "课程" }, { key: "title", title: "任务名称" }, { key: "type", title: "类型" }, { key: "endTime", title: "结束时间" }, { key: "status", title: "状态" }, { key: "action", title: "", sortable: false } ]; const getCourseLink = (item) => { const requestUrl = new URL(API_VISIT_COURSE); requestUrl.searchParams.append("courseid", item.courseId); requestUrl.searchParams.append("clazzid", item.clazzId); return requestUrl.href; }; vue.onMounted(async () => { try { progress.value = "正在获取课程列表..."; const courses = await fetchCourseList(); progress.value = `找到 ${courses.length} 个课程,正在获取任务...`; const allActivities = []; const batchSize = 3; for (let i = 0; i < courses.length; i += batchSize) { const batch = courses.slice(i, i + batchSize); progress.value = `正在获取课程任务 (${Math.min(i + batchSize, courses.length)}/${courses.length})...`; const batchResults = await Promise.all( batch.map(course => fetchCourseActivities(course)) ); allActivities.push(...batchResults.flat()); } activitiesList.value = allActivities; progress.value = ""; } catch (error) { console.error('[课程任务] 加载失败:', error); progress.value = "加载失败,请刷新重试"; } finally { loading.value = false; } }); return (_ctx, _cache) => { const _component_v_text_field = vue.resolveComponent("v-text-field"); const _component_v_chip = vue.resolveComponent("v-chip"); const _component_v_btn = vue.resolveComponent("v-btn"); const _component_v_data_table = vue.resolveComponent("v-data-table"); const _component_v_card = vue.resolveComponent("v-card"); const _component_v_progress_linear = vue.resolveComponent("v-progress-linear"); return vue.openBlock(), vue.createBlock(_component_v_card, { title: "课程任务列表", variant: "flat", subtitle: progress.value }, { text: vue.withCtx(() => [ vue.createVNode(_component_v_text_field, { modelValue: search.value, "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => search.value = $event), label: "搜索", "prepend-inner-icon": "search", variant: "outlined", "hide-details": "", "single-line": "" }), loading.value ? vue.createVNode(_component_v_progress_linear, { indeterminate: "", color: "primary", class: "mt-4" }) : null ]), default: vue.withCtx(() => [ vue.createVNode(_component_v_data_table, { items: activitiesList.value, loading: loading.value, search: search.value, hover: "", headers, sticky: "", "items-per-page": "-1", "hide-default-footer": "" }, { "item.type": vue.withCtx(({ item }) => [ vue.createVNode(_component_v_chip, { color: 'teal', size: 'small', label: '' }, { default: () => [vue.createTextVNode(item.type || '活动')] }) ]), "item.status": vue.withCtx(({ item }) => [ vue.createVNode(_component_v_chip, { color: item.ongoing ? 'orange' : 'grey', size: 'small', label: '' }, { default: () => [vue.createTextVNode(item.status)] }) ]), "item.action": vue.withCtx(({ item }) => [ vue.createVNode(_component_v_btn, { variant: item.finished ? "plain" : "tonal", color: "primary", href: getCourseLink(item), target: "_blank" }, { default: vue.withCtx(() => [vue.createTextVNode(item.finished ? "查看详情" : "前往完成")]) }, 8, ["variant", "href"]) ]) }, 8, ["items", "loading", "search"]) ]) }, 8, ["subtitle"]); }; } }); // --- 便当盒仪表盘组件 --- const _sfc_dashboard = /* @__PURE__ */ vue.defineComponent({ __name: "dashboard", setup(__props) { const fetchUserName = async () => { const userNameEl = document.querySelector('.user-name'); if (userNameEl && userNameEl.textContent.trim()) { return userNameEl.textContent.trim(); } const personalNameEl = document.querySelector('.personalName'); if (personalNameEl && personalNameEl.textContent.trim()) { return personalNameEl.textContent.trim(); } const extractTextById = (html, id) => { const idPattern = new RegExp(`id=["']?${id}["']?[^>]*>([^<]*)`, 'i'); const match = html.match(idPattern); return match ? match[1].trim() : ''; }; const extractNumberById = (html, id) => { const idText = extractTextById(html, id); if (idText) { const numeric = idText.match(/\d+(\.\d+)?/); return numeric ? numeric[0] : ''; } const loosePattern = new RegExp(`${id}[^\\d]*(\\d+(?:\\.\\d+)?)`, 'i'); const looseMatch = html.match(loosePattern); return looseMatch ? looseMatch[1] : ''; }; const extractPairNearLabel = (html, label) => { const pairPattern = new RegExp(`${label}[^\\d]{0,50}(\\d+)\\s*\\/\\s*(\\d+)`, 'i'); const match = html.match(pairPattern); return match ? { first: match[1], second: match[2] } : null; }; const extractNumberNearLabel = (html, label) => { const numberPattern = new RegExp(`${label}[^\\d]{0,50}(\\d+(?:\\.\\d+)?)`, 'i'); const match = html.match(numberPattern); return match ? match[1] : ''; }; const getSiblingText = (element, selector) => { const parent = element?.closest('li') || element?.parentElement; if (!parent) return ''; const target = parent.querySelector(selector); return target?.textContent.trim() || ''; }; const extractTaskPairFromDom = (doc) => { const label = Array.from(doc.querySelectorAll('p')).find((el) => el.textContent.includes('完成进度')); if (!label) return null; const text = getSiblingText(label, 'h2'); const match = text.match(/(\d+)\s*\/\s*(\d+)/); return match ? { first: match[1], second: match[2] } : null; }; const extractRankFromDom = (doc) => { const label = Array.from(doc.querySelectorAll('p')).find((el) => el.textContent.includes('当前排名')); if (!label) return ''; const text = getSiblingText(label, 'h2'); const match = text.match(/(\d+)/); return match ? match[1] : ''; }; const extractPointFromDom = (doc) => { const title = Array.from(doc.querySelectorAll('h2')).find((el) => el.textContent.includes('课程积分')); if (!title) return ''; const card = title.closest('.whiteBg') || title.parentElement; const value = card?.querySelector('.strong, #point'); return value?.textContent.trim() || ''; }; const extractQuizPairFromDom = (doc) => { const label = Array.from(doc.querySelectorAll('.statistics-bar-list .left-label')) .find((el) => el.textContent.includes('章节测验')); if (!label) return null; const item = label.closest('li'); const rightRate = item?.querySelector('.right-rate'); const text = rightRate?.textContent.trim() || ''; const match = text.match(/(\d+)\s*\/\s*(\d+)/); return match ? { first: match[1], second: match[2] } : null; }; return new Promise((resolve) => { if (typeof GM_xmlhttpRequest !== 'undefined') { GM_xmlhttpRequest({ method: 'GET', url: 'https://i.chaoxing.com/base', timeout: 5000, onload: (response) => { try { const parser = new DOMParser(); const doc = parser.parseFromString(response.responseText, 'text/html'); const nameEl = doc.querySelector('.user-name') || doc.querySelector('.personalName'); if (nameEl && nameEl.textContent.trim()) { resolve(nameEl.textContent.trim()); } else { resolve('同学'); } } catch (e) { resolve('同学'); } }, onerror: () => resolve('同学'), ontimeout: () => resolve('同学') }); } else { resolve('同学'); } }); }; const getGreeting = () => { const hour = new Date().getHours(); if (hour < 6) return '夜深了'; if (hour < 12) return '早上好'; if (hour < 14) return '中午好'; if (hour < 18) return '下午好'; return '晚上好'; }; const getFormattedDate = () => { const now = new Date(); const weekdays = ['日', '一', '二', '三', '四', '五', '六']; return `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日 · 星期${weekdays[now.getDay()]}`; }; const userName = vue.ref('同学'); // 先显示默认值 const greeting = vue.ref(getGreeting()); const dateInfo = vue.ref(getFormattedDate()); // 异步获取用户名 fetchUserName().then(name => { userName.value = name; }); const loading = vue.ref({ todo: true, homework: false, exam: false, activities: true }); const todoItems = vue.ref([]); const homeworkItems = vue.ref([]); const examItems = vue.ref([]); const activitiesItems = vue.ref([]); const urgentTasks = vue.ref([]); // 课程进度数据 const courseProgressItems = vue.ref([]); const loadingProgress = vue.ref(false); const progressLastUpdate = vue.ref(null); const getAllCourses = async () => { return new Promise((resolve) => { // 使用与课程任务相同的 API const courseListUrl = 'https://mooc1-api.chaoxing.com/mycourse/backclazzdata?view=json&mcode='; if (typeof GM_xmlhttpRequest !== 'undefined') { GM_xmlhttpRequest({ method: 'GET', url: courseListUrl, onload: (response) => { try { console.log('[课程进度] 课程列表响应前200字符:', response.responseText.substring(0, 200)); const data = JSON.parse(response.responseText); const courses = []; if (data && data.channelList) { data.channelList.forEach(channel => { if (channel.cataid === '100000002' && channel.content && channel.content.course) { const courseData = channel.content.course.data; if (courseData && Array.isArray(courseData)) { courseData.forEach(course => { courses.push({ courseId: course.id, clazzId: channel.key, cpi: channel.cpi, courseName: course.name, teacherName: course.teacherfactor || '' }); }); } } }); } console.log('[课程进度] 解析到课程:', courses.length, '个'); resolve(courses); } catch (e) { console.error('[课程进度] 解析课程列表失败:', e); resolve([]); } }, onerror: (err) => { console.error('[课程进度] 请求课程列表失败:', err); resolve([]); } }); } else { resolve([]); } }); }; // 获取课程进度 - 从学习记录页面抓取任务点和排名等信息 const getCourseProgress = async (course) => { const studyDataUrl = `https://stat2-ans.chaoxing.com/study-data/index?clazzid=${course.clazzId}&courseid=${course.courseId}&cpi=${course.cpi}&ut=s`; const courseEntryUrl = `https://mooc1.chaoxing.com/visit/stucoursemiddle?ismooc2=1&courseid=${course.courseId}&clazzid=${course.clazzId}&pageHeader=6`; const getProgressFromChapter = () => new Promise((resolve) => { const chapterUrl = `https://mooc2-ans.chaoxing.com/mooc2-ans/mycourse/studentcourse?clazzid=${course.clazzId}&courseid=${course.courseId}&cpi=${course.cpi}`; if (typeof GM_xmlhttpRequest === 'undefined') { resolve({ totalTasks: 0, completedTasks: 0, completionRate: '点击查看', shouldFilter: false }); return; } GM_xmlhttpRequest({ method: 'GET', url: chapterUrl, timeout: 10000, onload: (response) => { let completedTasks = 0; let totalTasks = 0; let completionRate = '点击查看'; let shouldFilter = false; try { const parser = new DOMParser(); const doc = parser.parseFromString(response.responseText, 'text/html'); const warnTxt = doc.querySelector('.top-tips .warn-txt') || doc.querySelector('.warn-txt'); if (warnTxt && warnTxt.textContent.includes('本课程已结课')) { shouldFilter = true; console.log('[课程进度] 过滤已结课课程:', course.courseName); } const headEl = doc.querySelector('.chapter_head h2.xs_head_name'); if (headEl) { const text = headEl.textContent || ''; const match = text.match(/(\d+)\s*\/\s*(\d+)/); if (match) { completedTasks = parseInt(match[1], 10) || 0; totalTasks = parseInt(match[2], 10) || 0; completionRate = totalTasks > 0 ? `${Math.round((completedTasks / totalTasks) * 100)}%` : '0%'; } } if (response.responseText.includes('暂无任务点') || response.responseText.includes('没有任务点')) { shouldFilter = true; console.log('[课程进度] 过滤无任务点课程:', course.courseName); } } catch (e) { console.log('[课程进度]', course.courseName, '章节解析失败'); } resolve({ totalTasks, completedTasks, completionRate, shouldFilter }); }, onerror: () => resolve({ totalTasks: 0, completedTasks: 0, completionRate: '点击查看', shouldFilter: false }), ontimeout: () => resolve({ totalTasks: 0, completedTasks: 0, completionRate: '点击查看', shouldFilter: false }) }); }); return new Promise((resolve) => { if (typeof GM_xmlhttpRequest === 'undefined') { resolve({ ...course, totalTasks: 0, completedTasks: 0, completionRate: '点击查看', unfinishedTasks: [], unfinishedCount: 0, studyDataUrl, isComplete: false, shouldFilter: false, courseScore: '--', chapterQuiz: '--', ranking: '--' }); return; } GM_xmlhttpRequest({ method: 'GET', url: studyDataUrl, timeout: 10000, onload: async (studyResponse) => { let completedTasks = 0; let totalTasks = 0; let completionRate = '点击查看'; let shouldFilter = false; let courseScore = '--'; let chapterQuiz = '--'; let ranking = '--'; try { const parser = new DOMParser(); let studyDoc = parser.parseFromString(studyResponse.responseText, 'text/html'); let responseText = studyResponse.responseText; const iframe = studyDoc.getElementById('frame_content-cj'); if (iframe && iframe.getAttribute('srcdoc')) { try { const srcdoc = iframe.getAttribute('srcdoc'); const iframeDoc = new DOMParser().parseFromString(srcdoc, 'text/html'); if (iframeDoc) { studyDoc = iframeDoc; responseText = srcdoc; console.log('[学习记录]', course.courseName, '成功解析 iframe 数据源'); } } catch (e) { console.warn('[学习记录]', course.courseName, '解析 iframe 失败:', e); } } if (responseText.includes('本课程已结课')) { shouldFilter = true; console.log('[课程进度] 过滤已结课课程:', course.courseName); } const extractDataFromDomByLabel = (doc, label) => { if (!doc) return null; const candidates = Array.from(doc.querySelectorAll('*')).filter(el => el.children.length === 0 && el.textContent.includes(label) ); for (const el of candidates) { let container = el.parentElement; let attempts = 3; while (container && attempts > 0) { const text = container.textContent.trim(); const textAfterLabel = text.substring(text.indexOf(label) + label.length); const match = textAfterLabel.match(/[::\s]*(\d+(?:\.\d+)?)/); if (match) return match[1]; container = container.parentElement; attempts--; } } return null; }; const completedEl = studyDoc.querySelector('#jobfinish'); const totalEl = studyDoc.querySelector('#jobPublish'); const percentEl = studyDoc.querySelector('#jobPer'); const rankEl = studyDoc.querySelector('#jobRank'); const pointEl = studyDoc.querySelector('#point'); const testNumEl = studyDoc.querySelector('#testNum'); const publishTestNumEl = studyDoc.querySelector('#publishTestNum'); const progressPair = extractPairNearLabel(responseText, '完成进度'); const progressPairFromDom = extractTaskPairFromDom(studyDoc); const completedText = completedEl?.textContent.trim() || extractNumberById(responseText, 'jobfinish') || progressPairFromDom?.first || progressPair?.first || (extractDataFromDomByLabel(studyDoc, '完成进度') ? null : null); const totalText = totalEl?.textContent.trim() || extractNumberById(responseText, 'jobPublish') || progressPairFromDom?.second || progressPair?.second; const percentText = percentEl?.textContent.trim() || extractNumberById(responseText, 'jobPer'); const rankText = extractDataFromDomByLabel(studyDoc, '当前排名') || extractDataFromDomByLabel(studyDoc, '班级排名') || extractDataFromDomByLabel(studyDoc, '排名') || rankEl?.textContent.trim() || extractNumberById(responseText, 'jobRank') || extractRankFromDom(studyDoc) || extractNumberNearLabel(responseText, '当前排名'); const pointText = extractDataFromDomByLabel(studyDoc, '课程积分') || pointEl?.textContent.trim() || extractNumberById(responseText, 'point') || extractPointFromDom(studyDoc) || extractNumberNearLabel(responseText, '课程积分'); const testNumText = testNumEl?.textContent.trim() || extractNumberById(responseText, 'testNum'); const publishTestNumText = publishTestNumEl?.textContent.trim() || extractNumberById(responseText, 'publishTestNum'); const quizPair = extractPairNearLabel(responseText, '章节测验'); let quizFromLabel = null; const quizLabelNum = extractDataFromDomByLabel(studyDoc, '章节测验'); const quizPairFromDom = extractQuizPairFromDom(studyDoc); const normalizedTestNumText = testNumText || quizPairFromDom?.first || quizPair?.first; const normalizedPublishTestNumText = publishTestNumText || quizPairFromDom?.second || quizPair?.second; completedTasks = parseInt(completedText || '0', 10) || 0; totalTasks = parseInt(totalText || '0', 10) || 0; if (percentText) { completionRate = percentText.includes('%') ? percentText : `${percentText}%`; } else if (totalTasks > 0) { completionRate = `${Math.round((completedTasks / totalTasks) * 100)}%`; } if (rankText) { ranking = rankText.includes('名') ? rankText : `第${rankText}名`; } if (pointText) { courseScore = pointText.includes('分') ? pointText : `${pointText}分`; } if (normalizedTestNumText || normalizedPublishTestNumText) { chapterQuiz = `${normalizedTestNumText || '0'}/${normalizedPublishTestNumText || '0'}`; } } catch (e) { console.log('[学习记录]', course.courseName, '解析失败'); } const missingStudyData = totalTasks === 0 && completedTasks === 0 && ranking === '--' && courseScore === '--'; if (missingStudyData) { const chapterProgress = await getProgressFromChapter(); totalTasks = chapterProgress.totalTasks; completedTasks = chapterProgress.completedTasks; completionRate = chapterProgress.completionRate; shouldFilter = shouldFilter || chapterProgress.shouldFilter; } console.log('[学习记录]', course.courseName, '任务点:', `${completedTasks}/${totalTasks}`, '排名:', ranking, '积分:', courseScore); const unfinishedCount = Math.max(totalTasks - completedTasks, 0); resolve({ ...course, totalTasks, completedTasks, completionRate, unfinishedTasks: [], unfinishedCount, studyDataUrl: courseEntryUrl, isComplete: totalTasks > 0 && completedTasks >= totalTasks, shouldFilter, courseScore, chapterQuiz, ranking }); }, onerror: async () => { const chapterProgress = await getProgressFromChapter(); resolve({ ...course, totalTasks: chapterProgress.totalTasks, completedTasks: chapterProgress.completedTasks, completionRate: chapterProgress.completionRate, unfinishedTasks: [], unfinishedCount: Math.max(chapterProgress.totalTasks - chapterProgress.completedTasks, 0), studyDataUrl: courseEntryUrl, isComplete: chapterProgress.totalTasks > 0 && chapterProgress.completedTasks >= chapterProgress.totalTasks, shouldFilter: chapterProgress.shouldFilter, courseScore: '--', chapterQuiz: '--', ranking: '--' }); }, ontimeout: async () => { const chapterProgress = await getProgressFromChapter(); resolve({ ...course, totalTasks: chapterProgress.totalTasks, completedTasks: chapterProgress.completedTasks, completionRate: chapterProgress.completionRate, unfinishedTasks: [], unfinishedCount: Math.max(chapterProgress.totalTasks - chapterProgress.completedTasks, 0), studyDataUrl: courseEntryUrl, isComplete: chapterProgress.totalTasks > 0 && chapterProgress.completedTasks >= chapterProgress.totalTasks, shouldFilter: chapterProgress.shouldFilter, courseScore: '--', chapterQuiz: '--', ranking: '--' }); } }); }); }; // 加载所有课程进度 const loadAllCourseProgress = async () => { console.log('[课程进度] 开始加载课程进度...'); loadingProgress.value = true; try { const courses = await getAllCourses(); console.log('[课程进度] 获取到课程:', courses.length, '个,准备请求前20门...'); if (courses.length > 0) { const progressPromises = courses.map(c => getCourseProgress(c)); // 获取所有课程进度 console.log('[课程进度] 等待所有进度请求完成...'); const results = await Promise.all(progressPromises); // 过滤并排序结果 courseProgressItems.value = results .filter(item => !item.shouldFilter) // 过滤掉标记为需要过滤的课程 .sort((a, b) => { const aHasTasks = a.totalTasks > 0; const bHasTasks = b.totalTasks > 0; if (aHasTasks !== bHasTasks) return aHasTasks ? -1 : 1; if (a.isComplete && !b.isComplete) return 1; if (!a.isComplete && b.isComplete) return -1; return parseInt(a.completionRate) - parseInt(b.completionRate); }); progressLastUpdate.value = new Date().toLocaleTimeString(); } } catch (e) { console.error('加载课程进度失败:', e); } loadingProgress.value = false; }; // 自动刷新课程进度(每1小时) let progressRefreshTimer = null; const startProgressAutoRefresh = () => { if (progressRefreshTimer) clearInterval(progressRefreshTimer); progressRefreshTimer = setInterval(() => { loadAllCourseProgress(); }, 60 * 60 * 1000); // 1小时 }; // 当前视图状态:'dashboard' | 'todo' | 'homework' | 'exam' | 'activities' | 'progress' const currentView = vue.ref('dashboard'); // 排序选项 const sortOptions = [ { value: 'urgent', label: '紧急优先(默认)' }, { value: 'time-asc', label: '剩余时间升序' }, { value: 'time-desc', label: '剩余时间降序' }, { value: 'status', label: '按状态分组' }, { value: 'name', label: '按名称排序' } ]; const currentSort = vue.ref('urgent'); // 解析剩余时间为分钟数用于排序 const parseTimeToMinutes = (timeStr) => { if (!timeStr) return Infinity; // 无时间的排到最后 const str = String(timeStr); // 匹配各种格式:剩余X天X小时、X小时X分钟、已过期等 if (str.includes('过期') || str.includes('截止')) return -1; let minutes = 0; const dayMatch = str.match(/(\d+)\s*天/); const hourMatch = str.match(/(\d+)\s*小时/); const minMatch = str.match(/(\d+)\s*分/); if (dayMatch) minutes += parseInt(dayMatch[1]) * 24 * 60; if (hourMatch) minutes += parseInt(hourMatch[1]) * 60; if (minMatch) minutes += parseInt(minMatch[1]); return minutes || Infinity; }; // 智能排序函数:未完成+剩余时间短的在最上面 const sortItems = (items, type, sortType = 'urgent') => { const arr = [...items]; switch (sortType) { case 'urgent': // 默认排序:未完成优先,然后按剩余时间升序 return arr.sort((a, b) => { // 已完成的放最后 if (a.finished && !b.finished) return 1; if (!a.finished && b.finished) return -1; // 未提交/进行中的优先 if (a.uncommitted && !b.uncommitted) return -1; if (!a.uncommitted && b.uncommitted) return 1; // 按剩余时间排序 const timeA = parseTimeToMinutes(a.leftTime || a.timeLeft || a.info); const timeB = parseTimeToMinutes(b.leftTime || b.timeLeft || b.info); return timeA - timeB; }); case 'time-asc': return arr.sort((a, b) => { const timeA = parseTimeToMinutes(a.leftTime || a.timeLeft || a.info); const timeB = parseTimeToMinutes(b.leftTime || b.timeLeft || b.info); return timeA - timeB; }); case 'time-desc': return arr.sort((a, b) => { const timeA = parseTimeToMinutes(a.leftTime || a.timeLeft || a.info); const timeB = parseTimeToMinutes(b.leftTime || b.timeLeft || b.info); return timeB - timeA; }); case 'status': return arr.sort((a, b) => { if (a.uncommitted && !b.uncommitted) return -1; if (!a.uncommitted && b.uncommitted) return 1; if (a.finished && !b.finished) return 1; if (!a.finished && b.finished) return -1; return 0; }); case 'name': return arr.sort((a, b) => (a.title || '').localeCompare(b.title || '')); default: return arr; } }; // 获取待办任务数据 const loadTodoData = async () => { loading.value.todo = true; const currentTasks = extractTasks(); const pendingTasks = currentTasks.filter(t => t.uncommitted).map(t => ({ ...t, info: t.leftTime })); let pendingExams = []; const seenExamIds = new Set(); try { const res1 = await fetch('https://mooc1-api.chaoxing.com/exam-ans/exam/phone/examcode'); const text1 = await res1.text(); const parser1 = new DOMParser(); const doc1 = parser1.parseFromString(text1, 'text/html'); const exams1 = extractExams(doc1); exams1.filter(e => !e.finished && !e.expired).forEach(e => { const key = e.examId || e.title; if (!seenExamIds.has(key)) { seenExamIds.add(key); pendingExams.push({ ...e, course: "考试课程", info: e.timeLeft }); } }); } catch (e) { console.error("Fetch exams failed", e); } try { const res2 = await fetch(API_EXAM_LIST); const text2 = await res2.text(); const parser2 = new DOMParser(); const doc2 = parser2.parseFromString(text2, 'text/html'); const exams2 = extractExamsFromTable(doc2); exams2.filter(e => !e.finished && !e.expired).forEach(e => { const key = e.examId || e.title; if (!seenExamIds.has(key)) { seenExamIds.add(key); pendingExams.push({ ...e, course: "考试课程", info: e.timeLeft }); } }); } catch (e) { console.error("Fetch exams from table failed", e); } // 获取进行中的课程活动 let ongoingActivities = []; try { const courses = await fetchCourseList(); const batchSize = 5; for (let i = 0; i < courses.length; i += batchSize) { const batch = courses.slice(i, i + batchSize); const batchResults = await Promise.all(batch.map(course => fetchCourseActivities(course))); batchResults.flat().filter(activity => activity.ongoing).forEach(activity => { ongoingActivities.push({ type: activity.type, title: activity.title, course: activity.courseName, info: activity.endTime || '进行中', status: '进行中', courseId: activity.courseId, clazzId: activity.clazzId, isActivity: true }); }); } } catch (e) { console.error('[仪表盘] 获取课程任务失败:', e); } todoItems.value = [...pendingTasks, ...pendingExams, ...ongoingActivities]; // 计算紧急任务 urgentTasks.value = todoItems.value.filter(item => { const timeStr = item.leftTime || item.timeLeft || item.info || ''; if (timeStr.includes('小时')) return parseInt(timeStr) <= 24; if (timeStr.includes('天')) return parseInt(timeStr) < 1; if (timeStr.includes('分钟') || timeStr.includes('分')) return true; if (item.isActivity && item.status === '进行中') return true; return false; }); loading.value.todo = false; }; // 获取作业数据 const loadHomeworkData = async () => { loading.value.homework = true; try { const res = await fetch('https://mooc1-api.chaoxing.com/work/stu-work'); const text = await res.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, 'text/html'); homeworkItems.value = extractTasks(doc); } catch (e) { console.error('[仪表盘] 获取作业失败:', e); } loading.value.homework = false; }; // 获取考试数据 const loadExamData = async () => { loading.value.exam = true; try { const res = await fetch('https://mooc1-api.chaoxing.com/exam-ans/exam/phone/examcode'); const text = await res.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, 'text/html'); examItems.value = extractExams(doc); } catch (e) { console.error('[仪表盘] 获取考试失败:', e); } loading.value.exam = false; }; // 获取课程任务数据 const loadActivitiesData = async () => { loading.value.activities = true; try { const courses = await fetchCourseList(); const allActivities = []; const batchSize = 3; for (let i = 0; i < courses.length; i += batchSize) { const batch = courses.slice(i, i + batchSize); const batchResults = await Promise.all(batch.map(course => fetchCourseActivities(course))); allActivities.push(...batchResults.flat()); } activitiesItems.value = allActivities; } catch (error) { console.error('[仪表盘] 加载课程任务失败:', error); } loading.value.activities = false; }; // 链接生成函数 const getTodoLink = (item) => { if (item.isActivity) { const url = new URL(API_VISIT_COURSE); url.searchParams.append("courseid", item.courseId); url.searchParams.append("clazzid", item.clazzId); url.searchParams.append("pageHeader", "0"); return url.href; } else if (item.type === "作业") { const url = new URL(API_VISIT_COURSE); url.searchParams.append("courseid", item.courseId); url.searchParams.append("clazzid", item.clazzId); url.searchParams.append("pageHeader", "8"); return url.href; } else { 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; } }; const getHomeworkLink = (item) => { const url = new URL(API_VISIT_COURSE); url.searchParams.append("courseid", item.courseId); url.searchParams.append("clazzid", item.clazzId); url.searchParams.append("pageHeader", "8"); return url.href; }; const getExamLink = (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; }; const getActivityLink = (item) => { const url = new URL(API_VISIT_COURSE); url.searchParams.append("courseid", item.courseId); url.searchParams.append("clazzid", item.clazzId); return url.href; }; // 切换视图函数(不跳转外部页面,在内部切换视图) const openFullScreen = (type) => { currentView.value = type; }; // 返回仪表盘 const backToDashboard = () => { currentView.value = 'dashboard'; }; // 跳转到原始页面(真正的外部跳转) const navigateToOriginal = (type) => { let url = ''; switch (type) { case 'todo': url = URL_TODO; break; case 'homework': url = URL_HOMEWORK; break; case 'exam': url = URL_EXAM; break; case 'activities': url = URL_ACTIVITIES; break; } if (url) window.location.href = url; }; vue.onMounted(() => { loadTodoData(); loadHomeworkData(); loadExamData(); loadActivitiesData(); // 加载课程进度数据 loadAllCourseProgress(); startProgressAutoRefresh(); }); return (_ctx, _cache) => { const _component_v_card = vue.resolveComponent("v-card"); const _component_v_card_title = vue.resolveComponent("v-card-title"); const _component_v_card_text = vue.resolveComponent("v-card-text"); const _component_v_list = vue.resolveComponent("v-list"); const _component_v_list_item = vue.resolveComponent("v-list-item"); const _component_v_chip = vue.resolveComponent("v-chip"); const _component_v_btn = vue.resolveComponent("v-btn"); const _component_v_progress_linear = vue.resolveComponent("v-progress-linear"); const _component_v_alert = vue.resolveComponent("v-alert"); const _component_v_spacer = vue.resolveComponent("v-spacer"); const _component_v_icon = vue.resolveComponent("v-icon"); const _component_v_container = vue.resolveComponent("v-container"); // 仪表盘 CSS 样式 (v2.2.0 新设计) const dashboardStyle = ` /* 全局重置 */ .dashboard-wrapper { font-family: "Microsoft YaHei", "PingFang SC", -apple-system, BlinkMacSystemFont, sans-serif; background-color: #f5f7fa; min-height: 100vh; padding: 20px; } .dashboard-wrapper * { box-sizing: border-box; } .dashboard-wrapper a { text-decoration: none; color: inherit; } /* 主体内容 */ .main-content { width: 100%; max-width: 1200px; margin: 0 auto; } /* 滚动条美化 */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: #e0e0e0; border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { background: #d0d0d0; } /* 欢迎语区域 */ .dashboard-header { margin-bottom: 24px; background: #fff; padding: 20px 24px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); display: flex; flex-direction: column; gap: 16px; } .welcome-row { display: flex; justify-content: space-between; align-items: center; } .welcome-text { font-size: 22px; font-weight: 600; color: #262626; display: flex; align-items: center; gap: 10px; } .date-info { font-size: 14px; color: #8c8c8c; } /* 紧急提醒条 */ .urgent-strip { display: flex; align-items: center; justify-content: space-between; background-color: #fff1f0; border: 1px solid #ffccc7; padding: 10px 16px; border-radius: 6px; color: #5c0011; font-size: 14px; width: 100%; transition: all 0.2s; cursor: pointer; } .urgent-strip:hover { background-color: #ffccc7; } .urgent-left { display: flex; align-items: center; gap: 10px; } .urgent-icon { font-size: 16px; } .urgent-count { font-weight: bold; color: #cf1322; } .urgent-action { font-size: 13px; color: #8c8c8c; } /* 仪表盘网格 */ .dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 20px; align-items: start; } /* 卡片通用样式 */ .card { background: #fff; border-radius: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); transition: all 0.3s ease; display: flex; flex-direction: column; overflow: hidden; } .card:hover { box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08); transform: translateY(-2px); } /* 卡片标题栏 */ .card-header { padding: 18px 24px; border-bottom: 1px solid #f5f5f5; display: flex; justify-content: space-between; align-items: center; } .card-title { font-size: 16px; font-weight: 600; display: flex; align-items: center; gap: 10px; color: #333; } .indicator-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } .card-more { font-size: 13px; color: #999; cursor: pointer; } .card-more:hover { color: #1890ff; } /* 卡片内容区 */ .card-body { padding: 0 12px; max-height: 350px; overflow-y: auto; } /* 列表项 */ .list-item { display: flex; justify-content: space-between; align-items: center; padding: 16px 12px; border-bottom: 1px dashed #f0f0f0; cursor: pointer; transition: background 0.2s; } .list-item:last-child { border-bottom: none; } .list-item:hover { background-color: #fafafa; border-radius: 6px; } /* 左侧内容 */ .item-main { flex: 1; margin-right: 12px; min-width: 0; } .task-title { font-size: 14px; color: #333; margin-bottom: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 500; } .task-meta { display: flex; align-items: center; font-size: 12px; color: #999; gap: 8px; } .course-tag { background: #f7f7f7; padding: 2px 6px; border-radius: 4px; color: #666; font-size: 12px; } /* 右侧状态 */ .item-status { text-align: right; flex-shrink: 0; } /* 状态胶囊 */ .badge { font-size: 12px; padding: 2px 8px; border-radius: 10px; font-weight: normal; display: inline-block; } .status-urgent { background: #fff1f0; color: #cf1322; border: 1px solid #ffa39e; } .status-warning { background: #fffbe6; color: #d48806; border: 1px solid #ffe58f; } .status-normal { background: #e6f7ff; color: #096dd9; border: 1px solid #91d5ff; } .status-done { background: #f6ffed; color: #389e0d; border: 1px solid #b7eb8f; } .status-gray { background: #f5f5f5; color: #8c8c8c; border: 1px solid #d9d9d9; } /* 空状态 */ .empty-state { text-align: center; padding: 30px 0; color: #bfbfbf; font-size: 14px; } /* 美化进度条 */ .loading-state { text-align: center; padding: 40px 0; } .loading-spinner { width: 40px; height: 40px; border: 3px solid #f3f3f3; border-top: 3px solid #1890ff; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .loading-text { margin-top: 12px; color: #999; font-size: 13px; } /* 详情页视图 */ .detail-view { background: #fff; border-radius: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); min-height: 500px; } .detail-header { display: flex; align-items: center; justify-content: space-between; padding: 20px 24px; border-bottom: 1px solid #f5f5f5; } .detail-header-left { display: flex; align-items: center; gap: 12px; } .back-btn { display: flex; align-items: center; gap: 6px; padding: 8px 16px; background: #f5f5f5; border: none; border-radius: 6px; color: #666; font-size: 14px; cursor: pointer; transition: all 0.2s; } .back-btn:hover { background: #e8e8e8; color: #333; } .detail-title { font-size: 18px; font-weight: 600; color: #333; display: flex; align-items: center; gap: 10px; } .detail-count { font-size: 13px; color: #999; font-weight: normal; } .external-link-btn { padding: 8px 16px; background: #1890ff; border: none; border-radius: 6px; color: #fff; font-size: 13px; cursor: pointer; transition: all 0.2s; } .external-link-btn:hover { background: #40a9ff; } .detail-body { padding: 0 24px 24px; max-height: 600px; overflow-y: auto; } .detail-list-item { display: flex; justify-content: space-between; align-items: center; padding: 16px 0; border-bottom: 1px dashed #f0f0f0; cursor: pointer; transition: background 0.2s; } .detail-list-item:hover { background: #fafafa; margin: 0 -12px; padding: 16px 12px; border-radius: 6px; } .detail-list-item:last-child { border-bottom: none; } .detail-item-main { flex: 1; min-width: 0; margin-right: 16px; } .detail-item-title { font-size: 15px; color: #333; margin-bottom: 6px; font-weight: 500; } .detail-item-meta { display: flex; align-items: center; gap: 12px; font-size: 13px; color: #999; } .detail-item-status { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; } .status-time { font-size: 13px; color: #666; font-weight: 500; } .status-time.urgent { color: #cf1322; } /* 详情页工具栏 */ .detail-toolbar { display: flex; align-items: center; justify-content: space-between; padding: 12px 24px; background: #fafafa; border-bottom: 1px solid #f0f0f0; } .sort-container { display: flex; align-items: center; gap: 8px; } .sort-label { font-size: 13px; color: #666; } .sort-select { padding: 6px 12px; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 13px; color: #333; background: #fff; cursor: pointer; outline: none; } .sort-select:hover { border-color: #40a9ff; } .sort-select:focus { border-color: #1890ff; box-shadow: 0 0 0 2px rgba(24,144,255,0.2); } /* 查看按钮 */ .view-btn { padding: 6px 12px; background: #fff; border: 1px solid #1890ff; border-radius: 4px; color: #1890ff; font-size: 13px; cursor: pointer; transition: all 0.2s; } .view-btn:hover { background: #1890ff; color: #fff; } /* 列表项时间和状态组合显示 - 新布局 */ .item-time-status { display: flex; flex-direction: row; align-items: center; gap: 8px; flex-shrink: 0; } .status-info { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; } .time-display { font-size: 12px; color: #999; } .time-display.urgent { color: #cf1322; font-weight: 500; } .detail-list-item .item-time-status { flex-direction: row; } .detail-list-item .status-info { min-width: 80px; } /* 课程进度卡片样式 - 占用2列 */ .progress-card { min-height: 200px; grid-column: span 2; } .progress-items-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px 16px; align-items: stretch; grid-auto-rows: 1fr; } .progress-item { display: flex; align-items: flex-start; padding: 14px; border: 1px solid #f0f0f0; border-radius: 8px; cursor: pointer; transition: all 0.2s; position: relative; min-height: 80px; } .progress-item:hover { background: #f5f7fa; border-color: #1890ff; box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15); z-index: 10; } .progress-item:last-child { border-bottom: none; } /* 悬浮提示样式 - 左侧显示(右边卡片使用左侧悬浮) */ .progress-tooltip { display: none; position: absolute; left: calc(100% + 12px); top: 0; width: 240px; max-width: calc(100vw - 32px); padding: 14px; background: #fff; border-radius: 8px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.18); z-index: 1000; font-size: 13px; } .progress-tooltip::before { content: ''; position: absolute; left: -8px; top: 20px; border: 8px solid transparent; border-right-color: #fff; } /* 右侧卡片(偶数项)悬浮提示显示在左侧 */ .progress-item:nth-child(even) .progress-tooltip { left: auto; right: calc(100% + 12px); } .progress-item:nth-child(even) .progress-tooltip::before { left: auto; right: -8px; border-right-color: transparent; border-left-color: #fff; } .progress-item:hover .progress-tooltip { display: block; } @media (max-width: 960px) { .progress-items-grid { grid-template-columns: 1fr; } } @media (max-width: 720px) { .progress-card .card-body { overflow-x: hidden; } .progress-tooltip { left: 50%; right: auto; top: calc(100% + 8px); transform: translateX(-50%); width: min(280px, calc(100vw - 32px)); } .progress-tooltip::before { left: 50%; top: -8px; right: auto; transform: translateX(-50%); border-right-color: transparent; border-left-color: transparent; border-bottom-color: #fff; } .progress-item:nth-child(even) .progress-tooltip { left: 50%; right: auto; } .progress-item:nth-child(even) .progress-tooltip::before { left: 50%; right: auto; border-left-color: transparent; } } .tooltip-title { font-size: 14px; font-weight: 600; color: #333; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid #f0f0f0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .tooltip-row { display: flex; justify-content: space-between; margin-bottom: 6px; color: #666; } .tooltip-row .label { color: #999; font-size: 12px; } .tooltip-row .value { font-weight: 500; color: #333; } .tooltip-row .value.highlight { color: #1890ff; } .tooltip-row .value.success { color: #52c41a; } .tooltip-row .value.warning { color: #faad14; } .tooltip-loading { text-align: center; padding: 20px; color: #999; } .progress-item-main { flex: 1; min-width: 0; margin-right: 12px; } .progress-item-title { font-size: 14px; color: #333; font-weight: 500; margin-bottom: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .progress-bar-wrapper { display: flex; align-items: center; gap: 8px; } .progress-bar { flex: 1; height: 8px; background: #f0f0f0; border-radius: 4px; overflow: hidden; } .progress-bar-fill { height: 100%; border-radius: 4px; transition: width 0.3s; } .progress-bar-fill.complete { background: linear-gradient(90deg, #52c41a, #73d13d); } .progress-bar-fill.warning { background: linear-gradient(90deg, #faad14, #ffc53d); } .progress-bar-fill.danger { background: linear-gradient(90deg, #f5222d, #ff4d4f); } .progress-bar-fill.normal { background: linear-gradient(90deg, #1890ff, #40a9ff); } .progress-rate { font-size: 14px; font-weight: 600; min-width: 50px; text-align: right; } .progress-rate.complete { color: #52c41a; } .progress-rate.warning { color: #faad14; } .progress-rate.danger { color: #cf1322; } .progress-rate.normal { color: #1890ff; } .progress-tasks-count { font-size: 12px; color: #999; margin-top: 4px; } .progress-unfinished { margin-top: 4px; font-size: 12px; color: #f5222d; } .progress-last-update { font-size: 12px; color: #bbb; margin-left: auto; } .refresh-btn { padding: 4px 8px; background: transparent; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 12px; color: #666; cursor: pointer; transition: all 0.2s; } .refresh-btn:hover { border-color: #1890ff; color: #1890ff; } /* 移动端适配 */ @media (max-width: 768px) { .dashboard-wrapper { padding: 10px; } .welcome-row { flex-direction: column; align-items: flex-start; gap: 5px; } .dashboard-header { padding: 15px; } .card-header { padding: 15px; } .dashboard-grid { grid-template-columns: 1fr; } } `; // 注入样式 if (!document.querySelector('#dashboard-style-v2')) { const styleEl = document.createElement('style'); styleEl.id = 'dashboard-style-v2'; styleEl.textContent = dashboardStyle; document.head.appendChild(styleEl); } // 获取状态类名 const getStatusClass = (item) => { if (item.isUrgent) return 'status-urgent'; if (item.uncommitted || item.ongoing) return 'status-warning'; if (item.finished) return 'status-done'; return 'status-normal'; }; // 获取视图标题 const getViewTitle = (type) => { switch (type) { case 'todo': return '待办任务'; case 'homework': return '全部作业'; case 'exam': return '全部考试'; case 'activities': return '课程任务'; default: return ''; } }; // 获取视图数据 const getViewItems = (type) => { switch (type) { case 'todo': return todoItems.value; case 'homework': return homeworkItems.value; case 'exam': return examItems.value; case 'activities': return activitiesItems.value; default: return []; } }; // 获取视图加载状态 const getViewLoading = (type) => { switch (type) { case 'todo': return loading.value.todo; case 'homework': return loading.value.homework; case 'exam': return loading.value.exam; case 'activities': return loading.value.activities; default: return false; } }; // 获取项目链接 const getItemLink = (type, item) => { switch (type) { case 'todo': return getTodoLink(item); case 'homework': return getHomeworkLink(item); case 'exam': return getExamLink(item); case 'activities': return getActivityLink(item); default: return '#'; } }; // 获取列表项状态显示 const getItemStatus = (type, item) => { switch (type) { case 'todo': return item.info || item.leftTime || item.type || '待办'; case 'homework': return item.uncommitted ? '待提交' : '已提交'; case 'exam': return item.finished ? '已完成' : (item.expired ? '已过期' : (item.timeLeft || '进行中')); case 'activities': return item.status || (item.finished ? '已完成' : '进行中'); default: return ''; } }; // 获取状态 badge 类名 const getItemBadgeClass = (type, item) => { switch (type) { case 'todo': return item.isActivity ? 'status-warning' : (item.type === '作业' ? 'status-normal' : 'status-urgent'); case 'homework': return item.uncommitted ? 'status-warning' : 'status-done'; case 'exam': return item.finished ? 'status-done' : (item.expired ? 'status-gray' : 'status-urgent'); case 'activities': return item.finished ? 'status-done' : (item.ongoing ? 'status-warning' : 'status-normal'); default: return 'status-normal'; } }; // 渲染详情页视图 const renderDetailView = (type) => { const rawItems = getViewItems(type); const sortedItems = sortItems(rawItems, type, currentSort.value); const isLoading = getViewLoading(type); const title = getViewTitle(type); const dotColors = { todo: '#1890ff', homework: '#faad14', exam: '#f5222d', activities: '#52c41a' }; return vue.createVNode("div", { class: "detail-view" }, [ // 详情页头部 vue.createVNode("div", { class: "detail-header" }, [ vue.createVNode("div", { class: "detail-header-left" }, [ vue.createVNode("button", { class: "back-btn", onClick: backToDashboard }, [ "← 返回仪表盘" ]), vue.createVNode("div", { class: "detail-title" }, [ vue.createVNode("span", { class: "indicator-dot", style: `background: ${dotColors[type]};` }), title, vue.createVNode("span", { class: "detail-count" }, `共 ${rawItems.length} 项`) ]) ]), vue.createVNode("button", { class: "external-link-btn", onClick: () => navigateToOriginal(type) }, "在原始页面打开") ]), // 工具栏:排序选择器 vue.createVNode("div", { class: "detail-toolbar" }, [ vue.createVNode("div", { class: "sort-container" }, [ vue.createVNode("span", { class: "sort-label" }, "排序方式:"), vue.createVNode("select", { class: "sort-select", value: currentSort.value, onChange: (e) => { currentSort.value = e.target.value; } }, sortOptions.map(opt => vue.createVNode("option", { value: opt.value }, opt.label) )) ]), vue.createVNode("div", { style: "font-size: 13px; color: #999;" }, `未完成: ${rawItems.filter(i => !i.finished && (i.uncommitted !== false)).length} 项` ) ]), // 详情页内容 vue.createVNode("div", { class: "detail-body" }, [ isLoading ? vue.createVNode("div", { class: "loading-state" }, [ vue.createVNode("div", { class: "loading-spinner" }), vue.createVNode("div", { class: "loading-text" }, "加载中...") ]) : sortedItems.length === 0 ? vue.createVNode("div", { class: "empty-state" }, "暂无数据") : sortedItems.map(item => vue.createVNode("div", { class: "detail-list-item" }, [ vue.createVNode("div", { class: "detail-item-main" }, [ vue.createVNode("div", { class: "detail-item-title" }, item.title), vue.createVNode("div", { class: "detail-item-meta" }, [ vue.createVNode("span", {}, item.course || item.courseName || ''), type === 'activities' ? vue.createVNode("span", {}, `· ${item.type || '活动'}`) : null ]) ]), vue.createVNode("div", { class: "item-time-status" }, [ // 状态信息区 vue.createVNode("div", { class: "status-info" }, [ // 剩余时间显示 (item.leftTime || item.timeLeft || item.info) ? vue.createVNode("span", { class: `time-display ${item.isUrgent || parseTimeToMinutes(item.leftTime || item.timeLeft || item.info) < 24 * 60 ? 'urgent' : ''}` }, item.leftTime || item.timeLeft || item.info) : null, // 状态显示 vue.createVNode("span", { class: `badge ${getItemBadgeClass(type, item)}` }, getItemStatus(type, item)) ]), // 查看按钮 vue.createVNode("button", { class: "view-btn", onClick: (e) => { e.stopPropagation(); window.open(getItemLink(type, item), '_blank'); } }, "查看") ]) ]) ) ]) ]); }; // 根据当前视图渲染 if (currentView.value !== 'dashboard') { return vue.createVNode("div", { class: "dashboard-wrapper" }, [ vue.createVNode("div", { class: "main-content" }, [ renderDetailView(currentView.value) ]) ]); } // 渲染仪表盘视图 return vue.createVNode("div", { class: "dashboard-wrapper" }, [ vue.createVNode("div", { class: "main-content" }, [ // 头部欢迎区域 vue.createVNode("div", { class: "dashboard-header" }, [ vue.createVNode("div", { class: "welcome-row" }, [ vue.createVNode("div", { class: "welcome-text" }, `👋 ${greeting.value},${userName.value}`), vue.createVNode("div", { class: "date-info" }, dateInfo.value) ]), // 紧急提醒条 urgentTasks.value.length > 0 ? vue.createVNode("div", { class: "urgent-strip", onClick: () => openFullScreen('todo') }, [ vue.createVNode("div", { class: "urgent-left" }, [ vue.createVNode("span", { class: "urgent-icon" }, "🔔"), vue.createVNode("span", {}, [ "你还有 ", vue.createVNode("span", { class: "urgent-count" }, urgentTasks.value.length), ` 个高优先级任务待处理:${urgentTasks.value.slice(0, 2).map(t => t.title).join('、')}${urgentTasks.value.length > 2 ? '等...' : ''}` ]) ]), vue.createVNode("div", { class: "urgent-action" }, "去处理 >") ]) : null ]), // 仪表盘网格 vue.createVNode("div", { class: "dashboard-grid" }, [ // 待办任务卡片 vue.createVNode("div", { class: "card" }, [ vue.createVNode("div", { class: "card-header" }, [ vue.createVNode("div", { class: "card-title" }, [ vue.createVNode("span", { class: "indicator-dot", style: "background: #1890ff;" }), "待办任务" ]), vue.createVNode("a", { class: "card-more", onClick: () => openFullScreen('todo') }, "查看全部") ]), vue.createVNode("div", { class: "card-body" }, [ loading.value.todo ? vue.createVNode("div", { class: "loading-state" }, [ vue.createVNode("div", { class: "loading-spinner" }), vue.createVNode("div", { class: "loading-text" }, "加载中...") ]) : todoItems.value.length === 0 ? vue.createVNode("div", { class: "empty-state" }, "🎉 暂无待办任务") : sortItems(todoItems.value, 'todo', 'urgent').map(item => vue.createVNode("div", { class: "list-item", onClick: () => window.open(getTodoLink(item), '_blank') }, [ vue.createVNode("div", { class: "item-main" }, [ vue.createVNode("div", { class: "task-title" }, item.title), vue.createVNode("div", { class: "task-meta" }, [ vue.createVNode("span", { class: "course-tag" }, item.course || item.type || '任务') ]) ]), vue.createVNode("div", { class: "item-time-status" }, [ // 剩余时间 (item.leftTime || item.info) ? vue.createVNode("span", { class: `time-display ${item.isUrgent || parseTimeToMinutes(item.leftTime || item.info) < 24 * 60 ? 'urgent' : ''}` }, item.leftTime || item.info) : null, // 状态 vue.createVNode("span", { class: `badge ${item.isActivity ? 'status-warning' : (item.type === '作业' ? 'status-normal' : 'status-urgent')}` }, item.type || '待办') ]) ]) ) ]) ]), // 全部作业卡片 vue.createVNode("div", { class: "card" }, [ vue.createVNode("div", { class: "card-header" }, [ vue.createVNode("div", { class: "card-title" }, [ vue.createVNode("span", { class: "indicator-dot", style: "background: #faad14;" }), "全部作业" ]), vue.createVNode("a", { class: "card-more", onClick: () => openFullScreen('homework') }, "查看全部") ]), vue.createVNode("div", { class: "card-body" }, [ loading.value.homework ? vue.createVNode("div", { class: "loading-state" }, [ vue.createVNode("div", { class: "loading-spinner" }), vue.createVNode("div", { class: "loading-text" }, "加载中...") ]) : homeworkItems.value.length === 0 ? vue.createVNode("div", { class: "empty-state" }, "暂无作业") : sortItems(homeworkItems.value, 'homework', 'urgent').map(item => vue.createVNode("div", { class: "list-item", onClick: () => window.open(getHomeworkLink(item), '_blank') }, [ vue.createVNode("div", { class: "item-main" }, [ vue.createVNode("div", { class: "task-title" }, item.title), vue.createVNode("div", { class: "task-meta" }, item.course || '') ]), vue.createVNode("div", { class: "item-time-status" }, [ (item.leftTime) ? vue.createVNode("span", { class: `time-display ${parseTimeToMinutes(item.leftTime) < 24 * 60 ? 'urgent' : ''}` }, item.leftTime) : null, vue.createVNode("span", { class: `badge ${item.uncommitted ? 'status-warning' : 'status-done'}` }, item.uncommitted ? "待提交" : "已提交") ]) ]) ) ]) ]), // 全部考试卡片 vue.createVNode("div", { class: "card" }, [ vue.createVNode("div", { class: "card-header" }, [ vue.createVNode("div", { class: "card-title" }, [ vue.createVNode("span", { class: "indicator-dot", style: "background: #f5222d;" }), "全部考试" ]), vue.createVNode("a", { class: "card-more", onClick: () => openFullScreen('exam') }, "查看全部") ]), vue.createVNode("div", { class: "card-body" }, [ loading.value.exam ? vue.createVNode("div", { class: "loading-state" }, [ vue.createVNode("div", { class: "loading-spinner" }), vue.createVNode("div", { class: "loading-text" }, "加载中...") ]) : examItems.value.length === 0 ? vue.createVNode("div", { class: "empty-state" }, "🎉 暂无考试") : sortItems(examItems.value, 'exam', 'urgent').map(item => vue.createVNode("div", { class: "list-item", onClick: () => window.open(getExamLink(item), '_blank') }, [ vue.createVNode("div", { class: "item-main" }, [ vue.createVNode("div", { class: "task-title" }, item.title), vue.createVNode("div", { class: "task-meta" }, item.course || '') ]), vue.createVNode("div", { class: "item-time-status" }, [ (item.timeLeft) ? vue.createVNode("span", { class: `time-display ${!item.finished && parseTimeToMinutes(item.timeLeft) < 24 * 60 ? 'urgent' : ''}` }, item.timeLeft) : null, vue.createVNode("span", { class: `badge ${item.finished ? 'status-done' : (item.expired ? 'status-gray' : 'status-urgent')}` }, item.finished ? "已完成" : (item.expired ? "已过期" : "进行中")) ]) ]) ) ]) ]), // 课程任务卡片 vue.createVNode("div", { class: "card" }, [ vue.createVNode("div", { class: "card-header" }, [ vue.createVNode("div", { class: "card-title" }, [ vue.createVNode("span", { class: "indicator-dot", style: "background: #52c41a;" }), "课程任务" ]), vue.createVNode("a", { class: "card-more", onClick: () => openFullScreen('activities') }, "查看全部") ]), vue.createVNode("div", { class: "card-body" }, [ loading.value.activities ? vue.createVNode("div", { class: "loading-state" }, [ vue.createVNode("div", { class: "loading-spinner" }), vue.createVNode("div", { class: "loading-text" }, "加载中...") ]) : activitiesItems.value.length === 0 ? vue.createVNode("div", { class: "empty-state" }, "暂无课程任务") : sortItems(activitiesItems.value, 'activities', 'urgent').map(item => vue.createVNode("div", { class: "list-item", onClick: () => window.open(getActivityLink(item), '_blank') }, [ vue.createVNode("div", { class: "item-main" }, [ vue.createVNode("div", { class: "task-title" }, item.title), vue.createVNode("div", { class: "task-meta" }, [ vue.createVNode("span", { class: "course-tag" }, item.courseName || ''), ` · ${item.type || '活动'}` ]) ]), vue.createVNode("div", { class: "item-time-status" }, [ (item.leftTime) ? vue.createVNode("span", { class: `time-display ${parseTimeToMinutes(item.leftTime) < 24 * 60 ? 'urgent' : ''}` }, item.leftTime) : null, vue.createVNode("span", { class: `badge ${item.finished ? 'status-done' : (item.ongoing ? 'status-warning' : 'status-normal')}` }, item.status || (item.finished ? "已完成" : "进行中")) ]) ]) ) ]) ]), // 课程进度卡片 vue.createVNode("div", { class: "card progress-card" }, [ vue.createVNode("div", { class: "card-header" }, [ vue.createVNode("div", { class: "card-title" }, [ vue.createVNode("span", { class: "indicator-dot", style: "background: #722ed1;" }), "课程进度" ]), vue.createVNode("div", { style: "display: flex; align-items: center; gap: 8px;" }, [ progressLastUpdate.value ? vue.createVNode("span", { class: "progress-last-update" }, `更新于 ${progressLastUpdate.value}`) : null, vue.createVNode("button", { class: "refresh-btn", onClick: () => loadAllCourseProgress() }, loadingProgress.value ? "刷新中..." : "🔄 刷新") ]) ]), vue.createVNode("div", { class: "card-body" }, [ loadingProgress.value ? vue.createVNode("div", { class: "loading-state" }, [ vue.createVNode("div", { class: "loading-spinner" }), vue.createVNode("div", { class: "loading-text" }, "正在获取课程进度...") ]) : courseProgressItems.value.length === 0 ? vue.createVNode("div", { class: "empty-state" }, "暂无课程进度数据") : vue.createVNode("div", { class: "progress-items-grid" }, courseProgressItems.value.map(course => { const rate = parseInt(course.completionRate) || 0; const rateClass = rate >= 100 ? 'complete' : (rate >= 60 ? 'warning' : (rate >= 30 ? 'normal' : 'danger')); const remainingTasks = course.totalTasks - course.completedTasks; return vue.createVNode("div", { class: "progress-item", onClick: () => { if (course.studyDataUrl) { window.open(course.studyDataUrl, '_blank'); } } }, [ vue.createVNode("div", { class: "progress-item-main" }, [ vue.createVNode("div", { class: "progress-item-title" }, course.courseName), vue.createVNode("div", { class: "progress-bar-wrapper" }, [ vue.createVNode("div", { class: "progress-bar" }, [ vue.createVNode("div", { class: `progress-bar-fill ${rateClass}`, style: `width: ${Math.min(rate, 100)}%;` }) ]), vue.createVNode("span", { class: `progress-rate ${rateClass}` }, course.completionRate) ]), vue.createVNode("div", { class: "progress-tasks-count" }, `${course.completedTasks}/${course.totalTasks} 任务点` ) ]), // 悬浮提示 vue.createVNode("div", { class: "progress-tooltip" }, [ vue.createVNode("div", { class: "tooltip-title" }, course.courseName), vue.createVNode("div", { class: "tooltip-row" }, [ vue.createVNode("span", { class: "label" }, "完成进度"), vue.createVNode("span", { class: `value ${rate >= 100 ? 'success' : (rate >= 60 ? 'warning' : 'highlight')}` }, course.completionRate) ]), vue.createVNode("div", { class: "tooltip-row" }, [ vue.createVNode("span", { class: "label" }, "课程积分"), vue.createVNode("span", { class: "value" }, course.courseScore || "点击查看") ]), vue.createVNode("div", { class: "tooltip-row" }, [ vue.createVNode("span", { class: "label" }, "章节测验"), vue.createVNode("span", { class: "value" }, course.chapterQuiz || "点击查看") ]), vue.createVNode("div", { class: "tooltip-row" }, [ vue.createVNode("span", { class: "label" }, "当前排名"), vue.createVNode("span", { class: "value highlight" }, course.ranking || "点击查看") ]), vue.createVNode("div", { style: "margin-top: 10px; padding-top: 8px; border-top: 1px solid #f0f0f0; font-size: 12px; color: #1890ff; text-align: center; cursor: pointer;" }, "📊 点击查看完整学习记录") ]) ]); }) ) ]) ]) ]) ]) ]); }; } }); const appendApp = () => { const vuetify$1 = vuetify.createVuetify({ icons: { defaultSet: "md", aliases, sets: { md } } }); let app = _sfc_main$1; const urlDetect2 = urlDetection(); if (urlDetect2 === "homework") app = _sfc_main$2; if (urlDetect2 === "exam") app = _sfc_main; if (urlDetect2 === "todo") app = _sfc_todo; if (urlDetect2 === "activities") app = _sfc_activities; if (urlDetect2 === "dashboard") app = _sfc_dashboard; vue.createApp(app).use(vuetify$1).mount( (() => { const app2 = document.createElement("div"); document.body.append(app2); return app2; })() ); }; const urlDetect = urlDetection(); if (urlDetect === "homework" || urlDetect === "todo" || urlDetect === "activities" || urlDetect === "dashboard") { wrapElements(); removeStyles(); removeScripts(); appendApp(); } if (urlDetect === "exam") { wrapElements(); removeStyles(); removeScripts(); keepRemoveHtmlStyle(); appendApp(); } if (urlDetect === "home") { fixCssConflict(); initMenus(); // 延迟后自动点击仪表盘菜单 setTimeout(() => { const dashboardMenuItem = document.querySelector('#first1000000') || document.querySelector('#first_chaoxing_assignment_dashboard'); if (dashboardMenuItem) { dashboardMenuItem.click(); console.log('[脚本] 自动切换到学习仪表盘'); } }, 500); } if (urlDetect === "legacyHome") { fixCssConflict(); initMenus(); // 延迟后自动点击仪表盘菜单 setTimeout(() => { const dashboardMenuItem = document.querySelector('#first_chaoxing_assignment_dashboard') || document.querySelector('a[href*="chaoxing-dashboard"]'); if (dashboardMenuItem) { dashboardMenuItem.click(); console.log('[脚本] 自动切换到学习仪表盘'); } }, 500); } })(Vuetify, Vue);