feat: can navigate to TC
This commit is contained in:
parent
52d1caa7de
commit
e8bbb7e7c6
@ -44,6 +44,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^3.13.0",
|
"@antfu/eslint-config": "^3.13.0",
|
||||||
"@iconify-json/carbon": "^1.2.5",
|
"@iconify-json/carbon": "^1.2.5",
|
||||||
|
"@iconify/json": "^2.2.304",
|
||||||
"@intlify/unplugin-vue-i18n": "^6.0.3",
|
"@intlify/unplugin-vue-i18n": "^6.0.3",
|
||||||
"@shikijs/markdown-it": "^1.26.1",
|
"@shikijs/markdown-it": "^1.26.1",
|
||||||
"@types/markdown-it-link-attributes": "^3.0.5",
|
"@types/markdown-it-link-attributes": "^3.0.5",
|
||||||
|
|||||||
11
pnpm-lock.yaml
generated
11
pnpm-lock.yaml
generated
@ -89,6 +89,9 @@ importers:
|
|||||||
'@iconify-json/carbon':
|
'@iconify-json/carbon':
|
||||||
specifier: ^1.2.5
|
specifier: ^1.2.5
|
||||||
version: 1.2.5
|
version: 1.2.5
|
||||||
|
'@iconify/json':
|
||||||
|
specifier: ^2.2.304
|
||||||
|
version: 2.2.304
|
||||||
'@intlify/unplugin-vue-i18n':
|
'@intlify/unplugin-vue-i18n':
|
||||||
specifier: ^6.0.3
|
specifier: ^6.0.3
|
||||||
version: 6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.18.0(jiti@2.4.0))(rollup@4.30.1)(typescript@5.7.3)(vue-i18n@11.0.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
|
version: 6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.18.0(jiti@2.4.0))(rollup@4.30.1)(typescript@5.7.3)(vue-i18n@11.0.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
|
||||||
@ -1413,6 +1416,9 @@ packages:
|
|||||||
'@iconify-json/carbon@1.2.5':
|
'@iconify-json/carbon@1.2.5':
|
||||||
resolution: {integrity: sha512-aI3TEzOrUDGhs74zIT3ym/ZQBUEziyu8JifntX2Hb4siVzsP5sQ/QEfVdmcCUj37kQUYT3TYBSeAw2vTfCJx9w==}
|
resolution: {integrity: sha512-aI3TEzOrUDGhs74zIT3ym/ZQBUEziyu8JifntX2Hb4siVzsP5sQ/QEfVdmcCUj37kQUYT3TYBSeAw2vTfCJx9w==}
|
||||||
|
|
||||||
|
'@iconify/json@2.2.304':
|
||||||
|
resolution: {integrity: sha512-8eO5m27lIfYLRFCxNgZQD/AMcDYw9NzPT6ViSYJDvX+Vevuj/smaQ/SfHVTNFzj5JcISDh5AV6n2nllQthceog==}
|
||||||
|
|
||||||
'@iconify/types@2.0.0':
|
'@iconify/types@2.0.0':
|
||||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||||
|
|
||||||
@ -7606,6 +7612,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/types': 2.0.0
|
'@iconify/types': 2.0.0
|
||||||
|
|
||||||
|
'@iconify/json@2.2.304':
|
||||||
|
dependencies:
|
||||||
|
'@iconify/types': 2.0.0
|
||||||
|
pathe: 1.1.2
|
||||||
|
|
||||||
'@iconify/types@2.0.0': {}
|
'@iconify/types@2.0.0': {}
|
||||||
|
|
||||||
'@iconify/utils@2.2.1':
|
'@iconify/utils@2.2.1':
|
||||||
|
|||||||
12
src/components.d.ts
vendored
12
src/components.d.ts
vendored
@ -34,11 +34,14 @@ declare module 'vue' {
|
|||||||
DrawerHeader: typeof import('./components/ui/drawer/DrawerHeader.vue')['default']
|
DrawerHeader: typeof import('./components/ui/drawer/DrawerHeader.vue')['default']
|
||||||
DrawerOverlay: typeof import('./components/ui/drawer/DrawerOverlay.vue')['default']
|
DrawerOverlay: typeof import('./components/ui/drawer/DrawerOverlay.vue')['default']
|
||||||
DrawerTitle: typeof import('./components/ui/drawer/DrawerTitle.vue')['default']
|
DrawerTitle: typeof import('./components/ui/drawer/DrawerTitle.vue')['default']
|
||||||
|
InfoPan: typeof import('./components/InfoPan.vue')['default']
|
||||||
Input: typeof import('./components/ui/input/Input.vue')['default']
|
Input: typeof import('./components/ui/input/Input.vue')['default']
|
||||||
Label: typeof import('./components/ui/label/Label.vue')['default']
|
Label: typeof import('./components/ui/label/Label.vue')['default']
|
||||||
|
OptForm: typeof import('./components/OptForm.vue')['default']
|
||||||
Popover: typeof import('./components/ui/popover/Popover.vue')['default']
|
Popover: typeof import('./components/ui/popover/Popover.vue')['default']
|
||||||
PopoverContent: typeof import('./components/ui/popover/PopoverContent.vue')['default']
|
PopoverContent: typeof import('./components/ui/popover/PopoverContent.vue')['default']
|
||||||
PopoverTrigger: typeof import('./components/ui/popover/PopoverTrigger.vue')['default']
|
PopoverTrigger: typeof import('./components/ui/popover/PopoverTrigger.vue')['default']
|
||||||
|
PosForm: typeof import('./components/PosForm.vue')['default']
|
||||||
README: typeof import('./components/README.md')['default']
|
README: typeof import('./components/README.md')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
@ -53,6 +56,15 @@ declare module 'vue' {
|
|||||||
SelectSeparator: typeof import('./components/ui/select/SelectSeparator.vue')['default']
|
SelectSeparator: typeof import('./components/ui/select/SelectSeparator.vue')['default']
|
||||||
SelectTrigger: typeof import('./components/ui/select/SelectTrigger.vue')['default']
|
SelectTrigger: typeof import('./components/ui/select/SelectTrigger.vue')['default']
|
||||||
SelectValue: typeof import('./components/ui/select/SelectValue.vue')['default']
|
SelectValue: typeof import('./components/ui/select/SelectValue.vue')['default']
|
||||||
|
Table: typeof import('./components/ui/table/Table.vue')['default']
|
||||||
|
TableBody: typeof import('./components/ui/table/TableBody.vue')['default']
|
||||||
|
TableCaption: typeof import('./components/ui/table/TableCaption.vue')['default']
|
||||||
|
TableCell: typeof import('./components/ui/table/TableCell.vue')['default']
|
||||||
|
TableEmpty: typeof import('./components/ui/table/TableEmpty.vue')['default']
|
||||||
|
TableFooter: typeof import('./components/ui/table/TableFooter.vue')['default']
|
||||||
|
TableHead: typeof import('./components/ui/table/TableHead.vue')['default']
|
||||||
|
TableHeader: typeof import('./components/ui/table/TableHeader.vue')['default']
|
||||||
|
TableRow: typeof import('./components/ui/table/TableRow.vue')['default']
|
||||||
Textarea: typeof import('./components/ui/textarea/Textarea.vue')['default']
|
Textarea: typeof import('./components/ui/textarea/Textarea.vue')['default']
|
||||||
TheCounter: typeof import('./components/TheCounter.vue')['default']
|
TheCounter: typeof import('./components/TheCounter.vue')['default']
|
||||||
TheFooter: typeof import('./components/TheFooter.vue')['default']
|
TheFooter: typeof import('./components/TheFooter.vue')['default']
|
||||||
|
|||||||
0
src/components/InfoPan.vue
Normal file
0
src/components/InfoPan.vue
Normal file
172
src/components/OptForm.vue
Normal file
172
src/components/OptForm.vue
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useOptions } from '@/composables/dateOpt'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import {
|
||||||
|
DateFormatter,
|
||||||
|
type DateValue,
|
||||||
|
getLocalTimeZone,
|
||||||
|
isSameDay,
|
||||||
|
parseDate,
|
||||||
|
} from '@internationalized/date'
|
||||||
|
|
||||||
|
const df = new DateFormatter('en-US', {
|
||||||
|
dateStyle: 'long',
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
options,
|
||||||
|
castBeginDate,
|
||||||
|
castTargetDate,
|
||||||
|
} = useOptions()
|
||||||
|
|
||||||
|
const mapping = ref < Record<string, string>>({})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const resp = await baseFetch('/tc/metadata').json()
|
||||||
|
const data = resp.data.value.data
|
||||||
|
mapping.value = data
|
||||||
|
options.mapping = data
|
||||||
|
})
|
||||||
|
|
||||||
|
const isDateDisabled = computed(() => {
|
||||||
|
const availableDates = Object.keys(mapping.value)
|
||||||
|
.map((date: string) => {
|
||||||
|
// date is like 20020202, we need to first trans to 2002-02-02
|
||||||
|
try {
|
||||||
|
const newDateStr = `${date.slice(0, 4)}-${date.slice(4, 6)}-${date.slice(6, 8)}`
|
||||||
|
const newDate = parseDate(newDateStr)
|
||||||
|
return newDate
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(date => date !== null) as DateValue[]
|
||||||
|
function _isDateDisabled(date: DateValue): boolean {
|
||||||
|
const hasMatch = availableDates.some((thisDate) => {
|
||||||
|
return isSameDay(date, thisDate)
|
||||||
|
})
|
||||||
|
return !hasMatch
|
||||||
|
}
|
||||||
|
return _isDateDisabled
|
||||||
|
})
|
||||||
|
|
||||||
|
const isTargetDateDisabled = computed(() => {
|
||||||
|
const castBegin = castBeginDate.value
|
||||||
|
function _functionIsTargetDateDisabled(date: DateValue): boolean {
|
||||||
|
if (!castBegin) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const castEnd = castBegin.add({ days: 29 })
|
||||||
|
return date.compare(castBegin) < 0 || date.compare(castEnd) > 0
|
||||||
|
}
|
||||||
|
return _functionIsTargetDateDisabled
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form class="grid w-full items-start gap-6">
|
||||||
|
<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">
|
||||||
|
<Label for="model">渲染模式</Label>
|
||||||
|
<Select v-model="options.renderMode">
|
||||||
|
<SelectTrigger
|
||||||
|
id="model"
|
||||||
|
class="items-start [&_[data-description]]:hidden"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Select a model" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent class="z-400">
|
||||||
|
<SelectItem value="heatmap">
|
||||||
|
<div class="flex items-start gap-3 text-muted-foreground">
|
||||||
|
<span class="i-tabler-sun size-5" />
|
||||||
|
<div class="grid gap-0.5">
|
||||||
|
<p>
|
||||||
|
热图
|
||||||
|
<span class="text-foreground font-medium">
|
||||||
|
使用点状热力图渲染
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="text-xs" data-description>
|
||||||
|
放大缩小可能不准确
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="image">
|
||||||
|
<div class="flex items-start gap-3 text-muted-foreground">
|
||||||
|
<span class="i-material-symbols-image size-5" />
|
||||||
|
<div class="grid gap-0.5">
|
||||||
|
<p>
|
||||||
|
图片
|
||||||
|
<span class="text-foreground font-medium">
|
||||||
|
使用图片叠加层渲染
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="text-xs" data-description>
|
||||||
|
相对准确
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<Label for="temperature">发布日期</Label>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
:class="cn(
|
||||||
|
'w-full justify-start text-left font-normal',
|
||||||
|
!castBeginDate && 'text-muted-foreground',
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<span class="i-uil-calender mr-2 h-4 w-4" />
|
||||||
|
{{ castBeginDate ? df.format(castBeginDate.toDate(getLocalTimeZone())) : "Pick a date" }}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="z-300 w-auto p-0">
|
||||||
|
<Calendar v-model="castBeginDate" class="z-300" initial-focus :is-date-disabled="isDateDisabled" />
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label for="top-p">目标预报日期</Label>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
:class="cn(
|
||||||
|
'w-full justify-start text-left font-normal',
|
||||||
|
!castTargetDate && 'text-muted-foreground',
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<span class="i-uil-calender mr-2 h-4 w-4" />
|
||||||
|
|
||||||
|
{{ castTargetDate ? df.format(castTargetDate.toDate(getLocalTimeZone())) : "Pick a date" }}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="z-300 w-auto p-0">
|
||||||
|
<Calendar
|
||||||
|
v-model="castTargetDate"
|
||||||
|
class="z-300" initial-focus
|
||||||
|
:is-date-disabled="isTargetDateDisabled"
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-3" />
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
49
src/components/PosForm.vue
Normal file
49
src/components/PosForm.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const {
|
||||||
|
currentTcLoc,
|
||||||
|
currentFocus,
|
||||||
|
} = useOptions()
|
||||||
|
|
||||||
|
function jump(postion: [number, number]) {
|
||||||
|
const getter = () => postion
|
||||||
|
currentFocus.value = getter
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<fieldset class="grid gap-6 border rounded-lg p-4">
|
||||||
|
<legend class="px-1 text-sm font-medium -ml-1">
|
||||||
|
预报台风
|
||||||
|
</legend>
|
||||||
|
<Table v-if="currentTcLoc">
|
||||||
|
<TableCaption>预报台风</TableCaption>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Lat </TableHead>
|
||||||
|
<TableHead>Lon</TableHead>
|
||||||
|
<TableHead class="text-right">
|
||||||
|
操作
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow v-for="postion in currentTcLoc" :key="postion[0]">
|
||||||
|
<TableCell>{{ postion[0].toFixed(2) }}</TableCell>
|
||||||
|
<TableCell>{{ postion[1].toFixed(2) }}</TableCell>
|
||||||
|
<TableCell class="text-right">
|
||||||
|
<Button
|
||||||
|
class="h-6 text-[13.2px]" variant="outline"
|
||||||
|
@click.prevent="jump(postion)"
|
||||||
|
>
|
||||||
|
跳转
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</fieldset>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
16
src/components/ui/table/Table.vue
Normal file
16
src/components/ui/table/Table.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative w-full overflow-auto">
|
||||||
|
<table :class="cn('w-full caption-bottom text-sm', props.class)">
|
||||||
|
<slot />
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
14
src/components/ui/table/TableBody.vue
Normal file
14
src/components/ui/table/TableBody.vue
Normal 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>
|
||||||
|
<tbody :class="cn('[&_tr:last-child]:border-0', props.class)">
|
||||||
|
<slot />
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
14
src/components/ui/table/TableCaption.vue
Normal file
14
src/components/ui/table/TableCaption.vue
Normal 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>
|
||||||
|
<caption :class="cn('mt-4 text-sm text-muted-foreground', props.class)">
|
||||||
|
<slot />
|
||||||
|
</caption>
|
||||||
|
</template>
|
||||||
21
src/components/ui/table/TableCell.vue
Normal file
21
src/components/ui/table/TableCell.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<td
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'p-4 align-middle [&:has([role=checkbox])]:pr-0',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
37
src/components/ui/table/TableEmpty.vue
Normal file
37
src/components/ui/table/TableEmpty.vue
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { computed, type HTMLAttributes } from 'vue'
|
||||||
|
import TableCell from './TableCell.vue'
|
||||||
|
import TableRow from './TableRow.vue'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
colspan?: number
|
||||||
|
}>(), {
|
||||||
|
colspan: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'p-4 whitespace-nowrap align-middle text-sm text-foreground',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center py-10">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</template>
|
||||||
14
src/components/ui/table/TableFooter.vue
Normal file
14
src/components/ui/table/TableFooter.vue
Normal 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>
|
||||||
|
<tfoot :class="cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', props.class)">
|
||||||
|
<slot />
|
||||||
|
</tfoot>
|
||||||
|
</template>
|
||||||
14
src/components/ui/table/TableHead.vue
Normal file
14
src/components/ui/table/TableHead.vue
Normal 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>
|
||||||
|
<th :class="cn('h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0', props.class)">
|
||||||
|
<slot />
|
||||||
|
</th>
|
||||||
|
</template>
|
||||||
14
src/components/ui/table/TableHeader.vue
Normal file
14
src/components/ui/table/TableHeader.vue
Normal 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>
|
||||||
|
<thead :class="cn('[&_tr]:border-b', props.class)">
|
||||||
|
<slot />
|
||||||
|
</thead>
|
||||||
|
</template>
|
||||||
14
src/components/ui/table/TableRow.vue
Normal file
14
src/components/ui/table/TableRow.vue
Normal 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>
|
||||||
|
<tr :class="cn('border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted', props.class)">
|
||||||
|
<slot />
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
9
src/components/ui/table/index.ts
Normal file
9
src/components/ui/table/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export { default as Table } from './Table.vue'
|
||||||
|
export { default as TableBody } from './TableBody.vue'
|
||||||
|
export { default as TableCaption } from './TableCaption.vue'
|
||||||
|
export { default as TableCell } from './TableCell.vue'
|
||||||
|
export { default as TableEmpty } from './TableEmpty.vue'
|
||||||
|
export { default as TableFooter } from './TableFooter.vue'
|
||||||
|
export { default as TableHead } from './TableHead.vue'
|
||||||
|
export { default as TableHeader } from './TableHeader.vue'
|
||||||
|
export { default as TableRow } from './TableRow.vue'
|
||||||
@ -7,7 +7,25 @@ function _useOptions() {
|
|||||||
renderMode: 'heatmap' as 'heatmap' | 'image',
|
renderMode: 'heatmap' as 'heatmap' | 'image',
|
||||||
isPlaying: false,
|
isPlaying: false,
|
||||||
mapping: {} as Record<string, string>,
|
mapping: {} as Record<string, string>,
|
||||||
|
|
||||||
})
|
})
|
||||||
|
const castBeginDate = ref<DateValue>()
|
||||||
|
const castTargetDate = ref<DateValue>()
|
||||||
|
|
||||||
|
const currentTcLoc = ref<null | [number, number][]>(null)
|
||||||
|
type PosGetter = () => [number, number]
|
||||||
|
const currentFocus = ref<null | PosGetter>(null)
|
||||||
|
|
||||||
|
function getISOcastBeginDate() {
|
||||||
|
const castBeginDateStr = options.castBeginDate.toString()
|
||||||
|
if (castBeginDateStr.length !== 8) {
|
||||||
|
console.error(castBeginDateStr)
|
||||||
|
throw new Error('castBeginDate is invalid')
|
||||||
|
}
|
||||||
|
const newDateStr = `${castBeginDateStr.slice(0, 4)}-${castBeginDateStr.slice(4, 6)}-${castBeginDateStr.slice(6, 8)}`
|
||||||
|
const castBeginDate = parseDate(newDateStr)
|
||||||
|
return castBeginDate
|
||||||
|
}
|
||||||
|
|
||||||
function setCastBeginDate(date: DateValue) {
|
function setCastBeginDate(date: DateValue) {
|
||||||
const datestr = date.toString()
|
const datestr = date.toString()
|
||||||
@ -27,10 +45,40 @@ function _useOptions() {
|
|||||||
return dayNo
|
return dayNo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshCurrentTcLoc() {
|
||||||
|
currentTcLoc.value = null
|
||||||
|
const q = new URLSearchParams()
|
||||||
|
q.set('day', options.castDayNo.toString())
|
||||||
|
const path = options.mapping[options.castBeginDate]
|
||||||
|
q.set('path', path)
|
||||||
|
const base = import.meta.env.VITE_BACKEND_URL
|
||||||
|
|
||||||
|
const url = `${base}/tc/metadata/centroids?${q}`
|
||||||
|
|
||||||
|
const resp = await baseFetch(url).json()
|
||||||
|
|
||||||
|
const data = resp.data.value!
|
||||||
|
currentTcLoc.value = data as [number, number][]
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([castBeginDate, castTargetDate], () => {
|
||||||
|
if (!castBeginDate.value || !castTargetDate.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setCastBeginDate(castBeginDate.value)
|
||||||
|
setTargetDate(castTargetDate.value)
|
||||||
|
refreshCurrentTcLoc()
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
options,
|
options,
|
||||||
setCastBeginDate,
|
setCastBeginDate,
|
||||||
setTargetDate,
|
setTargetDate,
|
||||||
|
getISOcastBeginDate,
|
||||||
|
currentFocus,
|
||||||
|
castBeginDate,
|
||||||
|
castTargetDate,
|
||||||
|
currentTcLoc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,219 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useOptions } from '@/composables/dateOpt'
|
import { DrawerClose, DrawerTrigger } from 'vaul-vue'
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import {
|
|
||||||
DateFormatter,
|
|
||||||
type DateValue,
|
|
||||||
getLocalTimeZone,
|
|
||||||
isSameDay,
|
|
||||||
parseDate,
|
|
||||||
} from '@internationalized/date'
|
|
||||||
import { DrawerTrigger } from 'vaul-vue'
|
|
||||||
|
|
||||||
const [DefineFormTemplate, ReuseFormTemplate] = createReusableTemplate()
|
|
||||||
const df = new DateFormatter('en-US', {
|
|
||||||
dateStyle: 'long',
|
|
||||||
})
|
|
||||||
|
|
||||||
const castBeginDate = ref<DateValue>()
|
|
||||||
const castTargetDate = ref<DateValue>()
|
|
||||||
|
|
||||||
const { options, setCastBeginDate, setTargetDate } = useOptions()
|
|
||||||
|
|
||||||
const mapping = ref < Record<string, string>>({})
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
const resp = await baseFetch('/tc/metadata').json()
|
|
||||||
const data = resp.data.value.data
|
|
||||||
mapping.value = data
|
|
||||||
options.mapping = data
|
|
||||||
})
|
|
||||||
|
|
||||||
const isDateDisabled = computed(() => {
|
|
||||||
const availableDates = Object.keys(mapping.value)
|
|
||||||
.map((date: string) => {
|
|
||||||
// date is like 20020202, we need to first trans to 2002-02-02
|
|
||||||
try {
|
|
||||||
const newDateStr = `${date.slice(0, 4)}-${date.slice(4, 6)}-${date.slice(6, 8)}`
|
|
||||||
const newDate = parseDate(newDateStr)
|
|
||||||
return newDate
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(date => date !== null) as DateValue[]
|
|
||||||
function _isDateDisabled(date: DateValue): boolean {
|
|
||||||
const hasMatch = availableDates.some((thisDate) => {
|
|
||||||
return isSameDay(date, thisDate)
|
|
||||||
})
|
|
||||||
return !hasMatch
|
|
||||||
}
|
|
||||||
return _isDateDisabled
|
|
||||||
})
|
|
||||||
|
|
||||||
const isTargetDateDisabled = computed(() => {
|
|
||||||
const castBegin = castBeginDate.value
|
|
||||||
function _functionIsTargetDateDisabled(date: DateValue): boolean {
|
|
||||||
if (!castBegin) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
const castEnd = castBegin.add({ days: 29 })
|
|
||||||
return date.compare(castBegin) < 0 || date.compare(castEnd) > 0
|
|
||||||
}
|
|
||||||
return _functionIsTargetDateDisabled
|
|
||||||
})
|
|
||||||
|
|
||||||
watch([castBeginDate, castTargetDate], () => {
|
|
||||||
if (castBeginDate.value && castTargetDate.value) {
|
|
||||||
setCastBeginDate(castBeginDate.value)
|
|
||||||
setTargetDate(castTargetDate.value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="grid h-screen w-full">
|
<div class="grid h-screen w-full">
|
||||||
<DefineFormTemplate>
|
|
||||||
<form class="grid w-full items-start gap-6">
|
|
||||||
<fieldset class="grid gap-6 border rounded-lg p-4">
|
|
||||||
<legend class="px-1 text-sm font-medium -ml-1">
|
|
||||||
日期设置
|
|
||||||
</legend>
|
|
||||||
{{ options }}
|
|
||||||
<div class="grid gap-3">
|
|
||||||
<Label for="model">渲染模式</Label>
|
|
||||||
<Select v-model="options.renderMode">
|
|
||||||
<SelectTrigger
|
|
||||||
id="model"
|
|
||||||
class="items-start [&_[data-description]]:hidden"
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Select a model" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="heatmap">
|
|
||||||
<div class="flex items-start gap-3 text-muted-foreground">
|
|
||||||
<Rabbit class="size-5" />
|
|
||||||
<div class="grid gap-0.5">
|
|
||||||
<p>
|
|
||||||
热图
|
|
||||||
<span class="text-foreground font-medium">
|
|
||||||
使用点状热力图渲染
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p class="text-xs" data-description>
|
|
||||||
放大缩小可能不准确
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="image">
|
|
||||||
<div class="flex items-start gap-3 text-muted-foreground">
|
|
||||||
<Bird class="size-5" />
|
|
||||||
<div class="grid gap-0.5">
|
|
||||||
<p>
|
|
||||||
图片
|
|
||||||
<span class="text-foreground font-medium">
|
|
||||||
使用图片叠加层渲染
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p class="text-xs" data-description>
|
|
||||||
相对准确
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-3">
|
|
||||||
<div>
|
|
||||||
<Label for="temperature">发布日期</Label>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger as-child>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
:class="cn(
|
|
||||||
'w-full justify-start text-left font-normal',
|
|
||||||
!castBeginDate && 'text-muted-foreground',
|
|
||||||
)"
|
|
||||||
>
|
|
||||||
<CalendarIcon class="mr-2 h-4 w-4" />
|
|
||||||
{{ castBeginDate ? df.format(castBeginDate.toDate(getLocalTimeZone())) : "Pick a date" }}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent class="w-auto p-0">
|
|
||||||
<Calendar v-model="castBeginDate" initial-focus :is-date-disabled="isDateDisabled" />
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label for="top-p">目标预报日期</Label>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger as-child>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
:class="cn(
|
|
||||||
'w-full justify-start text-left font-normal',
|
|
||||||
!castTargetDate && 'text-muted-foreground',
|
|
||||||
)"
|
|
||||||
>
|
|
||||||
<CalendarIcon class="mr-2 h-4 w-4" />
|
|
||||||
{{ castTargetDate ? df.format(castTargetDate.toDate(getLocalTimeZone())) : "Pick a date" }}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent class="w-auto p-0">
|
|
||||||
<Calendar
|
|
||||||
v-model="castTargetDate" initial-focus
|
|
||||||
:is-date-disabled="isTargetDateDisabled"
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="grid gap-3" />
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class="grid gap-6 border rounded-lg p-4">
|
|
||||||
<legend class="px-1 text-sm font-medium -ml-1">
|
|
||||||
渲染
|
|
||||||
</legend>
|
|
||||||
<Button>
|
|
||||||
开始自动播放
|
|
||||||
</Button>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
</DefineFormTemplate>
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<header
|
<header
|
||||||
class="sticky top-0 z-10 h-[53px] flex items-center gap-1 border-b bg-background px-4"
|
class="sticky top-0 z-10 h-[53px] flex items-center gap-1 border-b bg-background px-4"
|
||||||
>
|
>
|
||||||
|
<span class="i-mingcute-typhoon-fill size-6 px-5" />
|
||||||
<h1 class="text-xl font-semibold">
|
<h1 class="text-xl font-semibold">
|
||||||
次季节台风预报系统
|
次季节台风预报系统
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<Drawer>
|
|
||||||
<DrawerTrigger as-child>
|
|
||||||
<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>地图设置</DrawerTitle>
|
|
||||||
<DrawerDescription>
|
|
||||||
选择日期和渲染方式
|
|
||||||
</DrawerDescription>
|
|
||||||
</DrawerHeader>
|
|
||||||
<ReuseFormTemplate />
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
</header>
|
</header>
|
||||||
<main
|
<main
|
||||||
class="grid flex-1 gap-4 overflow-auto p-4 lg:grid-cols-3 md:grid-cols-2"
|
class="grid flex-1 gap-4 overflow-auto p-4 lg:grid-cols-3 md:grid-cols-2"
|
||||||
>
|
>
|
||||||
<div class="relative hidden flex-col items-start gap-8 md:flex">
|
<div class="relative hidden flex-col items-stretch gap-8 md:flex">
|
||||||
<ReuseFormTemplate />
|
<OptForm />
|
||||||
|
<PosForm />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="relative h-full min-h-[50vh] flex flex-col rounded-xl bg-muted/50 p-4 lg:col-span-2"
|
class="relative h-full min-h-[50vh] flex flex-col rounded-xl bg-muted/50 p-4 lg:col-span-2"
|
||||||
@ -223,18 +28,79 @@ watch([castBeginDate, castTargetDate], () => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
<RouterView />
|
<RouterView />
|
||||||
<form
|
<div
|
||||||
class="relative overflow-hidden border rounded-lg bg-background focus-within:ring-1 focus-within:ring-ring"
|
class="relative border rounded-lg bg-background"
|
||||||
>
|
>
|
||||||
<Label for="message" class="sr-only"> Message </Label>
|
|
||||||
<Card
|
<Card
|
||||||
id="message"
|
id="message"
|
||||||
placeholder="Type your message here..."
|
placeholder="Type your message here..."
|
||||||
class="min-h-22 resize-none border-0 p-3 shadow-none focus-visible:ring-0"
|
flex="~ col items-stretch gap-4"
|
||||||
|
class="border-0 p-3 shadow-none focus-visible:ring-0"
|
||||||
>
|
>
|
||||||
something here
|
<!-- <div class="pb-3">
|
||||||
|
<div v-if="options.castDayNo !== -1">
|
||||||
|
这一天是 {{ currentDate }}
|
||||||
|
<br>
|
||||||
|
是 {{ options.castBeginDate }} 发布预报的第 {{ options.castDayNo }} 天
|
||||||
|
</div>
|
||||||
|
<div v-if="currentTcLoc">
|
||||||
|
{{ currentTcLoc.length }} 个区域预报出台风
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<Button w-full variant="outline">
|
||||||
|
开始自动播放
|
||||||
|
</Button>
|
||||||
|
<Drawer class="z-100">
|
||||||
|
<DrawerTrigger as-child>
|
||||||
|
<Button size="icon" variant="outline" class="w-full md:hidden">
|
||||||
|
<span class="i-mingcute-typhoon-fill size-4" />
|
||||||
|
<span class="">台风位置</span>
|
||||||
|
</Button>
|
||||||
|
</DrawerTrigger>
|
||||||
|
<DrawerContent class="z-200 max-h-[80vh] p-5">
|
||||||
|
<DrawerHeader>
|
||||||
|
<DrawerTitle>台风位置</DrawerTitle>
|
||||||
|
<DrawerDescription>
|
||||||
|
点击可以跳转
|
||||||
|
</DrawerDescription>
|
||||||
|
</DrawerHeader>
|
||||||
|
<PosForm />
|
||||||
|
<DrawerClose as-child>
|
||||||
|
<div class="py-3">
|
||||||
|
<Button class="w-full">
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DrawerClose>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
<Drawer class="z-100">
|
||||||
|
<DrawerTrigger as-child>
|
||||||
|
<Button size="icon" class="w-full md:hidden">
|
||||||
|
<span class="i-material-symbols-settings size-4" />
|
||||||
|
<span class="">地图设置</span>
|
||||||
|
</Button>
|
||||||
|
</DrawerTrigger>
|
||||||
|
<DrawerContent class="z-200 max-h-[80vh] p-5">
|
||||||
|
<DrawerHeader>
|
||||||
|
<DrawerTitle>地图设置</DrawerTitle>
|
||||||
|
<DrawerDescription>
|
||||||
|
选择日期和渲染方式
|
||||||
|
</DrawerDescription>
|
||||||
|
</DrawerHeader>
|
||||||
|
<OptForm />
|
||||||
|
<DrawerClose as-child>
|
||||||
|
<div class="py-3">
|
||||||
|
<Button class="w-full">
|
||||||
|
应用
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DrawerClose>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
</Card>
|
</Card>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -14,7 +14,7 @@ const previousLayer = ref<null | AMap.ImageLayer>(null)
|
|||||||
|
|
||||||
const apiKey = import.meta.env.VITE_AMAP_API_KEY
|
const apiKey = import.meta.env.VITE_AMAP_API_KEY
|
||||||
|
|
||||||
const { options } = useOptions()
|
const { options, currentFocus } = useOptions()
|
||||||
|
|
||||||
watch(options, async () => {
|
watch(options, async () => {
|
||||||
if (!map.value || !amapInstance.value) {
|
if (!map.value || !amapInstance.value) {
|
||||||
@ -26,6 +26,16 @@ watch(options, async () => {
|
|||||||
updateMap()
|
updateMap()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(currentFocus, (newFocus) => {
|
||||||
|
if (!newFocus) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const [lat, lon] = newFocus()
|
||||||
|
// debugger
|
||||||
|
map.value?.setCenter([lat, lon - 35])
|
||||||
|
map.value?.setZoom(4)
|
||||||
|
})
|
||||||
|
|
||||||
function updateMap() {
|
function updateMap() {
|
||||||
if (!map.value || !amapInstance.value) {
|
if (!map.value || !amapInstance.value) {
|
||||||
return
|
return
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user