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 { useEditionsApi } from './useEditionsApi'; |
||||
export * from './useTenantsApi'; |
export { useTenantsApi } from './useTenantsApi'; |
||||
|
|||||
Loading…
Reference in new issue