├── .env ├── .env.production ├── .env.qa ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .stylelintignore ├── .stylelintrc.js ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── commitlint.config.js ├── favicon.ico ├── index.html ├── package.json ├── postcss.config.js ├── screenshot.png ├── src ├── assets │ ├── images │ │ └── react.png │ └── svg │ │ └── react.svg ├── components │ ├── AutoSizer.tsx │ ├── Error │ │ ├── index.module.scss │ │ ├── index.test.tsx │ │ └── index.tsx │ └── PageLoading │ │ ├── index.module.scss │ │ └── index.tsx ├── constants │ ├── index.ts │ └── socket.ts ├── containers │ ├── shared │ │ ├── App │ │ │ ├── HashRouter.tsx │ │ │ ├── IntlWrapper.tsx │ │ │ ├── Provider.tsx │ │ │ ├── ht.ts │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ └── NotAuthRouteComponent │ │ │ └── index.tsx │ └── views │ │ ├── DouyinVideo │ │ ├── index.module.scss │ │ └── index.tsx │ │ ├── Home │ │ ├── Header │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Sider │ │ │ ├── Menu.tsx │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── index.module.scss │ │ ├── index.tsx │ │ └── menu.tsx │ │ ├── Login │ │ ├── index.module.scss │ │ └── index.tsx │ │ ├── SocketDebugger │ │ ├── Browse │ │ │ ├── Message.tsx │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Handler │ │ │ ├── Connect.tsx │ │ │ ├── DataFormat.tsx │ │ │ ├── Send.tsx │ │ │ ├── Type.tsx │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── index.module.scss │ │ └── index.tsx │ │ └── Users │ │ ├── Header │ │ ├── index.module.scss │ │ └── index.tsx │ │ ├── UserModal.tsx │ │ ├── UserTable.tsx │ │ ├── index.module.scss │ │ └── index.tsx ├── env.d.ts ├── errorHandler.ts ├── index.scss ├── index.tsx ├── locales │ ├── en_US.json │ ├── loader.ts │ └── zh_CN.json ├── services │ └── websocket │ │ ├── index.ts │ │ ├── socketIO.ts │ │ └── websocket.ts ├── setupTests.ts ├── store │ ├── authStore │ │ ├── index.ts │ │ ├── syncUserInfo.ts │ │ └── type.d.ts │ ├── globalStore │ │ ├── index.ts │ │ └── type.d.ts │ ├── index.ts │ ├── socketStore │ │ ├── index.ts │ │ └── type.d.ts │ ├── useRootStore.ts │ └── userStore │ │ ├── index.ts │ │ └── type.d.ts ├── styles │ ├── _base.scss │ └── _var.scss └── utils │ ├── hooks.ts │ ├── index.ts │ └── request.ts ├── tsconfig.json ├── typings ├── assets.d.ts ├── attr.d.ts ├── global.d.ts └── store.d.ts ├── vite.config.ts └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | VITE_BASEURL=https://showcase.jackple.com/ 2 | VITE_APP_ENV=dev -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | VITE_BASEURL=https://showcase.jackple.com/ 2 | VITE_APP_ENV=prod -------------------------------------------------------------------------------- /.env.qa: -------------------------------------------------------------------------------- 1 | VITE_BASEURL=https://showcase.jackple.com/ 2 | VITE_APP_ENV=qa -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_nodules 2 | /dist 3 | /src/**/*.scss.d.ts 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Specifies the ESLint parser 3 | parser: '@typescript-eslint/parser', 4 | extends: [ 5 | // Uses the recommended rules from @eslint-plugin-react 6 | 'plugin:react/recommended', 7 | // Uses the recommended rules from @typescript-eslint/eslint-plugin 8 | 'plugin:@typescript-eslint/recommended', 9 | 'plugin:prettier/recommended' 10 | ], 11 | parserOptions: { 12 | // Allows for the parsing of modern ECMAScript features 13 | ecmaVersion: 2018, 14 | // Allows for the use of imports 15 | sourceType: 'module', 16 | ecmaFeatures: { 17 | // Allows for the parsing of JSX 18 | jsx: true 19 | } 20 | }, 21 | rules: { 22 | 'prettier/prettier': 'error', 23 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 24 | // e.g. '@typescript-eslint/explicit-function-return-type': 'off', 25 | '@typescript-eslint/explicit-function-return-type': 'off', 26 | '@typescript-eslint/explicit-module-boundary-types': 'off', 27 | 'prefer-const': 'error', 28 | 'no-var': 'error', 29 | 'comma-dangle': 'off', 30 | 'arrow-parens': 'off', 31 | 'no-multiple-empty-lines': 'error', 32 | '@typescript-eslint/no-explicit-any': 'off', 33 | '@typescript-eslint/no-var-requires': 'off', 34 | '@typescript-eslint/no-unused-vars': 'off', 35 | '@typescript-eslint/explicit-member-accessibility': 'off', 36 | '@typescript-eslint/interface-name-prefix': 'off', 37 | '@typescript-eslint/no-empty-interface': 'off', 38 | 'react/prop-types': 'off', 39 | 'react/display-name': 'off' 40 | }, 41 | settings: { 42 | react: { 43 | // Tells eslint-plugin-react to automatically detect the version of React to use 44 | version: 'detect' 45 | } 46 | }, 47 | env: { 48 | browser: true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug* 3 | /dist 4 | /package-lock.json 5 | /.cache-loader 6 | /.awcache 7 | /deploy 8 | /stats.html -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint-staged -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "src/**/*.{ts,tsx}": ["eslint --fix", "prettier --write", "git add"], 3 | "src/**/*.scss": ["stylelint --fix", "prettier --write", "git add"] 4 | } 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /src/**/*.scss.d.ts 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "arrowParens": "avoid", 6 | "semi": false, 7 | "printWidth": 120, 8 | "endOfLine": "auto" 9 | } 10 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /**/*.md 4 | /**/*.ts 5 | /**/*.tsx 6 | /**/*.js -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | module.exports = { 3 | customSyntax: 'postcss-scss', 4 | extends: ['prettier-stylelint/config.js', 'stylelint-prettier/recommended'], 5 | plugins: ['stylelint-order'], 6 | rules: { 7 | 'prettier/prettier': true, 8 | 'selector-pseudo-class-no-unknown': [true, { ignorePseudoClasses: ['global', 'local', 'export'] }], 9 | 'property-no-unknown': [true, { ignoreProperties: ['composes', '/^var/'] }], 10 | 'order/order': ['custom-properties', 'declarations'], 11 | 'order/properties-alphabetical-order': true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "typescript.tsdk": "node_modules/typescript/lib", 4 | "scss.lint.validProperties": ["composes"], 5 | "jest.autoRun": "off", 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.stylelint": true, 8 | "source.fixAll.eslint": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 shanghaitao 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | There is an example [use-webpack](https://github.com/YDJ-FE/ts-react-vite_or_webpack/tree/use-webpack) 2 | 3 | This is a simple (admin) starter with typescript, react and vite. 4 | 5 | Have a quick view: 6 | 7 | 8 | 9 | ## setup 10 | 11 | > for husky 12 | 13 | ```bash 14 | $ npm run prepare 15 | ``` 16 | 17 | > If you do not need the taobao registry, you can change it in `.npmrc` 18 | 19 | ```bash 20 | $ npm i 21 | ``` 22 | 23 | ## test 24 | 25 | ```bash 26 | $ npm test 27 | ``` 28 | 29 | ## build for development 30 | 31 | ```bash 32 | $ npm run dev 33 | ``` 34 | 35 | ## build for production 36 | 37 | ```bash 38 | $ npm run build:(qa/prod) 39 | ``` 40 | 41 | ## characteristics 42 | 43 | - use [ant design](https://ant.design/index-cn) as UI framework 44 | - use ServiceWorker 45 | - use husky{pre-commit/commit-msg} hooks 46 | - use [react-intl-universal](https://github.com/alibaba/react-intl-universal) for i18n. 47 | - use [react-virtualized](https://github.com/bvaughn/react-virtualized) for fat list. 48 | 49 | ## pages 50 | 51 | - The Index page became a [Socket Debugger](https://starter.jackple.com/#/) 52 | 53 | ## TODO 54 | 55 | - config menu by user with permission 56 | - more functional pages like Socket Debugger 57 | 58 | ## component example 59 | 60 | ```jsx 61 | import React from 'react' 62 | import { observer } from 'mobx-react' 63 | import { Button } from 'antd' 64 | 65 | import history from '@shared/App/ht' 66 | 67 | function Test() { 68 | function gotoHome() { 69 | history.push('/') 70 | } 71 | return ( 72 | 75 | ) 76 | } 77 | 78 | export default observer(Test) 79 | ``` 80 | 81 | [live example](https://github.com/YDJ-FE/ts-react-webpack4/blob/master/src/containers/views/Login/index.tsx?1532570619900) 82 | 83 | ## necessary extensions (on vscode) 84 | 85 | - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) 86 | 87 | - [stylelint](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint) 88 | 89 | - [Prettier - Code formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 90 | 91 | ## how to upload file to server 92 | 93 | ```bash 94 | #!/bin/bash 95 | 96 | TIMESPAN=$(date '+%s') 97 | DEPLOYNAME=ts-react.qa.${TIMESPAN} 98 | DEPLOYFILES=${DEPLOYNAME}.tar.gz 99 | SERVER=0.0.0.0 100 | 101 | # make compression 102 | cd dist/qa 103 | tar -zcvf ${DEPLOYFILES} ./* 104 | 105 | # upload 106 | scp -P 22 -o StrictHostKeyChecking=no ${DEPLOYFILES} node@${SERVER}:/home/pages/ts-react/tarfiles 107 | 108 | # make decompression 109 | ssh -p 22 -o StrictHostKeyChecking=no node@${SERVER} tar xzf /home/pages/ts-react/tarfiles/${DEPLOYFILES} -C /home/pages/ts-react 110 | 111 | if [ $? -ne 0 ]; then 112 | echo "success" 113 | else 114 | echo "fail" 115 | fi 116 | ``` 117 | 118 | ## how to deploy with nginx 119 | 120 | ```nginx 121 | server { 122 | listen 9993; 123 | server_name localhost:9993; 124 | 125 | location / { 126 | root ~/Documents/react/ts-react/dist/qa/; 127 | index index.html; 128 | } 129 | } 130 | ``` 131 | 132 | ## the scaffold 133 | 134 | [steamer-react-redux-ts](https://github.com/YDJ-FE/steamer-react-redux-ts) 135 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] } 2 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YDJ-FE/ts-react-vite_or_webpack/e150d4d1b39527c8c6532171504da60ca0020705/favicon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | oosh! 7 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-react-vite", 3 | "version": "1.0.0", 4 | "description": "an admin starter-template with typescript, react, mobx and vite...", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "prepare": "husky install", 9 | "lint-staged": "lint-staged", 10 | "lint": "eslint src --ext .ts,.tsx && stylelint \"./src/**/*.scss\"", 11 | "dev": "vite --port 8080", 12 | "build:qa": "vite build --mode qa", 13 | "preview:qa": "yarn build:qa && vite preview --port 8081", 14 | "build:prod": "vite build", 15 | "preview:prod": "vite build && vite preview --port 8081" 16 | }, 17 | "keywords": [ 18 | "vite", 19 | "typescript", 20 | "admin", 21 | "react", 22 | "mobx", 23 | "starter-template" 24 | ], 25 | "author": "jackple", 26 | "license": "MIT", 27 | "dependencies": { 28 | "antd": "4.21.2", 29 | "axios": "0.27.2", 30 | "bourbon": "7.2.0", 31 | "eventemitter3": "4.0.7", 32 | "lodash-es": "^4.17.21", 33 | "mobx": "6.6.0", 34 | "mobx-react": "7.5.0", 35 | "react": "18.2.0", 36 | "react-dom": "18.2.0", 37 | "react-intl-universal": "2.5.3", 38 | "react-json-view": "1.21.3", 39 | "react-router-dom": "6.3.0", 40 | "react-virtualized": "9.22.3", 41 | "socket.io-client": "4.5.1", 42 | "socketio-wildcard": "2.0.0" 43 | }, 44 | "devDependencies": { 45 | "@commitlint/cli": "17.0.2", 46 | "@commitlint/config-conventional": "17.0.2", 47 | "@types/classnames": "2.3.1", 48 | "@types/enzyme": "3.10.12", 49 | "@types/enzyme-adapter-react-16": "^1.0.5", 50 | "@types/jest": "28.1.1", 51 | "@types/lodash-es": "4.17.6", 52 | "@types/node": "17.0.43", 53 | "@types/qs": "6.9.7", 54 | "@types/react": "18.0.12", 55 | "@types/react-dom": "18.0.5", 56 | "@types/react-router-dom": "5.3.3", 57 | "@types/react-virtualized": "9.21.21", 58 | "@types/socket.io-client": "3.0.0", 59 | "@typescript-eslint/eslint-plugin": "5.28.0", 60 | "@typescript-eslint/parser": "5.28.0", 61 | "@vitejs/plugin-legacy": "1.8.2", 62 | "@vitejs/plugin-react": "1.3.2", 63 | "autoprefixer": "10.4.7", 64 | "classnames": "2.3.1", 65 | "enzyme": "^3.10.0", 66 | "enzyme-adapter-react-16": "1.15.6", 67 | "enzyme-to-json": "3.6.2", 68 | "eslint": "8.17.0", 69 | "eslint-config-prettier": "8.5.0", 70 | "eslint-plugin-prettier": "4.0.0", 71 | "eslint-plugin-react": "7.30.0", 72 | "husky": "8.0.1", 73 | "identity-obj-proxy": "^3.0.0", 74 | "jest": "28.1.1", 75 | "less": "4.1.3", 76 | "lint-staged": "13.0.1", 77 | "postcss": "8.4.14", 78 | "prettier": "2.7.0", 79 | "prettier-stylelint": "^0.4.2", 80 | "rollup-plugin-visualizer": "5.6.0", 81 | "sass": "1.52.3", 82 | "stylelint": "14.9.1", 83 | "stylelint-config-prettier": "9.0.3", 84 | "stylelint-order": "5.0.0", 85 | "stylelint-prettier": "2.0.0", 86 | "ts-jest": "28.0.5", 87 | "typescript": "4.7.3", 88 | "vite": "2.9.12", 89 | "vite-plugin-pwa": "0.12.0", 90 | "vite-tsconfig-paths": "3.5.0" 91 | }, 92 | "browserslist": [ 93 | "> 1%", 94 | "last 2 versions", 95 | "not ie <= 11" 96 | ], 97 | "jest": { 98 | "moduleFileExtensions": [ 99 | "ts", 100 | "tsx", 101 | "js" 102 | ], 103 | "transform": { 104 | "^.+\\.tsx?$": "ts-jest" 105 | }, 106 | "setupFiles": [ 107 | "raf/polyfill" 108 | ], 109 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", 110 | "setupFilesAfterEnv": [ 111 | "src/setupTests.ts" 112 | ], 113 | "snapshotSerializers": [ 114 | "enzyme-to-json" 115 | ], 116 | "moduleNameMapper": { 117 | "\\.(css|less|scss|svg|jpg|jpeg|png|gif)$": "identity-obj-proxy", 118 | "^@constants/(.*)$": "/src/constants//$1", 119 | "^@services/(.*)$": "/src/services//$1", 120 | "^@utils/(.*)$": "/src/utils//$1", 121 | "^@assets/(.*)$": "/src/assets//$1", 122 | "^@components/(.*)$": "/src/components//$1", 123 | "^@views/(.*)$": "/src/containers/views//$1", 124 | "^@shared/(.*)$": "/src/containers/shared//$1" 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('autoprefixer')] 3 | } 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YDJ-FE/ts-react-vite_or_webpack/e150d4d1b39527c8c6532171504da60ca0020705/screenshot.png -------------------------------------------------------------------------------- /src/assets/images/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YDJ-FE/ts-react-vite_or_webpack/e150d4d1b39527c8c6532171504da60ca0020705/src/assets/images/react.png -------------------------------------------------------------------------------- /src/assets/svg/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/AutoSizer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface Size { 4 | width: number 5 | height: number 6 | } 7 | 8 | interface IP { 9 | className?: string 10 | style?: React.CSSProperties 11 | children?: (props: Size) => React.ReactNode 12 | } 13 | 14 | interface IS { 15 | height: number 16 | width: number 17 | } 18 | 19 | class AutoSizer extends React.Component { 20 | state = { height: 0, width: 0 } 21 | 22 | private containerRef: HTMLDivElement = null 23 | private timer: NodeJS.Timer = null 24 | 25 | listenResize = () => { 26 | clearTimeout(this.timer) 27 | this.timer = setTimeout(() => { 28 | this.setSize() 29 | }, 100) 30 | } 31 | 32 | setSize = () => { 33 | if (this.containerRef) { 34 | const { clientHeight, clientWidth } = this.containerRef 35 | this.setState({ height: clientHeight, width: clientWidth }) 36 | } 37 | } 38 | 39 | setRef = (ref: HTMLDivElement) => { 40 | this.containerRef = ref 41 | this.setSize() 42 | } 43 | 44 | bindOrUnbindResize = (type: 'bind' | 'unbind') => { 45 | const listener = type === 'bind' ? window.addEventListener : window.removeEventListener 46 | listener('resize', this.listenResize, false) 47 | } 48 | 49 | componentDidMount() { 50 | this.bindOrUnbindResize('bind') 51 | } 52 | 53 | componentWillUnmount() { 54 | this.bindOrUnbindResize('unbind') 55 | } 56 | 57 | render() { 58 | const { className, style, children } = this.props 59 | const { width, height } = this.state 60 | return ( 61 |
62 | {children({ width, height })} 63 |
64 | ) 65 | } 66 | } 67 | 68 | export default AutoSizer 69 | -------------------------------------------------------------------------------- /src/components/Error/index.module.scss: -------------------------------------------------------------------------------- 1 | .centered { 2 | flex-direction: column; 3 | text-align: center; 4 | } 5 | 6 | .emoji { 7 | font-size: 9em; 8 | user-select: none; 9 | } 10 | 11 | .title { 12 | color: grey; 13 | font-size: 3em; 14 | text-align: center; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Error/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import enzyme from 'enzyme' 3 | 4 | import styles from './index.module.scss' 5 | import Error from './' 6 | 7 | it('renders the correct text', () => { 8 | const component = enzyme.shallow() 9 | const avatars = component.find(`.${styles.title}`) 10 | expect(avatars.text()).toEqual('Ooooops!') 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/Error/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import styles from './index.module.scss' 4 | 5 | const Error = () => ( 6 |
7 |
😭
8 |

Ooooops!

9 |

This page does not exist anymore.

10 |
11 | ) 12 | 13 | export default Error 14 | -------------------------------------------------------------------------------- /src/components/PageLoading/index.module.scss: -------------------------------------------------------------------------------- 1 | .pageLoading { 2 | align-content: center; 3 | display: flex; 4 | flex: 1; 5 | flex-direction: column; 6 | justify-content: center; 7 | } 8 | 9 | .spin { 10 | display: initial; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/PageLoading/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Spin } from 'antd' 3 | 4 | import styles from './index.module.scss' 5 | 6 | function PageLoading() { 7 | return ( 8 |
9 | 10 |
11 | ) 12 | } 13 | 14 | export default PageLoading 15 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export enum COOKIE_KEYS { 2 | LANG = 'lang' 3 | } 4 | 5 | export enum LOCALSTORAGE_KEYS { 6 | USERINFO = 'userInfo', 7 | NAV_OPEN_KEYS = 'navOpenKeys', 8 | SIDE_BAR_THEME = 'sideBarTheme', 9 | SIDE_BAR_COLLAPSED = 'sideBarCollapsed', 10 | // about socket 11 | SOCKET_URL = 'socketUrl', 12 | SOCKET_TYPE = 'socketType', 13 | SOCKET_IO_EVENTS = '_socketIOEvents', 14 | DATA_FORMAT = 'dataFormat', 15 | NOT_SUPPORT_POLLING = 'notSupportPolling' 16 | } 17 | 18 | export const LOGIN_CATEGORY = ['user', 'admin'] 19 | 20 | export const GITHUB_LINK = 'https://github.com/YDJ-FE' 21 | -------------------------------------------------------------------------------- /src/constants/socket.ts: -------------------------------------------------------------------------------- 1 | export enum SOCKET_TYPE { 2 | SOCKETIO = 'socket.io', 3 | WEBSOCKET = 'websocket' 4 | } 5 | export const SOCKER_TYPES: SOCKET_TYPE[] = [SOCKET_TYPE.SOCKETIO, SOCKET_TYPE.WEBSOCKET] 6 | 7 | export enum DATA_FORMAT_TYPE { 8 | JSON = 'json', 9 | TEXT = 'text' 10 | } 11 | export const DATA_FORMATS: DATA_FORMAT_TYPE[] = [DATA_FORMAT_TYPE.JSON, DATA_FORMAT_TYPE.TEXT] 12 | -------------------------------------------------------------------------------- /src/containers/shared/App/HashRouter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { History } from 'history' 3 | import { HashRouterProps as NativeHashRouterProps, Router } from 'react-router-dom' 4 | 5 | export interface HashRouterProps extends Omit { 6 | history: History 7 | } 8 | 9 | const HashRouter: React.FC = React.memo(props => { 10 | const { history, ...restProps } = props 11 | const [state, setState] = React.useState({ 12 | action: history.action, 13 | location: history.location 14 | }) 15 | 16 | React.useLayoutEffect(() => history.listen(setState), [history]) 17 | 18 | return 19 | }) 20 | 21 | export default HashRouter 22 | -------------------------------------------------------------------------------- /src/containers/shared/App/IntlWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import intl from 'react-intl-universal' 3 | import { find } from 'lodash-es' 4 | import { Select, ConfigProvider } from 'antd' 5 | import { Locale } from 'antd/lib/locale-provider' 6 | 7 | import styles from './index.module.scss' 8 | import { useOnMount } from '@utils/hooks' 9 | import { setCookie } from '@utils/index' 10 | import { COOKIE_KEYS } from '@constants/index' 11 | import PageLoading from '@components/PageLoading' 12 | import { SUPPOER_LOCALES, LOCALES_KEYS, getLocaleLoader } from '@locales/loader' 13 | 14 | const IntlWrapper: React.ReactFCWithChildren = ({ children }) => { 15 | const [currentLocale, setCurrentLocale] = useState('') 16 | const [antdLocaleData, setAntdLocaleData] = useState(null) 17 | 18 | function loadLocales() { 19 | let targetLocale = intl.determineLocale({ cookieLocaleKey: COOKIE_KEYS.LANG }) as LOCALES_KEYS 20 | // default is English 21 | if (!find(SUPPOER_LOCALES, { value: targetLocale })) { 22 | targetLocale = LOCALES_KEYS.EN_US 23 | } 24 | getLocaleLoader(targetLocale).then(res => { 25 | intl.init({ currentLocale: targetLocale, locales: { [targetLocale]: res.localeData } }).then(() => { 26 | setCurrentLocale(targetLocale) 27 | setAntdLocaleData(res.antdLocaleData) 28 | }) 29 | }) 30 | } 31 | 32 | function onSelectLocale(val: string) { 33 | setCookie(COOKIE_KEYS.LANG, val) 34 | location.reload() 35 | } 36 | 37 | useOnMount(loadLocales) 38 | 39 | if (!currentLocale) { 40 | return 41 | } 42 | const selectLanguage = ( 43 | 50 | ) 51 | return ( 52 | 53 | 54 | {selectLanguage} 55 | {children} 56 | 57 | 58 | ) 59 | } 60 | 61 | export default IntlWrapper 62 | -------------------------------------------------------------------------------- /src/containers/shared/App/Provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, ReactElement } from 'react' 2 | import { Observer } from 'mobx-react' 3 | 4 | import * as store from '@store/index' 5 | 6 | interface ChildrenProps { 7 | children: (value: T) => ReactElement 8 | } 9 | 10 | export const RootContext = createContext(null) 11 | 12 | /** 13 | * 已包含Observer 14 | * @param param0 15 | */ 16 | export const RootConsumer = ({ children }: ChildrenProps) => {() => children(store)} 17 | 18 | const Provider: React.ReactFCWithChildren = ({ children }) => { 19 | return {children} 20 | } 21 | 22 | export default Provider 23 | -------------------------------------------------------------------------------- /src/containers/shared/App/ht.ts: -------------------------------------------------------------------------------- 1 | import { createHashHistory } from 'history' 2 | 3 | const history = createHashHistory() 4 | 5 | export default history 6 | -------------------------------------------------------------------------------- /src/containers/shared/App/index.module.scss: -------------------------------------------------------------------------------- 1 | .appWrapper { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100%; 5 | margin: 0 auto; 6 | min-height: 100%; 7 | overflow: auto; 8 | width: 100%; 9 | } 10 | 11 | .intlSelect:global.ant-select { 12 | position: absolute; 13 | right: 15px; 14 | top: 15px; 15 | width: 100px; 16 | } 17 | -------------------------------------------------------------------------------- /src/containers/shared/App/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense, lazy } from 'react' 2 | import { Routes, Route } from 'react-router-dom' 3 | 4 | import styles from './index.module.scss' 5 | import PageLoading from '@components/PageLoading' 6 | import Provider from './Provider' 7 | import IntlWrapper from './IntlWrapper' 8 | import HashRouter from './HashRouter' 9 | import history from './ht' 10 | 11 | const Home = lazy(() => import('@views/Home')) 12 | const Login = lazy(() => import('@views/Login')) 13 | 14 | const AppWrapper: React.ReactFCWithChildren = ({ children }) =>
{children}
15 | 16 | function App() { 17 | return ( 18 | 19 | 20 | 21 | 22 | }> 23 | 24 | } /> 25 | } /> 26 | 27 | 28 | 29 | 30 | 31 | 32 | ) 33 | } 34 | 35 | export default App 36 | -------------------------------------------------------------------------------- /src/containers/shared/NotAuthRouteComponent/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class NotAuthRouteComponent extends React.Component { 4 | render() { 5 | return

403

6 | } 7 | } 8 | 9 | export default NotAuthRouteComponent 10 | -------------------------------------------------------------------------------- /src/containers/views/DouyinVideo/index.module.scss: -------------------------------------------------------------------------------- 1 | .douyinVideo { 2 | align-items: center; 3 | display: flex; 4 | flex: 1; 5 | flex-direction: column; 6 | justify-content: center; 7 | } 8 | 9 | .container { 10 | box-shadow: 0 0 100px rgba(0, 0, 0, 0.08); 11 | padding: 20px 36px 36px; 12 | width: 320px; 13 | } 14 | 15 | .link { 16 | margin-top: 30px; 17 | @include ellipsis; 18 | } 19 | 20 | .tips { 21 | color: #b5b0b0; 22 | font-size: 12px; 23 | line-height: 1.5; 24 | margin-top: 10px; 25 | padding-left: 10px; 26 | } 27 | -------------------------------------------------------------------------------- /src/containers/views/DouyinVideo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Input, Button, message } from 'antd' 3 | import axios from 'axios' 4 | 5 | import styles from './index.module.scss' 6 | 7 | function DouyinVideo() { 8 | const [loading, setLoading] = React.useState(false) 9 | const [url, setUrl] = React.useState('') 10 | const [targetUrl, setTargetUrl] = React.useState('') 11 | 12 | async function submit() { 13 | setLoading(true) 14 | try { 15 | const { data } = await axios.get('https://jackple.com/', { params: { url } }) 16 | if (data.startsWith('http')) { 17 | return setTargetUrl(data) 18 | } 19 | throw new Error() 20 | } catch (err) { 21 | message.error('sth error, please check input') 22 | } finally { 23 | setLoading(false) 24 | } 25 | } 26 | 27 | return ( 28 |
29 |
30 |

获取抖音无水印视频

31 | setUrl(e.target.value)} 35 | onPressEnter={submit} 36 | /> 37 |
38 | 仅限以下域名的链接:
39 | iesdouyin.com
40 | douyin.com 41 |
42 | 45 | 46 | {targetUrl} 47 | 48 |
49 |
50 | ) 51 | } 52 | 53 | export default DouyinVideo 54 | -------------------------------------------------------------------------------- /src/containers/views/Home/Header/index.module.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | align-items: center; 3 | background-color: white; 4 | border-left: 1px solid $border-light-color; 5 | display: flex; 6 | justify-content: space-between; 7 | padding: 0 135px 0 0; 8 | } 9 | 10 | .trigger { 11 | cursor: pointer; 12 | font-size: 18px; 13 | line-height: 64px; 14 | padding: 0 24px; 15 | transition: color 0.3s; 16 | 17 | &:hover { 18 | color: $primary-color; 19 | } 20 | } 21 | 22 | .right { 23 | align-items: center; 24 | display: flex; 25 | } 26 | 27 | .rightIcon { 28 | cursor: pointer; 29 | font-size: 24px; 30 | margin-left: 20px; 31 | } 32 | -------------------------------------------------------------------------------- /src/containers/views/Home/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { observer } from 'mobx-react' 3 | import { Layout } from 'antd' 4 | import { MenuUnfoldOutlined, MenuFoldOutlined, GithubOutlined, LogoutOutlined } from '@ant-design/icons' 5 | 6 | import styles from './index.module.scss' 7 | import useRootStore from '@store/useRootStore' 8 | import { GITHUB_LINK } from '@constants/index' 9 | 10 | function Header() { 11 | const { globalStore, authStore } = useRootStore() 12 | const IconMenuFold = globalStore.sideBarCollapsed ? MenuUnfoldOutlined : MenuFoldOutlined 13 | return ( 14 | 15 | 16 |
17 | window.open(GITHUB_LINK)} /> 18 | 19 |
20 |
21 | ) 22 | } 23 | 24 | export default observer(Header) 25 | -------------------------------------------------------------------------------- /src/containers/views/Home/Sider/Menu.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo } from 'react' 2 | import { observer } from 'mobx-react' 3 | import { Menu, message } from 'antd' 4 | import { MenuInfo } from 'rc-menu/lib/interface' 5 | import { useNavigate, useLocation } from 'react-router-dom' 6 | 7 | import styles from './index.module.scss' 8 | import menus, { menusWithRoute, routeMap, routeKeys, RouteMapValue } from './../menu' 9 | import useRootStore from '@store/useRootStore' 10 | 11 | function SiderMenu() { 12 | const navigate = useNavigate() 13 | const location = useLocation() 14 | 15 | const { 16 | globalStore: { sideBarTheme, navOpenKeys, setOpenKeys } 17 | } = useRootStore() 18 | 19 | const selectedKeys = useMemo(() => { 20 | for (const routeKey of routeKeys) { 21 | const item = routeMap[routeKey] 22 | if (item.path === location.pathname) { 23 | return [routeKey] 24 | } 25 | } 26 | return [] 27 | }, [location.pathname]) 28 | 29 | const goto = useCallback( 30 | (info: MenuInfo) => { 31 | const selectedMenu = menusWithRoute.find(item => item.key === info.key) 32 | if (!selectedMenu) { 33 | message.error('菜单不能匹配到路由, 请检查') 34 | } 35 | const item = routeMap[selectedMenu.key] as RouteMapValue 36 | if (item && item.path !== location.pathname) { 37 | navigate(item.path) 38 | } 39 | }, 40 | [location.pathname] 41 | ) 42 | 43 | return ( 44 | 54 | ) 55 | } 56 | 57 | export default observer(SiderMenu) 58 | -------------------------------------------------------------------------------- /src/containers/views/Home/Sider/index.module.scss: -------------------------------------------------------------------------------- 1 | .sider { 2 | :global(.ant-layout-sider-children) { 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | } 7 | 8 | .logoBox { 9 | font-size: 50px; 10 | padding: 10px 0; 11 | text-align: center; 12 | 13 | &.dark { 14 | color: rgba(white, 0.65); 15 | } 16 | } 17 | 18 | .menu { 19 | flex: 1; 20 | } 21 | 22 | .changeTheme { 23 | align-items: center; 24 | border-top: 1px solid $border-light-color; 25 | display: flex; 26 | font-size: $font-size-sm; 27 | justify-content: space-around; 28 | padding: 12px 10px; 29 | transition: all 0.3s; 30 | 31 | &.dark { 32 | background-color: $dark-color; 33 | border-color: $dark-color; 34 | color: rgba(white, 0.65); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/containers/views/Home/Sider/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classnames from 'classnames' 3 | import { observer } from 'mobx-react' 4 | import { Layout, Switch } from 'antd' 5 | import { AntDesignOutlined } from '@ant-design/icons' 6 | 7 | import styles from './index.module.scss' 8 | import useRootStore from '@store/useRootStore' 9 | import SiderMenu from './Menu' 10 | 11 | function Sider() { 12 | const { sideBarCollapsed, sideBarTheme, changeSiderTheme } = useRootStore().globalStore 13 | 14 | const ChangeTheme = ( 15 |
16 | Switch Theme 17 | changeSiderTheme(val ? 'dark' : 'light')} 22 | /> 23 |
24 | ) 25 | return ( 26 | 33 |
34 | 35 |
36 | 37 | {!sideBarCollapsed && ChangeTheme} 38 |
39 | ) 40 | } 41 | 42 | export default observer(Sider) 43 | -------------------------------------------------------------------------------- /src/containers/views/Home/index.module.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | align-items: center; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: center; 6 | padding: 15px; 7 | 8 | & > * { 9 | background-color: white; 10 | display: flex; 11 | flex: 1; 12 | overflow-y: auto; 13 | padding: 15px; 14 | width: 100%; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/containers/views/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react' 2 | import { Layout } from 'antd' 3 | import { Routes, Route } from 'react-router-dom' 4 | 5 | import styles from './index.module.scss' 6 | import Error from '@components/Error' 7 | import PageLoading from '@components/PageLoading' 8 | import { routeMap, menusWithRoute, RouteMapValue } from './menu' 9 | import Header from './Header' 10 | import Sider from './Sider' 11 | 12 | function Home() { 13 | return ( 14 | 15 | 16 | 17 |
18 | 19 | }> 20 | 21 | {menusWithRoute.map(m => { 22 | const item = routeMap[m.key] as RouteMapValue 23 | const Component = item.component 24 | return } /> 25 | })} 26 | } /> 27 | 28 | 29 | 30 | 31 | 32 | ) 33 | } 34 | 35 | export default Home 36 | -------------------------------------------------------------------------------- /src/containers/views/Home/menu.tsx: -------------------------------------------------------------------------------- 1 | import React, { lazy } from 'react' 2 | import { CoffeeOutlined, UserOutlined, VideoCameraOutlined } from '@ant-design/icons' 3 | import { ItemType } from 'antd/lib/menu/hooks/useItems' 4 | 5 | export type RouteMapValue = { 6 | component: React.LazyExoticComponent<() => JSX.Element> 7 | path: string 8 | } 9 | 10 | export const routeMap = { 11 | SocketDebugger: { 12 | component: lazy(() => import('@views/SocketDebugger')), 13 | path: '/' 14 | }, 15 | Users: { 16 | component: lazy(() => import('@views/Users')), 17 | path: '/users' 18 | }, 19 | DouyinVideo: { 20 | component: lazy(() => import('@views/DouyinVideo')), 21 | path: '/dy-v' 22 | } 23 | } 24 | 25 | /** 26 | * menu 27 | * PS: 有路由时, key必须与routeMap对应, 否则不能匹配跳转 28 | */ 29 | const menus: ItemType[] = [ 30 | { key: 'SocketDebugger', label: 'SocketDebugger', icon: }, 31 | { key: 'DouyinVideo', label: 'dy', icon: }, 32 | { key: 'Users', label: 'Users', icon: } 33 | ] 34 | 35 | export default menus 36 | 37 | /** 38 | * all routers key 39 | */ 40 | export type RouteMapKey = keyof typeof routeMap 41 | 42 | /** 43 | * all routers key 44 | */ 45 | export const routeKeys = Object.keys(routeMap) as RouteMapKey[] 46 | 47 | /** 48 | * 带跳转地址的menu 49 | */ 50 | export const menusWithRoute: ItemType[] = [] 51 | // 递归寻找 52 | function match(items: ItemType[]) { 53 | items.forEach(function (item) { 54 | if (routeKeys.includes(item.key as RouteMapKey)) { 55 | menusWithRoute.push(item) 56 | } 57 | const children = (item as any).children as ItemType[] 58 | if (Array.isArray(children)) { 59 | match(children) 60 | } 61 | }) 62 | } 63 | match(menus) 64 | -------------------------------------------------------------------------------- /src/containers/views/Login/index.module.scss: -------------------------------------------------------------------------------- 1 | :export { 2 | varDarkColor: $dark-color; 3 | } 4 | 5 | .login { 6 | align-items: center; 7 | display: flex; 8 | flex: 1; 9 | flex-direction: column; 10 | justify-content: center; 11 | } 12 | 13 | .form { 14 | box-shadow: 0 0 100px rgba(0, 0, 0, 0.08); 15 | padding: 36px 36px 12px; 16 | width: 320px; 17 | } 18 | 19 | .logoBox { 20 | font-size: 55px; 21 | margin-bottom: 15px; 22 | text-align: center; 23 | } 24 | 25 | .tips { 26 | color: $gray-color; 27 | display: flex; 28 | font-size: $font-size-sm; 29 | justify-content: space-around; 30 | margin: -10px 0 10px 0; 31 | } 32 | -------------------------------------------------------------------------------- /src/containers/views/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { observer } from 'mobx-react' 3 | import { Form, Input, Button } from 'antd' 4 | import intl from 'react-intl-universal' 5 | import { UserOutlined, LockOutlined, AntDesignOutlined } from '@ant-design/icons' 6 | 7 | import styles from './index.module.scss' 8 | import useRootStore from '@store/useRootStore' 9 | 10 | const FormItem = Form.Item 11 | 12 | function Login() { 13 | const { authStore } = useRootStore() 14 | 15 | const [loading, setLoading] = React.useState(false) 16 | 17 | async function submit(values: IAuthStore.LoginParams) { 18 | setLoading(true) 19 | try { 20 | await authStore.login(values) 21 | } finally { 22 | setLoading(false) 23 | } 24 | } 25 | 26 | return ( 27 |
28 |
29 |
30 | 31 |
32 | 33 | } placeholder="account" /> 34 | 35 | 36 | } 38 | type="password" 39 | placeholder="password" 40 | /> 41 | 42 | 43 |
44 | {intl.get('USERNAME')}: admin 45 | {intl.get('PASSWORD')}: admin 46 |
47 | 50 |
51 |
52 |
53 | ) 54 | } 55 | 56 | export default observer(Login) 57 | -------------------------------------------------------------------------------- /src/containers/views/SocketDebugger/Browse/Message.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { observer } from 'mobx-react' 3 | import moment from 'moment' 4 | import { Tag } from 'antd' 5 | 6 | import styles from './index.module.scss' 7 | 8 | interface IProps { 9 | message: ISocketStore.Message 10 | style: React.CSSProperties 11 | } 12 | 13 | function Message({ message, style }: IProps) { 14 | const time = moment(message.time).format('h:mm:ss a') 15 | const color = message.from === 'browser' ? '#87d068' : message.from === 'server' ? '#2db7f5' : '#108ee9' 16 | const fromText = message.from === 'browser' ? 'You' : message.from === 'server' ? 'Server' : 'Console' 17 | const content = typeof message.data === 'object' ? JSON.stringify(message.data) : message.data 18 | 19 | return ( 20 |
21 |
22 | {message.event && {message.event}} 23 | {fromText} 24 | {time} 25 |
26 |
{content}
27 |
28 | ) 29 | } 30 | 31 | export default observer(Message) 32 | -------------------------------------------------------------------------------- /src/containers/views/SocketDebugger/Browse/index.module.scss: -------------------------------------------------------------------------------- 1 | .browse { 2 | background-color: white; 3 | border-radius: 5px; 4 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12), 0 0 6px 0 rgba(0, 0, 0, 0.04); 5 | flex: 1; 6 | margin-left: 20px; 7 | } 8 | 9 | .message { 10 | margin: 10px 0; 11 | padding: 0 15px; 12 | } 13 | 14 | .messageHeader { 15 | align-items: center; 16 | display: flex; 17 | } 18 | 19 | .content { 20 | border-bottom: 1px dotted #cac5c5; 21 | margin-bottom: 10px; 22 | padding-bottom: 10px; 23 | word-break: break-all; 24 | word-wrap: break-word; 25 | } 26 | -------------------------------------------------------------------------------- /src/containers/views/SocketDebugger/Browse/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { reaction } from 'mobx' 3 | import { observer } from 'mobx-react' 4 | import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' 5 | import { CellMeasurerCache, CellMeasurer } from 'react-virtualized/dist/es/CellMeasurer' 6 | import { List as VList, ListRowProps } from 'react-virtualized/dist/es/List' 7 | 8 | import styles from './index.module.scss' 9 | import useRootStore from '@store/useRootStore' 10 | import { useOnMount } from '@utils/hooks' 11 | import Message from './Message' 12 | 13 | function Browse() { 14 | const { socketStore } = useRootStore() 15 | 16 | const vList = React.useRef(null) 17 | const measureCache = new CellMeasurerCache({ 18 | fixedWidth: true, 19 | minHeight: 43 20 | }) 21 | 22 | function handleMessagesChanged(len: number) { 23 | if (len === 0) { 24 | return measureCache.clearAll() 25 | } 26 | if (vList.current) { 27 | vList.current.scrollToRow(len - 1) 28 | } 29 | } 30 | 31 | function listenMessagesLen() { 32 | return reaction(() => socketStore.messages.length, handleMessagesChanged) 33 | } 34 | 35 | useOnMount(listenMessagesLen) 36 | 37 | function renderItem({ index, key, parent, style }: ListRowProps) { 38 | const item = socketStore.messages[index] 39 | return ( 40 | 41 | 42 | 43 | ) 44 | } 45 | const rowCount = socketStore.messages.length 46 | return ( 47 |
48 | 49 | {({ width, height }) => ( 50 | 60 | )} 61 | 62 |
63 | ) 64 | } 65 | 66 | export default observer(Browse) 67 | -------------------------------------------------------------------------------- /src/containers/views/SocketDebugger/Handler/Connect.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { observer } from 'mobx-react' 3 | import { message, Input, Button, Checkbox } from 'antd' 4 | 5 | import styles from './index.module.scss' 6 | import useRootStore from '@store/useRootStore' 7 | import { socketConnect, socketDisconnect } from '@services/websocket' 8 | import { LOCALSTORAGE_KEYS } from '@constants/index' 9 | 10 | function Connect() { 11 | const { socketStore } = useRootStore() 12 | 13 | const [url, setUrl] = React.useState(localStorage.getItem(LOCALSTORAGE_KEYS.SOCKET_URL)) 14 | 15 | function handleChange(e: React.ChangeEvent) { 16 | const { value } = e.target 17 | setUrl(value) 18 | localStorage.setItem(LOCALSTORAGE_KEYS.SOCKET_URL, value) 19 | } 20 | 21 | function handleConnect() { 22 | if (!url) { 23 | message.destroy() 24 | return message.error('Please input socket url!') 25 | } 26 | socketConnect(url) 27 | socketStore.clearMessages() 28 | } 29 | 30 | return ( 31 |
32 |
33 | 34 | {socketStore.isSocketIO && ( 35 | socketStore.setNotSupportPolling(e.target.checked)} 40 | > 41 | no polling 42 | 43 | )} 44 | 52 | 60 |
61 |
62 | protocol//ip or domain:host (example: 63 | {socketStore.isSocketIO ? ' wss://showcase.jackple.com' : ' ws://127.0.0.1:3001'}) 64 |
65 |
66 | ) 67 | } 68 | 69 | export default observer(Connect) 70 | -------------------------------------------------------------------------------- /src/containers/views/SocketDebugger/Handler/DataFormat.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { observer } from 'mobx-react' 3 | import { Select } from 'antd' 4 | 5 | import useRootStore from '@store/useRootStore' 6 | import { LOCALSTORAGE_KEYS } from '@constants/index' 7 | import { DATA_FORMATS } from '@constants/socket' 8 | 9 | function DataFormat() { 10 | const { socketStore } = useRootStore() 11 | 12 | function handleChange(val: ISocketStore.DataFormatType) { 13 | socketStore.setDataFormat(val) 14 | localStorage.setItem(LOCALSTORAGE_KEYS.DATA_FORMAT, val) 15 | } 16 | return ( 17 | 18 | value={socketStore.dataFormat} 19 | style={{ width: 120 }} 20 | onChange={handleChange} 21 | > 22 | {DATA_FORMATS.map(d => ( 23 | 24 | {d} 25 | 26 | ))} 27 | 28 | ) 29 | } 30 | 31 | export default observer(DataFormat) 32 | -------------------------------------------------------------------------------- /src/containers/views/SocketDebugger/Handler/Send.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { observer } from 'mobx-react' 3 | import { Button, AutoComplete, Popconfirm, Modal, Input, message } from 'antd' 4 | import ReactJson from 'react-json-view' 5 | 6 | import styles from './index.module.scss' 7 | import useRootStore from '@store/useRootStore' 8 | import { LOCALSTORAGE_KEYS } from '@constants/index' 9 | import { DATA_FORMATS } from '@constants/socket' 10 | import { send } from '@services/websocket' 11 | 12 | const localSocketIOEvents = localStorage.getItem(LOCALSTORAGE_KEYS.SOCKET_IO_EVENTS) 13 | let initialSocketIOEvents: { value: string }[] = localSocketIOEvents ? JSON.parse(localSocketIOEvents) : [] 14 | if (initialSocketIOEvents.length > 30) { 15 | initialSocketIOEvents = initialSocketIOEvents.slice(0, 30) 16 | } 17 | 18 | function Send() { 19 | const { socketStore } = useRootStore() 20 | 21 | const [content, setContent] = React.useState('') 22 | const [textContent, setTextContent] = React.useState('') 23 | const [jsonContent, setJsonContent] = React.useState({}) 24 | const [socketIOEvent, setSocketIOEvent] = React.useState('') 25 | const [socketIOEvents, setSocketIOEvents] = React.useState(initialSocketIOEvents) 26 | const [modalVisible, setModalVisible] = React.useState(false) 27 | 28 | const canSend = React.useMemo(() => { 29 | if (socketStore.isSocketIO && !socketIOEvent) { 30 | return false 31 | } 32 | return socketStore.socketIsConnected 33 | }, [socketStore.isSocketIO, socketIOEvent, socketStore.socketIsConnected]) 34 | 35 | const sendingContent = React.useMemo( 36 | () => (socketStore.dataFormat === DATA_FORMATS[0] ? jsonContent : textContent), 37 | [socketStore.dataFormat, jsonContent, textContent] 38 | ) 39 | 40 | function toggleModalVisible() { 41 | setModalVisible(visible => !visible) 42 | } 43 | 44 | function handleOK() { 45 | try { 46 | setJsonContent(JSON.parse(content)) 47 | toggleModalVisible() 48 | } catch (err) { 49 | console.error(err) 50 | message.destroy() 51 | message.error('Please input json string!') 52 | } 53 | } 54 | 55 | function handleSubmit() { 56 | if (!socketStore.isSocketIO) { 57 | return send(null, sendingContent) 58 | } else if (!socketIOEvent) { 59 | message.destroy() 60 | return message.error('Please input event name!') 61 | } 62 | const hasStoraged = socketIOEvents.some(e => e.value === socketIOEvent) 63 | if (!hasStoraged) { 64 | const newSocketIOEvents = [{ value: socketIOEvent }, ...socketIOEvents] 65 | setSocketIOEvents(newSocketIOEvents) 66 | localStorage.setItem(LOCALSTORAGE_KEYS.SOCKET_IO_EVENTS, JSON.stringify(newSocketIOEvents)) 67 | } 68 | send(socketIOEvent, sendingContent) 69 | } 70 | 71 | return ( 72 |
73 | {socketStore.isSocketIO && ( 74 | setSocketIOEvent(e as string)} 80 | filterOption={(inputValue, option) => option.value.toUpperCase().includes(inputValue.toUpperCase())} 81 | /> 82 | )} 83 | {socketStore.dataFormat === DATA_FORMATS[0] ? ( 84 |
85 |
86 | setJsonContent({})}> 87 | 88 | 89 | 92 | 99 | setContent(target.value)} 104 | /> 105 | 106 |
107 | setJsonContent(uSrc as PlainObject)} 120 | onEdit={({ updated_src: uSrc }) => setJsonContent(uSrc as PlainObject)} 121 | onDelete={({ updated_src: uSrc }) => setJsonContent(uSrc as PlainObject)} 122 | src={jsonContent} 123 | /> 124 |
125 | ) : ( 126 | setTextContent(target.value)} 132 | /> 133 | )} 134 | 137 |
138 | ) 139 | } 140 | 141 | export default observer(Send) 142 | -------------------------------------------------------------------------------- /src/containers/views/SocketDebugger/Handler/Type.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { observer } from 'mobx-react' 3 | import { Radio } from 'antd' 4 | import { RadioChangeEvent } from 'antd/lib/radio' 5 | 6 | import useRootStore from '@store/useRootStore' 7 | import { LOCALSTORAGE_KEYS } from '@constants/index' 8 | import { SOCKER_TYPES } from '@constants/socket' 9 | 10 | function Type() { 11 | const { socketStore } = useRootStore() 12 | 13 | function handleTypeChange(e: RadioChangeEvent) { 14 | const { value } = e.target 15 | socketStore.setSocketType(value) 16 | localStorage.setItem(LOCALSTORAGE_KEYS.SOCKET_TYPE, value) 17 | } 18 | return ( 19 | 24 | {SOCKER_TYPES.map(s => ( 25 | 26 | {s} 27 | 28 | ))} 29 | 30 | ) 31 | } 32 | 33 | export default observer(Type) 34 | -------------------------------------------------------------------------------- /src/containers/views/SocketDebugger/Handler/index.module.scss: -------------------------------------------------------------------------------- 1 | .handler { 2 | width: 50%; 3 | } 4 | 5 | .head { 6 | align-items: center; 7 | display: flex; 8 | justify-content: space-between; 9 | } 10 | 11 | // Type 12 | .reset { 13 | margin-bottom: 15px; 14 | } 15 | 16 | .autoComplete { 17 | margin-top: 15px; 18 | width: 100%; 19 | } 20 | 21 | .btnCover { 22 | margin-left: 10px; 23 | } 24 | 25 | .content { 26 | border: 1px dotted #cac5c5; 27 | border-radius: 3px; 28 | margin: 10px 0; 29 | padding: 10px; 30 | } 31 | 32 | .textContent:global(.ant-input) { 33 | margin: 10px 0; 34 | } 35 | 36 | // Connect 37 | .container { 38 | margin: 20px 0 50px; 39 | } 40 | 41 | .socketUrlInput:global(.ant-input) { 42 | flex: 1; 43 | margin-right: 15px; 44 | } 45 | 46 | .connect { 47 | align-items: center; 48 | display: flex; 49 | } 50 | 51 | .btn { 52 | margin-left: 7px; 53 | } 54 | 55 | .checkbox { 56 | align-items: center; 57 | composes: btn; 58 | display: flex; 59 | 60 | :global { 61 | .ant-checkbox { 62 | top: 0; 63 | 64 | & + span { 65 | white-space: nowrap; 66 | } 67 | } 68 | } 69 | } 70 | 71 | .tips { 72 | border-left: 4px solid #ebedf0; 73 | color: #697b8c; 74 | font-size: 90%; 75 | margin: 1em 0; 76 | padding-left: 0.8em; 77 | } 78 | -------------------------------------------------------------------------------- /src/containers/views/SocketDebugger/Handler/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import styles from './index.module.scss' 4 | import Type from './Type' 5 | import DataFormat from './DataFormat' 6 | import Connect from './Connect' 7 | import Send from './Send' 8 | 9 | function Handler() { 10 | return ( 11 |
12 |
13 | 14 | 15 |
16 | 17 | 18 |
19 | ) 20 | } 21 | 22 | export default Handler 23 | -------------------------------------------------------------------------------- /src/containers/views/SocketDebugger/index.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex: 1; 4 | } 5 | -------------------------------------------------------------------------------- /src/containers/views/SocketDebugger/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import styles from './index.module.scss' 4 | import Handler from './Handler' 5 | import Browse from './Browse' 6 | 7 | function SocketDebugger() { 8 | return ( 9 |
10 | 11 | 12 |
13 | ) 14 | } 15 | 16 | export default SocketDebugger 17 | -------------------------------------------------------------------------------- /src/containers/views/Users/Header/index.module.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | align-items: center; 3 | display: flex; 4 | padding-bottom: 15px; 5 | } 6 | -------------------------------------------------------------------------------- /src/containers/views/Users/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button } from 'antd' 3 | 4 | import styles from './index.module.scss' 5 | import UserModal from './../UserModal' 6 | 7 | function Header() { 8 | const [modalVisible, setModalVisible] = React.useState(false) 9 | 10 | function toggleModalVisible() { 11 | setModalVisible(visible => !visible) 12 | } 13 | 14 | return ( 15 |
16 | 19 | 20 |
21 | ) 22 | } 23 | 24 | export default Header 25 | -------------------------------------------------------------------------------- /src/containers/views/Users/UserModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { observer } from 'mobx-react' 3 | import { Modal, Form, Input, Select } from 'antd' 4 | 5 | import useRootStore from '@store/useRootStore' 6 | 7 | const FormItem = Form.Item 8 | 9 | const formItemLayout = { 10 | labelCol: { 11 | xs: { span: 24 }, 12 | sm: { span: 5 } 13 | }, 14 | wrapperCol: { 15 | xs: { span: 24 }, 16 | sm: { span: 19 } 17 | } 18 | } 19 | 20 | const userCategory = ['user', 'admin'] 21 | 22 | interface IProps { 23 | visible: boolean 24 | onCancel: () => void 25 | user?: IUserStore.IUser 26 | } 27 | 28 | function UserModal({ visible, onCancel, user }: IProps) { 29 | const { userStore } = useRootStore() 30 | const [form] = Form.useForm() 31 | 32 | const [loading, setLoading] = React.useState(false) 33 | 34 | const typeIsAdd = user === undefined 35 | 36 | function toggleLoading() { 37 | setLoading(l => !l) 38 | } 39 | 40 | async function submit(values: IUserStore.IUser) { 41 | toggleLoading() 42 | try { 43 | if (typeIsAdd) { 44 | await userStore.createUser(values) 45 | } else { 46 | await userStore.modifyUser({ ...values, id: user.id }) 47 | } 48 | onCancel() 49 | } finally { 50 | toggleLoading() 51 | } 52 | } 53 | 54 | useEffect(() => { 55 | if (visible) { 56 | form.setFieldsValue({ 57 | account: user ? user.account : '', 58 | category: user ? user.category : userCategory[0], 59 | password: null 60 | }) 61 | } 62 | }, [visible]) 63 | 64 | return ( 65 | 73 |
74 | 75 | 76 | 77 | {typeIsAdd && ( 78 | 79 | 80 | 81 | )} 82 | 83 | 90 | 91 |
92 |
93 | ) 94 | } 95 | 96 | export default observer(UserModal) 97 | -------------------------------------------------------------------------------- /src/containers/views/Users/UserTable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Table, Divider, Popconfirm } from 'antd' 3 | import { observer } from 'mobx-react' 4 | 5 | import styles from './index.module.scss' 6 | import { useOnMount } from '@utils/hooks' 7 | import useRootStore from '@store/useRootStore' 8 | import UserModal from './UserModal' 9 | 10 | interface IProps { 11 | scrollY: number 12 | } 13 | 14 | function UserTable({ scrollY }: IProps) { 15 | const { userStore } = useRootStore() 16 | 17 | const [modalVisible, setModalVisible] = React.useState(false) 18 | const [currentUser, setCurrentUser] = React.useState(null) 19 | 20 | function modifyUser(user: IUserStore.IUser) { 21 | setCurrentUser(user) 22 | setModalVisible(true) 23 | } 24 | 25 | useOnMount(userStore.getUsers) 26 | 27 | return ( 28 | 29 | 30 | className="center-table" 31 | style={{ width: '100%' }} 32 | bordered 33 | rowKey="id" 34 | loading={userStore.getUsersloading} 35 | dataSource={userStore.users} 36 | scroll={{ y: scrollY }} 37 | pagination={{ 38 | current: userStore.pageIndex, 39 | showSizeChanger: true, 40 | pageSize: userStore.pageSize, 41 | pageSizeOptions: ['30', '20', '10'], 42 | total: userStore.total 43 | }} 44 | onChange={userStore.handleTableChange} 45 | > 46 | key="account" title="Account" dataIndex="account" width={200} /> 47 | key="category" title="Category" dataIndex="category" width={100} /> 48 | key="createdAt" title="CreatedAt" dataIndex="createdAt" width={200} /> 49 | 50 | key="action" 51 | title="Action" 52 | width={120} 53 | render={(_, record) => ( 54 | 55 | modifyUser(record)}> 56 | Modify 57 | 58 | 59 | userStore.deleteUser(record.id)} 63 | > 64 | Delete 65 | 66 | 67 | )} 68 | /> 69 | 70 | setModalVisible(false)} user={currentUser} /> 71 | 72 | ) 73 | } 74 | 75 | export default observer(UserTable) 76 | -------------------------------------------------------------------------------- /src/containers/views/Users/index.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .tableBox { 7 | flex: 1; 8 | overflow-y: hidden; 9 | } 10 | 11 | .ctrlEle { 12 | color: $primary-color; 13 | cursor: pointer; 14 | } 15 | -------------------------------------------------------------------------------- /src/containers/views/Users/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import styles from './index.module.scss' 4 | import Header from './Header' 5 | import UserTable from './UserTable' 6 | import AutoSizer from '@components/AutoSizer' 7 | 8 | export default function Users() { 9 | return ( 10 |
11 |
12 | {({ height }) => } 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_APP_ENV: string 5 | readonly VITE_BASEURL: string 6 | } 7 | 8 | interface ImportMeta { 9 | readonly env: ImportMetaEnv 10 | } 11 | -------------------------------------------------------------------------------- /src/errorHandler.ts: -------------------------------------------------------------------------------- 1 | import { get } from 'lodash-es' 2 | import { message } from 'antd' 3 | 4 | const NETWORK_ERRORS_PREFIX = ['network error', 'connect etimedout', 'getaddrinfo enotfound'] 5 | const REQUEST_TIMEOUT_PREFIX = ['timeout of '] 6 | 7 | /** 8 | * check if is specific error 9 | * 10 | * @param {string} msg 11 | * @param {('network' | 'timeout')} type 12 | * @returns 13 | */ 14 | function checkErrorType(msg: string, type: 'network' | 'timeout') { 15 | msg = msg.toLowerCase() 16 | const prefixs = type === 'network' ? NETWORK_ERRORS_PREFIX : REQUEST_TIMEOUT_PREFIX 17 | for (const p of prefixs) { 18 | if (msg.startsWith(p)) { 19 | return true 20 | } 21 | } 22 | return false 23 | } 24 | 25 | /** 26 | * define error msg 27 | * 28 | * @export 29 | * @enum {number} 30 | */ 31 | export enum ERROR_TYPE { 32 | COMMON_NETWORK_ERROR = 'network error', 33 | NETWORK_OFFLINE = 'you are offline', 34 | REQUEST_TIMEOUT = 'request timeout' 35 | } 36 | 37 | function toastError(e: any) { 38 | const errMsg: string = get(e, 'reason.response.data.message') || get(e, 'reason.message') 39 | if (errMsg && typeof errMsg === 'string') { 40 | if (checkErrorType(errMsg, 'network')) { 41 | return message.error(ERROR_TYPE.COMMON_NETWORK_ERROR) 42 | } 43 | if (checkErrorType(errMsg, 'timeout')) { 44 | return message.error(ERROR_TYPE.REQUEST_TIMEOUT) 45 | } 46 | message.error(errMsg) 47 | } 48 | } 49 | 50 | function handleError(e: any) { 51 | console.error(e) 52 | toastError(e) 53 | } 54 | 55 | /** 56 | * catch unhandledrejection and toast 57 | * 58 | * @export 59 | */ 60 | export default function catchUnhandledRejection() { 61 | window.addEventListener('unhandledrejection', handleError) 62 | } 63 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | width: 100%; 5 | } 6 | 7 | #app { 8 | display: flex; 9 | flex-direction: column; 10 | height: 100%; 11 | position: relative; 12 | width: 100%; 13 | } 14 | 15 | svg, 16 | svg path { 17 | fill: currentcolor; 18 | } 19 | 20 | .center-table { 21 | table td { 22 | text-align: center; 23 | } 24 | 25 | .ant-table-thead > tr > th { 26 | text-align: center; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import 'antd/dist/antd.less' 2 | import './index.scss' 3 | 4 | import React from 'react' 5 | import { createRoot } from 'react-dom/client' 6 | import { configure } from 'mobx' 7 | 8 | import App from '@shared/App' 9 | import catchUnhandledRejection from './errorHandler' 10 | 11 | configure({ enforceActions: 'observed' }) 12 | catchUnhandledRejection() 13 | 14 | const render = (Component: React.ComponentType) => { 15 | const root = createRoot(document.getElementById('app')) 16 | root.render() 17 | } 18 | 19 | render(App) 20 | -------------------------------------------------------------------------------- /src/locales/en_US.json: -------------------------------------------------------------------------------- 1 | { 2 | "USERNAME": "username", 3 | "PASSWORD": "password", 4 | "LOGIN": "Login" 5 | } 6 | -------------------------------------------------------------------------------- /src/locales/loader.ts: -------------------------------------------------------------------------------- 1 | import { Locale } from 'antd/lib/locale-provider' 2 | 3 | export enum LOCALES_KEYS { 4 | EN_US = 'en-US', 5 | ZH_CN = 'zh-CN' 6 | } 7 | 8 | export const SUPPOER_LOCALES = [ 9 | { 10 | name: 'English', 11 | value: LOCALES_KEYS.EN_US 12 | }, 13 | { 14 | name: '简体中文', 15 | value: LOCALES_KEYS.ZH_CN 16 | } 17 | ] 18 | 19 | export interface LocaleResponse { 20 | localeData: StringObject 21 | antdLocaleData: Locale 22 | } 23 | 24 | export function getLocaleLoader(locale: string): Promise { 25 | switch (locale) { 26 | case LOCALES_KEYS.ZH_CN: 27 | return new Promise(async resolve => { 28 | const loc = await import('./zh_CN.json').then(m => m.default) 29 | const antdLoc = await import('antd/lib/locale-provider/zh_CN').then(m => m.default) 30 | resolve({ localeData: loc, antdLocaleData: antdLoc }) 31 | }) 32 | default: 33 | return new Promise(async resolve => { 34 | const loc = await import('./en_US.json').then(m => m.default) 35 | const antdLoc = await import('antd/lib/locale-provider/en_US').then(m => m.default) 36 | resolve({ localeData: loc, antdLocaleData: antdLoc }) 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/locales/zh_CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "USERNAME": "用户名", 3 | "PASSWORD": "密码", 4 | "LOGIN": "登录" 5 | } 6 | -------------------------------------------------------------------------------- /src/services/websocket/index.ts: -------------------------------------------------------------------------------- 1 | import { socketStore } from '@store/index' 2 | import { 3 | socketConnect as socketConnectFromSocketIO, 4 | socketDisconnect as socketDisconnectFromSocketIO, 5 | send as sendFromSocketIO 6 | } from './socketIO' 7 | import { 8 | socketConnect as socketConnectFromWebsocket, 9 | socketDisconnect as socketDisconnectFromWebsocket, 10 | send as sendFromWebsocket 11 | } from './websocket' 12 | 13 | export const socketConnect = (url: string) => { 14 | return socketStore.isSocketIO ? socketConnectFromSocketIO(url) : socketConnectFromWebsocket(url) 15 | } 16 | 17 | export const socketDisconnect = () => { 18 | return socketStore.isSocketIO ? socketDisconnectFromSocketIO() : socketDisconnectFromWebsocket() 19 | } 20 | 21 | export const send = (event: string, data: any) => { 22 | return socketStore.isSocketIO ? sendFromSocketIO(event, data) : sendFromWebsocket(event, data) 23 | } 24 | -------------------------------------------------------------------------------- /src/services/websocket/socketIO.ts: -------------------------------------------------------------------------------- 1 | import io, { Manager, Socket } from 'socket.io-client' 2 | import socketioWildcard from 'socketio-wildcard' 3 | import { message } from 'antd' 4 | import { reaction } from 'mobx' 5 | 6 | import { socketStore } from '@store/index' 7 | 8 | const patch = socketioWildcard(Manager) 9 | 10 | /** 11 | * socket 通信 12 | * 13 | * @export 14 | * @class Socket 15 | */ 16 | class _Socket { 17 | socket: Socket 18 | 19 | send(event: string, data: any, retry = 0) { 20 | if (this.socket && this.socket.connected) { 21 | this.socket.emit(event, data) 22 | } else if (retry < 3) { 23 | setTimeout(() => { 24 | this.send(data, retry++) 25 | }, 300) 26 | } 27 | } 28 | 29 | open(url: string) { 30 | const transports = ['websocket'] 31 | if (!socketStore.notSupportPolling) { 32 | transports.unshift('polling') 33 | } 34 | this.socket = io(url, { 35 | reconnectionDelay: 1000, 36 | reconnection: true, 37 | reconnectionAttempts: 5, 38 | transports 39 | }) 40 | 41 | patch(this.socket) 42 | 43 | reaction( 44 | () => socketStore.socketType, 45 | (_, __, r) => { 46 | this.socket.close() 47 | r.dispose() 48 | } 49 | ) 50 | 51 | this.socket.on('reconnect', attemptNumber => { 52 | const text = `socket reconnect after attempt ${attemptNumber} times !!!` 53 | socketStore.addMessage({ 54 | event: 'reconnect', 55 | from: 'console', 56 | data: text 57 | }) 58 | }) 59 | 60 | // 被断开, 不重连 61 | this.socket.on('disconnect', reason => { 62 | socketStore.setSocketIsConnected(false) 63 | const text = `socket disconnect because: ${reason} !!!` 64 | socketStore.addMessage({ 65 | event: 'disconnect', 66 | from: 'console', 67 | data: text 68 | }) 69 | }) 70 | 71 | this.socket.on('connect_timeout', timeout => { 72 | const text = `socket connect_timeout: ${timeout} !!!` 73 | socketStore.addMessage({ 74 | event: 'connect_timeout', 75 | from: 'console', 76 | data: text 77 | }) 78 | }) 79 | 80 | // 连接错误 81 | this.socket.on('connect_error', err => { 82 | const text = 'socket connect_error !!!' 83 | socketStore.addMessage({ 84 | event: 'connect_error', 85 | from: 'console', 86 | data: text 87 | }) 88 | console.warn(err) 89 | }) 90 | 91 | // 错误捕获 92 | this.socket.on('error', err => { 93 | socketStore.setSocketIsConnected(false) 94 | const text = 'socket error !!!' 95 | socketStore.addMessage({ 96 | event: 'error', 97 | from: 'console', 98 | data: text 99 | }) 100 | console.warn(err) 101 | }) 102 | 103 | this.socket.on('connect', () => { 104 | socketStore.setSocketIsConnected(true) 105 | const text = 'socket connected !!!' 106 | socketStore.addMessage({ 107 | event: 'connect', 108 | from: 'console', 109 | data: text 110 | }) 111 | }) 112 | 113 | this.socket.on('ping', () => { 114 | socketStore.addMessage({ 115 | event: 'ping', 116 | from: 'browser', 117 | data: null 118 | }) 119 | }) 120 | 121 | this.socket.on('pong', () => { 122 | socketStore.addMessage({ 123 | event: 'pong', 124 | from: 'server', 125 | data: null 126 | }) 127 | }) 128 | 129 | this.socket.on('*', pkg => { 130 | console.log('on all socket callback: ', pkg) 131 | if (pkg && pkg.data instanceof Array && pkg.data.length > 1) { 132 | const event = pkg.data[0] 133 | const data = pkg.data[1] 134 | socketStore.addMessage({ 135 | event, 136 | from: 'server', 137 | data 138 | }) 139 | } 140 | }) 141 | } 142 | } 143 | 144 | const socketInstance = new _Socket() 145 | 146 | function canSocketOpen() { 147 | return !(socketInstance.socket && socketInstance.socket.connected) 148 | } 149 | 150 | export function socketConnect(url: string) { 151 | if (!canSocketOpen()) { 152 | return message.error('Please disconnect the existing instance!!!') 153 | } 154 | socketInstance.open(url) 155 | } 156 | 157 | export function socketDisconnect() { 158 | if (socketInstance.socket && socketInstance.socket.connected) { 159 | socketInstance.socket.close() 160 | } 161 | } 162 | 163 | export function send(event: string, data: any) { 164 | if (!socketInstance.socket || !socketInstance.socket.connected) { 165 | return message.error('Please connect to server!!!') 166 | } 167 | socketInstance.send(event, data) 168 | socketStore.addMessage({ 169 | event, 170 | from: 'browser', 171 | data 172 | }) 173 | } 174 | -------------------------------------------------------------------------------- /src/services/websocket/websocket.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'eventemitter3' 2 | import { message } from 'antd' 3 | import { reaction } from 'mobx' 4 | 5 | import { socketStore } from '@store/index' 6 | 7 | let reopenTimer: number = null 8 | // 是否主动断开 9 | let disconnectInitiative = false 10 | 11 | interface SocketMsg { 12 | data: any 13 | } 14 | 15 | /** 16 | * socket 通信 17 | * 18 | * @export 19 | * @class Socket 20 | */ 21 | class Socket extends EventEmitter { 22 | onopen: () => void 23 | onmessage: (msg: SocketMsg) => void 24 | conn: WebSocket = null 25 | 26 | constructor() { 27 | super() 28 | this.run() 29 | } 30 | 31 | run() { 32 | this.onopen = () => { 33 | const text = 'socket connected !!!' 34 | socketStore.setSocketIsConnected(true) 35 | socketStore.addMessage({ 36 | event: 'connect', 37 | from: 'console', 38 | data: text 39 | }) 40 | } 41 | 42 | this.onmessage = (msg: SocketMsg) => { 43 | if (!msg || !msg.data) { 44 | return 45 | } 46 | socketStore.addMessage({ 47 | event: 'message', 48 | from: 'server', 49 | data: typeof msg.data === 'object' ? JSON.stringify(msg.data) : msg.data 50 | }) 51 | } 52 | } 53 | 54 | send(data: any, retry = 0) { 55 | if (this.conn && this.conn.readyState === this.conn.OPEN) { 56 | this.conn.send(typeof data === 'object' ? JSON.stringify(data) : data) 57 | } else if (retry < 3) { 58 | setTimeout(() => { 59 | this.send(data, retry++) 60 | }, 300) 61 | } 62 | } 63 | 64 | open(url: string) { 65 | this.conn = new WebSocket(url) 66 | this.conn.onclose = evt => { 67 | socketStore.setSocketIsConnected(false) 68 | const text = `socket close: ${typeof evt === 'object' ? evt.code : ''}` 69 | socketStore.addMessage({ 70 | event: 'close', 71 | from: 'console', 72 | data: text 73 | }) 74 | clearTimeout(reopenTimer) 75 | if (!disconnectInitiative) { 76 | reopenTimer = window.setTimeout(() => { 77 | this.open(url) 78 | }, 3000) 79 | } 80 | disconnectInitiative = false 81 | } 82 | this.conn.onerror = evt => { 83 | socketStore.setSocketIsConnected(false) 84 | const text = `socket error: ${typeof evt === 'object' ? JSON.stringify((evt as any).code) : ''}` 85 | socketStore.addMessage({ 86 | event: 'error', 87 | from: 'console', 88 | data: text 89 | }) 90 | } 91 | 92 | reaction( 93 | () => socketStore.socketType, 94 | (_, __, r) => { 95 | clearTimeout(reopenTimer) 96 | r.dispose() 97 | } 98 | ) 99 | 100 | if (this.onopen) { 101 | this.conn.onopen = this.onopen 102 | } 103 | if (this.onmessage) { 104 | this.conn.onmessage = this.onmessage 105 | } 106 | return this 107 | } 108 | } 109 | 110 | const socketInstance = new Socket() 111 | 112 | function canSocketOpen() { 113 | return !(socketInstance.conn && socketInstance.conn.readyState === socketInstance.conn.OPEN) 114 | } 115 | 116 | export function socketConnect(url: string) { 117 | if (!canSocketOpen()) { 118 | return message.error('Please disconnect the existing instance!!!') 119 | } 120 | socketInstance.open(url) 121 | } 122 | 123 | export function socketDisconnect() { 124 | if (socketInstance.conn && socketInstance.conn.readyState === socketInstance.conn.OPEN) { 125 | socketInstance.conn.close() 126 | } 127 | disconnectInitiative = true 128 | } 129 | 130 | export function send(_, data: any) { 131 | if (!socketInstance.conn && socketInstance.conn.readyState !== socketInstance.conn.OPEN) { 132 | return message.error('Please connect to server!!!') 133 | } 134 | socketInstance.send(data) 135 | socketStore.addMessage({ 136 | event: null, 137 | from: 'browser', 138 | data 139 | }) 140 | } 141 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme' 2 | import Adapter from 'enzyme-adapter-react-16' 3 | 4 | Enzyme.configure({ 5 | adapter: new Adapter() 6 | }) 7 | -------------------------------------------------------------------------------- /src/store/authStore/index.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable, action, reaction } from 'mobx' 2 | import { isPlainObject } from 'lodash-es' 3 | 4 | import { initialUserInfo, syncUserInfo } from './syncUserInfo' 5 | import { LOCALSTORAGE_KEYS } from '@constants/index' 6 | import request from '@utils/request' 7 | import history from '@shared/App/ht' 8 | 9 | export class AuthStore { 10 | /** 11 | * 用户信息 12 | * 13 | * @type {IAuthStore.UserInfo} 14 | * @memberof AuthStore 15 | */ 16 | userInfo: IAuthStore.UserInfo = initialUserInfo 17 | 18 | constructor() { 19 | makeAutoObservable(this) 20 | reaction(() => this.userInfo, syncUserInfo) 21 | } 22 | 23 | @action 24 | login = async (params: IAuthStore.LoginParams) => { 25 | const { data } = await request.post('auth/login', params) 26 | this.setUserInfo(isPlainObject(data) ? data : null) 27 | localStorage.setItem(LOCALSTORAGE_KEYS.USERINFO, JSON.stringify(data)) 28 | history.replace('/') 29 | } 30 | 31 | logout = () => { 32 | this.setUserInfo(null) 33 | localStorage.removeItem(LOCALSTORAGE_KEYS.USERINFO) 34 | history.replace('/login') 35 | } 36 | 37 | /** 38 | * 初始化用户信息 39 | * 40 | * @memberof AuthStore 41 | */ 42 | @action 43 | setUserInfo = (userInfo: IAuthStore.UserInfo): IAuthStore.UserInfo => { 44 | this.userInfo = userInfo 45 | return userInfo 46 | } 47 | } 48 | 49 | export default new AuthStore() 50 | -------------------------------------------------------------------------------- /src/store/authStore/syncUserInfo.ts: -------------------------------------------------------------------------------- 1 | import { LOCALSTORAGE_KEYS } from '@constants/index' 2 | 3 | export const initialUserInfo = (() => { 4 | const localUserInfo = localStorage.getItem(LOCALSTORAGE_KEYS.USERINFO) 5 | const _userInfo: IAuthStore.UserInfo = localUserInfo ? JSON.parse(localUserInfo) : null 6 | return _userInfo 7 | })() 8 | 9 | export let userInfo: IAuthStore.UserInfo = initialUserInfo 10 | 11 | /** 12 | * syncUserInfo for http 13 | * 14 | * @export 15 | * @param {IAuthStore.UserInfo} data 16 | */ 17 | export function syncUserInfo(data: IAuthStore.UserInfo) { 18 | userInfo = data 19 | } 20 | -------------------------------------------------------------------------------- /src/store/authStore/type.d.ts: -------------------------------------------------------------------------------- 1 | import { AuthStore as AuthStoreModel } from './index' 2 | 3 | export as namespace IAuthStore 4 | 5 | export interface AuthStore extends AuthStoreModel {} 6 | 7 | export interface LoginParams { 8 | account: string 9 | password: string 10 | } 11 | 12 | export interface UserInfo { 13 | msg?: string 14 | token: string 15 | category: string 16 | } 17 | -------------------------------------------------------------------------------- /src/store/globalStore/index.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx' 2 | 3 | import { LOCALSTORAGE_KEYS } from '@constants/index' 4 | 5 | export class GlobalStore { 6 | constructor() { 7 | makeAutoObservable(this) 8 | } 9 | 10 | /** 11 | * 菜单栏折叠 12 | * 13 | * @type {boolean} 14 | * @memberof GlobalStore 15 | */ 16 | sideBarCollapsed: boolean = localStorage.getItem(LOCALSTORAGE_KEYS.SIDE_BAR_COLLAPSED) === '1' 17 | /** 18 | * 菜单栏主题 19 | * 20 | * @type {IGlobalStore.SideBarTheme} 21 | * @memberof GlobalStore 22 | */ 23 | sideBarTheme: IGlobalStore.SideBarTheme = 24 | (localStorage.getItem(LOCALSTORAGE_KEYS.SIDE_BAR_THEME) as IGlobalStore.SideBarTheme) || 'light' 25 | /** 26 | * 打开的菜单key 27 | * 28 | * @type {string[]} 29 | * @memberof GlobalStore 30 | */ 31 | navOpenKeys: string[] = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEYS.NAV_OPEN_KEYS)) || [] 32 | 33 | toggleSideBarCollapsed = () => { 34 | this.sideBarCollapsed = !this.sideBarCollapsed 35 | localStorage.setItem(LOCALSTORAGE_KEYS.SIDE_BAR_COLLAPSED, this.sideBarCollapsed ? '1' : '0') 36 | } 37 | 38 | changeSiderTheme = (theme: IGlobalStore.SideBarTheme) => { 39 | this.sideBarTheme = theme 40 | localStorage.setItem(LOCALSTORAGE_KEYS.SIDE_BAR_THEME, theme) 41 | } 42 | 43 | setOpenKeys = (openKeys: string[]) => { 44 | this.navOpenKeys = openKeys 45 | localStorage.setItem(LOCALSTORAGE_KEYS.NAV_OPEN_KEYS, JSON.stringify(openKeys)) 46 | } 47 | } 48 | 49 | export default new GlobalStore() 50 | -------------------------------------------------------------------------------- /src/store/globalStore/type.d.ts: -------------------------------------------------------------------------------- 1 | import { GlobalStore as GlobalStoreModel } from './index' 2 | 3 | export as namespace IGlobalStore 4 | 5 | export interface GlobalStore extends GlobalStoreModel {} 6 | 7 | export type SideBarTheme = 'dark' | 'light' 8 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | export { default as socketStore } from './socketStore' 2 | export { default as globalStore } from './globalStore' 3 | export { default as authStore } from './authStore' 4 | export { default as userStore } from './userStore' 5 | -------------------------------------------------------------------------------- /src/store/socketStore/index.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx' 2 | 3 | import { LOCALSTORAGE_KEYS } from '@constants/index' 4 | import { SOCKER_TYPES, DATA_FORMATS } from '@constants/socket' 5 | 6 | /** 7 | * socket debugger store 8 | * 9 | * @export 10 | * @class SocketStore 11 | */ 12 | export class SocketStore { 13 | constructor() { 14 | makeAutoObservable(this) 15 | } 16 | 17 | socketType: ISocketStore.SocketType = 18 | (localStorage.getItem(LOCALSTORAGE_KEYS.SOCKET_TYPE) as ISocketStore.SocketType) || SOCKER_TYPES[0] 19 | dataFormat: ISocketStore.DataFormatType = 20 | (localStorage.getItem(LOCALSTORAGE_KEYS.DATA_FORMAT) as ISocketStore.DataFormatType) || DATA_FORMATS[0] 21 | socketIsConnected = false 22 | messages: ISocketStore.Message[] = [] 23 | notSupportPolling: boolean = localStorage.getItem(LOCALSTORAGE_KEYS.NOT_SUPPORT_POLLING) === 'true' 24 | 25 | get isSocketIO() { 26 | return this.socketType === SOCKER_TYPES[0] 27 | } 28 | 29 | setSocketType = (type: ISocketStore.SocketType) => { 30 | this.socketType = type 31 | } 32 | 33 | setDataFormat = (dataFormat: ISocketStore.DataFormatType) => { 34 | this.dataFormat = dataFormat 35 | } 36 | 37 | setSocketIsConnected = (socketIsConnected: boolean) => { 38 | this.socketIsConnected = socketIsConnected 39 | } 40 | 41 | clearMessages = () => { 42 | this.messages = [] 43 | } 44 | 45 | addMessage = (message: ISocketStore.Message) => { 46 | if (!message.time) { 47 | message.time = new Date().getTime() 48 | } 49 | this.messages.push(message) 50 | } 51 | 52 | setNotSupportPolling = (val: boolean) => { 53 | this.notSupportPolling = val 54 | localStorage.setItem(LOCALSTORAGE_KEYS.NOT_SUPPORT_POLLING, String(val)) 55 | } 56 | } 57 | 58 | export default new SocketStore() 59 | -------------------------------------------------------------------------------- /src/store/socketStore/type.d.ts: -------------------------------------------------------------------------------- 1 | import { SocketStore as SocketStoreModel } from './index' 2 | import { SOCKET_TYPE, DATA_FORMAT_TYPE } from '@constants/socket' 3 | 4 | export as namespace ISocketStore 5 | 6 | export interface SocketStore extends SocketStoreModel {} 7 | 8 | export import SocketType = SOCKET_TYPE 9 | export import DataFormatType = DATA_FORMAT_TYPE 10 | 11 | export interface Message { 12 | event: string 13 | time?: number 14 | from: 'browser' | 'server' | 'console' 15 | data: any 16 | } 17 | -------------------------------------------------------------------------------- /src/store/useRootStore.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | 3 | import { RootContext } from '@shared/App/Provider' 4 | 5 | export default function useRootStore() { 6 | return useContext(RootContext) 7 | } 8 | -------------------------------------------------------------------------------- /src/store/userStore/index.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable, observable, runInAction } from 'mobx' 2 | import { TablePaginationConfig } from 'antd/lib/table' 3 | 4 | import request from '@utils/request' 5 | 6 | export class UserStore { 7 | constructor() { 8 | makeAutoObservable(this, { users: observable.ref }) 9 | } 10 | /** 11 | * 加载用户列表时的loading 12 | * 13 | * @memberof UserStore 14 | */ 15 | getUsersloading = false 16 | /** 17 | * 用户列表 18 | * 19 | * @type {IUserStore.IUser[]} 20 | * @memberof UserStore 21 | */ 22 | users: IUserStore.IUser[] = [] 23 | /** 24 | * table pageIndex 25 | * 26 | * @memberof UserStore 27 | */ 28 | pageIndex = 1 29 | /** 30 | * table pageSize 31 | * 32 | * @memberof UserStore 33 | */ 34 | pageSize = 30 35 | /** 36 | * users total 37 | * 38 | * @memberof UserStore 39 | */ 40 | total = 0 41 | 42 | /** 43 | * 加载用户列表 44 | * 45 | * @memberof UserStore 46 | */ 47 | getUsers = async () => { 48 | this.getUsersloading = true 49 | try { 50 | const { data } = await request.get('user', { 51 | params: { pageIndex: this.pageIndex, pageSize: this.pageSize } 52 | }) 53 | runInAction(() => { 54 | this.users = data.users 55 | this.total = data.total 56 | }) 57 | } finally { 58 | runInAction(() => { 59 | this.getUsersloading = false 60 | }) 61 | } 62 | } 63 | 64 | createUser = async (user: IUserStore.IUser) => { 65 | await request.post('user/create', user) 66 | this.changePageIndex(1) 67 | } 68 | 69 | modifyUser = async (user: IUserStore.IUser) => { 70 | const { id, ...rest } = user 71 | await request.put(`user/${id}`, rest) 72 | this.getUsers() 73 | } 74 | 75 | deleteUser = async (id: string) => { 76 | await request.delete(`user/${id}`) 77 | this.getUsers() 78 | } 79 | 80 | changePageIndex = (pageIndex: number) => { 81 | this.pageIndex = pageIndex 82 | this.getUsers() 83 | } 84 | 85 | changePageSize = (pageSize: number) => { 86 | this.pageSize = pageSize 87 | this.getUsers() 88 | } 89 | 90 | handleTableChange = ({ current, pageSize }: TablePaginationConfig) => { 91 | if (current !== this.pageIndex) { 92 | this.changePageIndex(current) 93 | } 94 | if (pageSize !== this.pageSize) { 95 | this.changePageSize(pageSize) 96 | } 97 | } 98 | } 99 | 100 | export default new UserStore() 101 | -------------------------------------------------------------------------------- /src/store/userStore/type.d.ts: -------------------------------------------------------------------------------- 1 | import { UserStore as UserStoreModel } from './index' 2 | 3 | export as namespace IUserStore 4 | 5 | export interface UserStore extends UserStoreModel {} 6 | 7 | export interface IUser { 8 | id?: string 9 | account: string 10 | password?: string 11 | category?: string 12 | createdAt?: string 13 | } 14 | -------------------------------------------------------------------------------- /src/styles/_base.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 所有sass文件依赖基础入口 3 | * 注意: 因为所有scss文件都依赖, 4 | * 此文件包括此文件依赖的所有模块都不能有实际的css代码输出, 5 | * 避免太多重复的css代码输出 6 | */ 7 | 8 | @import './_var.scss'; 9 | -------------------------------------------------------------------------------- /src/styles/_var.scss: -------------------------------------------------------------------------------- 1 | // global variables 2 | // -------- Colors ----------- 3 | $primary-color: #1da57a; 4 | $dark-color: #001529; 5 | $gray-color: #888; 6 | $border-light-color: #f8f8f8; 7 | 8 | // *** font-size ***/ 9 | $font-size-base: 14px; 10 | $font-size-lg: 16px; 11 | $font-size-sm: 12px; 12 | -------------------------------------------------------------------------------- /src/utils/hooks.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | /** 4 | * componentDidMount in hook way 5 | * 6 | * @export 7 | * @param {() => any} onMount 8 | * @returns 9 | */ 10 | export function useOnMount(onMount: () => any) { 11 | return React.useEffect(() => { 12 | if (onMount) { 13 | onMount() 14 | } 15 | }, []) 16 | } 17 | 18 | /** 19 | * componentWillUnmount in hook way 20 | * 21 | * @export 22 | * @param {() => any} onUnmount 23 | * @returns 24 | */ 25 | export function useOnUnmount(onUnmount: () => any) { 26 | return React.useEffect(() => { 27 | return () => onUnmount && onUnmount() 28 | }, []) 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * setCookie 3 | * 4 | * @export 5 | * @param {string} name 6 | * @param {string} value 7 | * @param {number} [expiredays=365] 8 | */ 9 | export function setCookie(name: string, value: string, expiredays = 365) { 10 | const exdate = new Date() 11 | exdate.setDate(exdate.getDate() + expiredays) 12 | document.cookie = `${name}=${escape(value)};expires=${exdate.toUTCString()}` 13 | } 14 | 15 | /** 16 | * getCookie 17 | * 18 | * @export 19 | * @param {string} name 20 | * @returns 21 | */ 22 | export function getCookie(name: string) { 23 | if (document.cookie.length > 0) { 24 | let cStart = document.cookie.indexOf(name + '=') 25 | if (cStart !== -1) { 26 | cStart = cStart + name.length + 1 27 | let cEnd = document.cookie.indexOf(';', cStart) 28 | if (cEnd === -1) { 29 | cEnd = document.cookie.length 30 | } 31 | return unescape(document.cookie.substring(cStart, cEnd)) 32 | } 33 | } 34 | return '' 35 | } 36 | 37 | /** 38 | * clearCookie 39 | * 40 | * @export 41 | * @param {string} name 42 | */ 43 | export function clearCookie(name: string) { 44 | setCookie(name, '') 45 | } 46 | 47 | /** 48 | * 从url获取参数 49 | * 50 | * @export 51 | * @param {string} name 52 | * @returns {string} 53 | */ 54 | export function queryURL(name: string): string { 55 | const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i') 56 | const result = window.location.search.substr(1).match(reg) 57 | if (result !== null) { 58 | return decodeURI(result[2]) 59 | } 60 | return null 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig } from 'axios' 2 | 3 | import { userInfo } from '@store/authStore/syncUserInfo' 4 | import history from '@shared/App/ht' 5 | 6 | const TIMEOUT = 2 * 60000 7 | 8 | // if you want another config, create one!! 9 | const DEFAULTCONFIG: AxiosRequestConfig = { 10 | baseURL: import.meta.env.VITE_BASEURL, 11 | timeout: TIMEOUT 12 | } 13 | 14 | const NO_NEED_AUTH_URLS = ['auth/login'] 15 | 16 | function getAxiosInstance() { 17 | const instance = axios.create(DEFAULTCONFIG) 18 | instance.interceptors.request.use(config => { 19 | if (!NO_NEED_AUTH_URLS.includes(config.url) && userInfo?.token) { 20 | config.headers['Authorization'] = `Bearer ${userInfo.token}` 21 | } 22 | return config 23 | }) 24 | instance.interceptors.response.use( 25 | function (response) { 26 | return response.data 27 | }, 28 | function (error) { 29 | if (error?.response?.data?.message === 'invalid token' || error?.response?.status === 401) { 30 | history.replace('/login') 31 | } 32 | return Promise.reject(error) 33 | } 34 | ) 35 | 36 | return instance 37 | } 38 | 39 | export default getAxiosInstance() 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true, 8 | "sourceMap": true, 9 | "removeComments": false, 10 | "rootDirs": ["./src"], 11 | "baseUrl": "./src", 12 | "paths": { 13 | "@constants/*": ["constants/*"], 14 | "@services/*": ["services/*"], 15 | "@store/*": ["store/*"], 16 | "@utils/*": ["utils/*"], 17 | "@assets/*": ["assets/*"], 18 | "@locales/*": ["locales/*"], 19 | "@components/*": ["components/*"], 20 | "@views/*": ["containers/views/*"], 21 | "@shared/*": ["containers/shared/*"] 22 | }, 23 | "jsx": "react", 24 | "alwaysStrict": true, 25 | "noUnusedLocals": true, 26 | "importHelpers": true, 27 | "experimentalDecorators": true, 28 | "resolveJsonModule": true, 29 | "lib": ["es7", "dom"], 30 | "skipLibCheck": true, 31 | "typeRoots": ["node", "node_modules/@types", "./typings"] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /typings/assets.d.ts: -------------------------------------------------------------------------------- 1 | declare interface SvgrComponent extends React.StatelessComponent> {} 2 | declare module '*.svg' { 3 | const content: SvgrComponent 4 | export default content 5 | } 6 | 7 | declare module '*.png' { 8 | const content: any 9 | export default content 10 | } 11 | declare module '*.jpg' { 12 | const content: any 13 | export default content 14 | } 15 | declare module '*.jpeg' { 16 | const content: any 17 | export default content 18 | } 19 | declare module '*.gif' { 20 | const content: any 21 | export default content 22 | } 23 | 24 | // for css-module 25 | declare module '*.scss' { 26 | const content: any 27 | export = content 28 | } 29 | -------------------------------------------------------------------------------- /typings/attr.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | type PropsWithChildrenOnly = React.PropsWithChildren 4 | declare module 'react' { 5 | interface ImgHTMLAttributes extends HTMLAttributes { 6 | referrerPolicy?: 7 | | 'no-referrer' 8 | | 'no-referrer-when-downgrade' 9 | | 'origin' 10 | | 'origin-when-cross-origin' 11 | | 'unsafe-url' 12 | } 13 | 14 | type ReactFCWithChildren = React.FC 15 | } 16 | -------------------------------------------------------------------------------- /typings/global.d.ts: -------------------------------------------------------------------------------- 1 | declare interface PlainObject { 2 | [propName: string]: unknown 3 | } 4 | 5 | declare interface BooleanObject { 6 | [propName: string]: boolean 7 | } 8 | 9 | declare interface StringObject { 10 | [propName: string]: string 11 | } 12 | 13 | declare interface NumberObject { 14 | [propName: string]: number 15 | } 16 | -------------------------------------------------------------------------------- /typings/store.d.ts: -------------------------------------------------------------------------------- 1 | export declare global { 2 | /** 3 | * type for all store 4 | * 5 | * @interface IStore 6 | */ 7 | interface IStore { 8 | authStore: IAuthStore.AuthStore 9 | userStore: IUserStore.UserStore 10 | globalStore: IGlobalStore.GlobalStore 11 | socketStore: ISocketStore.SocketStore 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { defineConfig, loadEnv } from 'vite' 3 | import react from '@vitejs/plugin-react' 4 | import legacy from '@vitejs/plugin-legacy' 5 | import { VitePWA } from 'vite-plugin-pwa' 6 | import tsconfigPaths from 'vite-tsconfig-paths' 7 | 8 | const pathSrc = path.resolve(__dirname, './src') 9 | const pathNodeModules = path.resolve(__dirname, './node_modules') 10 | 11 | // https://vitejs.dev/config/ 12 | export default ({ mode }) => { 13 | const env = loadEnv(mode, process.cwd()) 14 | const domain = env.VITE_APP_ENV !== 'prod' ? 'https://starter.jackple.com' : 'https://your_domain' 15 | const base = env.VITE_APP_ENV !== 'prod' ? '/' : 'https://your_cdn_domain/' 16 | const staticDomain = base === '/' ? domain + base : base 17 | 18 | return defineConfig({ 19 | base, 20 | plugins: [ 21 | tsconfigPaths(), 22 | react(), 23 | legacy({ targets: ['defaults', 'not IE 11'] }), 24 | VitePWA({ 25 | base: '/', 26 | workbox: { 27 | cacheId: 'ts-react-vite', 28 | clientsClaim: true, 29 | skipWaiting: true, 30 | offlineGoogleAnalytics: false, 31 | inlineWorkboxRuntime: true, 32 | runtimeCaching: [ 33 | { 34 | // match html 35 | urlPattern: new RegExp(domain), 36 | handler: 'NetworkFirst' 37 | }, 38 | { 39 | // match static resource 40 | urlPattern: new RegExp(`${staticDomain.replace(/\//g, '\\/')}\\/assets`), 41 | handler: 'StaleWhileRevalidate' 42 | } 43 | ] 44 | } 45 | }) 46 | // require('rollup-plugin-visualizer').default({ open: true, gzipSize: true, brotliSize: true }) 47 | ], 48 | css: { 49 | preprocessorOptions: { 50 | scss: { 51 | charset: false, 52 | additionalData: ` 53 | @import "${pathNodeModules.replace(/\\/g, '/')}/bourbon/core/_bourbon.scss"; 54 | @import "${pathSrc.replace(/\\/g, '/')}/styles/_base.scss"; 55 | ` 56 | }, 57 | less: { 58 | modifyVars: { 59 | 'primary-color': '#1DA57A' 60 | }, 61 | javascriptEnabled: true 62 | } 63 | } 64 | } 65 | }) 66 | } 67 | --------------------------------------------------------------------------------