一、项目搭建 1.1 安装脚手架
1.2 选择版本
1.3 安装依赖
1.4 启动项目
1.5 点击链接进入浏览器
二、初始化项目 项目接口文档 https://www.showdoc.com.cn/1207745568269674?page_id=6094279351627422
2.1 删掉多余的文件 在编译器中打开项目 删掉\src\pages 中TableList
文件夹,Admin.jsx
、Welcome.jsx
、Welcome.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={}
否则会编程默认的样子
const defaultFooterDom = ( <DefaultFooter copyright={`${new Date ().getFullYear()} 融职商城` } links={null } /> )
底部文字就更改好了 在\Econfig\defaultSettings.js 中更改title
,这里的title
是更改的网页标题和左上角文字 在\src\assets 文件夹中提换掉 logo,并在用到的地方重新导入 logo 文件,否则会报错
2.3 更改刷新时的 logo 打开控制台,到网络请求,选择所有请求,快速刷新页面会发现这个图标 将自己的 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
第一步我们用画图 的方式打开原始图片,可以看到这里是一张 png 格式的原始图片,如下图所示:
第二步点击画图中文件图标,选择“另存为->BMP 图片”
第四步我们将 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
import logo from '../assets/logo.png'
在 UserLayout.jsx 文件删除标签FormattedMessage
内defaultMessage=“”
<div className={styles.desc}> <FormattedMessage id="pages.layouts.userLayout.title" /> </div>
在国际化\src\locales\zh-CN\pages.js 文件中修改 pages.layouts.userLayout.title 的默认文字
'pages.layouts.userLayout.title' : '融职商城后台管理系统' ,
2.6 删除首页头部多余东西 在\src\components\GlobalHeader\RightContent.jsx 文件中删除搜索组件HeaderSearch
和文档组件Tooltip
2.7 优化登录页 2.7.1 优化登录页文件 将登录页移到 pages 下删除 User 文件夹,注意非必要不要随意更改 pages 下的文件夹,因为改动文件夹要配置对应路由
2.7.2 配置登录页路由 在\config\routes.js 将原来 user 的路由修改成 login 的路由
{ 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 中更改重定向的登录路由
if (!isLogin && window .location.pathname !== '/login' ) { return <Redirect to ={ `/login ?${queryString }`} /> }
2.8 封装网络请求 2.8.1 添加请求拦截器 具体如何找请求拦截器 1.先进入umijs 找到插件 选择plugin-request 进去找到request 2.点击参考文档地址找到Interceptor 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, credentials: 'include' , }) request.interceptors.request.use((url, options ) => { const token = 'hello' const headers = { Authorization: `Bearer ${token} ` , } return { url, options: { ...options, headers }, } })export default request
2.8.2 封装错误信息提示 1.重新启动项目 通过 **yarn dev**
启动项目会关闭 mock,之后就能添加自己的 api
在\package.json 可以查到相关配置
"start:dev" : "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev umi dev" ,
2.更改代理 在\config\proxy.js 中将 dev 的域名改成自己的
dev: { '/api/' : { target: 'https://api.shop.eduwork.cn/' , changeOrigin: true , pathRewrite: { '^' : '' , }, }, },
在\src\services\user.js 中将接口请求改成request.post('/api/admin/user')
export async function queryCurrent ( ) { 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() if (status === 422 ) { let errs = '' for (const key in result.errors) { errs += result.errors[key][0 ] } errorText += `[ ${errs} ]` } if (status === 400 ) { errorText += `[ ${result.message} ]` } message.error(errorText) } else if (!response) { message.error('网络发生异常,无法连接服务器' ) } return response }
4.简化接口前缀(初始化项目可不设置) 在\src\utils\request.js 中的 request 函数添加prefix: '/api'
,则可以自动添加前缀简化接口写法 在\src\services\user.js 中,前缀则可以少写/api
export async function queryCurrent ( ) { 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
请求方式
Body 请求参数
参数名
必选
类型
说明
email
是
string
邮箱
password
是
string
密码
返回参数
参数名
必含
类型
说明
access_token
是
string
token
token_type
是
string
token 类型
expires_in
是
int
过期时间
返回示例
{ "access_token" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9hcGkudGVzdFwvYXBpXC9hdXRoXC9sb2dpbiIsImlhdCI6MTYwNzUyMDE0MSwiZXhwIjoxNjA3NTIzNzQxLCJuYmYiOjE2MDc1MjAxNDEsImp0aSI6IktVdWFsTmxnOXYzZmlTZHEiLCJzdWIiOjMsInBydiI6IjIzYmQ1Yzg5NDlmNjAwYWRiMzllNzAxYzQwMDg3MmRiN2E1OTc2ZjcifQ.BpVdvBjKEhQ2aIZBfkE-SoU2a3UeFkYCKQKh42Ncbio" , "token_type" : "Bearer" , "expires_in" : 3600 }
{ "message" : "The given data was invalid." , "errors" : { "email" : [ "邮箱 不能为空。" ], "password" : [ "密码 不能为空。" ] }, "status_code" : 422 , }
3.1.2 添加登录接口 在 src\services\login.js 中
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, }) history.replace('/' ) message.success('🎉 🎉 🎉 登录成功!' ) } }, logout ( ) { const { redirect } = getPageQuery() if (window .location.pathname !== '/user/login' && !redirect) { history.replace({ pathname: '/user/login' , search: stringify({ redirect: window .location.href, }), }) } }, }, reducers: { changeLoginStatus (state, { payload } ) { localStorage .setItem('access_token' , payload.access_token) return { ...state } }, }, }export default Model
在\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 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 : '网关超时。' , }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() if (status === 422 ) { let errs = '' for (const key in result.errors) { errs += result.errors[key][0 ] } errorText += `[ ${errs} ]` } if (status === 400 ) { errorText += `[ ${result.message} ]` } message.error(errorText) } else if (!response) { message.error('网络发生异常,无法连接服务器' ) } return response }const request = extend({ errorHandler, credentials: 'include' , prefix: '/api' , }) request.interceptors.request.use((url, options ) => { const token = localStorage .getItem('access_token' ) || ' ' const headers = { Authorization: `Bearer ${token} ` , } return { url, options: { ...options, headers }, } })export default request
3.2 获取用户信息 3.2.1 登录信息接口文档 接口描述
请求 URL
请求方式
请求头部
参数名
必选
类型
说明
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
更新时间
返回示例
{ "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 中
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 } ) { let userInfo = JSON .parse(localStorage .getItem('userInfo' )) if (!userInfo) { userInfo = yield call(queryCurrent) 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;
因为后台返回的用户id
是id
不是userId
返回示例
{ "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 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
<Avatar size="small" className={styles.avatar} src={currentUser.avatar_url} alt="avatar" />
3.2.5 优化登录,判断登录之后重定向到首页 在\src\pages\Login\index.jsx 中 优化登录,判断登录之后重定向到首页 导入useEffect
和history
import React, { useEffect } from 'react' import { connect, history } from 'umi'
加入useEffect
代替生命周期函数
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
if (!userInfo) { userInfo = yield call(queryCurrent); if (userInfo.useCache !== false ) localStorage .setItem('userInfo' , JSON .stringify(userInfo)); }
3.3 退出 3.3.1 退出接口文档 接口描述
请求 URL
请求方式
请求头部
参数名
必选
类型
说明
Authorization
是
string
JWT token
返回示例
3.3.2 添加退出接口 在\src\services\login.js 中
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 } ) { const load = message.loading('登录中...' ) const response = yield call(fakeAccountLogin, payload) if (response.status === undefined ) { yield put({ type: 'changeLoginStatus' , payload: response, }) history.replace('/' ) message.success('🎉 🎉 🎉 登录成功!' ) } load() }, *logout (_, { call } ) { const load = message.loading('退出中...' ) const response = yield call(logout) if (response.status === undefined ) { localStorage .removeItem('access_token' ) localStorage .removeItem('userInfo' ) history.replace('/login' ) message.success('🎉 🎉 🎉 退出成功!' ) } load() }, }, reducers: { changeLoginStatus (state, { payload } ) { 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
请求方式
返回参数
参数名
必含
类型
说明
users_count
是
int
用户数量
goods_count
是
int
商品数量
order_count
是
int
订单数据
返回示例
{ "users_count" : 7 , "goods_count" : 237 , "order_count" : 1 }
4.4 添加统计面板接口 在 src\services\dashboard.js 添加统计面板接口
import request from '@/utils/request' export function fetchDashboard ( ) { return request('/admin/index' ) }
五、用户列表 5.1 用户基本列表 5.1.1 用户列表接口文档 接口描述
请求 URL
请求方式
请求头部
参数名
必选
类型
说明
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
下一页链接
返回示例
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 中
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 , 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
请求方式
请求头部
参数名
必选
类型
说明
Authorization
是
string
JWT token
RESET 参数
参数名
必选
类型
说明
user
是
int
用户 id
返回示例
5.2.2 添加禁用和启用接口 在\src\services\user.js 中添加禁启用接口
export async function lockUser (uid ) { return request.patch(`/admin/users/${uid} /lock` ) }
5.2.3 添加和启用方法 在\src\pages\User\index.jsx 中,先导入lockUser
import { getUsers, lockUser } from '@/services/user'
创建禁启用函数接收用户 id,因为成功后后端返回值是空,所以response.status===undefined
判断为空则操作成功
const heandleLockUser = async uid => { const response = await lockUser(uid) if (response.status === undefined ) { message.success('操作成功!' ) } else { message.error('操作失败!' ) } }
然后在columns
列表中,找到禁启用字段,使用禁启用函数,同时传出record.id
用户 id
{ 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
请求方式
请求头部
参数名
必选
类型
说明
Authorization
是
string
JWT token
Body 请求参数
参数名
必选
类型
说明
name
是
string
昵称
email
是
string
邮箱
password
是
string
密码
返回示例
状态码 201 创建成功
状态码 422 参数错误
{ "message" : "The given data was invalid." , "errors" : { "name" : [ "昵称 不能为空" ], "email" : [ "邮箱 不能为空。" ], "password" : [ "密码 不能为空。" ] }, "status_code" : 422 , }
5.3.2 添加添加用户接口 在\src\services\user.js 中 这里添加用户接口和获取用户列表是同一个接口/admin/users
,但是他们的请求方式不一样,添加用户接口用post
,而获取用户列表接口是用默认的get
方式。
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.jsxCreate
组件在父组件中使用,并用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 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
组件,并且将方法和实例传给子组件,在父组件里只做简单的显示关闭操作,不做过多的逻辑
<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 ) const actionRef = useRef() const getData = async params => { const response = await getUsers(params) return { data: response.data, success: true , 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
请求方式
REST 请求参数
参数名
必选
类型
说明
users
是
int
用户 id
Body 请求参数
参数名
必选
类型
说明
name
是
string
昵称
email
是
string
邮箱
返回示例
状态码 201 创建成功
状态码 422 参数错误
{ "message" : "The given data was invalid." , "errors" : { "name" : [ "昵称 不能为空" ], "email" : [ "邮箱 不能为空。" ], "password" : [ "密码 不能为空。" ] }, "status_code" : 422 , }
接口描述
请求 URL
请求方式
请求头部
参数名
必选
类型
说明
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
更新时间
返回示例
{ "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 中添加更新用户和用户详情接口,虽然这两个接口请求是同一个,但是他们的传参方式和参数是不一样的。updateUser
是put
方法,用于更新数据,需要传编辑的用户 id 和修改的参数,showUser
是get
方法,用于设置编辑栏上的默认值,只需要传编辑用户 id,后端返回改用户具体参数
export async function updateUser (editId, params ) { return request.put(`/admin/users/${editId} ` , { params }) }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, }) } }, []) 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
的骨架框,原因是页面渲染比接口请求快,在获取用户详情之前页面就渲染完了,导致编辑栏上没有得到该被编辑用户的数据,加入骨架框起到缓冲作用。 同时给骨架框和编辑表单添加了三元运算符,避免两个同时被渲染,判断接口请求接收到用户详情之后骨架框消失,编辑表单出现。一下是主要代码
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 ) const actionRef = useRef() const getData = async params => { const response = await getUsers(params) return { data: response.data, success: true , 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 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 => { 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.jsx
和Edit.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 ) const actionRef = useRef() const getData = async params => { const response = await getUsers(params) return { data: response.data, success: true , 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
请求方式
请求头部
参数名
必选
类型
说明
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
下一页链接
返回示例
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
根据接口文档添加商品列表接口
import request from '@/utils/request' export async function getGoods (params ) { return request('/admin/goods' , { params }) }
6.1.3 创建基本商品列表页面 在\src\pages 文件夹中,复制User
文件夹并重命名Goods
, 修改基本页面,添加商品图片预览, 其中valueType
是设置筛选的单选按钮,valueEnum
是选项,可以枚举也可以直接列出来,选择类 参考文档
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 ) const actionRef = useRef() const getData = async params => { const response = await getGoods(params) return { data: response.data, success: true , 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
请求方式
请求头部
参数名
必选
类型
说明
Authorization
是
string
JWT token
RESET 参数
参数名
必选
类型
说明
good
是
int
商品 id
返回示例
6.2.2 商品推荐和不推荐接口文档 接口描述
请求 URL
/api/admin/goods/{good}/recommend
请求方式
请求头部
参数名
必选
类型
说明
Authorization
是
string
JWT token
RESET 参数
参数名
必选
类型
说明
good
是
int
商品 id
返回示例
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 }) }export async function isOn (goodsId ) { return request.patch(`/admin/goods/${goodsId} /on` ) }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 ) const actionRef = useRef() const getData = async params => { const response = await getGoods(params) return { data: response.data, success: true , 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
请求方式
请求头部
参数名
必选
类型
说明
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 => { 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
请求方式
请求头部
参数名
必选
类型
说明
Authorization
是
string
JWT token
Query 请求参数
参数名
必选
类型
说明
type
否
string
all 查所有分类,包含禁用的。不传则只返回非禁用的
返回参数
参数名
必含
类型
说明
id
是
int
主键
pid
是
int
父级
name
是
string
名称
level
是
int
层级
status
是
int
状态: 0 正常 1 禁用
children
否
array
子类
返回示例
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
文件
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
请求查询分类数据
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
,设置分类的name
和rules
等,ProFormFields 表单项参考文档。 同时,后端返回来的字段和Cascader
官方的文档字段不一样时,查看 API 文档fieldNames
属性可以自定义字段
fieldNames
自定义 options 中 label name children 的字段
object
{ label: label, value: value, children: children }
<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 => { 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
请求方式
请求头部
参数名
必选
类型
说明
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
返回示例
{ "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
import request from '@/utils/request' 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() } 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, 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, 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 => { 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
组件 在\src\pages\Goods\components\CreateOrEdit.jsx 中,将原来上传图片的ProFormUploadButton
组件替换为AliyunOSSUpload
组件,添加验证规则并写成双标签,将在其中显示的内容写入。
<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: {}, } async componentDidMount ( ) { await this .init() } 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, 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 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, data: this .getExtraData, beforeUpload: this .beforeUpload, listType: 'picture' , maxCount: 1 , } return ( <Upload {...props}> {} {} <Button icon={<UploadOutlined /> }>{this .props.children}</Button> </Upload> ) } }
修复上传主图,显示默认文字 bug 其中Upload
内部直接写{this.props.children}
获取父组件的内容,无法渲染会报错,最后只需要在父组件中将AliyunOSSUpload
写成双标签,里边写显示的文字。将Button
封装在AliyunOSSUpload
组件内部,自取显示内容即可解决
<Upload {...props}> {} {} <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, data: this .getExtraData, beforeUpload: this .beforeUpload, listType: 'picture' , maxCount: 1 , }; return ( <Upload {...props}> {} {} <Button icon={<UploadOutlined /> }>{this .props.children}</Button> </Upload> ); }
在\src\pages\Goods\components\CreateOrEdit.jsx 中,AliyunOSSUpload
标签中设置accept
属性,accept 参考文档,详情
<ProForm.Item name="cover" label="上传商品主图" rules={[{ required : true , message : '请选择商品主图' }]}> <AliyunOSSUpload accept="image/*" >点击上传商品主图</AliyunOSSUpload> </ProForm.Item>
但是其中也有一个 bug,ProForm.Item
组件和我们封装的AliyunOSSUpload
组件(或者第三方组件)并不关联,ProForm.Item
当进行表单验证的时候,并没有包括AliyunOSSUpload
。所以当文件上传成功之后,把文件的 key,设置成表单某个字段的值。
使用通用方式完成文件验证以及解除组件受控 通过 Form.useForm 对表单数据域进行交互。 在\src\pages\Goods\components\CreateOrEdit.jsx 中,为ProForm
标签添加form={formObj}
控制实例
<ProForm form={formObj} initialValues={initialValues} onFinish={(values ) => { handleSubmit(values); }} >
定义Form
实例和setCoverKey
方法,用于当文件上传之后设置cover
字段的value
const [formObj] = ProForm.useForm()const setCoverKey = fileKey => formObj.setFieldsValue({ cover : fileKey })
在AliyunOSSUpload
组件中传入setCoverKey
方法
<AliyunOSSUpload setCoverKey={setCoverKey} accept="image/*" > 点击上传商品主图 </AliyunOSSUpload>
在\src\components\AliyunOSSUpload\index.jsx 中设置上传文件的回调函数,将文件的key
设置成文件某个字段的值。
onChange = ({ file } ) => { if (file.status === 'done' ) { this .props.setCoverKey(file.key) message.success('上传成功' ) } }
当点击上传文件时会报错 原因是当我们文件上传过程中触发的回调函数时通过 // 文件上传成功后,设置cover字段的value const setCoverKey = (fileKey) => formObj.setFieldsValue({ cover: fileKey });
设置了ProForm.Item
中name="cover"
的值,ProForm.Item
组件和AliyunOSSUpload
组件形成了受控组件,value
值被设置了,但是上传过程中触发的回调函数检测到文件还没有,拿不到文件就会报错。 查看Form.Item 的 api 就有介绍解决办法 :用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 => { const { isModalVisible, isShowModal, actionRef, editId } = props const [initialValues, setinitialValues] = useState(undefined ) const [options, setOptions] = useState([]) const [formObj] = ProForm.useForm() 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
6.6.2 封装富文本编辑器 1.简单封装文本编辑器 在\src\components 文件夹中,新建Editor
文件夹,并在Editor
下新建 index.jsx
和index.less
文件 在\src\components\Editor\index.less 中设置基本样式
.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: BraftEditor.createEditorState(null ), } handleEditorChange = editorState => { this .setState({ editorState }) } render ( ) { const { editorState } = this .state return ( <div className="my-component" > <BraftEditor value={editorState} onChange={this .handleEditorChange} /> </div> ) } }
2.使用富文本编辑器 在\src\pages\Goods\components\CreateOrEdit.jsx 中引入import Editor from '@/components/Editor';
将原来商品详情ProFormTextArea
组件的换成ProForm.Item
组件并使用 <Editor />
<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
,并添加这个方法
const setDetails = content => formObj.setFieldsValue({ details : content })
<ProForm.Item name="details" label="商品详情" rules={[{ required : true , message : '请输入商品详情' }]} > <Editor setDetails={setDetails} /> </ProForm.Item> </ProForm> )
在\src\components\Editor\index.jsx 中,接调用 editorState.toHTML()来获取 HTML 格式的内容,调用父组件的函数,将编辑器输入的内容传递回去
handleEditorChange = editorState => { this .setState({ editorState }) if (!editorState.isEmpty()) { 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: BraftEditor.createEditorState(null ), } handleEditorChange = editorState => { this .setState({ editorState }) if (!editorState.isEmpty()) { 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 }> {} <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: {}, } async componentDidMount ( ) { await this .init() } 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) { setCoverKey(file.key) } if (insertImage) { insertImage(file.url) } message.success('上传成功' ) } } getExtraData = file => { const { OSSData } = this .state return { key: file.key, OSSAccessKeyId: OSSData.accessid, 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 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, data: this .getExtraData, beforeUpload: this .beforeUpload, listType: 'picture' , maxCount: 1 , showUploadList, } return ( <Upload {...props}> {} {} {} {} {this .props.children} </Upload> ) } }
在\src\pages\Goods\components\CreateOrEdit.jsx 中,给AliyunOSSUpload
组件传值showUploadList={true}
显示文件图片,并将Button
在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 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 => { const { isModalVisible, isShowModal, actionRef, editId } = props const [initialValues, setinitialValues] = useState(undefined ) const [options, setOptions] = useState([]) const [formObj] = ProForm.useForm() const setCoverKey = fileKey => formObj.setFieldsValue({ cover : fileKey }) 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
请求方式
请求头部
参数名
必选
类型
说明
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 }) }export async function isOn (goodsId ) { return request.patch(`/admin/goods/${goodsId} /on` ) }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 { } if (response.status === undefined ) { message.success(`${type} 成功!` ) actionRef.current.reload() isShowModal(false ) } }
其中我们要添加category_id
二级分类的商品在 response = await addGoods({ ...values, category_id: values.category_id[1] });
中,我们先将...values
展开,随后再处理二级分类的商品
6.8 修改商品 6.8.1 商品详情接口文档、修改商品接口文档 接口描述
请求 URL
请求方式
请求头部
参数名
必选
类型
说明
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
修改时间
返回示例
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
请求方式
请求头部
参数名
必选
类型
说明
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
详情
返回示例
{ "message" : "分类不存在" , "status_code" : 400 , }
{ "message" : "分类被禁用" , "status_code" : 400 , }
{ "message" : "只能向2级分类添加商品" , "status_code" : 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.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 }) }export async function isOn (goodsId ) { return request.patch(`/admin/goods/${goodsId} /on` ) }export async function isRecommend (goodsId ) { return request.patch(`/admin/goods/${goodsId} /recommend` ) }export async function addGoods (params ) { return request.post('/admin/goods' , { params }) }export async function showGoods (editId ) { return request(`/admin/goods/${editId} ?include=category` ) }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 => { const { isModalVisible, isShowModal, actionRef, editId } = props const [initialValues, setInitialValues] = useState(undefined ) const [options, setOptions] = useState([]) const [formObj] = ProForm.useForm() const setCoverKey = fileKey => formObj.setFieldsValue({ cover : fileKey }) 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
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
接收到原来的值
<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: BraftEditor.createEditorState(this .props.content ?? null ), } handleEditorChange = editorState => { this .setState({ editorState }) if (!editorState.isEmpty()) { 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 }> {} <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
state = { 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 } ) { let userInfo = JSON .parse(localStorage .getItem('userInfo' )); if (!userInfo) { userInfo = yield call(queryCurrent); if (userInfo.id !== undefined ) localStorage .setItem('userInfo' , JSON .stringify(userInfo)); } yield put({ type: 'saveCurrentUser' , payload: userInfo, }); },
7.3 优化 401 异常处理重定向到登录页 在\src\utils\request.js 中
if (status === 401 ) { 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 中
export async function addUser (data ) { return request.post('/admin/users' , { data }) }export async function updateUser (editId, data ) { return request.put(`/admin/users/${editId} ` , { data }) }
在\src\services\login.js 中
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 }) }export async function isOn (goodsId ) { return request.patch(`/admin/goods/${goodsId} /on` ) }export async function isRecommend (goodsId ) { return request.patch(`/admin/goods/${goodsId} /recommend` ) }export async function addGoods (data ) { return request.post('/admin/goods' , { data }) }export async function showGoods (editId ) { return request(`/admin/goods/${editId} ?include=category` ) }export async function updateGoods (editId, data ) { return request.put(`/admin/goods/${editId} ` , { data }) }