17 changed files with 497 additions and 12 deletions
@ -0,0 +1,39 @@ |
|||
{ |
|||
"name": "@abp/features", |
|||
"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/features" |
|||
}, |
|||
"license": "MIT", |
|||
"type": "module", |
|||
"sideEffects": [ |
|||
"**/*.css" |
|||
], |
|||
"exports": { |
|||
".": { |
|||
"types": "./src/index.ts", |
|||
"default": "./src/index.ts" |
|||
} |
|||
}, |
|||
"dependencies": { |
|||
"@abp/auditing": "workspace:*", |
|||
"@abp/core": "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:*", |
|||
"vxe-table": "catalog:" |
|||
}, |
|||
"devDependencies": {} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export { useFeaturesApi } from './useFeaturesApi'; |
|||
@ -0,0 +1,62 @@ |
|||
import type { |
|||
FeatureProvider, |
|||
GetFeatureListResultDto, |
|||
UpdateFeaturesDto, |
|||
} from '../types/features'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function useFeaturesApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 删除功能 |
|||
* @param {FeatureProvider} provider 参数 |
|||
* @returns {Promise<void>} |
|||
*/ |
|||
function deleteApi(provider: FeatureProvider): Promise<void> { |
|||
return request(`/api/feature-management/features`, { |
|||
method: 'DELETE', |
|||
params: provider, |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 查询功能 |
|||
* @param {FeatureProvider} provider 参数 |
|||
* @returns {Promise<GetFeatureListResultDto>} 功能实体数据传输对象 |
|||
*/ |
|||
function getApi(provider: FeatureProvider): Promise<GetFeatureListResultDto> { |
|||
return request<GetFeatureListResultDto>( |
|||
`/api/feature-management/features`, |
|||
{ |
|||
method: 'GET', |
|||
params: provider, |
|||
}, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* 更新功能 |
|||
* @param {FeatureProvider} provider |
|||
* @param {UpdateFeaturesDto} input 参数 |
|||
* @returns {Promise<void>} |
|||
*/ |
|||
function updateApi( |
|||
provider: FeatureProvider, |
|||
input: UpdateFeaturesDto, |
|||
): Promise<void> { |
|||
return request(`/api/feature-management/features`, { |
|||
data: input, |
|||
method: 'PUT', |
|||
params: provider, |
|||
}); |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
deleteApi, |
|||
getApi, |
|||
updateApi, |
|||
}; |
|||
} |
|||
@ -0,0 +1,258 @@ |
|||
<script setup lang="ts"> |
|||
import type { SelectionStringValueType, Validator } from '@abp/core'; |
|||
import type { FormInstance } from 'ant-design-vue/es/form/Form'; |
|||
|
|||
import type { FeatureGroupDto, UpdateFeaturesDto } from '../../types/features'; |
|||
|
|||
import { computed, ref, toValue, useTemplateRef } from 'vue'; |
|||
|
|||
import { useVbenModal } from '@vben/common-ui'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { useValidation } from '@abp/core'; |
|||
import { |
|||
Checkbox, |
|||
Form, |
|||
Input, |
|||
InputNumber, |
|||
message, |
|||
Select, |
|||
Tabs, |
|||
} from 'ant-design-vue'; |
|||
|
|||
import { useFeaturesApi } from '../../api/useFeaturesApi'; |
|||
|
|||
const FormItem = Form.Item; |
|||
const TabPane = Tabs.TabPane; |
|||
|
|||
interface ModalState { |
|||
displayName?: string; |
|||
providerKey?: string; |
|||
providerName: string; |
|||
readonly?: boolean; |
|||
} |
|||
|
|||
interface FormModel { |
|||
groups: FeatureGroupDto[]; |
|||
} |
|||
|
|||
const activeTabKey = ref(''); |
|||
const modelState = ref<ModalState>(); |
|||
const formModel = ref<FormModel>({ groups: [] }); |
|||
const form = useTemplateRef<FormInstance>('form'); |
|||
|
|||
const getModalTitle = computed(() => { |
|||
if (modelState.value?.displayName) { |
|||
return `${$t('AbpFeatureManagement.Features')} - ${modelState.value.displayName}`; |
|||
} |
|||
return $t('AbpFeatureManagement.Features'); |
|||
}); |
|||
|
|||
const { getApi, updateApi } = useFeaturesApi(); |
|||
const { |
|||
fieldMustBeetWeen, |
|||
fieldMustBeStringWithMinimumLengthAndMaximumLength, |
|||
fieldRequired, |
|||
} = useValidation(); |
|||
|
|||
const [Modal, modalApi] = useVbenModal({ |
|||
centered: true, |
|||
class: 'w-1/2', |
|||
async onConfirm() { |
|||
await form.value?.validate(); |
|||
await onSubmit(); |
|||
}, |
|||
async onOpenChange(isOpen) { |
|||
if (isOpen) { |
|||
formModel.value = { groups: [] }; |
|||
await onGet(); |
|||
if (formModel.value?.groups.length > 0) { |
|||
activeTabKey.value = formModel.value.groups[0]?.name!; |
|||
} |
|||
} |
|||
}, |
|||
}); |
|||
function mapFeatures(groups: FeatureGroupDto[]) { |
|||
groups.forEach((group) => { |
|||
group.features.forEach((feature) => { |
|||
if (feature.valueType.name === 'SelectionStringValueType') { |
|||
const valueType = |
|||
feature.valueType as unknown as SelectionStringValueType; |
|||
valueType.itemSource.items.forEach((valueItem) => { |
|||
valueItem.displayName = $t( |
|||
`${valueItem.displayText.resourceName}.${valueItem.displayText.name}`, |
|||
); |
|||
}); |
|||
} else { |
|||
switch (feature.valueType?.validator.name) { |
|||
case 'BOOLEAN': { |
|||
feature.value = |
|||
String(feature.value).toLocaleLowerCase() === 'true'; |
|||
break; |
|||
} |
|||
case 'NUMERIC': { |
|||
feature.value = Number(feature.value); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
}); |
|||
return groups; |
|||
} |
|||
function getFeatureInput(groups: FeatureGroupDto[]): UpdateFeaturesDto { |
|||
const input: UpdateFeaturesDto = { |
|||
features: [], |
|||
}; |
|||
groups.forEach((g) => { |
|||
g.features.forEach((f) => { |
|||
if (f.value !== null) { |
|||
input.features.push({ |
|||
name: f.name, |
|||
value: String(f.value), |
|||
}); |
|||
} |
|||
}); |
|||
}); |
|||
return input; |
|||
} |
|||
function createRules(field: string, validator: Validator) { |
|||
const featureRules: { [key: string]: any }[] = []; |
|||
if (validator.properties) { |
|||
switch (validator.name) { |
|||
case 'NUMERIC': { |
|||
featureRules.push( |
|||
...fieldMustBeetWeen({ |
|||
end: Number(validator.properties.MaxValue), |
|||
name: field, |
|||
start: Number(validator.properties.MinValue), |
|||
trigger: 'change', |
|||
}), |
|||
); |
|||
break; |
|||
} |
|||
case 'STRING': { |
|||
if ( |
|||
validator.properties.AllowNull && |
|||
validator.properties.AllowNull.toLowerCase() === 'true' |
|||
) { |
|||
featureRules.push( |
|||
...fieldRequired({ |
|||
name: field, |
|||
trigger: 'blur', |
|||
}), |
|||
); |
|||
} |
|||
featureRules.push( |
|||
...fieldMustBeStringWithMinimumLengthAndMaximumLength({ |
|||
maximum: Number(validator.properties.MaxValue), |
|||
minimum: Number(validator.properties.MinValue), |
|||
name: field, |
|||
trigger: 'blur', |
|||
}), |
|||
); |
|||
break; |
|||
} |
|||
default: { |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
return featureRules; |
|||
} |
|||
async function onGet() { |
|||
try { |
|||
modalApi.setState({ loading: true }); |
|||
const state = modalApi.getData<ModalState>(); |
|||
const { groups } = await getApi({ |
|||
providerKey: state.providerKey, |
|||
providerName: state.providerName, |
|||
}); |
|||
formModel.value = { |
|||
groups: mapFeatures(groups), |
|||
}; |
|||
modelState.value = state; |
|||
} finally { |
|||
modalApi.setState({ loading: false }); |
|||
} |
|||
} |
|||
async function onSubmit() { |
|||
try { |
|||
modalApi.setState({ submitting: true }); |
|||
const model = toValue(formModel); |
|||
const state = modalApi.getData<ModalState>(); |
|||
const input = getFeatureInput(model.groups); |
|||
await updateApi( |
|||
{ |
|||
providerKey: state.providerKey, |
|||
providerName: state.providerName, |
|||
}, |
|||
input, |
|||
); |
|||
message.success($t('AbpUi.SavedSuccessfully')); |
|||
await onGet(); |
|||
} finally { |
|||
modalApi.setState({ submitting: false }); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Modal :title="getModalTitle"> |
|||
<Form :model="formModel" ref="form"> |
|||
<Tabs tab-position="left" v-model:active-key="activeTabKey"> |
|||
<TabPane |
|||
v-for="(group, gi) in formModel.groups" |
|||
:key="group.name" |
|||
:tab="group.displayName" |
|||
> |
|||
<template v-for="(feature, fi) in group.features" :key="feature.name"> |
|||
<FormItem |
|||
v-if="feature.valueType !== null" |
|||
:name="['groups', gi, 'features', fi, 'value']" |
|||
:label="feature.displayName" |
|||
:extra="feature.description" |
|||
:rules=" |
|||
createRules(feature.displayName, feature.valueType.validator) |
|||
" |
|||
> |
|||
<Checkbox |
|||
v-if=" |
|||
feature.valueType.name === 'ToggleStringValueType' && |
|||
feature.valueType.validator.name === 'BOOLEAN' |
|||
" |
|||
v-model:checked="feature.value" |
|||
> |
|||
{{ feature.displayName }} |
|||
</Checkbox> |
|||
<div |
|||
v-else-if="feature.valueType.name === 'FreeTextStringValueType'" |
|||
> |
|||
<InputNumber |
|||
v-if="feature.valueType.validator.name === 'NUMERIC'" |
|||
style="width: 100%" |
|||
v-model:value="feature.value" |
|||
/> |
|||
<Input |
|||
v-else |
|||
v-model:value="feature.value" |
|||
autocomplete="off" |
|||
/> |
|||
</div> |
|||
<Select |
|||
v-else-if=" |
|||
feature.valueType.name === 'SelectionStringValueType' |
|||
" |
|||
v-model:value="feature.value" |
|||
:options="feature.valueType.itemSource.items" |
|||
:field-names="{ label: 'displayName', value: 'value' }" |
|||
/> |
|||
</FormItem> |
|||
</template> |
|||
</TabPane> |
|||
</Tabs> |
|||
</Form> |
|||
</Modal> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1 @@ |
|||
export { default as FeatureModal } from './features/FeatureModal.vue'; |
|||
@ -0,0 +1 @@ |
|||
export * from './permissions'; |
|||
@ -0,0 +1,4 @@ |
|||
export * from './api'; |
|||
export * from './components'; |
|||
export * from './constants'; |
|||
export * from './types'; |
|||
@ -0,0 +1,63 @@ |
|||
import type { Dictionary, NameValue } from '@abp/core'; |
|||
|
|||
interface FeatureProvider { |
|||
providerKey?: string; |
|||
providerName: string; |
|||
} |
|||
|
|||
interface IValueValidator { |
|||
[key: string]: any; |
|||
isValid(value?: any): boolean; |
|||
name: string; |
|||
properties: Dictionary<string, any>; |
|||
} |
|||
|
|||
interface IStringValueType { |
|||
[key: string]: any; |
|||
name: string; |
|||
properties: Dictionary<string, any>; |
|||
validator: IValueValidator; |
|||
} |
|||
|
|||
interface FeatureProviderDto { |
|||
key: string; |
|||
name: string; |
|||
} |
|||
|
|||
interface FeatureDto { |
|||
depth: number; |
|||
description?: string; |
|||
displayName: string; |
|||
name: string; |
|||
parentName?: string; |
|||
provider: FeatureProviderDto; |
|||
value?: any; |
|||
valueType: IStringValueType; |
|||
} |
|||
|
|||
interface FeatureGroupDto { |
|||
displayName: string; |
|||
features: FeatureDto[]; |
|||
name: string; |
|||
} |
|||
|
|||
interface GetFeatureListResultDto { |
|||
groups: FeatureGroupDto[]; |
|||
} |
|||
|
|||
type UpdateFeatureDto = NameValue<string>; |
|||
|
|||
interface UpdateFeaturesDto { |
|||
features: UpdateFeatureDto[]; |
|||
} |
|||
|
|||
export type { |
|||
FeatureDto, |
|||
FeatureGroupDto, |
|||
FeatureProvider, |
|||
GetFeatureListResultDto, |
|||
IStringValueType, |
|||
IValueValidator, |
|||
UpdateFeatureDto, |
|||
UpdateFeaturesDto, |
|||
}; |
|||
@ -0,0 +1 @@ |
|||
export * from './features'; |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/tsconfig", |
|||
"extends": "@vben/tsconfig/web.json", |
|||
"include": ["src"], |
|||
"exclude": ["node_modules"] |
|||
} |
|||
@ -1,2 +1,2 @@ |
|||
export * from './useEditionsApi'; |
|||
export * from './useTenantsApi'; |
|||
export { useEditionsApi } from './useEditionsApi'; |
|||
export { useTenantsApi } from './useTenantsApi'; |
|||
|
|||
Loading…
Reference in new issue