41 changed files with 1865 additions and 15 deletions
@ -0,0 +1,15 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { Page } from '@vben/common-ui'; |
||||
|
|
||||
|
import { EntityTypeInfoTable } from '@abp/data-protection'; |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'EntityTypeInfos', |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Page> |
||||
|
<EntityTypeInfoTable /> |
||||
|
</Page> |
||||
|
</template> |
||||
@ -0,0 +1,15 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { Page } from '@vben/common-ui'; |
||||
|
|
||||
|
import { BookTable } from '@abp/demo'; |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'DemoBooks', |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Page> |
||||
|
<BookTable /> |
||||
|
</Page> |
||||
|
</template> |
||||
@ -0,0 +1,36 @@ |
|||||
|
{ |
||||
|
"name": "@abp/data-protection", |
||||
|
"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/data-protection" |
||||
|
}, |
||||
|
"license": "MIT", |
||||
|
"type": "module", |
||||
|
"sideEffects": [ |
||||
|
"**/*.css" |
||||
|
], |
||||
|
"exports": { |
||||
|
".": { |
||||
|
"types": "./src/index.ts", |
||||
|
"default": "./src/index.ts" |
||||
|
} |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@abp/core": "workspace:*", |
||||
|
"@abp/request": "workspace:*", |
||||
|
"@abp/ui": "workspace:*", |
||||
|
"@ant-design/icons-vue": "catalog:", |
||||
|
"@vben/common-ui": "workspace:*", |
||||
|
"@vben/hooks": "workspace:*", |
||||
|
"@vben/icons": "workspace:*", |
||||
|
"@vben/layouts": "workspace:*", |
||||
|
"@vben/locales": "workspace:*", |
||||
|
"ant-design-vue": "catalog:", |
||||
|
"dayjs": "catalog:", |
||||
|
"vue": "catalog:*" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
export { useEntityTypeInfosApi } from './useEntityTypeInfosApi'; |
||||
|
export { useRoleEntityRulesApi } from './useRoleEntityRulesApi'; |
||||
|
export { useSubjectStrategysApi } from './useSubjectStrategysApi'; |
||||
@ -0,0 +1,33 @@ |
|||||
|
import type { PagedResultDto } from '@abp/core'; |
||||
|
|
||||
|
import type { |
||||
|
EntityTypeInfoDto, |
||||
|
GetEntityTypeInfoListInput, |
||||
|
} from '../types/entityTypeInfos'; |
||||
|
|
||||
|
import { useRequest } from '@abp/request'; |
||||
|
|
||||
|
export function useEntityTypeInfosApi() { |
||||
|
const { cancel, request } = useRequest(); |
||||
|
|
||||
|
function getApi(id: string): Promise<EntityTypeInfoDto> { |
||||
|
return request(`/api/data-protection-management/entity-type-infos/${id}`, { |
||||
|
method: 'GET', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function getPagedListApi( |
||||
|
input?: GetEntityTypeInfoListInput, |
||||
|
): Promise<PagedResultDto<EntityTypeInfoDto>> { |
||||
|
return request(`/api/data-protection-management/entity-type-infos`, { |
||||
|
method: 'GET', |
||||
|
params: input, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
cancel, |
||||
|
getApi, |
||||
|
getPagedListApi, |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
import type { |
||||
|
RoleEntityRuleCreateDto, |
||||
|
RoleEntityRuleDto, |
||||
|
RoleEntityRuleGetInput, |
||||
|
} from '../types/roleEntityRules'; |
||||
|
|
||||
|
import { useRequest } from '@abp/request'; |
||||
|
|
||||
|
export function useRoleEntityRulesApi() { |
||||
|
const { cancel, request } = useRequest(); |
||||
|
|
||||
|
function getApi(input: RoleEntityRuleGetInput): Promise<RoleEntityRuleDto> { |
||||
|
return request('/api/data-protection-management/entity-rule/roles', { |
||||
|
method: 'GET', |
||||
|
params: input, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function createApi( |
||||
|
input: RoleEntityRuleCreateDto, |
||||
|
): Promise<RoleEntityRuleDto> { |
||||
|
return request('/api/data-protection-management/entity-rule/roles', { |
||||
|
data: input, |
||||
|
method: 'POST', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function updateApi( |
||||
|
id: string, |
||||
|
input: RoleEntityRuleCreateDto, |
||||
|
): Promise<RoleEntityRuleDto> { |
||||
|
return request(`/api/data-protection-management/entity-rule/roles/${id}`, { |
||||
|
data: input, |
||||
|
method: 'PUT', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
cancel, |
||||
|
createApi, |
||||
|
getApi, |
||||
|
updateApi, |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
import type { |
||||
|
SubjectStrategyDto, |
||||
|
SubjectStrategyGetInput, |
||||
|
SubjectStrategySetInput, |
||||
|
} from '../types'; |
||||
|
|
||||
|
import { useRequest } from '@abp/request'; |
||||
|
|
||||
|
export function useSubjectStrategysApi() { |
||||
|
const { cancel, request } = useRequest(); |
||||
|
|
||||
|
/** |
||||
|
* 获取主体数据权限策略 |
||||
|
* @param input 参数 |
||||
|
* @returns 数据权限策略配置 |
||||
|
*/ |
||||
|
function getApi( |
||||
|
input: SubjectStrategyGetInput, |
||||
|
): Promise<null | SubjectStrategyDto> { |
||||
|
return request<null | SubjectStrategyDto>( |
||||
|
'/api/data-protection-management/subject-strategys', |
||||
|
{ |
||||
|
method: 'GET', |
||||
|
params: input, |
||||
|
}, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置主体数据权限策略 |
||||
|
* @param input 参数 |
||||
|
* @returns 数据权限策略配置 |
||||
|
*/ |
||||
|
function setApi(input: SubjectStrategySetInput): Promise<SubjectStrategyDto> { |
||||
|
return request<SubjectStrategyDto>( |
||||
|
'/api/data-protection-management/subject-strategys', |
||||
|
{ |
||||
|
data: input, |
||||
|
method: 'PUT', |
||||
|
}, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
cancel, |
||||
|
getApi, |
||||
|
setApi, |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,84 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { EntityTypeInfoDto } from '../../types'; |
||||
|
|
||||
|
import { onMounted, ref } from 'vue'; |
||||
|
|
||||
|
import { useVbenForm, useVbenModal } from '@vben/common-ui'; |
||||
|
|
||||
|
import { Select } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useEntityTypeInfosApi } from '../../api/useEntityTypeInfosApi'; |
||||
|
import { DataAccessOperation } from '../../types/entityRules'; |
||||
|
|
||||
|
const emits = defineEmits<{ |
||||
|
(event: 'entityChange', id: string): void; |
||||
|
}>(); |
||||
|
|
||||
|
const entityTypes = ref<EntityTypeInfoDto[]>([]); |
||||
|
const [Form, formApi] = useVbenForm({ |
||||
|
handleSubmit: onSubmit, |
||||
|
schema: [ |
||||
|
{ |
||||
|
component: 'Select', |
||||
|
fieldName: 'entityTypeId', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'Select', |
||||
|
componentProps: { |
||||
|
options: [ |
||||
|
{ |
||||
|
label: '查询', |
||||
|
value: DataAccessOperation.Read, |
||||
|
}, |
||||
|
{ |
||||
|
label: '编辑', |
||||
|
value: DataAccessOperation.Write, |
||||
|
}, |
||||
|
{ |
||||
|
label: '删除', |
||||
|
value: DataAccessOperation.Delete, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
fieldName: 'operation', |
||||
|
}, |
||||
|
], |
||||
|
showDefaultActions: false, |
||||
|
}); |
||||
|
const [Modal] = useVbenModal({ |
||||
|
async onConfirm() { |
||||
|
await formApi.validateAndSubmitForm(); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const { getPagedListApi } = useEntityTypeInfosApi(); |
||||
|
|
||||
|
async function onInit(filter?: string) { |
||||
|
const { items } = await getPagedListApi({ filter }); |
||||
|
entityTypes.value = items; |
||||
|
} |
||||
|
|
||||
|
async function onSubmit(values: Record<string, any>) { |
||||
|
console.log(values); |
||||
|
} |
||||
|
|
||||
|
onMounted(onInit); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Modal> |
||||
|
<Form> |
||||
|
<template #entityTypeId="{ model, field }"> |
||||
|
<Select |
||||
|
v-model:value="model[field]" |
||||
|
:options="entityTypes" |
||||
|
:field-names="{ label: 'displayName', value: 'id' }" |
||||
|
@change="(value) => emits('entityChange', value!.toString())" |
||||
|
/> |
||||
|
</template> |
||||
|
<slot name="subject"></slot> |
||||
|
</Form> |
||||
|
</Modal> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -0,0 +1,114 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { VxeGridListeners, VxeGridProps } from '@abp/ui'; |
||||
|
|
||||
|
import type { EntityTypeInfoDto } from '../../types/entityTypeInfos'; |
||||
|
|
||||
|
import { h } from 'vue'; |
||||
|
|
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { useLocalization, useLocalizationSerializer } from '@abp/core'; |
||||
|
import { useVbenVxeGrid } from '@abp/ui'; |
||||
|
import { EditOutlined } from '@ant-design/icons-vue'; |
||||
|
import { Button } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useEntityTypeInfosApi } from '../../api/useEntityTypeInfosApi'; |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'GdprTable', |
||||
|
}); |
||||
|
|
||||
|
const { getPagedListApi } = useEntityTypeInfosApi(); |
||||
|
|
||||
|
const { Lr } = useLocalization(); |
||||
|
const { deserialize } = useLocalizationSerializer(); |
||||
|
|
||||
|
const gridOptions: VxeGridProps<EntityTypeInfoDto> = { |
||||
|
columns: [ |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'name', |
||||
|
minWidth: 150, |
||||
|
title: $t('DataProtection.DisplayName:Name'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'displayName', |
||||
|
minWidth: 150, |
||||
|
title: $t('DataProtection.DisplayName:DisplayName'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'typeFullName', |
||||
|
minWidth: 200, |
||||
|
title: $t('DataProtection.DisplayName:TypeFullName'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'isAuditEnabled', |
||||
|
minWidth: 150, |
||||
|
title: $t('DataProtection.DisplayName:IsAuditEnabled'), |
||||
|
}, |
||||
|
{ |
||||
|
field: 'action', |
||||
|
fixed: 'right', |
||||
|
slots: { default: 'action' }, |
||||
|
title: $t('AbpUi.Actions'), |
||||
|
width: 180, |
||||
|
}, |
||||
|
], |
||||
|
exportConfig: {}, |
||||
|
keepSource: true, |
||||
|
proxyConfig: { |
||||
|
ajax: { |
||||
|
query: async ({ page }, formValues) => { |
||||
|
const { totalCount, items } = await getPagedListApi({ |
||||
|
maxResultCount: page.pageSize, |
||||
|
skipCount: (page.currentPage - 1) * page.pageSize, |
||||
|
...formValues, |
||||
|
}); |
||||
|
return { |
||||
|
totalCount, |
||||
|
items: items.map((item) => { |
||||
|
const displayName = deserialize(item.displayName); |
||||
|
return { |
||||
|
...item, |
||||
|
displayName: Lr(displayName.resourceName, displayName.name), |
||||
|
}; |
||||
|
}), |
||||
|
}; |
||||
|
}, |
||||
|
}, |
||||
|
response: { |
||||
|
total: 'totalCount', |
||||
|
list: 'items', |
||||
|
}, |
||||
|
}, |
||||
|
toolbarConfig: { |
||||
|
refresh: true, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
const gridEvents: VxeGridListeners<EntityTypeInfoDto> = { |
||||
|
cellClick: () => {}, |
||||
|
}; |
||||
|
const [Grid] = useVbenVxeGrid({ |
||||
|
gridEvents, |
||||
|
gridOptions, |
||||
|
}); |
||||
|
function onEdit(_record: EntityTypeInfoDto) {} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Grid :table-title="$t('DataProtection.EntityTypeInfos')"> |
||||
|
<template #action="{ row }"> |
||||
|
<div class="flex flex-row"> |
||||
|
<Button :icon="h(EditOutlined)" block type="link" @click="onEdit(row)"> |
||||
|
{{ $t('AbpUi.Edit') }} |
||||
|
</Button> |
||||
|
</div> |
||||
|
</template> |
||||
|
</Grid> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="scss" scoped></style> |
||||
@ -0,0 +1,2 @@ |
|||||
|
export { default as EntityRuleModal } from './entity-rules/EntityRuleModal.vue'; |
||||
|
export { default as EntityTypeInfoTable } from './entity-type-infos/EntityTypeInfoTable.vue'; |
||||
@ -0,0 +1,3 @@ |
|||||
|
export * from './api'; |
||||
|
export * from './components'; |
||||
|
export * from './types'; |
||||
@ -0,0 +1,101 @@ |
|||||
|
import type { AuditedEntityDto } from '@abp/core'; |
||||
|
|
||||
|
enum DataAccessOperation { |
||||
|
/** 删除 */ |
||||
|
Delete = 2, |
||||
|
/** 查询 */ |
||||
|
Read = 0, |
||||
|
/** 更新 */ |
||||
|
Write = 1, |
||||
|
} |
||||
|
|
||||
|
enum DataAccessFilterOperate { |
||||
|
/** 包含 */ |
||||
|
Contains = 9, |
||||
|
/** 右包含 */ |
||||
|
EndsWith = 8, |
||||
|
/** 等于 */ |
||||
|
Equal = 1, |
||||
|
/** 大于 */ |
||||
|
Greater = 5, |
||||
|
/** 大于或等于 */ |
||||
|
GreaterOrEqual = 6, |
||||
|
/** 小于 */ |
||||
|
Less = 3, |
||||
|
/** 小于或等于 */ |
||||
|
LessOrEqual = 4, |
||||
|
/** 不包含 */ |
||||
|
NotContains = 10, |
||||
|
/** 不等于 */ |
||||
|
NotEqual = 2, |
||||
|
/** 左包含 */ |
||||
|
StartsWith = 7, |
||||
|
} |
||||
|
|
||||
|
enum DataAccessFilterLogic { |
||||
|
/** 且 */ |
||||
|
And = 0, |
||||
|
/** 或 */ |
||||
|
Or = 1, |
||||
|
} |
||||
|
|
||||
|
interface DataAccessFilterRule { |
||||
|
[key: string]: any; |
||||
|
field: string; |
||||
|
isLeft: boolean; |
||||
|
javaScriptType: string; |
||||
|
operate: DataAccessFilterOperate; |
||||
|
typeFullName: string; |
||||
|
value: string; |
||||
|
} |
||||
|
|
||||
|
interface DataAccessFilterGroup { |
||||
|
groups: DataAccessFilterGroup[]; |
||||
|
logic: DataAccessFilterLogic; |
||||
|
rules: DataAccessFilterRule[]; |
||||
|
} |
||||
|
|
||||
|
interface EntityRuleDtoBase extends AuditedEntityDto<string> { |
||||
|
accessedProperties: string[]; |
||||
|
entityTypeFullName: string; |
||||
|
entityTypeId: string; |
||||
|
filterGroup: DataAccessFilterGroup; |
||||
|
isEnabled: boolean; |
||||
|
operation: DataAccessOperation; |
||||
|
tenantId?: string; |
||||
|
} |
||||
|
|
||||
|
interface EntityEnumInfoModel { |
||||
|
key: string; |
||||
|
value: any; |
||||
|
} |
||||
|
|
||||
|
interface EntityPropertyInfoModel { |
||||
|
displayName: string; |
||||
|
enums: EntityEnumInfoModel[]; |
||||
|
javaScriptName: string; |
||||
|
javaScriptType: string; |
||||
|
name: string; |
||||
|
operates: DataAccessFilterOperate[]; |
||||
|
typeFullName: string; |
||||
|
} |
||||
|
|
||||
|
interface EntityTypeInfoModel { |
||||
|
displayName: string; |
||||
|
name: string; |
||||
|
properties: EntityPropertyInfoModel[]; |
||||
|
} |
||||
|
|
||||
|
interface EntityTypeInfoGetModel { |
||||
|
operation: DataAccessOperation; |
||||
|
} |
||||
|
|
||||
|
export { DataAccessFilterLogic, DataAccessFilterOperate, DataAccessOperation }; |
||||
|
|
||||
|
export type { |
||||
|
DataAccessFilterGroup, |
||||
|
DataAccessFilterRule, |
||||
|
EntityRuleDtoBase, |
||||
|
EntityTypeInfoGetModel, |
||||
|
EntityTypeInfoModel, |
||||
|
}; |
||||
@ -0,0 +1,38 @@ |
|||||
|
import type { |
||||
|
AuditedEntityDto, |
||||
|
EntityDto, |
||||
|
PagedAndSortedResultRequestDto, |
||||
|
} from '@abp/core'; |
||||
|
|
||||
|
interface EntityEnumInfoDto extends EntityDto<string> { |
||||
|
displayName: string; |
||||
|
name: string; |
||||
|
value: string; |
||||
|
} |
||||
|
|
||||
|
interface EntityPropertyInfoDto extends EntityDto<string> { |
||||
|
displayName: string; |
||||
|
enums: EntityEnumInfoDto[]; |
||||
|
javaScriptType: string; |
||||
|
name: string; |
||||
|
typeFullName: string; |
||||
|
} |
||||
|
|
||||
|
interface EntityTypeInfoDto extends AuditedEntityDto<string> { |
||||
|
displayName: string; |
||||
|
isAuditEnabled: boolean; |
||||
|
name: string; |
||||
|
properties: EntityPropertyInfoDto[]; |
||||
|
typeFullName: string; |
||||
|
} |
||||
|
|
||||
|
interface GetEntityTypeInfoListInput extends PagedAndSortedResultRequestDto { |
||||
|
filter?: string; |
||||
|
isAuditEnabled?: boolean; |
||||
|
} |
||||
|
|
||||
|
export type { |
||||
|
EntityPropertyInfoDto, |
||||
|
EntityTypeInfoDto, |
||||
|
GetEntityTypeInfoListInput, |
||||
|
}; |
||||
@ -0,0 +1,4 @@ |
|||||
|
export * from './entityRules'; |
||||
|
export * from './entityTypeInfos'; |
||||
|
export * from './roleEntityRules'; |
||||
|
export * from './strategys'; |
||||
@ -0,0 +1,38 @@ |
|||||
|
import type { |
||||
|
DataAccessFilterGroup, |
||||
|
DataAccessOperation, |
||||
|
EntityRuleDtoBase, |
||||
|
} from './entityRules'; |
||||
|
|
||||
|
interface RoleEntityRuleGetInput { |
||||
|
entityTypeId: string; |
||||
|
operation: DataAccessOperation; |
||||
|
roleName: string; |
||||
|
} |
||||
|
|
||||
|
interface EntityRuleCreateOrUpdateDto { |
||||
|
accessedProperties?: string[]; |
||||
|
filterGroup: DataAccessFilterGroup; |
||||
|
isEnabled: boolean; |
||||
|
operation: DataAccessOperation; |
||||
|
} |
||||
|
|
||||
|
interface RoleEntityRuleCreateDto extends EntityRuleCreateOrUpdateDto { |
||||
|
entityTypeId: string; |
||||
|
roleId: string; |
||||
|
roleName: string; |
||||
|
} |
||||
|
|
||||
|
type RoleEntityRuleUpdateDto = EntityRuleCreateOrUpdateDto; |
||||
|
|
||||
|
interface RoleEntityRuleDto extends EntityRuleDtoBase { |
||||
|
roleId: string; |
||||
|
roleName: string; |
||||
|
} |
||||
|
|
||||
|
export type { |
||||
|
RoleEntityRuleCreateDto, |
||||
|
RoleEntityRuleDto, |
||||
|
RoleEntityRuleGetInput, |
||||
|
RoleEntityRuleUpdateDto, |
||||
|
}; |
||||
@ -0,0 +1,42 @@ |
|||||
|
/** 数据访问策略 */ |
||||
|
enum DataAccessStrategy { |
||||
|
/** 可以访问所有数据 */ |
||||
|
All = 0, |
||||
|
/** 仅当前用户组织机构及下级机构 */ |
||||
|
CurrentAndSubOrganizationUnits = 5, |
||||
|
/** 仅当前用户组织机构 */ |
||||
|
CurrentOrganizationUnits = 4, |
||||
|
/** 仅当前用户角色 */ |
||||
|
CurrentRoles = 3, |
||||
|
/** 仅当前用户 */ |
||||
|
CurrentUser = 2, |
||||
|
/** 自定义规则 */ |
||||
|
Custom = 1, |
||||
|
} |
||||
|
|
||||
|
interface SubjectStrategyGetInput { |
||||
|
subjectId: string; |
||||
|
subjectName: string; |
||||
|
} |
||||
|
|
||||
|
interface SubjectStrategySetInput { |
||||
|
isEnabled: boolean; |
||||
|
strategy: DataAccessStrategy; |
||||
|
subjectId: string; |
||||
|
subjectName: string; |
||||
|
} |
||||
|
|
||||
|
interface SubjectStrategyDto { |
||||
|
isEnabled: boolean; |
||||
|
strategy: DataAccessStrategy; |
||||
|
subjectId: string; |
||||
|
subjectName: string; |
||||
|
} |
||||
|
|
||||
|
export { DataAccessStrategy }; |
||||
|
|
||||
|
export type { |
||||
|
SubjectStrategyDto, |
||||
|
SubjectStrategyGetInput, |
||||
|
SubjectStrategySetInput, |
||||
|
}; |
||||
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"$schema": "https://json.schemastore.org/tsconfig", |
||||
|
"extends": "@vben/tsconfig/web.json", |
||||
|
"include": ["src"], |
||||
|
"exclude": ["node_modules"] |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
{ |
||||
|
"name": "@abp/demo", |
||||
|
"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/demo" |
||||
|
}, |
||||
|
"license": "MIT", |
||||
|
"type": "module", |
||||
|
"sideEffects": [ |
||||
|
"**/*.css" |
||||
|
], |
||||
|
"exports": { |
||||
|
".": { |
||||
|
"types": "./src/index.ts", |
||||
|
"default": "./src/index.ts" |
||||
|
} |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@abp/core": "workspace:*", |
||||
|
"@abp/data-protection": "workspace:*", |
||||
|
"@abp/request": "workspace:*", |
||||
|
"@abp/ui": "workspace:*", |
||||
|
"@ant-design/icons-vue": "catalog:", |
||||
|
"@vben/access": "workspace:*", |
||||
|
"@vben/common-ui": "workspace:*", |
||||
|
"@vben/hooks": "workspace:*", |
||||
|
"@vben/icons": "workspace:*", |
||||
|
"@vben/locales": "workspace:*", |
||||
|
"ant-design-vue": "catalog:", |
||||
|
"dayjs": "catalog:", |
||||
|
"vue": "catalog:*" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,2 @@ |
|||||
|
export { useAuthorsApi } from './useAuthorsApi'; |
||||
|
export { useBooksApi } from './useBooksApi'; |
||||
@ -0,0 +1,23 @@ |
|||||
|
import type { PagedResultDto } from '@abp/core'; |
||||
|
|
||||
|
import type { AuthorDto, GetAuthorPagedListInput } from '../types/authors'; |
||||
|
|
||||
|
import { useRequest } from '@abp/request'; |
||||
|
|
||||
|
export function useAuthorsApi() { |
||||
|
const { cancel, request } = useRequest(); |
||||
|
|
||||
|
function getPagedListApi( |
||||
|
input?: GetAuthorPagedListInput, |
||||
|
): Promise<PagedResultDto<AuthorDto>> { |
||||
|
return request<PagedResultDto<AuthorDto>>('/api/demo/authors', { |
||||
|
method: 'GET', |
||||
|
params: input, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
cancel, |
||||
|
getPagedListApi, |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,71 @@ |
|||||
|
import type { PagedResultDto } from '@abp/core'; |
||||
|
import type { |
||||
|
EntityTypeInfoGetModel, |
||||
|
EntityTypeInfoModel, |
||||
|
} from '@abp/data-protection'; |
||||
|
|
||||
|
import type { |
||||
|
BookDto, |
||||
|
CreateUpdateBookDto, |
||||
|
GetBookPagedListInput, |
||||
|
} from '../types/books'; |
||||
|
|
||||
|
import { useRequest } from '@abp/request'; |
||||
|
|
||||
|
export function useBooksApi() { |
||||
|
const { cancel, request } = useRequest(); |
||||
|
|
||||
|
function createApi(input: CreateUpdateBookDto): Promise<BookDto> { |
||||
|
return request<BookDto>(`/api/demo/books`, { |
||||
|
data: input, |
||||
|
method: 'POST', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function deleteApi(id: string): Promise<void> { |
||||
|
return request(`/api/demo/books/${id}`, { |
||||
|
method: 'DELETE', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function getApi(id: string): Promise<BookDto> { |
||||
|
return request<BookDto>(`/api/demo/books/${id}`, { |
||||
|
method: 'GET', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function getPagedListApi( |
||||
|
input?: GetBookPagedListInput, |
||||
|
): Promise<PagedResultDto<BookDto>> { |
||||
|
return request<PagedResultDto<BookDto>>('/api/demo/books', { |
||||
|
method: 'GET', |
||||
|
params: input, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function getEntityInfoApi( |
||||
|
input: EntityTypeInfoGetModel, |
||||
|
): Promise<EntityTypeInfoModel> { |
||||
|
return request<EntityTypeInfoModel>('/api/demo/books/entity', { |
||||
|
method: 'GET', |
||||
|
params: input, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function updateApi(id: string, input: CreateUpdateBookDto): Promise<BookDto> { |
||||
|
return request<BookDto>(`/api/demo/books/${id}`, { |
||||
|
data: input, |
||||
|
method: 'PUT', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
cancel, |
||||
|
createApi, |
||||
|
deleteApi, |
||||
|
getApi, |
||||
|
getEntityInfoApi, |
||||
|
getPagedListApi, |
||||
|
updateApi, |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,212 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { EntityTypeInfoModel } from '@abp/data-protection'; |
||||
|
|
||||
|
import type { FormSchema } from '../../../../../@core/ui-kit/form-ui/src/types'; |
||||
|
import type { BookDto, CreateUpdateBookDto } from '../../types/books'; |
||||
|
|
||||
|
import { nextTick, ref } from 'vue'; |
||||
|
|
||||
|
import { useVbenForm, useVbenModal } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { DataAccessOperation } from '@abp/data-protection'; |
||||
|
import { message } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useAuthorsApi } from '../../api/useAuthorsApi'; |
||||
|
import { useBooksApi } from '../../api/useBooksApi'; |
||||
|
import { BookType } from '../../types/books'; |
||||
|
|
||||
|
const emits = defineEmits<{ |
||||
|
(event: 'change', data: BookDto): void; |
||||
|
}>(); |
||||
|
const { getPagedListApi } = useAuthorsApi(); |
||||
|
const { createApi, getApi, getEntityInfoApi, updateApi } = useBooksApi(); |
||||
|
|
||||
|
const allowShowFields = ref<string[]>([]); |
||||
|
const defaultFormSchema: FormSchema[] = [ |
||||
|
{ |
||||
|
component: 'Input', |
||||
|
fieldName: 'id', |
||||
|
formItemClass: 'hidden', |
||||
|
label: 'id', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'Input', |
||||
|
fieldName: 'name', |
||||
|
label: $t('Demo.DisplayName:Name'), |
||||
|
rules: 'required', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'Select', |
||||
|
componentProps: { |
||||
|
options: [ |
||||
|
{ |
||||
|
label: $t('Demo.BookType:Undefined'), |
||||
|
value: BookType.Undefined, |
||||
|
}, |
||||
|
{ |
||||
|
label: $t('Demo.BookType:Adventure'), |
||||
|
value: BookType.Adventure, |
||||
|
}, |
||||
|
{ |
||||
|
label: $t('Demo.BookType:Biography'), |
||||
|
value: BookType.Biography, |
||||
|
}, |
||||
|
{ |
||||
|
label: $t('Demo.BookType:Dystopia'), |
||||
|
value: BookType.Dystopia, |
||||
|
}, |
||||
|
{ |
||||
|
label: $t('Demo.BookType:Fantastic'), |
||||
|
value: BookType.Fantastic, |
||||
|
}, |
||||
|
{ |
||||
|
label: $t('Demo.BookType:Horror'), |
||||
|
value: BookType.Horror, |
||||
|
}, |
||||
|
{ |
||||
|
label: $t('Demo.BookType:Science'), |
||||
|
value: BookType.Science, |
||||
|
}, |
||||
|
{ |
||||
|
label: $t('Demo.BookType:ScienceFiction'), |
||||
|
value: BookType.ScienceFiction, |
||||
|
}, |
||||
|
{ |
||||
|
label: $t('Demo.BookType:Poetry'), |
||||
|
value: BookType.Poetry, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
fieldName: 'type', |
||||
|
label: $t('Demo.DisplayName:Type'), |
||||
|
rules: 'selectRequired', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'DatePicker', |
||||
|
componentProps: { |
||||
|
valueFormat: 'YYYY-MM-DD', |
||||
|
}, |
||||
|
fieldName: 'publishDate', |
||||
|
label: $t('Demo.DisplayName:PublishDate'), |
||||
|
rules: 'selectRequired', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'InputNumber', |
||||
|
fieldName: 'price', |
||||
|
label: $t('Demo.DisplayName:Price'), |
||||
|
rules: 'required', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'ApiSelect', |
||||
|
componentProps: { |
||||
|
api: getPagedListApi, |
||||
|
labelField: 'name', |
||||
|
resultField: 'items', |
||||
|
valueField: 'id', |
||||
|
}, |
||||
|
fieldName: 'authorId', |
||||
|
label: $t('Demo.DisplayName:AuthorId'), |
||||
|
rules: 'selectRequired', |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
const [Form, formApi] = useVbenForm({ |
||||
|
commonConfig: { |
||||
|
colon: true, |
||||
|
controlClass: 'w-full', |
||||
|
}, |
||||
|
handleSubmit: onSubmit, |
||||
|
schema: [], |
||||
|
showDefaultActions: false, |
||||
|
}); |
||||
|
|
||||
|
const [Modal, modalApi] = useVbenModal({ |
||||
|
async onConfirm() { |
||||
|
await formApi.validateAndSubmitForm(); |
||||
|
}, |
||||
|
async onOpenChange(isOpen) { |
||||
|
if (isOpen) { |
||||
|
await onInit(); |
||||
|
await onGet(); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
async function onInit() { |
||||
|
const { id } = modalApi.getData<BookDto>(); |
||||
|
if (!id) { |
||||
|
nextTick(() => { |
||||
|
formApi.setState({ |
||||
|
schema: [...defaultFormSchema], |
||||
|
}); |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
const [entityReadRule, entityWriteRule] = await Promise.all([ |
||||
|
getEntityInfoApi({ |
||||
|
operation: DataAccessOperation.Read, |
||||
|
}), |
||||
|
getEntityInfoApi({ |
||||
|
operation: DataAccessOperation.Write, |
||||
|
}), |
||||
|
]); |
||||
|
updateSchema(entityReadRule, entityWriteRule); |
||||
|
} |
||||
|
|
||||
|
function updateSchema( |
||||
|
readRule: EntityTypeInfoModel, |
||||
|
writeRule: EntityTypeInfoModel, |
||||
|
) { |
||||
|
const readProps = readRule.properties.map((x) => x.javaScriptName); |
||||
|
const writeProps = new Set(writeRule.properties.map((x) => x.javaScriptName)); |
||||
|
const schemas = defaultFormSchema |
||||
|
.filter((schema) => readProps.includes(schema.fieldName)) |
||||
|
.map((schema) => { |
||||
|
return { |
||||
|
...schema, |
||||
|
disabled: !writeProps.has(schema.fieldName), |
||||
|
}; |
||||
|
}); |
||||
|
allowShowFields.value = readProps; |
||||
|
|
||||
|
nextTick(() => { |
||||
|
formApi.setState({ |
||||
|
schema: schemas, |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
async function onGet() { |
||||
|
formApi.resetForm(); |
||||
|
const { id } = modalApi.getData<BookDto>(); |
||||
|
if (id) { |
||||
|
const dto = await getApi(id); |
||||
|
allowShowFields.value.forEach((field) => { |
||||
|
formApi.setFieldValue(field, Reflect.get(dto, field)); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function onSubmit(values: Record<string, any>) { |
||||
|
const input = values as CreateUpdateBookDto; |
||||
|
try { |
||||
|
modalApi.setState({ submitting: true }); |
||||
|
const api = values.id ? updateApi(values.id, input) : createApi(input); |
||||
|
const dto = await api; |
||||
|
message.success($t('AbpUi.SavedSuccessfully')); |
||||
|
emits('change', dto); |
||||
|
modalApi.close(); |
||||
|
} finally { |
||||
|
modalApi.setState({ submitting: false }); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Modal :title="$t('Demo.Book')"> |
||||
|
<Form /> |
||||
|
</Modal> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -0,0 +1,204 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { VxeGridListeners, VxeGridProps, VxeGridPropTypes } from '@abp/ui'; |
||||
|
|
||||
|
import type { VbenFormProps } from '@vben/common-ui'; |
||||
|
|
||||
|
import type { BookDto } from '../../types/books'; |
||||
|
|
||||
|
import { defineAsyncComponent, h, nextTick, onMounted, reactive } from 'vue'; |
||||
|
|
||||
|
import { useVbenModal } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { DataAccessOperation } from '@abp/data-protection'; |
||||
|
import { useVbenVxeGrid } from '@abp/ui'; |
||||
|
import { DeleteOutlined, EditOutlined } from '@ant-design/icons-vue'; |
||||
|
import { Button, message, Modal } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useBooksApi } from '../../api/useBooksApi'; |
||||
|
import { BookPermissions } from '../../constants/permissions'; |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'GdprTable', |
||||
|
}); |
||||
|
|
||||
|
const { deleteApi, getEntityInfoApi, getPagedListApi } = useBooksApi(); |
||||
|
const formOptions: VbenFormProps = { |
||||
|
// 默认展开 |
||||
|
collapsed: false, |
||||
|
resetButtonOptions: { |
||||
|
show: false, |
||||
|
}, |
||||
|
schema: [ |
||||
|
{ |
||||
|
component: 'Input', |
||||
|
componentProps: { |
||||
|
allowClear: true, |
||||
|
autocomplete: 'off', |
||||
|
}, |
||||
|
fieldName: 'filter', |
||||
|
formItemClass: 'col-span-2 items-baseline', |
||||
|
label: $t('AbpUi.Search'), |
||||
|
}, |
||||
|
], |
||||
|
// 控制表单是否显示折叠按钮 |
||||
|
showCollapseButton: false, |
||||
|
// 按下回车时是否提交表单 |
||||
|
submitOnEnter: true, |
||||
|
}; |
||||
|
const actionColumns = reactive<VxeGridPropTypes.Columns<BookDto>>([ |
||||
|
{ |
||||
|
field: 'action', |
||||
|
fixed: 'right', |
||||
|
slots: { default: 'action' }, |
||||
|
title: $t('AbpUi.Actions'), |
||||
|
width: 180, |
||||
|
}, |
||||
|
]); |
||||
|
const baseColumns = reactive<VxeGridPropTypes.Columns<BookDto>>([ |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'name', |
||||
|
minWidth: 150, |
||||
|
title: $t('Demo.DisplayName:Name'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'authorName', |
||||
|
minWidth: 150, |
||||
|
title: $t('Demo.DisplayName:AuthorId'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'publishDate', |
||||
|
minWidth: 200, |
||||
|
title: $t('Demo.DisplayName:PublishDate'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'price', |
||||
|
minWidth: 150, |
||||
|
title: $t('Demo.DisplayName:Price'), |
||||
|
}, |
||||
|
]); |
||||
|
|
||||
|
const gridOptions: VxeGridProps<BookDto> = { |
||||
|
columns: [...baseColumns, ...actionColumns], |
||||
|
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: { |
||||
|
refresh: true, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
const gridEvents: VxeGridListeners<BookDto> = { |
||||
|
cellClick: () => {}, |
||||
|
}; |
||||
|
const [Grid, gridApi] = useVbenVxeGrid({ |
||||
|
formOptions, |
||||
|
gridEvents, |
||||
|
gridOptions, |
||||
|
}); |
||||
|
const [BookModal, modalApi] = useVbenModal({ |
||||
|
connectedComponent: defineAsyncComponent(() => import('./BookModal.vue')), |
||||
|
}); |
||||
|
function onCreate() { |
||||
|
modalApi.setData({}); |
||||
|
modalApi.open(); |
||||
|
} |
||||
|
function onEdit(record: BookDto) { |
||||
|
modalApi.setData(record); |
||||
|
modalApi.open(); |
||||
|
} |
||||
|
|
||||
|
async function onDelete(record: BookDto) { |
||||
|
Modal.confirm({ |
||||
|
centered: true, |
||||
|
content: $t('AbpUi.ItemWillBeDeletedMessageWithFormat', [record.name]), |
||||
|
maskClosable: true, |
||||
|
onOk: async () => { |
||||
|
await deleteApi(record.id); |
||||
|
message.success($t('AbpUi.DeletedSuccessfully')); |
||||
|
await gridApi.query(); |
||||
|
}, |
||||
|
title: $t('AbpUi.AreYouSure'), |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
async function onInit() { |
||||
|
const entityRule = await getEntityInfoApi({ |
||||
|
operation: DataAccessOperation.Read, |
||||
|
}); |
||||
|
if (!entityRule.properties?.length) { |
||||
|
return; |
||||
|
} |
||||
|
const allowProperties = new Set( |
||||
|
entityRule.properties.map((x) => x.javaScriptName), |
||||
|
); |
||||
|
nextTick(() => { |
||||
|
gridApi.setGridOptions({ |
||||
|
columns: [ |
||||
|
...baseColumns.filter((x) => allowProperties.has(x.field!)), |
||||
|
...actionColumns, |
||||
|
], |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
onMounted(onInit); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Grid :table-title="$t('Demo.Book')"> |
||||
|
<template #toolbar-tools> |
||||
|
<Button |
||||
|
type="primary" |
||||
|
v-access:code="[BookPermissions.Create]" |
||||
|
@click="onCreate" |
||||
|
> |
||||
|
{{ $t('Demo.NewBook') }} |
||||
|
</Button> |
||||
|
</template> |
||||
|
<template #action="{ row }"> |
||||
|
<div class="flex flex-row"> |
||||
|
<Button |
||||
|
v-access:code="[BookPermissions.Update]" |
||||
|
:icon="h(EditOutlined)" |
||||
|
type="link" |
||||
|
block |
||||
|
@click="onEdit(row)" |
||||
|
> |
||||
|
{{ $t('AbpUi.Edit') }} |
||||
|
</Button> |
||||
|
<Button |
||||
|
v-access:code="[BookPermissions.Delete]" |
||||
|
:icon="h(DeleteOutlined)" |
||||
|
type="link" |
||||
|
danger |
||||
|
block |
||||
|
@click="onDelete(row)" |
||||
|
> |
||||
|
{{ $t('AbpUi.Delete') }} |
||||
|
</Button> |
||||
|
</div> |
||||
|
</template> |
||||
|
</Grid> |
||||
|
<BookModal @change="() => gridApi.query()" /> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="scss" scoped></style> |
||||
@ -0,0 +1 @@ |
|||||
|
export { default as BookTable } from './books/BookTable.vue'; |
||||
@ -0,0 +1,8 @@ |
|||||
|
export const BookPermissions = { |
||||
|
/** 新增 */ |
||||
|
Create: 'Demo.Books.Create', |
||||
|
/** 删除 */ |
||||
|
Delete: 'Demo.Books.Delete', |
||||
|
/** 更新 */ |
||||
|
Update: 'Demo.Books.Edit', |
||||
|
}; |
||||
@ -0,0 +1,3 @@ |
|||||
|
export * from './api'; |
||||
|
export * from './components'; |
||||
|
export * from './types'; |
||||
@ -0,0 +1,13 @@ |
|||||
|
import type { EntityDto, PagedAndSortedResultRequestDto } from '@abp/core'; |
||||
|
|
||||
|
interface AuthorDto extends EntityDto<string> { |
||||
|
birthDate: string; |
||||
|
name: string; |
||||
|
shortBio?: string; |
||||
|
} |
||||
|
|
||||
|
interface GetAuthorPagedListInput extends PagedAndSortedResultRequestDto { |
||||
|
filter?: string; |
||||
|
} |
||||
|
|
||||
|
export type { AuthorDto, GetAuthorPagedListInput }; |
||||
@ -0,0 +1,41 @@ |
|||||
|
import type { |
||||
|
AuditedEntityDto, |
||||
|
PagedAndSortedResultRequestDto, |
||||
|
} from '@abp/core'; |
||||
|
|
||||
|
enum BookType { |
||||
|
Undefined, |
||||
|
Adventure, |
||||
|
Biography, |
||||
|
Dystopia, |
||||
|
Fantastic, |
||||
|
Horror, |
||||
|
Science, |
||||
|
ScienceFiction, |
||||
|
Poetry, |
||||
|
} |
||||
|
|
||||
|
interface BookDto extends AuditedEntityDto<string> { |
||||
|
authorId: string; |
||||
|
authorName: string; |
||||
|
name: string; |
||||
|
price?: number; |
||||
|
publishDate: string; |
||||
|
type: BookType; |
||||
|
} |
||||
|
|
||||
|
interface CreateUpdateBookDto { |
||||
|
authorId: string; |
||||
|
name: string; |
||||
|
price: number; |
||||
|
publishDate: string; |
||||
|
type: BookType; |
||||
|
} |
||||
|
|
||||
|
interface GetBookPagedListInput extends PagedAndSortedResultRequestDto { |
||||
|
filter?: string; |
||||
|
} |
||||
|
|
||||
|
export { BookType }; |
||||
|
|
||||
|
export type { BookDto, CreateUpdateBookDto, GetBookPagedListInput }; |
||||
@ -0,0 +1 @@ |
|||||
|
export * from './authors'; |
||||
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"$schema": "https://json.schemastore.org/tsconfig", |
||||
|
"extends": "@vben/tsconfig/web.json", |
||||
|
"include": ["src"], |
||||
|
"exclude": ["node_modules"] |
||||
|
} |
||||
@ -0,0 +1,528 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { |
||||
|
DataAccessFilterGroup, |
||||
|
DataAccessFilterRule, |
||||
|
EntityPropertyInfoDto, |
||||
|
EntityTypeInfoDto, |
||||
|
RoleEntityRuleDto, |
||||
|
} from '@abp/data-protection'; |
||||
|
import type { FormInstance } from 'ant-design-vue/es/form/Form'; |
||||
|
|
||||
|
import type { IdentityRoleDto } from '../../types'; |
||||
|
|
||||
|
import { h, reactive, ref, toValue, useTemplateRef } from 'vue'; |
||||
|
|
||||
|
import { useVbenDrawer } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { useLocalization, useLocalizationSerializer } from '@abp/core'; |
||||
|
import { |
||||
|
DataAccessFilterLogic, |
||||
|
DataAccessFilterOperate, |
||||
|
DataAccessOperation, |
||||
|
DataAccessStrategy, |
||||
|
useEntityTypeInfosApi, |
||||
|
useRoleEntityRulesApi, |
||||
|
useSubjectStrategysApi, |
||||
|
} from '@abp/data-protection'; |
||||
|
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue'; |
||||
|
import { |
||||
|
Button, |
||||
|
Checkbox, |
||||
|
DatePicker, |
||||
|
Form, |
||||
|
FormItem, |
||||
|
FormItemRest, |
||||
|
Input, |
||||
|
InputNumber, |
||||
|
message, |
||||
|
Popconfirm, |
||||
|
Select, |
||||
|
Switch, |
||||
|
} from 'ant-design-vue'; |
||||
|
|
||||
|
const defaultModel: RoleEntityRuleDto = { |
||||
|
accessedProperties: [], |
||||
|
creationTime: new Date(), |
||||
|
entityTypeFullName: '', |
||||
|
entityTypeId: '', |
||||
|
filterGroup: { |
||||
|
groups: [], |
||||
|
logic: DataAccessFilterLogic.Or, |
||||
|
rules: [], |
||||
|
}, |
||||
|
id: '', |
||||
|
isEnabled: true, |
||||
|
operation: DataAccessOperation.Read, |
||||
|
roleId: '', |
||||
|
roleName: '', |
||||
|
strategy: DataAccessStrategy.All, |
||||
|
}; |
||||
|
|
||||
|
const form = useTemplateRef<FormInstance>('form'); |
||||
|
const formModel = ref<RoleEntityRuleDto>({ ...defaultModel }); |
||||
|
const entityTypes = ref<EntityTypeInfoDto[]>([]); |
||||
|
const entityTypeProps = ref<EntityPropertyInfoDto[]>([]); |
||||
|
const strategyOptions = reactive([ |
||||
|
{ |
||||
|
label: '可以访问所有数据', |
||||
|
value: DataAccessStrategy.All, |
||||
|
}, |
||||
|
{ |
||||
|
label: '自定义规则', |
||||
|
value: DataAccessStrategy.Custom, |
||||
|
}, |
||||
|
{ |
||||
|
label: '仅当前用户', |
||||
|
value: DataAccessStrategy.CurrentUser, |
||||
|
}, |
||||
|
{ |
||||
|
label: '仅当前用户角色', |
||||
|
value: DataAccessStrategy.CurrentRoles, |
||||
|
}, |
||||
|
{ |
||||
|
label: '仅当前用户组织机构', |
||||
|
value: DataAccessStrategy.CurrentOrganizationUnits, |
||||
|
}, |
||||
|
{ |
||||
|
label: '仅当前用户组织机构及下级机构', |
||||
|
value: DataAccessStrategy.CurrentAndSubOrganizationUnits, |
||||
|
}, |
||||
|
]); |
||||
|
const operationOptions = reactive([ |
||||
|
{ |
||||
|
label: '查询', |
||||
|
value: DataAccessOperation.Read, |
||||
|
}, |
||||
|
{ |
||||
|
label: '编辑', |
||||
|
value: DataAccessOperation.Write, |
||||
|
}, |
||||
|
{ |
||||
|
label: '删除', |
||||
|
value: DataAccessOperation.Delete, |
||||
|
}, |
||||
|
]); |
||||
|
const logicOptions = reactive([ |
||||
|
{ |
||||
|
label: '且', |
||||
|
value: DataAccessFilterLogic.And, |
||||
|
}, |
||||
|
{ |
||||
|
label: '或', |
||||
|
value: DataAccessFilterLogic.Or, |
||||
|
}, |
||||
|
]); |
||||
|
const dataAccessFilterOptions = reactive([ |
||||
|
{ |
||||
|
label: '等于', |
||||
|
value: DataAccessFilterOperate.Equal, |
||||
|
}, |
||||
|
{ |
||||
|
label: '大于', |
||||
|
value: DataAccessFilterOperate.Greater, |
||||
|
}, |
||||
|
{ |
||||
|
label: '大于等于', |
||||
|
value: DataAccessFilterOperate.GreaterOrEqual, |
||||
|
}, |
||||
|
{ |
||||
|
label: '小于', |
||||
|
value: DataAccessFilterOperate.Less, |
||||
|
}, |
||||
|
{ |
||||
|
label: '小于等于', |
||||
|
value: DataAccessFilterOperate.LessOrEqual, |
||||
|
}, |
||||
|
{ |
||||
|
label: '不等于', |
||||
|
value: DataAccessFilterOperate.NotEqual, |
||||
|
}, |
||||
|
{ |
||||
|
label: '左包含', |
||||
|
value: DataAccessFilterOperate.StartsWith, |
||||
|
}, |
||||
|
{ |
||||
|
label: '右包含', |
||||
|
value: DataAccessFilterOperate.EndsWith, |
||||
|
}, |
||||
|
{ |
||||
|
label: '不包含', |
||||
|
value: DataAccessFilterOperate.NotContains, |
||||
|
}, |
||||
|
{ |
||||
|
label: '不等于', |
||||
|
value: DataAccessFilterOperate.NotEqual, |
||||
|
}, |
||||
|
]); |
||||
|
const [Drawer, drawerApi] = useVbenDrawer({ |
||||
|
class: 'w-1/2', |
||||
|
onConfirm: onSubmit, |
||||
|
async onOpenChange(isOpen) { |
||||
|
entityTypeProps.value = []; |
||||
|
formModel.value = { ...defaultModel }; |
||||
|
if (isOpen) { |
||||
|
await onInitStrategys(); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const { getApi: getEntityTypeInfoApi, getPagedListApi } = |
||||
|
useEntityTypeInfosApi(); |
||||
|
const { |
||||
|
createApi, |
||||
|
getApi: getRoleEntityRuleApi, |
||||
|
updateApi, |
||||
|
} = useRoleEntityRulesApi(); |
||||
|
const { getApi: getStrategysApi, setApi: setStrategysApi } = |
||||
|
useSubjectStrategysApi(); |
||||
|
const { deserialize } = useLocalizationSerializer(); |
||||
|
const { Lr } = useLocalization(); |
||||
|
|
||||
|
async function onInitStrategys() { |
||||
|
const roleInfo = drawerApi.getData<IdentityRoleDto>(); |
||||
|
const result = await getStrategysApi({ |
||||
|
subjectId: roleInfo.name, |
||||
|
subjectName: 'R', |
||||
|
}); |
||||
|
if (result) { |
||||
|
formModel.value.isEnabled = result.isEnabled; |
||||
|
formModel.value.strategy = result.strategy; |
||||
|
await onInitEntityTypes(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function onInitEntityTypes(filter?: string) { |
||||
|
const { items } = await getPagedListApi({ filter }); |
||||
|
entityTypes.value = items.map((item) => { |
||||
|
const displayName = deserialize(item.displayName); |
||||
|
return { |
||||
|
...item, |
||||
|
displayName: Lr(displayName.resourceName, displayName.name), |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function onNewGroup(logic: DataAccessFilterLogic) { |
||||
|
formModel.value.filterGroup.groups.push({ |
||||
|
groups: [], |
||||
|
logic, |
||||
|
rules: [], |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function onDeleteGroup(index: number) { |
||||
|
formModel.value.filterGroup.groups.splice(index, 1); |
||||
|
} |
||||
|
|
||||
|
function onNewRule(group: DataAccessFilterGroup) { |
||||
|
group.rules.push({ |
||||
|
field: '', |
||||
|
isLeft: false, |
||||
|
javaScriptType: '', |
||||
|
operate: DataAccessFilterOperate.Equal, |
||||
|
typeFullName: '', |
||||
|
value: '', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function onDeleteRule(group: DataAccessFilterGroup, index: number) { |
||||
|
group.rules.splice(index, 1); |
||||
|
} |
||||
|
|
||||
|
async function onSubmit() { |
||||
|
try { |
||||
|
await form.value?.validate(); |
||||
|
drawerApi.setState({ confirmLoading: true }); |
||||
|
const input = toValue(formModel); |
||||
|
await (input.strategy === DataAccessStrategy.Custom |
||||
|
? submitCustomRule(input) |
||||
|
: submitStrategys(input)); |
||||
|
message.success($t('AbpUi.SavedSuccessfully')); |
||||
|
drawerApi.close(); |
||||
|
} finally { |
||||
|
drawerApi.setState({ confirmLoading: false }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function submitStrategys(input: RoleEntityRuleDto) { |
||||
|
const roleInfo = drawerApi.getData<IdentityRoleDto>(); |
||||
|
await setStrategysApi({ |
||||
|
isEnabled: input.isEnabled, |
||||
|
strategy: input.strategy, |
||||
|
subjectId: roleInfo.name, |
||||
|
subjectName: 'R', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
async function submitCustomRule(input: RoleEntityRuleDto) { |
||||
|
const roleInfo = drawerApi.getData<IdentityRoleDto>(); |
||||
|
const api = input.id |
||||
|
? updateApi(input.id, { |
||||
|
accessedProperties: input.accessedProperties, |
||||
|
entityTypeId: input.entityTypeId, |
||||
|
filterGroup: input.filterGroup, |
||||
|
isEnabled: input.isEnabled, |
||||
|
operation: input.operation, |
||||
|
roleId: roleInfo.id, |
||||
|
roleName: roleInfo.id, |
||||
|
}) |
||||
|
: createApi({ |
||||
|
accessedProperties: input.accessedProperties, |
||||
|
entityTypeId: input.entityTypeId, |
||||
|
filterGroup: input.filterGroup, |
||||
|
isEnabled: input.isEnabled, |
||||
|
operation: input.operation, |
||||
|
roleId: roleInfo.id, |
||||
|
roleName: roleInfo.name, |
||||
|
}); |
||||
|
await api; |
||||
|
} |
||||
|
|
||||
|
async function onGetEntityType() { |
||||
|
try { |
||||
|
drawerApi.setState({ confirmLoading: true, loading: true }); |
||||
|
const { entityTypeId, operation } = formModel.value; |
||||
|
const roleInfo = drawerApi.getData<IdentityRoleDto>(); |
||||
|
const ruleEntityRule = await getRoleEntityRuleApi({ |
||||
|
entityTypeId, |
||||
|
operation, |
||||
|
roleName: roleInfo.name, |
||||
|
}); |
||||
|
formModel.value = ruleEntityRule?.id |
||||
|
? { |
||||
|
...ruleEntityRule, |
||||
|
strategy: DataAccessStrategy.Custom, |
||||
|
} |
||||
|
: { |
||||
|
...defaultModel, |
||||
|
entityTypeId, |
||||
|
operation, |
||||
|
}; |
||||
|
} finally { |
||||
|
drawerApi.setState({ confirmLoading: false, loading: false }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function onStrategyChange(strategy: DataAccessStrategy) { |
||||
|
if (strategy === DataAccessStrategy.Custom) { |
||||
|
await onInitEntityTypes(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function onEntityTypeChange(entityTypeId: string) { |
||||
|
const entityTypeInfo = await getEntityTypeInfoApi(entityTypeId); |
||||
|
entityTypeProps.value = entityTypeInfo.properties.map((item) => { |
||||
|
const displayName = deserialize(item.displayName); |
||||
|
if (item.enums) { |
||||
|
item.enums = item.enums.map((enumItem) => { |
||||
|
const enumDisplayName = deserialize(enumItem.displayName); |
||||
|
return { |
||||
|
...enumItem, |
||||
|
displayName: Lr(enumDisplayName.resourceName, enumDisplayName.name), |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
return { |
||||
|
...item, |
||||
|
displayName: Lr(displayName.resourceName, displayName.name), |
||||
|
}; |
||||
|
}); |
||||
|
await onGetEntityType(); |
||||
|
} |
||||
|
|
||||
|
// 属性选择改变触发对比值输入类型变化 |
||||
|
function onPropertyTypeChange( |
||||
|
rule: DataAccessFilterRule, |
||||
|
option: EntityPropertyInfoDto, |
||||
|
) { |
||||
|
rule.value = ''; |
||||
|
rule.typeFullName = option.typeFullName; |
||||
|
rule.javaScriptType = option.javaScriptType; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Drawer title="数据权限"> |
||||
|
<Form |
||||
|
:model="formModel" |
||||
|
:label-col="{ span: 6 }" |
||||
|
:wrapper-col="{ span: 24 }" |
||||
|
> |
||||
|
<FormItem name="isEnabled" label="是否启用"> |
||||
|
<Checkbox v-model:checked="formModel.isEnabled">是否启用</Checkbox> |
||||
|
</FormItem> |
||||
|
<FormItem name="isEnabled" label="数据权限策略"> |
||||
|
<Select |
||||
|
class="w-full" |
||||
|
:options="strategyOptions" |
||||
|
v-model:value="formModel.strategy" |
||||
|
@change="(value) => onStrategyChange(Number(value!))" |
||||
|
/> |
||||
|
</FormItem> |
||||
|
<template v-if="formModel.strategy === DataAccessStrategy.Custom"> |
||||
|
<FormItem required name="entityTypeId" label="实体类型"> |
||||
|
<Select |
||||
|
class="w-full" |
||||
|
:options="entityTypes" |
||||
|
:field-names="{ label: 'displayName', value: 'id' }" |
||||
|
v-model:value="formModel.entityTypeId" |
||||
|
@change="(value) => onEntityTypeChange(value!.toString())" |
||||
|
/> |
||||
|
</FormItem> |
||||
|
<FormItem required name="operation" label="操作"> |
||||
|
<Select |
||||
|
class="w-full" |
||||
|
:options="operationOptions" |
||||
|
v-model:value="formModel.operation" |
||||
|
@change="onGetEntityType" |
||||
|
/> |
||||
|
</FormItem> |
||||
|
<FormItem name="accessedProperties" label="可访问字段"> |
||||
|
<Select |
||||
|
class="w-full" |
||||
|
mode="multiple" |
||||
|
:options="entityTypeProps" |
||||
|
v-model:value="formModel.accessedProperties" |
||||
|
:field-names="{ label: 'displayName', value: 'name' }" |
||||
|
/> |
||||
|
</FormItem> |
||||
|
<FormItemRest> |
||||
|
<!-- 分组 --> |
||||
|
<div class="flex w-full flex-col"> |
||||
|
<div class="w-full items-center divide-dashed border-2"> |
||||
|
<div class="m-2 text-sm">数据访问规则</div> |
||||
|
<template |
||||
|
v-for="(group, gi) in formModel.filterGroup.groups" |
||||
|
:key="gi" |
||||
|
> |
||||
|
<div class="flex flex-row items-center justify-items-center"> |
||||
|
<!-- 条件 --> |
||||
|
<div class="m-2 flex w-full flex-row"> |
||||
|
<div class="w-full border-2 border-dashed"> |
||||
|
<template v-for="(rule, ri) in group.rules" :key="ri"> |
||||
|
<div |
||||
|
class="m-2 flex flex-row items-center justify-items-center gap-1" |
||||
|
> |
||||
|
<div class="basis-2/5"> |
||||
|
<Select |
||||
|
class="w-full" |
||||
|
:options="entityTypeProps" |
||||
|
v-model:value="rule.field" |
||||
|
:field-names="{ |
||||
|
label: 'displayName', |
||||
|
value: 'name', |
||||
|
}" |
||||
|
@change=" |
||||
|
(_, option: any) => |
||||
|
onPropertyTypeChange(rule, option) |
||||
|
" |
||||
|
/> |
||||
|
</div> |
||||
|
<div class="basis-1/5"> |
||||
|
<Select |
||||
|
class="w-full" |
||||
|
:options="dataAccessFilterOptions" |
||||
|
v-model:value="rule.operate" |
||||
|
/> |
||||
|
</div> |
||||
|
<div class="basis-2/5"> |
||||
|
<InputNumber |
||||
|
v-if="rule.javaScriptType === 'number'" |
||||
|
class="w-full" |
||||
|
v-model:value="rule.value" |
||||
|
/> |
||||
|
<Switch |
||||
|
v-else-if="rule.javaScriptType === 'boolean'" |
||||
|
v-model:checked="rule.value" |
||||
|
/> |
||||
|
<DatePicker |
||||
|
v-else-if="rule.javaScriptType === 'Date'" |
||||
|
class="w-full" |
||||
|
v-model:value="rule.value" |
||||
|
value-format="YYYY-MM-DDT00:00:00" |
||||
|
/> |
||||
|
<Input |
||||
|
v-else |
||||
|
class="w-full" |
||||
|
v-model:value="rule.value" |
||||
|
/> |
||||
|
</div> |
||||
|
<div class="basis-1/5"> |
||||
|
<Popconfirm |
||||
|
title="你确定吗?" |
||||
|
description="将删除此过滤条件" |
||||
|
@confirm="onDeleteRule(group, ri)" |
||||
|
> |
||||
|
<Button |
||||
|
type="link" |
||||
|
danger |
||||
|
:icon="h(DeleteOutlined)" |
||||
|
/> |
||||
|
</Popconfirm> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<div |
||||
|
class="flex flex-row items-center justify-items-center gap-2" |
||||
|
> |
||||
|
<div class="m-2 min-w-[60px]"> |
||||
|
<Select |
||||
|
size="small" |
||||
|
class="w-full" |
||||
|
v-model:value="group.logic" |
||||
|
:options="logicOptions" |
||||
|
/> |
||||
|
</div> |
||||
|
<Button |
||||
|
type="link" |
||||
|
:icon="h(PlusOutlined)" |
||||
|
@click="onNewRule(group)" |
||||
|
> |
||||
|
增加条件 |
||||
|
</Button> |
||||
|
<Popconfirm |
||||
|
title="你确定吗?" |
||||
|
description="将删除此条件分组" |
||||
|
@confirm="onDeleteGroup(gi)" |
||||
|
> |
||||
|
<Button type="link" :icon="h(DeleteOutlined)" danger> |
||||
|
删除分组 |
||||
|
</Button> |
||||
|
</Popconfirm> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<div |
||||
|
class="flex flex-row items-center justify-items-center gap-2" |
||||
|
> |
||||
|
<div class="m-2 min-w-[60px]"> |
||||
|
<Select |
||||
|
size="small" |
||||
|
class="w-full" |
||||
|
v-model:value="formModel.filterGroup.logic" |
||||
|
:options="logicOptions" |
||||
|
/> |
||||
|
</div> |
||||
|
<div> |
||||
|
<Button |
||||
|
size="small" |
||||
|
type="link" |
||||
|
:icon="h(PlusOutlined)" |
||||
|
@click="onNewGroup(formModel.filterGroup.logic)" |
||||
|
> |
||||
|
增加分组 |
||||
|
</Button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</FormItemRest> |
||||
|
</template> |
||||
|
</Form> |
||||
|
</Drawer> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
Loading…
Reference in new issue