fix:评论完善加载逻辑
This commit is contained in:
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -80,6 +80,7 @@ declare module 'vue' {
|
||||
Xxxx: typeof import('./src/components/xxxx/index.vue')['default']
|
||||
}
|
||||
export interface GlobalDirectives {
|
||||
vInfiniteScroll: typeof import('element-plus/es')['ElInfiniteScroll']
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,12 @@ $color-white: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
.comment-loading-status{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
// 3. 子评论卡片样式
|
||||
.sub-container {
|
||||
|
||||
@@ -2,182 +2,191 @@
|
||||
<div class="comment-app">
|
||||
<!-- 评论列表 -->
|
||||
<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-scrollbar @end-reached="loadMore">
|
||||
<!-- 骨架屏 -->
|
||||
<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: 80px; height: 18px; margin-right: 10px"
|
||||
style="width: 100%; height: 16px; margin: 8px 0"
|
||||
/>
|
||||
<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"
|
||||
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>
|
||||
</div>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
</div>
|
||||
</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.employee.name"
|
||||
:src="item.employee.avatar"
|
||||
:size="36"
|
||||
/>
|
||||
<div class="node-main">
|
||||
<!-- 当前用户信息展示 -->
|
||||
<div class="user-info">
|
||||
<span class="nickname">{{ item.employee.name }}</span>
|
||||
<span class="createTime">{{
|
||||
formatTime(item.createTime)
|
||||
}}</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> 回复
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
class="delete-btn"
|
||||
@click="deleteMainComment(item)"
|
||||
v-if="currentUser.id === item?.employee.id"
|
||||
>
|
||||
<el-icon><Delete /></el-icon> 删除
|
||||
</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>
|
||||
<!-- 详细的内容展示 -->
|
||||
<template v-else>
|
||||
<div v-for="item in commentData" :key="item.id" class="comment-group">
|
||||
<!-- 这个地方需要添加 查看更多-收起 -->
|
||||
<div class="parent-node">
|
||||
<name-avatar
|
||||
:name="item.employee.name"
|
||||
:src="item.employee.avatar"
|
||||
:size="36"
|
||||
/>
|
||||
<div class="node-main">
|
||||
<!-- 当前用户信息展示 -->
|
||||
<div class="user-info">
|
||||
<span class="nickname">{{ item.employee.name }}</span>
|
||||
<span class="createTime">{{
|
||||
formatTime(item.createTime)
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- 展开更多和收起 -->
|
||||
<div v-if="item.childrenCount > 0" class="sub-list-controls">
|
||||
<div class="expand-line"></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> 回复
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="item.children.length < item.childrenCount"
|
||||
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> 删除
|
||||
</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 }} 条回复
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
v-else-if="item.children.length < item.childrenCount"
|
||||
link
|
||||
:loading="item.loading"
|
||||
@click="loadReplies(item)"
|
||||
>
|
||||
更多
|
||||
{{ item.childrenCount - item.children.length }} 条回复
|
||||
</template>
|
||||
<el-icon v-if="!loading"><ArrowDown /></el-icon>
|
||||
<el-icon v-else class="is-loading"><Loading /></el-icon>
|
||||
</el-button>
|
||||
<el-icon v-if="!item.loading"><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
v-if="item.children.length > 0"
|
||||
link
|
||||
@click="collapseReplies(item)"
|
||||
>
|
||||
收起
|
||||
<el-icon><ArrowUp /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="item.showAllReplies"
|
||||
link
|
||||
@click="collapseReplies(item)"
|
||||
>
|
||||
收起 <el-icon><ArrowUp /></el-icon>
|
||||
</el-button>
|
||||
</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>
|
||||
</template>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
|
||||
<!-- 评论模块 -->
|
||||
@@ -206,7 +215,7 @@
|
||||
<!-- <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>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button
|
||||
@@ -322,14 +331,6 @@ const openReply = (target, group) => {
|
||||
if (!mainInput.value.includes(mentionStr)) {
|
||||
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输入框选择
|
||||
const onSelectEmoji = (emoji, type) => {
|
||||
console.log("emoji", emoji, type);
|
||||
if (type === "main") {
|
||||
mainInput.value += emoji;
|
||||
}
|
||||
const onSelectEmoji = (emoji) => {
|
||||
mainInput.value += emoji;
|
||||
};
|
||||
|
||||
// 取消评论
|
||||
@@ -374,7 +372,7 @@ const userList = [
|
||||
|
||||
// TODO:@ 获取选中的用户信息
|
||||
const onUserMentioned = (user) => {
|
||||
console.log("Mentioned:", user.nickname);
|
||||
console.log("Mentioned:", user);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -470,15 +468,40 @@ const updateUIAfterSend = (type, params) => {
|
||||
};
|
||||
|
||||
// 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) => {
|
||||
if(!loadingReply.value) return;
|
||||
loadingReply.value = true;
|
||||
if (item.loading) return;
|
||||
item.loading = true;
|
||||
try {
|
||||
// 后端获取最终的数据信息
|
||||
// const res = await xxxxxx()
|
||||
// 模拟延迟
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
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(
|
||||
(v, i, a) => a.findIndex((t) => t.id === v.id) === i
|
||||
);
|
||||
@@ -487,14 +510,17 @@ const loadReplies = async (item) => {
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
item.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 收起
|
||||
const collapseReplies = (item) => {
|
||||
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;
|
||||
};
|
||||
|
||||
// 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(() => {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<el-form-item label="域名">
|
||||
<el-input v-model="form.domain" placeholder="example.com">
|
||||
<template #prefix
|
||||
><el-icon><component is="Global" /></el-icon
|
||||
><el-icon><component :is="'Global'" /></el-icon
|
||||
></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
@@ -81,9 +81,9 @@
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" class="btn-confirm" @click="handleSubmit"
|
||||
<div class="org-ganization-footer">
|
||||
<el-button @click="dialogVisible = false" round>取消</el-button>
|
||||
<el-button type="primary" class="btn-confirm" round @click="handleSubmit"
|
||||
>确认创建</el-button
|
||||
>
|
||||
</div>
|
||||
@@ -164,6 +164,15 @@ const handleSubmit = () => {};
|
||||
}
|
||||
}
|
||||
|
||||
.org-ganization-footer{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.el-button{
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -143,44 +143,7 @@ const addDynamicRoutes = async () => {
|
||||
// 从后端获取路由菜单数据 (这边需要区分 后台的菜单 和用户的菜单)
|
||||
let allRoutes:any[] = [];
|
||||
if (userStore.isBackendUser) {
|
||||
// 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
|
||||
}
|
||||
];
|
||||
const backendResponse = await getRouteMenus();
|
||||
allRoutes = [
|
||||
{
|
||||
code: "stage",
|
||||
|
||||
Reference in New Issue
Block a user