《CMS后台系统》项目实战 详细分解(十)

256 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

文章编辑页面

PageHeader页头

传送门

ant.design/components/…

引入

import { PageHeader, Button } from 'antd';

复制页头去掉描述

    <PageHeader
        ghost={false}
        onBack={() => window.history.back()}
        title="Title"
        subTitle="This is a subtitle"
        extra={[
          <Button key="3">Operation</Button>,
          <Button key="2">Operation</Button>,
          <Button key="1" type="primary">
            Primary
          </Button>,
        ]}
      >
  </PageHeader>

修改按钮

extra={
  <Button key="1" type="primary">
    提交文章
  </Button>}

设置时间和标题

引入moment

import moment from 'moment'

修改时间

subTitle={"当前日期:" + moment(new Date()).format("YYYY-MM-DD")}

修改标题

        title="文章编辑"
  • 实现效果

image.png

  • 使用wangEditor

安装依赖

npm i wangeditor  --save

引入对象E

import E from 'wangeditor'

创建对象实例放入div盒子

使用useEffect

import React,{useEffect} from 'react'

// 模拟componentDidMount
useEffect(()=>{
  const editor = new E('#div1');
  editor.create()
},[])

<div id="div1"></div>

image.png

  • 富文本编辑器

改为外界声明editor

let editor = null;

书写editor的onChange函数

  editor.config.onchange = (newHtml) => {
    SVGTextContentElement(newHtml)
  }

销毁editor

  return () => {
      editor.destroy()
  }

注意使用useState创建content。

import React, { useEffect, useState } from 'react'

 const [content, setContent] = useState('')

为文本编辑框设置边距

 style={{ padding: '0 20px 20px', background: '#fff' }}

设置对话框

对话框传输门

ant.design/components/…

引入Modal

import { Modal, Button } from 'antd';

设置button

  <Button key="1" type="primary" onClick={showModal}>
    提交文章
  </Button>

添加方法和模块


  const [isModalVisible, setIsModalVisible] = useState(false);

  const showModal = () => {
    setIsModalVisible(true);
  };

  const handleOk = () => {
    setIsModalVisible(false);
  };

  const handleCancel = () => {
    setIsModalVisible(false);
  };

<Modal title="Basic Modal" visible={isModalVisible} onOk={handleOk} onCancel={handleCancel}>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</Modal>

处理层级问题

在Modal设置zIndex={99999}

<Modal zIndex={99999} title="Basic Modal" visible={isModalVisible} onOk={handleOk} onCancel={handleCancel}>

修改标题

image.png

 title="填写文章标题"

简化函数代码

onClick={() =>  setIsModalVisible(true)}


onCancel={() =>  setIsModalVisible(false)}

Form表单传送门

ant.design/components/…

引入Form、Input

import {Form, Input, PageHeader, Button ,Modal} from 'antd';

//在Model下添加表单代码
  <Form
  name="basic"
  labelCol={{ span: 3 }}
  wrapperCol={{ span: 21 }}
  autoComplete="off"
>
  <Form.Item
    label="标题"
    name="title"
    rules={[{ required: true, message: '请填写标题!' }]}
  >
    <Input />
  </Form.Item>

  <Form.Item
    label="副标题"
    name="subTitle"
  >
    <Input />
  </Form.Item>
</Form>

对话框获取表单的值

Form表单弹出层传送门

ant.design/components/…

 const [form] = Form.useForm();

Form表单上添加属性

          form={form}

对话框点击了提交

  // 对话框点击了提交
  const handleOk = () => {
    // setIsModalVisible(false); // 关闭对话框
    form
    .validateFields()
    .then(values => {
      form.resetFields();
      onCreate(values);
    })
    .catch(info => {
      console.log('Validate Failed:', info);
    });
  };

删除onCreate(values)、修改ok函数

  // 对话框点击了提交
  const handleOk = () => {
    // setIsModalVisible(false); // 关闭对话框
    form
    .validateFields()
    .then(values => {
      form.resetFields();
    })
    .catch(() => {
      return ;
    });
  };

  • 实现效果

image.png

onOk添加onText、cancelText

okText="提交" cancelText="取消"

image.png

发送文章请求

  • 添加token
  let token = localStorage.getItem('cms-token')
  if(token){
    config.headers = {
      'cms-token': token
    }
  }

image.png

  • 书写api
// 添加文章
export const ArticleAddApi = (params) => request.post('/article/add', params)

引入api

import {ArticleAddApi } from '../request/api'

验证content是否取到

    // setIsModalVisible(false); // 关闭对话框
    form
    .validateFields()
    .then(values => {
      // form.resetFields(); // reset重置
      console.log('Received values of form: ', values);
      let {title,subTitle} = values
      console.log(content)
      
    })
    .catch(() => {
      return ;
    });
editor.config.onchange = (newHtml) => {
  setContent(newHtml)
}

image.png

image.png

-发送请求

  // 请求
  ArticleAddApi({title,subTitle,content}).then(res => {
    console.log(res)
  })

实现效果

image.png

编辑id控制 实现页面跳转

在ListList中

引入useNavigate

import {  useNavigate, } from 'react-router-dom'

const navigate = useNavigate()

在onClick中实现路由跳转

  actions={[
    <Button type='primary' onClick={()=>navigate('/edit/'+item.id)}>编辑</Button>, 
    <Button type='danger' onClick={()=>console.log(item.id)}>删除</Button>
  ]}
  • 修改路由文件index

是添加!不是修改

<Route path='/edit' element={<Edit />}></Route>
<Route path='/edit/:id' element={<Edit />}></Route>
  • 实现效果

image.png

  • 实现只有路径带有id值是才会有返回箭头

引入useParams

import { useParams } from 'react-router-dom'

const params = useParams()

书写onBack

 onBack={ params.id ? () => window.history.back() : null}

-实现效果

image.png

wangeditor 内容渲染

  • 书写查看文章api
// 查看文章
export const ArticleSearchApi = (params) => request.get(`/article/${params.id}`)

引入查看文章api

import {ArticleAddApi,ArticleSearchApi } from '../request/api'

根据地址栏id做请求

// 根据地址栏id做请求
if(params.id) {
  ArticleSearchApi({
    id:params.id
  }).then(res=>{
    if(res.errCode === 0) {
      let {title,subTitle} = res.data;
      editor.txt.html(res.data.content) // 重新设置编辑器内容
    }
  })
}
  • 实现效果(点击编辑之后会跳转到编辑页面,并且显示出文本内容)

image.png

  • 设置title、subTitle
const [title, setTitle] = useState('')
const [subTitle, setSubTitle] = useState('')
  
  
setTitle(res.data.title)
setSubTitle(res.data.subTitle)
  • 为表单添加初始值

在Form 标签中使用该属性

      initialValues={{title:title,subTitle:subTitle}}
  • 实现效果

image.png

修改更新文章

书写更新api

// 重新编辑文章
export const ArticleUpdateApi = (params) => request.put('/article/update', params)

调用api

  // 地址栏有id代表现在想要更新一篇文章
  if(params.id) {
    ArticleUpdateApi({title,subTitle,content}).then(res => {
      console.log(res)
    })
  }else {
      // 添加文章的请求
    ArticleAddApi({title,subTitle,content}).then(res => {
      console.log(res)
    })
  }
  • 实现效果

image.png

  • 使用message提示修改成功并且跳转页面
  // 地址栏有id代表现在想要更新一篇文章
  if(params.id) {
    ArticleUpdateApi({title,subTitle,content}).then(res => {
      if(res.errCode === 0) {
        message.success(res.message);
        //跳转到list页面
        navigate('/listlist')
      }else{
        message.error(res.message)
      }
      setIsModalVisible(false) // 关闭对话框
    })
  }else {
      // 添加文章的请求
    ArticleAddApi({title,subTitle,content}).then(res => {
      console.log(res)
    })
  }

解决bug

点击编辑一篇文章之后,再次点击菜单栏,文章编辑。页面的路径更改但是文本框并没有清除。

image.png

解决方案:监听路由的变化

引入location

import { useParams, useNavigate, useLocation } from 'react-router-dom'

const location = useLocation()
  

image.png

  • Aside监听路由
    // 一般加个空数组就是为了模仿componentDidMounted
    useEffect(()=>{
      let path = location.pathname;
      let key = path.split('/')[1];
      setDefaultKey(key)
  }, [location.pathname])
  • 封装函数(使用message提示修改成功并且跳转页面)

删除文章

书写删除api

// 删除文章
export const ArticleDelApi = (params) => request.post('/article/remove', params)

在ListList中引入api

import { ArticleListApi ,ArticleDelApi} from '../request/api';

在button中修改点击事件

<Button type='danger' onClick={()=>delFn(item.id)}>删除</Button>

书写删除函数delFn

重新刷页面,要么重新请求这个列表的数据 window.reload 调用getList(1) 增加变量的检测

  // 删除
  const delFn = (id) => {
    ArticleDelApi({id}).then(res=>{
      if(res.errCode===0){
        message.success(res.message)
        // 重新刷页面,要么重新请求这个列表的数据   window.reload   调用getList(1)  增加变量的检测
        setUpdate(update+1)
      }else{
        message.success(res.message)
      }
    })
  }

监听刷新页面

const [update, setUpdate] = useState(1)

image.png

  • 为Table添加编辑、删除功能

注意引入的内容和函数(太冗杂不写了,傲娇~)

用户资料表单布局

书写类名,设置样式

import "./less/Means.less"

import React from 'react'

export default function Means() {
  return (
    <div className='means'>Means</div>
  )
}


.means{
    background: #fff;
    height: 100%;
    padding: 20px;
    box-sizing: border-box;
}
  • 实现效果

image.png

  • 引入form表单(设置宽度、修改按钮,修改宽高)
import React from 'react'
import { Form, Input,Button} from 'antd';
import "./less/Means.less"

export default function Means() {
  return (
    <div className='means'>
    <Form
    style={{width: '400px'}}
    name="basic"
    initialValues={{
      remember: true,
    }}

    autoComplete="off"
  >
    <Form.Item
      label="修改用户名"
      name="username"
      rules={[
        {
          required: true,
          message: 'Please input your username!',
        },
      ]}
    >
    <Input placeholder='请输入新用户名' />
    </Form.Item>

    <Form.Item
      label="修 改 密 码"
      name="password"
    >
    <Input.Password placeholder='请输入新密码' />
    </Form.Item>

    <Form.Item>
    <Button type="primary" htmlType="submit" style={{float: 'right'}}>提交</Button>
  </Form.Item>

  </Form>
    </div>
  )
}

获取用户请求

  • 书写api
// 获取用户资料
export const GetUserDataApi = () => request.get('/info')

import {GetUserDataApi} from '../request/api'

使用useEffect、更新请求用户,设置初始值。

  const [username1,setUsername1] = useState("");
  const [password1,setPassword1] = useState("")

  useEffect(() => {
      GetUserDataApi().then(res => {
        console.log(res)
        if(res.errCode === 0) {
          message.success(res.message);
          setUsername1(res.data.username);
          setPassword1(res.data.Password);
        }
      })
  }, []);
  • 实现效果

image.png

修改用户资料

  • 书写api
// 修改用户资料
export const ChangeUserDataApi = (params) => request.put('/info', params)

  • 引入
import {GetUserDataApi, ChangeUserDataApi} from '../request/api'
  • 发送请求
  // 表单提交的事件
  const onFinish = (values) => {
    // 如果表单的username有值,并且不等于初始化时拿到的username,同时密码非空
    if(values.username && values.username!==sessionStorage.getItem('username') && values.password.trim() !== ""){
      // 做表单的提交...
      ChangeUserDataApi({
        username: values.username,
        password: values.password
      }).then(res=>{
        console.log(res)
        // 当你修改成功的时候,不要忘了重新登录
      })
    }
  }

当你修改成功的时候,不要忘了重新登录!

Upload引入

添加标题

      <p>点击下方修改头像:</p>

Upload上传

传送门

ant.design/components/…

upload组件中直接书写请求体

书写action接口

action="/api/upload"

上传前 beforeUpload

// 限制图片大小只能是200KB
function beforeUpload(file) {
  const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
  if (!isJpgOrPng) {
    message.error('You can only upload JPG/PNG file!');
  }
  const isLt2M = file.size / 1024 / 1024 / 1024  < 200;
  if (!isLt2M) {
    message.error('请上传小于200KB的图!');
  }
  return isJpgOrPng && isLt2M;
}
  • 使用useState改写loading、imageUrl
import React, { useEffect, useState } from 'react'

  const [loading, setLoading] = useState(false)
  const [imageUrl, setImageUrl] = useState("")
  • 添加handleChange,并且修改

image.png

  // 点击了上传图片
 const  handleChange = info => {
    if (info.file.status === 'uploading') {
       setLoading(true);
      return;
    }
    if (info.file.status === 'done') {
      // Get this url from response in real world.
      getBase64(info.file.originFileObj, imageUrl => {
        setLoading(false)
        setImageUrl(imageUrl)       
      }
      );
    }
  };
  • 函数调用修改

image.png

引入base64函数

// 将图片路径改位base64
function getBase64(img, callback) {
  const reader = new FileReader();
  reader.addEventListener('load', () => callback(reader.result));
  reader.readAsDataURL(img);
}

注意引入

import { Form, Input,Button, message,Upload} from 'antd';
import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
  • 实现效果

image.png

Upload组件添加请求头

headers={{"cms-token": localStorage.getItem('cms-token')}}

image.png

  • 存储图片名称

这里需要打印info.file找到上传图片的名称。

// 存储图片名称
localStorage.setItem('avatar', info.file.response.data.filePath)

更新Header组件

使用useState设置mykey

    <Layout id='app'>
        <Header key={mykey} />
    <div className='container'>
     <Aside />       
    <div className='container_box'>
        <Bread/>
        <div className="container_content">
        <Outlet  setMyKey={setMyKey} />
      </div>
    </div>
  </div>  
   <footer>Respect | Copyright &copy; 2022 Author 你单排吧</footer>
  </Layout>

image.png

Means组件

注意记得函数接收产生props

  // 点击了上传图片
 const  handleChange = info => {
    if (info.file.status === 'uploading') {
       setLoading(true);
      return;
    }
    if (info.file.status === 'done') {
      // Get this url from response in real world.
      getBase64(info.file.originFileObj, imageUrl => {
        setLoading(false)
        setImageUrl(imageUrl)       
        // 存储图片名称
        localStorage.setItem('avatar', info.file.response.data.filePath)
        // 触发Header组件更新
        props.setMyKey(props.myKey+1)
      }
      );
    }
  };

image.png

添加强制刷新

window.location.reload()

使用react-redux

安装react-redux

yarn add react-redux