-
BIN.DS_Store
-
11.editorconfig
-
2.env
-
12.eslintignore
-
58.eslintrc copy.js
-
208.gitattributes
-
15.gitignore
-
15.prettierrc.js
-
94CODE_OF_CONDUCT.md
-
21LICENSE
-
100README.md
-
3config-overrides.js
-
82config/webpack.config.js
-
1env
-
48112package-lock.json
-
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
-
18src/App.tsx
-
58src/api/index.ts
-
11src/app.css
-
157src/assets/css/public.less
-
69src/assets/img/login-bg.svg
-
BINsrc/assets/img/logo.png
-
1src/assets/img/logo.svg
-
459src/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
-
305src/components/MyTable/index.tsx
-
77src/components/SearchForm/index.tsx
-
8src/components/TabPanes/TabPanes.module.less
-
319src/components/TabPanes/index.tsx
-
26src/index.tsx
-
34src/pages/container/Home.module.less
-
136src/pages/container/index.tsx
-
38src/pages/home/index.css
-
47src/pages/home/index.less
-
9src/pages/home/index.tsx
-
BINsrc/pages/home/vector.png
-
100src/pages/login/index.tsx
-
36src/pages/login/login.less
-
226src/pages/notify/news.tsx
-
148src/pages/notify/system.tsx
-
13src/pages/public/errorPage/index.tsx
-
344src/pages/swiper/index.tsx
-
12src/react-app-env.d.ts
-
48src/route/routes.ts
-
27src/store/actionTypes/index.ts
-
7src/store/actions/index.ts
-
25src/store/index.ts
-
21src/store/reducers/index.ts
-
25src/store/state/index.ts
-
9src/test/App.test.jsx
-
7src/types/store.d.ts
-
111src/utils/axios.ts
-
49src/utils/index.ts
-
49src/utils/tableHook.ts
-
26src/utils/userIdHook.ts
-
52tsconfig.json
-
23tslint.json
-
18typings/global.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,2 @@ |
|||
REACT_APP_BASE_URL=http://162.254.37.253:8082/api/v1 |
|||
SKIP_PREFLIGHT_CHECK=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,15 @@ |
|||
/node_modules |
|||
|
|||
.idea |
|||
build |
|||
.env.* |
|||
.env.development |
|||
.env.production |
|||
.env.local |
|||
.env.development.local |
|||
.env.test.local |
|||
.env.production.local |
|||
|
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
@ -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 |
48112
package-lock.json
File diff suppressed because it is too large
View File
@ -0,0 +1,95 @@ |
|||
{ |
|||
"name": "react-antd-admin", |
|||
"version": "0.1.0", |
|||
"private": true, |
|||
"dependencies": { |
|||
"@ant-design/icons": "^4.0.6", |
|||
"@testing-library/jest-dom": "^4.2.4", |
|||
"@testing-library/react": "^9.3.2", |
|||
"@testing-library/user-event": "^7.1.2", |
|||
"@types/axios": "^0.14.0", |
|||
"@types/jest": "^24.0.0", |
|||
"@types/node": "^12.0.0", |
|||
"@types/react": "^16.9.0", |
|||
"@types/react-dom": "^16.9.0", |
|||
"@types/react-redux": "^7.1.7", |
|||
"@types/react-router-dom": "^5.1.4", |
|||
"@types/redux-promise": "^0.5.28", |
|||
"antd": "4.2.0", |
|||
"axios": "^0.21.1", |
|||
"babel-plugin-import": "^1.12.2", |
|||
"classnames": "^2.2.6", |
|||
"connected-react-router": "^6.5.2", |
|||
"cross-env": "^7.0.2", |
|||
"dagre": "^0.8.5", |
|||
"dayjs": "^1.11.10", |
|||
"less": "^3.11.1", |
|||
"less-loader": "^5.0.0", |
|||
"moment": "^2.24.0", |
|||
"react": "^16.13.1", |
|||
"react-canvas-nest": "^1.0.10", |
|||
"react-dom": "^16.13.1", |
|||
"react-flow-renderer": "^10.3.17", |
|||
"react-redux": "^7.1.3", |
|||
"react-router-breadcrumbs-hoc": "^3.2.4", |
|||
"react-router-dom": "^5.1.2", |
|||
"react-scripts": "3.2.0", |
|||
"redux": "^4.0.4", |
|||
"redux-persist": "^6.0.0", |
|||
"redux-promise": "^0.6.0", |
|||
"typescript": "^4.8.3" |
|||
}, |
|||
"scripts": { |
|||
"start": "cross-env PORT=3000 react-app-rewired start", |
|||
"test": "react-app-rewired test", |
|||
"build": "react-app-rewired build", |
|||
"deploy:dev": "npm run build && scp -r ./build/* vps:/home/ubuntu/pzy/filpool", |
|||
"deploy:prod": "npm run build && scp -r ./build/* dcfilefast_prod:/data/wwwroot/apk.hippoim.us/admin-web" |
|||
}, |
|||
"eslintConfig": { |
|||
"extends": "react-app" |
|||
}, |
|||
"browserslist": { |
|||
"production": [ |
|||
">0.2%", |
|||
"not dead", |
|||
"not op_mini all" |
|||
], |
|||
"development": [ |
|||
"last 1 chrome version", |
|||
"last 1 firefox version", |
|||
"last 1 safari version" |
|||
] |
|||
}, |
|||
"devDependencies": { |
|||
"compression-webpack-plugin": "^3.0.1", |
|||
"customize-cra": "^0.8.0", |
|||
"eslint": "^6.8.0", |
|||
"eslint-config-airbnb": "^18.0.1", |
|||
"eslint-config-prettier": "^6.10.0", |
|||
"eslint-import-resolver-alias": "^1.1.2", |
|||
"eslint-import-resolver-webpack": "^0.11.1", |
|||
"eslint-plugin-html": "^6.0.0", |
|||
"eslint-plugin-import": "^2.20.1", |
|||
"eslint-plugin-jsx-a11y": "^6.2.3", |
|||
"eslint-plugin-prettier": "^3.1.2", |
|||
"eslint-plugin-react": "^7.18.3", |
|||
"eslint-plugin-react-hooks": "^2.3.0", |
|||
"husky": "^3.0.9", |
|||
"lint-staged": "^9.4.2", |
|||
"prettier": "1.18.2", |
|||
"react-app-rewired": "^2.1.5", |
|||
"redux-devtools": "^3.5.0", |
|||
"uglifyjs-webpack-plugin": "^2.2.0", |
|||
"webpack-bundle-analyzer": "^3.6.0" |
|||
}, |
|||
"homepage": ".", |
|||
"lint-staged": { |
|||
"*.{js,jsx}": "eslint" |
|||
}, |
|||
"husky": { |
|||
"hooks": { |
|||
"pre-commit": "lint-staged" |
|||
} |
|||
} |
|||
} |
@ -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,18 @@ |
|||
import React, { FC } from 'react' |
|||
import { HashRouter as Router, Route } from 'react-router-dom' |
|||
import Container from '@/pages/container' |
|||
import Login from '@/pages/login' |
|||
import './app.css' |
|||
|
|||
const App: FC = () => ( |
|||
<Router> |
|||
<Route exact path="/login" component={Login} /> |
|||
<Route |
|||
path="/" |
|||
key="container" |
|||
render={(props: any) => <Container {...props} />} |
|||
/> |
|||
</Router> |
|||
) |
|||
|
|||
export default App |
@ -0,0 +1,58 @@ |
|||
import $axios from '@/utils/axios' |
|||
|
|||
export default { |
|||
// 获取数据
|
|||
getList(params?: object): Promise<CommonObjectType<string>> { |
|||
return $axios.post('/admin/dappList', params) |
|||
}, |
|||
|
|||
// 登录
|
|||
login(params: object): Promise<CommonObjectType<string>> { |
|||
return $axios.post('/admin/login', params) |
|||
}, |
|||
|
|||
upload_image(file: File): Promise<CommonObjectType<string>> { |
|||
const body = new FormData(); |
|||
console.log(file); |
|||
|
|||
body.append("img", file); |
|||
return $axios.post("/admin/uploadImg", body, { |
|||
|
|||
}); |
|||
}, |
|||
|
|||
add_dapp_list(params: object) { |
|||
return $axios.post('/admin/addDapp', params) |
|||
}, |
|||
|
|||
delete_dapp_list(params: object) { |
|||
return $axios.post('/admin/delDapp', params) |
|||
}, |
|||
update_dapp_list(params: object) { |
|||
return $axios.post('/admin/updDapp', params) |
|||
}, |
|||
get_system_message(params: object) { |
|||
return $axios.post('/admin/systemMessage', params) |
|||
}, |
|||
add_system_message(params: object) { |
|||
return $axios.post('/admin/addSystemMessage', params) |
|||
}, |
|||
updata_system_message(params: object) { |
|||
return $axios.post('/admin/updSystemMessage', params) |
|||
}, |
|||
delete_system_message(params: object) { |
|||
return $axios.post('/admin/delSystemMessage', params) |
|||
}, |
|||
get_news_message(params: object) { |
|||
return $axios.post('/admin/buzzNewsMessage', params) |
|||
}, |
|||
add_news_message(params: object) { |
|||
return $axios.post('/admin/addBuzzNewsMessage', params) |
|||
}, |
|||
updata_news_message(params: object) { |
|||
return $axios.post('/admin/updBuzzNewsMessage', params) |
|||
}, |
|||
delete_new_message(params: object) { |
|||
return $axios.post('/admin/delBuzzNewsMessage', params) |
|||
} |
|||
} |
@ -0,0 +1,11 @@ |
|||
.text-overflow{ |
|||
display: -webkit-box; |
|||
-webkit-box-orient: vertical; |
|||
-webkit-line-clamp: 3; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
|
|||
.tp{ |
|||
cursor: pointer; |
|||
} |
@ -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,459 @@ |
|||
/* eslint-disable no-param-reassign */ |
|||
import { Modal } from 'antd' |
|||
import routes from '@/route/routes' |
|||
import ErrorPage from '@/pages/public/errorPage' |
|||
import { store } from '@/store' |
|||
|
|||
// 通用confirm方法
|
|||
export const commonConfirm = (title: string, cb: () => void) => { |
|||
const { confirm } = Modal |
|||
confirm({ |
|||
okText: '确定', |
|||
cancelText: '取消', |
|||
title, |
|||
onOk() { |
|||
cb() |
|||
}, |
|||
onCancel() {} |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 隐藏手机号码 |
|||
* @param {string} phone 手机号 |
|||
*/ |
|||
export const hidePhone = (phone: string) => |
|||
phone && phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') |
|||
|
|||
/** |
|||
* 以递归的方式展平react router数组 |
|||
* @param {object[]} arr 路由数组 |
|||
* @param {string} child 需要递归的字段名 |
|||
*/ |
|||
export const flattenRoutes = (arr: CommonObjectType<unknown>[]) => |
|||
arr.reduce( |
|||
(prev: CommonObjectType<unknown>[], item: CommonObjectType<unknown>) => { |
|||
if (Array.isArray(item.routes)) { |
|||
prev.push(item) |
|||
} |
|||
return prev.concat( |
|||
Array.isArray(item.routes) ? flattenRoutes(item.routes) : item |
|||
) |
|||
}, |
|||
[] |
|||
) |
|||
|
|||
/** |
|||
* 根据路径获取路由的name和key |
|||
* @param {string} path 路由 |
|||
*/ |
|||
export const getKeyName = (path: string = '/403') => { |
|||
const truePath = path.split('?')[0]; |
|||
const curRoute = flattenRoutes(routes).filter( |
|||
(item: { path: string | string[] }) => item.path.includes(truePath) |
|||
) |
|||
|
|||
if (!curRoute[0]) |
|||
return { title: '暂无权限', tabKey: '403', component: ErrorPage } |
|||
const { name, key, component } = curRoute[0] |
|||
return { title: name, tabKey: key, component } |
|||
} |
|||
|
|||
/** |
|||
* 同步执行操作,Currying |
|||
* @param {*} action 要执行的操作 |
|||
* @param {function} cb 下一步操作回调 |
|||
*/ |
|||
export const asyncAction = (action: unknown) => { |
|||
const wait = new Promise((resolve) => { |
|||
resolve(action) |
|||
}) |
|||
return (cb: () => void) => { |
|||
wait.then(() => setTimeout(() => cb())) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 页签关闭操作回调 |
|||
* @param {object} history 路由history对象。不能new新实例,不然参数无法传递 |
|||
* @param {string} returnUrl 返回地址 |
|||
* @param {function} cb 回调操作,可选 |
|||
*/ |
|||
export const closeTabAction = ( |
|||
history: CommonObjectType, |
|||
returnUrl: string = '/', |
|||
cb?: () => void |
|||
) => { |
|||
const { curTab } = store.getState().storeData |
|||
const { href } = window.location |
|||
const pathname = href.split('#')[1] |
|||
// 删除tab
|
|||
const tabArr = JSON.parse(JSON.stringify(curTab)) |
|||
const delIndex = tabArr.findIndex((item: string) => item === pathname) |
|||
tabArr.splice(delIndex, 1) |
|||
|
|||
// 如果要返回的页面被关闭了,再加进去
|
|||
if (!tabArr.includes(returnUrl)) { |
|||
tabArr.push(returnUrl) |
|||
} |
|||
|
|||
// 储存新的tabs数组
|
|||
const setTab = store.dispatch({ |
|||
type: 'SET_CURTAB', |
|||
payload: tabArr |
|||
}) |
|||
// 刷新tab
|
|||
const reloadTab = store.dispatch({ |
|||
type: 'SET_RELOADPATH', |
|||
payload: returnUrl |
|||
}) |
|||
// 停止刷新tab
|
|||
const stopReload = setTimeout(() => { |
|||
store.dispatch({ |
|||
type: 'SET_RELOADPATH', |
|||
payload: 'null' |
|||
}) |
|||
}, 500) |
|||
|
|||
const action = () => setTab && reloadTab && stopReload |
|||
|
|||
// 刷新回调
|
|||
const callback = () => { |
|||
if (cb && typeof cb === 'function') { |
|||
return cb |
|||
} |
|||
return history.push({ |
|||
pathname: returnUrl |
|||
}) |
|||
} |
|||
|
|||
asyncAction(action)(callback) |
|||
} |
|||
|
|||
/** |
|||
* 获取地址栏 ?参数,返回键值对对象 |
|||
*/ |
|||
export const getQuery = (): CommonObjectType<string> => { |
|||
const { href } = window.location |
|||
const query = href.split('?') |
|||
if (!query[1]) return {} |
|||
|
|||
const queryArr = decodeURI(query[1]).split('&') |
|||
const queryObj = queryArr.reduce((prev, next) => { |
|||
const item = next.split('=') |
|||
return { ...prev, [item[0]]: item[1] } |
|||
}, {}) |
|||
return queryObj |
|||
} |
|||
|
|||
/** |
|||
* 深拷贝操作,简单类型的对象的可以直接用 JSON.parse(JSON.stringify())或 [...]/{...} |
|||
* @param {object} obj 需要拷贝的对象 |
|||
*/ |
|||
export const deepClone = (obj: CommonObjectType) => { |
|||
if ( |
|||
obj === null || |
|||
typeof obj !== 'object' || |
|||
obj instanceof Date || |
|||
obj instanceof Function |
|||
) { |
|||
return obj |
|||
} |
|||
const cloneObj = Array.isArray(obj) ? [] : {} |
|||
Object.keys(obj).map((key) => { |
|||
cloneObj[key] = deepClone(obj[key]) |
|||
return cloneObj |
|||
}) |
|||
return cloneObj |
|||
} |
|||
|
|||
/** |
|||
* 获取图片地址 |
|||
* @param {*} html 富文本字符串 |
|||
*/ |
|||
export const getImgsUrl = (html?: string) => { |
|||
// 匹配图片(g表示匹配所有结果i表示区分大小写)
|
|||
const imgReg = /<img.*?(?:>|\/>)/gi |
|||
// 匹配src属性
|
|||
const srcReg = /src=['"]?([^'"]*)['"]?/i |
|||
const arr = html.match(imgReg) |
|||
if (!arr) return null |
|||
// 获取图片地址
|
|||
const urlArr = arr.reduce((prev, next) => { |
|||
const src = next.match(srcReg) |
|||
return src[1] ? [...prev, src[1]] : prev |
|||
}, []) |
|||
return urlArr |
|||
} |
|||
|
|||
/** |
|||
* 获取视频地址 |
|||
* @param {*} html 富文本字符串 |
|||
*/ |
|||
export const getVideoUrl = (html?: string) => { |
|||
// 匹配图片(g表示匹配所有结果i表示区分大小写)
|
|||
const imgReg = /<(video|iframe).*?(?:>|\/>)/gi |
|||
// 匹配src属性
|
|||
const srcReg = /src=['"]?([^'"]*)['"]?/i |
|||
const arr = html.match(imgReg) |
|||
if (!arr) return null |
|||
// 获取图片地址
|
|||
const urlArr = arr.reduce((prev, next) => { |
|||
const src = next.match(srcReg) |
|||
return src[1] ? [...prev, src[1]] : prev |
|||
}, []) |
|||
return urlArr |
|||
} |
|||
|
|||
/** |
|||
* 获取本地存储中的权限 |
|||
*/ |
|||
export const getPermission = () => localStorage.getItem('permissions') || [] |
|||
|
|||
/** |
|||
* 根据权限判断是否有权限 |
|||
*/ |
|||
export const isAuthorized = (val: string): boolean => { |
|||
const permissions = getPermission(); |
|||
return permissions.includes(val) |
|||
} |
|||
|
|||
/** |
|||
* 用requestAnimationFrame替代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 |
|||
|
|||
console.log(permission); |
|||
|
|||
const permissionArray = permission.reduce( |
|||
(prev: CommonObjectType<string>[], next: CommonObjectType<string>) => [ |
|||
...prev, |
|||
next.code |
|||
], |
|||
[] |
|||
); |
|||
|
|||
localStorage.setItem('permissions', permissionArray) |
|||
|
|||
const result = { |
|||
userName, |
|||
permission, |
|||
token: token || oldToken |
|||
} |
|||
action('SET_USERINFO', result) |
|||
} |
|||
|
|||
/** |
|||
* 生成tree结构数据 |
|||
* @param data |
|||
* @param parentIdField |
|||
*/ |
|||
export const makeTree = ( |
|||
data: Array<any>, |
|||
parentIdField: string = 'parentId' |
|||
): Array<any> => { |
|||
const dataMap: Object = {} |
|||
|
|||
data.forEach((item) => { |
|||
dataMap[item.id] = item |
|||
}) |
|||
|
|||
const dataTree: Array<any> = [] |
|||
data.forEach((item) => { |
|||
const parent = dataMap[item[parentIdField]] |
|||
if (parent) { |
|||
if (parent.children) { |
|||
parent.children.push(item) |
|||
} else { |
|||
parent.children = [] |
|||
parent.children.push(item) |
|||
} |
|||
} else { |
|||
dataTree.push(item) |
|||
} |
|||
}) |
|||
|
|||
return dataTree |
|||
} |
|||
|
|||
/** |
|||
* 生成TreeSelect组件结构数据 |
|||
* @param dataTree |
|||
* @param treeValueFiled |
|||
* @param treeTitleFiled |
|||
*/ |
|||
export const makeTreeSelectData = ( |
|||
dataTree: Array<any>, |
|||
treeValueFiled: string = 'value', |
|||
treeTitleFiled: string = 'title' |
|||
): Array<any> => { |
|||
const treeSelect: Array<any> = [] |
|||
dataTree.forEach((item) => { |
|||
const temp = { |
|||
value: item[treeValueFiled], |
|||
title: item[treeTitleFiled], |
|||
children: item.children || undefined |
|||
} |
|||
if (temp.children) { |
|||
temp.children = makeTreeSelectData( |
|||
temp.children, |
|||
treeValueFiled, |
|||
treeTitleFiled |
|||
) |
|||
} |
|||
treeSelect.push(temp) |
|||
}) |
|||
return treeSelect |
|||
} |
|||
|
|||
/** |
|||
* 生成Tree组件结构数据 |
|||
* @param dataTree |
|||
* @param treeKeyFiled |
|||
* @param treeTitleFiled |
|||
*/ |
|||
export const makeTreeData = ( |
|||
dataTree: Array<any>, |
|||
treeKeyFiled: string = 'key', |
|||
treeTitleFiled: string = 'title' |
|||
): Array<any> => { |
|||
const treeSelect: Array<any> = [] |
|||
dataTree.forEach((item) => { |
|||
const temp = { |
|||
key: item[treeKeyFiled], |
|||
title: item[treeTitleFiled], |
|||
children: item.children || undefined |
|||
} |
|||
if (temp.children) { |
|||
temp.children = makeTreeData( |
|||
temp.children, |
|||
treeKeyFiled, |
|||
treeTitleFiled |
|||
) |
|||
} |
|||
treeSelect.push(temp) |
|||
}) |
|||
return treeSelect |
|||
} |
|||
|
|||
/** |
|||
* 根据节点获取所有父节点id数组 |
|||
* @param tree |
|||
* @param id |
|||
*/ |
|||
export const treeFindParentById = (tree: Array<any>, id: string | number): Array<number | string> => { |
|||
const path = [] |
|||
if (tree.length === 0) return [] |
|||
const forFn = function (tree, id) { |
|||
for (let i = 0; i < tree.length; i++) { |
|||
const data = tree[i] |
|||
|
|||
if (data.id === id) { |
|||
return path |
|||
} |
|||
|
|||
path.push(data.id) |
|||
|
|||
if (data.children) { |
|||
const findChildren = forFn(data.children, id); |
|||
if (findChildren && findChildren.length > 0) return findChildren |
|||
} |
|||
|
|||
path.pop() |
|||
} |
|||
} |
|||
forFn(tree, id) |
|||
return path |
|||
} |
|||
|
|||
/** |
|||
* 获取数据差集 |
|||
* @param arr1 |
|||
* @param arr2 |
|||
*/ |
|||
export const diffArray = (arr1: Array<number | string>, arr2: Array<number | string>): Array< number | string>=> { |
|||
const diff: Array<number | string> = [] |
|||
for (let idx = 0; idx < arr1.length; idx++) { |
|||
if (!arr2.includes(arr1[idx])) { |
|||
diff.push(arr1[idx]) |
|||
} |
|||
} |
|||
return diff |
|||
} |
|||
|
|||
|
@ -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,305 @@ |
|||
import React, { |
|||
useState, |
|||
forwardRef, |
|||
useImperativeHandle, |
|||
useRef, |
|||
ReactNode, |
|||
FC |
|||
} from 'react' |
|||
import { Table } from 'antd' |
|||
import useService from '@/utils/tableHook' |
|||
import SearchView from '@/components/SearchForm' |
|||
|
|||
/** |
|||
* 封装列表、分页、多选、搜索组件 |
|||
* @param {RefType} ref 表格的实例,用于调用内部方法 |
|||
* @param {object[]} columns 表格列的配置 |
|||
* @param {function} apiFun 表格数据的请求方法 |
|||
* @param {object[]} searchConfigList 搜索栏配置 |
|||
* @param {function} beforeSearch 搜索前的操作(如处理一些特殊数据) |
|||
* @param {function} onFieldsChange 处理搜索栏表单联动事件 |
|||
* @param {object} extraProps 额外的搜索参数(不在搜索配置内的) |
|||
* @param {function} onSelectRow 复选框操作回调 |
|||
* @param {string} rowKey 表格行的key |
|||
* @param {function} sortConfig 自定义表格排序字段 |
|||
* @param {function} expandedRowRender 额外的展开行 |
|||
* @param {function} onExpand 点击展开图标时触发 |
|||
* @param {string} rowClassName 表格行的样式名 |
|||
* @param {boolean} small 表格和分页的展示大小 |
|||
* @param {string[]} extraPagation 额外的分页大小 |
|||
*/ |
|||
|
|||
interface TableProps { |
|||
columns: object[]; |
|||
apiFun: (arg0?: unknown[]) => Promise<{}>; |
|||
ref?: RefType; |
|||
searchConfigList?: object[]; |
|||
extraProps?: object; |
|||
rowKey?: string; |
|||
rowClassName?: string; |
|||
small?: boolean; |
|||
showHeader?: boolean; |
|||
extraPagation?: string[]; |
|||
beforeSearch?: (arg0?: unknown) => void; |
|||
onSelectRow?: (arg0?: string[], arg1?: string[]) => void; |
|||
onFieldsChange?: (arg0?: unknown, arg1?: unknown) => void; |
|||
sortConfig?: (arg0?: object) => any; |
|||
expandedRowRender?: () => ReactNode; |
|||
onExpand?: () => void; |
|||
header?: JSX.Element |
|||
} |
|||
|
|||
const MyTable: FC<TableProps> = forwardRef( |
|||
(props: TableProps, ref: RefType) => { |
|||
/** |
|||
* @forwardRef |
|||
* 引用父组件的ref实例,成为子组件的一个参数 |
|||
* 可以引用父组件的ref绑定到子组件自身的节点上. |
|||
*/ |
|||
const searchForm: RefType = useRef(null) |
|||
const { |
|||
columns, |
|||
apiFun, |
|||
searchConfigList, |
|||
extraProps, |
|||
rowKey, |
|||
rowClassName, |
|||
small, |
|||
showHeader, |
|||
extraPagation, |
|||
beforeSearch, |
|||
onSelectRow, |
|||
onFieldsChange, |
|||
sortConfig, |
|||
expandedRowRender, |
|||
onExpand, |
|||
header |
|||
} = props |
|||
|
|||
// 搜索参数,如果有特殊需要处理的参数,就处理
|
|||
const searchObj = searchConfigList.reduce( |
|||
(prev: CommonObjectType, next: CommonObjectType) => { |
|||
return Object.assign(prev, { |
|||
[next.key]: next.fn ? next.fn(next.initialValue) : next.initialValue |
|||
}) |
|||
}, |
|||
{} |
|||
) |
|||
|
|||
// 初始参数
|
|||
const initParams = { |
|||
...searchObj, |
|||
...extraProps, |
|||
page: 1, |
|||
page_size: 20 |
|||
} |
|||
|
|||
// 多选框的选择值
|
|||
const [selectedKeys, setSelectedKeys] = useState([]) |
|||
// 列表所有的筛选参数(包括搜索、分页、排序等)
|
|||
const [tableParams, setTableParams] = useState(initParams) |
|||
// 列表搜索参数
|
|||
const [searchParams, setSearchParams] = useState(searchObj) |
|||
// 列表排序参数
|
|||
const [sortParams, setSortParams] = useState({}) |
|||
// 列表分页参数
|
|||
const [curPageNo, setCurPageNo] = useState(initParams.page) |
|||
const [curPageSize, setCurPageSize] = useState(initParams.page_size) |
|||
|
|||
const { loading = false, response = {} }: CommonObjectType = useService( |
|||
apiFun, |
|||
tableParams |
|||
); |
|||
|
|||
let tableData = []; |
|||
let total = 0; |
|||
const validData = response?.data ? response.data : []; |
|||
if (Array.isArray(validData) && validData.length >= 0) { |
|||
tableData = validData; |
|||
total = validData.length; |
|||
} |
|||
|
|||
if (validData.rows) { |
|||
tableData = validData.rows; |
|||
total = validData.total; |
|||
} |
|||
|
|||
// const validData = response?.data ? response.data : {}
|
|||
// const { rows: tableData = [], total } = validData
|
|||
|
|||
// 执行搜索操作
|
|||
const handleSearch = (val: CommonObjectType): void => { |
|||
if (val.login) { |
|||
val.login = Number(val.login) |
|||
} |
|||
|
|||
const obj = { |
|||
...val |
|||
} |
|||
if (Array.isArray(obj.chain_id)) { |
|||
obj.chain_id = Number(obj.chain_id[0]) |
|||
} |
|||
|
|||
setSearchParams(obj) |
|||
|
|||
setTableParams({ ...tableParams, ...obj, page: 1 }) |
|||
} |
|||
|
|||
// 重置列表部分状态
|
|||
const resetAction = (page?: number): void => { |
|||
setSelectedKeys([]) |
|||
const nextPage = page || curPageNo |
|||
const nextParmas = page === 1 ? {} : { ...searchParams, ...sortParams } |
|||
setCurPageNo(nextPage) |
|||
setTableParams({ |
|||
...initParams, |
|||
...nextParmas, |
|||
page: nextPage, |
|||
page_size: curPageSize |
|||
}) |
|||
} |
|||
|
|||
// 列表复选框选中变化
|
|||
const onSelectChange = ( |
|||
selectedRowKeys: any[], |
|||
selectedRows: any[] |
|||
): void => { |
|||
setSelectedKeys(selectedRowKeys) |
|||
onSelectRow(selectedRowKeys, selectedRows) |
|||
} |
|||
// 复选框配置
|
|||
const rowSelection = { |
|||
selectedRowKeys: selectedKeys, |
|||
onChange: onSelectChange |
|||
} |
|||
// 判断是否有复选框显示
|
|||
const showCheckbox = onSelectRow ? { rowSelection } : {} |
|||
|
|||
// 展开配置
|
|||
const expendConfig = { |
|||
expandedRowRender, |
|||
onExpand, |
|||
rowClassName |
|||
} |
|||
// 判断是否有展开行
|
|||
const showExpend = expandedRowRender ? expendConfig : {} |
|||
|
|||
// 表格和分页的大小
|
|||
const tableSize = small ? 'small' : 'middle' |
|||
const pagationSize = small ? 'small' : 'default' |
|||
|
|||
// 分页、筛选、排序变化时触发
|
|||
const onTableChange = ( |
|||
pagination: CommonObjectType, |
|||
filters: CommonObjectType, |
|||
sorter: object |
|||
): void => { |
|||
// 如果有sort排序并且sort参数改变时,优先排序
|
|||
const sortObj = sortConfig ? sortConfig(sorter) : {} |
|||
setSortParams(sortObj) |
|||
|
|||
const { current: page, pageSize } = pagination |
|||
|
|||
setCurPageNo(page) |
|||
setCurPageSize(pageSize) |
|||
setTableParams({ |
|||
...initParams, |
|||
...searchParams, |
|||
...sortObj, |
|||
page, |
|||
page_size: pageSize |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* @useImperativeHandle |
|||
* 第一个参数,接收一个通过forwardRef引用父组件的ref实例 |
|||
* 第二个参数一个回调函数,返回一个对象,对象里面存储需要暴露给父组件的属性或方法 |
|||
*/ |
|||
useImperativeHandle(ref, () => ({ |
|||
// 更新列表
|
|||
update(page?: number): void { |
|||
resetAction(page) |
|||
}, |
|||
// 更新列表,并重置搜索字段
|
|||
resetForm(page?: number): void { |
|||
if (searchForm.current) searchForm.current.resetFields() |
|||
setSearchParams({}) |
|||
resetAction(page) |
|||
}, |
|||
// 仅重置搜索字段
|
|||
resetField(field?: string[]): void { |
|||
return field |
|||
? searchForm.current.resetFields([...field]) |
|||
: searchForm.current.resetFields() |
|||
}, |
|||
// 获取当前列表数据
|
|||
getTableData(): CommonObjectType[] { |
|||
return tableData |
|||
} |
|||
})) |
|||
|
|||
return ( |
|||
<div> |
|||
{/* 搜索栏 */} |
|||
{searchConfigList.length > 0 && ( |
|||
<div style={{ display: 'flex', justifyContent: 'space-between' }}> |
|||
<SearchView |
|||
ref={searchForm} |
|||
config={searchConfigList} |
|||
beforeSearch={beforeSearch} |
|||
handleSearch={handleSearch} |
|||
onFieldsChange={onFieldsChange} |
|||
/> |
|||
{header} |
|||
</div> |
|||
)} |
|||
{ |
|||
header && searchConfigList.length <= 0 && <div>{header}</div> |
|||
} |
|||
{/* 列表 */} |
|||
<Table |
|||
{...showCheckbox} |
|||
{...showExpend} |
|||
rowKey={rowKey} |
|||
loading={loading} |
|||
dataSource={tableData} |
|||
columns={columns} |
|||
onChange={onTableChange} |
|||
size={tableSize} |
|||
showHeader={showHeader} |
|||
scroll={{ x: 1800 }} |
|||
pagination={{ |
|||
size: pagationSize, |
|||
total, |
|||
pageSize: tableParams.page_size, |
|||
current: tableParams.page, |
|||
showQuickJumper: true, |
|||
showSizeChanger: true, |
|||
pageSizeOptions: ['20', '50', '100', '200', ...extraPagation], |
|||
showTotal: (all) => `共 ${all} 条` |
|||
}} |
|||
/> |
|||
</div> |
|||
) |
|||
} |
|||
) |
|||
|
|||
MyTable.defaultProps = { |
|||
searchConfigList: [], |
|||
ref: null, |
|||
extraProps: {}, |
|||
rowKey: 'id', |
|||
rowClassName: '', |
|||
small: false, |
|||
showHeader: true, |
|||
extraPagation: [], |
|||
beforeSearch: () => { }, |
|||
onSelectRow: () => { }, |
|||
onFieldsChange: () => { }, |
|||
sortConfig: () => { }, |
|||
expandedRowRender: null, |
|||
onExpand: () => { } |
|||
} |
|||
|
|||
export default MyTable |
@ -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 '@/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,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,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,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,9 @@ |
|||
import React from "react" |
|||
|
|||
const Home = () => { |
|||
return ( |
|||
<></> |
|||
) |
|||
} |
|||
|
|||
export default Home |
After Width: 90 | Height: 90 | Size: 589 B |
@ -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,226 @@ |
|||
import api from "@/api" |
|||
import MyTable from "@/components/MyTable" |
|||
import { RootState } from "@/store" |
|||
import { getTime } from "@/utils" |
|||
import { getBaseUrl } from "@/utils/axios" |
|||
import { InboxOutlined } from "@ant-design/icons" |
|||
import { Button, Drawer, Form, Input, Popconfirm, Tooltip, notification } from "antd" |
|||
import TextArea from "antd/lib/input/TextArea" |
|||
import Dragger from "antd/lib/upload/Dragger" |
|||
import { title } from "process" |
|||
import React, { useRef, useState } from "react" |
|||
import { useSelector } from "react-redux" |
|||
|
|||
const BUZZUPNews = () => { |
|||
|
|||
const { userInfo } = useSelector((state: RootState) => state.storeData) |
|||
|
|||
const [visible, setVisible] = useState(false) |
|||
const [form] = Form.useForm() |
|||
const currentType = useRef('' as 'create' | 'update') |
|||
const currentItem = useRef({} as any) |
|||
const tableRef = useRef(null) |
|||
|
|||
const columns = [ |
|||
{ |
|||
title: '图片', |
|||
dataIndex: 'image', |
|||
width: 300, |
|||
render: (logo) => { |
|||
return ( |
|||
<Tooltip title={ |
|||
<img src={logo} alt="" style={{ width: 240, height: 240, objectFit: 'contain' }} /> |
|||
}> |
|||
<img src={logo} className="tp" alt="" style={{ width: 50, height: 50, objectFit: 'contain' }} /> |
|||
</Tooltip> |
|||
) |
|||
} |
|||
}, |
|||
{ |
|||
title: '标题', |
|||
width: 300, |
|||
dataIndex: "title", |
|||
render: (title) => { |
|||
return ( |
|||
<Tooltip overlay={false} title={ |
|||
<div dangerouslySetInnerHTML={{ __html: title.replace(/\\n/g, '<br/>') }}></div> |
|||
}> |
|||
<div className="text-overflow tp" dangerouslySetInnerHTML={{ __html: title.replace(/\\n/g, '<br/>') }}></div> |
|||
</Tooltip> |
|||
) |
|||
} |
|||
}, |
|||
{ |
|||
title: '链接', |
|||
dataIndex: 'url', |
|||
width: 300 |
|||
}, |
|||
{ |
|||
title: '创建时间', |
|||
dataIndex: 'time', |
|||
render: (time) => ( |
|||
<div>{getTime(time * 1000)}</div> |
|||
) |
|||
}, |
|||
{ |
|||
title: 'options', |
|||
fixed: 'right', |
|||
width: 100, |
|||
render: (item) => ( |
|||
<div> |
|||
<Button type="primary" style={{ marginTop: 10 }} onClick={() => { |
|||
currentItem.current = item |
|||
currentType.current = 'update' |
|||
form.setFieldsValue(item) |
|||
setVisible(true) |
|||
}}>修改</Button> |
|||
<Popconfirm title={`确认删除“${item.title}”吗?`} onConfirm={() => deleteSystemMsg(item)}> |
|||
<Button type="primary" style={{ marginTop: 10, backgroundColor: 'red', borderColor: 'red' }}>删除</Button> |
|||
</Popconfirm> |
|||
</div> |
|||
) |
|||
} |
|||
] |
|||
|
|||
const deleteSystemMsg = async (item) => { |
|||
const res: any = await api.delete_new_message({ |
|||
id: item.id |
|||
}) |
|||
if (res.code === 0) { |
|||
tableRef.current.update() |
|||
notification.success({ |
|||
message: '删除成功' |
|||
}) |
|||
} |
|||
} |
|||
|
|||
const onClose = () => setVisible(false) |
|||
|
|||
const onFinish = async (values) => { |
|||
const parmas = { |
|||
...values |
|||
} |
|||
console.log(parmas); |
|||
|
|||
if (currentType.current === 'update') { |
|||
parmas.id = currentItem.current.id |
|||
} |
|||
let res: any; |
|||
if (currentType.current === 'create') { |
|||
res = await api.add_news_message(parmas) |
|||
} else { |
|||
res = await api.updata_news_message(parmas) |
|||
} |
|||
if (res.code === 0) { |
|||
setVisible(false) |
|||
tableRef.current.update() |
|||
form.resetFields() |
|||
notification.success({ |
|||
message: currentType.current === 'create' ? '添加成功' : '修改成功' |
|||
}) |
|||
} |
|||
} |
|||
|
|||
const uploadProps = { |
|||
name: 'img', |
|||
action: `${getBaseUrl()}/admin/uploadImg`, |
|||
headers: { |
|||
Token: userInfo.token, |
|||
}, |
|||
} |
|||
|
|||
const onChange = (info: any) => { |
|||
|
|||
if (info.file.status === 'removed') { |
|||
form.setFieldsValue({ |
|||
image: '' |
|||
}) |
|||
} |
|||
|
|||
if (info.file.status === 'done') { |
|||
if (info.file.response && info.file.response.code === 0) { |
|||
form.setFieldsValue({ |
|||
image: info.file.response.data.url |
|||
}) |
|||
notification.success({ |
|||
message: `图片上传成功` |
|||
}) |
|||
} |
|||
|
|||
} else if (info.file.status === 'error') { |
|||
notification.error({ |
|||
message: `图片上传失败` |
|||
}); |
|||
} |
|||
} |
|||
|
|||
return ( |
|||
<div> |
|||
<MyTable |
|||
apiFun={api.get_news_message} |
|||
ref={tableRef} |
|||
columns={columns} |
|||
header={ |
|||
<div style={{ width: '100%', display: 'flex', justifyContent: 'flex-end', marginBottom: 20 }}> |
|||
<Button type="primary" onClick={() => { |
|||
currentType.current = 'create' |
|||
form.resetFields() |
|||
setVisible(true) |
|||
}}>添加BUZZUPNews</Button> |
|||
</div> |
|||
} |
|||
/> |
|||
<Drawer title="添加系统通知" onClose={onClose} visible={visible} width="40%"> |
|||
<Form form={form} onFinish={onFinish}> |
|||
<Form.Item |
|||
name="url" |
|||
label="链接" |
|||
rules={[{ required: true, message: '请输入链接' }]} |
|||
> |
|||
<Input placeholder="请输入链接" /> |
|||
</Form.Item> |
|||
<Form.Item |
|||
name="title" |
|||
label="标题" |
|||
rules={[{ required: true, message: '请输入标题' }]} |
|||
> |
|||
<TextArea placeholder="请输入标题" style={{ minHeight: 200 }} /> |
|||
</Form.Item> |
|||
<Form.Item label="模版" shouldUpdate={(prevValues, currentValues) => prevValues.title !== currentValues.title}> |
|||
{({ getFieldValue }) => ( |
|||
<div dangerouslySetInnerHTML={{ __html: (getFieldValue('title') || '').replace(/\\n/g, '<br/>') }}></div> |
|||
)} |
|||
</Form.Item> |
|||
|
|||
<Form.Item |
|||
label="图片" |
|||
name="image" |
|||
rules={[{ required: true, message: 'Please Select Logo Image!' }]} |
|||
> |
|||
<Dragger |
|||
{...uploadProps} |
|||
onChange={onChange} |
|||
> |
|||
<p className="ant-upload-drag-icon" style={{ display: 'flex', justifyContent: 'center', paddingTop: 10 }}> |
|||
<InboxOutlined /> |
|||
</p> |
|||
<p className="ant-upload-text" style={{ textAlign: 'center' }}>点击或拖拽文件进行上传</p> |
|||
<p className="ant-upload-hint" style={{ textAlign: 'center', paddingBottom: 10 }}> |
|||
Support for a single or bulk upload. Strictly prohibited from uploading company data or other |
|||
banned files. |
|||
</p> |
|||
</Dragger> |
|||
</Form.Item> |
|||
|
|||
<Form.Item label=""> |
|||
<div style={{ display: 'flex', justifyContent: 'center' }}> |
|||
<Button type="primary" htmlType="submit">提交</Button> |
|||
</div> |
|||
</Form.Item> |
|||
</Form> |
|||
</Drawer> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default BUZZUPNews |
@ -0,0 +1,148 @@ |
|||
import api from "@/api" |
|||
import MyTable from "@/components/MyTable" |
|||
import { getTime } from "@/utils" |
|||
import { Button, Drawer, Form, Input, Popconfirm, Tooltip, notification } from "antd" |
|||
import TextArea from "antd/lib/input/TextArea" |
|||
import React, { useRef, useState } from "react" |
|||
|
|||
const System = () => { |
|||
|
|||
const [visible, setVisible] = useState(false) |
|||
const [form] = Form.useForm() |
|||
const currentType = useRef('' as 'create' | 'update') |
|||
const currentItem = useRef({} as any) |
|||
const tableRef = useRef(null) |
|||
|
|||
const columns = [ |
|||
{ |
|||
title: '标题', |
|||
dataIndex: 'title', |
|||
width: 300 |
|||
}, |
|||
{ |
|||
title: '内容', |
|||
width: 350, |
|||
dataIndex: "content", |
|||
render: (content) => { |
|||
return ( |
|||
<Tooltip overlay={false} title={ |
|||
<div dangerouslySetInnerHTML={{ __html: content.replace(/\\n/g, '<br/>') }}></div> |
|||
}> |
|||
<div className="text-overflow tp" dangerouslySetInnerHTML={{ __html: content.replace(/\\n/g, '<br/>') }}></div> |
|||
</Tooltip> |
|||
) |
|||
} |
|||
}, |
|||
{ |
|||
title: '创建时间', |
|||
dataIndex: 'time', |
|||
render: (time) => ( |
|||
<div>{getTime(time * 1000)}</div> |
|||
) |
|||
}, |
|||
{ |
|||
title: 'options', |
|||
fixed: 'right', |
|||
width: 100, |
|||
render: (item) => ( |
|||
<div> |
|||
<Button type="primary" style={{ marginTop: 10 }} onClick={() => { |
|||
currentItem.current = item |
|||
currentType.current = 'update' |
|||
form.setFieldsValue(item) |
|||
setVisible(true) |
|||
}}>修改</Button> |
|||
<Popconfirm title={`确认删除“${item.title}”吗?`} onConfirm={() => deleteSystemMsg(item)}> |
|||
<Button type="primary" style={{ marginTop: 10, backgroundColor: 'red', borderColor: 'red' }}>删除</Button> |
|||
</Popconfirm> |
|||
</div> |
|||
) |
|||
} |
|||
] |
|||
|
|||
const deleteSystemMsg = async (item) => { |
|||
const res: any = await api.delete_system_message({ |
|||
id: item.id |
|||
}) |
|||
if (res.code === 0) { |
|||
tableRef.current.update() |
|||
notification.success({ |
|||
message: '删除成功' |
|||
}) |
|||
} |
|||
} |
|||
|
|||
const onClose = () => setVisible(false) |
|||
|
|||
const onFinish = async (values) => { |
|||
const parmas = { |
|||
...values, |
|||
} |
|||
if (currentType.current === 'update') { |
|||
parmas.id = currentItem.current.id |
|||
} |
|||
let res: any; |
|||
if (currentType.current === 'create') { |
|||
res = await api.add_system_message(parmas) |
|||
} else { |
|||
res = await api.updata_system_message(parmas) |
|||
} |
|||
if (res.code === 0) { |
|||
setVisible(false) |
|||
tableRef.current.update() |
|||
form.resetFields() |
|||
notification.success({ |
|||
message: currentType.current === 'create' ? '添加成功' : '修改成功' |
|||
}) |
|||
} |
|||
} |
|||
|
|||
return ( |
|||
<div> |
|||
<MyTable |
|||
apiFun={api.get_system_message} |
|||
ref={tableRef} |
|||
columns={columns} |
|||
header={ |
|||
<div style={{ width: '100%', display: 'flex', justifyContent: 'flex-end', marginBottom: 20 }}> |
|||
<Button type="primary" onClick={() => { |
|||
currentType.current = 'create' |
|||
form.resetFields() |
|||
setVisible(true) |
|||
}}>添加系统公告</Button> |
|||
</div> |
|||
} |
|||
/> |
|||
<Drawer title="添加系统通知" onClose={onClose} visible={visible} width="40%"> |
|||
<Form form={form} onFinish={onFinish}> |
|||
<Form.Item |
|||
name="title" |
|||
label="标题" |
|||
rules={[{ required: true, message: '请输入标题' }]} |
|||
> |
|||
<Input placeholder="请输入标题" /> |
|||
</Form.Item> |
|||
<Form.Item |
|||
name="content" |
|||
label="内容" |
|||
rules={[{ required: true, message: '请输入内容' }]} |
|||
> |
|||
<TextArea placeholder="请输入内容" style={{ minHeight: 200 }} /> |
|||
</Form.Item> |
|||
<Form.Item label="模版" shouldUpdate={(prevValues, currentValues) => prevValues.content !== currentValues.content}> |
|||
{({ getFieldValue }) => ( |
|||
<div dangerouslySetInnerHTML={{ __html: (getFieldValue('content') || '').replace(/\\n/g, '<br/>') }}></div> |
|||
)} |
|||
</Form.Item> |
|||
<Form.Item label=""> |
|||
<div style={{ display: 'flex', justifyContent: 'center' }}> |
|||
<Button type="primary" htmlType="submit">提交</Button> |
|||
</div> |
|||
</Form.Item> |
|||
</Form> |
|||
</Drawer> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default System |
@ -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,344 @@ |
|||
import api from "@/api" |
|||
import MyTable from "@/components/MyTable" |
|||
import { RootState } from "@/store"; |
|||
import { getBaseUrl } from "@/utils/axios"; |
|||
import { InboxOutlined } from "@ant-design/icons"; |
|||
import { Button, Cascader, Drawer, Form, Input, Popconfirm, Select, Tooltip, notification } from "antd"; |
|||
import TextArea from "antd/lib/input/TextArea"; |
|||
import Dragger from "antd/lib/upload/Dragger"; |
|||
import React, { FC, useRef, useState } from "react" |
|||
import { DefaultRootState, useSelector } from "react-redux"; |
|||
|
|||
interface Option { |
|||
value: string | number; |
|||
label: string; |
|||
children?: Option[]; |
|||
} |
|||
|
|||
const Swiper: FC = () => { |
|||
|
|||
const [chainId, setChainId] = useState('56') |
|||
const { userInfo } = useSelector((state: RootState) => state.storeData) |
|||
const [visible, setVisible] = useState(false) |
|||
const [form] = Form.useForm() |
|||
const tableRef = useRef(null) |
|||
const currentItem = useRef({} as any) |
|||
const currentType = useRef('') |
|||
|
|||
const uploadProps = { |
|||
name: 'img', |
|||
action: `${getBaseUrl()}/admin/uploadImg`, |
|||
headers: { |
|||
Token: userInfo.token, |
|||
}, |
|||
} |
|||
|
|||
const onChange = (info: any) => { |
|||
|
|||
if (info.file.status === 'removed') { |
|||
form.setFieldsValue({ |
|||
logo_url: '' |
|||
}) |
|||
} |
|||
|
|||
if (info.file.status === 'done') { |
|||
if (info.file.response && info.file.response.code === 0) { |
|||
form.setFieldsValue({ |
|||
logo_url: info.file.response.data.url |
|||
}) |
|||
notification.success({ |
|||
message: `图片上传成功` |
|||
}) |
|||
} |
|||
|
|||
} else if (info.file.status === 'error') { |
|||
notification.error({ |
|||
message: `图片上传失败` |
|||
}); |
|||
} |
|||
} |
|||
|
|||
const columns = [ |
|||
{ |
|||
title: 'ID', |
|||
dataIndex: 'id' |
|||
}, |
|||
{ |
|||
title: '图片', |
|||
dataIndex: 'logo_url', |
|||
render: (logo) => { |
|||
|
|||
return ( |
|||
<img src={logo} alt="" style={{ width: 50, height: 50, objectFit: 'contain' }} /> |
|||
) |
|||
} |
|||
}, |
|||
{ |
|||
title: '标题', |
|||
dataIndex: 'title', |
|||
}, |
|||
{ |
|||
title: '排序', |
|||
dataIndex: 'sort', |
|||
}, |
|||
{ |
|||
title: '点击次数', |
|||
dataIndex: 'click' |
|||
}, |
|||
{ |
|||
title: '类型', |
|||
dataIndex: 'dapp_type' |
|||
}, |
|||
{ |
|||
title: '网页地址', |
|||
dataIndex: 'url', |
|||
}, |
|||
{ |
|||
title: '内容', |
|||
dataIndex: 'content', |
|||
render: (content) => { |
|||
return ( |
|||
<Tooltip overlay={false} title={content}> |
|||
<span style={{ width: 240 }} className="text-overflow tp">{content}</span> |
|||
</Tooltip> |
|||
) |
|||
} |
|||
}, |
|||
|
|||
{ |
|||
title: 'options', |
|||
fixed: 'right', |
|||
width: 100, |
|||
render: (item) => { |
|||
return ( |
|||
<div > |
|||
<Button type="primary" onClick={() => { |
|||
currentItem.current = item |
|||
currentType.current = 'update' |
|||
form.setFieldsValue(item) |
|||
setVisible(true) |
|||
}}>修改</Button> |
|||
<Popconfirm |
|||
title={`确认删除”${item.title}“吗?`} |
|||
okText="确认" |
|||
cancelText="取消" |
|||
onConfirm={() => { |
|||
deleteDapp(item) |
|||
}} |
|||
> |
|||
<Button type="primary" style={{ marginTop: 10, backgroundColor: 'red' }}>删除</Button> |
|||
</Popconfirm> |
|||
</div> |
|||
) |
|||
} |
|||
}, |
|||
|
|||
] |
|||
|
|||
const deleteDapp = async (item: any) => { |
|||
const res: any = await api.delete_dapp_list({ id: item.id }) |
|||
if (res.code === 0) { |
|||
tableRef.current.update() |
|||
notification.success({ |
|||
message: '删除成功' |
|||
}) |
|||
} |
|||
} |
|||
|
|||
const optionChianIds = [ |
|||
{ |
|||
value: '1', |
|||
label: 'Ethereum Mainnet' |
|||
}, |
|||
{ |
|||
value: '56', |
|||
label: 'BNB Smart Chain Mainnet' |
|||
}, |
|||
{ |
|||
value: '42161', |
|||
label: 'Arbitrum One' |
|||
}, |
|||
{ |
|||
value: '137', |
|||
label: 'Polygon Mainnet' |
|||
}, |
|||
{ |
|||
value: '10', |
|||
label: 'OP Mainnet' |
|||
}, |
|||
{ |
|||
value: '100', |
|||
label: 'xDai Chain' |
|||
}, |
|||
{ |
|||
value: '10', |
|||
label: 'OP Mainnet' |
|||
}, |
|||
{ |
|||
value: '314', |
|||
label: 'Filecoin Mainnet' |
|||
}, |
|||
{ |
|||
value: '97', |
|||
label: 'BSC Testnet' |
|||
}, |
|||
] |
|||
|
|||
const optionTypes = [ |
|||
{ |
|||
value: '1', |
|||
label: 'GAMEFI' |
|||
}, |
|||
{ |
|||
value: '2', |
|||
label: 'DEFI' |
|||
}, |
|||
{ |
|||
value: '3', |
|||
label: 'BANNER' |
|||
}, |
|||
{ |
|||
value: '4', |
|||
label: 'NEWS' |
|||
}, |
|||
] |
|||
|
|||
const searchConfigList = [ |
|||
{ |
|||
key: 'chain_id', |
|||
slot: <Cascader style={{ width: 300 }} options={optionChianIds} onChange={val => { |
|||
setChainId(val[0]) |
|||
}} />, |
|||
initialValue: [chainId], |
|||
fn: (ids) => { |
|||
return Number(ids[0]) |
|||
} |
|||
} |
|||
] |
|||
|
|||
const onClose = () => { |
|||
setVisible(false) |
|||
} |
|||
|
|||
const onFinish = async (obj: any) => { |
|||
const parmas = { |
|||
...obj, |
|||
chain_id: Number(obj.chain_id), |
|||
dapp_type: Number(obj.dapp_type), |
|||
sort: Number(obj.sort) |
|||
} |
|||
if (currentType.current === 'update') { |
|||
parmas.id = currentItem.current.id; |
|||
} |
|||
let res: any; |
|||
if (currentType.current === 'create') { |
|||
res = await api.add_dapp_list(parmas) |
|||
} else { |
|||
res = await api.update_dapp_list(parmas) |
|||
} |
|||
if (res.code === 0) { |
|||
tableRef.current.update() |
|||
form.resetFields() |
|||
setVisible(false) |
|||
notification.success({ |
|||
message: currentType.current === 'create' ? '添加DApp成功' : '修改DApp成功' |
|||
}) |
|||
} |
|||
} |
|||
|
|||
return ( |
|||
<div> |
|||
<MyTable |
|||
ref={tableRef} |
|||
columns={columns} |
|||
apiFun={api.getList} |
|||
searchConfigList={searchConfigList} |
|||
rowKey="id" |
|||
header={ |
|||
<Button type="primary" onClick={() => { |
|||
currentType.current = 'create' |
|||
form.resetFields() |
|||
setVisible(true) |
|||
}}>添加DAPP</Button> |
|||
} |
|||
/> |
|||
<Drawer title="添加DAPP" onClose={onClose} visible={visible} width="40%"> |
|||
<Form |
|||
onFinish={onFinish} |
|||
form={form} |
|||
> |
|||
<Form.Item |
|||
label="标题" |
|||
name="title" |
|||
rules={[{ required: true, message: 'Please input your title!' }]} |
|||
> |
|||
<Input /> |
|||
</Form.Item> |
|||
|
|||
<Form.Item |
|||
label="链接" |
|||
name="url" |
|||
rules={[{ required: true, message: 'Please input your url!' }]} |
|||
> |
|||
<Input /> |
|||
</Form.Item> |
|||
<Form.Item |
|||
label="内容" |
|||
name="content" |
|||
rules={[{ required: form.getFieldValue('dapp_type') != '3', message: 'Please input your content!' }]} |
|||
> |
|||
<TextArea /> |
|||
</Form.Item> |
|||
<Form.Item |
|||
label="类型" |
|||
name="dapp_type" |
|||
rules={[{ required: true, message: 'select your dapp_type!' }]} |
|||
> |
|||
<Select options={optionTypes} /> |
|||
</Form.Item> |
|||
<Form.Item |
|||
label="插入位置" |
|||
name="sort" |
|||
rules={[{ required: true, message: 'Please input your sort!' }]} |
|||
> |
|||
<Input /> |
|||
</Form.Item> |
|||
<Form.Item |
|||
label="链ID" |
|||
name="chain_id" |
|||
initialValue={chainId} |
|||
> |
|||
{/* <Cascader /> */} |
|||
<div>{chainId}</div> |
|||
</Form.Item> |
|||
|
|||
<Form.Item |
|||
label="LOGO" |
|||
name="logo_url" |
|||
rules={[{ required: true, message: 'Please Select Logo Image!' }]} |
|||
> |
|||
<Dragger |
|||
{...uploadProps} |
|||
onChange={onChange} |
|||
> |
|||
<p className="ant-upload-drag-icon" style={{ display: 'flex', justifyContent: 'center', paddingTop: 10 }}> |
|||
<InboxOutlined /> |
|||
</p> |
|||
<p className="ant-upload-text" style={{ textAlign: 'center' }}>点击或拖拽文件进行上传</p> |
|||
<p className="ant-upload-hint" style={{ textAlign: 'center', paddingBottom: 10 }}> |
|||
Support for a single or bulk upload. Strictly prohibited from uploading company data or other |
|||
banned files. |
|||
</p> |
|||
</Dragger> |
|||
</Form.Item> |
|||
<Form.Item> |
|||
<Button type="primary" htmlType="submit">Submit</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
</Drawer> |
|||
|
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default Swiper |
@ -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,48 @@ |
|||
import Home from "@/pages/home" |
|||
import Swiper from "@/pages/swiper" |
|||
import System from "@/pages/notify/system" |
|||
import { HomeOutlined, SolutionOutlined } from "@ant-design/icons" |
|||
import BUZZUPNews from "@/pages/notify/news" |
|||
|
|||
const routes = [ |
|||
{ |
|||
path: '/', |
|||
name: '首页', |
|||
exact: true, |
|||
key: 'home', |
|||
icon: HomeOutlined, |
|||
component: Home, |
|||
routes: [] |
|||
}, |
|||
{ |
|||
path: '/swiper', |
|||
name: 'DAPP', |
|||
exact: true, |
|||
key: 'swiper', |
|||
icon: SolutionOutlined, |
|||
component: Swiper, |
|||
routes: [] |
|||
}, |
|||
{ |
|||
path: '/system', |
|||
name: '系统通知', |
|||
exact: true, |
|||
key: 'system', |
|||
icon: SolutionOutlined, |
|||
component: System, |
|||
routes: [] |
|||
}, |
|||
{ |
|||
path: '/news', |
|||
name: 'BUZZUP News', |
|||
exact: true, |
|||
key: 'news', |
|||
icon: SolutionOutlined, |
|||
component: BUZZUPNews, |
|||
routes: [] |
|||
}, |
|||
|
|||
] |
|||
|
|||
|
|||
export default routes |
@ -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,25 @@ |
|||
import { createStore, applyMiddleware, combineReducers } from 'redux' |
|||
import { persistStore, persistReducer } from 'redux-persist' |
|||
import promiseMiddleware from 'redux-promise' |
|||
import storage from 'redux-persist/lib/storage' |
|||
import storeData from '@/store/reducers' |
|||
|
|||
const reducers = combineReducers({ |
|||
storeData |
|||
}) |
|||
|
|||
const persistConfig = { |
|||
key: 'root', |
|||
storage |
|||
} |
|||
|
|||
const myPersistReducer = persistReducer(persistConfig, reducers) |
|||
const store: any = createStore( |
|||
myPersistReducer, |
|||
applyMiddleware(promiseMiddleware) |
|||
) |
|||
const persistor: any = persistStore(store) |
|||
|
|||
export type RootState = ReturnType<typeof store.getState> |
|||
|
|||
export { store, persistor } |
@ -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' |
@ -0,0 +1,111 @@ |
|||
import Axios from 'axios' |
|||
import { message } from 'antd' |
|||
import { store } from '@/store' |
|||
import { HashRouter } from 'react-router-dom' |
|||
|
|||
interface AxiosConfig { |
|||
baseURL: string; |
|||
timeout: number; |
|||
headers: { |
|||
'Content-Type': string |
|||
}; |
|||
} |
|||
|
|||
export const getBaseUrl = () => { |
|||
// return `http://192.168.124.20:8008/api/v1`
|
|||
return `https://apk.hippoim.us/api/v1` |
|||
} |
|||
|
|||
const config: AxiosConfig = { |
|||
// baseURL: process.env.REACT_APP_BASE_URL,
|
|||
baseURL: getBaseUrl(), |
|||
timeout: 60 * 1000, |
|||
headers: { |
|||
'Content-Type': 'application/json' |
|||
} |
|||
} |
|||
|
|||
const axios = Axios.create(config) |
|||
|
|||
const router: CommonObjectType = new HashRouter({}) |
|||
|
|||
// token失效,清除用户信息并返回登录界面
|
|||
const clearAll = () => { |
|||
store.dispatch({ |
|||
type: 'SET_USERINFO', |
|||
payload: {} |
|||
}) |
|||
router.history.replace({ pathname: '/login' }) |
|||
} |
|||
|
|||
// 请求前拦截
|
|||
axios.interceptors.request.use( |
|||
(req) => { |
|||
const { token = '' } = store.getState().storeData.userInfo || {} |
|||
// req.headers.Authorization = token
|
|||
req.headers.Token = token |
|||
return req |
|||
}, |
|||
(err) => { |
|||
return Promise.reject(err) |
|||
} |
|||
) |
|||
|
|||
// 返回后拦截
|
|||
axios.interceptors.response.use( |
|||
({ data }): Promise<any> => { |
|||
if (data.code !== 0) { |
|||
message.error(data.message || data.msg) |
|||
return Promise.reject(data) |
|||
} |
|||
return Promise.resolve(data) |
|||
}, |
|||
(err) => { |
|||
message.destroy() |
|||
try { |
|||
if (JSON.stringify(err).includes('401')) { |
|||
clearAll() |
|||
message.error({ title: '提示', content: 'Unauthorized' }) |
|||
return Promise.reject(err) |
|||
} |
|||
} catch (error) { |
|||
clearAll() |
|||
} |
|||
message.error('网络异常') |
|||
return Promise.reject(err) |
|||
} |
|||
) |
|||
|
|||
// post请求
|
|||
axios.post = (url: string, params?: object): Promise<any> => |
|||
axios({ |
|||
method: 'post', |
|||
url, |
|||
data: params |
|||
}) |
|||
|
|||
// get请求
|
|||
axios.get = (url: string, params?: object): Promise<any> => |
|||
axios({ |
|||
method: 'get', |
|||
url, |
|||
params |
|||
}) |
|||
|
|||
// put请求
|
|||
axios.put = (url: string, params?: object): Promise<any> => |
|||
axios({ |
|||
method: 'put', |
|||
url, |
|||
data: params |
|||
}) |
|||
|
|||
// delete请求
|
|||
axios.delete = (url: string, params?: object): Promise<any> => |
|||
axios({ |
|||
method: 'delete', |
|||
url, |
|||
data: params |
|||
}) |
|||
|
|||
export default axios |
@ -0,0 +1,49 @@ |
|||
import { notification } from 'antd'; |
|||
import './axios' |
|||
|
|||
export const splitAddress = (address: string, index?: number) => { |
|||
try { |
|||
let idx = index ? index : 5; |
|||
return address.substring(0, idx) + '...' + address.substring(address.length - idx, address.length) |
|||
} catch (error) { |
|||
return '' |
|||
} |
|||
} |
|||
|
|||
export const getTime = (value: number) => { |
|||
let date = new Date(value); |
|||
let yy: number | string = date.getFullYear(); |
|||
let mm: number | string = date.getMonth() + 1; |
|||
let dd: number | string = date.getDate(); |
|||
let xs: number | string = date.getHours(); |
|||
let ff: number | string = date.getMinutes(); |
|||
let ss: number | string = date.getSeconds(); |
|||
mm = mm >= 10 ? mm : '0' + mm; |
|||
dd = dd >= 10 ? dd : '0' + dd; |
|||
xs = xs >= 10 ? xs : '0' + xs; |
|||
ff = ff >= 10 ? ff : '0' + ff; |
|||
ss = ss >= 10 ? ss : '0' + ss; |
|||
return `${yy}-${mm}-${dd} ${xs}:${ff}:${ss}` |
|||
}; |
|||
|
|||
export function copy(value: string) { |
|||
// 动态创建 textarea 标签
|
|||
const textarea: any = document.createElement('textarea') |
|||
// 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
|
|||
textarea.readOnly = 'readonly' |
|||
textarea.style.position = 'absolute' |
|||
textarea.style.left = '-9999px' |
|||
// 将要 copy 的值赋给 textarea 标签的 value 属性
|
|||
// 网上有些例子是赋值给innerText,这样也会赋值成功,但是识别不了\r\n的换行符,赋值给value属性就可以
|
|||
textarea.value = value |
|||
// 将 textarea 插入到 body 中
|
|||
document.body.appendChild(textarea) |
|||
// 选中值并复制
|
|||
textarea.select() |
|||
textarea.setSelectionRange(0, textarea.value.length) |
|||
document.execCommand('Copy') |
|||
document.body.removeChild(textarea) |
|||
notification.success({ |
|||
message: '复制成功' |
|||
}) |
|||
} |
@ -0,0 +1,49 @@ |
|||
/** |
|||
* tableHook.js 用于处理 Table组件的分页事件 |
|||
* 自定义的一个 server hook,该 hook 封装了 ajax 请求中的 { loading, error, response } 三个基础逻辑; |
|||
* 有了这个 hook 我们就能很轻松的在每次网络请求里面去处理各种异常逻辑了 |
|||
*/ |
|||
import { useEffect, useState, useCallback } from 'react' |
|||
|
|||
export const useServiceCallback = ( |
|||
service: (arg0?: any) => Promise<{}> |
|||
): CommonObjectType[] => { |
|||
const [loading, setLoading] = useState(false) |
|||
const [error, setError] = useState(null) |
|||
const [response, setResponse] = useState(null) |
|||
|
|||
// 使用 useCallback,来判断 service 是否改变
|
|||
const callback = useCallback( |
|||
(params) => { |
|||
|
|||
setLoading(true) |
|||
setError(null) |
|||
service(params) |
|||
.then((res) => { |
|||
setLoading(false) |
|||
setResponse(res) |
|||
}) |
|||
.catch(() => { |
|||
setLoading(false) |
|||
}) |
|||
}, |
|||
[service] |
|||
) |
|||
return [callback, { loading, error, response }] |
|||
} |
|||
|
|||
const useService = ( |
|||
service: (arg0?: any) => Promise<{}>, |
|||
params?: CommonObjectType |
|||
): object => { |
|||
const [callback, { loading, error, response }]: any[] = useServiceCallback( |
|||
service |
|||
) |
|||
useEffect(() => { |
|||
callback(params) |
|||
return () => {} |
|||
}, [callback, params]) |
|||
return { loading, error, response } |
|||
} |
|||
|
|||
export default useService |
@ -0,0 +1,26 @@ |
|||
/** |
|||
* 用于处理详情页多tab页签时,数据多余请求的问题 |
|||
* 当 userId 有值,且有变化时,才会去请求接口 |
|||
*/ |
|||
|
|||
import { useRef } from 'react' |
|||
|
|||
interface Props { |
|||
userIdRef: RefType; |
|||
canRequest: boolean; |
|||
} |
|||
|
|||
const useControl = (userId: string): Props => { |
|||
const userIdRef: RefType = useRef() |
|||
|
|||
// 如果没有 userId,或者 userId 不变
|
|||
if (!userId || userIdRef.current === userId) { |
|||
return { userIdRef, canRequest: false } |
|||
} |
|||
|
|||
// 记录上一次的 userId,用于比较
|
|||
userIdRef.current = userId |
|||
return { userIdRef, canRequest: true } |
|||
} |
|||
|
|||
export default useControl |
@ -0,0 +1,52 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"target": "es5", |
|||
"lib": [ |
|||
"dom", |
|||
"dom.iterable", |
|||
"esnext" |
|||
], |
|||
"allowJs": true, |
|||
"skipLibCheck": true, |
|||
"esModuleInterop": true, |
|||
"allowSyntheticDefaultImports": true, |
|||
"strict": true, |
|||
"forceConsistentCasingInFileNames": true, |
|||
"module": "esnext", |
|||
"moduleResolution": "node", |
|||
"resolveJsonModule": true, |
|||
"isolatedModules": true, |
|||
"noEmit": true, |
|||
"jsx": "react", |
|||
"experimentalDecorators": true, |
|||
"baseUrl": "src", |
|||
"noImplicitAny": false, |
|||
"noImplicitThis": false, |
|||
"strictNullChecks": false, |
|||
"downlevelIteration": true |
|||
}, |
|||
"rules": { |
|||
"member-access": false, |
|||
"ordered-imports": false, |
|||
"quotemark": false, |
|||
"no-console": false, |
|||
"semicolon": false, |
|||
"jsx-no-lambda": false |
|||
}, |
|||
"linterOptions": { |
|||
"exclude": [ |
|||
"config/**/*.js", |
|||
"node_modules/**/*.ts" |
|||
] |
|||
}, |
|||
"include": [ |
|||
"src", |
|||
"typings" |
|||
], |
|||
"exclude": [ |
|||
"node_modules", |
|||
"dist", |
|||
"build" |
|||
], |
|||
"extends": "./paths.json" |
|||
} |
@ -0,0 +1,23 @@ |
|||
{ |
|||
"defaultSeverity": "error", |
|||
"extends": [ |
|||
"tslint-react" |
|||
], |
|||
"jsRules": {}, |
|||
"rules": { |
|||
"member-access": false, |
|||
"ordered-imports": false, |
|||
"quotemark": false, |
|||
"no-console": false, |
|||
"semicolon": false, |
|||
"jsx-no-lambda": false, |
|||
"no-implicit-dependencies": ["optional", ["src"]] |
|||
}, |
|||
"rulesDirectory": [], |
|||
"linterOptions": { |
|||
"exclude": [ |
|||
"config/**/*.js", |
|||
"node_modules/**/*.ts" |
|||
] |
|||
} |
|||
} |
@ -0,0 +1,18 @@ |
|||
/// <reference types="react-scripts" />
|
|||
/// <reference types="node" />
|
|||
/// <reference types="react" />
|
|||
/// <reference types="react-dom" />
|
|||
|
|||
declare module "classnames" { |
|||
import classNames from 'classnames' |
|||
export default classNames |
|||
} |
|||
|
|||
interface ReduxProps { |
|||
storeData?: Record<string, any>; |
|||
setStoreData?: (type: string, payload: any) => object; |
|||
} |
|||
|
|||
type RefType = MutableRefObject<unknown> | ((instance: unknown) => void) |
|||
|
|||
type CommonObjectType<T = any> = Record<string, T> |