Browse Source

添加评论组件 UserCommentCard.vue

reghao 1 week ago
parent
commit
10668b44de

+ 4 - 4
src/api/comment.js

@@ -11,11 +11,11 @@ export function publishComment(data) {
 }
 }
 
 
 // 获取评论
 // 获取评论
-export function getComment(videoId, pageNumber) {
-  return get(commentApi.videoCommentApi + '?videoId=' + videoId + '&pageNumber=' + pageNumber)
+export function getComment(queryInfo) {
+  return get(commentApi.videoCommentApi, queryInfo)
 }
 }
 
 
 // 获取评论的子评论
 // 获取评论的子评论
-export function getChildComment(commentId, pageNumber) {
-  return get(commentApi.videoChildCommentApi + '?commentId=' + commentId + '&pageNumber=' + pageNumber)
+export function getChildComment(queryInfo) {
+  return get(commentApi.videoChildCommentApi, queryInfo)
 }
 }

+ 382 - 80
src/components/card/UserCommentCard.vue

@@ -1,125 +1,427 @@
 <template>
 <template>
   <el-card class="comment-card shadow-box">
   <el-card class="comment-card shadow-box">
-    <div slot="header" class="comment-header">
-      <span>全部评论 <small>{{ totalSize }}</small></span>
-    </div>
     <div class="comment-container">
     <div class="comment-container">
-      <comment
-          v-model="dataList"
-          :user="currentUser"
-          :props="commentProps"
-          :before-submit="submit"
-          :before-like="like"
-          :before-delete="deleteComment"
-          :upload-img="uploadImg"
-      />
-      <div class="pagination-wrapper">
-        <el-pagination
-            :small="screenWidth <= 768"
-            hide-on-single-page
-            background
-            layout="prev, pager, next"
-            :page-size="pageSize"
-            :current-page="currentPage"
-            :total="totalSize"
-            @current-change="handleCurrentChange"
+      <div class="comment-header">
+        <span class="total-count">{{ totalSize }} 评论</span>
+        <div class="sort-options">
+          <span :class="{ active: sortType === 'hot' }" @click="handleSort('hot')">最热</span>
+          <el-divider direction="vertical" />
+          <span :class="{ active: sortType === 'time' }" @click="handleSort('time')">最新</span>
+        </div>
+      </div>
+
+      <div class="main-reply-box">
+        <el-avatar
+          :size="48"
+          :src="currentUser.avatar || defaultAvatar"
+          class="clickable-avatar"
+          @click.native="goToUserHome(currentUser.userId)"
         />
         />
+        <div class="reply-input-wrapper">
+          <el-input
+            v-model="newComment"
+            type="textarea"
+            :rows="3"
+            placeholder="发一条友善的评论吧"
+            resize="none"
+          />
+          <el-button type="primary" class="post-btn" @click="postMainComment">发表评论</el-button>
+        </div>
       </div>
       </div>
+
+      <div
+        v-infinite-scroll="loadMoreMainComments"
+        class="comment-list"
+        :infinite-scroll-disabled="mainScrollDisabled"
+        :infinite-scroll-distance="20"
+      >
+        <div v-for="item in dataList" :key="item.id" class="comment-item">
+          <div class="comment-root">
+            <el-avatar
+              :size="48"
+              :src="item.user.avatar || defaultAvatar"
+              class="clickable-avatar"
+              @click.native="goToUserHome(item.user.userId || item.user.id)"
+            />
+            <div class="comment-content">
+              <div class="user-info">
+                <span class="user-name clickable-link" @click="goToUserHome(item.user.userId || item.user.id)">
+                  {{ item.user.name }}
+                </span>
+              </div>
+              <p class="text">{{ item.content }}</p>
+              <div class="comment-footer">
+                <span class="time">{{ formatDate(item.createAt) }}</span>
+                <span class="action" @click="showReplyInput(item, item)">回复</span>
+              </div>
+
+              <div v-if="item.total > 0" class="reply-list">
+                <div v-for="reply in item.children.slice(0, 3)" :key="reply.id" class="reply-item">
+                  <el-avatar
+                    :size="24"
+                    :src="reply.user.avatar || defaultAvatar"
+                    class="reply-avatar clickable-avatar"
+                    @click.native="goToUserHome(reply.user.userId || reply.user.id)"
+                  />
+                  <div class="reply-right">
+                    <span class="user-name clickable-link" @click="goToUserHome(reply.user.userId || reply.user.id)">
+                      {{ reply.user.name }}
+                    </span>
+                    <span v-if="reply.targetId && reply.targetId !== item.id">
+                      回复
+                      <span class="target-name clickable-link" @click="goToUserHome(reply.targetUserId)">
+                        @{{ reply.targetUsername }}
+                      </span> :
+                    </span>
+                    <span class="reply-content">{{ reply.content }}</span>
+                    <span class="sub-action" @click="showReplyInput(item, reply)">回复</span>
+                  </div>
+                </div>
+
+                <div v-if="item.total > 3" class="view-more-bar">
+                  共 {{ item.total }} 条回复,
+                  <span class="click-more" @click="openReplyDialog(item)">点击查看更多</span>
+                </div>
+              </div>
+
+              <div v-if="activeRootId === item.id && !dialogVisible" class="inner-reply-box">
+                <el-input v-model="replyText" size="small" :placeholder="replyPlaceholder" />
+                <el-button type="primary" size="small" @click="postReply">发布</el-button>
+                <el-button type="text" size="small" @click="activeRootId = null">取消</el-button>
+              </div>
+            </div>
+          </div>
+          <el-divider />
+        </div>
+
+        <p v-if="loading" class="list-status">加载中...</p>
+        <p v-if="noMoreMain && dataList.length > 0" class="list-status">没有更多评论了</p>
+        <el-empty v-if="!loading && dataList.length === 0" description="暂无评论,快来抢沙发" />
+      </div>
+
+      <el-dialog :visible.sync="dialogVisible" title="评论详情" width="600px" :append-to-body="true" custom-class="reply-dialog" @close="activeRootId = null">
+        <div v-if="currentParent" class="dialog-scroll-area">
+          <div class="parent-info">
+            <el-avatar
+              :size="40"
+              :src="currentParent.user.avatar || defaultAvatar"
+              class="clickable-avatar"
+              @click.native="goToUserHome(currentParent.user.userId || currentParent.user.id)"
+            />
+            <div class="parent-text">
+              <div class="user-name clickable-link" @click="goToUserHome(currentParent.user.userId || currentParent.user.id)">
+                {{ currentParent.user.name }}
+              </div>
+              <div class="content">{{ currentParent.content }}</div>
+              <div class="reply-footer">
+                <span class="time">{{ formatDate(currentParent.createAt) }}</span>
+                <span class="action" @click="showReplyInput(currentParent, currentParent)">回复</span>
+              </div>
+            </div>
+          </div>
+
+          <el-divider content-position="left">全部回复 ({{ currentParent.total }})</el-divider>
+
+          <div class="dialog-reply-list">
+            <div v-for="sub in dialogReplies" :key="sub.id" class="dialog-reply-item">
+              <el-avatar
+                :size="32"
+                :src="sub.user.avatar || defaultAvatar"
+                class="clickable-avatar"
+                @click.native="goToUserHome(sub.user.userId || sub.user.id)"
+              />
+              <div class="reply-body">
+                <span class="user-name clickable-link" @click="goToUserHome(sub.user.userId || sub.user.id)">
+                  {{ sub.user.name }}
+                </span>
+                <span v-if="sub.targetId && sub.targetId !== currentParent.id">
+                  回复
+                  <span class="target-name clickable-link" @click="goToUserHome(sub.targetUserId)">
+                    @{{ sub.targetUsername }}
+                  </span>
+                </span>: {{ sub.content }}
+                <div class="reply-footer">
+                  {{ formatDate(sub.createAt) }}
+                  <span class="action" @click="showReplyInput(currentParent, sub)">回复</span>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <div class="dialog-pagination">
+            <el-pagination
+              small
+              background
+              layout="prev, pager, next"
+              :total="currentParent.total"
+              :page-size="childPageSize"
+              :current-page.sync="childCurrentPage"
+              @current-change="handleChildPageChange"
+            />
+          </div>
+        </div>
+
+        <div v-if="dialogVisible && activeRootId === currentParent.id" class="dialog-inner-reply">
+          <el-divider />
+          <div class="inner-reply-box">
+            <el-input v-model="replyText" size="small" :placeholder="replyPlaceholder" />
+            <el-button type="primary" size="small" @click="postReply">发布</el-button>
+            <el-button type="text" size="small" @click="activeRootId = null">取消</el-button>
+          </div>
+        </div>
+      </el-dialog>
     </div>
     </div>
   </el-card>
   </el-card>
 </template>
 </template>
 
 
 <script>
 <script>
-import comment from '@/components/comment'
-import { publishComment, getComment } from '@/api/comment'
+import { getChildComment, getComment, publishComment } from '@/api/comment'
 
 
 export default {
 export default {
   name: 'UserCommentCard',
   name: 'UserCommentCard',
-  components: { comment },
   props: {
   props: {
     videoId: { type: String, required: true },
     videoId: { type: String, required: true },
-    currentUser: { type: Object, required: true },
-    screenWidth: { type: Number, default: 1200 }
+    currentUser: { type: Object, default: () => ({ userId: -1, name: '游客', avatar: '' }) }
   },
   },
   data() {
   data() {
     return {
     return {
-      currentPage: 1,
-      pageSize: 20,
-      totalSize: 0,
       dataList: [],
       dataList: [],
-      commentProps: {
-        id: 'commentId',
-        content: 'content',
-        imgSrc: 'imageUrl',
-        children: 'children',
-        likes: 'likes',
-        liked: 'liked',
-        reply: 'reply',
-        createAt: 'createAt',
-        total: 'total',
-        user: 'user'
-      }
+      totalSize: 0,
+      currentPage: 0,
+      pageSize: 10,
+      loading: false,
+      noMoreMain: false,
+      sortType: 'hot',
+      dialogVisible: false,
+      currentParent: null,
+      dialogReplies: [],
+      childCurrentPage: 1,
+      childPageSize: 10,
+      newComment: '',
+      replyText: '',
+      activeRootId: null,
+      replyTarget: null,
+      replyPlaceholder: '',
+      defaultAvatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
+    }
+  },
+  computed: {
+    mainScrollDisabled() {
+      return this.loading || this.noMoreMain
     }
     }
   },
   },
   watch: {
   watch: {
     videoId: {
     videoId: {
       immediate: true,
       immediate: true,
-      handler(newVal) {
-        if (newVal) this.getCommentWrapper(1)
+      handler(val) {
+        if (val) this.initCommentList()
       }
       }
     }
     }
   },
   },
   methods: {
   methods: {
-    handleCurrentChange(page) {
-      this.currentPage = page
-      this.getCommentWrapper(page)
-      window.scrollTo({ top: document.querySelector('.comment-card').offsetTop - 100, behavior: 'smooth' })
+    goToUserHome(userId) {
+      this.$message.info('goto userhome ' + userId)
     },
     },
-    getCommentWrapper(pageNumber) {
-      getComment(this.videoId, pageNumber).then(resp => {
+    initCommentList() {
+      this.dataList = []
+      this.currentPage = 0
+      this.noMoreMain = false
+      this.loadMoreMainComments()
+    },
+    async loadMoreMainComments() {
+      if (this.loading || this.noMoreMain) return
+      this.loading = true
+      this.currentPage++
+      try {
+        const resp = await getComment({
+          videoId: this.videoId,
+          pn: this.currentPage,
+          pageSize: this.pageSize,
+          sortBy: this.sortType
+        })
         if (resp.code === 0) {
         if (resp.code === 0) {
-          this.dataList = resp.data.list
+          const newList = resp.data.list || []
+          this.dataList = [...this.dataList, ...newList]
           this.totalSize = resp.data.totalSize
           this.totalSize = resp.data.totalSize
-        } else {
-          this.$message.error(resp.msg)
+          if (newList.length < this.pageSize || this.dataList.length >= this.totalSize) {
+            this.noMoreMain = true
+          }
         }
         }
-      }).catch(err => this.$message.error(err.message))
+      } catch (e) {
+        console.error('加载失败', e)
+      } finally {
+        this.loading = false
+      }
     },
     },
-    async submit(newComment, parent, add) {
-      // 模拟延迟
-      await new Promise(resolve => setTimeout(resolve, 300))
-
-      const payload = { newComment, parent, postId: this.videoId }
-      publishComment(payload).then(resp => {
+    handleSort(type) {
+      this.sortType = type
+      this.initCommentList()
+    },
+    showReplyInput(root, target) {
+      this.activeRootId = root.id
+      this.replyTarget = target
+      this.replyPlaceholder = `回复 @${target.user.name} :`
+      this.replyText = ''
+    },
+    postMainComment() {
+      if (!this.newComment.trim()) return
+      publishComment({
+        videoId: this.videoId,
+        content: this.newComment,
+        parentId: 0,
+        targetId: 0
+      }).then(resp => {
         if (resp.code === 0) {
         if (resp.code === 0) {
-          add(Object.assign(newComment, { postId: this.videoId }))
-          if (parent === null) this.totalSize += 1
-          this.$notify.success({ message: '评论已发布' })
-        } else {
-          this.$notify.warning({ message: '评论发布失败' })
+          this.$message.success('评论成功')
+          this.newComment = ''
+          this.initCommentList()
         }
         }
       })
       })
     },
     },
-    async like(comment) {
-      console.log('Like comment:', comment)
-      // 这里可以调用点赞接口
+    postReply() {
+      if (!this.replyText.trim()) return
+      publishComment({
+        videoId: this.videoId,
+        content: this.replyText,
+        parentId: this.activeRootId,
+        targetId: this.replyTarget.id,
+        targetUsername: this.replyTarget.user.name
+      }).then(resp => {
+        if (resp.code === 0) {
+          this.$message.success('回复成功')
+          this.replyText = ''
+          // 不再直接置 null,如果想让用户连续回复可以保持,但通常交互是关闭
+          this.activeRootId = null
+          if (this.dialogVisible) {
+            this.fetchDialogReplies(this.currentParent.id, this.childCurrentPage)
+          } else {
+            // 如果在主列表回复,可能需要刷新子列表或重新获取
+            this.initCommentList()
+          }
+        }
+      })
     },
     },
-    async deleteComment(comment, parent) {
-      console.log('Delete comment:', comment)
-      // 这里可以调用删除接口
+    async openReplyDialog(parentItem) {
+      this.currentParent = parentItem
+      this.childCurrentPage = 1
+      this.dialogVisible = true
+      this.activeRootId = null // 重置输入框状态
+      this.fetchDialogReplies(parentItem.id, 1)
     },
     },
-    async uploadImg({ file, callback }) {
-      const reader = new FileReader()
-      reader.readAsDataURL(file)
-      reader.onload = () => callback(reader.result)
+    fetchDialogReplies(commentId, pageNumber) {
+      getChildComment({ commentId, pn: pageNumber }).then(resp => {
+        if (resp.code === 0) {
+          this.dialogReplies = resp.data.list
+        }
+      })
+    },
+    handleChildPageChange(page) {
+      this.fetchDialogReplies(this.currentParent.id, page)
+    },
+    formatDate(ts) {
+      if (!ts) return ''
+      const date = new Date(ts)
+      return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
     }
     }
   }
   }
 }
 }
 </script>
 </script>
 
 
-<style scoped>
-.comment-card { margin-bottom: 40px; }
-.comment-header { font-weight: 600; font-size: 18px; }
-.comment-header small { font-weight: normal; color: #909399; margin-left: 8px; }
-.pagination-wrapper { margin-top: 30px; display: flex; justify-content: center; }
+<style scoped lang="scss">
+.comment-card { margin-top: 20px; border: none; }
+.comment-container { color: #18191c; }
+
+.clickable-avatar {
+  cursor: pointer;
+  transition: opacity 0.2s;
+  &:hover { opacity: 0.8; }
+}
+.clickable-link {
+  cursor: pointer;
+  &:hover { color: #00aeec !important; }
+}
+
+.comment-header {
+  margin-bottom: 24px; display: flex; align-items: center;
+  .total-count { font-size: 18px; font-weight: 500; margin-right: 20px; }
+  .sort-options span { font-size: 14px; color: #9499a0; cursor: pointer; &.active { color: #18191c; font-weight: bold; } }
+}
+
+.main-reply-box {
+  display: flex; gap: 15px; margin-bottom: 30px;
+  .reply-input-wrapper { flex: 1; .post-btn { margin-top: 10px; float: right; } }
+}
+
+.comment-list { min-height: 200px; }
+.list-status { text-align: center; color: #9499a0; font-size: 13px; padding: 20px 0; }
+.comment-root {
+  display: flex; gap: 15px;
+  .comment-content {
+    flex: 1;
+    .user-name { font-size: 13px; font-weight: bold; color: #61666d; }
+    .text { font-size: 15px; line-height: 1.6; margin: 8px 0; }
+    .comment-footer {
+      font-size: 13px; color: #9499a0; display: flex; gap: 20px; margin-bottom: 8px;
+      .action { cursor: pointer; &:hover { color: #00aeec; } }
+    }
+  }
+}
+
+.reply-list {
+  background: #f6f7f8; border-radius: 6px; padding: 12px;
+  .reply-item {
+    display: flex;
+    gap: 8px;
+    font-size: 14px; line-height: 22px; margin-bottom: 8px;
+    .reply-avatar { flex-shrink: 0; margin-top: 2px; }
+    .reply-right { flex: 1; }
+    .user-name { color: #61666d; font-weight: bold; margin-right: 5px; }
+    .target-name { color: #00aeec; font-weight: 500; margin: 0 4px; }
+    .sub-action {
+      margin-left: 10px; font-size: 12px; color: #9499a0; cursor: pointer;
+      display: none;
+    }
+    &:hover .sub-action { display: inline-block; }
+  }
+  .view-more-bar {
+    font-size: 13px; color: #9499a0; margin-top: 8px; padding-left: 32px;
+    .click-more { color: #00aeec; cursor: pointer; font-weight: 500; }
+  }
+}
+
+.inner-reply-box {
+  margin-top: 10px; display: flex; gap: 8px;
+  background: #fff; padding: 10px; border: 1px solid #e3e5e7; border-radius: 6px;
+}
+
+.dialog-scroll-area {
+  max-height: 50vh; overflow-y: auto; padding-right: 8px;
+  .parent-info {
+    display: flex; gap: 12px;
+    .parent-text {
+      flex: 1;
+      .user-name { font-weight: bold; color: #61666d; margin-bottom: 4px; }
+      .content { font-size: 15px; line-height: 1.6; margin-bottom: 8px; }
+      .reply-footer { font-size: 12px; color: #9499a0; .action { margin-left: 15px; cursor: pointer; &:hover { color: #00aeec; } } }
+    }
+  }
+  .dialog-reply-item {
+    display: flex; gap: 10px; margin-bottom: 15px;
+    .reply-body {
+      flex: 1; font-size: 14px;
+      .user-name { font-weight: bold; color: #61666d; }
+      .target-name { color: #00aeec; margin: 0 4px; }
+      .reply-footer { font-size: 12px; color: #9499a0; margin-top: 5px;
+        .action { margin-left: 10px; cursor: pointer; &:hover { color: #00aeec; } }
+      }
+    }
+  }
+  .dialog-pagination { display: flex; justify-content: center; margin-top: 20px; }
+}
+
+/* 弹窗专用回复框样式 */
+.dialog-inner-reply {
+  padding-top: 0;
+  .el-divider--horizontal { margin: 12px 0; }
+}
 </style>
 </style>

+ 0 - 449
src/components/comment/components/CommentForm.vue

@@ -1,449 +0,0 @@
-<template>
-  <div :class="`${className} comment-form`">
-    <div class="avatar-box">
-      <slot />
-    </div>
-
-    <div class="form-box">
-      <div class="rich-input" :class="{ focus: focus || value }">
-        <div class="grow-wrap" :data-replicated-value="value">
-          <textarea
-            ref="input"
-            rows="1"
-            :value="value"
-            :placeholder="placeholder"
-            @input="(e) => (value = e.target.value)"
-            @focus="focus = true"
-            @blur="handleBlur"
-            @mousedown="closeEmojiSelector"
-          />
-        </div>
-        <div v-show="imgSrc" ref="image-preview-box" class="image-preview-box">
-          <div
-            v-show="imgSrc"
-            :style="`background-image: url(${imgSrc})`"
-            class="image"
-          />
-          <div class="clean-btn" @mousedown.prevent="removeImg">
-            <svg
-              aria-hidden="true"
-              width="15"
-              height="15"
-              viewBox="0 0 21 21"
-              class="icon close-icon"
-            >
-              <g fill="none" fill-rule="evenodd" transform="translate(1 1)">
-                <circle
-                  cx="9.5"
-                  cy="9.5"
-                  r="9.5"
-                  fill="#000"
-                  stroke="#FFF"
-                  opacity=".5"
-                />
-                <path
-                  fill="#FFF"
-                  d="M13.743 5.964L10.207 9.5l3.536 3.536-.707.707L9.5 10.207l-3.536 3.536-.707-.707L8.793 9.5 5.257 5.964l.707-.707L9.5 8.793l3.536-3.536z"
-                />
-              </g>
-            </svg>
-          </div>
-        </div>
-      </div>
-      <div
-        v-show="focus || value || imgSrc"
-        class="option-box"
-        @mousedown.prevent="closeEmojiSelector($refs.input.focus())"
-      >
-        <div
-          class="emoji emoji-btn"
-          @mousedown.prevent.stop="openEmojiSelector"
-        >
-          <div class="emoji-box">
-            <div class="icon" />
-            <span>表情</span>
-          </div>
-          <EmojiSelector
-            v-show="showEmojiSelector"
-            @choose="(v) => (value += v)"
-          />
-        </div>
-        <div class="image-btn" @mousedown.prevent="triggerUpload">
-          <svg
-            aria-hidden="true"
-            width="22"
-            height="22"
-            viewBox="0 0 22 22"
-            class="icon image-icon"
-          >
-            <g fill="none" fill-rule="evenodd">
-              <path d="M1 1h20v20H1z" />
-              <g transform="translate(2 3)">
-                <path
-                  stroke="#027FFF"
-                  stroke-width=".9"
-                  d="M2.28.667h13.44c1.075 0 1.947.871 1.947 1.946v10.774a1.947 1.947 0 0 1-1.947 1.946H2.28a1.947 1.947 0 0 1-1.947-1.946V2.613c0-1.075.872-1.946 1.947-1.946zM.333 12.499L5 8l9.01 7.333m-6.343-4.842L10.333 8l7.136 5.914"
-                />
-                <circle cx="13.5" cy="4.5" r="1.5" fill="#027FFF" />
-              </g>
-            </g>
-          </svg>
-          <span>图片</span>
-          <input
-            ref="upload"
-            class="upload-file"
-            type="file"
-            @change="handleChange"
-            @click="onUpload = true"
-          >
-        </div>
-        <slot name="submitBtn">
-          <button
-            class="submit-btn"
-            :disabled="!value && !imgSrc"
-            @click.stop="handleSubmit"
-          >
-            评论
-          </button>
-        </slot>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-import EmojiSelector from './EmojiSelector'
-export default {
-  name: 'CommentForm',
-  components: { EmojiSelector },
-  props: {
-    placeholder: {
-      type: String,
-      default: '输入评论...'
-    },
-    id: {
-      type: [String, Number],
-      default: 'comment-root'
-    },
-    comment: {
-      type: Object,
-      default: () => {}
-    },
-    parent: {
-      type: Object,
-      default: () => {}
-    },
-    uploadImg: {
-      type: Function,
-      default: null
-    }
-  },
-  data() {
-    return {
-      focus: false, // * 聚焦状态
-      value: '', // * 输入框值
-      imgSrc: '', // * 粘贴的图片src
-      showEmojiSelector: false // * 表情选择框状态
-    }
-  },
-  computed: {
-    // 是否为顶部评论表单
-    isRoot() {
-      return this.id === 'comment-root'
-    },
-    // 是否为回复中的表单
-    isSub() {
-      return this.id.split('-').length === 3
-    },
-    className() {
-      return this.isRoot
-        ? 'comment-root'
-        : this.isSub
-          ? 'reply sub-reply'
-          : 'reply'
-    }
-  },
-  mounted() {
-    const richInput = this.$refs.input
-    !this.isRoot && richInput.focus()
-
-    richInput.addEventListener('paste', this.handlePaste)
-    this.$once('hook:beforeDestroy', () =>
-      richInput.removeEventListener('paste', this.handlePaste)
-    )
-  },
-  methods: {
-    // * 选择要上传的图片
-    handleChange(e) {
-      const files = e.target.files
-      if (!(files && files[0])) return
-      this.beforeSetImg(files[0])
-    },
-    // * 处理图片
-    async beforeSetImg(file) {
-      if (!/^image/.test(file.type)) {
-        throw new Error("file type must contain 'image'.")
-      }
-
-      if (typeof this.uploadImg === 'function') {
-        const callback = (src) => {
-          this.imgSrc = src
-        }
-        await this.uploadImg({ file, callback })
-        return
-      }
-
-      const reader = new FileReader()
-      reader.readAsDataURL(file)
-      reader.onload = () => {
-        this.imgSrc = reader.result
-      }
-      reader.onerror = () => {
-        throw new Error(
-          `read file errored, the error code is ${reader.error.code}.`
-        )
-      }
-    },
-    // * 点击图片触发上传
-    triggerUpload() {
-      this.$refs.upload.click()
-    },
-    // * 点击图片上的删除按钮
-    removeImg() {
-      this.imgSrc = ''
-      this.closeEmojiSelector()
-    },
-    // * textarea blur 事件
-    handleBlur(e) {
-      this.showEmojiSelector = false
-
-      if (this.onUpload) {
-        this.$nextTick(() => {
-          this.onUpload = false
-        })
-        return
-      }
-
-      if (this.value || this.imgSrc) return
-
-      this.focus = false
-
-      if (!this.isRoot) {
-        this.close()
-      }
-    },
-    // * textarea paste 事件
-    handlePaste(e) {
-      const file = e.clipboardData.files[0]
-      if (file) {
-        // 只处理复制图片
-        this.beforeSetImg(file)
-        e.preventDefault()
-      }
-    },
-    // * 点击评论
-    handleSubmit() {
-      if (!this.value.trim() && !this.imgSrc) return
-      const user = (this.comment && this.comment.user) || null
-
-      const data = {
-        id: this.id,
-        content: this.value,
-        imgSrc: this.imgSrc,
-        reply: (this.isSub && JSON.parse(JSON.stringify(user))) || null,
-        createAt: new Date().getTime(),
-        likes: 0,
-        callback: () => {
-          this.isRoot ? this.reset() : this.close()
-        }
-      }
-
-      if (!this.isSub) {
-        data.children = []
-      }
-
-      this.$emit('form-submit', { newComment: data, parent: this.parent })
-    },
-    // * 重置组件状态
-    reset() {
-      this.value = ''
-      this.imgSrc = ''
-      this.$refs.input.blur()
-    },
-    // * 销毁组件
-    close() {
-      this.$emit('form-delete', this.id)
-    },
-    // * 选择表情
-    openEmojiSelector() {
-      this.showEmojiSelector = !this.showEmojiSelector
-
-      if (document.activeElement === document.body) {
-        this.$refs.input.focus()
-      }
-      if (this.showEmojiSelector) {
-        // 移动光标到末尾
-        const input = this.$refs.input
-        input.selectionStart = input.selectionEnd = this.value.length
-      }
-    },
-    // * 关闭选择表情组件
-    closeEmojiSelector() {
-      if (this.showEmojiSelector) {
-        this.showEmojiSelector = false
-      }
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.comment-form {
-  max-width: 100%;
-  padding: 0.8rem 1.0664rem;
-  display: flex;
-  background-color: #fafbfc;
-  border-radius: 3px;
-  .avatar-box {
-    flex: 0 0 auto;
-    img {
-      margin: 0 0.8rem 0 0;
-    }
-  }
-  .form-box {
-    flex: 1 1 auto;
-    .rich-input {
-      border-radius: 3px;
-      border: 1px solid #f1f1f1;
-      background-color: #fff;
-      overflow: hidden;
-      &.focus {
-        border-color: #007fff;
-      }
-      .grow-wrap {
-        display: grid;
-        &::after {
-          content: attr(data-replicated-value) ' ';
-          white-space: pre-wrap;
-          visibility: hidden;
-        }
-        textarea {
-          outline: none;
-          border: none;
-          resize: none;
-          touch-action: none;
-          overflow: hidden;
-          &::placeholder {
-            color: #c2c2c2;
-          }
-        }
-        & > textarea,
-        &::after {
-          font: inherit;
-          grid-area: 1 / 1 / 2 / 2;
-          padding: 0.48rem 0.8rem;
-          min-height: 1.04rem;
-          line-height: 1.7;
-          font-size: 0.8664rem;
-          color: #17181a;
-          box-sizing: border-box;
-          word-break: break-all;
-        }
-      }
-
-      .image-preview-box {
-        display: inline-block;
-        position: relative;
-        margin: 0 0.8rem 0.4rem;
-        .image {
-          width: 5.3336rem;
-          height: 5.3336rem;
-          background-repeat: no-repeat;
-          background-size: cover;
-          background-position: 50%;
-        }
-        .clean-btn {
-          position: absolute;
-          top: 0.15rem;
-          right: 0.2rem;
-          cursor: pointer;
-        }
-      }
-    }
-    .option-box {
-      margin-top: 0.52rem;
-      display: flex;
-      align-items: center;
-      color: #027fff;
-      font-size: 0.8664rem;
-      .emoji {
-        position: relative;
-        .emoji-box {
-          display: flex;
-          align-items: center;
-          cursor: pointer;
-          .icon {
-            width: 1.2rem;
-            height: 1.2rem;
-            background-repeat: no-repeat;
-            background-size: cover;
-            background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMiIgaGVpZ2h0PSIyMiIgdmlld0JveD0iMCAwIDIyIDIyIj4KICAgIDxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPHBhdGggZD0iTTEgMWgyMHYyMEgxeiIvPgogICAgICAgIDxwYXRoIGZpbGw9IiMwMjdGRkYiIGZpbGwtcnVsZT0ibm9uemVybyIgZD0iTTExIDE4LjQzOGE3LjQzOCA3LjQzOCAwIDEgMCAwLTE0Ljg3NiA3LjQzOCA3LjQzOCAwIDAgMCAwIDE0Ljg3NnptMCAxLjA2MmE4LjUgOC41IDAgMSAxIDAtMTcgOC41IDguNSAwIDAgMSAwIDE3ek03LjgxMiA5LjkzN2ExLjA2MiAxLjA2MiAwIDEgMCAwLTIuMTI0IDEuMDYyIDEuMDYyIDAgMCAwIDAgMi4xMjV6bTYuMzc1IDBhMS4wNjMgMS4wNjMgMCAxIDAgMC0yLjEyNSAxLjA2MyAxLjA2MyAwIDAgMCAwIDIuMTI1ek0xMSAxNi4yMzJhMy4yNyAzLjI3IDAgMCAwIDMuMjctMy4yN0g3LjczYTMuMjcgMy4yNyAwIDAgMCAzLjI3IDMuMjd6Ii8+CiAgICA8L2c+Cjwvc3ZnPgo=');
-          }
-          &:hover {
-            opacity: 0.8;
-          }
-        }
-      }
-      .image-btn {
-        flex: 0 0 auto;
-        display: flex;
-        align-items: center;
-        margin-left: 20px;
-        cursor: pointer;
-        .icon {
-          margin-right: 0.2664rem;
-          width: 1.2rem;
-          height: 1.2rem;
-        }
-        &:hover {
-          opacity: 0.8;
-        }
-      }
-      .upload-file {
-        display: none;
-      }
-      .submit-btn {
-        flex: 0 0 auto;
-        margin-left: auto;
-        padding: 0.4rem 1.04rem;
-        font-size: 1rem;
-        color: #fff;
-        background-color: #027fff;
-        border-radius: 2px;
-        outline: none;
-        border: none;
-        cursor: pointer;
-        transition: all 0.3s;
-        &:hover {
-          background-color: #0371df;
-        }
-        &:disabled {
-          opacity: 0.4;
-          cursor: default;
-        }
-      }
-    }
-  }
-  &.reply {
-    margin-top: 0.8664rem;
-    padding: 0.8rem;
-    &.sub-reply {
-      background-color: #fff;
-      border: 1px solid #f1f1f2;
-    }
-    .avatar-box {
-      display: none;
-    }
-  }
-}
-</style>

+ 0 - 301
src/components/comment/components/CommentItem.vue

@@ -1,301 +0,0 @@
-<template>
-  <div class="comment-item" :class="{ 'sub-comment-item': isSubComment }">
-    <div class="comment">
-      <!-- 评论或回复人头像 -->
-      <img
-        class="avatar"
-        :src="comment.user.avatar || ''"
-        @error="(e) => e.target.classList.add('error')"
-      >
-      <div class="content-box">
-        <!-- 评论或回复人具体信息 -->
-        <div class="meta-box">
-          <slot name="userMeta">
-            <div class="user-popover-box">
-              <router-link target="_blank" :to="`/user/` + comment.user.userId">
-                <span v-if="comment.user">{{
-                  comment.user.name +
-                    (comment.user.author === true ? '(作者)' : '')
-                }}</span>
-              </router-link>
-            </div>
-          </slot>
-        </div>
-
-        <!-- 评论或回复内容 -->
-        <div class="content">
-          <span
-            v-if="comment.reply"
-            class="reply"
-          >回复
-            <span class="reply-target" :title="comment.reply.email">{{
-              comment.reply.name + ':'
-            }}</span>
-          </span>
-          {{ comment.content }}
-          <div v-if="comment.imgSrc" class="img-box">
-            <img
-              :src="comment.imgSrc || ''"
-              @error="(e) => e.target.classList.add('error')"
-            >
-          </div>
-        </div>
-
-        <!-- 评论或回复时间及操作 -->
-        <div class="reply-stat">
-          <time
-            :title="formatTime(comment.createAt, true)"
-            :datetime="comment.createAt"
-          >{{ formatTime(comment.createAt) }}</time>
-          <div
-            v-if="comment.user.author === true"
-            class="delete"
-            @click.stop="$emit('comment-delete', { id, comment, parent })"
-          >
-            <span>·</span>删除
-          </div>
-          <div class="action-box">
-            <div
-              class="like-action action"
-              :class="{ active: comment.liked }"
-              @click.stop="$emit('comment-like', { id, comment })"
-            >
-              <svg
-                aria-hidden="true"
-                viewBox="0 0 20 20"
-                class="icon like-icon"
-              >
-                <g fill="none" fill-rule="evenodd">
-                  <path d="M0 0h20v20H0z" />
-                  <path
-                    :stroke="comment.liked ? '#37C700' : '#8A93A0'"
-                    stroke-linejoin="round"
-                    :fill="comment.liked ? '#37c700' : 'none'"
-                    d="M4.58 8.25V17h-1.4C2.53 17 2 16.382 2 15.624V9.735c0-.79.552-1.485 1.18-1.485h1.4zM11.322 2c1.011.019 1.614.833 1.823 1.235.382.735.392 1.946.13 2.724-.236.704-.785 1.629-.785 1.629h4.11c.434 0 .838.206 1.107.563.273.365.363.84.24 1.272l-1.86 6.513A1.425 1.425 0 0 1 14.724 17H6.645V7.898C8.502 7.51 9.643 4.59 9.852 3.249A1.47 1.47 0 0 1 11.322 2z"
-                  />
-                </g>
-              </svg>
-              <span v-show="comment.likes" class="action-title">{{
-                comment.likes
-              }}</span>
-            </div>
-            <div
-              class="comment-action action"
-              @mousedown.prevent="$emit('comment-reply', id)"
-              @click.prevent
-            >
-              <svg
-                aria-hidden="true"
-                viewBox="0 0 20 20"
-                class="icon comment-icon"
-              >
-                <g fill="none" fill-rule="evenodd">
-                  <path d="M0 0h20v20H0z" />
-                  <path
-                    stroke="#8A93A0"
-                    stroke-linejoin="round"
-                    d="M10 17c-4.142 0-7.5-2.91-7.5-6.5S5.858 4 10 4c4.142 0 7.5 2.91 7.5 6.5 0 1.416-.522 2.726-1.41 3.794-.129.156.41 3.206.41 3.206l-3.265-1.134c-.998.369-2.077.634-3.235.634z"
-                  />
-                </g>
-              </svg>
-              <span class="action-title">回复</span>
-            </div>
-          </div>
-        </div>
-
-        <!-- 评论表单组件 -->
-        <slot :id="id" />
-
-        <!-- 回复列表 -->
-        <slot name="subList" :parentId="id" />
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'CommentItem',
-  props: {
-    comment: {
-      type: Object,
-      default: () => {},
-      required: true
-    },
-    id: {
-      type: [String, Number],
-      required: true
-    },
-    parent: {
-      type: Object,
-      default: () => {}
-    },
-    user: {
-      type: Object,
-      default: () => {}
-    }
-  },
-  computed: {
-    isSubComment() {
-      return this.id.split('-').length === 3
-    }
-  },
-  methods: {
-    formatTime(time, local = false) {
-      const d = new Date(time)
-
-      if (local) {
-        return d.toString()
-      }
-
-      const now = Date.now()
-      const diff = (now - d) / 1000
-
-      switch (true) {
-        case diff < 30:
-          return '刚刚'
-        case diff < 3600:
-          return Math.ceil(diff / 60) + '分钟前'
-        case diff < 3600 * 24:
-          return Math.ceil(diff / 3600) + '小时前'
-        case diff < 3600 * 24 * 30:
-          return Math.floor(diff / 3600 / 24) + '天前'
-        case diff < 3600 * 24 * 365:
-          return Math.floor(diff / 3600 / 24 / 30) + '月前'
-        default:
-          return Math.floor(diff / 3600 / 24 / 365) + '年前'
-      }
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.comment-item {
-  margin-bottom: 1.0664rem;
-  &:not(:last-child) {
-    .content-box {
-      border-bottom: 1px solid #f1f1f1;
-    }
-  }
-  &:hover {
-    .comment .reply-stat .delete {
-      visibility: visible;
-    }
-  }
-  .comment {
-    display: flex;
-    .content-box {
-      margin-left: 0.6664rem;
-      flex: 1 1 auto;
-      &.focus {
-        padding-bottom: 0.4rem;
-      }
-      .meta-box {
-        display: flex;
-        align-items: center;
-        font-size: 0.8664rem;
-        line-height: 1.25;
-        white-space: nowrap;
-        .user-popover-box {
-          cursor: pointer;
-        }
-      }
-      .content {
-        margin-top: 0.44rem;
-        font-size: 0.8664rem;
-        line-height: 1.4664rem;
-        white-space: pre-line;
-        word-break: break-all;
-        color: #505050;
-        overflow: hidden;
-        .img-box {
-          margin-top: 0.5rem;
-          img {
-            max-width: 100%;
-            max-height: 20rem;
-            object-fit: cover;
-          }
-        }
-        .reply {
-          vertical-align: top;
-        }
-        .reply-target {
-          cursor: pointer;
-          color: #406599;
-        }
-      }
-    }
-    .reply-stat {
-      display: flex;
-      margin-top: 7px;
-      font-weight: 400;
-      time,
-      .delete {
-        font-size: 0.8664rem;
-        color: #8a9aa9;
-      }
-      .delete {
-        visibility: hidden;
-        cursor: pointer;
-        span {
-          margin: 0 0.2rem;
-        }
-      }
-      .action-box {
-        flex: 0 0 auto;
-        display: flex;
-        justify-content: space-between;
-        margin-left: auto;
-        min-width: 7.04rem;
-        color: #8a93a0;
-        user-select: none;
-        .action {
-          display: flex;
-          align-items: center;
-          margin-left: 0.4rem;
-          cursor: pointer;
-          &:hover {
-            opacity: 0.8;
-          }
-          &.active {
-            color: #37c700;
-          }
-          .icon {
-            min-width: 16.5px;
-            min-height: 16.5px;
-            width: 0.8rem;
-            height: 0.8rem;
-          }
-          .action-title {
-            margin-left: 0.2rem;
-            font-size: 0.8rem;
-          }
-        }
-      }
-    }
-  }
-}
-
-.sub-comment-list {
-  margin: 0.8rem 0;
-  padding: 0 0 0 0.8rem;
-  background-color: #fafbfc;
-  border-radius: 3px;
-  .comment-item {
-    margin-bottom: 0;
-    &:last-child .content-box {
-      border-bottom: none;
-    }
-    .comment {
-      position: relative;
-      padding: 0.8rem 0 0;
-
-      .content-box {
-        margin-right: 0.8rem;
-        padding-bottom: 0.8rem;
-      }
-    }
-  }
-}
-</style>

+ 0 - 12
src/components/comment/components/CommentList.js

@@ -1,12 +0,0 @@
-export default {
-  props: {
-    sub: {
-      type: Boolean,
-      default: false
-    }
-  },
-  render(h) {
-    const className = this.sub ? 'sub-comment-list' : 'comment-list'
-    return h('div', { class: className }, this.$slots.default)
-  }
-}

+ 0 - 264
src/components/comment/components/EmojiSelector.vue

@@ -1,264 +0,0 @@
-<template>
-  <div class="emoji-selector" @mousedown.prevent @mousedown.stop>
-    <div class="triangle" />
-    <div class="emoji-content">
-      <div class="category">
-        <div
-          v-for="(item, i) in currentEmojis"
-          :key="`emoji-${i}`"
-          class="item"
-          @click="$emit('choose', item)"
-        >
-          {{ item }}
-        </div>
-      </div>
-    </div>
-    <div class="next-page">
-      <div
-        v-for="cat in Object.keys(emojis)"
-        :key="cat"
-        :class="{ active: currentCat === cat }"
-        @click="currentCat = cat"
-      />
-    </div>
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'EmojiSelector',
-  data() {
-    return {
-      currentCat: 'FrequentlyUsed',
-      emojis: {
-        FrequentlyUsed: [
-          '😃',
-          '😘',
-          '😂',
-          '😳',
-          '😍',
-          '👏',
-          '👍',
-          '👎',
-          '😁',
-          '😉',
-          '😠',
-          '😞',
-          '😥',
-          '😭',
-          '😝',
-          '😡',
-          '❤',
-          '💔',
-          '😣',
-          '😔',
-          '😄',
-          '😷',
-          '😚',
-          '😓',
-          '😊',
-          '😢',
-          '😜',
-          '😨',
-          '😰',
-          '😲',
-          '😏',
-          '😱',
-          '😪',
-          '😖',
-          '😌',
-          '😒',
-          '👻',
-          '🎅',
-          '👧',
-          '👦',
-          '👩',
-          '👨',
-          '🐶',
-          '🐱',
-          '👊',
-          '✊',
-          '✌',
-          '💪',
-          '👆',
-          '👇',
-          '👉',
-          '👈',
-          '👌',
-          '💩'
-        ],
-        Symbols0: [
-          '🤗',
-          '😎',
-          '🤓',
-          '👩‍💻',
-          '👨‍💻',
-          '🙄',
-          '😭',
-          '😨',
-          '🤪',
-          '🎉',
-          '🤔',
-          '🐵',
-          '😇',
-          '🤬',
-          '🐈',
-          '😹',
-          '🙀',
-          '🇨🇳',
-          '👮',
-          '🐕',
-          '✅',
-          '👋',
-          '🔥',
-          '🐛',
-          '🍉',
-          '👽',
-          '🤖',
-          '⌚',
-          '🤝',
-          '🏳️‍🌈',
-          '🚩',
-          '💤',
-          '®',
-          '©',
-          '💯',
-          '™',
-          '💻',
-          '📅',
-          '📌',
-          '✉',
-          '⌨',
-          '📗',
-          '🤳',
-          '🛌',
-          '🎣',
-          '🎨',
-          '🎧',
-          '🎸',
-          '🎤',
-          '🏸',
-          '🏀',
-          '⚽',
-          '🎮',
-          '🏊'
-        ],
-        Symbols1: [
-          '🍗',
-          '🦄',
-          '🔞',
-          '🙏',
-          '☀',
-          '🌙',
-          '🌟',
-          '⚡',
-          '☁',
-          '☔',
-          '🍁',
-          '🌻',
-          '🍃',
-          '👗',
-          '🎀',
-          '👄',
-          '🌹',
-          '☕',
-          '🎂',
-          '🕙',
-          '🍺',
-          '🔍',
-          '📱',
-          '🏠',
-          '🚗',
-          '🎁',
-          '⚽',
-          '💣',
-          '💎',
-          '💊',
-          '🤮',
-          '🏆',
-          '👿'
-        ]
-      }
-    }
-  },
-  computed: {
-    currentEmojis() {
-      return this.emojis[this.currentCat]
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.emoji-selector {
-  padding: 0.8rem;
-  position: absolute;
-  top: 2.24rem;
-  z-index: 1;
-  bottom: 0;
-  width: 19rem;
-  height: 14rem;
-  border-radius: 2px;
-  background-color: #fff;
-  box-shadow: 0 5px 18px 0 rgba(0, 0, 0, 0.16);
-  box-sizing: content-box;
-  .triangle {
-    position: absolute;
-    top: -0.56rem;
-    left: 14%;
-    width: 0;
-    height: 0;
-    transform: translate(-50%, -50%);
-    border: 0.64rem solid transparent;
-    border-bottom-color: #fff;
-  }
-  .emoji-content {
-    height: 100%;
-    overflow: auto;
-    margin-bottom: 10px;
-    .category {
-      max-width: 19rem;
-      max-height: 13rem;
-      display: flex;
-      flex-wrap: wrap;
-      align-items: center;
-      justify-content: flex-start;
-      overflow: hidden;
-      .item {
-        width: calc(19rem / 9);
-        height: calc(13rem / 6);
-        font-size: 1.25rem;
-        text-align: center;
-        line-height: calc(10rem / 6);
-        cursor: pointer;
-        &:hover {
-          font-size: 1.6rem;
-        }
-      }
-    }
-  }
-  .next-page {
-    list-style: none;
-    position: absolute;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    bottom: 10px;
-    left: 50%;
-    transform: translate(-50%, -50%);
-    div {
-      list-style: none;
-      margin: 0 5px;
-      width: 6px;
-      height: 6px;
-      border-radius: 100%;
-      background-color: #f0f0f0;
-      cursor: pointer;
-      &.active {
-        cursor: default;
-        background-color: #d8d8d8;
-      }
-    }
-  }
-}
-</style>

+ 0 - 11
src/components/comment/index.js

@@ -1,11 +0,0 @@
-import JuejinComment from './index.vue'
-
-JuejinComment.install = function(Vue) {
-  Vue.component(JuejinComment.name, JuejinComment)
-}
-
-if (typeof window !== 'undefined' && window.Vue) {
-  JuejinComment.install(window.Vue)
-}
-
-export default JuejinComment

+ 0 - 561
src/components/comment/index.vue

@@ -1,561 +0,0 @@
-<template>
-  <div id="comment" ref="comment">
-    <!-- 顶部评论表单 -->
-    <comment-form :upload-img="uploadImg" @form-submit="formSubmit">
-      <img
-        class="avatar"
-        :src="user.avatar || ''"
-        @error="(e) => e.target.classList.add('error')"
-      >
-    </comment-form>
-
-    <!-- 底部评论列表 -->
-    <comment-list v-if="cacheData.length > 0" ref="comment-list">
-      <!-- 单条评论 -->
-      <comment-item
-        v-for="(comment, i) in cacheData"
-        :id="`comment-${i}`"
-        :key="`comment-${i}`"
-        :ref="`comment-${i}`"
-        :user="user"
-        :comment="comment"
-        @comment-reply="hasForm"
-        @comment-like="handleCommentLike"
-        @comment-delete="handleCommentDelete"
-      >
-        <!-- 回复表单 -->
-        <template #default="{ id }">
-          <comment-form
-            v-if="forms.includes(id)"
-            :id="id"
-            :parent="comment"
-            :placeholder="`回复${comment.user.name}...`"
-            :upload-img="uploadImg"
-            @form-submit="formSubmit"
-            @form-delete="deleteForm"
-          />
-        </template>
-
-        <!-- 单条评论下的回复列表 -->
-        <template #subList="{ parentId }">
-          <div>
-            <comment-list sub>
-              <!-- 单条回复 -->
-              <comment-item
-                v-for="(child, j) in comment.children"
-                :id="`${parentId}-${j}`"
-                :key="`${parentId}-${j}`"
-                :ref="`${parentId}-${j}`"
-                :comment="child"
-                :user="user"
-                :parent="comment"
-                @comment-reply="hasForm"
-                @comment-like="handleCommentLike"
-                @comment-delete="handleCommentDelete"
-              >
-                <!-- 单条回复的回复表单 -->
-                <comment-form
-                  v-if="forms.includes(`${parentId}-${j}`)"
-                  :id="`${parentId}-${j}`"
-                  :comment="child"
-                  :parent="comment"
-                  :placeholder="`回复${child.user && child.user.name}...`"
-                  :upload-img="uploadImg"
-                  @form-delete="deleteForm"
-                  @form-submit="formSubmit"
-                />
-              </comment-item>
-            </comment-list>
-            <el-pagination
-              :small="screenWidth <= 768"
-              hide-on-single-page
-              layout="prev, pager, next"
-              :page-size="pageSize"
-              :current-page="currentPage"
-              :total="comment.total"
-              @current-change="(val) => handleCurrentChange(comment, val)"
-            />
-          </div>
-        </template>
-      </comment-item>
-    </comment-list>
-  </div>
-</template>
-
-<script>
-import CommentForm from './components/CommentForm'
-import CommentList from './components/CommentList'
-import CommentItem from './components/CommentItem'
-import { getChildComment } from '@/api/comment'
-
-export default {
-  name: 'JuejinComment',
-  components: { CommentList, CommentItem, CommentForm },
-  inheritAttrs: false,
-  // 接收父组件通过 v-model 绑定的值
-  model: {
-    prop: 'videoComments',
-    event: 'input'
-  },
-  props: {
-    /* 数据 */
-    // model 中的 videoComments prop
-    videoComments: {
-      type: Array,
-      default: () => [],
-      required: true
-    },
-    /* 当前用户 */
-    user: {
-      type: Object,
-      default: () => {},
-      required: true
-    },
-    /* 配置属性 */
-    props: {
-      type: Object,
-      default: () => {}
-    },
-    /* 提交表单前事件 */
-    beforeSubmit: {
-      type: Function,
-      required: true
-    },
-    /* 执行点赞前事件 */
-    beforeLike: {
-      type: Function,
-      required: true
-    },
-    /* 执行删除前事件 */
-    beforeDelete: {
-      type: Function,
-      required: true
-    },
-    /* 上传图片 */
-    uploadImg: {
-      type: Function,
-      required: true
-    }
-  },
-  data() {
-    return {
-      // 屏幕宽度, 为了控制分页条的大小
-      screenWidth: document.body.clientWidth,
-      currentPage: 1,
-      pageSize: 10,
-      totalSize: 20,
-      dataList: [],
-      // ********************************************************************/
-      forms: [], // 显示在视图上的所有表单 id
-      cacheData: [],
-      length: 0
-    }
-  },
-  computed: {
-    computedProps({ props }) {
-      if (!props) return null
-      const entries = Object.entries(props)
-      return entries.length > 0 ? entries : null
-    }
-  },
-  watch: {
-    videoComments: {
-      immediate: true,
-      handler(value) {
-        // 数据发生变化时加载新数据
-        this.processVideoComments()
-      }
-    }
-  },
-  mounted() {
-    // 当窗口宽度改变时获取屏幕宽度
-    window.onresize = () => {
-      return () => {
-        window.screenWidth = document.body.clientWidth
-        this.screenWidth = window.screenWidth
-      }
-    }
-  },
-  created() {
-    // 监听并执行一次
-    const cancel = this.$watch('data', () => {
-      this.processData()
-      cancel && cancel()
-    })
-  },
-  methods: {
-    /**
-     * 处理初始数据
-     */
-    processData() {
-      this.cacheData = this.data.map(this.comparePropsAndValues)
-    },
-    processVideoComments() {
-      this.cacheData = this.videoComments.map(this.comparePropsAndValues)
-      // console.log(this.videoComments)
-    },
-    /** 对比和检查每条评论对象字段值 */
-    comparePropsAndValues(comment) {
-      // 初始对象
-      const originObj = {
-        id: '',
-        content: '',
-        imgSrc: '',
-        children: [],
-        likes: 0,
-        liked: false,
-        reply: null,
-        createAt: null,
-        total: 0,
-        user: {
-          userId: -1,
-          name: '',
-          avatar: '',
-          author: false
-        }
-      }
-      // 赋值
-      for (const key in originObj) {
-        originObj[key] =
-          comment[this.props[key]] || comment[key] || originObj[key]
-
-        // 校验
-        this.validate({ key, value: originObj[key] })
-      }
-
-      if (originObj.children.length > 0) {
-        originObj.children = originObj.children.map(this.comparePropsAndValues)
-      }
-
-      return originObj
-    },
-
-    /** 校验数据 */
-    validate({ key, value }) {
-      const map = {
-        user: {
-          validate: function(v) {
-            return (
-              (typeof v !== 'object' || JSON.stringify(v) === '{}') &&
-              this.message
-            )
-          },
-          message: 'User must be an object with props.'
-        },
-        reply: {
-          validate: function(v) {
-            return typeof v !== 'object' && this.message
-          },
-          message: 'Reply must be an object'
-        },
-        children: {
-          validate: function(v) {
-            return !Array.isArray(v) && this.message
-          },
-          message: 'Children must be an array'
-        },
-        createAt: {
-          validate: function() {
-            return new Date(value).toString() === 'Invalid Date' && this.message
-          },
-          message: 'CreateAt is not a valid date.'
-        }
-      }
-
-      const target = map[key]
-      if (!target) return
-
-      const res = target.validate(value)
-      if (res) {
-        throw new Error(`validate(): ${res}`)
-      }
-    },
-
-    /**
-     * 将更新后的数组中的对象数据转换为初始对象结构
-     */
-    transformToOriginObj(comment) {
-      try {
-        const _comment = JSON.parse(JSON.stringify(comment))
-
-        if (_comment.children && _comment.children.length > 0) {
-          _comment.children = _comment.children.map(this.transformToOriginObj)
-        }
-
-        // 返回 props 中自定义的字段名
-        if (!this.computedProps) return _comment
-
-        for (const [key, value] of this.computedProps) {
-          if (key !== value && Object.hasOwnProperty.call(_comment, key)) {
-            _comment[value] = JSON.parse(JSON.stringify(_comment[key]))
-            delete _comment[key]
-          }
-        }
-
-        return _comment
-      } catch (e) {
-        console.error(e)
-      }
-    },
-
-    /**
-     * 判断是否已存在该id的表单,存在删除该表单,不存在则新增该表单,并触发其他表单blur事件
-     */
-    hasForm(id) {
-      this.forms.includes(id) ? this.deleteForm(id) : this.addForm(id)
-      this.broadcastBlur(this.$refs['comment-list'].$children, id)
-    },
-
-    /**
-     * 增加新表单
-     */
-    addForm(id) {
-      this.forms.push(id)
-      // this.scrollIntoView(`${id}-form`)
-    },
-
-    /** 删除表单 */
-    deleteForm(id) {
-      const index = this.forms.indexOf(id)
-      index > -1 && this.forms.splice(index, 1)
-    },
-
-    /**
-     * 评论或回复
-     */
-    async formSubmit({
-      newComment: { id, callback, ...params },
-      parent = null
-    }) {
-      const _params = Object.assign(params, { user: this.user })
-
-      // 等待外部提交事件执行
-      if (typeof this.beforeSubmit === 'function') {
-        try {
-          const data = this.transformToOriginObj(_params)
-
-          const add = (data) => {
-            this.addComment(id, this.comparePropsAndValues(data))
-            callback()
-          }
-
-          await this.beforeSubmit(data, parent, add)
-        } catch (e) {
-          console.error(e)
-        }
-      }
-    },
-
-    async handleCommentLike({ id, comment: { children, ...params }}) {
-      const _params = Object.assign(params, { user: this.user })
-      if (typeof this.beforeLike === 'function') {
-        try {
-          await this.beforeLike(this.transformToOriginObj(_params))
-
-          this.storeLikes(id)
-        } catch (e) {
-          console.error(e)
-        }
-      }
-    },
-
-    /**
-     * 删除评论或回复
-     */
-    async handleCommentDelete({ id, comment, parent = null }) {
-      if (typeof this.beforeDelete === 'function') {
-        try {
-          const data = this.transformToOriginObj(comment)
-          await this.beforeDelete(data, parent)
-
-          this.deleteComment(id)
-        } catch (e) {
-          console.error(e)
-        }
-      }
-    },
-
-    /**
-     * 保存点赞
-     */
-    storeLikes(id) {
-      const { commentIndex, replyIndex } = this.getIndex(id)
-
-      let comment = this.cacheData[commentIndex]
-
-      if (!isNaN(replyIndex)) {
-        comment = comment.children[replyIndex]
-      }
-
-      comment.liked = !comment.liked
-
-      if (comment.likes) {
-        comment.liked ? comment.likes++ : comment.likes--
-      } else {
-        comment.likes = 1
-      }
-
-      const data = this.cacheData.map(this.transformToOriginObj)
-      this.$emit('input', data)
-    },
-
-    /**
-     * 存储新评论或回复
-     */
-    addComment(id, rawData) {
-      const { commentIndex } = this.getIndex(id)
-
-      // 更新视图
-      if (commentIndex === 'root') {
-        this.cacheData.push(rawData)
-      } else {
-        const comment = this.cacheData[commentIndex]
-        comment.children.push(rawData)
-      }
-
-      // 滚动至可见视图上
-      const signal =
-        commentIndex === 'root'
-          ? this.cacheData.length - 1
-          : `${commentIndex}-${
-            this.cacheData[commentIndex].children.length - 1
-          }`
-      this.scrollIntoView(`comment-${signal}`)
-
-      // 更新外部数据
-      const data = this.cacheData.map(this.transformToOriginObj)
-      this.$emit('input', data)
-    },
-
-    /**
-     * 删除评论或回复
-     */
-    deleteComment(id) {
-      const { commentIndex, replyIndex } = this.getIndex(id)
-
-      this.cacheData = this.cacheData.filter((c, i) => {
-        if (isNaN(replyIndex)) {
-          return i !== commentIndex
-        } else {
-          c.children = c.children.filter((r, j) => j !== replyIndex)
-          return c
-        }
-      })
-
-      const data = this.cacheData.map(this.transformToOriginObj)
-      this.$emit('input', data)
-    },
-
-    /**
-     * 向下递归触发表单blur事件
-     */
-    broadcastBlur(target, id) {
-      if (id && target.id === id) return
-
-      if (Array.isArray(target)) {
-        target.map((c) => this.broadcastBlur(c, id))
-      } else {
-        const children = target.$children
-        children && this.broadcastBlur(children, id)
-
-        const richInput = target.$refs['rich-input']
-        richInput && richInput.blur()
-      }
-    },
-
-    /**
-     * 从id中提取出序号
-     */
-    getIndex(id) {
-      const [, c, r] = id.split('-')
-      return { commentIndex: c === 'root' ? c : +c, replyIndex: +r }
-    },
-
-    /**
-     * 将子组件滚动到视图可见区域
-     */
-    scrollIntoView(ref) {
-      this.$nextTick(() => {
-        this.$refs[ref][0].$el.scrollIntoView(false)
-      })
-    },
-    handleCurrentChange(comment, currentPage) {
-      this.currentPage = currentPage
-      this.getChildCommentWrapper(comment, this.currentPage)
-    },
-    getChildCommentWrapper(comment, pageNumber) {
-      getChildComment(comment.id, pageNumber).then(resp => {
-        if (resp.code === 0) {
-          comment.children = resp.data.list
-        } else {
-          console.error(resp.msg)
-        }
-      }).catch(error => {
-        console.error(error.message)
-      })
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-#comment {
-  // border-top: 1px solid #ebebeb;
-  padding-top: 1.0664rem;
-  & > .comment-form {
-    margin: 0 1.3328rem 1.0664rem;
-  }
-  & > .comment-list {
-    margin: 0 1.3328rem 0 5.2rem;
-    background-color: #fff;
-  }
-
-  ::v-deep {
-    img {
-      user-select: none;
-      -webkit-user-drag: none;
-      &.avatar {
-        width: 2.1336rem;
-        height: 2.1336rem;
-        border-radius: 50%;
-        cursor: pointer;
-      }
-      &.error {
-        display: inline-block;
-        transform: scale(0.5);
-        content: '';
-        color: transparent;
-        &::before {
-          content: '';
-          position: absolute;
-          left: 0;
-          top: 0;
-          width: 100%;
-          height: 100%;
-          border-radius: 50%;
-          border: 1px solid #e7e7e7;
-          box-sizing: border-box;
-          transform: scale(2);
-          background: #f5f5f5
-            url("data:image/svg+xml,%3Csvg class='icon' viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cpath d='M304.128 456.192c48.64 0 88.064-39.424 88.064-88.064s-39.424-88.064-88.064-88.064-88.064 39.424-88.064 88.064 39.424 88.064 88.064 88.064zm0-116.224c15.36 0 28.16 12.288 28.16 28.16s-12.288 28.16-28.16 28.16-28.16-12.288-28.16-28.16 12.288-28.16 28.16-28.16z' fill='%23e6e6e6'/%3E%3Cpath d='M887.296 159.744H136.704C96.768 159.744 64 192 64 232.448v559.104c0 39.936 32.256 72.704 72.704 72.704h198.144L500.224 688.64l-36.352-222.72 162.304-130.56-61.44 143.872 92.672 214.016-105.472 171.008h335.36C927.232 864.256 960 832 960 791.552V232.448c0-39.936-32.256-72.704-72.704-72.704zm-138.752 71.68v.512H857.6c16.384 0 30.208 13.312 30.208 30.208v399.872L673.28 408.064l75.264-176.64zM304.64 792.064H165.888c-16.384 0-30.208-13.312-30.208-30.208v-9.728l138.752-164.352 104.96 124.416-74.752 79.872zm81.92-355.84l37.376 228.864-.512.512-142.848-169.984c-3.072-3.584-9.216-3.584-12.288 0L135.68 652.8V262.144c0-16.384 13.312-30.208 30.208-30.208h474.624L386.56 436.224zm501.248 325.632c0 16.896-13.312 30.208-29.696 30.208H680.96l57.344-93.184-87.552-202.24 7.168-7.68 229.888 272.896z' fill='%23e6e6e6'/%3E%3C/svg%3E")
-            no-repeat center / 50% 50%;
-        }
-      }
-    }
-  }
-}
-
-@media screen and (max-width: 600px) {
-  #comment {
-    & > .comment-list {
-      margin: 0 1.6rem;
-    }
-    & > .comment-form {
-      margin: 1rem 1.6rem;
-    }
-    & > ::v-deep .comment-root .avatar-box {
-      display: none;
-    }
-  }
-}
-</style>