declare global { interface Window { T?: { Map: new (container: HTMLElement | string) => TiandituMap LngLat: new (lng: number, lat: number) => TiandituLngLat Marker: new (lnglat: TiandituLngLat, options?: { icon?: TiandituIcon }) => TiandituMarker Icon: new (options: { iconUrl: string iconSize: TiandituPoint iconAnchor?: TiandituPoint }) => TiandituIcon Point: new (x: number, y: number) => TiandituPoint Label: new (options: { text: string position: TiandituLngLat offset?: TiandituPoint }) => TiandituLabel Overlay: { extend: (proto: Record) => new ( lnglat: TiandituLngLat, options: SchoolMapOverlayOptions, ) => TiandituSchoolOverlay } } } } export interface SchoolMapOverlayOptions { name: string active?: boolean } export interface TiandituLngLat { lng: number lat: number } export interface TiandituMarker { addEventListener(type: string, handler: () => void): void } export interface TiandituLabel { addEventListener?(type: string, handler: () => void): void setStyle?(style: Record): void getElement?(): HTMLElement | null } export interface TiandituSchoolOverlay { addEventListener(type: string, handler: () => void): void setActive?(active: boolean): void } export interface TiandituIcon { // marker icon handle } export interface TiandituPoint { x: number y: number } export interface TiandituMap { centerAndZoom(lnglat: TiandituLngLat, zoom: number): void enableScrollWheelZoom(): void addOverLay(overlay: TiandituMarker | TiandituSchoolOverlay): void removeOverLay(overlay: TiandituMarker | TiandituSchoolOverlay): void setViewport?(points: TiandituLngLat[]): void clearOverLays?(): void addEventListener?(type: string, handler: () => void): void removeEventListener?(type: string, handler: () => void): void lngLatToLayerPoint?(lnglat: TiandituLngLat): TiandituPoint getPanes?(): { overlayPane: HTMLElement } } let loadPromise: Promise> | null = null let schoolMapOverlayClass: (new ( lnglat: TiandituLngLat, options: SchoolMapOverlayOptions, ) => TiandituSchoolOverlay) | null = null /** 天地图浏览器端 Key(国家测绘) */ const DEFAULT_TIANDITU_TK = 'cc3f61fa6cafe2f0ad440018d0f07b13' export function getTiandituKey(): string { return import.meta.env.VITE_TIANDITU_TK?.trim() || DEFAULT_TIANDITU_TK } export function loadTianditu(): Promise> { if (window.T) { return Promise.resolve(window.T) } if (!loadPromise) { loadPromise = new Promise((resolve, reject) => { const tk = getTiandituKey() if (!tk) { reject(new Error('未配置 VITE_TIANDITU_TK')) return } const script = document.createElement('script') script.src = `https://api.tianditu.gov.cn/api?v=4.0&tk=${encodeURIComponent(tk)}` script.async = true script.onload = () => { if (window.T) { resolve(window.T) } else { reject(new Error('天地图 SDK 加载失败')) } } script.onerror = () => reject(new Error('天地图脚本加载失败')) document.head.appendChild(script) }) } return loadPromise } /** 地图默认中心:苏州市 */ export const SUZHOU_MAP_CENTER = { lng: 120.585316, lat: 31.298886 } /** 默认缩放:以苏州为中心,首屏仅展示苏州市域 */ export const SUZHOU_MAP_ZOOM = 11 function escapeMapLabelHtml(text: string): string { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') } function getSchoolMapOverlayClass(T: NonNullable) { if (schoolMapOverlayClass) return schoolMapOverlayClass schoolMapOverlayClass = T.Overlay.extend({ initialize(lnglat: TiandituLngLat, options: SchoolMapOverlayOptions) { this.lnglat = lnglat this.options = options || { name: '' } }, onAdd(map: TiandituMap) { this.map = map const div = document.createElement('div') div.className = 'slake-map-school-marker' if (this.options.active) div.classList.add('is-active') div.setAttribute('role', 'button') div.setAttribute('tabindex', '0') div.innerHTML = '' + `${escapeMapLabelHtml(this.options.name.trim() || '—')}` this._div = div map.getPanes?.().overlayPane.appendChild(div) this._onMapChange = () => this.update() map.addEventListener?.('move', this._onMapChange) map.addEventListener?.('zoomend', this._onMapChange) this.update() }, onRemove() { const map = this.map as TiandituMap | null if (map && this._onMapChange) { map.removeEventListener?.('move', this._onMapChange) map.removeEventListener?.('zoomend', this._onMapChange) } if (this._div?.parentNode) { this._div.parentNode.removeChild(this._div) } this.map = null this._div = null }, update() { const map = this.map as TiandituMap | null if (!map?.lngLatToLayerPoint || !this._div) return const pos = map.lngLatToLayerPoint(this.lnglat) this._div.style.left = `${pos.x}px` this._div.style.top = `${pos.y}px` }, setActive(active: boolean) { this.options.active = active if (this._div) { this._div.classList.toggle('is-active', active) } }, addEventListener(type: string, handler: () => void) { if (this._div) { this._div.addEventListener(type, handler) } }, }) as new (lnglat: TiandituLngLat, options: SchoolMapOverlayOptions) => TiandituSchoolOverlay return schoolMapOverlayClass } /** 圆点 + 校名一体覆盖物(flex 横排,与原型一致) */ export function createSchoolMapOverlay( T: NonNullable, school: { name: string; longitude: number; latitude: number }, active = false, ): TiandituSchoolOverlay { const OverlayClass = getSchoolMapOverlayClass(T) return new OverlayClass(new T.LngLat(school.longitude, school.latitude), { name: school.name, active, }) } export function shortenSchoolMapLabel(name: string, max = 12): string { const t = name.trim() if (!t) return '—' return t.length > max ? `${t.slice(0, max)}…` : t } export function fitMapToSchools( map: TiandituMap, T: NonNullable, schools: { latitude: number; longitude: number }[], ) { if (!schools.length) return const points = schools.map((s) => new T.LngLat(s.longitude, s.latitude)) if (schools.length === 1) { map.centerAndZoom(points[0], SUZHOU_MAP_ZOOM) return } if (typeof map.setViewport === 'function') { map.setViewport(points) return } const lngs = schools.map((s) => s.longitude) const lats = schools.map((s) => s.latitude) const center = new T.LngLat( (Math.min(...lngs) + Math.max(...lngs)) / 2, (Math.min(...lats) + Math.max(...lats)) / 2, ) map.centerAndZoom(center, SUZHOU_MAP_ZOOM) } export {}