many changes
This commit is contained in:
parent
5b800dcce8
commit
2598cdc3a9
2
.env.example
Normal file
2
.env.example
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
VITE_AMAP_API_KEY=xxx
|
||||||
|
VITE_BACKEND_URL=http://kuroneko.bison-banana.ts.net:35000
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
target
|
target
|
||||||
dist
|
dist
|
||||||
.env
|
.env
|
||||||
|
.vite-ssg-temp
|
||||||
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal 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}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -6,7 +6,7 @@
|
|||||||
"build": "vite-ssg build",
|
"build": "vite-ssg build",
|
||||||
"dev": "vite --port 3333 --open",
|
"dev": "vite --port 3333 --open",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview --port 35122",
|
||||||
"preview-https": "serve dist",
|
"preview-https": "serve dist",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:e2e": "cypress open",
|
"test:e2e": "cypress open",
|
||||||
@ -85,7 +85,8 @@
|
|||||||
"vite-ssg": "^25.0.0",
|
"vite-ssg": "^25.0.0",
|
||||||
"vite-ssg-sitemap": "^0.8.1",
|
"vite-ssg-sitemap": "^0.8.1",
|
||||||
"vitest": "^2.1.8",
|
"vitest": "^2.1.8",
|
||||||
"vue-tsc": "^2.2.0"
|
"vue-tsc": "^2.2.0",
|
||||||
|
"workbox-window": "^7.3.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"unplugin": "^2.1.2",
|
"unplugin": "^2.1.2",
|
||||||
|
|||||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@ -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))
|
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:
|
vite-plugin-pwa:
|
||||||
specifier: ^0.21.1
|
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:
|
vite-plugin-vue-devtools:
|
||||||
specifier: ^7.7.0
|
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))
|
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:
|
vue-tsc:
|
||||||
specifier: ^2.2.0
|
specifier: ^2.2.0
|
||||||
version: 2.2.0(typescript@5.7.3)
|
version: 2.2.0(typescript@5.7.3)
|
||||||
|
workbox-window:
|
||||||
|
specifier: ^7.3.0
|
||||||
|
version: 7.3.0
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@ -6179,6 +6182,9 @@ packages:
|
|||||||
workbox-core@7.1.0:
|
workbox-core@7.1.0:
|
||||||
resolution: {integrity: sha512-5KB4KOY8rtL31nEF7BfvU7FMzKT4B5TkbYa2tzkS+Peqj0gayMT9SytSFtNzlrvMaWgv6y/yvP9C0IbpFjV30Q==}
|
resolution: {integrity: sha512-5KB4KOY8rtL31nEF7BfvU7FMzKT4B5TkbYa2tzkS+Peqj0gayMT9SytSFtNzlrvMaWgv6y/yvP9C0IbpFjV30Q==}
|
||||||
|
|
||||||
|
workbox-core@7.3.0:
|
||||||
|
resolution: {integrity: sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw==}
|
||||||
|
|
||||||
workbox-expiration@7.1.0:
|
workbox-expiration@7.1.0:
|
||||||
resolution: {integrity: sha512-m5DcMY+A63rJlPTbbBNtpJ20i3enkyOtSgYfv/l8h+D6YbbNiA0zKEkCUaMsdDlxggla1oOfRkyqTvl5Ni5KQQ==}
|
resolution: {integrity: sha512-m5DcMY+A63rJlPTbbBNtpJ20i3enkyOtSgYfv/l8h+D6YbbNiA0zKEkCUaMsdDlxggla1oOfRkyqTvl5Ni5KQQ==}
|
||||||
|
|
||||||
@ -6212,6 +6218,9 @@ packages:
|
|||||||
workbox-window@7.1.0:
|
workbox-window@7.1.0:
|
||||||
resolution: {integrity: sha512-ZHeROyqR+AS5UPzholQRDttLFqGMwP0Np8MKWAdyxsDETxq3qOAyXvqessc3GniohG6e0mAqSQyKOHmT8zPF7g==}
|
resolution: {integrity: sha512-ZHeROyqR+AS5UPzholQRDttLFqGMwP0Np8MKWAdyxsDETxq3qOAyXvqessc3GniohG6e0mAqSQyKOHmT8zPF7g==}
|
||||||
|
|
||||||
|
workbox-window@7.3.0:
|
||||||
|
resolution: {integrity: sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA==}
|
||||||
|
|
||||||
wrap-ansi@6.2.0:
|
wrap-ansi@6.2.0:
|
||||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -13162,14 +13171,14 @@ snapshots:
|
|||||||
- rollup
|
- rollup
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
debug: 4.4.0(supports-color@8.1.1)
|
debug: 4.4.0(supports-color@8.1.1)
|
||||||
pretty-bytes: 6.1.1
|
pretty-bytes: 6.1.1
|
||||||
tinyglobby: 0.2.10
|
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)
|
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-build: 7.1.1
|
||||||
workbox-window: 7.1.0
|
workbox-window: 7.3.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@ -13485,6 +13494,8 @@ snapshots:
|
|||||||
|
|
||||||
workbox-core@7.1.0: {}
|
workbox-core@7.1.0: {}
|
||||||
|
|
||||||
|
workbox-core@7.3.0: {}
|
||||||
|
|
||||||
workbox-expiration@7.1.0:
|
workbox-expiration@7.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
idb: 7.1.1
|
idb: 7.1.1
|
||||||
@ -13540,6 +13551,11 @@ snapshots:
|
|||||||
'@types/trusted-types': 2.0.3
|
'@types/trusted-types': 2.0.3
|
||||||
workbox-core: 7.1.0
|
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:
|
wrap-ansi@6.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
// you can use this to manipulate the document head in any components,
|
// you can use this to manipulate the document head in any components,
|
||||||
// they will be rendered correctly in the html results with vite-ssg
|
// they will be rendered correctly in the html results with vite-ssg
|
||||||
useHead({
|
useHead({
|
||||||
title: 'Project Vertex',
|
title: '季节内台风预报系统',
|
||||||
meta: [
|
meta: [
|
||||||
{
|
{
|
||||||
name: 'description',
|
name: 'description',
|
||||||
|
|||||||
BIN
src/assets/colorbar.png
Normal file
BIN
src/assets/colorbar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
4
src/components.d.ts
vendored
4
src/components.d.ts
vendored
@ -7,6 +7,7 @@ export {}
|
|||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
AMapClient: typeof import('./components/AMapClient.vue')['default']
|
||||||
Badge: typeof import('./components/ui/badge/Badge.vue')['default']
|
Badge: typeof import('./components/ui/badge/Badge.vue')['default']
|
||||||
Button: typeof import('./components/ui/button/Button.vue')['default']
|
Button: typeof import('./components/ui/button/Button.vue')['default']
|
||||||
Calendar: typeof import('./components/ui/calendar/Calendar.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']
|
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']
|
PosForm: typeof import('./components/PosForm.vue')['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']
|
||||||
Select: typeof import('./components/ui/select/Select.vue')['default']
|
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']
|
TableHeader: typeof import('./components/ui/table/TableHeader.vue')['default']
|
||||||
TableRow: typeof import('./components/ui/table/TableRow.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']
|
|
||||||
TheFooter: typeof import('./components/TheFooter.vue')['default']
|
|
||||||
TheInput: typeof import('./components/TheInput.vue')['default']
|
TheInput: typeof import('./components/TheInput.vue')['default']
|
||||||
Tooltip: typeof import('./components/ui/tooltip/Tooltip.vue')['default']
|
Tooltip: typeof import('./components/ui/tooltip/Tooltip.vue')['default']
|
||||||
TooltipContent: typeof import('./components/ui/tooltip/TooltipContent.vue')['default']
|
TooltipContent: typeof import('./components/ui/tooltip/TooltipContent.vue')['default']
|
||||||
|
|||||||
162
src/components/AMapClient.vue
Normal file
162
src/components/AMapClient.vue
Normal 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>
|
||||||
@ -26,6 +26,15 @@ onMounted(async () => {
|
|||||||
const data = resp.data.value.data
|
const data = resp.data.value.data
|
||||||
mapping.value = data
|
mapping.value = data
|
||||||
options.mapping = 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(() => {
|
const isDateDisabled = computed(() => {
|
||||||
@ -68,10 +77,10 @@ const isTargetDateDisabled = computed(() => {
|
|||||||
<form class="grid w-full items-start gap-6">
|
<form class="grid w-full items-start gap-6">
|
||||||
<fieldset class="grid gap-6 border rounded-lg p-4">
|
<fieldset class="grid gap-6 border rounded-lg p-4">
|
||||||
<legend class="px-1 text-sm font-medium -ml-1">
|
<legend class="px-1 text-sm font-medium -ml-1">
|
||||||
日期设置
|
<span class="i-material-symbols-settings size-5" />
|
||||||
</legend>
|
</legend>
|
||||||
<div class="grid gap-3">
|
<div class="grid gap-3">
|
||||||
<Label for="model">渲染模式</Label>
|
<Label for="model">展示方式</Label>
|
||||||
<Select v-model="options.renderMode">
|
<Select v-model="options.renderMode">
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
id="model"
|
id="model"
|
||||||
@ -131,8 +140,12 @@ const isTargetDateDisabled = computed(() => {
|
|||||||
{{ castBeginDate ? df.format(castBeginDate.toDate(getLocalTimeZone())) : "Pick a date" }}
|
{{ castBeginDate ? df.format(castBeginDate.toDate(getLocalTimeZone())) : "Pick a date" }}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent class="z-300 w-auto p-0">
|
<PopoverContent class="w-aut1o z-300 p-0">
|
||||||
<Calendar v-model="castBeginDate" class="z-300" initial-focus :is-date-disabled="isDateDisabled" />
|
<Calendar
|
||||||
|
v-model="castBeginDate" class="z-300"
|
||||||
|
initial-focus :is-date-disabled="isDateDisabled"
|
||||||
|
prevent-deselect
|
||||||
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
@ -157,6 +170,7 @@ const isTargetDateDisabled = computed(() => {
|
|||||||
v-model="castTargetDate"
|
v-model="castTargetDate"
|
||||||
class="z-300" initial-focus
|
class="z-300" initial-focus
|
||||||
:is-date-disabled="isTargetDateDisabled"
|
:is-date-disabled="isTargetDateDisabled"
|
||||||
|
prevent-deselect
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@ -8,6 +8,20 @@ function jump(postion: [number, number]) {
|
|||||||
const getter = () => postion
|
const getter = () => postion
|
||||||
currentFocus.value = getter
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -16,7 +30,7 @@ function jump(postion: [number, number]) {
|
|||||||
预报台风
|
预报台风
|
||||||
</legend>
|
</legend>
|
||||||
<Table v-if="currentTcLoc">
|
<Table v-if="currentTcLoc">
|
||||||
<TableCaption>预报台风</TableCaption>
|
<!-- <TableCaption>预报台风</TableCaption> -->
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Lat </TableHead>
|
<TableHead>Lat </TableHead>
|
||||||
@ -28,8 +42,8 @@ function jump(postion: [number, number]) {
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow v-for="postion in currentTcLoc" :key="postion[0]">
|
<TableRow v-for="postion in currentTcLoc" :key="postion[0]">
|
||||||
<TableCell>{{ postion[0].toFixed(2) }}</TableCell>
|
<TableCell>{{ getPostionText(postion).getX() }}</TableCell>
|
||||||
<TableCell>{{ postion[1].toFixed(2) }}</TableCell>
|
<TableCell>{{ getPostionText(postion).getY() }}</TableCell>
|
||||||
<TableCell class="text-right">
|
<TableCell class="text-right">
|
||||||
<Button
|
<Button
|
||||||
class="h-6 text-[13.2px]" variant="outline"
|
class="h-6 text-[13.2px]" variant="outline"
|
||||||
|
|||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -2,20 +2,38 @@ import { type DateValue, parseDate } from '@internationalized/date'
|
|||||||
|
|
||||||
function _useOptions() {
|
function _useOptions() {
|
||||||
const options = reactive({
|
const options = reactive({
|
||||||
|
// it is inited as '', which means the date is not set
|
||||||
castBeginDate: '',
|
castBeginDate: '',
|
||||||
|
// it is inited as -1, which means the date is not set
|
||||||
castDayNo: -1,
|
castDayNo: -1,
|
||||||
renderMode: 'heatmap' as 'heatmap' | 'image',
|
renderMode: 'image' as 'heatmap' | 'image',
|
||||||
isPlaying: false,
|
// maps date to path
|
||||||
mapping: {} as Record<string, string>,
|
mapping: {} as Record<string, string>,
|
||||||
|
|
||||||
})
|
})
|
||||||
const castBeginDate = ref<DateValue>()
|
const castBeginDate = ref<DateValue>()
|
||||||
const castTargetDate = ref<DateValue>()
|
const castTargetDate = ref<DateValue>()
|
||||||
|
const interval = ref(3000)
|
||||||
|
|
||||||
const currentTcLoc = ref<null | [number, number][]>(null)
|
const currentTcLoc = ref<null | [number, number][]>(null)
|
||||||
type PosGetter = () => [number, number]
|
type PosGetter = () => [number, number]
|
||||||
const currentFocus = ref<null | PosGetter>(null)
|
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() {
|
function getISOcastBeginDate() {
|
||||||
const castBeginDateStr = options.castBeginDate.toString()
|
const castBeginDateStr = options.castBeginDate.toString()
|
||||||
if (castBeginDateStr.length !== 8) {
|
if (castBeginDateStr.length !== 8) {
|
||||||
@ -30,6 +48,13 @@ function _useOptions() {
|
|||||||
function setCastBeginDate(date: DateValue) {
|
function setCastBeginDate(date: DateValue) {
|
||||||
const datestr = date.toString()
|
const datestr = date.toString()
|
||||||
options.castBeginDate = datestr.replaceAll('-', '')
|
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) {
|
function setTargetDate(date: DateValue) {
|
||||||
@ -44,13 +69,41 @@ function _useOptions() {
|
|||||||
options.castDayNo = dayNo
|
options.castDayNo = dayNo
|
||||||
return dayNo
|
return dayNo
|
||||||
}
|
}
|
||||||
|
function buildBaseQuery() {
|
||||||
async function refreshCurrentTcLoc() {
|
|
||||||
currentTcLoc.value = null
|
|
||||||
const q = new URLSearchParams()
|
const q = new URLSearchParams()
|
||||||
q.set('day', options.castDayNo.toString())
|
q.set('day', options.castDayNo.toString())
|
||||||
const path = options.mapping[options.castBeginDate]
|
const path = options.mapping[options.castBeginDate]
|
||||||
q.set('path', path)
|
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 base = import.meta.env.VITE_BACKEND_URL
|
||||||
|
|
||||||
const url = `${base}/tc/metadata/centroids?${q}`
|
const url = `${base}/tc/metadata/centroids?${q}`
|
||||||
@ -59,6 +112,12 @@ function _useOptions() {
|
|||||||
|
|
||||||
const data = resp.data.value!
|
const data = resp.data.value!
|
||||||
currentTcLoc.value = data as [number, number][]
|
currentTcLoc.value = data as [number, number][]
|
||||||
|
|
||||||
|
if (shouldAutoNavigate.value) {
|
||||||
|
currentFocus.value = () => {
|
||||||
|
return data[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([castBeginDate, castTargetDate], () => {
|
watch([castBeginDate, castTargetDate], () => {
|
||||||
@ -73,12 +132,16 @@ function _useOptions() {
|
|||||||
return {
|
return {
|
||||||
options,
|
options,
|
||||||
setCastBeginDate,
|
setCastBeginDate,
|
||||||
|
interval,
|
||||||
setTargetDate,
|
setTargetDate,
|
||||||
getISOcastBeginDate,
|
getISOcastBeginDate,
|
||||||
|
getHeatPoints,
|
||||||
|
getImageUrl,
|
||||||
currentFocus,
|
currentFocus,
|
||||||
castBeginDate,
|
castBeginDate,
|
||||||
castTargetDate,
|
castTargetDate,
|
||||||
currentTcLoc,
|
currentTcLoc,
|
||||||
|
autoPlayer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
0
src/composables/utils.ts
Normal file
0
src/composables/utils.ts
Normal file
@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { t } = useI18n()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -11,7 +10,7 @@ const { t } = useI18n()
|
|||||||
<RouterView />
|
<RouterView />
|
||||||
<div>
|
<div>
|
||||||
<button text-sm btn m="3 t8" @click="router.back()">
|
<button text-sm btn m="3 t8" @click="router.back()">
|
||||||
{{ t('button.back') }}
|
Back
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@ -1,5 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DrawerClose, DrawerTrigger } from 'vaul-vue'
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -8,10 +19,13 @@ import { DrawerClose, DrawerTrigger } from 'vaul-vue'
|
|||||||
<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" />
|
<span class="i-mingcute-typhoon-fill size-6 px-5" @click="flipDark" />
|
||||||
<h1 class="text-xl font-semibold">
|
<h1 class="text-xl font-semibold">
|
||||||
次季节台风预报系统
|
季节内台风预报系统
|
||||||
</h1>
|
</h1>
|
||||||
|
<div class="size-5">
|
||||||
|
<span class="i-tabler-sun size-5" @click="flipDark" />
|
||||||
|
</div>
|
||||||
</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"
|
||||||
@ -23,9 +37,13 @@ import { DrawerClose, DrawerTrigger } from 'vaul-vue'
|
|||||||
<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"
|
||||||
>
|
>
|
||||||
<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>
|
</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" />
|
<div class="flex-1" />
|
||||||
<RouterView />
|
<RouterView />
|
||||||
<div
|
<div
|
||||||
@ -47,58 +65,83 @@ import { DrawerClose, DrawerTrigger } from 'vaul-vue'
|
|||||||
{{ currentTcLoc.length }} 个区域预报出台风
|
{{ currentTcLoc.length }} 个区域预报出台风
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
|
<div>
|
||||||
<Button w-full variant="outline">
|
<img :src="Colorbar" mx-auto max-h="60px">
|
||||||
开始自动播放
|
</div>
|
||||||
</Button>
|
<div grid="~ cols-2 gap-4">
|
||||||
<Drawer class="z-100">
|
<Drawer class="z-100">
|
||||||
<DrawerTrigger as-child>
|
<DrawerTrigger as-child>
|
||||||
<Button size="icon" variant="outline" class="w-full md:hidden">
|
<Button size="icon" variant="outline" class="w-full md:hidden">
|
||||||
<span class="i-mingcute-typhoon-fill size-4" />
|
<span class="i-mingcute-typhoon-fill size-4" />
|
||||||
<span class="">台风位置</span>
|
<span class="">台风列表</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DrawerTrigger>
|
</DrawerTrigger>
|
||||||
<DrawerContent class="z-200 max-h-[80vh] p-5">
|
<DrawerContent class="z-200 max-h-[80vh] p-5">
|
||||||
<DrawerHeader>
|
<DrawerHeader>
|
||||||
<DrawerTitle>台风位置</DrawerTitle>
|
<DrawerTitle>台风列表</DrawerTitle>
|
||||||
<DrawerDescription>
|
<DrawerDescription>
|
||||||
点击可以跳转
|
点击可以跳转
|
||||||
</DrawerDescription>
|
</DrawerDescription>
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
<PosForm />
|
<PosForm />
|
||||||
<DrawerClose as-child>
|
<DrawerClose as-child>
|
||||||
<div class="py-3">
|
<div class="py-3">
|
||||||
<Button class="w-full">
|
<Button class="w-full">
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DrawerClose>
|
</DrawerClose>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
<Drawer class="z-100">
|
<Drawer class="z-100">
|
||||||
<DrawerTrigger as-child>
|
<DrawerTrigger as-child>
|
||||||
<Button size="icon" class="w-full md:hidden">
|
<Button size="icon" variant="outline" class="w-full md:hidden">
|
||||||
<span class="i-material-symbols-settings size-4" />
|
<span class="i-material-symbols-settings size-4" />
|
||||||
<span class="">地图设置</span>
|
<span class="">日期设置</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DrawerTrigger>
|
</DrawerTrigger>
|
||||||
<DrawerContent class="z-200 max-h-[80vh] p-5">
|
<DrawerContent class="z-200 max-h-[80vh] p-5">
|
||||||
<DrawerHeader>
|
<DrawerHeader>
|
||||||
<DrawerTitle>地图设置</DrawerTitle>
|
<DrawerTitle>地图设置</DrawerTitle>
|
||||||
<DrawerDescription>
|
<DrawerDescription>
|
||||||
选择日期和渲染方式
|
选择日期和渲染方式
|
||||||
</DrawerDescription>
|
</DrawerDescription>
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
<OptForm />
|
<OptForm />
|
||||||
<DrawerClose as-child>
|
<DrawerClose as-child>
|
||||||
<div class="py-3">
|
<div class="py-3">
|
||||||
<Button class="w-full">
|
<Button class="w-full">
|
||||||
应用
|
应用
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DrawerClose>
|
</DrawerClose>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</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>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export const createApp = ViteSSG(
|
|||||||
{
|
{
|
||||||
routes: setupLayouts(routes),
|
routes: setupLayouts(routes),
|
||||||
base: import.meta.env.BASE_URL,
|
base: import.meta.env.BASE_URL,
|
||||||
|
|
||||||
},
|
},
|
||||||
(ctx) => {
|
(ctx) => {
|
||||||
// install all modules under `modules/`
|
// install all modules under `modules/`
|
||||||
|
|||||||
@ -1,94 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import AMapClient from '@/components/AMapClient.vue'
|
||||||
|
// import from 'vite-ssg'
|
||||||
|
</script>
|
||||||
|
|
||||||
<route lang="yaml">
|
<route lang="yaml">
|
||||||
meta:
|
meta:
|
||||||
layout: map
|
layout: map
|
||||||
</route>
|
</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>
|
<template>
|
||||||
<div id="amap-container" class="m-0 h-full w-full p-0" />
|
<ClientOnly>
|
||||||
|
<AMapClient />
|
||||||
|
</ClientOnly>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -21,6 +21,7 @@ export default defineConfig({
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@/': `${path.resolve(__dirname, 'src')}/`,
|
'@/': `${path.resolve(__dirname, 'src')}/`,
|
||||||
|
'~/': `${path.resolve(__dirname, 'src')}/`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user