Browse Source

Merge pull request #1087 from colinin/sessions

Sessions
pull/1091/head
yx lin 1 year ago
committed by GitHub
parent
commit
0d324a0d45
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      apps/vben5/apps/app-antd/package.json
  2. 48
      apps/vben5/apps/app-antd/src/hooks/useSessions.ts
  3. 3
      apps/vben5/apps/app-antd/src/layouts/basic.vue
  4. 6
      apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json
  5. 6
      apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json
  6. 9
      apps/vben5/apps/app-antd/src/router/routes/modules/abp.ts
  7. 7
      apps/vben5/apps/app-antd/src/store/auth.ts
  8. 15
      apps/vben5/apps/app-antd/src/views/identity/sessions/index.vue
  9. 12
      apps/vben5/apps/app-antd/vite.config.mts
  10. 1
      apps/vben5/packages/@abp/account/package.json
  11. 1
      apps/vben5/packages/@abp/account/src/api/index.ts
  12. 41
      apps/vben5/packages/@abp/account/src/api/useMySessionApi.ts
  13. 28
      apps/vben5/packages/@abp/account/src/components/MySetting.vue
  14. 48
      apps/vben5/packages/@abp/account/src/components/components/SessionSettings.vue
  15. 10
      apps/vben5/packages/@abp/core/src/constants/events.ts
  16. 1
      apps/vben5/packages/@abp/core/src/constants/index.ts
  17. 1
      apps/vben5/packages/@abp/core/src/hooks/index.ts
  18. 30
      apps/vben5/packages/@abp/core/src/hooks/useEventBus.ts
  19. 1
      apps/vben5/packages/@abp/core/src/utils/index.ts
  20. 107
      apps/vben5/packages/@abp/core/src/utils/mitt.ts
  21. 4
      apps/vben5/packages/@abp/identity/package.json
  22. 1
      apps/vben5/packages/@abp/identity/src/api/index.ts
  23. 42
      apps/vben5/packages/@abp/identity/src/api/useUserSessionsApi.ts
  24. 2
      apps/vben5/packages/@abp/identity/src/components/index.ts
  25. 243
      apps/vben5/packages/@abp/identity/src/components/sessions/SessionTable.vue
  26. 158
      apps/vben5/packages/@abp/identity/src/components/sessions/UserSessionTable.vue
  27. 67
      apps/vben5/packages/@abp/identity/src/components/users/UserSessionDrawer.vue
  28. 24
      apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue
  29. 6
      apps/vben5/packages/@abp/identity/src/constants/permissions.ts
  30. 1
      apps/vben5/packages/@abp/identity/src/types/index.ts
  31. 25
      apps/vben5/packages/@abp/identity/src/types/sessions.ts
  32. 1
      apps/vben5/packages/@abp/notifications/package.json
  33. 1
      apps/vben5/packages/@abp/notifications/src/constants/index.ts
  34. 4
      apps/vben5/packages/@abp/notifications/src/constants/notifications.ts
  35. 2
      apps/vben5/packages/@abp/notifications/src/hooks/index.ts
  36. 56
      apps/vben5/packages/@abp/notifications/src/hooks/useNotificationSerializer.ts
  37. 113
      apps/vben5/packages/@abp/notifications/src/hooks/useNotifications.ts
  38. 1
      apps/vben5/packages/@abp/notifications/src/index.ts
  39. 14
      apps/vben5/packages/@abp/notifications/src/types/notifications.ts
  40. 27
      apps/vben5/packages/@abp/signalr/package.json
  41. 1
      apps/vben5/packages/@abp/signalr/src/hooks/index.ts
  42. 155
      apps/vben5/packages/@abp/signalr/src/hooks/useSignalR.ts
  43. 1
      apps/vben5/packages/@abp/signalr/src/index.ts
  44. 6
      apps/vben5/packages/@abp/signalr/tsconfig.json
  45. 1
      apps/vben5/pnpm-workspace.yaml

1
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:*",

48
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);
}

3
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<NotificationItem[]>([
},
]);
useSessions();
const { replace } = useRouter();
const userStore = useUserStore();
const authStore = useAuthStore();

6
apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json

@ -19,7 +19,8 @@
"claimTypes": "Claim Types",
"securityLogs": "Security Logs",
"organizationUnits": "Organization Units",
"auditLogs": "Audit Logs"
"auditLogs": "Audit Logs",
"sessions": "Sessions"
},
"permissions": {
"title": "Permissions",
@ -59,7 +60,8 @@
"bindSettings": "Bind Settings",
"noticeSettings": "Notice Settings",
"authenticatorSettings": "Authenticator Settings",
"changeAvatar": "Change Avatar"
"changeAvatar": "Change Avatar",
"sessionSettings": "Session Settings"
},
"profile": "My Profile"
}

6
apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json

@ -19,7 +19,8 @@
"claimTypes": "身份标识",
"securityLogs": "安全日志",
"organizationUnits": "组织机构",
"auditLogs": "审计日志"
"auditLogs": "审计日志",
"sessions": "会话管理"
},
"permissions": {
"title": "权限管理",
@ -59,7 +60,8 @@
"bindSettings": "账号绑定",
"noticeSettings": "新消息通知",
"authenticatorSettings": "身份验证程序",
"changeAvatar": "更改头像"
"changeAvatar": "更改头像",
"sessionSettings": "会话管理"
},
"profile": "个人中心"
}

9
apps/vben5/apps/app-antd/src/router/routes/modules/abp.ts

@ -79,6 +79,15 @@ const routes: RouteRecordRaw[] = [
icon: 'clarity:organization-line',
},
},
{
component: () => import('#/views/identity/sessions/index.vue'),
name: 'IdentitySessions',
path: '/manage/identity/sessions',
meta: {
title: $t('abp.manage.identity.sessions'),
icon: 'carbon:prompt-session',
},
},
],
},
{

7
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,

15
apps/vben5/apps/app-antd/src/views/identity/sessions/index.vue

@ -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>

12
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,
},

1
apps/vben5/packages/@abp/account/package.json

@ -21,6 +21,7 @@
},
"dependencies": {
"@abp/core": "workspace:*",
"@abp/identity": "workspace:*",
"@abp/request": "workspace:*",
"@abp/ui": "workspace:*",
"@ant-design/icons-vue": "catalog:",

1
apps/vben5/packages/@abp/account/src/api/index.ts

@ -1,4 +1,5 @@
export { useAccountApi } from './useAccountApi';
export { useMySessionApi } from './useMySessionApi';
export { useProfileApi } from './useProfileApi';
export { useTokenApi } from './useTokenApi';
export { useUserInfoApi } from './useUserInfoApi';

41
apps/vben5/packages/@abp/account/src/api/useMySessionApi.ts

@ -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,
};
}

28
apps/vben5/packages/@abp/account/src/components/MySetting.vue

@ -12,12 +12,25 @@ import { useUserStore } from '@vben/stores';
import { Card, Menu, message, Modal } from 'ant-design-vue';
import { useProfileApi } from '../api/useProfileApi';
import AuthenticatorSettings from './components/AuthenticatorSettings.vue';
import BasicSettings from './components/BasicSettings.vue';
import BindSettings from './components/BindSettings.vue';
import NoticeSettings from './components/NoticeSettings.vue';
import SecuritySettings from './components/SecuritySettings.vue';
const AuthenticatorSettings = defineAsyncComponent(
() => import('./components/AuthenticatorSettings.vue'),
);
const BasicSettings = defineAsyncComponent(
() => import('./components/BasicSettings.vue'),
);
const BindSettings = defineAsyncComponent(
() => import('./components/BindSettings.vue'),
);
const NoticeSettings = defineAsyncComponent(
() => import('./components/NoticeSettings.vue'),
);
const SecuritySettings = defineAsyncComponent(
() => import('./components/SecuritySettings.vue'),
);
const SessionSettings = defineAsyncComponent(
() => import('./components/SessionSettings.vue'),
);
const { getApi, updateApi } = useProfileApi();
const userStore = useUserStore();
const { query } = useRoute();
@ -37,6 +50,10 @@ const menuItems = reactive([
key: 'bind',
label: $t('abp.account.settings.bindSettings'),
},
{
key: 'session',
label: $t('abp.account.settings.sessionSettings'),
},
{
key: 'notice',
label: $t('abp.account.settings.noticeSettings'),
@ -137,6 +154,7 @@ onMounted(async () => {
<AuthenticatorSettings
v-else-if="selectedMenuKeys[0] === 'authenticator'"
/>
<SessionSettings v-else-if="selectedMenuKeys[0] === 'session'" />
</div>
</div>
</Card>

48
apps/vben5/packages/@abp/account/src/components/components/SessionSettings.vue

@ -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>

10
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',
};

1
apps/vben5/packages/@abp/core/src/constants/index.ts

@ -1 +1,2 @@
export * from './events';
export * from './validation';

1
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';

30
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<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
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';

107
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<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]);
}
},
};
}

4
apps/vben5/packages/@abp/identity/package.json

@ -33,6 +33,10 @@
"@vben/layouts": "workspace:*",
"@vben/locales": "workspace:*",
"ant-design-vue": "catalog:",
"lodash.debounce": "catalog:",
"vue": "catalog:*"
},
"devDependencies": {
"@types/lodash.debounce": "catalog:"
}
}

1
apps/vben5/packages/@abp/identity/src/api/index.ts

@ -4,3 +4,4 @@ export { useRolesApi } from './useRolesApi';
export { useSecurityLogsApi } from './useSecurityLogsApi';
export { useUserLookupApi } from './useUserLookupApi';
export { useUsersApi } from './useUsersApi';
export { useUserSessionsApi } from './useUserSessionsApi';

42
apps/vben5/packages/@abp/identity/src/api/useUserSessionsApi.ts

@ -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,
};
}

2
apps/vben5/packages/@abp/identity/src/components/index.ts

@ -2,4 +2,6 @@ export { default as ClaimTypeTable } from './claim-types/ClaimTypeTable.vue';
export { default as OrganizationUnitPage } from './organization-units/OrganizationUnitPage.vue';
export { default as RoleTable } from './roles/RoleTable.vue';
export { default as SecurityLogTable } from './security-logs/SecurityLogTable.vue';
export { default as SessionTable } from './sessions/SessionTable.vue';
export { default as UserSessionTable } from './sessions/UserSessionTable.vue';
export { default as UserTable } from './users/UserTable.vue';

243
apps/vben5/packages/@abp/identity/src/components/sessions/SessionTable.vue

@ -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>

158
apps/vben5/packages/@abp/identity/src/components/sessions/UserSessionTable.vue

@ -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>

67
apps/vben5/packages/@abp/identity/src/components/users/UserSessionDrawer.vue

@ -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>

24
apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue

@ -26,7 +26,10 @@ import {
import { Button, Dropdown, Menu, message, Modal } from 'ant-design-vue';
import { useUsersApi } from '../../api/useUsersApi';
import { IdentityUserPermissions } from '../../constants/permissions';
import {
IdentitySessionPermissions,
IdentityUserPermissions,
} from '../../constants/permissions';
defineOptions({
name: 'UserTable',
@ -46,6 +49,7 @@ const PasswordIcon = createIconifyIcon('carbon:password');
const MenuOutlined = createIconifyIcon('heroicons-outline:menu-alt-3');
const ClaimOutlined = createIconifyIcon('la:id-card-solid');
const PermissionsOutlined = createIconifyIcon('icon-park-outline:permissions');
const SessionIcon = createIconifyIcon('carbon:prompt-session');
const AuditLogIcon = createIconifyIcon('fluent-mdl2:compliance-audit');
const getLockEnd = computed(() => {
@ -166,6 +170,11 @@ const [UserPermissionModal, permissionModalApi] = useVbenModal({
const [UserChangeDrawer, userChangeDrawerApi] = useVbenDrawer({
connectedComponent: EntityChangeDrawer,
});
const [UserSessionDrawer, userSessionDrawerApi] = useVbenDrawer({
connectedComponent: defineAsyncComponent(
() => import('./UserSessionDrawer.vue'),
),
});
const [Grid, { query }] = useVbenVxeGrid({
formOptions,
gridEvents,
@ -240,6 +249,11 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
permissionModalApi.open();
break;
}
case 'session': {
userSessionDrawerApi.setData(row);
userSessionDrawerApi.open();
break;
}
case 'unlock': {
handleUnlock(row);
break;
@ -330,6 +344,13 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
>
{{ $t('AbpPermissionManagement.Permissions') }}
</MenuItem>
<MenuItem
v-if="hasAccessByCodes([IdentitySessionPermissions.Default])"
key="session"
:icon="h(SessionIcon)"
>
{{ $t('AbpIdentity.IdentitySessions') }}
</MenuItem>
<MenuItem
v-if="
hasAccessByCodes([IdentityUserPermissions.ManageClaims])
@ -376,6 +397,7 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
<UserEditModal @change="() => query()" />
<UserPasswordModal @change="query" />
<UserPermissionModal />
<UserSessionDrawer />
<UserChangeDrawer />
</template>

6
apps/vben5/packages/@abp/identity/src/constants/permissions.ts

@ -46,6 +46,12 @@ export const SecurityLogPermissions = {
/** 删除 */
Delete: 'AbpAuditing.SecurityLog.Delete',
};
/** 用户会话权限 */
export const IdentitySessionPermissions = {
Default: 'AbpIdentity.IdentitySessions',
/** 移除 */
Revoke: 'AbpIdentity.IdentitySessions.Revoke',
};
/**
*
* @deprecated 使.

1
apps/vben5/packages/@abp/identity/src/types/index.ts

@ -2,4 +2,5 @@ export * from './claim-types';
export * from './organization-units';
export * from './roles';
export * from './security-logs';
export * from './sessions';
export * from './users';

25
apps/vben5/packages/@abp/identity/src/types/sessions.ts

@ -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 };

1
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:*",

1
apps/vben5/packages/@abp/notifications/src/constants/index.ts

@ -0,0 +1 @@
export * from './notifications';

4
apps/vben5/packages/@abp/notifications/src/constants/notifications.ts

@ -0,0 +1,4 @@
export const NotificationNames = {
/** 会话过期通知 */
SessionExpiration: 'AbpIdentity.Session.Expiration',
};

2
apps/vben5/packages/@abp/notifications/src/hooks/index.ts

@ -0,0 +1,2 @@
export * from './useNotifications';
export * from './useNotificationSerializer';

56
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,
};
}

113
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,
};
}

1
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';

14
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<string, any>;
description?: string;
lifetime: NotificationLifetime;
message: string;
name: string;
severity: NotificationSeverity;
title: string;
type: NotificationType;
}
export type {
Notification,
NotificationData,
NotificationDto,
NotificationGroupDto,

27
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:*"
}
}

1
apps/vben5/packages/@abp/signalr/src/hooks/index.ts

@ -0,0 +1 @@
export * from './useSignalR';

155
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<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,
};
}

1
apps/vben5/packages/@abp/signalr/src/index.ts

@ -0,0 +1 @@
export * from './hooks';

6
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"]
}

1
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

Loading…
Cancel
Save