├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── bin └── index.ts ├── cache.json ├── images ├── done.png ├── group.png ├── login.png ├── mobile.png └── pc.png ├── package.json ├── pnpm-lock.yaml ├── src ├── api │ ├── answer.ts │ ├── data.ts │ ├── login.ts │ ├── push.ts │ └── user.ts ├── component │ ├── ExamBtn.ts │ ├── Frame.ts │ ├── Hr.ts │ ├── InfoItem.ts │ ├── LoginItem.ts │ ├── NoramlItem.ts │ ├── Panel.ts │ ├── ScheduleList.ts │ ├── ScoreItem.ts │ ├── Select.ts │ ├── SettingsPanel.ts │ ├── TaskBtn.ts │ ├── TaskItem.ts │ ├── TaskList.ts │ ├── TimeInput.ts │ └── Tip.ts ├── config │ ├── api.ts │ ├── compile.ts │ ├── script.ts │ ├── task.ts │ ├── url.ts │ └── version.ts ├── controller │ ├── exam.ts │ ├── frame.ts │ ├── login.ts │ ├── readAndWatch.ts │ ├── schedule.ts │ ├── tip.ts │ └── user.ts ├── css │ └── index.css ├── index.js ├── index.ts ├── shared │ └── index.ts ├── types │ └── index.ts └── utils │ ├── composition.ts │ ├── element.ts │ ├── log.ts │ ├── push.ts │ ├── random.ts │ ├── time.ts │ └── utils.ts ├── tech-study.js ├── tsconfig.json └── types └── global.d.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Xu22Web 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tech-study-js 2 | 3 | ### 原仓库 4 | 5 | > https://github.com/TechXueXi/techxuexi-js 6 | 7 | 基于原作者的原仓库,自行改进和完善 8 | 9 | ### 描述 Description 10 | 11 | - 灵活且貌似轻量的 `学习强国` 油猴脚本。 12 | 13 | - 与此同时,提供更加便捷的版本选择 14 | 15 | - [Node.js 版](https://github.com/Xu22Web/tech-study-node 'Node.js 版') 16 | 17 | - [Docker 版](https://github.com/Xu22Web/tech-study-docker 'Docker 版') 18 | 19 | ### 交流群 Telegram Group 20 | 21 | - 链接: [tech-study 互动群](https://t.me/+IJ_YzNc-Iew0MGRl) 22 | 23 | - 二维码: 24 | 25 | Telegram邀请二维码 26 | 27 | 注:介于脚本国内敏感,暂时不提供其他交流互动方式。 28 | 29 | ### 用法 Usage 30 | 31 | 1. 装个浏览器插件`Tampermonkey` 32 | 33 | 1. Microsoft Edge: [插件安装](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd?hl=zh-CN) 34 | 35 | 2. Google Chrome: [插件安装](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=zh) 36 | 37 | 2. 点击插件里添加按钮,去掉编辑框里原来的代码,复制 [tech-study.js](https://raw.githubusercontent.com/Xu22Web/tech-study-js/master/tech-study.js) 脚本,粘贴进编辑框保存。 38 | 39 | 3. 开启这个脚本,然后进入网页强国 https://www.xuexi.cn 。 40 | 41 | ### 优化 Promote 42 | 43 | 1. 优化整体交互设计,新增一体式扫码登录 44 | 45 | 2. 新增用户信息显示,包括昵称、头像、总分以及当天分数 46 | 47 | 3. 新增任务进度以及任务分数详情显示,任务情况清晰明了 48 | 49 | 4. 优化答题逻辑,新增滑动验证,远离验证烦恼 50 | 51 | 5. 新增同屏任务以及静默运行,仅需一个页面即可静默运行任务 52 | 53 | 6. 兼容桌面端以及移动端,手机电脑均可运行(设备均需支持油猴脚本,此外,移动端需要开启同屏任务) 54 | 55 | 7. 新增定时任务以及远程推送,定时刷新页面,远程微信推送登录二维码 56 | 57 | ### 使用流程 Process 58 | 59 | 1. 用户`登录` 60 | 61 | 登录 62 | 63 | 2. 点击 `开始学习`,等待完成任务运行 64 | 65 | - `桌面端`运行 66 | 67 | 桌面端运行 68 | 69 | - `移动端`运行 70 | 71 | 移动端运行 72 | 73 | 3. `完成学习`任务 74 | 75 | 完成学习 76 | 77 | ### 更新与维护 Update and Maintenance 78 | 79 | 1. 修复有声视频播放后,页面倒计时不继续的问题(注:不同标签页的有声视频需要用户交互才能播放,手动播放后会展示倒计时) 80 | 81 | 2. 与此同时,提供更加便捷的版本选择 82 | 83 | - [Node.js 版](https://github.com/Xu22Web/tech-study-node 'Node.js 版') 84 | 85 | - [Docker 版](https://github.com/Xu22Web/tech-study-docker 'Docker 版') 86 | 87 | ### 关于开发 Development 88 | 89 | - 脚本配置 90 | 91 | 1. 版本配置 `src/config/version.ts` 92 | 93 | 2. 脚本配置 `src/config/script.ts` 94 | 95 | 3. 编译配置 `src/config/compile.ts` 96 | 97 | 4. 接口配置 `src/config/api.ts` 98 | 99 | 5. 链接配置 `src/config/url.ts` 100 | 101 | 6. 任务配置 `src/config/task.ts` 102 | 103 | - CSS 文件 104 | 105 | `src/css/index.css` 106 | 107 | - 根据功能特性(i) 108 | 109 | ```js 110 | // 将文件'./css/index.css'文本内容赋值到'css' 111 | import css from './css/index.css?raw'; 112 | ``` 113 | 114 | - 根据 Tampermonkey API 函数 115 | 116 | ```js 117 | // 嵌入样式 118 | GM_addStyle(css); 119 | ``` 120 | 121 | - 脚本内容 122 | 123 | `src/tech-study.ts` 124 | 125 | - 编译 126 | 127 | ``` 128 | # 编译生成 'tech-study.js' 129 | pnpm build 130 | ``` 131 | 132 | 即 133 | 134 | ``` 135 | ✔ 完成编译: index.ts -> index.js 136 | ✔ 已生成 用户脚本配置 注释! 137 | ✔ 完成编译: ./config/api.ts -> api.js 138 | ✔ 完成编译: ./config/url.ts -> url.js 139 | ✔ 完成编译: ./config/task.ts -> task.js 140 | 141 | ... ... 142 | 143 | ✔ 导出整合的脚本文件: tech-study.js 144 | ``` 145 | 146 | - 功能特性(基于`TypeScript Compiler API`) 147 | 148 | 1. 包含`?raw`结尾的`import`语句 149 | 150 | ``` 151 | import var from 'file?raw'; 152 | ``` 153 | 154 | 1. 文件`file`文本内容赋值到`var` 155 | 156 | 2. 此类型`import`语句不会被编译到结果 157 | 158 | 2. 普通的`import`语句 159 | 160 | ``` 161 | import { funName } from 'file'; 162 | ``` 163 | 164 | 1. 文件`file`文本插入到主文件一起导出,相当于合并多个`*.ts`文件导出为一个`*.js`文件 165 | 166 | 2. 此类型`import`语句不会被编译到结果 167 | 168 | - 类似组合式接口(类似 `Composition API`) 169 | 170 | 模拟 `ref`,`watch`,`watchEffect` 等 API。 171 | -------------------------------------------------------------------------------- /bin/index.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import fs from 'fs'; 3 | import ora from 'ora'; 4 | import path from 'path'; 5 | import rollup from 'rollup'; 6 | import ts from 'typescript'; 7 | import COMPILE_CONFIG from '../src/config/compile'; 8 | import SCRIPT_CONFIG from '../src/config/script'; 9 | // 根目录 文件名 10 | const { input, output, rollupConfig, compress } = COMPILE_CONFIG; 11 | // 输入文件路径 12 | const inputFilePath = input.file; 13 | // 输入目录名 14 | const inputDirPath = path.dirname(input.file); 15 | // 输出文件路径 16 | const outputFilePath = output.file; 17 | // 输出目录名 18 | const outputDirPath = path.dirname(output.file); 19 | // 导入路径 20 | const importFilePaths: string[] = []; 21 | // 缓存 22 | const cache: { 23 | [key: string]: { timeStamp: string; data: string; from: string; to: string }; 24 | } = JSON.parse(fs.readFileSync('cache.json', { encoding: 'utf-8' })) || {}; 25 | 26 | /** 27 | * @description 编译 28 | * @param filePath 29 | * @returns 30 | */ 31 | const handleCompile = async ( 32 | filePath: string 33 | ): Promise< 34 | | { 35 | data: string; 36 | compileName: string; 37 | originName: string; 38 | originPath: string; 39 | originRelativePath: string; 40 | } 41 | | undefined 42 | > => { 43 | // 全路径 44 | const fullFilePath = path.resolve(filePath); 45 | // 文件名 46 | const fileName = getFileName(filePath); 47 | // 创建配置 48 | const options = createOptions(fullFilePath); 49 | // 创建项目 50 | const program = ts.createIncrementalProgram(options); 51 | // 根据项目配置获取源文件 52 | const sourceFile = program.getSourceFile(fullFilePath); 53 | // 自定义处理流程 54 | const customTransformers: ts.CustomTransformers = { 55 | before: [transformerFactory], 56 | }; 57 | return new Promise((resolve) => { 58 | // 编译生成 59 | program.emit( 60 | sourceFile, 61 | (name, text) => { 62 | // 编译后的文件数据 63 | const data = text.replace(/export (\{.*\}|default .*);/g, ''); 64 | // 编译后的文件名 65 | const compileName = getFileName(name); 66 | 67 | resolve({ 68 | data, 69 | compileName, 70 | originName: fileName, 71 | originPath: fullFilePath, 72 | originRelativePath: filePath, 73 | }); 74 | }, 75 | undefined, 76 | false, 77 | customTransformers 78 | ); 79 | }); 80 | }; 81 | /** 82 | * @description 解析模块 83 | * @param filePath 84 | * @returns 85 | */ 86 | const resoveModule = async ( 87 | rawModulePath: string 88 | ): Promise<{ 89 | modulePath: string; 90 | time: string | undefined; 91 | stats: boolean; 92 | }> => { 93 | // 文件路径 94 | const modulePath = path.join(rawModulePath); 95 | // 后缀 96 | const ext = path.extname(modulePath); 97 | // 文件名 98 | const fileName = getFileName(modulePath); 99 | // 路径存在状态 100 | const { stats, time } = handleFileStatus(modulePath); 101 | // 文件存在 102 | if (stats) { 103 | return { modulePath, time, stats }; 104 | } 105 | // 存在扩展 106 | if (!ext.length) { 107 | // 加后缀 108 | const res = await resoveModule(`${modulePath}.ts`); 109 | if (res.stats) { 110 | return res; 111 | } 112 | if (fileName !== 'index.ts') { 113 | const res = await resoveModule(`${modulePath}/index.ts`); 114 | if (res.stats) { 115 | return res; 116 | } 117 | } 118 | } 119 | return { modulePath, time, stats }; 120 | }; 121 | /** 122 | * @description 获取文件名 123 | * @param filePath 124 | * @returns 125 | */ 126 | const getFileName = (filePath) => { 127 | return filePath.substring(filePath.lastIndexOf('/') + 1); 128 | }; 129 | /** 130 | * @description 文件存在 131 | * @param filePath 132 | * @returns 133 | */ 134 | const handleFileStatus = (filePath: string) => { 135 | // 路径存在状态 136 | const exists = fs.existsSync(filePath); 137 | // 路径存在 138 | if (exists) { 139 | // 文件信息 140 | const fileInfo = fs.statSync(filePath); 141 | // 是文件 142 | if (fileInfo.isFile()) { 143 | const { mtime } = fileInfo; 144 | return { stats: true, time: mtime.toJSON() }; 145 | } 146 | } 147 | return { stats: false }; 148 | }; 149 | /** 150 | * @description 生成配置 151 | * @param filePath 152 | * @returns 153 | */ 154 | const createOptions = (filePath: string): ts.CreateProgramOptions => { 155 | const { target, module } = COMPILE_CONFIG; 156 | // 项目配置 157 | const programOptions = { 158 | rootNames: [filePath], 159 | options: { 160 | target, 161 | module, 162 | outputDirPath, 163 | }, 164 | }; 165 | return programOptions; 166 | }; 167 | /** 168 | * @description 创建用户脚本注释配置 169 | * @returns 170 | */ 171 | const createConfigComment = () => { 172 | // 脚本数据 173 | const data: string[] = []; 174 | data.push('// ==UserScript=='); 175 | for (const key in SCRIPT_CONFIG) { 176 | if (typeof SCRIPT_CONFIG[key] === 'string') { 177 | data.push(`// @${key} ${SCRIPT_CONFIG[key]}`); 178 | } 179 | if (Array.isArray(SCRIPT_CONFIG[key])) { 180 | for (const i in SCRIPT_CONFIG[key]) { 181 | data.push(`// @${key} ${SCRIPT_CONFIG[key][i]}`); 182 | } 183 | } 184 | } 185 | data.push('// ==/UserScript=='); 186 | return data.join('\r\n'); 187 | }; 188 | // 处理流程工厂 189 | const transformerFactory: ts.TransformerFactory = (context) => { 190 | return (node) => { 191 | // 访问 192 | const visitor: ts.Visitor = (rootNode) => { 193 | // 节点 194 | const node = ts.visitEachChild(rootNode, visitor, context); 195 | // 是导入声明 196 | if (ts.isImportDeclaration(node)) { 197 | // 获取变量名 198 | const identifierText = node.importClause?.getText(); 199 | // 获取导入模块名 200 | const moduleText = node.moduleSpecifier.getText(); 201 | // 检查是否满足路径 202 | const rawSpecificPath = moduleText.match( 203 | /(?<=(["'`]))(?:\.{0,2}(?:\/|(?:\\{1,2}))[-_.a-zA-Z]*)+(?=\?raw\1)/ 204 | ); 205 | if (identifierText && rawSpecificPath) { 206 | // 提取模块相对路径 207 | const [relativefilePath] = rawSpecificPath; 208 | // 获取实际路径 209 | const filePath = path.resolve(inputDirPath, relativefilePath); 210 | // 获取文本信息 211 | const content = fs 212 | .readFileSync(filePath, { 213 | encoding: 'utf8', 214 | }) 215 | .replace(/\n|\\n/g, ''); 216 | // 创建变量标识符 217 | const name = ts.factory.createIdentifier(identifierText); 218 | // 创建字符串 219 | const value = ts.factory.createStringLiteral(content, true); 220 | // 创建变量声明 221 | const declaration = ts.factory.createVariableDeclaration( 222 | name, 223 | ts.factory.createToken(ts.SyntaxKind.ExclamationToken), 224 | ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), 225 | value 226 | ); 227 | // 创建声明列表 指定为 const 228 | const declaratioList = ts.factory.createVariableDeclarationList( 229 | [declaration], 230 | ts.NodeFlags.Const 231 | ); 232 | // 创建变量声明 233 | return ts.factory.createVariableStatement(undefined, declaratioList); 234 | } 235 | // 检查是否满足路径 236 | const resPath = moduleText.match( 237 | /(?<=(["'`]))(?:\.{0,2}(?:\/|(?:\\{1,2}))[-_.a-zA-Z]*)+(?=\1)/ 238 | ); 239 | // 检查是否满足路径 240 | if (resPath) { 241 | // 相对路径 242 | const relativefilePath = resPath[0]; 243 | importFilePaths.push(relativefilePath); 244 | return ts.factory.createNotEmittedStatement(node); 245 | } 246 | return node; 247 | } 248 | // 导出声明 249 | if (ts.isExportDeclaration(node)) { 250 | return ts.factory.createNotEmittedStatement(node); 251 | } 252 | return node; 253 | }; 254 | // 调用 visitor 255 | return ts.visitNode(node, visitor); 256 | }; 257 | }; 258 | 259 | // 主函数 260 | const main = async () => { 261 | // 开始编译 262 | const progress = ora('准备编译生成脚本文件...'); 263 | // 数据 264 | const fullData: string[] = []; 265 | 266 | // 注释 267 | progress.start(`正在生成 ${chalk.blueBright('用户脚本配置')} 注释...`); 268 | // 用户脚本注释配置 269 | const config = createConfigComment(); 270 | fullData.push(config); 271 | // 注释 272 | progress.succeed(`已生成 ${chalk.blueBright('用户脚本配置')} 注释!`); 273 | 274 | // 解析模块 275 | const { modulePath } = await resoveModule(inputFilePath); 276 | progress.start(`正在编译... ${chalk.blueBright(modulePath)}`); 277 | // 编译 278 | const res = await handleCompile(modulePath); 279 | if (res) { 280 | // 编译信息 281 | const { compileName, data } = res; 282 | // 脚本内容 283 | const content: string[] = []; 284 | content.push(data); 285 | progress.succeed( 286 | `完成编译: ${chalk.blueBright(modulePath)} -> ${chalk.blueBright( 287 | compileName 288 | )}` 289 | ); 290 | // 编译相对路径 291 | const compileRelativePath = path.join(inputDirPath, compileName); 292 | 293 | // 源文件名 编译文件名 数据 294 | for (const i in importFilePaths) { 295 | // 相对路径 296 | const relativefilePath = importFilePaths[i]; 297 | // 文件路径 298 | const filePath = path.join(inputDirPath, relativefilePath); 299 | // 解析模块 300 | const { time, modulePath, stats } = await resoveModule(filePath); 301 | 302 | progress.start(`正在编译... ${chalk.blueBright(modulePath)}`); 303 | // 存在模块 304 | if (stats) { 305 | // 缓存 306 | if (cache[modulePath]) { 307 | const { timeStamp, from, to } = cache[modulePath]; 308 | if (time === timeStamp) { 309 | // 缓存取数据 310 | content.push(cache[modulePath].data); 311 | progress.succeed( 312 | `缓存编译: ${chalk.blueBright(from)} -> ${chalk.blueBright(to)}` 313 | ); 314 | continue; 315 | } 316 | } 317 | 318 | // 编译 319 | const res = await handleCompile(modulePath); 320 | if (res) { 321 | // 编译信息 322 | const { originRelativePath, compileName, data } = res; 323 | content.push(data); 324 | // 缓存 325 | cache[modulePath] = { 326 | timeStamp: time, 327 | data, 328 | from: originRelativePath, 329 | to: compileName, 330 | }; 331 | progress.succeed( 332 | `直接编译: ${chalk.blueBright( 333 | originRelativePath 334 | )} -> ${chalk.blueBright(compileName)}` 335 | ); 336 | continue; 337 | } 338 | } 339 | progress.fail(`编译失败: ${chalk.red(filePath)}, 请检查导入文件路径!`); 340 | break; 341 | } 342 | 343 | progress.start(`正在生成js文件...`); 344 | // 生成js文件 345 | fs.writeFileSync(compileRelativePath, content.join('\r\n')); 346 | progress.succeed(`生成js文件成功!`); 347 | 348 | if (compress) { 349 | progress.start(`正在压缩js文件...`); 350 | // rollup 压缩 351 | const bundle = await rollup.rollup(rollupConfig.inputOptions); 352 | const { output } = await bundle.write(rollupConfig.outputOptions); 353 | fullData.push(output[0].code); 354 | progress.succeed(`压缩js文件成功!`); 355 | } else { 356 | fullData.push(content.join('\r\n').replace(/(\r\n){2,}/g, '\r\n')); 357 | } 358 | 359 | progress.start( 360 | `正在导出整合的脚本文件... ${chalk.blueBright(outputFilePath)}` 361 | ); 362 | // 导出文件 363 | fs.writeFileSync('cache.json', JSON.stringify(cache)); 364 | // 导出文件 365 | fs.writeFileSync(outputFilePath, fullData.join('\r\n')); 366 | progress.succeed(`导出整合的脚本文件: ${chalk.blueBright(outputFilePath)}`); 367 | return; 368 | } 369 | progress.fail(`编译失败,请检查文件路径!`); 370 | }; 371 | 372 | main(); 373 | -------------------------------------------------------------------------------- /images/done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xu22Web/tech-study-js/a1da2057696ace84715ade359f34cc1cb038d2a6/images/done.png -------------------------------------------------------------------------------- /images/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xu22Web/tech-study-js/a1da2057696ace84715ade359f34cc1cb038d2a6/images/group.png -------------------------------------------------------------------------------- /images/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xu22Web/tech-study-js/a1da2057696ace84715ade359f34cc1cb038d2a6/images/login.png -------------------------------------------------------------------------------- /images/mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xu22Web/tech-study-js/a1da2057696ace84715ade359f34cc1cb038d2a6/images/mobile.png -------------------------------------------------------------------------------- /images/pc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xu22Web/tech-study-js/a1da2057696ace84715ade359f34cc1cb038d2a6/images/pc.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tech-study", 3 | "version": "1.0.0", 4 | "description": "a flexible and light tampermonkey plugin for xuexiqiangguo.", 5 | "main": "tech-study.js", 6 | "scripts": { 7 | "build": "ts-node bin/index.ts" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Xu22Web/tech-study-js.git" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/Xu22Web/tech-study-js/issues" 18 | }, 19 | "homepage": "https://github.com/Xu22Web/tech-study-js#readme", 20 | "devDependencies": { 21 | "@rollup/plugin-terser": "^0.4.0", 22 | "@types/node": "^18.14.6", 23 | "@types/tampermonkey": "^4.0.10", 24 | "chalk": "^4.1.2", 25 | "ora": "^4.1.1", 26 | "rollup": "^3.18.0", 27 | "ts-node": "^10.9.1", 28 | "typescript": "^4.9.5" 29 | } 30 | } -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.4 2 | 3 | specifiers: 4 | '@rollup/plugin-terser': ^0.4.0 5 | '@types/node': ^18.14.6 6 | '@types/tampermonkey': ^4.0.10 7 | chalk: ^4.1.2 8 | ora: ^4.1.1 9 | rollup: ^3.18.0 10 | ts-node: ^10.9.1 11 | typescript: ^4.9.5 12 | 13 | devDependencies: 14 | '@rollup/plugin-terser': 0.4.0_rollup@3.18.0 15 | '@types/node': 18.14.6 16 | '@types/tampermonkey': 4.0.10 17 | chalk: 4.1.2 18 | ora: 4.1.1 19 | rollup: 3.18.0 20 | ts-node: 10.9.1_alpjt73dvgv6kni625hu7f2l4m 21 | typescript: 4.9.5 22 | 23 | packages: 24 | 25 | /@cspotcode/source-map-support/0.8.1: 26 | resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 27 | engines: {node: '>=12'} 28 | dependencies: 29 | '@jridgewell/trace-mapping': 0.3.9 30 | dev: true 31 | 32 | /@jridgewell/gen-mapping/0.3.2: 33 | resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} 34 | engines: {node: '>=6.0.0'} 35 | dependencies: 36 | '@jridgewell/set-array': 1.1.2 37 | '@jridgewell/sourcemap-codec': 1.4.14 38 | '@jridgewell/trace-mapping': 0.3.17 39 | dev: true 40 | 41 | /@jridgewell/resolve-uri/3.1.0: 42 | resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} 43 | engines: {node: '>=6.0.0'} 44 | dev: true 45 | 46 | /@jridgewell/set-array/1.1.2: 47 | resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} 48 | engines: {node: '>=6.0.0'} 49 | dev: true 50 | 51 | /@jridgewell/source-map/0.3.2: 52 | resolution: {integrity: sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==} 53 | dependencies: 54 | '@jridgewell/gen-mapping': 0.3.2 55 | '@jridgewell/trace-mapping': 0.3.17 56 | dev: true 57 | 58 | /@jridgewell/sourcemap-codec/1.4.14: 59 | resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} 60 | dev: true 61 | 62 | /@jridgewell/trace-mapping/0.3.17: 63 | resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} 64 | dependencies: 65 | '@jridgewell/resolve-uri': 3.1.0 66 | '@jridgewell/sourcemap-codec': 1.4.14 67 | dev: true 68 | 69 | /@jridgewell/trace-mapping/0.3.9: 70 | resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 71 | dependencies: 72 | '@jridgewell/resolve-uri': 3.1.0 73 | '@jridgewell/sourcemap-codec': 1.4.14 74 | dev: true 75 | 76 | /@rollup/plugin-terser/0.4.0_rollup@3.18.0: 77 | resolution: {integrity: sha512-Ipcf3LPNerey1q9ZMjiaWHlNPEHNU/B5/uh9zXLltfEQ1lVSLLeZSgAtTPWGyw8Ip1guOeq+mDtdOlEj/wNxQw==} 78 | engines: {node: '>=14.0.0'} 79 | peerDependencies: 80 | rollup: ^2.x || ^3.x 81 | peerDependenciesMeta: 82 | rollup: 83 | optional: true 84 | dependencies: 85 | rollup: 3.18.0 86 | serialize-javascript: 6.0.1 87 | smob: 0.0.6 88 | terser: 5.16.5 89 | dev: true 90 | 91 | /@tsconfig/node10/1.0.9: 92 | resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} 93 | dev: true 94 | 95 | /@tsconfig/node12/1.0.11: 96 | resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} 97 | dev: true 98 | 99 | /@tsconfig/node14/1.0.3: 100 | resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} 101 | dev: true 102 | 103 | /@tsconfig/node16/1.0.3: 104 | resolution: {integrity: sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==} 105 | dev: true 106 | 107 | /@types/node/18.14.6: 108 | resolution: {integrity: sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==} 109 | dev: true 110 | 111 | /@types/tampermonkey/4.0.10: 112 | resolution: {integrity: sha512-E3SYtXgeXG/nnq6uAPiZh7i0XA8jLbtiXraxxHTnsSjzQcQMxWBzbcoGkHgiC+zbHXxxkynUT9zt85SpF8loNw==} 113 | dev: true 114 | 115 | /acorn-walk/8.2.0: 116 | resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} 117 | engines: {node: '>=0.4.0'} 118 | dev: true 119 | 120 | /acorn/8.8.2: 121 | resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} 122 | engines: {node: '>=0.4.0'} 123 | hasBin: true 124 | dev: true 125 | 126 | /ansi-regex/5.0.1: 127 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 128 | engines: {node: '>=8'} 129 | dev: true 130 | 131 | /ansi-styles/3.2.1: 132 | resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} 133 | engines: {node: '>=4'} 134 | dependencies: 135 | color-convert: 1.9.3 136 | dev: true 137 | 138 | /ansi-styles/4.3.0: 139 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 140 | engines: {node: '>=8'} 141 | dependencies: 142 | color-convert: 2.0.1 143 | dev: true 144 | 145 | /arg/4.1.3: 146 | resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} 147 | dev: true 148 | 149 | /buffer-from/1.1.2: 150 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 151 | dev: true 152 | 153 | /chalk/2.4.2: 154 | resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} 155 | engines: {node: '>=4'} 156 | dependencies: 157 | ansi-styles: 3.2.1 158 | escape-string-regexp: 1.0.5 159 | supports-color: 5.5.0 160 | dev: true 161 | 162 | /chalk/3.0.0: 163 | resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} 164 | engines: {node: '>=8'} 165 | dependencies: 166 | ansi-styles: 4.3.0 167 | supports-color: 7.2.0 168 | dev: true 169 | 170 | /chalk/4.1.2: 171 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 172 | engines: {node: '>=10'} 173 | dependencies: 174 | ansi-styles: 4.3.0 175 | supports-color: 7.2.0 176 | dev: true 177 | 178 | /cli-cursor/3.1.0: 179 | resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} 180 | engines: {node: '>=8'} 181 | dependencies: 182 | restore-cursor: 3.1.0 183 | dev: true 184 | 185 | /cli-spinners/2.7.0: 186 | resolution: {integrity: sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==} 187 | engines: {node: '>=6'} 188 | dev: true 189 | 190 | /clone/1.0.4: 191 | resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} 192 | engines: {node: '>=0.8'} 193 | dev: true 194 | 195 | /color-convert/1.9.3: 196 | resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} 197 | dependencies: 198 | color-name: 1.1.3 199 | dev: true 200 | 201 | /color-convert/2.0.1: 202 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 203 | engines: {node: '>=7.0.0'} 204 | dependencies: 205 | color-name: 1.1.4 206 | dev: true 207 | 208 | /color-name/1.1.3: 209 | resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} 210 | dev: true 211 | 212 | /color-name/1.1.4: 213 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 214 | dev: true 215 | 216 | /commander/2.20.3: 217 | resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} 218 | dev: true 219 | 220 | /create-require/1.1.1: 221 | resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} 222 | dev: true 223 | 224 | /defaults/1.0.4: 225 | resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} 226 | dependencies: 227 | clone: 1.0.4 228 | dev: true 229 | 230 | /diff/4.0.2: 231 | resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} 232 | engines: {node: '>=0.3.1'} 233 | dev: true 234 | 235 | /escape-string-regexp/1.0.5: 236 | resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} 237 | engines: {node: '>=0.8.0'} 238 | dev: true 239 | 240 | /fsevents/2.3.2: 241 | resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} 242 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 243 | os: [darwin] 244 | requiresBuild: true 245 | dev: true 246 | optional: true 247 | 248 | /has-flag/3.0.0: 249 | resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} 250 | engines: {node: '>=4'} 251 | dev: true 252 | 253 | /has-flag/4.0.0: 254 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 255 | engines: {node: '>=8'} 256 | dev: true 257 | 258 | /is-interactive/1.0.0: 259 | resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} 260 | engines: {node: '>=8'} 261 | dev: true 262 | 263 | /log-symbols/3.0.0: 264 | resolution: {integrity: sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==} 265 | engines: {node: '>=8'} 266 | dependencies: 267 | chalk: 2.4.2 268 | dev: true 269 | 270 | /make-error/1.3.6: 271 | resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} 272 | dev: true 273 | 274 | /mimic-fn/2.1.0: 275 | resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} 276 | engines: {node: '>=6'} 277 | dev: true 278 | 279 | /mute-stream/0.0.8: 280 | resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} 281 | dev: true 282 | 283 | /onetime/5.1.2: 284 | resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} 285 | engines: {node: '>=6'} 286 | dependencies: 287 | mimic-fn: 2.1.0 288 | dev: true 289 | 290 | /ora/4.1.1: 291 | resolution: {integrity: sha512-sjYP8QyVWBpBZWD6Vr1M/KwknSw6kJOz41tvGMlwWeClHBtYKTbHMki1PsLZnxKpXMPbTKv9b3pjQu3REib96A==} 292 | engines: {node: '>=8'} 293 | dependencies: 294 | chalk: 3.0.0 295 | cli-cursor: 3.1.0 296 | cli-spinners: 2.7.0 297 | is-interactive: 1.0.0 298 | log-symbols: 3.0.0 299 | mute-stream: 0.0.8 300 | strip-ansi: 6.0.1 301 | wcwidth: 1.0.1 302 | dev: true 303 | 304 | /randombytes/2.1.0: 305 | resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} 306 | dependencies: 307 | safe-buffer: 5.2.1 308 | dev: true 309 | 310 | /restore-cursor/3.1.0: 311 | resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} 312 | engines: {node: '>=8'} 313 | dependencies: 314 | onetime: 5.1.2 315 | signal-exit: 3.0.7 316 | dev: true 317 | 318 | /rollup/3.18.0: 319 | resolution: {integrity: sha512-J8C6VfEBjkvYPESMQYxKHxNOh4A5a3FlP+0BETGo34HEcE4eTlgCrO2+eWzlu2a/sHs2QUkZco+wscH7jhhgWg==} 320 | engines: {node: '>=14.18.0', npm: '>=8.0.0'} 321 | hasBin: true 322 | optionalDependencies: 323 | fsevents: 2.3.2 324 | dev: true 325 | 326 | /safe-buffer/5.2.1: 327 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 328 | dev: true 329 | 330 | /serialize-javascript/6.0.1: 331 | resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} 332 | dependencies: 333 | randombytes: 2.1.0 334 | dev: true 335 | 336 | /signal-exit/3.0.7: 337 | resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} 338 | dev: true 339 | 340 | /smob/0.0.6: 341 | resolution: {integrity: sha512-V21+XeNni+tTyiST1MHsa84AQhT1aFZipzPpOFAVB8DkHzwJyjjAmt9bgwnuZiZWnIbMo2duE29wybxv/7HWUw==} 342 | dev: true 343 | 344 | /source-map-support/0.5.21: 345 | resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 346 | dependencies: 347 | buffer-from: 1.1.2 348 | source-map: 0.6.1 349 | dev: true 350 | 351 | /source-map/0.6.1: 352 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 353 | engines: {node: '>=0.10.0'} 354 | dev: true 355 | 356 | /strip-ansi/6.0.1: 357 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 358 | engines: {node: '>=8'} 359 | dependencies: 360 | ansi-regex: 5.0.1 361 | dev: true 362 | 363 | /supports-color/5.5.0: 364 | resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} 365 | engines: {node: '>=4'} 366 | dependencies: 367 | has-flag: 3.0.0 368 | dev: true 369 | 370 | /supports-color/7.2.0: 371 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 372 | engines: {node: '>=8'} 373 | dependencies: 374 | has-flag: 4.0.0 375 | dev: true 376 | 377 | /terser/5.16.5: 378 | resolution: {integrity: sha512-qcwfg4+RZa3YvlFh0qjifnzBHjKGNbtDo9yivMqMFDy9Q6FSaQWSB/j1xKhsoUFJIqDOM3TsN6D5xbrMrFcHbg==} 379 | engines: {node: '>=10'} 380 | hasBin: true 381 | dependencies: 382 | '@jridgewell/source-map': 0.3.2 383 | acorn: 8.8.2 384 | commander: 2.20.3 385 | source-map-support: 0.5.21 386 | dev: true 387 | 388 | /ts-node/10.9.1_alpjt73dvgv6kni625hu7f2l4m: 389 | resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} 390 | hasBin: true 391 | peerDependencies: 392 | '@swc/core': '>=1.2.50' 393 | '@swc/wasm': '>=1.2.50' 394 | '@types/node': '*' 395 | typescript: '>=2.7' 396 | peerDependenciesMeta: 397 | '@swc/core': 398 | optional: true 399 | '@swc/wasm': 400 | optional: true 401 | dependencies: 402 | '@cspotcode/source-map-support': 0.8.1 403 | '@tsconfig/node10': 1.0.9 404 | '@tsconfig/node12': 1.0.11 405 | '@tsconfig/node14': 1.0.3 406 | '@tsconfig/node16': 1.0.3 407 | '@types/node': 18.14.6 408 | acorn: 8.8.2 409 | acorn-walk: 8.2.0 410 | arg: 4.1.3 411 | create-require: 1.1.1 412 | diff: 4.0.2 413 | make-error: 1.3.6 414 | typescript: 4.9.5 415 | v8-compile-cache-lib: 3.0.1 416 | yn: 3.1.1 417 | dev: true 418 | 419 | /typescript/4.9.5: 420 | resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} 421 | engines: {node: '>=4.2.0'} 422 | hasBin: true 423 | dev: true 424 | 425 | /v8-compile-cache-lib/3.0.1: 426 | resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} 427 | dev: true 428 | 429 | /wcwidth/1.0.1: 430 | resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} 431 | dependencies: 432 | defaults: 1.0.4 433 | dev: true 434 | 435 | /yn/3.1.1: 436 | resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} 437 | engines: {node: '>=6'} 438 | dev: true 439 | -------------------------------------------------------------------------------- /src/api/answer.ts: -------------------------------------------------------------------------------- 1 | import API_CONFIG from '../config/api'; 2 | import { log } from '../utils/log'; 3 | /* 答案 API */ 4 | 5 | /** 6 | * @description 获取答案 7 | */ 8 | async function getAnswer(question: string) { 9 | // 数据 10 | const data = { 11 | txt_name: md5(question), 12 | password: '', 13 | }; 14 | try { 15 | const params = new URLSearchParams(data); 16 | // 请求 17 | const res = await fetch(API_CONFIG.answerSearch, { 18 | method: 'POST', 19 | mode: 'cors', 20 | headers: { 21 | 'Content-Type': 'application/x-www-form-urlencoded', 22 | }, 23 | body: params.toString(), 24 | }); 25 | // 请求成功 26 | if (res.ok) { 27 | const result = await res.json(); 28 | const { data, status } = < 29 | { 30 | status: number; 31 | data: { txt_content: string; txt_name: string }; 32 | } 33 | >result; 34 | if (status !== 0) { 35 | // 答案列表 36 | const answerList: { content: string; title: string }[] = JSON.parse( 37 | data.txt_content 38 | ); 39 | // 答案 40 | const answers = answerList[0].content.split(/[;\s]/); 41 | return answers; 42 | } 43 | } 44 | } catch (error) {} 45 | return []; 46 | } 47 | 48 | /** 49 | * @description 保存答案 50 | */ 51 | async function saveAnswer(question: string, answer: string) { 52 | try { 53 | // 内容 54 | const content = JSON.stringify([{ title: md5(question), content: answer }]); 55 | // 数据 56 | const data = { 57 | txt_name: md5(question), 58 | txt_content: content, 59 | password: '', 60 | v_id: '', 61 | }; 62 | const params = new URLSearchParams(data); 63 | // 请求 64 | const res = await fetch(API_CONFIG.answerSave, { 65 | method: 'POST', 66 | mode: 'cors', 67 | headers: { 68 | 'Content-Type': 'application/x-www-form-urlencoded', 69 | }, 70 | body: params.toString(), 71 | }); 72 | // 请求成功 73 | if (res.ok) { 74 | const data = await res.json(); 75 | return data; 76 | } 77 | } catch (error) {} 78 | } 79 | 80 | export { getAnswer, saveAnswer }; 81 | -------------------------------------------------------------------------------- /src/api/data.ts: -------------------------------------------------------------------------------- 1 | import API_CONFIG from '../config/api'; 2 | import { NewsVideoList } from '../types'; 3 | /* 数据 API */ 4 | 5 | /** 6 | * @description 获取新闻数据 7 | */ 8 | async function getNewsList() { 9 | // 随机 10 | const randNum = ~~(Math.random() * API_CONFIG.todayNews.length); 11 | try { 12 | // 获取重要新闻 13 | const res = await fetch(API_CONFIG.todayNews[randNum], { 14 | method: 'GET', 15 | }); 16 | // 请求成功 17 | if (res.ok) { 18 | const data = await res.json(); 19 | return data; 20 | } 21 | } catch (err) {} 22 | } 23 | 24 | /** 25 | * @description 获取视频数据 26 | */ 27 | async function getVideoList() { 28 | // 随机 29 | const randNum = ~~(Math.random() * API_CONFIG.todayVideos.length); 30 | try { 31 | // 获取重要新闻 32 | const res = await fetch(API_CONFIG.todayVideos[randNum], { 33 | method: 'GET', 34 | }); 35 | // 请求成功 36 | if (res.ok) { 37 | const data = await res.json(); 38 | return data; 39 | } 40 | } catch (err) {} 41 | } 42 | 43 | /** 44 | * @description 专项练习数据 45 | */ 46 | async function getExamPaper(pageNo: number) { 47 | // 链接 48 | const url = `${API_CONFIG.paperList}?pageSize=50&pageNo=${pageNo}`; 49 | try { 50 | // 获取专项练习 51 | const res = await fetch(url, { 52 | method: 'GET', 53 | credentials: 'include', 54 | }); 55 | // 请求成功 56 | if (res.ok) { 57 | const data = await res.json(); 58 | const paperJson = decodeURIComponent( 59 | escape(window.atob(data.data_str.replace(/-/g, '+').replace(/_/g, '/'))) 60 | ); 61 | // JSON格式化 62 | const paper = JSON.parse(paperJson); 63 | return paper; 64 | } 65 | } catch (err) { 66 | return []; 67 | } 68 | return []; 69 | } 70 | 71 | export { getVideoList, getNewsList, getExamPaper }; 72 | -------------------------------------------------------------------------------- /src/api/login.ts: -------------------------------------------------------------------------------- 1 | import API_CONFIG from '../config/api'; 2 | 3 | /** 4 | * @description 生成二维码 5 | */ 6 | async function generateQRCode() { 7 | try { 8 | // 推送 9 | const res = await fetch(API_CONFIG.generateQRCode, { 10 | method: 'GET', 11 | mode: 'cors', 12 | }); 13 | // 请求成功 14 | if (res.ok) { 15 | const data = await res.json(); 16 | if (data.success) { 17 | return data.result; 18 | } 19 | } 20 | } catch (error) {} 21 | } 22 | 23 | /** 24 | * @description 用二维码登录 25 | */ 26 | async function loginWithQRCode(qrCode: string) { 27 | try { 28 | const params = new URLSearchParams({ 29 | qrCode, 30 | goto: 'https://oa.xuexi.cn', 31 | pdmToken: '', 32 | }); 33 | // 推送 34 | const res = await fetch(API_CONFIG.loginWithQRCode, { 35 | method: 'POST', 36 | mode: 'cors', 37 | credentials: 'include', 38 | headers: { 39 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 40 | }, 41 | body: params.toString(), 42 | }); 43 | // 请求成功 44 | if (res.ok) { 45 | const data = await res.json(); 46 | return <{ data: string; code: string; success: boolean }>data; 47 | } 48 | } catch (error) {} 49 | } 50 | 51 | /** 52 | * @description 签名 53 | */ 54 | async function getSign() { 55 | try { 56 | // 推送 57 | const res = await fetch(API_CONFIG.sign, { 58 | method: 'GET', 59 | mode: 'cors', 60 | credentials: 'include', 61 | }); 62 | // 请求成功 63 | if (res.ok) { 64 | const data = await res.json(); 65 | if (data.ok) { 66 | return data.data.sign; 67 | } 68 | } 69 | } catch (error) {} 70 | } 71 | 72 | /** 73 | * @description 安全检查 74 | * @param data 75 | */ 76 | async function secureCheck(data: { code: string; state: string }) { 77 | try { 78 | const params = new URLSearchParams(data); 79 | const url = `${API_CONFIG.secureCheck}?${params}`; 80 | // 推送 81 | const res = await fetch(url, { 82 | method: 'GET', 83 | mode: 'cors', 84 | credentials: 'include', 85 | }); 86 | // 请求成功 87 | if (res.ok) { 88 | const data = await res.json(); 89 | return data.success; 90 | } 91 | } catch (error) {} 92 | return false; 93 | } 94 | 95 | export { generateQRCode, loginWithQRCode, getSign, secureCheck }; 96 | -------------------------------------------------------------------------------- /src/api/push.ts: -------------------------------------------------------------------------------- 1 | import API_CONFIG from '../config/api'; 2 | /* 推送 API */ 3 | 4 | /** 5 | * @description 推送 6 | */ 7 | async function pushPlus( 8 | token: string, 9 | title: string, 10 | content: string, 11 | template: string, 12 | toToken?: string 13 | ) { 14 | try { 15 | // 参数体 16 | const body: { 17 | token: string; 18 | title: string; 19 | content: string; 20 | template: string; 21 | to?: string; 22 | } = { 23 | token, 24 | title, 25 | content, 26 | template, 27 | }; 28 | // 好友令牌 29 | if (toToken) { 30 | body.to = toToken; 31 | } 32 | // 推送 33 | const res = await fetch(API_CONFIG.push, { 34 | method: 'POST', 35 | mode: 'cors', 36 | headers: { 37 | 'Content-Type': 'application/json', 38 | }, 39 | body: JSON.stringify(body), 40 | }); 41 | // 请求成功 42 | if (res.ok) { 43 | const data = await res.json(); 44 | return data; 45 | } 46 | } catch (error) {} 47 | } 48 | 49 | export { pushPlus }; 50 | -------------------------------------------------------------------------------- /src/api/user.ts: -------------------------------------------------------------------------------- 1 | import API_CONFIG from '../config/api'; 2 | 3 | /** 4 | * @description 用户信息 5 | */ 6 | type UserInfo = { 7 | avatarMediaUrl?: string; 8 | nick: string; 9 | }; 10 | /* 用户 API */ 11 | 12 | /** 13 | * @description 获取用户信息 14 | */ 15 | async function getUserInfo(): Promise { 16 | try { 17 | const res = await fetch(API_CONFIG.userInfo, { 18 | method: 'GET', 19 | credentials: 'include', 20 | }); 21 | // 请求成功 22 | if (res.ok) { 23 | const { data } = await res.json(); 24 | return data; 25 | } 26 | } catch (err) {} 27 | } 28 | 29 | /** 30 | * @description 获取总积分 31 | */ 32 | async function getTotalScore() { 33 | try { 34 | const res = await fetch(API_CONFIG.totalScore, { 35 | method: 'GET', 36 | credentials: 'include', 37 | }); 38 | // 请求成功 39 | if (res.ok) { 40 | const { data } = await res.json(); 41 | // 总分 42 | const { score } = data; 43 | return score; 44 | } 45 | } catch (err) {} 46 | } 47 | 48 | /** 49 | * @description 获取当天总积分 50 | */ 51 | async function getTodayScore() { 52 | try { 53 | const res = await fetch(API_CONFIG.todayScore, { 54 | method: 'GET', 55 | credentials: 'include', 56 | }); 57 | // 请求成功 58 | if (res.ok) { 59 | const { data } = await res.json(); 60 | // 当天总分 61 | const { score } = data; 62 | return score; 63 | } 64 | } catch (err) {} 65 | } 66 | 67 | /** 68 | * @description 获取任务列表 69 | */ 70 | async function getTaskList() { 71 | try { 72 | const res = await fetch(API_CONFIG.taskList, { 73 | method: 'GET', 74 | credentials: 'include', 75 | }); 76 | // 请求成功 77 | if (res.ok) { 78 | const { data } = await res.json(); 79 | // 进度和当天总分 80 | const { taskProgress } = data; 81 | return taskProgress; 82 | } 83 | } catch (err) {} 84 | } 85 | 86 | export { getUserInfo, getTotalScore, getTodayScore, getTaskList }; 87 | -------------------------------------------------------------------------------- /src/component/ExamBtn.ts: -------------------------------------------------------------------------------- 1 | import { examPause, settings } from '../shared'; 2 | import { SettingType } from '../types'; 3 | import { watchEffect, watchEffectRef } from '../utils/composition'; 4 | import { createElementNode, createTextNode } from '../utils/element'; 5 | 6 | /** 7 | * @description 答题按钮 8 | */ 9 | function ExamBtn() { 10 | // 设置初始状态 11 | watchEffect(() => (examPause.value = !settings[SettingType.AUTO_ANSWER])); 12 | return createElementNode( 13 | 'button', 14 | undefined, 15 | { 16 | class: watchEffectRef( 17 | () => `egg_exam_btn${examPause.value ? ' manual' : ''}` 18 | ), 19 | type: 'button', 20 | onclick(e: Event) { 21 | e.stopPropagation(); 22 | examPause.value = !examPause.value; 23 | }, 24 | onmousedown(e: Event) { 25 | e.stopPropagation(); 26 | }, 27 | onmousemove(e: Event) { 28 | e.stopPropagation(); 29 | }, 30 | onmouseup(e: Event) { 31 | e.stopPropagation(); 32 | }, 33 | onmouseenter(e: Event) { 34 | e.stopPropagation(); 35 | }, 36 | onmouseleave(e: Event) { 37 | e.stopPropagation(); 38 | }, 39 | onmouseover(e: Event) { 40 | e.stopPropagation(); 41 | }, 42 | ontouchstart(e: Event) { 43 | e.stopPropagation(); 44 | }, 45 | ontouchmove(e: Event) { 46 | e.stopPropagation(); 47 | }, 48 | ontouchend(e: Event) { 49 | e.stopPropagation(); 50 | }, 51 | oninput(e: Event) { 52 | e.stopPropagation(); 53 | }, 54 | onchange(e: Event) { 55 | e.stopPropagation(); 56 | }, 57 | onblur(e: Event) { 58 | e.stopPropagation(); 59 | }, 60 | }, 61 | createTextNode( 62 | watchEffectRef( 63 | () => `${examPause.value ? '开启自动答题' : '关闭自动答题'}` 64 | ) 65 | ) 66 | ); 67 | } 68 | 69 | export { ExamBtn }; 70 | -------------------------------------------------------------------------------- /src/component/Frame.ts: -------------------------------------------------------------------------------- 1 | import { closeFrame, closeTaskWin, closeWin } from '../controller/frame'; 2 | import { frame, login, running, settings, taskStatus } from '../shared'; 3 | import { SettingType, TaskStatusType, TaskType } from '../types'; 4 | import { ref, watch, watchEffectRef, watchRef } from '../utils/composition'; 5 | import { createElementNode, createNSElementNode } from '../utils/element'; 6 | import { debounce } from '../utils/utils'; 7 | 8 | /** 9 | * @description 任务窗口 10 | * @returns 11 | */ 12 | function Frame() { 13 | // 最大化 14 | const max = ref(false); 15 | // 容器 16 | return createElementNode( 17 | 'div', 18 | undefined, 19 | { 20 | class: watchEffectRef(() => `egg_frame_wrap${frame.show ? '' : ' hide'}`), 21 | onclick(e: Event) { 22 | e.stopPropagation(); 23 | }, 24 | onmousedown(e: Event) { 25 | e.stopPropagation(); 26 | }, 27 | onmousemove(e: Event) { 28 | e.stopPropagation(); 29 | }, 30 | onmouseup(e: Event) { 31 | e.stopPropagation(); 32 | }, 33 | onmouseenter(e: Event) { 34 | e.stopPropagation(); 35 | }, 36 | onmouseleave(e: Event) { 37 | e.stopPropagation(); 38 | }, 39 | onmouseover(e: Event) { 40 | e.stopPropagation(); 41 | }, 42 | ontouchstart(e: Event) { 43 | e.stopPropagation(); 44 | }, 45 | ontouchmove(e: Event) { 46 | e.stopPropagation(); 47 | }, 48 | ontouchend(e: Event) { 49 | e.stopPropagation(); 50 | }, 51 | oninput(e: Event) { 52 | e.stopPropagation(); 53 | }, 54 | onchange(e: Event) { 55 | e.stopPropagation(); 56 | }, 57 | onblur(e: Event) { 58 | e.stopPropagation(); 59 | }, 60 | }, 61 | watchRef( 62 | () => [login.value, settings[SettingType.SAME_TAB]], 63 | () => { 64 | // 同屏任务 65 | if (login.value && settings[SettingType.SAME_TAB]) { 66 | return [ 67 | // 遮罩 68 | createElementNode('div', undefined, { class: 'egg_frame_mask' }), 69 | // 窗口内容 70 | createElementNode( 71 | 'div', 72 | undefined, 73 | { 74 | class: watchEffectRef( 75 | () => `egg_frame_content_wrap ${max.value ? ' max' : ''}` 76 | ), 77 | }, 78 | [ 79 | // 窗口控制 80 | createElementNode( 81 | 'div', 82 | undefined, 83 | { class: 'egg_frame_controls_wrap' }, 84 | [ 85 | // 标题 86 | createElementNode('div', undefined, { 87 | class: 'egg_frame_title', 88 | }), 89 | createElementNode( 90 | 'div', 91 | undefined, 92 | { 93 | class: 'egg_frame_controls', 94 | }, 95 | [ 96 | // 隐藏 97 | createElementNode( 98 | 'button', 99 | undefined, 100 | { 101 | class: 'egg_frame_btn', 102 | type: 'button', 103 | title: '隐藏', 104 | onclick: debounce(() => { 105 | // 隐藏窗口 106 | frame.show = false; 107 | }, 300), 108 | }, 109 | createNSElementNode( 110 | 'svg', 111 | undefined, 112 | { 113 | viewBox: '0 0 1024 1024', 114 | class: 'egg_icon', 115 | }, 116 | createNSElementNode('path', undefined, { 117 | d: 'M863.7 552.5H160.3c-10.6 0-19.2-8.6-19.2-19.2v-41.7c0-10.6 8.6-19.2 19.2-19.2h703.3c10.6 0 19.2 8.6 19.2 19.2v41.7c0 10.6-8.5 19.2-19.1 19.2z', 118 | }) 119 | ) 120 | ), 121 | // 改变大小 122 | createElementNode( 123 | 'button', 124 | undefined, 125 | { 126 | class: 'egg_frame_btn', 127 | type: 'button', 128 | title: '缩放', 129 | onclick: debounce(() => { 130 | max.value = !max.value; 131 | }, 300), 132 | }, 133 | createNSElementNode( 134 | 'svg', 135 | undefined, 136 | { 137 | viewBox: '0 0 1024 1024', 138 | class: 'egg_icon', 139 | }, 140 | createNSElementNode('path', undefined, { 141 | d: 'M609.52 584.92a35.309 35.309 0 0 1 24.98-10.36c9.37 0 18.36 3.73 24.98 10.36l189.29 189.22-0.07-114.3 0.57-6.35c3.25-17.98 19.7-30.5 37.9-28.85 18.2 1.65 32.12 16.92 32.09 35.2v200.23c-0.05 1.49-0.19 2.97-0.42 4.45l-0.21 1.13c-0.22 1.44-0.55 2.85-0.99 4.24l-0.57 1.62-0.56 1.41a34.163 34.163 0 0 1-7.62 11.36l2.12-2.4-0.14 0.14-0.92 1.06-1.06 1.2-0.57 0.57-0.56 0.57a36.378 36.378 0 0 1-16.23 8.39l-3.53 0.5-4.02 0.35h-199.6l-6.35-0.63c-16.73-3.06-28.9-17.63-28.93-34.64l0.56-6.35c3.07-16.76 17.67-28.93 34.71-28.92l114.29-0.14-189.07-189.1-4.09-4.94c-9.71-14.01-8.01-32.95 4.02-45.02z m-162.06 0c12.06 12.05 13.78 30.99 4.09 45.01l-4.09 4.94-189.15 189.08 114.3 0.14c17.04-0.01 31.65 12.17 34.71 28.92l0.57 6.35c-0.03 17.01-12.19 31.58-28.92 34.64l-6.35 0.63H173.09l-4.23-0.42-3.39-0.49a36.38 36.38 0 0 1-17.36-9.52l-1.06-1.13-0.98-1.13 0.98 1.06-1.97-2.26 0.85 1.06-0.42-0.56a35.137 35.137 0 0 1-3.74-5.64l-1.13-2.68a34.71 34.71 0 0 1-2.11-7.33l-0.28-1.13c-0.21-1.47-0.33-2.96-0.36-4.45V659.78c-0.03-18.28 13.89-33.55 32.09-35.2 18.2-1.65 34.65 10.87 37.9 28.85l0.57 6.35-0.07 114.36 189.29-189.22c13.77-13.77 36.11-13.77 49.88 0h-0.09z m-74.71-471.71l6.35 0.57c16.76 3.06 28.93 17.67 28.92 34.71l-0.63 6.35c-3.07 16.76-17.67 28.93-34.71 28.92l-114.3 0.14 189.15 189.08 4.09 4.94c10.26 15.02 7.42 35.37-6.55 47.01-13.98 11.63-34.51 10.74-47.42-2.07L208.29 233.71l0.07 114.3-0.57 6.35c-3.25 17.98-19.7 30.5-37.9 28.85-18.2-1.65-32.12-16.92-32.09-35.2V147.78c0-1.55 0.14-3.03 0.35-4.51l0.21-1.13c0.24-1.44 0.59-2.85 1.06-4.23a34.97 34.97 0 0 1 8.68-14.39l-2.12 2.4-0.42 0.57 1.55-1.84-0.99 1.06 0.92-0.98 2.26-2.33c3.04-2.73 6.52-4.92 10.3-6.49l2.82-1.06c3.45-1.07 7.04-1.62 10.65-1.62l-3.6 0.14h0.49l1.48-0.14h201.31z m512.91 0l1.41 0.14h0.42c2.43 0.29 4.84 0.79 7.19 1.48l2.82 1.06 2.61 1.2 3.04 1.76c2.09 1.33 4.03 2.89 5.78 4.66l1.13 1.2 0.78 0.98 0.21 0.14 0.49 0.64 2.33 3.17c2.35 3.83 3.98 8.07 4.8 12.49l0.21 1.13c0.21 1.48 0.35 2.96 0.35 4.44v200.37c-0.16 18.13-14.03 33.19-32.08 34.83-18.06 1.64-34.42-10.67-37.83-28.48l-0.57-6.35V233.65L659.54 422.87c-12.9 12.95-33.56 13.91-47.59 2.2-14.04-11.71-16.81-32.2-6.38-47.22l4.02-4.86 189.22-189.08-114.29-0.14c-17.06 0.04-31.71-12.14-34.78-28.92l-0.63-6.35c-0.01-17.04 12.16-31.65 28.93-34.71l6.35-0.57h201.27z m0 0', 142 | }) 143 | ) 144 | ), 145 | // 关闭 146 | createElementNode( 147 | 'button', 148 | undefined, 149 | { 150 | class: 'egg_frame_btn', 151 | type: 'button', 152 | title: '关闭', 153 | onclick: debounce(() => { 154 | // 关闭窗口 155 | closeFrame(); 156 | }, 300), 157 | }, 158 | createNSElementNode( 159 | 'svg', 160 | undefined, 161 | { 162 | viewBox: '0 0 1024 1024', 163 | class: 'egg_icon', 164 | }, 165 | createNSElementNode('path', undefined, { 166 | d: 'M453.44 512L161.472 220.032a41.408 41.408 0 0 1 58.56-58.56L512 453.44 803.968 161.472a41.408 41.408 0 0 1 58.56 58.56L570.56 512l291.968 291.968a41.408 41.408 0 0 1-58.56 58.56L512 570.56 220.032 862.528a41.408 41.408 0 0 1-58.56-58.56L453.44 512z', 167 | }) 168 | ) 169 | ), 170 | ] 171 | ), 172 | ] 173 | ), 174 | // 窗口内容 175 | createElementNode( 176 | 'div', 177 | undefined, 178 | { 179 | class: 'egg_frame_content', 180 | }, 181 | watchEffectRef(() => 182 | frame.src 183 | ? [ 184 | createElementNode( 185 | 'iframe', 186 | undefined, 187 | { 188 | class: 'egg_frame', 189 | src: frame.src, 190 | ref(ele) { 191 | frame.ele = ele; 192 | }, 193 | }, 194 | undefined 195 | ), 196 | ] 197 | : undefined 198 | ) 199 | ), 200 | ], 201 | { 202 | onMounted() { 203 | // 隐藏窗口 204 | watch( 205 | () => [ 206 | taskStatus.value, 207 | running.value, 208 | settings[SettingType.SAME_TAB], 209 | settings[SettingType.SILENT_RUN], 210 | ], 211 | () => { 212 | // 同屏任务 213 | if ( 214 | settings[SettingType.SAME_TAB] && 215 | (taskStatus.value === TaskStatusType.START || 216 | taskStatus.value === TaskStatusType.PAUSE || 217 | running.value) 218 | ) { 219 | // 设置窗口显示 220 | frame.show = !settings[SettingType.SILENT_RUN]; 221 | } 222 | } 223 | ); 224 | }, 225 | } 226 | ), 227 | ]; 228 | } 229 | } 230 | ), 231 | { 232 | onMounted() { 233 | // 关闭窗口 234 | watch( 235 | () => [login.value, settings[SettingType.SAME_TAB]], 236 | () => { 237 | if (login.value) { 238 | if (settings[SettingType.SAME_TAB]) { 239 | frame.exist = true; 240 | closeWin(); 241 | } else { 242 | closeFrame(); 243 | frame.exist = false; 244 | } 245 | } else { 246 | closeWin(); 247 | closeFrame(); 248 | frame.exist = false; 249 | } 250 | } 251 | ); 252 | }, 253 | } 254 | ); 255 | } 256 | 257 | export { Frame }; 258 | -------------------------------------------------------------------------------- /src/component/Hr.ts: -------------------------------------------------------------------------------- 1 | import { createElementNode, createTextNode } from '../utils/element'; 2 | 3 | /** 4 | * @description 分隔符 5 | * @returns 6 | */ 7 | function Hr({ text }: { text: string }) { 8 | return createElementNode( 9 | 'div', 10 | undefined, 11 | { 12 | class: 'egg_hr_wrap', 13 | }, 14 | [ 15 | createElementNode('div', undefined, { class: 'egg_hr' }), 16 | createElementNode( 17 | 'div', 18 | undefined, 19 | { class: 'egg_hr_title' }, 20 | createTextNode(text) 21 | ), 22 | createElementNode('div', undefined, { class: 'egg_hr' }), 23 | ] 24 | ); 25 | } 26 | 27 | export { Hr }; 28 | -------------------------------------------------------------------------------- /src/component/InfoItem.ts: -------------------------------------------------------------------------------- 1 | import { handleLogout } from '../controller/login'; 2 | import { refreshUserInfo } from '../controller/user'; 3 | import { login, userinfo } from '../shared'; 4 | import { watchEffectRef } from '../utils/composition'; 5 | import { createElementNode, createTextNode } from '../utils/element'; 6 | import { debounce } from '../utils/utils'; 7 | 8 | /** 9 | * @description 信息 10 | * @returns 11 | */ 12 | function InfoItem() { 13 | return watchEffectRef(() => { 14 | if (login.value) { 15 | return createElementNode( 16 | 'div', 17 | undefined, 18 | { 19 | class: 'egg_info_item', 20 | }, 21 | [ 22 | // 用户信息 23 | createElementNode('div', undefined, { class: 'egg_userinfo' }, [ 24 | // 头像 25 | createElementNode( 26 | 'div', 27 | undefined, 28 | { class: 'egg_avatar' }, 29 | watchEffectRef(() => { 30 | return [ 31 | userinfo.avatar 32 | ? createElementNode('img', undefined, { 33 | src: userinfo.avatar, 34 | class: 'egg_avatar_img', 35 | }) 36 | : createElementNode( 37 | 'div', 38 | undefined, 39 | { 40 | class: 'egg_avatar_nick', 41 | }, 42 | createTextNode( 43 | watchEffectRef(() => userinfo.nick.substring(1, 3)) 44 | ) 45 | ), 46 | ]; 47 | }) 48 | ), 49 | // 昵称 50 | createElementNode( 51 | 'div', 52 | undefined, 53 | { class: 'egg_nick' }, 54 | createTextNode(watchEffectRef(() => userinfo.nick)) 55 | ), 56 | ]), 57 | // 退出按钮 58 | createElementNode( 59 | 'button', 60 | undefined, 61 | { 62 | type: 'button', 63 | class: 'egg_login_btn', 64 | onclick: debounce(() => { 65 | // 退出登录 66 | handleLogout(); 67 | }, 300), 68 | }, 69 | createTextNode('退出') 70 | ), 71 | ], 72 | { 73 | onMounted() { 74 | // 刷新用户信息 75 | refreshUserInfo(); 76 | }, 77 | } 78 | ); 79 | } 80 | }); 81 | } 82 | 83 | export { InfoItem }; 84 | -------------------------------------------------------------------------------- /src/component/LoginItem.ts: -------------------------------------------------------------------------------- 1 | import { handleLogin } from '../controller/login'; 2 | import { login, loginQRCodeShow, settings } from '../shared'; 3 | import { SettingType } from '../types'; 4 | import { watch, watchEffectRef } from '../utils/composition'; 5 | import { createElementNode, createTextNode } from '../utils/element'; 6 | import { debounce } from '../utils/utils'; 7 | 8 | /** 9 | * @description 登录 10 | */ 11 | function LoginItem() { 12 | return watchEffectRef(() => { 13 | return login.value 14 | ? undefined 15 | : createElementNode( 16 | 'div', 17 | undefined, 18 | { 19 | class: 'egg_login_item', 20 | }, 21 | [ 22 | // 登录按钮 23 | createElementNode( 24 | 'button', 25 | undefined, 26 | { 27 | type: 'button', 28 | class: 'egg_login_btn', 29 | onclick: debounce(async () => { 30 | // 开始登录 31 | handleLogin(); 32 | }, 300), 33 | }, 34 | createTextNode('扫码登录') 35 | ), 36 | // 窗口 37 | createElementNode( 38 | 'div', 39 | undefined, 40 | { 41 | class: watchEffectRef( 42 | () => 43 | `egg_login_img_wrap${ 44 | loginQRCodeShow.value ? ' active' : '' 45 | }` 46 | ), 47 | }, 48 | createElementNode('img', undefined, { 49 | class: 'egg_login_img', 50 | }) 51 | ), 52 | ], 53 | { 54 | onMounted() { 55 | watch( 56 | () => settings[SettingType.SCHEDULE_RUN], 57 | () => { 58 | // 未开启定时展示二维码 59 | if (!settings[SettingType.SCHEDULE_RUN]) { 60 | // 开始登录 61 | handleLogin(); 62 | } 63 | }, 64 | true 65 | ); 66 | }, 67 | } 68 | ); 69 | }); 70 | } 71 | 72 | export { LoginItem }; 73 | -------------------------------------------------------------------------------- /src/component/NoramlItem.ts: -------------------------------------------------------------------------------- 1 | import { createElementNode, createTextNode } from '../utils/element'; 2 | 3 | /** 4 | * @description 设置普通项 5 | * @returns 6 | */ 7 | function NormalItem({ 8 | title, 9 | tip, 10 | checked, 11 | onchange, 12 | }: { 13 | title: string; 14 | tip: string; 15 | checked: boolean; 16 | onchange: (e: Event) => void; 17 | }) { 18 | return createElementNode('div', undefined, { class: 'egg_setting_item' }, [ 19 | createElementNode('div', undefined, { class: 'egg_label_wrap' }, [ 20 | createElementNode('label', undefined, { class: 'egg_task_title' }, [ 21 | createTextNode(title), 22 | createElementNode( 23 | 'span', 24 | undefined, 25 | { 26 | class: 'egg_detail', 27 | title: tip, 28 | }, 29 | createTextNode('i') 30 | ), 31 | ]), 32 | ]), 33 | createElementNode('input', undefined, { 34 | title: tip, 35 | class: 'egg_switch', 36 | type: 'checkbox', 37 | checked, 38 | onchange, 39 | }), 40 | ]); 41 | } 42 | 43 | export { NormalItem }; 44 | -------------------------------------------------------------------------------- /src/component/Panel.ts: -------------------------------------------------------------------------------- 1 | import { doExamPaper } from '../controller/exam'; 2 | import { createTip } from '../controller/tip'; 3 | import { frame, login, running, settings, taskStatus } from '../shared'; 4 | import { SettingType, TaskStatusType } from '../types'; 5 | import { ref, watchEffectRef, watchRef } from '../utils/composition'; 6 | import { 7 | createElementNode, 8 | createNSElementNode, 9 | createTextNode, 10 | } from '../utils/element'; 11 | import { debounce, hasMobile } from '../utils/utils'; 12 | import { Hr } from './Hr'; 13 | import { InfoItem } from './InfoItem'; 14 | import { LoginItem } from './LoginItem'; 15 | import { NormalItem } from './NoramlItem'; 16 | import { ScoreItem } from './ScoreItem'; 17 | import { SettingsPanel } from './SettingsPanel'; 18 | import { TaskBtn } from './TaskBtn'; 19 | import { TaskList } from './TaskList'; 20 | 21 | /** 22 | * @description 面板 23 | * @returns 24 | */ 25 | function Panel() { 26 | // 运行设置标签 27 | const runLabels = [ 28 | { 29 | title: '自动开始', 30 | tip: '启动时, 自动开始任务, 在倒计时结束前自动开始可随时取消; 如果在自动开始前手动开始任务, 此次自动开始将取消', 31 | type: SettingType.AUTO_START, 32 | }, 33 | { 34 | title: '同屏任务', 35 | tip: '运行任务时,所有任务均在当前页面以弹窗方式运行', 36 | type: SettingType.SAME_TAB, 37 | }, 38 | { 39 | title: '静默运行', 40 | tip: '同屏任务时, 不显示任务弹窗静默运行', 41 | type: SettingType.SILENT_RUN, 42 | }, 43 | { 44 | title: '定时刷新', 45 | tip: '定时刷新页面,重新进行任务,此功能需要长时间占用浏览器', 46 | type: SettingType.SCHEDULE_RUN, 47 | }, 48 | { 49 | title: '视频静音', 50 | tip: '视听学习时,静音播放视频', 51 | type: SettingType.VIDEO_MUTED, 52 | }, 53 | ]; 54 | // 运行设置标签 55 | const examLabels = [ 56 | { 57 | title: '随机作答', 58 | tip: '无答案时, 随机选择或者填入答案, 不保证正确', 59 | type: SettingType.RANDOM_EXAM, 60 | }, 61 | { 62 | title: '自动答题', 63 | tip: '进入答题页面时,自动答题并提交答案', 64 | type: SettingType.AUTO_ANSWER, 65 | }, 66 | ]; 67 | // 推送设置标签 68 | const pushLabels = [ 69 | { 70 | title: '远程推送', 71 | tip: '利用 pushplus 推送, 将登录二维码直接推送到微信公众号', 72 | type: SettingType.REMOTE_PUSH, 73 | }, 74 | ]; 75 | // 处理设置变化 76 | const handleSettingsChange = (e: Event, type: SettingType, title: string) => { 77 | // 开关 78 | const { checked } = e.target; 79 | if (settings[type] !== checked) { 80 | settings[type] = checked; 81 | // 设置 82 | GM_setValue('studySettings', JSON.stringify(settings)); 83 | // 创建提示 84 | createTip(`${title} ${checked ? '打开' : '关闭'}!`); 85 | } 86 | }; 87 | // 任务显示 88 | const scheduleShow = ref(false); 89 | // 面板显示 90 | const panelShow = ref(false); 91 | return createElementNode( 92 | 'div', 93 | undefined, 94 | { 95 | class: `egg_panel_wrap${hasMobile() ? ' mobile' : ''}`, 96 | onclick(e: Event) { 97 | e.stopPropagation(); 98 | }, 99 | onmousedown(e: Event) { 100 | e.stopPropagation(); 101 | }, 102 | onmousemove(e: Event) { 103 | e.stopPropagation(); 104 | }, 105 | onmouseup(e: Event) { 106 | e.stopPropagation(); 107 | }, 108 | onmouseenter(e: Event) { 109 | e.stopPropagation(); 110 | }, 111 | onmouseleave(e: Event) { 112 | e.stopPropagation(); 113 | }, 114 | onmouseover(e: Event) { 115 | e.stopPropagation(); 116 | }, 117 | ontouchstart(e: Event) { 118 | e.stopPropagation(); 119 | }, 120 | ontouchmove(e: Event) { 121 | e.stopPropagation(); 122 | }, 123 | ontouchend(e: Event) { 124 | e.stopPropagation(); 125 | }, 126 | oninput(e: Event) { 127 | e.stopPropagation(); 128 | }, 129 | onchange(e: Event) { 130 | e.stopPropagation(); 131 | }, 132 | onblur(e: Event) { 133 | e.stopPropagation(); 134 | }, 135 | }, 136 | createElementNode( 137 | 'div', 138 | undefined, 139 | { 140 | class: watchEffectRef( 141 | () => `egg_panel${panelShow.value ? ' hide' : ''}` 142 | ), 143 | }, 144 | [ 145 | // 登录 146 | LoginItem(), 147 | // 信息 148 | InfoItem(), 149 | // 分数 150 | ScoreItem(), 151 | // 任务部分 152 | Hr({ text: '任务' }), 153 | TaskList(), 154 | // 运行部分 155 | Hr({ text: '运行' }), 156 | createElementNode( 157 | 'div', 158 | undefined, 159 | { class: 'egg_run_list' }, 160 | runLabels.map((label) => { 161 | return NormalItem({ 162 | title: label.title, 163 | tip: label.tip, 164 | checked: settings[label.type], 165 | onchange: debounce((e) => { 166 | handleSettingsChange(e, label.type, label.title); 167 | }, 300), 168 | }); 169 | }) 170 | ), 171 | // 答题部分 172 | Hr({ text: '答题' }), 173 | createElementNode( 174 | 'div', 175 | undefined, 176 | { class: 'egg_exam_list' }, 177 | examLabels.map((label) => { 178 | return NormalItem({ 179 | title: label.title, 180 | tip: label.tip, 181 | checked: settings[label.type], 182 | onchange: debounce((e) => { 183 | handleSettingsChange(e, label.type, label.title); 184 | }, 300), 185 | }); 186 | }) 187 | ), 188 | // 推送部分 189 | Hr({ text: '推送' }), 190 | createElementNode( 191 | 'div', 192 | undefined, 193 | { class: 'egg_push_list' }, 194 | pushLabels.map((label) => { 195 | return NormalItem({ 196 | title: label.title, 197 | tip: label.tip, 198 | checked: settings[label.type], 199 | onchange: debounce((e) => { 200 | handleSettingsChange(e, label.type, label.title); 201 | }, 300), 202 | }); 203 | }) 204 | ), 205 | // 提示部分 206 | Hr({ text: '提示' }), 207 | createElementNode( 208 | 'div', 209 | undefined, 210 | { class: 'egg_tip_list' }, 211 | watchRef(login, () => 212 | login.value 213 | ? [ 214 | createTextNode('专项练习已被移除, 如需使用, 请点击'), 215 | createElementNode( 216 | 'button', 217 | undefined, 218 | { 219 | class: 'egg_tip_btn', 220 | type: 'button', 221 | onclick: debounce(doExamPaper, 300), 222 | disabled: watchRef( 223 | () => [running.value, taskStatus.value], 224 | () => 225 | running.value || 226 | taskStatus.value === TaskStatusType.START || 227 | taskStatus.value === TaskStatusType.PAUSE 228 | ), 229 | }, 230 | createTextNode('去完成') 231 | ), 232 | ] 233 | : [ 234 | createElementNode( 235 | 'div', 236 | undefined, 237 | { class: 'egg_tip_content' }, 238 | createTextNode('请先登录!') 239 | ), 240 | ] 241 | ) 242 | ), 243 | // 按钮集合 244 | createElementNode( 245 | 'div', 246 | undefined, 247 | { 248 | class: 'egg_btns_wrap', 249 | }, 250 | [ 251 | createElementNode( 252 | 'button', 253 | undefined, 254 | { 255 | class: watchRef( 256 | () => [frame.exist, frame.show], 257 | () => 258 | `egg_frame_show_btn${ 259 | !frame.exist || frame.show ? ' hide' : '' 260 | }` 261 | ), 262 | title: '窗口', 263 | type: 'button', 264 | onclick: debounce(() => { 265 | // 窗口显示 266 | frame.show = true; 267 | }, 300), 268 | }, 269 | createNSElementNode( 270 | 'svg', 271 | undefined, 272 | { 273 | viewBox: '0 0 1024 1024', 274 | class: 'egg_icon', 275 | }, 276 | createNSElementNode('path', undefined, { 277 | d: 'M836.224 106.666667h-490.666667a85.589333 85.589333 0 0 0-85.333333 85.333333V256h-64a85.589333 85.589333 0 0 0-85.333333 85.333333v490.666667a85.589333 85.589333 0 0 0 85.333333 85.333333h490.666667a85.589333 85.589333 0 0 0 85.333333-85.333333V768h64a85.589333 85.589333 0 0 0 85.333333-85.333333V192a85.589333 85.589333 0 0 0-85.333333-85.333333z m-132.266667 725.333333a20.138667 20.138667 0 0 1-21.333333 21.333333h-490.666667a20.138667 20.138667 0 0 1-21.333333-21.333333V341.333333a20.138667 20.138667 0 0 1 21.333333-21.333333h494.933334a20.138667 20.138667 0 0 1 21.333333 21.333333v490.666667z m153.6-149.333333a20.138667 20.138667 0 0 1-21.333333 21.333333h-64V341.333333a85.589333 85.589333 0 0 0-85.333333-85.333333h-362.666667V192a20.138667 20.138667 0 0 1 21.333333-21.333333h490.666667a20.138667 20.138667 0 0 1 21.333333 21.333333z', 278 | }) 279 | ) 280 | ), 281 | createElementNode( 282 | 'button', 283 | undefined, 284 | { 285 | class: 'egg_panel_show_btn', 286 | title: '面板', 287 | type: 'button', 288 | onclick: debounce(() => { 289 | panelShow.value = !panelShow.value; 290 | }, 300), 291 | }, 292 | createNSElementNode( 293 | 'svg', 294 | undefined, 295 | { 296 | viewBox: '0 0 1024 1024', 297 | class: 'egg_icon', 298 | }, 299 | createNSElementNode('path', undefined, { 300 | d: 'M332.16 883.84a40.96 40.96 0 0 0 58.24 0l338.56-343.04a40.96 40.96 0 0 0 0-58.24L390.4 140.16a40.96 40.96 0 0 0-58.24 58.24L640 512l-307.84 314.24a40.96 40.96 0 0 0 0 57.6z', 301 | }) 302 | ) 303 | ), 304 | createElementNode( 305 | 'button', 306 | undefined, 307 | { 308 | class: watchEffectRef( 309 | () => 310 | `egg_settings_show_btn${ 311 | scheduleShow.value ? ' active' : '' 312 | }` 313 | ), 314 | title: '设置', 315 | type: 'button', 316 | onclick: debounce(() => { 317 | scheduleShow.value = !scheduleShow.value; 318 | }, 300), 319 | }, 320 | createNSElementNode( 321 | 'svg', 322 | undefined, 323 | { 324 | viewBox: '0 0 1024 1024', 325 | class: 'egg_icon', 326 | }, 327 | [ 328 | createNSElementNode('path', undefined, { 329 | d: 'M7.25325 705.466473a503.508932 503.508932 0 0 0 75.26742 121.391295 95.499302 95.499302 0 0 0 93.211173 31.07039 168.59902 168.59902 0 0 1 114.526906 16.257763 148.487566 148.487566 0 0 1 71.052444 83.456515 91.163899 91.163899 0 0 0 75.989987 61.538643 578.053784 578.053784 0 0 0 148.969278 0A91.163899 91.163899 0 0 0 662.380873 957.642436a148.487566 148.487566 0 0 1 72.256723-83.456515 168.59902 168.59902 0 0 1 114.406478-16.257763 95.61973 95.61973 0 0 0 93.331601-31.07039 503.508932 503.508932 0 0 0 75.267419-121.391295 84.29951 84.29951 0 0 0-18.545892-94.897163 138.251197 138.251197 0 0 1 0-197.140426 84.29951 84.29951 0 0 0 18.545892-94.897163 503.508932 503.508932 0 0 0-75.869559-121.391295 95.499302 95.499302 0 0 0-93.211173-31.070391A168.59902 168.59902 0 0 1 734.637596 149.812272a148.848849 148.848849 0 0 1-72.256723-83.456515A91.163899 91.163899 0 0 0 586.631741 4.817115a581.907476 581.907476 0 0 0-148.969277 0A91.163899 91.163899 0 0 0 361.311193 66.355757a148.848849 148.848849 0 0 1-71.413728 83.456515 168.59902 168.59902 0 0 1-114.406478 16.257763 95.378874 95.378874 0 0 0-93.3316 31.070391A503.508932 503.508932 0 0 0 7.25325 318.531721a84.29951 84.29951 0 0 0 18.545893 94.897163 140.057615 140.057615 0 0 1 41.30676 98.509999 140.057615 140.057615 0 0 1-41.30676 98.630427A84.29951 84.29951 0 0 0 7.25325 705.466473z m929.462315-349.240828a219.901294 219.901294 0 0 0 0 312.028615c0.842995 0.842995 2.649413 3.010697 1.806418 5.057971a427.398517 427.398517 0 0 1-63.104205 101.520696 9.513802 9.513802 0 0 1-9.032091 2.167702 255.547944 255.547944 0 0 0-173.777418 24.928569 231.823653 231.823653 0 0 0-111.275354 130.302957 6.984817 6.984817 0 0 1-6.021394 4.937543 492.790851 492.790851 0 0 1-126.328837 0 6.984817 6.984817 0 0 1-6.021394-4.937543 231.823653 231.823653 0 0 0-111.275353-130.302957 255.668372 255.668372 0 0 0-120.427872-30.468252 258.919924 258.919924 0 0 0-52.747408 5.539683 9.513802 9.513802 0 0 1-9.03209-2.167702 427.398517 427.398517 0 0 1-63.104205-101.520696c-0.842995-2.047274 0.963423-4.214976 1.806418-5.057971a221.82814 221.82814 0 0 0 64.910623-156.556233 221.707712 221.707712 0 0 0-65.512762-155.713238c-0.842995-0.842995-2.649413-3.010697-1.806418-5.057971a427.398517 427.398517 0 0 1 63.104205-101.520696 9.393374 9.393374 0 0 1 8.911662-2.167701 255.7888 255.7888 0 0 0 173.897847-24.92857 231.823653 231.823653 0 0 0 111.275353-130.302957 6.984817 6.984817 0 0 1 6.021394-4.937543 492.790851 492.790851 0 0 1 126.328837 0 6.984817 6.984817 0 0 1 6.021394 4.937543 231.823653 231.823653 0 0 0 111.275354 130.302957 255.547944 255.547944 0 0 0 173.777418 24.92857 9.513802 9.513802 0 0 1 9.032091 2.167701 423.063113 423.063113 0 0 1 62.983777 101.520696c0.963423 2.047274-0.842995 4.214976-1.68599 5.057971z', 330 | }), 331 | createNSElementNode('path', undefined, { 332 | d: 'M512.086889 305.766366a206.292944 206.292944 0 1 0 206.172516 206.172517 206.413372 206.413372 0 0 0-206.172516-206.172517z m123.197713 206.172517a123.197713 123.197713 0 1 1-123.197713-123.077285 123.318141 123.318141 0 0 1 123.197713 123.077285z', 333 | }), 334 | ] 335 | ) 336 | ), 337 | createElementNode( 338 | 'button', 339 | undefined, 340 | { 341 | class: 'egg_settings_reset_btn', 342 | title: '重置', 343 | type: 'button', 344 | onclick: debounce(() => { 345 | // 任务配置 346 | GM_setValue('taskConfig', null); 347 | // 设置 348 | GM_setValue('studySettings', null); 349 | // 最大阅读 350 | GM_setValue('maxRead', null); 351 | // 最大观看 352 | GM_setValue('maxWatch', null); 353 | // 主题色 354 | GM_setValue('themeColor', null); 355 | // 刷新页面 356 | location.reload(); 357 | }, 300), 358 | }, 359 | createNSElementNode( 360 | 'svg', 361 | undefined, 362 | { 363 | viewBox: '0 0 1024 1024', 364 | class: 'egg_icon', 365 | }, 366 | [ 367 | createNSElementNode('path', undefined, { 368 | d: 'M943.8 484.1c-17.5-13.7-42.8-10.7-56.6 6.8-5.7 7.3-8.5 15.8-8.6 24.4h-0.4c-0.6 78.3-26.1 157-78 223.3-124.9 159.2-356 187.1-515.2 62.3-31.7-24.9-58.2-54-79.3-85.9h77.1c22.4 0 40.7-18.3 40.7-40.7v-3c0-22.4-18.3-40.7-40.7-40.7H105.5c-22.4 0-40.7 18.3-40.7 40.7v177.3c0 22.4 18.3 40.7 40.7 40.7h3c22.4 0 40.7-18.3 40.7-40.7v-73.1c24.2 33.3 53 63.1 86 89 47.6 37.3 101 64.2 158.9 79.9 55.9 15.2 113.5 19.3 171.2 12.3 57.7-7 112.7-24.7 163.3-52.8 52.5-29 98-67.9 135.3-115.4 37.3-47.6 64.2-101 79.9-158.9 10.2-37.6 15.4-76 15.6-114.6h-0.1c-0.3-11.6-5.5-23.1-15.5-30.9zM918.7 135.2h-3c-22.4 0-40.7 18.3-40.7 40.7V249c-24.2-33.3-53-63.1-86-89-47.6-37.3-101-64.2-158.9-79.9-55.9-15.2-113.5-19.3-171.2-12.3-57.7 7-112.7 24.7-163.3 52.8-52.5 29-98 67.9-135.3 115.4-37.3 47.5-64.2 101-79.9 158.8-10.2 37.6-15.4 76-15.6 114.6h0.1c0.2 11.7 5.5 23.2 15.4 30.9 17.5 13.7 42.8 10.7 56.6-6.8 5.7-7.3 8.5-15.8 8.6-24.4h0.4c0.6-78.3 26.1-157 78-223.3 124.9-159.2 356-187.1 515.2-62.3 31.7 24.9 58.2 54 79.3 85.9h-77.1c-22.4 0-40.7 18.3-40.7 40.7v3c0 22.4 18.3 40.7 40.7 40.7h177.3c22.4 0 40.7-18.3 40.7-40.7V175.8c0.1-22.3-18.2-40.6-40.6-40.6z', 369 | }), 370 | ] 371 | ) 372 | ), 373 | ] 374 | ), 375 | // 任务按钮 376 | TaskBtn(), 377 | createElementNode('div', undefined, { class: 'egg_settings_item' }, [ 378 | SettingsPanel({ show: scheduleShow }), 379 | ]), 380 | ] 381 | ) 382 | ); 383 | } 384 | 385 | export { Panel }; 386 | -------------------------------------------------------------------------------- /src/component/ScheduleList.ts: -------------------------------------------------------------------------------- 1 | import { refreshScheduleTask } from '../controller/schedule'; 2 | import { createTip } from '../controller/tip'; 3 | import { scheduleList, settings } from '../shared'; 4 | import { SettingType } from '../types'; 5 | import { watchEffectRef } from '../utils/composition'; 6 | import { 7 | createElementNode, 8 | createNSElementNode, 9 | createTextNode, 10 | } from '../utils/element'; 11 | import { isLate } from '../utils/time'; 12 | import { debounce } from '../utils/utils'; 13 | 14 | /** 15 | * @description 定时项目 16 | * @returns 17 | */ 18 | function ScheduleList() { 19 | return createElementNode( 20 | 'div', 21 | undefined, 22 | { class: 'egg_schedule_list' }, 23 | watchEffectRef(() => { 24 | return scheduleList.length 25 | ? scheduleList.map((schedule) => 26 | createElementNode( 27 | 'div', 28 | undefined, 29 | { class: 'egg_schedule_item' }, 30 | [ 31 | createElementNode( 32 | 'div', 33 | undefined, 34 | { 35 | class: `egg_schedule_detail_time_wrap${ 36 | isLate(schedule) ? ' inactive' : '' 37 | }`, 38 | }, 39 | [ 40 | createElementNode( 41 | 'div', 42 | undefined, 43 | { 44 | class: 'egg_schedule_detail_icon', 45 | }, 46 | createNSElementNode( 47 | 'svg', 48 | undefined, 49 | { 50 | viewBox: '0 0 1024 1024', 51 | class: 'egg_icon', 52 | }, 53 | [ 54 | createNSElementNode('path', undefined, { 55 | d: 'M810.137703 213.860762c-164.388001-164.4187-431.887404-164.4187-596.277452 0-164.417677 164.388001-164.417677 431.889451 0 596.278475 164.390048 164.417677 431.890474 164.417677 596.277452 0C974.557426 645.750213 974.557426 378.248763 810.137703 213.860762zM767.347131 767.345596c-140.797723 140.829446-369.927237 140.797723-510.693238 0-140.828422-140.797723-140.828422-369.895515 0-510.708588 140.767024-140.783397 369.896538-140.813073 510.693238 0C908.14383 397.420405 908.14383 626.578572 767.347131 767.345596z', 56 | }), 57 | createNSElementNode('path', undefined, { 58 | d: 'M721.450824 521.495258 515.404028 521.495258l0.028653-227.948619c0-15.124466-12.362562-27.458375-27.501354-27.458375s-27.443026 12.33391-27.443026 27.458375l0 235.115855c0 0.835018-1.013073 20.48659 12.094456 34.459836 8.331759 8.809643 20.038382 13.288654 35.148521 13.288654l213.720569 0.031722c15.140839 0 27.472702-12.304234 27.472702-27.474748C748.922503 533.887496 736.620315 521.584286 721.450824 521.495258z', 59 | }), 60 | ] 61 | ) 62 | ), 63 | createElementNode( 64 | 'div', 65 | undefined, 66 | { class: 'egg_schedule_detail_time' }, 67 | createTextNode(schedule.time) 68 | ), 69 | ] 70 | ), 71 | createElementNode( 72 | 'div', 73 | undefined, 74 | { class: 'egg_schedule_detail_del_wrap' }, 75 | [ 76 | createElementNode( 77 | 'button', 78 | undefined, 79 | { 80 | class: 'egg_schedule_del_btn', 81 | onclick: debounce(() => { 82 | // 定时刷新 83 | if (!settings[SettingType.SCHEDULE_RUN]) { 84 | createTip('未开启定时刷新!'); 85 | return; 86 | } 87 | // 索引 88 | const index = scheduleList.findIndex( 89 | (s) => s === schedule 90 | ); 91 | // 删除元素 92 | scheduleList.splice(index, 1); 93 | // 存储 94 | GM_setValue( 95 | 'scheduleList', 96 | JSON.stringify(scheduleList) 97 | ); 98 | // 刷新任务 99 | refreshScheduleTask(); 100 | }, 300), 101 | }, 102 | createNSElementNode( 103 | 'svg', 104 | undefined, 105 | { 106 | viewBox: '0 0 1024 1024', 107 | class: 'egg_icon', 108 | }, 109 | [ 110 | createNSElementNode('path', undefined, { 111 | d: 'M896.22 896.22c14.262-14.263 11.263-40.449-6.583-58.295L230.473 178.76c-17.847-17.847-44.105-20.846-58.295-6.583-14.263 14.19-11.264 40.448 6.583 58.295l659.164 659.164c17.846 17.846 44.032 20.845 58.294 6.582', 112 | }), 113 | createNSElementNode('path', undefined, { 114 | d: 'M172.178 896.22c-14.263-14.263-11.264-40.449 6.583-58.295L837.925 178.76c17.846-17.847 44.032-20.846 58.294-6.583 14.263 14.19 11.264 40.448-6.582 58.295L230.4 889.637c-17.847 17.846-44.105 20.845-58.295 6.582', 115 | }), 116 | ] 117 | ) 118 | ), 119 | ] 120 | ), 121 | ] 122 | ) 123 | ) 124 | : [ 125 | createElementNode( 126 | 'div', 127 | undefined, 128 | { class: 'egg_schedule_list_none' }, 129 | [ 130 | createNSElementNode( 131 | 'svg', 132 | undefined, 133 | { 134 | viewBox: '0 0 1024 1024', 135 | class: 'egg_icon', 136 | }, 137 | [ 138 | createNSElementNode('path', undefined, { 139 | d: 'M238.1 520.5c-17.6 0-31.9-14.3-31.9-31.9 0-17.6 14.3-31.9 31.9-31.9h293c17.6 0 31.9 14.3 31.9 31.9 0 17.6-14.3 31.9-31.9 31.9h-293zM238.1 733.6c-17.6 0-31.9-14.3-31.9-31.9s14.3-31.9 31.9-31.9h186.5c17.6 0 31.9 14.3 31.9 31.9s-14.3 31.9-31.9 31.9H238.1zM241.6 314.9c-17.6 0-31.9-14.3-31.9-31.9s14.3-31.9 31.9-31.9h426.1c17.6 0 31.9 14.3 31.9 31.9 0 17.5-14.3 31.7-31.8 31.9H241.6z', 140 | }), 141 | createNSElementNode('path', undefined, { 142 | d: 'M160 926.6c-46.9 0-85.1-38.2-85.1-85.1V149.1c0-46.9 38.2-85.1 85.1-85.1h586c46.9 0 85.1 38.2 85.1 85.1v297.4c0 17.6-14.3 31.9-31.9 31.9-17.6 0-31.9-14.3-31.9-31.9V149.1c0-11.8-9.6-21.4-21.4-21.4H160c-11.8 0-21.4 9.6-21.4 21.4v692.4c0 11.8 9.6 21.4 21.4 21.4h304.5c17.5 0 31.8 14.2 31.9 31.8 0 17.6-14.3 31.8-31.9 31.8H160z', 143 | }), 144 | createNSElementNode('path', undefined, { 145 | d: 'M917.2 959.9c-8.5 0-16.5-3.3-22.5-9.3l-78.5-78.5-5.3-0.5-0.6 0.4c-31.7 21.6-68.7 33-107 33-105.2 0-190.8-85.6-190.8-190.8s85.6-190.8 190.8-190.8c105.2 0 190.8 85.6 190.8 190.8 0 38.2-11.4 75.2-33 107l-0.4 0.6 0.5 5.3 78.5 78.5c6 6 9.3 14 9.3 22.5s-3.4 16.5-9.4 22.5c-5.9 6-13.9 9.3-22.4 9.3zM703.4 587c-70.1 0-127.2 57.1-127.2 127.2s57.1 127.2 127.2 127.2 127.2-57.1 127.2-127.2S773.6 587 703.4 587z', 146 | }), 147 | ] 148 | ), 149 | createElementNode( 150 | 'div', 151 | undefined, 152 | { 153 | class: 'egg_schedule_list_none_text', 154 | }, 155 | createTextNode('暂无定时任务') 156 | ), 157 | ] 158 | ), 159 | ]; 160 | }) 161 | ); 162 | } 163 | 164 | export { ScheduleList }; 165 | -------------------------------------------------------------------------------- /src/component/ScoreItem.ts: -------------------------------------------------------------------------------- 1 | import { refreshScoreInfo } from '../controller/user'; 2 | import { login, taskConfig, todayScore, totalScore } from '../shared'; 3 | import { ref, watchEffectRef } from '../utils/composition'; 4 | import { 5 | createElementNode, 6 | createNSElementNode, 7 | createTextNode, 8 | } from '../utils/element'; 9 | import { debounce } from '../utils/utils'; 10 | 11 | /** 12 | * @description 分数详情 13 | */ 14 | function ScoreItem() { 15 | return watchEffectRef(() => { 16 | if (login.value) { 17 | // 分数显示 18 | const scoreShow = ref(false); 19 | // 分数信息 20 | return createElementNode( 21 | 'div', 22 | undefined, 23 | { 24 | class: 'egg_score_item', 25 | }, 26 | createElementNode('div', undefined, { class: 'egg_scoreinfo' }, [ 27 | createElementNode( 28 | 'div', 29 | undefined, 30 | { 31 | class: 'egg_totalscore', 32 | }, 33 | [ 34 | createTextNode('总积分'), 35 | createElementNode( 36 | 'span', 37 | undefined, 38 | undefined, 39 | createTextNode(totalScore) 40 | ), 41 | ] 42 | ), 43 | createElementNode( 44 | 'div', 45 | undefined, 46 | { 47 | class: 'egg_todayscore', 48 | }, 49 | [ 50 | createElementNode( 51 | 'button', 52 | undefined, 53 | { 54 | type: 'button', 55 | class: 'egg_todayscore_btn', 56 | title: '查看分数详情', 57 | onclick: debounce(() => { 58 | scoreShow.value = !scoreShow.value; 59 | }, 300), 60 | onblur: () => { 61 | scoreShow.value = false; 62 | }, 63 | }, 64 | [ 65 | createTextNode('当天分数'), 66 | // 当天分数 67 | createElementNode( 68 | 'span', 69 | undefined, 70 | undefined, 71 | createTextNode(todayScore) 72 | ), 73 | // icon 74 | createNSElementNode( 75 | 'svg', 76 | undefined, 77 | { 78 | viewBox: '0 0 1024 1024', 79 | class: 'egg_icon', 80 | }, 81 | createNSElementNode('path', undefined, { 82 | d: 'M332.16 883.84a40.96 40.96 0 0 0 58.24 0l338.56-343.04a40.96 40.96 0 0 0 0-58.24L390.4 140.16a40.96 40.96 0 0 0-58.24 58.24L640 512l-307.84 314.24a40.96 40.96 0 0 0 0 57.6z', 83 | }) 84 | ), 85 | createElementNode( 86 | 'div', 87 | undefined, 88 | { 89 | class: watchEffectRef( 90 | () => 91 | `egg_score_details${scoreShow.value ? '' : ' hide'}` 92 | ), 93 | }, 94 | [ 95 | createElementNode( 96 | 'div', 97 | undefined, 98 | { class: 'egg_score_title' }, 99 | [ 100 | createNSElementNode( 101 | 'svg', 102 | undefined, 103 | { 104 | viewBox: '0 0 1024 1024', 105 | class: 'egg_icon', 106 | }, 107 | [ 108 | createNSElementNode('path', undefined, { 109 | d: 'M314.81 304.01h415.86v58.91H314.81zM314.81 440.24h415.86v58.91H314.81z', 110 | }), 111 | createNSElementNode('path', undefined, { 112 | d: 'M814.8 892.74h-8.64l-283.51-182-283.51 182h-8.64A69.85 69.85 0 0 1 160.72 823V188.22a69.85 69.85 0 0 1 69.77-69.77H814.8a69.85 69.85 0 0 1 69.77 69.77V823a69.85 69.85 0 0 1-69.77 69.74zM230.5 177.35a10.87 10.87 0 0 0-10.86 10.86V823a10.86 10.86 0 0 0 5 9.11l298.01-191.42 298.06 191.38a10.86 10.86 0 0 0 5-9.11V188.22a10.87 10.87 0 0 0-10.86-10.86z', 113 | }), 114 | ] 115 | ), 116 | createElementNode( 117 | 'div', 118 | undefined, 119 | { 120 | class: 'egg_score_title_text', 121 | }, 122 | createTextNode('积分详情') 123 | ), 124 | ] 125 | ), 126 | ...taskConfig.map((task) => 127 | createElementNode( 128 | 'div', 129 | undefined, 130 | { class: 'egg_score_item' }, 131 | [ 132 | createTextNode(task.title), 133 | createElementNode( 134 | 'span', 135 | undefined, 136 | { 137 | class: 'egg_score_detail', 138 | }, 139 | createTextNode(watchEffectRef(() => task.score)) 140 | ), 141 | ] 142 | ) 143 | ), 144 | ] 145 | ), 146 | ] 147 | ), 148 | ] 149 | ), 150 | ]), 151 | { 152 | onMounted() { 153 | // 刷新分数信息 154 | refreshScoreInfo(); 155 | }, 156 | } 157 | ); 158 | } 159 | }); 160 | } 161 | 162 | export { ScoreItem }; 163 | -------------------------------------------------------------------------------- /src/component/Select.ts: -------------------------------------------------------------------------------- 1 | import { 2 | reactive, 3 | Ref, 4 | ref, 5 | shallowRef, 6 | watchEffectRef, 7 | watchRef, 8 | watch, 9 | } from '../utils/composition'; 10 | import { createElementNode, createTextNode } from '../utils/element'; 11 | import { debounce } from '../utils/utils'; 12 | 13 | function Select({ 14 | data, 15 | maxlength, 16 | placeholder = '', 17 | onchange, 18 | onblur, 19 | value, 20 | keep, 21 | }: { 22 | data: { label: string; value: T; selected?: boolean }[]; 23 | maxlength?: number; 24 | placeholder?: string; 25 | onchange?: (data: { value: T; label: string }) => void; 26 | onblur?: (data: { value: T; label: string } | undefined) => void; 27 | value?: Ref; 28 | keep?: boolean; 29 | }) { 30 | const selectData = reactive< 31 | { 32 | label: string; 33 | value: T; 34 | selected: boolean; 35 | active: boolean; 36 | ele: HTMLElement | undefined; 37 | }[] 38 | >( 39 | data.map((v) => ({ selected: false, active: false, ele: undefined, ...v })) 40 | ); 41 | const focus = ref(false); 42 | const input = shallowRef(undefined); 43 | const list = shallowRef(undefined); 44 | const valueRef = ref(''); 45 | value && 46 | watch( 47 | value, 48 | () => { 49 | const item = selectData.find((v) => v.value === value.value); 50 | valueRef.value = item ? item.label : ''; 51 | if (!item) { 52 | selectData.forEach((v) => (v.selected = false)); 53 | list.value && (list.value.scrollTop = 0); 54 | } 55 | }, 56 | true 57 | ); 58 | return createElementNode( 59 | 'div', 60 | undefined, 61 | { 62 | class: 'egg_select', 63 | }, 64 | [ 65 | createElementNode( 66 | 'input', 67 | { value: valueRef }, 68 | { 69 | class: 'egg_select_input', 70 | type: 'text', 71 | placeholder, 72 | maxlength, 73 | ref: input, 74 | onfocus() { 75 | if (list.value && input.value) { 76 | focus.value = true; 77 | if (input.value.value && valueRef.value) { 78 | const index = selectData.findIndex( 79 | (v) => v.label === valueRef.value 80 | ); 81 | if (index + 1) { 82 | list.value.scrollTop = selectData[index].ele?.offsetTop || 0; 83 | selectData.forEach((v, i) => (v.selected = i === index)); 84 | } 85 | return; 86 | } 87 | } 88 | }, 89 | oninput() { 90 | if (list.value && input.value) { 91 | const { value } = input.value; 92 | // 文本存在 93 | if (value) { 94 | const index = selectData.findIndex((v) => 95 | v.label.includes(value) 96 | ); 97 | // 存在匹配 98 | if (index + 1) { 99 | list.value.scrollTop = selectData[index].ele?.offsetTop || 0; 100 | selectData.forEach((v, i) => { 101 | v.active = i === index; 102 | v.active && 103 | setTimeout(() => { 104 | v.active = false; 105 | }, 300); 106 | }); 107 | } 108 | return; 109 | } 110 | // 清除 111 | selectData.forEach((v) => (v.active = v.selected = false)); 112 | list.value.scrollTop = 0; 113 | } 114 | }, 115 | onblur() { 116 | if (list.value && input.value) { 117 | const item = selectData.find((v) => v.selected); 118 | // 关闭选项 119 | if (item || !input.value.value) { 120 | setTimeout(() => { 121 | focus.value = false; 122 | }, 100); 123 | } 124 | // 恢复文本 125 | if (item && input.value.value !== item.label) { 126 | input.value.value = item.label; 127 | } 128 | // 保留文本 129 | if (!item && keep) { 130 | input.value.value = valueRef.value; 131 | } 132 | onblur && 133 | onblur( 134 | item ? { label: item.label, value: item.value } : undefined 135 | ); 136 | } 137 | }, 138 | } 139 | ), 140 | createElementNode( 141 | 'div', 142 | undefined, 143 | { 144 | class: watchEffectRef( 145 | () => `egg_select_list${focus.value ? '' : ' hide'}` 146 | ), 147 | ref: list, 148 | }, 149 | selectData.map((v, index) => 150 | createElementNode( 151 | 'div', 152 | undefined, 153 | { 154 | class: watchRef( 155 | () => [v.selected, v.active], 156 | () => 157 | `egg_select_item${ 158 | v.selected ? ' selected' : v.active ? ' active' : '' 159 | }` 160 | ), 161 | ref: (e) => (v.ele = e), 162 | onclick: debounce(() => { 163 | if (valueRef.value !== v.label) { 164 | onchange && onchange({ label: v.label, value: v.value }); 165 | selectData.forEach((v, i) => { 166 | v.selected = i === index; 167 | v.selected && (valueRef.value = v.label); 168 | }); 169 | } 170 | focus.value = false; 171 | }, 300), 172 | }, 173 | createTextNode(v.label) 174 | ) 175 | ) 176 | ), 177 | ] 178 | ); 179 | } 180 | 181 | export { Select }; 182 | -------------------------------------------------------------------------------- /src/component/TaskBtn.ts: -------------------------------------------------------------------------------- 1 | import { doExamPractice } from '../controller/exam'; 2 | import { closeFrame } from '../controller/frame'; 3 | import { readNews, watchVideo } from '../controller/readAndWatch'; 4 | import { createTip } from '../controller/tip'; 5 | import { 6 | frame, 7 | login, 8 | pushToken, 9 | running, 10 | settings, 11 | taskConfig, 12 | taskStatus, 13 | todayScore, 14 | totalScore, 15 | userinfo, 16 | } from '../shared'; 17 | import { SettingType, TaskStatusType, TaskType } from '../types'; 18 | import { watch, watchEffectRef, watchRef } from '../utils/composition'; 19 | import { createElementNode, createTextNode } from '../utils/element'; 20 | import { error, log } from '../utils/log'; 21 | import { getHighlightHTML, getProgressHTML, pushModal } from '../utils/push'; 22 | import { debounce, studyPauseLock } from '../utils/utils'; 23 | 24 | /** 25 | * @description 任务按钮 26 | */ 27 | function TaskBtn() { 28 | return watchEffectRef(() => { 29 | if (login.value) { 30 | /** 31 | * @description 学习 32 | */ 33 | async function study() { 34 | // 创建提示 35 | createTip('开始学习!'); 36 | // 暂停 37 | await studyPauseLock(); 38 | // 文章选读 39 | if ( 40 | taskConfig[TaskType.READ].active && 41 | !taskConfig[TaskType.READ].status 42 | ) { 43 | log('任务一: 文章选读'); 44 | // 创建提示 45 | createTip('任务一: 文章选读'); 46 | // 暂停 47 | await studyPauseLock(); 48 | // 看新闻 49 | await readNews(); 50 | } 51 | log('任务一: 文章选读已完成!'); 52 | 53 | // 视听学习 54 | if ( 55 | taskConfig[TaskType.WATCH].active && 56 | !taskConfig[TaskType.WATCH].status 57 | ) { 58 | log('任务二: 视听学习'); 59 | // 创建提示 60 | createTip('任务二: 视听学习'); 61 | // 暂停 62 | await studyPauseLock(); 63 | // 看视频 64 | await watchVideo(); 65 | } 66 | log('任务二: 视听学习已完成!'); 67 | 68 | // 每日答题 69 | if ( 70 | taskConfig[TaskType.PRACTICE].active && 71 | !taskConfig[TaskType.PRACTICE].status 72 | ) { 73 | log('任务三: 每日答题'); 74 | // 创建提示 75 | createTip('任务三: 每日答题'); 76 | // 暂停 77 | await studyPauseLock(); 78 | // 做每日答题 79 | await doExamPractice(); 80 | } 81 | log('任务三: 每日答题已完成!'); 82 | } 83 | /** 84 | * @description 暂停任务 85 | */ 86 | function pauseTask() { 87 | // 全局暂停 88 | GM_setValue('pauseStudy', true); 89 | taskStatus.value = TaskStatusType.PAUSE; 90 | } 91 | /** 92 | * @description 继续任务 93 | */ 94 | function continueTask() { 95 | // 全局暂停 96 | GM_setValue('pauseStudy', false); 97 | taskStatus.value = TaskStatusType.START; 98 | } 99 | /** 100 | * @description 开始任务 101 | */ 102 | async function startTask() { 103 | // 未完成任务 104 | if (taskConfig.some((task) => task.active && !task.status)) { 105 | // 开始任务 106 | taskStatus.value = TaskStatusType.START; 107 | try { 108 | // 学习 109 | await study(); 110 | // 同屏任务 111 | if (settings[SettingType.SAME_TAB]) { 112 | // 关闭窗口 113 | closeFrame(); 114 | // 窗口不存在 115 | frame.exist = false; 116 | } 117 | } catch (err: unknown) { 118 | if (err instanceof Error) { 119 | // 提示 120 | createTip(err.message); 121 | // 错误 122 | error(err.message); 123 | return; 124 | } 125 | // 提示 126 | createTip(String(err)); 127 | // 错误 128 | error(err); 129 | } 130 | } 131 | // 刷新任务 132 | taskStatus.value = TaskStatusType.FINISH; 133 | log('已完成'); 134 | // 创建提示 135 | createTip('完成学习!'); 136 | // 远程推送 137 | if (settings[SettingType.REMOTE_PUSH]) { 138 | // 推送 139 | const res = await pushModal( 140 | { 141 | title: '学习推送', 142 | to: userinfo.nick, 143 | content: [ 144 | '学习强国, 学习完成!', 145 | `当天积分: ${getHighlightHTML(todayScore.value)} 分`, 146 | `总积分: ${getHighlightHTML(totalScore.value)} 分`, 147 | ...taskConfig.map((task) => 148 | getProgressHTML( 149 | task.title, 150 | task.currentScore, 151 | task.dayMaxScore 152 | ) 153 | ), 154 | ], 155 | type: 'success', 156 | }, 157 | pushToken.value 158 | ); 159 | createTip(`学习推送${res ? '成功' : '失败'}!`); 160 | } 161 | } 162 | // 已在等待 163 | let flag = false; 164 | // 自动答题 165 | watch( 166 | () => [taskStatus.value, settings[SettingType.AUTO_START]], 167 | async () => { 168 | // 加载完毕 169 | if (!flag && taskStatus.value === TaskStatusType.LOADED) { 170 | // 自动答题 171 | if (settings[SettingType.AUTO_START]) { 172 | // 等待中 173 | flag = true; 174 | // 创建提示 175 | const tip = createTip('即将自动开始任务', 5, true); 176 | // 等待倒计时结束 177 | await tip.waitCountDown(); 178 | // 再次查看是否开启 179 | if ( 180 | settings[SettingType.AUTO_START] && 181 | taskStatus.value !== TaskStatusType.START 182 | ) { 183 | // 创建提示 184 | createTip('自动开始任务'); 185 | // 开始任务 186 | startTask(); 187 | return; 188 | } 189 | // 取消等待 190 | flag = false; 191 | // 创建提示 192 | createTip('已取消自动开始任务!'); 193 | } 194 | } 195 | } 196 | ); 197 | // 切换开关任务未完成 198 | taskConfig.forEach((task) => { 199 | watch( 200 | () => [task.active], 201 | () => { 202 | if (taskStatus.value === TaskStatusType.FINISH) { 203 | if (task.active && !task.status) { 204 | taskStatus.value = TaskStatusType.LOADED; 205 | } 206 | } 207 | } 208 | ); 209 | }); 210 | return createElementNode( 211 | 'div', 212 | undefined, 213 | { class: 'egg_study_item' }, 214 | createElementNode( 215 | 'button', 216 | undefined, 217 | { 218 | class: watchEffectRef( 219 | () => 220 | `egg_study_btn${ 221 | taskStatus.value === TaskStatusType.START ? ' loading' : '' 222 | }` 223 | ), 224 | 225 | type: 'button', 226 | disabled: watchRef( 227 | () => [running.value, taskStatus.value], 228 | () => 229 | running.value || 230 | taskStatus.value === TaskStatusType.LOADING || 231 | taskStatus.value === TaskStatusType.FINISH 232 | ), 233 | onclick: watchEffectRef(() => 234 | taskStatus.value === TaskStatusType.LOADED 235 | ? debounce(startTask, 300) 236 | : taskStatus.value === TaskStatusType.START 237 | ? debounce(pauseTask, 300) 238 | : taskStatus.value === TaskStatusType.PAUSE 239 | ? debounce(continueTask, 300) 240 | : undefined 241 | ), 242 | }, 243 | createTextNode( 244 | watchEffectRef( 245 | () => 246 | `${ 247 | taskStatus.value === TaskStatusType.LOADING 248 | ? '等待中' 249 | : taskStatus.value === TaskStatusType.LOADED 250 | ? '开始学习' 251 | : taskStatus.value === TaskStatusType.START 252 | ? '正在学习, 点击暂停' 253 | : taskStatus.value === TaskStatusType.PAUSE 254 | ? '继续学习' 255 | : taskStatus.value === TaskStatusType.FINISH 256 | ? '已完成' 257 | : '' 258 | }` 259 | ) 260 | ) 261 | ) 262 | ); 263 | } 264 | }); 265 | } 266 | 267 | export { TaskBtn }; 268 | -------------------------------------------------------------------------------- /src/component/TaskItem.ts: -------------------------------------------------------------------------------- 1 | import { Ref, watchEffectRef } from '../utils/composition'; 2 | import { createElementNode, createTextNode } from '../utils/element'; 3 | 4 | /** 5 | * @description 设置任务项 6 | * @returns 7 | */ 8 | function TaskItem({ 9 | title, 10 | tip, 11 | checked, 12 | currentScore, 13 | dayMaxScore, 14 | onchange, 15 | immutable, 16 | }: { 17 | title: string; 18 | tip: string; 19 | checked: Ref; 20 | currentScore: Ref; 21 | dayMaxScore: Ref; 22 | onchange: (...args: any[]) => void; 23 | immutable: boolean; 24 | }) { 25 | return createElementNode( 26 | 'div', 27 | undefined, 28 | { 29 | class: 'egg_task_item', 30 | }, 31 | [ 32 | createElementNode('div', undefined, { class: 'egg_label_wrap' }, [ 33 | createElementNode('div', undefined, { class: 'egg_task_title_wrap' }, [ 34 | createElementNode( 35 | 'div', 36 | undefined, 37 | { class: 'egg_task_title' }, 38 | createTextNode(title) 39 | ), 40 | createElementNode( 41 | 'div', 42 | undefined, 43 | { class: 'egg_task_progress_wrap' }, 44 | [ 45 | createElementNode( 46 | 'div', 47 | undefined, 48 | { 49 | class: 'egg_task_current', 50 | }, 51 | createTextNode(currentScore) 52 | ), 53 | createElementNode( 54 | 'div', 55 | undefined, 56 | { 57 | class: 'egg_task_max', 58 | }, 59 | createTextNode(watchEffectRef(() => `/${dayMaxScore.value}`)) 60 | ), 61 | ] 62 | ), 63 | ]), 64 | createElementNode('div', undefined, { class: 'egg_progress' }, [ 65 | createElementNode( 66 | 'div', 67 | undefined, 68 | { class: 'egg_track' }, 69 | createElementNode('div', undefined, { 70 | class: 'egg_bar', 71 | style: watchEffectRef( 72 | () => 73 | `width: ${( 74 | (100 * currentScore.value) / 75 | dayMaxScore.value 76 | ).toFixed(1)}%;` 77 | ), 78 | }) 79 | ), 80 | ]), 81 | ]), 82 | createElementNode('input', undefined, { 83 | title: tip, 84 | class: 'egg_switch', 85 | type: 'checkbox', 86 | checked, 87 | onchange, 88 | disabled: immutable, 89 | }), 90 | ] 91 | ); 92 | } 93 | 94 | export { TaskItem }; 95 | -------------------------------------------------------------------------------- /src/component/TaskList.ts: -------------------------------------------------------------------------------- 1 | import { createTip } from '../controller/tip'; 2 | import { refreshTaskList } from '../controller/user'; 3 | import { login, taskConfig, taskStatus } from '../shared'; 4 | import { TaskStatusType, TaskType } from '../types'; 5 | import { watch, watchEffectRef } from '../utils/composition'; 6 | import { createElementNode } from '../utils/element'; 7 | import { debounce } from '../utils/utils'; 8 | import { TaskItem } from './TaskItem'; 9 | 10 | /** 11 | * @description 任务 12 | */ 13 | function TaskList() { 14 | // 处理任务设置变化 15 | const handleTaskChange = (e: Event, type: TaskType, title: string) => { 16 | // 开关 17 | const { checked } = e.target; 18 | if (taskConfig[type].active !== checked) { 19 | taskConfig[type].active = checked; 20 | // 设置 21 | GM_setValue('taskConfig', JSON.stringify(taskConfig)); 22 | // 创建提示 23 | createTip(`${title} ${checked ? '打开' : '关闭'}!`); 24 | } 25 | }; 26 | // 登录加载 27 | watch( 28 | login, 29 | async () => { 30 | if (login.value) { 31 | // 加载任务列表 32 | await refreshTaskList(); 33 | // 未完成任务 34 | if (taskConfig.some((task) => task.active && !task.status)) { 35 | // 全局暂停 36 | GM_setValue('pauseStudy', false); 37 | // 加载完毕 38 | taskStatus.value = TaskStatusType.LOADED; 39 | return; 40 | } 41 | // 任务完毕 42 | taskStatus.value = TaskStatusType.FINISH; 43 | } 44 | }, 45 | true 46 | ); 47 | return createElementNode( 48 | 'div', 49 | undefined, 50 | { 51 | class: 'egg_task_list', 52 | }, 53 | taskConfig.map((label) => 54 | label.immutable 55 | ? TaskItem({ 56 | title: label.title, 57 | tip: label.tip, 58 | checked: watchEffectRef(() => label.active), 59 | currentScore: watchEffectRef(() => label.currentScore), 60 | dayMaxScore: watchEffectRef(() => label.dayMaxScore), 61 | onchange: debounce((e) => { 62 | handleTaskChange(e, label.type, label.title); 63 | }, 300), 64 | immutable: label.immutable, 65 | }) 66 | : TaskItem({ 67 | title: label.title, 68 | tip: label.tip, 69 | checked: watchEffectRef(() => label.active), 70 | currentScore: watchEffectRef(() => label.currentScore), 71 | dayMaxScore: watchEffectRef(() => label.dayMaxScore), 72 | onchange: debounce((e) => { 73 | handleTaskChange(e, label.type, label.title); 74 | }, 300), 75 | immutable: label.immutable, 76 | }) 77 | ) 78 | ); 79 | } 80 | 81 | export { TaskList }; 82 | -------------------------------------------------------------------------------- /src/component/TimeInput.ts: -------------------------------------------------------------------------------- 1 | import { Ref, ref, watchEffectRef } from '../utils/composition'; 2 | import { createElementNode, createTextNode } from '../utils/element'; 3 | import { formatDateNum } from '../utils/time'; 4 | import { Select } from './Select'; 5 | 6 | /** 7 | * @description 时间输入 8 | * @returns 9 | */ 10 | function TimeInput({ 11 | hour, 12 | minute, 13 | onchange, 14 | }: { 15 | hour: Ref; 16 | minute: Ref; 17 | onchange?: (data: { hour: number; minute: number }) => void; 18 | }) { 19 | // 小时 20 | const hours = new Array(24).fill(undefined).map((v, i) => ({ 21 | value: i, 22 | label: formatDateNum(i), 23 | })); 24 | // 分钟 25 | const minutes = new Array(60).fill(undefined).map((v, i) => ({ 26 | value: i, 27 | label: formatDateNum(i), 28 | })); 29 | const valueRef = watchEffectRef(() => { 30 | const h = hours.find((h) => h.value === hour.value); 31 | const min = minutes.find((min) => min.value === minute.value); 32 | return { 33 | hour: h ? h.value : -1, 34 | minute: min ? min.value : -1, 35 | }; 36 | }); 37 | return createElementNode('div', undefined, { class: 'egg_time_input' }, [ 38 | createElementNode('div', undefined, { class: 'egg_hour_wrap' }, [ 39 | Select({ 40 | data: hours, 41 | placeholder: '00', 42 | maxlength: 2, 43 | value: hour, 44 | onchange({ value }) { 45 | valueRef.value.hour = value; 46 | onchange && onchange(valueRef.value); 47 | }, 48 | onblur(res) { 49 | if (!res) { 50 | valueRef.value.hour = -1; 51 | onchange && onchange(valueRef.value); 52 | } 53 | }, 54 | }), 55 | ]), 56 | createElementNode( 57 | 'span', 58 | undefined, 59 | { class: 'egg_separator' }, 60 | createTextNode(':') 61 | ), 62 | createElementNode('div', undefined, { class: 'egg_minute_wrap' }, [ 63 | Select({ 64 | data: minutes, 65 | placeholder: '00', 66 | maxlength: 2, 67 | value: minute, 68 | onchange({ value }) { 69 | valueRef.value.minute = value; 70 | onchange && onchange(valueRef.value); 71 | }, 72 | onblur(res) { 73 | if (!res) { 74 | valueRef.value.minute = -1; 75 | onchange && onchange(valueRef.value); 76 | } 77 | }, 78 | }), 79 | ]), 80 | ]); 81 | } 82 | 83 | export { TimeInput }; 84 | -------------------------------------------------------------------------------- /src/component/Tip.ts: -------------------------------------------------------------------------------- 1 | import { Ref, watchEffectRef, watchRef } from '../utils/composition'; 2 | import { createElementNode, createTextNode } from '../utils/element'; 3 | 4 | function Tip({ 5 | text, 6 | count, 7 | show, 8 | delayShow, 9 | countShow, 10 | callback, 11 | }: { 12 | text: Ref; 13 | count: Ref; 14 | show: Ref; 15 | delayShow: Ref; 16 | countShow: Ref; 17 | callback: (count: number) => void; 18 | }) { 19 | return createElementNode( 20 | 'div', 21 | undefined, 22 | { 23 | class: watchRef( 24 | [show, delayShow], 25 | () => 26 | `egg_tip${ 27 | show.value ? (delayShow.value ? ' active delay' : ' active') : '' 28 | }` 29 | ), 30 | }, 31 | [ 32 | createElementNode( 33 | 'span', 34 | undefined, 35 | { 36 | class: 'egg_text', 37 | }, 38 | createTextNode(text) 39 | ), 40 | watchEffectRef(() => 41 | countShow.value 42 | ? createElementNode( 43 | 'span', 44 | undefined, 45 | { 46 | class: 'egg_countdown', 47 | }, 48 | createTextNode(watchEffectRef(() => `${count.value}s`)) 49 | ) 50 | : undefined 51 | ), 52 | ], 53 | { 54 | onMounted() { 55 | // 倒计时 56 | const countDown = async () => { 57 | // 倒计时回调 58 | await callback(count.value); 59 | // 倒计时结束 60 | if (!count.value) { 61 | show.value = false; 62 | return; 63 | } 64 | count.value--; 65 | setTimeout(countDown, 1000); 66 | }; 67 | countDown(); 68 | }, 69 | } 70 | ); 71 | } 72 | 73 | export { Tip }; 74 | -------------------------------------------------------------------------------- /src/config/api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description api配置 3 | */ 4 | const API_CONFIG = { 5 | // 用户信息 6 | userInfo: 'https://pc-api.xuexi.cn/open/api/user/info', 7 | // 总分 8 | totalScore: 'https://pc-proxy-api.xuexi.cn/delegate/score/get', 9 | // 当天分数 10 | todayScore: 'https://pc-proxy-api.xuexi.cn/delegate/score/today/query', 11 | // 任务列表 12 | taskList: 13 | 'https://pc-proxy-api.xuexi.cn/delegate/score/days/listScoreProgress?sence=score&deviceType=2', 14 | // 新闻数据 15 | todayNews: [ 16 | 'https://www.xuexi.cn/lgdata/35il6fpn0ohq.json', 17 | 'https://www.xuexi.cn/lgdata/1ap1igfgdn2.json', 18 | 'https://www.xuexi.cn/lgdata/vdppiu92n1.json', 19 | 'https://www.xuexi.cn/lgdata/152mdtl3qn1.json', 20 | ], 21 | // 视频数据 22 | todayVideos: [ 23 | 'https://www.xuexi.cn/lgdata/525pi8vcj24p.json', 24 | 'https://www.xuexi.cn/lgdata/11vku6vt6rgom.json', 25 | 'https://www.xuexi.cn/lgdata/2qfjjjrprmdh.json', 26 | 'https://www.xuexi.cn/lgdata/3o3ufqgl8rsn.json', 27 | 'https://www.xuexi.cn/lgdata/591ht3bc22pi.json', 28 | 'https://www.xuexi.cn/lgdata/1742g60067k.json', 29 | 'https://www.xuexi.cn/lgdata/1novbsbi47k.json', 30 | ], 31 | // 专项练习列表 32 | paperList: 'https://pc-proxy-api.xuexi.cn/api/exam/service/paper/pc/list', 33 | // 文本服务器保存答案 34 | answerSave: 'https://a6.qikekeji.com/txt/data/save', 35 | // 文本服务器获取答案 36 | answerSearch: 'https://a6.qikekeji.com/txt/data/detail', 37 | // 推送 38 | push: 'https://www.pushplus.plus/send', 39 | // 生成二维码 40 | generateQRCode: 'https://login.xuexi.cn/user/qrcode/generate', 41 | //二维码登录 42 | loginWithQRCode: 'https://login.xuexi.cn/login/login_with_qr', 43 | // 签名 44 | sign: 'https://pc-api.xuexi.cn/open/api/sns/sign', 45 | // 安全检查 46 | secureCheck: 'https://pc-api.xuexi.cn/login/secure_check', 47 | // 二维码 48 | qrcode: 'https://api.qrserver.com/v1/create-qr-code', 49 | }; 50 | export default API_CONFIG; 51 | -------------------------------------------------------------------------------- /src/config/compile.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | import { ModuleFormat } from 'rollup'; 3 | import terser from '@rollup/plugin-terser'; 4 | /** 5 | * @description 编译配置 6 | */ 7 | const COMPILE_CONFIG = { 8 | input: { 9 | file: 'src/index.ts', 10 | }, 11 | output: { 12 | file: 'tech-study.js', 13 | }, 14 | /** 15 | * @description 目标版本 16 | */ 17 | target: ts.ScriptTarget.ESNext, 18 | /** 19 | * @description 模块版本 20 | */ 21 | module: ts.ModuleKind.ESNext, 22 | /** 23 | * @description 是否压缩代码 24 | */ 25 | compress: false, 26 | /** 27 | * @description rollup 配置 28 | */ 29 | rollupConfig: { 30 | inputOptions: { input: 'src/index.js' }, 31 | outputOptions: { 32 | file: 'src/index.min.js', 33 | format: 'es', 34 | plugins: [terser()], 35 | }, 36 | }, 37 | }; 38 | 39 | export default COMPILE_CONFIG; 40 | -------------------------------------------------------------------------------- /src/config/script.ts: -------------------------------------------------------------------------------- 1 | import { version } from './version'; 2 | 3 | /** 4 | * @description 脚本配置 5 | */ 6 | const SCRIPT_CONFIG = { 7 | /** 8 | * @description 脚本名 9 | */ 10 | name: '不学习何以强国', 11 | /** 12 | * @description 命名空间 13 | */ 14 | namespace: 'http://tampermonkey.net/', 15 | /** 16 | * @description 版本 17 | */ 18 | version, 19 | /** 20 | * @description 脚本描述 21 | */ 22 | description: 23 | '有趣的 `学习强国` 油猴插件。读文章,看视频,做习题。问题反馈: https://github.com/Xu22Web/tech-study-js/issues 。', 24 | /** 25 | * @description 作者 26 | */ 27 | author: '原作者:techxuexi 荷包蛋。现作者:Xu22Web', 28 | /** 29 | * @description 链接匹配 30 | */ 31 | match: [ 32 | 'https://www.xuexi.cn/*', 33 | 'https://pc.xuexi.cn/points/exam-practice.html', 34 | 'https://pc.xuexi.cn/points/exam-weekly-detail.html?id=*', 35 | 'https://pc.xuexi.cn/points/exam-paper-detail.html?id=*', 36 | 'https://login.xuexi.cn/login/xuexiWeb?appid=dingoankubyrfkttorhpou&goto=https%3A%2F%2Foa.xuexi.cn&type=1&state=ffdea2ded23f45ab%2FKQreTlDFe1Id3B7BVdaaYcTMp6lsTBB%2Fs3gGevuMKfvpbABDEl9ymG3bbOgtpSN&check_login=https%3A%2F%2Fpc-api.xuexi.cn', 37 | ], 38 | /** 39 | * @description 所需脚本 40 | */ 41 | require: ['https://cdn.jsdelivr.net/npm/blueimp-md5@2.9.0'], 42 | /** 43 | * @description 脚本注入的时间 44 | */ 45 | 'run-at': 'document-start', 46 | /** 47 | * @description 权限 48 | */ 49 | grant: [ 50 | 'GM_addStyle', 51 | 'GM_setValue', 52 | 'GM_getValue', 53 | 'GM_openInTab', 54 | 'GM_addValueChangeListener', 55 | 'unsafeWindow', 56 | ], 57 | updateURL: 58 | 'https://raw.githubusercontent.com/Xu22Web/tech-study-js/master/tech-study.js', 59 | downloadURL: 60 | 'https://raw.githubusercontent.com/Xu22Web/tech-study-js/master/tech-study.js', 61 | supportURL: 'https://github.com/Xu22Web', 62 | }; 63 | 64 | export default SCRIPT_CONFIG; 65 | -------------------------------------------------------------------------------- /src/config/task.ts: -------------------------------------------------------------------------------- 1 | /* task·配置 */ 2 | /** 3 | * @description 单次最大新闻数 4 | */ 5 | const maxNewsNum = 6; 6 | /** 7 | * @description 单次最大视频数 8 | */ 9 | const maxVideoNum = 6; 10 | /** 11 | * @description 二维码最大刷新次数 12 | */ 13 | const maxRefreshCount = 10; 14 | /** 15 | * @description 二维码自动刷新间隔 16 | */ 17 | const autoRefreshQRCodeInterval = 100000; 18 | 19 | export { maxNewsNum, maxVideoNum, maxRefreshCount, autoRefreshQRCodeInterval }; 20 | -------------------------------------------------------------------------------- /src/config/url.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description url配置 3 | */ 4 | const URL_CONFIG = { 5 | // 主页正则 6 | home: /^https\:\/\/www\.xuexi\.cn(\/(index\.html)?)?$/, 7 | // 主页 8 | homeOrigin: 'https://www.xuexi.cn', 9 | // 每日答题页面 10 | examPractice: 'https://pc.xuexi.cn/points/exam-practice.html', 11 | // 专项练习页面 12 | examPaper: 'https://pc.xuexi.cn/points/exam-paper-detail.html', 13 | }; 14 | export default URL_CONFIG; 15 | -------------------------------------------------------------------------------- /src/config/version.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 版本号 3 | */ 4 | const version = '1.7.5'; 5 | 6 | export { version }; 7 | -------------------------------------------------------------------------------- /src/controller/frame.ts: -------------------------------------------------------------------------------- 1 | import URL_CONFIG from '../config/url'; 2 | import { frame, id, page, settings } from '../shared'; 3 | import { SettingType } from '../types'; 4 | import { $_ } from '../utils/element'; 5 | import { log } from '../utils/log'; 6 | import { generateMix } from '../utils/random'; 7 | 8 | /** 9 | * @description 初始化主页面 10 | */ 11 | function initMainListener() { 12 | // 监听关闭 13 | window.addEventListener('message', (msg: MessageEvent) => { 14 | const { data } = msg; 15 | if (data.id === id.value && data.closed) { 16 | // 关闭窗口 17 | closeFrame(); 18 | return; 19 | } 20 | }); 21 | } 22 | 23 | /** 24 | * @description 初始化子页面 25 | */ 26 | function initChildListener() { 27 | window.addEventListener('message', (msg: MessageEvent) => { 28 | const { data } = msg; 29 | if (data.id && !data.closed) { 30 | // 设置窗口id 31 | id.value = data.id; 32 | log(`初始化窗口 ID: ${id.value}`); 33 | return; 34 | } 35 | }); 36 | } 37 | 38 | /** 39 | * @description 打开窗口 40 | * @param url 41 | * @returns 42 | */ 43 | async function openFrame(url: string, title?: string) { 44 | // 设置 URL 45 | frame.src = url; 46 | // 等待元素 47 | await $_('.egg_frame'); 48 | if (frame.ele) { 49 | // id 50 | id.value = generateMix(10); 51 | // 打开 52 | frame.closed = false; 53 | // 设置标题 54 | frame.title = title || ''; 55 | // 等待页面加载 56 | await waitFrameLoaded(frame.ele); 57 | // 发送窗口 ID 58 | frame.ele.contentWindow?.postMessage({ id: id.value, closed: false }, url); 59 | return true; 60 | } 61 | return false; 62 | } 63 | 64 | /** 65 | * @description 关闭窗口 66 | */ 67 | function closeFrame() { 68 | log(`关闭窗口 ID: ${id.value}`); 69 | // 窗口显示 70 | frame.show = false; 71 | // 关闭 72 | frame.closed = true; 73 | // 标题 74 | frame.title = ''; 75 | // src 76 | frame.src = ''; 77 | } 78 | 79 | /** 80 | * @description 关闭 frame 81 | */ 82 | function handleCloseFrame() { 83 | window.parent.postMessage( 84 | { id: id.value, closed: true }, 85 | URL_CONFIG.homeOrigin 86 | ); 87 | } 88 | 89 | /** 90 | * @description 等待窗口任务结束 91 | * @param id 92 | * @returns 93 | */ 94 | function waitFrameClose() { 95 | return new Promise((resolve) => { 96 | const timer = setInterval(() => { 97 | // 窗口关闭 98 | if (frame.closed) { 99 | clearInterval(timer); 100 | resolve(true); 101 | } 102 | }, 100); 103 | }); 104 | } 105 | 106 | // 等待窗口加载 107 | function waitFrameLoaded(iframe: HTMLElement) { 108 | return new Promise((resolve) => { 109 | iframe.addEventListener('load', () => { 110 | resolve(true); 111 | }); 112 | }); 113 | } 114 | 115 | /** 116 | * @description 打开新窗口 117 | */ 118 | function openWin(url: string) { 119 | return GM_openInTab(url, { 120 | active: true, 121 | insert: true, 122 | setParent: true, 123 | }); 124 | } 125 | 126 | /** 127 | * @description 关闭窗口 128 | */ 129 | function closeWin() { 130 | page.value && page.value.close(); 131 | } 132 | 133 | /** 134 | * @description 关闭子窗口 135 | */ 136 | function handleCloseWin() { 137 | try { 138 | window.opener = window; 139 | const win = window.open('', '_self'); 140 | win?.close(); 141 | top?.close(); 142 | } catch (e) {} 143 | } 144 | 145 | /** 146 | * @description 等待窗口关闭 147 | * @param newPage 148 | * @returns 149 | */ 150 | function waitWinClose(newPage: Tampermonkey.OpenTabObject) { 151 | return new Promise((resolve) => { 152 | newPage.onclose = () => { 153 | resolve(undefined); 154 | }; 155 | }); 156 | } 157 | 158 | /** 159 | * @description 关闭任务窗口 160 | */ 161 | function closeTaskWin() { 162 | // 同屏任务 163 | if (settings[SettingType.SAME_TAB] && id.value) { 164 | closeFrame(); 165 | return; 166 | } 167 | // 非同屏任务 168 | closeWin(); 169 | } 170 | 171 | /** 172 | * @description 关闭任务窗口 173 | */ 174 | function handleCloseTaskWin() { 175 | // 同屏任务 176 | if (settings[SettingType.SAME_TAB] && id.value) { 177 | handleCloseFrame(); 178 | return; 179 | } 180 | // 子窗口 181 | handleCloseWin(); 182 | } 183 | 184 | /** 185 | * @description 打开并等待任务结束 186 | */ 187 | async function waitTaskWin(url: string, title?: string) { 188 | // 同屏任务 189 | if (settings[SettingType.SAME_TAB]) { 190 | // 窗口存在 191 | frame.exist = true; 192 | // 显示窗体 193 | frame.show = !settings[SettingType.SILENT_RUN]; 194 | // 新窗口 195 | const res = await openFrame(url, title); 196 | if (res) { 197 | // 等待窗口关闭 198 | await waitFrameClose(); 199 | } 200 | return; 201 | } 202 | // 子页面任务 203 | page.value = openWin(url); 204 | await waitWinClose(page.value); 205 | } 206 | 207 | export { 208 | openFrame, 209 | closeFrame, 210 | waitFrameClose, 211 | waitFrameLoaded, 212 | openWin, 213 | closeWin, 214 | waitWinClose, 215 | waitTaskWin, 216 | closeTaskWin, 217 | initMainListener, 218 | initChildListener, 219 | handleCloseTaskWin, 220 | }; 221 | -------------------------------------------------------------------------------- /src/controller/login.ts: -------------------------------------------------------------------------------- 1 | import { 2 | generateQRCode, 3 | getSign, 4 | loginWithQRCode, 5 | secureCheck, 6 | } from '../api/login'; 7 | import API_CONFIG from '../config/api'; 8 | import { autoRefreshQRCodeInterval, maxRefreshCount } from '../config/task'; 9 | import { 10 | frame, 11 | login, 12 | loginQRCodeShow, 13 | pushToken, 14 | refreshCount, 15 | settings, 16 | taskConfig, 17 | taskStatus, 18 | todayScore, 19 | totalScore, 20 | userinfo, 21 | } from '../shared'; 22 | import { SettingType, TaskStatusType } from '../types'; 23 | import { $$ } from '../utils/element'; 24 | import { log } from '../utils/log'; 25 | import { 26 | getHighlightHTML, 27 | getImgHTML, 28 | getProgressHTML, 29 | pushModal, 30 | } from '../utils/push'; 31 | import { delCookie } from '../utils/utils'; 32 | import { closeFrame } from './frame'; 33 | import { createTip } from './tip'; 34 | import { refreshScoreInfo, refreshTaskList, refreshUserInfo } from './user'; 35 | 36 | /** 37 | * @description 二维码刷新定时器 38 | */ 39 | let refreshTimer = -1; 40 | /** 41 | * @description 尝试登录 42 | */ 43 | let tryLoginTimer = -1; 44 | 45 | /** 46 | * @description 生成二维码 47 | */ 48 | async function getQRCode() { 49 | log('正在生成登录二维码...'); 50 | const qrCode = await generateQRCode(); 51 | if (qrCode) { 52 | log('生成登录二维码成功!'); 53 | // 链接 54 | const url = `https://login.xuexi.cn/login/qrcommit?showmenu=false&code=${qrCode}&appId=dingoankubyrfkttorhpou`; 55 | return { 56 | code: qrCode, 57 | src: `${API_CONFIG.qrcode}?data=${encodeURIComponent(url)}`, 58 | url, 59 | }; 60 | } 61 | log('生成登录二维码失败!'); 62 | } 63 | 64 | /** 65 | * @description 验证登录二维码 66 | * @param code 67 | * @returns 68 | */ 69 | async function checkQRCode(code: string) { 70 | log('尝试用二维码登录...'); 71 | // 二维码登录 72 | const res = await loginWithQRCode(code); 73 | if (res) { 74 | const { data, code, success } = res; 75 | // 临时登录验证码 76 | if (success && data) { 77 | return data; 78 | } 79 | // 二维码失效 80 | if (code === '11019') { 81 | return; 82 | } 83 | } 84 | return new Promise((resolve) => { 85 | // 清除定时 86 | clearTimeout(tryLoginTimer); 87 | // 设置定时 88 | tryLoginTimer = (setTimeout(async () => { 89 | resolve(await checkQRCode(code)); 90 | }, 1000)); 91 | }); 92 | } 93 | 94 | /** 95 | * @description 尝试二维码登录 96 | */ 97 | async function tryLogin(checkCode: string) { 98 | log('正在获取签名...'); 99 | // 获取签名 100 | const sign = await getSign(); 101 | if (sign) { 102 | // 生成uuid 103 | const uuid = crypto.randomUUID(); 104 | const [, code] = checkCode.split('='); 105 | const state = `${sign}${uuid}`; 106 | // 安全检查 107 | const res = await secureCheck({ code, state }); 108 | return res; 109 | } 110 | } 111 | 112 | /** 113 | * @description 刷新登录二维码 114 | */ 115 | async function handleLogin() { 116 | // 清除刷新 117 | clearInterval(refreshTimer); 118 | // 每隔一段时间刷新 119 | refreshTimer = (setInterval(() => { 120 | // 刷新二维码 121 | handleLogin(); 122 | }, autoRefreshQRCodeInterval)); 123 | // 是否超出次数 124 | if (refreshCount.value >= maxRefreshCount) { 125 | createTip('超过最大重试次数, 登录失败!'); 126 | // 重置刷新数 127 | refreshCount.value = 0; 128 | // 隐藏二维码 129 | loginQRCodeShow.value = false; 130 | // 远程推送 131 | if (settings[SettingType.REMOTE_PUSH]) { 132 | // 推送 133 | const res = await pushModal( 134 | { 135 | title: '登录推送', 136 | content: '超过最大重试次数, 登录失败!', 137 | type: 'fail', 138 | }, 139 | pushToken.value 140 | ); 141 | createTip(`登录推送${res ? '成功' : '失败'}!`); 142 | } 143 | return; 144 | } 145 | // 配置 146 | const imgWrap = $$('.egg_login_img_wrap')[0]; 147 | // 图片 148 | const img = $$('.egg_login_img', imgWrap)[0]; 149 | if (imgWrap && img) { 150 | // 刷新二维码 151 | log('刷新登录二维码!'); 152 | // 刷新次数累加 153 | refreshCount.value++; 154 | // 获取二维码 155 | const qrCode = await getQRCode(); 156 | if (qrCode) { 157 | // 获取连接 158 | const { src, code, url } = qrCode; 159 | // src 160 | img.src = src; 161 | // 开始登录 162 | loginQRCodeShow.value = true; 163 | // 远程推送 164 | if (settings[SettingType.REMOTE_PUSH]) { 165 | // img html 166 | const imgWrap = getImgHTML(src); 167 | // 跳转链接 168 | const aWrap = ` 169 |
170 | 或在浏览器 171 | ${getHighlightHTML('打开学习强国APP')} 178 |
179 | `; 180 | // 推送 181 | const res = await pushModal( 182 | { 183 | title: '登录推送', 184 | content: ['扫一扫, 登录学习强国!', aWrap, imgWrap], 185 | type: 'info', 186 | }, 187 | pushToken.value 188 | ); 189 | createTip(`登录推送${res ? '成功' : '失败'}!`); 190 | } 191 | // 获取验证码 192 | const checkCode = await checkQRCode(code); 193 | // 验证成功 194 | if (checkCode) { 195 | // 尝试登录 196 | const loginRes = await tryLogin(checkCode); 197 | if (loginRes) { 198 | // 清除刷新 199 | clearInterval(refreshTimer); 200 | // 二维码显示 201 | loginQRCodeShow.value = false; 202 | // 登录成功 203 | log('登录成功!'); 204 | // 创建提示 205 | createTip('登录成功!'); 206 | // 登录成功 207 | login.value = true; 208 | // 刷新用户信息 209 | await refreshUserInfo(); 210 | // 刷新分数信息 211 | await refreshScoreInfo(); 212 | // 刷新任务信息 213 | await refreshTaskList(); 214 | // 远程推送 215 | if (settings[SettingType.REMOTE_PUSH]) { 216 | const res = await pushModal( 217 | { 218 | title: '登录推送', 219 | to: userinfo.nick, 220 | content: [ 221 | '学习强国, 登录成功!', 222 | `当天积分: ${getHighlightHTML(todayScore.value)} 分`, 223 | `总积分: ${getHighlightHTML(totalScore.value)} 分`, 224 | ...taskConfig.map((task) => 225 | getProgressHTML(task.title, task.currentScore,task.dayMaxScore) 226 | ), 227 | ], 228 | type: 'success', 229 | }, 230 | pushToken.value 231 | ); 232 | createTip(`登录推送${res ? '成功' : '失败'}!`); 233 | } 234 | } 235 | return; 236 | } 237 | // 二维码失效 238 | log('登录二维码失效!'); 239 | // 二维码失效刷新 240 | handleLogin(); 241 | } 242 | } 243 | } 244 | 245 | /** 246 | * @description 退出登录 247 | */ 248 | function handleLogout() { 249 | // 删除token 250 | delCookie('token', '.xuexi.cn'); 251 | // 关闭窗口 252 | closeFrame(); 253 | frame.exist = false; 254 | // 退出登录 255 | login.value = false; 256 | // 清除用户信息 257 | userinfo.nick = ''; 258 | userinfo.avatar = ''; 259 | // 总分 260 | totalScore.value = 0; 261 | // 当天分数 262 | todayScore.value = 0; 263 | // 任务进度重置 264 | taskConfig.forEach((task) => { 265 | task.currentScore = 0; 266 | }); 267 | taskStatus.value = TaskStatusType.LOADING; 268 | // 退出登录 269 | log('退出登录'); 270 | } 271 | 272 | export { getQRCode, checkQRCode, tryLogin, handleLogin, handleLogout }; 273 | -------------------------------------------------------------------------------- /src/controller/readAndWatch.ts: -------------------------------------------------------------------------------- 1 | import { getNewsList, getVideoList } from '../api/data'; 2 | import { maxNewsNum, maxVideoNum } from '../config/task'; 3 | import { maxRead, maxWatch, settings, taskConfig } from '../shared'; 4 | import { NewsVideoList, SettingType, TaskType } from '../types'; 5 | import { watchEffect } from '../utils/composition'; 6 | import { $$, $_ } from '../utils/element'; 7 | import { log } from '../utils/log'; 8 | import { sleep, studyPauseLock } from '../utils/utils'; 9 | import { handleCloseTaskWin, waitTaskWin } from './frame'; 10 | import { createTip } from './tip'; 11 | import { refreshScoreInfo, refreshTaskList } from './user'; 12 | 13 | /** 14 | * @description 新闻 15 | */ 16 | let news: NewsVideoList = []; 17 | 18 | /** 19 | * @description 视频 20 | */ 21 | let videos: NewsVideoList = []; 22 | 23 | /** 24 | * @description 处理文章 25 | */ 26 | async function handleNews() { 27 | // section 28 | const sections = await $_('section', undefined, 5000); 29 | const section = sections[0]; 30 | if (!(section && section.innerText.includes('系统正在维护中'))) { 31 | // 文章选读 32 | reading(0); 33 | return; 34 | } 35 | log('未找到文章!'); 36 | // 提示 37 | createTip('未找到文章!'); 38 | // 关闭页面 39 | handleCloseTaskWin(); 40 | } 41 | 42 | /** 43 | * @description 处理视频 44 | */ 45 | async function handleVideo() { 46 | // videos 47 | const videos = await $_('video', undefined, 10000); 48 | // 视频 49 | const video = videos[0]; 50 | // 播放按键 51 | const playBtn = $$('.prism-play-btn')[0]; 52 | if (video && playBtn) { 53 | log('正在尝试播放视频...'); 54 | // 播放超时 55 | const timeout = setTimeout(() => { 56 | log('视频播放超时!'); 57 | // 提示 58 | createTip('视频播放超时!'); 59 | // 关闭页面 60 | handleCloseTaskWin(); 61 | }, 20000); 62 | // 设置是否静音 63 | watchEffect(() => (video.muted = settings[SettingType.VIDEO_MUTED])); 64 | // 能播放 65 | video.addEventListener( 66 | 'canplay', 67 | () => { 68 | const timer = setInterval(() => { 69 | // 尝试点击播放按钮播放 70 | playBtn.click(); 71 | // 播放未成功 72 | if (video.paused) { 73 | // 尝试使用js的方式播放 74 | video.play(); 75 | } 76 | }, 1000); 77 | video.addEventListener( 78 | 'playing', 79 | () => { 80 | // 清除超时定时器 81 | clearTimeout(timeout); 82 | // 清除定时器 83 | clearInterval(timer); 84 | log('播放视频成功!'); 85 | // 视听学习 86 | reading(1); 87 | return; 88 | }, 89 | { once: true } 90 | ); 91 | }, 92 | { once: true } 93 | ); 94 | return; 95 | } 96 | log('未找到视频!'); 97 | // 关闭页面 98 | handleCloseTaskWin(); 99 | } 100 | 101 | /** 102 | * @description 读新闻或者看视频 103 | * @param type :0为新闻,1为视频 104 | */ 105 | async function reading(type: number) { 106 | let time = 30; 107 | // 文章选读 108 | if (type === 0) { 109 | // 章节 110 | const sections = $$('section'); 111 | // 最大字数 112 | const maxTextCount = Math.max( 113 | ...sections.map((s) => s.innerText.length), 114 | 200 115 | ); 116 | // 预计时间 117 | const predictTime = ~~((60 * maxTextCount) / 1000); 118 | // min(predictTime, maxWatch.value) 秒后关闭页面 119 | time = Math.min(predictTime, maxRead.value); 120 | } 121 | // 视听学习 122 | if (type === 1) { 123 | // 视频 124 | const video = $$('video')[0]; 125 | // 预计时间 126 | const predictTime = ~~video.duration; 127 | // min(predictTime, maxWatch.value) 秒后关闭页面 128 | time = Math.min(predictTime, maxWatch.value); 129 | } 130 | // 随机 131 | time = time - ~~(Math.random() * 10) + 5; 132 | // 第一次滚动时间 133 | const firstTime = time - (~~(Math.random() * 4) + 4); 134 | // 第二次滚动时间 135 | const secendTime = ~~(Math.random() * 4) + 8; 136 | // 窗口 137 | const window = unsafeWindow; 138 | // 创建提示 139 | const tip = createTip('距离关闭页面还剩', time, true, async (time) => { 140 | // 暂停锁 141 | await studyPauseLock((flag) => { 142 | if (type === 1) { 143 | // 视频 144 | const video = $$('video')[0]; 145 | // 排除反复设置 146 | if (video.paused === !flag) { 147 | return; 148 | } 149 | // 设置播放状态 150 | video[flag ? 'play' : 'pause'](); 151 | } 152 | }); 153 | // 第一次滚动 154 | if (time === firstTime) { 155 | // 滚动 156 | window.scrollTo(0, 400); 157 | // 模拟滚动 158 | const scroll = new Event('scroll', { 159 | bubbles: true, 160 | }); 161 | document.dispatchEvent(scroll); 162 | // 模拟滑动 163 | const mousemove = new MouseEvent('mousemove', { 164 | bubbles: true, 165 | }); 166 | document.dispatchEvent(mousemove); 167 | // 模拟点击 168 | const click = new Event('click', { 169 | bubbles: true, 170 | }); 171 | document.dispatchEvent(click); 172 | } 173 | // 第二次滚动 174 | if (time === secendTime) { 175 | // 滚动长度 176 | const scrollLength = document.body.scrollHeight / 2; 177 | // 滚动 178 | window.scrollTo(0, scrollLength); 179 | // 模拟滚动 180 | const scroll = new Event('scroll', { 181 | bubbles: true, 182 | }); 183 | document.dispatchEvent(scroll); 184 | // 模拟滑动 185 | const mousemove = new MouseEvent('mousemove', { 186 | bubbles: true, 187 | }); 188 | document.dispatchEvent(mousemove); 189 | // 模拟点击 190 | const click = new Event('click', { 191 | bubbles: true, 192 | }); 193 | document.dispatchEvent(click); 194 | } 195 | }); 196 | // 倒计时结束 197 | await tip.waitCountDown(); 198 | // 关闭任务窗口 199 | handleCloseTaskWin(); 200 | } 201 | 202 | /** 203 | * @description 获取新闻列表 204 | */ 205 | async function getNews() { 206 | // 需要学习的新闻数量 207 | const need = 208 | taskConfig[TaskType.READ].need < maxNewsNum 209 | ? taskConfig[TaskType.READ].need 210 | : maxNewsNum; 211 | log(`剩余 ${need} 个新闻`); 212 | // 获取新闻 213 | const data = await getNewsList(); 214 | if (data && data.length) { 215 | // 索引 216 | let i = 0; 217 | // 最新新闻 218 | const latestItems = data.slice(0, 100); 219 | // 当前年份 220 | const currentYear = new Date().getFullYear().toString(); 221 | // 查找今年新闻 222 | while (i < need) { 223 | const randomIndex = ~~(Math.random() * latestItems.length); 224 | // 新闻 225 | const item = latestItems[randomIndex]; 226 | // 是否存在 227 | if (item.publishTime.startsWith(currentYear) && item.type === 'tuwen') { 228 | news[i] = item; 229 | i++; 230 | } 231 | } 232 | } else { 233 | news = []; 234 | } 235 | } 236 | 237 | /** 238 | * @description 获取视频列表 239 | */ 240 | async function getVideos() { 241 | // 需要学习的视频数量 242 | const need = 243 | taskConfig[TaskType.WATCH].need < maxVideoNum 244 | ? taskConfig[TaskType.WATCH].need 245 | : maxVideoNum; 246 | log(`剩余 ${need} 个视频`); 247 | // 获取视频 248 | const data = await getVideoList(); 249 | if (data && data.length) { 250 | // 索引 251 | let i = 0; 252 | // 最新视频 253 | const latestItems = data.slice(0, 100); 254 | // 当前年份 255 | const currentYear = new Date().getFullYear().toString(); 256 | // 查找今年视频 257 | while (i < need) { 258 | const randomIndex = ~~(Math.random() * latestItems.length); 259 | // 新闻 260 | const item = latestItems[randomIndex]; 261 | // 是否存在 262 | if ( 263 | item.publishTime.startsWith(currentYear) && 264 | (item.type === 'shipin' || item.type === 'juji') 265 | ) { 266 | videos[i] = item; 267 | i++; 268 | } 269 | } 270 | } else { 271 | videos = []; 272 | } 273 | } 274 | 275 | /** 276 | * @description 阅读文章 277 | */ 278 | async function readNews() { 279 | // 获取文章 280 | await getNews(); 281 | // 观看文章 282 | for (const i in news) { 283 | // 任务关闭跳出循环 284 | if (!taskConfig[TaskType.READ].active) { 285 | return; 286 | } 287 | // 暂停 288 | await studyPauseLock(); 289 | log(`正在阅读第 ${Number(i) + 1} 个新闻...`); 290 | // 创建提示 291 | createTip(`正在阅读第 ${Number(i) + 1} 个新闻`); 292 | // 链接 293 | const { url } = news[i]; 294 | // 链接 295 | GM_setValue('readingUrl', url); 296 | // 等待任务窗口 297 | await waitTaskWin(url, '文章选读'); 298 | // 清空链接 299 | GM_setValue('readingUrl', null); 300 | // 创建提示 301 | createTip(`完成阅读第 ${Number(i) + 1} 个新闻!`); 302 | // 等待一段时间 303 | await sleep(1500); 304 | // 刷新分数数据 305 | await refreshScoreInfo(); 306 | // 刷新任务数据 307 | await refreshTaskList(); 308 | // 任务完成跳出循环 309 | if (taskConfig[TaskType.READ].active && taskConfig[TaskType.READ].status) { 310 | break; 311 | } 312 | } 313 | // 任务关闭跳出循环 314 | if (!taskConfig[TaskType.READ].active) { 315 | return; 316 | } 317 | // 任务完成状况 318 | if (taskConfig[TaskType.READ].active && !taskConfig[TaskType.READ].status) { 319 | log('任务未完成, 继续阅读新闻!'); 320 | // 创建提示 321 | createTip('任务未完成, 继续阅读新闻!'); 322 | await readNews(); 323 | } 324 | } 325 | 326 | /** 327 | * @description 观看视频 328 | */ 329 | async function watchVideo() { 330 | // 获取视频 331 | await getVideos(); 332 | // 观看视频 333 | for (const i in videos) { 334 | // 任务关闭跳出循环 335 | if (!taskConfig[TaskType.WATCH].active) { 336 | return; 337 | } 338 | // 暂停 339 | await studyPauseLock(); 340 | log(`正在观看第 ${Number(i) + 1} 个视频...`); 341 | // 创建提示 342 | createTip(`正在观看第 ${Number(i) + 1} 个视频`); 343 | // 链接 344 | const { url } = videos[i]; 345 | // 链接 346 | GM_setValue('watchingUrl', url); 347 | // 等待任务窗口 348 | await waitTaskWin(url, '视听学习'); 349 | // 清空链接 350 | GM_setValue('watchingUrl', null); 351 | // 创建提示 352 | createTip(`完成观看第 ${Number(i) + 1} 个视频!`); 353 | // 等待一段时间 354 | await sleep(1500); 355 | // 刷新分数数据 356 | await refreshScoreInfo(); 357 | // 刷新任务数据 358 | await refreshTaskList(); 359 | // 任务完成跳出循环 360 | if ( 361 | taskConfig[TaskType.WATCH].active && 362 | taskConfig[TaskType.WATCH].status 363 | ) { 364 | break; 365 | } 366 | } 367 | // 任务关闭跳出循环 368 | if (!taskConfig[TaskType.WATCH].active) { 369 | return; 370 | } 371 | // 任务完成状况 372 | if (taskConfig[TaskType.WATCH].active && !taskConfig[TaskType.WATCH].status) { 373 | log('任务未完成, 继续观看视频!'); 374 | // 创建提示 375 | createTip('任务未完成, 继续观看看视频!'); 376 | await watchVideo(); 377 | } 378 | } 379 | 380 | export { handleNews, handleVideo, readNews, reading, watchVideo }; 381 | -------------------------------------------------------------------------------- /src/controller/schedule.ts: -------------------------------------------------------------------------------- 1 | import { login, scheduleList } from '../shared'; 2 | import { log } from '../utils/log'; 3 | import { isLate, isNow } from '../utils/time'; 4 | import { handleLogin } from './login'; 5 | import { createTip } from './tip'; 6 | 7 | /** 8 | * @description 定时刷新定时器 9 | */ 10 | let scheduleTimer = -1; 11 | 12 | /** 13 | * @description 刷新定时任务 14 | */ 15 | async function refreshScheduleTask() { 16 | // 清除定时刷新 17 | clearInterval(scheduleTimer); 18 | // 未登录 19 | if (!login.value) { 20 | // 剩余定时任务 21 | const restList = scheduleList.filter((s) => !isLate(s)); 22 | // 存在剩余任务 23 | if (restList.length) { 24 | const rest = restList[0]; 25 | log(`已设置 ${rest.time} 的定时任务!`); 26 | // 提示 27 | createTip(`已设置 ${rest.time} 的定时任务!`); 28 | // 时间 29 | let time = 0; 30 | // 刷新间隔 31 | const interval = 10; 32 | scheduleTimer = (setInterval(() => { 33 | if (!(time++ % interval)) { 34 | log('定时刷新正在运行...'); 35 | } 36 | // 到达定时 37 | if (isNow(rest)) { 38 | clearInterval(scheduleTimer); 39 | log(`执行 ${rest.time} 的定时任务!`); 40 | // 提示 41 | createTip(`执行 ${rest.time} 的定时任务!`); 42 | // 登录 43 | handleLogin(); 44 | } 45 | }, 1000)); 46 | } 47 | } 48 | } 49 | 50 | /** 51 | * @description 清除定时 52 | */ 53 | function clearScheduleTask() { 54 | clearInterval(scheduleTimer); 55 | } 56 | 57 | export { refreshScheduleTask, clearScheduleTask }; 58 | -------------------------------------------------------------------------------- /src/controller/tip.ts: -------------------------------------------------------------------------------- 1 | import { Tip } from '../component/Tip'; 2 | import { ref } from '../utils/composition'; 3 | import { $$, mountElement } from '../utils/element'; 4 | 5 | /** 6 | * @description 创建学习提示 7 | */ 8 | function createTip( 9 | text: string, 10 | delay: number = 2, 11 | countShow: boolean = false, 12 | callback?: (current: number) => any 13 | ) { 14 | const tipWrap = $$('.egg_tip_wrap')[0]; 15 | // 提前去除 16 | const tips = $$ void }>('.egg_tip'); 17 | if (tips.length) { 18 | tips.forEach((t) => t.delay()); 19 | } 20 | // 延迟 21 | const delayCount = ref(delay); 22 | // 文字 23 | const textContent = ref(text); 24 | //显示 25 | const show = ref(false); 26 | // 延迟显示 27 | const delayShow = ref(false); 28 | // 销毁 29 | let destroyed = false; 30 | // 倒计时结束 31 | let done = false; 32 | // 提示 33 | const tip = Tip({ 34 | text: textContent, 35 | count: delayCount, 36 | show, 37 | delayShow, 38 | countShow: ref(countShow), 39 | callback: async (count) => { 40 | callback && (await callback(count)); 41 | // 恢复显示 42 | if (delayShow.value && count === delay) { 43 | delayShow.value = false; 44 | } 45 | // 倒计时结束 46 | if (count <= 0) { 47 | done = true; 48 | operate.destroy(); 49 | } 50 | }, 51 | }); 52 | // 操作 53 | const operate = { 54 | destroy() { 55 | if (!destroyed) { 56 | // 隐藏 57 | operate.hide(); 58 | // 销毁 59 | destroyed = true; 60 | return new Promise((resolve) => { 61 | setTimeout(() => { 62 | tip.ele.remove(); 63 | resolve(undefined); 64 | }, 300); 65 | }); 66 | } 67 | }, 68 | hide() { 69 | if (!destroyed) { 70 | show.value = false; 71 | } 72 | }, 73 | show() { 74 | if (!destroyed) { 75 | return new Promise((resolve) => { 76 | setTimeout(() => { 77 | show.value = true; 78 | resolve(undefined); 79 | }, 300); 80 | }); 81 | } 82 | }, 83 | setText(text: string) { 84 | if (!destroyed) { 85 | textContent.value = text; 86 | } 87 | }, 88 | waitCountDown() { 89 | return new Promise((resolve) => { 90 | // 计时器 91 | const timer = setInterval(() => { 92 | // 结束 93 | if (done) { 94 | clearInterval(timer); 95 | resolve(true); 96 | } 97 | }, 100); 98 | }); 99 | }, 100 | delay() { 101 | if (!destroyed) { 102 | delayShow.value = true; 103 | delayCount.value += 2; 104 | } 105 | }, 106 | }; 107 | Object.assign(tip.ele, operate); 108 | // 插入节点 109 | mountElement(tip, tipWrap); 110 | // 显示 111 | operate.show(); 112 | return operate; 113 | } 114 | 115 | export { createTip }; 116 | -------------------------------------------------------------------------------- /src/controller/user.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getTaskList, 3 | getTodayScore, 4 | getTotalScore, 5 | getUserInfo, 6 | } from '../api/user'; 7 | import { login, taskConfig, todayScore, totalScore, userinfo } from '../shared'; 8 | import { TaskType } from '../types'; 9 | import { log } from '../utils/log'; 10 | import { sleep } from '../utils/utils'; 11 | 12 | /** 13 | * @description 刷新用户信息 14 | */ 15 | async function refreshUserInfo() { 16 | // 未登录 17 | if (!login.value) { 18 | throw new Error('用户未登录!'); 19 | } 20 | // 已存在信息 21 | if (userinfo.nick) { 22 | return true; 23 | } 24 | log('加载用户信息...'); 25 | // 获取用户信息 26 | const res = await getUserInfo(); 27 | if (res) { 28 | const { avatarMediaUrl = '', nick: nickRes } = res; 29 | if (nickRes) { 30 | // 设置昵称 31 | userinfo.nick = nickRes; 32 | // 设置头像 33 | userinfo.avatar = avatarMediaUrl; 34 | return true; 35 | } 36 | } 37 | log('加载用户信息失败!'); 38 | return false; 39 | } 40 | 41 | /** 42 | * @description 刷新分数信息 43 | */ 44 | async function refreshScoreInfo() { 45 | // 未登录 46 | if (!login.value) { 47 | throw new Error('用户未登录!'); 48 | } 49 | log('加载分数信息...'); 50 | // 获取总分 51 | const totalScoreRes = await getTotalScore(); 52 | // 获取当天总分 53 | const todayScoreRes = await getTodayScore(); 54 | // 整数值 55 | if (Number.isInteger(totalScoreRes) && Number.isInteger(todayScoreRes)) { 56 | // 设置分数 57 | totalScore.value = totalScoreRes; 58 | todayScore.value = todayScoreRes; 59 | return true; 60 | } 61 | log('加载分数信息失败!'); 62 | return false; 63 | } 64 | 65 | /** 66 | * @description 刷新任务列表 67 | */ 68 | async function refreshTaskList() { 69 | // 未登录 70 | if (!login.value) { 71 | throw new Error('用户未登录!'); 72 | } 73 | log('加载任务进度...'); 74 | // 原始任务进度 75 | const taskProgress = await getTaskList(); 76 | if (taskProgress) { 77 | // 登录 78 | taskConfig[TaskType.LOGIN].currentScore = taskProgress[2].currentScore; 79 | taskConfig[TaskType.LOGIN].dayMaxScore = taskProgress[2].dayMaxScore; 80 | taskConfig[TaskType.LOGIN].need = 81 | taskProgress[2].dayMaxScore - taskProgress[2].currentScore; 82 | // 文章选读 83 | taskConfig[TaskType.READ].currentScore = taskProgress[0].currentScore; 84 | taskConfig[TaskType.READ].dayMaxScore = taskProgress[0].dayMaxScore; 85 | taskConfig[TaskType.READ].need = 86 | taskProgress[0].dayMaxScore - taskProgress[0].currentScore; 87 | // 视听学习 88 | taskConfig[TaskType.WATCH].currentScore = taskProgress[1].currentScore; 89 | taskConfig[TaskType.WATCH].dayMaxScore = taskProgress[1].dayMaxScore; 90 | taskConfig[TaskType.WATCH].need = 91 | taskProgress[1].dayMaxScore - taskProgress[1].currentScore; 92 | // 每日答题 93 | taskConfig[TaskType.PRACTICE].currentScore = taskProgress[3].currentScore; 94 | taskConfig[TaskType.PRACTICE].dayMaxScore = taskProgress[3].dayMaxScore; 95 | taskConfig[TaskType.PRACTICE].need = taskProgress[3].dayMaxScore; 96 | // 更新数据 97 | for (const i in taskConfig) { 98 | const { currentScore, dayMaxScore } = taskConfig[i]; 99 | // 进度 100 | const rate = Number(((100 * currentScore) / dayMaxScore).toFixed(1)); 101 | // 分数 102 | taskConfig[i].score = currentScore; 103 | // 完成状态 104 | taskConfig[i].status = rate === 100; 105 | } 106 | return; 107 | } 108 | // 重试 109 | await sleep(2000); 110 | refreshTaskList(); 111 | return; 112 | } 113 | 114 | export { refreshUserInfo, refreshScoreInfo, refreshTaskList }; 115 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {} from './api/answer'; 2 | import {} from './api/data'; 3 | import {} from './api/login'; 4 | import {} from './api/push'; 5 | import {} from './api/user'; 6 | import {} from './config/task'; 7 | import URL_CONFIG from './config/url'; 8 | import {} from './config/api'; 9 | import { version } from './config/version'; 10 | import { Settings } from './types'; 11 | import { watch } from './utils/composition'; 12 | import { $$, $_, createElementNode, mountElement } from './utils/element'; 13 | import { error, log } from './utils/log'; 14 | import {} from './utils/push'; 15 | import {} from './utils/random'; 16 | import {} from './utils/time'; 17 | import { hasMobile, load } from './utils/utils'; 18 | import { maxRead, maxWatch, settings, taskConfig, themeColor } from './shared'; 19 | import { doingExam, ExamType } from './controller/exam'; 20 | import { initChildListener, initMainListener } from './controller/frame'; 21 | import {} from './controller/login'; 22 | import { handleNews, handleVideo } from './controller/readAndWatch'; 23 | import {} from './controller/schedule'; 24 | import { createTip } from './controller/tip'; 25 | import {} from './controller/user'; 26 | import {} from './component/Tip'; 27 | import {} from './component/Hr'; 28 | import {} from './component/Select'; 29 | import { ExamBtn } from './component/ExamBtn'; 30 | import { Frame } from './component/Frame'; 31 | import {} from './component/LoginItem'; 32 | import {} from './component/InfoItem'; 33 | import {} from './component/ScoreItem'; 34 | import {} from './component/NoramlItem'; 35 | import {} from './component/TaskItem'; 36 | import {} from './component/TaskList'; 37 | import {} from './component/TaskBtn'; 38 | import {} from './component/ScheduleList'; 39 | import {} from './component/TimeInput'; 40 | import {} from './component/SettingsPanel'; 41 | import { Panel } from './component/Panel'; 42 | import css from './css/index.css?raw'; 43 | 44 | /** 45 | * @description 嵌入样式 46 | */ 47 | GM_addStyle(css); 48 | 49 | load( 50 | (href) => href.match(URL_CONFIG.home), 51 | () => { 52 | // 初始化logo 53 | initLogo(); 54 | // 页面提示 55 | log('进入主页面!'); 56 | 57 | // 初始化主题 58 | initThemeColor(); 59 | // 初始化任务配置 60 | initTaskConfig(); 61 | // 初始化设置 62 | initSettings(); 63 | // 设置字体 64 | initFontSize(); 65 | // 初始化主页面 66 | initMainListener(); 67 | // 初始化提示 68 | renderTip(); 69 | // 渲染面板 70 | renderPanel(); 71 | // 渲染窗口 72 | renderFrame(); 73 | } 74 | ); 75 | 76 | load( 77 | (href) => href === GM_getValue('readingUrl'), 78 | async () => { 79 | // 页面提示 80 | log('进入文章选读页面!'); 81 | 82 | // 初始化主题 83 | initThemeColor(); 84 | // 初始化设置 85 | initSettings(); 86 | // 设置字体 87 | initFontSize(); 88 | // 最大阅读 89 | initMaxRead(); 90 | // 初始化子页面 91 | initChildListener(); 92 | // 初始化提示 93 | renderTip(); 94 | try { 95 | // 处理文章 96 | await handleNews(); 97 | } catch (err: unknown) { 98 | if (err instanceof Error) { 99 | // 提示 100 | createTip(err.message); 101 | // 错误 102 | error(err.message); 103 | return; 104 | } 105 | // 提示 106 | createTip(String(err)); 107 | // 错误 108 | error(err); 109 | } 110 | } 111 | ); 112 | 113 | load( 114 | (href) => href === GM_getValue('watchingUrl'), 115 | async () => { 116 | // 页面提示 117 | log('进入视听学习页面!'); 118 | 119 | // 初始化主题 120 | initThemeColor(); 121 | // 初始化设置 122 | initSettings(); 123 | // 设置字体 124 | initFontSize(); 125 | // 最大视听 126 | initMaxWatch(); 127 | // 初始化子页面 128 | initChildListener(); 129 | // 初始化提示 130 | renderTip(); 131 | try { 132 | // 处理视频 133 | await handleVideo(); 134 | } catch (err: unknown) { 135 | if (err instanceof Error) { 136 | // 提示 137 | createTip(err.message); 138 | // 错误 139 | error(err.message); 140 | return; 141 | } 142 | // 提示 143 | createTip(String(err)); 144 | // 错误 145 | error(err); 146 | } 147 | } 148 | ); 149 | 150 | load( 151 | (href) => href === URL_CONFIG.examPractice, 152 | async () => { 153 | // 页面提示 154 | log('进入每日答题页面!'); 155 | 156 | // 初始化主题 157 | initThemeColor(); 158 | // 初始化设置 159 | initSettings(); 160 | // 设置字体 161 | initFontSize(); 162 | // 初始化子页面 163 | initChildListener(); 164 | // 初始化提示 165 | renderTip(); 166 | // 创建答题按钮 167 | await renderExamBtn(); 168 | try { 169 | // 开始答题 170 | await doingExam(ExamType.PRACTICE); 171 | } catch (err: unknown) { 172 | if (err instanceof Error) { 173 | // 提示 174 | createTip(err.message); 175 | // 错误 176 | error(err.message); 177 | return; 178 | } 179 | // 提示 180 | createTip(String(err)); 181 | // 错误 182 | error(err); 183 | } 184 | } 185 | ); 186 | 187 | load( 188 | (href) => href.includes(URL_CONFIG.examPaper), 189 | async () => { 190 | // 页面提示 191 | log('进入专项练习页面!'); 192 | 193 | // 初始化主题 194 | initThemeColor(); 195 | // 初始化设置 196 | initSettings(); 197 | // 设置字体 198 | initFontSize(); 199 | // 初始化子页面 200 | initChildListener(); 201 | // 初始化提示 202 | renderTip(); 203 | // 创建答题按钮 204 | await renderExamBtn(); 205 | // 开始答题 206 | doingExam(ExamType.PAPER); 207 | return; 208 | } 209 | ); 210 | 211 | /** 212 | * @description 初始化logo 213 | */ 214 | function initLogo() { 215 | console.log( 216 | `%c tech-study.js %c ${version} `, 217 | 'background:dodgerblue;color:white;font-size:15px;border-radius:4px 0 0 4px;padding:2px 0;', 218 | 'background:black;color:gold;font-size:15px;border-radius:0 4px 4px 0;padding:2px 0;' 219 | ); 220 | } 221 | 222 | /** 223 | * @description 初始化配置 224 | */ 225 | function initTaskConfig() { 226 | try { 227 | const taskTemp = JSON.parse(GM_getValue('taskConfig')); 228 | if (taskTemp && Array.isArray(taskTemp)) { 229 | if (taskTemp.length === taskConfig.length) { 230 | taskConfig.forEach((task, i) => { 231 | task.active = taskTemp[i].active; 232 | }); 233 | } 234 | } 235 | // 监听值变化 236 | GM_addValueChangeListener('taskConfig', (key, oldVal, newVal, remote) => { 237 | if (remote) { 238 | const taskTemp = JSON.parse(newVal); 239 | if (taskTemp && Array.isArray(taskTemp)) { 240 | if (taskTemp.length === taskConfig.length) { 241 | taskConfig.forEach((task, i) => { 242 | task.active = taskTemp[i].active; 243 | }); 244 | } 245 | } 246 | } 247 | }); 248 | } catch (e) {} 249 | } 250 | 251 | /** 252 | * @description 初始化配置 253 | */ 254 | function initSettings() { 255 | try { 256 | const settingsTemp = JSON.parse(GM_getValue('studySettings')); 257 | if (settingsTemp && Array.isArray(settingsTemp)) { 258 | if (settingsTemp.length === settings.length) { 259 | for (const i in settingsTemp) { 260 | settings[i] = (settingsTemp)[i]; 261 | } 262 | } 263 | } 264 | // 监听值变化 265 | GM_addValueChangeListener( 266 | 'studySettings', 267 | (key, oldVal, newVal, remote) => { 268 | if (remote) { 269 | const settingsTemp = JSON.parse(newVal); 270 | if (settingsTemp && Array.isArray(settingsTemp)) { 271 | if (settingsTemp.length === settings.length) { 272 | for (const i in settingsTemp) { 273 | settings[i] = (settingsTemp)[i]; 274 | } 275 | } 276 | } 277 | } 278 | } 279 | ); 280 | } catch (e) {} 281 | } 282 | 283 | /** 284 | * @description 初始化配置 285 | */ 286 | function initFontSize() { 287 | // 移动端 288 | const moblie = hasMobile(); 289 | if (moblie) { 290 | // 清除缩放 291 | const meta = $$('meta[name=viewport]')[0]; 292 | if (meta) { 293 | meta.content = 'initial-scale=0, user-scalable=yes'; 294 | } 295 | // 缩放比例 296 | const scale = ~~(window.innerWidth / window.outerWidth) || 1; 297 | document.documentElement.style.setProperty('--scale', String(scale)); 298 | } 299 | } 300 | 301 | /** 302 | * @description 初始化最大阅读时长 303 | */ 304 | function initMaxRead() { 305 | try { 306 | const maxReadTemp = GM_getValue('maxRead'); 307 | if (maxReadTemp) { 308 | maxRead.value = maxReadTemp; 309 | } 310 | } catch (error) {} 311 | } 312 | 313 | /** 314 | * @description 初始化最大视听时长 315 | */ 316 | function initMaxWatch() { 317 | try { 318 | const maxWatchTemp = GM_getValue('maxWatch'); 319 | if (maxWatchTemp) { 320 | maxWatch.value = maxWatchTemp; 321 | } 322 | } catch (error) {} 323 | } 324 | 325 | /** 326 | * @description 初始化主题色 327 | */ 328 | function initThemeColor() { 329 | try { 330 | // 监听主题变化 331 | watch(themeColor, () => { 332 | // 设置主题 333 | document.documentElement.style.setProperty( 334 | '--themeColor', 335 | themeColor.value 336 | ); 337 | }); 338 | // 主题色 339 | const themeColorTemp = GM_getValue('themeColor'); 340 | if (themeColorTemp) { 341 | themeColor.value = themeColorTemp; 342 | } 343 | // 监听值变化 344 | GM_addValueChangeListener('themeColor', (key, oldVal, newVal, remote) => { 345 | if (remote) { 346 | // 主题色 347 | const themeColorTemp = newVal; 348 | if (themeColorTemp) { 349 | themeColor.value = themeColorTemp; 350 | } 351 | } 352 | }); 353 | } catch (error) {} 354 | } 355 | 356 | /** 357 | * @description 渲染提示 358 | */ 359 | function renderTip() { 360 | const tipWrap = createElementNode('div', undefined, { 361 | class: 'egg_tip_wrap', 362 | onclick(e: Event) { 363 | e.stopPropagation(); 364 | }, 365 | onmousedown(e: Event) { 366 | e.stopPropagation(); 367 | }, 368 | onmousemove(e: Event) { 369 | e.stopPropagation(); 370 | }, 371 | onmouseup(e: Event) { 372 | e.stopPropagation(); 373 | }, 374 | onmouseenter(e: Event) { 375 | e.stopPropagation(); 376 | }, 377 | onmouseleave(e: Event) { 378 | e.stopPropagation(); 379 | }, 380 | onmouseover(e: Event) { 381 | e.stopPropagation(); 382 | }, 383 | ontouchstart(e: Event) { 384 | e.stopPropagation(); 385 | }, 386 | ontouchmove(e: Event) { 387 | e.stopPropagation(); 388 | }, 389 | ontouchend(e: Event) { 390 | e.stopPropagation(); 391 | }, 392 | oninput(e: Event) { 393 | e.stopPropagation(); 394 | }, 395 | onchange(e: Event) { 396 | e.stopPropagation(); 397 | }, 398 | onblur(e: Event) { 399 | e.stopPropagation(); 400 | }, 401 | }); 402 | mountElement(tipWrap); 403 | } 404 | 405 | /** 406 | * @description 渲染答题按钮 407 | */ 408 | async function renderExamBtn() { 409 | const titles = await $_('.title'); 410 | if (titles.length) { 411 | // 插入节点 412 | titles[0].parentNode?.insertBefore(ExamBtn().ele, titles[0].nextSibling); 413 | } 414 | } 415 | 416 | /** 417 | * @description 渲染面板 418 | * @returns 419 | */ 420 | async function renderPanel() { 421 | // 面板 422 | const panel = Panel(); 423 | // 插入节点 424 | mountElement(panel); 425 | } 426 | 427 | /** 428 | * @description 渲染窗口 429 | */ 430 | function renderFrame() { 431 | // 窗口 432 | const frame = Frame(); 433 | // 插入节点 434 | mountElement(frame); 435 | } 436 | -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | /* 变量 */ 2 | 3 | import { Schedule, Settings, TaskStatusType, TaskType } from '../types'; 4 | import { reactive, ref, shallowReactive } from '../utils/composition'; 5 | import { getCookie } from '../utils/utils'; 6 | 7 | /** 8 | * @description 链接 9 | */ 10 | const href = window.location.href; 11 | 12 | /** 13 | * @description 任务配置 14 | */ 15 | const taskConfig = reactive([ 16 | { 17 | title: '登录', 18 | currentScore: 0, 19 | dayMaxScore: 0, 20 | need: 0, 21 | status: false, 22 | tip: '每日首次登录积1分。', 23 | score: 0, 24 | active: true, 25 | immutable: true, 26 | type: TaskType.LOGIN, 27 | }, 28 | { 29 | title: '文章选读', 30 | currentScore: 0, 31 | dayMaxScore: 0, 32 | need: 0, 33 | status: false, 34 | tip: '每有效阅读一篇文章积1分,上限6分。有效阅读文章累计1分钟积1分,上限6分。每日上限积12分。', 35 | score: 0, 36 | active: true, 37 | immutable: false, 38 | type: TaskType.READ, 39 | }, 40 | { 41 | title: '视听学习', 42 | currentScore: 0, 43 | dayMaxScore: 0, 44 | need: 0, 45 | status: false, 46 | tip: '每有效一个音频或观看一个视频积1分,上限6分。有效收听音频或观看视频累计1分钟积1分,上限6分。每日上限积12分。', 47 | score: 0, 48 | active: true, 49 | immutable: false, 50 | type: TaskType.WATCH, 51 | }, 52 | { 53 | title: '每日答题', 54 | currentScore: 0, 55 | dayMaxScore: 0, 56 | need: 0, 57 | status: false, 58 | tip: '每组答题每答对1道积1分。每日上限积5分。', 59 | score: 0, 60 | active: true, 61 | immutable: false, 62 | type: TaskType.PRACTICE, 63 | }, 64 | ]); 65 | 66 | /** 67 | * @description 设置 68 | */ 69 | const settings = reactive([ 70 | false, 71 | false, 72 | false, 73 | false, 74 | false, 75 | false, 76 | false, 77 | false, 78 | ]); 79 | 80 | /** 81 | * @description 总分 82 | */ 83 | const totalScore = ref(0); 84 | 85 | /** 86 | * @description 当天分数 87 | */ 88 | const todayScore = ref(0); 89 | 90 | /** 91 | * @description 用户信息 92 | */ 93 | const userinfo = reactive({ 94 | nick: '', 95 | avatar: '', 96 | }); 97 | 98 | /** 99 | * @description 进度 100 | */ 101 | const taskStatus = ref(TaskStatusType.LOADING); 102 | 103 | /** 104 | * @description 答题暂停 105 | */ 106 | const examPause = ref(false); 107 | 108 | /** 109 | * @description 登录 110 | */ 111 | const login = ref(!!getCookie('token')); 112 | 113 | /** 114 | * @description 窗口id 115 | */ 116 | const id = ref(''); 117 | 118 | /** 119 | * @description 定时刷新列表 120 | */ 121 | const scheduleList = shallowReactive([]); 122 | 123 | /** 124 | * @description 推送token 125 | */ 126 | const pushToken = ref(''); 127 | 128 | /** 129 | * @description 刷新次数 130 | */ 131 | const refreshCount = ref(0); 132 | 133 | /** 134 | * @description 窗口关闭 135 | */ 136 | const frame = reactive<{ 137 | title: string; 138 | show: boolean; 139 | exist: boolean; 140 | closed: boolean; 141 | ele: HTMLIFrameElement | undefined; 142 | src: string; 143 | }>({ 144 | title: '', 145 | show: false, 146 | exist: false, 147 | closed: true, 148 | ele: undefined, 149 | src: '', 150 | }); 151 | 152 | /** 153 | * @description 页面 154 | */ 155 | const page = ref(undefined); 156 | 157 | /** 158 | * @description 开始登录 159 | */ 160 | const loginQRCodeShow = ref(false); 161 | 162 | /** 163 | * @description 最大选读时长 164 | */ 165 | const maxRead = ref(100); 166 | 167 | /** 168 | * @description 最大视听时长 169 | */ 170 | const maxWatch = ref(120); 171 | 172 | /** 173 | * @description 运行其他任务 174 | */ 175 | const running = ref(false); 176 | 177 | /** 178 | * @description 主题色 179 | */ 180 | const themeColor = ref('#fa3333'); 181 | 182 | export { 183 | href, 184 | taskConfig, 185 | settings, 186 | todayScore, 187 | totalScore, 188 | userinfo, 189 | taskStatus, 190 | examPause, 191 | login, 192 | id, 193 | scheduleList, 194 | pushToken, 195 | refreshCount, 196 | loginQRCodeShow, 197 | frame, 198 | page, 199 | maxRead, 200 | maxWatch, 201 | running, 202 | themeColor, 203 | }; 204 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 任务类型 3 | */ 4 | enum TaskType { 5 | LOGIN, 6 | READ, 7 | WATCH, 8 | PRACTICE, 9 | } 10 | 11 | /** 12 | * @description 设置类型 13 | */ 14 | enum SettingType { 15 | AUTO_START, 16 | SAME_TAB, 17 | SILENT_RUN, 18 | SCHEDULE_RUN, 19 | VIDEO_MUTED, 20 | RANDOM_EXAM, 21 | AUTO_ANSWER, 22 | REMOTE_PUSH, 23 | } 24 | 25 | /** 26 | * @description 设置 27 | */ 28 | type Settings = [ 29 | boolean, 30 | boolean, 31 | boolean, 32 | boolean, 33 | boolean, 34 | boolean, 35 | boolean, 36 | boolean 37 | ]; 38 | 39 | /** 40 | * @description 定时信息 41 | */ 42 | type Schedule = { 43 | time: string; 44 | hour: number; 45 | minute: number; 46 | }; 47 | 48 | /** 49 | * @description 进度类型 50 | */ 51 | enum TaskStatusType { 52 | LOADING, 53 | LOADED, 54 | START, 55 | PAUSE, 56 | FINISH, 57 | } 58 | 59 | /** 60 | * @description 文章视听列表 61 | */ 62 | type NewsVideoList = { 63 | publishTime: string; 64 | title: string; 65 | type: string; 66 | url: string; 67 | showSource: string; 68 | dataValid: boolean; 69 | itemType: string; 70 | }[]; 71 | 72 | export { 73 | TaskType, 74 | SettingType, 75 | Settings, 76 | Schedule, 77 | TaskStatusType, 78 | NewsVideoList, 79 | }; 80 | -------------------------------------------------------------------------------- /src/utils/composition.ts: -------------------------------------------------------------------------------- 1 | // 当前订阅 2 | let currentSub: ((newVal?: any, oldVal?: any) => any) | undefined; 3 | 4 | // 订阅 5 | const subscription = new WeakMap< 6 | object, 7 | Map any>> 8 | >(); 9 | 10 | /** 11 | * @description Proxy Map 12 | */ 13 | const proxyMap = new WeakMap(); 14 | 15 | /** 16 | * @description 收集 Ref 依赖 17 | * @param target 18 | * @param key 19 | */ 20 | const trackRef = (target: object) => { 21 | // 当前订阅 22 | if (!currentSub) { 23 | return; 24 | } 25 | // target 订阅列表 26 | let subList = subscription.get(target); 27 | // 不存在订阅列表 28 | if (!subList) { 29 | subList = new Map(); 30 | // 键订阅 31 | const subkeyList = new Set<(newVal: any, oldVal: any) => any>(); 32 | // 添加订阅 33 | subkeyList.add(currentSub); 34 | subList.set('value', subkeyList); 35 | subscription.set(target, subList); 36 | return; 37 | } 38 | // 键订阅 39 | let subkeyList = subList.get('value'); 40 | if (!subkeyList) { 41 | // 键订阅 42 | subkeyList = new Set<(newVal: any, oldVal: any) => any>(); 43 | // 添加订阅 44 | subkeyList.add(currentSub); 45 | subList.set('value', subkeyList); 46 | subscription.set(target, subList); 47 | return; 48 | } 49 | // 添加订阅 50 | subkeyList.add(currentSub); 51 | }; 52 | 53 | /** 54 | * @description 通知 Ref 订阅 55 | * @param terget 56 | * @param key 57 | * @returns 58 | */ 59 | function triggerRef(target: object, newVal: any, oldVal: any) { 60 | // target 订阅列表 61 | const subList = subscription.get(target); 62 | if (!subList) { 63 | return; 64 | } 65 | // 键订阅 66 | let subkeyList = subList.get('value'); 67 | if (!subkeyList) { 68 | return; 69 | } 70 | // 通知订阅 71 | for (const fn of subkeyList) { 72 | if (fn instanceof Function) { 73 | fn(newVal, oldVal); 74 | } 75 | } 76 | } 77 | 78 | /** 79 | * @description 收集依赖 80 | * @param target 81 | * @param key 82 | */ 83 | const track = (target: object, key: string) => { 84 | // 当前订阅 85 | if (!currentSub) { 86 | return; 87 | } 88 | // proxy 89 | const proxyTarget = proxyMap.get(target); 90 | if (!proxyTarget) { 91 | return; 92 | } 93 | // target 订阅列表 94 | let subList = subscription.get(target); 95 | // 不存在订阅列表 96 | if (!subList) { 97 | subList = new Map(); 98 | // 键订阅 99 | const subkeyList = new Set<(newVal: any, oldVal: any) => any>(); 100 | // 添加订阅 101 | subkeyList.add(currentSub); 102 | subList.set(key, subkeyList); 103 | subscription.set(target, subList); 104 | return; 105 | } 106 | // 键订阅 107 | let subkeyList = subList.get(key); 108 | if (!subkeyList) { 109 | // 键订阅 110 | subkeyList = new Set<(newVal: any, oldVal: any) => any>(); 111 | // 添加订阅 112 | subkeyList.add(currentSub); 113 | subList.set(key, subkeyList); 114 | subscription.set(target, subList); 115 | return; 116 | } 117 | // 添加订阅 118 | subkeyList.add(currentSub); 119 | }; 120 | 121 | /** 122 | * @description 通知订阅 123 | * @param terget 124 | * @param key 125 | * @returns 126 | */ 127 | function trigger(target: object, key: string, newVal: any, oldVal: any) { 128 | // proxy 129 | const proxyTarget = proxyMap.get(target); 130 | if (!proxyTarget) { 131 | return; 132 | } 133 | // proxyTarget 订阅列表 134 | const subList = subscription.get(target); 135 | if (!subList) { 136 | return; 137 | } 138 | // 键订阅 139 | let subkeyList = subList.get(key); 140 | if (!subkeyList) { 141 | return; 142 | } 143 | // 通知订阅 144 | for (const fn of subkeyList) { 145 | fn(newVal, oldVal); 146 | } 147 | } 148 | 149 | /** 150 | * @description 只读键 151 | */ 152 | enum ReactiveFlags { 153 | IS_REF = '_isRef', 154 | IS_SHALLOW = '_isShallow', 155 | IS_REACTIVE = '_isReactive', 156 | IS_READONLY = '_isReadonly', 157 | } 158 | 159 | /** 160 | * @description Ref 161 | */ 162 | class Ref { 163 | readonly _isShallow: boolean = false; 164 | readonly _isRef: boolean = true; 165 | _value: T; 166 | value: T; 167 | constructor(val: T, shallow: boolean = false) { 168 | const _this = this; 169 | this._isShallow = shallow; 170 | if (val && typeof val === 'object' && shallow) { 171 | const reactiveVal = reactive(val); 172 | this._value = reactiveVal; 173 | this.value = reactiveVal; 174 | } else { 175 | this._value = val; 176 | this.value = val; 177 | } 178 | // 定义属性 179 | Object.defineProperty(this, 'value', { 180 | get() { 181 | // 收集依赖 182 | trackRef(this); 183 | return _this._value; 184 | }, 185 | set(newVal: any) { 186 | // 旧数据 187 | const oldVal = this._value; 188 | // 数据变化 189 | if (oldVal !== newVal) { 190 | // 设置新数据值 191 | _this._value = newVal; 192 | // 通知依赖 193 | triggerRef(this, newVal, oldVal); 194 | } 195 | }, 196 | }); 197 | } 198 | toJSON() { 199 | return this._value; 200 | } 201 | } 202 | 203 | /** 204 | * @description 脱除 ref 205 | */ 206 | type UnwrapRef = T extends Ref ? P : T; 207 | 208 | /** 209 | * @description 数组脱除 ref 210 | */ 211 | type UnwrapRefArray = T extends Ref[] 212 | ? K[] 213 | : T extends [infer K, ...infer P] 214 | ? P extends Ref[] 215 | ? [UnwrapRef, ...UnwrapRefArray

] 216 | : [UnwrapRef] 217 | : []; 218 | 219 | /** 220 | * @description ref 221 | * @param v 222 | * @returns 223 | */ 224 | const isRef = (v: Ref | unknown): v is Ref => { 225 | return !!(v && v[ReactiveFlags.IS_REF]); 226 | }; 227 | 228 | /** 229 | * @description 浅层 shallow 230 | * @param v 231 | * @returns 232 | */ 233 | const isShallow = (v: unknown) => { 234 | return !!(v && v[ReactiveFlags.IS_SHALLOW]); 235 | }; 236 | 237 | /** 238 | * @description 创建 ref 239 | * @param v 240 | * @returns 241 | */ 242 | const createRef = (rawVal: T, shallow: boolean) => { 243 | return new Ref(rawVal, shallow); 244 | }; 245 | 246 | /** 247 | * @description 解除 ref 248 | * @param val 249 | * @returns 250 | */ 251 | const unref = (val: T) => { 252 | return >(isRef(val) ? val.value : val); 253 | }; 254 | 255 | /** 256 | * @description 顶层 ref 257 | * @param v 258 | * @returns 259 | */ 260 | const ref = (value: T): Ref> => { 261 | return isRef>(value) 262 | ? value 263 | : createRef(>value, true); 264 | }; 265 | 266 | /** 267 | * @description ref 268 | * @param value 269 | * @returns 270 | */ 271 | const shallowRef = (value: T): Ref> => { 272 | return isRef>(value) 273 | ? value 274 | : createRef(>value, false); 275 | }; 276 | 277 | /** 278 | * @description 创建处理 reactive 279 | * @param isReadonly 280 | * @param isShallow 281 | * @returns 282 | */ 283 | const createReactiveHandlers = (isReadonly: boolean, isShallow: boolean) => { 284 | return { 285 | get: createGetters(isReadonly, isShallow), 286 | set: createSetters(isReadonly, isShallow), 287 | }; 288 | }; 289 | 290 | /** 291 | * @description getters 292 | * @param isReadonly 293 | * @param isShallow 294 | * @returns 295 | */ 296 | const createGetters = (isReadonly: boolean, isShallow: boolean) => { 297 | return function get(target, key, receiver) { 298 | if (key === ReactiveFlags.IS_REACTIVE) { 299 | return !isReadonly; 300 | } 301 | if (key === ReactiveFlags.IS_READONLY) { 302 | return isReadonly; 303 | } 304 | if (key === ReactiveFlags.IS_SHALLOW) { 305 | return isShallow; 306 | } 307 | // 结果 308 | const res = Reflect.get(target, key, receiver); 309 | if (!isReadonly) { 310 | // 收集依赖 311 | track(target, key); 312 | } 313 | if (isShallow) { 314 | return res; 315 | } 316 | if (isRef(res)) { 317 | return res.value; 318 | } 319 | if (res && typeof res === 'object') { 320 | if (res instanceof Element) { 321 | return res; 322 | } 323 | return isReadonly ? readonly(res) : reactive(res); 324 | } 325 | return res; 326 | }; 327 | }; 328 | 329 | /** 330 | * @description setters 331 | * @param readonly 332 | * @param shallow 333 | * @returns 334 | */ 335 | const createSetters = (readonly: boolean, shallow: boolean) => { 336 | return function set(target, key, newVal, receiver) { 337 | // 只读 338 | if (readonly) { 339 | return false; 340 | } 341 | // 旧值 342 | const oldVal = target[key]; 343 | if (isReadonly(oldVal) && isRef(oldVal) && !isRef(newVal)) { 344 | return false; 345 | } 346 | if (!shallow) { 347 | if (isRef(oldVal) && !isRef(newVal)) { 348 | oldVal.value = newVal; 349 | return true; 350 | } 351 | } 352 | const res = Reflect.set(target, key, newVal, receiver); 353 | // length 354 | if (Array.isArray(target) && key === 'length') { 355 | // 通知依赖 356 | trigger(target, key, newVal, oldVal); 357 | return res; 358 | } 359 | // 数据变化 360 | if (oldVal !== newVal) { 361 | // 通知依赖 362 | trigger(target, key, newVal, oldVal); 363 | } 364 | return res; 365 | }; 366 | }; 367 | 368 | // 响应式 369 | type Reactive = { 370 | _isReactive: boolean; 371 | _isReadonly: boolean; 372 | } & T; 373 | 374 | /** 375 | * @description reactive object 376 | */ 377 | const createReactiveObj = ( 378 | target: T, 379 | isReadonly: boolean, 380 | shallow: boolean 381 | ) => { 382 | // 存在 Proxy 383 | const existingProxy = proxyMap.get(target); 384 | if (existingProxy) { 385 | return >existingProxy; 386 | } 387 | // 新建 388 | const proxy = new Proxy(target, createReactiveHandlers(isReadonly, shallow)); 389 | proxyMap.set(target, proxy); 390 | return >proxy; 391 | }; 392 | 393 | /** 394 | * @description reactive 395 | * @param val 396 | * @returns 397 | */ 398 | const isReactive = (val: unknown): val is boolean => { 399 | return !!(val && val[ReactiveFlags.IS_REACTIVE]); 400 | }; 401 | 402 | /** 403 | * @description 创建 reactive 404 | * @param target 405 | * @returns 406 | */ 407 | const createReactive = (target: T): Reactive => { 408 | return createReactiveObj(target, false, false); 409 | }; 410 | 411 | /** 412 | * @description 顶层 reactive 413 | * @param target 414 | * @returns 415 | */ 416 | const shallowReactive = (target: T) => { 417 | return createReactiveObj(target, false, true); 418 | }; 419 | 420 | /** 421 | * @description reactive 422 | * @param val 423 | * @returns 424 | */ 425 | const isReadonly = (val: unknown): val is object => { 426 | return !!(val && val[ReactiveFlags.IS_READONLY]); 427 | }; 428 | 429 | /** 430 | * @description 创建 readonly 431 | * @param target 432 | * @returns 433 | */ 434 | const createReadonly = (target: T): T => { 435 | return createReactiveObj(target, true, false); 436 | }; 437 | 438 | /** 439 | * @description 顶层 readonly 440 | * @param target 441 | * @returns 442 | */ 443 | const shallowReadonly = (target: T) => { 444 | return createReactiveObj(target, true, true); 445 | }; 446 | 447 | /** 448 | * @description proxy 449 | * @param val 450 | * @returns 451 | */ 452 | const isProxy = (val: unknown): val is Reactive => { 453 | return isReactive(val) || isReadonly(val); 454 | }; 455 | 456 | /** 457 | * @description reactive 458 | * @param target 459 | * @returns 460 | */ 461 | const reactive = (target: T) => { 462 | return createReactive(target); 463 | }; 464 | 465 | /** 466 | * @description readonly 467 | * @param target 468 | * @returns 469 | */ 470 | const readonly = (target: T) => { 471 | return createReadonly(target); 472 | }; 473 | 474 | /** 475 | * @description 监听数据变化 476 | * @param source 477 | * @param callback 478 | */ 479 | const watch = any)>( 480 | source: T, 481 | callback: ( 482 | newValue: T extends Ref[] 483 | ? UnwrapRefArray 484 | : T extends () => infer P 485 | ? P extends Ref[] 486 | ? UnwrapRefArray

487 | : P 488 | : UnwrapRef, 489 | oldValue: T extends Ref[] 490 | ? UnwrapRefArray 491 | : T extends () => infer P 492 | ? P extends Ref[] 493 | ? UnwrapRefArray

494 | : P 495 | : UnwrapRef 496 | ) => void, 497 | immediate: boolean = false 498 | ) => { 499 | // 立刻执行 500 | immediate && callback(unref(source), unref(source)); 501 | // array 502 | if (Array.isArray(source) && source.every((s) => isRef(s))) { 503 | for (const i in source) { 504 | // Proxy 505 | if (isProxy(source[i])) { 506 | watch(source[i], () => { 507 | const res = source.map((s) => unref(s)); 508 | callback(res, res); 509 | }); 510 | } 511 | } 512 | watch<() => any>(() => source.map((s) => unref(s)), callback); 513 | return; 514 | } 515 | // function 516 | if (source instanceof Function) { 517 | watch(watchEffectRef(source), (n, o) => { 518 | callback(unref(n), unref(o)); 519 | }); 520 | return; 521 | } 522 | // Proxy 523 | if (isProxy(source)) { 524 | for (const key in source) { 525 | currentSub = () => { 526 | callback(source, source); 527 | }; 528 | // sub source 529 | const subSource = source[key]; 530 | currentSub = undefined; 531 | watch(subSource, () => { 532 | callback(source, source); 533 | }); 534 | } 535 | return; 536 | } 537 | // Ref 538 | if (isRef(source)) { 539 | // Ref.value Proxy 540 | if (isProxy(source.value)) { 541 | watch(source.value, () => { 542 | callback(unref(source), unref(source)); 543 | }); 544 | } 545 | currentSub = callback; 546 | // 收集依赖 547 | trackRef(source); 548 | currentSub = undefined; 549 | return; 550 | } 551 | }; 552 | 553 | /** 554 | * @description 监听数据变化影响 555 | * @param callback 556 | * @returns 557 | */ 558 | const watchEffect = (callback: () => any) => { 559 | currentSub = callback; 560 | // 收集依赖 561 | callback(); 562 | currentSub = undefined; 563 | }; 564 | 565 | /** 566 | * @description 监听影响 ref 567 | * @param refVal 568 | * @param callback 569 | * @returns 570 | */ 571 | const watchRef = any), P>( 572 | source: T, 573 | callback: () => P 574 | ) => { 575 | // 收集依赖 576 | const effectRes = shallowRef

(callback()); 577 | // 监听 578 | watch(source, () => (effectRes.value = unref(callback()))); 579 | return effectRes; 580 | }; 581 | 582 | /** 583 | * @description 监听影响 ref 584 | * @param refVal 585 | * @param callback 586 | * @returns 587 | */ 588 | const watchEffectRef = (callback: () => T) => { 589 | // 收集依赖 590 | const effectRes = shallowRef(undefined); 591 | // 监听 592 | watchEffect(() => (effectRes.value = unref(callback()))); 593 | return >>effectRes; 594 | }; 595 | 596 | export { 597 | Ref, 598 | ref, 599 | shallowRef, 600 | unref, 601 | isRef, 602 | watch, 603 | watchRef, 604 | watchEffectRef, 605 | watchEffect, 606 | reactive, 607 | shallowReactive, 608 | isReactive, 609 | readonly, 610 | shallowReadonly, 611 | isReadonly, 612 | isProxy, 613 | isShallow, 614 | Reactive, 615 | }; 616 | -------------------------------------------------------------------------------- /src/utils/log.ts: -------------------------------------------------------------------------------- 1 | import { formatDateTime } from './time'; 2 | 3 | /** 4 | * @description 打印日志 5 | * @param text 6 | */ 7 | function log(...text: any[]) { 8 | printColor('dodgerblue', ...text); 9 | } 10 | 11 | /** 12 | * @description 打印错误 13 | * @param text 14 | */ 15 | function error(...text: any[]) { 16 | printColor('red', ...text); 17 | } 18 | 19 | /** 20 | * @description 打印信息 21 | * @param text 22 | */ 23 | function info(...text: any[]) { 24 | printColor('yellow', ...text); 25 | } 26 | 27 | /** 28 | * @description 打印颜色 29 | * @param text 30 | * @param color 31 | */ 32 | function printColor(color: string, ...text: any[]) { 33 | const textFormatted = text 34 | .map((t) => (typeof t === 'object' ? JSON.stringify(t) : String(t))) 35 | .join(' '); 36 | console.log( 37 | `%c[${formatDateTime()}] %c${textFormatted}`, 38 | '', 39 | `color: ${color}` 40 | ); 41 | } 42 | 43 | export { log, error, info }; 44 | -------------------------------------------------------------------------------- /src/utils/push.ts: -------------------------------------------------------------------------------- 1 | import { pushPlus } from '../api/push'; 2 | import { formatDateTime } from './time'; 3 | /** 4 | * @description 消息模板类型 5 | */ 6 | type TemplateType = 'html' | 'txt' | 'json' | 'markdown' | 'cloudMonitor'; 7 | 8 | /** 9 | * @description 推送选项 10 | */ 11 | type PushOptions = { 12 | title: string; 13 | content: string; 14 | template: TemplateType; 15 | toToken?: string; 16 | fromToken: string; 17 | }; 18 | 19 | /** 20 | * @description 模态框 21 | */ 22 | type ModalOptions = { 23 | title: string; 24 | subTitle?: string; 25 | content: string | string[]; 26 | to?: string; 27 | from?: string; 28 | type: ModalType; 29 | }; 30 | 31 | /** 32 | * @description 类型 33 | */ 34 | type ModalType = 'info' | 'warn' | 'fail' | 'success'; 35 | 36 | /** 37 | * @description html进度条 38 | * @param title 39 | * @param percent 40 | * @returns 41 | */ 42 | function getProgressHTML(title: string, current: number, total: number) { 43 | // html 44 | const progressHTML = `

52 | ${title} 53 | ${getHighlightHTML(`${current}`)} / ${total} 54 |
55 |
64 |
72 |
`; 73 | return progressHTML; 74 | } 75 | /** 76 | * @description html高亮文本 77 | * @param text 78 | * @returns 79 | */ 80 | function getHighlightHTML(text: string | number) { 81 | // html 82 | const highlightHTML = `${text}`; 83 | return highlightHTML; 84 | } 85 | /** 86 | * @description 二维码 87 | * @param src 88 | */ 89 | function getImgHTML(src: string) { 90 | // 图片 91 | return ` 92 |
93 |
103 | 104 |
105 |
106 | `; 107 | } 108 | /** 109 | * @description 创建模态框 110 | * @param options 选项 111 | * @returns 112 | */ 113 | function createModal(options: ModalOptions) { 114 | // 配置 115 | const { 116 | title, 117 | subTitle = '', 118 | to = '用户', 119 | content, 120 | type, 121 | from = 'tech-study.js', 122 | } = options; 123 | // 内容文本 124 | let contentText = ''; 125 | if (Array.isArray(content)) { 126 | contentText = content.map((ct) => `
${ct}
`).join(''); 127 | } else { 128 | contentText = content; 129 | } 130 | // 日期 131 | const dateTime = formatDateTime(); 132 | // 类型html 133 | let typeHTML = ''; 134 | if (type && type.length) { 135 | if (type === 'info') { 136 | typeHTML = ` 137 | `; 147 | } 148 | if (type === 'warn') { 149 | typeHTML = ` 150 | 160 | `; 161 | } 162 | if (type === 'success') { 163 | typeHTML = ` 164 | 174 | `; 175 | } 176 | if (type === 'fail') { 177 | typeHTML = ` 178 | 188 | `; 189 | } 190 | } 191 | // 类型 192 | const typeWrap = ` 193 | 201 | ${typeHTML} 202 | 203 | `; 204 | // 基础html 205 | const baseHTML = ` 206 |
214 |
223 |
231 |
232 | ${typeWrap} 233 | ${title} 234 |
235 |
${subTitle}
236 |
237 |
238 | 239 |
240 |
241 | ${getHighlightHTML(to)}, 你好! 242 |
243 |
${contentText}
244 |
245 |
255 |
${dateTime}
256 |
257 | 来自 258 | ${from} 259 |
260 |
261 |
262 |
263 | `; 264 | return baseHTML; 265 | } 266 | 267 | /** 268 | * @description 推送消息 269 | */ 270 | async function pushMessage(options: PushOptions) { 271 | // 选项 272 | const { title, content, template, fromToken, toToken } = options; 273 | // 推送 274 | const res = await pushPlus(fromToken, title, content, template, toToken); 275 | return res; 276 | } 277 | /** 278 | * @description 推送模态框 279 | */ 280 | async function pushModal( 281 | options: ModalOptions, 282 | fromToken: string, 283 | toToken?: string 284 | ) { 285 | // html 286 | const html = createModal(options); 287 | // 推送 288 | const res = await pushMessage({ 289 | title: '消息提示', 290 | content: html, 291 | fromToken, 292 | toToken, 293 | template: 'html', 294 | }); 295 | if (res && res.code === 200) { 296 | return res; 297 | } 298 | return; 299 | } 300 | 301 | export { 302 | createModal, 303 | getHighlightHTML, 304 | getImgHTML, 305 | getProgressHTML, 306 | pushModal, 307 | }; 308 | -------------------------------------------------------------------------------- /src/utils/random.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 点 3 | */ 4 | type Point = { x: number; y: number }; 5 | 6 | /** 7 | * @description 范围 8 | */ 9 | type Bounds = { x: number; y: number; width: number; height: number }; 10 | /** 11 | * @description 创建随机点 12 | * @param bounds 范围 13 | * @returns 14 | */ 15 | function createRandomPoint(bounds: Bounds): Point { 16 | // 范围 17 | const { x, y, width, height } = bounds; 18 | // 横坐标 19 | const randX = x + Math.random() * width * 0.5 + width * 0.25; 20 | // 纵坐标 21 | const randY = y + Math.random() * height * 0.5 + height * 0.25; 22 | return { 23 | x: randX, 24 | y: randY, 25 | }; 26 | } 27 | 28 | /** 29 | * @description 生成随机路径 30 | * @param start 31 | * @param end 32 | * @param steps 33 | * @returns 34 | */ 35 | function createRandomPath(start: Point, end: Point, steps: number) { 36 | // 最小水平增量 37 | const minDeltaX = (end.x - start.x) / steps; 38 | // 最大垂直增量 39 | const maxDeltaY = (end.y - start.y) / steps; 40 | 41 | const path: Point[] = []; 42 | // 开始节点 43 | path.push(start); 44 | // 插入点 45 | for (let i = 0; i < steps; i++) { 46 | // 横坐标 47 | const x = path[i].x + Math.random() * 5 + minDeltaX; 48 | // 纵坐标 49 | const y = 50 | path[i].y + 51 | Math.random() * 5 * Math.pow(-1, ~~(Math.random() * 2 + 1)) + 52 | maxDeltaY; 53 | path.push({ 54 | x, 55 | y, 56 | }); 57 | } 58 | return path; 59 | } 60 | /** 61 | * @description 随机数字 62 | * @returns 63 | */ 64 | function generateNumAsChar(): string { 65 | return (~~(Math.random() * 10)).toString(); 66 | } 67 | /** 68 | * @description 随机大写字母 69 | * @returns 70 | */ 71 | function generateUpperAsChar(): string { 72 | return String.fromCharCode(~~(Math.random() * 26) + 65); 73 | } 74 | /** 75 | * @description 随机小写字母 76 | * @returns 77 | */ 78 | function generateLowerAsChar(): string { 79 | return String.fromCharCode(~~(Math.random() * 26) + 97); 80 | } 81 | /** 82 | * @description 随机混合字符 83 | * @param length 84 | * @returns 85 | */ 86 | function generateMix(length: number = 6): string { 87 | // 随机字符串 88 | const randomText: string[] = []; 89 | // 生成器 90 | const typeGenerator: (() => string)[] = [ 91 | generateNumAsChar, 92 | generateUpperAsChar, 93 | generateLowerAsChar, 94 | ]; 95 | if (length) { 96 | for (let i = 0; i < length; i++) { 97 | // 随机位置 98 | const randomIndex = ~~(Math.random() * typeGenerator.length); 99 | randomText.push(typeGenerator[randomIndex]()); 100 | } 101 | } 102 | return randomText.join(''); 103 | } 104 | 105 | export { createRandomPoint, createRandomPath, generateMix }; 106 | -------------------------------------------------------------------------------- /src/utils/time.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 格式化日期时间数字 3 | * @param num 4 | * @returns 5 | */ 6 | function formatDateNum(num: number) { 7 | return num < 10 ? `0${num}` : `${num}`; 8 | } 9 | 10 | /** 11 | * @description 格式化日期时间 12 | * @param time 13 | * @returns 14 | * @example 15 | * formatDateTime() -> "2022-09-01 08:00:00" 16 | * formatDateTime(new Date()) -> "2022-09-01 08:00:00" 17 | * formatDateTime(Date.now()) -> "2022-09-01 08:00:00" 18 | */ 19 | function formatDateTime(time: Date | string | number = Date.now()) { 20 | const date = new Date(time); 21 | const s = date.getSeconds(); 22 | const min = date.getMinutes(); 23 | const h = date.getHours(); 24 | const d = date.getDate(); 25 | const m = date.getMonth() + 1; 26 | const y = date.getFullYear(); 27 | // 日期 28 | const dateText = [y, m, d].map(formatDateNum).join('-'); 29 | // 时间 30 | const timeText = [h, min, s].map(formatDateNum).join(':'); 31 | // 日期时间 32 | const dateTimeText = `${dateText} ${timeText}`; 33 | return dateTimeText; 34 | } 35 | 36 | /** 37 | * @description 格式化时间 38 | * @param time 39 | * @returns 40 | * @example 41 | * formatTime() -> "08:00:00" 42 | * formatTime(new Date()) -> "08:00:00" 43 | * formatTime(Date.now()) -> "08:00:00" 44 | */ 45 | const formatTime = (time: Date | string | number = Date.now()) => { 46 | const date = new Date(time); 47 | const s = date.getSeconds(); 48 | const min = date.getMinutes(); 49 | const h = date.getHours(); 50 | // 时间 51 | const timeText = [h, min, s].map(formatDateNum).join(':'); 52 | return timeText; 53 | }; 54 | 55 | /** 56 | * @description 时间已过 57 | * @param hour 58 | * @param minute 59 | * @returns 60 | */ 61 | function isLate({ hour, minute }: { hour: number; minute: number }) { 62 | const date = new Date(); 63 | const h = date.getHours(); 64 | const min = date.getMinutes(); 65 | return h > hour || (h === hour && min >= minute); 66 | } 67 | 68 | /** 69 | * @description 时间已过 70 | * @param hour 71 | * @param minute 72 | * @returns 73 | */ 74 | function isNow({ hour, minute }: { hour: number; minute: number }) { 75 | const date = new Date(); 76 | const h = date.getHours(); 77 | const min = date.getMinutes(); 78 | const s = date.getSeconds(); 79 | return h === hour && min === minute && s === 0; 80 | } 81 | 82 | export { formatDateNum, formatDateTime, isLate, isNow }; 83 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { log } from './log'; 2 | 3 | /* 工具函数 */ 4 | 5 | /** 6 | * @description 设置cookie 7 | * @param name 8 | * @param value 9 | * @param expires 10 | */ 11 | function setCookie( 12 | name: string, 13 | value: string, 14 | expires: number, 15 | domain: string 16 | ) { 17 | // 当前日期 18 | const date = new Date(); 19 | // 过期日期 20 | date.setTime(date.getTime() + expires); 21 | // 设置cookie 22 | document.cookie = `${name}=${value};expires=${date.toUTCString()};path=/;domain=${domain}`; 23 | } 24 | 25 | /** 26 | * @description 获取cookie 27 | * @param name 28 | * @returns 29 | */ 30 | function getCookie(name: string) { 31 | // 获取当前所有cookie 32 | const strCookies = document.cookie; 33 | // 截取变成cookie数组 34 | const cookieText = strCookies.split(';'); 35 | // 循环每个cookie 36 | for (const i in cookieText) { 37 | // 将cookie截取成两部分 38 | const item = cookieText[i].split('='); 39 | // 判断cookie的name 是否相等 40 | if (item[0].trim() === name) { 41 | return item[1].trim(); 42 | } 43 | } 44 | return null; 45 | } 46 | 47 | /** 48 | * @description 删除cookie 49 | * @param name 50 | */ 51 | function delCookie(name: string, domain: string) { 52 | // 存在cookie 53 | const value = getCookie(name); 54 | if (value !== null) { 55 | setCookie(name, '', -1, domain); 56 | } 57 | } 58 | 59 | /** 60 | * @description 防抖 61 | * @param callback 62 | * @param delay 63 | * @returns 64 | */ 65 | function debounce any>(callback: T, delay: number) { 66 | let timer = -1; 67 | return function (this: any, ...args: Parameters) { 68 | if (timer !== -1) { 69 | clearTimeout(timer); 70 | } 71 | timer = setTimeout(() => { 72 | callback.apply(this, args); 73 | }, delay); 74 | }; 75 | } 76 | 77 | /** 78 | * @description 判断是否为移动端 79 | * @returns 80 | */ 81 | function hasMobile() { 82 | let isMobile = false; 83 | if ( 84 | navigator.userAgent.match( 85 | /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i 86 | ) 87 | ) { 88 | log('移动端'); 89 | isMobile = true; 90 | } 91 | if (document.body.clientWidth < 800) { 92 | log('小尺寸设备端'); 93 | isMobile = true; 94 | } 95 | return isMobile; 96 | } 97 | 98 | /** 99 | * @description 等待时间 100 | * @param time 101 | * @returns 102 | */ 103 | function sleep(time: number) { 104 | // 延时 105 | let timeDelay = Number(time); 106 | if (!Number.isInteger(timeDelay)) { 107 | timeDelay = 1000; 108 | } 109 | timeDelay += Math.random() * 500 - 250; 110 | return new Promise((resolve) => { 111 | setTimeout(() => { 112 | resolve(undefined); 113 | }, timeDelay); 114 | }); 115 | } 116 | 117 | /** 118 | * @description 暂停学习锁 119 | */ 120 | function studyPauseLock(callback?: (msg: boolean) => void) { 121 | return new Promise((resolve) => { 122 | // 暂停 123 | const pauseStudy = GM_getValue('pauseStudy') || false; 124 | if (pauseStudy) { 125 | const doing = setInterval(() => { 126 | // 暂停 127 | const pauseStudy = GM_getValue('pauseStudy') || false; 128 | if (!pauseStudy) { 129 | // 停止定时器 130 | clearInterval(doing); 131 | log('学习等待结束!'); 132 | if (callback && callback instanceof Function) { 133 | callback(true); 134 | } 135 | resolve(true); 136 | return; 137 | } 138 | if (callback && callback instanceof Function) { 139 | callback(false); 140 | } 141 | log('学习等待...'); 142 | }, 500); 143 | return; 144 | } 145 | resolve(true); 146 | }); 147 | } 148 | 149 | /** 150 | * @description 加载 151 | * @param match 152 | * @param callback 153 | */ 154 | function load( 155 | match: string | RegExp | ((href: string) => any) | boolean, 156 | callback: () => void 157 | ) { 158 | // 链接 159 | const { href } = window.location; 160 | window.addEventListener('load', () => { 161 | // 函数 162 | if (match instanceof Function) { 163 | match(href) && callback(); 164 | return; 165 | } 166 | // 布尔 167 | if (typeof match === 'boolean') { 168 | match && callback(); 169 | return; 170 | } 171 | // 字符正则 172 | if (href.match(match)) { 173 | callback(); 174 | return; 175 | } 176 | }); 177 | } 178 | 179 | export { 180 | debounce, 181 | delCookie, 182 | getCookie, 183 | hasMobile, 184 | load, 185 | setCookie, 186 | sleep, 187 | studyPauseLock, 188 | }; 189 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": [ 16 | "DOM", 17 | "ESNext" 18 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 19 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 20 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 21 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 22 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 23 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 24 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 25 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 26 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 27 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 28 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 29 | 30 | /* Modules */ 31 | "module": "CommonJS" /* Specify what module code is generated. */, 32 | // "rootDir": "./", /* Specify the root folder within your source files. */ 33 | "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, 34 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 35 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 36 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 37 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 38 | // "types": [] /* Specify type package names to be included without being referenced in a source file. */, 39 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 40 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 41 | // "resolveJsonModule": true, /* Enable importing .json files. */ 42 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 43 | 44 | /* JavaScript Support */ 45 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 46 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 47 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 48 | 49 | /* Emit */ 50 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 51 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 52 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 53 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 54 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 55 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 56 | // "removeComments": true, /* Disable emitting comments. */ 57 | // "noEmit": true, /* Disable emitting files from a compilation. */ 58 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 59 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 60 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 61 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 63 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 64 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 65 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 66 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 67 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 68 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 69 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 70 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 71 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 72 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 73 | 74 | /* Interop Constraints */ 75 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 76 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 77 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 78 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 79 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 80 | 81 | /* Type Checking */ 82 | "strict": true /* Enable all strict type-checking options. */, 83 | "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied 'any' type. */, 84 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 85 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 86 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 87 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 88 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 89 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 90 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 91 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 92 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 93 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 94 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 95 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 96 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 97 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 98 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 99 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 100 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 101 | 102 | /* Completeness */ 103 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 104 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 105 | }, 106 | "include": ["bin/*.ts", "src/**/*", "types/**/*.d.ts"] 107 | } 108 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | module '*?raw' { 3 | const text: string; 4 | export default text; 5 | } 6 | function md5(value: string): string; 7 | } 8 | 9 | export {}; 10 | --------------------------------------------------------------------------------