5 changed files with 574 additions and 132 deletions
@ -1,130 +0,0 @@ |
|||||
<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 '@/stores/modules/userStore' |
|
||||
import { useUiStore } from '@/stores/modules/uiStore' |
|
||||
|
|
||||
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> |
|
||||
@ -0,0 +1,121 @@ |
|||||
|
<script setup> |
||||
|
import { ref, watch } from 'vue' |
||||
|
import { useRoute } from 'vue-router' |
||||
|
import SideMenu from '@/layout/components/SideMenu.vue' |
||||
|
import TopBar from '@/layout/components/TopBar.vue' |
||||
|
|
||||
|
const route = useRoute() |
||||
|
const sidebarOpen = ref(false) |
||||
|
|
||||
|
watch( |
||||
|
() => route.fullPath, |
||||
|
() => { |
||||
|
sidebarOpen.value = false |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
function toggleSidebar() { |
||||
|
sidebarOpen.value = !sidebarOpen.value |
||||
|
} |
||||
|
|
||||
|
function closeSidebar() { |
||||
|
sidebarOpen.value = false |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<t-layout :class="s.root"> |
||||
|
<t-aside :class="[s.aside, { [s.asideOpen]: sidebarOpen }]"> |
||||
|
<SideMenu /> |
||||
|
</t-aside> |
||||
|
<div v-if="sidebarOpen" :class="s.mask" @click="closeSidebar" /> |
||||
|
<t-layout :class="s.main"> |
||||
|
<t-header :class="s.header"> |
||||
|
<TopBar @toggle-sidebar="toggleSidebar" /> |
||||
|
</t-header> |
||||
|
<t-content :class="s.content"> |
||||
|
<!-- MapLayer mounts in Task 5; keep relative content for overlay --> |
||||
|
<router-view /> |
||||
|
</t-content> |
||||
|
</t-layout> |
||||
|
</t-layout> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="less" module="s"> |
||||
|
.root { |
||||
|
height: 100vh; |
||||
|
overflow: hidden; |
||||
|
background: #eef3f7; |
||||
|
|
||||
|
:global { |
||||
|
.t-layout__sider { |
||||
|
width: 208px !important; |
||||
|
min-width: 208px; |
||||
|
flex-shrink: 0; |
||||
|
background: #1a2b36 !important; |
||||
|
border: none !important; |
||||
|
} |
||||
|
|
||||
|
.t-layout__header { |
||||
|
height: auto; |
||||
|
padding: 0; |
||||
|
background: transparent; |
||||
|
border: none; |
||||
|
line-height: normal; |
||||
|
} |
||||
|
|
||||
|
.t-layout__content { |
||||
|
padding: 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.aside { |
||||
|
height: 100vh; |
||||
|
z-index: 30; |
||||
|
} |
||||
|
|
||||
|
.main { |
||||
|
min-width: 0; |
||||
|
height: 100vh; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.header { |
||||
|
flex-shrink: 0; |
||||
|
z-index: 20; |
||||
|
} |
||||
|
|
||||
|
.content { |
||||
|
position: relative; |
||||
|
height: 100%; |
||||
|
overflow: auto; |
||||
|
background: #eef3f7; |
||||
|
} |
||||
|
|
||||
|
.mask { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 1020px) { |
||||
|
.aside { |
||||
|
position: fixed; |
||||
|
left: -208px; |
||||
|
top: 0; |
||||
|
transition: left 0.2s ease; |
||||
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.28); |
||||
|
} |
||||
|
|
||||
|
.asideOpen { |
||||
|
left: 0; |
||||
|
} |
||||
|
|
||||
|
.mask { |
||||
|
display: block; |
||||
|
position: fixed; |
||||
|
inset: 0; |
||||
|
background: rgba(15, 23, 32, 0.36); |
||||
|
z-index: 25; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,365 @@ |
|||||
|
<script setup> |
||||
|
import { computed, ref, watch } from 'vue' |
||||
|
import { useRoute, useRouter } from 'vue-router' |
||||
|
import { useUserStore } from '@/stores/modules/userStore' |
||||
|
import { useUiStore } from '@/stores/modules/uiStore' |
||||
|
|
||||
|
const route = useRoute() |
||||
|
const router = useRouter() |
||||
|
const userStore = useUserStore() |
||||
|
const ui = useUiStore() |
||||
|
|
||||
|
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 profileOpen = ref(false) |
||||
|
|
||||
|
const activeValue = computed(() => { |
||||
|
const name = route.name |
||||
|
if (name === 'devices' || name === 'device-detail' || name === 'control') return 'devices' |
||||
|
if (name === 'tasks' || name === 'replay') { |
||||
|
const view = name === 'replay' ? 'records' : route.query.view || 'plans' |
||||
|
return `tasks:${view}` |
||||
|
} |
||||
|
if (name === 'media') return `media:${route.query.view || 'videos'}` |
||||
|
return name |
||||
|
}) |
||||
|
|
||||
|
const expanded = ref([]) |
||||
|
|
||||
|
watch( |
||||
|
() => route.name, |
||||
|
(name) => { |
||||
|
const next = [] |
||||
|
if (name === 'tasks' || name === 'replay') next.push('tasks') |
||||
|
if (name === 'media') next.push('media') |
||||
|
expanded.value = next |
||||
|
profileOpen.value = false |
||||
|
}, |
||||
|
{ immediate: true } |
||||
|
) |
||||
|
|
||||
|
function onChange(value) { |
||||
|
if (typeof value !== 'string') return |
||||
|
if (value.startsWith('tasks:')) { |
||||
|
router.push({ name: 'tasks', query: { view: value.slice(6) } }) |
||||
|
return |
||||
|
} |
||||
|
if (value.startsWith('media:')) { |
||||
|
router.push({ name: 'media', query: { view: value.slice(6) } }) |
||||
|
return |
||||
|
} |
||||
|
router.push({ name: value }) |
||||
|
} |
||||
|
|
||||
|
function onExpand(value) { |
||||
|
expanded.value = value |
||||
|
} |
||||
|
|
||||
|
async function logout() { |
||||
|
profileOpen.value = false |
||||
|
const ok = await ui.confirm('确认退出当前账号?', '退出后需要重新登录才能访问平台。', [ |
||||
|
'退出当前会话', |
||||
|
'保留登录状态', |
||||
|
'清除本地凭证', |
||||
|
]) |
||||
|
if (ok) { |
||||
|
userStore.logout() |
||||
|
router.push('/login') |
||||
|
ui.toast('已退出登录') |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div :class="s.root"> |
||||
|
<div :class="s.brand"> |
||||
|
<div :class="s.brandMark"><span /><span /><span /></div> |
||||
|
<div> |
||||
|
<strong>嘉谷</strong> |
||||
|
<small>低空智控平台</small> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<t-menu |
||||
|
theme="dark" |
||||
|
:value="activeValue" |
||||
|
:expanded="expanded" |
||||
|
width="208px" |
||||
|
:class="s.menu" |
||||
|
@change="onChange" |
||||
|
@expand="onExpand" |
||||
|
> |
||||
|
<t-menu-item value="monitor"> |
||||
|
<template #icon> |
||||
|
<svg :class="s.icon"><use href="#i-radio" /></svg> |
||||
|
</template> |
||||
|
实时监控 |
||||
|
</t-menu-item> |
||||
|
|
||||
|
<t-menu-item value="devices"> |
||||
|
<template #icon> |
||||
|
<svg :class="s.icon"><use href="#i-box" /></svg> |
||||
|
</template> |
||||
|
设备管理 |
||||
|
</t-menu-item> |
||||
|
|
||||
|
<t-submenu v-if="!isAdmin" value="tasks" title="任务管理"> |
||||
|
<template #icon> |
||||
|
<svg :class="s.icon"><use href="#i-route" /></svg> |
||||
|
</template> |
||||
|
<t-menu-item value="tasks:plans">任务计划</t-menu-item> |
||||
|
<t-menu-item value="tasks:records">执行记录</t-menu-item> |
||||
|
<t-menu-item value="tasks:routes">航线库</t-menu-item> |
||||
|
</t-submenu> |
||||
|
|
||||
|
<t-submenu v-if="!isAdmin" value="media" title="媒体中心"> |
||||
|
<template #icon> |
||||
|
<svg :class="s.icon"><use href="#i-video" /></svg> |
||||
|
</template> |
||||
|
<t-menu-item value="media:live">直播中心</t-menu-item> |
||||
|
<t-menu-item value="media:videos">原始视频</t-menu-item> |
||||
|
</t-submenu> |
||||
|
|
||||
|
<t-menu-item v-if="!isAdmin" value="alarms"> |
||||
|
<template #icon> |
||||
|
<svg :class="s.icon"><use href="#i-alert" /></svg> |
||||
|
</template> |
||||
|
告警运维 |
||||
|
</t-menu-item> |
||||
|
|
||||
|
<t-menu-item v-if="!isAdmin" value="account"> |
||||
|
<template #icon> |
||||
|
<svg :class="s.icon"><use href="#i-wallet" /></svg> |
||||
|
</template> |
||||
|
账户中心 |
||||
|
</t-menu-item> |
||||
|
|
||||
|
<t-menu-item v-if="isAdmin" value="users"> |
||||
|
<template #icon> |
||||
|
<svg :class="s.icon"><use href="#i-user" /></svg> |
||||
|
</template> |
||||
|
用户列表 |
||||
|
</t-menu-item> |
||||
|
|
||||
|
<t-menu-item v-if="isAdmin" value="firmwares"> |
||||
|
<template #icon> |
||||
|
<svg :class="s.icon"><use href="#i-download" /></svg> |
||||
|
</template> |
||||
|
固件管理 |
||||
|
</t-menu-item> |
||||
|
|
||||
|
<t-menu-item v-if="isAdmin" value="system"> |
||||
|
<template #icon> |
||||
|
<svg :class="s.icon"><use href="#i-settings" /></svg> |
||||
|
</template> |
||||
|
系统管理 |
||||
|
</t-menu-item> |
||||
|
</t-menu> |
||||
|
|
||||
|
<div :class="s.bottom"> |
||||
|
<button :class="s.profile" type="button" :aria-expanded="String(profileOpen)" @click="profileOpen = !profileOpen"> |
||||
|
<span>{{ userName.charAt(0) }}</span> |
||||
|
<div> |
||||
|
<strong>{{ userName }}</strong> |
||||
|
<small>嘉谷科技</small> |
||||
|
</div> |
||||
|
<svg :class="s.chevron"><use href="#i-chevron" /></svg> |
||||
|
</button> |
||||
|
<div v-show="profileOpen" :class="s.profileMenu"> |
||||
|
<div :class="s.profileHeading"> |
||||
|
<strong>{{ userName }}</strong> |
||||
|
<small>{{ userEmail }}</small> |
||||
|
</div> |
||||
|
<button type="button" :class="s.logout" @click="logout">退出登录</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="less" module="s"> |
||||
|
.root { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
height: 100%; |
||||
|
background: #1a2b36; |
||||
|
color: #d7e0e8; |
||||
|
} |
||||
|
|
||||
|
.brand { |
||||
|
height: 64px; |
||||
|
padding: 0 20px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 11px; |
||||
|
color: #fff; |
||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08); |
||||
|
flex-shrink: 0; |
||||
|
|
||||
|
strong { |
||||
|
display: block; |
||||
|
font-size: 19px; |
||||
|
line-height: 20px; |
||||
|
} |
||||
|
|
||||
|
small { |
||||
|
display: block; |
||||
|
margin-top: 2px; |
||||
|
font-size: 12px; |
||||
|
color: #8fa0ad; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.brandMark { |
||||
|
width: 28px; |
||||
|
height: 28px; |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(3, 1fr); |
||||
|
gap: 3px; |
||||
|
|
||||
|
span { |
||||
|
display: block; |
||||
|
border-radius: 2px; |
||||
|
background: #7db5f5; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.menu { |
||||
|
flex: 1; |
||||
|
overflow: auto; |
||||
|
border-right: none !important; |
||||
|
background: transparent !important; |
||||
|
|
||||
|
:global { |
||||
|
.t-default-menu__inner { |
||||
|
background: transparent; |
||||
|
} |
||||
|
|
||||
|
.t-menu__item { |
||||
|
margin: 0 10px 5px; |
||||
|
border-radius: 5px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.icon { |
||||
|
width: 17px; |
||||
|
height: 17px; |
||||
|
fill: none; |
||||
|
stroke: currentColor; |
||||
|
stroke-width: 1.8; |
||||
|
stroke-linecap: round; |
||||
|
stroke-linejoin: round; |
||||
|
} |
||||
|
|
||||
|
.bottom { |
||||
|
position: relative; |
||||
|
padding: 0 10px 14px; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
.profile { |
||||
|
width: 100%; |
||||
|
min-height: 44px; |
||||
|
padding: 0 10px; |
||||
|
border: 0; |
||||
|
border-radius: 5px; |
||||
|
background: transparent; |
||||
|
color: #dbe4eb; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 10px; |
||||
|
cursor: pointer; |
||||
|
text-align: left; |
||||
|
|
||||
|
&:hover { |
||||
|
color: #fff; |
||||
|
background: rgba(255, 255, 255, 0.06); |
||||
|
} |
||||
|
|
||||
|
> span { |
||||
|
width: 30px; |
||||
|
height: 30px; |
||||
|
border-radius: 50%; |
||||
|
background: #2d6fba; |
||||
|
color: #fff; |
||||
|
display: inline-flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
font-size: 13px; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
strong { |
||||
|
display: block; |
||||
|
font-size: 12px; |
||||
|
color: #f2f6f9; |
||||
|
} |
||||
|
|
||||
|
small { |
||||
|
display: block; |
||||
|
color: #8fa0ad; |
||||
|
font-size: 10px; |
||||
|
} |
||||
|
|
||||
|
div { |
||||
|
flex: 1; |
||||
|
min-width: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.chevron { |
||||
|
width: 12px; |
||||
|
height: 12px; |
||||
|
margin-left: auto; |
||||
|
transform: rotate(-90deg); |
||||
|
fill: none; |
||||
|
stroke: currentColor; |
||||
|
stroke-width: 1.8; |
||||
|
stroke-linecap: round; |
||||
|
stroke-linejoin: round; |
||||
|
color: #8fa0ad; |
||||
|
} |
||||
|
|
||||
|
.profileMenu { |
||||
|
position: absolute; |
||||
|
left: 10px; |
||||
|
right: 10px; |
||||
|
bottom: 62px; |
||||
|
padding: 10px; |
||||
|
border-radius: 8px; |
||||
|
background: #243746; |
||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28); |
||||
|
z-index: 5; |
||||
|
} |
||||
|
|
||||
|
.profileHeading { |
||||
|
margin-bottom: 8px; |
||||
|
|
||||
|
strong { |
||||
|
display: block; |
||||
|
color: #fff; |
||||
|
font-size: 13px; |
||||
|
} |
||||
|
|
||||
|
small { |
||||
|
color: #8fa0ad; |
||||
|
font-size: 11px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.logout { |
||||
|
width: 100%; |
||||
|
height: 34px; |
||||
|
border: 0; |
||||
|
border-radius: 5px; |
||||
|
background: rgba(200, 69, 75, 0.16); |
||||
|
color: #ffb4b8; |
||||
|
cursor: pointer; |
||||
|
|
||||
|
&:hover { |
||||
|
background: rgba(200, 69, 75, 0.28); |
||||
|
color: #fff; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,86 @@ |
|||||
|
<script setup> |
||||
|
import { computed } from 'vue' |
||||
|
import { useRoute } from 'vue-router' |
||||
|
|
||||
|
const emit = defineEmits(['toggle-sidebar']) |
||||
|
const route = useRoute() |
||||
|
|
||||
|
const titles = { |
||||
|
monitor: '实时监控', |
||||
|
devices: '设备管理', |
||||
|
'device-detail': '设备详情', |
||||
|
control: '设备控制', |
||||
|
tasks: '任务管理', |
||||
|
replay: '任务回放', |
||||
|
media: '媒体中心', |
||||
|
alarms: '告警运维', |
||||
|
account: '账户中心', |
||||
|
users: '用户列表', |
||||
|
firmwares: '固件管理', |
||||
|
system: '系统管理', |
||||
|
logs: '操作日志', |
||||
|
} |
||||
|
|
||||
|
const pageTitle = computed(() => titles[route.name] || '嘉谷低空智控平台') |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div :class="s.root"> |
||||
|
<button :class="s.mobileMenu" type="button" title="打开导航" @click="emit('toggle-sidebar')"> |
||||
|
<svg><use href="#i-menu" /></svg> |
||||
|
</button> |
||||
|
<div :class="s.title">{{ pageTitle }}</div> |
||||
|
<div :class="s.spacer" /> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="less" module="s"> |
||||
|
.root { |
||||
|
height: 56px; |
||||
|
padding: 0 16px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 12px; |
||||
|
background: #fff; |
||||
|
border-bottom: 1px solid #e7edf2; |
||||
|
} |
||||
|
|
||||
|
.mobileMenu { |
||||
|
display: none; |
||||
|
width: 36px; |
||||
|
height: 36px; |
||||
|
border: 0; |
||||
|
border-radius: 8px; |
||||
|
background: rgba(26, 43, 54, 0.76); |
||||
|
color: #fff; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
cursor: pointer; |
||||
|
|
||||
|
svg { |
||||
|
width: 18px; |
||||
|
height: 18px; |
||||
|
fill: none; |
||||
|
stroke: currentColor; |
||||
|
stroke-width: 1.8; |
||||
|
stroke-linecap: round; |
||||
|
stroke-linejoin: round; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.title { |
||||
|
font-size: 16px; |
||||
|
font-weight: 600; |
||||
|
color: #1f2a33; |
||||
|
} |
||||
|
|
||||
|
.spacer { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 1020px) { |
||||
|
.mobileMenu { |
||||
|
display: inline-flex; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
Loading…
Reference in new issue