25 changed files with 595 additions and 9 deletions
@ -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); |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
export const Events = { |
|||
/** 收到服务器消息 */ |
|||
GetNotification: 'get-notification', |
|||
/** 新通知消息 */ |
|||
NotificationRecevied: 'sys_notifications_recevied', |
|||
/** 用户登录事件 */ |
|||
UserLogin: 'sys_user_login', |
|||
/** 用户登出事件 */ |
|||
UserLogout: 'sys_user_logout', |
|||
}; |
|||
@ -1 +1,2 @@ |
|||
export * from './events'; |
|||
export * from './validation'; |
|||
|
|||
@ -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<T = any>(type: EventType, event?: T): void; |
|||
|
|||
/** 订阅事件 */ |
|||
subscribe(type: '*', handler: WildcardHandler): void; |
|||
/** 订阅事件 */ |
|||
subscribe<T = any>(type: EventType, handler: Handler<T>): void; |
|||
|
|||
/** 退订事件 */ |
|||
unSubscribe(type: '*', handler: WildcardHandler): void; |
|||
/** 退订事件 */ |
|||
unSubscribe<T = any>(type: EventType, handler: Handler<T>): void; |
|||
} |
|||
|
|||
export function useEventBus(): EventBus { |
|||
return { |
|||
publish: emitter.emit, |
|||
subscribe: emitter.on, |
|||
unSubscribe: emitter.off, |
|||
}; |
|||
} |
|||
@ -1,4 +1,5 @@ |
|||
export * from './date'; |
|||
export * from './mitt'; |
|||
export * from './regex'; |
|||
export * from './string'; |
|||
export * from './tree'; |
|||
|
|||
@ -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<T = any> = (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<Handler>; |
|||
export type WildCardEventHandlerList = Array<WildcardHandler>; |
|||
|
|||
// 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<T = any>(type: EventType, event?: T): void; |
|||
off(type: '*', handler: WildcardHandler): void; |
|||
|
|||
off<T = any>(type: EventType, handler: Handler<T>): void; |
|||
on(type: '*', handler: WildcardHandler): void; |
|||
on<T = any>(type: EventType, handler: Handler<T>): 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<T = any>(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<T = any>(type: EventType, handler: Handler<T>) { |
|||
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<T = any>(type: EventType, handler: Handler<T>) { |
|||
const handlers = all?.get(type); |
|||
const added = handlers && handlers.push(handler); |
|||
if (!added) { |
|||
all?.set(type, [handler]); |
|||
} |
|||
}, |
|||
}; |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './notifications'; |
|||
@ -0,0 +1,4 @@ |
|||
export const NotificationNames = { |
|||
/** 会话过期通知 */ |
|||
SessionExpiration: 'AbpIdentity.Session.Expiration', |
|||
}; |
|||
@ -0,0 +1,2 @@ |
|||
export * from './useNotifications'; |
|||
export * from './useNotificationSerializer'; |
|||
@ -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, |
|||
}; |
|||
} |
|||
@ -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, |
|||
}; |
|||
} |
|||
@ -1,4 +1,5 @@ |
|||
export * from './api'; |
|||
export * from './components'; |
|||
export * from './constants'; |
|||
export * from './hooks'; |
|||
export * from './types'; |
|||
|
|||
@ -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:*" |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './useSignalR'; |
|||
@ -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<void> { |
|||
_throwIfNotInit(); |
|||
publish('signalR:beforeStart'); |
|||
try { |
|||
await connection!.start(); |
|||
publish('signalR:onStart'); |
|||
} catch (error) { |
|||
publish('signalR:onError', error); |
|||
} |
|||
} |
|||
|
|||
/** 关闭连接 */ |
|||
async function stop(): Promise<void> { |
|||
_throwIfNotInit(); |
|||
publish('signalR:beforeStop'); |
|||
try { |
|||
await connection!.stop(); |
|||
publish('signalR:onStop'); |
|||
} catch (error) { |
|||
publish('signalR:onError', error); |
|||
} |
|||
} |
|||
|
|||
/** 连接前事件 */ |
|||
function beforeStart<T = any>(callback: (event?: T) => void) { |
|||
subscribe('signalR:beforeStart', callback); |
|||
} |
|||
|
|||
/** 连接后事件 */ |
|||
function onStart<T = any>(callback: (event?: T) => void) { |
|||
subscribe('signalR:onStart', callback); |
|||
} |
|||
|
|||
/** 关闭连接前事件 */ |
|||
function beforeStop<T = any>(callback: (event?: T) => void) { |
|||
subscribe('signalR:beforeStop', callback); |
|||
} |
|||
|
|||
/** 关闭连接后事件 */ |
|||
function onStop<T = any>(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<void> { |
|||
_throwIfNotInit(); |
|||
return connection!.send(methodName, ...args); |
|||
} |
|||
|
|||
/** 调用函数 */ |
|||
function invoke<T = any>(methodName: string, ...args: any[]): Promise<T> { |
|||
_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, |
|||
}; |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './hooks'; |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/tsconfig", |
|||
"extends": "@vben/tsconfig/web.json", |
|||
"include": ["src"], |
|||
"exclude": ["node_modules"] |
|||
} |
|||
Loading…
Reference in new issue