界面正在加载中,请稍后

简易贴吧评论 (Vue)

JarryChenJarryChen 发布时间:2020-12-12 文章字数:2265 预计用时:11分20秒

  • 项目开发过程中,遇到一个内容是给文章评论,原有的评论样式一个是散乱,还有一个是不够直观清楚地看到互相回复的;所以在此基础上,参考了百度贴吧的样式,重新制作了一个简易版的贴吧评论。
  • UI方面仅分页是使用ant design vue的组件,其余都是原生写的。

# 1. 优先处理分页的位置

  • 分页部分代码如下:
<!-- 因为分页允许切换个数,且是不设置高度(不要滚动)的情况下,
如果内容太多,返回到顶部也难定位,
故在列表的上下各做了一个分页,上面的右对齐,下面的左对齐 -->
<template>
  <div class="comment-component">
    <a-pagination
      :total="total"
      :show-total="total => `${total} 条回复贴`"
      showQuickJumper
      showSizeChanger
      hideOnSinglePage
      v-model="pagination.pageNo"
      @change="ListPageChange"
      @showSizeChange="ListSizeChange"
      :pageSize.sync="pagination.pageSize"
      class="diy-pagination"
    />
    <ul class="comment-list-data">
      <!-- 评论列表内容 -->
    </ul>
    <a-pagination
      :total="total"
      :show-total="total => `${total} 条回复贴`"
      showQuickJumper
      showSizeChanger
      hideOnSinglePage
      v-model="pagination.pageNo"
      :pageSize.sync="pagination.pageSize"
      @change="ListPageChange"
      @showSizeChange="ListSizeChange"
      class="diy-pagination-bottom"
    />
  </div>
</template>

<script>
/* eslint-disable */
import Editor from '@/xxx/CKEditor'
import $ from 'jquery'
export default {
  name: 'DIYComment',
  components: {
    Editor
  },
  props: {
    /* 富文本需要的上传url */
    CKEditorUrl: {
      type: String,
      default: ''
    },
    /* 是否需要子评论面板 */
    needSubPanel: {
      type: Boolean,
      default: false
    },
    /* 拉取父评论列表接口,必须是promise */
    getCommentList: {
      type: Function,
      required: true
    },
    /* 拉取子评论列表接口,必须是promise */
    getSubCommentList: {
      type: Function
    },
    /* 必备字段的替换obj */
    replaceFields: {
      type: Object,
      default: () => {}
    }
  },
  data() {
    return {
      pagination: {
        pageNo: 1,
        pageSize: 10
      },
      total: 0,
      commentListData: []
    }
  },
  computed: {
    /*
    ...
    */
    diyReplace() {
      return Object.assign(
        {},
        {
          id: 'id',
          content: 'comment',
          creator: 'creator',
          time: 'createdDate'
        },
        this.replaceFields
      )
    }
  },
  created() {
    /* 组件创建即初始化父列表 */
    this.loadCommentList()
  },
  methods: {
    /*
    ...
    */
    // 父评论页数大小改变
    ListSizeChange(current, size) {
      Object.assign(this.pagination, {
        pageNo: 1,
        pageSize: size
      })
      this.loadCommentList()
    },

    // 父评论页数切换
    ListPageChange(pageNo, pageSize) {
      Object.assign(this.pagination, {
        pageNo,
        pageSize
      })
      this.loadCommentList()
    },

    // 评论列表获取
    loadCommentList() {
      const result = this.getCommentList(this.pagination)
      if ((typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
        result.then(r => {
          if (this.total !== r.totalCount) {
            this.total = r.totalCount
          }
          /* 如果没有数据了,就走前一页 */
          if (Array.isArray(r.data) && !r.data.length) {
            this.pagination.pageNo = Math.max(this.pagination.pageNo - 1, 1)
            this.loadCommentList()
          }
          /* 是数组了再走后面的处理 */
          Array.isArray(r?.data) &&
            (this.commentListData = r.data.map(item => {
              const datetime =
                typeof item[this.diyReplace.time] === 'string'
                  ? item[this.diyReplace.time].slice(0, item[this.diyReplace.time].lastIndexOf('.'))
                  : item[this.diyReplace.time]
              return {
                id: item[this.diyReplace.id],
                content: item[this.diyReplace.content],
                showReply: false,
                author: Number(item[this.diyReplace.creator]),
                datetime,
                reply: false,
                comment: '',
                isAnonymous: 0,
                pagination: {
                  pageNo: 1,
                  pageSize: 10
                },
                total: 0,
                children: [],
                anonymous: item.isAnonymous
              }
            }))
        })
      }
    },

    // 列表刷新,提供给外部组件ref进行调用
    refresh(bool = true) {
      bool &&
        (this.pagination = Object.assign(
          {},
          {
            pageNo: 1,
            pageSize: this.pagination.pageSize
          }
        ))
      this.loadCommentList()
    }
    /*
    ...
    */
  }
}
</script>

<style lang="less" src="./style.less" />

# 2. 列表内容的安排

  • 关键部分代码如下:
<!-- eslint-disable -->
<template>
  <div class="comment-component">
    <!-- 上分页 -->
    <ul class="comment-list-data">
      <li v-for="(item, index) in commentListData" :key="item.datetime" class="list-item">
        <div class="left-icon">
          <!-- 因为没有头像,取回复人的名字当头像 -->
          <p class="author-avatar">
            <span>{{ renameAuthor(item) }}</span>
          </p>
          <!-- 取回复人名字 -->
          <p class="author-name">
            <span>{{ item.anonymous ? '匿名' : $filterUserText(item.author) }}</span>
          </p>
        </div>
        <div class="comment-content">
          <!-- 回复内容 -->
          <p v-html="removeInvalid(item.content)" class="main-content" />
          <!-- 第几楼,回复时间,回复按钮,删除按钮 -->
          <p class="tips">
            <span>{{ (pagination.pageNo - 1) * pagination.pageSize + index + 1 }}楼</span>
            <span>{{ item.datetime }} </span>
            <span
              class="reply"
              v-if="needSubPanel"
              @click="
                () => {
                  item.showReply = !item.showReply
                  loadCommentSubList(item)
                }
              "
              :class="item.showReply ? 'show' : 'hide'"
            >
              {{ item.showReply ? '收起' : '' }}回复{{ item.total || '' }}
            </span>
            <span class="reply" v-if="userId === item.author" @click="deleteComment(item.id)"> 删除 </span>
          </p>
          <!-- 子评论列表面板 -->
          <!-- 来一个动画效果 -->
          <transition name="slide-fade-editor">
            <div class="reply-list" v-if="item.showReply && needSubPanel">
              <template v-if="item.children.length">
                <div v-for="subcomment in item.children" :key="subcomment.id" class="reply-item">
                 <!-- 特殊处理了谁回复了谁 -->
                  <p class="main-info">
                    <a href="javascript:void(0)"
                      ><a-icon type="user" />{{
                        subcomment.isAnonymous ? '匿名' : $filterUserText(subcomment.author)
                      }}</a
                    >
                    <span style="padding-left: 5px" v-if="isReply(subcomment.content)">回复</span>
                    <a href="javascript:void(0)" v-if="getReplyAuthor(subcomment.content)"
                      ><a-icon type="user" />{{ getReplyAuthor(subcomment.content) }}</a
                    ><span v-html="removeSubContentInvalid(subcomment.content)" />
                  </p>
                  <!-- 删除或者是回复这条评论 -->
                  <p class="bottom-info">
                    <span class="delete" @click="deleteComment(subcomment.id)" v-if="userId === subcomment.author"
                      >删除 |</span
                    >
                    <span>{{ subcomment.datetime }}</span>
                    <span class="reply" @click="SubCommentReply(item, subcomment)">回复</span>
                  </p>
                </div>
              </template>
              <template v-else>
                <p style="text-align: center; color: #aaa">————&emsp;暂无回复&emsp;————</p>
              </template>
              <p class="reply-button">
               <!-- 子评论简易翻页器 -->
                <a-pagination
                  simple
                  size="small"
                  hideOnSinglePage
                  @change="(pageNo, pageSize) => SubListPageChange(item, pageNo, pageSize)"
                  :total="item.total"
                  :pageSize.sync="item.pagination.pageSize"
                  v-model="item.pagination.pageNo"
                />
                <!-- 子评论里评论 -->
                <a-button
                  size="small"
                  @click="
                    () => {
                      item.reply = !item.reply
                      if (!item.reply) {
                        item.comment = ''
                      }
                    }
                  "
                  >我也说一句</a-button
                >
              </p>
              <!-- 显示富文本编辑器和是否匿名 -->
              <transition name="slide-fade">
                <div id="button-reply">
                  <editor
                    v-if="item.reply && item.showReply"
                    key="editor"
                    v-model="item.comment"
                    :uploadUrl="CKEditorUrl"
                  />
                  <p class="reply-by-api" v-if="item.reply && item.showReply" key="button-reply">
                    <span
                      >是否匿名:
                      <a-radio-group name="radioGroup" v-model="item.isAnonymous">
                        <a-radio :value="0"></a-radio>
                        <a-radio :value="1"></a-radio>
                      </a-radio-group>
                    </span>
                    <a-button size="small" type="primary" @click="SubCommentCommit(item)">发表</a-button>
                  </p>
                </div>
              </transition>
            </div>
          </transition>
        </div>
      </li>
    </ul>
    <!-- 下分页 -->
  </div>
</template>

<script>
/* eslint-disable */
/* 省略引入部分 */
export default {
  name: 'DIYComment',
  components: {
    Editor
  },
  /* 省略 props部分 */
  data() {
    return {
      pagination: {
        pageNo: 1,
        pageSize: 10
      },
      total: 0,
      commentListData: []
    }
  },
  computed: {
    userId() {
      return this.$getCurrentUser()?.id
    },
    /* 省略diyReplace */
  },
  created() {
    /* 调用列表刷新 */
  },
  methods: {
    // 判断是否有回复
    isReply(content) {
      const newContent = this.removeInvalid(content)
      return newContent.indexOf('回复') === newContent.indexOf('>') + 1
    },

    // 过滤非法的评论(这里只是个粗略版本)
    removeInvalid(content) {
      return content.includes('<title>') ? content.replace(/<[^<>]+>/g, '') : content
    },

    // 取出回复子评论作者
    getReplyAuthor(content) {
      const newContent = content.replace(/<[^<>]+>/g, '')
      if (newContent.includes(':') && newContent.indexOf('回复') === 0) {
        return newContent.slice(2, newContent.indexOf(':')).trim()
      }
      return null
    },

    // 子评论内容处理
    removeSubContentInvalid(content) {
      let newContent = this.removeInvalid(content)
      if (newContent.includes(':') && this.isReply(newContent)) {
        newContent = newContent.slice(newContent.indexOf(':') + 1)
      }
      return newContent
    },

    // 处理作者的名字
    renameAuthor({ author, anonymous }) {
      if (anonymous) {
        return '匿名'
      } else {
        const name = this.$filterUserText(author)
        return name.length >= 2 ? name.slice(name.length - 2) : '匿名'
      }
    },

    // 子评论里回复
    SubCommentReply(parent, { author, isAnonymous }) {
      parent.reply = true
      this.$nextTick(() => {
        $('html, body').animate({ scrollTop: $('#button-reply').offset().top }, 800)
        parent.comment = `回复 ${isAnonymous ? '匿名' : this.$filterUserText(author)}`
      })
    },

    /* 省略父评论列表翻页方法 */

    // 子评论翻页
    SubListPageChange(item, pageNo, pageSize) {
      Object.assign(item.pagination, {
        pageNo,
        pageSize
      })
      this.loadCommentSubList(item)
    },

    // 子评论获取列表
    loadCommentSubList(item) {
      let result
      if (typeof this.getSubCommentList === 'function') {
        result = this.getSubCommentList(item)
      }
      if ((typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
        result.then(r => {
          console.log(r)
          if (item.total !== r.totalCount) {
            item.total = r.totalCount
          }
          if (Array.isArray(r.data) && !r.data.length) {
            typeof item?.pagination?.pageNo === 'number' &&
              (item.pagination.pageNo = Math.max(item.pagination.pageNo - 1, 1))
            this.loadCommentSubList(item)
          }
          Array.isArray(r?.data) &&
            (item.children = r.data.map(item => {
              const datetime =
                typeof item[this.diyReplace.time] === 'string'
                  ? item[this.diyReplace.time].slice(0, item[this.diyReplace.time].lastIndexOf('.'))
                  : item[this.diyReplace.time]
              return {
                id: item[this.diyReplace.id],
                content: item[this.diyReplace.content],
                author: Number(item[this.diyReplace.creator]),
                datetime,
                isAnonymous: item.isAnonymous
              }
            }))
        })
      }
    },

    /* 省略拉取父评论列表方法 */

    // 评论删除
    deleteComment(id) {
      this.$emit('deletecomment', id)
    },

    // 子评论提交
    SubCommentCommit(item) {
      const param = {
        comment: item.comment,
        isAnonymous: item.isAnonymous,
        parentCommentId: item.id
      }
      const callback = (param, item) => {
        item.comment = ''
        item.isAnonymous = 0
        this.loadCommentSubList(item)
      }
      this.$emit('savecomment', param, 'sub', item, callback)
    },

    /* 省略刷新方法 */
  }
}
</script>

# 3. 整合内容及分页

  • 组件vue页面代码如下:
<!-- eslint-disable -->
<template>
  <div class="comment-component">
    <a-pagination
      :total="total"
      :show-total="total => `${total} 条回复贴`"
      showQuickJumper
      showSizeChanger
      hideOnSinglePage
      v-model="pagination.pageNo"
      @change="ListPageChange"
      @showSizeChange="ListSizeChange"
      :pageSize.sync="pagination.pageSize"
      class="diy-pagination"
    />
    <ul class="comment-list-data">
      <li v-for="(item, index) in commentListData" :key="item.datetime" class="list-item">
        <div class="left-icon">
          <p class="author-avatar">
            <span>{{ renameAuthor(item) }}</span>
          </p>
          <p class="author-name">
            <span>{{ item.anonymous ? '匿名' : $filterUserText(item.author) }}</span>
          </p>
        </div>
        <div class="comment-content">
          <p v-html="removeInvalid(item.content)" class="main-content" />
          <p class="tips">
            <span>{{ (pagination.pageNo - 1) * pagination.pageSize + index + 1 }}楼</span>
            <span>{{ item.datetime }} </span>
            <span
              class="reply"
              v-if="needSubPanel"
              @click="
                () => {
                  item.showReply = !item.showReply
                  loadCommentSubList(item)
                }
              "
              :class="item.showReply ? 'show' : 'hide'"
            >
              {{ item.showReply ? '收起' : '' }}回复{{ item.total || '' }}
            </span>
            <span class="reply" v-if="userId === item.author" @click="deleteComment(item.id)"> 删除 </span>
          </p>
          <transition name="slide-fade-editor">
            <div class="reply-list" v-if="item.showReply && needSubPanel">
              <template v-if="item.children.length">
                <div v-for="subcomment in item.children" :key="subcomment.id" class="reply-item">
                  <p class="main-info">
                    <a href="javascript:void(0)"
                      ><a-icon type="user" />{{
                        subcomment.isAnonymous ? '匿名' : $filterUserText(subcomment.author)
                      }}</a
                    >
                    <span style="padding-left: 5px" v-if="isReply(subcomment.content)">回复</span>
                    <a href="javascript:void(0)" v-if="getReplyAuthor(subcomment.content)"
                      ><a-icon type="user" />{{ getReplyAuthor(subcomment.content) }}</a
                    ><span v-html="removeSubContentInvalid(subcomment.content)" />
                  </p>
                  <p class="bottom-info">
                    <span class="delete" @click="deleteComment(subcomment.id)" v-if="userId === subcomment.author"
                      >删除 |</span
                    >
                    <span>{{ subcomment.datetime }}</span>
                    <span class="reply" @click="SubCommentReply(item, subcomment)">回复</span>
                  </p>
                </div>
              </template>
              <template v-else>
                <p style="text-align: center; color: #aaa">————&emsp;暂无回复&emsp;————</p>
              </template>
              <p class="reply-button">
                <a-pagination
                  simple
                  size="small"
                  hideOnSinglePage
                  @change="(pageNo, pageSize) => SubListPageChange(item, pageNo, pageSize)"
                  :total="item.total"
                  :pageSize.sync="item.pagination.pageSize"
                  v-model="item.pagination.pageNo"
                />
                <a-button
                  size="small"
                  @click="
                    () => {
                      item.reply = !item.reply
                      if (!item.reply) {
                        item.comment = ''
                      }
                    }
                  "
                  >我也说一句</a-button
                >
              </p>
              <transition name="slide-fade">
                <div id="button-reply">
                  <editor
                    v-if="item.reply && item.showReply"
                    key="editor"
                    v-model="item.comment"
                    :uploadUrl="CKEditorUrl"
                  />
                  <p class="reply-by-api" v-if="item.reply && item.showReply" key="button-reply">
                    <span
                      >是否匿名:
                      <a-radio-group name="radioGroup" v-model="item.isAnonymous">
                        <a-radio :value="0"></a-radio>
                        <a-radio :value="1"></a-radio>
                      </a-radio-group>
                    </span>
                    <a-button size="small" type="primary" @click="SubCommentCommit(item)">发表</a-button>
                  </p>
                </div>
              </transition>
            </div>
          </transition>
        </div>
      </li>
    </ul>
    <a-pagination
      :total="total"
      :show-total="total => `${total} 条回复贴`"
      showQuickJumper
      showSizeChanger
      hideOnSinglePage
      v-model="pagination.pageNo"
      :pageSize.sync="pagination.pageSize"
      @change="ListPageChange"
      @showSizeChange="ListSizeChange"
      class="diy-pagination-bottom"
    />
  </div>
</template>

<script>
/* eslint-disable */
import Editor from '/xxx/CKEditor'
import $ from 'jquery'
export default {
  name: 'DIYComment',
  components: {
    Editor
  },
  props: {
    CKEditorUrl: {
      type: String,
      default: ''
    },
    needSubPanel: {
      type: Boolean,
      default: false
    },
    getCommentList: {
      type: Function,
      required: true
    },
    getSubCommentList: {
      type: Function
    },
    replaceFields: {
      type: Object,
      default: () => {}
    }
  },
  data() {
    return {
      pagination: {
        pageNo: 1,
        pageSize: 10
      },
      total: 0,
      commentListData: []
    }
  },
  computed: {
    userId() {
      return this.$getCurrentUser()?.id
    },
    diyReplace() {
      return Object.assign(
        {},
        {
          id: 'id',
          content: 'comment',
          creator: 'creator',
          time: 'createdDate'
        },
        this.replaceFields
      )
    }
  },
  created() {
    this.loadCommentList()
  },
  methods: {
    // 判断是否有回复
    isReply(content) {
      const newContent = this.removeInvalid(content)
      return newContent.indexOf('回复') === newContent.indexOf('>') + 1
    },

    // 过滤非法的评论
    removeInvalid(content) {
      return content.includes('<title>') ? content.replace(/<[^<>]+>/g, '') : content
    },

    // 取出回复子评论作者
    getReplyAuthor(content) {
      const newContent = content.replace(/<[^<>]+>/g, '')
      if (newContent.includes(':') && newContent.indexOf('回复') === 0) {
        return newContent.slice(2, newContent.indexOf(':')).trim()
      }
      return null
    },

    // 子评论内容处理
    removeSubContentInvalid(content) {
      let newContent = this.removeInvalid(content)
      if (newContent.includes(':') && this.isReply(newContent)) {
        newContent = newContent.slice(newContent.indexOf(':') + 1)
      }
      return newContent
    },

    // 处理作者的名字
    renameAuthor({ author, anonymous }) {
      if (anonymous) {
        return '匿名'
      } else {
        const name = this.$filterUserText(author)
        return name.length >= 2 ? name.slice(name.length - 2) : '匿名'
      }
    },

    // 子评论里回复
    SubCommentReply(parent, { author, isAnonymous }) {
      parent.reply = true
      this.$nextTick(() => {
        $('html, body').animate({ scrollTop: $('#button-reply').offset().top }, 800)
        parent.comment = `回复 ${isAnonymous ? '匿名' : this.$filterUserText(author)}`
      })
    },

    // 父评论页数大小改变
    ListSizeChange(current, size) {
      Object.assign(this.pagination, {
        pageNo: 1,
        pageSize: size
      })
      this.loadCommentList()
    },

    // 父评论页数切换
    ListPageChange(pageNo, pageSize) {
      Object.assign(this.pagination, {
        pageNo,
        pageSize
      })
      this.loadCommentList()
    },

    // 子评论翻页
    SubListPageChange(item, pageNo, pageSize) {
      Object.assign(item.pagination, {
        pageNo,
        pageSize
      })
      this.loadCommentSubList(item)
    },

    // 子评论获取列表
    loadCommentSubList(item) {
      let result
      if (typeof this.getSubCommentList === 'function') {
        result = this.getSubCommentList(item)
      }
      if ((typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
        result.then(r => {
          console.log(r)
          if (item.total !== r.totalCount) {
            item.total = r.totalCount
          }
          if (Array.isArray(r.data) && !r.data.length) {
            typeof item?.pagination?.pageNo === 'number' &&
              (item.pagination.pageNo = Math.max(item.pagination.pageNo - 1, 1))
            this.loadCommentSubList(item)
          }
          Array.isArray(r?.data) &&
            (item.children = r.data.map(item => {
              const datetime =
                typeof item[this.diyReplace.time] === 'string'
                  ? item[this.diyReplace.time].slice(0, item[this.diyReplace.time].lastIndexOf('.'))
                  : item[this.diyReplace.time]
              return {
                id: item[this.diyReplace.id],
                content: item[this.diyReplace.content],
                author: Number(item[this.diyReplace.creator]),
                datetime,
                isAnonymous: item.isAnonymous
              }
            }))
        })
      }
    },

    // 评论列表获取
    loadCommentList() {
      const result = this.getCommentList(this.pagination)
      if ((typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
        result.then(r => {
          if (this.total !== r.totalCount) {
            this.total = r.totalCount
          }
          if (Array.isArray(r.data) && !r.data.length) {
            this.pagination.pageNo = Math.max(this.pagination.pageNo - 1, 1)
            this.loadCommentList()
          }
          Array.isArray(r?.data) &&
            (this.commentListData = r.data.map(item => {
              const datetime =
                typeof item[this.diyReplace.time] === 'string'
                  ? item[this.diyReplace.time].slice(0, item[this.diyReplace.time].lastIndexOf('.'))
                  : item[this.diyReplace.time]
              return {
                id: item[this.diyReplace.id],
                content: item[this.diyReplace.content],
                showReply: false,
                author: Number(item[this.diyReplace.creator]),
                datetime,
                reply: false,
                comment: '',
                isAnonymous: 0,
                pagination: {
                  pageNo: 1,
                  pageSize: 10
                },
                total: 0,
                children: [],
                anonymous: item.isAnonymous
              }
            }))
        })
      }
    },

    // 评论删除
    deleteComment(id) {
      this.$emit('deletecomment', id)
    },

    // 子评论提交
    SubCommentCommit(item) {
      const param = {
        comment: item.comment,
        isAnonymous: item.isAnonymous,
        parentCommentId: item.id
      }
      const callback = (param, item) => {
        item.comment = ''
        item.isAnonymous = 0
        this.loadCommentSubList(item)
      }
      this.$emit('savecomment', param, 'sub', item, callback)
    },

    // 列表刷新
    refresh(bool = true) {
      bool &&
        (this.pagination = Object.assign(
          {},
          {
            pageNo: 1,
            pageSize: this.pagination.pageSize
          }
        ))
      this.loadCommentList()
    }
  }
}
</script>

<style lang="less" src="./style.less" />

# 4. 评论样式

  • less代码如下:
.comment-component {
    .comment-list-data {
        padding : 0;
        margin  : 20px auto;
        overflow: hidden auto;

        .list-item {
            display: flex;
            border : solid 1px #d9d9d9;

            &:not(:last-child) {
                border-bottom: none;
            }

            &:last-child {
                border-bottom: solid 1px #d9d9d9;
            }

            .left-icon {
                padding: 30px;

                .author-avatar {
                    background-color: lighten(#1890ff, 10%);
                    border-radius   : 50%;
                    font-size       : 26px;
                    color           : #fff;

                    span {
                        color: #fff;
                    }

                    padding : 20px 15px;
                }

                .author-name {
                    color     : #2d64b3;
                    cursor    : pointer;
                    text-align: center;
                    margin-top: -15px;
                }
            }

            .comment-content {
                flex          : 1;
                padding       : 20px 10px 0;
                position      : relative;
                display       : flex;
                flex-direction: column;

                img {
                    max-width: 100%;
                }

                .main-content {
                    flex: 1;
                }

                .tips {
                    text-align : right;
                    line-height: 30px;
                    height     : 30px;

                    span {
                        padding: 5px 6px;
                    }

                    .reply {
                        color : #2d64b3;
                        cursor: pointer;

                        &.show {
                            background-color: #f7f8fa;
                            border          : solid 1px #f0f1f2;
                            padding         : 5px 10px;
                        }

                        &.hide {
                            background-color: transparent;
                            border          : none;
                        }
                    }
                }

                .reply-list {
                    background-color: #f7f8fa;
                    margin-top      : -20px;
                    padding         : 20px;

                    .reply-item {
                        border-bottom: solid 1px #d9d9d9;
                        padding      : 10px 0 0;

                        &:hover {
                            .bottom-info .delete {
                                opacity: 1;
                            }
                        }

                        .main-info {
                            display: flex;

                            a {
                                cursor: default;
                            }

                            .anticon {
                                font-size: 16px;
                                padding  : 0 4px;
                            }
                        }

                        .bottom-info {
                            margin-top: -30px;
                            text-align: right;

                            .reply,
                            .delete {
                                cursor: pointer;
                            }

                            .delete {
                                opacity   : 0;
                                color     : #2d64b3;
                                transition: all 0.3s;
                            }

                            span {
                                padding: 0 4px;
                            }
                        }
                    }

                    .reply-button {
                        margin : 15px 0;
                        display: flex;

                        .ant-pagination {
                            flex: 1;
                        }
                    }

                    .cke_contents {
                        height    : auto !important;
                        min-height: 100px;
                    }

                    .reply-by-api {
                        display: flex;
                        margin : 10px auto 0;

                        span {
                            flex: 1;
                        }

                        .ant-btn {
                            text-align: right;
                        }
                    }
                }
            }
        }
    }

    .diy-pagination,
    .diy-pagination-bottom {
        text-align: right;

        .ant-pagination-item-link {
            border          : none;
            background-color: transparent;
        }

        .ant-pagination-next,
        .ant-pagination-prev {
            min-width: 0;
            width    : fit-content;
            padding  : 0 5px;
            margin   : 0;
        }

        .ant-pagination-item {
            background-color: transparent;
            border          : none;
            min-width       : 0;
            width           : fit-content;
            padding         : 0 5px;
            margin          : 0;
        }
    }

    .diy-pagination-bottom {
        text-align: left;
        margin    : 20px 0;
    }
}

.slide-fade-enter-active {
    transition: all .3s ease;
}

.slide-fade-leave-active {
    transition: all .5s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}

.slide-fade-enter,
.slide-fade-leave-to {
    transform: translateY(10px);
    opacity  : 0;
}

.slide-fade-editor-enter-active {
    transition: all .3s ease;
}

.slide-fade-editor-leave-active {
    transition: all .1s;
}

.slide-fade-editor-enter,
.slide-fade-editor-leave-to {
    transform: translateY(10px);
    opacity  : 0;
}

# 5. 运行结果

  • 运行结果如下图:

结语

以上只是针对原有评论的一部分改进(需要后端的配合),本网站是静态页面,故评论还是采取畅言的api,如有其它更好的实现方式,也欢迎各位评论区留言指教