jingrowtools/src/app/layouts/AppSidebar.vue

227 lines
5.9 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>
<div class="app-sidebar">
<!-- Logo区域 -->
<div class="sidebar-header">
<div class="logo">
<router-link to="/" class="logo-link">
<img src="/logo.svg" :alt="appName" width="32" height="32" />
<span v-if="!collapsed" class="logo-text">{{ appName }}</span>
</router-link>
</div>
</div>
<!-- 导航菜单 -->
<div class="menu-container">
<n-menu
:collapsed="collapsed"
:collapsed-width="64"
:collapsed-icon-size="24"
:options="menuOptions"
:value="currentRoute"
@update:value="handleMenuSelect"
/>
</div>
<!-- 用户菜单区域 -->
<div class="sidebar-footer">
<UserMenu :collapsed="collapsed" />
</div>
</div>
</template>
<script setup lang="ts">
import { computed, h } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { NMenu } from 'naive-ui'
import { t } from '../../shared/i18n'
import { useMenuStore, type AppMenuItem } from '../../shared/stores/menu'
import { pageTypeToSlug } from '../../shared/utils/slug'
import DynamicIcon from '../../core/components/DynamicIcon.vue'
import UserMenu from '../../shared/components/UserMenu.vue'
interface Props {
collapsed: boolean
}
defineProps<Props>()
// 定义emit事件
const emit = defineEmits<{
'menu-select': []
}>()
const router = useRouter()
const route = useRoute()
const currentRoute = computed(() => route.name as string)
const menuStore = useMenuStore()
// 从 localStorage 读取应用名称
const appName = computed(() => localStorage.getItem('appName') || 'Jingrow')
// 根据 parentId 构建树,并在 group 类型上承载二级菜单
type MenuOption = {
label: string
key: string
icon?: any
children?: MenuOption[]
}
const buildTree = (items: AppMenuItem[]) => {
const byId: Record<string, AppMenuItem & { children: (AppMenuItem & { children?: any[] })[] }> = {}
items.forEach((i: AppMenuItem) => (byId[i.id] = { ...(i as any), children: [] }))
const roots: (AppMenuItem & { children: any[] })[] = []
items.forEach((i: AppMenuItem) => {
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])
})
return roots
}
// 默认不自动展开任何二级菜单
const renderIcon = (name?: string, size = 24) =>
name
? () => h(DynamicIcon, { name, size, color: '#64748b', iconLibrary: 'tabler' })
: undefined
const toMenuOptions = (nodes: (AppMenuItem & { children?: any[] })[]): MenuOption[] =>
nodes.map((n: any) => ({
label: t(n.label),
key: n.id,
icon: renderIcon(n.icon, 24),
children: n.children && n.children.length
? n.children.map((c: any) => ({
label: t(c.label),
key: c.id,
icon: renderIcon(c.icon, 20),
children: c.children?.length ? toMenuOptions(c.children as any) : undefined
}))
: undefined
}))
const menuOptions = computed(() => {
const tree = buildTree(menuStore.visibleItems)
return toMenuOptions(tree)
})
const handleMenuSelect = (key: string) => {
// 支持点击任意层级项:在全量 items 中查找
const menuItem = menuStore.items.find(item => item.id === key)
if (!menuItem) return
// 发出菜单选择事件,让父组件处理移动端关闭逻辑
emit('menu-select')
// 根据菜单类型处理导航
if (menuItem.type === 'pagetype' && menuItem.pagetype) {
// 页面类型:统一跳转到列表页,由列表页内部判断是否为单页模式
const slug = pageTypeToSlug(menuItem.pagetype)
router.push({ name: 'PageTypeList', params: { entity: slug } })
} else if (menuItem.type === 'route' && menuItem.routeName) {
// 路由名使用routeName字段
router.push({ name: menuItem.routeName })
} else if (menuItem.type === 'url' && menuItem.url) {
// URL直接使用url字段
if (menuItem.url.startsWith('http://') || menuItem.url.startsWith('https://')) {
// 外部链接,在新窗口打开
window.open(menuItem.url, '_blank')
} else {
// 内部链接使用router跳转
router.push(menuItem.url)
}
} else {
// 新增Workspace 类型
if (menuItem.type === 'workspace' && menuItem.workspaceName) {
const slug = pageTypeToSlug(menuItem.workspaceName)
router.push(`/workspace/${slug}`)
return
}
// 兼容旧版本优先使用url字段如果没有则使用routeName
if (menuItem.url) {
if (menuItem.url.startsWith('http://') || menuItem.url.startsWith('https://')) {
window.open(menuItem.url, '_blank')
} else {
router.push(menuItem.url)
}
} else if (menuItem.routeName) {
router.push({ name: menuItem.routeName })
}
}
}
</script>
<style scoped>
.app-sidebar {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
padding: 16px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.menu-container {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
}
.logo {
display: flex;
align-items: center;
gap: 12px;
font-size: 16px;
font-weight: 700;
color: #1f2937;
}
.logo-link {
display: inline-flex;
align-items: center;
gap: 12px;
color: inherit;
text-decoration: none;
}
.logo-text {
white-space: nowrap;
}
/* 菜单图标样式优化 */
:deep(.n-menu-item-content) {
transition: all 0.2s ease;
}
:deep(.n-menu-item-content:hover) {
background-color: rgba(24, 160, 88, 0.1);
}
:deep(.n-menu-item-content.n-menu-item-content--selected) {
background-color: rgba(24, 160, 88, 0.15);
}
:deep(.n-menu-item-content.n-menu-item-content--selected .n-icon) {
color: #18a058 !important;
}
:deep(.n-menu-item-content:hover .n-icon) {
color: #18a058 !important;
transform: scale(1.1);
}
/* 侧边栏底部用户菜单区域 */
.sidebar-footer {
flex-shrink: 0;
padding: 12px;
border-top: 1px solid var(--border-color);
}
</style>