mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-24 07:49:47 +08:00
feat: 新增菜单管理页面
This commit is contained in:
453
src/views/system/menu/index.vue
Normal file
453
src/views/system/menu/index.vue
Normal file
@ -0,0 +1,453 @@
|
||||
<script setup lang="tsx">
|
||||
import { ref } from 'vue';
|
||||
import { useBoolean, useLoading } from '@sa/hooks';
|
||||
import type { DataTableColumns, TreeInst, TreeOption } from 'naive-ui';
|
||||
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 ButtonIcon from '@/components/custom/button-icon.vue';
|
||||
import { $t } from '@/locales';
|
||||
import { handleMenuTree } from '@/utils/ruoyi';
|
||||
import StatusTag from '@/components/common/status-tag.vue';
|
||||
import MenuOperateDrawer from './modules/menu-operate-drawer.vue';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const editingData = ref<Api.System.Menu>();
|
||||
const operateType = ref<NaiveUI.TableOperateType>('add');
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
const { bool: drawerVisible, setTrue: openDrawer } = useBoolean();
|
||||
const { loading: btnLoading, startLoading: startBtnLoading, endLoading: endBtnLoading } = useLoading();
|
||||
/** tree pattern name , use tree search */
|
||||
const name = ref<string>();
|
||||
const createType = ref<Api.System.MenuType>();
|
||||
const createPid = ref<CommonType.IdType>(0);
|
||||
const currentMenu = ref<Api.System.Menu>();
|
||||
const treeData = ref<Api.System.Menu[]>([]);
|
||||
const checkedKeys = ref<CommonType.IdType[]>([0]);
|
||||
const expandedKeys = ref<CommonType.IdType[]>([0]);
|
||||
|
||||
const menuTreeRef = ref<TreeInst>();
|
||||
const btnData = ref<Api.System.MenuList>([]);
|
||||
|
||||
const getMeunTree = async () => {
|
||||
startLoading();
|
||||
const { data, error } = await fetchGetMenuList();
|
||||
if (!error) {
|
||||
treeData.value = [
|
||||
{
|
||||
menuId: 0,
|
||||
menuName: '根目录',
|
||||
icon: 'material-symbols:home-outline-rounded',
|
||||
children: handleMenuTree(data, 'menuId')
|
||||
}
|
||||
] as Api.System.Menu[];
|
||||
}
|
||||
endLoading();
|
||||
};
|
||||
|
||||
getMeunTree();
|
||||
|
||||
async function handleSubmitted() {
|
||||
getMeunTree();
|
||||
if (operateType.value === 'edit') {
|
||||
currentMenu.value = menuTreeRef.value?.getCheckedData().options[0] as Api.System.Menu;
|
||||
}
|
||||
if (createType.value === 'F') {
|
||||
getBtnMenuList();
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddMenu(pid: CommonType.IdType) {
|
||||
createPid.value = pid;
|
||||
createType.value = pid === 0 ? 'M' : 'C';
|
||||
operateType.value = 'add';
|
||||
openDrawer();
|
||||
}
|
||||
|
||||
function handleUpdateMenu() {
|
||||
operateType.value = 'edit';
|
||||
editingData.value = currentMenu.value;
|
||||
openDrawer();
|
||||
}
|
||||
|
||||
async function handleDeleteMenu(id?: CommonType.IdType) {
|
||||
const { error } = await fetchDeleteMenu(id || checkedKeys.value[0]);
|
||||
if (error) return;
|
||||
window.$message?.success($t('common.deleteSuccess'));
|
||||
if (id) {
|
||||
getBtnMenuList();
|
||||
return;
|
||||
}
|
||||
expandedKeys.value.filter(item => !checkedKeys.value.includes(item));
|
||||
currentMenu.value = undefined;
|
||||
checkedKeys.value = [];
|
||||
getBtnMenuList();
|
||||
}
|
||||
|
||||
function renderPrefix({ option }: { option: TreeOption }) {
|
||||
const renderLocalIcon = String(option.icon).startsWith('icon-');
|
||||
const icon = renderLocalIcon ? undefined : String(option.icon);
|
||||
const localIcon = renderLocalIcon ? String(option.icon).replace('icon-', 'menu-') : undefined;
|
||||
return <SvgIcon icon={icon} localIcon={localIcon} />;
|
||||
}
|
||||
|
||||
function renderSuffix({ option }: { option: TreeOption }) {
|
||||
if (!['M'].includes(String(option.menuType))) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="flex-center gap-8px">
|
||||
<ButtonIcon
|
||||
text
|
||||
class="h-18px"
|
||||
icon="ic-round-plus"
|
||||
tooltip-content="新增子菜单"
|
||||
onClick={(event: Event) => {
|
||||
event.stopPropagation();
|
||||
handleAddMenu(option.menuId as CommonType.IdType);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
name.value = undefined;
|
||||
getMeunTree();
|
||||
}
|
||||
|
||||
function handleClickTree(option: Array<TreeOption | null>) {
|
||||
checkedKeys.value = option?.map(item => item?.menuId as CommonType.IdType);
|
||||
|
||||
const menu = option[0] as Api.System.Menu;
|
||||
if (menu?.menuId === 0) {
|
||||
return;
|
||||
}
|
||||
currentMenu.value = menu;
|
||||
getBtnMenuList();
|
||||
}
|
||||
|
||||
const tagMap: Record<Api.Common.EnableStatus, NaiveUI.ThemeColor> = {
|
||||
'0': 'success',
|
||||
'1': 'warning'
|
||||
};
|
||||
|
||||
async function getBtnMenuList() {
|
||||
if (!currentMenu.value?.menuId) {
|
||||
return;
|
||||
}
|
||||
startBtnLoading();
|
||||
btnData.value = [];
|
||||
const { data, error } = await fetchGetMenuList({ parentId: currentMenu.value?.menuId, menuType: 'F' });
|
||||
if (error) return;
|
||||
btnData.value = data || [];
|
||||
endBtnLoading();
|
||||
}
|
||||
|
||||
function addBtnMenu() {
|
||||
operateType.value = 'add';
|
||||
createType.value = 'F';
|
||||
createPid.value = currentMenu.value?.menuId || 0;
|
||||
openDrawer();
|
||||
}
|
||||
|
||||
function handleDeleteBtnMenu(id: CommonType.IdType) {
|
||||
handleDeleteMenu(id);
|
||||
}
|
||||
|
||||
function handleUpdateBtnMenu(row: Api.System.Menu) {
|
||||
operateType.value = 'edit';
|
||||
editingData.value = row;
|
||||
openDrawer();
|
||||
}
|
||||
|
||||
const btnColumns: DataTableColumns<Api.System.Menu> = [
|
||||
{
|
||||
key: 'index',
|
||||
width: 64,
|
||||
align: 'center',
|
||||
title() {
|
||||
return (
|
||||
<NButton circle type="primary" size="small" onClick={() => addBtnMenu()}>
|
||||
{{
|
||||
icon: () => (
|
||||
<NIcon>
|
||||
<SvgIcon icon="ic-round-plus" />
|
||||
</NIcon>
|
||||
)
|
||||
}}
|
||||
</NButton>
|
||||
);
|
||||
},
|
||||
render(_, index) {
|
||||
return index + 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '权限名称',
|
||||
key: 'menuName',
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
title: '权限标识',
|
||||
key: 'perms',
|
||||
align: 'center',
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
minWidth: 80,
|
||||
align: 'center',
|
||||
render(row) {
|
||||
return <StatusTag size="small" value={row.status} />;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
minWidth: 150
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
render(row) {
|
||||
return (
|
||||
<>
|
||||
<ButtonIcon
|
||||
type="primary"
|
||||
text
|
||||
icon="ep:edit"
|
||||
tooltipContent="修改"
|
||||
onClick={() => handleUpdateBtnMenu(row)}
|
||||
/>
|
||||
<NTooltip placement="bottom">
|
||||
{{
|
||||
trigger: () => (
|
||||
<NPopconfirm onPositiveClick={() => handleDeleteBtnMenu(row.menuId!)}>
|
||||
{{
|
||||
default: () => $t('common.confirmDelete'),
|
||||
trigger: () => (
|
||||
<NButton class="ml-16px h-36px text-icon" type="error" text>
|
||||
{{
|
||||
default: () => (
|
||||
<div class="flex-center gap-8px">
|
||||
<SvgIcon icon="ep:delete" />
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</NButton>
|
||||
)
|
||||
}}
|
||||
</NPopconfirm>
|
||||
),
|
||||
default: () => <>{$t('common.delete')}</>
|
||||
}}
|
||||
</NTooltip>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableSiderLayout default-expanded>
|
||||
<template #header>菜单列表</template>
|
||||
<template #header-extra>
|
||||
<ButtonIcon
|
||||
size="small"
|
||||
icon="ic-round-plus"
|
||||
class="h-28px text-icon"
|
||||
tooltip-content="新增菜单"
|
||||
@click.stop="handleAddMenu(0)"
|
||||
/>
|
||||
<ButtonIcon
|
||||
size="small"
|
||||
icon="ic-round-refresh"
|
||||
class="h-28px text-icon"
|
||||
tooltip-content="刷新"
|
||||
@click.stop="reset"
|
||||
/>
|
||||
</template>
|
||||
<template #sider>
|
||||
<div class="flex gap-6px">
|
||||
<NInput v-model:value="name" size="small" placeholder="请输入菜单名称" />
|
||||
</div>
|
||||
<NSpin :show="loading" class="infinite-scroll">
|
||||
<NTree
|
||||
ref="menuTreeRef"
|
||||
v-model:checked-keys="checkedKeys"
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
:cancelable="false"
|
||||
:data="treeData as []"
|
||||
:default-expanded-keys="[0]"
|
||||
:show-irrelevant-nodes="false"
|
||||
:pattern="name"
|
||||
block-line
|
||||
class="h-full min-h-200px py-3"
|
||||
key-field="menuId"
|
||||
label-field="menuName"
|
||||
virtual-scroll
|
||||
checkable
|
||||
:render-prefix="renderPrefix"
|
||||
:render-suffix="renderSuffix"
|
||||
@update:selected-keys="(_: Array<string & number>, option: Array<TreeOption | null>) => handleClickTree(option)"
|
||||
>
|
||||
<template #empty>
|
||||
<NEmpty description="暂无菜单" class="h-full min-h-200px justify-center" />
|
||||
</template>
|
||||
</NTree>
|
||||
</NSpin>
|
||||
</template>
|
||||
<div class="h-full flex-col-stretch gap-16px">
|
||||
<template v-if="currentMenu">
|
||||
<NCard
|
||||
title="菜单详情"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
class="max-h-50% card-wrapper"
|
||||
content-class="overflow-auto mb-12px"
|
||||
>
|
||||
<template #header-extra>
|
||||
<NSpace>
|
||||
<NButton
|
||||
v-if="currentMenu.menuType === 'M'"
|
||||
size="small"
|
||||
ghost
|
||||
type="primary"
|
||||
@click="handleAddMenu(currentMenu.menuId!)"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-ic-round-plus />
|
||||
</template>
|
||||
新增子菜单
|
||||
</NButton>
|
||||
<NButton size="small" ghost type="primary" @click="handleUpdateMenu">
|
||||
<template #icon>
|
||||
<icon-ic-round-edit />
|
||||
</template>
|
||||
编辑
|
||||
</NButton>
|
||||
<NPopconfirm @positive-click="() => handleDeleteMenu()">
|
||||
<template #trigger>
|
||||
<NButton size="small" ghost type="error">
|
||||
<template #icon>
|
||||
<icon-ic-round-delete />
|
||||
</template>
|
||||
{{ $t('common.delete') }}
|
||||
</NButton>
|
||||
</template>
|
||||
{{ $t('common.confirmDelete') }}
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
</template>
|
||||
<NDescriptions
|
||||
label-placement="left"
|
||||
size="small"
|
||||
bordered
|
||||
:column="appStore.isMobile ? 1 : 2"
|
||||
label-class="w-20% min-w-88px"
|
||||
content-class="w-100px"
|
||||
>
|
||||
<NDescriptionsItem label="菜单类型">
|
||||
<NTag size="small" type="primary">{{ menuTypeRecord[currentMenu.menuType!] }}</NTag>
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="菜单状态">
|
||||
<StatusTag size="small" :value="currentMenu.status" />
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="菜单名称">{{ currentMenu.menuName }}</NDescriptionsItem>
|
||||
<NDescriptionsItem v-if="currentMenu.menuType === 'C'" label="组件路径">
|
||||
{{ currentMenu.component }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="路由地址">{{ currentMenu.path }}</NDescriptionsItem>
|
||||
<NDescriptionsItem v-if="currentMenu.menuType === 'C'" label="路由参数">
|
||||
{{ currentMenu.queryParam }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem v-if="currentMenu.menuType !== 'M'" label="权限标识">
|
||||
{{ currentMenu.perms }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="是否外链">
|
||||
<BooleanTag size="small" :value="currentMenu.isFrame!" />
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="显示状态">
|
||||
<NTag v-if="currentMenu.visible" size="small" :type="tagMap[currentMenu.visible]">
|
||||
{{ currentMenu.visible === '0' ? '显示' : '隐藏' }}
|
||||
</NTag>
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem v-if="currentMenu.menuType === 'C'" label="是否缓存">
|
||||
<NTag v-if="currentMenu.isCache" size="small" :type="tagMap[currentMenu.isCache]">
|
||||
{{ currentMenu.isCache === '0' ? '缓存' : '不缓存' }}
|
||||
</NTag>
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</NCard>
|
||||
|
||||
<NCard
|
||||
title="接口权限列表"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
class="h-full overflow-auto card-wrapper"
|
||||
content-class="overflow-auto mb-12px"
|
||||
>
|
||||
<template #header-extra>
|
||||
<ButtonIcon
|
||||
size="small"
|
||||
icon="ic-round-refresh"
|
||||
class="h-28px text-icon"
|
||||
tooltip-content="刷新"
|
||||
@click.stop="getBtnMenuList"
|
||||
/>
|
||||
</template>
|
||||
<NDataTable class="h-full" :loading="btnLoading" :columns="btnColumns" :data="btnData" />
|
||||
</NCard>
|
||||
</template>
|
||||
<NCard v-else :bordered="false" size="small" class="h-full card-wrapper">
|
||||
<NEmpty class="h-full flex-center" size="large" />
|
||||
</NCard>
|
||||
</div>
|
||||
<MenuOperateDrawer
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
:tree-data="treeData"
|
||||
:pid="createPid"
|
||||
:menu-type="createType"
|
||||
@submitted="handleSubmitted"
|
||||
/>
|
||||
</TableSiderLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.infinite-scroll) {
|
||||
height: calc(100vh - 224px - var(--calc-footer-height, 0px)) !important;
|
||||
max-height: calc(100vh - 224px - var(--calc-footer-height, 0px)) !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
:deep(.infinite-scroll) {
|
||||
height: calc(100vh - 227px - var(--calc-footer-height, 0px)) !important;
|
||||
max-height: calc(100vh - 227px - var(--calc-footer-height, 0px)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.n-spin-content) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.n-tree-node-checkbox) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.n-data-table-base-table) {
|
||||
height: 100% !important;
|
||||
}
|
||||
</style>
|
425
src/views/system/menu/modules/menu-operate-drawer.vue
Normal file
425
src/views/system/menu/modules/menu-operate-drawer.vue
Normal file
@ -0,0 +1,425 @@
|
||||
<script setup lang="tsx">
|
||||
import { computed, defineComponent, reactive, ref, watch } from 'vue';
|
||||
import type { SelectOption } from 'naive-ui';
|
||||
import { NTooltip } from 'naive-ui';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { $t } from '@/locales';
|
||||
import { fetchCreateMenu, fetchUpdateMenu } from '@/service/api';
|
||||
import { enableStatusOptions, menuIconTypeOptions, menuTypeOptions } from '@/constants/business';
|
||||
import SvgIcon from '@/components/custom/svg-icon.vue';
|
||||
import { getLocalMenuIcons } from '@/utils/icon';
|
||||
import { humpToLine, isNotNull } from '@/utils/common';
|
||||
|
||||
defineOptions({
|
||||
name: 'MenuOperateDrawer'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** the type of operation */
|
||||
operateType: NaiveUI.TableOperateType;
|
||||
/** the edit row data */
|
||||
rowData?: Api.System.Menu | null;
|
||||
/** tree option data */
|
||||
treeData?: Api.System.Menu[] | null;
|
||||
/** parent id */
|
||||
pid?: string | number;
|
||||
/** menu type */
|
||||
menuType?: Api.System.MenuType;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const iconType = ref<Api.System.IconType>('1');
|
||||
const { formRef, validate, restoreValidation } = useNaiveForm();
|
||||
const { defaultRequiredRule } = useFormRules();
|
||||
const queryList = ref<{ key: string; value: string }[]>([]);
|
||||
|
||||
const drawerTitle = computed(() => {
|
||||
const titles: Record<NaiveUI.TableOperateType, string> = {
|
||||
add: '新增菜单',
|
||||
edit: '编辑菜单'
|
||||
};
|
||||
return titles[props.operateType];
|
||||
});
|
||||
|
||||
type Model = Api.System.MenuOperateParams;
|
||||
|
||||
const model: Model = reactive(createDefaultModel());
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
parentId: props.pid || 0,
|
||||
menuName: '',
|
||||
orderNum: 1,
|
||||
path: '',
|
||||
component: '',
|
||||
queryParam: '',
|
||||
isFrame: '1',
|
||||
isCache: '1',
|
||||
menuType: props.menuType || 'M',
|
||||
visible: '0',
|
||||
status: '0',
|
||||
perms: '',
|
||||
icon: '',
|
||||
remark: ''
|
||||
};
|
||||
}
|
||||
|
||||
type RuleKey = Extract<keyof Model, 'menuName' | 'orderNum' | 'path' | 'component'>;
|
||||
|
||||
const rules: Record<RuleKey, App.Global.FormRule> = {
|
||||
menuName: { ...defaultRequiredRule, message: '菜单名称不能为空' },
|
||||
orderNum: { ...defaultRequiredRule, type: 'number', message: '菜单排序不能为空' },
|
||||
path: { ...defaultRequiredRule, message: '路由地址不能为空' },
|
||||
component: { ...defaultRequiredRule, message: '组件路径不能为空' }
|
||||
};
|
||||
|
||||
const isBtn = computed(() => model.menuType === 'F');
|
||||
const isMenu = computed(() => model.menuType === 'C');
|
||||
const localIcons = getLocalMenuIcons();
|
||||
const localIconOptions = localIcons.map<SelectOption>(item => ({
|
||||
label: () => (
|
||||
<div class="flex-y-center gap-16px">
|
||||
<SvgIcon localIcon={`menu-${item}`} class="text-icon" />
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
),
|
||||
value: `icon-${item}`
|
||||
}));
|
||||
|
||||
function handleInitModel() {
|
||||
queryList.value = [];
|
||||
Object.assign(model, createDefaultModel());
|
||||
|
||||
if (props.operateType === 'edit' && props.rowData) {
|
||||
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] }));
|
||||
}
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
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,
|
||||
menuName,
|
||||
orderNum,
|
||||
queryParam,
|
||||
isFrame,
|
||||
isCache,
|
||||
menuType,
|
||||
visible: menuVisible,
|
||||
status,
|
||||
perms,
|
||||
remark
|
||||
} = model;
|
||||
|
||||
const path = !model.path?.startsWith('/') ? `/${model.path}` : model.path;
|
||||
|
||||
let icon;
|
||||
if (model.icon) {
|
||||
icon = iconType.value === '1' ? model.icon : model.icon?.replace('menu-', 'icon-');
|
||||
}
|
||||
|
||||
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') {
|
||||
component = 'iframe-page';
|
||||
}
|
||||
|
||||
// request
|
||||
if (props.operateType === 'add') {
|
||||
const { error } = await fetchCreateMenu({
|
||||
menuName,
|
||||
path,
|
||||
parentId,
|
||||
orderNum,
|
||||
queryParam,
|
||||
isFrame,
|
||||
isCache,
|
||||
menuType,
|
||||
visible: menuVisible,
|
||||
status,
|
||||
perms,
|
||||
icon,
|
||||
component,
|
||||
remark
|
||||
});
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
window.$message?.success($t('common.addSuccess'));
|
||||
}
|
||||
|
||||
if (props.operateType === 'edit') {
|
||||
const { error } = await fetchUpdateMenu({
|
||||
menuId,
|
||||
menuName,
|
||||
path,
|
||||
parentId,
|
||||
orderNum,
|
||||
queryParam,
|
||||
isFrame,
|
||||
isCache,
|
||||
menuType,
|
||||
visible: menuVisible,
|
||||
status,
|
||||
perms,
|
||||
icon,
|
||||
component,
|
||||
remark
|
||||
});
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
window.$message?.success($t('common.updateSuccess'));
|
||||
}
|
||||
|
||||
closeDrawer();
|
||||
emit('submitted');
|
||||
}
|
||||
|
||||
watch(visible, () => {
|
||||
if (visible.value) {
|
||||
handleInitModel();
|
||||
restoreValidation();
|
||||
}
|
||||
});
|
||||
|
||||
function onCreate() {
|
||||
return {
|
||||
key: '',
|
||||
value: ''
|
||||
};
|
||||
}
|
||||
|
||||
const FormTipComponent = defineComponent({
|
||||
name: 'FormTipComponent',
|
||||
props: {
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
setup(tipProps) {
|
||||
return () => (
|
||||
<NTooltip trigger="hover">
|
||||
{{
|
||||
default: () => <span>{tipProps.content}</span>,
|
||||
trigger: () => (
|
||||
<div>
|
||||
<SvgIcon class="text-15px" icon="ep:warning" />
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</NTooltip>
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="800" class="max-w-90%">
|
||||
<NDrawerContent :title="drawerTitle" :native-scrollbar="false" closable>
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NGrid responsive="screen" item-responsive>
|
||||
<NFormItemGi :span="24" label="上级菜单" path="pid">
|
||||
<NTreeSelect
|
||||
v-model:value="model.parentId"
|
||||
:options="treeData as []"
|
||||
label-field="menuName"
|
||||
key-field="menuId"
|
||||
:default-expanded-keys="[0]"
|
||||
placeholder="请选择上级菜单"
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi v-if="menuType !== 'F'" :span="24" label="菜单类型" path="menuType">
|
||||
<NRadioGroup v-model:value="model.menuType">
|
||||
<NRadioButton v-for="item in menuTypeOptions" :key="item.value" :value="item.value" :label="item.label" />
|
||||
</NRadioGroup>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi :span="24" label="菜单名称" path="menuName">
|
||||
<NInput v-model:value="model.menuName" placeholder="请输入菜单名称" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi v-if="!isBtn" span="24" label="图标类型">
|
||||
<NRadioGroup v-model:value="iconType">
|
||||
<NRadio v-for="item in menuIconTypeOptions" :key="item.value" :value="item.value" :label="item.label" />
|
||||
</NRadioGroup>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi v-if="!isBtn" span="24" path="icon">
|
||||
<template #label>
|
||||
<div class="flex-center">
|
||||
<FormTipComponent content="iconify 地址:`https://icones.js.org`" />
|
||||
<span class="pl-3px">菜单图标</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="iconType === '1'">
|
||||
<NInput v-model:value="model.icon" placeholder="请输入图标" class="flex-1">
|
||||
<template #suffix>
|
||||
<SvgIcon v-if="model.icon" :icon="model.icon" class="text-icon" />
|
||||
</template>
|
||||
</NInput>
|
||||
</template>
|
||||
<template v-if="iconType === '2'">
|
||||
<NSelect v-model:value="model.icon" placeholder="请选择本地图标" :options="localIconOptions" />
|
||||
</template>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi v-if="!isBtn" :span="24" path="path">
|
||||
<template #label>
|
||||
<div class="flex-center">
|
||||
<FormTipComponent content="访问的路由地址,如:`/user`,如外网地址需内链访问则以 `http(s)://` 开头" />
|
||||
<span class="pl-3px">路由地址</span>
|
||||
</div>
|
||||
</template>
|
||||
<NInput v-model:value="model.path" placeholder="请输入路由地址" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi v-if="isMenu" :span="24" path="component">
|
||||
<template #label>
|
||||
<div class="flex-center">
|
||||
<FormTipComponent content="访问的组件路径,如:`system/user`,默认在`views`目录下" />
|
||||
<span class="pl-3px">组件路径</span>
|
||||
</div>
|
||||
</template>
|
||||
<NInputGroup>
|
||||
<NInputGroupLabel>views/</NInputGroupLabel>
|
||||
<NInput v-model:value="model.component" placeholder="请输入组件地址" />
|
||||
<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">
|
||||
<template #default="{ index }">
|
||||
<div class="w-full flex">
|
||||
<NFormItem
|
||||
class="w-full"
|
||||
ignore-path-change
|
||||
:show-label="false"
|
||||
:path="`query[${index}].key`"
|
||||
:rule="{ ...defaultRequiredRule, validator: value => isNotNull(value) }"
|
||||
>
|
||||
<NInput v-model:value="queryList[index].key" placeholder="Key" @keydown.enter.prevent />
|
||||
</NFormItem>
|
||||
<div class="mx-8px h-34px lh-34px">=</div>
|
||||
<NFormItem
|
||||
class="w-full"
|
||||
ignore-path-change
|
||||
:show-label="false"
|
||||
:path="`query[${index}].value`"
|
||||
:rule="{ ...defaultRequiredRule, validator: value => isNotNull(value) }"
|
||||
>
|
||||
<NInput v-model:value="queryList[index].value" placeholder="Value" @keydown.enter.prevent />
|
||||
</NFormItem>
|
||||
</div>
|
||||
</template>
|
||||
</NDynamicInput>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi :span="24" path="perms">
|
||||
<template #label>
|
||||
<div class="flex-center">
|
||||
<FormTipComponent content="控制器中定义的权限字符,如:@SaCheckPermission('system:user:list')" />
|
||||
<span class="pl-3px">权限字符</span>
|
||||
</div>
|
||||
</template>
|
||||
<NInput v-model:value="model.perms" placeholder="请输入菜单名称" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi v-if="!isBtn" :span="12" path="isFrame">
|
||||
<template #label>
|
||||
<div class="flex-center">
|
||||
<FormTipComponent content="选择是外链则路由地址需要以`http(s)://`开头" />
|
||||
<span class="pl-3px">是否外链</span>
|
||||
</div>
|
||||
</template>
|
||||
<NRadioGroup v-model:value="model.isFrame">
|
||||
<NSpace>
|
||||
<NRadio value="0" label="是" />
|
||||
<NRadio value="1" label="否" />
|
||||
</NSpace>
|
||||
</NRadioGroup>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi v-if="isMenu" :span="12" path="isCache">
|
||||
<template #label>
|
||||
<div class="flex-center">
|
||||
<FormTipComponent content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" />
|
||||
<span class="pl-3px">是否缓存</span>
|
||||
</div>
|
||||
</template>
|
||||
<NRadioGroup v-model:value="model.isCache">
|
||||
<NSpace>
|
||||
<NRadio value="0" label="是" />
|
||||
<NRadio value="1" label="否" />
|
||||
</NSpace>
|
||||
</NRadioGroup>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi v-if="!isBtn" :span="12" label="显示状态" path="visible">
|
||||
<template #label>
|
||||
<div class="flex-center">
|
||||
<FormTipComponent content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" />
|
||||
<span class="pl-3px">显示状态</span>
|
||||
</div>
|
||||
</template>
|
||||
<NRadioGroup v-model:value="model.visible">
|
||||
<NSpace>
|
||||
<NRadio value="0" label="显示" />
|
||||
<NRadio value="1" label="隐藏" />
|
||||
</NSpace>
|
||||
</NRadioGroup>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi :span="12" path="status">
|
||||
<template #label>
|
||||
<div class="flex-center">
|
||||
<FormTipComponent content="选择停用则路由将不会出现在侧边栏,也不能被访问" />
|
||||
<span class="pl-3px">菜单状态</span>
|
||||
</div>
|
||||
</template>
|
||||
<NRadioGroup v-model:value="model.status">
|
||||
<NSpace>
|
||||
<NRadio v-for="item in enableStatusOptions" :key="item.value" :value="item.value" :label="item.label" />
|
||||
</NSpace>
|
||||
</NRadioGroup>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi :span="12" label="显示排序" path="orderNum">
|
||||
<NInputNumber v-model:value="model.orderNum" placeholder="请输入排序" />
|
||||
</NFormItemGi>
|
||||
</NGrid>
|
||||
</NForm>
|
||||
<template #footer>
|
||||
<NSpace :size="16">
|
||||
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
|
||||
<NButton type="primary" @click="handleSubmit">{{ $t('common.save') }}</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
Reference in New Issue
Block a user