Browse Source

first

master
yyy9608 1 year ago
commit
47e89eb07e
  1. 11
      .editorconfig
  2. 12
      .eslintignore
  3. 58
      .eslintrc copy.js
  4. 208
      .gitattributes
  5. 16
      .gitignore
  6. 15
      .prettierrc.js
  7. 94
      CODE_OF_CONDUCT.md
  8. 21
      LICENSE
  9. 100
      README.md
  10. 3
      config-overrides.js
  11. 82
      config/webpack.config.js
  12. 1
      env
  13. 95
      package.json
  14. 8
      paths.json
  15. BIN
      public/favicon.ico
  16. 57
      public/index.html
  17. 15
      public/manifest.json
  18. 9914
      public/static/color.less
  19. 17
      public/static/less.min.js
  20. 3
      src-tauri/.gitignore
  21. 3496
      src-tauri/Cargo.lock
  22. 26
      src-tauri/Cargo.toml
  23. 3
      src-tauri/build.rs
  24. BIN
      src-tauri/icons/128x128.png
  25. BIN
      src-tauri/icons/128x128@2x.png
  26. BIN
      src-tauri/icons/32x32.png
  27. BIN
      src-tauri/icons/Square107x107Logo.png
  28. BIN
      src-tauri/icons/Square142x142Logo.png
  29. BIN
      src-tauri/icons/Square150x150Logo.png
  30. BIN
      src-tauri/icons/Square284x284Logo.png
  31. BIN
      src-tauri/icons/Square30x30Logo.png
  32. BIN
      src-tauri/icons/Square310x310Logo.png
  33. BIN
      src-tauri/icons/Square44x44Logo.png
  34. BIN
      src-tauri/icons/Square71x71Logo.png
  35. BIN
      src-tauri/icons/Square89x89Logo.png
  36. BIN
      src-tauri/icons/StoreLogo.png
  37. BIN
      src-tauri/icons/icon.icns
  38. BIN
      src-tauri/icons/icon.ico
  39. BIN
      src-tauri/icons/icon.png
  40. 8
      src-tauri/src/main.rs
  41. 63
      src-tauri/tauri.conf.json
  42. 17
      src/App.tsx
  43. 13
      src/api/index.ts
  44. 157
      src/assets/css/public.less
  45. 69
      src/assets/img/login-bg.svg
  46. BIN
      src/assets/img/logo.png
  47. 1
      src/assets/img/logo.svg
  48. 456
      src/assets/js/publicFunc.ts
  49. 42
      src/components/BreadCrumb/index.tsx
  50. 29
      src/components/Header/Header.module.less
  51. 97
      src/components/Header/index.tsx
  52. 8
      src/components/Menu/Menu.module.less
  53. 145
      src/components/Menu/index.tsx
  54. 7
      src/components/MyIconfont/index.tsx
  55. 55
      src/components/MySelect/index.tsx
  56. 287
      src/components/MyTable/index.tsx
  57. 77
      src/components/SearchForm/index.tsx
  58. 8
      src/components/TabPanes/TabPanes.module.less
  59. 319
      src/components/TabPanes/index.tsx
  60. 10
      src/config/index.ts
  61. 26
      src/index.tsx
  62. 26
      src/package/SimpleTrade/api/admin-permission.ts
  63. 33
      src/package/SimpleTrade/api/admin-user.ts
  64. 105
      src/package/SimpleTrade/api/client.ts
  65. 40
      src/package/SimpleTrade/api/index.ts
  66. 68
      src/package/SimpleTrade/config/layout.ts
  67. 117
      src/package/SimpleTrade/config/menu.ts
  68. 53
      src/package/SimpleTrade/pages/admininvite/chart/data.ts
  69. 131
      src/package/SimpleTrade/pages/admininvite/chart/index.tsx
  70. 268
      src/package/SimpleTrade/pages/adminpermission/list/index.tsx
  71. 43
      src/package/SimpleTrade/pages/adminupload/uploadapk/index.tsx
  72. 3
      src/package/SimpleTrade/pages/adminuser/adminuser.less
  73. 211
      src/package/SimpleTrade/pages/adminuser/edit/index.tsx
  74. 250
      src/package/SimpleTrade/pages/adminuser/list/index.tsx
  75. 61
      src/package/SimpleTrade/pages/client/mt4history/index.tsx
  76. 110
      src/package/SimpleTrade/pages/client/mt4order/index.tsx
  77. 47
      src/package/SimpleTrade/pages/client/mt4settlementorder/index.tsx
  78. 90
      src/package/SimpleTrade/pages/client/recharge/index.tsx
  79. 236
      src/package/SimpleTrade/pages/client/user/index.tsx
  80. 128
      src/package/SimpleTrade/pages/client/withdraw/index.tsx
  81. 38
      src/package/SimpleTrade/pages/home/index.css
  82. 47
      src/package/SimpleTrade/pages/home/index.less
  83. 352
      src/package/SimpleTrade/pages/home/index.tsx
  84. BIN
      src/package/SimpleTrade/pages/home/vector.png
  85. 177
      src/package/SimpleTrade/route/routes.ts
  86. 38
      src/package/SimpleTrade/types/enum.ts
  87. 34
      src/pages/container/Home.module.less
  88. 136
      src/pages/container/index.tsx
  89. 100
      src/pages/login/index.tsx
  90. 36
      src/pages/login/login.less
  91. 13
      src/pages/public/errorPage/index.tsx
  92. 12
      src/react-app-env.d.ts
  93. 9
      src/route/routes.ts
  94. 27
      src/store/actionTypes/index.ts
  95. 7
      src/store/actions/index.ts
  96. 23
      src/store/index.ts
  97. 21
      src/store/reducers/index.ts
  98. 25
      src/store/state/index.ts
  99. 9
      src/test/App.test.jsx
  100. 7
      src/types/store.d.ts

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

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

16
.gitignore

@ -0,0 +1,16 @@
/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

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": "^3.8.3"
},
"scripts": {
"start": "cross-env PORT=3000 react-app-rewired start",
"test": "react-app-rewired test",
"build": "react-app-rewired build",
"deploy:dev": "npm run build && scp -r ./build/* vps:/home/ubuntu/pzy/simple",
"deploy:prod": "npm run build && scp -r ./build/* metatrader_prod:/data/wwwroot/admin-simple"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"compression-webpack-plugin": "^3.0.1",
"customize-cra": "^0.8.0",
"eslint": "^6.8.0",
"eslint-config-airbnb": "^18.0.1",
"eslint-config-prettier": "^6.10.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-webpack": "^0.11.1",
"eslint-plugin-html": "^6.0.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-react": "^7.18.3",
"eslint-plugin-react-hooks": "^2.3.0",
"husky": "^3.0.9",
"lint-staged": "^9.4.2",
"prettier": "1.18.2",
"react-app-rewired": "^2.1.5",
"redux-devtools": "^3.5.0",
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack-bundle-analyzer": "^3.6.0"
},
"homepage": ".",
"lint-staged": {
"*.{js,jsx}": "eslint"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}
}

8
paths.json

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

BIN
public/favicon.ico

57
public/index.html

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

15
public/manifest.json

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

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

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

3
src-tauri/.gitignore

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

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

26
src-tauri/Cargo.toml

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

3
src-tauri/build.rs

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

BIN
src-tauri/icons/128x128.png

After

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

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

After

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

BIN
src-tauri/icons/32x32.png

After

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

BIN
src-tauri/icons/Square107x107Logo.png

After

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

BIN
src-tauri/icons/Square142x142Logo.png

After

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

BIN
src-tauri/icons/Square150x150Logo.png

After

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

BIN
src-tauri/icons/Square284x284Logo.png

After

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

BIN
src-tauri/icons/Square30x30Logo.png

After

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

BIN
src-tauri/icons/Square310x310Logo.png

After

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

BIN
src-tauri/icons/Square44x44Logo.png

After

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

BIN
src-tauri/icons/Square71x71Logo.png

After

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

BIN
src-tauri/icons/Square89x89Logo.png

After

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

BIN
src-tauri/icons/StoreLogo.png

After

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

BIN
src-tauri/icons/icon.icns

BIN
src-tauri/icons/icon.ico

BIN
src-tauri/icons/icon.png

After

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

8
src-tauri/src/main.rs

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

63
src-tauri/tauri.conf.json

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

17
src/App.tsx

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

13
src/api/index.ts

@ -0,0 +1,13 @@
import $axios from '@/utils/axios'
export default {
// 获取数据
getList(params?: object): Promise<CommonObjectType<string>> {
return $axios.get('https://randomuser.me/api', params)
},
// 登录
login(params: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/login', params)
}
}

157
src/assets/css/public.less

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

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

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

BIN
src/assets/img/logo.png

After

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

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

456
src/assets/js/publicFunc.ts

@ -0,0 +1,456 @@
/* 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
const permissionArray = permission.reduce(
(prev: CommonObjectType<string>[], next: CommonObjectType<string>) => [
...prev,
next.code
],
[]
);
localStorage.setItem('permissions', permissionArray)
const result = {
userName,
permission,
token: token || oldToken
}
action('SET_USERINFO', result)
}
/**
* tree结构数据
* @param data
* @param parentIdField
*/
export const makeTree = (
data: Array<any>,
parentIdField: string = 'parentId'
): Array<any> => {
const dataMap: Object = {}
data.forEach((item) => {
dataMap[item.id] = item
})
const dataTree: Array<any> = []
data.forEach((item) => {
const parent = dataMap[item[parentIdField]]
if (parent) {
if (parent.children) {
parent.children.push(item)
} else {
parent.children = []
parent.children.push(item)
}
} else {
dataTree.push(item)
}
})
return dataTree
}
/**
* TreeSelect组件结构数据
* @param dataTree
* @param treeValueFiled
* @param treeTitleFiled
*/
export const makeTreeSelectData = (
dataTree: Array<any>,
treeValueFiled: string = 'value',
treeTitleFiled: string = 'title'
): Array<any> => {
const treeSelect: Array<any> = []
dataTree.forEach((item) => {
const temp = {
value: item[treeValueFiled],
title: item[treeTitleFiled],
children: item.children || undefined
}
if (temp.children) {
temp.children = makeTreeSelectData(
temp.children,
treeValueFiled,
treeTitleFiled
)
}
treeSelect.push(temp)
})
return treeSelect
}
/**
* Tree组件结构数据
* @param dataTree
* @param treeKeyFiled
* @param treeTitleFiled
*/
export const makeTreeData = (
dataTree: Array<any>,
treeKeyFiled: string = 'key',
treeTitleFiled: string = 'title'
): Array<any> => {
const treeSelect: Array<any> = []
dataTree.forEach((item) => {
const temp = {
key: item[treeKeyFiled],
title: item[treeTitleFiled],
children: item.children || undefined
}
if (temp.children) {
temp.children = makeTreeData(
temp.children,
treeKeyFiled,
treeTitleFiled
)
}
treeSelect.push(temp)
})
return treeSelect
}
/**
* id数组
* @param tree
* @param id
*/
export const treeFindParentById = (tree: Array<any>, id: string | number): Array<number | string> => {
const path = []
if (tree.length === 0) return []
const forFn = function (tree, id) {
for (let i = 0; i < tree.length; i++) {
const data = tree[i]
if (data.id === id) {
return path
}
path.push(data.id)
if (data.children) {
const findChildren = forFn(data.children, id);
if (findChildren && findChildren.length > 0) return findChildren
}
path.pop()
}
}
forFn(tree, id)
return path
}
/**
*
* @param arr1
* @param arr2
*/
export const diffArray = (arr1: Array<number | string>, arr2: Array<number | string>): Array< number | string>=> {
const diff: Array<number | string> = []
for (let idx = 0; idx < arr1.length; idx++) {
if (!arr2.includes(arr1[idx])) {
diff.push(arr1[idx])
}
}
return diff
}

42
src/components/BreadCrumb/index.tsx

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

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

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

97
src/components/Header/index.tsx

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

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

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

145
src/components/Menu/index.tsx

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

7
src/components/MyIconfont/index.tsx

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

55
src/components/MySelect/index.tsx

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

287
src/components/MyTable/index.tsx

@ -0,0 +1,287 @@
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;
}
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
} = 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)
}
setSearchParams(val)
setTableParams({ ...tableParams, ...val, 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 && (
<SearchView
ref={searchForm}
config={searchConfigList}
beforeSearch={beforeSearch}
handleSearch={handleSearch}
onFieldsChange={onFieldsChange}
/>
)}
{/* 列表 */}
<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 '@/package/SimpleTrade/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)

10
src/config/index.ts

@ -0,0 +1,10 @@
export const httpIp = {
simpleTradeKey: {
dev: 'http://125.94.244.254:30303/api/v1',
prod: 'https://simpletrade.site/api/v1'
}
}
export const getBaseURL = () => {
return httpIp.simpleTradeKey.dev
}

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')
)

26
src/package/SimpleTrade/api/admin-permission.ts

@ -0,0 +1,26 @@
import $axios from '@/utils/axios'
export default {
getAdminPermissions(params?: object): Promise<CommonObjectType<string>> {
return $axios.get('/admin/adminpermissions', params)
},
getAdminPermission(id: number | string): Promise<CommonObjectType<string>> {
return $axios.get(`/admin/adminpermissions/${id}`)
},
addAdminPermission(params: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/adminpermissions', params)
},
updateAdminPermission(
id: number,
params: object
): Promise<CommonObjectType<string>> {
return $axios.put(`/admin/adminpermissions/${id}`, params)
},
deleteAdminPermission(params: object): Promise<CommonObjectType<string>> {
return $axios.delete('/admin/adminpermissions', params)
}
}

33
src/package/SimpleTrade/api/admin-user.ts

@ -0,0 +1,33 @@
import $axios from '@/utils/axios'
export default {
getAdminUserList(params?: object): Promise<CommonObjectType<string>> {
return $axios.get('/admin/adminusers', params)
},
getAdminUser(id: number | string): Promise<CommonObjectType<string>> {
return $axios.get(`/admin/adminusers/${id}`)
},
addAdminUser(params: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/adminusers', params)
},
updateAdminUser(
id: number | string,
params: object
): Promise<CommonObjectType<string>> {
return $axios.put(`/admin/adminusers/${id}`, params)
},
updateAdminUserPwd(
id: number,
params: object
): Promise<CommonObjectType<string>> {
return $axios.put(`/admin/adminusers/pwd/${id}`, params)
},
deleteAdminUser(params: object): Promise<CommonObjectType<string>> {
return $axios.delete('/admin/adminusers', params)
}
}

105
src/package/SimpleTrade/api/client.ts

@ -0,0 +1,105 @@
import $axios from '@/utils/axios'
export default {
/**
* @description
* @param {page}
* @param {page_size}
*/
user_list(params?: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/user/list', params);
},
/**
* @description
* @param {page}
* @param {page_size}
*/
withdraw_list(params?: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/chain/withdraw', params);
},
/**
* @description
* @param {page}
* @param {page_size}
*/
recharge_list(params?: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/chain/deposit', params);
},
/**
* @description
*/
user_relation(): Promise<CommonObjectType<string>> {
return $axios.post('/admin/user/relation');
},
/**
* @description
*/
add_relation(params: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/user/relation/add', params);
},
/**
* @description MT4历史记录
*/
mt4_history(params: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/forex/mt4/history', params);
},
/**
* @description MT4下单列表
*/
mt4_order_list(params: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/forex/order/list', params);
},
/**
* @description MT4结算列表
*/
mt4_settle_list(params: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/forex/settle/list', params);
},
/**
* @description
*/
up_levle(params: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/debug/upLevel', params);
},
/**
* @description
*/
add_debug_user(params: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/debug/addDebugUser', params);
},
/**
* @description
*/
withdraw_check(params: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/chain/withdraw/check', params);
},
/**
* @description
*/
close_order(params: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/forex/orderClose', params);
},
/**
* @description
*/
user_rename(params: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/user/upRemark', params);
},
/**
* @description
*/
user_deactivate(params: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/user/upDeactivate', params);
}
}

40
src/package/SimpleTrade/api/index.ts

@ -0,0 +1,40 @@
import $axios from '@/utils/axios'
export default {
// 获取数据
getList(params?: object): Promise<CommonObjectType<string>> {
return $axios.get('https://randomuser.me/api', params)
},
// 登录
login(params: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/login', params)
},
// 首页用户统计
user_statistics(): Promise<CommonObjectType<string>> {
return $axios.post('/admin/userAmount')
},
// 首页充值提现统计
recharge_withdraw_statistics(): Promise<CommonObjectType<string>> {
return $axios.post('/admin/depositWithdrawal', { type: 1 })
},
// 首页周期充值提现统计
week_recharge_withdraw_statistics(
params: object
): Promise<CommonObjectType<string>> {
return $axios.post('/admin/depositWithdrawal', params)
},
/**
* @description
* @param {number} end_time
* @param {number} start_time
* @param {number} type 1. 2.
*/
transaction_statistics(params?: object): Promise<CommonObjectType<string>> {
return $axios.post('/admin/prodruct', params)
}
}

68
src/package/SimpleTrade/config/layout.ts

@ -0,0 +1,68 @@
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 3 }
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 17 }
}
}
const wrapperCol = {
xs: { span: 24, offset: 0 },
sm: { span: 16, offset: 3 }
}
const modalLayoutSm = {
labelCol: {
xs: { span: 4 },
sm: { span: 4 }
},
wrapperCol: {
xs: { span: 20 },
sm: { span: 20 }
}
}
const modalLayoutSm5 = {
labelCol: {
xs: { span: 5 },
sm: { span: 5 }
},
wrapperCol: {
xs: { span: 20 },
sm: { span: 20 }
}
}
const modalLayoutMd = {
labelCol: {
xs: { span: 6 },
sm: { span: 6 }
},
wrapperCol: {
xs: { span: 18 },
sm: { span: 18 }
}
}
const modalLayoutLg = {
labelCol: {
xs: { span: 8 },
sm: { span: 8 }
},
wrapperCol: {
xs: { span: 16 },
sm: { span: 16 }
}
}
export {
formItemLayout,
wrapperCol,
modalLayoutSm,
modalLayoutSm5,
modalLayoutMd,
modalLayoutLg
}

117
src/package/SimpleTrade/config/menu.ts

@ -0,0 +1,117 @@
import { HomeOutlined, UserOutlined, AuditOutlined } from '@ant-design/icons'
const menus = [
{
path: '/',
name: '首页',
key: 'home',
icon: HomeOutlined,
routes: [],
},
{
path: '/adminuser',
name: '用户管理',
key: 'adminuser',
type: 'subMenu',
icon: UserOutlined,
iconfont: 'icon-xiaoshouzongjian',
routes: [
{
path: '/adminuser/list',
name: '用户列表',
key: 'adminuser:list:view'
}
]
},
{
path: '/adminpermission',
name: '权限管理',
key: 'adminpermission',
type: 'subMenu',
icon: AuditOutlined,
routes: [
{
path: '/adminpermission/list',
name: '权限列表',
key: 'adminpermission:list:view',
remove: true
}
]
},
{
path: '/admininvite',
name: '邀请',
key: 'admininvite',
type: 'subMenu',
icon: AuditOutlined,
routes: [
{
path: '/admininvite/chart',
name: '关联图',
key: 'admininvite:list:chart'
}
]
},
{
path: '/adminupload',
name: '上传管理',
key: 'adminupload',
type: 'subMenu',
icon: AuditOutlined,
routes: [
{
path: '/adminupload/uploadapk',
name: '上传APK',
key: 'admininvite:list:uploadapk'
}
]
},
{
path: '/client',
name: '客户端',
key: 'client',
type: 'subMenu',
icon: AuditOutlined,
routes: [
{
path: '/client/userlist',
name: '用户列表',
exact: true,
key: 'client:userlist:view',
remove: true
},
{
path: '/client/recharge',
name: '充值记录',
exact: true,
key: 'client:recharge:view',
},
{
path: '/client/withdraw',
name: '提现记录',
exact: true,
key: 'client:withdraw:view',
},
{
path: '/client/mt4history',
name: 'MT4历史记录',
exact: true,
key: 'client:mt4history:view',
},
{
path: '/client/mt4order',
name: 'MT4订单列表',
exact: true,
key: 'client:mt4order:view',
},
{
path: '/client/mt4settlementorder',
name: 'MT4结算订单列表',
exact: true,
key: 'client:mt4settlementorder:view',
}
]
}
]
export default menus

53
src/package/SimpleTrade/pages/admininvite/chart/data.ts

@ -0,0 +1,53 @@
import dagre from 'dagre';
const edgeType = 'smoothstep';
export const flattenTree = (tree) => {
let stack = [tree];
let nodes = [];
let edges = [];
while (stack.length > 0) {
let node = stack.pop();
nodes.push({ id: node.id, data: { ...node.data, label: `${node.data.name}` }, position: node.position });
if (node.children && node.children.length > 0) {
node.children.map(item => {
edges.push({ source: node.id, target: item.id, id: item.id, type: edgeType, animated: true });
stack.unshift(item);
});
};
};
return { nodes, edges };
};
export const getLayoutedElements = (nodes, edges, direction = 'TB') => {
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
const nodeWidth = 172;
const nodeHeight = 36;
const isHorizontal = direction === 'LR';
dagreGraph.setGraph({ rankdir: direction });
nodes.forEach((node: any) => {
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
});
edges.forEach((edge: any) => {
dagreGraph.setEdge(edge.source, edge.target);
});
dagre.layout(dagreGraph);
nodes.forEach((node: any) => {
const nodeWithPosition = dagreGraph.node(node.id);
node.targetPosition = isHorizontal ? 'left' : 'top';
node.sourcePosition = isHorizontal ? 'right' : 'bottom';
node.position = {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2,
};
return node;
});
return { nodes, edges };
};

131
src/package/SimpleTrade/pages/admininvite/chart/index.tsx

@ -0,0 +1,131 @@
import React, { useCallback, FC, useEffect, useState, useRef } from 'react';
import ReactFlow, {
addEdge,
ConnectionLineType,
useNodesState,
useEdgesState,
Controls
} from 'react-flow-renderer';
import { getLayoutedElements, flattenTree } from './data';
import { Button, Input, Modal, notification } from 'antd';
import clientApi from '@/package/SimpleTrade/api/client';
const InviteChart: FC = () => {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [isOpenModal, setIsOpenModal] = useState(false)
const [loading, setLoading] = useState(false)
const [inviteCode, setInviteCode] = useState('')
const [address, setAddress] = useState('')
const onConnect = useCallback(
(params: any) => {
setEdges((eds) =>
addEdge({ ...params, type: ConnectionLineType.SmoothStep, animated: true }, eds)
)
},
[]
);
const onLayout = useCallback(
(direction: any) => {
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
nodes,
edges,
direction
);
setNodes([...layoutedNodes]);
setEdges([...layoutedEdges]);
},
[nodes, edges]
);
// 获取节点
const getNodes = async () => {
const res: any = await clientApi.user_relation();
if (res.code === 0 && Object.keys(res.data).length > 0) {
let { nodes, edges } = flattenTree(res.data) // 处理节点及连接线
let { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(nodes, edges)//计算节点位置
setNodes(layoutedNodes)
setEdges(layoutedEdges)
};
};
// 添加
const addNodes = async () => {
if (!address) {
notification.error({
message: '请输入地址'
})
return;
}
setLoading(true)
try {
const res: any = await clientApi.add_debug_user({
address,
invite_code: inviteCode
});
setLoading(false)
if (res.code === 0) {
notification.success({
message: '创建成功'
})
getNodes()
setIsOpenModal(false)
// let { nodes, edges } = flattenTree(res.data) // 处理节点及连接线
// let { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(nodes, edges)//计算节点位置
// setNodes(layoutedNodes)
// setEdges(layoutedEdges)
}
} catch (error) {
setLoading(false)
}
}
useEffect(() => {
getNodes();
}, []);
useEffect(() => {
if (!isOpenModal) {
setInviteCode('')
setAddress('')
}
}, [isOpenModal])
return (
<div>
<Button type='primary' onClick={() => onLayout('TB')}></Button>
<Button type='primary' onClick={() => onLayout('LR')}></Button>
<div style={{ width: '100%', height: window.innerHeight - 165 }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
connectionLineType={ConnectionLineType.SmoothStep}
fitView
onNodeClick={(e, node) => {
setInviteCode(node.data.invite_code)
setIsOpenModal(true)
}}
>
<Controls />
</ReactFlow>
</div>
<Modal
visible={isOpenModal}
onCancel={() => setIsOpenModal(false)}
onOk={addNodes}
title="创建模拟账号"
confirmLoading={loading}
>
<Input value={address} onChange={e => setAddress(e.target.value)} placeholder="请输入地址" />
</Modal>
</div>
);
};
export default InviteChart;

268
src/package/SimpleTrade/pages/adminpermission/list/index.tsx

@ -0,0 +1,268 @@
import React, { FC, useEffect, useRef, useState } from 'react'
import {
isAuthorized,
makeTree,
makeTreeSelectData
} from '@/assets/js/publicFunc'
import {
Button,
Table,
Modal,
Form,
Switch,
Input,
TreeSelect,
message,
Spin
} from 'antd'
import { modalLayoutSm } from '@/package/SimpleTrade/config/layout'
import AdminPermissionApi from '@/package/SimpleTrade/api/admin-permission'
import SearchForm from '@/components/SearchForm'
interface AdminPermission {
id: number;
name: string;
code: string;
isHelp: boolean;
parentId: number;
updateTime: string;
createTime: string;
}
const AdminPermissionList: FC = () => {
const [loading, setLoading] = useState<boolean>(false)
const [btnLoading, setBtnLoading] = useState<boolean>(false)
const [visible, setVisible] = useState<boolean>(false)
const [title, setTitle] = useState<string>('新增权限')
const [permissionId, setPermissionId] = useState<number>(0)
const [permissionList, setPermissionList] = useState<Array<any>>([])
const [treeSelectList, setTreeSelectList] = useState<Array<any>>([])
const [form] = Form.useForm()
const { setFieldsValue, getFieldsValue, resetFields } = form
const searchForm = useRef()
const getAdminPermissions = (search: object = {}) => {
AdminPermissionApi.getAdminPermissions(search).then((res: any) => {
const permissions = res.data ? res.data : []
const adminPermissionsTree = makeTree(permissions)
setPermissionList(adminPermissionsTree)
})
}
const setTreeSelectData = () => {
const root = [{ value: 0, title: '根所属' }]
const treeSelectData = root.concat(
makeTreeSelectData(permissionList, 'id', 'name')
)
setTreeSelectList(treeSelectData)
}
useEffect(() => {
getAdminPermissions()
// eslint-disable-next-line
}, [])
const add = () => {
resetFields()
setPermissionId(0)
setVisible(true)
setTitle('新增权限')
setFieldsValue({ parentId: 0, code: '' })
setTreeSelectData()
}
const edit = (record: any) => {
setPermissionId(record.id)
setFieldsValue({ ...record })
setVisible(true)
setTitle('编辑权限')
setTreeSelectData()
}
const handleSearch = (values) => {
getAdminPermissions(values)
}
const cancelModel = () => {
setVisible(false)
}
const delAdminPermission = (record: AdminPermission) => {
setBtnLoading(true)
const ids = [record.id]
AdminPermissionApi.deleteAdminPermission(ids)
.then((res) => {
message.success(res.message)
getAdminPermissions()
})
.finally(() => {
setBtnLoading(false)
})
}
const handleSubmit = () => {
setLoading(true)
if (permissionId) {
AdminPermissionApi.updateAdminPermission(permissionId, getFieldsValue())
.then((res) => {
message.success(res.message)
setVisible(false)
getAdminPermissions()
})
.finally(() => {
setLoading(false)
})
} else {
AdminPermissionApi.addAdminPermission(getFieldsValue())
.then((res) => {
message.success(res.message)
setVisible(false)
getAdminPermissions()
})
.finally(() => {
setLoading(false)
})
}
}
const AddBtn = () => (
<Button className="fr" onClick={add} type="primary">
</Button>
)
const columns: any = [
{
title: '名称',
dataIndex: 'name'
},
{
title: '权限标识',
dataIndex: 'code'
},
{
title: '操作',
dataIndex: 'operations',
align: 'center',
render: (text, record) => (
<>
{isAuthorized('adminpermission:list:del') && (
<Button
className="btn mr-5"
onClick={() => delAdminPermission(record)}
size="small"
type="primary"
loading={btnLoading}
danger
>
</Button>
)}
{isAuthorized('adminpermission:list:edit') && (
<Button
className="btn"
onClick={() => edit(record)}
size="small"
type="primary"
>
</Button>
)}
</>
)
}
]
// 搜索栏配置项
const searchConfigList = [
{
key: 'name',
slot: <Input placeholder="名称" allowClear />,
initialValue: ''
}
]
const nameValidator = (rule, value) => {
if (!value) {
return Promise.reject(new Error('请输入名称'))
}
return Promise.resolve()
}
const codeValidator = (rule, value) => {
const parentId = form.getFieldValue('parentId')
const isHelp = form.getFieldValue('isHelp')
if (isHelp) {
return Promise.resolve()
}
if (!parentId && !value) {
return Promise.reject(new Error('请输入权限标识'))
}
if (!parentId && !/^[a-zA-Z0-9:]+$/.test(value)) {
return Promise.reject(new Error('权限标识格式不正确'))
}
return Promise.resolve()
}
return (
<>
{isAuthorized('adminpermission:list:add') && <AddBtn />}
{visible && (
<Modal
title={title}
visible={visible}
onCancel={cancelModel}
footer={null}
>
<Spin spinning={loading}>
<Form {...modalLayoutSm} form={form} onFinish={handleSubmit}>
<Form.Item label="所属" name="parentId">
<TreeSelect
style={{ width: '100%' }}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={treeSelectList}
placeholder="请选择"
/>
</Form.Item>
<Form.Item label="辅助信息" name="isHelp" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item
label="名称"
name="name"
rules={[{ validator: nameValidator }]}
>
<Input placeholder="请输入名称" />
</Form.Item>
<Form.Item
label="权限标识"
name="code"
rules={[{ validator: codeValidator }]}
>
<Input placeholder="格式(字母数字:组合):abc:cde" />
</Form.Item>
<Form.Item
wrapperCol={{
xs: { span: 20, offset: 4 },
sm: { span: 20, offset: 4 }
}}
>
<Button type="primary" htmlType="submit">
</Button>
</Form.Item>
</Form>
</Spin>
</Modal>
)}
<SearchForm
ref={searchForm}
handleSearch={handleSearch}
config={searchConfigList}
/>
<Table
rowKey={(record) => record.id}
columns={columns}
dataSource={permissionList}
pagination={false}
/>
</>
)
}
export default AdminPermissionList

43
src/package/SimpleTrade/pages/adminupload/uploadapk/index.tsx

@ -0,0 +1,43 @@
import React, { FC } from 'react';
import { InboxOutlined } from '@ant-design/icons';
import { message, Upload } from 'antd';
const props = {
name: 'file',
multiple: true,
action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76',
onChange(info) {
const { status } = info.file;
if (status !== 'uploading') {
console.log(info.file, info.fileList);
}
if (status === 'done') {
message.success(`${info.file.name} file uploaded successfully.`);
} else if (status === 'error') {
message.error(`${info.file.name} file upload failed.`);
}
},
onDrop(e) {
console.log('Dropped files', e.dataTransfer.files);
},
};
const { Dragger } = Upload;
const UploadAPK: FC = () => {
return (
<div style={{ height: '50vh' }}>
<div style={{ width: '100%' }}>
<Dragger {...props}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text"></p>
<p className="ant-upload-hint"></p>
</Dragger>
</div>
</div>
)
}
export default UploadAPK;

3
src/package/SimpleTrade/pages/adminuser/adminuser.less

@ -0,0 +1,3 @@
.mr-5 {
margin-right: 5px;
}

211
src/package/SimpleTrade/pages/adminuser/edit/index.tsx

@ -0,0 +1,211 @@
import React, { useState, FC, useEffect } from 'react'
import { useHistory } from 'react-router-dom'
import { Form, Input, Button, message, Spin, Tree } from 'antd'
import { formItemLayout, wrapperCol } from '@/package/SimpleTrade/config/layout'
import {
closeTabAction,
getQuery,
makeTree,
makeTreeData,
treeFindParentById,
diffArray
} from '@/assets/js/publicFunc'
import MySelect from '@/components/MySelect'
import AdminUserApi from '@/package/SimpleTrade/api/admin-user'
import AdminPermissionApi from '@/package/SimpleTrade/api/admin-permission'
const FormView: FC = () => {
const query = getQuery()
const { id } = query
// 原始权限tree
let originalTreeData: Array<any> = []
const [form] = Form.useForm()
const { setFieldsValue, getFieldsValue, resetFields } = form
const [loading, setLoading] = useState<boolean>(false)
const [treeData, setTreeData] = useState<Array<any>>([])
const [checkedKeys, setCheckedKeys] = useState<Array<number | string>>([])
const history: CommonObjectType = useHistory()
// 获取权限列表数据
const getPermissions = async () => {
await AdminPermissionApi.getAdminPermissions().then((res: any) => {
const data: Array<any> = res.data ? res.data : []
originalTreeData = makeTree(data)
const treeDataTemp = makeTreeData(originalTreeData, 'id', 'name')
setTreeData(treeDataTemp)
})
}
useEffect(() => {
getPermissions()
if (id) {
AdminUserApi.getAdminUser(id).then((res) => {
const data: any = res.data ? res.data : {}
const adminUsers = data.adminUser ? data.adminUser : {}
const permissions = data.permissions ? data.permissions : []
const permissionIds = permissions.map((item) => {
return item.permissionId
})
// tree父子受控回显需要过滤掉父级ID
let parents: Array<number | string> = []
permissionIds.forEach((item: number | string) => {
const temp: Array<number | string> = treeFindParentById(
originalTreeData,
item
)
parents = parents.concat(temp)
})
const checkIds: Array<number | string> = diffArray(
permissionIds,
parents
)
setCheckedKeys(checkIds)
adminUsers.status = adminUsers.status.toString()
resetFields()
setFieldsValue({ ...adminUsers })
})
} else {
setFieldsValue({ status: '1' })
}
// eslint-disable-next-line
}, [id])
const onCheck = (checkedKeysValue: any) => {
setCheckedKeys(checkedKeysValue)
setFieldsValue({ permissions: checkedKeysValue })
}
const handleSubmit = () => {
setLoading(true)
const submitForm: any = { ...getFieldsValue() }
if (typeof submitForm.status === 'string') {
submitForm.status = parseInt(submitForm.status, 0)
}
if (id) {
AdminUserApi.updateAdminUser(id, submitForm)
.then((res) => {
message.success(res.message)
})
.finally(() => {
setLoading(false)
})
} else {
AdminUserApi.addAdminUser(submitForm)
.then((res) => {
setLoading(false)
message.success(res.message)
const returnUrl = '/adminuser/list'
closeTabAction(history, returnUrl)
})
.catch(() => {
setLoading(false)
})
}
}
const nameValidator = (rule, value) => {
if (value.length < 4) {
return Promise.reject(new Error('用户名最少4个字符'))
}
if (!/^[a-zA-Z0-9]{4,}$/.test(value)) {
return Promise.reject(new Error('用户名只能是数字字母下划线组合'))
}
return Promise.resolve()
}
const repasswordValidator = (rule, value) => {
const password = form.getFieldValue('password')
if (password && password !== value) {
return Promise.reject(new Error('两次密码输入不一致'))
}
return Promise.resolve()
}
return (
<Spin spinning={loading}>
<Form {...formItemLayout} form={form} onFinish={handleSubmit}>
<Form.Item
label="用户名"
name="name"
rules={[
{
required: true,
validator: nameValidator
}
]}
>
<Input placeholder="请输入用户名" />
</Form.Item>
{!id && (
<Form.Item
label="密码"
name="password"
rules={[
{
required: true,
message: '请输入密码'
},
{
min: 6
}
]}
>
<Input placeholder="请输入密码" />
</Form.Item>
)}
{!id && (
<Form.Item
label="确认密码"
name="repassword"
rules={[
{
required: true,
validator: repasswordValidator
}
]}
>
<Input placeholder="请输入确认密码" />
</Form.Item>
)}
<Form.Item
label="状态"
name="status"
rules={[
{
required: true,
message: '请选择状态'
}
]}
>
<MySelect
data={[{ key: '0', name: '冻结' }, { key: '1', name: '正常' }]}
placeholder="请选择状态"
/>
</Form.Item>
<Form.Item label="权限" name="permissions">
<Tree
checkable
onCheck={onCheck}
checkedKeys={checkedKeys}
treeData={treeData}
/>
</Form.Item>
<Form.Item wrapperCol={wrapperCol}>
<Button type="primary" htmlType="submit">
</Button>
</Form.Item>
</Form>
</Spin>
)
}
export default FormView

250
src/package/SimpleTrade/pages/adminuser/list/index.tsx

@ -0,0 +1,250 @@
import React, { useRef, FC, useState } from 'react'
import { useHistory } from 'react-router-dom'
import { Button, Form, Input, Modal, Tag, Spin, message } from 'antd'
import MyTable from '@/components/MyTable'
import { isAuthorized } from '@/assets/js/publicFunc'
import MySelect from '@/components/MySelect'
import AdminUserApi from '@/package/SimpleTrade/api/admin-user'
import { modalLayoutSm5 } from '@/package/SimpleTrade/config/layout'
import '../adminuser.less'
interface AdminUser {
id: number;
name: string;
status: number;
updateTime: string;
createTime: string;
}
const StatusArr: Array<string> = ['冻结', '正常']
const StatusTagArr: Array<string> = ['gold', 'blue']
const AdminUserList: FC = () => {
const tableRef: RefType = useRef()
const history = useHistory()
const [form] = Form.useForm()
const { getFieldsValue, resetFields } = form
const [loading, setLoading] = useState<boolean>(false)
const [btnLoading, setBtnLoading] = useState<boolean>(false)
const [visible, setVisible] = useState<boolean>(false)
const [id, setId] = useState<number>(0)
const [delIds, setDelIds] = useState<Array<string | number>>([])
const [delDisabeld, setDelDisabeld] = useState<boolean>(true)
const addAdminUser = () => {
history.push('/adminuser/list/add')
}
const editAdminUserPwd = (record: AdminUser) => {
resetFields()
setVisible(true)
setId(record.id)
}
const cancelModel = () => {
setVisible(false)
}
const editAdminUser = (record: AdminUser) => {
history.push(`/adminuser/list/edit?id=${record.id}`)
}
const onSelectRow = (rowKeys: Array<string | number>) => {
setDelIds(rowKeys)
if (rowKeys.length > 0) {
setDelDisabeld(false)
} else {
setDelDisabeld(true)
}
}
const repasswordValidator = (rule, value) => {
const password = form.getFieldValue('password')
if (password && password !== value) {
return Promise.reject(new Error('两次密码输入不一致'))
}
return Promise.resolve()
}
const delAdminUser = () => {
setBtnLoading(true)
AdminUserApi.deleteAdminUser(delIds)
.then((res: any) => {
message.success(res.message)
tableRef.current.update()
})
.finally(() => {
setBtnLoading(false)
})
}
const handleSubmit = () => {
setLoading(true)
const submitForm = { ...getFieldsValue() }
AdminUserApi.updateAdminUserPwd(id, submitForm)
.then((res: any) => {
message.success(res.message)
})
.finally(() => {
setLoading(false)
})
}
// 搜索栏配置项
const searchConfigList = [
{
key: 'name',
slot: <Input placeholder="用户名" allowClear />,
initialValue: ''
},
{
key: 'status',
slot: (
<MySelect
data={[{ name: '正常', key: '1' }, { name: '冻结', key: '0' }]}
placeholder="状态"
/>
)
}
]
const columns = [
{
title: '用户名',
dataIndex: 'name'
},
{
title: '状态',
dataIndex: 'status',
render: (status: number) => (
<>
<Tag color={StatusTagArr[status]}>{StatusArr[status]}</Tag>
</>
)
},
{
title: '更新时间',
dataIndex: 'updateTime'
},
{
title: '创建时间',
dataIndex: 'createTime'
},
{
title: '操作',
dataIndex: 'operations',
align: 'center',
render: (text, record) => (
<>
{isAuthorized('adminuser:list:edit') && (
<div>
<Button
className="btn mr-5"
onClick={() => editAdminUserPwd(record)}
size="small"
type="primary"
danger
>
</Button>
<Button
className="btn"
onClick={() => editAdminUser(record)}
size="small"
type="primary"
>
</Button>
</div>
)}
</>
)
}
]
const delAdminUserEl = (
<Button
className="fr mr-5"
onClick={delAdminUser}
type="primary"
disabled={delDisabeld}
loading={btnLoading}
danger
>
</Button>
)
const addAdminUserEl = (
<Button className="fr" onClick={addAdminUser} type="primary">
</Button>
)
return (
<>
{visible && (
<Modal
title="修改密码"
visible={visible}
onCancel={cancelModel}
footer={null}
>
<Spin spinning={loading}>
<Form {...modalLayoutSm5} form={form} onFinish={handleSubmit}>
<Form.Item
label="新密码"
name="password"
rules={[
{
required: true,
message: '请输入新密码'
},
{
min: 6,
message: '新密码长度最少6个字符'
}
]}
>
<Input placeholder="请输入新密码" />
</Form.Item>
<Form.Item
label="确认新密码"
name="repassword"
rules={[
{
required: true,
validator: repasswordValidator
}
]}
>
<Input placeholder="请输入确认新密码" />
</Form.Item>
<Form.Item
wrapperCol={{
xs: { span: 20, offset: 5 },
sm: { span: 20, offset: 5 }
}}
>
<Button type="primary" htmlType="submit">
</Button>
</Form.Item>
</Form>
</Spin>
</Modal>
)}
{isAuthorized('adminuser:list:add') && addAdminUserEl}
{isAuthorized('adminuser:list:del') && delAdminUserEl}
<MyTable
apiFun={AdminUserApi.getAdminUserList}
columns={columns}
ref={tableRef}
onSelectRow={onSelectRow}
searchConfigList={searchConfigList}
/>
</>
)
}
export default AdminUserList

61
src/package/SimpleTrade/pages/client/mt4history/index.tsx

@ -0,0 +1,61 @@
import MyTable from "@/components/MyTable";
import React, { FC } from "react";
import clientApi from "@/package/SimpleTrade/api/client";
import { Input } from "antd";
const MT4History: FC = () => {
const column = [
{
title: '订单号',
dataIndex: 'ticket',
align: 'center'
},
{
title: '手数',
dataIndex: 'volume',
align: 'center'
},
{
title: '开仓价格',
dataIndex: 'open_price',
align: 'center'
},
{
title: '开仓时间',
dataIndex: 'open_time',
align: 'center'
},
{
title: '平仓价格',
dataIndex: 'close_price',
align: 'center'
},
{
title: '平仓时间',
dataIndex: 'close_time',
align: 'center'
},
]
const searchConfigList = [
{
key: 'login',
slot: <Input placeholder="输入MT4账号" allowClear />,
initialValue: '',
}
]
return (
<div>
<MyTable
apiFun={clientApi.mt4_history}
columns={column}
rowKey="ticket"
searchConfigList={searchConfigList}
/>
</div>
)
}
export default MT4History;

110
src/package/SimpleTrade/pages/client/mt4order/index.tsx

@ -0,0 +1,110 @@
import React, { FC, useState, useMemo, useRef } from "react";
import MyTable from "@/components/MyTable";
import clientApi from "@/package/SimpleTrade/api/client";
import { Button, Modal, notification } from "antd";
const MT4Order: FC = () => {
const tableRefs = useRef<any>()
const [visible, setVisible] = useState(false)
const [currentItem, setCurrentItem] = useState({} as { [key: string]: any })
const statusText = useMemo(() => (
{
'-2': '创建订单失败',
'-1': '取消订单',
'0': '发送前',
'1': '挂单中',
'2': '寻找B仓',
'3': '止盈止损设置',
'4': '持仓中',
'5': '已结算',
}
), [])
const column = useMemo(() => [
{
title: '订单ID',
dataIndex: 'order'
},
{
title: '账号',
dataIndex: 'login'
},
{
title: '开仓价格',
dataIndex: 'open_price'
},
{
title: '开仓时间',
dataIndex: 'open_time',
render: (_time) => (<div>{_time}</div>)
},
{
title: '平仓价格',
dataIndex: 'close_price',
},
{
title: '平仓时间',
dataIndex: 'close_time',
render: (time) => <div>{time.indexOf('1970-01-01') < 0 && time}</div>
},
{
title: '状态',
dataIndex: 'handle_status',
render: (status) => (
<div>{statusText[`${status}`] || ''}</div>
)
},
// {
// title: '操作',
// dataIndex: 'operations',
// align: 'center',
// key: Date.now(),
// render: (text, record) => (
// <div>
// {record.account_forex_type === 1 && record.handle_status === 4 && (
// <Button type='primary' size='small' onClick={() => {
// setVisible(true)
// setCurrentItem(record)
// }}>平仓</Button>
// )}
// </div>
// )
// }
], [])
// const closePosition = async () => {
// let params = {
// login: currentItem.login,
// ticket: `${currentItem.order}`,
// volume: currentItem.volume
// }
// setVisible(false)
// const res: any = await clientApi.close_order(params)
// if (res.code === 0) {
// notification.success({
// message: '平仓成功'
// })
// setCurrentItem({})
// setTimeout(() => {
// tableRefs.current?.update()
// }, 1000)
// }
// }
return (
<div>
<MyTable
ref={tableRefs}
apiFun={clientApi.mt4_order_list}
columns={column}
rowKey="order"
/>
{/* <Modal visible={visible} onCancel={() => setVisible(false)} title='' onOk={closePosition}>
<div></div>
</Modal> */}
</div>
)
}
export default MT4Order;

47
src/package/SimpleTrade/pages/client/mt4settlementorder/index.tsx

@ -0,0 +1,47 @@
import MyTable from "@/components/MyTable";
import React, { FC } from "react";
import clientApi from "@/package/SimpleTrade/api/client";
const MT4SettlementOrder: FC = () => {
const columns = [
{
title: '订单ID',
dataIndex: 'ticket'
},
{
title: '用户ID',
dataIndex: 'user_id',
align: 'center'
},
{
title: '手数',
dataIndex: 'volume',
align: 'center',
render: (volume) => (
<div>{Number(volume) / 100}</div>
)
},
{
title: '开仓价格',
dataIndex: 'open_price',
align: 'center'
},
{
title: '开仓时间',
dataIndex: 'open_time',
align: 'center'
},
]
return (
<div>
<MyTable
apiFun={clientApi.mt4_settle_list}
columns={columns}
/>
</div>
)
}
export default MT4SettlementOrder;

90
src/package/SimpleTrade/pages/client/recharge/index.tsx

@ -0,0 +1,90 @@
import React, { FC } from "react";
import clientApi from "@/package/SimpleTrade/api/client";
import MyTable from "@/components/MyTable";
import { Input } from "antd";
import { copy, getTime, splitAddress } from "@/utils";
import { CopyOutlined } from "@ant-design/icons";
const RechargeRecord: FC = () => {
const columns = [
{
title: '账号ID',
dataIndex: 'a_mt4_login'
},
{
title: '备注昵称',
dataIndex: 'remark'
},
{
title: 'from地址',
dataIndex: 'from_address',
render: (address) => (
<div style={{ display: 'flex', alignItems: 'center' }}>
<div>{splitAddress(address, 5)}</div>
<div style={{ marginLeft: 5, cursor: 'pointer' }} onClick={() => copy(address)}>
<CopyOutlined />
</div>
</div>
)
},
{
title: 'to地址',
dataIndex: 'to_address',
render: (address) => (
<div style={{ display: 'flex', alignItems: 'center' }}>
<div>{splitAddress(address, 5)}</div>
<div style={{ marginLeft: 5, cursor: 'pointer' }} onClick={() => copy(address)}>
<CopyOutlined />
</div>
</div>
)
},
{
title: '充值状态',
dataIndex: 'status'
},
{
title: '链ID',
dataIndex: 'chain_id'
},
{
title: '区块高度',
dataIndex: 'block_height'
},
{
title: '数量',
dataIndex: 'balance_real'
},
{
title: '币种',
dataIndex: 'symbol'
},
{
title: '时间',
dataIndex: 'create_time',
render: (_time) => (<div>{getTime(_time * 1000)}</div>)
}
]
// 搜索栏配置项
const searchConfigList = [
{
key: 'name',
slot: <Input placeholder="用户名" allowClear />,
initialValue: ''
}
]
return (
<div>
<MyTable
columns={columns}
apiFun={clientApi.recharge_list}
searchConfigList={searchConfigList}
rowKey="ID"
/>
</div>
)
};
export default RechargeRecord;

236
src/package/SimpleTrade/pages/client/user/index.tsx

@ -0,0 +1,236 @@
import React, { FC, useEffect, useMemo, useRef, useState } from "react";
import clientApi from "@/package/SimpleTrade/api/client";
import MyTable from "@/components/MyTable";
import { Button, Input, Modal, notification, Popconfirm, Select } from "antd";
import { copy, splitAddress } from "@/utils";
import { CopyOutlined } from "@ant-design/icons";
const UserList: FC = () => {
const [currentItem, setCurrentItem] = useState({} as any)
const [isModalOpen, setIsModalOpen] = useState(false)
const [lv, setLv] = useState(0);
const tableRefs = useRef<any>()
const options = useMemo(() => [
{ value: 0, label: 'R' },
{ value: 1, label: 'G' },
{ value: 2, label: 'G1' },
{ value: 3, label: 'G2' },
{ value: 4, label: 'G3' },
{ value: 5, label: 'G4' },
{ value: 6, label: 'G5' },
], [])
const columns = [
{
title: '邮箱',
dataIndex: 'email'
},
{
title: '备注昵称',
dataIndex: 'remark'
},
{
title: '地址',
dataIndex: 'address',
render: (address) => (
<div style={{ display: 'flex', alignItems: 'center' }}>
<div>{splitAddress(address, 5)}</div>
<div style={{ marginLeft: 5, cursor: 'pointer' }} onClick={() => copy(address)}>
<CopyOutlined />
</div>
</div>
)
},
{
title: 'A账号',
dataIndex: 'a_mt4_login'
},
{
title: 'A仓余额',
dataIndex: 'a_mt4_balance'
},
{
title: 'A仓订单数量',
dataIndex: 'a_product'
},
{
title: 'B账号',
dataIndex: 'b_mt4_login'
},
{
title: 'B仓余额',
dataIndex: 'b_mt4_balance'
},
{
title: 'B仓订单数量',
dataIndex: 'b_product'
},
{
title: '手数',
dataIndex: 'volume'
},
{
title: '级别',
dataIndex: 'level',
render: (level) => (
<div>{options[level] ? options[level].label : level}</div>
)
},
{
title: '保险开关',
dataIndex: 'insurance',
render: (status) => (
<div>{status === 1 ? '开' : '关'}</div>
)
},
{
title: '邀请码',
dataIndex: 'invite_code'
},
{
title: '推荐人',
dataIndex: 'referrer'
},
{
title: '操作',
dataIndex: 'operations',
align: 'center',
fixed: 'right',
key: Date.now(),
width: 180,
render: (text, record) => {
return (
<div>
<div>
<Button type="primary" className="btn" size="small" onClick={() => {
setCurrentItem(record)
setIsModalOpen(true)
setLv(record.level)
}}></Button>
<Button style={{ marginLeft: 10 }} type="primary" className="btn" size="small" onClick={() => rename(record.remark, record.invite_code)}></Button>
</div>
<div style={{ marginTop: 10 }}>
<Popconfirm title={`确认${!record.deactivate?'停用':'启用'}账号?`} onConfirm={()=>deactivateAccount(record.invite_code,record.deactivate)}>
<Button type="primary" className="btn" size="small" danger={!record.deactivate}>{!record.deactivate ? '停用账号' :'启用账号'}</Button>
</Popconfirm>
</div>
</div>
)
}
}
]
// 搜索栏配置项
const searchConfigList = [
{
key: 'name',
slot: <Input placeholder="用户名" allowClear />,
initialValue: ''
}
]
const nicknameRefs = useRef(null)
const deactivateAccount = async (code:string,status:boolean)=>{
const res:any = await clientApi.user_deactivate({
invite_code:code,
deactivate:!status
})
if(res.code === 0){
notification.success({
message: !status ? '停用成功' : '启用成功'
})
tableRefs.current?.update()
}
}
const rename = async (name: string, code: string) => {
Modal.confirm({
title: '修改昵称',
content: (
<Input ref={nicknameRefs} placeholder="请输入昵称" />
),
onOk: async () => {
let _name = nicknameRefs.current.state.value
const res: any = await clientApi.user_rename({
invite_code: code,
remark: _name
})
if (res.code === 0) {
notification.success({
message: '设置成功'
})
tableRefs.current?.update()
}
},
onCancel: () => {
nicknameRefs.current && nicknameRefs.current.setValue('')
},
})
setTimeout(() => {
nicknameRefs.current && nicknameRefs.current.setValue(name)
}, 100)
}
const setLevel = async () => {
if (currentItem.level === lv) {
notification.error({
message: '设置等级相同',
})
return;
}
let res: any = await clientApi.up_levle({
level: lv,
invite_code: currentItem.invite_code
})
if (res.code === 0) {
notification.success({
message: '设置成功'
})
setIsModalOpen(false)
tableRefs.current?.update()
}
}
const handleChange = (_, e) => {
setLv(e.value)
}
useEffect(() => {
if (!isModalOpen) {
setCurrentItem({})
setLv(0)
}
}, [isModalOpen])
return (
<div>
<MyTable
ref={tableRefs}
columns={columns}
apiFun={clientApi.user_list}
searchConfigList={searchConfigList}
rowKey="invite_code"
/>
<Modal
title="设置等级"
visible={isModalOpen}
onOk={setLevel}
onCancel={() => setIsModalOpen(false)}
>
{
isModalOpen && (
<Select
defaultValue={currentItem.level || 0}
options={options}
style={{ width: '100%' }}
onChange={handleChange}
/>
)
}
</Modal>
</div>
)
};
export default UserList;

128
src/package/SimpleTrade/pages/client/withdraw/index.tsx

@ -0,0 +1,128 @@
import React, { FC, useRef } from "react";
import clientApi from "@/package/SimpleTrade/api/client";
import MyTable from "@/components/MyTable";
import { Button, Input, notification, Popconfirm } from "antd";
import { CopyOutlined } from "@ant-design/icons";
import { copy, getTime, splitAddress } from "@/utils";
import { Withdraw_Check } from "@/package/SimpleTrade/types/enum";
const WithdrawRecord: FC = () => {
const tableRefs = useRef(null)
const columns = [
{
title: '账号ID',
dataIndex: 'a_mt4_login'
},
{
title: '备注昵称',
dataIndex: 'remark'
},
{
title: '交易ID',
dataIndex: 'tx_hash',
render: (tx_hash) => (
<div style={{ display: 'flex', alignItems: 'center' }}>
<div>{splitAddress(tx_hash, 5)}</div>
<div style={{ marginLeft: 5, cursor: 'pointer' }} onClick={() => copy(tx_hash)}>
<CopyOutlined />
</div>
</div>
)
},
{
title: '链ID',
dataIndex: 'chain_id'
},
{
title: '提币地址',
dataIndex: 'to_address',
render: (to_address) => (
<div style={{ display: 'flex', alignItems: 'center' }}>
<div>{splitAddress(to_address, 5)}</div>
<div style={{ marginLeft: 5, cursor: 'pointer' }} onClick={() => copy(to_address)}>
<CopyOutlined />
</div>
</div>
)
},
{
title: '数量',
dataIndex: 'amount'
},
{
title: '币种',
dataIndex: 'symbol'
},
{
title: '提现状态',
dataIndex: 'status'
},
{
title: '处理时间',
dataIndex: 'handle_time',
render: (time) => (
<div>{getTime(time * 1000)}</div>
)
},
{
title: '操作',
dataIndex: 'operations',
align: 'center',
key: Date.now(),
render: (_, item) => {
return item.check_status === 1 ? (<div>
<Popconfirm
title="确认拒绝吗?"
onConfirm={() => handleOrder(item, Withdraw_Check.WithdrawCheckReject)}
>
<Button size="small" type="primary" danger style={{ marginRight: 20 }}></Button>
</Popconfirm>
<Popconfirm
title="确认同意吗?"
onConfirm={() => handleOrder(item, Withdraw_Check.WithdrawCheckAgree)}
>
<Button size="small" type="primary"></Button>
</Popconfirm>
</div>) : (<></>)
}
}
]
// 搜索栏配置项
const searchConfigList = [
{
key: 'name',
slot: <Input placeholder="用户名" allowClear />,
initialValue: ''
}
]
const handleOrder = async (item, type: Withdraw_Check) => {
const res: any = await clientApi.withdraw_check({
id: item.id,
status: type
})
if (res.code === 0) {
notification.success({
message: type === Withdraw_Check.WithdrawCheckAgree ? '已同意' : '已拒绝'
})
tableRefs.current?.update()
}
}
return (
<div>
<MyTable
ref={tableRefs}
columns={columns}
apiFun={clientApi.withdraw_list}
searchConfigList={searchConfigList}
rowKey="id"
/>
</div>
)
};
export default WithdrawRecord;

38
src/package/SimpleTrade/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/package/SimpleTrade/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;
}

352
src/package/SimpleTrade/pages/home/index.tsx

@ -0,0 +1,352 @@
import React, { FC, useEffect, useState } from 'react'
import './index.less'
import { useSelector } from 'react-redux'
import api from '@/package/SimpleTrade/api'
import { DatePicker } from 'antd';
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import weekday from "dayjs/plugin/weekday"
import localeData from "dayjs/plugin/localeData"
dayjs.extend(customParseFormat);
dayjs.extend(weekday);
dayjs.extend(localeData);
const { RangePicker } = DatePicker;
const dateFormat = 'YYYY-MM-DD';
const Home: FC = () => {
const theme = useSelector((state: any) => state.storeData.theme)
const [userTable, setUserTable] = useState([])
const [newToDay, setNewToDay] = useState(0) //今日新增
const [userTotal, setUserTotal] = useState(0) //用户统计
const start_time = dayjs().subtract(7, 'days')
const end_time = dayjs()
const [defaultValue, setDefaultValue] = useState([start_time, end_time] as any) //周期合约时间
const [withdrawTime, setWithdrawTime] = useState([start_time, end_time] as any) //周期充值提现时间
const [toDayTx, setToDayTx] = useState([
{ title: '今日订单金额', value: '0' },
{ title: '今日订单数量', value: '0' },
{ title: 'A仓盈利订单数量', value: '0', color: '#3BB900' },
{ title: 'B仓盈利订单数量', value: '0', color: '#3BB900' },
{ title: 'A仓亏损订单数量', value: '0', color: '#F4002C' },
{ title: 'B仓亏损订单数量', value: '0', color: '#F4002C' },
{ title: '盈利订单金额', value: '0', color: '#3BB900' },
{ title: '亏损订单金额', value: '0', color: '#F4002C' },
])
const [weekTx, setWeekTx] = useState([
{ title: '订单总金额', value: '0' },
{ title: '订单总数量', value: '0' },
{ title: 'A仓盈利订单数量', value: '0', color: '#3BB900' },
{ title: 'B仓盈利订单数量', value: '0', color: '#3BB900' },
{ title: 'A仓亏损订单数量', value: '0', color: '#F4002C' },
{ title: 'B仓亏损订单数量', value: '0', color: '#F4002C' },
{ title: '盈利订单金额', value: '0', color: '#3BB900' },
{ title: '亏损订单金额', value: '0', color: '#F4002C' },
])
const [transaction, setTransaction] = useState([])
const [weekTransaction, setWeekTransaction] = useState([])
// 用户统计
const getUserData = async () => {
const res: any = await api.user_statistics()
if (res.code === 0) {
setNewToDay(res.data.NewToday)
setUserTotal(res.data.total_amount)
setUserTable(res.data.AdminUserAmountList)
}
}
// 周期合约交易订单统计
const getWeekTxOrderStatic = async () => {
const start = Math.floor(defaultValue[0].valueOf() / 1000)
const end = Math.floor(defaultValue[1].valueOf() / 1000)
const res: any = await api.transaction_statistics({
end_time: end,
start_time: start,
type: 2
})
if (res.code === 0) {
weekTx[0].value = res.data.product_amount
weekTx[1].value = res.data.product_num
weekTx[2].value = res.data.a_profit_num
weekTx[3].value = res.data.b_profit_num
weekTx[4].value = res.data.a_loss_num
weekTx[5].value = res.data.b_loss_num
weekTx[6].value = res.data.profit_amount
weekTx[7].value = res.data.loss_amount
setWeekTx([...weekTx])
}
}
// 周期充值提现
const getWeekTransaction = async () => {
const start = Math.floor(withdrawTime[0].valueOf() / 1000)
const end = Math.floor(withdrawTime[1].valueOf() / 1000)
const res: any = await api.week_recharge_withdraw_statistics({
end_time: end,
start_time: start,
type: 2
})
if (res.code === 0 && res.data) {
setWeekTransaction(res.data)
}
}
// 今日合约交易订单统计
const getDayTxOrderStatic = async () => {
const res: any = await api.transaction_statistics({
type: 1
})
if (res.code === 0) {
toDayTx[0].value = res.data.product_amount
toDayTx[1].value = res.data.product_num
toDayTx[2].value = res.data.a_profit_num
toDayTx[3].value = res.data.b_profit_num
toDayTx[4].value = res.data.a_loss_num
toDayTx[5].value = res.data.b_loss_num
toDayTx[6].value = res.data.profit_amount
toDayTx[7].value = res.data.loss_amount
setToDayTx([...toDayTx])
}
}
// 充值提现统计
const getRechargeAndWithdraw = async () => {
const res: any = await api.recharge_withdraw_statistics()
if (res.code === 0) {
setTransaction(res.data)
}
}
useEffect(() => {
getUserData()
getDayTxOrderStatic()
getRechargeAndWithdraw()
}, [])
useEffect(() => {
getWeekTxOrderStatic()
}, [defaultValue])
useEffect(() => {
getWeekTransaction()
}, [withdrawTime])
return (
<div className='home' style={{ minHeight: '80vh', backgroundColor: theme && '#fff' }}>
<h3>Hello, Randy!</h3>
<div>Today is a good day to start trading crypto assets!</div>
{/* */}
<div className='box' style={{ backgroundColor: theme ? '#f5f5f5' : '#333' }}>
<div style={{ fontWeight: 'bold' }}></div>
<div className='row'>
<div style={{ flex: 3, marginTop: 30 }}>
<h2>{userTotal.toLocaleString()}</h2>
<div className='row-items' style={{ marginTop: 20 }}>
<div className='img-box'>
<img src={require('./vector.png')} alt="" />
</div>
<div style={{ marginLeft: 10 }}>
<div style={{ fontWeight: 'bold' }}>+{newToDay.toLocaleString()}</div>
<div style={{ fontSize: 12, marginTop: 5 }}></div>
</div>
</div>
</div>
<div className='solid' style={{ backgroundColor: theme ? '#e8e8e8' : '#555' }}></div>
<div style={{ flex: 4, marginLeft: 20 }}>
<div className='row' style={{ textAlign: 'center' }}>
<div style={{ flex: 1 }}></div>
<div style={{ flex: 1 }}></div>
<div style={{ flex: 1 }}></div>
</div>
<div>
{
userTable.map((item, index) => (
<div key={index} className='row' style={{ textAlign: 'center', marginTop: 20 }}>
<div style={{ flex: 1 }}>{item.type}</div>
<div style={{ flex: 1 }}>
<div>{Number(item.yesterday_amount).toLocaleString()}</div>
<div>{item.yesterday_percentage}</div>
</div>
<div style={{ flex: 1, color: '#44D600' }}>
<div>{Number(item.new_today_amount).toLocaleString()}</div>
<div>{item.new_today_percentage}</div>
</div>
</div>
))
}
</div>
</div>
</div>
</div>
{/* */}
<div className='box row' style={{ backgroundColor: theme ? '#f5f5f5' : '#333', marginTop: 30 }}>
<div style={{ flex: 3 }}>
<div style={{ fontWeight: 'bold', fontSize: 14 }}></div>
<div style={{ marginTop: 20, textAlign: 'center', flexWrap: 'wrap' }} className='row-items'>
{
toDayTx.map((item, index) => (
<div key={index} style={{ width: '50%', marginTop: 10 }}>
<div style={{ fontSize: 14 }}>{item.title}</div>
<div style={{ fontWeight: 'bold', fontSize: 14, color: item.color }}>{Number(item.value).toLocaleString()}</div>
</div>
))
}
</div>
</div>
<div className='solid' style={{ backgroundColor: theme ? '#e8e8e8' : '#555', margin: '0 20px' }}></div>
<div style={{ flex: 4 }}>
<div className='row-between'>
<div className='row-items'>
<div style={{ fontWeight: 'bold', fontSize: 14 }}></div>
<div style={{ marginLeft: 10 }}>
<RangePicker
defaultValue={defaultValue}
onChange={setDefaultValue}
format={dateFormat}
/>
</div>
</div>
<div className='row-items'>
{/* <div>导出</div> */}
</div>
</div>
<div style={{ marginTop: 20, textAlign: 'center', flexWrap: 'wrap' }} className='row-items'>
{
weekTx.map((item, index) => (
<div key={index} style={{ width: '50%', marginTop: 10 }}>
<div style={{ fontSize: 14 }}>{item.title}</div>
<div style={{ fontWeight: 'bold', fontSize: 14, color: item.color }}>{Number(item.value).toLocaleString()}</div>
</div>
))
}
</div>
</div>
</div>
{/* */}
{transaction[0] && transaction[1] && <div className='box row' style={{ backgroundColor: theme ? '#f5f5f5' : '#333', marginTop: 30 }}>
<div style={{ flex: 4 }}>
<div style={{ fontWeight: 'bold', fontSize: 14 }}>{transaction[0].symbol} </div>
{/* <div style={{ fontSize: 14, marginTop: 30 }}><span style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>2131223</span></div>
<div style={{ fontSize: 14, marginTop: 30 }}><span style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>150%</span></div> */}
<div className='row' style={{ textAlign: 'center', marginTop: 30 }}>
<div style={{ flex: 1 }}>
<div></div>
<div style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>{Number(transaction[0].deposit_amount).toLocaleString()}</div>
</div>
<div style={{ flex: 1 }}>
<div></div>
<div style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>{transaction[0].deposit_num}</div>
</div>
</div>
<div className='row' style={{ textAlign: 'center', marginTop: 20 }}>
<div style={{ flex: 1 }}>
<div></div>
<div style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>{Number(transaction[0].withdrawal_amount).toLocaleString()}</div>
</div>
<div style={{ flex: 1 }}>
<div></div>
<div style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>{transaction[0].withdrawal_num}</div>
</div>
</div>
</div>
<div className='solid' style={{ backgroundColor: theme ? '#e8e8e8' : '#555', margin: '0 20px' }}></div>
<div style={{ flex: 4 }}>
<div style={{ fontWeight: 'bold', fontSize: 14 }}>{transaction[1].symbol} </div>
<div className='row' style={{ textAlign: 'center', marginTop: 30 }}>
<div style={{ flex: 1 }}>
<div></div>
<div style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>{Number(transaction[1].deposit_amount).toLocaleString()}</div>
</div>
<div style={{ flex: 1 }}>
<div></div>
<div style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>{transaction[1].deposit_num}</div>
</div>
</div>
<div className='row' style={{ textAlign: 'center', marginTop: 20 }}>
<div style={{ flex: 1 }}>
<div></div>
<div style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>{Number(transaction[1].withdrawal_amount).toLocaleString()}</div>
</div>
<div style={{ flex: 1 }}>
<div></div>
<div style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>{transaction[1].withdrawal_num}</div>
</div>
</div>
</div>
</div>}
{/* */}
{weekTransaction[0] && weekTransaction[1] && (
<div className='box' style={{ marginTop: 30, backgroundColor: theme ? '#f5f5f5' : '#333' }}>
<div>
<RangePicker
defaultValue={withdrawTime}
onChange={setWithdrawTime}
format={dateFormat}
/>
</div>
<div className='row' style={{ marginTop: 20 }}>
<div style={{ flex: 4 }}>
<div style={{ fontWeight: 'bold', fontSize: 14 }}>{weekTransaction[0].symbol} </div>
{/* <div style={{ fontSize: 14, marginTop: 30 }}><span style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>2131223</span></div>
<div style={{ fontSize: 14, marginTop: 30 }}><span style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>150%</span></div> */}
<div className='row' style={{ textAlign: 'center', marginTop: 30 }}>
<div style={{ flex: 1 }}>
<div></div>
<div style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>{Number(weekTransaction[0].deposit_amount).toLocaleString()}</div>
</div>
<div style={{ flex: 1 }}>
<div></div>
<div style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>{weekTransaction[0].deposit_num}</div>
</div>
</div>
<div className='row' style={{ textAlign: 'center', marginTop: 20 }}>
<div style={{ flex: 1 }}>
<div></div>
<div style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>{Number(weekTransaction[0].withdrawal_amount).toLocaleString()}</div>
</div>
<div style={{ flex: 1 }}>
<div></div>
<div style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>{weekTransaction[0].withdrawal_num}</div>
</div>
</div>
</div>
<div className='solid' style={{ backgroundColor: theme ? '#e8e8e8' : '#555', margin: '0 20px' }}></div>
<div style={{ flex: 4 }}>
<div style={{ fontWeight: 'bold', fontSize: 14 }}>{weekTransaction[1].symbol} </div>
<div className='row' style={{ textAlign: 'center', marginTop: 30 }}>
<div style={{ flex: 1 }}>
<div></div>
<div style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>{Number(weekTransaction[1].deposit_amount).toLocaleString()}</div>
</div>
<div style={{ flex: 1 }}>
<div></div>
<div style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>{weekTransaction[1].deposit_num}</div>
</div>
</div>
<div className='row' style={{ textAlign: 'center', marginTop: 20 }}>
<div style={{ flex: 1 }}>
<div></div>
<div style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>{Number(weekTransaction[1].withdrawal_amount).toLocaleString()}</div>
</div>
<div style={{ flex: 1 }}>
<div></div>
<div style={{ fontSize: 20, color: '#3BB900', fontWeight: 'bold' }}>{weekTransaction[1].withdrawal_num}</div>
</div>
</div>
</div>
</div>
</div>
)}
<div style={{ display: 'block', height: 30 }}></div>
</div >
)
}
export default Home

BIN
src/package/SimpleTrade/pages/home/vector.png

After

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

177
src/package/SimpleTrade/route/routes.ts

@ -0,0 +1,177 @@
import Home from '@/package/SimpleTrade/pages/home'
import ErrorPage from '@/pages/public/errorPage'
import AdminUserList from '@/package/SimpleTrade/pages/adminuser/list'
import AdminUserEdit from '@/package/SimpleTrade/pages/adminuser/edit'
import AdminPermissionList from '@/package/SimpleTrade/pages/adminpermission/list'
import InviteChart from '@/package/SimpleTrade/pages/admininvite/chart'
import UploadAPK from '@/package/SimpleTrade/pages/adminupload/uploadapk'
import UserList from '@/package/SimpleTrade/pages/client/user'
import WithdrawRecord from '@/package/SimpleTrade/pages/client/withdraw'
import RechargeRecord from '@/package/SimpleTrade/pages/client/recharge'
import MT4History from '@/package/SimpleTrade/pages/client/mt4history'
import MT4Order from '@/package/SimpleTrade/pages/client/mt4order'
import MT4SettlementOrder from '@/package/SimpleTrade/pages/client/mt4settlementorder'
import { HomeOutlined, UserOutlined, AuditOutlined } from '@ant-design/icons'
/**
* path
* component
* exact true的时候则精确匹配
*/
const menus = [
{
path: '/',
name: '首页',
exact: true,
key: 'home',
icon: HomeOutlined,
component: Home,
routes: []
},
{
path: '/adminuser',
name: '用户管理',
key: 'adminuser',
type: 'subMenu',
icon: UserOutlined,
iconfont: 'icon-xiaoshouzongjian',
routes: [
{
path: '/adminuser/list',
name: '用户列表',
exact: true,
key: 'adminuser:list:view',
component: AdminUserList
},
{
path: '/adminuser/list/add',
name: '新增用户',
exact: true,
key: 'adminuser:list:add',
component: AdminUserEdit,
remove: true
},
{
path: '/adminuser/list/edit',
name: '编辑用户',
exact: true,
key: 'adminuser:list:edit',
component: AdminUserEdit,
remove: true
}
]
},
{
path: '/adminpermission',
name: '权限管理',
key: 'adminpermission',
type: 'subMenu',
icon: AuditOutlined,
routes: [
{
path: '/adminpermission/list',
name: '权限列表',
exact: true,
key: 'adminpermission:list:view',
component: AdminPermissionList
}
]
},
{
path: '/admininvite',
name: '邀请',
key: 'admininvite',
type: 'subMenu',
icon: AuditOutlined,
routes: [
{
path: '/admininvite/chart',
name: '关联图',
exact: true,
key: 'admininvite:chart:view',
component: InviteChart
}
]
},
{
path: '/adminupload',
name: '上传管理',
key: 'adminupload',
type: 'subMenu',
icon: AuditOutlined,
routes: [
{
path: '/adminupload/uploadapk',
name: '上传APK',
exact: true,
key: 'adminupload:uploadapk:view',
component: UploadAPK
}
]
},
{
path: '/client',
name: '客户端',
key: 'client',
type: 'subMenu',
icon: AuditOutlined,
routes: [
{
path: '/client/userlist',
name: '用户列表',
exact: true,
key: 'client:userlist:view',
component: UserList
},
{
path: '/client/recharge',
name: '充值记录',
exact: true,
key: 'client:recharge:view',
component: RechargeRecord
},
{
path: '/client/withdraw',
name: '提现记录',
exact: true,
key: 'client:withdraw:view',
component: WithdrawRecord
},
{
path: '/client/mt4history',
name: 'MT4历史记录',
exact: true,
key: 'client:mt4history:view',
component: MT4History
},
{
path: '/client/mt4order',
name: 'MT4订单列表',
exact: true,
key: 'client:mt4order:view',
component: MT4Order
},
{
path: '/client/mt4settlementorder',
name: 'MT4结算订单列表',
exact: true,
key: 'client:mt4settlementorder:view',
component: MT4SettlementOrder
}
]
},
{
path: '/403',
name: '暂无权限',
exact: true,
key: '/403',
component: ErrorPage,
remove: true
}
]
export default menus

38
src/package/SimpleTrade/types/enum.ts

@ -0,0 +1,38 @@
// 提币状态
export enum Withdraw_Status {
WithdrawStatusFail = -1, //链上错误
WithdrawStatusInit = 0, //初始状态
WithdrawStatusCheck = 1, //审核
WithdrawStatusHex = 2, //生成签名数据
WithdrawStatusSend = 3, //已发送
WithdrawStatusConfirm = 4, //确认中
WithdrawStatusEnd = 5, //完成
}
export enum Withdraw_Check {
WithdrawCheckNIL =0, // 无需审核0
WithdrawCheckWait =1, // 审核中1
WithdrawCheckReject =2, // 审核 驳回2
WithdrawCheckAgree =3, // 审核同意完成3
}
// 提现状态
export enum Recharge_Status {
TxStatusErr = -1 ,//业务错误
TxStatusFail = -2, //链上错误
TxStatusInit = 0, //初始记录
TxStatusConfirm = 1, //确认数到账 100块后 转入到账户中
TxStatusNotify = 2, //允许交易
}
// 用户状态
export enum User_Status{
EmailUserStatusRoot = 0, //根邀请人
EmailUserStatusInit = 1, //H5注册
EmailUserStatusUse = 2, //提交激活信息
EmailUserStatusAMT4 = 3, //创建A仓账号
EmailUserStatusBMT4 = 4, //创建B仓账号
EmailUserStatusCopy = 5, //A->B账号关联反向下单
EmailUserStatusSendEmail = 6, //仓位账号初始化完成进行邮件发送
EmailUserStatusActivated = 7, //完成激活
}

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)

100
src/pages/login/index.tsx

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

36
src/pages/login/login.less

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

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

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'

9
src/route/routes.ts

@ -0,0 +1,9 @@
import SimpleTradeRoutes from '@/package/SimpleTrade/route/routes'
const routes = {
simpleTradeKey: [...SimpleTradeRoutes],
}
const currentItem = routes.simpleTradeKey
export default currentItem

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

23
src/store/index.ts

@ -0,0 +1,23 @@
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 = persistStore(store)
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'

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

Loading…
Cancel
Save