Merge pull request #706 from rocboss/jc/orziz

update: optimize frontend code
pull/707/head
ROC 6 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 { storeToRefs } from 'pinia';
import { Api } from '@/utils/request';
import UserAction from '@/composables/useUserAction';
const defaultavatar =
'https://assets.paopao.info/public/avatar/default/admin.png';
@ -221,49 +222,21 @@ const emit = defineEmits<{
}>();
const onHandleFollowAction = (message: Item.MessageProps) => {
let user =
message.type == 4 && message.sender_user_id == userInfo.value.id
? message.receiver_user
: message.sender_user;
dialog.success({
title: '',
content:
'' +
(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就更新到新状态
setTimeout(() => {
emit('reload');
}, 50);
})
.catch((_err) => {});
}
},
});
let user =
message.type == 4 && message.sender_user_id == userInfo.value.id
? message.receiver_user
: message.sender_user;
UserAction.followAction(dialog, user.id, user.username, user.is_following)
.then(_action => {
user.is_following = _action;
// TODO: 这里暴力处理简单重新加载更好的做法是遍历所有message如果是对应user就更新到新状态
setTimeout(() => {
emit('reload');
}, 50);
})
.catch(err => {
console.log(err);
});
};
const handleAction = (item: 'whisper' | 'follow' | 'unfollow') => {

@ -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 { useStoreUser } from '@/store/user';
import { Api } from '@/utils/request';
import UserAction from '@/composables/useUserAction';
import { usePostContent } from '@/composables/usePostContent';
const useFriendship =
import.meta.env.VITE_USE_FRIENDSHIP.toLowerCase() === 'true';
@ -313,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 () => {
@ -491,37 +453,10 @@ const adminOptions = computed(() => {
});
const onHandleFollowAction = (post: Item.PostProps) => {
dialog.success({
title: '',
content:
'' +
(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) => {});
}
},
});
UserAction.followAction(dialog, post.user.id, post.user.username, post.user.is_following)
.then(_action => {
post.user.is_following = _action;
})
};
const goPostDetail = (id: number) => {

@ -45,16 +45,21 @@
>
</n-tag>
<div v-if="isMobile">
<span class="timestamp-mobile">
{{ formatPrettyDate(post.created_on) }} {{ post.ip_loc }}
</span>
</div>
</template>
<template #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 }}
{{ formatPrettyDate(post.created_on) }}
</span>
<n-dropdown
placement="bottom-end"
trigger="hover"
:trigger="isMobile ? 'click' : 'hover'"
size="small"
:options="tweetOptions"
@select="handleTweetAction"
@ -70,7 +75,16 @@
</div>
</template>
<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
v-else
v-for="content in post.texts"
:key="content.id"
class="post-text hover"
@ -128,7 +142,7 @@
import { h, ref, computed } from 'vue';
import { useStoreMain } from '@/store/main';
import { useRouter } from 'vue-router';
import { NIcon } from 'naive-ui';
import { NIcon, useDialog } from 'naive-ui';
import type { Component } from 'vue';
import type { DropdownOption } from 'naive-ui';
import { formatPrettyDate } from '@/utils/formatTime';
@ -149,6 +163,9 @@ import { MoreHorizFilled } from '@vicons/material';
import copy from 'copy-to-clipboard';
import { useStoreProfile } from '@/store/profile';
import { storeToRefs } from 'pinia';
import { Api } from '@/utils/request';
import UserAction from '@/composables/useUserAction';
import { usePostContent } from '@/composables/usePostContent';
const router = useRouter();
@ -156,21 +173,26 @@ const storeMain = useStoreMain();
const storeProfile = useStoreProfile();
const { profile } = storeToRefs(storeProfile);
const dialog = useDialog();
const inFoldStyle = ref<boolean>(true);
const props = withDefaults(
defineProps<{
const props = withDefaults(defineProps<{
post: Item.PostProps;
isOwner: boolean;
addFriendAction: boolean;
addFollowAction: boolean;
}>(),
{},
);
addFriendAction?: boolean;
addFollowAction?: boolean;
isMobile?: boolean;
}>(), {
addFollowAction: false,
addFriendAction: false,
isMobile: false,
});
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;
(e: 'post-follow-action', user_id: number, is_following: boolean): void;
}>();
const renderIcon = (icon: Component) => {
@ -253,53 +275,19 @@ const handleTweetAction = async (
break;
case 'follow':
case 'unfollow':
emit('handle-follow-action', props.post);
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);
break;
default:
break;
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;
},
});
// 使用 usePostContent composable
const post = usePostContent(props.post);
const handlePostStar = () => {
postStar({
id: post.value.id,
@ -402,6 +390,11 @@ const doClickText = (e: MouseEvent, id: number) => {
.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;

@ -1,5 +1,5 @@
<template>
<div class="follow-item">
<div class="user-card">
<n-thing content-indented>
<template #avatar>
<n-avatar :size="54" :src="contact.avatar" />
@ -17,9 +17,9 @@
{{ contact.nickname }}
</router-link>
</span>
<span class="username-wrap"> @{{ contact.username }} </span>
<span class="username-wrap"> @{{ contact.username }} </span>
<n-tag
v-if="contact.is_following"
v-if="showFollowingTag && contact.is_following"
class="top-tag" type="success" size="small" round>
</n-tag>
@ -62,12 +62,26 @@ import { NIcon, useDialog, DropdownOption } from 'naive-ui';
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 '@/composables/useUserAction';
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<{
(e: 'send-whisper', user: Item.UserInfo): void;
(e: 'unfollow-success'): void;
}>();
const renderIcon = (icon: Component) => {
@ -79,50 +93,19 @@ const renderIcon = (icon: Component) => {
};
const handleFollowUser = () => {
dialog.success({
title: '',
content:
'' +
(props.contact.is_following ? ' @' : ' @') +
props.contact.username +
' ',
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) => {
console.log(err);
});
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);
});
};
const props = withDefaults(
defineProps<{
contact: Item.ContactItemProps;
}>(),
{},
);
const actionOpts = computed(() => {
let options: DropdownOption[] = [
{
@ -131,19 +114,23 @@ const actionOpts = computed(() => {
icon: renderIcon(PaperPlaneOutline),
},
];
if (props.contact.is_following) {
options.push({
label: ' @' + props.contact.username,
key: 'unfollow',
icon: renderIcon(WalkOutline),
});
} else {
options.push({
label: ' @' + props.contact.username,
key: 'follow',
icon: renderIcon(BodyOutline),
});
if (enableFollowAction.value) {
if (props.contact.is_following) {
options.push({
label: ' @' + props.contact.username,
key: 'unfollow',
icon: renderIcon(WalkOutline),
});
} else {
options.push({
label: ' @' + props.contact.username,
key: 'follow',
icon: renderIcon(BodyOutline),
});
}
}
return options;
});
@ -176,13 +163,10 @@ const handleAction = (item: 'follow' | 'unfollow' | 'whisper') => {
</script>
<style lang="less" scoped>
.follow-item {
display: border-box;
.user-card {
width: 100%;
padding-left: 16px;
padding-right: 16px;
padding-top: 12px;
padding-bottom: 12px;
box-sizing: border-box;
padding: 12px 16px;
&:hover {
background: #f7f9f9;
@ -216,11 +200,11 @@ const handleAction = (item: 'follow' | 'unfollow' | 'whisper') => {
}
.dark {
.follow-item {
.user-card {
&:hover {
background: #18181c;
}
background-color: rgba(16, 16, 20, 0.75);
}
}
</style>
</style>

@ -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({
method,
url: _path.join('/'),
data: args[0],
...(method === 'get' ? { params: args[0] } : { data: args[0] }),
});
}, {
get(target: any, p: string) {

@ -11,24 +11,14 @@
<n-empty size="large" description="暂无数据" />
</div>
<div v-if="desktopModelShow">
<n-list-item v-for="post in list" :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-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>
<n-list-item v-for="post in list" :key="post.id">
<post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:isMobile="!desktopModelShow"
addFollowAction
@send-whisper="onSendWhisper"
@post-follow-action="postFollowAction" />
</n-list-item>
</div>
<!-- -->
<whisper :show="showWhisper" :user="whisperReceiver" @success="whisperSuccess" />
@ -94,37 +84,6 @@ const whisperSuccess = () => {
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) {
for (let index in list.value) {
if (list.value[index].user_id == userId) {

@ -12,38 +12,33 @@
</div>
<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>
</div>
<!-- -->
<whisper :show="showWhisper" :user="whisperReceiver" @success="whisperSuccess" />
</n-list>
<infinite-load-more
:total-page="totalPage"
:no-more="noMore"
complete-text="没有更多好友了"
@load-more="nextPage"
/>
</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>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import InfiniteLoading from 'v3-infinite-loading';
import { useRoute } from 'vue-router';
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 loading = ref(false);
const noMore = ref(false);
const { loading, noMore, page, pageSize, totalPage } = usePagination(20);
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 whisperReceiver = ref<Item.UserInfo>({
id: 0,
@ -59,6 +54,9 @@ const whisperReceiver = ref<Item.UserInfo>({
status: 1,
});
// 初始化页码
page.value = +(route.query.p as string) || 1;
const onSendWhisper = (user: Item.UserInfo) => {
whisperReceiver.value = user;
showWhisper.value = true;
@ -117,22 +115,6 @@ const loadContacts = (scrollToBottom: boolean = false) => {
</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;
}
}
}
.dark {
.main-content-wrap, .empty-wrap, .skeleton-wrap {
background-color: rgba(16, 16, 20, 0.75);

@ -16,7 +16,7 @@
</div>
<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>
</div>
<!-- -->
@ -24,14 +24,14 @@
</n-list>
</div>
<n-space v-if="totalPage > 0" justify="center">
<InfiniteLoading class="load-more" :slots="{ complete: completeStr, error: '加载出错' }" @infinite="nextPage">
<template #spinner>
<div class="load-more-wrap">
<n-spin :size="14" v-if="!noMore" />
<span class="load-more-spinner">{{ noMore ? completeStr : '' }}</span>
</div>
</template>
</InfiniteLoading>
<InfiniteLoading class="load-more" :slots="{ complete: completeStr, error: '加载出错' }" @infinite="handleNextPage">
<template #spinner>
<div class="load-more-wrap">
<n-spin :size="14" v-if="!noMore" />
<span class="load-more-spinner">{{ noMore ? completeStr : '' }}</span>
</div>
</template>
</InfiniteLoading>
</n-space>
</template>
@ -40,38 +40,27 @@ 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';
import UserCard from '@/components/user-card.vue';
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 +72,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 +89,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>
@ -59,26 +54,15 @@
<div class="empty-wrap" v-if="list.length === 0">
<n-empty size="large" description="暂无数据" />
</div>
<div v-if="desktopModelShow">
<n-list-item v-for="post in list" :key="post.id">
<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 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>
<n-list-item v-for="post in list" :key="post.id">
<post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:isMobile="!desktopModelShow"
addFollowAction
@send-whisper="onSendWhisper"
@post-follow-action="postFollowAction"
@handle-friend-action="onHandleFriendAction" />
</n-list-item>
</div>
<!-- -->
<whisper :show="showWhisper" :user="whisperReceiver" @success="whisperSuccess" />
@ -87,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" />
@ -114,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();
@ -127,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);
@ -186,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;
@ -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) {
for (let index in list.value) {
if (list.value[index].user_id == userId) {
list.value[index].user.is_following = isFollowing;
}
}
// 如果是在【正在关注】tab且是取消关注操作isFollowing 为 false则刷新列表
if (targetStyle.value === 3 && !isFollowing) {
resetAll();
loadPosts('following');
}
}
const updateTitle = () => {
@ -331,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;
@ -528,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');
});
@ -553,7 +495,7 @@ watch(
(to, from) => {
updateTitle();
if (to.refresh !== from.refresh) {
reset();
resetAll();
setTimeout(() => {
loadContacts();
loadMorePosts();
@ -561,7 +503,7 @@ watch(
return;
}
if (from.path !== '/post' && to.path === '/') {
reset();
resetAll();
setTimeout(() => {
loadContacts();
loadMorePosts();

@ -3,38 +3,38 @@
<main-nav title="消息" />
<n-list class="main-content-wrap messages-wrap" bordered>
<!-- -->
<!-- -->
<whisper :show="showWhisper" :user="whisperReceiver" @success="whisperSuccess" />
<n-space justify="space-between">
<div class="title title-action">
<n-button text size="small" :focusable="false" @click="handleUnreadMessage">
<template #icon>
<n-icon>
<UnreadIcon />
</n-icon>
</template>
{{ unreadMsgCount }}
</n-button>
<n-divider vertical />
<n-button text size="small" :focusable="false" @click="handleReadAll"></n-button>
</div>
<div class="title title-filter">
<n-dropdown
placement="bottom-end"
trigger="click"
size="small"
:options="options"
@select="handleAction">
<n-button text>
<template #icon>
<n-icon>
<OptionsIcon />
</n-icon>
</template>
{{ messageStyle }}
</n-button>
</n-dropdown>
</div>
<div class="title title-action">
<n-button text size="small" :focusable="false" @click="handleUnreadMessage">
<template #icon>
<n-icon>
<UnreadIcon />
</n-icon>
</template>
{{ unreadMsgCount }}
</n-button>
<n-divider vertical />
<n-button text size="small" :focusable="false" @click="handleReadAll"></n-button>
</div>
<div class="title title-filter">
<n-dropdown
placement="bottom-end"
trigger="click"
size="small"
:options="options"
@select="handleAction">
<n-button text>
<template #icon>
<n-icon>
<OptionsIcon />
</n-icon>
</template>
{{ messageStyle }}
</n-button>
</n-dropdown>
</div>
</n-space>
<div v-if="loading && list.length === 0" class="skeleton-wrap">
<message-skeleton :num="pageSize" />
@ -50,16 +50,12 @@
</div>
</div>
</n-list>
<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>
<infinite-load-more
:total-page="totalPage"
:no-more="noMore"
complete-text="没有更多消息了"
@load-more="nextPage"
/>
</div>
</template>
@ -69,7 +65,6 @@ import type { Component } from 'vue';
import { NIcon, DropdownOption } from 'naive-ui';
import { useStoreMain } from '@/store/main';
import { useRoute } from 'vue-router';
import InfiniteLoading from 'v3-infinite-loading';
import {
LayersOutline as AllIcon,
AtOutline as SystemIcon,
@ -81,17 +76,18 @@ import {
import { useStoreUser } from '@/store/user';
import { storeToRefs } from 'pinia';
import { Api } from '@/utils/request';
import { usePagination } from '@/composables/usePagination';
import InfiniteLoadMore from '@/components/infinite-load-more.vue';
const storeMain = useStoreMain();
const storeUser = useStoreUser();
const { unreadMsgCount } = storeToRefs(storeMain);
const route = useRoute();
const loading = ref(false);
const noMore = ref(false);
const page = ref(+(route.query.p as string) || 1);
const pageSize = ref(20);
const totalPage = ref(0);
const { loading, noMore, page, pageSize, totalPage, reset: resetPagination } = usePagination(20);
// 初始化页码
page.value = +(route.query.p as string) || 1;
const list = ref<Item.MessageProps[]>([]);
const messageStyle = ref<
'' | '' | '' | '' | ''
@ -115,9 +111,7 @@ const whisperReceiver = ref<Item.UserInfo>({
});
const reset = () => {
noMore.value = false;
page.value = 1;
totalPage.value = 0;
resetPagination();
list.value = [];
};
@ -363,22 +357,6 @@ onMounted(() => {
</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;
}
}
}
.title {
padding-top: 4px;
opacity: 0.9;

@ -92,100 +92,14 @@
<div class="empty-wrap" v-if="list.length === 0">
<n-empty size="large" description="暂无数据" />
</div>
<div v-if="desktopModelShow">
<div v-if="pageType === 'post'">
<n-list-item v-for="post in postList" :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 === '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>
<n-list-item v-for="post in listData" :key="post.id">
<post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:addFollowAction="true"
:isMobile="!desktopModelShow"
@send-whisper="onSendWhisper"
@post-follow-action="postFollowAction" />
</n-list-item>
</div>
<!-- -->
<whisper :show="showWhisper" :user="whisperReceiver" @success="whisperSuccess" />
@ -219,6 +133,8 @@ import { useStoreUser } from '@/store/user';
import { storeToRefs } from 'pinia';
import { Api } from '@/utils/request';
type PageType = 'post' | 'comment' | 'highlight' | 'media' | 'star';
const storeMain = useStoreMain();
const storeUser = useStoreUser();
const { refresh, desktopModelShow } = storeToRefs(storeMain);
@ -235,9 +151,7 @@ const commentList = ref<Item.PostProps[]>([]);
const highlightList = ref<Item.PostProps[]>([]);
const mediaList = ref<Item.PostProps[]>([]);
const starList = ref<Item.PostProps[]>([]);
const pageType = ref<'post' | 'comment' | 'highlight' | 'media' | 'star'>(
'post',
);
const pageType = ref<PageType>('post');
const postPage = ref(+(route.query.p as string) || 1);
const commentPage = ref(1);
const highlightPage = ref(1);
@ -266,6 +180,23 @@ const whisperReceiver = ref<Item.UserInfo>({
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) => {
return () => {
return h(NIcon, null, {
@ -309,40 +240,6 @@ const whisperSuccess = () => {
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) {
updateFolloing(postList.value, userId, isFollowing);
updateFolloing(commentList.value, userId, isFollowing);
@ -366,153 +263,27 @@ function updateFolloing(
}
const loadPage = () => {
switch (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: userInfo.value.username,
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: userInfo.value.username,
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;
});
loadPostsByStyle(pageType.value);
};
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 styleListMap = {
post: { list: postList, totalPage: postTotalPage },
comment: { list: commentList, totalPage: commentTotalPage },
highlight: { list: highlightList, totalPage: highlightTotalPage },
media: { list: mediaList, totalPage: mediaTotalPage },
star: { list: starList, totalPage: starTotalPage },
};
const loadMediaPosts = () => {
loading.value = true;
Api.v1.user.get.posts({
username: userInfo.value.username,
style: 'media',
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);
mediaList.value = list.value;
mediaTotalPage.value = totalPage.value;
})
.catch((err) => {
list.value = [];
if (page.value > 1) {
page.value--;
}
loading.value = false;
});
const stylePageMap = {
post: postPage,
comment: commentPage,
highlight: highlightPage,
media: mediaPage,
star: starPage,
};
const loadStarPosts = () => {
function loadPostsByStyle(style: keyof typeof styleListMap) {
loading.value = true;
Api.v1.user.get.posts({
username: userInfo.value.username,
style: 'star',
style,
page: page.value,
page_size: pageSize.value,
})
@ -528,77 +299,27 @@ const loadStarPosts = () => {
window.scrollTo(0, 0);
}
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
starList.value = list.value;
starTotalPage.value = totalPage.value;
styleListMap[style].list.value = list.value;
styleListMap[style].totalPage.value = totalPage.value;
})
.catch((err) => {
.catch((_err) => {
list.value = [];
if (page.value > 1) {
page.value--;
}
loading.value = false;
});
};
const changeTab = (
tab: 'post' | 'comment' | 'highlight' | 'media' | 'star',
) => {
}
const changeTab = (tab: PageType) => {
pageType.value = tab;
switch (pageType.value) {
case 'post':
list.value = postList.value;
page.value = postPage.value;
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;
}
list.value = styleListMap[tab].list.value;
page.value = stylePageMap[tab].value;
totalPage.value = styleListMap[tab].totalPage.value;
loadPostsByStyle(tab);
};
const updatePage = () => {
switch (pageType.value) {
case 'post':
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;
}
stylePageMap[pageType.value].value = page.value;
loadPostsByStyle(pageType.value);
};
const nextPage = () => {
if (page.value < totalPage.value || totalPage.value == 0) {

@ -104,100 +104,14 @@
<div class="empty-wrap" v-if="list.length === 0">
<n-empty size="large" description="暂无数据" />
</div>
<div v-if="desktopModelShow">
<div v-if="pageType === 'post'">
<n-list-item v-for="post in postList" :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 === '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>
<n-list-item v-for="post in listData" :key="post.id">
<post-item :post="post"
:isOwner="userInfo.id == post.user_id"
:isMobile="!desktopModelShow"
addFollowAction
@send-whisper="onSendWhisper"
@post-follow-action="postFollowAction" />
</n-list-item>
</div>
</n-list>
@ -239,6 +153,9 @@ import { useStoreUser } from '@/store/user';
import { useStoreProfile } from '@/store/profile';
import { storeToRefs } from 'pinia';
import { Api } from '@/utils/request';
import UserAction from '@/composables/useUserAction';
type PageType = 'post' | 'comment' | 'highlight' | 'media' | 'star';
const dialog = useDialog();
@ -255,18 +172,18 @@ const router = useRouter();
const loading = ref(false);
const noMore = ref(false);
const user = reactive<Item.UserInfo>({
id: 0,
avatar: '',
username: '',
nickname: '',
is_admin: false,
is_friend: true,
is_following: false,
created_on: 0,
follows: 0,
followings: 0,
tweets_count: 0,
status: 1,
id: 0,
avatar: '',
username: '',
nickname: '',
is_admin: false,
is_friend: true,
is_following: false,
created_on: 0,
follows: 0,
followings: 0,
tweets_count: 0,
status: 1,
});
const userLoading = ref(false);
const showWhisper = ref(false);
@ -279,9 +196,7 @@ const mediaList = ref<Item.PostProps[]>([]);
const starList = ref<Item.PostProps[]>([]);
const username = ref(route.query.s || '');
const page = ref(+(route.query.p as string) || 1);
const pageType = ref<'post' | 'comment' | 'highlight' | 'media' | 'star'>(
'post',
);
const pageType = ref<PageType>('post');
const postPage = ref(+(route.query.p as string) || 1);
const commentPage = ref(1);
const highlightPage = ref(1);
@ -295,302 +210,129 @@ const highlightTotalPage = ref(0);
const mediaTotalPage = ref(0);
const starTotalPage = ref(0);
const onSendWhisper = (receiver: Item.UserInfo) => {
user.id = receiver.id;
user.username = receiver.username;
user.nickname = receiver.nickname;
user.avatar = receiver.avatar;
showWhisper.value = true;
};
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 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) => {});
}
},
});
const onSendWhisper = (receiver: Item.UserInfo) => {
user.id = receiver.id;
user.username = receiver.username;
user.nickname = receiver.nickname;
user.avatar = receiver.avatar;
showWhisper.value = true;
};
function postFollowAction(userId: number, isFollowing: boolean) {
updateFolloing(postList, userId, isFollowing);
updateFolloing(commentList, userId, isFollowing);
updateFolloing(highlightList, userId, isFollowing);
updateFolloing(mediaList, userId, isFollowing);
updateFolloing(starList, userId, isFollowing);
updateFolloing(postList, userId, isFollowing);
updateFolloing(commentList, userId, isFollowing);
updateFolloing(highlightList, userId, isFollowing);
updateFolloing(mediaList, userId, isFollowing);
updateFolloing(starList, userId, isFollowing);
}
function updateFolloing(
posts: Ref<Item.PostProps[]>,
userId: number,
isFollowing: boolean,
posts: Ref<Item.PostProps[]>,
userId: number,
isFollowing: boolean,
) {
if (posts.value && posts.value.length > 0) {
for (let index in posts.value) {
if (posts.value[index].user_id == userId) {
posts.value[index].user.is_following = isFollowing;
}
}
}
if (posts.value && posts.value.length > 0) {
for (let index in posts.value) {
if (posts.value[index].user_id == userId) {
posts.value[index].user.is_following = isFollowing;
}
}
}
}
const reset = () => {
noMore.value = false;
list.value = [];
postList.value = [];
commentList.value = [];
highlightList.value = [];
mediaList.value = [];
starList.value = [];
pageType.value = 'post';
page.value = 1;
postPage.value = 1;
commentPage.value = 1;
highlightPage.value = 1;
mediaPage.value = 1;
starPage.value = 1;
totalPage.value = 0;
postTotalPage.value = 0;
commentTotalPage.value = 0;
highlightTotalPage.value = 0;
mediaTotalPage.value = 0;
starTotalPage.value = 0;
noMore.value = false;
list.value = [];
postList.value = [];
commentList.value = [];
highlightList.value = [];
mediaList.value = [];
starList.value = [];
pageType.value = 'post';
page.value = 1;
postPage.value = 1;
commentPage.value = 1;
highlightPage.value = 1;
mediaPage.value = 1;
starPage.value = 1;
totalPage.value = 0;
postTotalPage.value = 0;
commentTotalPage.value = 0;
highlightTotalPage.value = 0;
mediaTotalPage.value = 0;
starTotalPage.value = 0;
};
const loadPage = () => {
switch (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;
});
loadPostsByStyle(pageType.value);
};
const loadHighlightPosts = () => {
loading.value = true;
Api.v1.user.get.posts({
username: username.value as string,
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 styleListMap = {
post: { list: postList, totalPage: postTotalPage },
comment: { list: commentList, totalPage: commentTotalPage },
highlight: { list: highlightList, totalPage: highlightTotalPage },
media: { list: mediaList, totalPage: mediaTotalPage },
star: { list: starList, totalPage: starTotalPage },
};
const loadMediaPosts = () => {
loading.value = true;
Api.v1.user.get.posts({
username: username.value as string,
style: 'media',
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);
mediaList.value = list.value;
mediaTotalPage.value = totalPage.value;
})
.catch((err) => {
list.value = [];
if (page.value > 1) {
page.value--;
}
loading.value = false;
});
function loadPostsByStyle(style: keyof typeof styleListMap) {
loading.value = true;
Api.v1.user.get.posts({
username: username.value as string,
style,
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);
styleListMap[style].list.value = list.value;
styleListMap[style].totalPage.value = totalPage.value;
})
.catch((_err) => {
list.value = [];
if (page.value > 1) {
page.value--;
}
loading.value = false;
});
};
const loadStarPosts = () => {
loading.value = true;
Api.v1.user.get.posts({
username: username.value as string,
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) {
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 stylePageMap = {
post: postPage,
comment: commentPage,
highlight: highlightPage,
media: mediaPage,
star: starPage,
};
const changeTab = (
tab: 'post' | 'comment' | 'highlight' | 'media' | 'star',
) => {
pageType.value = tab;
switch (pageType.value) {
case 'post':
list.value = postList.value;
page.value = postPage.value;
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;
}
function changeTab(tab: PageType) {
pageType.value = tab;
list.value = styleListMap[tab].list.value;
page.value = stylePageMap[tab].value;
totalPage.value = styleListMap[tab].totalPage.value;
loadPostsByStyle(tab);
};
const loadUser = () => {
userLoading.value = true;
@ -621,28 +363,8 @@ const loadUser = () => {
});
};
const updatePage = () => {
switch (pageType.value) {
case 'post':
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;
}
stylePageMap[pageType.value].value = page.value;
loadPostsByStyle(pageType.value);
};
const openWhisper = () => {
showWhisper.value = true;
@ -780,10 +502,10 @@ const openDeleteFriend = () => {
Api.v1.friend.post.delete({
user_id: user.id,
})
.then((res) => {
.then((_res) => {
userLoading.value = false;
user.is_friend = false;
loadPosts();
loadPostsByStyle('post');
})
.catch((err) => {
userLoading.value = false;
@ -793,46 +515,15 @@ const openDeleteFriend = () => {
});
};
const handleFollowUser = () => {
dialog.success({
title: '',
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) => {
UserAction.followAction(dialog, user.id, user.username, user.is_following)
.then(_action => {
userLoading.value = false;
window.$message.success('');
loadUser();
})
.catch((err) => {
userLoading.value = false;
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);
});
}
},
});
})
.catch(err => {
userLoading.value = false;
console.log(err);
});
};
const banUser = () => {
dialog.warning({

Loading…
Cancel
Save