// Author: fruit.com.cn import { computed, onMounted, reactive, ref } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { createMenuApi, deleteMenuApi, getMenuTreeApi, updateMenuApi } from '@/api/admin/role' import { MenuIconOptions, normalizeAppIcon } from '@/constants/icons' export const menuIconNames = MenuIconOptions /** 菜单管理:直接读写后端 sys_menu,变更后重新登录生效。 */ export function useMenuManagement() { const menuTree = ref([]) const loading = ref(false) const form = reactive({ id: null, parentId: 0, title: '', routePath: '', routeName: '', component: '', icon: '', permissionCode: '', type: 'MENU', sort: 1, visible: 1, status: 1 }) function mapNodes(nodes) { return (nodes || []).map((node) => ({ id: node.id, label: node.meta?.title || node.path || String(node.id), raw: node, children: node.children?.length ? mapNodes(node.children) : undefined })) } const treeData = computed(() => mapNodes(menuTree.value)) const parentOptions = computed(() => { // 顶级菜单哨兵跟接口的 parentId 一样用字符串 "0":菜单 id 是字符串, // 数字 0 会在下拉里对不上、回显成裸 "0" const options = [{ id: '0', label: '顶级菜单' }] const walk = (nodes, depth) => { ;(nodes || []).forEach((node) => { if (node.type !== 'BUTTON') { options.push({ id: node.id, label: `${' '.repeat(depth)}${node.meta?.title || node.path || node.id}` }) } if (node.children?.length) walk(node.children, depth + 1) }) } walk(menuTree.value, 0) return options }) async function load() { loading.value = true try { const res = await getMenuTreeApi() menuTree.value = res.data || [] } finally { loading.value = false } } function resetForm(parentId = '0') { Object.assign(form, { id: null, parentId, title: '', routePath: '', routeName: '', component: '', icon: '', permissionCode: '', type: 'MENU', sort: 1, visible: 1, status: 1 }) } function selectNode(node) { const raw = node.raw || {} Object.assign(form, { id: raw.id, parentId: raw.parentId ?? '0', title: raw.meta?.title || '', routePath: raw.path || '', routeName: raw.name || '', component: raw.component === '#' ? '' : raw.component || '', icon: raw.type === 'CATALOG' ? normalizeAppIcon(raw.icon) : '', permissionCode: raw.permissionCode || '', type: raw.type || 'MENU', sort: raw.sort ?? 1, visible: raw.visible ?? 1, status: raw.status ?? 1 }) } async function saveMenu() { if (!form.title.trim()) return ElMessage.warning('请填写菜单名称') if (form.type !== 'BUTTON' && !form.routePath.trim()) { return ElMessage.warning('请填写对应路由') } const payload = { parentId: form.parentId || '0', title: form.title.trim(), routePath: form.routePath.trim(), routeName: form.routeName.trim(), component: form.type === 'CATALOG' ? '#' : form.component.trim(), icon: form.type === 'CATALOG' ? normalizeAppIcon(form.icon) : '', permissionCode: form.permissionCode.trim(), type: form.type, sort: Number(form.sort) || 1, visible: form.visible, status: form.status } const isCreate = !form.id if (isCreate) { await createMenuApi(payload) } else { await updateMenuApi(form.id, payload) } await load() resetForm() ElMessage.success(isCreate ? '菜单已添加,重新登录后生效' : '菜单已保存,重新登录后生效') } async function deleteMenu() { if (!form.id) return ElMessage.warning('请先在左侧选择一个菜单') try { await ElMessageBox.confirm(`确认删除「${form.title}」菜单吗?`, '删除确认', { confirmButtonText: '确认删除', cancelButtonText: '取消', type: 'warning', confirmButtonClass: 'el-button--danger' }) } catch { return } await deleteMenuApi(form.id) ElMessage.success('菜单已删除,重新登录后生效') resetForm() await load() } onMounted(load) return { menuTree, treeData, loading, form, parentOptions, resetForm, selectNode, saveMenu, deleteMenu } }