fix:联调评论接口、新增流程管理列表模块页面
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
// 评论组件骨架屏
|
||||
|
||||
@@ -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> 删除
|
||||
</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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user