many changes

This commit is contained in:
Dustella 2025-02-13 13:55:03 +08:00
parent 5b800dcce8
commit 2598cdc3a9
Signed by: Dustella
GPG Key ID: 35AA0AA3DC402D5C
20 changed files with 417 additions and 222 deletions

2
.env.example Normal file
View File

@ -0,0 +1,2 @@
VITE_AMAP_API_KEY=xxx
VITE_BACKEND_URL=http://kuroneko.bison-banana.ts.net:35000

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
node_modules
target
dist
.env
.env
.vite-ssg-temp

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "msedge",
"request": "launch",
"name": "Launch Edge against localhost",
"url": "http://localhost:3333",
"webRoot": "${workspaceFolder}"
}
]
}

View File

@ -6,7 +6,7 @@
"build": "vite-ssg build",
"dev": "vite --port 3333 --open",
"lint": "eslint .",
"preview": "vite preview",
"preview": "vite preview --port 35122",
"preview-https": "serve dist",
"test": "vitest",
"test:e2e": "cypress open",
@ -85,7 +85,8 @@
"vite-ssg": "^25.0.0",
"vite-ssg-sitemap": "^0.8.1",
"vitest": "^2.1.8",
"vue-tsc": "^2.2.0"
"vue-tsc": "^2.2.0",
"workbox-window": "^7.3.0"
},
"resolutions": {
"unplugin": "^2.1.2",

22
pnpm-lock.yaml generated
View File

@ -196,7 +196,7 @@ importers:
version: 10.0.6(rollup@4.30.1)(vite@6.0.7(@types/node@20.2.3)(jiti@2.4.0)(stylus@0.57.0)(terser@5.17.6)(tsx@4.19.2)(yaml@2.6.1))
vite-plugin-pwa:
specifier: ^0.21.1
version: 0.21.1(vite@6.0.7(@types/node@20.2.3)(jiti@2.4.0)(stylus@0.57.0)(terser@5.17.6)(tsx@4.19.2)(yaml@2.6.1))(workbox-build@7.1.1)(workbox-window@7.1.0)
version: 0.21.1(vite@6.0.7(@types/node@20.2.3)(jiti@2.4.0)(stylus@0.57.0)(terser@5.17.6)(tsx@4.19.2)(yaml@2.6.1))(workbox-build@7.1.1)(workbox-window@7.3.0)
vite-plugin-vue-devtools:
specifier: ^7.7.0
version: 7.7.0(rollup@4.30.1)(vite@6.0.7(@types/node@20.2.3)(jiti@2.4.0)(stylus@0.57.0)(terser@5.17.6)(tsx@4.19.2)(yaml@2.6.1))(vue@3.5.13(typescript@5.7.3))
@ -215,6 +215,9 @@ importers:
vue-tsc:
specifier: ^2.2.0
version: 2.2.0(typescript@5.7.3)
workbox-window:
specifier: ^7.3.0
version: 7.3.0
packages:
@ -6179,6 +6182,9 @@ packages:
workbox-core@7.1.0:
resolution: {integrity: sha512-5KB4KOY8rtL31nEF7BfvU7FMzKT4B5TkbYa2tzkS+Peqj0gayMT9SytSFtNzlrvMaWgv6y/yvP9C0IbpFjV30Q==}
workbox-core@7.3.0:
resolution: {integrity: sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw==}
workbox-expiration@7.1.0:
resolution: {integrity: sha512-m5DcMY+A63rJlPTbbBNtpJ20i3enkyOtSgYfv/l8h+D6YbbNiA0zKEkCUaMsdDlxggla1oOfRkyqTvl5Ni5KQQ==}
@ -6212,6 +6218,9 @@ packages:
workbox-window@7.1.0:
resolution: {integrity: sha512-ZHeROyqR+AS5UPzholQRDttLFqGMwP0Np8MKWAdyxsDETxq3qOAyXvqessc3GniohG6e0mAqSQyKOHmT8zPF7g==}
workbox-window@7.3.0:
resolution: {integrity: sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA==}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
@ -13162,14 +13171,14 @@ snapshots:
- rollup
- supports-color
vite-plugin-pwa@0.21.1(vite@6.0.7(@types/node@20.2.3)(jiti@2.4.0)(stylus@0.57.0)(terser@5.17.6)(tsx@4.19.2)(yaml@2.6.1))(workbox-build@7.1.1)(workbox-window@7.1.0):
vite-plugin-pwa@0.21.1(vite@6.0.7(@types/node@20.2.3)(jiti@2.4.0)(stylus@0.57.0)(terser@5.17.6)(tsx@4.19.2)(yaml@2.6.1))(workbox-build@7.1.1)(workbox-window@7.3.0):
dependencies:
debug: 4.4.0(supports-color@8.1.1)
pretty-bytes: 6.1.1
tinyglobby: 0.2.10
vite: 6.0.7(@types/node@20.2.3)(jiti@2.4.0)(stylus@0.57.0)(terser@5.17.6)(tsx@4.19.2)(yaml@2.6.1)
workbox-build: 7.1.1
workbox-window: 7.1.0
workbox-window: 7.3.0
transitivePeerDependencies:
- supports-color
@ -13485,6 +13494,8 @@ snapshots:
workbox-core@7.1.0: {}
workbox-core@7.3.0: {}
workbox-expiration@7.1.0:
dependencies:
idb: 7.1.1
@ -13540,6 +13551,11 @@ snapshots:
'@types/trusted-types': 2.0.3
workbox-core: 7.1.0
workbox-window@7.3.0:
dependencies:
'@types/trusted-types': 2.0.3
workbox-core: 7.3.0
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0

View File

@ -3,7 +3,7 @@
// you can use this to manipulate the document head in any components,
// they will be rendered correctly in the html results with vite-ssg
useHead({
title: 'Project Vertex',
title: '季节内台风预报系统',
meta: [
{
name: 'description',

BIN
src/assets/colorbar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

4
src/components.d.ts vendored
View File

@ -7,6 +7,7 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AMapClient: typeof import('./components/AMapClient.vue')['default']
Badge: typeof import('./components/ui/badge/Badge.vue')['default']
Button: typeof import('./components/ui/button/Button.vue')['default']
Calendar: typeof import('./components/ui/calendar/Calendar.vue')['default']
@ -42,7 +43,6 @@ declare module 'vue' {
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']
Select: typeof import('./components/ui/select/Select.vue')['default']
@ -66,8 +66,6 @@ declare module 'vue' {
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']
TheInput: typeof import('./components/TheInput.vue')['default']
Tooltip: typeof import('./components/ui/tooltip/Tooltip.vue')['default']
TooltipContent: typeof import('./components/ui/tooltip/TooltipContent.vue')['default']

View File

@ -0,0 +1,162 @@
<route lang="yaml">
meta:
layout: map
</route>
<script setup lang="ts">
import { useOptions } from '@/composables/dateOpt'
import '@amap/amap-jsapi-types'
const amapInstance = ref<null | typeof AMap>(null)
const map = ref<null | AMap.Map>(null)
const previousLayer = ref<null | AMap.ImageLayer>(null)
const FORCE_MODE_OVERRIDE = 'image' as 'image' | 'heatmap' | null
function getPostionText(position: [number, number]) {
const [x, y] = position
function getX() {
// add E or W
const suffix = x < 180 ? 'E°' : 'W°'
return `${x.toFixed(2)} ${suffix}`
}
function getY() {
// add N or S
const suffix = y < 0 ? 'S°' : 'N°'
return `${y.toFixed(2)} ${suffix}`
}
return { getX, getY }
}
const apiKey = import.meta.env.VITE_AMAP_API_KEY
const { options, currentFocus, getImageUrl, getHeatPoints } = useOptions()
watch(options, async () => {
if (!map.value || !amapInstance.value) {
return
}
if (options.castBeginDate === '' || options.castDayNo === -1) {
return
}
const mode = FORCE_MODE_OVERRIDE ?? options.renderMode
if (mode === 'heatmap') {
await updateMapHeatpoints()
}
if (mode === 'image') {
await updateMapImage()
}
})
watch(currentFocus, (newFocus) => {
if (!newFocus) {
return
}
const [lat, lon] = newFocus()
// debugger
map.value?.setCenter([lat, lon])
map.value?.setZoom(4)
})
const isDark = useDark()
watch(isDark, (newValue) => {
map.value?.setMapStyle(newValue ? 'amap://styles/darkblue' : 'amap://styles/whitesmoke')
})
async function updateMapImage() {
if (!map.value || !amapInstance.value) {
return
}
const url = await getImageUrl()
const image = new amapInstance.value.ImageLayer({
url,
// @ts-expect-error AMap messed up their definition
bounds: new AMap.Bounds([-180, -35], [180, 35]),
// bounds: [-150, -35, 180, 35],
zIndex: 9,
opacity: 1,
// zooms: [15, 20],
})
map.value.add(image)
image.on('complete', () => {
previousLayer.value?.hide()
previousLayer.value = image
})
}
async function updateMapHeatpoints() {
if (!map.value || !amapInstance.value) {
return
}
const data = await getHeatPoints()
let heatMap: any
const _map = map.value
// @ts-expect-error Amap not providing heatmap types
_map.plugin(['AMap.Heatmap'], () => {
// @ts-expect-error Amap not providing heatmap types
heatMap = new amapInstance.value.HeatMap(_map, {
radius: 100, //
opacity: [0, 0.8],
})
heatMap.setDataSet({
data,
max: 1000,
})
heatMap.show()
})
}
onMounted(async () => {
if (!import.meta.env.SSR) {
const AMapLoader = await import('@amap/amap-jsapi-loader')
const _AMap: typeof AMap = await AMapLoader.load({
key: apiKey,
version: '2.0',
plugins: [],
})
amapInstance.value = _AMap
map.value = new _AMap.Map('amap-container', {
zoom: 5,
center: [130, 17],
zooms: [4, 20],
mapStyle: isDark.value ? 'amap://styles/darkblue' : 'amap://styles/whitesmoke',
// layers: [
/* new AMap.TileLayer.Satellite() */
// new AMap.TileLayer.RoadNet(),
// ],
})
map.value.setBounds(new AMap.Bounds([-180, -35], [180, 35]))
map.value.setLimitBounds(new AMap.Bounds([-180, -35], [180, 35]))
map.value.setZoom(5)
map.value.on('click', (e) => {
const position = [e.lnglat.lng, e.lnglat.lat] as [number, number]
const infoWindow = new amapInstance.value!.InfoWindow({
content: `
<div class="p-2 text-black">
${getPostionText(position).getX()}
,
${getPostionText(position).getY()}
</div>
`,
})
infoWindow.open(map.value!, e.lnglat)
})
}
})
</script>
<template>
<div id="amap-container" class="m-0 h-full w-full p-0" />
</template>

View File

@ -26,6 +26,15 @@ onMounted(async () => {
const data = resp.data.value.data
mapping.value = data
options.mapping = data
const date = Object.keys(data)[0]
const newDateStr = `${date.slice(0, 4)}-${date.slice(4, 6)}-${date.slice(6, 8)}`
if (castBeginDate.value === undefined) {
castBeginDate.value = parseDate(newDateStr)
}
if (options.castDayNo === -1) {
castTargetDate.value = castBeginDate.value.add({ days: 1 })
}
})
const isDateDisabled = computed(() => {
@ -68,10 +77,10 @@ const isTargetDateDisabled = computed(() => {
<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">
日期设置
<span class="i-material-symbols-settings size-5" />
</legend>
<div class="grid gap-3">
<Label for="model">渲染模</Label>
<Label for="model">展示方</Label>
<Select v-model="options.renderMode">
<SelectTrigger
id="model"
@ -131,8 +140,12 @@ const isTargetDateDisabled = computed(() => {
{{ 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 class="w-aut1o z-300 p-0">
<Calendar
v-model="castBeginDate" class="z-300"
initial-focus :is-date-disabled="isDateDisabled"
prevent-deselect
/>
</PopoverContent>
</Popover>
</div>
@ -157,6 +170,7 @@ const isTargetDateDisabled = computed(() => {
v-model="castTargetDate"
class="z-300" initial-focus
:is-date-disabled="isTargetDateDisabled"
prevent-deselect
/>
</PopoverContent>
</Popover>

View File

@ -8,6 +8,20 @@ function jump(postion: [number, number]) {
const getter = () => postion
currentFocus.value = getter
}
function getPostionText(position: [number, number]) {
const [x, y] = position
function getX() {
// add E or W
const suffix = x < 180 ? 'E°' : 'W°'
return `${x.toFixed(2)} ${suffix}`
}
function getY() {
// add N or S
const suffix = y < 0 ? 'S°' : 'N°'
return `${y.toFixed(2)} ${suffix}`
}
return { getX, getY }
}
</script>
<template>
@ -16,7 +30,7 @@ function jump(postion: [number, number]) {
预报台风
</legend>
<Table v-if="currentTcLoc">
<TableCaption>预报台风</TableCaption>
<!-- <TableCaption>预报台风</TableCaption> -->
<TableHeader>
<TableRow>
<TableHead>Lat </TableHead>
@ -28,8 +42,8 @@ function jump(postion: [number, number]) {
</TableHeader>
<TableBody>
<TableRow v-for="postion in currentTcLoc" :key="postion[0]">
<TableCell>{{ postion[0].toFixed(2) }}</TableCell>
<TableCell>{{ postion[1].toFixed(2) }}</TableCell>
<TableCell>{{ getPostionText(postion).getX() }}</TableCell>
<TableCell>{{ getPostionText(postion).getY() }}</TableCell>
<TableCell class="text-right">
<Button
class="h-6 text-[13.2px]" variant="outline"

View File

@ -1,19 +0,0 @@
<script setup lang="ts">
const props = defineProps<{
initial: number
}>()
const { count, inc, dec } = useCounter(props.initial)
</script>
<template>
<div>
{{ count }}
<button class="inc" @click="inc()">
+
</button>
<button class="dec" @click="dec()">
-
</button>
</div>
</template>

View File

@ -1,37 +0,0 @@
<script setup lang="ts">
import { availableLocales, loadLanguageAsync } from '@/modules/i18n'
const { t, locale } = useI18n()
async function toggleLocales() {
// change to some real logic
const locales = availableLocales
const newLocale = locales[(locales.indexOf(locale.value) + 1) % locales.length]
await loadLanguageAsync(newLocale)
locale.value = newLocale
}
</script>
<template>
<nav flex="~ gap-4" mt-6 justify-center text-xl>
<RouterLink icon-btn to="/" :title="t('button.home')">
<div i-carbon-campsite />
</RouterLink>
<button icon-btn :title="t('button.toggle_dark')" @click="toggleDark()">
<div i="carbon-sun dark:carbon-moon" />
</button>
<a icon-btn :title="t('button.toggle_langs')" @click="toggleLocales()">
<div i-carbon-language />
</a>
<RouterLink icon-btn to="/about" :title="t('button.about')" data-test-id="about">
<div i-carbon-dicom-overlay />
</RouterLink>
<a icon-btn rel="noreferrer" href="https://github.com/antfu/vitesse" target="_blank" title="GitHub">
<div i-carbon-logo-github />
</a>
</nav>
</template>

View File

@ -2,20 +2,38 @@ import { type DateValue, parseDate } from '@internationalized/date'
function _useOptions() {
const options = reactive({
// it is inited as '', which means the date is not set
castBeginDate: '',
// it is inited as -1, which means the date is not set
castDayNo: -1,
renderMode: 'heatmap' as 'heatmap' | 'image',
isPlaying: false,
renderMode: 'image' as 'heatmap' | 'image',
// maps date to path
mapping: {} as Record<string, string>,
})
const castBeginDate = ref<DateValue>()
const castTargetDate = ref<DateValue>()
const interval = ref(3000)
const currentTcLoc = ref<null | [number, number][]>(null)
type PosGetter = () => [number, number]
const currentFocus = ref<null | PosGetter>(null)
const shouldAutoNavigate = ref(true)
const autoPlayer = useIntervalFn(() => {
if (options.castDayNo < 0) {
autoPlayer.pause()
}
if (options.castDayNo >= 29) {
autoPlayer.pause()
}
setTargetDate(castTargetDate.value!.add({ days: 1 }))
castTargetDate.value = castTargetDate.value?.add({ days: 1 })
}, interval, {
immediate: false,
})
function getISOcastBeginDate() {
const castBeginDateStr = options.castBeginDate.toString()
if (castBeginDateStr.length !== 8) {
@ -30,6 +48,13 @@ function _useOptions() {
function setCastBeginDate(date: DateValue) {
const datestr = date.toString()
options.castBeginDate = datestr.replaceAll('-', '')
// check if Target date is later than begin date
// if so, set target date to begin date
if (castTargetDate.value) {
if (castTargetDate.value.compare(date) < 0) {
castTargetDate.value = date.add({ days: 1 })
}
}
}
function setTargetDate(date: DateValue) {
@ -44,13 +69,41 @@ function _useOptions() {
options.castDayNo = dayNo
return dayNo
}
async function refreshCurrentTcLoc() {
currentTcLoc.value = null
function buildBaseQuery() {
const q = new URLSearchParams()
q.set('day', options.castDayNo.toString())
const path = options.mapping[options.castBeginDate]
q.set('path', path)
return q
}
async function getHeatPoints() {
const q = buildBaseQuery()
const resp = await baseFetch<{
lat: number
lon: number
count: number
}[]>(`/tc/render/heatpoint?${q}`).json()
return resp.data.value
}
async function getImageUrl() {
const q = buildBaseQuery()
const resp = await baseFetch(`/tc/render?${q}`).blob()
// use URL.createObjectURL to avoid CORS issue
const blob = await resp.data.value
if (!blob) {
console.error(`no image for ${q}`)
return ''
}
const url = URL.createObjectURL(blob)
return url ?? ''
}
async function refreshCurrentTcLoc() {
currentTcLoc.value = null
const q = buildBaseQuery()
const base = import.meta.env.VITE_BACKEND_URL
const url = `${base}/tc/metadata/centroids?${q}`
@ -59,6 +112,12 @@ function _useOptions() {
const data = resp.data.value!
currentTcLoc.value = data as [number, number][]
if (shouldAutoNavigate.value) {
currentFocus.value = () => {
return data[0]
}
}
}
watch([castBeginDate, castTargetDate], () => {
@ -73,12 +132,16 @@ function _useOptions() {
return {
options,
setCastBeginDate,
interval,
setTargetDate,
getISOcastBeginDate,
getHeatPoints,
getImageUrl,
currentFocus,
castBeginDate,
castTargetDate,
currentTcLoc,
autoPlayer,
}
}

0
src/composables/utils.ts Normal file
View File

View File

@ -1,6 +1,5 @@
<script setup lang="ts">
const router = useRouter()
const { t } = useI18n()
</script>
<template>
@ -11,7 +10,7 @@ const { t } = useI18n()
<RouterView />
<div>
<button text-sm btn m="3 t8" @click="router.back()">
{{ t('button.back') }}
Back
</button>
</div>
</main>

View File

@ -1,5 +1,16 @@
<script setup lang="ts">
import { DrawerClose, DrawerTrigger } from 'vaul-vue'
import Colorbar from '../assets/colorbar.png'
const { autoPlayer, options, castBeginDate, castTargetDate } = useOptions()
const isPlaying = ref(false)
const dark = useDark()
function flipDark() {
dark.value = !dark.value
}
</script>
<template>
@ -8,10 +19,13 @@ import { DrawerClose, DrawerTrigger } from 'vaul-vue'
<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" />
<span class="i-mingcute-typhoon-fill size-6 px-5" @click="flipDark" />
<h1 class="text-xl font-semibold">
季节台风预报系统
季节台风预报系统
</h1>
<div class="size-5">
<span class="i-tabler-sun size-5" @click="flipDark" />
</div>
</header>
<main
class="grid flex-1 gap-4 overflow-auto p-4 lg:grid-cols-3 md:grid-cols-2"
@ -23,9 +37,13 @@ import { DrawerClose, DrawerTrigger } from 'vaul-vue'
<div
class="relative h-full min-h-[50vh] flex flex-col rounded-xl bg-muted/50 p-4 lg:col-span-2"
>
<Badge variant="outline" class="absolute left-3 top-3 z-20 bg-slate-100">
<Badge variant="outline" class="absolute left-3 top-3 z-20 bg-slate-100 dark:bg-black">
地图
</Badge>
<Card v-if="options.castDayNo !== -1" class="absolute right-3 top-3 z-400 p-3">
当前发布日期是 <br>{{ castBeginDate }}<br>
当前目标预报日期是<br> {{ castTargetDate }}
</Card>
<div class="flex-1" />
<RouterView />
<div
@ -47,58 +65,83 @@ import { DrawerClose, DrawerTrigger } from 'vaul-vue'
{{ 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>
<div>
<img :src="Colorbar" mx-auto max-h="60px">
</div>
<div grid="~ cols-2 gap-4">
<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" variant="outline" 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>
</div>
<div>
<Button
v-if="!isPlaying"
:disabled="options.castDayNo === -1" w-full @click.prevent="() => {
isPlaying = true
autoPlayer.resume()
}"
>
<span i-mdi-play />
{{
options.castDayNo === -1 ? "选择一个日期后可以自动播放"
: "开始自动播放"
}}
</Button>
<Button
v-else w-full @click.prevent="() => {
isPlaying = false
autoPlayer.pause()
}"
>
<span i-material-symbols-pause />
暂停自动播放
</Button>
</div>
</Card>
</div>
</div>

View File

@ -15,6 +15,7 @@ export const createApp = ViteSSG(
{
routes: setupLayouts(routes),
base: import.meta.env.BASE_URL,
},
(ctx) => {
// install all modules under `modules/`

View File

@ -1,94 +1,15 @@
<script setup lang="ts">
import AMapClient from '@/components/AMapClient.vue'
// import from 'vite-ssg'
</script>
<route lang="yaml">
meta:
layout: map
</route>
<script setup lang="ts">
import { useOptions } from '@/composables/dateOpt'
import AMapLoader from '@amap/amap-jsapi-loader'
import '@amap/amap-jsapi-types'
const amapInstance = ref<null | typeof AMap>(null)
const map = ref<null | AMap.Map>(null)
const previousLayer = ref<null | AMap.ImageLayer>(null)
const apiKey = import.meta.env.VITE_AMAP_API_KEY
const { options, currentFocus } = useOptions()
watch(options, async () => {
if (!map.value || !amapInstance.value) {
return
}
if (options.castBeginDate === '' || options.castDayNo === -1) {
return
}
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
}
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/render?${q}`
const image = new amapInstance.value.ImageLayer({
url,
// @ts-expect-error AMap messed up their definition
bounds: new AMap.Bounds([-180, -35], [180, 35]),
// bounds: [-150, -35, 180, 35],
zIndex: 9,
opacity: 1,
// zooms: [15, 20],
})
map.value.add(image)
image.on('complete', () => {
previousLayer.value?.hide()
previousLayer.value = image
})
}
onMounted(async () => {
const _AMap: typeof AMap = await AMapLoader.load({
key: apiKey,
version: '2.0',
plugins: [''],
})
amapInstance.value = _AMap
map.value = new _AMap.Map('amap-container', {
zoom: 4,
center: [130, 17],
zooms: [4, 20],
// layers: [
/* new AMap.TileLayer.Satellite() */
// new AMap.TileLayer.RoadNet(),
// ],
})
map.value.setBounds(new AMap.Bounds([-180, -35], [180, 35]))
map.value.setLimitBounds(new AMap.Bounds([-180, -35], [180, 35]))
map.value.setZoom(4)
})
</script>
<template>
<div id="amap-container" class="m-0 h-full w-full p-0" />
<ClientOnly>
<AMapClient />
</ClientOnly>
</template>

View File

@ -21,6 +21,7 @@ export default defineConfig({
resolve: {
alias: {
'@/': `${path.resolve(__dirname, 'src')}/`,
'~/': `${path.resolve(__dirname, 'src')}/`,
},
},