diff --git a/web/src/components/follow-item.vue b/web/src/components/follow-item.vue index 5bd4e135..e768d5a7 100644 --- a/web/src/components/follow-item.vue +++ b/web/src/components/follow-item.vue @@ -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); diff --git a/web/src/components/infinite-load-more.vue b/web/src/components/infinite-load-more.vue new file mode 100644 index 00000000..91e59e58 --- /dev/null +++ b/web/src/components/infinite-load-more.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/web/src/components/message-item.vue b/web/src/components/message-item.vue index 514f32cc..58dc27a3 100644 --- a/web/src/components/message-item.vue +++ b/web/src/components/message-item.vue @@ -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: 这里暴力处理,简单重新加载,更好的做法是遍历所有message,如果是对应user就更新到新状态 diff --git a/web/src/components/post-detail.vue b/web/src/components/post-detail.vue index 3e041257..02b9800d 100644 --- a/web/src/components/post-detail.vue +++ b/web/src/components/post-detail.vue @@ -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; }) diff --git a/web/src/components/post-item.vue b/web/src/components/post-item.vue index 7c1c7e62..0b05c5cb 100644 --- a/web/src/components/post-item.vue +++ b/web/src/components/post-item.vue @@ -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, diff --git a/web/src/composables/usePagination.ts b/web/src/composables/usePagination.ts new file mode 100644 index 00000000..6b5a73f7 --- /dev/null +++ b/web/src/composables/usePagination.ts @@ -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, + }; +} diff --git a/web/src/composables/usePostContent.ts b/web/src/composables/usePostContent.ts new file mode 100644 index 00000000..ce5cb845 --- /dev/null +++ b/web/src/composables/usePostContent.ts @@ -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; + } + }, + }); +} diff --git a/web/src/utils/useUserAction.ts b/web/src/composables/useUserAction.ts similarity index 51% rename from web/src/utils/useUserAction.ts rename to web/src/composables/useUserAction.ts index 2770acef..20695ae5 100644 --- a/web/src/utils/useUserAction.ts +++ b/web/src/composables/useUserAction.ts @@ -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, userId: number, userName: string, isFollowing: boolean) { return new Promise((resolve, reject) => { dialog.success({ title: '提示', @@ -45,4 +51,41 @@ export default class UserAction { }); } -} \ No newline at end of file + /** + * 用户私信相关操作 + * 保持与原有代码完全一致的行为 + */ + static useWhisper() { + const showWhisper = ref(false); + const whisperReceiver = ref({ + 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, + }; + } + +} diff --git a/web/src/views/Following.vue b/web/src/views/Following.vue index a0ef8e22..eb0988ac 100644 --- a/web/src/views/Following.vue +++ b/web/src/views/Following.vue @@ -16,7 +16,7 @@ - + @@ -24,7 +24,7 @@ - +