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' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
CardItem: typeof import('./src/components/cardItem/index.vue')['default']
|
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']
|
CommonFilter: typeof import('./src/components/commonFilter/index.vue')['default']
|
||||||
ElAside: typeof import('element-plus/es')['ElAside']
|
ElAside: typeof import('element-plus/es')['ElAside']
|
||||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
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
|
pagination
|
||||||
height="calc(100vh - 186px)"
|
height="calc(100vh - 186px)"
|
||||||
:immediate="false"
|
:immediate="false"
|
||||||
row-key="id"
|
|
||||||
lazy
|
|
||||||
:load="load"
|
|
||||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
|
||||||
:request-api="fetchData"
|
:request-api="fetchData"
|
||||||
>
|
>
|
||||||
|
<!-- 名称点击 -->
|
||||||
|
<template #labelName="{ row }">
|
||||||
|
<el-button link type="primary" @click="onLevelNext">{{ row.label }}</el-button>
|
||||||
|
</template>
|
||||||
<!-- 状态插槽 -->
|
<!-- 状态插槽 -->
|
||||||
<template #status="{ row }">
|
<template #status="{ row }">
|
||||||
<div
|
<div
|
||||||
@@ -178,6 +178,7 @@ const columns = [
|
|||||||
prop: "label",
|
prop: "label",
|
||||||
label: "字典名称",
|
label: "字典名称",
|
||||||
align: "center",
|
align: "center",
|
||||||
|
slot: "labelName",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: "value",
|
prop: "value",
|
||||||
@@ -209,6 +210,11 @@ const columns = [
|
|||||||
{ prop: "actions", label: "操作", align: "right", width: "300" },
|
{ prop: "actions", label: "操作", align: "right", width: "300" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 点击获取二级菜单数据
|
||||||
|
const onLevelNext = () =>{
|
||||||
|
console.log('next')
|
||||||
|
}
|
||||||
|
|
||||||
// 请求数据信息
|
// 请求数据信息
|
||||||
const fetchData = async (params) => {
|
const fetchData = async (params) => {
|
||||||
try {
|
try {
|
||||||
@@ -217,14 +223,6 @@ const fetchData = async (params) => {
|
|||||||
keyword: searchQuery.value,
|
keyword: searchQuery.value,
|
||||||
...filterForm,
|
...filterForm,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.records) {
|
|
||||||
response.records = response.records.map((item) => ({
|
|
||||||
...item,
|
|
||||||
children: item.children || [],
|
|
||||||
hasChildren: true, // 设置为true加载子集数据信息
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("getTableData Error", error);
|
console.log("getTableData Error", error);
|
||||||
@@ -270,11 +268,10 @@ const onConfirmSuccess = () => {
|
|||||||
tableRef.value && tableRef.value.refresh();
|
tableRef.value && tableRef.value.refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// 关闭popover 重置数据信息
|
// 关闭popover 重置数据信息
|
||||||
const onCloseFilter = () => {
|
const onCloseFilter = () => {
|
||||||
filterForm.status = "";
|
filterForm.status = "";
|
||||||
}
|
};
|
||||||
|
|
||||||
// 筛选重置
|
// 筛选重置
|
||||||
const onReset = () => {
|
const onReset = () => {
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="">
|
<div class="">
|
||||||
|
<Comment />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<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>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user