feat: can navigate to TC
This commit is contained in:
parent
52d1caa7de
commit
e8bbb7e7c6
@ -44,6 +44,7 @@
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^3.13.0",
|
||||
"@iconify-json/carbon": "^1.2.5",
|
||||
"@iconify/json": "^2.2.304",
|
||||
"@intlify/unplugin-vue-i18n": "^6.0.3",
|
||||
"@shikijs/markdown-it": "^1.26.1",
|
||||
"@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':
|
||||
specifier: ^1.2.5
|
||||
version: 1.2.5
|
||||
'@iconify/json':
|
||||
specifier: ^2.2.304
|
||||
version: 2.2.304
|
||||
'@intlify/unplugin-vue-i18n':
|
||||
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))
|
||||
@ -1413,6 +1416,9 @@ packages:
|
||||
'@iconify-json/carbon@1.2.5':
|
||||
resolution: {integrity: sha512-aI3TEzOrUDGhs74zIT3ym/ZQBUEziyu8JifntX2Hb4siVzsP5sQ/QEfVdmcCUj37kQUYT3TYBSeAw2vTfCJx9w==}
|
||||
|
||||
'@iconify/json@2.2.304':
|
||||
resolution: {integrity: sha512-8eO5m27lIfYLRFCxNgZQD/AMcDYw9NzPT6ViSYJDvX+Vevuj/smaQ/SfHVTNFzj5JcISDh5AV6n2nllQthceog==}
|
||||
|
||||
'@iconify/types@2.0.0':
|
||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||
|
||||
@ -7606,6 +7612,11 @@ snapshots:
|
||||
dependencies:
|
||||
'@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/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']
|
||||
DrawerOverlay: typeof import('./components/ui/drawer/DrawerOverlay.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']
|
||||
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']
|
||||
PopoverContent: typeof import('./components/ui/popover/PopoverContent.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']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
@ -53,6 +56,15 @@ declare module 'vue' {
|
||||
SelectSeparator: typeof import('./components/ui/select/SelectSeparator.vue')['default']
|
||||
SelectTrigger: typeof import('./components/ui/select/SelectTrigger.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']
|
||||
TheCounter: typeof import('./components/TheCounter.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',
|
||||
isPlaying: false,
|
||||
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) {
|
||||
const datestr = date.toString()
|
||||
@ -27,10 +45,40 @@ function _useOptions() {
|
||||
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 {
|
||||
options,
|
||||
setCastBeginDate,
|
||||
setTargetDate,
|
||||
getISOcastBeginDate,
|
||||
currentFocus,
|
||||
castBeginDate,
|
||||
castTargetDate,
|
||||
currentTcLoc,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,219 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { useOptions } from '@/composables/dateOpt'
|
||||
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)
|
||||
}
|
||||
})
|
||||
import { DrawerClose, DrawerTrigger } from 'vaul-vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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">
|
||||
<header
|
||||
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>
|
||||
|
||||
<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>
|
||||
<main
|
||||
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">
|
||||
<ReuseFormTemplate />
|
||||
<div class="relative hidden flex-col items-stretch gap-8 md:flex">
|
||||
<OptForm />
|
||||
<PosForm />
|
||||
</div>
|
||||
<div
|
||||
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>
|
||||
<div class="flex-1" />
|
||||
<RouterView />
|
||||
<form
|
||||
class="relative overflow-hidden border rounded-lg bg-background focus-within:ring-1 focus-within:ring-ring"
|
||||
<div
|
||||
class="relative border rounded-lg bg-background"
|
||||
>
|
||||
<Label for="message" class="sr-only"> Message </Label>
|
||||
<Card
|
||||
id="message"
|
||||
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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@ -14,7 +14,7 @@ const previousLayer = ref<null | AMap.ImageLayer>(null)
|
||||
|
||||
const apiKey = import.meta.env.VITE_AMAP_API_KEY
|
||||
|
||||
const { options } = useOptions()
|
||||
const { options, currentFocus } = useOptions()
|
||||
|
||||
watch(options, async () => {
|
||||
if (!map.value || !amapInstance.value) {
|
||||
@ -26,6 +26,16 @@ watch(options, async () => {
|
||||
updateMap()
|
||||
})
|
||||
|
||||
watch(currentFocus, (newFocus) => {
|
||||
if (!newFocus) {
|
||||
return
|
||||
}
|
||||
const [lat, lon] = newFocus()
|
||||
// debugger
|
||||
map.value?.setCenter([lat, lon - 35])
|
||||
map.value?.setZoom(4)
|
||||
})
|
||||
|
||||
function updateMap() {
|
||||
if (!map.value || !amapInstance.value) {
|
||||
return
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user