feat: lazy load reply
This commit is contained in:
parent
cd511f5ede
commit
dbdcfa84fd
|
@ -44,6 +44,11 @@ interface CommentDraft {
|
|||
content: string
|
||||
}
|
||||
|
||||
// 获取回复的请求
|
||||
interface ReplyRequest {
|
||||
sortField: string
|
||||
sortOrder: 'asc' | 'desc'
|
||||
}
|
||||
// 话题页面的 store
|
||||
interface Topic {
|
||||
// 是否正在被编辑
|
||||
|
@ -56,7 +61,7 @@ interface Topic {
|
|||
// 回复的缓存
|
||||
replyDraft: ReplyDraft
|
||||
// 获取回复的请求接口格式
|
||||
replyRequest: TopicReplyRequestData
|
||||
replyRequest: ReplyRequest
|
||||
|
||||
// 评论的缓存
|
||||
commentDraft: CommentDraft
|
||||
|
@ -81,11 +86,8 @@ export const useKUNGalgameTopicStore = defineStore({
|
|||
publishStatus: false,
|
||||
},
|
||||
replyRequest: {
|
||||
tid: 0,
|
||||
page: 1,
|
||||
limit: 5,
|
||||
sortField: '',
|
||||
sortOrder: '',
|
||||
sortOrder: 'asc',
|
||||
},
|
||||
commentDraft: {
|
||||
c_uid: 0,
|
||||
|
@ -135,13 +137,17 @@ export const useKUNGalgameTopicStore = defineStore({
|
|||
})
|
||||
},
|
||||
// 获取回复
|
||||
getReplies(tid: number): Promise<TopicReplyResponseData> {
|
||||
getReplies(
|
||||
tid: number,
|
||||
page?: number,
|
||||
limit?: number
|
||||
): Promise<TopicReplyResponseData> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 这里的默认值用于初始化
|
||||
const requestData: TopicReplyRequestData = {
|
||||
tid: tid,
|
||||
page: this.replyRequest.page || 1,
|
||||
limit: this.replyRequest.limit || 5,
|
||||
page: page || 1,
|
||||
limit: limit || 5,
|
||||
sortField: this.replyRequest.sortField || 'floor',
|
||||
sortOrder: this.replyRequest.sortOrder || 'desc',
|
||||
}
|
||||
|
|
|
@ -9,8 +9,11 @@ import {
|
|||
onBeforeMount,
|
||||
computed,
|
||||
watch,
|
||||
onBeforeUnmount,
|
||||
} from 'vue'
|
||||
import { onBeforeRouteLeave } from 'vue-router'
|
||||
import { onBeforeRouteLeave, useRoute } from 'vue-router'
|
||||
|
||||
import { TopicDetail, TopicReply } from '@/api'
|
||||
|
||||
// Aside
|
||||
import Aside from './aside/Aside.vue'
|
||||
|
@ -25,13 +28,9 @@ const ReplyPanel = defineAsyncComponent(
|
|||
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
||||
// 导入话题页面 store
|
||||
import { useKUNGalgameTopicStore } from '@/store/modules/topic'
|
||||
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import { TopicDetail, TopicReply } from '@/api'
|
||||
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
// 当前的路由
|
||||
const route = useRoute()
|
||||
|
||||
// 使用设置面板的 store
|
||||
|
@ -51,18 +50,20 @@ const tid = computed(() => {
|
|||
const topicData = ref<TopicDetail>()
|
||||
// 单个话题的回复数据
|
||||
const repliesData = ref<TopicReply[]>([])
|
||||
// 页面的容器,用于计算是否到达底部
|
||||
const content = ref<HTMLElement>()
|
||||
// 当前的页数
|
||||
const currentPage = ref(1)
|
||||
// 是否加载,因为已经加载完了
|
||||
const isLoading = ref(true)
|
||||
|
||||
const fetchTopicData = async () => {
|
||||
/** 这里拿到的已经是后端返回回来的 data 数据了 */
|
||||
onMounted(async () => {
|
||||
// 获取单个话题的数据
|
||||
const topicResponseData = (
|
||||
await useKUNGalgameTopicStore().getTopicByTid(tid.value)
|
||||
).data
|
||||
topicData.value = topicResponseData
|
||||
}
|
||||
|
||||
/** 这里拿到的已经是后端返回回来的 data 数据了 */
|
||||
onMounted(async () => {
|
||||
await fetchTopicData()
|
||||
})
|
||||
|
||||
// 调用 getReplies 获取回复数据(watch 大法好!)
|
||||
|
@ -73,13 +74,66 @@ watch(
|
|||
replyDraft.value.publishStatus,
|
||||
],
|
||||
async () => {
|
||||
repliesData.value = (
|
||||
const replyResponseData = (
|
||||
await useKUNGalgameTopicStore().getReplies(tid.value)
|
||||
).data
|
||||
repliesData.value = replyResponseData
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 滚动事件处理函数
|
||||
const scrollHandler = async () => {
|
||||
// 滚动到底部的处理逻辑
|
||||
if (isScrollAtBottom() && isLoading.value) {
|
||||
// 自动增加页数
|
||||
currentPage.value++
|
||||
// 获取下一页的回复数据
|
||||
|
||||
const lazyLoadReplies = await useKUNGalgameTopicStore().getReplies(
|
||||
tid.value,
|
||||
currentPage.value
|
||||
)
|
||||
|
||||
// 判断是否已经将数据加载完,加载完则不需要加载了
|
||||
if (!lazyLoadReplies.data.length) {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
// 将新加载的回复数据追加到已有的回复数据中
|
||||
repliesData.value = [...repliesData.value, ...lazyLoadReplies.data]
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否滚动到底部
|
||||
const isScrollAtBottom = () => {
|
||||
if (content.value) {
|
||||
const scrollHeight = content.value.scrollHeight
|
||||
const scrollTop = content.value.scrollTop
|
||||
const clientHeight = content.value.clientHeight
|
||||
|
||||
return scrollHeight - scrollTop === clientHeight
|
||||
}
|
||||
}
|
||||
|
||||
// 在组件挂载后,添加滚动事件监听器
|
||||
onMounted(() => {
|
||||
// 获取滚动元素的引用
|
||||
const element = content.value
|
||||
|
||||
if (element) {
|
||||
element.addEventListener('scroll', scrollHandler)
|
||||
}
|
||||
})
|
||||
|
||||
// 在组件卸载前,移除滚动事件监听器
|
||||
onBeforeUnmount(() => {
|
||||
const element = content.value
|
||||
|
||||
if (element) {
|
||||
element.removeEventListener('scroll', scrollHandler)
|
||||
}
|
||||
})
|
||||
/* 话题界面的页面宽度 */
|
||||
const topicPageWidth = computed(() => {
|
||||
return showKUNGalgamePageWidth.value.Topic + '%'
|
||||
|
@ -112,7 +166,7 @@ onBeforeMount(() => {
|
|||
<Aside v-if="topicData?.tags" :tags="topicData.tags" />
|
||||
|
||||
<!-- 内容区 -->
|
||||
<div class="content">
|
||||
<div class="content" ref="content">
|
||||
<Master v-if="topicData" :topicData="topicData" />
|
||||
<Reply v-if="repliesData" :repliesData="repliesData" />
|
||||
</div>
|
||||
|
@ -133,7 +187,8 @@ onBeforeMount(() => {
|
|||
.content-container {
|
||||
width: v-bind(topicPageWidth);
|
||||
transition: all 0.2s;
|
||||
height: 100%;
|
||||
height: calc(100vh - 65px);
|
||||
min-height: 500px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
/* 设置背景的毛玻璃效果 */
|
||||
|
@ -144,6 +199,7 @@ onBeforeMount(() => {
|
|||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 右侧内容区 */
|
||||
|
@ -153,6 +209,8 @@ onBeforeMount(() => {
|
|||
/* 右侧内容区为弹性盒(用户可以一直向下滑) */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: scroll;
|
||||
overflow-x: visible;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
|
|
|
@ -61,8 +61,6 @@ watch(
|
|||
<style lang="scss" scoped>
|
||||
/* 侧边栏部分 */
|
||||
.aside {
|
||||
top: 70px;
|
||||
position: sticky;
|
||||
/* 侧边栏距离文章区域的距离 */
|
||||
margin-right: 5px;
|
||||
width: v-bind('asideWidth');
|
||||
|
@ -77,6 +75,8 @@ watch(
|
|||
span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
overflow-y: scroll;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* 侧边栏交互 */
|
||||
.nav-aside {
|
||||
|
|
|
@ -67,7 +67,6 @@ const props = defineProps<{
|
|||
margin-top: 27px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--kungalgame-blue-1);
|
||||
border-top: 1px solid var(--kungalgame-blue-1);
|
||||
}
|
||||
.info {
|
||||
margin-left: 50px;
|
||||
|
|
|
@ -97,6 +97,7 @@ const {
|
|||
align-items: center;
|
||||
/* 楼主话题距离其它人话题的距离 */
|
||||
margin-bottom: 5px;
|
||||
flex-shrink: 0;
|
||||
/* TODO: */
|
||||
/* 楼主话题背景图 */
|
||||
}
|
||||
|
|
|
@ -26,69 +26,74 @@ defineProps<{
|
|||
|
||||
<template>
|
||||
<!-- 其它人的回复 -->
|
||||
<TransitionGroup class="trans-list" name="list" tag="div">
|
||||
<!-- 这里使用 Math.random 的原因是 key 必须唯一 -->
|
||||
<div
|
||||
class="other-topic-container"
|
||||
v-for="(reply, index) in repliesData"
|
||||
:key="`${index}${Math.random()}`"
|
||||
>
|
||||
<!-- 每个人的单个话题 -->
|
||||
<!-- 楼层标志 -->
|
||||
<div class="floor">
|
||||
<span>F{{ reply.floor }}</span>
|
||||
</div>
|
||||
<!-- 其他人话题内容区的容器 -->
|
||||
<div class="container">
|
||||
<!-- 其它人回复的内容区 -->
|
||||
<div class="content">
|
||||
<!-- 其他人回复的上部 -->
|
||||
<div class="article">
|
||||
<!-- 其它人回复的上部左侧区域 -->
|
||||
<KUNGalgamerInfo :user="reply.r_user" />
|
||||
<!-- 其它人回复的上部右侧区域 -->
|
||||
<div class="right">
|
||||
<!-- 右侧的上部区域 -->
|
||||
<div class="top">
|
||||
<!-- 上部区域的左边 -->
|
||||
<div class="reply">
|
||||
<!-- TODO: 跳转到页面中话题的位置 -->
|
||||
<span
|
||||
>回复给 @
|
||||
<router-link to="#">{{
|
||||
reply.to_user.name
|
||||
}}</router-link></span
|
||||
>
|
||||
<!-- 这里使用 Math.random 的原因是 key 必须唯一 -->
|
||||
<Transition
|
||||
enter-active-class="animate__animated animate__fadeInUp animate__faster"
|
||||
appear
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="other-topic-container"
|
||||
v-for="(reply, index) in repliesData"
|
||||
:key="`${index}${Math.random()}`"
|
||||
>
|
||||
<!-- 每个人的单个话题 -->
|
||||
<!-- 楼层标志 -->
|
||||
<div class="floor">
|
||||
<span>F{{ reply.floor }}</span>
|
||||
</div>
|
||||
<!-- 其他人话题内容区的容器 -->
|
||||
<div class="container">
|
||||
<!-- 其它人回复的内容区 -->
|
||||
<div class="content">
|
||||
<!-- 其他人回复的上部 -->
|
||||
<div class="article">
|
||||
<!-- 其它人回复的上部左侧区域 -->
|
||||
<KUNGalgamerInfo :user="reply.r_user" />
|
||||
<!-- 其它人回复的上部右侧区域 -->
|
||||
<div class="right">
|
||||
<!-- 右侧的上部区域 -->
|
||||
<div class="top">
|
||||
<!-- 上部区域的左边 -->
|
||||
<div class="reply">
|
||||
<!-- TODO: 跳转到页面中话题的位置 -->
|
||||
<span
|
||||
>回复给 @
|
||||
<router-link to="#">{{
|
||||
reply.to_user.name
|
||||
}}</router-link></span
|
||||
>
|
||||
</div>
|
||||
<!-- 上部区域的右边 -->
|
||||
<Rewrite v-if="reply.edited" :time="reply.edited" />
|
||||
</div>
|
||||
<!-- 上部区域的右边 -->
|
||||
<Rewrite v-if="reply.edited" :time="reply.edited" />
|
||||
<!-- 右侧部分分文本 -->
|
||||
<div class="text" v-html="reply.content"></div>
|
||||
</div>
|
||||
<!-- 右侧部分分文本 -->
|
||||
<div class="text" v-html="reply.content"></div>
|
||||
</div>
|
||||
<!-- 其他人回复的下部 -->
|
||||
<div class="bottom">
|
||||
<Tags :tags="reply.tags" />
|
||||
<Time :time="reply.time" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 其他人回复的下部 -->
|
||||
<div class="bottom">
|
||||
<Tags :tags="reply.tags" />
|
||||
<Time :time="reply.time" />
|
||||
</div>
|
||||
<!-- 其它人回复的底部 -->
|
||||
<!-- isOthersTopic 区别楼主和回复的 footer,info 代表 footer 的点赞等信息,rUser 表示当前的回复用户信息 -->
|
||||
<TopicFooter
|
||||
:isOthersTopic="true"
|
||||
:info="{
|
||||
likes: reply.likes,
|
||||
dislikes: reply.dislikes,
|
||||
replies: reply.cid.length,
|
||||
upvotes: reply.upvote,
|
||||
}"
|
||||
:rUser="reply.r_user"
|
||||
/>
|
||||
<Comments />
|
||||
</div>
|
||||
<!-- 其它人回复的底部 -->
|
||||
<!-- isOthersTopic 区别楼主和回复的 footer,info 代表 footer 的点赞等信息,rUser 表示当前的回复用户信息 -->
|
||||
<TopicFooter
|
||||
:isOthersTopic="true"
|
||||
:info="{
|
||||
likes: reply.likes,
|
||||
dislikes: reply.dislikes,
|
||||
replies: reply.cid.length,
|
||||
upvotes: reply.upvote,
|
||||
}"
|
||||
:rUser="reply.r_user"
|
||||
/>
|
||||
<Comments />
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -112,12 +117,11 @@ defineProps<{
|
|||
font-weight: bold;
|
||||
font-style: oblique;
|
||||
color: var(--kungalgame-red-3);
|
||||
padding: 5px;
|
||||
border-bottom: none;
|
||||
/* 这里的阴影只能这么绘制 */
|
||||
filter: drop-shadow(1px 2px 4px var(--kungalgame-trans-blue-4));
|
||||
filter: drop-shadow(1px 2px 2px var(--kungalgame-trans-blue-4));
|
||||
span {
|
||||
transform: rotate(10deg) translateY(10px) translateX(30px);
|
||||
transform: rotate(10deg) translateY(40px);
|
||||
padding: 0 30px;
|
||||
text-align: center;
|
||||
background-color: var(--kungalgame-trans-white-2);
|
||||
|
@ -193,25 +197,6 @@ defineProps<{
|
|||
color: var(--kungalgame-font-color-3);
|
||||
}
|
||||
|
||||
.list-move, /* 对移动中的元素应用的过渡 */
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
/* 确保将离开的元素从布局流中删除
|
||||
以便能够正确地计算移动的动画。 */
|
||||
.list-leave-active {
|
||||
transform: scale(0.5);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.top {
|
||||
flex-direction: column;
|
||||
|
|
Loading…
Reference in a new issue