Compare commits
28 Commits
dev-cosmic
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 11666081f9 | |||
| 24e686dafa | |||
| 6aeb429b3c | |||
| 7b6e55e67a | |||
| 0909d5a32c | |||
| 6f72a1a611 | |||
| 70298d740b | |||
| 3b3294d39f | |||
| 27e35c3665 | |||
| 94746d9a85 | |||
| 1e27f4ab9c | |||
| 1d4892a076 | |||
| 4fc70895fa | |||
| 1092eb3706 | |||
| 2f77ce8500 | |||
| 044a4b56e2 | |||
| 6d7d6cde37 | |||
| 023258c9b9 | |||
| c2151b929a | |||
| a0569a41d0 | |||
| 9c53ec252c | |||
| de6a6ac0a5 | |||
| 949ebf420a | |||
| 69df9232e0 | |||
| 247d7e37db | |||
| b3eeef29a7 | |||
| 52deafb19f | |||
| f80f93a9b3 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,3 +7,4 @@ dist-ssr
|
||||
node_modules
|
||||
.idea/
|
||||
*.log
|
||||
*.7z
|
||||
|
||||
9
auto-imports.d.ts
vendored
9
auto-imports.d.ts
vendored
@ -8,7 +8,9 @@ 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']
|
||||
@ -32,12 +34,14 @@ 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']
|
||||
@ -112,6 +116,7 @@ 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']
|
||||
@ -129,6 +134,7 @@ declare global {
|
||||
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useBackendOnline: typeof import('./src/composables/fetch')['useBackendOnline']
|
||||
const useBase64: typeof import('@vueuse/core')['useBase64']
|
||||
const useBattery: typeof import('@vueuse/core')['useBattery']
|
||||
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
|
||||
@ -307,7 +313,9 @@ 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']>
|
||||
@ -409,6 +417,7 @@ 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
20
components.d.ts
vendored
@ -14,6 +14,7 @@ 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']
|
||||
@ -53,8 +54,17 @@ 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']
|
||||
CoolBack: typeof import('./src/components/CoolBack.vue')['default']
|
||||
Day_cycle_power_wave_plot: typeof import('./src/components/dense/saber/day_cycle_power_wave_plot.vue')['default']
|
||||
Day_fft_ifft_plot: typeof import('./src/components/dense/saber/day_fft_ifft_plot.vue')['default']
|
||||
DefaultLayout: typeof import('./src/components/DefaultLayout.vue')['default']
|
||||
DenseFramework: typeof import('./src/components/DenseFramework.vue')['default']
|
||||
Drawer: typeof import('./src/components/ui/drawer/Drawer.vue')['default']
|
||||
DrawerContent: typeof import('./src/components/ui/drawer/DrawerContent.vue')['default']
|
||||
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']
|
||||
@ -94,6 +104,7 @@ 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']
|
||||
@ -108,11 +119,12 @@ 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']
|
||||
@ -169,14 +181,10 @@ 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']
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<meta name="description" content="Opinionated Vite Starter Template" />
|
||||
</head>
|
||||
<body class="font-sans dark:text-white dark:bg-hex-121212">
|
||||
<div id="app"></div>
|
||||
<div id="app" class="w-full h-full"></div>
|
||||
<noscript>
|
||||
<div>Please enable JavaScript to use this application.</div>
|
||||
</noscript>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"packageManager": "pnpm@9.14.4",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite --port 3333 --open",
|
||||
"dev": "vite --port 10514 --host",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"preview": "vite preview",
|
||||
@ -25,6 +25,7 @@
|
||||
"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
17
pnpm-lock.yaml
generated
@ -48,6 +48,9 @@ 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))
|
||||
@ -3899,6 +3902,12 @@ 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:
|
||||
@ -8610,6 +8619,14 @@ 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
|
||||
|
||||
254
src/App.vue
254
src/App.vue
@ -1,242 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue'
|
||||
// import { useBackendOnline } from './composables'
|
||||
import { useAPIOnline } from './composables/online'
|
||||
|
||||
import { ChevronRight, ChevronsUpDown } from 'lucide-vue-next'
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from '~/components/ui/breadcrumb'
|
||||
const online = useAPIOnline()
|
||||
|
||||
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
|
||||
onMounted(() => {
|
||||
online.connect()
|
||||
})
|
||||
onUnmounted(() => {
|
||||
online.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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 h-full w-full>
|
||||
<div v-if="!online.isOnline.value" class="absolute z-1000000 h-full w-full bg-gray/4 backdrop-blur-md">
|
||||
<div class="h-full w-full flex flex-col items-center justify-center gap-3">
|
||||
<h1 class="flex flex-row items-center gap-3 text-3xl">
|
||||
<div class="i-tabler-network" />
|
||||
后端服务不在线
|
||||
</h1>
|
||||
<div>
|
||||
请检查后端是否运行, 或者服务器是否在维护
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DefaultLayout />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export const API_BASE_URL = 'http://localhost:5000'
|
||||
// export const API_BASE_URL = 'http://localhost:5000'
|
||||
export const API_BASE_URL = `/api`
|
||||
// export const API_BASE_URL = 'https://gca-api.dustella.net:8443'
|
||||
|
||||
BIN
src/assets/map.avif
Normal file
BIN
src/assets/map.avif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
80
src/components/AuthBlock.vue
Normal file
80
src/components/AuthBlock.vue
Normal file
@ -0,0 +1,80 @@
|
||||
<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>
|
||||
236
src/components/DefaultLayout.vue
Normal file
236
src/components/DefaultLayout.vue
Normal file
@ -0,0 +1,236 @@
|
||||
<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> </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>
|
||||
@ -6,11 +6,69 @@ import {
|
||||
ResizablePanelGroup,
|
||||
} from '~/components/ui/resizable'
|
||||
|
||||
defineProps<{
|
||||
imageResult: ImageResult
|
||||
const props = defineProps<{
|
||||
imageQuery: string
|
||||
extraErrorHandle?: (resp: Response) => {
|
||||
type: string
|
||||
message: string
|
||||
}
|
||||
extraResponseHandle?: (resp: Response) => Promise<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>
|
||||
@ -22,14 +80,20 @@ defineEmits(['submit'])
|
||||
>
|
||||
<ResizablePanel id="demo-panel-1" :default-size="200" py-5>
|
||||
<ParamsCard
|
||||
@submit="$emit('submit')"
|
||||
@submit="() => {
|
||||
$emit('submit')
|
||||
execute()
|
||||
}"
|
||||
@download="download"
|
||||
>
|
||||
<slot />
|
||||
</ParamsCard>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle id="demo-handle-1" />
|
||||
<ResizablePanel id="demo-panel-2" :default-size="500">
|
||||
<ImageContainer :image-result="imageResult" />
|
||||
<ResizablePanel id="demo-panel-2" :default-size="350">
|
||||
<ImageContainer v-slot="metadata" :image-result="imageResult">
|
||||
<slot name="extra-meta" :metadata="metadata.metadata" />
|
||||
</ImageContainer>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
|
||||
@ -3,8 +3,9 @@ import { Icon } from '@iconify/vue/dist/iconify.js'
|
||||
|
||||
export interface ImageResult {
|
||||
result: 'success' | 'error' | 'pending' | 'idle'
|
||||
imageUrl: string
|
||||
resourceId: string
|
||||
message?: string
|
||||
extraMetadata?: Record<string, any>
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
@ -17,18 +18,14 @@ 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.imageUrl" />
|
||||
<Image v-else-if="imageResult.result === 'success'" class="flex flex-1 items-center justify-center text-xl" :image-url="imageResult.resourceId" />
|
||||
<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>
|
||||
|
||||
@ -1,79 +0,0 @@
|
||||
<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>
|
||||
@ -1,32 +1,34 @@
|
||||
<script lang="ts" setup>
|
||||
defineEmits(['submit'])
|
||||
defineEmits(['submit', 'download'])
|
||||
</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>
|
||||
</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>
|
||||
<Button type="submit" class="mt-5 w-full" @click.prevent="$emit('download')">
|
||||
下载
|
||||
</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
<!-- </DrawerContent>
|
||||
</Drawer> -->
|
||||
</template>
|
||||
|
||||
@ -1,150 +0,0 @@
|
||||
<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>
|
||||
@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
@ -1,78 +0,0 @@
|
||||
<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>
|
||||
@ -1,123 +0,0 @@
|
||||
<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>
|
||||
233
src/components/dense/RadarSingle.vue
Normal file
233
src/components/dense/RadarSingle.vue
Normal file
@ -0,0 +1,233 @@
|
||||
<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>
|
||||
157
src/components/dense/saber/day_cycle_power_wave_plot.vue
Normal file
157
src/components/dense/saber/day_cycle_power_wave_plot.vue
Normal file
@ -0,0 +1,157 @@
|
||||
<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>
|
||||
146
src/components/dense/saber/day_fft_ifft_plot.vue
Normal file
146
src/components/dense/saber/day_fft_ifft_plot.vue
Normal file
@ -0,0 +1,146 @@
|
||||
<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>
|
||||
144
src/components/dense/saber/month_year_power_wave_plot.vue
Normal file
144
src/components/dense/saber/month_year_power_wave_plot.vue
Normal file
@ -0,0 +1,144 @@
|
||||
<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>
|
||||
142
src/components/dense/saber/plot_wave_fitting.vue
Normal file
142
src/components/dense/saber/plot_wave_fitting.vue
Normal file
@ -0,0 +1,142 @@
|
||||
<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>
|
||||
47
src/components/dense/saber/utils.ts
Normal file
47
src/components/dense/saber/utils.ts
Normal file
@ -0,0 +1,47 @@
|
||||
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 }
|
||||
19
src/components/ui/drawer/Drawer.vue
Normal file
19
src/components/ui/drawer/Drawer.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<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>
|
||||
28
src/components/ui/drawer/DrawerContent.vue
Normal file
28
src/components/ui/drawer/DrawerContent.vue
Normal file
@ -0,0 +1,28 @@
|
||||
<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>
|
||||
20
src/components/ui/drawer/DrawerDescription.vue
Normal file
20
src/components/ui/drawer/DrawerDescription.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<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>
|
||||
14
src/components/ui/drawer/DrawerFooter.vue
Normal file
14
src/components/ui/drawer/DrawerFooter.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<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>
|
||||
14
src/components/ui/drawer/DrawerHeader.vue
Normal file
14
src/components/ui/drawer/DrawerHeader.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<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>
|
||||
18
src/components/ui/drawer/DrawerOverlay.vue
Normal file
18
src/components/ui/drawer/DrawerOverlay.vue
Normal file
@ -0,0 +1,18 @@
|
||||
<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>
|
||||
20
src/components/ui/drawer/DrawerTitle.vue
Normal file
20
src/components/ui/drawer/DrawerTitle.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<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>
|
||||
8
src/components/ui/drawer/index.ts
Normal file
8
src/components/ui/drawer/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
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'
|
||||
74
src/composables/fetch.ts
Normal file
74
src/composables/fetch.ts
Normal file
@ -0,0 +1,74 @@
|
||||
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,
|
||||
// }
|
||||
// })
|
||||
0
src/composables/header.ts
Normal file
0
src/composables/header.ts
Normal file
@ -1 +1,2 @@
|
||||
export * from './dark'
|
||||
export * from './fetch'
|
||||
|
||||
@ -1,3 +1,163 @@
|
||||
export const hasConnection = ref(false)
|
||||
import { useIntervalFn } from '@vueuse/core'
|
||||
import { API_BASE_URL } from '~/CONSTANT'
|
||||
|
||||
// get `https://`
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { routes } from 'vue-router/auto-routes'
|
||||
import { handleHotUpdate, routes } from 'vue-router/auto-routes'
|
||||
import App from './App.vue'
|
||||
|
||||
import '@unocss/reset/tailwind.css'
|
||||
@ -13,5 +13,9 @@ const router = createRouter({
|
||||
routes,
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
})
|
||||
if (import.meta.hot) {
|
||||
handleHotUpdate(router)
|
||||
}
|
||||
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
|
||||
12
src/pages/auth.vue
Normal file
12
src/pages/auth.vue
Normal file
@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<AuthBlock />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
156
src/pages/balloon/gravity_wave/0-perday.vue
Normal file
156
src/pages/balloon/gravity_wave/0-perday.vue
Normal file
@ -0,0 +1,156 @@
|
||||
<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>
|
||||
223
src/pages/balloon/gravity_wave/1-year.vue
Normal file
223
src/pages/balloon/gravity_wave/1-year.vue
Normal file
@ -0,0 +1,223 @@
|
||||
<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>
|
||||
@ -1,101 +0,0 @@
|
||||
<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>
|
||||
@ -1,117 +0,0 @@
|
||||
<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>
|
||||
147
src/pages/cosmic/gravity_wave/0-perday.vue
Normal file
147
src/pages/cosmic/gravity_wave/0-perday.vue
Normal file
@ -0,0 +1,147 @@
|
||||
<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>
|
||||
100
src/pages/cosmic/gravity_wave/1-multiday.vue
Normal file
100
src/pages/cosmic/gravity_wave/1-multiday.vue
Normal file
@ -0,0 +1,100 @@
|
||||
<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>
|
||||
170
src/pages/cosmic/planet_wave/0-perday.vue
Normal file
170
src/pages/cosmic/planet_wave/0-perday.vue
Normal file
@ -0,0 +1,170 @@
|
||||
<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>
|
||||
104
src/pages/cosmic/planet_wave/1-daily.vue
Normal file
104
src/pages/cosmic/planet_wave/1-daily.vue
Normal file
@ -0,0 +1,104 @@
|
||||
<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>
|
||||
14
src/pages/debug.vue
Normal file
14
src/pages/debug.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<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>
|
||||
24
src/pages/radar/planet_wave/0-single.vue
Normal file
24
src/pages/radar/planet_wave/0-single.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<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>
|
||||
14
src/pages/radar/planet_wave/1-stats.vue
Normal file
14
src/pages/radar/planet_wave/1-stats.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"title": "流星雷达",
|
||||
"description": "行星波统计",
|
||||
"group": "流星雷达",
|
||||
"item_name": "行星波统计"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<RadarSingle wave-type="行星波" />
|
||||
</template>
|
||||
24
src/pages/radar/tidal_wave/0-single.vue
Normal file
24
src/pages/radar/tidal_wave/0-single.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<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>
|
||||
14
src/pages/radar/tidal_wave/1-stats.vue
Normal file
14
src/pages/radar/tidal_wave/1-stats.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"title": "流星雷达",
|
||||
"description": "潮汐波统计",
|
||||
"group": "流星雷达",
|
||||
"item_name": "潮汐波统计"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<RadarSingle wave-type="潮汐波" />
|
||||
</template>
|
||||
@ -1,4 +1,4 @@
|
||||
<route lang="json">
|
||||
<!-- <route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"title": "流星雷达-热力图",
|
||||
@ -7,28 +7,19 @@
|
||||
"item_name": "热力图"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
</route> -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ImageResult } from '~/components/ImageContainer.vue'
|
||||
import { API_BASE_URL } from '~/CONSTANT'
|
||||
|
||||
const selectedMode = ref('潮汐波')
|
||||
const selectedMode = ref('2日行星波')
|
||||
const selectedWave = ref('潮汐波')
|
||||
const selectedDateType = ref('day')
|
||||
const selectedStation = ref('武汉左岭镇站')
|
||||
const selectedYear = ref('2017')
|
||||
const selectedWindType = ref('uwind')
|
||||
const selectedH = ref(90000)
|
||||
const selectedH = ref(94000)
|
||||
const selectedDate = ref('20170316')
|
||||
const selectedMonth = ref('1')
|
||||
|
||||
const imageResult = reactive<ImageResult>({
|
||||
result: 'idle',
|
||||
imageUrl: '',
|
||||
message: '请你选择一个模式和日期',
|
||||
})
|
||||
|
||||
const modes = [
|
||||
'2日行星波',
|
||||
'5日行星波',
|
||||
@ -55,6 +46,7 @@ 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)
|
||||
@ -63,32 +55,17 @@ 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 = `${API_BASE_URL}/radar/render/heatmap?${query}`
|
||||
const path = `/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 fetch(`${API_BASE_URL}/radar/metadata`)
|
||||
const data = await resp.json()
|
||||
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\\(.*?)\\/
|
||||
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]
|
||||
@ -108,10 +85,16 @@ onMounted(async () => {
|
||||
|
||||
paths.value = data
|
||||
})
|
||||
|
||||
watch(selectedYear, (newV) => {
|
||||
selectedDate.value = Array.from(dates.value).filter(
|
||||
d => d.startsWith(newV),
|
||||
)[0]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DenseFramework :image-result="imageResult" @submit="execute">
|
||||
<DenseFramework :image-query="queryUrl">
|
||||
<div>
|
||||
<div flex="~ col gap-3" py-3>
|
||||
<Label for="waves">波类型</Label>
|
||||
@ -127,7 +110,7 @@ onMounted(async () => {
|
||||
</Tabs>
|
||||
<div v-if="selectedWave === '行星波'">
|
||||
<Label>行星波类型</Label>
|
||||
<Tabs v-model="selectedMode" default-value="uwind">
|
||||
<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("行星波", "") }}
|
||||
@ -179,7 +162,7 @@ onMounted(async () => {
|
||||
id="start"
|
||||
v-model:model-value="selectedH" :format-options="{
|
||||
useGrouping: false,
|
||||
}" :default-value="90000"
|
||||
}" :default-value="94000"
|
||||
>
|
||||
<NumberFieldContent>
|
||||
<NumberFieldDecrement />
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<route lang="json">
|
||||
<!-- <route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"title": "流星雷达-潮汐波时空变化",
|
||||
@ -7,23 +7,16 @@
|
||||
"item_name": "潮汐波时空变化"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
</route> -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ImageResult } from '~/components/ImageContainer.vue'
|
||||
import { API_BASE_URL } from '~/CONSTANT'
|
||||
|
||||
const selectedMode = ref('潮汐波')
|
||||
const selectedMode = ref('2日行星波')
|
||||
const selectedWave = ref('潮汐波')
|
||||
const selectedStation = ref('武汉左岭镇站')
|
||||
const selectedYear = ref('2017')
|
||||
const imageResult = reactive<ImageResult>({
|
||||
result: 'idle',
|
||||
imageUrl: '',
|
||||
message: '请你选择一个模式和日期',
|
||||
})
|
||||
const selectedMonthRange = reactive({ start: '1', end: '12' })
|
||||
|
||||
const modes = [
|
||||
'潮汐波',
|
||||
'2日行星波',
|
||||
'5日行星波',
|
||||
'10日行星波',
|
||||
@ -35,36 +28,24 @@ const stations = ref<Set<string>>(new Set())
|
||||
const years = ref<Set<string>>(new Set())
|
||||
|
||||
const queryUrl = computed(() => {
|
||||
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}`
|
||||
// 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}`
|
||||
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 fetch(`${API_BASE_URL}/radar/metadata`)
|
||||
const data = await resp.json()
|
||||
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\\(.*?)\\/
|
||||
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]
|
||||
@ -83,22 +64,31 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DenseFramework :image-result="imageResult" @submit="execute">
|
||||
<DenseFramework :image-query="queryUrl">
|
||||
<div>
|
||||
<div flex="~ col items-stretch gap-3" py-3>
|
||||
<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>
|
||||
<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="selectedStation">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a fruit" />
|
||||
@ -112,19 +102,56 @@ 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>
|
||||
|
||||
@ -1,121 +0,0 @@
|
||||
<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>
|
||||
@ -1,121 +0,0 @@
|
||||
<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>
|
||||
48
src/pages/saber/gravity_wave/0-perday.vue
Normal file
48
src/pages/saber/gravity_wave/0-perday.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<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>
|
||||
11
src/pages/saber/gravity_wave/1-monthly.vue
Normal file
11
src/pages/saber/gravity_wave/1-monthly.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
title: Saber 重力波月统计
|
||||
description: Saber 重力波月统计
|
||||
group: Saber
|
||||
item_name: 重力波统计
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<Month_year_power_wave_plot />
|
||||
</template>
|
||||
@ -1,89 +0,0 @@
|
||||
<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>
|
||||
188
src/pages/saber/planet_wave/0-perday.vue
Normal file
188
src/pages/saber/planet_wave/0-perday.vue
Normal file
@ -0,0 +1,188 @@
|
||||
<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 5,10,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>
|
||||
118
src/pages/saber/planet_wave/1-monthly.vue
Normal file
118
src/pages/saber/planet_wave/1-monthly.vue
Normal file
@ -0,0 +1,118 @@
|
||||
<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 5,10,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>
|
||||
@ -1,121 +0,0 @@
|
||||
<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>
|
||||
@ -1,4 +1,4 @@
|
||||
import { API_BASE_URL } from '~/CONSTANT'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const saberPaths = ref<string[]>([])
|
||||
const currentSaberDays = ref<string>('')
|
||||
@ -6,15 +6,44 @@ const currentSaberDays = ref<string>('')
|
||||
async function refreshPath() {
|
||||
if (saberPaths.value.length)
|
||||
return
|
||||
const resp = await fetch(`${API_BASE_URL}/saber/metadata`)
|
||||
const data = await resp.json()
|
||||
saberPaths.value = data
|
||||
const resp = await baseFetch<string[]>(`/saber/metadata`).json()
|
||||
const data = resp.data.value
|
||||
saberPaths.value = data!
|
||||
}
|
||||
|
||||
async function refreshCurrentSaberDays(path: string) {
|
||||
const resp = await fetch(`${API_BASE_URL}/saber/metadata/list_days?path=${path}`)
|
||||
const data = await resp.json()
|
||||
currentSaberDays.value = data
|
||||
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]}`
|
||||
}
|
||||
|
||||
export { currentSaberDays, refreshCurrentSaberDays, refreshPath, saberPaths }
|
||||
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 }
|
||||
|
||||
92
src/pages/tidi/gravity_wave/monthly.vue
Normal file
92
src/pages/tidi/gravity_wave/monthly.vue
Normal file
@ -0,0 +1,92 @@
|
||||
<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>
|
||||
137
src/pages/tidi/planet_wave/daily.vue
Normal file
137
src/pages/tidi/planet_wave/daily.vue
Normal file
@ -0,0 +1,137 @@
|
||||
<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
24
typed-router.d.ts
vendored
@ -20,13 +20,25 @@ 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> }>,
|
||||
'/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>>,
|
||||
'/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>>,
|
||||
'/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/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>>,
|
||||
'/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>>,
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,18 @@ 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')}/`,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user