You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

130 lines
5.8 KiB

<template>
<div class="app-shell">
<aside class="sidebar" :class="{ open: sidebarOpen }">
<div class="brand">
<div class="brand-mark"><span></span><span></span><span></span></div>
<div><strong>嘉谷</strong><small>低空智控平台</small></div>
</div>
<nav class="main-nav" aria-label="主导航">
<button class="nav-item" :class="{ active: isActive('monitor') }" @click="go('monitor')"><svg><use href="#i-radio"/></svg><span>实时监控</span></button>
<button class="nav-item" :class="{ active: isActive('devices') || isActive('device-detail') || isActive('control') }" @click="go('devices')"><svg><use href="#i-box"/></svg><span>设备管理</span></button>
<div v-if="!isAdmin" class="nav-group" :class="{ open: taskOpen }">
<button class="nav-item" :class="{ active: isActive('tasks') || isActive('replay'), expanded: taskOpen }" @click="toggleTask">
<svg><use href="#i-route"/></svg><span>任务管理</span><svg class="nav-arrow"><use href="#i-chevron"/></svg>
</button>
<div class="subnav">
<button class="subnav-item" :class="{ active: taskView === 'plans' }" @click="go('tasks', { view: 'plans' })"><span></span>任务计划</button>
<button class="subnav-item" :class="{ active: taskView === 'records' }" @click="go('tasks', { view: 'records' })"><span></span>执行记录</button>
<button class="subnav-item" :class="{ active: taskView === 'routes' }" @click="go('tasks', { view: 'routes' })"><span></span>航线库</button>
</div>
</div>
<div v-if="!isAdmin" class="nav-group" :class="{ open: mediaOpen }">
<button class="nav-item" :class="{ active: isActive('media'), expanded: mediaOpen }" @click="toggleMedia">
<svg><use href="#i-video"/></svg><span>媒体中心</span><svg class="nav-arrow"><use href="#i-chevron"/></svg>
</button>
<div class="subnav">
<button class="subnav-item" :class="{ active: mediaView === 'live' }" @click="go('media', { view: 'live' })"><span></span>直播中心</button>
<button class="subnav-item" :class="{ active: mediaView === 'videos' }" @click="go('media', { view: 'videos' })"><span></span>原始视频</button>
</div>
</div>
<button v-if="!isAdmin" class="nav-item" :class="{ active: isActive('alarms') }" @click="go('alarms')"><svg><use href="#i-alert"/></svg><span>告警运维</span></button>
<button v-if="!isAdmin" class="nav-item" :class="{ active: isActive('account') }" @click="go('account')"><svg><use href="#i-wallet"/></svg><span>账户中心</span></button>
<button v-if="isAdmin" class="nav-item" :class="{ active: isActive('users') }" @click="go('users')"><svg><use href="#i-user"/></svg><span>用户列表</span></button>
<button v-if="isAdmin" class="nav-item" :class="{ active: isActive('firmwares') }" @click="go('firmwares')"><svg><use href="#i-download"/></svg><span>固件管理</span></button>
<button v-if="isAdmin" class="nav-item" :class="{ active: isActive('system') }" @click="go('system')"><svg><use href="#i-settings"/></svg><span>系统管理</span></button>
</nav>
<div class="sidebar-bottom">
<div class="profile-wrap">
<button class="sidebar-profile" @click="profileOpen = !profileOpen" :aria-expanded="String(profileOpen)">
<span>{{ userName.charAt(0) }}</span>
<div><strong>{{ userName }}</strong><small>嘉谷科技</small></div>
<svg><use href="#i-chevron"/></svg>
</button>
<div class="profile-menu" v-show="profileOpen">
<div class="profile-menu-heading"><strong>{{ userName }}</strong><small>{{ userEmail }}</small></div>
<button class="logout-action" @click="logout">退出登录</button>
</div>
</div>
</div>
</aside>
<main class="workspace">
<button class="icon-button mobile-menu" title="打开导航" @click="sidebarOpen = !sidebarOpen"><svg><use href="#i-menu"/></svg></button>
<router-view />
</main>
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '../store/user'
import { useUiStore } from '../store/ui'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const ui = useUiStore()
const sidebarOpen = ref(false)
const profileOpen = ref(false)
const taskOpen = ref(false)
const mediaOpen = ref(false)
const isAdmin = computed(() => userStore.isAdmin())
const userName = computed(() => userStore.user?.name || userStore.user?.phone || '平台管理员')
const userEmail = computed(() => userStore.user?.email || 'admin@jiagu-tech.cn')
const taskView = computed(() => route.query.view || 'plans')
const mediaView = computed(() => route.query.view || 'videos')
watch(
() => route.name,
(name) => {
taskOpen.value = name === 'tasks' || name === 'replay'
mediaOpen.value = name === 'media'
sidebarOpen.value = false
profileOpen.value = false
},
{ immediate: true }
)
function isActive(name) {
return route.name === name
}
function go(name, query) {
router.push({ name, query })
}
function toggleTask() {
if (route.name === 'tasks') {
taskOpen.value = !taskOpen.value
} else {
router.push({ name: 'tasks' })
}
}
function toggleMedia() {
if (route.name === 'media') {
mediaOpen.value = !mediaOpen.value
} else {
router.push({ name: 'media' })
}
}
async function logout() {
const ok = await ui.confirm('确认退出当前账号?', '退出后需要重新登录才能访问平台。', ['退出当前会话', '保留登录状态', '清除本地凭证'])
if (ok) {
userStore.logout()
router.push('/login')
ui.toast('已退出登录')
}
}
</script>