ApplePine 140f40c168 initial: 初始版本
- 在系统课程列表界面读取发送请求,获取课程信息
- 在课程列表界面为课程卡片补充提示信息(包含是否已经学习过、学时、学分、课程类型等)
2025-07-20 11:41:29 +08:00

145 lines
4.8 KiB
TypeScript

export default defineContentScript({
matches: [
`*://${import.meta.env.WXT_PUBLIC_UCLASS_HOSTNAME}/*`,
`*://${import.meta.env.WXT_PUBLIC_INTRANET_HOSTNAME}/*`
],
world: 'MAIN',
runAt: 'document_start',
main() {
const uclassHostname = import.meta.env.WXT_PUBLIC_UCLASS_HOSTNAME;
const intranetHostname = import.meta.env.WXT_PUBLIC_INTRANET_HOSTNAME;
const intranetPort = import.meta.env.WXT_PUBLIC_INTRANET_PORT;
const isTargetPage =
window.location.hostname === uclassHostname ||
(window.location.hostname === intranetHostname && window.location.port === intranetPort)
if (!isTargetPage) {
return
}
console.log('[XHR Interceptor] 脚本已通过 WXT 成功注入。')
if (typeof XMLHttpRequest === 'undefined') {
return
}
const original_open = XMLHttpRequest.prototype.open
const original_send = XMLHttpRequest.prototype.send
interface PatchedXHR extends XMLHttpRequest {
_url?: string | URL
}
XMLHttpRequest.prototype.open = function (
this: PatchedXHR,
method: string,
url: string | URL,
...args: any[]
) {
this._url = url
return original_open.apply(this, arguments as any)
}
XMLHttpRequest.prototype.send = function (this: PatchedXHR, body) {
this.addEventListener('load', () => {
if (
this._url &&
typeof this._url === 'string' &&
this._url.includes('/lms/api//user/course/registList')
) {
try {
console.log('========== [WXT Main World] 检测到目标XHR请求 ==========')
console.log('请求URL:', this._url)
const responseData = JSON.parse(this.responseText)
console.log('响应JSON数据:', responseData)
// 检查响应数据是否是我们期望的格式
if (responseData && responseData.page && Array.isArray(responseData.page.content)) {
// 为课程对象定义一个更详细的类型
type Course = {
name: string
status: string
score: number | string
hour: number | string
cumulativeStudyCount: number
studyCount: number
[key: string]: any
}
const courses: Course[] = responseData.page.content
const cleanupAndApplyStyles = () => {
document.querySelectorAll('.chrome-ext-course-prefix').forEach((el) => el.remove())
document.querySelectorAll('[data-course-processed="true"]').forEach((el) => {
const htmlEl = el as HTMLElement
const title = htmlEl.querySelector<HTMLElement>('.title, .course-name, h3, h4') // 尝试一些常见的标题选择器
if (title) title.style.color = ''
htmlEl.removeAttribute('data-course-processed')
})
courses.forEach((course) => {
if (course && typeof course.name === 'string') {
const courseName = course.name.trim()
if (courseName === '') return
try {
const xpath = `//*[normalize-space(text())="${courseName}"]`
const titleElements: HTMLElement[] = []
const result = document.evaluate(
xpath,
document,
null,
XPathResult.ORDERED_NODE_ITERATOR_TYPE,
null,
)
let node: Node | null
while ((node = result.iterateNext())) {
if (node instanceof HTMLElement) {
titleElements.push(node)
}
}
titleElements.forEach((titleElement) => {
const parent = titleElement.parentElement
if (!parent) return
parent.dataset.courseProcessed = 'true'
const score = course.score ?? 'N/A'
const hour = course.hour ?? 'N/A'
const cumulativeStudyCount = course.cumulativeStudyCount ?? 'N/A'
const studyCount = course.studyCount ?? 'N/A'
const isVideo = cumulativeStudyCount / studyCount > 5 ? '大概率是文档' : '大概率是视频'
const prefixSpan = document.createElement('span')
prefixSpan.className = 'chrome-ext-course-prefix'
prefixSpan.textContent = `(${score}分-${hour}学时)-${isVideo}`
parent.insertBefore(prefixSpan, titleElement)
titleElement.style.color = course.status !== 'FINISH' ? 'red' : ''
})
} catch (e) {
console.error(`为课程 "${courseName}" 应用样式时出错:`, e)
}
}
})
console.log('样式和前缀已更新。')
}
setTimeout(cleanupAndApplyStyles, 1000)
} else {
console.log('[WXT Main World] 响应数据中未找到有效的课程列表 (response.page.content)')
}
console.log('======================================================')
} catch (e) {
console.error('[WXT Main World] 解析XHR响应失败:', e, '原始响应:', this.responseText)
}
}
})
return original_send.apply(this, arguments as any)
}
},
})