极客园PC-第4章 文章列表
文章列表功能
card组件与面包屑导航
- card组件,文档:https://ant.design/components/card-cn/
import { Card } from 'antd'
export default class ArticleList extends Component {
render() {
return (
<div className="articleList">
<Card title="面包屑导航">我是内容</Card>
</div>
)
}
}
- 面包屑导航的使用
export default class ArticleList extends Component {
render() {
return (
<div className={styles.root}>
<Card
title={
<Breadcrumb>
<Breadcrumb.Item>
<Link to="/home">首页</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>文章列表</Breadcrumb.Item>
</Breadcrumb>
}
>
内容
</Card>
</div>
)
}
}
搜索表单基本结构
- 复制表单的基本结构到组件中
- 修改表单结构
<Card
title={
<Breadcrumb separator="/">
<Breadcrumb.Item>
<Link to="/home">首页</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>文章列表</Breadcrumb.Item>
</Breadcrumb>
}
>
<Form>
<Form.Item label="状态" name="username">
<Input />
</Form.Item>
<Form.Item label="频道" name="password">
<Input.Password />
</Form.Item>
<Form.Item label="日期" name="password">
<Input.Password />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
筛选
</Button>
</Form.Item>
</Form>
</Card>
- 状态的基本结构
<Form initialValues={{ status: null }}>
<Form.Item label="状态" name="status">
<Radio.Group>
<Radio value={null}>全部</Radio>
<Radio value={0}>草稿</Radio>
<Radio value={1}>待审核</Radio>
<Radio value={2}>审核通过</Radio>
<Radio value={3}>审核失败</Radio>
</Radio.Group>
</Form.Item>
- 下拉框结构
<Form.Item label="频道" name="password">
<Select placeholder="请选择频道" style={{ width: 200 }}>
<Option value="jack">Jack</Option>
<Option value="lucy">Lucy</Option>
<Option value="Yiminghe">yiminghe</Option>
</Select>
</Form.Item>
- 日期选择基本结构
import { Card, Breadcrumb, Form, Button, Radio, Select, DatePicker } from 'antd'
const { RangePicker } = DatePicker
<Form.Item label="日期" name="password">
<RangePicker />
</Form.Item>
日期中文处理
在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(
<ConfigProvider locale={locale}>
<App />
</ConfigProvider>,
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()
}
- 渲染频道数据
<Select placeholder="请选择频道" style={{ width: 200 }}>
{this.state.channels.map((item) => (
<Option value={item.id} key={item.id}>
{item.name}
</Option>
))}
</Select>
表格基本结构
- 基本结构
import {
Card,
Breadcrumb,
Form,
Button,
Radio,
Select,
DatePicker,
Table,
Tag,
Space,
} from 'antd'
render() {
const columns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (text) => <a>{text}</a>,
},
{
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 color={color} key={tag}>
{tag.toUpperCase()}
</Tag>
)
})}
</>
),
},
{
title: 'Action',
key: 'action',
render: (text, record) => (
<Space size="middle">
<a>Invite {record.name}</a>
<a>Delete</a>
</Space>
),
},
]
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 (
<div className="articleList">
<Card title={`根据筛选条件共查询到${0}条数据`}>
<Table dataSource={data} columns={columns} />
</Card>
</div>
)
}
获取文章列表数据
- 封装接口
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 (
<Image width={200} preview={false} height={150} src={defaultImg} />
)
}
return (
<Image width={200} height={150} src={images[0]} fallback={defaultImg} />
)
},
},
- 状态处理
// 通过对象来优化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 <Tag color={tagObj.color}>{tagObj.text}</Tag>
},
},
- 操作功能
{
title: '操作',
render() {
return (
<Space>
<Button
type="primary"
shape="circle"
icon={<EditOutlined />}
></Button>
<Button
type="primary"
shape="circle"
danger
icon={<DeleteOutlined />}
></Button>
</Space>
)
},
},
key属性处理
<Card title={`根据筛选条件共查询到${this.state.total}条数据`}>
<Table
rowKey="id"
dataSource={this.state.articles}
columns={columns}
/>
</Card>
分页功能
- 使用分页组件
<Card title={`根据筛选条件共查询到${this.state.total_count}条数据`}>
<Table
rowKey="id"
dataSource={results}
columns={columns}
pagination={{
position: ['bottomCenter'],
current: page,
pageSize: per_page,
total: total_count,
// 每页大小 或者 页码 改变时,触发的事件
onChange: this.changePage,
}}
/>
</Card>
- 提供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,
})
}
获取表单的值进行筛选
- 给表单注册事件
<Form initialValues={{ status: -1 }} onFinish={this.onFinish}>
- 给表单元素提供name属性
<Form.Item label="状态" name="status">
<Radio.Group>
<Radio value={-1}>全部</Radio>
<Radio value={0}>草稿</Radio>
<Radio value={1}>待审核</Radio>
<Radio value={2}>审核通过</Radio>
<Radio value={3}>审核失败</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="频道" name="channel_id">
<Select placeholder="请选择频道" style={{ width: 200 }}>
{this.state.channels.map((item) => (
<Option value={item.id} key={item.id}>
{item.name}
</Option>
))}
</Select>
</Form.Item>
<Form.Item label="日期" name="date">
<RangePicker />
</Form.Item>
- 发送请求,获取数据
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,
})
}
删除功能
- 注册点击事件
<Button
type="primary"
shape="circle"
danger
icon={<DeleteOutlined />}
onClick={() => this.handleDelete(data.id)}
></Button>
- 准备弹窗
handleDelete = (id) => {
confirm({
title: '温馨提示?',
icon: <ExclamationCircleOutlined />,
content: '你确定要删除文章吗',
onOk() {
// 发送请求进行删除
},
})
}
- 封装接口进行删除
/**
* 删除文章
* @param {*} id
* @returns
*/
export const delArticle = (id) => {
return request({
url: `/mp/articles/${id}`,
method: 'delete',
})
}
- 删除功能完成
handleDelete = (id) => {
confirm({
title: '温馨提示?',
icon: <ExclamationCircleOutlined />,
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 (
<div className="ArticleList">
<Card
title={
<Breadcrumb separator=">">
<Breadcrumb.Item>
<Link to="/home">首页</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>发布文章</Breadcrumb.Item>
</Breadcrumb>
}
></Card>
</div>
)
}
}
- 表单
import { Card, Breadcrumb, Form, Input, Radio, Space, Button } from 'antd'
<Form labelCol={{ span: 4 }} initialValues={{ type: 0 }}>
<Form.Item label="标题" name="title">
<Input placeholder="请输入文章标题" style={{ width: 400 }} />
</Form.Item>
<Form.Item label="频道" name="channel_id">
频道组件
</Form.Item>
<Form.Item label="封面">
<Form.Item name="type">
<Radio.Group onChange={this.changeImageType}>
<Radio value={0}>无图</Radio>
<Radio value={1}>单图</Radio>
<Radio value={3}>三图</Radio>
{/* <Radio value={-1}>自动</Radio> */}
</Radio.Group>
</Form.Item>
图片上传组件
</Form.Item>
<Form.Item label="内容" name="content">
文章内容
</Form.Item>
<Form.Item wrapperCol={{ offset: 4 }}>
<Space>
<Button size="large" type="primary" htmlType="submit">
发布文章
</Button>
<Button size="large">存入草稿</Button>
</Space>
</Form.Item>
</Form>
- 给表单注册事件
<Form
labelCol={{ span: 4 }}
initialValues={{ type: 0 }}
onFinish={this.onFinish}
>
onFinish = (values) => {
console.log(values)
}
频道组件封装
- 基础封装
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 (
<Select placeholder="请选择文章频道">
{channels.map((item) => (
<Option key={item.id} value={item.id}>
{item.name}
</Option>
))}
</Select>
)
}
}
export default Channel
- 使用频道组件
import Channel from 'components/Channel'
<Form.Item label="频道" name="channel_id">
<Channel></Channel>
</Form.Item>
- 让频道组件受控
参考文档: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 (
<Select
placeholder="请选择文章频道"
style={{ width: 200 }}
value={value}
onChange={onChange}
>
{channels.map((item) => (
<Option key={item.id} value={item.id}>
{item.name}
</Option>
))}
</Select>
)
}
文章内容处理
- 使用react-quill富文本编辑器 https://github.com/zenoamaro/react-quill
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
<Form.Item label="内容" name="content">
<ReactQuill
theme="snow"
placeholder="请输入文章内容..."
></ReactQuill>
</Form.Item>
- 注意:必须提供默认值,不然会报错
- 提供样式
.publish {
:global {
.ql-editor {
min-height: 300px;
}
}
}
图片上传组件
- 基本结构
import {
Card,
Breadcrumb,
Form,
Input,
Radio,
Space,
Button,
Upload,
} from 'antd'
import { PlusOutlined } from '@ant-design/icons'
<Upload listType="picture-card">
<PlusOutlined></PlusOutlined>
</Upload>
- 设置图片默认显示
<Upload
listType="picture-card"
name="image"
fileList={this.state.fileList}
>
<PlusOutlined></PlusOutlined>
</Upload>
state = {
// 存放上传的文件列表
fileList: [
{
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
],
}
- 图片上传功能, 需要提供name和action参数
<Upload
listType="picture-card"
name="image"
action={`${baseURL}upload`}
onChange={this.uploadImages}
fileList={this.state.fileList}
>
<PlusOutlined></PlusOutlined>
</Upload>
- 获取上传成功的图片地址
uploadImages = ({ file, fileList }) => {
this.setState({
fileList,
})
}
控制图片的上传数量
- 控制type的切换
state = {
// 存放上传的文件列表
fileList: [],
type: 0,
}
<Radio.Group onChange={this.changeImageType}>
<Radio value={0}>无图</Radio>
<Radio value={1}>单图</Radio>
<Radio value={3}>三图</Radio>
</Radio.Group>
changeImageType = (e) => {
this.setState({
type: e.target.value,
})
}
- 根据type控制图片的显示
{this.state.type !== 0 && (
<Upload
listType="picture-card"
name="image"
action={`${baseURL}upload`}
onChange={this.uploadImages}
fileList={this.state.fileList}
>
<PlusOutlined></PlusOutlined>
</Upload>
)}
- 控制图片的上传数据
{this.state.type !== 0 && (
<Upload
listType="picture-card"
name="image"
action={`${baseURL}upload`}
onChange={this.uploadImages}
fileList={this.state.fileList}
>
{this.state.fileList.length < this.state.type && (
<PlusOutlined></PlusOutlined>
)}
</Upload>
)}
图片预览功能
图片格式校验
表单校验功能
- 表单基本校验
<Form.Item
label="标题"
name="title"
rules={[{ required: true, message: '请输入文章标题' }]}
>
<Input placeholder="请输入文章标题" style={{ width: 400 }} />
</Form.Item>
<Form.Item
label="频道"
name="channel_id"
rules={[{ required: true, message: '请选择文章频道' }]}
>
<Channel></Channel>
</Form.Item>
<Form.Item label="封面">
<Form.Item name="type">
<Radio.Group onChange={this.changeImageType}>
<Radio value={1}>单图</Radio>
<Radio value={3}>三图</Radio>
<Radio value={0}>无图</Radio>
{/* <Radio value={-1}>自动</Radio> */}
</Radio.Group>
</Form.Item>
<div className="upload-list">
{this.state.type !== 0 && (
<Upload
listType="picture-card"
name="image"
action={`${baseURL}upload`}
onChange={this.uploadImages}
fileList={this.state.fileList}
>
{this.state.fileList.length < this.state.type && (
<PlusOutlined></PlusOutlined>
)}
</Upload>
)}
</div>
</Form.Item>
<Form.Item
label="内容"
name="content"
rules={[{ required: true, message: '请输入文章内容' }]}
>
<ReactQuill
theme="snow"
placeholder="请输入文章内容..."
></ReactQuill>
</Form.Item>
- 图片长度的校验
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,
})
}
- 注册点击事件
<Button size="large" onClick={this.addDraft}>
存入草稿
</Button>
- 提供事件
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)
}
修改功能
配置修改文章的路由
- 给修改按钮注册点击事件
<Button
type="primary"
shape="circle"
icon={<EditOutlined />}
onClick={() => this.handleEdit(data.id)}
/>
// 修改
handleEdit = (id) => {
this.props.history.push(`/home/publish/${id}`)
}
- 配置修改文章的路由
{/* 新增 */}
<Route
exact
path="/home/publish"
component={ArticlePublish}
></Route>
{/* 修改的路由 */}
<Route
path="/home/publish/:id"
component={ArticlePublish}
></Route>
数据回显-获取数据
思路
- 在组件中判断能够通过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
{/* 新增 */}
<Route
exact
path="/home/publish"
component={ArticlePublish}
key="add"
></Route>
{/* 修改的路由 */}
<Route
path="/home/publish/:id"
component={ArticlePublish}
key="edit"
></Route>
修改功能完成
- 封装接口,用于修改文章
- 判断是否有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属性
<Menu
theme="dark"
mode="inline"
selectedKeys={[this.state.selectedKey]}
style={{ height: '100%', borderRight: 0 }}
>
state = {
profile: {},
selectedKey: this.props.location.pathname,
}
- 需要在组件更新的时候,修改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必须添加判断,不然会死循环
· [翻译] 为什么 Tracebit 用 C# 开发
· 腾讯ima接入deepseek-r1,借用别人脑子用用成真了~
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· DeepSeek崛起:程序员“饭碗”被抢,还是职业进化新起点?
· RFID实践——.NET IoT程序读取高频RFID卡/标签