Compare commits

..

No commits in common. "main" and "dev-cosmic" have entirely different histories.

67 changed files with 1480 additions and 3489 deletions

1
.gitignore vendored
View File

@ -7,4 +7,3 @@ dist-ssr
node_modules
.idea/
*.log
*.7z

9
auto-imports.d.ts vendored
View File

@ -8,9 +8,7 @@ export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const authCode: typeof import('./src/composables/fetch')['authCode']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const baseFetch: typeof import('./src/composables/fetch')['baseFetch']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
@ -34,14 +32,12 @@ 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']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const hasConnection: typeof import('./src/composables/online')['hasConnection']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
@ -116,7 +112,6 @@ declare global {
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useAPIOnline: typeof import('./src/composables/online')['useAPIOnline']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
@ -134,7 +129,6 @@ 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']
@ -313,9 +307,7 @@ declare module 'vue' {
interface ComponentCustomProperties {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly authCode: UnwrapRef<typeof import('./src/composables/fetch')['authCode']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly baseFetch: UnwrapRef<typeof import('./src/composables/fetch')['baseFetch']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
@ -417,7 +409,6 @@ declare module 'vue' {
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useAPIOnline: UnwrapRef<typeof import('./src/composables/online')['useAPIOnline']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>

20
components.d.ts vendored
View File

@ -14,7 +14,6 @@ declare module 'vue' {
Alert: typeof import('./src/components/ui/alert/Alert.vue')['default']
AlertDescription: typeof import('./src/components/ui/alert/AlertDescription.vue')['default']
AlertTitle: typeof import('./src/components/ui/alert/AlertTitle.vue')['default']
AuthBlock: typeof import('./src/components/AuthBlock.vue')['default']
AutoForm: typeof import('./src/components/ui/auto-form/AutoForm.vue')['default']
AutoFormField: typeof import('./src/components/ui/auto-form/AutoFormField.vue')['default']
AutoFormFieldArray: typeof import('./src/components/ui/auto-form/AutoFormFieldArray.vue')['default']
@ -54,17 +53,8 @@ declare module 'vue' {
Collapsible: typeof import('./src/components/ui/collapsible/Collapsible.vue')['default']
CollapsibleContent: typeof import('./src/components/ui/collapsible/CollapsibleContent.vue')['default']
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']
CoolBack: typeof import('./src/components/CoolBack.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']
DrawerDescription: typeof import('./src/components/ui/drawer/DrawerDescription.vue')['default']
DrawerFooter: typeof import('./src/components/ui/drawer/DrawerFooter.vue')['default']
DrawerHeader: typeof import('./src/components/ui/drawer/DrawerHeader.vue')['default']
DrawerOverlay: typeof import('./src/components/ui/drawer/DrawerOverlay.vue')['default']
DrawerTitle: typeof import('./src/components/ui/drawer/DrawerTitle.vue')['default']
DropdownMenu: typeof import('./src/components/ui/dropdown-menu/DropdownMenu.vue')['default']
DropdownMenuCheckboxItem: typeof import('./src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue')['default']
DropdownMenuContent: typeof import('./src/components/ui/dropdown-menu/DropdownMenuContent.vue')['default']
@ -104,7 +94,6 @@ declare module 'vue' {
MenubarSubContent: typeof import('./src/components/ui/menubar/MenubarSubContent.vue')['default']
MenubarSubTrigger: typeof import('./src/components/ui/menubar/MenubarSubTrigger.vue')['default']
MenubarTrigger: typeof import('./src/components/ui/menubar/MenubarTrigger.vue')['default']
Month_year_power_wave_plot: typeof import('./src/components/dense/saber/month_year_power_wave_plot.vue')['default']
NavigationMenu: typeof import('./src/components/ui/navigation-menu/NavigationMenu.vue')['default']
NavigationMenuContent: typeof import('./src/components/ui/navigation-menu/NavigationMenuContent.vue')['default']
NavigationMenuIndicator: typeof import('./src/components/ui/navigation-menu/NavigationMenuIndicator.vue')['default']
@ -119,12 +108,11 @@ declare module 'vue' {
NumberFieldDecrement: typeof import('./src/components/ui/number-field/NumberFieldDecrement.vue')['default']
NumberFieldIncrement: typeof import('./src/components/ui/number-field/NumberFieldIncrement.vue')['default']
NumberFieldInput: typeof import('./src/components/ui/number-field/NumberFieldInput.vue')['default']
Paper: typeof import('./src/components/Paper.vue')['default']
ParamsCard: typeof import('./src/components/ParamsCard.vue')['default']
Plot_wave_fitting: typeof import('./src/components/dense/saber/plot_wave_fitting.vue')['default']
Popover: typeof import('./src/components/ui/popover/Popover.vue')['default']
PopoverContent: typeof import('./src/components/ui/popover/PopoverContent.vue')['default']
PopoverTrigger: typeof import('./src/components/ui/popover/PopoverTrigger.vue')['default']
RadarSingle: typeof import('./src/components/dense/RadarSingle.vue')['default']
RadioGroup: typeof import('./src/components/ui/radio-group/RadioGroup.vue')['default']
RadioGroupItem: typeof import('./src/components/ui/radio-group/RadioGroupItem.vue')['default']
ResizableHandle: typeof import('./src/components/ui/resizable/ResizableHandle.vue')['default']
@ -181,10 +169,14 @@ declare module 'vue' {
TabsContent: typeof import('./src/components/ui/tabs/TabsContent.vue')['default']
TabsList: typeof import('./src/components/ui/tabs/TabsList.vue')['default']
TabsTrigger: typeof import('./src/components/ui/tabs/TabsTrigger.vue')['default']
TestHeader: typeof import('./src/components/TestHeader.vue')['default']
Textarea: typeof import('./src/components/ui/textarea/Textarea.vue')['default']
TheFooter: typeof import('./src/components/TheFooter.vue')['default']
TheHeader: typeof import('./src/components/TheHeader.vue')['default']
Tooltip: typeof import('./src/components/ui/tooltip/Tooltip.vue')['default']
TooltipContent: typeof import('./src/components/ui/tooltip/TooltipContent.vue')['default']
TooltipProvider: typeof import('./src/components/ui/tooltip/TooltipProvider.vue')['default']
TooltipTrigger: typeof import('./src/components/ui/tooltip/TooltipTrigger.vue')['default']
TopHeader: typeof import('./src/components/TopHeader.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" class="w-full h-full"></div>
<div id="app"></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 10514 --host",
"dev": "vite --port 3333 --open",
"lint": "eslint .",
"typecheck": "vue-tsc --noEmit",
"preview": "vite preview",
@ -25,7 +25,6 @@
"shadcn-vue": "^0.11.3",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7",
"vaul-vue": "^0.2.0",
"vee-validate": "^4.15.0",
"vue": "^3.5.13",
"vue-router": "^4.5.0",

17
pnpm-lock.yaml generated
View File

@ -48,9 +48,6 @@ importers:
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7
vaul-vue:
specifier: ^0.2.0
version: 0.2.0(radix-vue@1.9.11(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3))
vee-validate:
specifier: ^4.15.0
version: 4.15.0(vue@3.5.13(typescript@5.6.3))
@ -3902,12 +3899,6 @@ packages:
validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
vaul-vue@0.2.0:
resolution: {integrity: sha512-YV0zqxc8NiVzr1z/Awwbaty0UDDchxj5BfhFbLiYu+Uz0rCfSaDK2zwmuXZvejBJKLGbWw9I5GLHJRse14lQew==}
peerDependencies:
radix-vue: ^1.4.0
vue: ^3.3.0
vee-validate@4.15.0:
resolution: {integrity: sha512-PGJh1QCFwCBjbHu5aN6vB8macYVWrajbDvgo1Y/8fz9n/RVIkLmZCJDpUgu7+mUmCOPMxeyq7vXUOhbwAqdXcA==}
peerDependencies:
@ -8619,14 +8610,6 @@ snapshots:
spdx-correct: 3.2.0
spdx-expression-parse: 3.0.1
vaul-vue@0.2.0(radix-vue@1.9.11(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3)):
dependencies:
'@vueuse/core': 10.11.1(vue@3.5.13(typescript@5.6.3))
radix-vue: 1.9.11(vue@3.5.13(typescript@5.6.3))
vue: 3.5.13(typescript@5.6.3)
transitivePeerDependencies:
- '@vue/composition-api'
vee-validate@4.15.0(vue@3.5.13(typescript@5.6.3)):
dependencies:
'@vue/devtools-api': 7.7.0

View File

@ -1,30 +1,242 @@
<script setup lang="ts">
// import { useBackendOnline } from './composables'
import { useAPIOnline } from './composables/online'
import { Icon } from '@iconify/vue'
const online = useAPIOnline()
import { ChevronRight, ChevronsUpDown } from 'lucide-vue-next'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '~/components/ui/breadcrumb'
onMounted(() => {
online.connect()
})
onUnmounted(() => {
online.disconnect()
})
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'
// 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/v1',
},
{
title: '潮汐波时空变化',
url: '/radar/v2',
},
],
},
{
title: 'Saber',
url: '#',
icon: 'game-icons:cracked-saber',
isActive: true,
items: [
{
title: '波动拟合图',
url: '/saber/plot_wave_fitting',
},
{
title: '日数据傅里叶变换分析',
url: '/saber/day_fft_ifft_plot',
},
{
title: '日周期波动能量分析',
url: '/saber/day_cycle_power_wave_plot',
},
{
title: '月度波动能量分析',
url: '/saber/month_power_wave_plot',
},
],
},
{
title: '探空气球',
url: '#',
icon: 'bxs:balloon',
isActive: true,
items: [
{
title: '重力波单次',
url: '/balloon/single',
},
{
title: '重力波统计',
url: '/balloon/year',
},
],
},
],
关于: [
{
name: 'Design Engineering',
url: '#',
icon: 'Frame',
},
],
} as const
</script>
<template>
<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>
<div>
<!-- <TopHeader /> -->
</div>
<div>
<SidebarProvider>
<Sidebar collapsible="icon">
<SidebarHeader>
<Alert>
<Terminal class="h-4 w-4" />
<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>{{ 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">
<DropdownMenuItem>
<Icon icon="heroicons-solid:logout" class="h-4 w-4" />
退出系统
</DropdownMenuItem>
</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>
<DefaultLayout />
</div>
</template>

View File

@ -1,3 +1,2 @@
// export const API_BASE_URL = 'http://localhost:5000'
export const API_BASE_URL = `/api`
export const API_BASE_URL = 'http://localhost:5000'
// export const API_BASE_URL = 'https://gca-api.dustella.net:8443'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

View File

@ -1,80 +0,0 @@
<script setup lang="ts">
import { Button } from '~/components/ui/button'
import { Input } from '~/components/ui/input'
import { Label } from '~/components/ui/label'
import { authCode } from '~/composables'
import manba from '../../public/pack.png'
const code = ref('')
const router = useRouter()
const remember = ref(false)
function auth() {
authCode.value = code.value
const resp = baseFetch(`/ping`, {
})
if (!resp.error.value) {
if (remember.value) {
localStorage.setItem('authCode', code.value)
}
else {
sessionStorage.setItem('authCode', code.value)
}
router.push('/')
return
}
// eslint-disable-next-line no-alert
alert('认证失败')
}
</script>
<template>
<div class="w-full lg:grid lg:grid-cols-2 lg:min-h-[600px] xl:min-h-[800px]">
<div class="hidden p-30 lg:block">
<img
:src="manba"
alt="Image"
class="h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
>
</div>
<div class="flex items-center justify-center bg-muted py-12">
<div class="grid mx-auto w-[350px] gap-6">
<div class="grid gap-2 text-center">
<h1 class="text-3xl font-bold">
系统认证
</h1>
<p class="text-balance text-muted-foreground">
本系统仅供内部使用
</p>
</div>
<div class="grid gap-4">
<div class="grid gap-2">
<div class="flex items-center">
<Label for="password">口令</Label>
</div>
<Input id="password" v-model="code" type="password" required />
</div>
<div flex="~ items-center gap-2">
<Checkbox
id="remember" :checked="remember" @update:checked="(state) => {
remember = state
}"
/>
<Label for="remember" class="flex items-center">
记住我
</Label>
</div>
<Button type="submit" class="w-full" @click.prevent="auth">
认证
</Button>
</div>
<div class="mt-4 text-center text-sm">
丢失密码请前往后端重置
<!-- <a href="#" class="underline">
Sign up
</a> -->
</div>
</div>
</div>
</div>
</template>

View File

@ -1,236 +0,0 @@
<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 } from '../composables'
const router = useRouter()
function logout() {
localStorage.clear()
sessionStorage.clear()
authCode.value = ''
router.push('/auth')
}
const IconMapping = {
探空气球: 'bxs:balloon',
SABER: 'game-icons:cracked-saber',
流星雷达: 'ri-radar-fill',
TIDI: 'mdi:telescope',
COSMIC: 'mdi:telescope',
} as Record<string, string>
function generateNavigationFromRoutes() {
const allRoutesWithMeta = router.getRoutes().filter(
(r) => {
// check if r.meta is {}
return Object.keys(r.meta).length > 0 && !!r.meta.group
},
)
// @ts-expect-error i expect grpup
const groupedItems = Object.groupBy(allRoutesWithMeta, r => r.meta.group)
return Object.entries(groupedItems).map(([group, routes]) => {
return {
title: group,
url: '#',
icon: IconMapping[group] ?? 'ri-radar-fill',
isActive: true,
items: routes!.map((r) => {
return {
title: r.meta.item_name as string,
url: r.path,
}
}),
}
})
}
const data = {
user: {
name: 'shadcn',
email: 'm@example.com',
avatar: '/avatars/shadcn.jpg',
},
navMain: generateNavigationFromRoutes(),
关于: [
{
name: 'Design Engineering',
url: '#',
icon: 'Frame',
},
],
} as const
onMounted(() => {
if (!authCode.value) {
router.push('/auth')
}
})
</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>
</div>
</template>

View File

@ -6,69 +6,11 @@ import {
ResizablePanelGroup,
} from '~/components/ui/resizable'
const props = defineProps<{
imageQuery: string
extraErrorHandle?: (resp: Response) => {
type: string
message: string
}
extraResponseHandle?: (resp: Response) => Promise<ImageResult>
defineProps<{
imageResult: ImageResult
}>()
defineEmits(['submit'])
const imageResult = reactive<ImageResult>({
result: 'idle',
resourceId: '',
message: '请你选择一个模式和日期',
})
const urlRef = computed(() => {
return props.imageQuery
})
const { onFetchResponse, isFetching, execute } = baseFetch(
urlRef,
{ immediate: false, updateDataOnError: true, onFetchError: async ({ error, data, response }) => {
// same as afterFetch
imageResult.result = 'error'
const rawErrorMsg = (await response?.json()).error.message as string ?? '未知错误'
if (rawErrorMsg.includes('Improper input')) {
imageResult.message = '数据异常,无法拟合. 请使用其他时间段再试'
return { error, data }
}
imageResult.message = rawErrorMsg ?? '未知错误'
return { error, data }
} },
)
watch(isFetching, (fetching) => {
if (fetching) {
imageResult.result = 'pending'
}
})
onFetchResponse(async (resp) => {
if (props.extraResponseHandle) {
const newResult = await props.extraResponseHandle(resp)
imageResult.result = newResult.result
imageResult.resourceId = newResult.resourceId
imageResult.message = newResult.message
}
else {
const blob = await resp.blob()
const url = URL.createObjectURL(blob)
imageResult.result = 'success'
imageResult.resourceId = url
}
})
function download() {
if (imageResult.result === 'success') {
const a = document.createElement('a')
a.href = imageResult.resourceId
a.download = 'image.png'
a.click()
}
}
</script>
<template>
@ -80,20 +22,14 @@ function download() {
>
<ResizablePanel id="demo-panel-1" :default-size="200" py-5>
<ParamsCard
@submit="() => {
$emit('submit')
execute()
}"
@download="download"
@submit="$emit('submit')"
>
<slot />
</ParamsCard>
</ResizablePanel>
<ResizableHandle id="demo-handle-1" />
<ResizablePanel id="demo-panel-2" :default-size="350">
<ImageContainer v-slot="metadata" :image-result="imageResult">
<slot name="extra-meta" :metadata="metadata.metadata" />
</ImageContainer>
<ResizablePanel id="demo-panel-2" :default-size="500">
<ImageContainer :image-result="imageResult" />
</ResizablePanel>
</ResizablePanelGroup>
</div>

View File

@ -3,9 +3,8 @@ import { Icon } from '@iconify/vue/dist/iconify.js'
export interface ImageResult {
result: 'success' | 'error' | 'pending' | 'idle'
resourceId: string
imageUrl: string
message?: string
extraMetadata?: Record<string, any>
}
defineProps<{
@ -18,14 +17,18 @@ defineProps<{
<Badge variant="outline" class="absolute left-3 top-3">
图片输出
</Badge>
<slot :metadata="imageResult.extraMetadata" />
<div v-if="imageResult.result === 'pending'" class="flex flex-1 items-center justify-center text-xl">
<Icon icon="akar-icons:loading" class="mr-2 animate-spin" />
<span>正在加载图片</span>
</div>
<Image v-else-if="imageResult.result === 'success'" class="flex flex-1 items-center justify-center text-xl" :image-url="imageResult.resourceId" />
<Image v-else-if="imageResult.result === 'success'" class="flex flex-1 items-center justify-center text-xl" :image-url="imageResult.imageUrl" />
<div v-else class="flex flex-1 items-center justify-center text-xl">
{{ imageResult.message }}
</div>
<Button type="submit" size="sm" class="ml-auto gap-1.5">
下载图片
<CornerDownLeft class="size-3.5" />
</Button>
</div>
</template>

79
src/components/Paper.vue Normal file
View File

@ -0,0 +1,79 @@
<script setup lang='ts'>
import { useEventListener, useFullscreen } from '@vueuse/core'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const fullscreen = useFullscreen(ref(document.querySelector('html')))
// const isDark = useDark()
useEventListener('keydown', (e) => {
if (document.activeElement === document.body) {
if (e.key === 'f') {
if (fullscreen.isFullscreen.value)
fullscreen.exit()
else
fullscreen.enter()
}
}
})
let no = route.path.slice(1)
if (no.startsWith('x'))
no = no.slice(1)
const shot = Boolean(route.query.shot)
const hideFrame = Boolean(route.query.hideFrame !== undefined || route.query.full !== undefined)
</script>
<template>
<div class="paper" :class="{ shot }">
<div v-if="!shot && !hideFrame" class="nav font-mono">
<router-link to="/" class="link block pt-1 text-xl">
<carbon-chevron-left />
</router-link>
</div>
<slot />
</div>
</template>
<style scoped>
.nav {
position: fixed;
top: 0;
left: 0;
right: 0;
padding: 12px;
}
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 8px 12px;
}
.shot .nav {
padding: 24px;
}
.shot .bottom-nav {
padding: 20px 24px;
}
.nav-links .next,
.nav-links .prev {
opacity: 0;
transition: 0.3s all ease-in-out;
margin-top: -1.5em;
display: block;
}
.nav-links:hover .next,
.nav-links:hover .prev {
opacity: 1;
margin-top: 0;
}
</style>

View File

@ -1,34 +1,32 @@
<script lang="ts" setup>
defineEmits(['submit', 'download'])
defineEmits(['submit'])
</script>
<template>
<!-- <Drawer>
<Drawer>
<DrawerTrigger as-child class="min-w-[20rem]">
<Button variant="ghost" size="icon" class="md:hidden">
<Settings class="size-4" />
<span class="sr-only">参数设置</span>
</Button>
</DrawerTrigger>
<DrawerContent class="max-h-[80vh]">
<DrawerHeader>
<DrawerTitle />
</DrawerHeader> -->
<form class="grid min-w-[20rem] w-full items-start gap-6 overflow-auto p-4 pt-0">
<fieldset class="grid gap-6 border rounded-lg p-4">
<legend class="px-1 text-sm font-medium -ml-1">
参数设置
</legend>
<div class="grid gap-3">
<slot />
<Button type="submit" class="mt-5 w-full" @click.prevent="$emit('submit')">
绘制
</Button>
<Button type="submit" class="mt-5 w-full" @click.prevent="$emit('download')">
下载
</Button>
</div>
</fieldset>
</form>
<!-- </DrawerContent>
</Drawer> -->
</DrawerHeader>
<form class="grid min-w-[20rem] w-full items-start gap-6 overflow-auto p-4 pt-0">
<fieldset class="grid gap-6 border rounded-lg p-4">
<legend class="px-1 text-sm font-medium -ml-1">
参数设置
</legend>
<div class="grid gap-3">
<slot />
<Button type="submit" class="mt-5 w-full" @click.prevent="$emit('submit')">
绘制
</Button>
</div>
</fieldset>
</form>
</DrawerContent>
</Drawer>
</template>

View File

@ -0,0 +1,150 @@
<script setup lang="ts">
import {
NavigationMenuContent,
NavigationMenuIndicator,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuRoot,
NavigationMenuTrigger,
NavigationMenuViewport,
} from 'radix-vue'
import { ref } from 'vue'
import NavigationMenuListItem from './NavigationMenuListItem.vue'
const currentTrigger = ref('')
</script>
<template>
<NavigationMenuRoot
v-model="currentTrigger"
class="relative z-[1] mx-auto flex justify-center"
>
<NavigationMenuList class="center shadow-blackA7 m-0 flex list-none rounded-[6px] bg-white p-1 shadow-[0_2px_10px]">
<NavigationMenuItem>
<NavigationMenuTrigger
class="group text-grass11 flex select-none items-center justify-between gap-[2px] rounded-[4px] px-3 py-2 text-[15px] font-medium leading-none outline-none hover:bg-green3 focus:shadow-[0_0_0_2px] focus:shadow-green7"
>
Learn
<Icon
icon="radix-icons:caret-down"
class="text-green10 relative top-[1px] transition-transform duration-[250ms] ease-in group-data-[state=open]:-rotate-180"
/>
</NavigationMenuTrigger>
<NavigationMenuContent
class="data-[motion=from-start]:animate-enterFromLeft data-[motion=from-end]:animate-enterFromRight data-[motion=to-start]:animate-exitToLeft data-[motion=to-end]:animate-exitToRight absolute left-0 top-0 w-full sm:w-auto"
>
<ul class="one grid m-0 list-none gap-x-[10px] p-[22px] sm:grid-cols-[0.75fr_1fr] sm:w-[500px]">
<li class="grid row-span-3">
<NavigationMenuLink as-child>
<a
class="h-full w-full flex flex-col select-none justify-end rounded-[6px] from-green9 to-teal9 bg-gradient-to-b p-[25px] no-underline outline-none focus:shadow-[0_0_0_2px] focus:shadow-green7"
href="/"
>
<img
class="w-16"
src="https://www.radix-vue.com/logo.svg"
>
<div class="mb-[7px] mt-4 text-[18px] text-white font-medium leading-[1.2]">Radix Primitives</div>
<p class="text-mauve4 text-[14px] leading-[1.3]">Unstyled, accessible components for Vue.</p>
</a>
</NavigationMenuLink>
</li>
<NavigationMenuListItem
href="https://stitches.dev/"
title="Stitches"
>
CSS-in-JS with best-in-class developer experience.
</NavigationMenuListItem>
<NavigationMenuListItem
href="/colors"
title="Colors"
>
Beautiful, thought-out palettes with auto dark mode.
</NavigationMenuListItem>
<NavigationMenuListItem
href="https://icons.radix-ui.com/"
title="Icons"
>
A crisp set of 15x15 icons, balanced and consistent.
</NavigationMenuListItem>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger
class="text-grass11 group flex select-none items-center justify-between gap-[2px] rounded-[4px] px-3 py-2 text-[15px] font-medium leading-none outline-none hover:bg-green3 focus:shadow-[0_0_0_2px] focus:shadow-green7"
>
Overview
<Icon
icon="radix-icons:caret-down"
class="text-green10 relative top-[1px] transition-transform duration-[250ms] ease-in group-data-[state=open]:-rotate-180"
/>
</NavigationMenuTrigger>
<NavigationMenuContent class="data-[motion=from-start]:animate-enterFromLeft data-[motion=from-end]:animate-enterFromRight data-[motion=to-start]:animate-exitToLeft data-[motion=to-end]:animate-exitToRight absolute left-0 top-0 w-full sm:w-auto">
<ul class="grid m-0 list-none gap-x-[10px] p-[22px] sm:grid-flow-col sm:grid-rows-3 sm:w-[600px]">
<NavigationMenuListItem
title="Introduction"
href="/docs/primitives/overview/introduction"
>
Build high-quality, accessible design systems and web apps.
</NavigationMenuListItem>
<NavigationMenuListItem
title="Getting started"
href="/docs/primitives/overview/getting-started"
>
A quick tutorial to get you up and running with Radix Primitives.
</NavigationMenuListItem>
<NavigationMenuListItem
title="Styling"
href="/docs/primitives/guides/styling"
>
Unstyled and compatible with any styling solution.
</NavigationMenuListItem>
<NavigationMenuListItem
title="Animation"
href="/docs/primitives/guides/animation"
>
Use CSS keyframes or any animation library of your choice.
</NavigationMenuListItem>
<NavigationMenuListItem
title="Accessibility"
href="/docs/primitives/overview/accessibility"
>
Tested in a range of browsers and assistive technologies.
</NavigationMenuListItem>
<NavigationMenuListItem
title="Releases"
href="/docs/primitives/overview/releases"
>
Radix Primitives releases and their changelogs.
</NavigationMenuListItem>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink
class="text-grass11 block select-none rounded-[4px] px-3 py-2 text-[15px] font-medium leading-none no-underline outline-none hover:bg-green3 focus:shadow-[0_0_0_2px] focus:shadow-green7"
href="https://github.com/unovue/radix-vue"
>
Github
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuIndicator
class="transition-[all,transform_250ms_ease] data-[state=visible]:animate-fadeIn data-[state=hidden]:animate-fadeOut top-full z-[1] h-[10px] flex items-end justify-center overflow-hidden duration-200 data-[state=hidden]:opacity-0"
>
<div class="relative top-[70%] h-[10px] w-[10px] rotate-[45deg] rounded-tl-[2px] bg-white" />
</NavigationMenuIndicator>
</NavigationMenuList>
<div class="absolute left-0 top-full w-full flex perspective-[2000px] justify-center">
<NavigationMenuViewport
class="transition-[width,_height] data-[state=open]:animate-scaleIn data-[state=closed]:animate-scaleOut relative mt-[10px] h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] overflow-hidden rounded-[10px] bg-white duration-300 sm:w-[var(--radix-navigation-menu-viewport-width)]"
/>
</div>
</NavigationMenuRoot>
</template>

View File

@ -0,0 +1,3 @@
<template>
<div />
</template>

View File

@ -0,0 +1,78 @@
<script setup lang="ts">
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from './ui/navigation-menu'
const header_data: Record<string, { title: string, href: string }[]> = {
探空气球: [{
title: '重力波单次',
href: '/balloon/single',
}, {
title: '重力波统计',
href: '/balloon/year',
}],
流星雷达: [{
title: '潮汐波强度',
href: '/radar/v1',
}, {
title: '潮汐波时空变化',
href: '/radar/v2',
}],
Saber: [
{
title: '波动拟合图',
href: '/saber/plot_wave_fitting',
},
{
title: '日数据傅里叶变换分析',
href: '/saber/day_fft_ifft_plot',
},
{
title: '日周期波动能量分析',
href: '/saber/day_cycle_power_wave_plot',
},
{
// month_power_wave_plot
title: ' 月度波动能量分析',
href: '/saber/month_power_wave_plot',
},
],
}
</script>
<template>
<NavigationMenu class="mx-auto">
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuLink href="/" :class="navigationMenuTriggerStyle()">
主页
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem v-for="(thisHeader, headers) in header_data" :key="headers">
<NavigationMenuTrigger>{{ headers }}</NavigationMenuTrigger>
<NavigationMenuContent>
<ul class="w-60 flex flex-col gap-3 p-6">
<li v-for="header in thisHeader" :key="header.href" class="row-span-3">
<NavigationMenuLink as-child>
<a
class="block select-none rounded-md p-3 leading-none no-underline outline-none transition-colors space-y-1 focus:bg-accent hover:bg-accent focus:text-accent-foreground hover:text-accent-foreground"
:href="header.href"
>
<div class="text-sm font-medium leading-none">
{{ header.title }}
</div>
</a>
</NavigationMenuLink>
</li>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
</template>

View File

@ -0,0 +1,123 @@
<script setup lang="ts">
import {
Menubar,
MenubarCheckboxItem,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSeparator,
MenubarShortcut,
MenubarSub,
MenubarSubContent,
MenubarSubTrigger,
MenubarTrigger,
} from '~/components/ui/menubar'
</script>
<template>
<Menubar>
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarItem>
New Tab <MenubarShortcut>T</MenubarShortcut>
</MenubarItem>
<MenubarItem>
New Window <MenubarShortcut>N</MenubarShortcut>
</MenubarItem>
<MenubarItem disabled>
New Incognito Window
</MenubarItem>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>Share</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem>Email link</MenubarItem>
<MenubarItem>Messages</MenubarItem>
<MenubarItem>Notes</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSeparator />
<MenubarItem>
Print... <MenubarShortcut>P</MenubarShortcut>
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Edit</MenubarTrigger>
<MenubarContent>
<MenubarItem>
Undo <MenubarShortcut>Z</MenubarShortcut>
</MenubarItem>
<MenubarItem>
Redo <MenubarShortcut>Z</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>Find</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem>Search the web</MenubarItem>
<MenubarSeparator />
<MenubarItem>Find...</MenubarItem>
<MenubarItem>Find Next</MenubarItem>
<MenubarItem>Find Previous</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSeparator />
<MenubarItem>Cut</MenubarItem>
<MenubarItem>Copy</MenubarItem>
<MenubarItem>Paste</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>View</MenubarTrigger>
<MenubarContent>
<MenubarCheckboxItem>Always Show Bookmarks Bar</MenubarCheckboxItem>
<MenubarCheckboxItem checked>
Always Show Full URLs
</MenubarCheckboxItem>
<MenubarSeparator />
<MenubarItem inset>
Reload <MenubarShortcut>R</MenubarShortcut>
</MenubarItem>
<MenubarItem disabled inset>
Force Reload <MenubarShortcut>R</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem inset>
Toggle Fullscreen
</MenubarItem>
<MenubarSeparator />
<MenubarItem inset>
Hide Sidebar
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Profiles</MenubarTrigger>
<MenubarContent>
<MenubarRadioGroup value="benoit">
<MenubarRadioItem value="andy">
Andy
</MenubarRadioItem>
<MenubarRadioItem value="benoit">
Benoit
</MenubarRadioItem>
<MenubarRadioItem value="Luis">
Luis
</MenubarRadioItem>
</MenubarRadioGroup>
<MenubarSeparator />
<MenubarItem inset>
Edit...
</MenubarItem>
<MenubarSeparator />
<MenubarItem inset>
Add Profile...
</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>
</template>

View File

@ -1,233 +0,0 @@
<script setup lang="ts">
const props = defineProps<{
waveType: '潮汐波' | '行星波'
isDay?: boolean
}>()
const selectedWave = computed(() => props.waveType)
const selectedMode = ref('2日行星波')
const selectedDateType = ref('month')
const selectedStation = ref('武汉左岭镇站')
const selectedYear = ref('2017')
const selectedWindType = ref('uwind')
const selectedH = ref(94)
const selectedDate = ref('20170316')
const selectedMonth = ref('1')
const modes = [
'2日行星波',
'5日行星波',
'10日行星波',
'16日行星波',
]
const paths = ref([] as string[])
const stations = ref<Set<string>>(new Set())
const years = ref<Set<string>>(new Set())
const dates = ref<Set<string>>(new Set())
const queryUrl = computed(() => {
const station = selectedStation.value
const year = encodeURIComponent(selectedYear.value)
const mode = selectedWave.value === '潮汐波'
? '潮汐波'
: selectedMode.value
const windType = selectedWindType.value
const query = new URLSearchParams()
query.set('station', station)
query.set('year', year)
query.set('model_name', mode)
query.set('wind_type', windType)
query.set('H', (selectedH.value * 1000).toString())
query.set('mode', props.isDay ? 'day' : selectedDateType.value)
if (props.isDay) {
const queryDay = `${selectedDate.value.slice(0, 4)}-${selectedDate.value.slice(4, 6)}-${selectedDate.value.slice(6, 8)}`
query.set('day', queryDay)
}
if (!props.isDay && selectedDateType.value === 'month') {
query.set('month', selectedMonth.value)
}
// const query = `?station=${station}&year=${year}&model_name=${mode}&wind_type=${windType}&H=${selectedH.value}`
const path = `/radar/render/heatmap?${query}`
return path
})
const urlModel = defineModel<string>()
syncRef(urlModel, queryUrl, { direction: 'rtl' })
onMounted(async () => {
const resp = await baseFetch(`/radar/metadata`).json()
const data = await resp.data.value
// use regex to extract the year from the path,
// ./radar/data\\\\2017\\ZLT_MET01_DLL_L21_01D_20170316.txt
const station_pattern = /data\/radar\/(.*?)\//
const year_pattern = /(\d{4})\/ZLT_/
const date_pattern = /01D_(\d{8})\.txt/
const pairs = data.map((source_text: string) => {
const station = source_text.match(station_pattern)?.[1]
const year = source_text.match(year_pattern)?.[1]
const date = source_text.match(date_pattern)?.[1]
return { station, year, date }
})
pairs.forEach(({ station, year, date }: {
station: string
year: string
date: string
}) => {
stations.value.add(station)
years.value.add(year)
dates.value.add(date)
})
paths.value = data
if (props.isDay) {
selectedDate.value = Array.from(dates.value).filter(
d => d.startsWith(`${selectedYear.value}${selectedMonth.value.padStart(2, '0')}`),
)[0]
}
})
watch([selectedYear, selectedMonth], () => {
selectedDate.value = Array.from(dates.value).filter(
d => d.startsWith(`${selectedYear.value}${selectedMonth.value.padStart(2, '0')}`),
)[0]
})
</script>
<template>
<DenseFramework :image-query="queryUrl">
<div>
<div flex="~ col gap-3" py-3>
<div v-if="selectedWave === '行星波'">
<Label>行星波周期</Label>
<Tabs v-model="selectedMode" default-value="2日行星波">
<TabsList class="grid grid-cols-4 w-full">
<TabsTrigger v-for="m in modes" :key="m" :value="m">
{{ m.replace("行星波", "") }}
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<Label for="wind_type">风类型</Label>
<Tabs id="wind_type" v-model="selectedWindType" default-value="uwind">
<TabsList class="grid grid-cols-2 w-full">
<TabsTrigger value="uwind">
纬向风
</TabsTrigger>
<TabsTrigger value="vwind">
经向风
</TabsTrigger>
</TabsList>
</Tabs>
<div v-if="!props.isDay">
<Label for="H">日期类型</Label>
<Tabs id="date_type" v-model="selectedDateType" default-value="month">
<TabsList class="grid grid-cols-2 w-full">
<!-- <TabsTrigger value="day">
</TabsTrigger> -->
<TabsTrigger value="month">
</TabsTrigger>
<TabsTrigger value="year">
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<Label>观测站</Label>
<Select v-model="selectedStation">
<SelectTrigger>
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>观测站</SelectLabel>
<SelectItem v-for="station in stations" :key="station" :value="station">
{{ station }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label>高度km</Label>
<NumberField
id="start"
v-model:model-value="selectedH" :format-options="{
useGrouping: false,
}" :default-value="94"
:step="2"
:max="110"
:min="70"
>
<NumberFieldContent>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
<Label>年份</Label>
<Select v-model="selectedYear">
<SelectTrigger>
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
<SelectItem v-for="year in years" :key="year" :value="year">
{{ year }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<div v-if="selectedDateType === 'month'">
<Label>月份</Label>
<Select v-model="selectedMonth">
<SelectTrigger>
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
<SelectItem
v-for="month in [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
]" :key="month" :value="month.toString()"
>
{{ month }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<div v-if="props.isDay ">
<Label>日期</Label>
<Select v-model="selectedDate">
<SelectTrigger>
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
<SelectItem
v-for="date in Array.from(dates).filter(
(d) => d.startsWith(`${selectedYear}${selectedMonth.padStart(2, '0')}`),
)" :key="date" :value="date"
>
{{ date }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
</div>
</Denseframework>
</template>
<style lang="scss" scoped>
</style>

View File

@ -1,157 +0,0 @@
<route lang="json">
{
"meta":{
"title":"Saber日周期功率波图",
"description":"Saber日周期功率波图",
"group":"Saber",
"item_name":"日周期功率波图"
}
}
</route>
<script setup lang="ts">
import { currentSaberDays, parseDayOfYear, refreshCurrentSaberDays, refreshPath, renderPath, saberPaths } from './utils'
const lat_ranges = [
'0,10',
'10,20',
'20,30',
'30,40',
'40,50',
'50,60',
]
const lon_ranges = [
'0 ~ 24',
'24 ~ 48',
'48 ~ 72',
'72 ~ 96',
'96 ~ 120',
'120 ~ 144',
'144 ~ 168',
'168 ~ 192',
'192 ~ 216',
'216 ~ 240',
'240 ~ 264',
'264 ~ 288',
'288 ~ 312',
'312 ~ 336',
'336 ~ 360',
]
const selected = reactive({
path: '',
day: '',
cycle_no: '1',
lat_range: '0,10',
})
const urll = computed(() => {
const query = new URLSearchParams()
query.set('path', selected.path)
query.set('day', selected.day)
query.set('cycle_no', selected.cycle_no.toString())
query.set('lat_range', selected.lat_range)
return `/saber/render/gravity_wave/per_day/power_wave_plot?${query}`
})
onMounted(async () => {
await refreshPath()
selected.path = saberPaths.value[1]
})
watch(() => selected.path, async () => {
if (selected.path === '') {
return
}
await refreshCurrentSaberDays(selected.path)
if (selected.day === '' && currentSaberDays.value.length > 0) {
selected.day = currentSaberDays.value[1]
}
})
</script>
<template>
<DenseFramework :image-query="urll">
<div>
<div flex="~ col items-stretch gap-3" py-3>
<slot />
<Label>纬度带°</Label>
<Select v-model="selected.lat_range">
<SelectTrigger>
<SelectValue placeholder="选择纬度带" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>纬度带°</SelectLabel>
<SelectItem v-for="lat_range in lat_ranges" :key="lat_range" :value="lat_range">
{{ lat_range.replace(",", " ~ ") }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label for="day">年月</Label>
<Select v-model="selected.path">
<SelectTrigger>
<SelectValue placeholder="选择日期" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>月份</SelectLabel>
<SelectItem v-for="path in saberPaths.sort((a, b) => renderPath(a).localeCompare(renderPath(b)))" :key="path" :value="path">
{{ renderPath(path) }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label for="day">日期</Label>
<Select id="day" v-model="selected.day">
<SelectTrigger>
<SelectValue placeholder="选择日期" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Day</SelectLabel>
<SelectItem v-for="day in currentSaberDays.slice(1)" :key="day" :value="day">
{{ parseDayOfYear (day.toString()).toLocaleDateString("zh", {
year: 'numeric',
month: 'long',
day: 'numeric',
}) }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label for="age">经度范围°</Label>
<Select v-model="selected.cycle_no">
<SelectTrigger>
<SelectValue placeholder="选择范围" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>经度范围°</SelectLabel>
<SelectItem v-for="i in 15" :key="i" :value="i.toString()">
{{ lon_ranges[i - 1] }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<!-- <NumberField
id="age"
v-model:model-value="selected.cycle_no"
:default-value="1" :min="0"
>
<NumberFieldContent>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField> -->
</div>
</div>
</DenseFramework>
</template>
<style scoped>
</style>

View File

@ -1,146 +0,0 @@
<route lang="json">
{
"meta": {
"title": "Saber日周期功率波图",
"description": "Saber日周期功率波图",
"group": "Saber",
"item_name": "日周期功率波图"
}
}
</route>
<script setup lang="ts">
import { currentSaberDays, parseDayOfYear, refreshCurrentSaberDays, refreshPath, renderPath, saberPaths } from './utils'
const selected = reactive({
path: './saber/data/2012/SABER_Temp_O3_April2012_v2.0.nc',
day: '',
cycle_no: '1',
lat_range: '0,10',
})
const lat_ranges = [
'0,10',
'10,20',
'20,30',
'30,40',
'40,50',
'50,60',
]
const lon_ranges = [
'0 ~ 24',
'24 ~ 48',
'48 ~ 72',
'72 ~ 96',
'96 ~ 120',
'120 ~ 144',
'144 ~ 168',
'168 ~ 192',
'192 ~ 216',
'216 ~ 240',
'240 ~ 264',
'264 ~ 288',
'288 ~ 312',
'312 ~ 336',
'336 ~ 360',
]
const urll = computed(() => {
// const path = encodeURIComponent(selected.path)
// const day = encodeURIComponent(selected.day)
// const query = `path=${path}&day=${day}&cycle_no=${selected.cycle_no}`
const query = new URLSearchParams()
query.set('path', selected.path)
query.set('day', selected.day)
query.set('cycle_no', selected.cycle_no.toString())
query.set('lat_range', selected.lat_range)
return `/saber/render/gravity_wave/per_day/fft_ifft?${query}`
})
onMounted(async () => {
await refreshPath()
selected.path = saberPaths.value[1]
})
watch(() => selected.path, async () => {
await refreshCurrentSaberDays(selected.path)
if (selected.day === '' && currentSaberDays.value.length > 0) {
selected.day = currentSaberDays.value[1]
}
})
</script>
<template>
<DenseFramework :image-query="urll">
<div>
<div flex="~ col items-stretch gap-3" py-3>
<slot />
<Label>纬度带°</Label>
<Select v-model:model-value="selected.lat_range">
<SelectTrigger>
<SelectValue placeholder="选择纬度带" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>纬度带°</SelectLabel>
<SelectItem v-for="lat_range in lat_ranges" :key="lat_range" :value="lat_range">
{{ lat_range.replace(",", " ~ ") }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label for="day">年月</Label>
<Select v-model="selected.path">
<SelectTrigger>
<SelectValue placeholder="选择日期" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>月份</SelectLabel>
<SelectItem v-for="path in saberPaths.sort((a, b) => renderPath(a).localeCompare(renderPath(b)))" :key="path" :value="path">
{{ renderPath(path) }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label for="day">日期</Label>
<Select id="day" v-model="selected.day">
<SelectTrigger>
<SelectValue placeholder="选择日期" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Day</SelectLabel>
<SelectItem v-for="day in currentSaberDays.slice(1)" :key="day" :value="day">
{{ parseDayOfYear(day.toString()).toLocaleDateString("zh", {
year: 'numeric',
month: 'long',
day: 'numeric',
}) }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label for="age">经度范围°</Label>
<Select v-model="selected.cycle_no">
<SelectTrigger>
<SelectValue placeholder="选择范围" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>经度范围°</SelectLabel>
<SelectItem v-for="i in 15" :key="i" :value="i.toString()">
{{ lon_ranges[i - 1] }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
</DenseFramework>
</template>
<style scoped>
</style>

View File

@ -1,144 +0,0 @@
<route lang="json">
{
"meta": {
"title": "Saber月周期功率波图",
"description": "Saber月周期功率波图",
"group": "Saber",
"item_name": "月周期功率波图"
}
}
</route>
<script setup lang="ts">
import { refreshPath, renderPath, saberPaths } from './utils'
const currentMode = ref('月统计图')
const allYears = ref([] as string[])
const lat_ranges = [
'-60,-50',
'-50,-40',
'-40,-30',
'-30,-20',
'-20,-10',
'-10,0',
'0,10',
'10,20',
'20,30',
'30,40',
'40,50',
'50,60',
]
const selected = reactive({
path: '',
lat_range: '0,10',
year: '',
})
const urll = computed(() => {
if (currentMode.value === '月统计图') {
const query = new URLSearchParams()
query.set('path', selected.path)
query.set('lat_range', selected.lat_range)
return `/saber/render/gravity_wave/per_month/power_wave_plot?${query}`
}
else {
const query = new URLSearchParams()
query.set('year', selected.year)
query.set('lat_range', selected.lat_range)
return `/saber/render/gravity_wave/per_year/power_wave_plot?${query}`
}
})
onMounted(async () => {
await refreshPath()
selected.path = saberPaths.value[0]
// traverse all paths to get all years,
// in each item, str is like ../data/saber/2017/SABER_Temp_O3_June2017_v2.0.nc
const years = Array.from(new Set(saberPaths.value.map((str: string) => str.match(/(\d{4})/)?.[1])))
allYears.value = years.filter(year => year !== null).map(year => year!)
selected.year = allYears.value[0]
})
</script>
<template>
<DenseFramework :image-query="urll">
<div>
<Label>模式</Label>
<Tabs v-model="currentMode">
<TabsList class="grid grid-cols-2 w-full">
<TabsTrigger value="月统计图">
月统计图
</TabsTrigger>
<TabsTrigger value="年统计图">
年统计图
</TabsTrigger>
</TabsList>
</Tabs>
<div v-if="currentMode === '月统计图'" flex="~ col items-stretch gap-3" py-3>
<Label for="day">年月</Label>
<Select v-model="selected.path">
<SelectTrigger>
<SelectValue placeholder="选择日期" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>月份</SelectLabel>
<SelectItem v-for="path in saberPaths.sort((a, b) => renderPath(a).localeCompare(renderPath(b)))" :key="path" :value="path">
{{ renderPath(path) }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label>纬度带°</Label>
<Select v-model="selected.lat_range">
<SelectTrigger>
<SelectValue placeholder="选择纬度带" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>纬度带°</SelectLabel>
<SelectItem v-for="lat_range in lat_ranges" :key="lat_range" :value="lat_range">
{{ lat_range.replace(",", " ~ ") }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<div v-else class="flex flex-col gap-3 py-2">
<Label>年份</Label>
<Select v-model="selected.year">
<SelectTrigger>
<SelectValue placeholder="选择年份" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>年份</SelectLabel>
<SelectItem v-for="year in allYears" :key="year" :value="year">
{{ year }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label>纬度带°</Label>
<Select v-model="selected.lat_range">
<SelectTrigger>
<SelectValue placeholder="选择纬度带" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>纬度带°</SelectLabel>
<SelectItem v-for="lat_range in lat_ranges" :key="lat_range" :value="lat_range">
{{ lat_range.replace(",", " ~ ") }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
</DenseFramework>
</template>
<style scoped>
</style>

View File

@ -1,142 +0,0 @@
<route lang="json">
{
"meta": {
"title": "Saber拟合",
"description": "Saber拟合",
"group": "Saber",
"item_name": "拟合"
}
}
</route>
<script setup lang="ts">
import { currentSaberDays, parseDayOfYear, refreshCurrentSaberDays, refreshPath, renderPath, saberPaths } from './utils'
const lat_ranges = [
'0,10',
'10,20',
'20,30',
'30,40',
'40,50',
'50,60',
]
const selected = reactive({
path: './saber/data\\2012\\SABER_Temp_O3_April2012_v2.0.nc',
day: '',
height_no: '1',
lat_ranges: '0,10',
})
const urll = computed(() => {
const query = new URLSearchParams()
query.set('path', selected.path)
query.set('day', selected.day)
query.set('height_no', selected.height_no.toString())
query.set('lat_range', selected.lat_ranges)
return `/saber/render/gravity_wave/per_day/wave_fitting?${query}`
})
onMounted(async () => {
await refreshPath()
selected.path = saberPaths.value[1]
})
watch(() => selected.path, async () => {
await refreshCurrentSaberDays(selected.path)
if (saberPaths.value.length > 0) {
selected.path = saberPaths.value[1]
}
})
function mapHeightValue(input: number) {
// Input range: 1 to 157
// Output range: 30 to 90
const inputMin = 1
const inputMax = 157
const outputMin = 30
const outputMax = 90
// Calculate slope
const m = (outputMax - outputMin) / (inputMax - inputMin)
// Calculate y-intercept
const b = outputMin - m * inputMin
// Apply linear mapping
return m * input + b
}
</script>
<template>
<DenseFramework :image-query="urll">
<div>
<div flex="~ col justify-stretch gap-3" py-3>
<slot />
<Label>纬度带°</Label>
<Select v-model="selected.lat_ranges">
<SelectTrigger>
<SelectValue placeholder="选择纬度带" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>纬度带°</SelectLabel>
<SelectItem v-for="lat_range in lat_ranges" :key="lat_range" :value="lat_range">
{{ lat_range.replace(",", " ~ ") }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label for="day">年月</Label>
<Select v-model="selected.path">
<SelectTrigger>
<SelectValue placeholder="选择日期" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>月份</SelectLabel>
<SelectItem v-for="path in saberPaths.sort((a, b) => renderPath(a).localeCompare(renderPath(b)))" :key="path" :value="path">
{{ renderPath(path) }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label for="day">日期</Label>
<Select id="day" v-model="selected.day">
<SelectTrigger>
<SelectValue placeholder="选择日期" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Day</SelectLabel>
<SelectItem v-for="day in currentSaberDays.slice(1)" :key="day" :value="day">
{{ parseDayOfYear(day.toString()).toLocaleDateString("zh", {
year: 'numeric',
month: 'long',
day: 'numeric',
}) }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label for="age">高度</Label>
<Select v-model="selected.height_no">
<SelectTrigger>
<SelectValue placeholder="选择高度" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>高度</SelectLabel>
<SelectItem v-for="height_no in 157" :key="height_no" :value="height_no.toString()">
{{ mapHeightValue(height_no).toFixed(2) }} km
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
</DenseFramework>
</template>
<style scoped>
</style>

View File

@ -1,47 +0,0 @@
const saberPaths = ref<string[]>([])
const currentSaberDays = ref<string>('')
async function refreshPath() {
if (saberPaths.value.length)
return
const resp = await baseFetch<string[]>(`/saber/metadata`).json()
const data = resp.data.value
saberPaths.value = data!
}
async function refreshCurrentSaberDays(path: string) {
const resp = await baseFetch<string>(`/saber/metadata/list_days?path=${path}`).json()
const data = resp.data.value
currentSaberDays.value = data!
}
function renderPath(path: string) {
const yearPattern = /\/data\/saber\/(\d{4})/
const year = path.match(yearPattern)?.[1]
const monthPattern = /Temp_O3_(.*)(\d{4})/
const month_mapping = {
January: '01月',
February: '02月',
March: '03月',
April: '04月',
May: '05月',
June: '06月',
July: '07月',
August: '08月',
September: '09月',
October: '10月',
November: '11月',
December: '12月',
} as const
const month = path.match(monthPattern)?.[1] as keyof typeof month_mapping
return `${year}${month_mapping[month]}`
}
function parseDayOfYear(dateString: string): Date {
const year = Number.parseInt(dateString.substring(0, 4))
const dayOfYear = Number.parseInt(dateString.substring(4)) - 1 // subtract 1 because JS dates are 0-based
const date = new Date(year, 0) // Start with January 1st of the year
date.setDate(dayOfYear + 1) // Add the days
return date
}
export { currentSaberDays, parseDayOfYear, refreshCurrentSaberDays, refreshPath, renderPath, saberPaths }

View File

@ -1,19 +0,0 @@
<script lang="ts" setup>
import type { DrawerRootEmits, DrawerRootProps } from 'vaul-vue'
import { useForwardPropsEmits } from 'radix-vue'
import { DrawerRoot } from 'vaul-vue'
const props = withDefaults(defineProps<DrawerRootProps>(), {
shouldScaleBackground: true,
})
const emits = defineEmits<DrawerRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DrawerRoot v-bind="forwarded">
<slot />
</DrawerRoot>
</template>

View File

@ -1,28 +0,0 @@
<script lang="ts" setup>
import type { DialogContentEmits, DialogContentProps } from 'radix-vue'
import type { HtmlHTMLAttributes } from 'vue'
import { useForwardPropsEmits } from 'radix-vue'
import { DrawerContent, DrawerPortal } from 'vaul-vue'
import { cn } from '~/lib/utils'
import DrawerOverlay from './DrawerOverlay.vue'
const props = defineProps<DialogContentProps & { class?: HtmlHTMLAttributes['class'] }>()
const emits = defineEmits<DialogContentEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DrawerPortal>
<DrawerOverlay />
<DrawerContent
v-bind="forwarded" :class="cn(
'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background',
props.class,
)"
>
<div class="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
<slot />
</DrawerContent>
</DrawerPortal>
</template>

View File

@ -1,20 +0,0 @@
<script lang="ts" setup>
import type { DrawerDescriptionProps } from 'vaul-vue'
import { DrawerDescription } from 'vaul-vue'
import { computed, type HtmlHTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<DrawerDescriptionProps & { class?: HtmlHTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DrawerDescription v-bind="delegatedProps" :class="cn('text-sm text-muted-foreground', props.class)">
<slot />
</DrawerDescription>
</template>

View File

@ -1,14 +0,0 @@
<script lang="ts" setup>
import type { HtmlHTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HtmlHTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('mt-auto flex flex-col gap-2 p-4', props.class)">
<slot />
</div>
</template>

View File

@ -1,14 +0,0 @@
<script lang="ts" setup>
import type { HtmlHTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HtmlHTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('grid gap-1.5 p-4 text-center sm:text-left', props.class)">
<slot />
</div>
</template>

View File

@ -1,18 +0,0 @@
<script lang="ts" setup>
import type { DialogOverlayProps } from 'radix-vue'
import { DrawerOverlay } from 'vaul-vue'
import { computed, type HtmlHTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<DialogOverlayProps & { class?: HtmlHTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DrawerOverlay v-bind="delegatedProps" :class="cn('fixed inset-0 z-50 bg-black/80', props.class)" />
</template>

View File

@ -1,20 +0,0 @@
<script lang="ts" setup>
import type { DrawerTitleProps } from 'vaul-vue'
import { DrawerTitle } from 'vaul-vue'
import { computed, type HtmlHTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<DrawerTitleProps & { class?: HtmlHTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DrawerTitle v-bind="delegatedProps" :class="cn('text-lg font-semibold leading-none tracking-tight', props.class)">
<slot />
</DrawerTitle>
</template>

View File

@ -1,8 +0,0 @@
export { default as Drawer } from './Drawer.vue'
export { default as DrawerContent } from './DrawerContent.vue'
export { default as DrawerDescription } from './DrawerDescription.vue'
export { default as DrawerFooter } from './DrawerFooter.vue'
export { default as DrawerHeader } from './DrawerHeader.vue'
export { default as DrawerOverlay } from './DrawerOverlay.vue'
export { default as DrawerTitle } from './DrawerTitle.vue'
export { DrawerClose, DrawerPortal, DrawerTrigger } from 'vaul-vue'

View File

@ -1,74 +0,0 @@
import { createFetch } from '@vueuse/core'
import { API_BASE_URL } from '~/CONSTANT'
export const authCode = ref(
localStorage.getItem('authCode')
|| sessionStorage.getItem('authCode')
|| '',
)
export const baseFetch = createFetch({
baseUrl: API_BASE_URL,
options: {
async beforeFetch({ options }) {
options.headers = {
...options.headers,
Authorization: `${authCode.value}`,
}
return { options }
},
},
fetchOptions: {
mode: 'cors',
},
})
// 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,
// }
// })

View File

@ -1,2 +1 @@
export * from './dark'
export * from './fetch'

View File

@ -1,163 +1,3 @@
import { useIntervalFn } from '@vueuse/core'
import { API_BASE_URL } from '~/CONSTANT'
export const hasConnection = ref(false)
interface UseAPIOnlineOptions {
/**
* WebSocket endpoint URL
*/
url?: string
/**
* (ms)
* @default 30000
*/
heartbeatInterval?: number
/**
* (ms)
* @default 5000
*/
reconnectDelay?: number
/**
*
*/
onConnected?: () => void
/**
*
*/
onDisconnected?: () => void
/**
*
*/
onError?: (error: any) => void
}
export function useAPIOnline(options: UseAPIOnlineOptions = {}) {
// check current protocol, if is https, use wss, if is http, use ws
const {
wsProtocol,
wsBaseURL,
} = (() => {
let wsProtocol = API_BASE_URL.startsWith('https') ? 'wss' : 'ws'
if (!API_BASE_URL.startsWith('http')) {
wsProtocol = location.protocol === 'https:' ? 'wss' : 'ws'
}
let wsBaseURL = API_BASE_URL.replace('https://', '').replace('http://', '')
if (!API_BASE_URL.startsWith('http')) {
wsBaseURL = location.host + API_BASE_URL
}
return { wsProtocol, wsBaseURL }
})()
const {
url = `${wsProtocol}://${wsBaseURL}/ping/ws`,
heartbeatInterval = 30000,
reconnectDelay = 5000,
onConnected,
onDisconnected,
onError,
} = options
const ws = ref<WebSocket | null>(null)
const isOnline = ref(false)
const isConnecting = ref(false)
// 最后一次收到pong的时间戳
const lastPongTime = ref(0)
// 检查是否需要重连的定时器
const { pause: pauseHealthCheck, resume: resumeHealthCheck } = useIntervalFn(() => {
const now = Date.now()
// 如果45秒没有收到pong认为连接断开
if (now - lastPongTime.value > 45000) {
isOnline.value = false
reconnect()
}
}, 15000)
// 心跳定时器
const { pause: pauseHeartbeat, resume: resumeHeartbeat } = useIntervalFn(() => {
if (ws.value?.readyState === WebSocket.OPEN) {
ws.value.send(JSON.stringify({ type: 'ping' }))
}
}, heartbeatInterval)
const connect = () => {
if (isConnecting.value || (ws.value?.readyState === WebSocket.CONNECTING)) {
return
}
isConnecting.value = true
// 清理旧的连接
if (ws.value) {
ws.value.close()
ws.value = null
}
const socket = new WebSocket(url)
socket.onopen = () => {
isOnline.value = true
isConnecting.value = false
lastPongTime.value = Date.now()
resumeHeartbeat()
resumeHealthCheck()
onConnected?.()
}
socket.onclose = () => {
isOnline.value = false
isConnecting.value = false
pauseHeartbeat()
pauseHealthCheck()
onDisconnected?.()
setTimeout(reconnect, reconnectDelay)
}
socket.onerror = (error) => {
isOnline.value = false
isConnecting.value = false
pauseHeartbeat()
pauseHealthCheck()
onError?.(error)
}
socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
if (data.type === 'pong') {
lastPongTime.value = Date.now()
}
}
catch (e) {
console.error('Failed to parse websocket message:', e)
}
}
ws.value = socket
}
function reconnect() {
if (!isOnline.value && !isConnecting.value) {
connect()
}
}
const disconnect = () => {
if (ws.value) {
ws.value.close()
ws.value = null
}
isOnline.value = false
isConnecting.value = false
pauseHeartbeat()
pauseHealthCheck()
}
return {
isOnline,
isConnecting,
connect,
disconnect,
reconnect,
}
}
// get `https://`

View File

@ -1,6 +1,6 @@
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { handleHotUpdate, routes } from 'vue-router/auto-routes'
import { routes } from 'vue-router/auto-routes'
import App from './App.vue'
import '@unocss/reset/tailwind.css'
@ -13,9 +13,5 @@ const router = createRouter({
routes,
history: createWebHistory(import.meta.env.BASE_URL),
})
if (import.meta.hot) {
handleHotUpdate(router)
}
app.use(router)
app.mount('#app')

View File

@ -1,12 +0,0 @@
<script setup lang="ts">
</script>
<template>
<div>
<AuthBlock />
</div>
</template>
<style scoped>
</style>

View File

@ -1,156 +0,0 @@
<route lang="yaml">
meta:
title: 探空气球-重力波单次
description: 探空气球-重力波单次
group: 探空气球
item_name: 重力波提取
</route>
<script setup lang="ts">
import type { ImageResult } from '~/components/ImageContainer.vue'
const modes = [
'观测的二阶多项式拟合',
'扰动分量的正弦波拟合',
'径向风-纬向风矢量图',
'温度-水平风矢量图',
] as const
const nameModeMapping: Record<string, string> = {
'径向风-纬向风矢量图': '经向风-纬向风矢量图',
}
const allPaths = ref([] as string[])
const allStations = ref([] as string[])
const selected = reactive({
selectedMode: '观测的二阶多项式拟合',
selectedPath: '',
station: 'LIN',
})
const currentPathesList = computed(() => {
const station = selected.station
return allPaths.value.filter((a: string) => a.includes(station))
})
onMounted(async () => {
await baseFetch<string []>(`/balloon/metadata`).json().then(({ data }) => {
const das = data.value!
allPaths.value = das
selected.selectedPath = das[0]
const _allStations = das.map((a: string) => {
const stationPattern = /data\/balloon\/(.+)\//
return a.match(stationPattern)?.[1].split('/')[0] ?? ''
}) as string[]
const stations = Array.from(new Set(_allStations))
allStations.value = stations
selected.station = stations[0]
})
})
function renderDate(str: string) {
const datePattern = /_\d{8}T\d{6}_/
const date = str.match(datePattern)![0]
const utcstr = date.replaceAll('_', '')
const realTime = new Date(utcstr.replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/, '$1-$2-$3T$4:$5:$6'))
return realTime.toLocaleString('zh', {
year: 'numeric',
month: 'long',
day: '2-digit',
hour: '2-digit',
minute: 'numeric',
second: 'numeric',
})
}
const queryUrl = computed(() => {
const selectedMode = selected.selectedMode
const selectedDate = selected.selectedPath
return `/balloon/render/single?mode=${encodeURIComponent(selectedMode)}&path=${encodeURIComponent(selectedDate)}`
})
const metadata = ref({} as Record<string, string>)
async function customHandle(resp: Response) {
if (resp.status === 204) {
const res: ImageResult = {
message: '这时刻没有重力波',
result: 'error',
resourceId: '',
}
metadata.value = {}
return res
}
if (resp.status === 200) {
const data = await resp.json()
const blob = await fetch(`data:image/png;base64,${data.image}`).then(r => r.blob())
const url = URL.createObjectURL(blob)
metadata.value = data.metadata
const res: ImageResult = {
message: '成功',
result: 'success',
resourceId: url,
}
return res
}
return {
message: '未知错误',
result: 'error',
resourceId: '',
} as ImageResult
}
</script>
<template>
<DenseFramework :image-query="queryUrl" :extra-response-handle="customHandle">
<template #extra-meta>
<div v-if="!(Object.keys(metadata).length === 0)" class="absolute right-3 top-3 rounded-md bg-white p-3 drop-shadow">
<span v-for="(v, k) in metadata" :key="k">{{ k }}: {{ v }}</span>
</div>
</template>
<Label>选择台站</Label>
<Select v-model="selected.station">
<SelectTrigger>
<SelectValue placeholder="选择台站" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>台站</SelectLabel>
<SelectItem v-for="s in allStations" :key="s" :value="s">
{{ s }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label>选择日期</Label>
<Select v-model="selected.selectedPath">
<SelectTrigger>
<SelectValue placeholder="选择日期" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>日期</SelectLabel>
<SelectItem v-for="path in currentPathesList" :key="path" :value="path">
{{ renderDate(path) }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label>选择模式</Label>
<Tabs v-model="selected.selectedMode" default-value="观测的二阶多项式拟合">
<TabsList class="grid grid-cols-1 w-full">
<TabsTrigger v-for="m in modes" :key="m" :value="m">
{{ nameModeMapping[m] ?? m }}
</TabsTrigger>
</TabsList>
</Tabs>
</DenseFramework>
</template>
<style lang="scss" scoped>
</style>

View File

@ -1,223 +0,0 @@
<route lang="yaml">
meta:
title: 探空气球-重力波统计
description: 探空气球-重力波统计
group: 探空气球
item_name: 重力波统计
</route>
<script setup lang="ts">
const selectedMode = ref('w/f值统计结果')
const selectedStation = ref('LIN')
const allStations = ref([] as string[])
const allPaths = ref([] as string[])
const isIllegal = ref(false)
const startYear = ref(2017)
const endYear = ref(2024)
onMounted(async () => {
await baseFetch<string []>(`/balloon/metadata`).json().then(({ data }) => {
const das = data.value!
allPaths.value = das
const _allStations = das.map((a: string) => {
const stationPattern = /data\/balloon\/(.+)\//
return a.match(stationPattern)?.[1].split('/')[0] ?? ''
}) as string[]
const stations = Array.from(new Set(_allStations))
allStations.value = stations
selectedStation.value = stations[0]
})
// set default start year and end year
const station = selectedStation.value
// filter out the data that belong to the selected station
const stationData = allPaths.value.filter(a => a.includes(station))
const stationYears = new Set(stationData.map((a) => {
const yearPattern = /-(\d{4})/
return Number.parseInt(a.match(yearPattern)?.[1] ?? '-1')
}).filter(a => a !== -1))
const minYear = Math.min(...Array.from(stationYears))
const maxYear = Math.max(...Array.from(stationYears))
startYear.value = minYear
endYear.value = maxYear
})
const currentMinBeginYear = computed(() => {
const station = selectedStation.value
// filter out the data that belong to the selected station
const stationData = allPaths.value.filter(a => a.includes(station))
const stationYears = new Set(stationData.map((a) => {
const yearPattern = /-(\d{4})/
return Number.parseInt(a.match(yearPattern)?.[1] ?? '-1')
}).filter(a => a !== -1))
return Math.min(...Array.from(stationYears))
})
const currentMaxEndYear = computed(() => {
const station = selectedStation.value
// filter out the data that belong to the selected station
const stationData = allPaths.value.filter(a => a.includes(station))
const stationYears = new Set(stationData.map((a) => {
const yearPattern = /-(\d{4})/
return Number.parseInt(a.match(yearPattern)?.[1] ?? '-1')
}).filter(a => a !== -1))
const result = Math.max(...Array.from(stationYears))
return result
})
watch([currentMinBeginYear, currentMaxEndYear], () => {
if (startYear.value < currentMinBeginYear.value) {
startYear.value = currentMinBeginYear.value
}
if (endYear.value > currentMaxEndYear.value) {
endYear.value = currentMaxEndYear.value
}
// finally, if start year is greater than end year, set end year to start year
if (startYear.value > endYear.value) {
endYear.value = startYear.value
}
// or if
})
const modes = [
'w/f值统计结果',
'周期统计结果',
'垂直波长分布',
'水平波长分布',
'纬向本征相速度',
'经向本征相速度',
'垂直本征相速度',
'Zonal wind amplitude (m/s)',
'扰动振幅统计结果',
'Temperature amplitude (K)',
'纬向动量通量统计结果',
'经向动量通量统计结果',
'horizontal propagation',
'每月上传/下传重力波占比',
'动能和势能分布情况',
] as const
const mode_display_mapping = {
'w/f值统计结果': '本征频率与惯性频率的比值',
'周期统计结果': '周期统计',
'垂直波长分布': '垂直波长',
'水平波长分布': '水平波长',
'纬向本征相速度': '纬向本征相速度',
'经向本征相速度': '经向本征相速度',
'垂直本征相速度': '垂直本征相速度',
'Zonal wind amplitude (m/s)': '纬向风扰动幅度',
'扰动振幅统计结果': '经向风扰动幅度',
'Temperature amplitude (K)': '温度扰动振幅',
'纬向动量通量统计结果': '纬向动量通量统计',
'经向动量通量统计结果': '经向动量通量统计',
'horizontal propagation': '水平传播',
'每月上传/下传重力波占比': '垂直传播',
'动能和势能分布情况': '动能和势能分布情况',
}
const queryUrl = computed(() => {
const params = new URLSearchParams({
mode: selectedMode.value,
start_year: startYear.value.toString(),
end_year: endYear.value.toString(),
station: selectedStation.value,
})
return `/balloon/render/year?${params.toString()}`
})
watch([selectedMode, startYear, endYear], () => {
if (startYear.value > endYear.value) {
isIllegal.value = true
return
}
isIllegal.value = false
})
</script>
<template>
<DenseFramework :image-query="queryUrl">
<div flex="col gap-5 ~ justify-center">
<div>
<Label>选择台站</Label>
<Select v-model="selectedStation">
<SelectTrigger>
<SelectValue placeholder="选择台站" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>台站</SelectLabel>
<SelectItem v-for="s in allStations" :key="s" :value="s">
{{ s }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<div>
<!-- {{ currentMaxEndYear }}{{ currentMinBeginYear }} -->
台站 {{ selectedStation }} 可选的数据范围为 {{ currentMinBeginYear }} - {{ currentMaxEndYear }}
</div>
<NumberField
id="start"
v-model:model-value="startYear" :format-options="{
useGrouping: false,
}" :default-value="2017"
:min="currentMinBeginYear"
:max="endYear"
>
<Label for="start">起始年</Label>
<NumberFieldContent>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
<NumberField
id="end"
v-model:model-value="endYear" :format-options="{
style: 'decimal',
notation: 'standard',
useGrouping: false,
}" :default-value="2017"
:min="startYear" :max="currentMaxEndYear"
>
<Label for="end">终止年</Label>
<NumberFieldContent>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
<div>
<Label>统计参数</Label>
<Select v-model="selectedMode">
<SelectTrigger>
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>模式</SelectLabel>
<SelectItem
v-for="mode in Array.from(modes).sort(((a, b) => {
return mode_display_mapping[a].length - mode_display_mapping[b].length
}))"
:key="mode" :value="mode"
>
{{ mode_display_mapping[mode] ?? mode }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
</DenseFramework>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,101 @@
<route lang="yaml">
meta:
title: 探空气球-重力波单次
description: 探空气球-重力波单次
group: 探空气球
item_name: 重力波单次
</route>
<script setup lang="ts">
import type { ImageResult } from '~/components/ImageContainer.vue'
import { useForm } from 'vee-validate'
import { z } from 'zod'
import { API_BASE_URL } from '~/CONSTANT'
const imageResult = reactive<ImageResult>({
result: 'idle',
imageUrl: '',
message: '请你选择一个模式和日期',
})
const modes = [
'观测的二阶多项式拟合',
'扰动分量的正弦波拟合',
'径向风-纬向风矢量图',
'温度-水平风矢量图',
] as const
const formSchema = shallowRef<z.ZodObject< any, any, any >>(z.object({
selectedMode: z.enum(modes).describe('选择一个模式'),
selectedDate: z.enum(['no date']).describe('选择一个日期'),
}))
const form = useForm()
const stagedDates = ref<string[]>([])
onMounted(async () => {
await fetch(`${API_BASE_URL}/balloon/metadata`).then(resp => resp.json()).then((data) => {
stagedDates.value = data
formSchema.value = z.object({
selectedMode: z.enum(modes).describe('选择一个模式'),
selectedDate: z.enum(data.map((d: string) => {
const datePattern = /_\d{8}T\d{6}/
if (!datePattern.test(d)) {
return ''
}
const capture = datePattern.exec(d)
if (!capture) {
return ''
}
return capture[0]
})),
}).describe('选择一个日期')
form.setFieldValue('selectedDate', data[0])
form.setFieldValue('selectedMode', modes[0])
})
})
async function get_image(
selectedMode: string,
selectedDate: string,
) {
const resp = await fetch(`${API_BASE_URL}/balloon/render/single?mode=${encodeURIComponent(selectedMode)}&path=${encodeURIComponent(selectedDate)}`)
// check for MIME Type, check if is png
const isPng = resp.headers.get('Content-Type') === 'image/png'
if (!isPng) {
imageResult.result = 'error' as const
imageResult.message = '这一天没有数据'
return
}
imageResult.result = 'success' as const
const blob = await resp.blob()
const url = URL.createObjectURL(blob)
imageResult.imageUrl = url
}
function submit() {
const value = form.values
const selectedMode = value.selectedMode
const selectedDate = stagedDates.value.find(d => d.includes(value.selectedDate))
get_image(selectedMode, selectedDate!)
}
</script>
<template>
<DenseFramework :image-result="imageResult" @submit="submit">
<AutoForm
:form="form"
:field-config="{
selectedMode: {
component: 'radio',
},
}"
:schema="formSchema"
/>
</DenseFramework>
</template>
<style lang="scss" scoped>
</style>

117
src/pages/balloon/year.vue Normal file
View File

@ -0,0 +1,117 @@
<route lang="yaml">
meta:
title: 探空气球-重力波统计
description: 探空气球-重力波统计
group: 探空气球
item_name: 重力波统计
</route>
<script setup lang="ts">
import type { ImageResult } from '~/components/ImageContainer.vue'
import { API_BASE_URL } from '~/CONSTANT'
const selectedMode = ref('w/f值统计结果')
const imageResult = reactive<ImageResult>({
result: 'idle',
imageUrl: '',
message: '请你选择一个模式和日期',
})
const isIllegal = ref(false)
const startYear = ref(2017)
const endYear = ref(2024)
const modes = [
'w/f值统计结果',
'周期统计结果',
'垂直波长分布',
'水平波长分布',
'纬向本征相速度',
'经向本征相速度',
'垂直本征相速度',
'Zonal wind amplitude (m/s)',
'扰动振幅统计结果',
'Temperature amplitude (K)',
'纬向动量通量统计结果',
'经向动量通量统计结果',
'horizontal propagation',
'每月上传/下传重力波占比',
'动能和势能分布情况',
]
async function refreshImage() {
const url = `${API_BASE_URL}/balloon/render/year?mode=${encodeURIComponent(selectedMode.value)}&start_year=${startYear.value}&end_year=${endYear.value}`
const resp = await fetch(url)
const blob = await resp.blob()
const u = URL.createObjectURL(blob)
imageResult.result = 'success'
imageResult.imageUrl = u
}
watch([selectedMode, startYear, endYear], () => {
if (startYear.value > endYear.value) {
isIllegal.value = true
return
}
isIllegal.value = false
refreshImage()
})
</script>
<template>
<DenseFramework :image-result="imageResult" @submit="refreshImage">
<div flex="col gap-5 ~ justify-center">
<div>
<Label>计算模式</Label>
<Select v-model="selectedMode">
<SelectTrigger class="w-[180px]">
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>模式</SelectLabel>
<SelectItem v-for="mode in modes" :key="mode" :value="mode">
{{ mode }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<NumberField
id="start"
v-model:model-value="startYear" :format-options="{
useGrouping: false,
}" :default-value="2017" :min="2017" :max="2024"
>
<Label for="start">起始年</Label>
<NumberFieldContent>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
<NumberField
id="end"
v-model:model-value="endYear" :format-options="{
style: 'decimal',
notation: 'standard',
useGrouping: false,
}" :default-value="2017" :min="2017" :max="2024"
>
<Label for="end">终止年</Label>
<NumberFieldContent>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
</div>
</DenseFramework>
</template>
<style scoped>
</style>

View File

@ -1,147 +0,0 @@
<route lang='json'>
{
"meta":{
"title":"COSMIC单次",
"description":"COSMIC重力波单次",
"group":"COSMIC",
"item_name":"重力波提取"
}
}
</route>
<script setup lang="ts">
import {
CalendarDate,
createCalendar,
DateFormatter,
type DateValue,
getLocalTimeZone,
toCalendar,
} from '@internationalized/date'
import { Calendar as CalendarIcon } from 'lucide-vue-next'
import { ref } from 'vue'
import { cn } from '~/lib/utils'
function getDayOfYear(date: Date) {
const start = new Date(date.getFullYear(), 0, 0)
// @ts-expect-error date - start
const diff = date - start
const oneDay = 1000 * 60 * 60 * 24
return Math.floor(diff / oneDay)
}
const df = new DateFormatter('zh-CN', {
dateStyle: 'long',
})
const lat_ranges = [
'-60 ~ -50',
'-50 ~ -40',
'-40 ~ -30',
'-30 ~ -20',
'-20 ~ -10',
'-10 ~ 0',
'0 ~ 10',
'10 ~ 20',
'20 ~ 30',
'30 ~ 40',
'40 ~ 50',
'50 ~ 60',
]
const selected = reactive({
year: '2008',
day: '1',
mode: 'mean_ktemp_Nz',
lat_range: '0 ~ 10',
})
const value = ref<DateValue>(
)
onMounted(() => {
value.value = toCalendar(new CalendarDate(2008, 1, 1), createCalendar('zh-CN'))
})
const queryUrl = computed(() => {
const query = new URLSearchParams()
query.set('year', selected.year)
const theDate = value.value?.toDate(getLocalTimeZone()) ?? new Date(2008, 1, 1)
const day_No = getDayOfYear(theDate)
query.set('day', day_No.toString())
query.set('mode', selected.mode)
query.set('lat_range', selected.lat_range)
return `/cosmic/render/gravity_wave/perday?${query}`
})
</script>
<template>
<DenseFramework :image-query="queryUrl">
<Label>统计参数</Label>
<Tabs v-model:model-value="selected.mode">
<TabsList class="grid grid-cols-1 w-full">
<TabsTrigger value="mean_ktemp_Nz">
平均浮力频率随高度变化
</TabsTrigger>
<TabsTrigger value="mean_ktemp_Ptz">
平均势能随高度变化
</TabsTrigger>
<TabsTrigger value="residual_mean">
残差温度值曲线图
</TabsTrigger>
<TabsTrigger value="background_temp">
背景温度曲线图
</TabsTrigger>
</TabsList>
</Tabs>
<Label>年份</Label>
<Select v-model="selected.year">
<SelectTrigger>
<SelectValue placeholder="Select a year" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>年份</SelectLabel>
<SelectItem value="2008">
2008
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label>日期</Label>
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
:class="cn(
'w-full justify-start text-left font-normal',
!value && 'text-muted-foreground',
)"
>
<CalendarIcon class="mr-2 h-4 w-4" />
{{ value ? df.format(value.toDate(getLocalTimeZone())) : "选择日期" }}
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<!-- @ts-expect-error dsa -->
<Calendar v-model="value " initial-focus />
</PopoverContent>
</Popover>
<Label>纬度带°</Label>
<Select v-model="selected.lat_range">
<SelectTrigger>
<SelectValue placeholder="选择纬度带" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>纬度带°</SelectLabel>
<SelectItem v-for="lat_range in lat_ranges" :key="lat_range" :value="lat_range">
{{ lat_range }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</DenseFramework>
</template>
<style scoped>
</style>

View File

@ -1,100 +0,0 @@
<route lang="yaml">
meta:
title: Cosmic 重力波多日
description: Cosmic 重力波多日
group: COSMIC
item_name: 重力波统计
</route>
<script setup lang="ts">
/**
if mode == "布伦特-维萨拉频率分布":
await run_sync(p.plot_heatmap_tempNz)()
elif mode == "不同高度下的逐日统计分析":
await run_sync(p.plot_heatmap_tempPtz)()
elif mode == "每月浮力频率变化趋势":
await run_sync(p.plot_floatage_trend)()
elif mode == "每月平均重力势能的折线图":
await run_sync(p.plot_monthly_energy)()
*/
const MODES = [
'不同高度下的逐日统计分析',
// '',
'每月平均重力势能的折线图',
'浮力频率均值',
'每月平均N^2的折线图',
]
const lat_ranges = [
'-60 ~ -50',
'-50 ~ -40',
'-40 ~ -30',
'-30 ~ -20',
'-20 ~ -10',
'-10 ~ 0',
'0 ~ 10',
'10 ~ 20',
'20 ~ 30',
'30 ~ 40',
'40 ~ 50',
'50 ~ 60',
]
const selected = reactive({
year: '2008',
// begin_day
mode: '不同高度下的逐日统计分析',
lat_range: '0 ~ 10',
})
const queryUrl = computed(() => {
const q = new URLSearchParams()
q.set('year', selected.year)
q.set('mode', selected.mode)
q.set('lat_range', selected.lat_range)
return `/cosmic/render/gravity_wave/multiday?${q}`
})
</script>
<template>
<DenseFramework :image-query="queryUrl">
<Label>年份</Label>
<Select v-model="selected.year">
<SelectTrigger>
<SelectValue placeholder="选择年份" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>年份</SelectLabel>
<SelectItem v-for="year in ['2008']" :key="year" :value="year">
{{ year }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label>统计参数</Label>
<Tabs v-model="selected.mode">
<TabsList class="grid grid-cols-1 w-full">
<TabsTrigger v-for="mode in MODES" :key="mode" :value="mode">
{{ mode }}
</TabsTrigger>
</TabsList>
</Tabs>
<Label>纬度带°</Label>
<Select v-model="selected.lat_range">
<SelectTrigger>
<SelectValue placeholder="选择纬度带" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>纬度带°</SelectLabel>
<SelectItem v-for="lat_range in lat_ranges" :key="lat_range" :value="lat_range">
{{ lat_range }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</DenseFramework>
</template>
<style scoped>
</style>

View File

@ -1,170 +0,0 @@
<route lang="json">
{"meta":{
"title":"COSMIC",
"description":"行星波提取",
"group":"COSMIC",
"item_name":"行星波提取"
}}
</route>
<script setup lang="ts">
import {
CalendarDate,
createCalendar,
DateFormatter,
type DateValue,
getLocalTimeZone,
toCalendar,
} from '@internationalized/date'
import DenseFramework from '~/components/DenseFramework.vue'
import { cn } from '~/lib/utils'
const selectedT = ref<'5' | '10' | '16'>('5')
const selectedH = ref(50)
const selectedRange = ref('-30')
const selectedYear = ref('2008')
const ranges = ['-30', '-60', '30', '60']
const df = new DateFormatter('zh-CN', {
dateStyle: 'long',
})
const ca = createCalendar('zh-CN')
const targetDate = ref<DateValue>()
function getDayOfYear(date: Date) {
const start = new Date(date.getFullYear(), 0, 0)
// @ts-expect-error date - start
const diff = date - start
const oneDay = 1000 * 60 * 60 * 24
return Math.floor(diff / oneDay)
}
watch(selectedYear, () => {
targetDate.value = toCalendar(
new CalendarDate(Number.parseInt(selectedYear.value), 1, 1),
ca,
).add({ days: Math.floor(Number.parseInt(selectedT.value) * 1.5) })
})
const minDateValue = computed(() => {
const currentYearBeginDate = new CalendarDate(Number.parseInt(selectedYear.value), 1, 1)
return toCalendar(currentYearBeginDate, ca).add({ days: Math.floor(Number.parseInt(selectedT.value) * 1.5) })
})
const maxDateValue = computed(() => {
const currentYearBeginDate = new CalendarDate(Number.parseInt(selectedYear.value) + 1, 1, 1)
return toCalendar(currentYearBeginDate, ca).subtract({ days: Math.floor(Number.parseInt(selectedT.value) * 1.5) })
})
onMounted(() => {
targetDate.value = toCalendar(
new CalendarDate(Number.parseInt(selectedYear.value), 1, 1),
ca,
).add({ days: Math.floor(Number.parseInt(selectedT.value) * 1.5) })
})
const fetchUrl = computed(() => {
const query = new URLSearchParams()
if (!targetDate.value) {
return ''
}
const targetDay = getDayOfYear(targetDate.value.toDate(getLocalTimeZone()))
const startDay = targetDay - Math.floor(1.5 * Number.parseInt(selectedT.value))
query.set('T', selectedT.value)
query.set('target_h', selectedH.value.toString())
query.set('target_lat', selectedRange.value)
query.set('year', selectedYear.value)
query.set('start_day', startDay.toString())
return `/cosmic/render/planet_wave/perday?${query}`
})
</script>
<template>
<DenseFramework :image-query="fetchUrl">
<Label>年份</Label>
<Select v-model="selectedYear">
<SelectTrigger>
<SelectValue placeholder="选择年份" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>年份</SelectLabel>
<SelectItem v-for="year in ['2008']" :key="year" :value="year">
{{ year }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label for="T_range">行星波周期</Label>
<Tabs id="T_range" v-model="selectedT" default-value="5">
<TabsList class="grid grid-cols-3 w-full">
<TabsTrigger value="5">
5
</TabsTrigger>
<TabsTrigger value="10">
10
</TabsTrigger>
<TabsTrigger value="16">
16
</TabsTrigger>
</TabsList>
</Tabs>
<!-- TODO: not implemented -->
<Label>高度km</Label>
<NumberField
id="start"
v-model:model-value="selectedH" :format-options="{
useGrouping: false,
}" :default-value="60"
:step="10"
:max="60"
:min="0"
>
<NumberFieldContent>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
<Label>纬度带°</Label>
<Select v-model="selectedRange">
<SelectTrigger>
<SelectValue placeholder="选择纬度带" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>纬度带°</SelectLabel>
<SelectItem v-for="range in ranges" :key="range" :value="range">
{{ range }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label>日期</Label>
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
:class="cn(
'w-full justify-start text-left font-normal',
!targetDate && 'text-muted-foreground',
)"
>
<span i-md-calendar />
{{ targetDate ? df.format(targetDate.toDate(getLocalTimeZone())) : "选择日期" }}
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<!-- @ts-expect-error dsa -->
<Calendar
v-model="targetDate " initial-focus
:is-date-disabled="(date:DateValue) => date < minDateValue || date > maxDateValue"
:prevent-deselect="true"
/>
</PopoverContent>
</Popover>
<div>
拟合起始日期 {{ targetDate?.subtract({ days: Math.floor(1.5 * Number.parseInt(selectedT)) }) }}
<br>
拟合结束日期 {{ targetDate?.add({ days: Math.floor(1.5 * Number.parseInt(selectedT)) }) }}
</div>
</DenseFramework>
</template>

View File

@ -1,104 +0,0 @@
<route lang="json">
{"meta":{
"title":"COSMIC",
"description":"行星波统计",
"group":"COSMIC",
"item_name":"行星波统计"
}}
</route>
<script setup lang="ts">
import DenseFramework from '~/components/DenseFramework.vue'
const selectedT = ref<'5' | '10' | '16'>('5')
const selectedH = ref(50)
const selectedRange = ref('-30')
const selectedK = ref(0)
const selectedYear = ref('2008')
const ranges = ['-30', '-60', '30', '60']
const fetchUrl = computed(() => {
const query = new URLSearchParams()
query.set('T', selectedT.value)
query.set('target_h', selectedH.value.toString())
query.set('target_lat', selectedRange.value)
query.set('k', selectedK.value.toString())
return `/cosmic/render/planet_wave/daily?${query}`
})
</script>
<template>
<DenseFramework :image-query="fetchUrl">
<Label>年份</Label>
<Select v-model="selectedYear">
<SelectTrigger>
<SelectValue placeholder="选择年份" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>年份</SelectLabel>
<SelectItem v-for="year in ['2008']" :key="year" :value="year">
{{ year }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label for="T_range">行星波周期</Label>
<Tabs id="T_range" v-model="selectedT" default-value="5">
<TabsList class="grid grid-cols-3 w-full">
<TabsTrigger value="5">
5
</TabsTrigger>
<TabsTrigger value="10">
10
</TabsTrigger>
<TabsTrigger value="16">
16
</TabsTrigger>
</TabsList>
</Tabs>
<!-- TODO: not implemented -->
<Label>高度km</Label>
<NumberField
id="start"
v-model:model-value="selectedH" :format-options="{
useGrouping: false,
}" :default-value="50"
:step="10"
:max="60"
:min="0"
>
<NumberFieldContent>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
<Label>纬度带°</Label>
<Select v-model="selectedRange">
<SelectTrigger>
<SelectValue placeholder="选择纬度带" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>纬度带°</SelectLabel>
<SelectItem v-for="range in ranges" :key="range" :value="range">
{{ range }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label>波数 k</Label>
<NumberField
v-model="selectedK"
:default-value="0"
:max="4" :min="-4" :step="1"
>
<NumberFieldContent>
<NumberFieldIncrement />
<NumberFieldInput />
<NumberFieldDecrement />
</NumberFieldContent>
</NumberField>
</DenseFramework>
</template>

View File

@ -1,14 +0,0 @@
<script setup lang="ts">
</script>
<template>
<div class="grid grid-cols-3">
<pre>
{{ JSON.stringify($router.getRoutes(), null, 4) }}
</pre>
</div>
</template>
<style scoped>
</style>

View File

@ -1,24 +0,0 @@
<route lang="json">
{
"meta":{
"title":"流星雷达 行星波单次",
"description":"行星波提取",
"group":"流星雷达",
"item_name":"行星波提取"
}
}
</route>
<script setup lang="ts">
const url = ref('')
const mode = '行星波' as const
</script>
<template>
<RadarSingle is-day :model-value="url" :wave-type="mode" />
</template>
<style scoped>
</style>

View File

@ -1,14 +0,0 @@
<route lang="json">
{
"meta": {
"title": "流星雷达",
"description": "行星波统计",
"group": "流星雷达",
"item_name": "行星波统计"
}
}
</route>
<template>
<RadarSingle wave-type="行星波" />
</template>

View File

@ -1,24 +0,0 @@
<route lang="json">
{
"meta":{
"title":"潮汐波单次",
"description":"潮汐波提取",
"group":"流星雷达",
"item_name":"潮汐波提取"
}
}
</route>
<script setup lang="ts">
const url = ref('')
const mode = '潮汐波' as const
</script>
<template>
<RadarSingle :model-value="url" :wave-type="mode" :is-day="true" />
</template>
<style scoped>
</style>

View File

@ -1,14 +0,0 @@
<route lang="json">
{
"meta": {
"title": "流星雷达",
"description": "潮汐波统计",
"group": "流星雷达",
"item_name": "潮汐波统计"
}
}
</route>
<template>
<RadarSingle wave-type="潮汐波" />
</template>

View File

@ -1,4 +1,4 @@
<!-- <route lang="json">
<route lang="json">
{
"meta": {
"title": "流星雷达-热力图",
@ -7,19 +7,28 @@
"item_name": "热力图"
}
}
</route> -->
</route>
<script setup lang="ts">
const selectedMode = ref('2日行星波')
import type { ImageResult } from '~/components/ImageContainer.vue'
import { API_BASE_URL } from '~/CONSTANT'
const selectedMode = ref('潮汐波')
const selectedWave = ref('潮汐波')
const selectedDateType = ref('day')
const selectedStation = ref('武汉左岭镇站')
const selectedYear = ref('2017')
const selectedWindType = ref('uwind')
const selectedH = ref(94000)
const selectedH = ref(90000)
const selectedDate = ref('20170316')
const selectedMonth = ref('1')
const imageResult = reactive<ImageResult>({
result: 'idle',
imageUrl: '',
message: '请你选择一个模式和日期',
})
const modes = [
'2日行星波',
'5日行星波',
@ -46,7 +55,6 @@ const queryUrl = computed(() => {
query.set('model_name', mode)
query.set('wind_type', windType)
query.set('H', selectedH.value.toString())
query.set('mode', selectedDateType.value)
if (selectedDateType.value === 'day') {
const queryDay = `${selectedDate.value.slice(0, 4)}-${selectedDate.value.slice(4, 6)}-${selectedDate.value.slice(6, 8)}`
query.set('day', queryDay)
@ -55,17 +63,32 @@ const queryUrl = computed(() => {
query.set('month', selectedMonth.value)
}
// const query = `?station=${station}&year=${year}&model_name=${mode}&wind_type=${windType}&H=${selectedH.value}`
const path = `/radar/render/heatmap?${query}`
const path = `${API_BASE_URL}/radar/render/heatmap?${query}`
return path
})
const { onFetchResponse, isFetching, execute } = useFetch(queryUrl, { immediate: false })
onFetchResponse(async (resp) => {
const blob = await resp.blob()
const url = URL.createObjectURL(blob)
imageResult.result = 'success'
imageResult.imageUrl = url
})
watch(isFetching, (fetching) => {
if (fetching) {
imageResult.result = 'pending'
}
})
onMounted(async () => {
const resp = await baseFetch(`/radar/metadata`).json()
const data = await resp.data.value
const resp = await fetch(`${API_BASE_URL}/radar/metadata`)
const data = await resp.json()
// use regex to extract the year from the path,
// ./radar/data\\\\2017\\ZLT_MET01_DLL_L21_01D_20170316.txt
const station_pattern = /data\/(.*?)\//
const year_pattern = /(\d{4})\/ZLT_/
const station_pattern = /data\\(.*?)\\/
const year_pattern = /(\d{4})\\ZLT_/
const date_pattern = /01D_(\d{8})\.txt/
const pairs = data.map((source_text: string) => {
const station = source_text.match(station_pattern)?.[1]
@ -85,16 +108,10 @@ onMounted(async () => {
paths.value = data
})
watch(selectedYear, (newV) => {
selectedDate.value = Array.from(dates.value).filter(
d => d.startsWith(newV),
)[0]
})
</script>
<template>
<DenseFramework :image-query="queryUrl">
<DenseFramework :image-result="imageResult" @submit="execute">
<div>
<div flex="~ col gap-3" py-3>
<Label for="waves">波类型</Label>
@ -110,7 +127,7 @@ watch(selectedYear, (newV) => {
</Tabs>
<div v-if="selectedWave === '行星波'">
<Label>行星波类型</Label>
<Tabs v-model="selectedMode" default-value="2日行星波">
<Tabs v-model="selectedMode" default-value="uwind">
<TabsList class="grid grid-cols-4 w-full">
<TabsTrigger v-for="m in modes" :key="m" :value="m">
{{ m.replace("行星波", "") }}
@ -162,7 +179,7 @@ watch(selectedYear, (newV) => {
id="start"
v-model:model-value="selectedH" :format-options="{
useGrouping: false,
}" :default-value="94000"
}" :default-value="90000"
>
<NumberFieldContent>
<NumberFieldDecrement />

View File

@ -1,4 +1,4 @@
<!-- <route lang="json">
<route lang="json">
{
"meta": {
"title": "流星雷达-潮汐波时空变化",
@ -7,16 +7,23 @@
"item_name": "潮汐波时空变化"
}
}
</route> -->
</route>
<script setup lang="ts">
const selectedMode = ref('2日行星波')
const selectedWave = ref('潮汐波')
import type { ImageResult } from '~/components/ImageContainer.vue'
import { API_BASE_URL } from '~/CONSTANT'
const selectedMode = ref('潮汐波')
const selectedStation = ref('武汉左岭镇站')
const selectedYear = ref('2017')
const selectedMonthRange = reactive({ start: '1', end: '12' })
const imageResult = reactive<ImageResult>({
result: 'idle',
imageUrl: '',
message: '请你选择一个模式和日期',
})
const modes = [
'潮汐波',
'2日行星波',
'5日行星波',
'10日行星波',
@ -28,24 +35,36 @@ const stations = ref<Set<string>>(new Set())
const years = ref<Set<string>>(new Set())
const queryUrl = computed(() => {
// const query = `?station=${station}&year=${year}&model_name=${mode}`
const query = new URLSearchParams()
query.set('station', selectedStation.value)
query.set('year', selectedYear.value)
query.set('model_name', selectedWave.value === '潮汐波' ? '潮汐波' : selectedMode.value)
query.set('start_month', selectedMonthRange.start)
query.set('end_month', selectedMonthRange.end)
const path = `/radar/render/changes?${query}`
const station = encodeURIComponent(selectedStation.value)
const year = encodeURIComponent(selectedYear.value)
const mode = encodeURIComponent(selectedMode.value)
const query = `?station=${station}&year=${year}&model_name=${mode}`
const path = `${API_BASE_URL}/radar/render/v2${query}`
return path
})
const { onFetchResponse, isFetching, execute } = useFetch(queryUrl, { refetch: true })
onFetchResponse(async (resp) => {
const blob = await resp.blob()
const url = URL.createObjectURL(blob)
imageResult.result = 'success'
imageResult.imageUrl = url
})
watch(isFetching, (fetching) => {
if (fetching) {
imageResult.result = 'pending'
}
})
onMounted(async () => {
const resp = await baseFetch(`/radar/metadata`).json()
const data = await resp.data.value!
const resp = await fetch(`${API_BASE_URL}/radar/metadata`)
const data = await resp.json()
// use regex to extract the year from the path,
// ./radar/data\\\\2017\\ZLT_MET01_DLL_L21_01D_20170316.txt
const station_pattern = /data\/(.*?)\//
const year_pattern = /(\d{4})\/ZLT_/
const station_pattern = /data\\(.*?)\\/
const year_pattern = /(\d{4})\\ZLT_/
const pairs = data.map((source_text: string) => {
const station = source_text.match(station_pattern)?.[1]
const year = source_text.match(year_pattern)?.[1]
@ -64,31 +83,22 @@ onMounted(async () => {
</script>
<template>
<DenseFramework :image-query="queryUrl">
<DenseFramework :image-result="imageResult" @submit="execute">
<div>
<div flex="~ col items-stretch gap-3" py-3>
<Label>计算模式</Label>
<Tabs id="waves" v-model="selectedWave" default-value="潮汐波">
<TabsList class="grid grid-cols-2 w-full">
<TabsTrigger value="潮汐波">
潮汐波
</TabsTrigger>
<TabsTrigger value="行星波">
行星波
</TabsTrigger>
</TabsList>
</Tabs>
<div v-if="selectedWave === '行星波'">
<Label>行星波类型</Label>
<Tabs v-model="selectedMode" default-value="uwind">
<TabsList class="grid grid-cols-4 w-full">
<TabsTrigger v-for="m in modes" :key="m" :value="m">
{{ m.replace("行星波", "") }}
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<Label>观测站</Label>
<Select v-model="selectedMode">
<SelectTrigger>
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>计算模式</SelectLabel>
<SelectItem v-for="mode in modes" :key="mode" :value="mode">
{{ mode }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Select v-model="selectedStation">
<SelectTrigger>
<SelectValue placeholder="Select a fruit" />
@ -102,56 +112,19 @@ onMounted(async () => {
</SelectGroup>
</SelectContent>
</Select>
<Label>年份</Label>
<Select v-model="selectedYear">
<SelectTrigger>
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
<SelectLabel></SelectLabel>
<SelectItem v-for="year in years" :key="year" :value="year">
{{ year }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label>起始月份</Label>
<Select v-model="selectedMonthRange.start">
<SelectTrigger>
<SelectValue placeholder="选择起始月" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
<SelectItem
v-for="month in [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
]" :key="month" :value="month.toString()"
>
{{ month }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label>终止月份</Label>
<Select v-model="selectedMonthRange.end">
<SelectTrigger>
<SelectValue placeholder="选择终止月" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
<SelectItem
v-for="month in [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
]" :key="month" :value="month.toString()"
>
{{ month }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
</DenseFramework>

View File

@ -0,0 +1,121 @@
<route lang="json">
{
"meta":{
"title":"Saber日周期功率波图",
"description":"Saber日周期功率波图",
"group":"Saber",
"item_name":"日周期功率波图"
}
}
</route>
<script setup lang="ts">
import type { ImageResult } from '~/components/ImageContainer.vue'
import { API_BASE_URL } from '~/CONSTANT'
import { currentSaberDays, refreshCurrentSaberDays, refreshPath, saberPaths } from './utils'
const selected = reactive({
path: '',
day: '',
cycle_no: 1,
})
const imageResult = reactive<ImageResult>({
result: 'idle',
imageUrl: '',
message: '请你选择一个模式和日期',
})
const urll = computed(() => {
const path = encodeURIComponent(selected.path)
const day = encodeURIComponent(selected.day)
const query = `path=${path}&day=${day}&cycle_no=${selected.cycle_no}`
return `${API_BASE_URL}/saber/render/day_cycle_power_wave_plot?${query}`
})
const { onFetchResponse, isFetching, execute } = useFetch(
urll,
{ immediate: false },
)
watch(isFetching, (fetching) => {
if (fetching) {
imageResult.result = 'pending'
}
})
onFetchResponse(async (resp) => {
const blob = await resp.blob()
const url = URL.createObjectURL(blob)
imageResult.result = 'success'
imageResult.imageUrl = url
})
onMounted(() => {
refreshPath()
selected.path = saberPaths.value[0]
})
watch(() => selected.path, () => {
refreshCurrentSaberDays(selected.path)
if (selected.day === '') {
selected.day = currentSaberDays.value[0]
}
})
function renderPath(path: string) {
const yearPattern = /\/data\\(\d{4})/
const year = path.match(yearPattern)?.[1]
const monthPattern = /Temp_O3_(.*)(\d{4})/
const month = path.match(monthPattern)?.[1]
return `${year}${month}`
}
</script>
<template>
<DenseFramework :image-result="imageResult" @submit="execute">
<div>
<div flex="~ col items-stretch gap-3" py-3>
<Label for="day">年月</Label>
<Select v-model="selected.path">
<SelectTrigger>
<SelectValue placeholder="选择日期" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>月份</SelectLabel>
<SelectItem v-for="path in saberPaths" :key="path" :value="path">
{{ renderPath(path) }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label for="day">天数</Label>
<Select id="day" v-model="selected.day">
<SelectTrigger>
<SelectValue placeholder="选择日期" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Day</SelectLabel>
<SelectItem v-for="day in currentSaberDays" :key="day" :value="day">
{{ day }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label for="age">Cycle No.</Label>
<NumberField id="age" v-model:model-value="selected.cycle_no" :default-value="1" :min="0">
<NumberFieldContent>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
</div>
</div>
</DenseFramework>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,121 @@
<route lang="json">
{
"meta": {
"title": "Saber日周期功率波图",
"description": "Saber日周期功率波图",
"group": "Saber",
"item_name": "日周期功率波图"
}
}
</route>
<script setup lang="ts">
import type { ImageResult } from '~/components/ImageContainer.vue'
import { API_BASE_URL } from '~/CONSTANT'
import { currentSaberDays, refreshCurrentSaberDays, refreshPath, saberPaths } from './utils'
const selected = reactive({
path: './saber/data\\2012\\SABER_Temp_O3_April2012_v2.0.nc',
day: '',
cycle_no: 1,
})
const imageResult = reactive<ImageResult>({
result: 'idle',
imageUrl: '',
message: '请你选择一个模式和日期',
})
const urll = computed(() => {
const path = encodeURIComponent(selected.path)
const day = encodeURIComponent(selected.day)
const query = `path=${path}&day=${day}&cycle_no=${selected.cycle_no}`
return `${API_BASE_URL}/saber/render/day_fft_ifft_plot?${query}`
})
const { onFetchResponse, isFetching, execute } = useFetch(
urll,
{ immediate: false },
)
watch(isFetching, (fetching) => {
if (fetching) {
imageResult.result = 'pending'
}
})
onFetchResponse(async (resp) => {
const blob = await resp.blob()
const url = URL.createObjectURL(blob)
imageResult.result = 'success'
imageResult.imageUrl = url
})
onMounted(() => {
refreshPath()
selected.path = saberPaths.value[0]
})
watch(() => selected.path, () => {
refreshCurrentSaberDays(selected.path)
if (selected.day === '') {
selected.day = currentSaberDays.value[0]
}
})
function renderPath(path: string) {
const yearPattern = /\/data\\(\d{4})/
const year = path.match(yearPattern)?.[1]
const monthPattern = /Temp_O3_(.*)(\d{4})/
const month = path.match(monthPattern)?.[1]
return `${year}${month}`
}
</script>
<template>
<DenseFramework :image-result="imageResult" @submit="execute">
<div>
<div flex="~ col items-stretch gap-3" py-3>
<Label for="day">年月</Label>
<Select v-model="selected.path">
<SelectTrigger>
<SelectValue placeholder="选择日期" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>月份</SelectLabel>
<SelectItem v-for="path in saberPaths" :key="path" :value="path">
{{ renderPath(path) }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label for="day">天数</Label>
<Select id="day" v-model="selected.day">
<SelectTrigger>
<SelectValue placeholder="选择日期" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Day</SelectLabel>
<SelectItem v-for="day in currentSaberDays" :key="day" :value="day">
{{ day }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label for="age">Cycle No.</Label>
<NumberField id="age" v-model:model-value="selected.cycle_no" :default-value="1" :min="0">
<NumberFieldContent>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
</div>
</div>
</DenseFramework>
</template>
<style scoped>
</style>

View File

@ -1,48 +0,0 @@
<route lang="yaml">
meta:
title: Saber 重力波单日
description: Saber 重力波单日
group: Saber
item_name: 重力波提取
</route>
<script setup lang="ts">
import Day_cycle_power_wave_plot from '~/components/dense/saber/day_cycle_power_wave_plot.vue'
import Day_fft_ifft_plot from '~/components/dense/saber/day_fft_ifft_plot.vue'
import Plot_wave_fitting from '~/components/dense/saber/plot_wave_fitting.vue'
const component_mapping = {
day_cycle_power_wave_plot: Day_cycle_power_wave_plot,
day_fft_ifft_plot: Day_fft_ifft_plot,
plot_wave_fitting: Plot_wave_fitting,
} as const
const currentMode = ref<keyof typeof component_mapping>('day_cycle_power_wave_plot')
const currentComponent = computed(() => {
return component_mapping[currentMode.value]
})
</script>
<template>
<component :is="currentComponent">
<div>
<Label>选择模式</Label>
<Tabs v-model="currentMode" class="w-full" default-value="day_cycle_power_wave_plot">
<TabsList class="grid grid-cols-1 w-full">
<TabsTrigger v-for="key in Object.keys(component_mapping)" :key="key" class="w-full" :value="key">
{{ {
day_cycle_power_wave_plot: '平均浮力频率和平均势能随高度变化',
day_fft_ifft_plot: '傅里叶变化滤波处理',
plot_wave_fitting: '逐日波形拟合',
}[key] }}
</TabsTrigger>
</TabsList>
</Tabs>
</div>
</component>
</template>
<style scoped>
</style>

View File

@ -1,11 +0,0 @@
<route lang="yaml">
meta:
title: Saber 重力波月统计
description: Saber 重力波月统计
group: Saber
item_name: 重力波统计
</route>
<template>
<Month_year_power_wave_plot />
</template>

View File

@ -0,0 +1,89 @@
<route lang="json">
{
"meta": {
"title": "Saber月周期功率波图",
"description": "Saber月周期功率波图",
"group": "Saber",
"item_name": "月周期功率波图"
}
}
</route>
<script setup lang="ts">
import type { ImageResult } from '~/components/ImageContainer.vue'
import { API_BASE_URL } from '~/CONSTANT'
import { refreshPath, saberPaths } from './utils'
const selected = reactive({
path: '',
})
const imageResult = reactive<ImageResult>({
result: 'idle',
imageUrl: '',
message: '请你选择一个模式和日期',
})
const urll = computed(() => {
const path = encodeURIComponent(selected.path)
const query = `path=${path}`
return `${API_BASE_URL}/saber/render/month_power_wave_plot?${query}`
})
const { onFetchResponse, execute, isFetching } = useFetch(
urll,
{ immediate: false },
)
watch(isFetching, (fetching) => {
if (fetching) {
imageResult.result = 'pending'
}
})
onFetchResponse(async (resp) => {
const blob = await resp.blob()
const url = URL.createObjectURL(blob)
imageResult.result = 'success'
imageResult.imageUrl = url
})
onMounted(() => {
refreshPath()
selected.path = saberPaths.value[0]
})
function renderPath(path: string) {
const yearPattern = /\/data\\(\d{4})/
const year = path.match(yearPattern)?.[1]
const monthPattern = /Temp_O3_(.*)(\d{4})/
const month = path.match(monthPattern)?.[1]
return `${year}${month}`
}
</script>
<template>
<DenseFramework :image-result="imageResult" @submit="execute">
<div>
<div flex="~ row items-center gap-3" py-3>
<Label for="day">年月</Label>
<Select v-model="selected.path">
<SelectTrigger>
<SelectValue placeholder="选择日期" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>月份</SelectLabel>
<SelectItem v-for="path in saberPaths" :key="path" :value="path">
{{ renderPath(path) }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
</DenseFramework>
</template>
<style scoped>
</style>

View File

@ -1,188 +0,0 @@
<route lang="json">
{
"meta":{
"title":"Saber 行星波月统计",
"description":"Saber 行星波月统计",
"group":"Saber",
"item_name":"行星波提取"
}
}
</route>
<script setup lang="ts">
import {
CalendarDate,
createCalendar,
DateFormatter,
type DateValue,
getLocalTimeZone,
toCalendar,
} from '@internationalized/date'
import { cn } from '~/lib/utils'
const df = new DateFormatter('zh-CN', {
dateStyle: 'long',
})
const ca = createCalendar('zh-CN')
const selected = reactive({
year: '2018',
T: '16',
k: 0,
H: 90,
range: '60',
})
const targetDate = ref<DateValue>()
function getDayOfYear(date: Date) {
const start = new Date(date.getFullYear(), 0, 0)
// @ts-expect-error date - start
const diff = date - start
const oneDay = 1000 * 60 * 60 * 24
return Math.floor(diff / oneDay)
}
watch(() => selected.year, () => {
targetDate.value = toCalendar(
new CalendarDate(Number.parseInt(selected.year), 1, 1),
ca,
).add({ days: Number.parseInt(selected.T) * 1.5 })
})
const minDateValue = computed(() => {
const currentYearBeginDate = new CalendarDate(Number.parseInt(selected.year), 1, 1)
return toCalendar(currentYearBeginDate, ca).add({ days: Math.floor(Number.parseInt(selected.T) * 1.5) })
})
const maxDateValue = computed(() => {
const currentYearBeginDate = new CalendarDate(Number.parseInt(selected.year) + 1, 1, 1)
return toCalendar(currentYearBeginDate, ca).subtract({ days: Math.floor(Number.parseInt(selected.T) * 1.5) })
})
const ranges = [
'-30',
'30',
'-60',
'60',
]
onMounted(() => {
targetDate.value = toCalendar(
new CalendarDate(Number.parseInt(selected.year), 1, 1),
ca,
).add({ days: Math.floor(Number.parseInt(selected.T) * 1.5) })
})
const queryUrl = computed(() => {
const q = new URLSearchParams()
if (!targetDate.value) {
return ''
}
const targetDay = getDayOfYear(targetDate.value.toDate(getLocalTimeZone()))
const startDay = targetDay - Math.floor(1.5 * Number.parseInt(selected.T))
q.set('year', selected.year)
q.set('T', selected.T.toString())
q.set('k', selected.k.toString())
q.set('H', selected.H.toString())
q.set('lat_target', selected.range)
q.set('start_day', startDay.toString())
return `/saber/render/planet_wave/per_day?${q}`
})
</script>
<template>
<DenseFramework :image-query="queryUrl">
<Label>年份</Label>
<Select v-model="selected.year">
<SelectTrigger>
<SelectValue placeholder="选择年份" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>年份</SelectLabel>
<SelectItem v-for="year in ['2018', '2015']" :key="year" :value="year">
{{ year }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label>行星波周期</Label>
<!-- can be 510,16 -->
<!-- use Tabs -->
<Tabs v-model="selected.T" default-value="16">
<TabsList class="grid grid-cols-3 w-full">
<TabsTrigger value="5">
5
</TabsTrigger>
<TabsTrigger value="10">
10
</TabsTrigger>
<TabsTrigger value="16">
16
</TabsTrigger>
</TabsList>
</Tabs>
<Label>高度km</Label>
<NumberField
id="start"
v-model:model-value="selected.H" :format-options="{
useGrouping: false,
}" :default-value="94"
:step="20"
:max="110"
:min="30"
>
<NumberFieldContent>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
<Label>纬度带°</Label>
<Select v-model="selected.range">
<SelectTrigger>
<SelectValue placeholder="选择纬度带" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>纬度带°</SelectLabel>
<SelectItem v-for="range in ranges" :key="range" :value="range">
{{ range }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label>日期</Label>
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
:class="cn(
'w-full justify-start text-left font-normal',
!targetDate && 'text-muted-foreground',
)"
>
<span i-md-calendar />
{{ targetDate ? df.format(targetDate.toDate(getLocalTimeZone())) : "选择日期" }}
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<!-- @ts-expect-error dsa -->
<Calendar
v-model="targetDate " initial-focus
:is-date-disabled="(date:DateValue) => date < minDateValue || date > maxDateValue"
:prevent-deselect="true"
/>
</PopoverContent>
</Popover>
<div>
拟合起始日期 {{ targetDate?.subtract({ days: Math.floor(1.5 * Number.parseInt(selected.T)) }) }}
<br>
拟合结束日期 {{ targetDate?.add({ days: Math.floor(1.5 * Number.parseInt(selected.T)) }) }}
</div>
</DenseFramework>
</template>
<style scoped>
</style>

View File

@ -1,118 +0,0 @@
<route lang="json">
{
"meta":{
"title":"Saber 行星波月统计",
"description":"Saber 行星波月统计",
"group":"Saber",
"item_name":"行星波统计"
}
}
</route>
<script setup lang="ts">
const selected = reactive({
year: '2018',
T: '16',
k: 0,
H: 90,
range: '60',
})
const ranges = [
'-30',
'30',
'-60',
'60',
]
const queryUrl = computed(() => {
const q = new URLSearchParams()
q.set('year', selected.year)
q.set('T', selected.T.toString())
q.set('k', selected.k.toString())
q.set('H', selected.H.toString())
q.set('lat_target', selected.range)
return `/saber/render/planet_wave/per_year/energy_plot?${q}`
})
</script>
<template>
<DenseFramework :image-query="queryUrl">
<Label>年份</Label>
<Select v-model="selected.year">
<SelectTrigger>
<SelectValue placeholder="选择年份" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>年份</SelectLabel>
<SelectItem v-for="year in ['2018', '2015']" :key="year" :value="year">
{{ year }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label>行星波周期</Label>
<!-- can be 510,16 -->
<!-- use Tabs -->
<Tabs v-model="selected.T" default-value="16">
<TabsList class="grid grid-cols-3 w-full">
<TabsTrigger value="5">
5
</TabsTrigger>
<TabsTrigger value="10">
10
</TabsTrigger>
<TabsTrigger value="16">
16
</TabsTrigger>
</TabsList>
</Tabs>
<Label>高度km</Label>
<NumberField
id="start"
v-model:model-value="selected.H" :format-options="{
useGrouping: false,
}" :default-value="94"
:step="20"
:max="110"
:min="30"
>
<NumberFieldContent>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
<Label>纬度带°</Label>
<Select v-model="selected.range">
<SelectTrigger>
<SelectValue placeholder="选择纬度带" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>纬度带°</SelectLabel>
<SelectItem v-for="range in ranges" :key="range" :value="range">
{{ range }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label>波数 k</Label>
<NumberField
v-model="selected.k"
:default-value="0"
:max="4" :min="-4" :step="1"
>
<NumberFieldContent>
<NumberFieldIncrement />
<NumberFieldInput />
<NumberFieldDecrement />
</NumberFieldContent>
</NumberField>
</DenseFramework>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,121 @@
<route lang="json">
{
"meta": {
"title": "Saber拟合",
"description": "Saber拟合",
"group": "Saber",
"item_name": "拟合"
}
}
</route>
<script setup lang="ts">
import type { ImageResult } from '~/components/ImageContainer.vue'
import { API_BASE_URL } from '~/CONSTANT'
import { currentSaberDays, refreshCurrentSaberDays, refreshPath, saberPaths } from './utils'
const selected = reactive({
path: './saber/data\\2012\\SABER_Temp_O3_April2012_v2.0.nc',
day: '',
height_no: 1,
})
const imageResult = reactive<ImageResult>({
result: 'idle',
imageUrl: '',
message: '请你选择一个模式和日期',
})
const urll = computed(() => {
const path = encodeURIComponent(selected.path)
const day = encodeURIComponent(selected.day)
const query = `path=${path}&day=${day}&height=${selected.height_no}`
return `${API_BASE_URL}/saber/render/plot_wave_fitting?${query}`
})
const { onFetchResponse, execute, isFetching } = useFetch(
urll,
{ immediate: false },
)
watch(isFetching, (fetching) => {
if (fetching) {
imageResult.result = 'pending'
}
})
onFetchResponse(async (resp) => {
const blob = await resp.blob()
const url = URL.createObjectURL(blob)
imageResult.result = 'success'
imageResult.imageUrl = url
})
onMounted(() => {
refreshPath()
selected.path = saberPaths.value[0]
})
watch(() => selected.path, () => {
refreshCurrentSaberDays(selected.path)
if (saberPaths.value.length > 0) {
selected.path = saberPaths.value[0]
}
})
function renderPath(path: string) {
const yearPattern = /\/data\\(\d{4})/
const year = path.match(yearPattern)?.[1]
const monthPattern = /Temp_O3_(.*)(\d{4})/
const month = path.match(monthPattern)?.[1]
return `${year}${month}`
}
</script>
<template>
<DenseFramework :image-result="imageResult" @submit="execute">
<div>
<div flex="~ col justify-stretch gap-3" py-3>
<Label for="day">年月</Label>
<Select v-model="selected.path">
<SelectTrigger>
<SelectValue placeholder="选择日期" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>月份</SelectLabel>
<SelectItem v-for="path in saberPaths" :key="path" :value="path">
{{ renderPath(path) }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label for="day">天数</Label>
<Select id="day" v-model="selected.day">
<SelectTrigger>
<SelectValue placeholder="选择日期" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Day</SelectLabel>
<SelectItem v-for="day in currentSaberDays" :key="day" :value="day">
{{ day }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label for="age">高度</Label>
<NumberField id="age" :default-value="18" :min="0">
<NumberFieldContent>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
</div>
</div>
</DenseFramework>
</template>
<style scoped>
</style>

View File

@ -1,4 +1,4 @@
import { ref } from 'vue'
import { API_BASE_URL } from '~/CONSTANT'
const saberPaths = ref<string[]>([])
const currentSaberDays = ref<string>('')
@ -6,44 +6,15 @@ const currentSaberDays = ref<string>('')
async function refreshPath() {
if (saberPaths.value.length)
return
const resp = await baseFetch<string[]>(`/saber/metadata`).json()
const data = resp.data.value
saberPaths.value = data!
const resp = await fetch(`${API_BASE_URL}/saber/metadata`)
const data = await resp.json()
saberPaths.value = data
}
async function refreshCurrentSaberDays(path: string) {
const resp = await baseFetch<string>(`/saber/metadata/list_days?path=${path}`).json()
const data = resp.data.value
currentSaberDays.value = data!
}
function renderPath(path: string) {
const yearPattern = /\/data\/(\d{4})/
const year = path.match(yearPattern)?.[1]
const monthPattern = /Temp_O3_(.*)(\d{4})/
const month_mapping = {
January: '01月',
February: '02月',
March: '03月',
April: '04月',
May: '05月',
June: '06月',
July: '07月',
August: '08月',
September: '09月',
October: '10月',
November: '11月',
December: '12月',
} as const
const month = path.match(monthPattern)?.[1] as keyof typeof month_mapping
return `${year}${month_mapping[month]}`
const resp = await fetch(`${API_BASE_URL}/saber/metadata/list_days?path=${path}`)
const data = await resp.json()
currentSaberDays.value = data
}
function parseDayOfYear(dateString: string): Date {
const year = Number.parseInt(dateString.substring(0, 4))
const dayOfYear = Number.parseInt(dateString.substring(4)) - 1 // subtract 1 because JS dates are 0-based
const date = new Date(year, 0) // Start with January 1st of the year
date.setDate(dayOfYear + 1) // Add the days
return date
}
export { currentSaberDays, parseDayOfYear, refreshCurrentSaberDays, refreshPath, renderPath, saberPaths }
export { currentSaberDays, refreshCurrentSaberDays, refreshPath, saberPaths }

View File

@ -1,92 +0,0 @@
<route lang="json">
{
"meta":{
"title":"TIDI 重力波统计",
"description":"TIDI 重力波统计",
"group":"TIDI",
"item_name":"重力波统计"
}
}
</route>
<script setup lang="ts">
const selected = reactive({
year: '2015',
mode: 'monthly_height',
lat_range: '0 ~ 20',
})
const allYears = ref([] as string[])
const lat_ranges = ['0 ~ 20']
const queryUrl = computed(() => {
const query = new URLSearchParams()
query.set('year', selected.year)
const mode = selected.mode
return `/tidi/render/gravity_wave/${mode}?${query}`
})
onMounted(async () => {
const resp = await baseFetch(`/tidi/metadata`).json()
allYears.value = Array.from(new Set(resp.data.value.path.map((a: string) => {
const year_pattern = /data\/tidi\/(\d{4})\//
return a.match(year_pattern)?.[1]
})))
})
</script>
<template>
<DenseFramework :image-query="queryUrl">
<Label>选择模式</Label>
<Tabs v-model="selected.mode" default-value="v1">
<TabsList class="grid grid-cols-1 w-full">
<TabsTrigger value="monthly_height">
重力波动能(取log)随高度变化热力图
</TabsTrigger>
<TabsTrigger value="monthly_energy">
重力波动能(取log)变化折线图
</TabsTrigger>
</TabsList>
</Tabs>
<Label>纬度带°</Label>
<Select v-model="selected.lat_range">
<SelectTrigger>
<SelectValue placeholder="选择纬度带" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>纬度带°</SelectLabel>
<SelectItem
v-for="lat_range in lat_ranges"
:key="lat_range"
:value="lat_range"
>
{{ lat_range }}
</SelectItem>
</SelectGroup>
</SelectContent>
<Label>年份</Label>
<Select v-model="selected.year">
<SelectTrigger>
<SelectValue placeholder="选择年份" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>年份</SelectLabel>
<SelectItem
v-for="y in allYears"
:key="y"
:value="y"
>
{{ y }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</select>
</DenseFramework>
</template>
<style scoped>
</style>

View File

@ -1,137 +0,0 @@
<route lang="json">
{
"meta":{
"title":"TIDI 行星波振幅",
"icon":"mdi:telescope",
"group":"TIDI",
"item_name":"行星波提取"
}
}
</route>
<script setup lang="ts">
const years = ref([] as string[])
const ranges = ['0 ~ 5', '5 ~ 10', '10 ~ 15', '15 ~ 20']
const heights = [70, 72.5, 75, 77.5, 80, 82.5, 85, 87.5, 90, 92.5, 95, 97.5, 100, 102.5, 105, 107.5, 110, 112.5, 115, 117.5, 120].map(String)
const selectedMode = ref('V_Meridional')
const selectedYear = ref('2017')
// const k = [ -4,-3,-2,-1,0,1,2,3,4]
const selectedK = ref(0)
const selectedT = ref('5')
const selectedRange = ref('0 ~ 5')
const selectedHeight = ref('85')
const queryUrl = computed(() => {
const query = new URLSearchParams()
query.set('mode', selectedMode.value)
query.set('year', selectedYear.value)
query.set('k', selectedK.value.toString())
query.set('T', selectedT.value.toString())
query.set('lat_range', selectedRange.value)
query.set('height', selectedHeight.value)
return `/tidi/render/planet_wave/daily?${query}`
})
onMounted(async () => {
const metas = await baseFetch<{ path: string [] }>(`/tidi/metadata`).json()
const _years = metas.data.value.path.map((a: string) => {
const year_pattern = /data\/tidi\/(\d{4})\//
return a.match(year_pattern)?.[1]
})
_years.sort()
years.value = Array.from(new Set(_years))
selectedYear.value = years.value[0]
})
</script>
<template>
<DenseFramework :image-query="queryUrl">
<div flex="col gap-3 ~ justify-stretch">
<Label>计算模式</Label>
<Tabs id="modes" v-model="selectedMode" default-value="V_Meridional">
<TabsList class="grid grid-cols-2 w-full">
<TabsTrigger value="V_Meridional">
纬向风
</TabsTrigger>
<TabsTrigger value="V_Zonal">
经向风
</TabsTrigger>
</TabsList>
</Tabs>
<Label>年份</Label>
<Select v-model="selectedYear">
<SelectTrigger>
<SelectValue placeholder="选择年份" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>年份</SelectLabel>
<SelectItem v-for="year in years" :key="year" :value="year">
{{ year }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label>行星波周期</Label>
<Tabs v-model:model-value="selectedT" default-value="15">
<TabsList class="grid grid-cols-3 w-full">
<TabsTrigger value="5">
5
</TabsTrigger>
<TabsTrigger value="10">
10
</TabsTrigger>
<TabsTrigger value="16">
16
</TabsTrigger>
</TabsList>
</Tabs>
<Label>高度</Label>
<Select v-model="selectedHeight">
<SelectTrigger>
<SelectValue placeholder="选择高度" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>高度</SelectLabel>
<SelectItem v-for="height in heights" :key="height" :value="height">
{{ height }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label>纬度范围</Label>
<Select v-model="selectedRange">
<SelectTrigger>
<SelectValue placeholder="选择纬度范围" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>纬度范围</SelectLabel>
<SelectItem v-for="range in ranges" :key="range" :value="range">
{{ range }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Label>波数 k</Label>
<NumberField
v-model="selectedK"
:max="4" :min="-4" :step="1"
>
<NumberFieldContent>
<NumberFieldIncrement />
<NumberFieldInput />
<NumberFieldDecrement />
</NumberFieldContent>
</NumberField>
</div>
</DenseFramework>
</template>
<style scoped>
</style>

24
typed-router.d.ts vendored
View File

@ -20,25 +20,13 @@ declare module 'vue-router/auto-routes' {
export interface RouteNamedMap {
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
'/[...all]': RouteRecordInfo<'/[...all]', '/:all(.*)', { all: ParamValue<true> }, { all: ParamValue<false> }>,
'/auth': RouteRecordInfo<'/auth', '/auth', Record<never, never>, Record<never, never>>,
'/balloon/gravity_wave/0-perday': RouteRecordInfo<'/balloon/gravity_wave/0-perday', '/balloon/gravity_wave/0-perday', Record<never, never>, Record<never, never>>,
'/balloon/gravity_wave/1-year': RouteRecordInfo<'/balloon/gravity_wave/1-year', '/balloon/gravity_wave/1-year', Record<never, never>, Record<never, never>>,
'/cosmic/gravity_wave/0-perday': RouteRecordInfo<'/cosmic/gravity_wave/0-perday', '/cosmic/gravity_wave/0-perday', Record<never, never>, Record<never, never>>,
'/cosmic/gravity_wave/1-multiday': RouteRecordInfo<'/cosmic/gravity_wave/1-multiday', '/cosmic/gravity_wave/1-multiday', Record<never, never>, Record<never, never>>,
'/cosmic/planet_wave/0-perday': RouteRecordInfo<'/cosmic/planet_wave/0-perday', '/cosmic/planet_wave/0-perday', Record<never, never>, Record<never, never>>,
'/cosmic/planet_wave/1-daily': RouteRecordInfo<'/cosmic/planet_wave/1-daily', '/cosmic/planet_wave/1-daily', Record<never, never>, Record<never, never>>,
'/debug': RouteRecordInfo<'/debug', '/debug', Record<never, never>, Record<never, never>>,
'/radar/planet_wave/0-single': RouteRecordInfo<'/radar/planet_wave/0-single', '/radar/planet_wave/0-single', Record<never, never>, Record<never, never>>,
'/radar/planet_wave/1-stats': RouteRecordInfo<'/radar/planet_wave/1-stats', '/radar/planet_wave/1-stats', Record<never, never>, Record<never, never>>,
'/radar/tidal_wave/0-single': RouteRecordInfo<'/radar/tidal_wave/0-single', '/radar/tidal_wave/0-single', Record<never, never>, Record<never, never>>,
'/radar/tidal_wave/1-stats': RouteRecordInfo<'/radar/tidal_wave/1-stats', '/radar/tidal_wave/1-stats', Record<never, never>, Record<never, never>>,
'/balloon/single': RouteRecordInfo<'/balloon/single', '/balloon/single', Record<never, never>, Record<never, never>>,
'/balloon/year': RouteRecordInfo<'/balloon/year', '/balloon/year', Record<never, never>, Record<never, never>>,
'/radar/v1': RouteRecordInfo<'/radar/v1', '/radar/v1', Record<never, never>, Record<never, never>>,
'/radar/v2': RouteRecordInfo<'/radar/v2', '/radar/v2', Record<never, never>, Record<never, never>>,
'/saber/gravity_wave/0-perday': RouteRecordInfo<'/saber/gravity_wave/0-perday', '/saber/gravity_wave/0-perday', Record<never, never>, Record<never, never>>,
'/saber/gravity_wave/1-monthly': RouteRecordInfo<'/saber/gravity_wave/1-monthly', '/saber/gravity_wave/1-monthly', Record<never, never>, Record<never, never>>,
'/saber/planet_wave/0-perday': RouteRecordInfo<'/saber/planet_wave/0-perday', '/saber/planet_wave/0-perday', Record<never, never>, Record<never, never>>,
'/saber/planet_wave/1-monthly': RouteRecordInfo<'/saber/planet_wave/1-monthly', '/saber/planet_wave/1-monthly', Record<never, never>, Record<never, never>>,
'/tidi/gravity_wave/monthly': RouteRecordInfo<'/tidi/gravity_wave/monthly', '/tidi/gravity_wave/monthly', Record<never, never>, Record<never, never>>,
'/tidi/planet_wave/daily': RouteRecordInfo<'/tidi/planet_wave/daily', '/tidi/planet_wave/daily', Record<never, never>, Record<never, never>>,
'/saber/day_cycle_power_wave_plot': RouteRecordInfo<'/saber/day_cycle_power_wave_plot', '/saber/day_cycle_power_wave_plot', Record<never, never>, Record<never, never>>,
'/saber/day_fft_ifft_plot': RouteRecordInfo<'/saber/day_fft_ifft_plot', '/saber/day_fft_ifft_plot', Record<never, never>, Record<never, never>>,
'/saber/month_power_wave_plot': RouteRecordInfo<'/saber/month_power_wave_plot', '/saber/month_power_wave_plot', Record<never, never>, Record<never, never>>,
'/saber/plot_wave_fitting': RouteRecordInfo<'/saber/plot_wave_fitting', '/saber/plot_wave_fitting', Record<never, never>, Record<never, never>>,
}
}

View File

@ -11,18 +11,6 @@ import { defineConfig } from 'vite'
import vueDevTools from 'vite-plugin-vue-devtools'
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:58620',
},
'/api/ping': {
target: 'ws://localhost:58620',
ws: true,
rewriteWsOrigin: false,
},
},
},
resolve: {
alias: {
'~/': `${path.resolve(__dirname, 'src')}/`,