Browse Source

feat: add saas package.

pull/1115/head
colin 1 year ago
parent
commit
1980346393
  1. 1
      apps/vben5/apps/app-antd/package.json
  2. 5
      apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json
  3. 5
      apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json
  4. 28
      apps/vben5/apps/app-antd/src/router/routes/modules/abp.ts
  5. 15
      apps/vben5/apps/app-antd/src/views/saas/editions/index.vue
  6. 15
      apps/vben5/apps/app-antd/src/views/saas/tenants/index.vue
  7. 55
      apps/vben5/packages/@abp/core/src/types/dto.ts
  8. 8
      apps/vben5/packages/@abp/identity/src/types/users.ts
  9. 2
      apps/vben5/packages/@abp/permissions/src/components/definitions/permissions/PermissionDefinitionTable.vue
  10. 10
      apps/vben5/packages/@abp/request/src/hooks/useWrapperResult.ts
  11. 45
      apps/vben5/packages/@abp/saas/package.json
  12. 2
      apps/vben5/packages/@abp/saas/src/api/index.ts
  13. 84
      apps/vben5/packages/@abp/saas/src/api/useEditionsApi.ts
  14. 154
      apps/vben5/packages/@abp/saas/src/api/useTenantsApi.ts
  15. 102
      apps/vben5/packages/@abp/saas/src/components/editions/EditionModal.vue
  16. 215
      apps/vben5/packages/@abp/saas/src/components/editions/EditionTable.vue
  17. 2
      apps/vben5/packages/@abp/saas/src/components/index.ts
  18. 76
      apps/vben5/packages/@abp/saas/src/components/tenants/ConnectionStringModal.vue
  19. 114
      apps/vben5/packages/@abp/saas/src/components/tenants/ConnectionStringTable.vue
  20. 71
      apps/vben5/packages/@abp/saas/src/components/tenants/ConnectionStringsModal.vue
  21. 305
      apps/vben5/packages/@abp/saas/src/components/tenants/TenantModal.vue
  22. 258
      apps/vben5/packages/@abp/saas/src/components/tenants/TenantTable.vue
  23. 1
      apps/vben5/packages/@abp/saas/src/constants/index.ts
  24. 28
      apps/vben5/packages/@abp/saas/src/constants/permissions.ts
  25. 4
      apps/vben5/packages/@abp/saas/src/index.ts
  26. 34
      apps/vben5/packages/@abp/saas/src/types/editions.ts
  27. 2
      apps/vben5/packages/@abp/saas/src/types/index.ts
  28. 68
      apps/vben5/packages/@abp/saas/src/types/tenants.ts
  29. 6
      apps/vben5/packages/@abp/saas/tsconfig.json

1
apps/vben5/apps/app-antd/package.json

@ -35,6 +35,7 @@
"@abp/permissions": "workspace:*", "@abp/permissions": "workspace:*",
"@abp/platform": "workspace:*", "@abp/platform": "workspace:*",
"@abp/request": "workspace:*", "@abp/request": "workspace:*",
"@abp/saas": "workspace:*",
"@abp/settings": "workspace:*", "@abp/settings": "workspace:*",
"@abp/ui": "workspace:*", "@abp/ui": "workspace:*",
"@vben/access": "workspace:*", "@vben/access": "workspace:*",

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

@ -76,5 +76,10 @@
"email": "Email Messages", "email": "Email Messages",
"sms": "Sms Messages" "sms": "Sms Messages"
} }
},
"saas": {
"title": "Saas",
"editions": "Editions",
"tenants": "Tenants"
} }
} }

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

@ -76,5 +76,10 @@
"email": "邮件消息", "email": "邮件消息",
"sms": "短信消息" "sms": "短信消息"
} }
},
"saas": {
"title": "Saas",
"editions": "版本管理",
"tenants": "租户管理"
} }
} }

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

@ -177,6 +177,34 @@ const routes: RouteRecordRaw[] = [
}, },
], ],
}, },
{
meta: {
title: $t('abp.saas.title'),
icon: 'ant-design:cloud-server-outlined',
},
name: 'Saas',
path: '/saas',
children: [
{
meta: {
title: $t('abp.saas.editions'),
icon: 'icon-park-outline:multi-rectangle',
},
name: 'SaasEditions',
path: '/saas/editions',
component: () => import('#/views/saas/editions/index.vue'),
},
{
meta: {
title: $t('abp.saas.tenants'),
icon: 'arcticons:tenantcloud-pro',
},
name: 'SaasTenants',
path: '/saas/tenants',
component: () => import('#/views/saas/tenants/index.vue'),
},
],
},
{ {
meta: { meta: {
title: $t('abp.openiddict.title'), title: $t('abp.openiddict.title'),

15
apps/vben5/apps/app-antd/src/views/saas/editions/index.vue

@ -0,0 +1,15 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { EditionTable } from '@abp/saas';
defineOptions({
name: 'SaasEditions',
});
</script>
<template>
<Page>
<EditionTable />
</Page>
</template>

15
apps/vben5/apps/app-antd/src/views/saas/tenants/index.vue

@ -0,0 +1,15 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { TenantTable } from '@abp/saas';
defineOptions({
name: 'SaasTenants',
});
</script>
<template>
<Page>
<TenantTable />
</Page>
</template>

55
apps/vben5/packages/@abp/core/src/types/dto.ts

@ -1,15 +1,15 @@
import { import type {
type Clock, Clock,
type CurrentCulture, CurrentCulture,
type CurrentTenant, CurrentTenant,
type CurrentUser, CurrentUser,
type Dictionary, Dictionary,
type ExtraPropertyDictionary, ExtraPropertyDictionary,
type IHasExtraProperties, IHasExtraProperties,
type LanguageInfo, LanguageInfo,
type MultiTenancyInfo, MultiTenancyInfo,
type NameValue, NameValue,
type TimeZone, TimeZone,
} from './global'; } from './global';
/** 包装器数据传输对象 */ /** 包装器数据传输对象 */
interface WrapResult<T> { interface WrapResult<T> {
@ -47,6 +47,7 @@ interface RemoteServiceErrorInfo {
} }
/** 扩展属性数据传输对象 */ /** 扩展属性数据传输对象 */
interface ExtensibleObject { interface ExtensibleObject {
[key: string]: any;
/** 扩展属性 */ /** 扩展属性 */
extraProperties: ExtraPropertyDictionary; extraProperties: ExtraPropertyDictionary;
} }
@ -102,7 +103,7 @@ interface FullAuditedEntityWithUserDto<TPrimaryKey, TUserDto>
deleter: TUserDto; deleter: TUserDto;
} }
/** 实体扩展属性数据传输对象 */ /** 实体扩展属性数据传输对象 */
interface ExtensibleEntityDto<TKey> extends ExtensibleObject, EntityDto<TKey> {} interface ExtensibleEntityDto<TKey> extends EntityDto<TKey>, ExtensibleObject {}
/** 实体新增扩展属性数据传输对象 */ /** 实体新增扩展属性数据传输对象 */
interface ExtensibleCreationAuditedEntityDto<TPrimaryKey> interface ExtensibleCreationAuditedEntityDto<TPrimaryKey>
extends CreationAuditedEntityDto<TPrimaryKey>, extends CreationAuditedEntityDto<TPrimaryKey>,
@ -121,12 +122,12 @@ interface ExtensibleAuditedEntityWithUserDto<TPrimaryKey, TUserDto>
ExtensibleEntityDto<TPrimaryKey> {} ExtensibleEntityDto<TPrimaryKey> {}
/** 实体审计全属性扩展数据传输对象 */ /** 实体审计全属性扩展数据传输对象 */
interface ExtensibleFullAuditedEntityDto<TPrimaryKey> interface ExtensibleFullAuditedEntityDto<TPrimaryKey>
extends FullAuditedEntityDto<TPrimaryKey>, extends ExtensibleEntityDto<TPrimaryKey>,
ExtensibleEntityDto<TPrimaryKey> {} FullAuditedEntityDto<TPrimaryKey> {}
/** 实体审计用户全属性扩展数据传输对象 */ /** 实体审计用户全属性扩展数据传输对象 */
interface ExtensibleFullAuditedEntityWithUserDto<TPrimaryKey, TUserDto> interface ExtensibleFullAuditedEntityWithUserDto<TPrimaryKey, TUserDto>
extends FullAuditedEntityWithUserDto<TPrimaryKey, TUserDto>, extends ExtensibleEntityDto<TPrimaryKey>,
ExtensibleEntityDto<TPrimaryKey> {} FullAuditedEntityWithUserDto<TPrimaryKey, TUserDto> {}
/** 最大请求数据传输对象 */ /** 最大请求数据传输对象 */
interface LimitedResultRequestDto { interface LimitedResultRequestDto {
/** 最大返回数据大小 */ /** 最大返回数据大小 */
@ -134,8 +135,8 @@ interface LimitedResultRequestDto {
} }
/** 最大请求扩展数据传输对象 */ /** 最大请求扩展数据传输对象 */
interface ExtensibleLimitedResultRequestDto interface ExtensibleLimitedResultRequestDto
extends LimitedResultRequestDto, extends ExtensibleObject,
ExtensibleObject {} LimitedResultRequestDto {}
/** 排序请求数据传输对象 */ /** 排序请求数据传输对象 */
interface SortedResultRequest { interface SortedResultRequest {
/** /**
@ -160,8 +161,8 @@ interface PagedAndSortedResultRequestDto
SortedResultRequest {} SortedResultRequest {}
/** 分页排序请求扩展数据传输对象 */ /** 分页排序请求扩展数据传输对象 */
interface ExtensiblePagedAndSortedResultRequestDto interface ExtensiblePagedAndSortedResultRequestDto
extends PagedAndSortedResultRequestDto, extends ExtensibleObject,
ExtensibleObject {} PagedAndSortedResultRequestDto {}
/** 列表数据传输对象 */ /** 列表数据传输对象 */
interface ListResultDto<T> { interface ListResultDto<T> {
/** 返回项目列表 */ /** 返回项目列表 */
@ -169,8 +170,8 @@ interface ListResultDto<T> {
} }
/** 列表扩展数据传输对象 */ /** 列表扩展数据传输对象 */
interface ExtensibleListResultDto<T> interface ExtensibleListResultDto<T>
extends ListResultDto<T>, extends ExtensibleObject,
ExtensibleObject {} ListResultDto<T> {}
/** 分页列表数据传输对象 */ /** 分页列表数据传输对象 */
interface PagedResultDto<T> extends ListResultDto<T> { interface PagedResultDto<T> extends ListResultDto<T> {
/** 符合条件的最大数量 */ /** 符合条件的最大数量 */
@ -178,12 +179,12 @@ interface PagedResultDto<T> extends ListResultDto<T> {
} }
/** 分页列表扩展数据传输对象 */ /** 分页列表扩展数据传输对象 */
interface ExtensiblePagedResultDto<T> interface ExtensiblePagedResultDto<T>
extends PagedResultDto<T>, extends ExtensibleObject,
ExtensibleObject {} PagedResultDto<T> {}
/** 分页列表扩展数据传输对象 */ /** 分页列表扩展数据传输对象 */
interface ExtensiblePagedResultRequestDto interface ExtensiblePagedResultRequestDto
extends PagedResultRequestDto, extends ExtensibleObject,
ExtensibleObject {} PagedResultRequestDto {}
/** 应用程序本地化资源数据传输对象 */ /** 应用程序本地化资源数据传输对象 */
interface ApplicationLocalizationResourceDto { interface ApplicationLocalizationResourceDto {
/** 继承资源名称列表 */ /** 继承资源名称列表 */

8
apps/vben5/packages/@abp/identity/src/types/users.ts

@ -45,9 +45,9 @@ interface IdentityUserOrganizationUnitUpdateDto {
/** 用户实体数据传输对象 */ /** 用户实体数据传输对象 */
interface IdentityUserDto interface IdentityUserDto
extends FullAuditedEntityDto<string>, extends FullAuditedEntityDto<string>,
IUser,
IHasConcurrencyStamp, IHasConcurrencyStamp,
IHasExtraProperties { IHasExtraProperties,
IUser {
[key: string]: any; [key: string]: any;
/** 邮箱已验证 */ /** 邮箱已验证 */
emailConfirmed: boolean; emailConfirmed: boolean;
@ -95,8 +95,8 @@ interface UserLookupCountInput {
} }
interface UserLookupSearchInput interface UserLookupSearchInput
extends UserLookupCountInput, extends PagedAndSortedResultRequestDto,
PagedAndSortedResultRequestDto {} UserLookupCountInput {}
export type { export type {
ChangeMyPasswordInput, ChangeMyPasswordInput,

2
apps/vben5/packages/@abp/permissions/src/components/definitions/permissions/PermissionDefinitionTable.vue

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VbenFormProps, VxeGridListeners, VxeGridProps } from '@abp/ui'; import type { VbenFormProps, VxeGridListeners, VxeGridProps } from '@abp/ui';
import type { PermissionDefinitionDto } from '../../../types/definitions';
import type { PermissionGroupDefinitionDto } from '../../../types/groups'; import type { PermissionGroupDefinitionDto } from '../../../types/groups';
import { defineAsyncComponent, h, onMounted, reactive, ref } from 'vue'; import { defineAsyncComponent, h, onMounted, reactive, ref } from 'vue';
@ -25,7 +26,6 @@ import { VxeGrid } from 'vxe-table';
import { usePermissionDefinitionsApi } from '../../../api/usePermissionDefinitionsApi'; import { usePermissionDefinitionsApi } from '../../../api/usePermissionDefinitionsApi';
import { usePermissionGroupDefinitionsApi } from '../../../api/usePermissionGroupDefinitionsApi'; import { usePermissionGroupDefinitionsApi } from '../../../api/usePermissionGroupDefinitionsApi';
import { GroupDefinitionsPermissions } from '../../../constants/permissions'; import { GroupDefinitionsPermissions } from '../../../constants/permissions';
import { type PermissionDefinitionDto } from '../../../types/definitions';
import { useTypesMap } from './types'; import { useTypesMap } from './types';
defineOptions({ defineOptions({

10
apps/vben5/packages/@abp/request/src/hooks/useWrapperResult.ts

@ -24,7 +24,15 @@ export function useWrapperResult(response: AxiosResponse) {
const hasSuccess = data && Reflect.has(data, 'code') && code === '0'; const hasSuccess = data && Reflect.has(data, 'code') && code === '0';
if (!hasSuccess) { if (!hasSuccess) {
const content = details || message; const content = details || message;
throw Object.assign({}, response, { message: content, response }); throw Object.assign({}, response, {
response: {
...response,
data: {
...response.data,
message: content,
},
},
});
} }
} }

45
apps/vben5/packages/@abp/saas/package.json

@ -0,0 +1,45 @@
{
"name": "@abp/saas",
"version": "9.0.4",
"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/saas"
},
"license": "MIT",
"type": "module",
"sideEffects": [
"**/*.css"
],
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"dependencies": {
"@abp/auditing": "workspace:*",
"@abp/components": "workspace:*",
"@abp/core": "workspace:*",
"@abp/request": "workspace:*",
"@abp/signalr": "workspace:*",
"@abp/ui": "workspace:*",
"@ant-design/icons-vue": "catalog:",
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*",
"@vben/layouts": "workspace:*",
"@vben/locales": "workspace:*",
"ant-design-vue": "catalog:",
"dayjs": "catalog:",
"lodash.debounce": "catalog:",
"vue": "catalog:*",
"vxe-table": "catalog:"
},
"devDependencies": {
"@types/lodash.debounce": "catalog:"
}
}

2
apps/vben5/packages/@abp/saas/src/api/index.ts

@ -0,0 +1,2 @@
export * from './useEditionsApi';
export * from './useTenantsApi';

84
apps/vben5/packages/@abp/saas/src/api/useEditionsApi.ts

@ -0,0 +1,84 @@
import type { PagedResultDto } from '@abp/core';
import type {
EditionCreateDto,
EditionDto,
EditionUpdateDto,
GetEditionPagedListInput,
} from '../types';
import { useRequest } from '@abp/request';
export function useEditionsApi() {
const { cancel, request } = useRequest();
/**
*
* @param {EditionCreateDto} input
* @returns
*/
function createApi(input: EditionCreateDto): Promise<EditionDto> {
return request<EditionDto>('/api/saas/editions', {
data: input,
method: 'POST',
});
}
/**
*
* @param {string} id
* @param {EditionUpdateDto} input
* @returns
*/
function updateApi(id: string, input: EditionUpdateDto): Promise<EditionDto> {
return request<EditionDto>(`/api/saas/editions/${id}`, {
data: input,
method: 'PUT',
});
}
/**
*
* @param {string} id Id
* @returns
*/
function getApi(id: string): Promise<EditionDto> {
return request<EditionDto>(`/api/saas/editions/${id}`, {
method: 'GET',
});
}
/**
*
* @param {string} id Id
* @returns {void}
*/
function deleteApi(id: string): Promise<void> {
return request(`/api/saas/editions/${id}`, {
method: 'DELETE',
});
}
/**
*
* @param {GetEditionPagedListInput} input
* @returns {void}
*/
function getPagedListApi(
input?: GetEditionPagedListInput,
): Promise<PagedResultDto<EditionDto>> {
return request<PagedResultDto<EditionDto>>(`/api/saas/editions`, {
method: 'GET',
params: input,
});
}
return {
cancel,
createApi,
deleteApi,
getApi,
getPagedListApi,
updateApi,
};
}

154
apps/vben5/packages/@abp/saas/src/api/useTenantsApi.ts

@ -0,0 +1,154 @@
import type { ListResultDto, PagedResultDto } from '@abp/core';
import type {
GetTenantPagedListInput,
TenantConnectionStringDto,
TenantConnectionStringSetInput,
TenantCreateDto,
TenantDto,
TenantUpdateDto,
} from '../types';
import { useRequest } from '@abp/request';
export function useTenantsApi() {
const { cancel, request } = useRequest();
/**
*
* @param {TenantCreateDto} input
* @returns
*/
function createApi(input: TenantCreateDto): Promise<TenantDto> {
return request<TenantDto>('/api/saas/tenants', {
data: input,
method: 'POST',
});
}
/**
*
* @param {string} id
* @param {TenantUpdateDto} input
* @returns
*/
function updateApi(id: string, input: TenantUpdateDto): Promise<TenantDto> {
return request<TenantDto>(`/api/saas/tenants/${id}`, {
data: input,
method: 'PUT',
});
}
/**
*
* @param {string} id Id
* @returns
*/
function getApi(id: string): Promise<TenantDto> {
return request<TenantDto>(`/api/saas/tenants/${id}`, {
method: 'GET',
});
}
/**
*
* @param {string} id Id
* @returns {void}
*/
function deleteApi(id: string): Promise<void> {
return request(`/api/saas/tenants/${id}`, {
method: 'DELETE',
});
}
/**
*
* @param {GetTenantPagedListInput} input
* @returns {void}
*/
function getPagedListApi(
input?: GetTenantPagedListInput,
): Promise<PagedResultDto<TenantDto>> {
return request<PagedResultDto<TenantDto>>(`/api/saas/tenants`, {
method: 'GET',
params: input,
});
}
/**
*
* @param {string} id Id
* @param {TenantConnectionStringSetInput} input
* @returns
*/
function setConnectionStringApi(
id: string,
input: TenantConnectionStringSetInput,
): Promise<TenantConnectionStringDto> {
return request<TenantConnectionStringDto>(
`/api/saas/tenants/${id}/connection-string`,
{
data: input,
method: 'PUT',
},
);
}
/**
*
* @param {string} id Id
* @param {string} name
* @returns
*/
function getConnectionStringApi(
id: string,
name: string,
): Promise<TenantConnectionStringDto> {
return request<TenantConnectionStringDto>(
`/api/saas/tenants/${id}/connection-string/${name}`,
{
method: 'GET',
},
);
}
/**
*
* @param {string} id Id
* @returns
*/
function getConnectionStringsApi(
id: string,
): Promise<ListResultDto<TenantConnectionStringDto>> {
return request<ListResultDto<TenantConnectionStringDto>>(
`/api/saas/tenants/${id}/connection-string`,
{
method: 'GET',
},
);
}
/**
*
* @param {string} id Id
* @param {string} name
* @returns {void}
*/
function deleteConnectionStringApi(id: string, name: string): Promise<void> {
return request(`/api/saas/tenants/${id}/connection-string/${name}`, {
method: 'DELETE',
});
}
return {
cancel,
createApi,
deleteApi,
deleteConnectionStringApi,
getApi,
getConnectionStringApi,
getConnectionStringsApi,
getPagedListApi,
setConnectionStringApi,
updateApi,
};
}

102
apps/vben5/packages/@abp/saas/src/components/editions/EditionModal.vue

@ -0,0 +1,102 @@
<script setup lang="ts">
import type { EditionDto } from '../../types';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { useVbenForm } from '@abp/ui';
import { message } from 'ant-design-vue';
import { useEditionsApi } from '../../api';
const emits = defineEmits<{
(event: 'change', val: EditionDto): void;
}>();
const edition = ref<EditionDto>();
const { cancel, createApi, getApi, updateApi } = useEditionsApi();
const [Form, formApi] = useVbenForm({
async handleSubmit(values) {
await onSubmit(values);
},
schema: [
{
component: 'Input',
componentProps: {
allowClear: true,
autocomplete: 'off',
},
fieldName: 'displayName',
label: $t('AbpSaas.DisplayName:EditionName'),
rules: 'required',
},
],
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
onClosed: cancel,
async onConfirm() {
await formApi.validateAndSubmitForm();
},
async onOpenChange(isOpen) {
if (isOpen) {
await onGet();
}
},
title: $t('AbpSaas.Editions'),
});
async function onGet() {
const { id } = modalApi.getData<EditionDto>();
if (!id) {
formApi.setValues({});
edition.value = undefined;
modalApi.setState({ title: $t('AbpSaas.NewEdition') });
return;
}
try {
modalApi.setState({ loading: true });
const editionDto = await getApi(id);
modalApi.setState({
title: `${$t('AbpSaas.Editions')} - ${editionDto.displayName}`,
});
formApi.setValues(editionDto);
edition.value = editionDto;
} finally {
modalApi.setState({ loading: false });
}
}
async function onSubmit(values: Record<string, any>) {
const api = edition.value?.id
? updateApi(edition.value!.id, {
concurrencyStamp: values.concurrencyStamp,
displayName: values.displayName,
})
: createApi({
displayName: values.displayName,
});
try {
modalApi.setState({ submitting: true });
const dto = await api;
message.success($t('AbpUi.SavedSuccessfully'));
emits('change', dto);
modalApi.close();
} finally {
modalApi.setState({ submitting: false });
}
}
</script>
<template>
<Modal>
<Form />
</Modal>
</template>
<style scoped></style>

215
apps/vben5/packages/@abp/saas/src/components/editions/EditionTable.vue

@ -0,0 +1,215 @@
<script setup lang="ts">
import type { VbenFormProps, VxeGridListeners, VxeGridProps } from '@abp/ui';
import type { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import type { EditionDto } from '../../types/editions';
import { defineAsyncComponent, h } from 'vue';
import { useAccess } from '@vben/access';
import { useVbenDrawer, useVbenModal } from '@vben/common-ui';
import { createIconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { AuditLogPermissions, EntityChangeDrawer } from '@abp/auditing';
import { useFeatures } from '@abp/core';
import { useVbenVxeGrid } from '@abp/ui';
import {
DeleteOutlined,
EditOutlined,
EllipsisOutlined,
} from '@ant-design/icons-vue';
import { Button, Dropdown, Menu, message, Modal } from 'ant-design-vue';
import { useEditionsApi } from '../../api/useEditionsApi';
import { EditionsPermissions } from '../../constants/permissions';
defineOptions({
name: 'EditionTable',
});
const MenuItem = Menu.Item;
const AuditLogIcon = createIconifyIcon('fluent-mdl2:compliance-audit');
const { isEnabled } = useFeatures();
const { hasAccessByCodes } = useAccess();
const { cancel, deleteApi, getPagedListApi } = useEditionsApi();
const formOptions: VbenFormProps = {
//
collapsed: false,
schema: [
{
component: 'Input',
componentProps: {
allowClear: true,
autocomplete: 'off',
},
fieldName: 'filter',
formItemClass: 'col-span-2 items-baseline',
label: $t('AbpUi.Search'),
},
],
//
showCollapseButton: true,
//
submitOnEnter: true,
};
const gridOptions: VxeGridProps<EditionDto> = {
columns: [
{
align: 'left',
field: 'displayName',
title: $t('AbpSaas.DisplayName:EditionName'),
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: $t('AbpUi.Actions'),
visible: hasAccessByCodes([
EditionsPermissions.Update,
EditionsPermissions.Delete,
]),
width: 220,
},
],
exportConfig: {},
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getPagedListApi({
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<EditionDto> = {
cellClick: () => {},
};
const [EditionModal, modalApi] = useVbenModal({
connectedComponent: defineAsyncComponent(() => import('./EditionModal.vue')),
});
const [Grid, { query }] = useVbenVxeGrid({
formOptions,
gridEvents,
gridOptions,
});
const [EditionChangeDrawer, entityChangeDrawerApi] = useVbenDrawer({
connectedComponent: EntityChangeDrawer,
});
const onCreate = () => {
modalApi.setData({});
modalApi.open();
};
const onUpdate = (row: EditionDto) => {
modalApi.setData(row);
modalApi.open();
};
const onDelete = (row: EditionDto) => {
Modal.confirm({
centered: true,
content: $t('AbpSaas.EditionDeletionConfirmationMessage', [
row.displayName,
]),
onCancel: () => {
cancel();
},
onOk: async () => {
await deleteApi(row.id);
message.success($t('AbpUi.SuccessfullyDeleted'));
query();
},
title: $t('AbpUi.AreYouSure'),
});
};
const onMenuClick = (row: EditionDto, info: MenuInfo) => {
switch (info.key) {
case 'entity-changes': {
entityChangeDrawerApi.setData({
entityId: row.id,
entityTypeFullName: 'LINGYUN.Abp.Saas.Edition',
subject: row.displayName,
});
entityChangeDrawerApi.open();
break;
}
}
};
</script>
<template>
<Grid :table-title="$t('AbpSaas.Editions')">
<template #toolbar-tools>
<Button
type="primary"
v-access:code="[EditionsPermissions.Create]"
@click="onCreate"
>
{{ $t('AbpSaas.NewEdition') }}
</Button>
</template>
<template #action="{ row }">
<div class="flex flex-row">
<Button
:icon="h(EditOutlined)"
block
type="link"
v-access:code="[EditionsPermissions.Update]"
@click="onUpdate(row)"
>
{{ $t('AbpUi.Edit') }}
</Button>
<Button
:icon="h(DeleteOutlined)"
block
danger
type="link"
v-access:code="[EditionsPermissions.Delete]"
@click="onDelete(row)"
>
{{ $t('AbpUi.Delete') }}
</Button>
<Dropdown v-if="isEnabled('AbpAuditing.Logging.AuditLog')">
<template #overlay>
<Menu @click="(info) => onMenuClick(row, info)">
<MenuItem
v-if="hasAccessByCodes([AuditLogPermissions.Default])"
key="entity-changes"
:icon="h(AuditLogIcon)"
>
{{ $t('AbpAuditLogging.EntitiesChanged') }}
</MenuItem>
</Menu>
</template>
<Button :icon="h(EllipsisOutlined)" type="link" />
</Dropdown>
</div>
</template>
</Grid>
<EditionModal @change="() => query()" />
<EditionChangeDrawer />
</template>
<style lang="scss" scoped></style>

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

@ -0,0 +1,2 @@
export { default as EditionTable } from './editions/EditionTable.vue';
export { default as TenantTable } from './tenants/TenantTable.vue';

76
apps/vben5/packages/@abp/saas/src/components/tenants/ConnectionStringModal.vue

@ -0,0 +1,76 @@
<script setup lang="ts">
import type { FormExpose } from 'ant-design-vue/es/form/Form';
import type { TenantConnectionStringDto } from '../../types';
import { ref, toValue, useTemplateRef } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { Form, Input, Textarea } from 'ant-design-vue';
const props = defineProps<{
submit?: (data: TenantConnectionStringDto) => Promise<void>;
}>();
const FormItem = Form.Item;
const isEditModal = ref(false);
const form = useTemplateRef<FormExpose>('form');
const formModel = ref({} as TenantConnectionStringDto);
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
await form.value?.validate();
onSubmit();
},
onOpenChange(isOpen) {
isEditModal.value = false;
let title = $t('AbpSaas.ConnectionStrings');
if (isOpen) {
form.value?.resetFields();
const dto = modalApi.getData<TenantConnectionStringDto>();
formModel.value = { ...dto };
if (dto.name) {
isEditModal.value = true;
title = `${$t('AbpSaas.ConnectionStrings')} - ${dto.name}`;
}
}
modalApi.setState({ title });
},
title: $t('AbpSaas.ConnectionStrings'),
});
async function onSubmit() {
modalApi.setState({ submitting: true });
try {
props.submit && (await props.submit(toValue(formModel)));
modalApi.close();
} finally {
modalApi.setState({ submitting: false });
}
}
</script>
<template>
<Modal>
<Form
ref="form"
:label-col="{ span: 4 }"
:wapper-col="{ span: 20 }"
:model="formModel"
>
<FormItem required name="name" :label="$t('AbpSaas.DisplayName:Name')">
<Input
:disabled="isEditModal"
autocomplete="off"
v-model:value="formModel.name"
/>
</FormItem>
<FormItem required name="value" :label="$t('AbpSaas.DisplayName:Value')">
<Textarea :auto-size="{ minRows: 3 }" v-model:value="formModel.value" />
</FormItem>
</Form>
</Modal>
</template>
<style scoped></style>

114
apps/vben5/packages/@abp/saas/src/components/tenants/ConnectionStringTable.vue

@ -0,0 +1,114 @@
<script setup lang="ts">
import type { VxeGridListeners, VxeGridProps } from '@abp/ui';
import type { TenantConnectionStringDto } from '../../types/tenants';
import { defineAsyncComponent, h } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
import { Button, Popconfirm } from 'ant-design-vue';
import { VxeGrid } from 'vxe-table';
const props = defineProps<{
connectionStrings: TenantConnectionStringDto[];
delete?: (data: TenantConnectionStringDto) => Promise<void>;
submit?: (data: TenantConnectionStringDto) => Promise<void>;
}>();
const [ConnectionStringModal, connectionModalApi] = useVbenModal({
connectedComponent: defineAsyncComponent(
() => import('./ConnectionStringModal.vue'),
),
});
const gridOptions: VxeGridProps<TenantConnectionStringDto> = {
columns: [
{
align: 'left',
field: 'name',
title: $t('AbpSaas.DisplayName:Name'),
width: 150,
},
{
align: 'left',
field: 'value',
title: $t('AbpSaas.DisplayName:Value'),
},
{
align: 'center',
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: $t('AbpUi.Actions'),
width: 180,
},
],
exportConfig: {},
keepSource: true,
toolbarConfig: {
buttons: [
{
code: 'add',
icon: 'vxe-icon-add',
name: $t('AbpSaas.ConnectionStrings:AddNew'),
status: 'primary',
},
],
},
};
const gridEvents: VxeGridListeners<TenantConnectionStringDto> = {
toolbarButtonClick(params) {
if (params.code === 'add') {
handleCreate();
}
},
};
function handleCreate() {
connectionModalApi.setData({});
connectionModalApi.open();
}
function handleUpdate(row: TenantConnectionStringDto) {
connectionModalApi.setData(row);
connectionModalApi.open();
}
async function onDelete(row: TenantConnectionStringDto) {
props.delete && (await props.delete(row));
}
</script>
<template>
<div>
<VxeGrid v-bind="gridOptions" v-on="gridEvents" :data="connectionStrings">
<template #action="{ row }">
<div class="flex flex-row">
<Button
:icon="h(EditOutlined)"
block
type="link"
@click="handleUpdate(row)"
>
{{ $t('AbpUi.Edit') }}
</Button>
<Popconfirm
:title="$t('AbpUi.AreYouSure')"
trigger="click"
@confirm="onDelete(row)"
>
<template #description>
<span>{{
$t('AbpUi.ItemWillBeDeletedMessageWithFormat', [row.name])
}}</span>
</template>
<Button :icon="h(DeleteOutlined)" block danger type="link">
{{ $t('AbpUi.Delete') }}
</Button>
</Popconfirm>
</div>
</template>
</VxeGrid>
<ConnectionStringModal :submit="submit" />
</div>
</template>
<style scoped></style>

71
apps/vben5/packages/@abp/saas/src/components/tenants/ConnectionStringsModal.vue

@ -0,0 +1,71 @@
<script setup lang="ts">
import type { TenantConnectionStringDto, TenantDto } from '../../types';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { message } from 'ant-design-vue';
import { useTenantsApi } from '../../api/useTenantsApi';
import ConnectionStringTable from './ConnectionStringTable.vue';
const connectionStrings = ref<TenantConnectionStringDto[]>([]);
const {
cancel,
deleteConnectionStringApi,
getConnectionStringsApi,
setConnectionStringApi,
} = useTenantsApi();
const [Modal, modalApi] = useVbenModal({
class: 'w-[800px]',
onClosed: cancel,
async onOpenChange(isOpen) {
connectionStrings.value = [];
if (isOpen) {
const dto = modalApi.getData<TenantDto>();
await onGet(dto.id);
}
},
});
async function onGet(id: string) {
const { items } = await getConnectionStringsApi(id);
connectionStrings.value = items;
}
async function onChange(data: TenantConnectionStringDto) {
const dto = modalApi.getData<TenantDto>();
try {
modalApi.setState({ submitting: true });
await setConnectionStringApi(dto.id, data);
message.success($t('AbpUi.SavedSuccessfully'));
await onGet(dto.id);
} finally {
modalApi.setState({ submitting: false });
}
}
async function onDelete(data: TenantConnectionStringDto) {
const dto = modalApi.getData<TenantDto>();
try {
modalApi.setState({ submitting: true });
await deleteConnectionStringApi(dto.id, data.name);
message.success($t('AbpUi.DeletedSuccessfully'));
await onGet(dto.id);
} finally {
modalApi.setState({ submitting: false });
}
}
</script>
<template>
<Modal :title="$t('AbpSaas.ConnectionStrings')">
<ConnectionStringTable
:connection-strings="connectionStrings"
:delete="onDelete"
:submit="onChange"
/>
</Modal>
</template>
<style scoped></style>

305
apps/vben5/packages/@abp/saas/src/components/tenants/TenantModal.vue

@ -0,0 +1,305 @@
<script setup lang="ts">
import type { FormExpose } from 'ant-design-vue/es/form/Form';
import type { EditionDto } from '../../types';
import type {
TenantConnectionStringDto,
TenantCreateDto,
TenantDto,
TenantUpdateDto,
} from '../../types/tenants';
import { computed, onMounted, ref, useTemplateRef } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { useValidation } from '@abp/core';
import {
Checkbox,
DatePicker,
Form,
Input,
InputPassword,
message,
Select,
Textarea,
} from 'ant-design-vue';
import dayjs from 'dayjs';
import debounce from 'lodash.debounce';
import { useEditionsApi } from '../../api/useEditionsApi';
import { useTenantsApi } from '../../api/useTenantsApi';
import ConnectionStringTable from './ConnectionStringTable.vue';
const emits = defineEmits<{
(event: 'change', val: TenantDto): void;
}>();
const FormItem = Form.Item;
const defaultModel = {
connectionStrings: [],
isActive: true,
useSharedDatabase: true,
} as unknown as TenantDto;
const { fieldDoNotValidEmailAddress, fieldRequired } = useValidation();
const form = useTemplateRef<FormExpose>('form');
const tenant = ref({ ...defaultModel });
const editions = ref<EditionDto[]>([]);
const activeTabKey = ref('basic');
const getFormRules = computed(() => {
return {
adminEmailAddress: [
...fieldRequired({
name: 'AdminEmailAddress',
prefix: 'DisplayName',
resourceName: 'AbpSaas',
}),
...fieldDoNotValidEmailAddress({
name: 'AdminEmailAddress',
prefix: 'DisplayName',
resourceName: 'AbpSaas',
}),
],
adminPassword: fieldRequired({
name: 'AdminPassword',
prefix: 'DisplayName',
resourceName: 'AbpSaas',
}),
defaultConnectionString: fieldRequired({
name: 'DefaultConnectionString',
prefix: 'DisplayName',
resourceName: 'AbpSaas',
}),
name: fieldRequired({
name: 'TenantName',
prefix: 'DisplayName',
resourceName: 'AbpSaas',
}),
};
});
/** 启用时间不可晚于禁用时间 */
const getDisabledEnableTime = (current: dayjs.Dayjs) => {
if (!tenant.value.disableTime) {
return false;
}
return (
current &&
current > dayjs(tenant.value.disableTime).add(-1, 'day').endOf('day')
);
};
/** 禁用时间不可早于启用时间 */
const getDisabledDisableTime = (current: dayjs.Dayjs) => {
if (!tenant.value.enableTime) {
return false;
}
return current && current < dayjs(tenant.value.enableTime).endOf('day');
};
const { cancel, createApi, getApi, updateApi } = useTenantsApi();
const { getPagedListApi: getEditions } = useEditionsApi();
const [Modal, modalApi] = useVbenModal({
class: 'w-[600px]',
onClosed: cancel,
async onConfirm() {
await form.value?.validate();
await onSubmit();
},
async onOpenChange(isOpen) {
activeTabKey.value = 'basic';
if (isOpen) {
await onGet();
}
},
title: $t('AbpSaas.Tenants'),
});
async function onGet() {
const { id } = modalApi.getData<TenantDto>();
if (!id) {
tenant.value = { ...defaultModel };
modalApi.setState({ title: $t('AbpSaas.NewTenant') });
return;
}
try {
modalApi.setState({ loading: true });
const editionDto = await getApi(id);
modalApi.setState({
title: `${$t('AbpSaas.Tenants')} - ${editionDto.name}`,
});
tenant.value = editionDto;
} finally {
modalApi.setState({ loading: false });
}
}
async function onSubmit() {
try {
modalApi.setState({ submitting: true });
const api = tenant.value.id
? updateApi(tenant.value.id, tenant.value as TenantUpdateDto)
: createApi(tenant.value as unknown as TenantCreateDto);
const dto = await api;
message.success($t('AbpUi.SavedSuccessfully'));
emits('change', dto);
modalApi.close();
} finally {
modalApi.setState({ submitting: false });
}
}
function onNameChange(name?: string) {
if (
!tenant.value.id &&
(!tenant.value.adminEmailAddress ||
!tenant.value.adminEmailAddress?.endsWith(`@${name}.com`))
) {
tenant.value.adminEmailAddress = `admin@${name}.com`;
}
}
function onConnectionChange(data: TenantConnectionStringDto) {
return new Promise<void>((resolve) => {
tenant.value.connectionStrings ??= [];
let connectionString = tenant.value.connectionStrings.find(
(x: TenantConnectionStringDto) => x.name === data.name,
);
if (connectionString) {
connectionString.value = data.value;
} else {
connectionString = data;
tenant.value.connectionStrings = [
...tenant.value.connectionStrings,
data,
];
}
resolve();
});
}
function onConnectionDelete(data: TenantConnectionStringDto) {
return new Promise<void>((resolve) => {
tenant.value.connectionStrings ??= [];
tenant.value.connectionStrings = tenant.value.connectionStrings.filter(
(x: TenantConnectionStringDto) => x.name !== data.name,
);
resolve();
});
}
const onSearchEditions = debounce(async (filter?: string) => {
const { items } = await getEditions({ filter });
editions.value = items;
}, 500);
onMounted(onSearchEditions);
</script>
<template>
<Modal>
<Form
ref="form"
:model="tenant"
:label-col="{ span: 6 }"
:wapper-col="{ span: 18 }"
:rules="getFormRules"
>
<FormItem name="isActive" :label="$t('AbpSaas.DisplayName:IsActive')">
<Checkbox v-model:checked="tenant.isActive">
{{ $t('AbpSaas.DisplayName:IsActive') }}
</Checkbox>
</FormItem>
<FormItem
v-if="!tenant.id"
name="adminEmailAddress"
:label="$t('AbpSaas.DisplayName:AdminEmailAddress')"
:label-col="{ span: 8 }"
:wapper-col="{ span: 16 }"
>
<Input
type="email"
v-model:value="tenant.adminEmailAddress"
autocomplete="off"
/>
</FormItem>
<FormItem
v-if="!tenant.id"
name="adminPassword"
:label="$t('AbpSaas.DisplayName:AdminPassword')"
>
<InputPassword
v-model:value="tenant.adminPassword"
autocomplete="off"
/>
</FormItem>
<FormItem name="name" :label="$t('AbpSaas.DisplayName:TenantName')">
<Input
v-model:value="tenant.name"
@change="(e) => onNameChange(e.target.value)"
autocomplete="off"
/>
</FormItem>
<FormItem name="editionId" :label="$t('AbpSaas.DisplayName:EditionName')">
<Select
:options="editions"
:field-names="{ label: 'displayName', value: 'id' }"
v-model:value="tenant.editionId"
allow-clear
show-search
:filter-option="false"
@search="onSearchEditions"
/>
</FormItem>
<FormItem name="enableTime" :label="$t('AbpSaas.DisplayName:EnableTime')">
<DatePicker
class="w-full"
value-format="YYYY-MM-DD"
:disabled-date="getDisabledEnableTime"
v-model:value="tenant.enableTime"
/>
</FormItem>
<FormItem
name="disableTime"
:label="$t('AbpSaas.DisplayName:DisableTime')"
>
<DatePicker
class="w-full"
value-format="YYYY-MM-DD"
:disabled-date="getDisabledDisableTime"
v-model:value="tenant.disableTime"
/>
</FormItem>
<FormItem
v-if="!tenant.id"
name="useSharedDatabase"
:label="$t('AbpSaas.DisplayName:UseSharedDatabase')"
>
<Checkbox v-model:checked="tenant.useSharedDatabase">
{{ $t('AbpSaas.DisplayName:UseSharedDatabase') }}
</Checkbox>
</FormItem>
<template v-if="!tenant.id && !tenant.useSharedDatabase">
<FormItem
name="defaultConnectionString"
:label="$t('AbpSaas.DisplayName:DefaultConnectionString')"
>
<Textarea
:auto-size="{ minRows: 2 }"
v-model:value="tenant.defaultConnectionString"
/>
</FormItem>
<ConnectionStringTable
:connection-strings="tenant.connectionStrings"
:submit="onConnectionChange"
:delete="onConnectionDelete"
/>
</template>
</Form>
</Modal>
</template>
<style scoped></style>

258
apps/vben5/packages/@abp/saas/src/components/tenants/TenantTable.vue

@ -0,0 +1,258 @@
<script setup lang="ts">
import type { VbenFormProps, VxeGridListeners, VxeGridProps } from '@abp/ui';
import type { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import type { TenantDto } from '../../types/tenants';
import { defineAsyncComponent, h } from 'vue';
import { useAccess } from '@vben/access';
import { useVbenDrawer, useVbenModal } from '@vben/common-ui';
import { createIconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { AuditLogPermissions, EntityChangeDrawer } from '@abp/auditing';
import { useFeatures } from '@abp/core';
import { useVbenVxeGrid } from '@abp/ui';
import {
DeleteOutlined,
EditOutlined,
EllipsisOutlined,
} from '@ant-design/icons-vue';
import { Button, Dropdown, Menu, message, Modal } from 'ant-design-vue';
import { useTenantsApi } from '../../api/useTenantsApi';
import { TenantsPermissions } from '../../constants/permissions';
defineOptions({
name: 'EditionTable',
});
const MenuItem = Menu.Item;
const CheckIcon = createIconifyIcon('ant-design:check-outlined');
const CloseIcon = createIconifyIcon('ant-design:close-outlined');
const AuditLogIcon = createIconifyIcon('fluent-mdl2:compliance-audit');
const ConnectionIcon = createIconifyIcon('mdi:connection');
const { isEnabled } = useFeatures();
const { hasAccessByCodes } = useAccess();
const { cancel, deleteApi, getPagedListApi } = useTenantsApi();
const formOptions: VbenFormProps = {
//
collapsed: false,
schema: [
{
component: 'Input',
componentProps: {
allowClear: true,
autocomplete: 'off',
},
fieldName: 'filter',
formItemClass: 'col-span-2 items-baseline',
label: $t('AbpUi.Search'),
},
],
//
showCollapseButton: true,
//
submitOnEnter: true,
};
const gridOptions: VxeGridProps<TenantDto> = {
columns: [
{
align: 'center',
field: 'isActive',
slots: { default: 'isActive' },
title: $t('AbpSaas.DisplayName:IsActive'),
width: 120,
},
{
align: 'left',
field: 'name',
title: $t('AbpSaas.DisplayName:Name'),
},
{
align: 'left',
field: 'editionName',
title: $t('AbpSaas.DisplayName:EditionName'),
width: 160,
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: $t('AbpUi.Actions'),
visible: hasAccessByCodes([
TenantsPermissions.Update,
TenantsPermissions.Delete,
]),
width: 220,
},
],
exportConfig: {},
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getPagedListApi({
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<TenantDto> = {
cellClick: () => {},
};
const [TenantModal, modalApi] = useVbenModal({
connectedComponent: defineAsyncComponent(() => import('./TenantModal.vue')),
});
const [TenantConnectionStringsModal, connectionStringsModalApi] = useVbenModal({
connectedComponent: defineAsyncComponent(
() => import('./ConnectionStringsModal.vue'),
),
});
const [TenantChangeDrawer, entityChangeDrawerApi] = useVbenDrawer({
connectedComponent: EntityChangeDrawer,
});
const [Grid, { query }] = useVbenVxeGrid({
formOptions,
gridEvents,
gridOptions,
});
const onCreate = () => {
modalApi.setData({});
modalApi.open();
};
const onUpdate = (row: TenantDto) => {
modalApi.setData(row);
modalApi.open();
};
const onDelete = (row: TenantDto) => {
Modal.confirm({
centered: true,
content: $t('AbpSaas.TenantDeletionConfirmationMessage', [row.name]),
onCancel: () => {
cancel();
},
onOk: async () => {
await deleteApi(row.id);
message.success($t('AbpUi.SuccessfullyDeleted'));
query();
},
title: $t('AbpUi.AreYouSure'),
});
};
const onMenuClick = (row: TenantDto, info: MenuInfo) => {
switch (info.key) {
case 'connection-strings': {
connectionStringsModalApi.setData(row);
connectionStringsModalApi.open();
break;
}
case 'entity-changes': {
entityChangeDrawerApi.setData({
entityId: row.id,
entityTypeFullName: 'LINGYUN.Abp.Saas.Tenant',
subject: row.name,
});
entityChangeDrawerApi.open();
break;
}
}
};
</script>
<template>
<Grid :table-title="$t('AbpSaas.Tenants')">
<template #toolbar-tools>
<Button
type="primary"
v-access:code="[TenantsPermissions.Create]"
@click="onCreate"
>
{{ $t('AbpSaas.NewTenant') }}
</Button>
</template>
<template #isActive="{ row }">
<div class="flex flex-row justify-center">
<CheckIcon v-if="row.isActive" class="text-green-500" />
<CloseIcon v-else class="text-red-500" />
</div>
</template>
<template #action="{ row }">
<div class="flex flex-row">
<Button
:icon="h(EditOutlined)"
block
type="link"
v-access:code="[TenantsPermissions.Update]"
@click="onUpdate(row)"
>
{{ $t('AbpUi.Edit') }}
</Button>
<Button
:icon="h(DeleteOutlined)"
block
danger
type="link"
v-access:code="[TenantsPermissions.Delete]"
@click="onDelete(row)"
>
{{ $t('AbpUi.Delete') }}
</Button>
<Dropdown>
<template #overlay>
<Menu @click="(info) => onMenuClick(row, info)">
<MenuItem
v-if="
hasAccessByCodes([TenantsPermissions.ManageConnectionStrings])
"
key="connection-strings"
:icon="h(ConnectionIcon)"
>
{{ $t('AbpSaas.ConnectionStrings') }}
</MenuItem>
<MenuItem
v-if="
isEnabled('AbpAuditing.Logging.AuditLog') &&
hasAccessByCodes([AuditLogPermissions.Default])
"
key="entity-changes"
:icon="h(AuditLogIcon)"
>
{{ $t('AbpAuditLogging.EntitiesChanged') }}
</MenuItem>
</Menu>
</template>
<Button :icon="h(EllipsisOutlined)" type="link" />
</Dropdown>
</div>
</template>
</Grid>
<TenantModal @change="() => query()" />
<TenantConnectionStringsModal />
<TenantChangeDrawer />
</template>
<style lang="scss" scoped></style>

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

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

28
apps/vben5/packages/@abp/saas/src/constants/permissions.ts

@ -0,0 +1,28 @@
/** 版本权限 */
export const EditionsPermissions = {
/** 新增 */
Create: 'AbpSaas.Editions.Create',
/** 默认 */
Default: 'AbpSaas.Editions',
/** 删除 */
Delete: 'AbpSaas.Editions.Delete',
/** 管理功能 */
ManageFeatures: 'AbpSaas.Editions.ManageFeatures',
/** 更新 */
Update: 'AbpSaas.Editions.Update',
};
/** 租户权限 */
export const TenantsPermissions = {
/** 新增 */
Create: 'AbpSaas.Tenants.Create',
/** 默认 */
Default: 'AbpSaas.Tenants',
/** 删除 */
Delete: 'AbpSaas.Tenants.Delete',
/** 管理连接字符串 */
ManageConnectionStrings: 'AbpSaas.Tenants.ManageConnectionStrings',
/** 管理功能 */
ManageFeatures: 'AbpSaas.Tenants.ManageFeatures',
/** 更新 */
Update: 'AbpSaas.Tenants.Update',
};

4
apps/vben5/packages/@abp/saas/src/index.ts

@ -0,0 +1,4 @@
export * from './api';
export * from './components';
export * from './constants';
export * from './types';

34
apps/vben5/packages/@abp/saas/src/types/editions.ts

@ -0,0 +1,34 @@
import type {
ExtensibleAuditedEntityDto,
IHasConcurrencyStamp,
PagedAndSortedResultRequestDto,
} from '@abp/core';
interface EditionDto
extends ExtensibleAuditedEntityDto<string>,
IHasConcurrencyStamp {
/** 显示名称 */
displayName: string;
}
interface EditionCreateOrUpdateBase {
/** 显示名称 */
displayName: string;
}
type EditionCreateDto = EditionCreateOrUpdateBase;
interface EditionUpdateDto
extends EditionCreateOrUpdateBase,
IHasConcurrencyStamp {}
interface GetEditionPagedListInput extends PagedAndSortedResultRequestDto {
filter?: string;
}
export type {
EditionCreateDto,
EditionDto,
EditionUpdateDto,
GetEditionPagedListInput,
};

2
apps/vben5/packages/@abp/saas/src/types/index.ts

@ -0,0 +1,2 @@
export * from './editions';
export * from './tenants';

68
apps/vben5/packages/@abp/saas/src/types/tenants.ts

@ -0,0 +1,68 @@
import type {
ExtensibleAuditedEntityDto,
ExtensibleObject,
IHasConcurrencyStamp,
NameValue,
PagedAndSortedResultRequestDto,
} from '@abp/core';
type TenantConnectionStringDto = NameValue<string>;
type TenantConnectionStringSetInput = NameValue<string>;
interface TenantDto
extends ExtensibleAuditedEntityDto<string>,
IHasConcurrencyStamp {
/** 禁用时间 */
disableTime?: string;
/** 版本Id */
editionId?: string;
/** 版本名称 */
editionName?: string;
/** 启用时间 */
enableTime?: string;
/** 是否可用 */
isActive: boolean;
/** 名称 */
name: string;
/** 名称 */
normalizedName: string;
}
interface GetTenantPagedListInput extends PagedAndSortedResultRequestDto {
filter?: string;
}
interface TenantCreateOrUpdateBase extends ExtensibleObject {
/** 禁用时间 */
disableTime?: string;
/** 版本Id */
editionId?: string;
/** 启用时间 */
enableTime?: string;
/** 是否可用 */
isActive: boolean;
/** 名称 */
name: string;
}
interface TenantCreateDto extends TenantCreateOrUpdateBase {
adminEmailAddress: string;
adminPassword: string;
connectionStrings?: TenantConnectionStringSetInput[];
defaultConnectionString?: string;
useSharedDatabase: boolean;
}
interface TenantUpdateDto
extends IHasConcurrencyStamp,
TenantCreateOrUpdateBase {}
export type {
GetTenantPagedListInput,
TenantConnectionStringDto,
TenantConnectionStringSetInput,
TenantCreateDto,
TenantDto,
TenantUpdateDto,
};

6
apps/vben5/packages/@abp/saas/tsconfig.json

@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web.json",
"include": ["src"],
"exclude": ["node_modules"]
}
Loading…
Cancel
Save