<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>
<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>