2024/7/31更新,新增了组件回复框的追踪控制,采用provide / inject
实现
引用时需要在顶层组件生成一个追踪变量和更新方法
组件内部 **postReply** 方法是发表评论接口示例,请根据自己的项目需要,自行变更修改接口
const activeReplyId = ref<number | null>(null);
const setActiveReplyId = (id: number | null) => activeReplyId.value = id;
provide('activeReplyId', activeReplyId);
provide('setActiveReplyId', setActiveReplyId);
2024/6/23发布
此组件旨在创建一个具备嵌套回复能力的通用评论区域,适用于构建动态、互动性强的用户讨论场景
代码比较简单,方便大家二次开发,旨在快速提供基础的样式模板,自行迭代定制
2024/7/31预览
2024/6/23预览
简介
!!!!!!!!注意!!!!!!!!!
父组件需要采用provide
全层级传递两个方法才能正常处理回复框
const activeReplyId = ref<number | null>(null);
const setActiveReplyId = (id: number | null) => activeReplyId.value = id;
provide('activeReplyId', activeReplyId);
provide('setActiveReplyId', setActiveReplyId);
-
通用评论组件
-
组件功能
- 此组件旨在创建一个具备嵌套回复能力的通用评论区域,适用于构建动态、互动性强的用户讨论场景。
-
接收数据结构
- 组件通过 Props 接收数据,数据模型设计详细描述了评论及其嵌套回复的所有必要信息。
- @property {Array} data - 评论列表
-
Comment 类型定义
-
- userImg: string - 用户头像URL
-
- userName: string - 用户名
-
- time: string - 评论时间戳(如 “17小时前”)
-
- content: string - 评论内容
-
- ReplyData: Array - 子评论集合
-
Reply 类型定义
-
- userImg1: string - 回复者头像URL
-
- userName1: string - 回复者用户名
-
- userImg2: string - 被回复者头像URL(可选)
-
- userName2: string - 被回复者用户名(可选)
-
- replytime: string - 回复时间
-
- replycontent: string - 回复内容
-
- anthTags: number - 回复者身份标识
-
- 0: 无特殊身份
-
- 1: 回复者为原贴作者
-
- 2: 被回复者为原贴作者
-
- 3: 原贴作者回复原贴作者
-
示例数据说明
- 提供的数据结构实例展示了如何组织顶级评论及其关联的回复数据,以便于组件正确解析并渲染。
-
使用提示
-
- 确认传递给组件的数据严格遵循上述结构,以确保界面的正确显示。
-
- 利用Vue 3的Composition API特性,可以进一步优化状态管理和逻辑处理。
代码
父组件
<template>
<div class="firstglobals">
<!-- 头像 -->
<div class="avatar">
<img :src="data.userImg" style="width: 56px; height: 56px; border-radius: 50%;" alt="" />
</div>
<!-- 内容 -->
<div class="content">
<div class="usertop">
<div style="display: flex;gap: 10px;">
<div class="username">{{ data.userName }}</div>
<div class="tags" v-if="data.tag === 1">
作者
</div>
</div>
<div class="time">{{ data.time }}</div>
</div>
<div style="display: flex; flex-direction: column; margin-top: 1em;">
<div class="text">{{ data.content }}</div>
<div style="display: flex; align-self: flex-end;">
<img src="@/assets/globals/点赞默认.png" style="width: 20px;" alt="" />
</div>
<div class="but" @click="toggleReplyBox(data.id)">回复</div>
</div>
<div v-if="isReplyBoxVisible" class="reply-box">
<textarea v-model="replyContent" placeholder="输入你的回复..." rows="3"></textarea>
<button @click="submitReply(data.id)" v-loading='isLoading'>发布</button>
</div>
<!-- 回复组件 -->
<div>
<div v-for="(item, index) in displayedReplies" :key="index">
<LuxCommentSectionItem :dataCode="dataCode" :replyData="item"
@PostsubmitReply="handleSubmitReply" />
</div>
<div v-if="!showAllReplies && data.ReplyData.length > 2" class="load-more"
@click="showAllReplies = true">
加载更多回复 ...
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, inject, Ref, defineEmits, onMounted } from 'vue';
import LuxCommentSectionItem from '@/components/currency/LuxCommentSectionItem.vue';
import { postReply } from '@/axios/commentApi/comment';
interface ReplyData {
userImg1: string,
userName1: string,
userImg2: string,
userName2: string,
replytime: string,
replycontent: string,
anthTags: any,
id: any
}
interface CommentData {
userImg: string;
userName: string;
time: string;
content: string;
ReplyData: ReplyData[];
id: any;
tag?:any
}
const emits = defineEmits(['PostsubmitReply']);
const handleSubmitReply = () => {
emits("PostsubmitReply")
}
const props = defineProps<{
data: CommentData;
dataCode?: number
}>();
const isLoading = ref(false);
const showAllReplies = ref(false);
const replyContent = ref('');
const activeReplyId = inject<Ref<number | null>>('activeReplyId', ref(null));
const setActiveReplyId = inject<Function>('setActiveReplyId');
const isReplyBoxVisible = computed(() => activeReplyId?.value === props.data.id);
const toggleReplyBox = (id: any) => {
if (setActiveReplyId) {
activeReplyId.value === id ? setActiveReplyId(null) : setActiveReplyId(id);
}
};
const displayedReplies = computed(() => {
if (showAllReplies.value || props.data.ReplyData.length <= 2) {
return props.data.ReplyData;
} else {
return props.data.ReplyData.slice(0, 2);
}
});
const submitReply = (id: string) => {
if (replyContent.value.trim()) {
// 在这里处理回复的提交逻辑,例如发送请求到服务器或更新本地数据
console.log('提交的回复内容:', replyContent.value);
// 提交后关闭回复框
isLoading.value = true;
postReply(
{
"code": props.dataCode,
"comments": replyContent.value,
"pid": id
}
).then(res => {
activeReplyId.value = null;
replyContent.value = '';
isLoading.value = false;
// @ts-ignore
ElMessage.success('回复成功');
emits("PostsubmitReply")
}).catch(err => {
activeReplyId.value = null;
replyContent.value = '';
isLoading.value = false;
});
} else {
// @ts-ignore
ElMessage.error('请输入回复内容');
isLoading.value = false;
}
};
onMounted(() => {
console.log("评论数据",props.data.ReplyData);
});
</script>
<style scoped>
.but {
width: 60px;
padding: 5px 0px;
background: #f1f1f1;
border-radius: 4px;
display: flex;
justify-content: center;
align-content: center;
font-weight: 400;
font-size: 14px;
color: #0f1014;
text-align: left;
font-style: normal;
text-transform: none;
}
.tags {
width: 32px;
height: 18px;
background: #0F1014;
border-radius: 4px;
color: #FFFFFF;
font-weight: 400;
font-size: 10px;
line-height: 12px;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
}
.but:hover {
background: #ebe4e4;
}
.text {
font-weight: 400;
font-size: 18px;
color: #000000;
line-height: 21px;
text-align: left;
font-style: normal;
text-transform: none;
}
.time {
font-weight: 400;
font-size: 12px;
color: #666666;
line-height: 14px;
text-align: left;
font-style: normal;
text-transform: none;
}
.avatar {
width: 56px;
height: 56px;
border-radius: 30px;
}
.usertop {
display: flex;
flex-direction: column;
gap: 5px;
}
.username {
font-weight: 700;
font-size: 16px;
color: #0f1014;
line-height: 19px;
text-align: left;
font-style: normal;
text-transform: none;
}
.content {
display: flex;
flex-direction: column;
margin-left: 1em;
margin-top: 10px;
flex: 1;
}
.firstglobals {
display: flex;
justify-content: start;
margin-top: 2em;
}
.load-more {
margin-top: 30px;
margin-left: 2em;
color: #0066cc;
cursor: pointer;
font-size: 14px;
}
.load-more:hover {
text-decoration: underline;
}
.reply-box {
margin-top: 10px;
display: flex;
flex-direction: column;
gap: 10px;
align-items: end;
position: relative;
width: 98%;
align-self: flex-end;
}
.reply-box textarea {
width: 100%;
padding: 10px;
border-radius: 4px;
border: 1px solid #ddd;
resize: none;
outline: none;
}
.reply-box button {
align-self: flex-end;
padding: 10px 20px;
border-radius: 4px;
border: none;
background-color: #070707;
color: white;
cursor: pointer;
}
.reply-box button:hover {
background-color: #2d2d2d;
}
</style>
子组件
<template>
<div class="reply-comments">
<div class="top-user">
<div style="display: flex; justify-content: center; align-content: center; gap: 8px;">
<img :src="replyData.userImg1" style="width: 24px; height: 24px; border-radius: 50%;" alt="">
<span class="username">{{ replyData.userName1 }}</span>
</div>
<div class="tags" v-if="replyData.anthTags === 1 || replyData.anthTags === 3">
作者
</div>
<div class="hf">
回复
</div>
<div style="display: flex; justify-content: center; align-content: center; gap: 8px;">
<img :src="replyData.userImg2" style="width: 24px; height: 24px; border-radius: 50%;" alt="">
<span class="username">{{ replyData.userName2 }}</span>
</div>
<div class="tags" v-if="replyData.anthTags === 2 || replyData.anthTags === 3">
作者
</div>
<div class="time">
{{ replyData.replytime }}
</div>
</div>
<div class="content">
{{ replyData.replycontent }}
</div>
<div style="display: flex; align-self: flex-end;">
<img src="@/assets/globals/点赞默认.png" style="width: 20px;" alt="">
</div>
<div class="but" @click="toggleReplyBox">
回复
</div>
<div v-if="isReplyBoxVisible" class="reply-box">
<textarea v-model="replyContent" placeholder="输入你的回复..." rows="3"></textarea>
<button @click="submitReply(replyData.id)" v-loading="isLoading">发布</button>
</div>
</div>
</template>
<script setup lang="ts">
import { postReply } from '@/axios/commentApi/comment';
import { ref, inject, computed, Ref, defineEmits, watch, onMounted } from 'vue';
const emits = defineEmits(['PostsubmitReply'])
interface ReplyData {
userImg1: string,
userName1: string,
userImg2: string,
userName2: string,
replytime: string,
replycontent: string,
anthTags: number,
id: number
}
const props = defineProps<{
replyData: ReplyData;
dataCode?: number;
}>();
const activeReplyId = inject<Ref<number | null>>('activeReplyId', ref(null));
const setActiveReplyId = inject<Function>('setActiveReplyId');
const replyContent = ref('');
const isReplyBoxVisible = computed(() => activeReplyId?.value === props.replyData.id);
const isLoading = ref(false);
const toggleReplyBox = () => {
if (setActiveReplyId) {
setActiveReplyId(isReplyBoxVisible.value ? null : props.replyData.id);
}
};
const submitReply = (id: any) => {
if (replyContent.value.trim()) {
// 在这里处理回复的提交逻辑,例如发送请求到服务器或更新本地数据
console.log('提交的回复内容:', replyContent.value);
isLoading.value = true;
// 提交后关闭回复框
postReply(
{
"code": props.dataCode,
"comments": replyContent.value,
"pid": id
}
).then(res => {
activeReplyId.value = null;
replyContent.value = '';
isLoading.value = false;
// @ts-ignore
ElMessage.success('回复成功');
emits("PostsubmitReply")
}).catch(err => {
activeReplyId.value = null;
replyContent.value = '';
isLoading.value = false;
});
} else {
// @ts-ignore
ElMessage.error('请输入回复内容');
}
};
watch(() => props.replyData, (newValue) => {
console.log('newValue', newValue.anthTags);
}, { deep: true });
onMounted(() => {
console.log("newValue", props.replyData.anthTags);
});
</script>
<style scoped>
.but {
width: 60px;
padding: 5px 0px;
background: #F1F1F1;
border-radius: 4px;
display: flex;
justify-content: center;
align-content: center;
font-weight: 400;
font-size: 14px;
color: #0F1014;
margin-left: 32px;
cursor: pointer;
}
.but:hover {
background: #ebe4e4;
}
.content {
font-weight: 400;
font-size: 18px;
color: #000000;
line-height: 21px;
text-align: left;
margin-left: 32px;
margin-top: 10px;
}
.time {
font-weight: 400;
font-size: 12px;
color: #666666;
line-height: 14px;
text-align: left;
}
.hf {
font-weight: 400;
font-size: 14px;
color: #B9B9B9;
line-height: 16px;
text-align: left;
}
.tags {
width: 32px;
height: 18px;
background: #0F1014;
border-radius: 4px;
color: #FFFFFF;
font-weight: 400;
font-size: 10px;
line-height: 12px;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
}
.username {
height: 24px;
font-weight: 500;
font-size: 13px;
color: #0F1014;
line-height: 15px;
display: flex;
justify-content: center;
align-items: center;
}
.top-user {
display: flex;
align-items: center;
gap: 8px;
}
.reply-comments {
display: flex;
flex-direction: column;
margin-top: 1em;
}
.reply-box {
margin-top: 10px;
display: flex;
flex-direction: column;
gap: 10px;
align-items: end;
position: relative;
width: 95%;
align-self: flex-end;
}
.reply-box textarea {
width: 100%;
padding: 10px;
border-radius: 4px;
border: 1px solid #ddd;
resize: none;
outline: none;
}
.reply-box button {
align-self: flex-end;
padding: 10px 20px;
border-radius: 4px;
border: none;
background-color: #070707;
color: white;
cursor: pointer;
}
.reply-box button:hover {
background-color: #2d2d2d;
}
</style>