瀏覽代碼

feat: 通知模块自定义加强

雪忆天堂 2 月之前
父節點
當前提交
fe77bc8bc9

+ 30 - 0
apps/web-antd/src/layouts/basic.vue

@@ -147,6 +147,34 @@ function remove(id: number | string) {
 function handleMakeAll() {
   notifications.value.forEach((item) => (item.isRead = true));
 }
+
+const viewAll = () => {};
+
+const handleClick = (item: NotificationItem) => {
+  // 如果通知项有链接,点击时跳转
+  if (item.link) {
+    navigateTo(item.link, item.query, item.state);
+  }
+};
+
+function navigateTo(
+  link: string,
+  query?: Record<string, any>,
+  state?: Record<string, any>,
+) {
+  if (link.startsWith('http://') || link.startsWith('https://')) {
+    // 外部链接,在新标签页打开
+    window.open(link, '_blank');
+  } else {
+    // 内部路由链接,支持 query 参数和 state
+    router.push({
+      path: link,
+      query: query || {},
+      state,
+    });
+  }
+}
+
 watch(
   () => ({
     enable: preferences.app.watermark,
@@ -189,6 +217,8 @@ watch(
         @read="(item) => item.id && markRead(item.id)"
         @remove="(item) => item.id && remove(item.id)"
         @make-all="handleMakeAll"
+        @on-click="handleClick"
+        @view-all="viewAll"
       />
     </template>
     <template #extra>

+ 30 - 0
apps/web-antdv-next/src/layouts/basic.vue

@@ -147,6 +147,34 @@ function remove(id: number | string) {
 function handleMakeAll() {
   notifications.value.forEach((item) => (item.isRead = true));
 }
+
+const viewAll = () => {};
+
+const handleClick = (item: NotificationItem) => {
+  // 如果通知项有链接,点击时跳转
+  if (item.link) {
+    navigateTo(item.link, item.query, item.state);
+  }
+};
+
+function navigateTo(
+  link: string,
+  query?: Record<string, any>,
+  state?: Record<string, any>,
+) {
+  if (link.startsWith('http://') || link.startsWith('https://')) {
+    // 外部链接,在新标签页打开
+    window.open(link, '_blank');
+  } else {
+    // 内部路由链接,支持 query 参数和 state
+    router.push({
+      path: link,
+      query: query || {},
+      state,
+    });
+  }
+}
+
 watch(
   () => ({
     enable: preferences.app.watermark,
@@ -189,6 +217,8 @@ watch(
         @read="(item) => item.id && markRead(item.id)"
         @remove="(item) => item.id && remove(item.id)"
         @make-all="handleMakeAll"
+        @on-click="handleClick"
+        @view-all="viewAll"
       />
     </template>
     <template #extra>

+ 30 - 0
apps/web-ele/src/layouts/basic.vue

@@ -147,6 +147,34 @@ function remove(id: number | string) {
 function handleMakeAll() {
   notifications.value.forEach((item) => (item.isRead = true));
 }
+
+const viewAll = () => {};
+
+const handleClick = (item: NotificationItem) => {
+  // 如果通知项有链接,点击时跳转
+  if (item.link) {
+    navigateTo(item.link, item.query, item.state);
+  }
+};
+
+function navigateTo(
+  link: string,
+  query?: Record<string, any>,
+  state?: Record<string, any>,
+) {
+  if (link.startsWith('http://') || link.startsWith('https://')) {
+    // 外部链接,在新标签页打开
+    window.open(link, '_blank');
+  } else {
+    // 内部路由链接,支持 query 参数和 state
+    router.push({
+      path: link,
+      query: query || {},
+      state,
+    });
+  }
+}
+
 watch(
   () => ({
     enable: preferences.app.watermark,
@@ -189,6 +217,8 @@ watch(
         @read="(item) => item.id && markRead(item.id)"
         @remove="(item) => item.id && remove(item.id)"
         @make-all="handleMakeAll"
+        @on-click="handleClick"
+        @view-all="viewAll"
       />
     </template>
     <template #extra>

+ 29 - 0
apps/web-naive/src/layouts/basic.vue

@@ -148,6 +148,33 @@ function handleMakeAll() {
   notifications.value.forEach((item) => (item.isRead = true));
 }
 
+const viewAll = () => {};
+
+const handleClick = (item: NotificationItem) => {
+  // 如果通知项有链接,点击时跳转
+  if (item.link) {
+    navigateTo(item.link, item.query, item.state);
+  }
+};
+
+function navigateTo(
+  link: string,
+  query?: Record<string, any>,
+  state?: Record<string, any>,
+) {
+  if (link.startsWith('http://') || link.startsWith('https://')) {
+    // 外部链接,在新标签页打开
+    window.open(link, '_blank');
+  } else {
+    // 内部路由链接,支持 query 参数和 state
+    router.push({
+      path: link,
+      query: query || {},
+      state,
+    });
+  }
+}
+
 watch(
   () => ({
     enable: preferences.app.watermark,
@@ -190,6 +217,8 @@ watch(
         @read="(item) => item.id && markRead(item.id)"
         @remove="(item) => item.id && remove(item.id)"
         @make-all="handleMakeAll"
+        @on-click="handleClick"
+        @view-all="viewAll"
       />
     </template>
     <template #extra>

+ 30 - 0
apps/web-tdesign/src/layouts/basic.vue

@@ -147,6 +147,34 @@ function remove(id: number | string) {
 function handleMakeAll() {
   notifications.value.forEach((item) => (item.isRead = true));
 }
+
+const viewAll = () => {};
+
+const handleClick = (item: NotificationItem) => {
+  // 如果通知项有链接,点击时跳转
+  if (item.link) {
+    navigateTo(item.link, item.query, item.state);
+  }
+};
+
+function navigateTo(
+  link: string,
+  query?: Record<string, any>,
+  state?: Record<string, any>,
+) {
+  if (link.startsWith('http://') || link.startsWith('https://')) {
+    // 外部链接,在新标签页打开
+    window.open(link, '_blank');
+  } else {
+    // 内部路由链接,支持 query 参数和 state
+    router.push({
+      path: link,
+      query: query || {},
+      state,
+    });
+  }
+}
+
 watch(
   () => ({
     enable: preferences.app.watermark,
@@ -189,6 +217,8 @@ watch(
         @read="(item) => item.id && markRead(item.id)"
         @remove="(item) => item.id && remove(item.id)"
         @make-all="handleMakeAll"
+        @on-click="handleClick"
+        @view-all="viewAll"
       />
     </template>
     <template #extra>

+ 1 - 0
packages/@core/base/icons/src/lucide.ts

@@ -25,6 +25,7 @@ export {
   CircleX,
   Copy,
   CornerDownLeft,
+  Download,
   Ellipsis,
   Eraser,
   Expand,

+ 1 - 0
packages/effects/common-ui/src/components/index.ts

@@ -24,6 +24,7 @@ export {
   VbenContextMenu,
   VbenCountToAnimator,
   VbenFullScreen,
+  VbenIconButton,
   VbenInputPassword,
   VbenLoading,
   VbenLogo,

+ 74 - 98
packages/effects/layouts/src/widgets/notification/notification.vue

@@ -1,8 +1,6 @@
 <script lang="ts" setup>
 import type { NotificationItem } from './types';
 
-import { useRouter } from 'vue-router';
-
 import { Bell, CircleCheckBig, CircleX, MailCheck } from '@vben/icons';
 import { $t } from '@vben/locales';
 
@@ -15,76 +13,48 @@ import {
 
 import { useToggle } from '@vueuse/core';
 
-interface Props {
-  /**
-   * 显示圆点
-   */
-  dot?: boolean;
-  /**
-   * 消息列表
-   */
-  notifications?: NotificationItem[];
-}
-
 defineOptions({ name: 'NotificationPopup' });
 
-withDefaults(defineProps<Props>(), {
-  dot: false,
-  notifications: () => [],
-});
+withDefaults(
+  defineProps<{
+    /** 显示圆点 */
+    dot?: boolean;
+    /** 消息列表 */
+    notifications?: NotificationItem[];
+  }>(),
+  {
+    dot: false,
+    notifications: () => [],
+  },
+);
 
 const emit = defineEmits<{
   clear: [];
   makeAll: [];
+  onClick: [NotificationItem];
   read: [NotificationItem];
   remove: [NotificationItem];
   viewAll: [];
 }>();
 
-const router = useRouter();
 const [open, toggle] = useToggle();
 
-function close() {
+const close = () => {
   open.value = false;
-}
+};
 
-function handleViewAll() {
+const handleViewAll = () => {
   emit('viewAll');
   close();
-}
+};
 
-function handleMakeAll() {
+const handleMakeAll = () => {
   emit('makeAll');
-}
+};
 
-function handleClear() {
+const handleClear = () => {
   emit('clear');
-}
-
-function handleClick(item: NotificationItem) {
-  // 如果通知项有链接,点击时跳转
-  if (item.link) {
-    navigateTo(item.link, item.query, item.state);
-  }
-}
-
-function navigateTo(
-  link: string,
-  query?: Record<string, any>,
-  state?: Record<string, any>,
-) {
-  if (link.startsWith('http://') || link.startsWith('https://')) {
-    // 外部链接,在新标签页打开
-    window.open(link, '_blank');
-  } else {
-    // 内部路由链接,支持 query 参数和 state
-    router.push({
-      path: link,
-      query: query || {},
-      state,
-    });
-  }
-}
+};
 </script>
 <template>
   <VbenPopover v-model:open="open" content-class="relative right-2 w-90 p-0">
@@ -104,66 +74,72 @@ function navigateTo(
       <div class="flex items-center justify-between p-4 py-3">
         <div class="text-foreground">{{ $t('ui.widgets.notifications') }}</div>
         <VbenIconButton
-          :disabled="notifications.length <= 0"
+          :disabled="!notifications || notifications.length <= 0"
           :tooltip="$t('ui.widgets.markAllAsRead')"
           @click="handleMakeAll"
         >
           <MailCheck class="size-4" />
         </VbenIconButton>
       </div>
-      <VbenScrollbar v-if="notifications.length > 0">
+      <VbenScrollbar v-if="!notifications || notifications.length > 0">
         <ul class="flex! max-h-90 w-full flex-col">
           <template v-for="item in notifications" :key="item.id ?? item.title">
             <li
               class="relative flex w-full cursor-pointer items-start gap-5 border-t border-border p-3 hover:bg-accent"
-              @click="handleClick(item)"
+              @click="emit('onClick', item)"
             >
-              <span
-                v-if="!item.isRead"
-                class="absolute top-2 right-2 size-2 rounded-sm bg-primary"
-              ></span>
-
-              <span
-                class="relative flex size-10 shrink-0 overflow-hidden rounded-full"
-              >
-                <img
-                  :src="item.avatar"
-                  class="aspect-square size-full object-cover"
-                />
-              </span>
-              <div class="flex flex-col gap-1 leading-none">
-                <p class="font-semibold">{{ item.title }}</p>
-                <p class="my-1 line-clamp-2 text-xs text-muted-foreground">
-                  {{ item.message }}
-                </p>
-                <p class="line-clamp-2 text-xs text-muted-foreground">
-                  {{ item.date }}
-                </p>
-              </div>
-              <div
-                class="absolute top-1/2 right-3 flex -translate-y-1/2 flex-col gap-2"
-              >
-                <VbenIconButton
+              <slot name="content" :item="item">
+                <span
                   v-if="!item.isRead"
-                  size="xs"
-                  variant="ghost"
-                  class="h-6 px-2"
-                  :tooltip="$t('common.confirm')"
-                  @click.stop="emit('read', item)"
+                  class="absolute top-2 right-2 size-2 rounded-sm bg-primary"
+                ></span>
+
+                <span
+                  class="relative flex size-10 shrink-0 overflow-hidden rounded-full"
                 >
-                  <CircleCheckBig class="size-4" />
-                </VbenIconButton>
-                <VbenIconButton
-                  v-if="item.isRead"
-                  size="xs"
-                  variant="ghost"
-                  class="h-6 px-2 text-destructive"
-                  :tooltip="$t('common.delete')"
-                  @click.stop="emit('remove', item)"
+                  <img
+                    :src="item.avatar"
+                    class="aspect-square size-full object-cover"
+                  />
+                </span>
+                <div class="flex flex-col gap-1 leading-none">
+                  <p class="font-semibold">{{ item.title }}</p>
+                  <p class="my-1 line-clamp-2 text-xs text-muted-foreground">
+                    {{ item.message }}
+                  </p>
+                  <p class="line-clamp-2 text-xs text-muted-foreground">
+                    {{ item.date }}
+                  </p>
+                </div>
+                <div
+                  class="absolute top-1/2 right-3 flex -translate-y-1/2 flex-row gap-1"
                 >
-                  <CircleX class="size-4" />
-                </VbenIconButton>
-              </div>
+                  <slot name="action" :item="item">
+                    <slot name="action-prepend" :item="item"></slot>
+                    <VbenIconButton
+                      v-if="!item.isRead"
+                      size="xs"
+                      variant="ghost"
+                      class="h-6 px-2"
+                      :tooltip="$t('common.confirm')"
+                      @click.stop="emit('read', item)"
+                    >
+                      <CircleCheckBig class="size-4" />
+                    </VbenIconButton>
+                    <VbenIconButton
+                      v-if="item.isRead"
+                      size="xs"
+                      variant="ghost"
+                      class="h-6 px-2 text-destructive"
+                      :tooltip="$t('common.delete')"
+                      @click.stop="emit('remove', item)"
+                    >
+                      <CircleX class="size-4" />
+                    </VbenIconButton>
+                    <slot name="action-append" :item="item"></slot>
+                  </slot>
+                </div>
+              </slot>
             </li>
           </template>
         </ul>
@@ -179,7 +155,7 @@ function navigateTo(
         class="flex items-center justify-between border-t border-border px-4 py-3"
       >
         <VbenButton
-          :disabled="notifications.length <= 0"
+          :disabled="!notifications || notifications.length <= 0"
           size="sm"
           variant="ghost"
           @click="handleClear"

+ 2 - 0
packages/effects/layouts/src/widgets/notification/types.ts

@@ -12,6 +12,8 @@ interface NotificationItem {
   link?: string;
   query?: Record<string, any>;
   state?: Record<string, any>;
+  /** 业务字段 */
+  [key: string]: any;
 }
 
 export type { NotificationItem };

+ 29 - 0
playground/src/layouts/basic.vue

@@ -163,6 +163,33 @@ function handleMakeAll() {
 
 function handleClickLogo() {}
 
+const viewAll = () => {};
+
+const handleClick = (item: NotificationItem) => {
+  // 如果通知项有链接,点击时跳转
+  if (item.link) {
+    navigateTo(item.link, item.query, item.state);
+  }
+};
+
+function navigateTo(
+  link: string,
+  query?: Record<string, any>,
+  state?: Record<string, any>,
+) {
+  if (link.startsWith('http://') || link.startsWith('https://')) {
+    // 外部链接,在新标签页打开
+    window.open(link, '_blank');
+  } else {
+    // 内部路由链接,支持 query 参数和 state
+    router.push({
+      path: link,
+      query: query || {},
+      state,
+    });
+  }
+}
+
 watch(
   () => ({
     enable: preferences.app.watermark,
@@ -215,6 +242,8 @@ onBeforeMount(() => {
         @read="(item) => item.id && markRead(item.id)"
         @remove="(item) => item.id && remove(item.id)"
         @make-all="handleMakeAll"
+        @on-click="handleClick"
+        @view-all="viewAll"
       />
     </template>
     <template #extra>