feat: 重构 maps — Vue 组件化(canvas 地图/搜索/POI/缩放)
This commit is contained in:
parent
0a2ca982bd
commit
af2d1341e5
@ -1 +1,197 @@
|
|||||||
<template><div></div></template>
|
<template>
|
||||||
|
<div class="maps-body">
|
||||||
|
<canvas ref="canvasEl" class="maps-canvas" @wheel.prevent="onWheel" @pointerdown="onPointerDown"></canvas>
|
||||||
|
<div class="maps-top">
|
||||||
|
<input class="text-input maps-search" type="search" placeholder="搜索地点或地址" v-model="query" @input="onSearch">
|
||||||
|
</div>
|
||||||
|
<div ref="resultsEl" :class="'maps-results' + (results.length ? '' : ' hidden')">
|
||||||
|
<div v-for="r in results" :key="r.name" class="maps-result" @click="goToPOI(r)">
|
||||||
|
<b>{{ r.name }}</b>
|
||||||
|
<small>{{ kindLabel(r.kind) }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="segmented maps-seg">
|
||||||
|
<button :class="{ on: mode === 'std' }" @click="setMode('std')">标准</button>
|
||||||
|
<button :class="{ on: mode === 'sat' }" @click="setMode('sat')">卫星</button>
|
||||||
|
</div>
|
||||||
|
<div class="maps-zoom-col">
|
||||||
|
<button class="maps-zoom" title="放大" @click="zoomBy(1)">+</button>
|
||||||
|
<button class="maps-zoom" title="缩小" @click="zoomBy(-1)">−</button>
|
||||||
|
</div>
|
||||||
|
<button class="maps-locate" title="我的位置" @click="goHome">◎</button>
|
||||||
|
<div class="maps-offline-tag">离线地图 · 北京市(示意图)</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { clamp } from '../../utils'
|
||||||
|
|
||||||
|
const props = defineProps<{ win: any }>()
|
||||||
|
|
||||||
|
// POI 数据
|
||||||
|
const POIS = [
|
||||||
|
{ name: '天安门广场', x: 0.50, y: 0.52, kind: 'landmark' },
|
||||||
|
{ name: '故宫博物院', x: 0.50, y: 0.45, kind: 'landmark' },
|
||||||
|
{ name: '王府井大街', x: 0.56, y: 0.50, kind: 'shopping' },
|
||||||
|
{ name: '北海公园', x: 0.44, y: 0.42, kind: 'park' },
|
||||||
|
{ name: '颐和园', x: 0.18, y: 0.18, kind: 'park' },
|
||||||
|
{ name: '北京站', x: 0.60, y: 0.62, kind: 'transit' },
|
||||||
|
{ name: '中关村', x: 0.30, y: 0.25, kind: 'shopping' },
|
||||||
|
{ name: '三里屯', x: 0.68, y: 0.42, kind: 'shopping' },
|
||||||
|
{ name: '奥林匹克公园', x: 0.52, y: 0.20, kind: 'park' },
|
||||||
|
{ name: '北京西站', x: 0.32, y: 0.66, kind: 'transit' },
|
||||||
|
{ name: '国贸 CBD', x: 0.66, y: 0.56, kind: 'landmark' },
|
||||||
|
{ name: '南锣鼓巷', x: 0.50, y: 0.40, kind: 'landmark' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const kindLabels: Record<string, string> = { park: '公园', transit: '车站', shopping: '商圈', landmark: '地标' }
|
||||||
|
function kindLabel(k: string) { return kindLabels[k] || k }
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
const mode = ref<'std' | 'sat'>('std')
|
||||||
|
const zoom = ref(3)
|
||||||
|
const cx = ref(0.5)
|
||||||
|
const cy = ref(0.48)
|
||||||
|
const pin = ref<any>(null)
|
||||||
|
const query = ref('')
|
||||||
|
const results = ref<any[]>([])
|
||||||
|
const canvasEl = ref<HTMLCanvasElement>()
|
||||||
|
const resultsEl = ref<HTMLElement>()
|
||||||
|
let timer: any = null
|
||||||
|
|
||||||
|
// canvas 上下文
|
||||||
|
let ctx: CanvasRenderingContext2D | null = null
|
||||||
|
let dpr = 2
|
||||||
|
|
||||||
|
function setMode(m: string) { mode.value = m as any; draw() }
|
||||||
|
function zoomBy(d: number) { zoom.value = clamp(zoom.value + d, 1, 6); draw() }
|
||||||
|
|
||||||
|
function goHome() {
|
||||||
|
cx.value = 0.5; cy.value = 0.52
|
||||||
|
pin.value = { name: '我的位置', x: 0.5, y: 0.52 }
|
||||||
|
draw()
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToPOI(poi: any) {
|
||||||
|
cx.value = poi.x; cy.value = poi.y
|
||||||
|
zoom.value = Math.max(zoom.value, 4)
|
||||||
|
pin.value = poi
|
||||||
|
query.value = poi.name
|
||||||
|
results.value = []
|
||||||
|
draw()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearch() {
|
||||||
|
const q = query.value.trim()
|
||||||
|
if (!q) { results.value = []; return }
|
||||||
|
results.value = POIS.filter(p => p.name.includes(q))
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWheel(e: WheelEvent) { zoomBy(e.deltaY < 0 ? 1 : -1) }
|
||||||
|
|
||||||
|
function onPointerDown(e: PointerEvent) {
|
||||||
|
const sx = e.clientX, sy = e.clientY, ox = cx.value, oy = cy.value
|
||||||
|
const move = (ev: PointerEvent) => {
|
||||||
|
const scale = Math.pow(1.7, zoom.value - 1)
|
||||||
|
cx.value = clamp(ox - (ev.clientX - sx) / (canvasEl.value!.width * scale / 2), 0, 1)
|
||||||
|
cy.value = clamp(oy - (ev.clientY - sy) / (canvasEl.value!.height * scale / 2), 0, 1)
|
||||||
|
draw()
|
||||||
|
}
|
||||||
|
const up = () => { document.removeEventListener('pointermove', move); document.removeEventListener('pointerup', up) }
|
||||||
|
document.addEventListener('pointermove', move)
|
||||||
|
document.addEventListener('pointerup', up)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toScreen(px: number, py: number): [number, number] {
|
||||||
|
const W = canvasEl.value!.width, H = canvasEl.value!.height
|
||||||
|
const scale = Math.pow(1.7, zoom.value - 1)
|
||||||
|
return [W / 2 + (px - cx.value) * W * scale / 2, H / 2 + (py - cy.value) * H * scale / 2]
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
if (!canvasEl.value) return
|
||||||
|
const c = canvasEl.value
|
||||||
|
const W = c.width = c.clientWidth * dpr
|
||||||
|
const H = c.height = c.clientHeight * dpr
|
||||||
|
ctx = c.getContext('2d')!
|
||||||
|
const sat = mode.value === 'sat'
|
||||||
|
ctx.fillStyle = sat ? '#1c2418' : '#e8e4da'; ctx.fillRect(0, 0, W, H)
|
||||||
|
const scale = Math.pow(1.7, zoom.value - 1)
|
||||||
|
|
||||||
|
// 水域
|
||||||
|
ctx.fillStyle = sat ? '#16202e' : '#a8c8e8'
|
||||||
|
;[[0.44, 0.42, 0.05, 0.035], [0.17, 0.17, 0.045, 0.05], [0.52, 0.21, 0.035, 0.03]].forEach(([lx, ly, rx, ry]) => {
|
||||||
|
const [x, y] = toScreen(lx!, ly!)
|
||||||
|
ctx!.beginPath(); ctx!.ellipse(x, y, rx! * W * scale / 2, ry! * W * scale / 2, 0, 0, Math.PI * 2); ctx!.fill()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 公园
|
||||||
|
ctx.fillStyle = sat ? '#22331f' : '#b8d9a8'
|
||||||
|
;[[0.44, 0.42, 0.045], [0.52, 0.20, 0.05], [0.18, 0.18, 0.05]].forEach(([px, py, r]) => {
|
||||||
|
const [x, y] = toScreen(px!, py!)
|
||||||
|
ctx!.beginPath(); ctx!.arc(x, y, r! * W * scale / 2, 0, Math.PI * 2); ctx!.fill()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 道路
|
||||||
|
const main = sat ? '#4a4640' : '#ffffff', ring = sat ? '#57534b' : '#f8d67c'
|
||||||
|
for (let i = 1; i < 9; i++) { road(i / 9, 0.05, i / 9, 0.95, 3, main) }
|
||||||
|
for (let j = 1; j < 8; j++) { road(0.05, j / 8, 0.95, j / 8, 3, main) }
|
||||||
|
road(0.1, 0.52, 0.9, 0.52, 6, ring); road(0.5, 0.08, 0.5, 0.92, 5, ring)
|
||||||
|
|
||||||
|
// 二环
|
||||||
|
ctx.strokeStyle = ring; ctx.lineWidth = 4 * dpr
|
||||||
|
const [ccx, ccy] = toScreen(0.5, 0.5)
|
||||||
|
ctx.beginPath(); ctx.arc(ccx, ccy, 0.16 * W * scale / 2, 0, Math.PI * 2); ctx.stroke()
|
||||||
|
|
||||||
|
// 标签
|
||||||
|
if (zoom.value >= 3) {
|
||||||
|
ctx.fillStyle = sat ? '#b8b2a4' : '#8a8578'; ctx.font = `${11 * dpr}px sans-serif`
|
||||||
|
ctx.fillText('长安街', ...toScreen(0.42, 0.505))
|
||||||
|
ctx.fillText('中轴路', ...toScreen(0.505, 0.3))
|
||||||
|
ctx.fillText('二环路', ...toScreen(0.63, 0.36))
|
||||||
|
}
|
||||||
|
|
||||||
|
// POI
|
||||||
|
POIS.forEach((p: any) => {
|
||||||
|
const [x, y] = toScreen(p.x, p.y)
|
||||||
|
if (x < -60 || y < -60 || x > W + 60 || y > H + 60) return
|
||||||
|
ctx!.fillStyle = ({ park: '#3f9d4e', transit: '#3a7bd5', shopping: '#c86dd7', landmark: '#d5663a' } as any)[p.kind]
|
||||||
|
ctx!.beginPath(); ctx!.arc(x, y, 5 * dpr, 0, Math.PI * 2); ctx!.fill()
|
||||||
|
if (zoom.value >= 2) { ctx!.fillStyle = sat ? '#e8e4da' : '#3a372f'; ctx!.font = `${10.5 * dpr}px sans-serif`; ctx!.fillText(p.name, x + 8 * dpr, y + 4 * dpr) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 大头针
|
||||||
|
if (pin.value) {
|
||||||
|
const [x, y] = toScreen(pin.value.x, pin.value.y)
|
||||||
|
ctx.fillStyle = '#ff453a'
|
||||||
|
ctx.beginPath(); ctx.arc(x, y - 14 * dpr, 8 * dpr, 0, Math.PI * 2); ctx.fill()
|
||||||
|
ctx.beginPath(); ctx.moveTo(x - 6 * dpr, y - 9 * dpr); ctx.lineTo(x, y + 2 * dpr); ctx.lineTo(x + 6 * dpr, y - 9 * dpr); ctx.fill()
|
||||||
|
ctx.fillStyle = '#fff'; ctx.font = `bold ${11 * dpr}px sans-serif`
|
||||||
|
ctx.fillText(pin.value.name, x + 12 * dpr, y - 10 * dpr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function road(x1: number, y1: number, x2: number, y2: number, w: number, color: string) {
|
||||||
|
if (!ctx) return
|
||||||
|
const [ax, ay] = toScreen(x1, y1), [bx, by] = toScreen(x2, y2)
|
||||||
|
ctx.strokeStyle = color; ctx.lineWidth = w * dpr * (0.6 + zoom.value * 0.3); ctx.lineCap = 'round'
|
||||||
|
ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露给 menus
|
||||||
|
props.win.appState = { mode, setMode, zoomBy }
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(() => {
|
||||||
|
dpr = window.devicePixelRatio || 1
|
||||||
|
draw()
|
||||||
|
timer = setInterval(() => {
|
||||||
|
if (canvasEl.value && canvasEl.value.width !== canvasEl.value.clientWidth * dpr) draw()
|
||||||
|
}, 500)
|
||||||
|
;(props.win as any)._mapsTimer = timer
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => { clearInterval(timer) })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,24 +1,8 @@
|
|||||||
|
// 地图应用 — Vue 组件化
|
||||||
import { h, render } from 'vue'
|
import { h, render } from 'vue'
|
||||||
// 地图应用 — 从 js/apps3.js 手动转写
|
|
||||||
import { el, clamp, debounce } from '../../utils'
|
|
||||||
import { Apps, stdMenus } from '../../composables/useApps'
|
import { Apps, stdMenus } from '../../composables/useApps'
|
||||||
import MapsComponent from './Maps.vue'
|
import MapsComponent from './Maps.vue'
|
||||||
|
|
||||||
const MAP_POIS = [
|
|
||||||
{ name: '天安门广场', x: 0.50, y: 0.52, kind: 'landmark' },
|
|
||||||
{ name: '故宫博物院', x: 0.50, y: 0.45, kind: 'landmark' },
|
|
||||||
{ name: '王府井大街', x: 0.56, y: 0.50, kind: 'shopping' },
|
|
||||||
{ name: '北海公园', x: 0.44, y: 0.42, kind: 'park' },
|
|
||||||
{ name: '颐和园', x: 0.18, y: 0.18, kind: 'park' },
|
|
||||||
{ name: '北京站', x: 0.60, y: 0.62, kind: 'transit' },
|
|
||||||
{ name: '中关村', x: 0.30, y: 0.25, kind: 'shopping' },
|
|
||||||
{ name: '三里屯', x: 0.68, y: 0.42, kind: 'shopping' },
|
|
||||||
{ name: '奥林匹克公园', x: 0.52, y: 0.20, kind: 'park' },
|
|
||||||
{ name: '北京西站', x: 0.32, y: 0.66, kind: 'transit' },
|
|
||||||
{ name: '国贸 CBD', x: 0.66, y: 0.56, kind: 'landmark' },
|
|
||||||
{ name: '南锣鼓巷', x: 0.50, y: 0.40, kind: 'landmark' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const MapsApp = {
|
export const MapsApp = {
|
||||||
id: 'maps', name: '地图', icon: '/assets/icons/maps.png',
|
id: 'maps', name: '地图', icon: '/assets/icons/maps.png',
|
||||||
w: 860, h: 580, minW: 520, minH: 360,
|
w: 860, h: 580, minW: 520, minH: 360,
|
||||||
@ -26,8 +10,8 @@ export const MapsApp = {
|
|||||||
const st = win?.appState
|
const st = win?.appState
|
||||||
return stdMenus(this, {
|
return stdMenus(this, {
|
||||||
view: [
|
view: [
|
||||||
{ label: '标准', checked: st?.mode === 'std', action: () => st?.setMode('std') },
|
{ label: '标准', checked: st?.mode?.value === 'std', action: () => st?.setMode('std') },
|
||||||
{ label: '卫星', checked: st?.mode === 'sat', action: () => st?.setMode('sat') },
|
{ label: '卫星', checked: st?.mode?.value === 'sat', action: () => st?.setMode('sat') },
|
||||||
{ sep: true },
|
{ sep: true },
|
||||||
{ label: '放大', key: '⌘+', action: () => st?.zoomBy(1) },
|
{ label: '放大', key: '⌘+', action: () => st?.zoomBy(1) },
|
||||||
{ label: '缩小', key: '⌘−', action: () => st?.zoomBy(-1) },
|
{ label: '缩小', key: '⌘−', action: () => st?.zoomBy(-1) },
|
||||||
@ -35,85 +19,8 @@ export const MapsApp = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
render(win: any) {
|
render(win: any) {
|
||||||
const st = win.appState = { mode: 'std', zoom: 3, cx: 0.5, cy: 0.48, pin: null as any }
|
const vnode = h(MapsComponent, { win })
|
||||||
win.body.classList.add('maps-body')
|
render(vnode, win.body)
|
||||||
const search = el('input', { class: 'text-input maps-search', type: 'search', placeholder: '搜索地点或地址' }) as HTMLInputElement
|
},
|
||||||
const results = el('div', { class: 'maps-results hidden' })
|
|
||||||
const seg = el('div', { class: 'segmented maps-seg' },
|
|
||||||
el('button', { text: '标准', class: 'on', onclick: () => { st.setMode('std'); (seg.children[0] as HTMLElement).classList.add('on'); (seg.children[1] as HTMLElement).classList.remove('on') } }),
|
|
||||||
el('button', { text: '卫星', onclick: () => { st.setMode('sat'); (seg.children[1] as HTMLElement).classList.add('on'); (seg.children[0] as HTMLElement).classList.remove('on') } }))
|
|
||||||
const zIn = el('button', { class: 'maps-zoom', text: '+', title: '放大' })
|
|
||||||
const zOut = el('button', { class: 'maps-zoom', text: '−', title: '缩小' })
|
|
||||||
const locate = el('button', { class: 'maps-locate', html: '◎', title: '我的位置' })
|
|
||||||
const canvas = el('canvas', { class: 'maps-canvas' }) as HTMLCanvasElement
|
|
||||||
const offlineTag = el('div', { class: 'maps-offline-tag', text: '离线地图 · 北京市(示意图)' })
|
|
||||||
win.body.append(canvas, el('div', { class: 'maps-top' }, search), results, seg,
|
|
||||||
el('div', { class: 'maps-zoom-col' }, zIn, zOut), locate, offlineTag)
|
|
||||||
const ctx = canvas.getContext('2d')!
|
|
||||||
const devicePixelRatio = window.devicePixelRatio || 1
|
|
||||||
st.setMode = (m: string) => { st.mode = m; draw() }
|
|
||||||
st.zoomBy = (d: number) => { st.zoom = clamp(st.zoom + d, 1, 6); draw() }
|
|
||||||
zIn.addEventListener('click', () => st.zoomBy(1))
|
|
||||||
zOut.addEventListener('click', () => st.zoomBy(-1))
|
|
||||||
locate.addEventListener('click', () => { st.cx = 0.5; st.cy = 0.52; st.pin = { name: '我的位置', x: 0.5, y: 0.52 }; draw() })
|
|
||||||
canvas.addEventListener('wheel', (e: WheelEvent) => { e.preventDefault(); st.zoomBy(e.deltaY < 0 ? 1 : -1) }, { passive: false })
|
|
||||||
canvas.addEventListener('pointerdown', (e: PointerEvent) => {
|
|
||||||
const sx = e.clientX, sy = e.clientY, ox = st.cx, oy = st.cy
|
|
||||||
const scale = Math.pow(1.7, st.zoom - 1)
|
|
||||||
const move = (ev: PointerEvent) => { st.cx = clamp(ox - (ev.clientX - sx) / (canvas.width * scale / 2), 0, 1); st.cy = clamp(oy - (ev.clientY - sy) / (canvas.height * scale / 2), 0, 1); draw() }
|
|
||||||
const up = () => { document.removeEventListener('pointermove', move); document.removeEventListener('pointerup', up) }
|
|
||||||
document.addEventListener('pointermove', move); document.addEventListener('pointerup', up)
|
|
||||||
})
|
|
||||||
const toScreen = (px: number, py: number) => {
|
|
||||||
const scale = Math.pow(1.7, st.zoom - 1)
|
|
||||||
return [canvas.width / 2 + (px - st.cx) * canvas.width * scale / 2, canvas.height / 2 + (py - st.cy) * canvas.height * scale / 2]
|
|
||||||
}
|
|
||||||
const draw = () => {
|
|
||||||
const W = canvas.width = canvas.clientWidth * devicePixelRatio
|
|
||||||
const H = canvas.height = canvas.clientHeight * devicePixelRatio
|
|
||||||
const sat = st.mode === 'sat'
|
|
||||||
ctx.fillStyle = sat ? '#1c2418' : '#e8e4da'; ctx.fillRect(0, 0, W, H)
|
|
||||||
const scale = Math.pow(1.7, st.zoom - 1)
|
|
||||||
ctx.fillStyle = sat ? '#16202e' : '#a8c8e8'
|
|
||||||
const lake = (cx: number, cy: number, rx: number, ry: number) => { const [x, y] = toScreen(cx, cy); ctx.beginPath(); ctx.ellipse(x, y, rx * W * scale / 2, ry * W * scale / 2, 0, 0, Math.PI * 2); ctx.fill() }
|
|
||||||
lake(0.44, 0.42, 0.05, 0.035); lake(0.17, 0.17, 0.045, 0.05); lake(0.52, 0.21, 0.035, 0.03)
|
|
||||||
ctx.fillStyle = sat ? '#22331f' : '#b8d9a8'
|
|
||||||
const park = (cx: number, cy: number, r: number) => { const [x, y] = toScreen(cx, cy); ctx.beginPath(); ctx.arc(x, y, r * W * scale / 2, 0, Math.PI * 2); ctx.fill() }
|
|
||||||
park(0.44, 0.42, 0.045); park(0.52, 0.20, 0.05); park(0.18, 0.18, 0.05)
|
|
||||||
const road = (x1: number, y1: number, x2: number, y2: number, w: number, color: string) => {
|
|
||||||
const [ax, ay] = toScreen(x1, y1), [bx, by] = toScreen(x2, y2)
|
|
||||||
ctx.strokeStyle = color; ctx.lineWidth = w * devicePixelRatio * (0.6 + st.zoom * 0.3); ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke()
|
|
||||||
}
|
|
||||||
const main = sat ? '#4a4640' : '#ffffff', ring = sat ? '#57534b' : '#f8d67c'
|
|
||||||
for (let i = 1; i < 9; i++) road(i / 9, 0.05, i / 9, 0.95, 3, main)
|
|
||||||
for (let j = 1; j < 8; j++) road(0.05, j / 8, 0.95, j / 8, 3, main)
|
|
||||||
road(0.1, 0.52, 0.9, 0.52, 6, ring); road(0.5, 0.08, 0.5, 0.92, 5, ring)
|
|
||||||
ctx.strokeStyle = ring; ctx.lineWidth = 4 * devicePixelRatio
|
|
||||||
const [ccx, ccy] = toScreen(0.5, 0.5); ctx.beginPath(); ctx.arc(ccx, ccy, 0.16 * W * scale / 2, 0, Math.PI * 2); ctx.stroke()
|
|
||||||
if (st.zoom >= 3) { ctx.fillStyle = sat ? '#b8b2a4' : '#8a8578'; ctx.font = `${11 * devicePixelRatio}px sans-serif`; ctx.fillText('长安街', toScreen(0.42, 0.505)[0], toScreen(0.42, 0.505)[1]); ctx.fillText('中轴路', toScreen(0.505, 0.3)[0], toScreen(0.505, 0.3)[1]); ctx.fillText('二环路', toScreen(0.63, 0.36)[0], toScreen(0.63, 0.36)[1]) }
|
|
||||||
MAP_POIS.forEach((p: any) => {
|
|
||||||
const [x, y] = toScreen(p.x, p.y); if (x < -60 || y < -60 || x > W + 60 || y > H + 60) return
|
|
||||||
ctx.fillStyle = ({ park: '#3f9d4e', transit: '#3a7bd5', shopping: '#c86dd7', landmark: '#d5663a' } as any)[p.kind]
|
|
||||||
ctx.beginPath(); ctx.arc(x, y, 5 * devicePixelRatio, 0, Math.PI * 2); ctx.fill()
|
|
||||||
if (st.zoom >= 2) { ctx.fillStyle = sat ? '#e8e4da' : '#3a372f'; ctx.font = `${10.5 * devicePixelRatio}px sans-serif`; ctx.fillText(p.name, x + 8 * devicePixelRatio, y + 4 * devicePixelRatio) }
|
|
||||||
})
|
|
||||||
if (st.pin) { const [x, y] = toScreen(st.pin.x, st.pin.y); ctx.fillStyle = '#ff453a'; ctx.beginPath(); ctx.arc(x, y - 14 * devicePixelRatio, 8 * devicePixelRatio, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.moveTo(x - 6 * devicePixelRatio, y - 9 * devicePixelRatio); ctx.lineTo(x, y + 2 * devicePixelRatio); ctx.lineTo(x + 6 * devicePixelRatio, y - 9 * devicePixelRatio); ctx.fill(); ctx.fillStyle = '#fff'; ctx.font = `bold ${11 * devicePixelRatio}px sans-serif`; ctx.fillText(st.pin.name, x + 12 * devicePixelRatio, y - 10 * devicePixelRatio) }
|
|
||||||
}
|
|
||||||
win.timers.push(setInterval(() => { if (canvas.width !== canvas.clientWidth * devicePixelRatio) draw() }, 500))
|
|
||||||
setTimeout(draw, 30)
|
|
||||||
search.addEventListener('input', debounce(() => {
|
|
||||||
const q = search.value.trim(); results.innerHTML = ''
|
|
||||||
if (!q) { results.classList.add('hidden'); return }
|
|
||||||
const hits = MAP_POIS.filter((p: any) => p.name.includes(q))
|
|
||||||
results.classList.remove('hidden')
|
|
||||||
if (!hits.length) results.append(el('div', { class: 'maps-result', text: '本地结果中没有"' + q + '"(离线地图)' }))
|
|
||||||
hits.forEach((p: any) => {
|
|
||||||
const r = el('div', { class: 'maps-result' }, el('b', { text: p.name }), el('small', { text: ({ park: '公园', transit: '车站', shopping: '商圈', landmark: '地标' } as any)[p.kind] }))
|
|
||||||
r.addEventListener('click', () => { st.cx = p.x; st.cy = p.y; st.zoom = Math.max(st.zoom, 4); st.pin = p; results.classList.add('hidden'); search.value = p.name; draw() })
|
|
||||||
results.append(r)
|
|
||||||
})
|
|
||||||
}, 200))
|
|
||||||
search.addEventListener('keydown', (e: KeyboardEvent) => e.stopPropagation())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Apps.register(MapsApp)
|
Apps.register(MapsApp)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user