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
}
// 获取回复的请求
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',
}

View file

@ -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) {

View file

@ -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 {

View file

@ -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;

View file

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

View file

@ -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;