support comments pagination.

pull/282/head
ROC 1 year ago
parent 8ed21b5c0f
commit 60b217bcd9

@ -4,74 +4,48 @@
<span class="time-item">
{{ formatPrettyTime(comment.created_on) }}
</span>
<div
v-if="!store.state.userLogined"
class="action-item"
>
<n-icon size="medium">
<thumb-up-outlined/>
</n-icon>
<span class="upvote-count">{{ thumbsUpCount }}</span>
</div>
<div
v-if="store.state.userLogined"
class="action-item hover"
@click.stop="handleThumbsUp"
>
<n-icon size="medium">
<thumb-up-outlined v-if="!hasThumbsUp" />
<thumb-up-twotone v-if="hasThumbsUp" class="show" />
</n-icon>
<span class="upvote-count">{{ thumbsUpCount }}</span>
</div>
<div
v-if="!store.state.userLogined"
class="action-item"
>
<n-icon size="medium">
<thumb-down-outlined />
</n-icon>
</div>
<div
v-if="store.state.userLogined"
class="action-item hover"
@click.stop="handleThumbsDown"
>
<n-icon size="medium">
<thumb-down-outlined v-if="!hasThumbsDown" />
<thumb-down-twotone v-if="hasThumbsDown" class="show" />
</n-icon>
<div class="actions">
<div v-if="!store.state.userLogined" class="action-item">
<n-icon size="medium">
<thumb-up-outlined />
</n-icon>
<span class="upvote-count">{{ thumbsUpCount }}</span>
</div>
<div v-if="store.state.userLogined" class="action-item hover" @click.stop="handleThumbsUp">
<n-icon size="medium">
<thumb-up-outlined v-if="!hasThumbsUp" />
<thumb-up-twotone v-if="hasThumbsUp" class="show" />
</n-icon>
<span class="upvote-count">{{ thumbsUpCount }}</span>
</div>
<div v-if="!store.state.userLogined" class="action-item">
<n-icon size="medium">
<thumb-down-outlined />
</n-icon>
</div>
<div v-if="store.state.userLogined" class="action-item hover" @click.stop="handleThumbsDown">
<n-icon size="medium">
<thumb-down-outlined v-if="!hasThumbsDown" />
<thumb-down-twotone v-if="hasThumbsDown" class="show" />
</n-icon>
</div>
<span class="show opacity-item" v-if="store.state.userLogined && !showReply" @click="switchReply(true)">
</span>
<span class="hide" v-if="store.state.userLogined && showReply" @click="switchReply(false)">
</span>
</div>
<span class="show opacity-item" v-if="store.state.userLogined && !showReply" @click="switchReply(true)">
</span>
<span class="hide" v-if="store.state.userLogined && showReply" @click="switchReply(false)">
</span>
</div>
<div class="reply-input-wrap" v-if="showReply">
<n-input-group>
<n-input
ref="inputInstRef"
size="small"
:placeholder="
props.atUsername
? '@' + props.atUsername
: '..'
"
maxlength="100"
v-model:value="replyContent"
show-count
clearable
/>
<n-button
type="primary"
size="small"
ghost
:loading="submitting"
@click="submitReply"
>
<n-input ref="inputInstRef" size="small" :placeholder="
props.atUsername
? '@' + props.atUsername
: '..'
" maxlength="100" v-model:value="replyContent" show-count clearable />
<n-button type="primary" size="small" ghost :loading="submitting" @click="submitReply">
</n-button>
</n-input-group>
@ -120,36 +94,36 @@ const handleThumbsUp = () => {
tweet_id: props.comment.post_id,
comment_id: props.comment.id,
})
.then((_res) => {
hasThumbsUp.value = !hasThumbsUp.value
if (hasThumbsUp.value) {
thumbsUpCount.value++
hasThumbsDown.value = false
} else {
thumbsUpCount.value--
}
})
.catch((err) => {
console.log(err);
});
.then((_res) => {
hasThumbsUp.value = !hasThumbsUp.value
if (hasThumbsUp.value) {
thumbsUpCount.value++
hasThumbsDown.value = false
} else {
thumbsUpCount.value--
}
})
.catch((err) => {
console.log(err);
});
};
const handleThumbsDown = () => {
thumbsDownTweetComment({
tweet_id: props.comment.post_id,
comment_id: props.comment.id,
})
.then((_res) => {
hasThumbsDown.value = !hasThumbsDown.value
if ( hasThumbsDown.value) {
if ( hasThumbsUp.value) {
thumbsUpCount.value--
hasThumbsUp.value = false
.then((_res) => {
hasThumbsDown.value = !hasThumbsDown.value
if (hasThumbsDown.value) {
if (hasThumbsUp.value) {
thumbsUpCount.value--
hasThumbsUp.value = false
}
}
}
})
.catch((err) => {
console.log(err);
});
})
.catch((err) => {
console.log(err);
});
};
const switchReply = (status: boolean) => {
showReply.value = status;
@ -188,47 +162,66 @@ defineExpose({ switchReply });
.reply-switch {
display: flex;
align-items: center;
justify-content: space-between;
text-align: right;
font-size: 12px;
margin: 10px 0;
.actions {
display: flex;
align-items: center;
text-align: right;
font-size: 12px;
margin: 10px 0;
}
.time-item {
font-size: 12px;
opacity: 0.65;
margin-right: 18px;
}
.action-item {
display: flex;
align-items: center;
margin-right: 18px;
margin-left: 18px;
opacity: 0.65;
.upvote-count {
margin-left: 4px;
font-size: 12px;
}
&.hover {
cursor: pointer;
}
}
.opacity-item {
opacity: 0.75;
}
opacity: 0.75;
margin-left: 18px;
}
.show {
color: #18a058;
cursor: pointer;
}
.hide {
opacity: 0.75;
cursor: pointer;
}
}
}
.dark {
.reply-compose-wrap {
background-color: rgba(16, 16, 20, 0.75);
.reply-switch {
.show {
color: #63e2b7;
}
}
}
}
}

@ -2,41 +2,29 @@
<div class="reply-item">
<div class="header-wrap">
<div class="username">
<router-link
class="user-link"
:to="{
name: 'user',
query: { username: props.reply.user.username },
}"
>
<router-link class="user-link" :to="{
name: 'user',
query: { username: props.reply.user.username },
}">
{{ props.reply.user.username }}
</router-link>
<span class="reply-name">
{{ props.reply.at_user_id > 0 ? '' : ':' }}
</span>
<router-link
class="user-link"
:to="{
name: 'user',
query: { username: props.reply.at_user.username },
}"
v-if="props.reply.at_user_id > 0"
>
<router-link class="user-link" :to="{
name: 'user',
query: { username: props.reply.at_user.username },
}" v-if="props.reply.at_user_id > 0">
{{ props.reply.at_user.username }}
</router-link>
</div>
<div class="timestamp">
{{ props.reply.ip_loc }}
<n-popconfirm
v-if="
store.state.userInfo.is_admin ||
store.state.userInfo.id === props.reply.user.id
"
negative-text="取消"
positive-text="确认"
@positive-click="execDelAction"
>
<n-popconfirm v-if="
store.state.userInfo.is_admin ||
store.state.userInfo.id === props.reply.user.id
" negative-text="" positive-text="" @positive-click="execDelAction">
<template #trigger>
<n-button quaternary circle size="tiny" class="del-btn">
<template #icon>
@ -57,46 +45,34 @@
<span class="time-item">
{{ formatPrettyTime(props.reply.created_on) }}
</span>
<div
v-if="!store.state.userLogined"
class="action-item"
@click.stop=""
>
<n-icon size="medium">
<thumb-up-outlined/>
</n-icon>
<span class="upvote-count">{{ thumbsUpCount }}</span>
<div class="actions">
<div v-if="!store.state.userLogined" class="action-item" @click.stop="">
<n-icon size="medium">
<thumb-up-outlined />
</n-icon>
<span class="upvote-count">{{ thumbsUpCount }}</span>
</div>
<div v-if="store.state.userLogined" class="action-item hover" @click.stop="handleThumbsUp">
<n-icon size="medium">
<thumb-up-outlined v-if="!hasThumbsUp" />
<thumb-up-twotone v-if="hasThumbsUp" class="show" />
</n-icon>
<span class="upvote-count">{{ thumbsUpCount }}</span>
</div>
<div v-if="!store.state.userLogined" class="action-item">
<n-icon size="medium">
<thumb-down-outlined />
</n-icon>
</div>
<div v-if="store.state.userLogined" class="action-item hover" @click.stop="handleThumbsDown">
<n-icon size="medium">
<thumb-down-outlined v-if="!hasThumbsDown" />
<thumb-down-twotone v-if="hasThumbsDown" class="show" />
</n-icon>
</div>
<span v-if="store.state.userLogined" class="show opacity-item" @click="focusReply"> </span>
</div>
<div
v-if="store.state.userLogined"
class="action-item hover"
@click.stop="handleThumbsUp"
>
<n-icon size="medium">
<thumb-up-outlined v-if="!hasThumbsUp" />
<thumb-up-twotone v-if="hasThumbsUp" class="show" />
</n-icon>
<span class="upvote-count">{{ thumbsUpCount }}</span>
</div>
<div
v-if="!store.state.userLogined"
class="action-item"
>
<n-icon size="medium">
<thumb-down-outlined />
</n-icon>
</div>
<div
v-if="store.state.userLogined"
class="action-item hover"
@click.stop="handleThumbsDown"
>
<n-icon size="medium">
<thumb-down-outlined v-if="!hasThumbsDown" />
<thumb-down-twotone v-if="hasThumbsDown" class="show" />
</n-icon>
</div>
<span v-if="store.state.userLogined" class="show opacity-item" @click="focusReply"> </span>
</div>
</div>
</div>
@ -136,18 +112,18 @@ const handleThumbsUp = () => {
comment_id: props.reply.comment_id,
reply_id: props.reply.id,
})
.then((_res) => {
hasThumbsUp.value = !hasThumbsUp.value
if (hasThumbsUp.value) {
thumbsUpCount.value++
hasThumbsDown.value= false
} else {
thumbsUpCount.value--
}
})
.catch((err) => {
console.log(err);
});
.then((_res) => {
hasThumbsUp.value = !hasThumbsUp.value
if (hasThumbsUp.value) {
thumbsUpCount.value++
hasThumbsDown.value = false
} else {
thumbsUpCount.value--
}
})
.catch((err) => {
console.log(err);
});
};
const handleThumbsDown = () => {
thumbsDownTweetReply({
@ -155,18 +131,18 @@ const handleThumbsDown = () => {
comment_id: props.reply.comment_id,
reply_id: props.reply.id,
})
.then((_res) => {
hasThumbsDown.value = !hasThumbsDown.value
if (hasThumbsDown.value) {
if (hasThumbsUp.value) {
thumbsUpCount.value--
hasThumbsUp.value = false
.then((_res) => {
hasThumbsDown.value = !hasThumbsDown.value
if (hasThumbsDown.value) {
if (hasThumbsUp.value) {
thumbsUpCount.value--
hasThumbsUp.value = false
}
}
}
})
.catch((err) => {
console.log(err);
});
})
.catch((err) => {
console.log(err);
});
};
const focusReply = () => {
emit('focusReply', props.reply);
@ -201,16 +177,19 @@ const execDelAction = () => {
display: flex;
align-items: center;
justify-content: space-between;
.username {
max-width: 50%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.reply-name {
margin: 0 3px;
opacity: 0.75;
}
}
.timestamp {
opacity: 0.75;
text-align: right;
@ -220,8 +199,10 @@ const execDelAction = () => {
white-space: nowrap;
}
}
.base-wrap {
display: block;
.content {
width: calc(100% - 40px);
margin-top: 4px;
@ -229,36 +210,54 @@ const execDelAction = () => {
text-align: justify;
line-height: 2;
}
.reply-switch {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
margin: 10px 0 0;
.actions {
display: flex;
align-items: center;
text-align: right;
font-size: 12px;
margin: 10px 0;
}
.time-item {
font-size: 12px;
opacity: 0.75;
margin-right: 18px;
}
.action-item {
display: flex;
align-items: center;
margin-right: 18px;
margin-left: 18px;
opacity: 0.65;
.upvote-count {
margin-left: 4px;
font-size: 12px;
}
&.hover {
cursor: pointer;
}
}
.opacity-item {
opacity: 0.75;
margin-left: 18px;
}
.show {
color: #18a058;
cursor: pointer;
}
.hide {
opacity: 0.75;
cursor: pointer;
@ -266,6 +265,7 @@ const execDelAction = () => {
}
}
}
.dark {
.reply-item {
border-bottom: 1px solid #262628;
@ -277,7 +277,7 @@ const execDelAction = () => {
color: #63e2b7;
}
}
}
}
}
}
</style>

@ -35,7 +35,7 @@ declare module NetParams {
id: number;
}
interface UserGetUnreadMsgCount {}
interface UserGetUnreadMsgCount { }
interface UserGetMessages {
page: number;
@ -82,7 +82,7 @@ declare module NetParams {
imgCaptcha: string;
}
interface UserGetCaptcha {}
interface UserGetCaptcha { }
interface UserWhisper {
user_id: number;
@ -173,9 +173,11 @@ declare module NetParams {
interface PostGetPostComments {
id: number;
sort_strategy: "default" | "newest";
page?: number;
page_size?: number;
}
interface GetContacts {}
interface GetContacts { }
interface PostCreatePost {
/** 帖子内容列表 */

@ -23,11 +23,7 @@
</n-tabs>
</div>
<n-list-item v-if="post.id > 0">
<compose-comment
:lock="post.is_lock"
:post-id="post.id"
@post-success="loadComments(true)"
/>
<compose-comment :lock="post.is_lock" :post-id="post.id" @post-success="loadComments(true)" />
</n-list-item>
<div v-if="post.id > 0">
@ -36,26 +32,24 @@
</div>
<div v-else>
<div class="empty-wrap" v-if="comments.length === 0">
<n-empty
size="large"
description="暂无评论,快来抢沙发"
/>
<n-empty size="large" description="暂无评论,快来抢沙发" />
</div>
<n-list-item v-for="comment in comments" :key="comment.id">
<comment-item
:comment="comment"
@reload="loadComments"
/>
<comment-item :comment="comment" @reload="loadComments" />
</n-list-item>
</div>
</div>
<div class="load-more-ele" v-if="!noMore" ref="bottomElement">
...
</div>
</n-list>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted, computed, Ref } from 'vue';
import { ref, watch, onMounted, onUnmounted, computed, Ref } from 'vue';
import { useRoute } from 'vue-router';
import { getPost, getPostComments } from '@/api/post';
@ -66,6 +60,9 @@ const commentLoading = ref(false);
const comments = ref<Item.CommentProps[]>([]);
const postId = computed(() => +(route.query.id as string));
const sortStrategy = ref<"default" | "newest">('default');
const bottomElement = ref<HTMLElement | null>(null);
const page = ref<number>(1);
const noMore = ref(false);
const commentTab = (tab: "default" | "newest") => {
sortStrategy.value = tab;
@ -98,9 +95,19 @@ const loadComments = (scrollToBottom: boolean = false) => {
getPostComments({
id: post.value.id as number,
sort_strategy: sortStrategy.value,
page: page.value,
page_size: 20
})
.then((res) => {
comments.value = res.list;
if (res.list.length === 0) {
noMore.value = true
}
if (page.value === 1) {
comments.value = res.list;
} else {
comments.value = comments.value.concat(res.list);
}
commentLoading.value = false;
if (scrollToBottom) {
@ -113,9 +120,38 @@ const loadComments = (scrollToBottom: boolean = false) => {
commentLoading.value = false;
});
};
const loadMoreComments = () => {
if (!commentLoading.value && comments.value.length > 0) {
page.value = page.value + 1;
loadComments();
}
};
const observer = new IntersectionObserver((entries: IntersectionObserverEntry[]) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadMoreComments();
}
});
}, {
root: null,
rootMargin: '0px',
threshold: 1
});
onMounted(() => {
if (bottomElement.value) {
observer.observe(bottomElement.value);
}
loadPost();
});
onUnmounted(() => {
observer.disconnect()
});
watch(postId, () => {
if (postId.value > 0 && route.name === 'post') {
loadPost();
@ -127,19 +163,32 @@ watch(postId, () => {
.detail-wrap {
min-height: 100px;
}
.comment-opts-wrap {
padding-top: 6px;
padding-left: 16px;
padding-right: 16px;
opacity: 0.75;
.comment-title-item {
padding-top: 4px;
font-size: 16px;
text-align: center;
}
}
.load-more-ele {
font-size: 12px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.dark {
.main-content-wrap, .skeleton-wrap {
.main-content-wrap,
.skeleton-wrap {
background-color: rgba(16, 16, 20, 0.75);
}
}

Loading…
Cancel
Save