feat: 菜单支持 iframe

This commit is contained in:
xlsea 2024-09-07 12:48:39 +08:00
parent 226568b3af
commit 9a0df2e4f3
8 changed files with 101 additions and 46 deletions

2
.env
View File

@ -12,7 +12,7 @@ VITE_ICON_PREFIX=icon
VITE_ICON_LOCAL_PREFIX=icon-local
# auth route mode: static dynamic
VITE_AUTH_ROUTE_MODE=static
VITE_AUTH_ROUTE_MODE=dynamic
# static auth route home
VITE_ROUTE_HOME=home

View File

@ -202,9 +202,6 @@ importers:
packages/materials:
dependencies:
'@sa/hooks':
specifier: workspace:*
version: link:../hooks
'@sa/utils':
specifier: workspace:*
version: link:../utils

View File

@ -17,6 +17,15 @@ export const menuTypeRecord: Record<Api.System.MenuType, string> = {
export const menuTypeOptions = transformRecordToOption(menuTypeRecord);
/** menu is frame */
export const menuIsFrameRecord: Record<Api.System.IsMenuFrame, string> = {
'0': '缓存',
'1': '不缓存',
'2': 'iframe'
};
export const menuIsFrameOptions = transformRecordToOption(menuIsFrameRecord);
/** menu icon type */
export const menuIconTypeRecord: Record<Api.System.IconType, string> = {
'1': 'iconify',

View File

@ -93,7 +93,9 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
function parseRouter(route: ElegantConstRoute, parent?: ElegantConstRoute) {
if (authRouteMode.value === 'dynamic') {
route.path = route.path.substring(1);
// @ts-expect-error no query field
const query = route.query ? String(route.query) : undefined;
route.path = route.path.startsWith('//') ? route.path.substring(1) : route.path;
const name = humpToLine(route.path.substring(1).replace('/', '_'));
route.name = parent ? `${parent.name}_${name}` : name;
@ -116,10 +118,17 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
}
if (route.component.endsWith('iframe-page')) {
route.meta.href = String(route.meta.link);
route.path = '/iframe-page/123';
route.name = 'iframe_page';
route.component = 'view.iframe-page';
if (query) {
route.props = {
url: query
};
} else {
route.meta.href = String(route.meta.link);
const randomValue = Math.random().toString(36).slice(2, 12);
route.path = `/iframe-page/${randomValue}`;
route.name = `iframe_page_${randomValue}`;
}
}
delete route.meta.link;

View File

@ -150,6 +150,15 @@ declare namespace Api {
*/
type MenuType = 'M' | 'C' | 'F';
/**
*
*
* - "0": "是"
* - "1": "否"
* - "2": "iframe"
*/
type IsMenuFrame = '0' | '1' | '2';
type Menu = Common.CommonRecord<{
/** 菜单 ID */
menuId?: CommonType.IdType;
@ -165,8 +174,8 @@ declare namespace Api {
component?: string;
/** 路由参数 */
queryParam?: string;
/** 是否为外链0是 1否 */
isFrame?: Common.YesOrNoStatus;
/** 是否为外链0是 1否 2iframe */
isFrame?: IsMenuFrame;
/** 是否缓存0缓存 1不缓存 */
isCache?: Common.YesOrNoStatus;
/** 菜单类型M目录 C菜单 F按钮 */

View File

@ -6,7 +6,7 @@ import { NButton, NIcon, NInput, NPopconfirm, NTooltip } from 'naive-ui';
import { fetchDeleteMenu, fetchGetMenuList } from '@/service/api';
import SvgIcon from '@/components/custom/svg-icon.vue';
import { useAppStore } from '@/store/modules/app';
import { menuTypeRecord } from '@/constants/business';
import { menuIsFrameRecord, menuTypeRecord } from '@/constants/business';
import ButtonIcon from '@/components/custom/button-icon.vue';
import { $t } from '@/locales';
import { handleMenuTree } from '@/utils/ruoyi';
@ -133,9 +133,10 @@ function handleClickTree(option: Array<TreeOption | null>) {
getBtnMenuList();
}
const tagMap: Record<Api.Common.EnableStatus, NaiveUI.ThemeColor> = {
const tagMap: Record<'0' | '1' | '2', NaiveUI.ThemeColor> = {
'0': 'success',
'1': 'warning'
'1': 'warning',
'2': 'primary'
};
async function getBtnMenuList() {
@ -291,6 +292,7 @@ const { record: showHideRecord } = useDict('sys_show_hide');
v-model:checked-keys="checkedKeys"
v-model:expanded-keys="expandedKeys"
:cancelable="false"
show-line
:data="treeData as []"
:default-expanded-keys="[0]"
:show-irrelevant-nodes="false"
@ -379,7 +381,9 @@ const { record: showHideRecord } = useDict('sys_show_hide');
{{ currentMenu.perms }}
</NDescriptionsItem>
<NDescriptionsItem label="是否外链">
<BooleanTag size="small" :value="currentMenu.isFrame!" />
<NTag v-if="currentMenu.isFrame" size="small" :type="tagMap[currentMenu.isFrame]">
{{ menuIsFrameRecord[currentMenu.isFrame] }}
</NTag>
</NDescriptionsItem>
<NDescriptionsItem label="显示状态">
<NTag v-if="currentMenu.visible" size="small" :type="tagMap[currentMenu.visible]">
@ -395,7 +399,7 @@ const { record: showHideRecord } = useDict('sys_show_hide');
</NCard>
<NCard
title="接口权限列表"
title="按钮权限列表"
:bordered="false"
size="small"
class="h-full overflow-auto card-wrapper"
@ -429,7 +433,7 @@ const { record: showHideRecord } = useDict('sys_show_hide');
</TableSiderLayout>
</template>
<style scoped>
<style scoped lang="scss">
:deep(.infinite-scroll) {
height: calc(100vh - 224px - var(--calc-footer-height, 0px)) !important;
max-height: calc(100vh - 224px - var(--calc-footer-height, 0px)) !important;
@ -453,4 +457,20 @@ const { record: showHideRecord } = useDict('sys_show_hide');
:deep(.n-data-table-base-table) {
height: 100% !important;
}
.menu-tree {
:deep(.n-tree-node) {
height: 33px;
}
:deep(.n-tree-node-switcher) {
height: 33px;
}
:deep(.n-tree-node-switcher__icon) {
font-size: 16px !important;
height: 16px !important;
width: 16px !important;
}
}
</style>

View File

@ -5,7 +5,7 @@ import { NTooltip } from 'naive-ui';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
import { fetchCreateMenu, fetchUpdateMenu } from '@/service/api';
import { menuIconTypeOptions, menuTypeOptions } from '@/constants/business';
import { menuIconTypeOptions, menuIsFrameOptions, menuTypeOptions } from '@/constants/business';
import SvgIcon from '@/components/custom/svg-icon.vue';
import { getLocalMenuIcons } from '@/utils/icon';
import { humpToLine, isNotNull } from '@/utils/common';
@ -109,10 +109,13 @@ function handleInitModel() {
Object.assign(model, props.rowData);
model.component = model.component?.replaceAll('_', '/');
iconType.value = model.icon?.startsWith('icon-') ? '2' : '1';
if (model.isFrame !== '2') {
const queryObj: { [key: string]: string } = JSON.parse(model.queryParam || '{}');
queryList.value = Object.keys(queryObj).map(item => ({ key: item, value: queryObj[item] }));
}
}
}
function closeDrawer() {
visible.value = false;
@ -121,10 +124,6 @@ function closeDrawer() {
async function handleSubmit() {
await validate();
const queryObj: { [key: string]: string } = {};
queryList.value.forEach(item => (queryObj[item.key] = item.value));
model.queryParam = JSON.stringify(queryObj);
const {
menuId,
parentId,
@ -140,9 +139,10 @@ async function handleSubmit() {
remark
} = model;
let path = model.path;
if (model.isFrame === '1') {
path = !model.path?.startsWith('/') ? `/${model.path}` : model.path;
if (isFrame !== '2' && queryList.value.length) {
const queryObj: { [key: string]: string } = {};
queryList.value.forEach(item => (queryObj[item.key] = item.value));
model.queryParam = JSON.stringify(queryObj);
}
let icon;
@ -150,18 +150,15 @@ async function handleSubmit() {
icon = iconType.value === '1' ? model.icon : model.icon?.replace('menu-', 'icon-');
}
let path = model.path;
let component = model.component;
if (model.menuType === 'C') {
component = humpToLine(model.component?.replaceAll('/', '_') || '');
}
if (model.menuType === 'M') {
component = model.parentId === 0 ? 'layout.base' : undefined;
}
if (model.isFrame === '0') {
if (isFrame !== '0') {
component = 'iframe-page';
path = !model.path?.startsWith('/') ? `/${model.path}` : model.path;
} else if (model.menuType === 'C') {
component = humpToLine(model.component?.replaceAll('/', '_') || '');
} else if (model.menuType === 'M') {
component = model.parentId === 0 ? 'layout.base' : undefined;
}
// request
@ -276,8 +273,7 @@ const FormTipComponent = defineComponent({
<NFormItemGi v-if="menuType !== 'F'" :span="24" label="菜单类型" path="menuType">
<NRadioGroup v-model:value="model.menuType">
<NRadioButton
v-for="item in menuTypeOptions"
v-show="item.value !== 'F'"
v-for="item in menuTypeOptions.filter(item => item.value !== 'F')"
:key="item.value"
:value="item.value"
:label="item.label"
@ -314,12 +310,12 @@ const FormTipComponent = defineComponent({
<template #label>
<div class="flex-center">
<FormTipComponent content="访问的路由地址,如:`/user`,如外网地址需内链访问则以 `http(s)://` 开头" />
<span class="pl-3px">路由地址</span>
<span class="pl-3px">{{ model.isFrame !== '0' ? '路由地址' : '外链地址' }}</span>
</div>
</template>
<NInput v-model:value="model.path" placeholder="请输入路由地址" />
</NFormItemGi>
<NFormItemGi v-if="isMenu" :span="24" path="component">
<NFormItemGi v-if="isMenu && model.isFrame === '1'" :span="24" path="component">
<template #label>
<div class="flex-center">
<FormTipComponent content="访问的组件路径,如:`system/user`,默认在`views`目录下" />
@ -332,8 +328,18 @@ const FormTipComponent = defineComponent({
<NInputGroupLabel>/index.vue</NInputGroupLabel>
</NInputGroup>
</NFormItemGi>
<NFormItemGi v-if="isMenu" span="24" :show-feedback="!queryList.length" label="路由参数">
<NDynamicInput v-model:value="queryList" item-style="margin-bottom: 0" :on-create="onCreate">
<NFormItemGi
v-if="isMenu && model.isFrame !== '0'"
span="24"
:show-feedback="!queryList.length"
:label="model.isFrame !== '2' ? '路由参数' : 'iframe 地址'"
>
<NDynamicInput
v-if="model.isFrame !== '2'"
v-model:value="queryList"
item-style="margin-bottom: 0"
:on-create="onCreate"
>
<template #default="{ index }">
<div class="w-full flex">
<NFormItem
@ -358,6 +364,7 @@ const FormTipComponent = defineComponent({
</div>
</template>
</NDynamicInput>
<NInput v-else v-model:value="model.queryParam" placeholder="请输入 iframe 地址" />
</NFormItemGi>
<NFormItemGi :span="24" path="perms">
<template #label>
@ -377,8 +384,12 @@ const FormTipComponent = defineComponent({
</template>
<NRadioGroup v-model:value="model.isFrame">
<NSpace>
<NRadio value="0" label="是" />
<NRadio value="1" label="否" />
<NRadio
v-for="option in menuIsFrameOptions"
:key="option.value"
:value="option.value"
:label="option.label"
/>
</NSpace>
</NRadioGroup>
</NFormItemGi>