ruoyi-plus-soybean/src/views/system/menu/index.vue

468 lines
14 KiB
Vue

<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/custom/status-tag.vue';
import { useDict } from '@/hooks/business/dict';
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(menuType?: Api.System.MenuType) {
if (menuType === 'F') {
await getBtnMenuList();
return;
}
await getMeunTree();
if (operateType.value === 'edit') {
currentMenu.value = menuTreeRef.value?.getCheckedData().options[0] as Api.System.Menu;
}
}
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 = [];
getMeunTree();
}
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>
</>
);
}
}
];
const enableStatusRecord = ref<CommonType.Record>({});
const showHideRecord = ref<CommonType.Record>({});
async function initDictData() {
const { getDictRecord } = useDict();
const { sys_show_hide, sys_normal_disable } = await getDictRecord('sys_show_hide', 'sys_normal_disable');
enableStatusRecord.value = sys_normal_disable;
showHideRecord.value = sys_show_hide;
}
initDictData();
</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" :disabled="btnData.length > 0 || btnLoading">
<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]">
{{ showHideRecord[currentMenu.visible] }}
</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>