start
This commit is contained in:
parent
5fb3d5cbaf
commit
8f84f22e95
16
.editorconfig
Normal file
16
.editorconfig
Normal file
@ -0,0 +1,16 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
8
.eslintignore
Normal file
8
.eslintignore
Normal file
@ -0,0 +1,8 @@
|
||||
/lambda/
|
||||
/scripts
|
||||
/config
|
||||
.history
|
||||
public
|
||||
dist
|
||||
.umi
|
||||
mock
|
||||
124
.eslintrc.json
Normal file
124
.eslintrc.json
Normal file
@ -0,0 +1,124 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"react",
|
||||
"react-hooks",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
//原生eslint配置
|
||||
//规则文档: https://eslint.org/docs/latest/
|
||||
"indent": [
|
||||
"error",
|
||||
2,
|
||||
{
|
||||
"SwitchCase": 1,
|
||||
"ignoredNodes": [ "ConditionalExpression" ]
|
||||
}
|
||||
],
|
||||
"linebreak-style": [ "error", "windows" ],
|
||||
"quotes": [ "error", "single" ],
|
||||
"semi": [ "error", "always" ],
|
||||
"arrow-parens": [ "error", "always" ],
|
||||
"array-bracket-spacing": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"singleValue": true,
|
||||
"objectsInArrays": true,
|
||||
"arraysInArrays": true
|
||||
}
|
||||
],
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"arraysInObjects": true,
|
||||
"objectsInObjects": true
|
||||
}
|
||||
],
|
||||
"no-lonely-if": "error",
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
{
|
||||
"arrays": "always-multiline",
|
||||
"objects": "always-multiline",
|
||||
"imports": "always-multiline",
|
||||
"exports": "always-multiline",
|
||||
"functions": "always-multiline"
|
||||
}
|
||||
],
|
||||
"eol-last": [ "error", "always" ],
|
||||
"no-multiple-empty-lines": [
|
||||
"error",
|
||||
{
|
||||
"max": 1,
|
||||
"maxEOF": 0
|
||||
}
|
||||
],
|
||||
"space-before-function-paren": [
|
||||
"error",
|
||||
{
|
||||
"named": "never",
|
||||
"anonymous": "always",
|
||||
"asyncArrow": "always"
|
||||
}
|
||||
],
|
||||
"keyword-spacing": [
|
||||
"error",
|
||||
{
|
||||
"before": true,
|
||||
"after": true
|
||||
}
|
||||
],
|
||||
"space-before-blocks": "error",
|
||||
//eslint-plugin-react配置
|
||||
//规则文档: https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules
|
||||
"react/sort-comp": [ 2 ],
|
||||
"react/jsx-curly-newline": [
|
||||
"error",
|
||||
{
|
||||
"multiline": "consistent",
|
||||
"singleline": "consistent"
|
||||
}
|
||||
],
|
||||
"react/jsx-max-props-per-line": [
|
||||
2,
|
||||
{
|
||||
"maximum": 1,
|
||||
"when": "always"
|
||||
}
|
||||
],
|
||||
"react/jsx-first-prop-new-line": [
|
||||
"error",
|
||||
"multiline-multiprop"
|
||||
],
|
||||
"react/self-closing-comp": [
|
||||
"error",
|
||||
{
|
||||
"component": true,
|
||||
"html": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
/node_modules
|
||||
/.env.local
|
||||
/.umirc.local.ts
|
||||
/config/config.local.ts
|
||||
/src/.umi
|
||||
/src/.umi-production
|
||||
/src/.umi-test
|
||||
/dist
|
||||
.swc
|
||||
*.md
|
||||
!README.md
|
||||
!README-en_US.md
|
||||
*.log
|
||||
.env
|
||||
1
.husky/.gitignore
vendored
Normal file
1
.husky/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
_
|
||||
7
.husky/commit-msg
Normal file
7
.husky/commit-msg
Normal file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
# Export Git hook params
|
||||
export GIT_PARAMS=$*
|
||||
|
||||
npx --no-install fabric verify-commit
|
||||
201
README-en_US.md
Normal file
201
README-en_US.md
Normal file
@ -0,0 +1,201 @@
|
||||
[简体中文](./README.md) | English
|
||||
|
||||
The recommended `node` version is `18.14.2`, and the recommended `yarn` version is `1.22.x`
|
||||
|
||||
> October 8, 2024: Today, all dependencies were updated to the latest versions. The Node.js version used is the most recent one, 20.18.0. If you encounter issues with installing dependencies or running the program using Node.js 18.14.2, you can try using 20.18.0, or attempt using other versions.
|
||||
|
||||
# Overview
|
||||
|
||||
A lightweight development framework built with [umi4](https://umijs.org/), integrated conventional routing, custom layout(including custom routing rendering), retrieve menu data from server side, and global state management and permission verification, newest pkg and the best development experience, clone to use
|
||||
|
||||
# Git Work Flow
|
||||
```
|
||||
Legal format of commit log were shown as bellow(emoji and module are optional):
|
||||
|
||||
[<emoji>] [revert: ?]<type>[(scope)?]: <message>
|
||||
|
||||
💥 feat(module): added a great feature
|
||||
🐛 fix(module): fixed some bugs
|
||||
📝 docs(module): updated the documentation
|
||||
🌷 UI(module): modified the style
|
||||
🏰 chore(module): made some changes to the scaffold
|
||||
🌐 locale(module): made some small contributions to i18n
|
||||
|
||||
Other commit types: refactor, perf, workflow, build, CI, typos, tests, types, wip, release, dep
|
||||
```
|
||||
|
||||
# Mock
|
||||
Built in `mock service`, start development even backend is not ready~~they always do that~~
|
||||
|
||||
## Interface Response Format
|
||||
```
|
||||
/**
|
||||
* response body
|
||||
* @description data
|
||||
* @description code 0 success, other failure
|
||||
* @description message
|
||||
*/
|
||||
type ResponstBody<T> = {
|
||||
data: T;
|
||||
code: number;
|
||||
message: string;
|
||||
};
|
||||
```
|
||||
This is a common response format, it's suit for most developer, and i choose it as a conventional response format
|
||||
|
||||
# Proxy
|
||||
Nothing much to say, `proxy` is nessary for morden web app development
|
||||
|
||||
# Environment Variable
|
||||
Custom environment variable should start with `UMI_APP_`, and write to `.env` file, so we can visit it via `process.env.UMI_APP_xxx`
|
||||
|
||||
Should i commmit `.env` file to my `git` repository? good question, refer to these 2 articles:
|
||||
|
||||
[Should I commit my .env file?](https://github.com/motdotla/dotenv#should-i-commit-my-env-file)
|
||||
|
||||
[Should I have multiple .env files?](https://github.com/motdotla/dotenv#should-i-have-multiple-env-files)
|
||||
|
||||
I agree not to commit the `.env` file to the repo and to have only one `.env` file, cuz the config in it is not safe for commit to repo, additionally, each environment has different config, we can share `.env` file separately with ohter collaborators via email or IM sorftware if we need to collaborate on development
|
||||
|
||||
In other words: one `.env` file on the developer's computer for development, and one `.env` file on the test server and one on the online server for testing and production packaging respectively.
|
||||
|
||||
# Route
|
||||
Conventional routing. And which specific route is valid is depends on the data returned from menu interface, the data returned from menu interface will be displayed in the left menu bar. While a route(menu data) has been returned by an interface, it's considered valid since it's provided by the interface. However, due to the use of conventional routing, the route will be rendered correctlly only when it has been created in the project directory
|
||||
|
||||
Additionally, the route of login page doesn't need to be returned by menu interface(otherwise it will displayed in the left menu bar), it doesn't use route-based logic to determine it's location (cuz it's not returned by the menu interface). The specific routing and redirection logic can be viewed in `/src/utils/handleRedirect.ts`
|
||||
|
||||
# Menu
|
||||
The menu is returned by server side, and stored in global state, the format of return data should like this: [ItemType](https://ant.design/components/menu#itemtype) which can be consumed by[Menu](https://ant.design/components/menu), and don't include `access` field any more. The menu data of current user is what can be accessed by current user, but not all operations within the page may be accessible to the current user. Page authentication mainly prevents the current user from opening other users' routes (such as opening bookmarks saved by other users). The permission content will be described later, and the `ts` definition for the menu is as bellow:
|
||||
```
|
||||
/**
|
||||
* @description id Data's id
|
||||
|
||||
* @description pid Data's parent id
|
||||
|
||||
* @description key The unique identifier of the menu item, use `string` type instead `React.Key`:
|
||||
https://ant.design/components/menu-cn#itemtype,
|
||||
otherwise there may be a problem where the menu item can't be selected due to an incorrect key type
|
||||
|
||||
* @description lable Menu's title
|
||||
|
||||
* @description hideInMenu Should hidden in menu bar or not
|
||||
|
||||
* @description path Route path, each menu will have this field whether it has `children` field or not,
|
||||
for menus without `children` field, it will redirect to this value, while for menus with `children` field,
|
||||
it will redirect to `redirect` field. Cuz having `children` field which means this menu is expandable,
|
||||
at this time, the `path` with `children` only represents it's position, rather than a truly valid route
|
||||
|
||||
* @description redirect Redirect route path, only the menu which has `children` field has this field,
|
||||
when selectable menu whithin the `children` field of this menu, this value will be the
|
||||
1st selectable menu's `path`, when there are no selectable menu in `children` field of this menu,
|
||||
but still have `children` field, the value is the path of the first selectable menu item in its
|
||||
children's children, that's say no matter how, this value is always the 1st valid route,
|
||||
check menu data in the mock data out for specifics. Also, this field should be an optional field
|
||||
theoretically, however, in order to make it easier for the backend to process, it's written as a
|
||||
fixed field over here, in data where this field is not necessary, the backend can return an
|
||||
empty string
|
||||
|
||||
* @description children Sub menu
|
||||
*/
|
||||
type MenuItem = {
|
||||
id: number;
|
||||
pid?: number;
|
||||
key: string;
|
||||
path: string;
|
||||
redirect: string;
|
||||
hideInMenu?: boolean;
|
||||
label: React.ReactElement | string;
|
||||
children?: MenuItem[];
|
||||
}
|
||||
```
|
||||
|
||||
# Layout
|
||||
Abandoned the default `/src/app.tsx` layout and switched to using a custom layout: `/src/layouts/index.tsx`
|
||||
|
||||
## Implement Custom Rendering In Custom Layout
|
||||
`/src/layouts/index.tsx` won't disturb the login page, beside: when user is login already, if user access login page again at this time(e.g. user manually enter the login page or access it through a bookmark) it will be redirected to a non-login page(if there is a `redirect` field then redirect to `redirect`, or redirect to index page), check this file out for specific codes: `/src/components/LayoutWrapper.tsx`
|
||||
|
||||
# Page Title
|
||||
Using `umi4`'s global `title` config, each page's `location` info and menu data returned from interface implemented dynamic page title for each page, check this out for details: `/src/components/LayoutWrapper.tsx`
|
||||
|
||||
# Data Flow(State Management)
|
||||
Using [dva](https://dvajs.com/) instead of `initial-state` of `umi4` ~~i won't tell u that it's because i abandoed the default `/src/app.tsx` layout, which resulted in the inability to use `initial-state` and `dva` is the only `react` state management solution i know how to use ;)~~ and we can consider combining [dva_umi4](https://umijs.org/docs/max/dva) and [@umijs/plugin-dva](https://v3.umijs.org/plugins/plugin-dva) together
|
||||
|
||||
# Request
|
||||
The request lib is [axios](https://github.com/axios/axios) of course, and the structure of request codes are as bellow:
|
||||
```
|
||||
//...
|
||||
.
|
||||
├── src
|
||||
│ ├── layouts
|
||||
│ │ ├── index.tsx
|
||||
│ │ ├── index.less
|
||||
│ ├── services
|
||||
│ │ └── user.ts
|
||||
│ ├── pages
|
||||
│ │ ├── index.tsx
|
||||
│ │ ├── index.less
|
||||
│ │ ├── pageA
|
||||
│ │ │ └── index.tsx
|
||||
│ │ │ └── index.less
|
||||
│ │ │ ├── services.ts
|
||||
//...
|
||||
```
|
||||
The request codes for `pageA` are all in `serveces.ts`
|
||||
|
||||
# Permission
|
||||
```
|
||||
/**
|
||||
* Page permission type
|
||||
* @description key is route's path, value is permisson array
|
||||
*/
|
||||
type PageAuthority = {
|
||||
[path: string]: string[];
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
/**
|
||||
* permission type for elements within a page
|
||||
* @description key is permission name, value is a specific permission string
|
||||
*/
|
||||
type Authority = {
|
||||
[key: string]: string;
|
||||
}
|
||||
```
|
||||
|
||||
The details of permission logic:
|
||||
|
||||
1. page permission(for entire page): `/src/components/PageAccess.tsx`
|
||||
1. have permission: render page normally(`children`)
|
||||
2. don't have permission: return[result_antd](https://ant.design/components/result-cn)component's[403](https://ant.design/components/result-cn#components-result-demo-403)result
|
||||
|
||||
The processing of page authorization has been placed in `/src/layouts/index.tsx`, cuz this component is the parent of all pages that require authorization processing, making it the most suitable location for handing this
|
||||
|
||||
2. page internal(for certain elements in the page): `/src/components/Access.tsx`
|
||||
1. have permission: render elements normally(`children`)
|
||||
2. don't have permission:
|
||||
1. dont't have `fallback`: render nothing
|
||||
2. have `fallback`: render `fallback`
|
||||
|
||||
The processing of certain elements's permission in the page requires using the `<Access />` component to be separately processed in each individual page
|
||||
|
||||
Both the user info and user permission returned by backend after login are stored in global state, which is `dva`
|
||||
|
||||
# Lint
|
||||
This part refer to [12 essential ESLint rules for React](https://blog.logrocket.com/12-essential-eslint-rules-react/), and i have made some additional configurations as well:
|
||||
```
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
```
|
||||
And the shortcut config of course:
|
||||
```
|
||||
{
|
||||
"key": "alt+f",
|
||||
"command": "eslint.executeAutofix"
|
||||
}
|
||||
```
|
||||
For me, this can imporve experience of programming greatly, i think it should be the same for you all, so i recommend everyone to config it this way
|
||||
|
||||
***Finally, happy hacking my friend ;)***
|
||||
15
config/config.ts
Normal file
15
config/config.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// import { defineConfig } from "umi";
|
||||
// import proxy from './proxy';
|
||||
|
||||
//不使用defineConfig则能在其他地方获取这些配置
|
||||
export default {
|
||||
plugins: [
|
||||
'@umijs/plugins/dist/dva'
|
||||
],
|
||||
// proxy,
|
||||
dva: {},
|
||||
title: 'UMI4 Admin',
|
||||
favicons: [
|
||||
'/favicon.svg'
|
||||
]
|
||||
};
|
||||
8
config/proxy.ts
Normal file
8
config/proxy.ts
Normal file
@ -0,0 +1,8 @@
|
||||
const proxy = {
|
||||
'/api': {
|
||||
target: 'https://preview.pro.ant.design',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
|
||||
export default proxy;
|
||||
259
mock/user.ts
Normal file
259
mock/user.ts
Normal file
@ -0,0 +1,259 @@
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
const waitTime = (time: number = 100) => (
|
||||
new Promise((resolve) => {
|
||||
setTimeout(
|
||||
() => {
|
||||
resolve(true);
|
||||
},
|
||||
time
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
const handleCommonRes = (data: Record<string, unknown> | Record<string, unknown>[], code = 0) => ({
|
||||
data,
|
||||
code,
|
||||
message: !code ? '成功' : '失败'
|
||||
});
|
||||
|
||||
const userApi = {
|
||||
//登录
|
||||
'POST /api/user/login': async (req: Request, res: Response) => {
|
||||
console.log('req', req);
|
||||
console.log('res', res);
|
||||
|
||||
return
|
||||
|
||||
const { password, username, mobile, captcha } = req.body;
|
||||
await waitTime(2000);
|
||||
|
||||
switch (true) {
|
||||
case username === 'admin' && password === 'ant.design':
|
||||
case username === 'user' && password === 'ant.design':
|
||||
case /^1\d{10}$/.test(mobile) && Boolean(captcha):
|
||||
res.send(
|
||||
handleCommonRes({
|
||||
token: 'Bearer xxx'
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
res.send(
|
||||
handleCommonRes(
|
||||
{
|
||||
token: 'Wrong Bearer'
|
||||
},
|
||||
10001
|
||||
)
|
||||
);
|
||||
},
|
||||
//用户信息
|
||||
'GET /api/user/info': async (req: Request, res: Response) => {
|
||||
await waitTime(2000);
|
||||
|
||||
if (!req.headers.authorization) {
|
||||
res.status(401).send();
|
||||
return;
|
||||
}
|
||||
|
||||
res.send(
|
||||
handleCommonRes({
|
||||
name: 'Serati Ma',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
|
||||
userid: '00000001',
|
||||
email: 'antdesign@alipay.com',
|
||||
signature: '海纳百川,有容乃大',
|
||||
title: '交互专家',
|
||||
group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
|
||||
tags: [
|
||||
{
|
||||
key: '0',
|
||||
label: '很有想法的',
|
||||
},
|
||||
{
|
||||
key: '1',
|
||||
label: '专注设计',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: '辣~',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: '大长腿',
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
label: '川妹子',
|
||||
},
|
||||
{
|
||||
key: '5',
|
||||
label: '海纳百川',
|
||||
},
|
||||
],
|
||||
notifyCount: 12,
|
||||
unreadCount: 11,
|
||||
country: 'China',
|
||||
geographic: {
|
||||
province: {
|
||||
label: '浙江省',
|
||||
key: '330000',
|
||||
},
|
||||
city: {
|
||||
label: '杭州市',
|
||||
key: '330100',
|
||||
},
|
||||
},
|
||||
address: '西湖区工专路 77 号',
|
||||
phone: '0752-268888888',
|
||||
})
|
||||
);
|
||||
},
|
||||
//用户权限
|
||||
'GET /api/user/authority': async (req: Request, res: Response) => {
|
||||
await waitTime(1500);
|
||||
|
||||
res.send(
|
||||
handleCommonRes({
|
||||
authority: [
|
||||
'/',
|
||||
'/about/u/1',
|
||||
'/about/u/2',
|
||||
'/about/m',
|
||||
'/about/um',
|
||||
'/teacher/u',
|
||||
'/teacher/m',
|
||||
'/teacher/um',
|
||||
'/student',
|
||||
'/about/m/update'
|
||||
]
|
||||
})
|
||||
);
|
||||
},
|
||||
//菜单
|
||||
'GET /api/user/menu': async (req: Request, res: Response) => {
|
||||
await waitTime(1000);
|
||||
|
||||
res.send(
|
||||
handleCommonRes([
|
||||
{
|
||||
id: 1,
|
||||
key: '1',
|
||||
path: '/',
|
||||
redirect: '',
|
||||
name: '首页'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
key: '2',
|
||||
name: '关于',
|
||||
path: '/about',
|
||||
redirect: '/about/m',
|
||||
children: [
|
||||
{
|
||||
id: 21,
|
||||
key: '2-1',
|
||||
name: '关于你',
|
||||
path: '/about/u',
|
||||
redirect: '/about/u/1',
|
||||
pid: 2,
|
||||
children: [
|
||||
{
|
||||
id: 211,
|
||||
key: '2-1-1',
|
||||
name: '关于你1',
|
||||
path: '/about/u/1',
|
||||
redirect: '',
|
||||
pid: 21
|
||||
},
|
||||
{
|
||||
id: 212,
|
||||
key: '2-1-2',
|
||||
name: '关于你2',
|
||||
path: '/about/u/2',
|
||||
redirect: '',
|
||||
pid: 21
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
key: '2-2',
|
||||
path: '/about/m',
|
||||
redirect: '',
|
||||
name: '(页面元素权限)关于我',
|
||||
pid: 2
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
key: '2-3',
|
||||
path: '/about/um',
|
||||
redirect: '',
|
||||
name: '关于你和我',
|
||||
pid: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
key: '3',
|
||||
name: '教师',
|
||||
path: '/teacher',
|
||||
redirect: '/teacher/u',
|
||||
children: [
|
||||
{
|
||||
id: 31,
|
||||
key: '3-1',
|
||||
path: '/teacher/u',
|
||||
redirect: '',
|
||||
name: '(403)关于你教师',
|
||||
pid: 3
|
||||
},
|
||||
{
|
||||
id: 32,
|
||||
key: '3-2',
|
||||
path: '/teacher/m',
|
||||
redirect: '',
|
||||
name: '关于我教师',
|
||||
pid: 3
|
||||
},
|
||||
{
|
||||
id: 33,
|
||||
key: '3-3',
|
||||
path: '/teacher/um',
|
||||
redirect: '',
|
||||
name: '关于你和我教师',
|
||||
pid: 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
key: '4',
|
||||
name: '(404)学生',
|
||||
path: '/student',
|
||||
redirect: '',
|
||||
}
|
||||
])
|
||||
);
|
||||
},
|
||||
//登出
|
||||
'POST /api/user/logout': (req: Request, res: Response) => {
|
||||
res.send(
|
||||
handleCommonRes({})
|
||||
);
|
||||
},
|
||||
//验证码
|
||||
'GET /api/user/captcha': async (req: Request, res: Response) => {
|
||||
await waitTime(2000);
|
||||
return res.send(
|
||||
handleCommonRes({
|
||||
captcha: 'captcha-xxx'
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default userApi;
|
||||
33
package.json
Normal file
33
package.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"private": true,
|
||||
"author": "小镇靓仔 <misunderstandjerry@qq.com>",
|
||||
"scripts": {
|
||||
"dev": "umi dev",
|
||||
"build": "umi build",
|
||||
"setup": "umi setup",
|
||||
"postinstall": "umi setup",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.5.1",
|
||||
"@ant-design/pro-components": "^2.8.6",
|
||||
"antd": "^5.21.2",
|
||||
"axios": "^1.7.7",
|
||||
"dva": "^3.0.0-alpha.1",
|
||||
"umi": "^4.3.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.8.1",
|
||||
"@typescript-eslint/parser": "^8.8.1",
|
||||
"@umijs/fabric": "^4.0.1",
|
||||
"@umijs/plugins": "^4.3.24",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-plugin-react": "^7.37.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"husky": "^9.1.6",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
1
public/favicon.svg
Normal file
1
public/favicon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="28" height="27" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M26.594 12.968c0 .912-.74 1.651-1.652 1.651H3.058a1.651 1.651 0 01-.852-3.065 1.65 1.65 0 01.543-2.547 1.651 1.651 0 011.778-2.213A1.651 1.651 0 016.79 4.532a1.651 1.651 0 012.472-1.654 1.652 1.652 0 012.977-.786 1.652 1.652 0 013.11 0 1.652 1.652 0 012.977.785 1.651 1.651 0 012.472 1.654 1.651 1.651 0 012.263 2.262 1.651 1.651 0 011.778 2.213 1.65 1.65 0 01.65 2.404 1.652 1.652 0 011.105 1.558z" stroke="#000" stroke-width=".8" fill="#FFF" stroke-linejoin="round"/><path fill="#000" d="M9.531 24.937h8.938V27H9.531z"/><path d="M1 13.794h26c-.42 6.909-6.08 12.38-13 12.38S1.42 20.704 1 13.795h0z" stroke="#000" stroke-width=".8" fill="#1890FF"/><path d="M8.313 8.016a.41.41 0 01-.407-.413.41.41 0 01.407-.413.41.41 0 01.406.413.41.41 0 01-.406.413zm-1.626 2.063a.41.41 0 01-.406-.412.41.41 0 01.407-.413.41.41 0 01.406.413.41.41 0 01-.407.412zm1.625 1.651a.41.41 0 01-.406-.413.41.41 0 01.407-.412.41.41 0 01.406.412.41.41 0 01-.406.413zm1.626-3.3a.41.41 0 01-.407-.413.41.41 0 01.406-.413.41.41 0 01.407.413.41.41 0 01-.406.413zm9.75 1.65a.41.41 0 01-.407-.412.41.41 0 01.407-.413.41.41 0 01.406.413.41.41 0 01-.407.412z" fill="#000"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
public/logo.svg
Normal file
1
public/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200" version="1.1" viewBox="0 0 200 200"><title>Group 28 Copy 5</title><desc>Created with Sketch.</desc><defs><linearGradient id="linearGradient-1" x1="62.102%" x2="108.197%" y1="0%" y2="37.864%"><stop offset="0%" stop-color="#4285EB"/><stop offset="100%" stop-color="#2EC7FF"/></linearGradient><linearGradient id="linearGradient-2" x1="69.644%" x2="54.043%" y1="0%" y2="108.457%"><stop offset="0%" stop-color="#29CDFF"/><stop offset="37.86%" stop-color="#148EFF"/><stop offset="100%" stop-color="#0A60FF"/></linearGradient><linearGradient id="linearGradient-3" x1="69.691%" x2="16.723%" y1="-12.974%" y2="117.391%"><stop offset="0%" stop-color="#FA816E"/><stop offset="41.473%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient><linearGradient id="linearGradient-4" x1="68.128%" x2="30.44%" y1="-35.691%" y2="114.943%"><stop offset="0%" stop-color="#FA8E7D"/><stop offset="51.264%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient></defs><g id="Page-1" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><g id="logo" transform="translate(-20.000000, -20.000000)"><g id="Group-28-Copy-5" transform="translate(20.000000, 20.000000)"><g id="Group-27-Copy-3"><g id="Group-25" fill-rule="nonzero"><g id="2"><path id="Shape" fill="url(#linearGradient-1)" d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C99.2571609,26.9692191 101.032305,26.9692191 102.20193,28.1378823 L129.985225,55.8983314 C134.193707,60.1033528 141.017005,60.1033528 145.225487,55.8983314 C149.433969,51.69331 149.433969,44.8756232 145.225487,40.6706018 L108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z"/><path id="Shape" fill="url(#linearGradient-2)" d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C100.999864,25.6271836 105.751642,20.541824 112.729652,19.3524487 C117.915585,18.4685261 123.585219,20.4140239 129.738554,25.1889424 C125.624663,21.0784292 118.571995,14.0340304 108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z"/></g><path id="Shape" fill="url(#linearGradient-3)" d="M153.685633,135.854579 C157.894115,140.0596 164.717412,140.0596 168.925894,135.854579 L195.959977,108.842726 C200.659183,104.147384 200.659183,96.5636133 195.960527,91.8688194 L168.690777,64.7181159 C164.472332,60.5180858 157.646868,60.5241425 153.435895,64.7316526 C149.227413,68.936674 149.227413,75.7543607 153.435895,79.9593821 L171.854035,98.3623765 C173.02366,99.5310396 173.02366,101.304724 171.854035,102.473387 L153.685633,120.626849 C149.47715,124.83187 149.47715,131.649557 153.685633,135.854579 Z"/></g><ellipse id="Combined-Shape" cx="100.519" cy="100.437" fill="url(#linearGradient-4)" rx="23.6" ry="23.581"/></g></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
0
src/assets/.gitkeep
Normal file
0
src/assets/.gitkeep
Normal file
BIN
src/assets/login-bg.png
Normal file
BIN
src/assets/login-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
37
src/assets/logo.svg
Normal file
37
src/assets/logo.svg
Normal file
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="35px" height="30px" viewBox="0 0 35 30" enable-background="new 0 0 35 30" xml:space="preserve"> <image id="image0" width="35" height="30" x="0" y="0"
|
||||
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAeCAYAAACmPacqAAAABGdBTUEAALGPC/xhBQAAACBjSFJN
|
||||
AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAA
|
||||
B3RJTUUH5QkBEx07b/a9lAAABN5JREFUWMO1l1tsVUUUhr+16ZVyCbESEi/RB1BAtPVBg0ZAgyjB
|
||||
qBFFuRoRUe4FVIiJGJUEXoxBQaNAJDTcJFK5aJSbRZ7QBzSIYGgowSZiKBcFbaGc9fuw99lnn/aU
|
||||
tiDzMvvMnj3zzT//zFrHJNFWSU2dUyrxNDAMqRy4GQiQHKhDHAD2gKryPl1S0+aArRS7HMylKXOK
|
||||
kN4FpgHFpPvGnyjzHL/TTmBh3soP9/xvMJdemX090g7grjYmTwAq/on0OTAtf9Wy+quCuTS5IgB2
|
||||
gwZ3YPKW76XfgRH5lR8fuHKYl2aNQ6pMDH4CcRBUK3ESaEIqBW5CuhfoGU2eC7ge8UD+2k8OXxnM
|
||||
izOqEadAX0l8n//Z0lZN2TR+iiHdB8xGjMxsZVhH4x9GlBdsWN7YYZimF6Z3z/9s6V/tkTbru9GT
|
||||
H5a0FihNAkVKVRRsXLmkwzBXUy6OmjQAaS/QvZl/jgJzkb4rqFqVc6E5Yfou/7uXxDi5PYK4TW69
|
||||
5OQjzsutTs5PyHbKqTr6WufTLYBGTnwWaX0IoqiKt+8iYiOwqHDL6oOtwvRbdTYftwUSr8qtCAfJ
|
||||
kANuSCC3UP2wrVFimdzerp1XfC458IUnn69GDG5xHWS2LwVajHir8Os1qSyY/qvPFiK2yW2oBGkQ
|
||||
PAQI20KwuD0G5YBkD9bOKz4Vwzw+4VakERHQcERJMw+l1VoHjC/6dl0qiCUyLcMYaibMACOuM8+C
|
||||
AMwUtUV9AwaYkWXOwi2rawu3Vi4t3Fb5DCm/AfePlHKUcuRCKQcXuI8m5W/EygxYe2aonB0ovfLM
|
||||
tmRtT1KNqC18NuRIsltqXy8+3pq5G4c9Nx9pUdZJC+uLiN5B5Jx3sldOpk6oQMC/mLabsRpjO8bZ
|
||||
hIpmpscud9KKtq9fjPubuENKpGu5F8h9ct6dG07fbcZABaFPDCGzuCYAc5Cx3tD036Z2i33R+4Pz
|
||||
RQpsnLnek1k3oKyto1+0a+PChiEj+yLGZDwEoOEBpqeIFMisspknTF9YwJjDL2dAAI7M7NJYM6tk
|
||||
BcYgAp02o2dbMACkNAf3C7E67uDqE5gxJGnQlubVBTNmHJrUrdXbsaai5GczRmHq1B6W4r2b/sRV
|
||||
HUGkgboEGH3MFJ2oJFBaHbb9OrH7H21NUFNRssuMqnYpA+B+Ahfy6HS5GvLMuE6EWyIMA2RhfwuE
|
||||
3H5o9wQB69rbVa7SZlH+SB4W3Y+W6GkhVgTZ7jSyZlZJQweU6Zt9xNkXgE5aBJMxLkn/dG33BB0p
|
||||
rrqMZwTuywOMQ7lPUgz06LWB8TlIx3A1Is3tvP+bH62s6uR8iUVhIhQGwfimDW9gSTz0y9ge1dcE
|
||||
KlECjEozmmI1oHk8MjO23rHmzIRrDWOSKP+y/n1EhUQYZ6JbMROdScef/ZKtwNktcSKKW13l1qkd
|
||||
6UayLSW3c+l3kjUdX1D4Twizub4EsU+iP82B0gM2C5BxoOx4utGyTdYgMTjOZ8o319+I2CFxe1KV
|
||||
GCQrkuccMJw8oUYSMBs+rVLmO2R743xm/xOldRj3mLESi+69HPlMjnCR9c6CzGmM+0ZtOW735Nj3
|
||||
58yBy6vq+0nMRDZKokcL/6RXllAnVCDXtrTMh9LjJBVDHLvsv4OyTfWFyAZKDEKUya03opdkpa37
|
||||
p1nylcs/nnPbx/4HXgSzauZflRIAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjEtMDktMDFUMTE6Mjk6
|
||||
NTkrMDg6MDCXs8feAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIxLTA5LTAxVDExOjI5OjU5KzA4OjAw
|
||||
5u5/YgAAACB0RVh0c29mdHdhcmUAaHR0cHM6Ly9pbWFnZW1hZ2ljay5vcme8zx2dAAAAGHRFWHRU
|
||||
aHVtYjo6RG9jdW1lbnQ6OlBhZ2VzADGn/7svAAAAF3RFWHRUaHVtYjo6SW1hZ2U6OkhlaWdodAAz
|
||||
MMb6mdgAAAAWdEVYdFRodW1iOjpJbWFnZTo6V2lkdGgAMzVOP63aAAAAGXRFWHRUaHVtYjo6TWlt
|
||||
ZXR5cGUAaW1hZ2UvcG5nP7JWTgAAABd0RVh0VGh1bWI6Ok1UaW1lADE2MzA0NjY5OTka9aiIAAAA
|
||||
EnRFWHRUaHVtYjo6U2l6ZQAxMzg5QkJc76D9AAAARnRFWHRUaHVtYjo6VVJJAGZpbGU6Ly8vYXBw
|
||||
L3RtcC9pbWFnZWxjL2ltZ3ZpZXcyXzlfMTYzMDQ2NDI3MzkwMjUxNjNfMzVfWzBdmaJpYAAAAABJ
|
||||
RU5ErkJggg==" ></image>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
32
src/components/Access.tsx
Normal file
32
src/components/Access.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import type { FC, ReactElement } from 'react';
|
||||
import { connect } from 'umi';
|
||||
import type { UserConnectedProps } from '@/models/user';
|
||||
|
||||
type Props = {
|
||||
authority: string;
|
||||
children: ReactElement;
|
||||
fallback?: ReactElement;
|
||||
} & UserConnectedProps;
|
||||
|
||||
//处理页面元素权限的组件
|
||||
const Access: FC<Props> = (props): ReactElement | null => {
|
||||
const { user, authority, fallback, children } = props;
|
||||
const { authority: userAuthority } = user;
|
||||
const accessible = userAuthority.some((item: string) => item === authority);
|
||||
|
||||
let res = null;
|
||||
|
||||
if (accessible) {
|
||||
res = children;
|
||||
} else if (fallback) {
|
||||
res = fallback;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export default connect(
|
||||
({ user }: { user: UserConnectedProps['user'] }) => ({
|
||||
user,
|
||||
}),
|
||||
)(Access);
|
||||
18
src/components/Avatar/index.less
Normal file
18
src/components/Avatar/index.less
Normal file
@ -0,0 +1,18 @@
|
||||
@import '@/global.less';
|
||||
|
||||
.avatar-container{
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
.avatar{
|
||||
display: block;
|
||||
width: 30.9px;
|
||||
height: 30.9px;
|
||||
}
|
||||
.username{
|
||||
color: #fff;
|
||||
}
|
||||
&:hover {
|
||||
// background: #252a3d;
|
||||
}
|
||||
}
|
||||
59
src/components/Avatar/index.tsx
Normal file
59
src/components/Avatar/index.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import type { FC } from 'react';
|
||||
import { connect } from 'umi';
|
||||
import { Dropdown, Space } from 'antd';
|
||||
import { LogoutOutlined } from '@ant-design/icons';
|
||||
import type { UserConnectedProps } from '@/models/user';
|
||||
import './index.less';
|
||||
|
||||
const Avatar: FC<UserConnectedProps> = (props) => {
|
||||
const {
|
||||
user: { data }, dispatch,
|
||||
} = props;
|
||||
|
||||
const handleLogout = () => {
|
||||
dispatch?.({
|
||||
type: 'user/logout',
|
||||
});
|
||||
};
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: 'logout',
|
||||
label: (
|
||||
<a
|
||||
onClick={handleLogout}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '0 8px',
|
||||
}}
|
||||
>
|
||||
<LogoutOutlined />
|
||||
<span>退出登录</span>
|
||||
</a>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
menu={{ items }}
|
||||
>
|
||||
<Space className="avatar-container">
|
||||
<img
|
||||
alt="avatar"
|
||||
src={data.avatar}
|
||||
className="avatar"
|
||||
/>
|
||||
<span className="username">{data.name}</span>
|
||||
</Space>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
({ user }: { user: UserConnectedProps['user'] }) => ({
|
||||
user,
|
||||
}),
|
||||
)(Avatar);
|
||||
47
src/components/Footer.tsx
Normal file
47
src/components/Footer.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* @Author: cclu 1106109051@qq.com
|
||||
* @Date: 2025-02-25 11:39:58
|
||||
* @LastEditors: cclu 1106109051@qq.com
|
||||
* @LastEditTime: 2025-02-26 11:25:46
|
||||
* @FilePath: \umi4-admin-main\src\components\Footer.tsx
|
||||
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
*/
|
||||
import type { FC } from 'react';
|
||||
import { DefaultFooter } from '@ant-design/pro-layout';
|
||||
|
||||
const Footer: FC = () => {
|
||||
return (
|
||||
<DefaultFooter
|
||||
style={{ background: "#f0f2f5", color: "rgba(0, 0, 0, 0.45)" }}
|
||||
copyright={`Copyright © 2013-${new Date().getFullYear()} Eshang Cloud. All Rights Reserved. 驿商云 版权所有`}
|
||||
/>
|
||||
// <DefaultFooter
|
||||
// style={{
|
||||
// backgroundColor: 'inherit',
|
||||
// }}
|
||||
// copyright={`${currentYear} ${defaultMessage}`}
|
||||
// links={[
|
||||
// {
|
||||
// key: 'Ant Design Pro',
|
||||
// title: 'Ant Design Pro',
|
||||
// href: 'https://pro.ant.design',
|
||||
// blankTarget: true,
|
||||
// },
|
||||
// {
|
||||
// key: 'github',
|
||||
// title: <GithubOutlined />,
|
||||
// href: 'https://github.com/ant-design/ant-design-pro',
|
||||
// blankTarget: true,
|
||||
// },
|
||||
// {
|
||||
// key: 'Ant Design',
|
||||
// title: 'Ant Design',
|
||||
// href: 'https://ant.design',
|
||||
// blankTarget: true,
|
||||
// },
|
||||
// ]}
|
||||
// />
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
96
src/components/LayoutWrapper.tsx
Normal file
96
src/components/LayoutWrapper.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import { Fragment, useEffect } from 'react';
|
||||
import type { FC, ReactElement } from 'react';
|
||||
import { Spin } from 'antd';
|
||||
import { Helmet, useLocation, connect } from 'umi';
|
||||
import type { UserConnectedProps } from '@/models/user';
|
||||
import LoginPage from '@/pages/user/login';
|
||||
import cfg from '../../config/config';
|
||||
|
||||
type Props = {
|
||||
children: ReactElement;
|
||||
} & UserConnectedProps;
|
||||
|
||||
//登录页不走/src/layouts, 登录完之后的页面才走/src/layouts
|
||||
const LayoutWrapper: FC<Props> = (props) => {
|
||||
const { pathname } = useLocation();
|
||||
const {
|
||||
dispatch, children,
|
||||
user: { isLogin, layoutWrapperLoading, indexValidMenuItemByPath },
|
||||
} = props;
|
||||
const tabTitle = indexValidMenuItemByPath[pathname]?.label;
|
||||
const projectTitle = cfg.title;
|
||||
const title = tabTitle ? `${tabTitle} - ${projectTitle}` : projectTitle;
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
//调用户信息相关接口查询登录状态(401表示未登录)
|
||||
dispatch?.({
|
||||
type: 'user/getUserInfoAuthorityMenu',
|
||||
payload: {
|
||||
type: 'relay',
|
||||
},
|
||||
});
|
||||
},
|
||||
[ dispatch ],
|
||||
);
|
||||
|
||||
//进任何页面都需要先loading
|
||||
let pageContent = (
|
||||
<Spin
|
||||
size="large"
|
||||
style={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
//页面不loading
|
||||
/**
|
||||
一开始我是这么写的:
|
||||
if(!layoutWrapperLoading) {
|
||||
if(isLogin){
|
||||
if(pathname === '/user/login') {
|
||||
登录过了, 此时页面url还是登录页的时候我希望不再进入登录页而是直接跳转离开
|
||||
handleRedirect();
|
||||
这里如果使用了history.push则会报如下的错误:
|
||||
Warning: Cannot update a component (`BrowserRoutes`) while rendering a different component (`LayoutWrapper`).
|
||||
To locate the bad setState() call inside `LayoutWrapper`, follow the stack trace as described in
|
||||
https://reactjs.org/link/setstate-in-render
|
||||
大意是不能在render的时候更新路由, 也就是调history.push更新BrowserRoutes
|
||||
使用window.location.href = 'xxx';的方式用户会再一次看到整个页面的loading, 体验不好
|
||||
后来我将逻辑写到用户登录状态确认之后, 也就是全局状态管理中: /src/models/user.ts就可以了
|
||||
搜索 handleRedirect 即可找到相应的代码
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
if (!layoutWrapperLoading) {
|
||||
if (isLogin) {
|
||||
if (pathname !== '/user/login') {
|
||||
pageContent = children;
|
||||
}
|
||||
} else {
|
||||
//只要没登陆过就渲染登录页而不是跳转到登录页, 因为登录页不走/src/layouts
|
||||
pageContent = <LoginPage />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Helmet>
|
||||
<title>{title}</title>
|
||||
</Helmet>
|
||||
{pageContent}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
({ user }: { user: UserConnectedProps['user'] }) => ({
|
||||
user,
|
||||
}),
|
||||
)(LayoutWrapper);
|
||||
24
src/components/Nav/index.less
Normal file
24
src/components/Nav/index.less
Normal file
@ -0,0 +1,24 @@
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
padding: 0 15px;
|
||||
// background-color: #001529;
|
||||
.former {
|
||||
display: flex;
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.latter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0 15px;
|
||||
}
|
||||
}
|
||||
43
src/components/Nav/index.tsx
Normal file
43
src/components/Nav/index.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import type { FC } from 'react';
|
||||
import { Link } from 'umi';
|
||||
import Avatar from '../Avatar';
|
||||
import './index.less';
|
||||
|
||||
const Nav: FC = () => {
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="former">
|
||||
{/* <div className="left">
|
||||
<Link
|
||||
to="/"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0 15px',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt="logo"
|
||||
src="/logo.svg"
|
||||
className="logo"
|
||||
/>
|
||||
<h1
|
||||
style={{
|
||||
margin: 0,
|
||||
color: '#fff',
|
||||
fontSize: '18px',
|
||||
}}
|
||||
>UMI4 Admin</h1>
|
||||
</Link>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="latter">
|
||||
<Avatar />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Nav;
|
||||
48
src/components/PageAccess.tsx
Normal file
48
src/components/PageAccess.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import type { FC, ReactElement } from 'react';
|
||||
import { connect, history, useLocation } from 'umi';
|
||||
import { Button, Result } from 'antd';
|
||||
import authority from '@/pages/authority';
|
||||
import type { UserConnectedProps } from '@/models/user';
|
||||
|
||||
type Props = {
|
||||
children: ReactElement;
|
||||
} & UserConnectedProps;
|
||||
|
||||
//处理页面权限的组件, 放置在所有需要鉴权的页面的最外层
|
||||
const PageAccess: FC<Props> = (props): ReactElement | null => {
|
||||
const { user, children } = props;
|
||||
const { authority: userAuthority } = user;
|
||||
const { pathname } = useLocation();
|
||||
//可选操作符用来处理子路由跳转的情形, 因为这里的权限都是有效路由的权限,
|
||||
//当遇到输入的是菜单组件中可展开的节点的path的时候就没有权限了, 此时可能会报错,
|
||||
//加可选操作符可避免这个报错导致的页面渲染问题
|
||||
const accessible = authority[pathname]?.some((item: string) => userAuthority.includes(item));
|
||||
|
||||
let res = children;
|
||||
|
||||
if (!accessible) {
|
||||
res = (
|
||||
<Result
|
||||
title="403"
|
||||
status="403"
|
||||
subTitle="对不起, 您没有访问此页面的权限"
|
||||
extra={(
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={
|
||||
() => history.push('/')
|
||||
}
|
||||
>返回首页</Button>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export default connect(
|
||||
({ user }: { user: UserConnectedProps['user'] }) => ({
|
||||
user,
|
||||
}),
|
||||
)(PageAccess);
|
||||
7
src/global.less
Normal file
7
src/global.less
Normal file
@ -0,0 +1,7 @@
|
||||
html,body{
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
*{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
56
src/layouts/index.less
Normal file
56
src/layouts/index.less
Normal file
@ -0,0 +1,56 @@
|
||||
.proLayoutBox{
|
||||
.ant-layout{
|
||||
.ant-layout-sider{
|
||||
background-color: #021529;
|
||||
.ant-layout-sider-children{
|
||||
padding: 0;
|
||||
.ant-pro-sider-logo{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
a{
|
||||
display: block;
|
||||
h1{
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
line-height: 32px;
|
||||
vertical-align: none;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-layout-sider-children{
|
||||
.ant-pro-sider-extra{
|
||||
margin: 0;
|
||||
.ant-layout{
|
||||
ul{
|
||||
li{
|
||||
span{
|
||||
font-size: 14px;
|
||||
color: #ffffffA6;
|
||||
}
|
||||
i{
|
||||
font-size: 14px;
|
||||
color: #ffffffA6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.ant-pro-layout-container{
|
||||
background-color: #f0f2f5;
|
||||
main{
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
346
src/layouts/index.tsx
Normal file
346
src/layouts/index.tsx
Normal file
@ -0,0 +1,346 @@
|
||||
import type { FC } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Dropdown, Layout, Menu, Tabs, Tooltip } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Outlet, Link, useLocation, connect } from 'umi';
|
||||
import PageAccess from '@/components/PageAccess';
|
||||
import type { UserConnectedProps, UserModelState } from '@/models/user';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper';
|
||||
import Nav from '@/components/Nav';
|
||||
import Page404 from '@/pages/404';
|
||||
import handleRecursiveNestedData from '@/utils/handleRecursiveNestedData';
|
||||
import handleGetCurrentLocation from '@/utils/handleGetCurrentLocation';
|
||||
import { MenuDataItem, ProLayout } from '@ant-design/pro-components';
|
||||
import './index.less'
|
||||
import logo from '../assets/logo.svg';
|
||||
import Icon, { DoubleRightOutlined, SmileOutlined } from '@ant-design/icons';
|
||||
import { ProfileModelState } from '@/models/global';
|
||||
import React from 'react';
|
||||
|
||||
const { Header, Content } = Layout;
|
||||
/**
|
||||
* 获取openKeys的方法
|
||||
* @param currentLocation 当前位置, 由handleGetCurrentLocation方法返回
|
||||
* @returns openKeys
|
||||
*/
|
||||
const handleGetOpenKeys = (currentLocation: API.MenuItem[] | []): string[] => {
|
||||
//currentLocation为空
|
||||
if (!currentLocation.length) return [''];
|
||||
|
||||
//currentLocation只有一项
|
||||
if (currentLocation.length === 1) {
|
||||
return currentLocation.map((item: API.MenuItem) => `${item.key}`);
|
||||
}
|
||||
|
||||
const res = [];
|
||||
|
||||
//currentLocation有多项, 只要前n-1项
|
||||
for (let i = 0; i < currentLocation.length - 1; i++) {
|
||||
res.push(`${currentLocation[i].key}`);
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
//自定义的layout页面, 顶部导航通栏+侧边栏(菜单)布局, 可根据需要做调整
|
||||
const BasicLayout: FC<{ user: UserModelState, global: ProfileModelState, dispatch: any }> = (props) => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [openKeys, setOpenKeys] = useState(['']);
|
||||
const { pathname } = useLocation();
|
||||
const {
|
||||
dispatch,
|
||||
user: {
|
||||
menu, rootSubmenuKeys, indexAllMenuItemById,
|
||||
indexValidMenuItemByPath
|
||||
},
|
||||
global: {
|
||||
|
||||
}
|
||||
} = props;
|
||||
|
||||
console.log('props', props);
|
||||
|
||||
|
||||
const validMenuItem = indexValidMenuItemByPath[pathname];
|
||||
const selectedKeys = validMenuItem?.key;
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
//每次页面重新渲染都要设置openKeys
|
||||
setOpenKeys(
|
||||
handleGetOpenKeys(
|
||||
handleGetCurrentLocation(indexValidMenuItemByPath[pathname], indexAllMenuItemById),
|
||||
),
|
||||
);
|
||||
},
|
||||
[pathname, indexAllMenuItemById, indexValidMenuItemByPath],
|
||||
);
|
||||
|
||||
//Menu中的selectedKeys和openKeys不是一回事:
|
||||
//openKeys:
|
||||
//当前展开的SubMenu菜单项key数组, 有子菜单的父菜单, 当selectedKeys为没子菜单的父菜单时该值应该设为[''],
|
||||
//也就是关闭所有有子菜单的父菜单;
|
||||
//selectedKeys:
|
||||
//当前选中的菜单项key数组, 有子菜单则是子菜单(叶子节点), 没有子菜单则是父菜单(一级菜单), 始终是可选中的
|
||||
|
||||
//点击有子菜单的父菜单的回调
|
||||
// const onOpenChange: MenuProps['onOpenChange'] = (keys) => {
|
||||
// setOpenKeys(keys)
|
||||
// console.log('keys', keys);
|
||||
// dispatch({
|
||||
// namespace: "global/setProfileData",
|
||||
// payload: {
|
||||
// keys
|
||||
// }
|
||||
// })
|
||||
// };
|
||||
|
||||
//所有MenuItem:
|
||||
//有children的: 一定都有path, lable不动, children下的label修改为<Link to={path} />
|
||||
//无children的: 有path的label修改为<Link to={path} />, 没path的label不动
|
||||
const consumableMenu = handleRecursiveNestedData(
|
||||
menu,
|
||||
(item: API.MenuItem) => ({
|
||||
...item,
|
||||
name: item.path
|
||||
? (
|
||||
<Link
|
||||
to={item.path}
|
||||
style={{ color: 'inherit' }}
|
||||
>{item.name}</Link>
|
||||
)
|
||||
: item.name,
|
||||
}),
|
||||
);
|
||||
// const consumableMenu: MenuDataItem[] = [
|
||||
// {
|
||||
// path: '/',
|
||||
// name: 'Dashboard',
|
||||
// icon: <SmileOutlined />,
|
||||
// },
|
||||
// {
|
||||
// path: '/about',
|
||||
// name: 'Users',
|
||||
// icon: <SmileOutlined />,
|
||||
// children: [
|
||||
// {
|
||||
// path: '/about/u',
|
||||
// name: 'User List',
|
||||
// },
|
||||
// {
|
||||
// path: '/about/m',
|
||||
// name: 'User Profile',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ];
|
||||
console.log('consumableMenu', consumableMenu);
|
||||
const location = useLocation();
|
||||
|
||||
// 改变panes
|
||||
const handleTabsPanes = (payload: any): void => {
|
||||
dispatch({
|
||||
type: "global/saveTabsRoutes",
|
||||
payload: {
|
||||
data: payload,
|
||||
action: 'add'
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<ProLayout
|
||||
logo={logo}
|
||||
contentWidth={"Fluid"}
|
||||
fixedHeader={true}
|
||||
fixSiderbar={true}
|
||||
colorWeak={false}
|
||||
title={"驿商云平台"}
|
||||
pwa={false}
|
||||
iconfontUrl={'//at.alicdn.com/t/font_2794551_djdgwbunsvg.js'}
|
||||
footerRender={false}
|
||||
location={location}
|
||||
style={{ minHeight: '100vh' }}
|
||||
siderWidth={208}
|
||||
menuDataRender={() =>
|
||||
consumableMenu
|
||||
// menu
|
||||
}
|
||||
subMenuItemRender={(menuItemProps) => {
|
||||
if (menuItemProps.icon && typeof menuItemProps.icon === 'string') {
|
||||
const ele = React.createElement(Icon[menuItemProps.icon])
|
||||
return <div className="ant-pro-menu-item">
|
||||
<span style={{ marginRight: 10 }} className="ant-pro-menu-item">{ele}</span>
|
||||
<span className='ant-pro-menu-item-title'>{menuItemProps.name}</span>
|
||||
</div>;
|
||||
}
|
||||
return <div>
|
||||
{menuItemProps.name}
|
||||
</div>;
|
||||
}}
|
||||
menuItemRender={(menuItemProps, defaultDom) => {
|
||||
if (
|
||||
menuItemProps.isUrl ||
|
||||
!menuItemProps.path ||
|
||||
location.pathname === menuItemProps.path
|
||||
) {
|
||||
return defaultDom;
|
||||
}
|
||||
return <Link to={menuItemProps.path || '/'} onClick={() => {
|
||||
}}>
|
||||
{defaultDom}
|
||||
{/* {menuItemProps.name} */}
|
||||
</Link>;
|
||||
}}
|
||||
breadcrumbRender={false}
|
||||
itemRender={(route, params, routes, paths) => {
|
||||
const first = routes.indexOf(route) === 0;
|
||||
return first ? (
|
||||
<Link to={paths.join('/')}>{route.breadcrumbName}</Link>
|
||||
) : (
|
||||
<span>{route.breadcrumbName}</span>
|
||||
);
|
||||
}}
|
||||
actionsRender={() => <nav />}
|
||||
onPageChange={(location) => {
|
||||
console.log('location', location);
|
||||
if (location?.pathname && location?.pathname !== '/') {
|
||||
const nextModule = consumableMenu.find(n => location?.pathname && n?.path && location?.pathname.match(n?.path))
|
||||
console.log('nextModule', nextModule);
|
||||
|
||||
}
|
||||
handleTabsPanes({ path: location?.pathname, key: location?.pathname })
|
||||
}}
|
||||
>
|
||||
|
||||
{/* menuExtraRender={() =>
|
||||
<Layout>
|
||||
<Menu
|
||||
mode="inline"
|
||||
openKeys={openKeys}
|
||||
items={consumableMenu}
|
||||
onOpenChange={(e: any, dom: any) => {
|
||||
onOpenChange(e)
|
||||
}}
|
||||
onSelect={(item: any, key: any) => {
|
||||
console.log('item', item);
|
||||
console.log('key', key);
|
||||
}}
|
||||
selectedKeys={[selectedKeys]}
|
||||
/>
|
||||
</Layout>
|
||||
} */}
|
||||
|
||||
<Layout>
|
||||
<Header style={{ background: '#fff', height: '48px', padding: '0 16px' }}>
|
||||
<Nav />
|
||||
</Header>
|
||||
<Content>
|
||||
<Tabs
|
||||
hideAdd
|
||||
type="editable-card"
|
||||
size="small"
|
||||
className='main-tab'
|
||||
tabBarExtraContent={
|
||||
<Tooltip title="关闭选项卡" placement="topRight">
|
||||
<Dropdown overlay={
|
||||
<Menu onClick={(targetKey) => {
|
||||
}}>
|
||||
<Menu.Item key="other">关闭其他标签</Menu.Item>
|
||||
<Menu.Item key="now">关闭当前标签</Menu.Item>
|
||||
<Menu.Item key="all">关闭全部标签</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
trigger={['click']}
|
||||
placement="bottomRight"
|
||||
arrow>
|
||||
<div onClick={(e) => {
|
||||
e.preventDefault()
|
||||
|
||||
}}>
|
||||
<img style={{ width: '20px', height: '20px', cursor: 'pointer' }} />
|
||||
</div>
|
||||
</Dropdown >
|
||||
</Tooltip>
|
||||
}
|
||||
moreIcon={<DoubleRightOutlined style={{ color: "#7b828c" }} />}
|
||||
>
|
||||
{/* {tabsPanes && tabsPanes.map((item: any) =>
|
||||
<TabPane
|
||||
tab={item.title} key={item?.path}
|
||||
style={{ padding: 24, paddingTop: 0 }}>
|
||||
{
|
||||
//统一对所有有效路由做页面鉴权的处理
|
||||
validMenuItem
|
||||
? (
|
||||
<PageAccess>
|
||||
<Outlet />
|
||||
</PageAccess>
|
||||
)
|
||||
: <Page404 />
|
||||
}
|
||||
</TabPane>)
|
||||
} */}
|
||||
</Tabs>
|
||||
|
||||
</Content>
|
||||
</Layout>
|
||||
</ProLayout>
|
||||
</LayoutWrapper >
|
||||
|
||||
// <LayoutWrapper>
|
||||
// <Layout style={{ minHeight: '100vh' }}>
|
||||
// <Header
|
||||
// style={{
|
||||
// padding: '0',
|
||||
// height: 'auto',
|
||||
// }}
|
||||
// >
|
||||
// <Nav />
|
||||
// </Header>
|
||||
// <Layout>
|
||||
// <Sider
|
||||
// collapsible
|
||||
// theme="light"
|
||||
// collapsed={collapsed}
|
||||
// onCollapse={(value) => setCollapsed(value)}
|
||||
// >
|
||||
// <Menu
|
||||
// mode="inline"
|
||||
// openKeys={openKeys}
|
||||
// items={consumableMenu}
|
||||
// onOpenChange={onOpenChange}
|
||||
// selectedKeys={[selectedKeys]}
|
||||
// />
|
||||
// </Sider>
|
||||
// <Layout>
|
||||
// <Content style={{ margin: '0 16px' }}>
|
||||
// {
|
||||
// //统一对所有有效路由做页面鉴权的处理
|
||||
// validMenuItem
|
||||
// ? (
|
||||
// <PageAccess>
|
||||
// <Outlet />
|
||||
// </PageAccess>
|
||||
// )
|
||||
// //这里不再使用<Outlet />是因为当一条路由在菜单接口中没有返回, 但实际项目中创建了,
|
||||
// //此时访问应该显示404页面; 菜单中没有, 实际项目中有表示这条路由/页面当前登录用户无法访问,
|
||||
// //而如果继续使用<Outlet />则那条路由/页面依旧会被渲染, 因为对于umi来说这条路由/页面是有效的,
|
||||
// //是存在的, 而对于本项目来说则不是
|
||||
// : <Page404 />
|
||||
// }
|
||||
// </Content>
|
||||
// </Layout>
|
||||
// </Layout>
|
||||
// </Layout>
|
||||
// </LayoutWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
({ user, global }: { user: UserConnectedProps['user'], global: any }) => ({
|
||||
user,
|
||||
global
|
||||
}),
|
||||
)(BasicLayout);
|
||||
156
src/layouts/old.tsx
Normal file
156
src/layouts/old.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
import type { FC } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Layout, Menu } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Outlet, Link, useLocation, connect } from 'umi';
|
||||
import PageAccess from '@/components/PageAccess';
|
||||
import type { UserConnectedProps } from '@/models/user';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper';
|
||||
import Nav from '@/components/Nav';
|
||||
import Page404 from '@/pages/404';
|
||||
import handleRecursiveNestedData from '@/utils/handleRecursiveNestedData';
|
||||
import handleGetCurrentLocation from '@/utils/handleGetCurrentLocation';
|
||||
|
||||
const { Header, Content, Sider } = Layout;
|
||||
|
||||
/**
|
||||
* 获取openKeys的方法
|
||||
* @param currentLocation 当前位置, 由handleGetCurrentLocation方法返回
|
||||
* @returns openKeys
|
||||
*/
|
||||
const handleGetOpenKeys = (currentLocation: API.MenuItem[] | []): string[] => {
|
||||
//currentLocation为空
|
||||
if (!currentLocation.length) return [''];
|
||||
|
||||
//currentLocation只有一项
|
||||
if (currentLocation.length === 1) {
|
||||
return currentLocation.map((item: API.MenuItem) => `${item.key}`);
|
||||
}
|
||||
|
||||
const res = [];
|
||||
|
||||
//currentLocation有多项, 只要前n-1项
|
||||
for (let i = 0; i < currentLocation.length - 1; i++) {
|
||||
res.push(`${currentLocation[i].key}`);
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
//自定义的layout页面, 顶部导航通栏+侧边栏(菜单)布局, 可根据需要做调整
|
||||
const BasicLayout: FC<UserConnectedProps> = (props) => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [openKeys, setOpenKeys] = useState(['']);
|
||||
const { pathname } = useLocation();
|
||||
const {
|
||||
user: {
|
||||
menu, rootSubmenuKeys, indexAllMenuItemById,
|
||||
indexValidMenuItemByPath,
|
||||
},
|
||||
} = props;
|
||||
const validMenuItem = indexValidMenuItemByPath[pathname];
|
||||
const selectedKeys = validMenuItem?.key;
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
//每次页面重新渲染都要设置openKeys
|
||||
setOpenKeys(
|
||||
handleGetOpenKeys(
|
||||
handleGetCurrentLocation(indexValidMenuItemByPath[pathname], indexAllMenuItemById),
|
||||
),
|
||||
);
|
||||
},
|
||||
[pathname, indexAllMenuItemById, indexValidMenuItemByPath],
|
||||
);
|
||||
|
||||
//Menu中的selectedKeys和openKeys不是一回事:
|
||||
//openKeys:
|
||||
//当前展开的SubMenu菜单项key数组, 有子菜单的父菜单, 当selectedKeys为没子菜单的父菜单时该值应该设为[''],
|
||||
//也就是关闭所有有子菜单的父菜单;
|
||||
//selectedKeys:
|
||||
//当前选中的菜单项key数组, 有子菜单则是子菜单(叶子节点), 没有子菜单则是父菜单(一级菜单), 始终是可选中的
|
||||
|
||||
//点击有子菜单的父菜单的回调
|
||||
const onOpenChange: MenuProps['onOpenChange'] = (keys) => {
|
||||
const latestOpenKey = keys.find((key) => !openKeys.includes(key));
|
||||
|
||||
if (!rootSubmenuKeys.includes(`${latestOpenKey}`)) {
|
||||
setOpenKeys(keys);
|
||||
} else {
|
||||
setOpenKeys(latestOpenKey ? [latestOpenKey] : ['']);
|
||||
}
|
||||
};
|
||||
|
||||
//所有MenuItem:
|
||||
//有children的: 一定都有path, lable不动, children下的label修改为<Link to={path} />
|
||||
//无children的: 有path的label修改为<Link to={path} />, 没path的label不动
|
||||
const consumableMenu = handleRecursiveNestedData(
|
||||
menu,
|
||||
(item: API.MenuItem) => ({
|
||||
...item,
|
||||
label: item.path
|
||||
? (
|
||||
<Link
|
||||
to={item.path}
|
||||
style={{ color: 'inherit' }}
|
||||
>{item.label}</Link>
|
||||
)
|
||||
: item.label,
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Header
|
||||
style={{
|
||||
padding: '0',
|
||||
height: 'auto',
|
||||
}}
|
||||
>
|
||||
<Nav />
|
||||
</Header>
|
||||
<Layout>
|
||||
<Sider
|
||||
collapsible
|
||||
theme="light"
|
||||
collapsed={collapsed}
|
||||
onCollapse={(value) => setCollapsed(value)}
|
||||
>
|
||||
<Menu
|
||||
mode="inline"
|
||||
openKeys={openKeys}
|
||||
items={consumableMenu}
|
||||
onOpenChange={onOpenChange}
|
||||
selectedKeys={[selectedKeys]}
|
||||
/>
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Content style={{ margin: '0 16px' }}>
|
||||
{
|
||||
//统一对所有有效路由做页面鉴权的处理
|
||||
validMenuItem
|
||||
? (
|
||||
<PageAccess>
|
||||
<Outlet />
|
||||
</PageAccess>
|
||||
)
|
||||
//这里不再使用<Outlet />是因为当一条路由在菜单接口中没有返回, 但实际项目中创建了,
|
||||
//此时访问应该显示404页面; 菜单中没有, 实际项目中有表示这条路由/页面当前登录用户无法访问,
|
||||
//而如果继续使用<Outlet />则那条路由/页面依旧会被渲染, 因为对于umi来说这条路由/页面是有效的,
|
||||
//是存在的, 而对于本项目来说则不是
|
||||
: <Page404 />
|
||||
}
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</LayoutWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
({ user }: { user: UserConnectedProps['user'] }) => ({
|
||||
user,
|
||||
}),
|
||||
)(BasicLayout);
|
||||
74
src/models/global.ts
Normal file
74
src/models/global.ts
Normal file
@ -0,0 +1,74 @@
|
||||
// models/profile.ts
|
||||
import { Effect, Reducer } from 'umi';
|
||||
|
||||
export interface ProfileModelState {
|
||||
avatar: string;
|
||||
bio: string;
|
||||
}
|
||||
|
||||
|
||||
export interface ProfileModelType {
|
||||
namespace: 'global';
|
||||
state: any;
|
||||
reducers: {
|
||||
saveTabsRoutes: Reducer<GlobalModelState>;
|
||||
};
|
||||
effects: {
|
||||
changeTabsRoutes: Effect;
|
||||
};
|
||||
}
|
||||
|
||||
export type tabsRoute = {
|
||||
title: string;
|
||||
path: string;
|
||||
key: string;
|
||||
children: any;
|
||||
}
|
||||
|
||||
export type GlobalModelState = {
|
||||
tabsRoutes: tabsRoute[];
|
||||
};
|
||||
|
||||
const ProfileModel: ProfileModelType = {
|
||||
namespace: 'global',
|
||||
state: {
|
||||
tabsRoutes: [],
|
||||
},
|
||||
reducers: {
|
||||
saveTabsRoutes(state = { tabsRoutes: [] }, { payload }): GlobalModelState {
|
||||
return {
|
||||
...state,
|
||||
tabsRoutes: payload
|
||||
}
|
||||
},
|
||||
},
|
||||
effects: {
|
||||
* changeTabsRoutes({ payload, callback }, { put, select }) { // 缓存路由栈
|
||||
const tabsRoutes: tabsRoute[] = yield select((state: ConnectState) => {
|
||||
const { data } = payload
|
||||
if (payload.action === 'add') {
|
||||
const index = state.global.tabsRoutes.findIndex(n => n.path === data.path)
|
||||
if (index === -1) { // 没缓存 则添加
|
||||
return [...state.global.tabsRoutes, { ...data, index: state.global.tabsRoutes.length }]
|
||||
}
|
||||
// 否则不操作
|
||||
return [...state.global.tabsRoutes]
|
||||
}
|
||||
if (payload.action === 'removeAll') {
|
||||
return []
|
||||
}
|
||||
if (callback && typeof callback === 'function') {
|
||||
callback.call()
|
||||
}
|
||||
// 移除单个/多个路由
|
||||
return state.global.tabsRoutes.filter(n => !data.includes(n.path))
|
||||
});
|
||||
yield put({
|
||||
type: 'saveTabsRoutes',
|
||||
payload: tabsRoutes,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default ProfileModel;
|
||||
263
src/models/user.ts
Normal file
263
src/models/user.ts
Normal file
@ -0,0 +1,263 @@
|
||||
import { history } from 'umi';
|
||||
import {
|
||||
userLogin, retrieveUserInfo, retrieveUserInfoAuthorityMenu, userLogout,
|
||||
retrieveUserAuthorityMenu,
|
||||
} from '@/services/user';
|
||||
import type { Effect, Reducer, ConnectProps } from 'umi';
|
||||
import handleRedirect from '@/utils/handleRedirect';
|
||||
import handleGetRootSubmenuKeys from '@/utils/handleGetRootSubmenuKeys';
|
||||
import handleGetEachDatumFromNestedDataByKey from '@/utils/handleGetEachDatumFromNestedDataByKey';
|
||||
import handleGetIndexValidMenuItemByPath from '@/utils/handleGetIndexValidMenuItemByPath';
|
||||
|
||||
/**
|
||||
* 全局用户数据
|
||||
* @description isLogin 是否登录过
|
||||
* @description data 用户信息
|
||||
* @description menu 菜单数据
|
||||
* @description authority 权限数组
|
||||
* @description loginBtnLoading 按钮loading状态
|
||||
* @description rootSubmenuKeys 子菜单的父级菜单key
|
||||
* @description layoutWrapperLoading 布局外层容器loading状态
|
||||
* @description indexAllMenuItemById 通过id索引索引菜单项的映射
|
||||
* @description indexAllMenuItemByPath 通过path索引所有菜单项的映射
|
||||
* @description indexValidMenuItemByPath 通过path索引有效菜单项的映射
|
||||
*/
|
||||
export type UserModelState = {
|
||||
isLogin: boolean;
|
||||
data: API.UserInfo;
|
||||
menu: API.MenuData;
|
||||
authority: string[];
|
||||
loginBtnLoading: boolean;
|
||||
rootSubmenuKeys: React.Key[];
|
||||
layoutWrapperLoading: boolean;
|
||||
indexAllMenuItemById: IndexAllMenuItemByKey<'id'>;
|
||||
indexValidMenuItemByPath: IndexValidMenuItemByPath;
|
||||
indexAllMenuItemByPath: IndexAllMenuItemByKey<'path'>;
|
||||
}
|
||||
|
||||
export type UserConnectedProps = {
|
||||
user: UserModelState;
|
||||
} & ConnectProps;
|
||||
|
||||
type UserModelType = {
|
||||
namespace: 'user';
|
||||
state: UserModelState;
|
||||
effects: {
|
||||
login: Effect;
|
||||
logout: Effect;
|
||||
resetLoginStatus: Effect;
|
||||
getUserInfoAuthorityMenu: Effect;
|
||||
};
|
||||
reducers: {
|
||||
save: Reducer<UserModelState>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求顺序
|
||||
* @description 并发 | 继发
|
||||
*/
|
||||
type ReqOrder = 'concurrent' | 'relay';
|
||||
|
||||
const UserModel: UserModelType = {
|
||||
namespace: 'user',
|
||||
state: {
|
||||
data: {},
|
||||
authority: [],
|
||||
isLogin: false,
|
||||
rootSubmenuKeys: [],
|
||||
loginBtnLoading: false,
|
||||
layoutWrapperLoading: true,
|
||||
menu: [
|
||||
{
|
||||
id: 1,
|
||||
key: '1',
|
||||
path: '/',
|
||||
label: '首页',
|
||||
redirect: '',
|
||||
},
|
||||
],
|
||||
indexAllMenuItemById: {
|
||||
1: {
|
||||
id: 1,
|
||||
key: '1',
|
||||
path: '/',
|
||||
label: '首页',
|
||||
redirect: '',
|
||||
},
|
||||
},
|
||||
indexAllMenuItemByPath: {
|
||||
'/': {
|
||||
id: 1,
|
||||
key: '1',
|
||||
path: '/',
|
||||
label: '首页',
|
||||
redirect: '',
|
||||
},
|
||||
},
|
||||
indexValidMenuItemByPath: {
|
||||
'/': {
|
||||
id: 1,
|
||||
key: '1',
|
||||
path: '/',
|
||||
label: '首页',
|
||||
redirect: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
effects: {
|
||||
//登录
|
||||
*login({ payload }, { call, put }) {
|
||||
|
||||
yield put({
|
||||
type: 'save',
|
||||
payload: {
|
||||
loginBtnLoading: true,
|
||||
},
|
||||
});
|
||||
|
||||
const res: API.LoginResponse = yield call(userLogin, payload);
|
||||
|
||||
//登录成功之后设置token并获取用户信息等数据
|
||||
if (!res.code) {
|
||||
localStorage.setItem('Authorization', res.data.token);
|
||||
|
||||
yield put({
|
||||
type: 'getUserInfoAuthorityMenu',
|
||||
payload: {
|
||||
type: 'concurrent',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
yield put({
|
||||
type: 'save',
|
||||
payload: {
|
||||
loginBtnLoading: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
//获取用户信息和权限以及菜单
|
||||
*getUserInfoAuthorityMenu({ payload }, { call, put }) {
|
||||
const { type }: { type: ReqOrder } = payload;
|
||||
|
||||
let userInfoRes: API.UserInfoResponse = {
|
||||
data: {},
|
||||
code: 0,
|
||||
message: '',
|
||||
};
|
||||
|
||||
let userAuthorityRes: API.UserAuthorityResponse = {
|
||||
data: {
|
||||
authority: [],
|
||||
},
|
||||
code: 0,
|
||||
message: '',
|
||||
};
|
||||
|
||||
let menuRes: API.MenuDataResponse = {
|
||||
data: [],
|
||||
code: 0,
|
||||
message: '',
|
||||
};
|
||||
|
||||
//用户在登录页登录完成之后执行
|
||||
if (type === 'concurrent') {
|
||||
const res: API.UserInfoAuthMenuResponse = yield call(retrieveUserInfoAuthorityMenu);
|
||||
userInfoRes = res[0] as API.UserInfoResponse;
|
||||
userAuthorityRes = res[1] as API.UserAuthorityResponse;
|
||||
menuRes = res[2] as API.MenuDataResponse;
|
||||
} else {
|
||||
//其他情形首先查询用户的登录状态, 未登录则不继续操作
|
||||
try {
|
||||
userInfoRes = yield call(retrieveUserInfo);
|
||||
} catch (error) {
|
||||
//接口报错了, 比如返回了401
|
||||
yield put({
|
||||
type: 'save',
|
||||
payload: {
|
||||
layoutWrapperLoading: false,
|
||||
},
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const res: API.UserAuthMenuResponse = yield call(retrieveUserAuthorityMenu);
|
||||
userAuthorityRes = res[0] as API.UserAuthorityResponse;
|
||||
menuRes = res[1] as API.MenuDataResponse;
|
||||
}
|
||||
|
||||
const indexAllMenuItemByPath = handleGetEachDatumFromNestedDataByKey(menuRes.data, 'path');
|
||||
const indexValidMenuItemByPath = handleGetIndexValidMenuItemByPath(menuRes.data);
|
||||
|
||||
//在登录完获取菜单数据之后做是否需要重定向的操作
|
||||
yield call(
|
||||
handleRedirect,
|
||||
window.location.pathname === '/user/login',
|
||||
indexAllMenuItemByPath,
|
||||
indexValidMenuItemByPath,
|
||||
);
|
||||
|
||||
yield put({
|
||||
type: 'save',
|
||||
payload: {
|
||||
isLogin: true,
|
||||
menu: menuRes.data,
|
||||
data: userInfoRes.data,
|
||||
loginBtnLoading: false,
|
||||
indexAllMenuItemByPath,
|
||||
indexValidMenuItemByPath,
|
||||
layoutWrapperLoading: false,
|
||||
authority: userAuthorityRes.data.authority,
|
||||
rootSubmenuKeys: handleGetRootSubmenuKeys(menuRes.data),
|
||||
indexAllMenuItemById: handleGetEachDatumFromNestedDataByKey(menuRes.data, 'id'),
|
||||
},
|
||||
});
|
||||
|
||||
//为保证所有语句都return, 因此这里加一句这个
|
||||
return true;
|
||||
},
|
||||
//登出
|
||||
*logout({ payload }, { call, put }) {
|
||||
const res: API.LogoutResponse = yield call(userLogout, payload);
|
||||
|
||||
if (!res.code) {
|
||||
yield put({
|
||||
type: 'resetLoginStatus',
|
||||
});
|
||||
}
|
||||
},
|
||||
//重置登录状态
|
||||
*resetLoginStatus(_, { put }) {
|
||||
localStorage.removeItem('Authorization');
|
||||
|
||||
yield put({
|
||||
type: 'save',
|
||||
payload: {
|
||||
isLogin: false,
|
||||
loginBtnLoading: false,
|
||||
},
|
||||
});
|
||||
|
||||
//当前页面不是登录页, 且没有redirect参数的时候再设置redirect参数
|
||||
//而且这个redirect参数要包含url pathname和查询字符串参数, 即pathname及其后面的所有字符
|
||||
if (window.location.pathname !== '/user/login') {
|
||||
if (!/redirect=/.test(window.location.search)) {
|
||||
const redirectValue = `${window.location.pathname}${window.location.search}`;
|
||||
history.push(`/user/login?redirect=${encodeURIComponent(redirectValue)}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
reducers: {
|
||||
save(state, action) {
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default UserModel;
|
||||
24
src/pages/404.tsx
Normal file
24
src/pages/404.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import type { FC } from 'react';
|
||||
import { history } from 'umi';
|
||||
import { Button, Result } from 'antd';
|
||||
|
||||
const _404: FC = () => {
|
||||
|
||||
return (
|
||||
<Result
|
||||
title="404"
|
||||
status="404"
|
||||
subTitle="对不起, 您访问的页面不存在"
|
||||
extra={(
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={
|
||||
() => history.push('/')
|
||||
}
|
||||
>返回首页</Button>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default _404;
|
||||
12
src/pages/about/index.tsx
Normal file
12
src/pages/about/index.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
const Index: FC = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>/about</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
5
src/pages/about/m/authority.ts
Normal file
5
src/pages/about/m/authority.ts
Normal file
@ -0,0 +1,5 @@
|
||||
const authority: Authority = {
|
||||
aboutMUpdate: '/about/m/update',
|
||||
};
|
||||
|
||||
export default authority;
|
||||
24
src/pages/about/m/index.tsx
Normal file
24
src/pages/about/m/index.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import type { FC } from 'react';
|
||||
import Access from '@/components/Access';
|
||||
import authority from './authority';
|
||||
|
||||
const { aboutMUpdate } = authority;
|
||||
|
||||
const Index: FC = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>/about/m</div>
|
||||
<Access
|
||||
authority={aboutMUpdate}
|
||||
fallback={
|
||||
<div>冇权限显示/操作页面内的元素</div>
|
||||
}
|
||||
>
|
||||
<div>有权限显示/操作页面内的元素</div>
|
||||
</Access>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
14
src/pages/about/u/$id.tsx
Normal file
14
src/pages/about/u/$id.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { useParams } from 'umi';
|
||||
|
||||
const Index = () => {
|
||||
|
||||
const params = useParams();
|
||||
|
||||
console.log(params);
|
||||
|
||||
return (
|
||||
<div>{`动态路由: /about/u/${params.id}`}</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
14
src/pages/about/u/index.tsx
Normal file
14
src/pages/about/u/index.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
class Index extends PureComponent {
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<div>/about/u</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Index;
|
||||
14
src/pages/about/um/index.tsx
Normal file
14
src/pages/about/um/index.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
class Index extends PureComponent {
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<div>/about/um</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Index;
|
||||
31
src/pages/authority.ts
Normal file
31
src/pages/authority.ts
Normal file
@ -0,0 +1,31 @@
|
||||
const authority: PageAuthority = {
|
||||
'/': [
|
||||
'/',
|
||||
],
|
||||
'/about/u/1': [
|
||||
'/about/u/1',
|
||||
],
|
||||
'/about/u/2': [
|
||||
'/about/u/2',
|
||||
],
|
||||
'/about/m': [
|
||||
'/about/m',
|
||||
],
|
||||
'/about/um': [
|
||||
'/about/um',
|
||||
],
|
||||
'/teacher/u': [
|
||||
'/ttt',
|
||||
],
|
||||
'/teacher/m': [
|
||||
'/teacher/m',
|
||||
],
|
||||
'/teacher/um': [
|
||||
'/teacher/um',
|
||||
],
|
||||
'/student': [
|
||||
'/student',
|
||||
],
|
||||
};
|
||||
|
||||
export default authority;
|
||||
22
src/pages/index.tsx
Normal file
22
src/pages/index.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { PureComponent } from 'react';
|
||||
import { connect } from 'umi';
|
||||
import type { UserConnectedProps } from '@/models/user';
|
||||
|
||||
class Index extends PureComponent<UserConnectedProps> {
|
||||
|
||||
render() {
|
||||
const { user } = this.props;
|
||||
const { data } = user;
|
||||
const { name } = data;
|
||||
|
||||
return (
|
||||
<div>{name}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
({ user }: { user: UserConnectedProps['user'] }) => ({
|
||||
user,
|
||||
}),
|
||||
)(Index);
|
||||
10
src/pages/teacher/m/index.tsx
Normal file
10
src/pages/teacher/m/index.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
const Index: FC = () => {
|
||||
|
||||
return (
|
||||
<div>/teacher/m</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
10
src/pages/teacher/u/index.tsx
Normal file
10
src/pages/teacher/u/index.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
const Index: FC = () => {
|
||||
|
||||
return (
|
||||
<div>/teacher/u</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
10
src/pages/teacher/um/index.tsx
Normal file
10
src/pages/teacher/um/index.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
const Index: FC = () => {
|
||||
|
||||
return (
|
||||
<div>/teacher/um</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
168
src/pages/user/login/index.less
Normal file
168
src/pages/user/login/index.less
Normal file
@ -0,0 +1,168 @@
|
||||
|
||||
.lang {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
display:flex;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
box-shadow: 0px 3px 9px 0px rgba(204,212,230,0.84);
|
||||
:global(.ant-dropdown-trigger) {
|
||||
margin-right: 24px;
|
||||
}
|
||||
}
|
||||
.link{
|
||||
display:flex;
|
||||
align-items: center;
|
||||
}
|
||||
.headtitle {
|
||||
color: #191919;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 35px;
|
||||
height: 30px;
|
||||
margin-right: 16px;
|
||||
margin-left: 24px;
|
||||
// vertical-align: top;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
// .lang {
|
||||
// width: 100%;
|
||||
// height: 40px;
|
||||
// line-height: 44px;
|
||||
// text-align: right;
|
||||
// :global(.ant-dropdown-trigger) {
|
||||
// margin-right: 24px;
|
||||
// }
|
||||
// }
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 32px 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center 110px;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 32px 0 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 8px;
|
||||
color: rgba(0, 0, 0, 0.2);
|
||||
font-size: 24px;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.footer{
|
||||
width: 100%;
|
||||
.ant-layout-footer{
|
||||
background: #f0f2f5!important;
|
||||
.ant-pro-global-footer{
|
||||
background: #f0f2f5!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.top {
|
||||
padding-left: 12%;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
padding-left: 9%;
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
.loginbg {
|
||||
width: 100%;
|
||||
max-width: 875px;
|
||||
max-height: 507px;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
font-size: 34px;
|
||||
font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 40px;
|
||||
color: #787C86;
|
||||
}
|
||||
|
||||
|
||||
.main {
|
||||
width: 675px;
|
||||
// margin: 0 auto;
|
||||
// margin-right: 10%;
|
||||
background-color: #fff;
|
||||
border: 1px solid #f5f5f5;
|
||||
box-shadow: 0 4px 18px 0 rgba(175,175,175,0.43);
|
||||
|
||||
.title {
|
||||
font-size: 26px;
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
border-bottom: 1px solid #d7d8d9;
|
||||
}
|
||||
.form {
|
||||
padding-top:14%;
|
||||
}
|
||||
.icon {
|
||||
margin-left: 16px;
|
||||
color: rgba(0, 0, 0, 0.2);
|
||||
font-size: 24px;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.prefixIcon {
|
||||
color: #D7D8D9;
|
||||
}
|
||||
.form-bottom {
|
||||
margin: 10% 20% 36px 20%;
|
||||
.divider {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #A7A7A7;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
147
src/pages/user/login/index.tsx
Normal file
147
src/pages/user/login/index.tsx
Normal file
@ -0,0 +1,147 @@
|
||||
import type { FC } from 'react';
|
||||
import { useState, Fragment } from 'react';
|
||||
import { Col, Divider, notification, Row, Space, Tabs, Typography } from 'antd';
|
||||
import { connect, Link } from 'umi';
|
||||
import {
|
||||
AlipayCircleOutlined,
|
||||
LockFilled,
|
||||
LockOutlined,
|
||||
MobileOutlined,
|
||||
TaobaoCircleOutlined,
|
||||
UserOutlined,
|
||||
WeiboCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { ProFormCaptcha, ProFormCheckbox, ProFormText, LoginForm } from '@ant-design/pro-components';
|
||||
import Footer from '@/components/Footer';
|
||||
import { retrieveCaptcha } from '@/services/user';
|
||||
import type { UserConnectedProps } from '@/models/user';
|
||||
import styles from './index.less';
|
||||
import logo from '@/assets/logo.svg'
|
||||
import loginBg from '@/assets/login-bg.png'
|
||||
|
||||
const Index: FC<UserConnectedProps> = (props) => {
|
||||
const [type, setType] = useState<string>('account');
|
||||
const { user, dispatch } = props;
|
||||
|
||||
const handleSubmit = async (values: API.LoginParams) => {
|
||||
dispatch?.({
|
||||
type: 'user/login',
|
||||
payload: values,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.lang}>
|
||||
<Link className={styles.link} to="/">
|
||||
<img alt="logo" className={styles.logo} src={logo} />
|
||||
<span className={styles.headtitle}>驿商云</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
||||
<div className={styles.content}>
|
||||
<Row gutter={16} align="middle" style={{ margin: 0, height: '100%' }}>
|
||||
<Col xl={12} lg={16} md={12}>
|
||||
|
||||
<div className={styles.top}>
|
||||
<div className={styles.header}>
|
||||
|
||||
<div className={styles.title}>云上服务区商业Saas平台</div>
|
||||
|
||||
<div className={styles.desc}>服务区管理一站式解决方案</div>
|
||||
</div>
|
||||
<img src={loginBg} className={styles.loginbg} alt="" />
|
||||
</div>
|
||||
</Col>
|
||||
<Col xl={12} lg={8} md={12}>
|
||||
<div className={styles.main}>
|
||||
<div className={styles.title}>平台账号登录</div>
|
||||
<LoginForm
|
||||
className={styles.form}
|
||||
initialValues={{
|
||||
autoLogin: true,
|
||||
}}
|
||||
submitter={{
|
||||
searchConfig: {
|
||||
submitText: '登录',
|
||||
},
|
||||
submitButtonProps: {
|
||||
size: 'large',
|
||||
// danger: true,
|
||||
style: {
|
||||
width: '100%',
|
||||
// background: '#E2364B',
|
||||
marginTop: 0
|
||||
},
|
||||
},
|
||||
}}
|
||||
onFinish={(values) => {
|
||||
handleSubmit({
|
||||
...values,
|
||||
})
|
||||
// LoginIP: baseInfo?.ip ? baseInfo?.ip : '',
|
||||
// LoginPlace: `${baseInfo?.prov ? baseInfo?.prov : ''}${baseInfo?.prov && baseInfo?.city ? '-' : ''}${baseInfo?.city ? baseInfo?.city : ''}`,
|
||||
// BrowserVersion: browser || '',
|
||||
// OperatingSystem: systemInfo || '',
|
||||
return Promise.resolve();
|
||||
}}
|
||||
>
|
||||
<ProFormText
|
||||
name="UserPassport"
|
||||
|
||||
fieldProps={{
|
||||
size: 'large',
|
||||
prefix: <UserOutlined className={styles.prefixIcon} />,
|
||||
}}
|
||||
placeholder='账号'
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入您的账号',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ProFormText.Password
|
||||
name="UserPassWord"
|
||||
fieldProps={{
|
||||
size: 'large',
|
||||
prefix: <LockFilled className={styles.prefixIcon} />,
|
||||
}}
|
||||
placeholder='密码'
|
||||
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入您的密码',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</LoginForm>
|
||||
<div style={{ textAlign: 'center', width: 328, margin: 'auto' }}>
|
||||
<Space split>
|
||||
<Link to="/user/register">商户注册</Link> | <Link to="/user/forgetPassword"><Typography.Text type="secondary" >忘记密码?</Typography.Text></Link>
|
||||
</Space>
|
||||
</div>
|
||||
<div className={styles['form-bottom']}>
|
||||
<Divider className={styles.divider}>温馨提示</Divider>
|
||||
<p>
|
||||
我们为您提供驿商云帐号服务,在登录过程中会使用到您的帐号和网络信息提 升登录体验。点击“登录”表示您同意以上内容。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
({ user }: { user: UserConnectedProps['user'] }) => ({
|
||||
user,
|
||||
}),
|
||||
)(Index);
|
||||
48
src/services/user.ts
Normal file
48
src/services/user.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
//登录
|
||||
export const userLogin = (params: Record<string, unknown>): Promise<API.LoginResponse> => (
|
||||
request.post('/api/user/login', params)
|
||||
);
|
||||
|
||||
//获取用户信息
|
||||
export const retrieveUserInfo = (): Promise<API.UserInfoResponse> => (
|
||||
request.get('/api/user/info')
|
||||
);
|
||||
|
||||
//获取用户权限
|
||||
export const retrieveUserAuthority = (): Promise<API.UserAuthorityResponse> => (
|
||||
request.get('/api/user/authority')
|
||||
);
|
||||
|
||||
//获取菜单数据
|
||||
export const retrieveMenuData = (): Promise<API.MenuDataResponse> => (
|
||||
request.get('/api/user/menu')
|
||||
);
|
||||
|
||||
//获取用户信息和权限以及菜单
|
||||
export const retrieveUserInfoAuthorityMenu = (): Promise<API.UserInfoAuthMenuResponse> => (
|
||||
Promise.all([
|
||||
retrieveUserInfo(),
|
||||
retrieveUserAuthority(),
|
||||
retrieveMenuData(),
|
||||
])
|
||||
);
|
||||
|
||||
//获取用户权限以及菜单
|
||||
export const retrieveUserAuthorityMenu = (): Promise<API.UserAuthMenuResponse> => (
|
||||
Promise.all([
|
||||
retrieveUserAuthority(),
|
||||
retrieveMenuData(),
|
||||
])
|
||||
);
|
||||
|
||||
//登出
|
||||
export const userLogout = (): Promise<API.LogoutResponse> => (
|
||||
request.post('/api/user/logout')
|
||||
);
|
||||
|
||||
//获取验证码
|
||||
export const retrieveCaptcha = (params: Record<string, string>): Promise<API.CaptchaResponse> => (
|
||||
request.get('/api/user/captcha', { params })
|
||||
);
|
||||
34
src/utils/handleGetCurrentLocation.ts
Normal file
34
src/utils/handleGetCurrentLocation.ts
Normal file
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 获取当前位置信息的方法
|
||||
* @description 该方法获得的返回值是从第一个菜单到最后一个菜单, 如果是有n级菜单,
|
||||
* 那么只有最后一个可被选中, 其余都不可被选中, 都是展开的菜单
|
||||
* @param currentMenuItem 当前菜单项, 当url中的pathname不在菜单中, 则该值为undefined
|
||||
* @param indexAllMenuItemById 通过id索引菜单项的映射
|
||||
* @returns API.MenuItem[] | []
|
||||
*/
|
||||
const handleGetCurrentLocation = (
|
||||
currentMenuItem: API.MenuItem | undefined,
|
||||
indexAllMenuItemById: IndexAllMenuItemByKey<'id'>,
|
||||
): API.MenuItem[] | [] => {
|
||||
let res: API.MenuItem[] = [];
|
||||
|
||||
if (!currentMenuItem) return res;
|
||||
|
||||
res.push({
|
||||
id: currentMenuItem.id,
|
||||
key: currentMenuItem.key,
|
||||
path: currentMenuItem.path,
|
||||
label: currentMenuItem.label,
|
||||
redirect: currentMenuItem.redirect,
|
||||
});
|
||||
|
||||
if (currentMenuItem.pid) {
|
||||
res = [
|
||||
...res,
|
||||
...handleGetCurrentLocation(indexAllMenuItemById[currentMenuItem.pid], indexAllMenuItemById),
|
||||
];
|
||||
}
|
||||
return res.reverse();
|
||||
};
|
||||
|
||||
export default handleGetCurrentLocation;
|
||||
32
src/utils/handleGetEachDatumFromNestedDataByKey.ts
Normal file
32
src/utils/handleGetEachDatumFromNestedDataByKey.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 获取 从嵌套数据中通过key索引每一条数据的映射 的方法
|
||||
* @param data 数据
|
||||
* @param key 数据的key
|
||||
* @returns 通过key索引每一条数据的映射
|
||||
*/
|
||||
const handleGetEachDatumFromNestedDataByKey = <T extends { children?: T[] }>(
|
||||
data: T[],
|
||||
key: keyof T, //确定key的类型为T的键名
|
||||
): Record<string, T> => {
|
||||
//显式声明byKey的类型
|
||||
let byKey: Record<string, T> = {};
|
||||
|
||||
data.forEach((datum) => {
|
||||
//添加类型检查, 避免undefined作为对象的键名
|
||||
if (typeof datum[key] !== 'undefined') {
|
||||
//强制转换为字符串类型, 避免非字符串类型作为对象的键名
|
||||
byKey[String(datum[key])] = datum;
|
||||
}
|
||||
|
||||
if (datum.children) {
|
||||
byKey = {
|
||||
...byKey,
|
||||
...handleGetEachDatumFromNestedDataByKey(datum.children, key),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return byKey;
|
||||
};
|
||||
|
||||
export default handleGetEachDatumFromNestedDataByKey;
|
||||
25
src/utils/handleGetIndexValidMenuItemByPath.ts
Normal file
25
src/utils/handleGetIndexValidMenuItemByPath.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 获取 通过path索引菜单项的映射 的方法
|
||||
* @param menu 菜单数据
|
||||
* @returns 通过path索引菜单项的映射
|
||||
*/
|
||||
const handleGetIndexValidMenuItemByPath = (
|
||||
menu: API.MenuData,
|
||||
): IndexValidMenuItemByPath => {
|
||||
let byPath: IndexValidMenuItemByPath = {};
|
||||
|
||||
menu.forEach((item: API.MenuItem) => {
|
||||
if (!item.children) {
|
||||
byPath[item.path] = item;
|
||||
} else {
|
||||
byPath = {
|
||||
...byPath,
|
||||
...handleGetIndexValidMenuItemByPath(item.children),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return byPath;
|
||||
};
|
||||
|
||||
export default handleGetIndexValidMenuItemByPath;
|
||||
18
src/utils/handleGetRootSubmenuKeys.ts
Normal file
18
src/utils/handleGetRootSubmenuKeys.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 获取子菜单的父级菜单keys的方法
|
||||
* @param menu 菜单数据
|
||||
* @returns 父级菜单key数组
|
||||
*/
|
||||
const handleGetRootSubmenuKeys = (menu: API.MenuData): React.Key[] => {
|
||||
const keys: React.Key[] = [];
|
||||
|
||||
menu.forEach((item: API.MenuItem) => {
|
||||
if (item.children) {
|
||||
keys.push(item.key);
|
||||
}
|
||||
});
|
||||
|
||||
return keys;
|
||||
};
|
||||
|
||||
export default handleGetRootSubmenuKeys;
|
||||
32
src/utils/handleRecursiveNestedData.ts
Normal file
32
src/utils/handleRecursiveNestedData.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 递归处理嵌套数据的方法
|
||||
* @param data 数据
|
||||
* @param datumCb 对单条数据进行操作的回调
|
||||
* @returns 处理过的数据
|
||||
*/
|
||||
const handleRecursiveNestedData = (
|
||||
data: API.MenuItem[],
|
||||
datumCb: (datum: API.MenuItem) => API.MenuItem,
|
||||
): API.MenuItem[] => {
|
||||
|
||||
const res = [] as API.MenuItem[];
|
||||
|
||||
for (const datum of data) {
|
||||
if (datum.hideInMenu) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (datum.children) {
|
||||
res.push({
|
||||
...datum,
|
||||
children: handleRecursiveNestedData(datum.children, datumCb),
|
||||
});
|
||||
} else {
|
||||
res.push(datumCb(datum));
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export default handleRecursiveNestedData;
|
||||
69
src/utils/handleRedirect.ts
Normal file
69
src/utils/handleRedirect.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { history } from 'umi';
|
||||
|
||||
/**
|
||||
* 重定向的方法
|
||||
* @description 如果是登录页, url上有redirect参数则跳转到redirect参数, 同时要考虑redirect的有效性,
|
||||
* 没有就跳第一个有效路由; 非登录页, 看该页面的路由是否有效, 有效就跳转,
|
||||
* 无效就看是否有子路由, 有就跳它的第一个子路由, 也就是看这个路由是不是有子路由, 是在Menu中可展开的形式,
|
||||
* 没有也跳, 此时就交由umi处理404的情况
|
||||
* @param isLoginPage 是否是登录页
|
||||
* @param indexAllMenuItemByPath IndexAllMenuItemByKey<'path'>
|
||||
* @param indexValidMenuItemByPath IndexValidMenuItemByPath
|
||||
* @returns Promise<boolean>
|
||||
*/
|
||||
const handleRedirect = (
|
||||
isLoginPage: boolean,
|
||||
indexAllMenuItemByPath: IndexAllMenuItemByKey<'path'>,
|
||||
indexValidMenuItemByPath: IndexValidMenuItemByPath,
|
||||
): Promise<boolean> => (
|
||||
new Promise((resolve) => {
|
||||
let routePath = Object.keys(indexValidMenuItemByPath)[0];
|
||||
|
||||
if (isLoginPage) {
|
||||
const queryString = window.location.search;
|
||||
|
||||
if (queryString) {
|
||||
const matchedRes = queryString.match(/redirect=(.*)/);
|
||||
|
||||
if (matchedRes) {
|
||||
//还要考虑redirect参数是否有效
|
||||
const decodeRedirect = decodeURIComponent(matchedRes[1]);
|
||||
//有效: 跳转
|
||||
if (indexValidMenuItemByPath[decodeRedirect]) {
|
||||
routePath = decodeRedirect;
|
||||
} else if (indexAllMenuItemByPath[decodeRedirect]) {
|
||||
//无效
|
||||
//有子路由: 跳子路由
|
||||
routePath = indexAllMenuItemByPath[decodeRedirect].redirect;
|
||||
} else {
|
||||
//无子路由: 还是要跳, 此时就是交由umi处理404的情况了
|
||||
routePath = decodeRedirect;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const {
|
||||
location: { search, pathname },
|
||||
} = window;
|
||||
|
||||
//考虑url上有查询字符串参数的情况
|
||||
//有效
|
||||
if (indexValidMenuItemByPath[pathname]) {
|
||||
routePath = `${pathname}${search}`;
|
||||
} else if (indexAllMenuItemByPath[pathname]) {
|
||||
//无效
|
||||
//有子路由: 跳子路由
|
||||
routePath = `${indexAllMenuItemByPath[pathname].redirect}${search}`;
|
||||
} else {
|
||||
//无子路由: 也跳, 此时交由umi处理404的情况
|
||||
routePath = `${pathname}${search}`;
|
||||
}
|
||||
}
|
||||
|
||||
history.push(routePath);
|
||||
|
||||
return resolve(true);
|
||||
})
|
||||
);
|
||||
|
||||
export default handleRedirect;
|
||||
73
src/utils/request.ts
Normal file
73
src/utils/request.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import axios from 'axios';
|
||||
import { getDvaApp } from 'umi';
|
||||
import { notification } from 'antd';
|
||||
import type { AxiosRequestHeaders } from 'axios/index';
|
||||
|
||||
const { UMI_APP_BASEURL } = process.env;
|
||||
|
||||
const instance = axios.create({ baseURL: 'https://api.eshangtech.com/EShangApiMain' });
|
||||
|
||||
instance.interceptors.request.use(
|
||||
(config) => {
|
||||
config.headers = {
|
||||
...config.headers,
|
||||
Authorization: localStorage.getItem('Authorization') || '',
|
||||
} as AxiosRequestHeaders;
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error),
|
||||
);
|
||||
|
||||
instance.interceptors.response.use(
|
||||
//状态码为2xx的时候执行
|
||||
(response) => {
|
||||
const { data } = response;
|
||||
const { code } = data;
|
||||
|
||||
if (code) {
|
||||
notification.error({
|
||||
message: data.message,
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
//状态码不为2xx的时候执行
|
||||
(error) => {
|
||||
const { response } = error;
|
||||
const { status } = response;
|
||||
|
||||
enum CodeMessage {
|
||||
'发出的请求有错误,服务器没有进行新建或修改数据的操作。' = 400,
|
||||
'用户未登录。' = 401,
|
||||
'用户得到授权,但是访问是被禁止的。' = 403,
|
||||
'发出的请求针对的是不存在的记录,服务器没有进行操作。' = 404,
|
||||
'请求的格式不可得。' = 406,
|
||||
'请求的资源被永久删除,且不会再得到的。' = 410,
|
||||
'当创建一个对象时,发生一个验证错误。' = 422,
|
||||
'服务器发生错误,请检查服务器。' = 500,
|
||||
'网关错误。' = 502,
|
||||
'服务不可用,服务器暂时过载或维护。' = 503,
|
||||
'网关超时。' = 504
|
||||
}
|
||||
|
||||
notification.error({
|
||||
message: response.data.message || CodeMessage[status],
|
||||
});
|
||||
|
||||
if (status === 401) {
|
||||
const {
|
||||
_store: { dispatch },
|
||||
} = getDvaApp();
|
||||
|
||||
dispatch({
|
||||
type: 'user/resetLoginStatus',
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
export default instance;
|
||||
39
tsconfig.json
Normal file
39
tsconfig.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"extends": "./src/.umi/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "build/dist",
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"lib": ["esnext", "dom"],
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"jsx": "react-jsx",
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "node",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": [
|
||||
"mock/**/*",
|
||||
"src/**/*",
|
||||
"playwright.config.ts",
|
||||
"tests/**/*",
|
||||
"test/**/*",
|
||||
"__test__/**/*",
|
||||
"typings/**/*",
|
||||
"config/**/*",
|
||||
".eslintrc.js",
|
||||
".stylelintrc.js",
|
||||
".prettierrc.js",
|
||||
"jest.config.js",
|
||||
"mock/*",
|
||||
"typings.d.ts"
|
||||
],
|
||||
"exclude": ["node_modules", "build", "dist", "scripts", "src/.umi/*", "webpack", "jest"]
|
||||
}
|
||||
162
typings.d.ts
vendored
Normal file
162
typings.d.ts
vendored
Normal file
@ -0,0 +1,162 @@
|
||||
// import 'umi/typings';
|
||||
/// reference path="./node_modules/@types/react/index.d.ts" />
|
||||
|
||||
declare module '*.less';
|
||||
|
||||
/**
|
||||
* 页面权限类型
|
||||
* @description key是路由path, value是权限数组
|
||||
*/
|
||||
type PageAuthority = {
|
||||
[path: string]: string[];
|
||||
}
|
||||
/**
|
||||
* 权限类型
|
||||
* @description key是权限名称, value是具体的权限字符串
|
||||
*/
|
||||
type Authority = {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过path索引有效菜单项的映射(也就是可以被选中, 可以更改url pathname的菜单项)
|
||||
* @description key是path, value是API.MenuItem
|
||||
*/
|
||||
type IndexValidMenuItemByPath = {
|
||||
[path: string]: API.MenuItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过API.MenuItem的key索引所有菜单项的映射(包括不可以被选中, 不可以更改url pathname的菜单项)
|
||||
* @description key是API.MenuItem的key, value是API.MenuItem
|
||||
*/
|
||||
type IndexAllMenuItemByKey<T extends keyof API.MenuItem> = T extends keyof API.MenuItem
|
||||
? {
|
||||
[key in API.MenuItem[T]]: API.MenuItem
|
||||
}
|
||||
: never;
|
||||
|
||||
declare namespace API {
|
||||
/**
|
||||
* 响应体
|
||||
* @description data 数据
|
||||
* @description code 返回码: 0 成功, 其他 失败
|
||||
* @description message 消息: 返回的消息
|
||||
*/
|
||||
type ResponstBody<T> = {
|
||||
data: T;
|
||||
code: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 登录数据
|
||||
* @description token token票据
|
||||
*/
|
||||
type LoginData = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
/** 登录响应结果 */
|
||||
type LoginResponse = ResponstBody<LoginData>;
|
||||
|
||||
/** 用户信息数据 */
|
||||
type UserInfo = {
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
userid?: string;
|
||||
email?: string;
|
||||
signature?: string;
|
||||
title?: string;
|
||||
group?: string;
|
||||
tags?: { key?: string; label?: string }[];
|
||||
notifyCount?: number;
|
||||
unreadCount?: number;
|
||||
country?: string;
|
||||
access?: string;
|
||||
geographic?: {
|
||||
province?: { label?: string; key?: string };
|
||||
city?: { label?: string; key?: string };
|
||||
};
|
||||
address?: string;
|
||||
phone?: string;
|
||||
};
|
||||
|
||||
/** 用户信息响应结果 */
|
||||
type UserInfoResponse = ResponstBody<UserInfo>;
|
||||
|
||||
/**
|
||||
* 用户权限
|
||||
* @description authority 权限数组(包含页面和页面内元素的权限)
|
||||
*/
|
||||
type UserAuthorityData = {
|
||||
authority: string[];
|
||||
}
|
||||
|
||||
/** 用户权限响应结果 */
|
||||
type UserAuthorityResponse = ResponstBody<UserAuthorityData>;
|
||||
|
||||
/**
|
||||
* 菜单项
|
||||
* @description id 数据库中数据的id
|
||||
* @description pid 数据库中数据的id(父级的id)
|
||||
* @description key 菜单项的唯一标志, 使用string类型代替React.Key:
|
||||
* https://ant.design/components/menu-cn#itemtype, 不然会出现key类型不对导致的菜单项无法被选中的问题
|
||||
* @description lable 菜单的标题
|
||||
* @description hideInMenu 在菜单中隐藏
|
||||
* @description path 路由路径,
|
||||
* 有无children的菜单都会有这个字段, 无children的菜单跳转这个值, 有children的跳redirect,
|
||||
* 因为有children表示这个菜单是可展开的, 此时有children的path只是表示它的一个位置, 而非真正有效的路由
|
||||
* @description redirect 重定向路由路径,
|
||||
* 只有有children的菜单有, 当这个菜单的children中有可选中的菜单时, 这个值为第一个可选中的菜单的path,
|
||||
* 当这个菜单的children中没可以选中的菜单, 而是还有children时,
|
||||
* 该值就是它children中的children的第一个可选中的菜单的path,
|
||||
* 就是无论如何, 这个值都是第一个有效路由, 具体可看mock数据中的菜单数据
|
||||
* 以及这个字段理论上来说应该是可选的字段, 但为了让后端容易处理, 这里写成固定有的字段,
|
||||
* 在不需要这个字段的数据中后端返回空串即可
|
||||
* @description children 子菜单
|
||||
*/
|
||||
type MenuItem = {
|
||||
id: number;
|
||||
pid?: number;
|
||||
key: string;
|
||||
path: string;
|
||||
icon?: string;
|
||||
redirect: string;
|
||||
hideInMenu?: boolean;
|
||||
label: React.ReactElement | string;
|
||||
name: React.ReactElement | string;
|
||||
children?: MenuItem[];
|
||||
}
|
||||
|
||||
/** 菜单数据 */
|
||||
type MenuData = MenuItem[];
|
||||
|
||||
/** 菜单数据响应结果 */
|
||||
type MenuDataResponse = ResponstBody<MenuData>;
|
||||
|
||||
/** 用户信息权限菜单响应结果 */
|
||||
type UserInfoAuthMenuResponse = (UserInfoResponse | UserAuthorityResponse | MenuDataResponse)[];
|
||||
|
||||
/** 用户权限菜单响应结果 */
|
||||
type UserAuthMenuResponse = (UserAuthorityResponse | MenuDataResponse)[];
|
||||
|
||||
/** 登出响应结果 */
|
||||
type LogoutResponse = ResponstBody<Record<string, never>>;
|
||||
|
||||
/** 登录参数 */
|
||||
type LoginParams = {
|
||||
username?: string;
|
||||
password?: string;
|
||||
autoLogin?: boolean;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
/** 验证码数据 */
|
||||
type CaptchaData = {
|
||||
captcha: string;
|
||||
}
|
||||
|
||||
/** 验证码响应结果 */
|
||||
type CaptchaResponse = ResponstBody<CaptchaData>;
|
||||
}
|
||||
BIN
umi4-admin-main.zip
Normal file
BIN
umi4-admin-main.zip
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user