Repository: Xu22Web/tech-study-js Branch: master Commit: a1da2057696a Files: 56 Total size: 861.2 KB Directory structure: gitextract_aws1pt39/ ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── bin/ │ └── index.ts ├── cache.json ├── package.json ├── 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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms ================================================ FILE: .gitignore ================================================ node_modules ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 Xu22Web Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # tech-study-js ### 原仓库 > https://github.com/TechXueXi/techxuexi-js 基于原作者的原仓库,自行改进和完善 ### 描述 Description - 灵活且貌似轻量的 `学习强国` 油猴脚本。 - 与此同时,提供更加便捷的版本选择 - [Node.js 版](https://github.com/Xu22Web/tech-study-node 'Node.js 版') - [Docker 版](https://github.com/Xu22Web/tech-study-docker 'Docker 版') ### 交流群 Telegram Group - 链接: [tech-study 互动群](https://t.me/+IJ_YzNc-Iew0MGRl) - 二维码: Telegram邀请二维码 注:介于脚本国内敏感,暂时不提供其他交流互动方式。 ### 用法 Usage 1. 装个浏览器插件`Tampermonkey` 1. Microsoft Edge: [插件安装](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd?hl=zh-CN) 2. Google Chrome: [插件安装](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=zh) 2. 点击插件里添加按钮,去掉编辑框里原来的代码,复制 [tech-study.js](https://raw.githubusercontent.com/Xu22Web/tech-study-js/master/tech-study.js) 脚本,粘贴进编辑框保存。 3. 开启这个脚本,然后进入网页强国 https://www.xuexi.cn 。 ### 优化 Promote 1. 优化整体交互设计,新增一体式扫码登录 2. 新增用户信息显示,包括昵称、头像、总分以及当天分数 3. 新增任务进度以及任务分数详情显示,任务情况清晰明了 4. 优化答题逻辑,新增滑动验证,远离验证烦恼 5. 新增同屏任务以及静默运行,仅需一个页面即可静默运行任务 6. 兼容桌面端以及移动端,手机电脑均可运行(设备均需支持油猴脚本,此外,移动端需要开启同屏任务) 7. 新增定时任务以及远程推送,定时刷新页面,远程微信推送登录二维码 ### 使用流程 Process 1. 用户`登录` 登录 2. 点击 `开始学习`,等待完成任务运行 - `桌面端`运行 桌面端运行 - `移动端`运行 移动端运行 3. `完成学习`任务 完成学习 ### 更新与维护 Update and Maintenance 1. 修复有声视频播放后,页面倒计时不继续的问题(注:不同标签页的有声视频需要用户交互才能播放,手动播放后会展示倒计时) 2. 与此同时,提供更加便捷的版本选择 - [Node.js 版](https://github.com/Xu22Web/tech-study-node 'Node.js 版') - [Docker 版](https://github.com/Xu22Web/tech-study-docker 'Docker 版') ### 关于开发 Development - 脚本配置 1. 版本配置 `src/config/version.ts` 2. 脚本配置 `src/config/script.ts` 3. 编译配置 `src/config/compile.ts` 4. 接口配置 `src/config/api.ts` 5. 链接配置 `src/config/url.ts` 6. 任务配置 `src/config/task.ts` - CSS 文件 `src/css/index.css` - 根据功能特性(i) ```js // 将文件'./css/index.css'文本内容赋值到'css' import css from './css/index.css?raw'; ``` - 根据 Tampermonkey API 函数 ```js // 嵌入样式 GM_addStyle(css); ``` - 脚本内容 `src/tech-study.ts` - 编译 ``` # 编译生成 'tech-study.js' pnpm build ``` 即 ``` ✔ 完成编译: index.ts -> index.js ✔ 已生成 用户脚本配置 注释! ✔ 完成编译: ./config/api.ts -> api.js ✔ 完成编译: ./config/url.ts -> url.js ✔ 完成编译: ./config/task.ts -> task.js ... ... ✔ 导出整合的脚本文件: tech-study.js ``` - 功能特性(基于`TypeScript Compiler API`) 1. 包含`?raw`结尾的`import`语句 ``` import var from 'file?raw'; ``` 1. 文件`file`文本内容赋值到`var` 2. 此类型`import`语句不会被编译到结果 2. 普通的`import`语句 ``` import { funName } from 'file'; ``` 1. 文件`file`文本插入到主文件一起导出,相当于合并多个`*.ts`文件导出为一个`*.js`文件 2. 此类型`import`语句不会被编译到结果 - 类似组合式接口(类似 `Composition API`) 模拟 `ref`,`watch`,`watchEffect` 等 API。 ================================================ FILE: bin/index.ts ================================================ import chalk from 'chalk'; import fs from 'fs'; import ora from 'ora'; import path from 'path'; import rollup from 'rollup'; import ts from 'typescript'; import COMPILE_CONFIG from '../src/config/compile'; import SCRIPT_CONFIG from '../src/config/script'; // 根目录 文件名 const { input, output, rollupConfig, compress } = COMPILE_CONFIG; // 输入文件路径 const inputFilePath = input.file; // 输入目录名 const inputDirPath = path.dirname(input.file); // 输出文件路径 const outputFilePath = output.file; // 输出目录名 const outputDirPath = path.dirname(output.file); // 导入路径 const importFilePaths: string[] = []; // 缓存 const cache: { [key: string]: { timeStamp: string; data: string; from: string; to: string }; } = JSON.parse(fs.readFileSync('cache.json', { encoding: 'utf-8' })) || {}; /** * @description 编译 * @param filePath * @returns */ const handleCompile = async ( filePath: string ): Promise< | { data: string; compileName: string; originName: string; originPath: string; originRelativePath: string; } | undefined > => { // 全路径 const fullFilePath = path.resolve(filePath); // 文件名 const fileName = getFileName(filePath); // 创建配置 const options = createOptions(fullFilePath); // 创建项目 const program = ts.createIncrementalProgram(options); // 根据项目配置获取源文件 const sourceFile = program.getSourceFile(fullFilePath); // 自定义处理流程 const customTransformers: ts.CustomTransformers = { before: [transformerFactory], }; return new Promise((resolve) => { // 编译生成 program.emit( sourceFile, (name, text) => { // 编译后的文件数据 const data = text.replace(/export (\{.*\}|default .*);/g, ''); // 编译后的文件名 const compileName = getFileName(name); resolve({ data, compileName, originName: fileName, originPath: fullFilePath, originRelativePath: filePath, }); }, undefined, false, customTransformers ); }); }; /** * @description 解析模块 * @param filePath * @returns */ const resoveModule = async ( rawModulePath: string ): Promise<{ modulePath: string; time: string | undefined; stats: boolean; }> => { // 文件路径 const modulePath = path.join(rawModulePath); // 后缀 const ext = path.extname(modulePath); // 文件名 const fileName = getFileName(modulePath); // 路径存在状态 const { stats, time } = handleFileStatus(modulePath); // 文件存在 if (stats) { return { modulePath, time, stats }; } // 存在扩展 if (!ext.length) { // 加后缀 const res = await resoveModule(`${modulePath}.ts`); if (res.stats) { return res; } if (fileName !== 'index.ts') { const res = await resoveModule(`${modulePath}/index.ts`); if (res.stats) { return res; } } } return { modulePath, time, stats }; }; /** * @description 获取文件名 * @param filePath * @returns */ const getFileName = (filePath) => { return filePath.substring(filePath.lastIndexOf('/') + 1); }; /** * @description 文件存在 * @param filePath * @returns */ const handleFileStatus = (filePath: string) => { // 路径存在状态 const exists = fs.existsSync(filePath); // 路径存在 if (exists) { // 文件信息 const fileInfo = fs.statSync(filePath); // 是文件 if (fileInfo.isFile()) { const { mtime } = fileInfo; return { stats: true, time: mtime.toJSON() }; } } return { stats: false }; }; /** * @description 生成配置 * @param filePath * @returns */ const createOptions = (filePath: string): ts.CreateProgramOptions => { const { target, module } = COMPILE_CONFIG; // 项目配置 const programOptions = { rootNames: [filePath], options: { target, module, outputDirPath, }, }; return programOptions; }; /** * @description 创建用户脚本注释配置 * @returns */ const createConfigComment = () => { // 脚本数据 const data: string[] = []; data.push('// ==UserScript=='); for (const key in SCRIPT_CONFIG) { if (typeof SCRIPT_CONFIG[key] === 'string') { data.push(`// @${key} ${SCRIPT_CONFIG[key]}`); } if (Array.isArray(SCRIPT_CONFIG[key])) { for (const i in SCRIPT_CONFIG[key]) { data.push(`// @${key} ${SCRIPT_CONFIG[key][i]}`); } } } data.push('// ==/UserScript=='); return data.join('\r\n'); }; // 处理流程工厂 const transformerFactory: ts.TransformerFactory = (context) => { return (node) => { // 访问 const visitor: ts.Visitor = (rootNode) => { // 节点 const node = ts.visitEachChild(rootNode, visitor, context); // 是导入声明 if (ts.isImportDeclaration(node)) { // 获取变量名 const identifierText = node.importClause?.getText(); // 获取导入模块名 const moduleText = node.moduleSpecifier.getText(); // 检查是否满足路径 const rawSpecificPath = moduleText.match( /(?<=(["'`]))(?:\.{0,2}(?:\/|(?:\\{1,2}))[-_.a-zA-Z]*)+(?=\?raw\1)/ ); if (identifierText && rawSpecificPath) { // 提取模块相对路径 const [relativefilePath] = rawSpecificPath; // 获取实际路径 const filePath = path.resolve(inputDirPath, relativefilePath); // 获取文本信息 const content = fs .readFileSync(filePath, { encoding: 'utf8', }) .replace(/\n|\\n/g, ''); // 创建变量标识符 const name = ts.factory.createIdentifier(identifierText); // 创建字符串 const value = ts.factory.createStringLiteral(content, true); // 创建变量声明 const declaration = ts.factory.createVariableDeclaration( name, ts.factory.createToken(ts.SyntaxKind.ExclamationToken), ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), value ); // 创建声明列表 指定为 const const declaratioList = ts.factory.createVariableDeclarationList( [declaration], ts.NodeFlags.Const ); // 创建变量声明 return ts.factory.createVariableStatement(undefined, declaratioList); } // 检查是否满足路径 const resPath = moduleText.match( /(?<=(["'`]))(?:\.{0,2}(?:\/|(?:\\{1,2}))[-_.a-zA-Z]*)+(?=\1)/ ); // 检查是否满足路径 if (resPath) { // 相对路径 const relativefilePath = resPath[0]; importFilePaths.push(relativefilePath); return ts.factory.createNotEmittedStatement(node); } return node; } // 导出声明 if (ts.isExportDeclaration(node)) { return ts.factory.createNotEmittedStatement(node); } return node; }; // 调用 visitor return ts.visitNode(node, visitor); }; }; // 主函数 const main = async () => { // 开始编译 const progress = ora('准备编译生成脚本文件...'); // 数据 const fullData: string[] = []; // 注释 progress.start(`正在生成 ${chalk.blueBright('用户脚本配置')} 注释...`); // 用户脚本注释配置 const config = createConfigComment(); fullData.push(config); // 注释 progress.succeed(`已生成 ${chalk.blueBright('用户脚本配置')} 注释!`); // 解析模块 const { modulePath } = await resoveModule(inputFilePath); progress.start(`正在编译... ${chalk.blueBright(modulePath)}`); // 编译 const res = await handleCompile(modulePath); if (res) { // 编译信息 const { compileName, data } = res; // 脚本内容 const content: string[] = []; content.push(data); progress.succeed( `完成编译: ${chalk.blueBright(modulePath)} -> ${chalk.blueBright( compileName )}` ); // 编译相对路径 const compileRelativePath = path.join(inputDirPath, compileName); // 源文件名 编译文件名 数据 for (const i in importFilePaths) { // 相对路径 const relativefilePath = importFilePaths[i]; // 文件路径 const filePath = path.join(inputDirPath, relativefilePath); // 解析模块 const { time, modulePath, stats } = await resoveModule(filePath); progress.start(`正在编译... ${chalk.blueBright(modulePath)}`); // 存在模块 if (stats) { // 缓存 if (cache[modulePath]) { const { timeStamp, from, to } = cache[modulePath]; if (time === timeStamp) { // 缓存取数据 content.push(cache[modulePath].data); progress.succeed( `缓存编译: ${chalk.blueBright(from)} -> ${chalk.blueBright(to)}` ); continue; } } // 编译 const res = await handleCompile(modulePath); if (res) { // 编译信息 const { originRelativePath, compileName, data } = res; content.push(data); // 缓存 cache[modulePath] = { timeStamp: time, data, from: originRelativePath, to: compileName, }; progress.succeed( `直接编译: ${chalk.blueBright( originRelativePath )} -> ${chalk.blueBright(compileName)}` ); continue; } } progress.fail(`编译失败: ${chalk.red(filePath)}, 请检查导入文件路径!`); break; } progress.start(`正在生成js文件...`); // 生成js文件 fs.writeFileSync(compileRelativePath, content.join('\r\n')); progress.succeed(`生成js文件成功!`); if (compress) { progress.start(`正在压缩js文件...`); // rollup 压缩 const bundle = await rollup.rollup(rollupConfig.inputOptions); const { output } = await bundle.write(rollupConfig.outputOptions); fullData.push(output[0].code); progress.succeed(`压缩js文件成功!`); } else { fullData.push(content.join('\r\n').replace(/(\r\n){2,}/g, '\r\n')); } progress.start( `正在导出整合的脚本文件... ${chalk.blueBright(outputFilePath)}` ); // 导出文件 fs.writeFileSync('cache.json', JSON.stringify(cache)); // 导出文件 fs.writeFileSync(outputFilePath, fullData.join('\r\n')); progress.succeed(`导出整合的脚本文件: ${chalk.blueBright(outputFilePath)}`); return; } progress.fail(`编译失败,请检查文件路径!`); }; main(); ================================================ FILE: cache.json ================================================ {"src\\api\\answer.ts":{"timeStamp":"2023-03-07T02:25:45.829Z","data":"/* 答案 API */\r\n/**\r\n * @description 获取答案\r\n */\r\nasync function getAnswer(question) {\r\n // 数据\r\n const data = {\r\n txt_name: md5(question),\r\n password: '',\r\n };\r\n try {\r\n const params = new URLSearchParams(data);\r\n // 请求\r\n const res = await fetch(API_CONFIG.answerSearch, {\r\n method: 'POST',\r\n mode: 'cors',\r\n headers: {\r\n 'Content-Type': 'application/x-www-form-urlencoded',\r\n },\r\n body: params.toString(),\r\n });\r\n // 请求成功\r\n if (res.ok) {\r\n const result = await res.json();\r\n const { data, status } = result;\r\n if (status !== 0) {\r\n // 答案列表\r\n const answerList = JSON.parse(data.txt_content);\r\n // 答案\r\n const answers = answerList[0].content.split(/[;\\s]/);\r\n return answers;\r\n }\r\n }\r\n }\r\n catch (error) { }\r\n return [];\r\n}\r\n/**\r\n * @description 保存答案\r\n */\r\nasync function saveAnswer(question, answer) {\r\n try {\r\n // 内容\r\n const content = JSON.stringify([{ title: md5(question), content: answer }]);\r\n // 数据\r\n const data = {\r\n txt_name: md5(question),\r\n txt_content: content,\r\n password: '',\r\n v_id: '',\r\n };\r\n const params = new URLSearchParams(data);\r\n // 请求\r\n const res = await fetch(API_CONFIG.answerSave, {\r\n method: 'POST',\r\n mode: 'cors',\r\n headers: {\r\n 'Content-Type': 'application/x-www-form-urlencoded',\r\n },\r\n body: params.toString(),\r\n });\r\n // 请求成功\r\n if (res.ok) {\r\n const data = await res.json();\r\n return data;\r\n }\r\n }\r\n catch (error) { }\r\n}\r\n\r\n","from":"src\\api\\answer.ts","to":"answer.js"},"src\\api\\data.ts":{"timeStamp":"2023-06-02T06:24:50.525Z","data":"/* 数据 API */\r\n/**\r\n * @description 获取新闻数据\r\n */\r\nasync function getNewsList() {\r\n // 随机\r\n const randNum = ~~(Math.random() * API_CONFIG.todayNews.length);\r\n try {\r\n // 获取重要新闻\r\n const res = await fetch(API_CONFIG.todayNews[randNum], {\r\n method: 'GET',\r\n });\r\n // 请求成功\r\n if (res.ok) {\r\n const data = await res.json();\r\n return data;\r\n }\r\n }\r\n catch (err) { }\r\n}\r\n/**\r\n * @description 获取视频数据\r\n */\r\nasync function getVideoList() {\r\n // 随机\r\n const randNum = ~~(Math.random() * API_CONFIG.todayVideos.length);\r\n try {\r\n // 获取重要新闻\r\n const res = await fetch(API_CONFIG.todayVideos[randNum], {\r\n method: 'GET',\r\n });\r\n // 请求成功\r\n if (res.ok) {\r\n const data = await res.json();\r\n return data;\r\n }\r\n }\r\n catch (err) { }\r\n}\r\n/**\r\n * @description 专项练习数据\r\n */\r\nasync function getExamPaper(pageNo) {\r\n // 链接\r\n const url = `${API_CONFIG.paperList}?pageSize=50&pageNo=${pageNo}`;\r\n try {\r\n // 获取专项练习\r\n const res = await fetch(url, {\r\n method: 'GET',\r\n credentials: 'include',\r\n });\r\n // 请求成功\r\n if (res.ok) {\r\n const data = await res.json();\r\n const paperJson = decodeURIComponent(escape(window.atob(data.data_str.replace(/-/g, '+').replace(/_/g, '/'))));\r\n // JSON格式化\r\n const paper = JSON.parse(paperJson);\r\n return paper;\r\n }\r\n }\r\n catch (err) {\r\n return [];\r\n }\r\n return [];\r\n}\r\n\r\n","from":"src\\api\\data.ts","to":"data.js"},"src\\api\\login.ts":{"timeStamp":"2023-02-11T12:24:53.231Z","data":"/**\r\n * @description 生成二维码\r\n */\r\nasync function generateQRCode() {\r\n try {\r\n // 推送\r\n const res = await fetch(API_CONFIG.generateQRCode, {\r\n method: 'GET',\r\n mode: 'cors',\r\n });\r\n // 请求成功\r\n if (res.ok) {\r\n const data = await res.json();\r\n if (data.success) {\r\n return data.result;\r\n }\r\n }\r\n }\r\n catch (error) { }\r\n}\r\n/**\r\n * @description 用二维码登录\r\n */\r\nasync function loginWithQRCode(qrCode) {\r\n try {\r\n const params = new URLSearchParams({\r\n qrCode,\r\n goto: 'https://oa.xuexi.cn',\r\n pdmToken: '',\r\n });\r\n // 推送\r\n const res = await fetch(API_CONFIG.loginWithQRCode, {\r\n method: 'POST',\r\n mode: 'cors',\r\n credentials: 'include',\r\n headers: {\r\n 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',\r\n },\r\n body: params.toString(),\r\n });\r\n // 请求成功\r\n if (res.ok) {\r\n const data = await res.json();\r\n return data;\r\n }\r\n }\r\n catch (error) { }\r\n}\r\n/**\r\n * @description 签名\r\n */\r\nasync function getSign() {\r\n try {\r\n // 推送\r\n const res = await fetch(API_CONFIG.sign, {\r\n method: 'GET',\r\n mode: 'cors',\r\n credentials: 'include',\r\n });\r\n // 请求成功\r\n if (res.ok) {\r\n const data = await res.json();\r\n if (data.ok) {\r\n return data.data.sign;\r\n }\r\n }\r\n }\r\n catch (error) { }\r\n}\r\n/**\r\n * @description 安全检查\r\n * @param data\r\n */\r\nasync function secureCheck(data) {\r\n try {\r\n const params = new URLSearchParams(data);\r\n const url = `${API_CONFIG.secureCheck}?${params}`;\r\n // 推送\r\n const res = await fetch(url, {\r\n method: 'GET',\r\n mode: 'cors',\r\n credentials: 'include',\r\n });\r\n // 请求成功\r\n if (res.ok) {\r\n const data = await res.json();\r\n return data.success;\r\n }\r\n }\r\n catch (error) { }\r\n return false;\r\n}\r\n\r\n","from":"src\\api\\login.ts","to":"login.js"},"src\\api\\push.ts":{"timeStamp":"2023-02-11T12:24:42.072Z","data":"/* 推送 API */\r\n/**\r\n * @description 推送\r\n */\r\nasync function pushPlus(token, title, content, template, toToken) {\r\n try {\r\n // 参数体\r\n const body = {\r\n token,\r\n title,\r\n content,\r\n template,\r\n };\r\n // 好友令牌\r\n if (toToken) {\r\n body.to = toToken;\r\n }\r\n // 推送\r\n const res = await fetch(API_CONFIG.push, {\r\n method: 'POST',\r\n mode: 'cors',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n },\r\n body: JSON.stringify(body),\r\n });\r\n // 请求成功\r\n if (res.ok) {\r\n const data = await res.json();\r\n return data;\r\n }\r\n }\r\n catch (error) { }\r\n}\r\n\r\n","from":"src\\api\\push.ts","to":"push.js"},"src\\api\\user.ts":{"timeStamp":"2023-02-12T08:26:02.766Z","data":"/* 用户 API */\r\n/**\r\n * @description 获取用户信息\r\n */\r\nasync function getUserInfo() {\r\n try {\r\n const res = await fetch(API_CONFIG.userInfo, {\r\n method: 'GET',\r\n credentials: 'include',\r\n });\r\n // 请求成功\r\n if (res.ok) {\r\n const { data } = await res.json();\r\n return data;\r\n }\r\n }\r\n catch (err) { }\r\n}\r\n/**\r\n * @description 获取总积分\r\n */\r\nasync function getTotalScore() {\r\n try {\r\n const res = await fetch(API_CONFIG.totalScore, {\r\n method: 'GET',\r\n credentials: 'include',\r\n });\r\n // 请求成功\r\n if (res.ok) {\r\n const { data } = await res.json();\r\n // 总分\r\n const { score } = data;\r\n return score;\r\n }\r\n }\r\n catch (err) { }\r\n}\r\n/**\r\n * @description 获取当天总积分\r\n */\r\nasync function getTodayScore() {\r\n try {\r\n const res = await fetch(API_CONFIG.todayScore, {\r\n method: 'GET',\r\n credentials: 'include',\r\n });\r\n // 请求成功\r\n if (res.ok) {\r\n const { data } = await res.json();\r\n // 当天总分\r\n const { score } = data;\r\n return score;\r\n }\r\n }\r\n catch (err) { }\r\n}\r\n/**\r\n * @description 获取任务列表\r\n */\r\nasync function getTaskList() {\r\n try {\r\n const res = await fetch(API_CONFIG.taskList, {\r\n method: 'GET',\r\n credentials: 'include',\r\n });\r\n // 请求成功\r\n if (res.ok) {\r\n const { data } = await res.json();\r\n // 进度和当天总分\r\n const { taskProgress } = data;\r\n return taskProgress;\r\n }\r\n }\r\n catch (err) { }\r\n}\r\n\r\n","from":"src\\api\\user.ts","to":"user.js"},"src\\config\\task.ts":{"timeStamp":"2023-07-12T05:19:25.996Z","data":"/* task·配置 */\r\n/**\r\n * @description 单次最大新闻数\r\n */\r\nconst maxNewsNum = 6;\r\n/**\r\n * @description 单次最大视频数\r\n */\r\nconst maxVideoNum = 6;\r\n/**\r\n * @description 二维码最大刷新次数\r\n */\r\nconst maxRefreshCount = 10;\r\n/**\r\n * @description 二维码自动刷新间隔\r\n */\r\nconst autoRefreshQRCodeInterval = 100000;\r\n\r\n","from":"src\\config\\task.ts","to":"task.js"},"src\\config\\url.ts":{"timeStamp":"2023-01-11T12:31:08.253Z","data":"/**\r\n * @description url配置\r\n */\r\nconst URL_CONFIG = {\r\n // 主页正则\r\n home: /^https\\:\\/\\/www\\.xuexi\\.cn(\\/(index\\.html)?)?$/,\r\n // 主页\r\n homeOrigin: 'https://www.xuexi.cn',\r\n // 每日答题页面\r\n examPractice: 'https://pc.xuexi.cn/points/exam-practice.html',\r\n // 专项练习页面\r\n examPaper: 'https://pc.xuexi.cn/points/exam-paper-detail.html',\r\n};\r\n\r\n","from":"src\\config\\url.ts","to":"url.js"},"src\\config\\api.ts":{"timeStamp":"2023-03-21T07:23:02.891Z","data":"/**\r\n * @description api配置\r\n */\r\nconst API_CONFIG = {\r\n // 用户信息\r\n userInfo: 'https://pc-api.xuexi.cn/open/api/user/info',\r\n // 总分\r\n totalScore: 'https://pc-proxy-api.xuexi.cn/delegate/score/get',\r\n // 当天分数\r\n todayScore: 'https://pc-proxy-api.xuexi.cn/delegate/score/today/query',\r\n // 任务列表\r\n taskList: 'https://pc-proxy-api.xuexi.cn/delegate/score/days/listScoreProgress?sence=score&deviceType=2',\r\n // 新闻数据\r\n todayNews: [\r\n 'https://www.xuexi.cn/lgdata/35il6fpn0ohq.json',\r\n 'https://www.xuexi.cn/lgdata/1ap1igfgdn2.json',\r\n 'https://www.xuexi.cn/lgdata/vdppiu92n1.json',\r\n 'https://www.xuexi.cn/lgdata/152mdtl3qn1.json',\r\n ],\r\n // 视频数据\r\n todayVideos: [\r\n 'https://www.xuexi.cn/lgdata/525pi8vcj24p.json',\r\n 'https://www.xuexi.cn/lgdata/11vku6vt6rgom.json',\r\n 'https://www.xuexi.cn/lgdata/2qfjjjrprmdh.json',\r\n 'https://www.xuexi.cn/lgdata/3o3ufqgl8rsn.json',\r\n 'https://www.xuexi.cn/lgdata/591ht3bc22pi.json',\r\n 'https://www.xuexi.cn/lgdata/1742g60067k.json',\r\n 'https://www.xuexi.cn/lgdata/1novbsbi47k.json',\r\n ],\r\n // 专项练习列表\r\n paperList: 'https://pc-proxy-api.xuexi.cn/api/exam/service/paper/pc/list',\r\n // 文本服务器保存答案\r\n answerSave: 'https://a6.qikekeji.com/txt/data/save',\r\n // 文本服务器获取答案\r\n answerSearch: 'https://a6.qikekeji.com/txt/data/detail',\r\n // 推送\r\n push: 'https://www.pushplus.plus/send',\r\n // 生成二维码\r\n generateQRCode: 'https://login.xuexi.cn/user/qrcode/generate',\r\n //二维码登录\r\n loginWithQRCode: 'https://login.xuexi.cn/login/login_with_qr',\r\n // 签名\r\n sign: 'https://pc-api.xuexi.cn/open/api/sns/sign',\r\n // 安全检查\r\n secureCheck: 'https://pc-api.xuexi.cn/login/secure_check',\r\n // 二维码\r\n qrcode: 'https://api.qrserver.com/v1/create-qr-code',\r\n};\r\n\r\n","from":"src\\config\\api.ts","to":"api.js"},"src\\config\\version.ts":{"timeStamp":"2023-07-13T13:35:55.841Z","data":"/**\r\n * @description 版本号\r\n */\r\nconst version = '1.7.5';\r\n\r\n","from":"src\\config\\version.ts","to":"version.js"},"src\\types\\index.ts":{"timeStamp":"2023-07-12T05:59:43.153Z","data":"/**\r\n * @description 任务类型\r\n */\r\nvar TaskType;\r\n(function (TaskType) {\r\n TaskType[TaskType[\"LOGIN\"] = 0] = \"LOGIN\";\r\n TaskType[TaskType[\"READ\"] = 1] = \"READ\";\r\n TaskType[TaskType[\"WATCH\"] = 2] = \"WATCH\";\r\n TaskType[TaskType[\"PRACTICE\"] = 3] = \"PRACTICE\";\r\n})(TaskType || (TaskType = {}));\r\n/**\r\n * @description 设置类型\r\n */\r\nvar SettingType;\r\n(function (SettingType) {\r\n SettingType[SettingType[\"AUTO_START\"] = 0] = \"AUTO_START\";\r\n SettingType[SettingType[\"SAME_TAB\"] = 1] = \"SAME_TAB\";\r\n SettingType[SettingType[\"SILENT_RUN\"] = 2] = \"SILENT_RUN\";\r\n SettingType[SettingType[\"SCHEDULE_RUN\"] = 3] = \"SCHEDULE_RUN\";\r\n SettingType[SettingType[\"VIDEO_MUTED\"] = 4] = \"VIDEO_MUTED\";\r\n SettingType[SettingType[\"RANDOM_EXAM\"] = 5] = \"RANDOM_EXAM\";\r\n SettingType[SettingType[\"AUTO_ANSWER\"] = 6] = \"AUTO_ANSWER\";\r\n SettingType[SettingType[\"REMOTE_PUSH\"] = 7] = \"REMOTE_PUSH\";\r\n})(SettingType || (SettingType = {}));\r\n/**\r\n * @description 进度类型\r\n */\r\nvar TaskStatusType;\r\n(function (TaskStatusType) {\r\n TaskStatusType[TaskStatusType[\"LOADING\"] = 0] = \"LOADING\";\r\n TaskStatusType[TaskStatusType[\"LOADED\"] = 1] = \"LOADED\";\r\n TaskStatusType[TaskStatusType[\"START\"] = 2] = \"START\";\r\n TaskStatusType[TaskStatusType[\"PAUSE\"] = 3] = \"PAUSE\";\r\n TaskStatusType[TaskStatusType[\"FINISH\"] = 4] = \"FINISH\";\r\n})(TaskStatusType || (TaskStatusType = {}));\r\n\r\n","from":"src\\types\\index.ts","to":"index.js"},"src\\utils\\composition.ts":{"timeStamp":"2023-03-07T10:10:48.749Z","data":"// 当前订阅\r\nlet currentSub;\r\n// 订阅\r\nconst subscription = new WeakMap();\r\n/**\r\n * @description Proxy Map\r\n */\r\nconst proxyMap = new WeakMap();\r\n/**\r\n * @description 收集 Ref 依赖\r\n * @param target\r\n * @param key\r\n */\r\nconst trackRef = (target) => {\r\n // 当前订阅\r\n if (!currentSub) {\r\n return;\r\n }\r\n // target 订阅列表\r\n let subList = subscription.get(target);\r\n // 不存在订阅列表\r\n if (!subList) {\r\n subList = new Map();\r\n // 键订阅\r\n const subkeyList = new Set();\r\n // 添加订阅\r\n subkeyList.add(currentSub);\r\n subList.set('value', subkeyList);\r\n subscription.set(target, subList);\r\n return;\r\n }\r\n // 键订阅\r\n let subkeyList = subList.get('value');\r\n if (!subkeyList) {\r\n // 键订阅\r\n subkeyList = new Set();\r\n // 添加订阅\r\n subkeyList.add(currentSub);\r\n subList.set('value', subkeyList);\r\n subscription.set(target, subList);\r\n return;\r\n }\r\n // 添加订阅\r\n subkeyList.add(currentSub);\r\n};\r\n/**\r\n * @description 通知 Ref 订阅\r\n * @param terget\r\n * @param key\r\n * @returns\r\n */\r\nfunction triggerRef(target, newVal, oldVal) {\r\n // target 订阅列表\r\n const subList = subscription.get(target);\r\n if (!subList) {\r\n return;\r\n }\r\n // 键订阅\r\n let subkeyList = subList.get('value');\r\n if (!subkeyList) {\r\n return;\r\n }\r\n // 通知订阅\r\n for (const fn of subkeyList) {\r\n if (fn instanceof Function) {\r\n fn(newVal, oldVal);\r\n }\r\n }\r\n}\r\n/**\r\n * @description 收集依赖\r\n * @param target\r\n * @param key\r\n */\r\nconst track = (target, key) => {\r\n // 当前订阅\r\n if (!currentSub) {\r\n return;\r\n }\r\n // proxy\r\n const proxyTarget = proxyMap.get(target);\r\n if (!proxyTarget) {\r\n return;\r\n }\r\n // target 订阅列表\r\n let subList = subscription.get(target);\r\n // 不存在订阅列表\r\n if (!subList) {\r\n subList = new Map();\r\n // 键订阅\r\n const subkeyList = new Set();\r\n // 添加订阅\r\n subkeyList.add(currentSub);\r\n subList.set(key, subkeyList);\r\n subscription.set(target, subList);\r\n return;\r\n }\r\n // 键订阅\r\n let subkeyList = subList.get(key);\r\n if (!subkeyList) {\r\n // 键订阅\r\n subkeyList = new Set();\r\n // 添加订阅\r\n subkeyList.add(currentSub);\r\n subList.set(key, subkeyList);\r\n subscription.set(target, subList);\r\n return;\r\n }\r\n // 添加订阅\r\n subkeyList.add(currentSub);\r\n};\r\n/**\r\n * @description 通知订阅\r\n * @param terget\r\n * @param key\r\n * @returns\r\n */\r\nfunction trigger(target, key, newVal, oldVal) {\r\n // proxy\r\n const proxyTarget = proxyMap.get(target);\r\n if (!proxyTarget) {\r\n return;\r\n }\r\n // proxyTarget 订阅列表\r\n const subList = subscription.get(target);\r\n if (!subList) {\r\n return;\r\n }\r\n // 键订阅\r\n let subkeyList = subList.get(key);\r\n if (!subkeyList) {\r\n return;\r\n }\r\n // 通知订阅\r\n for (const fn of subkeyList) {\r\n fn(newVal, oldVal);\r\n }\r\n}\r\n/**\r\n * @description 只读键\r\n */\r\nvar ReactiveFlags;\r\n(function (ReactiveFlags) {\r\n ReactiveFlags[\"IS_REF\"] = \"_isRef\";\r\n ReactiveFlags[\"IS_SHALLOW\"] = \"_isShallow\";\r\n ReactiveFlags[\"IS_REACTIVE\"] = \"_isReactive\";\r\n ReactiveFlags[\"IS_READONLY\"] = \"_isReadonly\";\r\n})(ReactiveFlags || (ReactiveFlags = {}));\r\n/**\r\n * @description Ref\r\n */\r\nclass Ref {\r\n _isShallow = false;\r\n _isRef = true;\r\n _value;\r\n value;\r\n constructor(val, shallow = false) {\r\n const _this = this;\r\n this._isShallow = shallow;\r\n if (val && typeof val === 'object' && shallow) {\r\n const reactiveVal = reactive(val);\r\n this._value = reactiveVal;\r\n this.value = reactiveVal;\r\n }\r\n else {\r\n this._value = val;\r\n this.value = val;\r\n }\r\n // 定义属性\r\n Object.defineProperty(this, 'value', {\r\n get() {\r\n // 收集依赖\r\n trackRef(this);\r\n return _this._value;\r\n },\r\n set(newVal) {\r\n // 旧数据\r\n const oldVal = this._value;\r\n // 数据变化\r\n if (oldVal !== newVal) {\r\n // 设置新数据值\r\n _this._value = newVal;\r\n // 通知依赖\r\n triggerRef(this, newVal, oldVal);\r\n }\r\n },\r\n });\r\n }\r\n toJSON() {\r\n return this._value;\r\n }\r\n}\r\n/**\r\n * @description ref\r\n * @param v\r\n * @returns\r\n */\r\nconst isRef = (v) => {\r\n return !!(v && v[ReactiveFlags.IS_REF]);\r\n};\r\n/**\r\n * @description 浅层 shallow\r\n * @param v\r\n * @returns\r\n */\r\nconst isShallow = (v) => {\r\n return !!(v && v[ReactiveFlags.IS_SHALLOW]);\r\n};\r\n/**\r\n * @description 创建 ref\r\n * @param v\r\n * @returns\r\n */\r\nconst createRef = (rawVal, shallow) => {\r\n return new Ref(rawVal, shallow);\r\n};\r\n/**\r\n * @description 解除 ref\r\n * @param val\r\n * @returns\r\n */\r\nconst unref = (val) => {\r\n return (isRef(val) ? val.value : val);\r\n};\r\n/**\r\n * @description 顶层 ref\r\n * @param v\r\n * @returns\r\n */\r\nconst ref = (value) => {\r\n return isRef(value)\r\n ? value\r\n : createRef(value, true);\r\n};\r\n/**\r\n * @description ref\r\n * @param value\r\n * @returns\r\n */\r\nconst shallowRef = (value) => {\r\n return isRef(value)\r\n ? value\r\n : createRef(value, false);\r\n};\r\n/**\r\n * @description 创建处理 reactive\r\n * @param isReadonly\r\n * @param isShallow\r\n * @returns\r\n */\r\nconst createReactiveHandlers = (isReadonly, isShallow) => {\r\n return {\r\n get: createGetters(isReadonly, isShallow),\r\n set: createSetters(isReadonly, isShallow),\r\n };\r\n};\r\n/**\r\n * @description getters\r\n * @param isReadonly\r\n * @param isShallow\r\n * @returns\r\n */\r\nconst createGetters = (isReadonly, isShallow) => {\r\n return function get(target, key, receiver) {\r\n if (key === ReactiveFlags.IS_REACTIVE) {\r\n return !isReadonly;\r\n }\r\n if (key === ReactiveFlags.IS_READONLY) {\r\n return isReadonly;\r\n }\r\n if (key === ReactiveFlags.IS_SHALLOW) {\r\n return isShallow;\r\n }\r\n // 结果\r\n const res = Reflect.get(target, key, receiver);\r\n if (!isReadonly) {\r\n // 收集依赖\r\n track(target, key);\r\n }\r\n if (isShallow) {\r\n return res;\r\n }\r\n if (isRef(res)) {\r\n return res.value;\r\n }\r\n if (res && typeof res === 'object') {\r\n if (res instanceof Element) {\r\n return res;\r\n }\r\n return isReadonly ? readonly(res) : reactive(res);\r\n }\r\n return res;\r\n };\r\n};\r\n/**\r\n * @description setters\r\n * @param readonly\r\n * @param shallow\r\n * @returns\r\n */\r\nconst createSetters = (readonly, shallow) => {\r\n return function set(target, key, newVal, receiver) {\r\n // 只读\r\n if (readonly) {\r\n return false;\r\n }\r\n // 旧值\r\n const oldVal = target[key];\r\n if (isReadonly(oldVal) && isRef(oldVal) && !isRef(newVal)) {\r\n return false;\r\n }\r\n if (!shallow) {\r\n if (isRef(oldVal) && !isRef(newVal)) {\r\n oldVal.value = newVal;\r\n return true;\r\n }\r\n }\r\n const res = Reflect.set(target, key, newVal, receiver);\r\n // length\r\n if (Array.isArray(target) && key === 'length') {\r\n // 通知依赖\r\n trigger(target, key, newVal, oldVal);\r\n return res;\r\n }\r\n // 数据变化\r\n if (oldVal !== newVal) {\r\n // 通知依赖\r\n trigger(target, key, newVal, oldVal);\r\n }\r\n return res;\r\n };\r\n};\r\n/**\r\n * @description reactive object\r\n */\r\nconst createReactiveObj = (target, isReadonly, shallow) => {\r\n // 存在 Proxy\r\n const existingProxy = proxyMap.get(target);\r\n if (existingProxy) {\r\n return existingProxy;\r\n }\r\n // 新建\r\n const proxy = new Proxy(target, createReactiveHandlers(isReadonly, shallow));\r\n proxyMap.set(target, proxy);\r\n return proxy;\r\n};\r\n/**\r\n * @description reactive\r\n * @param val\r\n * @returns\r\n */\r\nconst isReactive = (val) => {\r\n return !!(val && val[ReactiveFlags.IS_REACTIVE]);\r\n};\r\n/**\r\n * @description 创建 reactive\r\n * @param target\r\n * @returns\r\n */\r\nconst createReactive = (target) => {\r\n return createReactiveObj(target, false, false);\r\n};\r\n/**\r\n * @description 顶层 reactive\r\n * @param target\r\n * @returns\r\n */\r\nconst shallowReactive = (target) => {\r\n return createReactiveObj(target, false, true);\r\n};\r\n/**\r\n * @description reactive\r\n * @param val\r\n * @returns\r\n */\r\nconst isReadonly = (val) => {\r\n return !!(val && val[ReactiveFlags.IS_READONLY]);\r\n};\r\n/**\r\n * @description 创建 readonly\r\n * @param target\r\n * @returns\r\n */\r\nconst createReadonly = (target) => {\r\n return createReactiveObj(target, true, false);\r\n};\r\n/**\r\n * @description 顶层 readonly\r\n * @param target\r\n * @returns\r\n */\r\nconst shallowReadonly = (target) => {\r\n return createReactiveObj(target, true, true);\r\n};\r\n/**\r\n * @description proxy\r\n * @param val\r\n * @returns\r\n */\r\nconst isProxy = (val) => {\r\n return isReactive(val) || isReadonly(val);\r\n};\r\n/**\r\n * @description reactive\r\n * @param target\r\n * @returns\r\n */\r\nconst reactive = (target) => {\r\n return createReactive(target);\r\n};\r\n/**\r\n * @description readonly\r\n * @param target\r\n * @returns\r\n */\r\nconst readonly = (target) => {\r\n return createReadonly(target);\r\n};\r\n/**\r\n * @description 监听数据变化\r\n * @param source\r\n * @param callback\r\n */\r\nconst watch = (source, callback, immediate = false) => {\r\n // 立刻执行\r\n immediate && callback(unref(source), unref(source));\r\n // array\r\n if (Array.isArray(source) && source.every((s) => isRef(s))) {\r\n for (const i in source) {\r\n // Proxy\r\n if (isProxy(source[i])) {\r\n watch(source[i], () => {\r\n const res = source.map((s) => unref(s));\r\n callback(res, res);\r\n });\r\n }\r\n }\r\n watch(() => source.map((s) => unref(s)), callback);\r\n return;\r\n }\r\n // function\r\n if (source instanceof Function) {\r\n watch(watchEffectRef(source), (n, o) => {\r\n callback(unref(n), unref(o));\r\n });\r\n return;\r\n }\r\n // Proxy\r\n if (isProxy(source)) {\r\n for (const key in source) {\r\n currentSub = () => {\r\n callback(source, source);\r\n };\r\n // sub source\r\n const subSource = source[key];\r\n currentSub = undefined;\r\n watch(subSource, () => {\r\n callback(source, source);\r\n });\r\n }\r\n return;\r\n }\r\n // Ref\r\n if (isRef(source)) {\r\n // Ref.value Proxy\r\n if (isProxy(source.value)) {\r\n watch(source.value, () => {\r\n callback(unref(source), unref(source));\r\n });\r\n }\r\n currentSub = callback;\r\n // 收集依赖\r\n trackRef(source);\r\n currentSub = undefined;\r\n return;\r\n }\r\n};\r\n/**\r\n * @description 监听数据变化影响\r\n * @param callback\r\n * @returns\r\n */\r\nconst watchEffect = (callback) => {\r\n currentSub = callback;\r\n // 收集依赖\r\n callback();\r\n currentSub = undefined;\r\n};\r\n/**\r\n * @description 监听影响 ref\r\n * @param refVal\r\n * @param callback\r\n * @returns\r\n */\r\nconst watchRef = (source, callback) => {\r\n // 收集依赖\r\n const effectRes = shallowRef(callback());\r\n // 监听\r\n watch(source, () => (effectRes.value = unref(callback())));\r\n return effectRes;\r\n};\r\n/**\r\n * @description 监听影响 ref\r\n * @param refVal\r\n * @param callback\r\n * @returns\r\n */\r\nconst watchEffectRef = (callback) => {\r\n // 收集依赖\r\n const effectRes = shallowRef(undefined);\r\n // 监听\r\n watchEffect(() => (effectRes.value = unref(callback())));\r\n return effectRes;\r\n};\r\n\r\n","from":"src\\utils\\composition.ts","to":"composition.js"},"src\\utils\\element.ts":{"timeStamp":"2023-03-07T11:28:14.066Z","data":"/**\r\n * @description 创建元素节点\r\n * @param eleName\r\n * @param props\r\n * @param attrs\r\n * @param children\r\n * @returns\r\n */\r\nfunction createElementNode(tagName, props, attrs, children, options) {\r\n // 挂载状态\r\n let beforemount = ref(false);\r\n // 挂载状态\r\n let mounted = ref(false);\r\n const { onCreated, beforeCreat, onMounted, beforeMount } = options || {};\r\n // 订阅\r\n const subscribe = (e) => {\r\n const { onMounted, beforeMount } = e;\r\n if (beforeMount) {\r\n watch(beforemount, () => {\r\n if (beforemount.value) {\r\n beforeMount();\r\n return;\r\n }\r\n }, true);\r\n }\r\n if (onMounted) {\r\n watch(mounted, () => {\r\n if (mounted.value) {\r\n onMounted();\r\n return;\r\n }\r\n }, true);\r\n }\r\n };\r\n // 取消订阅\r\n const unsubscribe = (e) => {\r\n //懒得写\r\n };\r\n // 创建元素前\r\n beforeCreat && beforeCreat();\r\n // 创建普通元素\r\n const ele = document.createElement(tagName);\r\n // 处理属性\r\n handleProps(ele, props);\r\n // 处理属性\r\n handleAttributes(ele, attrs, subscribe, unsubscribe);\r\n // 处理子元素\r\n handleChildren(ele, children, subscribe, unsubscribe);\r\n // 收集挂载前\r\n const collectBeforeMount = () => {\r\n beforemount.value = true;\r\n beforeMount && beforeMount();\r\n };\r\n // 收集挂载\r\n const collectOnMounted = () => {\r\n mounted.value = true;\r\n onMounted && onMounted();\r\n };\r\n // 创建元素后\r\n onCreated && onCreated();\r\n return { ele, beforeMount: collectBeforeMount, onMounted: collectOnMounted };\r\n}\r\n/**\r\n * @description 创建svg元素\r\n * @param tagName\r\n * @param props\r\n * @param attrs\r\n * @param children\r\n * @returns\r\n */\r\nfunction createNSElementNode(tagName, props, attrs, children, options) {\r\n // 挂载状态\r\n let beforemount = ref(false);\r\n // 挂载状态\r\n let mounted = ref(false);\r\n const { onCreated, beforeCreat, onMounted, beforeMount } = options || {};\r\n // 订阅\r\n const subscribe = (e) => {\r\n const { onMounted, beforeMount } = e;\r\n if (beforeMount) {\r\n watch(beforemount, () => {\r\n if (beforemount.value) {\r\n beforeMount();\r\n return;\r\n }\r\n }, true);\r\n }\r\n if (onMounted) {\r\n watch(mounted, () => {\r\n if (mounted.value) {\r\n onMounted();\r\n return;\r\n }\r\n }, true);\r\n }\r\n };\r\n // 取消订阅\r\n const unsubscribe = (e) => {\r\n //懒得写\r\n };\r\n // 创建元素前\r\n beforeCreat && beforeCreat();\r\n // svg元素命名空间\r\n const ns = 'http://www.w3.org/2000/svg';\r\n // 创建svg元素\r\n const ele = document.createElementNS(ns, tagName);\r\n // 处理属性\r\n handleProps(ele, props);\r\n // 处理属性\r\n handleAttributes(ele, attrs, subscribe, unsubscribe);\r\n // 处理子元素\r\n handleChildren(ele, children, subscribe, unsubscribe);\r\n // 收集挂载前\r\n const collectBeforeMount = () => {\r\n beforemount.value = true;\r\n beforeMount && beforeMount();\r\n };\r\n // 收集挂载\r\n const collectOnMounted = () => {\r\n mounted.value = true;\r\n onMounted && onMounted();\r\n };\r\n // 创建元素后\r\n onCreated && onCreated();\r\n return { ele, beforeMount: collectBeforeMount, onMounted: collectOnMounted };\r\n}\r\n/**\r\n * @description 处理属性\r\n * @param ele\r\n * @param props\r\n */\r\nfunction handleProps(ele, props) {\r\n // props属性设置\r\n for (const key in props) {\r\n // Ref 属性\r\n if (isRef(props[key])) {\r\n const refVal = props[key];\r\n watchEffect(() => (ele[key] = refVal.value));\r\n continue;\r\n }\r\n ele[key] = props[key];\r\n }\r\n}\r\n/**\r\n * @description 处理svg属性\r\n * @param ele\r\n * @param attrs\r\n */\r\nfunction handleAttributes(ele, attrs, subscribe, unsubscribe) {\r\n // 属性存在\r\n if (attrs) {\r\n // attrs属性设置\r\n for (const key in attrs) {\r\n // 处理普通属性\r\n handleAttribute(ele, key, attrs[key], subscribe, unsubscribe);\r\n }\r\n }\r\n}\r\n/**\r\n * @description 处理事件选项\r\n */\r\nfunction handleEventOptions(option) {\r\n if (option.length) {\r\n const options = {\r\n capture: option.includes('capture'),\r\n once: option.includes('once'),\r\n passive: option.includes('passive'),\r\n };\r\n return options;\r\n }\r\n}\r\n/**\r\n * @description 处理属性\r\n * @param ele\r\n * @param key\r\n * @param value\r\n */\r\nfunction handleAttribute(ele, key, value, subscribe, unsubscribe) {\r\n // 处理完的key\r\n const formatKey = key.toLowerCase();\r\n // 事件绑定\r\n if (formatKey.startsWith('on')) {\r\n // 事件监听\r\n const [event] = formatKey.match(/(?<=on).*/);\r\n // 事件类型\r\n if (event) {\r\n const [eventType, ...option] = event.split('_');\r\n const options = handleEventOptions(option);\r\n // Ref 函数\r\n if (isRef(value)) {\r\n const refVal = value;\r\n const refListener = watchRef(refVal, () => refVal.value\r\n ? (e) => {\r\n option.includes('prevent') && e.preventDefault();\r\n option.includes('stop') && e.stopPropagation();\r\n const callback = refVal.value;\r\n callback(e);\r\n }\r\n : undefined);\r\n // 设置事件监听\r\n refListener.value &&\r\n ele.addEventListener(eventType, refListener.value, options);\r\n // 监听事件变化\r\n watch(refListener, (newVal, oldVal) => {\r\n // 移除旧事件监听\r\n oldVal && ele.removeEventListener(eventType, oldVal);\r\n // 设置新事件监听\r\n newVal && ele.addEventListener(eventType, newVal, options);\r\n });\r\n return;\r\n }\r\n // 普通函数\r\n if (value instanceof Function) {\r\n // 设置事件监听\r\n ele.addEventListener(eventType, value, options);\r\n }\r\n }\r\n return;\r\n }\r\n // 特殊属性\r\n const specificAttrs = ['checked', 'selected', 'disabled', 'enabled'];\r\n // 特殊 key\r\n if (specificAttrs.includes(formatKey)) {\r\n // Ref\r\n if (isRef(value)) {\r\n const refVal = value;\r\n watchEffect(() => {\r\n if (refVal.value) {\r\n ele.setAttribute(formatKey, '');\r\n }\r\n else {\r\n ele.removeAttribute(formatKey);\r\n }\r\n });\r\n return;\r\n }\r\n // 普通属性值\r\n if (value) {\r\n ele.setAttribute(formatKey, '');\r\n }\r\n else {\r\n ele.removeAttribute(formatKey);\r\n }\r\n return;\r\n }\r\n // ref 属性名\r\n if (key === 'ref') {\r\n // Ref\r\n if (isRef(value)) {\r\n const refVal = value;\r\n subscribe &&\r\n subscribe({\r\n onMounted() {\r\n refVal.value = ele;\r\n },\r\n });\r\n return;\r\n }\r\n // Ref 函数\r\n if (value instanceof Function) {\r\n const refFn = value;\r\n subscribe &&\r\n subscribe({\r\n onMounted() {\r\n refFn(ele);\r\n },\r\n });\r\n return;\r\n }\r\n return;\r\n }\r\n // xlink命名空间\r\n if (key.startsWith('xlink:')) {\r\n // xlink属性命名空间\r\n const attrNS = 'http://www.w3.org/1999/xlink';\r\n if (value) {\r\n ele.setAttributeNS(attrNS, key, value);\r\n }\r\n else {\r\n ele.removeAttributeNS(attrNS, key);\r\n }\r\n return;\r\n }\r\n // Ref 属性值\r\n if (key && isRef(value)) {\r\n const refVal = value;\r\n // 监听影响\r\n watchEffect(() => {\r\n ele.setAttribute(key, refVal.value);\r\n });\r\n return;\r\n }\r\n // 普通属性\r\n if (key) {\r\n // 普通属性\r\n ele.setAttribute(key, value);\r\n }\r\n}\r\n/**\r\n * @description 处理子元素\r\n * @param ele\r\n * @param children\r\n */\r\nfunction handleChildren(ele, children, subscribe, unsubscribe) {\r\n // Ref\r\n if (isRef(children)) {\r\n // 注释元素\r\n const comment = document.createComment('');\r\n // 监听元素变化\r\n watch(children, async (newEle, oldEle) => {\r\n if (!newEle && oldEle) {\r\n // Promise\r\n if (oldEle instanceof Promise) {\r\n const oldEleRes = await oldEle;\r\n if (oldEleRes) {\r\n oldEleRes.forEach((ele) => {\r\n unsubscribe && unsubscribe(ele);\r\n });\r\n }\r\n }\r\n // unPromise\r\n if (!(oldEle instanceof Promise)) {\r\n oldEle.forEach((ele) => {\r\n unsubscribe && unsubscribe(ele);\r\n });\r\n }\r\n ele.replaceChildren(comment);\r\n return;\r\n }\r\n if (newEle) {\r\n if (oldEle) {\r\n // Promise\r\n if (oldEle instanceof Promise) {\r\n const oldEleRes = await oldEle;\r\n if (oldEleRes) {\r\n oldEleRes.forEach((ele) => {\r\n unsubscribe && unsubscribe(ele);\r\n });\r\n }\r\n }\r\n // unPromise\r\n if (!(oldEle instanceof Promise)) {\r\n oldEle.forEach((ele) => {\r\n unsubscribe && unsubscribe(ele);\r\n });\r\n }\r\n }\r\n // Promise\r\n if (newEle instanceof Promise) {\r\n const newEleRes = await newEle;\r\n if (newEleRes) {\r\n const eles = newEleRes.map((v) => {\r\n if (v.beforeMount || v.onMounted) {\r\n subscribe && subscribe(v);\r\n }\r\n return v.ele;\r\n });\r\n ele.replaceChildren(createElementBlock(eles));\r\n }\r\n return;\r\n }\r\n // unPromise\r\n const eles = newEle.map((v) => {\r\n if (v.beforeMount || v.onMounted) {\r\n subscribe && subscribe(v);\r\n }\r\n return v.ele;\r\n });\r\n ele.replaceChildren(createElementBlock(eles));\r\n return;\r\n }\r\n });\r\n // Promise\r\n if (children.value instanceof Promise) {\r\n // 插入注释元素\r\n ele.appendChild(comment);\r\n children.value.then((childrenEle) => {\r\n if (childrenEle) {\r\n const eles = childrenEle.map((v) => {\r\n if (v.beforeMount || v.onMounted) {\r\n subscribe && subscribe(v);\r\n }\r\n return v.ele;\r\n });\r\n ele.replaceChildren(createElementBlock(eles));\r\n }\r\n });\r\n return;\r\n }\r\n // unPromise\r\n if (children.value) {\r\n const eles = children.value.map((v) => {\r\n if (v.beforeMount || v.onMounted) {\r\n subscribe && subscribe(v);\r\n }\r\n return v.ele;\r\n });\r\n ele.appendChild(createElementBlock(eles));\r\n return;\r\n }\r\n // 插入元素\r\n ele.appendChild(comment);\r\n return;\r\n }\r\n // Promise\r\n if (children instanceof Promise) {\r\n // 注释元素\r\n const comment = document.createComment('');\r\n // 插入注释元素\r\n ele.appendChild(comment);\r\n // 异步替换元素\r\n children.then((childEle) => {\r\n if (childEle) {\r\n const { beforeMount, onMounted } = childEle;\r\n if (beforeMount || onMounted) {\r\n subscribe && subscribe(childEle);\r\n }\r\n comment.replaceWith(childEle.ele);\r\n }\r\n });\r\n return;\r\n }\r\n // Array\r\n if (Array.isArray(children)) {\r\n // 处理过后\r\n const resChildren = [];\r\n for (const i in children) {\r\n const child = children[i];\r\n // Ref\r\n if (isRef(child)) {\r\n // 注释\r\n const comment = document.createComment('');\r\n // 监听影响\r\n watch(child, async (newEle, oldEle) => {\r\n // 新元素为空\r\n if (!newEle && oldEle) {\r\n // Promise\r\n if (oldEle instanceof Promise) {\r\n const oldEleRes = await oldEle;\r\n if (oldEleRes) {\r\n handleChangeElement(newEle, oldEleRes, comment, subscribe, unsubscribe);\r\n }\r\n return;\r\n }\r\n handleChangeElement(newEle, oldEle, comment, subscribe, unsubscribe);\r\n return;\r\n }\r\n // 旧元素为空\r\n if (newEle && !oldEle) {\r\n // Promise\r\n if (newEle instanceof Promise) {\r\n const newEleRes = await newEle;\r\n if (newEleRes) {\r\n handleChangeElement(newEleRes, oldEle, comment, subscribe, unsubscribe);\r\n }\r\n return;\r\n }\r\n handleChangeElement(newEle, oldEle, comment, subscribe, unsubscribe);\r\n return;\r\n }\r\n // 存在\r\n if (newEle && oldEle) {\r\n // Promise\r\n if (newEle instanceof Promise && oldEle instanceof Promise) {\r\n const newEleRes = await newEle;\r\n const oldEleRes = await oldEle;\r\n // 处理元素变化\r\n handleChangeElement(newEleRes, oldEleRes, comment, subscribe, unsubscribe);\r\n return;\r\n }\r\n // Promise\r\n if (newEle instanceof Promise && !(oldEle instanceof Promise)) {\r\n const newEleRes = await newEle;\r\n // 处理元素变化\r\n handleChangeElement(newEleRes, oldEle, comment, subscribe, unsubscribe);\r\n return;\r\n }\r\n // Promise\r\n if (!(newEle instanceof Promise) && oldEle instanceof Promise) {\r\n const oldEleRes = await oldEle;\r\n // 处理元素变化\r\n handleChangeElement(newEle, oldEleRes, comment, subscribe, unsubscribe);\r\n return;\r\n }\r\n // 非 Promise\r\n if (!(oldEle instanceof Promise) && !(newEle instanceof Promise)) {\r\n // 处理元素变化\r\n handleChangeElement(newEle, oldEle, comment, subscribe, unsubscribe);\r\n return;\r\n }\r\n }\r\n });\r\n // Promise\r\n if (child.value instanceof Promise) {\r\n // 注释\r\n resChildren[i] = { ele: comment };\r\n // 异步替换\r\n child.value.then((childEle) => {\r\n if (childEle) {\r\n const { beforeMount, onMounted } = childEle;\r\n if (beforeMount || onMounted) {\r\n subscribe && subscribe(childEle);\r\n }\r\n comment.replaceWith(childEle.ele);\r\n }\r\n });\r\n continue;\r\n }\r\n // unPromise\r\n if (child.value) {\r\n const { beforeMount, onMounted, ele } = child.value;\r\n resChildren[i] = { ele, beforeMount, onMounted };\r\n continue;\r\n }\r\n resChildren[i] = { ele: comment };\r\n continue;\r\n }\r\n // Promise\r\n if (child instanceof Promise) {\r\n // 注释\r\n const comment = document.createComment('');\r\n resChildren[i] = { ele: comment };\r\n // 异步替换元素\r\n child.then((childEle) => {\r\n if (childEle) {\r\n const { beforeMount, onMounted } = childEle;\r\n if (beforeMount || onMounted) {\r\n subscribe && subscribe(childEle);\r\n }\r\n comment.replaceWith(childEle.ele);\r\n }\r\n });\r\n continue;\r\n }\r\n // 普通元素\r\n if (child) {\r\n const { beforeMount, onMounted, ele } = child;\r\n resChildren[i] = { ele, beforeMount, onMounted };\r\n }\r\n }\r\n const eles = resChildren.map((v) => {\r\n if (v.beforeMount || v.onMounted) {\r\n subscribe && subscribe(v);\r\n }\r\n return v.ele;\r\n });\r\n // 插入元素\r\n ele.appendChild(createElementBlock(eles));\r\n return;\r\n }\r\n // 普通元素\r\n if (children) {\r\n const { beforeMount, onMounted } = children;\r\n if (beforeMount || onMounted) {\r\n subscribe && subscribe(children);\r\n }\r\n // 插入元素\r\n ele.appendChild(children.ele);\r\n return;\r\n }\r\n return;\r\n}\r\n/**\r\n * @description 元素变化\r\n * @param newEle\r\n * @param oldEle\r\n * @param comment\r\n */\r\nfunction handleChangeElement(newEle, oldEle, comment, subscribe, unsubscribe) {\r\n if (newEle && oldEle) {\r\n const { beforeMount, onMounted } = newEle;\r\n if (beforeMount || onMounted) {\r\n subscribe && subscribe(newEle);\r\n }\r\n oldEle.ele.replaceWith(newEle.ele);\r\n return;\r\n }\r\n if (newEle && !oldEle) {\r\n const { beforeMount, onMounted } = newEle;\r\n if (beforeMount || onMounted) {\r\n subscribe && subscribe(newEle);\r\n }\r\n comment.replaceWith(newEle.ele);\r\n return;\r\n }\r\n if (!newEle && oldEle) {\r\n unsubscribe && unsubscribe(oldEle);\r\n oldEle.ele.replaceWith(comment);\r\n return;\r\n }\r\n}\r\n/**\r\n * @description 创建文字节点\r\n * @param text\r\n * @returns\r\n */\r\nfunction createTextNode(text, options) {\r\n const { onCreated, beforeCreat, onMounted, beforeMount } = options || {};\r\n // 创建元素前\r\n beforeCreat && beforeCreat();\r\n // Ref\r\n if (isRef(text)) {\r\n // ref\r\n const refVal = text;\r\n // 元素\r\n const ele = document.createTextNode('');\r\n // 订阅变化\r\n watchEffect(() => {\r\n ele.data = refVal.value;\r\n });\r\n // 创建元素后\r\n onCreated && onCreated();\r\n return { ele, beforeMount, onMounted };\r\n }\r\n // 创建元素后\r\n onCreated && onCreated();\r\n return { ele: document.createTextNode(String(text)), beforeMount, onMounted };\r\n}\r\n/**\r\n * @description 挂载元素\r\n * @param eleOptions\r\n * @param parent\r\n */\r\nfunction mountElement(eleOptions, parent = document.body) {\r\n const { ele, beforeMount, onMounted } = eleOptions;\r\n if (ele) {\r\n // 触发挂载前事件\r\n beforeMount && beforeMount();\r\n parent.appendChild(ele);\r\n // 挂在后\r\n onMounted && onMounted();\r\n }\r\n}\r\n/**\r\n * @description 选择器\r\n * @param selector\r\n * @returns\r\n */\r\nfunction $$(selector, parent = document) {\r\n return Array.from(parent.querySelectorAll(selector));\r\n}\r\n/**\r\n * @description 异步选择器\r\n * @param selector\r\n * @returns\r\n */\r\nfunction $_(selector, parent = document, timeout) {\r\n return new Promise((resolve) => {\r\n const timer = setInterval(() => {\r\n const selectors = Array.from(parent.querySelectorAll(selector));\r\n // 存在元素\r\n if (selectors.length) {\r\n clearInterval(timer);\r\n resolve(selectors);\r\n }\r\n }, 10);\r\n // 超时\r\n if (timeout) {\r\n setTimeout(() => {\r\n clearInterval(timer);\r\n resolve([]);\r\n }, timeout);\r\n }\r\n });\r\n}\r\n/**\r\n * @description 创建元素块\r\n * @param eles\r\n * @returns\r\n */\r\nfunction createElementBlock(eles) {\r\n const fragment = document.createDocumentFragment();\r\n for (const i in eles) {\r\n fragment.appendChild(eles[i]);\r\n }\r\n return fragment;\r\n}\r\n\r\n","from":"src\\utils\\element.ts","to":"element.js"},"src\\utils\\log.ts":{"timeStamp":"2023-03-04T04:21:42.715Z","data":"/**\r\n * @description 打印日志\r\n * @param text\r\n */\r\nfunction log(...text) {\r\n printColor('dodgerblue', ...text);\r\n}\r\n/**\r\n * @description 打印错误\r\n * @param text\r\n */\r\nfunction error(...text) {\r\n printColor('red', ...text);\r\n}\r\n/**\r\n * @description 打印信息\r\n * @param text\r\n */\r\nfunction info(...text) {\r\n printColor('yellow', ...text);\r\n}\r\n/**\r\n * @description 打印颜色\r\n * @param text\r\n * @param color\r\n */\r\nfunction printColor(color, ...text) {\r\n const textFormatted = text\r\n .map((t) => (typeof t === 'object' ? JSON.stringify(t) : String(t)))\r\n .join(' ');\r\n console.log(`%c[${formatDateTime()}] %c${textFormatted}`, '', `color: ${color}`);\r\n}\r\n\r\n","from":"src\\utils\\log.ts","to":"log.js"},"src\\utils\\push.ts":{"timeStamp":"2023-07-04T09:34:28.693Z","data":"/**\r\n * @description html进度条\r\n * @param title\r\n * @param percent\r\n * @returns\r\n */\r\nfunction getProgressHTML(title, current, total) {\r\n // html\r\n const progressHTML = `\r\n ${title}\r\n ${getHighlightHTML(`${current}`)} / ${total}\r\n \r\n \r\n \r\n `;\r\n return progressHTML;\r\n}\r\n/**\r\n * @description html高亮文本\r\n * @param text\r\n * @returns\r\n */\r\nfunction getHighlightHTML(text) {\r\n // html\r\n const highlightHTML = `${text}`;\r\n return highlightHTML;\r\n}\r\n/**\r\n * @description 二维码\r\n * @param src\r\n */\r\nfunction getImgHTML(src) {\r\n // 图片\r\n return `\r\n
\r\n \r\n \r\n
\r\n \r\n`;\r\n}\r\n/**\r\n * @description 创建模态框\r\n * @param options 选项\r\n * @returns\r\n */\r\nfunction createModal(options) {\r\n // 配置\r\n const { title, subTitle = '', to = '用户', content, type, from = 'tech-study.js', } = options;\r\n // 内容文本\r\n let contentText = '';\r\n if (Array.isArray(content)) {\r\n contentText = content.map((ct) => `
${ct}
`).join('');\r\n }\r\n else {\r\n contentText = content;\r\n }\r\n // 日期\r\n const dateTime = formatDateTime();\r\n // 类型html\r\n let typeHTML = '';\r\n if (type && type.length) {\r\n if (type === 'info') {\r\n typeHTML = `\r\n \r\n \r\n `;\r\n }\r\n if (type === 'warn') {\r\n typeHTML = `\r\n \r\n \r\n \r\n `;\r\n }\r\n if (type === 'success') {\r\n typeHTML = `\r\n \r\n \r\n \r\n `;\r\n }\r\n if (type === 'fail') {\r\n typeHTML = `\r\n \r\n \r\n \r\n `;\r\n }\r\n }\r\n // 类型\r\n const typeWrap = `\r\n \r\n ${typeHTML}\r\n \r\n `;\r\n // 基础html\r\n const baseHTML = `\r\n \r\n \r\n \r\n
\r\n ${typeWrap}\r\n ${title}\r\n
\r\n
${subTitle}
\r\n \r\n
\r\n\r\n
\r\n
\r\n ${getHighlightHTML(to)}, 你好!\r\n
\r\n
${contentText}
\r\n
\r\n \r\n
${dateTime}
\r\n
\r\n 来自\r\n ${from}\r\n
\r\n \r\n \r\n \r\n `;\r\n return baseHTML;\r\n}\r\n/**\r\n * @description 推送消息\r\n */\r\nasync function pushMessage(options) {\r\n // 选项\r\n const { title, content, template, fromToken, toToken } = options;\r\n // 推送\r\n const res = await pushPlus(fromToken, title, content, template, toToken);\r\n return res;\r\n}\r\n/**\r\n * @description 推送模态框\r\n */\r\nasync function pushModal(options, fromToken, toToken) {\r\n // html\r\n const html = createModal(options);\r\n // 推送\r\n const res = await pushMessage({\r\n title: '消息提示',\r\n content: html,\r\n fromToken,\r\n toToken,\r\n template: 'html',\r\n });\r\n if (res && res.code === 200) {\r\n return res;\r\n }\r\n return;\r\n}\r\n\r\n","from":"src\\utils\\push.ts","to":"push.js"},"src\\utils\\random.ts":{"timeStamp":"2023-02-11T12:25:34.984Z","data":"/**\r\n * @description 创建随机点\r\n * @param bounds 范围\r\n * @returns\r\n */\r\nfunction createRandomPoint(bounds) {\r\n // 范围\r\n const { x, y, width, height } = bounds;\r\n // 横坐标\r\n const randX = x + Math.random() * width * 0.5 + width * 0.25;\r\n // 纵坐标\r\n const randY = y + Math.random() * height * 0.5 + height * 0.25;\r\n return {\r\n x: randX,\r\n y: randY,\r\n };\r\n}\r\n/**\r\n * @description 生成随机路径\r\n * @param start\r\n * @param end\r\n * @param steps\r\n * @returns\r\n */\r\nfunction createRandomPath(start, end, steps) {\r\n // 最小水平增量\r\n const minDeltaX = (end.x - start.x) / steps;\r\n // 最大垂直增量\r\n const maxDeltaY = (end.y - start.y) / steps;\r\n const path = [];\r\n // 开始节点\r\n path.push(start);\r\n // 插入点\r\n for (let i = 0; i < steps; i++) {\r\n // 横坐标\r\n const x = path[i].x + Math.random() * 5 + minDeltaX;\r\n // 纵坐标\r\n const y = path[i].y +\r\n Math.random() * 5 * Math.pow(-1, ~~(Math.random() * 2 + 1)) +\r\n maxDeltaY;\r\n path.push({\r\n x,\r\n y,\r\n });\r\n }\r\n return path;\r\n}\r\n/**\r\n * @description 随机数字\r\n * @returns\r\n */\r\nfunction generateNumAsChar() {\r\n return (~~(Math.random() * 10)).toString();\r\n}\r\n/**\r\n * @description 随机大写字母\r\n * @returns\r\n */\r\nfunction generateUpperAsChar() {\r\n return String.fromCharCode(~~(Math.random() * 26) + 65);\r\n}\r\n/**\r\n * @description 随机小写字母\r\n * @returns\r\n */\r\nfunction generateLowerAsChar() {\r\n return String.fromCharCode(~~(Math.random() * 26) + 97);\r\n}\r\n/**\r\n * @description 随机混合字符\r\n * @param length\r\n * @returns\r\n */\r\nfunction generateMix(length = 6) {\r\n // 随机字符串\r\n const randomText = [];\r\n // 生成器\r\n const typeGenerator = [\r\n generateNumAsChar,\r\n generateUpperAsChar,\r\n generateLowerAsChar,\r\n ];\r\n if (length) {\r\n for (let i = 0; i < length; i++) {\r\n // 随机位置\r\n const randomIndex = ~~(Math.random() * typeGenerator.length);\r\n randomText.push(typeGenerator[randomIndex]());\r\n }\r\n }\r\n return randomText.join('');\r\n}\r\n\r\n","from":"src\\utils\\random.ts","to":"random.js"},"src\\utils\\time.ts":{"timeStamp":"2023-02-11T12:25:31.463Z","data":"/**\r\n * @description 格式化日期时间数字\r\n * @param num\r\n * @returns\r\n */\r\nfunction formatDateNum(num) {\r\n return num < 10 ? `0${num}` : `${num}`;\r\n}\r\n/**\r\n * @description 格式化日期时间\r\n * @param time\r\n * @returns\r\n * @example\r\n * formatDateTime() -> \"2022-09-01 08:00:00\"\r\n * formatDateTime(new Date()) -> \"2022-09-01 08:00:00\"\r\n * formatDateTime(Date.now()) -> \"2022-09-01 08:00:00\"\r\n */\r\nfunction formatDateTime(time = Date.now()) {\r\n const date = new Date(time);\r\n const s = date.getSeconds();\r\n const min = date.getMinutes();\r\n const h = date.getHours();\r\n const d = date.getDate();\r\n const m = date.getMonth() + 1;\r\n const y = date.getFullYear();\r\n // 日期\r\n const dateText = [y, m, d].map(formatDateNum).join('-');\r\n // 时间\r\n const timeText = [h, min, s].map(formatDateNum).join(':');\r\n // 日期时间\r\n const dateTimeText = `${dateText} ${timeText}`;\r\n return dateTimeText;\r\n}\r\n/**\r\n * @description 格式化时间\r\n * @param time\r\n * @returns\r\n * @example\r\n * formatTime() -> \"08:00:00\"\r\n * formatTime(new Date()) -> \"08:00:00\"\r\n * formatTime(Date.now()) -> \"08:00:00\"\r\n */\r\nconst formatTime = (time = Date.now()) => {\r\n const date = new Date(time);\r\n const s = date.getSeconds();\r\n const min = date.getMinutes();\r\n const h = date.getHours();\r\n // 时间\r\n const timeText = [h, min, s].map(formatDateNum).join(':');\r\n return timeText;\r\n};\r\n/**\r\n * @description 时间已过\r\n * @param hour\r\n * @param minute\r\n * @returns\r\n */\r\nfunction isLate({ hour, minute }) {\r\n const date = new Date();\r\n const h = date.getHours();\r\n const min = date.getMinutes();\r\n return h > hour || (h === hour && min >= minute);\r\n}\r\n/**\r\n * @description 时间已过\r\n * @param hour\r\n * @param minute\r\n * @returns\r\n */\r\nfunction isNow({ hour, minute }) {\r\n const date = new Date();\r\n const h = date.getHours();\r\n const min = date.getMinutes();\r\n const s = date.getSeconds();\r\n return h === hour && min === minute && s === 0;\r\n}\r\n\r\n","from":"src\\utils\\time.ts","to":"time.js"},"src\\utils\\utils.ts":{"timeStamp":"2023-07-12T04:27:35.825Z","data":"/* 工具函数 */\r\n/**\r\n * @description 设置cookie\r\n * @param name\r\n * @param value\r\n * @param expires\r\n */\r\nfunction setCookie(name, value, expires, domain) {\r\n // 当前日期\r\n const date = new Date();\r\n // 过期日期\r\n date.setTime(date.getTime() + expires);\r\n // 设置cookie\r\n document.cookie = `${name}=${value};expires=${date.toUTCString()};path=/;domain=${domain}`;\r\n}\r\n/**\r\n * @description 获取cookie\r\n * @param name\r\n * @returns\r\n */\r\nfunction getCookie(name) {\r\n // 获取当前所有cookie\r\n const strCookies = document.cookie;\r\n // 截取变成cookie数组\r\n const cookieText = strCookies.split(';');\r\n // 循环每个cookie\r\n for (const i in cookieText) {\r\n // 将cookie截取成两部分\r\n const item = cookieText[i].split('=');\r\n // 判断cookie的name 是否相等\r\n if (item[0].trim() === name) {\r\n return item[1].trim();\r\n }\r\n }\r\n return null;\r\n}\r\n/**\r\n * @description 删除cookie\r\n * @param name\r\n */\r\nfunction delCookie(name, domain) {\r\n // 存在cookie\r\n const value = getCookie(name);\r\n if (value !== null) {\r\n setCookie(name, '', -1, domain);\r\n }\r\n}\r\n/**\r\n * @description 防抖\r\n * @param callback\r\n * @param delay\r\n * @returns\r\n */\r\nfunction debounce(callback, delay) {\r\n let timer = -1;\r\n return function (...args) {\r\n if (timer !== -1) {\r\n clearTimeout(timer);\r\n }\r\n timer = setTimeout(() => {\r\n callback.apply(this, args);\r\n }, delay);\r\n };\r\n}\r\n/**\r\n * @description 判断是否为移动端\r\n * @returns\r\n */\r\nfunction hasMobile() {\r\n let isMobile = false;\r\n if (navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i)) {\r\n log('移动端');\r\n isMobile = true;\r\n }\r\n if (document.body.clientWidth < 800) {\r\n log('小尺寸设备端');\r\n isMobile = true;\r\n }\r\n return isMobile;\r\n}\r\n/**\r\n * @description 等待时间\r\n * @param time\r\n * @returns\r\n */\r\nfunction sleep(time) {\r\n // 延时\r\n let timeDelay = Number(time);\r\n if (!Number.isInteger(timeDelay)) {\r\n timeDelay = 1000;\r\n }\r\n timeDelay += Math.random() * 500 - 250;\r\n return new Promise((resolve) => {\r\n setTimeout(() => {\r\n resolve(undefined);\r\n }, timeDelay);\r\n });\r\n}\r\n/**\r\n * @description 暂停学习锁\r\n */\r\nfunction studyPauseLock(callback) {\r\n return new Promise((resolve) => {\r\n // 暂停\r\n const pauseStudy = GM_getValue('pauseStudy') || false;\r\n if (pauseStudy) {\r\n const doing = setInterval(() => {\r\n // 暂停\r\n const pauseStudy = GM_getValue('pauseStudy') || false;\r\n if (!pauseStudy) {\r\n // 停止定时器\r\n clearInterval(doing);\r\n log('学习等待结束!');\r\n if (callback && callback instanceof Function) {\r\n callback(true);\r\n }\r\n resolve(true);\r\n return;\r\n }\r\n if (callback && callback instanceof Function) {\r\n callback(false);\r\n }\r\n log('学习等待...');\r\n }, 500);\r\n return;\r\n }\r\n resolve(true);\r\n });\r\n}\r\n/**\r\n * @description 加载\r\n * @param match\r\n * @param callback\r\n */\r\nfunction load(match, callback) {\r\n // 链接\r\n const { href } = window.location;\r\n window.addEventListener('load', () => {\r\n // 函数\r\n if (match instanceof Function) {\r\n match(href) && callback();\r\n return;\r\n }\r\n // 布尔\r\n if (typeof match === 'boolean') {\r\n match && callback();\r\n return;\r\n }\r\n // 字符正则\r\n if (href.match(match)) {\r\n callback();\r\n return;\r\n }\r\n });\r\n}\r\n\r\n","from":"src\\utils\\utils.ts","to":"utils.js"},"src\\shared\\index.ts":{"timeStamp":"2023-07-12T05:59:54.970Z","data":"/* 变量 */\r\n/**\r\n * @description 链接\r\n */\r\nconst href = window.location.href;\r\n/**\r\n * @description 任务配置\r\n */\r\nconst taskConfig = reactive([\r\n {\r\n title: '登录',\r\n currentScore: 0,\r\n dayMaxScore: 0,\r\n need: 0,\r\n status: false,\r\n tip: '每日首次登录积1分。',\r\n score: 0,\r\n active: true,\r\n immutable: true,\r\n type: TaskType.LOGIN,\r\n },\r\n {\r\n title: '文章选读',\r\n currentScore: 0,\r\n dayMaxScore: 0,\r\n need: 0,\r\n status: false,\r\n tip: '每有效阅读一篇文章积1分,上限6分。有效阅读文章累计1分钟积1分,上限6分。每日上限积12分。',\r\n score: 0,\r\n active: true,\r\n immutable: false,\r\n type: TaskType.READ,\r\n },\r\n {\r\n title: '视听学习',\r\n currentScore: 0,\r\n dayMaxScore: 0,\r\n need: 0,\r\n status: false,\r\n tip: '每有效一个音频或观看一个视频积1分,上限6分。有效收听音频或观看视频累计1分钟积1分,上限6分。每日上限积12分。',\r\n score: 0,\r\n active: true,\r\n immutable: false,\r\n type: TaskType.WATCH,\r\n },\r\n {\r\n title: '每日答题',\r\n currentScore: 0,\r\n dayMaxScore: 0,\r\n need: 0,\r\n status: false,\r\n tip: '每组答题每答对1道积1分。每日上限积5分。',\r\n score: 0,\r\n active: true,\r\n immutable: false,\r\n type: TaskType.PRACTICE,\r\n },\r\n]);\r\n/**\r\n * @description 设置\r\n */\r\nconst settings = reactive([\r\n false,\r\n false,\r\n false,\r\n false,\r\n false,\r\n false,\r\n false,\r\n false,\r\n]);\r\n/**\r\n * @description 总分\r\n */\r\nconst totalScore = ref(0);\r\n/**\r\n * @description 当天分数\r\n */\r\nconst todayScore = ref(0);\r\n/**\r\n * @description 用户信息\r\n */\r\nconst userinfo = reactive({\r\n nick: '',\r\n avatar: '',\r\n});\r\n/**\r\n * @description 进度\r\n */\r\nconst taskStatus = ref(TaskStatusType.LOADING);\r\n/**\r\n * @description 答题暂停\r\n */\r\nconst examPause = ref(false);\r\n/**\r\n * @description 登录\r\n */\r\nconst login = ref(!!getCookie('token'));\r\n/**\r\n * @description 窗口id\r\n */\r\nconst id = ref('');\r\n/**\r\n * @description 定时刷新列表\r\n */\r\nconst scheduleList = shallowReactive([]);\r\n/**\r\n * @description 推送token\r\n */\r\nconst pushToken = ref('');\r\n/**\r\n * @description 刷新次数\r\n */\r\nconst refreshCount = ref(0);\r\n/**\r\n * @description 窗口关闭\r\n */\r\nconst frame = reactive({\r\n title: '',\r\n show: false,\r\n exist: false,\r\n closed: true,\r\n ele: undefined,\r\n src: '',\r\n});\r\n/**\r\n * @description 页面\r\n */\r\nconst page = ref(undefined);\r\n/**\r\n * @description 开始登录\r\n */\r\nconst loginQRCodeShow = ref(false);\r\n/**\r\n * @description 最大选读时长\r\n */\r\nconst maxRead = ref(100);\r\n/**\r\n * @description 最大视听时长\r\n */\r\nconst maxWatch = ref(120);\r\n/**\r\n * @description 运行其他任务\r\n */\r\nconst running = ref(false);\r\n/**\r\n * @description 主题色\r\n */\r\nconst themeColor = ref('#fa3333');\r\n\r\n","from":"src\\shared\\index.ts","to":"index.js"},"src\\controller\\exam.ts":{"timeStamp":"2023-06-03T04:43:48.117Z","data":"/**\r\n * @description 考试类型\r\n */\r\nvar ExamType;\r\n(function (ExamType) {\r\n ExamType[ExamType[\"PRACTICE\"] = 0] = \"PRACTICE\";\r\n ExamType[ExamType[\"PAPER\"] = 1] = \"PAPER\";\r\n})(ExamType || (ExamType = {}));\r\n/**\r\n * @description 获取答题按钮\r\n */\r\nfunction getNextButton() {\r\n return new Promise((resolve) => {\r\n const timer = setInterval(() => {\r\n // 答题按钮\r\n const nextAll = $$('.ant-btn').filter((next) => next.innerText);\r\n if (nextAll.length) {\r\n // 停止定时器\r\n clearInterval(timer);\r\n if (nextAll.length === 2) {\r\n resolve(nextAll[1]);\r\n return;\r\n }\r\n resolve(nextAll[0]);\r\n }\r\n }, 500);\r\n });\r\n}\r\n/**\r\n * @description 处理滑动验证\r\n */\r\nfunction handleSlideVerify() {\r\n return new Promise(async (resolve) => {\r\n // 滑动验证\r\n const mask = $$('#nc_mask')[0];\r\n if (mask && getComputedStyle(mask).display !== 'none') {\r\n // 创建提示\r\n createTip('等待滑动验证');\r\n // 提高层级\r\n mask.style.zIndex = '999';\r\n // 轨道\r\n const track = (await $_('.nc_scale', undefined, 3000))[0];\r\n // 滑块\r\n const slide = (await $_('.btn_slide', undefined, 3000))[0];\r\n // 延时\r\n await sleep(2000);\r\n // 矩形范围\r\n const rectTrack = track.getBoundingClientRect();\r\n // 矩形范围\r\n const rectSlide = slide.getBoundingClientRect();\r\n // 窗口\r\n const window = unsafeWindow;\r\n // 范围内随机起点\r\n const start = createRandomPoint(rectSlide);\r\n // 终点\r\n const end = {\r\n x: rectTrack.x + rectTrack.width,\r\n y: rectTrack.y + rectTrack.height / 2,\r\n };\r\n // 路径\r\n const path = createRandomPath(start, end, 10);\r\n // 移动端\r\n const mobile = hasMobile();\r\n if (mobile) {\r\n slide.style.touchAction = 'none';\r\n const touchstartTouch = new Touch({\r\n identifier: 0,\r\n target: slide,\r\n clientX: path[0].x,\r\n clientY: path[0].y,\r\n });\r\n const touchstartList = [touchstartTouch];\r\n // 开始触摸\r\n const touchstart = new TouchEvent('touchstart', {\r\n targetTouches: touchstartList,\r\n touches: touchstartList,\r\n changedTouches: touchstartList,\r\n view: window,\r\n bubbles: true,\r\n });\r\n slide.dispatchEvent(touchstart);\r\n // 触摸滑动\r\n for (const i in path) {\r\n const touchmoveTouch = new Touch({\r\n identifier: 0,\r\n target: slide,\r\n clientX: path[i].x,\r\n clientY: path[i].y,\r\n });\r\n const touchmoveList = [touchmoveTouch];\r\n const touchmove = new TouchEvent('touchmove', {\r\n targetTouches: touchmoveList,\r\n touches: touchmoveList,\r\n changedTouches: touchmoveList,\r\n view: window,\r\n bubbles: true,\r\n });\r\n slide.dispatchEvent(touchmove);\r\n await sleep(10);\r\n }\r\n const touchendTouch = new Touch({\r\n identifier: 0,\r\n target: slide,\r\n clientX: path[path.length - 1].x,\r\n clientY: path[path.length - 1].y,\r\n });\r\n // 触摸结束\r\n const touchendList = [touchendTouch];\r\n // 开始触摸\r\n const touchend = new TouchEvent('touchend', {\r\n targetTouches: [],\r\n touches: [],\r\n changedTouches: touchendList,\r\n view: window,\r\n bubbles: true,\r\n });\r\n slide.dispatchEvent(touchend);\r\n }\r\n else {\r\n // 鼠标按下\r\n const mousedown = new MouseEvent('mousedown', {\r\n clientX: path[0].x,\r\n clientY: path[0].y,\r\n bubbles: true,\r\n view: window,\r\n });\r\n slide.dispatchEvent(mousedown);\r\n // 鼠标滑动\r\n for (const i in path) {\r\n const mousemove = new MouseEvent('mousemove', {\r\n clientX: path[i].x,\r\n clientY: path[i].y,\r\n bubbles: true,\r\n view: window,\r\n });\r\n slide.dispatchEvent(mousemove);\r\n await sleep(10);\r\n }\r\n // 鼠标抬起\r\n const mouseup = new MouseEvent('mouseup', {\r\n clientX: path[path.length - 1].x,\r\n clientY: path[path.length - 1].y,\r\n bubbles: true,\r\n view: window,\r\n });\r\n slide.dispatchEvent(mouseup);\r\n }\r\n // 创建提示\r\n createTip('滑动验证完成!');\r\n // 定时器\r\n const timer = setInterval(() => {\r\n // 滑动验证\r\n const mask = $$('#nc_mask')[0];\r\n if (!mask || getComputedStyle(mask).display === 'none') {\r\n log('滑动验证成功!');\r\n // 创建提示\r\n createTip('滑动验证成功!');\r\n clearInterval(timer);\r\n resolve(true);\r\n return;\r\n }\r\n resolve(false);\r\n log('滑动验证失败!');\r\n // 创建提示\r\n createTip('滑动验证失败!');\r\n }, 1000);\r\n return;\r\n }\r\n resolve(true);\r\n });\r\n}\r\n/**\r\n * @description 处理选项\r\n */\r\nfunction handleChoiceBtn(answers) {\r\n // 选项按钮\r\n const allBtns = $$('.q-answer');\r\n // 答案存在\r\n if (answers.length && allBtns.length) {\r\n // 作答\r\n return answers.every((answer) => {\r\n // 答案存在\r\n if (answer && answer.length) {\r\n // 包含答案最短长度选项\r\n let minLengthChoice;\r\n // 遍历\r\n allBtns.forEach((choice) => {\r\n // 选项文本\r\n const choiceText = choice.innerText.trim();\r\n // 无符号选项文本\r\n const unsignedChoiceText = choiceText.replaceAll(/[、,,。 ]/g, '');\r\n // 无符号答案\r\n const unsignedAnswer = answer.replaceAll(/[、,,。 ]/g, '');\r\n // 包含答案\r\n if (choiceText === answer ||\r\n choiceText.includes(answer) ||\r\n answer.includes(choiceText) ||\r\n unsignedChoiceText.includes(unsignedAnswer)) {\r\n // 最小长度选项有值\r\n if (minLengthChoice) {\r\n // 最短长度选项与当前选项比较长度\r\n if (minLengthChoice.innerText.length > choiceText.length) {\r\n minLengthChoice = choice;\r\n }\r\n }\r\n else {\r\n // 最小长度选项赋值\r\n minLengthChoice = choice;\r\n }\r\n }\r\n });\r\n // 存在选项\r\n if (minLengthChoice) {\r\n // 选择\r\n if (!minLengthChoice.classList.contains('chosen')) {\r\n minLengthChoice.click();\r\n }\r\n return true;\r\n }\r\n }\r\n return false;\r\n });\r\n }\r\n return false;\r\n}\r\n/**\r\n * @description 随机处理单选\r\n */\r\nfunction handleSingleChoiceRand() {\r\n // 选项按钮\r\n const allBtns = $$('.q-answer');\r\n // 按钮存在\r\n if (allBtns.length) {\r\n const index = ~~(Math.random() * allBtns.length);\r\n const randBtn = allBtns[index];\r\n // 选择\r\n if (!randBtn.classList.contains('chosen')) {\r\n randBtn.click();\r\n }\r\n }\r\n}\r\n/**\r\n * @description 随机处理多选\r\n */\r\nfunction handleMutiplyChoiceRand() {\r\n // 选项按钮\r\n const allBtns = $$('.q-answer');\r\n // 按钮存在\r\n if (allBtns.length) {\r\n allBtns.forEach((allBtn) => {\r\n // 选择\r\n if (!allBtn.classList.contains('chosen')) {\r\n allBtn.click();\r\n }\r\n });\r\n }\r\n}\r\n/**\r\n * @description 处理填空\r\n */\r\nconst handleBlankInput = (answers) => {\r\n // 所有填空\r\n const blanks = $$('.blank');\r\n // 答案存在\r\n if (blanks.length && answers.length) {\r\n // 填空数量和答案数量一致\r\n if (answers.length === blanks.length) {\r\n return answers.every((answer, i) => {\r\n // 答案存在\r\n if (answer && answer.length) {\r\n // 输入事件\r\n const inputEvent = new Event('input', {\r\n bubbles: true,\r\n });\r\n // 设置答案\r\n blanks[i].setAttribute('value', answer);\r\n // 触发输入input\r\n blanks[i].dispatchEvent(inputEvent);\r\n return true;\r\n }\r\n return false;\r\n });\r\n }\r\n // 填空数量为1和提示数量大于1\r\n if (blanks.length === 1 && answers.length > 1) {\r\n // 直接将所有答案整合填进去\r\n const answer = answers.join('');\r\n // 答案存在\r\n if (answer && answer.length) {\r\n // 输入事件\r\n const inputEvent = new Event('input', {\r\n bubbles: true,\r\n });\r\n // 设置答案\r\n blanks[0].setAttribute('value', answer);\r\n // 触发输入input\r\n blanks[0].dispatchEvent(inputEvent);\r\n return true;\r\n }\r\n }\r\n }\r\n return false;\r\n};\r\n/**\r\n * @description 处理填空随机\r\n */\r\nasync function handleBlankInputRand() {\r\n // 所有填空\r\n const blanks = $$('.blank');\r\n if (blanks.length) {\r\n // 输入事件\r\n const inputEvent = new Event('input', {\r\n bubbles: true,\r\n });\r\n blanks.forEach((blank) => {\r\n // 设置答案\r\n blank.setAttribute('value', '答案');\r\n // 触发输入input\r\n blank.dispatchEvent(inputEvent);\r\n });\r\n }\r\n}\r\n/**\r\n * @description 暂停锁\r\n */\r\nfunction examPauseLock(callback) {\r\n return new Promise((resolve) => {\r\n // 学习暂停\r\n const pauseStudy = (GM_getValue('pauseStudy') || false);\r\n // 全局暂停\r\n if (pauseStudy) {\r\n examPause.value = true;\r\n }\r\n // 暂停\r\n if (examPause.value) {\r\n // 创建提示\r\n createTip('已暂停, 手动开启自动答题! ', 10);\r\n const doing = setInterval(() => {\r\n if (!examPause.value) {\r\n // 停止定时器\r\n clearInterval(doing);\r\n log('答题等待结束!');\r\n if (callback && callback instanceof Function) {\r\n // 创建提示\r\n createTip('已开启, 自动答题!');\r\n callback(true);\r\n }\r\n resolve(true);\r\n return;\r\n }\r\n if (callback && callback instanceof Function) {\r\n callback(false);\r\n }\r\n log('答题等待...');\r\n }, 500);\r\n return;\r\n }\r\n resolve(true);\r\n });\r\n}\r\n/**\r\n * @description 答题\r\n */\r\nasync function doingExam(type) {\r\n // 下一个按钮\r\n let nextButton;\r\n // 下一个文本\r\n let nextText;\r\n // 保存答案\r\n let shouldSaveAnswer = false;\r\n while (true) {\r\n // 先等等再开始做题\r\n await sleep(2500);\r\n // 暂停\r\n await examPauseLock();\r\n // 获取下一个按钮\r\n nextButton = await getNextButton();\r\n // 下一个文本\r\n nextText = nextButton.innerText.replaceAll(' ', '');\r\n // 结束\r\n const finish = ['再练一次', '再来一组', '查看解析'];\r\n if (finish.includes(nextButton.innerText)) {\r\n break;\r\n }\r\n // 点击提示\r\n $$('.tips')[0]?.click();\r\n // 所有提示\r\n const allTips = $$('.line-feed font[color]');\r\n // 答案\r\n const answers = allTips.map((tip) => tip.innerText.trim());\r\n // 获取题目的文本内容\r\n const question = $$('.q-body')[0].innerText;\r\n // 等待一段时间\r\n await sleep(1500);\r\n // 暂停\r\n await examPauseLock();\r\n // 选项按钮\r\n const allBtns = $$('.q-answer');\r\n // 所有填空\r\n const blanks = $$('input[type=text][class=blank]');\r\n // 问题类型\r\n const questionType = ($$('.q-header')[0].innerText.substring(0, 3));\r\n // 暂停\r\n await examPauseLock();\r\n // 题型分类作答\r\n switch (questionType) {\r\n case '填空题': {\r\n // 根据提示作答\r\n if (answers.length) {\r\n const res = handleBlankInput(answers);\r\n // 成功\r\n if (res) {\r\n break;\r\n }\r\n }\r\n // 创建提示\r\n createTip('答案异常, 尝试网络题库获取!');\r\n log('正在获取答案...');\r\n // 尝试题库获取\r\n const answersNetwork = await getAnswer(question);\r\n log(`获取答案${answersNetwork.length ? '成功' : '失败'}!`, {\r\n question,\r\n answersNetwork,\r\n });\r\n // 根据题库作答\r\n if (answersNetwork.length) {\r\n const res = handleBlankInput(answersNetwork);\r\n // 成功\r\n if (res) {\r\n break;\r\n }\r\n }\r\n // 随机作答\r\n if (type === ExamType.PRACTICE || settings[SettingType.RANDOM_EXAM]) {\r\n log('答案不存在, 随机作答!');\r\n // 创建提示\r\n createTip('答案不存在, 随机作答!');\r\n await handleBlankInputRand();\r\n }\r\n else {\r\n // 推送\r\n const res = await pushModal({\r\n title: '学习推送',\r\n to: userinfo.nick,\r\n content: '答题存在异常, 已暂停答题!',\r\n type: 'fail',\r\n }, pushToken.value);\r\n createTip(`学习推送${res ? '成功' : '失败'}!`);\r\n // 暂停\r\n examPause.value = true;\r\n // 提交答案\r\n shouldSaveAnswer = true;\r\n }\r\n break;\r\n }\r\n case '多选题': {\r\n // 根据提示作答\r\n if (answers.length) {\r\n // 选项文本\r\n const choicesText = allBtns.map((btn) => btn.innerText);\r\n // 选项内容\r\n const choicesContent = choicesText\r\n .map((choiceText) => choiceText.split(/[A-Z]./)[1].trim())\r\n .join('');\r\n // 空格\r\n const blanks = question.match(/()/g);\r\n // 填空数量、选项数量、答案数量相同 | 选项全文等于答案全文\r\n if ((blanks && allBtns.length === blanks.length) ||\r\n question === choicesContent ||\r\n allBtns.length === 2) {\r\n // 全选\r\n allBtns.forEach((choice) => {\r\n if (!choice.classList.contains('chosen')) {\r\n choice.click();\r\n }\r\n });\r\n break;\r\n }\r\n // 选项数量大于等于答案\r\n if (allBtns.length >= answers.length) {\r\n const res = handleChoiceBtn(answers);\r\n // 成功\r\n if (res) {\r\n break;\r\n }\r\n }\r\n }\r\n // 创建提示\r\n createTip('答案异常, 尝试网络题库获取!');\r\n log('正在获取答案...');\r\n // 尝试题库获取\r\n const answersNetwork = await getAnswer(question);\r\n log(`获取答案${answersNetwork.length ? '成功' : '失败'}!`, {\r\n question,\r\n answersNetwork,\r\n });\r\n // 答案存在\r\n if (answersNetwork.length) {\r\n const res = handleChoiceBtn(answersNetwork);\r\n // 成功\r\n if (res) {\r\n break;\r\n }\r\n }\r\n // 随机作答\r\n if (type === ExamType.PRACTICE || settings[SettingType.RANDOM_EXAM]) {\r\n log('答案不存在, 随机作答!');\r\n // 创建提示\r\n createTip('答案不存在, 随机作答!');\r\n await handleMutiplyChoiceRand();\r\n }\r\n else {\r\n // 推送\r\n const res = await pushModal({\r\n title: '学习推送',\r\n to: userinfo.nick,\r\n content: '答题存在异常, 已暂停答题!',\r\n type: 'fail',\r\n }, pushToken.value);\r\n createTip(`学习推送${res ? '成功' : '失败'}!`);\r\n // 暂停\r\n examPause.value = true;\r\n // 提交答案\r\n shouldSaveAnswer = true;\r\n }\r\n break;\r\n }\r\n case '单选题': {\r\n // 根据提示作答\r\n if (answers.length) {\r\n // 创建提示为1\r\n if (answers.length === 1) {\r\n const res = handleChoiceBtn(answers);\r\n // 成功\r\n if (res) {\r\n break;\r\n }\r\n }\r\n else {\r\n // 可能的分隔符\r\n const seperator = [\r\n '',\r\n ' ',\r\n ',',\r\n ';',\r\n ',',\r\n '、',\r\n '-',\r\n '|',\r\n '+',\r\n '/',\r\n ];\r\n // 可能的答案\r\n const answersLike = seperator\r\n .map((s) => answers.join(s).trim())\r\n .filter((answer) => answer.length);\r\n // 答案存在\r\n if (answersLike.length) {\r\n // 可能答案是否正确\r\n const res = answersLike.some((answer) => {\r\n // 尝试查找点击\r\n return handleChoiceBtn([answer]);\r\n });\r\n if (res) {\r\n break;\r\n }\r\n }\r\n }\r\n }\r\n // 创建提示\r\n createTip('答案异常, 尝试网络题库获取!');\r\n log('正在获取答案...');\r\n // 尝试题库获取\r\n const answersNetwork = await getAnswer(question);\r\n log(`获取答案${answersNetwork.length ? '成功' : '失败'}!`, {\r\n question,\r\n answersNetwork,\r\n });\r\n // 存在答案\r\n if (answersNetwork.length) {\r\n // 单答案单选项\r\n if (answersNetwork.length === 1) {\r\n // 尝试查找点击\r\n const res = handleChoiceBtn(answersNetwork);\r\n if (res) {\r\n break;\r\n }\r\n }\r\n else {\r\n // 多答案单选项 选项意外拆分\r\n // 可能分隔符\r\n const seperator = ['', ' '];\r\n // 可能答案\r\n const answersLike = seperator.map((s) => answers.join(s));\r\n // 答案存在\r\n if (answersLike.every((answer) => answer.length)) {\r\n // 可能答案是否正确\r\n const res = answersLike.some((answer) => {\r\n // 尝试查找点击\r\n return handleChoiceBtn([answer]);\r\n });\r\n if (res) {\r\n break;\r\n }\r\n }\r\n }\r\n }\r\n // 随机作答\r\n if (type === ExamType.PRACTICE || settings[SettingType.RANDOM_EXAM]) {\r\n log('答案不存在, 随机作答!');\r\n // 创建提示\r\n createTip('答案不存在, 随机作答!');\r\n await handleSingleChoiceRand();\r\n }\r\n else {\r\n // 推送\r\n const res = await pushModal({\r\n title: '学习推送',\r\n to: userinfo.nick,\r\n content: '答题存在异常, 已暂停答题!',\r\n type: 'fail',\r\n }, pushToken.value);\r\n createTip(`学习推送${res ? '成功' : '失败'}!`);\r\n // 暂停\r\n examPause.value = true;\r\n // 提交答案\r\n shouldSaveAnswer = true;\r\n }\r\n break;\r\n }\r\n }\r\n // 暂停\r\n await examPauseLock();\r\n // 获取下一个按钮\r\n nextButton = await getNextButton();\r\n // 下一个文本\r\n nextText = nextButton.innerText.replaceAll(' ', '');\r\n // 需要提交答案\r\n if (shouldSaveAnswer) {\r\n // 答案\r\n const answers = [];\r\n if (questionType === '填空题') {\r\n blanks.forEach((blank) => {\r\n answers.push(blank.value);\r\n });\r\n }\r\n if (questionType === '单选题' || questionType === '多选题') {\r\n allBtns.forEach((choice) => {\r\n if (choice.classList.contains('chosen')) {\r\n // 带字母的选项\r\n const answerTemp = choice.innerText;\r\n // 从字符串中拿出答案\r\n const [, answer] = answerTemp.split('.');\r\n if (answer && answer.length) {\r\n answers.push(answer);\r\n }\r\n }\r\n });\r\n }\r\n // 答案\r\n const answer = answers.join(';');\r\n // 存在答案\r\n if (answer.length) {\r\n log('正在上传答案...');\r\n // 上传答案\r\n const res = await saveAnswer(question, answer);\r\n log(`上传答案${res ? '成功' : '失败'}!`, { question, answer });\r\n }\r\n // 重置\r\n shouldSaveAnswer = false;\r\n }\r\n // 确定\r\n if (nextText === '确定') {\r\n // 确认\r\n nextButton.click();\r\n // 等待一段时间\r\n await sleep(2000);\r\n // 暂停\r\n await examPauseLock();\r\n // 答案解析\r\n const answerBox = $$('.answer')[0];\r\n // 答题错误\r\n if (answerBox) {\r\n const answerTemp = answerBox.innerText;\r\n // 从字符串中拿出答案\r\n const [, answerText] = answerTemp.split(':');\r\n if (answerText && answerText.length) {\r\n const answer = answerText.replaceAll(' ', ';');\r\n log('正在上传答案...');\r\n // 上传答案\r\n const res = await saveAnswer(question, answer);\r\n log(`上传答案${res ? '成功' : '失败'}!`, { question, answer });\r\n }\r\n }\r\n }\r\n // 获取按钮\r\n nextButton = await getNextButton();\r\n // 下一个文本\r\n nextText = nextButton.innerText.replaceAll(' ', '');\r\n if (nextText === '下一题' || nextText === '完成' || nextText === '交卷') {\r\n // 等待一段时间\r\n await sleep(2500);\r\n // 下一题\r\n nextButton.click();\r\n }\r\n // 滑动验证\r\n await handleSlideVerify();\r\n }\r\n // 关闭任务窗口\r\n handleCloseTaskWin();\r\n}\r\n/**\r\n * @description 每日答题\r\n */\r\nasync function doExamPractice() {\r\n // 暂停\r\n await studyPauseLock();\r\n log('正在每日答题...');\r\n // 创建提示\r\n createTip('正在每日答题');\r\n // 链接\r\n const url = URL_CONFIG.examPractice;\r\n // 等待任务窗口\r\n await waitTaskWin(url, '每日答题');\r\n // 创建提示\r\n createTip('完成每日答题!');\r\n // 等待一段时间\r\n await sleep(1500);\r\n // 刷新分数数据\r\n await refreshScoreInfo();\r\n // 刷新任务数据\r\n await refreshTaskList();\r\n // 任务完成状况\r\n if (taskConfig[TaskType.PRACTICE].active &&\r\n !taskConfig[TaskType.PRACTICE].status) {\r\n log('任务未完成, 继续每日答题!');\r\n // 创建提示\r\n createTip('任务未完成, 继续每日答题!');\r\n await doExamPractice();\r\n }\r\n}\r\n/**\r\n * @description 专项练习\r\n */\r\nasync function doExamPaper() {\r\n running.value = true;\r\n log('正在专项练习...');\r\n // 创建提示\r\n createTip('正在专项练习');\r\n // id\r\n const examPaperId = await findExamPaper();\r\n if (examPaperId) {\r\n // 链接\r\n const url = `${URL_CONFIG.examPaper}?id=${examPaperId}`;\r\n log(`链接: ${url}`);\r\n // 等待窗口任务\r\n await waitTaskWin(url, '专项练习');\r\n // 创建提示\r\n createTip('完成专项练习!');\r\n running.value = false;\r\n // 同屏任务\r\n if (settings[SettingType.SAME_TAB]) {\r\n // 窗口不存在\r\n frame.exist = false;\r\n }\r\n return;\r\n }\r\n running.value = false;\r\n // 创建提示\r\n createTip('专项练习均已完成!');\r\n}\r\n/**\r\n * @description 初始化总页数属性\r\n */\r\nasync function initExam() {\r\n // 默认从第一页获取全部页属性\r\n const data = await getExamPaper(1);\r\n if (data) {\r\n // 等待\r\n await sleep(3000);\r\n return data.totalPageCount;\r\n }\r\n}\r\n/**\r\n * @description 查询专项练习列表\r\n */\r\nasync function findExamPaper() {\r\n // 获取总页数\r\n const total = await initExam();\r\n // 当前页数\r\n let current = 1;\r\n log(`正在寻找的专项练习...`);\r\n // 创建提示\r\n createTip(`正在寻找的专项练习...`);\r\n while (current <= total && current) {\r\n // 请求数据\r\n const data = await getExamPaper(current);\r\n if (data) {\r\n // 获取专项练习的列表\r\n const examPapers = data.list;\r\n for (const i in examPapers) {\r\n // 遍历查询有没有没做过的\r\n if (examPapers[i].status !== 2) {\r\n // status: 1 开始答题, 2 已满分/重新答题, 3 继续答题\r\n return examPapers[i].id;\r\n }\r\n }\r\n // 增加页码\r\n current += 1;\r\n // 等待\r\n await sleep(3000);\r\n }\r\n else {\r\n break;\r\n }\r\n }\r\n}\r\n\r\n","from":"src\\controller\\exam.ts","to":"exam.js"},"src\\controller\\frame.ts":{"timeStamp":"2023-03-04T06:19:57.161Z","data":"/**\r\n * @description 初始化主页面\r\n */\r\nfunction initMainListener() {\r\n // 监听关闭\r\n window.addEventListener('message', (msg) => {\r\n const { data } = msg;\r\n if (data.id === id.value && data.closed) {\r\n // 关闭窗口\r\n closeFrame();\r\n return;\r\n }\r\n });\r\n}\r\n/**\r\n * @description 初始化子页面\r\n */\r\nfunction initChildListener() {\r\n window.addEventListener('message', (msg) => {\r\n const { data } = msg;\r\n if (data.id && !data.closed) {\r\n // 设置窗口id\r\n id.value = data.id;\r\n log(`初始化窗口 ID: ${id.value}`);\r\n return;\r\n }\r\n });\r\n}\r\n/**\r\n * @description 打开窗口\r\n * @param url\r\n * @returns\r\n */\r\nasync function openFrame(url, title) {\r\n // 设置 URL\r\n frame.src = url;\r\n // 等待元素\r\n await $_('.egg_frame');\r\n if (frame.ele) {\r\n // id\r\n id.value = generateMix(10);\r\n // 打开\r\n frame.closed = false;\r\n // 设置标题\r\n frame.title = title || '';\r\n // 等待页面加载\r\n await waitFrameLoaded(frame.ele);\r\n // 发送窗口 ID\r\n frame.ele.contentWindow?.postMessage({ id: id.value, closed: false }, url);\r\n return true;\r\n }\r\n return false;\r\n}\r\n/**\r\n * @description 关闭窗口\r\n */\r\nfunction closeFrame() {\r\n log(`关闭窗口 ID: ${id.value}`);\r\n // 窗口显示\r\n frame.show = false;\r\n // 关闭\r\n frame.closed = true;\r\n // 标题\r\n frame.title = '';\r\n // src\r\n frame.src = '';\r\n}\r\n/**\r\n * @description 关闭 frame\r\n */\r\nfunction handleCloseFrame() {\r\n window.parent.postMessage({ id: id.value, closed: true }, URL_CONFIG.homeOrigin);\r\n}\r\n/**\r\n * @description 等待窗口任务结束\r\n * @param id\r\n * @returns\r\n */\r\nfunction waitFrameClose() {\r\n return new Promise((resolve) => {\r\n const timer = setInterval(() => {\r\n // 窗口关闭\r\n if (frame.closed) {\r\n clearInterval(timer);\r\n resolve(true);\r\n }\r\n }, 100);\r\n });\r\n}\r\n// 等待窗口加载\r\nfunction waitFrameLoaded(iframe) {\r\n return new Promise((resolve) => {\r\n iframe.addEventListener('load', () => {\r\n resolve(true);\r\n });\r\n });\r\n}\r\n/**\r\n * @description 打开新窗口\r\n */\r\nfunction openWin(url) {\r\n return GM_openInTab(url, {\r\n active: true,\r\n insert: true,\r\n setParent: true,\r\n });\r\n}\r\n/**\r\n * @description 关闭窗口\r\n */\r\nfunction closeWin() {\r\n page.value && page.value.close();\r\n}\r\n/**\r\n * @description 关闭子窗口\r\n */\r\nfunction handleCloseWin() {\r\n try {\r\n window.opener = window;\r\n const win = window.open('', '_self');\r\n win?.close();\r\n top?.close();\r\n }\r\n catch (e) { }\r\n}\r\n/**\r\n * @description 等待窗口关闭\r\n * @param newPage\r\n * @returns\r\n */\r\nfunction waitWinClose(newPage) {\r\n return new Promise((resolve) => {\r\n newPage.onclose = () => {\r\n resolve(undefined);\r\n };\r\n });\r\n}\r\n/**\r\n * @description 关闭任务窗口\r\n */\r\nfunction closeTaskWin() {\r\n // 同屏任务\r\n if (settings[SettingType.SAME_TAB] && id.value) {\r\n closeFrame();\r\n return;\r\n }\r\n // 非同屏任务\r\n closeWin();\r\n}\r\n/**\r\n * @description 关闭任务窗口\r\n */\r\nfunction handleCloseTaskWin() {\r\n // 同屏任务\r\n if (settings[SettingType.SAME_TAB] && id.value) {\r\n handleCloseFrame();\r\n return;\r\n }\r\n // 子窗口\r\n handleCloseWin();\r\n}\r\n/**\r\n * @description 打开并等待任务结束\r\n */\r\nasync function waitTaskWin(url, title) {\r\n // 同屏任务\r\n if (settings[SettingType.SAME_TAB]) {\r\n // 窗口存在\r\n frame.exist = true;\r\n // 显示窗体\r\n frame.show = !settings[SettingType.SILENT_RUN];\r\n // 新窗口\r\n const res = await openFrame(url, title);\r\n if (res) {\r\n // 等待窗口关闭\r\n await waitFrameClose();\r\n }\r\n return;\r\n }\r\n // 子页面任务\r\n page.value = openWin(url);\r\n await waitWinClose(page.value);\r\n}\r\n\r\n","from":"src\\controller\\frame.ts","to":"frame.js"},"src\\controller\\login.ts":{"timeStamp":"2023-07-04T09:31:09.444Z","data":"/**\r\n * @description 二维码刷新定时器\r\n */\r\nlet refreshTimer = -1;\r\n/**\r\n * @description 尝试登录\r\n */\r\nlet tryLoginTimer = -1;\r\n/**\r\n * @description 生成二维码\r\n */\r\nasync function getQRCode() {\r\n log('正在生成登录二维码...');\r\n const qrCode = await generateQRCode();\r\n if (qrCode) {\r\n log('生成登录二维码成功!');\r\n // 链接\r\n const url = `https://login.xuexi.cn/login/qrcommit?showmenu=false&code=${qrCode}&appId=dingoankubyrfkttorhpou`;\r\n return {\r\n code: qrCode,\r\n src: `${API_CONFIG.qrcode}?data=${encodeURIComponent(url)}`,\r\n url,\r\n };\r\n }\r\n log('生成登录二维码失败!');\r\n}\r\n/**\r\n * @description 验证登录二维码\r\n * @param code\r\n * @returns\r\n */\r\nasync function checkQRCode(code) {\r\n log('尝试用二维码登录...');\r\n // 二维码登录\r\n const res = await loginWithQRCode(code);\r\n if (res) {\r\n const { data, code, success } = res;\r\n // 临时登录验证码\r\n if (success && data) {\r\n return data;\r\n }\r\n // 二维码失效\r\n if (code === '11019') {\r\n return;\r\n }\r\n }\r\n return new Promise((resolve) => {\r\n // 清除定时\r\n clearTimeout(tryLoginTimer);\r\n // 设置定时\r\n tryLoginTimer = setTimeout(async () => {\r\n resolve(await checkQRCode(code));\r\n }, 1000);\r\n });\r\n}\r\n/**\r\n * @description 尝试二维码登录\r\n */\r\nasync function tryLogin(checkCode) {\r\n log('正在获取签名...');\r\n // 获取签名\r\n const sign = await getSign();\r\n if (sign) {\r\n // 生成uuid\r\n const uuid = crypto.randomUUID();\r\n const [, code] = checkCode.split('=');\r\n const state = `${sign}${uuid}`;\r\n // 安全检查\r\n const res = await secureCheck({ code, state });\r\n return res;\r\n }\r\n}\r\n/**\r\n * @description 刷新登录二维码\r\n */\r\nasync function handleLogin() {\r\n // 清除刷新\r\n clearInterval(refreshTimer);\r\n // 每隔一段时间刷新\r\n refreshTimer = setInterval(() => {\r\n // 刷新二维码\r\n handleLogin();\r\n }, autoRefreshQRCodeInterval);\r\n // 是否超出次数\r\n if (refreshCount.value >= maxRefreshCount) {\r\n createTip('超过最大重试次数, 登录失败!');\r\n // 重置刷新数\r\n refreshCount.value = 0;\r\n // 隐藏二维码\r\n loginQRCodeShow.value = false;\r\n // 远程推送\r\n if (settings[SettingType.REMOTE_PUSH]) {\r\n // 推送\r\n const res = await pushModal({\r\n title: '登录推送',\r\n content: '超过最大重试次数, 登录失败!',\r\n type: 'fail',\r\n }, pushToken.value);\r\n createTip(`登录推送${res ? '成功' : '失败'}!`);\r\n }\r\n return;\r\n }\r\n // 配置\r\n const imgWrap = $$('.egg_login_img_wrap')[0];\r\n // 图片\r\n const img = $$('.egg_login_img', imgWrap)[0];\r\n if (imgWrap && img) {\r\n // 刷新二维码\r\n log('刷新登录二维码!');\r\n // 刷新次数累加\r\n refreshCount.value++;\r\n // 获取二维码\r\n const qrCode = await getQRCode();\r\n if (qrCode) {\r\n // 获取连接\r\n const { src, code, url } = qrCode;\r\n // src\r\n img.src = src;\r\n // 开始登录\r\n loginQRCodeShow.value = true;\r\n // 远程推送\r\n if (settings[SettingType.REMOTE_PUSH]) {\r\n // img html\r\n const imgWrap = getImgHTML(src);\r\n // 跳转链接\r\n const aWrap = `\r\n
\r\n 或在浏览器\r\n ${getHighlightHTML('打开学习强国APP')}\r\n
\r\n `;\r\n // 推送\r\n const res = await pushModal({\r\n title: '登录推送',\r\n content: ['扫一扫, 登录学习强国!', aWrap, imgWrap],\r\n type: 'info',\r\n }, pushToken.value);\r\n createTip(`登录推送${res ? '成功' : '失败'}!`);\r\n }\r\n // 获取验证码\r\n const checkCode = await checkQRCode(code);\r\n // 验证成功\r\n if (checkCode) {\r\n // 尝试登录\r\n const loginRes = await tryLogin(checkCode);\r\n if (loginRes) {\r\n // 清除刷新\r\n clearInterval(refreshTimer);\r\n // 二维码显示\r\n loginQRCodeShow.value = false;\r\n // 登录成功\r\n log('登录成功!');\r\n // 创建提示\r\n createTip('登录成功!');\r\n // 登录成功\r\n login.value = true;\r\n // 刷新用户信息\r\n await refreshUserInfo();\r\n // 刷新分数信息\r\n await refreshScoreInfo();\r\n // 刷新任务信息\r\n await refreshTaskList();\r\n // 远程推送\r\n if (settings[SettingType.REMOTE_PUSH]) {\r\n const res = await pushModal({\r\n title: '登录推送',\r\n to: userinfo.nick,\r\n content: [\r\n '学习强国, 登录成功!',\r\n `当天积分: ${getHighlightHTML(todayScore.value)} 分`,\r\n `总积分: ${getHighlightHTML(totalScore.value)} 分`,\r\n ...taskConfig.map((task) => getProgressHTML(task.title, task.currentScore, task.dayMaxScore)),\r\n ],\r\n type: 'success',\r\n }, pushToken.value);\r\n createTip(`登录推送${res ? '成功' : '失败'}!`);\r\n }\r\n }\r\n return;\r\n }\r\n // 二维码失效\r\n log('登录二维码失效!');\r\n // 二维码失效刷新\r\n handleLogin();\r\n }\r\n }\r\n}\r\n/**\r\n * @description 退出登录\r\n */\r\nfunction handleLogout() {\r\n // 删除token\r\n delCookie('token', '.xuexi.cn');\r\n // 关闭窗口\r\n closeFrame();\r\n frame.exist = false;\r\n // 退出登录\r\n login.value = false;\r\n // 清除用户信息\r\n userinfo.nick = '';\r\n userinfo.avatar = '';\r\n // 总分\r\n totalScore.value = 0;\r\n // 当天分数\r\n todayScore.value = 0;\r\n // 任务进度重置\r\n taskConfig.forEach((task) => {\r\n task.currentScore = 0;\r\n });\r\n taskStatus.value = TaskStatusType.LOADING;\r\n // 退出登录\r\n log('退出登录');\r\n}\r\n\r\n","from":"src\\controller\\login.ts","to":"login.js"},"src\\controller\\readAndWatch.ts":{"timeStamp":"2023-07-13T13:15:08.730Z","data":"/**\r\n * @description 新闻\r\n */\r\nlet news = [];\r\n/**\r\n * @description 视频\r\n */\r\nlet videos = [];\r\n/**\r\n * @description 处理文章\r\n */\r\nasync function handleNews() {\r\n // section\r\n const sections = await $_('section', undefined, 5000);\r\n const section = sections[0];\r\n if (!(section && section.innerText.includes('系统正在维护中'))) {\r\n // 文章选读\r\n reading(0);\r\n return;\r\n }\r\n log('未找到文章!');\r\n // 提示\r\n createTip('未找到文章!');\r\n // 关闭页面\r\n handleCloseTaskWin();\r\n}\r\n/**\r\n * @description 处理视频\r\n */\r\nasync function handleVideo() {\r\n // videos\r\n const videos = await $_('video', undefined, 10000);\r\n // 视频\r\n const video = videos[0];\r\n // 播放按键\r\n const playBtn = $$('.prism-play-btn')[0];\r\n if (video && playBtn) {\r\n log('正在尝试播放视频...');\r\n // 播放超时\r\n const timeout = setTimeout(() => {\r\n log('视频播放超时!');\r\n // 提示\r\n createTip('视频播放超时!');\r\n // 关闭页面\r\n handleCloseTaskWin();\r\n }, 20000);\r\n // 设置是否静音\r\n watchEffect(() => (video.muted = settings[SettingType.VIDEO_MUTED]));\r\n // 能播放\r\n video.addEventListener('canplay', () => {\r\n const timer = setInterval(() => {\r\n // 尝试点击播放按钮播放\r\n playBtn.click();\r\n // 播放未成功\r\n if (video.paused) {\r\n // 尝试使用js的方式播放\r\n video.play();\r\n }\r\n }, 1000);\r\n video.addEventListener('playing', () => {\r\n // 清除超时定时器\r\n clearTimeout(timeout);\r\n // 清除定时器\r\n clearInterval(timer);\r\n log('播放视频成功!');\r\n // 视听学习\r\n reading(1);\r\n return;\r\n }, { once: true });\r\n }, { once: true });\r\n return;\r\n }\r\n log('未找到视频!');\r\n // 关闭页面\r\n handleCloseTaskWin();\r\n}\r\n/**\r\n * @description 读新闻或者看视频\r\n * @param type :0为新闻,1为视频\r\n */\r\nasync function reading(type) {\r\n let time = 30;\r\n // 文章选读\r\n if (type === 0) {\r\n // 章节\r\n const sections = $$('section');\r\n // 最大字数\r\n const maxTextCount = Math.max(...sections.map((s) => s.innerText.length), 200);\r\n // 预计时间\r\n const predictTime = ~~((60 * maxTextCount) / 1000);\r\n // min(predictTime, maxWatch.value) 秒后关闭页面\r\n time = Math.min(predictTime, maxRead.value);\r\n }\r\n // 视听学习\r\n if (type === 1) {\r\n // 视频\r\n const video = $$('video')[0];\r\n // 预计时间\r\n const predictTime = ~~video.duration;\r\n // min(predictTime, maxWatch.value) 秒后关闭页面\r\n time = Math.min(predictTime, maxWatch.value);\r\n }\r\n // 随机\r\n time = time - ~~(Math.random() * 10) + 5;\r\n // 第一次滚动时间\r\n const firstTime = time - (~~(Math.random() * 4) + 4);\r\n // 第二次滚动时间\r\n const secendTime = ~~(Math.random() * 4) + 8;\r\n // 窗口\r\n const window = unsafeWindow;\r\n // 创建提示\r\n const tip = createTip('距离关闭页面还剩', time, true, async (time) => {\r\n // 暂停锁\r\n await studyPauseLock((flag) => {\r\n if (type === 1) {\r\n // 视频\r\n const video = $$('video')[0];\r\n // 排除反复设置\r\n if (video.paused === !flag) {\r\n return;\r\n }\r\n // 设置播放状态\r\n video[flag ? 'play' : 'pause']();\r\n }\r\n });\r\n // 第一次滚动\r\n if (time === firstTime) {\r\n // 滚动\r\n window.scrollTo(0, 400);\r\n // 模拟滚动\r\n const scroll = new Event('scroll', {\r\n bubbles: true,\r\n });\r\n document.dispatchEvent(scroll);\r\n // 模拟滑动\r\n const mousemove = new MouseEvent('mousemove', {\r\n bubbles: true,\r\n });\r\n document.dispatchEvent(mousemove);\r\n // 模拟点击\r\n const click = new Event('click', {\r\n bubbles: true,\r\n });\r\n document.dispatchEvent(click);\r\n }\r\n // 第二次滚动\r\n if (time === secendTime) {\r\n // 滚动长度\r\n const scrollLength = document.body.scrollHeight / 2;\r\n // 滚动\r\n window.scrollTo(0, scrollLength);\r\n // 模拟滚动\r\n const scroll = new Event('scroll', {\r\n bubbles: true,\r\n });\r\n document.dispatchEvent(scroll);\r\n // 模拟滑动\r\n const mousemove = new MouseEvent('mousemove', {\r\n bubbles: true,\r\n });\r\n document.dispatchEvent(mousemove);\r\n // 模拟点击\r\n const click = new Event('click', {\r\n bubbles: true,\r\n });\r\n document.dispatchEvent(click);\r\n }\r\n });\r\n // 倒计时结束\r\n await tip.waitCountDown();\r\n // 关闭任务窗口\r\n handleCloseTaskWin();\r\n}\r\n/**\r\n * @description 获取新闻列表\r\n */\r\nasync function getNews() {\r\n // 需要学习的新闻数量\r\n const need = taskConfig[TaskType.READ].need < maxNewsNum\r\n ? taskConfig[TaskType.READ].need\r\n : maxNewsNum;\r\n log(`剩余 ${need} 个新闻`);\r\n // 获取新闻\r\n const data = await getNewsList();\r\n if (data && data.length) {\r\n // 索引\r\n let i = 0;\r\n // 最新新闻\r\n const latestItems = data.slice(0, 100);\r\n // 当前年份\r\n const currentYear = new Date().getFullYear().toString();\r\n // 查找今年新闻\r\n while (i < need) {\r\n const randomIndex = ~~(Math.random() * latestItems.length);\r\n // 新闻\r\n const item = latestItems[randomIndex];\r\n // 是否存在\r\n if (item.publishTime.startsWith(currentYear) && item.type === 'tuwen') {\r\n news[i] = item;\r\n i++;\r\n }\r\n }\r\n }\r\n else {\r\n news = [];\r\n }\r\n}\r\n/**\r\n * @description 获取视频列表\r\n */\r\nasync function getVideos() {\r\n // 需要学习的视频数量\r\n const need = taskConfig[TaskType.WATCH].need < maxVideoNum\r\n ? taskConfig[TaskType.WATCH].need\r\n : maxVideoNum;\r\n log(`剩余 ${need} 个视频`);\r\n // 获取视频\r\n const data = await getVideoList();\r\n if (data && data.length) {\r\n // 索引\r\n let i = 0;\r\n // 最新视频\r\n const latestItems = data.slice(0, 100);\r\n // 当前年份\r\n const currentYear = new Date().getFullYear().toString();\r\n // 查找今年视频\r\n while (i < need) {\r\n const randomIndex = ~~(Math.random() * latestItems.length);\r\n // 新闻\r\n const item = latestItems[randomIndex];\r\n // 是否存在\r\n if (item.publishTime.startsWith(currentYear) &&\r\n (item.type === 'shipin' || item.type === 'juji')) {\r\n videos[i] = item;\r\n i++;\r\n }\r\n }\r\n }\r\n else {\r\n videos = [];\r\n }\r\n}\r\n/**\r\n * @description 阅读文章\r\n */\r\nasync function readNews() {\r\n // 获取文章\r\n await getNews();\r\n // 观看文章\r\n for (const i in news) {\r\n // 任务关闭跳出循环\r\n if (!taskConfig[TaskType.READ].active) {\r\n return;\r\n }\r\n // 暂停\r\n await studyPauseLock();\r\n log(`正在阅读第 ${Number(i) + 1} 个新闻...`);\r\n // 创建提示\r\n createTip(`正在阅读第 ${Number(i) + 1} 个新闻`);\r\n // 链接\r\n const { url } = news[i];\r\n // 链接\r\n GM_setValue('readingUrl', url);\r\n // 等待任务窗口\r\n await waitTaskWin(url, '文章选读');\r\n // 清空链接\r\n GM_setValue('readingUrl', null);\r\n // 创建提示\r\n createTip(`完成阅读第 ${Number(i) + 1} 个新闻!`);\r\n // 等待一段时间\r\n await sleep(1500);\r\n // 刷新分数数据\r\n await refreshScoreInfo();\r\n // 刷新任务数据\r\n await refreshTaskList();\r\n // 任务完成跳出循环\r\n if (taskConfig[TaskType.READ].active && taskConfig[TaskType.READ].status) {\r\n break;\r\n }\r\n }\r\n // 任务关闭跳出循环\r\n if (!taskConfig[TaskType.READ].active) {\r\n return;\r\n }\r\n // 任务完成状况\r\n if (taskConfig[TaskType.READ].active && !taskConfig[TaskType.READ].status) {\r\n log('任务未完成, 继续阅读新闻!');\r\n // 创建提示\r\n createTip('任务未完成, 继续阅读新闻!');\r\n await readNews();\r\n }\r\n}\r\n/**\r\n * @description 观看视频\r\n */\r\nasync function watchVideo() {\r\n // 获取视频\r\n await getVideos();\r\n // 观看视频\r\n for (const i in videos) {\r\n // 任务关闭跳出循环\r\n if (!taskConfig[TaskType.WATCH].active) {\r\n return;\r\n }\r\n // 暂停\r\n await studyPauseLock();\r\n log(`正在观看第 ${Number(i) + 1} 个视频...`);\r\n // 创建提示\r\n createTip(`正在观看第 ${Number(i) + 1} 个视频`);\r\n // 链接\r\n const { url } = videos[i];\r\n // 链接\r\n GM_setValue('watchingUrl', url);\r\n // 等待任务窗口\r\n await waitTaskWin(url, '视听学习');\r\n // 清空链接\r\n GM_setValue('watchingUrl', null);\r\n // 创建提示\r\n createTip(`完成观看第 ${Number(i) + 1} 个视频!`);\r\n // 等待一段时间\r\n await sleep(1500);\r\n // 刷新分数数据\r\n await refreshScoreInfo();\r\n // 刷新任务数据\r\n await refreshTaskList();\r\n // 任务完成跳出循环\r\n if (taskConfig[TaskType.WATCH].active &&\r\n taskConfig[TaskType.WATCH].status) {\r\n break;\r\n }\r\n }\r\n // 任务关闭跳出循环\r\n if (!taskConfig[TaskType.WATCH].active) {\r\n return;\r\n }\r\n // 任务完成状况\r\n if (taskConfig[TaskType.WATCH].active && !taskConfig[TaskType.WATCH].status) {\r\n log('任务未完成, 继续观看视频!');\r\n // 创建提示\r\n createTip('任务未完成, 继续观看看视频!');\r\n await watchVideo();\r\n }\r\n}\r\n\r\n","from":"src\\controller\\readAndWatch.ts","to":"readAndWatch.js"},"src\\controller\\schedule.ts":{"timeStamp":"2023-02-25T03:17:29.804Z","data":"/**\r\n * @description 定时刷新定时器\r\n */\r\nlet scheduleTimer = -1;\r\n/**\r\n * @description 刷新定时任务\r\n */\r\nasync function refreshScheduleTask() {\r\n // 清除定时刷新\r\n clearInterval(scheduleTimer);\r\n // 未登录\r\n if (!login.value) {\r\n // 剩余定时任务\r\n const restList = scheduleList.filter((s) => !isLate(s));\r\n // 存在剩余任务\r\n if (restList.length) {\r\n const rest = restList[0];\r\n log(`已设置 ${rest.time} 的定时任务!`);\r\n // 提示\r\n createTip(`已设置 ${rest.time} 的定时任务!`);\r\n // 时间\r\n let time = 0;\r\n // 刷新间隔\r\n const interval = 10;\r\n scheduleTimer = setInterval(() => {\r\n if (!(time++ % interval)) {\r\n log('定时刷新正在运行...');\r\n }\r\n // 到达定时\r\n if (isNow(rest)) {\r\n clearInterval(scheduleTimer);\r\n log(`执行 ${rest.time} 的定时任务!`);\r\n // 提示\r\n createTip(`执行 ${rest.time} 的定时任务!`);\r\n // 登录\r\n handleLogin();\r\n }\r\n }, 1000);\r\n }\r\n }\r\n}\r\n/**\r\n * @description 清除定时\r\n */\r\nfunction clearScheduleTask() {\r\n clearInterval(scheduleTimer);\r\n}\r\n\r\n","from":"src\\controller\\schedule.ts","to":"schedule.js"},"src\\controller\\tip.ts":{"timeStamp":"2023-06-03T02:54:53.972Z","data":"/**\r\n * @description 创建学习提示\r\n */\r\nfunction createTip(text, delay = 2, countShow = false, callback) {\r\n const tipWrap = $$('.egg_tip_wrap')[0];\r\n // 提前去除\r\n const tips = $$('.egg_tip');\r\n if (tips.length) {\r\n tips.forEach((t) => t.delay());\r\n }\r\n // 延迟\r\n const delayCount = ref(delay);\r\n // 文字\r\n const textContent = ref(text);\r\n //显示\r\n const show = ref(false);\r\n // 延迟显示\r\n const delayShow = ref(false);\r\n // 销毁\r\n let destroyed = false;\r\n // 倒计时结束\r\n let done = false;\r\n // 提示\r\n const tip = Tip({\r\n text: textContent,\r\n count: delayCount,\r\n show,\r\n delayShow,\r\n countShow: ref(countShow),\r\n callback: async (count) => {\r\n callback && (await callback(count));\r\n // 恢复显示\r\n if (delayShow.value && count === delay) {\r\n delayShow.value = false;\r\n }\r\n // 倒计时结束\r\n if (count <= 0) {\r\n done = true;\r\n operate.destroy();\r\n }\r\n },\r\n });\r\n // 操作\r\n const operate = {\r\n destroy() {\r\n if (!destroyed) {\r\n // 隐藏\r\n operate.hide();\r\n // 销毁\r\n destroyed = true;\r\n return new Promise((resolve) => {\r\n setTimeout(() => {\r\n tip.ele.remove();\r\n resolve(undefined);\r\n }, 300);\r\n });\r\n }\r\n },\r\n hide() {\r\n if (!destroyed) {\r\n show.value = false;\r\n }\r\n },\r\n show() {\r\n if (!destroyed) {\r\n return new Promise((resolve) => {\r\n setTimeout(() => {\r\n show.value = true;\r\n resolve(undefined);\r\n }, 300);\r\n });\r\n }\r\n },\r\n setText(text) {\r\n if (!destroyed) {\r\n textContent.value = text;\r\n }\r\n },\r\n waitCountDown() {\r\n return new Promise((resolve) => {\r\n // 计时器\r\n const timer = setInterval(() => {\r\n // 结束\r\n if (done) {\r\n clearInterval(timer);\r\n resolve(true);\r\n }\r\n }, 100);\r\n });\r\n },\r\n delay() {\r\n if (!destroyed) {\r\n delayShow.value = true;\r\n delayCount.value += 2;\r\n }\r\n },\r\n };\r\n Object.assign(tip.ele, operate);\r\n // 插入节点\r\n mountElement(tip, tipWrap);\r\n // 显示\r\n operate.show();\r\n return operate;\r\n}\r\n\r\n","from":"src\\controller\\tip.ts","to":"tip.js"},"src\\controller\\user.ts":{"timeStamp":"2023-07-04T09:34:59.201Z","data":"/**\r\n * @description 刷新用户信息\r\n */\r\nasync function refreshUserInfo() {\r\n // 未登录\r\n if (!login.value) {\r\n throw new Error('用户未登录!');\r\n }\r\n // 已存在信息\r\n if (userinfo.nick) {\r\n return true;\r\n }\r\n log('加载用户信息...');\r\n // 获取用户信息\r\n const res = await getUserInfo();\r\n if (res) {\r\n const { avatarMediaUrl = '', nick: nickRes } = res;\r\n if (nickRes) {\r\n // 设置昵称\r\n userinfo.nick = nickRes;\r\n // 设置头像\r\n userinfo.avatar = avatarMediaUrl;\r\n return true;\r\n }\r\n }\r\n log('加载用户信息失败!');\r\n return false;\r\n}\r\n/**\r\n * @description 刷新分数信息\r\n */\r\nasync function refreshScoreInfo() {\r\n // 未登录\r\n if (!login.value) {\r\n throw new Error('用户未登录!');\r\n }\r\n log('加载分数信息...');\r\n // 获取总分\r\n const totalScoreRes = await getTotalScore();\r\n // 获取当天总分\r\n const todayScoreRes = await getTodayScore();\r\n // 整数值\r\n if (Number.isInteger(totalScoreRes) && Number.isInteger(todayScoreRes)) {\r\n // 设置分数\r\n totalScore.value = totalScoreRes;\r\n todayScore.value = todayScoreRes;\r\n return true;\r\n }\r\n log('加载分数信息失败!');\r\n return false;\r\n}\r\n/**\r\n * @description 刷新任务列表\r\n */\r\nasync function refreshTaskList() {\r\n // 未登录\r\n if (!login.value) {\r\n throw new Error('用户未登录!');\r\n }\r\n log('加载任务进度...');\r\n // 原始任务进度\r\n const taskProgress = await getTaskList();\r\n if (taskProgress) {\r\n // 登录\r\n taskConfig[TaskType.LOGIN].currentScore = taskProgress[2].currentScore;\r\n taskConfig[TaskType.LOGIN].dayMaxScore = taskProgress[2].dayMaxScore;\r\n taskConfig[TaskType.LOGIN].need =\r\n taskProgress[2].dayMaxScore - taskProgress[2].currentScore;\r\n // 文章选读\r\n taskConfig[TaskType.READ].currentScore = taskProgress[0].currentScore;\r\n taskConfig[TaskType.READ].dayMaxScore = taskProgress[0].dayMaxScore;\r\n taskConfig[TaskType.READ].need =\r\n taskProgress[0].dayMaxScore - taskProgress[0].currentScore;\r\n // 视听学习\r\n taskConfig[TaskType.WATCH].currentScore = taskProgress[1].currentScore;\r\n taskConfig[TaskType.WATCH].dayMaxScore = taskProgress[1].dayMaxScore;\r\n taskConfig[TaskType.WATCH].need =\r\n taskProgress[1].dayMaxScore - taskProgress[1].currentScore;\r\n // 每日答题\r\n taskConfig[TaskType.PRACTICE].currentScore = taskProgress[3].currentScore;\r\n taskConfig[TaskType.PRACTICE].dayMaxScore = taskProgress[3].dayMaxScore;\r\n taskConfig[TaskType.PRACTICE].need = taskProgress[3].dayMaxScore;\r\n // 更新数据\r\n for (const i in taskConfig) {\r\n const { currentScore, dayMaxScore } = taskConfig[i];\r\n // 进度\r\n const rate = Number(((100 * currentScore) / dayMaxScore).toFixed(1));\r\n // 分数\r\n taskConfig[i].score = currentScore;\r\n // 完成状态\r\n taskConfig[i].status = rate === 100;\r\n }\r\n return;\r\n }\r\n // 重试\r\n await sleep(2000);\r\n refreshTaskList();\r\n return;\r\n}\r\n\r\n","from":"src\\controller\\user.ts","to":"user.js"},"src\\component\\Tip.ts":{"timeStamp":"2023-03-08T03:09:22.868Z","data":"function Tip({ text, count, show, delayShow, countShow, callback, }) {\r\n return createElementNode('div', undefined, {\r\n class: watchRef([show, delayShow], () => `egg_tip${show.value ? (delayShow.value ? ' active delay' : ' active') : ''}`),\r\n }, [\r\n createElementNode('span', undefined, {\r\n class: 'egg_text',\r\n }, createTextNode(text)),\r\n watchEffectRef(() => countShow.value\r\n ? createElementNode('span', undefined, {\r\n class: 'egg_countdown',\r\n }, createTextNode(watchEffectRef(() => `${count.value}s`)))\r\n : undefined),\r\n ], {\r\n onMounted() {\r\n // 倒计时\r\n const countDown = async () => {\r\n // 倒计时回调\r\n await callback(count.value);\r\n // 倒计时结束\r\n if (!count.value) {\r\n show.value = false;\r\n return;\r\n }\r\n count.value--;\r\n setTimeout(countDown, 1000);\r\n };\r\n countDown();\r\n },\r\n });\r\n}\r\n\r\n","from":"src\\component\\Tip.ts","to":"Tip.js"},"src\\component\\Hr.ts":{"timeStamp":"2023-02-17T10:07:48.523Z","data":"/**\r\n * @description 分隔符\r\n * @returns\r\n */\r\nfunction Hr({ text }) {\r\n return createElementNode('div', undefined, {\r\n class: 'egg_hr_wrap',\r\n }, [\r\n createElementNode('div', undefined, { class: 'egg_hr' }),\r\n createElementNode('div', undefined, { class: 'egg_hr_title' }, createTextNode(text)),\r\n createElementNode('div', undefined, { class: 'egg_hr' }),\r\n ]);\r\n}\r\n\r\n","from":"src\\component\\Hr.ts","to":"Hr.js"},"src\\component\\Select.ts":{"timeStamp":"2023-03-08T14:04:23.134Z","data":"function Select({ data, maxlength, placeholder = '', onchange, onblur, value, keep, }) {\r\n const selectData = reactive(data.map((v) => ({ selected: false, active: false, ele: undefined, ...v })));\r\n const focus = ref(false);\r\n const input = shallowRef(undefined);\r\n const list = shallowRef(undefined);\r\n const valueRef = ref('');\r\n value &&\r\n watch(value, () => {\r\n const item = selectData.find((v) => v.value === value.value);\r\n valueRef.value = item ? item.label : '';\r\n if (!item) {\r\n selectData.forEach((v) => (v.selected = false));\r\n list.value && (list.value.scrollTop = 0);\r\n }\r\n }, true);\r\n return createElementNode('div', undefined, {\r\n class: 'egg_select',\r\n }, [\r\n createElementNode('input', { value: valueRef }, {\r\n class: 'egg_select_input',\r\n type: 'text',\r\n placeholder,\r\n maxlength,\r\n ref: input,\r\n onfocus() {\r\n if (list.value && input.value) {\r\n focus.value = true;\r\n if (input.value.value && valueRef.value) {\r\n const index = selectData.findIndex((v) => v.label === valueRef.value);\r\n if (index + 1) {\r\n list.value.scrollTop = selectData[index].ele?.offsetTop || 0;\r\n selectData.forEach((v, i) => (v.selected = i === index));\r\n }\r\n return;\r\n }\r\n }\r\n },\r\n oninput() {\r\n if (list.value && input.value) {\r\n const { value } = input.value;\r\n // 文本存在\r\n if (value) {\r\n const index = selectData.findIndex((v) => v.label.includes(value));\r\n // 存在匹配\r\n if (index + 1) {\r\n list.value.scrollTop = selectData[index].ele?.offsetTop || 0;\r\n selectData.forEach((v, i) => {\r\n v.active = i === index;\r\n v.active &&\r\n setTimeout(() => {\r\n v.active = false;\r\n }, 300);\r\n });\r\n }\r\n return;\r\n }\r\n // 清除\r\n selectData.forEach((v) => (v.active = v.selected = false));\r\n list.value.scrollTop = 0;\r\n }\r\n },\r\n onblur() {\r\n if (list.value && input.value) {\r\n const item = selectData.find((v) => v.selected);\r\n // 关闭选项\r\n if (item || !input.value.value) {\r\n setTimeout(() => {\r\n focus.value = false;\r\n }, 100);\r\n }\r\n // 恢复文本\r\n if (item && input.value.value !== item.label) {\r\n input.value.value = item.label;\r\n }\r\n // 保留文本\r\n if (!item && keep) {\r\n input.value.value = valueRef.value;\r\n }\r\n onblur &&\r\n onblur(item ? { label: item.label, value: item.value } : undefined);\r\n }\r\n },\r\n }),\r\n createElementNode('div', undefined, {\r\n class: watchEffectRef(() => `egg_select_list${focus.value ? '' : ' hide'}`),\r\n ref: list,\r\n }, selectData.map((v, index) => createElementNode('div', undefined, {\r\n class: watchRef(() => [v.selected, v.active], () => `egg_select_item${v.selected ? ' selected' : v.active ? ' active' : ''}`),\r\n ref: (e) => (v.ele = e),\r\n onclick: debounce(() => {\r\n if (valueRef.value !== v.label) {\r\n onchange && onchange({ label: v.label, value: v.value });\r\n selectData.forEach((v, i) => {\r\n v.selected = i === index;\r\n v.selected && (valueRef.value = v.label);\r\n });\r\n }\r\n focus.value = false;\r\n }, 300),\r\n }, createTextNode(v.label)))),\r\n ]);\r\n}\r\n\r\n","from":"src\\component\\Select.ts","to":"Select.js"},"src\\component\\ExamBtn.ts":{"timeStamp":"2023-07-12T06:10:48.404Z","data":"/**\r\n * @description 答题按钮\r\n */\r\nfunction ExamBtn() {\r\n // 设置初始状态\r\n watchEffect(() => (examPause.value = !settings[SettingType.AUTO_ANSWER]));\r\n return createElementNode('button', undefined, {\r\n class: watchEffectRef(() => `egg_exam_btn${examPause.value ? ' manual' : ''}`),\r\n type: 'button',\r\n onclick(e) {\r\n e.stopPropagation();\r\n examPause.value = !examPause.value;\r\n },\r\n onmousedown(e) {\r\n e.stopPropagation();\r\n },\r\n onmousemove(e) {\r\n e.stopPropagation();\r\n },\r\n onmouseup(e) {\r\n e.stopPropagation();\r\n },\r\n onmouseenter(e) {\r\n e.stopPropagation();\r\n },\r\n onmouseleave(e) {\r\n e.stopPropagation();\r\n },\r\n onmouseover(e) {\r\n e.stopPropagation();\r\n },\r\n ontouchstart(e) {\r\n e.stopPropagation();\r\n },\r\n ontouchmove(e) {\r\n e.stopPropagation();\r\n },\r\n ontouchend(e) {\r\n e.stopPropagation();\r\n },\r\n oninput(e) {\r\n e.stopPropagation();\r\n },\r\n onchange(e) {\r\n e.stopPropagation();\r\n },\r\n onblur(e) {\r\n e.stopPropagation();\r\n },\r\n }, createTextNode(watchEffectRef(() => `${examPause.value ? '开启自动答题' : '关闭自动答题'}`)));\r\n}\r\n\r\n","from":"src\\component\\ExamBtn.ts","to":"ExamBtn.js"},"src\\component\\Frame.ts":{"timeStamp":"2023-07-13T13:32:54.161Z","data":"/**\r\n * @description 任务窗口\r\n * @returns\r\n */\r\nfunction Frame() {\r\n // 最大化\r\n const max = ref(false);\r\n // 容器\r\n return createElementNode('div', undefined, {\r\n class: watchEffectRef(() => `egg_frame_wrap${frame.show ? '' : ' hide'}`),\r\n onclick(e) {\r\n e.stopPropagation();\r\n },\r\n onmousedown(e) {\r\n e.stopPropagation();\r\n },\r\n onmousemove(e) {\r\n e.stopPropagation();\r\n },\r\n onmouseup(e) {\r\n e.stopPropagation();\r\n },\r\n onmouseenter(e) {\r\n e.stopPropagation();\r\n },\r\n onmouseleave(e) {\r\n e.stopPropagation();\r\n },\r\n onmouseover(e) {\r\n e.stopPropagation();\r\n },\r\n ontouchstart(e) {\r\n e.stopPropagation();\r\n },\r\n ontouchmove(e) {\r\n e.stopPropagation();\r\n },\r\n ontouchend(e) {\r\n e.stopPropagation();\r\n },\r\n oninput(e) {\r\n e.stopPropagation();\r\n },\r\n onchange(e) {\r\n e.stopPropagation();\r\n },\r\n onblur(e) {\r\n e.stopPropagation();\r\n },\r\n }, watchRef(() => [login.value, settings[SettingType.SAME_TAB]], () => {\r\n // 同屏任务\r\n if (login.value && settings[SettingType.SAME_TAB]) {\r\n return [\r\n // 遮罩\r\n createElementNode('div', undefined, { class: 'egg_frame_mask' }),\r\n // 窗口内容\r\n createElementNode('div', undefined, {\r\n class: watchEffectRef(() => `egg_frame_content_wrap ${max.value ? ' max' : ''}`),\r\n }, [\r\n // 窗口控制\r\n createElementNode('div', undefined, { class: 'egg_frame_controls_wrap' }, [\r\n // 标题\r\n createElementNode('div', undefined, {\r\n class: 'egg_frame_title',\r\n }),\r\n createElementNode('div', undefined, {\r\n class: 'egg_frame_controls',\r\n }, [\r\n // 隐藏\r\n createElementNode('button', undefined, {\r\n class: 'egg_frame_btn',\r\n type: 'button',\r\n title: '隐藏',\r\n onclick: debounce(() => {\r\n // 隐藏窗口\r\n frame.show = false;\r\n }, 300),\r\n }, createNSElementNode('svg', undefined, {\r\n viewBox: '0 0 1024 1024',\r\n class: 'egg_icon',\r\n }, createNSElementNode('path', undefined, {\r\n 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',\r\n }))),\r\n // 改变大小\r\n createElementNode('button', undefined, {\r\n class: 'egg_frame_btn',\r\n type: 'button',\r\n title: '缩放',\r\n onclick: debounce(() => {\r\n max.value = !max.value;\r\n }, 300),\r\n }, createNSElementNode('svg', undefined, {\r\n viewBox: '0 0 1024 1024',\r\n class: 'egg_icon',\r\n }, createNSElementNode('path', undefined, {\r\n 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',\r\n }))),\r\n // 关闭\r\n createElementNode('button', undefined, {\r\n class: 'egg_frame_btn',\r\n type: 'button',\r\n title: '关闭',\r\n onclick: debounce(() => {\r\n // 关闭窗口\r\n closeFrame();\r\n }, 300),\r\n }, createNSElementNode('svg', undefined, {\r\n viewBox: '0 0 1024 1024',\r\n class: 'egg_icon',\r\n }, createNSElementNode('path', undefined, {\r\n 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',\r\n }))),\r\n ]),\r\n ]),\r\n // 窗口内容\r\n createElementNode('div', undefined, {\r\n class: 'egg_frame_content',\r\n }, watchEffectRef(() => frame.src\r\n ? [\r\n createElementNode('iframe', undefined, {\r\n class: 'egg_frame',\r\n src: frame.src,\r\n ref(ele) {\r\n frame.ele = ele;\r\n },\r\n }, undefined),\r\n ]\r\n : undefined)),\r\n ], {\r\n onMounted() {\r\n // 隐藏窗口\r\n watch(() => [\r\n taskStatus.value,\r\n running.value,\r\n settings[SettingType.SAME_TAB],\r\n settings[SettingType.SILENT_RUN],\r\n ], () => {\r\n // 同屏任务\r\n if (settings[SettingType.SAME_TAB] &&\r\n (taskStatus.value === TaskStatusType.START ||\r\n taskStatus.value === TaskStatusType.PAUSE ||\r\n running.value)) {\r\n // 设置窗口显示\r\n frame.show = !settings[SettingType.SILENT_RUN];\r\n }\r\n });\r\n },\r\n }),\r\n ];\r\n }\r\n }), {\r\n onMounted() {\r\n // 关闭窗口\r\n watch(() => [login.value, settings[SettingType.SAME_TAB]], () => {\r\n if (login.value) {\r\n if (settings[SettingType.SAME_TAB]) {\r\n frame.exist = true;\r\n closeWin();\r\n }\r\n else {\r\n closeFrame();\r\n frame.exist = false;\r\n }\r\n }\r\n else {\r\n closeWin();\r\n closeFrame();\r\n frame.exist = false;\r\n }\r\n });\r\n },\r\n });\r\n}\r\n\r\n","from":"src\\component\\Frame.ts","to":"Frame.js"},"src\\component\\LoginItem.ts":{"timeStamp":"2023-03-04T06:24:38.622Z","data":"/**\r\n * @description 登录\r\n */\r\nfunction LoginItem() {\r\n return watchEffectRef(() => {\r\n return login.value\r\n ? undefined\r\n : createElementNode('div', undefined, {\r\n class: 'egg_login_item',\r\n }, [\r\n // 登录按钮\r\n createElementNode('button', undefined, {\r\n type: 'button',\r\n class: 'egg_login_btn',\r\n onclick: debounce(async () => {\r\n // 开始登录\r\n handleLogin();\r\n }, 300),\r\n }, createTextNode('扫码登录')),\r\n // 窗口\r\n createElementNode('div', undefined, {\r\n class: watchEffectRef(() => `egg_login_img_wrap${loginQRCodeShow.value ? ' active' : ''}`),\r\n }, createElementNode('img', undefined, {\r\n class: 'egg_login_img',\r\n })),\r\n ], {\r\n onMounted() {\r\n watch(() => settings[SettingType.SCHEDULE_RUN], () => {\r\n // 未开启定时展示二维码\r\n if (!settings[SettingType.SCHEDULE_RUN]) {\r\n // 开始登录\r\n handleLogin();\r\n }\r\n }, true);\r\n },\r\n });\r\n });\r\n}\r\n\r\n","from":"src\\component\\LoginItem.ts","to":"LoginItem.js"},"src\\component\\InfoItem.ts":{"timeStamp":"2023-02-22T15:18:21.583Z","data":"/**\r\n * @description 信息\r\n * @returns\r\n */\r\nfunction InfoItem() {\r\n return watchEffectRef(() => {\r\n if (login.value) {\r\n return createElementNode('div', undefined, {\r\n class: 'egg_info_item',\r\n }, [\r\n // 用户信息\r\n createElementNode('div', undefined, { class: 'egg_userinfo' }, [\r\n // 头像\r\n createElementNode('div', undefined, { class: 'egg_avatar' }, watchEffectRef(() => {\r\n return [\r\n userinfo.avatar\r\n ? createElementNode('img', undefined, {\r\n src: userinfo.avatar,\r\n class: 'egg_avatar_img',\r\n })\r\n : createElementNode('div', undefined, {\r\n class: 'egg_avatar_nick',\r\n }, createTextNode(watchEffectRef(() => userinfo.nick.substring(1, 3)))),\r\n ];\r\n })),\r\n // 昵称\r\n createElementNode('div', undefined, { class: 'egg_nick' }, createTextNode(watchEffectRef(() => userinfo.nick))),\r\n ]),\r\n // 退出按钮\r\n createElementNode('button', undefined, {\r\n type: 'button',\r\n class: 'egg_login_btn',\r\n onclick: debounce(() => {\r\n // 退出登录\r\n handleLogout();\r\n }, 300),\r\n }, createTextNode('退出')),\r\n ], {\r\n onMounted() {\r\n // 刷新用户信息\r\n refreshUserInfo();\r\n },\r\n });\r\n }\r\n });\r\n}\r\n\r\n","from":"src\\component\\InfoItem.ts","to":"InfoItem.js"},"src\\component\\ScoreItem.ts":{"timeStamp":"2023-06-02T03:32:53.159Z","data":"/**\r\n * @description 分数详情\r\n */\r\nfunction ScoreItem() {\r\n return watchEffectRef(() => {\r\n if (login.value) {\r\n // 分数显示\r\n const scoreShow = ref(false);\r\n // 分数信息\r\n return createElementNode('div', undefined, {\r\n class: 'egg_score_item',\r\n }, createElementNode('div', undefined, { class: 'egg_scoreinfo' }, [\r\n createElementNode('div', undefined, {\r\n class: 'egg_totalscore',\r\n }, [\r\n createTextNode('总积分'),\r\n createElementNode('span', undefined, undefined, createTextNode(totalScore)),\r\n ]),\r\n createElementNode('div', undefined, {\r\n class: 'egg_todayscore',\r\n }, [\r\n createElementNode('button', undefined, {\r\n type: 'button',\r\n class: 'egg_todayscore_btn',\r\n title: '查看分数详情',\r\n onclick: debounce(() => {\r\n scoreShow.value = !scoreShow.value;\r\n }, 300),\r\n onblur: () => {\r\n scoreShow.value = false;\r\n },\r\n }, [\r\n createTextNode('当天分数'),\r\n // 当天分数\r\n createElementNode('span', undefined, undefined, createTextNode(todayScore)),\r\n // icon\r\n createNSElementNode('svg', undefined, {\r\n viewBox: '0 0 1024 1024',\r\n class: 'egg_icon',\r\n }, createNSElementNode('path', undefined, {\r\n 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',\r\n })),\r\n createElementNode('div', undefined, {\r\n class: watchEffectRef(() => `egg_score_details${scoreShow.value ? '' : ' hide'}`),\r\n }, [\r\n createElementNode('div', undefined, { class: 'egg_score_title' }, [\r\n createNSElementNode('svg', undefined, {\r\n viewBox: '0 0 1024 1024',\r\n class: 'egg_icon',\r\n }, [\r\n createNSElementNode('path', undefined, {\r\n d: 'M314.81 304.01h415.86v58.91H314.81zM314.81 440.24h415.86v58.91H314.81z',\r\n }),\r\n createNSElementNode('path', undefined, {\r\n 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',\r\n }),\r\n ]),\r\n createElementNode('div', undefined, {\r\n class: 'egg_score_title_text',\r\n }, createTextNode('积分详情')),\r\n ]),\r\n ...taskConfig.map((task) => createElementNode('div', undefined, { class: 'egg_score_item' }, [\r\n createTextNode(task.title),\r\n createElementNode('span', undefined, {\r\n class: 'egg_score_detail',\r\n }, createTextNode(watchEffectRef(() => task.score))),\r\n ])),\r\n ]),\r\n ]),\r\n ]),\r\n ]), {\r\n onMounted() {\r\n // 刷新分数信息\r\n refreshScoreInfo();\r\n },\r\n });\r\n }\r\n });\r\n}\r\n\r\n","from":"src\\component\\ScoreItem.ts","to":"ScoreItem.js"},"src\\component\\NoramlItem.ts":{"timeStamp":"2023-07-14T10:51:00.944Z","data":"/**\r\n * @description 设置普通项\r\n * @returns\r\n */\r\nfunction NormalItem({ title, tip, checked, onchange, }) {\r\n return createElementNode('div', undefined, { class: 'egg_setting_item' }, [\r\n createElementNode('div', undefined, { class: 'egg_label_wrap' }, [\r\n createElementNode('label', undefined, { class: 'egg_task_title' }, [\r\n createTextNode(title),\r\n createElementNode('span', undefined, {\r\n class: 'egg_detail',\r\n title: tip,\r\n }, createTextNode('i')),\r\n ]),\r\n ]),\r\n createElementNode('input', undefined, {\r\n title: tip,\r\n class: 'egg_switch',\r\n type: 'checkbox',\r\n checked,\r\n onchange,\r\n }),\r\n ]);\r\n}\r\n\r\n","from":"src\\component\\NoramlItem.ts","to":"NoramlItem.js"},"src\\component\\TaskItem.ts":{"timeStamp":"2023-07-04T09:36:52.245Z","data":"/**\r\n * @description 设置任务项\r\n * @returns\r\n */\r\nfunction TaskItem({ title, tip, checked, currentScore, dayMaxScore, onchange, immutable, }) {\r\n return createElementNode('div', undefined, {\r\n class: 'egg_task_item',\r\n }, [\r\n createElementNode('div', undefined, { class: 'egg_label_wrap' }, [\r\n createElementNode('div', undefined, { class: 'egg_task_title_wrap' }, [\r\n createElementNode('div', undefined, { class: 'egg_task_title' }, createTextNode(title)),\r\n createElementNode('div', undefined, { class: 'egg_task_progress_wrap' }, [\r\n createElementNode('div', undefined, {\r\n class: 'egg_task_current',\r\n }, createTextNode(currentScore)),\r\n createElementNode('div', undefined, {\r\n class: 'egg_task_max',\r\n }, createTextNode(watchEffectRef(() => `/${dayMaxScore.value}`))),\r\n ]),\r\n ]),\r\n createElementNode('div', undefined, { class: 'egg_progress' }, [\r\n createElementNode('div', undefined, { class: 'egg_track' }, createElementNode('div', undefined, {\r\n class: 'egg_bar',\r\n style: watchEffectRef(() => `width: ${((100 * currentScore.value) /\r\n dayMaxScore.value).toFixed(1)}%;`),\r\n })),\r\n ]),\r\n ]),\r\n createElementNode('input', undefined, {\r\n title: tip,\r\n class: 'egg_switch',\r\n type: 'checkbox',\r\n checked,\r\n onchange,\r\n disabled: immutable,\r\n }),\r\n ]);\r\n}\r\n\r\n","from":"src\\component\\TaskItem.ts","to":"TaskItem.js"},"src\\component\\TaskList.ts":{"timeStamp":"2023-07-04T09:37:38.008Z","data":"/**\r\n * @description 任务\r\n */\r\nfunction TaskList() {\r\n // 处理任务设置变化\r\n const handleTaskChange = (e, type, title) => {\r\n // 开关\r\n const { checked } = e.target;\r\n if (taskConfig[type].active !== checked) {\r\n taskConfig[type].active = checked;\r\n // 设置\r\n GM_setValue('taskConfig', JSON.stringify(taskConfig));\r\n // 创建提示\r\n createTip(`${title} ${checked ? '打开' : '关闭'}!`);\r\n }\r\n };\r\n // 登录加载\r\n watch(login, async () => {\r\n if (login.value) {\r\n // 加载任务列表\r\n await refreshTaskList();\r\n // 未完成任务\r\n if (taskConfig.some((task) => task.active && !task.status)) {\r\n // 全局暂停\r\n GM_setValue('pauseStudy', false);\r\n // 加载完毕\r\n taskStatus.value = TaskStatusType.LOADED;\r\n return;\r\n }\r\n // 任务完毕\r\n taskStatus.value = TaskStatusType.FINISH;\r\n }\r\n }, true);\r\n return createElementNode('div', undefined, {\r\n class: 'egg_task_list',\r\n }, taskConfig.map((label) => label.immutable\r\n ? TaskItem({\r\n title: label.title,\r\n tip: label.tip,\r\n checked: watchEffectRef(() => label.active),\r\n currentScore: watchEffectRef(() => label.currentScore),\r\n dayMaxScore: watchEffectRef(() => label.dayMaxScore),\r\n onchange: debounce((e) => {\r\n handleTaskChange(e, label.type, label.title);\r\n }, 300),\r\n immutable: label.immutable,\r\n })\r\n : TaskItem({\r\n title: label.title,\r\n tip: label.tip,\r\n checked: watchEffectRef(() => label.active),\r\n currentScore: watchEffectRef(() => label.currentScore),\r\n dayMaxScore: watchEffectRef(() => label.dayMaxScore),\r\n onchange: debounce((e) => {\r\n handleTaskChange(e, label.type, label.title);\r\n }, 300),\r\n immutable: label.immutable,\r\n })));\r\n}\r\n\r\n","from":"src\\component\\TaskList.ts","to":"TaskList.js"},"src\\component\\TaskBtn.ts":{"timeStamp":"2023-07-13T12:00:01.869Z","data":"/**\r\n * @description 任务按钮\r\n */\r\nfunction TaskBtn() {\r\n return watchEffectRef(() => {\r\n if (login.value) {\r\n /**\r\n * @description 学习\r\n */\r\n async function study() {\r\n // 创建提示\r\n createTip('开始学习!');\r\n // 暂停\r\n await studyPauseLock();\r\n // 文章选读\r\n if (taskConfig[TaskType.READ].active &&\r\n !taskConfig[TaskType.READ].status) {\r\n log('任务一: 文章选读');\r\n // 创建提示\r\n createTip('任务一: 文章选读');\r\n // 暂停\r\n await studyPauseLock();\r\n // 看新闻\r\n await readNews();\r\n }\r\n log('任务一: 文章选读已完成!');\r\n // 视听学习\r\n if (taskConfig[TaskType.WATCH].active &&\r\n !taskConfig[TaskType.WATCH].status) {\r\n log('任务二: 视听学习');\r\n // 创建提示\r\n createTip('任务二: 视听学习');\r\n // 暂停\r\n await studyPauseLock();\r\n // 看视频\r\n await watchVideo();\r\n }\r\n log('任务二: 视听学习已完成!');\r\n // 每日答题\r\n if (taskConfig[TaskType.PRACTICE].active &&\r\n !taskConfig[TaskType.PRACTICE].status) {\r\n log('任务三: 每日答题');\r\n // 创建提示\r\n createTip('任务三: 每日答题');\r\n // 暂停\r\n await studyPauseLock();\r\n // 做每日答题\r\n await doExamPractice();\r\n }\r\n log('任务三: 每日答题已完成!');\r\n }\r\n /**\r\n * @description 暂停任务\r\n */\r\n function pauseTask() {\r\n // 全局暂停\r\n GM_setValue('pauseStudy', true);\r\n taskStatus.value = TaskStatusType.PAUSE;\r\n }\r\n /**\r\n * @description 继续任务\r\n */\r\n function continueTask() {\r\n // 全局暂停\r\n GM_setValue('pauseStudy', false);\r\n taskStatus.value = TaskStatusType.START;\r\n }\r\n /**\r\n * @description 开始任务\r\n */\r\n async function startTask() {\r\n // 未完成任务\r\n if (taskConfig.some((task) => task.active && !task.status)) {\r\n // 开始任务\r\n taskStatus.value = TaskStatusType.START;\r\n try {\r\n // 学习\r\n await study();\r\n // 同屏任务\r\n if (settings[SettingType.SAME_TAB]) {\r\n // 关闭窗口\r\n closeFrame();\r\n // 窗口不存在\r\n frame.exist = false;\r\n }\r\n }\r\n catch (err) {\r\n if (err instanceof Error) {\r\n // 提示\r\n createTip(err.message);\r\n // 错误\r\n error(err.message);\r\n return;\r\n }\r\n // 提示\r\n createTip(String(err));\r\n // 错误\r\n error(err);\r\n }\r\n }\r\n // 刷新任务\r\n taskStatus.value = TaskStatusType.FINISH;\r\n log('已完成');\r\n // 创建提示\r\n createTip('完成学习!');\r\n // 远程推送\r\n if (settings[SettingType.REMOTE_PUSH]) {\r\n // 推送\r\n const res = await pushModal({\r\n title: '学习推送',\r\n to: userinfo.nick,\r\n content: [\r\n '学习强国, 学习完成!',\r\n `当天积分: ${getHighlightHTML(todayScore.value)} 分`,\r\n `总积分: ${getHighlightHTML(totalScore.value)} 分`,\r\n ...taskConfig.map((task) => getProgressHTML(task.title, task.currentScore, task.dayMaxScore)),\r\n ],\r\n type: 'success',\r\n }, pushToken.value);\r\n createTip(`学习推送${res ? '成功' : '失败'}!`);\r\n }\r\n }\r\n // 已在等待\r\n let flag = false;\r\n // 自动答题\r\n watch(() => [taskStatus.value, settings[SettingType.AUTO_START]], async () => {\r\n // 加载完毕\r\n if (!flag && taskStatus.value === TaskStatusType.LOADED) {\r\n // 自动答题\r\n if (settings[SettingType.AUTO_START]) {\r\n // 等待中\r\n flag = true;\r\n // 创建提示\r\n const tip = createTip('即将自动开始任务', 5, true);\r\n // 等待倒计时结束\r\n await tip.waitCountDown();\r\n // 再次查看是否开启\r\n if (settings[SettingType.AUTO_START] &&\r\n taskStatus.value !== TaskStatusType.START) {\r\n // 创建提示\r\n createTip('自动开始任务');\r\n // 开始任务\r\n startTask();\r\n return;\r\n }\r\n // 取消等待\r\n flag = false;\r\n // 创建提示\r\n createTip('已取消自动开始任务!');\r\n }\r\n }\r\n });\r\n // 切换开关任务未完成\r\n taskConfig.forEach((task) => {\r\n watch(() => [task.active], () => {\r\n if (taskStatus.value === TaskStatusType.FINISH) {\r\n if (task.active && !task.status) {\r\n taskStatus.value = TaskStatusType.LOADED;\r\n }\r\n }\r\n });\r\n });\r\n return createElementNode('div', undefined, { class: 'egg_study_item' }, createElementNode('button', undefined, {\r\n class: watchEffectRef(() => `egg_study_btn${taskStatus.value === TaskStatusType.START ? ' loading' : ''}`),\r\n type: 'button',\r\n disabled: watchRef(() => [running.value, taskStatus.value], () => running.value ||\r\n taskStatus.value === TaskStatusType.LOADING ||\r\n taskStatus.value === TaskStatusType.FINISH),\r\n onclick: watchEffectRef(() => taskStatus.value === TaskStatusType.LOADED\r\n ? debounce(startTask, 300)\r\n : taskStatus.value === TaskStatusType.START\r\n ? debounce(pauseTask, 300)\r\n : taskStatus.value === TaskStatusType.PAUSE\r\n ? debounce(continueTask, 300)\r\n : undefined),\r\n }, createTextNode(watchEffectRef(() => `${taskStatus.value === TaskStatusType.LOADING\r\n ? '等待中'\r\n : taskStatus.value === TaskStatusType.LOADED\r\n ? '开始学习'\r\n : taskStatus.value === TaskStatusType.START\r\n ? '正在学习, 点击暂停'\r\n : taskStatus.value === TaskStatusType.PAUSE\r\n ? '继续学习'\r\n : taskStatus.value === TaskStatusType.FINISH\r\n ? '已完成'\r\n : ''}`))));\r\n }\r\n });\r\n}\r\n\r\n","from":"src\\component\\TaskBtn.ts","to":"TaskBtn.js"},"src\\component\\ScheduleList.ts":{"timeStamp":"2023-03-08T05:39:30.595Z","data":"/**\r\n * @description 定时项目\r\n * @returns\r\n */\r\nfunction ScheduleList() {\r\n return createElementNode('div', undefined, { class: 'egg_schedule_list' }, watchEffectRef(() => {\r\n return scheduleList.length\r\n ? scheduleList.map((schedule) => createElementNode('div', undefined, { class: 'egg_schedule_item' }, [\r\n createElementNode('div', undefined, {\r\n class: `egg_schedule_detail_time_wrap${isLate(schedule) ? ' inactive' : ''}`,\r\n }, [\r\n createElementNode('div', undefined, {\r\n class: 'egg_schedule_detail_icon',\r\n }, createNSElementNode('svg', undefined, {\r\n viewBox: '0 0 1024 1024',\r\n class: 'egg_icon',\r\n }, [\r\n createNSElementNode('path', undefined, {\r\n 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',\r\n }),\r\n createNSElementNode('path', undefined, {\r\n 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',\r\n }),\r\n ])),\r\n createElementNode('div', undefined, { class: 'egg_schedule_detail_time' }, createTextNode(schedule.time)),\r\n ]),\r\n createElementNode('div', undefined, { class: 'egg_schedule_detail_del_wrap' }, [\r\n createElementNode('button', undefined, {\r\n class: 'egg_schedule_del_btn',\r\n onclick: debounce(() => {\r\n // 定时刷新\r\n if (!settings[SettingType.SCHEDULE_RUN]) {\r\n createTip('未开启定时刷新!');\r\n return;\r\n }\r\n // 索引\r\n const index = scheduleList.findIndex((s) => s === schedule);\r\n // 删除元素\r\n scheduleList.splice(index, 1);\r\n // 存储\r\n GM_setValue('scheduleList', JSON.stringify(scheduleList));\r\n // 刷新任务\r\n refreshScheduleTask();\r\n }, 300),\r\n }, createNSElementNode('svg', undefined, {\r\n viewBox: '0 0 1024 1024',\r\n class: 'egg_icon',\r\n }, [\r\n createNSElementNode('path', undefined, {\r\n 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',\r\n }),\r\n createNSElementNode('path', undefined, {\r\n 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',\r\n }),\r\n ])),\r\n ]),\r\n ]))\r\n : [\r\n createElementNode('div', undefined, { class: 'egg_schedule_list_none' }, [\r\n createNSElementNode('svg', undefined, {\r\n viewBox: '0 0 1024 1024',\r\n class: 'egg_icon',\r\n }, [\r\n createNSElementNode('path', undefined, {\r\n 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',\r\n }),\r\n createNSElementNode('path', undefined, {\r\n 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',\r\n }),\r\n createNSElementNode('path', undefined, {\r\n 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',\r\n }),\r\n ]),\r\n createElementNode('div', undefined, {\r\n class: 'egg_schedule_list_none_text',\r\n }, createTextNode('暂无定时任务')),\r\n ]),\r\n ];\r\n }));\r\n}\r\n\r\n","from":"src\\component\\ScheduleList.ts","to":"ScheduleList.js"},"src\\component\\TimeInput.ts":{"timeStamp":"2023-03-08T07:30:02.342Z","data":"/**\r\n * @description 时间输入\r\n * @returns\r\n */\r\nfunction TimeInput({ hour, minute, onchange, }) {\r\n // 小时\r\n const hours = new Array(24).fill(undefined).map((v, i) => ({\r\n value: i,\r\n label: formatDateNum(i),\r\n }));\r\n // 分钟\r\n const minutes = new Array(60).fill(undefined).map((v, i) => ({\r\n value: i,\r\n label: formatDateNum(i),\r\n }));\r\n const valueRef = watchEffectRef(() => {\r\n const h = hours.find((h) => h.value === hour.value);\r\n const min = minutes.find((min) => min.value === minute.value);\r\n return {\r\n hour: h ? h.value : -1,\r\n minute: min ? min.value : -1,\r\n };\r\n });\r\n return createElementNode('div', undefined, { class: 'egg_time_input' }, [\r\n createElementNode('div', undefined, { class: 'egg_hour_wrap' }, [\r\n Select({\r\n data: hours,\r\n placeholder: '00',\r\n maxlength: 2,\r\n value: hour,\r\n onchange({ value }) {\r\n valueRef.value.hour = value;\r\n onchange && onchange(valueRef.value);\r\n },\r\n onblur(res) {\r\n if (!res) {\r\n valueRef.value.hour = -1;\r\n onchange && onchange(valueRef.value);\r\n }\r\n },\r\n }),\r\n ]),\r\n createElementNode('span', undefined, { class: 'egg_separator' }, createTextNode(':')),\r\n createElementNode('div', undefined, { class: 'egg_minute_wrap' }, [\r\n Select({\r\n data: minutes,\r\n placeholder: '00',\r\n maxlength: 2,\r\n value: minute,\r\n onchange({ value }) {\r\n valueRef.value.minute = value;\r\n onchange && onchange(valueRef.value);\r\n },\r\n onblur(res) {\r\n if (!res) {\r\n valueRef.value.minute = -1;\r\n onchange && onchange(valueRef.value);\r\n }\r\n },\r\n }),\r\n ]),\r\n ]);\r\n}\r\n\r\n","from":"src\\component\\TimeInput.ts","to":"TimeInput.js"},"src\\component\\SettingsPanel.ts":{"timeStamp":"2023-06-06T09:16:08.312Z","data":"/**\r\n * @description 设置面板组件\r\n * @returns\r\n */\r\nfunction SettingsPanel({ show }) {\r\n // token\r\n let token = '';\r\n // 小时\r\n let hour = ref(-1);\r\n // 分钟\r\n let minute = ref(-1);\r\n return createElementNode('div', undefined, {\r\n class: watchEffectRef(() => `egg_settings${show.value ? ' active' : ''}`),\r\n }, [\r\n createElementNode('div', undefined, { class: 'egg_settings_version_wrap' }, [\r\n createElementNode('div', undefined, { class: 'egg_settings_label' }, createTextNode('版本信息')),\r\n createElementNode('div', undefined, {\r\n class: 'egg_settings_version',\r\n }, [\r\n createTextNode(`v${version}`),\r\n createElementNode('a', undefined, {\r\n class: 'egg_settings_version_detail',\r\n title: 'GitHub Xu22Web/tech-study-js',\r\n href: 'https://github.com/Xu22Web/tech-study-js',\r\n }, createNSElementNode('svg', undefined, {\r\n viewBox: '0 0 16 16',\r\n class: 'egg_icon',\r\n }, createNSElementNode('path', undefined, {\r\n d: 'M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z',\r\n }))),\r\n ]),\r\n ]),\r\n createElementNode('div', undefined, { class: 'egg_settings_theme_wrap' }, [\r\n createElementNode('div', undefined, { class: 'egg_settings_label' }, createTextNode('主题预设')),\r\n createElementNode('div', undefined, { class: 'egg_settings_theme_colors' }, [\r\n {\r\n value: '#fa3333',\r\n title: '强国红',\r\n detail: 'XueXi Red',\r\n code: 'none',\r\n },\r\n {\r\n value: '#bb2649',\r\n title: '非凡洋红',\r\n detail: 'Viva Magenta',\r\n code: '18-1750',\r\n },\r\n {\r\n value: '#35548a',\r\n title: '经典蓝',\r\n detail: 'Classic Blue',\r\n code: '19-4052',\r\n },\r\n {\r\n value: '#f36f63',\r\n title: '活珊瑚橘',\r\n detail: 'Living Coral',\r\n code: '16-1546',\r\n },\r\n {\r\n value: '#6d5b97',\r\n title: '紫外光色',\r\n detail: 'Ultra Violet',\r\n code: '18-3838',\r\n },\r\n {\r\n value: '#86af49',\r\n title: '草木绿',\r\n detail: 'Greenery',\r\n code: '15-0343',\r\n },\r\n {\r\n value: '#fc8bab',\r\n title: 'B站粉',\r\n detail: 'Bilibili Pink',\r\n code: 'none',\r\n },\r\n {\r\n value: '#056de8',\r\n title: '知乎蓝',\r\n detail: 'Zhihu Blue',\r\n code: 'none',\r\n },\r\n ].map((color) => createElementNode('div', undefined, {\r\n class: 'egg_settings_theme_color_wrap',\r\n }, createElementNode('button', undefined, {\r\n class: 'egg_settings_theme_color',\r\n type: 'button',\r\n style: watchEffectRef(() => `color: ${color.value};${themeColor.value === color.value\r\n ? ''\r\n : ` box-shadow: 0rem 0.4rem 0.1rem 0.1rem ${color.value}30;`}`),\r\n title: color.title,\r\n onclick: debounce(() => {\r\n if (themeColor.value !== color.value) {\r\n themeColor.value = color.value;\r\n // 存储\r\n GM_setValue('themeColor', themeColor.value);\r\n }\r\n }, 300),\r\n })))),\r\n ]),\r\n createElementNode('div', undefined, {\r\n class: 'egg_settings_read_time_wrap',\r\n }, [\r\n createElementNode('div', undefined, { class: 'egg_settings_label' }, createTextNode('最大文章时长')),\r\n Select({\r\n data: [\r\n {\r\n label: '40s',\r\n value: 40,\r\n },\r\n {\r\n label: '60s',\r\n value: 60,\r\n },\r\n {\r\n label: '80s',\r\n value: 80,\r\n },\r\n {\r\n label: '100s',\r\n value: 100,\r\n },\r\n ],\r\n value: maxRead,\r\n placeholder: '100s',\r\n maxlength: 4,\r\n keep: true,\r\n onchange({ value }) {\r\n // 创建提示\r\n createTip('最大文章时长 已保存!');\r\n maxRead.value = value;\r\n // 存储\r\n GM_setValue('maxRead', value);\r\n },\r\n }),\r\n ], {\r\n onMounted() {\r\n try {\r\n const maxReadTemp = GM_getValue('maxRead');\r\n if (maxReadTemp) {\r\n maxRead.value = maxReadTemp;\r\n }\r\n }\r\n catch (e) { }\r\n },\r\n }),\r\n createElementNode('div', undefined, {\r\n class: 'egg_settings_watch_time_wrap',\r\n }, [\r\n createElementNode('div', undefined, { class: 'egg_settings_label' }, createTextNode('最大视听时长')),\r\n Select({\r\n data: [\r\n {\r\n label: '40s',\r\n value: 40,\r\n },\r\n {\r\n label: '60s',\r\n value: 60,\r\n },\r\n {\r\n label: '80s',\r\n value: 80,\r\n },\r\n {\r\n label: '100s',\r\n value: 100,\r\n },\r\n {\r\n label: '120s',\r\n value: 120,\r\n },\r\n ],\r\n value: maxWatch,\r\n placeholder: '120s',\r\n maxlength: 4,\r\n keep: true,\r\n onchange({ value }) {\r\n // 创建提示\r\n createTip('最大视听时长 已保存!');\r\n maxWatch.value = value;\r\n // 存储\r\n GM_setValue('maxWatch', value);\r\n },\r\n }),\r\n ], {\r\n onMounted() {\r\n try {\r\n const maxWatchTemp = GM_getValue('maxWatch');\r\n if (maxWatchTemp) {\r\n maxWatch.value = maxWatchTemp;\r\n }\r\n }\r\n catch (e) { }\r\n },\r\n }),\r\n watchEffectRef(() => settings[SettingType.REMOTE_PUSH]\r\n ? createElementNode('div', undefined, { class: 'egg_settings_token_wrap' }, [\r\n createElementNode('div', undefined, { class: 'egg_settings_token' }, [\r\n createElementNode('div', undefined, { class: 'egg_settings_label' }, createTextNode('我的 token')),\r\n createElementNode('input', undefined, {\r\n class: 'egg_settings_token_input',\r\n placeholder: '用户 token',\r\n maxlength: 32,\r\n value: pushToken.value,\r\n onfocus: (e) => {\r\n const input = e.target;\r\n input.classList.add('active');\r\n const btnWrap = $$('.egg_settings_submit_btn_wrap')[0];\r\n btnWrap.classList.add('active');\r\n },\r\n onblur: (e) => {\r\n const input = e.target;\r\n // 去除空格\r\n const value = input.value.trim();\r\n if (/^[0-9a-z]{32}$/.test(value)) {\r\n token = value;\r\n input.value = value;\r\n }\r\n else {\r\n token = '';\r\n }\r\n input.classList.remove('active');\r\n setTimeout(() => {\r\n const btnWrap = $$('.egg_settings_submit_btn_wrap')[0];\r\n btnWrap.classList.remove('active');\r\n input.value = pushToken.value;\r\n }, 200);\r\n },\r\n }),\r\n ]),\r\n createElementNode('div', undefined, { class: 'egg_settings_submit_btn_wrap' }, createElementNode('button', undefined, {\r\n class: 'egg_settings_submit_btn',\r\n onclick: debounce(() => {\r\n // 创建提示\r\n createTip('用户 token 已保存!');\r\n if (token !== pushToken.value) {\r\n pushToken.value = token;\r\n // 存储\r\n GM_setValue('pushToken', token);\r\n }\r\n }, 300),\r\n }, createTextNode('保存'))),\r\n ])\r\n : undefined),\r\n watchEffectRef(() => settings[SettingType.SCHEDULE_RUN]\r\n ? createElementNode('div', undefined, { class: 'egg_schedule' }, [\r\n createElementNode('div', undefined, { class: 'egg_schedule_time_wrap' }, [\r\n createElementNode('div', undefined, { class: 'egg_schedule_time' }, [\r\n createElementNode('div', undefined, { class: 'egg_schedule_label' }, createTextNode('设置时间')),\r\n createElementNode('div', undefined, { class: 'egg_schedule_time_input_wrap' }, [\r\n TimeInput({\r\n hour,\r\n minute,\r\n onchange({ hour: h, minute: min }) {\r\n hour.value = h;\r\n minute.value = min;\r\n },\r\n }),\r\n createElementNode('button', undefined, {\r\n class: 'egg_schedule_add_btn',\r\n onclick: debounce(() => {\r\n // 定时刷新\r\n if (!settings[SettingType.SCHEDULE_RUN]) {\r\n createTip('未开启定时刷新!');\r\n return;\r\n }\r\n if (hour.value === -1 || minute.value === -1) {\r\n createTip('时间格式不符合要求!');\r\n return;\r\n }\r\n // 重复定时存在\r\n const exists = scheduleList.find((schedule) => schedule.hour === hour.value &&\r\n schedule.minute === minute.value);\r\n if (exists) {\r\n createTip('设置定时任务重复!');\r\n return;\r\n }\r\n createTip('设置定时任务成功!');\r\n // 添加\r\n scheduleList.push({\r\n hour: hour.value,\r\n minute: minute.value,\r\n time: `${formatDateNum(hour.value)}:${formatDateNum(minute.value)}`,\r\n });\r\n // 排序\r\n scheduleList.sort((a, b) => a.hour === b.hour\r\n ? a.minute - b.minute\r\n : a.hour - b.hour);\r\n // 存储\r\n GM_setValue('scheduleList', JSON.stringify(scheduleList));\r\n // 清空\r\n hour.value = -1;\r\n minute.value = -1;\r\n const inputs = $$('.egg_time_input input');\r\n inputs.forEach((i) => (i.value = ''));\r\n // 刷新任务\r\n refreshScheduleTask();\r\n }, 300),\r\n }, createNSElementNode('svg', undefined, {\r\n viewBox: '0 0 1024 1024',\r\n class: 'egg_icon',\r\n }, createNSElementNode('path', undefined, {\r\n d: 'M801.171 483.589H544V226.418c0-17.673-14.327-32-32-32s-32 14.327-32 32v257.171H222.83c-17.673 0-32 14.327-32 32s14.327 32 32 32H480v257.17c0 17.673 14.327 32 32 32s32-14.327 32-32v-257.17h257.171c17.673 0 32-14.327 32-32s-14.327-32-32-32z',\r\n }))),\r\n ]),\r\n ]),\r\n ]),\r\n ScheduleList(),\r\n ])\r\n : undefined),\r\n ], {\r\n onMounted() {\r\n // 刷新token\r\n watch(() => settings[SettingType.REMOTE_PUSH], () => {\r\n // 远程推送\r\n if (settings[SettingType.REMOTE_PUSH]) {\r\n try {\r\n const tokenTemp = GM_getValue('pushToken');\r\n if (tokenTemp) {\r\n pushToken.value = tokenTemp;\r\n }\r\n }\r\n catch (e) { }\r\n }\r\n }, true);\r\n // 刷新定时任务\r\n watch(() => settings[SettingType.SCHEDULE_RUN], () => {\r\n // 定时任务打开\r\n if (settings[SettingType.SCHEDULE_RUN]) {\r\n try {\r\n const scheduleTemp = JSON.parse(GM_getValue('scheduleList'));\r\n if (scheduleTemp && Array.isArray(scheduleTemp)) {\r\n for (const i in scheduleTemp) {\r\n scheduleList[i] = scheduleTemp[i];\r\n }\r\n }\r\n }\r\n catch (e) { }\r\n // 刷新定时任务\r\n refreshScheduleTask();\r\n return;\r\n }\r\n // 清除任务\r\n clearScheduleTask();\r\n }, true);\r\n },\r\n });\r\n}\r\n\r\n","from":"src\\component\\SettingsPanel.ts","to":"SettingsPanel.js"},"src\\component\\Panel.ts":{"timeStamp":"2023-07-14T10:51:35.712Z","data":"/**\r\n * @description 面板\r\n * @returns\r\n */\r\nfunction Panel() {\r\n // 运行设置标签\r\n const runLabels = [\r\n {\r\n title: '自动开始',\r\n tip: '启动时, 自动开始任务, 在倒计时结束前自动开始可随时取消; 如果在自动开始前手动开始任务, 此次自动开始将取消',\r\n type: SettingType.AUTO_START,\r\n },\r\n {\r\n title: '同屏任务',\r\n tip: '运行任务时,所有任务均在当前页面以弹窗方式运行',\r\n type: SettingType.SAME_TAB,\r\n },\r\n {\r\n title: '静默运行',\r\n tip: '同屏任务时, 不显示任务弹窗静默运行',\r\n type: SettingType.SILENT_RUN,\r\n },\r\n {\r\n title: '定时刷新',\r\n tip: '定时刷新页面,重新进行任务,此功能需要长时间占用浏览器',\r\n type: SettingType.SCHEDULE_RUN,\r\n },\r\n {\r\n title: '视频静音',\r\n tip: '视听学习时,静音播放视频',\r\n type: SettingType.VIDEO_MUTED,\r\n },\r\n ];\r\n // 运行设置标签\r\n const examLabels = [\r\n {\r\n title: '随机作答',\r\n tip: '无答案时, 随机选择或者填入答案, 不保证正确',\r\n type: SettingType.RANDOM_EXAM,\r\n },\r\n {\r\n title: '自动答题',\r\n tip: '进入答题页面时,自动答题并提交答案',\r\n type: SettingType.AUTO_ANSWER,\r\n },\r\n ];\r\n // 推送设置标签\r\n const pushLabels = [\r\n {\r\n title: '远程推送',\r\n tip: '利用 pushplus 推送, 将登录二维码直接推送到微信公众号',\r\n type: SettingType.REMOTE_PUSH,\r\n },\r\n ];\r\n // 处理设置变化\r\n const handleSettingsChange = (e, type, title) => {\r\n // 开关\r\n const { checked } = e.target;\r\n if (settings[type] !== checked) {\r\n settings[type] = checked;\r\n // 设置\r\n GM_setValue('studySettings', JSON.stringify(settings));\r\n // 创建提示\r\n createTip(`${title} ${checked ? '打开' : '关闭'}!`);\r\n }\r\n };\r\n // 任务显示\r\n const scheduleShow = ref(false);\r\n // 面板显示\r\n const panelShow = ref(false);\r\n return createElementNode('div', undefined, {\r\n class: `egg_panel_wrap${hasMobile() ? ' mobile' : ''}`,\r\n onclick(e) {\r\n e.stopPropagation();\r\n },\r\n onmousedown(e) {\r\n e.stopPropagation();\r\n },\r\n onmousemove(e) {\r\n e.stopPropagation();\r\n },\r\n onmouseup(e) {\r\n e.stopPropagation();\r\n },\r\n onmouseenter(e) {\r\n e.stopPropagation();\r\n },\r\n onmouseleave(e) {\r\n e.stopPropagation();\r\n },\r\n onmouseover(e) {\r\n e.stopPropagation();\r\n },\r\n ontouchstart(e) {\r\n e.stopPropagation();\r\n },\r\n ontouchmove(e) {\r\n e.stopPropagation();\r\n },\r\n ontouchend(e) {\r\n e.stopPropagation();\r\n },\r\n oninput(e) {\r\n e.stopPropagation();\r\n },\r\n onchange(e) {\r\n e.stopPropagation();\r\n },\r\n onblur(e) {\r\n e.stopPropagation();\r\n },\r\n }, createElementNode('div', undefined, {\r\n class: watchEffectRef(() => `egg_panel${panelShow.value ? ' hide' : ''}`),\r\n }, [\r\n // 登录\r\n LoginItem(),\r\n // 信息\r\n InfoItem(),\r\n // 分数\r\n ScoreItem(),\r\n // 任务部分\r\n Hr({ text: '任务' }),\r\n TaskList(),\r\n // 运行部分\r\n Hr({ text: '运行' }),\r\n createElementNode('div', undefined, { class: 'egg_run_list' }, runLabels.map((label) => {\r\n return NormalItem({\r\n title: label.title,\r\n tip: label.tip,\r\n checked: settings[label.type],\r\n onchange: debounce((e) => {\r\n handleSettingsChange(e, label.type, label.title);\r\n }, 300),\r\n });\r\n })),\r\n // 答题部分\r\n Hr({ text: '答题' }),\r\n createElementNode('div', undefined, { class: 'egg_exam_list' }, examLabels.map((label) => {\r\n return NormalItem({\r\n title: label.title,\r\n tip: label.tip,\r\n checked: settings[label.type],\r\n onchange: debounce((e) => {\r\n handleSettingsChange(e, label.type, label.title);\r\n }, 300),\r\n });\r\n })),\r\n // 推送部分\r\n Hr({ text: '推送' }),\r\n createElementNode('div', undefined, { class: 'egg_push_list' }, pushLabels.map((label) => {\r\n return NormalItem({\r\n title: label.title,\r\n tip: label.tip,\r\n checked: settings[label.type],\r\n onchange: debounce((e) => {\r\n handleSettingsChange(e, label.type, label.title);\r\n }, 300),\r\n });\r\n })),\r\n // 提示部分\r\n Hr({ text: '提示' }),\r\n createElementNode('div', undefined, { class: 'egg_tip_list' }, watchRef(login, () => login.value\r\n ? [\r\n createTextNode('专项练习已被移除, 如需使用, 请点击'),\r\n createElementNode('button', undefined, {\r\n class: 'egg_tip_btn',\r\n type: 'button',\r\n onclick: debounce(doExamPaper, 300),\r\n disabled: watchRef(() => [running.value, taskStatus.value], () => running.value ||\r\n taskStatus.value === TaskStatusType.START ||\r\n taskStatus.value === TaskStatusType.PAUSE),\r\n }, createTextNode('去完成')),\r\n ]\r\n : [\r\n createElementNode('div', undefined, { class: 'egg_tip_content' }, createTextNode('请先登录!')),\r\n ])),\r\n // 按钮集合\r\n createElementNode('div', undefined, {\r\n class: 'egg_btns_wrap',\r\n }, [\r\n createElementNode('button', undefined, {\r\n class: watchRef(() => [frame.exist, frame.show], () => `egg_frame_show_btn${!frame.exist || frame.show ? ' hide' : ''}`),\r\n title: '窗口',\r\n type: 'button',\r\n onclick: debounce(() => {\r\n // 窗口显示\r\n frame.show = true;\r\n }, 300),\r\n }, createNSElementNode('svg', undefined, {\r\n viewBox: '0 0 1024 1024',\r\n class: 'egg_icon',\r\n }, createNSElementNode('path', undefined, {\r\n 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',\r\n }))),\r\n createElementNode('button', undefined, {\r\n class: 'egg_panel_show_btn',\r\n title: '面板',\r\n type: 'button',\r\n onclick: debounce(() => {\r\n panelShow.value = !panelShow.value;\r\n }, 300),\r\n }, createNSElementNode('svg', undefined, {\r\n viewBox: '0 0 1024 1024',\r\n class: 'egg_icon',\r\n }, createNSElementNode('path', undefined, {\r\n 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',\r\n }))),\r\n createElementNode('button', undefined, {\r\n class: watchEffectRef(() => `egg_settings_show_btn${scheduleShow.value ? ' active' : ''}`),\r\n title: '设置',\r\n type: 'button',\r\n onclick: debounce(() => {\r\n scheduleShow.value = !scheduleShow.value;\r\n }, 300),\r\n }, createNSElementNode('svg', undefined, {\r\n viewBox: '0 0 1024 1024',\r\n class: 'egg_icon',\r\n }, [\r\n createNSElementNode('path', undefined, {\r\n 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',\r\n }),\r\n createNSElementNode('path', undefined, {\r\n 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',\r\n }),\r\n ])),\r\n createElementNode('button', undefined, {\r\n class: 'egg_settings_reset_btn',\r\n title: '重置',\r\n type: 'button',\r\n onclick: debounce(() => {\r\n // 任务配置\r\n GM_setValue('taskConfig', null);\r\n // 设置\r\n GM_setValue('studySettings', null);\r\n // 最大阅读\r\n GM_setValue('maxRead', null);\r\n // 最大观看\r\n GM_setValue('maxWatch', null);\r\n // 主题色\r\n GM_setValue('themeColor', null);\r\n // 刷新页面\r\n location.reload();\r\n }, 300),\r\n }, createNSElementNode('svg', undefined, {\r\n viewBox: '0 0 1024 1024',\r\n class: 'egg_icon',\r\n }, [\r\n createNSElementNode('path', undefined, {\r\n 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',\r\n }),\r\n ])),\r\n ]),\r\n // 任务按钮\r\n TaskBtn(),\r\n createElementNode('div', undefined, { class: 'egg_settings_item' }, [\r\n SettingsPanel({ show: scheduleShow }),\r\n ]),\r\n ]));\r\n}\r\n\r\n","from":"src\\component\\Panel.ts","to":"Panel.js"}} ================================================ FILE: package.json ================================================ { "name": "tech-study", "version": "1.0.0", "description": "a flexible and light tampermonkey plugin for xuexiqiangguo.", "main": "tech-study.js", "scripts": { "build": "ts-node bin/index.ts" }, "repository": { "type": "git", "url": "git+https://github.com/Xu22Web/tech-study-js.git" }, "keywords": [], "author": "", "license": "ISC", "bugs": { "url": "https://github.com/Xu22Web/tech-study-js/issues" }, "homepage": "https://github.com/Xu22Web/tech-study-js#readme", "devDependencies": { "@rollup/plugin-terser": "^0.4.0", "@types/node": "^18.14.6", "@types/tampermonkey": "^4.0.10", "chalk": "^4.1.2", "ora": "^4.1.1", "rollup": "^3.18.0", "ts-node": "^10.9.1", "typescript": "^4.9.5" } } ================================================ FILE: src/api/answer.ts ================================================ import API_CONFIG from '../config/api'; import { log } from '../utils/log'; /* 答案 API */ /** * @description 获取答案 */ async function getAnswer(question: string) { // 数据 const data = { txt_name: md5(question), password: '', }; try { const params = new URLSearchParams(data); // 请求 const res = await fetch(API_CONFIG.answerSearch, { method: 'POST', mode: 'cors', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: params.toString(), }); // 请求成功 if (res.ok) { const result = await res.json(); const { data, status } = < { status: number; data: { txt_content: string; txt_name: string }; } >result; if (status !== 0) { // 答案列表 const answerList: { content: string; title: string }[] = JSON.parse( data.txt_content ); // 答案 const answers = answerList[0].content.split(/[;\s]/); return answers; } } } catch (error) {} return []; } /** * @description 保存答案 */ async function saveAnswer(question: string, answer: string) { try { // 内容 const content = JSON.stringify([{ title: md5(question), content: answer }]); // 数据 const data = { txt_name: md5(question), txt_content: content, password: '', v_id: '', }; const params = new URLSearchParams(data); // 请求 const res = await fetch(API_CONFIG.answerSave, { method: 'POST', mode: 'cors', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: params.toString(), }); // 请求成功 if (res.ok) { const data = await res.json(); return data; } } catch (error) {} } export { getAnswer, saveAnswer }; ================================================ FILE: src/api/data.ts ================================================ import API_CONFIG from '../config/api'; import { NewsVideoList } from '../types'; /* 数据 API */ /** * @description 获取新闻数据 */ async function getNewsList() { // 随机 const randNum = ~~(Math.random() * API_CONFIG.todayNews.length); try { // 获取重要新闻 const res = await fetch(API_CONFIG.todayNews[randNum], { method: 'GET', }); // 请求成功 if (res.ok) { const data = await res.json(); return data; } } catch (err) {} } /** * @description 获取视频数据 */ async function getVideoList() { // 随机 const randNum = ~~(Math.random() * API_CONFIG.todayVideos.length); try { // 获取重要新闻 const res = await fetch(API_CONFIG.todayVideos[randNum], { method: 'GET', }); // 请求成功 if (res.ok) { const data = await res.json(); return data; } } catch (err) {} } /** * @description 专项练习数据 */ async function getExamPaper(pageNo: number) { // 链接 const url = `${API_CONFIG.paperList}?pageSize=50&pageNo=${pageNo}`; try { // 获取专项练习 const res = await fetch(url, { method: 'GET', credentials: 'include', }); // 请求成功 if (res.ok) { const data = await res.json(); const paperJson = decodeURIComponent( escape(window.atob(data.data_str.replace(/-/g, '+').replace(/_/g, '/'))) ); // JSON格式化 const paper = JSON.parse(paperJson); return paper; } } catch (err) { return []; } return []; } export { getVideoList, getNewsList, getExamPaper }; ================================================ FILE: src/api/login.ts ================================================ import API_CONFIG from '../config/api'; /** * @description 生成二维码 */ async function generateQRCode() { try { // 推送 const res = await fetch(API_CONFIG.generateQRCode, { method: 'GET', mode: 'cors', }); // 请求成功 if (res.ok) { const data = await res.json(); if (data.success) { return data.result; } } } catch (error) {} } /** * @description 用二维码登录 */ async function loginWithQRCode(qrCode: string) { try { const params = new URLSearchParams({ qrCode, goto: 'https://oa.xuexi.cn', pdmToken: '', }); // 推送 const res = await fetch(API_CONFIG.loginWithQRCode, { method: 'POST', mode: 'cors', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', }, body: params.toString(), }); // 请求成功 if (res.ok) { const data = await res.json(); return <{ data: string; code: string; success: boolean }>data; } } catch (error) {} } /** * @description 签名 */ async function getSign() { try { // 推送 const res = await fetch(API_CONFIG.sign, { method: 'GET', mode: 'cors', credentials: 'include', }); // 请求成功 if (res.ok) { const data = await res.json(); if (data.ok) { return data.data.sign; } } } catch (error) {} } /** * @description 安全检查 * @param data */ async function secureCheck(data: { code: string; state: string }) { try { const params = new URLSearchParams(data); const url = `${API_CONFIG.secureCheck}?${params}`; // 推送 const res = await fetch(url, { method: 'GET', mode: 'cors', credentials: 'include', }); // 请求成功 if (res.ok) { const data = await res.json(); return data.success; } } catch (error) {} return false; } export { generateQRCode, loginWithQRCode, getSign, secureCheck }; ================================================ FILE: src/api/push.ts ================================================ import API_CONFIG from '../config/api'; /* 推送 API */ /** * @description 推送 */ async function pushPlus( token: string, title: string, content: string, template: string, toToken?: string ) { try { // 参数体 const body: { token: string; title: string; content: string; template: string; to?: string; } = { token, title, content, template, }; // 好友令牌 if (toToken) { body.to = toToken; } // 推送 const res = await fetch(API_CONFIG.push, { method: 'POST', mode: 'cors', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); // 请求成功 if (res.ok) { const data = await res.json(); return data; } } catch (error) {} } export { pushPlus }; ================================================ FILE: src/api/user.ts ================================================ import API_CONFIG from '../config/api'; /** * @description 用户信息 */ type UserInfo = { avatarMediaUrl?: string; nick: string; }; /* 用户 API */ /** * @description 获取用户信息 */ async function getUserInfo(): Promise { try { const res = await fetch(API_CONFIG.userInfo, { method: 'GET', credentials: 'include', }); // 请求成功 if (res.ok) { const { data } = await res.json(); return data; } } catch (err) {} } /** * @description 获取总积分 */ async function getTotalScore() { try { const res = await fetch(API_CONFIG.totalScore, { method: 'GET', credentials: 'include', }); // 请求成功 if (res.ok) { const { data } = await res.json(); // 总分 const { score } = data; return score; } } catch (err) {} } /** * @description 获取当天总积分 */ async function getTodayScore() { try { const res = await fetch(API_CONFIG.todayScore, { method: 'GET', credentials: 'include', }); // 请求成功 if (res.ok) { const { data } = await res.json(); // 当天总分 const { score } = data; return score; } } catch (err) {} } /** * @description 获取任务列表 */ async function getTaskList() { try { const res = await fetch(API_CONFIG.taskList, { method: 'GET', credentials: 'include', }); // 请求成功 if (res.ok) { const { data } = await res.json(); // 进度和当天总分 const { taskProgress } = data; return taskProgress; } } catch (err) {} } export { getUserInfo, getTotalScore, getTodayScore, getTaskList }; ================================================ FILE: src/component/ExamBtn.ts ================================================ import { examPause, settings } from '../shared'; import { SettingType } from '../types'; import { watchEffect, watchEffectRef } from '../utils/composition'; import { createElementNode, createTextNode } from '../utils/element'; /** * @description 答题按钮 */ function ExamBtn() { // 设置初始状态 watchEffect(() => (examPause.value = !settings[SettingType.AUTO_ANSWER])); return createElementNode( 'button', undefined, { class: watchEffectRef( () => `egg_exam_btn${examPause.value ? ' manual' : ''}` ), type: 'button', onclick(e: Event) { e.stopPropagation(); examPause.value = !examPause.value; }, onmousedown(e: Event) { e.stopPropagation(); }, onmousemove(e: Event) { e.stopPropagation(); }, onmouseup(e: Event) { e.stopPropagation(); }, onmouseenter(e: Event) { e.stopPropagation(); }, onmouseleave(e: Event) { e.stopPropagation(); }, onmouseover(e: Event) { e.stopPropagation(); }, ontouchstart(e: Event) { e.stopPropagation(); }, ontouchmove(e: Event) { e.stopPropagation(); }, ontouchend(e: Event) { e.stopPropagation(); }, oninput(e: Event) { e.stopPropagation(); }, onchange(e: Event) { e.stopPropagation(); }, onblur(e: Event) { e.stopPropagation(); }, }, createTextNode( watchEffectRef( () => `${examPause.value ? '开启自动答题' : '关闭自动答题'}` ) ) ); } export { ExamBtn }; ================================================ FILE: src/component/Frame.ts ================================================ import { closeFrame, closeTaskWin, closeWin } from '../controller/frame'; import { frame, login, running, settings, taskStatus } from '../shared'; import { SettingType, TaskStatusType, TaskType } from '../types'; import { ref, watch, watchEffectRef, watchRef } from '../utils/composition'; import { createElementNode, createNSElementNode } from '../utils/element'; import { debounce } from '../utils/utils'; /** * @description 任务窗口 * @returns */ function Frame() { // 最大化 const max = ref(false); // 容器 return createElementNode( 'div', undefined, { class: watchEffectRef(() => `egg_frame_wrap${frame.show ? '' : ' hide'}`), onclick(e: Event) { e.stopPropagation(); }, onmousedown(e: Event) { e.stopPropagation(); }, onmousemove(e: Event) { e.stopPropagation(); }, onmouseup(e: Event) { e.stopPropagation(); }, onmouseenter(e: Event) { e.stopPropagation(); }, onmouseleave(e: Event) { e.stopPropagation(); }, onmouseover(e: Event) { e.stopPropagation(); }, ontouchstart(e: Event) { e.stopPropagation(); }, ontouchmove(e: Event) { e.stopPropagation(); }, ontouchend(e: Event) { e.stopPropagation(); }, oninput(e: Event) { e.stopPropagation(); }, onchange(e: Event) { e.stopPropagation(); }, onblur(e: Event) { e.stopPropagation(); }, }, watchRef( () => [login.value, settings[SettingType.SAME_TAB]], () => { // 同屏任务 if (login.value && settings[SettingType.SAME_TAB]) { return [ // 遮罩 createElementNode('div', undefined, { class: 'egg_frame_mask' }), // 窗口内容 createElementNode( 'div', undefined, { class: watchEffectRef( () => `egg_frame_content_wrap ${max.value ? ' max' : ''}` ), }, [ // 窗口控制 createElementNode( 'div', undefined, { class: 'egg_frame_controls_wrap' }, [ // 标题 createElementNode('div', undefined, { class: 'egg_frame_title', }), createElementNode( 'div', undefined, { class: 'egg_frame_controls', }, [ // 隐藏 createElementNode( 'button', undefined, { class: 'egg_frame_btn', type: 'button', title: '隐藏', onclick: debounce(() => { // 隐藏窗口 frame.show = false; }, 300), }, createNSElementNode( 'svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { 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', }) ) ), // 改变大小 createElementNode( 'button', undefined, { class: 'egg_frame_btn', type: 'button', title: '缩放', onclick: debounce(() => { max.value = !max.value; }, 300), }, createNSElementNode( 'svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { 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', }) ) ), // 关闭 createElementNode( 'button', undefined, { class: 'egg_frame_btn', type: 'button', title: '关闭', onclick: debounce(() => { // 关闭窗口 closeFrame(); }, 300), }, createNSElementNode( 'svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { 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', }) ) ), ] ), ] ), // 窗口内容 createElementNode( 'div', undefined, { class: 'egg_frame_content', }, watchEffectRef(() => frame.src ? [ createElementNode( 'iframe', undefined, { class: 'egg_frame', src: frame.src, ref(ele) { frame.ele = ele; }, }, undefined ), ] : undefined ) ), ], { onMounted() { // 隐藏窗口 watch( () => [ taskStatus.value, running.value, settings[SettingType.SAME_TAB], settings[SettingType.SILENT_RUN], ], () => { // 同屏任务 if ( settings[SettingType.SAME_TAB] && (taskStatus.value === TaskStatusType.START || taskStatus.value === TaskStatusType.PAUSE || running.value) ) { // 设置窗口显示 frame.show = !settings[SettingType.SILENT_RUN]; } } ); }, } ), ]; } } ), { onMounted() { // 关闭窗口 watch( () => [login.value, settings[SettingType.SAME_TAB]], () => { if (login.value) { if (settings[SettingType.SAME_TAB]) { frame.exist = true; closeWin(); } else { closeFrame(); frame.exist = false; } } else { closeWin(); closeFrame(); frame.exist = false; } } ); }, } ); } export { Frame }; ================================================ FILE: src/component/Hr.ts ================================================ import { createElementNode, createTextNode } from '../utils/element'; /** * @description 分隔符 * @returns */ function Hr({ text }: { text: string }) { return createElementNode( 'div', undefined, { class: 'egg_hr_wrap', }, [ createElementNode('div', undefined, { class: 'egg_hr' }), createElementNode( 'div', undefined, { class: 'egg_hr_title' }, createTextNode(text) ), createElementNode('div', undefined, { class: 'egg_hr' }), ] ); } export { Hr }; ================================================ FILE: src/component/InfoItem.ts ================================================ import { handleLogout } from '../controller/login'; import { refreshUserInfo } from '../controller/user'; import { login, userinfo } from '../shared'; import { watchEffectRef } from '../utils/composition'; import { createElementNode, createTextNode } from '../utils/element'; import { debounce } from '../utils/utils'; /** * @description 信息 * @returns */ function InfoItem() { return watchEffectRef(() => { if (login.value) { return createElementNode( 'div', undefined, { class: 'egg_info_item', }, [ // 用户信息 createElementNode('div', undefined, { class: 'egg_userinfo' }, [ // 头像 createElementNode( 'div', undefined, { class: 'egg_avatar' }, watchEffectRef(() => { return [ userinfo.avatar ? createElementNode('img', undefined, { src: userinfo.avatar, class: 'egg_avatar_img', }) : createElementNode( 'div', undefined, { class: 'egg_avatar_nick', }, createTextNode( watchEffectRef(() => userinfo.nick.substring(1, 3)) ) ), ]; }) ), // 昵称 createElementNode( 'div', undefined, { class: 'egg_nick' }, createTextNode(watchEffectRef(() => userinfo.nick)) ), ]), // 退出按钮 createElementNode( 'button', undefined, { type: 'button', class: 'egg_login_btn', onclick: debounce(() => { // 退出登录 handleLogout(); }, 300), }, createTextNode('退出') ), ], { onMounted() { // 刷新用户信息 refreshUserInfo(); }, } ); } }); } export { InfoItem }; ================================================ FILE: src/component/LoginItem.ts ================================================ import { handleLogin } from '../controller/login'; import { login, loginQRCodeShow, settings } from '../shared'; import { SettingType } from '../types'; import { watch, watchEffectRef } from '../utils/composition'; import { createElementNode, createTextNode } from '../utils/element'; import { debounce } from '../utils/utils'; /** * @description 登录 */ function LoginItem() { return watchEffectRef(() => { return login.value ? undefined : createElementNode( 'div', undefined, { class: 'egg_login_item', }, [ // 登录按钮 createElementNode( 'button', undefined, { type: 'button', class: 'egg_login_btn', onclick: debounce(async () => { // 开始登录 handleLogin(); }, 300), }, createTextNode('扫码登录') ), // 窗口 createElementNode( 'div', undefined, { class: watchEffectRef( () => `egg_login_img_wrap${ loginQRCodeShow.value ? ' active' : '' }` ), }, createElementNode('img', undefined, { class: 'egg_login_img', }) ), ], { onMounted() { watch( () => settings[SettingType.SCHEDULE_RUN], () => { // 未开启定时展示二维码 if (!settings[SettingType.SCHEDULE_RUN]) { // 开始登录 handleLogin(); } }, true ); }, } ); }); } export { LoginItem }; ================================================ FILE: src/component/NoramlItem.ts ================================================ import { createElementNode, createTextNode } from '../utils/element'; /** * @description 设置普通项 * @returns */ function NormalItem({ title, tip, checked, onchange, }: { title: string; tip: string; checked: boolean; onchange: (e: Event) => void; }) { return createElementNode('div', undefined, { class: 'egg_setting_item' }, [ createElementNode('div', undefined, { class: 'egg_label_wrap' }, [ createElementNode('label', undefined, { class: 'egg_task_title' }, [ createTextNode(title), createElementNode( 'span', undefined, { class: 'egg_detail', title: tip, }, createTextNode('i') ), ]), ]), createElementNode('input', undefined, { title: tip, class: 'egg_switch', type: 'checkbox', checked, onchange, }), ]); } export { NormalItem }; ================================================ FILE: src/component/Panel.ts ================================================ import { doExamPaper } from '../controller/exam'; import { createTip } from '../controller/tip'; import { frame, login, running, settings, taskStatus } from '../shared'; import { SettingType, TaskStatusType } from '../types'; import { ref, watchEffectRef, watchRef } from '../utils/composition'; import { createElementNode, createNSElementNode, createTextNode, } from '../utils/element'; import { debounce, hasMobile } from '../utils/utils'; import { Hr } from './Hr'; import { InfoItem } from './InfoItem'; import { LoginItem } from './LoginItem'; import { NormalItem } from './NoramlItem'; import { ScoreItem } from './ScoreItem'; import { SettingsPanel } from './SettingsPanel'; import { TaskBtn } from './TaskBtn'; import { TaskList } from './TaskList'; /** * @description 面板 * @returns */ function Panel() { // 运行设置标签 const runLabels = [ { title: '自动开始', tip: '启动时, 自动开始任务, 在倒计时结束前自动开始可随时取消; 如果在自动开始前手动开始任务, 此次自动开始将取消', type: SettingType.AUTO_START, }, { title: '同屏任务', tip: '运行任务时,所有任务均在当前页面以弹窗方式运行', type: SettingType.SAME_TAB, }, { title: '静默运行', tip: '同屏任务时, 不显示任务弹窗静默运行', type: SettingType.SILENT_RUN, }, { title: '定时刷新', tip: '定时刷新页面,重新进行任务,此功能需要长时间占用浏览器', type: SettingType.SCHEDULE_RUN, }, { title: '视频静音', tip: '视听学习时,静音播放视频', type: SettingType.VIDEO_MUTED, }, ]; // 运行设置标签 const examLabels = [ { title: '随机作答', tip: '无答案时, 随机选择或者填入答案, 不保证正确', type: SettingType.RANDOM_EXAM, }, { title: '自动答题', tip: '进入答题页面时,自动答题并提交答案', type: SettingType.AUTO_ANSWER, }, ]; // 推送设置标签 const pushLabels = [ { title: '远程推送', tip: '利用 pushplus 推送, 将登录二维码直接推送到微信公众号', type: SettingType.REMOTE_PUSH, }, ]; // 处理设置变化 const handleSettingsChange = (e: Event, type: SettingType, title: string) => { // 开关 const { checked } = e.target; if (settings[type] !== checked) { settings[type] = checked; // 设置 GM_setValue('studySettings', JSON.stringify(settings)); // 创建提示 createTip(`${title} ${checked ? '打开' : '关闭'}!`); } }; // 任务显示 const scheduleShow = ref(false); // 面板显示 const panelShow = ref(false); return createElementNode( 'div', undefined, { class: `egg_panel_wrap${hasMobile() ? ' mobile' : ''}`, onclick(e: Event) { e.stopPropagation(); }, onmousedown(e: Event) { e.stopPropagation(); }, onmousemove(e: Event) { e.stopPropagation(); }, onmouseup(e: Event) { e.stopPropagation(); }, onmouseenter(e: Event) { e.stopPropagation(); }, onmouseleave(e: Event) { e.stopPropagation(); }, onmouseover(e: Event) { e.stopPropagation(); }, ontouchstart(e: Event) { e.stopPropagation(); }, ontouchmove(e: Event) { e.stopPropagation(); }, ontouchend(e: Event) { e.stopPropagation(); }, oninput(e: Event) { e.stopPropagation(); }, onchange(e: Event) { e.stopPropagation(); }, onblur(e: Event) { e.stopPropagation(); }, }, createElementNode( 'div', undefined, { class: watchEffectRef( () => `egg_panel${panelShow.value ? ' hide' : ''}` ), }, [ // 登录 LoginItem(), // 信息 InfoItem(), // 分数 ScoreItem(), // 任务部分 Hr({ text: '任务' }), TaskList(), // 运行部分 Hr({ text: '运行' }), createElementNode( 'div', undefined, { class: 'egg_run_list' }, runLabels.map((label) => { return NormalItem({ title: label.title, tip: label.tip, checked: settings[label.type], onchange: debounce((e) => { handleSettingsChange(e, label.type, label.title); }, 300), }); }) ), // 答题部分 Hr({ text: '答题' }), createElementNode( 'div', undefined, { class: 'egg_exam_list' }, examLabels.map((label) => { return NormalItem({ title: label.title, tip: label.tip, checked: settings[label.type], onchange: debounce((e) => { handleSettingsChange(e, label.type, label.title); }, 300), }); }) ), // 推送部分 Hr({ text: '推送' }), createElementNode( 'div', undefined, { class: 'egg_push_list' }, pushLabels.map((label) => { return NormalItem({ title: label.title, tip: label.tip, checked: settings[label.type], onchange: debounce((e) => { handleSettingsChange(e, label.type, label.title); }, 300), }); }) ), // 提示部分 Hr({ text: '提示' }), createElementNode( 'div', undefined, { class: 'egg_tip_list' }, watchRef(login, () => login.value ? [ createTextNode('专项练习已被移除, 如需使用, 请点击'), createElementNode( 'button', undefined, { class: 'egg_tip_btn', type: 'button', onclick: debounce(doExamPaper, 300), disabled: watchRef( () => [running.value, taskStatus.value], () => running.value || taskStatus.value === TaskStatusType.START || taskStatus.value === TaskStatusType.PAUSE ), }, createTextNode('去完成') ), ] : [ createElementNode( 'div', undefined, { class: 'egg_tip_content' }, createTextNode('请先登录!') ), ] ) ), // 按钮集合 createElementNode( 'div', undefined, { class: 'egg_btns_wrap', }, [ createElementNode( 'button', undefined, { class: watchRef( () => [frame.exist, frame.show], () => `egg_frame_show_btn${ !frame.exist || frame.show ? ' hide' : '' }` ), title: '窗口', type: 'button', onclick: debounce(() => { // 窗口显示 frame.show = true; }, 300), }, createNSElementNode( 'svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { 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', }) ) ), createElementNode( 'button', undefined, { class: 'egg_panel_show_btn', title: '面板', type: 'button', onclick: debounce(() => { panelShow.value = !panelShow.value; }, 300), }, createNSElementNode( 'svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { 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', }) ) ), createElementNode( 'button', undefined, { class: watchEffectRef( () => `egg_settings_show_btn${ scheduleShow.value ? ' active' : '' }` ), title: '设置', type: 'button', onclick: debounce(() => { scheduleShow.value = !scheduleShow.value; }, 300), }, createNSElementNode( 'svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, [ createNSElementNode('path', undefined, { 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', }), createNSElementNode('path', undefined, { 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', }), ] ) ), createElementNode( 'button', undefined, { class: 'egg_settings_reset_btn', title: '重置', type: 'button', onclick: debounce(() => { // 任务配置 GM_setValue('taskConfig', null); // 设置 GM_setValue('studySettings', null); // 最大阅读 GM_setValue('maxRead', null); // 最大观看 GM_setValue('maxWatch', null); // 主题色 GM_setValue('themeColor', null); // 刷新页面 location.reload(); }, 300), }, createNSElementNode( 'svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, [ createNSElementNode('path', undefined, { 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', }), ] ) ), ] ), // 任务按钮 TaskBtn(), createElementNode('div', undefined, { class: 'egg_settings_item' }, [ SettingsPanel({ show: scheduleShow }), ]), ] ) ); } export { Panel }; ================================================ FILE: src/component/ScheduleList.ts ================================================ import { refreshScheduleTask } from '../controller/schedule'; import { createTip } from '../controller/tip'; import { scheduleList, settings } from '../shared'; import { SettingType } from '../types'; import { watchEffectRef } from '../utils/composition'; import { createElementNode, createNSElementNode, createTextNode, } from '../utils/element'; import { isLate } from '../utils/time'; import { debounce } from '../utils/utils'; /** * @description 定时项目 * @returns */ function ScheduleList() { return createElementNode( 'div', undefined, { class: 'egg_schedule_list' }, watchEffectRef(() => { return scheduleList.length ? scheduleList.map((schedule) => createElementNode( 'div', undefined, { class: 'egg_schedule_item' }, [ createElementNode( 'div', undefined, { class: `egg_schedule_detail_time_wrap${ isLate(schedule) ? ' inactive' : '' }`, }, [ createElementNode( 'div', undefined, { class: 'egg_schedule_detail_icon', }, createNSElementNode( 'svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, [ createNSElementNode('path', undefined, { 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', }), createNSElementNode('path', undefined, { 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', }), ] ) ), createElementNode( 'div', undefined, { class: 'egg_schedule_detail_time' }, createTextNode(schedule.time) ), ] ), createElementNode( 'div', undefined, { class: 'egg_schedule_detail_del_wrap' }, [ createElementNode( 'button', undefined, { class: 'egg_schedule_del_btn', onclick: debounce(() => { // 定时刷新 if (!settings[SettingType.SCHEDULE_RUN]) { createTip('未开启定时刷新!'); return; } // 索引 const index = scheduleList.findIndex( (s) => s === schedule ); // 删除元素 scheduleList.splice(index, 1); // 存储 GM_setValue( 'scheduleList', JSON.stringify(scheduleList) ); // 刷新任务 refreshScheduleTask(); }, 300), }, createNSElementNode( 'svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, [ createNSElementNode('path', undefined, { 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', }), createNSElementNode('path', undefined, { 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', }), ] ) ), ] ), ] ) ) : [ createElementNode( 'div', undefined, { class: 'egg_schedule_list_none' }, [ createNSElementNode( 'svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, [ createNSElementNode('path', undefined, { 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', }), createNSElementNode('path', undefined, { 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', }), createNSElementNode('path', undefined, { 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', }), ] ), createElementNode( 'div', undefined, { class: 'egg_schedule_list_none_text', }, createTextNode('暂无定时任务') ), ] ), ]; }) ); } export { ScheduleList }; ================================================ FILE: src/component/ScoreItem.ts ================================================ import { refreshScoreInfo } from '../controller/user'; import { login, taskConfig, todayScore, totalScore } from '../shared'; import { ref, watchEffectRef } from '../utils/composition'; import { createElementNode, createNSElementNode, createTextNode, } from '../utils/element'; import { debounce } from '../utils/utils'; /** * @description 分数详情 */ function ScoreItem() { return watchEffectRef(() => { if (login.value) { // 分数显示 const scoreShow = ref(false); // 分数信息 return createElementNode( 'div', undefined, { class: 'egg_score_item', }, createElementNode('div', undefined, { class: 'egg_scoreinfo' }, [ createElementNode( 'div', undefined, { class: 'egg_totalscore', }, [ createTextNode('总积分'), createElementNode( 'span', undefined, undefined, createTextNode(totalScore) ), ] ), createElementNode( 'div', undefined, { class: 'egg_todayscore', }, [ createElementNode( 'button', undefined, { type: 'button', class: 'egg_todayscore_btn', title: '查看分数详情', onclick: debounce(() => { scoreShow.value = !scoreShow.value; }, 300), onblur: () => { scoreShow.value = false; }, }, [ createTextNode('当天分数'), // 当天分数 createElementNode( 'span', undefined, undefined, createTextNode(todayScore) ), // icon createNSElementNode( 'svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { 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', }) ), createElementNode( 'div', undefined, { class: watchEffectRef( () => `egg_score_details${scoreShow.value ? '' : ' hide'}` ), }, [ createElementNode( 'div', undefined, { class: 'egg_score_title' }, [ createNSElementNode( 'svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, [ createNSElementNode('path', undefined, { d: 'M314.81 304.01h415.86v58.91H314.81zM314.81 440.24h415.86v58.91H314.81z', }), createNSElementNode('path', undefined, { 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', }), ] ), createElementNode( 'div', undefined, { class: 'egg_score_title_text', }, createTextNode('积分详情') ), ] ), ...taskConfig.map((task) => createElementNode( 'div', undefined, { class: 'egg_score_item' }, [ createTextNode(task.title), createElementNode( 'span', undefined, { class: 'egg_score_detail', }, createTextNode(watchEffectRef(() => task.score)) ), ] ) ), ] ), ] ), ] ), ]), { onMounted() { // 刷新分数信息 refreshScoreInfo(); }, } ); } }); } export { ScoreItem }; ================================================ FILE: src/component/Select.ts ================================================ import { reactive, Ref, ref, shallowRef, watchEffectRef, watchRef, watch, } from '../utils/composition'; import { createElementNode, createTextNode } from '../utils/element'; import { debounce } from '../utils/utils'; function Select({ data, maxlength, placeholder = '', onchange, onblur, value, keep, }: { data: { label: string; value: T; selected?: boolean }[]; maxlength?: number; placeholder?: string; onchange?: (data: { value: T; label: string }) => void; onblur?: (data: { value: T; label: string } | undefined) => void; value?: Ref; keep?: boolean; }) { const selectData = reactive< { label: string; value: T; selected: boolean; active: boolean; ele: HTMLElement | undefined; }[] >( data.map((v) => ({ selected: false, active: false, ele: undefined, ...v })) ); const focus = ref(false); const input = shallowRef(undefined); const list = shallowRef(undefined); const valueRef = ref(''); value && watch( value, () => { const item = selectData.find((v) => v.value === value.value); valueRef.value = item ? item.label : ''; if (!item) { selectData.forEach((v) => (v.selected = false)); list.value && (list.value.scrollTop = 0); } }, true ); return createElementNode( 'div', undefined, { class: 'egg_select', }, [ createElementNode( 'input', { value: valueRef }, { class: 'egg_select_input', type: 'text', placeholder, maxlength, ref: input, onfocus() { if (list.value && input.value) { focus.value = true; if (input.value.value && valueRef.value) { const index = selectData.findIndex( (v) => v.label === valueRef.value ); if (index + 1) { list.value.scrollTop = selectData[index].ele?.offsetTop || 0; selectData.forEach((v, i) => (v.selected = i === index)); } return; } } }, oninput() { if (list.value && input.value) { const { value } = input.value; // 文本存在 if (value) { const index = selectData.findIndex((v) => v.label.includes(value) ); // 存在匹配 if (index + 1) { list.value.scrollTop = selectData[index].ele?.offsetTop || 0; selectData.forEach((v, i) => { v.active = i === index; v.active && setTimeout(() => { v.active = false; }, 300); }); } return; } // 清除 selectData.forEach((v) => (v.active = v.selected = false)); list.value.scrollTop = 0; } }, onblur() { if (list.value && input.value) { const item = selectData.find((v) => v.selected); // 关闭选项 if (item || !input.value.value) { setTimeout(() => { focus.value = false; }, 100); } // 恢复文本 if (item && input.value.value !== item.label) { input.value.value = item.label; } // 保留文本 if (!item && keep) { input.value.value = valueRef.value; } onblur && onblur( item ? { label: item.label, value: item.value } : undefined ); } }, } ), createElementNode( 'div', undefined, { class: watchEffectRef( () => `egg_select_list${focus.value ? '' : ' hide'}` ), ref: list, }, selectData.map((v, index) => createElementNode( 'div', undefined, { class: watchRef( () => [v.selected, v.active], () => `egg_select_item${ v.selected ? ' selected' : v.active ? ' active' : '' }` ), ref: (e) => (v.ele = e), onclick: debounce(() => { if (valueRef.value !== v.label) { onchange && onchange({ label: v.label, value: v.value }); selectData.forEach((v, i) => { v.selected = i === index; v.selected && (valueRef.value = v.label); }); } focus.value = false; }, 300), }, createTextNode(v.label) ) ) ), ] ); } export { Select }; ================================================ FILE: src/component/SettingsPanel.ts ================================================ import { version } from '../config/version'; import { clearScheduleTask, refreshScheduleTask } from '../controller/schedule'; import { createTip } from '../controller/tip'; import { maxRead, maxWatch, pushToken, scheduleList, settings, themeColor, } from '../shared'; import { SettingType } from '../types'; import { Ref, ref, watch, watchEffectRef } from '../utils/composition'; import { $$, createElementNode, createNSElementNode, createTextNode, } from '../utils/element'; import { formatDateNum } from '../utils/time'; import { debounce } from '../utils/utils'; import { ScheduleList } from './ScheduleList'; import { Select } from './Select'; import { TimeInput } from './TimeInput'; /** * @description 设置面板组件 * @returns */ function SettingsPanel({ show }: { show: Ref }) { // token let token = ''; // 小时 let hour = ref(-1); // 分钟 let minute = ref(-1); return createElementNode( 'div', undefined, { class: watchEffectRef(() => `egg_settings${show.value ? ' active' : ''}`), }, [ createElementNode( 'div', undefined, { class: 'egg_settings_version_wrap' }, [ createElementNode( 'div', undefined, { class: 'egg_settings_label' }, createTextNode('版本信息') ), createElementNode( 'div', undefined, { class: 'egg_settings_version', }, [ createTextNode(`v${version}`), createElementNode( 'a', undefined, { class: 'egg_settings_version_detail', title: 'GitHub Xu22Web/tech-study-js', href: 'https://github.com/Xu22Web/tech-study-js', }, createNSElementNode( 'svg', undefined, { viewBox: '0 0 16 16', class: 'egg_icon', }, createNSElementNode('path', undefined, { d: 'M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z', }) ) ), ] ), ] ), createElementNode( 'div', undefined, { class: 'egg_settings_theme_wrap' }, [ createElementNode( 'div', undefined, { class: 'egg_settings_label' }, createTextNode('主题预设') ), createElementNode( 'div', undefined, { class: 'egg_settings_theme_colors' }, [ { value: '#fa3333', title: '强国红', detail: 'XueXi Red', code: 'none', }, { value: '#bb2649', title: '非凡洋红', detail: 'Viva Magenta', code: '18-1750', }, { value: '#35548a', title: '经典蓝', detail: 'Classic Blue', code: '19-4052', }, { value: '#f36f63', title: '活珊瑚橘', detail: 'Living Coral', code: '16-1546', }, { value: '#6d5b97', title: '紫外光色', detail: 'Ultra Violet', code: '18-3838', }, { value: '#86af49', title: '草木绿', detail: 'Greenery', code: '15-0343', }, { value: '#fc8bab', title: 'B站粉', detail: 'Bilibili Pink', code: 'none', }, { value: '#056de8', title: '知乎蓝', detail: 'Zhihu Blue', code: 'none', }, ].map((color) => createElementNode( 'div', undefined, { class: 'egg_settings_theme_color_wrap', }, createElementNode('button', undefined, { class: 'egg_settings_theme_color', type: 'button', style: watchEffectRef( () => `color: ${color.value};${ themeColor.value === color.value ? '' : ` box-shadow: 0rem 0.4rem 0.1rem 0.1rem ${color.value}30;` }` ), title: color.title, onclick: debounce(() => { if (themeColor.value !== color.value) { themeColor.value = color.value; // 存储 GM_setValue('themeColor', themeColor.value); } }, 300), }) ) ) ), ] ), createElementNode( 'div', undefined, { class: 'egg_settings_read_time_wrap', }, [ createElementNode( 'div', undefined, { class: 'egg_settings_label' }, createTextNode('最大文章时长') ), Select({ data: [ { label: '40s', value: 40, }, { label: '60s', value: 60, }, { label: '80s', value: 80, }, { label: '100s', value: 100, }, ], value: maxRead, placeholder: '100s', maxlength: 4, keep: true, onchange({ value }) { // 创建提示 createTip('最大文章时长 已保存!'); maxRead.value = value; // 存储 GM_setValue('maxRead', value); }, }), ], { onMounted() { try { const maxReadTemp = GM_getValue('maxRead'); if (maxReadTemp) { maxRead.value = maxReadTemp; } } catch (e) {} }, } ), createElementNode( 'div', undefined, { class: 'egg_settings_watch_time_wrap', }, [ createElementNode( 'div', undefined, { class: 'egg_settings_label' }, createTextNode('最大视听时长') ), Select({ data: [ { label: '40s', value: 40, }, { label: '60s', value: 60, }, { label: '80s', value: 80, }, { label: '100s', value: 100, }, { label: '120s', value: 120, }, ], value: maxWatch, placeholder: '120s', maxlength: 4, keep: true, onchange({ value }) { // 创建提示 createTip('最大视听时长 已保存!'); maxWatch.value = value; // 存储 GM_setValue('maxWatch', value); }, }), ], { onMounted() { try { const maxWatchTemp = GM_getValue('maxWatch'); if (maxWatchTemp) { maxWatch.value = maxWatchTemp; } } catch (e) {} }, } ), watchEffectRef(() => settings[SettingType.REMOTE_PUSH] ? createElementNode( 'div', undefined, { class: 'egg_settings_token_wrap' }, [ createElementNode( 'div', undefined, { class: 'egg_settings_token' }, [ createElementNode( 'div', undefined, { class: 'egg_settings_label' }, createTextNode('我的 token') ), createElementNode('input', undefined, { class: 'egg_settings_token_input', placeholder: '用户 token', maxlength: 32, value: pushToken.value, onfocus: (e: Event) => { const input = e.target; input.classList.add('active'); const btnWrap = $$('.egg_settings_submit_btn_wrap')[0]; btnWrap.classList.add('active'); }, onblur: (e: Event) => { const input = e.target; // 去除空格 const value = input.value.trim(); if (/^[0-9a-z]{32}$/.test(value)) { token = value; input.value = value; } else { token = ''; } input.classList.remove('active'); setTimeout(() => { const btnWrap = $$( '.egg_settings_submit_btn_wrap' )[0]; btnWrap.classList.remove('active'); input.value = pushToken.value; }, 200); }, }), ] ), createElementNode( 'div', undefined, { class: 'egg_settings_submit_btn_wrap' }, createElementNode( 'button', undefined, { class: 'egg_settings_submit_btn', onclick: debounce(() => { // 创建提示 createTip('用户 token 已保存!'); if (token !== pushToken.value) { pushToken.value = token; // 存储 GM_setValue('pushToken', token); } }, 300), }, createTextNode('保存') ) ), ] ) : undefined ), watchEffectRef(() => settings[SettingType.SCHEDULE_RUN] ? createElementNode('div', undefined, { class: 'egg_schedule' }, [ createElementNode( 'div', undefined, { class: 'egg_schedule_time_wrap' }, [ createElementNode( 'div', undefined, { class: 'egg_schedule_time' }, [ createElementNode( 'div', undefined, { class: 'egg_schedule_label' }, createTextNode('设置时间') ), createElementNode( 'div', undefined, { class: 'egg_schedule_time_input_wrap' }, [ TimeInput({ hour, minute, onchange({ hour: h, minute: min }) { hour.value = h; minute.value = min; }, }), createElementNode( 'button', undefined, { class: 'egg_schedule_add_btn', onclick: debounce(() => { // 定时刷新 if (!settings[SettingType.SCHEDULE_RUN]) { createTip('未开启定时刷新!'); return; } if (hour.value === -1 || minute.value === -1) { createTip('时间格式不符合要求!'); return; } // 重复定时存在 const exists = scheduleList.find( (schedule) => schedule.hour === hour.value && schedule.minute === minute.value ); if (exists) { createTip('设置定时任务重复!'); return; } createTip('设置定时任务成功!'); // 添加 scheduleList.push({ hour: hour.value, minute: minute.value, time: `${formatDateNum( hour.value )}:${formatDateNum(minute.value)}`, }); // 排序 scheduleList.sort((a, b) => a.hour === b.hour ? a.minute - b.minute : a.hour - b.hour ); // 存储 GM_setValue( 'scheduleList', JSON.stringify(scheduleList) ); // 清空 hour.value = -1; minute.value = -1; const inputs = $$( '.egg_time_input input' ); inputs.forEach((i) => (i.value = '')); // 刷新任务 refreshScheduleTask(); }, 300), }, createNSElementNode( 'svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { d: 'M801.171 483.589H544V226.418c0-17.673-14.327-32-32-32s-32 14.327-32 32v257.171H222.83c-17.673 0-32 14.327-32 32s14.327 32 32 32H480v257.17c0 17.673 14.327 32 32 32s32-14.327 32-32v-257.17h257.171c17.673 0 32-14.327 32-32s-14.327-32-32-32z', }) ) ), ] ), ] ), ] ), ScheduleList(), ]) : undefined ), ], { onMounted() { // 刷新token watch( () => settings[SettingType.REMOTE_PUSH], () => { // 远程推送 if (settings[SettingType.REMOTE_PUSH]) { try { const tokenTemp = GM_getValue('pushToken'); if (tokenTemp) { pushToken.value = tokenTemp; } } catch (e) {} } }, true ); // 刷新定时任务 watch( () => settings[SettingType.SCHEDULE_RUN], () => { // 定时任务打开 if (settings[SettingType.SCHEDULE_RUN]) { try { const scheduleTemp = JSON.parse(GM_getValue('scheduleList')); if (scheduleTemp && Array.isArray(scheduleTemp)) { for (const i in scheduleTemp) { scheduleList[i] = scheduleTemp[i]; } } } catch (e) {} // 刷新定时任务 refreshScheduleTask(); return; } // 清除任务 clearScheduleTask(); }, true ); }, } ); } export { SettingsPanel }; ================================================ FILE: src/component/TaskBtn.ts ================================================ import { doExamPractice } from '../controller/exam'; import { closeFrame } from '../controller/frame'; import { readNews, watchVideo } from '../controller/readAndWatch'; import { createTip } from '../controller/tip'; import { frame, login, pushToken, running, settings, taskConfig, taskStatus, todayScore, totalScore, userinfo, } from '../shared'; import { SettingType, TaskStatusType, TaskType } from '../types'; import { watch, watchEffectRef, watchRef } from '../utils/composition'; import { createElementNode, createTextNode } from '../utils/element'; import { error, log } from '../utils/log'; import { getHighlightHTML, getProgressHTML, pushModal } from '../utils/push'; import { debounce, studyPauseLock } from '../utils/utils'; /** * @description 任务按钮 */ function TaskBtn() { return watchEffectRef(() => { if (login.value) { /** * @description 学习 */ async function study() { // 创建提示 createTip('开始学习!'); // 暂停 await studyPauseLock(); // 文章选读 if ( taskConfig[TaskType.READ].active && !taskConfig[TaskType.READ].status ) { log('任务一: 文章选读'); // 创建提示 createTip('任务一: 文章选读'); // 暂停 await studyPauseLock(); // 看新闻 await readNews(); } log('任务一: 文章选读已完成!'); // 视听学习 if ( taskConfig[TaskType.WATCH].active && !taskConfig[TaskType.WATCH].status ) { log('任务二: 视听学习'); // 创建提示 createTip('任务二: 视听学习'); // 暂停 await studyPauseLock(); // 看视频 await watchVideo(); } log('任务二: 视听学习已完成!'); // 每日答题 if ( taskConfig[TaskType.PRACTICE].active && !taskConfig[TaskType.PRACTICE].status ) { log('任务三: 每日答题'); // 创建提示 createTip('任务三: 每日答题'); // 暂停 await studyPauseLock(); // 做每日答题 await doExamPractice(); } log('任务三: 每日答题已完成!'); } /** * @description 暂停任务 */ function pauseTask() { // 全局暂停 GM_setValue('pauseStudy', true); taskStatus.value = TaskStatusType.PAUSE; } /** * @description 继续任务 */ function continueTask() { // 全局暂停 GM_setValue('pauseStudy', false); taskStatus.value = TaskStatusType.START; } /** * @description 开始任务 */ async function startTask() { // 未完成任务 if (taskConfig.some((task) => task.active && !task.status)) { // 开始任务 taskStatus.value = TaskStatusType.START; try { // 学习 await study(); // 同屏任务 if (settings[SettingType.SAME_TAB]) { // 关闭窗口 closeFrame(); // 窗口不存在 frame.exist = false; } } catch (err: unknown) { if (err instanceof Error) { // 提示 createTip(err.message); // 错误 error(err.message); return; } // 提示 createTip(String(err)); // 错误 error(err); } } // 刷新任务 taskStatus.value = TaskStatusType.FINISH; log('已完成'); // 创建提示 createTip('完成学习!'); // 远程推送 if (settings[SettingType.REMOTE_PUSH]) { // 推送 const res = await pushModal( { title: '学习推送', to: userinfo.nick, content: [ '学习强国, 学习完成!', `当天积分: ${getHighlightHTML(todayScore.value)} 分`, `总积分: ${getHighlightHTML(totalScore.value)} 分`, ...taskConfig.map((task) => getProgressHTML( task.title, task.currentScore, task.dayMaxScore ) ), ], type: 'success', }, pushToken.value ); createTip(`学习推送${res ? '成功' : '失败'}!`); } } // 已在等待 let flag = false; // 自动答题 watch( () => [taskStatus.value, settings[SettingType.AUTO_START]], async () => { // 加载完毕 if (!flag && taskStatus.value === TaskStatusType.LOADED) { // 自动答题 if (settings[SettingType.AUTO_START]) { // 等待中 flag = true; // 创建提示 const tip = createTip('即将自动开始任务', 5, true); // 等待倒计时结束 await tip.waitCountDown(); // 再次查看是否开启 if ( settings[SettingType.AUTO_START] && taskStatus.value !== TaskStatusType.START ) { // 创建提示 createTip('自动开始任务'); // 开始任务 startTask(); return; } // 取消等待 flag = false; // 创建提示 createTip('已取消自动开始任务!'); } } } ); // 切换开关任务未完成 taskConfig.forEach((task) => { watch( () => [task.active], () => { if (taskStatus.value === TaskStatusType.FINISH) { if (task.active && !task.status) { taskStatus.value = TaskStatusType.LOADED; } } } ); }); return createElementNode( 'div', undefined, { class: 'egg_study_item' }, createElementNode( 'button', undefined, { class: watchEffectRef( () => `egg_study_btn${ taskStatus.value === TaskStatusType.START ? ' loading' : '' }` ), type: 'button', disabled: watchRef( () => [running.value, taskStatus.value], () => running.value || taskStatus.value === TaskStatusType.LOADING || taskStatus.value === TaskStatusType.FINISH ), onclick: watchEffectRef(() => taskStatus.value === TaskStatusType.LOADED ? debounce(startTask, 300) : taskStatus.value === TaskStatusType.START ? debounce(pauseTask, 300) : taskStatus.value === TaskStatusType.PAUSE ? debounce(continueTask, 300) : undefined ), }, createTextNode( watchEffectRef( () => `${ taskStatus.value === TaskStatusType.LOADING ? '等待中' : taskStatus.value === TaskStatusType.LOADED ? '开始学习' : taskStatus.value === TaskStatusType.START ? '正在学习, 点击暂停' : taskStatus.value === TaskStatusType.PAUSE ? '继续学习' : taskStatus.value === TaskStatusType.FINISH ? '已完成' : '' }` ) ) ) ); } }); } export { TaskBtn }; ================================================ FILE: src/component/TaskItem.ts ================================================ import { Ref, watchEffectRef } from '../utils/composition'; import { createElementNode, createTextNode } from '../utils/element'; /** * @description 设置任务项 * @returns */ function TaskItem({ title, tip, checked, currentScore, dayMaxScore, onchange, immutable, }: { title: string; tip: string; checked: Ref; currentScore: Ref; dayMaxScore: Ref; onchange: (...args: any[]) => void; immutable: boolean; }) { return createElementNode( 'div', undefined, { class: 'egg_task_item', }, [ createElementNode('div', undefined, { class: 'egg_label_wrap' }, [ createElementNode('div', undefined, { class: 'egg_task_title_wrap' }, [ createElementNode( 'div', undefined, { class: 'egg_task_title' }, createTextNode(title) ), createElementNode( 'div', undefined, { class: 'egg_task_progress_wrap' }, [ createElementNode( 'div', undefined, { class: 'egg_task_current', }, createTextNode(currentScore) ), createElementNode( 'div', undefined, { class: 'egg_task_max', }, createTextNode(watchEffectRef(() => `/${dayMaxScore.value}`)) ), ] ), ]), createElementNode('div', undefined, { class: 'egg_progress' }, [ createElementNode( 'div', undefined, { class: 'egg_track' }, createElementNode('div', undefined, { class: 'egg_bar', style: watchEffectRef( () => `width: ${( (100 * currentScore.value) / dayMaxScore.value ).toFixed(1)}%;` ), }) ), ]), ]), createElementNode('input', undefined, { title: tip, class: 'egg_switch', type: 'checkbox', checked, onchange, disabled: immutable, }), ] ); } export { TaskItem }; ================================================ FILE: src/component/TaskList.ts ================================================ import { createTip } from '../controller/tip'; import { refreshTaskList } from '../controller/user'; import { login, taskConfig, taskStatus } from '../shared'; import { TaskStatusType, TaskType } from '../types'; import { watch, watchEffectRef } from '../utils/composition'; import { createElementNode } from '../utils/element'; import { debounce } from '../utils/utils'; import { TaskItem } from './TaskItem'; /** * @description 任务 */ function TaskList() { // 处理任务设置变化 const handleTaskChange = (e: Event, type: TaskType, title: string) => { // 开关 const { checked } = e.target; if (taskConfig[type].active !== checked) { taskConfig[type].active = checked; // 设置 GM_setValue('taskConfig', JSON.stringify(taskConfig)); // 创建提示 createTip(`${title} ${checked ? '打开' : '关闭'}!`); } }; // 登录加载 watch( login, async () => { if (login.value) { // 加载任务列表 await refreshTaskList(); // 未完成任务 if (taskConfig.some((task) => task.active && !task.status)) { // 全局暂停 GM_setValue('pauseStudy', false); // 加载完毕 taskStatus.value = TaskStatusType.LOADED; return; } // 任务完毕 taskStatus.value = TaskStatusType.FINISH; } }, true ); return createElementNode( 'div', undefined, { class: 'egg_task_list', }, taskConfig.map((label) => label.immutable ? TaskItem({ title: label.title, tip: label.tip, checked: watchEffectRef(() => label.active), currentScore: watchEffectRef(() => label.currentScore), dayMaxScore: watchEffectRef(() => label.dayMaxScore), onchange: debounce((e) => { handleTaskChange(e, label.type, label.title); }, 300), immutable: label.immutable, }) : TaskItem({ title: label.title, tip: label.tip, checked: watchEffectRef(() => label.active), currentScore: watchEffectRef(() => label.currentScore), dayMaxScore: watchEffectRef(() => label.dayMaxScore), onchange: debounce((e) => { handleTaskChange(e, label.type, label.title); }, 300), immutable: label.immutable, }) ) ); } export { TaskList }; ================================================ FILE: src/component/TimeInput.ts ================================================ import { Ref, ref, watchEffectRef } from '../utils/composition'; import { createElementNode, createTextNode } from '../utils/element'; import { formatDateNum } from '../utils/time'; import { Select } from './Select'; /** * @description 时间输入 * @returns */ function TimeInput({ hour, minute, onchange, }: { hour: Ref; minute: Ref; onchange?: (data: { hour: number; minute: number }) => void; }) { // 小时 const hours = new Array(24).fill(undefined).map((v, i) => ({ value: i, label: formatDateNum(i), })); // 分钟 const minutes = new Array(60).fill(undefined).map((v, i) => ({ value: i, label: formatDateNum(i), })); const valueRef = watchEffectRef(() => { const h = hours.find((h) => h.value === hour.value); const min = minutes.find((min) => min.value === minute.value); return { hour: h ? h.value : -1, minute: min ? min.value : -1, }; }); return createElementNode('div', undefined, { class: 'egg_time_input' }, [ createElementNode('div', undefined, { class: 'egg_hour_wrap' }, [ Select({ data: hours, placeholder: '00', maxlength: 2, value: hour, onchange({ value }) { valueRef.value.hour = value; onchange && onchange(valueRef.value); }, onblur(res) { if (!res) { valueRef.value.hour = -1; onchange && onchange(valueRef.value); } }, }), ]), createElementNode( 'span', undefined, { class: 'egg_separator' }, createTextNode(':') ), createElementNode('div', undefined, { class: 'egg_minute_wrap' }, [ Select({ data: minutes, placeholder: '00', maxlength: 2, value: minute, onchange({ value }) { valueRef.value.minute = value; onchange && onchange(valueRef.value); }, onblur(res) { if (!res) { valueRef.value.minute = -1; onchange && onchange(valueRef.value); } }, }), ]), ]); } export { TimeInput }; ================================================ FILE: src/component/Tip.ts ================================================ import { Ref, watchEffectRef, watchRef } from '../utils/composition'; import { createElementNode, createTextNode } from '../utils/element'; function Tip({ text, count, show, delayShow, countShow, callback, }: { text: Ref; count: Ref; show: Ref; delayShow: Ref; countShow: Ref; callback: (count: number) => void; }) { return createElementNode( 'div', undefined, { class: watchRef( [show, delayShow], () => `egg_tip${ show.value ? (delayShow.value ? ' active delay' : ' active') : '' }` ), }, [ createElementNode( 'span', undefined, { class: 'egg_text', }, createTextNode(text) ), watchEffectRef(() => countShow.value ? createElementNode( 'span', undefined, { class: 'egg_countdown', }, createTextNode(watchEffectRef(() => `${count.value}s`)) ) : undefined ), ], { onMounted() { // 倒计时 const countDown = async () => { // 倒计时回调 await callback(count.value); // 倒计时结束 if (!count.value) { show.value = false; return; } count.value--; setTimeout(countDown, 1000); }; countDown(); }, } ); } export { Tip }; ================================================ FILE: src/config/api.ts ================================================ /** * @description api配置 */ const API_CONFIG = { // 用户信息 userInfo: 'https://pc-api.xuexi.cn/open/api/user/info', // 总分 totalScore: 'https://pc-proxy-api.xuexi.cn/delegate/score/get', // 当天分数 todayScore: 'https://pc-proxy-api.xuexi.cn/delegate/score/today/query', // 任务列表 taskList: 'https://pc-proxy-api.xuexi.cn/delegate/score/days/listScoreProgress?sence=score&deviceType=2', // 新闻数据 todayNews: [ 'https://www.xuexi.cn/lgdata/35il6fpn0ohq.json', 'https://www.xuexi.cn/lgdata/1ap1igfgdn2.json', 'https://www.xuexi.cn/lgdata/vdppiu92n1.json', 'https://www.xuexi.cn/lgdata/152mdtl3qn1.json', ], // 视频数据 todayVideos: [ 'https://www.xuexi.cn/lgdata/525pi8vcj24p.json', 'https://www.xuexi.cn/lgdata/11vku6vt6rgom.json', 'https://www.xuexi.cn/lgdata/2qfjjjrprmdh.json', 'https://www.xuexi.cn/lgdata/3o3ufqgl8rsn.json', 'https://www.xuexi.cn/lgdata/591ht3bc22pi.json', 'https://www.xuexi.cn/lgdata/1742g60067k.json', 'https://www.xuexi.cn/lgdata/1novbsbi47k.json', ], // 专项练习列表 paperList: 'https://pc-proxy-api.xuexi.cn/api/exam/service/paper/pc/list', // 文本服务器保存答案 answerSave: 'https://a6.qikekeji.com/txt/data/save', // 文本服务器获取答案 answerSearch: 'https://a6.qikekeji.com/txt/data/detail', // 推送 push: 'https://www.pushplus.plus/send', // 生成二维码 generateQRCode: 'https://login.xuexi.cn/user/qrcode/generate', //二维码登录 loginWithQRCode: 'https://login.xuexi.cn/login/login_with_qr', // 签名 sign: 'https://pc-api.xuexi.cn/open/api/sns/sign', // 安全检查 secureCheck: 'https://pc-api.xuexi.cn/login/secure_check', // 二维码 qrcode: 'https://api.qrserver.com/v1/create-qr-code', }; export default API_CONFIG; ================================================ FILE: src/config/compile.ts ================================================ import ts from 'typescript'; import { ModuleFormat } from 'rollup'; import terser from '@rollup/plugin-terser'; /** * @description 编译配置 */ const COMPILE_CONFIG = { input: { file: 'src/index.ts', }, output: { file: 'tech-study.js', }, /** * @description 目标版本 */ target: ts.ScriptTarget.ESNext, /** * @description 模块版本 */ module: ts.ModuleKind.ESNext, /** * @description 是否压缩代码 */ compress: false, /** * @description rollup 配置 */ rollupConfig: { inputOptions: { input: 'src/index.js' }, outputOptions: { file: 'src/index.min.js', format: 'es', plugins: [terser()], }, }, }; export default COMPILE_CONFIG; ================================================ FILE: src/config/script.ts ================================================ import { version } from './version'; /** * @description 脚本配置 */ const SCRIPT_CONFIG = { /** * @description 脚本名 */ name: '不学习何以强国', /** * @description 命名空间 */ namespace: 'http://tampermonkey.net/', /** * @description 版本 */ version, /** * @description 脚本描述 */ description: '有趣的 `学习强国` 油猴插件。读文章,看视频,做习题。问题反馈: https://github.com/Xu22Web/tech-study-js/issues 。', /** * @description 作者 */ author: '原作者:techxuexi 荷包蛋。现作者:Xu22Web', /** * @description 链接匹配 */ match: [ 'https://www.xuexi.cn/*', 'https://pc.xuexi.cn/points/exam-practice.html', 'https://pc.xuexi.cn/points/exam-weekly-detail.html?id=*', 'https://pc.xuexi.cn/points/exam-paper-detail.html?id=*', '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', ], /** * @description 所需脚本 */ require: ['https://cdn.jsdelivr.net/npm/blueimp-md5@2.9.0'], /** * @description 脚本注入的时间 */ 'run-at': 'document-start', /** * @description 权限 */ grant: [ 'GM_addStyle', 'GM_setValue', 'GM_getValue', 'GM_openInTab', 'GM_addValueChangeListener', 'unsafeWindow', ], updateURL: 'https://raw.githubusercontent.com/Xu22Web/tech-study-js/master/tech-study.js', downloadURL: 'https://raw.githubusercontent.com/Xu22Web/tech-study-js/master/tech-study.js', supportURL: 'https://github.com/Xu22Web', }; export default SCRIPT_CONFIG; ================================================ FILE: src/config/task.ts ================================================ /* task·配置 */ /** * @description 单次最大新闻数 */ const maxNewsNum = 6; /** * @description 单次最大视频数 */ const maxVideoNum = 6; /** * @description 二维码最大刷新次数 */ const maxRefreshCount = 10; /** * @description 二维码自动刷新间隔 */ const autoRefreshQRCodeInterval = 100000; export { maxNewsNum, maxVideoNum, maxRefreshCount, autoRefreshQRCodeInterval }; ================================================ FILE: src/config/url.ts ================================================ /** * @description url配置 */ const URL_CONFIG = { // 主页正则 home: /^https\:\/\/www\.xuexi\.cn(\/(index\.html)?)?$/, // 主页 homeOrigin: 'https://www.xuexi.cn', // 每日答题页面 examPractice: 'https://pc.xuexi.cn/points/exam-practice.html', // 专项练习页面 examPaper: 'https://pc.xuexi.cn/points/exam-paper-detail.html', }; export default URL_CONFIG; ================================================ FILE: src/config/version.ts ================================================ /** * @description 版本号 */ const version = '1.7.5'; export { version }; ================================================ FILE: src/controller/exam.ts ================================================ import { getAnswer, saveAnswer } from '../api/answer'; import { getExamPaper } from '../api/data'; import URL_CONFIG from '../config/url'; import { examPause, frame, pushToken, running, settings, taskConfig, userinfo, } from '../shared'; import { SettingType, TaskType } from '../types'; import { $$, $_ } from '../utils/element'; import { log } from '../utils/log'; import { pushModal } from '../utils/push'; import { createRandomPath, createRandomPoint } from '../utils/random'; import { hasMobile, sleep, studyPauseLock } from '../utils/utils'; import { handleCloseTaskWin, waitTaskWin } from './frame'; import { createTip } from './tip'; import { refreshScoreInfo, refreshTaskList } from './user'; /** * @description 考试类型 */ enum ExamType { PRACTICE, PAPER, } /** * @description 获取答题按钮 */ function getNextButton() { return new Promise((resolve) => { const timer = setInterval(() => { // 答题按钮 const nextAll = $$('.ant-btn').filter( (next) => next.innerText ); if (nextAll.length) { // 停止定时器 clearInterval(timer); if (nextAll.length === 2) { resolve(nextAll[1]); return; } resolve(nextAll[0]); } }, 500); }); } /** * @description 处理滑动验证 */ function handleSlideVerify() { return new Promise(async (resolve) => { // 滑动验证 const mask = $$('#nc_mask')[0]; if (mask && getComputedStyle(mask).display !== 'none') { // 创建提示 createTip('等待滑动验证'); // 提高层级 mask.style.zIndex = '999'; // 轨道 const track = (await $_('.nc_scale', undefined, 3000))[0]; // 滑块 const slide = (await $_('.btn_slide', undefined, 3000))[0]; // 延时 await sleep(2000); // 矩形范围 const rectTrack = track.getBoundingClientRect(); // 矩形范围 const rectSlide = slide.getBoundingClientRect(); // 窗口 const window = unsafeWindow; // 范围内随机起点 const start = createRandomPoint(rectSlide); // 终点 const end = { x: rectTrack.x + rectTrack.width, y: rectTrack.y + rectTrack.height / 2, }; // 路径 const path = createRandomPath(start, end, 10); // 移动端 const mobile = hasMobile(); if (mobile) { slide.style.touchAction = 'none'; const touchstartTouch = new Touch({ identifier: 0, target: slide, clientX: path[0].x, clientY: path[0].y, }); const touchstartList = [touchstartTouch]; // 开始触摸 const touchstart = new TouchEvent('touchstart', { targetTouches: touchstartList, touches: touchstartList, changedTouches: touchstartList, view: window, bubbles: true, }); slide.dispatchEvent(touchstart); // 触摸滑动 for (const i in path) { const touchmoveTouch = new Touch({ identifier: 0, target: slide, clientX: path[i].x, clientY: path[i].y, }); const touchmoveList = [touchmoveTouch]; const touchmove = new TouchEvent('touchmove', { targetTouches: touchmoveList, touches: touchmoveList, changedTouches: touchmoveList, view: window, bubbles: true, }); slide.dispatchEvent(touchmove); await sleep(10); } const touchendTouch = new Touch({ identifier: 0, target: slide, clientX: path[path.length - 1].x, clientY: path[path.length - 1].y, }); // 触摸结束 const touchendList = [touchendTouch]; // 开始触摸 const touchend = new TouchEvent('touchend', { targetTouches: [], touches: [], changedTouches: touchendList, view: window, bubbles: true, }); slide.dispatchEvent(touchend); } else { // 鼠标按下 const mousedown = new MouseEvent('mousedown', { clientX: path[0].x, clientY: path[0].y, bubbles: true, view: window, }); slide.dispatchEvent(mousedown); // 鼠标滑动 for (const i in path) { const mousemove = new MouseEvent('mousemove', { clientX: path[i].x, clientY: path[i].y, bubbles: true, view: window, }); slide.dispatchEvent(mousemove); await sleep(10); } // 鼠标抬起 const mouseup = new MouseEvent('mouseup', { clientX: path[path.length - 1].x, clientY: path[path.length - 1].y, bubbles: true, view: window, }); slide.dispatchEvent(mouseup); } // 创建提示 createTip('滑动验证完成!'); // 定时器 const timer = setInterval(() => { // 滑动验证 const mask = $$('#nc_mask')[0]; if (!mask || getComputedStyle(mask).display === 'none') { log('滑动验证成功!'); // 创建提示 createTip('滑动验证成功!'); clearInterval(timer); resolve(true); return; } resolve(false); log('滑动验证失败!'); // 创建提示 createTip('滑动验证失败!'); }, 1000); return; } resolve(true); }); } /** * @description 处理选项 */ function handleChoiceBtn(answers: string[]) { // 选项按钮 const allBtns = $$('.q-answer'); // 答案存在 if (answers.length && allBtns.length) { // 作答 return answers.every((answer) => { // 答案存在 if (answer && answer.length) { // 包含答案最短长度选项 let minLengthChoice: HTMLButtonElement | undefined; // 遍历 allBtns.forEach((choice) => { // 选项文本 const choiceText = choice.innerText.trim(); // 无符号选项文本 const unsignedChoiceText = choiceText.replaceAll(/[、,,。 ]/g, ''); // 无符号答案 const unsignedAnswer = answer.replaceAll(/[、,,。 ]/g, ''); // 包含答案 if ( choiceText === answer || choiceText.includes(answer) || answer.includes(choiceText) || unsignedChoiceText.includes(unsignedAnswer) ) { // 最小长度选项有值 if (minLengthChoice) { // 最短长度选项与当前选项比较长度 if (minLengthChoice.innerText.length > choiceText.length) { minLengthChoice = choice; } } else { // 最小长度选项赋值 minLengthChoice = choice; } } }); // 存在选项 if (minLengthChoice) { // 选择 if (!minLengthChoice.classList.contains('chosen')) { minLengthChoice.click(); } return true; } } return false; }); } return false; } /** * @description 随机处理单选 */ function handleSingleChoiceRand() { // 选项按钮 const allBtns = $$('.q-answer'); // 按钮存在 if (allBtns.length) { const index = ~~(Math.random() * allBtns.length); const randBtn = allBtns[index]; // 选择 if (!randBtn.classList.contains('chosen')) { randBtn.click(); } } } /** * @description 随机处理多选 */ function handleMutiplyChoiceRand() { // 选项按钮 const allBtns = $$('.q-answer'); // 按钮存在 if (allBtns.length) { allBtns.forEach((allBtn) => { // 选择 if (!allBtn.classList.contains('chosen')) { allBtn.click(); } }); } } /** * @description 处理填空 */ const handleBlankInput = (answers: string[]) => { // 所有填空 const blanks = $$('.blank'); // 答案存在 if (blanks.length && answers.length) { // 填空数量和答案数量一致 if (answers.length === blanks.length) { return answers.every((answer, i) => { // 答案存在 if (answer && answer.length) { // 输入事件 const inputEvent = new Event('input', { bubbles: true, }); // 设置答案 blanks[i].setAttribute('value', answer); // 触发输入input blanks[i].dispatchEvent(inputEvent); return true; } return false; }); } // 填空数量为1和提示数量大于1 if (blanks.length === 1 && answers.length > 1) { // 直接将所有答案整合填进去 const answer = answers.join(''); // 答案存在 if (answer && answer.length) { // 输入事件 const inputEvent = new Event('input', { bubbles: true, }); // 设置答案 blanks[0].setAttribute('value', answer); // 触发输入input blanks[0].dispatchEvent(inputEvent); return true; } } } return false; }; /** * @description 处理填空随机 */ async function handleBlankInputRand() { // 所有填空 const blanks = $$('.blank'); if (blanks.length) { // 输入事件 const inputEvent = new Event('input', { bubbles: true, }); blanks.forEach((blank) => { // 设置答案 blank.setAttribute('value', '答案'); // 触发输入input blank.dispatchEvent(inputEvent); }); } } /** * @description 暂停锁 */ function examPauseLock(callback?: (status: boolean) => void) { return new Promise((resolve) => { // 学习暂停 const pauseStudy = (GM_getValue('pauseStudy') || false); // 全局暂停 if (pauseStudy) { examPause.value = true; } // 暂停 if (examPause.value) { // 创建提示 createTip('已暂停, 手动开启自动答题! ', 10); const doing = setInterval(() => { if (!examPause.value) { // 停止定时器 clearInterval(doing); log('答题等待结束!'); if (callback && callback instanceof Function) { // 创建提示 createTip('已开启, 自动答题!'); callback(true); } resolve(true); return; } if (callback && callback instanceof Function) { callback(false); } log('答题等待...'); }, 500); return; } resolve(true); }); } /** * @description 答题 */ async function doingExam(type: ExamType) { // 下一个按钮 let nextButton: HTMLButtonElement; // 下一个文本 let nextText: string; // 保存答案 let shouldSaveAnswer = false; while (true) { // 先等等再开始做题 await sleep(2500); // 暂停 await examPauseLock(); // 获取下一个按钮 nextButton = await getNextButton(); // 下一个文本 nextText = nextButton.innerText.replaceAll(' ', ''); // 结束 const finish = ['再练一次', '再来一组', '查看解析']; if (finish.includes(nextButton.innerText)) { break; } // 点击提示 $$('.tips')[0]?.click(); // 所有提示 const allTips = $$('.line-feed font[color]'); // 答案 const answers = allTips.map((tip) => tip.innerText.trim()); // 获取题目的文本内容 const question = $$('.q-body')[0].innerText; // 等待一段时间 await sleep(1500); // 暂停 await examPauseLock(); // 选项按钮 const allBtns = $$('.q-answer'); // 所有填空 const blanks = $$('input[type=text][class=blank]'); // 问题类型 const questionType = <'填空题' | '单选题' | '多选题'>( $$('.q-header')[0].innerText.substring(0, 3) ); // 暂停 await examPauseLock(); // 题型分类作答 switch (questionType) { case '填空题': { // 根据提示作答 if (answers.length) { const res = handleBlankInput(answers); // 成功 if (res) { break; } } // 创建提示 createTip('答案异常, 尝试网络题库获取!'); log('正在获取答案...'); // 尝试题库获取 const answersNetwork = await getAnswer(question); log(`获取答案${answersNetwork.length ? '成功' : '失败'}!`, { question, answersNetwork, }); // 根据题库作答 if (answersNetwork.length) { const res = handleBlankInput(answersNetwork); // 成功 if (res) { break; } } // 随机作答 if (type === ExamType.PRACTICE || settings[SettingType.RANDOM_EXAM]) { log('答案不存在, 随机作答!'); // 创建提示 createTip('答案不存在, 随机作答!'); await handleBlankInputRand(); } else { // 推送 const res = await pushModal( { title: '学习推送', to: userinfo.nick, content: '答题存在异常, 已暂停答题!', type: 'fail', }, pushToken.value ); createTip(`学习推送${res ? '成功' : '失败'}!`); // 暂停 examPause.value = true; // 提交答案 shouldSaveAnswer = true; } break; } case '多选题': { // 根据提示作答 if (answers.length) { // 选项文本 const choicesText = allBtns.map((btn) => btn.innerText); // 选项内容 const choicesContent = choicesText .map((choiceText) => choiceText.split(/[A-Z]./)[1].trim()) .join(''); // 空格 const blanks = question.match(/()/g); // 填空数量、选项数量、答案数量相同 | 选项全文等于答案全文 if ( (blanks && allBtns.length === blanks.length) || question === choicesContent || allBtns.length === 2 ) { // 全选 allBtns.forEach((choice) => { if (!choice.classList.contains('chosen')) { choice.click(); } }); break; } // 选项数量大于等于答案 if (allBtns.length >= answers.length) { const res = handleChoiceBtn(answers); // 成功 if (res) { break; } } } // 创建提示 createTip('答案异常, 尝试网络题库获取!'); log('正在获取答案...'); // 尝试题库获取 const answersNetwork = await getAnswer(question); log(`获取答案${answersNetwork.length ? '成功' : '失败'}!`, { question, answersNetwork, }); // 答案存在 if (answersNetwork.length) { const res = handleChoiceBtn(answersNetwork); // 成功 if (res) { break; } } // 随机作答 if (type === ExamType.PRACTICE || settings[SettingType.RANDOM_EXAM]) { log('答案不存在, 随机作答!'); // 创建提示 createTip('答案不存在, 随机作答!'); await handleMutiplyChoiceRand(); } else { // 推送 const res = await pushModal( { title: '学习推送', to: userinfo.nick, content: '答题存在异常, 已暂停答题!', type: 'fail', }, pushToken.value ); createTip(`学习推送${res ? '成功' : '失败'}!`); // 暂停 examPause.value = true; // 提交答案 shouldSaveAnswer = true; } break; } case '单选题': { // 根据提示作答 if (answers.length) { // 创建提示为1 if (answers.length === 1) { const res = handleChoiceBtn(answers); // 成功 if (res) { break; } } else { // 可能的分隔符 const seperator = [ '', ' ', ',', ';', ',', '、', '-', '|', '+', '/', ]; // 可能的答案 const answersLike = seperator .map((s) => answers.join(s).trim()) .filter((answer) => answer.length); // 答案存在 if (answersLike.length) { // 可能答案是否正确 const res = answersLike.some((answer) => { // 尝试查找点击 return handleChoiceBtn([answer]); }); if (res) { break; } } } } // 创建提示 createTip('答案异常, 尝试网络题库获取!'); log('正在获取答案...'); // 尝试题库获取 const answersNetwork = await getAnswer(question); log(`获取答案${answersNetwork.length ? '成功' : '失败'}!`, { question, answersNetwork, }); // 存在答案 if (answersNetwork.length) { // 单答案单选项 if (answersNetwork.length === 1) { // 尝试查找点击 const res = handleChoiceBtn(answersNetwork); if (res) { break; } } else { // 多答案单选项 选项意外拆分 // 可能分隔符 const seperator = ['', ' ']; // 可能答案 const answersLike = seperator.map((s) => answers.join(s)); // 答案存在 if (answersLike.every((answer) => answer.length)) { // 可能答案是否正确 const res = answersLike.some((answer) => { // 尝试查找点击 return handleChoiceBtn([answer]); }); if (res) { break; } } } } // 随机作答 if (type === ExamType.PRACTICE || settings[SettingType.RANDOM_EXAM]) { log('答案不存在, 随机作答!'); // 创建提示 createTip('答案不存在, 随机作答!'); await handleSingleChoiceRand(); } else { // 推送 const res = await pushModal( { title: '学习推送', to: userinfo.nick, content: '答题存在异常, 已暂停答题!', type: 'fail', }, pushToken.value ); createTip(`学习推送${res ? '成功' : '失败'}!`); // 暂停 examPause.value = true; // 提交答案 shouldSaveAnswer = true; } break; } } // 暂停 await examPauseLock(); // 获取下一个按钮 nextButton = await getNextButton(); // 下一个文本 nextText = nextButton.innerText.replaceAll(' ', ''); // 需要提交答案 if (shouldSaveAnswer) { // 答案 const answers: string[] = []; if (questionType === '填空题') { blanks.forEach((blank) => { answers.push(blank.value); }); } if (questionType === '单选题' || questionType === '多选题') { allBtns.forEach((choice) => { if (choice.classList.contains('chosen')) { // 带字母的选项 const answerTemp = choice.innerText; // 从字符串中拿出答案 const [, answer] = answerTemp.split('.'); if (answer && answer.length) { answers.push(answer); } } }); } // 答案 const answer = answers.join(';'); // 存在答案 if (answer.length) { log('正在上传答案...'); // 上传答案 const res = await saveAnswer(question, answer); log(`上传答案${res ? '成功' : '失败'}!`, { question, answer }); } // 重置 shouldSaveAnswer = false; } // 确定 if (nextText === '确定') { // 确认 nextButton.click(); // 等待一段时间 await sleep(2000); // 暂停 await examPauseLock(); // 答案解析 const answerBox = $$('.answer')[0]; // 答题错误 if (answerBox) { const answerTemp = answerBox.innerText; // 从字符串中拿出答案 const [, answerText] = answerTemp.split(':'); if (answerText && answerText.length) { const answer = answerText.replaceAll(' ', ';'); log('正在上传答案...'); // 上传答案 const res = await saveAnswer(question, answer); log(`上传答案${res ? '成功' : '失败'}!`, { question, answer }); } } } // 获取按钮 nextButton = await getNextButton(); // 下一个文本 nextText = nextButton.innerText.replaceAll(' ', ''); if (nextText === '下一题' || nextText === '完成' || nextText === '交卷') { // 等待一段时间 await sleep(2500); // 下一题 nextButton.click(); } // 滑动验证 await handleSlideVerify(); } // 关闭任务窗口 handleCloseTaskWin(); } /** * @description 每日答题 */ async function doExamPractice() { // 暂停 await studyPauseLock(); log('正在每日答题...'); // 创建提示 createTip('正在每日答题'); // 链接 const url = URL_CONFIG.examPractice; // 等待任务窗口 await waitTaskWin(url, '每日答题'); // 创建提示 createTip('完成每日答题!'); // 等待一段时间 await sleep(1500); // 刷新分数数据 await refreshScoreInfo(); // 刷新任务数据 await refreshTaskList(); // 任务完成状况 if ( taskConfig[TaskType.PRACTICE].active && !taskConfig[TaskType.PRACTICE].status ) { log('任务未完成, 继续每日答题!'); // 创建提示 createTip('任务未完成, 继续每日答题!'); await doExamPractice(); } } /** * @description 专项练习 */ async function doExamPaper() { running.value = true; log('正在专项练习...'); // 创建提示 createTip('正在专项练习'); // id const examPaperId = await findExamPaper(); if (examPaperId) { // 链接 const url = `${URL_CONFIG.examPaper}?id=${examPaperId}`; log(`链接: ${url}`); // 等待窗口任务 await waitTaskWin(url, '专项练习'); // 创建提示 createTip('完成专项练习!'); running.value = false; // 同屏任务 if (settings[SettingType.SAME_TAB]) { // 窗口不存在 frame.exist = false; } return; } running.value = false; // 创建提示 createTip('专项练习均已完成!'); } /** * @description 初始化总页数属性 */ async function initExam() { // 默认从第一页获取全部页属性 const data = await getExamPaper(1); if (data) { // 等待 await sleep(3000); return data.totalPageCount; } } /** * @description 查询专项练习列表 */ async function findExamPaper() { // 获取总页数 const total = await initExam(); // 当前页数 let current = 1; log(`正在寻找的专项练习...`); // 创建提示 createTip(`正在寻找的专项练习...`); while (current <= total && current) { // 请求数据 const data = await getExamPaper(current); if (data) { // 获取专项练习的列表 const examPapers = data.list; for (const i in examPapers) { // 遍历查询有没有没做过的 if (examPapers[i].status !== 2) { // status: 1 开始答题, 2 已满分/重新答题, 3 继续答题 return examPapers[i].id; } } // 增加页码 current += 1; // 等待 await sleep(3000); } else { break; } } } export { doingExam, doExamPaper, doExamPractice, ExamType }; ================================================ FILE: src/controller/frame.ts ================================================ import URL_CONFIG from '../config/url'; import { frame, id, page, settings } from '../shared'; import { SettingType } from '../types'; import { $_ } from '../utils/element'; import { log } from '../utils/log'; import { generateMix } from '../utils/random'; /** * @description 初始化主页面 */ function initMainListener() { // 监听关闭 window.addEventListener('message', (msg: MessageEvent) => { const { data } = msg; if (data.id === id.value && data.closed) { // 关闭窗口 closeFrame(); return; } }); } /** * @description 初始化子页面 */ function initChildListener() { window.addEventListener('message', (msg: MessageEvent) => { const { data } = msg; if (data.id && !data.closed) { // 设置窗口id id.value = data.id; log(`初始化窗口 ID: ${id.value}`); return; } }); } /** * @description 打开窗口 * @param url * @returns */ async function openFrame(url: string, title?: string) { // 设置 URL frame.src = url; // 等待元素 await $_('.egg_frame'); if (frame.ele) { // id id.value = generateMix(10); // 打开 frame.closed = false; // 设置标题 frame.title = title || ''; // 等待页面加载 await waitFrameLoaded(frame.ele); // 发送窗口 ID frame.ele.contentWindow?.postMessage({ id: id.value, closed: false }, url); return true; } return false; } /** * @description 关闭窗口 */ function closeFrame() { log(`关闭窗口 ID: ${id.value}`); // 窗口显示 frame.show = false; // 关闭 frame.closed = true; // 标题 frame.title = ''; // src frame.src = ''; } /** * @description 关闭 frame */ function handleCloseFrame() { window.parent.postMessage( { id: id.value, closed: true }, URL_CONFIG.homeOrigin ); } /** * @description 等待窗口任务结束 * @param id * @returns */ function waitFrameClose() { return new Promise((resolve) => { const timer = setInterval(() => { // 窗口关闭 if (frame.closed) { clearInterval(timer); resolve(true); } }, 100); }); } // 等待窗口加载 function waitFrameLoaded(iframe: HTMLElement) { return new Promise((resolve) => { iframe.addEventListener('load', () => { resolve(true); }); }); } /** * @description 打开新窗口 */ function openWin(url: string) { return GM_openInTab(url, { active: true, insert: true, setParent: true, }); } /** * @description 关闭窗口 */ function closeWin() { page.value && page.value.close(); } /** * @description 关闭子窗口 */ function handleCloseWin() { try { window.opener = window; const win = window.open('', '_self'); win?.close(); top?.close(); } catch (e) {} } /** * @description 等待窗口关闭 * @param newPage * @returns */ function waitWinClose(newPage: Tampermonkey.OpenTabObject) { return new Promise((resolve) => { newPage.onclose = () => { resolve(undefined); }; }); } /** * @description 关闭任务窗口 */ function closeTaskWin() { // 同屏任务 if (settings[SettingType.SAME_TAB] && id.value) { closeFrame(); return; } // 非同屏任务 closeWin(); } /** * @description 关闭任务窗口 */ function handleCloseTaskWin() { // 同屏任务 if (settings[SettingType.SAME_TAB] && id.value) { handleCloseFrame(); return; } // 子窗口 handleCloseWin(); } /** * @description 打开并等待任务结束 */ async function waitTaskWin(url: string, title?: string) { // 同屏任务 if (settings[SettingType.SAME_TAB]) { // 窗口存在 frame.exist = true; // 显示窗体 frame.show = !settings[SettingType.SILENT_RUN]; // 新窗口 const res = await openFrame(url, title); if (res) { // 等待窗口关闭 await waitFrameClose(); } return; } // 子页面任务 page.value = openWin(url); await waitWinClose(page.value); } export { openFrame, closeFrame, waitFrameClose, waitFrameLoaded, openWin, closeWin, waitWinClose, waitTaskWin, closeTaskWin, initMainListener, initChildListener, handleCloseTaskWin, }; ================================================ FILE: src/controller/login.ts ================================================ import { generateQRCode, getSign, loginWithQRCode, secureCheck, } from '../api/login'; import API_CONFIG from '../config/api'; import { autoRefreshQRCodeInterval, maxRefreshCount } from '../config/task'; import { frame, login, loginQRCodeShow, pushToken, refreshCount, settings, taskConfig, taskStatus, todayScore, totalScore, userinfo, } from '../shared'; import { SettingType, TaskStatusType } from '../types'; import { $$ } from '../utils/element'; import { log } from '../utils/log'; import { getHighlightHTML, getImgHTML, getProgressHTML, pushModal, } from '../utils/push'; import { delCookie } from '../utils/utils'; import { closeFrame } from './frame'; import { createTip } from './tip'; import { refreshScoreInfo, refreshTaskList, refreshUserInfo } from './user'; /** * @description 二维码刷新定时器 */ let refreshTimer = -1; /** * @description 尝试登录 */ let tryLoginTimer = -1; /** * @description 生成二维码 */ async function getQRCode() { log('正在生成登录二维码...'); const qrCode = await generateQRCode(); if (qrCode) { log('生成登录二维码成功!'); // 链接 const url = `https://login.xuexi.cn/login/qrcommit?showmenu=false&code=${qrCode}&appId=dingoankubyrfkttorhpou`; return { code: qrCode, src: `${API_CONFIG.qrcode}?data=${encodeURIComponent(url)}`, url, }; } log('生成登录二维码失败!'); } /** * @description 验证登录二维码 * @param code * @returns */ async function checkQRCode(code: string) { log('尝试用二维码登录...'); // 二维码登录 const res = await loginWithQRCode(code); if (res) { const { data, code, success } = res; // 临时登录验证码 if (success && data) { return data; } // 二维码失效 if (code === '11019') { return; } } return new Promise((resolve) => { // 清除定时 clearTimeout(tryLoginTimer); // 设置定时 tryLoginTimer = (setTimeout(async () => { resolve(await checkQRCode(code)); }, 1000)); }); } /** * @description 尝试二维码登录 */ async function tryLogin(checkCode: string) { log('正在获取签名...'); // 获取签名 const sign = await getSign(); if (sign) { // 生成uuid const uuid = crypto.randomUUID(); const [, code] = checkCode.split('='); const state = `${sign}${uuid}`; // 安全检查 const res = await secureCheck({ code, state }); return res; } } /** * @description 刷新登录二维码 */ async function handleLogin() { // 清除刷新 clearInterval(refreshTimer); // 每隔一段时间刷新 refreshTimer = (setInterval(() => { // 刷新二维码 handleLogin(); }, autoRefreshQRCodeInterval)); // 是否超出次数 if (refreshCount.value >= maxRefreshCount) { createTip('超过最大重试次数, 登录失败!'); // 重置刷新数 refreshCount.value = 0; // 隐藏二维码 loginQRCodeShow.value = false; // 远程推送 if (settings[SettingType.REMOTE_PUSH]) { // 推送 const res = await pushModal( { title: '登录推送', content: '超过最大重试次数, 登录失败!', type: 'fail', }, pushToken.value ); createTip(`登录推送${res ? '成功' : '失败'}!`); } return; } // 配置 const imgWrap = $$('.egg_login_img_wrap')[0]; // 图片 const img = $$('.egg_login_img', imgWrap)[0]; if (imgWrap && img) { // 刷新二维码 log('刷新登录二维码!'); // 刷新次数累加 refreshCount.value++; // 获取二维码 const qrCode = await getQRCode(); if (qrCode) { // 获取连接 const { src, code, url } = qrCode; // src img.src = src; // 开始登录 loginQRCodeShow.value = true; // 远程推送 if (settings[SettingType.REMOTE_PUSH]) { // img html const imgWrap = getImgHTML(src); // 跳转链接 const aWrap = ` `; // 推送 const res = await pushModal( { title: '登录推送', content: ['扫一扫, 登录学习强国!', aWrap, imgWrap], type: 'info', }, pushToken.value ); createTip(`登录推送${res ? '成功' : '失败'}!`); } // 获取验证码 const checkCode = await checkQRCode(code); // 验证成功 if (checkCode) { // 尝试登录 const loginRes = await tryLogin(checkCode); if (loginRes) { // 清除刷新 clearInterval(refreshTimer); // 二维码显示 loginQRCodeShow.value = false; // 登录成功 log('登录成功!'); // 创建提示 createTip('登录成功!'); // 登录成功 login.value = true; // 刷新用户信息 await refreshUserInfo(); // 刷新分数信息 await refreshScoreInfo(); // 刷新任务信息 await refreshTaskList(); // 远程推送 if (settings[SettingType.REMOTE_PUSH]) { const res = await pushModal( { title: '登录推送', to: userinfo.nick, content: [ '学习强国, 登录成功!', `当天积分: ${getHighlightHTML(todayScore.value)} 分`, `总积分: ${getHighlightHTML(totalScore.value)} 分`, ...taskConfig.map((task) => getProgressHTML(task.title, task.currentScore,task.dayMaxScore) ), ], type: 'success', }, pushToken.value ); createTip(`登录推送${res ? '成功' : '失败'}!`); } } return; } // 二维码失效 log('登录二维码失效!'); // 二维码失效刷新 handleLogin(); } } } /** * @description 退出登录 */ function handleLogout() { // 删除token delCookie('token', '.xuexi.cn'); // 关闭窗口 closeFrame(); frame.exist = false; // 退出登录 login.value = false; // 清除用户信息 userinfo.nick = ''; userinfo.avatar = ''; // 总分 totalScore.value = 0; // 当天分数 todayScore.value = 0; // 任务进度重置 taskConfig.forEach((task) => { task.currentScore = 0; }); taskStatus.value = TaskStatusType.LOADING; // 退出登录 log('退出登录'); } export { getQRCode, checkQRCode, tryLogin, handleLogin, handleLogout }; ================================================ FILE: src/controller/readAndWatch.ts ================================================ import { getNewsList, getVideoList } from '../api/data'; import { maxNewsNum, maxVideoNum } from '../config/task'; import { maxRead, maxWatch, settings, taskConfig } from '../shared'; import { NewsVideoList, SettingType, TaskType } from '../types'; import { watchEffect } from '../utils/composition'; import { $$, $_ } from '../utils/element'; import { log } from '../utils/log'; import { sleep, studyPauseLock } from '../utils/utils'; import { handleCloseTaskWin, waitTaskWin } from './frame'; import { createTip } from './tip'; import { refreshScoreInfo, refreshTaskList } from './user'; /** * @description 新闻 */ let news: NewsVideoList = []; /** * @description 视频 */ let videos: NewsVideoList = []; /** * @description 处理文章 */ async function handleNews() { // section const sections = await $_('section', undefined, 5000); const section = sections[0]; if (!(section && section.innerText.includes('系统正在维护中'))) { // 文章选读 reading(0); return; } log('未找到文章!'); // 提示 createTip('未找到文章!'); // 关闭页面 handleCloseTaskWin(); } /** * @description 处理视频 */ async function handleVideo() { // videos const videos = await $_('video', undefined, 10000); // 视频 const video = videos[0]; // 播放按键 const playBtn = $$('.prism-play-btn')[0]; if (video && playBtn) { log('正在尝试播放视频...'); // 播放超时 const timeout = setTimeout(() => { log('视频播放超时!'); // 提示 createTip('视频播放超时!'); // 关闭页面 handleCloseTaskWin(); }, 20000); // 设置是否静音 watchEffect(() => (video.muted = settings[SettingType.VIDEO_MUTED])); // 能播放 video.addEventListener( 'canplay', () => { const timer = setInterval(() => { // 尝试点击播放按钮播放 playBtn.click(); // 播放未成功 if (video.paused) { // 尝试使用js的方式播放 video.play(); } }, 1000); video.addEventListener( 'playing', () => { // 清除超时定时器 clearTimeout(timeout); // 清除定时器 clearInterval(timer); log('播放视频成功!'); // 视听学习 reading(1); return; }, { once: true } ); }, { once: true } ); return; } log('未找到视频!'); // 关闭页面 handleCloseTaskWin(); } /** * @description 读新闻或者看视频 * @param type :0为新闻,1为视频 */ async function reading(type: number) { let time = 30; // 文章选读 if (type === 0) { // 章节 const sections = $$('section'); // 最大字数 const maxTextCount = Math.max( ...sections.map((s) => s.innerText.length), 200 ); // 预计时间 const predictTime = ~~((60 * maxTextCount) / 1000); // min(predictTime, maxWatch.value) 秒后关闭页面 time = Math.min(predictTime, maxRead.value); } // 视听学习 if (type === 1) { // 视频 const video = $$('video')[0]; // 预计时间 const predictTime = ~~video.duration; // min(predictTime, maxWatch.value) 秒后关闭页面 time = Math.min(predictTime, maxWatch.value); } // 随机 time = time - ~~(Math.random() * 10) + 5; // 第一次滚动时间 const firstTime = time - (~~(Math.random() * 4) + 4); // 第二次滚动时间 const secendTime = ~~(Math.random() * 4) + 8; // 窗口 const window = unsafeWindow; // 创建提示 const tip = createTip('距离关闭页面还剩', time, true, async (time) => { // 暂停锁 await studyPauseLock((flag) => { if (type === 1) { // 视频 const video = $$('video')[0]; // 排除反复设置 if (video.paused === !flag) { return; } // 设置播放状态 video[flag ? 'play' : 'pause'](); } }); // 第一次滚动 if (time === firstTime) { // 滚动 window.scrollTo(0, 400); // 模拟滚动 const scroll = new Event('scroll', { bubbles: true, }); document.dispatchEvent(scroll); // 模拟滑动 const mousemove = new MouseEvent('mousemove', { bubbles: true, }); document.dispatchEvent(mousemove); // 模拟点击 const click = new Event('click', { bubbles: true, }); document.dispatchEvent(click); } // 第二次滚动 if (time === secendTime) { // 滚动长度 const scrollLength = document.body.scrollHeight / 2; // 滚动 window.scrollTo(0, scrollLength); // 模拟滚动 const scroll = new Event('scroll', { bubbles: true, }); document.dispatchEvent(scroll); // 模拟滑动 const mousemove = new MouseEvent('mousemove', { bubbles: true, }); document.dispatchEvent(mousemove); // 模拟点击 const click = new Event('click', { bubbles: true, }); document.dispatchEvent(click); } }); // 倒计时结束 await tip.waitCountDown(); // 关闭任务窗口 handleCloseTaskWin(); } /** * @description 获取新闻列表 */ async function getNews() { // 需要学习的新闻数量 const need = taskConfig[TaskType.READ].need < maxNewsNum ? taskConfig[TaskType.READ].need : maxNewsNum; log(`剩余 ${need} 个新闻`); // 获取新闻 const data = await getNewsList(); if (data && data.length) { // 索引 let i = 0; // 最新新闻 const latestItems = data.slice(0, 100); // 当前年份 const currentYear = new Date().getFullYear().toString(); // 查找今年新闻 while (i < need) { const randomIndex = ~~(Math.random() * latestItems.length); // 新闻 const item = latestItems[randomIndex]; // 是否存在 if (item.publishTime.startsWith(currentYear) && item.type === 'tuwen') { news[i] = item; i++; } } } else { news = []; } } /** * @description 获取视频列表 */ async function getVideos() { // 需要学习的视频数量 const need = taskConfig[TaskType.WATCH].need < maxVideoNum ? taskConfig[TaskType.WATCH].need : maxVideoNum; log(`剩余 ${need} 个视频`); // 获取视频 const data = await getVideoList(); if (data && data.length) { // 索引 let i = 0; // 最新视频 const latestItems = data.slice(0, 100); // 当前年份 const currentYear = new Date().getFullYear().toString(); // 查找今年视频 while (i < need) { const randomIndex = ~~(Math.random() * latestItems.length); // 新闻 const item = latestItems[randomIndex]; // 是否存在 if ( item.publishTime.startsWith(currentYear) && (item.type === 'shipin' || item.type === 'juji') ) { videos[i] = item; i++; } } } else { videos = []; } } /** * @description 阅读文章 */ async function readNews() { // 获取文章 await getNews(); // 观看文章 for (const i in news) { // 任务关闭跳出循环 if (!taskConfig[TaskType.READ].active) { return; } // 暂停 await studyPauseLock(); log(`正在阅读第 ${Number(i) + 1} 个新闻...`); // 创建提示 createTip(`正在阅读第 ${Number(i) + 1} 个新闻`); // 链接 const { url } = news[i]; // 链接 GM_setValue('readingUrl', url); // 等待任务窗口 await waitTaskWin(url, '文章选读'); // 清空链接 GM_setValue('readingUrl', null); // 创建提示 createTip(`完成阅读第 ${Number(i) + 1} 个新闻!`); // 等待一段时间 await sleep(1500); // 刷新分数数据 await refreshScoreInfo(); // 刷新任务数据 await refreshTaskList(); // 任务完成跳出循环 if (taskConfig[TaskType.READ].active && taskConfig[TaskType.READ].status) { break; } } // 任务关闭跳出循环 if (!taskConfig[TaskType.READ].active) { return; } // 任务完成状况 if (taskConfig[TaskType.READ].active && !taskConfig[TaskType.READ].status) { log('任务未完成, 继续阅读新闻!'); // 创建提示 createTip('任务未完成, 继续阅读新闻!'); await readNews(); } } /** * @description 观看视频 */ async function watchVideo() { // 获取视频 await getVideos(); // 观看视频 for (const i in videos) { // 任务关闭跳出循环 if (!taskConfig[TaskType.WATCH].active) { return; } // 暂停 await studyPauseLock(); log(`正在观看第 ${Number(i) + 1} 个视频...`); // 创建提示 createTip(`正在观看第 ${Number(i) + 1} 个视频`); // 链接 const { url } = videos[i]; // 链接 GM_setValue('watchingUrl', url); // 等待任务窗口 await waitTaskWin(url, '视听学习'); // 清空链接 GM_setValue('watchingUrl', null); // 创建提示 createTip(`完成观看第 ${Number(i) + 1} 个视频!`); // 等待一段时间 await sleep(1500); // 刷新分数数据 await refreshScoreInfo(); // 刷新任务数据 await refreshTaskList(); // 任务完成跳出循环 if ( taskConfig[TaskType.WATCH].active && taskConfig[TaskType.WATCH].status ) { break; } } // 任务关闭跳出循环 if (!taskConfig[TaskType.WATCH].active) { return; } // 任务完成状况 if (taskConfig[TaskType.WATCH].active && !taskConfig[TaskType.WATCH].status) { log('任务未完成, 继续观看视频!'); // 创建提示 createTip('任务未完成, 继续观看看视频!'); await watchVideo(); } } export { handleNews, handleVideo, readNews, reading, watchVideo }; ================================================ FILE: src/controller/schedule.ts ================================================ import { login, scheduleList } from '../shared'; import { log } from '../utils/log'; import { isLate, isNow } from '../utils/time'; import { handleLogin } from './login'; import { createTip } from './tip'; /** * @description 定时刷新定时器 */ let scheduleTimer = -1; /** * @description 刷新定时任务 */ async function refreshScheduleTask() { // 清除定时刷新 clearInterval(scheduleTimer); // 未登录 if (!login.value) { // 剩余定时任务 const restList = scheduleList.filter((s) => !isLate(s)); // 存在剩余任务 if (restList.length) { const rest = restList[0]; log(`已设置 ${rest.time} 的定时任务!`); // 提示 createTip(`已设置 ${rest.time} 的定时任务!`); // 时间 let time = 0; // 刷新间隔 const interval = 10; scheduleTimer = (setInterval(() => { if (!(time++ % interval)) { log('定时刷新正在运行...'); } // 到达定时 if (isNow(rest)) { clearInterval(scheduleTimer); log(`执行 ${rest.time} 的定时任务!`); // 提示 createTip(`执行 ${rest.time} 的定时任务!`); // 登录 handleLogin(); } }, 1000)); } } } /** * @description 清除定时 */ function clearScheduleTask() { clearInterval(scheduleTimer); } export { refreshScheduleTask, clearScheduleTask }; ================================================ FILE: src/controller/tip.ts ================================================ import { Tip } from '../component/Tip'; import { ref } from '../utils/composition'; import { $$, mountElement } from '../utils/element'; /** * @description 创建学习提示 */ function createTip( text: string, delay: number = 2, countShow: boolean = false, callback?: (current: number) => any ) { const tipWrap = $$('.egg_tip_wrap')[0]; // 提前去除 const tips = $$ void }>('.egg_tip'); if (tips.length) { tips.forEach((t) => t.delay()); } // 延迟 const delayCount = ref(delay); // 文字 const textContent = ref(text); //显示 const show = ref(false); // 延迟显示 const delayShow = ref(false); // 销毁 let destroyed = false; // 倒计时结束 let done = false; // 提示 const tip = Tip({ text: textContent, count: delayCount, show, delayShow, countShow: ref(countShow), callback: async (count) => { callback && (await callback(count)); // 恢复显示 if (delayShow.value && count === delay) { delayShow.value = false; } // 倒计时结束 if (count <= 0) { done = true; operate.destroy(); } }, }); // 操作 const operate = { destroy() { if (!destroyed) { // 隐藏 operate.hide(); // 销毁 destroyed = true; return new Promise((resolve) => { setTimeout(() => { tip.ele.remove(); resolve(undefined); }, 300); }); } }, hide() { if (!destroyed) { show.value = false; } }, show() { if (!destroyed) { return new Promise((resolve) => { setTimeout(() => { show.value = true; resolve(undefined); }, 300); }); } }, setText(text: string) { if (!destroyed) { textContent.value = text; } }, waitCountDown() { return new Promise((resolve) => { // 计时器 const timer = setInterval(() => { // 结束 if (done) { clearInterval(timer); resolve(true); } }, 100); }); }, delay() { if (!destroyed) { delayShow.value = true; delayCount.value += 2; } }, }; Object.assign(tip.ele, operate); // 插入节点 mountElement(tip, tipWrap); // 显示 operate.show(); return operate; } export { createTip }; ================================================ FILE: src/controller/user.ts ================================================ import { getTaskList, getTodayScore, getTotalScore, getUserInfo, } from '../api/user'; import { login, taskConfig, todayScore, totalScore, userinfo } from '../shared'; import { TaskType } from '../types'; import { log } from '../utils/log'; import { sleep } from '../utils/utils'; /** * @description 刷新用户信息 */ async function refreshUserInfo() { // 未登录 if (!login.value) { throw new Error('用户未登录!'); } // 已存在信息 if (userinfo.nick) { return true; } log('加载用户信息...'); // 获取用户信息 const res = await getUserInfo(); if (res) { const { avatarMediaUrl = '', nick: nickRes } = res; if (nickRes) { // 设置昵称 userinfo.nick = nickRes; // 设置头像 userinfo.avatar = avatarMediaUrl; return true; } } log('加载用户信息失败!'); return false; } /** * @description 刷新分数信息 */ async function refreshScoreInfo() { // 未登录 if (!login.value) { throw new Error('用户未登录!'); } log('加载分数信息...'); // 获取总分 const totalScoreRes = await getTotalScore(); // 获取当天总分 const todayScoreRes = await getTodayScore(); // 整数值 if (Number.isInteger(totalScoreRes) && Number.isInteger(todayScoreRes)) { // 设置分数 totalScore.value = totalScoreRes; todayScore.value = todayScoreRes; return true; } log('加载分数信息失败!'); return false; } /** * @description 刷新任务列表 */ async function refreshTaskList() { // 未登录 if (!login.value) { throw new Error('用户未登录!'); } log('加载任务进度...'); // 原始任务进度 const taskProgress = await getTaskList(); if (taskProgress) { // 登录 taskConfig[TaskType.LOGIN].currentScore = taskProgress[2].currentScore; taskConfig[TaskType.LOGIN].dayMaxScore = taskProgress[2].dayMaxScore; taskConfig[TaskType.LOGIN].need = taskProgress[2].dayMaxScore - taskProgress[2].currentScore; // 文章选读 taskConfig[TaskType.READ].currentScore = taskProgress[0].currentScore; taskConfig[TaskType.READ].dayMaxScore = taskProgress[0].dayMaxScore; taskConfig[TaskType.READ].need = taskProgress[0].dayMaxScore - taskProgress[0].currentScore; // 视听学习 taskConfig[TaskType.WATCH].currentScore = taskProgress[1].currentScore; taskConfig[TaskType.WATCH].dayMaxScore = taskProgress[1].dayMaxScore; taskConfig[TaskType.WATCH].need = taskProgress[1].dayMaxScore - taskProgress[1].currentScore; // 每日答题 taskConfig[TaskType.PRACTICE].currentScore = taskProgress[3].currentScore; taskConfig[TaskType.PRACTICE].dayMaxScore = taskProgress[3].dayMaxScore; taskConfig[TaskType.PRACTICE].need = taskProgress[3].dayMaxScore; // 更新数据 for (const i in taskConfig) { const { currentScore, dayMaxScore } = taskConfig[i]; // 进度 const rate = Number(((100 * currentScore) / dayMaxScore).toFixed(1)); // 分数 taskConfig[i].score = currentScore; // 完成状态 taskConfig[i].status = rate === 100; } return; } // 重试 await sleep(2000); refreshTaskList(); return; } export { refreshUserInfo, refreshScoreInfo, refreshTaskList }; ================================================ FILE: src/css/index.css ================================================ * { -webkit-tap-highlight-color: transparent; } :root { --themeColor: #fa3333; --scale: 1; font-size: calc(10px * var(--scale)); } @media (min-height: 678px) and (max-height: 768px) { :root { --scale: 0.8; } } @media (max-height: 667px) { :root { --scale: 0.75; } } @keyframes fade { from { opacity: 0.8; } to { opacity: 0.4; background: #ccc; } } .egg_icon { width: 1em; height: 1em; fill: currentColor; } .egg_hr_wrap { position: relative; display: flex; justify-content: center; color: #ccc; } .egg_hr_wrap .egg_hr { position: absolute; top: 50%; transform: translateY(-50%); background: currentColor; height: 0.1rem; width: 30%; } .egg_hr_wrap .egg_hr:nth-of-type(1) { left: 0; } .egg_hr_wrap .egg_hr:nth-last-of-type(1) { right: 0; } .egg_hr_title { font-size: 1.2rem; } .egg_exam_btn { transition: background 80ms; outline: none; border: none; padding: 1.2rem 2rem; border-radius: 1.2rem; cursor: pointer; font-size: 1.8rem; font-weight: bold; text-align: center; color: #ffffff; background: #ccc; } .egg_exam_btn.manual { background: var(--themeColor); } .egg_panel_wrap * { padding: 0; margin: 0; box-sizing: border-box; outline: none; border: none; } .egg_panel_wrap { position: fixed; left: 0; top: 0; z-index: 99999; width: 100%; height: 100%; color: #333; font-size: 1.6rem; pointer-events: none; } .egg_panel { position: absolute; top: 5rem; left: 1rem; padding: 1.2rem 2rem; border-radius: 1rem; background: #ffffffe6; backdrop-filter: blur(1rem); box-shadow: 0 0 0.1rem 0.1rem #f1f1f1; transition: 80ms ease-out; pointer-events: all; } .egg_panel.hide { left: 0; transform: translateX(-100%); } .egg_panel_wrap.mobile .egg_panel { top: 1rem; } @media (min-height: 678px) and (max-height: 768px) { .egg_panel { top: 2rem; } } @media (max-height: 667px) { .egg_panel { top: 1rem; } } .egg_panel button { outline: none; border: none; padding: 0; cursor: pointer; background: none; } .egg_panel .egg_btns_wrap { position: absolute; left: 100%; top: 50%; transform: translate(-50%, -50%); transition: 80ms ease; z-index: 9; } .egg_panel.hide .egg_btns_wrap { left: 100%; transform: translate(0, -50%); } .egg_panel .egg_btns_wrap button { border-radius: 50%; width: 3rem; height: 3rem; padding: 0; overflow: hidden; border: 0.2rem solid currentColor; color: white; display: grid; place-items: center; font-size: 1.8rem; } .egg_panel.hide .egg_panel_show_btn { background: var(--themeColor); } .egg_panel .egg_panel_show_btn { background: #ccc; } .egg_panel .egg_frame_show_btn { background: var(--themeColor); margin-bottom: 1rem; } .egg_panel .egg_frame_show_btn.hide { display: none; } .egg_panel .egg_settings_show_btn { background: #ccc; margin-top: 1rem; } .egg_panel .egg_settings_show_btn.active { background: var(--themeColor); } .egg_panel .egg_settings_reset_btn { background: #ccc; margin-top: 1rem; } .egg_panel .egg_settings_reset_btn:active { background: var(--themeColor); } .egg_login_item { display: flex; justify-content: center; align-items: center; flex-direction: column; padding: 0.5rem 0; } .egg_login_item .egg_login_btn { font-size: 1.4rem; border-radius: 1rem; transition: 80ms ease; color: white; background: var(--themeColor); padding: 0.8rem 2.4rem; } .egg_login_item .egg_login_btn:active { opacity: 0.8; } .egg_login_item .egg_login_img_wrap { height: 0; border-radius: 1rem; transition: height 80ms ease; overflow: hidden; } .egg_login_item .egg_login_img_wrap.active { padding: 0.8rem; margin-top: 0.8rem; height: auto; background: white; } .egg_login_img_wrap .egg_login_img { width: 15rem; height: 15rem; } .egg_info_item .egg_login_btn { font-size: 1.4rem; border-radius: 1rem; transition: 80ms ease; color: white; background: #ccc; padding: 0.4rem 0.8rem; } .egg_info_item .egg_login_btn:active { opacity: 0.8; } .egg_info_item { display: flex; justify-content: space-between; align-items: center; } .egg_info_item .egg_userinfo { display: flex; justify-content: center; align-items: center; padding: 0.5rem 0; } .egg_userinfo .egg_avatar .egg_avatar_nick, .egg_userinfo .egg_avatar .egg_avatar_img { height: 5rem; width: 5rem; border-radius: 50%; background: var(--themeColor); display: flex; justify-content: center; align-items: center; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; font-size: 2rem; color: white; } .egg_userinfo .egg_nick { padding-left: 0.5rem; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 10rem; } .egg_score_item .egg_scoreinfo { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0; } .egg_scoreinfo .egg_totalscore, .egg_scoreinfo .egg_todayscore { font-size: 1.2rem; user-select: none; } .egg_scoreinfo .egg_totalscore span, .egg_scoreinfo .egg_todayscore .egg_todayscore_btn span { padding-left: 0.2rem; } .egg_scoreinfo .egg_totalscore span, .egg_todayscore .egg_todayscore_btn span, .egg_todayscore .egg_score_details span { color: var(--themeColor); font-weight: bold; } .egg_scoreinfo .egg_todayscore { position: relative; } .egg_todayscore .egg_todayscore_btn { display: flex; align-items: center; } .egg_todayscore_btn .egg_icon { opacity: 0.3; } .egg_todayscore .egg_score_details { position: absolute; left: calc(100% + 1rem); top: 0; background: #fffffff2; border-radius: 0.5rem; opacity: 1; width: 10rem; box-shadow: 0 0 0.1rem 0.1rem #f1f1f1; transition: 80ms ease; z-index: 9; } .egg_todayscore .egg_score_details.hide { visibility: hidden; opacity: 0; left: 100%; } .egg_score_details .egg_score_title { border-bottom: 0.1rem solid #eee; padding: 0.5rem 0.8rem; display: flex; align-items: center; } .egg_score_details .egg_score_title .egg_icon { font-size: 1.4rem; } .egg_score_details .egg_score_title .egg_score_title_text { font-weight: bold; padding-left: 0.2rem; } .egg_score_details .egg_score_item { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0.8rem; } .egg_task_list { position: relative; } .egg_task_item { user-select: none; min-height: 3rem; min-width: 18rem; display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0; } .egg_task_item .egg_label_wrap { flex-grow: 1; padding-right: 0.5rem; } .egg_label_wrap .egg_task_title_wrap { display: flex; justify-content: space-between; align-items: center; } .egg_task_title_wrap .egg_task_progress_wrap { display: flex; align-items: center; font-size: 1.4rem; width: 3.5rem; } .egg_task_progress_wrap .egg_task_current { color: var(--themeColor); } .egg_task_progress_wrap .egg_task_max { color: #999; font-size: 1.2rem; } .egg_label_wrap .egg_progress { display: flex; justify-content: space-between; align-items: center; padding-top: 0.8rem; } .egg_progress .egg_track { background: #ccc; height: 0.5rem; border-radius: 1rem; flex: 1 1 auto; overflow: hidden; } .egg_progress .egg_track .egg_bar { height: 0.5rem; background: var(--themeColor); border-radius: 1rem; width: 0; transition: width 0.5s; } .egg_setting_item { min-height: 3rem; min-width: 18rem; display: flex; align-items: center; justify-content: space-between; box-sizing: border-box; } .egg_setting_item .egg_label_wrap { flex-grow: 1; } .egg_detail { background: #ccc; color: white; border-radius: 10rem; font-size: 1.2rem; width: 1.6rem; height: 1.6rem; margin-left: 0.4rem; display: inline-block; text-align: center; line-height: 1.6rem; cursor: pointer; } .egg_switch { cursor: pointer; margin: 0; outline: 0; appearance: none; -webkit-appearance: none; -moz-appearance: none; position: relative; width: 4.2rem; height: 2.2rem; background: #ccc; border-radius: 5rem; transition: background 0.3s; --border-padding: 0.5rem; box-shadow: -0.1rem 0 0.1rem -0.1rem #999 inset, 0.1rem 0 0.1rem -0.1rem #999 inset; } .egg_switch::after { content: ''; display: inline-block; width: 1.4rem; height: 1.4rem; border-radius: 50%; background: #fff; box-shadow: 0 0 0.2rem #999; transition: left 0.4s; position: absolute; top: calc(50% - (1.4rem / 2)); position: absolute; left: var(--border-padding); } .egg_switch:checked { background: var(--themeColor); } .egg_switch:disabled { opacity: 0.5; background: #ccc; } .egg_switch:checked::after { left: calc(100% - var(--border-padding) - 1.4rem); } .egg_tip_list { font-size: 1.2rem; max-width: 18rem; line-height: 2rem; color: var(--themeColor); } .egg_tip_list .egg_tip_btn { padding: 0.2rem 0.4rem; background: #f1f1f1; color: #333; } .egg_tip_list .egg_tip_btn:disabled { opacity: 0.5; background: #ccc; } .egg_tip_list .egg_tip_content { text-align: center; padding-top: 0.2rem; } .egg_study_item { display: flex; justify-content: center; padding-top: 0.5rem; } .egg_study_item .egg_study_btn { background: var(--themeColor); padding: 0.8rem 2.4rem; font-size: 1.4rem; border-radius: 1rem; color: white; transition: 80ms ease; } .egg_study_item .egg_study_btn:not(.loading):active { opacity: 0.8; } .egg_study_item .egg_study_btn.loading { animation: fade 2s ease infinite alternate; } .egg_study_item .egg_study_btn:disabled { background: #ccc; } .egg_tip_wrap { position: fixed; left: 0; top: 0; z-index: 999999; width: 100%; height: 100%; pointer-events: none; } .egg_tip_wrap * { padding: 0; margin: 0; box-sizing: border-box; outline: none; border: none; } .egg_tip_wrap .egg_tip { position: absolute; bottom: 2rem; left: 2rem; padding: 1.2rem 1.4rem; border: none; border-radius: 1rem; background: var(--themeColor); color: white; font-size: 1.4rem; transition: 200ms ease; opacity: 0; transform: scale(0.9) translateY(1rem); } .egg_tip_wrap .egg_tip.active { opacity: 1; transform: scale(1) translateY(0); } .egg_tip_wrap .egg_tip.active.delay { opacity: 0.5; } .egg_tip_wrap .egg_tip .egg_countdown { display: inline-block; color: var(--themeColor); background: white; border-radius: 0.5rem; padding: 0.2rem 0.4rem; font-weight: bold; margin-left: 0.4rem; font-size: 1.2rem; } .egg_frame_wrap { position: fixed; left: 0; top: 0; z-index: 999; width: 100%; height: 100%; visibility: visible; } .egg_frame_wrap * { padding: 0; margin: 0; box-sizing: border-box; outline: none; border: none; } .egg_frame_wrap.hide { visibility: hidden; } .egg_frame_wrap.hide .egg_frame_mask, .egg_frame_wrap.hide .egg_frame_content_wrap { opacity: 0; } .egg_frame_wrap.hide .egg_frame_content_wrap { transform: scale(0); } .egg_frame_mask { background: #00000030; width: 100%; height: 100%; opacity: 1; transition: 200ms ease; } .egg_frame_content_wrap { position: absolute; width: 80%; height: 80%; top: 10%; left: 10%; display: flex; flex-direction: column; transition: 200ms ease; border-radius: 1rem; background: #ffffffe6; backdrop-filter: blur(1rem); overflow: hidden; transform: scale(1); } .egg_frame_content_wrap.max { top: 0; left: 0; width: 100%; height: 100%; border-radius: 0; } .egg_frame_content_wrap .egg_frame_controls_wrap { width: 100%; display: flex; justify-content: space-between; align-items: center; box-sizing: border-box; } .egg_frame_controls_wrap .egg_frame_title { padding: 1rem 2rem; font-size: 1.6rem; } .egg_frame_controls .egg_frame_btn { outline: none; border: none; background: none; padding: 1rem 2rem; transition: 80ms ease; cursor: pointer; color: #333; font-size: 1.8rem; } .egg_frame_controls .egg_frame_btn:active { opacity: 0.8; } .egg_frame_wrap .egg_frame_content { width: 100%; flex-grow: 1; border-top: 0.1rem solid #ccc; min-height: 40rem; min-width: 30rem; background: white; } .egg_frame_content .egg_frame { width: 100%; height: 100%; outline: none; border: none; } .egg_time_input { display: inline-flex; align-items: center; justify-content: center; } .egg_time_input .egg_hour_wrap, .egg_time_input .egg_minute_wrap { width: 4rem; } .egg_time_input .egg_separator { padding: 0 0.5rem; font-size: 1.5rem; } .egg_settings_item { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; overflow: hidden; border-radius: 1rem; } .egg_settings_item .egg_settings { display: inline-flex; flex-direction: column; font-size: 1.4rem; background: white; border-radius: 1rem; overflow: hidden; width: 100%; height: 100%; pointer-events: all; transform: translateX(100%); transition: transform 300ms ease; padding-top: 1rem; } .egg_settings_item .egg_settings.active { transform: translateX(0); } .egg_settings .egg_settings_label { padding-bottom: 1rem; user-select: none; } .egg_settings_item .egg_settings_version_wrap { padding: 1rem 2rem 0 2rem; display: flex; align-items: center; justify-content: space-between; } .egg_settings_version_wrap .egg_settings_version { color: #999; display: flex; align-items: center; } .egg_settings_version .egg_settings_version_detail { color: #24292f; font-size: 1.6rem; width: 1.6rem; height: 1.6rem; margin-left: 0.4rem; } .egg_settings_item .egg_settings_theme_wrap { padding: 1rem 2rem 0 2rem; } .egg_settings_theme_wrap .egg_settings_theme_colors { display: flex; align-items: center; justify-content: space-between; } .egg_settings_theme_color_wrap .egg_settings_theme_color { border-radius: 50%; width: 1.6rem; height: 1.6rem; background: currentColor; } .egg_settings .egg_settings_read_time_wrap, .egg_settings .egg_settings_watch_time_wrap { padding: 1rem 2rem 0 2rem; display: flex; justify-content: space-between; align-items: center; } .egg_settings_read_time_wrap .egg_settings_label, .egg_settings_watch_time_wrap .egg_settings_label { padding: 0.5rem 0; } .egg_settings_read_time_wrap .egg_select, .egg_settings_watch_time_wrap .egg_select { width: 6rem; } .egg_settings .egg_settings_token_wrap { padding: 1rem 2rem 0 2rem; } .egg_settings_token_wrap .egg_settings_token_input { outline: none; border: 0.1rem solid #eee; padding: 1rem; background: white; border-radius: 0.2rem; width: 100%; box-sizing: border-box; color: #ccc; } .egg_settings_token_wrap .egg_settings_token_input.active { color: #333; } .egg_settings_token_input::placeholder { color: #ccc; } .egg_settings .egg_settings_submit_btn_wrap { text-align: right; padding-top: 1rem; display: none; } .egg_settings .egg_settings_submit_btn_wrap.active { display: block; } .egg_settings_submit_btn_wrap .egg_settings_submit_btn { outline: none; border: 0.1rem solid #eee; padding: 0.5rem 1rem; text-align: center; background: white; border-radius: 0.2rem; cursor: pointer; } .egg_settings_submit_btn_wrap .egg_settings_submit_btn:active { background: #eee; } .egg_schedule { height: 100%; display: flex; flex-direction: column; } .egg_schedule_time_wrap { padding: 1rem 2rem; border-bottom: 0.1rem solid #eee; } .egg_schedule_time .egg_schedule_label { padding-bottom: 1rem; user-select: none; } .egg_schedule_time .egg_schedule_time_input_wrap { display: flex; justify-content: space-between; align-items: center; } .egg_schedule_time_input_wrap .egg_schedule_add_btn { outline: none; border: 0.1rem solid #eee; padding: 0.5rem 1rem; text-align: center; background: white; border-radius: 0.2rem; cursor: pointer; } .egg_schedule_time_input_wrap .egg_schedule_add_btn:active { background: #eee; } .egg_schedule_list { height: 100%; overflow: auto; } .egg_schedule_list .egg_schedule_item { display: flex; justify-content: space-between; padding: 0.5rem 1.5rem; font-size: 1.4rem; border-bottom: 0.1rem solid #eee; } .egg_schedule_list::-webkit-scrollbar { width: 0.4rem; background: white; border-radius: 0.2rem; } .egg_schedule_list::-webkit-scrollbar-thumb { background: #ccc; border-radius: 0.2rem; } .egg_schedule_detail_time_wrap { display: flex; align-items: center; } .egg_schedule_detail_time_wrap.inactive { color: #ccc; } .egg_schedule_detail_time_wrap .egg_schedule_detail_icon { padding-right: 0.4rem; display: flex; color: #ccc; } .egg_schedule_detail_del_wrap .egg_schedule_del_btn { outline: none; padding: 1rem; text-align: center; background: white; border-radius: 0.2rem; font-size: 1.4rem; cursor: pointer; color: #ccc; } .egg_schedule_detail_del_wrap .egg_schedule_del_btn:hover { color: #333; } .egg_schedule_detail_del_wrap .egg_schedule_del_btn:active { color: #eee; } .egg_schedule_list .egg_schedule_list_none { width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; color: #ccc; } .egg_schedule_list_none .egg_icon { font-size: 2.5rem; } .egg_schedule_list_none_text { padding-top: 1rem; } .egg_select { position: relative; } .egg_select .egg_select_input { outline: none; border: 0.1rem solid #eee; padding: 0.8rem; text-align: center; background: white; border-radius: 0.2rem; display: inline-block; width: 100%; box-sizing: border-box; } .egg_select .egg_select_input::placeholder { color: #ccc; } .egg_select_list { max-height: 12rem; border-radius: 0 0 0.2rem 0.2rem; box-shadow: 0 0.1rem 0.1rem 0.1rem #eee; background: white; user-select: none; transition: 100ms ease; scrollbar-width: thin; overflow: auto; opacity: 1; z-index: 9; width: 100%; position: absolute; } .egg_select_list.hide { opacity: 0; visibility: hidden; } .egg_select_list::-webkit-scrollbar { width: 0.4rem; background: white; border-radius: 0.2rem; } .egg_select_list::-webkit-scrollbar-thumb { background: #ccc; border-radius: 0.2rem; } .egg_select_list .egg_select_item { padding: 0.6rem 1rem; border-bottom: 0.1rem solid #eee; cursor: pointer; color: #333; transition: 300ms ease; text-align: center; } .egg_select_list .egg_select_item.selected { font-weight: bold; background: #f6f6f6; } .egg_select_list .egg_select_item.active { background: #eee; } .egg_select_list .egg_select_item:hover { background: #eee; } ================================================ FILE: src/index.js ================================================ const css = '* { -webkit-tap-highlight-color: transparent;}:root { --themeColor: #fa3333; --scale: 1; font-size: calc(10px * var(--scale));}@media (min-height: 678px) and (max-height: 768px) { :root { --scale: 0.8; }}@media (max-height: 667px) { :root { --scale: 0.75; }}@keyframes fade { from { opacity: 0.8; } to { opacity: 0.4; background: #ccc; }}.egg_icon { width: 1em; height: 1em; fill: currentColor;}.egg_hr_wrap { position: relative; display: flex; justify-content: center; color: #ccc;}.egg_hr_wrap .egg_hr { position: absolute; top: 50%; transform: translateY(-50%); background: currentColor; height: 0.1rem; width: 30%;}.egg_hr_wrap .egg_hr:nth-of-type(1) { left: 0;}.egg_hr_wrap .egg_hr:nth-last-of-type(1) { right: 0;}.egg_hr_title { font-size: 1.2rem;}.egg_exam_btn { transition: background 80ms; outline: none; border: none; padding: 1.2rem 2rem; border-radius: 1.2rem; cursor: pointer; font-size: 1.8rem; font-weight: bold; text-align: center; color: #ffffff; background: #ccc;}.egg_exam_btn.manual { background: var(--themeColor);}.egg_panel_wrap * { padding: 0; margin: 0; box-sizing: border-box; outline: none; border: none;}.egg_panel_wrap { position: fixed; left: 0; top: 0; z-index: 99999; width: 100%; height: 100%; color: #333; font-size: 1.6rem; pointer-events: none;}.egg_panel { position: absolute; top: 5rem; left: 1rem; padding: 1.2rem 2rem; border-radius: 1rem; background: #ffffffe6; backdrop-filter: blur(1rem); box-shadow: 0 0 0.1rem 0.1rem #f1f1f1; transition: 80ms ease-out; pointer-events: all;}.egg_panel.hide { left: 0; transform: translateX(-100%);}.egg_panel_wrap.mobile .egg_panel { top: 1rem;}@media (min-height: 678px) and (max-height: 768px) { .egg_panel { top: 2rem; }}@media (max-height: 667px) { .egg_panel { top: 1rem; }}.egg_panel button { outline: none; border: none; padding: 0; cursor: pointer; background: none;}.egg_panel .egg_btns_wrap { position: absolute; left: 100%; top: 50%; transform: translate(-50%, -50%); transition: 80ms ease; z-index: 9;}.egg_panel.hide .egg_btns_wrap { left: 100%; transform: translate(0, -50%);}.egg_panel .egg_btns_wrap button { border-radius: 50%; width: 3rem; height: 3rem; padding: 0; overflow: hidden; border: 0.2rem solid currentColor; color: white; display: grid; place-items: center; font-size: 1.8rem;}.egg_panel.hide .egg_panel_show_btn { background: var(--themeColor);}.egg_panel .egg_panel_show_btn { background: #ccc;}.egg_panel .egg_frame_show_btn { background: var(--themeColor); margin-bottom: 1rem;}.egg_panel .egg_frame_show_btn.hide { display: none;}.egg_panel .egg_settings_show_btn { background: #ccc; margin-top: 1rem;}.egg_panel .egg_settings_show_btn.active { background: var(--themeColor);}.egg_panel .egg_settings_reset_btn { background: #ccc; margin-top: 1rem;}.egg_panel .egg_settings_reset_btn:active { background: var(--themeColor);}.egg_login_item { display: flex; justify-content: center; align-items: center; flex-direction: column; padding: 0.5rem 0;}.egg_login_item .egg_login_btn { font-size: 1.4rem; border-radius: 1rem; transition: 80ms ease; color: white; background: var(--themeColor); padding: 0.8rem 2.4rem;}.egg_login_item .egg_login_btn:active { opacity: 0.8;}.egg_login_item .egg_login_img_wrap { height: 0; border-radius: 1rem; transition: height 80ms ease; overflow: hidden;}.egg_login_item .egg_login_img_wrap.active { padding: 0.8rem; margin-top: 0.8rem; height: auto; background: white;}.egg_login_img_wrap .egg_login_img { width: 15rem; height: 15rem;}.egg_info_item .egg_login_btn { font-size: 1.4rem; border-radius: 1rem; transition: 80ms ease; color: white; background: #ccc; padding: 0.4rem 0.8rem;}.egg_info_item .egg_login_btn:active { opacity: 0.8;}.egg_info_item { display: flex; justify-content: space-between; align-items: center;}.egg_info_item .egg_userinfo { display: flex; justify-content: center; align-items: center; padding: 0.5rem 0;}.egg_userinfo .egg_avatar .egg_avatar_nick,.egg_userinfo .egg_avatar .egg_avatar_img { height: 5rem; width: 5rem; border-radius: 50%; background: var(--themeColor); display: flex; justify-content: center; align-items: center; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; font-size: 2rem; color: white;}.egg_userinfo .egg_nick { padding-left: 0.5rem; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 10rem;}.egg_score_item .egg_scoreinfo { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0;}.egg_scoreinfo .egg_totalscore,.egg_scoreinfo .egg_todayscore { font-size: 1.2rem; user-select: none;}.egg_scoreinfo .egg_totalscore span,.egg_scoreinfo .egg_todayscore .egg_todayscore_btn span { padding-left: 0.2rem;}.egg_scoreinfo .egg_totalscore span,.egg_todayscore .egg_todayscore_btn span,.egg_todayscore .egg_score_details span { color: var(--themeColor); font-weight: bold;}.egg_scoreinfo .egg_todayscore { position: relative;}.egg_todayscore .egg_todayscore_btn { display: flex; align-items: center;}.egg_todayscore_btn .egg_icon { opacity: 0.3;}.egg_todayscore .egg_score_details { position: absolute; left: calc(100% + 1rem); top: 0; background: #fffffff2; border-radius: 0.5rem; opacity: 1; width: 10rem; box-shadow: 0 0 0.1rem 0.1rem #f1f1f1; transition: 80ms ease; z-index: 9;}.egg_todayscore .egg_score_details.hide { visibility: hidden; opacity: 0; left: 100%;}.egg_score_details .egg_score_title { border-bottom: 0.1rem solid #eee; padding: 0.5rem 0.8rem; display: flex; align-items: center;}.egg_score_details .egg_score_title .egg_icon { font-size: 1.4rem;}.egg_score_details .egg_score_title .egg_score_title_text { font-weight: bold; padding-left: 0.2rem;}.egg_score_details .egg_score_item { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0.8rem;}.egg_task_list { position: relative;}.egg_task_item { user-select: none; min-height: 3rem; min-width: 18rem; display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0;}.egg_task_item .egg_label_wrap { flex-grow: 1; padding-right: 0.5rem;}.egg_label_wrap .egg_task_title_wrap { display: flex; justify-content: space-between; align-items: center;}.egg_task_title_wrap .egg_task_progress_wrap { display: flex; align-items: center; font-size: 1.4rem; width: 3.5rem;}.egg_task_progress_wrap .egg_task_current { color: var(--themeColor);}.egg_task_progress_wrap .egg_task_max { color: #999; font-size: 1.2rem;}.egg_label_wrap .egg_progress { display: flex; justify-content: space-between; align-items: center; padding-top: 0.8rem;}.egg_progress .egg_track { background: #ccc; height: 0.5rem; border-radius: 1rem; flex: 1 1 auto; overflow: hidden;}.egg_progress .egg_track .egg_bar { height: 0.5rem; background: var(--themeColor); border-radius: 1rem; width: 0; transition: width 0.5s;}.egg_setting_item { min-height: 3rem; min-width: 18rem; display: flex; align-items: center; justify-content: space-between; box-sizing: border-box;}.egg_setting_item .egg_label_wrap { flex-grow: 1;}.egg_detail { background: #ccc; color: white; border-radius: 10rem; font-size: 1.2rem; width: 1.6rem; height: 1.6rem; margin-left: 0.4rem; display: inline-block; text-align: center; line-height: 1.6rem; cursor: pointer;}.egg_switch { cursor: pointer; margin: 0; outline: 0; appearance: none; -webkit-appearance: none; -moz-appearance: none; position: relative; width: 4.2rem; height: 2.2rem; background: #ccc; border-radius: 5rem; transition: background 0.3s; --border-padding: 0.5rem; box-shadow: -0.1rem 0 0.1rem -0.1rem #999 inset, 0.1rem 0 0.1rem -0.1rem #999 inset;}.egg_switch::after { content: \'\'; display: inline-block; width: 1.4rem; height: 1.4rem; border-radius: 50%; background: #fff; box-shadow: 0 0 0.2rem #999; transition: left 0.4s; position: absolute; top: calc(50% - (1.4rem / 2)); position: absolute; left: var(--border-padding);}.egg_switch:checked { background: var(--themeColor);}.egg_switch:disabled { opacity: 0.5; background: #ccc;}.egg_switch:checked::after { left: calc(100% - var(--border-padding) - 1.4rem);}.egg_tip_list { font-size: 1.2rem; max-width: 18rem; line-height: 2rem; color: var(--themeColor);}.egg_tip_list .egg_tip_btn { padding: 0.2rem 0.4rem; background: #f1f1f1; color: #333;}.egg_tip_list .egg_tip_btn:disabled { opacity: 0.5; background: #ccc;}.egg_tip_list .egg_tip_content { text-align: center; padding-top: 0.2rem;}.egg_study_item { display: flex; justify-content: center; padding-top: 0.5rem;}.egg_study_item .egg_study_btn { background: var(--themeColor); padding: 0.8rem 2.4rem; font-size: 1.4rem; border-radius: 1rem; color: white; transition: 80ms ease;}.egg_study_item .egg_study_btn:not(.loading):active { opacity: 0.8;}.egg_study_item .egg_study_btn.loading { animation: fade 2s ease infinite alternate;}.egg_study_item .egg_study_btn:disabled { background: #ccc;}.egg_tip_wrap { position: fixed; left: 0; top: 0; z-index: 999999; width: 100%; height: 100%; pointer-events: none;}.egg_tip_wrap * { padding: 0; margin: 0; box-sizing: border-box; outline: none; border: none;}.egg_tip_wrap .egg_tip { position: absolute; bottom: 2rem; left: 2rem; padding: 1.2rem 1.4rem; border: none; border-radius: 1rem; background: var(--themeColor); color: white; font-size: 1.4rem; transition: 200ms ease; opacity: 0; transform: scale(0.9) translateY(1rem);}.egg_tip_wrap .egg_tip.active { opacity: 1; transform: scale(1) translateY(0);}.egg_tip_wrap .egg_tip.active.delay { opacity: 0.5;}.egg_tip_wrap .egg_tip .egg_countdown { display: inline-block; color: var(--themeColor); background: white; border-radius: 0.5rem; padding: 0.2rem 0.4rem; font-weight: bold; margin-left: 0.4rem; font-size: 1.2rem;}.egg_frame_wrap { position: fixed; left: 0; top: 0; z-index: 999; width: 100%; height: 100%; visibility: visible;}.egg_frame_wrap * { padding: 0; margin: 0; box-sizing: border-box; outline: none; border: none;}.egg_frame_wrap.hide { visibility: hidden;}.egg_frame_wrap.hide .egg_frame_mask,.egg_frame_wrap.hide .egg_frame_content_wrap { opacity: 0;}.egg_frame_wrap.hide .egg_frame_content_wrap { transform: scale(0);}.egg_frame_mask { background: #00000030; width: 100%; height: 100%; opacity: 1; transition: 200ms ease;}.egg_frame_content_wrap { position: absolute; width: 80%; height: 80%; top: 10%; left: 10%; display: flex; flex-direction: column; transition: 200ms ease; border-radius: 1rem; background: #ffffffe6; backdrop-filter: blur(1rem); overflow: hidden; transform: scale(1);}.egg_frame_content_wrap.max { top: 0; left: 0; width: 100%; height: 100%; border-radius: 0;}.egg_frame_content_wrap .egg_frame_controls_wrap { width: 100%; display: flex; justify-content: space-between; align-items: center; box-sizing: border-box;}.egg_frame_controls_wrap .egg_frame_title { padding: 1rem 2rem; font-size: 1.6rem;}.egg_frame_controls .egg_frame_btn { outline: none; border: none; background: none; padding: 1rem 2rem; transition: 80ms ease; cursor: pointer; color: #333; font-size: 1.8rem;}.egg_frame_controls .egg_frame_btn:active { opacity: 0.8;}.egg_frame_wrap .egg_frame_content { width: 100%; flex-grow: 1; border-top: 0.1rem solid #ccc; min-height: 40rem; min-width: 30rem; background: white;}.egg_frame_content .egg_frame { width: 100%; height: 100%; outline: none; border: none;}.egg_time_input { display: inline-flex; align-items: center; justify-content: center;}.egg_time_input .egg_hour_wrap,.egg_time_input .egg_minute_wrap { width: 4rem;}.egg_time_input .egg_separator { padding: 0 0.5rem; font-size: 1.5rem;}.egg_settings_item { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; overflow: hidden; border-radius: 1rem;}.egg_settings_item .egg_settings { display: inline-flex; flex-direction: column; font-size: 1.4rem; background: white; border-radius: 1rem; overflow: hidden; width: 100%; height: 100%; pointer-events: all; transform: translateX(100%); transition: transform 300ms ease; padding-top: 1rem;}.egg_settings_item .egg_settings.active { transform: translateX(0);}.egg_settings .egg_settings_label { padding-bottom: 1rem; user-select: none;}.egg_settings_item .egg_settings_version_wrap { padding: 1rem 2rem 0 2rem; display: flex; align-items: center; justify-content: space-between;}.egg_settings_version_wrap .egg_settings_version { color: #999; display: flex; align-items: center;}.egg_settings_version .egg_settings_version_detail { color: #24292f; font-size: 1.6rem; width: 1.6rem; height: 1.6rem; margin-left: 0.4rem;}.egg_settings_item .egg_settings_theme_wrap { padding: 1rem 2rem 0 2rem;}.egg_settings_theme_wrap .egg_settings_theme_colors { display: flex; align-items: center; justify-content: space-between;}.egg_settings_theme_color_wrap .egg_settings_theme_color { border-radius: 50%; width: 1.6rem; height: 1.6rem; background: currentColor;}.egg_settings .egg_settings_read_time_wrap,.egg_settings .egg_settings_watch_time_wrap { padding: 1rem 2rem 0 2rem; display: flex; justify-content: space-between; align-items: center;}.egg_settings_read_time_wrap .egg_settings_label,.egg_settings_watch_time_wrap .egg_settings_label { padding: 0.5rem 0;}.egg_settings_read_time_wrap .egg_select,.egg_settings_watch_time_wrap .egg_select { width: 6rem;}.egg_settings .egg_settings_token_wrap { padding: 1rem 2rem 0 2rem;}.egg_settings_token_wrap .egg_settings_token_input { outline: none; border: 0.1rem solid #eee; padding: 1rem; background: white; border-radius: 0.2rem; width: 100%; box-sizing: border-box; color: #ccc;}.egg_settings_token_wrap .egg_settings_token_input.active { color: #333;}.egg_settings_token_input::placeholder { color: #ccc;}.egg_settings .egg_settings_submit_btn_wrap { text-align: right; padding-top: 1rem; display: none;}.egg_settings .egg_settings_submit_btn_wrap.active { display: block;}.egg_settings_submit_btn_wrap .egg_settings_submit_btn { outline: none; border: 0.1rem solid #eee; padding: 0.5rem 1rem; text-align: center; background: white; border-radius: 0.2rem; cursor: pointer;}.egg_settings_submit_btn_wrap .egg_settings_submit_btn:active { background: #eee;}.egg_schedule { height: 100%; display: flex; flex-direction: column;}.egg_schedule_time_wrap { padding: 1rem 2rem; border-bottom: 0.1rem solid #eee;}.egg_schedule_time .egg_schedule_label { padding-bottom: 1rem; user-select: none;}.egg_schedule_time .egg_schedule_time_input_wrap { display: flex; justify-content: space-between; align-items: center;}.egg_schedule_time_input_wrap .egg_schedule_add_btn { outline: none; border: 0.1rem solid #eee; padding: 0.5rem 1rem; text-align: center; background: white; border-radius: 0.2rem; cursor: pointer;}.egg_schedule_time_input_wrap .egg_schedule_add_btn:active { background: #eee;}.egg_schedule_list { height: 100%; overflow: auto;}.egg_schedule_list .egg_schedule_item { display: flex; justify-content: space-between; padding: 0.5rem 1.5rem; font-size: 1.4rem; border-bottom: 0.1rem solid #eee;}.egg_schedule_list::-webkit-scrollbar { width: 0.4rem; background: white; border-radius: 0.2rem;}.egg_schedule_list::-webkit-scrollbar-thumb { background: #ccc; border-radius: 0.2rem;}.egg_schedule_detail_time_wrap { display: flex; align-items: center;}.egg_schedule_detail_time_wrap.inactive { color: #ccc;}.egg_schedule_detail_time_wrap .egg_schedule_detail_icon { padding-right: 0.4rem; display: flex; color: #ccc;}.egg_schedule_detail_del_wrap .egg_schedule_del_btn { outline: none; padding: 1rem; text-align: center; background: white; border-radius: 0.2rem; font-size: 1.4rem; cursor: pointer; color: #ccc;}.egg_schedule_detail_del_wrap .egg_schedule_del_btn:hover { color: #333;}.egg_schedule_detail_del_wrap .egg_schedule_del_btn:active { color: #eee;}.egg_schedule_list .egg_schedule_list_none { width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; color: #ccc;}.egg_schedule_list_none .egg_icon { font-size: 2.5rem;}.egg_schedule_list_none_text { padding-top: 1rem;}.egg_select { position: relative;}.egg_select .egg_select_input { outline: none; border: 0.1rem solid #eee; padding: 0.8rem; text-align: center; background: white; border-radius: 0.2rem; display: inline-block; width: 100%; box-sizing: border-box;}.egg_select .egg_select_input::placeholder { color: #ccc;}.egg_select_list { max-height: 12rem; border-radius: 0 0 0.2rem 0.2rem; box-shadow: 0 0.1rem 0.1rem 0.1rem #eee; background: white; user-select: none; transition: 100ms ease; scrollbar-width: thin; overflow: auto; opacity: 1; z-index: 9; width: 100%; position: absolute;}.egg_select_list.hide { opacity: 0; visibility: hidden;}.egg_select_list::-webkit-scrollbar { width: 0.4rem; background: white; border-radius: 0.2rem;}.egg_select_list::-webkit-scrollbar-thumb { background: #ccc; border-radius: 0.2rem;}.egg_select_list .egg_select_item { padding: 0.6rem 1rem; border-bottom: 0.1rem solid #eee; cursor: pointer; color: #333; transition: 300ms ease; text-align: center;}.egg_select_list .egg_select_item.selected { font-weight: bold; background: #f6f6f6;}.egg_select_list .egg_select_item.active { background: #eee;}.egg_select_list .egg_select_item:hover { background: #eee;}'; /** * @description 嵌入样式 */ GM_addStyle(css); load((href) => href.match(URL_CONFIG.home), () => { // 初始化logo initLogo(); // 页面提示 log('进入主页面!'); // 初始化主题 initThemeColor(); // 初始化任务配置 initTaskConfig(); // 初始化设置 initSettings(); // 设置字体 initFontSize(); // 初始化主页面 initMainListener(); // 初始化提示 renderTip(); // 渲染面板 renderPanel(); // 渲染窗口 renderFrame(); }); load((href) => href === GM_getValue('readingUrl'), async () => { // 页面提示 log('进入文章选读页面!'); // 初始化主题 initThemeColor(); // 初始化设置 initSettings(); // 设置字体 initFontSize(); // 最大阅读 initMaxRead(); // 初始化子页面 initChildListener(); // 初始化提示 renderTip(); try { // 处理文章 await handleNews(); } catch (err) { if (err instanceof Error) { // 提示 createTip(err.message); // 错误 error(err.message); return; } // 提示 createTip(String(err)); // 错误 error(err); } }); load((href) => href === GM_getValue('watchingUrl'), async () => { // 页面提示 log('进入视听学习页面!'); // 初始化主题 initThemeColor(); // 初始化设置 initSettings(); // 设置字体 initFontSize(); // 最大视听 initMaxWatch(); // 初始化子页面 initChildListener(); // 初始化提示 renderTip(); try { // 处理视频 await handleVideo(); } catch (err) { if (err instanceof Error) { // 提示 createTip(err.message); // 错误 error(err.message); return; } // 提示 createTip(String(err)); // 错误 error(err); } }); load((href) => href === URL_CONFIG.examPractice, async () => { // 页面提示 log('进入每日答题页面!'); // 初始化主题 initThemeColor(); // 初始化设置 initSettings(); // 设置字体 initFontSize(); // 初始化子页面 initChildListener(); // 初始化提示 renderTip(); // 创建答题按钮 await renderExamBtn(); try { // 开始答题 await doingExam(ExamType.PRACTICE); } catch (err) { if (err instanceof Error) { // 提示 createTip(err.message); // 错误 error(err.message); return; } // 提示 createTip(String(err)); // 错误 error(err); } }); load((href) => href.includes(URL_CONFIG.examPaper), async () => { // 页面提示 log('进入专项练习页面!'); // 初始化主题 initThemeColor(); // 初始化设置 initSettings(); // 设置字体 initFontSize(); // 初始化子页面 initChildListener(); // 初始化提示 renderTip(); // 创建答题按钮 await renderExamBtn(); // 开始答题 doingExam(ExamType.PAPER); return; }); /** * @description 初始化logo */ function initLogo() { console.log(`%c tech-study.js %c ${version} `, 'background:dodgerblue;color:white;font-size:15px;border-radius:4px 0 0 4px;padding:2px 0;', 'background:black;color:gold;font-size:15px;border-radius:0 4px 4px 0;padding:2px 0;'); } /** * @description 初始化配置 */ function initTaskConfig() { try { const taskTemp = JSON.parse(GM_getValue('taskConfig')); if (taskTemp && Array.isArray(taskTemp)) { if (taskTemp.length === taskConfig.length) { taskConfig.forEach((task, i) => { task.active = taskTemp[i].active; }); } } // 监听值变化 GM_addValueChangeListener('taskConfig', (key, oldVal, newVal, remote) => { if (remote) { const taskTemp = JSON.parse(newVal); if (taskTemp && Array.isArray(taskTemp)) { if (taskTemp.length === taskConfig.length) { taskConfig.forEach((task, i) => { task.active = taskTemp[i].active; }); } } } }); } catch (e) { } } /** * @description 初始化配置 */ function initSettings() { try { const settingsTemp = JSON.parse(GM_getValue('studySettings')); if (settingsTemp && Array.isArray(settingsTemp)) { if (settingsTemp.length === settings.length) { for (const i in settingsTemp) { settings[i] = settingsTemp[i]; } } } // 监听值变化 GM_addValueChangeListener('studySettings', (key, oldVal, newVal, remote) => { if (remote) { const settingsTemp = JSON.parse(newVal); if (settingsTemp && Array.isArray(settingsTemp)) { if (settingsTemp.length === settings.length) { for (const i in settingsTemp) { settings[i] = settingsTemp[i]; } } } } }); } catch (e) { } } /** * @description 初始化配置 */ function initFontSize() { // 移动端 const moblie = hasMobile(); if (moblie) { // 清除缩放 const meta = $$('meta[name=viewport]')[0]; if (meta) { meta.content = 'initial-scale=0, user-scalable=yes'; } // 缩放比例 const scale = ~~(window.innerWidth / window.outerWidth) || 1; document.documentElement.style.setProperty('--scale', String(scale)); } } /** * @description 初始化最大阅读时长 */ function initMaxRead() { try { const maxReadTemp = GM_getValue('maxRead'); if (maxReadTemp) { maxRead.value = maxReadTemp; } } catch (error) { } } /** * @description 初始化最大视听时长 */ function initMaxWatch() { try { const maxWatchTemp = GM_getValue('maxWatch'); if (maxWatchTemp) { maxWatch.value = maxWatchTemp; } } catch (error) { } } /** * @description 初始化主题色 */ function initThemeColor() { try { // 监听主题变化 watch(themeColor, () => { // 设置主题 document.documentElement.style.setProperty('--themeColor', themeColor.value); }); // 主题色 const themeColorTemp = GM_getValue('themeColor'); if (themeColorTemp) { themeColor.value = themeColorTemp; } // 监听值变化 GM_addValueChangeListener('themeColor', (key, oldVal, newVal, remote) => { if (remote) { // 主题色 const themeColorTemp = newVal; if (themeColorTemp) { themeColor.value = themeColorTemp; } } }); } catch (error) { } } /** * @description 渲染提示 */ function renderTip() { const tipWrap = createElementNode('div', undefined, { class: 'egg_tip_wrap', onclick(e) { e.stopPropagation(); }, onmousedown(e) { e.stopPropagation(); }, onmousemove(e) { e.stopPropagation(); }, onmouseup(e) { e.stopPropagation(); }, onmouseenter(e) { e.stopPropagation(); }, onmouseleave(e) { e.stopPropagation(); }, onmouseover(e) { e.stopPropagation(); }, ontouchstart(e) { e.stopPropagation(); }, ontouchmove(e) { e.stopPropagation(); }, ontouchend(e) { e.stopPropagation(); }, oninput(e) { e.stopPropagation(); }, onchange(e) { e.stopPropagation(); }, onblur(e) { e.stopPropagation(); }, }); mountElement(tipWrap); } /** * @description 渲染答题按钮 */ async function renderExamBtn() { const titles = await $_('.title'); if (titles.length) { // 插入节点 titles[0].parentNode?.insertBefore(ExamBtn().ele, titles[0].nextSibling); } } /** * @description 渲染面板 * @returns */ async function renderPanel() { // 面板 const panel = Panel(); // 插入节点 mountElement(panel); } /** * @description 渲染窗口 */ function renderFrame() { // 窗口 const frame = Frame(); // 插入节点 mountElement(frame); } /* 答案 API */ /** * @description 获取答案 */ async function getAnswer(question) { // 数据 const data = { txt_name: md5(question), password: '', }; try { const params = new URLSearchParams(data); // 请求 const res = await fetch(API_CONFIG.answerSearch, { method: 'POST', mode: 'cors', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: params.toString(), }); // 请求成功 if (res.ok) { const result = await res.json(); const { data, status } = result; if (status !== 0) { // 答案列表 const answerList = JSON.parse(data.txt_content); // 答案 const answers = answerList[0].content.split(/[;\s]/); return answers; } } } catch (error) { } return []; } /** * @description 保存答案 */ async function saveAnswer(question, answer) { try { // 内容 const content = JSON.stringify([{ title: md5(question), content: answer }]); // 数据 const data = { txt_name: md5(question), txt_content: content, password: '', v_id: '', }; const params = new URLSearchParams(data); // 请求 const res = await fetch(API_CONFIG.answerSave, { method: 'POST', mode: 'cors', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: params.toString(), }); // 请求成功 if (res.ok) { const data = await res.json(); return data; } } catch (error) { } } /* 数据 API */ /** * @description 获取新闻数据 */ async function getNewsList() { // 随机 const randNum = ~~(Math.random() * API_CONFIG.todayNews.length); try { // 获取重要新闻 const res = await fetch(API_CONFIG.todayNews[randNum], { method: 'GET', }); // 请求成功 if (res.ok) { const data = await res.json(); return data; } } catch (err) { } } /** * @description 获取视频数据 */ async function getVideoList() { // 随机 const randNum = ~~(Math.random() * API_CONFIG.todayVideos.length); try { // 获取重要新闻 const res = await fetch(API_CONFIG.todayVideos[randNum], { method: 'GET', }); // 请求成功 if (res.ok) { const data = await res.json(); return data; } } catch (err) { } } /** * @description 专项练习数据 */ async function getExamPaper(pageNo) { // 链接 const url = `${API_CONFIG.paperList}?pageSize=50&pageNo=${pageNo}`; try { // 获取专项练习 const res = await fetch(url, { method: 'GET', credentials: 'include', }); // 请求成功 if (res.ok) { const data = await res.json(); const paperJson = decodeURIComponent(escape(window.atob(data.data_str.replace(/-/g, '+').replace(/_/g, '/')))); // JSON格式化 const paper = JSON.parse(paperJson); return paper; } } catch (err) { return []; } return []; } /** * @description 生成二维码 */ async function generateQRCode() { try { // 推送 const res = await fetch(API_CONFIG.generateQRCode, { method: 'GET', mode: 'cors', }); // 请求成功 if (res.ok) { const data = await res.json(); if (data.success) { return data.result; } } } catch (error) { } } /** * @description 用二维码登录 */ async function loginWithQRCode(qrCode) { try { const params = new URLSearchParams({ qrCode, goto: 'https://oa.xuexi.cn', pdmToken: '', }); // 推送 const res = await fetch(API_CONFIG.loginWithQRCode, { method: 'POST', mode: 'cors', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', }, body: params.toString(), }); // 请求成功 if (res.ok) { const data = await res.json(); return data; } } catch (error) { } } /** * @description 签名 */ async function getSign() { try { // 推送 const res = await fetch(API_CONFIG.sign, { method: 'GET', mode: 'cors', credentials: 'include', }); // 请求成功 if (res.ok) { const data = await res.json(); if (data.ok) { return data.data.sign; } } } catch (error) { } } /** * @description 安全检查 * @param data */ async function secureCheck(data) { try { const params = new URLSearchParams(data); const url = `${API_CONFIG.secureCheck}?${params}`; // 推送 const res = await fetch(url, { method: 'GET', mode: 'cors', credentials: 'include', }); // 请求成功 if (res.ok) { const data = await res.json(); return data.success; } } catch (error) { } return false; } /* 推送 API */ /** * @description 推送 */ async function pushPlus(token, title, content, template, toToken) { try { // 参数体 const body = { token, title, content, template, }; // 好友令牌 if (toToken) { body.to = toToken; } // 推送 const res = await fetch(API_CONFIG.push, { method: 'POST', mode: 'cors', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); // 请求成功 if (res.ok) { const data = await res.json(); return data; } } catch (error) { } } /* 用户 API */ /** * @description 获取用户信息 */ async function getUserInfo() { try { const res = await fetch(API_CONFIG.userInfo, { method: 'GET', credentials: 'include', }); // 请求成功 if (res.ok) { const { data } = await res.json(); return data; } } catch (err) { } } /** * @description 获取总积分 */ async function getTotalScore() { try { const res = await fetch(API_CONFIG.totalScore, { method: 'GET', credentials: 'include', }); // 请求成功 if (res.ok) { const { data } = await res.json(); // 总分 const { score } = data; return score; } } catch (err) { } } /** * @description 获取当天总积分 */ async function getTodayScore() { try { const res = await fetch(API_CONFIG.todayScore, { method: 'GET', credentials: 'include', }); // 请求成功 if (res.ok) { const { data } = await res.json(); // 当天总分 const { score } = data; return score; } } catch (err) { } } /** * @description 获取任务列表 */ async function getTaskList() { try { const res = await fetch(API_CONFIG.taskList, { method: 'GET', credentials: 'include', }); // 请求成功 if (res.ok) { const { data } = await res.json(); // 进度和当天总分 const { taskProgress } = data; return taskProgress; } } catch (err) { } } /* task·配置 */ /** * @description 单次最大新闻数 */ const maxNewsNum = 6; /** * @description 单次最大视频数 */ const maxVideoNum = 6; /** * @description 二维码最大刷新次数 */ const maxRefreshCount = 10; /** * @description 二维码自动刷新间隔 */ const autoRefreshQRCodeInterval = 100000; /** * @description url配置 */ const URL_CONFIG = { // 主页正则 home: /^https\:\/\/www\.xuexi\.cn(\/(index\.html)?)?$/, // 主页 homeOrigin: 'https://www.xuexi.cn', // 每日答题页面 examPractice: 'https://pc.xuexi.cn/points/exam-practice.html', // 专项练习页面 examPaper: 'https://pc.xuexi.cn/points/exam-paper-detail.html', }; /** * @description api配置 */ const API_CONFIG = { // 用户信息 userInfo: 'https://pc-api.xuexi.cn/open/api/user/info', // 总分 totalScore: 'https://pc-proxy-api.xuexi.cn/delegate/score/get', // 当天分数 todayScore: 'https://pc-proxy-api.xuexi.cn/delegate/score/today/query', // 任务列表 taskList: 'https://pc-proxy-api.xuexi.cn/delegate/score/days/listScoreProgress?sence=score&deviceType=2', // 新闻数据 todayNews: [ 'https://www.xuexi.cn/lgdata/35il6fpn0ohq.json', 'https://www.xuexi.cn/lgdata/1ap1igfgdn2.json', 'https://www.xuexi.cn/lgdata/vdppiu92n1.json', 'https://www.xuexi.cn/lgdata/152mdtl3qn1.json', ], // 视频数据 todayVideos: [ 'https://www.xuexi.cn/lgdata/525pi8vcj24p.json', 'https://www.xuexi.cn/lgdata/11vku6vt6rgom.json', 'https://www.xuexi.cn/lgdata/2qfjjjrprmdh.json', 'https://www.xuexi.cn/lgdata/3o3ufqgl8rsn.json', 'https://www.xuexi.cn/lgdata/591ht3bc22pi.json', 'https://www.xuexi.cn/lgdata/1742g60067k.json', 'https://www.xuexi.cn/lgdata/1novbsbi47k.json', ], // 专项练习列表 paperList: 'https://pc-proxy-api.xuexi.cn/api/exam/service/paper/pc/list', // 文本服务器保存答案 answerSave: 'https://a6.qikekeji.com/txt/data/save', // 文本服务器获取答案 answerSearch: 'https://a6.qikekeji.com/txt/data/detail', // 推送 push: 'https://www.pushplus.plus/send', // 生成二维码 generateQRCode: 'https://login.xuexi.cn/user/qrcode/generate', //二维码登录 loginWithQRCode: 'https://login.xuexi.cn/login/login_with_qr', // 签名 sign: 'https://pc-api.xuexi.cn/open/api/sns/sign', // 安全检查 secureCheck: 'https://pc-api.xuexi.cn/login/secure_check', // 二维码 qrcode: 'https://api.qrserver.com/v1/create-qr-code', }; /** * @description 版本号 */ const version = '1.7.5'; /** * @description 任务类型 */ var TaskType; (function (TaskType) { TaskType[TaskType["LOGIN"] = 0] = "LOGIN"; TaskType[TaskType["READ"] = 1] = "READ"; TaskType[TaskType["WATCH"] = 2] = "WATCH"; TaskType[TaskType["PRACTICE"] = 3] = "PRACTICE"; })(TaskType || (TaskType = {})); /** * @description 设置类型 */ var SettingType; (function (SettingType) { SettingType[SettingType["AUTO_START"] = 0] = "AUTO_START"; SettingType[SettingType["SAME_TAB"] = 1] = "SAME_TAB"; SettingType[SettingType["SILENT_RUN"] = 2] = "SILENT_RUN"; SettingType[SettingType["SCHEDULE_RUN"] = 3] = "SCHEDULE_RUN"; SettingType[SettingType["VIDEO_MUTED"] = 4] = "VIDEO_MUTED"; SettingType[SettingType["RANDOM_EXAM"] = 5] = "RANDOM_EXAM"; SettingType[SettingType["AUTO_ANSWER"] = 6] = "AUTO_ANSWER"; SettingType[SettingType["REMOTE_PUSH"] = 7] = "REMOTE_PUSH"; })(SettingType || (SettingType = {})); /** * @description 进度类型 */ var TaskStatusType; (function (TaskStatusType) { TaskStatusType[TaskStatusType["LOADING"] = 0] = "LOADING"; TaskStatusType[TaskStatusType["LOADED"] = 1] = "LOADED"; TaskStatusType[TaskStatusType["START"] = 2] = "START"; TaskStatusType[TaskStatusType["PAUSE"] = 3] = "PAUSE"; TaskStatusType[TaskStatusType["FINISH"] = 4] = "FINISH"; })(TaskStatusType || (TaskStatusType = {})); // 当前订阅 let currentSub; // 订阅 const subscription = new WeakMap(); /** * @description Proxy Map */ const proxyMap = new WeakMap(); /** * @description 收集 Ref 依赖 * @param target * @param key */ const trackRef = (target) => { // 当前订阅 if (!currentSub) { return; } // target 订阅列表 let subList = subscription.get(target); // 不存在订阅列表 if (!subList) { subList = new Map(); // 键订阅 const subkeyList = new Set(); // 添加订阅 subkeyList.add(currentSub); subList.set('value', subkeyList); subscription.set(target, subList); return; } // 键订阅 let subkeyList = subList.get('value'); if (!subkeyList) { // 键订阅 subkeyList = new Set(); // 添加订阅 subkeyList.add(currentSub); subList.set('value', subkeyList); subscription.set(target, subList); return; } // 添加订阅 subkeyList.add(currentSub); }; /** * @description 通知 Ref 订阅 * @param terget * @param key * @returns */ function triggerRef(target, newVal, oldVal) { // target 订阅列表 const subList = subscription.get(target); if (!subList) { return; } // 键订阅 let subkeyList = subList.get('value'); if (!subkeyList) { return; } // 通知订阅 for (const fn of subkeyList) { if (fn instanceof Function) { fn(newVal, oldVal); } } } /** * @description 收集依赖 * @param target * @param key */ const track = (target, key) => { // 当前订阅 if (!currentSub) { return; } // proxy const proxyTarget = proxyMap.get(target); if (!proxyTarget) { return; } // target 订阅列表 let subList = subscription.get(target); // 不存在订阅列表 if (!subList) { subList = new Map(); // 键订阅 const subkeyList = new Set(); // 添加订阅 subkeyList.add(currentSub); subList.set(key, subkeyList); subscription.set(target, subList); return; } // 键订阅 let subkeyList = subList.get(key); if (!subkeyList) { // 键订阅 subkeyList = new Set(); // 添加订阅 subkeyList.add(currentSub); subList.set(key, subkeyList); subscription.set(target, subList); return; } // 添加订阅 subkeyList.add(currentSub); }; /** * @description 通知订阅 * @param terget * @param key * @returns */ function trigger(target, key, newVal, oldVal) { // proxy const proxyTarget = proxyMap.get(target); if (!proxyTarget) { return; } // proxyTarget 订阅列表 const subList = subscription.get(target); if (!subList) { return; } // 键订阅 let subkeyList = subList.get(key); if (!subkeyList) { return; } // 通知订阅 for (const fn of subkeyList) { fn(newVal, oldVal); } } /** * @description 只读键 */ var ReactiveFlags; (function (ReactiveFlags) { ReactiveFlags["IS_REF"] = "_isRef"; ReactiveFlags["IS_SHALLOW"] = "_isShallow"; ReactiveFlags["IS_REACTIVE"] = "_isReactive"; ReactiveFlags["IS_READONLY"] = "_isReadonly"; })(ReactiveFlags || (ReactiveFlags = {})); /** * @description Ref */ class Ref { _isShallow = false; _isRef = true; _value; value; constructor(val, shallow = false) { const _this = this; this._isShallow = shallow; if (val && typeof val === 'object' && shallow) { const reactiveVal = reactive(val); this._value = reactiveVal; this.value = reactiveVal; } else { this._value = val; this.value = val; } // 定义属性 Object.defineProperty(this, 'value', { get() { // 收集依赖 trackRef(this); return _this._value; }, set(newVal) { // 旧数据 const oldVal = this._value; // 数据变化 if (oldVal !== newVal) { // 设置新数据值 _this._value = newVal; // 通知依赖 triggerRef(this, newVal, oldVal); } }, }); } toJSON() { return this._value; } } /** * @description ref * @param v * @returns */ const isRef = (v) => { return !!(v && v[ReactiveFlags.IS_REF]); }; /** * @description 浅层 shallow * @param v * @returns */ const isShallow = (v) => { return !!(v && v[ReactiveFlags.IS_SHALLOW]); }; /** * @description 创建 ref * @param v * @returns */ const createRef = (rawVal, shallow) => { return new Ref(rawVal, shallow); }; /** * @description 解除 ref * @param val * @returns */ const unref = (val) => { return (isRef(val) ? val.value : val); }; /** * @description 顶层 ref * @param v * @returns */ const ref = (value) => { return isRef(value) ? value : createRef(value, true); }; /** * @description ref * @param value * @returns */ const shallowRef = (value) => { return isRef(value) ? value : createRef(value, false); }; /** * @description 创建处理 reactive * @param isReadonly * @param isShallow * @returns */ const createReactiveHandlers = (isReadonly, isShallow) => { return { get: createGetters(isReadonly, isShallow), set: createSetters(isReadonly, isShallow), }; }; /** * @description getters * @param isReadonly * @param isShallow * @returns */ const createGetters = (isReadonly, isShallow) => { return function get(target, key, receiver) { if (key === ReactiveFlags.IS_REACTIVE) { return !isReadonly; } if (key === ReactiveFlags.IS_READONLY) { return isReadonly; } if (key === ReactiveFlags.IS_SHALLOW) { return isShallow; } // 结果 const res = Reflect.get(target, key, receiver); if (!isReadonly) { // 收集依赖 track(target, key); } if (isShallow) { return res; } if (isRef(res)) { return res.value; } if (res && typeof res === 'object') { if (res instanceof Element) { return res; } return isReadonly ? readonly(res) : reactive(res); } return res; }; }; /** * @description setters * @param readonly * @param shallow * @returns */ const createSetters = (readonly, shallow) => { return function set(target, key, newVal, receiver) { // 只读 if (readonly) { return false; } // 旧值 const oldVal = target[key]; if (isReadonly(oldVal) && isRef(oldVal) && !isRef(newVal)) { return false; } if (!shallow) { if (isRef(oldVal) && !isRef(newVal)) { oldVal.value = newVal; return true; } } const res = Reflect.set(target, key, newVal, receiver); // length if (Array.isArray(target) && key === 'length') { // 通知依赖 trigger(target, key, newVal, oldVal); return res; } // 数据变化 if (oldVal !== newVal) { // 通知依赖 trigger(target, key, newVal, oldVal); } return res; }; }; /** * @description reactive object */ const createReactiveObj = (target, isReadonly, shallow) => { // 存在 Proxy const existingProxy = proxyMap.get(target); if (existingProxy) { return existingProxy; } // 新建 const proxy = new Proxy(target, createReactiveHandlers(isReadonly, shallow)); proxyMap.set(target, proxy); return proxy; }; /** * @description reactive * @param val * @returns */ const isReactive = (val) => { return !!(val && val[ReactiveFlags.IS_REACTIVE]); }; /** * @description 创建 reactive * @param target * @returns */ const createReactive = (target) => { return createReactiveObj(target, false, false); }; /** * @description 顶层 reactive * @param target * @returns */ const shallowReactive = (target) => { return createReactiveObj(target, false, true); }; /** * @description reactive * @param val * @returns */ const isReadonly = (val) => { return !!(val && val[ReactiveFlags.IS_READONLY]); }; /** * @description 创建 readonly * @param target * @returns */ const createReadonly = (target) => { return createReactiveObj(target, true, false); }; /** * @description 顶层 readonly * @param target * @returns */ const shallowReadonly = (target) => { return createReactiveObj(target, true, true); }; /** * @description proxy * @param val * @returns */ const isProxy = (val) => { return isReactive(val) || isReadonly(val); }; /** * @description reactive * @param target * @returns */ const reactive = (target) => { return createReactive(target); }; /** * @description readonly * @param target * @returns */ const readonly = (target) => { return createReadonly(target); }; /** * @description 监听数据变化 * @param source * @param callback */ const watch = (source, callback, immediate = false) => { // 立刻执行 immediate && callback(unref(source), unref(source)); // array if (Array.isArray(source) && source.every((s) => isRef(s))) { for (const i in source) { // Proxy if (isProxy(source[i])) { watch(source[i], () => { const res = source.map((s) => unref(s)); callback(res, res); }); } } watch(() => source.map((s) => unref(s)), callback); return; } // function if (source instanceof Function) { watch(watchEffectRef(source), (n, o) => { callback(unref(n), unref(o)); }); return; } // Proxy if (isProxy(source)) { for (const key in source) { currentSub = () => { callback(source, source); }; // sub source const subSource = source[key]; currentSub = undefined; watch(subSource, () => { callback(source, source); }); } return; } // Ref if (isRef(source)) { // Ref.value Proxy if (isProxy(source.value)) { watch(source.value, () => { callback(unref(source), unref(source)); }); } currentSub = callback; // 收集依赖 trackRef(source); currentSub = undefined; return; } }; /** * @description 监听数据变化影响 * @param callback * @returns */ const watchEffect = (callback) => { currentSub = callback; // 收集依赖 callback(); currentSub = undefined; }; /** * @description 监听影响 ref * @param refVal * @param callback * @returns */ const watchRef = (source, callback) => { // 收集依赖 const effectRes = shallowRef(callback()); // 监听 watch(source, () => (effectRes.value = unref(callback()))); return effectRes; }; /** * @description 监听影响 ref * @param refVal * @param callback * @returns */ const watchEffectRef = (callback) => { // 收集依赖 const effectRes = shallowRef(undefined); // 监听 watchEffect(() => (effectRes.value = unref(callback()))); return effectRes; }; /** * @description 创建元素节点 * @param eleName * @param props * @param attrs * @param children * @returns */ function createElementNode(tagName, props, attrs, children, options) { // 挂载状态 let beforemount = ref(false); // 挂载状态 let mounted = ref(false); const { onCreated, beforeCreat, onMounted, beforeMount } = options || {}; // 订阅 const subscribe = (e) => { const { onMounted, beforeMount } = e; if (beforeMount) { watch(beforemount, () => { if (beforemount.value) { beforeMount(); return; } }, true); } if (onMounted) { watch(mounted, () => { if (mounted.value) { onMounted(); return; } }, true); } }; // 取消订阅 const unsubscribe = (e) => { //懒得写 }; // 创建元素前 beforeCreat && beforeCreat(); // 创建普通元素 const ele = document.createElement(tagName); // 处理属性 handleProps(ele, props); // 处理属性 handleAttributes(ele, attrs, subscribe, unsubscribe); // 处理子元素 handleChildren(ele, children, subscribe, unsubscribe); // 收集挂载前 const collectBeforeMount = () => { beforemount.value = true; beforeMount && beforeMount(); }; // 收集挂载 const collectOnMounted = () => { mounted.value = true; onMounted && onMounted(); }; // 创建元素后 onCreated && onCreated(); return { ele, beforeMount: collectBeforeMount, onMounted: collectOnMounted }; } /** * @description 创建svg元素 * @param tagName * @param props * @param attrs * @param children * @returns */ function createNSElementNode(tagName, props, attrs, children, options) { // 挂载状态 let beforemount = ref(false); // 挂载状态 let mounted = ref(false); const { onCreated, beforeCreat, onMounted, beforeMount } = options || {}; // 订阅 const subscribe = (e) => { const { onMounted, beforeMount } = e; if (beforeMount) { watch(beforemount, () => { if (beforemount.value) { beforeMount(); return; } }, true); } if (onMounted) { watch(mounted, () => { if (mounted.value) { onMounted(); return; } }, true); } }; // 取消订阅 const unsubscribe = (e) => { //懒得写 }; // 创建元素前 beforeCreat && beforeCreat(); // svg元素命名空间 const ns = 'http://www.w3.org/2000/svg'; // 创建svg元素 const ele = document.createElementNS(ns, tagName); // 处理属性 handleProps(ele, props); // 处理属性 handleAttributes(ele, attrs, subscribe, unsubscribe); // 处理子元素 handleChildren(ele, children, subscribe, unsubscribe); // 收集挂载前 const collectBeforeMount = () => { beforemount.value = true; beforeMount && beforeMount(); }; // 收集挂载 const collectOnMounted = () => { mounted.value = true; onMounted && onMounted(); }; // 创建元素后 onCreated && onCreated(); return { ele, beforeMount: collectBeforeMount, onMounted: collectOnMounted }; } /** * @description 处理属性 * @param ele * @param props */ function handleProps(ele, props) { // props属性设置 for (const key in props) { // Ref 属性 if (isRef(props[key])) { const refVal = props[key]; watchEffect(() => (ele[key] = refVal.value)); continue; } ele[key] = props[key]; } } /** * @description 处理svg属性 * @param ele * @param attrs */ function handleAttributes(ele, attrs, subscribe, unsubscribe) { // 属性存在 if (attrs) { // attrs属性设置 for (const key in attrs) { // 处理普通属性 handleAttribute(ele, key, attrs[key], subscribe, unsubscribe); } } } /** * @description 处理事件选项 */ function handleEventOptions(option) { if (option.length) { const options = { capture: option.includes('capture'), once: option.includes('once'), passive: option.includes('passive'), }; return options; } } /** * @description 处理属性 * @param ele * @param key * @param value */ function handleAttribute(ele, key, value, subscribe, unsubscribe) { // 处理完的key const formatKey = key.toLowerCase(); // 事件绑定 if (formatKey.startsWith('on')) { // 事件监听 const [event] = formatKey.match(/(?<=on).*/); // 事件类型 if (event) { const [eventType, ...option] = event.split('_'); const options = handleEventOptions(option); // Ref 函数 if (isRef(value)) { const refVal = value; const refListener = watchRef(refVal, () => refVal.value ? (e) => { option.includes('prevent') && e.preventDefault(); option.includes('stop') && e.stopPropagation(); const callback = refVal.value; callback(e); } : undefined); // 设置事件监听 refListener.value && ele.addEventListener(eventType, refListener.value, options); // 监听事件变化 watch(refListener, (newVal, oldVal) => { // 移除旧事件监听 oldVal && ele.removeEventListener(eventType, oldVal); // 设置新事件监听 newVal && ele.addEventListener(eventType, newVal, options); }); return; } // 普通函数 if (value instanceof Function) { // 设置事件监听 ele.addEventListener(eventType, value, options); } } return; } // 特殊属性 const specificAttrs = ['checked', 'selected', 'disabled', 'enabled']; // 特殊 key if (specificAttrs.includes(formatKey)) { // Ref if (isRef(value)) { const refVal = value; watchEffect(() => { if (refVal.value) { ele.setAttribute(formatKey, ''); } else { ele.removeAttribute(formatKey); } }); return; } // 普通属性值 if (value) { ele.setAttribute(formatKey, ''); } else { ele.removeAttribute(formatKey); } return; } // ref 属性名 if (key === 'ref') { // Ref if (isRef(value)) { const refVal = value; subscribe && subscribe({ onMounted() { refVal.value = ele; }, }); return; } // Ref 函数 if (value instanceof Function) { const refFn = value; subscribe && subscribe({ onMounted() { refFn(ele); }, }); return; } return; } // xlink命名空间 if (key.startsWith('xlink:')) { // xlink属性命名空间 const attrNS = 'http://www.w3.org/1999/xlink'; if (value) { ele.setAttributeNS(attrNS, key, value); } else { ele.removeAttributeNS(attrNS, key); } return; } // Ref 属性值 if (key && isRef(value)) { const refVal = value; // 监听影响 watchEffect(() => { ele.setAttribute(key, refVal.value); }); return; } // 普通属性 if (key) { // 普通属性 ele.setAttribute(key, value); } } /** * @description 处理子元素 * @param ele * @param children */ function handleChildren(ele, children, subscribe, unsubscribe) { // Ref if (isRef(children)) { // 注释元素 const comment = document.createComment(''); // 监听元素变化 watch(children, async (newEle, oldEle) => { if (!newEle && oldEle) { // Promise if (oldEle instanceof Promise) { const oldEleRes = await oldEle; if (oldEleRes) { oldEleRes.forEach((ele) => { unsubscribe && unsubscribe(ele); }); } } // unPromise if (!(oldEle instanceof Promise)) { oldEle.forEach((ele) => { unsubscribe && unsubscribe(ele); }); } ele.replaceChildren(comment); return; } if (newEle) { if (oldEle) { // Promise if (oldEle instanceof Promise) { const oldEleRes = await oldEle; if (oldEleRes) { oldEleRes.forEach((ele) => { unsubscribe && unsubscribe(ele); }); } } // unPromise if (!(oldEle instanceof Promise)) { oldEle.forEach((ele) => { unsubscribe && unsubscribe(ele); }); } } // Promise if (newEle instanceof Promise) { const newEleRes = await newEle; if (newEleRes) { const eles = newEleRes.map((v) => { if (v.beforeMount || v.onMounted) { subscribe && subscribe(v); } return v.ele; }); ele.replaceChildren(createElementBlock(eles)); } return; } // unPromise const eles = newEle.map((v) => { if (v.beforeMount || v.onMounted) { subscribe && subscribe(v); } return v.ele; }); ele.replaceChildren(createElementBlock(eles)); return; } }); // Promise if (children.value instanceof Promise) { // 插入注释元素 ele.appendChild(comment); children.value.then((childrenEle) => { if (childrenEle) { const eles = childrenEle.map((v) => { if (v.beforeMount || v.onMounted) { subscribe && subscribe(v); } return v.ele; }); ele.replaceChildren(createElementBlock(eles)); } }); return; } // unPromise if (children.value) { const eles = children.value.map((v) => { if (v.beforeMount || v.onMounted) { subscribe && subscribe(v); } return v.ele; }); ele.appendChild(createElementBlock(eles)); return; } // 插入元素 ele.appendChild(comment); return; } // Promise if (children instanceof Promise) { // 注释元素 const comment = document.createComment(''); // 插入注释元素 ele.appendChild(comment); // 异步替换元素 children.then((childEle) => { if (childEle) { const { beforeMount, onMounted } = childEle; if (beforeMount || onMounted) { subscribe && subscribe(childEle); } comment.replaceWith(childEle.ele); } }); return; } // Array if (Array.isArray(children)) { // 处理过后 const resChildren = []; for (const i in children) { const child = children[i]; // Ref if (isRef(child)) { // 注释 const comment = document.createComment(''); // 监听影响 watch(child, async (newEle, oldEle) => { // 新元素为空 if (!newEle && oldEle) { // Promise if (oldEle instanceof Promise) { const oldEleRes = await oldEle; if (oldEleRes) { handleChangeElement(newEle, oldEleRes, comment, subscribe, unsubscribe); } return; } handleChangeElement(newEle, oldEle, comment, subscribe, unsubscribe); return; } // 旧元素为空 if (newEle && !oldEle) { // Promise if (newEle instanceof Promise) { const newEleRes = await newEle; if (newEleRes) { handleChangeElement(newEleRes, oldEle, comment, subscribe, unsubscribe); } return; } handleChangeElement(newEle, oldEle, comment, subscribe, unsubscribe); return; } // 存在 if (newEle && oldEle) { // Promise if (newEle instanceof Promise && oldEle instanceof Promise) { const newEleRes = await newEle; const oldEleRes = await oldEle; // 处理元素变化 handleChangeElement(newEleRes, oldEleRes, comment, subscribe, unsubscribe); return; } // Promise if (newEle instanceof Promise && !(oldEle instanceof Promise)) { const newEleRes = await newEle; // 处理元素变化 handleChangeElement(newEleRes, oldEle, comment, subscribe, unsubscribe); return; } // Promise if (!(newEle instanceof Promise) && oldEle instanceof Promise) { const oldEleRes = await oldEle; // 处理元素变化 handleChangeElement(newEle, oldEleRes, comment, subscribe, unsubscribe); return; } // 非 Promise if (!(oldEle instanceof Promise) && !(newEle instanceof Promise)) { // 处理元素变化 handleChangeElement(newEle, oldEle, comment, subscribe, unsubscribe); return; } } }); // Promise if (child.value instanceof Promise) { // 注释 resChildren[i] = { ele: comment }; // 异步替换 child.value.then((childEle) => { if (childEle) { const { beforeMount, onMounted } = childEle; if (beforeMount || onMounted) { subscribe && subscribe(childEle); } comment.replaceWith(childEle.ele); } }); continue; } // unPromise if (child.value) { const { beforeMount, onMounted, ele } = child.value; resChildren[i] = { ele, beforeMount, onMounted }; continue; } resChildren[i] = { ele: comment }; continue; } // Promise if (child instanceof Promise) { // 注释 const comment = document.createComment(''); resChildren[i] = { ele: comment }; // 异步替换元素 child.then((childEle) => { if (childEle) { const { beforeMount, onMounted } = childEle; if (beforeMount || onMounted) { subscribe && subscribe(childEle); } comment.replaceWith(childEle.ele); } }); continue; } // 普通元素 if (child) { const { beforeMount, onMounted, ele } = child; resChildren[i] = { ele, beforeMount, onMounted }; } } const eles = resChildren.map((v) => { if (v.beforeMount || v.onMounted) { subscribe && subscribe(v); } return v.ele; }); // 插入元素 ele.appendChild(createElementBlock(eles)); return; } // 普通元素 if (children) { const { beforeMount, onMounted } = children; if (beforeMount || onMounted) { subscribe && subscribe(children); } // 插入元素 ele.appendChild(children.ele); return; } return; } /** * @description 元素变化 * @param newEle * @param oldEle * @param comment */ function handleChangeElement(newEle, oldEle, comment, subscribe, unsubscribe) { if (newEle && oldEle) { const { beforeMount, onMounted } = newEle; if (beforeMount || onMounted) { subscribe && subscribe(newEle); } oldEle.ele.replaceWith(newEle.ele); return; } if (newEle && !oldEle) { const { beforeMount, onMounted } = newEle; if (beforeMount || onMounted) { subscribe && subscribe(newEle); } comment.replaceWith(newEle.ele); return; } if (!newEle && oldEle) { unsubscribe && unsubscribe(oldEle); oldEle.ele.replaceWith(comment); return; } } /** * @description 创建文字节点 * @param text * @returns */ function createTextNode(text, options) { const { onCreated, beforeCreat, onMounted, beforeMount } = options || {}; // 创建元素前 beforeCreat && beforeCreat(); // Ref if (isRef(text)) { // ref const refVal = text; // 元素 const ele = document.createTextNode(''); // 订阅变化 watchEffect(() => { ele.data = refVal.value; }); // 创建元素后 onCreated && onCreated(); return { ele, beforeMount, onMounted }; } // 创建元素后 onCreated && onCreated(); return { ele: document.createTextNode(String(text)), beforeMount, onMounted }; } /** * @description 挂载元素 * @param eleOptions * @param parent */ function mountElement(eleOptions, parent = document.body) { const { ele, beforeMount, onMounted } = eleOptions; if (ele) { // 触发挂载前事件 beforeMount && beforeMount(); parent.appendChild(ele); // 挂在后 onMounted && onMounted(); } } /** * @description 选择器 * @param selector * @returns */ function $$(selector, parent = document) { return Array.from(parent.querySelectorAll(selector)); } /** * @description 异步选择器 * @param selector * @returns */ function $_(selector, parent = document, timeout) { return new Promise((resolve) => { const timer = setInterval(() => { const selectors = Array.from(parent.querySelectorAll(selector)); // 存在元素 if (selectors.length) { clearInterval(timer); resolve(selectors); } }, 10); // 超时 if (timeout) { setTimeout(() => { clearInterval(timer); resolve([]); }, timeout); } }); } /** * @description 创建元素块 * @param eles * @returns */ function createElementBlock(eles) { const fragment = document.createDocumentFragment(); for (const i in eles) { fragment.appendChild(eles[i]); } return fragment; } /** * @description 打印日志 * @param text */ function log(...text) { printColor('dodgerblue', ...text); } /** * @description 打印错误 * @param text */ function error(...text) { printColor('red', ...text); } /** * @description 打印信息 * @param text */ function info(...text) { printColor('yellow', ...text); } /** * @description 打印颜色 * @param text * @param color */ function printColor(color, ...text) { const textFormatted = text .map((t) => (typeof t === 'object' ? JSON.stringify(t) : String(t))) .join(' '); console.log(`%c[${formatDateTime()}] %c${textFormatted}`, '', `color: ${color}`); } /** * @description html进度条 * @param title * @param percent * @returns */ function getProgressHTML(title, current, total) { // html const progressHTML = `
${title} ${getHighlightHTML(`${current}`)} / ${total}
`; return progressHTML; } /** * @description html高亮文本 * @param text * @returns */ function getHighlightHTML(text) { // html const highlightHTML = `${text}`; return highlightHTML; } /** * @description 二维码 * @param src */ function getImgHTML(src) { // 图片 return `
`; } /** * @description 创建模态框 * @param options 选项 * @returns */ function createModal(options) { // 配置 const { title, subTitle = '', to = '用户', content, type, from = 'tech-study.js', } = options; // 内容文本 let contentText = ''; if (Array.isArray(content)) { contentText = content.map((ct) => `
${ct}
`).join(''); } else { contentText = content; } // 日期 const dateTime = formatDateTime(); // 类型html let typeHTML = ''; if (type && type.length) { if (type === 'info') { typeHTML = ` `; } if (type === 'warn') { typeHTML = ` `; } if (type === 'success') { typeHTML = ` `; } if (type === 'fail') { typeHTML = ` `; } } // 类型 const typeWrap = ` ${typeHTML} `; // 基础html const baseHTML = `
${typeWrap} ${title}
${subTitle}
${getHighlightHTML(to)}, 你好!
${contentText}
${dateTime}
来自 ${from}
`; return baseHTML; } /** * @description 推送消息 */ async function pushMessage(options) { // 选项 const { title, content, template, fromToken, toToken } = options; // 推送 const res = await pushPlus(fromToken, title, content, template, toToken); return res; } /** * @description 推送模态框 */ async function pushModal(options, fromToken, toToken) { // html const html = createModal(options); // 推送 const res = await pushMessage({ title: '消息提示', content: html, fromToken, toToken, template: 'html', }); if (res && res.code === 200) { return res; } return; } /** * @description 创建随机点 * @param bounds 范围 * @returns */ function createRandomPoint(bounds) { // 范围 const { x, y, width, height } = bounds; // 横坐标 const randX = x + Math.random() * width * 0.5 + width * 0.25; // 纵坐标 const randY = y + Math.random() * height * 0.5 + height * 0.25; return { x: randX, y: randY, }; } /** * @description 生成随机路径 * @param start * @param end * @param steps * @returns */ function createRandomPath(start, end, steps) { // 最小水平增量 const minDeltaX = (end.x - start.x) / steps; // 最大垂直增量 const maxDeltaY = (end.y - start.y) / steps; const path = []; // 开始节点 path.push(start); // 插入点 for (let i = 0; i < steps; i++) { // 横坐标 const x = path[i].x + Math.random() * 5 + minDeltaX; // 纵坐标 const y = path[i].y + Math.random() * 5 * Math.pow(-1, ~~(Math.random() * 2 + 1)) + maxDeltaY; path.push({ x, y, }); } return path; } /** * @description 随机数字 * @returns */ function generateNumAsChar() { return (~~(Math.random() * 10)).toString(); } /** * @description 随机大写字母 * @returns */ function generateUpperAsChar() { return String.fromCharCode(~~(Math.random() * 26) + 65); } /** * @description 随机小写字母 * @returns */ function generateLowerAsChar() { return String.fromCharCode(~~(Math.random() * 26) + 97); } /** * @description 随机混合字符 * @param length * @returns */ function generateMix(length = 6) { // 随机字符串 const randomText = []; // 生成器 const typeGenerator = [ generateNumAsChar, generateUpperAsChar, generateLowerAsChar, ]; if (length) { for (let i = 0; i < length; i++) { // 随机位置 const randomIndex = ~~(Math.random() * typeGenerator.length); randomText.push(typeGenerator[randomIndex]()); } } return randomText.join(''); } /** * @description 格式化日期时间数字 * @param num * @returns */ function formatDateNum(num) { return num < 10 ? `0${num}` : `${num}`; } /** * @description 格式化日期时间 * @param time * @returns * @example * formatDateTime() -> "2022-09-01 08:00:00" * formatDateTime(new Date()) -> "2022-09-01 08:00:00" * formatDateTime(Date.now()) -> "2022-09-01 08:00:00" */ function formatDateTime(time = Date.now()) { const date = new Date(time); const s = date.getSeconds(); const min = date.getMinutes(); const h = date.getHours(); const d = date.getDate(); const m = date.getMonth() + 1; const y = date.getFullYear(); // 日期 const dateText = [y, m, d].map(formatDateNum).join('-'); // 时间 const timeText = [h, min, s].map(formatDateNum).join(':'); // 日期时间 const dateTimeText = `${dateText} ${timeText}`; return dateTimeText; } /** * @description 格式化时间 * @param time * @returns * @example * formatTime() -> "08:00:00" * formatTime(new Date()) -> "08:00:00" * formatTime(Date.now()) -> "08:00:00" */ const formatTime = (time = Date.now()) => { const date = new Date(time); const s = date.getSeconds(); const min = date.getMinutes(); const h = date.getHours(); // 时间 const timeText = [h, min, s].map(formatDateNum).join(':'); return timeText; }; /** * @description 时间已过 * @param hour * @param minute * @returns */ function isLate({ hour, minute }) { const date = new Date(); const h = date.getHours(); const min = date.getMinutes(); return h > hour || (h === hour && min >= minute); } /** * @description 时间已过 * @param hour * @param minute * @returns */ function isNow({ hour, minute }) { const date = new Date(); const h = date.getHours(); const min = date.getMinutes(); const s = date.getSeconds(); return h === hour && min === minute && s === 0; } /* 工具函数 */ /** * @description 设置cookie * @param name * @param value * @param expires */ function setCookie(name, value, expires, domain) { // 当前日期 const date = new Date(); // 过期日期 date.setTime(date.getTime() + expires); // 设置cookie document.cookie = `${name}=${value};expires=${date.toUTCString()};path=/;domain=${domain}`; } /** * @description 获取cookie * @param name * @returns */ function getCookie(name) { // 获取当前所有cookie const strCookies = document.cookie; // 截取变成cookie数组 const cookieText = strCookies.split(';'); // 循环每个cookie for (const i in cookieText) { // 将cookie截取成两部分 const item = cookieText[i].split('='); // 判断cookie的name 是否相等 if (item[0].trim() === name) { return item[1].trim(); } } return null; } /** * @description 删除cookie * @param name */ function delCookie(name, domain) { // 存在cookie const value = getCookie(name); if (value !== null) { setCookie(name, '', -1, domain); } } /** * @description 防抖 * @param callback * @param delay * @returns */ function debounce(callback, delay) { let timer = -1; return function (...args) { if (timer !== -1) { clearTimeout(timer); } timer = setTimeout(() => { callback.apply(this, args); }, delay); }; } /** * @description 判断是否为移动端 * @returns */ function hasMobile() { let isMobile = false; if (navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i)) { log('移动端'); isMobile = true; } if (document.body.clientWidth < 800) { log('小尺寸设备端'); isMobile = true; } return isMobile; } /** * @description 等待时间 * @param time * @returns */ function sleep(time) { // 延时 let timeDelay = Number(time); if (!Number.isInteger(timeDelay)) { timeDelay = 1000; } timeDelay += Math.random() * 500 - 250; return new Promise((resolve) => { setTimeout(() => { resolve(undefined); }, timeDelay); }); } /** * @description 暂停学习锁 */ function studyPauseLock(callback) { return new Promise((resolve) => { // 暂停 const pauseStudy = GM_getValue('pauseStudy') || false; if (pauseStudy) { const doing = setInterval(() => { // 暂停 const pauseStudy = GM_getValue('pauseStudy') || false; if (!pauseStudy) { // 停止定时器 clearInterval(doing); log('学习等待结束!'); if (callback && callback instanceof Function) { callback(true); } resolve(true); return; } if (callback && callback instanceof Function) { callback(false); } log('学习等待...'); }, 500); return; } resolve(true); }); } /** * @description 加载 * @param match * @param callback */ function load(match, callback) { // 链接 const { href } = window.location; window.addEventListener('load', () => { // 函数 if (match instanceof Function) { match(href) && callback(); return; } // 布尔 if (typeof match === 'boolean') { match && callback(); return; } // 字符正则 if (href.match(match)) { callback(); return; } }); } /* 变量 */ /** * @description 链接 */ const href = window.location.href; /** * @description 任务配置 */ const taskConfig = reactive([ { title: '登录', currentScore: 0, dayMaxScore: 0, need: 0, status: false, tip: '每日首次登录积1分。', score: 0, active: true, immutable: true, type: TaskType.LOGIN, }, { title: '文章选读', currentScore: 0, dayMaxScore: 0, need: 0, status: false, tip: '每有效阅读一篇文章积1分,上限6分。有效阅读文章累计1分钟积1分,上限6分。每日上限积12分。', score: 0, active: true, immutable: false, type: TaskType.READ, }, { title: '视听学习', currentScore: 0, dayMaxScore: 0, need: 0, status: false, tip: '每有效一个音频或观看一个视频积1分,上限6分。有效收听音频或观看视频累计1分钟积1分,上限6分。每日上限积12分。', score: 0, active: true, immutable: false, type: TaskType.WATCH, }, { title: '每日答题', currentScore: 0, dayMaxScore: 0, need: 0, status: false, tip: '每组答题每答对1道积1分。每日上限积5分。', score: 0, active: true, immutable: false, type: TaskType.PRACTICE, }, ]); /** * @description 设置 */ const settings = reactive([ false, false, false, false, false, false, false, false, ]); /** * @description 总分 */ const totalScore = ref(0); /** * @description 当天分数 */ const todayScore = ref(0); /** * @description 用户信息 */ const userinfo = reactive({ nick: '', avatar: '', }); /** * @description 进度 */ const taskStatus = ref(TaskStatusType.LOADING); /** * @description 答题暂停 */ const examPause = ref(false); /** * @description 登录 */ const login = ref(!!getCookie('token')); /** * @description 窗口id */ const id = ref(''); /** * @description 定时刷新列表 */ const scheduleList = shallowReactive([]); /** * @description 推送token */ const pushToken = ref(''); /** * @description 刷新次数 */ const refreshCount = ref(0); /** * @description 窗口关闭 */ const frame = reactive({ title: '', show: false, exist: false, closed: true, ele: undefined, src: '', }); /** * @description 页面 */ const page = ref(undefined); /** * @description 开始登录 */ const loginQRCodeShow = ref(false); /** * @description 最大选读时长 */ const maxRead = ref(100); /** * @description 最大视听时长 */ const maxWatch = ref(120); /** * @description 运行其他任务 */ const running = ref(false); /** * @description 主题色 */ const themeColor = ref('#fa3333'); /** * @description 考试类型 */ var ExamType; (function (ExamType) { ExamType[ExamType["PRACTICE"] = 0] = "PRACTICE"; ExamType[ExamType["PAPER"] = 1] = "PAPER"; })(ExamType || (ExamType = {})); /** * @description 获取答题按钮 */ function getNextButton() { return new Promise((resolve) => { const timer = setInterval(() => { // 答题按钮 const nextAll = $$('.ant-btn').filter((next) => next.innerText); if (nextAll.length) { // 停止定时器 clearInterval(timer); if (nextAll.length === 2) { resolve(nextAll[1]); return; } resolve(nextAll[0]); } }, 500); }); } /** * @description 处理滑动验证 */ function handleSlideVerify() { return new Promise(async (resolve) => { // 滑动验证 const mask = $$('#nc_mask')[0]; if (mask && getComputedStyle(mask).display !== 'none') { // 创建提示 createTip('等待滑动验证'); // 提高层级 mask.style.zIndex = '999'; // 轨道 const track = (await $_('.nc_scale', undefined, 3000))[0]; // 滑块 const slide = (await $_('.btn_slide', undefined, 3000))[0]; // 延时 await sleep(2000); // 矩形范围 const rectTrack = track.getBoundingClientRect(); // 矩形范围 const rectSlide = slide.getBoundingClientRect(); // 窗口 const window = unsafeWindow; // 范围内随机起点 const start = createRandomPoint(rectSlide); // 终点 const end = { x: rectTrack.x + rectTrack.width, y: rectTrack.y + rectTrack.height / 2, }; // 路径 const path = createRandomPath(start, end, 10); // 移动端 const mobile = hasMobile(); if (mobile) { slide.style.touchAction = 'none'; const touchstartTouch = new Touch({ identifier: 0, target: slide, clientX: path[0].x, clientY: path[0].y, }); const touchstartList = [touchstartTouch]; // 开始触摸 const touchstart = new TouchEvent('touchstart', { targetTouches: touchstartList, touches: touchstartList, changedTouches: touchstartList, view: window, bubbles: true, }); slide.dispatchEvent(touchstart); // 触摸滑动 for (const i in path) { const touchmoveTouch = new Touch({ identifier: 0, target: slide, clientX: path[i].x, clientY: path[i].y, }); const touchmoveList = [touchmoveTouch]; const touchmove = new TouchEvent('touchmove', { targetTouches: touchmoveList, touches: touchmoveList, changedTouches: touchmoveList, view: window, bubbles: true, }); slide.dispatchEvent(touchmove); await sleep(10); } const touchendTouch = new Touch({ identifier: 0, target: slide, clientX: path[path.length - 1].x, clientY: path[path.length - 1].y, }); // 触摸结束 const touchendList = [touchendTouch]; // 开始触摸 const touchend = new TouchEvent('touchend', { targetTouches: [], touches: [], changedTouches: touchendList, view: window, bubbles: true, }); slide.dispatchEvent(touchend); } else { // 鼠标按下 const mousedown = new MouseEvent('mousedown', { clientX: path[0].x, clientY: path[0].y, bubbles: true, view: window, }); slide.dispatchEvent(mousedown); // 鼠标滑动 for (const i in path) { const mousemove = new MouseEvent('mousemove', { clientX: path[i].x, clientY: path[i].y, bubbles: true, view: window, }); slide.dispatchEvent(mousemove); await sleep(10); } // 鼠标抬起 const mouseup = new MouseEvent('mouseup', { clientX: path[path.length - 1].x, clientY: path[path.length - 1].y, bubbles: true, view: window, }); slide.dispatchEvent(mouseup); } // 创建提示 createTip('滑动验证完成!'); // 定时器 const timer = setInterval(() => { // 滑动验证 const mask = $$('#nc_mask')[0]; if (!mask || getComputedStyle(mask).display === 'none') { log('滑动验证成功!'); // 创建提示 createTip('滑动验证成功!'); clearInterval(timer); resolve(true); return; } resolve(false); log('滑动验证失败!'); // 创建提示 createTip('滑动验证失败!'); }, 1000); return; } resolve(true); }); } /** * @description 处理选项 */ function handleChoiceBtn(answers) { // 选项按钮 const allBtns = $$('.q-answer'); // 答案存在 if (answers.length && allBtns.length) { // 作答 return answers.every((answer) => { // 答案存在 if (answer && answer.length) { // 包含答案最短长度选项 let minLengthChoice; // 遍历 allBtns.forEach((choice) => { // 选项文本 const choiceText = choice.innerText.trim(); // 无符号选项文本 const unsignedChoiceText = choiceText.replaceAll(/[、,,。 ]/g, ''); // 无符号答案 const unsignedAnswer = answer.replaceAll(/[、,,。 ]/g, ''); // 包含答案 if (choiceText === answer || choiceText.includes(answer) || answer.includes(choiceText) || unsignedChoiceText.includes(unsignedAnswer)) { // 最小长度选项有值 if (minLengthChoice) { // 最短长度选项与当前选项比较长度 if (minLengthChoice.innerText.length > choiceText.length) { minLengthChoice = choice; } } else { // 最小长度选项赋值 minLengthChoice = choice; } } }); // 存在选项 if (minLengthChoice) { // 选择 if (!minLengthChoice.classList.contains('chosen')) { minLengthChoice.click(); } return true; } } return false; }); } return false; } /** * @description 随机处理单选 */ function handleSingleChoiceRand() { // 选项按钮 const allBtns = $$('.q-answer'); // 按钮存在 if (allBtns.length) { const index = ~~(Math.random() * allBtns.length); const randBtn = allBtns[index]; // 选择 if (!randBtn.classList.contains('chosen')) { randBtn.click(); } } } /** * @description 随机处理多选 */ function handleMutiplyChoiceRand() { // 选项按钮 const allBtns = $$('.q-answer'); // 按钮存在 if (allBtns.length) { allBtns.forEach((allBtn) => { // 选择 if (!allBtn.classList.contains('chosen')) { allBtn.click(); } }); } } /** * @description 处理填空 */ const handleBlankInput = (answers) => { // 所有填空 const blanks = $$('.blank'); // 答案存在 if (blanks.length && answers.length) { // 填空数量和答案数量一致 if (answers.length === blanks.length) { return answers.every((answer, i) => { // 答案存在 if (answer && answer.length) { // 输入事件 const inputEvent = new Event('input', { bubbles: true, }); // 设置答案 blanks[i].setAttribute('value', answer); // 触发输入input blanks[i].dispatchEvent(inputEvent); return true; } return false; }); } // 填空数量为1和提示数量大于1 if (blanks.length === 1 && answers.length > 1) { // 直接将所有答案整合填进去 const answer = answers.join(''); // 答案存在 if (answer && answer.length) { // 输入事件 const inputEvent = new Event('input', { bubbles: true, }); // 设置答案 blanks[0].setAttribute('value', answer); // 触发输入input blanks[0].dispatchEvent(inputEvent); return true; } } } return false; }; /** * @description 处理填空随机 */ async function handleBlankInputRand() { // 所有填空 const blanks = $$('.blank'); if (blanks.length) { // 输入事件 const inputEvent = new Event('input', { bubbles: true, }); blanks.forEach((blank) => { // 设置答案 blank.setAttribute('value', '答案'); // 触发输入input blank.dispatchEvent(inputEvent); }); } } /** * @description 暂停锁 */ function examPauseLock(callback) { return new Promise((resolve) => { // 学习暂停 const pauseStudy = (GM_getValue('pauseStudy') || false); // 全局暂停 if (pauseStudy) { examPause.value = true; } // 暂停 if (examPause.value) { // 创建提示 createTip('已暂停, 手动开启自动答题! ', 10); const doing = setInterval(() => { if (!examPause.value) { // 停止定时器 clearInterval(doing); log('答题等待结束!'); if (callback && callback instanceof Function) { // 创建提示 createTip('已开启, 自动答题!'); callback(true); } resolve(true); return; } if (callback && callback instanceof Function) { callback(false); } log('答题等待...'); }, 500); return; } resolve(true); }); } /** * @description 答题 */ async function doingExam(type) { // 下一个按钮 let nextButton; // 下一个文本 let nextText; // 保存答案 let shouldSaveAnswer = false; while (true) { // 先等等再开始做题 await sleep(2500); // 暂停 await examPauseLock(); // 获取下一个按钮 nextButton = await getNextButton(); // 下一个文本 nextText = nextButton.innerText.replaceAll(' ', ''); // 结束 const finish = ['再练一次', '再来一组', '查看解析']; if (finish.includes(nextButton.innerText)) { break; } // 点击提示 $$('.tips')[0]?.click(); // 所有提示 const allTips = $$('.line-feed font[color]'); // 答案 const answers = allTips.map((tip) => tip.innerText.trim()); // 获取题目的文本内容 const question = $$('.q-body')[0].innerText; // 等待一段时间 await sleep(1500); // 暂停 await examPauseLock(); // 选项按钮 const allBtns = $$('.q-answer'); // 所有填空 const blanks = $$('input[type=text][class=blank]'); // 问题类型 const questionType = ($$('.q-header')[0].innerText.substring(0, 3)); // 暂停 await examPauseLock(); // 题型分类作答 switch (questionType) { case '填空题': { // 根据提示作答 if (answers.length) { const res = handleBlankInput(answers); // 成功 if (res) { break; } } // 创建提示 createTip('答案异常, 尝试网络题库获取!'); log('正在获取答案...'); // 尝试题库获取 const answersNetwork = await getAnswer(question); log(`获取答案${answersNetwork.length ? '成功' : '失败'}!`, { question, answersNetwork, }); // 根据题库作答 if (answersNetwork.length) { const res = handleBlankInput(answersNetwork); // 成功 if (res) { break; } } // 随机作答 if (type === ExamType.PRACTICE || settings[SettingType.RANDOM_EXAM]) { log('答案不存在, 随机作答!'); // 创建提示 createTip('答案不存在, 随机作答!'); await handleBlankInputRand(); } else { // 推送 const res = await pushModal({ title: '学习推送', to: userinfo.nick, content: '答题存在异常, 已暂停答题!', type: 'fail', }, pushToken.value); createTip(`学习推送${res ? '成功' : '失败'}!`); // 暂停 examPause.value = true; // 提交答案 shouldSaveAnswer = true; } break; } case '多选题': { // 根据提示作答 if (answers.length) { // 选项文本 const choicesText = allBtns.map((btn) => btn.innerText); // 选项内容 const choicesContent = choicesText .map((choiceText) => choiceText.split(/[A-Z]./)[1].trim()) .join(''); // 空格 const blanks = question.match(/()/g); // 填空数量、选项数量、答案数量相同 | 选项全文等于答案全文 if ((blanks && allBtns.length === blanks.length) || question === choicesContent || allBtns.length === 2) { // 全选 allBtns.forEach((choice) => { if (!choice.classList.contains('chosen')) { choice.click(); } }); break; } // 选项数量大于等于答案 if (allBtns.length >= answers.length) { const res = handleChoiceBtn(answers); // 成功 if (res) { break; } } } // 创建提示 createTip('答案异常, 尝试网络题库获取!'); log('正在获取答案...'); // 尝试题库获取 const answersNetwork = await getAnswer(question); log(`获取答案${answersNetwork.length ? '成功' : '失败'}!`, { question, answersNetwork, }); // 答案存在 if (answersNetwork.length) { const res = handleChoiceBtn(answersNetwork); // 成功 if (res) { break; } } // 随机作答 if (type === ExamType.PRACTICE || settings[SettingType.RANDOM_EXAM]) { log('答案不存在, 随机作答!'); // 创建提示 createTip('答案不存在, 随机作答!'); await handleMutiplyChoiceRand(); } else { // 推送 const res = await pushModal({ title: '学习推送', to: userinfo.nick, content: '答题存在异常, 已暂停答题!', type: 'fail', }, pushToken.value); createTip(`学习推送${res ? '成功' : '失败'}!`); // 暂停 examPause.value = true; // 提交答案 shouldSaveAnswer = true; } break; } case '单选题': { // 根据提示作答 if (answers.length) { // 创建提示为1 if (answers.length === 1) { const res = handleChoiceBtn(answers); // 成功 if (res) { break; } } else { // 可能的分隔符 const seperator = [ '', ' ', ',', ';', ',', '、', '-', '|', '+', '/', ]; // 可能的答案 const answersLike = seperator .map((s) => answers.join(s).trim()) .filter((answer) => answer.length); // 答案存在 if (answersLike.length) { // 可能答案是否正确 const res = answersLike.some((answer) => { // 尝试查找点击 return handleChoiceBtn([answer]); }); if (res) { break; } } } } // 创建提示 createTip('答案异常, 尝试网络题库获取!'); log('正在获取答案...'); // 尝试题库获取 const answersNetwork = await getAnswer(question); log(`获取答案${answersNetwork.length ? '成功' : '失败'}!`, { question, answersNetwork, }); // 存在答案 if (answersNetwork.length) { // 单答案单选项 if (answersNetwork.length === 1) { // 尝试查找点击 const res = handleChoiceBtn(answersNetwork); if (res) { break; } } else { // 多答案单选项 选项意外拆分 // 可能分隔符 const seperator = ['', ' ']; // 可能答案 const answersLike = seperator.map((s) => answers.join(s)); // 答案存在 if (answersLike.every((answer) => answer.length)) { // 可能答案是否正确 const res = answersLike.some((answer) => { // 尝试查找点击 return handleChoiceBtn([answer]); }); if (res) { break; } } } } // 随机作答 if (type === ExamType.PRACTICE || settings[SettingType.RANDOM_EXAM]) { log('答案不存在, 随机作答!'); // 创建提示 createTip('答案不存在, 随机作答!'); await handleSingleChoiceRand(); } else { // 推送 const res = await pushModal({ title: '学习推送', to: userinfo.nick, content: '答题存在异常, 已暂停答题!', type: 'fail', }, pushToken.value); createTip(`学习推送${res ? '成功' : '失败'}!`); // 暂停 examPause.value = true; // 提交答案 shouldSaveAnswer = true; } break; } } // 暂停 await examPauseLock(); // 获取下一个按钮 nextButton = await getNextButton(); // 下一个文本 nextText = nextButton.innerText.replaceAll(' ', ''); // 需要提交答案 if (shouldSaveAnswer) { // 答案 const answers = []; if (questionType === '填空题') { blanks.forEach((blank) => { answers.push(blank.value); }); } if (questionType === '单选题' || questionType === '多选题') { allBtns.forEach((choice) => { if (choice.classList.contains('chosen')) { // 带字母的选项 const answerTemp = choice.innerText; // 从字符串中拿出答案 const [, answer] = answerTemp.split('.'); if (answer && answer.length) { answers.push(answer); } } }); } // 答案 const answer = answers.join(';'); // 存在答案 if (answer.length) { log('正在上传答案...'); // 上传答案 const res = await saveAnswer(question, answer); log(`上传答案${res ? '成功' : '失败'}!`, { question, answer }); } // 重置 shouldSaveAnswer = false; } // 确定 if (nextText === '确定') { // 确认 nextButton.click(); // 等待一段时间 await sleep(2000); // 暂停 await examPauseLock(); // 答案解析 const answerBox = $$('.answer')[0]; // 答题错误 if (answerBox) { const answerTemp = answerBox.innerText; // 从字符串中拿出答案 const [, answerText] = answerTemp.split(':'); if (answerText && answerText.length) { const answer = answerText.replaceAll(' ', ';'); log('正在上传答案...'); // 上传答案 const res = await saveAnswer(question, answer); log(`上传答案${res ? '成功' : '失败'}!`, { question, answer }); } } } // 获取按钮 nextButton = await getNextButton(); // 下一个文本 nextText = nextButton.innerText.replaceAll(' ', ''); if (nextText === '下一题' || nextText === '完成' || nextText === '交卷') { // 等待一段时间 await sleep(2500); // 下一题 nextButton.click(); } // 滑动验证 await handleSlideVerify(); } // 关闭任务窗口 handleCloseTaskWin(); } /** * @description 每日答题 */ async function doExamPractice() { // 暂停 await studyPauseLock(); log('正在每日答题...'); // 创建提示 createTip('正在每日答题'); // 链接 const url = URL_CONFIG.examPractice; // 等待任务窗口 await waitTaskWin(url, '每日答题'); // 创建提示 createTip('完成每日答题!'); // 等待一段时间 await sleep(1500); // 刷新分数数据 await refreshScoreInfo(); // 刷新任务数据 await refreshTaskList(); // 任务完成状况 if (taskConfig[TaskType.PRACTICE].active && !taskConfig[TaskType.PRACTICE].status) { log('任务未完成, 继续每日答题!'); // 创建提示 createTip('任务未完成, 继续每日答题!'); await doExamPractice(); } } /** * @description 专项练习 */ async function doExamPaper() { running.value = true; log('正在专项练习...'); // 创建提示 createTip('正在专项练习'); // id const examPaperId = await findExamPaper(); if (examPaperId) { // 链接 const url = `${URL_CONFIG.examPaper}?id=${examPaperId}`; log(`链接: ${url}`); // 等待窗口任务 await waitTaskWin(url, '专项练习'); // 创建提示 createTip('完成专项练习!'); running.value = false; // 同屏任务 if (settings[SettingType.SAME_TAB]) { // 窗口不存在 frame.exist = false; } return; } running.value = false; // 创建提示 createTip('专项练习均已完成!'); } /** * @description 初始化总页数属性 */ async function initExam() { // 默认从第一页获取全部页属性 const data = await getExamPaper(1); if (data) { // 等待 await sleep(3000); return data.totalPageCount; } } /** * @description 查询专项练习列表 */ async function findExamPaper() { // 获取总页数 const total = await initExam(); // 当前页数 let current = 1; log(`正在寻找的专项练习...`); // 创建提示 createTip(`正在寻找的专项练习...`); while (current <= total && current) { // 请求数据 const data = await getExamPaper(current); if (data) { // 获取专项练习的列表 const examPapers = data.list; for (const i in examPapers) { // 遍历查询有没有没做过的 if (examPapers[i].status !== 2) { // status: 1 开始答题, 2 已满分/重新答题, 3 继续答题 return examPapers[i].id; } } // 增加页码 current += 1; // 等待 await sleep(3000); } else { break; } } } /** * @description 初始化主页面 */ function initMainListener() { // 监听关闭 window.addEventListener('message', (msg) => { const { data } = msg; if (data.id === id.value && data.closed) { // 关闭窗口 closeFrame(); return; } }); } /** * @description 初始化子页面 */ function initChildListener() { window.addEventListener('message', (msg) => { const { data } = msg; if (data.id && !data.closed) { // 设置窗口id id.value = data.id; log(`初始化窗口 ID: ${id.value}`); return; } }); } /** * @description 打开窗口 * @param url * @returns */ async function openFrame(url, title) { // 设置 URL frame.src = url; // 等待元素 await $_('.egg_frame'); if (frame.ele) { // id id.value = generateMix(10); // 打开 frame.closed = false; // 设置标题 frame.title = title || ''; // 等待页面加载 await waitFrameLoaded(frame.ele); // 发送窗口 ID frame.ele.contentWindow?.postMessage({ id: id.value, closed: false }, url); return true; } return false; } /** * @description 关闭窗口 */ function closeFrame() { log(`关闭窗口 ID: ${id.value}`); // 窗口显示 frame.show = false; // 关闭 frame.closed = true; // 标题 frame.title = ''; // src frame.src = ''; } /** * @description 关闭 frame */ function handleCloseFrame() { window.parent.postMessage({ id: id.value, closed: true }, URL_CONFIG.homeOrigin); } /** * @description 等待窗口任务结束 * @param id * @returns */ function waitFrameClose() { return new Promise((resolve) => { const timer = setInterval(() => { // 窗口关闭 if (frame.closed) { clearInterval(timer); resolve(true); } }, 100); }); } // 等待窗口加载 function waitFrameLoaded(iframe) { return new Promise((resolve) => { iframe.addEventListener('load', () => { resolve(true); }); }); } /** * @description 打开新窗口 */ function openWin(url) { return GM_openInTab(url, { active: true, insert: true, setParent: true, }); } /** * @description 关闭窗口 */ function closeWin() { page.value && page.value.close(); } /** * @description 关闭子窗口 */ function handleCloseWin() { try { window.opener = window; const win = window.open('', '_self'); win?.close(); top?.close(); } catch (e) { } } /** * @description 等待窗口关闭 * @param newPage * @returns */ function waitWinClose(newPage) { return new Promise((resolve) => { newPage.onclose = () => { resolve(undefined); }; }); } /** * @description 关闭任务窗口 */ function closeTaskWin() { // 同屏任务 if (settings[SettingType.SAME_TAB] && id.value) { closeFrame(); return; } // 非同屏任务 closeWin(); } /** * @description 关闭任务窗口 */ function handleCloseTaskWin() { // 同屏任务 if (settings[SettingType.SAME_TAB] && id.value) { handleCloseFrame(); return; } // 子窗口 handleCloseWin(); } /** * @description 打开并等待任务结束 */ async function waitTaskWin(url, title) { // 同屏任务 if (settings[SettingType.SAME_TAB]) { // 窗口存在 frame.exist = true; // 显示窗体 frame.show = !settings[SettingType.SILENT_RUN]; // 新窗口 const res = await openFrame(url, title); if (res) { // 等待窗口关闭 await waitFrameClose(); } return; } // 子页面任务 page.value = openWin(url); await waitWinClose(page.value); } /** * @description 二维码刷新定时器 */ let refreshTimer = -1; /** * @description 尝试登录 */ let tryLoginTimer = -1; /** * @description 生成二维码 */ async function getQRCode() { log('正在生成登录二维码...'); const qrCode = await generateQRCode(); if (qrCode) { log('生成登录二维码成功!'); // 链接 const url = `https://login.xuexi.cn/login/qrcommit?showmenu=false&code=${qrCode}&appId=dingoankubyrfkttorhpou`; return { code: qrCode, src: `${API_CONFIG.qrcode}?data=${encodeURIComponent(url)}`, url, }; } log('生成登录二维码失败!'); } /** * @description 验证登录二维码 * @param code * @returns */ async function checkQRCode(code) { log('尝试用二维码登录...'); // 二维码登录 const res = await loginWithQRCode(code); if (res) { const { data, code, success } = res; // 临时登录验证码 if (success && data) { return data; } // 二维码失效 if (code === '11019') { return; } } return new Promise((resolve) => { // 清除定时 clearTimeout(tryLoginTimer); // 设置定时 tryLoginTimer = setTimeout(async () => { resolve(await checkQRCode(code)); }, 1000); }); } /** * @description 尝试二维码登录 */ async function tryLogin(checkCode) { log('正在获取签名...'); // 获取签名 const sign = await getSign(); if (sign) { // 生成uuid const uuid = crypto.randomUUID(); const [, code] = checkCode.split('='); const state = `${sign}${uuid}`; // 安全检查 const res = await secureCheck({ code, state }); return res; } } /** * @description 刷新登录二维码 */ async function handleLogin() { // 清除刷新 clearInterval(refreshTimer); // 每隔一段时间刷新 refreshTimer = setInterval(() => { // 刷新二维码 handleLogin(); }, autoRefreshQRCodeInterval); // 是否超出次数 if (refreshCount.value >= maxRefreshCount) { createTip('超过最大重试次数, 登录失败!'); // 重置刷新数 refreshCount.value = 0; // 隐藏二维码 loginQRCodeShow.value = false; // 远程推送 if (settings[SettingType.REMOTE_PUSH]) { // 推送 const res = await pushModal({ title: '登录推送', content: '超过最大重试次数, 登录失败!', type: 'fail', }, pushToken.value); createTip(`登录推送${res ? '成功' : '失败'}!`); } return; } // 配置 const imgWrap = $$('.egg_login_img_wrap')[0]; // 图片 const img = $$('.egg_login_img', imgWrap)[0]; if (imgWrap && img) { // 刷新二维码 log('刷新登录二维码!'); // 刷新次数累加 refreshCount.value++; // 获取二维码 const qrCode = await getQRCode(); if (qrCode) { // 获取连接 const { src, code, url } = qrCode; // src img.src = src; // 开始登录 loginQRCodeShow.value = true; // 远程推送 if (settings[SettingType.REMOTE_PUSH]) { // img html const imgWrap = getImgHTML(src); // 跳转链接 const aWrap = ` `; // 推送 const res = await pushModal({ title: '登录推送', content: ['扫一扫, 登录学习强国!', aWrap, imgWrap], type: 'info', }, pushToken.value); createTip(`登录推送${res ? '成功' : '失败'}!`); } // 获取验证码 const checkCode = await checkQRCode(code); // 验证成功 if (checkCode) { // 尝试登录 const loginRes = await tryLogin(checkCode); if (loginRes) { // 清除刷新 clearInterval(refreshTimer); // 二维码显示 loginQRCodeShow.value = false; // 登录成功 log('登录成功!'); // 创建提示 createTip('登录成功!'); // 登录成功 login.value = true; // 刷新用户信息 await refreshUserInfo(); // 刷新分数信息 await refreshScoreInfo(); // 刷新任务信息 await refreshTaskList(); // 远程推送 if (settings[SettingType.REMOTE_PUSH]) { const res = await pushModal({ title: '登录推送', to: userinfo.nick, content: [ '学习强国, 登录成功!', `当天积分: ${getHighlightHTML(todayScore.value)} 分`, `总积分: ${getHighlightHTML(totalScore.value)} 分`, ...taskConfig.map((task) => getProgressHTML(task.title, task.currentScore, task.dayMaxScore)), ], type: 'success', }, pushToken.value); createTip(`登录推送${res ? '成功' : '失败'}!`); } } return; } // 二维码失效 log('登录二维码失效!'); // 二维码失效刷新 handleLogin(); } } } /** * @description 退出登录 */ function handleLogout() { // 删除token delCookie('token', '.xuexi.cn'); // 关闭窗口 closeFrame(); frame.exist = false; // 退出登录 login.value = false; // 清除用户信息 userinfo.nick = ''; userinfo.avatar = ''; // 总分 totalScore.value = 0; // 当天分数 todayScore.value = 0; // 任务进度重置 taskConfig.forEach((task) => { task.currentScore = 0; }); taskStatus.value = TaskStatusType.LOADING; // 退出登录 log('退出登录'); } /** * @description 新闻 */ let news = []; /** * @description 视频 */ let videos = []; /** * @description 处理文章 */ async function handleNews() { // section const sections = await $_('section', undefined, 5000); const section = sections[0]; if (!(section && section.innerText.includes('系统正在维护中'))) { // 文章选读 reading(0); return; } log('未找到文章!'); // 提示 createTip('未找到文章!'); // 关闭页面 handleCloseTaskWin(); } /** * @description 处理视频 */ async function handleVideo() { // videos const videos = await $_('video', undefined, 10000); // 视频 const video = videos[0]; // 播放按键 const playBtn = $$('.prism-play-btn')[0]; if (video && playBtn) { log('正在尝试播放视频...'); // 播放超时 const timeout = setTimeout(() => { log('视频播放超时!'); // 提示 createTip('视频播放超时!'); // 关闭页面 handleCloseTaskWin(); }, 20000); // 设置是否静音 watchEffect(() => (video.muted = settings[SettingType.VIDEO_MUTED])); // 能播放 video.addEventListener('canplay', () => { const timer = setInterval(() => { // 尝试点击播放按钮播放 playBtn.click(); // 播放未成功 if (video.paused) { // 尝试使用js的方式播放 video.play(); } }, 1000); video.addEventListener('playing', () => { // 清除超时定时器 clearTimeout(timeout); // 清除定时器 clearInterval(timer); log('播放视频成功!'); // 视听学习 reading(1); return; }, { once: true }); }, { once: true }); return; } log('未找到视频!'); // 关闭页面 handleCloseTaskWin(); } /** * @description 读新闻或者看视频 * @param type :0为新闻,1为视频 */ async function reading(type) { let time = 30; // 文章选读 if (type === 0) { // 章节 const sections = $$('section'); // 最大字数 const maxTextCount = Math.max(...sections.map((s) => s.innerText.length), 200); // 预计时间 const predictTime = ~~((60 * maxTextCount) / 1000); // min(predictTime, maxWatch.value) 秒后关闭页面 time = Math.min(predictTime, maxRead.value); } // 视听学习 if (type === 1) { // 视频 const video = $$('video')[0]; // 预计时间 const predictTime = ~~video.duration; // min(predictTime, maxWatch.value) 秒后关闭页面 time = Math.min(predictTime, maxWatch.value); } // 随机 time = time - ~~(Math.random() * 10) + 5; // 第一次滚动时间 const firstTime = time - (~~(Math.random() * 4) + 4); // 第二次滚动时间 const secendTime = ~~(Math.random() * 4) + 8; // 窗口 const window = unsafeWindow; // 创建提示 const tip = createTip('距离关闭页面还剩', time, true, async (time) => { // 暂停锁 await studyPauseLock((flag) => { if (type === 1) { // 视频 const video = $$('video')[0]; // 排除反复设置 if (video.paused === !flag) { return; } // 设置播放状态 video[flag ? 'play' : 'pause'](); } }); // 第一次滚动 if (time === firstTime) { // 滚动 window.scrollTo(0, 400); // 模拟滚动 const scroll = new Event('scroll', { bubbles: true, }); document.dispatchEvent(scroll); // 模拟滑动 const mousemove = new MouseEvent('mousemove', { bubbles: true, }); document.dispatchEvent(mousemove); // 模拟点击 const click = new Event('click', { bubbles: true, }); document.dispatchEvent(click); } // 第二次滚动 if (time === secendTime) { // 滚动长度 const scrollLength = document.body.scrollHeight / 2; // 滚动 window.scrollTo(0, scrollLength); // 模拟滚动 const scroll = new Event('scroll', { bubbles: true, }); document.dispatchEvent(scroll); // 模拟滑动 const mousemove = new MouseEvent('mousemove', { bubbles: true, }); document.dispatchEvent(mousemove); // 模拟点击 const click = new Event('click', { bubbles: true, }); document.dispatchEvent(click); } }); // 倒计时结束 await tip.waitCountDown(); // 关闭任务窗口 handleCloseTaskWin(); } /** * @description 获取新闻列表 */ async function getNews() { // 需要学习的新闻数量 const need = taskConfig[TaskType.READ].need < maxNewsNum ? taskConfig[TaskType.READ].need : maxNewsNum; log(`剩余 ${need} 个新闻`); // 获取新闻 const data = await getNewsList(); if (data && data.length) { // 索引 let i = 0; // 最新新闻 const latestItems = data.slice(0, 100); // 当前年份 const currentYear = new Date().getFullYear().toString(); // 查找今年新闻 while (i < need) { const randomIndex = ~~(Math.random() * latestItems.length); // 新闻 const item = latestItems[randomIndex]; // 是否存在 if (item.publishTime.startsWith(currentYear) && item.type === 'tuwen') { news[i] = item; i++; } } } else { news = []; } } /** * @description 获取视频列表 */ async function getVideos() { // 需要学习的视频数量 const need = taskConfig[TaskType.WATCH].need < maxVideoNum ? taskConfig[TaskType.WATCH].need : maxVideoNum; log(`剩余 ${need} 个视频`); // 获取视频 const data = await getVideoList(); if (data && data.length) { // 索引 let i = 0; // 最新视频 const latestItems = data.slice(0, 100); // 当前年份 const currentYear = new Date().getFullYear().toString(); // 查找今年视频 while (i < need) { const randomIndex = ~~(Math.random() * latestItems.length); // 新闻 const item = latestItems[randomIndex]; // 是否存在 if (item.publishTime.startsWith(currentYear) && (item.type === 'shipin' || item.type === 'juji')) { videos[i] = item; i++; } } } else { videos = []; } } /** * @description 阅读文章 */ async function readNews() { // 获取文章 await getNews(); // 观看文章 for (const i in news) { // 任务关闭跳出循环 if (!taskConfig[TaskType.READ].active) { return; } // 暂停 await studyPauseLock(); log(`正在阅读第 ${Number(i) + 1} 个新闻...`); // 创建提示 createTip(`正在阅读第 ${Number(i) + 1} 个新闻`); // 链接 const { url } = news[i]; // 链接 GM_setValue('readingUrl', url); // 等待任务窗口 await waitTaskWin(url, '文章选读'); // 清空链接 GM_setValue('readingUrl', null); // 创建提示 createTip(`完成阅读第 ${Number(i) + 1} 个新闻!`); // 等待一段时间 await sleep(1500); // 刷新分数数据 await refreshScoreInfo(); // 刷新任务数据 await refreshTaskList(); // 任务完成跳出循环 if (taskConfig[TaskType.READ].active && taskConfig[TaskType.READ].status) { break; } } // 任务关闭跳出循环 if (!taskConfig[TaskType.READ].active) { return; } // 任务完成状况 if (taskConfig[TaskType.READ].active && !taskConfig[TaskType.READ].status) { log('任务未完成, 继续阅读新闻!'); // 创建提示 createTip('任务未完成, 继续阅读新闻!'); await readNews(); } } /** * @description 观看视频 */ async function watchVideo() { // 获取视频 await getVideos(); // 观看视频 for (const i in videos) { // 任务关闭跳出循环 if (!taskConfig[TaskType.WATCH].active) { return; } // 暂停 await studyPauseLock(); log(`正在观看第 ${Number(i) + 1} 个视频...`); // 创建提示 createTip(`正在观看第 ${Number(i) + 1} 个视频`); // 链接 const { url } = videos[i]; // 链接 GM_setValue('watchingUrl', url); // 等待任务窗口 await waitTaskWin(url, '视听学习'); // 清空链接 GM_setValue('watchingUrl', null); // 创建提示 createTip(`完成观看第 ${Number(i) + 1} 个视频!`); // 等待一段时间 await sleep(1500); // 刷新分数数据 await refreshScoreInfo(); // 刷新任务数据 await refreshTaskList(); // 任务完成跳出循环 if (taskConfig[TaskType.WATCH].active && taskConfig[TaskType.WATCH].status) { break; } } // 任务关闭跳出循环 if (!taskConfig[TaskType.WATCH].active) { return; } // 任务完成状况 if (taskConfig[TaskType.WATCH].active && !taskConfig[TaskType.WATCH].status) { log('任务未完成, 继续观看视频!'); // 创建提示 createTip('任务未完成, 继续观看看视频!'); await watchVideo(); } } /** * @description 定时刷新定时器 */ let scheduleTimer = -1; /** * @description 刷新定时任务 */ async function refreshScheduleTask() { // 清除定时刷新 clearInterval(scheduleTimer); // 未登录 if (!login.value) { // 剩余定时任务 const restList = scheduleList.filter((s) => !isLate(s)); // 存在剩余任务 if (restList.length) { const rest = restList[0]; log(`已设置 ${rest.time} 的定时任务!`); // 提示 createTip(`已设置 ${rest.time} 的定时任务!`); // 时间 let time = 0; // 刷新间隔 const interval = 10; scheduleTimer = setInterval(() => { if (!(time++ % interval)) { log('定时刷新正在运行...'); } // 到达定时 if (isNow(rest)) { clearInterval(scheduleTimer); log(`执行 ${rest.time} 的定时任务!`); // 提示 createTip(`执行 ${rest.time} 的定时任务!`); // 登录 handleLogin(); } }, 1000); } } } /** * @description 清除定时 */ function clearScheduleTask() { clearInterval(scheduleTimer); } /** * @description 创建学习提示 */ function createTip(text, delay = 2, countShow = false, callback) { const tipWrap = $$('.egg_tip_wrap')[0]; // 提前去除 const tips = $$('.egg_tip'); if (tips.length) { tips.forEach((t) => t.delay()); } // 延迟 const delayCount = ref(delay); // 文字 const textContent = ref(text); //显示 const show = ref(false); // 延迟显示 const delayShow = ref(false); // 销毁 let destroyed = false; // 倒计时结束 let done = false; // 提示 const tip = Tip({ text: textContent, count: delayCount, show, delayShow, countShow: ref(countShow), callback: async (count) => { callback && (await callback(count)); // 恢复显示 if (delayShow.value && count === delay) { delayShow.value = false; } // 倒计时结束 if (count <= 0) { done = true; operate.destroy(); } }, }); // 操作 const operate = { destroy() { if (!destroyed) { // 隐藏 operate.hide(); // 销毁 destroyed = true; return new Promise((resolve) => { setTimeout(() => { tip.ele.remove(); resolve(undefined); }, 300); }); } }, hide() { if (!destroyed) { show.value = false; } }, show() { if (!destroyed) { return new Promise((resolve) => { setTimeout(() => { show.value = true; resolve(undefined); }, 300); }); } }, setText(text) { if (!destroyed) { textContent.value = text; } }, waitCountDown() { return new Promise((resolve) => { // 计时器 const timer = setInterval(() => { // 结束 if (done) { clearInterval(timer); resolve(true); } }, 100); }); }, delay() { if (!destroyed) { delayShow.value = true; delayCount.value += 2; } }, }; Object.assign(tip.ele, operate); // 插入节点 mountElement(tip, tipWrap); // 显示 operate.show(); return operate; } /** * @description 刷新用户信息 */ async function refreshUserInfo() { // 未登录 if (!login.value) { throw new Error('用户未登录!'); } // 已存在信息 if (userinfo.nick) { return true; } log('加载用户信息...'); // 获取用户信息 const res = await getUserInfo(); if (res) { const { avatarMediaUrl = '', nick: nickRes } = res; if (nickRes) { // 设置昵称 userinfo.nick = nickRes; // 设置头像 userinfo.avatar = avatarMediaUrl; return true; } } log('加载用户信息失败!'); return false; } /** * @description 刷新分数信息 */ async function refreshScoreInfo() { // 未登录 if (!login.value) { throw new Error('用户未登录!'); } log('加载分数信息...'); // 获取总分 const totalScoreRes = await getTotalScore(); // 获取当天总分 const todayScoreRes = await getTodayScore(); // 整数值 if (Number.isInteger(totalScoreRes) && Number.isInteger(todayScoreRes)) { // 设置分数 totalScore.value = totalScoreRes; todayScore.value = todayScoreRes; return true; } log('加载分数信息失败!'); return false; } /** * @description 刷新任务列表 */ async function refreshTaskList() { // 未登录 if (!login.value) { throw new Error('用户未登录!'); } log('加载任务进度...'); // 原始任务进度 const taskProgress = await getTaskList(); if (taskProgress) { // 登录 taskConfig[TaskType.LOGIN].currentScore = taskProgress[2].currentScore; taskConfig[TaskType.LOGIN].dayMaxScore = taskProgress[2].dayMaxScore; taskConfig[TaskType.LOGIN].need = taskProgress[2].dayMaxScore - taskProgress[2].currentScore; // 文章选读 taskConfig[TaskType.READ].currentScore = taskProgress[0].currentScore; taskConfig[TaskType.READ].dayMaxScore = taskProgress[0].dayMaxScore; taskConfig[TaskType.READ].need = taskProgress[0].dayMaxScore - taskProgress[0].currentScore; // 视听学习 taskConfig[TaskType.WATCH].currentScore = taskProgress[1].currentScore; taskConfig[TaskType.WATCH].dayMaxScore = taskProgress[1].dayMaxScore; taskConfig[TaskType.WATCH].need = taskProgress[1].dayMaxScore - taskProgress[1].currentScore; // 每日答题 taskConfig[TaskType.PRACTICE].currentScore = taskProgress[3].currentScore; taskConfig[TaskType.PRACTICE].dayMaxScore = taskProgress[3].dayMaxScore; taskConfig[TaskType.PRACTICE].need = taskProgress[3].dayMaxScore; // 更新数据 for (const i in taskConfig) { const { currentScore, dayMaxScore } = taskConfig[i]; // 进度 const rate = Number(((100 * currentScore) / dayMaxScore).toFixed(1)); // 分数 taskConfig[i].score = currentScore; // 完成状态 taskConfig[i].status = rate === 100; } return; } // 重试 await sleep(2000); refreshTaskList(); return; } function Tip({ text, count, show, delayShow, countShow, callback, }) { return createElementNode('div', undefined, { class: watchRef([show, delayShow], () => `egg_tip${show.value ? (delayShow.value ? ' active delay' : ' active') : ''}`), }, [ createElementNode('span', undefined, { class: 'egg_text', }, createTextNode(text)), watchEffectRef(() => countShow.value ? createElementNode('span', undefined, { class: 'egg_countdown', }, createTextNode(watchEffectRef(() => `${count.value}s`))) : undefined), ], { onMounted() { // 倒计时 const countDown = async () => { // 倒计时回调 await callback(count.value); // 倒计时结束 if (!count.value) { show.value = false; return; } count.value--; setTimeout(countDown, 1000); }; countDown(); }, }); } /** * @description 分隔符 * @returns */ function Hr({ text }) { return createElementNode('div', undefined, { class: 'egg_hr_wrap', }, [ createElementNode('div', undefined, { class: 'egg_hr' }), createElementNode('div', undefined, { class: 'egg_hr_title' }, createTextNode(text)), createElementNode('div', undefined, { class: 'egg_hr' }), ]); } function Select({ data, maxlength, placeholder = '', onchange, onblur, value, keep, }) { const selectData = reactive(data.map((v) => ({ selected: false, active: false, ele: undefined, ...v }))); const focus = ref(false); const input = shallowRef(undefined); const list = shallowRef(undefined); const valueRef = ref(''); value && watch(value, () => { const item = selectData.find((v) => v.value === value.value); valueRef.value = item ? item.label : ''; if (!item) { selectData.forEach((v) => (v.selected = false)); list.value && (list.value.scrollTop = 0); } }, true); return createElementNode('div', undefined, { class: 'egg_select', }, [ createElementNode('input', { value: valueRef }, { class: 'egg_select_input', type: 'text', placeholder, maxlength, ref: input, onfocus() { if (list.value && input.value) { focus.value = true; if (input.value.value && valueRef.value) { const index = selectData.findIndex((v) => v.label === valueRef.value); if (index + 1) { list.value.scrollTop = selectData[index].ele?.offsetTop || 0; selectData.forEach((v, i) => (v.selected = i === index)); } return; } } }, oninput() { if (list.value && input.value) { const { value } = input.value; // 文本存在 if (value) { const index = selectData.findIndex((v) => v.label.includes(value)); // 存在匹配 if (index + 1) { list.value.scrollTop = selectData[index].ele?.offsetTop || 0; selectData.forEach((v, i) => { v.active = i === index; v.active && setTimeout(() => { v.active = false; }, 300); }); } return; } // 清除 selectData.forEach((v) => (v.active = v.selected = false)); list.value.scrollTop = 0; } }, onblur() { if (list.value && input.value) { const item = selectData.find((v) => v.selected); // 关闭选项 if (item || !input.value.value) { setTimeout(() => { focus.value = false; }, 100); } // 恢复文本 if (item && input.value.value !== item.label) { input.value.value = item.label; } // 保留文本 if (!item && keep) { input.value.value = valueRef.value; } onblur && onblur(item ? { label: item.label, value: item.value } : undefined); } }, }), createElementNode('div', undefined, { class: watchEffectRef(() => `egg_select_list${focus.value ? '' : ' hide'}`), ref: list, }, selectData.map((v, index) => createElementNode('div', undefined, { class: watchRef(() => [v.selected, v.active], () => `egg_select_item${v.selected ? ' selected' : v.active ? ' active' : ''}`), ref: (e) => (v.ele = e), onclick: debounce(() => { if (valueRef.value !== v.label) { onchange && onchange({ label: v.label, value: v.value }); selectData.forEach((v, i) => { v.selected = i === index; v.selected && (valueRef.value = v.label); }); } focus.value = false; }, 300), }, createTextNode(v.label)))), ]); } /** * @description 答题按钮 */ function ExamBtn() { // 设置初始状态 watchEffect(() => (examPause.value = !settings[SettingType.AUTO_ANSWER])); return createElementNode('button', undefined, { class: watchEffectRef(() => `egg_exam_btn${examPause.value ? ' manual' : ''}`), type: 'button', onclick(e) { e.stopPropagation(); examPause.value = !examPause.value; }, onmousedown(e) { e.stopPropagation(); }, onmousemove(e) { e.stopPropagation(); }, onmouseup(e) { e.stopPropagation(); }, onmouseenter(e) { e.stopPropagation(); }, onmouseleave(e) { e.stopPropagation(); }, onmouseover(e) { e.stopPropagation(); }, ontouchstart(e) { e.stopPropagation(); }, ontouchmove(e) { e.stopPropagation(); }, ontouchend(e) { e.stopPropagation(); }, oninput(e) { e.stopPropagation(); }, onchange(e) { e.stopPropagation(); }, onblur(e) { e.stopPropagation(); }, }, createTextNode(watchEffectRef(() => `${examPause.value ? '开启自动答题' : '关闭自动答题'}`))); } /** * @description 任务窗口 * @returns */ function Frame() { // 最大化 const max = ref(false); // 容器 return createElementNode('div', undefined, { class: watchEffectRef(() => `egg_frame_wrap${frame.show ? '' : ' hide'}`), onclick(e) { e.stopPropagation(); }, onmousedown(e) { e.stopPropagation(); }, onmousemove(e) { e.stopPropagation(); }, onmouseup(e) { e.stopPropagation(); }, onmouseenter(e) { e.stopPropagation(); }, onmouseleave(e) { e.stopPropagation(); }, onmouseover(e) { e.stopPropagation(); }, ontouchstart(e) { e.stopPropagation(); }, ontouchmove(e) { e.stopPropagation(); }, ontouchend(e) { e.stopPropagation(); }, oninput(e) { e.stopPropagation(); }, onchange(e) { e.stopPropagation(); }, onblur(e) { e.stopPropagation(); }, }, watchRef(() => [login.value, settings[SettingType.SAME_TAB]], () => { // 同屏任务 if (login.value && settings[SettingType.SAME_TAB]) { return [ // 遮罩 createElementNode('div', undefined, { class: 'egg_frame_mask' }), // 窗口内容 createElementNode('div', undefined, { class: watchEffectRef(() => `egg_frame_content_wrap ${max.value ? ' max' : ''}`), }, [ // 窗口控制 createElementNode('div', undefined, { class: 'egg_frame_controls_wrap' }, [ // 标题 createElementNode('div', undefined, { class: 'egg_frame_title', }), createElementNode('div', undefined, { class: 'egg_frame_controls', }, [ // 隐藏 createElementNode('button', undefined, { class: 'egg_frame_btn', type: 'button', title: '隐藏', onclick: debounce(() => { // 隐藏窗口 frame.show = false; }, 300), }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { 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', }))), // 改变大小 createElementNode('button', undefined, { class: 'egg_frame_btn', type: 'button', title: '缩放', onclick: debounce(() => { max.value = !max.value; }, 300), }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { 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', }))), // 关闭 createElementNode('button', undefined, { class: 'egg_frame_btn', type: 'button', title: '关闭', onclick: debounce(() => { // 关闭窗口 closeFrame(); }, 300), }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { 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', }))), ]), ]), // 窗口内容 createElementNode('div', undefined, { class: 'egg_frame_content', }, watchEffectRef(() => frame.src ? [ createElementNode('iframe', undefined, { class: 'egg_frame', src: frame.src, ref(ele) { frame.ele = ele; }, }, undefined), ] : undefined)), ], { onMounted() { // 隐藏窗口 watch(() => [ taskStatus.value, running.value, settings[SettingType.SAME_TAB], settings[SettingType.SILENT_RUN], ], () => { // 同屏任务 if (settings[SettingType.SAME_TAB] && (taskStatus.value === TaskStatusType.START || taskStatus.value === TaskStatusType.PAUSE || running.value)) { // 设置窗口显示 frame.show = !settings[SettingType.SILENT_RUN]; } }); }, }), ]; } }), { onMounted() { // 关闭窗口 watch(() => [login.value, settings[SettingType.SAME_TAB]], () => { if (login.value) { if (settings[SettingType.SAME_TAB]) { frame.exist = true; closeWin(); } else { closeFrame(); frame.exist = false; } } else { closeWin(); closeFrame(); frame.exist = false; } }); }, }); } /** * @description 登录 */ function LoginItem() { return watchEffectRef(() => { return login.value ? undefined : createElementNode('div', undefined, { class: 'egg_login_item', }, [ // 登录按钮 createElementNode('button', undefined, { type: 'button', class: 'egg_login_btn', onclick: debounce(async () => { // 开始登录 handleLogin(); }, 300), }, createTextNode('扫码登录')), // 窗口 createElementNode('div', undefined, { class: watchEffectRef(() => `egg_login_img_wrap${loginQRCodeShow.value ? ' active' : ''}`), }, createElementNode('img', undefined, { class: 'egg_login_img', })), ], { onMounted() { watch(() => settings[SettingType.SCHEDULE_RUN], () => { // 未开启定时展示二维码 if (!settings[SettingType.SCHEDULE_RUN]) { // 开始登录 handleLogin(); } }, true); }, }); }); } /** * @description 信息 * @returns */ function InfoItem() { return watchEffectRef(() => { if (login.value) { return createElementNode('div', undefined, { class: 'egg_info_item', }, [ // 用户信息 createElementNode('div', undefined, { class: 'egg_userinfo' }, [ // 头像 createElementNode('div', undefined, { class: 'egg_avatar' }, watchEffectRef(() => { return [ userinfo.avatar ? createElementNode('img', undefined, { src: userinfo.avatar, class: 'egg_avatar_img', }) : createElementNode('div', undefined, { class: 'egg_avatar_nick', }, createTextNode(watchEffectRef(() => userinfo.nick.substring(1, 3)))), ]; })), // 昵称 createElementNode('div', undefined, { class: 'egg_nick' }, createTextNode(watchEffectRef(() => userinfo.nick))), ]), // 退出按钮 createElementNode('button', undefined, { type: 'button', class: 'egg_login_btn', onclick: debounce(() => { // 退出登录 handleLogout(); }, 300), }, createTextNode('退出')), ], { onMounted() { // 刷新用户信息 refreshUserInfo(); }, }); } }); } /** * @description 分数详情 */ function ScoreItem() { return watchEffectRef(() => { if (login.value) { // 分数显示 const scoreShow = ref(false); // 分数信息 return createElementNode('div', undefined, { class: 'egg_score_item', }, createElementNode('div', undefined, { class: 'egg_scoreinfo' }, [ createElementNode('div', undefined, { class: 'egg_totalscore', }, [ createTextNode('总积分'), createElementNode('span', undefined, undefined, createTextNode(totalScore)), ]), createElementNode('div', undefined, { class: 'egg_todayscore', }, [ createElementNode('button', undefined, { type: 'button', class: 'egg_todayscore_btn', title: '查看分数详情', onclick: debounce(() => { scoreShow.value = !scoreShow.value; }, 300), onblur: () => { scoreShow.value = false; }, }, [ createTextNode('当天分数'), // 当天分数 createElementNode('span', undefined, undefined, createTextNode(todayScore)), // icon createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { 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', })), createElementNode('div', undefined, { class: watchEffectRef(() => `egg_score_details${scoreShow.value ? '' : ' hide'}`), }, [ createElementNode('div', undefined, { class: 'egg_score_title' }, [ createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, [ createNSElementNode('path', undefined, { d: 'M314.81 304.01h415.86v58.91H314.81zM314.81 440.24h415.86v58.91H314.81z', }), createNSElementNode('path', undefined, { 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', }), ]), createElementNode('div', undefined, { class: 'egg_score_title_text', }, createTextNode('积分详情')), ]), ...taskConfig.map((task) => createElementNode('div', undefined, { class: 'egg_score_item' }, [ createTextNode(task.title), createElementNode('span', undefined, { class: 'egg_score_detail', }, createTextNode(watchEffectRef(() => task.score))), ])), ]), ]), ]), ]), { onMounted() { // 刷新分数信息 refreshScoreInfo(); }, }); } }); } /** * @description 设置普通项 * @returns */ function NormalItem({ title, tip, checked, onchange, }) { return createElementNode('div', undefined, { class: 'egg_setting_item' }, [ createElementNode('div', undefined, { class: 'egg_label_wrap' }, [ createElementNode('label', undefined, { class: 'egg_task_title' }, [ createTextNode(title), createElementNode('span', undefined, { class: 'egg_detail', title: tip, }, createTextNode('i')), ]), ]), createElementNode('input', undefined, { title: tip, class: 'egg_switch', type: 'checkbox', checked, onchange, }), ]); } /** * @description 设置任务项 * @returns */ function TaskItem({ title, tip, checked, currentScore, dayMaxScore, onchange, immutable, }) { return createElementNode('div', undefined, { class: 'egg_task_item', }, [ createElementNode('div', undefined, { class: 'egg_label_wrap' }, [ createElementNode('div', undefined, { class: 'egg_task_title_wrap' }, [ createElementNode('div', undefined, { class: 'egg_task_title' }, createTextNode(title)), createElementNode('div', undefined, { class: 'egg_task_progress_wrap' }, [ createElementNode('div', undefined, { class: 'egg_task_current', }, createTextNode(currentScore)), createElementNode('div', undefined, { class: 'egg_task_max', }, createTextNode(watchEffectRef(() => `/${dayMaxScore.value}`))), ]), ]), createElementNode('div', undefined, { class: 'egg_progress' }, [ createElementNode('div', undefined, { class: 'egg_track' }, createElementNode('div', undefined, { class: 'egg_bar', style: watchEffectRef(() => `width: ${((100 * currentScore.value) / dayMaxScore.value).toFixed(1)}%;`), })), ]), ]), createElementNode('input', undefined, { title: tip, class: 'egg_switch', type: 'checkbox', checked, onchange, disabled: immutable, }), ]); } /** * @description 任务 */ function TaskList() { // 处理任务设置变化 const handleTaskChange = (e, type, title) => { // 开关 const { checked } = e.target; if (taskConfig[type].active !== checked) { taskConfig[type].active = checked; // 设置 GM_setValue('taskConfig', JSON.stringify(taskConfig)); // 创建提示 createTip(`${title} ${checked ? '打开' : '关闭'}!`); } }; // 登录加载 watch(login, async () => { if (login.value) { // 加载任务列表 await refreshTaskList(); // 未完成任务 if (taskConfig.some((task) => task.active && !task.status)) { // 全局暂停 GM_setValue('pauseStudy', false); // 加载完毕 taskStatus.value = TaskStatusType.LOADED; return; } // 任务完毕 taskStatus.value = TaskStatusType.FINISH; } }, true); return createElementNode('div', undefined, { class: 'egg_task_list', }, taskConfig.map((label) => label.immutable ? TaskItem({ title: label.title, tip: label.tip, checked: watchEffectRef(() => label.active), currentScore: watchEffectRef(() => label.currentScore), dayMaxScore: watchEffectRef(() => label.dayMaxScore), onchange: debounce((e) => { handleTaskChange(e, label.type, label.title); }, 300), immutable: label.immutable, }) : TaskItem({ title: label.title, tip: label.tip, checked: watchEffectRef(() => label.active), currentScore: watchEffectRef(() => label.currentScore), dayMaxScore: watchEffectRef(() => label.dayMaxScore), onchange: debounce((e) => { handleTaskChange(e, label.type, label.title); }, 300), immutable: label.immutable, }))); } /** * @description 任务按钮 */ function TaskBtn() { return watchEffectRef(() => { if (login.value) { /** * @description 学习 */ async function study() { // 创建提示 createTip('开始学习!'); // 暂停 await studyPauseLock(); // 文章选读 if (taskConfig[TaskType.READ].active && !taskConfig[TaskType.READ].status) { log('任务一: 文章选读'); // 创建提示 createTip('任务一: 文章选读'); // 暂停 await studyPauseLock(); // 看新闻 await readNews(); } log('任务一: 文章选读已完成!'); // 视听学习 if (taskConfig[TaskType.WATCH].active && !taskConfig[TaskType.WATCH].status) { log('任务二: 视听学习'); // 创建提示 createTip('任务二: 视听学习'); // 暂停 await studyPauseLock(); // 看视频 await watchVideo(); } log('任务二: 视听学习已完成!'); // 每日答题 if (taskConfig[TaskType.PRACTICE].active && !taskConfig[TaskType.PRACTICE].status) { log('任务三: 每日答题'); // 创建提示 createTip('任务三: 每日答题'); // 暂停 await studyPauseLock(); // 做每日答题 await doExamPractice(); } log('任务三: 每日答题已完成!'); } /** * @description 暂停任务 */ function pauseTask() { // 全局暂停 GM_setValue('pauseStudy', true); taskStatus.value = TaskStatusType.PAUSE; } /** * @description 继续任务 */ function continueTask() { // 全局暂停 GM_setValue('pauseStudy', false); taskStatus.value = TaskStatusType.START; } /** * @description 开始任务 */ async function startTask() { // 未完成任务 if (taskConfig.some((task) => task.active && !task.status)) { // 开始任务 taskStatus.value = TaskStatusType.START; try { // 学习 await study(); // 同屏任务 if (settings[SettingType.SAME_TAB]) { // 关闭窗口 closeFrame(); // 窗口不存在 frame.exist = false; } } catch (err) { if (err instanceof Error) { // 提示 createTip(err.message); // 错误 error(err.message); return; } // 提示 createTip(String(err)); // 错误 error(err); } } // 刷新任务 taskStatus.value = TaskStatusType.FINISH; log('已完成'); // 创建提示 createTip('完成学习!'); // 远程推送 if (settings[SettingType.REMOTE_PUSH]) { // 推送 const res = await pushModal({ title: '学习推送', to: userinfo.nick, content: [ '学习强国, 学习完成!', `当天积分: ${getHighlightHTML(todayScore.value)} 分`, `总积分: ${getHighlightHTML(totalScore.value)} 分`, ...taskConfig.map((task) => getProgressHTML(task.title, task.currentScore, task.dayMaxScore)), ], type: 'success', }, pushToken.value); createTip(`学习推送${res ? '成功' : '失败'}!`); } } // 已在等待 let flag = false; // 自动答题 watch(() => [taskStatus.value, settings[SettingType.AUTO_START]], async () => { // 加载完毕 if (!flag && taskStatus.value === TaskStatusType.LOADED) { // 自动答题 if (settings[SettingType.AUTO_START]) { // 等待中 flag = true; // 创建提示 const tip = createTip('即将自动开始任务', 5, true); // 等待倒计时结束 await tip.waitCountDown(); // 再次查看是否开启 if (settings[SettingType.AUTO_START] && taskStatus.value !== TaskStatusType.START) { // 创建提示 createTip('自动开始任务'); // 开始任务 startTask(); return; } // 取消等待 flag = false; // 创建提示 createTip('已取消自动开始任务!'); } } }); // 切换开关任务未完成 taskConfig.forEach((task) => { watch(() => [task.active], () => { if (taskStatus.value === TaskStatusType.FINISH) { if (task.active && !task.status) { taskStatus.value = TaskStatusType.LOADED; } } }); }); return createElementNode('div', undefined, { class: 'egg_study_item' }, createElementNode('button', undefined, { class: watchEffectRef(() => `egg_study_btn${taskStatus.value === TaskStatusType.START ? ' loading' : ''}`), type: 'button', disabled: watchRef(() => [running.value, taskStatus.value], () => running.value || taskStatus.value === TaskStatusType.LOADING || taskStatus.value === TaskStatusType.FINISH), onclick: watchEffectRef(() => taskStatus.value === TaskStatusType.LOADED ? debounce(startTask, 300) : taskStatus.value === TaskStatusType.START ? debounce(pauseTask, 300) : taskStatus.value === TaskStatusType.PAUSE ? debounce(continueTask, 300) : undefined), }, createTextNode(watchEffectRef(() => `${taskStatus.value === TaskStatusType.LOADING ? '等待中' : taskStatus.value === TaskStatusType.LOADED ? '开始学习' : taskStatus.value === TaskStatusType.START ? '正在学习, 点击暂停' : taskStatus.value === TaskStatusType.PAUSE ? '继续学习' : taskStatus.value === TaskStatusType.FINISH ? '已完成' : ''}`)))); } }); } /** * @description 定时项目 * @returns */ function ScheduleList() { return createElementNode('div', undefined, { class: 'egg_schedule_list' }, watchEffectRef(() => { return scheduleList.length ? scheduleList.map((schedule) => createElementNode('div', undefined, { class: 'egg_schedule_item' }, [ createElementNode('div', undefined, { class: `egg_schedule_detail_time_wrap${isLate(schedule) ? ' inactive' : ''}`, }, [ createElementNode('div', undefined, { class: 'egg_schedule_detail_icon', }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, [ createNSElementNode('path', undefined, { 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', }), createNSElementNode('path', undefined, { 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', }), ])), createElementNode('div', undefined, { class: 'egg_schedule_detail_time' }, createTextNode(schedule.time)), ]), createElementNode('div', undefined, { class: 'egg_schedule_detail_del_wrap' }, [ createElementNode('button', undefined, { class: 'egg_schedule_del_btn', onclick: debounce(() => { // 定时刷新 if (!settings[SettingType.SCHEDULE_RUN]) { createTip('未开启定时刷新!'); return; } // 索引 const index = scheduleList.findIndex((s) => s === schedule); // 删除元素 scheduleList.splice(index, 1); // 存储 GM_setValue('scheduleList', JSON.stringify(scheduleList)); // 刷新任务 refreshScheduleTask(); }, 300), }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, [ createNSElementNode('path', undefined, { 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', }), createNSElementNode('path', undefined, { 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', }), ])), ]), ])) : [ createElementNode('div', undefined, { class: 'egg_schedule_list_none' }, [ createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, [ createNSElementNode('path', undefined, { 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', }), createNSElementNode('path', undefined, { 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', }), createNSElementNode('path', undefined, { 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', }), ]), createElementNode('div', undefined, { class: 'egg_schedule_list_none_text', }, createTextNode('暂无定时任务')), ]), ]; })); } /** * @description 时间输入 * @returns */ function TimeInput({ hour, minute, onchange, }) { // 小时 const hours = new Array(24).fill(undefined).map((v, i) => ({ value: i, label: formatDateNum(i), })); // 分钟 const minutes = new Array(60).fill(undefined).map((v, i) => ({ value: i, label: formatDateNum(i), })); const valueRef = watchEffectRef(() => { const h = hours.find((h) => h.value === hour.value); const min = minutes.find((min) => min.value === minute.value); return { hour: h ? h.value : -1, minute: min ? min.value : -1, }; }); return createElementNode('div', undefined, { class: 'egg_time_input' }, [ createElementNode('div', undefined, { class: 'egg_hour_wrap' }, [ Select({ data: hours, placeholder: '00', maxlength: 2, value: hour, onchange({ value }) { valueRef.value.hour = value; onchange && onchange(valueRef.value); }, onblur(res) { if (!res) { valueRef.value.hour = -1; onchange && onchange(valueRef.value); } }, }), ]), createElementNode('span', undefined, { class: 'egg_separator' }, createTextNode(':')), createElementNode('div', undefined, { class: 'egg_minute_wrap' }, [ Select({ data: minutes, placeholder: '00', maxlength: 2, value: minute, onchange({ value }) { valueRef.value.minute = value; onchange && onchange(valueRef.value); }, onblur(res) { if (!res) { valueRef.value.minute = -1; onchange && onchange(valueRef.value); } }, }), ]), ]); } /** * @description 设置面板组件 * @returns */ function SettingsPanel({ show }) { // token let token = ''; // 小时 let hour = ref(-1); // 分钟 let minute = ref(-1); return createElementNode('div', undefined, { class: watchEffectRef(() => `egg_settings${show.value ? ' active' : ''}`), }, [ createElementNode('div', undefined, { class: 'egg_settings_version_wrap' }, [ createElementNode('div', undefined, { class: 'egg_settings_label' }, createTextNode('版本信息')), createElementNode('div', undefined, { class: 'egg_settings_version', }, [ createTextNode(`v${version}`), createElementNode('a', undefined, { class: 'egg_settings_version_detail', title: 'GitHub Xu22Web/tech-study-js', href: 'https://github.com/Xu22Web/tech-study-js', }, createNSElementNode('svg', undefined, { viewBox: '0 0 16 16', class: 'egg_icon', }, createNSElementNode('path', undefined, { d: 'M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z', }))), ]), ]), createElementNode('div', undefined, { class: 'egg_settings_theme_wrap' }, [ createElementNode('div', undefined, { class: 'egg_settings_label' }, createTextNode('主题预设')), createElementNode('div', undefined, { class: 'egg_settings_theme_colors' }, [ { value: '#fa3333', title: '强国红', detail: 'XueXi Red', code: 'none', }, { value: '#bb2649', title: '非凡洋红', detail: 'Viva Magenta', code: '18-1750', }, { value: '#35548a', title: '经典蓝', detail: 'Classic Blue', code: '19-4052', }, { value: '#f36f63', title: '活珊瑚橘', detail: 'Living Coral', code: '16-1546', }, { value: '#6d5b97', title: '紫外光色', detail: 'Ultra Violet', code: '18-3838', }, { value: '#86af49', title: '草木绿', detail: 'Greenery', code: '15-0343', }, { value: '#fc8bab', title: 'B站粉', detail: 'Bilibili Pink', code: 'none', }, { value: '#056de8', title: '知乎蓝', detail: 'Zhihu Blue', code: 'none', }, ].map((color) => createElementNode('div', undefined, { class: 'egg_settings_theme_color_wrap', }, createElementNode('button', undefined, { class: 'egg_settings_theme_color', type: 'button', style: watchEffectRef(() => `color: ${color.value};${themeColor.value === color.value ? '' : ` box-shadow: 0rem 0.4rem 0.1rem 0.1rem ${color.value}30;`}`), title: color.title, onclick: debounce(() => { if (themeColor.value !== color.value) { themeColor.value = color.value; // 存储 GM_setValue('themeColor', themeColor.value); } }, 300), })))), ]), createElementNode('div', undefined, { class: 'egg_settings_read_time_wrap', }, [ createElementNode('div', undefined, { class: 'egg_settings_label' }, createTextNode('最大文章时长')), Select({ data: [ { label: '40s', value: 40, }, { label: '60s', value: 60, }, { label: '80s', value: 80, }, { label: '100s', value: 100, }, ], value: maxRead, placeholder: '100s', maxlength: 4, keep: true, onchange({ value }) { // 创建提示 createTip('最大文章时长 已保存!'); maxRead.value = value; // 存储 GM_setValue('maxRead', value); }, }), ], { onMounted() { try { const maxReadTemp = GM_getValue('maxRead'); if (maxReadTemp) { maxRead.value = maxReadTemp; } } catch (e) { } }, }), createElementNode('div', undefined, { class: 'egg_settings_watch_time_wrap', }, [ createElementNode('div', undefined, { class: 'egg_settings_label' }, createTextNode('最大视听时长')), Select({ data: [ { label: '40s', value: 40, }, { label: '60s', value: 60, }, { label: '80s', value: 80, }, { label: '100s', value: 100, }, { label: '120s', value: 120, }, ], value: maxWatch, placeholder: '120s', maxlength: 4, keep: true, onchange({ value }) { // 创建提示 createTip('最大视听时长 已保存!'); maxWatch.value = value; // 存储 GM_setValue('maxWatch', value); }, }), ], { onMounted() { try { const maxWatchTemp = GM_getValue('maxWatch'); if (maxWatchTemp) { maxWatch.value = maxWatchTemp; } } catch (e) { } }, }), watchEffectRef(() => settings[SettingType.REMOTE_PUSH] ? createElementNode('div', undefined, { class: 'egg_settings_token_wrap' }, [ createElementNode('div', undefined, { class: 'egg_settings_token' }, [ createElementNode('div', undefined, { class: 'egg_settings_label' }, createTextNode('我的 token')), createElementNode('input', undefined, { class: 'egg_settings_token_input', placeholder: '用户 token', maxlength: 32, value: pushToken.value, onfocus: (e) => { const input = e.target; input.classList.add('active'); const btnWrap = $$('.egg_settings_submit_btn_wrap')[0]; btnWrap.classList.add('active'); }, onblur: (e) => { const input = e.target; // 去除空格 const value = input.value.trim(); if (/^[0-9a-z]{32}$/.test(value)) { token = value; input.value = value; } else { token = ''; } input.classList.remove('active'); setTimeout(() => { const btnWrap = $$('.egg_settings_submit_btn_wrap')[0]; btnWrap.classList.remove('active'); input.value = pushToken.value; }, 200); }, }), ]), createElementNode('div', undefined, { class: 'egg_settings_submit_btn_wrap' }, createElementNode('button', undefined, { class: 'egg_settings_submit_btn', onclick: debounce(() => { // 创建提示 createTip('用户 token 已保存!'); if (token !== pushToken.value) { pushToken.value = token; // 存储 GM_setValue('pushToken', token); } }, 300), }, createTextNode('保存'))), ]) : undefined), watchEffectRef(() => settings[SettingType.SCHEDULE_RUN] ? createElementNode('div', undefined, { class: 'egg_schedule' }, [ createElementNode('div', undefined, { class: 'egg_schedule_time_wrap' }, [ createElementNode('div', undefined, { class: 'egg_schedule_time' }, [ createElementNode('div', undefined, { class: 'egg_schedule_label' }, createTextNode('设置时间')), createElementNode('div', undefined, { class: 'egg_schedule_time_input_wrap' }, [ TimeInput({ hour, minute, onchange({ hour: h, minute: min }) { hour.value = h; minute.value = min; }, }), createElementNode('button', undefined, { class: 'egg_schedule_add_btn', onclick: debounce(() => { // 定时刷新 if (!settings[SettingType.SCHEDULE_RUN]) { createTip('未开启定时刷新!'); return; } if (hour.value === -1 || minute.value === -1) { createTip('时间格式不符合要求!'); return; } // 重复定时存在 const exists = scheduleList.find((schedule) => schedule.hour === hour.value && schedule.minute === minute.value); if (exists) { createTip('设置定时任务重复!'); return; } createTip('设置定时任务成功!'); // 添加 scheduleList.push({ hour: hour.value, minute: minute.value, time: `${formatDateNum(hour.value)}:${formatDateNum(minute.value)}`, }); // 排序 scheduleList.sort((a, b) => a.hour === b.hour ? a.minute - b.minute : a.hour - b.hour); // 存储 GM_setValue('scheduleList', JSON.stringify(scheduleList)); // 清空 hour.value = -1; minute.value = -1; const inputs = $$('.egg_time_input input'); inputs.forEach((i) => (i.value = '')); // 刷新任务 refreshScheduleTask(); }, 300), }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { d: 'M801.171 483.589H544V226.418c0-17.673-14.327-32-32-32s-32 14.327-32 32v257.171H222.83c-17.673 0-32 14.327-32 32s14.327 32 32 32H480v257.17c0 17.673 14.327 32 32 32s32-14.327 32-32v-257.17h257.171c17.673 0 32-14.327 32-32s-14.327-32-32-32z', }))), ]), ]), ]), ScheduleList(), ]) : undefined), ], { onMounted() { // 刷新token watch(() => settings[SettingType.REMOTE_PUSH], () => { // 远程推送 if (settings[SettingType.REMOTE_PUSH]) { try { const tokenTemp = GM_getValue('pushToken'); if (tokenTemp) { pushToken.value = tokenTemp; } } catch (e) { } } }, true); // 刷新定时任务 watch(() => settings[SettingType.SCHEDULE_RUN], () => { // 定时任务打开 if (settings[SettingType.SCHEDULE_RUN]) { try { const scheduleTemp = JSON.parse(GM_getValue('scheduleList')); if (scheduleTemp && Array.isArray(scheduleTemp)) { for (const i in scheduleTemp) { scheduleList[i] = scheduleTemp[i]; } } } catch (e) { } // 刷新定时任务 refreshScheduleTask(); return; } // 清除任务 clearScheduleTask(); }, true); }, }); } /** * @description 面板 * @returns */ function Panel() { // 运行设置标签 const runLabels = [ { title: '自动开始', tip: '启动时, 自动开始任务, 在倒计时结束前自动开始可随时取消; 如果在自动开始前手动开始任务, 此次自动开始将取消', type: SettingType.AUTO_START, }, { title: '同屏任务', tip: '运行任务时,所有任务均在当前页面以弹窗方式运行', type: SettingType.SAME_TAB, }, { title: '静默运行', tip: '同屏任务时, 不显示任务弹窗静默运行', type: SettingType.SILENT_RUN, }, { title: '定时刷新', tip: '定时刷新页面,重新进行任务,此功能需要长时间占用浏览器', type: SettingType.SCHEDULE_RUN, }, { title: '视频静音', tip: '视听学习时,静音播放视频', type: SettingType.VIDEO_MUTED, }, ]; // 运行设置标签 const examLabels = [ { title: '随机作答', tip: '无答案时, 随机选择或者填入答案, 不保证正确', type: SettingType.RANDOM_EXAM, }, { title: '自动答题', tip: '进入答题页面时,自动答题并提交答案', type: SettingType.AUTO_ANSWER, }, ]; // 推送设置标签 const pushLabels = [ { title: '远程推送', tip: '利用 pushplus 推送, 将登录二维码直接推送到微信公众号', type: SettingType.REMOTE_PUSH, }, ]; // 处理设置变化 const handleSettingsChange = (e, type, title) => { // 开关 const { checked } = e.target; if (settings[type] !== checked) { settings[type] = checked; // 设置 GM_setValue('studySettings', JSON.stringify(settings)); // 创建提示 createTip(`${title} ${checked ? '打开' : '关闭'}!`); } }; // 任务显示 const scheduleShow = ref(false); // 面板显示 const panelShow = ref(false); return createElementNode('div', undefined, { class: `egg_panel_wrap${hasMobile() ? ' mobile' : ''}`, onclick(e) { e.stopPropagation(); }, onmousedown(e) { e.stopPropagation(); }, onmousemove(e) { e.stopPropagation(); }, onmouseup(e) { e.stopPropagation(); }, onmouseenter(e) { e.stopPropagation(); }, onmouseleave(e) { e.stopPropagation(); }, onmouseover(e) { e.stopPropagation(); }, ontouchstart(e) { e.stopPropagation(); }, ontouchmove(e) { e.stopPropagation(); }, ontouchend(e) { e.stopPropagation(); }, oninput(e) { e.stopPropagation(); }, onchange(e) { e.stopPropagation(); }, onblur(e) { e.stopPropagation(); }, }, createElementNode('div', undefined, { class: watchEffectRef(() => `egg_panel${panelShow.value ? ' hide' : ''}`), }, [ // 登录 LoginItem(), // 信息 InfoItem(), // 分数 ScoreItem(), // 任务部分 Hr({ text: '任务' }), TaskList(), // 运行部分 Hr({ text: '运行' }), createElementNode('div', undefined, { class: 'egg_run_list' }, runLabels.map((label) => { return NormalItem({ title: label.title, tip: label.tip, checked: settings[label.type], onchange: debounce((e) => { handleSettingsChange(e, label.type, label.title); }, 300), }); })), // 答题部分 Hr({ text: '答题' }), createElementNode('div', undefined, { class: 'egg_exam_list' }, examLabels.map((label) => { return NormalItem({ title: label.title, tip: label.tip, checked: settings[label.type], onchange: debounce((e) => { handleSettingsChange(e, label.type, label.title); }, 300), }); })), // 推送部分 Hr({ text: '推送' }), createElementNode('div', undefined, { class: 'egg_push_list' }, pushLabels.map((label) => { return NormalItem({ title: label.title, tip: label.tip, checked: settings[label.type], onchange: debounce((e) => { handleSettingsChange(e, label.type, label.title); }, 300), }); })), // 提示部分 Hr({ text: '提示' }), createElementNode('div', undefined, { class: 'egg_tip_list' }, watchRef(login, () => login.value ? [ createTextNode('专项练习已被移除, 如需使用, 请点击'), createElementNode('button', undefined, { class: 'egg_tip_btn', type: 'button', onclick: debounce(doExamPaper, 300), disabled: watchRef(() => [running.value, taskStatus.value], () => running.value || taskStatus.value === TaskStatusType.START || taskStatus.value === TaskStatusType.PAUSE), }, createTextNode('去完成')), ] : [ createElementNode('div', undefined, { class: 'egg_tip_content' }, createTextNode('请先登录!')), ])), // 按钮集合 createElementNode('div', undefined, { class: 'egg_btns_wrap', }, [ createElementNode('button', undefined, { class: watchRef(() => [frame.exist, frame.show], () => `egg_frame_show_btn${!frame.exist || frame.show ? ' hide' : ''}`), title: '窗口', type: 'button', onclick: debounce(() => { // 窗口显示 frame.show = true; }, 300), }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { 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', }))), createElementNode('button', undefined, { class: 'egg_panel_show_btn', title: '面板', type: 'button', onclick: debounce(() => { panelShow.value = !panelShow.value; }, 300), }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { 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', }))), createElementNode('button', undefined, { class: watchEffectRef(() => `egg_settings_show_btn${scheduleShow.value ? ' active' : ''}`), title: '设置', type: 'button', onclick: debounce(() => { scheduleShow.value = !scheduleShow.value; }, 300), }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, [ createNSElementNode('path', undefined, { 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', }), createNSElementNode('path', undefined, { 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', }), ])), createElementNode('button', undefined, { class: 'egg_settings_reset_btn', title: '重置', type: 'button', onclick: debounce(() => { // 任务配置 GM_setValue('taskConfig', null); // 设置 GM_setValue('studySettings', null); // 最大阅读 GM_setValue('maxRead', null); // 最大观看 GM_setValue('maxWatch', null); // 主题色 GM_setValue('themeColor', null); // 刷新页面 location.reload(); }, 300), }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, [ createNSElementNode('path', undefined, { 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', }), ])), ]), // 任务按钮 TaskBtn(), createElementNode('div', undefined, { class: 'egg_settings_item' }, [ SettingsPanel({ show: scheduleShow }), ]), ])); } ================================================ FILE: src/index.ts ================================================ import {} from './api/answer'; import {} from './api/data'; import {} from './api/login'; import {} from './api/push'; import {} from './api/user'; import {} from './config/task'; import URL_CONFIG from './config/url'; import {} from './config/api'; import { version } from './config/version'; import { Settings } from './types'; import { watch } from './utils/composition'; import { $$, $_, createElementNode, mountElement } from './utils/element'; import { error, log } from './utils/log'; import {} from './utils/push'; import {} from './utils/random'; import {} from './utils/time'; import { hasMobile, load } from './utils/utils'; import { maxRead, maxWatch, settings, taskConfig, themeColor } from './shared'; import { doingExam, ExamType } from './controller/exam'; import { initChildListener, initMainListener } from './controller/frame'; import {} from './controller/login'; import { handleNews, handleVideo } from './controller/readAndWatch'; import {} from './controller/schedule'; import { createTip } from './controller/tip'; import {} from './controller/user'; import {} from './component/Tip'; import {} from './component/Hr'; import {} from './component/Select'; import { ExamBtn } from './component/ExamBtn'; import { Frame } from './component/Frame'; import {} from './component/LoginItem'; import {} from './component/InfoItem'; import {} from './component/ScoreItem'; import {} from './component/NoramlItem'; import {} from './component/TaskItem'; import {} from './component/TaskList'; import {} from './component/TaskBtn'; import {} from './component/ScheduleList'; import {} from './component/TimeInput'; import {} from './component/SettingsPanel'; import { Panel } from './component/Panel'; import css from './css/index.css?raw'; /** * @description 嵌入样式 */ GM_addStyle(css); load( (href) => href.match(URL_CONFIG.home), () => { // 初始化logo initLogo(); // 页面提示 log('进入主页面!'); // 初始化主题 initThemeColor(); // 初始化任务配置 initTaskConfig(); // 初始化设置 initSettings(); // 设置字体 initFontSize(); // 初始化主页面 initMainListener(); // 初始化提示 renderTip(); // 渲染面板 renderPanel(); // 渲染窗口 renderFrame(); } ); load( (href) => href === GM_getValue('readingUrl'), async () => { // 页面提示 log('进入文章选读页面!'); // 初始化主题 initThemeColor(); // 初始化设置 initSettings(); // 设置字体 initFontSize(); // 最大阅读 initMaxRead(); // 初始化子页面 initChildListener(); // 初始化提示 renderTip(); try { // 处理文章 await handleNews(); } catch (err: unknown) { if (err instanceof Error) { // 提示 createTip(err.message); // 错误 error(err.message); return; } // 提示 createTip(String(err)); // 错误 error(err); } } ); load( (href) => href === GM_getValue('watchingUrl'), async () => { // 页面提示 log('进入视听学习页面!'); // 初始化主题 initThemeColor(); // 初始化设置 initSettings(); // 设置字体 initFontSize(); // 最大视听 initMaxWatch(); // 初始化子页面 initChildListener(); // 初始化提示 renderTip(); try { // 处理视频 await handleVideo(); } catch (err: unknown) { if (err instanceof Error) { // 提示 createTip(err.message); // 错误 error(err.message); return; } // 提示 createTip(String(err)); // 错误 error(err); } } ); load( (href) => href === URL_CONFIG.examPractice, async () => { // 页面提示 log('进入每日答题页面!'); // 初始化主题 initThemeColor(); // 初始化设置 initSettings(); // 设置字体 initFontSize(); // 初始化子页面 initChildListener(); // 初始化提示 renderTip(); // 创建答题按钮 await renderExamBtn(); try { // 开始答题 await doingExam(ExamType.PRACTICE); } catch (err: unknown) { if (err instanceof Error) { // 提示 createTip(err.message); // 错误 error(err.message); return; } // 提示 createTip(String(err)); // 错误 error(err); } } ); load( (href) => href.includes(URL_CONFIG.examPaper), async () => { // 页面提示 log('进入专项练习页面!'); // 初始化主题 initThemeColor(); // 初始化设置 initSettings(); // 设置字体 initFontSize(); // 初始化子页面 initChildListener(); // 初始化提示 renderTip(); // 创建答题按钮 await renderExamBtn(); // 开始答题 doingExam(ExamType.PAPER); return; } ); /** * @description 初始化logo */ function initLogo() { console.log( `%c tech-study.js %c ${version} `, 'background:dodgerblue;color:white;font-size:15px;border-radius:4px 0 0 4px;padding:2px 0;', 'background:black;color:gold;font-size:15px;border-radius:0 4px 4px 0;padding:2px 0;' ); } /** * @description 初始化配置 */ function initTaskConfig() { try { const taskTemp = JSON.parse(GM_getValue('taskConfig')); if (taskTemp && Array.isArray(taskTemp)) { if (taskTemp.length === taskConfig.length) { taskConfig.forEach((task, i) => { task.active = taskTemp[i].active; }); } } // 监听值变化 GM_addValueChangeListener('taskConfig', (key, oldVal, newVal, remote) => { if (remote) { const taskTemp = JSON.parse(newVal); if (taskTemp && Array.isArray(taskTemp)) { if (taskTemp.length === taskConfig.length) { taskConfig.forEach((task, i) => { task.active = taskTemp[i].active; }); } } } }); } catch (e) {} } /** * @description 初始化配置 */ function initSettings() { try { const settingsTemp = JSON.parse(GM_getValue('studySettings')); if (settingsTemp && Array.isArray(settingsTemp)) { if (settingsTemp.length === settings.length) { for (const i in settingsTemp) { settings[i] = (settingsTemp)[i]; } } } // 监听值变化 GM_addValueChangeListener( 'studySettings', (key, oldVal, newVal, remote) => { if (remote) { const settingsTemp = JSON.parse(newVal); if (settingsTemp && Array.isArray(settingsTemp)) { if (settingsTemp.length === settings.length) { for (const i in settingsTemp) { settings[i] = (settingsTemp)[i]; } } } } } ); } catch (e) {} } /** * @description 初始化配置 */ function initFontSize() { // 移动端 const moblie = hasMobile(); if (moblie) { // 清除缩放 const meta = $$('meta[name=viewport]')[0]; if (meta) { meta.content = 'initial-scale=0, user-scalable=yes'; } // 缩放比例 const scale = ~~(window.innerWidth / window.outerWidth) || 1; document.documentElement.style.setProperty('--scale', String(scale)); } } /** * @description 初始化最大阅读时长 */ function initMaxRead() { try { const maxReadTemp = GM_getValue('maxRead'); if (maxReadTemp) { maxRead.value = maxReadTemp; } } catch (error) {} } /** * @description 初始化最大视听时长 */ function initMaxWatch() { try { const maxWatchTemp = GM_getValue('maxWatch'); if (maxWatchTemp) { maxWatch.value = maxWatchTemp; } } catch (error) {} } /** * @description 初始化主题色 */ function initThemeColor() { try { // 监听主题变化 watch(themeColor, () => { // 设置主题 document.documentElement.style.setProperty( '--themeColor', themeColor.value ); }); // 主题色 const themeColorTemp = GM_getValue('themeColor'); if (themeColorTemp) { themeColor.value = themeColorTemp; } // 监听值变化 GM_addValueChangeListener('themeColor', (key, oldVal, newVal, remote) => { if (remote) { // 主题色 const themeColorTemp = newVal; if (themeColorTemp) { themeColor.value = themeColorTemp; } } }); } catch (error) {} } /** * @description 渲染提示 */ function renderTip() { const tipWrap = createElementNode('div', undefined, { class: 'egg_tip_wrap', onclick(e: Event) { e.stopPropagation(); }, onmousedown(e: Event) { e.stopPropagation(); }, onmousemove(e: Event) { e.stopPropagation(); }, onmouseup(e: Event) { e.stopPropagation(); }, onmouseenter(e: Event) { e.stopPropagation(); }, onmouseleave(e: Event) { e.stopPropagation(); }, onmouseover(e: Event) { e.stopPropagation(); }, ontouchstart(e: Event) { e.stopPropagation(); }, ontouchmove(e: Event) { e.stopPropagation(); }, ontouchend(e: Event) { e.stopPropagation(); }, oninput(e: Event) { e.stopPropagation(); }, onchange(e: Event) { e.stopPropagation(); }, onblur(e: Event) { e.stopPropagation(); }, }); mountElement(tipWrap); } /** * @description 渲染答题按钮 */ async function renderExamBtn() { const titles = await $_('.title'); if (titles.length) { // 插入节点 titles[0].parentNode?.insertBefore(ExamBtn().ele, titles[0].nextSibling); } } /** * @description 渲染面板 * @returns */ async function renderPanel() { // 面板 const panel = Panel(); // 插入节点 mountElement(panel); } /** * @description 渲染窗口 */ function renderFrame() { // 窗口 const frame = Frame(); // 插入节点 mountElement(frame); } ================================================ FILE: src/shared/index.ts ================================================ /* 变量 */ import { Schedule, Settings, TaskStatusType, TaskType } from '../types'; import { reactive, ref, shallowReactive } from '../utils/composition'; import { getCookie } from '../utils/utils'; /** * @description 链接 */ const href = window.location.href; /** * @description 任务配置 */ const taskConfig = reactive([ { title: '登录', currentScore: 0, dayMaxScore: 0, need: 0, status: false, tip: '每日首次登录积1分。', score: 0, active: true, immutable: true, type: TaskType.LOGIN, }, { title: '文章选读', currentScore: 0, dayMaxScore: 0, need: 0, status: false, tip: '每有效阅读一篇文章积1分,上限6分。有效阅读文章累计1分钟积1分,上限6分。每日上限积12分。', score: 0, active: true, immutable: false, type: TaskType.READ, }, { title: '视听学习', currentScore: 0, dayMaxScore: 0, need: 0, status: false, tip: '每有效一个音频或观看一个视频积1分,上限6分。有效收听音频或观看视频累计1分钟积1分,上限6分。每日上限积12分。', score: 0, active: true, immutable: false, type: TaskType.WATCH, }, { title: '每日答题', currentScore: 0, dayMaxScore: 0, need: 0, status: false, tip: '每组答题每答对1道积1分。每日上限积5分。', score: 0, active: true, immutable: false, type: TaskType.PRACTICE, }, ]); /** * @description 设置 */ const settings = reactive([ false, false, false, false, false, false, false, false, ]); /** * @description 总分 */ const totalScore = ref(0); /** * @description 当天分数 */ const todayScore = ref(0); /** * @description 用户信息 */ const userinfo = reactive({ nick: '', avatar: '', }); /** * @description 进度 */ const taskStatus = ref(TaskStatusType.LOADING); /** * @description 答题暂停 */ const examPause = ref(false); /** * @description 登录 */ const login = ref(!!getCookie('token')); /** * @description 窗口id */ const id = ref(''); /** * @description 定时刷新列表 */ const scheduleList = shallowReactive([]); /** * @description 推送token */ const pushToken = ref(''); /** * @description 刷新次数 */ const refreshCount = ref(0); /** * @description 窗口关闭 */ const frame = reactive<{ title: string; show: boolean; exist: boolean; closed: boolean; ele: HTMLIFrameElement | undefined; src: string; }>({ title: '', show: false, exist: false, closed: true, ele: undefined, src: '', }); /** * @description 页面 */ const page = ref(undefined); /** * @description 开始登录 */ const loginQRCodeShow = ref(false); /** * @description 最大选读时长 */ const maxRead = ref(100); /** * @description 最大视听时长 */ const maxWatch = ref(120); /** * @description 运行其他任务 */ const running = ref(false); /** * @description 主题色 */ const themeColor = ref('#fa3333'); export { href, taskConfig, settings, todayScore, totalScore, userinfo, taskStatus, examPause, login, id, scheduleList, pushToken, refreshCount, loginQRCodeShow, frame, page, maxRead, maxWatch, running, themeColor, }; ================================================ FILE: src/types/index.ts ================================================ /** * @description 任务类型 */ enum TaskType { LOGIN, READ, WATCH, PRACTICE, } /** * @description 设置类型 */ enum SettingType { AUTO_START, SAME_TAB, SILENT_RUN, SCHEDULE_RUN, VIDEO_MUTED, RANDOM_EXAM, AUTO_ANSWER, REMOTE_PUSH, } /** * @description 设置 */ type Settings = [ boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean ]; /** * @description 定时信息 */ type Schedule = { time: string; hour: number; minute: number; }; /** * @description 进度类型 */ enum TaskStatusType { LOADING, LOADED, START, PAUSE, FINISH, } /** * @description 文章视听列表 */ type NewsVideoList = { publishTime: string; title: string; type: string; url: string; showSource: string; dataValid: boolean; itemType: string; }[]; export { TaskType, SettingType, Settings, Schedule, TaskStatusType, NewsVideoList, }; ================================================ FILE: src/utils/composition.ts ================================================ // 当前订阅 let currentSub: ((newVal?: any, oldVal?: any) => any) | undefined; // 订阅 const subscription = new WeakMap< object, Map any>> >(); /** * @description Proxy Map */ const proxyMap = new WeakMap(); /** * @description 收集 Ref 依赖 * @param target * @param key */ const trackRef = (target: object) => { // 当前订阅 if (!currentSub) { return; } // target 订阅列表 let subList = subscription.get(target); // 不存在订阅列表 if (!subList) { subList = new Map(); // 键订阅 const subkeyList = new Set<(newVal: any, oldVal: any) => any>(); // 添加订阅 subkeyList.add(currentSub); subList.set('value', subkeyList); subscription.set(target, subList); return; } // 键订阅 let subkeyList = subList.get('value'); if (!subkeyList) { // 键订阅 subkeyList = new Set<(newVal: any, oldVal: any) => any>(); // 添加订阅 subkeyList.add(currentSub); subList.set('value', subkeyList); subscription.set(target, subList); return; } // 添加订阅 subkeyList.add(currentSub); }; /** * @description 通知 Ref 订阅 * @param terget * @param key * @returns */ function triggerRef(target: object, newVal: any, oldVal: any) { // target 订阅列表 const subList = subscription.get(target); if (!subList) { return; } // 键订阅 let subkeyList = subList.get('value'); if (!subkeyList) { return; } // 通知订阅 for (const fn of subkeyList) { if (fn instanceof Function) { fn(newVal, oldVal); } } } /** * @description 收集依赖 * @param target * @param key */ const track = (target: object, key: string) => { // 当前订阅 if (!currentSub) { return; } // proxy const proxyTarget = proxyMap.get(target); if (!proxyTarget) { return; } // target 订阅列表 let subList = subscription.get(target); // 不存在订阅列表 if (!subList) { subList = new Map(); // 键订阅 const subkeyList = new Set<(newVal: any, oldVal: any) => any>(); // 添加订阅 subkeyList.add(currentSub); subList.set(key, subkeyList); subscription.set(target, subList); return; } // 键订阅 let subkeyList = subList.get(key); if (!subkeyList) { // 键订阅 subkeyList = new Set<(newVal: any, oldVal: any) => any>(); // 添加订阅 subkeyList.add(currentSub); subList.set(key, subkeyList); subscription.set(target, subList); return; } // 添加订阅 subkeyList.add(currentSub); }; /** * @description 通知订阅 * @param terget * @param key * @returns */ function trigger(target: object, key: string, newVal: any, oldVal: any) { // proxy const proxyTarget = proxyMap.get(target); if (!proxyTarget) { return; } // proxyTarget 订阅列表 const subList = subscription.get(target); if (!subList) { return; } // 键订阅 let subkeyList = subList.get(key); if (!subkeyList) { return; } // 通知订阅 for (const fn of subkeyList) { fn(newVal, oldVal); } } /** * @description 只读键 */ enum ReactiveFlags { IS_REF = '_isRef', IS_SHALLOW = '_isShallow', IS_REACTIVE = '_isReactive', IS_READONLY = '_isReadonly', } /** * @description Ref */ class Ref { readonly _isShallow: boolean = false; readonly _isRef: boolean = true; _value: T; value: T; constructor(val: T, shallow: boolean = false) { const _this = this; this._isShallow = shallow; if (val && typeof val === 'object' && shallow) { const reactiveVal = reactive(val); this._value = reactiveVal; this.value = reactiveVal; } else { this._value = val; this.value = val; } // 定义属性 Object.defineProperty(this, 'value', { get() { // 收集依赖 trackRef(this); return _this._value; }, set(newVal: any) { // 旧数据 const oldVal = this._value; // 数据变化 if (oldVal !== newVal) { // 设置新数据值 _this._value = newVal; // 通知依赖 triggerRef(this, newVal, oldVal); } }, }); } toJSON() { return this._value; } } /** * @description 脱除 ref */ type UnwrapRef = T extends Ref ? P : T; /** * @description 数组脱除 ref */ type UnwrapRefArray = T extends Ref[] ? K[] : T extends [infer K, ...infer P] ? P extends Ref[] ? [UnwrapRef, ...UnwrapRefArray

] : [UnwrapRef] : []; /** * @description ref * @param v * @returns */ const isRef = (v: Ref | unknown): v is Ref => { return !!(v && v[ReactiveFlags.IS_REF]); }; /** * @description 浅层 shallow * @param v * @returns */ const isShallow = (v: unknown) => { return !!(v && v[ReactiveFlags.IS_SHALLOW]); }; /** * @description 创建 ref * @param v * @returns */ const createRef = (rawVal: T, shallow: boolean) => { return new Ref(rawVal, shallow); }; /** * @description 解除 ref * @param val * @returns */ const unref = (val: T) => { return >(isRef(val) ? val.value : val); }; /** * @description 顶层 ref * @param v * @returns */ const ref = (value: T): Ref> => { return isRef>(value) ? value : createRef(>value, true); }; /** * @description ref * @param value * @returns */ const shallowRef = (value: T): Ref> => { return isRef>(value) ? value : createRef(>value, false); }; /** * @description 创建处理 reactive * @param isReadonly * @param isShallow * @returns */ const createReactiveHandlers = (isReadonly: boolean, isShallow: boolean) => { return { get: createGetters(isReadonly, isShallow), set: createSetters(isReadonly, isShallow), }; }; /** * @description getters * @param isReadonly * @param isShallow * @returns */ const createGetters = (isReadonly: boolean, isShallow: boolean) => { return function get(target, key, receiver) { if (key === ReactiveFlags.IS_REACTIVE) { return !isReadonly; } if (key === ReactiveFlags.IS_READONLY) { return isReadonly; } if (key === ReactiveFlags.IS_SHALLOW) { return isShallow; } // 结果 const res = Reflect.get(target, key, receiver); if (!isReadonly) { // 收集依赖 track(target, key); } if (isShallow) { return res; } if (isRef(res)) { return res.value; } if (res && typeof res === 'object') { if (res instanceof Element) { return res; } return isReadonly ? readonly(res) : reactive(res); } return res; }; }; /** * @description setters * @param readonly * @param shallow * @returns */ const createSetters = (readonly: boolean, shallow: boolean) => { return function set(target, key, newVal, receiver) { // 只读 if (readonly) { return false; } // 旧值 const oldVal = target[key]; if (isReadonly(oldVal) && isRef(oldVal) && !isRef(newVal)) { return false; } if (!shallow) { if (isRef(oldVal) && !isRef(newVal)) { oldVal.value = newVal; return true; } } const res = Reflect.set(target, key, newVal, receiver); // length if (Array.isArray(target) && key === 'length') { // 通知依赖 trigger(target, key, newVal, oldVal); return res; } // 数据变化 if (oldVal !== newVal) { // 通知依赖 trigger(target, key, newVal, oldVal); } return res; }; }; // 响应式 type Reactive = { _isReactive: boolean; _isReadonly: boolean; } & T; /** * @description reactive object */ const createReactiveObj = ( target: T, isReadonly: boolean, shallow: boolean ) => { // 存在 Proxy const existingProxy = proxyMap.get(target); if (existingProxy) { return >existingProxy; } // 新建 const proxy = new Proxy(target, createReactiveHandlers(isReadonly, shallow)); proxyMap.set(target, proxy); return >proxy; }; /** * @description reactive * @param val * @returns */ const isReactive = (val: unknown): val is boolean => { return !!(val && val[ReactiveFlags.IS_REACTIVE]); }; /** * @description 创建 reactive * @param target * @returns */ const createReactive = (target: T): Reactive => { return createReactiveObj(target, false, false); }; /** * @description 顶层 reactive * @param target * @returns */ const shallowReactive = (target: T) => { return createReactiveObj(target, false, true); }; /** * @description reactive * @param val * @returns */ const isReadonly = (val: unknown): val is object => { return !!(val && val[ReactiveFlags.IS_READONLY]); }; /** * @description 创建 readonly * @param target * @returns */ const createReadonly = (target: T): T => { return createReactiveObj(target, true, false); }; /** * @description 顶层 readonly * @param target * @returns */ const shallowReadonly = (target: T) => { return createReactiveObj(target, true, true); }; /** * @description proxy * @param val * @returns */ const isProxy = (val: unknown): val is Reactive => { return isReactive(val) || isReadonly(val); }; /** * @description reactive * @param target * @returns */ const reactive = (target: T) => { return createReactive(target); }; /** * @description readonly * @param target * @returns */ const readonly = (target: T) => { return createReadonly(target); }; /** * @description 监听数据变化 * @param source * @param callback */ const watch = any)>( source: T, callback: ( newValue: T extends Ref[] ? UnwrapRefArray : T extends () => infer P ? P extends Ref[] ? UnwrapRefArray

: P : UnwrapRef, oldValue: T extends Ref[] ? UnwrapRefArray : T extends () => infer P ? P extends Ref[] ? UnwrapRefArray

: P : UnwrapRef ) => void, immediate: boolean = false ) => { // 立刻执行 immediate && callback(unref(source), unref(source)); // array if (Array.isArray(source) && source.every((s) => isRef(s))) { for (const i in source) { // Proxy if (isProxy(source[i])) { watch(source[i], () => { const res = source.map((s) => unref(s)); callback(res, res); }); } } watch<() => any>(() => source.map((s) => unref(s)), callback); return; } // function if (source instanceof Function) { watch(watchEffectRef(source), (n, o) => { callback(unref(n), unref(o)); }); return; } // Proxy if (isProxy(source)) { for (const key in source) { currentSub = () => { callback(source, source); }; // sub source const subSource = source[key]; currentSub = undefined; watch(subSource, () => { callback(source, source); }); } return; } // Ref if (isRef(source)) { // Ref.value Proxy if (isProxy(source.value)) { watch(source.value, () => { callback(unref(source), unref(source)); }); } currentSub = callback; // 收集依赖 trackRef(source); currentSub = undefined; return; } }; /** * @description 监听数据变化影响 * @param callback * @returns */ const watchEffect = (callback: () => any) => { currentSub = callback; // 收集依赖 callback(); currentSub = undefined; }; /** * @description 监听影响 ref * @param refVal * @param callback * @returns */ const watchRef = any), P>( source: T, callback: () => P ) => { // 收集依赖 const effectRes = shallowRef

(callback()); // 监听 watch(source, () => (effectRes.value = unref(callback()))); return effectRes; }; /** * @description 监听影响 ref * @param refVal * @param callback * @returns */ const watchEffectRef = (callback: () => T) => { // 收集依赖 const effectRes = shallowRef(undefined); // 监听 watchEffect(() => (effectRes.value = unref(callback()))); return >>effectRes; }; export { Ref, ref, shallowRef, unref, isRef, watch, watchRef, watchEffectRef, watchEffect, reactive, shallowReactive, isReactive, readonly, shallowReadonly, isReadonly, isProxy, isShallow, Reactive, }; ================================================ FILE: src/utils/element.ts ================================================ import { isRef, ref, Ref, watch, watchEffect, watchRef } from './composition'; type BaseEleChild = Ele; // 子元素 type EleChild = BaseEleChild | EleChildPromise | undefined; // 异步子元素 type EleChildPromise = Promise; // 子元素集合 type EleChildren = ( | BaseEleChild | EleChildPromise | Ref )[]; // 异步子元素 type EleChildrenPromise = Promise; // 子元素 type EleChildenRef = Ref; /** * @description 元素创建配置 */ type EleCreateOptions = { beforeCreat?: () => void; onCreated?: () => void; }; /** * @description 元素挂载配置 */ type EleMountOptions = { beforeMount?: () => void; onMounted?: () => void; }; /** * @description 事件配置 */ type EleEventOptions = EleCreateOptions & EleMountOptions; /** * @description 元素 */ type Ele = EleEventOptions & { ele: T; }; /** * @description 创建元素节点 * @param eleName * @param props * @param attrs * @param children * @returns */ function createElementNode( tagName: T, props?: { [key: string]: any }, attrs?: { [key: string]: any }, children?: EleChild | EleChildenRef | EleChildren, options?: EleEventOptions ): Ele { // 挂载状态 let beforemount = ref(false); // 挂载状态 let mounted = ref(false); const { onCreated, beforeCreat, onMounted, beforeMount } = options || {}; // 订阅 const subscribe = (e: EleMountOptions) => { const { onMounted, beforeMount } = e; if (beforeMount) { watch( beforemount, () => { if (beforemount.value) { beforeMount(); return; } }, true ); } if (onMounted) { watch( mounted, () => { if (mounted.value) { onMounted(); return; } }, true ); } }; // 取消订阅 const unsubscribe = (e: EleMountOptions) => { //懒得写 }; // 创建元素前 beforeCreat && beforeCreat(); // 创建普通元素 const ele = document.createElement(tagName); // 处理属性 handleProps(ele, props); // 处理属性 handleAttributes(ele, attrs, subscribe, unsubscribe); // 处理子元素 handleChildren(ele, children, subscribe, unsubscribe); // 收集挂载前 const collectBeforeMount = () => { beforemount.value = true; beforeMount && beforeMount(); }; // 收集挂载 const collectOnMounted = () => { mounted.value = true; onMounted && onMounted(); }; // 创建元素后 onCreated && onCreated(); return { ele, beforeMount: collectBeforeMount, onMounted: collectOnMounted }; } /** * @description 创建svg元素 * @param tagName * @param props * @param attrs * @param children * @returns */ function createNSElementNode( tagName: T, props?: { [key: string]: any }, attrs?: { [key: string]: any }, children?: EleChild | EleChildenRef | EleChildren, options?: EleEventOptions ): Ele { // 挂载状态 let beforemount = ref(false); // 挂载状态 let mounted = ref(false); const { onCreated, beforeCreat, onMounted, beforeMount } = options || {}; // 订阅 const subscribe = (e: EleMountOptions) => { const { onMounted, beforeMount } = e; if (beforeMount) { watch( beforemount, () => { if (beforemount.value) { beforeMount(); return; } }, true ); } if (onMounted) { watch( mounted, () => { if (mounted.value) { onMounted(); return; } }, true ); } }; // 取消订阅 const unsubscribe = (e: EleMountOptions) => { //懒得写 }; // 创建元素前 beforeCreat && beforeCreat(); // svg元素命名空间 const ns = 'http://www.w3.org/2000/svg'; // 创建svg元素 const ele = document.createElementNS(ns, tagName); // 处理属性 handleProps(ele, props); // 处理属性 handleAttributes(ele, attrs, subscribe, unsubscribe); // 处理子元素 handleChildren(ele, children, subscribe, unsubscribe); // 收集挂载前 const collectBeforeMount = () => { beforemount.value = true; beforeMount && beforeMount(); }; // 收集挂载 const collectOnMounted = () => { mounted.value = true; onMounted && onMounted(); }; // 创建元素后 onCreated && onCreated(); return { ele, beforeMount: collectBeforeMount, onMounted: collectOnMounted }; } /** * @description 处理属性 * @param ele * @param props */ function handleProps( ele: HTMLElement | SVGElement, props?: { [key: string]: any } ) { // props属性设置 for (const key in props) { // Ref 属性 if (isRef(props[key])) { const refVal = props[key]; watchEffect(() => (ele[key] = refVal.value)); continue; } ele[key] = props[key]; } } /** * @description 处理svg属性 * @param ele * @param attrs */ function handleAttributes( ele: HTMLElement | SVGElement, attrs?: { [key: string]: any }, subscribe?: (e: EleMountOptions) => void, unsubscribe?: (e: EleMountOptions) => void ) { // 属性存在 if (attrs) { // attrs属性设置 for (const key in attrs) { // 处理普通属性 handleAttribute(ele, key, attrs[key], subscribe, unsubscribe); } } } /** * @description 处理事件选项 */ function handleEventOptions(option: string[]) { if (option.length) { const options: AddEventListenerOptions = { capture: option.includes('capture'), once: option.includes('once'), passive: option.includes('passive'), }; return options; } } /** * @description 处理属性 * @param ele * @param key * @param value */ function handleAttribute( ele: HTMLElement | SVGElement, key: string, value: any, subscribe?: (e: EleMountOptions) => void, unsubscribe?: (e: EleMountOptions) => void ) { // 处理完的key const formatKey = key.toLowerCase(); // 事件绑定 if (formatKey.startsWith('on')) { // 事件监听 const [event] = formatKey.match(/(?<=on).*/)!; // 事件类型 if (event) { const [eventType, ...option] = event.split('_'); const options = handleEventOptions(option); // Ref 函数 if (isRef(value)) { const refVal = >value; const refListener = watchRef(refVal, () => refVal.value ? (e) => { option.includes('prevent') && e.preventDefault(); option.includes('stop') && e.stopPropagation(); const callback = refVal.value; callback(e); } : undefined ); // 设置事件监听 refListener.value && ele.addEventListener(eventType, refListener.value, options); // 监听事件变化 watch(refListener, (newVal, oldVal) => { // 移除旧事件监听 oldVal && ele.removeEventListener(eventType, oldVal); // 设置新事件监听 newVal && ele.addEventListener(eventType, newVal, options); }); return; } // 普通函数 if (value instanceof Function) { // 设置事件监听 ele.addEventListener(eventType, value, options); } } return; } // 特殊属性 const specificAttrs = ['checked', 'selected', 'disabled', 'enabled']; // 特殊 key if (specificAttrs.includes(formatKey)) { // Ref if (isRef(value)) { const refVal = value; watchEffect(() => { if (refVal.value) { ele.setAttribute(formatKey, ''); } else { ele.removeAttribute(formatKey); } }); return; } // 普通属性值 if (value) { ele.setAttribute(formatKey, ''); } else { ele.removeAttribute(formatKey); } return; } // ref 属性名 if (key === 'ref') { // Ref if (isRef(value)) { const refVal = value; subscribe && subscribe({ onMounted() { refVal.value = ele; }, }); return; } // Ref 函数 if (value instanceof Function) { const refFn = value; subscribe && subscribe({ onMounted() { refFn(ele); }, }); return; } return; } // xlink命名空间 if (key.startsWith('xlink:')) { // xlink属性命名空间 const attrNS = 'http://www.w3.org/1999/xlink'; if (value) { ele.setAttributeNS(attrNS, key, value); } else { ele.removeAttributeNS(attrNS, key); } return; } // Ref 属性值 if (key && isRef(value)) { const refVal = value; // 监听影响 watchEffect(() => { ele.setAttribute(key, refVal.value); }); return; } // 普通属性 if (key) { // 普通属性 ele.setAttribute(key, value); } } /** * @description 处理子元素 * @param ele * @param children */ function handleChildren( ele: HTMLElement | SVGElement, children?: EleChild | EleChildenRef | EleChildren, subscribe?: (e: EleMountOptions) => void, unsubscribe?: (e: EleMountOptions) => void ) { // Ref if (isRef(children)) { // 注释元素 const comment = document.createComment(''); // 监听元素变化 watch(children, async (newEle, oldEle) => { if (!newEle && oldEle) { // Promise if (oldEle instanceof Promise) { const oldEleRes = await oldEle; if (oldEleRes) { oldEleRes.forEach((ele) => { unsubscribe && unsubscribe(ele); }); } } // unPromise if (!(oldEle instanceof Promise)) { oldEle.forEach((ele) => { unsubscribe && unsubscribe(ele); }); } ele.replaceChildren(comment); return; } if (newEle) { if (oldEle) { // Promise if (oldEle instanceof Promise) { const oldEleRes = await oldEle; if (oldEleRes) { oldEleRes.forEach((ele) => { unsubscribe && unsubscribe(ele); }); } } // unPromise if (!(oldEle instanceof Promise)) { oldEle.forEach((ele) => { unsubscribe && unsubscribe(ele); }); } } // Promise if (newEle instanceof Promise) { const newEleRes = await newEle; if (newEleRes) { const eles = newEleRes.map((v) => { if (v.beforeMount || v.onMounted) { subscribe && subscribe(v); } return v.ele; }); ele.replaceChildren(createElementBlock(eles)); } return; } // unPromise const eles = newEle.map((v) => { if (v.beforeMount || v.onMounted) { subscribe && subscribe(v); } return v.ele; }); ele.replaceChildren(createElementBlock(eles)); return; } }); // Promise if (children.value instanceof Promise) { // 插入注释元素 ele.appendChild(comment); children.value.then((childrenEle) => { if (childrenEle) { const eles = childrenEle.map((v) => { if (v.beforeMount || v.onMounted) { subscribe && subscribe(v); } return v.ele; }); ele.replaceChildren(createElementBlock(eles)); } }); return; } // unPromise if (children.value) { const eles = children.value.map((v) => { if (v.beforeMount || v.onMounted) { subscribe && subscribe(v); } return v.ele; }); ele.appendChild(createElementBlock(eles)); return; } // 插入元素 ele.appendChild(comment); return; } // Promise if (children instanceof Promise) { // 注释元素 const comment = document.createComment(''); // 插入注释元素 ele.appendChild(comment); // 异步替换元素 children.then((childEle) => { if (childEle) { const { beforeMount, onMounted } = childEle; if (beforeMount || onMounted) { subscribe && subscribe(childEle); } comment.replaceWith(childEle.ele); } }); return; } // Array if (Array.isArray(children)) { // 处理过后 const resChildren: BaseEleChild[] = []; for (const i in children) { const child = children[i]; // Ref if (isRef(child)) { // 注释 const comment = document.createComment(''); // 监听影响 watch(child, async (newEle, oldEle) => { // 新元素为空 if (!newEle && oldEle) { // Promise if (oldEle instanceof Promise) { const oldEleRes = await oldEle; if (oldEleRes) { handleChangeElement( newEle, oldEleRes, comment, subscribe, unsubscribe ); } return; } handleChangeElement( newEle, oldEle, comment, subscribe, unsubscribe ); return; } // 旧元素为空 if (newEle && !oldEle) { // Promise if (newEle instanceof Promise) { const newEleRes = await newEle; if (newEleRes) { handleChangeElement( newEleRes, oldEle, comment, subscribe, unsubscribe ); } return; } handleChangeElement( newEle, oldEle, comment, subscribe, unsubscribe ); return; } // 存在 if (newEle && oldEle) { // Promise if (newEle instanceof Promise && oldEle instanceof Promise) { const newEleRes = await newEle; const oldEleRes = await oldEle; // 处理元素变化 handleChangeElement( newEleRes, oldEleRes, comment, subscribe, unsubscribe ); return; } // Promise if (newEle instanceof Promise && !(oldEle instanceof Promise)) { const newEleRes = await newEle; // 处理元素变化 handleChangeElement( newEleRes, oldEle, comment, subscribe, unsubscribe ); return; } // Promise if (!(newEle instanceof Promise) && oldEle instanceof Promise) { const oldEleRes = await oldEle; // 处理元素变化 handleChangeElement( newEle, oldEleRes, comment, subscribe, unsubscribe ); return; } // 非 Promise if (!(oldEle instanceof Promise) && !(newEle instanceof Promise)) { // 处理元素变化 handleChangeElement( newEle, oldEle, comment, subscribe, unsubscribe ); return; } } }); // Promise if (child.value instanceof Promise) { // 注释 resChildren[i] = { ele: comment }; // 异步替换 child.value.then((childEle) => { if (childEle) { const { beforeMount, onMounted } = childEle; if (beforeMount || onMounted) { subscribe && subscribe(childEle); } comment.replaceWith(childEle.ele); } }); continue; } // unPromise if (child.value) { const { beforeMount, onMounted, ele } = child.value; resChildren[i] = { ele, beforeMount, onMounted }; continue; } resChildren[i] = { ele: comment }; continue; } // Promise if (child instanceof Promise) { // 注释 const comment = document.createComment(''); resChildren[i] = { ele: comment }; // 异步替换元素 child.then((childEle) => { if (childEle) { const { beforeMount, onMounted } = childEle; if (beforeMount || onMounted) { subscribe && subscribe(childEle); } comment.replaceWith(childEle.ele); } }); continue; } // 普通元素 if (child) { const { beforeMount, onMounted, ele } = child; resChildren[i] = { ele, beforeMount, onMounted }; } } const eles = resChildren.map((v) => { if (v.beforeMount || v.onMounted) { subscribe && subscribe(v); } return v.ele; }); // 插入元素 ele.appendChild(createElementBlock(eles)); return; } // 普通元素 if (children) { const { beforeMount, onMounted } = children; if (beforeMount || onMounted) { subscribe && subscribe(children); } // 插入元素 ele.appendChild(children.ele); return; } return; } /** * @description 元素变化 * @param newEle * @param oldEle * @param comment */ function handleChangeElement( newEle: BaseEleChild | undefined, oldEle: BaseEleChild | undefined, comment: Comment, subscribe?: (e: Ele) => void, unsubscribe?: (e: Ele) => void ) { if (newEle && oldEle) { const { beforeMount, onMounted } = newEle; if (beforeMount || onMounted) { subscribe && subscribe(newEle); } oldEle.ele.replaceWith(newEle.ele); return; } if (newEle && !oldEle) { const { beforeMount, onMounted } = newEle; if (beforeMount || onMounted) { subscribe && subscribe(newEle); } comment.replaceWith(newEle.ele); return; } if (!newEle && oldEle) { unsubscribe && unsubscribe(oldEle); oldEle.ele.replaceWith(comment); return; } } /** * @description 创建文字节点 * @param text * @returns */ function createTextNode(text: any, options?: EleEventOptions): Ele { const { onCreated, beforeCreat, onMounted, beforeMount } = options || {}; // 创建元素前 beforeCreat && beforeCreat(); // Ref if (isRef(text)) { // ref const refVal = text; // 元素 const ele = document.createTextNode(''); // 订阅变化 watchEffect(() => { ele.data = refVal.value; }); // 创建元素后 onCreated && onCreated(); return { ele, beforeMount, onMounted }; } // 创建元素后 onCreated && onCreated(); return { ele: document.createTextNode(String(text)), beforeMount, onMounted }; } /** * @description 挂载元素 * @param eleOptions * @param parent */ function mountElement( eleOptions: Ele, parent: HTMLElement | SVGElement = document.body ) { const { ele, beforeMount, onMounted } = eleOptions; if (ele) { // 触发挂载前事件 beforeMount && beforeMount(); parent.appendChild(ele); // 挂在后 onMounted && onMounted(); } } /** * @description 选择器 * @param selector * @returns */ function $$( selector: string, parent: Document | Element = document ) { return Array.from(parent.querySelectorAll(selector)); } /** * @description 异步选择器 * @param selector * @returns */ function $_( selector: string, parent: Document | Element = document, timeout?: number ) { return new Promise((resolve) => { const timer = setInterval(() => { const selectors = Array.from(parent.querySelectorAll(selector)); // 存在元素 if (selectors.length) { clearInterval(timer); resolve(selectors); } }, 10); // 超时 if (timeout) { setTimeout(() => { clearInterval(timer); resolve([]); }, timeout); } }); } /** * @description 创建元素块 * @param eles * @returns */ function createElementBlock( eles: (HTMLElement | SVGElement | Text | Comment)[] ) { const fragment = document.createDocumentFragment(); for (const i in eles) { fragment.appendChild(eles[i]); } return fragment; } export { createElementNode, createNSElementNode, createTextNode, $$, $_, createElementBlock, mountElement, Ele, }; ================================================ FILE: src/utils/log.ts ================================================ import { formatDateTime } from './time'; /** * @description 打印日志 * @param text */ function log(...text: any[]) { printColor('dodgerblue', ...text); } /** * @description 打印错误 * @param text */ function error(...text: any[]) { printColor('red', ...text); } /** * @description 打印信息 * @param text */ function info(...text: any[]) { printColor('yellow', ...text); } /** * @description 打印颜色 * @param text * @param color */ function printColor(color: string, ...text: any[]) { const textFormatted = text .map((t) => (typeof t === 'object' ? JSON.stringify(t) : String(t))) .join(' '); console.log( `%c[${formatDateTime()}] %c${textFormatted}`, '', `color: ${color}` ); } export { log, error, info }; ================================================ FILE: src/utils/push.ts ================================================ import { pushPlus } from '../api/push'; import { formatDateTime } from './time'; /** * @description 消息模板类型 */ type TemplateType = 'html' | 'txt' | 'json' | 'markdown' | 'cloudMonitor'; /** * @description 推送选项 */ type PushOptions = { title: string; content: string; template: TemplateType; toToken?: string; fromToken: string; }; /** * @description 模态框 */ type ModalOptions = { title: string; subTitle?: string; content: string | string[]; to?: string; from?: string; type: ModalType; }; /** * @description 类型 */ type ModalType = 'info' | 'warn' | 'fail' | 'success'; /** * @description html进度条 * @param title * @param percent * @returns */ function getProgressHTML(title: string, current: number, total: number) { // html const progressHTML = `

${title} ${getHighlightHTML(`${current}`)} / ${total}
`; return progressHTML; } /** * @description html高亮文本 * @param text * @returns */ function getHighlightHTML(text: string | number) { // html const highlightHTML = `${text}`; return highlightHTML; } /** * @description 二维码 * @param src */ function getImgHTML(src: string) { // 图片 return `
`; } /** * @description 创建模态框 * @param options 选项 * @returns */ function createModal(options: ModalOptions) { // 配置 const { title, subTitle = '', to = '用户', content, type, from = 'tech-study.js', } = options; // 内容文本 let contentText = ''; if (Array.isArray(content)) { contentText = content.map((ct) => `
${ct}
`).join(''); } else { contentText = content; } // 日期 const dateTime = formatDateTime(); // 类型html let typeHTML = ''; if (type && type.length) { if (type === 'info') { typeHTML = ` `; } if (type === 'warn') { typeHTML = ` `; } if (type === 'success') { typeHTML = ` `; } if (type === 'fail') { typeHTML = ` `; } } // 类型 const typeWrap = ` ${typeHTML} `; // 基础html const baseHTML = `
${typeWrap} ${title}
${subTitle}
${getHighlightHTML(to)}, 你好!
${contentText}
${dateTime}
来自 ${from}
`; return baseHTML; } /** * @description 推送消息 */ async function pushMessage(options: PushOptions) { // 选项 const { title, content, template, fromToken, toToken } = options; // 推送 const res = await pushPlus(fromToken, title, content, template, toToken); return res; } /** * @description 推送模态框 */ async function pushModal( options: ModalOptions, fromToken: string, toToken?: string ) { // html const html = createModal(options); // 推送 const res = await pushMessage({ title: '消息提示', content: html, fromToken, toToken, template: 'html', }); if (res && res.code === 200) { return res; } return; } export { createModal, getHighlightHTML, getImgHTML, getProgressHTML, pushModal, }; ================================================ FILE: src/utils/random.ts ================================================ /** * @description 点 */ type Point = { x: number; y: number }; /** * @description 范围 */ type Bounds = { x: number; y: number; width: number; height: number }; /** * @description 创建随机点 * @param bounds 范围 * @returns */ function createRandomPoint(bounds: Bounds): Point { // 范围 const { x, y, width, height } = bounds; // 横坐标 const randX = x + Math.random() * width * 0.5 + width * 0.25; // 纵坐标 const randY = y + Math.random() * height * 0.5 + height * 0.25; return { x: randX, y: randY, }; } /** * @description 生成随机路径 * @param start * @param end * @param steps * @returns */ function createRandomPath(start: Point, end: Point, steps: number) { // 最小水平增量 const minDeltaX = (end.x - start.x) / steps; // 最大垂直增量 const maxDeltaY = (end.y - start.y) / steps; const path: Point[] = []; // 开始节点 path.push(start); // 插入点 for (let i = 0; i < steps; i++) { // 横坐标 const x = path[i].x + Math.random() * 5 + minDeltaX; // 纵坐标 const y = path[i].y + Math.random() * 5 * Math.pow(-1, ~~(Math.random() * 2 + 1)) + maxDeltaY; path.push({ x, y, }); } return path; } /** * @description 随机数字 * @returns */ function generateNumAsChar(): string { return (~~(Math.random() * 10)).toString(); } /** * @description 随机大写字母 * @returns */ function generateUpperAsChar(): string { return String.fromCharCode(~~(Math.random() * 26) + 65); } /** * @description 随机小写字母 * @returns */ function generateLowerAsChar(): string { return String.fromCharCode(~~(Math.random() * 26) + 97); } /** * @description 随机混合字符 * @param length * @returns */ function generateMix(length: number = 6): string { // 随机字符串 const randomText: string[] = []; // 生成器 const typeGenerator: (() => string)[] = [ generateNumAsChar, generateUpperAsChar, generateLowerAsChar, ]; if (length) { for (let i = 0; i < length; i++) { // 随机位置 const randomIndex = ~~(Math.random() * typeGenerator.length); randomText.push(typeGenerator[randomIndex]()); } } return randomText.join(''); } export { createRandomPoint, createRandomPath, generateMix }; ================================================ FILE: src/utils/time.ts ================================================ /** * @description 格式化日期时间数字 * @param num * @returns */ function formatDateNum(num: number) { return num < 10 ? `0${num}` : `${num}`; } /** * @description 格式化日期时间 * @param time * @returns * @example * formatDateTime() -> "2022-09-01 08:00:00" * formatDateTime(new Date()) -> "2022-09-01 08:00:00" * formatDateTime(Date.now()) -> "2022-09-01 08:00:00" */ function formatDateTime(time: Date | string | number = Date.now()) { const date = new Date(time); const s = date.getSeconds(); const min = date.getMinutes(); const h = date.getHours(); const d = date.getDate(); const m = date.getMonth() + 1; const y = date.getFullYear(); // 日期 const dateText = [y, m, d].map(formatDateNum).join('-'); // 时间 const timeText = [h, min, s].map(formatDateNum).join(':'); // 日期时间 const dateTimeText = `${dateText} ${timeText}`; return dateTimeText; } /** * @description 格式化时间 * @param time * @returns * @example * formatTime() -> "08:00:00" * formatTime(new Date()) -> "08:00:00" * formatTime(Date.now()) -> "08:00:00" */ const formatTime = (time: Date | string | number = Date.now()) => { const date = new Date(time); const s = date.getSeconds(); const min = date.getMinutes(); const h = date.getHours(); // 时间 const timeText = [h, min, s].map(formatDateNum).join(':'); return timeText; }; /** * @description 时间已过 * @param hour * @param minute * @returns */ function isLate({ hour, minute }: { hour: number; minute: number }) { const date = new Date(); const h = date.getHours(); const min = date.getMinutes(); return h > hour || (h === hour && min >= minute); } /** * @description 时间已过 * @param hour * @param minute * @returns */ function isNow({ hour, minute }: { hour: number; minute: number }) { const date = new Date(); const h = date.getHours(); const min = date.getMinutes(); const s = date.getSeconds(); return h === hour && min === minute && s === 0; } export { formatDateNum, formatDateTime, isLate, isNow }; ================================================ FILE: src/utils/utils.ts ================================================ import { log } from './log'; /* 工具函数 */ /** * @description 设置cookie * @param name * @param value * @param expires */ function setCookie( name: string, value: string, expires: number, domain: string ) { // 当前日期 const date = new Date(); // 过期日期 date.setTime(date.getTime() + expires); // 设置cookie document.cookie = `${name}=${value};expires=${date.toUTCString()};path=/;domain=${domain}`; } /** * @description 获取cookie * @param name * @returns */ function getCookie(name: string) { // 获取当前所有cookie const strCookies = document.cookie; // 截取变成cookie数组 const cookieText = strCookies.split(';'); // 循环每个cookie for (const i in cookieText) { // 将cookie截取成两部分 const item = cookieText[i].split('='); // 判断cookie的name 是否相等 if (item[0].trim() === name) { return item[1].trim(); } } return null; } /** * @description 删除cookie * @param name */ function delCookie(name: string, domain: string) { // 存在cookie const value = getCookie(name); if (value !== null) { setCookie(name, '', -1, domain); } } /** * @description 防抖 * @param callback * @param delay * @returns */ function debounce any>(callback: T, delay: number) { let timer = -1; return function (this: any, ...args: Parameters) { if (timer !== -1) { clearTimeout(timer); } timer = setTimeout(() => { callback.apply(this, args); }, delay); }; } /** * @description 判断是否为移动端 * @returns */ function hasMobile() { let isMobile = false; if ( navigator.userAgent.match( /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i ) ) { log('移动端'); isMobile = true; } if (document.body.clientWidth < 800) { log('小尺寸设备端'); isMobile = true; } return isMobile; } /** * @description 等待时间 * @param time * @returns */ function sleep(time: number) { // 延时 let timeDelay = Number(time); if (!Number.isInteger(timeDelay)) { timeDelay = 1000; } timeDelay += Math.random() * 500 - 250; return new Promise((resolve) => { setTimeout(() => { resolve(undefined); }, timeDelay); }); } /** * @description 暂停学习锁 */ function studyPauseLock(callback?: (msg: boolean) => void) { return new Promise((resolve) => { // 暂停 const pauseStudy = GM_getValue('pauseStudy') || false; if (pauseStudy) { const doing = setInterval(() => { // 暂停 const pauseStudy = GM_getValue('pauseStudy') || false; if (!pauseStudy) { // 停止定时器 clearInterval(doing); log('学习等待结束!'); if (callback && callback instanceof Function) { callback(true); } resolve(true); return; } if (callback && callback instanceof Function) { callback(false); } log('学习等待...'); }, 500); return; } resolve(true); }); } /** * @description 加载 * @param match * @param callback */ function load( match: string | RegExp | ((href: string) => any) | boolean, callback: () => void ) { // 链接 const { href } = window.location; window.addEventListener('load', () => { // 函数 if (match instanceof Function) { match(href) && callback(); return; } // 布尔 if (typeof match === 'boolean') { match && callback(); return; } // 字符正则 if (href.match(match)) { callback(); return; } }); } export { debounce, delCookie, getCookie, hasMobile, load, setCookie, sleep, studyPauseLock, }; ================================================ FILE: tech-study.js ================================================ // ==UserScript== // @name 不学习何以强国 // @namespace http://tampermonkey.net/ // @version 1.7.5 // @description 有趣的 `学习强国` 油猴插件。读文章,看视频,做习题。问题反馈: https://github.com/Xu22Web/tech-study-js/issues 。 // @author 原作者:techxuexi 荷包蛋。现作者:Xu22Web // @match https://www.xuexi.cn/* // @match https://pc.xuexi.cn/points/exam-practice.html // @match https://pc.xuexi.cn/points/exam-weekly-detail.html?id=* // @match https://pc.xuexi.cn/points/exam-paper-detail.html?id=* // @match 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 // @require https://cdn.jsdelivr.net/npm/blueimp-md5@2.9.0 // @run-at document-start // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_openInTab // @grant GM_addValueChangeListener // @grant unsafeWindow // @updateURL https://raw.githubusercontent.com/Xu22Web/tech-study-js/master/tech-study.js // @downloadURL https://raw.githubusercontent.com/Xu22Web/tech-study-js/master/tech-study.js // @supportURL https://github.com/Xu22Web // ==/UserScript== const css = '* { -webkit-tap-highlight-color: transparent;}:root { --themeColor: #fa3333; --scale: 1; font-size: calc(10px * var(--scale));}@media (min-height: 678px) and (max-height: 768px) { :root { --scale: 0.8; }}@media (max-height: 667px) { :root { --scale: 0.75; }}@keyframes fade { from { opacity: 0.8; } to { opacity: 0.4; background: #ccc; }}.egg_icon { width: 1em; height: 1em; fill: currentColor;}.egg_hr_wrap { position: relative; display: flex; justify-content: center; color: #ccc;}.egg_hr_wrap .egg_hr { position: absolute; top: 50%; transform: translateY(-50%); background: currentColor; height: 0.1rem; width: 30%;}.egg_hr_wrap .egg_hr:nth-of-type(1) { left: 0;}.egg_hr_wrap .egg_hr:nth-last-of-type(1) { right: 0;}.egg_hr_title { font-size: 1.2rem;}.egg_exam_btn { transition: background 80ms; outline: none; border: none; padding: 1.2rem 2rem; border-radius: 1.2rem; cursor: pointer; font-size: 1.8rem; font-weight: bold; text-align: center; color: #ffffff; background: #ccc;}.egg_exam_btn.manual { background: var(--themeColor);}.egg_panel_wrap * { padding: 0; margin: 0; box-sizing: border-box; outline: none; border: none;}.egg_panel_wrap { position: fixed; left: 0; top: 0; z-index: 99999; width: 100%; height: 100%; color: #333; font-size: 1.6rem; pointer-events: none;}.egg_panel { position: absolute; top: 5rem; left: 1rem; padding: 1.2rem 2rem; border-radius: 1rem; background: #ffffffe6; backdrop-filter: blur(1rem); box-shadow: 0 0 0.1rem 0.1rem #f1f1f1; transition: 80ms ease-out; pointer-events: all;}.egg_panel.hide { left: 0; transform: translateX(-100%);}.egg_panel_wrap.mobile .egg_panel { top: 1rem;}@media (min-height: 678px) and (max-height: 768px) { .egg_panel { top: 2rem; }}@media (max-height: 667px) { .egg_panel { top: 1rem; }}.egg_panel button { outline: none; border: none; padding: 0; cursor: pointer; background: none;}.egg_panel .egg_btns_wrap { position: absolute; left: 100%; top: 50%; transform: translate(-50%, -50%); transition: 80ms ease; z-index: 9;}.egg_panel.hide .egg_btns_wrap { left: 100%; transform: translate(0, -50%);}.egg_panel .egg_btns_wrap button { border-radius: 50%; width: 3rem; height: 3rem; padding: 0; overflow: hidden; border: 0.2rem solid currentColor; color: white; display: grid; place-items: center; font-size: 1.8rem;}.egg_panel.hide .egg_panel_show_btn { background: var(--themeColor);}.egg_panel .egg_panel_show_btn { background: #ccc;}.egg_panel .egg_frame_show_btn { background: var(--themeColor); margin-bottom: 1rem;}.egg_panel .egg_frame_show_btn.hide { display: none;}.egg_panel .egg_settings_show_btn { background: #ccc; margin-top: 1rem;}.egg_panel .egg_settings_show_btn.active { background: var(--themeColor);}.egg_panel .egg_settings_reset_btn { background: #ccc; margin-top: 1rem;}.egg_panel .egg_settings_reset_btn:active { background: var(--themeColor);}.egg_login_item { display: flex; justify-content: center; align-items: center; flex-direction: column; padding: 0.5rem 0;}.egg_login_item .egg_login_btn { font-size: 1.4rem; border-radius: 1rem; transition: 80ms ease; color: white; background: var(--themeColor); padding: 0.8rem 2.4rem;}.egg_login_item .egg_login_btn:active { opacity: 0.8;}.egg_login_item .egg_login_img_wrap { height: 0; border-radius: 1rem; transition: height 80ms ease; overflow: hidden;}.egg_login_item .egg_login_img_wrap.active { padding: 0.8rem; margin-top: 0.8rem; height: auto; background: white;}.egg_login_img_wrap .egg_login_img { width: 15rem; height: 15rem;}.egg_info_item .egg_login_btn { font-size: 1.4rem; border-radius: 1rem; transition: 80ms ease; color: white; background: #ccc; padding: 0.4rem 0.8rem;}.egg_info_item .egg_login_btn:active { opacity: 0.8;}.egg_info_item { display: flex; justify-content: space-between; align-items: center;}.egg_info_item .egg_userinfo { display: flex; justify-content: center; align-items: center; padding: 0.5rem 0;}.egg_userinfo .egg_avatar .egg_avatar_nick,.egg_userinfo .egg_avatar .egg_avatar_img { height: 5rem; width: 5rem; border-radius: 50%; background: var(--themeColor); display: flex; justify-content: center; align-items: center; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; font-size: 2rem; color: white;}.egg_userinfo .egg_nick { padding-left: 0.5rem; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 10rem;}.egg_score_item .egg_scoreinfo { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0;}.egg_scoreinfo .egg_totalscore,.egg_scoreinfo .egg_todayscore { font-size: 1.2rem; user-select: none;}.egg_scoreinfo .egg_totalscore span,.egg_scoreinfo .egg_todayscore .egg_todayscore_btn span { padding-left: 0.2rem;}.egg_scoreinfo .egg_totalscore span,.egg_todayscore .egg_todayscore_btn span,.egg_todayscore .egg_score_details span { color: var(--themeColor); font-weight: bold;}.egg_scoreinfo .egg_todayscore { position: relative;}.egg_todayscore .egg_todayscore_btn { display: flex; align-items: center;}.egg_todayscore_btn .egg_icon { opacity: 0.3;}.egg_todayscore .egg_score_details { position: absolute; left: calc(100% + 1rem); top: 0; background: #fffffff2; border-radius: 0.5rem; opacity: 1; width: 10rem; box-shadow: 0 0 0.1rem 0.1rem #f1f1f1; transition: 80ms ease; z-index: 9;}.egg_todayscore .egg_score_details.hide { visibility: hidden; opacity: 0; left: 100%;}.egg_score_details .egg_score_title { border-bottom: 0.1rem solid #eee; padding: 0.5rem 0.8rem; display: flex; align-items: center;}.egg_score_details .egg_score_title .egg_icon { font-size: 1.4rem;}.egg_score_details .egg_score_title .egg_score_title_text { font-weight: bold; padding-left: 0.2rem;}.egg_score_details .egg_score_item { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0.8rem;}.egg_task_list { position: relative;}.egg_task_item { user-select: none; min-height: 3rem; min-width: 18rem; display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0;}.egg_task_item .egg_label_wrap { flex-grow: 1; padding-right: 0.5rem;}.egg_label_wrap .egg_task_title_wrap { display: flex; justify-content: space-between; align-items: center;}.egg_task_title_wrap .egg_task_progress_wrap { display: flex; align-items: center; font-size: 1.4rem; width: 3.5rem;}.egg_task_progress_wrap .egg_task_current { color: var(--themeColor);}.egg_task_progress_wrap .egg_task_max { color: #999; font-size: 1.2rem;}.egg_label_wrap .egg_progress { display: flex; justify-content: space-between; align-items: center; padding-top: 0.8rem;}.egg_progress .egg_track { background: #ccc; height: 0.5rem; border-radius: 1rem; flex: 1 1 auto; overflow: hidden;}.egg_progress .egg_track .egg_bar { height: 0.5rem; background: var(--themeColor); border-radius: 1rem; width: 0; transition: width 0.5s;}.egg_setting_item { min-height: 3rem; min-width: 18rem; display: flex; align-items: center; justify-content: space-between; box-sizing: border-box;}.egg_setting_item .egg_label_wrap { flex-grow: 1;}.egg_detail { background: #ccc; color: white; border-radius: 10rem; font-size: 1.2rem; width: 1.6rem; height: 1.6rem; margin-left: 0.4rem; display: inline-block; text-align: center; line-height: 1.6rem; cursor: pointer;}.egg_switch { cursor: pointer; margin: 0; outline: 0; appearance: none; -webkit-appearance: none; -moz-appearance: none; position: relative; width: 4.2rem; height: 2.2rem; background: #ccc; border-radius: 5rem; transition: background 0.3s; --border-padding: 0.5rem; box-shadow: -0.1rem 0 0.1rem -0.1rem #999 inset, 0.1rem 0 0.1rem -0.1rem #999 inset;}.egg_switch::after { content: \'\'; display: inline-block; width: 1.4rem; height: 1.4rem; border-radius: 50%; background: #fff; box-shadow: 0 0 0.2rem #999; transition: left 0.4s; position: absolute; top: calc(50% - (1.4rem / 2)); position: absolute; left: var(--border-padding);}.egg_switch:checked { background: var(--themeColor);}.egg_switch:disabled { opacity: 0.5; background: #ccc;}.egg_switch:checked::after { left: calc(100% - var(--border-padding) - 1.4rem);}.egg_tip_list { font-size: 1.2rem; max-width: 18rem; line-height: 2rem; color: var(--themeColor);}.egg_tip_list .egg_tip_btn { padding: 0.2rem 0.4rem; background: #f1f1f1; color: #333;}.egg_tip_list .egg_tip_btn:disabled { opacity: 0.5; background: #ccc;}.egg_tip_list .egg_tip_content { text-align: center; padding-top: 0.2rem;}.egg_study_item { display: flex; justify-content: center; padding-top: 0.5rem;}.egg_study_item .egg_study_btn { background: var(--themeColor); padding: 0.8rem 2.4rem; font-size: 1.4rem; border-radius: 1rem; color: white; transition: 80ms ease;}.egg_study_item .egg_study_btn:not(.loading):active { opacity: 0.8;}.egg_study_item .egg_study_btn.loading { animation: fade 2s ease infinite alternate;}.egg_study_item .egg_study_btn:disabled { background: #ccc;}.egg_tip_wrap { position: fixed; left: 0; top: 0; z-index: 999999; width: 100%; height: 100%; pointer-events: none;}.egg_tip_wrap * { padding: 0; margin: 0; box-sizing: border-box; outline: none; border: none;}.egg_tip_wrap .egg_tip { position: absolute; bottom: 2rem; left: 2rem; padding: 1.2rem 1.4rem; border: none; border-radius: 1rem; background: var(--themeColor); color: white; font-size: 1.4rem; transition: 200ms ease; opacity: 0; transform: scale(0.9) translateY(1rem);}.egg_tip_wrap .egg_tip.active { opacity: 1; transform: scale(1) translateY(0);}.egg_tip_wrap .egg_tip.active.delay { opacity: 0.5;}.egg_tip_wrap .egg_tip .egg_countdown { display: inline-block; color: var(--themeColor); background: white; border-radius: 0.5rem; padding: 0.2rem 0.4rem; font-weight: bold; margin-left: 0.4rem; font-size: 1.2rem;}.egg_frame_wrap { position: fixed; left: 0; top: 0; z-index: 999; width: 100%; height: 100%; visibility: visible;}.egg_frame_wrap * { padding: 0; margin: 0; box-sizing: border-box; outline: none; border: none;}.egg_frame_wrap.hide { visibility: hidden;}.egg_frame_wrap.hide .egg_frame_mask,.egg_frame_wrap.hide .egg_frame_content_wrap { opacity: 0;}.egg_frame_wrap.hide .egg_frame_content_wrap { transform: scale(0);}.egg_frame_mask { background: #00000030; width: 100%; height: 100%; opacity: 1; transition: 200ms ease;}.egg_frame_content_wrap { position: absolute; width: 80%; height: 80%; top: 10%; left: 10%; display: flex; flex-direction: column; transition: 200ms ease; border-radius: 1rem; background: #ffffffe6; backdrop-filter: blur(1rem); overflow: hidden; transform: scale(1);}.egg_frame_content_wrap.max { top: 0; left: 0; width: 100%; height: 100%; border-radius: 0;}.egg_frame_content_wrap .egg_frame_controls_wrap { width: 100%; display: flex; justify-content: space-between; align-items: center; box-sizing: border-box;}.egg_frame_controls_wrap .egg_frame_title { padding: 1rem 2rem; font-size: 1.6rem;}.egg_frame_controls .egg_frame_btn { outline: none; border: none; background: none; padding: 1rem 2rem; transition: 80ms ease; cursor: pointer; color: #333; font-size: 1.8rem;}.egg_frame_controls .egg_frame_btn:active { opacity: 0.8;}.egg_frame_wrap .egg_frame_content { width: 100%; flex-grow: 1; border-top: 0.1rem solid #ccc; min-height: 40rem; min-width: 30rem; background: white;}.egg_frame_content .egg_frame { width: 100%; height: 100%; outline: none; border: none;}.egg_time_input { display: inline-flex; align-items: center; justify-content: center;}.egg_time_input .egg_hour_wrap,.egg_time_input .egg_minute_wrap { width: 4rem;}.egg_time_input .egg_separator { padding: 0 0.5rem; font-size: 1.5rem;}.egg_settings_item { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; overflow: hidden; border-radius: 1rem;}.egg_settings_item .egg_settings { display: inline-flex; flex-direction: column; font-size: 1.4rem; background: white; border-radius: 1rem; overflow: hidden; width: 100%; height: 100%; pointer-events: all; transform: translateX(100%); transition: transform 300ms ease; padding-top: 1rem;}.egg_settings_item .egg_settings.active { transform: translateX(0);}.egg_settings .egg_settings_label { padding-bottom: 1rem; user-select: none;}.egg_settings_item .egg_settings_version_wrap { padding: 1rem 2rem 0 2rem; display: flex; align-items: center; justify-content: space-between;}.egg_settings_version_wrap .egg_settings_version { color: #999; display: flex; align-items: center;}.egg_settings_version .egg_settings_version_detail { color: #24292f; font-size: 1.6rem; width: 1.6rem; height: 1.6rem; margin-left: 0.4rem;}.egg_settings_item .egg_settings_theme_wrap { padding: 1rem 2rem 0 2rem;}.egg_settings_theme_wrap .egg_settings_theme_colors { display: flex; align-items: center; justify-content: space-between;}.egg_settings_theme_color_wrap .egg_settings_theme_color { border-radius: 50%; width: 1.6rem; height: 1.6rem; background: currentColor;}.egg_settings .egg_settings_read_time_wrap,.egg_settings .egg_settings_watch_time_wrap { padding: 1rem 2rem 0 2rem; display: flex; justify-content: space-between; align-items: center;}.egg_settings_read_time_wrap .egg_settings_label,.egg_settings_watch_time_wrap .egg_settings_label { padding: 0.5rem 0;}.egg_settings_read_time_wrap .egg_select,.egg_settings_watch_time_wrap .egg_select { width: 6rem;}.egg_settings .egg_settings_token_wrap { padding: 1rem 2rem 0 2rem;}.egg_settings_token_wrap .egg_settings_token_input { outline: none; border: 0.1rem solid #eee; padding: 1rem; background: white; border-radius: 0.2rem; width: 100%; box-sizing: border-box; color: #ccc;}.egg_settings_token_wrap .egg_settings_token_input.active { color: #333;}.egg_settings_token_input::placeholder { color: #ccc;}.egg_settings .egg_settings_submit_btn_wrap { text-align: right; padding-top: 1rem; display: none;}.egg_settings .egg_settings_submit_btn_wrap.active { display: block;}.egg_settings_submit_btn_wrap .egg_settings_submit_btn { outline: none; border: 0.1rem solid #eee; padding: 0.5rem 1rem; text-align: center; background: white; border-radius: 0.2rem; cursor: pointer;}.egg_settings_submit_btn_wrap .egg_settings_submit_btn:active { background: #eee;}.egg_schedule { height: 100%; display: flex; flex-direction: column;}.egg_schedule_time_wrap { padding: 1rem 2rem; border-bottom: 0.1rem solid #eee;}.egg_schedule_time .egg_schedule_label { padding-bottom: 1rem; user-select: none;}.egg_schedule_time .egg_schedule_time_input_wrap { display: flex; justify-content: space-between; align-items: center;}.egg_schedule_time_input_wrap .egg_schedule_add_btn { outline: none; border: 0.1rem solid #eee; padding: 0.5rem 1rem; text-align: center; background: white; border-radius: 0.2rem; cursor: pointer;}.egg_schedule_time_input_wrap .egg_schedule_add_btn:active { background: #eee;}.egg_schedule_list { height: 100%; overflow: auto;}.egg_schedule_list .egg_schedule_item { display: flex; justify-content: space-between; padding: 0.5rem 1.5rem; font-size: 1.4rem; border-bottom: 0.1rem solid #eee;}.egg_schedule_list::-webkit-scrollbar { width: 0.4rem; background: white; border-radius: 0.2rem;}.egg_schedule_list::-webkit-scrollbar-thumb { background: #ccc; border-radius: 0.2rem;}.egg_schedule_detail_time_wrap { display: flex; align-items: center;}.egg_schedule_detail_time_wrap.inactive { color: #ccc;}.egg_schedule_detail_time_wrap .egg_schedule_detail_icon { padding-right: 0.4rem; display: flex; color: #ccc;}.egg_schedule_detail_del_wrap .egg_schedule_del_btn { outline: none; padding: 1rem; text-align: center; background: white; border-radius: 0.2rem; font-size: 1.4rem; cursor: pointer; color: #ccc;}.egg_schedule_detail_del_wrap .egg_schedule_del_btn:hover { color: #333;}.egg_schedule_detail_del_wrap .egg_schedule_del_btn:active { color: #eee;}.egg_schedule_list .egg_schedule_list_none { width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; color: #ccc;}.egg_schedule_list_none .egg_icon { font-size: 2.5rem;}.egg_schedule_list_none_text { padding-top: 1rem;}.egg_select { position: relative;}.egg_select .egg_select_input { outline: none; border: 0.1rem solid #eee; padding: 0.8rem; text-align: center; background: white; border-radius: 0.2rem; display: inline-block; width: 100%; box-sizing: border-box;}.egg_select .egg_select_input::placeholder { color: #ccc;}.egg_select_list { max-height: 12rem; border-radius: 0 0 0.2rem 0.2rem; box-shadow: 0 0.1rem 0.1rem 0.1rem #eee; background: white; user-select: none; transition: 100ms ease; scrollbar-width: thin; overflow: auto; opacity: 1; z-index: 9; width: 100%; position: absolute;}.egg_select_list.hide { opacity: 0; visibility: hidden;}.egg_select_list::-webkit-scrollbar { width: 0.4rem; background: white; border-radius: 0.2rem;}.egg_select_list::-webkit-scrollbar-thumb { background: #ccc; border-radius: 0.2rem;}.egg_select_list .egg_select_item { padding: 0.6rem 1rem; border-bottom: 0.1rem solid #eee; cursor: pointer; color: #333; transition: 300ms ease; text-align: center;}.egg_select_list .egg_select_item.selected { font-weight: bold; background: #f6f6f6;}.egg_select_list .egg_select_item.active { background: #eee;}.egg_select_list .egg_select_item:hover { background: #eee;}'; /** * @description 嵌入样式 */ GM_addStyle(css); load((href) => href.match(URL_CONFIG.home), () => { // 初始化logo initLogo(); // 页面提示 log('进入主页面!'); // 初始化主题 initThemeColor(); // 初始化任务配置 initTaskConfig(); // 初始化设置 initSettings(); // 设置字体 initFontSize(); // 初始化主页面 initMainListener(); // 初始化提示 renderTip(); // 渲染面板 renderPanel(); // 渲染窗口 renderFrame(); }); load((href) => href === GM_getValue('readingUrl'), async () => { // 页面提示 log('进入文章选读页面!'); // 初始化主题 initThemeColor(); // 初始化设置 initSettings(); // 设置字体 initFontSize(); // 最大阅读 initMaxRead(); // 初始化子页面 initChildListener(); // 初始化提示 renderTip(); try { // 处理文章 await handleNews(); } catch (err) { if (err instanceof Error) { // 提示 createTip(err.message); // 错误 error(err.message); return; } // 提示 createTip(String(err)); // 错误 error(err); } }); load((href) => href === GM_getValue('watchingUrl'), async () => { // 页面提示 log('进入视听学习页面!'); // 初始化主题 initThemeColor(); // 初始化设置 initSettings(); // 设置字体 initFontSize(); // 最大视听 initMaxWatch(); // 初始化子页面 initChildListener(); // 初始化提示 renderTip(); try { // 处理视频 await handleVideo(); } catch (err) { if (err instanceof Error) { // 提示 createTip(err.message); // 错误 error(err.message); return; } // 提示 createTip(String(err)); // 错误 error(err); } }); load((href) => href === URL_CONFIG.examPractice, async () => { // 页面提示 log('进入每日答题页面!'); // 初始化主题 initThemeColor(); // 初始化设置 initSettings(); // 设置字体 initFontSize(); // 初始化子页面 initChildListener(); // 初始化提示 renderTip(); // 创建答题按钮 await renderExamBtn(); try { // 开始答题 await doingExam(ExamType.PRACTICE); } catch (err) { if (err instanceof Error) { // 提示 createTip(err.message); // 错误 error(err.message); return; } // 提示 createTip(String(err)); // 错误 error(err); } }); load((href) => href.includes(URL_CONFIG.examPaper), async () => { // 页面提示 log('进入专项练习页面!'); // 初始化主题 initThemeColor(); // 初始化设置 initSettings(); // 设置字体 initFontSize(); // 初始化子页面 initChildListener(); // 初始化提示 renderTip(); // 创建答题按钮 await renderExamBtn(); // 开始答题 doingExam(ExamType.PAPER); return; }); /** * @description 初始化logo */ function initLogo() { console.log(`%c tech-study.js %c ${version} `, 'background:dodgerblue;color:white;font-size:15px;border-radius:4px 0 0 4px;padding:2px 0;', 'background:black;color:gold;font-size:15px;border-radius:0 4px 4px 0;padding:2px 0;'); } /** * @description 初始化配置 */ function initTaskConfig() { try { const taskTemp = JSON.parse(GM_getValue('taskConfig')); if (taskTemp && Array.isArray(taskTemp)) { if (taskTemp.length === taskConfig.length) { taskConfig.forEach((task, i) => { task.active = taskTemp[i].active; }); } } // 监听值变化 GM_addValueChangeListener('taskConfig', (key, oldVal, newVal, remote) => { if (remote) { const taskTemp = JSON.parse(newVal); if (taskTemp && Array.isArray(taskTemp)) { if (taskTemp.length === taskConfig.length) { taskConfig.forEach((task, i) => { task.active = taskTemp[i].active; }); } } } }); } catch (e) { } } /** * @description 初始化配置 */ function initSettings() { try { const settingsTemp = JSON.parse(GM_getValue('studySettings')); if (settingsTemp && Array.isArray(settingsTemp)) { if (settingsTemp.length === settings.length) { for (const i in settingsTemp) { settings[i] = settingsTemp[i]; } } } // 监听值变化 GM_addValueChangeListener('studySettings', (key, oldVal, newVal, remote) => { if (remote) { const settingsTemp = JSON.parse(newVal); if (settingsTemp && Array.isArray(settingsTemp)) { if (settingsTemp.length === settings.length) { for (const i in settingsTemp) { settings[i] = settingsTemp[i]; } } } } }); } catch (e) { } } /** * @description 初始化配置 */ function initFontSize() { // 移动端 const moblie = hasMobile(); if (moblie) { // 清除缩放 const meta = $$('meta[name=viewport]')[0]; if (meta) { meta.content = 'initial-scale=0, user-scalable=yes'; } // 缩放比例 const scale = ~~(window.innerWidth / window.outerWidth) || 1; document.documentElement.style.setProperty('--scale', String(scale)); } } /** * @description 初始化最大阅读时长 */ function initMaxRead() { try { const maxReadTemp = GM_getValue('maxRead'); if (maxReadTemp) { maxRead.value = maxReadTemp; } } catch (error) { } } /** * @description 初始化最大视听时长 */ function initMaxWatch() { try { const maxWatchTemp = GM_getValue('maxWatch'); if (maxWatchTemp) { maxWatch.value = maxWatchTemp; } } catch (error) { } } /** * @description 初始化主题色 */ function initThemeColor() { try { // 监听主题变化 watch(themeColor, () => { // 设置主题 document.documentElement.style.setProperty('--themeColor', themeColor.value); }); // 主题色 const themeColorTemp = GM_getValue('themeColor'); if (themeColorTemp) { themeColor.value = themeColorTemp; } // 监听值变化 GM_addValueChangeListener('themeColor', (key, oldVal, newVal, remote) => { if (remote) { // 主题色 const themeColorTemp = newVal; if (themeColorTemp) { themeColor.value = themeColorTemp; } } }); } catch (error) { } } /** * @description 渲染提示 */ function renderTip() { const tipWrap = createElementNode('div', undefined, { class: 'egg_tip_wrap', onclick(e) { e.stopPropagation(); }, onmousedown(e) { e.stopPropagation(); }, onmousemove(e) { e.stopPropagation(); }, onmouseup(e) { e.stopPropagation(); }, onmouseenter(e) { e.stopPropagation(); }, onmouseleave(e) { e.stopPropagation(); }, onmouseover(e) { e.stopPropagation(); }, ontouchstart(e) { e.stopPropagation(); }, ontouchmove(e) { e.stopPropagation(); }, ontouchend(e) { e.stopPropagation(); }, oninput(e) { e.stopPropagation(); }, onchange(e) { e.stopPropagation(); }, onblur(e) { e.stopPropagation(); }, }); mountElement(tipWrap); } /** * @description 渲染答题按钮 */ async function renderExamBtn() { const titles = await $_('.title'); if (titles.length) { // 插入节点 titles[0].parentNode?.insertBefore(ExamBtn().ele, titles[0].nextSibling); } } /** * @description 渲染面板 * @returns */ async function renderPanel() { // 面板 const panel = Panel(); // 插入节点 mountElement(panel); } /** * @description 渲染窗口 */ function renderFrame() { // 窗口 const frame = Frame(); // 插入节点 mountElement(frame); } /* 答案 API */ /** * @description 获取答案 */ async function getAnswer(question) { // 数据 const data = { txt_name: md5(question), password: '', }; try { const params = new URLSearchParams(data); // 请求 const res = await fetch(API_CONFIG.answerSearch, { method: 'POST', mode: 'cors', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: params.toString(), }); // 请求成功 if (res.ok) { const result = await res.json(); const { data, status } = result; if (status !== 0) { // 答案列表 const answerList = JSON.parse(data.txt_content); // 答案 const answers = answerList[0].content.split(/[;\s]/); return answers; } } } catch (error) { } return []; } /** * @description 保存答案 */ async function saveAnswer(question, answer) { try { // 内容 const content = JSON.stringify([{ title: md5(question), content: answer }]); // 数据 const data = { txt_name: md5(question), txt_content: content, password: '', v_id: '', }; const params = new URLSearchParams(data); // 请求 const res = await fetch(API_CONFIG.answerSave, { method: 'POST', mode: 'cors', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: params.toString(), }); // 请求成功 if (res.ok) { const data = await res.json(); return data; } } catch (error) { } } /* 数据 API */ /** * @description 获取新闻数据 */ async function getNewsList() { // 随机 const randNum = ~~(Math.random() * API_CONFIG.todayNews.length); try { // 获取重要新闻 const res = await fetch(API_CONFIG.todayNews[randNum], { method: 'GET', }); // 请求成功 if (res.ok) { const data = await res.json(); return data; } } catch (err) { } } /** * @description 获取视频数据 */ async function getVideoList() { // 随机 const randNum = ~~(Math.random() * API_CONFIG.todayVideos.length); try { // 获取重要新闻 const res = await fetch(API_CONFIG.todayVideos[randNum], { method: 'GET', }); // 请求成功 if (res.ok) { const data = await res.json(); return data; } } catch (err) { } } /** * @description 专项练习数据 */ async function getExamPaper(pageNo) { // 链接 const url = `${API_CONFIG.paperList}?pageSize=50&pageNo=${pageNo}`; try { // 获取专项练习 const res = await fetch(url, { method: 'GET', credentials: 'include', }); // 请求成功 if (res.ok) { const data = await res.json(); const paperJson = decodeURIComponent(escape(window.atob(data.data_str.replace(/-/g, '+').replace(/_/g, '/')))); // JSON格式化 const paper = JSON.parse(paperJson); return paper; } } catch (err) { return []; } return []; } /** * @description 生成二维码 */ async function generateQRCode() { try { // 推送 const res = await fetch(API_CONFIG.generateQRCode, { method: 'GET', mode: 'cors', }); // 请求成功 if (res.ok) { const data = await res.json(); if (data.success) { return data.result; } } } catch (error) { } } /** * @description 用二维码登录 */ async function loginWithQRCode(qrCode) { try { const params = new URLSearchParams({ qrCode, goto: 'https://oa.xuexi.cn', pdmToken: '', }); // 推送 const res = await fetch(API_CONFIG.loginWithQRCode, { method: 'POST', mode: 'cors', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', }, body: params.toString(), }); // 请求成功 if (res.ok) { const data = await res.json(); return data; } } catch (error) { } } /** * @description 签名 */ async function getSign() { try { // 推送 const res = await fetch(API_CONFIG.sign, { method: 'GET', mode: 'cors', credentials: 'include', }); // 请求成功 if (res.ok) { const data = await res.json(); if (data.ok) { return data.data.sign; } } } catch (error) { } } /** * @description 安全检查 * @param data */ async function secureCheck(data) { try { const params = new URLSearchParams(data); const url = `${API_CONFIG.secureCheck}?${params}`; // 推送 const res = await fetch(url, { method: 'GET', mode: 'cors', credentials: 'include', }); // 请求成功 if (res.ok) { const data = await res.json(); return data.success; } } catch (error) { } return false; } /* 推送 API */ /** * @description 推送 */ async function pushPlus(token, title, content, template, toToken) { try { // 参数体 const body = { token, title, content, template, }; // 好友令牌 if (toToken) { body.to = toToken; } // 推送 const res = await fetch(API_CONFIG.push, { method: 'POST', mode: 'cors', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); // 请求成功 if (res.ok) { const data = await res.json(); return data; } } catch (error) { } } /* 用户 API */ /** * @description 获取用户信息 */ async function getUserInfo() { try { const res = await fetch(API_CONFIG.userInfo, { method: 'GET', credentials: 'include', }); // 请求成功 if (res.ok) { const { data } = await res.json(); return data; } } catch (err) { } } /** * @description 获取总积分 */ async function getTotalScore() { try { const res = await fetch(API_CONFIG.totalScore, { method: 'GET', credentials: 'include', }); // 请求成功 if (res.ok) { const { data } = await res.json(); // 总分 const { score } = data; return score; } } catch (err) { } } /** * @description 获取当天总积分 */ async function getTodayScore() { try { const res = await fetch(API_CONFIG.todayScore, { method: 'GET', credentials: 'include', }); // 请求成功 if (res.ok) { const { data } = await res.json(); // 当天总分 const { score } = data; return score; } } catch (err) { } } /** * @description 获取任务列表 */ async function getTaskList() { try { const res = await fetch(API_CONFIG.taskList, { method: 'GET', credentials: 'include', }); // 请求成功 if (res.ok) { const { data } = await res.json(); // 进度和当天总分 const { taskProgress } = data; return taskProgress; } } catch (err) { } } /* task·配置 */ /** * @description 单次最大新闻数 */ const maxNewsNum = 6; /** * @description 单次最大视频数 */ const maxVideoNum = 6; /** * @description 二维码最大刷新次数 */ const maxRefreshCount = 10; /** * @description 二维码自动刷新间隔 */ const autoRefreshQRCodeInterval = 100000; /** * @description url配置 */ const URL_CONFIG = { // 主页正则 home: /^https\:\/\/www\.xuexi\.cn(\/(index\.html)?)?$/, // 主页 homeOrigin: 'https://www.xuexi.cn', // 每日答题页面 examPractice: 'https://pc.xuexi.cn/points/exam-practice.html', // 专项练习页面 examPaper: 'https://pc.xuexi.cn/points/exam-paper-detail.html', }; /** * @description api配置 */ const API_CONFIG = { // 用户信息 userInfo: 'https://pc-api.xuexi.cn/open/api/user/info', // 总分 totalScore: 'https://pc-proxy-api.xuexi.cn/delegate/score/get', // 当天分数 todayScore: 'https://pc-proxy-api.xuexi.cn/delegate/score/today/query', // 任务列表 taskList: 'https://pc-proxy-api.xuexi.cn/delegate/score/days/listScoreProgress?sence=score&deviceType=2', // 新闻数据 todayNews: [ 'https://www.xuexi.cn/lgdata/35il6fpn0ohq.json', 'https://www.xuexi.cn/lgdata/1ap1igfgdn2.json', 'https://www.xuexi.cn/lgdata/vdppiu92n1.json', 'https://www.xuexi.cn/lgdata/152mdtl3qn1.json', ], // 视频数据 todayVideos: [ 'https://www.xuexi.cn/lgdata/525pi8vcj24p.json', 'https://www.xuexi.cn/lgdata/11vku6vt6rgom.json', 'https://www.xuexi.cn/lgdata/2qfjjjrprmdh.json', 'https://www.xuexi.cn/lgdata/3o3ufqgl8rsn.json', 'https://www.xuexi.cn/lgdata/591ht3bc22pi.json', 'https://www.xuexi.cn/lgdata/1742g60067k.json', 'https://www.xuexi.cn/lgdata/1novbsbi47k.json', ], // 专项练习列表 paperList: 'https://pc-proxy-api.xuexi.cn/api/exam/service/paper/pc/list', // 文本服务器保存答案 answerSave: 'https://a6.qikekeji.com/txt/data/save', // 文本服务器获取答案 answerSearch: 'https://a6.qikekeji.com/txt/data/detail', // 推送 push: 'https://www.pushplus.plus/send', // 生成二维码 generateQRCode: 'https://login.xuexi.cn/user/qrcode/generate', //二维码登录 loginWithQRCode: 'https://login.xuexi.cn/login/login_with_qr', // 签名 sign: 'https://pc-api.xuexi.cn/open/api/sns/sign', // 安全检查 secureCheck: 'https://pc-api.xuexi.cn/login/secure_check', // 二维码 qrcode: 'https://api.qrserver.com/v1/create-qr-code', }; /** * @description 版本号 */ const version = '1.7.5'; /** * @description 任务类型 */ var TaskType; (function (TaskType) { TaskType[TaskType["LOGIN"] = 0] = "LOGIN"; TaskType[TaskType["READ"] = 1] = "READ"; TaskType[TaskType["WATCH"] = 2] = "WATCH"; TaskType[TaskType["PRACTICE"] = 3] = "PRACTICE"; })(TaskType || (TaskType = {})); /** * @description 设置类型 */ var SettingType; (function (SettingType) { SettingType[SettingType["AUTO_START"] = 0] = "AUTO_START"; SettingType[SettingType["SAME_TAB"] = 1] = "SAME_TAB"; SettingType[SettingType["SILENT_RUN"] = 2] = "SILENT_RUN"; SettingType[SettingType["SCHEDULE_RUN"] = 3] = "SCHEDULE_RUN"; SettingType[SettingType["VIDEO_MUTED"] = 4] = "VIDEO_MUTED"; SettingType[SettingType["RANDOM_EXAM"] = 5] = "RANDOM_EXAM"; SettingType[SettingType["AUTO_ANSWER"] = 6] = "AUTO_ANSWER"; SettingType[SettingType["REMOTE_PUSH"] = 7] = "REMOTE_PUSH"; })(SettingType || (SettingType = {})); /** * @description 进度类型 */ var TaskStatusType; (function (TaskStatusType) { TaskStatusType[TaskStatusType["LOADING"] = 0] = "LOADING"; TaskStatusType[TaskStatusType["LOADED"] = 1] = "LOADED"; TaskStatusType[TaskStatusType["START"] = 2] = "START"; TaskStatusType[TaskStatusType["PAUSE"] = 3] = "PAUSE"; TaskStatusType[TaskStatusType["FINISH"] = 4] = "FINISH"; })(TaskStatusType || (TaskStatusType = {})); // 当前订阅 let currentSub; // 订阅 const subscription = new WeakMap(); /** * @description Proxy Map */ const proxyMap = new WeakMap(); /** * @description 收集 Ref 依赖 * @param target * @param key */ const trackRef = (target) => { // 当前订阅 if (!currentSub) { return; } // target 订阅列表 let subList = subscription.get(target); // 不存在订阅列表 if (!subList) { subList = new Map(); // 键订阅 const subkeyList = new Set(); // 添加订阅 subkeyList.add(currentSub); subList.set('value', subkeyList); subscription.set(target, subList); return; } // 键订阅 let subkeyList = subList.get('value'); if (!subkeyList) { // 键订阅 subkeyList = new Set(); // 添加订阅 subkeyList.add(currentSub); subList.set('value', subkeyList); subscription.set(target, subList); return; } // 添加订阅 subkeyList.add(currentSub); }; /** * @description 通知 Ref 订阅 * @param terget * @param key * @returns */ function triggerRef(target, newVal, oldVal) { // target 订阅列表 const subList = subscription.get(target); if (!subList) { return; } // 键订阅 let subkeyList = subList.get('value'); if (!subkeyList) { return; } // 通知订阅 for (const fn of subkeyList) { if (fn instanceof Function) { fn(newVal, oldVal); } } } /** * @description 收集依赖 * @param target * @param key */ const track = (target, key) => { // 当前订阅 if (!currentSub) { return; } // proxy const proxyTarget = proxyMap.get(target); if (!proxyTarget) { return; } // target 订阅列表 let subList = subscription.get(target); // 不存在订阅列表 if (!subList) { subList = new Map(); // 键订阅 const subkeyList = new Set(); // 添加订阅 subkeyList.add(currentSub); subList.set(key, subkeyList); subscription.set(target, subList); return; } // 键订阅 let subkeyList = subList.get(key); if (!subkeyList) { // 键订阅 subkeyList = new Set(); // 添加订阅 subkeyList.add(currentSub); subList.set(key, subkeyList); subscription.set(target, subList); return; } // 添加订阅 subkeyList.add(currentSub); }; /** * @description 通知订阅 * @param terget * @param key * @returns */ function trigger(target, key, newVal, oldVal) { // proxy const proxyTarget = proxyMap.get(target); if (!proxyTarget) { return; } // proxyTarget 订阅列表 const subList = subscription.get(target); if (!subList) { return; } // 键订阅 let subkeyList = subList.get(key); if (!subkeyList) { return; } // 通知订阅 for (const fn of subkeyList) { fn(newVal, oldVal); } } /** * @description 只读键 */ var ReactiveFlags; (function (ReactiveFlags) { ReactiveFlags["IS_REF"] = "_isRef"; ReactiveFlags["IS_SHALLOW"] = "_isShallow"; ReactiveFlags["IS_REACTIVE"] = "_isReactive"; ReactiveFlags["IS_READONLY"] = "_isReadonly"; })(ReactiveFlags || (ReactiveFlags = {})); /** * @description Ref */ class Ref { _isShallow = false; _isRef = true; _value; value; constructor(val, shallow = false) { const _this = this; this._isShallow = shallow; if (val && typeof val === 'object' && shallow) { const reactiveVal = reactive(val); this._value = reactiveVal; this.value = reactiveVal; } else { this._value = val; this.value = val; } // 定义属性 Object.defineProperty(this, 'value', { get() { // 收集依赖 trackRef(this); return _this._value; }, set(newVal) { // 旧数据 const oldVal = this._value; // 数据变化 if (oldVal !== newVal) { // 设置新数据值 _this._value = newVal; // 通知依赖 triggerRef(this, newVal, oldVal); } }, }); } toJSON() { return this._value; } } /** * @description ref * @param v * @returns */ const isRef = (v) => { return !!(v && v[ReactiveFlags.IS_REF]); }; /** * @description 浅层 shallow * @param v * @returns */ const isShallow = (v) => { return !!(v && v[ReactiveFlags.IS_SHALLOW]); }; /** * @description 创建 ref * @param v * @returns */ const createRef = (rawVal, shallow) => { return new Ref(rawVal, shallow); }; /** * @description 解除 ref * @param val * @returns */ const unref = (val) => { return (isRef(val) ? val.value : val); }; /** * @description 顶层 ref * @param v * @returns */ const ref = (value) => { return isRef(value) ? value : createRef(value, true); }; /** * @description ref * @param value * @returns */ const shallowRef = (value) => { return isRef(value) ? value : createRef(value, false); }; /** * @description 创建处理 reactive * @param isReadonly * @param isShallow * @returns */ const createReactiveHandlers = (isReadonly, isShallow) => { return { get: createGetters(isReadonly, isShallow), set: createSetters(isReadonly, isShallow), }; }; /** * @description getters * @param isReadonly * @param isShallow * @returns */ const createGetters = (isReadonly, isShallow) => { return function get(target, key, receiver) { if (key === ReactiveFlags.IS_REACTIVE) { return !isReadonly; } if (key === ReactiveFlags.IS_READONLY) { return isReadonly; } if (key === ReactiveFlags.IS_SHALLOW) { return isShallow; } // 结果 const res = Reflect.get(target, key, receiver); if (!isReadonly) { // 收集依赖 track(target, key); } if (isShallow) { return res; } if (isRef(res)) { return res.value; } if (res && typeof res === 'object') { if (res instanceof Element) { return res; } return isReadonly ? readonly(res) : reactive(res); } return res; }; }; /** * @description setters * @param readonly * @param shallow * @returns */ const createSetters = (readonly, shallow) => { return function set(target, key, newVal, receiver) { // 只读 if (readonly) { return false; } // 旧值 const oldVal = target[key]; if (isReadonly(oldVal) && isRef(oldVal) && !isRef(newVal)) { return false; } if (!shallow) { if (isRef(oldVal) && !isRef(newVal)) { oldVal.value = newVal; return true; } } const res = Reflect.set(target, key, newVal, receiver); // length if (Array.isArray(target) && key === 'length') { // 通知依赖 trigger(target, key, newVal, oldVal); return res; } // 数据变化 if (oldVal !== newVal) { // 通知依赖 trigger(target, key, newVal, oldVal); } return res; }; }; /** * @description reactive object */ const createReactiveObj = (target, isReadonly, shallow) => { // 存在 Proxy const existingProxy = proxyMap.get(target); if (existingProxy) { return existingProxy; } // 新建 const proxy = new Proxy(target, createReactiveHandlers(isReadonly, shallow)); proxyMap.set(target, proxy); return proxy; }; /** * @description reactive * @param val * @returns */ const isReactive = (val) => { return !!(val && val[ReactiveFlags.IS_REACTIVE]); }; /** * @description 创建 reactive * @param target * @returns */ const createReactive = (target) => { return createReactiveObj(target, false, false); }; /** * @description 顶层 reactive * @param target * @returns */ const shallowReactive = (target) => { return createReactiveObj(target, false, true); }; /** * @description reactive * @param val * @returns */ const isReadonly = (val) => { return !!(val && val[ReactiveFlags.IS_READONLY]); }; /** * @description 创建 readonly * @param target * @returns */ const createReadonly = (target) => { return createReactiveObj(target, true, false); }; /** * @description 顶层 readonly * @param target * @returns */ const shallowReadonly = (target) => { return createReactiveObj(target, true, true); }; /** * @description proxy * @param val * @returns */ const isProxy = (val) => { return isReactive(val) || isReadonly(val); }; /** * @description reactive * @param target * @returns */ const reactive = (target) => { return createReactive(target); }; /** * @description readonly * @param target * @returns */ const readonly = (target) => { return createReadonly(target); }; /** * @description 监听数据变化 * @param source * @param callback */ const watch = (source, callback, immediate = false) => { // 立刻执行 immediate && callback(unref(source), unref(source)); // array if (Array.isArray(source) && source.every((s) => isRef(s))) { for (const i in source) { // Proxy if (isProxy(source[i])) { watch(source[i], () => { const res = source.map((s) => unref(s)); callback(res, res); }); } } watch(() => source.map((s) => unref(s)), callback); return; } // function if (source instanceof Function) { watch(watchEffectRef(source), (n, o) => { callback(unref(n), unref(o)); }); return; } // Proxy if (isProxy(source)) { for (const key in source) { currentSub = () => { callback(source, source); }; // sub source const subSource = source[key]; currentSub = undefined; watch(subSource, () => { callback(source, source); }); } return; } // Ref if (isRef(source)) { // Ref.value Proxy if (isProxy(source.value)) { watch(source.value, () => { callback(unref(source), unref(source)); }); } currentSub = callback; // 收集依赖 trackRef(source); currentSub = undefined; return; } }; /** * @description 监听数据变化影响 * @param callback * @returns */ const watchEffect = (callback) => { currentSub = callback; // 收集依赖 callback(); currentSub = undefined; }; /** * @description 监听影响 ref * @param refVal * @param callback * @returns */ const watchRef = (source, callback) => { // 收集依赖 const effectRes = shallowRef(callback()); // 监听 watch(source, () => (effectRes.value = unref(callback()))); return effectRes; }; /** * @description 监听影响 ref * @param refVal * @param callback * @returns */ const watchEffectRef = (callback) => { // 收集依赖 const effectRes = shallowRef(undefined); // 监听 watchEffect(() => (effectRes.value = unref(callback()))); return effectRes; }; /** * @description 创建元素节点 * @param eleName * @param props * @param attrs * @param children * @returns */ function createElementNode(tagName, props, attrs, children, options) { // 挂载状态 let beforemount = ref(false); // 挂载状态 let mounted = ref(false); const { onCreated, beforeCreat, onMounted, beforeMount } = options || {}; // 订阅 const subscribe = (e) => { const { onMounted, beforeMount } = e; if (beforeMount) { watch(beforemount, () => { if (beforemount.value) { beforeMount(); return; } }, true); } if (onMounted) { watch(mounted, () => { if (mounted.value) { onMounted(); return; } }, true); } }; // 取消订阅 const unsubscribe = (e) => { //懒得写 }; // 创建元素前 beforeCreat && beforeCreat(); // 创建普通元素 const ele = document.createElement(tagName); // 处理属性 handleProps(ele, props); // 处理属性 handleAttributes(ele, attrs, subscribe, unsubscribe); // 处理子元素 handleChildren(ele, children, subscribe, unsubscribe); // 收集挂载前 const collectBeforeMount = () => { beforemount.value = true; beforeMount && beforeMount(); }; // 收集挂载 const collectOnMounted = () => { mounted.value = true; onMounted && onMounted(); }; // 创建元素后 onCreated && onCreated(); return { ele, beforeMount: collectBeforeMount, onMounted: collectOnMounted }; } /** * @description 创建svg元素 * @param tagName * @param props * @param attrs * @param children * @returns */ function createNSElementNode(tagName, props, attrs, children, options) { // 挂载状态 let beforemount = ref(false); // 挂载状态 let mounted = ref(false); const { onCreated, beforeCreat, onMounted, beforeMount } = options || {}; // 订阅 const subscribe = (e) => { const { onMounted, beforeMount } = e; if (beforeMount) { watch(beforemount, () => { if (beforemount.value) { beforeMount(); return; } }, true); } if (onMounted) { watch(mounted, () => { if (mounted.value) { onMounted(); return; } }, true); } }; // 取消订阅 const unsubscribe = (e) => { //懒得写 }; // 创建元素前 beforeCreat && beforeCreat(); // svg元素命名空间 const ns = 'http://www.w3.org/2000/svg'; // 创建svg元素 const ele = document.createElementNS(ns, tagName); // 处理属性 handleProps(ele, props); // 处理属性 handleAttributes(ele, attrs, subscribe, unsubscribe); // 处理子元素 handleChildren(ele, children, subscribe, unsubscribe); // 收集挂载前 const collectBeforeMount = () => { beforemount.value = true; beforeMount && beforeMount(); }; // 收集挂载 const collectOnMounted = () => { mounted.value = true; onMounted && onMounted(); }; // 创建元素后 onCreated && onCreated(); return { ele, beforeMount: collectBeforeMount, onMounted: collectOnMounted }; } /** * @description 处理属性 * @param ele * @param props */ function handleProps(ele, props) { // props属性设置 for (const key in props) { // Ref 属性 if (isRef(props[key])) { const refVal = props[key]; watchEffect(() => (ele[key] = refVal.value)); continue; } ele[key] = props[key]; } } /** * @description 处理svg属性 * @param ele * @param attrs */ function handleAttributes(ele, attrs, subscribe, unsubscribe) { // 属性存在 if (attrs) { // attrs属性设置 for (const key in attrs) { // 处理普通属性 handleAttribute(ele, key, attrs[key], subscribe, unsubscribe); } } } /** * @description 处理事件选项 */ function handleEventOptions(option) { if (option.length) { const options = { capture: option.includes('capture'), once: option.includes('once'), passive: option.includes('passive'), }; return options; } } /** * @description 处理属性 * @param ele * @param key * @param value */ function handleAttribute(ele, key, value, subscribe, unsubscribe) { // 处理完的key const formatKey = key.toLowerCase(); // 事件绑定 if (formatKey.startsWith('on')) { // 事件监听 const [event] = formatKey.match(/(?<=on).*/); // 事件类型 if (event) { const [eventType, ...option] = event.split('_'); const options = handleEventOptions(option); // Ref 函数 if (isRef(value)) { const refVal = value; const refListener = watchRef(refVal, () => refVal.value ? (e) => { option.includes('prevent') && e.preventDefault(); option.includes('stop') && e.stopPropagation(); const callback = refVal.value; callback(e); } : undefined); // 设置事件监听 refListener.value && ele.addEventListener(eventType, refListener.value, options); // 监听事件变化 watch(refListener, (newVal, oldVal) => { // 移除旧事件监听 oldVal && ele.removeEventListener(eventType, oldVal); // 设置新事件监听 newVal && ele.addEventListener(eventType, newVal, options); }); return; } // 普通函数 if (value instanceof Function) { // 设置事件监听 ele.addEventListener(eventType, value, options); } } return; } // 特殊属性 const specificAttrs = ['checked', 'selected', 'disabled', 'enabled']; // 特殊 key if (specificAttrs.includes(formatKey)) { // Ref if (isRef(value)) { const refVal = value; watchEffect(() => { if (refVal.value) { ele.setAttribute(formatKey, ''); } else { ele.removeAttribute(formatKey); } }); return; } // 普通属性值 if (value) { ele.setAttribute(formatKey, ''); } else { ele.removeAttribute(formatKey); } return; } // ref 属性名 if (key === 'ref') { // Ref if (isRef(value)) { const refVal = value; subscribe && subscribe({ onMounted() { refVal.value = ele; }, }); return; } // Ref 函数 if (value instanceof Function) { const refFn = value; subscribe && subscribe({ onMounted() { refFn(ele); }, }); return; } return; } // xlink命名空间 if (key.startsWith('xlink:')) { // xlink属性命名空间 const attrNS = 'http://www.w3.org/1999/xlink'; if (value) { ele.setAttributeNS(attrNS, key, value); } else { ele.removeAttributeNS(attrNS, key); } return; } // Ref 属性值 if (key && isRef(value)) { const refVal = value; // 监听影响 watchEffect(() => { ele.setAttribute(key, refVal.value); }); return; } // 普通属性 if (key) { // 普通属性 ele.setAttribute(key, value); } } /** * @description 处理子元素 * @param ele * @param children */ function handleChildren(ele, children, subscribe, unsubscribe) { // Ref if (isRef(children)) { // 注释元素 const comment = document.createComment(''); // 监听元素变化 watch(children, async (newEle, oldEle) => { if (!newEle && oldEle) { // Promise if (oldEle instanceof Promise) { const oldEleRes = await oldEle; if (oldEleRes) { oldEleRes.forEach((ele) => { unsubscribe && unsubscribe(ele); }); } } // unPromise if (!(oldEle instanceof Promise)) { oldEle.forEach((ele) => { unsubscribe && unsubscribe(ele); }); } ele.replaceChildren(comment); return; } if (newEle) { if (oldEle) { // Promise if (oldEle instanceof Promise) { const oldEleRes = await oldEle; if (oldEleRes) { oldEleRes.forEach((ele) => { unsubscribe && unsubscribe(ele); }); } } // unPromise if (!(oldEle instanceof Promise)) { oldEle.forEach((ele) => { unsubscribe && unsubscribe(ele); }); } } // Promise if (newEle instanceof Promise) { const newEleRes = await newEle; if (newEleRes) { const eles = newEleRes.map((v) => { if (v.beforeMount || v.onMounted) { subscribe && subscribe(v); } return v.ele; }); ele.replaceChildren(createElementBlock(eles)); } return; } // unPromise const eles = newEle.map((v) => { if (v.beforeMount || v.onMounted) { subscribe && subscribe(v); } return v.ele; }); ele.replaceChildren(createElementBlock(eles)); return; } }); // Promise if (children.value instanceof Promise) { // 插入注释元素 ele.appendChild(comment); children.value.then((childrenEle) => { if (childrenEle) { const eles = childrenEle.map((v) => { if (v.beforeMount || v.onMounted) { subscribe && subscribe(v); } return v.ele; }); ele.replaceChildren(createElementBlock(eles)); } }); return; } // unPromise if (children.value) { const eles = children.value.map((v) => { if (v.beforeMount || v.onMounted) { subscribe && subscribe(v); } return v.ele; }); ele.appendChild(createElementBlock(eles)); return; } // 插入元素 ele.appendChild(comment); return; } // Promise if (children instanceof Promise) { // 注释元素 const comment = document.createComment(''); // 插入注释元素 ele.appendChild(comment); // 异步替换元素 children.then((childEle) => { if (childEle) { const { beforeMount, onMounted } = childEle; if (beforeMount || onMounted) { subscribe && subscribe(childEle); } comment.replaceWith(childEle.ele); } }); return; } // Array if (Array.isArray(children)) { // 处理过后 const resChildren = []; for (const i in children) { const child = children[i]; // Ref if (isRef(child)) { // 注释 const comment = document.createComment(''); // 监听影响 watch(child, async (newEle, oldEle) => { // 新元素为空 if (!newEle && oldEle) { // Promise if (oldEle instanceof Promise) { const oldEleRes = await oldEle; if (oldEleRes) { handleChangeElement(newEle, oldEleRes, comment, subscribe, unsubscribe); } return; } handleChangeElement(newEle, oldEle, comment, subscribe, unsubscribe); return; } // 旧元素为空 if (newEle && !oldEle) { // Promise if (newEle instanceof Promise) { const newEleRes = await newEle; if (newEleRes) { handleChangeElement(newEleRes, oldEle, comment, subscribe, unsubscribe); } return; } handleChangeElement(newEle, oldEle, comment, subscribe, unsubscribe); return; } // 存在 if (newEle && oldEle) { // Promise if (newEle instanceof Promise && oldEle instanceof Promise) { const newEleRes = await newEle; const oldEleRes = await oldEle; // 处理元素变化 handleChangeElement(newEleRes, oldEleRes, comment, subscribe, unsubscribe); return; } // Promise if (newEle instanceof Promise && !(oldEle instanceof Promise)) { const newEleRes = await newEle; // 处理元素变化 handleChangeElement(newEleRes, oldEle, comment, subscribe, unsubscribe); return; } // Promise if (!(newEle instanceof Promise) && oldEle instanceof Promise) { const oldEleRes = await oldEle; // 处理元素变化 handleChangeElement(newEle, oldEleRes, comment, subscribe, unsubscribe); return; } // 非 Promise if (!(oldEle instanceof Promise) && !(newEle instanceof Promise)) { // 处理元素变化 handleChangeElement(newEle, oldEle, comment, subscribe, unsubscribe); return; } } }); // Promise if (child.value instanceof Promise) { // 注释 resChildren[i] = { ele: comment }; // 异步替换 child.value.then((childEle) => { if (childEle) { const { beforeMount, onMounted } = childEle; if (beforeMount || onMounted) { subscribe && subscribe(childEle); } comment.replaceWith(childEle.ele); } }); continue; } // unPromise if (child.value) { const { beforeMount, onMounted, ele } = child.value; resChildren[i] = { ele, beforeMount, onMounted }; continue; } resChildren[i] = { ele: comment }; continue; } // Promise if (child instanceof Promise) { // 注释 const comment = document.createComment(''); resChildren[i] = { ele: comment }; // 异步替换元素 child.then((childEle) => { if (childEle) { const { beforeMount, onMounted } = childEle; if (beforeMount || onMounted) { subscribe && subscribe(childEle); } comment.replaceWith(childEle.ele); } }); continue; } // 普通元素 if (child) { const { beforeMount, onMounted, ele } = child; resChildren[i] = { ele, beforeMount, onMounted }; } } const eles = resChildren.map((v) => { if (v.beforeMount || v.onMounted) { subscribe && subscribe(v); } return v.ele; }); // 插入元素 ele.appendChild(createElementBlock(eles)); return; } // 普通元素 if (children) { const { beforeMount, onMounted } = children; if (beforeMount || onMounted) { subscribe && subscribe(children); } // 插入元素 ele.appendChild(children.ele); return; } return; } /** * @description 元素变化 * @param newEle * @param oldEle * @param comment */ function handleChangeElement(newEle, oldEle, comment, subscribe, unsubscribe) { if (newEle && oldEle) { const { beforeMount, onMounted } = newEle; if (beforeMount || onMounted) { subscribe && subscribe(newEle); } oldEle.ele.replaceWith(newEle.ele); return; } if (newEle && !oldEle) { const { beforeMount, onMounted } = newEle; if (beforeMount || onMounted) { subscribe && subscribe(newEle); } comment.replaceWith(newEle.ele); return; } if (!newEle && oldEle) { unsubscribe && unsubscribe(oldEle); oldEle.ele.replaceWith(comment); return; } } /** * @description 创建文字节点 * @param text * @returns */ function createTextNode(text, options) { const { onCreated, beforeCreat, onMounted, beforeMount } = options || {}; // 创建元素前 beforeCreat && beforeCreat(); // Ref if (isRef(text)) { // ref const refVal = text; // 元素 const ele = document.createTextNode(''); // 订阅变化 watchEffect(() => { ele.data = refVal.value; }); // 创建元素后 onCreated && onCreated(); return { ele, beforeMount, onMounted }; } // 创建元素后 onCreated && onCreated(); return { ele: document.createTextNode(String(text)), beforeMount, onMounted }; } /** * @description 挂载元素 * @param eleOptions * @param parent */ function mountElement(eleOptions, parent = document.body) { const { ele, beforeMount, onMounted } = eleOptions; if (ele) { // 触发挂载前事件 beforeMount && beforeMount(); parent.appendChild(ele); // 挂在后 onMounted && onMounted(); } } /** * @description 选择器 * @param selector * @returns */ function $$(selector, parent = document) { return Array.from(parent.querySelectorAll(selector)); } /** * @description 异步选择器 * @param selector * @returns */ function $_(selector, parent = document, timeout) { return new Promise((resolve) => { const timer = setInterval(() => { const selectors = Array.from(parent.querySelectorAll(selector)); // 存在元素 if (selectors.length) { clearInterval(timer); resolve(selectors); } }, 10); // 超时 if (timeout) { setTimeout(() => { clearInterval(timer); resolve([]); }, timeout); } }); } /** * @description 创建元素块 * @param eles * @returns */ function createElementBlock(eles) { const fragment = document.createDocumentFragment(); for (const i in eles) { fragment.appendChild(eles[i]); } return fragment; } /** * @description 打印日志 * @param text */ function log(...text) { printColor('dodgerblue', ...text); } /** * @description 打印错误 * @param text */ function error(...text) { printColor('red', ...text); } /** * @description 打印信息 * @param text */ function info(...text) { printColor('yellow', ...text); } /** * @description 打印颜色 * @param text * @param color */ function printColor(color, ...text) { const textFormatted = text .map((t) => (typeof t === 'object' ? JSON.stringify(t) : String(t))) .join(' '); console.log(`%c[${formatDateTime()}] %c${textFormatted}`, '', `color: ${color}`); } /** * @description html进度条 * @param title * @param percent * @returns */ function getProgressHTML(title, current, total) { // html const progressHTML = `
${title} ${getHighlightHTML(`${current}`)} / ${total}
`; return progressHTML; } /** * @description html高亮文本 * @param text * @returns */ function getHighlightHTML(text) { // html const highlightHTML = `${text}`; return highlightHTML; } /** * @description 二维码 * @param src */ function getImgHTML(src) { // 图片 return `
`; } /** * @description 创建模态框 * @param options 选项 * @returns */ function createModal(options) { // 配置 const { title, subTitle = '', to = '用户', content, type, from = 'tech-study.js', } = options; // 内容文本 let contentText = ''; if (Array.isArray(content)) { contentText = content.map((ct) => `
${ct}
`).join(''); } else { contentText = content; } // 日期 const dateTime = formatDateTime(); // 类型html let typeHTML = ''; if (type && type.length) { if (type === 'info') { typeHTML = ` `; } if (type === 'warn') { typeHTML = ` `; } if (type === 'success') { typeHTML = ` `; } if (type === 'fail') { typeHTML = ` `; } } // 类型 const typeWrap = ` ${typeHTML} `; // 基础html const baseHTML = `
${typeWrap} ${title}
${subTitle}
${getHighlightHTML(to)}, 你好!
${contentText}
${dateTime}
来自 ${from}
`; return baseHTML; } /** * @description 推送消息 */ async function pushMessage(options) { // 选项 const { title, content, template, fromToken, toToken } = options; // 推送 const res = await pushPlus(fromToken, title, content, template, toToken); return res; } /** * @description 推送模态框 */ async function pushModal(options, fromToken, toToken) { // html const html = createModal(options); // 推送 const res = await pushMessage({ title: '消息提示', content: html, fromToken, toToken, template: 'html', }); if (res && res.code === 200) { return res; } return; } /** * @description 创建随机点 * @param bounds 范围 * @returns */ function createRandomPoint(bounds) { // 范围 const { x, y, width, height } = bounds; // 横坐标 const randX = x + Math.random() * width * 0.5 + width * 0.25; // 纵坐标 const randY = y + Math.random() * height * 0.5 + height * 0.25; return { x: randX, y: randY, }; } /** * @description 生成随机路径 * @param start * @param end * @param steps * @returns */ function createRandomPath(start, end, steps) { // 最小水平增量 const minDeltaX = (end.x - start.x) / steps; // 最大垂直增量 const maxDeltaY = (end.y - start.y) / steps; const path = []; // 开始节点 path.push(start); // 插入点 for (let i = 0; i < steps; i++) { // 横坐标 const x = path[i].x + Math.random() * 5 + minDeltaX; // 纵坐标 const y = path[i].y + Math.random() * 5 * Math.pow(-1, ~~(Math.random() * 2 + 1)) + maxDeltaY; path.push({ x, y, }); } return path; } /** * @description 随机数字 * @returns */ function generateNumAsChar() { return (~~(Math.random() * 10)).toString(); } /** * @description 随机大写字母 * @returns */ function generateUpperAsChar() { return String.fromCharCode(~~(Math.random() * 26) + 65); } /** * @description 随机小写字母 * @returns */ function generateLowerAsChar() { return String.fromCharCode(~~(Math.random() * 26) + 97); } /** * @description 随机混合字符 * @param length * @returns */ function generateMix(length = 6) { // 随机字符串 const randomText = []; // 生成器 const typeGenerator = [ generateNumAsChar, generateUpperAsChar, generateLowerAsChar, ]; if (length) { for (let i = 0; i < length; i++) { // 随机位置 const randomIndex = ~~(Math.random() * typeGenerator.length); randomText.push(typeGenerator[randomIndex]()); } } return randomText.join(''); } /** * @description 格式化日期时间数字 * @param num * @returns */ function formatDateNum(num) { return num < 10 ? `0${num}` : `${num}`; } /** * @description 格式化日期时间 * @param time * @returns * @example * formatDateTime() -> "2022-09-01 08:00:00" * formatDateTime(new Date()) -> "2022-09-01 08:00:00" * formatDateTime(Date.now()) -> "2022-09-01 08:00:00" */ function formatDateTime(time = Date.now()) { const date = new Date(time); const s = date.getSeconds(); const min = date.getMinutes(); const h = date.getHours(); const d = date.getDate(); const m = date.getMonth() + 1; const y = date.getFullYear(); // 日期 const dateText = [y, m, d].map(formatDateNum).join('-'); // 时间 const timeText = [h, min, s].map(formatDateNum).join(':'); // 日期时间 const dateTimeText = `${dateText} ${timeText}`; return dateTimeText; } /** * @description 格式化时间 * @param time * @returns * @example * formatTime() -> "08:00:00" * formatTime(new Date()) -> "08:00:00" * formatTime(Date.now()) -> "08:00:00" */ const formatTime = (time = Date.now()) => { const date = new Date(time); const s = date.getSeconds(); const min = date.getMinutes(); const h = date.getHours(); // 时间 const timeText = [h, min, s].map(formatDateNum).join(':'); return timeText; }; /** * @description 时间已过 * @param hour * @param minute * @returns */ function isLate({ hour, minute }) { const date = new Date(); const h = date.getHours(); const min = date.getMinutes(); return h > hour || (h === hour && min >= minute); } /** * @description 时间已过 * @param hour * @param minute * @returns */ function isNow({ hour, minute }) { const date = new Date(); const h = date.getHours(); const min = date.getMinutes(); const s = date.getSeconds(); return h === hour && min === minute && s === 0; } /* 工具函数 */ /** * @description 设置cookie * @param name * @param value * @param expires */ function setCookie(name, value, expires, domain) { // 当前日期 const date = new Date(); // 过期日期 date.setTime(date.getTime() + expires); // 设置cookie document.cookie = `${name}=${value};expires=${date.toUTCString()};path=/;domain=${domain}`; } /** * @description 获取cookie * @param name * @returns */ function getCookie(name) { // 获取当前所有cookie const strCookies = document.cookie; // 截取变成cookie数组 const cookieText = strCookies.split(';'); // 循环每个cookie for (const i in cookieText) { // 将cookie截取成两部分 const item = cookieText[i].split('='); // 判断cookie的name 是否相等 if (item[0].trim() === name) { return item[1].trim(); } } return null; } /** * @description 删除cookie * @param name */ function delCookie(name, domain) { // 存在cookie const value = getCookie(name); if (value !== null) { setCookie(name, '', -1, domain); } } /** * @description 防抖 * @param callback * @param delay * @returns */ function debounce(callback, delay) { let timer = -1; return function (...args) { if (timer !== -1) { clearTimeout(timer); } timer = setTimeout(() => { callback.apply(this, args); }, delay); }; } /** * @description 判断是否为移动端 * @returns */ function hasMobile() { let isMobile = false; if (navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i)) { log('移动端'); isMobile = true; } if (document.body.clientWidth < 800) { log('小尺寸设备端'); isMobile = true; } return isMobile; } /** * @description 等待时间 * @param time * @returns */ function sleep(time) { // 延时 let timeDelay = Number(time); if (!Number.isInteger(timeDelay)) { timeDelay = 1000; } timeDelay += Math.random() * 500 - 250; return new Promise((resolve) => { setTimeout(() => { resolve(undefined); }, timeDelay); }); } /** * @description 暂停学习锁 */ function studyPauseLock(callback) { return new Promise((resolve) => { // 暂停 const pauseStudy = GM_getValue('pauseStudy') || false; if (pauseStudy) { const doing = setInterval(() => { // 暂停 const pauseStudy = GM_getValue('pauseStudy') || false; if (!pauseStudy) { // 停止定时器 clearInterval(doing); log('学习等待结束!'); if (callback && callback instanceof Function) { callback(true); } resolve(true); return; } if (callback && callback instanceof Function) { callback(false); } log('学习等待...'); }, 500); return; } resolve(true); }); } /** * @description 加载 * @param match * @param callback */ function load(match, callback) { // 链接 const { href } = window.location; window.addEventListener('load', () => { // 函数 if (match instanceof Function) { match(href) && callback(); return; } // 布尔 if (typeof match === 'boolean') { match && callback(); return; } // 字符正则 if (href.match(match)) { callback(); return; } }); } /* 变量 */ /** * @description 链接 */ const href = window.location.href; /** * @description 任务配置 */ const taskConfig = reactive([ { title: '登录', currentScore: 0, dayMaxScore: 0, need: 0, status: false, tip: '每日首次登录积1分。', score: 0, active: true, immutable: true, type: TaskType.LOGIN, }, { title: '文章选读', currentScore: 0, dayMaxScore: 0, need: 0, status: false, tip: '每有效阅读一篇文章积1分,上限6分。有效阅读文章累计1分钟积1分,上限6分。每日上限积12分。', score: 0, active: true, immutable: false, type: TaskType.READ, }, { title: '视听学习', currentScore: 0, dayMaxScore: 0, need: 0, status: false, tip: '每有效一个音频或观看一个视频积1分,上限6分。有效收听音频或观看视频累计1分钟积1分,上限6分。每日上限积12分。', score: 0, active: true, immutable: false, type: TaskType.WATCH, }, { title: '每日答题', currentScore: 0, dayMaxScore: 0, need: 0, status: false, tip: '每组答题每答对1道积1分。每日上限积5分。', score: 0, active: true, immutable: false, type: TaskType.PRACTICE, }, ]); /** * @description 设置 */ const settings = reactive([ false, false, false, false, false, false, false, false, ]); /** * @description 总分 */ const totalScore = ref(0); /** * @description 当天分数 */ const todayScore = ref(0); /** * @description 用户信息 */ const userinfo = reactive({ nick: '', avatar: '', }); /** * @description 进度 */ const taskStatus = ref(TaskStatusType.LOADING); /** * @description 答题暂停 */ const examPause = ref(false); /** * @description 登录 */ const login = ref(!!getCookie('token')); /** * @description 窗口id */ const id = ref(''); /** * @description 定时刷新列表 */ const scheduleList = shallowReactive([]); /** * @description 推送token */ const pushToken = ref(''); /** * @description 刷新次数 */ const refreshCount = ref(0); /** * @description 窗口关闭 */ const frame = reactive({ title: '', show: false, exist: false, closed: true, ele: undefined, src: '', }); /** * @description 页面 */ const page = ref(undefined); /** * @description 开始登录 */ const loginQRCodeShow = ref(false); /** * @description 最大选读时长 */ const maxRead = ref(100); /** * @description 最大视听时长 */ const maxWatch = ref(120); /** * @description 运行其他任务 */ const running = ref(false); /** * @description 主题色 */ const themeColor = ref('#fa3333'); /** * @description 考试类型 */ var ExamType; (function (ExamType) { ExamType[ExamType["PRACTICE"] = 0] = "PRACTICE"; ExamType[ExamType["PAPER"] = 1] = "PAPER"; })(ExamType || (ExamType = {})); /** * @description 获取答题按钮 */ function getNextButton() { return new Promise((resolve) => { const timer = setInterval(() => { // 答题按钮 const nextAll = $$('.ant-btn').filter((next) => next.innerText); if (nextAll.length) { // 停止定时器 clearInterval(timer); if (nextAll.length === 2) { resolve(nextAll[1]); return; } resolve(nextAll[0]); } }, 500); }); } /** * @description 处理滑动验证 */ function handleSlideVerify() { return new Promise(async (resolve) => { // 滑动验证 const mask = $$('#nc_mask')[0]; if (mask && getComputedStyle(mask).display !== 'none') { // 创建提示 createTip('等待滑动验证'); // 提高层级 mask.style.zIndex = '999'; // 轨道 const track = (await $_('.nc_scale', undefined, 3000))[0]; // 滑块 const slide = (await $_('.btn_slide', undefined, 3000))[0]; // 延时 await sleep(2000); // 矩形范围 const rectTrack = track.getBoundingClientRect(); // 矩形范围 const rectSlide = slide.getBoundingClientRect(); // 窗口 const window = unsafeWindow; // 范围内随机起点 const start = createRandomPoint(rectSlide); // 终点 const end = { x: rectTrack.x + rectTrack.width, y: rectTrack.y + rectTrack.height / 2, }; // 路径 const path = createRandomPath(start, end, 10); // 移动端 const mobile = hasMobile(); if (mobile) { slide.style.touchAction = 'none'; const touchstartTouch = new Touch({ identifier: 0, target: slide, clientX: path[0].x, clientY: path[0].y, }); const touchstartList = [touchstartTouch]; // 开始触摸 const touchstart = new TouchEvent('touchstart', { targetTouches: touchstartList, touches: touchstartList, changedTouches: touchstartList, view: window, bubbles: true, }); slide.dispatchEvent(touchstart); // 触摸滑动 for (const i in path) { const touchmoveTouch = new Touch({ identifier: 0, target: slide, clientX: path[i].x, clientY: path[i].y, }); const touchmoveList = [touchmoveTouch]; const touchmove = new TouchEvent('touchmove', { targetTouches: touchmoveList, touches: touchmoveList, changedTouches: touchmoveList, view: window, bubbles: true, }); slide.dispatchEvent(touchmove); await sleep(10); } const touchendTouch = new Touch({ identifier: 0, target: slide, clientX: path[path.length - 1].x, clientY: path[path.length - 1].y, }); // 触摸结束 const touchendList = [touchendTouch]; // 开始触摸 const touchend = new TouchEvent('touchend', { targetTouches: [], touches: [], changedTouches: touchendList, view: window, bubbles: true, }); slide.dispatchEvent(touchend); } else { // 鼠标按下 const mousedown = new MouseEvent('mousedown', { clientX: path[0].x, clientY: path[0].y, bubbles: true, view: window, }); slide.dispatchEvent(mousedown); // 鼠标滑动 for (const i in path) { const mousemove = new MouseEvent('mousemove', { clientX: path[i].x, clientY: path[i].y, bubbles: true, view: window, }); slide.dispatchEvent(mousemove); await sleep(10); } // 鼠标抬起 const mouseup = new MouseEvent('mouseup', { clientX: path[path.length - 1].x, clientY: path[path.length - 1].y, bubbles: true, view: window, }); slide.dispatchEvent(mouseup); } // 创建提示 createTip('滑动验证完成!'); // 定时器 const timer = setInterval(() => { // 滑动验证 const mask = $$('#nc_mask')[0]; if (!mask || getComputedStyle(mask).display === 'none') { log('滑动验证成功!'); // 创建提示 createTip('滑动验证成功!'); clearInterval(timer); resolve(true); return; } resolve(false); log('滑动验证失败!'); // 创建提示 createTip('滑动验证失败!'); }, 1000); return; } resolve(true); }); } /** * @description 处理选项 */ function handleChoiceBtn(answers) { // 选项按钮 const allBtns = $$('.q-answer'); // 答案存在 if (answers.length && allBtns.length) { // 作答 return answers.every((answer) => { // 答案存在 if (answer && answer.length) { // 包含答案最短长度选项 let minLengthChoice; // 遍历 allBtns.forEach((choice) => { // 选项文本 const choiceText = choice.innerText.trim(); // 无符号选项文本 const unsignedChoiceText = choiceText.replaceAll(/[、,,。 ]/g, ''); // 无符号答案 const unsignedAnswer = answer.replaceAll(/[、,,。 ]/g, ''); // 包含答案 if (choiceText === answer || choiceText.includes(answer) || answer.includes(choiceText) || unsignedChoiceText.includes(unsignedAnswer)) { // 最小长度选项有值 if (minLengthChoice) { // 最短长度选项与当前选项比较长度 if (minLengthChoice.innerText.length > choiceText.length) { minLengthChoice = choice; } } else { // 最小长度选项赋值 minLengthChoice = choice; } } }); // 存在选项 if (minLengthChoice) { // 选择 if (!minLengthChoice.classList.contains('chosen')) { minLengthChoice.click(); } return true; } } return false; }); } return false; } /** * @description 随机处理单选 */ function handleSingleChoiceRand() { // 选项按钮 const allBtns = $$('.q-answer'); // 按钮存在 if (allBtns.length) { const index = ~~(Math.random() * allBtns.length); const randBtn = allBtns[index]; // 选择 if (!randBtn.classList.contains('chosen')) { randBtn.click(); } } } /** * @description 随机处理多选 */ function handleMutiplyChoiceRand() { // 选项按钮 const allBtns = $$('.q-answer'); // 按钮存在 if (allBtns.length) { allBtns.forEach((allBtn) => { // 选择 if (!allBtn.classList.contains('chosen')) { allBtn.click(); } }); } } /** * @description 处理填空 */ const handleBlankInput = (answers) => { // 所有填空 const blanks = $$('.blank'); // 答案存在 if (blanks.length && answers.length) { // 填空数量和答案数量一致 if (answers.length === blanks.length) { return answers.every((answer, i) => { // 答案存在 if (answer && answer.length) { // 输入事件 const inputEvent = new Event('input', { bubbles: true, }); // 设置答案 blanks[i].setAttribute('value', answer); // 触发输入input blanks[i].dispatchEvent(inputEvent); return true; } return false; }); } // 填空数量为1和提示数量大于1 if (blanks.length === 1 && answers.length > 1) { // 直接将所有答案整合填进去 const answer = answers.join(''); // 答案存在 if (answer && answer.length) { // 输入事件 const inputEvent = new Event('input', { bubbles: true, }); // 设置答案 blanks[0].setAttribute('value', answer); // 触发输入input blanks[0].dispatchEvent(inputEvent); return true; } } } return false; }; /** * @description 处理填空随机 */ async function handleBlankInputRand() { // 所有填空 const blanks = $$('.blank'); if (blanks.length) { // 输入事件 const inputEvent = new Event('input', { bubbles: true, }); blanks.forEach((blank) => { // 设置答案 blank.setAttribute('value', '答案'); // 触发输入input blank.dispatchEvent(inputEvent); }); } } /** * @description 暂停锁 */ function examPauseLock(callback) { return new Promise((resolve) => { // 学习暂停 const pauseStudy = (GM_getValue('pauseStudy') || false); // 全局暂停 if (pauseStudy) { examPause.value = true; } // 暂停 if (examPause.value) { // 创建提示 createTip('已暂停, 手动开启自动答题! ', 10); const doing = setInterval(() => { if (!examPause.value) { // 停止定时器 clearInterval(doing); log('答题等待结束!'); if (callback && callback instanceof Function) { // 创建提示 createTip('已开启, 自动答题!'); callback(true); } resolve(true); return; } if (callback && callback instanceof Function) { callback(false); } log('答题等待...'); }, 500); return; } resolve(true); }); } /** * @description 答题 */ async function doingExam(type) { // 下一个按钮 let nextButton; // 下一个文本 let nextText; // 保存答案 let shouldSaveAnswer = false; while (true) { // 先等等再开始做题 await sleep(2500); // 暂停 await examPauseLock(); // 获取下一个按钮 nextButton = await getNextButton(); // 下一个文本 nextText = nextButton.innerText.replaceAll(' ', ''); // 结束 const finish = ['再练一次', '再来一组', '查看解析']; if (finish.includes(nextButton.innerText)) { break; } // 点击提示 $$('.tips')[0]?.click(); // 所有提示 const allTips = $$('.line-feed font[color]'); // 答案 const answers = allTips.map((tip) => tip.innerText.trim()); // 获取题目的文本内容 const question = $$('.q-body')[0].innerText; // 等待一段时间 await sleep(1500); // 暂停 await examPauseLock(); // 选项按钮 const allBtns = $$('.q-answer'); // 所有填空 const blanks = $$('input[type=text][class=blank]'); // 问题类型 const questionType = ($$('.q-header')[0].innerText.substring(0, 3)); // 暂停 await examPauseLock(); // 题型分类作答 switch (questionType) { case '填空题': { // 根据提示作答 if (answers.length) { const res = handleBlankInput(answers); // 成功 if (res) { break; } } // 创建提示 createTip('答案异常, 尝试网络题库获取!'); log('正在获取答案...'); // 尝试题库获取 const answersNetwork = await getAnswer(question); log(`获取答案${answersNetwork.length ? '成功' : '失败'}!`, { question, answersNetwork, }); // 根据题库作答 if (answersNetwork.length) { const res = handleBlankInput(answersNetwork); // 成功 if (res) { break; } } // 随机作答 if (type === ExamType.PRACTICE || settings[SettingType.RANDOM_EXAM]) { log('答案不存在, 随机作答!'); // 创建提示 createTip('答案不存在, 随机作答!'); await handleBlankInputRand(); } else { // 推送 const res = await pushModal({ title: '学习推送', to: userinfo.nick, content: '答题存在异常, 已暂停答题!', type: 'fail', }, pushToken.value); createTip(`学习推送${res ? '成功' : '失败'}!`); // 暂停 examPause.value = true; // 提交答案 shouldSaveAnswer = true; } break; } case '多选题': { // 根据提示作答 if (answers.length) { // 选项文本 const choicesText = allBtns.map((btn) => btn.innerText); // 选项内容 const choicesContent = choicesText .map((choiceText) => choiceText.split(/[A-Z]./)[1].trim()) .join(''); // 空格 const blanks = question.match(/()/g); // 填空数量、选项数量、答案数量相同 | 选项全文等于答案全文 if ((blanks && allBtns.length === blanks.length) || question === choicesContent || allBtns.length === 2) { // 全选 allBtns.forEach((choice) => { if (!choice.classList.contains('chosen')) { choice.click(); } }); break; } // 选项数量大于等于答案 if (allBtns.length >= answers.length) { const res = handleChoiceBtn(answers); // 成功 if (res) { break; } } } // 创建提示 createTip('答案异常, 尝试网络题库获取!'); log('正在获取答案...'); // 尝试题库获取 const answersNetwork = await getAnswer(question); log(`获取答案${answersNetwork.length ? '成功' : '失败'}!`, { question, answersNetwork, }); // 答案存在 if (answersNetwork.length) { const res = handleChoiceBtn(answersNetwork); // 成功 if (res) { break; } } // 随机作答 if (type === ExamType.PRACTICE || settings[SettingType.RANDOM_EXAM]) { log('答案不存在, 随机作答!'); // 创建提示 createTip('答案不存在, 随机作答!'); await handleMutiplyChoiceRand(); } else { // 推送 const res = await pushModal({ title: '学习推送', to: userinfo.nick, content: '答题存在异常, 已暂停答题!', type: 'fail', }, pushToken.value); createTip(`学习推送${res ? '成功' : '失败'}!`); // 暂停 examPause.value = true; // 提交答案 shouldSaveAnswer = true; } break; } case '单选题': { // 根据提示作答 if (answers.length) { // 创建提示为1 if (answers.length === 1) { const res = handleChoiceBtn(answers); // 成功 if (res) { break; } } else { // 可能的分隔符 const seperator = [ '', ' ', ',', ';', ',', '、', '-', '|', '+', '/', ]; // 可能的答案 const answersLike = seperator .map((s) => answers.join(s).trim()) .filter((answer) => answer.length); // 答案存在 if (answersLike.length) { // 可能答案是否正确 const res = answersLike.some((answer) => { // 尝试查找点击 return handleChoiceBtn([answer]); }); if (res) { break; } } } } // 创建提示 createTip('答案异常, 尝试网络题库获取!'); log('正在获取答案...'); // 尝试题库获取 const answersNetwork = await getAnswer(question); log(`获取答案${answersNetwork.length ? '成功' : '失败'}!`, { question, answersNetwork, }); // 存在答案 if (answersNetwork.length) { // 单答案单选项 if (answersNetwork.length === 1) { // 尝试查找点击 const res = handleChoiceBtn(answersNetwork); if (res) { break; } } else { // 多答案单选项 选项意外拆分 // 可能分隔符 const seperator = ['', ' ']; // 可能答案 const answersLike = seperator.map((s) => answers.join(s)); // 答案存在 if (answersLike.every((answer) => answer.length)) { // 可能答案是否正确 const res = answersLike.some((answer) => { // 尝试查找点击 return handleChoiceBtn([answer]); }); if (res) { break; } } } } // 随机作答 if (type === ExamType.PRACTICE || settings[SettingType.RANDOM_EXAM]) { log('答案不存在, 随机作答!'); // 创建提示 createTip('答案不存在, 随机作答!'); await handleSingleChoiceRand(); } else { // 推送 const res = await pushModal({ title: '学习推送', to: userinfo.nick, content: '答题存在异常, 已暂停答题!', type: 'fail', }, pushToken.value); createTip(`学习推送${res ? '成功' : '失败'}!`); // 暂停 examPause.value = true; // 提交答案 shouldSaveAnswer = true; } break; } } // 暂停 await examPauseLock(); // 获取下一个按钮 nextButton = await getNextButton(); // 下一个文本 nextText = nextButton.innerText.replaceAll(' ', ''); // 需要提交答案 if (shouldSaveAnswer) { // 答案 const answers = []; if (questionType === '填空题') { blanks.forEach((blank) => { answers.push(blank.value); }); } if (questionType === '单选题' || questionType === '多选题') { allBtns.forEach((choice) => { if (choice.classList.contains('chosen')) { // 带字母的选项 const answerTemp = choice.innerText; // 从字符串中拿出答案 const [, answer] = answerTemp.split('.'); if (answer && answer.length) { answers.push(answer); } } }); } // 答案 const answer = answers.join(';'); // 存在答案 if (answer.length) { log('正在上传答案...'); // 上传答案 const res = await saveAnswer(question, answer); log(`上传答案${res ? '成功' : '失败'}!`, { question, answer }); } // 重置 shouldSaveAnswer = false; } // 确定 if (nextText === '确定') { // 确认 nextButton.click(); // 等待一段时间 await sleep(2000); // 暂停 await examPauseLock(); // 答案解析 const answerBox = $$('.answer')[0]; // 答题错误 if (answerBox) { const answerTemp = answerBox.innerText; // 从字符串中拿出答案 const [, answerText] = answerTemp.split(':'); if (answerText && answerText.length) { const answer = answerText.replaceAll(' ', ';'); log('正在上传答案...'); // 上传答案 const res = await saveAnswer(question, answer); log(`上传答案${res ? '成功' : '失败'}!`, { question, answer }); } } } // 获取按钮 nextButton = await getNextButton(); // 下一个文本 nextText = nextButton.innerText.replaceAll(' ', ''); if (nextText === '下一题' || nextText === '完成' || nextText === '交卷') { // 等待一段时间 await sleep(2500); // 下一题 nextButton.click(); } // 滑动验证 await handleSlideVerify(); } // 关闭任务窗口 handleCloseTaskWin(); } /** * @description 每日答题 */ async function doExamPractice() { // 暂停 await studyPauseLock(); log('正在每日答题...'); // 创建提示 createTip('正在每日答题'); // 链接 const url = URL_CONFIG.examPractice; // 等待任务窗口 await waitTaskWin(url, '每日答题'); // 创建提示 createTip('完成每日答题!'); // 等待一段时间 await sleep(1500); // 刷新分数数据 await refreshScoreInfo(); // 刷新任务数据 await refreshTaskList(); // 任务完成状况 if (taskConfig[TaskType.PRACTICE].active && !taskConfig[TaskType.PRACTICE].status) { log('任务未完成, 继续每日答题!'); // 创建提示 createTip('任务未完成, 继续每日答题!'); await doExamPractice(); } } /** * @description 专项练习 */ async function doExamPaper() { running.value = true; log('正在专项练习...'); // 创建提示 createTip('正在专项练习'); // id const examPaperId = await findExamPaper(); if (examPaperId) { // 链接 const url = `${URL_CONFIG.examPaper}?id=${examPaperId}`; log(`链接: ${url}`); // 等待窗口任务 await waitTaskWin(url, '专项练习'); // 创建提示 createTip('完成专项练习!'); running.value = false; // 同屏任务 if (settings[SettingType.SAME_TAB]) { // 窗口不存在 frame.exist = false; } return; } running.value = false; // 创建提示 createTip('专项练习均已完成!'); } /** * @description 初始化总页数属性 */ async function initExam() { // 默认从第一页获取全部页属性 const data = await getExamPaper(1); if (data) { // 等待 await sleep(3000); return data.totalPageCount; } } /** * @description 查询专项练习列表 */ async function findExamPaper() { // 获取总页数 const total = await initExam(); // 当前页数 let current = 1; log(`正在寻找的专项练习...`); // 创建提示 createTip(`正在寻找的专项练习...`); while (current <= total && current) { // 请求数据 const data = await getExamPaper(current); if (data) { // 获取专项练习的列表 const examPapers = data.list; for (const i in examPapers) { // 遍历查询有没有没做过的 if (examPapers[i].status !== 2) { // status: 1 开始答题, 2 已满分/重新答题, 3 继续答题 return examPapers[i].id; } } // 增加页码 current += 1; // 等待 await sleep(3000); } else { break; } } } /** * @description 初始化主页面 */ function initMainListener() { // 监听关闭 window.addEventListener('message', (msg) => { const { data } = msg; if (data.id === id.value && data.closed) { // 关闭窗口 closeFrame(); return; } }); } /** * @description 初始化子页面 */ function initChildListener() { window.addEventListener('message', (msg) => { const { data } = msg; if (data.id && !data.closed) { // 设置窗口id id.value = data.id; log(`初始化窗口 ID: ${id.value}`); return; } }); } /** * @description 打开窗口 * @param url * @returns */ async function openFrame(url, title) { // 设置 URL frame.src = url; // 等待元素 await $_('.egg_frame'); if (frame.ele) { // id id.value = generateMix(10); // 打开 frame.closed = false; // 设置标题 frame.title = title || ''; // 等待页面加载 await waitFrameLoaded(frame.ele); // 发送窗口 ID frame.ele.contentWindow?.postMessage({ id: id.value, closed: false }, url); return true; } return false; } /** * @description 关闭窗口 */ function closeFrame() { log(`关闭窗口 ID: ${id.value}`); // 窗口显示 frame.show = false; // 关闭 frame.closed = true; // 标题 frame.title = ''; // src frame.src = ''; } /** * @description 关闭 frame */ function handleCloseFrame() { window.parent.postMessage({ id: id.value, closed: true }, URL_CONFIG.homeOrigin); } /** * @description 等待窗口任务结束 * @param id * @returns */ function waitFrameClose() { return new Promise((resolve) => { const timer = setInterval(() => { // 窗口关闭 if (frame.closed) { clearInterval(timer); resolve(true); } }, 100); }); } // 等待窗口加载 function waitFrameLoaded(iframe) { return new Promise((resolve) => { iframe.addEventListener('load', () => { resolve(true); }); }); } /** * @description 打开新窗口 */ function openWin(url) { return GM_openInTab(url, { active: true, insert: true, setParent: true, }); } /** * @description 关闭窗口 */ function closeWin() { page.value && page.value.close(); } /** * @description 关闭子窗口 */ function handleCloseWin() { try { window.opener = window; const win = window.open('', '_self'); win?.close(); top?.close(); } catch (e) { } } /** * @description 等待窗口关闭 * @param newPage * @returns */ function waitWinClose(newPage) { return new Promise((resolve) => { newPage.onclose = () => { resolve(undefined); }; }); } /** * @description 关闭任务窗口 */ function closeTaskWin() { // 同屏任务 if (settings[SettingType.SAME_TAB] && id.value) { closeFrame(); return; } // 非同屏任务 closeWin(); } /** * @description 关闭任务窗口 */ function handleCloseTaskWin() { // 同屏任务 if (settings[SettingType.SAME_TAB] && id.value) { handleCloseFrame(); return; } // 子窗口 handleCloseWin(); } /** * @description 打开并等待任务结束 */ async function waitTaskWin(url, title) { // 同屏任务 if (settings[SettingType.SAME_TAB]) { // 窗口存在 frame.exist = true; // 显示窗体 frame.show = !settings[SettingType.SILENT_RUN]; // 新窗口 const res = await openFrame(url, title); if (res) { // 等待窗口关闭 await waitFrameClose(); } return; } // 子页面任务 page.value = openWin(url); await waitWinClose(page.value); } /** * @description 二维码刷新定时器 */ let refreshTimer = -1; /** * @description 尝试登录 */ let tryLoginTimer = -1; /** * @description 生成二维码 */ async function getQRCode() { log('正在生成登录二维码...'); const qrCode = await generateQRCode(); if (qrCode) { log('生成登录二维码成功!'); // 链接 const url = `https://login.xuexi.cn/login/qrcommit?showmenu=false&code=${qrCode}&appId=dingoankubyrfkttorhpou`; return { code: qrCode, src: `${API_CONFIG.qrcode}?data=${encodeURIComponent(url)}`, url, }; } log('生成登录二维码失败!'); } /** * @description 验证登录二维码 * @param code * @returns */ async function checkQRCode(code) { log('尝试用二维码登录...'); // 二维码登录 const res = await loginWithQRCode(code); if (res) { const { data, code, success } = res; // 临时登录验证码 if (success && data) { return data; } // 二维码失效 if (code === '11019') { return; } } return new Promise((resolve) => { // 清除定时 clearTimeout(tryLoginTimer); // 设置定时 tryLoginTimer = setTimeout(async () => { resolve(await checkQRCode(code)); }, 1000); }); } /** * @description 尝试二维码登录 */ async function tryLogin(checkCode) { log('正在获取签名...'); // 获取签名 const sign = await getSign(); if (sign) { // 生成uuid const uuid = crypto.randomUUID(); const [, code] = checkCode.split('='); const state = `${sign}${uuid}`; // 安全检查 const res = await secureCheck({ code, state }); return res; } } /** * @description 刷新登录二维码 */ async function handleLogin() { // 清除刷新 clearInterval(refreshTimer); // 每隔一段时间刷新 refreshTimer = setInterval(() => { // 刷新二维码 handleLogin(); }, autoRefreshQRCodeInterval); // 是否超出次数 if (refreshCount.value >= maxRefreshCount) { createTip('超过最大重试次数, 登录失败!'); // 重置刷新数 refreshCount.value = 0; // 隐藏二维码 loginQRCodeShow.value = false; // 远程推送 if (settings[SettingType.REMOTE_PUSH]) { // 推送 const res = await pushModal({ title: '登录推送', content: '超过最大重试次数, 登录失败!', type: 'fail', }, pushToken.value); createTip(`登录推送${res ? '成功' : '失败'}!`); } return; } // 配置 const imgWrap = $$('.egg_login_img_wrap')[0]; // 图片 const img = $$('.egg_login_img', imgWrap)[0]; if (imgWrap && img) { // 刷新二维码 log('刷新登录二维码!'); // 刷新次数累加 refreshCount.value++; // 获取二维码 const qrCode = await getQRCode(); if (qrCode) { // 获取连接 const { src, code, url } = qrCode; // src img.src = src; // 开始登录 loginQRCodeShow.value = true; // 远程推送 if (settings[SettingType.REMOTE_PUSH]) { // img html const imgWrap = getImgHTML(src); // 跳转链接 const aWrap = ` `; // 推送 const res = await pushModal({ title: '登录推送', content: ['扫一扫, 登录学习强国!', aWrap, imgWrap], type: 'info', }, pushToken.value); createTip(`登录推送${res ? '成功' : '失败'}!`); } // 获取验证码 const checkCode = await checkQRCode(code); // 验证成功 if (checkCode) { // 尝试登录 const loginRes = await tryLogin(checkCode); if (loginRes) { // 清除刷新 clearInterval(refreshTimer); // 二维码显示 loginQRCodeShow.value = false; // 登录成功 log('登录成功!'); // 创建提示 createTip('登录成功!'); // 登录成功 login.value = true; // 刷新用户信息 await refreshUserInfo(); // 刷新分数信息 await refreshScoreInfo(); // 刷新任务信息 await refreshTaskList(); // 远程推送 if (settings[SettingType.REMOTE_PUSH]) { const res = await pushModal({ title: '登录推送', to: userinfo.nick, content: [ '学习强国, 登录成功!', `当天积分: ${getHighlightHTML(todayScore.value)} 分`, `总积分: ${getHighlightHTML(totalScore.value)} 分`, ...taskConfig.map((task) => getProgressHTML(task.title, task.currentScore, task.dayMaxScore)), ], type: 'success', }, pushToken.value); createTip(`登录推送${res ? '成功' : '失败'}!`); } } return; } // 二维码失效 log('登录二维码失效!'); // 二维码失效刷新 handleLogin(); } } } /** * @description 退出登录 */ function handleLogout() { // 删除token delCookie('token', '.xuexi.cn'); // 关闭窗口 closeFrame(); frame.exist = false; // 退出登录 login.value = false; // 清除用户信息 userinfo.nick = ''; userinfo.avatar = ''; // 总分 totalScore.value = 0; // 当天分数 todayScore.value = 0; // 任务进度重置 taskConfig.forEach((task) => { task.currentScore = 0; }); taskStatus.value = TaskStatusType.LOADING; // 退出登录 log('退出登录'); } /** * @description 新闻 */ let news = []; /** * @description 视频 */ let videos = []; /** * @description 处理文章 */ async function handleNews() { // section const sections = await $_('section', undefined, 5000); const section = sections[0]; if (!(section && section.innerText.includes('系统正在维护中'))) { // 文章选读 reading(0); return; } log('未找到文章!'); // 提示 createTip('未找到文章!'); // 关闭页面 handleCloseTaskWin(); } /** * @description 处理视频 */ async function handleVideo() { // videos const videos = await $_('video', undefined, 10000); // 视频 const video = videos[0]; // 播放按键 const playBtn = $$('.prism-play-btn')[0]; if (video && playBtn) { log('正在尝试播放视频...'); // 播放超时 const timeout = setTimeout(() => { log('视频播放超时!'); // 提示 createTip('视频播放超时!'); // 关闭页面 handleCloseTaskWin(); }, 20000); // 设置是否静音 watchEffect(() => (video.muted = settings[SettingType.VIDEO_MUTED])); // 能播放 video.addEventListener('canplay', () => { const timer = setInterval(() => { // 尝试点击播放按钮播放 playBtn.click(); // 播放未成功 if (video.paused) { // 尝试使用js的方式播放 video.play(); } }, 1000); video.addEventListener('playing', () => { // 清除超时定时器 clearTimeout(timeout); // 清除定时器 clearInterval(timer); log('播放视频成功!'); // 视听学习 reading(1); return; }, { once: true }); }, { once: true }); return; } log('未找到视频!'); // 关闭页面 handleCloseTaskWin(); } /** * @description 读新闻或者看视频 * @param type :0为新闻,1为视频 */ async function reading(type) { let time = 30; // 文章选读 if (type === 0) { // 章节 const sections = $$('section'); // 最大字数 const maxTextCount = Math.max(...sections.map((s) => s.innerText.length), 200); // 预计时间 const predictTime = ~~((60 * maxTextCount) / 1000); // min(predictTime, maxWatch.value) 秒后关闭页面 time = Math.min(predictTime, maxRead.value); } // 视听学习 if (type === 1) { // 视频 const video = $$('video')[0]; // 预计时间 const predictTime = ~~video.duration; // min(predictTime, maxWatch.value) 秒后关闭页面 time = Math.min(predictTime, maxWatch.value); } // 随机 time = time - ~~(Math.random() * 10) + 5; // 第一次滚动时间 const firstTime = time - (~~(Math.random() * 4) + 4); // 第二次滚动时间 const secendTime = ~~(Math.random() * 4) + 8; // 窗口 const window = unsafeWindow; // 创建提示 const tip = createTip('距离关闭页面还剩', time, true, async (time) => { // 暂停锁 await studyPauseLock((flag) => { if (type === 1) { // 视频 const video = $$('video')[0]; // 排除反复设置 if (video.paused === !flag) { return; } // 设置播放状态 video[flag ? 'play' : 'pause'](); } }); // 第一次滚动 if (time === firstTime) { // 滚动 window.scrollTo(0, 400); // 模拟滚动 const scroll = new Event('scroll', { bubbles: true, }); document.dispatchEvent(scroll); // 模拟滑动 const mousemove = new MouseEvent('mousemove', { bubbles: true, }); document.dispatchEvent(mousemove); // 模拟点击 const click = new Event('click', { bubbles: true, }); document.dispatchEvent(click); } // 第二次滚动 if (time === secendTime) { // 滚动长度 const scrollLength = document.body.scrollHeight / 2; // 滚动 window.scrollTo(0, scrollLength); // 模拟滚动 const scroll = new Event('scroll', { bubbles: true, }); document.dispatchEvent(scroll); // 模拟滑动 const mousemove = new MouseEvent('mousemove', { bubbles: true, }); document.dispatchEvent(mousemove); // 模拟点击 const click = new Event('click', { bubbles: true, }); document.dispatchEvent(click); } }); // 倒计时结束 await tip.waitCountDown(); // 关闭任务窗口 handleCloseTaskWin(); } /** * @description 获取新闻列表 */ async function getNews() { // 需要学习的新闻数量 const need = taskConfig[TaskType.READ].need < maxNewsNum ? taskConfig[TaskType.READ].need : maxNewsNum; log(`剩余 ${need} 个新闻`); // 获取新闻 const data = await getNewsList(); if (data && data.length) { // 索引 let i = 0; // 最新新闻 const latestItems = data.slice(0, 100); // 当前年份 const currentYear = new Date().getFullYear().toString(); // 查找今年新闻 while (i < need) { const randomIndex = ~~(Math.random() * latestItems.length); // 新闻 const item = latestItems[randomIndex]; // 是否存在 if (item.publishTime.startsWith(currentYear) && item.type === 'tuwen') { news[i] = item; i++; } } } else { news = []; } } /** * @description 获取视频列表 */ async function getVideos() { // 需要学习的视频数量 const need = taskConfig[TaskType.WATCH].need < maxVideoNum ? taskConfig[TaskType.WATCH].need : maxVideoNum; log(`剩余 ${need} 个视频`); // 获取视频 const data = await getVideoList(); if (data && data.length) { // 索引 let i = 0; // 最新视频 const latestItems = data.slice(0, 100); // 当前年份 const currentYear = new Date().getFullYear().toString(); // 查找今年视频 while (i < need) { const randomIndex = ~~(Math.random() * latestItems.length); // 新闻 const item = latestItems[randomIndex]; // 是否存在 if (item.publishTime.startsWith(currentYear) && (item.type === 'shipin' || item.type === 'juji')) { videos[i] = item; i++; } } } else { videos = []; } } /** * @description 阅读文章 */ async function readNews() { // 获取文章 await getNews(); // 观看文章 for (const i in news) { // 任务关闭跳出循环 if (!taskConfig[TaskType.READ].active) { return; } // 暂停 await studyPauseLock(); log(`正在阅读第 ${Number(i) + 1} 个新闻...`); // 创建提示 createTip(`正在阅读第 ${Number(i) + 1} 个新闻`); // 链接 const { url } = news[i]; // 链接 GM_setValue('readingUrl', url); // 等待任务窗口 await waitTaskWin(url, '文章选读'); // 清空链接 GM_setValue('readingUrl', null); // 创建提示 createTip(`完成阅读第 ${Number(i) + 1} 个新闻!`); // 等待一段时间 await sleep(1500); // 刷新分数数据 await refreshScoreInfo(); // 刷新任务数据 await refreshTaskList(); // 任务完成跳出循环 if (taskConfig[TaskType.READ].active && taskConfig[TaskType.READ].status) { break; } } // 任务关闭跳出循环 if (!taskConfig[TaskType.READ].active) { return; } // 任务完成状况 if (taskConfig[TaskType.READ].active && !taskConfig[TaskType.READ].status) { log('任务未完成, 继续阅读新闻!'); // 创建提示 createTip('任务未完成, 继续阅读新闻!'); await readNews(); } } /** * @description 观看视频 */ async function watchVideo() { // 获取视频 await getVideos(); // 观看视频 for (const i in videos) { // 任务关闭跳出循环 if (!taskConfig[TaskType.WATCH].active) { return; } // 暂停 await studyPauseLock(); log(`正在观看第 ${Number(i) + 1} 个视频...`); // 创建提示 createTip(`正在观看第 ${Number(i) + 1} 个视频`); // 链接 const { url } = videos[i]; // 链接 GM_setValue('watchingUrl', url); // 等待任务窗口 await waitTaskWin(url, '视听学习'); // 清空链接 GM_setValue('watchingUrl', null); // 创建提示 createTip(`完成观看第 ${Number(i) + 1} 个视频!`); // 等待一段时间 await sleep(1500); // 刷新分数数据 await refreshScoreInfo(); // 刷新任务数据 await refreshTaskList(); // 任务完成跳出循环 if (taskConfig[TaskType.WATCH].active && taskConfig[TaskType.WATCH].status) { break; } } // 任务关闭跳出循环 if (!taskConfig[TaskType.WATCH].active) { return; } // 任务完成状况 if (taskConfig[TaskType.WATCH].active && !taskConfig[TaskType.WATCH].status) { log('任务未完成, 继续观看视频!'); // 创建提示 createTip('任务未完成, 继续观看看视频!'); await watchVideo(); } } /** * @description 定时刷新定时器 */ let scheduleTimer = -1; /** * @description 刷新定时任务 */ async function refreshScheduleTask() { // 清除定时刷新 clearInterval(scheduleTimer); // 未登录 if (!login.value) { // 剩余定时任务 const restList = scheduleList.filter((s) => !isLate(s)); // 存在剩余任务 if (restList.length) { const rest = restList[0]; log(`已设置 ${rest.time} 的定时任务!`); // 提示 createTip(`已设置 ${rest.time} 的定时任务!`); // 时间 let time = 0; // 刷新间隔 const interval = 10; scheduleTimer = setInterval(() => { if (!(time++ % interval)) { log('定时刷新正在运行...'); } // 到达定时 if (isNow(rest)) { clearInterval(scheduleTimer); log(`执行 ${rest.time} 的定时任务!`); // 提示 createTip(`执行 ${rest.time} 的定时任务!`); // 登录 handleLogin(); } }, 1000); } } } /** * @description 清除定时 */ function clearScheduleTask() { clearInterval(scheduleTimer); } /** * @description 创建学习提示 */ function createTip(text, delay = 2, countShow = false, callback) { const tipWrap = $$('.egg_tip_wrap')[0]; // 提前去除 const tips = $$('.egg_tip'); if (tips.length) { tips.forEach((t) => t.delay()); } // 延迟 const delayCount = ref(delay); // 文字 const textContent = ref(text); //显示 const show = ref(false); // 延迟显示 const delayShow = ref(false); // 销毁 let destroyed = false; // 倒计时结束 let done = false; // 提示 const tip = Tip({ text: textContent, count: delayCount, show, delayShow, countShow: ref(countShow), callback: async (count) => { callback && (await callback(count)); // 恢复显示 if (delayShow.value && count === delay) { delayShow.value = false; } // 倒计时结束 if (count <= 0) { done = true; operate.destroy(); } }, }); // 操作 const operate = { destroy() { if (!destroyed) { // 隐藏 operate.hide(); // 销毁 destroyed = true; return new Promise((resolve) => { setTimeout(() => { tip.ele.remove(); resolve(undefined); }, 300); }); } }, hide() { if (!destroyed) { show.value = false; } }, show() { if (!destroyed) { return new Promise((resolve) => { setTimeout(() => { show.value = true; resolve(undefined); }, 300); }); } }, setText(text) { if (!destroyed) { textContent.value = text; } }, waitCountDown() { return new Promise((resolve) => { // 计时器 const timer = setInterval(() => { // 结束 if (done) { clearInterval(timer); resolve(true); } }, 100); }); }, delay() { if (!destroyed) { delayShow.value = true; delayCount.value += 2; } }, }; Object.assign(tip.ele, operate); // 插入节点 mountElement(tip, tipWrap); // 显示 operate.show(); return operate; } /** * @description 刷新用户信息 */ async function refreshUserInfo() { // 未登录 if (!login.value) { throw new Error('用户未登录!'); } // 已存在信息 if (userinfo.nick) { return true; } log('加载用户信息...'); // 获取用户信息 const res = await getUserInfo(); if (res) { const { avatarMediaUrl = '', nick: nickRes } = res; if (nickRes) { // 设置昵称 userinfo.nick = nickRes; // 设置头像 userinfo.avatar = avatarMediaUrl; return true; } } log('加载用户信息失败!'); return false; } /** * @description 刷新分数信息 */ async function refreshScoreInfo() { // 未登录 if (!login.value) { throw new Error('用户未登录!'); } log('加载分数信息...'); // 获取总分 const totalScoreRes = await getTotalScore(); // 获取当天总分 const todayScoreRes = await getTodayScore(); // 整数值 if (Number.isInteger(totalScoreRes) && Number.isInteger(todayScoreRes)) { // 设置分数 totalScore.value = totalScoreRes; todayScore.value = todayScoreRes; return true; } log('加载分数信息失败!'); return false; } /** * @description 刷新任务列表 */ async function refreshTaskList() { // 未登录 if (!login.value) { throw new Error('用户未登录!'); } log('加载任务进度...'); // 原始任务进度 const taskProgress = await getTaskList(); if (taskProgress) { // 登录 taskConfig[TaskType.LOGIN].currentScore = taskProgress[2].currentScore; taskConfig[TaskType.LOGIN].dayMaxScore = taskProgress[2].dayMaxScore; taskConfig[TaskType.LOGIN].need = taskProgress[2].dayMaxScore - taskProgress[2].currentScore; // 文章选读 taskConfig[TaskType.READ].currentScore = taskProgress[0].currentScore; taskConfig[TaskType.READ].dayMaxScore = taskProgress[0].dayMaxScore; taskConfig[TaskType.READ].need = taskProgress[0].dayMaxScore - taskProgress[0].currentScore; // 视听学习 taskConfig[TaskType.WATCH].currentScore = taskProgress[1].currentScore; taskConfig[TaskType.WATCH].dayMaxScore = taskProgress[1].dayMaxScore; taskConfig[TaskType.WATCH].need = taskProgress[1].dayMaxScore - taskProgress[1].currentScore; // 每日答题 taskConfig[TaskType.PRACTICE].currentScore = taskProgress[3].currentScore; taskConfig[TaskType.PRACTICE].dayMaxScore = taskProgress[3].dayMaxScore; taskConfig[TaskType.PRACTICE].need = taskProgress[3].dayMaxScore; // 更新数据 for (const i in taskConfig) { const { currentScore, dayMaxScore } = taskConfig[i]; // 进度 const rate = Number(((100 * currentScore) / dayMaxScore).toFixed(1)); // 分数 taskConfig[i].score = currentScore; // 完成状态 taskConfig[i].status = rate === 100; } return; } // 重试 await sleep(2000); refreshTaskList(); return; } function Tip({ text, count, show, delayShow, countShow, callback, }) { return createElementNode('div', undefined, { class: watchRef([show, delayShow], () => `egg_tip${show.value ? (delayShow.value ? ' active delay' : ' active') : ''}`), }, [ createElementNode('span', undefined, { class: 'egg_text', }, createTextNode(text)), watchEffectRef(() => countShow.value ? createElementNode('span', undefined, { class: 'egg_countdown', }, createTextNode(watchEffectRef(() => `${count.value}s`))) : undefined), ], { onMounted() { // 倒计时 const countDown = async () => { // 倒计时回调 await callback(count.value); // 倒计时结束 if (!count.value) { show.value = false; return; } count.value--; setTimeout(countDown, 1000); }; countDown(); }, }); } /** * @description 分隔符 * @returns */ function Hr({ text }) { return createElementNode('div', undefined, { class: 'egg_hr_wrap', }, [ createElementNode('div', undefined, { class: 'egg_hr' }), createElementNode('div', undefined, { class: 'egg_hr_title' }, createTextNode(text)), createElementNode('div', undefined, { class: 'egg_hr' }), ]); } function Select({ data, maxlength, placeholder = '', onchange, onblur, value, keep, }) { const selectData = reactive(data.map((v) => ({ selected: false, active: false, ele: undefined, ...v }))); const focus = ref(false); const input = shallowRef(undefined); const list = shallowRef(undefined); const valueRef = ref(''); value && watch(value, () => { const item = selectData.find((v) => v.value === value.value); valueRef.value = item ? item.label : ''; if (!item) { selectData.forEach((v) => (v.selected = false)); list.value && (list.value.scrollTop = 0); } }, true); return createElementNode('div', undefined, { class: 'egg_select', }, [ createElementNode('input', { value: valueRef }, { class: 'egg_select_input', type: 'text', placeholder, maxlength, ref: input, onfocus() { if (list.value && input.value) { focus.value = true; if (input.value.value && valueRef.value) { const index = selectData.findIndex((v) => v.label === valueRef.value); if (index + 1) { list.value.scrollTop = selectData[index].ele?.offsetTop || 0; selectData.forEach((v, i) => (v.selected = i === index)); } return; } } }, oninput() { if (list.value && input.value) { const { value } = input.value; // 文本存在 if (value) { const index = selectData.findIndex((v) => v.label.includes(value)); // 存在匹配 if (index + 1) { list.value.scrollTop = selectData[index].ele?.offsetTop || 0; selectData.forEach((v, i) => { v.active = i === index; v.active && setTimeout(() => { v.active = false; }, 300); }); } return; } // 清除 selectData.forEach((v) => (v.active = v.selected = false)); list.value.scrollTop = 0; } }, onblur() { if (list.value && input.value) { const item = selectData.find((v) => v.selected); // 关闭选项 if (item || !input.value.value) { setTimeout(() => { focus.value = false; }, 100); } // 恢复文本 if (item && input.value.value !== item.label) { input.value.value = item.label; } // 保留文本 if (!item && keep) { input.value.value = valueRef.value; } onblur && onblur(item ? { label: item.label, value: item.value } : undefined); } }, }), createElementNode('div', undefined, { class: watchEffectRef(() => `egg_select_list${focus.value ? '' : ' hide'}`), ref: list, }, selectData.map((v, index) => createElementNode('div', undefined, { class: watchRef(() => [v.selected, v.active], () => `egg_select_item${v.selected ? ' selected' : v.active ? ' active' : ''}`), ref: (e) => (v.ele = e), onclick: debounce(() => { if (valueRef.value !== v.label) { onchange && onchange({ label: v.label, value: v.value }); selectData.forEach((v, i) => { v.selected = i === index; v.selected && (valueRef.value = v.label); }); } focus.value = false; }, 300), }, createTextNode(v.label)))), ]); } /** * @description 答题按钮 */ function ExamBtn() { // 设置初始状态 watchEffect(() => (examPause.value = !settings[SettingType.AUTO_ANSWER])); return createElementNode('button', undefined, { class: watchEffectRef(() => `egg_exam_btn${examPause.value ? ' manual' : ''}`), type: 'button', onclick(e) { e.stopPropagation(); examPause.value = !examPause.value; }, onmousedown(e) { e.stopPropagation(); }, onmousemove(e) { e.stopPropagation(); }, onmouseup(e) { e.stopPropagation(); }, onmouseenter(e) { e.stopPropagation(); }, onmouseleave(e) { e.stopPropagation(); }, onmouseover(e) { e.stopPropagation(); }, ontouchstart(e) { e.stopPropagation(); }, ontouchmove(e) { e.stopPropagation(); }, ontouchend(e) { e.stopPropagation(); }, oninput(e) { e.stopPropagation(); }, onchange(e) { e.stopPropagation(); }, onblur(e) { e.stopPropagation(); }, }, createTextNode(watchEffectRef(() => `${examPause.value ? '开启自动答题' : '关闭自动答题'}`))); } /** * @description 任务窗口 * @returns */ function Frame() { // 最大化 const max = ref(false); // 容器 return createElementNode('div', undefined, { class: watchEffectRef(() => `egg_frame_wrap${frame.show ? '' : ' hide'}`), onclick(e) { e.stopPropagation(); }, onmousedown(e) { e.stopPropagation(); }, onmousemove(e) { e.stopPropagation(); }, onmouseup(e) { e.stopPropagation(); }, onmouseenter(e) { e.stopPropagation(); }, onmouseleave(e) { e.stopPropagation(); }, onmouseover(e) { e.stopPropagation(); }, ontouchstart(e) { e.stopPropagation(); }, ontouchmove(e) { e.stopPropagation(); }, ontouchend(e) { e.stopPropagation(); }, oninput(e) { e.stopPropagation(); }, onchange(e) { e.stopPropagation(); }, onblur(e) { e.stopPropagation(); }, }, watchRef(() => [login.value, settings[SettingType.SAME_TAB]], () => { // 同屏任务 if (login.value && settings[SettingType.SAME_TAB]) { return [ // 遮罩 createElementNode('div', undefined, { class: 'egg_frame_mask' }), // 窗口内容 createElementNode('div', undefined, { class: watchEffectRef(() => `egg_frame_content_wrap ${max.value ? ' max' : ''}`), }, [ // 窗口控制 createElementNode('div', undefined, { class: 'egg_frame_controls_wrap' }, [ // 标题 createElementNode('div', undefined, { class: 'egg_frame_title', }), createElementNode('div', undefined, { class: 'egg_frame_controls', }, [ // 隐藏 createElementNode('button', undefined, { class: 'egg_frame_btn', type: 'button', title: '隐藏', onclick: debounce(() => { // 隐藏窗口 frame.show = false; }, 300), }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { 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', }))), // 改变大小 createElementNode('button', undefined, { class: 'egg_frame_btn', type: 'button', title: '缩放', onclick: debounce(() => { max.value = !max.value; }, 300), }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { 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', }))), // 关闭 createElementNode('button', undefined, { class: 'egg_frame_btn', type: 'button', title: '关闭', onclick: debounce(() => { // 关闭窗口 closeFrame(); }, 300), }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { 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', }))), ]), ]), // 窗口内容 createElementNode('div', undefined, { class: 'egg_frame_content', }, watchEffectRef(() => frame.src ? [ createElementNode('iframe', undefined, { class: 'egg_frame', src: frame.src, ref(ele) { frame.ele = ele; }, }, undefined), ] : undefined)), ], { onMounted() { // 隐藏窗口 watch(() => [ taskStatus.value, running.value, settings[SettingType.SAME_TAB], settings[SettingType.SILENT_RUN], ], () => { // 同屏任务 if (settings[SettingType.SAME_TAB] && (taskStatus.value === TaskStatusType.START || taskStatus.value === TaskStatusType.PAUSE || running.value)) { // 设置窗口显示 frame.show = !settings[SettingType.SILENT_RUN]; } }); }, }), ]; } }), { onMounted() { // 关闭窗口 watch(() => [login.value, settings[SettingType.SAME_TAB]], () => { if (login.value) { if (settings[SettingType.SAME_TAB]) { frame.exist = true; closeWin(); } else { closeFrame(); frame.exist = false; } } else { closeWin(); closeFrame(); frame.exist = false; } }); }, }); } /** * @description 登录 */ function LoginItem() { return watchEffectRef(() => { return login.value ? undefined : createElementNode('div', undefined, { class: 'egg_login_item', }, [ // 登录按钮 createElementNode('button', undefined, { type: 'button', class: 'egg_login_btn', onclick: debounce(async () => { // 开始登录 handleLogin(); }, 300), }, createTextNode('扫码登录')), // 窗口 createElementNode('div', undefined, { class: watchEffectRef(() => `egg_login_img_wrap${loginQRCodeShow.value ? ' active' : ''}`), }, createElementNode('img', undefined, { class: 'egg_login_img', })), ], { onMounted() { watch(() => settings[SettingType.SCHEDULE_RUN], () => { // 未开启定时展示二维码 if (!settings[SettingType.SCHEDULE_RUN]) { // 开始登录 handleLogin(); } }, true); }, }); }); } /** * @description 信息 * @returns */ function InfoItem() { return watchEffectRef(() => { if (login.value) { return createElementNode('div', undefined, { class: 'egg_info_item', }, [ // 用户信息 createElementNode('div', undefined, { class: 'egg_userinfo' }, [ // 头像 createElementNode('div', undefined, { class: 'egg_avatar' }, watchEffectRef(() => { return [ userinfo.avatar ? createElementNode('img', undefined, { src: userinfo.avatar, class: 'egg_avatar_img', }) : createElementNode('div', undefined, { class: 'egg_avatar_nick', }, createTextNode(watchEffectRef(() => userinfo.nick.substring(1, 3)))), ]; })), // 昵称 createElementNode('div', undefined, { class: 'egg_nick' }, createTextNode(watchEffectRef(() => userinfo.nick))), ]), // 退出按钮 createElementNode('button', undefined, { type: 'button', class: 'egg_login_btn', onclick: debounce(() => { // 退出登录 handleLogout(); }, 300), }, createTextNode('退出')), ], { onMounted() { // 刷新用户信息 refreshUserInfo(); }, }); } }); } /** * @description 分数详情 */ function ScoreItem() { return watchEffectRef(() => { if (login.value) { // 分数显示 const scoreShow = ref(false); // 分数信息 return createElementNode('div', undefined, { class: 'egg_score_item', }, createElementNode('div', undefined, { class: 'egg_scoreinfo' }, [ createElementNode('div', undefined, { class: 'egg_totalscore', }, [ createTextNode('总积分'), createElementNode('span', undefined, undefined, createTextNode(totalScore)), ]), createElementNode('div', undefined, { class: 'egg_todayscore', }, [ createElementNode('button', undefined, { type: 'button', class: 'egg_todayscore_btn', title: '查看分数详情', onclick: debounce(() => { scoreShow.value = !scoreShow.value; }, 300), onblur: () => { scoreShow.value = false; }, }, [ createTextNode('当天分数'), // 当天分数 createElementNode('span', undefined, undefined, createTextNode(todayScore)), // icon createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { 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', })), createElementNode('div', undefined, { class: watchEffectRef(() => `egg_score_details${scoreShow.value ? '' : ' hide'}`), }, [ createElementNode('div', undefined, { class: 'egg_score_title' }, [ createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, [ createNSElementNode('path', undefined, { d: 'M314.81 304.01h415.86v58.91H314.81zM314.81 440.24h415.86v58.91H314.81z', }), createNSElementNode('path', undefined, { 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', }), ]), createElementNode('div', undefined, { class: 'egg_score_title_text', }, createTextNode('积分详情')), ]), ...taskConfig.map((task) => createElementNode('div', undefined, { class: 'egg_score_item' }, [ createTextNode(task.title), createElementNode('span', undefined, { class: 'egg_score_detail', }, createTextNode(watchEffectRef(() => task.score))), ])), ]), ]), ]), ]), { onMounted() { // 刷新分数信息 refreshScoreInfo(); }, }); } }); } /** * @description 设置普通项 * @returns */ function NormalItem({ title, tip, checked, onchange, }) { return createElementNode('div', undefined, { class: 'egg_setting_item' }, [ createElementNode('div', undefined, { class: 'egg_label_wrap' }, [ createElementNode('label', undefined, { class: 'egg_task_title' }, [ createTextNode(title), createElementNode('span', undefined, { class: 'egg_detail', title: tip, }, createTextNode('i')), ]), ]), createElementNode('input', undefined, { title: tip, class: 'egg_switch', type: 'checkbox', checked, onchange, }), ]); } /** * @description 设置任务项 * @returns */ function TaskItem({ title, tip, checked, currentScore, dayMaxScore, onchange, immutable, }) { return createElementNode('div', undefined, { class: 'egg_task_item', }, [ createElementNode('div', undefined, { class: 'egg_label_wrap' }, [ createElementNode('div', undefined, { class: 'egg_task_title_wrap' }, [ createElementNode('div', undefined, { class: 'egg_task_title' }, createTextNode(title)), createElementNode('div', undefined, { class: 'egg_task_progress_wrap' }, [ createElementNode('div', undefined, { class: 'egg_task_current', }, createTextNode(currentScore)), createElementNode('div', undefined, { class: 'egg_task_max', }, createTextNode(watchEffectRef(() => `/${dayMaxScore.value}`))), ]), ]), createElementNode('div', undefined, { class: 'egg_progress' }, [ createElementNode('div', undefined, { class: 'egg_track' }, createElementNode('div', undefined, { class: 'egg_bar', style: watchEffectRef(() => `width: ${((100 * currentScore.value) / dayMaxScore.value).toFixed(1)}%;`), })), ]), ]), createElementNode('input', undefined, { title: tip, class: 'egg_switch', type: 'checkbox', checked, onchange, disabled: immutable, }), ]); } /** * @description 任务 */ function TaskList() { // 处理任务设置变化 const handleTaskChange = (e, type, title) => { // 开关 const { checked } = e.target; if (taskConfig[type].active !== checked) { taskConfig[type].active = checked; // 设置 GM_setValue('taskConfig', JSON.stringify(taskConfig)); // 创建提示 createTip(`${title} ${checked ? '打开' : '关闭'}!`); } }; // 登录加载 watch(login, async () => { if (login.value) { // 加载任务列表 await refreshTaskList(); // 未完成任务 if (taskConfig.some((task) => task.active && !task.status)) { // 全局暂停 GM_setValue('pauseStudy', false); // 加载完毕 taskStatus.value = TaskStatusType.LOADED; return; } // 任务完毕 taskStatus.value = TaskStatusType.FINISH; } }, true); return createElementNode('div', undefined, { class: 'egg_task_list', }, taskConfig.map((label) => label.immutable ? TaskItem({ title: label.title, tip: label.tip, checked: watchEffectRef(() => label.active), currentScore: watchEffectRef(() => label.currentScore), dayMaxScore: watchEffectRef(() => label.dayMaxScore), onchange: debounce((e) => { handleTaskChange(e, label.type, label.title); }, 300), immutable: label.immutable, }) : TaskItem({ title: label.title, tip: label.tip, checked: watchEffectRef(() => label.active), currentScore: watchEffectRef(() => label.currentScore), dayMaxScore: watchEffectRef(() => label.dayMaxScore), onchange: debounce((e) => { handleTaskChange(e, label.type, label.title); }, 300), immutable: label.immutable, }))); } /** * @description 任务按钮 */ function TaskBtn() { return watchEffectRef(() => { if (login.value) { /** * @description 学习 */ async function study() { // 创建提示 createTip('开始学习!'); // 暂停 await studyPauseLock(); // 文章选读 if (taskConfig[TaskType.READ].active && !taskConfig[TaskType.READ].status) { log('任务一: 文章选读'); // 创建提示 createTip('任务一: 文章选读'); // 暂停 await studyPauseLock(); // 看新闻 await readNews(); } log('任务一: 文章选读已完成!'); // 视听学习 if (taskConfig[TaskType.WATCH].active && !taskConfig[TaskType.WATCH].status) { log('任务二: 视听学习'); // 创建提示 createTip('任务二: 视听学习'); // 暂停 await studyPauseLock(); // 看视频 await watchVideo(); } log('任务二: 视听学习已完成!'); // 每日答题 if (taskConfig[TaskType.PRACTICE].active && !taskConfig[TaskType.PRACTICE].status) { log('任务三: 每日答题'); // 创建提示 createTip('任务三: 每日答题'); // 暂停 await studyPauseLock(); // 做每日答题 await doExamPractice(); } log('任务三: 每日答题已完成!'); } /** * @description 暂停任务 */ function pauseTask() { // 全局暂停 GM_setValue('pauseStudy', true); taskStatus.value = TaskStatusType.PAUSE; } /** * @description 继续任务 */ function continueTask() { // 全局暂停 GM_setValue('pauseStudy', false); taskStatus.value = TaskStatusType.START; } /** * @description 开始任务 */ async function startTask() { // 未完成任务 if (taskConfig.some((task) => task.active && !task.status)) { // 开始任务 taskStatus.value = TaskStatusType.START; try { // 学习 await study(); // 同屏任务 if (settings[SettingType.SAME_TAB]) { // 关闭窗口 closeFrame(); // 窗口不存在 frame.exist = false; } } catch (err) { if (err instanceof Error) { // 提示 createTip(err.message); // 错误 error(err.message); return; } // 提示 createTip(String(err)); // 错误 error(err); } } // 刷新任务 taskStatus.value = TaskStatusType.FINISH; log('已完成'); // 创建提示 createTip('完成学习!'); // 远程推送 if (settings[SettingType.REMOTE_PUSH]) { // 推送 const res = await pushModal({ title: '学习推送', to: userinfo.nick, content: [ '学习强国, 学习完成!', `当天积分: ${getHighlightHTML(todayScore.value)} 分`, `总积分: ${getHighlightHTML(totalScore.value)} 分`, ...taskConfig.map((task) => getProgressHTML(task.title, task.currentScore, task.dayMaxScore)), ], type: 'success', }, pushToken.value); createTip(`学习推送${res ? '成功' : '失败'}!`); } } // 已在等待 let flag = false; // 自动答题 watch(() => [taskStatus.value, settings[SettingType.AUTO_START]], async () => { // 加载完毕 if (!flag && taskStatus.value === TaskStatusType.LOADED) { // 自动答题 if (settings[SettingType.AUTO_START]) { // 等待中 flag = true; // 创建提示 const tip = createTip('即将自动开始任务', 5, true); // 等待倒计时结束 await tip.waitCountDown(); // 再次查看是否开启 if (settings[SettingType.AUTO_START] && taskStatus.value !== TaskStatusType.START) { // 创建提示 createTip('自动开始任务'); // 开始任务 startTask(); return; } // 取消等待 flag = false; // 创建提示 createTip('已取消自动开始任务!'); } } }); // 切换开关任务未完成 taskConfig.forEach((task) => { watch(() => [task.active], () => { if (taskStatus.value === TaskStatusType.FINISH) { if (task.active && !task.status) { taskStatus.value = TaskStatusType.LOADED; } } }); }); return createElementNode('div', undefined, { class: 'egg_study_item' }, createElementNode('button', undefined, { class: watchEffectRef(() => `egg_study_btn${taskStatus.value === TaskStatusType.START ? ' loading' : ''}`), type: 'button', disabled: watchRef(() => [running.value, taskStatus.value], () => running.value || taskStatus.value === TaskStatusType.LOADING || taskStatus.value === TaskStatusType.FINISH), onclick: watchEffectRef(() => taskStatus.value === TaskStatusType.LOADED ? debounce(startTask, 300) : taskStatus.value === TaskStatusType.START ? debounce(pauseTask, 300) : taskStatus.value === TaskStatusType.PAUSE ? debounce(continueTask, 300) : undefined), }, createTextNode(watchEffectRef(() => `${taskStatus.value === TaskStatusType.LOADING ? '等待中' : taskStatus.value === TaskStatusType.LOADED ? '开始学习' : taskStatus.value === TaskStatusType.START ? '正在学习, 点击暂停' : taskStatus.value === TaskStatusType.PAUSE ? '继续学习' : taskStatus.value === TaskStatusType.FINISH ? '已完成' : ''}`)))); } }); } /** * @description 定时项目 * @returns */ function ScheduleList() { return createElementNode('div', undefined, { class: 'egg_schedule_list' }, watchEffectRef(() => { return scheduleList.length ? scheduleList.map((schedule) => createElementNode('div', undefined, { class: 'egg_schedule_item' }, [ createElementNode('div', undefined, { class: `egg_schedule_detail_time_wrap${isLate(schedule) ? ' inactive' : ''}`, }, [ createElementNode('div', undefined, { class: 'egg_schedule_detail_icon', }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, [ createNSElementNode('path', undefined, { 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', }), createNSElementNode('path', undefined, { 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', }), ])), createElementNode('div', undefined, { class: 'egg_schedule_detail_time' }, createTextNode(schedule.time)), ]), createElementNode('div', undefined, { class: 'egg_schedule_detail_del_wrap' }, [ createElementNode('button', undefined, { class: 'egg_schedule_del_btn', onclick: debounce(() => { // 定时刷新 if (!settings[SettingType.SCHEDULE_RUN]) { createTip('未开启定时刷新!'); return; } // 索引 const index = scheduleList.findIndex((s) => s === schedule); // 删除元素 scheduleList.splice(index, 1); // 存储 GM_setValue('scheduleList', JSON.stringify(scheduleList)); // 刷新任务 refreshScheduleTask(); }, 300), }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, [ createNSElementNode('path', undefined, { 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', }), createNSElementNode('path', undefined, { 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', }), ])), ]), ])) : [ createElementNode('div', undefined, { class: 'egg_schedule_list_none' }, [ createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, [ createNSElementNode('path', undefined, { 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', }), createNSElementNode('path', undefined, { 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', }), createNSElementNode('path', undefined, { 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', }), ]), createElementNode('div', undefined, { class: 'egg_schedule_list_none_text', }, createTextNode('暂无定时任务')), ]), ]; })); } /** * @description 时间输入 * @returns */ function TimeInput({ hour, minute, onchange, }) { // 小时 const hours = new Array(24).fill(undefined).map((v, i) => ({ value: i, label: formatDateNum(i), })); // 分钟 const minutes = new Array(60).fill(undefined).map((v, i) => ({ value: i, label: formatDateNum(i), })); const valueRef = watchEffectRef(() => { const h = hours.find((h) => h.value === hour.value); const min = minutes.find((min) => min.value === minute.value); return { hour: h ? h.value : -1, minute: min ? min.value : -1, }; }); return createElementNode('div', undefined, { class: 'egg_time_input' }, [ createElementNode('div', undefined, { class: 'egg_hour_wrap' }, [ Select({ data: hours, placeholder: '00', maxlength: 2, value: hour, onchange({ value }) { valueRef.value.hour = value; onchange && onchange(valueRef.value); }, onblur(res) { if (!res) { valueRef.value.hour = -1; onchange && onchange(valueRef.value); } }, }), ]), createElementNode('span', undefined, { class: 'egg_separator' }, createTextNode(':')), createElementNode('div', undefined, { class: 'egg_minute_wrap' }, [ Select({ data: minutes, placeholder: '00', maxlength: 2, value: minute, onchange({ value }) { valueRef.value.minute = value; onchange && onchange(valueRef.value); }, onblur(res) { if (!res) { valueRef.value.minute = -1; onchange && onchange(valueRef.value); } }, }), ]), ]); } /** * @description 设置面板组件 * @returns */ function SettingsPanel({ show }) { // token let token = ''; // 小时 let hour = ref(-1); // 分钟 let minute = ref(-1); return createElementNode('div', undefined, { class: watchEffectRef(() => `egg_settings${show.value ? ' active' : ''}`), }, [ createElementNode('div', undefined, { class: 'egg_settings_version_wrap' }, [ createElementNode('div', undefined, { class: 'egg_settings_label' }, createTextNode('版本信息')), createElementNode('div', undefined, { class: 'egg_settings_version', }, [ createTextNode(`v${version}`), createElementNode('a', undefined, { class: 'egg_settings_version_detail', title: 'GitHub Xu22Web/tech-study-js', href: 'https://github.com/Xu22Web/tech-study-js', }, createNSElementNode('svg', undefined, { viewBox: '0 0 16 16', class: 'egg_icon', }, createNSElementNode('path', undefined, { d: 'M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z', }))), ]), ]), createElementNode('div', undefined, { class: 'egg_settings_theme_wrap' }, [ createElementNode('div', undefined, { class: 'egg_settings_label' }, createTextNode('主题预设')), createElementNode('div', undefined, { class: 'egg_settings_theme_colors' }, [ { value: '#fa3333', title: '强国红', detail: 'XueXi Red', code: 'none', }, { value: '#bb2649', title: '非凡洋红', detail: 'Viva Magenta', code: '18-1750', }, { value: '#35548a', title: '经典蓝', detail: 'Classic Blue', code: '19-4052', }, { value: '#f36f63', title: '活珊瑚橘', detail: 'Living Coral', code: '16-1546', }, { value: '#6d5b97', title: '紫外光色', detail: 'Ultra Violet', code: '18-3838', }, { value: '#86af49', title: '草木绿', detail: 'Greenery', code: '15-0343', }, { value: '#fc8bab', title: 'B站粉', detail: 'Bilibili Pink', code: 'none', }, { value: '#056de8', title: '知乎蓝', detail: 'Zhihu Blue', code: 'none', }, ].map((color) => createElementNode('div', undefined, { class: 'egg_settings_theme_color_wrap', }, createElementNode('button', undefined, { class: 'egg_settings_theme_color', type: 'button', style: watchEffectRef(() => `color: ${color.value};${themeColor.value === color.value ? '' : ` box-shadow: 0rem 0.4rem 0.1rem 0.1rem ${color.value}30;`}`), title: color.title, onclick: debounce(() => { if (themeColor.value !== color.value) { themeColor.value = color.value; // 存储 GM_setValue('themeColor', themeColor.value); } }, 300), })))), ]), createElementNode('div', undefined, { class: 'egg_settings_read_time_wrap', }, [ createElementNode('div', undefined, { class: 'egg_settings_label' }, createTextNode('最大文章时长')), Select({ data: [ { label: '40s', value: 40, }, { label: '60s', value: 60, }, { label: '80s', value: 80, }, { label: '100s', value: 100, }, ], value: maxRead, placeholder: '100s', maxlength: 4, keep: true, onchange({ value }) { // 创建提示 createTip('最大文章时长 已保存!'); maxRead.value = value; // 存储 GM_setValue('maxRead', value); }, }), ], { onMounted() { try { const maxReadTemp = GM_getValue('maxRead'); if (maxReadTemp) { maxRead.value = maxReadTemp; } } catch (e) { } }, }), createElementNode('div', undefined, { class: 'egg_settings_watch_time_wrap', }, [ createElementNode('div', undefined, { class: 'egg_settings_label' }, createTextNode('最大视听时长')), Select({ data: [ { label: '40s', value: 40, }, { label: '60s', value: 60, }, { label: '80s', value: 80, }, { label: '100s', value: 100, }, { label: '120s', value: 120, }, ], value: maxWatch, placeholder: '120s', maxlength: 4, keep: true, onchange({ value }) { // 创建提示 createTip('最大视听时长 已保存!'); maxWatch.value = value; // 存储 GM_setValue('maxWatch', value); }, }), ], { onMounted() { try { const maxWatchTemp = GM_getValue('maxWatch'); if (maxWatchTemp) { maxWatch.value = maxWatchTemp; } } catch (e) { } }, }), watchEffectRef(() => settings[SettingType.REMOTE_PUSH] ? createElementNode('div', undefined, { class: 'egg_settings_token_wrap' }, [ createElementNode('div', undefined, { class: 'egg_settings_token' }, [ createElementNode('div', undefined, { class: 'egg_settings_label' }, createTextNode('我的 token')), createElementNode('input', undefined, { class: 'egg_settings_token_input', placeholder: '用户 token', maxlength: 32, value: pushToken.value, onfocus: (e) => { const input = e.target; input.classList.add('active'); const btnWrap = $$('.egg_settings_submit_btn_wrap')[0]; btnWrap.classList.add('active'); }, onblur: (e) => { const input = e.target; // 去除空格 const value = input.value.trim(); if (/^[0-9a-z]{32}$/.test(value)) { token = value; input.value = value; } else { token = ''; } input.classList.remove('active'); setTimeout(() => { const btnWrap = $$('.egg_settings_submit_btn_wrap')[0]; btnWrap.classList.remove('active'); input.value = pushToken.value; }, 200); }, }), ]), createElementNode('div', undefined, { class: 'egg_settings_submit_btn_wrap' }, createElementNode('button', undefined, { class: 'egg_settings_submit_btn', onclick: debounce(() => { // 创建提示 createTip('用户 token 已保存!'); if (token !== pushToken.value) { pushToken.value = token; // 存储 GM_setValue('pushToken', token); } }, 300), }, createTextNode('保存'))), ]) : undefined), watchEffectRef(() => settings[SettingType.SCHEDULE_RUN] ? createElementNode('div', undefined, { class: 'egg_schedule' }, [ createElementNode('div', undefined, { class: 'egg_schedule_time_wrap' }, [ createElementNode('div', undefined, { class: 'egg_schedule_time' }, [ createElementNode('div', undefined, { class: 'egg_schedule_label' }, createTextNode('设置时间')), createElementNode('div', undefined, { class: 'egg_schedule_time_input_wrap' }, [ TimeInput({ hour, minute, onchange({ hour: h, minute: min }) { hour.value = h; minute.value = min; }, }), createElementNode('button', undefined, { class: 'egg_schedule_add_btn', onclick: debounce(() => { // 定时刷新 if (!settings[SettingType.SCHEDULE_RUN]) { createTip('未开启定时刷新!'); return; } if (hour.value === -1 || minute.value === -1) { createTip('时间格式不符合要求!'); return; } // 重复定时存在 const exists = scheduleList.find((schedule) => schedule.hour === hour.value && schedule.minute === minute.value); if (exists) { createTip('设置定时任务重复!'); return; } createTip('设置定时任务成功!'); // 添加 scheduleList.push({ hour: hour.value, minute: minute.value, time: `${formatDateNum(hour.value)}:${formatDateNum(minute.value)}`, }); // 排序 scheduleList.sort((a, b) => a.hour === b.hour ? a.minute - b.minute : a.hour - b.hour); // 存储 GM_setValue('scheduleList', JSON.stringify(scheduleList)); // 清空 hour.value = -1; minute.value = -1; const inputs = $$('.egg_time_input input'); inputs.forEach((i) => (i.value = '')); // 刷新任务 refreshScheduleTask(); }, 300), }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { d: 'M801.171 483.589H544V226.418c0-17.673-14.327-32-32-32s-32 14.327-32 32v257.171H222.83c-17.673 0-32 14.327-32 32s14.327 32 32 32H480v257.17c0 17.673 14.327 32 32 32s32-14.327 32-32v-257.17h257.171c17.673 0 32-14.327 32-32s-14.327-32-32-32z', }))), ]), ]), ]), ScheduleList(), ]) : undefined), ], { onMounted() { // 刷新token watch(() => settings[SettingType.REMOTE_PUSH], () => { // 远程推送 if (settings[SettingType.REMOTE_PUSH]) { try { const tokenTemp = GM_getValue('pushToken'); if (tokenTemp) { pushToken.value = tokenTemp; } } catch (e) { } } }, true); // 刷新定时任务 watch(() => settings[SettingType.SCHEDULE_RUN], () => { // 定时任务打开 if (settings[SettingType.SCHEDULE_RUN]) { try { const scheduleTemp = JSON.parse(GM_getValue('scheduleList')); if (scheduleTemp && Array.isArray(scheduleTemp)) { for (const i in scheduleTemp) { scheduleList[i] = scheduleTemp[i]; } } } catch (e) { } // 刷新定时任务 refreshScheduleTask(); return; } // 清除任务 clearScheduleTask(); }, true); }, }); } /** * @description 面板 * @returns */ function Panel() { // 运行设置标签 const runLabels = [ { title: '自动开始', tip: '启动时, 自动开始任务, 在倒计时结束前自动开始可随时取消; 如果在自动开始前手动开始任务, 此次自动开始将取消', type: SettingType.AUTO_START, }, { title: '同屏任务', tip: '运行任务时,所有任务均在当前页面以弹窗方式运行', type: SettingType.SAME_TAB, }, { title: '静默运行', tip: '同屏任务时, 不显示任务弹窗静默运行', type: SettingType.SILENT_RUN, }, { title: '定时刷新', tip: '定时刷新页面,重新进行任务,此功能需要长时间占用浏览器', type: SettingType.SCHEDULE_RUN, }, { title: '视频静音', tip: '视听学习时,静音播放视频', type: SettingType.VIDEO_MUTED, }, ]; // 运行设置标签 const examLabels = [ { title: '随机作答', tip: '无答案时, 随机选择或者填入答案, 不保证正确', type: SettingType.RANDOM_EXAM, }, { title: '自动答题', tip: '进入答题页面时,自动答题并提交答案', type: SettingType.AUTO_ANSWER, }, ]; // 推送设置标签 const pushLabels = [ { title: '远程推送', tip: '利用 pushplus 推送, 将登录二维码直接推送到微信公众号', type: SettingType.REMOTE_PUSH, }, ]; // 处理设置变化 const handleSettingsChange = (e, type, title) => { // 开关 const { checked } = e.target; if (settings[type] !== checked) { settings[type] = checked; // 设置 GM_setValue('studySettings', JSON.stringify(settings)); // 创建提示 createTip(`${title} ${checked ? '打开' : '关闭'}!`); } }; // 任务显示 const scheduleShow = ref(false); // 面板显示 const panelShow = ref(false); return createElementNode('div', undefined, { class: `egg_panel_wrap${hasMobile() ? ' mobile' : ''}`, onclick(e) { e.stopPropagation(); }, onmousedown(e) { e.stopPropagation(); }, onmousemove(e) { e.stopPropagation(); }, onmouseup(e) { e.stopPropagation(); }, onmouseenter(e) { e.stopPropagation(); }, onmouseleave(e) { e.stopPropagation(); }, onmouseover(e) { e.stopPropagation(); }, ontouchstart(e) { e.stopPropagation(); }, ontouchmove(e) { e.stopPropagation(); }, ontouchend(e) { e.stopPropagation(); }, oninput(e) { e.stopPropagation(); }, onchange(e) { e.stopPropagation(); }, onblur(e) { e.stopPropagation(); }, }, createElementNode('div', undefined, { class: watchEffectRef(() => `egg_panel${panelShow.value ? ' hide' : ''}`), }, [ // 登录 LoginItem(), // 信息 InfoItem(), // 分数 ScoreItem(), // 任务部分 Hr({ text: '任务' }), TaskList(), // 运行部分 Hr({ text: '运行' }), createElementNode('div', undefined, { class: 'egg_run_list' }, runLabels.map((label) => { return NormalItem({ title: label.title, tip: label.tip, checked: settings[label.type], onchange: debounce((e) => { handleSettingsChange(e, label.type, label.title); }, 300), }); })), // 答题部分 Hr({ text: '答题' }), createElementNode('div', undefined, { class: 'egg_exam_list' }, examLabels.map((label) => { return NormalItem({ title: label.title, tip: label.tip, checked: settings[label.type], onchange: debounce((e) => { handleSettingsChange(e, label.type, label.title); }, 300), }); })), // 推送部分 Hr({ text: '推送' }), createElementNode('div', undefined, { class: 'egg_push_list' }, pushLabels.map((label) => { return NormalItem({ title: label.title, tip: label.tip, checked: settings[label.type], onchange: debounce((e) => { handleSettingsChange(e, label.type, label.title); }, 300), }); })), // 提示部分 Hr({ text: '提示' }), createElementNode('div', undefined, { class: 'egg_tip_list' }, watchRef(login, () => login.value ? [ createTextNode('专项练习已被移除, 如需使用, 请点击'), createElementNode('button', undefined, { class: 'egg_tip_btn', type: 'button', onclick: debounce(doExamPaper, 300), disabled: watchRef(() => [running.value, taskStatus.value], () => running.value || taskStatus.value === TaskStatusType.START || taskStatus.value === TaskStatusType.PAUSE), }, createTextNode('去完成')), ] : [ createElementNode('div', undefined, { class: 'egg_tip_content' }, createTextNode('请先登录!')), ])), // 按钮集合 createElementNode('div', undefined, { class: 'egg_btns_wrap', }, [ createElementNode('button', undefined, { class: watchRef(() => [frame.exist, frame.show], () => `egg_frame_show_btn${!frame.exist || frame.show ? ' hide' : ''}`), title: '窗口', type: 'button', onclick: debounce(() => { // 窗口显示 frame.show = true; }, 300), }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { 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', }))), createElementNode('button', undefined, { class: 'egg_panel_show_btn', title: '面板', type: 'button', onclick: debounce(() => { panelShow.value = !panelShow.value; }, 300), }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, createNSElementNode('path', undefined, { 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', }))), createElementNode('button', undefined, { class: watchEffectRef(() => `egg_settings_show_btn${scheduleShow.value ? ' active' : ''}`), title: '设置', type: 'button', onclick: debounce(() => { scheduleShow.value = !scheduleShow.value; }, 300), }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, [ createNSElementNode('path', undefined, { 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', }), createNSElementNode('path', undefined, { 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', }), ])), createElementNode('button', undefined, { class: 'egg_settings_reset_btn', title: '重置', type: 'button', onclick: debounce(() => { // 任务配置 GM_setValue('taskConfig', null); // 设置 GM_setValue('studySettings', null); // 最大阅读 GM_setValue('maxRead', null); // 最大观看 GM_setValue('maxWatch', null); // 主题色 GM_setValue('themeColor', null); // 刷新页面 location.reload(); }, 300), }, createNSElementNode('svg', undefined, { viewBox: '0 0 1024 1024', class: 'egg_icon', }, [ createNSElementNode('path', undefined, { 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', }), ])), ]), // 任务按钮 TaskBtn(), createElementNode('div', undefined, { class: 'egg_settings_item' }, [ SettingsPanel({ show: scheduleShow }), ]), ])); } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { /* Visit https://aka.ms/tsconfig to read more about this file */ /* Projects */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, "lib": [ "DOM", "ESNext" ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ "module": "CommonJS" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ // "types": [] /* Specify type package names to be included without being referenced in a source file. */, // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "resolveJsonModule": true, /* Enable importing .json files. */ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ /* Emit */ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "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. */ // "outDir": "./", /* Specify an output folder for all emitted files. */ // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ // "newLine": "crlf", /* Set the newline character for emitting files. */ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ /* Interop Constraints */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, /* Type Checking */ "strict": true /* Enable all strict type-checking options. */, "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied 'any' type. */, // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "include": ["bin/*.ts", "src/**/*", "types/**/*.d.ts"] } ================================================ FILE: types/global.d.ts ================================================ declare global { module '*?raw' { const text: string; export default text; } function md5(value: string): string; } export {};