antd_pro_shop_admin项目笔记

本文最后更新于:2 年前

一、项目搭建

1.1 安装脚手架

1
yarn create umi

image.png

1.2 选择版本

image.png

1.3 安装依赖

1
yarn

1.4 启动项目

1
npm start

image.png

1.5 点击链接进入浏览器

image.png

二、初始化项目

项目接口文档

https://www.showdoc.com.cn/1207745568269674?page_id=6094279351627422

2.1 删掉多余的文件

在编译器中打开项目
删掉\src\pages 中TableList文件夹,Admin.jsxWelcome.jsxWelcome.less文件
删掉\mock 中listTableList.js文件
删掉\config\routes.js 文件夹中,删掉对应不用的路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
export default [
{
path: '/',
component: '../layouts/BlankLayout',
routes: [
{
path: '/user',
component: '../layouts/UserLayout',
routes: [
{
name: 'login',
path: '/user/login',
component: './User/login',
},
],
},
{
path: '/',
component: '../layouts/SecurityLayout',
routes: [
{
path: '/',
component: '../layouts/BasicLayout',
routes: [
{
path: '/',
},
{
component: './404',
},
],
},
{
component: './404',
},
],
},
],
},
{
component: './404',
},
]

2.2 更改 Logo 和底部文字替换

在\src\layouts\BasicLayout.jsx 文件中找到defaultFooterDom更改默认文字,将links={null}设置为空,不能删掉links={}否则会编程默认的样子

1
2
3
const defaultFooterDom = (
<DefaultFooter copyright={`${new Date().getFullYear()} 融职商城`} links={null} />
)

底部文字就更改好了
image.png
在\Econfig\defaultSettings.js 中更改title,这里的title是更改的网页标题和左上角文字
image.png
在\src\assets 文件夹中提换掉 logo,并在用到的地方重新导入 logo 文件,否则会报错

打开控制台,到网络请求,选择所有请求,快速刷新页面会发现这个图标
image.png
将自己的 logo.png 文件复制到\public 文件下
然后在\src\pages\document.ejs 中更改自己的 logo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<div
style="
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 420px;
">
<img src="<%= context.config.publicPath +'logo.png'%>" alt="logo" width="256" />
<div class="page-loading-warp">
<div class="ant-spin ant-spin-lg ant-spin-spinning">
<span class="ant-spin-dot ant-spin-dot-spin">
<i class="ant-spin-dot-item"></i>
<i class="ant-spin-dot-item"></i>
<i class="ant-spin-dot-item"></i>
<i class="ant-spin-dot-item"></i>
</span>
</div>
</div>
<div style="display: flex; align-items: center; justify-content: center">
<img src="<%= context.config.publicPath +'logo.png'%>" width="32" style="margin-right: 8px" />
融职商城
</div>
</div>

2.4 更改网页标题的 ico

2.4.1 将图片格式改成 ioc

  1. 第一步我们用画图的方式打开原始图片,可以看到这里是一张 png 格式的原始图片,如下图所示:
  2. image.png
  3. 第二步点击画图中文件图标,选择“另存为->BMP 图片”
  4. 第四步我们将 BMP 格式的后缀名改为 ico 格式的,弹出框之后,点击确定,可以看到已经成功更改为 ico 格式的图片,需要注意的是有时候直接改后缀名会出现图片默认是白色,点击进去时正常的,不影响程序使用

2.4.2 更改网页默认的 ico

在\public 中将自己的 ico 提换掉 favicon.ico

2.5 删除登录页多余的东西

在\src\pages\User\login\index.jsx 文件中精简代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import { LockOutlined, UserOutlined } from '@ant-design/icons'
import { Alert, Tabs } from 'antd'
import React, { useState } from 'react'
import ProForm, { ProFormText } from '@ant-design/pro-form'
import { useIntl, connect, FormattedMessage } from 'umi'
import styles from './index.less'

const LoginMessage = ({ content }) => (
<Alert
style={{
marginBottom: 24,
}}
message={content}
type="error"
showIcon
/>
)

const Login = props => {
const { userLogin = {}, submitting } = props
const { status, type: loginType } = userLogin
const [type, setType] = useState('account')
const intl = useIntl()

const handleSubmit = values => {
const { dispatch } = props
dispatch({
type: 'login/login',
payload: { ...values, type },
})
}

return (
<div className={styles.main}>
<ProForm
initialValues={{
autoLogin: true,
}}
submitter={{
render: (_, dom) => dom.pop(),
submitButtonProps: {
loading: submitting,
size: 'large',
style: {
width: '100%',
},
},
}}
onFinish={values => {
handleSubmit(values)
return Promise.resolve()
}}>
<Tabs activeKey={type} onChange={setType}>
<Tabs.TabPane
key="account"
tab={intl.formatMessage({
id: 'pages.login.accountLogin.tab',
defaultMessage: 'Account password login',
})}
/>
</Tabs>

{status === 'error' && !submitting && (
<LoginMessage
content={intl.formatMessage({
id: 'pages.login.accountLogin.errorMessage',
defaultMessage: 'Incorrect account or password(admin/ant.design)',
})}
/>
)}
<ProFormText
name="userName"
fieldProps={{
size: 'large',
prefix: <UserOutlined className={styles.prefixIcon} />,
}}
placeholder={intl.formatMessage({
id: 'pages.login.username.placeholder',
defaultMessage: 'Username: admin or user',
})}
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.login.username.required"
defaultMessage="Please enter user name!"
/>
),
},
]}
/>
<ProFormText.Password
name="password"
fieldProps={{
size: 'large',
prefix: <LockOutlined className={styles.prefixIcon} />,
}}
placeholder={intl.formatMessage({
id: 'pages.login.password.placeholder',
defaultMessage: 'Password: ant.design',
})}
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.login.password.required"
defaultMessage="Please enter password!"
/>
),
},
]}
/>
<div
style={{
marginBottom: 24,
}}></div>
</ProForm>
</div>
)
}

export default connect(({ login, loading }) => ({
userLogin: login,
submitting: loading.effects['login/login'],
}))(Login)

在\src\layouts\UserLayout.jsx 中将默认的 logo 替换成自己的 logo

1
import logo from '../assets/logo.png'

在 UserLayout.jsx 文件删除标签FormattedMessagedefaultMessage=“”

1
2
3
<div className={styles.desc}>
<FormattedMessage id="pages.layouts.userLayout.title" />
</div>

在国际化\src\locales\zh-CN\pages.js 文件中修改 pages.layouts.userLayout.title 的默认文字

1
'pages.layouts.userLayout.title': '融职商城后台管理系统',

2.6 删除首页头部多余东西

在\src\components\GlobalHeader\RightContent.jsx 文件中删除搜索组件HeaderSearch和文档组件Tooltip

2.7 优化登录页

2.7.1 优化登录页文件

将登录页移到 pages 下删除 User 文件夹,注意非必要不要随意更改 pages 下的文件夹,因为改动文件夹要配置对应路由
image.png

2.7.2 配置登录页路由

在\config\routes.js 将原来 user 的路由修改成 login 的路由

1
2
3
4
5
6
7
8
9
10
11
{
path: '/login',
component: '../layouts/LoginLayout',
routes: [
{
name: 'login',
path: '/login',
component: './Login',
},
],
},

同时修改\src\layouts 文件下 UserLayout.jsx less 文件重命名为 LginLayout 的
以及修改 UserLayout.jsx 中的样式导入import styles from './LoginLayout.less';
在\src\layouts\SecurityLayout.jsx 中更改重定向的登录路由

1
2
3
if (!isLogin && window.location.pathname !== '/login') {
return <Redirect to={`/login?${queryString}`} />
}

2.8 封装网络请求

2.8.1 添加请求拦截器

具体如何找请求拦截器 1.先进入umijs找到插件选择plugin-request进去找到requestimage.png 2.点击参考文档地址找到Interceptor
image.png 3.添加请求头
在\src\utils\request.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const request = extend({
errorHandler,
// default error handling
credentials: 'include', // Does the default request bring cookies
})

// 请求拦截器,在请求之前添加Header头
request.interceptors.request.use((url, options) => {
// 获取token
const token = 'hello'
// 设置Header头
const headers = {
Authorization: `Bearer ${token}`,
}
return {
url,
options: { ...options, headers },
}
})

export default request

2.8.2 封装错误信息提示

1.重新启动项目

通过**yarn dev**启动项目会关闭 mock,之后就能添加自己的 api

1
yarn dev

image.png
在\package.json 可以查到相关配置

1
"start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev umi dev",

2.更改代理

在\config\proxy.js 中将 dev 的域名改成自己的

1
2
3
4
5
6
7
8
9
dev: {
'/api/': {
target: 'https://api.shop.eduwork.cn/',
changeOrigin: true,
pathRewrite: {
'^': '',
},
},
},

在\src\services\user.js 中将接口请求改成request.post('/api/admin/user')

1
2
3
4
export async function queryCurrent() {
// return request('/api/currentUser');
return request('/api/admin/user')
}

3.修改错误提示

在\src\utils\request.js 中,通过async await替换promise完成异步请求
async/await 场景:这是一个用同步的思维来解决异步问题的方案,当前端接口调用需要等到接口返回值以后渲染页面时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const errorHandler = async error => {
const { response } = error

if (response && response.status) {
let errorText = codeMessage[response.status] || response.statusText
const { status } = response
const result = await response.json()

// 处理422未验证通过的情况
if (status === 422) {
let errs = ''
for (const key in result.errors) {
errs += result.errors[key][0]
}
errorText += `[ ${errs} ]`
}
// 处理400的情况
if (status === 400) {
errorText += `[ ${result.message} ]`
}
message.error(errorText)
} else if (!response) {
message.error('网络发生异常,无法连接服务器')
}

return response
}

4.简化接口前缀(初始化项目可不设置)

在\src\utils\request.js 中的 request 函数添加prefix: '/api',则可以自动添加前缀简化接口写法
image.png
在\src\services\user.js 中,前缀则可以少写/api

1
2
3
4
export async function queryCurrent() {
// return request('/api/currentUser');
return request.post('/auth/login')
}

三、登录/退出功能

3.1 登录基本设置

在 src\pages\Login\index.jsx 登录页简化,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import { LockOutlined, UserOutlined } from '@ant-design/icons'
import { Tabs } from 'antd'
import React from 'react'
import ProForm, { ProFormText } from '@ant-design/pro-form'
import { connect } from 'umi'
import styles from './index.less'

const Login = props => {
const { submitting } = props

const handleSubmit = values => {
const { dispatch } = props
dispatch({
type: 'login/login',
payload: { ...values },
})
}

return (
<div className={styles.main}>
<ProForm
initialValues={{
autoLogin: true,
}}
submitter={{
render: (_, dom) => dom.pop(),
submitButtonProps: {
loading: submitting,
size: 'large',
style: {
width: '100%',
},
},
}}
onFinish={values => {
handleSubmit(values)
return Promise.resolve()
}}>
<Tabs activeKey="account">
<Tabs.TabPane key="account" tab="账号密码登录" />
</Tabs>

<ProFormText
name="email"
fieldProps={{
size: 'large',
prefix: <UserOutlined className={styles.prefixIcon} />,
}}
placeholder="邮箱:super@a.com"
rules={[
{
required: true,
message: '请输入邮箱',
},
{
type: 'email',
message: '请输入正确的邮箱格式',
},
]}
/>
<ProFormText.Password
name="password"
fieldProps={{
size: 'large',
prefix: <LockOutlined className={styles.prefixIcon} />,
}}
placeholder="密码:123123"
rules={[
{
required: true,
message: '请输入密码',
},
]}
/>
<div
style={{
marginBottom: 24,
}}></div>
</ProForm>
</div>
)
}

export default connect(({ login, loading }) => ({
userLogin: login,
submitting: loading.effects['login/login'],
}))(Login)

3.1.1 用户登录接口文档

接口描述
  • 用户登录接口
请求 URL
  • /api/auth/login
请求方式
  • POST
Body 请求参数
参数名 必选 类型 说明
email string 邮箱
password string 密码
返回参数
参数名 必含 类型 说明
access_token string token
token_type string token 类型
expires_in int 过期时间
返回示例
  • 状态码 200 请求成功
1
2
3
4
5
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9hcGkudGVzdFwvYXBpXC9hdXRoXC9sb2dpbiIsImlhdCI6MTYwNzUyMDE0MSwiZXhwIjoxNjA3NTIzNzQxLCJuYmYiOjE2MDc1MjAxNDEsImp0aSI6IktVdWFsTmxnOXYzZmlTZHEiLCJzdWIiOjMsInBydiI6IjIzYmQ1Yzg5NDlmNjAwYWRiMzllNzAxYzQwMDg3MmRiN2E1OTc2ZjcifQ.BpVdvBjKEhQ2aIZBfkE-SoU2a3UeFkYCKQKh42Ncbio",
"token_type": "Bearer",
"expires_in": 3600
}
  • 状态码 422 参数错误
1
2
3
4
5
6
7
8
9
10
11
12
{
"message": "The given data was invalid.",
"errors": {
"email": [
"邮箱 不能为空。"
],
"password": [
"密码 不能为空。"
]
},
"status_code": 422,
}

3.1.2 添加登录接口

在 src\services\login.js 中

1
2
3
4
5
6
7
8
9
10
11
import request from '@/utils/request'

export async function fakeAccountLogin(params) {
return request('/auth/login', {
method: 'POST',
data: params,
})
}
export async function getFakeCaptcha(mobile) {
return request(`/api/login/captcha?mobile=${mobile}`)
}

1.将token存入localStorage

在\src\models\login.js 判断是否登录,并跳转到首页,将token存入localStorage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import { stringify } from 'querystring'
import { history } from 'umi'
import { fakeAccountLogin } from '@/services/login'
import { setAuthority } from '@/utils/authority'
import { getPageQuery } from '@/utils/utils'
import { message } from 'antd'

const Model = {
namespace: 'login',
state: {},
effects: {
*login({ payload }, { call, put }) {
const response = yield call(fakeAccountLogin, payload)
// 判断是否登陆成功
if (response.status === undefined) {
yield put({
type: 'changeLoginStatus',
payload: response,
}) // Login successfully

// 跳转到首页
history.replace('/')
message.success('🎉 🎉 🎉 登录成功!')
}
},

logout() {
const { redirect } = getPageQuery() // Note: There may be security issues, please note

if (window.location.pathname !== '/user/login' && !redirect) {
history.replace({
pathname: '/user/login',
search: stringify({
redirect: window.location.href,
}),
})
}
},
},
reducers: {
changeLoginStatus(state, { payload }) {
// 将token存入localStorage
localStorage.setItem('access_token', payload.access_token)
return { ...state }
},
},
}
export default Model

2.取出token,加在Header头中

在\src\utils\request.js 取出token,加在Header头中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/** Request 网络请求工具 更详细的 api 文档: https://github.com/umijs/umi-request */
import { extend } from 'umi-request'
import { message } from 'antd'

const codeMessage = {
200: '服务器成功返回请求的数据。',
201: '新建数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
204: '处理成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
406: '请求的格式不可得。',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。',
}
/**
* @zh-CN 异常处理程序
* @en-US Exception handler
*/

const errorHandler = async error => {
const { response } = error

if (response && response.status) {
let errorText = codeMessage[response.status] || response.statusText
const { status } = response
const result = await response.json()
// 处理422未验证通过的情况
if (status === 422) {
let errs = ''
for (const key in result.errors) {
errs += result.errors[key][0]
}
errorText += `[ ${errs} ]`
}
// 处理400的情况
if (status === 400) {
errorText += `[ ${result.message} ]`
}
message.error(errorText)
} else if (!response) {
message.error('网络发生异常,无法连接服务器')
}

return response
}
/**
* @en-US Configure the default parameters for request
* @zh-CN 配置request请求时的默认参数
*/

const request = extend({
errorHandler,
// default error handling
credentials: 'include', // Does the default request bring cookies
prefix: '/api',
})

// 请求拦截器,在请求之前添加Header头
request.interceptors.request.use((url, options) => {
// 获取token
const token = localStorage.getItem('access_token') || ' '
// 设置Header头
const headers = {
Authorization: `Bearer ${token}`,
}
return {
url,
options: { ...options, headers },
}
})

export default request

3.2 获取用户信息

3.2.1 登录信息接口文档

接口描述
  • 登录信息
请求 URL
  • /api/admin/user
请求方式
  • GET
请求头部
参数名 必选 类型 说明
Authorization string JWT token
返回参数
参数名 必含 类型 说明
id int 主键
name string 昵称
email string 邮箱
phone string 手机号
avatar string 头像
avatar_url string 头像地址
is_locked int 是否锁定: 0 正常 1 锁定
created_at timestamp 创建时间
updated_at timestamp 更新时间
返回示例
  • 状态码 200 请求成功
1
2
3
4
5
6
7
8
9
10
11
{
"id": 1,
"name": "超级管理员",
"email": "super@a.com",
"phone": null,
"avatar": null,
"avatar_url": "",
"is_locked": 0,
"created_at": "2020-12-22T02:58:08.000000Z",
"updated_at": "2020-12-22T04:32:27.000000Z"
}

3.2.2 添加获取用户登录信息接口

在\src\services\user.js 中

1
2
3
4
5
6
7
8
9
10
11
12
import request from '@/utils/request'

export async function query() {
return request('/api/users')
}
// 获取当前登录用户信息
export async function queryCurrent() {
return request('/admin/user')
}
export async function queryNotices() {
return request('/api/notices')
}

3.2.3 将用户信息存入localstorage

在\src\models\user.js 中判断localstorage是否有用户信息,没有则请求,再将用户信息存入localstorage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { queryCurrent, query as queryUsers } from '@/services/user'

const UserModel = {
namespace: 'user',
state: {
currentUser: {},
},
effects: {
*fetch(_, { call, put }) {
const response = yield call(queryUsers)
yield put({
type: 'save',
payload: response,
})
},

// 获取用户信息
*fetchCurrent(_, { call, put }) {
// 查看localstorage是否有用户信息,没有再去请求
let userInfo = JSON.parse(localStorage.getItem('userInfo'))
if (!userInfo) {
userInfo = yield call(queryCurrent)
//修复BUG:有时候userInfo返回的是useCache=false被误存入localStorage,错误的userInfo导致页面一直刷新
if (userInfo.useCache !== false) localStorage.setItem('userInfo', JSON.stringify(userInfo))
}

yield put({
type: 'saveCurrentUser',
payload: userInfo,
})
},
},
reducers: {
saveCurrentUser(state, action) {
return { ...state, currentUser: action.payload || {} }
},
},
}
export default UserModel

1.判断用户是否登录

在\src\layouts\SecurityLayout.jsx 中精简代码,判断用户是否登录
//关键代码 const isLogin = currentUser && currentUser.id;因为后台返回的用户idid不是userId

返回示例
  • 状态码 200 请求成功
1
2
3
4
5
6
7
8
9
10
11
{
"id": 1,
"name": "超级管理员",
"email": "super@a.com",
"phone": null,
"avatar": null,
"avatar_url": "",
"is_locked": 0,
"created_at": "2020-12-22T02:58:08.000000Z",
"updated_at": "2020-12-22T04:32:27.000000Z"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import React from 'react'
import { PageLoading } from '@ant-design/pro-layout'
import { Redirect, connect } from 'umi'
import { stringify } from 'querystring'

class SecurityLayout extends React.Component {
state = {
isReady: false,
}

componentDidMount() {
this.setState({
isReady: true,
})
const { dispatch } = this.props

if (dispatch) {
dispatch({
type: 'user/fetchCurrent',
})
}
}

render() {
const { isReady } = this.state
const { children, loading, currentUser } = this.props // You can replace it to your authentication rule (such as check token exists)
// You can replace it with your own login authentication rules (such as judging whether the token exists)

const isLogin = currentUser && currentUser.id
const queryString = stringify({
redirect: window.location.href,
})

if ((!isLogin && loading) || !isReady) {
return <PageLoading />
}

if (!isLogin && window.location.pathname !== '/login') {
return <Redirect to={`/login?${queryString}`} />
}

return children
}
}

export default connect(({ user, loading }) => ({
currentUser: user.currentUser,
loading: loading.models.user,
}))(SecurityLayout)

3.2.4 修改管理员头像

在\src\components\GlobalHeader\AvatarDropdown.jsx 修改管理员头像,将currentUser.avatar更改为currentUser.avatar_url

1
<Avatar size="small" className={styles.avatar} src={currentUser.avatar_url} alt="avatar" />

3.2.5 优化登录,判断登录之后重定向到首页

在\src\pages\Login\index.jsx 中 优化登录,判断登录之后重定向到首页
导入useEffecthistory

1
2
import React, { useEffect } from 'react'
import { connect, history } from 'umi'

加入useEffect代替生命周期函数

1
2
3
4
5
useEffect(() => {
// 判断如果已经登录过,直接去首页
const userInfo = localStorage.getItem('userInfo')
if (userInfo) history.replace('/')
}, [])

3.2.6 登录 bug

在在\src\models\user.js 中 将用户数据存入localStorage时,有时候userInfo返回的是useCache=false被误存入localStorage,错误的userInfo导致页面一直刷新

1.解决

在接口请求获取userInfo之后判断是否为正确的数据,是才存入localStorage

1
2
3
4
5
if (!userInfo) {
userInfo = yield call(queryCurrent);
//修复BUG:有时候userInfo返回的是useCache=false被误存入localStorage,错误的userInfo导致页面一直刷新
if (userInfo.useCache !== false) localStorage.setItem('userInfo', JSON.stringify(userInfo));
}

3.3 退出

3.3.1 退出接口文档

接口描述
  • 退出登录
请求 URL
  • /api/auth/logout
请求方式
  • POST
请求头部
参数名 必选 类型 说明
Authorization string JWT token
返回示例
  • 状态码 204 请求成功

3.3.2 添加退出接口

在\src\services\login.js 中

1
2
3
export async function logout() {
return request.post('/auth/logout')
}

3.3.3 添加退出方法

在\src\models\login.js 中导入logout并添加退出方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { history } from 'umi'
import { fakeAccountLogin, logout } from '@/services/login'

import { message } from 'antd'

const Model = {
namespace: 'login',
state: {},
effects: {
*login({ payload }, { call, put }) {
// loading
const load = message.loading('登录中...')

const response = yield call(fakeAccountLogin, payload)
// 判断是否登陆成功
if (response.status === undefined) {
yield put({
type: 'changeLoginStatus',
payload: response,
}) // Login successfully

// 跳转到首页
history.replace('/')
message.success('🎉 🎉 🎉 登录成功!')
}
load()
},

*logout(_, { call }) {
// loading
const load = message.loading('退出中...')

// 请求Api,退出登录
const response = yield call(logout)

// 判断是否成功退出
if (response.status === undefined) {
// 删除本地存储的token和userInfo
localStorage.removeItem('access_token')
localStorage.removeItem('userInfo')
// 重定向到登录页
history.replace('/login')
message.success('🎉 🎉 🎉 退出成功!')
}
load()
},
},
reducers: {
changeLoginStatus(state, { payload }) {
// 将token存入localStorage
localStorage.setItem('access_token', payload.access_token)
return { ...state }
},
},
}
export default Model

四、首页统计

4.1 新建统计面板文件

在 src\pages 文件夹下创建一个文件夹和文件 DashBoard\index.jsx
因为只有一个接口请求,而且不需要获取共享数据,就直接在文件里写请求,可以不用models

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import React, { useEffect, useState } from 'react'
import { Statistic, Card, Row, Col } from 'antd'
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'
import { fetchDashboard } from '@/services/dashboard'

const DashBoard = () => {
// 定义组件状态,状态改变,重新渲染组件
const [data, setData] = useState({})
useEffect(async () => {
// 发送请求,获取统计数据
const resData = await fetchDashboard()
// 得到数据之后更新组件状态
setData(resData)
}, [])

return (
<div>
<Row gutter={16}>
<Col span={8}>
<Card>
<Statistic
title="用户数量"
value={data.users_count}
precision={0}
valueStyle={{ color: '#3f8600' }}
prefix={<ArrowUpOutlined />}
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title="订单数量"
value={data.goods_count}
precision={0}
valueStyle={{ color: '#cf1322' }}
prefix={<ArrowDownOutlined />}
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title="商品数量"
value={data.order_count}
precision={0}
valueStyle={{ color: '#234abc' }}
prefix={<ArrowDownOutlined />}
/>
</Card>
</Col>
</Row>
</div>
)
}

export default DashBoard

4.2 添加统计面板路由

在 config\routes.js 中添加统计面板路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
export default [
{
path: '/',
component: '../layouts/BlankLayout',
routes: [
{
path: '/login',
component: '../layouts/LoginLayout',
routes: [
{
name: 'login',
path: '/login',
component: './Login',
},
],
},
{
path: '/',
component: '../layouts/SecurityLayout',
routes: [
{
path: '/',
component: '../layouts/BasicLayout',
routes: [
{
path: '/',
redirect: '/dashboard',
},
{
name: 'dashboard',
path: '/dashboard',
icon: 'PieChartOutlined',
component: '@/pages/DashBoard',
},

{
component: './404',
},
],
},
{
component: './404',
},
],
},
],
},
{
component: './404',
},
]

4.3 统计面板接口文档

接口描述
  • 首页统计
请求 URL
  • /api/admin/index
请求方式
  • GET
返回参数
参数名 必含 类型 说明
users_count int 用户数量
goods_count int 商品数量
order_count int 订单数据
返回示例
  • 状态码 200 请求成功
1
2
3
4
5
{
"users_count": 7,
"goods_count": 237,
"order_count": 1
}

4.4 添加统计面板接口

在 src\services\dashboard.js 添加统计面板接口

1
2
3
4
5
6
7
8
import request from '@/utils/request'

/**
* 获取统计面板数据
*/
export function fetchDashboard() {
return request('/admin/index')
}

五、用户列表

5.1 用户基本列表

5.1.1 用户列表接口文档

接口描述
  • 用户列表
请求 URL
  • /api/admin/users
请求方式
  • GET
请求头部
参数名 必选 类型 说明
Authorization string JWT token
Query 请求参数
参数名 必选 类型 说明
current int 分页-当前页
name string 姓名模糊搜索
email string 邮箱匹配搜索
phone string 手机号匹配搜索
返回参数

data

参数名 必含 类型 说明
id int 主键
name string 昵称
email string 邮箱
phone string 手机号
avatar string 头像
avatar_url string 头像地址
is_locked int 是否锁定: 0 正常 1 锁定
created_at timestamp 创建时间
updated_at timestamp 更新时间

meta.pagination

参数名 必含 类型 说明
total int 数据总数
count int 当前页数据
per_page int 每页显示条数
current_page int 当前页页码
total_pages int 总页数
links.previous string 上一页链接
links.next string 下一页链接
返回示例
  • 状态码 200 请求成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{
"data": [
{
"id": 1,
"name": "超级管理员",
"email": "super@a.com",
"phone": null,
"avatar": null,
"avatar_url": "",
"is_locked": 1,
"created_at": "2020-12-22T02:58:08.000000Z",
"updated_at": "2020-12-24T06:40:44.000000Z"
},
{
"id": 2,
"name": "xx",
"email": "12311@qq.com",
"phone": null,
"avatar": null,
"avatar_url": "",
"is_locked": 0,
"created_at": "2020-12-24T03:47:48.000000Z",
"updated_at": "2020-12-24T06:44:43.000000Z"
}
],
"meta": {
"pagination": {
"total": 7,
"count": 2,
"per_page": 2,
"current_page": 1,
"total_pages": 4,
"links": {
"previous": null,
"next": "http://shopapi.mamp/api/admin/users?page=2"
}
}
}
}

5.1.2 添加用户列表接口

在 src\services\user.js 中

1
2
3
4
5
6
7
8
9
10
11
import request from '@/utils/request'

// 获取当前登录用户信息
export async function queryCurrent() {
return request('/admin/user')
}

// 获取用户列表
export async function getUsers(params) {
return request('/admin/users', { params })
}

5.1.3 创建基本的用户列表

ProTable参考文档
在\src\pages\User\index.jsx 中新建用户列表文件和文件,创建基本的 table,根据接口文档写需要的字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import React, { useRef } from 'react'
import { PageContainer } from '@ant-design/pro-layout'
import ProTable from '@ant-design/pro-table'
import { Button, Avatar, Switch } from 'antd'
import { PlusOutlined, UserOutlined } from '@ant-design/icons'
import { getUsers } from '@/services/user'

const index = () => {
const actionRef = useRef()

const columns = [
{
title: '头像',
dataIndex: 'avatar_url',
hideInSearch: true,
render: (_, record) => <Avatar src={record.avatar_url} size={32} icon={<UserOutlined />} />,
},
{
title: '姓名',
dataIndex: 'name',
},
{
title: '邮箱',
dataIndex: 'email',
},
{
title: '是否禁用',
dataIndex: 'is_locked',
hideInSearch: true,
render: (_, record) => (
<Switch
checkedChildren="启用"
unCheckedChildren="禁用"
defaultChecked={record.is_locked === 0}
onChange={() => {}}
/>
),
},
{
title: '创建时间',
dataIndex: 'created_at',
hideInSearch: true,
},
{
title: '操作',
render: (_, record) => <a onChange={() => {}}>编辑</a>,
},
]

// 获取用户列表数据
const getData = async params => {
const response = await getUsers(params)
return {
data: response.data,
// success 请返回 true,
// 不然 table 会停止解析数据,即使有数据
success: true,
// 不传会使用 data 的长度,如果是分页一定要传
total: response.meta.pagination.total,
}
}

return (
<PageContainer>
<ProTable
columns={columns}
actionRef={actionRef}
request={async (params = {}) => getData(params)}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
pageSize: 10,
}}
dateFormatter="string"
headerTitle="用户列表"
toolBarRender={() => [
<Button key="button" icon={<PlusOutlined />} type="primary">
新建
</Button>,
]}
/>
</PageContainer>
)
}

export default index

5.2 禁用和启用

5.2.1 禁启和启用接口文档

接口描述
  • 禁用和启用
请求 URL
  • /api/admin/users/{user}/lock
请求方式
  • PATCH
请求头部
参数名 必选 类型 说明
Authorization string JWT token
RESET 参数
参数名 必选 类型 说明
user int 用户 id
返回示例
  • 状态码 204 请求成功

5.2.2 添加禁用和启用接口

在\src\services\user.js 中添加禁启用接口

1
2
3
4
5
6
7
8
/**
* 禁用和启用
* @param {用户id} uid
* @returns
*/
export async function lockUser(uid) {
return request.patch(`/admin/users/${uid}/lock`)
}

5.2.3 添加和启用方法

在\src\pages\User\index.jsx 中,先导入lockUser

1
import { getUsers, lockUser } from '@/services/user'

创建禁启用函数接收用户 id,因为成功后后端返回值是空,所以response.status===undefined判断为空则操作成功

1
2
3
4
5
6
7
8
9
// 禁启用
const heandleLockUser = async uid => {
const response = await lockUser(uid)
if (response.status === undefined) {
message.success('操作成功!')
} else {
message.error('操作失败!')
}
}

然后在columns列表中,找到禁启用字段,使用禁启用函数,同时传出record.id用户 id

1
2
3
4
5
6
7
8
9
10
11
{
title:"是否禁用",
dataIndex:"is_locked",
hideInSearch:true,
render:(_,record)=> <Switch
checkedChildren="启用"
unCheckedChildren="禁用"
defaultChecked={record.is_locked === 0}
onChange={()=>{heandleLockUser(record.id)}}
/>
},

5.3 添加用户

Modal对话框文档 ProForm高级表单文档

5.3.1 添加用户接口文档

接口描述
  • 添加用户

注意: 后台 Api 做了 RBAC 权限验证, 新创建的用户无法登陆, 必须为新创建的用户分配响应的角色或权限才可以
注意: 权限管理暂未开放 Api

请求 URL
  • /api/admin/users
请求方式
  • POST
请求头部
参数名 必选 类型 说明
Authorization string JWT token
Body 请求参数
参数名 必选 类型 说明
name string 昵称
email string 邮箱
password string 密码
返回示例
  • 状态码 201 创建成功
  • 状态码 422 参数错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"message": "The given data was invalid.",
"errors": {
"name": [
"昵称 不能为空"
],
"email": [
"邮箱 不能为空。"
],
"password": [
"密码 不能为空。"
]
},
"status_code": 422,
}

5.3.2 添加添加用户接口

在\src\services\user.js 中
这里添加用户接口和获取用户列表是同一个接口/admin/users,但是他们的请求方式不一样,添加用户接口用post,而获取用户列表接口是用默认的get方式。

1
2
3
4
5
6
7
8
/**
* 添加用户
* @param {*} params
* @returns
*/
export async function addUser(params) {
return request.post('/admin/users', { params })
}

5.3.3 封装添加用户模态框

1.封装添加用户模态框组件Create

在\src\pages\User 文件夹下新建公共文件夹和Create.jsx文件 \src\pages\User\components\Create.jsx
Create组件在父组件中使用,并用props接收父组件传的方法和实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import React from 'react'
import ProForm, { ProFormText } from '@ant-design/pro-form'
import { Modal, message } from 'antd'
import { addUser } from '@/services/user'

const Create = props => {
const { isModalVisible, isShowModal, actionRef } = props

/**
* 添加用户
* @param {表单数据} values
*/
const createUser = async values => {
// 发送请求,添加用户
const response = await addUser(values)
if (response.status === undefined) {
message.success('添加成功!')
// 刷新表格数据
actionRef.current.reload()
// 关闭模态框
isShowModal(false)
}
}

return (
<Modal
title="添加用户"
visible={isModalVisible}
onCancel={() => isShowModal(false)}
footer={null}
destroyOnClose={true}>
<ProForm
onFinish={values => {
createUser(values)
}}>
<ProFormText
name="name"
label="昵称"
placeholder="请输入昵称"
rules={[{ required: true, message: '请输入昵称' }]}
/>
<ProFormText
name="email"
label="邮箱"
placeholder="请输入邮箱"
rules={[
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '邮箱格式不正确' },
]}
/>
<ProFormText.Password
name="password"
label="密码"
placeholder="请输入密码"
rules={[
{ required: true, message: '请输入密码' },
{ min: 6, message: '密码最小6位' },
]}
/>
</ProForm>
</Modal>
)
}

export default Create

2.调用封装的Create组件

这里是使用Create组件,并且将方法和实例传给子组件,在父组件里只做简单的显示关闭操作,不做过多的逻辑

1
<Create isModalVisible={isModalVisible} isShowModal={isShowModal} actionRef={actionRef} />

在\src\pages\User\index.jsx,在父组件中导入import Create from './components/Create';

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import React, { useRef, useState } from 'react'
import { PageContainer } from '@ant-design/pro-layout'
import ProTable from '@ant-design/pro-table'
import { Button, Avatar, Switch, message } from 'antd'
import { PlusOutlined, UserOutlined } from '@ant-design/icons'
import { getUsers, lockUser } from '@/services/user'
import Create from './components/Create'

const index = () => {
const [isModalVisible, setisModalVisible] = useState(false)

// 表格的ref,便于操作自定义操作表格
const actionRef = useRef()

// 获取用户列表数据
const getData = async params => {
const response = await getUsers(params)
return {
data: response.data,
// success 请返回 true,
// 不然 table 会停止解析数据,即使有数据
success: true,
// 不传会使用 data 的长度,如果是分页一定要传
total: response.meta.pagination.total,
}
}

// 禁启用
const heandleLockUser = async uid => {
const response = await lockUser(uid)
if (response.status === undefined) {
message.success('操作成功!')
}
}

// 控制新建用户模态框显示和隐藏
const isShowModal = show => {
setisModalVisible(show)
}

const columns = [
{
title: '头像',
dataIndex: 'avatar_url',
hideInSearch: true,
render: (_, record) => <Avatar src={record.avatar_url} size={32} icon={<UserOutlined />} />,
},
{
title: '姓名',
dataIndex: 'name',
},
{
title: '邮箱',
dataIndex: 'email',
},
{
title: '是否禁用',
dataIndex: 'is_locked',
hideInSearch: true,
render: (_, record) => (
<Switch
checkedChildren="启用"
unCheckedChildren="禁用"
defaultChecked={record.is_locked === 0}
onChange={() => {
heandleLockUser(record.id)
}}
/>
),
},
{
title: '创建时间',
dataIndex: 'created_at',
hideInSearch: true,
},
{
title: '操作',
render: (_, record) => <a onChange={() => {}}>编辑</a>,
},
]

return (
<PageContainer>
<ProTable
columns={columns}
actionRef={actionRef}
request={async (params = {}) => getData(params)}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
pageSize: 10,
}}
dateFormatter="string"
headerTitle="用户列表"
toolBarRender={() => [
<Button
key="button"
icon={<PlusOutlined />}
type="primary"
onClick={() => isShowModal(true)}>
新建
</Button>,
]}
/>
<Create isModalVisible={isModalVisible} isShowModal={isShowModal} actionRef={actionRef} />
</PageContainer>
)
}

export default index

5.4 编辑用户

5.4.1 更新用户信息和用户详情接口文档

接口描述
  • 更新用户信息
请求 URL
  • /api/admin/users/{users}
请求方式
  • PUT
REST 请求参数
参数名 必选 类型 说明
users int 用户 id
Body 请求参数
参数名 必选 类型 说明
name string 昵称
email string 邮箱
返回示例
  • 状态码 201 创建成功
  • 状态码 422 参数错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"message": "The given data was invalid.",
"errors": {
"name": [
"昵称 不能为空"
],
"email": [
"邮箱 不能为空。"
],
"password": [
"密码 不能为空。"
]
},
"status_code": 422,
}
接口描述
  • 用户详情
请求 URL
  • /api/admin/users/{user}
请求方式
  • GET
请求头部
参数名 必选 类型 说明
Authorization string JWT token
RESET 参数
参数名 必选 类型 说明
user string 用户 id
返回参数
参数名 必含 类型 说明
id int 主键
name string 昵称
email string 邮箱
phone string 手机号
avatar string 头像
avatar_url string 头像地址
is_locked int 是否锁定: 0 正常 1 锁定
created_at timestamp 创建时间
updated_at timestamp 更新时间
返回示例
  • 状态码 200 请求成功
1
2
3
4
5
6
7
8
9
10
11
{
"id": 1,
"name": "超级管理员",
"email": "super@a.com",
"phone": null,
"avatar": null,
"avatar_url": "",
"is_locked": 0,
"created_at": "2020-12-22T02:58:08.000000Z",
"updated_at": "2020-12-22T04:32:27.000000Z"
}

5.4.2 添加更新用户信息和用户详情接口

在\src\services\user.js 中添加更新用户和用户详情接口,虽然这两个接口请求是同一个,但是他们的传参方式和参数是不一样的。
updateUserput方法,用于更新数据,需要传编辑的用户 id 和修改的参数,
showUserget方法,用于设置编辑栏上的默认值,只需要传编辑用户 id,后端返回改用户具体参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 更新用户
* @param {*} params
* @returns
*/
export async function updateUser(editId, params) {
return request.put(`/admin/users/${editId}`, { params })
}
/**
* 用户详情
* @param {*} editId
* @returns
*/
export async function showUser(editId) {
return request(`/admin/users/${editId}`)
}

5.4.3 封装编辑用户模态框

1.封装编辑模态框组件 Edit

在\src\pages\User\components 文件夹下创建编辑用户组件Edit.jsx,先导入接口请求方法import { showUser, updateUser } from '@/services/user';

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import React, { useEffect, useState } from 'react'
import ProForm, { ProFormText } from '@ant-design/pro-form'
import { Modal, message, Skeleton } from 'antd'
import { showUser, updateUser } from '@/services/user'

const Edit = props => {
const { isModalVisible, isShowModal, actionRef, editId } = props
const [initialValues, setinitialValues] = useState(undefined)

useEffect(async () => {
// 发送请求,获取用户详情
if (editId !== undefined) {
const response = await showUser(editId)
setinitialValues({
name: response.name,
email: response.email,
})
}
}, [])

/**
* 添加用户
* @param {表单数据} values
*/
const editUser = async values => {
// 发送请求,更新用户
const response = await updateUser(editId, values)
if (response.status === undefined) {
message.success('更新成功!')
// 刷新表格数据
actionRef.current.reload()
// 关闭模态框
isShowModal(false)
}
}

return (
<Modal
title="编辑用户"
visible={isModalVisible}
onCancel={() => isShowModal(false)}
footer={null}
destroyOnClose={true}>
{initialValues === undefined ? (
<Skeleton active={true} paragraph={{ rows: 4 }} />
) : (
<ProForm
initialValues={initialValues}
onFinish={values => {
editUser(values)
}}>
<ProFormText
name="name"
label="昵称"
placeholder="请输入昵称"
rules={[{ required: true, message: '请输入昵称' }]}
/>
<ProFormText
name="email"
label="邮箱"
placeholder="请输入邮箱"
rules={[
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '邮箱格式不正确' },
]}
/>
</ProForm>
)}
</Modal>
)
}

export default Edit

其中加入了antd的骨架框,原因是页面渲染比接口请求快,在获取用户详情之前页面就渲染完了,导致编辑栏上没有得到该被编辑用户的数据,加入骨架框起到缓冲作用。
image.png
同时给骨架框和编辑表单添加了三元运算符,避免两个同时被渲染,判断接口请求接收到用户详情之后骨架框消失,编辑表单出现。一下是主要代码

1
2
3
4
5
6
7
8
9
10
11
12
const [initialValues, setinitialValues] = useState(undefined)

useEffect(async () => {
// 发送请求,获取用户详情
if (editId !== undefined) {
const response = await showUser(editId)
setinitialValues({
name: response.name,
email: response.email,
})
}
}, [])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
initialValues === undefined ? (
<Skeleton active={true} paragraph={{ rows: 4 }} />
) : (
<ProForm
initialValues={initialValues}
onFinish={values => {
editUser(values)
}}>
<ProFormText
name="name"
label="昵称"
placeholder="请输入昵称"
rules={[{ required: true, message: '请输入昵称' }]}
/>
<ProFormText
name="email"
label="邮箱"
placeholder="请输入邮箱"
rules={[
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '邮箱格式不正确' },
]}
/>
</ProForm>
)
}

2.调用封装的Edit组件

在\src\pages\User\index.jsx 中,导入编辑组件import Edit from './components/Edit';
调用了Edit组件并且多传一个被编辑用户 id editId={editId}给子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import React, { useRef, useState } from 'react'
import { PageContainer } from '@ant-design/pro-layout'
import ProTable from '@ant-design/pro-table'
import { Button, Avatar, Switch, message } from 'antd'
import { PlusOutlined, UserOutlined } from '@ant-design/icons'
import { getUsers, lockUser } from '@/services/user'
import Create from './components/Create'
import Edit from './components/Edit'

const index = () => {
const [isModalVisible, setisModalVisible] = useState(false)
const [isModalVisibleEdit, setisModalVisibleEdit] = useState(false)
const [editId, setEditId] = useState(undefined)

// 表格的ref,便于操作自定义操作表格
const actionRef = useRef()

// 获取用户列表数据
const getData = async params => {
const response = await getUsers(params)
return {
data: response.data,
// success 请返回 true,
// 不然 table 会停止解析数据,即使有数据
success: true,
// 不传会使用 data 的长度,如果是分页一定要传
total: response.meta.pagination.total,
}
}

// 禁启用
const heandleLockUser = async uid => {
const response = await lockUser(uid)
if (response.status === undefined) {
message.success('操作成功!')
}
}

// 控制新建用户模态框显示和隐藏
const isShowModal = show => {
setisModalVisible(show)
}

// 控制编辑用户模态框显示和隐藏
const isShowModalEdit = (show, id) => {
setisModalVisibleEdit(show)
setEditId(id)
}

const columns = [
{
title: '头像',
dataIndex: 'avatar_url',
hideInSearch: true,
render: (_, record) => <Avatar src={record.avatar_url} size={32} icon={<UserOutlined />} />,
},
{
title: '姓名',
dataIndex: 'name',
},
{
title: '邮箱',
dataIndex: 'email',
},
{
title: '是否禁用',
dataIndex: 'is_locked',
hideInSearch: true,
render: (_, record) => (
<Switch
checkedChildren="启用"
unCheckedChildren="禁用"
defaultChecked={record.is_locked === 0}
onChange={() => {
heandleLockUser(record.id)
}}
/>
),
},
{
title: '创建时间',
dataIndex: 'created_at',
hideInSearch: true,
},
{
title: '操作',
render: (_, record) => <a onClick={() => isShowModalEdit(true, record.id)}>编辑</a>,
},
]

return (
<PageContainer>
<ProTable
columns={columns}
actionRef={actionRef}
request={async (params = {}) => getData(params)}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
pageSize: 10,
}}
dateFormatter="string"
headerTitle="用户列表"
toolBarRender={() => [
<Button
key="button"
icon={<PlusOutlined />}
type="primary"
onClick={() => isShowModal(true)}>
新建
</Button>,
]}
/>
<Create isModalVisible={isModalVisible} isShowModal={isShowModal} actionRef={actionRef} />

{!isModalVisibleEdit ? (
''
) : (
<Edit
isModalVisible={isModalVisibleEdit}
isShowModal={isShowModalEdit}
actionRef={actionRef}
editId={editId}
/>
)}
</PageContainer>
)
}

export default index

其中关键代码
这里设置了编辑模态框的打开或者关闭,并且设置被编辑用户 id,传给Edit子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const [isModalVisibleEdit, setisModalVisibleEdit] = useState(false);
const [editId, setEditId] = useState(undefined);



// 控制编辑用户模态框显示和隐藏
const isShowModalEdit = (show, id) => {
setisModalVisibleEdit(show);
setEditId(id);
};



{
title: '操作',
render: (_, record) => <a onClick={() => isShowModalEdit(true, record.id)}>编辑</a>,
},

这里也设置了三目运算,主要原因是因为每次编辑都会有不同的用户 id,在编辑组件时挂载时,触发Edit子组件的生命周期请求用户 id,编辑组件关闭时卸载生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//关键代码,  isModalVisibleEdit编辑模态框显示时,挂载生命周期获取用户详情,编辑模态框关闭时卸载生命周期函数
useEffect(async () => {
// 发送请求,获取用户详情
if (editId !== undefined) {
const response = await showUser(editId)
setinitialValues({
name: response.name,
email: response.email,
})
}
}, [])

{
!isModalVisibleEdit ? (
''
) : (
<Edit
isModalVisible={isModalVisibleEdit}
isShowModal={isShowModalEdit}
actionRef={actionRef}
editId={editId}
/>
)
}

5.5 封装编辑和添加用户

1.在\src\pages\User\components 文件夹中,复制Edit.jsx并重命名CreateOrEdit.jsx,将Create组件和Edit组件合并在一起,通过有editId判断是编辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import React, { useEffect, useState } from 'react'
import ProForm, { ProFormText } from '@ant-design/pro-form'
import { Modal, message, Skeleton } from 'antd'
import { showUser, updateUser, addUser } from '@/services/user'

const CreateOrEdit = props => {
/**
* isModalVisible 模态框是否显示
* isShowModal 操作模态框显示隐藏的方法
* actionRef 父组件传来的表格的引用,可以用来操作表格,比如刷新表单
* editId 要编辑的id,添加的时候是undefined,只有编辑时才有
*/
const { isModalVisible, isShowModal, actionRef, editId } = props

// 将表单初始化的值设置成状态,在编辑的时候使用这个状态
const [initialValues, setinitialValues] = useState(undefined)

// 添加或者编辑的描述
const type = editId === undefined ? '添加' : '编辑'

useEffect(async () => {
// 发送请求,获取用户详情
if (editId !== undefined) {
const response = await showUser(editId)
// 获取数据之后,修改状态;状态改变,组件重新渲染,骨架框消失,编辑表单出现
setinitialValues({
name: response.name,
email: response.email,
})
}
}, [])

// 提交表单,执行编辑或者添加
const handleSubmit = async values => {
let response = []
if (editId === undefined) {
// 执行添加
// 发送请求,添加用户
response = await addUser(values)
} else {
// 执行编辑
// 发送请求,更新用户
response = await updateUser(editId, values)
}
if (response.status === undefined) {
message.success(`${type}成功!`)
// 刷新表格数据
actionRef.current.reload()
// 关闭模态框
isShowModal(false)
}
}

return (
<Modal
title={`${type}用户`}
visible={isModalVisible}
onCancel={() => isShowModal(false)}
footer={null}
destroyOnClose={true}>
{
// 只有是编辑的情况下,并且要显示的数据还有返回,才显示骨架框
initialValues === undefined && editId !== undefined ? (
<Skeleton active={true} paragraph={{ rows: 4 }} />
) : (
<ProForm
initialValues={initialValues}
onFinish={values => {
handleSubmit(values)
}}>
<ProFormText
name="name"
label="昵称"
placeholder="请输入昵称"
rules={[{ required: true, message: '请输入昵称' }]}
/>
<ProFormText
name="email"
label="邮箱"
placeholder="请输入邮箱"
rules={[
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '邮箱格式不正确' },
]}
/>
{
// 只有添加用户才有密码框
editId !== undefined ? (
''
) : (
<ProFormText.Password
name="password"
label="密码"
placeholder="请输入密码"
rules={[
{ required: true, message: '请输入密码' },
{ min: 6, message: '密码最小6位' },
]}
/>
)
}
</ProForm>
)
}
</Modal>
)
}

export default CreateOrEdit

2.在\src\pages\User\index.jsx 中导入import CreateOrEdit from './components/CreateOrEdit';``CreateOrEdit组件,将编辑和添加用户的方法改成相同的,并用过是否有editId来判断是编辑(有 id 是编辑)还是添加。最后删除掉\src\pages\User\components 文件夹中的Create.jsxEdit.jsx文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import React, { useRef, useState } from 'react'
import { PageContainer } from '@ant-design/pro-layout'
import ProTable from '@ant-design/pro-table'
import { Button, Avatar, Switch, message } from 'antd'
import { PlusOutlined, UserOutlined } from '@ant-design/icons'
import { getUsers, lockUser } from '@/services/user'
import CreateOrEdit from './components/CreateOrEdit'

const index = () => {
const [isModalVisible, setisModalVisible] = useState(false)
const [editId, setEditId] = useState(undefined)

// 表格的ref,便于操作自定义操作表格
const actionRef = useRef()

// 获取用户列表数据
const getData = async params => {
const response = await getUsers(params)
return {
data: response.data,
// success 请返回 true,
// 不然 table 会停止解析数据,即使有数据
success: true,
// 不传会使用 data 的长度,如果是分页一定要传
total: response.meta.pagination.total,
}
}

// 禁启用
const heandleLockUser = async uid => {
const response = await lockUser(uid)
if (response.status === undefined) {
message.success('操作成功!')
}
}

// 控制新建/添加用户模态框显示和隐藏
const isShowModal = (show, id = undefined) => {
setEditId(id)
setisModalVisible(show)
}

const columns = [
{
title: '头像',
dataIndex: 'avatar_url',
hideInSearch: true,
render: (_, record) => <Avatar src={record.avatar_url} size={32} icon={<UserOutlined />} />,
},
{
title: '姓名',
dataIndex: 'name',
},
{
title: '邮箱',
dataIndex: 'email',
},
{
title: '是否禁用',
dataIndex: 'is_locked',
hideInSearch: true,
render: (_, record) => (
<Switch
checkedChildren="启用"
unCheckedChildren="禁用"
defaultChecked={record.is_locked === 0}
onChange={() => {
heandleLockUser(record.id)
}}
/>
),
},
{
title: '创建时间',
dataIndex: 'created_at',
hideInSearch: true,
},
{
title: '操作',
render: (_, record) => <a onClick={() => isShowModal(true, record.id)}>编辑</a>,
},
]

return (
<PageContainer>
<ProTable
columns={columns}
actionRef={actionRef}
request={async (params = {}) => getData(params)}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
pageSize: 10,
}}
dateFormatter="string"
headerTitle="用户列表"
toolBarRender={() => [
<Button
key="button"
icon={<PlusOutlined />}
type="primary"
onClick={() => isShowModal(true)}>
新建
</Button>,
]}
/>

{
// 模态框隐藏的时候,不挂载组件,显示的时候挂载组件,这是为了触发子组件的生命周期
!isModalVisible ? (
''
) : (
<CreateOrEdit
isModalVisible={isModalVisible}
isShowModal={isShowModal}
actionRef={actionRef}
editId={editId}
/>
)
}
</PageContainer>
)
}

export default index

六、商品列表

6.1 商品基本列表

6.1.1 商品列表接口

接口描述
  • 商品列表
请求 URL
  • /api/admin/goods
请求方式
  • GET
请求头部
参数名 必选 类型 说明
Authorization string JWT token
Query 请求参数
参数名 必选 类型 说明
current int 分页-当前页
title string 商品名模糊搜索
category_id int 分类
is_on int 是否上架 0 不上架 1 上架
is_recommend int 是否推荐 0 不推荐 1 推荐
include string 包含额外的数据: category 分类,user 用户, comments 评论

inlude 可以返回额外的数据, 多个使用,分隔, 比如:include=category,user,comments

返回参数

data

参数名 必含 类型 说明
id int 自增长主键 ID
user_id int 创建者
category_id int 分类
title string 标题
description string 描述
price int 价格
stock int 库存
sales int 销量
cover string 封面图
cover_url string 封面图 url
pics array 小图集
pics_url array 小图集 url
is_on int 是否上架 0 不上架 1 上架
is_recommend int 是否推荐 0 不推荐 1 推荐
details string 详情
created_at timestamp 注册时间
updated_at timestamp 修改时间

meta.pagination

参数名 必含 类型 说明
total int 数据总数
count int 当前页数据
per_page int 每页显示条数
current_page int 当前页页码
total_pages int 总页数
links.previous string 上一页链接
links.next string 下一页链接
返回示例
  • 状态码 200 请求成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
{
"data": [
{
"id": 1,
"title": "电脑11111电脑",
"category_id": 7,
"description": "这是一个电脑1111",
"price": 5000,
"stock": 999,
"sales": 2,
"cover": "100x100.jpg",
"cover_url": "https://laravel-shop-api.oss-cn-beijing.aliyuncs.com/100x100.jpg",
"pics": [
"a.png",
"b.png"
],
"pics_url": [
"https://laravel-shop-api.oss-cn-beijing.aliyuncs.com/a.png",
"https://laravel-shop-api.oss-cn-beijing.aliyuncs.com/b.png"
],
"details": "这是一个电脑这是一个电脑这是一个电脑这是一个电脑",
"is_on": 1,
"is_recommend": 1,
"created_at": "2020-12-12T07:38:37.000000Z",
"updated_at": "2020-12-12T10:13:45.000000Z"
},
{
"id": 2,
"title": "电脑2",
"category_id": 7,
"description": "这是一个电脑",
"price": 5000,
"stock": 999,
"sales": 2,
"cover": "/imgs/img1.png",
"cover_url": "https://laravel-shop-api.oss-cn-beijing.aliyuncs.com//imgs/img1.png",
"pics": [
"a.png",
"b.png"
],
"pics_url": [
"https://laravel-shop-api.oss-cn-beijing.aliyuncs.com/a.png",
"https://laravel-shop-api.oss-cn-beijing.aliyuncs.com/b.png"
],
"details": "这是一个电脑这是一个电脑这是一个电脑这是一个电脑",
"is_on": 0,
"is_recommend": 0,
"created_at": "2020-12-12T07:38:45.000000Z",
"updated_at": "2020-12-12T07:38:45.000000Z"
}
],
"meta": {
"pagination": {
"total": 7,
"count": 2,
"per_page": 2,
"current_page": 1,
"total_pages": 4,
"links": {
"previous": null,
"next": "http://api.test/api/admin/goods?page=2"
}
}
}
}

6.1.2 添加商品列表接口

在\src\services 文件夹中复制user.jsx文件夹并重命名goods.jsx
根据接口文档添加商品列表接口

1
2
3
4
5
6
import request from '@/utils/request'

// 获取商品列表
export async function getGoods(params) {
return request('/admin/goods', { params })
}

6.1.3 创建基本商品列表页面

在\src\pages 文件夹中,复制User文件夹并重命名Goods
修改基本页面,添加商品图片预览,
其中valueType是设置筛选的单选按钮,valueEnum是选项,可以枚举也可以直接列出来,选择类参考文档

1
2
3
4
5
valueType: 'radioButton',
valueEnum: {
1: { text: '已推荐' },
0: { text: '未推荐' },
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
import React, { useRef, useState } from 'react'
import { PageContainer } from '@ant-design/pro-layout'
import ProTable from '@ant-design/pro-table'
import { Button, Image, Switch, message } from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import { getGoods } from '@/services/goods'
import CreateOrEdit from './components/CreateOrEdit'

const index = () => {
const [isModalVisible, setisModalVisible] = useState(false)
const [editId, setEditId] = useState(undefined)

// 表格的ref,便于操作自定义操作表格
const actionRef = useRef()

// 获取商品列表数据
const getData = async params => {
const response = await getGoods(params)

return {
data: response.data,
// success 请返回 true,
// 不然 table 会停止解析数据,即使有数据
success: true,
// 不传会使用 data 的长度,如果是分页一定要传
total: response.meta.pagination.total,
}
}

// 禁启用
const heandleLockUser = async uid => {
const response = await lockUser(uid)
if (response.status === undefined) {
message.success('操作成功!')
}
}

// 控制新建/添加用户模态框显示和隐藏
const isShowModal = (show, id = undefined) => {
setEditId(id)
setisModalVisible(show)
}

const columns = [
{
title: '商品图',
dataIndex: 'cover_url',
hideInSearch: true,
render: (_, record) => (
<Image
width={64}
src={record.cover_url}
placeholder={<Image preview={false} src={record.cover_url} width={200} />}
/>
),
},
{
title: '标题',
dataIndex: 'title',
},
{
title: '价格',
dataIndex: 'price',
hideInSearch: true,
},
{
title: '库存',
dataIndex: 'stock',
hideInSearch: true,
},
{
title: '销量',
dataIndex: 'sales',
hideInSearch: true,
},

{
title: '是否上架',
dataIndex: 'is_on',
render: (_, record) => (
<Switch
checkedChildren="已上架"
unCheckedChildren="未上架"
defaultChecked={record.is_on === 1}
onChange={() => {
heandleLockUser(record.id)
}}
/>
),
valueType: 'radioButton',
valueEnum: {
1: { text: '已上架' },
0: { text: '未上架' },
},
},
{
title: '是否推荐',
dataIndex: 'is_recommend',
render: (_, record) => (
<Switch
checkedChildren="已推荐"
unCheckedChildren="未推荐"
defaultChecked={record.is_recommend === 1}
onChange={() => {
heandleLockUser(record.id)
}}
/>
),
valueType: 'radioButton',
valueEnum: {
1: { text: '已推荐' },
0: { text: '未推荐' },
},
},
{
title: '创建时间',
dataIndex: 'created_at',
hideInSearch: true,
},
{
title: '操作',
hideInSearch: true,
render: (_, record) => <a onClick={() => isShowModal(true, record.id)}>编辑</a>,
},
]

return (
<PageContainer>
<ProTable
columns={columns}
actionRef={actionRef}
request={async (params = {}) => getData(params)}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
pageSize: 10,
}}
dateFormatter="string"
headerTitle="用户列表"
toolBarRender={() => [
<Button
key="button"
icon={<PlusOutlined />}
type="primary"
onClick={() => isShowModal(true)}>
新建
</Button>,
]}
/>

{
// 模态框隐藏的时候,不挂载组件,显示的时候挂载组件,这是为了触发子组件的生命周期
!isModalVisible ? (
''
) : (
<CreateOrEdit
isModalVisible={isModalVisible}
isShowModal={isShowModal}
actionRef={actionRef}
editId={editId}
/>
)
}
</PageContainer>
)
}

export default index

6.2 是否上架/推荐商品

6.2.1 商品上架和下架接口文档

接口描述
  • 上架和下架
请求 URL
  • /api/admin/goods/{good}/on
请求方式
  • PATCH
请求头部
参数名 必选 类型 说明
Authorization string JWT token
RESET 参数
参数名 必选 类型 说明
good int 商品 id
返回示例
  • 状态码 204 请求成功

6.2.2 商品推荐和不推荐接口文档

接口描述
  • 推荐和不推荐
请求 URL
  • /api/admin/goods/{good}/recommend
请求方式
  • PATCH
请求头部
参数名 必选 类型 说明
Authorization string JWT token
RESET 参数
参数名 必选 类型 说明
good int 商品 id
返回示例
  • 状态码 204 请求成功

6.2.3 添加是否上架/推荐商品接口

在\src\services\goods.js 中添加是否上架/推荐商品接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import request from '@/utils/request'

// 获取商品列表
export async function getGoods(params) {
return request('/admin/goods', { params })
}

/**
* 上架和下架商品
* @param {商品id} goodsid
* @returns
*/
export async function isOn(goodsId) {
return request.patch(`/admin/goods/${goodsId}/on`)
}
/**
* 推荐和不推荐商品
* @param {商品id} goodsid
* @returns
*/
export async function isRecommend(goodsId) {
return request.patch(`/admin/goods/${goodsId}/recommend`)
}

6.2.4 添加是否上架/推荐商品方法

在\src\pages\Goods\index.jsx 中,先导入接口import { getGoods, isOn, isRecommend } from '@/services/goods';
修改并添加是否上架/推荐商品方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import React, { useRef, useState } from 'react'
import { PageContainer } from '@ant-design/pro-layout'
import ProTable from '@ant-design/pro-table'
import { Button, Image, Switch, message } from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import { getGoods, isOn, isRecommend } from '@/services/goods'
import CreateOrEdit from './components/CreateOrEdit'

const index = () => {
const [isModalVisible, setisModalVisible] = useState(false)
const [editId, setEditId] = useState(undefined)

// 表格的ref,便于操作自定义操作表格
const actionRef = useRef()

// 获取商品列表数据
const getData = async params => {
const response = await getGoods(params)

return {
data: response.data,
// success 请返回 true,
// 不然 table 会停止解析数据,即使有数据
success: true,
// 不传会使用 data 的长度,如果是分页一定要传
total: response.meta.pagination.total,
}
}

// 是否上架商品
const heandleIsOn = async goodsId => {
const response = await isOn(goodsId)
if (response.status === undefined) {
message.success('操作成功!')
}
}
// 是否推荐商品
const heandleIsRecommend = async goodsId => {
const response = await isRecommend(goodsId)
if (response.status === undefined) {
message.success('操作成功!')
}
}

// 控制新建/添加用户模态框显示和隐藏
const isShowModal = (show, id = undefined) => {
setEditId(id)
setisModalVisible(show)
}

const columns = [
{
title: '商品图',
dataIndex: 'cover_url',
hideInSearch: true,
render: (_, record) => (
<Image
width={64}
src={record.cover_url}
placeholder={<Image preview={false} src={record.cover_url} width={200} />}
/>
),
},
{
title: '标题',
dataIndex: 'title',
},
{
title: '价格',
dataIndex: 'price',
hideInSearch: true,
},
{
title: '库存',
dataIndex: 'stock',
hideInSearch: true,
},
{
title: '销量',
dataIndex: 'sales',
hideInSearch: true,
},

{
title: '是否上架',
dataIndex: 'is_on',
render: (_, record) => (
<Switch
checkedChildren="已上架"
unCheckedChildren="未上架"
defaultChecked={record.is_on === 1}
onChange={() => {
heandleIsOn(record.id)
}}
/>
),
valueType: 'radioButton',
valueEnum: {
1: { text: '已上架' },
0: { text: '未上架' },
},
},
{
title: '是否推荐',
dataIndex: 'is_recommend',
render: (_, record) => (
<Switch
checkedChildren="已推荐"
unCheckedChildren="未推荐"
defaultChecked={record.is_recommend === 1}
onChange={() => {
heandleIsRecommend(record.id)
}}
/>
),
valueType: 'radioButton',
valueEnum: {
1: { text: '已推荐' },
0: { text: '未推荐' },
},
},
{
title: '创建时间',
dataIndex: 'created_at',
hideInSearch: true,
},
{
title: '操作',
hideInSearch: true,
render: (_, record) => <a onClick={() => isShowModal(true, record.id)}>编辑</a>,
},
]

return (
<PageContainer>
<ProTable
columns={columns}
actionRef={actionRef}
request={async (params = {}) => getData(params)}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
pageSize: 10,
}}
dateFormatter="string"
headerTitle="商品列表"
toolBarRender={() => [
<Button
key="button"
icon={<PlusOutlined />}
type="primary"
onClick={() => isShowModal(true)}>
新建
</Button>,
]}
/>

{
// 模态框隐藏的时候,不挂载组件,显示的时候挂载组件,这是为了触发子组件的生命周期
!isModalVisible ? (
''
) : (
<CreateOrEdit
isModalVisible={isModalVisible}
isShowModal={isShowModal}
actionRef={actionRef}
editId={editId}
/>
)
}
</PageContainer>
)
}

export default index

6.3 新建商品页面

6.3.1 添加商品接口文档

接口描述
  • 添加商品
请求 URL
  • /api/admin/goods
请求方式
  • POST
请求头部
参数名 必选 类型 说明
Authorization string JWT token
Body 请求参数
参数名 必选 类型 说明
category_id int 分类
title string 标题
description string 描述
price int 价格
stock int 库存
cover string 封面图
pics array 小图集
details string 详情
返回示例
  • 状态码 201 创建成功
  • 状态码 400 请求错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
{
"message": "分类不存在",
"status_code": 400,
}

{
"message": "分类被禁用",
"status_code": 400,
}

{
"message": "只能向2级分类添加商品",
"status_code": 400,
}

状态码 422 参数错误
{
"message": "The given data was invalid.",
"errors": {
"title": [
"标题 不能为空。"
],
"category_id": [
"category id 不能为空。"
],
"description": [
"描述 不能为空。"
],
"price": [
"price 不能为空。"
],
"stock": [
"stock 不能为空。"
],
"cover": [
"cover 不能为空。"
],
"pics": [
"pics 不能为空。"
],
"details": [
"details 不能为空。"
]
},
"status_code": 422,
}

6.3.2 编辑添加商品页面

在\src\pages\Goods\components\CreateOrEdit.jsx 中,ProFormFields 表单项 参考文档。
只是简单完成添加商品必须的页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import React, { useEffect, useState } from 'react'
import ProForm, {
ProFormText,
ProFormTextArea,
ProFormDigit,
ProFormUploadButton,
} from '@ant-design/pro-form'
import { Modal, message, Skeleton } from 'antd'
import { showUser, updateUser, addUser } from '@/services/user'

const CreateOrEdit = props => {
/**
* isModalVisible 模态框是否显示
* isShowModal 操作模态框显示隐藏的方法
* actionRef 父组件传来的表格的引用,可以用来操作表格,比如刷新表单
* editId 要编辑的id,添加的时候是undefined,只有编辑时才有
*/
const { isModalVisible, isShowModal, actionRef, editId } = props

// 将表单初始化的值设置成状态,在编辑的时候使用这个状态
const [initialValues, setinitialValues] = useState(undefined)

// 添加或者编辑的描述
const type = editId === undefined ? '添加' : '编辑'

useEffect(async () => {
// 发送请求,获取用户详情
if (editId !== undefined) {
const response = await showUser(editId)
// 获取数据之后,修改状态;状态改变,组件重新渲染,骨架框消失,编辑表单出现
setinitialValues({
name: response.name,
email: response.email,
})
}
}, [])

// 提交表单,执行编辑或者添加
const handleSubmit = async values => {
let response = []
if (editId === undefined) {
// 执行添加
// 发送请求,添加用户
response = await addUser(values)
} else {
// 执行编辑
// 发送请求,更新用户
response = await updateUser(editId, values)
}
if (response.status === undefined) {
message.success(`${type}成功!`)
// 刷新表格数据
actionRef.current.reload()
// 关闭模态框
isShowModal(false)
}
}

return (
<Modal
title={`${type}商品`}
visible={isModalVisible}
onCancel={() => isShowModal(false)}
footer={null}
destroyOnClose={true}>
{
// 只有是编辑的情况下,并且要显示的数据还有返回,才显示骨架框
initialValues === undefined && editId !== undefined ? (
<Skeleton active={true} paragraph={{ rows: 4 }} />
) : (
<ProForm
initialValues={initialValues}
onFinish={values => {
handleSubmit(values)
}}>
<ProFormText
name="category_id"
label="分类"
placeholder="请输入分类"
rules={[{ required: true, message: '请输入分类' }]}
/>
<ProFormText
name="title"
label="商品名"
placeholder="请输入商品名"
rules={[{ required: true, message: '请输入商品名' }]}
/>
<ProFormTextArea
name="description"
label="描述"
placeholder="请输入商品描述"
rules={[{ required: true, message: '请输入商品描述' }]}
/>
<ProFormDigit
name="price"
label="价格"
placeholder="请输入商品价格"
min={0}
max={99999999}
rules={[{ required: true, message: '请输入商商品价格' }]}
/>
<ProFormDigit
name="stock"
label="库存"
placeholder="请输入商品库存"
min={0}
max={99999999}
rules={[{ required: true, message: '请输入商品库存' }]}
/>
<ProFormUploadButton
label="上传封面图"
name="cover"
action="upload.do"
rules={[{ required: true, message: '请选择商品主图' }]}
/>
<ProFormTextArea
name="details"
label="详情"
placeholder="请输入商品详情"
rules={[{ required: true, message: '请输入商品详情' }]}
/>
</ProForm>
)
}
</Modal>
)
}

export default CreateOrEdit

6.4 处理商品分类

6.4.1 商品分类接口文档

接口描述
  • 分类列表
请求 URL
  • /api/admin/category
请求方式
  • GET
请求头部
参数名 必选 类型 说明
Authorization string JWT token
Query 请求参数
参数名 必选 类型 说明
type string all 查所有分类,包含禁用的。不传则只返回非禁用的
返回参数
参数名 必含 类型 说明
id int 主键
pid int 父级
name string 名称
level int 层级
status int 状态: 0 正常 1 禁用
children array 子类
返回示例
  • 状态码 200 请求成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
;[
{
id: 1,
pid: 0,
name: '电子数码',
level: 1,
status: 1,
children: [
{
id: 3,
pid: 1,
name: '手机',
level: 2,
status: 1,
children: [
{
id: 5,
pid: 3,
name: '华为',
level: 3,
status: 1,
},
{
id: 6,
pid: 3,
name: '小米',
level: 3,
status: 1,
},
],
},
{
id: 4,
pid: 1,
name: '电脑',
level: 2,
status: 1,
children: [
{
id: 7,
pid: 4,
name: '戴尔',
level: 3,
status: 1,
},
],
},
],
},
{
id: 2,
pid: 0,
name: '服装衣帽',
level: 1,
status: 1,
children: [
{
id: 9,
pid: 2,
name: '男装',
level: 2,
status: 1,
children: [],
},
{
id: 10,
pid: 2,
name: '女装',
level: 2,
status: 1,
children: [],
},
],
},
]

6.4.2 添加分类列表接口–非禁用的分类

在 src\services 中新建一个category.js文件

1
2
3
4
5
6
import request from '@/utils/request'

// 获取分类列表-非禁用的分类
export async function getCategory() {
return request('/admin/category')
}

6.4.3 添加商品分类Select组件

在\src\pages\Goods\components\CreateOrEdit.jsx 中
设置options为空,储存后端返回的数据 const [options, setOptions] = useState([]);
在生命周期函数中useEffect请求查询分类数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
useEffect(async () => {
// 查询分类数据
const resCategory = await getCategory()
if (resCategory.status === undefined) setOptions(resCategory)

// 发送请求,获取用户详情
if (editId !== undefined) {
const response = await showUser(editId)
// 获取数据之后,修改状态;状态改变,组件重新渲染,骨架框消失,编辑表单出现
setinitialValues({
name: response.name,
email: response.email,
})
}
}, [])

其中需要加 Cascader 级联选择,还要加ProForm.Item标签包裹Cascader,设置分类的namerules等,ProFormFields表单项参考文档。
同时,后端返回来的字段和Cascader官方的文档字段不一样时,查看 API 文档fieldNames属性可以自定义字段

fieldNames 自定义 options 中 label name children 的字段 object { label: label, value: value, children: children }
1
2
3
4
5
6
7
<ProForm.Item name="category_id" label="分类" rules={[{ required: true, message: '请输入分类' }]}>
<Cascader
fieldNames={{ label: 'name', value: 'id' }}
options={options}
placeholder="请输入分类"
/>
</ProForm.Item>

然后导入import { getCategory } from '@/services/category';

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import React, { useEffect, useState } from 'react'
import ProForm, {
ProFormText,
ProFormTextArea,
ProFormDigit,
ProFormUploadButton,
} from '@ant-design/pro-form'
import { Modal, message, Skeleton, Cascader } from 'antd'
import { showUser, updateUser, addUser } from '@/services/user'
import { getCategory } from '@/services/category'

const CreateOrEdit = props => {
/**
* isModalVisible 模态框是否显示
* isShowModal 操作模态框显示隐藏的方法
* actionRef 父组件传来的表格的引用,可以用来操作表格,比如刷新表单
* editId 要编辑的id,添加的时候是undefined,只有编辑时才有
*/
const { isModalVisible, isShowModal, actionRef, editId } = props

// 将表单初始化的值设置成状态,在编辑的时候使用这个状态
const [initialValues, setinitialValues] = useState(undefined)
const [options, setOptions] = useState([])

// 添加或者编辑的描述
const type = editId === undefined ? '添加' : '编辑'

useEffect(async () => {
// 查询分类数据
const resCategory = await getCategory()
if (resCategory.status === undefined) setOptions(resCategory)

// 发送请求,获取用户详情
if (editId !== undefined) {
const response = await showUser(editId)
// 获取数据之后,修改状态;状态改变,组件重新渲染,骨架框消失,编辑表单出现
setinitialValues({
name: response.name,
email: response.email,
})
}
}, [])

// 提交表单,执行编辑或者添加
const handleSubmit = async values => {
let response = []
if (editId === undefined) {
// 执行添加
// 发送请求,添加用户
response = await addUser(values)
} else {
// 执行编辑
// 发送请求,更新用户
response = await updateUser(editId, values)
}
if (response.status === undefined) {
message.success(`${type}成功!`)
// 刷新表格数据
actionRef.current.reload()
// 关闭模态框
isShowModal(false)
}
}

return (
<Modal
title={`${type}商品`}
visible={isModalVisible}
onCancel={() => isShowModal(false)}
footer={null}
destroyOnClose={true}>
{
// 只有是编辑的情况下,并且要显示的数据还有返回,才显示骨架框
initialValues === undefined && editId !== undefined ? (
<Skeleton active={true} paragraph={{ rows: 4 }} />
) : (
<ProForm
initialValues={initialValues}
onFinish={values => {
handleSubmit(values)
}}>
<ProForm.Item
name="category_id"
label="分类"
rules={[{ required: true, message: '请输入分类' }]}>
<Cascader
fieldNames={{ label: 'name', value: 'id' }}
options={options}
placeholder="请输入分类"
/>
</ProForm.Item>
<ProFormText
name="title"
label="商品名"
placeholder="请输入商品名"
rules={[{ required: true, message: '请输入商品名' }]}
/>
<ProFormTextArea
name="description"
label="描述"
placeholder="请输入商品描述"
rules={[{ required: true, message: '请输入商品描述' }]}
/>
<ProFormDigit
name="price"
label="价格"
placeholder="请输入商品价格"
min={0}
max={99999999}
rules={[{ required: true, message: '请输入商商品价格' }]}
/>
<ProFormDigit
name="stock"
label="库存"
placeholder="请输入商品库存"
min={0}
max={99999999}
rules={[{ required: true, message: '请输入商品库存' }]}
/>
<ProFormUploadButton
label="上传封面图"
name="cover"
action="upload.do"
rules={[{ required: true, message: '请选择商品主图' }]}
/>
<ProFormTextArea
name="details"
label="详情"
placeholder="请输入商品详情"
rules={[{ required: true, message: '请输入商品详情' }]}
/>
</ProForm>
)
}
</Modal>
)
}

export default CreateOrEdit

6.5 封装 OSS 上传

6.5.1 获取阿里云 OSS Token 接口文档

接口描述
  • 获取阿里云 OSS Token,用于前端直传文件使用
请求 URL
  • /api/auth/oss/token
请求方式
  • GET
请求头部
参数名 必选 类型 说明
Authorization string JWT token
返回参数
参数名 必含 类型 说明
accessid string accessid
host string host
policy string policy
signature string signature
expire int expire
callback string callback
callback-var string callback-var
dir string dir
返回示例
  • 状态码 200 请求成功
1
2
3
4
5
6
7
8
9
10
{
"accessid": "C4jYcNjUFLSKHToP",
"host": "http:/laravel_shop_api.luwnto.oss-cn-beijing.aliyuncs.com/",
"policy": "eyJleHBpcmF0aW9uIjoiMjAyMC0xMi0yM1QwMToyMzo1OFoiLCJjb25kaXRpb25zIjpbWyJjb250ZW50LWxlbmd0aC1yYW5nZSIsMCwxMDQ4NTc2MDAwXSxbInN0YXJ0cy13aXRoIiwiJGtleSIsIiJdXX0=",
"signature": "Vx3jPcUQXVQ7rKSJvYRHyYCS5pA=",
"expire": 1608686638,
"callback": "eyJjYWxsYmFja1VybCI6IiIsImNhbGxiYWNrQm9keSI6ImJ1Y2tldD0ke2J1Y2tldH0mZXRhZz0ke2V0YWd9JmZpbGVuYW1lPSR7b2JqZWN0fSZzaXplPSR7c2l6ZX0mbWltZVR5cGU9JHttaW1lVHlwZX0maGVpZ2h0PSR7aW1hZ2VJbmZvLmhlaWdodH0md2lkdGg9JHtpbWFnZUluZm8ud2lkdGh9JmZvcm1hdD0ke2ltYWdlSW5mby5mb3JtYXR9IiwiY2FsbGJhY2tCb2R5VHlwZSI6ImFwcGxpY2F0aW9uXC94LXd3dy1mb3JtLXVybGVuY29kZWQifQ==",
"callback-var": [],
"dir": ""
}

6.5.2 添加阿里云 OSS Token 接口

在\src\services 文件夹中新建一个commom.js

1
2
3
4
5
6
7
8
9
import request from '@/utils/request'

/**
* 获取oss上传策略和签名
* @returns
*/
export function ossConfig() {
return request('/auth/oss/token')
}

6.5.3 初步封装公共AliyunOSSUpload组件

在 src\components 中新建AliyunOSSUpload文件夹index.jsx文件
Upload参考文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import React from 'react'
import { Form, Upload, message, Button } from 'antd'
import { UploadOutlined } from '@ant-design/icons'
import { ossConfig } from '@/services/commom'

export default class AliyunOSSUpload extends React.Component {
state = {
OSSData: {},
}

async componentDidMount() {
await this.init()
}

// 初始化获取oss上传签名
init = async () => {
try {
const OSSData = await ossConfig()

this.setState({
OSSData,
})
} catch (error) {
message.error(error)
}
}

// 文件上传过程中触发的回调函数,直到上传完成
onChange = ({ fileList }) => {
const { onChange } = this.props
console.log('Aliyun OSS:', fileList)
if (onChange) {
onChange([...fileList])
}
}

// 额外的上传参数
getExtraData = file => {
const { OSSData } = this.state

return {
key: file.url,
OSSAccessKeyId: OSSData.accessid, // 注意查看后端返回的字段是否和官方的OSSData一致
policy: OSSData.policy,
Signature: OSSData.signature,
}
}

// 选择文件之后,上传文件之前,执行的回调
beforeUpload = async file => {
const { OSSData } = this.state
const expire = OSSData.expire * 1000

if (expire < Date.now()) {
await this.init()
}

const suffix = file.name.slice(file.name.lastIndexOf('.'))
const filename = Date.now() + suffix
file.url = OSSData.dir + filename

return file
}

render() {
const { value } = this.props
const props = {
name: 'file',
fileList: value,
action: this.state.OSSData.host,
onChange: this.onChange,
// onRemove: this.onRemove,
data: this.getExtraData,
beforeUpload: this.beforeUpload,
listType: 'picture',
maxCount: 1,
}
return (
<Upload {...props}>
<Button icon={<UploadOutlined />}>Click to Upload</Button>
</Upload>
)
}
}

其中注意额外上传的参数,后端返回的字段是否和阿里云 OSS 字段一致,

6.5.4 在新建商品模态框中使用AliyunOSSUpload组件

在\src\pages\Goods\components\CreateOrEdit.jsx 中,先导入AliyunOSSUpload,最后在上传封面图标签后添加<AliyunOSSUpload />标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import React, { useEffect, useState } from 'react'
import ProForm, {
ProFormText,
ProFormTextArea,
ProFormDigit,
ProFormUploadButton,
} from '@ant-design/pro-form'
import { Modal, message, Skeleton, Cascader } from 'antd'
import { showUser, updateUser, addUser } from '@/services/user'
import { getCategory } from '@/services/category'
import AliyunOSSUpload from '@/components/AliyunOSSUpload'

const CreateOrEdit = props => {
/**
* isModalVisible 模态框是否显示
* isShowModal 操作模态框显示隐藏的方法
* actionRef 父组件传来的表格的引用,可以用来操作表格,比如刷新表单
* editId 要编辑的id,添加的时候是undefined,只有编辑时才有
*/
const { isModalVisible, isShowModal, actionRef, editId } = props

// 将表单初始化的值设置成状态,在编辑的时候使用这个状态
const [initialValues, setinitialValues] = useState(undefined)
const [options, setOptions] = useState([])

// 添加或者编辑的描述
const type = editId === undefined ? '添加' : '编辑'

useEffect(async () => {
// 查询分类数据
const resCategory = await getCategory()
if (resCategory.status === undefined) setOptions(resCategory)

// 发送请求,获取用户详情
if (editId !== undefined) {
const response = await showUser(editId)
// 获取数据之后,修改状态;状态改变,组件重新渲染,骨架框消失,编辑表单出现
setinitialValues({
name: response.name,
email: response.email,
})
}
}, [])

// 提交表单,执行编辑或者添加
const handleSubmit = async values => {
let response = []
if (editId === undefined) {
// 执行添加
// 发送请求,添加用户
response = await addUser(values)
} else {
// 执行编辑
// 发送请求,更新用户
response = await updateUser(editId, values)
}
if (response.status === undefined) {
message.success(`${type}成功!`)
// 刷新表格数据
actionRef.current.reload()
// 关闭模态框
isShowModal(false)
}
}

return (
<Modal
title={`${type}商品`}
visible={isModalVisible}
onCancel={() => isShowModal(false)}
footer={null}
destroyOnClose={true}>
{
// 只有是编辑的情况下,并且要显示的数据还有返回,才显示骨架框
initialValues === undefined && editId !== undefined ? (
<Skeleton active={true} paragraph={{ rows: 4 }} />
) : (
<ProForm
initialValues={initialValues}
onFinish={values => {
handleSubmit(values)
}}>
<ProForm.Item
name="category_id"
label="分类"
rules={[{ required: true, message: '请输入分类' }]}>
<Cascader
fieldNames={{ label: 'name', value: 'id' }}
options={options}
placeholder="请输入分类"
/>
</ProForm.Item>
<ProFormText
name="title"
label="商品名"
placeholder="请输入商品名"
rules={[{ required: true, message: '请输入商品名' }]}
/>
<ProFormTextArea
name="description"
label="描述"
placeholder="请输入商品描述"
rules={[{ required: true, message: '请输入商品描述' }]}
/>
<ProFormDigit
name="price"
label="价格"
placeholder="请输入商品价格"
min={0}
max={99999999}
rules={[{ required: true, message: '请输入商商品价格' }]}
/>
<ProFormDigit
name="stock"
label="库存"
placeholder="请输入商品库存"
min={0}
max={99999999}
rules={[{ required: true, message: '请输入商品库存' }]}
/>
<ProFormUploadButton
label="上传封面图"
name="cover"
action="upload.do"
rules={[{ required: true, message: '请选择商品主图' }]}
/>

<AliyunOSSUpload />
<ProFormTextArea
name="details"
label="详情"
placeholder="请输入商品详情"
rules={[{ required: true, message: '请输入商品详情' }]}
/>
</ProForm>
)
}
</Modal>
)
}

export default CreateOrEdit

6.5.5 简单封装优化AliyunOSSUpload组件

1.将原来上传图片的ProFormUploadButton组件替换成AliyunOSSUpload组件

在\src\pages\Goods\components\CreateOrEdit.jsx 中,将原来上传图片的ProFormUploadButton组件替换为AliyunOSSUpload组件,添加验证规则并写成双标签,将在其中显示的内容写入。

1
2
3
4
5
6
<ProForm.Item
name="cover"
label="上传商品主图"
rules={[{ required: true, message: '请选择商品主图' }]}>
<AliyunOSSUpload>点击上传商品主图</AliyunOSSUpload>
</ProForm.Item>

2.修改OSSData文件上传路径,简单封装优化AliyunOSSUpload组件

在\src\components\AliyunOSSUpload\index.jsx 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import React from 'react'
import { Upload, message, Button } from 'antd'
import { UploadOutlined } from '@ant-design/icons'
import { ossConfig } from '@/services/commom'

export default class AliyunOSSUpload extends React.Component {
state = {
OSSData: {},
}

// 组件挂载完成后,进行初始化,获取oss配置
async componentDidMount() {
await this.init()
}

// 初始化获取oss上传签名
init = async () => {
try {
const OSSData = await ossConfig()

this.setState({
OSSData,
})
} catch (error) {
message.error(error)
}
}

// 文件上传过程中触发的回调函数,直到上传完成
onChange = ({ file }) => {
if (file.status === 'done') message.success('上传成功!')
}

// 额外的上传参数
getExtraData = file => {
const { OSSData } = this.state

return {
key: file.key,
OSSAccessKeyId: OSSData.accessid, // 注意查看后端返回的字段是否和官方的OSSData一致
policy: OSSData.policy,
Signature: OSSData.signature,
}
}

// 选择文件之后,上传文件之前,执行的回调
beforeUpload = async file => {
const { OSSData } = this.state
const expire = OSSData.expire * 1000

// 如果签名过期了就重新获取
if (expire < Date.now()) {
await this.init()
}

const dir = 'react/' // 定义上传的目录

const suffix = file.name.slice(file.name.lastIndexOf('.'))
const filename = OSSData.dir + dir + Date.now() + suffix
file.key = OSSData.dir + dir + filename // 在getExtraData 函数中会用到,在云存储的文件的 key
file.url = OSSData.host + OSSData.dir + dir + filename // 上传完成后,用于显示内容

return file
}

render() {
const { value } = this.props
const props = {
name: 'file',
fileList: value,
action: this.state.OSSData.host,
onChange: this.onChange,
// onRemove: this.onRemove,
data: this.getExtraData,
beforeUpload: this.beforeUpload,
listType: 'picture',
maxCount: 1,
}

return (
<Upload {...props}>
{/* 将Button标签放在在AliyunOSSUpload组件里写,这里直接使用{this.props.children},会报错 */}
{/* 这里的解决方案是,Button标签封装在AliyunOSSUpload组件内部,其他函数使用AliyunOSSUpload组件时,只需要将AliyunOSSUpload写成双标签,里边写显示的文字 */}
<Button icon={<UploadOutlined />}>{this.props.children}</Button>
</Upload>
)
}
}

修复上传主图,显示默认文字 bug
其中Upload内部直接写{this.props.children}获取父组件的内容,无法渲染会报错,最后只需要在父组件中将AliyunOSSUpload写成双标签,里边写显示的文字。将Button封装在AliyunOSSUpload组件内部,自取显示内容即可解决

1
2
3
4
5
<Upload {...props}>
{/* 将Button标签放在在AliyunOSSUpload组件里写,这里直接使用{this.props.children},会报错 */}
{/* 这里的解决方案是,Button标签封装在AliyunOSSUpload组件内部,其他函数使用AliyunOSSUpload组件时,只需要将AliyunOSSUpload写成双标签,里边写显示的文字 */}
<Button icon={<UploadOutlined />}>{this.props.children}</Button>
</Upload>

后期富文本编辑器将显示默认文字 bug解决了,可以将button封装在AliyunOSSUpload

3.限制上传文件类型为图片

在\src\components\AliyunOSSUpload\index.jsx 中,解构accept,并且设置accept的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
render() {
const { value, accept } = this.props;
const props = {
accept: accept || '',
name: 'file',
fileList: value,
action: this.state.OSSData.host,
onChange: this.onChange,
// onRemove: this.onRemove,
data: this.getExtraData,
beforeUpload: this.beforeUpload,
listType: 'picture',
maxCount: 1,
};

return (
<Upload {...props}>
{/* 将Button标签放在在AliyunOSSUpload组件里写,这里直接使用{this.props.children},会报错 */}
{/* 这里的解决方案是,Button标签封装在AliyunOSSUpload组件内部,其他函数使用AliyunOSSUpload组件时,只需要将AliyunOSSUpload写成双标签,里边写显示的文字 */}
<Button icon={<UploadOutlined />}>{this.props.children}</Button>
</Upload>
);
}

在\src\pages\Goods\components\CreateOrEdit.jsx 中,AliyunOSSUpload标签中设置accept属性,
accept参考文档,详情

1
2
3
4
5
6
<ProForm.Item
name="cover"
label="上传商品主图"
rules={[{ required: true, message: '请选择商品主图' }]}>
<AliyunOSSUpload accept="image/*">点击上传商品主图</AliyunOSSUpload>
</ProForm.Item>

但是其中也有一个 bug,ProForm.Item组件和我们封装的AliyunOSSUpload组件(或者第三方组件)并不关联,ProForm.Item当进行表单验证的时候,并没有包括AliyunOSSUpload。所以当文件上传成功之后,把文件的 key,设置成表单某个字段的值。

4.关联ProForm.ItemAliyunOSSUpload,完成图片验证

使用通用方式完成文件验证以及解除组件受控

通过 Form.useForm 对表单数据域进行交互。
在\src\pages\Goods\components\CreateOrEdit.jsx 中,为ProForm标签添加form={formObj}控制实例

1
2
3
4
5
6
7
<ProForm
form={formObj}
initialValues={initialValues}
onFinish={(values) => {
handleSubmit(values);
}}
>

定义Form实例和setCoverKey方法,用于当文件上传之后设置cover字段的value

1
2
3
4
5
// 定义Form实例,用来操作表单
const [formObj] = ProForm.useForm()

// 文件上传成功后,设置cover字段的value
const setCoverKey = fileKey => formObj.setFieldsValue({ cover: fileKey })

AliyunOSSUpload组件中传入setCoverKey方法

1
2
3
<AliyunOSSUpload setCoverKey={setCoverKey} accept="image/*">
点击上传商品主图
</AliyunOSSUpload>

在\src\components\AliyunOSSUpload\index.jsx 中设置上传文件的回调函数,将文件的key设置成文件某个字段的值。

1
2
3
4
5
6
7
8
// 文件上传过程中触发的回调函数,直到上传完成
onChange = ({ file }) => {
if (file.status === 'done') {
// 上传成功之后,把文件的key,设置成表单某个字段的值
this.props.setCoverKey(file.key)
message.success('上传成功')
}
}

当点击上传文件时会报错
image.png
原因是当我们文件上传过程中触发的回调函数时通过 // 文件上传成功后,设置cover字段的value const setCoverKey = (fileKey) => formObj.setFieldsValue({ cover: fileKey });设置了ProForm.Itemname="cover"的值,ProForm.Item组件和AliyunOSSUpload组件形成了受控组件,value值被设置了,但是上传过程中触发的回调函数检测到文件还没有,拿不到文件就会报错。
查看Form.Item的 api 就有介绍
image.png
解决办法:用div标签将AliyunOSSUpload组件包裹,div成为ProForm.Item的第一个子组件,他们两个形成受控组件,div受它控制。AliyunOSSUpload组件可以验证但是不受控,就解决了这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import React, { useEffect, useState } from 'react'
import ProForm, {
ProFormText,
ProFormTextArea,
ProFormDigit,
ProFormUploadButton,
UploadOutlined,
} from '@ant-design/pro-form'
import { Modal, message, Skeleton, Cascader, Button } from 'antd'
import { showUser, updateUser, addUser } from '@/services/user'
import { getCategory } from '@/services/category'
import AliyunOSSUpload from '@/components/AliyunOSSUpload'

const CreateOrEdit = props => {
/**
* isModalVisible 模态框是否显示
* isShowModal 操作模态框显示隐藏的方法
* actionRef 父组件传来的表格的引用,可以用来操作表格,比如刷新表单
* editId 要编辑的id,添加的时候是undefined,只有编辑时才有
*/
const { isModalVisible, isShowModal, actionRef, editId } = props

// 将表单初始化的值设置成状态,在编辑的时候使用这个状态
const [initialValues, setinitialValues] = useState(undefined)
const [options, setOptions] = useState([])

// 定义Form实例,用来操作表单
const [formObj] = ProForm.useForm()

// 文件上传成功后,设置cover字段的value
const setCoverKey = fileKey => formObj.setFieldsValue({ cover: fileKey })

// 添加或者编辑的描述
const type = editId === undefined ? '添加' : '编辑'

useEffect(async () => {
// 查询分类数据
const resCategory = await getCategory()
if (resCategory.status === undefined) setOptions(resCategory)

// 发送请求,获取用户详情
if (editId !== undefined) {
const response = await showUser(editId)
// 获取数据之后,修改状态;状态改变,组件重新渲染,骨架框消失,编辑表单出现
setinitialValues({
name: response.name,
email: response.email,
})
}
}, [])

// 提交表单,执行编辑或者添加
const handleSubmit = async values => {
let response = []
if (editId === undefined) {
// 执行添加
// 发送请求,添加用户
response = await addUser(values)
} else {
// 执行编辑
// 发送请求,更新用户
response = await updateUser(editId, values)
}
if (response.status === undefined) {
message.success(`${type}成功!`)
// 刷新表格数据
actionRef.current.reload()
// 关闭模态框
isShowModal(false)
}
}

return (
<Modal
title={`${type}商品`}
visible={isModalVisible}
onCancel={() => isShowModal(false)}
footer={null}
destroyOnClose={true}>
{
// 只有是编辑的情况下,并且要显示的数据还有返回,才显示骨架框
initialValues === undefined && editId !== undefined ? (
<Skeleton active={true} paragraph={{ rows: 4 }} />
) : (
<ProForm
form={formObj}
initialValues={initialValues}
onFinish={values => {
handleSubmit(values)
}}>
<ProForm.Item
name="category_id"
label="分类"
rules={[{ required: true, message: '请输入分类' }]}>
<Cascader
fieldNames={{ label: 'name', value: 'id' }}
options={options}
placeholder="请输入分类"
/>
</ProForm.Item>
<ProFormText
name="title"
label="商品名"
placeholder="请输入商品名"
rules={[{ required: true, message: '请输入商品名' }]}
/>
<ProFormTextArea
name="description"
label="描述"
placeholder="请输入商品描述"
rules={[{ required: true, message: '请输入商品描述' }]}
/>
<ProFormDigit
name="price"
label="价格"
placeholder="请输入商品价格"
min={0}
max={99999999}
rules={[{ required: true, message: '请输入商商品价格' }]}
/>
<ProFormDigit
name="stock"
label="库存"
placeholder="请输入商品库存"
min={0}
max={99999999}
rules={[{ required: true, message: '请输入商品库存' }]}
/>

<ProForm.Item
name="cover"
label="上传商品主图"
rules={[{ required: true, message: '请选择商品主图' }]}>
<div>
<AliyunOSSUpload setCoverKey={setCoverKey} accept="image/*">
点击上传商品主图
</AliyunOSSUpload>
</div>
</ProForm.Item>

<ProFormTextArea
name="details"
label="详情"
placeholder="请输入商品详情"
rules={[{ required: true, message: '请输入商品详情' }]}
/>
</ProForm>
)
}
</Modal>
)
}

export default CreateOrEdit

6.6 使用富文本编辑器

6.6.1 安装富文本编辑器

富文本编辑器文档在antd官网=>组件=>社区精选组件可以找到

我们选择的是braft-editor

1
yarn add braft-editor

6.6.2 封装富文本编辑器

1.简单封装文本编辑器

在\src\components 文件夹中,新建Editor文件夹,并在Editor下新建 index.jsxindex.less文件
在\src\components\Editor\index.less 中设置基本样式

1
2
3
4
.my-component {
border: 1px solid #d1d1d1;
border-radius: 5px;
}

参考braft-editor官方手册将EditorDemo拷贝,将不用的暂时注释掉,并引入样式import './index.less';

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import React from 'react'
// 引入编辑器组件
import BraftEditor from 'braft-editor'
// 引入编辑器样式
import 'braft-editor/dist/index.css'
import './index.less'

export default class EditorDemo extends React.Component {
state = {
// 创建一个空的editorState作为初始值
editorState: BraftEditor.createEditorState(null),
}

// async componentDidMount() {
// // 假设此处从服务端获取html格式的编辑器内容
// const htmlContent = await fetchEditorContent();
// // 使用BraftEditor.createEditorState将html字符串转换为编辑器需要的editorStat
// this.setState({
// editorState: BraftEditor.createEditorState(htmlContent),
// });
// }

// submitContent = async () => {
// // 在编辑器获得焦点时按下ctrl+s会执行此方法
// // 编辑器内容提交到服务端之前,可直接调用editorState.toHTML()来获取HTML格式的内容
// const htmlContent = this.state.editorState.toHTML();
// const result = await saveEditorContent(htmlContent);
// };

handleEditorChange = editorState => {
this.setState({ editorState })
}

render() {
const { editorState } = this.state
return (
<div className="my-component">
<BraftEditor
value={editorState}
onChange={this.handleEditorChange}
// onSave={this.submitContent}
/>
</div>
)
}
}

2.使用富文本编辑器

在\src\pages\Goods\components\CreateOrEdit.jsx 中引入import Editor from '@/components/Editor';
将原来商品详情ProFormTextArea组件的换成ProForm.Item组件并使用 <Editor />

1
2
3
4
5
6
7
<ProForm.Item
name="details"
label="商品详情"
placeholder="请输入商品详情"
rules={[{ required: true, message: '请输入商品详情' }]}>
<Editor />
</ProForm.Item>

3.处理富文本编辑器表单验证

在\src\pages\Goods\components\CreateOrEdit.jsx 中,给Editor传入一个设置details字段的value的方法
将富文本输入的内容设置成details字段的value,并添加这个方法

1
2
// 文件上传成功后,设置details字段的value
const setDetails = content => formObj.setFieldsValue({ details: content })
1
2
3
4
5
6
7
8
9
    <ProForm.Item
name="details"
label="商品详情"
rules={[{ required: true, message: '请输入商品详情' }]}
>
<Editor setDetails={setDetails} />
</ProForm.Item>
</ProForm>
)

在\src\components\Editor\index.jsx 中,接调用 editorState.toHTML()来获取 HTML 格式的内容,调用父组件的函数,将编辑器输入的内容传递回去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
handleEditorChange = editorState => {
// 更新编辑器的状态
this.setState({ editorState })
// 要判断输入的内容,如果有内容设置输入的内容;如果没有内容设置成空字符串
// 为什么要这样判断,因为即使是空内容editorState.toHTML()也是一对空标签,不能直接给表单使用
if (!editorState.isEmpty()) {
// 可直接调用editorState.toHTML()来获取HTML格式的内容
const content = editorState.toHTML()
// 调用父组件的函数,将编辑器输入的内容传递回去
this.props.setDetails(content)
} else {
this.props.setDetails('')
}
}

其中当富文本获取到焦点时,并没有写入任何内容,但是editorState.toHTML()也是一对空标签<p></p>,不能直接给表单使用
调用editorState.isEmpty()会判断是否为空,没有写入任何内容会返回 true,并设置成空字符串

6.6.3 富文本编辑器集成阿里 OSS 上传

1.自定义控件–插入图片

集成Ant Design上传组件
在\src\components\Editor\index.jsx 中,引入自定义控件–插入图片的例子,适当修改。
AliyunOSSUpload组件添加insertImage方法,图片上传完成后执行此方法,将url传给父组件用来在编译器中显示图片。showUploadList用来控制是否展示文件列表 showUploadList文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import 'braft-editor/dist/index.css'
import React from 'react'
// 引入编辑器组件
import BraftEditor from 'braft-editor'
// 引入编辑器样式
import 'braft-editor/dist/index.css'
import './index.less'
import AliyunOSSUpload from '@/components/AliyunOSSUpload'
import { ContentUtils } from 'braft-utils'

export default class EditorDemo extends React.Component {
state = {
// 创建一个空的editorState作为初始值
editorState: BraftEditor.createEditorState(null),
}

// async componentDidMount() {
// // 假设此处从服务端获取html格式的编辑器内容
// const htmlContent = await fetchEditorContent();
// // 使用BraftEditor.createEditorState将html字符串转换为编辑器需要的editorStat
// this.setState({
// editorState: BraftEditor.createEditorState(htmlContent),
// });
// }

// 编辑器内容改变的时候执行
handleEditorChange = editorState => {
// 更新编辑器的状态
this.setState({ editorState })
// 要判断输入的内容,如果有内容设置输入的内容;如果没有内容设置成空字符串
// 为什么要这样判断,因为即使是空内容editorState.toHTML()也是一对空标签,不能直接给表单使用
if (!editorState.isEmpty()) {
// 可直接调用editorState.toHTML()来获取HTML格式的内容
const content = editorState.toHTML()
// 调用父组件的函数,将编辑器输入的内容传递回去
this.props.setDetails(content)
} else {
this.props.setDetails('')
}
}

// 图片上传完成后执行此方法,用来在编译器中显示图片
insertImage = url => {
this.setState({
editorState: ContentUtils.insertMedias(this.state.editorState, [
{
type: 'IMAGE',
url,
},
]),
})
}

render() {
// 自定义控件--插入图片
const extendControls = [
{
key: 'antd-uploader',
type: 'component',
component: (
<AliyunOSSUpload insertImage={this.insertImage} accept="image/*" showUploadList={false}>
{/* 这里的按钮最好加上type="button",以避免在表单容器中触发表单提交,用Antd的Button组件则无需如此 */}
<button
type="button"
className="control-item button upload-button"
data-title="插入图片">
插入图片
</button>
</AliyunOSSUpload>
),
},
]

const { editorState } = this.state
return (
<div className="my-component">
<BraftEditor
value={editorState}
onChange={this.handleEditorChange}
extendControls={extendControls}
/>
</div>
)
}
}

2.添加富文本图片显示,修复显示默认文字 bug

在\src\components\AliyunOSSUpload\index.jsx 中,insertImage(file.url)在文件上传完成之后,如果需要 url,那么返回 url 给父组件。
添加解构showUploadList,默认展示文件列表,
修复直接使用{this.props.children},会报错的 bug

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import React from 'react'
import { Upload, message } from 'antd'
import { ossConfig } from '@/services/commom'
export default class AliyunOSSUpload extends React.Component {
state = {
OSSData: {},
}

// 组件挂载完成后,进行初始化获取oss配置
async componentDidMount() {
await this.init()
}

// 初始化获取oss上传签名
init = async () => {
try {
const OSSData = await ossConfig()

this.setState({
OSSData,
})
} catch (error) {
message.error(error)
}
}

// 文件上传过程中触发的回调函数,直到上传完成
onChange = ({ file }) => {
if (file.status === 'done') {
const { setCoverKey, insertImage } = this.props
if (setCoverKey) {
// 上传成功之后,把文件的key,设置成表单某个字段的值
setCoverKey(file.key)
}

// 上传完成之后,如果需要url,那么返回url给父组件
if (insertImage) {
insertImage(file.url)
}

message.success('上传成功')
}
}

// 额外的上传参数
getExtraData = file => {
const { OSSData } = this.state

return {
key: file.key,
OSSAccessKeyId: OSSData.accessid, // 注意查看后端返回的字段是否和官方的OSSData一致
policy: OSSData.policy,
Signature: OSSData.signature,
}
}

// 选择文件之后,上传文件之前,执行的回调
beforeUpload = async file => {
const { OSSData } = this.state
const expire = OSSData.expire * 1000

// 如果签名过期了就重新获取
if (expire < Date.now()) {
await this.init()
}

const dir = 'react/' // 定义上传的目录

const suffix = file.name.slice(file.name.lastIndexOf('.'))
const filename = OSSData.dir + dir + Date.now() + suffix
file.key = OSSData.dir + dir + filename // 在getExtraData 函数中会用到,在云存储的文件的 key
file.url = OSSData.host + OSSData.dir + dir + filename // 上传完成后,用于显示内容

return file
}

render() {
const { value, accept, showUploadList } = this.props
const props = {
accept: accept || '',
name: 'file',
fileList: value,
action: this.state.OSSData.host,
onChange: this.onChange,
// onRemove: this.onRemove,
data: this.getExtraData,
beforeUpload: this.beforeUpload,
listType: 'picture',
maxCount: 1,
showUploadList,
}

return (
<Upload {...props}>
{/* 将Button标签放在在AliyunOSSUpload组件里写,这里直接使用{this.props.children},会报错 */}
{/* 这里的解决方案是,Button标签封装在AliyunOSSUpload组件内部,其他函数使用AliyunOSSUpload组件时,只需要将AliyunOSSUpload写成双标签,里边写显示的文字 */}
{/* <Button icon={<UploadOutlined />}>{this.props.children}</Button> */}
{/* 修复直接使用{this.props.children},会报错的bug */}
{this.props.children}
</Upload>
)
}
}

在\src\pages\Goods\components\CreateOrEdit.jsx 中,给AliyunOSSUpload组件传值showUploadList={true}显示文件图片,并将ButtonAliyunOSSUpload中写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import React, { useEffect, useState } from 'react'
import ProForm, { ProFormText, ProFormTextArea, ProFormDigit } from '@ant-design/pro-form'
import { Modal, message, Skeleton, Cascader, Button } from 'antd'
import { UploadOutlined } from '@ant-design/icons'
import { showUser, updateUser, addUser } from '@/services/user'
import { getCategory } from '@/services/category'
import AliyunOSSUpload from '@/components/AliyunOSSUpload'
import Editor from '@/components/Editor'

const CreateOrEdit = props => {
/**
* isModalVisible 模态框是否显示
* isShowModal 操作模态框显示隐藏的方法
* actionRef 父组件传来的表格的引用,可以用来操作表格,比如刷新表单
* editId 要编辑的id,添加的时候是undefined,只有编辑时才有
*/
const { isModalVisible, isShowModal, actionRef, editId } = props

// 将表单初始化的值设置成状态,在编辑的时候使用这个状态
const [initialValues, setinitialValues] = useState(undefined)
const [options, setOptions] = useState([])

// 定义Form实例,用来操作表单
const [formObj] = ProForm.useForm()

// 文件上传成功后,设置cover字段的value
const setCoverKey = fileKey => formObj.setFieldsValue({ cover: fileKey })

// 文件上传成功后,设置details字段的value
const setDetails = content => formObj.setFieldsValue({ details: content })

// 添加或者编辑的描述
const type = editId === undefined ? '添加' : '编辑'

useEffect(async () => {
// 查询分类数据
const resCategory = await getCategory()
if (resCategory.status === undefined) setOptions(resCategory)

// 发送请求,获取用户详情
if (editId !== undefined) {
const response = await showUser(editId)
// 获取数据之后,修改状态;状态改变,组件重新渲染,骨架框消失,编辑表单出现
setinitialValues({
name: response.name,
email: response.email,
})
}
}, [])

// 提交表单,执行编辑或者添加
const handleSubmit = async values => {
let response = []
if (editId === undefined) {
// 执行添加
// 发送请求,添加用户
response = await addUser(values)
} else {
// 执行编辑
// 发送请求,更新用户
response = await updateUser(editId, values)
}
if (response.status === undefined) {
message.success(`${type}成功!`)
// 刷新表格数据
actionRef.current.reload()
// 关闭模态框
isShowModal(false)
}
}

return (
<Modal
title={`${type}商品`}
visible={isModalVisible}
onCancel={() => isShowModal(false)}
footer={null}
destroyOnClose={true}>
{
// 只有是编辑的情况下,并且要显示的数据还有返回,才显示骨架框
initialValues === undefined && editId !== undefined ? (
<Skeleton active={true} paragraph={{ rows: 4 }} />
) : (
<ProForm
form={formObj}
initialValues={initialValues}
onFinish={values => {
handleSubmit(values)
}}>
<ProForm.Item
name="category_id"
label="分类"
rules={[{ required: true, message: '请输入分类' }]}>
<Cascader
fieldNames={{ label: 'name', value: 'id' }}
options={options}
placeholder="请输入分类"
/>
</ProForm.Item>
<ProFormText
name="title"
label="商品名"
placeholder="请输入商品名"
rules={[{ required: true, message: '请输入商品名' }]}
/>
<ProFormTextArea
name="description"
label="描述"
placeholder="请输入商品描述"
rules={[{ required: true, message: '请输入商品描述' }]}
/>
<ProFormDigit
name="price"
label="价格"
placeholder="请输入商品价格"
min={0}
max={99999999}
rules={[{ required: true, message: '请输入商商品价格' }]}
/>
<ProFormDigit
name="stock"
label="库存"
placeholder="请输入商品库存"
min={0}
max={99999999}
rules={[{ required: true, message: '请输入商品库存' }]}
/>

<ProForm.Item
name="cover"
label="上传商品主图"
rules={[{ required: true, message: '请选择商品主图' }]}>
<div>
<AliyunOSSUpload setCoverKey={setCoverKey} accept="image/*" showUploadList={true}>
<Button icon={<UploadOutlined />}>点击上传商品主图</Button>
</AliyunOSSUpload>
</div>
</ProForm.Item>

<ProForm.Item
name="details"
label="商品详情"
rules={[{ required: true, message: '请输入商品详情' }]}>
<Editor setDetails={setDetails} />
</ProForm.Item>
</ProForm>
)
}
</Modal>
)
}

export default CreateOrEdit

6.7 添加商品

6.7.1 添加商品接口文档

接口描述
  • 添加商品
请求 URL
  • /api/admin/goods
请求方式
  • POST
请求头部
参数名 必选 类型 说明
Authorization string JWT token
Body 请求参数
参数名 必选 类型 说明
category_id int 分类
title string 标题
description string 描述
price int 价格
stock int 库存
cover string 封面图
pics array 小图集
details string 详情
返回示例
  • 状态码 201 创建成功
  • 状态码 400 请求错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"message": "The given data was invalid.",
"errors": {
"title": [
"标题 不能为空。"
],
"category_id": [
"category id 不能为空。"
],
"description": [
"描述 不能为空。"
],
"price": [
"price 不能为空。"
],
"stock": [
"stock 不能为空。"
],
"cover": [
"cover 不能为空。"
],
"pics": [
"pics 不能为空。"
],
"details": [
"details 不能为空。"
]
},
"status_code": 422,
}

6.7.2 添加添加商品接口

在\src\services\goods.js 中,添加商品接口和获取商品列表接口是同一个接口,但是获取商品列表是get请求,添加商品是post

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import request from '@/utils/request'

// 获取商品列表
export async function getGoods(params) {
return request('/admin/goods', { params })
}

/**
* 上架和下架商品
* @param {商品id} goodsid
* @returns
*/
export async function isOn(goodsId) {
return request.patch(`/admin/goods/${goodsId}/on`)
}
/**
* 推荐和不推荐商品
* @param {商品id} goodsid
* @returns
*/
export async function isRecommend(goodsId) {
return request.patch(`/admin/goods/${goodsId}/recommend`)
}

// 添加商品
export async function addGoods(params) {
return request.post('/admin/goods', { params })
}

在\src\pages\Goods\components\CreateOrEdit.jsx 中,引入import { addGoods } from '@/services/goods';添加商品接口
在提交表单时,执行添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 提交表单,执行编辑或者添加
const handleSubmit = async values => {
console.log(values)
let response = []
if (editId === undefined) {
// 执行添加
// 发送请求,添加商品
response = await addGoods({ ...values, category_id: values.category_id[1] })
} else {
// 执行编辑
// 发送请求,更新商品
// response = await updateUser(editId, values);
}
if (response.status === undefined) {
message.success(`${type}成功!`)
// 刷新表格数据
actionRef.current.reload()
// 关闭模态框
isShowModal(false)
}
}

image.png
其中我们要添加category_id二级分类的商品在 response = await addGoods({ ...values, category_id: values.category_id[1] });中,我们先将...values展开,随后再处理二级分类的商品

6.8 修改商品

6.8.1 商品详情接口文档、修改商品接口文档

接口描述
  • 商品详情
请求 URL
  • /api/admin/goods/{good}
请求方式
  • GET
请求头部
参数名 必选 类型 说明
Authorization string JWT token
REST 参数
参数名 必选 类型 说明
good int 商品 ID
Query 请求参数
参数名 必选 类型 说明
include string 包含额外的数据: category 分类,user 用户, comments 评论

inlude 可以返回额外的数据, 多个使用 , 分隔, 比如: include=category,user,comments

返回参数
参数名 必含 类型 说明
user_id int 创建者
category_id int 分类
title string 标题
description string 描述
price int 价格
stock int 库存
sales int 销量
cover string 封面图
cover_url string 封面图 url
pics array 小图集
pics_url array 小图集 url
is_on int 是否上架 0 不上架 1 上架
is_recommend int 是否推荐 0 不推荐 1 推荐
details string 详情
category object 额外的 分类 数据,使用 include 才会返回
user object 额外的 用户 数据,使用 include 才会返回
comments object 额外的 评论 数数,使用 include 才会返回
created_at timestamp 添加时间
updated_at timestamp 修改时间
返回示例
  • 状态码 200 请求成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"id": 1,
"title": "电脑11111电脑",
"category_id": 7,
"description": "这是一个电脑1111",
"price": 5000,
"stock": 999,
"sales": 2,
"cover": "100x100.jpg",
"cover_url": "https://laravel-shop-api.oss-cn-beijing.aliyuncs.com/100x100.jpg",
"pics": [
"a.png",
"b.png"
],
"pics_url": [
"https://laravel-shop-api.oss-cn-beijing.aliyuncs.com/a.png",
"https://laravel-shop-api.oss-cn-beijing.aliyuncs.com/b.png"
],
"details": "这是一个电脑这是一个电脑这是一个电脑这是一个电脑",
"is_on": 1,
"is_recommend": 1,
"created_at": "2020-12-12T07:38:37.000000Z",
"updated_at": "2020-12-12T10:13:45.000000Z"
}
接口描述
  • 修改商品
请求 URL
  • /api/admin/goods/{good}
请求方式
  • PUT
请求头部
参数名 必选 类型 说明
Authorization string JWT token
RESET 参数
参数名 必选 类型 说明
good int 商品 id
Body 请求参数
参数名 必选 类型 说明
category_id int 分类
title string 标题
description string 描述
price int 价格
stock int 库存
cover string 封面图
pics array 小图集
details string 详情
返回示例
  • 状态码 204 成功
  • 状态码 400 请求错误
1
2
3
4
{
"message": "分类不存在",
"status_code": 400,
}
1
2
3
4
{
"message": "分类被禁用",
"status_code": 400,
}
1
2
3
4
5
{
"message": "只能向2级分类添加商品",
"status_code": 400,
}

  • 状态码 422 参数错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"message": "The given data was invalid.",
"errors": {
"title": [
"标题 不能为空。"
],
"category_id": [
"category id 不能为空。"
],
"description": [
"描述 不能为空。"
],
"price": [
"price 不能为空。"
],
"stock": [
"stock 不能为空。"
],
"cover": [
"cover 不能为空。"
],
"pics": [
"pics 不能为空。"
],
"details": [
"details 不能为空。"
]
},
"status_code": 422,
}

6.8.2 添加商品详情接口、修改商品接口

在\src\services\goods.js 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import request from '@/utils/request'

// 获取商品列表
export async function getGoods(params) {
return request('/admin/goods', { params })
}

/**
* 上架和下架商品
* @param {商品id} goodsid
* @returns
*/
export async function isOn(goodsId) {
return request.patch(`/admin/goods/${goodsId}/on`)
}
/**
* 推荐和不推荐商品
* @param {商品id} goodsid
* @returns
*/
export async function isRecommend(goodsId) {
return request.patch(`/admin/goods/${goodsId}/recommend`)
}

// 添加商品
export async function addGoods(params) {
return request.post('/admin/goods', { params })
}
/**
* 商品详情
* @param {*} editId
* @returns
*/
export async function showGoods(editId) {
return request(`/admin/goods/${editId}?include=category`)
}
/**
* 更新商品
* @param {*} params
* @returns
*/
export async function updateGoods(editId, params) {
return request.put(`/admin/goods/${editId}`, { params })
}

其中商品详情接口,由于有二级列表所以要加上?include=category

6.8.3 获取商品详情数据并提交修改

在\src\pages\Goods\components\CreateOrEdit.jsx 中先引入import { addGoods, showGoods, updateGoods } from '@/services/goods';

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import React, { useEffect, useState } from 'react'
import ProForm, { ProFormText, ProFormTextArea, ProFormDigit } from '@ant-design/pro-form'
import { Modal, message, Skeleton, Cascader, Button, Image } from 'antd'
import { UploadOutlined } from '@ant-design/icons'
import { addGoods, showGoods, updateGoods } from '@/services/goods'
import { getCategory } from '@/services/category'
import AliyunOSSUpload from '@/components/AliyunOSSUpload'
import Editor from '@/components/Editor'

const CreateOrEdit = props => {
/**
* isModalVisible 模态框是否显示
* isShowModal 操作模态框显示隐藏的方法
* actionRef 父组件传来的表格的引用,可以用来操作表格,比如刷新表单
* editId 要编辑的id,添加的时候是undefined,只有编辑时才有
*/
const { isModalVisible, isShowModal, actionRef, editId } = props

// 将表单初始化的值设置成状态,在编辑的时候使用这个状态
const [initialValues, setInitialValues] = useState(undefined)
const [options, setOptions] = useState([])

// 定义Form实例,用来操作表单
const [formObj] = ProForm.useForm()

// 文件上传成功后,设置cover字段的value
const setCoverKey = fileKey => formObj.setFieldsValue({ cover: fileKey })

// 文件上传成功后,设置details字段的value
const setDetails = content => formObj.setFieldsValue({ details: content })

// 添加或者编辑的描述
const type = editId === undefined ? '添加' : '编辑'

useEffect(async () => {
// 查询分类数据
const resCategory = await getCategory()
if (resCategory.status === undefined) setOptions(resCategory)

// 发送请求,获取商品详情
if (editId !== undefined) {
const response = await showGoods(editId)
// 获取数据之后,修改状态;状态改变,组件重新渲染,骨架框消失,编辑表单出现
const { pid, id } = response.category
const defaultCategory = pid === 0 ? [id] : [pid, id]
setInitialValues({ ...response, category_id: defaultCategory })
}
}, [])

// 提交表单,执行编辑或者添加
const handleSubmit = async values => {
let response = []
if (editId === undefined) {
// 执行添加
// 发送请求,添加商品
response = await addGoods({ ...values, category_id: values.category_id[1] })
} else {
// 执行编辑
// 发送请求,更新商品
response = await updateGoods(editId, { ...values, category_id: values.category_id[1] })
}
if (response.status === undefined) {
message.success(`${type}成功!`)
// 刷新表格数据
actionRef.current.reload()
// 关闭模态框
isShowModal(false)
}
}

return (
<Modal
title={`${type}商品`}
visible={isModalVisible}
onCancel={() => isShowModal(false)}
footer={null}
destroyOnClose={true}>
{
// 只有是编辑的情况下,并且要显示的数据还有返回,才显示骨架框
initialValues === undefined && editId !== undefined ? (
<Skeleton active={true} paragraph={{ rows: 4 }} />
) : (
<ProForm
form={formObj}
initialValues={initialValues}
onFinish={values => {
handleSubmit(values)
}}>
<ProForm.Item
name="category_id"
label="分类"
rules={[{ required: true, message: '请输入分类' }]}>
<Cascader
fieldNames={{ label: 'name', value: 'id' }}
options={options}
placeholder="请输入分类"
/>
</ProForm.Item>
<ProFormText
name="title"
label="商品名"
placeholder="请输入商品名"
rules={[{ required: true, message: '请输入商品名' }]}
/>
<ProFormTextArea
name="description"
label="描述"
placeholder="请输入商品描述"
rules={[{ required: true, message: '请输入商品描述' }]}
/>
<ProFormDigit
name="price"
label="价格"
placeholder="请输入商品价格"
min={0}
max={99999999}
rules={[{ required: true, message: '请输入商商品价格' }]}
/>
<ProFormDigit
name="stock"
label="库存"
placeholder="请输入商品库存"
min={0}
max={99999999}
rules={[{ required: true, message: '请输入商品库存' }]}
/>

<ProFormText name="cover" hidden={true} />
<ProForm.Item
name="cover"
label="上传商品主图"
rules={[{ required: true, message: '请选择商品主图' }]}>
<div>
<AliyunOSSUpload setCoverKey={setCoverKey} accept="image/*" showUploadList={true}>
<Button icon={<UploadOutlined />}>点击上传商品主图</Button>
</AliyunOSSUpload>
{!initialValues.cover_url ? (
''
) : (
<Image width={200} src={initialValues.cover_url} />
)}
</div>
</ProForm.Item>

<ProForm.Item
name="details"
label="商品详情"
rules={[{ required: true, message: '请输入商品详情' }]}>
<Editor setDetails={setDetails} content={initialValues.details} />
</ProForm.Item>
</ProForm>
)
}
</Modal>
)
}

export default CreateOrEdit

1.获取商品列表,处理商品分类

在\src\pages\Goods\components\CreateOrEdit.jsx 中
后端字段和前端设置的字段一样能够直接赋值,所以先将数据...response展开。后单独设置category_id: defaultCategory,因为有二级菜单,后端用的数组表示,所以解构response.category分别将二级菜单pid,一级菜单id填入数组赋值给category_id

1
2
3
4
5
6
7
8
// 发送请求,获取商品详情
if (editId !== undefined) {
const response = await showGoods(editId)
// 获取数据之后,修改状态;状态改变,组件重新渲染,骨架框消失,编辑表单出现
const { pid, id } = response.category
const defaultCategory = pid === 0 ? [id] : [pid, id]
setInitialValues({ ...response, category_id: defaultCategory })
}

2.处理图片获取

在\src\pages\Goods\components\CreateOrEdit.jsx 中,
先处理图片显示,用三目运算符判断原来是否有图片,没有则为空,有则添加图片<Image width={200} src={initialValues.cover_url} />
但是我们在点击上传图片时需要上传cover,当我们添加新图片时cover会被重新设置,所以将它隐藏起来
<ProFormText name="cover" hidden={true} />

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<ProFormText name="cover" hidden={true} />
<ProForm.Item
name="cover"
label="上传商品主图"
rules={[{ required: true, message: '请选择商品主图' }]}
>
<div>
<AliyunOSSUpload setCoverKey={setCoverKey} accept="image/*" showUploadList={true}>
<Button icon={<UploadOutlined />}>点击上传商品主图</Button>
</AliyunOSSUpload>
{!initialValues.cover_url ? (
''
) : (
<Image width={200} src={initialValues.cover_url} />
)}
</div>
</ProForm.Item>

3.处理富文本显示

在 src\pages\Goods\components\CreateOrEdit.jsx 中,在Editor组件设置content接收到原来的值

1
<Editor setDetails={setDetails} content={initialValues.details} />

在\src\components\Editor\index.jsx 中,,设置显示富文本的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import 'braft-editor/dist/index.css'
import React from 'react'
// 引入编辑器组件
import BraftEditor from 'braft-editor'
// 引入编辑器样式
import 'braft-editor/dist/index.css'
import './index.less'
import AliyunOSSUpload from '@/components/AliyunOSSUpload'
import { ContentUtils } from 'braft-utils'

export default class EditorDemo extends React.Component {
state = {
// 创建一个空的editorState作为初始值
editorState: BraftEditor.createEditorState(this.props.content ?? null),
}

// 编辑器内容改变的时候执行
handleEditorChange = editorState => {
// 更新编辑器的状态
this.setState({ editorState })
// 要判断输入的内容,如果有内容设置输入的内容;如果没有内容设置成空字符串
// 为什么要这样判断,因为即使是空内容editorState.toHTML()也是一对空标签,不能直接给表单使用
if (!editorState.isEmpty()) {
// 可直接调用editorState.toHTML()来获取HTML格式的内容
const content = editorState.toHTML()
// 调用父组件的函数,将编辑器输入的内容传递回去
this.props.setDetails(content)
} else {
this.props.setDetails('')
}
}

// 图片上传完成后执行此方法,用来在编译器中显示图片
insertImage = url => {
this.setState({
editorState: ContentUtils.insertMedias(this.state.editorState, [
{
type: 'IMAGE',
url,
},
]),
})
}

render() {
// 自定义控件--插入图片
const extendControls = [
{
key: 'antd-uploader',
type: 'component',
component: (
<AliyunOSSUpload insertImage={this.insertImage} accept="image/*" showUploadList={false}>
{/* 这里的按钮最好加上type="button",以避免在表单容器中触发表单提交,用Antd的Button组件则无需如此 */}
<button
type="button"
className="control-item button upload-button"
data-title="插入图片">
插入图片
</button>
</AliyunOSSUpload>
),
},
]

const { editorState } = this.state
return (
<div className="my-component">
<BraftEditor
value={editorState}
onChange={this.handleEditorChange}
extendControls={extendControls}
/>
</div>
)
}
}

其中editorState: BraftEditor.createEditorState(this.props.content ?? null),是关键代码,

this.props.content ?? null是三目运算符简写,如果this.props.content有值就传值显示在富文本上,没有就null

1
2
3
4
state = {
// 创建一个空的editorState作为初始值
editorState: BraftEditor.createEditorState(this.props.content ?? null),
}

4.提交表单更新商品

在\src\pages\Goods\components\CreateOrEdit.jsx 中,添加updateGoods接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 提交表单,执行编辑或者添加
const handleSubmit = async (values) => {
let response = [];
if (editId === undefined) {
// 执行添加
// 发送请求,添加商品
response = await addGoods({ ...values, category_id: values.category_id[1] });
} else {
// 执行编辑
// 发送请求,更新商品
response = await updateGoods(editId, { ...values, category_id: values.category_id[1] });
}
if (response.status === undefined) {
message.success(`${type}成功!`);
// 刷新表格数据
actionRef.current.reload();
// 关闭模态框
isShowModal(false);
}
};

七、项目总结和优化

7.1 优化新建时报错

在\src\pages\Goods\components\CreateOrEdit.jsx 中,由于initialValues没有初始化,刚开始是undefined,所以添加一个判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<ProForm.Item
name="cover"
label="上传商品主图"
rules={[{ required: true, message: '请选择商品主图' }]}
>
<div>
<AliyunOSSUpload setCoverKey={setCoverKey} accept="image/*" showUploadList={true}>
<Button icon={<UploadOutlined />}>点击上传商品主图</Button>
</AliyunOSSUpload>
{initialValues === undefined || !initialValues.cover_url ? (
''
) : (
<Image
width={200}
src={initialValues === undefined ? '' : initialValues.cover_url}
/>
)}
</div>
</ProForm.Item>

<ProForm.Item
name="details"
label="商品详情"
rules={[{ required: true, message: '请输入商品详情' }]}
>
<Editor
setDetails={setDetails}
content={initialValues === undefined ? '' : initialValues.details}
/>
</ProForm.Item>

7.2 优化刷新重定向问题

在\src\models\user.js 中,先判断是请求的接口否有userInfo.id,有的话才将用户信息存入localStorage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 获取用户信息
*fetchCurrent(_, { call, put }) {
// 查看localstorage是否有用户信息,没有再去请求
let userInfo = JSON.parse(localStorage.getItem('userInfo'));
if (!userInfo) {
userInfo = yield call(queryCurrent);

// 判断是否获取到用户信息,再把用户信息存入localstorage
if (userInfo.id !== undefined) localStorage.setItem('userInfo', JSON.stringify(userInfo));

// 完善登录,修复BUG:有时候userInfo返回的是useCache=false被误存入localStorage,错误的userInfo导致页面一直刷新
// if (userInfo.useCache !== false) localStorage.setItem('userInfo', JSON.stringify(userInfo));
}
yield put({
type: 'saveCurrentUser',
payload: userInfo,
});
},

7.3 优化 401 异常处理重定向到登录页

在\src\utils\request.js 中

1
2
3
4
5
6
7
8
9
10
11
// 处理401的情况
if (status === 401) {
// 清空用户本地缓存的token和用户信息
// 删除本地存储的token和userInfo
localStorage.removeItem('access_token')
localStorage.removeItem('userInfo')

// 跳转到登录页
// 重定向到登录页
history.replace('/login')
}

7.4 修复传参问题

关于 request 第二参数,常用两个传参方式

1.params 传参,也就是 query 传参,多用于 get 请求,查询数据使用,类型是对象或者 URLSearchParams
2.data 传参,也就是 body 传参,多用于提交表单数据,类型是 any,推荐使用对象

在\src\services\user.js 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 添加用户
* @param {*} params
* @returns
*/
export async function addUser(data) {
return request.post('/admin/users', { data })
}
/**
* 更新用户
* @param {*} params
* @returns
*/
export async function updateUser(editId, data) {
return request.put(`/admin/users/${editId}`, { data })
}

在\src\services\login.js 中

1
2
3
4
5
6
7
8
9
10
11
import request from '@/utils/request'

export async function fakeAccountLogin(data) {
return request('/auth/login', {
method: 'POST',
data,
})
}
export async function logout() {
return request.post('/auth/logout')
}

在\src\services\goods.js 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import request from '@/utils/request'

// 获取商品列表
export async function getGoods(params) {
return request('/admin/goods', { params })
}

/**
* 上架和下架商品
* @param {商品id} goodsid
* @returns
*/
export async function isOn(goodsId) {
return request.patch(`/admin/goods/${goodsId}/on`)
}
/**
* 推荐和不推荐商品
* @param {商品id} goodsid
* @returns
*/
export async function isRecommend(goodsId) {
return request.patch(`/admin/goods/${goodsId}/recommend`)
}

// 添加商品
export async function addGoods(data) {
return request.post('/admin/goods', { data })
}
/**
* 商品详情
* @param {*} editId
* @returns
*/
export async function showGoods(editId) {
return request(`/admin/goods/${editId}?include=category`)
}
/**
* 更新商品
* @param {*} params
* @returns
*/
export async function updateGoods(editId, data) {
return request.put(`/admin/goods/${editId}`, { data })
}