fix:联调评论接口、新增流程管理列表模块页面

This commit is contained in:
liangdong
2026-01-08 18:34:05 +08:00
parent cbdc1231ce
commit 6d93092f10
24 changed files with 824 additions and 645 deletions

View File

@@ -189,7 +189,6 @@ $color-white: #fff;
.sub-list-controls {
display: flex;
align-items: center;
margin-top: 12px;
padding-left: 46px; // 与头像对齐的偏移量
.expand-line {
@@ -230,6 +229,12 @@ $color-white: #fff;
background-color: rgba(64, 158, 255, 0.2);
}
}
.observer-anchor{
text-align: center;
font-size: 12px;
color: #808080;
}
}
// 评论组件骨架屏

View File

@@ -51,14 +51,14 @@
<!-- 这个地方需要添加 查看更多-收起 -->
<div class="parent-node">
<name-avatar
:name="item.employee.name"
:name="item.employee.username"
:src="item.employee.avatar"
:size="36"
/>
<div class="node-main">
<!-- 当前用户信息展示 -->
<div class="user-info">
<span class="nickname">{{ item.employee.name }}</span>
<span class="nickname">{{ item.employee.username }}</span>
<span class="createTime">{{
formatTime(item.createTime)
}}</span>
@@ -78,30 +78,25 @@
link
class="delete-btn"
@click="deleteMainComment(item)"
v-if="currentUser.id === item?.employee.id"
v-if="currentUser.id === item?.employee.userId"
>
<el-icon><Delete /></el-icon>&nbsp;删除
</el-button>
</div>
<!-- 回复内容展示二级-子集评论内容 -->
<!-- 回复内容展示二级-子集评论内容) 通过id进行关联加载二级 -->
<div
v-if="item.children?.length || item.localReplies?.length"
v-if="item.childrenCount || item.localReplies?.length"
class="sub-container"
>
<!-- 临时数据 -->
<div
v-for="replies in [
...(item.localReplies || []),
...(item.showAllReplies
? item.children
: item.children.slice(0, 1)),
]"
v-for="replies in item.children"
:key="replies.id"
class="sub-node"
>
<name-avatar
:name="replies.employee.name"
:name="replies.employee.username"
:src="replies.employee.avatar"
:size="36"
/>
@@ -109,13 +104,12 @@
<div class="sub-header">
<div class="sub-user-info">
<span class="nickname">{{
replies.employee.name
replies.employee.username
}}</span>
<!-- TODO:这个地方判断 不是c-b这种回复就不展示 -->
<template v-if="replies.reply">
<template v-if="replies.replyId && replies.replyId !== replies.employee.userId">
<span class="reply-text">回复</span>
<span class="target-name"
>@{{ replies.reply.name }}</span
>@{{ replies.replyUser.username }}</span
>
</template>
</div>
@@ -136,7 +130,7 @@
<el-button
link
class="delete-btn"
v-if="currentUser.id === replies?.employee.id"
v-if="currentUser.id === replies?.employee.userId"
@click="deleteReply(replies, item)"
>
删除
@@ -152,7 +146,8 @@
<el-button
v-if="!item.showAllReplies"
link
@click="item.showAllReplies = true"
:loading="item.loading"
@click="handleExpand(item)"
>
展开 {{ item.childrenCount }} 条回复
<el-icon><ArrowDown /></el-icon>
@@ -162,7 +157,7 @@
v-else-if="item.children.length < item.childrenCount"
link
:loading="item.loading"
@click="loadReplies(item)"
@click="loadMoreReplies(item)"
>
更多
{{ item.childrenCount - item.children.length }} 条回复
@@ -173,8 +168,7 @@
v-if="item.showAllReplies"
link
@click="collapseReplies(item)"
>
收起 <el-icon><ArrowUp /></el-icon>
>收起 <el-icon><ArrowUp /></el-icon>
</el-button>
</div>
</div>
@@ -182,6 +176,11 @@
</div>
</div>
</template>
<!-- 滚动加载元素 -->
<div ref="loadMoreAnchor" class="observer-anchor">
<el-icon v-if="infinityLoading" class="is-loading" size="20"><Loading/></el-icon>
<span v-else-if="noMore">没有更多评论了</span>
</div>
</el-scrollbar>
</div>
@@ -219,6 +218,7 @@
<name-avatar
:name="item.name"
:size="24"
:src="item.avatar"
style="margin-right: 10px"
/>
<span class="mention-name">{{ item.name }}</span>
@@ -264,6 +264,11 @@ import { useUserStore } from "@/store";
import { useRelativeTime } from "@/hooks/useRelativeTime";
import { useUserSearch } from "./useUserSearch";
import { parseMention } from "./utils";
import {
getComment,
addReplyComment,
deleteComment,
} from "@/api/modules/Comment";
const { formatTime } = useRelativeTime();
const userStore = useUserStore();
const { t } = useI18n();
@@ -279,7 +284,7 @@ const props = defineProps({
queryParams: {
//外部传入的请求参数-获取评论
type: Object,
default: () => ({}),
default: () => ({ instanceId: 1, moduleId: 1 }),
},
});
const {
@@ -293,88 +298,22 @@ const {
} = useUserSearch((keyword, signal) => handleFetchSearch(keyword, signal));
// 评论业务逻辑
const activeReply = reactive({ parentId: null, targetName: "" }); // 点击reply回复的数据信息
const activeReply = reactive({
replyUserId: "",
parentId: null,
targetName: "",
}); // 点击reply回复的数据信息
const mainInput = ref("");
const loading = ref(true); //当前骨架屏显示
const expandingCount = ref(0); //展开收起统计
// 评论数据
const commentData = ref([
{
id: 1,
content: "这是我的测试评论数据信息@张三1 😄",
createTime: "2023-10-27T14:30:00",
mentions: [{
"id": 4,
"name": "张三1",
"start": 12,
"end": 16
}],
employee: {
id: 1,
name: "李星倩",
avatar: "",
},
childrenCount: 10,
children: [
{
content: "好的那我来测试下艾特人员信息@冯娜 @张三1 你们好啊",
mentions: [
{
id: 2,
name: "冯娜",
start: 14,
end: 17,
},
{
id: 4,
name: "张三1",
start: 18,
end: 22,
},
],
employee: {
id: 1,
name: "李星倩",
avatar: "",
},
reply: {
id: 1,
name: 1,
},
},
{
id: 102,
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2",
content: "收到,数据已入库,我马上看下。",
createTime: 1767604936684,
mentions: [{ userId: 2, name: "冯娜", start: 11, end: 15 }],
replyEmployee: {
id: 1,
name: "冯娜",
},
employee: {
id: 2,
name: "zhanghan",
avatar: "",
},
},
{
id: 103,
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2",
content: "收到,数据已入库,我马上看下。",
createTime: 1767604936684,
mentions: [{ userId: 2, name: "冯娜", start: 11, end: 15 }],
employee: {
id: 3,
name: "王五",
avatar: "",
},
},
],
},
]);
const commentData = ref([]);
// TODO:请求用户列表的接口函数
// 滚动加载
const infinityLoading = ref(false);
const loadMoreAnchor = ref(null);
const noMore = ref(false);
// FIXME:请求用户列表的接口函数
const handleFetchSearch = async (keyword, signal) => {
console.log("获取参数信息", keyword, signal);
await new Promise((resolve) => setTimeout(resolve, 300));
@@ -392,6 +331,22 @@ const handleFetchSearch = async (keyword, signal) => {
];
};
// 获取当前的的评论信息
const getCommentData = async (childItem) => {
const queryData = {
pageNo: 1,
pageSize: 20,
...props.queryParams,
...childItem,
};
try {
const response = await getComment(queryData);
return response;
} catch (error) {
console.log("comment error:", error);
}
};
// 点击回复插入 mentions 块
const handleKeyDown = (e) => {
if (e.key === "Backspace") {
@@ -412,19 +367,20 @@ const handleKeyDown = (e) => {
// 用户选中@圈人的操作
const onUserSelect = (user: any) => {
console.log("获取当前返回的用户信息:", user);
recordSelection(user);
};
// 回复
const openReply = (target, group) => {
// 1. 设置回复的目标关系
activeReply.groupId = group.id; // 根评论ID
activeReply.parentId = target.id; // 直接父级ID
activeReply.targetName = target.employee.name;
activeReply.groupId = target.rootId; // 根评论ID
activeReply.replyUserId = target.employee.userId; //回复-人的id
activeReply.parentId = target.parentId; // 直接父级ID
activeReply.targetName = target.employee.username; //回复人员的username
activeReply.id = target.id;
// 2. 沿用 @ 逻辑:自动在输入框插入 @某人
const mentionStr = `@${target.employee.name} `;
const mentionStr = `@${target.employee.username} `;
// 如果输入框里已经有内容了,在前面追加,否则直接赋值
if (!mainInput.value.includes(mentionStr)) {
@@ -433,19 +389,42 @@ const openReply = (target, group) => {
};
// 删除回复-删除评论
const deleteReply = (target, group) => {
const index = group.children.findIndex((item) => item.id === target.id);
group.children.splice(index, 1);
const deleteReply = async (target, group) => {
try {
// 删除成功
await deleteComment(target.id);
ElMessage.success("删除成功");
const index = group.children.findIndex((item) => item.id === target.id);
if (index !== -1) {
group.children.splice(index, 1);
if (group.childrenCount > 0) {
group.childrenCount--;
}
}
// 删除后如果子评论数量为0则隐藏所有回复 childrenCount为0时会自动隐藏展开操作
if (group.childrenCount === 0) {
group.showAllReplies = false;
}
} catch (error) {
console.log("删除失败", error);
}
};
// 删除主评论-以及所有的子评论
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();
const deleteMainComment = async (target) => {
try {
await deleteComment(target.id);
ElMessage.success("删除成功");
// 前端动态操作
const index = commentData.value.findIndex((item) => item.id === target.id);
if (index !== -1) {
commentData.value.splice(index, 1);
if (activeReply.groupId === target.id) {
cancelReply();
}
}
} catch (error) {
console.log("删除失败", error);
}
};
@@ -474,17 +453,18 @@ const handleSendComment = async () => {
let rawText = mainInput.value;
if (!rawText.trim()) return ElMessage.warning("内容不能为空");
let finalParentId = null;
let finalReplyId = null;
const expectedPrefix = `@${activeReply.targetName} `;
const isActuallyReply =
activeReply.parentId && rawText.startsWith(expectedPrefix);
activeReply.replyUserId && rawText.startsWith(expectedPrefix);
if (isActuallyReply) {
finalParentId = activeReply.parentId;
finalReplyId = activeReply.replyUserId;
rawText = rawText.slice(expectedPrefix.length);
} else {
finalParentId = null;
finalReplyId = null;
}
const type = finalParentId ? "reply" : "main"; //ui构造渲染判断
const type = finalReplyId ? "reply" : "main"; //ui构造渲染判断
// 构造 Payload
const mentionList: any[] = [];
const localCache = new Map();
@@ -513,24 +493,23 @@ const handleSendComment = async () => {
}
// 组装接口请求的参数
const params = {
content: rawText,
mentions: mentionList,
// TODO:如果是回复,带上关联 ID
reply: {
id: activeReply.groupId,
name: activeReply.groupId,
},
mentions: Object.keys(mentionList).length ? mentionList : null,
...props.queryParams,
};
console.log("获取传递给后端的数据信息:", params);
// 回复数据复制
if(type === "reply"){
params.rootId = activeReply.groupId ? activeReply.groupId : activeReply.id;
params.parentId = activeReply.parentId;
params.replyUserId = activeReply.replyUserId;
}
try {
// 请求后端接口提交数据
// const res = await api.postComment(params);
const res = await addReplyComment(params);
// 前端 UI 更新
updateUIAfterSend(type, params);
updateUIAfterSend(type, params, res);
// 清空输入框
cancelReply();
clearSelection();
@@ -541,13 +520,18 @@ const handleSendComment = async () => {
};
// 更新当前的UI层
const updateUIAfterSend = (type, params) => {
const updateUIAfterSend = (type, params, response) => {
const newComment = {
id: Date.now(), // TODO:这个地方需要替换为后端返回的真实id 用作后续的删除
id: response,
employee: {
...currentUser.value,
},
reply: params.reply,
replyId: params.replyUserId,
replyUser: {
userId: activeReply.replyUserId,
username: activeReply.targetName,
},
rootId: params.groupId,
content: params.content,
mentions: params.mentionList,
createTime: new Date().valueOf(),
@@ -559,87 +543,114 @@ const updateUIAfterSend = (type, params) => {
commentData.value.unshift(newComment);
} else {
//回复某人的数据渲染
const targetGroup = commentData.value.find((i) => i.id === params.reply.id);
const targetGroup = commentData.value.find(
(i) => i.id === params.rootId
); //获取返回的数据信息
if (targetGroup) {
newComment.replyTo = activeReply.targetName;
if (!targetGroup.localReplies) targetGroup.localReplies = [];
targetGroup.localReplies.unshift(newComment);
if (!targetGroup.children) targetGroup.children = [];
targetGroup.children.unshift(newComment);
targetGroup.childrenCount = targetGroup.children.length;
}
}
};
// TODO:展开
const MOCK_REPLIES_POOL = Array.from({ length: 20 }, (_, i) => ({
id: 200 + i,
content: `这是模拟的第 ${i + 1} 条回复内容,@张三 用于测试分页加载。`,
createTime: Date.now() - i * 100000,
mentions: [{ id: 1, name: "张三", start: 3, end: 10 }],
employee: {
id: 10 + i,
name: `同事${i + 1}`,
avatar: "",
},
reply: null,
}));
const loadReplies = async (item) => {
if (item.loading) return;
expandingCount.value++;
item.loading = true;
try {
// 后端获取最终的数据信息
// const res = await xxxxxx()
// 模拟延迟
await new Promise((resolve) => setTimeout(resolve, 800));
const res = [];
// 滚动加载数据
const setupObserver = () => {
const observer = new IntersectionObserver(
(entries) => {
// 如果探测器进入视口,且当前没在加载,且还有更多数据
if (
entries[0].isIntersecting &&
!infinityLoading.value &&
!noMore.value
) {
loadMainComments();
}
},
{
threshold: 0.1,
}
);
// 模拟的数据
const currentLength = item.children.length;
const pageSize = 3;
const nextBatch = MOCK_REPLIES_POOL.slice(
currentLength,
currentLength + pageSize
);
const combined = [
...(item.localReplies || []),
...res,
...item.children,
...nextBatch,
];
item.children = combined.filter(
(v, i, a) => a.findIndex((t) => t.id === v.id) === i
);
item.localReplies = [];
item.showAllReplies = true;
} catch (error) {
console.log("error", error);
} finally {
item.loading = false;
setTimeout(() => {
expandingCount.value--;
}, 100);
if (loadMoreAnchor.value) {
observer.observe(loadMoreAnchor.value);
}
};
// 收起
const collapseReplies = (item) => {
expandingCount.value++;
item.showAllReplies = false;
console.log("收起数据", expandingCount.value);
nextTick(() => {
const el = document.getElementById(`comment-${item.id}`);
el?.scrollIntoView({ behavior: "smooth", block: "nearest" });
setTimeout(() => {
expandingCount.value--;
}, 300);
});
const nextPage = ref(1);
const loadMainComments = async () => {
infinityLoading.value = true;
try {
const res = await getCommentData({ pageNo: nextPage.value });
const processedRecords = res.records.map(item => ({
...item,
children: item.children || [],
localReplies: [],
loading: false,
showAllReplies: false,
currentPage: 1
}));
commentData.value = [...commentData.value,...processedRecords];
if (nextPage.value >= res.totalPage) {
noMore.value = true;
return;
}
} finally {
infinityLoading.value = false;
}
};
// TODO:展开 加载二级内容(包含回复)
const PAGE_SIZE_MORE = 3;
const handleExpand = async (item) => {
item.loading = true;
try {
const response = await getCommentData({
rootId: item.id,
pageNo: 1,
pageSize: PAGE_SIZE_MORE,
});
item.children = response.records; // 填充数据
item.showAllReplies = true;
item.currentPage = 1; // 记录当前页码
} catch (error) {
console.error("加载回复失败", error);
} finally {
item.loading = false;
}
};
// 加载更多的页码
const loadMoreReplies = async (item) => {
item.loading = true;
const nextPage = (item.currentPage || 1) + 1;
try {
const res = await getCommentData({
rootId: item.id,
pageNum: nextPage,
pageSize: PAGE_SIZE_MORE,
});
// 将新数据追加到列表末尾
item.children = [...item.children, ...res];
item.currentPage = nextPage;
} finally {
item.loading = false;
}
};
// 收起
const collapseReplies = (item) => {
item.showAllReplies = false;
item.children = []; //清空数据
item.currentPage = 0; //重置页码
};
// 初始化 (骨架屏展示)
onMounted(() => {
setTimeout(() => {
loading.value = false;
}, 500);
}, 300);
setupObserver();
});
</script>