-
11.editorconfig
-
12.eslintignore
-
58.eslintrc copy.js
-
208.gitattributes
-
16.gitignore
-
15.prettierrc.js
-
94CODE_OF_CONDUCT.md
-
21LICENSE
-
100README.md
-
3config-overrides.js
-
82config/webpack.config.js
-
1env
-
95package.json
-
8paths.json
-
BINpublic/favicon.ico
-
57public/index.html
-
15public/manifest.json
-
9914public/static/color.less
-
17public/static/less.min.js
-
3src-tauri/.gitignore
-
3496src-tauri/Cargo.lock
-
26src-tauri/Cargo.toml
-
3src-tauri/build.rs
-
BINsrc-tauri/icons/128x128.png
-
BINsrc-tauri/icons/128x128@2x.png
-
BINsrc-tauri/icons/32x32.png
-
BINsrc-tauri/icons/Square107x107Logo.png
-
BINsrc-tauri/icons/Square142x142Logo.png
-
BINsrc-tauri/icons/Square150x150Logo.png
-
BINsrc-tauri/icons/Square284x284Logo.png
-
BINsrc-tauri/icons/Square30x30Logo.png
-
BINsrc-tauri/icons/Square310x310Logo.png
-
BINsrc-tauri/icons/Square44x44Logo.png
-
BINsrc-tauri/icons/Square71x71Logo.png
-
BINsrc-tauri/icons/Square89x89Logo.png
-
BINsrc-tauri/icons/StoreLogo.png
-
BINsrc-tauri/icons/icon.icns
-
BINsrc-tauri/icons/icon.ico
-
BINsrc-tauri/icons/icon.png
-
8src-tauri/src/main.rs
-
63src-tauri/tauri.conf.json
-
17src/App.tsx
-
13src/api/index.ts
-
157src/assets/css/public.less
-
69src/assets/img/login-bg.svg
-
BINsrc/assets/img/logo.png
-
1src/assets/img/logo.svg
-
456src/assets/js/publicFunc.ts
-
42src/components/BreadCrumb/index.tsx
-
29src/components/Header/Header.module.less
-
97src/components/Header/index.tsx
-
8src/components/Menu/Menu.module.less
-
145src/components/Menu/index.tsx
-
7src/components/MyIconfont/index.tsx
-
55src/components/MySelect/index.tsx
-
287src/components/MyTable/index.tsx
-
77src/components/SearchForm/index.tsx
-
8src/components/TabPanes/TabPanes.module.less
-
319src/components/TabPanes/index.tsx
-
10src/config/index.ts
-
26src/index.tsx
-
26src/package/SimpleTrade/api/admin-permission.ts
-
33src/package/SimpleTrade/api/admin-user.ts
-
105src/package/SimpleTrade/api/client.ts
-
40src/package/SimpleTrade/api/index.ts
-
68src/package/SimpleTrade/config/layout.ts
-
117src/package/SimpleTrade/config/menu.ts
-
53src/package/SimpleTrade/pages/admininvite/chart/data.ts
-
131src/package/SimpleTrade/pages/admininvite/chart/index.tsx
-
268src/package/SimpleTrade/pages/adminpermission/list/index.tsx
-
43src/package/SimpleTrade/pages/adminupload/uploadapk/index.tsx
-
3src/package/SimpleTrade/pages/adminuser/adminuser.less
-
211src/package/SimpleTrade/pages/adminuser/edit/index.tsx
-
250src/package/SimpleTrade/pages/adminuser/list/index.tsx
-
61src/package/SimpleTrade/pages/client/mt4history/index.tsx
-
110src/package/SimpleTrade/pages/client/mt4order/index.tsx
-
47src/package/SimpleTrade/pages/client/mt4settlementorder/index.tsx
-
90src/package/SimpleTrade/pages/client/recharge/index.tsx
-
236src/package/SimpleTrade/pages/client/user/index.tsx
-
128src/package/SimpleTrade/pages/client/withdraw/index.tsx
-
38src/package/SimpleTrade/pages/home/index.css
-
47src/package/SimpleTrade/pages/home/index.less
-
352src/package/SimpleTrade/pages/home/index.tsx
-
BINsrc/package/SimpleTrade/pages/home/vector.png
-
177src/package/SimpleTrade/route/routes.ts
-
38src/package/SimpleTrade/types/enum.ts
-
34src/pages/container/Home.module.less
-
136src/pages/container/index.tsx
-
100src/pages/login/index.tsx
-
36src/pages/login/login.less
-
13src/pages/public/errorPage/index.tsx
-
12src/react-app-env.d.ts
-
9src/route/routes.ts
-
27src/store/actionTypes/index.ts
-
7src/store/actions/index.ts
-
23src/store/index.ts
-
21src/store/reducers/index.ts
-
25src/store/state/index.ts
-
9src/test/App.test.jsx
-
7src/types/store.d.ts
@ -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 |
@ -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 |
@ -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
|
|||
} |
|||
} |
|||
] |
|||
} |
@ -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 |
@ -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* |
@ -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' |
|||
} |
@ -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._ |
@ -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. |
@ -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 |
|||
|
|||
``` |
@ -0,0 +1,3 @@ |
|||
const webpackConfig = require('./config/webpack.config') |
|||
|
|||
module.exports = webpackConfig |
@ -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
|
|||
}) |
|||
) |
@ -0,0 +1 @@ |
|||
REACT_APP_BASE_URL=http://14.29.101.215:30303/api/v1 |
@ -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" |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,8 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"baseUrl": "src", |
|||
"paths": { |
|||
"@/*": ["*"] |
|||
} |
|||
} |
|||
} |
@ -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> |
@ -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
@ -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
@ -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" ] |
@ -0,0 +1,3 @@ |
|||
fn main() {
|
|||
tauri_build::build()
|
|||
}
|
After Width: 128 | Height: 128 | Size: 11 KiB |
After Width: 256 | Height: 256 | Size: 23 KiB |
After Width: 32 | Height: 32 | Size: 2.2 KiB |
After Width: 107 | Height: 107 | Size: 9.0 KiB |
After Width: 142 | Height: 142 | Size: 12 KiB |
After Width: 150 | Height: 150 | Size: 13 KiB |
After Width: 284 | Height: 284 | Size: 25 KiB |
After Width: 30 | Height: 30 | Size: 2.0 KiB |
After Width: 310 | Height: 310 | Size: 28 KiB |
After Width: 44 | Height: 44 | Size: 3.3 KiB |
After Width: 71 | Height: 71 | Size: 5.9 KiB |
After Width: 89 | Height: 89 | Size: 7.4 KiB |
After Width: 50 | Height: 50 | Size: 3.9 KiB |
After Width: 512 | Height: 512 | Size: 49 KiB |
@ -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");
|
|||
}
|
@ -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 |
|||
}] |
|||
} |
|||
} |
@ -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 |
@ -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) |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
@ -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> |
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
@ -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替代setTimeout、setInterval,解决内存溢出 |
|||
* @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 |
|||
} |
|||
|
|||
|
@ -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) |
@ -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; |
|||
} |
@ -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) |
@ -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; |
|||
} |
@ -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) |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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; |
|||
} |
@ -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) |
@ -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 |
|||
} |
@ -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') |
|||
) |
@ -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) |
|||
} |
|||
} |
@ -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) |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -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) |
|||
} |
|||
} |
@ -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 |
|||
} |
@ -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 |
@ -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 }; |
|||
}; |
@ -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; |
@ -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 |
@ -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; |
@ -0,0 +1,3 @@ |
|||
.mr-5 { |
|||
margin-right: 5px; |
|||
} |
@ -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 |
@ -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 |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
|||
} |
@ -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; |
|||
} |
@ -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 |
After Width: 90 | Height: 90 | Size: 589 B |
@ -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 |
@ -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, //完成激活
|
|||
} |
@ -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; |
|||
} |
|||
} |
@ -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) |
@ -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) |
@ -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; |
|||
} |
|||
} |
@ -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 |
@ -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' |
@ -0,0 +1,9 @@ |
|||
import SimpleTradeRoutes from '@/package/SimpleTrade/route/routes' |
|||
|
|||
const routes = { |
|||
simpleTradeKey: [...SimpleTradeRoutes], |
|||
} |
|||
|
|||
const currentItem = routes.simpleTradeKey |
|||
|
|||
export default currentItem |
@ -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' |
|||
} |
|||
} |
@ -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 } |
|||
} |
@ -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 } |
@ -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 |
@ -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 |
@ -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) |
|||
}) |
@ -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' |