backend/src/views/system/menus/components/edit.vue
2026-03-21 14:12:44 +08:00

441 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- 添加/编辑菜单对话框 -->
<el-dialog :model-value="visible" :title="dialogTitle" width="500px" :close-on-click-modal="false" @update:model-value="handleDialogClose">
<el-form
:model="currentMenu"
label-width="100px"
:rules="formRules"
ref="menuFormRef"
>
<el-form-item label="父级菜单" prop="pid">
<el-cascader
:model-value="pidValue"
@update:model-value="handlePidUpdate"
:options="parentMenuOptions"
:props="cascaderProps"
placeholder="请选择父级菜单"
clearable
style="width: 100%"
/>
</el-form-item>
<el-form-item label="菜单名称" prop="title">
<el-input v-model="currentMenu.title" placeholder="请输入菜单名称" />
</el-form-item>
<el-form-item label="菜单类型" prop="type">
<el-radio-group v-model="currentMenu.type" style="width: 100%">
<el-radio-button :value="1">目录</el-radio-button>
<el-radio-button :value="2">页面</el-radio-button>
<el-radio-button :value="3">接口</el-radio-button>
</el-radio-group>
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary);">
<div> 目录只有路由地址用于<span style="color: var(--el-color-primary);">目录管理</span><span style="color: var(--el-color-primary);">菜单分组</span></div>
<div> 页面有路由和组件地址用于<span style="color: var(--el-color-primary);">页面管理</span></div>
<div> 接口无路由和组件用于<span style="color: var(--el-color-primary);">接口管理</span><span style="color: var(--el-color-primary);">权限控制</span></div>
</div>
</el-form-item>
<el-form-item
label="路由地址"
prop="path"
v-if="currentMenu.type !== 3"
>
<el-input v-model="currentMenu.path" placeholder="例如:/system" />
</el-form-item>
<el-form-item
label="组件路径"
prop="component_path"
v-if="currentMenu.type === 2"
>
<el-input
v-model="currentMenu.component_path"
placeholder="例如:/apps/knowledge/index.vue"
/>
</el-form-item>
<el-form-item label="图标" prop="icon">
<el-input
v-model="currentMenu.icon"
placeholder="例如fas fa-tachometer-alt"
/>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number
v-model="currentMenu.sort"
:min="0"
placeholder="数字越小越靠前"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch
v-model="currentMenu.status"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
<el-form-item label="是否显示" prop="is_visible">
<el-switch
v-model="currentMenu.is_visible"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
<el-form-item label="权限标识" prop="permission">
<el-input
v-model="currentMenu.permission"
placeholder="请输入权限标识"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSave">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch } from "vue";
import { ElMessage, ElForm } from "element-plus";
// 定义菜单数据类型
interface Menu {
id: number;
pid: number;
title: string;
path: string;
component_path: string;
icon: string;
sort: number;
status: 0 | 1;
is_visible: 0 | 1;
type: 1 | 2 | 3;
permission: string;
children?: Menu[];
hasChildren?: boolean;
}
// Props
interface Props {
visible: boolean;
menu: Partial<Menu> | null;
parentMenuOptions: Menu[];
dialogType: 'add' | 'edit' | 'addSub';
parentTitle?: string;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
menu: null,
parentMenuOptions: () => [],
dialogType: 'add',
parentTitle: ''
});
// Emits
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
(e: 'save', menu: Partial<Menu>): void;
(e: 'cancel'): void;
}>();
// 表单引用
const menuFormRef = ref<InstanceType<typeof ElForm>>();
// 当前操作的菜单
const currentMenu = ref<Partial<Menu>>({
id: 0,
pid: 0,
title: '',
path: '',
component_path: '',
icon: '',
sort: 0,
status: 1,
is_visible: 1,
type: 1,
permission: '',
});
// 使用计算属性处理 pid确保始终是单个整数
const pidValue = computed({
get: () => {
const pid = currentMenu.value.pid;
// 如果已经是数组取最后一个元素el-cascader 返回的是路径数组)
if (Array.isArray(pid)) {
const normalized = pid.length > 0 ? (parseInt(String(pid[pid.length - 1]), 10) || 0) : 0;
// 立即修正 currentMenu.value.pid
currentMenu.value.pid = normalized;
return normalized;
}
// 确保是整数
const normalized = typeof pid === 'number' ? pid : (pid ? parseInt(String(pid), 10) || 0 : 0);
// 如果值被修改了,更新回去
if (normalized !== pid && pid !== null && pid !== undefined) {
currentMenu.value.pid = normalized;
}
return normalized;
},
set: (value: number | number[] | null | undefined) => {
// 处理数组情况el-cascader 可能返回路径数组,取最后一个元素
let pid: number = 0;
if (Array.isArray(value)) {
pid = value.length > 0 ? (parseInt(String(value[value.length - 1]), 10) || 0) : 0;
} else if (value !== null && value !== undefined) {
pid = parseInt(String(value), 10) || 0;
}
currentMenu.value.pid = pid;
}
});
// 处理父级菜单更新
const handlePidUpdate = (value: number | number[] | null | undefined) => {
pidValue.value = value;
};
// 监听props变化更新当前菜单
watch(() => props.menu, (newMenu) => {
if (newMenu) {
// 确保 pid 是整数,处理数组情况
let normalizedPid = 0;
if (newMenu.pid !== null && newMenu.pid !== undefined) {
if (Array.isArray(newMenu.pid)) {
normalizedPid = newMenu.pid.length > 0 ? (parseInt(String(newMenu.pid[newMenu.pid.length - 1]), 10) || 0) : 0;
} else {
normalizedPid = parseInt(String(newMenu.pid), 10) || 0;
}
}
currentMenu.value = {
...newMenu,
pid: normalizedPid,
};
} else {
// 重置表单
currentMenu.value = {
id: 0,
pid: props.dialogType === 'addSub' ? props.parentMenuOptions[0]?.id || 0 : 0,
title: '',
path: '',
component_path: '',
icon: '',
sort: 0,
status: 1,
is_visible: 1,
type: 1,
permission: '',
};
}
}, { immediate: true });
// 监听 currentMenu.value.pid 的变化,确保始终是整数(防止被直接修改为数组)
watch(
() => currentMenu.value.pid,
(newPid) => {
if (Array.isArray(newPid)) {
// 如果变成了数组,立即转换为整数(取最后一个元素,因为 el-cascader 返回的是路径数组)
const normalizedPid = newPid.length > 0 ? (parseInt(String(newPid[newPid.length - 1]), 10) || 0) : 0;
currentMenu.value.pid = normalizedPid;
} else if (typeof newPid !== 'number' && newPid !== null && newPid !== undefined) {
// 如果不是数字,转换为整数
const normalizedPid = parseInt(String(newPid), 10) || 0;
currentMenu.value.pid = normalizedPid;
}
},
{ deep: true }
);
// 监听props.visible变化
watch(() => props.visible, (newVisible) => {
if (newVisible && props.dialogType === 'add') {
// 新增时重置表单
currentMenu.value = {
id: 0,
pid: 0,
title: '',
path: '',
component_path: '',
icon: '',
sort: 0,
status: 1,
is_visible: 1,
type: 1,
permission: '',
};
}
});
// 对话框标题
const dialogTitle = computed(() => {
switch (props.dialogType) {
case 'add':
return '添加菜单';
case 'edit':
return '编辑菜单';
case 'addSub':
return `添加子菜单 (父菜单: ${props.parentTitle || '顶级菜单'})`;
default:
return '操作菜单';
}
});
// 表单验证规则
const formRules = ref({
title: [{ required: true, message: "请输入菜单名称", trigger: "blur" }],
path: [
{
required: true,
validator: (rule: any, value: any, callback: any) => {
if (currentMenu.value.type === 3) {
// 接口类型不需要路径
callback();
} else if (!value || value.trim() === "") {
callback(new Error("请输入路由地址"));
} else {
callback();
}
},
trigger: "blur"
}
],
component_path: [
{
required: true,
validator: (rule: any, value: any, callback: any) => {
if (currentMenu.value.type === 2) {
// 页面类型需要组件路径
if (!value || value.trim() === "") {
callback(new Error("请输入组件路径"));
} else {
callback();
}
} else {
// 目录和接口类型不需要组件路径
callback();
}
},
trigger: "blur"
}
],
sort: [{ required: true, message: "请输入排序号", trigger: "blur" }],
});
// 级联选择器配置
const cascaderProps = ref({
value: "id",
label: "title",
children: "children",
checkStrictly: true,
emitpath: false,
});
// 监听菜单类型变化,自动清空不相关的字段
watch(() => currentMenu.value.type, (newType, oldType) => {
if (newType === oldType) return; // 避免初始化时的触发
if (newType === 1) {
// 目录:清空组件路径,保留路径
currentMenu.value.component_path = "";
} else if (newType === 2) {
// 页面:保留路径和组件路径
// 不清空,保持现有值
} else if (newType === 3) {
// 接口:清空路径和组件路径
currentMenu.value.path = "";
currentMenu.value.component_path = "";
}
});
// 取消操作
const handleCancel = () => {
emit('update:visible', false);
emit('cancel');
};
// 处理对话框关闭
const handleDialogClose = (value: boolean) => {
if (!value) {
emit('update:visible', false);
emit('cancel');
}
};
// 保存菜单
const handleSave = async () => {
// 表单验证
if (!menuFormRef.value) return;
const valid = await menuFormRef.value.validate();
if (!valid) return;
// 先确保 currentMenu.value.pid 是整数(防止 el-cascader 直接修改为数组)
let rawPid = currentMenu.value.pid;
if (Array.isArray(rawPid)) {
// el-cascader 返回的是路径数组,取最后一个元素
rawPid = rawPid.length > 0 ? rawPid[rawPid.length - 1] : 0;
}
const normalizedPid = typeof rawPid === 'number' ? rawPid : (rawPid ? parseInt(String(rawPid), 10) || 0 : 0);
currentMenu.value.pid = normalizedPid;
// 解决后端时间字段问题:过滤掉不需要的字段
const payload = { ...currentMenu.value };
// 再次确保 pid 是整数类型(双重保险)
// 处理数组情况:如果 pid 是数组取最后一个元素el-cascader 返回的是路径数组)
let pidValue: any = payload.pid;
// 如果是数组,取最后一个元素
if (Array.isArray(pidValue)) {
pidValue = pidValue.length > 0 ? pidValue[pidValue.length - 1] : null;
}
// 强制转换为整数
if (pidValue === null || pidValue === undefined || pidValue === '') {
payload.pid = 0;
} else {
const parsedPid = parseInt(String(pidValue), 10);
// 如果转换失败NaN设置为 0
if (isNaN(parsedPid)) {
payload.pid = 0;
} else {
payload.pid = parsedPid;
}
}
// 最终验证:确保 payload.pid 是数字类型,不是数组
if (Array.isArray(payload.pid)) {
payload.pid = Array.isArray(payload.pid) && payload.pid.length > 0
? parseInt(String(payload.pid[payload.pid.length - 1]), 10) || 0
: 0;
}
// 确保是数字类型
if (typeof payload.pid !== 'number') {
payload.pid = parseInt(String(payload.pid), 10) || 0;
}
// 触发保存事件
emit('save', payload);
};
</script>
<style lang="less" scoped>
/* 对话框样式精简 */
:deep(.el-dialog__body) {
padding: 20px;
max-height: 60vh;
overflow-y: auto;
}
:deep(.el-form-item) {
margin-bottom: 16px;
}
</style>