feat(projects): add menu route field

This commit is contained in:
paynezhuang 2024-04-26 20:00:23 +08:00
parent a1920fcad9
commit dbe31eb1dc
7 changed files with 258 additions and 66 deletions

View File

@ -355,12 +355,15 @@ const local: App.I18n.Schema = {
localIcon: 'Local Icon',
iconTypeTitle: 'Icon Type',
order: 'Order',
constant: 'Constant',
keepAlive: 'Keep Alive',
href: 'Href',
hideInMenu: 'Hide In Menu',
activeMenu: 'Active Menu',
multiTab: 'Multi Tab',
fixedIndexInTab: 'Fixed Index In Tab',
roles: 'Roles',
query: 'Query Params',
button: 'Button',
buttonCode: 'Button Code',
buttonDesc: 'Button Desc',
@ -380,10 +383,13 @@ const local: App.I18n.Schema = {
keepAlive: 'Please select whether to cache route',
href: 'Please enter href',
hideInMenu: 'Please select whether to hide menu',
activeMenu: 'Please enter the route name of the highlighted menu',
activeMenu: 'Please select route name of the highlighted menu',
multiTab: 'Please select whether to support multiple tabs',
fixedInTab: 'Please select whether to fix in the tab',
fixedIndexInTab: 'Please enter the index fixed in the tab',
roles: 'Please select roles',
queryKey: 'Please enter route parameter Key',
queryValue: 'Please enter route parameter Value',
button: 'Please select whether it is a button',
buttonCode: 'Please enter button code',
buttonDesc: 'Please enter button description',

View File

@ -355,12 +355,15 @@ const local: App.I18n.Schema = {
localIcon: '本地图标',
iconTypeTitle: '图标类型',
order: '排序',
constant: '常量路由',
keepAlive: '缓存路由',
href: '外链',
hideInMenu: '隐藏菜单',
activeMenu: '高亮的菜单',
multiTab: '支持多页签',
fixedIndexInTab: '固定在页签中的序号',
roles: '角色',
query: '路由参数',
button: '按钮',
buttonCode: '按钮编码',
buttonDesc: '按钮描述',
@ -380,10 +383,13 @@ const local: App.I18n.Schema = {
keepAlive: '请选择是否缓存路由',
href: '请输入外链',
hideInMenu: '请选择是否隐藏菜单',
activeMenu: '请输入高亮的菜单的路由名称',
activeMenu: '请选择高亮的菜单的路由名称',
multiTab: '请选择是否支持多标签',
fixedInTab: '请选择是否固定在页签中',
fixedIndexInTab: '请输入固定在页签中的序号',
roles: '请选择角色',
queryKey: '请输入路由参数Key',
queryValue: '请输入路由参数Value',
button: '请选择是否按钮',
buttonCode: '请输入按钮编码',
buttonDesc: '请输入按钮描述',

14
src/typings/api.d.ts vendored
View File

@ -198,6 +198,12 @@ declare namespace Api {
order: number;
/** whether to cache the route */
keepAlive?: boolean;
/**
* Is constant route
*
* Does not need to login, and the route is defined in the front-end
*/
constant?: boolean;
/** outer link */
href?: string;
/** whether to hide the route in the menu */
@ -215,8 +221,16 @@ declare namespace Api {
multiTab?: boolean;
/** If set, the route will be fixed in tabs, and the value is the order of fixed tabs */
fixedIndexInTab?: number;
/** if set query parameters, it will be automatically carried when entering the route */
query: Record<string, string>;
/** menu buttons */
buttons?: MenuButton[];
/**
* Roles of the route
*
* Route can be accessed if the current user has at least one of the roles
*/
roles?: string[];
/** children menu */
children?: Menu[];
}>;

View File

@ -539,12 +539,15 @@ declare namespace App {
localIcon: string;
iconTypeTitle: string;
order: string;
constant: string;
keepAlive: string;
href: string;
hideInMenu: string;
activeMenu: string;
multiTab: string;
fixedIndexInTab: string;
roles: string;
query: string;
button: string;
buttonCode: string;
buttonDesc: string;
@ -568,6 +571,9 @@ declare namespace App {
multiTab: string;
fixedInTab: string;
fixedIndexInTab: string;
roles: string;
queryKey: string;
queryValue: string;
button: string;
buttonCode: string;
buttonDesc: string;

View File

@ -19,9 +19,11 @@ declare module 'vue' {
IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined')['default']
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
'IconIc:roundPlus': typeof import('~icons/ic/round-plus')['default']
IconIcRoundDelete: typeof import('~icons/ic/round-delete')['default']
IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default']
IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
IconIcRoundRemove: typeof import('~icons/ic/round-remove')['default']
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
IconLocalBanner: typeof import('~icons/local/banner')['default']
IconLocalLogo: typeof import('~icons/local/logo')['default']
@ -49,6 +51,7 @@ declare module 'vue' {
NDrawer: typeof import('naive-ui')['NDrawer']
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
NDropdown: typeof import('naive-ui')['NDropdown']
NDynamicInput: typeof import('naive-ui')['NDynamicInput']
NEmpty: typeof import('naive-ui')['NEmpty']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']

View File

@ -1,12 +1,20 @@
<script setup lang="tsx">
import { computed, reactive, watch } from 'vue';
import type { Ref } from 'vue';
import { computed, reactive, ref, watch } from 'vue';
import type { SelectOption } from 'naive-ui';
import type { LastLevelRouteKey } from '@elegant-router/types';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
import { enableStatusOptions, menuIconTypeOptions, menuTypeOptions } from '@/constants/business';
import SvgIcon from '@/components/custom/svg-icon.vue';
import { getLocalIcons } from '@/utils/icon';
import { getLayoutAndPage, transformLayoutAndPageToComponent } from './shared';
import { fetchGetAllRoles } from '@/service/api';
import {
getLayoutAndPage,
transformLayoutAndPageToComponent,
transformToKeyValuePairs,
transformToQueryObject
} from './shared';
defineOptions({
name: 'MenuOperateDrawer'
@ -51,6 +59,7 @@ type Model = Pick<
Api.SystemManage.Menu,
| 'menuType'
| 'menuName'
| 'i18nKey'
| 'icon'
| 'iconType'
| 'routeName'
@ -58,8 +67,17 @@ type Model = Pick<
| 'component'
| 'status'
| 'hideInMenu'
| 'activeMenu'
| 'order'
| 'parentId'
| 'constant'
| 'keepAlive'
| 'href'
| 'multiTab'
| 'fixedIndexInTab'
| 'roles'
| 'buttons'
| 'query'
> & {
layout: string;
page: string;
@ -71,16 +89,26 @@ function createDefaultModel(): Model {
return {
menuType: '1',
menuName: '',
i18nKey: '' as App.I18n.I18nKey,
icon: '',
iconType: '1',
routeName: '',
routePath: '',
layout: '',
page: '',
status: null,
status: '1',
hideInMenu: false,
activeMenu: '' as LastLevelRouteKey,
order: 0,
parentId: 0
parentId: 0,
constant: false,
keepAlive: false,
href: '',
multiTab: false,
fixedIndexInTab: 0,
roles: [],
buttons: [],
query: {}
};
}
@ -136,6 +164,29 @@ const layoutOptions: CommonType.Option[] = [
}
];
const dynamicQueryKeyValuePairs: Ref<Record<string, string>[]> = ref([
{
key: '',
value: ''
}
]);
/** the enabled role options */
const roleOptions = ref<CommonType.Option<string>[]>([]);
async function getRoleOptions() {
const { error, data } = await fetchGetAllRoles();
if (!error) {
const options = data.map(item => ({
label: item.roleName,
value: item.roleCode
}));
roleOptions.value = [...options];
}
}
function handleUpdateModel() {
if (props.operateType === 'add') {
Object.assign(model, createDefaultModel());
@ -150,11 +201,12 @@ function handleUpdateModel() {
}
if (props.operateType === 'edit' && props.rowData) {
const { component, ...rest } = props.rowData;
const { component, query, ...rest } = props.rowData;
const { layout, page } = getLayoutAndPage(component);
Object.assign(model, rest, { layout, page });
dynamicQueryKeyValuePairs.value = transformToKeyValuePairs(query);
}
}
@ -166,6 +218,12 @@ async function handleSubmit() {
await validate();
model.component = transformLayoutAndPageToComponent(model.layout, model.page);
model.query = transformToQueryObject(dynamicQueryKeyValuePairs.value);
// model.buttons = [];
// Need: Get buttons based on roles
console.log('model:', model);
// request
window.$message?.success($t('common.updateSuccess'));
@ -177,73 +235,154 @@ watch(visible, () => {
if (visible.value) {
handleUpdateModel();
restoreValidation();
getRoleOptions();
}
});
</script>
<template>
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="360">
<NDrawer v-model:show="visible" display-directive="show" :width="400">
<NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules" label-placement="left" :label-width="80">
<NFormItem :label="$t('page.manage.menu.menuType')" path="menuType">
<NRadioGroup v-model:value="model.menuType" :disabled="disabledMenuType">
<NRadio v-for="item in menuTypeOptions" :key="item.value" :value="item.value" :label="$t(item.label)" />
</NRadioGroup>
</NFormItem>
<NFormItem :label="$t('page.manage.menu.menuName')" path="menuName">
<NInput v-model:value="model.menuName" :placeholder="$t('page.manage.menu.form.menuName')" />
</NFormItem>
<NFormItem :label="$t('page.manage.menu.iconTypeTitle')" path="iconType">
<NRadioGroup v-model:value="model.iconType">
<NRadio v-for="item in menuIconTypeOptions" :key="item.value" :value="item.value" :label="$t(item.label)" />
</NRadioGroup>
</NFormItem>
<NFormItem :label="$t('page.manage.menu.icon')" path="icon">
<template v-if="model.iconType === '1'">
<NInput v-model:value="model.icon" :placeholder="$t('page.manage.menu.form.icon')" class="flex-1">
<template #suffix>
<SvgIcon v-if="model.icon" :icon="model.icon" class="text-icon" />
</template>
</NInput>
</template>
<template v-if="model.iconType === '2'">
<NSelect
v-model:value="model.icon"
:placeholder="$t('page.manage.menu.form.localIcon')"
:options="localIconOptions"
<NGrid>
<NFormItemGi span="12" :label="$t('page.manage.menu.menuType')" path="menuType">
<NRadioGroup v-model:value="model.menuType" :disabled="disabledMenuType">
<NRadio v-for="item in menuTypeOptions" :key="item.value" :value="item.value" :label="$t(item.label)" />
</NRadioGroup>
</NFormItemGi>
<NFormItemGi span="12" :label="$t('page.manage.menu.order')" path="order">
<NInputNumber v-model:value="model.order" :placeholder="$t('page.manage.menu.form.order')" />
</NFormItemGi>
<NFormItemGi span="24" :label="$t('page.manage.menu.menuName')" path="menuName">
<NInput v-model:value="model.menuName" :placeholder="$t('page.manage.menu.form.menuName')" />
</NFormItemGi>
<NFormItemGi span="24" :label="$t('page.manage.menu.i18nKey')" path="i18nKey">
<NInput v-model:value="model.i18nKey" :placeholder="$t('page.manage.menu.form.i18nKey')" />
</NFormItemGi>
<NFormItemGi span="12" :label="$t('page.manage.menu.menuStatus')" path="status">
<NRadioGroup v-model:value="model.status">
<NRadio
v-for="item in enableStatusOptions"
:key="item.value"
:value="item.value"
:label="$t(item.label)"
/>
</NRadioGroup>
</NFormItemGi>
<NFormItemGi span="12" :label="$t('page.manage.menu.hideInMenu')" path="hideInMenu">
<NRadioGroup v-model:value="model.hideInMenu">
<NRadio value :label="$t('common.yesOrNo.yes')" />
<NRadio :value="false" :label="$t('common.yesOrNo.no')" />
</NRadioGroup>
</NFormItemGi>
<NFormItemGi span="12" :label="$t('page.manage.menu.keepAlive')" path="keepAlive">
<NRadioGroup v-model:value="model.keepAlive">
<NRadio value :label="$t('common.yesOrNo.yes')" />
<NRadio :value="false" :label="$t('common.yesOrNo.no')" />
</NRadioGroup>
</NFormItemGi>
<NFormItemGi span="12" :label="$t('page.manage.menu.constant')" path="constant">
<NRadioGroup v-model:value="model.constant">
<NRadio value :label="$t('common.yesOrNo.yes')" />
<NRadio :value="false" :label="$t('common.yesOrNo.no')" />
</NRadioGroup>
</NFormItemGi>
<NFormItemGi span="12" :label="$t('page.manage.menu.multiTab')" path="multiTab">
<NRadioGroup v-model:value="model.multiTab">
<NRadio value :label="$t('common.yesOrNo.yes')" />
<NRadio :value="false" :label="$t('common.yesOrNo.no')" />
</NRadioGroup>
</NFormItemGi>
<NFormItemGi span="12" :label="$t('page.manage.menu.fixedIndexInTab')" path="fixedIndexInTab">
<NInputNumber
v-model:value="model.fixedIndexInTab"
:placeholder="$t('page.manage.menu.form.fixedIndexInTab')"
/>
</template>
</NFormItem>
<NFormItem :label="$t('page.manage.menu.routeName')" path="routeName">
<NInput v-model:value="model.routeName" :placeholder="$t('page.manage.menu.form.routeName')" />
</NFormItem>
<NFormItem :label="$t('page.manage.menu.routePath')" path="routePath">
<NInput v-model:value="model.routePath" :placeholder="$t('page.manage.menu.form.routePath')" />
</NFormItem>
<NFormItem v-if="showLayout" :label="$t('page.manage.menu.layout')" path="layout">
<NSelect
v-model:value="model.layout"
:options="layoutOptions"
:placeholder="$t('page.manage.menu.form.layout')"
/>
</NFormItem>
<NFormItem v-if="showPage" :label="$t('page.manage.menu.page')" path="page">
<NSelect v-model:value="model.page" :options="pageOptions" :placeholder="$t('page.manage.menu.form.page')" />
</NFormItem>
<NFormItem :label="$t('page.manage.menu.menuStatus')" path="status">
<NRadioGroup v-model:value="model.status">
<NRadio v-for="item in enableStatusOptions" :key="item.value" :value="item.value" :label="$t(item.label)" />
</NRadioGroup>
</NFormItem>
<NFormItem :label="$t('page.manage.menu.hideInMenu')" path="hideInMenu">
<NRadioGroup v-model:value="model.hideInMenu">
<NRadio value :label="$t('common.yesOrNo.yes')" />
<NRadio :value="false" :label="$t('common.yesOrNo.no')" />
</NRadioGroup>
</NFormItem>
<NFormItem :label="$t('page.manage.menu.order')" path="order">
<NInputNumber v-model:value="model.order" :placeholder="$t('page.manage.menu.form.order')" />
</NFormItem>
</NFormItemGi>
<NFormItemGi span="24" :label="$t('page.manage.menu.iconTypeTitle')" path="iconType">
<NRadioGroup v-model:value="model.iconType">
<NRadio
v-for="item in menuIconTypeOptions"
:key="item.value"
:value="item.value"
:label="$t(item.label)"
/>
</NRadioGroup>
</NFormItemGi>
<NFormItemGi span="24" :label="$t('page.manage.menu.icon')" path="icon">
<template v-if="model.iconType === '1'">
<NInput v-model:value="model.icon" :placeholder="$t('page.manage.menu.form.icon')" class="flex-1">
<template #suffix>
<SvgIcon v-if="model.icon" :icon="model.icon" class="text-icon" />
</template>
</NInput>
</template>
<template v-if="model.iconType === '2'">
<NSelect
v-model:value="model.icon"
:placeholder="$t('page.manage.menu.form.localIcon')"
:options="localIconOptions"
/>
</template>
</NFormItemGi>
<NFormItemGi span="24" :label="$t('page.manage.menu.routeName')" path="routeName">
<NInput v-model:value="model.routeName" :placeholder="$t('page.manage.menu.form.routeName')" />
</NFormItemGi>
<NFormItemGi span="24" :label="$t('page.manage.menu.routePath')" path="routePath">
<NInput v-model:value="model.routePath" :placeholder="$t('page.manage.menu.form.routePath')" />
</NFormItemGi>
<NFormItemGi v-if="showLayout" span="24" :label="$t('page.manage.menu.layout')" path="layout">
<NSelect
v-model:value="model.layout"
:options="layoutOptions"
:placeholder="$t('page.manage.menu.form.layout')"
/>
</NFormItemGi>
<NFormItemGi v-if="showPage" span="24" :label="$t('page.manage.menu.page')" path="page">
<NSelect
v-model:value="model.page"
:options="pageOptions"
:placeholder="$t('page.manage.menu.form.page')"
/>
</NFormItemGi>
<NFormItemGi v-if="showPage" span="24" :label="$t('page.manage.menu.activeMenu')" path="activeMenu">
<NSelect
v-model:value="model.activeMenu"
:options="pageOptions"
:placeholder="$t('page.manage.menu.form.activeMenu')"
/>
</NFormItemGi>
<NFormItemGi span="24" :label="$t('page.manage.menu.href')" path="href">
<NInput v-model:value="model.href" :placeholder="$t('page.manage.menu.form.href')" />
</NFormItemGi>
<NFormItemGi span="24" :label="$t('page.manage.menu.roles')" path="roles">
<NSelect
v-model:value="model.roles"
multiple
:options="roleOptions"
:placeholder="$t('page.manage.menu.form.roles')"
/>
</NFormItemGi>
<NFormItemGi span="24" :label="$t('page.manage.menu.query')">
<NDynamicInput
v-model:value="dynamicQueryKeyValuePairs"
preset="pair"
:key-placeholder="$t('page.manage.menu.form.queryKey')"
:value-placeholder="$t('page.manage.menu.form.queryValue')"
>
<template #action="{ index, create, remove }">
<NSpace class="ml-2">
<NButton size="medium" @click="() => create(index)">
<icon-ic:round-plus class="text-icon" />
</NButton>
<NButton size="medium" @click="() => remove(index)">
<icon-ic-round-remove class="text-icon" />
</NButton>
</NSpace>
</template>
</NDynamicInput>
</NFormItemGi>
</NGrid>
</NForm>
<template #footer>
<NSpace :size="16">

View File

@ -40,3 +40,21 @@ export function transformLayoutAndPageToComponent(layout: string, page: string)
return '';
}
export function transformToQueryObject(data: Record<string, string>[]) {
const query: Record<string, string> = {};
data.forEach(pair => {
if (pair.key && pair.value) {
query[pair.key] = pair.value;
}
});
return query;
}
export function transformToKeyValuePairs(query?: Record<string, string>) {
const safeQuery = query || {};
return Object.entries(safeQuery).map(([key, value]) => ({
key,
value
}));
}