13-React项目实战

7/24/2023

# 一,项目实战

# 1,路由的使用

使用我们自己配置的webpack,如下:

1675404482887

运行项目如下:

1675404530630

路由官网:https://reactrouter.com/en/main

1675404625863

学习V5版本,它的官网:https://v5.reactrouter.com/web/guides/quick-start

1675404861672

安装路由:

cnpm i react-router-dom -S
1

1675404901686

在真实开发中,路由有两套版本:V5和V6。 V6是在21年9月份才出来的。

路由相关的有三个包:

  • react-router 路由核心包
  • react-router-dom 基于浏览器的路由包,专用于做web开发
  • react-router-native 基于RN平台的路由包,专用于原生APP开发

创建两个组件,如下:

1675405160584

1675405177242

在React中没有Vue一样,创建一个router的文件夹,在App组件中配置路由,如下:

1675405615162

效果如下:

1675405631249

# 2,antd的使用

官网:https://ant.design/docs/react/introduce-cn

我们使用4.22.6版本。文档:https://4x.ant.design/index-cn/

1675406354716

安装之,如下:

1675405800594

1675406313310

引入样式,如下:

1675406072857

1675406399831

1675406427263

copy连接到public文件,如下:

1675406470026

在项目中引入之,一般情况下,做管理系统,直接全部引入,不会按需加载。我们就不配置按需加载了。

测试Button组件,如下:

1675406575648

测试如下:

1675406584307

到此,我们就把antd集成到项目中了。

要使用antd中的字体图标,需要安装,如下:

cnpm install --save @ant-design/icons
1

1675407506097

# 3,搭建后台首页面

对应的组件,如下:

1675407608325

开始大胆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>
    )
}
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

效果如下:

1675408086438

需要写一些全局样式,如下:

1675408339444

引入全局样式:

1677462211975

效果如下:

1675408439435

创建layout的样式,如下:

1675408514886

开始写layout的样式,如下:

1675408708816

1677462441447

效果如下:

1675408723948

把侧边菜单,抽离成一个单独的组件,如下:

1675409125327

参考代码如下:

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',
                    },
                ]}
            />
        </>
    )
}
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

对应的idnex.jsx的代码如下:

1675409171489

把React.createElement写法,换成jsx写法,如下:

1675409673361

要把折叠收缩按钮放到侧边栏,抽离组件如下:

1675410189938

参考代码如下:

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} />
        </>
    )
}
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

在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>
    )
}
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

样式如下:

1675410245391

效果如下:

1675410266634

处理logo,有两个logo,一个大logo,一个小logo,如下:

1675410873109

默认情况下,显示大logo,折叠时,显示小logo,处理如下:

1675412212937

样式如下:

1675412228962

效果如下:

1675412249266

然后,我们处理侧边菜单,如下:

1675412446596

效果如下:

1675412456124

现在创建一些页面组件的组件,配置items,如下:

1675646277562

然后配置二级路由,如下:

1675646311830

一级路由不需要指定出口,但是二级路由需要指定出口,如下:

1675646376371

效果如下:

1675646419784

1675412930458

然后,配置items,因为侧边菜单长什么,取决items。

# 4,侧边栏菜单生成

和vue-element-admin一样,把侧边栏菜单抽离成一个配置文件,如下:

1675646583302

参考代码如下:

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: "用户管理"
    }
];
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

需要生成items,如下:

1675646687176

1675646795084

getItems如下:

1677467854297

在MlSider中,根据asyncRoutes生成items,如下:

1675647336713

在Menu组件中使用items,如下:

1675647381815

效果如下:

1675647529674

在vue-element-admin中,可以控制一个菜单的显示与隐藏,如下:

1675647611024

修改一下算法,如下:

1675647678221

效果如下:

1675647712494

还需要点击进行跳转,使用Link组件,如下:

1675647942824

1675647954028

点击测试OK,如下:

1675647968095

也需要App.jsx中的路由配置写活,如下:

1675648031827

生成的算法,如下:

1675648844570

使用算法,如下:

1675648858416

动了getItem,如下:

1675648970198

测试OK,如下:

1675648893040

现在侧边菜单有两个问题:

  • 当选中某个菜单后,刷新后,此菜单并没有选中
  • 对于二级菜单,打开后,当刷新后,又折叠起来了

解决这两个问题,需要靠两个props来解决,如下:

1675650177415

但是你上面写的1002和1003是写死的,现在需要写一个算法,动态生成上面的两个数值,我们使用自定义hook实现算法,如下:

1675650456641

在组件中使用自定义hook,如下:

1675650480562

1675650515143

我们期望得到两个key,如下:

1675650691160

开始实现useMenu这个自定义hook,如下:

1675651187146

使用之,如下:

1675651200858

测试OK,如下:

1675651225825

能不能把上面计算这两个key的代码缓存起来,使用useMemo,如下:

1675651487762

测试OK。

总结自定义hook的使用:

  • 组件主是要用来抽离UI界面的,hook和vue3一样,主要是用来抽离逻辑。抽离完后,是可以复用的,并且多次使用,彼此之间是独立的。
  • 和vue3一样,自定义hook需要使用use开头。
  • 自定义hook只能在函数式组件中使用,写在组件内,return之前。
  • 网上还有一些比较优秀的开源hook,如:react-use,ahooks。

# 5,实现面包屑导航

需求如下:

1675663922103

在组件库中找到面包屑导航,如下:

1675664038501

抽离MlHeader组件,如下:

1675664231075

在index.jsx中,使用MlHeader组件,如下:

1675664290472

浏览器中测试如下:

1675664325974

定义类名,写一点样式,如下:

1675664411900

1675664655812

现在需要小一个小算法,生成面包屑,如下:

1675665336368

在MlHeader组件中使用之,如下:

1675665365079

根据breads,生成JSX,如下:

1675665502913

优化面包屑导航,如下:

1675665620472

这样写,和我们刚才是一样的,如下:

1675665709722

引入一个hook,实现编程式路由跳转,如下:

1675666175593

点击测试OK。

如果可以跳转,鼠标放上去,变成手形,如果不能跳转,鼠标放上去,不是手形,如下:

1675666310945

测试OK。

还可以设置如果不能跳转,就变成灰色,能跳转就白色,如下:

1675666458034

如果能跳转,就设置成白色,如下:

1675666541455

效果如下:

1675666561304

判断条件也可以封装成一个函数,如下:

1675666673105

# 7,全屏与退出全屏

需求:

1675667827832

准备上面的三个icon,如下:

1675667875469

开始布局,如下:

1675668160671

1675668274526

书写样式,如下:

1675668176439

效果如下:

1675668291732

然后实现全屏与退出全屏,使用screenfull来实现,安装如下:

1675668357628

绑定点击事件,使用之,如下:

1675668665154

测试OK。

还需要优化,没有全屏和全屏下,按钮长的是不一样的,如下:

1675668769638

需要定义一个状态来控制显示哪一个图标,如下:

1675669119090

1675669133714

1675669145603

效果如下:

1675669154478

1675669166583

# 8,集成Redux

类似于vuex和pinia,用来集中状态管理的,需要使用到的依赖:

  • 经典架构:redux + react + react-redux + redux-thunk + immer
  • 最新架构:@reduxjs/toolkit + react + react-redux +ts

安装redux,如下:

1675670503480

创建仓库,语法如下:

1675670825464

需要记一些东西,如下:

  • 三个API: createStore / combineReducers / applyMiddleware
  • 三个特点:store必须是单 一数据源 / store中的状态是只读 / store中的数据只能通过reducer来修改
  • 三个概念:store是数据仓库, action(用于改变数据的对象{type: 信号,payload}),reducer(专门用来修改状态)

现在创建一个子reducer(代表创建一个子store),如下:

1675671285916

利用immer对上面的第1步进行操作,安装之,如下:

1675671344241

使用poduce进行操作,如下:

1675671816952

在index.js中,引入,合并(现在就一个,一会就会创建多个),如下:

1675671640297

到此,这个子store就准备完毕了,同理还可以准备一个子store,如下(文件名写错了,应该是user):

1675672025335

在index.js中进行合并,如下(文件名写错了,应该是user):

1675672075184

到此,仓库就准备好,你要在react组件中使用仓库中的状态,需要使用react-redux,如下:

1675672146494

在App.jsx中,使用Provider提供store,这样,子子孙孙都可以使用store,如下:

1675672306644

打印出store,如下:

1675672335626

浏览器中测试如下:

1675672493696

想获取仓库中的状态,如下:

1675672575790

现在要在组件中获取仓库中的状态,如下:

1675672674969

1675672772920

现在想获取user模块中的状态,如下:

1675672856090

简写如下:

1675672936161

修改size状态,使用dispatch,格式如下:

  • dispatch(action)
  • action是一个对象,对象中有一个type和payload

修改size,如下:

1675673166654

小的reducer收到了action,如下:

1675673259326

然后根据type,对老的状态进行操作,如下:

1675673470012

对size一次减3操作,如下:

1675673542878

reduer开始收到信号,开始操作状态,如下:

1675673606357

到此,我们就把reducx集成到了项目中。有一个图,如下:

1675673730108

# 9,组件大小的全局切换

需求:

1675732632465

实现下拉菜单,如下:

1675732728531

copy代码如下:

1675733744592

1675733757188

效果如下:

1675733783980

点击large,middle,点击small,改变仓库中的size的大小。定义状态,如下:

1675734021955

在组件中使用状态,如下:

1675734230422

1675734241454

1675734290848

测试之,如下:

1675734353264

使用行内样式,如下:

1675734435417

点击small和large,目的是让仓库中的size变化,绑定点击事件,如下:

1675734530423

实现toggleSize方法,如下:

1675734675825

1675734688827

1675734700818

点击时,如何判断这个action有没有传给reducer,可以安装一个调试工具,大家可以搜索一个调试工具,安装一下,除了安装调试工具外,还可以使用redux-logger,类似于console.log。可以新状态和老状态都打印出来。

安装之,如下:

1675734848668

这个logger是一个中间件,要使用中间件,如下:

1675735033505

浏览器中测试,如下:

1675735092579

现在就需要修改状态,如下:

1675735160176

浏览器再次测试之,如下:

1675735198123

到此,就可以完成仓库中状态的修改了。但是action我们是写死的组件中的,如下:

1675735270543

抽离action,如下:

1675735378870

在组件中使用action creater,如下:

1675735495313

1675735505641

浏览器中测试之,如下:

1675735521500

使用两个组件,如下:

1675735709991

效果如下:

1675735723042

然后要实现组件大小的全局切换,文档如下:

1675735808243

和vue-element-admin一样,全局功能都在layout文件夹中,使用之,如下:

1675735997671

1675736014571

这样就可以改变组件的大小了。测试OK。

现在有一个问题,如下:

1675736138787

解决如下 :

1675736214535

测试之,如下:

1675736287051

# 10,国际化

需求:

1675737093764

准备数据,如下:

1675737252789

浏览器中也可以测试当前支持的默认语言,如下:

1675737304592

先把下拉菜单整出来,如下:

1675737401443

效果如下:

1675737410057

在仓库中准备状态,如下:

1675737450444

在组件中就可以使用状态了,如下:

1675737537929

1675737552830

浏览器效果如下:

1675737567544

然后准备action creator,如下:

1675737636496

给下拉菜单绑定点击事件,点击diapath一个action,如下:

1675737755500

1675737769666

reducer需要接收状态,修改reducer,如下:

1675737820810

浏览器中测试之,如下:

1675737854284

现在先去实现组件的国际化,文档如下:

1675737957570

1675738005231

语言包如下:

1675738059612

使用之,如下:

1675738161289

使用之,如下:

1675738356082

测试如下:

1675738373056

1675738383996

到此,组件内置的国际就实现了。

还需要对lang进行持久化,如下:

1675757851720

通过navigator.language得到的是"zh-CN",组件中的数据如下:

1675757893623

此时测试肯定不OK。

修改组件中的数据,如下:

1675757949538

在仓库中获取zh,如下:

1675758001471

在layout组件中,也需要修改之,如下:

1675758107633

再次测试之,如下:

1675758153587

除了组件的国际化外,还有型业务中国际化,实现方案:

  • 在vue中,使用vue-i18n
  • 在react中,使用react-intl

安装react-intl,如下:

1675758307509

在src下面创建locales文件夹,如下:

1675758635414

1675758648761

使用之,如下:

1675758974851

1675759222791

此时,需要国际化的地方,就不能写死了,如下:

1675759259827

除了上面的使用方式外,还有一种方式,如下:

1675759405766

现在业务文字的国际就实现了。

现在我想把左侧菜单也实现国际化,如下:

1675759823787

1675759834686

1675759859014

路由的配置文件,参考代码如下:

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" />,
    }
];

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

效果如下:

1675759945239

1675759964497

# 11,主题色切换

什么是主题色:

1675760606800

1675760637862

1675760652817

1675760680577

1675760747014

实现设置按钮,如下:

1675760834939

把主题内容区域抽离成一个组件,如下:

1675761030531

在index.jsx中引入,使用之,如下:

1675761054311

1675761066424

效果如下:

1675761074575

书写setting的样式。如下:

1675761220998

效果如下:

1675761237043

当点击上面的按钮,需要弹出抽屉组件,如下:

1675761672370

测试效果如下:

1675761685281

提两个地方,要做国际化:

1675761720714

实现步骤,如下:

1675761890328

1675761977236

在抽屉上面,实现颜色的拾色器,使用react-color这个依赖,安装之,如下:

1675762094072

使用之,如下:

1675762260239

1675762270424

效果如下:

1675762283051

颜色也是需要存储到仓库中的,如下:

1675762466795

在组件中使用之,通过颜色选择器选中了别的颜色,需要修改仓库中的颜色,如下:

1675762626814

给颜色选择器指定默认颜色,如下:

1675762649175

效果如下:

1675762678746

当我们选择了别的颜色,需要绑定事件,如下:

1675762754142

浏览器中测试如下:

1675762779188

先定义action creater,如下:

1675762842647

然后,就可以派发这个action,如下:

1675762918352

然后是reducer收信到信号,处理之,如下:

1675762995325

浏览器测试之,如下:

1675763016984

到此,就把仓库中的颜色修改了。然后就需要改变页面中的主题色了。如下:

1675763074742

实现之,如下:

1675763342406

1675763365016

需要在main.js中引入样式,如下:

1677641319435

浏览器测试之,如下:

1675763424995

有一个问题,如下:

1675763489928

解决:

1675763519849

1675763667521

效果如下:

1675763681415

数据持久化,如下:

1675763772115

测试OK。

# 12,绘制商品表格页面(copy)

大致需求:

1675763967019

在商品列表页面和商品表格页面中都使用到了商品的分类,把商品的分类抽离成一个组件,如下:

1675819253878

参考的代码如下:

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>
    )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

在商品列表中,直接使用之,如下:

1675819314591

效果如下:

1675819324174

然后,书写一些样式,如下:

1675819351061

参考样式如下:

.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;
    }
}
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

在组件中引入样式,如下 :

1675819416368

效果如下:

1675819430323

在layout文件夹下的index.jsx中添加了如下的样式:

1675819471615

添加了最小的宽度,意味着当缩小浏览器时,页面布局就不会压缩了,如下:

1675819572106

在缩小浏览器时,到一定程度,需要让左侧菜单折叠起来,实现如下:

1675819975729

测试:

1675820014946

优化一下,如下:

1675821166620

然后绘制表格,和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>
    )
}
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

效果如下:

1675821588079

参考ant design pro,地址:https://preview.pro.ant.design/dashboard/analysis

1675821737240

先导入对应的字体图标,如下:

1675821864070

1675821924801

绘制如下:

1675822491772

效果如下:

1675822505379

还有分页,如下:

1675822626824

把表格中的都有哪列设置一下,大家直接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>
    )
}

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

效果如下:

1675823137198

当点击商品新增时,要去商品新增页面,如下:

1675823228115

1675823288466

在侧边菜单中,需要把商品新增和商品编辑隐藏掉,如下:

1675823337202

浏览器测试之,如下:

1675823356899

# 13,绘制商品表单页面(copy)

大致需求:

1675764000542

可以添加一个页面头,如下:

1675824390465

使用之,如下:

1675824603306

效果如下 :

1675824613723

开始绘制表单,如下 :

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>
    )
}
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

效果如下:

1675825650633

对商品图片上传处理,找到一个带有裁剪功能的组件,如下:

1675825699136

安装antd-img-crop,如下:

1675825732247

封装一个组件,如下:

1675825921363

在表单中使用之,如下:

1675826057265

1675826069870

效果如下:

1675826118062

处理一个这个组件,如下:

1675826398556

效果如下:

1675826409789

现在有个警告,如下:

1675826526260

解决如下:

1675826630799

测试就没有警告了,如下:

1675826649461

现在看一下,能不能收集到数据,如下:

1675826759483

收集分类数据,如下:

1675826843516

测试如下:

1675826861464

对表单进行校验,如下:

1675827041548

1675827059035

效果如下:

1675827093616

到此,商品表单页面就绘制完毕了。

商品表单页面的代码参考如下:

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>
    )
}
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

商品分类组件的代码参考如下:

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>
    )
}
1
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>
    )
}
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

# 14,绘制登录页面和联调准备

绘制结构如下:

1675827542615

参考结构如下:

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>
    )
}
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

参考样式,如下:

.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;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

效果如下:

1675827597954

安装axios,如下:

1675828573631

axios二次封装,如下:

1675827922662

参考代码如下:

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
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

配置代理解决跨域,如下:

1675828017938

参考代码如下:

 devServer: {
     port: 8080,
     open: true,  // 打包成功后,自动打开浏览器
     client: {
         overlay: {
             errors: true,
             warnings: false
         }
     },
     proxy: {
         '/api': {
             target: 'http://47.94.210.129:9999',
             changeOrigin: true
         }
     }
 },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

封装API接口,如下:

1675828134598

参考代码如下:

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
        }
    })
}
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

1675828177583

参考代码如下:

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: {}
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 15,联调登录接口(权限/难)

# 1)登录获取token,存储token

数据库有如下的数据:

1675836338573

在组件中调用接口实现登录,如下:

1675836520885

服务器响应如下:

1675836559390

思考一下,前端权限校验的流程:

  1. 点击登录,调用接口,获取token。token需要存储到redux中,还需要存储到localstorage中。
  2. 带个token,再去调用一个接口,获取用户信息,用户信息中包含角色(role)信息。也需要存储到redux中。
  3. 使用后端返回 的role和路由表,执行算法,得到当前登录用户有权访问的路由们(accessRoutes)。也是需要存储到redux中。
  4. 根据accessRoutes动态生成路由规则和Layout菜单。
  5. 跳到后面首页面/,接着重定向到/dashboard。

刚才我们是在组件中发一个登录请求,实际上这个登录请求,需要在redux中发送,类似于vuex中action中发请求。在redux中,默认情况下,是不支持写异步代码的。在redux中要写异步代码,需要借助redux-thunk中间件。安装之,如下:

1675836990817

使用中间件,如下:

1675837100678

现在准备action,action是有分类的,先看一下,我们之前写的action,如下:

1675837257662

现在写一个异步的aciton,如下:

1675837469856

在登录组件中派发异步action,如下:

1675837707106

在浏览器中测试之,如下:

1675837758732

总结:

  • redux中action creator分两类:同步action creator 和 异步action creator
  • 同步action是一个对象:{type, payload}
  • 异步action是一个函数,函数中可以写异步代码
  • dispatch默认情况下只能派发一个同步的action
  • dispatch要派发一个异步的action,需要使用redux-thunk中间件

此时,登录就OK了,如下:

1675838006361

登录成功后,把token存储到本地,和redux中,如下:

1675838175513

reduer需要处理同步action,如下:

1675838304991

浏览器测试之,如下:

1675838337080

1675838370841

到此,登录成功后,把token存储到本地和redux中了。最好判断一个登录成功了,如下:

1675839579128

在redux中,也可以对token进行持久化,如下:

1675839657092

后面再去发请求时,需要带上token,如下:

1675839724860

也就意味着,后面再去发请求,就会带上token。

# 2)模拟路由守卫

所谓的权限校验,如果你有权限,可以访问某个路由,如果没有权限,是不能访问这些路由。

现在我们的路由是所有人都可以访问的,如下:

1675839932004

删除上面的函数调用,如下:

1675840078783

测试如下:

1675840097584

1675840141552

除了上面的配置路由之外,还有一种方式,如下:

1675840433553

1675840420232

测试是OK的,如下:

1675840447664

1675840482573

可以把Page组件抽离出去,如下:

1675840673402

在App.jsx中使用Permission,如下:

1675840700823

浏览器再测试如下:

1675840712146

1675840721506

把Permission中的规则放到我们之前规则中,如下:

1675840839464

在Permisson.jsx中导入,如下:

1675841224720

再次测试之,如下:

1675841246572

1675841256177

可以把Permission.jsx当成vue中的路由守卫。

# 3)获取用户信息

在Permmion.jsx中获取token,监听token的变化,如果token变了需要重新获取用户信息,代码如下:

1675842947256

浏览器测试如下:

1675842965942

1675842981206

然后需要获取用户信息,对应的API,如下:

1675843013067

发请求是异步代码,需要写在异步的action中,如下:

1675843122081

在守卫中,token变了,就需要获取用户信息了,如下:

1675843260017

在浏览器中测试如下:

1675843296987

把用户信息存储到redux中,如下:

1675843362415

在仓库中定义状态,接收action,如下:

1675843617929

把axios的二次封装,注释一部分代码,如下:

1675843645791

浏览器测试之,如下:

1675843672968

# 4)根据roles获取accessRoutes

使用后端返回的roles数组,和路由表,算法,生成当用户可以访问的路由规则们。写一个副作用,如下:

1675843930560

在计算accessRoutes之前,给路由规则,配置meta,如下:

1675844081613

1675844472629

书写算法,如下:

1675844965348

参考代码如下:

// 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 })
    }
}
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

现在角色有了,全部的路由规则有了,算法有了,就可以生成当前用户可以访问的路由规则们,如下:

1675845604081

浏览器中测试之,如下:

1675845077653

然后,需要把路由规则们,存储到redux中,如下:

1675845135192

定义状态,让reducer接收信号,如下:

1675845633486

测试如下:

1675845671560

# 5)生成全部路由规则和Layout菜单

上面已经根据角色,计算出来,可以访问的路由规则们,然后生成全部的路由规则。你刚才计算出来的是动态路由规则,除了动态路由规则,还有静态路由规则,开始计算全部的路由规则,如下:

1675847134139

浏览器中效果如下:

1675847160641

然后,根据规则,生成侧边菜单,如下:

1675847401191

1675847418635

还需要进行跳转,如下:

1675847544742

浏览器测试之,如下:

1675847581018

1675847606636

再次测试一下:

1675847687141

1675847696148

还有一个比较大的流程图:

1675847817226

# 16,优化一下权限设计

让admin用户看到首页大屏,商品管理,用户管理,如下:

1675905428308

admin用户登录上去,如下:

1675905517306

之前写了多个副作用,如下:

1675905805252

在第1个副作用中添加一个判断,如下:

1675905880624

现在把token删除了,当刷新时,就会直接去登录页面,如下:

1675905945086

根据之前vue-element-admin那个权限设计图,优化一下代码。先设置一个白名单,如下:

1675906001887

需要获取当前访问的路径,如下:

1675906273204

去访问非login路由,并且没有token,如下:

1675906330818

还有在获取用户信息时,最好也加上判断,如下:

1675906447137

继续优化,如果有token,你访问的还是登录页面,需要放行到后台首页面,如下:

1675906821816

现在放到了/这个路由了,如下:

1675906839891

如果访问了/,还需要重定向到/dashboard,如下:

1675906930375

访问/login,放到了/,重定向到了/dashboard,如下:

1675906958539

如果token被篡改,过期了,如下:

1675907429355

看服务器响应什么,如下:

1675907463347

在响应拦截器中处理之,如下:

1675907540295

书写resetUser这个action,如下:

1675907630459

reducer接收到这个信号,处理之,如下:

1675907723364

现在token被篡改了,测试如下:

1675907749065

1675907769402

1675907798246

1675907817251

完整的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;
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

# 17,echarts和bizcharts

在管理系统中,图表也是非常重要的。我们说两个

  • echarts 不管是vue或react都可以使用,DOM图。
  • bizcharts 只能在react中使用,里面是一堆的组件,用什么图,导入什么组件。非常简单。
  • 除了上面的这两个图表之外,还有其它的图表

先讲echarts,官网:https://echarts.apache.org/zh/index.html

1675908022670

不用通过npm去安装,直接使用cdn,如下:

1675908087659

1675908151140

1675908321276

证明echart导入成功了,如下:

1675908337203

浏览器中测试之如下:

1675908352681

echarts是用来绘制图表的,每一个图表都需要一个容器,就是一个div,还需要指定宽高,如下:

1675908477528

开始绘图,绘图的步骤:

  • 1)创建图表实例
  • 2)配置图表选项
  • 3)setOption()

在管理系统中,有几类图表用的是最多的

  • 柱状图
  • 折线图
  • 饼图

开始绘制,如下:

1675910826645

参考代码如下:


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>
    )
}
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

效果如下:

1675910862521

把面的图,封装成一个组件,如下:

1675912008516

在在大屏页面中,导入,如下:

1675912312165

在vue-element-admin,它的图表是响应式,如下:

1675912388405

现在我们的图表并不是响应的,如下:

1675912433424

把图片处理成响应式,需要设置容器的宽度是100%,如下:

1675912471354

当浏览器宽度变小时,需要重新调用echarts实例的resize方法,如下:

1675912708840

1678086786493

现在浏览器容器一变化,立即触发resize,要做防抖处理。在vue-element-admin中人家封装好了防抖函数,我们只需要直接引入这个防抖函数,使用之。自习实现。

下去,大家把vue-element-admin中的四个图表,迁移到react项目中。

我们再去了解一种图片,叫bizchart,只能在react中使用,都是封装好的组件。

官网:https://bizcharts.taobao.com/

1675913077195

1675913096661

上面的图,都是以组件的形式提供的。如下:

1675913171121

要使用bizcharts,需要安装,如下:

1675913212262

封装一个柱状图,如下:

1675913654524

在首页面中引入之,如下:

1675913671326

效果如下:

1675913684319

# 18,联调商品模块的接口

# 1)商品分类的渲染

商品分类接口如下 :

1676009094682

封装action,如下:

1676009126742

在商品分类模块中调用接口如下:

1676009173366

浏览器中就可以得到商品分类数据,如下:

1676009191196

把数据存储到redux中,如下:

1676009230956

准备good模块,如下:

1676009277874

合并之,如下:

1676009319790

获取分类数据并渲染如下:

1676009425932

浏览器中测试如下:

1676009439352

添加全部分类,如下:

1676009488041

测试如下:

1676009504259

在使用商品分类组件中,指定value,如下:

1676009590009

定义相关状态,收集数据,如下:

1676009742170

测试是否可以获取商品分类数据,如下:

1676009873324

上面这样的组件,叫受控组件。

# 2)商品列表的渲染

准备API接口如下:

1676009941359

在组件中直接引入,然后调用接口如下:

1676010179708

浏览器中测试之,如下:

1676010196407

更改表格的数据源,如下:

1676010298304

浏览器中测试如下:

1676010259955

指定分页,如下:

1676010375514

1676010390303

浏览器中测试如下:

1676010412012

安装moment模块,处理发布时间,如下:

1676010575762

设置表格的列信息如下:

1676010922324

参考代码如下:

// 表格的列表(如果要做自定义列,使用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
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

浏览器效果如下:

1676010952767

# 3)商品新增

点击跳转到商品新增页面,如下:

1676011092106

点击测试可以到达商品新增页面,收集页面中的数据,只能图片还不能收集,如下:

1676011184102

此时,商品分类的下拉菜单不能显示分类了,处理之,如下:

1676011278722

在商品列表中传入hasAll,如下:

1676011330963

这样,在商品列表页面有全部选项,在商品添加页面没有全部选项,如下:

1676011364064

1676011372366

二次封装图片上传组件(自行分析),如下:

1676011494500

参考代码如下:

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>
    )
}
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

测试如下:

1676011637887

此时,再去收集数据,就OK了,如下:

1676011702849

点击添加数据,API接口,如下:

1676011905803

点击确定,实现添加,如下:

1676011947979

测试如下:

1676012007075

到此,商品添加就实现了。

# 4)商品编辑实现

配置动态路由,如下:

1676012138176

点击编辑去商品编辑页面,如下:

1676012246205

点击测试之,如下:

1676012268355

此时,商品新增和商品编辑共用同一个页面。

在商品编辑页面中,获取到商品ID,如下:

1676012356614

根据ID发送ajax请求,获取商品详情,实现数据回显如下:

1676012489407

浏览器中测试之,如下:

1676012502530

然后实现编辑,如下:

1676012578419

测试如下:

1676012622878

1676012630942

到此,商品编辑实现。

# 5)优化

指定表单页面是新增还是编辑,如下:

1676014617871

指定提交按钮是新增还是编辑,如下:

1676014669700

测试如下:

1676014698170

实现分页,如下:

1676015169037

1676015180460

实现搜索:

1676015849947

1676015868316

浏览器测试如下:

1676015882703

Last Updated: 7/24/2023, 8:24:40 AM