Browse Source

Merge pull request #1186 from colinin/vben5-menu-manage

Vben5 menu manage
pull/1211/head
yx lin 11 months ago
committed by GitHub
parent
commit
3c22aea4f3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json
  2. 4
      apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json
  3. 18
      apps/vben5/apps/app-antd/src/router/routes/modules/abp.ts
  4. 15
      apps/vben5/apps/app-antd/src/views/platform/layouts/index.vue
  5. 15
      apps/vben5/apps/app-antd/src/views/platform/menus/index.vue
  6. 2
      apps/vben5/packages/@abp/platform/src/api/index.ts
  7. 58
      apps/vben5/packages/@abp/platform/src/api/useLayoutsApi.ts
  8. 56
      apps/vben5/packages/@abp/platform/src/api/useMenusApi.ts
  9. 2
      apps/vben5/packages/@abp/platform/src/components/data-dictionaries/DataDictionaryItemDrawer.vue
  10. 2
      apps/vben5/packages/@abp/platform/src/components/index.ts
  11. 177
      apps/vben5/packages/@abp/platform/src/components/layouts/LayoutModal.vue
  12. 206
      apps/vben5/packages/@abp/platform/src/components/layouts/LayoutTable.vue
  13. 459
      apps/vben5/packages/@abp/platform/src/components/menus/MenuDrawer.vue
  14. 290
      apps/vben5/packages/@abp/platform/src/components/menus/MenuTable.vue
  15. 2
      apps/vben5/packages/@abp/platform/src/types/index.ts
  16. 35
      apps/vben5/packages/@abp/platform/src/types/layouts.ts
  17. 39
      apps/vben5/packages/@abp/platform/src/types/menus.ts
  18. 12
      apps/vben5/packages/@abp/platform/src/types/routes.ts

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

@ -98,7 +98,9 @@
"email": "Email Messages",
"sms": "Sms Messages"
},
"dataDictionaries": "Data Dictionaries"
"dataDictionaries": "Data Dictionaries",
"layouts": "Layouts",
"menus": "Menus"
},
"saas": {
"title": "Saas",

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

@ -98,7 +98,9 @@
"email": "邮件消息",
"sms": "短信消息"
},
"dataDictionaries": "数据字典"
"dataDictionaries": "数据字典",
"layouts": "布局管理",
"menus": "菜单管理"
},
"saas": {
"title": "Saas",

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

@ -409,6 +409,24 @@ const routes: RouteRecordRaw[] = [
component: () =>
import('#/views/platform/data-dictionaries/index.vue'),
},
{
meta: {
title: $t('abp.platform.layouts'),
icon: 'material-symbols-light:responsive-layout',
},
name: 'PlatformLayouts',
path: '/platform/layouts',
component: () => import('#/views/platform/layouts/index.vue'),
},
{
meta: {
title: $t('abp.platform.menus'),
icon: 'material-symbols-light:menu',
},
name: 'PlatformMenus',
path: '/platform/menus',
component: () => import('#/views/platform/menus/index.vue'),
},
{
meta: {
title: $t('abp.platform.messages.title'),

15
apps/vben5/apps/app-antd/src/views/platform/layouts/index.vue

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

15
apps/vben5/apps/app-antd/src/views/platform/menus/index.vue

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

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

@ -1,3 +1,5 @@
export { useDataDictionariesApi } from './useDataDictionariesApi';
export { useEmailMessagesApi } from './useEmailMessagesApi';
export { useLayoutsApi } from './useLayoutsApi';
export { useMenusApi } from './useMenusApi';
export { useSmsMessagesApi } from './useSmsMessagesApi';

58
apps/vben5/packages/@abp/platform/src/api/useLayoutsApi.ts

@ -0,0 +1,58 @@
import type { PagedResultDto } from '@abp/core';
import type {
LayoutCreateDto,
LayoutDto,
LayoutGetPagedListInput,
LayoutUpdateDto,
} from '../types/layouts';
import { useRequest } from '@abp/request';
export function useLayoutsApi() {
const { cancel, request } = useRequest();
function createApi(input: LayoutCreateDto): Promise<LayoutDto> {
return request<LayoutDto>(`/api/platform/layouts`, {
data: input,
method: 'POST',
});
}
function getPagedListApi(
input?: LayoutGetPagedListInput,
): Promise<PagedResultDto<LayoutDto>> {
return request<PagedResultDto<LayoutDto>>('/api/platform/layouts', {
method: 'GET',
params: input,
});
}
function getApi(id: string): Promise<LayoutDto> {
return request<LayoutDto>(`/api/platform/layouts/${id}`, {
method: 'GET',
});
}
function deleteApi(id: string): Promise<void> {
return request(`/api/platform/layouts/${id}`, {
method: 'DELETE',
});
}
function updateApi(id: string, input: LayoutUpdateDto): Promise<LayoutDto> {
return request<LayoutDto>(`/api/platform/layouts/${id}`, {
data: input,
method: 'PUT',
});
}
return {
cancel,
createApi,
deleteApi,
getApi,
getPagedListApi,
updateApi,
};
}

56
apps/vben5/packages/@abp/platform/src/api/useMenusApi.ts

@ -0,0 +1,56 @@
import type { ListResultDto } from '@abp/core';
import type {
MenuCreateDto,
MenuDto,
MenuGetAllInput,
MenuUpdateDto,
} from '../types/menus';
import { useRequest } from '@abp/request';
export function useMenusApi() {
const { cancel, request } = useRequest();
function createApi(input: MenuCreateDto): Promise<MenuDto> {
return request<MenuDto>(`/api/platform/menus`, {
data: input,
method: 'POST',
});
}
function getAllApi(input?: MenuGetAllInput): Promise<ListResultDto<MenuDto>> {
return request<ListResultDto<MenuDto>>('/api/platform/menus/all', {
method: 'GET',
params: input,
});
}
function getApi(id: string): Promise<MenuDto> {
return request<MenuDto>(`/api/platform/menus/${id}`, {
method: 'GET',
});
}
function deleteApi(id: string): Promise<void> {
return request(`/api/platform/menus/${id}`, {
method: 'DELETE',
});
}
function updateApi(id: string, input: MenuUpdateDto): Promise<MenuDto> {
return request<MenuDto>(`/api/platform/menus/${id}`, {
data: input,
method: 'PUT',
});
}
return {
cancel,
createApi,
deleteApi,
getAllApi,
getApi,
updateApi,
};
}

2
apps/vben5/packages/@abp/platform/src/components/data-dictionaries/DataDictionaryItemDrawer.vue

@ -41,7 +41,7 @@ const valueTypeMaps: { [key: number]: string } = {
};
const [Drawer, drawerApi] = useVbenDrawer({
class: 'w-1/2',
class: 'w-2/3',
async onOpenChange(isOpen) {
if (isOpen) {
const { name } = drawerApi.getData<DataDto>();

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

@ -1,3 +1,5 @@
export { default as DataDictionaryTable } from './data-dictionaries/DataDictionaryTable.vue';
export { default as LayoutTable } from './layouts/LayoutTable.vue';
export { default as MenuTable } from './menus/MenuTable.vue';
export { default as EmailMessageTable } from './messages/email/EmailMessageTable.vue';
export { default as SmsMessageTable } from './messages/sms/SmsMessageTable.vue';

177
apps/vben5/packages/@abp/platform/src/components/layouts/LayoutModal.vue

@ -0,0 +1,177 @@
<script setup lang="ts">
import type {
LayoutCreateDto,
LayoutDto,
LayoutUpdateDto,
} from '../../types/layouts';
import { useVbenForm, useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { isNullOrWhiteSpace, listToTree } from '@abp/core';
import { message } from 'ant-design-vue';
import { useDataDictionariesApi, useLayoutsApi } from '../../api';
const emits = defineEmits<{
(event: 'change', data: LayoutDto): void;
}>();
const { createApi, getApi, updateApi } = useLayoutsApi();
const {
getAllApi: getDataDictionariesApi,
getByNameApi: getDataDictionaryByNameApi,
} = useDataDictionariesApi();
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
},
handleSubmit: onSubmit,
schema: [
{
component: 'Input',
dependencies: {
show: false,
triggerFields: ['name'],
},
fieldName: 'id',
},
{
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: () => getDataDictionaryByNameApi('UI Framework'),
labelField: 'displayName',
resultField: 'items',
valueField: 'name',
},
dependencies: {
if: (values) => {
return !values?.id;
},
triggerFields: ['id'],
},
fieldName: 'framework',
label: $t('AppPlatform.DisplayName:UIFramework'),
rules: 'selectRequired',
},
{
component: 'ApiTreeSelect',
componentProps: {
allowClear: true,
api: async () => {
const { items } = await getDataDictionariesApi();
return listToTree(items, {
id: 'id',
pid: 'parentId',
});
},
labelField: 'displayName',
valueField: 'id',
childrenField: 'children',
},
dependencies: {
if: (values) => {
return !values?.id;
},
triggerFields: ['id'],
},
fieldName: 'dataId',
label: $t('AppPlatform.DisplayName:LayoutConstraint'),
rules: 'selectRequired',
},
{
component: 'Input',
fieldName: 'name',
label: $t('AppPlatform.DisplayName:Name'),
rules: 'required',
},
{
component: 'Input',
fieldName: 'displayName',
label: $t('AppPlatform.DisplayName:DisplayName'),
rules: 'required',
},
{
component: 'Input',
fieldName: 'path',
label: $t('AppPlatform.DisplayName:Path'),
rules: 'required',
},
{
component: 'Input',
fieldName: 'redirect',
label: $t('AppPlatform.DisplayName:Redirect'),
},
{
component: 'Textarea',
componentProps: {
autoSize: {
minRows: 3,
},
},
fieldName: 'description',
label: $t('AppPlatform.DisplayName:Description'),
},
],
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
closeOnClickModal: false,
closeOnPressEscape: false,
draggable: true,
onConfirm: async () => {
await formApi.validateAndSubmitForm();
},
onOpenChange: async (isOpen) => {
if (isOpen) {
await onGet();
}
},
});
async function onGet() {
formApi.resetForm();
const { id } = modalApi.getData<LayoutDto>();
if (isNullOrWhiteSpace(id)) {
modalApi.setState({ title: $t('AppPlatform.Layout:AddNew') });
return;
}
try {
modalApi.setState({ loading: true });
const dto = await getApi(id);
formApi.setValues(dto, false);
modalApi.setState({
title: `${$t('AppPlatform.Layout:Edit')} - ${dto.displayName}`,
});
} finally {
modalApi.setState({ loading: false });
}
}
async function onSubmit(values: Record<string, any>) {
try {
modalApi.setState({ submitting: true });
const api = values.id
? updateApi(values.id, values as LayoutUpdateDto)
: createApi(values as LayoutCreateDto);
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>

206
apps/vben5/packages/@abp/platform/src/components/layouts/LayoutTable.vue

@ -0,0 +1,206 @@
<script setup lang="ts">
import type { VxeGridProps } from '@abp/ui';
import type { VbenFormProps } from '@vben/common-ui';
import type { LayoutDto } from '../../types/layouts';
import { defineAsyncComponent, h } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { useVbenVxeGrid } from '@abp/ui';
import {
DeleteOutlined,
EditOutlined,
PlusOutlined,
} from '@ant-design/icons-vue';
import { Button, message, Modal } from 'ant-design-vue';
import { useLayoutsApi } from '../../api/useLayoutsApi';
defineOptions({
name: 'LayoutTable',
});
const { deleteApi, getPagedListApi } = useLayoutsApi();
const formOptions: VbenFormProps = {
//
collapsed: true,
//
commonConfig: {
// label
colon: true,
//
componentProps: {
class: 'w-full',
},
},
schema: [
{
component: 'Input',
fieldName: 'filter',
formItemClass: 'col-span-2 items-baseline',
label: $t('AbpUi.Search'),
},
],
//
showCollapseButton: true,
//
submitOnEnter: true,
};
const gridOptions: VxeGridProps<LayoutDto> = {
columns: [
{
align: 'center',
fixed: 'left',
type: 'seq',
width: 80,
},
{
align: 'left',
field: 'name',
fixed: 'left',
minWidth: 180,
title: $t('AppPlatform.DisplayName:Name'),
},
{
align: 'left',
field: 'displayName',
minWidth: 150,
title: $t('AppPlatform.DisplayName:DisplayName'),
},
{
align: 'center',
field: 'path',
minWidth: 200,
title: $t('AppPlatform.DisplayName:Path'),
},
{
align: 'left',
field: 'framework',
minWidth: 180,
title: $t('AppPlatform.DisplayName:UIFramework'),
},
{
align: 'left',
field: 'description',
minWidth: 220,
title: $t('AppPlatform.DisplayName:Description'),
},
{
align: 'left',
field: 'redirect',
minWidth: 160,
title: $t('AppPlatform.DisplayName:Redirect'),
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: $t('AbpUi.Actions'),
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,
refresh: true,
zoom: true,
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions,
});
const [LayoutModal, modalApi] = useVbenModal({
connectedComponent: defineAsyncComponent(() => import('./LayoutModal.vue')),
});
function onCreate() {
modalApi.setData({});
modalApi.open();
}
function onUpdate(row: LayoutDto) {
modalApi.setData(row);
modalApi.open();
}
function onDelete(row: LayoutDto) {
Modal.confirm({
afterClose: () => {
gridApi.setLoading(false);
},
centered: true,
content: `${$t('AbpUi.ItemWillBeDeletedMessage')}`,
onOk: async () => {
try {
gridApi.setLoading(true);
await deleteApi(row.id);
message.success($t('AbpUi.DeletedSuccessfully'));
gridApi.query();
} finally {
gridApi.setLoading(false);
}
},
title: $t('AbpUi.AreYouSure'),
});
}
</script>
<template>
<Grid :table-title="$t('AppPlatform.DisplayName:Layout')">
<template #toolbar-tools>
<Button :icon="h(PlusOutlined)" type="primary" @click="onCreate">
{{ $t('AppPlatform.Layout:AddNew') }}
</Button>
</template>
<template #action="{ row }">
<div class="flex flex-row">
<Button
:icon="h(EditOutlined)"
block
type="link"
@click="onUpdate(row)"
>
{{ $t('AbpUi.Edit') }}
</Button>
<Button
:icon="h(DeleteOutlined)"
block
danger
type="link"
@click="onDelete(row)"
>
{{ $t('AbpUi.Delete') }}
</Button>
</div>
</template>
</Grid>
<LayoutModal @change="() => gridApi.query()" />
</template>
<style lang="scss" scoped></style>

459
apps/vben5/packages/@abp/platform/src/components/menus/MenuDrawer.vue

@ -0,0 +1,459 @@
<script setup lang="ts">
import type { MenuDto } from '../../types';
import type { DataItemDto } from '../../types/dataDictionaries';
import { ref } from 'vue';
import { useVbenDrawer, useVbenForm } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
formatToDate,
formatToDateTime,
isNullOrWhiteSpace,
listToTree,
} from '@abp/core';
import { Button, Card, message, Steps } from 'ant-design-vue';
import { useMenusApi } from '../../api';
import { useDataDictionariesApi } from '../../api/useDataDictionariesApi';
import { useLayoutsApi } from '../../api/useLayoutsApi';
import { ValueType } from '../../types/dataDictionaries';
const emits = defineEmits<{
(event: 'change', data: MenuDto): void;
}>();
const Step = Steps.Step;
type TabKey = 'basic' | 'meta';
type RenderComponentContentType = (
value: Partial<Record<string, any>>,
) => Record<string, any>;
const currentStep = ref(0);
const submiting = ref(false);
const showMetaForm = ref(false);
const parentMenu = ref<MenuDto>();
const menuMetas = ref<DataItemDto[]>([]);
const activeTabKey = ref<TabKey>('basic');
const { createApi, getAllApi, getApi, updateApi } = useMenusApi();
const { getApi: getLayoutApi, getPagedListApi: getLayoutsApi } =
useLayoutsApi();
const { getApi: getDataDictionaryApi } = useDataDictionariesApi();
const [BasicForm, basicFormApi] = useVbenForm({
commonConfig: {
colon: true,
componentProps: {
class: 'w-full',
},
},
handleSubmit: onNextStep,
schema: [
{
component: 'Input',
dependencies: {
show: false,
triggerFields: ['name'],
},
fieldName: 'id',
},
{
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: getLayoutsApi,
labelField: 'displayName',
onChange: onLayoutChange,
resultField: 'items',
valueField: 'id',
},
fieldName: 'layoutId',
label: $t('AppPlatform.DisplayName:Layout'),
rules: 'selectRequired',
},
{
component: 'Checkbox',
fieldName: 'isPublic',
label: $t('AppPlatform.DisplayName:IsPublic'),
renderComponentContent: () => {
return {
default: () => [$t('AppPlatform.DisplayName:IsPublic')],
};
},
},
{
component: 'ApiTreeSelect',
componentProps: {
allowClear: true,
api: async () => {
const { items } = await getAllApi();
return listToTree(items, {
id: 'id',
pid: 'parentId',
});
},
labelField: 'displayName',
onChange: onParentIdChange,
valueField: 'id',
childrenField: 'children',
},
fieldName: 'parentId',
label: $t('AppPlatform.DisplayName:ParentMenu'),
},
{
component: 'Input',
fieldName: 'name',
label: $t('AppPlatform.DisplayName:Name'),
rules: 'required',
},
{
component: 'Input',
fieldName: 'displayName',
label: $t('AppPlatform.DisplayName:DisplayName'),
rules: 'required',
},
{
component: 'Input',
fieldName: 'path',
label: $t('AppPlatform.DisplayName:Path'),
rules: 'required',
},
{
component: 'Input',
fieldName: 'component',
label: $t('AppPlatform.DisplayName:Component'),
rules: 'required',
},
{
component: 'Input',
fieldName: 'redirect',
label: $t('AppPlatform.DisplayName:Redirect'),
},
{
component: 'Textarea',
componentProps: {
autoSize: {
minRows: 3,
},
},
fieldName: 'description',
label: $t('AppPlatform.DisplayName:Description'),
},
],
showDefaultActions: false,
submitButtonOptions: {
content: $t('AppPlatform.NextStep'),
},
});
const [MetaForm, metaFormApi] = useVbenForm({
commonConfig: {
colon: true,
componentProps: {
class: 'w-full',
},
labelWidth: 150,
},
handleSubmit: onSubmit,
resetButtonOptions: {
content: $t('AppPlatform.PreStep'),
},
schema: [],
showDefaultActions: false,
});
const [Drawer, drawerApi] = useVbenDrawer({
class: 'w-2/3',
onConfirm: onSubmit,
onOpenChange: async (isOpen) => {
if (isOpen) {
await onInit();
}
},
});
function onPreStep() {
currentStep.value = 0;
}
async function onNextStep() {
currentStep.value = 1;
}
async function onInit() {
metaFormApi.removeSchemaByFields(menuMetas.value.map((x) => x.name));
activeTabKey.value = 'basic';
parentMenu.value = undefined;
showMetaForm.value = false;
submiting.value = false;
menuMetas.value = [];
currentStep.value = 0;
const { id, layoutId, parentId } = drawerApi.getData<MenuDto>();
if (!isNullOrWhiteSpace(layoutId)) {
await onLayoutChange(layoutId);
await basicFormApi.setFieldValue('layoutId', layoutId);
}
if (isNullOrWhiteSpace(id)) {
await basicFormApi.setFieldValue('parentId', parentId);
await onParentIdChange(parentId);
drawerApi.setState({ title: $t('AppPlatform.Menu:AddNew') });
return;
}
const dto = await getApi(id);
await basicFormApi.setValues(dto);
// 使
await onParentIdChange(undefined);
onInitMetaFormValues(dto.meta);
drawerApi.setState({ title: `${$t('AppPlatform.Menu:Edit')} - ${dto.name}` });
}
async function onParentIdChange(menuId?: string) {
basicFormApi.updateSchema([
{
componentProps: {
addonBefore: '',
},
fieldName: 'path',
},
]);
if (menuId) {
const parentMenuDto = await getApi(menuId);
parentMenu.value = parentMenuDto;
basicFormApi.updateSchema([
{
componentProps: {
addonBefore: parentMenuDto.path,
},
fieldName: 'path',
},
]);
}
}
async function onLayoutChange(layoutId?: string) {
metaFormApi.removeSchemaByFields(menuMetas.value.map((x) => x.name));
if (!layoutId) {
showMetaForm.value = false;
menuMetas.value = [];
return;
}
try {
drawerApi.setState({ loading: true });
const layoutDto = await getLayoutApi(layoutId);
const dataDto = await getDataDictionaryApi(layoutDto.dataId);
basicFormApi.setFieldValue('component', layoutDto.path);
menuMetas.value = dataDto.items.sort();
onInitMetaFormSchemas();
} finally {
drawerApi.setState({ loading: false });
}
}
function onInitMetaFormSchemas() {
metaFormApi.removeSchemaByFields(menuMetas.value.map((x) => x.name));
menuMetas.value.forEach((dataItem) => {
metaFormApi.setState((pre) => {
let component = 'Input';
let defaultValue: any | undefined;
let componentProps: Record<string, any> | undefined;
let renderComponentContent: RenderComponentContentType | undefined;
switch (dataItem.valueType) {
case ValueType.Array: {
component = 'Select';
componentProps = {
mode: 'tags',
};
if (!isNullOrWhiteSpace(dataItem.defaultValue)) {
defaultValue = dataItem.defaultValue.split(',');
}
break;
}
case ValueType.Boolean: {
component = 'Checkbox';
defaultValue = dataItem.defaultValue === 'true';
renderComponentContent = () => {
return {
default: () => [dataItem.displayName],
};
};
break;
}
case ValueType.Date:
case ValueType.DateTime: {
component = 'DatePicker';
defaultValue = dataItem.defaultValue;
componentProps = {
showTime: dataItem.valueType === ValueType.DateTime,
valueFormat:
dataItem.valueType === ValueType.DateTime
? 'YYYY-MM-DD HH:mm:ss'
: 'YYYY-MM-DD',
};
break;
}
case ValueType.Numeic: {
component = 'InputNumber';
defaultValue = dataItem.defaultValue
? Number(dataItem.defaultValue)
: undefined;
break;
}
case ValueType.String: {
componentProps = {
autocomplete: 'off',
};
defaultValue = dataItem.defaultValue;
break;
}
}
return {
schema: [
...(pre?.schema ?? []),
{
component,
componentProps,
defaultValue,
fieldName: dataItem.name,
help: dataItem.description,
label: dataItem.displayName,
renderComponentContent,
rules: dataItem.allowBeNull ? undefined : 'required',
},
],
};
});
});
showMetaForm.value = true;
}
function onInitMetaFormValues(meta?: Record<string, any>) {
metaFormApi.resetForm();
if (!meta) {
return;
}
const values: Record<string, any> = {};
menuMetas.value.forEach((dataItem) => {
const metaValue = meta[dataItem.name];
if (isNullOrWhiteSpace(metaValue)) {
return;
}
switch (dataItem.valueType) {
case ValueType.Array: {
const metaValueArr = String(metaValue);
values[dataItem.name] = metaValueArr.split(',');
break;
}
case ValueType.Boolean: {
values[dataItem.name] = Boolean(metaValue);
break;
}
case ValueType.Date: {
values[dataItem.name] = formatToDate(metaValue);
break;
}
case ValueType.DateTime: {
values[dataItem.name] = formatToDateTime(metaValue);
break;
}
case ValueType.Numeic: {
values[dataItem.name] = Number(metaValue);
break;
}
case ValueType.String: {
values[dataItem.name] = metaValue;
break;
}
}
});
metaFormApi.setValues(values);
}
async function onSubmit() {
try {
submiting.value = true;
drawerApi.setState({ loading: true });
const basicInput = await basicFormApi.getValues();
const metaInput = await metaFormApi.getValues();
let combPath = String(basicInput.path);
if (!combPath.startsWith('/')) {
combPath = `/${combPath}`;
}
if (parentMenu.value && !combPath.startsWith(parentMenu.value.path)) {
combPath = `${parentMenu.value?.path ?? ''}${combPath}`;
}
const api = basicInput.id
? updateApi(basicInput.id, {
component: basicInput.component,
description: basicInput.description,
displayName: basicInput.displayName,
isPublic: basicInput.isPublic,
meta: metaInput,
name: basicInput.name,
parentId: basicInput.parentId,
path: combPath,
redirect: basicInput.redirect,
})
: createApi({
component: basicInput.component,
description: basicInput.description,
displayName: basicInput.displayName,
isPublic: basicInput.isPublic,
layoutId: basicInput.layoutId,
meta: metaInput,
name: basicInput.name,
parentId: basicInput.parentId,
path: combPath,
redirect: basicInput.redirect,
});
const dto = await api;
message.success($t('AbpUi.SavedSuccessfully'));
emits('change', dto);
drawerApi.close();
} finally {
submiting.value = false;
drawerApi.setState({ loading: false });
}
}
</script>
<template>
<Drawer>
<Card>
<div class="mx-auto max-w-lg">
<Steps :current="currentStep">
<Step :title="$t('AppPlatform.DisplayName:Basic')" />
<Step :title="$t('AppPlatform.DisplayName:Meta')" />
</Steps>
</div>
<div class="p-20">
<BasicForm v-show="currentStep === 0" />
<MetaForm v-show="currentStep === 1" />
</div>
</Card>
<template #footer>
<Button v-if="currentStep === 1" @click="onPreStep">
{{ $t('AppPlatform.PreStep') }}
</Button>
<Button
v-if="currentStep === 0"
type="primary"
@click="basicFormApi.validateAndSubmitForm"
:loading="submiting"
>
{{ $t('AppPlatform.NextStep') }}
</Button>
<Button
v-if="currentStep === 1"
type="primary"
@click="metaFormApi.validateAndSubmitForm"
:loading="submiting"
>
{{ $t('AbpUi.Submit') }}
</Button>
</template>
</Drawer>
</template>
<style scoped></style>

290
apps/vben5/packages/@abp/platform/src/components/menus/MenuTable.vue

@ -0,0 +1,290 @@
<script setup lang="ts">
import type { VxeGridListeners, VxeGridProps } from '@abp/ui';
import type { VbenFormProps } from '@vben/common-ui';
import type { MenuDto } from '../../types/menus';
import { defineAsyncComponent, h, onMounted, reactive, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { listToTree } from '@abp/core';
import { useVbenVxeGrid } from '@abp/ui';
import {
DeleteOutlined,
EditOutlined,
PlusOutlined,
} from '@ant-design/icons-vue';
import { Button, message, Modal } from 'ant-design-vue';
import { useLayoutsApi } from '../../api';
import { useMenusApi } from '../../api/useMenusApi';
defineOptions({
name: 'MenuTable',
});
const { deleteApi, getAllApi } = useMenusApi();
const { getPagedListApi: getLayoutsApi } = useLayoutsApi();
const expandRowKeys = ref<string[]>([]);
const dataDictionaries = ref<MenuDto[]>([]);
const pageState = reactive({
current: 1,
size: 10,
total: 0,
});
const formOptions: VbenFormProps = {
//
collapsed: true,
collapsedRows: 2,
//
commonConfig: {
// label
colon: true,
//
componentProps: {
class: 'w-full',
},
},
handleSubmit: onGet,
schema: [
{
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: getLayoutsApi,
labelField: 'displayName',
resultField: 'items',
valueField: 'id',
},
fieldName: 'layoutId',
label: $t('AppPlatform.DisplayName:Layout'),
},
{
component: 'Input',
fieldName: 'filter',
formItemClass: 'col-span-2 items-baseline',
label: $t('AbpUi.Search'),
},
],
//
showCollapseButton: true,
//
submitOnChange: true,
//
submitOnEnter: true,
};
const gridOptions: VxeGridProps<MenuDto> = {
columns: [
{
align: 'center',
type: 'seq',
width: 50,
},
{
align: 'left',
field: 'name',
minWidth: 150,
slots: { default: 'name' },
title: $t('AppPlatform.DisplayName:Name'),
treeNode: true,
},
{
align: 'left',
field: 'displayName',
minWidth: 150,
title: $t('AppPlatform.DisplayName:DisplayName'),
},
{
align: 'left',
field: 'description',
minWidth: 150,
title: $t('AppPlatform.DisplayName:Description'),
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: $t('AbpUi.Actions'),
width: 350,
},
],
exportConfig: {},
keepSource: true,
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
custom: true,
export: true,
refresh: {
queryMethod: onGet,
},
zoom: true,
},
treeConfig: {
accordion: true,
parentField: 'parentId',
rowField: 'id',
transform: false,
},
};
const gridEvents: VxeGridListeners<MenuDto> = {
pageChange(params) {
pageState.current = params.currentPage;
pageState.size = params.pageSize;
onPageChange();
},
toggleTreeExpand(params) {
if (params.expanded) {
expandRowKeys.value.push(params.row.id);
} else {
expandRowKeys.value = expandRowKeys.value.filter(
(key) => key !== params.row.id,
);
}
onExpandChange();
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridEvents,
gridOptions,
});
const [MenuDrawer, drawerApi] = useVbenDrawer({
connectedComponent: defineAsyncComponent(() => import('./MenuDrawer.vue')),
});
async function onGet() {
try {
gridApi.setLoading(true);
const input = await gridApi.formApi.getValues();
const { items } = await getAllApi(input);
const treeItems = listToTree(items, {
id: 'id',
pid: 'parentId',
});
pageState.total = treeItems.length;
dataDictionaries.value = treeItems;
onPageChange();
} finally {
gridApi.setLoading(false);
}
}
function onExpandChange() {
gridApi.setGridOptions({
treeConfig: {
expandRowKeys: expandRowKeys.value,
},
});
}
function onPageChange() {
const items = dataDictionaries.value.slice(
(pageState.current - 1) * pageState.size,
pageState.current * pageState.size,
);
gridApi.setGridOptions({
data: items,
pagerConfig: {
currentPage: pageState.current,
pageSize: pageState.size,
total: pageState.total,
},
});
}
function onCreate(row?: MenuDto) {
drawerApi.setData({
layoutId: row?.layoutId,
parentId: row?.id,
});
drawerApi.open();
}
function onUpdate(row: MenuDto) {
drawerApi.setData(row);
drawerApi.open();
}
function onDelete(row: MenuDto) {
Modal.confirm({
afterClose: () => {
gridApi.setLoading(false);
},
centered: true,
content: `${$t('AbpUi.ItemWillBeDeletedMessage')}`,
onOk: async () => {
try {
gridApi.setLoading(true);
await deleteApi(row.id);
message.success($t('AbpUi.DeletedSuccessfully'));
onGet();
} finally {
gridApi.setLoading(false);
}
},
title: $t('AbpUi.AreYouSure'),
});
}
onMounted(onGet);
</script>
<template>
<Grid :table-title="$t('AppPlatform.DisplayName:DataDictionary')">
<template #toolbar-tools>
<Button :icon="h(PlusOutlined)" type="primary" @click="onCreate()">
{{ $t('AppPlatform.Menu:AddNew') }}
</Button>
</template>
<template #name="{ row }">
<div class="flex flex-row items-center gap-2">
<IconifyIcon v-if="row.meta.icon" :icon="row.meta.icon" />
<span>{{ row.name }}</span>
</div>
</template>
<template #action="{ row }">
<div class="flex flex-row">
<Button
:icon="h(PlusOutlined)"
block
type="link"
@click="onCreate(row)"
>
{{ $t('AppPlatform.Menu:AddChildren') }}
</Button>
<Button
:icon="h(EditOutlined)"
block
type="link"
@click="onUpdate(row)"
>
{{ $t('AbpUi.Edit') }}
</Button>
<Button
:icon="h(DeleteOutlined)"
block
danger
type="link"
@click="onDelete(row)"
>
{{ $t('AbpUi.Delete') }}
</Button>
</div>
</template>
</Grid>
<MenuDrawer @change="onGet" />
</template>
<style scoped></style>

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

@ -1,2 +1,4 @@
export * from './dataDictionaries';
export * from './layouts';
export * from './menus';
export * from './messages';

35
apps/vben5/packages/@abp/platform/src/types/layouts.ts

@ -0,0 +1,35 @@
import type { PagedAndSortedResultRequestDto } from '@abp/core';
import type { RouteDto } from './routes';
interface LayoutDto extends RouteDto {
dataId: string;
framework: string;
}
interface LayoutCreateOrUpdateDto {
description?: string;
displayName: string;
name: string;
path: string;
redirect?: string;
}
interface LayoutCreateDto extends LayoutCreateOrUpdateDto {
dataId: string;
framework: string;
}
type LayoutUpdateDto = LayoutCreateOrUpdateDto;
interface LayoutGetPagedListInput extends PagedAndSortedResultRequestDto {
filter?: string;
framework?: string;
}
export type {
LayoutCreateDto,
LayoutDto,
LayoutGetPagedListInput,
LayoutUpdateDto,
};

39
apps/vben5/packages/@abp/platform/src/types/menus.ts

@ -0,0 +1,39 @@
import type { RouteDto } from './routes';
interface MenuDto extends RouteDto {
code: string;
component: string;
framework: string;
isPublic: boolean;
layoutId: string;
parentId?: string;
startup: boolean;
}
interface MenuCreateOrUpdateDto {
component: string;
description?: string;
displayName: string;
isPublic: boolean;
meta: Record<string, string>;
name: string;
parentId?: string;
path: string;
redirect?: string;
}
interface MenuCreateDto extends MenuCreateOrUpdateDto {
layoutId: string;
}
type MenuUpdateDto = MenuCreateOrUpdateDto;
interface MenuGetAllInput {
filter?: string;
framework?: string;
layoutId?: string;
parentId?: string;
sorting?: string;
}
export type { MenuCreateDto, MenuDto, MenuGetAllInput, MenuUpdateDto };

12
apps/vben5/packages/@abp/platform/src/types/routes.ts

@ -0,0 +1,12 @@
import type { EntityDto } from '@abp/core';
interface RouteDto extends EntityDto<string> {
description?: string;
displayName: string;
meta: Record<string, string>;
name: string;
path: string;
redirect?: string;
}
export type { RouteDto };
Loading…
Cancel
Save