fix:完善字典管理功能优化全局组件

This commit is contained in:
liangdong
2026-01-05 17:14:09 +08:00
parent bae034d6eb
commit 98c941e60c
26 changed files with 1225 additions and 563 deletions

View 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>&nbsp;回复
</el-button>
<el-button
link
class="delete-btn"
@click="deleteMainComment(item)"
v-if="item.canDelete"
>
<el-icon><Delete /></el-icon>&nbsp;删除
</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, "&lt;").replace(/>/g, "&gt;");
// 正则匹配 @用户,包裹为 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>