mac 6 months ago
commit
6d5a5d7d71
  1. BIN
      .DS_Store
  2. 11
      .editorconfig
  3. 2
      .env
  4. 12
      .eslintignore
  5. 58
      .eslintrc copy.js
  6. 208
      .gitattributes
  7. 15
      .gitignore
  8. 15
      .prettierrc.js
  9. 94
      CODE_OF_CONDUCT.md
  10. 21
      LICENSE
  11. 100
      README.md
  12. 3
      config-overrides.js
  13. 82
      config/webpack.config.js
  14. 1
      env
  15. 48112
      package-lock.json
  16. 95
      package.json
  17. 8
      paths.json
  18. BIN
      public/favicon.ico
  19. 57
      public/index.html
  20. 15
      public/manifest.json
  21. 9914
      public/static/color.less
  22. 17
      public/static/less.min.js
  23. 3
      src-tauri/.gitignore
  24. 3496
      src-tauri/Cargo.lock
  25. 26
      src-tauri/Cargo.toml
  26. 3
      src-tauri/build.rs
  27. BIN
      src-tauri/icons/128x128.png
  28. BIN
      src-tauri/icons/128x128@2x.png
  29. BIN
      src-tauri/icons/32x32.png
  30. BIN
      src-tauri/icons/Square107x107Logo.png
  31. BIN
      src-tauri/icons/Square142x142Logo.png
  32. BIN
      src-tauri/icons/Square150x150Logo.png
  33. BIN
      src-tauri/icons/Square284x284Logo.png
  34. BIN
      src-tauri/icons/Square30x30Logo.png
  35. BIN
      src-tauri/icons/Square310x310Logo.png
  36. BIN
      src-tauri/icons/Square44x44Logo.png
  37. BIN
      src-tauri/icons/Square71x71Logo.png
  38. BIN
      src-tauri/icons/Square89x89Logo.png
  39. BIN
      src-tauri/icons/StoreLogo.png
  40. BIN
      src-tauri/icons/icon.icns
  41. BIN
      src-tauri/icons/icon.ico
  42. BIN
      src-tauri/icons/icon.png
  43. 8
      src-tauri/src/main.rs
  44. 63
      src-tauri/tauri.conf.json
  45. 18
      src/App.tsx
  46. 58
      src/api/index.ts
  47. 11
      src/app.css
  48. 157
      src/assets/css/public.less
  49. 69
      src/assets/img/login-bg.svg
  50. BIN
      src/assets/img/logo.png
  51. 1
      src/assets/img/logo.svg
  52. 459
      src/assets/js/publicFunc.ts
  53. 42
      src/components/BreadCrumb/index.tsx
  54. 29
      src/components/Header/Header.module.less
  55. 97
      src/components/Header/index.tsx
  56. 8
      src/components/Menu/Menu.module.less
  57. 145
      src/components/Menu/index.tsx
  58. 7
      src/components/MyIconfont/index.tsx
  59. 55
      src/components/MySelect/index.tsx
  60. 305
      src/components/MyTable/index.tsx
  61. 77
      src/components/SearchForm/index.tsx
  62. 8
      src/components/TabPanes/TabPanes.module.less
  63. 319
      src/components/TabPanes/index.tsx
  64. 26
      src/index.tsx
  65. 34
      src/pages/container/Home.module.less
  66. 136
      src/pages/container/index.tsx
  67. 38
      src/pages/home/index.css
  68. 47
      src/pages/home/index.less
  69. 9
      src/pages/home/index.tsx
  70. BIN
      src/pages/home/vector.png
  71. 100
      src/pages/login/index.tsx
  72. 36
      src/pages/login/login.less
  73. 226
      src/pages/notify/news.tsx
  74. 148
      src/pages/notify/system.tsx
  75. 13
      src/pages/public/errorPage/index.tsx
  76. 344
      src/pages/swiper/index.tsx
  77. 12
      src/react-app-env.d.ts
  78. 48
      src/route/routes.ts
  79. 27
      src/store/actionTypes/index.ts
  80. 7
      src/store/actions/index.ts
  81. 25
      src/store/index.ts
  82. 21
      src/store/reducers/index.ts
  83. 25
      src/store/state/index.ts
  84. 9
      src/test/App.test.jsx
  85. 7
      src/types/store.d.ts
  86. 111
      src/utils/axios.ts
  87. 49
      src/utils/index.ts
  88. 49
      src/utils/tableHook.ts
  89. 26
      src/utils/userIdHook.ts
  90. 52
      tsconfig.json
  91. 23
      tslint.json
  92. 18
      typings/global.d.ts

BIN
.DS_Store

11
.editorconfig

@ -0,0 +1,11 @@
# 🎨 editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

2
.env

@ -0,0 +1,2 @@
REACT_APP_BASE_URL=http://162.254.37.253:8082/api/v1
SKIP_PREFLIGHT_CHECK=true

12
.eslintignore

@ -0,0 +1,12 @@
src/registerServiceWorker.js
src/**/test/**
config-overrides.js
/build
/config
src/serviceWorker.js
/test.jsx
react-app-env.d.ts
/typings
!.eslintrc.js
echarts-dark-theme.*
/public

58
.eslintrc copy.js

@ -0,0 +1,58 @@
module.exports = {
extends: [
'eslint:all',
'react-app', // react帮配置好了一些语法,譬如箭头函数
'airbnb',
'plugin:prettier/recommended' // prettier配置
],
rules: {
'import/no-extraneous-dependencies': 0,
'import/extensions': 'off',
'import/no-unresolved': 0,
'default-param-last': 0,
'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx', 'tsx'] }], // 关闭airbnb对于jsx必须写在jsx文件中的设置
'react/prop-types': 'off', // 关闭airbnb对于必须添加prop-types的校验
'react/destructuring-assignment': [
1,
'always',
{
ignoreClassFields: false
}
],
'react/jsx-one-expression-per-line': 'off', // 关闭要求一个表达式必须换行的要求,和Prettier冲突了
'react/jsx-wrap-multilines': 0, // 关闭要求jsx属性中写jsx必须要加括号,和Prettier冲突了
'comma-dangle': ['error', 'never'],
'react/jsx-first-prop-new-line': [1, 'multiline-multiprop'],
'react/prefer-stateless-function': [0, { ignorePureComponents: true }],
'jsx-a11y/no-static-element-interactions': 'off', // 关闭非交互元素加事件必须加 role
'jsx-a11y/click-events-have-key-events': 'off', // 关闭click事件要求有对应键盘事件
'no-bitwise': 'off', // 不让用位操作符,不知道为啥,先关掉
'react/jsx-indent': [2, 2],
'react/jsx-no-undef': [2, { allowGlobals: true }],
'jsx-control-statements/jsx-use-if-tag': 0,
'react/no-array-index-key': 0,
'react/jsx-props-no-spreading': 0,
// 禁止使用 var
'no-var': 'error',
semi: ['error', 'never'],
quotes: [2, 'single'],
// @fixable 必须使用 === 或 !==,禁止使用 == 或 !=,与 null 比较时除外
eqeqeq: [
'warn',
'always',
{
null: 'ignore'
}
],
'no-use-before-define': ['error', { functions: false }],
'prettier/prettier': ['error', { parser: 'flow' }]
},
overrides: [
{
files: ['**/Mi/*.js', '**/Mi/*.jsx'],
rules: {
'react/prop-types': 'error' // Mi 文件夹下的是系统组件,必须写prop-types
}
}
]
}

208
.gitattributes

@ -0,0 +1,208 @@
# Set the default behavior, in case people don't have core.autolf set.
* text eol=lf
## GITATTRIBUTES FOR WEB PROJECTS
#
# These settings are for any web project.
#
# Details per file setting:
# text These files should be normalized (i.e. convert CRLF to LF).
# binary These files are binary and should be left untouched.
#
# Note that binary is a macro for -text -diff.
######################################################################
# Auto detect
## Handle line endings automatically for files detected as
## text and leave all files detected as binary untouched.
## This will handle all files NOT defined below.
* text=auto
# Source code
*.bash text eol=lf
*.bat text eol=lf
*.cmd text eol=lf
*.coffee text
*.css text
*.htm text diff=html
*.html text diff=html
*.inc text
*.ini text
*.js text
*.json text
*.jsx text
*.less text
*.ls text
*.map text -diff
*.od text
*.onlydata text
*.php text diff=php
*.pl text
*.ps1 text eol=lf
*.py text diff=python
*.rb text diff=ruby
*.sass text
*.scm text
*.scss text diff=css
*.sh text eol=lf
*.sql text
*.styl text
*.tag text
*.ts text
*.tsx text
*.xml text
*.xhtml text diff=html
# Docker
*.dockerignore text
Dockerfile text
# Documentation
*.ipynb text
*.markdown text
*.md text
*.mdwn text
*.mdown text
*.mkd text
*.mkdn text
*.mdtxt text
*.mdtext text
*.txt text
AUTHORS text
CHANGELOG text
CHANGES text
CONTRIBUTING text
COPYING text
copyright text
*COPYRIGHT* text
INSTALL text
license text
LICENSE text
NEWS text
readme text
*README* text
TODO text
# Templates
*.dot text
*.ejs text
*.haml text
*.handlebars text
*.hbs text
*.hbt text
*.jade text
*.latte text
*.mustache text
*.njk text
*.phtml text
*.tmpl text
*.tpl text
*.twig text
*.vue text
# Linters
.csslintrc text
.eslintrc text
.htmlhintrc text
.jscsrc text
.jshintrc text
.jshintignore text
.stylelintrc text
# Configs
*.bowerrc text
*.cnf text
*.conf text
*.config text
.babelrc text
.browserslistrc text
.editorconfig text
.env text
.gitattributes text
.gitconfig text
.htaccess text
*.lock text -diff
package-lock.json text -diff
*.npmignore text
*.yaml text
*.yml text
browserslist text
Makefile text
makefile text
# Heroku
Procfile text
.slugignore text
# Graphics
*.ai binary
*.bmp binary
*.eps binary
*.gif binary
*.gifv binary
*.ico binary
*.jng binary
*.jp2 binary
*.jpg binary
*.jpeg binary
*.jpx binary
*.jxr binary
*.pdf binary
*.png binary
*.psb binary
*.psd binary
# SVG treated as an asset (binary) by default.
*.svg text
# If you want to treat it as binary,
# use the following line instead.
# *.svg binary
*.svgz binary
*.tif binary
*.tiff binary
*.wbmp binary
*.webp binary
# Audio
*.kar binary
*.m4a binary
*.mid binary
*.midi binary
*.mp3 binary
*.ogg binary
*.ra binary
# Video
*.3gpp binary
*.3gp binary
*.as binary
*.asf binary
*.asx binary
*.fla binary
*.flv binary
*.m4v binary
*.mng binary
*.mov binary
*.mp4 binary
*.mpeg binary
*.mpg binary
*.ogv binary
*.swc binary
*.swf binary
*.webm binary
# Archives
*.7z binary
*.gz binary
*.jar binary
*.rar binary
*.tar binary
*.zip binary
# Fonts
*.ttf binary
*.eot binary
*.otf binary
*.woff binary
*.woff2 binary
# Executables
*.exe binary
*.pyc binary

15
.gitignore

@ -0,0 +1,15 @@
/node_modules
.idea
build
.env.*
.env.development
.env.production
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

15
.prettierrc.js

@ -0,0 +1,15 @@
module.exports = {
printWidth: 80,
tabWidth: 2,
trailingComma: 'none', //尾随逗号
bracketSpacing: true,
jsxBracketSameLine: false,
arrowParens: 'always',
parser: 'typescript',
semi: false,
singleQuote: true,
jsxSingleQuote: false,
endOfLine: 'lf',
parser: 'babel',
proseWrap: 'always'
}

94
CODE_OF_CONDUCT.md

@ -0,0 +1,94 @@
# Citizen Code of Conduct
## 1. Purpose
A primary goal of React Antd Multi Tabs Admin is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof).
This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior.
We invite all those who participate in React Antd Multi Tabs Admin to help us create safe and positive experiences for everyone.
## 2. Open [Source/Culture/Tech] Citizenship
A supplemental goal of this Code of Conduct is to increase open [source/culture/tech] citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community.
Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society.
If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know.
## 3. Expected Behavior
The following behaviors are expected and requested of all community members:
* Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community.
* Exercise consideration and respect in your speech and actions.
* Attempt collaboration before conflict.
* Refrain from demeaning, discriminatory, or harassing behavior and speech.
* Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential.
* Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations.
## 4. Unacceptable Behavior
The following behaviors are considered harassment and are unacceptable within our community:
* Violence, threats of violence or violent language directed against another person.
* Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language.
* Posting or displaying sexually explicit or violent material.
* Posting or threatening to post other people's personally identifying information ("doxing").
* Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability.
* Inappropriate photography or recording.
* Inappropriate physical contact. You should have someone's consent before touching them.
* Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances.
* Deliberate intimidation, stalking or following (online or in person).
* Advocating for, or encouraging, any of the above behavior.
* Sustained disruption of community events, including talks and presentations.
## 5. Weapons Policy
No weapons will be allowed at React Antd Multi Tabs Admin events, community spaces, or in other spaces covered by the scope of this Code of Conduct. Weapons include but are not limited to guns, explosives (including fireworks), and large knives such as those used for hunting or display, as well as any other item used for the purpose of causing injury or harm to others. Anyone seen in possession of one of these items will be asked to leave immediately, and will only be allowed to return without the weapon. Community members are further expected to comply with all state and local laws on this matter.
## 6. Consequences of Unacceptable Behavior
Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated.
Anyone asked to stop unacceptable behavior is expected to comply immediately.
If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event).
## 7. Reporting Guidelines
If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. 582631730@qq.com.
Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress.
## 8. Addressing Grievances
If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies.
## 9. Scope
We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues--online and in-person--as well as in all one-on-one communications pertaining to community business.
This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members.
## 10. Contact info
582631730@qq.com
## 11. License and attribution
The Citizen Code of Conduct is distributed by [Stumptown Syndicate](http://stumptownsyndicate.org) under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/).
Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy).
_Revision 2.3. Posted 6 March 2017._
_Revision 2.2. Posted 4 February 2016._
_Revision 2.1. Posted 23 June 2014._
_Revision 2.0, adopted by the [Stumptown Syndicate](http://stumptownsyndicate.org) board on 10 January 2013. Posted 17 March 2013._

21
LICENSE

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 bigfool-cn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

100
README.md

@ -0,0 +1,100 @@
# React Antd Admin 管理后台
react + antd + typescript 集成轻量级管理后台
## 注意
node version require 16.19.0
修改config中的配置 dev 测试,prod生成
## 功能
- 多页标签
- 动态权限
- 主题配置
## 使用
用户名:admin 密码:123456
### 使用命令行
```bash
npm install -g typescript
yarn install
yarn start
cp env .env.development
cp env .env.production
```
### Redux 的使用说明
```
# 在/src/store/actionTypes/index1.tsx 定义新字段,格式如下
export default {
...,
SET_ACTION: {
name: 'SET_ACTION',
field: 'action'
}
}
# 在/src/store/state/index1.tsx 也定义新字段,格式如下
interface StoreState {
...;
action: string;
}
const initState: StoreState = {
...,
action: ''
}
# 在要使用的组件中
import { connect } from 'react-redux'
import * as actions from '@/store/actions'
export default connect(
(state) => state,
actions
)(ComponentName)
# 然后在 props 就有 setStoreData 属性,可用来 dispatch
setStoreData('SET_ACTION', '')
# 只需要定义 type 和 state,不需要写每个action,效率提高了木有有!!!
```
### 路由/菜单配置
```
# 所有路由写在 /src/route/routes.ts (包括菜单栏的路由)
用于路由权限控制
# 左侧菜单路由写在 /src/config/menu.ts
仅用于菜单栏展示
# 分两套的原因是,方便维护,如果不嫌麻烦,可以都写在 routes 里,用一个字段标识菜单路由即可
```
### 关于换肤配置
> 本框架是使用 less.js 实现动态切换主题,js文件在 /public/less.min.js
```
# 主题配置文件在 /public/color.less
引用了 antd 组件后,基本不需要自己额外自定义主题样式,因为主题文件里都有。
但是!!!
如果自己写了自定义组件,切换主题后样式显示不正常,
则需要自己在 color.less 底部添加深浅主题对应的样式,具体参考主题文件内额外配置。
```
```bash
# 上传生产环境
yarn deploy:prod:dc
# 访问路径
https://filefast.io/OTUxNDdiZW/#/login
上传命令:yarn deploy:prod:dc
代码路径:/data/wwwroot/admin-filefast
访问路径:
https://filefast.io/OTUxNDdiZW/#/login
```

3
config-overrides.js

@ -0,0 +1,3 @@
const webpackConfig = require('./config/webpack.config')
module.exports = webpackConfig

82
config/webpack.config.js

@ -0,0 +1,82 @@
//用于修改webpack默认配置
const { override, addWebpackAlias, fixBabelImports, addLessLoader, addBabelPlugin } = require('customize-cra')
const CompressionWebpackPlugin = require('compression-webpack-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
.BundleAnalyzerPlugin
const webpack = require('webpack')
const path = require('path')
const darkThemeVars = require('antd/dist/dark-theme');
// 分析打包大小
const addAnalyze = () => (config) => {
let plugins = [new BundleAnalyzerPlugin({ analyzerPort: 7777 })]
config.plugins = [...config.plugins, ...plugins]
return config
}
// 打包体积优化
const addOptimization = () => (config) => {
if (process.env.NODE_ENV === 'production') {
config.optimization = {
splitChunks: {
chunks: 'all',
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: 10
},
default: {
minChunks: 2,
priority: -10,
reuseExistingChunk: true
}
}
}
}
// 关闭sourceMap
config.devtool = false
// 添加js打包gzip配置
config.plugins.push(
new CompressionWebpackPlugin({
test: /\.js$|\.css$/,
threshold: 1024
}),
new webpack.optimize.AggressiveMergingPlugin(), //合并块
new webpack.optimize.ModuleConcatenationPlugin()
)
}
return config
}
module.exports = override(
// addAnalyze(),
// 配置路径别名
addWebpackAlias({
'@': path.resolve('src')
}),
addOptimization(),
// 针对antd 实现按需打包:根据import来打包 (使用babel-plugin-import)
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true//自动打包相关的样式 默认为 style:'css'
}),
// 使用less-loader对源码重的less的变量进行重新制定,设置antd自定义主题
addLessLoader({
javascriptEnabled: true,
modifyVars: {
'hack': `true;@import "${require.resolve('antd/lib/style/color/colorPalette.less')}";`,
...darkThemeVars,
'@primary-color': '#6e41ff',
},
localIdentName: '[local]--[hash:base64:5]' // use less-modules
})
)

1
env

@ -0,0 +1 @@
REACT_APP_BASE_URL=http://14.29.101.215:30303/api/v1

48112
package-lock.json
File diff suppressed because it is too large
View File

95
package.json

@ -0,0 +1,95 @@
{
"name": "react-antd-admin",
"version": "0.1.0",
"private": true,
"dependencies": {
"@ant-design/icons": "^4.0.6",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/axios": "^0.14.0",
"@types/jest": "^24.0.0",
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@types/react-redux": "^7.1.7",
"@types/react-router-dom": "^5.1.4",
"@types/redux-promise": "^0.5.28",
"antd": "4.2.0",
"axios": "^0.21.1",
"babel-plugin-import": "^1.12.2",
"classnames": "^2.2.6",
"connected-react-router": "^6.5.2",
"cross-env": "^7.0.2",
"dagre": "^0.8.5",
"dayjs": "^1.11.10",
"less": "^3.11.1",
"less-loader": "^5.0.0",
"moment": "^2.24.0",
"react": "^16.13.1",
"react-canvas-nest": "^1.0.10",
"react-dom": "^16.13.1",
"react-flow-renderer": "^10.3.17",
"react-redux": "^7.1.3",
"react-router-breadcrumbs-hoc": "^3.2.4",
"react-router-dom": "^5.1.2",
"react-scripts": "3.2.0",
"redux": "^4.0.4",
"redux-persist": "^6.0.0",
"redux-promise": "^0.6.0",
"typescript": "^4.8.3"
},
"scripts": {
"start": "cross-env PORT=3000 react-app-rewired start",
"test": "react-app-rewired test",
"build": "react-app-rewired build",
"deploy:dev": "npm run build && scp -r ./build/* vps:/home/ubuntu/pzy/filpool",
"deploy:prod": "npm run build && scp -r ./build/* dcfilefast_prod:/data/wwwroot/apk.hippoim.us/admin-web"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"compression-webpack-plugin": "^3.0.1",
"customize-cra": "^0.8.0",
"eslint": "^6.8.0",
"eslint-config-airbnb": "^18.0.1",
"eslint-config-prettier": "^6.10.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-webpack": "^0.11.1",
"eslint-plugin-html": "^6.0.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-react": "^7.18.3",
"eslint-plugin-react-hooks": "^2.3.0",
"husky": "^3.0.9",
"lint-staged": "^9.4.2",
"prettier": "1.18.2",
"react-app-rewired": "^2.1.5",
"redux-devtools": "^3.5.0",
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack-bundle-analyzer": "^3.6.0"
},
"homepage": ".",
"lint-staged": {
"*.{js,jsx}": "eslint"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}
}

8
paths.json

@ -0,0 +1,8 @@
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@/*": ["*"]
}
}
}

BIN
public/favicon.ico

57
public/index.html

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta data-n-head="ssr" name="renderer" content="webkit">
<meta data-n-head="ssr" http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="description" content="React-Antd-Admin管理后台" />
<link rel="apple-touch-icon" href="logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React Antd Admin管理后台</title>
</head>
<body>
<!-- 主题 -->
<link type="text/css" rel="stylesheet/less" href="/static/color.less">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script>
// 切换为当前主题
const themeStyle = localStorage.getItem('themeStyle')
if(themeStyle) {
const styles = document.createElement('style')
styles.id = 'less:color'
styles.innerText = themeStyle
document.body.appendChild(styles)
}
</script>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

15
public/manifest.json

@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

9914
public/static/color.less
File diff suppressed because it is too large
View File

17
public/static/less.min.js
File diff suppressed because it is too large
View File

3
src-tauri/.gitignore

@ -0,0 +1,3 @@
# Generated by Cargo
# will have compiled files and executables
/target/

3496
src-tauri/Cargo.lock
File diff suppressed because it is too large
View File

26
src-tauri/Cargo.toml

@ -0,0 +1,26 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
default-run = "app"
edition = "2021"
rust-version = "1.60"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.4.0", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.4.0", features = [] }
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
# DO NOT REMOVE!!
custom-protocol = [ "tauri/custom-protocol" ]

3
src-tauri/build.rs

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

BIN
src-tauri/icons/128x128.png

After

Width: 128  |  Height: 128  |  Size: 11 KiB

BIN
src-tauri/icons/128x128@2x.png

After

Width: 256  |  Height: 256  |  Size: 23 KiB

BIN
src-tauri/icons/32x32.png

After

Width: 32  |  Height: 32  |  Size: 2.2 KiB

BIN
src-tauri/icons/Square107x107Logo.png

After

Width: 107  |  Height: 107  |  Size: 9.0 KiB

BIN
src-tauri/icons/Square142x142Logo.png

After

Width: 142  |  Height: 142  |  Size: 12 KiB

BIN
src-tauri/icons/Square150x150Logo.png

After

Width: 150  |  Height: 150  |  Size: 13 KiB

BIN
src-tauri/icons/Square284x284Logo.png

After

Width: 284  |  Height: 284  |  Size: 25 KiB

BIN
src-tauri/icons/Square30x30Logo.png

After

Width: 30  |  Height: 30  |  Size: 2.0 KiB

BIN
src-tauri/icons/Square310x310Logo.png

After

Width: 310  |  Height: 310  |  Size: 28 KiB

BIN
src-tauri/icons/Square44x44Logo.png

After

Width: 44  |  Height: 44  |  Size: 3.3 KiB

BIN
src-tauri/icons/Square71x71Logo.png

After

Width: 71  |  Height: 71  |  Size: 5.9 KiB

BIN
src-tauri/icons/Square89x89Logo.png

After

Width: 89  |  Height: 89  |  Size: 7.4 KiB

BIN
src-tauri/icons/StoreLogo.png

After

Width: 50  |  Height: 50  |  Size: 3.9 KiB

BIN
src-tauri/icons/icon.icns

BIN
src-tauri/icons/icon.ico

BIN
src-tauri/icons/icon.png

After

Width: 512  |  Height: 512  |  Size: 49 KiB

8
src-tauri/src/main.rs

@ -0,0 +1,8 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
tauri::Builder::default()
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

63
src-tauri/tauri.conf.json

@ -0,0 +1,63 @@
{
"build": {
"beforeBuildCommand": "npm run build",
"beforeDevCommand": "npm run start",
"devPath": "http://localhost:3000",
"distDir": "http://192.168.124.20:3001"
},
"package": {
"productName": "metatrader-admin",
"version": "0.1.0"
},
"tauri": {
"allowlist": {
"all": false
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"deb": {
"depends": []
},
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "com.react.admin",
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"security": {
"csp": null
},
"updater": {
"active": false
},
"windows": [{
"fullscreen": true,
"minHeight": 600,
"resizable": true,
"title": "metatrader-admin",
"minWidth": 900
}]
}
}

18
src/App.tsx

@ -0,0 +1,18 @@
import React, { FC } from 'react'
import { HashRouter as Router, Route } from 'react-router-dom'
import Container from '@/pages/container'
import Login from '@/pages/login'
import './app.css'
const App: FC = () => (
<Router>
<Route exact path="/login" component={Login} />
<Route
path="/"
key="container"
render={(props: any) => <Container {...props} />}
/>
</Router>
)
export default App

58
src/api/index.ts

@ -0,0 +1,58 @@
import $axios from '@/utils/axios'
export default {
// 获取数据
getList(params?: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/dappList', params)
},
// 登录
login(params: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/login', params)
},
upload_image(file: File): Promise<CommonObjectType<string>> {
const body = new FormData();
console.log(file);
body.append("img", file);
return $axios.post("/admin/uploadImg", body, {
});
},
add_dapp_list(params: object) {
return $axios.post('/admin/addDapp', params)
},
delete_dapp_list(params: object) {
return $axios.post('/admin/delDapp', params)
},
update_dapp_list(params: object) {
return $axios.post('/admin/updDapp', params)
},
get_system_message(params: object) {
return $axios.post('/admin/systemMessage', params)
},
add_system_message(params: object) {
return $axios.post('/admin/addSystemMessage', params)
},
updata_system_message(params: object) {
return $axios.post('/admin/updSystemMessage', params)
},
delete_system_message(params: object) {
return $axios.post('/admin/delSystemMessage', params)
},
get_news_message(params: object) {
return $axios.post('/admin/buzzNewsMessage', params)
},
add_news_message(params: object) {
return $axios.post('/admin/addBuzzNewsMessage', params)
},
updata_news_message(params: object) {
return $axios.post('/admin/updBuzzNewsMessage', params)
},
delete_new_message(params: object) {
return $axios.post('/admin/delBuzzNewsMessage', params)
}
}

11
src/app.css

@ -0,0 +1,11 @@
.text-overflow{
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
text-overflow: ellipsis;
}
.tp{
cursor: pointer;
}

157
src/assets/css/public.less

@ -0,0 +1,157 @@
.App, #root {
// height: 100vh;
overflow: hidden;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.fl {
float: left;
z-index: 8;
}
.fr {
float: right;
z-index: 8;
}
.clearfix:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
.global-spin {
position: fixed;
top: 50%;
left: 50%;
}
/* 滚动条 */
/*滚动条宽度*/
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
/* Handle样式 */
::-webkit-scrollbar-thumb {
border-radius: 10px;
background: rgba(0, 0, 0, 0.2);
}
/*当前窗口未激活的情况下*/
::-webkit-scrollbar-thumb:window-inactive {
background: rgba(0, 0, 0, 0.1);
}
/*hover到滚动条上*/
::-webkit-scrollbar-thumb:vertical:hover {
background-color: rgba(0, 0, 0, 0.3);
}
/*滚动条按下*/
::-webkit-scrollbar-thumb:vertical:active {
background-color: rgba(0, 0, 0, 0.7);
}
.bf-modal {
z-index: 999 !important;
}
input::-webkit-inner-spin-button {
display: none !important;
}
.ant-menu-item.ant-menu-item-only-child .anticon{
margin-right: 10px!important;
}
.ant-layout-sider{
height: 100%;
&::-webkit-scrollbar {
width: 0;
}
}
/* 自动填充样式修改 */
input:-webkit-autofill {
box-shadow: 0 0 0px 1000px #1f1f1f inset !important;
text-fill-color: white;
-webkit-text-fill-color: white;
border-radius: 0;
}
.ant-tabs-bar{
user-select: none;
}
.ant-switch-inner{
margin-right: 6px;
margin-left: 24px;
}
.webTheme{
width: 24px;
height: 24px;
border-radius: 4px;
margin: 20px;
background: @primary-color;
cursor: pointer;
}
.avart {
display: inline-block;
width: 26px;
height: 26px;
background-color: @primary-color;
text-align: center;
line-height: 22px;
border-radius: 50%;
margin-right: 8px;
color: #fff;
}
.ant-layout-content, .ant-layout{
min-height: unset!important;
}
.logo{
position: relative;
padding: 0 24px;
overflow: hidden;
cursor: pointer;
transition: all .3s;
>a{
display: flex;
align-items: center;
height: 64px;
}
img {
display: inline-block;
height: 32px;
vertical-align: middle;
}
h1{
display: inline-block;
margin: 0 0 0 12px;
color: #fff;
font-weight: 600;
font-size: 18px;
white-space: nowrap;
vertical-align: middle;
animation: fade-in;
animation-duration: .3s;
}
}

69
src/assets/img/login-bg.svg

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
<title>Group 21</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Ant-Design-Pro-3.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="账户密码登录-校验" transform="translate(-79.000000, -82.000000)">
<g id="Group-21" transform="translate(77.000000, 73.000000)">
<g id="Group-18" opacity="0.8" transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
<ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479" ry="21.766008"></ellipse>
<ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913" ry="5.21330997"></ellipse>
<path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z" id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"></path>
<path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6" stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7" stroke-width="0.702678964" opacity="0.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9" stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<g id="Group-17" transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)" fill="#CFDAE6">
<ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653" ry="9.12768076"></ellipse>
<path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z" id="Oval-4" transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
</g>
</g>
<g id="Group-14" transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471" ry="29.1402439"></ellipse>
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275" ry="21.5853659"></ellipse>
<ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902" ry="23.7439024"></ellipse>
<ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137" ry="10.7926829"></ellipse>
<path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z" id="Oval-2" fill="#BACAD9"></path>
<g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
<ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
<path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z" id="Oval-2-Copy-2" transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
</g>
<ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275" ry="2.15853659"></ellipse>
<path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6" opacity="0.8"></path>
</g>
<g id="Group-10" opacity="0.799999952" transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
<ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407" ry="11.2941176"></ellipse>
<g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
<ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627" ry="8.55614973"></ellipse>
<path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z" id="Oval-7"></path>
</g>
<path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6" stroke-width="0.941176471"></path>
<ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186" ry="3.29411765"></ellipse>
<ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
<path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6" stroke-width="0.941176471"></path>
</g>
<g id="Group-19" opacity="0.33" transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
<g id="Group-17" transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)" fill="#BACAD9">
<circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
<path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z" id="Oval-4" transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
</g>
<circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
<path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9" stroke-width="1.16666667"></path>
<polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667" points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
<path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7" stroke-width="1.16666667" opacity="0.6"></path>
<path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6" stroke-width="1.16666667"></path>
<circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
<circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
<circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
<circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
<circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
</g>
</g>
</g>
</g>
</svg>

BIN
src/assets/img/logo.png

After

Width: 64  |  Height: 64  |  Size: 1.5 KiB

1
src/assets/img/logo.svg
File diff suppressed because it is too large
View File

459
src/assets/js/publicFunc.ts

@ -0,0 +1,459 @@
/* eslint-disable no-param-reassign */
import { Modal } from 'antd'
import routes from '@/route/routes'
import ErrorPage from '@/pages/public/errorPage'
import { store } from '@/store'
// 通用confirm方法
export const commonConfirm = (title: string, cb: () => void) => {
const { confirm } = Modal
confirm({
okText: '确定',
cancelText: '取消',
title,
onOk() {
cb()
},
onCancel() {}
})
}
/**
*
* @param {string} phone
*/
export const hidePhone = (phone: string) =>
phone && phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
/**
* react router数组
* @param {object[]} arr
* @param {string} child
*/
export const flattenRoutes = (arr: CommonObjectType<unknown>[]) =>
arr.reduce(
(prev: CommonObjectType<unknown>[], item: CommonObjectType<unknown>) => {
if (Array.isArray(item.routes)) {
prev.push(item)
}
return prev.concat(
Array.isArray(item.routes) ? flattenRoutes(item.routes) : item
)
},
[]
)
/**
* name和key
* @param {string} path
*/
export const getKeyName = (path: string = '/403') => {
const truePath = path.split('?')[0];
const curRoute = flattenRoutes(routes).filter(
(item: { path: string | string[] }) => item.path.includes(truePath)
)
if (!curRoute[0])
return { title: '暂无权限', tabKey: '403', component: ErrorPage }
const { name, key, component } = curRoute[0]
return { title: name, tabKey: key, component }
}
/**
* Currying
* @param {*} action
* @param {function} cb
*/
export const asyncAction = (action: unknown) => {
const wait = new Promise((resolve) => {
resolve(action)
})
return (cb: () => void) => {
wait.then(() => setTimeout(() => cb()))
}
}
/**
*
* @param {object} history history对象new新实例
* @param {string} returnUrl
* @param {function} cb
*/
export const closeTabAction = (
history: CommonObjectType,
returnUrl: string = '/',
cb?: () => void
) => {
const { curTab } = store.getState().storeData
const { href } = window.location
const pathname = href.split('#')[1]
// 删除tab
const tabArr = JSON.parse(JSON.stringify(curTab))
const delIndex = tabArr.findIndex((item: string) => item === pathname)
tabArr.splice(delIndex, 1)
// 如果要返回的页面被关闭了,再加进去
if (!tabArr.includes(returnUrl)) {
tabArr.push(returnUrl)
}
// 储存新的tabs数组
const setTab = store.dispatch({
type: 'SET_CURTAB',
payload: tabArr
})
// 刷新tab
const reloadTab = store.dispatch({
type: 'SET_RELOADPATH',
payload: returnUrl
})
// 停止刷新tab
const stopReload = setTimeout(() => {
store.dispatch({
type: 'SET_RELOADPATH',
payload: 'null'
})
}, 500)
const action = () => setTab && reloadTab && stopReload
// 刷新回调
const callback = () => {
if (cb && typeof cb === 'function') {
return cb
}
return history.push({
pathname: returnUrl
})
}
asyncAction(action)(callback)
}
/**
* ?
*/
export const getQuery = (): CommonObjectType<string> => {
const { href } = window.location
const query = href.split('?')
if (!query[1]) return {}
const queryArr = decodeURI(query[1]).split('&')
const queryObj = queryArr.reduce((prev, next) => {
const item = next.split('=')
return { ...prev, [item[0]]: item[1] }
}, {})
return queryObj
}
/**
* JSON.parse(JSON.stringify()) [...]/{...}
* @param {object} obj
*/
export const deepClone = (obj: CommonObjectType) => {
if (
obj === null ||
typeof obj !== 'object' ||
obj instanceof Date ||
obj instanceof Function
) {
return obj
}
const cloneObj = Array.isArray(obj) ? [] : {}
Object.keys(obj).map((key) => {
cloneObj[key] = deepClone(obj[key])
return cloneObj
})
return cloneObj
}
/**
*
* @param {*} html
*/
export const getImgsUrl = (html?: string) => {
// 匹配图片(g表示匹配所有结果i表示区分大小写)
const imgReg = /<img.*?(?:>|\/>)/gi
// 匹配src属性
const srcReg = /src=['"]?([^'"]*)['"]?/i
const arr = html.match(imgReg)
if (!arr) return null
// 获取图片地址
const urlArr = arr.reduce((prev, next) => {
const src = next.match(srcReg)
return src[1] ? [...prev, src[1]] : prev
}, [])
return urlArr
}
/**
*
* @param {*} html
*/
export const getVideoUrl = (html?: string) => {
// 匹配图片(g表示匹配所有结果i表示区分大小写)
const imgReg = /<(video|iframe).*?(?:>|\/>)/gi
// 匹配src属性
const srcReg = /src=['"]?([^'"]*)['"]?/i
const arr = html.match(imgReg)
if (!arr) return null
// 获取图片地址
const urlArr = arr.reduce((prev, next) => {
const src = next.match(srcReg)
return src[1] ? [...prev, src[1]] : prev
}, [])
return urlArr
}
/**
*
*/
export const getPermission = () => localStorage.getItem('permissions') || []
/**
*
*/
export const isAuthorized = (val: string): boolean => {
const permissions = getPermission();
return permissions.includes(val)
}
/**
* requestAnimationFrame替代setTimeoutsetInterval
* @export
* @param {*} cb
* @param {*} interval
*/
export const customizeTimer = {
intervalTimer: null,
timeoutTimer: null,
setTimeout(cb: () => void, interval: number) {
// 实现setTimeout功能
const { now } = Date
const stime = now()
let etime = stime
const loop = () => {
this.timeoutTimer = requestAnimationFrame(loop)
etime = now()
if (etime - stime >= interval) {
cb()
cancelAnimationFrame(this.timeoutTimer)
}
}
this.timeoutTimer = requestAnimationFrame(loop)
return this.timeoutTimer
},
clearTimeout() {
cancelAnimationFrame(this.timeoutTimer)
},
setInterval(cb: () => void, interval: number) {
// 实现setInterval功能
const { now } = Date
let stime = now()
let etime = stime
const loop = () => {
this.intervalTimer = requestAnimationFrame(loop)
etime = now()
if (etime - stime >= interval) {
stime = now()
etime = stime
cb()
}
}
this.intervalTimer = requestAnimationFrame(loop)
return this.intervalTimer
},
clearInterval() {
cancelAnimationFrame(this.intervalTimer)
}
}
/**
*
*/
export const previewImg = (children: string | React.ReactNode) => {
Modal.info({
title: '预览',
icon: false,
okText: '关闭',
maskClosable: true,
content: children
})
}
/**
* ±
* @param {string} val
*/
export const limitDecimal = (val: string) =>
val.replace(/^(-)*(\d+)\.(\d\d).*$/, '$1$2.$3')
/**
*
*/
export const setUserInfo = (
userInfo: CommonObjectType,
action: (arg0: string, arg1: unknown) => unknown,
oldToken?: string
) => {
const { permission, userName, token } = userInfo
console.log(permission);
const permissionArray = permission.reduce(
(prev: CommonObjectType<string>[], next: CommonObjectType<string>) => [
...prev,
next.code
],
[]
);
localStorage.setItem('permissions', permissionArray)
const result = {
userName,
permission,
token: token || oldToken
}
action('SET_USERINFO', result)
}
/**
* tree结构数据
* @param data
* @param parentIdField
*/
export const makeTree = (
data: Array<any>,
parentIdField: string = 'parentId'
): Array<any> => {
const dataMap: Object = {}
data.forEach((item) => {
dataMap[item.id] = item
})
const dataTree: Array<any> = []
data.forEach((item) => {
const parent = dataMap[item[parentIdField]]
if (parent) {
if (parent.children) {
parent.children.push(item)
} else {
parent.children = []
parent.children.push(item)
}
} else {
dataTree.push(item)
}
})
return dataTree
}
/**
* TreeSelect组件结构数据
* @param dataTree
* @param treeValueFiled
* @param treeTitleFiled
*/
export const makeTreeSelectData = (
dataTree: Array<any>,
treeValueFiled: string = 'value',
treeTitleFiled: string = 'title'
): Array<any> => {
const treeSelect: Array<any> = []
dataTree.forEach((item) => {
const temp = {
value: item[treeValueFiled],
title: item[treeTitleFiled],
children: item.children || undefined
}
if (temp.children) {
temp.children = makeTreeSelectData(
temp.children,
treeValueFiled,
treeTitleFiled
)
}
treeSelect.push(temp)
})
return treeSelect
}
/**
* Tree组件结构数据
* @param dataTree
* @param treeKeyFiled
* @param treeTitleFiled
*/
export const makeTreeData = (
dataTree: Array<any>,
treeKeyFiled: string = 'key',
treeTitleFiled: string = 'title'
): Array<any> => {
const treeSelect: Array<any> = []
dataTree.forEach((item) => {
const temp = {
key: item[treeKeyFiled],
title: item[treeTitleFiled],
children: item.children || undefined
}
if (temp.children) {
temp.children = makeTreeData(
temp.children,
treeKeyFiled,
treeTitleFiled
)
}
treeSelect.push(temp)
})
return treeSelect
}
/**
* id数组
* @param tree
* @param id
*/
export const treeFindParentById = (tree: Array<any>, id: string | number): Array<number | string> => {
const path = []
if (tree.length === 0) return []
const forFn = function (tree, id) {
for (let i = 0; i < tree.length; i++) {
const data = tree[i]
if (data.id === id) {
return path
}
path.push(data.id)
if (data.children) {
const findChildren = forFn(data.children, id);
if (findChildren && findChildren.length > 0) return findChildren
}
path.pop()
}
}
forFn(tree, id)
return path
}
/**
*
* @param arr1
* @param arr2
*/
export const diffArray = (arr1: Array<number | string>, arr2: Array<number | string>): Array< number | string>=> {
const diff: Array<number | string> = []
for (let idx = 0; idx < arr1.length; idx++) {
if (!arr2.includes(arr1[idx])) {
diff.push(arr1[idx])
}
}
return diff
}

42
src/components/BreadCrumb/index.tsx

@ -0,0 +1,42 @@
import React, { FC } from 'react'
import { useHistory } from 'react-router-dom'
import withBreadcrumbs from 'react-router-breadcrumbs-hoc'
import { Breadcrumb, Button } from 'antd'
import routes from '@/route/routes'
import { flattenRoutes } from '@/assets/js/publicFunc'
const allRoutes = flattenRoutes(routes)
interface Props {
breadcrumbs: any[];
}
// 通用面包屑
const Breadcrumbs: FC<Props> = ({ breadcrumbs }) => {
const history = useHistory()
return (
<Breadcrumb style={{ display: 'inline-block' }}>
{breadcrumbs.map((bc: CommonObjectType, index: number) => {
return (
<Breadcrumb.Item key={bc.key}>
<Button
disabled={
(!bc.exact && bc.match.path !== '/') ||
index === breadcrumbs.length - 1
}
onClick={() => {
history.push(bc.match.path)
}}
style={{ padding: '0' }}
type="link"
>
{bc.name}
</Button>
</Breadcrumb.Item>
)
})}
</Breadcrumb>
)
}
export default withBreadcrumbs(allRoutes)(Breadcrumbs)

29
src/components/Header/Header.module.less

@ -0,0 +1,29 @@
.header {
width: 100%;
background: @component-background;
height: 64px;
padding: 0;
box-shadow: 0 0.3px 0.9px rgba(0,0,0,0.12), 0 1.6px 3.6px rgba(0,0,0,0.12);
z-index: 2;
}
.content {
height: 64px;
display: flex;
align-items: center;
cursor: pointer;
padding: 0 30px;
transition: all .3s;
&:hover {
background: rgba(0, 0, 0, .025);
}
}
.toggleMenu{
display: inline-block;
height: 64px;
line-height: 64px;
padding: 0 24px;
font-size: 20px;
cursor: pointer;
transition: all .3s,padding 0s;
}

97
src/components/Header/index.tsx

@ -0,0 +1,97 @@
import React, { useEffect, FC } from 'react'
import { useHistory } from 'react-router-dom'
import { Menu, Dropdown, Layout } from 'antd'
import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons'
import Breadcrumb from '@/components/BreadCrumb'
import { connect } from 'react-redux'
import * as actions from '@/store/actions'
import style from './Header.module.less'
interface Props extends ReduxProps {}
const Header: FC<Props> = ({
storeData: { collapsed, theme, userInfo },
setStoreData
}) => {
const history = useHistory()
const { userName = '-' } = userInfo
const firstWord = userName.slice(0, 1)
const logout = async () => {
await setStoreData('SET_USERINFO', {})
history.replace({ pathname: '/login' })
}
const changeTheme = (themes: string) => {
setStoreData('SET_THEME', themes)
}
const menu = (
<Menu>
<Menu.Item onClick={logout}>
<span>退</span>
</Menu.Item>
</Menu>
)
const changeMenu = (
<Menu>
<Menu.Item onClick={() => changeTheme('')}>
<span></span>
</Menu.Item>
<Menu.Item onClick={() => changeTheme('default')}>
<span></span>
</Menu.Item>
</Menu>
)
const toggle = (): void => {
setStoreData('SET_COLLAPSED', !collapsed)
}
// 更换主题
useEffect(() => {
if (theme === 'default') {
const script = document.createElement('script')
script.id = 'themeJs'
script.src = '/static/less.min.js'
document.body.appendChild(script)
setTimeout(() => {
const themeStyle = document.getElementById('less:color')
if (themeStyle) localStorage.setItem('themeStyle', themeStyle.innerText)
}, 500)
} else {
const themeJs = document.getElementById('themeJs')
const themeStyle = document.getElementById('less:color')
if (themeJs) themeJs.remove()
if (themeStyle) themeStyle.remove()
localStorage.removeItem('themeStyle')
}
}, [theme])
return (
<Layout.Header className={style.header}>
<div className={style.toggleMenu} onClick={toggle}>
{collapsed ? (
<MenuUnfoldOutlined className={style.trigger} />
) : (
<MenuFoldOutlined className={style.trigger} />
)}
</div>
<Breadcrumb />
<Dropdown className={`fr ${style.content}`} overlay={menu}>
<span className={style.user}>
<span className="avart">{firstWord}</span>
<span>{userName}</span>
</span>
</Dropdown>
<Dropdown overlay={changeMenu}>
<div title="更换主题" className="fr webTheme" />
</Dropdown>
</Layout.Header>
)
}
export default connect(
(state) => state,
actions
)(Header)

8
src/components/Menu/Menu.module.less

@ -0,0 +1,8 @@
.noselect {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

145
src/components/Menu/index.tsx

@ -0,0 +1,145 @@
import React, { useState, useEffect, useCallback, FC } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { Menu, Layout } from 'antd'
import MyIconFont from '@/components/MyIconfont'
import { getKeyName, flattenRoutes } from '@/assets/js/publicFunc'
import menus from '@/route/routes'
import logo from '@/assets/img/logo.png'
import { connect } from 'react-redux'
import * as actions from '@/store/actions'
import styles from './Menu.module.less'
const { SubMenu } = Menu
const flatMenu = flattenRoutes(menus)
interface Props extends ReduxProps { }
type MenuType = CommonObjectType<string>
const MenuView: FC<Props> = ({ storeData: { theme, userInfo, collapsed } }) => {
const { pathname } = useLocation()
const { tabKey: curKey = 'home' } = getKeyName(pathname)
const [current, setCurrent] = useState(curKey)
const { permission = [] } = userInfo
// 递归逐级向上获取最近一级的菜单,并高亮
const higherMenuKey = useCallback(
(checkKey = 'home', path = pathname) => {
const higherKey = checkKey
if (
checkKey === '403' ||
flatMenu.some((item: MenuType) => item.key === checkKey)
) {
return higherKey
}
const higherPath = path.match(/(.*)\//g)[0].replace(/(.*)\//, '$1')
const { tabKey } = getKeyName(higherPath)
return higherMenuKey(tabKey, higherPath)
},
[pathname]
)
useEffect(() => {
const { tabKey } = getKeyName(pathname)
const higherKey = higherMenuKey(tabKey)
setCurrent(higherKey)
}, [higherMenuKey, pathname])
// 菜单点击事件
const handleClick = ({ key }): void => {
setCurrent(key)
}
// 子菜单的标题
const subMenuTitle = (data: MenuType): JSX.Element => {
const { icon: MenuIcon, iconfont } = data
return (
<span>
{iconfont ? (
<MyIconFont type={iconfont} style={{ fontSize: '14px' }} />
) : (
!!MenuIcon && <MenuIcon />
)}
<span className={styles.noselect}>{data.name}</span>
</span>
)
}
// 创建可跳转的多级子菜单
const createMenuItem = (data: MenuType): JSX.Element => {
return (
<Menu.Item className={styles.noselect} key={data.key} title={data.name}>
<Link to={data.path}>{subMenuTitle(data)}</Link>
</Menu.Item>
)
}
// 创建可展开的第一级子菜单
const creatSubMenu = (data: CommonObjectType): JSX.Element => {
const menuItemList = [];
data.routes.map((item: MenuType) => {
// const arr = permission.filter((ele: MenuType) => item.key === ele.code);
// if (arr.length > 0) {
!item.remove && menuItemList.push(renderMenu(item))
// }
// return arr
})
return menuItemList.length > 0 ? (
<SubMenu key={data.key} title={subMenuTitle(data)}>
{menuItemList}
</SubMenu>
) : createMenuItem(data)
}
// 创建菜单树
const renderMenuMap = (list: CommonObjectType): JSX.Element[] => {
return list.map((item) => !item.remove && renderMenu(item))
}
// 判断是否有子菜单,渲染不同组件
function renderMenu(item: MenuType) {
return item.type === 'subMenu' ? creatSubMenu(item) : createMenuItem(item)
}
const setDefaultKey = flatMenu
.filter((item: MenuType) => item.type === 'subMenu')
.reduce((prev: MenuType[], next: MenuType) => [...prev, next.key], [])
const showKeys = document.body.clientWidth <= 1366 ? [] : setDefaultKey
return (
<Layout.Sider
collapsed={collapsed}
style={{
overflow: 'auto',
height: '100vh',
position: 'fixed',
left: 0,
userSelect: 'none'
}}
width={220}
>
<div className="logo">
<Link to={{ pathname: '/' }}>
<img alt="logo" src={logo} />
{!collapsed && <h1>React Antd Admin</h1>}
</Link>
</div>
<Menu
// defaultOpenKeys={showKeys}
mode="inline"
onClick={handleClick}
selectedKeys={[current]}
theme={theme === 'default' ? 'light' : 'dark'}
>
{renderMenuMap(menus)}
</Menu>
</Layout.Sider>
)
}
export default connect(
(state) => state,
actions
)(MenuView)

7
src/components/MyIconfont/index.tsx

@ -0,0 +1,7 @@
import { createFromIconfontCN } from '@ant-design/icons'
const MyIconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/font_1830242_6vhnrbj73u5.js' // 在 iconfont.cn 上生成
})
export default MyIconFont

55
src/components/MySelect/index.tsx

@ -0,0 +1,55 @@
import React, { FC } from 'react'
import { Select } from 'antd'
const { Option } = Select
interface Props {
data: CommonObjectType<string>[];
placeholder?: string;
value?: string;
onChange?: (arg0: unknown) => void;
width?: string | number;
disabled?: boolean;
onSearch?: () => void;
defaultValue?: string | number;
}
const MySelect: FC<Props> = (props) => {
const {
data,
placeholder = '请输入搜索条件',
value,
onChange = () => {},
width = '100%',
disabled,
onSearch,
defaultValue
} = props
const handerChange = (val: string | number): void => {
onChange(val)
}
return (
<Select
allowClear
disabled={disabled}
onChange={handerChange}
optionFilterProp="children"
placeholder={placeholder}
showSearch
style={{ width }}
value={value ? `${value}` : value}
onSearch={onSearch}
defaultValue={defaultValue}
>
{data.map((item) => (
<Option key={item.key} title={item.name} value={item.key}>
{item.name}
</Option>
))}
</Select>
)
}
export default MySelect

305
src/components/MyTable/index.tsx

@ -0,0 +1,305 @@
import React, {
useState,
forwardRef,
useImperativeHandle,
useRef,
ReactNode,
FC
} from 'react'
import { Table } from 'antd'
import useService from '@/utils/tableHook'
import SearchView from '@/components/SearchForm'
/**
*
* @param {RefType} ref
* @param {object[]} columns
* @param {function} apiFun
* @param {object[]} searchConfigList
* @param {function} beforeSearch
* @param {function} onFieldsChange
* @param {object} extraProps
* @param {function} onSelectRow
* @param {string} rowKey key
* @param {function} sortConfig
* @param {function} expandedRowRender
* @param {function} onExpand
* @param {string} rowClassName
* @param {boolean} small
* @param {string[]} extraPagation
*/
interface TableProps {
columns: object[];
apiFun: (arg0?: unknown[]) => Promise<{}>;
ref?: RefType;
searchConfigList?: object[];
extraProps?: object;
rowKey?: string;
rowClassName?: string;
small?: boolean;
showHeader?: boolean;
extraPagation?: string[];
beforeSearch?: (arg0?: unknown) => void;
onSelectRow?: (arg0?: string[], arg1?: string[]) => void;
onFieldsChange?: (arg0?: unknown, arg1?: unknown) => void;
sortConfig?: (arg0?: object) => any;
expandedRowRender?: () => ReactNode;
onExpand?: () => void;
header?: JSX.Element
}
const MyTable: FC<TableProps> = forwardRef(
(props: TableProps, ref: RefType) => {
/**
* @forwardRef
* ref实例
* ref绑定到子组件自身的节点上.
*/
const searchForm: RefType = useRef(null)
const {
columns,
apiFun,
searchConfigList,
extraProps,
rowKey,
rowClassName,
small,
showHeader,
extraPagation,
beforeSearch,
onSelectRow,
onFieldsChange,
sortConfig,
expandedRowRender,
onExpand,
header
} = props
// 搜索参数,如果有特殊需要处理的参数,就处理
const searchObj = searchConfigList.reduce(
(prev: CommonObjectType, next: CommonObjectType) => {
return Object.assign(prev, {
[next.key]: next.fn ? next.fn(next.initialValue) : next.initialValue
})
},
{}
)
// 初始参数
const initParams = {
...searchObj,
...extraProps,
page: 1,
page_size: 20
}
// 多选框的选择值
const [selectedKeys, setSelectedKeys] = useState([])
// 列表所有的筛选参数(包括搜索、分页、排序等)
const [tableParams, setTableParams] = useState(initParams)
// 列表搜索参数
const [searchParams, setSearchParams] = useState(searchObj)
// 列表排序参数
const [sortParams, setSortParams] = useState({})
// 列表分页参数
const [curPageNo, setCurPageNo] = useState(initParams.page)
const [curPageSize, setCurPageSize] = useState(initParams.page_size)
const { loading = false, response = {} }: CommonObjectType = useService(
apiFun,
tableParams
);
let tableData = [];
let total = 0;
const validData = response?.data ? response.data : [];
if (Array.isArray(validData) && validData.length >= 0) {
tableData = validData;
total = validData.length;
}
if (validData.rows) {
tableData = validData.rows;
total = validData.total;
}
// const validData = response?.data ? response.data : {}
// const { rows: tableData = [], total } = validData
// 执行搜索操作
const handleSearch = (val: CommonObjectType): void => {
if (val.login) {
val.login = Number(val.login)
}
const obj = {
...val
}
if (Array.isArray(obj.chain_id)) {
obj.chain_id = Number(obj.chain_id[0])
}
setSearchParams(obj)
setTableParams({ ...tableParams, ...obj, page: 1 })
}
// 重置列表部分状态
const resetAction = (page?: number): void => {
setSelectedKeys([])
const nextPage = page || curPageNo
const nextParmas = page === 1 ? {} : { ...searchParams, ...sortParams }
setCurPageNo(nextPage)
setTableParams({
...initParams,
...nextParmas,
page: nextPage,
page_size: curPageSize
})
}
// 列表复选框选中变化
const onSelectChange = (
selectedRowKeys: any[],
selectedRows: any[]
): void => {
setSelectedKeys(selectedRowKeys)
onSelectRow(selectedRowKeys, selectedRows)
}
// 复选框配置
const rowSelection = {
selectedRowKeys: selectedKeys,
onChange: onSelectChange
}
// 判断是否有复选框显示
const showCheckbox = onSelectRow ? { rowSelection } : {}
// 展开配置
const expendConfig = {
expandedRowRender,
onExpand,
rowClassName
}
// 判断是否有展开行
const showExpend = expandedRowRender ? expendConfig : {}
// 表格和分页的大小
const tableSize = small ? 'small' : 'middle'
const pagationSize = small ? 'small' : 'default'
// 分页、筛选、排序变化时触发
const onTableChange = (
pagination: CommonObjectType,
filters: CommonObjectType,
sorter: object
): void => {
// 如果有sort排序并且sort参数改变时,优先排序
const sortObj = sortConfig ? sortConfig(sorter) : {}
setSortParams(sortObj)
const { current: page, pageSize } = pagination
setCurPageNo(page)
setCurPageSize(pageSize)
setTableParams({
...initParams,
...searchParams,
...sortObj,
page,
page_size: pageSize
})
}
/**
* @useImperativeHandle
* forwardRef引用父组件的ref实例
* ,
*/
useImperativeHandle(ref, () => ({
// 更新列表
update(page?: number): void {
resetAction(page)
},
// 更新列表,并重置搜索字段
resetForm(page?: number): void {
if (searchForm.current) searchForm.current.resetFields()
setSearchParams({})
resetAction(page)
},
// 仅重置搜索字段
resetField(field?: string[]): void {
return field
? searchForm.current.resetFields([...field])
: searchForm.current.resetFields()
},
// 获取当前列表数据
getTableData(): CommonObjectType[] {
return tableData
}
}))
return (
<div>
{/* 搜索栏 */}
{searchConfigList.length > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<SearchView
ref={searchForm}
config={searchConfigList}
beforeSearch={beforeSearch}
handleSearch={handleSearch}
onFieldsChange={onFieldsChange}
/>
{header}
</div>
)}
{
header && searchConfigList.length <= 0 && <div>{header}</div>
}
{/* 列表 */}
<Table
{...showCheckbox}
{...showExpend}
rowKey={rowKey}
loading={loading}
dataSource={tableData}
columns={columns}
onChange={onTableChange}
size={tableSize}
showHeader={showHeader}
scroll={{ x: 1800 }}
pagination={{
size: pagationSize,
total,
pageSize: tableParams.page_size,
current: tableParams.page,
showQuickJumper: true,
showSizeChanger: true,
pageSizeOptions: ['20', '50', '100', '200', ...extraPagation],
showTotal: (all) => `${all}`
}}
/>
</div>
)
}
)
MyTable.defaultProps = {
searchConfigList: [],
ref: null,
extraProps: {},
rowKey: 'id',
rowClassName: '',
small: false,
showHeader: true,
extraPagation: [],
beforeSearch: () => { },
onSelectRow: () => { },
onFieldsChange: () => { },
sortConfig: () => { },
expandedRowRender: null,
onExpand: () => { }
}
export default MyTable

77
src/components/SearchForm/index.tsx

@ -0,0 +1,77 @@
import React, { forwardRef, useImperativeHandle, FC } from 'react'
import { Form, Button } from 'antd'
interface SearchProps {
config: object[];
handleSearch: (arg0?: object) => void;
ref: RefType;
beforeSearch?: (arg0?: object) => void;
onFieldsChange?: (arg0?: unknown, arg1?: unknown) => void;
}
const SearchForm: FC<SearchProps> = forwardRef(
(props: SearchProps, ref: RefType) => {
const { config, handleSearch, beforeSearch, onFieldsChange } = props
const [form] = Form.useForm()
const getFields = (): JSX.Element[] => {
return config.map((item: CommonObjectType) => {
return (
<Form.Item
key={item.key}
name={item.key}
rules={item.rules}
style={{ marginBottom: '6px' }}
>
{item.slot}
</Form.Item>
)
})
}
const emitSearch = (values: object): void => {
// beforeSearch用于处理一些特殊情况
beforeSearch(values)
handleSearch(values)
}
const initialValues = config.reduce(
(prev: CommonObjectType, next: CommonObjectType) => ({
...prev,
[next.key]: next.initialValue
}),
{}
)
useImperativeHandle(ref, () => ({
// 重置搜索字段
resetFields(field: string[]) {
return field ? form.resetFields([...field]) : form.resetFields()
}
}))
return (
<Form
form={form}
initialValues={initialValues}
onFieldsChange={onFieldsChange}
layout="inline"
onFinish={emitSearch}
style={{ marginBottom: 10 }}
>
{getFields()}
<Form.Item>
<Button htmlType="submit" type="primary">
</Button>
</Form.Item>
</Form>
)
}
)
SearchForm.defaultProps = {
beforeSearch: () => {},
onFieldsChange: () => {}
}
export default SearchForm

8
src/components/TabPanes/TabPanes.module.less

@ -0,0 +1,8 @@
.tabs {
padding: 0 25px;
background: @component-background;
box-shadow: 0 1px 4px rgba(0,0,0,0.12), 0 2px 4px rgba(0,0,0,0.12);
height: 100%;
overflow-y: auto;
padding-top: 10px;
}

319
src/components/TabPanes/index.tsx

@ -0,0 +1,319 @@
import React, {
FC,
useState,
useEffect,
useRef,
useCallback,
Component
} from 'react'
import { useHistory, useLocation } from 'react-router-dom'
import { Tabs, Alert, Dropdown, Menu } from 'antd'
import Home from '@/pages/home'
import { getKeyName, isAuthorized } from '@/assets/js/publicFunc'
import { SyncOutlined } from '@ant-design/icons'
import { connect } from 'react-redux'
import * as actions from '@/store/actions'
import style from './TabPanes.module.less'
const { TabPane } = Tabs
const initPane = [
{
title: '首页',
key: 'home',
content: Home,
closable: false,
path: '/'
}
]
interface Props extends ReduxProps {
defaultActiveKey: string;
panesItem: {
title: string,
content: Component,
key: string,
closable: boolean,
path: string
};
tabActiveKey: string;
}
// 多页签组件
const TabPanes: FC<Props> = (props) => {
const [activeKey, setActiveKey] = useState<string>('')
const [panes, setPanes] = useState<CommonObjectType[]>(initPane)
const [isReload, setIsReload] = useState<boolean>(false)
const [selectedPanel, setSelectedPanel] = useState<CommonObjectType>({})
const pathRef: RefType = useRef<string>('')
const {
storeData: { curTab, reloadPath },
setStoreData,
defaultActiveKey,
panesItem,
tabActiveKey
} = props
const history = useHistory()
const { pathname, search } = useLocation()
const fullPath = pathname + search
// 记录当前打开的tab
const storeTabs = useCallback(
(ps): void => {
const pathArr = ps.reduce(
(prev: CommonObjectType[], next: CommonObjectType) => [
...prev,
next.path
],
[]
)
setStoreData('SET_CURTAB', pathArr)
},
[setStoreData]
)
// 从本地存储中恢复已打开的tab列表
const resetTabs = useCallback((): void => {
const initPanes = curTab.reduce(
(prev: CommonObjectType[], next: string) => {
const { title, tabKey, component: Content } = getKeyName(next)
return [
...prev,
{
title,
key: tabKey,
content: Content,
closable: tabKey !== 'home',
path: next
}
]
},
[]
)
const { tabKey } = getKeyName(pathname)
setPanes(initPanes)
setActiveKey(tabKey)
}, [curTab, pathname])
// 初始化页面
useEffect(() => {
resetTabs()
}, [resetTabs])
// tab切换
const onChange = (tabKey: string): void => {
setActiveKey(tabKey)
}
// 移除tab
const remove = (targetKey: string): void => {
const delIndex = panes.findIndex(
(item: CommonObjectType) => item.key === targetKey
)
panes.splice(delIndex, 1)
// 删除非当前tab
if (targetKey !== activeKey) {
const nextKey = activeKey
setPanes(panes)
setActiveKey(nextKey)
storeTabs(panes)
return
}
// 删除当前tab,地址往前推
const nextPath = curTab[delIndex - 1]
const { tabKey } = getKeyName(nextPath)
// 如果当前tab关闭后,上一个tab无权限,就一起关掉
if (!isAuthorized(tabKey) && nextPath !== '/') {
remove(tabKey)
history.push(curTab[delIndex - 2])
} else {
history.push(nextPath)
}
setPanes(panes)
storeTabs(panes)
}
// tab新增删除操作
const onEdit = (targetKey: string | any, action: string) =>
action === 'remove' && remove(targetKey)
// tab点击
const onTabClick = (targetKey: string): void => {
const { path } = panes.filter(
(item: CommonObjectType) => item.key === targetKey
)[0]
history.push({ pathname: path })
}
// 刷新当前 tab
const refreshTab = (): void => {
setIsReload(true)
setTimeout(() => {
setIsReload(false)
}, 1000)
setStoreData('SET_RELOADPATH', pathname + search)
setTimeout(() => {
setStoreData('SET_RELOADPATH', 'null')
}, 500)
}
// 关闭其他或关闭所有
const removeAll = async (isCloseAll?: boolean) => {
const { path, key } = selectedPanel
history.push(isCloseAll ? '/' : path)
const homePanel = [
{
title: '首页',
key: 'home',
content: Home,
closable: false,
path: '/'
}
]
const nowPanes =
key !== 'home' && !isCloseAll ? [...homePanel, selectedPanel] : homePanel
setPanes(nowPanes)
setActiveKey(isCloseAll ? 'home' : key)
storeTabs(nowPanes)
}
useEffect(() => {
const newPath = pathname + search
// 当前的路由和上一次的一样,return
if (!panesItem.path || panesItem.path === pathRef.current) return
// 保存这次的路由地址
pathRef.current = newPath
const index = panes.findIndex(
(_: CommonObjectType) => _.key === panesItem.key
)
// 无效的新tab,return
if (!panesItem.key || (index > -1 && newPath === panes[index].path)) {
setActiveKey(tabActiveKey)
return
}
// 新tab已存在,重新覆盖掉(解决带参数地址数据错乱问题)
if (index > -1) {
panes[index].path = newPath
setPanes(panes)
setActiveKey(tabActiveKey)
return
}
// 添加新tab并保存起来
panes.push(panesItem)
setPanes(panes)
setActiveKey(tabActiveKey)
storeTabs(panes)
}, [panes, panesItem, pathname, resetTabs, search, storeTabs, tabActiveKey])
const isDisabled = () => selectedPanel.key === 'home'
// tab右击菜单
const menu = (
<Menu>
<Menu.Item
key="1"
onClick={() => refreshTab()}
disabled={selectedPanel.path !== fullPath}
>
</Menu.Item>
<Menu.Item
key="2"
onClick={(e) => {
e.domEvent.stopPropagation()
remove(selectedPanel.key)
}}
disabled={isDisabled()}
>
</Menu.Item>
<Menu.Item
key="3"
onClick={(e) => {
e.domEvent.stopPropagation()
removeAll()
}}
>
</Menu.Item>
<Menu.Item
key="4"
onClick={(e) => {
e.domEvent.stopPropagation()
removeAll(true)
}}
disabled={isDisabled()}
>
</Menu.Item>
</Menu>
)
// 阻止右键默认事件
const preventDefault = (e: CommonObjectType, panel: object) => {
e.preventDefault()
setSelectedPanel(panel)
}
return (
<div>
<Tabs
activeKey={activeKey}
className={style.tabs}
defaultActiveKey={defaultActiveKey}
hideAdd
onChange={onChange}
onEdit={onEdit}
onTabClick={onTabClick}
type="editable-card"
>
{panes.map((pane: CommonObjectType) => (
<TabPane
closable={pane.closable}
key={pane.key}
tab={
<Dropdown
overlay={menu}
placement="bottomLeft"
trigger={['contextMenu']}
>
<span onContextMenu={(e) => preventDefault(e, pane)}>
{isReload &&
pane.path === fullPath &&
pane.path !== '/403' && (
<SyncOutlined title="刷新" spin={isReload} />
)}
{pane.title}
</span>
</Dropdown>
}
>
{reloadPath !== pane.path ? (
<pane.content path={pane.path} />
) : (
<div style={{ height: '100vh' }}>
<Alert message="刷新中..." type="info" />
</div>
)}
</TabPane>
))}
</Tabs>
</div>
)
}
export default connect(
(state) => state,
actions
)(TabPanes)

26
src/index.tsx

@ -0,0 +1,26 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider as ReduxProvider } from 'react-redux'
import { PersistGate } from 'redux-persist/lib/integration/react'
import { store, persistor } from '@/store'
import { ConfigProvider } from 'antd'
import zhCN from 'antd/es/locale/zh_CN'
import moment from 'moment'
import 'moment/locale/zh-cn'
import App from './App'
import '@/assets/css/public.less'
import '@/utils'
moment.locale('zh-cn')
ReactDOM.render(
<ReduxProvider store={store}>
<PersistGate loading={null} persistor={persistor}>
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>
</PersistGate>
</ReduxProvider>,
document.getElementById('root')
)

34
src/pages/container/Home.module.less

@ -0,0 +1,34 @@
.container {
width: 100vw;
}
.menu {
height: 100vh;
box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
position: fixed;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0;
}
}
.content {
height: 100%;
display: flex;
flex: auto;
flex-direction: column;
width: 100%;
margin-left: 220px;
overflow-x: hidden;
transition: margin .2s;
&.collapsed{
margin-left: 80px;
}
.mainContent {
background: @component-background;
margin: 0 16px 16px;
padding: 16px;
overflow: auto;
transition: all .5s;
}
}

136
src/pages/container/index.tsx

@ -0,0 +1,136 @@
import React, { FC, useState, useEffect, useRef, Component } from 'react'
import { useHistory, useLocation } from 'react-router-dom'
import MenuView from '@/components/Menu'
import classNames from 'classnames'
import { Layout, BackTop } from 'antd'
import { getKeyName, isAuthorized } from '@/assets/js/publicFunc'
import Header from '@/components/Header'
import TabPanes from '@/components/TabPanes'
import { connect } from 'react-redux'
import * as actions from '@/store/actions'
import styles from './Home.module.less'
const noNewTab = ['/login'] // 不需要新建 tab的页面
const noCheckAuth = ['/', '/403'] // 不需要检查权限的页面
// 检查权限
const checkAuth = (newPathname: string): boolean => {
// 不需要检查权限的
// if (noCheckAuth.includes(newPathname)) {
// return true
// }
// const { tabKey: currentKey } = getKeyName(newPathname)
// return isAuthorized(currentKey)
return true;
}
interface Props extends ReduxProps {}
interface PanesItemProps {
title: string;
content: Component;
key: string;
closable: boolean;
path: string;
}
const Home: FC<Props> = (props) => {
const [tabActiveKey, setTabActiveKey] = useState<string>('home')
const [panesItem, setPanesItem] = useState<PanesItemProps>({
title: '',
content: null,
key: '',
closable: false,
path: ''
})
const pathRef: RefType = useRef<string>('')
const history = useHistory()
const { pathname, search } = useLocation()
const {
storeData: { collapsed, userInfo },
setStoreData
} = props
const { token } = userInfo
useEffect(() => {
setStoreData('SET_COLLAPSED', document.body.clientWidth <= 1024)
// 未登录
if (!token && pathname !== '/login') {
history.replace({ pathname: '/login' })
return
}
const { tabKey, title, component: Content } = getKeyName(pathname)
// 新tab已存在或不需要新建tab,return
if (pathname === pathRef.current || noNewTab.includes(pathname)) {
setTabActiveKey(tabKey)
return
}
// 检查权限,比如直接从地址栏输入的,提示无权限
const isHasAuth = checkAuth(pathname)
if (!isHasAuth) {
const errorUrl = '/403'
const {
tabKey: errorKey,
title: errorTitle,
component: errorContent
} = getKeyName(errorUrl)
setPanesItem({
title: errorTitle,
content: errorContent,
key: errorKey,
closable: true,
path: errorUrl
})
pathRef.current = errorUrl
setTabActiveKey(errorKey)
history.replace(errorUrl)
return
}
// 记录新的路径,用于下次更新比较
const newPath = search ? pathname + search : pathname
pathRef.current = newPath
setPanesItem({
title,
content: Content,
key: tabKey,
closable: tabKey !== 'home',
path: newPath
})
setTabActiveKey(tabKey)
}, [history, pathname, search, setStoreData, token])
return (
<Layout
className={styles.container}
onContextMenu={(e) => e.preventDefault()}
style={{ display: pathname.includes('/login') ? 'none' : 'flex' }}
>
<MenuView />
<Layout
className={classNames(styles.content, {
[styles.collapsed]: collapsed
})}
>
<Header />
<Layout.Content>
<TabPanes
defaultActiveKey="home"
panesItem={panesItem}
tabActiveKey={tabActiveKey}
/>
</Layout.Content>
</Layout>
<BackTop visibilityHeight={1080} />
</Layout>
)
}
export default connect(
(state) => state,
actions
)(Home)

38
src/pages/home/index.css

@ -0,0 +1,38 @@
.home .box {
width: 100%;
margin-top: 20px;
border-radius: 10px;
padding: 20px;
box-sizing: border-box;
min-height: 280px;
}
.home .box .solid {
min-height: 200px;
width: 1px;
}
.home .box .img-box {
width: 60px;
height: 60px;
background-color: #44D600;
border-radius: 6px;
display: flex;
justify-content: center;
align-items: center;
}
.home .box .img-box img {
width: 30px;
height: 30px;
object-fit: cover;
}
.row {
display: flex;
}
.row-items {
display: flex;
align-items: center;
}
.row-between {
display: flex;
align-items: center;
justify-content: space-between;
}

47
src/pages/home/index.less

@ -0,0 +1,47 @@
.home {
.box {
width: 100%;
margin-top: 20px;
border-radius: 10px;
padding: 20px;
box-sizing: border-box;
min-height: 280px;
.solid{
min-height: 200px;
width: 1px;
}
.img-box{
width: 60px;
height: 60px;
background-color: #44D600;
border-radius: 6px;
display: flex;
justify-content: center;
align-items: center;
img{
width: 30px;
height: 30px;
object-fit: cover;
}
}
}
}
.row{
display: flex;
}
.row-items{
display: flex;
align-items: center;
}
.row-between{
display: flex;
align-items: center;
justify-content: space-between;
}

9
src/pages/home/index.tsx

@ -0,0 +1,9 @@
import React from "react"
const Home = () => {
return (
<></>
)
}
export default Home

BIN
src/pages/home/vector.png

After

Width: 90  |  Height: 90  |  Size: 589 B

100
src/pages/login/index.tsx

@ -0,0 +1,100 @@
import React, { useEffect, FC, useMemo } from 'react'
import { useHistory } from 'react-router-dom'
import { LockOutlined, UserOutlined } from '@ant-design/icons'
import { Form, Input, Button, Select, Cascader } from 'antd'
import ReactCanvasNest from 'react-canvas-nest'
import './login.less'
import Logo from '@/assets/img/logo.png'
import { setUserInfo } from '@/assets/js/publicFunc'
import { connect } from 'react-redux'
import * as actions from '@/store/actions'
import common from '@/api'
interface Props extends ReduxProps { }
const LoginForm: FC<Props> = ({
storeData: { theme, userInfo = {} },
setStoreData
}) => {
const history = useHistory()
useEffect(() => {
const { token } = userInfo
if (token) {
history.push('/')
return
}
// 重置 tab栏为首页
setStoreData('SET_CURTAB', ['/'])
}, [history, setStoreData, userInfo])
// 触发登录方法
const onFinish = (values: CommonObjectType<string>): void => {
const { username } = values;
common.login(values).then((res: any) => {
// 登录后返回的数据,包括权限
const resData = {
userName: username,
token: res.data.token,
permission: res.data.permissions || []
}
setUserInfo(resData, setStoreData)
history.push('/')
})
}
const FormView = (
<Form className="login-form" name="login-form" onFinish={onFinish}>
<Form.Item
name="username"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input placeholder="用户名" prefix={<UserOutlined />} size="large" />
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password
placeholder="密码"
prefix={<LockOutlined />}
size="large"
/>
</Form.Item>
<Form.Item>
<Button
className="login-form-button"
htmlType="submit"
size="large"
type="primary"
>
</Button>
</Form.Item>
</Form>
)
const floatColor = theme === 'default' ? '24,144,255' : '110,65,255'
return (
<div className="login-layout" id="login-layout">
<ReactCanvasNest
config={{
pointColor: floatColor,
lineColor: floatColor,
pointOpacity: 0.6
}}
style={{ zIndex: 1 }}
/>
<div className="logo-box">
<img alt="" className="logo" src={Logo} />
<span className="logo-name">React Antd Admin</span>
</div>
{FormView}
</div>
)
}
export default connect(
(state) => state,
actions
)(LoginForm)

36
src/pages/login/login.less

@ -0,0 +1,36 @@
.login-layout {
display: flex;
flex-direction: column;
height: 100vh;
overflow: auto;
background: url(../../assets/img/login-bg.svg) @component-background no-repeat center 110px;
background-size: 100%;
overflow: hidden;
min-width: 380px;
.login-form {
width: 368px;
margin: 0 auto;
z-index: 2;
}
.login-form-button {
width: 100%;
}
.logo-box {
margin: 200px auto 40px;
}
.logo {
height: 55px;
margin-right: 8px;
}
.logo-name {
position: relative;
top: 8px;
color: #fdfdfe;
font-weight: 600;
font-size: 26px;
}
}

226
src/pages/notify/news.tsx

@ -0,0 +1,226 @@
import api from "@/api"
import MyTable from "@/components/MyTable"
import { RootState } from "@/store"
import { getTime } from "@/utils"
import { getBaseUrl } from "@/utils/axios"
import { InboxOutlined } from "@ant-design/icons"
import { Button, Drawer, Form, Input, Popconfirm, Tooltip, notification } from "antd"
import TextArea from "antd/lib/input/TextArea"
import Dragger from "antd/lib/upload/Dragger"
import { title } from "process"
import React, { useRef, useState } from "react"
import { useSelector } from "react-redux"
const BUZZUPNews = () => {
const { userInfo } = useSelector((state: RootState) => state.storeData)
const [visible, setVisible] = useState(false)
const [form] = Form.useForm()
const currentType = useRef('' as 'create' | 'update')
const currentItem = useRef({} as any)
const tableRef = useRef(null)
const columns = [
{
title: '图片',
dataIndex: 'image',
width: 300,
render: (logo) => {
return (
<Tooltip title={
<img src={logo} alt="" style={{ width: 240, height: 240, objectFit: 'contain' }} />
}>
<img src={logo} className="tp" alt="" style={{ width: 50, height: 50, objectFit: 'contain' }} />
</Tooltip>
)
}
},
{
title: '标题',
width: 300,
dataIndex: "title",
render: (title) => {
return (
<Tooltip overlay={false} title={
<div dangerouslySetInnerHTML={{ __html: title.replace(/\\n/g, '<br/>') }}></div>
}>
<div className="text-overflow tp" dangerouslySetInnerHTML={{ __html: title.replace(/\\n/g, '<br/>') }}></div>
</Tooltip>
)
}
},
{
title: '链接',
dataIndex: 'url',
width: 300
},
{
title: '创建时间',
dataIndex: 'time',
render: (time) => (
<div>{getTime(time * 1000)}</div>
)
},
{
title: 'options',
fixed: 'right',
width: 100,
render: (item) => (
<div>
<Button type="primary" style={{ marginTop: 10 }} onClick={() => {
currentItem.current = item
currentType.current = 'update'
form.setFieldsValue(item)
setVisible(true)
}}></Button>
<Popconfirm title={`确认删除“${item.title}”吗?`} onConfirm={() => deleteSystemMsg(item)}>
<Button type="primary" style={{ marginTop: 10, backgroundColor: 'red', borderColor: 'red' }}></Button>
</Popconfirm>
</div>
)
}
]
const deleteSystemMsg = async (item) => {
const res: any = await api.delete_new_message({
id: item.id
})
if (res.code === 0) {
tableRef.current.update()
notification.success({
message: '删除成功'
})
}
}
const onClose = () => setVisible(false)
const onFinish = async (values) => {
const parmas = {
...values
}
console.log(parmas);
if (currentType.current === 'update') {
parmas.id = currentItem.current.id
}
let res: any;
if (currentType.current === 'create') {
res = await api.add_news_message(parmas)
} else {
res = await api.updata_news_message(parmas)
}
if (res.code === 0) {
setVisible(false)
tableRef.current.update()
form.resetFields()
notification.success({
message: currentType.current === 'create' ? '添加成功' : '修改成功'
})
}
}
const uploadProps = {
name: 'img',
action: `${getBaseUrl()}/admin/uploadImg`,
headers: {
Token: userInfo.token,
},
}
const onChange = (info: any) => {
if (info.file.status === 'removed') {
form.setFieldsValue({
image: ''
})
}
if (info.file.status === 'done') {
if (info.file.response && info.file.response.code === 0) {
form.setFieldsValue({
image: info.file.response.data.url
})
notification.success({
message: `图片上传成功`
})
}
} else if (info.file.status === 'error') {
notification.error({
message: `图片上传失败`
});
}
}
return (
<div>
<MyTable
apiFun={api.get_news_message}
ref={tableRef}
columns={columns}
header={
<div style={{ width: '100%', display: 'flex', justifyContent: 'flex-end', marginBottom: 20 }}>
<Button type="primary" onClick={() => {
currentType.current = 'create'
form.resetFields()
setVisible(true)
}}>BUZZUPNews</Button>
</div>
}
/>
<Drawer title="添加系统通知" onClose={onClose} visible={visible} width="40%">
<Form form={form} onFinish={onFinish}>
<Form.Item
name="url"
label="链接"
rules={[{ required: true, message: '请输入链接' }]}
>
<Input placeholder="请输入链接" />
</Form.Item>
<Form.Item
name="title"
label="标题"
rules={[{ required: true, message: '请输入标题' }]}
>
<TextArea placeholder="请输入标题" style={{ minHeight: 200 }} />
</Form.Item>
<Form.Item label="模版" shouldUpdate={(prevValues, currentValues) => prevValues.title !== currentValues.title}>
{({ getFieldValue }) => (
<div dangerouslySetInnerHTML={{ __html: (getFieldValue('title') || '').replace(/\\n/g, '<br/>') }}></div>
)}
</Form.Item>
<Form.Item
label="图片"
name="image"
rules={[{ required: true, message: 'Please Select Logo Image!' }]}
>
<Dragger
{...uploadProps}
onChange={onChange}
>
<p className="ant-upload-drag-icon" style={{ display: 'flex', justifyContent: 'center', paddingTop: 10 }}>
<InboxOutlined />
</p>
<p className="ant-upload-text" style={{ textAlign: 'center' }}></p>
<p className="ant-upload-hint" style={{ textAlign: 'center', paddingBottom: 10 }}>
Support for a single or bulk upload. Strictly prohibited from uploading company data or other
banned files.
</p>
</Dragger>
</Form.Item>
<Form.Item label="">
<div style={{ display: 'flex', justifyContent: 'center' }}>
<Button type="primary" htmlType="submit"></Button>
</div>
</Form.Item>
</Form>
</Drawer>
</div>
)
}
export default BUZZUPNews

148
src/pages/notify/system.tsx

@ -0,0 +1,148 @@
import api from "@/api"
import MyTable from "@/components/MyTable"
import { getTime } from "@/utils"
import { Button, Drawer, Form, Input, Popconfirm, Tooltip, notification } from "antd"
import TextArea from "antd/lib/input/TextArea"
import React, { useRef, useState } from "react"
const System = () => {
const [visible, setVisible] = useState(false)
const [form] = Form.useForm()
const currentType = useRef('' as 'create' | 'update')
const currentItem = useRef({} as any)
const tableRef = useRef(null)
const columns = [
{
title: '标题',
dataIndex: 'title',
width: 300
},
{
title: '内容',
width: 350,
dataIndex: "content",
render: (content) => {
return (
<Tooltip overlay={false} title={
<div dangerouslySetInnerHTML={{ __html: content.replace(/\\n/g, '<br/>') }}></div>
}>
<div className="text-overflow tp" dangerouslySetInnerHTML={{ __html: content.replace(/\\n/g, '<br/>') }}></div>
</Tooltip>
)
}
},
{
title: '创建时间',
dataIndex: 'time',
render: (time) => (
<div>{getTime(time * 1000)}</div>
)
},
{
title: 'options',
fixed: 'right',
width: 100,
render: (item) => (
<div>
<Button type="primary" style={{ marginTop: 10 }} onClick={() => {
currentItem.current = item
currentType.current = 'update'
form.setFieldsValue(item)
setVisible(true)
}}></Button>
<Popconfirm title={`确认删除“${item.title}”吗?`} onConfirm={() => deleteSystemMsg(item)}>
<Button type="primary" style={{ marginTop: 10, backgroundColor: 'red', borderColor: 'red' }}></Button>
</Popconfirm>
</div>
)
}
]
const deleteSystemMsg = async (item) => {
const res: any = await api.delete_system_message({
id: item.id
})
if (res.code === 0) {
tableRef.current.update()
notification.success({
message: '删除成功'
})
}
}
const onClose = () => setVisible(false)
const onFinish = async (values) => {
const parmas = {
...values,
}
if (currentType.current === 'update') {
parmas.id = currentItem.current.id
}
let res: any;
if (currentType.current === 'create') {
res = await api.add_system_message(parmas)
} else {
res = await api.updata_system_message(parmas)
}
if (res.code === 0) {
setVisible(false)
tableRef.current.update()
form.resetFields()
notification.success({
message: currentType.current === 'create' ? '添加成功' : '修改成功'
})
}
}
return (
<div>
<MyTable
apiFun={api.get_system_message}
ref={tableRef}
columns={columns}
header={
<div style={{ width: '100%', display: 'flex', justifyContent: 'flex-end', marginBottom: 20 }}>
<Button type="primary" onClick={() => {
currentType.current = 'create'
form.resetFields()
setVisible(true)
}}></Button>
</div>
}
/>
<Drawer title="添加系统通知" onClose={onClose} visible={visible} width="40%">
<Form form={form} onFinish={onFinish}>
<Form.Item
name="title"
label="标题"
rules={[{ required: true, message: '请输入标题' }]}
>
<Input placeholder="请输入标题" />
</Form.Item>
<Form.Item
name="content"
label="内容"
rules={[{ required: true, message: '请输入内容' }]}
>
<TextArea placeholder="请输入内容" style={{ minHeight: 200 }} />
</Form.Item>
<Form.Item label="模版" shouldUpdate={(prevValues, currentValues) => prevValues.content !== currentValues.content}>
{({ getFieldValue }) => (
<div dangerouslySetInnerHTML={{ __html: (getFieldValue('content') || '').replace(/\\n/g, '<br/>') }}></div>
)}
</Form.Item>
<Form.Item label="">
<div style={{ display: 'flex', justifyContent: 'center' }}>
<Button type="primary" htmlType="submit"></Button>
</div>
</Form.Item>
</Form>
</Drawer>
</div>
)
}
export default System

13
src/pages/public/errorPage/index.tsx

@ -0,0 +1,13 @@
import React, { FC } from 'react'
import { Result } from 'antd'
const ErrorPage: FC = () => (
<Result
style={{ height: '100vh' }}
status="403"
title="403"
subTitle="抱歉,您无权访问此页面,如有疑问请联系管理员!"
/>
)
export default ErrorPage

344
src/pages/swiper/index.tsx

@ -0,0 +1,344 @@
import api from "@/api"
import MyTable from "@/components/MyTable"
import { RootState } from "@/store";
import { getBaseUrl } from "@/utils/axios";
import { InboxOutlined } from "@ant-design/icons";
import { Button, Cascader, Drawer, Form, Input, Popconfirm, Select, Tooltip, notification } from "antd";
import TextArea from "antd/lib/input/TextArea";
import Dragger from "antd/lib/upload/Dragger";
import React, { FC, useRef, useState } from "react"
import { DefaultRootState, useSelector } from "react-redux";
interface Option {
value: string | number;
label: string;
children?: Option[];
}
const Swiper: FC = () => {
const [chainId, setChainId] = useState('56')
const { userInfo } = useSelector((state: RootState) => state.storeData)
const [visible, setVisible] = useState(false)
const [form] = Form.useForm()
const tableRef = useRef(null)
const currentItem = useRef({} as any)
const currentType = useRef('')
const uploadProps = {
name: 'img',
action: `${getBaseUrl()}/admin/uploadImg`,
headers: {
Token: userInfo.token,
},
}
const onChange = (info: any) => {
if (info.file.status === 'removed') {
form.setFieldsValue({
logo_url: ''
})
}
if (info.file.status === 'done') {
if (info.file.response && info.file.response.code === 0) {
form.setFieldsValue({
logo_url: info.file.response.data.url
})
notification.success({
message: `图片上传成功`
})
}
} else if (info.file.status === 'error') {
notification.error({
message: `图片上传失败`
});
}
}
const columns = [
{
title: 'ID',
dataIndex: 'id'
},
{
title: '图片',
dataIndex: 'logo_url',
render: (logo) => {
return (
<img src={logo} alt="" style={{ width: 50, height: 50, objectFit: 'contain' }} />
)
}
},
{
title: '标题',
dataIndex: 'title',
},
{
title: '排序',
dataIndex: 'sort',
},
{
title: '点击次数',
dataIndex: 'click'
},
{
title: '类型',
dataIndex: 'dapp_type'
},
{
title: '网页地址',
dataIndex: 'url',
},
{
title: '内容',
dataIndex: 'content',
render: (content) => {
return (
<Tooltip overlay={false} title={content}>
<span style={{ width: 240 }} className="text-overflow tp">{content}</span>
</Tooltip>
)
}
},
{
title: 'options',
fixed: 'right',
width: 100,
render: (item) => {
return (
<div >
<Button type="primary" onClick={() => {
currentItem.current = item
currentType.current = 'update'
form.setFieldsValue(item)
setVisible(true)
}}></Button>
<Popconfirm
title={`确认删除”${item.title}“吗?`}
okText="确认"
cancelText="取消"
onConfirm={() => {
deleteDapp(item)
}}
>
<Button type="primary" style={{ marginTop: 10, backgroundColor: 'red' }}></Button>
</Popconfirm>
</div>
)
}
},
]
const deleteDapp = async (item: any) => {
const res: any = await api.delete_dapp_list({ id: item.id })
if (res.code === 0) {
tableRef.current.update()
notification.success({
message: '删除成功'
})
}
}
const optionChianIds = [
{
value: '1',
label: 'Ethereum Mainnet'
},
{
value: '56',
label: 'BNB Smart Chain Mainnet'
},
{
value: '42161',
label: 'Arbitrum One'
},
{
value: '137',
label: 'Polygon Mainnet'
},
{
value: '10',
label: 'OP Mainnet'
},
{
value: '100',
label: 'xDai Chain'
},
{
value: '10',
label: 'OP Mainnet'
},
{
value: '314',
label: 'Filecoin Mainnet'
},
{
value: '97',
label: 'BSC Testnet'
},
]
const optionTypes = [
{
value: '1',
label: 'GAMEFI'
},
{
value: '2',
label: 'DEFI'
},
{
value: '3',
label: 'BANNER'
},
{
value: '4',
label: 'NEWS'
},
]
const searchConfigList = [
{
key: 'chain_id',
slot: <Cascader style={{ width: 300 }} options={optionChianIds} onChange={val => {
setChainId(val[0])
}} />,
initialValue: [chainId],
fn: (ids) => {
return Number(ids[0])
}
}
]
const onClose = () => {
setVisible(false)
}
const onFinish = async (obj: any) => {
const parmas = {
...obj,
chain_id: Number(obj.chain_id),
dapp_type: Number(obj.dapp_type),
sort: Number(obj.sort)
}
if (currentType.current === 'update') {
parmas.id = currentItem.current.id;
}
let res: any;
if (currentType.current === 'create') {
res = await api.add_dapp_list(parmas)
} else {
res = await api.update_dapp_list(parmas)
}
if (res.code === 0) {
tableRef.current.update()
form.resetFields()
setVisible(false)
notification.success({
message: currentType.current === 'create' ? '添加DApp成功' : '修改DApp成功'
})
}
}
return (
<div>
<MyTable
ref={tableRef}
columns={columns}
apiFun={api.getList}
searchConfigList={searchConfigList}
rowKey="id"
header={
<Button type="primary" onClick={() => {
currentType.current = 'create'
form.resetFields()
setVisible(true)
}}>DAPP</Button>
}
/>
<Drawer title="添加DAPP" onClose={onClose} visible={visible} width="40%">
<Form
onFinish={onFinish}
form={form}
>
<Form.Item
label="标题"
name="title"
rules={[{ required: true, message: 'Please input your title!' }]}
>
<Input />
</Form.Item>
<Form.Item
label="链接"
name="url"
rules={[{ required: true, message: 'Please input your url!' }]}
>
<Input />
</Form.Item>
<Form.Item
label="内容"
name="content"
rules={[{ required: form.getFieldValue('dapp_type') != '3', message: 'Please input your content!' }]}
>
<TextArea />
</Form.Item>
<Form.Item
label="类型"
name="dapp_type"
rules={[{ required: true, message: 'select your dapp_type!' }]}
>
<Select options={optionTypes} />
</Form.Item>
<Form.Item
label="插入位置"
name="sort"
rules={[{ required: true, message: 'Please input your sort!' }]}
>
<Input />
</Form.Item>
<Form.Item
label="链ID"
name="chain_id"
initialValue={chainId}
>
{/* <Cascader /> */}
<div>{chainId}</div>
</Form.Item>
<Form.Item
label="LOGO"
name="logo_url"
rules={[{ required: true, message: 'Please Select Logo Image!' }]}
>
<Dragger
{...uploadProps}
onChange={onChange}
>
<p className="ant-upload-drag-icon" style={{ display: 'flex', justifyContent: 'center', paddingTop: 10 }}>
<InboxOutlined />
</p>
<p className="ant-upload-text" style={{ textAlign: 'center' }}></p>
<p className="ant-upload-hint" style={{ textAlign: 'center', paddingBottom: 10 }}>
Support for a single or bulk upload. Strictly prohibited from uploading company data or other
banned files.
</p>
</Dragger>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">Submit</Button>
</Form.Item>
</Form>
</Drawer>
</div>
)
}
export default Swiper

12
src/react-app-env.d.ts

@ -0,0 +1,12 @@
/// <reference types="react-scripts" />
declare module '*.css'
declare module '*.less'
declare module '*.scss'
declare module '*.svg'
declare module '*.png'
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.gif'
declare module '*.bmp'
declare module '*.tiff'

48
src/route/routes.ts

@ -0,0 +1,48 @@
import Home from "@/pages/home"
import Swiper from "@/pages/swiper"
import System from "@/pages/notify/system"
import { HomeOutlined, SolutionOutlined } from "@ant-design/icons"
import BUZZUPNews from "@/pages/notify/news"
const routes = [
{
path: '/',
name: '首页',
exact: true,
key: 'home',
icon: HomeOutlined,
component: Home,
routes: []
},
{
path: '/swiper',
name: 'DAPP',
exact: true,
key: 'swiper',
icon: SolutionOutlined,
component: Swiper,
routes: []
},
{
path: '/system',
name: '系统通知',
exact: true,
key: 'system',
icon: SolutionOutlined,
component: System,
routes: []
},
{
path: '/news',
name: 'BUZZUP News',
exact: true,
key: 'news',
icon: SolutionOutlined,
component: BUZZUPNews,
routes: []
},
]
export default routes

27
src/store/actionTypes/index.ts

@ -0,0 +1,27 @@
/**
* redux的 type常量
* @param {name} string action diapatch
* @param {field} string action
*/
export default {
SET_USERINFO: {
name: 'SET_USERINFO',
field: 'userInfo'
},
SET_COLLAPSED: {
name: 'SET_COLLAPSED',
field: 'collapsed'
},
SET_CURTAB: {
name: 'SET_CURTAB',
field: 'curTab'
},
SET_THEME: {
name: 'SET_THEME',
field: 'theme'
},
SET_RELOADPATH: {
name: 'SET_RELOADPATH',
field: 'reloadPath'
}
}

7
src/store/actions/index.ts

@ -0,0 +1,7 @@
import actionTypes from '@/store/actionTypes'
// eslint-disable-next-line import/prefer-default-export
export const setStoreData = (type: string, payload: any): object => {
if (!actionTypes[type]) throw new Error('请传入修改的数据类型和值')
return { type, payload }
}

25
src/store/index.ts

@ -0,0 +1,25 @@
import { createStore, applyMiddleware, combineReducers } from 'redux'
import { persistStore, persistReducer } from 'redux-persist'
import promiseMiddleware from 'redux-promise'
import storage from 'redux-persist/lib/storage'
import storeData from '@/store/reducers'
const reducers = combineReducers({
storeData
})
const persistConfig = {
key: 'root',
storage
}
const myPersistReducer = persistReducer(persistConfig, reducers)
const store: any = createStore(
myPersistReducer,
applyMiddleware(promiseMiddleware)
)
const persistor: any = persistStore(store)
export type RootState = ReturnType<typeof store.getState>
export { store, persistor }

21
src/store/reducers/index.ts

@ -0,0 +1,21 @@
import initState from '@/store/state'
import actionTypes from '@/store/actionTypes'
interface StoreAction {
type: string;
payload: any;
}
const storeData = (
state = initState,
{ type, payload }: StoreAction
): object => {
if (!actionTypes[type]) return state
const { field } = actionTypes[type]
return {
...state,
[field]: payload
}
}
export default storeData

25
src/store/state/index.ts

@ -0,0 +1,25 @@
interface StoreState {
userInfo: {
userName: string,
permission: string[],
token: string
};
collapsed: boolean;
curTab: string[];
theme: string;
reloadPath: string;
}
const initState: StoreState = {
userInfo: {
userName: '',
permission: [],
token: ''
}, // 用户信息
collapsed: false, // 菜单收纳状态
curTab: [], // 当前tab页面
theme: '', // 网站主题
reloadPath: 'null', // 需要刷新的tab路径
}
export default initState

9
src/test/App.test.jsx

@ -0,0 +1,9 @@
import React from 'react'
import ReactDOM from 'react-dom'
import App from '../App'
it('renders without crashing', () => {
const div = document.createElement('div')
ReactDOM.render(<App />, div)
ReactDOM.unmountComponentAtNode(div)
})

7
src/types/store.d.ts

@ -0,0 +1,7 @@
export const WALLETKEY = 'walletKey'
export const SIMPLETRADEKEY = 'simpleTradeKey'
export const FILEFASTKEY = 'filefastKey'
export const DEV_ENV = 'dev'
export const PROD_ENV = 'prod'
export const ROUTEKEY = 'routeKey'
export const ISENV = 'isEnv'

111
src/utils/axios.ts

@ -0,0 +1,111 @@
import Axios from 'axios'
import { message } from 'antd'
import { store } from '@/store'
import { HashRouter } from 'react-router-dom'
interface AxiosConfig {
baseURL: string;
timeout: number;
headers: {
'Content-Type': string
};
}
export const getBaseUrl = () => {
// return `http://192.168.124.20:8008/api/v1`
return `https://apk.hippoim.us/api/v1`
}
const config: AxiosConfig = {
// baseURL: process.env.REACT_APP_BASE_URL,
baseURL: getBaseUrl(),
timeout: 60 * 1000,
headers: {
'Content-Type': 'application/json'
}
}
const axios = Axios.create(config)
const router: CommonObjectType = new HashRouter({})
// token失效,清除用户信息并返回登录界面
const clearAll = () => {
store.dispatch({
type: 'SET_USERINFO',
payload: {}
})
router.history.replace({ pathname: '/login' })
}
// 请求前拦截
axios.interceptors.request.use(
(req) => {
const { token = '' } = store.getState().storeData.userInfo || {}
// req.headers.Authorization = token
req.headers.Token = token
return req
},
(err) => {
return Promise.reject(err)
}
)
// 返回后拦截
axios.interceptors.response.use(
({ data }): Promise<any> => {
if (data.code !== 0) {
message.error(data.message || data.msg)
return Promise.reject(data)
}
return Promise.resolve(data)
},
(err) => {
message.destroy()
try {
if (JSON.stringify(err).includes('401')) {
clearAll()
message.error({ title: '提示', content: 'Unauthorized' })
return Promise.reject(err)
}
} catch (error) {
clearAll()
}
message.error('网络异常')
return Promise.reject(err)
}
)
// post请求
axios.post = (url: string, params?: object): Promise<any> =>
axios({
method: 'post',
url,
data: params
})
// get请求
axios.get = (url: string, params?: object): Promise<any> =>
axios({
method: 'get',
url,
params
})
// put请求
axios.put = (url: string, params?: object): Promise<any> =>
axios({
method: 'put',
url,
data: params
})
// delete请求
axios.delete = (url: string, params?: object): Promise<any> =>
axios({
method: 'delete',
url,
data: params
})
export default axios

49
src/utils/index.ts

@ -0,0 +1,49 @@
import { notification } from 'antd';
import './axios'
export const splitAddress = (address: string, index?: number) => {
try {
let idx = index ? index : 5;
return address.substring(0, idx) + '...' + address.substring(address.length - idx, address.length)
} catch (error) {
return ''
}
}
export const getTime = (value: number) => {
let date = new Date(value);
let yy: number | string = date.getFullYear();
let mm: number | string = date.getMonth() + 1;
let dd: number | string = date.getDate();
let xs: number | string = date.getHours();
let ff: number | string = date.getMinutes();
let ss: number | string = date.getSeconds();
mm = mm >= 10 ? mm : '0' + mm;
dd = dd >= 10 ? dd : '0' + dd;
xs = xs >= 10 ? xs : '0' + xs;
ff = ff >= 10 ? ff : '0' + ff;
ss = ss >= 10 ? ss : '0' + ss;
return `${yy}-${mm}-${dd} ${xs}:${ff}:${ss}`
};
export function copy(value: string) {
// 动态创建 textarea 标签
const textarea: any = document.createElement('textarea')
// 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
textarea.readOnly = 'readonly'
textarea.style.position = 'absolute'
textarea.style.left = '-9999px'
// 将要 copy 的值赋给 textarea 标签的 value 属性
// 网上有些例子是赋值给innerText,这样也会赋值成功,但是识别不了\r\n的换行符,赋值给value属性就可以
textarea.value = value
// 将 textarea 插入到 body 中
document.body.appendChild(textarea)
// 选中值并复制
textarea.select()
textarea.setSelectionRange(0, textarea.value.length)
document.execCommand('Copy')
document.body.removeChild(textarea)
notification.success({
message: '复制成功'
})
}

49
src/utils/tableHook.ts

@ -0,0 +1,49 @@
/**
* tableHook.js Table组件的分页事件
* server hook hook ajax { loading, error, response }
* hook
*/
import { useEffect, useState, useCallback } from 'react'
export const useServiceCallback = (
service: (arg0?: any) => Promise<{}>
): CommonObjectType[] => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [response, setResponse] = useState(null)
// 使用 useCallback,来判断 service 是否改变
const callback = useCallback(
(params) => {
setLoading(true)
setError(null)
service(params)
.then((res) => {
setLoading(false)
setResponse(res)
})
.catch(() => {
setLoading(false)
})
},
[service]
)
return [callback, { loading, error, response }]
}
const useService = (
service: (arg0?: any) => Promise<{}>,
params?: CommonObjectType
): object => {
const [callback, { loading, error, response }]: any[] = useServiceCallback(
service
)
useEffect(() => {
callback(params)
return () => {}
}, [callback, params])
return { loading, error, response }
}
export default useService

26
src/utils/userIdHook.ts

@ -0,0 +1,26 @@
/**
* tab页签时
* userId
*/
import { useRef } from 'react'
interface Props {
userIdRef: RefType;
canRequest: boolean;
}
const useControl = (userId: string): Props => {
const userIdRef: RefType = useRef()
// 如果没有 userId,或者 userId 不变
if (!userId || userIdRef.current === userId) {
return { userIdRef, canRequest: false }
}
// 记录上一次的 userId,用于比较
userIdRef.current = userId
return { userIdRef, canRequest: true }
}
export default useControl

52
tsconfig.json

@ -0,0 +1,52 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react",
"experimentalDecorators": true,
"baseUrl": "src",
"noImplicitAny": false,
"noImplicitThis": false,
"strictNullChecks": false,
"downlevelIteration": true
},
"rules": {
"member-access": false,
"ordered-imports": false,
"quotemark": false,
"no-console": false,
"semicolon": false,
"jsx-no-lambda": false
},
"linterOptions": {
"exclude": [
"config/**/*.js",
"node_modules/**/*.ts"
]
},
"include": [
"src",
"typings"
],
"exclude": [
"node_modules",
"dist",
"build"
],
"extends": "./paths.json"
}

23
tslint.json

@ -0,0 +1,23 @@
{
"defaultSeverity": "error",
"extends": [
"tslint-react"
],
"jsRules": {},
"rules": {
"member-access": false,
"ordered-imports": false,
"quotemark": false,
"no-console": false,
"semicolon": false,
"jsx-no-lambda": false,
"no-implicit-dependencies": ["optional", ["src"]]
},
"rulesDirectory": [],
"linterOptions": {
"exclude": [
"config/**/*.js",
"node_modules/**/*.ts"
]
}
}

18
typings/global.d.ts

@ -0,0 +1,18 @@
/// <reference types="react-scripts" />
/// <reference types="node" />
/// <reference types="react" />
/// <reference types="react-dom" />
declare module "classnames" {
import classNames from 'classnames'
export default classNames
}
interface ReduxProps {
storeData?: Record<string, any>;
setStoreData?: (type: string, payload: any) => object;
}
type RefType = MutableRefObject<unknown> | ((instance: unknown) => void)
type CommonObjectType<T = any> = Record<string, T>
Loading…
Cancel
Save