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
|
VITE_ICON_LOCAL_PREFIX=icon-local
|
||||||
|
|
||||||
# auth route mode: static | dynamic
|
# auth route mode: static | dynamic
|
||||||
VITE_AUTH_ROUTE_MODE=static
|
VITE_AUTH_ROUTE_MODE=dynamic
|
||||||
|
|
||||||
# static auth route home
|
# static auth route home
|
||||||
VITE_ROUTE_HOME=home
|
VITE_ROUTE_HOME=home
|
||||||
|
@ -202,9 +202,6 @@ importers:
|
|||||||
|
|
||||||
packages/materials:
|
packages/materials:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sa/hooks':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../hooks
|
|
||||||
'@sa/utils':
|
'@sa/utils':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../utils
|
version: link:../utils
|
||||||
|
@ -17,6 +17,15 @@ export const menuTypeRecord: Record<Api.System.MenuType, string> = {
|
|||||||
|
|
||||||
export const menuTypeOptions = transformRecordToOption(menuTypeRecord);
|
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 */
|
/** menu icon type */
|
||||||
export const menuIconTypeRecord: Record<Api.System.IconType, string> = {
|
export const menuIconTypeRecord: Record<Api.System.IconType, string> = {
|
||||||
'1': 'iconify',
|
'1': 'iconify',
|
||||||
|
@ -93,7 +93,9 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
|||||||
|
|
||||||
function parseRouter(route: ElegantConstRoute, parent?: ElegantConstRoute) {
|
function parseRouter(route: ElegantConstRoute, parent?: ElegantConstRoute) {
|
||||||
if (authRouteMode.value === 'dynamic') {
|
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('/', '_'));
|
const name = humpToLine(route.path.substring(1).replace('/', '_'));
|
||||||
route.name = parent ? `${parent.name}_${name}` : name;
|
route.name = parent ? `${parent.name}_${name}` : name;
|
||||||
|
|
||||||
@ -116,10 +118,17 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (route.component.endsWith('iframe-page')) {
|
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';
|
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;
|
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';
|
type MenuType = 'M' | 'C' | 'F';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否外链
|
||||||
|
*
|
||||||
|
* - "0": "是"
|
||||||
|
* - "1": "否"
|
||||||
|
* - "2": "iframe"
|
||||||
|
*/
|
||||||
|
type IsMenuFrame = '0' | '1' | '2';
|
||||||
|
|
||||||
type Menu = Common.CommonRecord<{
|
type Menu = Common.CommonRecord<{
|
||||||
/** 菜单 ID */
|
/** 菜单 ID */
|
||||||
menuId?: CommonType.IdType;
|
menuId?: CommonType.IdType;
|
||||||
@ -165,8 +174,8 @@ declare namespace Api {
|
|||||||
component?: string;
|
component?: string;
|
||||||
/** 路由参数 */
|
/** 路由参数 */
|
||||||
queryParam?: string;
|
queryParam?: string;
|
||||||
/** 是否为外链(0是 1否) */
|
/** 是否为外链(0是 1否 2iframe) */
|
||||||
isFrame?: Common.YesOrNoStatus;
|
isFrame?: IsMenuFrame;
|
||||||
/** 是否缓存(0缓存 1不缓存) */
|
/** 是否缓存(0缓存 1不缓存) */
|
||||||
isCache?: Common.YesOrNoStatus;
|
isCache?: Common.YesOrNoStatus;
|
||||||
/** 菜单类型(M目录 C菜单 F按钮) */
|
/** 菜单类型(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}`;
|
component: `view.${K}`;
|
||||||
}
|
}
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the center level route
|
* the center level route
|
||||||
*/
|
*/
|
||||||
@ -184,7 +184,7 @@ declare module "@elegant-router/types" {
|
|||||||
children: (CenterLevelRoute<GetChildRouteKey<K>> | LastLevelRoute<GetChildRouteKey<K>>)[];
|
children: (CenterLevelRoute<GetChildRouteKey<K>> | LastLevelRoute<GetChildRouteKey<K>>)[];
|
||||||
}
|
}
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the custom first level route
|
* 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 { fetchDeleteMenu, fetchGetMenuList } from '@/service/api';
|
||||||
import SvgIcon from '@/components/custom/svg-icon.vue';
|
import SvgIcon from '@/components/custom/svg-icon.vue';
|
||||||
import { useAppStore } from '@/store/modules/app';
|
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 ButtonIcon from '@/components/custom/button-icon.vue';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import { handleMenuTree } from '@/utils/ruoyi';
|
import { handleMenuTree } from '@/utils/ruoyi';
|
||||||
@ -133,9 +133,10 @@ function handleClickTree(option: Array<TreeOption | null>) {
|
|||||||
getBtnMenuList();
|
getBtnMenuList();
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagMap: Record<Api.Common.EnableStatus, NaiveUI.ThemeColor> = {
|
const tagMap: Record<'0' | '1' | '2', NaiveUI.ThemeColor> = {
|
||||||
'0': 'success',
|
'0': 'success',
|
||||||
'1': 'warning'
|
'1': 'warning',
|
||||||
|
'2': 'primary'
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getBtnMenuList() {
|
async function getBtnMenuList() {
|
||||||
@ -291,6 +292,7 @@ const { record: showHideRecord } = useDict('sys_show_hide');
|
|||||||
v-model:checked-keys="checkedKeys"
|
v-model:checked-keys="checkedKeys"
|
||||||
v-model:expanded-keys="expandedKeys"
|
v-model:expanded-keys="expandedKeys"
|
||||||
:cancelable="false"
|
:cancelable="false"
|
||||||
|
show-line
|
||||||
:data="treeData as []"
|
:data="treeData as []"
|
||||||
:default-expanded-keys="[0]"
|
:default-expanded-keys="[0]"
|
||||||
:show-irrelevant-nodes="false"
|
:show-irrelevant-nodes="false"
|
||||||
@ -379,7 +381,9 @@ const { record: showHideRecord } = useDict('sys_show_hide');
|
|||||||
{{ currentMenu.perms }}
|
{{ currentMenu.perms }}
|
||||||
</NDescriptionsItem>
|
</NDescriptionsItem>
|
||||||
<NDescriptionsItem label="是否外链">
|
<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>
|
||||||
<NDescriptionsItem label="显示状态">
|
<NDescriptionsItem label="显示状态">
|
||||||
<NTag v-if="currentMenu.visible" size="small" :type="tagMap[currentMenu.visible]">
|
<NTag v-if="currentMenu.visible" size="small" :type="tagMap[currentMenu.visible]">
|
||||||
@ -395,7 +399,7 @@ const { record: showHideRecord } = useDict('sys_show_hide');
|
|||||||
</NCard>
|
</NCard>
|
||||||
|
|
||||||
<NCard
|
<NCard
|
||||||
title="接口权限列表"
|
title="按钮权限列表"
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
size="small"
|
size="small"
|
||||||
class="h-full overflow-auto card-wrapper"
|
class="h-full overflow-auto card-wrapper"
|
||||||
@ -429,7 +433,7 @@ const { record: showHideRecord } = useDict('sys_show_hide');
|
|||||||
</TableSiderLayout>
|
</TableSiderLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="scss">
|
||||||
:deep(.infinite-scroll) {
|
:deep(.infinite-scroll) {
|
||||||
height: calc(100vh - 224px - var(--calc-footer-height, 0px)) !important;
|
height: calc(100vh - 224px - var(--calc-footer-height, 0px)) !important;
|
||||||
max-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) {
|
:deep(.n-data-table-base-table) {
|
||||||
height: 100% !important;
|
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>
|
</style>
|
||||||
|
@ -5,7 +5,7 @@ import { NTooltip } from 'naive-ui';
|
|||||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import { fetchCreateMenu, fetchUpdateMenu } from '@/service/api';
|
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 SvgIcon from '@/components/custom/svg-icon.vue';
|
||||||
import { getLocalMenuIcons } from '@/utils/icon';
|
import { getLocalMenuIcons } from '@/utils/icon';
|
||||||
import { humpToLine, isNotNull } from '@/utils/common';
|
import { humpToLine, isNotNull } from '@/utils/common';
|
||||||
@ -109,8 +109,11 @@ function handleInitModel() {
|
|||||||
Object.assign(model, props.rowData);
|
Object.assign(model, props.rowData);
|
||||||
model.component = model.component?.replaceAll('_', '/');
|
model.component = model.component?.replaceAll('_', '/');
|
||||||
iconType.value = model.icon?.startsWith('icon-') ? '2' : '1';
|
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() {
|
async function handleSubmit() {
|
||||||
await validate();
|
await validate();
|
||||||
|
|
||||||
const queryObj: { [key: string]: string } = {};
|
|
||||||
queryList.value.forEach(item => (queryObj[item.key] = item.value));
|
|
||||||
model.queryParam = JSON.stringify(queryObj);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
menuId,
|
menuId,
|
||||||
parentId,
|
parentId,
|
||||||
@ -140,9 +139,10 @@ async function handleSubmit() {
|
|||||||
remark
|
remark
|
||||||
} = model;
|
} = model;
|
||||||
|
|
||||||
let path = model.path;
|
if (isFrame !== '2' && queryList.value.length) {
|
||||||
if (model.isFrame === '1') {
|
const queryObj: { [key: string]: string } = {};
|
||||||
path = !model.path?.startsWith('/') ? `/${model.path}` : model.path;
|
queryList.value.forEach(item => (queryObj[item.key] = item.value));
|
||||||
|
model.queryParam = JSON.stringify(queryObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
let icon;
|
let icon;
|
||||||
@ -150,18 +150,15 @@ async function handleSubmit() {
|
|||||||
icon = iconType.value === '1' ? model.icon : model.icon?.replace('menu-', 'icon-');
|
icon = iconType.value === '1' ? model.icon : model.icon?.replace('menu-', 'icon-');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let path = model.path;
|
||||||
let component = model.component;
|
let component = model.component;
|
||||||
|
if (isFrame !== '0') {
|
||||||
if (model.menuType === 'C') {
|
|
||||||
component = humpToLine(model.component?.replaceAll('/', '_') || '');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.menuType === 'M') {
|
|
||||||
component = model.parentId === 0 ? 'layout.base' : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.isFrame === '0') {
|
|
||||||
component = 'iframe-page';
|
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
|
// request
|
||||||
@ -276,8 +273,7 @@ const FormTipComponent = defineComponent({
|
|||||||
<NFormItemGi v-if="menuType !== 'F'" :span="24" label="菜单类型" path="menuType">
|
<NFormItemGi v-if="menuType !== 'F'" :span="24" label="菜单类型" path="menuType">
|
||||||
<NRadioGroup v-model:value="model.menuType">
|
<NRadioGroup v-model:value="model.menuType">
|
||||||
<NRadioButton
|
<NRadioButton
|
||||||
v-for="item in menuTypeOptions"
|
v-for="item in menuTypeOptions.filter(item => item.value !== 'F')"
|
||||||
v-show="item.value !== 'F'"
|
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:value="item.value"
|
:value="item.value"
|
||||||
:label="item.label"
|
:label="item.label"
|
||||||
@ -314,12 +310,12 @@ const FormTipComponent = defineComponent({
|
|||||||
<template #label>
|
<template #label>
|
||||||
<div class="flex-center">
|
<div class="flex-center">
|
||||||
<FormTipComponent content="访问的路由地址,如:`/user`,如外网地址需内链访问则以 `http(s)://` 开头" />
|
<FormTipComponent content="访问的路由地址,如:`/user`,如外网地址需内链访问则以 `http(s)://` 开头" />
|
||||||
<span class="pl-3px">路由地址</span>
|
<span class="pl-3px">{{ model.isFrame !== '0' ? '路由地址' : '外链地址' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<NInput v-model:value="model.path" placeholder="请输入路由地址" />
|
<NInput v-model:value="model.path" placeholder="请输入路由地址" />
|
||||||
</NFormItemGi>
|
</NFormItemGi>
|
||||||
<NFormItemGi v-if="isMenu" :span="24" path="component">
|
<NFormItemGi v-if="isMenu && model.isFrame === '1'" :span="24" path="component">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div class="flex-center">
|
<div class="flex-center">
|
||||||
<FormTipComponent content="访问的组件路径,如:`system/user`,默认在`views`目录下" />
|
<FormTipComponent content="访问的组件路径,如:`system/user`,默认在`views`目录下" />
|
||||||
@ -332,8 +328,18 @@ const FormTipComponent = defineComponent({
|
|||||||
<NInputGroupLabel>/index.vue</NInputGroupLabel>
|
<NInputGroupLabel>/index.vue</NInputGroupLabel>
|
||||||
</NInputGroup>
|
</NInputGroup>
|
||||||
</NFormItemGi>
|
</NFormItemGi>
|
||||||
<NFormItemGi v-if="isMenu" span="24" :show-feedback="!queryList.length" label="路由参数">
|
<NFormItemGi
|
||||||
<NDynamicInput v-model:value="queryList" item-style="margin-bottom: 0" :on-create="onCreate">
|
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 }">
|
<template #default="{ index }">
|
||||||
<div class="w-full flex">
|
<div class="w-full flex">
|
||||||
<NFormItem
|
<NFormItem
|
||||||
@ -358,6 +364,7 @@ const FormTipComponent = defineComponent({
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</NDynamicInput>
|
</NDynamicInput>
|
||||||
|
<NInput v-else v-model:value="model.queryParam" placeholder="请输入 iframe 地址" />
|
||||||
</NFormItemGi>
|
</NFormItemGi>
|
||||||
<NFormItemGi :span="24" path="perms">
|
<NFormItemGi :span="24" path="perms">
|
||||||
<template #label>
|
<template #label>
|
||||||
@ -377,8 +384,12 @@ const FormTipComponent = defineComponent({
|
|||||||
</template>
|
</template>
|
||||||
<NRadioGroup v-model:value="model.isFrame">
|
<NRadioGroup v-model:value="model.isFrame">
|
||||||
<NSpace>
|
<NSpace>
|
||||||
<NRadio value="0" label="是" />
|
<NRadio
|
||||||
<NRadio value="1" label="否" />
|
v-for="option in menuIsFrameOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
/>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</NRadioGroup>
|
</NRadioGroup>
|
||||||
</NFormItemGi>
|
</NFormItemGi>
|
||||||
|
Loading…
Reference in New Issue
Block a user