fix:评论完善加载逻辑

This commit is contained in:
liangdong
2026-01-06 21:09:10 +08:00
parent c21e778036
commit b7ca434172
5 changed files with 271 additions and 216 deletions

1
components.d.ts vendored
View File

@@ -80,6 +80,7 @@ declare module 'vue' {
Xxxx: typeof import('./src/components/xxxx/index.vue')['default'] Xxxx: typeof import('./src/components/xxxx/index.vue')['default']
} }
export interface GlobalDirectives { export interface GlobalDirectives {
vInfiniteScroll: typeof import('element-plus/es')['ElInfiniteScroll']
vLoading: typeof import('element-plus/es')['ElLoadingDirective'] vLoading: typeof import('element-plus/es')['ElLoadingDirective']
} }
} }

View File

@@ -128,6 +128,12 @@ $color-white: #fff;
} }
} }
} }
.comment-loading-status{
display: flex;
justify-content: center;
align-items: center;
padding: 5px 0;
}
// 3. 子评论卡片样式 // 3. 子评论卡片样式
.sub-container { .sub-container {

View File

@@ -2,182 +2,191 @@
<div class="comment-app"> <div class="comment-app">
<!-- 评论列表 --> <!-- 评论列表 -->
<div class="comment-list"> <div class="comment-list">
<!-- 骨架屏 --> <el-scrollbar @end-reached="loadMore">
<div v-if="loading"> <!-- 骨架屏 -->
<div v-for="n in 2" :key="'skeleton-' + n" class="comment-group"> <div v-if="loading">
<el-skeleton :rows="1" :animated="true"> <div v-for="n in 2" :key="'skeleton-' + n" class="comment-group">
<template #template> <el-skeleton :rows="1" :animated="true">
<div class="skeleton-comment-parent-node"> <template #template>
<el-skeleton-item variant="circle" class="skeleton-avatar" /> <div class="skeleton-comment-parent-node">
<div class="skeleton-node-main"> <el-skeleton-item variant="circle" class="skeleton-avatar" />
<div class="skeleton-user-info"> <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 <el-skeleton-item
variant="p" variant="p"
style="width: 80px; height: 18px; margin-right: 10px" style="width: 100%; height: 16px; margin: 8px 0"
/> />
<el-skeleton-item <el-skeleton-item
variant="p" variant="p"
style="width: 60px; height: 14px" style="width: 70%; height: 16px; margin-bottom: 12px"
/>
</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 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>
</div> </div>
</div> </template>
</template> </el-skeleton>
</el-skeleton> </div>
</div> </div>
</div> <!-- 详细的内容展示 -->
<!-- 详细的内容展示 --> <template v-else>
<template v-else> <div v-for="item in commentData" :key="item.id" class="comment-group">
<div v-for="item in commentData" :key="item.id" class="comment-group"> <!-- 这个地方需要添加 查看更多-收起 -->
<!-- 这个地方需要添加 查看更多-收起 --> <div class="parent-node">
<div class="parent-node"> <name-avatar
<name-avatar :name="item.employee.name"
:name="item.employee.name" :src="item.employee.avatar"
:src="item.employee.avatar" :size="36"
:size="36" />
/> <div class="node-main">
<div class="node-main"> <!-- 当前用户信息展示 -->
<!-- 当前用户信息展示 --> <div class="user-info">
<div class="user-info"> <span class="nickname">{{ item.employee.name }}</span>
<span class="nickname">{{ item.employee.name }}</span> <span class="createTime">{{
<span class="createTime">{{ formatTime(item.createTime)
formatTime(item.createTime) }}</span>
}}</span>
</div>
<!-- 回复内容模块 -->
<div
class="content"
v-html="parseMention(item.content, item.mentions)"
></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="currentUser.id === item?.employee.id"
>
<el-icon><Delete /></el-icon>&nbsp;删除
</el-button>
</div>
<!-- 回复内容展示二级-子集评论内容 -->
<div
v-if="item.children?.length || item.localReplies?.length"
class="sub-container"
>
<!-- 临时数据 -->
<div
v-for="replies in [
...(item.localReplies || []),
...(item.showAllReplies
? item.children
: item.children.slice(0, 1)),
]"
:key="replies.id"
class="sub-node"
>
<name-avatar
:name="replies.employee.name"
:src="replies.employee.avatar"
:size="36"
/>
<div class="sub-main">
<div class="sub-header">
<div class="sub-user-info">
<span class="nickname">{{
replies.employee.name
}}</span>
<!-- TODO:这个地方判断 不是c-b这种回复就不展示 -->
<template v-if="replies.reply">
<span class="reply-text">回复</span>
<span class="target-name"
>@{{ replies.reply.name }}</span
>
</template>
</div>
<span class="createTime">{{
formatTime(replies.createTime)
}}</span>
</div>
<div
class="content-body"
v-html="parseMention(replies.content, replies.mentions)"
></div>
<!-- 回复 删除功能 -->
<div class="actions">
<el-button link @click="openReply(replies, item)">
回复
</el-button>
<!-- 删除功能-只有自己评论的可以删除 -->
<el-button
link
class="delete-btn"
v-if="currentUser.id === replies?.employee.id"
@click="deleteReply(replies, item)"
>
删除
</el-button>
</div>
</div>
</div> </div>
<!-- 展开更多和收起 --> <!-- 回复内容模块 -->
<div v-if="item.childrenCount > 0" class="sub-list-controls"> <div
<div class="expand-line"></div> class="content"
v-html="parseMention(item.content, item.mentions)"
></div>
<!-- 回复内容-子集内容操作模块 -->
<div class="actions">
<el-button link @click="openReply(item, item)">
<el-icon><ChatDotSquare /></el-icon>&nbsp;回复
</el-button>
<el-button <el-button
v-if="item.children.length < item.childrenCount"
link link
@click="loadReplies(item)" class="delete-btn"
@click="deleteMainComment(item)"
v-if="currentUser.id === item?.employee.id"
> >
<template v-if="item.children.length === 0"> <el-icon><Delete /></el-icon>&nbsp;删除
</el-button>
</div>
<!-- 回复内容展示二级-子集评论内容 -->
<div
v-if="item.children?.length || item.localReplies?.length"
class="sub-container"
>
<!-- 临时数据 -->
<div
v-for="replies in [
...(item.localReplies || []),
...(item.showAllReplies
? item.children
: item.children.slice(0, 1)),
]"
:key="replies.id"
class="sub-node"
>
<name-avatar
:name="replies.employee.name"
:src="replies.employee.avatar"
:size="36"
/>
<div class="sub-main">
<div class="sub-header">
<div class="sub-user-info">
<span class="nickname">{{
replies.employee.name
}}</span>
<!-- TODO:这个地方判断 不是c-b这种回复就不展示 -->
<template v-if="replies.reply">
<span class="reply-text">回复</span>
<span class="target-name"
>@{{ replies.reply.name }}</span
>
</template>
</div>
<span class="createTime">{{
formatTime(replies.createTime)
}}</span>
</div>
<div
class="content-body"
v-html="parseMention(replies.content, replies.mentions)"
></div>
<!-- 回复 删除功能 -->
<div class="actions">
<el-button link @click="openReply(replies, item)">
回复
</el-button>
<!-- 删除功能-只有自己评论的可以删除 -->
<el-button
link
class="delete-btn"
v-if="currentUser.id === replies?.employee.id"
@click="deleteReply(replies, item)"
>
删除
</el-button>
</div>
</div>
</div>
<!-- 展开更多和收起 -->
<div v-if="item.childrenCount > 0" class="sub-list-controls">
<div class="expand-line"></div>
<el-button
v-if="!item.showAllReplies"
link
@click="item.showAllReplies = true"
>
展开 {{ item.childrenCount }} 条回复 展开 {{ item.childrenCount }} 条回复
</template> <el-icon><ArrowDown /></el-icon>
<template v-else> </el-button>
<el-button
v-else-if="item.children.length < item.childrenCount"
link
:loading="item.loading"
@click="loadReplies(item)"
>
更多 更多
{{ item.childrenCount - item.children.length }} 条回复 {{ item.childrenCount - item.children.length }} 条回复
</template> <el-icon v-if="!item.loading"><ArrowDown /></el-icon>
<el-icon v-if="!loading"><ArrowDown /></el-icon> </el-button>
<el-icon v-else class="is-loading"><Loading /></el-icon>
</el-button>
<el-button <el-button
v-if="item.children.length > 0" v-if="item.showAllReplies"
link link
@click="collapseReplies(item)" @click="collapseReplies(item)"
> >
收起 收起 <el-icon><ArrowUp /></el-icon>
<el-icon><ArrowUp /></el-icon> </el-button>
</el-button> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template>
<div v-if="loadingMore" class="comment-loading-status">
<el-icon class="is-loading" :size="24"><Loading /></el-icon>
</div> </div>
</template> </el-scrollbar>
</div> </div>
<!-- 评论模块 --> <!-- 评论模块 -->
@@ -206,7 +215,7 @@
<!-- <el-button link title="附件"><el-icon><Paperclip /></el-icon></el-button> --> <!-- <el-button link title="附件"><el-icon><Paperclip /></el-icon></el-button> -->
<!-- 表情功能 --> <!-- 表情功能 -->
<emoji-picker @select="(e) => onSelectEmoji(e, 'main')" /> <emoji-picker @select="(e) => onSelectEmoji(e)" />
</div> </div>
<el-divider direction="vertical" /> <el-divider direction="vertical" />
<el-button <el-button
@@ -322,14 +331,6 @@ const openReply = (target, group) => {
if (!mainInput.value.includes(mentionStr)) { if (!mainInput.value.includes(mentionStr)) {
mainInput.value = mentionStr + mainInput.value; mainInput.value = mentionStr + mainInput.value;
} }
// 3. 聚焦到输入框
nextTick(() => {
const textarea = document.querySelector(".fixed-publisher textarea");
textarea?.focus();
// 飞书细节:光标移到最后
textarea.selectionStart = textarea.value.length;
});
}; };
// 删除回复-删除评论 // 删除回复-删除评论
@@ -350,11 +351,8 @@ const deleteMainComment = (target) => {
}; };
// emoji输入框选择 // emoji输入框选择
const onSelectEmoji = (emoji, type) => { const onSelectEmoji = (emoji) => {
console.log("emoji", emoji, type); mainInput.value += emoji;
if (type === "main") {
mainInput.value += emoji;
}
}; };
// 取消评论 // 取消评论
@@ -374,7 +372,7 @@ const userList = [
// TODO:@ 获取选中的用户信息 // TODO:@ 获取选中的用户信息
const onUserMentioned = (user) => { const onUserMentioned = (user) => {
console.log("Mentioned:", user.nickname); console.log("Mentioned:", user);
}; };
/** /**
@@ -470,15 +468,40 @@ const updateUIAfterSend = (type, params) => {
}; };
// TODO:展开 // TODO:展开
const loadingReply = ref(false); const MOCK_REPLIES_POOL = Array.from({ length: 20 }, (_, i) => ({
id: 200 + i,
content: `这是模拟的第 ${i + 1} 条回复内容,用于测试分页加载。`,
createTime: Date.now() - i * 100000,
employee: {
id: 10 + i,
name: `同事${i + 1}`,
avatar: "",
},
reply: null,
}));
const loadReplies = async (item) => { const loadReplies = async (item) => {
if(!loadingReply.value) return; if (item.loading) return;
loadingReply.value = true; item.loading = true;
try { try {
// 后端获取最终的数据信息 // 后端获取最终的数据信息
// const res = await xxxxxx() // const res = await xxxxxx()
// 模拟延迟
await new Promise((resolve) => setTimeout(resolve, 800));
const res = []; const res = [];
const combined = [...(item.localReplies || []), ...res, ...item.children];
// 模拟的数据
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( item.children = combined.filter(
(v, i, a) => a.findIndex((t) => t.id === v.id) === i (v, i, a) => a.findIndex((t) => t.id === v.id) === i
); );
@@ -487,14 +510,17 @@ const loadReplies = async (item) => {
} catch (error) { } catch (error) {
console.log("error", error); console.log("error", error);
} finally { } finally {
loading.value = false; item.loading = false;
} }
}; };
// 收起 // 收起
const collapseReplies = (item) => { const collapseReplies = (item) => {
item.showAllReplies = false; item.showAllReplies = false;
// nextTick(() => scrollIntoView(item.id)); nextTick(() => {
const el = document.getElementById(`comment-${item.id}`);
el?.scrollIntoView({ behavior: "smooth", block: "nearest" });
});
}; };
/** /**
@@ -523,6 +549,56 @@ const parseMention = (text, atUsers = userList) => {
return result; return result;
}; };
// FIXME:加载更多
const page = ref(1);
const loadingMore = ref(false); // 分页加载状态
const noMore = ref(false); // 是否全部加载完毕
const totalCount = ref(20); // 模拟后端返回的总评论数
const disabled = computed(() => loadingMore.value || noMore.value);
const loadMore = async () => {
if (disabled.value) return;
loadingMore.value = true;
// 模拟接口请求
await new Promise((resolve) => setTimeout(resolve, 1000));
try {
// 模拟构造下一页 Mock 数据
const nextBatch = [
{
id: Date.now(),
content: `这是第 ${page.value + 1} 页的评论内容`,
createTime: new Date().toISOString(),
employee: { id: 88, name: "滚动测试员", avatar: "" },
childrenCount: 2,
children: [],
},
{
id: Date.now() + 1,
content: "多拉出一条数据",
createTime: new Date().toISOString(),
employee: { id: 89, name: "张三", avatar: "" },
childrenCount: 0,
children: [],
},
];
// 合并数据
commentData.value.push(...nextBatch);
page.value++;
// 判断是否加载完(模拟)
if (commentData.value.length >= totalCount.value) {
noMore.value = true;
}
} catch (error) {
console.error("加载失败", error);
} finally {
loadingMore.value = false;
}
};
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
setTimeout(() => { setTimeout(() => {

View File

@@ -29,7 +29,7 @@
<el-form-item label="域名"> <el-form-item label="域名">
<el-input v-model="form.domain" placeholder="example.com"> <el-input v-model="form.domain" placeholder="example.com">
<template #prefix <template #prefix
><el-icon><component is="Global" /></el-icon ><el-icon><component :is="'Global'" /></el-icon
></template> ></template>
</el-input> </el-input>
</el-form-item> </el-form-item>
@@ -81,9 +81,9 @@
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="org-ganization-footer">
<el-button @click="dialogVisible = false">取消</el-button> <el-button @click="dialogVisible = false" round>取消</el-button>
<el-button type="primary" class="btn-confirm" @click="handleSubmit" <el-button type="primary" class="btn-confirm" round @click="handleSubmit"
>确认创建</el-button >确认创建</el-button
> >
</div> </div>
@@ -164,6 +164,15 @@ const handleSubmit = () => {};
} }
} }
.org-ganization-footer{
display: flex;
align-items: center;
justify-content: center;
.el-button{
width: 50%;
}
}
} }
} }
</style> </style>

View File

@@ -143,44 +143,7 @@ const addDynamicRoutes = async () => {
// 从后端获取路由菜单数据 (这边需要区分 后台的菜单 和用户的菜单) // 从后端获取路由菜单数据 (这边需要区分 后台的菜单 和用户的菜单)
let allRoutes:any[] = []; let allRoutes:any[] = [];
if (userStore.isBackendUser) { if (userStore.isBackendUser) {
// const backendResponse = await getRouteMenus(); const backendResponse = await getRouteMenus();
const backendResponse = [
{
"name": "字典管理",
"code": "dict",
"icon": "OfficeBuilding",
"metadata": null,
"children": null
},
{
"name": "组织管理",
"code": "origanization",
"icon": "OfficeBuilding",
"metadata": null,
"children": null
},
{
"name": "人员管理",
"code": "personnel",
"icon": "OfficeBuilding",
"metadata": null,
"children": null
},
{
"name": "权限管理",
"code": "permission",
"icon": "OfficeBuilding",
"metadata": null,
"children": null
},
{
"name": "流程管理",
"code": "flow",
"icon": "OfficeBuilding",
"metadata": null,
"children": null
}
];
allRoutes = [ allRoutes = [
{ {
code: "stage", code: "stage",