;(function() {
'use strict';
window.navTree = [
{
group: 'nav.group_monitoring',
icon: '■',
children: [
{ hash: '#/', label: 'nav.overview', icon: '★' },
{ hash: '#/clients', label: 'nav.clients', icon: '●' },
{ hash: '#/subscriptions', label: 'nav.subscriptions', icon: '☰' },
{ hash: '#/retains', label: 'nav.retained', icon: '♣' },
{ disabled: true, label: 'nav.delayed_publish', icon: '⏰' },
{ disabled: true, label: 'nav.alarms', icon: '⚠' },
],
},
{
group: 'nav.group_access_control',
icon: '🔒',
children: [
{ disabled: true, label: 'nav.auth', icon: '🔒' },
{ disabled: true, label: 'nav.acl', icon: '✅' },
{ disabled: true, label: 'nav.blacklist', icon: '✗' },
{ disabled: true, label: 'nav.flapping', icon: '⚡' },
],
},
{
group: 'nav.group_integration',
icon: '⇄',
children: [
{ disabled: true, label: 'nav.webhooks', icon: '→' },
{ disabled: true, label: 'nav.connectors', icon: '⇄' },
],
},
{
group: 'nav.group_management',
icon: '⚙',
children: [
{
group: 'nav.group_cluster_config',
icon: '⚙',
children: [
{ disabled: true, label: 'nav.mqtt_config', icon: '⚙' },
{ disabled: true, label: 'nav.cluster', icon: '▪' },
{ disabled: true, label: 'nav.listeners', icon: '♫' },
{ disabled: true, label: 'nav.logs', icon: '☰' },
{ disabled: true, label: 'nav.monitoring', icon: '▶' },
],
},
{
group: 'nav.group_mqtt_advanced',
icon: '⚙',
children: [
{ disabled: true, label: 'nav.topic_rewrite', icon: '✎' },
{ disabled: true, label: 'nav.auto_subscribe', icon: '♦' },
{ disabled: true, label: 'nav.delayed_publish', icon: '⏰' },
],
},
{
group: 'nav.group_plugin_ext',
icon: '⚙',
children: [
{ disabled: true, label: 'nav.hooks', icon: '⚖' },
{ hash: '#/plugins', label: 'nav.plugins', icon: '⚙' },
],
},
],
},
{
group: 'nav.group_diagnostics',
icon: '🔍',
children: [
{ disabled: true, label: 'nav.ws_client', icon: '💬' },
{ disabled: true, label: 'nav.topics', icon: '♦' },
{ disabled: true, label: 'nav.slow_sub', icon: '☰' },
{ disabled: true, label: 'nav.trace', icon: '🔍' },
{ disabled: true, label: 'nav.drop_analysis', icon: '📈' },
],
},
{
group: 'nav.group_system',
icon: '⚖',
children: [
{ disabled: true, label: 'nav.users', icon: '👤' },
{ disabled: true, label: 'nav.audit_log', icon: '📄' },
{ disabled: true, label: 'nav.api_keys', icon: '🔑' },
],
},
];
window.SidebarNav = {
name: 'SidebarNav',
props: {
collapsed: { type: Boolean, default: false },
currentHash: { type: String, default: '' },
localeVersion: { type: Number, default: 0 },
},
emits: ['navigate', 'toggle-collapse'],
data: function() {
return {
expandedGroups: {}, hoveredGroup: null, hoveredItem: null, };
},
_hoverTimer: null,
computed: {
treeData: function() {
return window.navTree || [];
},
defaultExpanded: function() {
var result = {};
var tree = this.treeData;
if (tree && tree.length > 0) {
result[tree[0].group] = true;
}
return result;
},
},
methods: {
$t: function(key) {
return window.i18n ? window.i18n.$t(key) : key;
},
toggleGroup: function(groupKey) {
this.expandedGroups[groupKey] = !this.expandedGroups[groupKey];
},
isExpanded: function(groupKey) {
if (this.expandedGroups[groupKey] !== undefined) {
return this.expandedGroups[groupKey];
}
return !!this.defaultExpanded[groupKey];
},
isActive: function(hash) {
const cur = (this.currentHash || '').split('?')[0];
return cur === hash;
},
isDisabled: function(item) {
return item.disabled === true;
},
handleClick: function(item) {
if (this.isDisabled(item)) return;
if (item.hash) {
this.$emit('navigate', item.hash);
}
},
onGroupEnter: function(group) {
if (!this.collapsed) return;
this._clearHoverTimer();
this.hoveredGroup = group.group;
},
onGroupLeave: function() {
if (!this.collapsed) return;
var self = this;
this._clearHoverTimer();
this._hoverTimer = setTimeout(function() {
self.hoveredGroup = null;
self.hoveredItem = null;
}, 250);
},
onPopupEnter: function() {
this._clearHoverTimer();
},
onPopupLeave: function() {
this.onGroupLeave();
},
_clearHoverTimer: function() {
if (this._hoverTimer) {
clearTimeout(this._hoverTimer);
this._hoverTimer = null;
}
},
onItemEnter: function(item) {
if (this.collapsed) this.hoveredItem = item;
},
onItemLeave: function() {
if (this.collapsed) this.hoveredItem = null;
},
isSubGroup: function(item) {
return item.group && item.children;
},
},
template: `
<nav class="sidebar-nav" :class="{ collapsed: collapsed }">
<!-- 隐藏的语言版本号触发器,用于响应 locale 切换 -->
<span style="display:none">{{ localeVersion }}</span>
<div class="sidebar-nav-header">
<div v-if="!collapsed" class="sidebar-nav-brand">
<h2>RMQTT</h2>
<span class="sidebar-subtitle">Dashboard</span>
</div>
<div v-else class="sidebar-nav-brand-mini">
<h2>R</h2>
</div>
<button class="sidebar-collapse-btn" @click="$emit('toggle-collapse')" :title="$t('nav.toggle_sidebar')">
{{ collapsed ? '▶' : '◀' }}
</button>
</div>
<!-- 导航菜单 -->
<div class="sidebar-nav-body">
<div v-for="group in treeData" :key="group.group" class="nav-group"
@mouseenter="onGroupEnter(group)" @mouseleave="onGroupLeave()">
<!-- 分组标题 -->
<div class="nav-group-title" @click="toggleGroup(group.group)">
<span class="nav-icon" v-show="collapsed" v-html="group.icon"></span>
<span v-show="!collapsed" class="nav-label">{{ $t(group.group) }}</span>
<span v-show="!collapsed" class="nav-arrow" :class="{ open: isExpanded(group.group) }">▼</span>
</div>
<!-- 子菜单列表 -->
<div v-show="!collapsed && isExpanded(group.group)" class="nav-children">
<template v-for="item in group.children" :key="item.label || item.group">
<!-- 子分组(三级菜单) -->
<div v-if="isSubGroup(item)" class="nav-subgroup">
<div class="nav-subgroup-title" @click="toggleGroup(item.group)">
<span class="nav-label-sub">{{ $t(item.group) }}</span>
<span class="nav-arrow" :class="{ open: isExpanded(item.group) }">▼</span>
</div>
<div v-show="isExpanded(item.group)" class="nav-subchildren">
<div v-for="sub in item.children" :key="sub.label"
class="nav-item" :class="{ active: isActive(sub.hash), disabled: isDisabled(sub) }"
@click="handleClick(sub)">
<span class="nav-icon-small" v-show="collapsed" v-html="sub.icon"></span>
<span class="nav-label">{{ $t(sub.label) }}</span>
<span v-if="isDisabled(sub)" class="nav-badge-coming">soon</span>
</div>
</div>
</div>
<!-- 普通菜单项 -->
<div v-else class="nav-item"
:class="{ active: isActive(item.hash), disabled: isDisabled(item) }"
@click="handleClick(item)">
<span class="nav-icon" v-show="collapsed" v-html="item.icon"></span>
<span class="nav-label">{{ $t(item.label) }}</span>
<span v-if="isDisabled(item)" class="nav-badge-coming">soon</span>
</div>
</template>
</div>
</div>
</div>
<!-- 收缩模式 hover 浮层 -->
<div v-if="collapsed && hoveredGroup" class="sidebar-hover-popup"
@mouseenter="onPopupEnter" @mouseleave="onPopupLeave">
<div class="popup-title">{{ $t(hoveredGroup) }}</div>
<template v-for="group in treeData" :key="group.group">
<template v-if="group.group === hoveredGroup">
<div v-for="item in group.children" :key="item.label || item.group" class="popup-item-wrap">
<div v-if="isSubGroup(item)" class="popup-subgroup">
<div class="popup-subgroup-title">{{ $t(item.group) }}</div>
<div v-for="sub in item.children" :key="sub.label"
class="popup-item" :class="{ active: isActive(sub.hash), disabled: isDisabled(sub) }"
@click="handleClick(sub)">
{{ $t(sub.label) }}
</div>
</div>
<div v-else class="popup-item" :class="{ active: isActive(item.hash), disabled: isDisabled(item) }"
@click="handleClick(item)">
{{ $t(item.label) }}
</div>
</div>
</template>
</template>
</div>
</nav>
`,
};
})();