Merge pull request #706 from rocboss/jc/orziz

update: optimize frontend code
pull/707/head
ROC 7 months ago committed by GitHub
commit 107ad5504c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,168 +0,0 @@
<template>
<div class="contact-item">
<n-thing content-indented>
<template #avatar>
<n-avatar :size="54" :src="contact.avatar" />
</template>
<template #header>
<span class="nickname-wrap">
<router-link
@click.stop
class="username-link"
:to="{
name: 'user',
query: { s: contact.username },
}"
>
{{ contact.nickname }}
</router-link>
</span>
<span class="username-wrap"> @{{ contact.username }} </span>
<!-- <n-tag
v-if="contact.is_following"
class="top-tag" type="success" size="small" round>
已关注
</n-tag> -->
<div class="user-info">
<span class="info-item">
UID. {{ contact.user_id }}
</span>
<span class="info-item">
{{ formatDate(contact.created_on) }}&nbsp;加入
</span>
</div>
</template>
<template #header-extra>
<div class="item-header-extra">
<n-dropdown
placement="bottom-end"
trigger="click"
size="small"
:options="actionOpts"
@select="handleAction"
>
<n-button quaternary circle>
<template #icon>
<n-icon>
<more-horiz-filled />
</n-icon>
</template>
</n-button>
</n-dropdown>
</div>
</template>
</n-thing>
</div>
</template>
<script setup lang="ts">
import { h, computed } from 'vue';
import { NIcon, DropdownOption } from 'naive-ui';
import type { Component } from 'vue';
import { formatDate } from '@/utils/formatTime';
import { MoreHorizFilled } from '@vicons/material';
import { PaperPlaneOutline } from '@vicons/ionicons5';
const emit = defineEmits<{
(e: 'send-whisper', user: Item.UserInfo): void;
}>();
const renderIcon = (icon: Component) => {
return () => {
return h(NIcon, null, {
default: () => h(icon),
});
};
};
const props = withDefaults(
defineProps<{
contact: Item.ContactItemProps;
}>(),
{},
);
const actionOpts = computed(() => {
let options: DropdownOption[] = [
{
label: '私信 @' + props.contact.username,
key: 'whisper',
icon: renderIcon(PaperPlaneOutline),
},
];
return options;
});
const handleAction = (item: 'whisper') => {
switch (item) {
case 'whisper':
const user: Item.UserInfo = {
id: props.contact.user_id,
avatar: props.contact.avatar,
username: props.contact.username,
nickname: props.contact.nickname,
is_admin: false,
is_friend: true,
is_following: false,
created_on: 0,
follows: 0,
followings: 0,
status: 1,
};
emit('send-whisper', user);
break;
default:
break;
}
};
</script>
<style lang="less" scoped>
.contact-item {
width: 100%;
box-sizing: border-box;
padding-left: 16px;
padding-right: 16px;
padding-top: 12px;
padding-bottom: 12px;
&:hover {
background: #f7f9f9;
}
.nickname-wrap {
line-height: 16px;
font-size: 16px;
}
.username-wrap {
line-height: 16px;
font-size: 16px;
}
.top-tag {
transform: scale(0.75);
}
.user-info {
.info-item {
font-size: 14px;
line-height: 14px;
margin-right: 8px;
opacity: 0.75;
}
}
.item-header-extra {
display: flex;
align-items: center;
opacity: 0.75;
}
}
.dark {
.contact-item {
&:hover {
background: #18181c;
}
background-color: rgba(16, 16, 20, 0.75);
}
}
</style>

@ -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,6 +157,7 @@ import { formatRelativeTime } from '@/utils/formatTime';
import { MoreHorizFilled } from '@vicons/material'; import { MoreHorizFilled } from '@vicons/material';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { Api } from '@/utils/request'; import { Api } from '@/utils/request';
import UserAction from '@/composables/useUserAction';
const defaultavatar = const defaultavatar =
'https://assets.paopao.info/public/avatar/default/admin.png'; 'https://assets.paopao.info/public/avatar/default/admin.png';
@ -225,44 +226,16 @@ const onHandleFollowAction = (message: Item.MessageProps) => {
message.type == 4 && message.sender_user_id == userInfo.value.id message.type == 4 && message.sender_user_id == userInfo.value.id
? message.receiver_user ? message.receiver_user
: message.sender_user; : message.sender_user;
dialog.success({ UserAction.followAction(dialog, user.id, user.username, user.is_following)
title: '提示', .then(_action => {
content: user.is_following = _action;
'确定' +
(user.is_following ? '取消关注 @' : '关注 @') +
user.username +
' 吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
if (user.is_following) {
Api.v1.user.post.unfollow({
user_id: user.id,
})
.then((_res) => {
window.$message.success('操作成功');
user.is_following = false;
// TODO: 这里暴力处理,简单重新加载,更好的做法是遍历所有message,如果是对应user就更新到新状态
setTimeout(() => {
emit('reload');
}, 50);
})
.catch((_err) => {});
} else {
Api.v1.user.post.follow({
user_id: user.id,
})
.then((_res) => {
window.$message.success('关注成功');
user.is_following = true;
// TODO: 这里暴力处理,简单重新加载,更好的做法是遍历所有message,如果是对应user就更新到新状态 // TODO: 这里暴力处理,简单重新加载,更好的做法是遍历所有message,如果是对应user就更新到新状态
setTimeout(() => { setTimeout(() => {
emit('reload'); emit('reload');
}, 50); }, 50);
}) })
.catch((_err) => {}); .catch(err => {
} console.log(err);
},
}); });
}; };

@ -1,458 +0,0 @@
<template>
<div class="post-item">
<n-thing content-indented>
<template #avatar>
<n-avatar round :size="30" :src="post.user.avatar" />
</template>
<template #header>
<span class="nickname-wrap">
<router-link
@click.stop
class="username-link"
:to="{
name: 'user',
query: { s: post.user.username },
}"
>
{{ post.user.nickname }}
</router-link>
</span>
<span class="username-wrap"> @{{ post.user.username }} </span>
<n-tag
v-if="post.is_top"
class="top-tag"
type="warning"
size="small"
round
>
置顶
</n-tag>
<n-tag
v-if="post.visibility == 1"
class="top-tag"
type="error"
size="small"
round
>
私密
</n-tag>
<n-tag
v-if="post.visibility == 2"
class="top-tag"
type="info"
size="small"
round
>
好友可见
</n-tag>
<div>
<span class="timestamp-mobile">
{{ formatPrettyDate(post.created_on) }} {{ post.ip_loc }}
</span>
</div>
</template>
<template #header-extra>
<div class="item-header-extra">
<n-dropdown
placement="bottom-end"
trigger="click"
size="small"
:options="tweetOptions"
@select="handleTweetAction"
>
<n-button quaternary circle>
<template #icon>
<n-icon>
<more-horiz-filled />
</n-icon>
</template>
</n-button>
</n-dropdown>
</div>
</template>
<template #description v-if="post.texts.length > 0">
<div @click="goPostDetail(post.id)">
<span v-for="content in post.texts"
:key="content.id"
class="post-text"
@click.stop="doClickText($event, post.id)"
v-html="preparePost(content.content, '展开', '收起', profile.tweetMobileEllipsisSize, inFoldStyle)"
></span>
</div>
</template>
<template #footer>
<post-attachment
v-if="post.attachments.length > 0"
:attachments="post.attachments" />
<post-attachment
v-if="post.charge_attachments.length > 0"
:attachments="post.charge_attachments"
:price="post.attachment_price"
/>
<post-image
v-if="post.imgs.length > 0"
:imgs="post.imgs" />
<post-video
v-if="post.videos.length > 0"
:videos="post.videos" />
<post-link
v-if="post.links.length > 0"
:links="post.links" />
</template>
<template #action>
<n-space justify="space-between">
<div class="opt-item" @click.stop="handlePostStar">
<n-icon size="18" class="opt-item-icon">
<heart-outline />
</n-icon>
{{ post.upvote_count }}
</div>
<div class="opt-item" @click.stop="goPostDetail(post.id)">
<n-icon size="18" class="opt-item-icon">
<chatbox-outline />
</n-icon>
{{ post.comment_count }}
</div>
<div class="opt-item" @click.stop="handlePostCollection">
<n-icon size="18" class="opt-item-icon">
<bookmark-outline />
</n-icon>
{{ post.collection_count }}
</div>
</n-space>
</template>
</n-thing>
</div>
</template>
<script setup lang="ts">
import { h, ref, computed } from 'vue';
import type { Component } from 'vue';
import { NIcon, DropdownOption } from 'naive-ui';
import { useStoreMain } from '@/store/main';
import { useRouter } from 'vue-router';
import { formatPrettyDate } from '@/utils/formatTime';
import { preparePost } from '@/utils/content';
import { postStar, postCollection } from '@/api/post';
import {
PaperPlaneOutline,
HeartOutline,
BookmarkOutline,
ChatboxOutline,
ShareSocialOutline,
PersonAddOutline,
PersonRemoveOutline,
BodyOutline,
WalkOutline,
} from '@vicons/ionicons5';
import { MoreHorizFilled } from '@vicons/material';
import copy from 'copy-to-clipboard';
import { useStoreProfile } from '@/store/profile';
import { storeToRefs } from 'pinia';
const router = useRouter();
const storeMain = useStoreMain();
const storeProfile = useStoreProfile();
const { profile } = storeToRefs(storeProfile);
const inFoldStyle = ref<boolean>(true);
const props = withDefaults(
defineProps<{
post: Item.PostProps;
isOwner: boolean;
addFriendAction: boolean;
addFollowAction: boolean;
}>(),
{},
);
const emit = defineEmits<{
(e: 'send-whisper', user: Item.UserInfo): void;
(e: 'handle-follow-action', user: Item.PostProps): void;
(e: 'handle-friend-action', user: Item.PostProps): void;
}>();
const renderIcon = (icon: Component) => {
return () => {
return h(NIcon, null, {
default: () => h(icon),
});
};
};
const tweetOptions = computed(() => {
let options: DropdownOption[] = [];
if (!props.isOwner) {
options.push({
label: '私信 @' + props.post.user.username,
key: 'whisper',
icon: renderIcon(PaperPlaneOutline),
});
}
if (!props.isOwner && props.addFollowAction) {
if (props.post.user.is_following) {
options.push({
label: '取消关注 @' + props.post.user.username,
key: 'unfollow',
icon: renderIcon(WalkOutline),
});
} else {
options.push({
label: '关注 @' + props.post.user.username,
key: 'follow',
icon: renderIcon(BodyOutline),
});
}
}
if (!props.isOwner && props.addFriendAction) {
if (props.post.user.is_friend) {
options.push({
label: '删除好友 @' + props.post.user.username,
key: 'delete',
icon: renderIcon(PersonRemoveOutline),
});
} else {
options.push({
label: '添加朋友 @' + props.post.user.username,
key: 'requesting',
icon: renderIcon(PersonAddOutline),
});
}
}
options.push({
label: '复制链接',
key: 'copyTweetLink',
icon: renderIcon(ShareSocialOutline),
});
return options;
});
const handleTweetAction = async (
item:
| 'copyTweetLink'
| 'whisper'
| 'follow'
| 'unfollow'
| 'delete'
| 'requesting',
) => {
switch (item) {
case 'copyTweetLink':
copy(
`${window.location.origin}/#/post?id=${post.value.id}&share=copy_link&t=${new Date().getTime()}`,
);
window.$message.success('链接已复制到剪贴板');
break;
case 'whisper':
emit('send-whisper', props.post.user);
break;
case 'delete':
case 'requesting':
emit('handle-friend-action', props.post);
break;
case 'follow':
case 'unfollow':
emit('handle-follow-action', props.post);
break;
default:
break;
}
};
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;
},
});
const handlePostStar = () => {
postStar({
id: post.value.id,
})
.then((res) => {
if (res.status) {
post.value = {
...post.value,
upvote_count: post.value.upvote_count + 1,
};
} else {
post.value = {
...post.value,
upvote_count:
post.value.upvote_count > 0 ? post.value.upvote_count - 1 : 0,
};
}
})
.catch((err) => {
console.log(err);
});
};
const handlePostCollection = () => {
postCollection({
id: post.value.id,
})
.then((res) => {
if (res.status) {
post.value = {
...post.value,
collection_count: post.value.collection_count + 1,
};
} else {
post.value = {
...post.value,
collection_count:
post.value.collection_count > 0
? post.value.collection_count - 1
: 0,
};
}
})
.catch((err) => {
console.log(err);
});
};
const goPostDetail = (id: number) => {
router.push({
name: 'post',
query: {
id,
},
});
};
const doClickText = (e: MouseEvent, id: number) => {
const detail = (e.target as any).dataset.detail;
if (detail && detail !== 'post') {
const d = detail.split(':');
if (d.length === 2) {
storeMain.doRefresh();
if (d[0] === 'tag') {
router.push({
name: 'home',
query: {
q: d[1],
t: 'tag',
},
});
} else {
router.push({
name: 'user',
query: {
s: d[1],
},
});
}
}
} else if (detail && detail === 'post') {
inFoldStyle.value = !inFoldStyle.value;
} else {
goPostDetail(id);
}
};
</script>
<style lang="less">
.post-item {
width: 100%;
padding: 16px;
box-sizing: border-box;
.nickname-wrap {
font-size: 14px;
}
.username-wrap {
font-size: 14px;
opacity: 0.75;
}
.top-tag {
transform: scale(0.75);
}
.timestamp-mobile {
margin-top: 2px;
opacity: 0.75;
font-size: 11px;
}
.item-header-extra {
display: flex;
align-items: center;
opacity: 0.75;
.timestamp {
font-size: 12px;
}
}
.post-text {
text-align: justify;
overflow: hidden;
white-space: pre-wrap;
word-break: break-all;
}
.opt-item {
display: flex;
align-items: center;
opacity: 0.7;
.opt-item-icon {
margin-right: 10px;
}
}
&:hover {
background: #f7f9f9;
cursor: pointer;
}
.n-thing-avatar {
margin-top: 0;
}
.n-thing-header {
line-height: 16px;
margin-bottom: 8px !important;
}
}
.dark {
.post-item {
&:hover {
background: #18181c;
}
background-color: rgba(16, 16, 20, 0.75);
}
}
</style>

@ -259,6 +259,8 @@ import copy from 'copy-to-clipboard';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useStoreUser } from '@/store/user'; import { useStoreUser } from '@/store/user';
import { Api } from '@/utils/request'; import { Api } from '@/utils/request';
import UserAction from '@/composables/useUserAction';
import { usePostContent } from '@/composables/usePostContent';
const useFriendship = const useFriendship =
import.meta.env.VITE_USE_FRIENDSHIP.toLowerCase() === 'true'; import.meta.env.VITE_USE_FRIENDSHIP.toLowerCase() === 'true';
@ -313,48 +315,8 @@ const emit = defineEmits<{
(e: 'reload', post_id: number): void; (e: 'reload', post_id: number): void;
}>(); }>();
const post = computed({ // 使用 usePostContent composable (包含额外字段)
get: () => { const post = usePostContent(props.post, true);
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;
},
});
const renderIcon = (icon: Component) => { const renderIcon = (icon: Component) => {
return () => { return () => {
@ -491,37 +453,10 @@ const adminOptions = computed(() => {
}); });
const onHandleFollowAction = (post: Item.PostProps) => { const onHandleFollowAction = (post: Item.PostProps) => {
dialog.success({ UserAction.followAction(dialog, post.user.id, post.user.username, post.user.is_following)
title: '提示', .then(_action => {
content: post.user.is_following = _action;
'确定' +
(post.user.is_following ? '取消关注 @' : '关注 @') +
props.post.user.username +
' 吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
if (post.user.is_following) {
Api.v1.user.post.unfollow({
user_id: post.user.id,
})
.then((_res) => {
window.$message.success('操作成功');
post.user.is_following = false;
})
.catch((_err) => {});
} else {
Api.v1.user.post.follow({
user_id: post.user.id,
}) })
.then((_res) => {
window.$message.success('操作成功');
post.user.is_following = true;
})
.catch((_err) => {});
}
},
});
}; };
const goPostDetail = (id: number) => { const goPostDetail = (id: number) => {

@ -45,16 +45,21 @@
> >
好友可见 好友可见
</n-tag> </n-tag>
<div v-if="isMobile">
<span class="timestamp-mobile">
{{ formatPrettyDate(post.created_on) }} {{ post.ip_loc }}
</span>
</div>
</template> </template>
<template #header-extra> <template #header-extra>
<div class="item-header-extra"> <div class="item-header-extra">
<span class="timestamp"> <span v-if="!isMobile" class="timestamp">
{{ post.ip_loc ? post.ip_loc + ' · ' : post.ip_loc }} {{ post.ip_loc ? post.ip_loc + ' · ' : post.ip_loc }}
{{ formatPrettyDate(post.created_on) }} {{ formatPrettyDate(post.created_on) }}
</span> </span>
<n-dropdown <n-dropdown
placement="bottom-end" placement="bottom-end"
trigger="hover" :trigger="isMobile ? 'click' : 'hover'"
size="small" size="small"
:options="tweetOptions" :options="tweetOptions"
@select="handleTweetAction" @select="handleTweetAction"
@ -70,7 +75,16 @@
</div> </div>
</template> </template>
<template #description v-if="post.texts.length > 0"> <template #description v-if="post.texts.length > 0">
<div v-if="isMobile" @click="goPostDetail(post.id)">
<span v-for="content in post.texts"
:key="content.id"
class="post-text"
@click.stop="doClickText($event, post.id)"
v-html="preparePost(content.content, '展开', '收起', profile.tweetMobileEllipsisSize, inFoldStyle)"
></span>
</div>
<span <span
v-else
v-for="content in post.texts" v-for="content in post.texts"
:key="content.id" :key="content.id"
class="post-text hover" class="post-text hover"
@ -128,7 +142,7 @@
import { h, ref, computed } from 'vue'; import { h, ref, computed } from 'vue';
import { useStoreMain } from '@/store/main'; import { useStoreMain } from '@/store/main';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { NIcon } from 'naive-ui'; import { NIcon, useDialog } from 'naive-ui';
import type { Component } from 'vue'; import type { Component } from 'vue';
import type { DropdownOption } from 'naive-ui'; import type { DropdownOption } from 'naive-ui';
import { formatPrettyDate } from '@/utils/formatTime'; import { formatPrettyDate } from '@/utils/formatTime';
@ -149,6 +163,9 @@ import { MoreHorizFilled } from '@vicons/material';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import { useStoreProfile } from '@/store/profile'; import { useStoreProfile } from '@/store/profile';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { Api } from '@/utils/request';
import UserAction from '@/composables/useUserAction';
import { usePostContent } from '@/composables/usePostContent';
const router = useRouter(); const router = useRouter();
@ -156,21 +173,26 @@ const storeMain = useStoreMain();
const storeProfile = useStoreProfile(); const storeProfile = useStoreProfile();
const { profile } = storeToRefs(storeProfile); const { profile } = storeToRefs(storeProfile);
const dialog = useDialog();
const inFoldStyle = ref<boolean>(true); const inFoldStyle = ref<boolean>(true);
const props = withDefaults( const props = withDefaults(defineProps<{
defineProps<{
post: Item.PostProps; post: Item.PostProps;
isOwner: boolean; isOwner: boolean;
addFriendAction: boolean; addFriendAction?: boolean;
addFollowAction: boolean; addFollowAction?: boolean;
}>(), isMobile?: boolean;
{}, }>(), {
); addFollowAction: false,
addFriendAction: false,
isMobile: false,
});
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'send-whisper', user: Item.UserInfo): void; (e: 'send-whisper', user: Item.UserInfo): void;
(e: 'handle-follow-action', user: Item.PostProps): void; (e: 'handle-follow-action', user: Item.PostProps): void;
(e: 'handle-friend-action', user: Item.PostProps): void; (e: 'handle-friend-action', user: Item.PostProps): void;
(e: 'post-follow-action', user_id: number, is_following: boolean): void;
}>(); }>();
const renderIcon = (icon: Component) => { const renderIcon = (icon: Component) => {
@ -253,6 +275,10 @@ const handleTweetAction = async (
break; break;
case 'follow': case 'follow':
case 'unfollow': case 'unfollow':
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);
})
emit('handle-follow-action', props.post); emit('handle-follow-action', props.post);
break; break;
default: default:
@ -260,46 +286,8 @@ const handleTweetAction = async (
} }
}; };
const post = computed({ // 使用 usePostContent composable
get: () => { const post = usePostContent(props.post);
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;
},
});
const handlePostStar = () => { const handlePostStar = () => {
postStar({ postStar({
id: post.value.id, id: post.value.id,
@ -402,6 +390,11 @@ const doClickText = (e: MouseEvent, id: number) => {
.top-tag { .top-tag {
transform: scale(0.75); transform: scale(0.75);
} }
.timestamp-mobile {
margin-top: 2px;
opacity: 0.75;
font-size: 11px;
}
.item-header-extra { .item-header-extra {
display: flex; display: flex;
align-items: center; align-items: center;

@ -1,5 +1,5 @@
<template> <template>
<div class="follow-item"> <div class="user-card">
<n-thing content-indented> <n-thing content-indented>
<template #avatar> <template #avatar>
<n-avatar :size="54" :src="contact.avatar" /> <n-avatar :size="54" :src="contact.avatar" />
@ -19,7 +19,7 @@
</span> </span>
<span class="username-wrap"> @{{ contact.username }} </span> <span class="username-wrap"> @{{ contact.username }} </span>
<n-tag <n-tag
v-if="contact.is_following" v-if="showFollowingTag && contact.is_following"
class="top-tag" type="success" size="small" round> class="top-tag" type="success" size="small" round>
已关注 已关注
</n-tag> </n-tag>
@ -62,12 +62,26 @@ import { NIcon, useDialog, DropdownOption } from 'naive-ui';
import { formatDate } from '@/utils/formatTime'; import { formatDate } from '@/utils/formatTime';
import { MoreHorizFilled } from '@vicons/material'; import { MoreHorizFilled } from '@vicons/material';
import { PaperPlaneOutline, BodyOutline, WalkOutline } from '@vicons/ionicons5'; import { PaperPlaneOutline, BodyOutline, WalkOutline } from '@vicons/ionicons5';
import { Api } from '@/utils/request'; import UserAction from '@/composables/useUserAction';
const dialog = useDialog(); const dialog = useDialog();
const props = withDefaults(
defineProps<{
contact: Item.ContactItemProps;
type?: 'contact' | 'follow';
}>(),
{
type: 'contact',
},
);
const showFollowingTag = computed(() => props.type === 'follow');
const enableFollowAction = computed(() => props.type === 'follow');
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'send-whisper', user: Item.UserInfo): void; (e: 'send-whisper', user: Item.UserInfo): void;
(e: 'unfollow-success'): void;
}>(); }>();
const renderIcon = (icon: Component) => { const renderIcon = (icon: Component) => {
@ -79,50 +93,19 @@ const renderIcon = (icon: Component) => {
}; };
const handleFollowUser = () => { const handleFollowUser = () => {
dialog.success({ const wasFollowing = props.contact.is_following;
title: '提示', UserAction.followAction(dialog, props.contact.user_id, props.contact.username, props.contact.is_following)
content: .then(_action => {
'确定' + props.contact.is_following = _action;
(props.contact.is_following ? '取消关注 @' : '关注 @') + if (wasFollowing && !_action) {
props.contact.username + emit('unfollow-success');
' 吗?', }
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
if (props.contact.is_following) {
Api.v1.user.post.unfollow({
user_id: props.contact.user_id,
})
.then((_res) => {
window.$message.success('取消关注成功');
props.contact.is_following = false;
})
.catch((err) => {
console.log(err);
});
} else {
Api.v1.user.post.follow({
user_id: props.contact.user_id,
})
.then((_res) => {
window.$message.success('关注成功');
props.contact.is_following = true;
}) })
.catch((err) => { .catch(err => {
console.log(err); console.log(err);
}); });
}
},
});
}; };
const props = withDefaults(
defineProps<{
contact: Item.ContactItemProps;
}>(),
{},
);
const actionOpts = computed(() => { const actionOpts = computed(() => {
let options: DropdownOption[] = [ let options: DropdownOption[] = [
{ {
@ -131,6 +114,8 @@ const actionOpts = computed(() => {
icon: renderIcon(PaperPlaneOutline), icon: renderIcon(PaperPlaneOutline),
}, },
]; ];
if (enableFollowAction.value) {
if (props.contact.is_following) { if (props.contact.is_following) {
options.push({ options.push({
label: '取消关注 @' + props.contact.username, label: '取消关注 @' + props.contact.username,
@ -144,6 +129,8 @@ const actionOpts = computed(() => {
icon: renderIcon(BodyOutline), icon: renderIcon(BodyOutline),
}); });
} }
}
return options; return options;
}); });
@ -176,13 +163,10 @@ const handleAction = (item: 'follow' | 'unfollow' | 'whisper') => {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.follow-item { .user-card {
display: border-box;
width: 100%; width: 100%;
padding-left: 16px; box-sizing: border-box;
padding-right: 16px; padding: 12px 16px;
padding-top: 12px;
padding-bottom: 12px;
&:hover { &:hover {
background: #f7f9f9; background: #f7f9f9;
@ -216,7 +200,7 @@ const handleAction = (item: 'follow' | 'unfollow' | 'whisper') => {
} }
.dark { .dark {
.follow-item { .user-card {
&:hover { &:hover {
background: #18181c; background: #18181c;
} }

@ -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;
}
},
});
}

@ -0,0 +1,91 @@
import { ref } from 'vue';
import { useDialog } from "naive-ui";
import { Api } from "../utils/request";
export default class UserAction {
/**
* 用户关注/取消关注操作
* @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: '提示',
content:
'确定' +
(isFollowing ? '取消关注 @' : '关注 @') +
userName +
' 吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
if (isFollowing) {
Api.v1.user.post.unfollow({
user_id: userId,
})
.then((_res) => {
window.$message.success('操作成功');
resolve(false);
})
.catch((_err) => {
reject(_err);
});
} else {
Api.v1.user.post.follow({
user_id: userId,
})
.then((_res) => {
window.$message.success('关注成功');
resolve(true);
})
.catch((_err) => {
reject(_err);
});
}
},
});
});
}
/**
* 用户私信相关操作
* 保持与原有代码完全一致的行为
*/
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,
};
}
}

@ -81,7 +81,7 @@ export function createApi<T>(): Readonly<T> {
return request({ return request({
method, method,
url: _path.join('/'), url: _path.join('/'),
data: args[0], ...(method === 'get' ? { params: args[0] } : { data: args[0] }),
}); });
}, { }, {
get(target: any, p: string) { get(target: any, p: string) {

@ -11,25 +11,15 @@
<n-empty size="large" description="暂无数据" /> <n-empty size="large" description="暂无数据" />
</div> </div>
<div v-if="desktopModelShow">
<n-list-item v-for="post in list" :key="post.id"> <n-list-item v-for="post in list" :key="post.id">
<post-item :post="post" <post-item :post="post"
:isOwner="userInfo.id == post.user_id" :isOwner="userInfo.id == post.user_id"
:addFollowAction="true" :isMobile="!desktopModelShow"
addFollowAction
@send-whisper="onSendWhisper" @send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" /> @post-follow-action="postFollowAction" />
</n-list-item> </n-list-item>
</div> </div>
<div v-else>
<n-list-item v-for="post in list" :key="post.id">
<mobile-post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" />
</n-list-item>
</div>
</div>
<!-- 私信组件 --> <!-- 私信组件 -->
<whisper :show="showWhisper" :user="whisperReceiver" @success="whisperSuccess" /> <whisper :show="showWhisper" :user="whisperReceiver" @success="whisperSuccess" />
</n-list> </n-list>
@ -94,37 +84,6 @@ const whisperSuccess = () => {
showWhisper.value = false; showWhisper.value = false;
}; };
const onHandleFollowAction = (post: Item.PostProps) => {
dialog.success({
title: '提示',
content:
'确定' + (post.user.is_following ? '取消关注' : '关注') + '该用户吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
if (post.user.is_following) {
Api.v1.user.post.unfollow({
user_id: post.user.id,
})
.then((_res) => {
window.$message.success('操作成功');
postFollowAction(post.user_id, false);
})
.catch((_err) => {});
} else {
Api.v1.user.post.follow({
user_id: post.user.id,
})
.then((_res) => {
window.$message.success('关注成功');
postFollowAction(post.user_id, true);
})
.catch((_err) => {});
}
},
});
};
function postFollowAction(userId: number, isFollowing: boolean) { function postFollowAction(userId: number, isFollowing: boolean) {
for (let index in list.value) { for (let index in list.value) {
if (list.value[index].user_id == userId) { if (list.value[index].user_id == userId) {

@ -12,38 +12,33 @@
</div> </div>
<n-list-item class="list-item" v-for="contact in list" :key="contact.user_id"> <n-list-item class="list-item" v-for="contact in list" :key="contact.user_id">
<contact-item :contact="contact" @send-whisper="onSendWhisper" /> <user-card type="contact" :contact="contact" @send-whisper="onSendWhisper" />
</n-list-item> </n-list-item>
</div> </div>
<!-- 私信组件 --> <!-- 私信组件 -->
<whisper :show="showWhisper" :user="whisperReceiver" @success="whisperSuccess" /> <whisper :show="showWhisper" :user="whisperReceiver" @success="whisperSuccess" />
</n-list> </n-list>
<infinite-load-more
:total-page="totalPage"
:no-more="noMore"
complete-text="没有更多好友了"
@load-more="nextPage"
/>
</div> </div>
<n-space v-if="totalPage > 0" justify="center">
<InfiniteLoading class="load-more" :slots="{ complete: '没有更多好友了', error: '加载出错' }" @infinite="nextPage">
<template #spinner>
<div class="load-more-wrap">
<n-spin :size="14" v-if="!noMore" />
<span class="load-more-spinner">{{ noMore ? '没有更多好友了' : '加载更多' }}</span>
</div>
</template>
</InfiniteLoading>
</n-space>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import InfiniteLoading from 'v3-infinite-loading';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { Api } from '@/utils/request'; import { Api } from '@/utils/request';
import { usePagination } from '@/composables/usePagination';
import InfiniteLoadMore from '@/components/infinite-load-more.vue';
import UserCard from '@/components/user-card.vue';
const route = useRoute(); const route = useRoute();
const loading = ref(false); const { loading, noMore, page, pageSize, totalPage } = usePagination(20);
const noMore = ref(false);
const list = ref<Item.ContactItemProps[]>([]); const list = ref<Item.ContactItemProps[]>([]);
const page = ref(+(route.query.p as string) || 1);
const pageSize = ref(20);
const totalPage = ref(0);
const showWhisper = ref(false); const showWhisper = ref(false);
const whisperReceiver = ref<Item.UserInfo>({ const whisperReceiver = ref<Item.UserInfo>({
id: 0, id: 0,
@ -59,6 +54,9 @@ const whisperReceiver = ref<Item.UserInfo>({
status: 1, status: 1,
}); });
// 初始化页码
page.value = +(route.query.p as string) || 1;
const onSendWhisper = (user: Item.UserInfo) => { const onSendWhisper = (user: Item.UserInfo) => {
whisperReceiver.value = user; whisperReceiver.value = user;
showWhisper.value = true; showWhisper.value = true;
@ -117,22 +115,6 @@ const loadContacts = (scrollToBottom: boolean = false) => {
</script> </script>
<style lang="less" scoped> <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;
}
}
}
.dark { .dark {
.main-content-wrap, .empty-wrap, .skeleton-wrap { .main-content-wrap, .empty-wrap, .skeleton-wrap {
background-color: rgba(16, 16, 20, 0.75); background-color: rgba(16, 16, 20, 0.75);

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

@ -31,23 +31,18 @@
</n-list-item> </n-list-item>
<div class="style-wrap" v-else-if="showTrendsTag"> <div class="style-wrap" v-else-if="showTrendsTag">
<n-space > <n-space >
<n-button v-if="newestTweetsStyle !== 'newest'" size="small" :bordered="false" @click="onNewestTweets" class="style-item" secondary round> <n-button
全部 v-for="btn in filterButtons"
</n-button> :key="btn.key"
<n-button v-if="newestTweetsStyle === 'newest'" size="small" type="success" :bordered="false" @click="onNewestTweets" class="style-item" secondary round> size="small"
全部 :type="newestTweetsStyle === btn.key ? 'success' : undefined"
</n-button> :bordered="false"
<n-button v-if="newestTweetsStyle !== 'hots'" size="small" :bordered="false" @click="onHotTweets" class="style-item" secondary round> @click="onFilterClick(btn.key, btn.index)"
热门推荐 class="style-item"
</n-button> secondary
<n-button v-if="newestTweetsStyle === 'hots'" size="small" type="success" :bordered="false" @click="onHotTweets" class="style-item" secondary round> round
热门推荐 >
</n-button> {{ btn.label }}
<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> </n-button>
</n-space> </n-space>
</div> </div>
@ -59,27 +54,16 @@
<div class="empty-wrap" v-if="list.length === 0"> <div class="empty-wrap" v-if="list.length === 0">
<n-empty size="large" description="暂无数据" /> <n-empty size="large" description="暂无数据" />
</div> </div>
<div v-if="desktopModelShow">
<n-list-item v-for="post in list" :key="post.id"> <n-list-item v-for="post in list" :key="post.id">
<post-item :post="post" <post-item :post="post"
:isOwner="userInfo.id == post.user_id" :isOwner="userInfo.id == post.user_id"
:addFollowAction="true" :isMobile="!desktopModelShow"
addFollowAction
@send-whisper="onSendWhisper" @send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" @post-follow-action="postFollowAction"
@handle-friend-action="onHandleFriendAction" /> @handle-friend-action="onHandleFriendAction" />
</n-list-item> </n-list-item>
</div> </div>
<div v-else>
<n-list-item v-for="post in list" :key="post.id">
<mobile-post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction"
@handle-friend-action="onHandleFriendAction" />
</n-list-item>
</div>
</div>
<!-- 私信组件 --> <!-- 私信组件 -->
<whisper :show="showWhisper" :user="whisperReceiver" @success="whisperSuccess" /> <whisper :show="showWhisper" :user="whisperReceiver" @success="whisperSuccess" />
<!-- 加好友组件 --> <!-- 加好友组件 -->
@ -87,7 +71,7 @@
</n-list> </n-list>
<n-space v-if="totalPage > 0" justify="center"> <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> <template #spinner>
<div class="load-more-wrap"> <div class="load-more-wrap">
<n-spin :size="14" v-if="!noMore" /> <n-spin :size="14" v-if="!noMore" />
@ -114,6 +98,8 @@ import { useStoreUser } from '@/store/user';
import { useStoreProfile } from '@/store/profile'; import { useStoreProfile } from '@/store/profile';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { Api } from '@/utils/request'; import { Api } from '@/utils/request';
import { usePagination } from '@/composables/usePagination';
import UserAction from '@/composables/useUserAction';
const storeMain = useStoreMain(); const storeMain = useStoreMain();
const storeUser = useStoreUser(); const storeUser = useStoreUser();
@ -127,6 +113,20 @@ const router = useRouter();
const dialog = useDialog(); const dialog = useDialog();
const newestTweetsStyle = ref<'newest' | 'hots' | 'following'>('newest'); 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 = () => { const onNewestTweets = () => {
newestTweetsStyle.value = 'newest'; newestTweetsStyle.value = 'newest';
handleBarClick(slideBarList.value[0], 0); handleBarClick(slideBarList.value[0], 0);
@ -186,38 +186,16 @@ const user = reactive<Item.UserInfo>({
const inActionPost = ref<Item.PostProps | null>(null); const inActionPost = ref<Item.PostProps | null>(null);
const title = ref<string>('泡泡广场'); const title = ref<string>('泡泡广场');
const loading = ref(false);
const noMore = ref(false);
const targetStyle = ref<number>(1); const targetStyle = ref<number>(1);
const targetUsername = ref<string>(''); const targetUsername = ref<string>('');
const list = ref<any[]>([]); 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 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) => { // 使用 usePagination composable
whisperReceiver.value = user; const { loading, noMore, page, pageSize, totalPage, reset, nextPage } = usePagination(20);
showWhisper.value = true;
};
const whisperSuccess = () => { // 使用 UserAction.useWhisper()
showWhisper.value = false; const { showWhisper, whisperReceiver, onSendWhisper, whisperSuccess } = UserAction.useWhisper();
};
const openAddFriendWhisper = () => { const openAddFriendWhisper = () => {
showAddFriendWhisper.value = true; showAddFriendWhisper.value = true;
@ -262,46 +240,18 @@ const onHandleFriendAction = (post: Item.PostProps) => {
} }
}; };
const onHandleFollowAction = (post: Item.PostProps) => {
dialog.success({
title: '提示',
content:
'确定' +
(post.user.is_following ? '取消关注 @' : '关注 @') +
post.user.username +
' 吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
if (post.user.is_following) {
Api.v1.user.post.unfollow({
user_id: post.user.id,
})
.then((_res) => {
window.$message.success('操作成功');
postFollowAction(post.user_id, false);
})
.catch((_err) => {});
} else {
Api.v1.user.post.follow({
user_id: post.user.id,
})
.then((_res) => {
window.$message.success('关注成功');
postFollowAction(post.user_id, true);
})
.catch((_err) => {});
}
},
});
};
function postFollowAction(userId: number, isFollowing: boolean) { function postFollowAction(userId: number, isFollowing: boolean) {
for (let index in list.value) { for (let index in list.value) {
if (list.value[index].user_id == userId) { if (list.value[index].user_id == userId) {
list.value[index].user.is_following = isFollowing; list.value[index].user.is_following = isFollowing;
} }
} }
// 如果是在【正在关注】tab,且是取消关注操作(isFollowing 为 false),则刷新列表
if (targetStyle.value === 3 && !isFollowing) {
resetAll();
loadPosts('following');
}
} }
const updateTitle = () => { const updateTitle = () => {
@ -331,16 +281,14 @@ const showTrendsBar = computed(() => {
); );
}); });
const reset = () => { // 重写 reset 方法以包含 list 的重置
loading.value = false; const resetAll = () => {
noMore.value = false; reset();
list.value = []; list.value = [];
page.value = 1;
totalPage.value = 0;
}; };
const handleBarClick = (data: Item.SlideBarItem, index: number) => { const handleBarClick = (data: Item.SlideBarItem, index: number) => {
reset(); resetAll();
targetStyle.value = data.style; targetStyle.value = data.style;
if (route.query.q) { if (route.query.q) {
route.query.q = null; route.query.q = null;
@ -528,18 +476,12 @@ const loadMorePosts = () => {
} }
}; };
const nextPage = () => { const handleNextPage = () => {
if (page.value < totalPage.value || totalPage.value == 0) { nextPage(loadMorePosts);
noMore.value = false;
page.value++;
loadMorePosts();
} else {
noMore.value = true;
}
}; };
onMounted(() => { onMounted(() => {
reset(); resetAll();
loadContacts(); loadContacts();
loadPosts('newest'); loadPosts('newest');
}); });
@ -553,7 +495,7 @@ watch(
(to, from) => { (to, from) => {
updateTitle(); updateTitle();
if (to.refresh !== from.refresh) { if (to.refresh !== from.refresh) {
reset(); resetAll();
setTimeout(() => { setTimeout(() => {
loadContacts(); loadContacts();
loadMorePosts(); loadMorePosts();
@ -561,7 +503,7 @@ watch(
return; return;
} }
if (from.path !== '/post' && to.path === '/') { if (from.path !== '/post' && to.path === '/') {
reset(); resetAll();
setTimeout(() => { setTimeout(() => {
loadContacts(); loadContacts();
loadMorePosts(); loadMorePosts();

@ -50,16 +50,12 @@
</div> </div>
</div> </div>
</n-list> </n-list>
<n-space v-if="totalPage > 0" justify="center"> <infinite-load-more
<InfiniteLoading class="load-more" :slots="{ complete: '没有更多消息了', error: '加载出错' }" @infinite="nextPage"> :total-page="totalPage"
<template #spinner> :no-more="noMore"
<div class="load-more-wrap"> complete-text="没有更多消息了"
<n-spin :size="14" v-if="!noMore" /> @load-more="nextPage"
<span class="load-more-spinner">{{ noMore ? '没有更多消息了' : '加载更多' }}</span> />
</div>
</template>
</InfiniteLoading>
</n-space>
</div> </div>
</template> </template>
@ -69,7 +65,6 @@ import type { Component } from 'vue';
import { NIcon, DropdownOption } from 'naive-ui'; import { NIcon, DropdownOption } from 'naive-ui';
import { useStoreMain } from '@/store/main'; import { useStoreMain } from '@/store/main';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import InfiniteLoading from 'v3-infinite-loading';
import { import {
LayersOutline as AllIcon, LayersOutline as AllIcon,
AtOutline as SystemIcon, AtOutline as SystemIcon,
@ -81,17 +76,18 @@ import {
import { useStoreUser } from '@/store/user'; import { useStoreUser } from '@/store/user';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { Api } from '@/utils/request'; import { Api } from '@/utils/request';
import { usePagination } from '@/composables/usePagination';
import InfiniteLoadMore from '@/components/infinite-load-more.vue';
const storeMain = useStoreMain(); const storeMain = useStoreMain();
const storeUser = useStoreUser(); const storeUser = useStoreUser();
const { unreadMsgCount } = storeToRefs(storeMain); const { unreadMsgCount } = storeToRefs(storeMain);
const route = useRoute(); const route = useRoute();
const loading = ref(false); const { loading, noMore, page, pageSize, totalPage, reset: resetPagination } = usePagination(20);
const noMore = ref(false); // 初始化页码
const page = ref(+(route.query.p as string) || 1); page.value = +(route.query.p as string) || 1;
const pageSize = ref(20);
const totalPage = ref(0);
const list = ref<Item.MessageProps[]>([]); const list = ref<Item.MessageProps[]>([]);
const messageStyle = ref< const messageStyle = ref<
'所有消息' | '系统消息' | '我的私信' | '好友申请' | '未读消息' '所有消息' | '系统消息' | '我的私信' | '好友申请' | '未读消息'
@ -115,9 +111,7 @@ const whisperReceiver = ref<Item.UserInfo>({
}); });
const reset = () => { const reset = () => {
noMore.value = false; resetPagination();
page.value = 1;
totalPage.value = 0;
list.value = []; list.value = [];
}; };
@ -363,22 +357,6 @@ onMounted(() => {
</script> </script>
<style lang="less" scoped> <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;
}
}
}
.title { .title {
padding-top: 4px; padding-top: 4px;
opacity: 0.9; opacity: 0.9;

@ -92,101 +92,15 @@
<div class="empty-wrap" v-if="list.length === 0"> <div class="empty-wrap" v-if="list.length === 0">
<n-empty size="large" description="暂无数据" /> <n-empty size="large" description="暂无数据" />
</div> </div>
<div v-if="desktopModelShow"> <n-list-item v-for="post in listData" :key="post.id">
<div v-if="pageType === 'post'">
<n-list-item v-for="post in postList" :key="post.id">
<post-item :post="post" <post-item :post="post"
:isOwner="userInfo.id == post.user_id" :isOwner="userInfo.id == post.user_id"
:addFollowAction="true" :addFollowAction="true"
:isMobile="!desktopModelShow"
@send-whisper="onSendWhisper" @send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" /> @post-follow-action="postFollowAction" />
</n-list-item> </n-list-item>
</div> </div>
<div v-if="pageType === 'comment'">
<n-list-item v-for="post in commentList" :key="post.id">
<post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" />
</n-list-item>
</div>
<div v-if="pageType === 'highlight'">
<n-list-item v-for="post in highlightList" :key="post.id">
<post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" />
</n-list-item>
</div>
<div v-if="pageType === 'media'">
<n-list-item v-for="post in mediaList" :key="post.id">
<post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" />
</n-list-item>
</div>
<div v-if="pageType === 'star'">
<n-list-item v-for="post in starList" :key="post.id">
<post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" />
</n-list-item>
</div>
</div>
<div v-else>
<div v-if="pageType === 'post'">
<n-list-item v-for="post in postList" :key="post.id">
<mobile-post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" />
</n-list-item>
</div>
<div v-if="pageType === 'comment'">
<n-list-item v-for="post in commentList" :key="post.id">
<mobile-post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" />
</n-list-item>
</div>
<div v-if="pageType === 'highlight'">
<n-list-item v-for="post in highlightList" :key="post.id">
<mobile-post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" />
</n-list-item>
</div>
<div v-if="pageType === 'media'">
<n-list-item v-for="post in mediaList" :key="post.id">
<mobile-post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" />
</n-list-item>
</div>
<div v-if="pageType === 'star'">
<n-list-item v-for="post in starList" :key="post.id">
<mobile-post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" />
</n-list-item>
</div>
</div>
</div>
<!-- 私信组件 --> <!-- 私信组件 -->
<whisper :show="showWhisper" :user="whisperReceiver" @success="whisperSuccess" /> <whisper :show="showWhisper" :user="whisperReceiver" @success="whisperSuccess" />
</n-list> </n-list>
@ -219,6 +133,8 @@ import { useStoreUser } from '@/store/user';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { Api } from '@/utils/request'; import { Api } from '@/utils/request';
type PageType = 'post' | 'comment' | 'highlight' | 'media' | 'star';
const storeMain = useStoreMain(); const storeMain = useStoreMain();
const storeUser = useStoreUser(); const storeUser = useStoreUser();
const { refresh, desktopModelShow } = storeToRefs(storeMain); const { refresh, desktopModelShow } = storeToRefs(storeMain);
@ -235,9 +151,7 @@ const commentList = ref<Item.PostProps[]>([]);
const highlightList = ref<Item.PostProps[]>([]); const highlightList = ref<Item.PostProps[]>([]);
const mediaList = ref<Item.PostProps[]>([]); const mediaList = ref<Item.PostProps[]>([]);
const starList = ref<Item.PostProps[]>([]); const starList = ref<Item.PostProps[]>([]);
const pageType = ref<'post' | 'comment' | 'highlight' | 'media' | 'star'>( const pageType = ref<PageType>('post');
'post',
);
const postPage = ref(+(route.query.p as string) || 1); const postPage = ref(+(route.query.p as string) || 1);
const commentPage = ref(1); const commentPage = ref(1);
const highlightPage = ref(1); const highlightPage = ref(1);
@ -266,6 +180,23 @@ const whisperReceiver = ref<Item.UserInfo>({
status: 1, status: 1,
}); });
const listData = computed(() => {
switch (pageType.value) {
case 'post':
return postList.value;
case 'comment':
return commentList.value;
case 'highlight':
return highlightList.value;
case 'media':
return mediaList.value;
case 'star':
return starList.value;
default:
return [];
}
})
const renderIcon = (icon: Component) => { const renderIcon = (icon: Component) => {
return () => { return () => {
return h(NIcon, null, { return h(NIcon, null, {
@ -309,40 +240,6 @@ const whisperSuccess = () => {
showWhisper.value = false; showWhisper.value = false;
}; };
const onHandleFollowAction = (post: Item.PostProps) => {
dialog.success({
title: '提示',
content:
'确定' +
(post.user.is_following ? '取消关注 @' : '关注 @') +
post.user.username +
' 吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
if (post.user.is_following) {
Api.v1.user.post.unfollow({
user_id: post.user.id,
})
.then((_res) => {
window.$message.success('操作成功');
postFollowAction(post.user_id, false);
})
.catch((_err) => {});
} else {
Api.v1.user.post.follow({
user_id: post.user.id,
})
.then((_res) => {
window.$message.success('关注成功');
postFollowAction(post.user_id, true);
})
.catch((_err) => {});
}
},
});
};
function postFollowAction(userId: number, isFollowing: boolean) { function postFollowAction(userId: number, isFollowing: boolean) {
updateFolloing(postList.value, userId, isFollowing); updateFolloing(postList.value, userId, isFollowing);
updateFolloing(commentList.value, userId, isFollowing); updateFolloing(commentList.value, userId, isFollowing);
@ -366,122 +263,27 @@ function updateFolloing(
} }
const loadPage = () => { const loadPage = () => {
switch (pageType.value) { loadPostsByStyle(pageType.value);
case 'post':
loadPosts();
break;
case 'comment':
loadCommentPosts();
break;
case 'highlight':
loadHighlightPosts();
break;
case 'media':
loadMediaPosts();
break;
case 'star':
loadStarPosts();
break;
}
}; };
const loadPosts = () => { const styleListMap = {
loading.value = true; post: { list: postList, totalPage: postTotalPage },
Api.v1.user.get.posts({ comment: { list: commentList, totalPage: commentTotalPage },
username: userInfo.value.username, highlight: { list: highlightList, totalPage: highlightTotalPage },
style: 'post', media: { list: mediaList, totalPage: mediaTotalPage },
page: page.value, star: { list: starList, totalPage: starTotalPage },
page_size: pageSize.value,
})
.then((rsp) => {
loading.value = false;
if (rsp.list.length === 0) {
noMore.value = true;
}
if (page.value > 1) {
list.value = list.value.concat(rsp.list);
} else {
list.value = rsp.list || [];
window.scrollTo(0, 0);
}
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
postList.value = list.value;
postTotalPage.value = totalPage.value;
})
.catch((err) => {
list.value = [];
if (page.value > 1) {
page.value--;
}
loading.value = false;
});
}; };
const loadCommentPosts = () => { const stylePageMap = {
loading.value = true; post: postPage,
Api.v1.user.get.posts({ comment: commentPage,
username: userInfo.value.username, highlight: highlightPage,
style: 'comment', media: mediaPage,
page: page.value, star: starPage,
page_size: pageSize.value,
})
.then((rsp) => {
loading.value = false;
if (rsp.list.length === 0) {
noMore.value = true;
}
if (page.value > 1) {
list.value = list.value.concat(rsp.list);
} else {
list.value = rsp.list || [];
window.scrollTo(0, 0);
}
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
commentList.value = list.value;
commentTotalPage.value = totalPage.value;
})
.catch((err) => {
list.value = [];
if (page.value > 1) {
page.value--;
}
loading.value = false;
});
};
const loadHighlightPosts = () => {
loading.value = true;
Api.v1.user.get.posts({
username: userInfo.value.username,
style: 'highlight',
page: page.value,
page_size: pageSize.value,
})
.then((rsp) => {
loading.value = false;
if (rsp.list.length === 0) {
noMore.value = true;
}
if (page.value > 1) {
list.value = list.value.concat(rsp.list);
} else {
list.value = rsp.list || [];
window.scrollTo(0, 0);
}
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
highlightList.value = list.value;
highlightTotalPage.value = totalPage.value;
})
.catch((err) => {
list.value = [];
if (page.value > 1) {
page.value--;
}
loading.value = false;
});
}; };
const loadMediaPosts = () => { function loadPostsByStyle(style: keyof typeof styleListMap) {
loading.value = true; loading.value = true;
Api.v1.user.get.posts({ Api.v1.user.get.posts({
username: userInfo.value.username, username: userInfo.value.username,
style: 'media', style,
page: page.value, page: page.value,
page_size: pageSize.value, page_size: pageSize.value,
}) })
@ -497,108 +299,27 @@ const loadMediaPosts = () => {
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value); totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
mediaList.value = list.value; styleListMap[style].list.value = list.value;
mediaTotalPage.value = totalPage.value; styleListMap[style].totalPage.value = totalPage.value;
}) })
.catch((err) => { .catch((_err) => {
list.value = []; list.value = [];
if (page.value > 1) { if (page.value > 1) {
page.value--; page.value--;
} }
loading.value = false; loading.value = false;
}); });
};
const loadStarPosts = () => {
loading.value = true;
Api.v1.user.get.posts({
username: userInfo.value.username,
style: 'star',
page: page.value,
page_size: pageSize.value,
})
.then((rsp) => {
loading.value = false;
if (rsp.list.length === 0) {
noMore.value = true;
} }
if (page.value > 1) { const changeTab = (tab: PageType) => {
list.value = list.value.concat(rsp.list);
} else {
list.value = rsp.list || [];
window.scrollTo(0, 0);
}
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
starList.value = list.value;
starTotalPage.value = totalPage.value;
})
.catch((err) => {
list.value = [];
if (page.value > 1) {
page.value--;
}
loading.value = false;
});
};
const changeTab = (
tab: 'post' | 'comment' | 'highlight' | 'media' | 'star',
) => {
pageType.value = tab; pageType.value = tab;
switch (pageType.value) { list.value = styleListMap[tab].list.value;
case 'post': page.value = stylePageMap[tab].value;
list.value = postList.value; totalPage.value = styleListMap[tab].totalPage.value;
page.value = postPage.value; loadPostsByStyle(tab);
totalPage.value = postTotalPage.value;
loadPosts();
break;
case 'comment':
list.value = commentList.value;
page.value = commentPage.value;
totalPage.value = commentTotalPage.value;
loadCommentPosts();
break;
case 'highlight':
list.value = highlightList.value;
page.value = highlightPage.value;
totalPage.value = highlightTotalPage.value;
loadHighlightPosts();
break;
case 'media':
list.value = mediaList.value;
page.value = mediaPage.value;
totalPage.value = mediaTotalPage.value;
loadMediaPosts();
break;
case 'star':
list.value = starList.value;
page.value = starPage.value;
totalPage.value = starTotalPage.value;
loadStarPosts();
break;
}
}; };
const updatePage = () => { const updatePage = () => {
switch (pageType.value) { stylePageMap[pageType.value].value = page.value;
case 'post': loadPostsByStyle(pageType.value);
postPage.value = page.value;
loadPosts();
break;
case 'comment':
commentPage.value = page.value;
loadCommentPosts();
break;
case 'highlight':
highlightPage.value = page.value;
loadHighlightPosts();
break;
case 'media':
mediaPage.value = page.value;
loadMediaPosts();
break;
case 'star':
starPage.value = page.value;
loadStarPosts();
break;
}
}; };
const nextPage = () => { const nextPage = () => {
if (page.value < totalPage.value || totalPage.value == 0) { if (page.value < totalPage.value || totalPage.value == 0) {

@ -104,101 +104,15 @@
<div class="empty-wrap" v-if="list.length === 0"> <div class="empty-wrap" v-if="list.length === 0">
<n-empty size="large" description="暂无数据" /> <n-empty size="large" description="暂无数据" />
</div> </div>
<div v-if="desktopModelShow"> <n-list-item v-for="post in listData" :key="post.id">
<div v-if="pageType === 'post'">
<n-list-item v-for="post in postList" :key="post.id">
<post-item :post="post" <post-item :post="post"
:isOwner="userInfo.id == post.user_id" :isOwner="userInfo.id == post.user_id"
:addFollowAction="true" :isMobile="!desktopModelShow"
addFollowAction
@send-whisper="onSendWhisper" @send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" /> @post-follow-action="postFollowAction" />
</n-list-item> </n-list-item>
</div> </div>
<div v-if="pageType === 'comment'">
<n-list-item v-for="post in commentList" :key="post.id">
<post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" />
</n-list-item>
</div>
<div v-if="pageType === 'highlight'">
<n-list-item v-for="post in highlightList" :key="post.id">
<post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" />
</n-list-item>
</div>
<div v-if="pageType === 'media'">
<n-list-item v-for="post in mediaList" :key="post.id">
<post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" />
</n-list-item>
</div>
<div v-if="pageType === 'star'">
<n-list-item v-for="post in starList" :key="post.id">
<post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" />
</n-list-item>
</div>
</div>
<div v-else>
<div v-if="pageType === 'post'">
<n-list-item v-for="post in postList" :key="post.id">
<mobile-post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" />
</n-list-item>
</div>
<div v-if="pageType === 'comment'">
<n-list-item v-for="post in commentList" :key="post.id">
<mobile-post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" />
</n-list-item>
</div>
<div v-if="pageType === 'highlight'">
<n-list-item v-for="post in highlightList" :key="post.id">
<mobile-post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" />
</n-list-item>
</div>
<div v-if="pageType === 'media'">
<n-list-item v-for="post in mediaList" :key="post.id">
<mobile-post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" />
</n-list-item>
</div>
<div v-if="pageType === 'star'">
<n-list-item v-for="post in starList" :key="post.id">
<mobile-post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
@send-whisper="onSendWhisper"
@handle-follow-action="onHandleFollowAction" />
</n-list-item>
</div>
</div>
</div>
</n-list> </n-list>
<n-space v-if="totalPage > 0" justify="center"> <n-space v-if="totalPage > 0" justify="center">
@ -239,6 +153,9 @@ import { useStoreUser } from '@/store/user';
import { useStoreProfile } from '@/store/profile'; import { useStoreProfile } from '@/store/profile';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { Api } from '@/utils/request'; import { Api } from '@/utils/request';
import UserAction from '@/composables/useUserAction';
type PageType = 'post' | 'comment' | 'highlight' | 'media' | 'star';
const dialog = useDialog(); const dialog = useDialog();
@ -279,9 +196,7 @@ const mediaList = ref<Item.PostProps[]>([]);
const starList = ref<Item.PostProps[]>([]); const starList = ref<Item.PostProps[]>([]);
const username = ref(route.query.s || ''); const username = ref(route.query.s || '');
const page = ref(+(route.query.p as string) || 1); const page = ref(+(route.query.p as string) || 1);
const pageType = ref<'post' | 'comment' | 'highlight' | 'media' | 'star'>( const pageType = ref<PageType>('post');
'post',
);
const postPage = ref(+(route.query.p as string) || 1); const postPage = ref(+(route.query.p as string) || 1);
const commentPage = ref(1); const commentPage = ref(1);
const highlightPage = ref(1); const highlightPage = ref(1);
@ -295,6 +210,23 @@ const highlightTotalPage = ref(0);
const mediaTotalPage = ref(0); const mediaTotalPage = ref(0);
const starTotalPage = ref(0); const starTotalPage = ref(0);
const listData = computed(() => {
switch (pageType.value) {
case 'post':
return postList.value;
case 'comment':
return commentList.value;
case 'highlight':
return highlightList.value;
case 'media':
return mediaList.value;
case 'star':
return starList.value;
default:
return [];
}
})
const onSendWhisper = (receiver: Item.UserInfo) => { const onSendWhisper = (receiver: Item.UserInfo) => {
user.id = receiver.id; user.id = receiver.id;
user.username = receiver.username; user.username = receiver.username;
@ -303,40 +235,6 @@ const onSendWhisper = (receiver: Item.UserInfo) => {
showWhisper.value = true; showWhisper.value = true;
}; };
const onHandleFollowAction = (post: Item.PostProps) => {
dialog.success({
title: '提示',
content:
'确定' +
(post.user.is_following ? '取消关注 @' : '关注 @') +
post.user.username +
' 吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
if (post.user.is_following) {
Api.v1.user.post.unfollow({
user_id: post.user.id,
})
.then((_res) => {
window.$message.success('操作成功');
postFollowAction(post.user_id, false);
})
.catch((_err) => {});
} else {
Api.v1.user.post.follow({
user_id: post.user.id,
})
.then((_res) => {
window.$message.success('关注成功');
postFollowAction(post.user_id, true);
})
.catch((_err) => {});
}
},
});
};
function postFollowAction(userId: number, isFollowing: boolean) { function postFollowAction(userId: number, isFollowing: boolean) {
updateFolloing(postList, userId, isFollowing); updateFolloing(postList, userId, isFollowing);
updateFolloing(commentList, userId, isFollowing); updateFolloing(commentList, userId, isFollowing);
@ -382,122 +280,20 @@ const reset = () => {
starTotalPage.value = 0; starTotalPage.value = 0;
}; };
const loadPage = () => { const loadPage = () => {
switch (pageType.value) { loadPostsByStyle(pageType.value);
case 'post':
loadPosts();
break;
case 'comment':
loadCommentPosts();
break;
case 'highlight':
loadHighlightPosts();
break;
case 'media':
loadMediaPosts();
break;
case 'star':
loadStarPosts();
break;
}
};
const loadPosts = () => {
loading.value = true;
Api.v1.user.get.posts({
username: username.value as string,
style: 'post',
page: page.value,
page_size: pageSize.value,
})
.then((rsp) => {
loading.value = false;
if (rsp.list.length === 0) {
noMore.value = true;
}
if (page.value > 1) {
list.value = list.value.concat(rsp.list);
} else {
list.value = rsp.list || [];
window.scrollTo(0, 0);
}
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
postList.value = list.value;
postTotalPage.value = totalPage.value;
})
.catch((err) => {
list.value = [];
if (page.value > 1) {
page.value--;
}
loading.value = false;
});
};
const loadCommentPosts = () => {
loading.value = true;
Api.v1.user.get.posts({
username: username.value as string,
style: 'comment',
page: page.value,
page_size: pageSize.value,
})
.then((rsp) => {
loading.value = false;
if (rsp.list.length === 0) {
noMore.value = true;
}
if (page.value > 1) {
list.value = list.value.concat(rsp.list);
} else {
list.value = rsp.list || [];
window.scrollTo(0, 0);
}
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
commentList.value = list.value;
commentTotalPage.value = totalPage.value;
})
.catch((err) => {
list.value = [];
if (page.value > 1) {
page.value--;
}
loading.value = false;
});
}; };
const loadHighlightPosts = () => { const styleListMap = {
loading.value = true; post: { list: postList, totalPage: postTotalPage },
Api.v1.user.get.posts({ comment: { list: commentList, totalPage: commentTotalPage },
username: username.value as string, highlight: { list: highlightList, totalPage: highlightTotalPage },
style: 'highlight', media: { list: mediaList, totalPage: mediaTotalPage },
page: page.value, star: { list: starList, totalPage: starTotalPage },
page_size: pageSize.value,
})
.then((rsp) => {
loading.value = false;
if (rsp.list.length === 0) {
noMore.value = true;
}
if (page.value > 1) {
list.value = list.value.concat(rsp.list);
} else {
list.value = rsp.list || [];
window.scrollTo(0, 0);
}
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
highlightList.value = list.value;
highlightTotalPage.value = totalPage.value;
})
.catch((err) => {
list.value = [];
if (page.value > 1) {
page.value--;
}
loading.value = false;
});
}; };
const loadMediaPosts = () => { function loadPostsByStyle(style: keyof typeof styleListMap) {
loading.value = true; loading.value = true;
Api.v1.user.get.posts({ Api.v1.user.get.posts({
username: username.value as string, username: username.value as string,
style: 'media', style,
page: page.value, page: page.value,
page_size: pageSize.value, page_size: pageSize.value,
}) })
@ -513,10 +309,10 @@ const loadMediaPosts = () => {
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value); totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
mediaList.value = list.value; styleListMap[style].list.value = list.value;
mediaTotalPage.value = totalPage.value; styleListMap[style].totalPage.value = totalPage.value;
}) })
.catch((err) => { .catch((_err) => {
list.value = []; list.value = [];
if (page.value > 1) { if (page.value > 1) {
page.value--; page.value--;
@ -524,73 +320,19 @@ const loadMediaPosts = () => {
loading.value = false; loading.value = false;
}); });
}; };
const loadStarPosts = () => { const stylePageMap = {
loading.value = true; post: postPage,
Api.v1.user.get.posts({ comment: commentPage,
username: username.value as string, highlight: highlightPage,
style: 'star', media: mediaPage,
page: page.value, star: starPage,
page_size: pageSize.value,
})
.then((rsp) => {
loading.value = false;
if (rsp.list.length === 0) {
noMore.value = true;
}
if (page.value > 1) {
list.value = list.value.concat(rsp.list);
} else {
list.value = rsp.list || [];
window.scrollTo(0, 0);
}
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
starList.value = list.value;
starTotalPage.value = totalPage.value;
})
.catch((err) => {
list.value = [];
if (page.value > 1) {
page.value--;
}
loading.value = false;
});
}; };
const changeTab = ( function changeTab(tab: PageType) {
tab: 'post' | 'comment' | 'highlight' | 'media' | 'star',
) => {
pageType.value = tab; pageType.value = tab;
switch (pageType.value) { list.value = styleListMap[tab].list.value;
case 'post': page.value = stylePageMap[tab].value;
list.value = postList.value; totalPage.value = styleListMap[tab].totalPage.value;
page.value = postPage.value; loadPostsByStyle(tab);
totalPage.value = postTotalPage.value;
loadPosts();
break;
case 'comment':
list.value = commentList.value;
page.value = commentPage.value;
totalPage.value = commentTotalPage.value;
loadCommentPosts();
break;
case 'highlight':
list.value = highlightList.value;
page.value = highlightPage.value;
totalPage.value = highlightTotalPage.value;
loadHighlightPosts();
break;
case 'media':
list.value = mediaList.value;
page.value = mediaPage.value;
totalPage.value = mediaTotalPage.value;
loadMediaPosts();
break;
case 'star':
list.value = starList.value;
page.value = starPage.value;
totalPage.value = starTotalPage.value;
loadStarPosts();
break;
}
}; };
const loadUser = () => { const loadUser = () => {
userLoading.value = true; userLoading.value = true;
@ -621,28 +363,8 @@ const loadUser = () => {
}); });
}; };
const updatePage = () => { const updatePage = () => {
switch (pageType.value) { stylePageMap[pageType.value].value = page.value;
case 'post': loadPostsByStyle(pageType.value);
postPage.value = page.value;
loadPosts();
break;
case 'comment':
commentPage.value = page.value;
loadCommentPosts();
break;
case 'highlight':
highlightPage.value = page.value;
loadHighlightPosts();
break;
case 'media':
mediaPage.value = page.value;
loadMediaPosts();
break;
case 'star':
starPage.value = page.value;
loadStarPosts();
break;
}
}; };
const openWhisper = () => { const openWhisper = () => {
showWhisper.value = true; showWhisper.value = true;
@ -780,10 +502,10 @@ const openDeleteFriend = () => {
Api.v1.friend.post.delete({ Api.v1.friend.post.delete({
user_id: user.id, user_id: user.id,
}) })
.then((res) => { .then((_res) => {
userLoading.value = false; userLoading.value = false;
user.is_friend = false; user.is_friend = false;
loadPosts(); loadPostsByStyle('post');
}) })
.catch((err) => { .catch((err) => {
userLoading.value = false; userLoading.value = false;
@ -793,46 +515,15 @@ const openDeleteFriend = () => {
}); });
}; };
const handleFollowUser = () => { const handleFollowUser = () => {
dialog.success({ UserAction.followAction(dialog, user.id, user.username, user.is_following)
title: '提示', .then(_action => {
content:
'确定' +
(user.is_following ? '取消关注 @' : '关注 @') +
user.username +
' 吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
userLoading.value = true;
if (user.is_following) {
Api.v1.user.post.unfollow({
user_id: user.id,
})
.then((_res) => {
userLoading.value = false; userLoading.value = false;
window.$message.success('操作成功');
loadUser(); loadUser();
}) })
.catch((err) => { .catch(err => {
userLoading.value = false; userLoading.value = false;
console.log(err); console.log(err);
}); });
} else {
Api.v1.user.post.follow({
user_id: user.id,
})
.then((_res) => {
userLoading.value = false;
window.$message.success('操作成功');
loadUser();
})
.catch((err) => {
userLoading.value = false;
console.log(err);
});
}
},
});
}; };
const banUser = () => { const banUser = () => {
dialog.warning({ dialog.warning({

Loading…
Cancel
Save