├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .postcssrc.js ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── README.md ├── babel.config.js ├── package.json ├── public ├── favicon.ico └── icons │ ├── favicon-128x128.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── favicon-96x96.png ├── quasar.conf.js ├── src ├── App.vue ├── api │ ├── index.ts │ └── models.ts ├── assets │ ├── background.webp │ ├── login.webp │ └── quasar-logo-full.svg ├── boot │ ├── .gitkeep │ ├── axios.ts │ ├── composition-api.ts │ └── geetest.ts ├── components │ ├── DarkModeButton.vue │ ├── GameAccountForm.vue │ ├── GameCaptchaDialog.vue │ ├── GameConfigControls.vue │ ├── GameDetailCard.vue │ ├── GameLogTimeline.vue │ ├── GameSquadsPanel.vue │ ├── LoginForm.vue │ ├── LogoutButton.vue │ └── WebSocketStatus.vue ├── css │ ├── app.scss │ └── quasar.variables.scss ├── env.d.ts ├── index.template.html ├── layouts │ └── AltLayout.vue ├── pages │ ├── Error404.vue │ ├── GameView.vue │ ├── IndexView.vue │ ├── LoginView.vue │ └── SideBarView.vue ├── router │ ├── index.ts │ └── routes.ts ├── shims-vue.d.ts ├── store │ ├── activity.ts │ ├── index.ts │ ├── login.ts │ └── store-flag.d.ts └── utils │ ├── geetest.js │ └── index.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /src-bex/www 3 | /src-capacitor 4 | /src-cordova 5 | /.quasar 6 | /node_modules 7 | /src-ssr 8 | .eslintrc.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | module.exports = { 3 | // https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy 4 | // This option interrupts the configuration hierarchy at this file 5 | // Remove this if you have an higher level ESLint config file (it usually happens into a monorepos) 6 | root: true, 7 | 8 | // https://eslint.vuejs.org/user-guide/#how-to-use-custom-parser 9 | // Must use parserOptions instead of "parser" to allow vue-eslint-parser to keep working 10 | // `parser: 'vue-eslint-parser'` is already included with any 'plugin:vue/**' config and should be omitted 11 | parserOptions: { 12 | // https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser#configuration 13 | // https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#eslint 14 | // Needed to make the parser take into account 'vue' files 15 | extraFileExtensions: ['.vue'], 16 | parser: '@typescript-eslint/parser', 17 | project: resolve(__dirname, './tsconfig.json'), 18 | tsconfigRootDir: __dirname, 19 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 20 | sourceType: 'module' // Allows for the use of imports 21 | }, 22 | 23 | env: { 24 | browser: true 25 | }, 26 | 27 | // Rules order is important, please avoid shuffling them 28 | extends: [ 29 | // Base ESLint recommended rules 30 | // 'eslint:recommended', 31 | 32 | // https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage 33 | // ESLint typescript rules 34 | 'plugin:@typescript-eslint/recommended', 35 | // consider disabling this class of rules if linting takes too long 36 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 37 | 38 | // Uncomment any of the lines below to choose desired strictness, 39 | // but leave only one uncommented! 40 | // See https://eslint.vuejs.org/rules/#available-rules 41 | 'plugin:vue/essential', // Priority A: Essential (Error Prevention) 42 | // 'plugin:vue/strongly-recommended', // Priority B: Strongly Recommended (Improving Readability) 43 | // 'plugin:vue/recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead) 44 | 45 | // https://github.com/prettier/eslint-config-prettier#installation 46 | // usage with Prettier, provided by 'eslint-config-prettier'. 47 | 'prettier' 48 | ], 49 | 50 | plugins: [ 51 | // required to apply rules which need type information 52 | '@typescript-eslint', 53 | 54 | // https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-file 55 | // required to lint *.vue files 56 | 'vue', 57 | 58 | // https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674 59 | // Prettier has not been included as plugin to avoid performance impact 60 | // add it as an extension for your IDE 61 | ], 62 | 63 | globals: { 64 | ga: 'readonly', // Google Analytics 65 | cordova: 'readonly', 66 | __statics: 'readonly', 67 | process: 'readonly', 68 | Capacitor: 'readonly', 69 | chrome: 'readonly' 70 | }, 71 | 72 | // add your custom rules here 73 | rules: { 74 | 'prefer-promise-reject-errors': 'off', 75 | 76 | // TypeScript 77 | quotes: ['warn', 'single', { avoidEscape: true }], 78 | '@typescript-eslint/explicit-function-return-type': 'off', 79 | '@typescript-eslint/explicit-module-boundary-types': 'off', 80 | 81 | // allow debugger during development only 82 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | # Check for updates managed by Composer once a week 7 | interval: "weekly" 8 | commit-message: 9 | prefix: ":arrow_up: " 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the master branch 6 | - push 7 | - pull_request 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | - workflow_dispatch 11 | 12 | env: 13 | TARGET_REGISTRY: https://registry.yarnpkg.com 14 | ORIGIN_REGISTRY: https://registry.npm.taobao.org 15 | 16 | jobs: 17 | lint: 18 | name: Lint 19 | # The type of runner that the job will run on 20 | runs-on: ubuntu-latest 21 | 22 | # Steps represent a sequence of tasks that will be executed as part of the job 23 | steps: 24 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 25 | - uses: actions/checkout@v2 26 | 27 | - name: Setup Node.js environment 28 | uses: actions/setup-node@v2 29 | 30 | - name: Replace registry 31 | run: sed -i -e "s#$TARGET_REGISTRY/#$ORIGIN_REGISTRY#g" yarn.lock 32 | 33 | - name: Get yarn cache directory path 34 | id: yarn-cache-dir-path 35 | run: echo "::set-output name=dir::$(yarn cache dir)" 36 | 37 | - uses: actions/cache@v2 38 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 39 | with: 40 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 41 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 42 | restore-keys: ${{ runner.os }}-yarn- 43 | 44 | - name: Install depends 45 | run: yarn install 46 | 47 | - name: Lint code 48 | run: yarn run lint 49 | 50 | build: 51 | name: Build Dist 52 | runs-on: ubuntu-latest 53 | needs: [lint] 54 | 55 | steps: 56 | - uses: actions/checkout@v2 57 | 58 | - name: Setup Node.js environment 59 | uses: actions/setup-node@v2 60 | 61 | - name: Replace registry 62 | run: sed -i -e "s#$TARGET_REGISTRY/#$ORIGIN_REGISTRY#g" yarn.lock 63 | 64 | - name: Get yarn cache directory path 65 | id: yarn-cache-dir-path 66 | run: echo "::set-output name=dir::$(yarn cache dir)" 67 | 68 | - uses: actions/cache@v2 69 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 70 | with: 71 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 72 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 73 | restore-keys: ${{ runner.os }}-yarn- 74 | 75 | - name: Install depends 76 | run: yarn install 77 | 78 | - name: Build dist 79 | run: yarn run quasar build 80 | 81 | - name: Upload dist artifact 82 | uses: actions/upload-artifact@v2 83 | with: 84 | name: dist 85 | path: dist/spa 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .thumbs.db 3 | node_modules 4 | 5 | # Quasar core related directories 6 | .quasar 7 | /dist 8 | 9 | # Cordova related directories and files 10 | /src-cordova/node_modules 11 | /src-cordova/platforms 12 | /src-cordova/plugins 13 | /src-cordova/www 14 | 15 | # Capacitor related directories and files 16 | /src-capacitor/www 17 | /src-capacitor/node_modules 18 | 19 | # BEX related directories and files 20 | /src-bex/www 21 | /src-bex/js/core 22 | 23 | # Log files 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # Editor directories and files 29 | .idea 30 | *.suo 31 | *.ntvs* 32 | *.njsproj 33 | *.sln 34 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | plugins: [ 5 | // to edit target browsers: use "browserslist" field in package.json 6 | require('autoprefixer') 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "octref.vetur" 6 | ], 7 | "unwantedRecommendations": [ 8 | "hookyqr.beautify", 9 | "dbaeumer.jshint", 10 | "ms-vscode.vscode-typescript-tslint-plugin" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "firefox", 6 | "request": "launch", 7 | "name": "vuejs: firefox", 8 | "url": "http://localhost:8080", 9 | "webRoot": "${workspaceFolder}/src", 10 | "pathMappings": [ 11 | { 12 | "url": "webpack:///src/", 13 | "path": "${webRoot}/" 14 | } 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "vetur.validation.template": false, 3 | "vetur.format.enable": true, 4 | "eslint.validate": [ 5 | "javascript", 6 | "javascriptreact", 7 | "typescript", 8 | "vue" 9 | ], 10 | "typescript.tsdk": "node_modules/typescript/lib", 11 | "vetur.experimental.templateInterpolationService": true, 12 | "editor.formatOnPaste": true, 13 | "editor.formatOnSave": true, 14 | "editor.codeActionsOnSave": { 15 | "source.fixAll": true 16 | }, 17 | "cSpell.words": [ 18 | "Geetest" 19 | ], 20 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ClosureFrontline (arknight-offilne-frontend) 2 | 3 | **Official frontend for [Closure Studio](https://ak.nai-ve.com/)** 4 | 5 | --- 6 | 7 | ## Deployment 8 | 9 | ### Install the dependencies 10 | 11 | ```bash 12 | yarn install 13 | ``` 14 | 15 | ### Start the app in development mode (hot-code reloading, error reporting, etc.) 16 | 17 | ```bash 18 | quasar dev 19 | ``` 20 | 21 | ### Lint the files 22 | 23 | ```bash 24 | yarn run lint 25 | ``` 26 | 27 | ### Build the app for production 28 | 29 | ```bash 30 | quasar build 31 | ``` 32 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | presets: [ 4 | '@quasar/babel-preset-app' 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arknight-offilne-frontend", 3 | "version": "0.0.1", 4 | "description": "Offline game agent for Arknights", 5 | "productName": "Arknights Offline", 6 | "author": "", 7 | "private": true, 8 | "scripts": { 9 | "lint": "eslint --ext .js,.ts,.vue ./", 10 | "test": "echo \"No test specified\" && exit 0", 11 | "dev": "quasar dev" 12 | }, 13 | "dependencies": { 14 | "@quasar/extras": "^1.10.4", 15 | "@vue/composition-api": "^1.0.0-rc.9", 16 | "axios": "^0.21.1", 17 | "core-js": "^3.13.0", 18 | "quasar": "^1.15.18" 19 | }, 20 | "devDependencies": { 21 | "@quasar/app": "^2.2.7", 22 | "@types/node": "^15.6.1", 23 | "@typescript-eslint/eslint-plugin": "^4.22.0", 24 | "@typescript-eslint/parser": "^4.16.1", 25 | "babel-eslint": "^10.0.1", 26 | "eslint": "^7.25.0", 27 | "eslint-config-prettier": "^8.3.0", 28 | "eslint-plugin-vue": "^7.7.0" 29 | }, 30 | "browserslist": [ 31 | "last 10 Chrome versions", 32 | "last 10 Firefox versions", 33 | "last 4 Edge versions", 34 | "last 7 Safari versions", 35 | "last 8 Android versions", 36 | "last 8 ChromeAndroid versions", 37 | "last 8 FirefoxAndroid versions", 38 | "last 10 iOS versions", 39 | "last 5 Opera versions" 40 | ], 41 | "engines": { 42 | "node": ">= 10.18.1", 43 | "npm": ">= 6.13.4", 44 | "yarn": ">= 1.21.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/closure-studio/arknights-offline-frontend/f64496847d12a83f2b3783231ae429ad3e2a6556/public/favicon.ico -------------------------------------------------------------------------------- /public/icons/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/closure-studio/arknights-offline-frontend/f64496847d12a83f2b3783231ae429ad3e2a6556/public/icons/favicon-128x128.png -------------------------------------------------------------------------------- /public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/closure-studio/arknights-offline-frontend/f64496847d12a83f2b3783231ae429ad3e2a6556/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/closure-studio/arknights-offline-frontend/f64496847d12a83f2b3783231ae429ad3e2a6556/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/closure-studio/arknights-offline-frontend/f64496847d12a83f2b3783231ae429ad3e2a6556/public/icons/favicon-96x96.png -------------------------------------------------------------------------------- /quasar.conf.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file runs in a Node context (it's NOT transpiled by Babel), so use only 3 | * the ES6 features that are supported by your Node version. https://node.green/ 4 | */ 5 | 6 | // Configuration for your app 7 | // https://quasar.dev/quasar-cli/quasar-conf-js 8 | /* eslint-env node */ 9 | /* eslint-disable @typescript-eslint/no-var-requires */ 10 | const { configure } = require('quasar/wrappers'); 11 | 12 | module.exports = configure(function(/* ctx */) { 13 | return { 14 | // https://quasar.dev/quasar-cli/supporting-ts 15 | supportTS: { 16 | tsCheckerConfig: { 17 | eslint: true 18 | } 19 | }, 20 | 21 | // https://quasar.dev/quasar-cli/prefetch-feature 22 | preFetch: true, 23 | 24 | // app boot file (/src/boot) 25 | // --> boot files are part of "main.js" 26 | // https://quasar.dev/quasar-cli/boot-files 27 | boot: ['composition-api', 'axios', 'geetest'], 28 | 29 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css 30 | css: ['app.scss'], 31 | 32 | // https://github.com/quasarframework/quasar/tree/dev/extras 33 | extras: [ 34 | // 'ionicons-v4', 35 | // 'mdi-v5', 36 | // 'fontawesome-v5', 37 | // 'eva-icons', 38 | // 'themify', 39 | // 'line-awesome', 40 | // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both! 41 | 42 | 'roboto-font', // optional, you are not bound to it 43 | 'material-icons' // optional, you are not bound to it 44 | ], 45 | 46 | // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build 47 | build: { 48 | vueRouterMode: 'hash', // available values: 'hash', 'history' 49 | 50 | // transpile: false, 51 | 52 | // Add dependencies for transpiling with Babel (Array of string/regex) 53 | // (from node_modules, which are by default not transpiled). 54 | // Applies only if "transpile" is set to true. 55 | // transpileDependencies: [], 56 | 57 | // rtl: false, // https://quasar.dev/options/rtl-support 58 | // preloadChunks: true, 59 | // showProgress: false, 60 | // gzip: true, 61 | // analyze: true, 62 | 63 | // Options below are automatically set depending on the env, set them if you want to override 64 | // extractCSS: false, 65 | 66 | // https://quasar.dev/quasar-cli/handling-webpack 67 | extendWebpack(/* cfg */) { 68 | // Add here your webpack customizations 69 | } 70 | }, 71 | 72 | // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer 73 | devServer: { 74 | https: false, 75 | port: 8080, 76 | open: true // opens browser window automatically 77 | }, 78 | 79 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework 80 | framework: { 81 | iconSet: 'material-icons', // Quasar icon set 82 | lang: 'zh-hans', // Quasar language pack 83 | config: {}, 84 | 85 | // Possible values for "importStrategy": 86 | // * 'auto' - (DEFAULT) Auto-import needed Quasar components & directives 87 | // * 'all' - Manually specify what to import 88 | importStrategy: 'auto', 89 | 90 | // For special cases outside of where "auto" importStrategy can have an impact 91 | // (like functional components as one of the examples), 92 | // you can manually specify Quasar components/directives to be available everywhere: 93 | // 94 | // components: [], 95 | // directives: [], 96 | 97 | // Quasar plugins 98 | plugins: ['LocalStorage', 'SessionStorage', 'Notify', 'Loading', 'Dialog'] 99 | }, 100 | 101 | // animations: 'all', // --- includes all animations 102 | // https://quasar.dev/options/animations 103 | animations: [], 104 | 105 | // https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr 106 | ssr: { 107 | pwa: false 108 | }, 109 | 110 | // https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa 111 | pwa: { 112 | workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest' 113 | workboxOptions: {}, // only for GenerateSW 114 | manifest: { 115 | name: 'Arknights Offline', 116 | short_name: 'Arknights Offline', 117 | description: 'Offline game agent for Arknights', 118 | display: 'standalone', 119 | orientation: 'portrait', 120 | background_color: '#ffffff', 121 | theme_color: '#027be3', 122 | icons: [ 123 | { 124 | src: 'icons/icon-128x128.png', 125 | sizes: '128x128', 126 | type: 'image/png' 127 | }, 128 | { 129 | src: 'icons/icon-192x192.png', 130 | sizes: '192x192', 131 | type: 'image/png' 132 | }, 133 | { 134 | src: 'icons/icon-256x256.png', 135 | sizes: '256x256', 136 | type: 'image/png' 137 | }, 138 | { 139 | src: 'icons/icon-384x384.png', 140 | sizes: '384x384', 141 | type: 'image/png' 142 | }, 143 | { 144 | src: 'icons/icon-512x512.png', 145 | sizes: '512x512', 146 | type: 'image/png' 147 | } 148 | ] 149 | } 150 | }, 151 | 152 | // Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova 153 | cordova: { 154 | // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing 155 | }, 156 | 157 | // Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor 158 | capacitor: { 159 | hideSplashscreen: true 160 | }, 161 | 162 | // Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron 163 | electron: { 164 | bundler: 'packager', // 'packager' or 'builder' 165 | 166 | packager: { 167 | // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options 168 | // OS X / Mac App Store 169 | // appBundleId: '', 170 | // appCategoryType: '', 171 | // osxSign: '', 172 | // protocol: 'myapp://path', 173 | // Windows only 174 | // win32metadata: { ... } 175 | }, 176 | 177 | builder: { 178 | // https://www.electron.build/configuration/configuration 179 | 180 | appId: 'arknight-offilne-frontend' 181 | }, 182 | 183 | // More info: https://quasar.dev/quasar-cli/developing-electron-apps/node-integration 184 | nodeIntegration: true, 185 | 186 | extendWebpack(/* cfg */) { 187 | // do something with Electron main process Webpack cfg 188 | // chainWebpack also available besides this extendWebpack 189 | } 190 | } 191 | }; 192 | }); 193 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 13 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import _axios, { AxiosError } from 'axios'; 3 | import { Store } from 'vuex'; 4 | import { QVueGlobals } from 'quasar'; 5 | 6 | import * as models from './models'; 7 | import { StateInterface } from '../store'; 8 | 9 | export const baseApi = new URL( 10 | process.env.API_ADDRESS || 'https://akapi.nai-ve.com' 11 | ); 12 | export const baseStatic = new URL( 13 | process.env.STATIC_ADDRESS || 'https://akres.nai-ve.com' 14 | ); 15 | 16 | export const axios = _axios.create({ 17 | timeout: 6000, 18 | baseURL: baseApi.toString(), 19 | responseType: 'json' 20 | }); 21 | 22 | async function request(endpoint: string, data?: unknown) { 23 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 24 | const store = Vue.prototype.$store as Store; 25 | const token = store.state.login.account?.token; 26 | axios.defaults.headers = token ? { Token: token } : {}; 27 | 28 | try { 29 | const response = await axios.post(endpoint, data); 30 | return response.data as models.GeneralResponse; 31 | } catch (err) { 32 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 33 | const quasar = Vue.prototype.$q as QVueGlobals; 34 | const error = err as AxiosError; 35 | 36 | if (error.response) { 37 | quasar.notify({ 38 | type: 'negative', 39 | progress: true, 40 | position: 'top', 41 | message: `服务器返回错误 - ${error.response.status}`, 42 | caption: error.response.data.message 43 | }); 44 | } else { 45 | quasar.notify({ 46 | type: 'negative', 47 | progress: true, 48 | position: 'top', 49 | message: '网络请求过程出错', 50 | caption: error.message 51 | }); 52 | } 53 | 54 | throw error; 55 | } 56 | } 57 | 58 | export default { 59 | async userLogin(data: { username: string; password: string }) { 60 | return await request('/auth/userlogin', data); 61 | }, 62 | async verifyToken(data: { uid: number }) { 63 | return await request('/auth/verifyToken', data); 64 | }, 65 | async userRegister(data: { username: string; password: string }) { 66 | return await request('/auth/userregister', data); 67 | }, 68 | async getGamesAccounts() { 69 | return await request('/user/getGamesAccounts'); 70 | }, 71 | async createGame(data: { 72 | account: string; 73 | password: string; 74 | platform: number; 75 | }) { 76 | return await request('/user/createGame', data); 77 | }, 78 | async delGame(data: { account: string }) { 79 | return await request('/user/delGame', { 80 | ...data, 81 | password: '' 82 | }); 83 | }, 84 | async getGameData(data: { account: string }) { 85 | return await request('/game/getGameData', data); 86 | }, 87 | async gameLogin(data: { account: string }) { 88 | return await request<[]>('/game/gameLogin', data); 89 | }, 90 | async setGamePause(data: { account: string }) { 91 | return await request<[]>('/game/setGamePause', data); 92 | }, 93 | async setGameResume(data: { account: string }) { 94 | return await request<[]>('/game/setGameResume', data); 95 | }, 96 | async setAutoBattle(data: { 97 | account: string; 98 | autoBattle: boolean; 99 | autoRecruit: boolean; 100 | squadSelected: number; 101 | mapId: string; 102 | modelName: string; 103 | reserveAP: number; 104 | }) { 105 | return await request('/game/setAutoBattle', data); 106 | }, 107 | async setCaptchaData(data: { 108 | account: string; 109 | geetest_challenge: string; 110 | geetest_seccode: string; 111 | geetest_validate: string; 112 | }) { 113 | return await request('/game/setCaptchaData', data); 114 | }, 115 | async getSystemInfo() { 116 | return await request('/system/getSystemInfo'); 117 | }, 118 | async getAllModels() { 119 | return await request('/system/getAllModels'); 120 | } 121 | }; 122 | -------------------------------------------------------------------------------- /src/api/models.ts: -------------------------------------------------------------------------------- 1 | export interface GeneralResponse { 2 | code: number; 3 | message: string; 4 | data: T; 5 | } 6 | export interface UserInfoData { 7 | userID: number; 8 | name: string; 9 | token: string; 10 | } 11 | export interface TokenRefreshData { 12 | name: { 13 | ID: number; 14 | name: string; 15 | password: string; 16 | isactivity: number; 17 | isadmin: number; 18 | registertime: string; 19 | token: null | string; 20 | }; 21 | userID: number; 22 | token: string; 23 | } 24 | export interface SystemInfoData { 25 | launchTime: string; 26 | } 27 | export interface GameAccountData { 28 | userID: number; 29 | platform: number; 30 | account: string; 31 | } 32 | export interface GameInfoData { 33 | PlayerStatus: { 34 | LongMenBi: number; 35 | YuanShi: { 36 | Android: number; 37 | iOS: number; 38 | }; 39 | UserName: string; 40 | Level: number; 41 | Exp: number; 42 | XinYong: number; 43 | LiZhi: number; 44 | LiZhiMax: number; 45 | XunFangTicket: number; 46 | ShiLianTicket: number; 47 | GongZhaoJuan: number; 48 | LizhiHuiFu: [number, number]; 49 | TouXiang: string; 50 | YueKa: { 51 | Has: boolean; 52 | Start: number; 53 | End: number; 54 | }; 55 | Mishu: { 56 | Mishu: string; 57 | Skin: string; 58 | }; 59 | }; 60 | Inventory: { 61 | Id: string; 62 | Quantity: number; 63 | CNName: string; 64 | }[]; 65 | Squads: { 66 | [key: string]: { 67 | squadId: string; 68 | name: string; 69 | slots: ({ 70 | charInstId: number; 71 | skillIndex: number; 72 | name: string; 73 | } | null)[]; 74 | }; 75 | }; 76 | Log: { 77 | id: number; 78 | account: string; 79 | logtime: string; 80 | text: string; 81 | level: number; 82 | }[]; 83 | GameConfig: { 84 | isPause: boolean; 85 | nextAutoRunTime: string; 86 | isAutoBattle: boolean; 87 | squadSelected: number; 88 | mapId: string; 89 | modelName: string; 90 | reserveAP: number; 91 | autoRecruit: boolean; 92 | captchaData?: 93 | | { 94 | result: number; 95 | error: string; 96 | captcha: { 97 | success: number; 98 | challenge: string; 99 | gt: string; 100 | new_captcha: boolean; 101 | }; 102 | } 103 | | { 104 | captcha: undefined; 105 | requestId: string; 106 | timestamp: string; 107 | code: number; 108 | challenge: string; 109 | gt_user_id: string; 110 | gs: number; 111 | gt: string; 112 | captcha_type: number; 113 | server_message: string; 114 | }; 115 | }; 116 | } 117 | export interface GameConfigData { 118 | isPause: boolean; 119 | nextAutoRunTime: string; 120 | autoBattle: boolean; 121 | squadSelected: number; 122 | mapId: string; 123 | modelName: string; 124 | } 125 | export interface GameMapData { 126 | id: 1; 127 | mapId: string; 128 | modelName: string; 129 | forceChar: string; 130 | description: string; 131 | } 132 | -------------------------------------------------------------------------------- /src/assets/background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/closure-studio/arknights-offline-frontend/f64496847d12a83f2b3783231ae429ad3e2a6556/src/assets/background.webp -------------------------------------------------------------------------------- /src/assets/login.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/closure-studio/arknights-offline-frontend/f64496847d12a83f2b3783231ae429ad3e2a6556/src/assets/login.webp -------------------------------------------------------------------------------- /src/assets/quasar-logo-full.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 63 | 66 | 69 | 75 | 79 | 83 | 87 | 91 | 95 | 99 | 103 | 104 | 105 | 106 | 107 | 113 | 118 | 126 | 133 | 142 | 151 | 160 | 169 | 178 | 187 | 188 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /src/boot/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/closure-studio/arknights-offline-frontend/f64496847d12a83f2b3783231ae429ad3e2a6556/src/boot/.gitkeep -------------------------------------------------------------------------------- /src/boot/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from 'axios'; 2 | import { boot } from 'quasar/wrappers'; 3 | 4 | declare module 'vue/types/vue' { 5 | interface Vue { 6 | $axios: AxiosInstance; 7 | } 8 | } 9 | 10 | export default boot(({ Vue }) => { 11 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 12 | Vue.prototype.$axios = axios; 13 | }); 14 | -------------------------------------------------------------------------------- /src/boot/composition-api.ts: -------------------------------------------------------------------------------- 1 | import VueCompositionApi from '@vue/composition-api'; 2 | import { boot } from 'quasar/wrappers'; 3 | 4 | export default boot(({ Vue }) => { 5 | Vue.use(VueCompositionApi); 6 | }); 7 | -------------------------------------------------------------------------------- /src/boot/geetest.ts: -------------------------------------------------------------------------------- 1 | import geetest from '../utils/geetest'; 2 | import { boot } from 'quasar/wrappers'; 3 | 4 | interface Callback { 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | (arg0: { (): void }): void; 7 | } 8 | 9 | interface CaptchaObj { 10 | appendTo: { (position: string | HTMLElement): void }; 11 | bindForm: { (position: string): void }; 12 | getValidate: { 13 | (): 14 | | { 15 | geetest_challenge: string; 16 | geetest_validate: string; 17 | geetest_seccode: string; 18 | } 19 | | false; 20 | }; 21 | reset: VoidFunction; 22 | verify: VoidFunction; 23 | onReady: Callback; 24 | onSuccess: Callback; 25 | onError: Callback; 26 | onClose: Callback; 27 | destroy: VoidFunction; 28 | } 29 | 30 | interface GeetestInit { 31 | ( 32 | userConfig: { 33 | gt: string; 34 | challenge: string; 35 | offline: boolean; 36 | new_captcha: boolean; 37 | 38 | product?: 'float' | 'popup' | 'custom' | 'bind'; 39 | width?: string; 40 | lang?: 41 | | 'zh-cn' 42 | | 'zh-hk' 43 | | 'zh-tw' 44 | | 'en' 45 | | 'ja' 46 | | 'ko' 47 | | 'id' 48 | | 'ru' 49 | | 'ar' 50 | | 'es' 51 | | 'pt-pt' 52 | | 'fr' 53 | | 'de'; 54 | https?: boolean; 55 | timeout?: number; 56 | remUnit?: number; 57 | zoomEle?: string; 58 | hideSuccess?: boolean; 59 | hideClose?: boolean; 60 | hideRefresh?: boolean; 61 | }, 62 | callback: { (obj: CaptchaObj): void } 63 | ): ''; 64 | } 65 | 66 | declare global { 67 | interface Window { 68 | initGeetest: GeetestInit; 69 | } 70 | } 71 | 72 | declare module 'vue/types/vue' { 73 | interface Vue { 74 | $initGeetest: GeetestInit; 75 | } 76 | } 77 | 78 | export default boot(({ Vue }) => { 79 | geetest(window); 80 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 81 | Vue.prototype.$initGeetest = window.initGeetest; 82 | }); 83 | -------------------------------------------------------------------------------- /src/components/DarkModeButton.vue: -------------------------------------------------------------------------------- 1 | 10 | 43 | -------------------------------------------------------------------------------- /src/components/GameAccountForm.vue: -------------------------------------------------------------------------------- 1 | 41 | 125 | -------------------------------------------------------------------------------- /src/components/GameCaptchaDialog.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 166 | > -------------------------------------------------------------------------------- /src/components/GameConfigControls.vue: -------------------------------------------------------------------------------- 1 | 87 | 243 | -------------------------------------------------------------------------------- /src/components/GameDetailCard.vue: -------------------------------------------------------------------------------- 1 | 48 | 73 | -------------------------------------------------------------------------------- /src/components/GameLogTimeline.vue: -------------------------------------------------------------------------------- 1 | 16 | 54 | -------------------------------------------------------------------------------- /src/components/GameSquadsPanel.vue: -------------------------------------------------------------------------------- 1 | 38 | 78 | -------------------------------------------------------------------------------- /src/components/LoginForm.vue: -------------------------------------------------------------------------------- 1 | 50 | 86 | -------------------------------------------------------------------------------- /src/components/LogoutButton.vue: -------------------------------------------------------------------------------- 1 | 4 | 93 | -------------------------------------------------------------------------------- /src/components/WebSocketStatus.vue: -------------------------------------------------------------------------------- 1 | 10 | 102 | -------------------------------------------------------------------------------- /src/css/app.scss: -------------------------------------------------------------------------------- 1 | // app global css in SCSS form 2 | -------------------------------------------------------------------------------- /src/css/quasar.variables.scss: -------------------------------------------------------------------------------- 1 | // Quasar SCSS (& Sass) Variables 2 | // -------------------------------------------------- 3 | // To customize the look and feel of this app, you can override 4 | // the Sass/SCSS variables found in Quasar's source Sass/SCSS files. 5 | 6 | // Check documentation for full list of Quasar variables 7 | 8 | // Your own variables (that are declared here) and Quasar's own 9 | // ones will be available out of the box in your .vue/.scss/.sass files 10 | 11 | // It's highly recommended to change the default colors 12 | // to match your app's branding. 13 | // Tip: Use the "Theme Builder" on Quasar's documentation website. 14 | 15 | $primary : #1976D2; 16 | $secondary : #26A69A; 17 | $accent : #9C27B0; 18 | 19 | $dark : #1D1D1D; 20 | 21 | $positive : #21BA45; 22 | $negative : #C10015; 23 | $info : #31CCEC; 24 | $warning : #F2C037; 25 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | DEV: string; 4 | NODE_ENV: string; 5 | API_ADDRESS: string | undefined; 6 | STATIC_ADDRESS: string | undefined; 7 | VUE_ROUTER_MODE: 'hash' | 'history' | 'abstract' | undefined; 8 | VUE_ROUTER_BASE: string | undefined; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= productName %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /src/layouts/AltLayout.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | -------------------------------------------------------------------------------- /src/pages/Error404.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 32 | -------------------------------------------------------------------------------- /src/pages/GameView.vue: -------------------------------------------------------------------------------- 1 | 136 | -------------------------------------------------------------------------------- /src/pages/IndexView.vue: -------------------------------------------------------------------------------- 1 | 54 | -------------------------------------------------------------------------------- /src/pages/LoginView.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 58 | -------------------------------------------------------------------------------- /src/pages/SideBarView.vue: -------------------------------------------------------------------------------- 1 | 56 | 140 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { route } from 'quasar/wrappers'; 2 | import VueRouter from 'vue-router'; 3 | import { Store } from 'vuex'; 4 | import { StateInterface } from '../store'; 5 | import routes from './routes'; 6 | 7 | /* 8 | * If not building with SSR mode, you can 9 | * directly export the Router instantiation 10 | */ 11 | 12 | export default route>(function ({ Vue }) { 13 | Vue.use(VueRouter); 14 | 15 | const Router = new VueRouter({ 16 | scrollBehavior: () => ({ x: 0, y: 0 }), 17 | routes, 18 | 19 | // Leave these as is and change from quasar.conf.js instead! 20 | // quasar.conf.js -> build -> vueRouterMode 21 | // quasar.conf.js -> build -> publicPath 22 | mode: process.env.VUE_ROUTER_MODE, 23 | base: process.env.VUE_ROUTER_BASE 24 | }); 25 | 26 | return Router; 27 | }) 28 | -------------------------------------------------------------------------------- /src/router/routes.ts: -------------------------------------------------------------------------------- 1 | import { RouteConfig } from 'vue-router'; 2 | 3 | const routes: RouteConfig[] = [ 4 | { 5 | path: '/', 6 | component: () => import('layouts/AltLayout.vue'), 7 | children: [ 8 | { path: '', component: () => import('pages/IndexView.vue') }, 9 | { path: '/login', component: () => import('pages/LoginView.vue') }, 10 | { path: '/game/:account?', component: () => import('pages/GameView.vue') } 11 | ] 12 | }, 13 | 14 | // Always leave this as last one, 15 | // but you can also remove it 16 | { 17 | path: '*', 18 | component: () => import('pages/Error404.vue') 19 | } 20 | ]; 21 | 22 | export default routes; 23 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | // Mocks all files ending in `.vue` showing them as plain Vue instances 2 | declare module '*.vue' { 3 | import Vue from 'vue'; 4 | export default Vue; 5 | } 6 | -------------------------------------------------------------------------------- /src/store/activity.ts: -------------------------------------------------------------------------------- 1 | import { Module, ActionTree, GetterTree, MutationTree } from 'vuex'; 2 | import { StateInterface } from './index'; 3 | 4 | export interface ActivityStateInterface { 5 | activities: Map>; 6 | debounce: Map; 7 | websocket: boolean; 8 | listeners: Array<(account: string, message: string) => unknown>; 9 | } 10 | 11 | const state = function(): ActivityStateInterface { 12 | return { 13 | activities: new Map() as Map>, 14 | debounce: new Map() as Map, 15 | websocket: false, 16 | listeners: [] 17 | }; 18 | }; 19 | 20 | const actions: ActionTree = {}; 21 | 22 | const mutations: MutationTree = { 23 | create(state, message: { account: string; message: string }) { 24 | if (state.debounce.get(message.account) == message.message) { 25 | return; 26 | } 27 | const activity = 28 | state.activities.get(message.account) || (new Map() as Map); 29 | activity.set(new Date(), message.message); 30 | state.activities.set(message.account, activity); 31 | state.debounce.set(message.account, message.message); 32 | state.listeners.forEach(listener => 33 | listener(message.account, message.message) 34 | ); 35 | }, 36 | connect(state) { 37 | state.websocket = true; 38 | }, 39 | disconnect(state) { 40 | state.websocket = false; 41 | }, 42 | listen(state, callback: (account: string, message: string) => unknown) { 43 | state.listeners.push(callback); 44 | } 45 | }; 46 | 47 | const getters: GetterTree = {}; 48 | 49 | const module: Module = { 50 | namespaced: true, 51 | getters, 52 | actions, 53 | mutations, 54 | state 55 | }; 56 | 57 | export default module; 58 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { store } from 'quasar/wrappers'; 2 | import Vuex from 'vuex'; 3 | 4 | import login, { LoginStateInterface } from './login'; 5 | import activity, { ActivityStateInterface } from './activity'; 6 | 7 | /* 8 | * If not building with SSR mode, you can 9 | * directly export the Store instantiation 10 | */ 11 | 12 | export interface StateInterface { 13 | // Define your own store structure, using submodules if needed 14 | // example: ExampleStateInterface; 15 | // Declared as unknown to avoid linting issue. Best to strongly type as per the line above. 16 | login: LoginStateInterface; 17 | activity: ActivityStateInterface; 18 | } 19 | 20 | export default store(function({ Vue }) { 21 | Vue.use(Vuex); 22 | 23 | const Store = new Vuex.Store({ 24 | modules: { 25 | login, 26 | activity 27 | }, 28 | 29 | // enable strict mode (adds overhead!) 30 | // for dev mode only 31 | strict: !!process.env.DEBUGGING 32 | }); 33 | 34 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 35 | Vue.prototype.$store = Store; 36 | return Store; 37 | }); 38 | -------------------------------------------------------------------------------- /src/store/login.ts: -------------------------------------------------------------------------------- 1 | import { Module, ActionTree, GetterTree, MutationTree } from 'vuex'; 2 | import { LocalStorage } from 'quasar'; 3 | 4 | import api from '../api'; 5 | import { StateInterface } from './index'; 6 | 7 | export interface AccountObject { 8 | username: string; 9 | id: number; 10 | token: string; 11 | } 12 | 13 | export interface LoginStateInterface { 14 | account: AccountObject | null; 15 | listeners: Array<(account: AccountObject | null) => unknown>; 16 | } 17 | 18 | const state = function(): LoginStateInterface { 19 | return { 20 | account: LocalStorage.getItem('account')?.valueOf() as AccountObject, 21 | listeners: [] 22 | }; 23 | }; 24 | 25 | const actions: ActionTree = { 26 | async refreshToken(state) { 27 | if (!this.state.login.account?.id) { 28 | throw new Error('Failed to refresh token, no uid in local.'); 29 | } 30 | try { 31 | const result = await api.verifyToken({ 32 | uid: this.state.login.account.id 33 | }); 34 | state.commit('login', { 35 | username: result.data.name.name, 36 | id: result.data.name.ID, 37 | token: result.data.token 38 | }); 39 | } catch (err) { 40 | state.commit('logout'); 41 | throw err; 42 | } 43 | }, 44 | async loginAccount(state, account: { username: string; password: string }) { 45 | const result = await api.userLogin({ 46 | username: account.username, 47 | password: account.password 48 | }); 49 | state.commit('login', { 50 | username: result.data.name, 51 | id: result.data.userID, 52 | token: result.data.token 53 | }); 54 | } 55 | }; 56 | 57 | const mutations: MutationTree = { 58 | login(state, account: AccountObject) { 59 | LocalStorage.set('account', account); 60 | state.account = account; 61 | state.listeners.forEach(listener => listener(state.account)); 62 | }, 63 | logout(state) { 64 | state.account = null; 65 | LocalStorage.remove('account'); 66 | state.listeners.forEach(listener => listener(null)); 67 | }, 68 | listen(state, callback: (account: AccountObject | null) => unknown) { 69 | state.listeners.push(callback); 70 | } 71 | }; 72 | 73 | const getters: GetterTree = {}; 74 | 75 | const module: Module = { 76 | state, 77 | actions, 78 | mutations, 79 | getters, 80 | namespaced: true 81 | }; 82 | 83 | export default module; 84 | -------------------------------------------------------------------------------- /src/store/store-flag.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // THIS FEATURE-FLAG FILE IS AUTOGENERATED, 3 | // REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING 4 | import "quasar/dist/types/feature-flag"; 5 | 6 | declare module "quasar/dist/types/feature-flag" { 7 | interface QuasarFeatureFlags { 8 | store: true; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/geetest.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | /** 4 | * 5 | * @param {Window} window 6 | */ 7 | export default function(window) { 8 | 'use strict'; 9 | if (typeof window === 'undefined') { 10 | throw new Error('Geetest requires browser environment'); 11 | } 12 | 13 | var document = window.document; 14 | var Math = window.Math; 15 | var head = document.getElementsByTagName('head')[0]; 16 | 17 | function _Object(obj) { 18 | this._obj = obj; 19 | } 20 | 21 | _Object.prototype = { 22 | _each: function(process) { 23 | var _obj = this._obj; 24 | for (var k in _obj) { 25 | if (_obj.hasOwnProperty(k)) { 26 | process(k, _obj[k]); 27 | } 28 | } 29 | return this; 30 | } 31 | }; 32 | 33 | function Config(config) { 34 | var self = this; 35 | new _Object(config)._each(function(key, value) { 36 | self[key] = value; 37 | }); 38 | } 39 | 40 | Config.prototype = { 41 | api_server: 'api.geetest.com', 42 | protocol: 'http://', 43 | typePath: '/gettype.php', 44 | fallback_config: { 45 | slide: { 46 | static_servers: ['static.geetest.com', 'dn-staticdown.qbox.me'], 47 | type: 'slide', 48 | slide: '/static/js/geetest.0.0.0.js' 49 | }, 50 | fullpage: { 51 | static_servers: ['static.geetest.com', 'dn-staticdown.qbox.me'], 52 | type: 'fullpage', 53 | fullpage: '/static/js/fullpage.0.0.0.js' 54 | } 55 | }, 56 | _get_fallback_config: function() { 57 | var self = this; 58 | if (isString(self.type)) { 59 | return self.fallback_config[self.type]; 60 | } else if (self.new_captcha) { 61 | return self.fallback_config.fullpage; 62 | } else { 63 | return self.fallback_config.slide; 64 | } 65 | }, 66 | _extend: function(obj) { 67 | var self = this; 68 | new _Object(obj)._each(function(key, value) { 69 | self[key] = value; 70 | }); 71 | } 72 | }; 73 | var isNumber = function(value) { 74 | return typeof value === 'number'; 75 | }; 76 | var isString = function(value) { 77 | return typeof value === 'string'; 78 | }; 79 | var isBoolean = function(value) { 80 | return typeof value === 'boolean'; 81 | }; 82 | var isObject = function(value) { 83 | return typeof value === 'object' && value !== null; 84 | }; 85 | var isFunction = function(value) { 86 | return typeof value === 'function'; 87 | }; 88 | var MOBILE = /Mobi/i.test(navigator.userAgent); 89 | var pt = MOBILE ? 3 : 0; 90 | 91 | var callbacks = {}; 92 | var status = {}; 93 | 94 | var nowDate = function() { 95 | var date = new Date(); 96 | var year = date.getFullYear(); 97 | var month = date.getMonth() + 1; 98 | var day = date.getDate(); 99 | var hours = date.getHours(); 100 | var minutes = date.getMinutes(); 101 | var seconds = date.getSeconds(); 102 | 103 | if (month >= 1 && month <= 9) { 104 | month = '0' + month; 105 | } 106 | if (day >= 0 && day <= 9) { 107 | day = '0' + day; 108 | } 109 | if (hours >= 0 && hours <= 9) { 110 | hours = '0' + hours; 111 | } 112 | if (minutes >= 0 && minutes <= 9) { 113 | minutes = '0' + minutes; 114 | } 115 | if (seconds >= 0 && seconds <= 9) { 116 | seconds = '0' + seconds; 117 | } 118 | var currentdate = 119 | year + 120 | '-' + 121 | month + 122 | '-' + 123 | day + 124 | ' ' + 125 | hours + 126 | ':' + 127 | minutes + 128 | ':' + 129 | seconds; 130 | return currentdate; 131 | }; 132 | 133 | var random = function() { 134 | return parseInt(Math.random() * 10000) + new Date().valueOf(); 135 | }; 136 | 137 | var loadScript = function(url, cb) { 138 | var script = document.createElement('script'); 139 | script.charset = 'UTF-8'; 140 | script.async = true; 141 | 142 | // 对geetest的静态资源添加 crossOrigin 143 | if (/static\.geetest\.com/g.test(url)) { 144 | script.crossOrigin = 'anonymous'; 145 | } 146 | 147 | script.onerror = function() { 148 | cb(true); 149 | }; 150 | var loaded = false; 151 | script.onload = script.onreadystatechange = function() { 152 | if ( 153 | !loaded && 154 | (!script.readyState || 155 | 'loaded' === script.readyState || 156 | 'complete' === script.readyState) 157 | ) { 158 | loaded = true; 159 | setTimeout(function() { 160 | cb(false); 161 | }, 0); 162 | } 163 | }; 164 | script.src = url; 165 | head.appendChild(script); 166 | }; 167 | 168 | var normalizeDomain = function(domain) { 169 | // special domain: uems.sysu.edu.cn/jwxt/geetest/ 170 | // return domain.replace(/^https?:\/\/|\/.*$/g, ''); uems.sysu.edu.cn 171 | return domain.replace(/^https?:\/\/|\/$/g, ''); // uems.sysu.edu.cn/jwxt/geetest 172 | }; 173 | var normalizePath = function(path) { 174 | path = path.replace(/\/+/g, '/'); 175 | if (path.indexOf('/') !== 0) { 176 | path = '/' + path; 177 | } 178 | return path; 179 | }; 180 | var normalizeQuery = function(query) { 181 | if (!query) { 182 | return ''; 183 | } 184 | var q = '?'; 185 | new _Object(query)._each(function(key, value) { 186 | if (isString(value) || isNumber(value) || isBoolean(value)) { 187 | q = q + encodeURIComponent(key) + '=' + encodeURIComponent(value) + '&'; 188 | } 189 | }); 190 | if (q === '?') { 191 | q = ''; 192 | } 193 | return q.replace(/&$/, ''); 194 | }; 195 | var makeURL = function(protocol, domain, path, query) { 196 | domain = normalizeDomain(domain); 197 | 198 | var url = normalizePath(path) + normalizeQuery(query); 199 | if (domain) { 200 | url = protocol + domain + url; 201 | } 202 | 203 | return url; 204 | }; 205 | 206 | var load = function(config, send, protocol, domains, path, query, cb) { 207 | var tryRequest = function(at) { 208 | var url = makeURL(protocol, domains[at], path, query); 209 | loadScript(url, function(err) { 210 | if (err) { 211 | if (at >= domains.length - 1) { 212 | cb(true); 213 | // report gettype error 214 | if (send) { 215 | config.error_code = 508; 216 | var url = protocol + domains[at] + path; 217 | reportError(config, url); 218 | } 219 | } else { 220 | tryRequest(at + 1); 221 | } 222 | } else { 223 | cb(false); 224 | } 225 | }); 226 | }; 227 | tryRequest(0); 228 | }; 229 | 230 | var jsonp = function(domains, path, config, callback) { 231 | if (isObject(config.getLib)) { 232 | config._extend(config.getLib); 233 | callback(config); 234 | return; 235 | } 236 | if (config.offline) { 237 | callback(config._get_fallback_config()); 238 | return; 239 | } 240 | 241 | var cb = 'geetest_' + random(); 242 | window[cb] = function(data) { 243 | if (data.status == 'success') { 244 | callback(data.data); 245 | } else if (!data.status) { 246 | callback(data); 247 | } else { 248 | callback(config._get_fallback_config()); 249 | } 250 | window[cb] = undefined; 251 | try { 252 | delete window[cb]; 253 | } catch (e) {} 254 | }; 255 | load( 256 | config, 257 | true, 258 | config.protocol, 259 | domains, 260 | path, 261 | { 262 | gt: config.gt, 263 | callback: cb 264 | }, 265 | function(err) { 266 | if (err) { 267 | callback(config._get_fallback_config()); 268 | } 269 | } 270 | ); 271 | }; 272 | 273 | var reportError = function(config, url) { 274 | load( 275 | config, 276 | false, 277 | config.protocol, 278 | ['monitor.geetest.com'], 279 | '/monitor/send', 280 | { 281 | time: nowDate(), 282 | captcha_id: config.gt, 283 | challenge: config.challenge, 284 | pt: pt, 285 | exception_url: url, 286 | error_code: config.error_code 287 | }, 288 | function(err) {} 289 | ); 290 | }; 291 | 292 | var throwError = function(errorType, config) { 293 | var errors = { 294 | networkError: '网络错误', 295 | gtTypeError: 'gt字段不是字符串类型' 296 | }; 297 | if (typeof config.onError === 'function') { 298 | config.onError(errors[errorType]); 299 | } else { 300 | throw new Error(errors[errorType]); 301 | } 302 | }; 303 | 304 | var detect = function() { 305 | return window.Geetest || document.getElementById('gt_lib'); 306 | }; 307 | 308 | if (detect()) { 309 | status.slide = 'loaded'; 310 | } 311 | 312 | window.initGeetest = function(userConfig, callback) { 313 | var config = new Config(userConfig); 314 | 315 | if (userConfig.https) { 316 | config.protocol = 'https://'; 317 | } else if (!userConfig.protocol) { 318 | config.protocol = window.location.protocol + '//'; 319 | } 320 | 321 | // for KFC 322 | if ( 323 | userConfig.gt === '050cffef4ae57b5d5e529fea9540b0d1' || 324 | userConfig.gt === '3bd38408ae4af923ed36e13819b14d42' 325 | ) { 326 | config.apiserver = 'yumchina.geetest.com/'; // for old js 327 | config.api_server = 'yumchina.geetest.com'; 328 | } 329 | 330 | if (userConfig.gt) { 331 | window.GeeGT = userConfig.gt; 332 | } 333 | 334 | if (userConfig.challenge) { 335 | window.GeeChallenge = userConfig.challenge; 336 | } 337 | 338 | if (isObject(userConfig.getType)) { 339 | config._extend(userConfig.getType); 340 | } 341 | jsonp( 342 | [config.api_server || config.apiserver], 343 | config.typePath, 344 | config, 345 | function(newConfig) { 346 | var type = newConfig.type; 347 | var init = function() { 348 | config._extend(newConfig); 349 | callback(new window.Geetest(config)); 350 | }; 351 | 352 | callbacks[type] = callbacks[type] || []; 353 | var s = status[type] || 'init'; 354 | if (s === 'init') { 355 | status[type] = 'loading'; 356 | 357 | callbacks[type].push(init); 358 | 359 | load( 360 | config, 361 | true, 362 | config.protocol, 363 | newConfig.static_servers || newConfig.domains, 364 | newConfig[type] || newConfig.path, 365 | null, 366 | function(err) { 367 | if (err) { 368 | status[type] = 'fail'; 369 | throwError('networkError', config); 370 | } else { 371 | status[type] = 'loaded'; 372 | var cbs = callbacks[type]; 373 | for (var i = 0, len = cbs.length; i < len; i = i + 1) { 374 | var cb = cbs[i]; 375 | if (isFunction(cb)) { 376 | cb(); 377 | } 378 | } 379 | callbacks[type] = []; 380 | } 381 | } 382 | ); 383 | } else if (s === 'loaded') { 384 | init(); 385 | } else if (s === 'fail') { 386 | throwError('networkError', config); 387 | } else if (s === 'loading') { 388 | callbacks[type].push(init); 389 | } 390 | } 391 | ); 392 | }; 393 | } 394 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { QVueGlobals, QDialogOptions } from 'quasar'; 2 | import VueRouter from 'vue-router'; 3 | 4 | import { baseStatic } from '../api'; 5 | 6 | export default { 7 | async sleep(ms: number) { 8 | await new Promise(resolve => { 9 | setTimeout(resolve, ms); 10 | }); 11 | }, 12 | async dialog(quasar: QVueGlobals, options: QDialogOptions) { 13 | return await new Promise((resolve, reject) => { 14 | quasar 15 | .dialog(options) 16 | .onCancel(() => reject('cancel')) 17 | .onOk((data: unknown) => resolve(data)); 18 | }); 19 | }, 20 | async redirect(router: VueRouter, path: string) { 21 | if (router.currentRoute.path != path) { 22 | return await router.push(path); 23 | } 24 | }, 25 | resource(name: string, ext = 'webp'): string { 26 | return new URL(`${name}.${ext}`, baseStatic).toString(); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@quasar/app/tsconfig-preset", 3 | "compilerOptions": { 4 | "baseUrl": "." 5 | } 6 | } 7 | --------------------------------------------------------------------------------