diff --git a/.env b/.env index 38050b0..7ad52f6 100644 --- a/.env +++ b/.env @@ -7,3 +7,7 @@ VITE_AMAP_JSAPI_KEY=f71bdfbf074620937b6b127128f49086 # 高德 Web Service Key(用于服务端 API,如地理编码、路径规划等) VITE_AMAP_WEB_KEY=6a30265609e6099d02eddc4e3085de5c + +# 高德安全密钥 — 2021/12/02 后申请的 Key 必须配置 +# 在 https://console.amap.com/dev/key/app 找到对应应用,复制「安全密钥」 +VITE_AMAP_SECURITY_CODE=999344a49e71dbbc97bcf59ba242bb50 diff --git a/src/composables/useAmap.ts b/src/composables/useAmap.ts index 004e898..76bc182 100644 --- a/src/composables/useAmap.ts +++ b/src/composables/useAmap.ts @@ -4,7 +4,7 @@ import { ref, shallowRef, onUnmounted } from 'vue' import AMapLoader from '@amap/amap-jsapi-loader' -import { AMAP_JSAPI_KEY, AMAP_VERSION, AMAP_PLUGINS } from '@/config/amap' +import { AMAP_JSAPI_KEY, AMAP_SECURITY_CODE, AMAP_VERSION, AMAP_PLUGINS } from '@/config/amap' /** 全局加载状态:避免重复加载 JSAPI 脚本 */ let amapPromise: Promise | null = null @@ -20,6 +20,15 @@ export async function loadAMap(): Promise { if (AMapGlobal) return AMapGlobal if (!amapPromise) { + // ⚠️ 安全密钥必须在 JSAPI 脚本加载之前设置 + // 2021/12/02 之后申请的 Key 必须配合 securityJsCode 使用 + // 否则 Driving / Geocoder 等服务会报 INVALID_USER_SCODE + if (AMAP_SECURITY_CODE && !(window as any)._AMapSecurityConfig) { + ;(window as any)._AMapSecurityConfig = { + securityJsCode: AMAP_SECURITY_CODE, + } + } + amapPromise = AMapLoader.load({ key: AMAP_JSAPI_KEY, version: AMAP_VERSION, diff --git a/src/config/amap.ts b/src/config/amap.ts index ef18ef2..e59b43f 100644 --- a/src/config/amap.ts +++ b/src/config/amap.ts @@ -9,14 +9,18 @@ export const AMAP_JSAPI_KEY = import.meta.env.VITE_AMAP_JSAPI_KEY as string /** 高德地图 Web Service Key — 用于服务端 API 调用 */ export const AMAP_WEB_KEY = import.meta.env.VITE_AMAP_WEB_KEY as string +/** 高德地图安全密钥 — 2021/12/02 后申请的 Key 必须配合此密钥使用 */ +export const AMAP_SECURITY_CODE = import.meta.env.VITE_AMAP_SECURITY_CODE as string + /** 高德地图 JSAPI 版本 */ export const AMAP_VERSION = '2.0' /** 需要加载的高德地图插件列表 */ export const AMAP_PLUGINS = [ - 'AMap.Geocoder', // 地理编码/逆地理编码 - 'AMap.AutoComplete', // 输入提示 - 'AMap.PlaceSearch', // 搜索服务 - 'AMap.Geolocation', // 定位 + 'AMap.Geocoder', // 地理编码/逆地理编码 + 'AMap.AutoComplete', // 输入提示 + 'AMap.PlaceSearch', // 搜索服务 + 'AMap.Geolocation', // 定位 'AMap.MarkerClusterer', // 点聚合 + 'AMap.Driving', // 驾车路线规划 ] as const diff --git a/src/views/MapView.vue b/src/views/MapView.vue index fdcb681..61dc78e 100644 --- a/src/views/MapView.vue +++ b/src/views/MapView.vue @@ -119,38 +119,126 @@ ➕ 添加老人 + + +
+ + 📍🏁 标记 + ● 就绪 + + +
+ + + + + + + + + + + +
+
+ + +
+ + 👩‍⚕️ 路线 + (未添加护理员) + +
+ + + + + + + + + {{ (routeInfo.distance / 1000).toFixed(0) }}km + + + + + + +
+
+ + +
+ + 🎯 点击地图放置{{ settingMode === 'origin' ? '起点(📍)' : '终点(🏁)' }}标记  |  再次点击按钮取消 + +
+ + +
+
+ 👩‍⚕️ 护理员移动中... {{ (animProgress * 100).toFixed(0) }}% +
@@ -177,6 +265,7 @@ import { onMounted, nextTick, onUnmounted } from 'vue' import { useAmap } from '@/composables/useAmap' import { useGeofence } from '@/composables/useGeofence' +import { useRouteTrack } from '@/composables/useRouteTrack' // ═══ 地图初始化 ═══ const { containerRef, mapInstance, loading, error, initMap } = useAmap({ @@ -218,9 +307,37 @@ const { destroyGeofence, } = useGeofence(mapInstance) +// ═══ 护理员轨迹 ═══ +const { + routeInfo, + isPlanning, + isAnimating, + animProgress, + settingMode, + hasCaregiver, + hasRoute, + hasOrigin, + hasDest, + canPlanRoute, + enterSetMode, + exitSetMode, + addCaregiver, + removeCaregiver, + removeOrigin, + removeDest, + planRoute, + clearRoute, + startAnimation, + stopAnimation, + resetToOrigin, + clearAll, + destroyRouteTrack, +} = useRouteTrack(mapInstance) + // ═══ 生命周期清理 ═══ onUnmounted(() => { destroyGeofence() + destroyRouteTrack() }) @@ -623,6 +740,86 @@ onUnmounted(() => { background: #f5f5f5; color: #666; } + +/* ── 路线按钮 ── */ +.btn-route { + border-color: #4A90D9 !important; + color: #4A90D9 !important; +} + +.btn-route:hover:not(:disabled) { + background: #4A90D9 !important; + color: #fff !important; +} + +.btn-start { + border-color: #27ae60 !important; + background: #27ae60 !important; + color: #fff !important; + font-weight: 600; +} + +.btn-start:hover:not(:disabled) { + background: #219a52 !important; +} + +.btn-reset { + border-color: #95a5a6 !important; + color: #95a5a6 !important; +} + +.btn-reset:hover:not(:disabled) { + background: #95a5a6 !important; + color: #fff !important; +} + +.route-summary { + font-size: 12px; + color: #4A90D9; + font-weight: 600; + white-space: nowrap; +} + +.route-ready { + font-size: 11px; + color: #27ae60; + margin-left: 4px; +} + +.route-planning { + font-size: 11px; + color: #faad14; + margin-left: 4px; + animation: status-alarm-pulse 0.6s ease-in-out infinite alternate; +} + +/* ── 动画进度条 ── */ +.route-progress-bar { + position: relative; + height: 22px; + background: #e8f0fe; + border-radius: 0; + overflow: hidden; +} + +.route-progress-fill { + height: 100%; + background: linear-gradient(90deg, #4A90D9, #357ABD); + transition: width 0.04s linear; + border-radius: 0; +} + +.route-progress-text { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 600; + color: #2c5f8a; + mix-blend-mode: multiply; +} @@ -686,4 +883,33 @@ onUnmounted(() => { cursor: grab; user-select: none; } + +/* ── 护理员标记 ── */ +.geofence-caregiver-marker { + width: 34px; + height: 34px; + border-radius: 50%; + background: linear-gradient(135deg, #4A90D9, #357ABD); + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + border: 2px solid #fff; + box-shadow: 0 2px 10px rgba(74, 144, 217, 0.4); + user-select: none; +} + +/* ── 终点旗帜标记 ── */ +.geofence-dest-marker { + font-size: 24px; + filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3)); + user-select: none; +} + +/* ── 起点标记 ── */ +.geofence-origin-marker { + font-size: 20px; + filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3)); + user-select: none; +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index c8bd138..927ade3 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -7,6 +7,8 @@ interface ImportMetaEnv { readonly VITE_AMAP_JSAPI_KEY: string readonly VITE_AMAP_WEB_KEY: string + /** 高德安全密钥 — 2021/12/02 后申请的 Key 必须配置,否则 Driving 等服务报 INVALID_USER_SCODE */ + readonly VITE_AMAP_SECURITY_CODE: string } interface ImportMeta { @@ -104,6 +106,15 @@ declare namespace AMap { constructor(map: Map, markers: any[], opts?: Record) } + // ── 经纬度 ── + class LngLat { + constructor(lng: number, lat: number) + lng: number + lat: number + getLng(): number + getLat(): number + } + // ── 覆盖物基类 ── class Overlay { setMap(map: Map | null): void @@ -240,6 +251,47 @@ declare namespace AMap { close(): void setContent(content: string): void } + + // ── 驾车路线规划 ── + interface DrivingOptions { + map?: Map + panel?: string | HTMLElement + policy?: number // 0-5 驾车策略 + [key: string]: any + } + + interface DrivingStep { + path: { lng: number; lat: number }[] + instruction: string + distance: number + [key: string]: any + } + + interface DrivingRoute { + steps: DrivingStep[] + distance: number + time: number + policy: string + [key: string]: any + } + + interface DrivingResult { + routes: DrivingRoute[] + info: string + origin: { lng: number; lat: number } + destination: { lng: number; lat: number } + [key: string]: any + } + + class Driving { + constructor(opts?: DrivingOptions) + search( + origin: [number, number] | { keyword: string; city?: string }, + destination: [number, number] | { keyword: string; city?: string }, + callback: (status: string, result: DrivingResult) => void + ): void + clear(): void + } } declare module '*.vue' {