feat: lazy load reply

This commit is contained in:
KUN1007 2023-09-15 21:45:44 +08:00
parent cd511f5ede
commit dbdcfa84fd
6 changed files with 153 additions and 104 deletions

View file

@ -44,6 +44,11 @@ interface CommentDraft {
content: string content: string
} }
// 获取回复的请求
interface ReplyRequest {
sortField: string
sortOrder: 'asc' | 'desc'
}
// 话题页面的 store // 话题页面的 store
interface Topic { interface Topic {
// 是否正在被编辑 // 是否正在被编辑
@ -56,7 +61,7 @@ interface Topic {
// 回复的缓存 // 回复的缓存
replyDraft: ReplyDraft replyDraft: ReplyDraft
// 获取回复的请求接口格式 // 获取回复的请求接口格式
replyRequest: TopicReplyRequestData replyRequest: ReplyRequest
// 评论的缓存 // 评论的缓存
commentDraft: CommentDraft commentDraft: CommentDraft
@ -81,11 +86,8 @@ export const useKUNGalgameTopicStore = defineStore({
publishStatus: false, publishStatus: false,
}, },
replyRequest: { replyRequest: {
tid: 0,
page: 1,
limit: 5,
sortField: '', sortField: '',
sortOrder: '', sortOrder: 'asc',
}, },
commentDraft: { commentDraft: {
c_uid: 0, 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) => { return new Promise((resolve, reject) => {
// 这里的默认值用于初始化 // 这里的默认值用于初始化
const requestData: TopicReplyRequestData = { const requestData: TopicReplyRequestData = {
tid: tid, tid: tid,
page: this.replyRequest.page || 1, page: page || 1,
limit: this.replyRequest.limit || 5, limit: limit || 5,
sortField: this.replyRequest.sortField || 'floor', sortField: this.replyRequest.sortField || 'floor',
sortOrder: this.replyRequest.sortOrder || 'desc', sortOrder: this.replyRequest.sortOrder || 'desc',
} }

View file

@ -9,8 +9,11 @@ import {
onBeforeMount, onBeforeMount,
computed, computed,
watch, watch,
onBeforeUnmount,
} from 'vue' } from 'vue'
import { onBeforeRouteLeave } from 'vue-router' import { onBeforeRouteLeave, useRoute } from 'vue-router'
import { TopicDetail, TopicReply } from '@/api'
// Aside // Aside
import Aside from './aside/Aside.vue' import Aside from './aside/Aside.vue'
@ -25,13 +28,9 @@ const ReplyPanel = defineAsyncComponent(
import { useKUNGalgameSettingsStore } from '@/store/modules/settings' import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
// store // store
import { useKUNGalgameTopicStore } from '@/store/modules/topic' import { useKUNGalgameTopicStore } from '@/store/modules/topic'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { TopicDetail, TopicReply } from '@/api' //
import { useRoute } from 'vue-router'
const route = useRoute() const route = useRoute()
// 使 store // 使 store
@ -51,18 +50,20 @@ const tid = computed(() => {
const topicData = ref<TopicDetail>() const topicData = ref<TopicDetail>()
// //
const repliesData = ref<TopicReply[]>([]) const repliesData = ref<TopicReply[]>([])
//
const content = ref<HTMLElement>()
//
const currentPage = ref(1)
//
const isLoading = ref(true)
const fetchTopicData = async () => { /** 这里拿到的已经是后端返回回来的 data 数据了 */
onMounted(async () => {
// //
const topicResponseData = ( const topicResponseData = (
await useKUNGalgameTopicStore().getTopicByTid(tid.value) await useKUNGalgameTopicStore().getTopicByTid(tid.value)
).data ).data
topicData.value = topicResponseData topicData.value = topicResponseData
}
/** 这里拿到的已经是后端返回回来的 data 数据了 */
onMounted(async () => {
await fetchTopicData()
}) })
// getReplies watch // getReplies watch
@ -73,13 +74,66 @@ watch(
replyDraft.value.publishStatus, replyDraft.value.publishStatus,
], ],
async () => { async () => {
repliesData.value = ( const replyResponseData = (
await useKUNGalgameTopicStore().getReplies(tid.value) await useKUNGalgameTopicStore().getReplies(tid.value)
).data ).data
repliesData.value = replyResponseData
}, },
{ immediate: true } { 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(() => { const topicPageWidth = computed(() => {
return showKUNGalgamePageWidth.value.Topic + '%' return showKUNGalgamePageWidth.value.Topic + '%'
@ -112,7 +166,7 @@ onBeforeMount(() => {
<Aside v-if="topicData?.tags" :tags="topicData.tags" /> <Aside v-if="topicData?.tags" :tags="topicData.tags" />
<!-- 内容区 --> <!-- 内容区 -->
<div class="content"> <div class="content" ref="content">
<Master v-if="topicData" :topicData="topicData" /> <Master v-if="topicData" :topicData="topicData" />
<Reply v-if="repliesData" :repliesData="repliesData" /> <Reply v-if="repliesData" :repliesData="repliesData" />
</div> </div>
@ -133,7 +187,8 @@ onBeforeMount(() => {
.content-container { .content-container {
width: v-bind(topicPageWidth); width: v-bind(topicPageWidth);
transition: all 0.2s; transition: all 0.2s;
height: 100%; height: calc(100vh - 65px);
min-height: 500px;
margin: 0 auto; margin: 0 auto;
display: flex; display: flex;
/* 设置背景的毛玻璃效果 */ /* 设置背景的毛玻璃效果 */
@ -144,6 +199,7 @@ onBeforeMount(() => {
border-radius: 5px; border-radius: 5px;
padding: 5px; padding: 5px;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden;
} }
/* 右侧内容区 */ /* 右侧内容区 */
@ -153,6 +209,8 @@ onBeforeMount(() => {
/* 右侧内容区为弹性盒(用户可以一直向下滑) */ /* 右侧内容区为弹性盒(用户可以一直向下滑) */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: scroll;
overflow-x: visible;
} }
@media (max-width: 1000px) { @media (max-width: 1000px) {

View file

@ -61,8 +61,6 @@ watch(
<style lang="scss" scoped> <style lang="scss" scoped>
/* 侧边栏部分 */ /* 侧边栏部分 */
.aside { .aside {
top: 70px;
position: sticky;
/* 侧边栏距离文章区域的距离 */ /* 侧边栏距离文章区域的距离 */
margin-right: 5px; margin-right: 5px;
width: v-bind('asideWidth'); width: v-bind('asideWidth');
@ -77,6 +75,8 @@ watch(
span { span {
white-space: nowrap; white-space: nowrap;
} }
overflow-y: scroll;
flex-shrink: 0;
} }
/* 侧边栏交互 */ /* 侧边栏交互 */
.nav-aside { .nav-aside {

View file

@ -67,7 +67,6 @@ const props = defineProps<{
margin-top: 27px; margin-top: 27px;
padding-bottom: 10px; padding-bottom: 10px;
border-bottom: 1px solid var(--kungalgame-blue-1); border-bottom: 1px solid var(--kungalgame-blue-1);
border-top: 1px solid var(--kungalgame-blue-1);
} }
.info { .info {
margin-left: 50px; margin-left: 50px;

View file

@ -97,6 +97,7 @@ const {
align-items: center; align-items: center;
/* 楼主话题距离其它人话题的距离 */ /* 楼主话题距离其它人话题的距离 */
margin-bottom: 5px; margin-bottom: 5px;
flex-shrink: 0;
/* TODO: */ /* TODO: */
/* 楼主话题背景图 */ /* 楼主话题背景图 */
} }

View file

@ -26,8 +26,12 @@ defineProps<{
<template> <template>
<!-- 其它人的回复 --> <!-- 其它人的回复 -->
<TransitionGroup class="trans-list" name="list" tag="div">
<!-- 这里使用 Math.random 的原因是 key 必须唯一 --> <!-- 这里使用 Math.random 的原因是 key 必须唯一 -->
<Transition
enter-active-class="animate__animated animate__fadeInUp animate__faster"
appear
>
<div>
<div <div
class="other-topic-container" class="other-topic-container"
v-for="(reply, index) in repliesData" v-for="(reply, index) in repliesData"
@ -88,7 +92,8 @@ defineProps<{
<Comments /> <Comments />
</div> </div>
</div> </div>
</TransitionGroup> </div>
</Transition>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -112,12 +117,11 @@ defineProps<{
font-weight: bold; font-weight: bold;
font-style: oblique; font-style: oblique;
color: var(--kungalgame-red-3); color: var(--kungalgame-red-3);
padding: 5px;
border-bottom: none; 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 { span {
transform: rotate(10deg) translateY(10px) translateX(30px); transform: rotate(10deg) translateY(40px);
padding: 0 30px; padding: 0 30px;
text-align: center; text-align: center;
background-color: var(--kungalgame-trans-white-2); background-color: var(--kungalgame-trans-white-2);
@ -193,25 +197,6 @@ defineProps<{
color: var(--kungalgame-font-color-3); 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) { @media (max-width: 1000px) {
.top { .top {
flex-direction: column; flex-direction: column;