├── miniprogram ├── app.wxss ├── app.ts ├── pages │ ├── privacyTips │ │ ├── privacyTips.json │ │ ├── privacyTips.wxss │ │ ├── privacyTips.ts │ │ └── privacyTips.wxml │ ├── userInfo │ │ ├── userInfo.json │ │ ├── userInfo.wxss │ │ ├── userInfo.wxml │ │ └── userInfo.ts │ ├── locationPicker │ │ ├── locationPicker.json │ │ ├── locationPicker.wxss │ │ ├── locationPicker.ts │ │ └── locationPicker.wxml │ ├── index │ │ ├── index.json │ │ ├── index.wxss │ │ └── index.wxml │ └── userManager │ │ ├── userManager.json │ │ ├── userManager.wxss │ │ ├── userManager.wxml │ │ └── userManager.ts ├── sitemap.json ├── package.json ├── images │ └── location_on_FILL1_wght400_GRAD0_opsz48.svg ├── app.json ├── services │ ├── cxOrz_chaoxing-sign-cli_LICENSE │ ├── cloudStorage.ts │ ├── sign.ts │ ├── login.ts │ └── course.ts ├── package-lock.json ├── utils │ ├── types.ts │ └── util.ts └── miniprogram_npm │ └── @zlyboy │ └── wx-formdata │ ├── index.js │ └── index.js.map ├── typings ├── types │ ├── index.d.ts │ └── wx │ │ ├── lib.wx.behavior.d.ts │ │ ├── index.d.ts │ │ ├── lib.wx.page.d.ts │ │ ├── lib.wx.app.d.ts │ │ ├── lib.wx.component.d.ts │ │ └── lib.wx.cloud.d.ts └── index.d.ts ├── docs └── images │ ├── 上传.png │ ├── 预览.jpg │ ├── 审核管理.jpg │ ├── 导入源码.png │ ├── 成员管理.jpg │ ├── 效果图.png │ ├── 添加资格.jpg │ ├── 体验版二维码.jpg │ ├── 信任并运行.png │ ├── 导入小程序向导.png │ ├── 小程序二维码.jpg │ ├── 搜索受邀者.jpg │ ├── 选择小程序.jpg │ ├── 选择开发版.jpg │ ├── 配置服务器域名.png │ ├── GitHub下载.png │ ├── 登入微信开发者工具.jpg │ ├── Bitbucket下载.png │ ├── 小程序管理后台-开发设置.png │ └── 小程序管理后台-服务器域名.png ├── .gitignore ├── package.json ├── .eslintrc.js ├── tsconfig.json ├── project.config.json └── README.md /miniprogram/app.wxss: -------------------------------------------------------------------------------- 1 | /**app.wxss**/ 2 | -------------------------------------------------------------------------------- /typings/types/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /miniprogram/app.ts: -------------------------------------------------------------------------------- 1 | // app.ts 2 | App({ 3 | globalData: {}, 4 | onLaunch() {}, 5 | }); 6 | -------------------------------------------------------------------------------- /docs/images/上传.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/上传.png -------------------------------------------------------------------------------- /docs/images/预览.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/预览.jpg -------------------------------------------------------------------------------- /docs/images/审核管理.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/审核管理.jpg -------------------------------------------------------------------------------- /docs/images/导入源码.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/导入源码.png -------------------------------------------------------------------------------- /docs/images/成员管理.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/成员管理.jpg -------------------------------------------------------------------------------- /docs/images/效果图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/效果图.png -------------------------------------------------------------------------------- /docs/images/添加资格.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/添加资格.jpg -------------------------------------------------------------------------------- /docs/images/体验版二维码.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/体验版二维码.jpg -------------------------------------------------------------------------------- /docs/images/信任并运行.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/信任并运行.png -------------------------------------------------------------------------------- /docs/images/导入小程序向导.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/导入小程序向导.png -------------------------------------------------------------------------------- /docs/images/小程序二维码.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/小程序二维码.jpg -------------------------------------------------------------------------------- /docs/images/搜索受邀者.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/搜索受邀者.jpg -------------------------------------------------------------------------------- /docs/images/选择小程序.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/选择小程序.jpg -------------------------------------------------------------------------------- /docs/images/选择开发版.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/选择开发版.jpg -------------------------------------------------------------------------------- /docs/images/配置服务器域名.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/配置服务器域名.png -------------------------------------------------------------------------------- /miniprogram/pages/privacyTips/privacyTips.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "", 3 | "usingComponents": {} 4 | } 5 | -------------------------------------------------------------------------------- /miniprogram/pages/userInfo/userInfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "用户登入", 3 | "usingComponents": {} 4 | } 5 | -------------------------------------------------------------------------------- /docs/images/GitHub下载.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/GitHub下载.png -------------------------------------------------------------------------------- /docs/images/登入微信开发者工具.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/登入微信开发者工具.jpg -------------------------------------------------------------------------------- /docs/images/Bitbucket下载.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/Bitbucket下载.png -------------------------------------------------------------------------------- /docs/images/小程序管理后台-开发设置.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/小程序管理后台-开发设置.png -------------------------------------------------------------------------------- /docs/images/小程序管理后台-服务器域名.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroppingOutSpeedrun/dropping-out-speedrun/HEAD/docs/images/小程序管理后台-服务器域名.png -------------------------------------------------------------------------------- /miniprogram/pages/locationPicker/locationPicker.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "位置签到信息", 3 | "usingComponents": {} 4 | } 5 | -------------------------------------------------------------------------------- /miniprogram/pages/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "快进到退学", 3 | "enablePullDownRefresh": true, 4 | "usingComponents": {} 5 | } 6 | -------------------------------------------------------------------------------- /miniprogram/pages/userManager/userManager.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "用户管理", 3 | "enablePullDownRefresh": true, 4 | "usingComponents": {} 5 | } 6 | -------------------------------------------------------------------------------- /miniprogram/sitemap.json: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", 3 | "rules": [{ 4 | "action": "allow", 5 | "page": "*" 6 | }] 7 | } -------------------------------------------------------------------------------- /typings/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface IAppOption { 4 | globalData: { 5 | userInfo?: WechatMiniprogram.UserInfo, 6 | } 7 | userInfoReadyCallback?: WechatMiniprogram.GetUserInfoSuccessCallback, 8 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows 2 | [Dd]esktop.ini 3 | Thumbs.db 4 | $RECYCLE.BIN/ 5 | 6 | # macOS 7 | .DS_Store 8 | .fseventsd 9 | .Spotlight-V100 10 | .TemporaryItems 11 | .Trashes 12 | 13 | # Node.js 14 | node_modules/ 15 | 16 | project.private.config.json 17 | -------------------------------------------------------------------------------- /miniprogram/pages/userInfo/userInfo.wxss: -------------------------------------------------------------------------------- 1 | /* pages/userInfo/userInfo.wxss */ 2 | #container { 3 | padding-top: 0px; 4 | padding-bottom: calc(40px + constant(safe-area-inset-bottom)); 5 | padding-bottom: calc(40px + env(safe-area-inset-bottom)); 6 | } 7 | 8 | .weui-cells__group_form .weui-cells__tips_warn { 9 | color: var(--weui-RED); 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "miniprogram-ts-less-quickstart", 3 | "version": "1.0.0", 4 | "scripts": {}, 5 | "keywords": [], 6 | "author": "", 7 | "license": "ISC", 8 | "dependencies": { 9 | }, 10 | "devDependencies": { 11 | "miniprogram-api-typings": "^2.8.3-1" 12 | }, 13 | "main": ".eslintrc.js", 14 | "description": "" 15 | } 16 | -------------------------------------------------------------------------------- /miniprogram/pages/userManager/userManager.wxss: -------------------------------------------------------------------------------- 1 | /* pages/userManager/userManager.wxss */ 2 | #container { 3 | padding-top: 15px; 4 | padding-bottom: calc(40px + constant(safe-area-inset-bottom)); 5 | padding-bottom: calc(40px + env(safe-area-inset-bottom)); 6 | } 7 | 8 | #footer { 9 | padding-top: 20px; 10 | text-align: center; 11 | } 12 | 13 | .link { 14 | display: inline; 15 | color: var(--weui-LINK); 16 | } 17 | -------------------------------------------------------------------------------- /miniprogram/pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | /**index.wxss**/ 2 | #container { 3 | padding-top: 15px; 4 | padding-bottom: calc(40px + constant(safe-area-inset-bottom)); 5 | padding-bottom: calc(40px + env(safe-area-inset-bottom)); 6 | } 7 | 8 | #no-activity { 9 | text-align: center; 10 | } 11 | 12 | .weui-cell__desc { 13 | font-size: 0.7058823529411765rem; 14 | color: var(--weui-FG-2); 15 | line-height: 1.4; 16 | padding-top: 4px; 17 | } 18 | -------------------------------------------------------------------------------- /miniprogram/pages/locationPicker/locationPicker.wxss: -------------------------------------------------------------------------------- 1 | /* pages/locationPicker/locationPicker.wxss */ 2 | #container { 3 | padding-top: 0px; 4 | padding-bottom: calc(40px + constant(safe-area-inset-bottom)); 5 | padding-bottom: calc(40px + env(safe-area-inset-bottom)); 6 | } 7 | 8 | .link { 9 | display: inline; 10 | color: var(--weui-LINK); 11 | } 12 | 13 | .weui-cells__group_form .weui-cells__tips_warn { 14 | color: var(--weui-RED); 15 | } 16 | -------------------------------------------------------------------------------- /miniprogram/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "miniprogram", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@zlyboy/wx-formdata": "^1.0.2", 14 | "crypto-js": "^4.1.1" 15 | }, 16 | "devDependencies": { 17 | "@types/crypto-js": "^4.1.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /miniprogram/images/location_on_FILL1_wght400_GRAD0_opsz48.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /miniprogram/pages/privacyTips/privacyTips.wxss: -------------------------------------------------------------------------------- 1 | /* pages/remeberPasswordTips/remeberPasswordTips.wxss */ 2 | .container { 3 | padding-top: 15px; 4 | padding-bottom: calc(40px + constant(safe-area-inset-bottom)); 5 | padding-bottom: calc(40px + env(safe-area-inset-bottom)); 6 | } 7 | 8 | .link { 9 | display: inline; 10 | color: var(--weui-LINK); 11 | } 12 | 13 | .weui-article__ul { 14 | list-style: disc; 15 | margin-left: 1.2em; 16 | margin-bottom: 24px; 17 | } 18 | 19 | .weui-article__li { 20 | display: list-item; 21 | margin: .5em 0; 22 | } 23 | -------------------------------------------------------------------------------- /miniprogram/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "pages/index/index", 4 | "pages/userInfo/userInfo", 5 | "pages/userManager/userManager", 6 | "pages/privacyTips/privacyTips", 7 | "pages/locationPicker/locationPicker" 8 | ], 9 | "window": { 10 | "backgroundTextStyle": "light", 11 | "navigationBarBackgroundColor": "#fff", 12 | "navigationBarTitleText": "Weixin", 13 | "navigationBarTextStyle": "black" 14 | }, 15 | "style": "v2", 16 | "sitemapLocation": "sitemap.json", 17 | "lazyCodeLoading": "requiredComponents", 18 | "useExtendedLib": { 19 | "weui": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Eslint config file 3 | * Documentation: https://eslint.org/docs/user-guide/configuring/ 4 | * Install the Eslint extension before using this feature. 5 | */ 6 | module.exports = { 7 | env: { 8 | es6: true, 9 | browser: true, 10 | node: true, 11 | }, 12 | ecmaFeatures: { 13 | modules: true, 14 | }, 15 | parserOptions: { 16 | ecmaVersion: 2018, 17 | sourceType: 'module', 18 | }, 19 | globals: { 20 | wx: true, 21 | App: true, 22 | Page: true, 23 | getCurrentPages: true, 24 | getApp: true, 25 | Component: true, 26 | requirePlugin: true, 27 | requireMiniProgram: true, 28 | }, 29 | // extends: 'eslint:recommended', 30 | rules: {}, 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strictNullChecks": true, 4 | "noImplicitAny": true, 5 | "module": "CommonJS", 6 | "target": "ES2020", 7 | "allowJs": true, 8 | "allowSyntheticDefaultImports": true, 9 | "esModuleInterop": true, 10 | "experimentalDecorators": true, 11 | "noImplicitThis": true, 12 | "noImplicitReturns": true, 13 | "alwaysStrict": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "strict": true, 18 | "strictPropertyInitialization": true, 19 | "lib": ["ES2020"], 20 | "typeRoots": [ 21 | "./typings" 22 | ] 23 | }, 24 | "include": [ 25 | "./**/*.ts" 26 | ], 27 | "exclude": [ 28 | "node_modules" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目配置文件", 3 | "packOptions": { 4 | "ignore": [], 5 | "include": [] 6 | }, 7 | "miniprogramRoot": "miniprogram/", 8 | "compileType": "miniprogram", 9 | "projectname": "快进到退学", 10 | "setting": { 11 | "useCompilerPlugins": [ 12 | "typescript" 13 | ], 14 | "babelSetting": { 15 | "ignore": [], 16 | "disablePlugins": [], 17 | "outputPath": "" 18 | }, 19 | "condition": false, 20 | "es6": false, 21 | "enhance": false, 22 | "minified": true 23 | }, 24 | "simulatorType": "wechat", 25 | "simulatorPluginLibVersion": {}, 26 | "condition": {}, 27 | "srcMiniprogramRoot": "miniprogram/", 28 | "libVersion": "2.31.0", 29 | "editorSetting": { 30 | "tabIndent": "insertSpaces", 31 | "tabSize": 2 32 | } 33 | } -------------------------------------------------------------------------------- /miniprogram/pages/privacyTips/privacyTips.ts: -------------------------------------------------------------------------------- 1 | // pages/remeberPasswordTips/remeberPasswordTips.ts 2 | Page({ 3 | 4 | /** 5 | * 页面的初始数据 6 | */ 7 | data: { 8 | 9 | }, 10 | 11 | copyWeChatDocumentLink() { 12 | wx.setClipboardData({ 13 | data: 'https://developers.weixin.qq.com/miniprogram/dev/api/storage/wx.setStorage.html#Object-object', 14 | }); 15 | }, 16 | 17 | /** 18 | * 生命周期函数--监听页面加载 19 | */ 20 | onLoad() { 21 | 22 | }, 23 | 24 | /** 25 | * 生命周期函数--监听页面初次渲染完成 26 | */ 27 | onReady() { 28 | 29 | }, 30 | 31 | /** 32 | * 生命周期函数--监听页面显示 33 | */ 34 | onShow() { 35 | 36 | }, 37 | 38 | /** 39 | * 生命周期函数--监听页面隐藏 40 | */ 41 | onHide() { 42 | 43 | }, 44 | 45 | /** 46 | * 生命周期函数--监听页面卸载 47 | */ 48 | onUnload() { 49 | 50 | }, 51 | 52 | /** 53 | * 页面相关事件处理函数--监听用户下拉动作 54 | */ 55 | onPullDownRefresh() { 56 | 57 | }, 58 | 59 | /** 60 | * 页面上拉触底事件的处理函数 61 | */ 62 | onReachBottom() { 63 | 64 | }, 65 | 66 | /** 67 | * 用户点击右上角分享 68 | */ 69 | onShareAppMessage() { 70 | 71 | } 72 | }); 73 | -------------------------------------------------------------------------------- /miniprogram/services/cxOrz_chaoxing-sign-cli_LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 cxOrz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /miniprogram/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "miniprogram", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "miniprogram", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@zlyboy/wx-formdata": "^1.0.2", 13 | "crypto-js": "^4.1.1" 14 | }, 15 | "devDependencies": { 16 | "@types/crypto-js": "^4.1.1" 17 | } 18 | }, 19 | "node_modules/@types/crypto-js": { 20 | "version": "4.1.1", 21 | "resolved": "https://registry.npmmirror.com/@types/crypto-js/-/crypto-js-4.1.1.tgz", 22 | "integrity": "sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==", 23 | "dev": true 24 | }, 25 | "node_modules/@zlyboy/wx-formdata": { 26 | "version": "1.0.2", 27 | "resolved": "https://registry.npmmirror.com/@zlyboy/wx-formdata/-/wx-formdata-1.0.2.tgz", 28 | "integrity": "sha512-acHVmcIQasoLjXXSnDi40s6ggtGE+/q/5VcdPH83uqhBHScVk6fCDtnPDD0BkN0Mka7SzIpRhd+ybbUDy1QFIg==" 29 | }, 30 | "node_modules/crypto-js": { 31 | "version": "4.1.1", 32 | "resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.1.1.tgz", 33 | "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /miniprogram/pages/userManager/userManager.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 20 | {{user.name}} 21 | 22 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 源代码(Bitbucket) 35 | 36 | 37 | 源代码(GitHub) 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /miniprogram/pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | {{message}} 7 | 8 | 13 | 14 | 15 | {{activityInfo.courseInfo.course.name}}({{activityInfo.courseInfo.class.name}}):{{activityInfo.activity.name}}({{signMethods[activityId]}}) 16 | 17 | {{activityInfo.activity.endTimeForHuman}} 18 | 19 | 20 | 21 | 22 | 23 | {{user.name}} 24 | {{results[activityId][user.id]}} 25 | 26 | {{user.id}} 27 | 28 | 29 | 30 | 31 | 38 | 39 | 40 | 41 | 48 | 53 | 54 | 55 | 56 | 57 | 选择签到方式 58 | 59 | 60 | 61 | 66 | {{item}} 67 | 68 | 69 | 70 | 取消 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /miniprogram/pages/privacyTips/privacyTips.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 信息安全与隐私提醒 5 | 6 | 7 | 本页面只会在您从未登入用户时主动弹出,您仍可通过「添加账号」页面底部的超链接点击查看此页面。 8 | 9 | 10 | 本小程序在任何情况下都不会将您的信息上传到超星学习通以外的地方,也不会将您的信息储存或共享给微信以外的程序! 11 | 12 | 13 | 14 | 本小程序会储存一些信息用于签到。明文储存: 15 | 16 | 账户登录状态信息(Cookie) 17 | 账户中的课程和班级信息 18 | 19 | 加密储存: 20 | 21 | 账户ID 22 | 账户所属人的姓名 23 | 24 | 如果您选择了「记住密码」,还将加密储存: 25 | 26 | 账户ID 27 | 账号 28 | 密码 29 | 30 | 31 | 上述需要储存的信息,会交由微信储存在单独为本小程序准备的存储空间中,操作系统和微信会共同保证这些明文储存和加密储存的信息隐私不被其他小程序、其他app或其他人读取。而需要加密的信息,会交由微信使用业界通用的AES-128算法加密后储存微信加密说明文档)。但即使有加密,储存账号密码的行为仍然具有一定的风险不选择「记住密码」,本小程序则不会储存学习通账户的账号和密码。 32 | 33 | 34 | 35 | 36 | 在点击「登录」按钮前,您可以选择是否开启「记住密码」。在登录后,您可以在「用户管理」中选择用户,点击「登出」,即可从存储空间中删除该账号相关的所有信息。如果您怀疑密码已经泄露,可以在超星学习通内修改密码。 37 | 38 | 39 | 登入用户代表您已理解以上内容。 40 | 41 | 42 | 43 | 46 | 真啰嗦 47 | 48 | 49 | -------------------------------------------------------------------------------- /miniprogram/services/cloudStorage.ts: -------------------------------------------------------------------------------- 1 | import { CloudStorageTokenResult, Cookie, UploadFileResult } from "../utils/types"; 2 | import { isString, toCookieString } from "../utils/util"; 3 | // No @types/zlyboy__wx-formdata for @zlyboy/wx-formdata 4 | const FormData = require('@zlyboy/wx-formdata'); 5 | 6 | export const getCloudStorageToken = ( 7 | cookie: Cookie, 8 | ): Promise => new Promise((resolve, reject) => 9 | wx.request({ 10 | url: `https://pan-yz.chaoxing.com/api/token/uservalid`, 11 | header: { 12 | Cookie: toCookieString({ 13 | uf: cookie.uf, 14 | _d: cookie._d, 15 | UID: cookie.id, 16 | vc3: cookie.vc3, 17 | }), 18 | }, 19 | success: ({ data }) => { 20 | const rawData = data as any; 21 | 22 | if (!isString(rawData._token)) { 23 | resolve({ 24 | status: false, 25 | data, 26 | message: 'failed to parse token from data', 27 | cookie, 28 | token: '', 29 | }); 30 | return; 31 | } 32 | 33 | resolve({ 34 | status: true, 35 | data, 36 | message: '', 37 | cookie, 38 | token: rawData._token, 39 | }); 40 | }, 41 | fail: (e) => reject(e), 42 | })); 43 | 44 | export const uploadFile = ( 45 | cookie: Cookie, 46 | token: string, 47 | filePath: string, 48 | ): Promise => { 49 | const form = new FormData(); 50 | const filename = filePath.slice(filePath.lastIndexOf('/') + 1); 51 | form.appendFile('file', filePath, filename); 52 | form.append('puid', cookie.id); 53 | 54 | const data = form.getData(); 55 | return new Promise((resolve, reject) => wx.request({ 56 | method: 'POST', 57 | url: `https://pan-yz.chaoxing.com/upload?_from=mobilelearn&_token=${token}`, 58 | header: { 59 | Cookie: toCookieString({ 60 | uf: cookie.uf, 61 | _d: cookie._d, 62 | UID: cookie.id, 63 | vc3: cookie.vc3, 64 | }), 65 | 'Content-Type': data.contentType, 66 | }, 67 | data: data.buffer, 68 | success: (data) => { 69 | const rawData = data as any; 70 | 71 | if ( 72 | !rawData.data || !isString(rawData.data.objectId) 73 | || !(rawData.data.objectId.length > 0) 74 | ) { 75 | resolve({ 76 | status: false, 77 | data, 78 | message: '', 79 | cookie, 80 | token: '', 81 | filePath: '', 82 | fileId: '', 83 | }); 84 | } 85 | 86 | resolve({ 87 | status: true, 88 | data, 89 | message: '', 90 | cookie, 91 | token, 92 | filePath, 93 | fileId: rawData.data.objectId, 94 | }); 95 | }, 96 | fail: (e) => reject(e), 97 | })); 98 | }; 99 | -------------------------------------------------------------------------------- /typings/types/wx/lib.wx.behavior.d.ts: -------------------------------------------------------------------------------- 1 | /*! ***************************************************************************** 2 | Copyright (c) 2023 Tencent, Inc. All rights reserved. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ***************************************************************************** */ 22 | 23 | declare namespace WechatMiniprogram.Behavior { 24 | type BehaviorIdentifier = string 25 | type Instance< 26 | TData extends DataOption, 27 | TProperty extends PropertyOption, 28 | TMethod extends MethodOption, 29 | TCustomInstanceProperty extends IAnyObject = Record 30 | > = Component.Instance 31 | type TrivialInstance = Instance 32 | type TrivialOption = Options 33 | type Options< 34 | TData extends DataOption, 35 | TProperty extends PropertyOption, 36 | TMethod extends MethodOption, 37 | TCustomInstanceProperty extends IAnyObject = Record 38 | > = Partial> & 39 | Partial> & 40 | Partial> & 41 | Partial & 42 | Partial & 43 | ThisType> 44 | interface Constructor { 45 | < 46 | TData extends DataOption, 47 | TProperty extends PropertyOption, 48 | TMethod extends MethodOption, 49 | TCustomInstanceProperty extends IAnyObject = Record 50 | >( 51 | options: Options 52 | ): BehaviorIdentifier 53 | } 54 | 55 | type DataOption = Component.DataOption 56 | type PropertyOption = Component.PropertyOption 57 | type MethodOption = Component.MethodOption 58 | type Data = Component.Data 59 | type Property

= Component.Property

60 | type Method = Component.Method 61 | 62 | type DefinitionFilter = Component.DefinitionFilter 63 | type Lifetimes = Component.Lifetimes 64 | 65 | type OtherOption = Omit 66 | } 67 | /** 注册一个 `behavior`,接受一个 `Object` 类型的参数。*/ 68 | declare let Behavior: WechatMiniprogram.Behavior.Constructor 69 | -------------------------------------------------------------------------------- /miniprogram/pages/locationPicker/locationPicker.ts: -------------------------------------------------------------------------------- 1 | // pages/locationPicker/locationPicker.ts 2 | Page({ 3 | 4 | /** 5 | * 页面的初始数据 6 | */ 7 | data: { 8 | longitude: '', 9 | latitude: '', 10 | altitude: '', 11 | random: true, 12 | address: '', 13 | markers: [] as { [key: string]: string | number }[], 14 | coordinateErrorMessage: null as null | string, 15 | addressErrorMessage: null as null | string, 16 | }, 17 | 18 | emptyMethod() {}, 19 | 20 | copyBaiduCoordinatePicker() { 21 | wx.setClipboardData({ 22 | data: 'https://api.map.baidu.com/lbsapi/getpoint/index.html', 23 | }); 24 | }, 25 | 26 | setMarkers() { 27 | this.setData({ markers: [{ 28 | id: 0, 29 | longitude: Number.parseFloat(this.data.longitude) || 113.324520, 30 | latitude: Number.parseFloat(this.data.latitude) || 23.099994, 31 | iconPath: '../../images/location_on_FILL1_wght400_GRAD0_opsz48.svg', 32 | width: 45, 33 | height: 45, 34 | }] }); 35 | }, 36 | 37 | sign() { 38 | this.setData({ coordinateErrorMessage: null, addressErrorMessage: null }); 39 | 40 | const rawLongitude = this.data.longitude; 41 | const rawLatitude = this.data.latitude; 42 | const rawAltitude = this.data.altitude; 43 | 44 | const decimalOfLongitude = rawLongitude.slice(rawLongitude.indexOf('.') + 1); 45 | const decimalOfLatitude = rawLatitude.slice(rawLatitude.indexOf('.') + 1); 46 | 47 | if (decimalOfLongitude.length > 13 || decimalOfLatitude.length > 13) { 48 | return this.setData({ errorMessage: '经纬度小数长度不能超过13' }); 49 | } 50 | 51 | const longitude = Number.parseFloat(rawLongitude); 52 | const latitude = Number.parseFloat(rawLatitude); 53 | const altitude = Number.parseFloat(rawAltitude); 54 | 55 | if (Number.isNaN(longitude)) { 56 | return this.setData({ coordinateErrorMessage: '经度应当填写数字' }); 57 | } 58 | 59 | if (Number.isNaN(latitude)) { 60 | return this.setData({ coordinateErrorMessage: '纬度应当填写数字' }); 61 | } 62 | 63 | this.getOpenerEventChannel().emit( 64 | 'callback', 65 | this.data.random, 66 | longitude, 67 | latitude, 68 | altitude, 69 | this.data.address, 70 | ); 71 | wx.navigateBack(); 72 | }, 73 | 74 | cancel() { 75 | this.data.longitude = '-181'; 76 | this.data.latitude = '-91'; 77 | this.data.altitude = ''; 78 | this.data.random = false; 79 | this.data.address = ''; 80 | this.sign(); 81 | }, 82 | 83 | /** 84 | * 生命周期函数--监听页面加载 85 | */ 86 | onLoad() { 87 | if (!this.getOpenerEventChannel()) { 88 | console.warn('openerEventChannel not found'); 89 | wx.navigateBack(); 90 | } 91 | 92 | this.setMarkers(); 93 | }, 94 | 95 | /** 96 | * 生命周期函数--监听页面初次渲染完成 97 | */ 98 | onReady() { 99 | 100 | }, 101 | 102 | /** 103 | * 生命周期函数--监听页面显示 104 | */ 105 | onShow() { 106 | 107 | }, 108 | 109 | /** 110 | * 生命周期函数--监听页面隐藏 111 | */ 112 | onHide() { 113 | 114 | }, 115 | 116 | /** 117 | * 生命周期函数--监听页面卸载 118 | */ 119 | onUnload() { 120 | 121 | }, 122 | 123 | /** 124 | * 页面相关事件处理函数--监听用户下拉动作 125 | */ 126 | onPullDownRefresh() { 127 | 128 | }, 129 | 130 | /** 131 | * 页面上拉触底事件的处理函数 132 | */ 133 | onReachBottom() { 134 | 135 | }, 136 | 137 | /** 138 | * 用户点击右上角分享 139 | */ 140 | onShareAppMessage() { 141 | 142 | } 143 | }); 144 | -------------------------------------------------------------------------------- /miniprogram/pages/userInfo/userInfo.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 用户信息 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 登录信息 27 | 28 | 29 | 30 | 31 | 32 | 33 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 54 | 55 | 56 | 57 | 65 | 68 | {{errorMessage}} 69 | 70 | 71 | 72 | 73 | 74 | 课程数量:{{totalOfCourse}} 75 | 76 | 信息安全与隐私提醒 77 | 78 | 79 | 80 | 81 | 86 | 91 | 97 | 102 | 登出 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /miniprogram/utils/types.ts: -------------------------------------------------------------------------------- 1 | export interface RawActivity { 2 | id: number; 3 | status: number; 4 | startTime: number; 5 | endTime: number | ''; 6 | nameOne: string; 7 | nameFour: string; 8 | } 9 | 10 | export interface RawActivityWithKnownOtherId extends RawActivity { 11 | otherId: '0' | '2' | '3' | '4' | '5'; 12 | } 13 | 14 | export interface RawActivityListObject { 15 | data: { activeList: RawActivity[] }; 16 | } 17 | 18 | export type SignMethod = 'qrCode' | 'location' | 'gesture' | 'code' | 'clickOrPhoto' 19 | | 'unknown'; 20 | 21 | export type SignMethodForHuman = '二维码' | '位置' | '手势' | '签到码' | '点击/拍照' 22 | | '未知'; 23 | 24 | export type SignMethodForChoice = 'qrCode' | 'location' | 'gesture' | 'code' 25 | | 'click' | 'photo' | 'unknown'; 26 | 27 | export type SignMethodForHumanChoice = '二维码' | '位置' | '手势' | '签到码' 28 | | '点击' | '拍照' | '未知'; 29 | 30 | export interface Activity { 31 | id: number 32 | name: string; 33 | signMethod: SignMethod; 34 | startTime: number; 35 | endTime: number; 36 | endTimeForHuman: string; 37 | raw: RawActivityWithKnownOtherId; 38 | } 39 | 40 | export interface Course { 41 | id: number; 42 | name: string; 43 | } 44 | 45 | export interface Class extends Course {} 46 | 47 | export interface CourseInfo { 48 | course: Course; 49 | // TODO: array? 50 | class: Class; 51 | } 52 | 53 | export interface Cookie { 54 | expire: number; 55 | id: number; 56 | uf: string; 57 | vc3: string; 58 | _d: string; 59 | fid: string; 60 | } 61 | 62 | export interface User extends Cookie { 63 | name: string; 64 | } 65 | 66 | export type LoginType = 'fanya' | 'v11'; 67 | 68 | export interface Credential { 69 | id: number; 70 | loginType: LoginType; 71 | username: string; 72 | password: string; 73 | } 74 | 75 | export interface CookieWithCourseInfo { 76 | cookie: Cookie; 77 | courseInfoArray: CourseInfo[]; 78 | } 79 | 80 | export interface Result { 81 | status: boolean; 82 | message: string; 83 | data: any; 84 | } 85 | 86 | export interface CookieResult extends Result { 87 | cookie: Cookie; 88 | } 89 | 90 | export interface GetCookieSuccessResult extends Result { 91 | status: true; 92 | cookie: Cookie; 93 | } 94 | 95 | export interface GetCookieFailedResult extends Result { 96 | status: false; 97 | cookie: {}; 98 | } 99 | 100 | export interface NameSuccessResult extends CookieResult { 101 | status: true; 102 | name: string; 103 | } 104 | 105 | export interface NameFailedResult extends CookieResult { 106 | status: false; 107 | name: ''; 108 | } 109 | 110 | export interface LoginResult extends CookieResult { 111 | user: User; 112 | } 113 | 114 | export interface CourseInfoArrayResult extends CookieResult { 115 | courseInfoArray: CourseInfo[]; 116 | } 117 | 118 | export interface ActivitiesResult extends CookieResult { 119 | courseInfo: CourseInfo; 120 | activities: Activity[]; 121 | } 122 | 123 | export interface CookieStringResult extends Result { 124 | cookieString: string; 125 | } 126 | 127 | interface ActivityIdResult extends CookieResult { 128 | activityId: number; 129 | } 130 | 131 | export interface PreSignResult extends ActivityIdResult { 132 | courseId: number; 133 | classId: number; 134 | } 135 | 136 | export interface IsPhotoSignResult extends ActivityIdResult { 137 | isPhoto: boolean; 138 | } 139 | 140 | export interface SignResult extends ActivityIdResult { 141 | user: User; 142 | } 143 | 144 | export interface QrCodeSignResult extends SignResult { 145 | enc: string; 146 | } 147 | 148 | export interface LocationSignResult extends SignResult { 149 | longitude: number; 150 | latitude: number; 151 | address: string; 152 | } 153 | 154 | export interface PhotoSignResult extends SignResult { 155 | fileId: string; 156 | } 157 | 158 | export interface CloudStorageTokenResult extends CookieResult { 159 | token: string; 160 | } 161 | 162 | export interface UploadFileResult extends CookieResult { 163 | token: string; 164 | filePath: string; 165 | fileId: string; 166 | } 167 | -------------------------------------------------------------------------------- /miniprogram/pages/locationPicker/locationPicker.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 请使用百度地图拾取坐标系统获取坐标。 14 | 15 | 如果是无需定位的二维码签到,请点击取消。 16 | 17 | 18 | 20 | 经纬度 21 | 22 | 23 | 24 | 25 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 61 | 62 | 63 | 72 | 73 | 76 | {{coordinateErrorMessage}} 77 | 78 | 79 | 80 | 地址 81 | 82 | 83 | 84 | 93 | 94 | 95 | 96 | 99 | {{addressErrorMessage}} 100 | 101 | 102 | 103 | 104 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 快进到退学 2 | 「快进到退学」是[cxOrz/chaoxing-sign-cli: 超星学习通签到:支持普通签到、拍照签到、手势签到、位置签到、二维码签到,支持自动监测、QQ机器人签到与推送。](https://github.com/cxOrz/chaoxing-sign-cli)项目的微信小程序版本实现。本项目直接与超星学习通服务端进行通信,无后端依赖,可去中心化部署。 3 | 4 | ![效果图](/docs/images/效果图.png) 5 | 6 | ## 部署教程 7 | ### 注册微信小程序开发者账号并配置 8 | 打开[小程序](https://mp.weixin.qq.com/wxopen/waregister?action=step1),按照流程注册小程序开发者账号并实名。 9 | 10 | 在小程序管理后台内,选择左侧的「开发 - 开发管理」,再点击上方的「开发设置」。 11 | 12 | ![小程序管理后台-开发设置](/docs/images/小程序管理后台-开发设置.png) 13 | 14 | 点击服务器域名中的「开始配置」配置域名。 15 | 16 | ![小程序管理后台-服务器域名](/docs/images/小程序管理后台-服务器域名.png) 17 | 18 | **v1.3.2及以下版本**: 19 | 在「request合法域名」一栏中填入`https://mobilelearn.chaoxing.com;https://mooc1-1.chaoxing.com;https://pan-yz.chaoxing.com;https://passport2.chaoxing.com;`,点击「保存并提交」。 20 | 21 | **v1.4.0及以上版本**: 22 | 在「request合法域名」一栏中填入`https://mobilelearn.chaoxing.com;https://mooc1-1.chaoxing.com;https://pan-yz.chaoxing.com;https://passport2.chaoxing.com;https://passport2-api.chaoxing.com;`,点击「保存并提交」。 23 | 24 | ![配置服务器域名](/docs/images/配置服务器域名.png) 25 | 26 | ### 下载源码 27 | 点击下方任意一个打得开的链接下载最新版源码,然后解压到任意位置。 28 | 29 | - [Bitbucket下载](https://bitbucket.org/dropping-out-speedrun/dropping-out-speedrun/downloads/?tab=tags):点击zip 30 | 31 | ![Bitbucket下载](/docs/images/Bitbucket下载.png) 32 | 33 | - [GitHub下载](https://github.com/DroppingOutSpeedrun/dropping-out-speedrun/tags):点击zip 34 | 35 | ![GitHub下载](/docs/images/GitHub下载.png) 36 | 37 | ### 使用微信开发者工具进行部署 38 | 打开[微信开发者工具下载地址与更新日志 | 微信开放文档](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html),找到「稳定版」一栏。一般下载「Windows 64」版本,新款苹果电脑下载「macOS ARM64」版本,旧款苹果电脑下载「macOS x64」版本,不是苹果电脑但打不开「Windows 64」版本则下载「Windows 32」版本。 39 | 40 | 安装好微信开发者工具之后打开,然后使用微信扫描二维码,并在手机上确认登入。 41 | 42 | ![登入微信开发者工具](/docs/images/登入微信开发者工具.jpg) 43 | 44 | 点击导入,选择源码所在位置,打开到能看到miniprogram等文件夹时,点击选择文件夹。 45 | 46 | ![导入源码](/docs/images/导入源码.png) 47 | 48 | 点击AppID下拉菜单,选择一个AppID。然后将后端服务设置为「不使用云服务」,其他设置保持默认即可。最后点击确定。 49 | 50 | ![导入小程序向导](/docs/images/导入小程序向导.png) 51 | 52 | 选择「信任并运行」 53 | 54 | ![信任并运行](/docs/images/信任并运行.png) 55 | 56 | 点击「预览」,然后使用微信扫描二维码即可运行 57 | 58 | ![预览](/docs/images/预览.jpg) 59 | 60 | ### 将小程序分享给他人使用 61 | 选择「上传」,然后再点击上传 62 | 63 | ![上传](/docs/images/上传.png) 64 | 65 | 在微信中搜索「小程序助手」,选择小程序 66 | 67 | ![选择小程序](/docs/images/选择小程序.jpg) 68 | 69 | 点击「成员管理」 70 | 71 | ![成员管理](/docs/images/成员管理.jpg) 72 | 73 | 点击「体验成员」,再点击「新增体验成员」 74 | 75 | ![添加资格](/docs/images/添加资格.jpg) 76 | 77 | 输入受邀者微信号,搜索受邀者 78 | 79 | ![搜索受邀者](/docs/images/搜索受邀者.jpg) 80 | 81 | 回到主页,选择「审核管理」 82 | 83 | ![审核管理](/docs/images/审核管理.jpg) 84 | 85 | 点击刚发布的开发版 86 | 87 | ![选择开发版](/docs/images/选择开发版.jpg) 88 | 89 | 点击「体验版二维码」 90 | 91 | ![体验版二维码](/docs/images/体验版二维码.jpg) 92 | 93 | 将此二维码分享给受邀者即可 94 | 95 | ![小程序二维码](/docs/images/小程序二维码.jpg) 96 | 97 | ## 签到类型支持 98 | - 点击签到 99 | - 拍照签到 100 | - 手势签到 101 | - 位置签到 102 | - 二维码签到(支持附带位置信息的二维码签到) 103 | - 签到码签到 104 | 105 | ## 信息安全与隐私 106 | 本小程序不会与超星学习通以外的服务器进行通信,只在本地储存用户信息,通过[微信内置的AES-128加密储存API](https://developers.weixin.qq.com/miniprogram/dev/api/storage/wx.setStorage.html#Object-object)对敏感信息进行加密储存。 107 | 108 | ## Developing 109 | Clone this project. 110 | From GitHub: 111 | ```shell 112 | git clone https://github.com/DroppingOutSpeedrun/dropping-out-speedrun.git 113 | ``` 114 | 115 | Or from BitBucket: 116 | ```shell 117 | git clone https://bitbucket.org/dropping-out-speedrun/dropping-out-speedrun.git 118 | ``` 119 | 120 | You have to install [Node.js](https://nodejs.org/) and [pnpm](https://pnpm.io/) (not sure npm could be used) to install dependencies: 121 | ```shell 122 | cd ./dropping-out-speedrun/miniprogram/ 123 | pnpm install 124 | ``` 125 | 126 | Start reading by services is a good begining: 127 | - `getCookieByFanya()` in `services/login.ts` 128 | - `getCourseInfoArray()` in `services/course.ts` 129 | - `getActivities()` in `services/course.ts` 130 | - `preSign()` in `services/sign.ts` 131 | - `generalSign()` in `services/sign.ts` 132 | 133 | ## LICENSE 134 | Dropping Out Speedrun 135 | Copyright (C) 2023 Dropping Out Speedrun 136 | 137 | This program is free software: you can redistribute it and/or modify 138 | it under the terms of the GNU General Public License as published by 139 | the Free Software Foundation, either version 3 of the License, or 140 | (at your option) any later version. 141 | 142 | This program is distributed in the hope that it will be useful, 143 | but WITHOUT ANY WARRANTY; without even the implied warranty of 144 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 145 | GNU General Public License for more details. 146 | 147 | You should have received a copy of the GNU General Public License 148 | along with this program. If not, see . 149 | -------------------------------------------------------------------------------- /miniprogram/services/sign.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Cookie, CookieStringResult, IsPhotoSignResult, LocationSignResult, 3 | PhotoSignResult, PreSignResult, QrCodeSignResult, SignResult, User 4 | } from '../utils/types'; 5 | import { cookieForSign, parametersToString, toCookieString } from '../utils/util'; 6 | 7 | export const preSign = ( 8 | cookie: Cookie, activityId: number, courseId: number, classId: number, 9 | ): Promise => new Promise( 10 | (resolve, reject) => wx.request({ 11 | url: `https://mobilelearn.chaoxing.com/newsign/preSign?courseId=${courseId}&classId=${classId}&activePrimaryId=${activityId}&general=1&sys=1&ls=1&appType=15&&tid=&uid=${cookie.id}&ut=s`, 12 | header: { 13 | Cookie: toCookieString({ 14 | uf: cookie.uf, 15 | _d: cookie._d, 16 | UID: cookie.id, 17 | vc3: cookie.vc3, 18 | }) 19 | }, 20 | // we don't care about the returned data actually 21 | success: (data) => resolve({ 22 | status: true, 23 | data, 24 | message: '', 25 | cookie, 26 | activityId, 27 | courseId, 28 | classId, 29 | }), 30 | fail: (e) => reject(e), 31 | }) 32 | ); 33 | 34 | export const isPhotoSign = ( 35 | activityId: number, 36 | cookie: Cookie 37 | ): Promise => new Promise((resolve, reject) => 38 | wx.request({ 39 | url: `https://mobilelearn.chaoxing.com/v2/apis/active/getPPTActiveInfo?activeId=${activityId}`, 40 | header: { 41 | Cookie: toCookieString({ 42 | uf: cookie.uf, 43 | _d: cookie._d, 44 | UID: cookie.id, 45 | vc3: cookie.vc3, 46 | }), 47 | }, 48 | success: ({ data }) => { 49 | const rawData = data as any; 50 | 51 | if (!rawData.data || typeof rawData.data.ifphoto !== 'number') { 52 | resolve({ 53 | status: false, 54 | message: 'failed to parse ifphoto from data', 55 | data, 56 | cookie, 57 | activityId, 58 | isPhoto: false, 59 | }); 60 | return; 61 | } 62 | 63 | resolve({ 64 | status: true, 65 | message: '', 66 | data, 67 | cookie, 68 | activityId, 69 | isPhoto: rawData.data.ifphoto === 1, 70 | }); 71 | }, 72 | fail: (e) => reject(e), 73 | })); 74 | 75 | const sign = ( 76 | parameters: { [key: string]: any }, 77 | cookie: { [key: string]: any }, 78 | ): Promise => new Promise((resolve, reject) => wx.request({ 79 | url: `https://mobilelearn.chaoxing.com/pptSign/stuSignajax${Object.keys(parameters).length > 0 ? `?${parametersToString(parameters)}` : ''}`, 80 | header: { Cookie: toCookieString(cookie) }, 81 | success: ({ data }) => resolve({ 82 | status: data === 'success' || data === '您已签到过了', 83 | message: '', 84 | data, 85 | cookieString: toCookieString(cookie), 86 | }), 87 | fail: (e) => reject(e), 88 | })); 89 | 90 | export const generalSign = ( 91 | user: User, 92 | activityId: number, 93 | ): Promise => sign( 94 | { 95 | activeId: activityId.toString(), 96 | uid: user.id, 97 | clientip: '', 98 | latitude: '-1', 99 | longitude: '-1', 100 | appType: '15', 101 | fid: user.fid, 102 | name: encodeURIComponent(user.name), 103 | }, 104 | cookieForSign(user), 105 | ).then((result) => ({ ...result, cookie: user, user, activityId })); 106 | 107 | export const qrCodeSign = ( 108 | user: User, 109 | activityId: number, 110 | enc: string, 111 | longitude: number, 112 | latitude: number, 113 | altitude: number, 114 | address: string, 115 | ): Promise => sign( 116 | { 117 | enc, 118 | name: encodeURIComponent(user.name), 119 | activeId: activityId.toString(), 120 | uid: user.id, 121 | clientip: '', 122 | ...( 123 | !Number.isNaN(latitude) && !Number.isNaN(longitude) 124 | ? { 125 | location: encodeURIComponent(`{"result":"1","address":"${address}","latitude":${latitude},"longitude":${longitude},"altitude":${!Number.isNaN(altitude) ? altitude : -1}}`), 126 | } 127 | : {} 128 | ), 129 | useragent: '', 130 | latitude: '-1', 131 | longitude: '-1', 132 | fid: user.fid, 133 | appType: '15', 134 | }, 135 | cookieForSign(user), 136 | ).then((result) => ({ ...result, cookie: user, user, activityId, enc })); 137 | 138 | export const locationSign = ( 139 | user: User, 140 | activityId: number, 141 | longitude: number, 142 | latitude: number, 143 | address: string, 144 | ): Promise => sign( 145 | { 146 | name: encodeURIComponent(user.name), 147 | address, 148 | activeId: activityId.toString(), 149 | uid: user.id, 150 | clientip: '', 151 | latitude: latitude.toString(), 152 | longitude: longitude.toString(), 153 | fid: user.fid, 154 | appType: '15', 155 | ifTiJiao: '1', 156 | }, 157 | cookieForSign(user), 158 | ).then((result) => ({ 159 | ...result, 160 | cookie: user, 161 | user, 162 | activityId, 163 | longitude, 164 | latitude, 165 | address, 166 | })); 167 | 168 | export const photoSign = ( 169 | user: User, 170 | activityId: number, 171 | fileId: string, 172 | ): Promise => sign( 173 | { 174 | activeId: activityId.toString(), 175 | uid: user.id, 176 | clientip: '', 177 | useragent: '', 178 | latitude: '-1', 179 | longitude: '-1', 180 | appType: '15', 181 | fid: user.fid, 182 | objectId: fileId, 183 | name: encodeURIComponent(user.name), 184 | }, 185 | cookieForSign(user), 186 | ).then((result) => ({ ...result, cookie: user, user, activityId, fileId })); 187 | -------------------------------------------------------------------------------- /miniprogram/services/login.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Cookie, GetCookieFailedResult, GetCookieSuccessResult, NameSuccessResult, 3 | NameFailedResult, LoginResult, 4 | } from '../utils/types'; 5 | import { parseCookiesFromWx, toCookieString } from '../utils/util'; 6 | import crypto from 'crypto-js'; 7 | 8 | const toCookieResult = ( 9 | data: any, cookies: string[], 10 | ): GetCookieSuccessResult | GetCookieFailedResult => { 11 | const rawData = data as any; 12 | 13 | if (typeof rawData.status === 'boolean' && rawData.status) { 14 | try { 15 | return { 16 | status: true, 17 | data: rawData, 18 | message: '', 19 | cookie: parseCookiesFromWx(cookies), 20 | }; 21 | } catch (e) { 22 | return { 23 | status: false, 24 | data, 25 | message: e instanceof TypeError 26 | ? `failed to parse cookie: ${e.message}` 27 | : `unknown error: ${JSON.stringify(e)}`, 28 | cookie: {}, 29 | }; 30 | } 31 | } else { 32 | return { 33 | status: false, 34 | data, 35 | message: 'Chaoxing returned false of status', 36 | cookie: {}, 37 | }; 38 | } 39 | } 40 | 41 | export const getCookieByFanya = ( 42 | username: string, 43 | password: string, 44 | ): Promise => { 45 | const wordArray = crypto.enc.Utf8.parse('u2oh6Vu^HWe40fj'); 46 | const encryptedPassword = crypto.DES.encrypt(password, wordArray, { 47 | mode: crypto.mode.ECB, 48 | padding: crypto.pad.Pkcs7, 49 | }); 50 | const encryptedPasswordString = encryptedPassword.ciphertext.toString(); 51 | 52 | return new Promise((resolve, reject) => wx.request({ 53 | method: 'POST', 54 | header: { 55 | 'Content-Type': 'application/x-www-form-urlencoded', 56 | 'X-Requested-With': 'XMLHttpRequest', 57 | }, 58 | url: 'https://passport2.chaoxing.com/fanyalogin', 59 | data: `uname=${username}&password=${encryptedPasswordString}&fid=-1&t=true&refer=https%253A%252F%252Fi.chaoxing.com&forbidotherlogin=0&validate=`, 60 | success: ({ data, cookies }) => resolve(toCookieResult(data, cookies)), 61 | fail: (e) => reject(e), 62 | })); 63 | } 64 | 65 | // https://www.myitmx.com/123.html 66 | export const getCookieByV11 = ( 67 | username: string, 68 | password: string, 69 | ): Promise => ( 70 | new Promise((resolve, reject) => wx.request({ 71 | url: `https://passport2-api.chaoxing.com/v11/loginregister?code=${password}&cx_xxt_passport=json&uname=${username}&loginType=1&roleSelect=true`, 72 | success: ({ data, cookies }) => resolve(toCookieResult(data, cookies)), 73 | fail: (e) => reject(e), 74 | })) 75 | ) 76 | 77 | export const getName = ( 78 | cookie: Cookie, 79 | ): Promise => 80 | new Promise((resolve, reject) => wx.request({ 81 | url: 'https://passport2.chaoxing.com/mooc/accountManage', 82 | header: { 83 | Cookie: toCookieString({ 84 | uf: cookie.uf, 85 | _d: cookie._d, 86 | UID: cookie.id, 87 | vc3: cookie.vc3, 88 | }), 89 | }, 90 | success: ({ data }) => { 91 | if (typeof data !== 'string') { 92 | resolve({ 93 | status: false, 94 | data, 95 | message: 'cannot resolve data: data is not a string', 96 | cookie, 97 | name: '', 98 | }); 99 | return; 100 | } 101 | 102 | // TODO: possible different data from server? 103 | const nameEndIndex = data.indexOf('姓名'); 104 | if (nameEndIndex < 0) { 105 | resolve({ 106 | status: false, 107 | data, 108 | message: 'cannot find the name in data', 109 | cookie, 110 | name: '', 111 | }); 112 | return; 113 | } 114 | 115 | const endTagBeginingIndex = data.lastIndexOf('<', nameEndIndex); 116 | const startTagEndingIndex = data.lastIndexOf('>', endTagBeginingIndex); 117 | const name = data.slice(startTagEndingIndex + 1, endTagBeginingIndex); 118 | // https://stackoverflow.com/a/16369725 119 | const trimedName = name.replace(/^\s*$(?:\r\n?|\n)/gm, '').trim(); 120 | 121 | resolve({ 122 | status: true, 123 | data, 124 | message: '', 125 | cookie, 126 | name: trimedName, 127 | }); 128 | }, 129 | fail: (e) => reject(e), 130 | })); 131 | 132 | export const toLoginResult = ( 133 | result: GetCookieSuccessResult | GetCookieFailedResult 134 | ): Promise => 135 | new Promise((resolve) => { 136 | if (!result.status) { 137 | resolve(result); 138 | return; 139 | } 140 | 141 | getName(result.cookie).then((result) => resolve( 142 | result.status 143 | ? { 144 | ...result, 145 | user: { ...result.cookie, name: result.name }, 146 | } 147 | : result, 148 | )) 149 | }) 150 | 151 | export const loginByFanya = ( 152 | username: string, 153 | password: string, 154 | ): Promise => 155 | new Promise((resolve, reject) => 156 | getCookieByFanya(username, password).then((result) => 157 | toLoginResult(result).then((r) => resolve(r)) 158 | ).catch((error) => reject(error)) 159 | ); 160 | 161 | export const loginByV11 = ( 162 | username: string, 163 | password: string, 164 | ): Promise => 165 | new Promise((resolve, reject) => 166 | getCookieByV11(username, password).then((result) => 167 | toLoginResult(result).then((r) => resolve(r)) 168 | ).catch((error) => reject(error)) 169 | ); 170 | -------------------------------------------------------------------------------- /miniprogram/services/course.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Cookie, 3 | CourseInfo, 4 | RawActivityWithKnownOtherId, 5 | CourseInfoArrayResult, 6 | ActivitiesResult, 7 | } from '../utils/types'; 8 | import { 9 | isString, 10 | toActiveListObject, 11 | toActivity, 12 | toCookieString, 13 | toRawActivityWithKnownOtherId, 14 | } from '../utils/util'; 15 | 16 | export const getCourseInfoArray = (cookie: Cookie): Promise => 17 | new Promise((resolve, reject) => 18 | wx.request({ 19 | method: 'POST', 20 | url: 'https://mooc1-1.chaoxing.com/visit/courselistdata', 21 | header: { 22 | Accept: 'text/html, */*; q=0.01', 23 | 'Accept-Encoding': 'gzip, deflate', 24 | 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', 25 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8;', 26 | Cookie: toCookieString({ _uid: cookie.id, _d: cookie._d, vc3: cookie.vc3 }), 27 | }, 28 | data: 'courseType=1&courseFolderId=0&courseFolderSize=0', 29 | success: ({ data }) => { 30 | if (!isString(data) || !data.includes('course_')) { 31 | resolve({ 32 | status: false, 33 | data, 34 | message: 'cannot found any course in data', 35 | cookie, 36 | courseInfoArray: [], 37 | }); 38 | return; 39 | } 40 | 41 | let courseInfoArray: CourseInfo[] = []; 42 | 43 | const courseIdIdentifier = 'course_'; 44 | const idSpliter = '_'; 45 | const courseNameIdentifier = 'title="'; 46 | const classNameIdentifier = '班级:'; 47 | for (let courseIdBegining = 0; ; courseIdBegining++) { 48 | courseIdBegining = data.indexOf(courseIdIdentifier, courseIdBegining); 49 | if (courseIdBegining < 0) { 50 | break; 51 | } 52 | 53 | const idSpliterIndex = data.indexOf( 54 | idSpliter, 55 | courseIdBegining + courseIdIdentifier.length, 56 | ); 57 | const couseIdEnding = data.indexOf('"', idSpliterIndex + idSpliter.length); 58 | 59 | const courseNameSearchBeginingIndex = data.indexOf( 60 | 'class="course-name', 61 | couseIdEnding, 62 | ); 63 | const courseNameBeginingIndex = data.indexOf( 64 | courseNameIdentifier, 65 | courseNameSearchBeginingIndex, 66 | ); 67 | const courseNameEndingIndex = data.indexOf( 68 | '"', 69 | courseNameBeginingIndex + courseNameIdentifier.length, 70 | ); 71 | 72 | const rawCourseId = data.slice( 73 | courseIdBegining + courseIdIdentifier.length, 74 | idSpliterIndex, 75 | ); 76 | const rawClassId = data.slice( 77 | idSpliterIndex + idSpliter.length, 78 | couseIdEnding, 79 | ); 80 | 81 | const courseId = Number.parseInt(rawCourseId, 10); 82 | const classId = Number.parseInt(rawClassId, 10); 83 | 84 | const courseName = data.slice( 85 | courseNameBeginingIndex + courseNameIdentifier.length, 86 | courseNameEndingIndex, 87 | ); 88 | 89 | const classNameBeginingIndex = data.indexOf( 90 | classNameIdentifier, 91 | courseNameEndingIndex, 92 | ); 93 | const classNameEndingIndex = data.indexOf( 94 | ' reject(e), 121 | })); 122 | 123 | export const getActivities = ( 124 | cookie: Cookie, 125 | courseInfo: CourseInfo, 126 | ): Promise => new Promise((resolve, reject) => wx.request({ 127 | url: `https://mobilelearn.chaoxing.com/v2/apis/active/student/activelist?fid=0&courseId=${courseInfo.course.id}&classId=${courseInfo.class.id}&_=${new Date().getTime()}`, 128 | header: { Cookie: toCookieString({ 129 | uf: cookie.uf, 130 | _d: cookie._d, 131 | UID: cookie.id, 132 | vc3: cookie.vc3 133 | }) }, 134 | success: ({ data }) => { 135 | try { 136 | resolve({ 137 | status: true, 138 | data, 139 | message: '', 140 | cookie, 141 | courseInfo, 142 | activities: toActiveListObject(data).data.activeList 143 | .reduce((filtered, activity) => { 144 | try { 145 | return filtered.concat(toRawActivityWithKnownOtherId(activity)); 146 | } catch (e) { 147 | // append sign event only 148 | return filtered; 149 | } 150 | }, []) 151 | .map((rawActivity) => toActivity(rawActivity)), 152 | }); 153 | } catch (e) { 154 | resolve({ 155 | status: false, 156 | data, 157 | message: e instanceof TypeError 158 | ? `maybe hit the Chaoxing API limit` 159 | : `unknown error: ${JSON.stringify(e)}`, 160 | cookie, 161 | courseInfo, 162 | activities: [], 163 | }); 164 | } 165 | }, 166 | fail: (e) => reject(e), 167 | })); 168 | -------------------------------------------------------------------------------- /typings/types/wx/index.d.ts: -------------------------------------------------------------------------------- 1 | /*! ***************************************************************************** 2 | Copyright (c) 2023 Tencent, Inc. All rights reserved. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ***************************************************************************** */ 22 | 23 | /// 24 | /// 25 | /// 26 | /// 27 | /// 28 | /// 29 | /// 30 | 31 | declare namespace WechatMiniprogram { 32 | type IAnyObject = Record 33 | type Optional = F extends (arg: infer P) => infer R ? (arg?: P) => R : F 34 | type OptionalInterface = { [K in keyof T]: Optional } 35 | interface AsyncMethodOptionLike { 36 | success?: (...args: any[]) => void 37 | } 38 | type PromisifySuccessResult< 39 | P, 40 | T extends AsyncMethodOptionLike 41 | > = P extends { 42 | success: any 43 | } 44 | ? void 45 | : P extends { fail: any } 46 | ? void 47 | : P extends { complete: any } 48 | ? void 49 | : Promise>[0]> 50 | 51 | // TODO: Extract real definition from `lib.dom.d.ts` to replace this 52 | type IIRFilterNode = any 53 | type WaveShaperNode = any 54 | type ConstantSourceNode = any 55 | type OscillatorNode = any 56 | type GainNode = any 57 | type BiquadFilterNode = any 58 | type PeriodicWaveNode = any 59 | type BufferSourceNode = any 60 | type ChannelSplitterNode = any 61 | type ChannelMergerNode = any 62 | type DelayNode = any 63 | type DynamicsCompressorNode = any 64 | type ScriptProcessorNode = any 65 | type PannerNode = any 66 | type AnalyserNode = any 67 | type AudioListener = any 68 | type WebGLTexture = any 69 | type WebGLRenderingContext = any 70 | 71 | // TODO: fill worklet type 72 | type WorkletFunction = (...args: any) => any 73 | type AnimationObject = any 74 | type SharedValue = T 75 | type DerivedValue = T 76 | } 77 | 78 | declare let console: WechatMiniprogram.Console 79 | 80 | declare let wx: WechatMiniprogram.Wx 81 | /** 引入模块。返回模块通过 `module.exports` 或 `exports` 暴露的接口。 */ 82 | interface Require { 83 | ( 84 | /** 需要引入模块文件相对于当前文件的相对路径,或 npm 模块名,或 npm 模块路径。不支持绝对路径 */ 85 | module: string, 86 | /** 用于异步获取其他分包中的模块的引用结果,详见 [分包异步化]((subpackages/async)) */ 87 | callback?: (moduleExport: any) => void, 88 | /** 异步获取分包失败时的回调,详见 [分包异步化]((subpackages/async)) */ 89 | errorCallback?: (err: any) => void 90 | ): any 91 | /** 以 Promise 形式异步引入模块。返回模块通过 `module.exports` 或 `exports` 暴露的接口。 */ 92 | async( 93 | /** 需要引入模块文件相对于当前文件的相对路径,或 npm 模块名,或 npm 模块路径。不支持绝对路径 */ 94 | module: string 95 | ): Promise 96 | } 97 | declare const require: Require 98 | /** 引入插件。返回插件通过 `main` 暴露的接口。 */ 99 | interface RequirePlugin { 100 | ( 101 | /** 需要引入的插件的 alias */ 102 | module: string, 103 | /** 用于异步获取其他分包中的插件的引用结果,详见 [分包异步化]((subpackages/async)) */ 104 | callback?: (pluginExport: any) => void 105 | ): any 106 | /** 以 Promise 形式异步引入插件。返回插件通过 `main` 暴露的接口。 */ 107 | async( 108 | /** 需要引入的插件的 alias */ 109 | module: string 110 | ): Promise 111 | } 112 | declare const requirePlugin: RequirePlugin 113 | /** 插件引入当前使用者小程序。返回使用者小程序通过 [插件配置中 `export` 暴露的接口](https://developers.weixin.qq.com/miniprogram/dev/framework/plugin/using.html#%E5%AF%BC%E5%87%BA%E5%88%B0%E6%8F%92%E4%BB%B6)。 114 | * 115 | * 该接口只在插件中存在 116 | * 117 | * 最低基础库: `2.11.1` */ 118 | declare function requireMiniProgram(): any 119 | /** 当前模块对象 */ 120 | declare let module: { 121 | /** 模块向外暴露的对象,使用 `require` 引用该模块时可以获取 */ 122 | exports: any 123 | } 124 | /** `module.exports` 的引用 */ 125 | declare let exports: any 126 | 127 | /** [clearInterval(number intervalID)](https://developers.weixin.qq.com/miniprogram/dev/api/base/timer/clearInterval.html) 128 | * 129 | * 取消由 setInterval 设置的定时器。 */ 130 | declare function clearInterval( 131 | /** 要取消的定时器的 ID */ 132 | intervalID: number 133 | ): void 134 | /** [clearTimeout(number timeoutID)](https://developers.weixin.qq.com/miniprogram/dev/api/base/timer/clearTimeout.html) 135 | * 136 | * 取消由 setTimeout 设置的定时器。 */ 137 | declare function clearTimeout( 138 | /** 要取消的定时器的 ID */ 139 | timeoutID: number 140 | ): void 141 | /** [number setInterval(function callback, number delay, any rest)](https://developers.weixin.qq.com/miniprogram/dev/api/base/timer/setInterval.html) 142 | * 143 | * 设定一个定时器。按照指定的周期(以毫秒计)来执行注册的回调函数 */ 144 | declare function setInterval( 145 | /** 回调函数 */ 146 | callback: (...args: any[]) => any, 147 | /** 执行回调函数之间的时间间隔,单位 ms。 */ 148 | delay?: number, 149 | /** param1, param2, ..., paramN 等附加参数,它们会作为参数传递给回调函数。 */ 150 | rest?: any 151 | ): number 152 | /** [number setTimeout(function callback, number delay, any rest)](https://developers.weixin.qq.com/miniprogram/dev/api/base/timer/setTimeout.html) 153 | * 154 | * 设定一个定时器。在定时到期以后执行注册的回调函数 */ 155 | declare function setTimeout( 156 | /** 回调函数 */ 157 | callback: (...args: any[]) => any, 158 | /** 延迟的时间,函数的调用会在该延迟之后发生,单位 ms。 */ 159 | delay?: number, 160 | /** param1, param2, ..., paramN 等附加参数,它们会作为参数传递给回调函数。 */ 161 | rest?: any 162 | ): number 163 | -------------------------------------------------------------------------------- /miniprogram/pages/userInfo/userInfo.ts: -------------------------------------------------------------------------------- 1 | import { loginByFanya, loginByV11 } from "../../services/login"; 2 | import { 3 | Credential, 4 | GetCookieFailedResult, 5 | LoginResult, 6 | NameFailedResult, 7 | LoginType, 8 | } from "../../utils/types"; 9 | import { isString } from "../../utils/util"; 10 | 11 | // pages/userInfo/userInfo.ts 12 | Page({ 13 | 14 | /** 15 | * 页面的初始数据 16 | */ 17 | data: { 18 | id: -1, 19 | name: '', 20 | username: '', 21 | password: '', 22 | totalOfCourse: 0, 23 | remeber: false, 24 | firstTimeRemeber: true, 25 | hideUserManagements: true, 26 | errorMessage: null as null | string, 27 | }, 28 | emptyMethod() {}, 29 | /** 30 | * Remove user by channel 31 | */ 32 | removeUser() { 33 | const { id } = this.data; 34 | 35 | console.debug(`remove user ${id}`); 36 | 37 | this.getOpenerEventChannel().emit('removeUser', id); 38 | this.getOpenerEventChannel().emit('removeCredential', id); 39 | }, 40 | /** 41 | * Add user by channel 42 | */ 43 | addUser( 44 | loginType: LoginType, 45 | result: LoginResult | GetCookieFailedResult | NameFailedResult 46 | ) { 47 | if (!result.status) { 48 | console.error("failed to login user", result); 49 | this.setData({ errorMessage: JSON.stringify(result.data) }); 50 | return; 51 | } 52 | 53 | const { user } = result; 54 | 55 | this.getOpenerEventChannel().emit('addUser', user); 56 | if (this.data.remeber) { 57 | console.debug(`cache username and password for user ${user.id}`); 58 | 59 | const credential: Credential = { 60 | id: user.id, 61 | loginType, 62 | username: this.data.username, 63 | password: this.data.password, 64 | }; 65 | this.getOpenerEventChannel().emit('addCredential', credential); 66 | } else { 67 | // maybe user is updating cookie 68 | this.getOpenerEventChannel().emit('removeCredential', user.id); 69 | } 70 | 71 | this.setData({ errorMessage: null }); 72 | wx.navigateBack(); 73 | }, 74 | loginFanya() { 75 | const { username, password } = this.data; 76 | loginByFanya(username, password).then((result) => 77 | this.addUser('fanya', result) 78 | ).catch((e) => { 79 | console.error(e); 80 | this.setData({ errorMessage: JSON.stringify(e) }); 81 | }); 82 | }, 83 | loginV11() { 84 | const { username, password } = this.data; 85 | loginByV11(username, password).then((result) => 86 | this.addUser('v11', result) 87 | ).catch((e) => { 88 | console.error(e); 89 | this.setData({ errorMessage: JSON.stringify(e) }); 90 | }); 91 | }, 92 | /** 93 | * Jump to privacyTips if no credential saved 94 | */ 95 | toPrivacyTips() { 96 | if (this.data.firstTimeRemeber && this.data.remeber) { 97 | wx.navigateTo({ url: '../privacyTips/privacyTips' }); 98 | } 99 | }, 100 | /** 101 | * Refresh courseInfoArray by channel 102 | */ 103 | refreshCourseInfoArray() { 104 | console.debug(`refresh courseInfoArray of user ${this.data.id}`); 105 | this.getOpenerEventChannel().emit('refreshCourseInfoArray', this.data.id); 106 | wx.showToast({ title: '已请求刷新', icon: 'none' }); 107 | }, 108 | 109 | /** 110 | * 生命周期函数--监听页面加载 111 | */ 112 | onLoad(options) { 113 | if (!isString(options.channelOpened) || !this.getOpenerEventChannel()) { 114 | wx.navigateBack(); 115 | } 116 | 117 | const rawChannelOpened = options.channelOpened as string; 118 | try { 119 | const channelOpened = JSON.parse(rawChannelOpened); 120 | 121 | console.debug(`channelOpened =`, channelOpened); 122 | if (!channelOpened) { 123 | wx.navigateBack(); 124 | } 125 | } catch (e) { 126 | if (e instanceof TypeError) { 127 | console.warn('failed to parse channelOpened from opener'); 128 | wx.navigateBack(); 129 | } else { 130 | wx.navigateBack(); 131 | throw e; 132 | } 133 | } 134 | 135 | if (options.id) { 136 | try { 137 | const id = JSON.parse(options.id); 138 | 139 | console.debug(`id =`, id); 140 | 141 | const totalsOfCourse = getApp().globalData.totalsOfCourse; 142 | console.debug(`totalsOfCourse =`, totalsOfCourse); 143 | let totalOfCourse = totalsOfCourse[id]; 144 | 145 | if (typeof totalOfCourse !== 'number' || Number.isNaN(totalOfCourse)) { 146 | console.warn('failed to read totalsOfCourse from globalData'); 147 | totalOfCourse = 0; 148 | } 149 | 150 | this.setData({ 151 | id, 152 | totalOfCourse, 153 | hideUserManagements: false, 154 | }); 155 | } catch (e) { 156 | if (e instanceof TypeError) { 157 | console.warn('failed to parse id from opener'); 158 | wx.navigateBack(); 159 | } else { 160 | wx.navigateBack(); 161 | throw e; 162 | } 163 | } 164 | } 165 | 166 | if (options.name) { 167 | try { 168 | const name = JSON.parse(options.name); 169 | 170 | this.setData({ 171 | name, 172 | }); 173 | } catch (e) { 174 | if (e instanceof TypeError) { 175 | console.warn('failed to parse name from opener'); 176 | wx.navigateBack(); 177 | } else { 178 | wx.navigateBack(); 179 | throw e; 180 | } 181 | } 182 | } 183 | 184 | if (options.credential) { 185 | try { 186 | const credential: Credential = JSON.parse(options.credential); 187 | 188 | const totalsOfCourse = getApp().globalData.totalsOfCourse; 189 | console.debug(`totalsOfCourse =`, totalsOfCourse); 190 | let totalOfCourse = totalsOfCourse[credential.id]; 191 | 192 | if (typeof totalOfCourse !== 'number' || Number.isNaN(totalOfCourse)) { 193 | console.warn('failed to read totalsOfCourse from globalData'); 194 | totalOfCourse = 0; 195 | } 196 | 197 | this.setData({ 198 | remeber: true, 199 | id: credential.id, 200 | username: credential.username, 201 | password: credential.password, 202 | totalOfCourse, 203 | hideUserManagements: false, 204 | }); 205 | } catch (e) { 206 | if (e instanceof TypeError) { 207 | console.warn('failed to parse credential from opener'); 208 | wx.navigateBack(); 209 | } else { 210 | wx.navigateBack(); 211 | throw e; 212 | } 213 | } 214 | } 215 | 216 | if (options.firstTimeRemeber) { 217 | try { 218 | const firstTimeRemeber: boolean = JSON.parse(options.firstTimeRemeber); 219 | this.setData({ firstTimeRemeber }); 220 | } catch (e) { 221 | if (e instanceof TypeError) { 222 | console.warn('failed to parse firstTimeRemeber from opener'); 223 | wx.navigateBack(); 224 | } else { 225 | wx.navigateBack(); 226 | throw e; 227 | } 228 | } 229 | } 230 | }, 231 | 232 | /** 233 | * 生命周期函数--监听页面初次渲染完成 234 | */ 235 | onReady() { 236 | 237 | }, 238 | 239 | /** 240 | * 生命周期函数--监听页面显示 241 | */ 242 | onShow() { 243 | 244 | }, 245 | 246 | /** 247 | * 生命周期函数--监听页面隐藏 248 | */ 249 | onHide() { 250 | 251 | }, 252 | 253 | /** 254 | * 生命周期函数--监听页面卸载 255 | */ 256 | onUnload() { 257 | 258 | }, 259 | 260 | /** 261 | * 页面相关事件处理函数--监听用户下拉动作 262 | */ 263 | onPullDownRefresh() { 264 | 265 | }, 266 | 267 | /** 268 | * 页面上拉触底事件的处理函数 269 | */ 270 | onReachBottom() { 271 | 272 | }, 273 | 274 | /** 275 | * 用户点击右上角分享 276 | */ 277 | onShareAppMessage() { 278 | 279 | } 280 | }); 281 | -------------------------------------------------------------------------------- /miniprogram/utils/util.ts: -------------------------------------------------------------------------------- 1 | import { 2 | User, Cookie, RawActivity, RawActivityListObject, Activity, SignMethod, 3 | RawActivityWithKnownOtherId, Credential, CookieWithCourseInfo, CourseInfo, 4 | } from './types'; 5 | 6 | export const isString = (text: unknown): text is string => 7 | typeof text === 'string' || text instanceof String; 8 | 9 | const isCookie = (obj: any): obj is Cookie => 10 | typeof obj.expire === 'number' && typeof obj.id === 'number' && isString(obj.uf) 11 | && isString(obj.vc3) && isString(obj._d) && isString(obj.fid); 12 | 13 | const isCourseInfo = (obj: any): obj is CourseInfo => 14 | obj.course && typeof obj.course.id === 'number' && isString(obj.course.name) 15 | && obj.class && typeof obj.class.id === 'number' && isString(obj.class.name); 16 | 17 | const isUser = (obj: any): obj is User => isString(obj.name) && isCookie(obj); 18 | 19 | const isCredential = (obj: any): obj is Credential => typeof obj.id === 'number' 20 | && isString(obj.username) && isString(obj.password); 21 | 22 | const isRawActivity = (obj: any): obj is RawActivity => 23 | typeof obj.status === 'number' && typeof obj.startTime === 'number' 24 | && isString(obj.nameFour) 25 | && (typeof obj.endTime === 'number' || obj.endTime === '') 26 | && typeof obj.id === 'number' && isString(obj.nameOne); 27 | 28 | const isRawActivityListObject = (obj: any): obj is RawActivityListObject => { 29 | if (!( 30 | typeof obj.data === 'object' && obj.data !== null 31 | && Array.isArray(obj.data.activeList) 32 | )) { 33 | return false; 34 | } 35 | 36 | for (const active of obj.data.activeList) { 37 | if (!isRawActivity(active)) { 38 | console.log('active problem', active); 39 | return false; 40 | } 41 | } 42 | 43 | return true; 44 | } 45 | 46 | const toCookie = (obj: any): Cookie => { 47 | if (!isCookie(obj)) { 48 | throw TypeError('passed object is not a cookie'); 49 | } 50 | 51 | return obj; 52 | } 53 | 54 | export const toCache = (obj: any): CookieWithCourseInfo => { 55 | if (!Array.isArray(obj.courseInfoArray)) { 56 | throw TypeError('courseInfoArray in passed object is not a array'); 57 | } 58 | 59 | for (const courseInfo of obj.courseInfoArray) { 60 | if (!isCourseInfo(courseInfo)) { 61 | throw TypeError( 62 | 'courseInfo in courseInfoArray of passed object is not a array', 63 | ); 64 | } 65 | } 66 | 67 | return { ...obj, cookie: toCookie(obj.cookie) }; 68 | } 69 | 70 | export const toUser = (obj: any): User => { 71 | if (!isUser(obj)) { 72 | throw TypeError('passed object is not a User'); 73 | } 74 | 75 | return obj; 76 | } 77 | 78 | export const toCredential = (obj: any): Credential => { 79 | if (!isCredential(obj)) { 80 | throw TypeError('passed object is not a Credential'); 81 | } 82 | 83 | return obj; 84 | } 85 | 86 | export const toActiveListObject = (obj: any): RawActivityListObject => { 87 | if (!isRawActivityListObject(obj)) { 88 | throw TypeError('passed object is not a RawActivityListObject'); 89 | } 90 | 91 | return obj; 92 | } 93 | 94 | const isRawActivityWithKnownOtherId = (rawActivity: any): 95 | rawActivity is RawActivityWithKnownOtherId => isString(rawActivity.otherId) 96 | && ['0', '2', '3', '4', '5'].includes(rawActivity.otherId) 97 | && isRawActivity(rawActivity); 98 | 99 | export const toRawActivityWithKnownOtherId = (rawActivity: any): 100 | RawActivityWithKnownOtherId => { 101 | if (!isRawActivityWithKnownOtherId(rawActivity)) { 102 | throw TypeError('passed rawActivity have no otherId'); 103 | } 104 | 105 | return rawActivity; 106 | } 107 | 108 | export const isSignMethod = (text: string): text is SignMethod => [ 109 | 'clickOrPhoto', 'qrCode', 'gesture', 'location', 'code', 110 | ].includes(text); 111 | 112 | export const toActivity = (rawActivity: RawActivityWithKnownOtherId): Activity => { 113 | let signMethod: SignMethod = 'unknown'; 114 | switch (rawActivity.otherId) { 115 | case '0': 116 | signMethod = 'clickOrPhoto'; 117 | break; 118 | case '2': 119 | signMethod = 'qrCode'; 120 | break; 121 | case '3': 122 | signMethod = 'gesture'; 123 | break; 124 | case '4': 125 | signMethod = 'location'; 126 | break; 127 | case '5': 128 | signMethod = 'code'; 129 | break; 130 | } 131 | 132 | return { 133 | id: rawActivity.id, 134 | name: rawActivity.nameOne, 135 | signMethod, 136 | startTime: rawActivity.startTime, 137 | endTime: rawActivity.endTime === '' ? -1 : rawActivity.endTime, 138 | endTimeForHuman: rawActivity.nameFour, 139 | raw: rawActivity, 140 | }; 141 | }; 142 | 143 | export const parseCookiesFromWx = (cookiesFromWx: Array): Cookie => { 144 | let cookie: Cookie = { 145 | // I belive that Cookie will expire within 4 years 146 | expire: (new Date()).getTime() + 1000 * 60 * 60 * 24 * 365 * 4, 147 | id: -1, 148 | uf: '', 149 | vc3: '', 150 | _d: '', 151 | fid: '', 152 | }; 153 | // Do not check expire 154 | const keys = Object.keys(cookie) 155 | .filter((key) => key !== 'expire' && key !== 'id') as Array; 156 | 157 | const now = (new Date()).getTime(); 158 | cookiesFromWx.forEach((c) => { 159 | const pairs = c.split('; '); 160 | const important = pairs[0]; 161 | const equalIndex = important.indexOf('='); 162 | const key = important.slice(0, equalIndex); 163 | const value = important.slice(equalIndex + 1); 164 | 165 | if (key === '_uid') { 166 | const id = Number.parseInt(value, 10); 167 | cookie = { ...cookie, id }; 168 | } 169 | cookie = { ...cookie, [key]: value }; 170 | 171 | for (const pair of pairs) { 172 | if (pair.startsWith('Expires=')) { 173 | const date = pair.slice('Expires='.length); 174 | const parsedDate = Date.parse(date); 175 | if (!Number.isNaN(parsedDate)) { 176 | if (parsedDate < cookie.expire && parsedDate > now) { 177 | cookie.expire = parsedDate; 178 | } 179 | } 180 | 181 | break; 182 | } 183 | } 184 | }); 185 | 186 | keys.forEach((key) => { 187 | const value = cookie[key]; 188 | 189 | if (isString(value)) { 190 | if (!((cookie[key] as string).length > 0)) { 191 | throw TypeError(`Failed to parse cookie ${key}: ${value} to string`); 192 | } 193 | } else if (typeof value === 'number') { 194 | if (Number.isNaN(value)) { 195 | throw TypeError(`failed to parse cookie ${key}: ${value} to number`); 196 | } 197 | } else { 198 | throw TypeError(`failed to parse cookie ${key}: ${value}`); 199 | } 200 | }); 201 | return cookie; 202 | } 203 | 204 | export const userToCookie = (user: User): Cookie => { 205 | const keys = Object.keys(user) as (keyof User)[]; 206 | 207 | return keys.reduce((existedCookie, key) => key !== 'name' 208 | ? { ...existedCookie, [key]: user[key] } 209 | : existedCookie, {} as any); 210 | } 211 | 212 | export const toCookieString = (cookie: { [key: string]: number | string }): string => 213 | Object.entries(cookie).map(([key, value]) => `${key}=${value}; `) 214 | .concat('SameSite=Strict; ').join(''); 215 | 216 | export const parametersToString = ( 217 | parameters: { [key: string]: number | string } 218 | ): string => Object.entries(parameters) 219 | .map(([key, value]) => `${key}=${value}`).join('&'); 220 | 221 | export const parametersToStringifyString = ( 222 | parameters: { [key: string]: any } 223 | ): string => Object.entries(parameters) 224 | .map(([key, value]) => `${key}=${JSON.stringify(value)}`).join('&'); 225 | 226 | export const cookieForSign = (cookie: Cookie): { [key: string]: any } => ({ 227 | uf: cookie.uf, _d: cookie._d, UID: cookie.id, vc3: cookie.vc3 228 | }); 229 | -------------------------------------------------------------------------------- /miniprogram/pages/userManager/userManager.ts: -------------------------------------------------------------------------------- 1 | import { loginByFanya } from "../../services/login"; 2 | import { Credential, GetCookieFailedResult, LoginResult, NameFailedResult, User } from "../../utils/types"; 3 | import { isString, parametersToStringifyString, toCredential } from "../../utils/util"; 4 | 5 | // pages/userManager/userManager.ts 6 | Page({ 7 | 8 | /** 9 | * 页面的初始数据 10 | */ 11 | data: { 12 | users: [] as User[], 13 | credentials: [] as Credential[], 14 | idToCredentials: {} as { [id: number]: Credential }, 15 | }, 16 | /** 17 | * Convert credentials to Object for wxml 18 | * @param credentials credentials from storage 19 | */ 20 | toIdToCredentials(credentials: Credential[]) { 21 | return credentials.reduce((c, credential) => ({ 22 | ...c, 23 | [credential.id]: credential, 24 | }), {}); 25 | }, 26 | /** 27 | * Open userInfo from wxml 28 | * @param e id, name and credential from wxml 29 | */ 30 | openUserInfo(e: WechatMiniprogram.BaseEvent) { 31 | const { id, name, credential } = e.currentTarget.dataset as { 32 | [key: string]: string 33 | }; 34 | 35 | let existedParameters = {}; 36 | try { 37 | existedParameters = JSON.parse(e.target.dataset.parameters); 38 | } catch (e) { 39 | if (!(e instanceof SyntaxError)) { 40 | console.warn( 41 | 'unknown error occurred during parsing parameters for openUserInfo', 42 | e, 43 | ); 44 | } 45 | } 46 | 47 | const parameters = { 48 | ...existedParameters, 49 | channelOpened: this.getOpenerEventChannel() !== null, 50 | firstTimeRemeber: !(this.data.credentials.length > 0), 51 | ...(credential ? { credential } : {}), 52 | ...(id ? { id } : {}), 53 | ...(name ? { name } : {}), 54 | }; 55 | 56 | console.debug('parsed parameters =', parameters); 57 | 58 | wx.navigateTo({ 59 | url: `../userInfo/userInfo?${parametersToStringifyString(parameters)}`, 60 | events: { 61 | /** 62 | * Add user from channel 63 | * @param user user to be added 64 | */ 65 | addUser: (user: User) => { 66 | const filteredUsers = this.data.users.filter((u) => u.id !== user.id); 67 | const newUsers = filteredUsers.concat(user); 68 | 69 | console.debug('newUsers =', newUsers); 70 | 71 | this.setData({ users: newUsers }); 72 | this.getOpenerEventChannel().emit('addUser', user); 73 | }, 74 | /** 75 | * Add credential from channel 76 | * @param credential credential to be added 77 | */ 78 | addCredential: (credential: Credential) => { 79 | const filteredCredentials = this.data.credentials 80 | .filter((c) => c.id !== credential.id); 81 | const newCredentials = filteredCredentials.concat(credential); 82 | 83 | this.setData({ 84 | credentials: newCredentials, 85 | idToCredentials: this.toIdToCredentials(newCredentials), 86 | }); 87 | wx.setStorage({ key: 'credentials', encrypt: true, data: newCredentials }); 88 | }, 89 | /** 90 | * Remove user from channel 91 | * @param id id of user to be removed from caches, users and nameOfUsers 92 | */ 93 | removeUser: (id: number) => { 94 | console.debug(`remove user ${id}`); 95 | 96 | const users = this.data.users.filter((u) => u.id !== id); 97 | this.setData({ users }); 98 | this.getOpenerEventChannel().emit('removeUser', id); 99 | }, 100 | /** 101 | * Remove credential from channel 102 | * @param id id of credential to be removed from credentials 103 | */ 104 | removeCredential: (id: number) => { 105 | console.debug(`remove credential ${id}`); 106 | 107 | const credentials = this.data.credentials.filter((c) => c.id !== id); 108 | wx.setStorage({ key: 'credentials', encrypt: true, data: credentials }) 109 | .then(() => 110 | this.setData({ 111 | credentials: credentials, 112 | idToCredentials: this.toIdToCredentials(credentials), 113 | }) 114 | ); 115 | }, 116 | /** 117 | * Refresh courseInfoArray by ID from channel 118 | * @param id ID of user to be refreshed 119 | */ 120 | refreshCourseInfoArray: (id: number) => this.getOpenerEventChannel() 121 | .emit('refreshCourseInfoArray', id), 122 | } 123 | }); 124 | }, 125 | 126 | copyGitHubLink() { 127 | wx.setClipboardData({ 128 | data: 'https://github.com/DroppingOutSpeedrun/Dropping-Out-Speedrun', 129 | }); 130 | }, 131 | 132 | copyBitbucketLink() { 133 | wx.setClipboardData({ 134 | data: 'https://bitbucket.org/dropping-out-speedrun/dropping-out-speedrun', 135 | }); 136 | }, 137 | 138 | /** 139 | * 生命周期函数--监听页面加载 140 | */ 141 | onLoad(options) { 142 | if (!this.getOpenerEventChannel()) { 143 | console.warn('openerEventChannel not found'); 144 | wx.navigateBack(); 145 | } 146 | 147 | wx.getStorage({ key: 'credentials', encrypt: true }).then((result) => { 148 | console.debug('get credentials from storage', result); 149 | 150 | const credentials = result.data; 151 | if (!Array.isArray(credentials)) { 152 | console.warn('failed to parse credentials from storage', result); 153 | wx.showToast({ title: '读取账号密码信息时出错', icon: 'error' }); 154 | return; 155 | } 156 | 157 | try { 158 | const prasedCredentials = credentials.map((credential) => 159 | toCredential(credential) 160 | ); 161 | this.setData({ 162 | credentials: prasedCredentials, 163 | idToCredentials: this.toIdToCredentials(prasedCredentials), 164 | }); 165 | } catch (e) { 166 | if (e instanceof TypeError) { 167 | console.warn('failed to parse credentials'); 168 | wx.removeStorage({ key: 'credentials' }); 169 | } else { 170 | throw e; 171 | } 172 | } 173 | }).catch((e) => { 174 | if (isString(e.errMsg) && e.errMsg.includes('data not found')) { 175 | console.debug('credentials is empty yet'); 176 | } else { 177 | throw e; 178 | } 179 | }); 180 | 181 | if (options.users) { 182 | try { 183 | const users = JSON.parse(options.users); 184 | console.debug('users from opener', users); 185 | this.setData({ users }); 186 | } catch (e) { 187 | if (e instanceof TypeError) { 188 | console.warn('failed to parse users from opener'); 189 | } else { 190 | throw e; 191 | } 192 | } 193 | } 194 | }, 195 | 196 | /** 197 | * 生命周期函数--监听页面初次渲染完成 198 | */ 199 | onReady() { 200 | 201 | }, 202 | 203 | /** 204 | * 生命周期函数--监听页面显示 205 | */ 206 | onShow() { 207 | }, 208 | 209 | /** 210 | * 生命周期函数--监听页面隐藏 211 | */ 212 | onHide() { 213 | 214 | }, 215 | 216 | /** 217 | * 生命周期函数--监听页面卸载 218 | */ 219 | onUnload() { 220 | 221 | }, 222 | 223 | /** 224 | * 页面相关事件处理函数--监听用户下拉动作 225 | */ 226 | onPullDownRefresh() { 227 | console.debug('start refreshing cookies'); 228 | let promises: Promise[] = []; 229 | this.data.credentials.forEach(({ loginType, username, password }) => { 230 | switch (loginType) { 231 | case 'v11': 232 | promises = promises.concat(loginByFanya(username, password)); 233 | break; 234 | case 'fanya': 235 | default: 236 | if (loginType !== 'fanya') { 237 | console.warn('unknown `loginType` detected, use `fanya` in default'); 238 | } 239 | promises = promises.concat(loginByFanya(username, password)); 240 | break; 241 | } 242 | }); 243 | 244 | Promise.allSettled(promises).then((results) => results.forEach((result) => { 245 | if (result.status !== 'fulfilled') { 246 | console.error('failed to process this promise', result); 247 | wx.showToast({ 248 | title: `部分用户的登录信息刷新失败:${result.reason}`, 249 | icon: 'error', 250 | }); 251 | return; 252 | } 253 | 254 | const { status, message, data } = result.value; 255 | 256 | if (!status) { 257 | console.error(message, data); 258 | wx.showToast({ title: message, icon: 'error' }); 259 | return; 260 | } 261 | 262 | const user: User = (result.value as any).user; 263 | this.getOpenerEventChannel().emit('addUser', user); 264 | })).finally(() => wx.stopPullDownRefresh()); 265 | }, 266 | 267 | /** 268 | * 页面上拉触底事件的处理函数 269 | */ 270 | onReachBottom() { 271 | 272 | }, 273 | 274 | /** 275 | * 用户点击右上角分享 276 | */ 277 | onShareAppMessage() { 278 | 279 | } 280 | }); 281 | -------------------------------------------------------------------------------- /typings/types/wx/lib.wx.page.d.ts: -------------------------------------------------------------------------------- 1 | /*! ***************************************************************************** 2 | Copyright (c) 2023 Tencent, Inc. All rights reserved. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ***************************************************************************** */ 22 | 23 | declare namespace WechatMiniprogram.Page { 24 | type Instance< 25 | TData extends DataOption, 26 | TCustom extends CustomOption 27 | > = OptionalInterface & 28 | InstanceProperties & 29 | InstanceMethods & 30 | Data & 31 | TCustom 32 | type Options< 33 | TData extends DataOption, 34 | TCustom extends CustomOption 35 | > = (TCustom & 36 | Partial> & 37 | Partial & { 38 | options?: Component.ComponentOptions 39 | }) & 40 | ThisType> 41 | type TrivialInstance = Instance 42 | interface Constructor { 43 | ( 44 | options: Options 45 | ): void 46 | } 47 | interface ILifetime { 48 | /** 生命周期回调—监听页面加载 49 | * 50 | * 页面加载时触发。一个页面只会调用一次,可以在 onLoad 的参数中获取打开当前页面路径中的参数。 51 | */ 52 | onLoad( 53 | /** 打开当前页面路径中的参数 */ 54 | query: Record 55 | ): void | Promise 56 | /** 生命周期回调—监听页面显示 57 | * 58 | * 页面显示/切入前台时触发。 59 | */ 60 | onShow(): void | Promise 61 | /** 生命周期回调—监听页面初次渲染完成 62 | * 63 | * 页面初次渲染完成时触发。一个页面只会调用一次,代表页面已经准备妥当,可以和视图层进行交互。 64 | * 65 | 66 | * 注意:对界面内容进行设置的 API 如`wx.setNavigationBarTitle`,请在`onReady`之后进行。 67 | */ 68 | onReady(): void | Promise 69 | /** 生命周期回调—监听页面隐藏 70 | * 71 | * 页面隐藏/切入后台时触发。 如 `navigateTo` 或底部 `tab` 切换到其他页面,小程序切入后台等。 72 | */ 73 | onHide(): void | Promise 74 | /** 生命周期回调—监听页面卸载 75 | * 76 | * 页面卸载时触发。如`redirectTo`或`navigateBack`到其他页面时。 77 | */ 78 | onUnload(): void | Promise 79 | /** 监听用户下拉动作 80 | * 81 | * 监听用户下拉刷新事件。 82 | * - 需要在`app.json`的`window`选项中或页面配置中开启`enablePullDownRefresh`。 83 | * - 可以通过`wx.startPullDownRefresh`触发下拉刷新,调用后触发下拉刷新动画,效果与用户手动下拉刷新一致。 84 | * - 当处理完数据刷新后,`wx.stopPullDownRefresh`可以停止当前页面的下拉刷新。 85 | */ 86 | onPullDownRefresh(): void | Promise 87 | /** 页面上拉触底事件的处理函数 88 | * 89 | * 监听用户上拉触底事件。 90 | * - 可以在`app.json`的`window`选项中或页面配置中设置触发距离`onReachBottomDistance`。 91 | * - 在触发距离内滑动期间,本事件只会被触发一次。 92 | */ 93 | onReachBottom(): void | Promise 94 | /** 用户点击右上角转发 95 | * 96 | * 监听用户点击页面内转发按钮(`