|
|
<script setup lang="ts">
|
|
|
import PageTitle from '@/components/PageTitle.vue'
|
|
|
import { ref } from 'vue'
|
|
|
import { usePageLoad } from '@/composables/usePageLoad'
|
|
|
import { fetchMenuTree, createMenu, updateMenu, deleteMenu } from '@/api/admin/menus'
|
|
|
import type { MenuNode } from '@/api/types'
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
|
const loading = ref(false)
|
|
|
const tree = ref<MenuNode[]>([])
|
|
|
|
|
|
const dialog = ref(false)
|
|
|
const form = ref({
|
|
|
id: null as number | null,
|
|
|
parent_id: null as number | null,
|
|
|
path: '',
|
|
|
name: '',
|
|
|
title: '',
|
|
|
component: '',
|
|
|
icon: '',
|
|
|
sort: 0,
|
|
|
visible: 1,
|
|
|
keep_alive: 0,
|
|
|
permission_code: '',
|
|
|
status: 1,
|
|
|
})
|
|
|
|
|
|
const parentOptions = ref<{ label: string; value: number | null }[]>([])
|
|
|
|
|
|
function flatOptions(nodes: MenuNode[]): { label: string; value: number | null }[] {
|
|
|
const out: { label: string; value: number | null }[] = [{ label: '根节点', value: null }]
|
|
|
const walk = (list: MenuNode[], p: string) => {
|
|
|
for (const n of list) {
|
|
|
out.push({ label: `${p}${n.title} (#${n.id})`, value: n.id })
|
|
|
if (n.children?.length) walk(n.children, p + ' ')
|
|
|
}
|
|
|
}
|
|
|
walk(nodes, '')
|
|
|
return out
|
|
|
}
|
|
|
|
|
|
async function load() {
|
|
|
loading.value = true
|
|
|
try {
|
|
|
tree.value = await fetchMenuTree()
|
|
|
parentOptions.value = flatOptions(tree.value)
|
|
|
} finally {
|
|
|
loading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function openCreate(parentId: number | null = null) {
|
|
|
form.value = {
|
|
|
id: null,
|
|
|
parent_id: parentId,
|
|
|
path: '',
|
|
|
name: '',
|
|
|
title: '',
|
|
|
component: '',
|
|
|
icon: '',
|
|
|
sort: 0,
|
|
|
visible: 1,
|
|
|
keep_alive: 0,
|
|
|
permission_code: '',
|
|
|
status: 1,
|
|
|
}
|
|
|
dialog.value = true
|
|
|
}
|
|
|
|
|
|
function openEdit(row: MenuNode) {
|
|
|
form.value = {
|
|
|
id: row.id,
|
|
|
parent_id: row.parent_id ?? null,
|
|
|
path: row.path,
|
|
|
name: row.name,
|
|
|
title: row.title,
|
|
|
component: row.component || '',
|
|
|
icon: row.icon || '',
|
|
|
sort: row.sort,
|
|
|
visible: row.visible,
|
|
|
keep_alive: row.keep_alive,
|
|
|
permission_code: row.permission_code || '',
|
|
|
status: row.status ?? 1,
|
|
|
}
|
|
|
dialog.value = true
|
|
|
}
|
|
|
|
|
|
async function save() {
|
|
|
const payload = {
|
|
|
parent_id: form.value.parent_id,
|
|
|
path: form.value.path,
|
|
|
name: form.value.name,
|
|
|
title: form.value.title,
|
|
|
component: form.value.component || null,
|
|
|
icon: form.value.icon || null,
|
|
|
sort: form.value.sort,
|
|
|
visible: form.value.visible,
|
|
|
keep_alive: form.value.keep_alive,
|
|
|
permission_code: form.value.permission_code || null,
|
|
|
status: form.value.status,
|
|
|
}
|
|
|
|
|
|
if (form.value.id) {
|
|
|
await updateMenu(form.value.id, payload)
|
|
|
} else {
|
|
|
await createMenu(payload)
|
|
|
}
|
|
|
ElMessage.success('已保存')
|
|
|
dialog.value = false
|
|
|
await load()
|
|
|
}
|
|
|
|
|
|
async function remove(row: MenuNode) {
|
|
|
await ElMessageBox.confirm(`确定删除菜单「${row.title}」?`, '提示', { type: 'warning' })
|
|
|
await deleteMenu(row.id)
|
|
|
ElMessage.success('已删除')
|
|
|
await load()
|
|
|
}
|
|
|
|
|
|
usePageLoad(load)
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
<div class="list-page">
|
|
|
<div class="page-header">
|
|
|
<PageTitle />
|
|
|
<div class="page-header-actions">
|
|
|
<el-button @click="load">刷新</el-button>
|
|
|
<el-button type="primary" size="small" class="btn-create" @click="openCreate(null)">新增根菜单</el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<el-card v-loading="loading" shadow="never" class="admin-list-card">
|
|
|
<el-table
|
|
|
:data="tree"
|
|
|
row-key="id"
|
|
|
default-expand-all
|
|
|
:tree-props="{ children: 'children' }"
|
|
|
>
|
|
|
<el-table-column prop="title" label="标题" min-width="160" />
|
|
|
<el-table-column prop="path" label="Path" width="180" />
|
|
|
<el-table-column prop="name" label="路由 name" width="140" />
|
|
|
<el-table-column prop="component" label="组件" min-width="180" show-overflow-tooltip />
|
|
|
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
|
|
<el-table-column label="显示" width="80" align="center">
|
|
|
<template #default="{ row }">
|
|
|
{{ row.visible ? '是' : '否' }}
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="操作" width="240" fixed="right">
|
|
|
<template #default="{ row }">
|
|
|
<div class="table-row-actions">
|
|
|
<el-button class="btn-action-primary" @click="openCreate(row.id)">子菜单</el-button>
|
|
|
<el-button class="btn-action-secondary" @click="openEdit(row)">编辑</el-button>
|
|
|
<el-button class="btn-action-secondary" @click="remove(row)">删除</el-button>
|
|
|
</div>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
</el-card>
|
|
|
</div>
|
|
|
|
|
|
<el-dialog v-model="dialog" :title="form.id ? '编辑菜单' : '新增菜单'" width="560px">
|
|
|
<el-form label-width="110px">
|
|
|
<el-form-item label="父级">
|
|
|
<el-select v-model="form.parent_id" clearable filterable placeholder="根" style="width: 100%">
|
|
|
<el-option
|
|
|
v-for="o in parentOptions"
|
|
|
:key="String(o.value) + o.label"
|
|
|
:label="o.label"
|
|
|
:value="o.value"
|
|
|
/>
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="Path">
|
|
|
<el-input v-model="form.path" placeholder="如 /system/users" />
|
|
|
</el-form-item>
|
|
|
<el-form-item label="路由 name">
|
|
|
<el-input v-model="form.name" />
|
|
|
</el-form-item>
|
|
|
<el-form-item label="标题">
|
|
|
<el-input v-model="form.title" />
|
|
|
</el-form-item>
|
|
|
<el-form-item label="组件">
|
|
|
<el-input v-model="form.component" placeholder="如 system/users/index" />
|
|
|
</el-form-item>
|
|
|
<el-form-item label="图标">
|
|
|
<el-input v-model="form.icon" placeholder="Element Plus 图标名" />
|
|
|
</el-form-item>
|
|
|
<el-form-item label="权限码">
|
|
|
<el-input
|
|
|
v-model="form.permission_code"
|
|
|
placeholder="可选,预留与后期接口权限对齐;当前不校验"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="排序">
|
|
|
<el-input-number v-model="form.sort" />
|
|
|
</el-form-item>
|
|
|
<el-form-item label="侧栏显示">
|
|
|
<el-switch v-model="form.visible" :active-value="1" :inactive-value="0" />
|
|
|
</el-form-item>
|
|
|
<el-form-item label="KeepAlive">
|
|
|
<el-switch v-model="form.keep_alive" :active-value="1" :inactive-value="0" />
|
|
|
</el-form-item>
|
|
|
<el-form-item label="状态">
|
|
|
<el-switch v-model="form.status" :active-value="1" :inactive-value="0" />
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
<template #footer>
|
|
|
<el-button @click="dialog = false">取消</el-button>
|
|
|
<el-button type="primary" @click="save">保存</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
</template>
|