diff --git a/apps/vben5/apps/app-antd/package.json b/apps/vben5/apps/app-antd/package.json index a77e1336d..675f638eb 100644 --- a/apps/vben5/apps/app-antd/package.json +++ b/apps/vben5/apps/app-antd/package.json @@ -30,6 +30,7 @@ "@abp/auditing": "workspace:*", "@abp/core": "workspace:*", "@abp/identity": "workspace:*", + "@abp/notifications": "workspace:*", "@abp/openiddict": "workspace:*", "@abp/permissions": "workspace:*", "@abp/request": "workspace:*", diff --git a/apps/vben5/apps/app-antd/src/hooks/useSessions.ts b/apps/vben5/apps/app-antd/src/hooks/useSessions.ts new file mode 100644 index 000000000..ae8cc1e28 --- /dev/null +++ b/apps/vben5/apps/app-antd/src/hooks/useSessions.ts @@ -0,0 +1,48 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { Notification as NotificationInfo } from '@abp/notifications'; + +import { onMounted, onUnmounted } from 'vue'; + +import { useAbpStore, useEventBus } from '@abp/core'; +import { NotificationNames, useNotifications } from '@abp/notifications'; +import { Modal } from 'ant-design-vue'; + +import { useAuthStore } from '#/store'; + +export function useSessions() { + const authStore = useAuthStore(); + const abpStore = useAbpStore(); + const { subscribe, unSubscribe } = useEventBus(); + const { register, release } = useNotifications(); + + function _register() { + subscribe(NotificationNames.SessionExpiration, _onSessionExpired); + register(); + } + + function _release() { + unSubscribe(NotificationNames.SessionExpiration, _onSessionExpired); + release(); + } + + /** 处理会话过期事件 */ + function _onSessionExpired(event?: NotificationInfo) { + const { data, title, message } = event!; + const sessionId = data.SessionId; + if (sessionId === abpStore.application?.currentUser?.sessionId) { + _release(); + Modal.confirm({ + iconType: 'warning', + title, + content: message, + centered: true, + afterClose: () => { + authStore.logout(); + }, + }); + } + } + + onMounted(_register); + onUnmounted(_release); +} diff --git a/apps/vben5/apps/app-antd/src/layouts/basic.vue b/apps/vben5/apps/app-antd/src/layouts/basic.vue index bc20b84e5..5367f444e 100644 --- a/apps/vben5/apps/app-antd/src/layouts/basic.vue +++ b/apps/vben5/apps/app-antd/src/layouts/basic.vue @@ -23,6 +23,7 @@ import { preferences } from '@vben/preferences'; import { useAccessStore, useUserStore } from '@vben/stores'; import { openWindow } from '@vben/utils'; +import { useSessions } from '#/hooks/useSessions'; import { $t } from '#/locales'; import { useAuthStore } from '#/store'; import LoginForm from '#/views/_core/authentication/login.vue'; @@ -60,6 +61,8 @@ const notifications = ref([ }, ]); +useSessions(); + const { replace } = useRouter(); const userStore = useUserStore(); const authStore = useAuthStore(); diff --git a/apps/vben5/apps/app-antd/src/store/auth.ts b/apps/vben5/apps/app-antd/src/store/auth.ts index 407426e9a..deb7fe4b8 100644 --- a/apps/vben5/apps/app-antd/src/store/auth.ts +++ b/apps/vben5/apps/app-antd/src/store/auth.ts @@ -7,7 +7,7 @@ import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; import { useTokenApi, useUserInfoApi } from '@abp/account'; -import { useAbpStore } from '@abp/core'; +import { Events, useAbpStore, useEventBus } from '@abp/core'; import { notification } from 'ant-design-vue'; import { defineStore } from 'pinia'; @@ -15,6 +15,7 @@ import { useAbpConfigApi } from '#/api/core/useAbpConfigApi'; import { $t } from '#/locales'; export const useAuthStore = defineStore('auth', () => { + const { publish } = useEventBus(); const { loginApi } = useTokenApi(); const { getUserInfoApi } = useUserInfoApi(); const { getConfigApi } = useAbpConfigApi(); @@ -49,6 +50,8 @@ export const useAuthStore = defineStore('auth', () => { userStore.setUserInfo(userInfo); + publish(Events.UserLogin, userInfo); + if (accessStore.loginExpired) { accessStore.setLoginExpired(false); } else { @@ -83,6 +86,8 @@ export const useAuthStore = defineStore('auth', () => { resetAllStores(); accessStore.setLoginExpired(false); + publish(Events.UserLogout); + // 回登录页带上当前路由地址 await router.replace({ path: LOGIN_PATH, diff --git a/apps/vben5/apps/app-antd/vite.config.mts b/apps/vben5/apps/app-antd/vite.config.mts index 9ac834557..ae6f52e14 100644 --- a/apps/vben5/apps/app-antd/vite.config.mts +++ b/apps/vben5/apps/app-antd/vite.config.mts @@ -8,22 +8,18 @@ export default defineConfig(async () => { proxy: { '/.well-known': { changeOrigin: true, - // rewrite: (path) => path.replace(/^\/api/, ''), - // mock代理目标地址 target: 'http://127.0.0.1:30001/', - ws: true, }, '/api': { changeOrigin: true, - // rewrite: (path) => path.replace(/^\/api/, ''), - // mock代理目标地址 target: 'http://127.0.0.1:30001/', - ws: true, }, '/connect': { changeOrigin: true, - // rewrite: (path) => path.replace(/^\/api/, ''), - // mock代理目标地址 + target: 'http://127.0.0.1:30001/', + }, + '/signalr-hubs': { + changeOrigin: true, target: 'http://127.0.0.1:30001/', ws: true, }, diff --git a/apps/vben5/packages/@abp/core/src/constants/events.ts b/apps/vben5/packages/@abp/core/src/constants/events.ts new file mode 100644 index 000000000..eb2360775 --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/constants/events.ts @@ -0,0 +1,10 @@ +export const Events = { + /** 收到服务器消息 */ + GetNotification: 'get-notification', + /** 新通知消息 */ + NotificationRecevied: 'sys_notifications_recevied', + /** 用户登录事件 */ + UserLogin: 'sys_user_login', + /** 用户登出事件 */ + UserLogout: 'sys_user_logout', +}; diff --git a/apps/vben5/packages/@abp/core/src/constants/index.ts b/apps/vben5/packages/@abp/core/src/constants/index.ts index 4d5ffa36a..b624aad55 100644 --- a/apps/vben5/packages/@abp/core/src/constants/index.ts +++ b/apps/vben5/packages/@abp/core/src/constants/index.ts @@ -1 +1,2 @@ +export * from './events'; export * from './validation'; diff --git a/apps/vben5/packages/@abp/core/src/hooks/index.ts b/apps/vben5/packages/@abp/core/src/hooks/index.ts index 84b8a1dc5..cf94b1a90 100644 --- a/apps/vben5/packages/@abp/core/src/hooks/index.ts +++ b/apps/vben5/packages/@abp/core/src/hooks/index.ts @@ -1,4 +1,5 @@ export * from './useAuthorization'; +export * from './useEventBus'; export * from './useFeatures'; export * from './useGlobalFeatures'; export * from './useLocalization'; diff --git a/apps/vben5/packages/@abp/core/src/hooks/useEventBus.ts b/apps/vben5/packages/@abp/core/src/hooks/useEventBus.ts new file mode 100644 index 000000000..a3339d12a --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/hooks/useEventBus.ts @@ -0,0 +1,30 @@ +import type { EventType, Handler, WildcardHandler } from '../utils/mitt'; + +import mitt from '../utils/mitt'; + +const emitter = mitt(); + +interface EventBus { + /** 发布事件 */ + publish(type: '*', event?: any): void; + /** 发布事件 */ + publish(type: EventType, event?: T): void; + + /** 订阅事件 */ + subscribe(type: '*', handler: WildcardHandler): void; + /** 订阅事件 */ + subscribe(type: EventType, handler: Handler): void; + + /** 退订事件 */ + unSubscribe(type: '*', handler: WildcardHandler): void; + /** 退订事件 */ + unSubscribe(type: EventType, handler: Handler): void; +} + +export function useEventBus(): EventBus { + return { + publish: emitter.emit, + subscribe: emitter.on, + unSubscribe: emitter.off, + }; +} diff --git a/apps/vben5/packages/@abp/core/src/utils/index.ts b/apps/vben5/packages/@abp/core/src/utils/index.ts index 202185507..2109114a1 100644 --- a/apps/vben5/packages/@abp/core/src/utils/index.ts +++ b/apps/vben5/packages/@abp/core/src/utils/index.ts @@ -1,4 +1,5 @@ export * from './date'; +export * from './mitt'; export * from './regex'; export * from './string'; export * from './tree'; diff --git a/apps/vben5/packages/@abp/core/src/utils/mitt.ts b/apps/vben5/packages/@abp/core/src/utils/mitt.ts new file mode 100644 index 000000000..28668e4d2 --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/utils/mitt.ts @@ -0,0 +1,107 @@ +/* eslint-disable array-callback-return */ +/** + * copy to https://github.com/developit/mitt + * Expand clear method + */ + +export type EventType = string | symbol; + +// An event handler can take an optional event argument +// and should not return a value +export type Handler = (event?: T) => void; +export type WildcardHandler = (type: EventType, event?: any) => void; + +// An array of all currently registered event handlers for a type +export type EventHandlerList = Array; +export type WildCardEventHandlerList = Array; + +// A map of event types and their corresponding event handlers. +export type EventHandlerMap = Map< + EventType, + EventHandlerList | WildCardEventHandlerList +>; + +export interface Emitter { + all: EventHandlerMap; + + clear(): void; + emit(type: '*', event?: any): void; + + emit(type: EventType, event?: T): void; + off(type: '*', handler: WildcardHandler): void; + + off(type: EventType, handler: Handler): void; + on(type: '*', handler: WildcardHandler): void; + on(type: EventType, handler: Handler): void; +} + +/** + * Mitt: Tiny (~200b) functional event emitter / pubsub. + * @name mitt + * @returns {Mitt} Emitter + */ +export default function mitt(all?: EventHandlerMap): Emitter { + all = all || new Map(); + + return { + /** + * A Map of event names to registered handler functions. + */ + all, + + /** + * Clear all + */ + clear() { + this.all.clear(); + }, + + /** + * Invoke all handlers for the given type. + * If present, `"*"` handlers are invoked after type-matched handlers. + * + * Note: Manually firing "*" handlers is not supported. + * + * @param {string|symbol} type The event type to invoke + * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler + * @memberOf mitt + */ + emit(type: EventType, evt: T) { + [...((all?.get(type) || []) as EventHandlerList)].map((handler) => { + handler(evt); + }); + [...((all?.get('*') || []) as WildCardEventHandlerList)].map( + (handler) => { + handler(type, evt); + }, + ); + }, + + /** + * Remove an event handler for the given type. + * @param {string|symbol} type Type of event to unregister `handler` from, or `"*"` + * @param {Function} handler Handler function to remove + * @memberOf mitt + */ + off(type: EventType, handler: Handler) { + const handlers = all?.get(type); + if (handlers) { + handlers.splice(handlers.indexOf(handler) >>> 0, 1); + } + }, + + /** + * Register an event handler for the given type. + * @param {string|symbol} type Type of event to listen for, or `"*"` for all events + * @param {Function} handler Function to call in response to given event + * @memberOf mitt + */ + on(type: EventType, handler: Handler) { + const handlers = all?.get(type); + const added = handlers && handlers.push(handler); + if (!added) { + all?.set(type, [handler]); + } + }, + }; +} diff --git a/apps/vben5/packages/@abp/notifications/package.json b/apps/vben5/packages/@abp/notifications/package.json index 4d2262b37..9b8708530 100644 --- a/apps/vben5/packages/@abp/notifications/package.json +++ b/apps/vben5/packages/@abp/notifications/package.json @@ -22,6 +22,7 @@ "dependencies": { "@abp/core": "workspace:*", "@abp/request": "workspace:*", + "@abp/signalr": "workspace:*", "@abp/ui": "workspace:*", "@ant-design/icons-vue": "catalog:", "@vben/access": "workspace:*", diff --git a/apps/vben5/packages/@abp/notifications/src/constants/index.ts b/apps/vben5/packages/@abp/notifications/src/constants/index.ts index e69de29bb..9ea5ce778 100644 --- a/apps/vben5/packages/@abp/notifications/src/constants/index.ts +++ b/apps/vben5/packages/@abp/notifications/src/constants/index.ts @@ -0,0 +1 @@ +export * from './notifications'; diff --git a/apps/vben5/packages/@abp/notifications/src/constants/notifications.ts b/apps/vben5/packages/@abp/notifications/src/constants/notifications.ts new file mode 100644 index 000000000..7b932c481 --- /dev/null +++ b/apps/vben5/packages/@abp/notifications/src/constants/notifications.ts @@ -0,0 +1,4 @@ +export const NotificationNames = { + /** 会话过期通知 */ + SessionExpiration: 'AbpIdentity.Session.Expiration', +}; diff --git a/apps/vben5/packages/@abp/notifications/src/hooks/index.ts b/apps/vben5/packages/@abp/notifications/src/hooks/index.ts new file mode 100644 index 000000000..59c19dc79 --- /dev/null +++ b/apps/vben5/packages/@abp/notifications/src/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './useNotifications'; +export * from './useNotificationSerializer'; diff --git a/apps/vben5/packages/@abp/notifications/src/hooks/useNotificationSerializer.ts b/apps/vben5/packages/@abp/notifications/src/hooks/useNotificationSerializer.ts new file mode 100644 index 000000000..c9bfff54d --- /dev/null +++ b/apps/vben5/packages/@abp/notifications/src/hooks/useNotificationSerializer.ts @@ -0,0 +1,56 @@ +import type { Notification, NotificationInfo } from '../types'; + +import { useLocalization } from '@abp/core'; + +export function useNotificationSerializer() { + function deserialize(notificationInfo: NotificationInfo): Notification { + const { data } = notificationInfo; + let title = data.extraProperties.title; + let message = data.extraProperties.message; + let description = data.extraProperties.description; + if (data.extraProperties.L === true || data.extraProperties.L === 'true') { + const { L } = useLocalization([ + data.extraProperties.title.resourceName ?? + data.extraProperties.title.ResourceName, + data.extraProperties.message.resourceName ?? + data.extraProperties.message.ResourceName, + data.extraProperties.description?.resourceName ?? + data.extraProperties.description?.ResourceName ?? + 'AbpUi', + ]); + title = L( + data.extraProperties.title.name ?? data.extraProperties.title.Name, + data.extraProperties.title.values ?? data.extraProperties.title.Values, + ); + message = L( + data.extraProperties.message.name ?? data.extraProperties.message.Name, + data.extraProperties.message.values ?? + data.extraProperties.message.Values, + ); + if (description) { + description = L( + data.extraProperties.description.name ?? + data.extraProperties.description.Name, + data.extraProperties.description.values ?? + data.extraProperties.description.Values, + ); + } + } + return { + contentType: notificationInfo.contentType, + creationTime: notificationInfo.creationTime, + data: data.extraProperties, + description, + lifetime: notificationInfo.lifetime, + message, + name: notificationInfo.name, + severity: notificationInfo.severity, + title, + type: notificationInfo.type, + }; + } + + return { + deserialize, + }; +} diff --git a/apps/vben5/packages/@abp/notifications/src/hooks/useNotifications.ts b/apps/vben5/packages/@abp/notifications/src/hooks/useNotifications.ts new file mode 100644 index 000000000..39b2370a6 --- /dev/null +++ b/apps/vben5/packages/@abp/notifications/src/hooks/useNotifications.ts @@ -0,0 +1,113 @@ +import type { Notification, NotificationInfo } from '../types/notifications'; + +import { Events, useEventBus } from '@abp/core'; +import { useSignalR } from '@abp/signalr'; +import { notification } from 'ant-design-vue'; + +import { + NotificationContentType, + NotificationSeverity, + NotificationType, +} from '../types/notifications'; +import { useNotificationSerializer } from './useNotificationSerializer'; + +export function useNotifications() { + const { deserialize } = useNotificationSerializer(); + const { publish, subscribe, unSubscribe } = useEventBus(); + const { init, off, on, onStart, stop } = useSignalR(); + + /** 注册通知 */ + function register() { + _registerEvents(); + _init(); + } + + /** 释放通知 */ + function release() { + _releaseEvents(); + stop(); + } + + function _init() { + onStart(() => on(Events.GetNotification, _onNotifyReceived)); + init({ + autoStart: true, + serverUrl: '/signalr-hubs/notifications', + useAccessToken: true, + }); + } + + /** 注册通知事件 */ + function _registerEvents() { + subscribe(Events.UserLogout, stop); + } + + /** 释放通知事件 */ + function _releaseEvents() { + unSubscribe(Events.UserLogout, stop); + off(Events.GetNotification, _onNotifyReceived); + } + + /** 接收通知回调 */ + function _onNotifyReceived(notificationInfo: NotificationInfo) { + const notification = deserialize(notificationInfo); + if (notification.type === NotificationType.ServiceCallback) { + publish(notification.name, notification); + return; + } + publish(Events.NotificationRecevied, notification); + _notification(notification); + } + + /** 通知推送 */ + function _notification(notifier: Notification) { + let message = notifier.description; + switch (notifier.contentType) { + case NotificationContentType.Html: + case NotificationContentType.Json: + case NotificationContentType.Markdown: { + message = notifier.title; + break; + } + case NotificationContentType.Text: { + message = notifier.description; + } + } + switch (notifier.severity) { + case NotificationSeverity.Error: + case NotificationSeverity.Fatal: { + notification.error({ + description: message, + message: notifier.title, + }); + break; + } + case NotificationSeverity.Info: { + notification.info({ + description: message, + message: notifier.title, + }); + break; + } + case NotificationSeverity.Success: { + notification.success({ + description: message, + message: notifier.title, + }); + break; + } + case NotificationSeverity.Warn: { + notification.warning({ + description: message, + message: notifier.title, + }); + break; + } + } + } + + return { + register, + release, + }; +} diff --git a/apps/vben5/packages/@abp/notifications/src/index.ts b/apps/vben5/packages/@abp/notifications/src/index.ts index f43dbaee0..48e3b6454 100644 --- a/apps/vben5/packages/@abp/notifications/src/index.ts +++ b/apps/vben5/packages/@abp/notifications/src/index.ts @@ -1,4 +1,5 @@ export * from './api'; export * from './components'; export * from './constants'; +export * from './hooks'; export * from './types'; diff --git a/apps/vben5/packages/@abp/notifications/src/types/notifications.ts b/apps/vben5/packages/@abp/notifications/src/types/notifications.ts index bf780d040..a1eeb8b10 100644 --- a/apps/vben5/packages/@abp/notifications/src/types/notifications.ts +++ b/apps/vben5/packages/@abp/notifications/src/types/notifications.ts @@ -75,7 +75,21 @@ interface NotificationGroupDto { notifications: NotificationDto[]; } +interface Notification { + contentType: NotificationContentType; + creationTime: Date; + data: Record; + description?: string; + lifetime: NotificationLifetime; + message: string; + name: string; + severity: NotificationSeverity; + title: string; + type: NotificationType; +} + export type { + Notification, NotificationData, NotificationDto, NotificationGroupDto, diff --git a/apps/vben5/packages/@abp/signalr/package.json b/apps/vben5/packages/@abp/signalr/package.json new file mode 100644 index 000000000..62c8d715c --- /dev/null +++ b/apps/vben5/packages/@abp/signalr/package.json @@ -0,0 +1,27 @@ +{ + "name": "@abp/signalr", + "version": "8.3.2", + "homepage": "https://github.com/colinin/abp-next-admin", + "bugs": "https://github.com/colinin/abp-next-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/colinin/abp-next-admin.git", + "directory": "packages/@abp/signalr" + }, + "license": "MIT", + "type": "module", + "sideEffects": [ + "**/*.css" + ], + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "dependencies": { + "@abp/core": "workspace:*", + "@microsoft/signalr": "catalog:", + "@vben/stores": "workspace:*" + } +} diff --git a/apps/vben5/packages/@abp/signalr/src/hooks/index.ts b/apps/vben5/packages/@abp/signalr/src/hooks/index.ts new file mode 100644 index 000000000..d5bdc52ff --- /dev/null +++ b/apps/vben5/packages/@abp/signalr/src/hooks/index.ts @@ -0,0 +1 @@ +export * from './useSignalR'; diff --git a/apps/vben5/packages/@abp/signalr/src/hooks/useSignalR.ts b/apps/vben5/packages/@abp/signalr/src/hooks/useSignalR.ts new file mode 100644 index 000000000..99cc2c057 --- /dev/null +++ b/apps/vben5/packages/@abp/signalr/src/hooks/useSignalR.ts @@ -0,0 +1,155 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { HubConnection, IHttpConnectionOptions } from '@microsoft/signalr'; + +import { useAccessStore } from '@vben/stores'; + +import { useEventBus } from '@abp/core'; +import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr'; + +interface SignalROptions { + /** 断线自动重连 */ + automaticReconnect?: boolean; + /** 初始化自动建立连接 */ + autoStart?: boolean; + /** 下次重试间隔(ms) */ + nextRetryDelayInMilliseconds?: number; + /** 服务端url */ + serverUrl: string; + /** 是否携带访问令牌 */ + useAccessToken?: boolean; +} + +export function useSignalR() { + const { publish, subscribe } = useEventBus(); + + let connection: HubConnection | null = null; + + /** 初始化SignalR */ + async function init({ + automaticReconnect = true, + autoStart = false, + nextRetryDelayInMilliseconds = 60_000, + serverUrl, + useAccessToken = true, + }: SignalROptions) { + const httpOptions: IHttpConnectionOptions = {}; + if (useAccessToken) { + const accessStore = useAccessStore(); + const token = accessStore.accessToken; + if (token) { + httpOptions.accessTokenFactory = () => + token.startsWith('Bearer ') ? token.slice(7) : token; + } + } + const connectionBuilder = new HubConnectionBuilder() + .withUrl(serverUrl, httpOptions) + .configureLogging(LogLevel.Warning); + if (automaticReconnect && nextRetryDelayInMilliseconds) { + connectionBuilder.withAutomaticReconnect({ + nextRetryDelayInMilliseconds: () => nextRetryDelayInMilliseconds, + }); + } + connection = connectionBuilder.build(); + if (autoStart) { + await start(); + } + } + + /** 启动连接 */ + async function start(): Promise { + _throwIfNotInit(); + publish('signalR:beforeStart'); + try { + await connection!.start(); + publish('signalR:onStart'); + } catch (error) { + publish('signalR:onError', error); + } + } + + /** 关闭连接 */ + async function stop(): Promise { + _throwIfNotInit(); + publish('signalR:beforeStop'); + try { + await connection!.stop(); + publish('signalR:onStop'); + } catch (error) { + publish('signalR:onError', error); + } + } + + /** 连接前事件 */ + function beforeStart(callback: (event?: T) => void) { + subscribe('signalR:beforeStart', callback); + } + + /** 连接后事件 */ + function onStart(callback: (event?: T) => void) { + subscribe('signalR:onStart', callback); + } + + /** 关闭连接前事件 */ + function beforeStop(callback: (event?: T) => void) { + subscribe('signalR:beforeStop', callback); + } + + /** 关闭连接后事件 */ + function onStop(callback: (event?: T) => void) { + subscribe('signalR:onStop', callback); + } + + /** 连接错误事件 */ + function onError(callback: (error?: Error) => void) { + subscribe('signalR:onError', callback); + } + + /** 订阅服务端消息 */ + function on(methodName: string, newMethod: (...args: any[]) => void): void { + connection?.on(methodName, newMethod); + } + + /** 注销服务端消息 */ + function off(methodName: string, method: (...args: any[]) => void): void { + connection?.off(methodName, method); + } + + /** 连接关闭事件 */ + function onClose(callback: (error?: Error) => void): void { + connection?.onclose(callback); + } + + /** 发送消息 */ + function send(methodName: string, ...args: any[]): Promise { + _throwIfNotInit(); + return connection!.send(methodName, ...args); + } + + /** 调用函数 */ + function invoke(methodName: string, ...args: any[]): Promise { + _throwIfNotInit(); + return connection!.invoke(methodName, ...args); + } + + function _throwIfNotInit() { + if (connection === null) { + throw new Error('unable to send message, connection not initialized!'); + } + } + + return { + beforeStart, + beforeStop, + init, + invoke, + off, + on, + onClose, + onError, + onStart, + onStop, + send, + start, + stop, + }; +} diff --git a/apps/vben5/packages/@abp/signalr/src/index.ts b/apps/vben5/packages/@abp/signalr/src/index.ts new file mode 100644 index 000000000..4cc90d02b --- /dev/null +++ b/apps/vben5/packages/@abp/signalr/src/index.ts @@ -0,0 +1 @@ +export * from './hooks'; diff --git a/apps/vben5/packages/@abp/signalr/tsconfig.json b/apps/vben5/packages/@abp/signalr/tsconfig.json new file mode 100644 index 000000000..ce1a891fb --- /dev/null +++ b/apps/vben5/packages/@abp/signalr/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/vben5/pnpm-workspace.yaml b/apps/vben5/pnpm-workspace.yaml index 1337072fc..1f751e415 100644 --- a/apps/vben5/pnpm-workspace.yaml +++ b/apps/vben5/pnpm-workspace.yaml @@ -32,6 +32,7 @@ catalog: '@intlify/unplugin-vue-i18n': ^6.0.1 '@jspm/generator': ^2.4.1 '@manypkg/get-packages': ^2.2.2 + '@microsoft/signalr': ^8.0.7 '@nolebase/vitepress-plugin-git-changelog': ^2.11.1 '@playwright/test': ^1.49.1 '@pnpm/workspace.read-manifest': ^1000.0.1