feat: 新增 ele infra 我的站内信
							parent
							
								
									b5de0e8307
								
							
						
					
					
						commit
						4e1d842e7f
					
				|  | @ -1,12 +1,17 @@ | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import type { NotificationItem } from '@vben/layouts'; | import type { NotificationItem } from '@vben/layouts'; | ||||||
| 
 | 
 | ||||||
| import { computed, ref, watch } from 'vue'; | import { computed, onMounted, ref, watch } from 'vue'; | ||||||
| 
 | 
 | ||||||
| import { AuthenticationLoginExpiredModal } from '@vben/common-ui'; | import { AuthenticationLoginExpiredModal, useVbenModal } from '@vben/common-ui'; | ||||||
| import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants'; | import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants'; | ||||||
| import { useWatermark } from '@vben/hooks'; | import { useWatermark } from '@vben/hooks'; | ||||||
| import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons'; | import { | ||||||
|  |   AntdProfileOutlined, | ||||||
|  |   BookOpenText, | ||||||
|  |   CircleHelp, | ||||||
|  |   MdiGithub, | ||||||
|  | } from '@vben/icons'; | ||||||
| import { | import { | ||||||
|   BasicLayout, |   BasicLayout, | ||||||
|   LockScreen, |   LockScreen, | ||||||
|  | @ -15,52 +20,43 @@ import { | ||||||
| } from '@vben/layouts'; | } from '@vben/layouts'; | ||||||
| import { preferences } from '@vben/preferences'; | import { preferences } from '@vben/preferences'; | ||||||
| import { useAccessStore, useUserStore } from '@vben/stores'; | import { useAccessStore, useUserStore } from '@vben/stores'; | ||||||
| import { openWindow } from '@vben/utils'; | import { formatDateTime, openWindow } from '@vben/utils'; | ||||||
| 
 | 
 | ||||||
|  | import { | ||||||
|  |   getUnreadNotifyMessageCount, | ||||||
|  |   getUnreadNotifyMessageList, | ||||||
|  |   updateAllNotifyMessageRead, | ||||||
|  |   updateNotifyMessageRead, | ||||||
|  | } from '#/api/system/notify/message'; | ||||||
| import { $t } from '#/locales'; | import { $t } from '#/locales'; | ||||||
|  | import { router } from '#/router'; | ||||||
| import { useAuthStore } from '#/store'; | import { useAuthStore } from '#/store'; | ||||||
| import LoginForm from '#/views/_core/authentication/login.vue'; | import LoginForm from '#/views/_core/authentication/login.vue'; | ||||||
| 
 | 
 | ||||||
| const notifications = ref<NotificationItem[]>([ | import Help from './components/help.vue'; | ||||||
|   { | import TenantDropdown from './components/tenant-dropdown.vue'; | ||||||
|     avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB', |  | ||||||
|     date: '3小时前', |  | ||||||
|     isRead: true, |  | ||||||
|     message: '描述信息描述信息描述信息', |  | ||||||
|     title: '收到了 14 份新周报', |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     avatar: 'https://avatar.vercel.sh/1', |  | ||||||
|     date: '刚刚', |  | ||||||
|     isRead: false, |  | ||||||
|     message: '描述信息描述信息描述信息', |  | ||||||
|     title: '朱偏右 回复了你', |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     avatar: 'https://avatar.vercel.sh/1', |  | ||||||
|     date: '2024-01-01', |  | ||||||
|     isRead: false, |  | ||||||
|     message: '描述信息描述信息描述信息', |  | ||||||
|     title: '曲丽丽 评论了你', |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     avatar: 'https://avatar.vercel.sh/satori', |  | ||||||
|     date: '1天前', |  | ||||||
|     isRead: false, |  | ||||||
|     message: '描述信息描述信息描述信息', |  | ||||||
|     title: '代办提醒', |  | ||||||
|   }, |  | ||||||
| ]); |  | ||||||
| 
 | 
 | ||||||
| const userStore = useUserStore(); | const userStore = useUserStore(); | ||||||
| const authStore = useAuthStore(); | const authStore = useAuthStore(); | ||||||
| const accessStore = useAccessStore(); | const accessStore = useAccessStore(); | ||||||
| const { destroyWatermark, updateWatermark } = useWatermark(); | const { destroyWatermark, updateWatermark } = useWatermark(); | ||||||
| const showDot = computed(() => | 
 | ||||||
|   notifications.value.some((item) => !item.isRead), | const notifications = ref<NotificationItem[]>([]); | ||||||
| ); | const unreadCount = ref(0); | ||||||
|  | const showDot = computed(() => unreadCount.value > 0); | ||||||
|  | 
 | ||||||
|  | const [HelpModal, helpModalApi] = useVbenModal({ | ||||||
|  |   connectedComponent: Help, | ||||||
|  | }); | ||||||
| 
 | 
 | ||||||
| const menus = computed(() => [ | const menus = computed(() => [ | ||||||
|  |   { | ||||||
|  |     handler: () => { | ||||||
|  |       router.push({ name: 'Profile' }); | ||||||
|  |     }, | ||||||
|  |     icon: AntdProfileOutlined, | ||||||
|  |     text: $t('ui.widgets.profile'), | ||||||
|  |   }, | ||||||
|   { |   { | ||||||
|     handler: () => { |     handler: () => { | ||||||
|       openWindow(VBEN_DOC_URL, { |       openWindow(VBEN_DOC_URL, { | ||||||
|  | @ -81,9 +77,7 @@ const menus = computed(() => [ | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     handler: () => { |     handler: () => { | ||||||
|       openWindow(`${VBEN_GITHUB_URL}/issues`, { |       helpModalApi.open(); | ||||||
|         target: '_blank', |  | ||||||
|       }); |  | ||||||
|     }, |     }, | ||||||
|     icon: CircleHelp, |     icon: CircleHelp, | ||||||
|     text: $t('ui.widgets.qa'), |     text: $t('ui.widgets.qa'), | ||||||
|  | @ -98,19 +92,83 @@ async function handleLogout() { | ||||||
|   await authStore.logout(false); |   await authStore.logout(false); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function handleNoticeClear() { | /** 获得未读消息数 */ | ||||||
|  | async function handleNotificationGetUnreadCount() { | ||||||
|  |   unreadCount.value = await getUnreadNotifyMessageCount(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** 获得消息列表 */ | ||||||
|  | async function handleNotificationGetList() { | ||||||
|  |   const list = await getUnreadNotifyMessageList(); | ||||||
|  |   notifications.value = list.map((item) => ({ | ||||||
|  |     avatar: preferences.app.defaultAvatar, | ||||||
|  |     date: formatDateTime(item.createTime) as string, | ||||||
|  |     isRead: false, | ||||||
|  |     id: item.id, | ||||||
|  |     message: item.templateContent, | ||||||
|  |     title: item.templateNickname, | ||||||
|  |   })); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** 跳转我的站内信 */ | ||||||
|  | function handleNotificationViewAll() { | ||||||
|  |   router.push({ | ||||||
|  |     name: 'MyNotifyMessage', | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** 标记所有已读 */ | ||||||
|  | async function handleNotificationMakeAll() { | ||||||
|  |   await updateAllNotifyMessageRead(); | ||||||
|  |   unreadCount.value = 0; | ||||||
|   notifications.value = []; |   notifications.value = []; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function handleMakeAll() { | /** 清空通知 */ | ||||||
|   notifications.value.forEach((item) => (item.isRead = true)); | async function handleNotificationClear() { | ||||||
|  |   handleNotificationMakeAll(); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /** 标记单个已读 */ | ||||||
|  | async function handleNotificationRead(item: NotificationItem) { | ||||||
|  |   if (!item.id) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   await updateNotifyMessageRead([item.id]); | ||||||
|  |   await handleNotificationGetUnreadCount(); | ||||||
|  |   notifications.value = notifications.value.filter((n) => n.id !== item.id); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** 处理通知打开 */ | ||||||
|  | function handleNotificationOpen(open: boolean) { | ||||||
|  |   if (!open) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   handleNotificationGetList(); | ||||||
|  |   handleNotificationGetUnreadCount(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ========== 初始化 ========== | ||||||
|  | onMounted(() => { | ||||||
|  |   // 首次加载未读数量 | ||||||
|  |   handleNotificationGetUnreadCount(); | ||||||
|  |   // 轮询刷新未读数量 | ||||||
|  |   setInterval( | ||||||
|  |     () => { | ||||||
|  |       if (userStore.userInfo) { | ||||||
|  |         handleNotificationGetUnreadCount(); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     1000 * 60 * 2, | ||||||
|  |   ); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| watch( | watch( | ||||||
|   () => preferences.app.watermark, |   () => preferences.app.watermark, | ||||||
|   async (enable) => { |   async (enable) => { | ||||||
|     if (enable) { |     if (enable) { | ||||||
|       await updateWatermark({ |       await updateWatermark({ | ||||||
|         content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`, |         content: `${userStore.userInfo?.id} - ${userStore.userInfo?.nickname}`, | ||||||
|       }); |       }); | ||||||
|     } else { |     } else { | ||||||
|       destroyWatermark(); |       destroyWatermark(); | ||||||
|  | @ -128,9 +186,9 @@ watch( | ||||||
|       <UserDropdown |       <UserDropdown | ||||||
|         :avatar |         :avatar | ||||||
|         :menus |         :menus | ||||||
|         :text="userStore.userInfo?.realName" |         :text="userStore.userInfo?.nickname" | ||||||
|         description="ann.vben@gmail.com" |         :description="userStore.userInfo?.email" | ||||||
|         tag-text="Pro" |         :tag-text="userStore.userInfo?.username" | ||||||
|         @logout="handleLogout" |         @logout="handleLogout" | ||||||
|       /> |       /> | ||||||
|     </template> |     </template> | ||||||
|  | @ -138,10 +196,16 @@ watch( | ||||||
|       <Notification |       <Notification | ||||||
|         :dot="showDot" |         :dot="showDot" | ||||||
|         :notifications="notifications" |         :notifications="notifications" | ||||||
|         @clear="handleNoticeClear" |         @clear="handleNotificationClear" | ||||||
|         @make-all="handleMakeAll" |         @make-all="handleNotificationMakeAll" | ||||||
|  |         @view-all="handleNotificationViewAll" | ||||||
|  |         @open="handleNotificationOpen" | ||||||
|  |         @read="handleNotificationRead" | ||||||
|       /> |       /> | ||||||
|     </template> |     </template> | ||||||
|  |     <template #header-right-1> | ||||||
|  |       <TenantDropdown class="w-30 mr-2" /> | ||||||
|  |     </template> | ||||||
|     <template #extra> |     <template #extra> | ||||||
|       <AuthenticationLoginExpiredModal |       <AuthenticationLoginExpiredModal | ||||||
|         v-model:open="accessStore.loginExpired" |         v-model:open="accessStore.loginExpired" | ||||||
|  | @ -154,4 +218,5 @@ watch( | ||||||
|       <LockScreen :avatar @to-login="handleLogout" /> |       <LockScreen :avatar @to-login="handleLogout" /> | ||||||
|     </template> |     </template> | ||||||
|   </BasicLayout> |   </BasicLayout> | ||||||
|  |   <HelpModal /> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,91 @@ | ||||||
|  | <script lang="ts" setup> | ||||||
|  | // TODO @xingyu:这个有可能 3 端复用么?想着是把 layouts 下的 components 没有这个目录哈; | ||||||
|  | import { useVbenModal, VbenButton, VbenButtonGroup } from '@vben/common-ui'; | ||||||
|  | import { openWindow } from '@vben/utils'; | ||||||
|  | 
 | ||||||
|  | import { ElImage, ElTag } from 'element-plus'; | ||||||
|  | 
 | ||||||
|  | import { $t } from '#/locales'; | ||||||
|  | 
 | ||||||
|  | const [Modal, modalApi] = useVbenModal({ | ||||||
|  |   draggable: true, | ||||||
|  |   overlayBlur: 5, | ||||||
|  |   footer: false, | ||||||
|  |   onCancel() { | ||||||
|  |     modalApi.close(); | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | <template> | ||||||
|  |   <Modal class="w-[40%]" :title="$t('ui.widgets.qa')"> | ||||||
|  |     <div class="mt-2 flex flex-col"> | ||||||
|  |       <div class="mt-2 flex flex-row"> | ||||||
|  |         <VbenButtonGroup class="basis-1/3" :gap="2" border size="large"> | ||||||
|  |           <p class="p-2">项目地址:</p> | ||||||
|  |           <VbenButton | ||||||
|  |             variant="link" | ||||||
|  |             @click=" | ||||||
|  |               openWindow('https://gitee.com/yudaocode/yudao-ui-admin-vben') | ||||||
|  |             " | ||||||
|  |           > | ||||||
|  |             Gitee | ||||||
|  |           </VbenButton> | ||||||
|  |           <VbenButton | ||||||
|  |             variant="link" | ||||||
|  |             @click=" | ||||||
|  |               openWindow('https://github.com/yudaocode/yudao-ui-admin-vben') | ||||||
|  |             " | ||||||
|  |           > | ||||||
|  |             Github | ||||||
|  |           </VbenButton> | ||||||
|  |         </VbenButtonGroup> | ||||||
|  | 
 | ||||||
|  |         <VbenButtonGroup class="basis-1/3" :gap="2" border size="large"> | ||||||
|  |           <p class="p-2">issues:</p> | ||||||
|  |           <VbenButton | ||||||
|  |             variant="link" | ||||||
|  |             @click=" | ||||||
|  |               openWindow( | ||||||
|  |                 'https://gitee.com/yudaocode/yudao-ui-admin-vben/issues', | ||||||
|  |               ) | ||||||
|  |             " | ||||||
|  |           > | ||||||
|  |             Gitee | ||||||
|  |           </VbenButton> | ||||||
|  |           <VbenButton | ||||||
|  |             variant="link" | ||||||
|  |             @click=" | ||||||
|  |               openWindow( | ||||||
|  |                 'https://github.com/yudaocode/yudao-ui-admin-vben/issues', | ||||||
|  |               ) | ||||||
|  |             " | ||||||
|  |           > | ||||||
|  |             Github | ||||||
|  |           </VbenButton> | ||||||
|  |         </VbenButtonGroup> | ||||||
|  | 
 | ||||||
|  |         <VbenButtonGroup class="basis-1/3" :gap="2" border size="large"> | ||||||
|  |           <p class="p-2">开发文档:</p> | ||||||
|  |           <VbenButton | ||||||
|  |             variant="link" | ||||||
|  |             @click="openWindow('https://doc.iocoder.cn/quick-start/')" | ||||||
|  |           > | ||||||
|  |             项目文档 | ||||||
|  |           </VbenButton> | ||||||
|  |           <VbenButton variant="link" @click="openWindow('https://antdv.com/')"> | ||||||
|  |             antdv 文档 | ||||||
|  |           </VbenButton> | ||||||
|  |         </VbenButtonGroup> | ||||||
|  |       </div> | ||||||
|  |       <p class="mt-2 flex justify-center"> | ||||||
|  |         <span> | ||||||
|  |           <ElImage src="/wx-xingyu.png" alt="数舵科技" /> | ||||||
|  |         </span> | ||||||
|  |       </p> | ||||||
|  |       <p class="mt-2 flex justify-center pt-4 text-sm italic"> | ||||||
|  |         本项目采用<ElTag type="primary">MIT</ElTag>开源协议,个人与企业可100% | ||||||
|  |         免费使用。 | ||||||
|  |       </p> | ||||||
|  |     </div> | ||||||
|  |   </Modal> | ||||||
|  | </template> | ||||||
|  | @ -0,0 +1,66 @@ | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import type { SystemTenantApi } from '#/api/system/tenant'; | ||||||
|  | 
 | ||||||
|  | import { onMounted, ref } from 'vue'; | ||||||
|  | 
 | ||||||
|  | import { useAccess } from '@vben/access'; | ||||||
|  | import { isTenantEnable, useTabs } from '@vben/hooks'; | ||||||
|  | import { useAccessStore } from '@vben/stores'; | ||||||
|  | 
 | ||||||
|  | import { ElMessage, ElOption, ElSelect } from 'element-plus'; | ||||||
|  | 
 | ||||||
|  | import { getSimpleTenantList } from '#/api/system/tenant'; | ||||||
|  | import { $t } from '#/locales'; | ||||||
|  | 
 | ||||||
|  | const { closeOtherTabs, refreshTab } = useTabs(); | ||||||
|  | 
 | ||||||
|  | const { hasAccessByCodes } = useAccess(); | ||||||
|  | const accessStore = useAccessStore(); | ||||||
|  | 
 | ||||||
|  | const tenantEnable = isTenantEnable(); | ||||||
|  | 
 | ||||||
|  | const value = ref<number>(accessStore.visitTenantId ?? 0); // 当前访问的租户 ID | ||||||
|  | const tenants = ref<SystemTenantApi.Tenant[]>([]); // 租户列表 | ||||||
|  | 
 | ||||||
|  | // TODO @xingyu:这个有可能 3 端复用么? | ||||||
|  | async function handleChange(id: number) { | ||||||
|  |   if (id === null) return; | ||||||
|  | 
 | ||||||
|  |   // 设置访问租户 ID | ||||||
|  |   accessStore.setVisitTenantId(id); | ||||||
|  |   // 关闭其他标签页,只保留当前页 | ||||||
|  |   await closeOtherTabs(); | ||||||
|  |   // 刷新当前页面 | ||||||
|  |   await refreshTab(); | ||||||
|  |   // 提示切换成功 | ||||||
|  |   const tenant = tenants.value.find((item) => item.id === id); | ||||||
|  |   if (tenant) { | ||||||
|  |     ElMessage.success(`切换当前租户为: ${tenant.name}`); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | onMounted(async () => { | ||||||
|  |   if (!tenantEnable) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   tenants.value = await getSimpleTenantList(); | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | <template> | ||||||
|  |   <div v-if="tenantEnable && hasAccessByCodes(['system:tenant:visit'])"> | ||||||
|  |     <ElSelect | ||||||
|  |       v-model="value" | ||||||
|  |       :placeholder="$t('page.tenant.placeholder')" | ||||||
|  |       clearable | ||||||
|  |       class="w-40" | ||||||
|  |       @change="handleChange" | ||||||
|  |     > | ||||||
|  |       <ElOption | ||||||
|  |         v-for="item in tenants" | ||||||
|  |         :key="item.id" | ||||||
|  |         :label="item.name" | ||||||
|  |         :value="item.id || 0" | ||||||
|  |       /> | ||||||
|  |     </ElSelect> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | @ -0,0 +1,16 @@ | ||||||
|  | import type { RouteRecordRaw } from 'vue-router'; | ||||||
|  | 
 | ||||||
|  | const routes: RouteRecordRaw[] = [ | ||||||
|  |   { | ||||||
|  |     path: '/system/notify-message', | ||||||
|  |     component: () => import('#/views/system/notify/my/index.vue'), | ||||||
|  |     name: 'MyNotifyMessage', | ||||||
|  |     meta: { | ||||||
|  |       title: '我的站内信', | ||||||
|  |       icon: 'ant-design:message-filled', | ||||||
|  |       hideInMenu: true, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | export default routes; | ||||||
		Loading…
	
		Reference in New Issue
	
	 puhui999
						puhui999