删除菜单管理功能
This commit is contained in:
parent
51a8654248
commit
f434b9fe4e
@ -180,7 +180,6 @@ const breadcrumbItems = computed(() => {
|
|||||||
NodeList: t('Node Management'),
|
NodeList: t('Node Management'),
|
||||||
NodeDetail: t('Node Detail'),
|
NodeDetail: t('Node Detail'),
|
||||||
FlowBuilder: t('Flow Builder'),
|
FlowBuilder: t('Flow Builder'),
|
||||||
MenuManager: t('Menu Manager'),
|
|
||||||
Settings: t('Settings'),
|
Settings: t('Settings'),
|
||||||
SearchResults: t('Search Results')
|
SearchResults: t('Search Results')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,11 +60,6 @@ const router = createRouter({
|
|||||||
name: 'PageTypeDetailLegacy',
|
name: 'PageTypeDetailLegacy',
|
||||||
component: () => import('./detailPage')
|
component: () => import('./detailPage')
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'settings/menu',
|
|
||||||
name: 'MenuManager',
|
|
||||||
component: () => import('../../views/settings/MenuManager.vue')
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
|
|||||||
@ -34,11 +34,6 @@ const user = computed(() => authStore.user)
|
|||||||
|
|
||||||
const userMenuOptions = computed(() => {
|
const userMenuOptions = computed(() => {
|
||||||
const options: any[] = [
|
const options: any[] = [
|
||||||
{
|
|
||||||
label: t('Menu Management'),
|
|
||||||
key: 'menu-manager',
|
|
||||||
icon: () => h(Icon, { icon: 'tabler:menu-2' })
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: t('Settings'),
|
label: t('Settings'),
|
||||||
key: 'settings',
|
key: 'settings',
|
||||||
@ -61,8 +56,6 @@ const handleUserMenuSelect = async (key: string) => {
|
|||||||
if (key === 'logout') {
|
if (key === 'logout') {
|
||||||
await authStore.logout()
|
await authStore.logout()
|
||||||
// logout 函数内部已经处理了跳转,不需要再次跳转
|
// logout 函数内部已经处理了跳转,不需要再次跳转
|
||||||
} else if (key === 'menu-manager') {
|
|
||||||
router.push({ name: 'MenuManager' })
|
|
||||||
} else if (key === 'settings') {
|
} else if (key === 'settings') {
|
||||||
router.push({ name: 'Settings' })
|
router.push({ name: 'Settings' })
|
||||||
}
|
}
|
||||||
|
|||||||
3062
frontend/src/views/HomePage.vue
Normal file
3062
frontend/src/views/HomePage.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,701 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="page-menu-manager">
|
|
||||||
<div class="page-header">
|
|
||||||
<div class="header-left">
|
|
||||||
<h2>{{ t('Menu Management') }}</h2>
|
|
||||||
<p class="page-description">{{ t('Manage navigation menu items') }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="header-right">
|
|
||||||
<button class="reset-btn" @click="handleResetDefault">
|
|
||||||
<i class="fa fa-undo"></i>
|
|
||||||
{{ t('Reset to Default') }}
|
|
||||||
</button>
|
|
||||||
<button class="create-btn" @click="openCreate">
|
|
||||||
<i class="fa fa-plus"></i>
|
|
||||||
{{ t('Add Menu') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="page-content">
|
|
||||||
<n-space vertical :size="16">
|
|
||||||
|
|
||||||
<n-data-table
|
|
||||||
:columns="columns"
|
|
||||||
:data="tableData"
|
|
||||||
:bordered="false"
|
|
||||||
:row-key="(row: AppMenuItem) => row.id"
|
|
||||||
:row-props="(row: AppMenuItem, index: number) => getRowProps(row, index)"
|
|
||||||
@update:checked-row-keys="handleCheckedRowKeysChange"
|
|
||||||
/>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<n-modal v-model:show="showModal" preset="dialog" :title="editing ? t('Edit Menu') : t('Add Menu')">
|
|
||||||
<n-form :model="form" :rules="rules" label-width="100" ref="formRef">
|
|
||||||
<n-form-item :label="t('Display Name')"><n-input v-model:value="form.label" /></n-form-item>
|
|
||||||
|
|
||||||
<n-form-item :label="t('Menu Type')">
|
|
||||||
<n-select
|
|
||||||
v-model:value="form.type"
|
|
||||||
:options="typeOptions"
|
|
||||||
:placeholder="t('Select Menu Type')"
|
|
||||||
@update:value="onTypeChange"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- 上级菜单选择(所有类型都可选,默认为空) -->
|
|
||||||
<n-form-item :label="t('Parent Menu')" path="parentId">
|
|
||||||
<n-select
|
|
||||||
v-model:value="(form as any).parentId"
|
|
||||||
:options="parentMenuOptions"
|
|
||||||
:placeholder="t('No parent (top-level)')"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<!-- 页面类型 -->
|
|
||||||
<n-form-item v-if="form.type === 'pagetype'" :label="t('PageType Name')">
|
|
||||||
<n-input
|
|
||||||
v-model:value="form.pagetype"
|
|
||||||
:placeholder="t('Enter PageType name, e.g.: Knowledge Base')"
|
|
||||||
@input="onPageTypeChange"
|
|
||||||
/>
|
|
||||||
<template #feedback>
|
|
||||||
<n-text depth="3" style="font-size: 12px;">
|
|
||||||
{{ t('Enter PageType name, system will auto-generate friendly URL') }}:{{ previewUrl }}
|
|
||||||
</n-text>
|
|
||||||
</template>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<!-- 路由名 -->
|
|
||||||
<n-form-item v-if="form.type === 'route'" :label="t('Route Name')">
|
|
||||||
<n-input
|
|
||||||
v-model:value="form.routeName"
|
|
||||||
:placeholder="t('Enter route name, e.g.: Dashboard')"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<!-- URL路径 -->
|
|
||||||
<n-form-item v-if="form.type === 'url'" :label="t('URL Path')">
|
|
||||||
<n-input
|
|
||||||
v-model:value="form.url"
|
|
||||||
:placeholder="t('Enter complete URL path')"
|
|
||||||
/>
|
|
||||||
<template #feedback>
|
|
||||||
<n-text depth="3" style="font-size: 12px;">
|
|
||||||
{{ t('Internal path: /app/knowledge-base') }} <br>
|
|
||||||
{{ t('External link: starts with http:// or https://') }}
|
|
||||||
</n-text>
|
|
||||||
</template>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item :label="t('Icon')">
|
|
||||||
<IconPicker v-model="form.icon" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item :label="t('Order')"><n-input-number v-model:value="form.order" :min="0" /></n-form-item>
|
|
||||||
<n-form-item :label="t('Hidden')"><n-switch v-model:value="form.hidden" /></n-form-item>
|
|
||||||
</n-form>
|
|
||||||
<template #action>
|
|
||||||
<n-space>
|
|
||||||
<n-button @click="showModal = false">{{ t('Cancel') }}</n-button>
|
|
||||||
<n-button type="primary" @click="save">{{ t('Save') }}</n-button>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, h, computed } from 'vue'
|
|
||||||
import { NButton, NSpace, NDataTable, NModal, NForm, NFormItem, NInput, NSelect, NInputNumber, NSwitch, NText, useDialog, type FormInst, type FormRules } from 'naive-ui'
|
|
||||||
import { useMenuStore, type AppMenuItem } from '../../shared/stores/menu'
|
|
||||||
import { useAuthStore } from '../../shared/stores/auth'
|
|
||||||
import { pageTypeToSlug } from '../../shared/utils/slug'
|
|
||||||
import IconPicker from '../../core/components/IconPicker.vue'
|
|
||||||
import { t } from '../../shared/i18n'
|
|
||||||
|
|
||||||
const menuStore = useMenuStore()
|
|
||||||
const authStore = useAuthStore()
|
|
||||||
// 将扁平 items 构造成树形(不修改原数据)
|
|
||||||
function buildTree(items: AppMenuItem[]) {
|
|
||||||
const byId: Record<string, AppMenuItem & { children?: AppMenuItem[] }> = {}
|
|
||||||
items.forEach((i) => (byId[i.id] = { ...i, children: [] }))
|
|
||||||
const roots: (AppMenuItem & { children?: AppMenuItem[] })[] = []
|
|
||||||
items.forEach((i) => {
|
|
||||||
const pid = (i as any).parentId as string | null | undefined
|
|
||||||
if (pid && byId[pid]) {
|
|
||||||
byId[pid].children!.push(byId[i.id])
|
|
||||||
} else {
|
|
||||||
roots.push(byId[i.id])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 清理空 children,Naive UI 会根据 children 是否存在渲染树
|
|
||||||
const prune = (nodes: (AppMenuItem & { children?: AppMenuItem[] })[]) => {
|
|
||||||
nodes.forEach((n) => {
|
|
||||||
if (n.children && n.children.length) prune(n.children)
|
|
||||||
else delete n.children
|
|
||||||
})
|
|
||||||
}
|
|
||||||
prune(roots)
|
|
||||||
return roots
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据用户类型过滤菜单项
|
|
||||||
const filteredItems = computed(() => {
|
|
||||||
const userType = authStore.user?.user_type
|
|
||||||
const isSystemUser = userType === 'System User'
|
|
||||||
|
|
||||||
return menuStore.items.filter(m => {
|
|
||||||
// 过滤隐藏的菜单项
|
|
||||||
if (m.hidden) return false
|
|
||||||
|
|
||||||
// 非 System User 的过滤逻辑
|
|
||||||
if (!isSystemUser) {
|
|
||||||
// 过滤掉 pagetype 类型
|
|
||||||
if (m.type === 'pagetype') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只允许显示的根菜单:工具、开发
|
|
||||||
const allowedRootMenus = ['tools', 'dev-group']
|
|
||||||
if (!m.parentId && !allowedRootMenus.includes(m.id)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开发分组下只允许显示:工具市场(非 System User 不显示应用市场、节点市场、智能体市场)
|
|
||||||
if (m.parentId === 'dev-group') {
|
|
||||||
const allowedDevMenus = ['tool-marketplace']
|
|
||||||
if (!allowedDevMenus.includes(m.id)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const displayItems = computed(() => (draggedItem.value ? previewItems.value : filteredItems.value))
|
|
||||||
const tableData = computed(() => buildTree(displayItems.value))
|
|
||||||
const dialog = useDialog()
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ title: t('Display Name'), key: 'label' },
|
|
||||||
{ title: t('Type'), key: 'type', render: (row: AppMenuItem) => {
|
|
||||||
const typeMap = { 'pagetype': t('PageType'), 'route': t('Route'), 'url': t('URL'), 'group': t('Group') }
|
|
||||||
return typeMap[row.type] || row.type
|
|
||||||
}},
|
|
||||||
|
|
||||||
{ title: t('Parent Menu'), key: 'parentId', render: (row: AppMenuItem) => {
|
|
||||||
if (!(row as any).parentId) return t('None')
|
|
||||||
const p = menuStore.items.find(m => m.id === (row as any).parentId)
|
|
||||||
return p ? p.label : t('None')
|
|
||||||
}},
|
|
||||||
{ title: t('PageType'), key: 'pagetype' },
|
|
||||||
{ title: t('Route Name'), key: 'routeName' },
|
|
||||||
{ title: t('URL Path'), key: 'url' },
|
|
||||||
{ title: t('Icon'), key: 'icon' },
|
|
||||||
{ title: t('Order'), key: 'order' },
|
|
||||||
{ title: t('Hidden'), key: 'hidden', render: (row: AppMenuItem) => row.hidden ? t('Yes') : t('No') },
|
|
||||||
{ title: t('Actions'), key: 'actions', render: (row: AppMenuItem) => h(NSpace, {}, {
|
|
||||||
default: () => [
|
|
||||||
h(NButton, { size: 'small', onClick: () => openEdit(row) }, { default: () => t('Edit') }),
|
|
||||||
h(NButton, { size: 'small', type: 'error', onClick: () => onRemove(row) }, { default: () => t('Delete') })
|
|
||||||
]
|
|
||||||
}) }
|
|
||||||
]
|
|
||||||
|
|
||||||
const showModal = ref(false)
|
|
||||||
const editing = ref<AppMenuItem | null>(null)
|
|
||||||
const form = ref<AppMenuItem>({
|
|
||||||
id: '',
|
|
||||||
key: '',
|
|
||||||
label: '',
|
|
||||||
icon: 'MenuOutlined',
|
|
||||||
type: 'pagetype', // 默认选择页面类型
|
|
||||||
pagetype: '',
|
|
||||||
routeName: '',
|
|
||||||
url: '',
|
|
||||||
order: 0,
|
|
||||||
hidden: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const formRef = ref<FormInst | null>(null)
|
|
||||||
|
|
||||||
const rules: FormRules = {
|
|
||||||
label: [{ required: true, message: t('Please enter display name') }],
|
|
||||||
type: [{ required: true, message: t('Please select menu type') }],
|
|
||||||
}
|
|
||||||
|
|
||||||
const typeOptions = computed(() => {
|
|
||||||
const userType = authStore.user?.user_type
|
|
||||||
const isSystemUser = userType === 'System User'
|
|
||||||
|
|
||||||
const allOptions = [
|
|
||||||
{ label: t('PageType'), value: 'pagetype' },
|
|
||||||
{ label: t('Route'), value: 'route' },
|
|
||||||
{ label: t('URL'), value: 'url' },
|
|
||||||
{ label: t('Group'), value: 'group' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 非 System User 过滤掉 pagetype 选项
|
|
||||||
if (!isSystemUser) {
|
|
||||||
return allOptions.filter(opt => opt.value !== 'pagetype')
|
|
||||||
}
|
|
||||||
|
|
||||||
return allOptions
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const parentMenuOptions = computed(() => {
|
|
||||||
// 使用过滤后的菜单项作为父菜单选项
|
|
||||||
const opts = filteredItems.value.map(m => ({ label: m.label, value: m.id }))
|
|
||||||
return [{ label: t('None'), value: null as any }].concat(opts)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 预览URL
|
|
||||||
const previewUrl = computed(() => {
|
|
||||||
if (form.value.type === 'pagetype' && form.value.pagetype) {
|
|
||||||
const slug = pageTypeToSlug(form.value.pagetype)
|
|
||||||
return `/app/${slug}`
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
})
|
|
||||||
|
|
||||||
function openCreate() {
|
|
||||||
editing.value = null
|
|
||||||
const userType = authStore.user?.user_type
|
|
||||||
const isSystemUser = userType === 'System User'
|
|
||||||
|
|
||||||
form.value = {
|
|
||||||
id: '',
|
|
||||||
key: '',
|
|
||||||
label: '',
|
|
||||||
icon: '',
|
|
||||||
type: isSystemUser ? 'pagetype' : 'route', // 根据用户类型设置默认类型
|
|
||||||
pagetype: '',
|
|
||||||
routeName: '',
|
|
||||||
url: '',
|
|
||||||
order: 0,
|
|
||||||
hidden: false,
|
|
||||||
parentId: null
|
|
||||||
}
|
|
||||||
showModal.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function openEdit(row: AppMenuItem) {
|
|
||||||
editing.value = row
|
|
||||||
form.value = { ...row }
|
|
||||||
showModal.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 类型变化处理
|
|
||||||
function onTypeChange() {
|
|
||||||
// 清空其他字段
|
|
||||||
form.value.pagetype = ''
|
|
||||||
form.value.routeName = ''
|
|
||||||
form.value.url = ''
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// PageType变化处理
|
|
||||||
function onPageTypeChange() {
|
|
||||||
if (form.value.type === 'pagetype' && form.value.pagetype) {
|
|
||||||
const slug = pageTypeToSlug(form.value.pagetype)
|
|
||||||
form.value.url = `/app/${slug}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function save() {
|
|
||||||
const data = { ...form.value }
|
|
||||||
|
|
||||||
// 验证:非 System User 不允许保存 pagetype 类型
|
|
||||||
const userType = authStore.user?.user_type
|
|
||||||
const isSystemUser = userType === 'System User'
|
|
||||||
if (!isSystemUser && data.type === 'pagetype') {
|
|
||||||
dialog.error({
|
|
||||||
title: t('Permission Denied'),
|
|
||||||
content: t('Non-System User cannot create or edit pagetype menu items'),
|
|
||||||
positiveText: t('OK')
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据类型设置key
|
|
||||||
if (data.type === 'pagetype' && data.pagetype) {
|
|
||||||
data.key = pageTypeToSlug(data.pagetype)
|
|
||||||
data.url = `/app/${data.key}`
|
|
||||||
} else if (data.type === 'route' && data.routeName) {
|
|
||||||
data.key = data.routeName
|
|
||||||
} else if (data.type === 'url' && data.url) {
|
|
||||||
data.key = data.url
|
|
||||||
} else if (data.type === 'group') {
|
|
||||||
data.key = data.key || `group_${Date.now()}`
|
|
||||||
}
|
|
||||||
|
|
||||||
formRef.value?.validate((errors) => {
|
|
||||||
if (errors) return
|
|
||||||
if (editing.value) {
|
|
||||||
menuStore.updateMenu(editing.value.id, data)
|
|
||||||
} else {
|
|
||||||
data.id = ''
|
|
||||||
menuStore.addMenu(data)
|
|
||||||
}
|
|
||||||
showModal.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRemove(row: AppMenuItem) {
|
|
||||||
dialog.warning({
|
|
||||||
title: t('Confirm Delete'),
|
|
||||||
content: `${t('Are you sure you want to delete menu')}"${row.label}"?`,
|
|
||||||
positiveText: t('Delete'),
|
|
||||||
negativeText: t('Cancel'),
|
|
||||||
onPositiveClick: () => menuStore.removeMenu(row.id)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleResetDefault() {
|
|
||||||
dialog.warning({
|
|
||||||
title: t('Reset to Default'),
|
|
||||||
content: t('Are you sure to reset menus to default? This will overwrite current settings.'),
|
|
||||||
positiveText: t('Confirm'),
|
|
||||||
negativeText: t('Cancel'),
|
|
||||||
onPositiveClick: () => menuStore.resetDefault()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 拖拽排序 - 实时预览
|
|
||||||
const draggedItem = ref<AppMenuItem | null>(null)
|
|
||||||
const dropIndex = ref<number>(-1)
|
|
||||||
const previewItems = ref<AppMenuItem[]>([])
|
|
||||||
|
|
||||||
function getRowProps(row: AppMenuItem, _index: number) {
|
|
||||||
// 计算在扁平数组中的真实索引
|
|
||||||
const flatIndex = menuStore.items.findIndex(item => item.id === row.id)
|
|
||||||
|
|
||||||
return {
|
|
||||||
draggable: true,
|
|
||||||
class: {
|
|
||||||
'draggable-row': true,
|
|
||||||
'drag-over': dropIndex.value === flatIndex,
|
|
||||||
'dragging': draggedItem.value?.id === row.id
|
|
||||||
},
|
|
||||||
onDragstart: (e: DragEvent) => {
|
|
||||||
draggedItem.value = row
|
|
||||||
previewItems.value = [...menuStore.items]
|
|
||||||
e.dataTransfer!.effectAllowed = 'move'
|
|
||||||
},
|
|
||||||
onDragover: (e: DragEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
if (draggedItem.value && dropIndex.value !== flatIndex) {
|
|
||||||
dropIndex.value = flatIndex
|
|
||||||
updatePreview()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDragleave: () => {
|
|
||||||
// 延迟清除,避免快速移动时闪烁
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!draggedItem.value) {
|
|
||||||
dropIndex.value = -1
|
|
||||||
previewItems.value = [...menuStore.items]
|
|
||||||
}
|
|
||||||
}, 50)
|
|
||||||
},
|
|
||||||
onDrop: (e: DragEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
if (draggedItem.value && dropIndex.value !== -1) {
|
|
||||||
applyReorder()
|
|
||||||
}
|
|
||||||
resetDragState()
|
|
||||||
},
|
|
||||||
onDragend: resetDragState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePreview() {
|
|
||||||
if (!draggedItem.value || dropIndex.value === -1) return
|
|
||||||
|
|
||||||
const items = [...menuStore.items]
|
|
||||||
const currentIndex = items.findIndex(i => i.id === draggedItem.value!.id)
|
|
||||||
|
|
||||||
if (currentIndex === -1 || currentIndex === dropIndex.value) return
|
|
||||||
|
|
||||||
// 移除并重新插入
|
|
||||||
items.splice(currentIndex, 1)
|
|
||||||
// 如果目标位置在当前位置之后,需要调整插入位置
|
|
||||||
const insertIndex = dropIndex.value > currentIndex ? dropIndex.value - 1 : dropIndex.value
|
|
||||||
items.splice(insertIndex, 0, draggedItem.value)
|
|
||||||
|
|
||||||
previewItems.value = items
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyReorder() {
|
|
||||||
if (!draggedItem.value || dropIndex.value === -1) return
|
|
||||||
|
|
||||||
const items = [...previewItems.value]
|
|
||||||
|
|
||||||
// 分层重新计算order值
|
|
||||||
const rootMenus = items.filter(item => !item.parentId)
|
|
||||||
const childMenus = items.filter(item => item.parentId)
|
|
||||||
|
|
||||||
// 重新分配根菜单的order
|
|
||||||
rootMenus.forEach((item, index) => {
|
|
||||||
item.order = index + 1
|
|
||||||
})
|
|
||||||
|
|
||||||
// 重新分配子菜单的order(按父菜单分组)
|
|
||||||
const groupedChildren = childMenus.reduce((acc, child) => {
|
|
||||||
const parentId = child.parentId!
|
|
||||||
if (!acc[parentId]) acc[parentId] = []
|
|
||||||
acc[parentId].push(child)
|
|
||||||
return acc
|
|
||||||
}, {} as Record<string, AppMenuItem[]>)
|
|
||||||
|
|
||||||
Object.values(groupedChildren).forEach(children => {
|
|
||||||
children.forEach((child, index) => {
|
|
||||||
child.order = index + 1
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
menuStore.items = items
|
|
||||||
menuStore.persist()
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetDragState() {
|
|
||||||
draggedItem.value = null
|
|
||||||
dropIndex.value = -1
|
|
||||||
previewItems.value = [...menuStore.items]
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCheckedRowKeysChange(keys: (string | number)[]) {
|
|
||||||
// 处理选中行变化(如果需要的话)
|
|
||||||
console.log('Selected rows:', keys)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.page-menu-manager {
|
|
||||||
width: 100%;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 页面头部样式 */
|
|
||||||
.page-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left h2 {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1f2937;
|
|
||||||
margin: 0 0 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-description {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #6b7280;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 恢复默认按钮 */
|
|
||||||
.reset-btn {
|
|
||||||
height: 36px;
|
|
||||||
padding: 0 16px;
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #ffffff;
|
|
||||||
color: #6b7280;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reset-btn:hover {
|
|
||||||
background: #f9fafb;
|
|
||||||
color: #374151;
|
|
||||||
border-color: #9ca3af;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reset-btn:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reset-btn i {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 新增菜单按钮 - 使用柔和的品牌色系,与pagetype列表页创建按钮一致 */
|
|
||||||
.create-btn {
|
|
||||||
height: 36px;
|
|
||||||
padding: 0 16px;
|
|
||||||
border: 1px solid #1fc76f;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #e6f8f0;
|
|
||||||
color: #0d684b;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-btn:hover {
|
|
||||||
background: #dcfce7;
|
|
||||||
border-color: #1fc76f;
|
|
||||||
color: #166534;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 2px 8px rgba(31, 199, 111, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-btn:active {
|
|
||||||
background: #1fc76f;
|
|
||||||
border-color: #1fc76f;
|
|
||||||
color: white;
|
|
||||||
transform: translateY(0);
|
|
||||||
box-shadow: 0 1px 4px rgba(31, 199, 111, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-btn:disabled {
|
|
||||||
background: #f1f5f9;
|
|
||||||
border-color: #e2e8f0;
|
|
||||||
color: #94a3b8;
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
transform: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-btn:disabled:hover {
|
|
||||||
background: #f1f5f9;
|
|
||||||
border-color: #e2e8f0;
|
|
||||||
color: #94a3b8;
|
|
||||||
transform: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-btn i {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 页面内容区域 */
|
|
||||||
.page-content {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式设计 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.page-menu-manager {
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-right {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left h2 {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.page-menu-manager {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-right {
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reset-btn,
|
|
||||||
.create-btn {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left h2 {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 拖拽行 - 实时预览设计 */
|
|
||||||
:deep(.draggable-row) {
|
|
||||||
cursor: grab;
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.draggable-row:hover) {
|
|
||||||
background-color: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.draggable-row:active) {
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 拖拽中 - 实时预览效果 */
|
|
||||||
:deep(.draggable-row.dragging) {
|
|
||||||
opacity: 0.7;
|
|
||||||
background-color: #f0f9ff;
|
|
||||||
border: 1px dashed #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 拖拽目标 - 清晰指示 */
|
|
||||||
:deep(.draggable-row.drag-over) {
|
|
||||||
border-top: 2px solid #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 表格样式 */
|
|
||||||
:deep(.n-data-table) {
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 禁用文本选择 */
|
|
||||||
:deep(.draggable-row *) {
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 拖拽时的平滑过渡 */
|
|
||||||
:deep(.n-data-table-tbody tr) {
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user