committed by
GitHub
45 changed files with 1313 additions and 19 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,15 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { Page } from '@vben/common-ui'; |
||||
|
|
||||
|
import { SessionTable } from '@abp/identity'; |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'IdentitySessions', |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Page> |
||||
|
<SessionTable /> |
||||
|
</Page> |
||||
|
</template> |
||||
@ -1,4 +1,5 @@ |
|||||
export { useAccountApi } from './useAccountApi'; |
export { useAccountApi } from './useAccountApi'; |
||||
|
export { useMySessionApi } from './useMySessionApi'; |
||||
export { useProfileApi } from './useProfileApi'; |
export { useProfileApi } from './useProfileApi'; |
||||
export { useTokenApi } from './useTokenApi'; |
export { useTokenApi } from './useTokenApi'; |
||||
export { useUserInfoApi } from './useUserInfoApi'; |
export { useUserInfoApi } from './useUserInfoApi'; |
||||
|
|||||
@ -0,0 +1,41 @@ |
|||||
|
import type { PagedResultDto } from '@abp/core'; |
||||
|
import type { GetMySessionsInput, IdentitySessionDto } from '@abp/identity'; |
||||
|
|
||||
|
import { useRequest } from '@abp/request'; |
||||
|
|
||||
|
export function useMySessionApi() { |
||||
|
const { cancel, request } = useRequest(); |
||||
|
|
||||
|
/** |
||||
|
* 查询会话列表 |
||||
|
* @param { GetMySessionsInput } input 查询参数 |
||||
|
* @returns { Promise<PagedResultDto<IdentitySessionDto>> } 用户会话列表 |
||||
|
*/ |
||||
|
function getSessionsApi( |
||||
|
input?: GetMySessionsInput, |
||||
|
): Promise<PagedResultDto<IdentitySessionDto>> { |
||||
|
return request<PagedResultDto<IdentitySessionDto>>( |
||||
|
'/api/account/my-profile/sessions', |
||||
|
{ |
||||
|
method: 'GET', |
||||
|
params: input, |
||||
|
}, |
||||
|
); |
||||
|
} |
||||
|
/** |
||||
|
* 撤销会话 |
||||
|
* @param { string } sessionId 会话id |
||||
|
* @returns { Promise<void> } |
||||
|
*/ |
||||
|
function revokeSessionApi(sessionId: string): Promise<void> { |
||||
|
return request(`/api/account/my-profile/sessions/${sessionId}/revoke`, { |
||||
|
method: 'DELETE', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
cancel, |
||||
|
getSessionsApi, |
||||
|
revokeSessionApi, |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { IdentitySessionDto } from '@abp/identity'; |
||||
|
|
||||
|
import { onMounted, ref } from 'vue'; |
||||
|
|
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { UserSessionTable } from '@abp/identity'; |
||||
|
import { Card, message, Modal } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useMySessionApi } from '../../api/useMySessionApi'; |
||||
|
|
||||
|
const { cancel, getSessionsApi, revokeSessionApi } = useMySessionApi(); |
||||
|
|
||||
|
const sessions = ref<IdentitySessionDto[]>([]); |
||||
|
|
||||
|
async function getSessions() { |
||||
|
const { items } = await getSessionsApi(); |
||||
|
sessions.value = items; |
||||
|
} |
||||
|
|
||||
|
async function onRevoke(session: IdentitySessionDto) { |
||||
|
Modal.confirm({ |
||||
|
centered: true, |
||||
|
content: $t('AbpIdentity.SessionWillBeRevokedMessage'), |
||||
|
iconType: 'warning', |
||||
|
onCancel: () => { |
||||
|
cancel(); |
||||
|
}, |
||||
|
onOk: async () => { |
||||
|
await revokeSessionApi(session.sessionId); |
||||
|
message.success($t('AbpIdentity.SuccessfullyRevoked')); |
||||
|
await getSessions(); |
||||
|
}, |
||||
|
title: $t('AbpUi.AreYouSure'), |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
onMounted(getSessions); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Card :bordered="false" :title="$t('abp.account.settings.sessionSettings')"> |
||||
|
<UserSessionTable :sessions="sessions" @revoke="onRevoke" /> |
||||
|
</Card> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -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'; |
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 './date'; |
||||
|
export * from './mitt'; |
||||
export * from './regex'; |
export * from './regex'; |
||||
export * from './string'; |
export * from './string'; |
||||
export * from './tree'; |
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,42 @@ |
|||||
|
import type { PagedResultDto } from '@abp/core'; |
||||
|
|
||||
|
import type { GetUserSessionsInput, IdentitySessionDto } from '../types'; |
||||
|
|
||||
|
import { useRequest } from '@abp/request'; |
||||
|
|
||||
|
export function useUserSessionsApi() { |
||||
|
const { cancel, request } = useRequest(); |
||||
|
|
||||
|
/** |
||||
|
* 查询会话列表 |
||||
|
* @param { GetUserSessionsInput } input 查询参数 |
||||
|
* @returns { Promise<PagedResultDto<IdentitySessionDto>> } 用户会话列表 |
||||
|
*/ |
||||
|
function getSessionsApi( |
||||
|
input?: GetUserSessionsInput, |
||||
|
): Promise<PagedResultDto<IdentitySessionDto>> { |
||||
|
return request<PagedResultDto<IdentitySessionDto>>( |
||||
|
'/api/identity/sessions', |
||||
|
{ |
||||
|
method: 'GET', |
||||
|
params: input, |
||||
|
}, |
||||
|
); |
||||
|
} |
||||
|
/** |
||||
|
* 撤销会话 |
||||
|
* @param { string } sessionId 会话id |
||||
|
* @returns { Promise<void> } |
||||
|
*/ |
||||
|
function revokeSessionApi(sessionId: string): Promise<void> { |
||||
|
return request(`/api/identity/sessions/${sessionId}/revoke`, { |
||||
|
method: 'DELETE', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
cancel, |
||||
|
getSessionsApi, |
||||
|
revokeSessionApi, |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,243 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { VbenFormProps, VxeGridListeners, VxeGridProps } from '@abp/ui'; |
||||
|
import type { SelectValue } from 'ant-design-vue/es/select'; |
||||
|
|
||||
|
import type { IdentityUserDto } from '../../types'; |
||||
|
import type { IdentitySessionDto } from '../../types/sessions'; |
||||
|
|
||||
|
import { computed, h, onMounted, ref } from 'vue'; |
||||
|
|
||||
|
import { useAccess } from '@vben/access'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { useAbpStore } from '@abp/core'; |
||||
|
import { useVbenVxeGrid } from '@abp/ui'; |
||||
|
import { DeleteOutlined } from '@ant-design/icons-vue'; |
||||
|
import { Button, message, Modal, Select, Tag } from 'ant-design-vue'; |
||||
|
import debounce from 'lodash.debounce'; |
||||
|
|
||||
|
import { useUsersApi } from '../../api/useUsersApi'; |
||||
|
import { useUserSessionsApi } from '../../api/useUserSessionsApi'; |
||||
|
import { IdentitySessionPermissions } from '../../constants/permissions'; |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'SessionTable', |
||||
|
}); |
||||
|
const { hasAccessByCodes } = useAccess(); |
||||
|
const { getPagedListApi: getUserListApi } = useUsersApi(); |
||||
|
const { cancel, getSessionsApi, revokeSessionApi } = useUserSessionsApi(); |
||||
|
|
||||
|
const abpStore = useAbpStore(); |
||||
|
|
||||
|
const users = ref<IdentityUserDto[]>([]); |
||||
|
|
||||
|
/** 获取登录用户会话Id */ |
||||
|
const getMySessionId = computed(() => { |
||||
|
return abpStore.application?.currentUser.sessionId; |
||||
|
}); |
||||
|
/** 获取是否允许撤销会话 */ |
||||
|
const getAllowRevokeSession = computed(() => { |
||||
|
return (session: IdentitySessionDto) => { |
||||
|
return getMySessionId.value !== session.sessionId; |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
const formOptions: VbenFormProps = { |
||||
|
// 默认展开 |
||||
|
collapsed: false, |
||||
|
schema: [ |
||||
|
{ |
||||
|
component: 'Select', |
||||
|
fieldName: 'userId', |
||||
|
label: $t('AbpIdentity.DisplayName:UserName'), |
||||
|
}, |
||||
|
{ |
||||
|
component: 'Input', |
||||
|
fieldName: 'clientId', |
||||
|
label: $t('AbpIdentity.DisplayName:ClientId'), |
||||
|
}, |
||||
|
{ |
||||
|
component: 'Input', |
||||
|
fieldName: 'device', |
||||
|
label: $t('AbpIdentity.DisplayName:Device'), |
||||
|
}, |
||||
|
{ |
||||
|
component: 'Input', |
||||
|
fieldName: 'filter', |
||||
|
formItemClass: 'col-span-2 items-baseline', |
||||
|
label: $t('AbpUi.Search'), |
||||
|
}, |
||||
|
], |
||||
|
// 控制表单是否显示折叠按钮 |
||||
|
showCollapseButton: true, |
||||
|
// 按下回车时是否提交表单 |
||||
|
submitOnEnter: true, |
||||
|
}; |
||||
|
|
||||
|
const gridOptions: VxeGridProps<IdentitySessionDto> = { |
||||
|
columns: [ |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'sessionId', |
||||
|
minWidth: 150, |
||||
|
title: $t('AbpIdentity.DisplayName:SessionId'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'device', |
||||
|
minWidth: 120, |
||||
|
slots: { default: 'device' }, |
||||
|
title: $t('AbpIdentity.DisplayName:Device'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'deviceInfo', |
||||
|
title: $t('AbpIdentity.DisplayName:DeviceInfo'), |
||||
|
width: 'auto', |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'clientId', |
||||
|
minWidth: 120, |
||||
|
title: $t('AbpIdentity.DisplayName:ClientId'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'ipAddresses', |
||||
|
minWidth: 120, |
||||
|
title: $t('AbpIdentity.DisplayName:IpAddresses'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'signedIn', |
||||
|
minWidth: 120, |
||||
|
title: $t('AbpIdentity.DisplayName:SignedIn'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'lastAccessed', |
||||
|
minWidth: 120, |
||||
|
title: $t('AbpIdentity.DisplayName:LastAccessed'), |
||||
|
}, |
||||
|
{ |
||||
|
field: 'action', |
||||
|
fixed: 'right', |
||||
|
slots: { default: 'action' }, |
||||
|
title: $t('AbpUi.Actions'), |
||||
|
visible: hasAccessByCodes([ |
||||
|
IdentitySessionPermissions.Default, |
||||
|
IdentitySessionPermissions.Revoke, |
||||
|
]), |
||||
|
width: 150, |
||||
|
}, |
||||
|
], |
||||
|
exportConfig: {}, |
||||
|
keepSource: true, |
||||
|
proxyConfig: { |
||||
|
ajax: { |
||||
|
query: async ({ page }, formValues) => { |
||||
|
return await getSessionsApi({ |
||||
|
maxResultCount: page.pageSize, |
||||
|
skipCount: (page.currentPage - 1) * page.pageSize, |
||||
|
...formValues, |
||||
|
}); |
||||
|
}, |
||||
|
}, |
||||
|
response: { |
||||
|
total: 'totalCount', |
||||
|
list: 'items', |
||||
|
}, |
||||
|
}, |
||||
|
toolbarConfig: { |
||||
|
custom: true, |
||||
|
export: true, |
||||
|
// import: true, |
||||
|
refresh: true, |
||||
|
zoom: true, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
const gridEvents: VxeGridListeners<IdentitySessionDto> = { |
||||
|
cellClick: () => {}, |
||||
|
}; |
||||
|
|
||||
|
const [Grid, gridApi] = useVbenVxeGrid({ |
||||
|
formOptions, |
||||
|
gridEvents, |
||||
|
gridOptions, |
||||
|
}); |
||||
|
|
||||
|
const onDelete = (row: IdentitySessionDto) => { |
||||
|
Modal.confirm({ |
||||
|
centered: true, |
||||
|
content: $t('AbpIdentity.SessionWillBeRevokedMessage'), |
||||
|
onCancel: () => { |
||||
|
cancel(); |
||||
|
}, |
||||
|
onOk: async () => { |
||||
|
await revokeSessionApi(row.sessionId); |
||||
|
message.success($t('AbpIdentity.SuccessfullyRevoked')); |
||||
|
await gridApi.query(); |
||||
|
}, |
||||
|
title: $t('AbpUi.AreYouSure'), |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
const onGetUsers = debounce(async (filter?: string) => { |
||||
|
const { items } = await getUserListApi({ filter }); |
||||
|
users.value = items; |
||||
|
}, 500); |
||||
|
|
||||
|
function onFieldChange(field: string, value?: SelectValue) { |
||||
|
gridApi.formApi.setFieldValue(field, value); |
||||
|
} |
||||
|
|
||||
|
onMounted(onGetUsers); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Grid :table-title="$t('AbpIdentity.IdentitySessions')"> |
||||
|
<template #form-userId="{ modelValue }"> |
||||
|
<Select |
||||
|
:default-active-first-option="false" |
||||
|
:field-names="{ label: 'userName', value: 'id' }" |
||||
|
:filter-option="false" |
||||
|
:options="users" |
||||
|
:placeholder="$t('ui.placeholder.select')" |
||||
|
:value="modelValue" |
||||
|
allow-clear |
||||
|
class="w-full" |
||||
|
show-search |
||||
|
@change="(val) => onFieldChange('userId', val)" |
||||
|
@search="onGetUsers" |
||||
|
/> |
||||
|
</template> |
||||
|
<template #device="{ row }"> |
||||
|
<div class="flex flex-row"> |
||||
|
<span>{{ row.device }}</span> |
||||
|
<div class="pl-[5px]"> |
||||
|
<Tag v-if="row.sessionId === getMySessionId" color="#87d068"> |
||||
|
{{ $t('AbpIdentity.CurrentSession') }} |
||||
|
</Tag> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<template #action="{ row }"> |
||||
|
<div class="flex flex-row"> |
||||
|
<Button |
||||
|
v-if="getAllowRevokeSession(row)" |
||||
|
:icon="h(DeleteOutlined)" |
||||
|
block |
||||
|
danger |
||||
|
type="link" |
||||
|
v-access:code="[IdentitySessionPermissions.Revoke]" |
||||
|
@click="onDelete(row)" |
||||
|
> |
||||
|
{{ $t('AbpIdentity.RevokeSession') }} |
||||
|
</Button> |
||||
|
</div> |
||||
|
</template> |
||||
|
</Grid> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="scss" scoped></style> |
||||
@ -0,0 +1,158 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { VxeGridProps } from '@abp/ui'; |
||||
|
|
||||
|
import type { IdentitySessionDto } from '../../types/sessions'; |
||||
|
|
||||
|
import { computed, nextTick, reactive, watch } from 'vue'; |
||||
|
|
||||
|
import { useAccess } from '@vben/access'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { useAbpStore } from '@abp/core'; |
||||
|
import { useVbenVxeGrid } from '@abp/ui'; |
||||
|
import { Button, Descriptions, Tag } from 'ant-design-vue'; |
||||
|
|
||||
|
import { IdentitySessionPermissions } from '../../constants/permissions'; |
||||
|
|
||||
|
const props = defineProps<{ |
||||
|
sessions: IdentitySessionDto[]; |
||||
|
}>(); |
||||
|
const emits = defineEmits<{ |
||||
|
(event: 'revoke', session: IdentitySessionDto): void; |
||||
|
}>(); |
||||
|
const DescriptionItem = Descriptions.Item; |
||||
|
|
||||
|
const { hasAccessByCodes } = useAccess(); |
||||
|
const abpStore = useAbpStore(); |
||||
|
/** 获取登录用户会话Id */ |
||||
|
const getMySessionId = computed(() => { |
||||
|
return abpStore.application?.currentUser.sessionId; |
||||
|
}); |
||||
|
/** 获取是否允许撤销会话 */ |
||||
|
const getAllowRevokeSession = computed(() => { |
||||
|
return (session: IdentitySessionDto) => { |
||||
|
if (getMySessionId.value === session.sessionId) { |
||||
|
return false; |
||||
|
} |
||||
|
return hasAccessByCodes([IdentitySessionPermissions.Revoke]); |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
const gridOptions = reactive<VxeGridProps<IdentitySessionDto>>({ |
||||
|
columns: [ |
||||
|
{ |
||||
|
align: 'left', |
||||
|
slots: { content: 'deviceInfo' }, |
||||
|
type: 'expand', |
||||
|
width: 50, |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'device', |
||||
|
slots: { default: 'device' }, |
||||
|
title: $t('AbpIdentity.DisplayName:Device'), |
||||
|
width: 150, |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'signedIn', |
||||
|
minWidth: 200, |
||||
|
title: $t('AbpIdentity.DisplayName:SignedIn'), |
||||
|
}, |
||||
|
{ |
||||
|
field: 'action', |
||||
|
fixed: 'right', |
||||
|
slots: { default: 'action' }, |
||||
|
title: $t('AbpUi.Actions'), |
||||
|
width: 120, |
||||
|
}, |
||||
|
], |
||||
|
expandConfig: { |
||||
|
padding: true, |
||||
|
trigger: 'default', |
||||
|
}, |
||||
|
exportConfig: {}, |
||||
|
keepSource: true, |
||||
|
pagerConfig: { |
||||
|
autoHidden: true, |
||||
|
}, |
||||
|
toolbarConfig: {}, |
||||
|
}); |
||||
|
|
||||
|
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions }); |
||||
|
|
||||
|
watch( |
||||
|
() => props.sessions, |
||||
|
(sessions) => { |
||||
|
nextTick(() => { |
||||
|
gridApi.setGridOptions({ |
||||
|
data: sessions, |
||||
|
}); |
||||
|
}); |
||||
|
}, |
||||
|
{ |
||||
|
immediate: true, |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
function onDelete(session: IdentitySessionDto) { |
||||
|
emits('revoke', session); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Grid> |
||||
|
<template #device="{ row }"> |
||||
|
<div class="flex flex-row"> |
||||
|
<span>{{ row.device }}</span> |
||||
|
<div class="pl-[5px]"> |
||||
|
<Tag v-if="row.sessionId === getMySessionId" color="#87d068"> |
||||
|
{{ $t('AbpIdentity.CurrentSession') }} |
||||
|
</Tag> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<template #deviceInfo="{ row }"> |
||||
|
<Descriptions :colon="false" :column="2" bordered size="small"> |
||||
|
<DescriptionItem |
||||
|
:label="$t('AbpIdentity.DisplayName:SessionId')" |
||||
|
:span="2" |
||||
|
> |
||||
|
{{ row.sessionId }} |
||||
|
</DescriptionItem> |
||||
|
<DescriptionItem :label="$t('AbpIdentity.DisplayName:Device')"> |
||||
|
{{ row.device }} |
||||
|
</DescriptionItem> |
||||
|
<DescriptionItem :label="$t('AbpIdentity.DisplayName:DeviceInfo')"> |
||||
|
{{ row.deviceInfo }} |
||||
|
</DescriptionItem> |
||||
|
<DescriptionItem :label="$t('AbpIdentity.DisplayName:ClientId')"> |
||||
|
{{ row.clientId }} |
||||
|
</DescriptionItem> |
||||
|
<DescriptionItem :label="$t('AbpIdentity.DisplayName:IpAddresses')"> |
||||
|
{{ row.ipAddresses }} |
||||
|
</DescriptionItem> |
||||
|
<DescriptionItem :label="$t('AbpIdentity.DisplayName:SignedIn')"> |
||||
|
{{ row.signedIn }} |
||||
|
</DescriptionItem> |
||||
|
<DescriptionItem :label="$t('AbpIdentity.DisplayName:LastAccessed')"> |
||||
|
{{ row.lastAccessed }} |
||||
|
</DescriptionItem> |
||||
|
</Descriptions> |
||||
|
</template> |
||||
|
<template #action="{ row }"> |
||||
|
<div class="flex flex-row"> |
||||
|
<Button |
||||
|
v-if="getAllowRevokeSession(row)" |
||||
|
danger |
||||
|
size="small" |
||||
|
@click="onDelete(row)" |
||||
|
> |
||||
|
{{ $t('AbpIdentity.RevokeSession') }} |
||||
|
</Button> |
||||
|
</div> |
||||
|
</template> |
||||
|
</Grid> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -0,0 +1,67 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { IdentitySessionDto, IdentityUserDto } from '../../types'; |
||||
|
|
||||
|
import { defineAsyncComponent, ref } from 'vue'; |
||||
|
|
||||
|
import { useVbenDrawer } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { message, Modal } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useUserSessionsApi } from '../../api/useUserSessionsApi'; |
||||
|
|
||||
|
const UserSessionTable = defineAsyncComponent( |
||||
|
() => import('../sessions/UserSessionTable.vue'), |
||||
|
); |
||||
|
|
||||
|
const sessions = ref<IdentitySessionDto[]>([]); |
||||
|
|
||||
|
const { cancel, getSessionsApi, revokeSessionApi } = useUserSessionsApi(); |
||||
|
|
||||
|
const [Drawer, drawerApi] = useVbenDrawer({ |
||||
|
class: 'w-[800px]', |
||||
|
onBeforeClose: cancel, |
||||
|
onCancel() { |
||||
|
drawerApi.close(); |
||||
|
}, |
||||
|
onConfirm: async () => {}, |
||||
|
onOpenChange: async (isOpen: boolean) => { |
||||
|
isOpen && (await onRefresh()); |
||||
|
}, |
||||
|
title: $t('AbpIdentity.IdentitySessions'), |
||||
|
}); |
||||
|
async function onRefresh() { |
||||
|
try { |
||||
|
drawerApi.setState({ loading: true }); |
||||
|
const dto = drawerApi.getData<IdentityUserDto>(); |
||||
|
const { items } = await getSessionsApi({ userId: dto.id }); |
||||
|
sessions.value = items; |
||||
|
} finally { |
||||
|
drawerApi.setState({ loading: false }); |
||||
|
} |
||||
|
} |
||||
|
async function onRevoke(session: IdentitySessionDto) { |
||||
|
Modal.confirm({ |
||||
|
centered: true, |
||||
|
content: $t('AbpIdentity.SessionWillBeRevokedMessage'), |
||||
|
iconType: 'warning', |
||||
|
onCancel: () => { |
||||
|
cancel(); |
||||
|
}, |
||||
|
onOk: async () => { |
||||
|
await revokeSessionApi(session.sessionId); |
||||
|
message.success($t('AbpIdentity.SuccessfullyRevoked')); |
||||
|
await onRefresh(); |
||||
|
}, |
||||
|
title: $t('AbpUi.AreYouSure'), |
||||
|
}); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Drawer> |
||||
|
<UserSessionTable :sessions="sessions" @revoke="onRevoke" /> |
||||
|
</Drawer> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -0,0 +1,25 @@ |
|||||
|
import type { EntityDto, PagedAndSortedResultRequestDto } from '@abp/core'; |
||||
|
|
||||
|
interface IdentitySessionDto extends EntityDto<string> { |
||||
|
clientId?: string; |
||||
|
device: string; |
||||
|
deviceInfo: string; |
||||
|
ipAddresses?: string; |
||||
|
lastAccessed?: Date; |
||||
|
sessionId: string; |
||||
|
signedIn: Date; |
||||
|
userId: string; |
||||
|
} |
||||
|
|
||||
|
interface GetUserSessionsInput extends PagedAndSortedResultRequestDto { |
||||
|
clientId?: string; |
||||
|
device?: string; |
||||
|
userId?: string; |
||||
|
} |
||||
|
|
||||
|
interface GetMySessionsInput extends PagedAndSortedResultRequestDto { |
||||
|
clientId?: string; |
||||
|
device?: string; |
||||
|
} |
||||
|
|
||||
|
export type { GetMySessionsInput, GetUserSessionsInput, IdentitySessionDto }; |
||||
@ -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 './api'; |
||||
export * from './components'; |
export * from './components'; |
||||
export * from './constants'; |
export * from './constants'; |
||||
|
export * from './hooks'; |
||||
export * from './types'; |
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