fix:完善字典管理功能优化全局组件
This commit is contained in:
539
src/modules/Comment/index.vue
Normal file
539
src/modules/Comment/index.vue
Normal file
@@ -0,0 +1,539 @@
|
||||
<template>
|
||||
<div class="comment-app">
|
||||
<section class="main-publisher">
|
||||
<div class="input-wrapper">
|
||||
<el-input
|
||||
v-model="mainInput"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
:placeholder="t('comment.placeholder')"
|
||||
resize="none"
|
||||
/>
|
||||
<div class="input-tools">
|
||||
<div class="left-icons">
|
||||
<!-- 提到-后续迭代 -->
|
||||
<!-- <el-button link title="提到">@</el-button> -->
|
||||
<!-- 图片功能-后续迭代 -->
|
||||
<!-- <el-button link title="图片"><el-icon><Picture /></el-icon></el-button> -->
|
||||
<!-- 附件功能-后续迭代 -->
|
||||
<!-- <el-button link title="附件"><el-icon><Paperclip /></el-icon></el-button> -->
|
||||
|
||||
<!-- 表情功能 -->
|
||||
<emoji-picker @select="(e) => onSelectEmoji(e, 'main')" />
|
||||
</div>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button
|
||||
class="send-btn"
|
||||
type="primary"
|
||||
link
|
||||
@click="submitMainComment"
|
||||
>
|
||||
<el-icon :size="20" :title="t('comment.send')"><Promotion /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="comment-list">
|
||||
<!-- 骨架屏 -->
|
||||
<div v-if="loading">
|
||||
<div v-for="n in 2" :key="'skeleton-' + n" class="comment-group">
|
||||
<el-skeleton :rows="1" :animated="true">
|
||||
<template #template>
|
||||
<div class="skeleton-comment-parent-node">
|
||||
<el-skeleton-item variant="circle" class="skeleton-avatar" />
|
||||
<div class="skeleton-node-main">
|
||||
<div class="skeleton-user-info">
|
||||
<el-skeleton-item
|
||||
variant="p"
|
||||
style="width: 80px; height: 18px; margin-right: 10px"
|
||||
/>
|
||||
<el-skeleton-item
|
||||
variant="p"
|
||||
style="width: 60px; height: 14px"
|
||||
/>
|
||||
</div>
|
||||
<el-skeleton-item
|
||||
variant="p"
|
||||
style="width: 100%; height: 16px; margin: 8px 0"
|
||||
/>
|
||||
<el-skeleton-item
|
||||
variant="p"
|
||||
style="width: 70%; height: 16px; margin-bottom: 12px"
|
||||
/>
|
||||
<div class="skeleton-actions">
|
||||
<el-skeleton-item
|
||||
variant="button"
|
||||
style="width: 40px; height: 24px; margin-right: 8px"
|
||||
/>
|
||||
<el-skeleton-item
|
||||
variant="button"
|
||||
style="width: 40px; height: 24px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 详细的内容展示 -->
|
||||
<template v-else>
|
||||
<div v-for="item in commentData" :key="item.id" class="comment-group">
|
||||
<div class="parent-node">
|
||||
<name-avatar :name="item.nickname" :src="item.avatar" :size="36" />
|
||||
<div class="node-main">
|
||||
<!-- 当前用户信息展示 -->
|
||||
<div class="user-info">
|
||||
<span class="nickname">{{ item.nickname }}</span>
|
||||
<span class="time">{{ item.time }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 回复内容模块 -->
|
||||
<div class="content" v-html="parseMention(item.content)"></div>
|
||||
<!-- 回复内容-子集内容操作模块 -->
|
||||
<div class="actions">
|
||||
<el-button link @click="openReply(item, item)">
|
||||
<el-icon><ChatDotSquare /></el-icon> 回复
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
class="delete-btn"
|
||||
@click="deleteMainComment(item)"
|
||||
v-if="item.canDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon> 删除
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 回复内容展示(二级-子集评论内容) -->
|
||||
<div v-if="item.children?.length" class="sub-container">
|
||||
<div
|
||||
v-for="reply in item.children"
|
||||
:key="reply.id"
|
||||
class="sub-node"
|
||||
>
|
||||
<name-avatar
|
||||
:name="reply.nickname"
|
||||
:src="reply.avatar"
|
||||
:size="36"
|
||||
/>
|
||||
<div class="sub-main">
|
||||
<div class="sub-header">
|
||||
<div class="sub-user-info">
|
||||
<span class="nickname">{{ reply.nickname }}</span>
|
||||
<span class="reply-text">回复</span>
|
||||
<span class="target-name">@{{ reply.replyTo }}</span>
|
||||
</div>
|
||||
<span class="time">{{ reply.time }}</span>
|
||||
</div>
|
||||
<div class="content-body">{{ reply.content }}</div>
|
||||
<!-- 回复 删除功能 -->
|
||||
<div class="actions">
|
||||
<el-button link @click="openReply(item, item)">
|
||||
回复
|
||||
</el-button>
|
||||
<!-- 删除功能-只有自己评论的可以删除 -->
|
||||
<el-button
|
||||
link
|
||||
class="delete-btn"
|
||||
v-if="reply.canDelete"
|
||||
@click="deleteReply(reply, item)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统一回复输入框 -->
|
||||
<div
|
||||
v-if="activeReply.groupId === item.id"
|
||||
class="inline-publisher"
|
||||
>
|
||||
<div class="input-wrapper">
|
||||
<el-input
|
||||
v-model="replyInput"
|
||||
:placeholder="`${t('comment.reply')} @${activeReply.targetName}...`"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||
resize="none"
|
||||
/>
|
||||
<div class="input-tools">
|
||||
<div class="left-icons">
|
||||
<!-- 表情功能 -->
|
||||
<emoji-picker
|
||||
@select="(e) => onSelectEmoji(e, 'reply')"
|
||||
/>
|
||||
</div>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button
|
||||
class="send-btn"
|
||||
type="primary"
|
||||
link
|
||||
@click="submitReply"
|
||||
>
|
||||
<el-icon :size="20" :title="t('comment.send')"><Promotion /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import EmojiPicker from "./EmojiPicker.vue";
|
||||
import {
|
||||
Picture,
|
||||
Paperclip,
|
||||
Promotion,
|
||||
ChatDotSquare,
|
||||
Delete,
|
||||
} from "@element-plus/icons-vue";
|
||||
import NameAvatar from "@/components/nameAvatar/index.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useUserStore } from "@/store";
|
||||
const userStore = useUserStore();
|
||||
const { t } = useI18n();
|
||||
// 当前用户信息
|
||||
const currentUser = computed(() => {
|
||||
return {
|
||||
nickname: userStore.userInfo.nickname,
|
||||
avatar: userStore.userInfo.avatar,
|
||||
};
|
||||
});
|
||||
|
||||
// 评论业务逻辑
|
||||
const activeReply = reactive({ parentId: null, targetName: "" });
|
||||
const mainInput = ref("");
|
||||
const replyInput = ref("");
|
||||
const loading = ref(true); //当前骨架屏显示
|
||||
const commentData = ref([
|
||||
{
|
||||
id: 1,
|
||||
nickname: "李星倩",
|
||||
avatar: "",
|
||||
content: "已完成ROI测算,请审核。",
|
||||
time: "10分钟前",
|
||||
canDelete: true,
|
||||
children: [
|
||||
{
|
||||
id: 101,
|
||||
nickname: "冯娜",
|
||||
replyTo: "李星倩",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2",
|
||||
content: "收到,数据已入库,我马上看下。",
|
||||
time: "刚刚",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
// 回复
|
||||
const openReply = (target, group) => {
|
||||
activeReply.groupId = group.id;
|
||||
activeReply.targetName = target.nickname;
|
||||
replyInput.value = "";
|
||||
};
|
||||
|
||||
// 删除回复-删除评论
|
||||
const deleteReply = (target, group) => {
|
||||
const index = group.children.findIndex((item) => item.id === target.id);
|
||||
group.children.splice(index, 1);
|
||||
};
|
||||
|
||||
// 删除主评论-以及所有的子评论
|
||||
const deleteMainComment = (target) => {
|
||||
const index = commentData.value.findIndex((item) => item.id === target.id);
|
||||
if (index !== -1) {
|
||||
commentData.value.splice(index, 1);
|
||||
if (activeReply.groupId === target.id) {
|
||||
cancelReply();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// emoji输入框选择
|
||||
const onSelectEmoji = (emoji, type) => {
|
||||
console.log("emoji", emoji, type);
|
||||
if (type === "main") {
|
||||
mainInput.value += emoji;
|
||||
} else {
|
||||
replyInput.value += emoji;
|
||||
}
|
||||
};
|
||||
|
||||
const cancelReply = () => {
|
||||
activeReply.groupId = null;
|
||||
};
|
||||
// @ 识别解析函数
|
||||
const parseMention = (text) => {
|
||||
if (!text) return "";
|
||||
// 基础转义
|
||||
let safeText = text.replace(/</g, "<").replace(/>/g, ">");
|
||||
// 正则匹配 @用户,包裹为 span
|
||||
return safeText.replace(
|
||||
/@([\u4e00-\u9fa5\w-]+)/g,
|
||||
'<span class="mention-link">@$1</span>'
|
||||
);
|
||||
};
|
||||
// @提及 圈人操作
|
||||
const handleMentionAction = (name) => {
|
||||
const mentionStr = typeof name === "string" ? `@${name} ` : "@";
|
||||
mainInput.value += mentionStr;
|
||||
};
|
||||
|
||||
const submitMainComment = () => {
|
||||
if (!mainInput.value.trim()) return ElMessage.warning("内容不能为空");
|
||||
commentData.value.unshift({
|
||||
id: Date.now(),
|
||||
...currentUser.value,
|
||||
content: mainInput.value,
|
||||
time: "刚刚",
|
||||
canDelete: true,
|
||||
children: [],
|
||||
});
|
||||
mainInput.value = "";
|
||||
};
|
||||
|
||||
const submitReply = () => {
|
||||
const targetGroup = commentData.value.find(
|
||||
(i) => i.id === activeReply.groupId
|
||||
);
|
||||
if (targetGroup) {
|
||||
targetGroup.children.push({
|
||||
id: Date.now(),
|
||||
...currentUser.value,
|
||||
replyTo: activeReply.targetName,
|
||||
content: replyInput.value,
|
||||
time: "刚刚",
|
||||
});
|
||||
cancelReply();
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
}, 500);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$color-blue: #409eff;
|
||||
$color-blue-bg: #f5f8ff;
|
||||
$color-text-main: #303133;
|
||||
$color-text-sub: #99a2aa;
|
||||
$color-border: #e4e7ed;
|
||||
$color-white: #fff;
|
||||
|
||||
.comment-app {
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
font-family: -apple-system, sans-serif;
|
||||
|
||||
// 1. 输入框样式
|
||||
.input-wrapper {
|
||||
border: 1px solid $color-border;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
position: relative;
|
||||
transition: border-color 0.2s;
|
||||
background-color: $color-white;
|
||||
|
||||
&:focus-within {
|
||||
border-color: $color-blue;
|
||||
}
|
||||
|
||||
:deep(.el-textarea__inner) {
|
||||
border: none;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.input-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
|
||||
.left-icons {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
.el-button {
|
||||
color: $color-text-sub;
|
||||
font-size: 18px;
|
||||
padding: 4px;
|
||||
}
|
||||
.el-button + .el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
margin-left: 8px;
|
||||
color: $color-blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 评论列表样式
|
||||
.comment-list {
|
||||
margin-top: 30px;
|
||||
|
||||
.comment-group {
|
||||
margin-bottom: 30px;
|
||||
|
||||
.parent-node {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
.user-avatar {
|
||||
background-color: $color-blue;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.node-main {
|
||||
flex: 1;
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
.nickname {
|
||||
font-weight: bold;
|
||||
color: $color-text-main;
|
||||
}
|
||||
.time {
|
||||
font-size: 13px;
|
||||
color: $color-text-sub;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
margin: 8px 0;
|
||||
font-size: 15px;
|
||||
color: $color-text-main;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
.el-button {
|
||||
font-size: 13px;
|
||||
color: $color-text-sub;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
&:hover {
|
||||
color: $color-blue;
|
||||
}
|
||||
}
|
||||
.delete-btn:hover {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 子评论卡片样式
|
||||
.sub-container {
|
||||
background-color: $color-blue-bg;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #d9e5ff;
|
||||
padding: 16px;
|
||||
margin-top: 10px;
|
||||
|
||||
.sub-node {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sub-main {
|
||||
flex: 1;
|
||||
.sub-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.sub-user-info {
|
||||
font-size: 14px;
|
||||
.nickname {
|
||||
font-weight: bold;
|
||||
}
|
||||
.reply-text {
|
||||
margin: 0 6px;
|
||||
color: $color-text-sub;
|
||||
}
|
||||
.target-name {
|
||||
color: $color-blue;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: $color-text-sub;
|
||||
}
|
||||
}
|
||||
.content-body {
|
||||
font-size: 14px;
|
||||
color: $color-text-main;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inline-publisher {
|
||||
margin-top: 15px;
|
||||
.inline-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 评论组件骨架屏
|
||||
.skeleton-comment-parent-node {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
.skeleton-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.skeleton-node-main {
|
||||
flex: 1;
|
||||
|
||||
.skeleton-user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.skeleton-actions {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user