feat:新增Comment评论组件
This commit is contained in:
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -12,6 +12,7 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
CardItem: typeof import('./src/components/cardItem/index.vue')['default']
|
||||
Comment: typeof import('./src/components/comment/index.vue')['default']
|
||||
CommonFilter: typeof import('./src/components/commonFilter/index.vue')['default']
|
||||
ElAside: typeof import('element-plus/es')['ElAside']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
|
||||
329
src/components/comment/index.vue
Normal file
329
src/components/comment/index.vue
Normal file
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<div class="comment-app">
|
||||
<section class="main-publisher">
|
||||
<el-input
|
||||
v-model="mainInput"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="发一条友善的评论吧..."
|
||||
resize="none"
|
||||
/>
|
||||
<div class="pub-footer">
|
||||
<div class="tools">
|
||||
<el-button link class="tool-item">
|
||||
<el-icon><Picture /></el-icon> 插入图片 (预留)
|
||||
</el-button>
|
||||
<el-button link class="tool-item">
|
||||
<span class="emoji-icon">😀</span> 表情 (预留)
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button type="primary" round @click="submitMainComment"
|
||||
>发布评论</el-button
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="comment-list">
|
||||
<div v-for="item in commentData" :key="item.id" class="comment-group">
|
||||
<div class="parent-node">
|
||||
<el-avatar :size="42" :src="item.avatar" />
|
||||
<div class="node-main">
|
||||
<div class="user-info">
|
||||
<span class="nickname">{{ item.nickname }}</span>
|
||||
<el-tag v-if="item.isOwner" size="small">作者</el-tag>
|
||||
</div>
|
||||
<div class="content">{{ item.content }}</div>
|
||||
<div class="node-footer">
|
||||
<span class="time">{{ item.time }}</span>
|
||||
<div class="actions">
|
||||
<el-button link @click="openReply(item, item)">回复</el-button>
|
||||
<el-button link>点赞</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="item.children?.length" class="sub-container">
|
||||
<div
|
||||
v-for="reply in item.children"
|
||||
:key="reply.id"
|
||||
class="sub-node"
|
||||
>
|
||||
<el-avatar :size="24" :src="reply.avatar" />
|
||||
<div class="sub-main">
|
||||
<div class="sub-text">
|
||||
<span class="sub-nickname">{{ reply.nickname }}</span>
|
||||
<template v-if="reply.replyTo !== item.nickname">
|
||||
<span class="reply-label">回复</span>
|
||||
<span class="target-name">@{{ reply.replyTo }}</span>
|
||||
</template>
|
||||
<span class="sep">:</span>
|
||||
<span class="content-body">{{ reply.content }}</span>
|
||||
</div>
|
||||
<div class="node-footer">
|
||||
<span class="time">{{ reply.time }}</span>
|
||||
<el-button link @click="openReply(reply, item)"
|
||||
>回复</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="activeReply.groupId === item.id"
|
||||
class="inline-publisher"
|
||||
>
|
||||
<el-input
|
||||
v-model="replyInput"
|
||||
:placeholder="`回复 @${activeReply.targetName}...`"
|
||||
type="textarea"
|
||||
autosize
|
||||
/>
|
||||
<div class="pub-footer">
|
||||
<div class="tools">
|
||||
<el-button link size="small">😀 表情</el-button>
|
||||
</div>
|
||||
<div class="btns">
|
||||
<el-button size="small" @click="cancelReply">取消</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
round
|
||||
@click="submitReply"
|
||||
>发布</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Picture } from "@element-plus/icons-vue";
|
||||
|
||||
// 1. 模拟当前登录用户
|
||||
const currentUser = {
|
||||
nickname: "前端练习生",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Lucky",
|
||||
};
|
||||
|
||||
// 2. 响应式数据
|
||||
const mainInput = ref("");
|
||||
const replyInput = ref("");
|
||||
const commentData = ref([
|
||||
{
|
||||
id: 1,
|
||||
nickname: "张三",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=1",
|
||||
content: "扁平化递归不仅性能好,在手机端显示也非常整齐!",
|
||||
time: "2小时前",
|
||||
isOwner: true,
|
||||
children: [
|
||||
{
|
||||
id: 101,
|
||||
nickname: "李四",
|
||||
replyTo: "张三",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2",
|
||||
content: "确实,无限缩进到后面根本没法看。",
|
||||
time: "1小时前",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
// 记录当前正在回复的状态
|
||||
const activeReply = reactive({
|
||||
groupId: null, // 所属的一级评论ID
|
||||
targetName: "", // 正在回复的具体人名
|
||||
targetId: null, // 正在回复的评论ID
|
||||
});
|
||||
|
||||
// 3. 逻辑方法
|
||||
const submitMainComment = () => {
|
||||
if (!mainInput.value.trim()) return ElMessage.warning("内容不能为空");
|
||||
|
||||
const newComment = {
|
||||
id: Date.now(),
|
||||
...currentUser,
|
||||
content: mainInput.value,
|
||||
time: "刚刚",
|
||||
children: [],
|
||||
};
|
||||
commentData.value.unshift(newComment);
|
||||
mainInput.value = "";
|
||||
ElMessage.success("发表成功");
|
||||
};
|
||||
|
||||
const openReply = (target, group) => {
|
||||
activeReply.groupId = group.id;
|
||||
activeReply.targetName = target.nickname;
|
||||
activeReply.targetId = target.id;
|
||||
replyInput.value = "";
|
||||
};
|
||||
|
||||
const cancelReply = () => {
|
||||
activeReply.groupId = null;
|
||||
};
|
||||
|
||||
const submitReply = () => {
|
||||
if (!replyInput.value.trim()) return ElMessage.warning("回复内容不能为空");
|
||||
|
||||
const targetGroup = commentData.value.find(
|
||||
(item) => item.id === activeReply.groupId
|
||||
);
|
||||
if (targetGroup) {
|
||||
const newReply = {
|
||||
id: Date.now(),
|
||||
...currentUser,
|
||||
replyTo: activeReply.targetName,
|
||||
content: replyInput.value,
|
||||
time: "刚刚",
|
||||
};
|
||||
targetGroup.children.push(newReply);
|
||||
ElMessage.success("回复成功");
|
||||
cancelReply();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$color-primary: #409eff;
|
||||
$color-bg-sub: #f5f7fa;
|
||||
$color-text-main: #303133;
|
||||
$color-text-sub: #909399;
|
||||
$border-color: #ebeef5;
|
||||
|
||||
.comment-app {
|
||||
max-width: 760px;
|
||||
margin: 40px auto;
|
||||
padding: 0 20px;
|
||||
|
||||
// 发布区域公共样式
|
||||
.main-publisher,
|
||||
.inline-publisher {
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
|
||||
.pub-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
|
||||
.tool-item {
|
||||
color: $color-text-sub;
|
||||
font-size: 14px;
|
||||
.emoji-icon {
|
||||
font-size: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inline-publisher {
|
||||
margin-top: 16px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
.comment-list {
|
||||
margin-top: 32px;
|
||||
|
||||
.comment-group {
|
||||
margin-bottom: 28px;
|
||||
|
||||
.parent-node {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
|
||||
.node-main {
|
||||
flex: 1;
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.nickname {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: $color-text-main;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
margin: 8px 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
color: $color-text-main;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 底部信息和动作
|
||||
.node-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
font-size: 13px;
|
||||
color: $color-text-sub;
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
.el-button {
|
||||
font-size: 13px;
|
||||
color: $color-text-sub;
|
||||
&:hover {
|
||||
color: $color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 扁平化子评论容器
|
||||
.sub-container {
|
||||
background-color: $color-bg-sub;
|
||||
border-radius: 6px;
|
||||
padding: 4px 12px 12px;
|
||||
margin-top: 12px;
|
||||
|
||||
.sub-node {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
|
||||
.sub-main {
|
||||
flex: 1;
|
||||
.sub-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
.sub-nickname {
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
.reply-label {
|
||||
margin: 0 4px;
|
||||
color: $color-text-sub;
|
||||
font-size: 13px;
|
||||
}
|
||||
.target-name {
|
||||
color: $color-primary;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.content-body {
|
||||
color: $color-text-main;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -91,12 +91,12 @@
|
||||
pagination
|
||||
height="calc(100vh - 186px)"
|
||||
:immediate="false"
|
||||
row-key="id"
|
||||
lazy
|
||||
:load="load"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
:request-api="fetchData"
|
||||
>
|
||||
<!-- 名称点击 -->
|
||||
<template #labelName="{ row }">
|
||||
<el-button link type="primary" @click="onLevelNext">{{ row.label }}</el-button>
|
||||
</template>
|
||||
<!-- 状态插槽 -->
|
||||
<template #status="{ row }">
|
||||
<div
|
||||
@@ -178,6 +178,7 @@ const columns = [
|
||||
prop: "label",
|
||||
label: "字典名称",
|
||||
align: "center",
|
||||
slot: "labelName",
|
||||
},
|
||||
{
|
||||
prop: "value",
|
||||
@@ -209,6 +210,11 @@ const columns = [
|
||||
{ prop: "actions", label: "操作", align: "right", width: "300" },
|
||||
];
|
||||
|
||||
// 点击获取二级菜单数据
|
||||
const onLevelNext = () =>{
|
||||
console.log('next')
|
||||
}
|
||||
|
||||
// 请求数据信息
|
||||
const fetchData = async (params) => {
|
||||
try {
|
||||
@@ -217,14 +223,6 @@ const fetchData = async (params) => {
|
||||
keyword: searchQuery.value,
|
||||
...filterForm,
|
||||
});
|
||||
|
||||
if (response.records) {
|
||||
response.records = response.records.map((item) => ({
|
||||
...item,
|
||||
children: item.children || [],
|
||||
hasChildren: true, // 设置为true加载子集数据信息
|
||||
}));
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.log("getTableData Error", error);
|
||||
@@ -251,8 +249,8 @@ const load = async (
|
||||
resolve: (data: User[]) => void
|
||||
) => {
|
||||
try {
|
||||
const resp = await getNextDictMenu(parentId.value,row.id);
|
||||
console.log("获取当前返回的二级数据信息==>:",resp);
|
||||
const resp = await getNextDictMenu(parentId.value, row.id);
|
||||
console.log("获取当前返回的二级数据信息==>:", resp);
|
||||
resolve([]);
|
||||
} catch (error) {
|
||||
resolve([]);
|
||||
@@ -270,11 +268,10 @@ const onConfirmSuccess = () => {
|
||||
tableRef.value && tableRef.value.refresh();
|
||||
};
|
||||
|
||||
|
||||
// 关闭popover 重置数据信息
|
||||
const onCloseFilter = () =>{
|
||||
const onCloseFilter = () => {
|
||||
filterForm.status = "";
|
||||
}
|
||||
};
|
||||
|
||||
// 筛选重置
|
||||
const onReset = () => {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<template>
|
||||
<div class="">
|
||||
|
||||
</div>
|
||||
<div class="">
|
||||
<Comment />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {reactive,ref,onMounted} from "vue"
|
||||
import Comment from "@/components/comment/index.vue";
|
||||
import { reactive, ref, onMounted } from "vue";
|
||||
|
||||
defineOptions({})
|
||||
defineOptions({ name: "Personnel" });
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
Reference in New Issue
Block a user