newui: ballon

This commit is contained in:
Dustella 2025-01-14 14:21:46 +08:00
parent 522cf7dfb9
commit 41a78347d1
36 changed files with 1480 additions and 387 deletions

24
components.d.ts vendored
View File

@ -11,6 +11,9 @@ declare module 'vue' {
AccordionContent: typeof import('./src/components/ui/accordion/AccordionContent.vue')['default']
AccordionItem: typeof import('./src/components/ui/accordion/AccordionItem.vue')['default']
AccordionTrigger: typeof import('./src/components/ui/accordion/AccordionTrigger.vue')['default']
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']
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']
@ -50,6 +53,8 @@ declare module 'vue' {
Collapsible: typeof import('./src/components/ui/collapsible/Collapsible.vue')['default']
CollapsibleContent: typeof import('./src/components/ui/collapsible/CollapsibleContent.vue')['default']
CollapsibleTrigger: typeof import('./src/components/ui/collapsible/CollapsibleTrigger.vue')['default']
CoolBack: typeof import('./src/components/CoolBack.vue')['default']
DenseFramework: typeof import('./src/components/DenseFramework.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']
@ -74,6 +79,21 @@ declare module 'vue' {
Input: typeof import('./src/components/ui/input/Input.vue')['default']
Label: typeof import('./src/components/ui/label/Label.vue')['default']
Loading: typeof import('./src/components/Loading.vue')['default']
Menubar: typeof import('./src/components/ui/menubar/Menubar.vue')['default']
MenubarCheckboxItem: typeof import('./src/components/ui/menubar/MenubarCheckboxItem.vue')['default']
MenubarContent: typeof import('./src/components/ui/menubar/MenubarContent.vue')['default']
MenubarGroup: typeof import('./src/components/ui/menubar/MenubarGroup.vue')['default']
MenubarItem: typeof import('./src/components/ui/menubar/MenubarItem.vue')['default']
MenubarLabel: typeof import('./src/components/ui/menubar/MenubarLabel.vue')['default']
MenubarMenu: typeof import('./src/components/ui/menubar/MenubarMenu.vue')['default']
MenubarRadioGroup: typeof import('./src/components/ui/menubar/MenubarRadioGroup.vue')['default']
MenubarRadioItem: typeof import('./src/components/ui/menubar/MenubarRadioItem.vue')['default']
MenubarSeparator: typeof import('./src/components/ui/menubar/MenubarSeparator.vue')['default']
MenubarShortcut: typeof import('./src/components/ui/menubar/MenubarShortcut.vue')['default']
MenubarSub: typeof import('./src/components/ui/menubar/MenubarSub.vue')['default']
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']
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']
@ -88,12 +108,15 @@ 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']
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']
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']
ResizablePanelGroup: typeof import('./src/components/ui/resizable/ResizablePanelGroup.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Select: typeof import('./src/components/ui/select/Select.vue')['default']
@ -150,5 +173,6 @@ declare module 'vue' {
TooltipContent: typeof import('./src/components/ui/tooltip/TooltipContent.vue')['default']
TooltipProvider: typeof import('./src/components/ui/tooltip/TooltipProvider.vue')['default']
TooltipTrigger: typeof import('./src/components/ui/tooltip/TooltipTrigger.vue')['default']
TopHeader: typeof import('./src/components/TopHeader.vue')['default']
}
}

View File

@ -16,9 +16,11 @@
"@unocss/preset-icons": "^0.65.4",
"@vee-validate/zod": "^4.15.0",
"@vueuse/core": "^12.0.0",
"@vueuse/motion": "^2.2.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.468.0",
"p5i": "^0.6.0",
"radix-vue": "^1.9.11",
"shadcn-vue": "^0.11.3",
"tailwind-merge": "^2.5.5",

624
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { ChevronRight, ChevronsUpDown } from 'lucide-vue-next'
import {
Breadcrumb,
BreadcrumbItem,
@ -9,13 +10,14 @@ import {
BreadcrumbPage,
BreadcrumbSeparator,
} from '~/components/ui/breadcrumb'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '~/components/ui/collapsible'
import { Separator } from '~/components/ui/separator'
import {
Sidebar,
SidebarContent,
@ -31,7 +33,6 @@ import {
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarTrigger,
} from '~/components/ui/sidebar'
// This is sample data.
@ -108,83 +109,134 @@ const data = {
},
],
}
} as const
</script>
<template>
<SidebarProvider>
<Sidebar collapsible="icon">
<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>
<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"
<CollapsibleContent>
<SidebarMenuSub>
<SidebarMenuSubItem
v-for="subItem in item.items"
:key="subItem.title"
class="hover:bg-gray-100"
>
<span>{{ subItem.title }}</span>
</RouterLink>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</CollapsibleContent>
<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>
</Collapsible>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem />
</SidebarMenu>
</SidebarFooter>
<SidebarRail />
</Sidebar>
<SidebarInset>
<header class="group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12 h-16 flex shrink-0 items-center gap-2 transition-[width,height] ease-linear">
<div class="flex items-center gap-2 px-4">
<SidebarTrigger class="-ml-1" />
<Separator orientation="vertical" class="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<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>
</header>
<RouterView />
</SidebarInset>
</SidebarProvider>
</SidebarMenu>
</SidebarFooter>
<SidebarRail />
</Sidebar>
<SidebarInset>
<header class="group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12 h-16 flex shrink-0 items-center justify-between gap-2 transition-[width,height] ease-linear">
<div class="flex items-center gap-2 px-4">
<Icon icon="heroicons-solid:home" class="h-4 w-4" />
<Separator orientation="vertical" class="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">
中高层大气波动解析识别技术系统
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator class="hidden md:block" />
<BreadcrumbItem class="hidden md:block">
<BreadcrumbLink href="#">
{{ $route.meta.group }}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator class="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>{{ $route.meta.item_name }}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div px-7>
<Alert variant="default">
<Icon icon="el:ok-circle" class="h-4 w-4 text-green!" />
<AlertTitle>服务状态正常</AlertTitle>
</Alert>
</div>
</header>
<RouterView class="h-full overflow-hidden" />
</SidebarInset>
</SidebarProvider>
</div>
</div>
</template>

View File

@ -0,0 +1,36 @@
<script lang="ts" setup>
import type { ImageResult } from './ImageContainer.vue'
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '~/components/ui/resizable'
defineProps<{
imageResult: ImageResult
}>()
defineEmits(['submit'])
</script>
<template>
<div flex="~ row h-full items-begin justify-stretch gap-3 p-8 w-full">
<ResizablePanelGroup
id="demo-group-1"
direction="horizontal"
class="w-full border rounded-lg"
>
<ResizablePanel id="demo-panel-1" :default-size="200" py-5>
<ParamsCard
@submit="$emit('submit')"
>
<slot />
</ParamsCard>
</ResizablePanel>
<ResizableHandle id="demo-handle-1" />
<ResizablePanel id="demo-panel-2" :default-size="500">
<ImageContainer :image-result="imageResult" />
</ResizablePanel>
</ResizablePanelGroup>
</div>
</template>

View File

@ -6,7 +6,7 @@ defineProps<{
<template>
<div>
<img id="image_" class="w-400" :src="imageUrl">
<img id="image_" :src="imageUrl">
</div>
</template>

View File

@ -1,16 +1,29 @@
<script lang="ts" setup>
defineProps<{
import { Icon } from '@iconify/vue/dist/iconify.js'
export interface ImageResult {
result: 'success' | 'error' | 'pending' | 'idle'
imageUrl: string
message?: string
}
defineProps<{
imageResult: ImageResult
}>()
</script>
<template>
<div class="relative h-full min-h-[50vh] w-full flex flex-col rounded-xl bg-muted/50 p-4 lg:col-span-2">
<Badge variant="outline" class="absolute right-3 top-3">
Output
<Badge variant="outline" class="absolute left-3 top-3">
图片输出
</Badge>
<div class="flex-1" />
<Image :image-url="imageUrl" />
<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" />
</div>
<Image v-else-if="imageResult.result === 'success'" :image-url="imageResult.imageUrl" />
<div v-else class="flex flex-1 items-center justify-center text-xl">
{{ imageResult.message }}
</div>
<Button type="submit" size="sm" class="ml-auto gap-1.5">
下载图片

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

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

View File

@ -1,15 +1,5 @@
<script lang="ts" setup>
import type { ZodObject } from 'zod'
defineProps<{
schema: ZodObject<any, any, any> | null
}>()
defineEmits<{
submit: [value: {
[x: string]: any
}]
}>()
defineEmits(['submit'])
</script>
<template>
@ -17,7 +7,7 @@ defineEmits<{
<DrawerTrigger as-child class="min-w-[20rem]">
<Button variant="ghost" size="icon" class="md:hidden">
<Settings class="size-4" />
<span class="sr-only">Settings</span>
<span class="sr-only">参数设置</span>
</Button>
</DrawerTrigger>
<DrawerContent class="max-h-[80vh]">
@ -27,14 +17,13 @@ defineEmits<{
<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">
Settings
参数设置
</legend>
<div class="grid gap-3">
<AutoForm v-if="schema" :schema="schema" @submit="(e) => $emit('submit', e)">
<Button type="submit" class="w-full">
绘制
</Button>
</AutoForm>
<slot />
<Button type="submit" class="mt-5 w-full" @click.prevent="$emit('submit')">
绘制
</Button>
</div>
</fieldset>
</form>

View File

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

View File

@ -0,0 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
import { type AlertVariants, alertVariants } from '.'
const props = defineProps<{
class?: HTMLAttributes['class']
variant?: AlertVariants['variant']
}>()
</script>
<template>
<div :class="cn(alertVariants({ variant }), props.class)" role="alert">
<slot />
</div>
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('text-sm [&_p]:leading-relaxed', props.class)">
<slot />
</div>
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<h5 :class="cn('mb-1 font-medium leading-none tracking-tight', props.class)">
<slot />
</h5>
</template>

View File

@ -0,0 +1,23 @@
import { cva, type VariantProps } from 'class-variance-authority'
export { default as Alert } from './Alert.vue'
export { default as AlertDescription } from './AlertDescription.vue'
export { default as AlertTitle } from './AlertTitle.vue'
export const alertVariants = cva(
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
{
variants: {
variant: {
default: 'bg-background text-foreground',
destructive:
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
},
},
defaultVariants: {
variant: 'default',
},
},
)
export type AlertVariants = VariantProps<typeof alertVariants>

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
import {
MenubarRoot,
type MenubarRootEmits,
type MenubarRootProps,
useForwardPropsEmits,
} from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<MenubarRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<MenubarRootEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<MenubarRoot
v-bind="forwarded"
:class="
cn(
'flex h-10 items-center gap-x-1 rounded-md border bg-background p-1',
props.class,
)
"
>
<slot />
</MenubarRoot>
</template>

View File

@ -0,0 +1,40 @@
<script setup lang="ts">
import { Check } from 'lucide-vue-next'
import {
MenubarCheckboxItem,
type MenubarCheckboxItemEmits,
type MenubarCheckboxItemProps,
MenubarItemIndicator,
useForwardPropsEmits,
} from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<MenubarCheckboxItemProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<MenubarCheckboxItemEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<MenubarCheckboxItem
v-bind="forwarded"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class,
)"
>
<span class="absolute left-2 h-3.5 w-3.5 flex items-center justify-center">
<MenubarItemIndicator>
<Check class="h-4 w-4" />
</MenubarItemIndicator>
</span>
<slot />
</MenubarCheckboxItem>
</template>

View File

@ -0,0 +1,43 @@
<script setup lang="ts">
import {
MenubarContent,
type MenubarContentProps,
MenubarPortal,
useForwardProps,
} from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = withDefaults(
defineProps<MenubarContentProps & { class?: HTMLAttributes['class'] }>(),
{
align: 'start',
alignOffset: -4,
sideOffset: 8,
},
)
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<MenubarPortal>
<MenubarContent
v-bind="forwardedProps"
:class="
cn(
'z-50 min-w-48 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class,
)
"
>
<slot />
</MenubarContent>
</MenubarPortal>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { MenubarGroup, type MenubarGroupProps } from 'radix-vue'
const props = defineProps<MenubarGroupProps>()
</script>
<template>
<MenubarGroup v-bind="props">
<slot />
</MenubarGroup>
</template>

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
import {
MenubarItem,
type MenubarItemEmits,
type MenubarItemProps,
useForwardPropsEmits,
} from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<MenubarItemProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
const emits = defineEmits<MenubarItemEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<MenubarItem
v-bind="forwarded"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
props.class,
)"
>
<slot />
</MenubarItem>
</template>

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { MenubarLabel, type MenubarLabelProps } from 'radix-vue'
import { cn } from '~/lib/utils'
const props = defineProps<MenubarLabelProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
</script>
<template>
<MenubarLabel :class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)">
<slot />
</MenubarLabel>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { MenubarMenu, type MenubarMenuProps } from 'radix-vue'
const props = defineProps<MenubarMenuProps>()
</script>
<template>
<MenubarMenu v-bind="props">
<slot />
</MenubarMenu>
</template>

View File

@ -0,0 +1,20 @@
<script setup lang="ts">
import {
MenubarRadioGroup,
type MenubarRadioGroupEmits,
type MenubarRadioGroupProps,
useForwardPropsEmits,
} from 'radix-vue'
const props = defineProps<MenubarRadioGroupProps>()
const emits = defineEmits<MenubarRadioGroupEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<MenubarRadioGroup v-bind="forwarded">
<slot />
</MenubarRadioGroup>
</template>

View File

@ -0,0 +1,40 @@
<script setup lang="ts">
import { Circle } from 'lucide-vue-next'
import {
MenubarItemIndicator,
MenubarRadioItem,
type MenubarRadioItemEmits,
type MenubarRadioItemProps,
useForwardPropsEmits,
} from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<MenubarRadioItemProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<MenubarRadioItemEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<MenubarRadioItem
v-bind="forwarded"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class,
)"
>
<span class="absolute left-2 h-3.5 w-3.5 flex items-center justify-center">
<MenubarItemIndicator>
<Circle class="h-2 w-2 fill-current" />
</MenubarItemIndicator>
</span>
<slot />
</MenubarRadioItem>
</template>

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import { MenubarSeparator, type MenubarSeparatorProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<MenubarSeparatorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<MenubarSeparator :class=" cn('-mx-1 my-1 h-px bg-muted', props.class)" v-bind="forwardedProps" />
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<span :class="cn('ml-auto text-xs tracking-widest text-muted-foreground', props.class)">
<slot />
</span>
</template>

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import { MenubarSub, type MenubarSubEmits, useForwardPropsEmits } from 'radix-vue'
interface MenubarSubRootProps {
defaultOpen?: boolean
open?: boolean
}
const props = defineProps<MenubarSubRootProps>()
const emits = defineEmits<MenubarSubEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<MenubarSub v-bind="forwarded">
<slot />
</MenubarSub>
</template>

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
import {
MenubarPortal,
MenubarSubContent,
type MenubarSubContentEmits,
type MenubarSubContentProps,
useForwardPropsEmits,
} from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<MenubarSubContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<MenubarSubContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<MenubarPortal>
<MenubarSubContent
v-bind="forwarded"
:class="
cn(
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class,
)
"
>
<slot />
</MenubarSubContent>
</MenubarPortal>
</template>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import { ChevronRight } from 'lucide-vue-next'
import { MenubarSubTrigger, type MenubarSubTriggerProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<MenubarSubTriggerProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<MenubarSubTrigger
v-bind="forwardedProps"
:class="cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
inset && 'pl-8',
props.class,
)"
>
<slot />
<ChevronRight class="ml-auto h-4 w-4" />
</MenubarSubTrigger>
</template>

View File

@ -0,0 +1,29 @@
<script setup lang="ts">
import { MenubarTrigger, type MenubarTriggerProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<MenubarTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<MenubarTrigger
v-bind="forwardedProps"
:class="
cn(
'flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
props.class,
)
"
>
<slot />
</MenubarTrigger>
</template>

View File

@ -0,0 +1,15 @@
export { default as Menubar } from './Menubar.vue'
export { default as MenubarCheckboxItem } from './MenubarCheckboxItem.vue'
export { default as MenubarContent } from './MenubarContent.vue'
export { default as MenubarGroup } from './MenubarGroup.vue'
export { default as MenubarItem } from './MenubarItem.vue'
export { default as MenubarLabel } from './MenubarLabel.vue'
export { default as MenubarMenu } from './MenubarMenu.vue'
export { default as MenubarRadioGroup } from './MenubarRadioGroup.vue'
export { default as MenubarRadioItem } from './MenubarRadioItem.vue'
export { default as MenubarSeparator } from './MenubarSeparator.vue'
export { default as MenubarShortcut } from './MenubarShortcut.vue'
export { default as MenubarSub } from './MenubarSub.vue'
export { default as MenubarSubContent } from './MenubarSubContent.vue'
export { default as MenubarSubTrigger } from './MenubarSubTrigger.vue'
export { default as MenubarTrigger } from './MenubarTrigger.vue'

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import { GripVertical } from 'lucide-vue-next'
import { SplitterResizeHandle, type SplitterResizeHandleEmits, type SplitterResizeHandleProps, useForwardPropsEmits } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<SplitterResizeHandleProps & { class?: HTMLAttributes['class'], withHandle?: boolean }>()
const emits = defineEmits<SplitterResizeHandleEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<SplitterResizeHandle v-bind="forwarded" :class="cn('relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 [&[data-orientation=vertical]]:h-px [&[data-orientation=vertical]]:w-full [&[data-orientation=vertical]]:after:left-0 [&[data-orientation=vertical]]:after:h-1 [&[data-orientation=vertical]]:after:w-full [&[data-orientation=vertical]]:after:-translate-y-1/2 [&[data-orientation=vertical]]:after:translate-x-0 [&[data-orientation=vertical]>div]:rotate-90', props.class)">
<template v-if="props.withHandle">
<div class="z-10 h-4 w-3 flex items-center justify-center border rounded-sm bg-border">
<GripVertical class="h-2.5 w-2.5" />
</div>
</template>
</SplitterResizeHandle>
</template>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import { SplitterGroup, type SplitterGroupEmits, type SplitterGroupProps, useForwardPropsEmits } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<SplitterGroupProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<SplitterGroupEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<SplitterGroup v-bind="forwarded" :class="cn('flex h-full w-full data-[panel-group-direction=vertical]:flex-col', props.class)">
<slot />
</SplitterGroup>
</template>

View File

@ -0,0 +1,3 @@
export { default as ResizableHandle } from './ResizableHandle.vue'
export { default as ResizablePanelGroup } from './ResizablePanelGroup.vue'
export { SplitterPanel as ResizablePanel } from 'radix-vue'

View File

@ -7,13 +7,16 @@ meta:
</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 selectedMode = ref('观测的二阶多项式拟合')
const imageUrl = ref('')
const hasWaveThisDay = ref(false)
const imageResult = reactive<ImageResult>({
result: 'idle',
imageUrl: '',
message: '请你选择一个模式和日期',
})
const modes = [
'观测的二阶多项式拟合',
@ -21,16 +24,21 @@ const modes = [
'径向风-纬向风矢量图',
'温度-水平风矢量图',
] as const
const formSchema = shallowRef<z.ZodObject< any, any, any > | null>(null)
const stagedDates = ref<string[]>([])
const selectedDate = ref('2022-01-01')
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),
selectedMode: z.enum(modes).describe('选择一个模式'),
selectedDate: z.enum(data.map((d: string) => {
const datePattern = /_\d{8}T\d{6}/
if (!datePattern.test(d)) {
@ -42,50 +50,50 @@ onMounted(async () => {
}
return capture[0]
})),
})
}).describe('选择一个日期')
form.setFieldValue('selectedDate', data[0])
form.setFieldValue('selectedMode', modes[0])
})
})
async function get_image() {
const resp = await fetch(`${API_BASE_URL}/balloon/render/single?mode=${encodeURIComponent(selectedMode.value)}&path=${encodeURIComponent(selectedDate.value)}`)
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) {
hasWaveThisDay.value = false
imageResult.result = 'error' as const
imageResult.message = '这一天没有数据'
return
// return alert('No image available for this mode and date')
}
hasWaveThisDay.value = true
imageResult.result = 'success' as const
const blob = await resp.blob()
const url = URL.createObjectURL(blob)
imageUrl.value = url
imageResult.imageUrl = url
}
watch([selectedMode, selectedDate], () => {
get_image()
})
function submit(value: Record<string, any>) {
if (!(value.selectedMode && value.selectedDate)) {
return
}
selectedMode.value = value.selectedMode
// search for the date in stagedDates
for (const d of stagedDates.value) {
if (d.includes(value.selectedDate)) {
selectedDate.value = d
break
}
}
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>
<div flex="~ row items-begin justify-stretch gap-3 w-full">
<ParamsCard :schema="formSchema" @submit="(e) => submit(e)" />
<ImageContainer :image-url="imageUrl" />
</div>
<DenseFramework :image-result="imageResult" @submit="submit">
<AutoForm
:form="form"
:field-config="{
selectedMode: {
component: 'radio',
},
}"
:schema="formSchema"
/>
</DenseFramework>
</template>
<style lang="scss" scoped>

View File

@ -7,12 +7,18 @@ meta:
</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 imageUrl = ref('')
const startYear = ref(2017)
const endYear = ref(2024)
@ -41,7 +47,8 @@ async function refreshImage() {
const resp = await fetch(url)
const blob = await resp.blob()
const u = URL.createObjectURL(blob)
imageUrl.value = u
imageResult.result = 'success'
imageResult.imageUrl = u
}
watch([selectedMode, startYear, endYear], () => {
@ -55,69 +62,54 @@ watch([selectedMode, startYear, endYear], () => {
</script>
<template>
<div>
<div>
<div flex="row ~ items-center 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 :image-result="imageResult" @submit="refreshImage">
<div flex="col gap-5 ~ justify-center">
<div>
<div v-if="!isIllegal && imageUrl">
<Image :image-url="imageUrl" />
</div>
<div v-else h-40 flex="~ col items-center justify-center" text-xl>
<div v-if="isIllegal">
<div>Start year should be less than end year</div>
</div>
<div v-else>
<div>There is no data</div>
</div>
</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>
</div>
</DenseFramework>
</template>
<style scoped>

View File

@ -4,6 +4,7 @@ import manba from '../../public/pack.png'
<template>
<div flex="~ col items-center">
<!-- <CoolBack /> -->
<img :src="manba">
</div>
</template>