support comments pagination.

pull/282/head
ROC 2 years ago
parent 8ed21b5c0f
commit 60b217bcd9

@ -4,39 +4,26 @@
<span class="time-item"> <span class="time-item">
{{ formatPrettyTime(comment.created_on) }} {{ formatPrettyTime(comment.created_on) }}
</span> </span>
<div <div class="actions">
v-if="!store.state.userLogined" <div v-if="!store.state.userLogined" class="action-item">
class="action-item"
>
<n-icon size="medium"> <n-icon size="medium">
<thumb-up-outlined/> <thumb-up-outlined />
</n-icon> </n-icon>
<span class="upvote-count">{{ thumbsUpCount }}</span> <span class="upvote-count">{{ thumbsUpCount }}</span>
</div> </div>
<div <div v-if="store.state.userLogined" class="action-item hover" @click.stop="handleThumbsUp">
v-if="store.state.userLogined"
class="action-item hover"
@click.stop="handleThumbsUp"
>
<n-icon size="medium"> <n-icon size="medium">
<thumb-up-outlined v-if="!hasThumbsUp" /> <thumb-up-outlined v-if="!hasThumbsUp" />
<thumb-up-twotone v-if="hasThumbsUp" class="show" /> <thumb-up-twotone v-if="hasThumbsUp" class="show" />
</n-icon> </n-icon>
<span class="upvote-count">{{ thumbsUpCount }}</span> <span class="upvote-count">{{ thumbsUpCount }}</span>
</div> </div>
<div <div v-if="!store.state.userLogined" class="action-item">
v-if="!store.state.userLogined"
class="action-item"
>
<n-icon size="medium"> <n-icon size="medium">
<thumb-down-outlined /> <thumb-down-outlined />
</n-icon> </n-icon>
</div> </div>
<div <div v-if="store.state.userLogined" class="action-item hover" @click.stop="handleThumbsDown">
v-if="store.state.userLogined"
class="action-item hover"
@click.stop="handleThumbsDown"
>
<n-icon size="medium"> <n-icon size="medium">
<thumb-down-outlined v-if="!hasThumbsDown" /> <thumb-down-outlined v-if="!hasThumbsDown" />
<thumb-down-twotone v-if="hasThumbsDown" class="show" /> <thumb-down-twotone v-if="hasThumbsDown" class="show" />
@ -49,29 +36,16 @@
</span> </span>
</div> </div>
</div>
<div class="reply-input-wrap" v-if="showReply"> <div class="reply-input-wrap" v-if="showReply">
<n-input-group> <n-input-group>
<n-input <n-input ref="inputInstRef" size="small" :placeholder="
ref="inputInstRef"
size="small"
:placeholder="
props.atUsername props.atUsername
? '@' + props.atUsername ? '@' + props.atUsername
: '..' : '..'
" " maxlength="100" v-model:value="replyContent" show-count clearable />
maxlength="100" <n-button type="primary" size="small" ghost :loading="submitting" @click="submitReply">
v-model:value="replyContent"
show-count
clearable
/>
<n-button
type="primary"
size="small"
ghost
:loading="submitting"
@click="submitReply"
>
</n-button> </n-button>
</n-input-group> </n-input-group>
@ -140,8 +114,8 @@ const handleThumbsDown = () => {
}) })
.then((_res) => { .then((_res) => {
hasThumbsDown.value = !hasThumbsDown.value hasThumbsDown.value = !hasThumbsDown.value
if ( hasThumbsDown.value) { if (hasThumbsDown.value) {
if ( hasThumbsUp.value) { if (hasThumbsUp.value) {
thumbsUpCount.value-- thumbsUpCount.value--
hasThumbsUp.value = false hasThumbsUp.value = false
} }
@ -186,45 +160,64 @@ defineExpose({ switchReply });
<style lang="less" scoped> <style lang="less" scoped>
.reply-compose-wrap { .reply-compose-wrap {
.reply-switch { .reply-switch {
display: flex;
align-items: center;
justify-content: space-between;
text-align: right;
font-size: 12px;
margin: 10px 0;
.actions {
display: flex; display: flex;
align-items: center; align-items: center;
text-align: right; text-align: right;
font-size: 12px; font-size: 12px;
margin: 10px 0; margin: 10px 0;
}
.time-item { .time-item {
font-size: 12px; font-size: 12px;
opacity: 0.65; opacity: 0.65;
margin-right: 18px; margin-right: 18px;
} }
.action-item { .action-item {
display: flex; display: flex;
align-items: center; align-items: center;
margin-right: 18px; margin-left: 18px;
opacity: 0.65; opacity: 0.65;
.upvote-count { .upvote-count {
margin-left: 4px; margin-left: 4px;
font-size: 12px; font-size: 12px;
} }
&.hover { &.hover {
cursor: pointer; cursor: pointer;
} }
} }
.opacity-item { .opacity-item {
opacity: 0.75; opacity: 0.75;
margin-left: 18px;
} }
.show { .show {
color: #18a058; color: #18a058;
cursor: pointer; cursor: pointer;
} }
.hide { .hide {
opacity: 0.75; opacity: 0.75;
cursor: pointer; cursor: pointer;
} }
} }
} }
.dark { .dark {
.reply-compose-wrap { .reply-compose-wrap {
background-color: rgba(16, 16, 20, 0.75); background-color: rgba(16, 16, 20, 0.75);
.reply-switch { .reply-switch {
.show { .show {
color: #63e2b7; color: #63e2b7;

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

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

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

Loading…
Cancel
Save