MYW
3 years ago
9 changed files with 621 additions and 95 deletions
-
308react-pc-project/package-lock.json
-
1react-pc-project/package.json
-
247react-pc-project/src/pages/Article/index.js
-
0react-pc-project/src/pages/Article/index.scss
-
5react-pc-project/src/pages/Layout/index.js
-
120react-pc-project/src/pages/Publish/index.js
-
17react-pc-project/src/pages/Publish/index.scss
-
16react-pc-project/src/store/channel.Store.js
-
2react-pc-project/src/store/index.js
@ -1,7 +1,248 @@ |
|||
function Article () { |
|||
import { Link, useNavigate } from 'react-router-dom' |
|||
import { Card, Breadcrumb, Form, Button, Radio, DatePicker, Select, Table, Tag, Space, Popconfirm } from 'antd' |
|||
import 'moment/locale/zh-cn' |
|||
import locale from 'antd/es/date-picker/locale/zh_CN' |
|||
import { EditOutlined, DeleteOutlined } from '@ant-design/icons' |
|||
import img404 from '@/assets/error.png' |
|||
import './index.scss' |
|||
import { useEffect, useState } from 'react' |
|||
import { http } from '@/utils' |
|||
import { useStore } from '@/store' |
|||
import { observer } from 'mobx-react-lite' |
|||
|
|||
const { Option } = Select |
|||
const { RangePicker } = DatePicker |
|||
|
|||
const Article = () => { |
|||
const { channelStore } = useStore() |
|||
/* // 频道列表管理 |
|||
const [channeList, setChangelList] = useState([]) |
|||
|
|||
useEffect(() => { |
|||
const loadChannelList = async () => { |
|||
const res = await http.get('/channels') |
|||
setChangelList(res.data.channels) |
|||
} |
|||
loadChannelList() |
|||
}, []) */ |
|||
|
|||
// 文章列表管理 (统一管理数据,将来修改给 setList 传对象)
|
|||
const [articleData, setArticleData] = useState({ |
|||
list: [], // 文章列表
|
|||
count: 0 // 文章数量
|
|||
}) |
|||
|
|||
// 文章参数管理
|
|||
const [params, setParams] = useState({ |
|||
page: 1, |
|||
per_page: 10, |
|||
}) |
|||
|
|||
/** |
|||
* 如果异步请求函数需要依赖一些数据的变化而重新执行,推荐把异步请求函数写到 useEffect 内部 |
|||
* 统一不抽离函数到外面,只要涉及到异步请求的函数,都放到 useEffect 内部 |
|||
* 本质区别:异步请求函数写到 useEffect 外部,当每次组件更新都会重新进行函数初始化,这本身就是一次性能消耗;而写到 useEffect 中,只会会依赖项发生变化的时候,函数才会进行重新初始化,避免性能损失 |
|||
*/ |
|||
useEffect(() => { |
|||
const loadList = async () => { |
|||
const res = await http.get('/mp/articles', { params }) |
|||
console.log(res) |
|||
const { results, total_count } = res.data |
|||
setArticleData ({ |
|||
list: results, |
|||
count: total_count |
|||
}) |
|||
} |
|||
loadList() |
|||
}, [params]) |
|||
|
|||
// 筛选
|
|||
const onFinish = (values) => { |
|||
console.log(values) |
|||
const { channel_id, date, status } = values |
|||
// 数据处理
|
|||
const _params = {} |
|||
if (status !== -1) { |
|||
_params.status = status |
|||
} |
|||
if (channel_id) { |
|||
_params.channel_id = channel_id |
|||
} |
|||
if (date) { |
|||
// 开始日期
|
|||
_params.begin_pubdate = date[0].format('YYY-MM-DD') |
|||
// 结束日期
|
|||
_params.end_pubdate = date[1].format('YYY-MM-DD') |
|||
} |
|||
// 修改 params 数据,引起接口的重新发送,数据的字段是做一个对象的合并,对象的合并是一个整体的覆盖,改变了对象的整体应用
|
|||
// 以下是不覆盖整体对象:
|
|||
setParams({ |
|||
...params, |
|||
..._params |
|||
}) |
|||
} |
|||
|
|||
// 分页
|
|||
const pageChange = (page) => { |
|||
setParams({ |
|||
...params, |
|||
page |
|||
}) |
|||
} |
|||
|
|||
// 删除
|
|||
const delArticle = async (data) => { |
|||
await http.delete(`/mp/articles/${data.id}`) |
|||
// 删除完要刷新列表(改动 useEffect 的依赖项就会重新调用接口进行刷新)
|
|||
// 删完后会跳转回第一页
|
|||
setParams ({ |
|||
...params, |
|||
page: 1 |
|||
}) |
|||
} |
|||
|
|||
// 编辑
|
|||
const navigate = useNavigate() |
|||
const goPublish = (data) => { |
|||
navigate(`/publish?id=${data.id}`) |
|||
} |
|||
|
|||
const columns = [ |
|||
{ |
|||
title: '封面', |
|||
dataIndex: 'cover', |
|||
width: 120, |
|||
render: cover => { |
|||
return <img src={cover.images[0] || img404} width={80} height={60} alt="" /> |
|||
} |
|||
}, |
|||
{ |
|||
title: '标题', |
|||
dataIndex: 'title', |
|||
width: 220 |
|||
}, |
|||
{ |
|||
title: '状态', |
|||
dataIndex: 'status', |
|||
render: data => <Tag color="green">审核通过</Tag> |
|||
}, |
|||
{ |
|||
title: '发布时间', |
|||
dataIndex: 'pubdate' |
|||
}, |
|||
{ |
|||
title: '阅读数', |
|||
dataIndex: 'read_count' |
|||
}, |
|||
{ |
|||
title: '评论数', |
|||
dataIndex: 'comment_count' |
|||
}, |
|||
{ |
|||
title: '点赞数', |
|||
dataIndex: 'like_count' |
|||
}, |
|||
{ |
|||
title: '操作', |
|||
render: data => { |
|||
return ( |
|||
<Space size="middle"> |
|||
<Popconfirm |
|||
title="确定要编辑吗?" |
|||
okText="确定" |
|||
cancelText="取消" |
|||
onConfirm={() => goPublish(data)}> |
|||
<Button |
|||
type="primary" |
|||
shape="circle" |
|||
icon={<EditOutlined />} |
|||
/> |
|||
</Popconfirm> |
|||
|
|||
<Popconfirm |
|||
title="确定删除吗?" |
|||
okText="确定" |
|||
cancelText="取消" |
|||
onConfirm={() => delArticle(data)}> |
|||
<Button |
|||
type="primary" |
|||
danger |
|||
shape="circle" |
|||
icon={<DeleteOutlined />} |
|||
/> |
|||
</Popconfirm> |
|||
</Space> |
|||
) |
|||
} |
|||
} |
|||
] |
|||
|
|||
return ( |
|||
<div>Article</div> |
|||
<div> |
|||
{/* 筛选区域 */} |
|||
<Card |
|||
title={ |
|||
<Breadcrumb separator=">"> |
|||
<Breadcrumb.Item> |
|||
<Link to="/home">首页</Link> |
|||
</Breadcrumb.Item> |
|||
<Breadcrumb.Item>内容管理</Breadcrumb.Item> |
|||
</Breadcrumb> |
|||
} |
|||
style={{ marginBottom: 20 }} |
|||
> |
|||
<Form |
|||
onFinish={onFinish} |
|||
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="channel_id"> |
|||
<Select |
|||
placeholder="请选择文章频道" |
|||
style={{ width: 120 }} |
|||
> |
|||
|
|||
{channelStore.channelList.map(channel => <Option key={channel.id} value="channel.id">{channel.name}</Option>)} |
|||
|
|||
</Select> |
|||
</Form.Item> |
|||
|
|||
<Form.Item label="日期" name="date"> |
|||
{/* 传入locale属性 控制中文显示*/} |
|||
<RangePicker locale={locale}></RangePicker> |
|||
</Form.Item> |
|||
|
|||
<Form.Item> |
|||
<Button type="primary" htmlType="submit" style={{ marginLeft: 80 }}> |
|||
筛选 |
|||
</Button> |
|||
</Form.Item> |
|||
</Form> |
|||
</Card> |
|||
|
|||
{/* 文章列表区域 */} |
|||
<Card title={`根据筛选条件共查询到 ${articleData.count} 条结果:`}> |
|||
<Table |
|||
rowKey="id" |
|||
columns={columns} |
|||
dataSource={articleData.list} |
|||
pagination={{ |
|||
pageSize: params.per_page, |
|||
total: articleData.count, |
|||
onChange: pageChange |
|||
}} |
|||
/> |
|||
</Card> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default Article |
|||
export default observer(Article) |
@ -1,7 +1,121 @@ |
|||
function Publish () { |
|||
import { |
|||
Card, |
|||
Breadcrumb, |
|||
Form, |
|||
Button, |
|||
Radio, |
|||
Input, |
|||
Upload, |
|||
Space, |
|||
Select |
|||
} from 'antd' |
|||
import { PlusOutlined } from '@ant-design/icons' |
|||
import { Link } from 'react-router-dom' |
|||
import './index.scss' |
|||
import ReactQuill from 'react-quill' |
|||
import 'react-quill/dist/quill.snow.css' |
|||
import { useStore } from '@/store' |
|||
import { observer } from 'mobx-react-lite' |
|||
import { useState } from 'react' |
|||
|
|||
const { Option } = Select |
|||
|
|||
const Publish = () => { |
|||
const { channelStore } = useStore() |
|||
|
|||
// 存放上传图片的列表
|
|||
const { fileList, setFileList } = useState([]) |
|||
|
|||
const onUploadChange = ({fileList}) => { |
|||
console.log(fileList) |
|||
// 采取受控的写法,在最后一次 log 里 response,最终 react state fileList 中存放的数据有 response.data.url
|
|||
setFileList(fileList) |
|||
} |
|||
|
|||
return ( |
|||
<div>Publish</div> |
|||
<div className="publish"> |
|||
<Card |
|||
title={ |
|||
<Breadcrumb separator=">"> |
|||
<Breadcrumb.Item> |
|||
<Link to="/home">首页</Link> |
|||
</Breadcrumb.Item> |
|||
<Breadcrumb.Item>发布文章</Breadcrumb.Item> |
|||
</Breadcrumb> |
|||
} |
|||
> |
|||
<Form |
|||
labelCol={{ span: 4 }} |
|||
wrapperCol={{ span: 16 }} |
|||
initialValues={{ type: 1, content: 'this is content' }} |
|||
> |
|||
<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: '请选择文章频道' }]} |
|||
> |
|||
<Select placeholder="请选择文章频道" style={{ width: 400 }}> |
|||
{channelStore.channelList.map(item => ( |
|||
<Option key={item.id} value={item.id}>{item.name}</Option> |
|||
))} |
|||
</Select> |
|||
</Form.Item> |
|||
|
|||
<Form.Item label="封面"> |
|||
<Form.Item name="type"> |
|||
<Radio.Group> |
|||
<Radio value={1}>单图</Radio> |
|||
<Radio value={3}>三图</Radio> |
|||
<Radio value={0}>无图</Radio> |
|||
</Radio.Group> |
|||
</Form.Item> |
|||
<Upload |
|||
name="image" |
|||
listType="picture-card" |
|||
className="avatar-uploader" |
|||
showUploadList |
|||
action="http://geek.itheima.net/v1_0/upload" |
|||
fileList={fileList} |
|||
onChange={onUploadChange} |
|||
> |
|||
<div style={{ marginTop: 8 }}> |
|||
<PlusOutlined /> |
|||
</div> |
|||
</Upload> |
|||
</Form.Item> |
|||
|
|||
{/* 这里的富文本组件,已经被 Form.Item 控制,它的输入内容会在 onFinished 回调中收集起来 */} |
|||
<Form.Item |
|||
label="内容" |
|||
name="content" |
|||
rules={[{ required: true, message: '请输入文章内容' }]} |
|||
> |
|||
<ReactQuill |
|||
className="publish-quill" |
|||
theme="snow" |
|||
placeholder="请输入文章内容" |
|||
/> |
|||
</Form.Item> |
|||
|
|||
<Form.Item wrapperCol={{ offset: 4 }}> |
|||
<Space> |
|||
<Button size="large" type="primary" htmlType="submit"> |
|||
发布文章 |
|||
</Button> |
|||
</Space> |
|||
</Form.Item> |
|||
</Form> |
|||
</Card> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default Publish |
|||
export default observer(Publish) |
@ -0,0 +1,17 @@ |
|||
.publish { |
|||
position: relative; |
|||
} |
|||
|
|||
.ant-upload-list { |
|||
.ant-upload-list-picture-card-container, |
|||
.ant-upload-select { |
|||
width: 146px; |
|||
height: 146px; |
|||
} |
|||
} |
|||
|
|||
.publish-quill { |
|||
.ql-editor { |
|||
min-height: 300px; |
|||
} |
|||
} |
@ -0,0 +1,16 @@ |
|||
import { makeAutoObservable } from 'mobx' |
|||
import {http} from '@/utils' |
|||
|
|||
class ChannelStore { |
|||
channelList = [] |
|||
constructor () { |
|||
makeAutoObservable(this) |
|||
} |
|||
|
|||
loadChannelList = async () => { |
|||
const res = await http.get('/channels') |
|||
this.channelList = res.data.channels |
|||
} |
|||
} |
|||
|
|||
export default ChannelStore |
Write
Preview
Loading…
Cancel
Save
Reference in new issue