feat: 菜单支持 iframe
This commit is contained in:
parent
226568b3af
commit
9a0df2e4f3
2
.env
2
.env
@ -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
|
||||
|
@ -202,9 +202,6 @@ importers:
|
||||
|
||||
packages/materials:
|
||||
dependencies:
|
||||
'@sa/hooks':
|
||||
specifier: workspace:*
|
||||
version: link:../hooks
|
||||
'@sa/utils':
|
||||
specifier: workspace:*
|
||||
version: link:../utils
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
|
13
src/typings/api/system.api.d.ts
vendored
13
src/typings/api/system.api.d.ts
vendored
@ -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按钮) */
|
||||
|
4
src/typings/elegant-router.d.ts
vendored
4
src/typings/elegant-router.d.ts
vendored
@ -161,7 +161,7 @@ declare module "@elegant-router/types" {
|
||||
component: `view.${K}`;
|
||||
}
|
||||
: never;
|
||||
|
||||
|
||||
/**
|
||||
* the center level route
|
||||
*/
|
||||
@ -184,7 +184,7 @@ declare module "@elegant-router/types" {
|
||||
children: (CenterLevelRoute<GetChildRouteKey<K>> | LastLevelRoute<GetChildRouteKey<K>>)[];
|
||||
}
|
||||
: never;
|
||||
|
||||
|
||||
/**
|
||||
* the custom first level route
|
||||
*/
|
||||
|
@ -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>
|
||||
|
@ -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,8 +109,11 @@ function handleInitModel() {
|
||||
Object.assign(model, props.rowData);
|
||||
model.component = model.component?.replaceAll('_', '/');
|
||||
iconType.value = model.icon?.startsWith('icon-') ? '2' : '1';
|
||||
const queryObj: { [key: string]: string } = JSON.parse(model.queryParam || '{}');
|
||||
queryList.value = Object.keys(queryObj).map(item => ({ key: item, value: queryObj[item] }));
|
||||
|
||||
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] }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user