1. 创建可复用 composables (usePagination, usePostContent, useWhisper)

2. 移除重复代码,提升代码可维护性
3. 优化取消关注后的列表刷新逻辑
4. 修复 dialog 实例传递问题
5. 迁移 useUserAction 到 composables 目录
pull/706/head
orzi 4 months ago
parent 7a2e7ebfc4
commit f09fddc744

@ -63,12 +63,13 @@ import { formatDate } from '@/utils/formatTime';
import { MoreHorizFilled } from '@vicons/material';
import { PaperPlaneOutline, BodyOutline, WalkOutline } from '@vicons/ionicons5';
import { Api } from '@/utils/request';
import UserAction from '@/utils/useUserAction';
import UserAction from '@/composables/useUserAction';
const dialog = useDialog();
const emit = defineEmits<{
(e: 'send-whisper', user: Item.UserInfo): void;
(e: 'unfollow-success'): void;
}>();
const renderIcon = (icon: Component) => {
@ -80,9 +81,14 @@ const renderIcon = (icon: Component) => {
};
const handleFollowUser = () => {
UserAction.followAction(props.contact.user_id, props.contact.username, props.contact.is_following)
const wasFollowing = props.contact.is_following;
UserAction.followAction(dialog, props.contact.user_id, props.contact.username, props.contact.is_following)
.then(_action => {
props.contact.is_following = _action;
//
if (wasFollowing && !_action) {
emit('unfollow-success');
}
})
.catch(err => {
console.log(err);

@ -0,0 +1,55 @@
<template>
<n-space v-if="totalPage > 0" justify="center">
<InfiniteLoading
class="load-more"
:slots="{ complete: completeText, error: '加载出错' }"
@infinite="handleInfinite"
>
<template #spinner>
<div class="load-more-wrap">
<n-spin :size="14" v-if="!noMore" />
<span class="load-more-spinner">{{ noMore ? completeText : '加载更多' }}</span>
</div>
</template>
</InfiniteLoading>
</n-space>
</template>
<script setup lang="ts">
import InfiniteLoading from 'v3-infinite-loading';
withDefaults(defineProps<{
totalPage: number;
noMore: boolean;
completeText?: string;
}>(), {
completeText: '没有更多了',
});
const emit = defineEmits<{
(e: 'load-more'): void;
}>();
const handleInfinite = () => {
emit('load-more');
};
</script>
<style lang="less" scoped>
.load-more {
margin: 20px;
.load-more-wrap {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 14px;
.load-more-spinner {
font-size: 14px;
opacity: 0.65;
}
}
}
</style>

@ -157,7 +157,7 @@ import { formatRelativeTime } from '@/utils/formatTime';
import { MoreHorizFilled } from '@vicons/material';
import { storeToRefs } from 'pinia';
import { Api } from '@/utils/request';
import UserAction from '@/utils/useUserAction';
import UserAction from '@/composables/useUserAction';
const defaultavatar =
'https://assets.paopao.info/public/avatar/default/admin.png';
@ -226,7 +226,7 @@ const onHandleFollowAction = (message: Item.MessageProps) => {
message.type == 4 && message.sender_user_id == userInfo.value.id
? message.receiver_user
: message.sender_user;
UserAction.followAction(user.id, user.username, user.is_following)
UserAction.followAction(dialog, user.id, user.username, user.is_following)
.then(_action => {
user.is_following = _action;
// TODO: messageuser

@ -259,7 +259,8 @@ import copy from 'copy-to-clipboard';
import { storeToRefs } from 'pinia';
import { useStoreUser } from '@/store/user';
import { Api } from '@/utils/request';
import UserAction from '@/utils/useUserAction';
import UserAction from '@/composables/useUserAction';
import { usePostContent } from '@/composables/usePostContent';
const useFriendship =
import.meta.env.VITE_USE_FRIENDSHIP.toLowerCase() === 'true';
@ -314,48 +315,8 @@ const emit = defineEmits<{
(e: 'reload', post_id: number): void;
}>();
const post = computed({
get: () => {
let post: Item.PostComponentProps = Object.assign(
{
texts: [],
imgs: [],
videos: [],
links: [],
attachments: [],
charge_attachments: [],
},
props.post,
);
post.contents.map((content) => {
if (+content.type === 1 || +content.type === 2) {
post.texts.push(content);
}
if (+content.type === 3) {
post.imgs.push(content);
}
if (+content.type === 4) {
post.videos.push(content);
}
if (+content.type === 6) {
post.links.push(content);
}
if (+content.type === 7) {
post.attachments.push(content);
}
if (+content.type === 8) {
post.charge_attachments.push(content);
}
});
return post;
},
set: (newVal) => {
props.post.upvote_count = newVal.upvote_count;
props.post.comment_count = newVal.comment_count;
props.post.collection_count = newVal.collection_count;
props.post.is_essence = newVal.is_essence;
},
});
// 使 usePostContent composable ()
const post = usePostContent(props.post, true);
const renderIcon = (icon: Component) => {
return () => {
@ -492,7 +453,7 @@ const adminOptions = computed(() => {
});
const onHandleFollowAction = (post: Item.PostProps) => {
UserAction.followAction(post.user.id, post.user.username, post.user.is_following)
UserAction.followAction(dialog, post.user.id, post.user.username, post.user.is_following)
.then(_action => {
post.user.is_following = _action;
})

@ -164,7 +164,8 @@ import copy from 'copy-to-clipboard';
import { useStoreProfile } from '@/store/profile';
import { storeToRefs } from 'pinia';
import { Api } from '@/utils/request';
import UserAction from '@/utils/useUserAction';
import UserAction from '@/composables/useUserAction';
import { usePostContent } from '@/composables/usePostContent';
const router = useRouter();
@ -274,7 +275,7 @@ const handleTweetAction = async (
break;
case 'follow':
case 'unfollow':
UserAction.followAction(props.post.user.id, props.post.user.username, props.post.user.is_following)
UserAction.followAction(dialog, props.post.user.id, props.post.user.username, props.post.user.is_following)
.then(_action => {
emit('post-follow-action', props.post.user.id, _action);
})
@ -285,46 +286,8 @@ const handleTweetAction = async (
}
};
const post = computed({
get: () => {
let post: Item.PostComponentProps = Object.assign(
{
texts: [],
imgs: [],
videos: [],
links: [],
attachments: [],
charge_attachments: [],
},
props.post,
);
post.contents.map((content) => {
if (+content.type === 1 || +content.type === 2) {
post.texts.push(content);
}
if (+content.type === 3) {
post.imgs.push(content);
}
if (+content.type === 4) {
post.videos.push(content);
}
if (+content.type === 6) {
post.links.push(content);
}
if (+content.type === 7) {
post.attachments.push(content);
}
if (+content.type === 8) {
post.charge_attachments.push(content);
}
});
return post;
},
set: (newVal) => {
props.post.upvote_count = newVal.upvote_count;
props.post.collection_count = newVal.collection_count;
},
});
// 使 usePostContent composable
const post = usePostContent(props.post);
const handlePostStar = () => {
postStar({
id: post.value.id,

@ -0,0 +1,40 @@
import { ref } from 'vue';
/**
* composable
*
*/
export function usePagination(initialPageSize: number = 20) {
const loading = ref(false);
const noMore = ref(false);
const page = ref(1);
const pageSize = ref(initialPageSize);
const totalPage = ref(0);
const reset = () => {
loading.value = false;
noMore.value = false;
page.value = 1;
totalPage.value = 0;
};
const nextPage = (loadCallback: () => void) => {
if (page.value < totalPage.value || totalPage.value == 0) {
noMore.value = false;
page.value++;
loadCallback();
} else {
noMore.value = true;
}
};
return {
loading,
noMore,
page,
pageSize,
totalPage,
reset,
nextPage,
};
}

@ -0,0 +1,53 @@
import { computed } from 'vue';
/**
* Post composable
* post.contents texts, imgs, videos, links, attachments, charge_attachments
*
*/
export function usePostContent(post: Item.PostProps, includeExtraFields: boolean = false) {
return computed({
get: () => {
let postData: Item.PostComponentProps = Object.assign(
{
texts: [],
imgs: [],
videos: [],
links: [],
attachments: [],
charge_attachments: [],
},
post,
);
postData.contents.map((content) => {
if (+content.type === 1 || +content.type === 2) {
postData.texts.push(content);
}
if (+content.type === 3) {
postData.imgs.push(content);
}
if (+content.type === 4) {
postData.videos.push(content);
}
if (+content.type === 6) {
postData.links.push(content);
}
if (+content.type === 7) {
postData.attachments.push(content);
}
if (+content.type === 8) {
postData.charge_attachments.push(content);
}
});
return postData;
},
set: (newVal) => {
post.upvote_count = newVal.upvote_count;
post.collection_count = newVal.collection_count;
if (includeExtraFields) {
post.comment_count = newVal.comment_count;
post.is_essence = newVal.is_essence;
}
},
});
}

@ -1,11 +1,17 @@
import { ref } from 'vue';
import { useDialog } from "naive-ui";
import { Api } from "./request";
const dialog = useDialog();
import { Api } from "../utils/request";
export default class UserAction {
static followAction(userId: number, userName: string, isFollowing: boolean) {
/**
* /
* @param dialog dialog
* @param userId ID
* @param userName
* @param isFollowing
*/
static followAction(dialog: ReturnType<typeof useDialog>, userId: number, userName: string, isFollowing: boolean) {
return new Promise<boolean>((resolve, reject) => {
dialog.success({
title: '提示',
@ -45,4 +51,41 @@ export default class UserAction {
});
}
}
/**
*
*
*/
static useWhisper() {
const showWhisper = ref(false);
const whisperReceiver = ref<Item.UserInfo>({
id: 0,
avatar: '',
username: '',
nickname: '',
is_admin: false,
is_friend: true,
is_following: false,
created_on: 0,
follows: 0,
followings: 0,
status: 1,
});
const onSendWhisper = (user: Item.UserInfo) => {
whisperReceiver.value = user;
showWhisper.value = true;
};
const whisperSuccess = () => {
showWhisper.value = false;
};
return {
showWhisper,
whisperReceiver,
onSendWhisper,
whisperSuccess,
};
}
}

@ -16,7 +16,7 @@
</div>
<n-list-item v-for="contact in list" :key="contact.user_id">
<follow-item :contact="contact" @send-whisper="onSendWhisper" />
<follow-item :contact="contact" @send-whisper="onSendWhisper" @unfollow-success="handleUnfollowSuccess" />
</n-list-item>
</div>
<!-- 私信组件 -->
@ -24,7 +24,7 @@
</n-list>
</div>
<n-space v-if="totalPage > 0" justify="center">
<InfiniteLoading class="load-more" :slots="{ complete: completeStr, error: '加载出错' }" @infinite="nextPage">
<InfiniteLoading class="load-more" :slots="{ complete: completeStr, error: '加载出错' }" @infinite="handleNextPage">
<template #spinner>
<div class="load-more-wrap">
<n-spin :size="14" v-if="!noMore" />
@ -40,38 +40,26 @@ import { ref, onMounted, computed } from 'vue';
import InfiniteLoading from 'v3-infinite-loading';
import { useRoute } from 'vue-router';
import { Api } from '@/utils/request';
import { usePagination } from '@/composables/usePagination';
import UserAction from '@/composables/useUserAction';
const route = useRoute();
const loading = ref(false);
const noMore = ref(false);
const list = ref<Item.ContactItemProps[]>([]);
const nickname = (route.query.n as string) || '粉丝详情';
const username = (route.query.s as string) || '';
const tabler = ref((route.query.t as string) || 'follows');
const page = ref(+(route.query.p as string) || 1);
const pageSize = ref(20);
const totalPage = ref(0);
const showWhisper = ref(false);
const whisperReceiver = ref<Item.UserInfo>({
id: 0,
avatar: '',
username: '',
nickname: '',
is_admin: false,
is_friend: true,
is_following: false,
created_on: 0,
follows: 0,
followings: 0,
status: 1,
});
const showAddFriendWhisper = ref(false);
// 使 usePagination composable
const { loading, noMore, page, pageSize, totalPage, reset, nextPage } = usePagination(20);
// 使 UserAction.useWhisper()
const { showWhisper, whisperReceiver, onSendWhisper, whisperSuccess } = UserAction.useWhisper();
function resetPage(tab: 'follows' | 'followings') {
list.value = [];
loading.value = false;
noMore.value = false;
page.value = 1;
totalPage.value = 0;
reset();
tabler.value = tab;
}
@ -83,23 +71,8 @@ const completeStr = computed(() => {
}
});
const onSendWhisper = (user: Item.UserInfo) => {
whisperReceiver.value = user;
showWhisper.value = true;
};
const whisperSuccess = () => {
showWhisper.value = false;
};
const nextPage = () => {
if (page.value < totalPage.value || totalPage.value == 0) {
noMore.value = false;
page.value++;
loadPage();
} else {
noMore.value = true;
}
const handleNextPage = () => {
nextPage(loadPage);
};
const changeTab = (tab: 'follows' | 'followings') => {
@ -115,6 +88,14 @@ const loadPage = () => {
}
};
const handleUnfollowSuccess = () => {
//
if (tabler.value === 'follows') {
resetPage('follows');
loadPage();
}
};
const loadFollows = (username: string, scrollToBottom: boolean = false) => {
if (list.value.length === 0) {
loading.value = true;

@ -31,23 +31,18 @@
</n-list-item>
<div class="style-wrap" v-else-if="showTrendsTag">
<n-space >
<n-button v-if="newestTweetsStyle !== 'newest'" size="small" :bordered="false" @click="onNewestTweets" class="style-item" secondary round>
全部
</n-button>
<n-button v-if="newestTweetsStyle === 'newest'" size="small" type="success" :bordered="false" @click="onNewestTweets" class="style-item" secondary round>
全部
</n-button>
<n-button v-if="newestTweetsStyle !== 'hots'" size="small" :bordered="false" @click="onHotTweets" class="style-item" secondary round>
热门推荐
</n-button>
<n-button v-if="newestTweetsStyle === 'hots'" size="small" type="success" :bordered="false" @click="onHotTweets" class="style-item" secondary round>
热门推荐
</n-button>
<n-button v-if="newestTweetsStyle !== 'following'" size="small" :bordered="false" @click="onFollowingTweets" class="style-item" secondary round>
正在关注
</n-button>
<n-button v-if="newestTweetsStyle === 'following'" size="small" type="success" :bordered="false" @click="onFollowingTweets" class="style-item" secondary round>
正在关注
<n-button
v-for="btn in filterButtons"
:key="btn.key"
size="small"
:type="newestTweetsStyle === btn.key ? 'success' : undefined"
:bordered="false"
@click="onFilterClick(btn.key, btn.index)"
class="style-item"
secondary
round
>
{{ btn.label }}
</n-button>
</n-space>
</div>
@ -76,7 +71,7 @@
</n-list>
<n-space v-if="totalPage > 0" justify="center">
<InfiniteLoading class="load-more" :slots="{ complete: '没有更多泡泡了', error: '加载出错' }" @infinite="nextPage()">
<InfiniteLoading class="load-more" :slots="{ complete: '没有更多泡泡了', error: '加载出错' }" @infinite="handleNextPage">
<template #spinner>
<div class="load-more-wrap">
<n-spin :size="14" v-if="!noMore" />
@ -103,6 +98,8 @@ import { useStoreUser } from '@/store/user';
import { useStoreProfile } from '@/store/profile';
import { storeToRefs } from 'pinia';
import { Api } from '@/utils/request';
import { usePagination } from '@/composables/usePagination';
import UserAction from '@/composables/useUserAction';
const storeMain = useStoreMain();
const storeUser = useStoreUser();
@ -116,6 +113,20 @@ const router = useRouter();
const dialog = useDialog();
const newestTweetsStyle = ref<'newest' | 'hots' | 'following'>('newest');
//
const filterButtons = [
{ key: 'newest' as const, label: '全部', index: 0 },
{ key: 'hots' as const, label: '热门推荐', index: 1 },
{ key: 'following' as const, label: '正在关注', index: 2 },
];
const onFilterClick = (key: 'newest' | 'hots' | 'following', index: number) => {
newestTweetsStyle.value = key;
handleBarClick(slideBarList.value[index], index);
};
//
const onNewestTweets = () => {
newestTweetsStyle.value = 'newest';
handleBarClick(slideBarList.value[0], 0);
@ -175,38 +186,16 @@ const user = reactive<Item.UserInfo>({
const inActionPost = ref<Item.PostProps | null>(null);
const title = ref<string>('泡泡广场');
const loading = ref(false);
const noMore = ref(false);
const targetStyle = ref<number>(1);
const targetUsername = ref<string>('');
const list = ref<any[]>([]);
const page = ref(1);
const pageSize = ref(20);
const totalPage = ref(0);
const showWhisper = ref(false);
const showAddFriendWhisper = ref(false);
const whisperReceiver = ref<Item.UserInfo>({
id: 0,
avatar: '',
username: '',
nickname: '',
is_admin: false,
is_friend: true,
is_following: false,
created_on: 0,
follows: 0,
followings: 0,
status: 1,
});
const onSendWhisper = (user: Item.UserInfo) => {
whisperReceiver.value = user;
showWhisper.value = true;
};
// 使 usePagination composable
const { loading, noMore, page, pageSize, totalPage, reset, nextPage } = usePagination(20);
const whisperSuccess = () => {
showWhisper.value = false;
};
// 使 UserAction.useWhisper()
const { showWhisper, whisperReceiver, onSendWhisper, whisperSuccess } = UserAction.useWhisper();
const openAddFriendWhisper = () => {
showAddFriendWhisper.value = true;
@ -257,6 +246,12 @@ function postFollowAction(userId: number, isFollowing: boolean) {
list.value[index].user.is_following = isFollowing;
}
}
// tabisFollowing false
if (targetStyle.value === 3 && !isFollowing) {
resetAll();
loadPosts('following');
}
}
const updateTitle = () => {
@ -286,16 +281,14 @@ const showTrendsBar = computed(() => {
);
});
const reset = () => {
loading.value = false;
noMore.value = false;
// reset list
const resetAll = () => {
reset();
list.value = [];
page.value = 1;
totalPage.value = 0;
};
const handleBarClick = (data: Item.SlideBarItem, index: number) => {
reset();
resetAll();
targetStyle.value = data.style;
if (route.query.q) {
route.query.q = null;
@ -483,18 +476,12 @@ const loadMorePosts = () => {
}
};
const nextPage = () => {
if (page.value < totalPage.value || totalPage.value == 0) {
noMore.value = false;
page.value++;
loadMorePosts();
} else {
noMore.value = true;
}
const handleNextPage = () => {
nextPage(loadMorePosts);
};
onMounted(() => {
reset();
resetAll();
loadContacts();
loadPosts('newest');
});
@ -508,7 +495,7 @@ watch(
(to, from) => {
updateTitle();
if (to.refresh !== from.refresh) {
reset();
resetAll();
setTimeout(() => {
loadContacts();
loadMorePosts();
@ -516,7 +503,7 @@ watch(
return;
}
if (from.path !== '/post' && to.path === '/') {
reset();
resetAll();
setTimeout(() => {
loadContacts();
loadMorePosts();

@ -153,7 +153,7 @@ import { useStoreUser } from '@/store/user';
import { useStoreProfile } from '@/store/profile';
import { storeToRefs } from 'pinia';
import { Api } from '@/utils/request';
import UserAction from '@/utils/useUserAction';
import UserAction from '@/composables/useUserAction';
type PageType = 'post' | 'comment' | 'highlight' | 'media' | 'star';
@ -515,7 +515,7 @@ const openDeleteFriend = () => {
});
};
const handleFollowUser = () => {
UserAction.followAction(user.id, user.username, user.is_following)
UserAction.followAction(dialog, user.id, user.username, user.is_following)
.then(_action => {
userLoading.value = false;
loadUser();

Loading…
Cancel
Save