mirror of
https://github.com/bufanyun/hotgo.git
synced 2025-11-15 13:43:48 +08:00
版本预发布
This commit is contained in:
74
web/src/layout/components/Header/MessageList.vue
Normal file
74
web/src/layout/components/Header/MessageList.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<n-scrollbar style="max-height: 360px">
|
||||
<n-list>
|
||||
<n-list-item v-for="(item, index) in list" :key="item.id" @click="handleRead(index)">
|
||||
<n-thing class="px-15px" :class="{ 'opacity-30': item.isRead }">
|
||||
<template #avatar>
|
||||
<n-avatar round v-if="item.senderAvatar" :size="28" :src="item.senderAvatar" />
|
||||
<n-icon-wrapper v-else :size="28" :border-radius="10">
|
||||
<n-icon :size="20" :component="getIcon(item)" />
|
||||
</n-icon-wrapper>
|
||||
</template>
|
||||
<template #header>
|
||||
<n-ellipsis :line-clamp="1">
|
||||
{{ item.title }}
|
||||
<template #tooltip>
|
||||
{{ item.title }}
|
||||
</template>
|
||||
</n-ellipsis>
|
||||
</template>
|
||||
<template v-if="item.tagTitle" #header-extra>
|
||||
<n-tag v-bind="item.tagProps" size="small">{{ item.tagTitle }}</n-tag>
|
||||
</template>
|
||||
<template #description>
|
||||
<div v-if="item.content" class="description-box">
|
||||
<span v-html="item.content" class="description-html"> </span>
|
||||
</div>
|
||||
|
||||
<p>{{ item.createdAt }}</p>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-scrollbar>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { MessageRow, getIcon } from '@/enums/systemMessageEnum';
|
||||
interface Props {
|
||||
list?: MessageRow[];
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
list: () => [],
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'read', val: number): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
function handleRead(index: number) {
|
||||
emit('read', index);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.description-box) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 10px;
|
||||
}
|
||||
:deep(.description-html) {
|
||||
height: 100%;
|
||||
}
|
||||
:deep(.px-15px) {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
:deep(.text-34px) {
|
||||
font-size: 34px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<n-card
|
||||
:content-style="{ padding: '0px' }"
|
||||
:footer-style="{ padding: '0px' }"
|
||||
:bordered="false"
|
||||
:segmented="true"
|
||||
>
|
||||
<div v-if="notificationStore.messages.length > 0">
|
||||
<div
|
||||
class="flex items-center max-w-sm p-1 mx-auto space-x-2 rounded-xl"
|
||||
v-for="(item, index) of notificationStore.messages"
|
||||
:key="index"
|
||||
>
|
||||
<div class="flex-shrink-0">
|
||||
<n-icon size="40" color="#f00">
|
||||
<NotificationsCircle />
|
||||
</n-icon>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-medium">{{ item.title }}</div>
|
||||
<n-ellipsis :line-clamp="1" class="text-gray-500">{{ item.content }}</n-ellipsis>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<n-empty v-else description="暂无消息哦~" class="pt-20 pb-20" />
|
||||
<template #footer>
|
||||
<div class="flex justify-evenly">
|
||||
<n-button type="text" @click="onClearMessage">清空提醒</n-button>
|
||||
<n-button type="text" @click="onAllMessage">查看更多</n-button>
|
||||
</div>
|
||||
</template>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { NotificationsCircle } from '@vicons/ionicons5';
|
||||
import { notificationStoreWidthOut } from '@/store/modules/notification';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PopoverMessage',
|
||||
components: { NotificationsCircle },
|
||||
emits: ['clear'],
|
||||
setup(_props, { emit }) {
|
||||
const notificationStore = notificationStoreWidthOut();
|
||||
const router = useRouter();
|
||||
|
||||
function onClearMessage() {
|
||||
notificationStore.setMessages([]);
|
||||
emit('clear');
|
||||
}
|
||||
|
||||
function onAllMessage() {
|
||||
router.push({ name: 'apply_notice' });
|
||||
}
|
||||
|
||||
return {
|
||||
onClearMessage,
|
||||
notificationStore,
|
||||
onAllMessage,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
129
web/src/layout/components/Header/SystemMessage.vue
Normal file
129
web/src/layout/components/Header/SystemMessage.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<n-tabs v-model:value="currentTab" type="line" justify-content="space-evenly">
|
||||
<n-tab-pane
|
||||
v-for="(item, index) in notificationStore.getMessages"
|
||||
:key="item.key"
|
||||
:name="index"
|
||||
>
|
||||
<template #tab>
|
||||
<div>
|
||||
<span>{{ item.name }}</span>
|
||||
<n-badge
|
||||
v-bind="item.badgeProps"
|
||||
:value="item.list.filter((message) => !message.isRead).length"
|
||||
:max="99"
|
||||
show-zero
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<n-spin :show="loading">
|
||||
<n-empty v-show="item.list.length === 0" description="无数据" :show-icon="false">
|
||||
<template #extra>
|
||||
<n-button size="small" @click="handleLoadMore"> 查看更多</n-button>
|
||||
</template>
|
||||
</n-empty>
|
||||
|
||||
<message-list :list="item.list" @read="handleRead" />
|
||||
</n-spin>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
<n-space v-if="showAction" justify="center" size="large" class="flex border-t">
|
||||
<n-button class="act-btn" size="small" @click="handleClear">清空</n-button>
|
||||
<n-button class="act-btn" size="small" @click="handleAllRead">全部已读</n-button>
|
||||
<n-button class="act-btn" size="small" @click="handleLoadMore">查看更多</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import MessageList from './MessageList.vue';
|
||||
import { notificationStoreWidthOut } from '@/store/modules/notification';
|
||||
import { ReadAll, UpRead } from '@/api/apply/notice';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const notificationStore = notificationStoreWidthOut();
|
||||
const loading = ref(false);
|
||||
const currentTab = ref(0);
|
||||
const showAction = computed(
|
||||
() => notificationStore.getMessages[currentTab.value].list.length > 0
|
||||
);
|
||||
|
||||
function handleRead(index: number) {
|
||||
loading.value = true;
|
||||
const message = notificationStore.getMessages[currentTab.value].list[index];
|
||||
UpRead({ id: message.id })
|
||||
.then(() => {
|
||||
message.isRead = true;
|
||||
if (!message.isRead) {
|
||||
switch (message.type) {
|
||||
case 1:
|
||||
notificationStore.notifyUnread--;
|
||||
break;
|
||||
case 2:
|
||||
notificationStore.noticeUnread--;
|
||||
break;
|
||||
case 3:
|
||||
notificationStore.letterUnread--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function handleAllRead() {
|
||||
loading.value = true;
|
||||
ReadAll({ type: notificationStore.getMessages[currentTab.value].key })
|
||||
.then(() => {
|
||||
notificationStore.getMessages[currentTab.value].list.forEach((item) =>
|
||||
Object.assign(item, { isRead: true })
|
||||
);
|
||||
switch (notificationStore.getMessages[currentTab.value].key) {
|
||||
case 1:
|
||||
notificationStore.notifyUnread = 0;
|
||||
break;
|
||||
case 2:
|
||||
notificationStore.noticeUnread = 0;
|
||||
break;
|
||||
case 3:
|
||||
notificationStore.letterUnread = 0;
|
||||
break;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function handleClear() {
|
||||
notificationStore.getMessages[currentTab.value].list = [];
|
||||
switch (notificationStore.getMessages[currentTab.value].key) {
|
||||
case 1:
|
||||
notificationStore.notifyUnread = 0;
|
||||
break;
|
||||
case 2:
|
||||
notificationStore.noticeUnread = 0;
|
||||
break;
|
||||
case 3:
|
||||
notificationStore.letterUnread = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleLoadMore() {
|
||||
router.push({
|
||||
name: 'home_message',
|
||||
query: {
|
||||
type: notificationStore.getMessages[currentTab.value].key,
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.act-btn {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -93,16 +93,22 @@
|
||||
placement="bottom"
|
||||
v-if="item.icon === 'BellOutlined'"
|
||||
trigger="click"
|
||||
:width="300"
|
||||
:width="getIsMobile ? 276 : 420"
|
||||
>
|
||||
<template #trigger>
|
||||
<n-badge :value="notificationStore.messages.length" :max="99" processing>
|
||||
<n-icon size="18">
|
||||
<BellOutlined />
|
||||
</n-icon>
|
||||
</n-badge>
|
||||
<n-tooltip placement="bottom">
|
||||
<template #trigger>
|
||||
<n-badge :value="notificationStore.getUnreadCount()" :max="99" processing>
|
||||
<n-icon size="18">
|
||||
<BellOutlined />
|
||||
</n-icon>
|
||||
</n-badge>
|
||||
</template>
|
||||
<span>{{ item.tips }}</span>
|
||||
</n-tooltip>
|
||||
</template>
|
||||
<PopoverMessage />
|
||||
|
||||
<SystemMessage />
|
||||
</n-popover>
|
||||
|
||||
<div v-else>
|
||||
@@ -131,12 +137,8 @@
|
||||
<div class="layout-header-trigger layout-header-trigger-min">
|
||||
<n-dropdown trigger="hover" @select="avatarSelect" :options="avatarOptions">
|
||||
<div class="avatar">
|
||||
<n-avatar round>
|
||||
{{ username }}
|
||||
<template #icon>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</n-avatar>
|
||||
<n-avatar v-if="userStore.avatar" round :size="30" :src="userStore.avatar" />
|
||||
<n-avatar v-else round :size="30">{{ userStore.realName }}</n-avatar>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
@@ -162,7 +164,17 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, ref, computed, unref, watch, h } from 'vue';
|
||||
import {
|
||||
defineComponent,
|
||||
reactive,
|
||||
toRefs,
|
||||
ref,
|
||||
computed,
|
||||
unref,
|
||||
watch,
|
||||
h,
|
||||
onMounted,
|
||||
} from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import components from './components';
|
||||
import {
|
||||
@@ -170,8 +182,11 @@
|
||||
useDialog,
|
||||
useMessage,
|
||||
NAvatar,
|
||||
NTag,
|
||||
NIcon,
|
||||
useNotification,
|
||||
NotificationReactive,
|
||||
NButton,
|
||||
} from 'naive-ui';
|
||||
import { TABS_ROUTES } from '@/store/mutation-types';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
@@ -180,13 +195,19 @@
|
||||
import { AsideMenu } from '@/layout/components/Menu';
|
||||
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
|
||||
import { NotificationsOutline as NotificationsIcon } from '@vicons/ionicons5';
|
||||
import PopoverMessage from './PopoverMessage.vue';
|
||||
import SystemMessage from './SystemMessage.vue';
|
||||
import { notificationStoreWidthOut } from '@/store/modules/notification';
|
||||
import notificationImg from '@/assets/images/notification.png';
|
||||
import { getIcon } from '@/enums/systemMessageEnum';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PageHeader',
|
||||
components: { ...components, NDialogProvider, ProjectSetting, AsideMenu, PopoverMessage },
|
||||
components: {
|
||||
...components,
|
||||
NDialogProvider,
|
||||
ProjectSetting,
|
||||
AsideMenu,
|
||||
SystemMessage,
|
||||
},
|
||||
props: {
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
@@ -201,15 +222,21 @@
|
||||
const useLockscreen = useLockscreenStore();
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const { getNavMode, getNavTheme, getHeaderSetting, getMenuSetting, getCrumbsSetting } =
|
||||
useProjectSetting();
|
||||
|
||||
const { username } = userStore?.info || {};
|
||||
const {
|
||||
getNavMode,
|
||||
getNavTheme,
|
||||
getHeaderSetting,
|
||||
getMenuSetting,
|
||||
getCrumbsSetting,
|
||||
getIsMobile,
|
||||
} = useProjectSetting();
|
||||
|
||||
// const { username, avatar } = userStore?.info || {};
|
||||
const drawerSetting = ref();
|
||||
|
||||
const state = reactive({
|
||||
username: username || '',
|
||||
// username: username || '',
|
||||
// avatar: avatar || '',
|
||||
fullscreenIcon: 'FullscreenOutlined',
|
||||
navMode: getNavMode,
|
||||
navTheme: getNavTheme,
|
||||
@@ -334,7 +361,7 @@
|
||||
},
|
||||
{
|
||||
icon: 'BellOutlined',
|
||||
tips: '系统消息',
|
||||
tips: '我的消息',
|
||||
},
|
||||
{
|
||||
icon: 'LockOutlined',
|
||||
@@ -359,7 +386,7 @@
|
||||
const avatarSelect = (key) => {
|
||||
switch (key) {
|
||||
case 1:
|
||||
router.push({ name: 'setting_account' });
|
||||
router.push({ name: 'home_account' });
|
||||
break;
|
||||
case 2:
|
||||
doLogout();
|
||||
@@ -373,38 +400,84 @@
|
||||
}
|
||||
|
||||
const notification = useNotification();
|
||||
|
||||
const getMessages = computed(() => {
|
||||
return notificationStore.messages;
|
||||
return notificationStore.newMessage;
|
||||
});
|
||||
const nRef = ref<NotificationReactive | null>(null);
|
||||
// 监听新消息,推送通知
|
||||
watch(
|
||||
getMessages,
|
||||
(newVal, _oldVal) => {
|
||||
if (newVal[0] !== undefined) {
|
||||
let message = newVal[0];
|
||||
nRef.value = notification.create({
|
||||
title: message.title,
|
||||
description: message.description,
|
||||
content: message.content,
|
||||
meta: message.meta,
|
||||
duration: 5000,
|
||||
avatar: () =>
|
||||
h(NAvatar, {
|
||||
size: 'small',
|
||||
round: true,
|
||||
src: notificationImg,
|
||||
}),
|
||||
onClose: () => {
|
||||
nRef.value = null;
|
||||
},
|
||||
});
|
||||
if (newVal === null || newVal === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
nRef.value = notification.create({
|
||||
title: newVal.title,
|
||||
description:
|
||||
newVal.tagTitle === '' || newVal.tagTitle === undefined
|
||||
? undefined
|
||||
: () =>
|
||||
h(
|
||||
NTag,
|
||||
{
|
||||
style: {
|
||||
marginRight: '6px',
|
||||
},
|
||||
type: newVal.tagProps?.type,
|
||||
bordered: false,
|
||||
},
|
||||
{
|
||||
default: () => newVal.tagTitle,
|
||||
}
|
||||
),
|
||||
|
||||
content: () =>
|
||||
newVal.content === '' || newVal.content === undefined
|
||||
? undefined
|
||||
: h('div', { innerHTML: '<div>' + newVal.content + '</div>' }),
|
||||
meta: newVal.createdAt,
|
||||
avatar: () =>
|
||||
newVal.senderAvatar !== '' || newVal.senderAvatar === undefined
|
||||
? h(NAvatar, {
|
||||
size: 'small',
|
||||
round: true,
|
||||
src: newVal.senderAvatar,
|
||||
})
|
||||
: h(NIcon, null, { default: () => h(getIcon(newVal)) }),
|
||||
action: () =>
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
type: 'info',
|
||||
onClick: () => {
|
||||
(nRef.value as NotificationReactive).destroy();
|
||||
router.push({
|
||||
name: 'home_message',
|
||||
query: {
|
||||
type: newVal.type,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => '查看详情',
|
||||
}
|
||||
),
|
||||
onClose: () => {
|
||||
nRef.value = null;
|
||||
},
|
||||
});
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (notificationStore.getUnreadCount() === 0) {
|
||||
notificationStore.pullMessages();
|
||||
}
|
||||
});
|
||||
return {
|
||||
...toRefs(state),
|
||||
iconList,
|
||||
@@ -423,8 +496,10 @@
|
||||
getMenuLocation,
|
||||
mixMenu,
|
||||
NotificationsIcon,
|
||||
PopoverMessage,
|
||||
SystemMessage,
|
||||
notificationStore,
|
||||
getIsMobile,
|
||||
userStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user