极客园PC-第4章 文章列表
文章列表功能
card组件与面包屑导航
- card组件,文档:https://ant.design/components/card-cn/
import { Card } from 'antd'
export default class ArticleList extends Component {
render() {
return (
我是内容
)
}
}
- 面包屑导航的使用
export default class ArticleList extends Component {
render() {
return (
首页
文章列表
}
>
内容
)
}
}
搜索表单基本结构
- 复制表单的基本结构到组件中
- 修改表单结构
首页
文章列表
}
>
- 状态的基本结构
全部
草稿
待审核
审核通过
审核失败
- 下拉框结构
- 日期选择基本结构
import { Card, Breadcrumb, Form, Button, Radio, Select, DatePicker } from 'antd'
const { RangePicker } = DatePicker
日期中文处理
https://ant-design.gitee.io/components/date-picker-cn/
在index.js中
import React from 'react'
import ReactDOM from 'react-dom'
// 在 index.js 中导入 antd 的样式文件
import 'antd/dist/antd.css'
import './index.css'
import { ConfigProvider } from 'antd'
import 'moment/locale/zh-cn'
import locale from 'antd/lib/locale/zh_CN'
import App from './App'
ReactDOM.render(
,
document.getElementById('root')
)
频道数据管理
- 封装接口
import request from 'utils/request'
/*
获取所有的频道
*/
export const getChannels = () => {
return request.get('/channels')
}
- 发送请求获取数据
import { getChannels } from 'api/channel'
state = {
channels: [],
}
async getChannelList() {
const res = await getChannels()
this.setState({
channels: res.data.channels,
})
}
componentDidMount() {
this.getChannelList()
}
- 渲染频道数据
表格基本结构
- 基本结构
import {
Card,
Breadcrumb,
Form,
Button,
Radio,
Select,
DatePicker,
Table,
Tag,
Space,
} from 'antd'
render() {
const columns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (text) => {text},
},
{
title: 'Age',
dataIndex: 'age',
key: 'age',
},
{
title: 'Address',
dataIndex: 'address',
key: 'address',
},
{
title: 'Tags',
key: 'tags',
dataIndex: 'tags',
render: (tags) => (
<>
{tags.map((tag) => {
let color = tag.length > 5 ? 'geekblue' : 'green'
if (tag === 'loser') {
color = 'volcano'
}
return (
{tag.toUpperCase()}
)
})}
>
),
},
{
title: 'Action',
key: 'action',
render: (text, record) => (
Invite {record.name}
Delete
),
},
]
const data = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
tags: ['nice', 'developer'],
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
tags: ['loser'],
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
tags: ['cool', 'teacher'],
},
]
return (
)
}
获取文章列表数据
- 封装接口
import request from 'utils/request'
/**
* 获取文章列表
* @param {*} params
* @returns
*/
export const getArticles = (params) => {
return request({
url: '/mp/articles',
method: 'get',
params,
})
}
- 发送请求获取文章列表数据
state = {
channels: [],
articles: [],
total: 0,
}
async getChannelList() {
const res = await getChannels()
this.setState({
channels: res.data.channels,
})
}
async getArticleList() {
const res = await getArticles()
this.setState({
articles: res.data.results,
total: res.data.total_count,
})
}
componentDidMount() {
this.getChannelList()
this.getArticleList()
}
渲染表格数据
- 修改columns
const columns = [
{
title: '封面',
dataIndex: 'name',
},
{
title: '标题',
dataIndex: 'title',
},
{
title: '状态',
dataIndex: 'status',
},
{
title: '发布时间',
dataIndex: 'pubdate',
},
{
title: '阅读数',
dataIndex: 'read_count',
},
{
title: '评论数',
dataIndex: 'comment_count',
},
{
title: '点赞数',
dataIndex: 'like_count',
},
{
title: '操作',
},
]
- 封面处理
{
title: '封面',
dataIndex: 'cover',
render(data) {
const { images, type } = data
if (type === 0) {
return (
)
}
return (
)
},
},
- 状态处理
// 通过对象来优化if/switch
// 使用方式:articleStatus[0] => { text: '草稿', color: '' }
const articleStatus = {
0: { text: '草稿', color: 'gold' },
1: { text: '待审核', color: 'lime' },
2: { text: '审核通过', color: 'green' },
3: { text: '审核失败', color: 'red' },
}
{
title: '状态',
dataIndex: 'status',
render: (data) => {
const tagObj = articleStatus[data]
return {tagObj.text}
},
},
- 操作功能
{
title: '操作',
render() {
return (
}
>
}
>
)
},
},
key属性处理
分页功能
- 使用分页组件
- 提供changePage事件
changePage = async (page, pageSize) => {
console.log(page)
const res = await getArticles({
page,
per_page: this.state.articles.per_page,
})
this.setState({
articles: res.data,
})
}
获取表单的值进行筛选
- 给表单注册事件
- 给表单元素提供name属性
全部
草稿
待审核
审核通过
审核失败
- 发送请求,获取数据
onFinish = async (values) => {
console.log(values)
// 发送请求,获取数据
const params = {}
// 处理状态
if (values.status !== -1) {
params.status = values.status
}
// 处理频道
if (values.channel_id) {
params.channel_id = values.channel_id
}
// 处理日期
if (values.date) {
params.begin_pubdate = values.date[0].format('YYYY-MM-DD')
params.end_pubdate = values.date[1].format('YYYY-MM-DD')
}
params.page = 1
const res = await getArticles(params)
console.log(res.data)
this.setState({
articles: res.data,
})
}
时间的优化
// 处理日期
if (values.date) {
params.begin_pubdate = values.date[0]
.startOf('day')
.format('YYYY-MM-DD HH:mm:ss')
params.end_pubdate = values.date[1]
.endOf('day')
.format('YYYY-MM-DD HH:mm:ss')
}
修改分页bug
changePage = async (page, pageSize) => {
const res = await getArticles({
...this.params,
page,
per_page: this.state.articles.per_page,
})
this.setState({
articles: res.data,
})
}
onFinish = async (values) => {
console.log(values)
// 发送请求,获取数据
const params = {}
// 处理状态
if (values.status !== -1) {
params.status = values.status
}
// 处理频道
if (values.channel_id) {
params.channel_id = values.channel_id
}
// 处理日期
if (values.date) {
params.begin_pubdate = values.date[0]
.startOf('day')
.format('YYYY-MM-DD HH:mm:ss')
params.end_pubdate = values.date[1]
.endOf('day')
.format('YYYY-MM-DD HH:mm:ss')
}
params.page = 1
this.params = params
const res = await getArticles(params)
console.log(res.data)
this.setState({
articles: res.data,
})
}
删除功能
- 注册点击事件
}
onClick={() => this.handleDelete(data.id)}
>
- 准备弹窗
handleDelete = (id) => {
confirm({
title: '温馨提示?',
icon: ,
content: '你确定要删除文章吗',
onOk() {
// 发送请求进行删除
},
})
}
- 封装接口进行删除
/**
* 删除文章
* @param {*} id
* @returns
*/
export const delArticle = (id) => {
return request({
url: `/mp/articles/${id}`,
method: 'delete',
})
}
- 删除功能完成
handleDelete = (id) => {
confirm({
title: '温馨提示?',
icon: ,
content: '你确定要删除文章吗',
onOk: async () => {
// 发送请求进行删除
await delArticle(id)
this.getArticleList(this.params)
},
})
}
发布文章
基本结构准备
- 面包屑
import React, { Component } from 'react'
import { Card, Breadcrumb } from 'antd'
import { Link } from 'react-router-dom'
export default class ArticleList extends Component {
render() {
return (
首页
发布文章
}
>
)
}
}
- 表单
import { Card, Breadcrumb, Form, Input, Radio, Space, Button } from 'antd'
频道组件
无图
单图
三图
{/* 自动 */}
图片上传组件
文章内容
- 给表单注册事件
频道组件封装
- 基础封装
import { Component } from 'react'
import { Select } from 'antd'
import { getChannels } from 'api/channel'
const { Option } = Select
class Channel extends Component {
state = {
channels: [],
}
componentDidMount() {
this.getChannles()
}
// 获取频道列表数据的方法
async getChannles() {
const res = await getChannels()
this.setState({
channels: res.data.channels,
})
}
render() {
const { channels } = this.state
return (
)
}
}
export default Channel
- 使用频道组件
import Channel from 'components/Channel'
- 让频道组件受控
参考文档:https://ant-design.gitee.io/components/form-cn/#components-form-demo-customized-form-controls
render() {
const { channels } = this.state
const { value, onChange } = this.props
return (
)
}
文章内容处理
- 使用react-quill富文本编辑器 https://github.com/zenoamaro/react-quill
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
- 注意:必须提供默认值,不然会报错
- 提供样式
.publish {
:global {
.ql-editor {
min-height: 300px;
}
}
}
图片上传组件
- 基本结构
import {
Card,
Breadcrumb,
Form,
Input,
Radio,
Space,
Button,
Upload,
} from 'antd'
import { PlusOutlined } from '@ant-design/icons'
- 设置图片默认显示
state = {
// 存放上传的文件列表
fileList: [
{
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
],
}
- 图片上传功能, 需要提供name和action参数
- 获取上传成功的图片地址
uploadImages = ({ file, fileList }) => {
this.setState({
fileList,
})
}
控制图片的上传数量
- 控制type的切换
state = {
// 存放上传的文件列表
fileList: [],
type: 0,
}
无图
单图
三图
changeImageType = (e) => {
this.setState({
type: e.target.value,
})
}
- 根据type控制图片的显示
{this.state.type !== 0 && (
)}
- 控制图片的上传数据
{this.state.type !== 0 && (
{this.state.fileList.length < this.state.type && (
)}
)}
图片预览功能
图片格式校验
表单校验功能
- 表单基本校验
单图
三图
无图
{/* 自动 */}
{this.state.type !== 0 && (
{this.state.fileList.length < this.state.type && (
)}
)}
- 图片长度的校验
onFinish = async (values) => {
// 图片校验
if (this.state.type !== this.state.fileList.length) {
return message.warn('上传的图片数量不对')
}
}
发送请求-添加文章
- 封装接口
/**
* 发送请求添加文章
* @param {*} data
* @returns
*/
export const addArticle = (data) => {
return request({
url: '/mp/articles',
method: 'post',
data,
})
}
- 发送请求-处理数据并且添加文章
onFinish = async (values) => {
console.log(values)
// 处理数据,添加文章
const images = this.state.fileList.map((item) => {
if (item.url) {
return item.url
}
return item.response.data.url
})
const res = await addArticle({
...values,
cover: {
type: values.type,
images,
},
})
message.success('添加文章成功')
this.props.history.push('/home/list')
}
存入草稿功能
- 修改接口
/**
* 发送请求添加文章
* @param {*} data
* @returns
*/
export const addArticle = (data, draft = false) => {
return request({
url: '/mp/articles?draft=' + draft,
method: 'post',
data,
})
}
- 注册点击事件
- 提供事件
onFinish = async (values) => {
this.save(values, false)
}
save = async (values, draft) => {
// 图片校验
if (this.state.type !== this.state.fileList.length) {
return message.warn('上传的图片数量不对')
}
// 处理数据,添加文章
const images = this.state.fileList.map((item) => {
if (item.url) {
return item.url
}
return item.response.data.url
})
await addArticle(
{
...values,
cover: {
type: values.type,
images,
},
},
draft
)
message.success('添加文章成功')
this.props.history.push('/home/list')
}
addDraft = async () => {
// 获取表单的数据
const values = await this.formRef.current.validateFields()
this.save(values, true)
}
修改功能
配置修改文章的路由
- 给修改按钮注册点击事件
}
onClick={() => this.handleEdit(data.id)}
/>
// 修改
handleEdit = (id) => {
this.props.history.push(`/home/publish/${id}`)
}
- 配置修改文章的路由
{/* 新增 */}
{/* 修改的路由 */}
数据回显-获取数据
思路
- 在组件中判断能够通过params获取到id值,如果能够获取到id,说明是修改,如果获取不到id说明是新增
- 我们根据是否是新增可以修改对应的文本。【发布文章】或者【修改文章】
- 封装接口,用于获取文章的详情信息
- 发送请求,获取文章的详细信息
- 获取地址栏的id值
state = {
// 文章的封面类型
type: 1,
// 用于控制上传的图片以及图片的显示
fileList: [],
showPreview: false,
previewUrl: '',
// 编辑的id
+ id: this.props.match.params.id,
}
- 根据是否有id值,控制文本的显示
{id ? '编辑文章' : '发布文章'}
- 封装接口,获取文章详情
/**
* 获取文章详情信息
* @param {*} id
* @returns
*/
export const getArticleById = (id) => {
return request.get(`/mp/articles/${id}`)
}
- 页面渲染完成的时候,发送请求-获取数据
async componentDidMount() {
if (this.state.id) {
// 需要发请求,获取文章详细信息
const res = await getArticleById(this.state.id)
console.log('res', res)
}
}
数据回显-回显数据
思路:
- 通过Form组件提供的方法 setFieldsValue 可以给表单设置值
- 设置fileList的值,fileList控制封面显示的
- 设置表单的值
async componentDidMount() {
if (this.state.id) {
// 需要发请求,获取文章详细信息
const res = await getArticleById(this.state.id)
const values = {
...res.data,
type: res.data.cover.type,
}
// 给表单设置values值
this.formRef.current.setFieldsValue(values)
const fileList = res.data.cover.images.map((item) => {
return {
url: item,
}
})
this.setState({
fileList,
})
}
}
修复bug-修改切换到新增的bug
{/* 新增 */}
{/* 修改的路由 */}
修改功能完成
- 封装接口,用于修改文章
- 判断是否有id,如果有,发送请求修改,否则,发送请求新增
- 封装接口
/**
* 修改文章的接口
* @param {*} data
* @param {*} draft
* @returns
*/
export const updateArticle = (data, draft) => {
return request({
url: `/mp/articles/${data.id}?draft=${draft}`,
method: 'PUT',
data,
})
}
- 判断
async save(values, draft) {
const { fileList, type } = this.state
if (fileList.length !== type) {
return message.warn('上传的图片数量不正确')
}
// 根据fileList得到
const images = fileList.map((item) => {
return item.url || item.response.data.url
})
if (this.state.id) {
// 修改文章
await updateArticle(
{
...values,
cover: {
type,
images,
},
id: this.state.id,
},
draft
)
message.success('修改成功')
} else {
// 添加文章
await addAritcle(
{
...values,
cover: {
type,
images,
},
},
draft
)
message.success('添加成功')
}
this.props.history.push('/home/list')
}
导航高亮优化
- 给菜单按钮提供selectedKeys属性
- 需要在组件更新的时候,修改selectedKeys的值
// 组件更新完成的钩子函数,,,路由变化了,组件也是会重新渲染
// prevProps: 上一次的props
componentDidUpdate(prevProps) {
// 判断是否是url地址发生了变化,如果是,才更新
let pathname = this.props.location.pathname
if (this.props.location.pathname !== prevProps.location.pathname) {
// 考虑修改文章的高亮问题
if (pathname.startsWith('/home/publish')) {
pathname = '/home/publish'
}
this.setState({
selectedKey: pathname,
})
}
}
注意:在componentDidUpdate中想要调用setState必须添加判断,不然会死循环