[File-X Demo] — 事件的评论

[File-X Demo] — 事件的评论

接下来我们将会为 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)
}

小结

这个教程里里面,我们相对快速地实现了评论的查看、创建、修改、删除。每个页面的功能都是挺简单的,一般都是:取得数据->展示数据->提交数据->返回上层。

来到这里,这个系列教程的第一阶段,“基础功能实现”这部分已经完成的。下一个阶段,我们会针对“这个版本”中出现的问题,来进行优化处理。

发表评论

您的电子邮箱地址不会被公开。

您可以使用以下 HTML标签和属性:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Captcha Code