mac 1 year ago
commit
300dc8aa6c
  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. 90
      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. 30
      src/pages/account-review/index.tsx
  66. 65
      src/pages/assets-manage/assets-list.tsx
  67. 51
      src/pages/assets-manage/escrow-list.tsx
  68. 34
      src/pages/container/Home.module.less
  69. 136
      src/pages/container/index.tsx
  70. 38
      src/pages/home/index.css
  71. 47
      src/pages/home/index.less
  72. 124
      src/pages/home/index.tsx
  73. BIN
      src/pages/home/vector.png
  74. 127
      src/pages/login/index.tsx
  75. 36
      src/pages/login/login.less
  76. 39
      src/pages/manage/deposit.tsx
  77. 30
      src/pages/manage/receive.tsx
  78. 37
      src/pages/manage/withdraw.tsx
  79. 13
      src/pages/public/errorPage/index.tsx
  80. 67
      src/pages/review/deposit.tsx
  81. 34
      src/pages/review/withdraw.tsx
  82. 38
      src/pages/system/notify.tsx
  83. 26
      src/pages/system/receive.tsx
  84. 27
      src/pages/system/withdraw.tsx
  85. 230
      src/pages/vip-manage/proxy-list.tsx
  86. 12
      src/react-app-env.d.ts
  87. 177
      src/route/routes.ts
  88. 27
      src/store/actionTypes/index.ts
  89. 7
      src/store/actions/index.ts
  90. 25
      src/store/index.ts
  91. 21
      src/store/reducers/index.ts
  92. 25
      src/store/state/index.ts
  93. 9
      src/test/App.test.jsx
  94. 7
      src/types/store.d.ts
  95. 115
      src/utils/axios.ts
  96. 49
      src/utils/index.ts
  97. 49
      src/utils/tableHook.ts
  98. 26
      src/utils/userIdHook.ts
  99. 52
      tsconfig.json
  100. 23
      tslint.json

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": "npm run build && scp -r ./build/* dcfilefast_prod:/data/wwwroot/tarder-admin",
"deploy:prod": "npm run build && scp -r ./build/* metatrader_prod:/data/wwwroot/admin-matontrading.com"
},
"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>Maton Trading管理后台</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

90
src/api/index.ts

@ -0,0 +1,90 @@
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, {
});
},
get_code() {
return $axios.post('/admin/code')
},
get_homeData() {
return $axios.post('/admin/dashBoard')
},
get_userList(body: object) {
return $axios.post('/admin/userList', body)
},
get_escrowList(body: object) {
return $axios.post('/admin/escrowRecordsList', body)
},
get_assetsList(body: object) {
return $axios.post('/admin/assetsList', body)
},
get_depositReviewList(body: object) {
return $axios.post('/admin/depositReviewList', body)
},
get_withdrawReviewList(body: object) {
return $axios.post('/admin/withdrawReviewList', body)
},
get_depositManageList(body: object) {
return $axios.post('/admin/depositList', body)
},
get_receiveManageList(body: object) {
return $axios.post('/admin/receivingAccountList', body)
},
get_withdrawManageList(body: object) {
return $axios.post('/admin/withdrawList', body)
},
get_systemNofify(body: object) {
return $axios.post('/admin/messageList', body)
},
get_systemReceive(body: object) {
return $axios.post('/admin/platformDepositList', body)
},
get_systemWithdraw(body: object) {
return $axios.post('/admin/withdrawMethodList', body)
},
set_userWalletBalance(body: object) {
return $axios.post('/admin/userBalance', body)
},
set_loginPwd(body: object) {
return $axios.post('/admin/userPassword', body)
},
set_txPwd(body: object) {
return $axios.post('/admin/userAccountPassword', body)
},
set_lv(body: object) {
return $axios.post('/admin/userLevel', body)
},
delete_account(body: object) {
return $axios.post('/admin/delUser', body)
},
add_account(body: object) {
return $axios.post('/admin/newUser', body)
},
get_userToken(body: object) {
return $axios.post('/admin/userToken', body)
},
get_accountReviewList(body: object) {
return $axios.post('/admin/dataList', body)
},
set_openPrice(body: object) {
return $axios.post('/admin/updOpenPrice', body)
},
}

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: 564  |  Height: 442  |  Size: 100 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>Maton Trading</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')
)

30
src/pages/account-review/index.tsx

@ -0,0 +1,30 @@
import api from "@/api"
import MyTable from "@/components/MyTable"
import { getTime } from "@/utils"
import React from "react"
const AccountReview = () => {
const columns = [
{ dataIndex: 'account', title: '帐号', width: 200 },
{ dataIndex: 'password', title: '密码', width: 200 },
{
dataIndex: 'time', title: '时间', width: 200, render: (time) => (
<div>{getTime(time * 1000)}</div>
)
},
{ dataIndex: 'review', title: '审核人', width: 200 },
{ dataIndex: 'status', title: '状态' },
]
return (
<div>
<MyTable
apiFun={api.get_accountReviewList}
columns={columns}
/>
</div>
)
}
export default AccountReview

65
src/pages/assets-manage/assets-list.tsx

@ -0,0 +1,65 @@
import api from "@/api"
import MyTable from "@/components/MyTable"
import { getTime } from "@/utils"
import React from "react"
const AssetsList = () => {
const columns = [
{
title: '状态',
dataIndex: 'status',
width: 150,
},
{
title: '类型',
dataIndex: 'type',
width: 200,
},
{
title: '时间',
width: 200,
dataIndex: 'time',
render: (time) => (
<div>{getTime(time * 1000)}</div>
)
},
{
title: '用户',
dataIndex: 'email',
width: 240,
},
{
title: 'MT账号',
dataIndex: 'account',
width: 200,
},
{
title: '金额',
dataIndex: 'amount',
width: 200
},
{
title: '订单号',
dataIndex: 'order',
width: 200
},
{
title: '审核人',
dataIndex: 'review',
width: 200
},
]
return (
<div>
<MyTable
apiFun={api.get_assetsList}
columns={columns}
/>
</div>
)
}
export default AssetsList

51
src/pages/assets-manage/escrow-list.tsx

@ -0,0 +1,51 @@
import api from "@/api"
import MyTable from "@/components/MyTable"
import { getTime } from "@/utils"
import React from "react"
const EscrowList = () => {
const columns = [
{
title: '账号',
dataIndex: 'account',
width: 200,
},
{
title: '金额',
dataIndex: 'amount',
width: 200,
},
{
title: '类型',
dataIndex: 'type',
width: 200
},
{
title: '开始时间',
dataIndex: 'start_time',
width: 240,
render: (time) => (
<div>{getTime(time * 1000)}</div>
)
},
{
title: '结束时间',
dataIndex: 'end_time',
render: (time) => (
<div>{getTime(time * 1000)}</div>
)
}
]
return (
<div>
<MyTable
apiFun={api.get_escrowList}
columns={columns}
/>
</div>
)
}
export default EscrowList

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;
}

124
src/pages/home/index.tsx

@ -0,0 +1,124 @@
import api from "@/api"
import { Button, Form, Input, Modal, notification } from "antd"
import React, { useEffect, useState } from "react"
const Home = () => {
const [visible, setVisible] = useState(false)
const [form] = Form.useForm()
const [data, setData] = useState([
{
title: '累计入金',
value: '0',
color: '#97d3c5',
id: 1
},
{
title: '累计取款',
value: '0',
color: '#f4b162',
id: 2
},
{
title: '本月存款入金',
value: '0',
color: '#71d398',
id: 3
},
{
title: '本月转账入金',
value: '0',
color: '#f68484',
id: 4
},
{
title: '本月取款金额',
value: '0',
color: '#e18484',
id: 5
},
{
title: '本月新开户',
value: '0',
color: '#e65097',
id: 6
}
])
const onFinish = async (values) => {
setVisible(false)
const res: any = await api.set_openPrice({ ...values })
if (res.code === 0) {
form.resetFields()
notification.success({
message: '修改成功'
})
}
}
const getData = async () => {
const res: any = await api.get_homeData()
if (res.code === 0) {
data[0].value = res.data.total_deposit
data[1].value = res.data.total_withdraw
data[2].value = res.data.deposit
data[3].value = res.data.transfer
data[4].value = res.data.withdraw_amount
data[5].value = res.data.new_member
setData([...data])
}
}
useEffect(() => {
getData()
}, [])
useEffect(() => {
!visible && form.resetFields()
}, [visible])
return (
<div>
<Modal
visible={visible}
onCancel={() => setVisible(false)}
title="修改价格"
footer={() => null}
>
<Form form={form} onFinish={onFinish}>
<Form.Item label="价格" name="price">
<Input placeholder="请输入要修改的价格" />
</Form.Item>
<Form.Item>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button onClick={() => setVisible(false)}></Button>
<Button type="primary" htmlType="submit" style={{ marginLeft: 20 }}></Button>
</div>
</Form.Item>
</Form>
</Modal>
<Button style={{ marginBottom: 20 }} type="primary" onClick={() => setVisible(true)}></Button>
<div style={{ display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', width: 550 }}>
{
data.map(item => (
<div key={item.id} style={{ width: 260, height: 90, background: item.color, marginBottom: 20, borderRadius: 5, padding: '0 10px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div style={{ fontWeight: 'bold' }}>
<div style={{ fontSize: 32 }}>{item.value}</div>
<div>{item.title}</div>
</div>
{
item.id != 6 && (
<div style={{ fontSize: 32 }}>$</div>
)
}
</div>
</div>
))
}
</div>
</div>
)
}
export default Home

BIN
src/pages/home/vector.png

After

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

127
src/pages/login/index.tsx

@ -0,0 +1,127 @@
import React, { useEffect, FC, useMemo, useState, useRef } from 'react'
import { useHistory } from 'react-router-dom'
import { LockOutlined, UserOutlined } from '@ant-design/icons'
import { Form, Input, Button, Select, Cascader, Space, Row } 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()
const [codeUrl, setCodeUrl] = useState('')
const codeId = useRef('')
useEffect(() => {
const { token } = userInfo
if (token) {
history.push('/')
return
}
// 重置 tab栏为首页
setStoreData('SET_CURTAB', ['/'])
}, [history, setStoreData, userInfo])
const getCode = async () => {
const res: any = await common.get_code()
if (res.code === 0) {
setCodeUrl(res.data.url)
codeId.current = res.data.id
}
}
useEffect(() => {
getCode()
}, [])
// 触发登录方法
const onFinish = (values: CommonObjectType<string>): void => {
const { username } = values;
common.login({ ...values, id: codeId.current }).then((res: any) => {
// 登录后返回的数据,包括权限
const resData = {
userName: username,
token: res.data.token,
permission: res.data.c || []
}
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
name="code"
rules={[{
required: true, message: '请输入验证码'
}]}
>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input placeholder='' />
<img src={codeUrl} style={{ width: 150, height: 30 }} onClick={getCode} />
</div>
</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">Maton Trading</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;
}
}

39
src/pages/manage/deposit.tsx

@ -0,0 +1,39 @@
import api from "@/api"
import MyTable from "@/components/MyTable"
import { getTime } from "@/utils"
import React from "react"
const DepositManage = () => {
const columns = [
{ "dataIndex": "account", "title": "MT账户", width: 200 },
{ "dataIndex": "deposit_method", "title": "存款方式", width: 240 },
{
"dataIndex": "time", "title": "时间", render: (time) => (
<div>{getTime(time * 1000)}</div>
)
},
{
"dataIndex": "url", "title": "图片", render: (url) => (
<img src={url} style={{ width: 100, height: 50, objectFit: 'contain' }} alt="" onClick={() => {
window.open(url)
}} />
)
},
{ "dataIndex": "email", "title": "用户" },
{ "dataIndex": "order", "title": "订单号" },
{ "dataIndex": "status", "title": "状态" },
{ "dataIndex": "symbol", "title": "币种" },
]
return (
<div>
<MyTable
apiFun={api.get_depositManageList}
columns={columns}
/>
</div>
)
}
export default DepositManage

30
src/pages/manage/receive.tsx

@ -0,0 +1,30 @@
import api from "@/api"
import MyTable from "@/components/MyTable"
import React from "react"
const ReceiveManage = () => {
const columns = [
{ "dataIndex": "Withdraw_method", "title": "取款方式", width: 200 },
{ "dataIndex": "time", "title": "时间", width: 200 },
{ "dataIndex": "account", "title": "MT账户", width: 200 },
{
"dataIndex": "address", "title": "钱包地址", width: 240, render: (text) => (
<div style={{ width: 200 }}>{text}</div>
)
},
{ "dataIndex": "member_account", "title": "会员帐号", width: 300 },
{ "dataIndex": "type", "title": "钱包类型", width: 200 }
]
return (
<div>
<MyTable
apiFun={api.get_receiveManageList}
columns={columns}
/>
</div>
)
}
export default ReceiveManage

37
src/pages/manage/withdraw.tsx

@ -0,0 +1,37 @@
import api from "@/api"
import MyTable from "@/components/MyTable"
import { getTime } from "@/utils"
import React from "react"
const WithdrawManage = () => {
const columns = [
{ "dataIndex": "status", "title": "状态" },
{ "dataIndex": "Withdraw_method", "title": "取款方式" },
{
"dataIndex": "time", "title": "时间", render: (time) => (
<div>{getTime(time * 1000)}</div>
)
},
{ "dataIndex": "account", "title": "MT账户" },
{ "dataIndex": "address", "title": "钱包地址" },
{ "dataIndex": "amount", "title": "取款金额" },
{ "dataIndex": "email", "title": "用户" },
{ "dataIndex": "fee", "title": "取款手续费" },
{ "dataIndex": "order", "title": "订单号" },
{ "dataIndex": "symbol", "title": "币种" },
{ "dataIndex": "type", "title": "钱包类型" },
{ "dataIndex": "withdraw_source", "title": "取款来自" }
]
return (
<div>
<MyTable
apiFun={api.get_withdrawManageList}
columns={columns}
/>
</div>
)
}
export default WithdrawManage

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

67
src/pages/review/deposit.tsx

@ -0,0 +1,67 @@
import api from "@/api"
import MyTable from "@/components/MyTable"
import { getTime } from "@/utils"
import { Tooltip } from "antd"
import { title } from "process"
import React from "react"
const DepositReview = () => {
const columns = [
{
title: '状态',
dataIndex: 'status',
width: 150,
},
{
title: '存款方式',
dataIndex: 'deposit_method',
width: 200,
},
{
title: '时间',
dataIndex: 'time',
width: 200,
render: (time) => (
<div>{getTime(time * 1000)}</div>
)
},
{
title: '图片',
dataIndex: 'url',
width: 150,
align: 'center',
render: (url) => (
<img src={url} style={{ width: 100, height: 50, objectFit: 'contain' }} onClick={() => {
window.open(url)
}} />
)
},
{
title: '币种',
width: 150,
dataIndex: 'symbol'
},
{
title: '用户',
dataIndex: 'email',
width: 200
},
{
title: '订单号',
dataIndex: 'order'
},
]
return (
<div>
<MyTable
apiFun={api.get_depositReviewList}
columns={columns}
/>
</div>
)
}
export default DepositReview

34
src/pages/review/withdraw.tsx

@ -0,0 +1,34 @@
import api from "@/api"
import MyTable from "@/components/MyTable"
import { getTime } from "@/utils"
import React from "react"
const WithdrawReview = () => {
const columns = [
{ "dataIndex": "Withdraw_method", "title": "取款方式" },
{ "dataIndex": "withdraw_source", "title": "取款来自" },
{ "dataIndex": "account", "title": "MT账户" },
{ "dataIndex": "address", "title": "钱包地址" },
{ "dataIndex": "amount", "title": "取款金额" },
{ "dataIndex": "time", "title": "时间", render: (time) => (<div>{getTime(time * 1000)}</div>) },
{ "dataIndex": "email", "title": "用户" },
{ "dataIndex": "fee", "title": "取款手续费" },
{ "dataIndex": "order", "title": "订单号" },
{ "dataIndex": "status", "title": "状态" },
{ "dataIndex": "status_code", "title": "状态码" },
{ "dataIndex": "symbol", "title": "币种" },
{ "dataIndex": "type", "title": "钱包类型" },
]
return (
<div>
<MyTable
apiFun={api.get_withdrawReviewList}
columns={columns}
/>
</div>
)
}
export default WithdrawReview

38
src/pages/system/notify.tsx

@ -0,0 +1,38 @@
import api from "@/api"
import MyTable from "@/components/MyTable"
import { getTime } from "@/utils"
import { Tooltip } from "antd"
import React from "react"
const SystemNofify = () => {
const columns = [
{
"dataIndex": "title", "title": "标题", width: 340, render: (text) => (
<Tooltip title={text}>
<div style={{ maxWidth: 300, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{text}</div>
</Tooltip>
)
},
{
"dataIndex": "content", "title": "内容", width: 340, render: (text) => (
<Tooltip title={text}>
<div style={{ maxWidth: 300, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{text}</div>
</Tooltip>
)
},
{ "dataIndex": "status", "title": "状态" },
{ "dataIndex": "time", "title": "时间", render: (time) => (<div>{getTime(time * 1000)}</div>) },
]
return (
<div>
<MyTable
apiFun={api.get_systemNofify}
columns={columns}
/>
</div>
)
}
export default SystemNofify

26
src/pages/system/receive.tsx

@ -0,0 +1,26 @@
import api from "@/api"
import MyTable from "@/components/MyTable"
import React from "react"
const SystemReceive = () => {
const columns = [
{ "dataIndex": "address", "title": "钱包地址", width: 200 },
{ "dataIndex": "fee", "title": "手续费" },
{ "dataIndex": "name", "title": "名称", width: 200 },
{ "dataIndex": "status", "title": "状态" },
{ "dataIndex": "symbol", "title": "币符号" },
{ "dataIndex": "type", "title": "钱包类型" }
]
return (
<div>
<MyTable
apiFun={api.get_systemReceive}
columns={columns}
/>
</div>
)
}
export default SystemReceive

27
src/pages/system/withdraw.tsx

@ -0,0 +1,27 @@
import api from "@/api"
import MyTable from "@/components/MyTable"
import React from "react"
const SystemWithdraw = () => {
const columns = [
{ "dataIndex": "fee", "title": "手续费", width: 200 },
{ "dataIndex": "leastAmount", "title": "最小金额", width: 200 },
{ "dataIndex": "maxAmount", "title": "最大金额", width: 200 },
{ "dataIndex": "name", "title": "名称", width: 200 },
{ "dataIndex": "status", "title": "状态" },
{ "dataIndex": "symbol", "title": "币符号" }
]
return (
<div>
<MyTable
columns={columns}
apiFun={api.get_systemWithdraw}
/>
</div>
)
}
export default SystemWithdraw

230
src/pages/vip-manage/proxy-list.tsx

@ -0,0 +1,230 @@
import React, { useEffect, useRef, useState } from "react";
import api from "@/api";
import MyTable from "@/components/MyTable";
import { DeleteFilled, EditOutlined, RightCircleFilled } from "@ant-design/icons";
import { Button, Form, Input, Modal, Popconfirm, Radio, Select, Switch, notification } from "antd";
import { getOriginUrl } from "@/utils/axios";
const ProxyList = () => {
const [modal, setModal] = useState(false);
const [visible, setVisible] = useState(false);
const [path, setPath] = useState('wallet');
const currentAccount = useRef({} as any);
const [form] = Form.useForm();
const tableRef = useRef(null);
const options = [
{ "value": 1, "label": "IB" },
{ "value": 2, "label": "IB1" },
{ "value": 3, "label": "IB2" },
{ "value": 4, "label": "IB3" },
{ "value": 5, "label": "IB4" },
{ "value": 6, "label": "IB5" },
{ "value": 7, "label": "IB6" },
{ "value": 8, "label": "IB7" },
{ "value": 9, "label": "IB8" },
];
const getToken = async (item) => {
const url = getOriginUrl();
const targetWindow = window.open(url, '_blank');
const res: any = await api.get_userToken({ id: item.id });
if (res.code === 0) {
for (let i = 0; i < 5; i++) {
setTimeout(() => {
targetWindow.postMessage({ token: res.data.token }, url);
}, i * 1000);
}
}
};
const deleteAccount = async (item) => {
const res: any = await api.delete_account({ id: item.id });
if (res.code === 0) {
tableRef.current.update();
notification.success({
message: '删除成功!'
});
}
};
const onFinish = async (values, apiFunc, successMessage) => {
const res: any = await apiFunc({
...values,
id: currentAccount.current.id
});
if (res.code === 0) {
form.resetFields();
setModal(false);
tableRef.current.update();
notification.success({
message: successMessage
});
}
};
const renderForm = (path) => {
const forms = {
wallet: {
onFinish: (values) => onFinish(values, api.set_userWalletBalance, '修改成功'),
items: [
{ label: "当前余额", name: "balance", initialValue: currentAccount.current.balance, disabled: true },
{ label: "加减金额", name: "balance", rules: [{ required: true, message: '请输入加减金额' }] },
{ label: "邮箱通知", name: "email_flag", initialValue: true, valuePropName: "checked", component: <Switch /> }
]
},
lv: {
onFinish: (values) => onFinish(values, api.set_lv, '设置成功'),
items: [
{ label: "等级", name: "level", initialValue: currentAccount.current.level_code, rules: [{ required: true, message: '请选择等级' }], component: <Select options={options} /> }
]
},
pwd: {
onFinish: (values) => onFinish(values, api.set_loginPwd, '重置成功'),
items: [
{ label: "登录密码", name: "password", rules: [{ required: true, message: '请输入要修改的登录密码' }] },
{ label: "邮箱通知", name: "email_flag", initialValue: true, valuePropName: "checked", component: <Switch /> }
]
},
txPwd: {
onFinish: (values) => onFinish(values, api.set_txPwd, '重置成功'),
items: [
{ label: "交易密码", name: "account_password", rules: [{ required: true, message: '请输入要修改的交易密码' }] },
{ label: "邮箱通知", name: "email_flag", initialValue: true, valuePropName: "checked", component: <Switch /> }
]
}
};
const currentForm = forms[path];
if (currentForm) {
return (
<Form form={form} onFinish={currentForm.onFinish}>
{currentForm.items.map((item, index) => (
<Form.Item key={index} label={item.label} name={item.name} rules={item.rules} initialValue={item.initialValue} valuePropName={item.valuePropName}>
{item.component || <Input placeholder={`请输入${item.label}`} disabled={item.disabled} />}
</Form.Item>
))}
<Form.Item>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button onClick={() => setModal(false)}></Button>
<Button type="primary" htmlType="submit" style={{ marginLeft: 20 }}></Button>
</div>
</Form.Item>
</Form>
);
}
};
const onFinishAddAccount = async (values) => {
const res: any = await api.add_account({ ...values });
if (res.code === 0) {
form.resetFields();
setVisible(false);
tableRef.current.update();
notification.success({
message: '添加成功'
});
}
};
const columns = [
{
title: '操作',
render: (row) => {
return (
<div style={{ display: "flex", alignItems: 'center' }}>
<EditOutlined onClick={() => {
currentAccount.current = row;
setModal(true);
}} />
<RightCircleFilled style={{ margin: '0 10px' }} onClick={() => {
getToken(row);
}} />
{/* <Popconfirm title={` "${row.name}" `} onConfirm={() => deleteAccount(row)}>
<DeleteFilled />
</Popconfirm> */}
</div>
);
}
},
{ title: '姓名', dataIndex: 'name' },
{ title: 'MT账号', dataIndex: 'account' },
{ title: '总资产', dataIndex: 'total_assets' },
{ title: 'MT余额', dataIndex: 'account_amount' },
{ title: '钱包余额', dataIndex: 'balance' },
{ title: '本金', dataIndex: 'principal' },
{ title: '会员账号', dataIndex: 'email' },
{ title: '代理级别', dataIndex: 'level' },
{ title: '观察者密码', dataIndex: 'observer_password' },
{ title: '交易密码', dataIndex: 'account_password' },
{ title: '登录密码', dataIndex: 'password' },
{ title: '邀请码', dataIndex: 'inviti_code' },
{ title: '推荐人的邀请码', dataIndex: 'inviti' }
];
useEffect(() => {
!modal && form.resetFields();
!visible && form.resetFields();
}, [modal, visible]);
return (
<div>
<MyTable
apiFun={api.get_userList}
columns={columns}
ref={tableRef}
header={<div style={{ marginBottom: 20 }}>
<Button type="primary" onClick={() => setVisible(true)}></Button>
</div>}
/>
<Modal
visible={modal}
onCancel={() => setModal(false)}
width="50%"
title={`操作:${currentAccount.current.email}`}
bodyStyle={{ padding: "20px" }}
footer={() => null}
>
<Radio.Group value={path} onChange={e => setPath(e.target.value)}>
<Radio.Button value="wallet"></Radio.Button>
<Radio.Button value="lv"></Radio.Button>
<Radio.Button value="pwd"></Radio.Button>
<Radio.Button value="txPwd"></Radio.Button>
</Radio.Group>
<div style={{ marginTop: 20 }}>
{renderForm(path)}
</div>
</Modal>
<Modal
visible={visible}
onCancel={() => setVisible(false)}
width="50%"
title={`添加账户`}
bodyStyle={{ padding: "20px" }}
footer={() => null}
>
<Form form={form} onFinish={onFinishAddAccount}>
<Form.Item label="名字" name="name" rules={[{ required: true, message: '请输入用户名称' }]}>
<Input placeholder="请输入用户名称" />
</Form.Item>
<Form.Item label="邮箱" name="email" rules={[{ required: true, message: '请输入用户邮箱' }, { type: 'email', message: '请输入有效的邮箱' }]}>
<Input placeholder="请输入用户邮箱" />
</Form.Item>
<Form.Item label="密码" name="password" rules={[{ required: true, message: '请输入用户密码' }]}>
<Input placeholder="请输入用户密码" />
</Form.Item>
<Form.Item label="邀请码" name="inviti_code" rules={[{ required: true, message: '请输入邀请码' }]}>
<Input placeholder="请输入邀请码" />
</Form.Item>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button onClick={() => setVisible(false)}></Button>
<Button type="primary" htmlType="submit" style={{ marginLeft: 20 }}></Button>
</div>
</Form>
</Modal>
</div>
);
};
export default ProxyList;

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'

177
src/route/routes.ts

@ -0,0 +1,177 @@
import Home from "@/pages/home"
import ProxyList from '@/pages/vip-manage/proxy-list'
import AssetsManage from '@/pages/assets-manage/escrow-list'
import { HomeOutlined, SolutionOutlined } from "@ant-design/icons"
import AssetsList from "@/pages/assets-manage/assets-list"
import WithdrawReview from "@/pages/review/withdraw"
import DepositReview from "@/pages/review/deposit"
import WithdrawManage from "@/pages/manage/withdraw"
import DepositManage from "@/pages/manage/deposit"
import ReceiveManage from "@/pages/manage/receive"
import SystemWithdraw from "@/pages/system/withdraw"
import SystemNotify from "@/pages/system/notify"
import SystemReceive from "@/pages/system/receive"
import AccountReview from "@/pages/account-review"
const routes = [
{
path: '/',
name: '仪表盘',
exact: true,
key: 'home',
icon: HomeOutlined,
component: Home,
routes: []
},
{
path: '/account-review',
name: '账号审核',
exact: true,
key: 'account-review',
icon: SolutionOutlined,
component: AccountReview,
routes: []
},
{
path: '/system',
name: '系统设置',
key: 'system',
icon: SolutionOutlined,
exact: true,
type: 'subMenu',
routes: [
{
path: '/system-notify',
name: '平台公告',
exact: true,
key: 'system-notify',
component: SystemNotify,
routes: []
},
{
path: '/system-receive',
name: '平台收款管理',
exact: true,
key: 'system-receive',
component: SystemReceive,
routes: []
},
{
path: '/system-withdraw',
name: '取款方式管理',
exact: true,
key: 'system-withdraw',
component: SystemWithdraw,
routes: []
}
]
},
{
path: '/vip-manage',
name: '会员管理',
key: 'vip-manage',
exact: true,
icon: SolutionOutlined,
type: 'subMenu',
routes: [
{
path: '/proxy-list',
name: '代理列表',
exact: true,
key: 'proxy-list',
component: ProxyList,
routes: []
}
]
},
{
path: '/assets-manage',
name: '资金账户管理',
icon: SolutionOutlined,
key: 'assets-manage',
exact: true,
type: 'subMenu',
routes: [
{
path: '/escrow-list',
name: '托管记录',
exact: true,
key: 'escrow-list',
component: AssetsManage,
routes: []
},
{
path: '/assets-list',
name: '资金明细',
exact: true,
key: 'assets-list',
component: AssetsList,
routes: []
}
]
},
{
path: '/review',
name: '存取款审核',
key: 'review',
icon: SolutionOutlined,
exact: true,
type: 'subMenu',
routes: [
{
path: '/withdraw-review',
name: '取款审核',
exact: true,
key: 'withdraw-review',
component: WithdrawReview,
routes: []
},
{
path: '/deposit-review',
name: '存款审核',
exact: true,
key: 'deposit-review',
component: DepositReview,
routes: []
}
]
},
{
path: '/manage',
name: '存取款管理',
key: 'manage',
icon: SolutionOutlined,
exact: true,
type: 'subMenu',
routes: [
{
path: '/deposit-manage',
name: '转账存款列表',
exact: true,
key: 'deposit-manage',
component: DepositManage,
routes: []
},
{
path: '/withdraw-manage',
name: '会员取款列表',
exact: true,
key: 'withdraw-manage',
component: WithdrawManage,
routes: []
},
{
path: '/receiver-manage',
name: '收款信息列表',
exact: true,
key: 'receiver-manage',
component: ReceiveManage,
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'

115
src/utils/axios.ts

@ -0,0 +1,115 @@
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://203.161.61.234:8082/api/v1`
// return `https://matontrading.com/api/v1`
}
export const getOriginUrl = () => {
return `https://matontrading.com`
}
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"
]
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save