<template>
|
<div class="menu-management-container">
|
<el-card shadow="never">
|
<div class="header">
|
<!-- <el-button type="primary" @click="handleAdd" icon="Plus" plain>{{ t('menu.addNavigation') }}</el-button> -->
|
<el-button @click="refresh" icon="Refresh">{{ t("tabs.refresh") }}</el-button>
|
</div>
|
|
<el-table
|
:data="menuList"
|
row-key="id"
|
default-expand-all
|
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
v-loading="loading"
|
>
|
<el-table-column prop="name" :label="t('menu.menuName')" width="280" />
|
<el-table-column prop="path" :label="t('menu.menuPath')" width="300" />
|
<el-table-column prop="component" :label="t('menu.componentPath')" width="300" />
|
<el-table-column :label="t('menu.icon')" width="200">
|
<template #default="{ row }">
|
<i v-if="row.meta?.icon" :class="'ri-' + row.meta.icon" style="font-size: 1.2em; vertical-align: middle" />
|
<span v-else>-</span>
|
</template>
|
</el-table-column>
|
<el-table-column :label="t('menu.title')">
|
<template #default="{ row }">
|
{{ row.meta?.title || "-" }}
|
</template>
|
</el-table-column>
|
<el-table-column :label="t('menu.englishTitle')">
|
<template #default="{ row }">
|
{{ row.meta?.entitle || "-" }}
|
</template>
|
</el-table-column>
|
<el-table-column :label="t('menu.fullScreen')" width="120">
|
<template #default="{ row }">
|
<el-tag :type="row.meta?.isFull ? 'danger' : 'info'" size="small" effect="plain">
|
{{ row.meta?.isFull ? t("menu.yes") : t("menu.no") }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
|
<el-table-column :label="t('Config.operation')" width="300" fixed="right">
|
<template #default="{ row }">
|
<el-button @click="handleEdit(row)" type="primary" icon="Edit" plain>{{ t("Config.update") }}</el-button>
|
<el-button @click="handleDelete(row)" type="danger" icon="Delete" plain>{{ t("Config.delete") }}</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</el-card>
|
|
<!-- 新增/编辑对话框 -->
|
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" :close-on-click-modal="false">
|
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
<el-form-item :label="t('menu.menuName')" prop="name">
|
<el-input v-model="formData.name" :placeholder="t('menu.inputMenuName')" />
|
</el-form-item>
|
<el-form-item :label="t('menu.menuPath')" prop="path">
|
<el-input v-model="formData.path" :placeholder="t('menu.inputMenuPath')" />
|
</el-form-item>
|
<el-form-item :label="t('menu.componentPath')" prop="component">
|
<el-input v-model="formData.component" :placeholder="t('menu.inputComponentPath')" />
|
</el-form-item>
|
<el-form-item :label="t('menu.parentMenu')">
|
<el-cascader
|
v-model="formData.parentId"
|
:options="menuOptions"
|
:props="{ value: 'id', label: 'name', checkStrictly: true, emitPath: false }"
|
:placeholder="t('menu.selectParentMenu')"
|
clearable
|
/>
|
</el-form-item>
|
<el-form-item :label="t('menu.icon')">
|
<!-- 使用 el-select-v2 并启用虚拟滚动和搜索 -->
|
<el-select-v2
|
v-model="formData.meta.icon"
|
:options="filteredIconOptions"
|
filterable
|
clearable
|
:placeholder="t('menu.searchIcon')"
|
style="width: 100%"
|
@search="handleIconSearch"
|
>
|
<template #default="{ item }">
|
<div style="display: flex; align-items: center; padding: 5px 0">
|
<i :class="'ri-' + item.value" style="margin-right: 8px" />
|
<span>{{ item.value }}</span>
|
</div>
|
</template>
|
</el-select-v2>
|
</el-form-item>
|
<el-form-item :label="t('menu.title')">
|
<el-input v-model="formData.meta.title" :placeholder="t('menu.inputTitle')" />
|
</el-form-item>
|
<el-form-item :label="t('menu.englishTitle')">
|
<el-input v-model="formData.meta.entitle" :placeholder="t('menu.inputEnglishTitle')" />
|
</el-form-item>
|
<el-form-item :label="t('menu.fullScreen')">
|
<el-switch v-model="formData.meta.isFull" />
|
</el-form-item>
|
</el-form>
|
<template #footer>
|
<el-button @click="dialogVisible = false">{{ t("Config.cancel") }}</el-button>
|
<el-button type="primary" @click="submitForm">{{ t("Config.sure") }}</el-button>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script lang="ts" setup>
|
import { ref, onMounted, computed } from "vue";
|
import { useI18n } from "vue-i18n";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
import type { FormInstance, FormRules } from "element-plus";
|
|
import { getMenuList, editMenu, deleteMenu } from "@/api/modules/hxzk/system/menu";
|
import "remixicon/fonts/remixicon.css";
|
// 假设已将图标列表导出为 remix-icons.json
|
import { icons as riIcons } from "@iconify-json/ri";
|
// 获取所有图标名称并按字母排序
|
const allIconOptions = Object.keys(riIcons.icons).sort();
|
// 图标搜索逻辑
|
const searchQuery = ref("");
|
const filteredIconOptions = ref(
|
allIconOptions.slice(0, 3000).map(icon => ({
|
value: icon,
|
label: icon
|
}))
|
);
|
|
const { t } = useI18n();
|
|
const handleIconSearch = (query: string) => {
|
searchQuery.value = query.toLowerCase();
|
if (!query) {
|
// 无搜索词时显示前100个常用图标
|
filteredIconOptions.value = allIconOptions.slice(0, 3000).map(icon => ({
|
value: icon,
|
label: icon
|
}));
|
} else {
|
// 有搜索词时过滤匹配项
|
filteredIconOptions.value = allIconOptions
|
.filter(icon => icon.toLowerCase().includes(searchQuery.value))
|
.map(icon => ({
|
value: icon,
|
label: icon
|
}));
|
}
|
};
|
// 定义菜单数据类型
|
interface MenuItem {
|
id: number;
|
path: string;
|
name: string;
|
component: string | null;
|
redirect: string | null;
|
parent_id: number | null;
|
meta: {
|
icon?: string;
|
title?: string;
|
isFull?: boolean;
|
};
|
children?: MenuItem[];
|
}
|
|
// 表单引用
|
const formRef = ref<FormInstance>();
|
|
// 数据状态
|
const loading = ref(false);
|
const menuList = ref<MenuItem[]>([]);
|
const dialogVisible = ref(false);
|
const dialogType = ref<"add" | "edit">("add");
|
const formData = ref<Omit<MenuItem, "id" | "children"> & { id?: number }>({
|
path: "",
|
name: "",
|
component: "",
|
redirect: "",
|
parent_id: null,
|
meta: {}
|
});
|
|
// 表单验证规则
|
const rules = ref<FormRules>({
|
name: [{ required: true, message: t("menu.validation.menuNameRequired"), trigger: "blur" }],
|
path: [{ required: true, message: t("menu.validation.menuPathRequired"), trigger: "blur" }],
|
component: [{ required: true, message: t("menu.validation.componentPathRequired"), trigger: "blur" }]
|
});
|
|
// 计算属性
|
const dialogTitle = computed(() => (dialogType.value === "add" ? t("menu.addNavigation") : t("menu.editNavigation")));
|
const menuOptions = computed(() => {
|
const options: MenuItem[] = JSON.parse(JSON.stringify(menuList.value));
|
options.unshift({ id: 0, name: t("menu.topLevelMenu"), path: "", component: "", redirect: "", parent_id: null, meta: {} });
|
return options;
|
});
|
|
// 初始化数据
|
onMounted(() => {
|
fetchMenuList();
|
});
|
|
// 获取菜单列表
|
const fetchMenuList = async () => {
|
loading.value = true;
|
try {
|
// 这里替换为实际的API调用
|
const res1 = await getMenuList();
|
|
// 将扁平数据转换为树形结构
|
menuList.value = buildTree(res1);
|
} catch (error) {
|
console.error(t("menu.error.fetchMenuListFailed"), error);
|
ElMessage.error(t("menu.error.fetchMenuListFailed"));
|
} finally {
|
loading.value = false;
|
}
|
};
|
|
// 将扁平数据转换为树形结构
|
// 在 fetchMenuList 方法中,处理返回的数据时:
|
const buildTree = (items: MenuItem[], parentId: number | null = null): MenuItem[] => {
|
return items
|
.filter(item => item.parentId === parentId)
|
.map(item => ({
|
...item,
|
meta: typeof item.meta === "string" ? JSON.parse(item.meta) : item.meta || {},
|
children: buildTree(items, item.id)
|
}));
|
};
|
|
// 刷新
|
const refresh = () => {
|
fetchMenuList();
|
};
|
|
// 新增菜单
|
// const handleAdd = () => {
|
// dialogType.value = "add";
|
// formData.value = {
|
// path: "",
|
// name: "",
|
// component: "",
|
// redirect: "",
|
// parent_id: null,
|
// meta: {}
|
// };
|
// dialogVisible.value = true;
|
// };
|
|
// 编辑菜单
|
const handleEdit = (row: MenuItem) => {
|
dialogType.value = "edit";
|
if (row.parentId == null) {
|
row.parentId = 0;
|
}
|
formData.value = JSON.parse(JSON.stringify(row));
|
|
dialogVisible.value = true;
|
};
|
|
// 删除菜单
|
const handleDelete = (row: MenuItem) => {
|
ElMessageBox.confirm(t("menu.deleteConfirm", { name: row.name }), t("menu.prompt"), {
|
confirmButtonText: t("Config.sure"),
|
cancelButtonText: t("Config.cancel"),
|
type: "warning"
|
})
|
.then(async () => {
|
try {
|
// 这里替换为实际的API调用
|
await deleteMenu(row.id);
|
ElMessage.success(t("menu.success.deleteSuccess"));
|
refresh();
|
} catch (error) {
|
console.error(t("menu.error.deleteFailed"), error);
|
ElMessage.error(t("menu.error.deleteFailed"));
|
}
|
})
|
.catch(() => {
|
console.log("111");
|
});
|
};
|
|
// 提交表单
|
const submitForm = async () => {
|
if (!formRef.value) return;
|
await formRef.value.validate();
|
|
try {
|
if (dialogType.value === "add") {
|
// 新增菜单
|
// await addMenu(formData.value);
|
ElMessage.success(t("menu.success.addSuccess"));
|
} else {
|
// 编辑菜单
|
formData.value.meta = JSON.stringify(formData.value.meta);
|
await editMenu(formData.value);
|
|
ElMessage.success(t("menu.success.updateSuccess"));
|
}
|
dialogVisible.value = false;
|
refresh();
|
} catch (error) {
|
console.error(t("menu.error.operationFailed"), error);
|
ElMessage.error(t("menu.error.operationFailed"));
|
}
|
};
|
</script>
|
|
<style scoped>
|
.menu-management-container {
|
padding: 20px;
|
}
|
.header {
|
display: flex;
|
gap: 10px;
|
margin-bottom: 20px;
|
}
|
.el-table {
|
margin-top: 20px;
|
}
|
</style>
|