前端React项目_全球新闻发布管理系统

全球新闻发布管理系统

项目介绍

该项目是一个全球新闻发布管理系统,可供普通游客,超级管理员,区域管理员,和区域编辑四种角色访问,针对不同的角色所展示的页面也不相同。
对于游客而言可以访问到新闻展示页面和新闻详情页面;对于超级管理员而言,可以对用户列表,角色列表,不同角色对应的权限进行管理,并且可以撰写新闻和审核新闻;对于区域管理员而言,它可以管理相应的区域编辑,发布新闻,并且对该区域的新闻进行审核和发布;而对于区域编辑而言,仅可以撰写新闻,审核自己的新闻以及发布该新闻。

所用到的技术栈有React、Axios、
版本:

项目预览

项目准备

  1. react脚手架
    npm create-react-app global
  2. 安装依赖
  • npm i --save sass

    解决css嵌套。
    Sass详解

  • npm i --save http-proxy-middleware
    解决跨域问题
    proxy详解
    具体实现

//  src/setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');
 
module.exports = function (app) {
    app.use(
        '/api',
        createProxyMiddleware({
           //表示存储当前数据的目标路径
            target: 'http://localhost:5000',
            changeOrigin: true,
        })
    );
}
//  src/app.js
  useEffect(()=>{
      axios.get("/api/mmdb/movie/v3/list/hot.json?ct=%E7%B9%81%E6%98%8C%E5%8C%BA&ci=774&channelId=4").then(res=>{
        console.log(res.data)
      })
    },[])


  1. 报错解决
    部分报错原因解决

路由搭建

  1. 路由架构框架
    在这里插入图片描述
// src/router/indexrouter.js

import React from 'react'
import { HashRouter ,Route} from 'react-router-dom'
import login from '../views/login/login'
import NewsSandBox from '../views/sandbox/NewsSandBox'

// 精确匹配使用switch组件-即匹配到就break
//判断是否具有token授权,如果没有授权就会重定向到login中
export default function indexRouter() {
  return (
    <HashRouter>
        <switch>
        <Route path='/login' Component={login}/>
        {/*<Route path='/' Component={NewsSandBox}/>*/ }
        <Route path='/' render={()=>
        localStorage.getItem('token')?
        <NewsSandBox></NewsSandBox>:<redirect to='/login'/>
    }/>
        </switch>
    </HashRouter>
  )
}
  1. 路由搭建
  • 在src文件夹下建立component文件夹,写入页面中共享的SideMune侧边栏组件和TopHeader头部组件
  • 对于页面中的路由展示区,使用Switch(Route)组件来匹配路径以及对应路由

具体实现

import React from 'react'
import SideMenu from '../../components/sandbox/SideMenu'
import TopHeader from '../../components/sandbox/TopHeader'
import Home from '.\home\Home.js'
import UserList from './user-manage/UserList'
import RoleList from '.\right-manage\RoleList.js'
import RightList from '.\right-manage\RightList.js'
import Nopermission from '.\Nopermission\Nopermission.js'

export default function NewsSandBox() {
  return (
    <div>
      <SideMenu></SideMenu>
      <TopHeader></TopHeader>
      <Routes>
        <Route path="home" element={<Home />} />
        <Route path="user-manage/list" element={<UserList />} />
        <Route path="right-manage/role/list" element={<RoleList />} />
        <Route path="right-manage/right/list" element={<RightList />} />
        <Route path="/" element={<Navigate replace from="/" to="home" exact/>} />
        <Route path="/*" element={<Nopermission/>} />
      </Routes>


    </div>
  )
}

antd组件库的引入

  1. antd引入
    antd官网
    antd组件总览
  • npm i --save antd
    具体实现
    在这里插入图片描述
    在App.css中引入
    在这里插入图片描述
  1. layout布局
    antd布局
    具体实现
    在这里插入图片描述

组件

TopHeader组件
  • 细节处理
import React, { useState } from "react";
import { Layout, Dropdown, Menu, Avatar } from "antd";
import {
  MenuUnfoldOutlined,
  MenuFoldOutlined,
  UserOutlined,
} from "@ant-design/icons";

const { Header} = Layout;
const menu = (
  <Menu>
    <Menu.Item key="username">{role.roleName}</Menu.Item>
    <Menu.Item
      danger
      key="loginout"
      onClick={() => {
        localStorage.removeItem("token");
        props.history.replace("/login"); //路由定位到login中
      }}
    >
      退出登录
    </Menu.Item>
  </Menu>
);


export default function TopHeader() {
  return (
    <Header className="site-layout-background" style={{ paddingLeft: "16px" }}>
    {/* {React.createElement(
        this.state.collapsed ? MenuUnfoldOutlined : MenuFoldOutlined,
        {
          className: "trigger",
          onClick: this.toggle,
        }
      )} */}
    {props.isCollapsed ? (
      <MenuUnfoldOutlined onClick={changeCollapsed} />
    ) : (
      <MenuFoldOutlined onClick={changeCollapsed} />
    )}
    <div style={{ display: "inline", float: "right" }}>
      <span>
        欢迎<span style={{ color: "#1890ff" }}>{username}</span>回来
      </span>
      <Dropdown overlay={menu}>
        <Avatar size="large" icon={<UserOutlined />} />
      </Dropdown>
    </div>
  </Header>
  )
}

主要是添加了下拉菜单等操作,注意每个组件的调用,都要在前面import

  • menu
    在这里插入图片描述
  • 菜单头像图标设置
    在这里插入图片描述
  • 在渲染TopHeader组件时,首先判断本地存储中是否有用户的token,如果有则直接展示用户名,没有的话则重定向至登录界面。
const { role: { roleName }, username } = JSON.parse(localStorage.getItem("token"))
 
<div style={{ float: "right" }}>
                <span>欢迎<span style={{ color: "#1890ff" }}>{username}</span>回来</span>
                <Dropdown overlay={menu}>
                    {/* 头像图标 */}
                    <Avatar size="large" icon={<UserOutlined />} />
                </Dropdown>
            </div>
SideMenu组件
  • 下拉菜单
    具体实现
const { SubMenu } = Menu;
<SubMenu key='sub4' icon={<UploadOutlined/>}title='用户管理'>          
<Menu.Item key='9'>option 9</Menu.Item>
<Menu.Item key='10'>option 10</Menu.Item>
<Menu.Item key='11'>option 11</Menu.Item>
</SubMenu>
  • 动态SideMenu
    权限是来自后端传过来的一个数组结构,根据数组结构动态创建一个侧边栏。
    数组结构具有key title icon三种属性
  1. 数组结构模板
const menuList = [
  {
    key: "/home",
    title: "首页",
    icon: <HomeOutlined />,
  },
  {
    key: "/user-manage",
    title: "用户管理",
    icon: <UserOutlined />,
    children: [
      {
        key: "/user-manage/list",
        title: "用户列表",
        icon: <UserOutlined />,
      },
    ],
  },
  {
    key: "/right-manage",
    title: "权限管理",
    icon: <CrownOutlined />,
    children: [
      {
        key: "/right-manage/role/list",
        title: "角色列表",
        icon: <CrownOutlined />,
      },
      {
        key: "/right-manage/right/list",
        title: "权限列表",
        icon: <CrownOutlined />,
      },
    ],
  },
];
  1. 动态数据获取
    具体实现_____embed=children表示获取当前数据的内联数据
useEffect(() => {
    axios.get("/rights?_embed=children").then(res => {
      // console.log(res.data)
      setMeun(res.data)
    })
  }, [])

根据数据渲染界面,需要用到Menu和SubMenu两个antd组件库

  1. 利用函数实现动态数组

在这里插入图片描述
千峰视频中用到的高阶组件时 withRouter,但是本项目中使用的是useNavigate

高阶组件的定义以及使用场景

JsonServer

JsonServer使用详解

1. 引入
全局安装npm i -g json-server

  • 测试json-server:在任务管理器中执行:json-server --watch .\test.json --port 8000,这个表示给当前目录下的test.json文件开启8000端口号,在json文件中,一级的key会自动当成接口来使用。
  • json-server为了方便大家取数据,在启动创建server时已经解决了跨域的问题
  • 在 json 中就直接有接口

2.json-server的增、删、改、查,联合取

  • 查get
axios.get('http://localhost:8000/posts/2').then(res=>{
    console.log(res)
})
  • 增post
axios.post('http://localhost:8000/posts').then(res=>{
    //id不用写,会自增长
    title:'2222'
    author:'zhanzhanhz'
})
  • 改有put和patch两个字段,put的意思是全部替换,而patch则是只修改提交的那部分
axios.put('http://localhost:8000/posts/1').then(res=>{
    //全部改变
    title:'1-修改'
   })
axios.patch('http://localhost:8000/posts/1').then(res=>{
    //只会修改id为1的title,其他的信息不变
    title:'2222-22222'
   })
  • 删delete,但是这里有个问题,如果新闻被删除了,那么新闻所关联的一些评论也会被删除。
axios.delete('http://localhost:8000/posts/1')
  • _embed表连接,将所有的数据以及它们的关联数据都取出来–向下关联
axios.get('http://localhost:8000/posts?_embed=comments').then(res=>{
    console.log(res)
})
  • _expand向上查找,注意这里的comments,post和接口posts的写法–向上查找
axios.get('http://localhost:8000/comments?_expand=post').then(res=>{
    console.log(res)
})

后端SideMenu

  • 发出ajax请求
import axios from "axios";
function SideMenu(props) {
  useEffect(() => {
    axios.get("/rights?_embed=children").then((res) => {
      setMenu(res.data);
    });
  }, []);}
  • 获取后端权限列表数据并存储,将menu数据传入到 组件中
const [Menu, setMenu] = useState([])
  useEffect(() => {
    //内嵌它的子数据
    axios.get("/rights?_embed=children").then(res => {
      setMenu(res.data)
    })
  }, [])
{/* 封装函数遍历生成菜单项 */}
{renderMenu(menu)}
  • 用pagepermisson来控制SubMenu中的字段的显示与隐藏,事先在代码中设置好路径和图标的对应的键值对

在这里插入图片描述

checkPagePermission这个函数用来判断数据中是否有pagepermisson属性

  • 图标的映射
    在这里插入图片描述
    细节问题及解决方式
  1. 解决首页有下拉菜单
    在这里插入图片描述
  2. 解决滚动条侧边栏和首页一起滚动的问题
    在这里插入图片描述
  • SideMenu的默认选中
  1. 用高阶组件来获取祖辈传过来的props,其中props.location中有pathname这个路径值, const selectKeys = [props.location.pathname]:
  2. 默认展开是用到defaultOpenKeys这个属性,但是只对一级Menu生效,故需要对pathname进行截取。
  3. 受控和非受控的概念:
    受控组件:外部状态改变了,内部组件也会受到影响;
    非受控组件:外部状态改变了,内部组件只在第一次有影响就是非受控组件。
    在这里插入图片描述
    受控组件和非受控组件

–可产生同样的效果

P15在这里插入图片描述

权限列表

在这里插入图片描述

给整体权限进行配置,侧边栏通过权限列表进行动态配置

  1. 权限列表的数据获取
useEffect(() => {
        axios.get("/rights?_embed=children").then(res => {
            const list = res.data
            list.forEach(item => {
                if (item.children.length === 0) {
                    // 树形数据展示:当数据中有children字段时会自动展示为为树形表格,如果不需要或配置为其它字段,可以用childrenColumnName进行配置。
                    item.children = ""
                }
            })
            setdataSource(list)
        })
    }, [])

获取权限数据以及它的关联子数据存入dataSource中。

  1. 页面表格布局
    定义表格布局:
 const columns = [
        {
            title: 'ID',
            dataIndex: 'id',
            render: (id) => {
                return <b>{id}</b>
            }
        },
        {
            // 将dataSource中的key为title的值放在这一列
            title: '权限名称',
            dataIndex: 'title'
        },
        {
            title: "权限路径",
            dataIndex: 'key',
            render: (key) => {
                return <Tag color="orange">{key}</Tag>
            }
        },
        {
            title: "操作",
            // item为datasourse中当前点击的这一项
            render: (item) => {
                return <div>
                    <Button danger shape="circle" icon={<DeleteOutlined />} onClick={() => confirmMethod(item)} />
                    {/* 卡片弹框 配置项*/}
                    <Popover content={<div style={{ textAlign: "center" }}>
                        <Switch checked={item.pagepermisson} onChange={() => switchMethod(item)}></Switch>
                        {/* 如果没有配置pagepermisson这一项时,则会禁用按钮 trigger动态设置 item.pagepermisson是一个受控属性*/}
                    </div>} title="页面配置项" trigger={item.pagepermisson === undefined ? '' : 'click'}>
                        <Button type="primary" shape="circle" icon={<EditOutlined />} disabled={item.pagepermisson === undefined} />
                    </Popover>
                </div>
            }
        }
    ];

定义页面中操作数据的方法:
confirmMethod: 卡片弹框

    const confirmMethod = (item) => {
        confirm({
            title: '你确定要删除?',
            icon: <ExclamationCircleOutlined />,
            onOk() {
                deleteMethod(item)
            },
            onCancel() {
            },
        });
    }

deleteMethod:删除方法

    //删除,这里的item是当前点击的这一项的全部数据
    const deleteMethod = (item) => {
        // 当前页面同步状态 + 后端同步
        if (item.grade === 1) {
            // filter方法,过滤出能使表达式成立的项
            setdataSource(dataSource.filter(data => data.id !== item.id))
            axios.delete(`/rights/${item.id}`)
        } else {
            // 找到children的上一级数据(子找父)
            let list = dataSource.filter(data => data.id === item.rightId)
            // 父找子
            list[0].children = list[0].children.filter(data => data.id !== item.id)
            setdataSource([...dataSource])
            axios.delete(`/children/${item.id}`)
        }
    }

switchMethod:控制权限是否打开的开关

    const switchMethod = (item) => {
        // 改变dataSource中的pagepermisson值
        item.pagepermisson = item.pagepermisson === 1 ? 0 : 1
        setdataSource([...dataSource])
        //同步后端patch方法,put方法会全部替换掉,patch是补丁的更新
        // 如果是父,直接携带item.id至rights这个接口
        if (item.grade === 1) {
            axios.patch(`/rights/${item.id}`, {
                pagepermisson: item.pagepermisson
            })
            // 如果是子,携带item.id至children这个接口
        } else {
            axios.patch(`/children/${item.id}`, {
                pagepermisson: item.pagepermisson
            })
        }
    }

渲染Table布局

            <Table dataSource={dataSource} columns={columns}
                // 分页器
                pagination={{
                    pageSize: 5
                }} />

页面中用到的Button, Table, Tag, Modal, Popover, Switch组件都是从antd中拿到的,DeleteOutlined, EditOutlined, ExclamationCircleOutlined是从@ant-design/icons拿到的, confirm是从Modal中解构出来的。

角色列表

为某个用户分配到当前角色,就能拥有当前用户所需要的权限,该项目的角色分为:
超级管理员:可以写新闻,审批所有人的新闻,创建账号,修改角色的权限分配

区域管理员:可以写新闻,审核自己所在区域的新闻,发布新闻

区域编辑:负责写新闻,将新闻存储到草稿箱中

1. 数据获取

    useEffect(() => {
        //获取角色信息
        axios.get("/roles").then(res => {
            setdataSource(res.data)
        })
    }, [])
    useEffect(() => {
        // 将权限赋值给弹出框中的树形结构
        axios.get("/rights?_embed=children").then(res => {
            setRightList(res.data)
        })
    }, [])

2. 页面表格布局

  • 定义表格布局
    同权限列表类似,table中需要一个唯一key值,权限列表中由于后端返回的数据中有key这个选项,Table自动识别。
            <Table dataSource={dataSource} columns={columns}
                // table中需要一个唯一key值,字段中没有key值时,找id作为其key值rowKey
                rowKey={(item) => item.id}></Table>

树形结构

  • 定义弹出框
            <Modal title="权限分配" visible={isModalVisible} onOk={handleOk} onCancel={handleCancel}>
                <Tree
                    checkable
                    // 当前角色的权限,checkedKeys受控属性
                    //antd中加上default就是非受控,只有第一次才有效
                    checkedKeys={currentRights}
                    onCheck={onCheck}
                    //checkable状态下节点选择完全受控(父子节点选中状态不再关联)
                    checkStrictly={true}
                    // 将权限给treeData
                    treeData={rightList}
                />
            </Modal>
  • 处理数据的方法
    通过勾选弹出框中的用户权限选项编辑用户权限,再将更新后的权限同步datasource和后端
    在之前同步好item.id
    在这里插入图片描述
    const handleOk = () => {
    //console.log(currentRights)
        setisModalVisible(false)
        //同步datasource
        setdataSource(dataSource.map(item => {
            if (item.id === currentId) {
                return {
                //展开item
                    ...item,
                    // 当前勾选框中的权限赋值给dataSource中的rights
                    rights: currentRights
                }
            }
            return item
        }))
        //patch,将数据同步至后端
        axios.patch(`/roles/${currentId}`, {
            rights: currentRights
        })
    }

用户列表

用户列表作为某些角色,其角色又有哪些权限,来实现其权限的展示。
该部分是用来管理系统中各登录用户,包括开启/关闭用户登录状态,更新用户数据,删除用户以及添加用户等功能。
1. 数据获取

    useEffect(() => {
        const roleObj = {
            "1": "superadmin",
            "2": "admin",
            "3": "editor"
        }
        // 回获取用户数据以及其关联的角色数据
        axios.get("/users?_expand=role").then(res => {
            const list = res.data
            // 如果是超级管理员,则将信息全部展示出来
            setdataSource(roleObj[roleId] === "superadmin" ? list :
                // 不是超级管理员则将是管理员
                [
                    // 将自己过滤出来
                    ...list.filter(item => item.username === username),
                    // 将该管理员所在区域的编辑筛选出来
                    ...list.filter(item => item.region === region && roleObj[item.roleId] === "editor")
                ])
        })
    }, [roleId, region, username])
    // 获取表单下拉框的地区数据
    useEffect(() => {
        axios.get("/regions").then(res => {
            const list = res.data
            setregionList(list)
        })
    }, [])
    // 获取表单下拉框的角色数据
    useEffect(() => {
        axios.get("/roles").then(res => {
            const list = res.data
            setroleList(list)
        })
    }, [])
  1. 生成Table结构
    这部分同权限列表和用户列表,但是此处的区域的表头部分增加了一个筛选功能,用户可以自行选择查看某个区域的用户:
const colums=[
{
            title: "角色名称",
            //需要前面axios的地址指向users?expand=role
            dataIndex: 'role',
            render: (role) => {
                return role?.roleName
            }
        }
        ]
            filters: [
                ...regionList.map(item => ({
                    text: item.title,
                    value: item.value
                })),
                {
                    text: "全球",
                    value: "全球"
                }
            ],
            // onFilter为过滤后满足条件的值
            onFilter: (value, item) => {
                if (value === "全球") {
                    // 全球字段在后端的数据中显示为''
                    return item.region === ""
                }
                return item.region === value
            },
  1. 添加用户
    模态框如下:

模态框嵌入的是表单的内容

            <Modal
                visible={isAddVisible} title="添加用户" okText="确定" cancelText="取消"
                onCancel={() => {
                    setisAddVisible(false)
                }}
                onOk={() => addFormOK()}>
                <UserForm regionList={regionList} roleList={roleList} ref={addForm}></UserForm>
            </Modal>
      <Form>
      //垂直布局
        layout='vertical'
        <Form.Item
        name='username'
        label='用户名'
        rules={[{required:true,message:'Please input the title of collection!}]}
        ></Form.Item>
       <Form.Item
        name='ogin'
        label='quyu'
        rules={[{required:true,message:'Please input the title of collection!}]}
        >
        //在弹出的表单中显示下拉列表
        <Seclect>
        //<Option value='aaa'>aaa</Option>
        //<Option value='bbb'>bbb</Option>
        //<Option value='ccc'>ccc</Option>
        //动态创建option
        {
        //map遍历实现动态创建
        regionList.map(item=>
        <Option value={item.value} key={item.id}>{item.title}</Option>
        )
        }
        </Seclect>
        </Form.Item>
        <Input/>
      </Form>

此处将模态框的表单组件封装成一个共享组件:UserForm,供添加用户的模态框和更新用户的模态框复用在UserForm组件中引入表单antd组件库中的Form组件,为了使父组件能够拿到子组件中的表单值,这里使用了forwardRef
在这里插入图片描述

,用于将ref转发给父组件。收集子组件中表单列表的值,然后更新datasource和后台数据:
父组件Userlist往userform子组件上仍了一个ref,ref会出现在Form内部
高阶组件forwardRef的用法
forwardRef的用法

const UserForm = forwardRef((props, ref) => {
  const [isRegionDisabled, setIsRegionDisabled] = useState(false);

  const user = JSON.parse(localStorage.getItem("token"));

  useEffect(() => {
    setIsRegionDisabled(props.isUpdateRegionDisabled);
  }, [props.isUpdateRegionDisabled]);

  const isRegsionUnselected = (region) => {
    if (user.roleId === 1) {
      return false;
    } else {
      return user.region !== region.value;
    }
  };

  const isRoleUnselected = (role) => {
    if (user.roleId === 1) {
      return false;
    } else {
      return role.id <= user.roleId;
    }
  };

  return (
    <div>
      <Form layout="vertical" ref={ref}>
        <Form.Item
          name="username"
          label="用户名"
          rules={[
            {
              required: true,
              message: "请输入用户名",
            },
          ]}
        >
          <Input type="text" placeholder="请输入用户名" />
        </Form.Item>
        <Form.Item
          name="password"
          label="密码"
          rules={[
            {
              required: true,
              message: "请输入密码",
            },
          ]}
        >
          <Input type="password" placeholder="请输入密码" />
        </Form.Item>
        <Form.Item
          name="region"
          label="区域"
          rules={
            isRegionDisabled
              ? []
              : [
                  {
                    required: true,
                    message: "请选择区域",
                  },
                ]
          }
        >
          <Select
            style={{ width: "100%" }}
            placeholder="请选择区域"
            disabled={isRegionDisabled}
          >
            {props.regionList.map((region) => {
              return (
                <Option
                  key={region.id}
                  value={region.value}
                  disabled={isRegsionUnselected(region)}
                >
                  {region.title}
                </Option>
              );
            })}
          </Select>
        </Form.Item>
        <Form.Item
          name="roleId"
          label="角色"
          rules={[
            {
              required: true,
              message: "请选择角色",
            },
          ]}
        >
          <Select
            style={{ width: "100%" }}
            placeholder="请选择角色"
            onChange={(value) => {
              if (value === 1) {
                setIsRegionDisabled(true);
                ref.current.setFieldsValue({
                  region: "",
                });
              } else {
                setIsRegionDisabled(false);
              }
            }}
          >
            {props.roleList.map((role) => {
              return (
                <Option
                  key={role.id}
                  value={role.id}
                  disabled={isRoleUnselected(role)}
                >
                  {role.roleName}
                </Option>
              );
            })}
          </Select>
        </Form.Item>
      </Form>
    </div>
  );
});

export default UserForm;

添加用户
validateFields方法,如果成功走的是then,如果失败走的是catch
在这里插入图片描述

props属性,形参传给你信息
props详解

    const addFormOK = () => {
        // 收集表单的信息
        addForm.current.validateFields().then(value => {
            setisAddVisible(false)
            addForm.current.resetFields()
            //post到后端,生成id,再设置 datasource, 方便后面的删除和更新
            axios.post(`/users`, {
                ...value,
                "roleState": true,
                "default": false,
            }).then(res => {
                setdataSource([...dataSource, {
                    ...res.data,
                    // role这个字段是在联表中,故第一次刷新的过程中会拿不到,
                    role: roleList.filter(item => item.id === value.roleId)[0]
                }])
            })
        }).catch(err => {
            console.log(err)
        })
    }

更新用户
复用上一小节的UserForm组件,但是因为是更新列表,因此在弹出框中应该将原始数据展示出来,代码如下:

    const handleUpdate = (item) => {
        setisUpdateVisible(true)
        setTimeout(() => {
            if (item.roleId === 1) {
                //禁用 
                setisUpdateDisabled(true)
            } else {
                //取消禁用
                setisUpdateDisabled(false)
            }
            // 动态设置表单的初始值
            updateForm.current.setFieldsValue(item)
        }, 0)
 
        setcurrent(item)
    }

更新用户
在用户状态的开关的更新
在这里插入图片描述

在这里插入图片描述
模态框的热填充
需要使用setFieldValue,同时在react中状态更新并不保证同步,需要使用异步,使得模态框的创建和updateForm同步执行(同步触发)。
在这里插入图片描述
useEffect详解
副作用钩子
根据依赖的值是否发生改变再确定是否重新改变
在这里插入图片描述
钩子函数
钩子函数略解

钩子函数略解
钩子函数详解
全局钩子
钩子函数教程
回调函数
将更新后的数据重新提交至datasource和后端:

    const updateFormOK = () => {
        updateForm.current.validateFields().then(value => {
            // console.log(value)
            setisUpdateVisible(false)
            setdataSource(dataSource.map(item => {
                if (item.id === current.id) {
                    return {
                        ...item,
                        ...value,
                        role: roleList.filter(data => data.id === value.roleId)[0]
                    }
                }
                return item
            }))
            setisUpdateDisabled(!isUpdateDisabled)
 
            axios.patch(`/users/${current.id}`, value)
        })
    }

删除用户部分的逻辑和权限列表和角色列表相似。

– 筛选用户没看

登录界面

1. topHeader组件

登录界面的跳转有两种方式:

  1. 页面初始渲染时,路由的路径是’ ‘,将其重定向至登录路由组件;
    1. 点击退出按钮,自动跳转至/login界面,这里用到了高阶组件withrouter和路由的redirect属性,这里介绍后面一种:
            <Menu.Item danger onClick={() => {
                localStorage.removeItem("token")
                // console.log(props.history)
                props.history.replace("/login")
            }}>退出</Menu.Item>

2. login页面(粒子效果)

引入Form表单,点击提交按钮,立即触发表单校验功能,然后将数据提交给onFinish,在onFinish中,进行数据提交

  // json serve不能用用post请求,故用get请求代替post,如果get请求有返回,说明账号和密码正确
const onFinish = (values) => {
    // 向user接口发数据,查询username、password、roleState,以及用户列表的关联角色列表的数据,四项数据均正确才能返回正确的数据
    axios.get(`/users?username=${values.username}&password=${values.password}&roleState=true&_expand=role`).then(res => {
      if (res.data.length === 0) {
        message.error("用户名或密码不匹配")
      } else {
        localStorage.setItem("token", JSON.stringify(res.data[0]))
        console.log(JSON.stringify(res.data[0]));
        props.history.push("/")
      }
    })
  }

3. 粒子效果
引入react-tsparticles粒子库,在页面中使用该组件,

<Particles
    height={document.documentElement.clientHeight} params={...}

但是这样设置的话会使粒子界面的高度高过视图高度出现滚动栏,因此要给父元素设置 overflow: ‘hidden’

若一开始页面加载时重定向至登录页面,但访问者没有账号或不想登录时,我们给访问者提供一个游客访问的接口:

        <a style={{ color: "white" }} href={`#/news`}>游客模式</a>

4. sideMenu组件完善
根据登录的不同的用户展示不同的sideMenu,因此要在checkPagePermission的代码中判断当前登录的用户的权限列表包括item.key才有可能继续展示侧边栏。

  const { role: { rights } } = JSON.parse(localStorage.getItem("token"))
  // item.key表示侧边栏的数据
  const checkPagePermission = (item) => {
    // 当前登录的用户的权限列表包括item.key才有可能继续修改侧边栏,如果没有则应该被隐藏
    return item.pagepermisson && rights.includes(item.key)
  }

5. userList:不同的用户身份拥有不同的权限
在用户更新权限中,全球管理员拥有任何的权限,区域管理员的修改区域以及角色身份的选项被禁用,而在添加用户时,将区域编辑的修改角色的的选项给禁用掉,将区域管理员的修改区域的选项给禁用。

    const checkRegionDisabled = (item) => {
        // 如果是更新
        if (props.isUpdate) {
            if (roleObj[roleId] === "superadmin") {
                // 禁用为假,不禁用
                return false
            } else {
                return true
            }
            // 如果是创建
        } else {
            // 如果是超级管理员
            if (roleObj[roleId] === "superadmin") {
                return false
                // 如果是区域管理员,禁用非该区域
            } else {
                return item.value !== region
            }
        }
    }
    const checkRoleDisabled = (item) => {
        if (props.isUpdate) {
            if (roleObj[roleId] === "superadmin") {
                return false
            } else {
                return true
            }
        } else {
            if (roleObj[roleId] === "superadmin") {
                return false
            } else {
                // 将非编辑的角色按钮给禁用
                return roleObj[item.id] !== "editor"
            }
        }
    }

路由权限

动态创建路由
在这里插入图片描述

配置路由
将rights接口和children接口中的数据取出来
合并两个数据
在这里插入图片描述
并拼接存入BackRouteList,Switch组件遍历BackRouteList:
react的路由选择是模糊匹配,真正的路径不匹配路由匹配

            <Switch>
                {
                    BackRouteList.map(item => {
                        // 组件被渲染的两个条件
                        if (checkRoute(item) && checkUserPermission(item)) {
                            // 二级匹配,避免出现二级路由被重复匹配的情况
                            return <Route path={item.key} key={item.key} component={LocalRouterMap[item.key]} exact />
                        }
                        return null
                    }
                    )
                }
                <Redirect from="/" to="/home" exact />
                {
                    // 若数据还没回来,则不会重定向
                    BackRouteList.length > 0 && <Route path="*" component={Nopermission} />
                }
            </Switch>

判断当前用户登录状态下是否可以显示该路径的两个条件:

    // 当前角色下的权限
    const { role: { rights } } = JSON.parse(localStorage.getItem("token"))
    const checkRoute = (item) => {
        // 判断后端返回的路径中有没有当前路径,当前路径是item.key,并且该项权限是开着的且有routepermisson这个选项
        return LocalRouterMap[item.key] && (item.pagepermisson || item.routepermisson)
    }
    // 登录的用户有该权限
    const checkUserPermission = (item) => {
        return rights.includes(item.key)
    }

NProgress进度条
npm安装后在NewsSandBox.js引入使用:
点击切换页面时,匹配到最外层的路由,在渲染刚开始启动NProgress。

    // 渲染开始
    NProgress.start()
    // 渲染结束开始发送请求时
    useEffect(() => {
        NProgress.done()
    })

配置请求路径
新建util文件夹,在http.js文件中进行配置:

axios.defaults.baseURL = "http://localhost:5000"

新闻相关

新闻数据相关介绍:

      "title": "Introducing JSX",  //标题名
      "categoryId": 2,   //分类
      "content":    //内容
      "region": "全球",   //区域分类
      "author": "admin",   //作者
      "roleId": 1,   //作者所属角色
      "auditState": 2,   //审核状态:0-未审核(放进草稿箱);1-正在审核(放进审核列表);2-已通过;3-未通过
      "publishState": 2,   //发布状态:0-未发布;1-待发布;2-已发布;3-已下线
      "createTime": 1615778715619,   //创建时间
      "star": 600,   //点赞人数
      "view": 983,   //浏览次数
      "id": 3,   //id
      "publishTime": 1615778911762   //发布时间

1. 添加新闻

  • 步骤条

引入Steps组件,通过操作current的值来控制步骤进行到哪一步

            <Steps current={current}>
                <Step title="基本信息" description="新闻标题,新闻分类" />
                <Step title="新闻内容" description="新闻主体内容" />
                <Step title="新闻提交" description="保存草稿或者提交审核" />
            </Steps>

之后判断current的值来渲染步骤条下方的按钮:

            <div style={{ marginTop: "50px" }}>
                {
                    current === 2 && <span>
                        {/* 保存至草稿箱, 并将现在的审核状态0(表示未审核)传给handleSave函数*/}
                        <Button type="primary" onClick={() => handleSave(0)}>保存草稿箱</Button>
                        {/* 提交审核, 并将现在的审核状态1(表示未审核)传给handleSave函数 */}
                        <Button danger onClick={() => handleSave(1)}>提交审核</Button>
                    </span>
                }
                {
                    // 表单校验不成功不会跳转,handleNext:点击下一步的回调,将current的值加一
                    current < 2 && <Button type="primary" onClick={handleNext}>下一步</Button>
                }
                {
                    // handlePrevious:点击上一步的回调,将current的值减一
                    current > 0 && <Button onClick={handlePrevious}>上一步</Button>
                }
            </div>
  • 添加新闻标题以及分类(step1)
    引入Form组件,Form.Item的第一项为新闻标题,Form.Item的第二项为新闻分类,数据是从后台/categories的接口拿到的。

  • 编辑新闻正文
    引入NewsEditor:

        <div>
            <Editor
                // 编辑的内容
                editorState={editorState}
                // 自定义编辑器的样式
                toolbarClassName="aaaaa"
                wrapperClassName="bbbbb"
                editorClassName="ccccc"
                // editorState和onEditorStateChange让Editor成为受控组件
                onEditorStateChange={(editorState) => setEditorState(editorState)}
                // 失去焦点时,拿到状态值
                onBlur={() => {
                    // 这个状态值只有editor才认识,所以要经过变换,需要引入convertToRaw和draftToHtml这两个组件
                    props.getContent(draftToHtml(convertToRaw(editorState.getCurrentContent())))
                }}
            />
        </div>

上面这三个页面是不能同时出现的,并且上一个页面的数据是需要携带至下一个页面,不能用三元表达式来切换页面,页面会销毁,数据会丢失,因此需要判断current这个值来确定显示那个页面。

  • 提交新闻
    保存至草稿箱/提交审核:调用handleSave,若是保存至草稿箱则将auditState=0传入函数,若是提交审核则将auditState=1传入函数
    const handleSave = (auditState) => {
        axios.post('/news', {
            // title和分类
            ...formInfo,
            "content": content,
            // 本地中已经存好
            "region": User.region ? User.region : "全球",
            "author": User.username,
            "roleId": User.roleId,
            "auditState": auditState,
            "publishState": 0,
            "createTime": Date.now(),
            "star": 0,
            "view": 0,
            // "publishTime": 0
        }).then(res => {
            // 传0表示跳转至草稿箱列表,传1表示跳转到审核列表
            props.history.push(auditState === 0 ? '/news-manage/draft' : '/audit-manage/list')
 
            notification.info({
                message: `通知`,
                description:
                    `您可以到${auditState === 0 ? '草稿箱' : '审核列表'}中查看您的新闻`,
                // 位置
                placement: "bottomRight"
            });
        })
    }

2. 草稿箱列表

  • 从/news接口中拿到新闻的相关数据,并过滤出auditState=0以及属于当前登录用户撰写的新闻
 
    const { username } = JSON.parse(localStorage.getItem("token"))
    useEffect(() => {
        // news接口中的数据,只有auditState=0才会被放入草稿箱
        axios.get(`/news?author=${username}&auditState=0&_expand=category`).then(res => {
            const list = res.data
            setdataSource(list)
        })
        // 依赖的是username,若username发生变化,则调用该函数
    }, [username])

草稿箱中点击新闻标题可以携带当前新闻的id进入新闻预览界面:

        {
            title: '新闻标题',
            dataIndex: 'title',
            render: (title, item) => {
                // 点击标题可以进入预览界面
                return <a href={`#/news-manage/preview/${item.id}`}>{title}</a>
            }
        },
  • 新闻草稿箱列表中,可以将删除新闻,编辑新闻,将新闻提交至审核列表
    删除新闻,逻辑同之前的删除操作

编辑新闻:点击编辑按钮,跳转至更新页面

<Button shape="circle" icon={<EditOutlined />} onClick={() => {
    props.history.push(`/news-manage/update/${item.id}`)
}} />

更新新闻页面的结构同编辑新闻的页面相似,但是跳转至更新新闻的界面时,拿到要更新的新闻的id,并且将原来的新闻信息展现在表单中供作者修改:

    useEffect(() => {
        // console.log()
        // 拿到要更新的新闻的id,以及他们的分类和创建该新闻的角色
        axios.get(`/news/${props.match.params.id}?_expand=category&_expand=role`).then(res => {
            let { title, categoryId, content } = res.data
            // 当前的NewsForm设置初始值
            NewsForm.current.setFieldsValue({
                title,
                categoryId
            })
            setContent(content)
        })
    }, [props.match.params.id])

title和categoryId是在编辑新闻的第一步展示:而content是在第二步的富文本框展示,

    useEffect(() => {
        // 给的html片段
        // console.log(props.content)
        // html-===> draft, 
        const html = props.content
        // 如果html本文内容为空,则不渲染
        if (html === undefined) return
        const contentBlock = htmlToDraft(html);
        if (contentBlock) {
            const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks);
            const editorState = EditorState.createWithContent(contentState);
            setEditorState(editorState)
        }
        // 每次props.content改变的时候就会重新执行一遍
    }, [props.content])

提交审核:先将auditState的状态改为1,然后路由跳转至/audit-manage/list这个接口。

    const handleCheck = (id) => {
        axios.patch(`/news/${id}`, {
            // 0表示草稿箱,1表示待审核,2正在审核,3未通过,4已通过
            auditState: 1
        }).then(res => {
            props.history.push('/audit-manage/list')
 
            notification.info({
                message: `通知`,
                description:
                    `您可以到${'审核列表'}中查看您的新闻`,
                placement: "bottomRight"
            });
        })
    }

注意:draftjs-to-html和html-to-draftjs这两个组件在react18下安装时需要在后面加入后缀–legacy-peer-deps。

审核管理

1. 自己撰写的新闻当前的审核状态AuditList

  • 数据来源
    const { username } = JSON.parse(localStorage.getItem("token"))
    useEffect(() => {
        // auditState_ne表示auditState不等于0(等于0表示在草稿箱中),publishState_lte=1表示小于等于1(表示未发布)
        axios(`/news?author=${username}&auditState_ne=0&publishState_lte=1&_expand=category`).then(res => {
            // console.log(res.data)
            setdataSource(res.data)
        })
    }, [username])

拿到属于当前用户撰写的新闻,且该新闻时不在草稿箱中并且没有发布,故这些新闻有四个状态。将这四个状态的新闻渲染出来,每个状态对应的可操作的按钮也不一样,

                return <div>
                    {
                        // 正在审核的话可以将其撤销至草稿箱
                        item.auditState === 1 && <Button onClick={() => handleRervert(item)} >撤销</Button>
                    }
                    {   //已通过的话可以将其发布
                        item.auditState === 2 && <Button danger onClick={() => handlePublish(item)}>发布</Button>
                    }
                    {   //未通过的话,可以重新更新新闻
                        item.auditState === 3 && <Button type="primary" onClick={() => handleUpdate(item)}>更新</Button>
                    }
                </div>

撤销的函数,发布的函数,更新的函数相似,以撤销函数为例:

    const handleRervert = (item) => {
        // 先将当前数据从本地列表中移出  
        setdataSource(dataSource.filter(data => data.id !== item.id))
        // 并将news接口中的数据的auditState的接口改为0
        axios.patch(`/news/${item.id}`, {
            auditState: 0
        }).then(res => {
            notification.info({
                message: `通知`,
                description:
                    `您可以到草稿箱中查看您的新闻`,
                placement: "bottomRight"
            });
 
        })
    }

更新函数和发布函数只是在接口处和auditState/publishState有所不同。

2. 需要当前用户审核的新闻列表

数据来源:

    useEffect(() => {
        const roleObj = {
            "1": "superadmin",
            "2": "admin",
            "3": "editor"
        }
        axios.get(`/news?auditState=1&_expand=category`).then(res => {
            const list = res.data
            // 如果判断登录角色是superadmin,则将整个响应的列表返回,否则返回自己或者同一个区域下的编辑的数据
            setdataSource(roleObj[roleId] === "superadmin" ? list : [
                ...list.filter(item => item.author === username),
                ...list.filter(item => item.region === region && roleObj[item.roleId] === "editor")
            ])
        })
    }, [roleId, region, username])

点击新闻标题可以携带新闻的id跳转至新闻详情,故要调用#/news-manage/preview/${item.id}这个接口。对于列表中的新闻,可进行通过和驳回两种操作,对应两种不同的button,回调函数如下:

    const handleAudit = (item, auditState, publishState) => {
        setdataSource(dataSource.filter(data => data.id !== item.id))
 
        axios.patch(`/news/${item.id}`, {
            auditState,
            publishState
        }).then(res => {
            notification.info({
                message: `通知`,
                description:
                    `您可以到[审核管理/审核列表]中查看您的新闻的审核状态`,
                placement: "bottomRight"
            });
        })
    }

新闻分类

数据来源:/categories这个接口

1. 可编辑的文本框

  • 定制好可编辑的行
        <div>
            <Table dataSource={dataSource} columns={columns}
                pagination={{
                    pageSize: 5
                }}
                rowKey={item => item.id}
                // 定制好可编辑的行
                components={{
                    body: {
                        row: EditableRow,
                        cell: EditableCell,
                    }
                }}
            />
        </div>
  • 定义好可编辑的row和可编辑的cell
    // 可编辑的row
    const EditableRow = ({ index, ...props }) => {
        const [form] = Form.useForm();
        return (
            // 将form传给EditableContext
            <Form form={form} component={false}>
                <EditableContext.Provider value={form}>
                    <tr {...props} />
                </EditableContext.Provider>
            </Form>
        );
    };
    // 可编辑的cell
    const EditableCell = ({
        title,
        editable,
        children,
        dataIndex,
        record,
        handleSave,
        ...restProps
    }) => {
        const [editing, setEditing] = useState(false);
        const inputRef = useRef(null);
        const form = useContext(EditableContext);
        useEffect(() => {
            if (editing) {
                // 如果编译状态为真,则先获取焦点
                inputRef.current.focus();
            }
        }, [editing]);
 
        const toggleEdit = () => {
            setEditing(!editing);
            form.setFieldsValue({
                [dataIndex]: record[dataIndex],
            });
        };
        const save = async () => {
            try {
                const values = await form.validateFields();
                toggleEdit();
                handleSave({ ...record, ...values });
            } catch (errInfo) {
                console.log('Save failed:', errInfo);
            }
        };
        let childNode = children;
        if (editable) {
            childNode = editing ? (
                <Form.Item
                    style={{
                        margin: 0,
                    }}
                    name={dataIndex}
                    rules={[
                        {
                            required: true,
                            message: `${title} is required.`,
                        },
                    ]}
                >
                    {/* 失去焦点,调用save函数 */}
                    <Input ref={inputRef} onPressEnter={save} onBlur={save} />
                </Form.Item>
            ) : (
                <div
                    className="editable-cell-value-wrap"
                    style={{
                        paddingRight: 24,
                    }}
                    onClick={toggleEdit}
                >
                    {children}
                </div>
            );
        }
        return <td {...restProps}>{childNode}</td>;
    };

删除按钮的逻辑同权限列表类似

发布管理

项目中的待发布,已发布,已下线三个界面的布局几乎相同,故这里采用自定义hooks的当时来封装组件。

1. 自定义hooks:自定义函数部分

  • 获取数据
// react中的自定义hooks
function usePublish(type) {
    // 获取数据
    const { username } = JSON.parse(localStorage.getItem("token"))
    const [dataSource, setdataSource] = useState([])
    useEffect(() => {
    //根据路由组件传入的type不同,拿到不同的数据
        axios(`/news?author=${username}&publishState=${type}&_expand=category`).then(res => {
            // console.log(res.data)
            setdataSource(res.data)
        })
    }, [username, type])
  • 不同的button对应的回调函数
    未发布的列表中的发布按钮的回调:
    const handlePublish = (id) => {
        setdataSource(dataSource.filter(item => item.id !== id))
        axios.patch(`/news/${id}`, {
            "publishState": 2,
            "publishTime": Date.now()
        }).then(res => {
            notification.info({
                message: `通知`,
                description:
                    `您可以到【发布管理/已经发布】中查看您的新闻`,
                placement: "bottomRight"
            });
        })
    }

发布列表中的下线按钮的回调:

    const handleSunset = (id) => {
        setdataSource(dataSource.filter(item => item.id !== id))
 
        axios.patch(`/news/${id}`, {
            "publishState": 3,
        }).then(res => {
            notification.info({
                message: `通知`,
                description:
                    `您可以到【发布管理/已下线】中查看您的新闻`,
                placement: "bottomRight"
            });
        })
    }

下线列表中的删除按钮的回调:

    const handleDelete = (id) => {
        setdataSource(dataSource.filter(item => item.id !== id))
 
        axios.delete(`/news/${id}`).then(res => {
            notification.info({
                message: `通知`,
                description:
                    `您已经删除了已下线的新闻`,
                placement: "bottomRight"
            });
        })
    }

2. 自定义hooks:页面布局部分
自定义布局:

    return (
        <div>
            <Table dataSource={props.dataSource} columns={columns}
                pagination={{
                    pageSize: 5
                }}
                rowKey={item => item.id}
            />
        </div>
    )

3. 在待发布,已发布,已下线三个路由组件中调用上述的自定义hooks
以待发布路由组件为例为例:

import NewsPublish from '../../../components/publish-manage/NewsPublish'
import usePublish from '../../../components/publish-manage/usePublish'
import { Button } from 'antd'
// dataSource和按钮的回调都是从usePublish这个hooks中取到的
export default function Unpublished() {
    // 1=== 待发布的
    const { dataSource, handlePublish } = usePublish(1)
    return (
        <div>
            <NewsPublish dataSource={dataSource} button={(id) => <Button type="primary" onClick={() => handlePublish(id)}>
                发布
            </Button>} ></NewsPublish>
        </div>
    )
}

Redux

需要多处通信,多处共享,使用Redux(非父子通信)
redux详解
创建store
createstore中只允许存放一个reducer,将来要管理多个reducer的话要先对其进行合并处理(combineReducers)
redux的原则是单一store原则
在这里插入图片描述
1. 折叠sideMenu
创建reducer,接受数据之前的状态,返回加工后的状态。

// 本质是一个函数,接受数据之前的状态,action,返回加工后的状态,reducer这个函数是纯函数
// reducer被第一次调用时,是store自动触发的
export const CollApsedReducer = (prevState = {
    isCollapsed: false
}, action) => {
    // console.log(action)
    let { type } = action
    switch (type) {
        case "change_collapsed":
            let newstate = { ...prevState }
            newstate.isCollapsed = !newstate.isCollapsed
            return newstate
        default:
            return prevState
    }
}

在topHeader组件中调用connect,返回高阶组件,将组件和redux连接起来,实现监听和dispatch工作

// connect用法,connect()执行一次,返回一个高阶函数,再执行这个高阶函数
//connect可以拿到store中的state和方法
//给了一个选择权可以让我们决定用哪个属性、哪个值实现获取值
// 将数据和处理数据的方法提交至高阶组件connect中
//topHeader组件中调用connect
//conncet(mapStateToProps,mapDispatchToProps)(被包装的组件)
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(TopHeader))
// 将state中的状态映射成一个属性,传给组件
const mapStateToProps = ({ CollApsedReducer: { isCollapsed } }) => {
    return {
        isCollapsed
    }
}
// 将state中的方法映射成一个属性,传给组件
const mapDispatchToProps = {
    changeCollapsed() {
        return {
            type: "change_collapsed"
            // payload:
        }//action 
    }
}

供应商组件
在这里插入图片描述

调用changeCollapsed这个函数,派发action

sideMenu这个组件中,将state中的状态映射成一个属性,传给组件,然后通过collapsed={props.isCollapsed}拿到这个状态。

const mapStateToProps = ({ CollApsedReducer: { isCollapsed } }) => ({
  isCollapsed
})
// 将state中的状态映射成一个属性,传给组件
export default connect(mapStateToProps)(withRouter(SideMenu))

2. 异步加载数据时的loading框
在需要异步获取数据的路由组件外,包裹一个Spin组件,在数据未加载完成时显示loading框,数据加载完毕时,loading框消失。Spin组件的状态受控制

const mapStateToProps = ({ LoadingReducer: { isLoading } }) => ({
    isLoading
})
// 用connect进行包装,使之可以拿到store中的方法和数据
export default connect(mapStateToProps)(NewsRouter)

在http请求中添加请求拦截器和响应拦截器:在请求发送前派发action修改isLoading的状态:

axios.interceptors.request.use(function (config) {
  // 显示loading
  store.dispatch({
    type: "change_loading",
    payload: true
  })
  return config;
}, function (error) {
  return Promise.reject(error);
});

在数据加载完毕后派发action修改isLoading的状态:

// 响应拦截器,隐藏loading
axios.interceptors.response.use(function (response) {
  store.dispatch({
    type: "change_loading",
    payload: false
  })
  //隐藏loading
  return response;
}, function (error) {
  store.dispatch({
    type: "change_loading",
    payload: false
  })
  //隐藏loading
  return Promise.reject(error);
});

LoadingReducer中存储修改isLoading状态的方法:

export const LoadingReducer = (prevState={
    isLoading:false
},action)=>{
        // console.log(action)
    let {type,payload} =action
    switch(type){
        case "change_loading":
            let newstate = {...prevState}
            newstate.isLoading = payload
            return newstate
        default:
            return prevState
    }
}

3. store.js合并reducer并且持久化存储数据
合并reducer:redux的原则是单一设计原则,故引入combineReducers,

// 合并Reducers
const reducer = combineReducers({
    CollApsedReducer,
    LoadingReducer
})

持久化数据:redux的状态是存储在内存中,页面刷新的时候,数据会消失,需要持久化存储,引入persistStore,persistReducer和storage,持久化数据代码如下:

const persistConfig = {
    // 存到本地中key: 'kerwin'的值里面
    key: 'kerwin',
    storage,
    // 这里是黑名单,表示不会被持久化的
    blacklist: ['LoadingReducer']
}
// 将合并后的reducer作持久化,经过persistedReducer生成store
const persistedReducer = persistReducer(persistConfig, reducer)
// persistedReducer是为store服务的reducer,再将persistedReducer生成store
const store = createStore(persistedReducer);
const persistor = persistStore(store)
export {
    store,
    persistor
}

首页数据展示

1. 表格布局
从antd中引入Row布局,将页面平均分成三等份,每个等分使用Table布局,分别用来显示用户最常浏览,用户点赞最多以及用户资料卡。

  • 用户最常浏览:

数据获取:

    useEffect(() => {
        // _sort表示对数据进行排序,_order=desc表示降序,limit限制返回的数据
        axios.get("/news?publishState=2&_expand=category&_sort=view&_order=desc&_limit=6").then(res => {
            setviewList(res.data)
        })
    }, [])

页面布局:

                <Col span={8}>
                    {/* 给card加一个边框 */}
                    <Card title="用户最常浏览" bordered={true}>
                        <List
                            size="small"
                            // bordered
                            dataSource={viewList}
                            // 表示要渲染成什么数据
                            renderItem={item => <List.Item>
                                {/* 点击可以查看最长浏览的新闻详情 */}
                                <a href={`#/news-manage/preview/${item.id}`}>{item.title}</a>
                            </List.Item>}
                        />
                    </Card>
                </Col>
  • 用户点赞最多:

数据获取:

    useEffect(() => {
        axios.get("/news?publishState=2&_expand=category&_sort=star&_order=desc&_limit=6").then(res => {
            // console.log(res.data)
            setstarList(res.data)
        })
    }, [])

页面布局:

                <Col span={8}>
                    <Card title="用户点赞最多" bordered={true}>
                        <List
                            size="small"
                            // bordered
                            dataSource={starList}
                            renderItem={item => <List.Item>
                                <a href={`#/news-manage/preview/${item.id}`}>{item.title}</a>
                            </List.Item>}
                        />
                    </Card>
                </Col>
  • 用户资料卡:用户资料卡中点击setting按钮,调用Drawer组件弹出一个饼状图,该饼状图表示该登录用户发布的新闻分类统计:
            <Drawer
                width="500px"
                title="个人新闻分类"
                placement="right"
                closable={true}
                onClose={() => {
                    setvisible(false)
                }}
                visible={visible}
            >
                <div ref={pieRef} style={{
                    width: '100%',
                    height: "400px",
                    marginTop: "30px"
                }}></div>
            </Drawer>

饼状图的数据获取:

    useEffect(() => {
        axios.get("/news?publishState=2&_expand=category").then(res => {
            // lodash.groupBy以category.title进行分组
            renderBarView(_.groupBy(res.data, item => item.category.title))
            setallList(res.data)
        })
    }, [])

饼状图定义:

    const renderPieView = (obj) => {
        //数据处理工作
        // 筛选出该作者发布的新闻
        var currentList = allList.filter(item => item.author === username)
        var groupObj = _.groupBy(currentList, item => item.category.title)
        var list = []
        for (var i in groupObj) {
            list.push({
                name: i,
                value: groupObj[i].length
            })
        }
        var myChart;
        if (!pieChart) {
            // 只做一次初始化
            myChart = Echarts.init(pieRef.current);
            setpieChart(myChart)
        } else {
            myChart = pieChart
        }
        var option;
 
        option = {
            title: {
                text: '当前用户新闻分类图示',
                // subtext: '纯属虚构',
                left: 'center'
            },
            tooltip: {
                trigger: 'item'
            },
            legend: {
                orient: 'vertical',
                left: 'left',
            },
            series: [
                {
                    name: '发布数量',
                    type: 'pie',
                    radius: '50%',
                    data: list,
                    emphasis: {
                        itemStyle: {
                            shadowBlur: 10,
                            shadowOffsetX: 0,
                            shadowColor: 'rgba(0, 0, 0, 0.5)'
                        }
                    }
                }
            ]
        };
        option && myChart.setOption(option);
    }

柱状图:表示该系统下所有新闻的分类:

    const renderBarView = (obj) => {
        // 放到barRef这个容器中
        var myChart = Echarts.init(barRef.current);
        // 指定图表的配置项和数据
        var option = {
            title: {
                text: '新闻分类图示'
            },
            tooltip: {},
            legend: {
                data: ['数量']
            },
            xAxis: {
                data: Object.keys(obj),
                axisLabel: {
                    rotate: "45",
                    // 设置为0表示强制显示所有标签
                    interval: 0
                }
            },
            yAxis: {
                minInterval: 1
            },
            series: [{
                name: '数量',
                type: 'bar',
                data: Object.values(obj).map(item => item.length)
            }]
        };
        // 使用刚指定的配置项和数据显示图表。
        myChart.setOption(option);
        // 给window绑定resize
        window.onresize = () => {
            myChart.resize()
        }
    }

柱状图优化:在页面缩放时,柱状图需要随着页面的宽度调整宽度,因此在配置柱状图组件时,添加onresize配置:

        // 使用刚指定的配置项和数据显示图表。
        myChart.setOption(option);
        // 给window绑定resize
        window.onresize = () => {
            myChart.resize()
        }

在组件销毁时,在useEffect中添加如下配置:

        return () => {
            window.onresize = null
        }

游客访问

在登陆页面中,当访问者不想登录或者没有账号时,可以选择以临时游客身份浏览新闻界面和新闻详情界面。

1. 新闻界面

数据获取:拿到所有的新闻数据,并且使用lodash组件将数据分类,并转换成二维数组

    useEffect(() => {
        axios.get("/news?publishState=2&_expand=category").then(res => {
            // console.log()
            // 拿到分好类的数据,Object.entries转换成二维数组
            setlist(Object.entries(_.groupBy(res.data, item => item.category.title)))
        })
    }, [])

使用Row布局:

                {/* 控制上下左右间距的 */}
                <Row gutter={[16, 16]}>
                    {
                        list.map(item =>
                            <Col span={8} key={item[0]}>
                                {/* hoverable鼠标刚放上去会有这个属性 */}
                                <Card title={item[0]} bordered={true} hoverable={true}>
                                    <List
                                        size="small"
                                        dataSource={item[1]}
                                        pagination={{
                                            pageSize: 3
                                        }}
                                        //携带当前新闻的id跳转至新闻详情界面
                                        renderItem={data => <List.Item><a href={`#/detail/${data.id}`}>{data.title}</a></List.Item>}
                                    />
                                </Card>
                            </Col>
                        )
                    }
                </Row>

2. 新闻详情界面

数据获取:由于用户每访问一次该界面,该条新闻的浏览量都应该加一,因此在发送请求获取数据的同时,也要修改当前的数据:

    useEffect(() => {
        axios.get(`/news/${props.match.params.id}?_expand=category&_expand=role`).then(res => {
            setnewsInfo({
                ...res.data,
                // 每刷新一次就把view的值加一
                view: res.data.view + 1
            })
            //同步后端
            return res.data
        }).then(res => {
            axios.patch(`/news/${props.match.params.id}`, {
                view: res.view + 1
            })
        })
    }, [props.match.params.id])

新闻详情页面的布局可以直接复用NewsPreview这个组件的页面布局,在这个页面,游客可以给此条新闻点赞,代码逻辑:

    const handleStar = () => {
        // 本地和数据库都要更新数据
        setnewsInfo({
            ...newsInfo,
            star: newsInfo.star + 1
        })
        axios.patch(`/news/${props.match.params.id}`, {
            star: newsInfo.star + 1
        })
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值