diff --git a/src/components/navigation/GuideMapShell.vue b/src/components/navigation/GuideMapShell.vue index 034eb9e..4d3c5cc 100644 --- a/src/components/navigation/GuideMapShell.vue +++ b/src/components/navigation/GuideMapShell.vue @@ -38,7 +38,7 @@ - + - + - 馆内 + 馆内 - + 来馆 - + @@ -637,6 +649,8 @@ const showGuideFloatingActions = computed(() => ( && (!showGuideHomeDock.value || !homeSearchExpanded.value) )) +const isExplainQuickActionActive = computed(() => currentTab.value === 'explain') + const toRoutePointOption = (target: GuideRouteTarget): RoutePointOption => ({ poiId: target.poiId, name: target.name, @@ -1319,7 +1333,24 @@ const closeOutdoorNavPanel = () => { clearOutdoorNavState() } +const showOutdoorEntrancePreview = () => { + showOutdoorNavPanel.value = false + showRoutePlanner.value = false + selectedGuidePoi.value = null + isPoiCardCollapsed.value = false + isSimulatingRoute.value = false + clearRoutePreviewState() + clearOutdoorNavState() + is3DMode.value = false + guideOutdoorState.value = 'entrance' +} + const handleMoreOutdoorNav = () => { + if (!isOutdoorGuidePanelEnabled) { + showOutdoorEntrancePreview() + return + } + openOutdoorNavPanel() } @@ -2040,7 +2071,7 @@ const handleExplainBack = () => { backdrop-filter: blur(10px); } -.guide-quick-action.primary { +.guide-quick-action.active { background: var(--museum-accent); border-color: var(--museum-accent); } @@ -2057,7 +2088,7 @@ const handleExplainBack = () => { color: #151713; } -.guide-quick-action.primary .guide-quick-icon { +.guide-quick-action.active .guide-quick-icon { color: #0a0b08; } @@ -2139,10 +2170,23 @@ const handleExplainBack = () => { white-space: nowrap; } -.guide-quick-action-text.primary { +.guide-quick-action.active .guide-quick-action-text { color: #0a0b08; } +.guide-quick-action.active .guide-quick-pin::before { + content: ''; + position: absolute; + left: -7px; + top: -7px; + width: 24px; + height: 24px; + border: 1.5px solid currentColor; + border-radius: 50%; + opacity: 0; + animation: entrance-pin-pulse 1.6s ease-out infinite; +} + .route-sim-top-pill { position: absolute; top: 126px; @@ -2232,6 +2276,8 @@ const handleExplainBack = () => { background: rgba(255, 255, 255, 0.96); border: 1px solid #e5e6de; border-radius: 12px; + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.14); + animation: entrance-tip-in 0.34s ease-out both; z-index: 45; } @@ -2486,6 +2532,7 @@ const handleExplainBack = () => { border: 1px solid #ffffff; border-radius: 16px; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08); + animation: entrance-card-in 0.42s cubic-bezier(0.2, 0.82, 0.2, 1) both; z-index: 45; } @@ -2547,4 +2594,41 @@ const handleExplainBack = () => { color: var(--museum-accent); } +@keyframes entrance-tip-in { + from { + opacity: 0; + transform: translate3d(-10px, 8px, 0) scale(0.96); + } + + to { + opacity: 1; + transform: translate3d(0, 0, 0) scale(1); + } +} + +@keyframes entrance-card-in { + from { + opacity: 0; + transform: translate3d(0, 28px, 0); + } + + to { + opacity: 1; + transform: translate3d(0, 0, 0); + } +} + +@keyframes entrance-pin-pulse { + 0% { + opacity: 0.42; + transform: scale(0.68); + } + + 72%, + 100% { + opacity: 0; + transform: scale(1.36); + } +} + diff --git a/src/services/tencent/TencentMapService.ts b/src/services/tencent/TencentMapService.ts index f41858e..1c1c42e 100644 --- a/src/services/tencent/TencentMapService.ts +++ b/src/services/tencent/TencentMapService.ts @@ -6,6 +6,7 @@ const TENCENT_MAP_KEY = import.meta.env.VITE_TENCENT_MAP_KEY?.trim() || '' const TENCENT_MAP_BASE_URL = 'https://apis.map.qq.com/ws' const TENCENT_DIRECTION_BASE_URL = `${TENCENT_MAP_BASE_URL}/direction/v1` +const isH5BrowserRuntime = typeof window !== 'undefined' export interface TencentPoiResult { id: string @@ -42,6 +43,18 @@ const FALLBACK_MUSEUM_ENTRANCE: TencentPoiResult = { distance: 0 } +const createUnavailableSearchResult = (message = 'H5 端腾讯地图 WebService 搜索需服务端代理'): TencentSearchResult => ({ + status: -1, + message, + count: 0, + data: [] +}) + +const createUnavailableRouteResult = (message = 'H5 端腾讯地图路线规划需服务端代理'): RouteResult => ({ + status: -1, + message +}) + /** * 路线规划起点/终点坐标 */ @@ -158,6 +171,11 @@ export async function planWalkingRoute( from: RoutePoint, to: RoutePoint ): Promise { + if (isH5BrowserRuntime) { + console.info('H5 端跳过腾讯地图步行路线 WebService 请求,避免浏览器 CORS 拦截') + return createUnavailableRouteResult() + } + return new Promise((resolve) => { uni.request({ url: `${TENCENT_DIRECTION_BASE_URL}/walking`, @@ -231,6 +249,11 @@ export async function planDrivingRoute( from: RoutePoint, to: RoutePoint ): Promise { + if (isH5BrowserRuntime) { + console.info('H5 端跳过腾讯地图驾车路线 WebService 请求,避免浏览器 CORS 拦截') + return createUnavailableRouteResult() + } + return new Promise((resolve) => { uni.request({ url: `${TENCENT_DIRECTION_BASE_URL}/driving`, @@ -304,6 +327,11 @@ export async function planTransitRoute( from: RoutePoint, to: RoutePoint ): Promise { + if (isH5BrowserRuntime) { + console.info('H5 端跳过腾讯地图公交路线 WebService 请求,避免浏览器 CORS 拦截') + return createUnavailableRouteResult() + } + return new Promise((resolve) => { uni.request({ url: `${TENCENT_DIRECTION_BASE_URL}/transit`, @@ -388,6 +416,16 @@ export function searchPoi( pageIndex: number = 1, pageSize: number = 10 ): Promise { + if (isH5BrowserRuntime) { + console.info('H5 端跳过腾讯地图 POI WebService 搜索,避免浏览器 CORS 拦截:', { + keyword, + region, + pageIndex, + pageSize + }) + return Promise.resolve(createUnavailableSearchResult()) + } + const boundary = `region(${region},0)` return new Promise((resolve) => { @@ -432,6 +470,10 @@ export function searchPoi( * @returns 入口点位信息 */ export async function searchMuseumEntrance(): Promise { + if (isH5BrowserRuntime) { + return FALLBACK_MUSEUM_ENTRANCE + } + // 先搜索博物馆 const museumResult = await searchPoi('深圳自然博物馆', '深圳市', 1, 5) @@ -475,6 +517,16 @@ export function searchNearbyPoi( keyword: string, radius: number = 1000 ): Promise { + if (isH5BrowserRuntime) { + console.info('H5 端跳过腾讯地图周边 WebService 搜索,避免浏览器 CORS 拦截:', { + lat, + lng, + keyword, + radius + }) + return Promise.resolve(createUnavailableSearchResult()) + } + const boundary = `nearby(${lat},${lng},${radius})` return new Promise((resolve) => {