This commit is contained in:
吴红兵
2025-12-02 10:37:49 +08:00
commit 1f645dad3e
1183 changed files with 147673 additions and 0 deletions

View File

@@ -0,0 +1,280 @@
<template>
<el-dialog
:title="state.ruleForm.menuId ? $t('common.editBtn') : $t('common.addBtn')"
v-model="visible"
width="600"
:close-on-click-modal="false"
draggable
>
<el-form ref="menuDialogFormRef" :model="state.ruleForm" :rules="dataRules" label-width="100px" v-loading="loading">
<el-form-item :label="$t('sysmenu.menuType')" prop="menuType">
<el-radio-group v-model="state.ruleForm.menuType">
<el-radio border label="0">菜单</el-radio>
<el-radio border label="1">按钮</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('sysmenu.parentId')" prop="parentId">
<el-tree-select
v-model="state.ruleForm.parentId"
:data="state.parentData"
:render-after-expand="false"
:props="{ value: 'id', label: 'name', children: 'children' }"
class="w100"
clearable
check-strictly
:placeholder="$t('sysmenu.inputParentIdTip')"
>
</el-tree-select>
</el-form-item>
<el-form-item prop="name">
<template #label>
{{ state.ruleForm.menuType === '0' ? t('sysmenu.name') : t('sysmenu.buttonName') }}
</template>
<el-input v-model="state.ruleForm.name" clearable :placeholder="$t('sysmenu.inputNameTip')"></el-input>
</el-form-item>
<el-form-item :label="$t('sysmenu.path')" prop="path" v-if="state.ruleForm.menuType === '0'">
<el-input v-model="state.ruleForm.path" :placeholder="$t('sysmenu.inputPathTip')"/>
</el-form-item>
<el-form-item :label="$t('sysmenu.permission')" prop="permission" v-if="state.ruleForm.menuType === '1'">
<template #label>
{{ t('sysmenu.permission') }}
<tip content="对应后台接口@PreAuthorize注解入参字符串"></tip>
</template>
<el-input v-model="state.ruleForm.permission" maxlength="30" :placeholder="$t('sysmenu.inputPermissionTip')"/>
</el-form-item>
<el-form-item :label="$t('sysmenu.sortOrder')" prop="sortOrder">
<el-input-number v-model="state.ruleForm.sortOrder" :min="0" controls-position="right"/>
</el-form-item>
<el-form-item :label="$t('sysmenu.icon')" prop="icon" v-if="state.ruleForm.menuType === '0'">
<IconSelector :placeholder="$t('sysmenu.inputIconTip')" v-model="state.ruleForm.icon"/>
</el-form-item>
<el-row>
<el-col :span="12">
<el-form-item prop="keepAlive" v-if="state.ruleForm.menuType === '0'">
<template #label> {{ $t('sysmenu.keepAlive') }}
<tip content="组件保留状态,避免重新渲染"/>
</template>
<el-radio-group v-model="state.ruleForm.keepAlive">
<el-radio border label="0"></el-radio>
<el-radio border label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="visible" v-if="state.ruleForm.menuType === '0'">
<template #label> {{ $t('sysmenu.visible') }}
<tip content="左侧菜单树是否显示"/>
</template>
<el-radio-group v-model="state.ruleForm.visible">
<el-radio border label="0"></el-radio>
<el-radio border label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row class="mt-4">
<el-col :span="12">
<el-form-item prop="param" v-if="state.ruleForm.menuType === '0'">
<template #label> {{ $t('sysmenu.param') }}
<tip content="多个路径指向同一个组件"/>
</template>
<el-radio-group v-model="state.ruleForm.param">
<el-radio border label="0"></el-radio>
<el-radio border label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="embedded"
v-if="state.ruleForm.menuType === '0' && state.ruleForm.path?.startsWith('http')">
<template #label> {{ $t('sysmenu.embedded') }}
<tip content="iframe嵌套还是打开独立的Tab"/>
</template>
<el-radio-group v-model="state.ruleForm.embedded">
<el-radio border label="0"></el-radio>
<el-radio border label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item class="mt-4" :label="$t('sysmenu.component')" prop="component" v-if="state.ruleForm.menuType === '0'
&& state.ruleForm.param === '1'">
<el-input v-model="state.ruleForm.component" :placeholder="$t('sysmenu.inputComponentTip')"/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="systemMenuDialog">
import {useI18n} from 'vue-i18n';
import {getObj, pageList, putObj, addObj, validateExist} from '/@/api/admin/menu';
import {useMessage} from '/@/hooks/message';
import {rule, validateNull} from "/@/utils/validate";
import Tip from "/@/components/Tip/index.vue";
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const {t} = useI18n();
// 引入组件
const IconSelector = defineAsyncComponent(() => import('/@/components/IconSelector/index.vue'));
// 定义变量内容
const visible = ref(false);
const loading = ref(false);
const menuDialogFormRef = ref();
const originalName = ref(''); // To store the original menu name for comparison during edits
// 定义需要的数据
const state = reactive({
ruleForm: {
menuId: '',
name: '',
permission: '',
parentId: '',
icon: '',
path: '',
param: '0',
component: '',
sortOrder: 0,
menuType: '1',
keepAlive: '0',
visible: '1',
embedded: '0',
},
parentData: [] as any[], // 上级菜单数据
});
// 表单校验规则
const dataRules = reactive({
menuType: [{required: true, message: '菜单类型不能为空', trigger: 'blur'}],
parentId: [{required: true, message: '上级菜单不能为空', trigger: 'blur'}],
name: [{validator: rule.overLength, trigger: 'blur'}, {
required: true,
message: '菜单不能为空',
trigger: 'blur'
}, {
validator: (rule: any, value: any, callback: any) => {
// 如果是按钮类型菜单,跳过名称唯一性校验
if (state.ruleForm.menuType === '1') {
callback();
return;
}
// 如果是编辑状态且菜单名称未改变,跳过校验
if (state.ruleForm.menuId !== '' && value === originalName.value) {
callback();
return;
}
// 其他情况下,验证菜单名称唯一性
validateExist(rule, value, callback, false);
},
trigger: 'blur',
}],
path: [{validator: rule.overLength, trigger: 'blur'}, {required: true, message: '路径不能为空', trigger: 'blur'}],
icon: [{validator: rule.overLength, trigger: 'blur'}, {required: true, message: '图标不能为空', trigger: 'blur'}],
permission: [{validator: rule.overLength, trigger: 'blur'}, {
required: true,
message: '权限代码不能为空',
trigger: 'blur'
}, {
validator: (rule: any, value: any, callback: any) => {
validateExist(rule, value, callback, state.ruleForm.menuId !== '');
},
trigger: 'blur',
}],
sortOrder: [{required: true, message: '排序不能为空', trigger: 'blur'}],
component: [{min: 5, max: 255, message: '组件名称长度必须介于 5 和 255 之间', trigger: 'blur'},
{
validator: (rule: any, value: any, callback: any) => {
if (state.ruleForm.menuType === '0' && state.ruleForm.param === '1' && validateNull(state.ruleForm.component)) {
callback(new Error('请输入组件名称'));
} else {
return callback();
}
},
trigger: 'blur',
}],
});
// 打开弹窗
const openDialog = (type: string, row?: any) => {
state.ruleForm.menuId = '';
visible.value = true;
originalName.value = ''; // Reset the original name
nextTick(() => {
menuDialogFormRef.value?.resetFields();
state.ruleForm.parentId = row?.id || '-1';
});
if (row?.id && type === 'edit') {
state.ruleForm.menuId = row.id;
// 获取当前节点菜单信息
getMenuDetail(row.id);
}
// 渲染上级菜单列表树
getAllMenuData();
};
// 获取菜单节点的详细信息
const getMenuDetail = (id: string) => {
getObj({menuId: id}).then((res) => {
if (res.data[0].component) {
state.ruleForm.param = '1'
}
originalName.value = res.data[0].name; // Store the original name
Object.assign(state.ruleForm, res.data[0]);
});
};
// 从后端获取菜单信息(含层级)
const getAllMenuData = () => {
state.parentData = [];
pageList({
type: '0',
}).then((res) => {
let menu = {
id: '-1',
name: '根菜单',
children: [],
};
menu.children = res.data;
state.parentData.push(menu);
});
};
// 保存数据
const onSubmit = async () => {
// 立即设置 loading防止重复点击
if (loading.value) return;
loading.value = true;
try {
const valid = await menuDialogFormRef.value.validate().catch(() => {
});
if (!valid) {
loading.value = false;
return false;
}
state.ruleForm.menuId ? await putObj(state.ruleForm) : await addObj(state.ruleForm);
useMessage().success(t(state.ruleForm.menuId ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 暴露变量 只有暴漏出来的变量 父组件才能使用
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,37 @@
export default {
sysmenu: {
index: '#',
name: 'menu name',
buttonName: 'button name',
sortOrder: 'sortOrder',
path: 'path',
menuType: 'menuType',
keepAlive: 'keepAlive',
permission: 'permission',
inputNameTip: 'input name',
parentId: 'parent menu',
embedded: 'embedded',
param: 'param',
component: 'component',
visible: 'visible',
icon: 'icon',
inputMenuIdTip: 'input menuId',
inputPermissionTip: 'input permission',
inputPathTip: 'input path',
inputParentIdTip: 'input parentId',
inputIconTip: 'input icon',
inputVisibleTip: 'input visible',
inputSortOrderTip: 'input sortOrder',
inputKeepAliveTip: 'input keepAlive',
inputMenuTypeTip: 'input menuType',
inputCreateByTip: 'input createBy',
inputCreateTimeTip: 'input createTime',
inputUpdateByTip: 'input updateBy',
inputUpdateTimeTip: 'input updateTime',
inputDelFlagTip: 'input delFlag',
inputTenantIdTip: 'input tenantId',
inputEmbeddedTip: 'input embedded',
inputComponentTip: 'input component',
deleteDisabledTip: 'menu inclusion subordinates cannot be deleted',
},
};

View File

@@ -0,0 +1,31 @@
export default {
sysmenu: {
index: '#',
name: '菜单名称',
buttonName: '按钮名称',
sortOrder: '排序',
path: '路由',
menuType: '类型',
keepAlive: '缓冲',
permission: '权限标识',
inputNameTip: '请输入菜单名称',
parentId: '上级菜单',
embedded: '内嵌',
param: '带参',
component: '组件',
visible: '显示',
icon: '图标',
inputMenuIdTip: '',
inputPermissionTip: '请输入权限标识',
inputPathTip: '请输入路由路径',
inputParentIdTip: '请选择上级菜单',
inputIconTip: '请选择图标',
inputVisibleTip: '请选择是否显示',
inputSortOrderTip: '请输入排序',
inputKeepAliveTip: '请选择是否缓冲',
inputMenuTypeTip: '请选择菜单类型',
inputEmbeddedTip: '请选择是否内嵌',
inputComponentTip: '请输入组件名称',
deleteDisabledTip: '菜单包含下级不能删除',
},
};

View File

@@ -0,0 +1,197 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row shadow="hover" v-show="showSearch" class="ml10">
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
<el-form-item :label="$t('sysmenu.name')" prop="menuName">
<el-input :placeholder="$t('sysmenu.inputNameTip')" clearable style="max-width: 180px" v-model="state.queryForm.menuName" />
</el-form-item>
<el-form-item>
<el-button @click="query" class="ml10" icon="search" type="primary">
{{ $t('common.queryBtn') }}
</el-button>
<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button @click="onOpenAddMenu" class="ml10" icon="folder-add" type="primary" v-auth="'sys_menu_add'">
{{ $t('common.addBtn') }}
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
style="float: right; margin-right: 20px"
@queryTable="getDataList"
></right-toolbar>
</div>
</el-row>
<el-table
ref="tableRef"
:data="tableList"
lazy
:load="load"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
row-key="path"
style="width: 100%"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle?.headerCellStyle"
>
<el-table-column :label="$t('sysmenu.name')" fixed prop="name" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('sysmenu.sortOrder')" prop="sortOrder" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('sysmenu.icon')" prop="icon" show-overflow-tooltip>
<template #default="scope">
<SvgIcon :name="scope.row.meta.icon" />
</template>
</el-table-column>
<el-table-column :label="$t('sysmenu.path')" prop="path" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('sysmenu.menuType')" show-overflow-tooltip>
<template #default="scope">
<el-tag v-if="scope.row.menuType === '0'">左菜单</el-tag>
<el-tag v-if="scope.row.menuType === '2'">顶菜单</el-tag>
<el-tag type="success" v-if="scope.row.menuType === '1'">按钮</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('sysmenu.keepAlive')" show-overflow-tooltip>
<template #default="scope">
<el-tag v-if="scope.row.meta.isKeepAlive">开启</el-tag>
<el-tag type="info" v-else>关闭</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('sysmenu.permission')" :show-overflow-tooltip="true" prop="permission"></el-table-column>
<el-table-column :label="$t('common.action')" show-overflow-tooltip width="250">
<template #default="scope">
<el-button icon="folder-add" @click="onOpenAddMenu('add', scope.row)" text type="primary" v-auth="'sys_menu_add'">
{{ $t('common.addBtn') }}
</el-button>
<el-button icon="edit-pen" @click="onOpenEditMenu('edit', scope.row)" text type="primary" v-auth="'sys_menu_edit'"
>{{ $t('common.editBtn') }}
</el-button>
<el-tooltip icon="delete" :content="$t('sysmenu.deleteDisabledTip')" :disabled="!deleteMenuDisabled(scope.row)" placement="top">
<span style="margin-left: 12px">
<el-button
icon="delete"
:disabled="deleteMenuDisabled(scope.row)"
@click="handleDelete(scope.row)"
text
type="primary"
v-auth="'sys_menu_del'"
>
{{ $t('common.delBtn') }}
</el-button>
</span>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
<MenuDialog @refresh="query()" ref="menuDialogRef" />
</div>
</template>
<script lang="ts" name="systemMenu" setup>
import { delObj, pageList } from '/@/api/admin/menu';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
// 引入组件
const MenuDialog = defineAsyncComponent(() => import('./form.vue'));
const { t } = useI18n();
// 定义变量内容
const tableRef = ref();
const menuDialogRef = ref();
const showSearch = ref(true);
const queryRef = ref();
const state: BasicTableProps = reactive<BasicTableProps>({
pageList: pageList, // H
queryForm: {
parentId: -1,
menuName: '',
},
isPage: false,
});
const { getDataList, tableStyle } = useTable(state);
// 根据类型判断是否有子节点
const setHasChildren = (arr: any[]) => {
arr.forEach((item) => {
// 添加 hasChildren 属性
item.hasChildren = item.menuType !== '1';
});
};
const tableList = computed(() => {
const list = state.dataList;
if (Array.isArray(list)) {
setHasChildren(list);
}
return list;
});
// 打开新增菜单弹窗
const onOpenAddMenu = (type?: string, row?: any) => {
menuDialogRef.value.openDialog(type, row);
};
// 打开编辑菜单弹窗
const onOpenEditMenu = (type: string, row: any) => {
menuDialogRef.value.openDialog(type, row);
};
//是否禁用删除
const deleteMenuDisabled = (row: any) => {
return (row.children || []).length > 0;
};
// 搜索事件
const query = () => {
state.dataList = [];
state.queryForm.parentId = undefined;
getDataList();
};
// 清空搜索条件
const resetQuery = () => {
queryRef.value.resetFields();
state.dataList = [];
getDataList();
};
const load = (row: any, treeNode: unknown, resolve: (date: any[]) => void) => {
const param = {
parentId: row.id,
};
pageList(param)
.then((res) => {
const childrenList = res.data || [];
if (Array.isArray(childrenList)) {
setHasChildren(childrenList);
}
resolve(childrenList);
})
.catch(() => {
// Handle API error by resolving with empty array
resolve([]);
});
};
// 删除操作
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(row.id);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>