refactor: 重构项目结构
- 优化 `车站-设备-告警` 轮询机制 - 改进设备卡片的布局 - 支持修改设备 - 告警轮询中获取完整告警数据 - 车站告警详情支持导出完整的 `今日告警列表` - 支持将状态持久化到 `IndexedDB` - 新增轮询控制 (调试模式) - 新增离线开发模式 (调试模式) - 新增 `IndexedDB` 数据控制 (调试模式)
This commit is contained in:
229
src/layouts/app-layout.vue
Normal file
229
src/layouts/app-layout.vue
Normal file
@@ -0,0 +1,229 @@
|
||||
<script setup lang="ts">
|
||||
import { SettingsDrawer, SyncCameraResultModal } from '@/components';
|
||||
import { useLineStationsQuery, useStompClient } from '@/composables';
|
||||
import { useAlarmStore, useSettingStore, useUserStore } from '@/stores';
|
||||
import { parseErrorFeedback } from '@/utils';
|
||||
import { useIsFetching, useIsMutating, useMutation } from '@tanstack/vue-query';
|
||||
import { AlertFilled, CaretDownFilled, DoubleLeftOutlined, DoubleRightOutlined, EnvironmentFilled, FileTextFilled, HddFilled, LogoutOutlined, SettingOutlined } from '@vicons/antd';
|
||||
import { isCancel } from 'axios';
|
||||
import {
|
||||
NBadge,
|
||||
NButton,
|
||||
NDropdown,
|
||||
NFlex,
|
||||
NIcon,
|
||||
NLayout,
|
||||
NLayoutContent,
|
||||
NLayoutFooter,
|
||||
NLayoutHeader,
|
||||
NLayoutSider,
|
||||
NMenu,
|
||||
type DropdownOption,
|
||||
type DropdownProps,
|
||||
type MenuOption,
|
||||
} from 'naive-ui';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, h, ref, watchEffect, type Component, type VNode } from 'vue';
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { userInfo } = storeToRefs(userStore);
|
||||
|
||||
const alarmStore = useAlarmStore();
|
||||
const { unreadAlarmCount } = storeToRefs(alarmStore);
|
||||
|
||||
const settingStore = useSettingStore();
|
||||
const { menuCollpased, offlineDev } = storeToRefs(settingStore);
|
||||
|
||||
const { syncCameraResult, afterCheckSyncCameraResult } = useStompClient();
|
||||
|
||||
useLineStationsQuery();
|
||||
|
||||
// 带key的query和mutation用于全局轮询 可用于渲染loading状态
|
||||
const fetchingCount = useIsFetching({ predicate: (query) => !!query.options.queryKey });
|
||||
const mutatingCount = useIsMutating({ predicate: (mutation) => !!mutation.options.mutationKey });
|
||||
const loadingCount = computed(() => fetchingCount.value + mutatingCount.value);
|
||||
|
||||
const onToggleMenuCollapsed = () => {
|
||||
menuCollpased.value = !menuCollpased.value;
|
||||
};
|
||||
|
||||
const menuOptions: MenuOption[] = [
|
||||
{
|
||||
label: () => h(RouterLink, { to: '/station' }, { default: () => '车站状态' }),
|
||||
key: '/station',
|
||||
icon: renderIcon(EnvironmentFilled),
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: '/device' }, { default: () => '设备诊断' }),
|
||||
key: '/device',
|
||||
icon: renderIcon(HddFilled),
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: '/alarm' }, { default: () => '设备告警' }),
|
||||
key: '/alarm',
|
||||
icon: renderIcon(AlertFilled),
|
||||
},
|
||||
{
|
||||
label: '系统日志',
|
||||
key: '/log',
|
||||
icon: renderIcon(FileTextFilled),
|
||||
children: [
|
||||
{
|
||||
label: () => h(RouterLink, { to: '/log/vimp-log' }, { default: () => '视频平台日志' }),
|
||||
key: '/log/vimp-log',
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: '/log/call-log' }, { default: () => '上级调用日志' }),
|
||||
key: '/log/call-log',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const dropdownOptions: DropdownOption[] = [
|
||||
{
|
||||
label: '退出登录',
|
||||
key: 'logout',
|
||||
icon: renderIcon(LogoutOutlined),
|
||||
onClick: async () => {
|
||||
try {
|
||||
await userStore.userLogout();
|
||||
router.push({ path: '/login' });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
window.$message.error('退出登录失败');
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const onSelectDropdownOption: DropdownProps['onSelect'] = (value: string, option: DropdownOption) => {
|
||||
if (typeof option['onClick'] === 'function') {
|
||||
option['onClick']();
|
||||
}
|
||||
};
|
||||
|
||||
const showSettingsDrawer = ref(false);
|
||||
const openSettingsDrawer = () => {
|
||||
showSettingsDrawer.value = true;
|
||||
};
|
||||
|
||||
const routeToRoot = () => {
|
||||
router.push({ path: '/' });
|
||||
};
|
||||
|
||||
const routeToAlarmPage = () => {
|
||||
alarmStore.clearUnreadAlarms();
|
||||
if (route.path !== '/alarm') {
|
||||
router.push({ path: '/alarm' });
|
||||
}
|
||||
};
|
||||
|
||||
const { mutate: getUserInfo } = useMutation({
|
||||
mutationFn: async (params?: { signal?: AbortSignal }) => {
|
||||
const { signal } = params ?? {};
|
||||
await userStore.userGetInfo({ signal });
|
||||
},
|
||||
onError: (error) => {
|
||||
if (isCancel(error)) return;
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
|
||||
// 判断是否为离线开发模式 决定是否自动发送获取用户信息请求
|
||||
watchEffect((onCleanup) => {
|
||||
if (offlineDev.value) return;
|
||||
const abortController = new AbortController();
|
||||
getUserInfo({ signal: abortController.signal });
|
||||
onCleanup(() => abortController.abort());
|
||||
});
|
||||
|
||||
function renderIcon(icon: Component): () => VNode {
|
||||
return () => h(NIcon, null, { default: () => h(icon) });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NLayout has-sider style="min-width: 1440px">
|
||||
<!-- 左侧菜单 -->
|
||||
<NLayoutSider bordered :collapsed="menuCollpased" collapse-mode="width" :collapsed-width="64" @update:collapsed="onToggleMenuCollapsed">
|
||||
<NFlex vertical justify="space-between" :size="0" style="height: 100%">
|
||||
<NMenu :collapsed="menuCollpased" :collapsed-width="64" :collapsed-icon-size="20" :value="route.path" :options="menuOptions" />
|
||||
<NButton block quaternary :focusable="false" @click="onToggleMenuCollapsed">
|
||||
<template #icon>
|
||||
<NIcon :component="menuCollpased ? DoubleRightOutlined : DoubleLeftOutlined" />
|
||||
</template>
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</NLayoutSider>
|
||||
<!-- 主体内容区域 -->
|
||||
<NLayout>
|
||||
<NLayoutHeader bordered class="app-layout-header">
|
||||
<NFlex justify="space-between" align="center" :size="8" style="width: 100%; height: 100%">
|
||||
<NFlex align="center">
|
||||
<h3 style="margin: 0 0 0 16px; cursor: pointer" @click="routeToRoot">网络设备管理平台</h3>
|
||||
<NButton text size="tiny" :loading="loadingCount > 0"></NButton>
|
||||
</NFlex>
|
||||
<NFlex align="center" :size="0" style="height: 100%">
|
||||
<NDropdown trigger="hover" show-arrow :options="dropdownOptions" @select="onSelectDropdownOption">
|
||||
<NButton :focusable="false" quaternary icon-placement="right" style="height: 100%">
|
||||
<template #default>
|
||||
<span>{{ userInfo?.nickName ?? '' }}</span>
|
||||
</template>
|
||||
<template #icon>
|
||||
<NIcon :component="CaretDownFilled" />
|
||||
</template>
|
||||
</NButton>
|
||||
</NDropdown>
|
||||
<NButton quaternary :focusable="false" style="height: 100%" @click="openSettingsDrawer">
|
||||
<template #icon>
|
||||
<NIcon :component="SettingOutlined" />
|
||||
</template>
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</NLayoutHeader>
|
||||
<NLayoutContent class="app-layout-content">
|
||||
<RouterView />
|
||||
</NLayoutContent>
|
||||
<NLayoutFooter bordered class="app-layout-footer">
|
||||
<NFlex :align="'center'" style="height: 100%; margin: 0 16px">
|
||||
<NBadge :value="unreadAlarmCount">
|
||||
<NButton secondary strong @click="routeToAlarmPage">
|
||||
<template #icon>
|
||||
<NIcon :component="AlertFilled" />
|
||||
</template>
|
||||
</NButton>
|
||||
</NBadge>
|
||||
</NFlex>
|
||||
</NLayoutFooter>
|
||||
</NLayout>
|
||||
</NLayout>
|
||||
|
||||
<SettingsDrawer v-model:show="showSettingsDrawer" />
|
||||
|
||||
<SyncCameraResultModal :sync-camera-result="syncCameraResult" @after-leave="afterCheckSyncCameraResult" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$layout-header-height: 48px;
|
||||
$layout-footer-height: 48px;
|
||||
|
||||
.app-layout-header {
|
||||
height: $layout-header-height;
|
||||
}
|
||||
|
||||
.app-layout-content {
|
||||
height: calc(100vh - $layout-header-height - $layout-footer-height);
|
||||
}
|
||||
|
||||
.app-layout-footer {
|
||||
height: $layout-footer-height;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user