13-React项目实战
# 一,项目实战
# 1,路由的使用
使用我们自己配置的webpack,如下:
运行项目如下:
路由官网:https://reactrouter.com/en/main
学习V5版本,它的官网:https://v5.reactrouter.com/web/guides/quick-start
安装路由:
cnpm i react-router-dom -S
在真实开发中,路由有两套版本:V5和V6。 V6是在21年9月份才出来的。
路由相关的有三个包:
- react-router 路由核心包
- react-router-dom 基于浏览器的路由包,专用于做web开发
- react-router-native 基于RN平台的路由包,专用于原生APP开发
创建两个组件,如下:
在React中没有Vue一样,创建一个router的文件夹,在App组件中配置路由,如下:
效果如下:
# 2,antd的使用
官网:https://ant.design/docs/react/introduce-cn
我们使用4.22.6版本。文档:https://4x.ant.design/index-cn/
安装之,如下:
引入样式,如下:
copy连接到public文件,如下:
在项目中引入之,一般情况下,做管理系统,直接全部引入,不会按需加载。我们就不配置按需加载了。
测试Button组件,如下:
测试如下:
到此,我们就把antd集成到项目中了。
要使用antd中的字体图标,需要安装,如下:
cnpm install --save @ant-design/icons
# 3,搭建后台首页面
对应的组件,如下:
开始大胆copy代码,最好一行一行copy,如下:
// layout/index.jsx
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
UploadOutlined,
UserOutlined,
VideoCameraOutlined,
} from '@ant-design/icons';
import { Layout, Menu } from 'antd';
import React, { useState } from 'react';
const { Header, Sider, Content } = Layout;
export default () => {
const [collapsed, setCollapsed] = useState(false);
return (
<Layout>
<Sider trigger={null} collapsible collapsed={collapsed}>
<div className="logo" />
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['1']}
items={[
{
key: '1',
icon: <UserOutlined />,
label: 'nav 1',
},
{
key: '2',
icon: <VideoCameraOutlined />,
label: 'nav 2',
},
{
key: '3',
icon: <UploadOutlined />,
label: 'nav 3',
},
]}
/>
</Sider>
<Layout className="site-layout">
<Header
className="site-layout-background"
style={{
padding: 0,
}}
>
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
onClick: () => setCollapsed(!collapsed),
})}
</Header>
<Content
className="site-layout-background"
style={{
margin: '24px 16px',
padding: 24,
minHeight: 280,
}}
>
Content
</Content>
</Layout>
</Layout>
)
}
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
效果如下:
需要写一些全局样式,如下:
引入全局样式:
效果如下:
创建layout的样式,如下:
开始写layout的样式,如下:
效果如下:
把侧边菜单,抽离成一个单独的组件,如下:
参考代码如下:
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
UploadOutlined,
UserOutlined,
VideoCameraOutlined,
} from '@ant-design/icons';
import { Menu } from 'antd';
export default () => {
return (
<>
<div className="logo" />
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['1']}
items={[
{
key: '1',
icon: <UserOutlined />,
label: 'nav 1',
},
{
key: '2',
icon: <VideoCameraOutlined />,
label: 'nav 2',
},
{
key: '3',
icon: <UploadOutlined />,
label: 'nav 3',
},
]}
/>
</>
)
}
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
对应的idnex.jsx的代码如下:
把React.createElement写法,换成jsx写法,如下:
要把折叠收缩按钮放到侧边栏,抽离组件如下:
参考代码如下:
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
UploadOutlined,
UserOutlined,
VideoCameraOutlined,
} from '@ant-design/icons';
import { Menu } from 'antd';
const Toggle = ({ value, onChange }) => {
return (
<div onClick={onChange} className="trigger">
{
value
? <MenuUnfoldOutlined />
: <MenuFoldOutlined />
}
</div>
)
}
export default (props) => {
return (
<>
<div className="logo" />
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['1']}
items={[
{
key: '1',
icon: <UserOutlined />,
label: 'nav 1',
},
{
key: '2',
icon: <VideoCameraOutlined />,
label: 'nav 2',
},
{
key: '3',
icon: <UploadOutlined />,
label: 'nav 3',
},
]}
/>
<Toggle {...props} />
</>
)
}
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
在index.jsx中代码如下:
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
UploadOutlined,
UserOutlined,
VideoCameraOutlined,
} from '@ant-design/icons';
import React, { useState } from 'react';
import { Layout, Menu } from 'antd';
const { Header, Sider, Content } = Layout;
import MlSider from './MlSider';
import "./style.scss"
export default () => {
const [collapsed, setCollapsed] = useState(false);
return (
<div className='ml-layout'>
<Layout>
<Sider trigger={null} collapsible collapsed={collapsed}>
<MlSider value={collapsed} onChange={() => setCollapsed(!collapsed)}></MlSider>
</Sider>
<Layout className="site-layout">
<Header
className="site-layout-background"
style={{
padding: 0,
}}
>
</Header>
<Content
className="site-layout-background"
style={{
margin: '24px 16px',
padding: 24,
minHeight: 280,
}}
>
Content
</Content>
</Layout>
</Layout>
</div>
)
}
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
样式如下:
效果如下:
处理logo,有两个logo,一个大logo,一个小logo,如下:
默认情况下,显示大logo,折叠时,显示小logo,处理如下:
样式如下:
效果如下:
然后,我们处理侧边菜单,如下:
效果如下:
现在创建一些页面组件的组件,配置items,如下:
然后配置二级路由,如下:
一级路由不需要指定出口,但是二级路由需要指定出口,如下:
效果如下:
然后,配置items,因为侧边菜单长什么,取决items。
# 4,侧边栏菜单生成
和vue-element-admin一样,把侧边栏菜单抽离成一个配置文件,如下:
参考代码如下:
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
UploadOutlined,
UserOutlined,
VideoCameraOutlined,
} from '@ant-design/icons';
import Dashboard from "@/pages/dashboard/index"
import GoodList from "@/pages/good/goodList"
import GoodForm from "@/pages/good/goodForm"
import User from "@/pages/user/index"
// 动态路由,有权限的路由
export const asyncRoutes = [
{
key: 1001,
path: "/dashboard",
label: "首页大屏",
icon: <MenuFoldOutlined />,
element: <Dashboard />
},
{
key: 1002,
icon: <VideoCameraOutlined />,
label: "商品管理",
children: [
{
key: 100201,
path: "/good/list",
icon: null,
label: "商品列表",
element: <GoodList />
},
{
key: 100202,
path: "/good/add",
icon: null,
label: "商品新增",
element: <GoodForm />
},
{
key: 100203,
path: "/good/edit",
icon: null,
label: "商品编辑",
element: <GoodForm />
}
]
},
{
key: 1003,
path: "/user",
element: <User />,
icon: <UserOutlined />,
label: "用户管理"
}
];
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
需要生成items,如下:
getItems如下:
在MlSider中,根据asyncRoutes生成items,如下:
在Menu组件中使用items,如下:
效果如下:
在vue-element-admin中,可以控制一个菜单的显示与隐藏,如下:
修改一下算法,如下:
效果如下:
还需要点击进行跳转,使用Link组件,如下:
点击测试OK,如下:
也需要App.jsx中的路由配置写活,如下:
生成的算法,如下:
使用算法,如下:
动了getItem,如下:
测试OK,如下:
现在侧边菜单有两个问题:
- 当选中某个菜单后,刷新后,此菜单并没有选中
- 对于二级菜单,打开后,当刷新后,又折叠起来了
解决这两个问题,需要靠两个props来解决,如下:
但是你上面写的1002和1003是写死的,现在需要写一个算法,动态生成上面的两个数值,我们使用自定义hook实现算法,如下:
在组件中使用自定义hook,如下:
我们期望得到两个key,如下:
开始实现useMenu这个自定义hook,如下:
使用之,如下:
测试OK,如下:
能不能把上面计算这两个key的代码缓存起来,使用useMemo,如下:
测试OK。
总结自定义hook的使用:
- 组件主是要用来抽离UI界面的,hook和vue3一样,主要是用来抽离逻辑。抽离完后,是可以复用的,并且多次使用,彼此之间是独立的。
- 和vue3一样,自定义hook需要使用use开头。
- 自定义hook只能在函数式组件中使用,写在组件内,return之前。
- 网上还有一些比较优秀的开源hook,如:react-use,ahooks。
# 5,实现面包屑导航
需求如下:
在组件库中找到面包屑导航,如下:
抽离MlHeader组件,如下:
在index.jsx中,使用MlHeader组件,如下:
浏览器中测试如下:
定义类名,写一点样式,如下:
现在需要小一个小算法,生成面包屑,如下:
在MlHeader组件中使用之,如下:
根据breads,生成JSX,如下:
优化面包屑导航,如下:
这样写,和我们刚才是一样的,如下:
引入一个hook,实现编程式路由跳转,如下:
点击测试OK。
如果可以跳转,鼠标放上去,变成手形,如果不能跳转,鼠标放上去,不是手形,如下:
测试OK。
还可以设置如果不能跳转,就变成灰色,能跳转就白色,如下:
如果能跳转,就设置成白色,如下:
效果如下:
判断条件也可以封装成一个函数,如下:
# 7,全屏与退出全屏
需求:
准备上面的三个icon,如下:
开始布局,如下:
书写样式,如下:
效果如下:
然后实现全屏与退出全屏,使用screenfull来实现,安装如下:
绑定点击事件,使用之,如下:
测试OK。
还需要优化,没有全屏和全屏下,按钮长的是不一样的,如下:
需要定义一个状态来控制显示哪一个图标,如下:
效果如下:
# 8,集成Redux
类似于vuex和pinia,用来集中状态管理的,需要使用到的依赖:
- 经典架构:redux + react + react-redux + redux-thunk + immer
- 最新架构:@reduxjs/toolkit + react + react-redux +ts
安装redux,如下:
创建仓库,语法如下:
需要记一些东西,如下:
- 三个API: createStore / combineReducers / applyMiddleware
- 三个特点:store必须是单 一数据源 / store中的状态是只读 / store中的数据只能通过reducer来修改
- 三个概念:store是数据仓库, action(用于改变数据的对象{type: 信号,payload}),reducer(专门用来修改状态)
现在创建一个子reducer(代表创建一个子store),如下:
利用immer对上面的第1步进行操作,安装之,如下:
使用poduce进行操作,如下:
在index.js中,引入,合并(现在就一个,一会就会创建多个),如下:
到此,这个子store就准备完毕了,同理还可以准备一个子store,如下(文件名写错了,应该是user):
在index.js中进行合并,如下(文件名写错了,应该是user):
到此,仓库就准备好,你要在react组件中使用仓库中的状态,需要使用react-redux,如下:
在App.jsx中,使用Provider提供store,这样,子子孙孙都可以使用store,如下:
打印出store,如下:
浏览器中测试如下:
想获取仓库中的状态,如下:
现在要在组件中获取仓库中的状态,如下:
现在想获取user模块中的状态,如下:
简写如下:
修改size状态,使用dispatch,格式如下:
- dispatch(action)
- action是一个对象,对象中有一个type和payload
修改size,如下:
小的reducer收到了action,如下:
然后根据type,对老的状态进行操作,如下:
对size一次减3操作,如下:
reduer开始收到信号,开始操作状态,如下:
到此,我们就把reducx集成到了项目中。有一个图,如下:
# 9,组件大小的全局切换
需求:
实现下拉菜单,如下:
copy代码如下:
效果如下:
点击large,middle,点击small,改变仓库中的size的大小。定义状态,如下:
在组件中使用状态,如下:
测试之,如下:
使用行内样式,如下:
点击small和large,目的是让仓库中的size变化,绑定点击事件,如下:
实现toggleSize方法,如下:
点击时,如何判断这个action有没有传给reducer,可以安装一个调试工具,大家可以搜索一个调试工具,安装一下,除了安装调试工具外,还可以使用redux-logger,类似于console.log。可以新状态和老状态都打印出来。
安装之,如下:
这个logger是一个中间件,要使用中间件,如下:
浏览器中测试,如下:
现在就需要修改状态,如下:
浏览器再次测试之,如下:
到此,就可以完成仓库中状态的修改了。但是action我们是写死的组件中的,如下:
抽离action,如下:
在组件中使用action creater,如下:
浏览器中测试之,如下:
使用两个组件,如下:
效果如下:
然后要实现组件大小的全局切换,文档如下:
和vue-element-admin一样,全局功能都在layout文件夹中,使用之,如下:
这样就可以改变组件的大小了。测试OK。
现在有一个问题,如下:
解决如下 :
测试之,如下:
# 10,国际化
需求:
准备数据,如下:
浏览器中也可以测试当前支持的默认语言,如下:
先把下拉菜单整出来,如下:
效果如下:
在仓库中准备状态,如下:
在组件中就可以使用状态了,如下:
浏览器效果如下:
然后准备action creator,如下:
给下拉菜单绑定点击事件,点击diapath一个action,如下:
reducer需要接收状态,修改reducer,如下:
浏览器中测试之,如下:
现在先去实现组件的国际化,文档如下:
语言包如下:
使用之,如下:
使用之,如下:
测试如下:
到此,组件内置的国际就实现了。
还需要对lang进行持久化,如下:
通过navigator.language得到的是"zh-CN",组件中的数据如下:
此时测试肯定不OK。
修改组件中的数据,如下:
在仓库中获取zh,如下:
在layout组件中,也需要修改之,如下:
再次测试之,如下:
除了组件的国际化外,还有型业务中国际化,实现方案:
- 在vue中,使用vue-i18n
- 在react中,使用react-intl
安装react-intl,如下:
在src下面创建locales文件夹,如下:
使用之,如下:
此时,需要国际化的地方,就不能写死了,如下:
除了上面的使用方式外,还有一种方式,如下:
现在业务文字的国际就实现了。
现在我想把左侧菜单也实现国际化,如下:
路由的配置文件,参考代码如下:
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
UploadOutlined,
UserOutlined,
VideoCameraOutlined,
} from '@ant-design/icons';
import Dashboard from "@/pages/dashboard/index"
import GoodList from "@/pages/good/goodList"
import GoodForm from "@/pages/good/goodForm"
import User from "@/pages/user/index"
import { FormattedMessage } from "react-intl"
// 动态路由,有权限的路由
export const asyncRoutes = [
{
key: 1001,
path: "/dashboard",
label: <FormattedMessage id="menu.dashboard" />,
icon: <MenuFoldOutlined />,
element: <Dashboard />
},
{
key: 1002,
icon: <VideoCameraOutlined />,
label: <FormattedMessage id="menu.good" />,
children: [
{
key: 100201,
path: "/good/list",
icon: null,
label: <FormattedMessage id="menu.good.list" />,
element: <GoodList />
},
{
key: 100202,
path: "/good/add",
icon: null,
label: <FormattedMessage id="menu.good.add" />,
element: <GoodForm />
},
{
key: 100203,
path: "/good/edit",
icon: null,
label: <FormattedMessage id="menu.good.edit" />,
element: <GoodForm />,
// hidden: true
}
]
},
{
key: 1003,
path: "/user",
element: <User />,
icon: <UserOutlined />,
label: <FormattedMessage id="menu.user" />,
}
];
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
效果如下:
# 11,主题色切换
什么是主题色:
实现设置按钮,如下:
把主题内容区域抽离成一个组件,如下:
在index.jsx中引入,使用之,如下:
效果如下:
书写setting的样式。如下:
效果如下:
当点击上面的按钮,需要弹出抽屉组件,如下:
测试效果如下:
提两个地方,要做国际化:
实现步骤,如下:
在抽屉上面,实现颜色的拾色器,使用react-color这个依赖,安装之,如下:
使用之,如下:
效果如下:
颜色也是需要存储到仓库中的,如下:
在组件中使用之,通过颜色选择器选中了别的颜色,需要修改仓库中的颜色,如下:
给颜色选择器指定默认颜色,如下:
效果如下:
当我们选择了别的颜色,需要绑定事件,如下:
浏览器中测试如下:
先定义action creater,如下:
然后,就可以派发这个action,如下:
然后是reducer收信到信号,处理之,如下:
浏览器测试之,如下:
到此,就把仓库中的颜色修改了。然后就需要改变页面中的主题色了。如下:
实现之,如下:
需要在main.js中引入样式,如下:
浏览器测试之,如下:
有一个问题,如下:
解决:
效果如下:
数据持久化,如下:
测试OK。
# 12,绘制商品表格页面(copy)
大致需求:
在商品列表页面和商品表格页面中都使用到了商品的分类,把商品的分类抽离成一个组件,如下:
参考的代码如下:
import { Select } from 'antd';
const { Option } = Select;
export default () => {
return (
<Select
defaultValue="lucy"
style={{
width: 120,
}}
>
{
["a", "b", "c"].map(item => (
<Option key={item} value={item}>手机</Option>
))
}
</Select>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
在商品列表中,直接使用之,如下:
效果如下:
然后,书写一些样式,如下:
参考样式如下:
.good-list {
box-sizing: border-box;
.filter {
background-color: white;
padding: 15px;
box-sizing: border-box;
border-radius: 2px;
.ant-col-3 span {
display: block;
text-align: right;
padding-right: 5px;
}
}
.table {
margin-top: 20px;
background-color: white;
box-sizing: border-box;
padding: 0 15px;
.table-title {
display: inline-block;
.anticon {
margin-left: 15px;
cursor: pointer;
font-size: 16px;
}
}
.good {
text-align: center;
img {
display: inline-block;
width: 60px;
height: 60px;
}
}
}
}
.good-form {
// background-color: white;
.form {
background: white;
padding: 15px;
margin-top: 20px;
box-sizing: border-box;
padding-top: 25px;
}
}
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
在组件中引入样式,如下 :
效果如下:
在layout文件夹下的index.jsx中添加了如下的样式:
添加了最小的宽度,意味着当缩小浏览器时,页面布局就不会压缩了,如下:
在缩小浏览器时,到一定程度,需要让左侧菜单折叠起来,实现如下:
测试:
优化一下,如下:
然后绘制表格,和elementui中的表格不太一样,开始绘制,如下:
import { Button, DatePicker, Row, Col, Input, Table } from "antd"
import CateSelect from "./components/CateSelect"
const { RangePicker } = DatePicker
import { useIntl, FormattedMessage } from "react-intl"
import "./style.scss"
const columns = [
{
title: 'Name',
dataIndex: 'name',
render: (text) => <a>{text}</a>,
},
{
title: 'Age',
dataIndex: 'age',
},
{
title: 'Address',
dataIndex: 'address',
},
]
const data = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
},
{
key: '4',
name: 'Disabled User',
age: 99,
address: 'Sidney No. 1 Lake Park',
},
];
export default (props) => {
const intl = useIntl();
return (
<div className="good-list">
<div className="filter">
<Row justify="center" align="middle">
<Col span={3}>
<span>商品名称:</span>
</Col>
<Col span={4}>
<Input />
</Col>
<Col span={3}>
<span>商品分类:</span>
</Col>
<Col span={4}>
<CateSelect />
</Col>
</Row>
</div>
<div className="table">
<Table
rowSelection={{
type: "checkbox",
onChange: (keys, rows) => {
console.log(keys, rows);
}
}}
columns={columns}
dataSource={data}
/>;
</div>
</div>
)
}
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
效果如下:
参考ant design pro,地址:https://preview.pro.ant.design/dashboard/analysis
先导入对应的字体图标,如下:
绘制如下:
效果如下:
还有分页,如下:
把表格中的都有哪列设置一下,大家直接copy这个页面,如下:
import { Button, DatePicker, Row, Col, Input, Table } from "antd"
import CateSelect from "./components/CateSelect"
import { PlusOutlined, ReloadOutlined, ColumnHeightOutlined, SettingOutlined } from "@ant-design/icons"
const { RangePicker } = DatePicker
import { useIntl, FormattedMessage } from "react-intl"
import "./style.scss"
const columns = [
{
title: '商品',
align: 'center',
dataIndex: 'name',
render: (text) => {
return (
<div className="good">
<img src={`https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg`} alt="" />
<div>{text}</div>
</div>
)
},
},
{
title: '价格',
dataIndex: 'price',
align: 'center',
render: text => (
<div>{`¥${Number(text).toFixed(2)}`}</div>
)
},
{
title: '品类',
dataIndex: 'cate',
align: 'center',
render: cate => {
return (
<div>手机</div>
)
}
},
{
title: '是否热销',
dataIndex: 'hot',
align: 'center',
render: text => (
<div>{text ? '是' : '否'}</div>
)
},
{
title: '状态',
dataIndex: 'store_status',
align: 'center',
render: text => (
<div>{text ? '已上架' : '待上架'}</div>
)
},
{
title: '发布时间',
dataIndex: 'create_time',
align: 'center',
render: text => {
return (
<>
<div>{text}</div>
</>
)
}
},
{
title: '操作',
align: 'center',
render: (_, row) => (
<>
<Button
type='primary'
size='small'
>
编辑
</Button>
<Button danger size='small' style={{ marginLeft: '10px' }}>删除</Button>
</>
)
}
]
const data = [
{
key: '1',
name: '小米手机',
price: 32,
cate: '手机',
hot: true,
store_status: true,
create_time: "20220202"
},
];
export default (props) => {
const intl = useIntl();
return (
<div className="good-list">
<div className="filter">
<Row justify="center" align="middle">
<Col span={3}>
<span>商品名称:</span>
</Col>
<Col span={4}>
<Input />
</Col>
<Col span={3}>
<span>商品分类:</span>
</Col>
<Col span={4}>
<CateSelect />
</Col>
</Row>
</div>
<div className="table">
<Table
rowSelection={{
type: "checkbox",
onChange: (keys, rows) => {
console.log(keys, rows);
}
}}
columns={columns}
dataSource={data}
title={() => (
<Row>
<Col span={4}>商品列表</Col>
<Col span={6} offset={14} style={{ textAlign: "right" }}>
<Button type="primary" icon={<PlusOutlined />}>新增</Button>
<div className="table-title">
<ReloadOutlined />
<ColumnHeightOutlined />
<SettingOutlined />
</div>
</Col>
</Row>
)}
pagination={{
total: 30,
showSizeChanger: true,
showTitle: (total) => `Total ${total} items`
}}
/>;
</div>
</div>
)
}
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
效果如下:
当点击商品新增时,要去商品新增页面,如下:
在侧边菜单中,需要把商品新增和商品编辑隐藏掉,如下:
浏览器测试之,如下:
# 13,绘制商品表单页面(copy)
大致需求:
可以添加一个页面头,如下:
使用之,如下:
效果如下 :
开始绘制表单,如下 :
import {
PageHeader,
Checkbox,
Form,
Input,
Button,
Row,
Col,
InputNumber,
Select,
Switch
} from 'antd';
import { useNavigate } from "react-router-dom"
import CateSelect from './components/CateSelect'
import "./style.scss"
export default (props) => {
const navigate = useNavigate();
const [form] = Form.useForm();
const onFinish = (values) => {
console.log(values);
};
return (
<div className='good-form'>
<PageHeader
className="site-page-header"
onBack={() => navigate(-1)}
title="商品新增"
style={{ background: "#fff" }}
/>
<div className="form">
<Form
form={form}
name="goodAddEdit"
labelCol={{ span: 2 }}
wrapperCol={{ span: 10 }}
labelAlign="left"
onFinish={onFinish}
>
<Form.Item
name="name"
label="商品名称"
rules={[
{ required: true, message: "商品名称是必填项" }
]}
>
<Input />
</Form.Item>
<Form.Item
name="desc"
label="商品描述"
rules={[
{ required: true, message: "商品描述是必填项" }
]}
>
<Input.TextArea />
</Form.Item>
<Form.Item
name="cate"
label="商品品类"
rules={[
{ required: true, message: "商品品类是必填项" }
]}
>
<CateSelect />
</Form.Item>
<Form.Item
name="price"
label="商品价格"
rules={[
{ required: true, message: "商品价格是必填项" }
]}
>
<InputNumber />
</Form.Item>
<Form.Item
name="hot"
label="是否热销"
rules={[
// { required: true, message: "商品价格是必填项" }
]}
>
<Switch />
</Form.Item>
<Form.Item>
<Button type='primary' htmlType='submit'>提交</Button>
</Form.Item>
</Form>
</div>
</div>
)
}
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
效果如下:
对商品图片上传处理,找到一个带有裁剪功能的组件,如下:
安装antd-img-crop,如下:
封装一个组件,如下:
在表单中使用之,如下:
效果如下:
处理一个这个组件,如下:
效果如下:
现在有个警告,如下:
解决如下:
测试就没有警告了,如下:
现在看一下,能不能收集到数据,如下:
收集分类数据,如下:
测试如下:
对表单进行校验,如下:
效果如下:
到此,商品表单页面就绘制完毕了。
商品表单页面的代码参考如下:
import {
PageHeader,
Checkbox,
Form,
Input,
Button,
Row,
Col,
InputNumber,
Select,
Switch
} from 'antd';
import { useNavigate } from "react-router-dom"
import CateSelect from './components/CateSelect'
import GoodUpload from './components/GoodUpload';
import "./style.scss"
export default (props) => {
const navigate = useNavigate();
const [form] = Form.useForm();
const onFinish = (values) => {
console.log(values);
};
return (
<div className='good-form'>
<PageHeader
className="site-page-header"
onBack={() => navigate(-1)}
title="商品新增"
style={{ background: "#fff" }}
/>
<div className="form">
<Form
form={form}
name="goodAddEdit"
labelCol={{ span: 2 }}
wrapperCol={{ span: 10 }}
labelAlign="left"
validateTrigger="onBlur"
onFinish={onFinish}
>
<Form.Item
name="name"
label="商品名称"
rules={[
{ required: true, message: "商品名称是必填项" },
{ pattern: /[\u4E00-\u9FA5]{4,6}/, message: "商品名称要求是4~6个汉字" },
]}
>
<Input />
</Form.Item>
<Form.Item
name="desc"
label="商品描述"
rules={[
{ required: true, message: "商品描述是必填项" },
{ min: 10, max: 30, message: "商品描述要求10~20个字符" }
]}
>
<Input.TextArea />
</Form.Item>
<Form.Item
name="cate"
label="商品品类"
rules={[
{ required: true, message: "商品品类是必填项" }
]}
>
<CateSelect />
</Form.Item>
<Form.Item
name="price"
label="商品价格"
rules={[
{ required: true, message: "商品价格是必填项" }
]}
>
<InputNumber />
</Form.Item>
{/*
Form.Item可以帮我们自动收集数据,只要被Form.Item包裹的表单,相当于给它了一个value+onChange
*/}
<Form.Item
name="hot"
label="是否热销"
valuePropName='checked'
rules={[
// { required: true, message: "商品价格是必填项" }
]}
>
<Switch />
</Form.Item>
<Form.Item
name="img"
label="商品图片"
rules={[
// { required: true, message: "商品价格是必填项" }
]}
>
<GoodUpload />
</Form.Item>
<Form.Item>
<Button type='primary' htmlType='submit'>提交</Button>
</Form.Item>
</Form>
</div>
</div>
)
}
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
商品分类组件的代码参考如下:
import { Select } from 'antd';
const { Option } = Select;
export default ({ value, onChange }) => {
return (
<Select
defaultValue="lucy"
value={value}
onChange={onChange}
style={{
width: '100%',
}}
>
{
["a", "b", "c"].map(item => (
<Option key={item} value={item}>手机</Option>
))
}
</Select>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
封装的图片上传组件参考代码如下:
import { Upload, Image } from 'antd';
import ImgCrop from 'antd-img-crop';
import React, { useState } from 'react';
export default () => {
const [fileList, setFileList] = useState([
{
uid: '-1',
name: 'image.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
]);
const onChange = ({ fileList: newFileList }) => {
setFileList(newFileList);
};
const onPreview = async (file) => {
};
return (
<ImgCrop rotate>
<Upload
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
listType="picture-card"
fileList={fileList}
onChange={onChange}
onPreview={onPreview}
itemRender={(_, file) => {
return (
<Image src={file.url} />
)
}}
>
{fileList.length < 1 && '+ Upload'}
</Upload>
</ImgCrop>
)
}
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
# 14,绘制登录页面和联调准备
绘制结构如下:
参考结构如下:
import { Button, Checkbox, Form, Input } from 'antd'
import { useDispatch } from 'react-redux'
import './style.scss'
export default () => {
const dispatch = useDispatch()
// 登录提交
const submit = (values) => {
}
return (
<div className='login'>
<div className='layer'>
<div className='wrap'>
<h1 style={{ textAlign: "center" }}>欢迎使用XXX管理系统</h1>
<Form
name="basic"
labelCol={{ span: 5 }}
wrapperCol={{ span: 16 }}
initialValues={{
remember: true,
}}
onFinish={submit}
autoComplete="off"
validateTrigger={['onBlur']}
>
<Form.Item
label="用户名"
name="username"
rules={[
{ required: true, message: '用户名是必填字段' },
]}
>
<Input />
</Form.Item>
<Form.Item
label="密 码"
name="password"
rules={[
{ required: true, message: '密码是必填字段' },
]}
>
<Input.Password />
</Form.Item>
<Form.Item
name="remember"
valuePropName="checked"
wrapperCol={{
offset: 5,
span: 16,
}}
>
<Checkbox>记住用户名</Checkbox>
</Form.Item>
<Form.Item
wrapperCol={{
offset: 5,
span: 16,
}}
>
<Button type="primary" htmlType="submit">
登录
</Button>
</Form.Item>
</Form>
</div>
</div>
</div>
)
}
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
参考样式,如下:
.login {
.layer {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, .9);
.wrap {
position: absolute;
top: 150px;
left: 50%;
width: 520px;
margin-left: -260px;
background-color: white;
box-sizing: border-box;
border-radius: 3px;
padding: 25px;
padding-bottom: 0;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
效果如下:
安装axios,如下:
axios二次封装,如下:
参考代码如下:
import axios from 'axios'
import {
message,
Modal
} from 'antd'
import {
ExclamationCircleOutlined
} from '@ant-design/icons'
import store from '@/store'
import {
resetUser
} from '@/store/actions'
const {
confirm
} = Modal
// 为了解决调接口遭遇同源策略的阻塞,这里“前端项目调用当前端口上的接口”
const baseURL = 'http://localhost:8080'
const version = '/api/react'
// 创建axios实例
const service = axios.create({
baseURL: baseURL + version,
timeout: 5000
})
// 添加请求拦截器
service.interceptors.request.use(
config => {
// 添加Token
config.headers.Authorization = localStorage.getItem('token')
return config
},
error => {
console.log(error) // for debug
return Promise.reject(error)
}
)
// 添加响应拦截器
service.interceptors.response.use(
response => {
// 如果代码走到这里,HTTP状态码=200
const res = response.data
console.log('----响应拦截器', res)
// 对业务状态码进行判断
if (res.err !== 0) {
// 如果业务状态码不等于0,表示业务失败,就把后端的反馈信息弹出来。
message.error(res.msg || '入参有误')
// 当Token过期或者Token是伪造的,要求重新登录。
if (res.err === -1) {
// 登录重新登录
confirm({
title: '当前你的登录已失效',
icon: < ExclamationCircleOutlined / > ,
content: '请重新登录',
okText: '重新登录',
// 隐藏取消按钮,要求必须重新登录
cancelButtonProps: {
style: {
display: 'none'
}
},
onOk() {
store.dispatch(resetUser())
}
})
}
return Promise.reject(new Error(res.message || 'Error'))
} else {
return res.data
}
},
error => {
console.log('err' + error)
return Promise.reject(error)
}
)
export default service
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
配置代理解决跨域,如下:
参考代码如下:
devServer: {
port: 8080,
open: true, // 打包成功后,自动打开浏览器
client: {
overlay: {
errors: true,
warnings: false
}
},
proxy: {
'/api': {
target: 'http://47.94.210.129:9999',
changeOrigin: true
}
}
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
封装API接口,如下:
参考代码如下:
import request from '@/utils/request'
export function fetchCates() {
return request({
url: '/good/cates',
method: 'GET',
params: {}
})
}
export function fetchGoodList(params) {
return request({
url: '/good/list',
method: 'GET',
params
})
}
export function fetchGoodSubmit(data) {
return request({
url: '/good/update',
method: 'POST',
data
})
}
export function fetchGoodInfo(id) {
return request({
url: '/good/info',
method: 'GET',
params: {
id
}
})
}
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
参考代码如下:
import request from '@/utils/request'
export function fetchLogin(data) {
return request({
url: '/user/login',
method: 'POST',
data
})
}
export function fetchUserInfo() {
return request({
url: '/user/info',
method: 'GET',
params: {}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 15,联调登录接口(权限/难)
# 1)登录获取token,存储token
数据库有如下的数据:
在组件中调用接口实现登录,如下:
服务器响应如下:
思考一下,前端权限校验的流程:
- 点击登录,调用接口,获取token。token需要存储到redux中,还需要存储到localstorage中。
- 带个token,再去调用一个接口,获取用户信息,用户信息中包含角色(role)信息。也需要存储到redux中。
- 使用后端返回 的role和路由表,执行算法,得到当前登录用户有权访问的路由们(accessRoutes)。也是需要存储到redux中。
- 根据accessRoutes动态生成路由规则和Layout菜单。
- 跳到后面首页面/,接着重定向到/dashboard。
刚才我们是在组件中发一个登录请求,实际上这个登录请求,需要在redux中发送,类似于vuex中action中发请求。在redux中,默认情况下,是不支持写异步代码的。在redux中要写异步代码,需要借助redux-thunk中间件。安装之,如下:
使用中间件,如下:
现在准备action,action是有分类的,先看一下,我们之前写的action,如下:
现在写一个异步的aciton,如下:
在登录组件中派发异步action,如下:
在浏览器中测试之,如下:
总结:
- redux中action creator分两类:同步action creator 和 异步action creator
- 同步action是一个对象:{type, payload}
- 异步action是一个函数,函数中可以写异步代码
- dispatch默认情况下只能派发一个同步的action
- dispatch要派发一个异步的action,需要使用redux-thunk中间件
此时,登录就OK了,如下:
登录成功后,把token存储到本地,和redux中,如下:
reduer需要处理同步action,如下:
浏览器测试之,如下:
到此,登录成功后,把token存储到本地和redux中了。最好判断一个登录成功了,如下:
在redux中,也可以对token进行持久化,如下:
后面再去发请求时,需要带上token,如下:
也就意味着,后面再去发请求,就会带上token。
# 2)模拟路由守卫
所谓的权限校验,如果你有权限,可以访问某个路由,如果没有权限,是不能访问这些路由。
现在我们的路由是所有人都可以访问的,如下:
删除上面的函数调用,如下:
测试如下:
除了上面的配置路由之外,还有一种方式,如下:
测试是OK的,如下:
可以把Page组件抽离出去,如下:
在App.jsx中使用Permission,如下:
浏览器再测试如下:
把Permission中的规则放到我们之前规则中,如下:
在Permisson.jsx中导入,如下:
再次测试之,如下:
可以把Permission.jsx当成vue中的路由守卫。
# 3)获取用户信息
在Permmion.jsx中获取token,监听token的变化,如果token变了需要重新获取用户信息,代码如下:
浏览器测试如下:
然后需要获取用户信息,对应的API,如下:
发请求是异步代码,需要写在异步的action中,如下:
在守卫中,token变了,就需要获取用户信息了,如下:
在浏览器中测试如下:
把用户信息存储到redux中,如下:
在仓库中定义状态,接收action,如下:
把axios的二次封装,注释一部分代码,如下:
浏览器测试之,如下:
# 4)根据roles获取accessRoutes
使用后端返回的roles数组,和路由表,算法,生成当用户可以访问的路由规则们。写一个副作用,如下:
在计算accessRoutes之前,给路由规则,配置meta,如下:
书写算法,如下:
参考代码如下:
// roles 角色
// tmp 每一个规则
// hasPermission 判断一个角色是否有访问某个路由权限
// 如:admin meta: roles:["amdin","editor"]
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
return roles.some(role => route.meta.roles.includes(role))
} else {
// 没有meta
return true
}
}
// routes 所有动态路由
// roles 角色
export function filterAsyncRoutes(routes, roles) {
const res = []
routes.forEach(route => {
const tmp = {
...route
}
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
// generateRoutes 生成路由规则们
// asyncRoutes 所有动态路由
// roles 角色
export function generateRoutes(asyncRoutes, roles) {
return dispatch => {
let accessRoutes = []
// filterAsyncRoutes 生成当前用户可以访问的路由规则们
accessRoutes = filterAsyncRoutes(asyncRoutes, roles)
console.log("accessRoutes:::", accessRoutes);
// 把路由规则们存储到redux
// dispatch({ type: 'USER_PERMISSION', payload: accessRoutes })
}
}
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
现在角色有了,全部的路由规则有了,算法有了,就可以生成当前用户可以访问的路由规则们,如下:
浏览器中测试之,如下:
然后,需要把路由规则们,存储到redux中,如下:
定义状态,让reducer接收信号,如下:
测试如下:
# 5)生成全部路由规则和Layout菜单
上面已经根据角色,计算出来,可以访问的路由规则们,然后生成全部的路由规则。你刚才计算出来的是动态路由规则,除了动态路由规则,还有静态路由规则,开始计算全部的路由规则,如下:
浏览器中效果如下:
然后,根据规则,生成侧边菜单,如下:
还需要进行跳转,如下:
浏览器测试之,如下:
再次测试一下:
还有一个比较大的流程图:
# 16,优化一下权限设计
让admin用户看到首页大屏,商品管理,用户管理,如下:
admin用户登录上去,如下:
之前写了多个副作用,如下:
在第1个副作用中添加一个判断,如下:
现在把token删除了,当刷新时,就会直接去登录页面,如下:
根据之前vue-element-admin那个权限设计图,优化一下代码。先设置一个白名单,如下:
需要获取当前访问的路径,如下:
去访问非login路由,并且没有token,如下:
还有在获取用户信息时,最好也加上判断,如下:
继续优化,如果有token,你访问的还是登录页面,需要放行到后台首页面,如下:
现在放到了/这个路由了,如下:
如果访问了/,还需要重定向到/dashboard,如下:
访问/login,放到了/,重定向到了/dashboard,如下:
如果token被篡改,过期了,如下:
看服务器响应什么,如下:
在响应拦截器中处理之,如下:
书写resetUser这个action,如下:
reducer接收到这个信号,处理之,如下:
现在token被篡改了,测试如下:
完整的Permission.jsx的代码如下:
import { useRoutes, useNavigate, useLocation } from "react-router-dom"
import { useEffect, useMemo } from "react"
import { useSelector, useDispatch } from "react-redux"
import { getInfo } from "@/store/actions"
import { generateRoutes } from "@/store/permission"
import { constantRoutes, asyncRoutes } from "@/pages/index"
// 白名单
const whiteList = ['/login']
function Page() {
const dispatch = useDispatch();
const { token, roles, accessRoutes } = useSelector(state => state.user)
const navigator = useNavigate();
const { pathname } = useLocation();
// 监听token变化
useEffect(() => {
// 当token从无到有,有了token,才能获取用户信息
if (token) {
dispatch(getInfo())
} else {
navigator("/login", { replace: true })
}
}, [token])
useEffect(() => {
if (roles && roles.length > 0) {
// roles中有角色,配合路由表和算法,生成当前用户可以访问的路由规则们
// ....
dispatch(generateRoutes(asyncRoutes, roles))
}
}, [roles])
useEffect(() => {
if (accessRoutes && accessRoutes.length > 0) {
navigator("/dashboard", { replace: true })
}
}, [accessRoutes])
useEffect(() => {
// !whiteList.includes(pathname) 表示访问的路径并没有在白名单中
// 如果在白名单中,是不需要token
if (!whiteList.includes(pathname) && !token) {
navigator("/login", { replace: true })
}
// 表示已经登录了,已经有token了,又访问登录页面
if (token && pathname === "/login") {
navigator("/", { replace: true })
}
if (token && pathname === "/") {
navigator("/dashboard", { replace: true })
}
}, [pathname])
// 计算出全部的路由规则
const routes = useMemo(() => {
const result = [...constantRoutes]; // 静态路由规则copy一份
result[0].children = accessRoutes
return result;
}, [accessRoutes])
const element = useRoutes(constantRoutes)
return element;
}
export default Page;
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
# 17,echarts和bizcharts
在管理系统中,图表也是非常重要的。我们说两个
- echarts 不管是vue或react都可以使用,DOM图。
- bizcharts 只能在react中使用,里面是一堆的组件,用什么图,导入什么组件。非常简单。
- 除了上面的这两个图表之外,还有其它的图表
先讲echarts,官网:https://echarts.apache.org/zh/index.html
不用通过npm去安装,直接使用cdn,如下:
证明echart导入成功了,如下:
浏览器中测试之如下:
echarts是用来绘制图表的,每一个图表都需要一个容器,就是一个div,还需要指定宽高,如下:
开始绘图,绘图的步骤:
- 1)创建图表实例
- 2)配置图表选项
- 3)setOption()
在管理系统中,有几类图表用的是最多的
- 柱状图
- 折线图
- 饼图
开始绘制,如下:
参考代码如下:
import { useEffect } from "react"
export default (props) => {
useEffect(() => {
// 基于准备好的容器,初始化echarts实例
const chart = echarts.init(document.getElementById('main'));
// 配置图表选项(这里的配置至少1000多个)
// 但是你想你的需求,99%都可以通过配置实现
var options = {
color: ['gold', 'pink'],
backgroundColor: 'rgb(255,255,255)',
tooltip: {
show: true
},
toolbox: {
show: true,
feature: {
dataZoom: {
yAxisIndex: 'none'
},
dataView: { readOnly: false },
magicType: { type: ['line', 'bar'] },
restore: {},
saveAsImage: {}
}
},
title: {
text: "2023年中国汽车销售数据"
},
xAxis: {
data: ['大众', '奥迪', '起亚', '福特', '菲亚特']
},
yAxis: {
},
series: [
{
type: "bar",
data: [110, 300, 200, 180, 280],
showBackground: true,
name: "2022",
backgroundStyle: {
// color: 'rgba(180, 180, 180, 1)'
}
},
{
type: "bar",
data: [220, 180, 260, 280, 220],
showBackground: true,
name: "2023",
backgroundStyle: {
// color: 'rgba(180, 180, 180, 1)'
}
},
{
type: "bar",
data: [120, 380, 160, 260, 320],
showBackground: true,
name: "2023",
backgroundStyle: {
// color: 'rgba(180, 180, 180, 1)'
}
}
],
legend: {
top: "50%",
left: "90%",
orient: "vertical"
}
}
chart.setOption(options)
}, [])
return (
<div className="dashboard">
{/* div是用来装图表的 */}
<div id="main" style={{ width: '480px', height: '300px' }}></div>
</div>
)
}
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
效果如下:
把面的图,封装成一个组件,如下:
在在大屏页面中,导入,如下:
在vue-element-admin,它的图表是响应式,如下:
现在我们的图表并不是响应的,如下:
把图片处理成响应式,需要设置容器的宽度是100%,如下:
当浏览器宽度变小时,需要重新调用echarts实例的resize方法,如下:
现在浏览器容器一变化,立即触发resize,要做防抖处理。在vue-element-admin中人家封装好了防抖函数,我们只需要直接引入这个防抖函数,使用之。自习实现。
下去,大家把vue-element-admin中的四个图表,迁移到react项目中。
我们再去了解一种图片,叫bizchart,只能在react中使用,都是封装好的组件。
官网:https://bizcharts.taobao.com/
上面的图,都是以组件的形式提供的。如下:
要使用bizcharts,需要安装,如下:
封装一个柱状图,如下:
在首页面中引入之,如下:
效果如下:
# 18,联调商品模块的接口
# 1)商品分类的渲染
商品分类接口如下 :
封装action,如下:
在商品分类模块中调用接口如下:
浏览器中就可以得到商品分类数据,如下:
把数据存储到redux中,如下:
准备good模块,如下:
合并之,如下:
获取分类数据并渲染如下:
浏览器中测试如下:
添加全部分类,如下:
测试如下:
在使用商品分类组件中,指定value,如下:
定义相关状态,收集数据,如下:
测试是否可以获取商品分类数据,如下:
上面这样的组件,叫受控组件。
# 2)商品列表的渲染
准备API接口如下:
在组件中直接引入,然后调用接口如下:
浏览器中测试之,如下:
更改表格的数据源,如下:
浏览器中测试如下:
指定分页,如下:
浏览器中测试如下:
安装moment模块,处理发布时间,如下:
设置表格的列信息如下:
参考代码如下:
// 表格的列表(如果要做自定义列,使用useMemo)
const columns = useMemo(() => {
return [
{
title: '商品',
align: 'center',
dataIndex: 'name',
render: (text, row, idnx) => {
// do something
return (
<div className='good'>
<img src={`http://47.94.210.129:9999${row.img}`} alt="" />
<div>{text}</div>
</div>
)
}
},
{
title: '价格',
dataIndex: 'price',
align: 'center',
render: text => (
<div>{`¥${Number(text).toFixed(2)}`}</div>
)
},
{
title: '品类',
dataIndex: 'cate',
align: 'center',
render: cate => {
if (cates.length > 0) {
const [r] = cates.filter(ele => ele.cate === cate)
return <div>{r ? r.cate_zh : null}</div>
}
}
},
{
title: '是否热销',
dataIndex: 'hot',
align: 'center',
render: text => (
<div>{text ? '是' : '否'}</div>
)
},
{
title: '状态',
dataIndex: 'store_status',
align: 'center',
render: text => (
<div>{text ? '已上架' : '待上架'}</div>
)
},
{
title: '发布时间',
dataIndex: 'create_time',
align: 'center',
render: text => {
const m = moment(text)
return (
<>
<div>{m.format('YYYY年MM月DD')}</div>
<div>{m.format('HH:mm:ss')}</div>
</>
)
}
},
{
title: '操作',
align: 'center',
render: (_, row) => (
<>
<Button
type='primary'
size='small'
onClick={() => navigate('/good/edit/' + row._id)}>
编辑
</Button>
<Button danger size='small' style={{ marginLeft: '10px' }}>删除</Button>
</>
)
}
]
}, [cates, list]) // 当cates变化时,重新计算一个columns
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
浏览器效果如下:
# 3)商品新增
点击跳转到商品新增页面,如下:
点击测试可以到达商品新增页面,收集页面中的数据,只能图片还不能收集,如下:
此时,商品分类的下拉菜单不能显示分类了,处理之,如下:
在商品列表中传入hasAll,如下:
这样,在商品列表页面有全部选项,在商品添加页面没有全部选项,如下:
二次封装图片上传组件(自行分析),如下:
参考代码如下:
import { Upload, Image, message } from 'antd'
import ImgCrop from 'antd-img-crop'
import { useState, useEffect } from 'react'
// value + onChange 是 Form.Item 组件给的,用于表单自动取值
export default ({ value, onChange }) => {
const [fileList, setFileList] = useState([]) // 在编辑时给Upload加默认值
// 使用Form.Item传递过来的value渲染Upload
useEffect(() => {
if (value && fileList.length === 0) {
setFileList([
{ uid: Date.now(), thumbUrl: `http://47.94.210.129:9999${value}` }
])
}
}, [value])
const imgSuccess = ({ file, fileList }) => {
setFileList([...fileList]) // 解决“Upload受控后onChange只执行一次”的问题
if (file.status === 'done' && file.response.err === 0) {
const img = file.response.data.img
console.log('上传成功---img', img)
onChange(img) // 回传图片路径给Form.Item
}
}
// 图片上传前的校验
const imgCheck = file => {
const arr = ['image/png', 'image/jpg']
if (!arr.includes(file.type)) {
message.error('图片只能是 png / jpg 格式')
return Promise.reject()
}
// 图片不能大于2M
if (file.size / 1024 / 1024 > 2) {
message.error('图片不能大于 2M')
return Promise.reject()
}
return Promise.resolve(file) // 一定要传入file
}
return (
<ImgCrop rotate>
{/*itemRender={(_, file)=>(<Image src={file.url} />)*/}
<Upload
action="http://localhost:8080/api/react/upload/img"
name='good'
headers={{ Authorization: localStorage.getItem('token') }}
listType="picture-card"
fileList={fileList}
maxCount={1}
onChange={imgSuccess}
beforeUpload={imgCheck}
>
{fileList.length === 0 && '+Upload'}
</Upload>
</ImgCrop>
)
}
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
测试如下:
此时,再去收集数据,就OK了,如下:
点击添加数据,API接口,如下:
点击确定,实现添加,如下:
测试如下:
到此,商品添加就实现了。
# 4)商品编辑实现
配置动态路由,如下:
点击编辑去商品编辑页面,如下:
点击测试之,如下:
此时,商品新增和商品编辑共用同一个页面。
在商品编辑页面中,获取到商品ID,如下:
根据ID发送ajax请求,获取商品详情,实现数据回显如下:
浏览器中测试之,如下:
然后实现编辑,如下:
测试如下:
到此,商品编辑实现。
# 5)优化
指定表单页面是新增还是编辑,如下:
指定提交按钮是新增还是编辑,如下:
测试如下:
实现分页,如下:
实现搜索:
浏览器测试如下: