主题选单
概述
WNCMS 提供强大的选单系统,让您可以为主题建立导航选单。选单可以分配到不同位置(页首、页尾、侧边栏),并支援多层级阶层结构及自订样式和行为。
选单结构
Menu 模型
每个选单包含:
- Menu:容器(例如「主导航」、「页尾连结」)
- Menu Items:选单内的个别连结
- Hierarchy:子选单的父子关系
资料库结构
Menus 表:
php
- id
- name (可翻译) // 选单名称
- slug // 唯一识别码
- status // active/inactive
- created_at
- updated_at1
2
3
4
5
6
2
3
4
5
6
Menu Items 表:
php
- id
- menu_id // 父选单
- parent_id // 父项目(用于子选单)
- name (可翻译) // 显示文字
- display_name (可翻译) // 替代显示名称
- description (可翻译)
- url // 连结网址
- url_type // route/external/page/post/custom
- route_name // Laravel 路由名称
- page_id // 连结页面 ID
- post_id // 连结文章 ID
- is_new_window // 在新视窗开启
- css_class // 自订 CSS 类别
- icon // 图示类别(例如 FontAwesome)
- sort // 显示顺序
- status // active/inactive
- created_at
- updated_at1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在后台建立选单
1. 建立选单
在 WNCMS 后台:
- 导航至外观 → 选单
- 点击建立新选单
- 输入选单详细资讯:
- 名称:「主导航」
- 别名:「main-nav」
- 点击储存
2. 新增选单项目
对于每个选单项目:
- 选择要编辑的选单
- 点击新增选单项目
- 配置项目:
- 名称:显示文字(例如「首页」、「关于」)
- URL 类型:选择连结类型
route:Laravel 路由名称page:连结到页面post:连结到文章external:外部网址custom:自订网址
- URL/路由:根据类型而定
- 父项目:选择父项目以建立子选单
- 顺序:显示位置
- 新视窗:在新分页开启
- 图示:图示类别(选填)
- Font Awesome 免费图示查询:
https://fontawesome.com/v6/search?ic=free-collection
- Font Awesome 免费图示查询:
- CSS 类别:自订样式(选填)
3. 组织阶层
- 拖放项目以重新排序
- 将项目巢状至父项目下以建立子选单
- 建议最多 3 层深度
- 父级项目会在项目列内显示折叠切换图示(
fa-caret-right/fa-caret-down)用于展开与收合。 - 子层级排序在重新渲染后可保持稳定结构,仅更新 Nestable 根层列表。
- 当标签存在性 API 回传空值或非预期
ids时,检查会安全降级,不会阻断菜单编辑器 UI 控件。 - 菜单项目编辑输入(弹窗与列表行内控件)现使用明确且唯一的
id/name,并确保label for对应正确,同时补齐autocomplete提示,以降低浏览器自动填充与可访问性警告。
在主题中使用选单
主题配置
在 config.php 中定义选单位置:
php
return [
'option_tabs' => [
'layout' => [
[
'label' => '页首选单',
'name' => 'header_menu',
'type' => 'select',
'options' => 'menus',
'description' => '选择页首导航选单',
],
[
'label' => '页尾选单',
'name' => 'footer_menu',
'type' => 'select',
'options' => 'menus',
'description' => '选择页尾连结选单',
],
[
'label' => '行动选单',
'name' => 'mobile_menu',
'type' => 'select',
'options' => 'menus',
'description' => '选择行动导航选单',
],
],
],
'default' => [
'header_menu' => 1, // 预设选单 ID
'footer_menu' => 2,
],
];1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
基本选单显示
简单选单(无子选单):
blade
@if(gto('header_menu'))
<nav class="main-menu">
<ul>
@foreach(wncms()->menu()->getMenuParentItems(gto('header_menu')) as $menuItem)
@php
$menuItemUrl = wncms()->menu()->getMenuItemUrl($menuItem);
@endphp
<li class="{{ $menuItem->css_class }}">
<a href="{{ $menuItemUrl }}"
@if($menuItem->is_new_window) target="_blank" @endif
class="@if(wncms()->isActiveUrl($menuItemUrl)) active @endif">
@if($menuItem->icon)
<i class="{{ $menuItem->icon }}"></i>
@endif
{{ $menuItem->name }}
</a>
</li>
@endforeach
</ul>
</nav>
@endif1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
带子选单的选单
两层选单:
blade
@if(gto('header_menu'))
<nav class="main-menu">
<ul class="menu-list">
@foreach(wncms()->menu()->getMenuParentItems(gto('header_menu')) as $menuItem)
@php $menuItemUrl = wncms()->menu()->getMenuItemUrl($menuItem); @endphp
<li class="menu-item {{ $menuItem->css_class }}
@if($menuItem->children->count()) has-submenu @endif">
<a href="{{ $menuItemUrl }}"
class="menu-link @if(wncms()->isActiveUrl($menuItemUrl)) active @endif"
@if($menuItem->is_new_window) target="_blank" @endif>
@if($menuItem->icon)
<i class="{{ $menuItem->icon }}"></i>
@endif
{{ $menuItem->name }}
@if($menuItem->children->count())
<i class="fas fa-chevron-down"></i>
@endif
</a>
{{-- 子选单 --}}
@if($menuItem->children->count())
<ul class="submenu">
@foreach($menuItem->children as $subMenuItem)
@php $subMenuItemUrl = wncms()->menu()->getMenuItemUrl($subMenuItem); @endphp
<li class="submenu-item {{ $subMenuItem->css_class }}">
<a href="{{ $subMenuItemUrl }}"
class="submenu-link @if(wncms()->isActiveUrl($subMenuItemUrl)) active @endif"
@if($subMenuItem->is_new_window) target="_blank" @endif>
@if($subMenuItem->icon)
<i class="{{ $subMenuItem->icon }}"></i>
@endif
{{ $subMenuItem->name }}
</a>
</li>
@endforeach
</ul>
@endif
</li>
@endforeach
</ul>
</nav>
@endif1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
多层选单(递回)
用于深层阶层:
blade
@if(gto('header_menu'))
@php
$menuItems = wncms()->menu()->getMenuParentItems(gto('header_menu'));
@endphp
<nav class="main-menu">
@include("$themeId::components.menu-items", ['items' => $menuItems, 'level' => 1])
</nav>
@endif1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
components/menu-items.blade.php:
blade
<ul class="menu-level-{{ $level }}">
@foreach($items as $menuItem)
@php
$menuItemUrl = wncms()->menu()->getMenuItemUrl($menuItem);
$hasChildren = $menuItem->children->count() > 0;
@endphp
<li class="menu-item {{ $menuItem->css_class }} @if($hasChildren) has-children @endif">
<a href="{{ $menuItemUrl }}"
class="menu-link @if(wncms()->isActiveUrl($menuItemUrl)) active @endif"
@if($menuItem->is_new_window) target="_blank" @endif>
@if($menuItem->icon)
<i class="{{ $menuItem->icon }}"></i>
@endif
{{ $menuItem->name }}
@if($hasChildren)
<i class="fas fa-angle-right"></i>
@endif
</a>
@if($hasChildren)
@include("$themeId::components.menu-items", [
'items' => $menuItem->children,
'level' => $level + 1
])
@endif
</li>
@endforeach
</ul>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
选单辅助函数
取得选单项目
php
// 仅取得顶层选单项目
$parentItems = wncms()->menu()->getMenuParentItems($menuId);
// 取得所有选单项目(包括子项目)
$menu = wncms()->menu()->get(['id' => $menuId]);
$allItems = $menu->menu_items;
// 取得选单的直接子项目
$directItems = $menu->direct_menu_items;1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
取得选单项目 URL
php
$menuItemUrl = wncms()->menu()->getMenuItemUrl($menuItem);1
此函数处理不同的 URL 类型:
- route:从路由名称生成 URL
- page:透过 ID 连结到页面
- post:透过 ID 连结到文章
- external:回传外部 URL
- custom:回传自订 URL
检查启用 URL
blade
@if(wncms()->isActiveUrl($menuItemUrl))
{{-- 当前页面 --}}
@endif1
2
3
2
3
选单样式
基本 CSS
css
/* 主选单容器 */
.main-menu {
display: flex;
align-items: center;
}
/* 选单列表 */
.menu-list {
display: flex;
list-style: none;
margin: 0;
padding: 0;
}
/* 选单项目 */
.menu-item {
position: relative;
margin: 0 15px;
}
/* 选单连结 */
.menu-link {
display: block;
padding: 10px 15px;
text-decoration: none;
color: #333;
transition: all 0.3s;
}
.menu-link:hover,
.menu-link.active {
color: #007bff;
}
/* 子选单 */
.submenu {
position: absolute;
top: 100%;
left: 0;
display: none;
background: #fff;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
list-style: none;
margin: 0;
padding: 10px 0;
min-width: 200px;
}
.menu-item:hover > .submenu {
display: block;
}
.submenu-item {
margin: 0;
}
.submenu-link {
display: block;
padding: 10px 20px;
color: #333;
text-decoration: none;
}
.submenu-link:hover {
background: #f5f5f5;
color: #007bff;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
行动选单
css
/* 行动选单切换 */
.menu-toggle {
display: none;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
}
@media (max-width: 768px) {
.menu-toggle {
display: block;
}
.menu-list {
display: none;
flex-direction: column;
width: 100%;
position: absolute;
top: 100%;
left: 0;
background: #fff;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.menu-list.active {
display: flex;
}
.menu-item {
margin: 0;
border-bottom: 1px solid #eee;
}
.submenu {
position: static;
box-shadow: none;
background: #f5f5f5;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
切换脚本:
javascript
// 行动选单切换
$('.menu-toggle').on('click', function () {
$('.menu-list').toggleClass('active')
})
// 点击外部时关闭选单
$(document).on('click', function (e) {
if (!$(e.target).closest('.main-menu').length) {
$('.menu-list').removeClass('active')
}
})1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
进阶功能
带图示的选单
blade
@foreach($menuItems as $menuItem)
<li class="menu-item">
<a href="{{ wncms()->menu()->getMenuItemUrl($menuItem) }}">
@if($menuItem->icon)
<i class="{{ $menuItem->icon }}"></i>
@endif
<span>{{ $menuItem->name }}</span>
</a>
</li>
@endforeach1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
图示选单的 CSS:
css
.menu-link {
display: flex;
align-items: center;
gap: 8px;
}
.menu-link i {
font-size: 18px;
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
超级选单
用于复杂的多栏选单:
blade
@foreach($menuItems as $menuItem)
@if($menuItem->children->count())
<li class="menu-item has-megamenu">
<a href="{{ wncms()->menu()->getMenuItemUrl($menuItem) }}">
{{ $menuItem->name }}
</a>
<div class="megamenu">
<div class="megamenu-grid">
@foreach($menuItem->children as $subMenuItem)
<div class="megamenu-column">
<h4>{{ $subMenuItem->name }}</h4>
@if($subMenuItem->children->count())
<ul>
@foreach($subMenuItem->children as $grandchild)
<li>
<a href="{{ wncms()->menu()->getMenuItemUrl($grandchild) }}">
{{ $grandchild->name }}
</a>
</li>
@endforeach
</ul>
@endif
</div>
@endforeach
</div>
</div>
</li>
@endif
@endforeach1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
超级选单 CSS:
css
.megamenu {
position: absolute;
top: 100%;
left: 0;
display: none;
background: #fff;
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
padding: 30px;
min-width: 600px;
}
.has-megamenu:hover .megamenu {
display: block;
}
.megamenu-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 30px;
}
.megamenu-column h4 {
margin: 0 0 15px;
font-size: 16px;
font-weight: 600;
}
.megamenu-column ul {
list-style: none;
margin: 0;
padding: 0;
}
.megamenu-column li {
margin: 5px 0;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
从选单产生面包屑
blade
@php
$breadcrumbs = [];
// 从当前选单项目建立面包屑轨迹的逻辑
@endphp
@if(count($breadcrumbs) > 0)
<nav class="breadcrumb">
<a href="{{ route('frontend.pages.home') }}">首页</a>
@foreach($breadcrumbs as $crumb)
<span class="separator">/</span>
<a href="{{ $crumb->url }}">{{ $crumb->name }}</a>
@endforeach
</nav>
@endif1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
最佳实践
1. 选单深度
为了可用性,限制选单阶层为 2-3 层:
blade
@if($level <= 3)
{{-- 渲染子选单 --}}
@else
{{-- 不渲染更深层级 --}}
@endif1
2
3
4
5
2
3
4
5
2. 效能
快取选单查询以获得更好的效能:
php
$menuItems = Cache::remember("menu_{$menuId}_items", 3600, function() use ($menuId) {
return wncms()->menu()->getMenuParentItems($menuId);
});1
2
3
2
3
3. 无障碍性
为萤幕阅读器新增 ARIA 属性:
blade
<nav class="main-menu" aria-label="主导航">
<ul role="menubar">
@foreach($menuItems as $menuItem)
<li role="none">
<a href="{{ wncms()->menu()->getMenuItemUrl($menuItem) }}"
role="menuitem"
@if($menuItem->children->count())
aria-haspopup="true"
aria-expanded="false"
@endif>
{{ $menuItem->name }}
</a>
@if($menuItem->children->count())
<ul role="menu" aria-label="{{ $menuItem->name }} 子选单">
{{-- 子选单项目 --}}
</ul>
@endif
</li>
@endforeach
</ul>
</nav>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
4. SEO 友善
使用描述性连结文字和适当结构:
blade
{{-- 好 --}}
<a href="/services/web-development">网页开发服务</a>
{{-- 避免 --}}
<a href="/services/web-development">点击这里</a>1
2
3
4
5
2
3
4
5
疑难排解
选单未显示
- 检查选单 ID 是否在主题选项中设定
- 验证选单有启用的项目
- 检查
gto('header_menu')是否回传有效的 ID - 确保选单项目的
status = 'active'
网址不正确
- 验证
url_type设定正确 - 检查路由名称是否存在于
routes/web.php - 确保页面/文章 ID 有效
- 测试外部网址
子选单未显示
- 检查资料库中的父子关系
- 验证子选单可见性的 CSS
- 测试滑鼠悬停/点击 JavaScript 事件
- 检查重叠元素的 z-index