diff --git a/src/apps/maps/Maps.vue b/src/apps/maps/Maps.vue index 0ada1d8..63866bf 100644 --- a/src/apps/maps/Maps.vue +++ b/src/apps/maps/Maps.vue @@ -1 +1,197 @@ - + + + + + + + + + {{ r.name }} + {{ kindLabel(r.kind) }} + + + + 标准 + 卫星 + + + + + − + + ◎ + 离线地图 · 北京市(示意图) + + + + + diff --git a/src/apps/maps/index.ts b/src/apps/maps/index.ts index 526cd1a..7612d41 100644 --- a/src/apps/maps/index.ts +++ b/src/apps/maps/index.ts @@ -1,24 +1,8 @@ +// 地图应用 — Vue 组件化 import { h, render } from 'vue' -// 地图应用 — 从 js/apps3.js 手动转写 -import { el, clamp, debounce } from '../../utils' import { Apps, stdMenus } from '../../composables/useApps' 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 = { id: 'maps', name: '地图', icon: '/assets/icons/maps.png', w: 860, h: 580, minW: 520, minH: 360, @@ -26,8 +10,8 @@ export const MapsApp = { const st = win?.appState return stdMenus(this, { view: [ - { label: '标准', checked: st?.mode === 'std', action: () => st?.setMode('std') }, - { label: '卫星', checked: st?.mode === 'sat', action: () => st?.setMode('sat') }, + { label: '标准', checked: st?.mode?.value === 'std', action: () => st?.setMode('std') }, + { label: '卫星', checked: st?.mode?.value === 'sat', action: () => st?.setMode('sat') }, { sep: true }, { label: '放大', key: '⌘+', action: () => st?.zoomBy(1) }, { label: '缩小', key: '⌘−', action: () => st?.zoomBy(-1) }, @@ -35,85 +19,8 @@ export const MapsApp = { }) }, render(win: any) { - const st = win.appState = { mode: 'std', zoom: 3, cx: 0.5, cy: 0.48, pin: null as any } - win.body.classList.add('maps-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()) - } + const vnode = h(MapsComponent, { win }) + render(vnode, win.body) + }, } Apps.register(MapsApp)