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

View File

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

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

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

View File

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

View File

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

View File

@ -1,12 +1,20 @@
<script setup lang="tsx"> <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 { SelectOption } from 'naive-ui';
import type { LastLevelRouteKey } from '@elegant-router/types';
import { useFormRules, useNaiveForm } from '@/hooks/common/form'; import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales'; import { $t } from '@/locales';
import { enableStatusOptions, menuIconTypeOptions, menuTypeOptions } from '@/constants/business'; import { enableStatusOptions, menuIconTypeOptions, menuTypeOptions } from '@/constants/business';
import SvgIcon from '@/components/custom/svg-icon.vue'; import SvgIcon from '@/components/custom/svg-icon.vue';
import { getLocalIcons } from '@/utils/icon'; import { getLocalIcons } from '@/utils/icon';
import { getLayoutAndPage, transformLayoutAndPageToComponent } from './shared'; import { fetchGetAllRoles } from '@/service/api';
import {
getLayoutAndPage,
transformLayoutAndPageToComponent,
transformToKeyValuePairs,
transformToQueryObject
} from './shared';
defineOptions({ defineOptions({
name: 'MenuOperateDrawer' name: 'MenuOperateDrawer'
@ -51,6 +59,7 @@ type Model = Pick<
Api.SystemManage.Menu, Api.SystemManage.Menu,
| 'menuType' | 'menuType'
| 'menuName' | 'menuName'
| 'i18nKey'
| 'icon' | 'icon'
| 'iconType' | 'iconType'
| 'routeName' | 'routeName'
@ -58,8 +67,17 @@ type Model = Pick<
| 'component' | 'component'
| 'status' | 'status'
| 'hideInMenu' | 'hideInMenu'
| 'activeMenu'
| 'order' | 'order'
| 'parentId' | 'parentId'
| 'constant'
| 'keepAlive'
| 'href'
| 'multiTab'
| 'fixedIndexInTab'
| 'roles'
| 'buttons'
| 'query'
> & { > & {
layout: string; layout: string;
page: string; page: string;
@ -71,16 +89,26 @@ function createDefaultModel(): Model {
return { return {
menuType: '1', menuType: '1',
menuName: '', menuName: '',
i18nKey: '' as App.I18n.I18nKey,
icon: '', icon: '',
iconType: '1', iconType: '1',
routeName: '', routeName: '',
routePath: '', routePath: '',
layout: '', layout: '',
page: '', page: '',
status: null, status: '1',
hideInMenu: false, hideInMenu: false,
activeMenu: '' as LastLevelRouteKey,
order: 0, 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() { function handleUpdateModel() {
if (props.operateType === 'add') { if (props.operateType === 'add') {
Object.assign(model, createDefaultModel()); Object.assign(model, createDefaultModel());
@ -150,11 +201,12 @@ function handleUpdateModel() {
} }
if (props.operateType === 'edit' && props.rowData) { if (props.operateType === 'edit' && props.rowData) {
const { component, ...rest } = props.rowData; const { component, query, ...rest } = props.rowData;
const { layout, page } = getLayoutAndPage(component); const { layout, page } = getLayoutAndPage(component);
Object.assign(model, rest, { layout, page }); Object.assign(model, rest, { layout, page });
dynamicQueryKeyValuePairs.value = transformToKeyValuePairs(query);
} }
} }
@ -166,6 +218,12 @@ async function handleSubmit() {
await validate(); await validate();
model.component = transformLayoutAndPageToComponent(model.layout, model.page); 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 // request
window.$message?.success($t('common.updateSuccess')); window.$message?.success($t('common.updateSuccess'));
@ -177,73 +235,154 @@ watch(visible, () => {
if (visible.value) { if (visible.value) {
handleUpdateModel(); handleUpdateModel();
restoreValidation(); restoreValidation();
getRoleOptions();
} }
}); });
</script> </script>
<template> <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> <NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules" label-placement="left" :label-width="80"> <NForm ref="formRef" :model="model" :rules="rules" label-placement="left" :label-width="80">
<NFormItem :label="$t('page.manage.menu.menuType')" path="menuType"> <NGrid>
<NRadioGroup v-model:value="model.menuType" :disabled="disabledMenuType"> <NFormItemGi span="12" :label="$t('page.manage.menu.menuType')" path="menuType">
<NRadio v-for="item in menuTypeOptions" :key="item.value" :value="item.value" :label="$t(item.label)" /> <NRadioGroup v-model:value="model.menuType" :disabled="disabledMenuType">
</NRadioGroup> <NRadio v-for="item in menuTypeOptions" :key="item.value" :value="item.value" :label="$t(item.label)" />
</NFormItem> </NRadioGroup>
<NFormItem :label="$t('page.manage.menu.menuName')" path="menuName"> </NFormItemGi>
<NInput v-model:value="model.menuName" :placeholder="$t('page.manage.menu.form.menuName')" /> <NFormItemGi span="12" :label="$t('page.manage.menu.order')" path="order">
</NFormItem> <NInputNumber v-model:value="model.order" :placeholder="$t('page.manage.menu.form.order')" />
<NFormItem :label="$t('page.manage.menu.iconTypeTitle')" path="iconType"> </NFormItemGi>
<NRadioGroup v-model:value="model.iconType"> <NFormItemGi span="24" :label="$t('page.manage.menu.menuName')" path="menuName">
<NRadio v-for="item in menuIconTypeOptions" :key="item.value" :value="item.value" :label="$t(item.label)" /> <NInput v-model:value="model.menuName" :placeholder="$t('page.manage.menu.form.menuName')" />
</NRadioGroup> </NFormItemGi>
</NFormItem> <NFormItemGi span="24" :label="$t('page.manage.menu.i18nKey')" path="i18nKey">
<NFormItem :label="$t('page.manage.menu.icon')" path="icon"> <NInput v-model:value="model.i18nKey" :placeholder="$t('page.manage.menu.form.i18nKey')" />
<template v-if="model.iconType === '1'"> </NFormItemGi>
<NInput v-model:value="model.icon" :placeholder="$t('page.manage.menu.form.icon')" class="flex-1"> <NFormItemGi span="12" :label="$t('page.manage.menu.menuStatus')" path="status">
<template #suffix> <NRadioGroup v-model:value="model.status">
<SvgIcon v-if="model.icon" :icon="model.icon" class="text-icon" /> <NRadio
</template> v-for="item in enableStatusOptions"
</NInput> :key="item.value"
</template> :value="item.value"
<template v-if="model.iconType === '2'"> :label="$t(item.label)"
<NSelect />
v-model:value="model.icon" </NRadioGroup>
:placeholder="$t('page.manage.menu.form.localIcon')" </NFormItemGi>
:options="localIconOptions" <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> </NFormItemGi>
</NFormItem> <NFormItemGi span="24" :label="$t('page.manage.menu.iconTypeTitle')" path="iconType">
<NFormItem :label="$t('page.manage.menu.routeName')" path="routeName"> <NRadioGroup v-model:value="model.iconType">
<NInput v-model:value="model.routeName" :placeholder="$t('page.manage.menu.form.routeName')" /> <NRadio
</NFormItem> v-for="item in menuIconTypeOptions"
<NFormItem :label="$t('page.manage.menu.routePath')" path="routePath"> :key="item.value"
<NInput v-model:value="model.routePath" :placeholder="$t('page.manage.menu.form.routePath')" /> :value="item.value"
</NFormItem> :label="$t(item.label)"
<NFormItem v-if="showLayout" :label="$t('page.manage.menu.layout')" path="layout"> />
<NSelect </NRadioGroup>
v-model:value="model.layout" </NFormItemGi>
:options="layoutOptions" <NFormItemGi span="24" :label="$t('page.manage.menu.icon')" path="icon">
:placeholder="$t('page.manage.menu.form.layout')" <template v-if="model.iconType === '1'">
/> <NInput v-model:value="model.icon" :placeholder="$t('page.manage.menu.form.icon')" class="flex-1">
</NFormItem> <template #suffix>
<NFormItem v-if="showPage" :label="$t('page.manage.menu.page')" path="page"> <SvgIcon v-if="model.icon" :icon="model.icon" class="text-icon" />
<NSelect v-model:value="model.page" :options="pageOptions" :placeholder="$t('page.manage.menu.form.page')" /> </template>
</NFormItem> </NInput>
<NFormItem :label="$t('page.manage.menu.menuStatus')" path="status"> </template>
<NRadioGroup v-model:value="model.status"> <template v-if="model.iconType === '2'">
<NRadio v-for="item in enableStatusOptions" :key="item.value" :value="item.value" :label="$t(item.label)" /> <NSelect
</NRadioGroup> v-model:value="model.icon"
</NFormItem> :placeholder="$t('page.manage.menu.form.localIcon')"
<NFormItem :label="$t('page.manage.menu.hideInMenu')" path="hideInMenu"> :options="localIconOptions"
<NRadioGroup v-model:value="model.hideInMenu"> />
<NRadio value :label="$t('common.yesOrNo.yes')" /> </template>
<NRadio :value="false" :label="$t('common.yesOrNo.no')" /> </NFormItemGi>
</NRadioGroup> <NFormItemGi span="24" :label="$t('page.manage.menu.routeName')" path="routeName">
</NFormItem> <NInput v-model:value="model.routeName" :placeholder="$t('page.manage.menu.form.routeName')" />
<NFormItem :label="$t('page.manage.menu.order')" path="order"> </NFormItemGi>
<NInputNumber v-model:value="model.order" :placeholder="$t('page.manage.menu.form.order')" /> <NFormItemGi span="24" :label="$t('page.manage.menu.routePath')" path="routePath">
</NFormItem> <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> </NForm>
<template #footer> <template #footer>
<NSpace :size="16"> <NSpace :size="16">

View File

@ -40,3 +40,21 @@ export function transformLayoutAndPageToComponent(layout: string, page: string)
return ''; 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
}));
}