Vue3评论(Comment)

效果如下图:

在线预览

APIs

Comment Props

名称 说明 类型 默认值
actions 在评论内容下面呈现的操作项列表 Array<string | VNode> undefined
author 要显示为评论作者的元素 string undefined
avatar 要显示为评论头像的元素,通常为头像图片地址;prop 支持 VNode / 渲染函数,插槽形态请用同名 #avatar 插槽 string | VNode | (() => VNode) undefined
content 评论的主要内容 string undefined
datetime 展示时间描述 string undefined

Slots

名称 说明 用法
actions 在评论内容下面呈现的操作项列表 v-slot:actions
author 要显示为评论作者的元素 v-slot:author
avatar 要显示为评论头像的元素 v-slot:avatar
content 评论的主要内容 v-slot:content
datetime 展示时间描述 v-slot:datetime
default 嵌套评论,通常用于展示评论回复 v-slot:default

创建评论组件 Comment.vue

ts 复制代码
<script setup lang="ts">
import { computed, useSlots } from 'vue'
import type { VNode } from 'vue'
import { useSlotsExist } from 'components/utils'
export interface Props {
  actions?: Array<string | VNode> // 在评论内容下面呈现的操作项列表
  author?: string // 要显示为评论作者的元素
  avatar?: string | VNode | (() => VNode) // 要显示为评论头像的元素,通常为头像图片地址
  content?: string // 评论的主要内容
  datetime?: string // 展示时间描述
}
// 声明组件插槽类型
export interface CommentSlots {
  actions?: () => VNode[]
  author?: () => VNode[]
  avatar?: () => VNode[]
  content?: () => VNode[]
  datetime?: () => VNode[]
  default?: () => VNode[]
}
const props = withDefaults(defineProps<Props>(), {
  actions: undefined,
  author: undefined,
  avatar: undefined,
  content: undefined,
  datetime: undefined
})
defineSlots<CommentSlots>()
const slots = useSlots()
const slotsExist = useSlotsExist(['actions', 'author', 'avatar', 'datetime', 'default'])
const showAvatar = computed(() => {
  return slotsExist.avatar || props.avatar
})
// 头像 prop 支持「图片地址 / VNode / 渲染函数」三种形态,图片地址单独取出用于渲染 <img>
const avatarSrc = computed<string | undefined>(() => {
  return typeof props.avatar === 'string' ? props.avatar : undefined
})
// VNode / 渲染函数形态的头像原样渲染,不在组件内包一层 <Avatar>
const avatarRender = computed<VNode | undefined>(() => {
  const { avatar } = props
  if (!avatar || typeof avatar === 'string') {
    return undefined
  }
  return typeof avatar === 'function' ? avatar() : avatar
})
const showAuthor = computed(() => {
  return slotsExist.author || props.author
})
const showDatetime = computed(() => {
  return slotsExist.datetime || props.datetime
})
const showActions = computed(() => {
  return slotsExist.actions || (props.actions && props.actions.length)
})
// 合并 prop 与 slot 两种来源,统一为「每项一个 <li>」的操作项列表
const actionList = computed<Array<string | VNode>>(() => {
  if (slotsExist.actions) {
    return slots.actions?.() ?? []
  }
  if (Array.isArray(props.actions)) {
    return props.actions
  }
  return props.actions ? [props.actions] : []
})
const showNested = computed(() => {
  return slotsExist.default
})
</script>
<template>
  <div class="comment-wrap">
    <div class="comment-inner">
      <div v-if="showAvatar" class="comment-avatar">
        <slot name="avatar">
          <img v-if="avatarSrc" :src="avatarSrc" alt="comment-avatar" />
          <component :is="avatarRender" v-else />
        </slot>
      </div>
      <div class="comment-content">
        <div class="comment-content-author">
          <span v-if="showAuthor" class="comment-content-author-name">
            <slot name="author">{{ author }}</slot>
          </span>
          <span v-if="showDatetime" class="comment-content-author-time">
            <slot name="datetime">{{ datetime }}</slot>
          </span>
        </div>
        <div class="comment-content-detail">
          <slot name="content">{{ content }}</slot>
        </div>
        <ul v-if="showActions" class="comment-actions">
          <li v-for="(action, index) in actionList" :key="`action-${index}`">
            <span v-if="typeof action === 'string'">{{ action }}</span>
            <component :is="action" v-else />
          </li>
        </ul>
      </div>
    </div>
    <div v-if="showNested" class="comment-nested">
      <slot></slot>
    </div>
  </div>
</template>
<style lang="less" scoped>
.comment-wrap {
  position: relative;
  background-color: inherit;
  font-size: 14px;
  color: var(--va-text-color-1);
  .comment-inner {
    display: flex;
    padding: 16px 0;
    .comment-avatar {
      position: relative;
      flex-shrink: 0;
      margin-right: 12px;
      :deep(img) {
        width: 32px;
        height: 32px;
        border-radius: 50%;
      }
    }
    .comment-content {
      position: relative;
      flex: 1 1 auto;
      min-width: 1px;
      font-size: 14px;
      word-wrap: break-word;
      .comment-content-author {
        display: flex;
        flex-wrap: wrap;
        justify-content: flex-start;
        margin-bottom: 4px;
        font-size: 14px;
        :deep(> a),
        > span {
          padding-right: 8px;
          font-size: 12px;
          line-height: 18px;
        }
        .comment-content-author-name {
          color: var(--va-text-color-3);
          font-size: 14px;
          transition: color 0.3s;
          :deep(> *) {
            color: var(--va-text-color-3);
            &:hover {
              color: var(--va-text-color-3);
            }
          }
        }
        .comment-content-author-time {
          color: var(--va-text-color-disabled);
          white-space: nowrap;
          cursor: auto;
        }
      }
      .comment-content-detail {
        :deep(p) {
          margin-bottom: inherit;
          white-space: pre-wrap;
        }
      }
      .comment-actions {
        margin-top: 12px;
        margin-bottom: 0;
        padding-left: 0;
        :deep(> li) {
          display: inline-block;
          color: var(--va-text-color-3);
          > span {
            margin-right: 10px;
            color: var(--va-text-color-3);
            font-size: 12px;
            cursor: pointer;
            transition: color 0.3s;
            user-select: none;
            &:hover {
              color: var(--va-text-color-2);
            }
          }
        }
      }
    }
  }
  .comment-nested {
    margin-left: 44px;
  }
}
</style>

在要使用的页面引入

ts 复制代码
<script setup lang="ts">
import Comment from './Comment.vue'
import { h, ref } from 'vue'
import { LikeFilled, LikeOutlined, DislikeFilled, DislikeOutlined, UserOutlined } from '@ant-design/icons-vue'
// 基本评论
const likes = ref<number>(0)
const dislikes = ref<number>(0)
const action = ref<string>()
function like() {
  likes.value = 1
  dislikes.value = 0
  action.value = 'liked'
}
function dislike() {
  likes.value = 0
  dislikes.value = 1
  action.value = 'disliked'
}
// 配合 List 组件
const listData = [
  {
    actions: ['Reply to'],
    author: 'Han Solo',
    avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=1',
    content:
      'We supply a series of design principles, practical patterns and high quality design resources (Sketch and Axure), to help people create their product prototypes beautifully and efficiently.',
    datetime: '2 days ago'
  },
  {
    actions: ['Reply to'],
    author: 'Han Solo',
    avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=2',
    content:
      'We supply a series of design principles, practical patterns and high quality design resources (Sketch and Axure), to help people create their product prototypes beautifully and efficiently.',
    datetime: '3 days ago'
  }
]
// 回复框
interface CommentItem {
  author: string
  avatar: string
  content: string
  datetime: string
}
const comments = ref<CommentItem[]>([])
const submitting = ref<boolean>(false)
const value = ref<string>('')
function handleSubmit() {
  if (!value.value) {
    return
  }
  submitting.value = true
  setTimeout(() => {
    submitting.value = false
    comments.value = [
      {
        author: 'Han Solo',
        avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=me',
        content: value.value,
        datetime: 'just now'
      },
      ...comments.value
    ]
    value.value = ''
  }, 1000)
}
</script>
<template>
  <div>
    <h1>{{ $route.name }} {{ $route.meta.title }}</h1>
    <h2 class="mt30 mb10">基本评论</h2>
    <p class="mb10">一个基本的评论组件,带有作者、头像、时间和操作</p>
    <Comment>
      <template #actions>
        <span key="comment-basic-like">
          <Tooltip tooltip="Like" style="display: inline-block">
            <LikeFilled v-if="action === 'liked'" @click="like" />
            <LikeOutlined v-else @click="like" />
          </Tooltip>
          <span style="padding-left: 8px; cursor: auto">{{ likes }}</span>
        </span>
        <span key="comment-basic-dislike">
          <Tooltip tooltip="Dislike" style="display: inline-block">
            <DislikeFilled v-if="action === 'disliked'" @click="dislike" />
            <DislikeOutlined v-else @click="dislike" />
          </Tooltip>
          <span style="padding-left: 8px; cursor: auto">{{ dislikes }}</span>
        </span>
        <span key="comment-basic-reply-to">Reply to</span>
      </template>
      <template #author><a>Han Solo</a></template>
      <template #avatar>
        <Avatar src="https://api.dicebear.com/7.x/miniavs/svg?seed=han" alt="Han Solo" />
      </template>
      <template #content>
        <p>
          We supply a series of design principles, practical patterns and high quality design resources (Sketch and
          Axure), to help people create their product prototypes beautifully and efficiently.
        </p>
      </template>
      <template #datetime>
        <Tooltip tooltip="2016-11-22 11:22:33" style="display: inline-block">
          <span>8 hours ago</span>
        </Tooltip>
      </template>
    </Comment>
    <h2 class="mt30 mb10">配合 List 组件</h2>
    <p class="mb10">配合 <code>List</code> 组件展现评论列表</p>
    <List :header="`${listData.length} replies`">
      <ListItem v-for="(item, index) in listData" :key="index">
        <Comment :author="item.author" :avatar="item.avatar">
          <template #actions>
            <span v-for="(act, idx) in item.actions" :key="idx">{{ act }}</span>
          </template>
          <template #content>
            <p>{{ item.content }}</p>
          </template>
          <template #datetime>
            <Tooltip :tooltip="item.datetime" style="display: inline-block">
              <span>{{ item.datetime }}</span>
            </Tooltip>
          </template>
        </Comment>
      </ListItem>
    </List>
    <h2 class="mt30 mb10">嵌套评论</h2>
    <p class="mb10">评论可以嵌套</p>
    <Comment>
      <template #actions>
        <span key="comment-nested-reply-to">Reply to</span>
      </template>
      <template #author><a>Han Solo</a></template>
      <template #avatar>
        <Avatar src="https://api.dicebear.com/7.x/miniavs/svg?seed=han" alt="Han Solo" />
      </template>
      <template #content>
        <p
          >We supply a series of design principles, practical patterns and high quality design resources (Sketch and
          Axure).</p
        >
      </template>
      <Comment>
        <template #actions>
          <span>Reply to</span>
        </template>
        <template #author><a>Han Solo</a></template>
        <template #avatar>
          <Avatar src="https://api.dicebear.com/7.x/miniavs/svg?seed=han2" alt="Han Solo" />
        </template>
        <template #content>
          <p
            >We supply a series of design principles, practical patterns and high quality design resources (Sketch and
            Axure).</p
          >
        </template>
        <Comment>
          <template #actions>
            <span>Reply to</span>
          </template>
          <template #author><a>Han Solo</a></template>
          <template #avatar>
            <Avatar src="https://api.dicebear.com/7.x/miniavs/svg?seed=han3" alt="Han Solo" />
          </template>
          <template #content>
            <p
              >We supply a series of design principles, practical patterns and high quality design resources (Sketch and
              Axure).</p
            >
          </template>
        </Comment>
        <Comment>
          <template #actions>
            <span>Reply to</span>
          </template>
          <template #author><a>Han Solo</a></template>
          <template #avatar>
            <Avatar src="https://api.dicebear.com/7.x/miniavs/svg?seed=han4" alt="Han Solo" />
          </template>
          <template #content>
            <p
              >We supply a series of design principles, practical patterns and high quality design resources (Sketch and
              Axure).</p
            >
          </template>
        </Comment>
      </Comment>
    </Comment>
    <h2 class="mt30 mb10">回复框</h2>
    <p class="mb10">评论编辑器组件提供了相同样式的封装以支持自定义评论编辑器</p>
    <List v-if="comments.length" :header="`${comments.length} ${comments.length > 1 ? 'replies' : 'reply'}`">
      <ListItem v-for="(item, index) in comments" :key="index">
        <Comment :author="item.author" :avatar="item.avatar" :content="item.content" :datetime="item.datetime" />
      </ListItem>
    </List>
    <Comment>
      <template #avatar>
        <Avatar src="https://api.dicebear.com/7.x/miniavs/svg?seed=me" alt="Han Solo" />
      </template>
      <template #content>
        <Textarea v-model:value="value" :rows="4" />
        <Button html-type="submit" :loading="submitting" type="primary" class="mt10" @click="handleSubmit">
          Add Comment
        </Button>
      </template>
    </Comment>
    <h2 class="mt30 mb10">自定义头像</h2>
    <p class="mb10">头像属性支持 <code>VNode</code> / 渲染函数,也可继续使用 <code>#avatar</code> 插槽自定义</p>
    <Comment author="Han Solo" datetime="2 days ago" :avatar="() => h(UserOutlined, { style: 'font-size: 32px' })">
      <template #content>
        <p>
          We supply a series of design principles, practical patterns and high quality design resources (Sketch and
          Axure), to help people create their product prototypes beautifully and efficiently.
        </p>
      </template>
    </Comment>
    <Comment author="Han Solo" datetime="2 days ago">
      <template #avatar>
        <Avatar src="https://api.dicebear.com/7.x/miniavs/svg?seed=han" alt="Han Solo" />
      </template>
      <template #content>
        <p>
          We supply a series of design principles, practical patterns and high quality design resources (Sketch and
          Axure), to help people create their product prototypes beautifully and efficiently.
        </p>
      </template>
    </Comment>
  </div>
</template>
相关推荐
afumu8 小时前
把五个网页版 AI 拉进同一个群聊:不用 API key 的多 Agent 编排
人工智能·chrome·typescript
RuoyiOffice1 天前
Vue3+Gantt:把任务排期、依赖关系和里程碑放进项目管理
项目管理·vue3·springboot·dhtmlx-gantt·wbs·gantt·ruoyi office
孟陬2 天前
Foobar 技术周刊#160:大模型让大规模形式化软件验证成为现实丨AI写C++代码之后会发生什么?丨我们真的需要alpha() 函数吗丨重试,有时会让故障更
前端·后端·typescript
AI开源软件3 天前
SpringBoot3+Vue3 财务管理系统如何设计:从业务单据到总账的模块划分
spring boot·vue3·领域建模·会计凭证·ruoyi office·业财协同·财务系统设计
螺丝钉tina3 天前
[Playwright ][16]Playwright + TypeScript API 自动化测试实战:API测试
前端·javascript·typescript
华玥作者4 天前
Vue 3 + TypeScript + uni-app 组件库推荐:hy-app(华玥组件库)
vue.js·typescript·uni-app
Momo__4 天前
你写的 import 重试,从来没生效过
前端·javascript·typescript
长不大的杨瑞杰5 天前
OpenCode v2 Custom Tools 实战:开发数据库、工单与发布工具
人工智能·typescript·开发工具·opencode·custom tools
shmily麻瓜小菜鸡5 天前
TypeScript 知识点补充(第三部分:工程化与高级类型)
前端·javascript·typescript