initial: 初始版本
- 在系统课程列表界面读取发送请求,获取课程信息 - 在课程列表界面为课程卡片补充提示信息(包含是否已经学习过、学时、学分、课程类型等)
3
.env.example
Normal file
@ -0,0 +1,3 @@
|
||||
WXT_PUBLIC_UCLASS_HOSTNAME="xxx.xxxx.cn"
|
||||
WXT_PUBLIC_INTRANET_HOSTNAME="192.168.1.7"
|
||||
WXT_PUBLIC_INTRANET_PORT="1234"
|
||||
32
.gitignore
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.output
|
||||
stats.html
|
||||
stats-*.json
|
||||
.wxt
|
||||
web-ext.config.ts
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# lock file
|
||||
bun.lock
|
||||
|
||||
# env
|
||||
.env
|
||||
124
README.md
Normal file
@ -0,0 +1,124 @@
|
||||
# 金蝶课程系统助手
|
||||
|
||||
一个专为企业金蝶课程系统设计的浏览器扩展插件,能够为课程视频卡片添加智能提示信息,帮助用户更好地了解课程学习状态。
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
- 🎯 **智能课程识别**:自动识别页面中的课程列表
|
||||
- 📊 **学习状态显示**:显示课程的学习状态(已完成/未完成)
|
||||
- ⏱️ **学时学分信息**:显示课程的学分和学时信息
|
||||
- 🎬 **内容类型识别**:智能判断课程是视频还是文档类型
|
||||
- 🎨 **视觉提示**:未完成课程以红色标识,已完成课程保持原色
|
||||
- 🔄 **实时更新**:页面刷新后自动重新应用样式和信息
|
||||
|
||||
## 🚀 安装方法
|
||||
|
||||
### 方法一:从源码构建(推荐)
|
||||
|
||||
1. 克隆项目到本地:
|
||||
```bash
|
||||
git clone https://github.com/App1ePine/kingdeeCourseBot.git
|
||||
cd kingdeeCourseBot
|
||||
```
|
||||
|
||||
2. 安装依赖:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. 配置环境变量:
|
||||
- 将 `.env.example` 文件复制并重命名为 `.env`
|
||||
- 根据您的实际环境修改 `.env` 文件中的配置值
|
||||
|
||||
4. 构建扩展:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
5. 在浏览器中加载扩展:
|
||||
- **Chrome/Edge**: 打开 `chrome://extensions/`,开启"开发者模式",点击"加载已解压的扩展程序",选择 `dist` 文件夹
|
||||
- **Firefox**: 打开 `about:debugging#/runtime/this-firefox`,点击"加载临时附加组件",选择 `dist` 文件夹中的 `manifest.json`
|
||||
|
||||
### 方法二:开发模式
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 📖 使用说明
|
||||
|
||||
1. 安装扩展后,访问金蝶课程系统
|
||||
2. 插件会自动检测课程列表页面
|
||||
3. 每个课程卡片前会显示格式为 `(学分-学时)-内容类型` 的提示信息
|
||||
4. 未完成的课程标题会以红色显示
|
||||
5. 已完成的课程保持原色
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
- **框架**: [WXT](https://wxt.dev/) - 现代化的浏览器扩展开发框架
|
||||
- **前端**: Vue 3 + JavaScript
|
||||
- **构建工具**: Vite
|
||||
- **语言**: JavaScript
|
||||
|
||||
## 🔧 开发
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Node.js 16+
|
||||
- npm 或 yarn
|
||||
|
||||
### 开发命令
|
||||
|
||||
```bash
|
||||
# 开发模式(Chrome)
|
||||
npm run dev
|
||||
|
||||
# 开发模式(Firefox)
|
||||
npm run dev:firefox
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
|
||||
# 构建 Firefox 版本
|
||||
npm run build:firefox
|
||||
|
||||
# 打包扩展
|
||||
npm run zip
|
||||
|
||||
# 类型检查
|
||||
npm run compile
|
||||
```
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
kingdeeCourseBot/
|
||||
├── entrypoints/
|
||||
│ ├── background.ts # 后台脚本
|
||||
│ ├── content.ts # 内容脚本
|
||||
│ └── popup/ # 弹出窗口
|
||||
├── components/ # Vue 组件
|
||||
├── assets/ # 静态资源
|
||||
├── public/ # 公共资源
|
||||
└── wxt.config.ts # WXT 配置文件
|
||||
```
|
||||
|
||||
## ⚙️ 配置
|
||||
|
||||
插件通过环境变量配置目标网站。项目根目录包含 `.env.example` 文件,您需要:
|
||||
|
||||
1. 将 `.env.example` 文件复制并重命名为 `.env`
|
||||
2. 在 `.env` 文件中配置以下环境变量:
|
||||
|
||||
```bash
|
||||
# 金蝶课程系统主机名
|
||||
WXT_PUBLIC_UCLASS_HOSTNAME=your-uclass-hostname.com
|
||||
|
||||
# 内网主机名
|
||||
WXT_PUBLIC_INTRANET_HOSTNAME=your-intranet-hostname.com
|
||||
|
||||
# 内网端口
|
||||
WXT_PUBLIC_INTRANET_PORT=8080
|
||||
```
|
||||
|
||||
根据实际情况使用公网域名 或内网IP,请根据您的实际环境修改这些配置值。
|
||||
1
assets/vue.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 497 B |
3
entrypoints/background.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default defineBackground(() => {
|
||||
console.log('kingdeeCourseAutoPlayer 已启动', { id: browser.runtime.id });
|
||||
})
|
||||
144
entrypoints/content.ts
Normal file
@ -0,0 +1,144 @@
|
||||
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)
|
||||
}
|
||||
},
|
||||
})
|
||||
10
entrypoints/popup/App.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
13
entrypoints/popup/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Default Popup Title</title>
|
||||
<meta name="manifest.type" content="browser_action" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
5
entrypoints/popup/main.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue';
|
||||
import './style.css';
|
||||
import App from './App.vue';
|
||||
|
||||
createApp(App).mount('#app');
|
||||
80
entrypoints/popup/style.css
Normal file
@ -0,0 +1,80 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
25
package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "kingdee-course-bot",
|
||||
"description": "kingdee course bot",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
"dev:firefox": "wxt -b firefox",
|
||||
"build": "wxt build",
|
||||
"build:firefox": "wxt build -b firefox",
|
||||
"zip": "wxt zip",
|
||||
"zip:firefox": "wxt zip -b firefox",
|
||||
"compile": "vue-tsc --noEmit",
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@wxt-dev/module-vue": "^1.0.2",
|
||||
"typescript": "5.6.3",
|
||||
"vue-tsc": "^2.2.10",
|
||||
"wxt": "^0.20.6"
|
||||
}
|
||||
}
|
||||
BIN
public/icon/128.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
public/icon/16.png
Normal file
|
After Width: | Height: | Size: 559 B |
BIN
public/icon/32.png
Normal file
|
After Width: | Height: | Size: 916 B |
BIN
public/icon/48.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/icon/96.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
15
public/wxt.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_305_516)">
|
||||
<g clip-path="url(#clip1_305_516)">
|
||||
<path d="M49.0229 69.1875C54.1272 69.1875 58.265 65.0497 58.265 59.9454V50.7033H59.9454C65.0497 50.7033 69.1875 46.5655 69.1875 41.4612C69.1875 36.357 65.0497 32.2191 59.9454 32.2191H58.265V22.9771C58.265 17.8728 54.1272 13.735 49.0229 13.735H39.7809V12.0546C39.7809 6.95032 35.643 2.8125 30.5388 2.8125C25.4345 2.8125 21.2967 6.95032 21.2967 12.0546V13.735H12.0546C6.95032 13.735 2.8125 17.8728 2.8125 22.9771V32.2191H4.49288C9.59714 32.2191 13.735 36.357 13.735 41.4612C13.735 46.5655 9.59714 50.7033 4.49288 50.7033H2.8125V69.1875H21.2967V67.5071C21.2967 62.4029 25.4345 58.265 30.5388 58.265C35.643 58.265 39.7809 62.4029 39.7809 67.5071V69.1875H49.0229Z" stroke="#67D55E" stroke-width="5.625"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_305_516">
|
||||
<rect width="72" height="72" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_305_516">
|
||||
<rect width="72" height="72" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
3
tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./.wxt/tsconfig.json"
|
||||
}
|
||||
6
wxt.config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
// See https://wxt.dev/api/config.html
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/module-vue'],
|
||||
});
|
||||