1. 整合组件

2. 封装重复代码
pull/706/head
orzi 7 months ago
parent 164a2c0bc5
commit 50db7e5340

@ -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>

@ -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" />
@ -17,9 +17,9 @@
{{ contact.nickname }} {{ contact.nickname }}
</router-link> </router-link>
</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,11 +62,23 @@ 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'; 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; (e: 'unfollow-success'): void;
@ -85,7 +97,6 @@ const handleFollowUser = () => {
UserAction.followAction(dialog, props.contact.user_id, props.contact.username, props.contact.is_following) UserAction.followAction(dialog, props.contact.user_id, props.contact.username, props.contact.is_following)
.then(_action => { .then(_action => {
props.contact.is_following = _action; props.contact.is_following = _action;
// 如果是取消关注操作,触发事件
if (wasFollowing && !_action) { if (wasFollowing && !_action) {
emit('unfollow-success'); emit('unfollow-success');
} }
@ -95,13 +106,6 @@ const handleFollowUser = () => {
}); });
}; };
const props = withDefaults(
defineProps<{
contact: Item.ContactItemProps;
}>(),
{},
);
const actionOpts = computed(() => { const actionOpts = computed(() => {
let options: DropdownOption[] = [ let options: DropdownOption[] = [
{ {
@ -110,19 +114,23 @@ const actionOpts = computed(() => {
icon: renderIcon(PaperPlaneOutline), icon: renderIcon(PaperPlaneOutline),
}, },
]; ];
if (props.contact.is_following) {
options.push({ if (enableFollowAction.value) {
label: '取消关注 @' + props.contact.username, if (props.contact.is_following) {
key: 'unfollow', options.push({
icon: renderIcon(WalkOutline), label: '取消关注 @' + props.contact.username,
}); key: 'unfollow',
} else { icon: renderIcon(WalkOutline),
options.push({ });
label: '关注 @' + props.contact.username, } else {
key: 'follow', options.push({
icon: renderIcon(BodyOutline), label: '关注 @' + props.contact.username,
}); key: 'follow',
icon: renderIcon(BodyOutline),
});
}
} }
return options; return options;
}); });
@ -155,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;
@ -195,11 +200,11 @@ const handleAction = (item: 'follow' | 'unfollow' | 'whisper') => {
} }
.dark { .dark {
.follow-item { .user-card {
&:hover { &:hover {
background: #18181c; background: #18181c;
} }
background-color: rgba(16, 16, 20, 0.75); background-color: rgba(16, 16, 20, 0.75);
} }
} }
</style> </style>

@ -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" @unfollow-success="handleUnfollowSuccess" /> <user-card type="follow" :contact="contact" @send-whisper="onSendWhisper" @unfollow-success="handleUnfollowSuccess" />
</n-list-item> </n-list-item>
</div> </div>
<!-- 私信组件 --> <!-- 私信组件 -->
@ -24,14 +24,14 @@
</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="handleNextPage"> <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" />
<span class="load-more-spinner">{{ noMore ? completeStr : '加载更多' }}</span> <span class="load-more-spinner">{{ noMore ? completeStr : '加载更多' }}</span>
</div> </div>
</template> </template>
</InfiniteLoading> </InfiniteLoading>
</n-space> </n-space>
</template> </template>
@ -42,6 +42,7 @@ import { useRoute } from 'vue-router';
import { Api } from '@/utils/request'; import { Api } from '@/utils/request';
import { usePagination } from '@/composables/usePagination'; import { usePagination } from '@/composables/usePagination';
import UserAction from '@/composables/useUserAction'; import UserAction from '@/composables/useUserAction';
import UserCard from '@/components/user-card.vue';
const route = useRoute(); const route = useRoute();

@ -3,38 +3,38 @@
<main-nav title="消息" /> <main-nav title="消息" />
<n-list class="main-content-wrap messages-wrap" bordered> <n-list class="main-content-wrap messages-wrap" bordered>
<!-- 私信组件 --> <!-- 私信组件 -->
<whisper :show="showWhisper" :user="whisperReceiver" @success="whisperSuccess" /> <whisper :show="showWhisper" :user="whisperReceiver" @success="whisperSuccess" />
<n-space justify="space-between"> <n-space justify="space-between">
<div class="title title-action"> <div class="title title-action">
<n-button text size="small" :focusable="false" @click="handleUnreadMessage"> <n-button text size="small" :focusable="false" @click="handleUnreadMessage">
<template #icon> <template #icon>
<n-icon> <n-icon>
<UnreadIcon /> <UnreadIcon />
</n-icon> </n-icon>
</template> </template>
{{ unreadMsgCount }} 条未读 {{ unreadMsgCount }} 条未读
</n-button> </n-button>
<n-divider vertical /> <n-divider vertical />
<n-button text size="small" :focusable="false" @click="handleReadAll">全标已读</n-button> <n-button text size="small" :focusable="false" @click="handleReadAll">全标已读</n-button>
</div> </div>
<div class="title title-filter"> <div class="title title-filter">
<n-dropdown <n-dropdown
placement="bottom-end" placement="bottom-end"
trigger="click" trigger="click"
size="small" size="small"
:options="options" :options="options"
@select="handleAction"> @select="handleAction">
<n-button text> <n-button text>
<template #icon> <template #icon>
<n-icon> <n-icon>
<OptionsIcon /> <OptionsIcon />
</n-icon> </n-icon>
</template> </template>
{{ messageStyle }} {{ messageStyle }}
</n-button> </n-button>
</n-dropdown> </n-dropdown>
</div> </div>
</n-space> </n-space>
<div v-if="loading && list.length === 0" class="skeleton-wrap"> <div v-if="loading && list.length === 0" class="skeleton-wrap">
<message-skeleton :num="pageSize" /> <message-skeleton :num="pageSize" />
@ -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;

Loading…
Cancel
Save