├── .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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------