feat: offline status
Some checks failed
Test / build (lts/*, ubuntu-latest) (push) Has been cancelled
Test / build (lts/*, windows-latest) (push) Has been cancelled

This commit is contained in:
Dustella 2025-02-13 19:35:12 +08:00
parent 6d7d6cde37
commit 044a4b56e2
Signed by: Dustella
GPG Key ID: 35AA0AA3DC402D5C
8 changed files with 368 additions and 291 deletions

4
auto-imports.d.ts vendored
View File

@ -34,6 +34,7 @@ declare global {
const defineComponent: typeof import('vue')['defineComponent']
const defineLoader: typeof import('vue-router/auto')['defineLoader']
const definePage: typeof import('unplugin-vue-router/runtime')['definePage']
const doCheckOnline: typeof import('./src/composables/fetch')['doCheckOnline']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
@ -132,6 +133,7 @@ declare global {
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useBackendOnline: typeof import('./src/composables/fetch')['useBackendOnline']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
@ -334,6 +336,7 @@ declare module 'vue' {
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly doCheckOnline: UnwrapRef<typeof import('./src/composables/fetch')['doCheckOnline']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
@ -432,6 +435,7 @@ declare module 'vue' {
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useBackendOnline: UnwrapRef<typeof import('./src/composables/fetch')['useBackendOnline']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>

1
components.d.ts vendored
View File

@ -56,6 +56,7 @@ declare module 'vue' {
CollapsibleTrigger: typeof import('./src/components/ui/collapsible/CollapsibleTrigger.vue')['default']
Day_cycle_power_wave_plot: typeof import('./src/components/dense/saber/day_cycle_power_wave_plot.vue')['default']
Day_fft_ifft_plot: typeof import('./src/components/dense/saber/day_fft_ifft_plot.vue')['default']
DefaultLayout: typeof import('./src/components/DefaultLayout.vue')['default']
DenseFramework: typeof import('./src/components/DenseFramework.vue')['default']
Drawer: typeof import('./src/components/ui/drawer/Drawer.vue')['default']
DrawerContent: typeof import('./src/components/ui/drawer/DrawerContent.vue')['default']

View File

@ -8,7 +8,7 @@
<meta name="description" content="Opinionated Vite Starter Template" />
</head>
<body class="font-sans dark:text-white dark:bg-hex-121212">
<div id="app"></div>
<div id="app" class="w-full h-full"></div>
<noscript>
<div>Please enable JavaScript to use this application.</div>
</noscript>

View File

@ -4,7 +4,7 @@
"packageManager": "pnpm@9.14.4",
"scripts": {
"build": "vite build",
"dev": "vite --port 3333 --open",
"dev": "vite --port 10514 --open",
"lint": "eslint .",
"typecheck": "vue-tsc --noEmit",
"preview": "vite preview",

View File

@ -1,295 +1,22 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { useBackendOnline } from './composables'
import { ChevronRight, ChevronsUpDown } from 'lucide-vue-next'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '~/components/ui/breadcrumb'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '~/components/ui/collapsible'
import { Separator } from '~/components/ui/separator'
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
} from '~/components/ui/sidebar'
import { authCode } from './composables'
const router = useRouter()
function logout() {
localStorage.clear()
sessionStorage.clear()
authCode.value = ''
router.push('/auth')
}
onMounted(() => {
if (!authCode.value) {
router.push('/auth')
}
})
// This is sample data.
const data = {
user: {
name: 'shadcn',
email: 'm@example.com',
avatar: '/avatars/shadcn.jpg',
},
navMain: [
{
title: '流星雷达',
url: '#',
icon: 'ri-radar-fill',
isActive: true,
items: [
{
title: '潮汐波提取',
url: '/radar/TW-single',
},
{
title: '潮汐波统计',
url: '/radar/TW-stats',
},
{
title: '行星波提取',
url: '/radar/PW-single',
},
{
title: '行星波统计',
url: '/radar/PW-stats',
},
],
},
{
title: 'SABER',
url: '#',
icon: 'game-icons:cracked-saber',
isActive: true,
items: [
{
title: '重力波提取',
url: '/saber/gw/single',
},
{
title: '重力波统计',
url: '/saber/gw/stats',
},
],
},
{
title: '探空气球',
url: '#',
icon: 'bxs:balloon',
isActive: true,
items: [
{
title: '重力波单次',
url: '/balloon/single',
},
{
title: '重力波统计',
url: '/balloon/year',
},
],
},
{
title: 'TIDI',
url: '#',
icon: 'mdi:telescope',
isActive: true,
items: [
{
title: '行星波振幅',
url: '/tidi/waves',
},
{
title: '行星波月统计',
url: '/tidi/month_stats',
},
],
},
{
title: 'COSMIC',
url: '#',
icon: 'mdi:telescope',
isActive: true,
items: [
{
title: '行星波月统计',
url: '/cosmic/stats',
},
{
title: '行星波单次',
url: '/cosmic/single',
},
],
},
],
关于: [
{
name: 'Design Engineering',
url: '#',
icon: 'Frame',
},
],
} as const
const online = useBackendOnline()
</script>
<template>
<div>
<div v-if="$route.path === '/auth'">
<RouterView />
</div>
<div v-else>
<SidebarProvider>
<Sidebar collapsible="icon">
<SidebarHeader>
<Alert>
<AlertTitle>服务状态正常</AlertTitle>
</Alert>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>模型</SidebarGroupLabel>
<SidebarMenu>
<Collapsible
v-for="item in data.navMain"
:key="item.title"
as-child
:default-open="item.isActive"
class="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger as-child>
<SidebarMenuButton :tooltip="item.title">
<Icon :icon="item.icon" />
<span>{{ item.title }}</span>
<ChevronRight class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
<SidebarMenuSubItem
v-for="subItem in item.items"
:key="subItem.title"
class="hover:bg-gray-100"
>
<SidebarMenuSubButton as-child>
<RouterLink
:to="subItem.url"
active-class="bg-accent-foreground text-accent"
>
<span>&nbsp;</span>
<span>{{ subItem.title }}</span>
</RouterLink>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem flex="~ row items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">操作员</span>
<span class="truncate text-xs">已授权</span>
</div>
<ChevronsUpDown class="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
class="min-w-56 w-[--radix-dropdown-menu-trigger-width] rounded-lg" side="bottom" align="end"
:side-offset="4"
>
<div @click="logout">
<DropdownMenuItem>
<Icon icon="heroicons-solid:logout" class="h-4 w-4" />
退出系统
</DropdownMenuItem>
</div>
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" size="icon" class="ml-auto h-8 w-8">
<Icon icon="icon-park-outline:setting" class="h-4 w-4" />
<span class="sr-only">设置</span>
</Button>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
<SidebarRail />
</Sidebar>
<SidebarInset>
<header class="group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12 h-16 flex shrink-0 items-center justify-between gap-2 transition-[width,height] ease-linear">
<div class="flex items-center gap-2 px-4">
<Icon icon="heroicons-solid:home" class="h-4 w-4" />
<Separator orientation="vertical" class="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">
中高层大气波动解析识别技术系统
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator class="hidden md:block" />
<BreadcrumbItem class="hidden md:block">
<BreadcrumbLink href="#">
{{ $route.meta.group }}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator class="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>{{ $route.meta.item_name }}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div px-7>
<Alert variant="default">
<Icon icon="el:ok-circle" class="h-4 w-4 text-green!" />
<AlertTitle>服务状态正常</AlertTitle>
</Alert>
</div>
</header>
<RouterView class="h-full overflow-hidden" />
</SidebarInset>
</SidebarProvider>
<div h-full w-full>
<div v-if="!online.isOnline.value" class="absolute z-1000000 h-full w-full bg-gray/4 backdrop-blur-md">
<div class="h-full w-full flex flex-col items-center justify-center gap-3">
<h1 class="flex flex-row items-center gap-3 text-3xl">
<div class="i-tabler-network" />
后端服务不在线
</h1>
<div>
请检查后端是否运行, 或者服务器是否在维护
</div>
</div>
</div>
<DefaultLayout />
</div>
</template>

View File

@ -2,3 +2,111 @@ const KURONEKO_API = 'http://100.89.232.74:18200'
// export const API_BASE_URL = 'http://localhost:5000'
export const API_BASE_URL = import.meta.env.PROD ? 'https://gca-api.dustella.net:8443' : KURONEKO_API
// export const API_BASE_URL = 'https://gca-api.dustella.net:8443'
export const headerData = {
user: {
name: 'shadcn',
email: 'm@example.com',
avatar: '/avatars/shadcn.jpg',
},
navMain: [
{
title: '流星雷达',
url: '#',
icon: 'ri-radar-fill',
isActive: true,
items: [
{
title: '潮汐波提取',
url: '/radar/TW-single',
},
{
title: '潮汐波统计',
url: '/radar/TW-stats',
},
{
title: '行星波提取',
url: '/radar/PW-single',
},
{
title: '行星波统计',
url: '/radar/PW-stats',
},
],
},
{
title: 'SABER',
url: '#',
icon: 'game-icons:cracked-saber',
isActive: true,
items: [
{
title: '重力波提取',
url: '/saber/gw/single',
},
{
title: '重力波统计',
url: '/saber/gw/stats',
},
],
},
{
title: '探空气球',
url: '#',
icon: 'bxs:balloon',
isActive: true,
items: [
{
title: '重力波单次',
url: '/balloon/single',
},
{
title: '重力波统计',
url: '/balloon/year',
},
],
},
{
title: 'TIDI',
url: '#',
icon: 'mdi:telescope',
isActive: true,
items: [
{
title: '行星波振幅',
url: '/tidi/waves',
},
{
title: '行星波月统计',
url: '/tidi/month_stats',
},
],
},
{
title: 'COSMIC',
url: '#',
icon: 'mdi:telescope',
isActive: true,
items: [
{
title: '行星波月统计',
url: '/cosmic/stats',
},
{
title: '行星波单次',
url: '/cosmic/single',
},
],
},
],
: [
{
name: 'Design Engineering',
url: '#',
icon: 'Frame',
},
],
} as const

View File

@ -0,0 +1,190 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { ChevronRight, ChevronsUpDown } from 'lucide-vue-next'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '~/components/ui/breadcrumb'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '~/components/ui/collapsible'
import { Separator } from '~/components/ui/separator'
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
} from '~/components/ui/sidebar'
import { authCode, useBackendOnline } from '../composables'
import { headerData as data } from '../CONSTANT'
const router = useRouter()
function logout() {
localStorage.clear()
sessionStorage.clear()
authCode.value = ''
router.push('/auth')
}
onMounted(() => {
if (!authCode.value) {
router.push('/auth')
}
})
const online = useBackendOnline()
</script>
<template>
<div>
<div v-if="$route.path === '/auth'">
<RouterView />
</div>
<div v-else>
<SidebarProvider>
<Sidebar collapsible="icon">
<SidebarHeader>
<Alert>
<AlertTitle>服务状态正常 {{ online.isOnline }}</AlertTitle>
</Alert>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>模型</SidebarGroupLabel>
<SidebarMenu>
<Collapsible
v-for="item in data.navMain"
:key="item.title"
as-child
:default-open="item.isActive"
class="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger as-child>
<SidebarMenuButton :tooltip="item.title">
<Icon :icon="item.icon" />
<span>{{ item.title }}</span>
<ChevronRight class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
<SidebarMenuSubItem
v-for="subItem in item.items"
:key="subItem.title"
class="hover:bg-gray-100"
>
<SidebarMenuSubButton as-child>
<RouterLink
:to="subItem.url"
active-class="bg-accent-foreground text-accent"
>
<span>&nbsp;</span>
<span>{{ subItem.title }}</span>
</RouterLink>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem flex="~ row items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">操作员</span>
<span class="truncate text-xs">已授权</span>
</div>
<ChevronsUpDown class="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
class="min-w-56 w-[--radix-dropdown-menu-trigger-width] rounded-lg" side="bottom" align="end"
:side-offset="4"
>
<div @click="logout">
<DropdownMenuItem>
<Icon icon="heroicons-solid:logout" class="h-4 w-4" />
退出系统
</DropdownMenuItem>
</div>
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" size="icon" class="ml-auto h-8 w-8">
<Icon icon="icon-park-outline:setting" class="h-4 w-4" />
<span class="sr-only">设置</span>
</Button>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
<SidebarRail />
</Sidebar>
<SidebarInset>
<header class="group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12 h-16 flex shrink-0 items-center justify-between gap-2 transition-[width,height] ease-linear">
<div class="flex items-center gap-2 px-4">
<Icon icon="heroicons-solid:home" class="h-4 w-4" />
<Separator orientation="vertical" class="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">
中高层大气波动解析识别技术系统
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator class="hidden md:block" />
<BreadcrumbItem class="hidden md:block">
<BreadcrumbLink href="#">
{{ $route.meta.group }}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator class="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>{{ $route.meta.item_name }}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div px-7>
<Alert variant="default">
<Icon icon="el:ok-circle" class="h-4 w-4 text-green!" />
<AlertTitle>服务状态正常</AlertTitle>
</Alert>
</div>
</header>
<RouterView class="h-full overflow-hidden" />
</SidebarInset>
</SidebarProvider>
</div>
</div>
</template>

View File

@ -11,10 +11,9 @@ export const baseFetch = createFetch({
baseUrl: API_BASE_URL,
options: {
async beforeFetch({ options }) {
const code = '0101'
options.headers = {
...options.headers,
Authorization: `${code}`,
Authorization: `${authCode.value}`,
}
return { options }
},
@ -25,3 +24,51 @@ export const baseFetch = createFetch({
},
})
export async function doCheckOnline() {
let resp = null
try {
resp = await baseFetch('/ping', {
timeout: 2000,
})
const { error } = resp
if (error.value)
throw new Error(error.value)
const status = resp.response.value?.status
if (!status) {
return false
}
if (status === 200) {
return true
}
else if (status >= 400 && status < 500) {
return true
}
return false
}
catch (err) {
if (resp?.response.value?.status === 401) {
return true
}
console.error(err)
return false
}
}
export const useBackendOnline = createSharedComposable(() => {
const isOnline = ref(true)
const useCheckInterval = useIntervalFn(() => {
doCheckOnline().then((online) => {
isOnline.value = online
})
}, 5000, {
immediate: true,
immediateCallback: true,
})
useCheckInterval.resume()
return {
isOnline,
}
})