接下来我们将会为 File-X 客户端添加事件评论的评论功能。评论相关的功能有:
- 查看评论列表
- 创建评论
- 回复评论
- 修改评论
- 删除评论
我们可以看到,这个跟“事件”包含的功能还是挺像的,所以这一部分,我们就当作第一阶段的功能小结。那么我们就直接撸起袖子,开始干!!!
查看评论列表
事件评论列表的入口已经在 EventItem
中写好了。打开 src/EventComment.js
开始写代码。
抄页面结构,觉得哪个页面像就抄哪个。我这里选的 EventCreation.js
import React from 'react';
import CommentList from './CommentList';
function EventComment(props) {
const {
match: { params: { id } }
} = props;
return (
<div className='App'>
<div className="App__header">
<div className="PageHeader">
<div className="PageHeader__left pl_12">
<button
className='button button_merge'
type='button'
onClick={() => {
props.history.goBack();
}}
>
后退
</button>
</div>
<div className="PageHeader__center">
<h3 className="PageTitle">评论列表</h3>
</div>
<div className="PageHeader__right pr_12">
</div>
</div>
</div>
<div className="App__content">
<CommentList
eventId={id}
/>
</div>
</div>
)
}
export default EventComment;
编写 CommentList
组件,新建 src/CommentList.js
,从 EventList
中借鉴一部分代码
import React, { useState, useEffect } from 'react'
import InfiniteScroll from 'react-infinite-scroll-component'
import { fetchEventComments } from './requests'
import CommentItem from './CommentItem'
function CommentList (props) {
const { eventId } = props
const [state, setState] = useState({
loading: false,
items: [],
hasMore: true,
next: null
})
const loadMore = (reset = false) => {
if (state.loading) {
return
}
setState({
...state,
loading: true,
items: reset ? [] : state.items
})
const params = reset
? {
page: 1,
page_size: 12
}
: state.next || { page: 1, page_size: 12 }
fetchEventComments(eventId, params)
.then(({ data, pagination }) => {
setState({
...state,
loading: false,
next: pagination.next
? {
page: pagination.next,
page_size: pagination.page_size
}
: null,
hasMore: !!pagination.next,
items: reset ? data : [...state.items, ...data]
})
})
.catch(err => {
setState({
...state,
loading: false,
fetchError: err
})
})
}
const refresh = () => loadMore(true)
useEffect(loadMore, [])
return (
<div
id='FeedList'
style={{ height: '100%', overflow: 'auto', backgroundColor: '#f6f6f6' }}
>
<InfiniteScroll
dataLength={state.items.length}
next={loadMore}
hasMore={state.hasMore}
loader={
<div className='px_16 py_12'>
<span>加载中</span>
</div>
}
endMessage={
<div className='px_16 py_12'>
{!state.items.length && !state.hasMore ? (
<b>无相关内容</b>
) : (
<b>已经到底了</b>
)}
</div>
}
scrollableTarget='FeedList'
refreshFunction={refresh}
pullDownToRefresh
pullDownToRefreshThreshold={80}
pullDownToRefreshContent={
<h4 style={{ textAlign: 'center' }}>↓ 下拉刷新</h4>
}
releaseToRefreshContent={
<h4 style={{ textAlign: 'center' }}>↑ 释放后刷新</h4>
}
>
{state.fetchError
? [<div key='error'>{state.fetchError.message}</div>]
: state.items.map((item, index) => (
<CommentItem
key={item.id}
data={item}
/>
))}
</InfiniteScroll>
</div>
)
}
export default CommentList
打开 src/requests.js
,添加 fetchEventComments
方法
// define by featapi, ref: https://docs.featapi.com/api-docs/activity-api-reference/#Comment_TargetType
const COMMENT_TARGET_TYPE_EVENT = 300;
export const fetchEventComments = async (id, params) => {
const baseURL = `${API_ENDPOINT}/api/activity/comment/comment-list/`;
const query = {
object_id: id,
...params,
target_type: COMMENT_TARGET_TYPE_EVENT //
}
const url = `${baseURL}?${stringify(query)}`;
return await fetch(url, {
method: 'GET',
headers: {
Accept: 'application/json',
}
}).then(resHelper);
}
新建 src/CommentItem.js
,并开始编辑。
import React from 'react'
import classNames from 'classnames'
import { Link } from 'react-router-dom'
import './CommentItem.css'
function CommentItem (props) {
const { data, level } = props
return (
<div
className={classNames('CommentItem', {
CommentItem_root: level === 0
})}
>
<div className='CommentItem__main'>
<div className='Comment'>
<div className='Comment__header'>
<img
src={data.user.avatar}
alt={data.user.username}
className='Comment__userAvatar'
/>
<Link
to={{
pathname: `/user/${data.user.uid}`,
state: {
user: data.user
}
}}
className='Comment__username'
>
{data.user.username}
</Link>
<span className='Comment__date'>
{new Date(data.last_modified).toDateString()}
</span>
</div>
<div className='Comment__main'>
<div
className='Comment__content'
dangerouslySetInnerHTML={{ __html: data.content }}
/>
</div>
</div>
</div>
{data.children && !!data.children.length && (
<div className='CommentItem__children'>
{data.children.map(item => (
<CommentItem level={props.level + 1} key={item.id} data={item} />
))}
</div>
)}
</div>
)
}
CommentItem.defaultProps = {
level: 0
}
export default CommentItem
接口返回的数据就是一个嵌套结构的,所以这里会出现 CommentItem 中套有 CommentItem 的情况。详细的接口文档可查阅 Activity.Comment 概述 以及 Comment: commentList
为 CommentItem
添加样式,新建文件 src/CommentItem.css
并编辑
.CommentItem {
background-color: white;
padding-left: 16px;
padding-bottom: 3px;
}
.CommentItem_root {
padding-top: 8px;
padding-right: 16px;
margin-bottom: 16px;
}
.Comment__userAvatar {
width: 32px;
height: 32px;
border-radius: 16px;
}
.Comment__header {
display: flex;
align-items: center;
}
.Comment__username {
margin-left: 8px;
}
.Comment__date {
color: #666;
font-size: 14px;
margin-left: 8px;
}
.Comment__main {
display: flex;
}
.Comment__content {
flex: 1;
margin-top: 5px;
margin-bottom: 5px;
}
得到的结果如下图所示:
来的这里,评论列表的功能算是做好了。模块层级结构整理:
.
├── EventComment
│ ├── CommentList
│ │ └── CommentItem
发表评论
接下来,实现发布评论了功能。这简直跟“发布事件”一模一样。
添加“添加评论”按钮,打开 src/EventComment.js
开始编辑
import NewButton from './NewButton';
// ...
<div className="App__content">
<CommentList
eventId={id}
/>
<NewButton
onClick={() => {
props.history.push(`/event/${id}/comment/new`);
}}
/>
</div>
// ...
打开任意一个事件的评论列表,可以看到“添加评论”按钮,如下图所示:
评论创建表单,打开 src/CommentCreate.js
,开始从 src/EventCreation.js
借代码
import React, { useContext, useState } from 'react';
import { createComment } from './requests';
import { AuthInfo } from './AuthInfoProvider';
function CommentCreate(props) {
const {
match: { params: { id }}
} = props;
const { state: { token }} = useContext(AuthInfo);
const [formValues, setFormValues] = useState({
content: '',
})
const [isSubmitting, setIsSubmitting] = useState(false);
return (
<form
className="App"
onSubmit={(e) => {
e.preventDefault();
// TODO should validate data before submit.
setIsSubmitting(true);
createComment({
eventId: id,
content: `<p>${formValues.content}</p>`
}, token).then(() => {
props.history.goBack();
})
.catch((err) => {
global.alert(err.message);
setIsSubmitting(false);
});
}}
>
<div className="App__header">
<div className="PageHeader">
<div className="PageHeader__left pl_12">
<button
className='button button_merge'
type='button'
onClick={() => {
props.history.goBack();
}}
disabled={isSubmitting}
>
取消
</button>
</div>
<h3 className="PageTitle">新评论</h3>
<div className="PageHeader__right pr_12">
<button
className='button button_merge'
type='submit'
disabled={isSubmitting}
>
发表
</button>
</div>
</div>
</div>
<div className="App__content Form">
<div className="FormItem">
<textarea
id='content'
autoFocus
placeholder="想要说点什么?"
value={formValues.content}
name='content'
className='FormInput'
onChange={(e) => {
setFormValues({
...formValues,
content: e.target.value,
})
}}
/>
</div>
</div>
</form>
)
}
export default CommentCreate;
打开 src/requests.js
,添加 createComment
方法
export const createComment = async (data, token) => {
const url = `${API_ENDPOINT}/api/activity/comment/`;
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `${token.token_type} ${token.access_token}`,
}
return fetch(url, {
method: 'POST',
body: JSON.stringify({
object_id: data.eventId,
target_type: COMMENT_TARGET_TYPE_EVENT,
content: data.content
}),
headers
}).then(resHelper)
}
打开任意事件的评论列表,点击“+”按钮,进入“新评论“页面。效果如下所示
填写表单,并“发表”后,页面会返回到“评论列表”页面。
评论修改
下来我们来添加评论修改功能。
添加“修改”按钮。打开 src/CommentItem.js
,并开始编辑:
import React, { useContext } from 'react'
import { Link, useHistory, useLocation } from 'react-router-dom'
import { AuthInfo } from './AuthInfoProvider';
function CommentItem(props) {
const { state: { currentUser } } = useContext(AuthInfo);
const history = useHistory();
const location = useLocation()
const canEdit = currentUser && currentUser.uid === data.user.uid;
// ...
<div className='Comment__main'>
<div
className='Comment__content'
dangerouslySetInnerHTML={{ __html: data.content }}
/>
<div className='Comment__action'>
{canEdit && (
<button
className="button button_sm button_merge"
onClick={() => {
history.push(`${location.pathname}/${data.id}/edit`, {
comment: data
})
}}
>
E
</button>
)}
</div>
</div>
// ...
}
创建“编辑”表单,打开 src/CommentEdit.js
,从 src/CommentCreate.js
从借鉴代码:
import React, { useContext, useState } from 'react';
import { updateComment } from './requests';
import { AuthInfo } from './AuthInfoProvider';
function CommentEdit(props) {
const {
match: { params: { cid }},
location: {
state,
}
} = props;
const { state: { token }} = useContext(AuthInfo);
const [formValues, setFormValues] = useState({
content: state && state.comment ? state.comment.content : '',
})
const [isSubmitting, setIsSubmitting] = useState(false);
return (
<form
className="App"
onSubmit={(e) => {
e.preventDefault();
// TODO should validate data before submit.
setIsSubmitting(true);
updateComment(cid, formValues, token).then(() => {
props.history.goBack();
})
.catch((err) => {
global.alert(err.message);
setIsSubmitting(false);
});
}}
>
<div className="App__header">
<div className="PageHeader">
<div className="PageHeader__left pl_12">
<button
className='button button_merge'
type='button'
onClick={() => {
props.history.goBack();
}}
disabled={isSubmitting}
>
取消
</button>
</div>
<h3 className="PageTitle">修改评论</h3>
<div className="PageHeader__right pr_12">
<button
className='button button_merge'
type='submit'
disabled={isSubmitting}
>
更新
</button>
</div>
</div>
</div>
<div className="App__content Form">
<div className="FormItem">
<textarea
id='content'
autoFocus
placeholder="想要说点什么?"
value={formValues.content}
name='content'
className='FormInput'
onChange={(e) => {
setFormValues({
...formValues,
content: e.target.value,
})
}}
/>
</div>
</div>
</form>
)
}
export default CommentEdit;
编辑 src/requests.js
,添加 updateComment
方法
export const updateComment = async (id, data, token) => {
const url = `${API_ENDPOINT}/api/activity/comment/${id}/`;
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `${token.token_type} ${token.access_token}`,
}
return fetch(url, {
method: 'PATCH',
body: JSON.stringify(data),
headers
}).then(resHelper)
}
效果如下:
删除评论
们计划在“更新”评论的页面里加上“删除”评论的功能。当用户想要提交的数据是空数据时,提示用户是否想要删除评论。待用户确认后删除评论。
打开 src/CommentEdit.js
,继续编辑,修改提交表单的处理代码
import { updateComment, deleteComment } from './requests';
function CommentEdit() {
// .. codes
return (
<form
className="App"
onSubmit={(e) => {
e.preventDefault();
setIsSubmitting(true);
if (!formValues.content.trim()) {
if (global.confirm('想要删除评论?')) {
deleteComment(cid, token).then(() => {
props.history.goBack();
})
}
return;
}
updateComment(cid, formValues, token).then(() => {
props.history.goBack();
})
.catch((err) => {
global.alert(err.message);
setIsSubmitting(false);
});
}}
>
{/* .. */}
</form>
)
}
修改 src/requests.js
,添加 deleteComment
方法
export const deleteComment = async (id, token) => {
const url = `${API_ENDPOINT}/api/activity/comment/${id}/`;
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `${token.token_type} ${token.access_token}`,
}
return fetch(url, {
method: 'DELETE',
headers
}).then(resHelper)
}
回复评论
接下来,我们继续添加评论回复的功能。我们会像添加“修改”评论功能那样。先在 CommentItem
上添加“回复”按钮,然后继续添加“回复”表单。
添加“回复”按钮,打开 src/CommentItem.js
的组件开始编辑
function CommentItem (props) {
// ...
const canReply = currentUser && currentUser.uid !== data.user.uid;
return (
<div>
{/* ... */}
<div className="Comment__action">
{/* ... canEdit */}
{canReply && (
<button
className="button button_sm button_merge"
onClick={() => {
history.push(`${location.pathname}/${data.id}/reply`, {
comment: data
})
}}
>
R
</button>
)}
</div>
{/* ... */}
</div>
)
}
创建“回复”表单,打开 src/CommentReply.js
,开始编辑
import React, { useContext, useState } from 'react';
import { replyComment } from './requests';
import { AuthInfo } from './AuthInfoProvider';
function CommentReply(props) {
const {
match: { params: { id, cid }}
} = props;
const { state: { token }} = useContext(AuthInfo);
const [formValues, setFormValues] = useState({
content: '',
})
const [isSubmitting, setIsSubmitting] = useState(false);
return (
<form
className="App"
onSubmit={(e) => {
e.preventDefault();
// TODO should validate data before submit.
setIsSubmitting(true);
replyComment(cid, {
eventId: id,
content: formValues.content,
}, token).then(() => {
props.history.goBack();
})
.catch((err) => {
global.alert(err.message);
setIsSubmitting(false);
});
}}
>
<div className="App__header">
<div className="PageHeader">
<div className="PageHeader__left pl_12">
<button
className='button button_merge'
type='button'
onClick={() => {
props.history.goBack();
}}
disabled={isSubmitting}
>
取消
</button>
</div>
<h3 className="PageTitle">回复评论</h3>
<div className="PageHeader__right pr_12">
<button
className='button button_merge'
type='submit'
disabled={isSubmitting}
>
发表
</button>
</div>
</div>
</div>
<div className="App__content Form">
<div className="FormItem">
<textarea
id='content'
autoFocus
placeholder="想要说点什么?"
value={formValues.content}
name='content'
className='FormInput'
onChange={(e) => {
setFormValues({
...formValues,
content: e.target.value,
})
}}
/>
</div>
</div>
</form>
)
}
export default CommentReply;
打开 src/requests.js
,添加 replyComment
方法
export const replyComment = async (id, data, token) => {
const url = `${API_ENDPOINT}/api/activity/comment/${id}/reply/`;
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `${token.token_type} ${token.access_token}`,
}
return fetch(url, {
method: 'POST',
body: JSON.stringify({
content: data.content,
object_id: data.eventId,
target_type: COMMENT_TARGET_TYPE_EVENT,
}),
headers
}).then(resHelper)
}
小结
这个教程里里面,我们相对快速地实现了评论的查看、创建、修改、删除。每个页面的功能都是挺简单的,一般都是:取得数据->展示数据->提交数据->返回上层。
来到这里,这个系列教程的第一阶段,“基础功能实现”这部分已经完成的。下一个阶段,我们会针对“这个版本”中出现的问题,来进行优化处理。