import React, { useState, useEffect, useRef, useCallback } from 'react'; import { CheckSquare, Square, CheckCircle2, Map, Info, Clock, ExternalLink, Settings, Navigation, ChevronRight, Star } from 'lucide-react'; /* Global API rate limiting and queue management */ let apiCallCount = 0; const MAX_API_CALLS = 25; const callQueue = []; let isProcessingQueue = false; const processQueue = () => { if (isProcessingQueue || callQueue.length === 0) return; if (apiCallCount >= MAX_API_CALLS) { console.warn(`Google Places API limit reached (${MAX_API_CALLS} requests). Stopping further requests.`); /* Trigger custom event to show UI warning */ window.dispatchEvent(new CustomEvent('api-limit-reached')); return; } isProcessingQueue = true; const task = callQueue.shift(); apiCallCount++; task.execute(); /* 250ms delay between API calls to prevent OVER_QUERY_LIMIT */ setTimeout(() => { isProcessingQueue = false; processQueue(); }, 250); }; const enqueueApiCall = (execute) => { callQueue.push({ execute }); processQueue(); }; const tripData = [ { day: 1, date: "6月23日", title: "曼谷至考艾", drive: "约 2.5 - 3 小时", options: [ { id: "1-1", title: "地狱瀑布 (Haew Narok Waterfall)", desc: "令人难以置信的三层瀑布。通往观景台的铺砌小路相对较短,远低于您的1000米限制。" }, { id: "1-2", title: "Nong Phak Chi 野生动物观察塔", desc: "从公路步行一小段平坦的路即可到达观察塔,欣赏开阔草原的全景。" }, { id: "1-3", title: "PB Valley 考艾酒庄", desc: "乘坐电车享受风景优美的葡萄园之旅(几乎无需步行),欣赏周围群山的美景。" }, { id: "1-4", title: "Pa Deo Die 悬崖观景台", desc: "一直开车上山,然后走上一段非常短而平坦的木板路,即可到达一个拥有开阔山谷景色的壮丽悬崖。" }, { id: "1-5", title: "考艾艺术博物馆", desc: "一个平坦、风景优美的户外雕塑花园,背景是美丽的山脉。" } ] }, { day: 2, date: "6月24日", title: "考艾至那空那育", drive: "约 1.5 - 2 小时", confirmed: { id: "2-dam", title: "坤丹巴拉春水坝 (Khun Dan Prakarn Chon Dam)", desc: "驱车前往泰国最大的混凝土水坝,欣赏壮丽的全景。然后,从水坝直接乘长尾船进入水库,沿河而上,探寻隐藏的瀑布。" }, options: [ { id: "2-1", title: "Nang Rong 瀑布", desc: "乘船游览后,驱车前往这个瀑布,您可以将车停在瀑布旁边。" }, { id: "2-2", title: "Wang Ta Krai 自然公园", desc: "开车进入,停好车,立即坐在美丽茂盛的溪流旁。" }, { id: "2-3", title: "Sarika 瀑布", desc: "附近的另一个瀑布,其较低的几层非常靠近停车场,易于到达。" }, { id: "2-4", title: "Thung Bua Daeng", searchQuery: "Thung Bua Daeng Nakhon Nayok", desc: "在短途船上放松,或沿着一座短而平坦的桥走过一个开满红色睡莲的湖泊。" }, { id: "2-5", title: "Aden 农场 (那空那育)", desc: "一个非常平坦、易于步行的风景优美的农场区,有长长的竹桥横跨水面和五颜六色的雨伞装饰。" } ] }, { day: 3, date: "6月25日", title: "那空那育至尖竹汶", drive: "约 3.5 - 4 小时", options: [ { id: "3-1", title: "Noen Nang Phaya 观景台", desc: "在一个著名的海岸观景台停留。然后,走在 Kung Krabaen 湾的木板路上——这是一条完全平坦的、跨越红树林的木制小径。" }, { id: "3-2", title: "Namtok Phlio 国家公园", desc: "参观以水池中成千上万的鱼而闻名的 Phlio 瀑布。步道铺设良好,交通便利,轻松低于1000米。" }, { id: "3-3", title: "Taksin 大桥 (Laem Sing)", desc: "驾车穿过横跨河口的风景优美的悬索桥,然后在 Oasis Sea World 观看海豚表演(无需徒步)。" }, { id: "3-4", title: "Khao Laem Sing 森林公园", desc: "在海岸线附近停车,走一小段平路即可到达一个安静的海滩,欣赏壮丽的海景。" }, { id: "3-5", title: "圣母无染原罪主教座堂", searchQuery: "Cathedral of the Immaculate Conception Chanthaburi", desc: "一座美丽高耸的法式大教堂,位于平坦的河滨广场上,无需徒步即可欣赏到宏伟的建筑景观。" } ] }, { day: 4, date: "6月26日", title: "尖竹汶 (缓冲日)", drive: "本地探索", options: [ { id: "4-1", title: "Ao Khung Wiman 海滩驾车游", desc: "沿着海岸线进行一次壮丽的驾车之旅,在 Phra Stand 观景台停下来拍照,然后直接把车停在海滩上。" }, { id: "4-2", title: "Wat Pak Nam Khaem Nu", desc: "您旅程中可选的一座寺庙!它风景极其优美,完全覆盖着复杂的蓝白陶瓷。您可以将车停在它正前方。" }, { id: "4-3", title: "Namtok Trok Nong 瀑布", desc: "该地区一个易于到达的瀑布,拥有可爱的天然水池,从停车场步行很短的距离即可到达。" }, { id: "4-4", title: "海洋养殖示范单位", desc: "位于 Kung Krabaen,您可以沿着水上平坦的木制平台行走,观看和喂养海洋生物。" }, { id: "4-5", title: "Chao Lao 海滩", desc: "沿着尖竹汶最长的沙滩平行行驶。可以在路边的任何地方停车,立即进入海滩。" } ] }, { day: 5, date: "6月27日", title: "尖竹汶至象岛", drive: "约 1.5 小时车程 + 45 分钟渡轮", options: [ { id: "5-1", title: "Kai Bae 观景台", desc: "在西海岸的这个特定观景台停下来。从车上步行1分钟即可到达,并提供最佳的全景。" }, { id: "5-2", title: "白沙滩", searchQuery: "White Sand Beach Koh Chang", desc: "在主路上停车,直接走到沙滩上(无需徒步),欣赏美丽的海滨日落景色。" }, { id: "5-3", title: "Salak Phet 海湾", desc: "沿着安静的东海岸向下行驶至 Salak Phet。道路风景极其优美,您可以直接开车到水上的码头。" }, { id: "5-4", title: "象岛国家公园观景台", desc: "一个建在主路旁边的大型观景台。只需停好车,走20米即可欣赏到史诗般的海景。" }, { id: "5-5", title: "Klong Son 海湾", searchQuery: "Klong Son Bay Koh Chang", desc: "下船后,前往安静的北部海湾。这里可以非常方便地进入海滩,而且极其宁静。" } ] }, { day: 6, date: "6月28日", title: "象岛", drive: "本地岛屿探索", options: [ { id: "6-1", title: "Bang Bao 渔村", desc: "驱车前往岛屿南部,沿着完全建在水上的平坦水泥码头行走(总长不到1000米)。" }, { id: "6-2", title: "Salak Kok 红树林步道", desc: "一条完全平坦、短小的水泥木板路,穿过岛屿东侧一个宁静得惊人的红树林。" }, { id: "6-3", title: "象岛乘船游", desc: "直接从您的海滩租一艘小型私人船,环游附近的小岛。尽览美景,完全无需步行。" }, { id: "6-4", title: "Klong Plu 瀑布", desc: "步道维护良好,大部分路段平坦,直到最后一段,仍在您的1000米限制内,并提供一个很棒的游泳池。" }, { id: "6-5", title: "Than Mayom 瀑布", desc: "位于东海岸,这座历史悠久的瀑布从停车场步行比 Klong Plu 更短、更平坦。" } ] }, { day: 7, date: "6月29日", title: "象岛", drive: "本地岛屿探索", options: [ { id: "7-1", title: "Klong Prao 河口", desc: "在河流与大海交汇的河口附近停车。这是一个平坦的区域,可以欣赏到水和山脉的美景。" }, { id: "7-2", title: "孤独海滩 (Lonely Beach)", desc: "沿着海岸线向下行驶,停好车,走一小段路到沙滩,在海滩俱乐部放松。" }, { id: "7-3", title: "Salak Phet 红树林步道", desc: "以摄影闻名,这是一条完全平坦、鲜红色的木板路,穿过红树林延伸至大海。" }, { id: "7-4", title: "Rasta 观景台", desc: "南海岸一个非常酷、悠闲的观景台,您可以停车,坐下来喝一杯,俯瞰海湾。" }, { id: "7-5", title: "Chai Chet 海角", desc: "在 Chai Chet 度假区停车,然后平坦地走到岩石海角,欣赏无遮挡的日落景色。" } ] }, { day: 8, date: "6月30日", title: "象岛 (缓冲日)", drive: "本地岛屿探索", options: [ { id: "8-1", title: "Klong Nueng 瀑布", desc: "岛上最高的瀑布之一,位于较安静的东侧,较低的位置相对容易进入。" }, { id: "8-2", title: "Bailan 海湾", desc: "孤独海滩以南一个安静的、布满岩石和沙子的海湾。非常适合在海边度过一个宁静、轻松的下午。" }, { id: "8-3", title: "长滩 (Long Beach)", desc: "驱车前往岛屿的遥远东南角,找到这个偏远、美丽的海滩,几乎不需要步行。" }, { id: "8-4", title: "小象岛 (Koh Chang Noi)", desc: "从海岸线观赏北部的小岛。渡轮码头附近有方便的平坦观景点。" }, { id: "8-5", title: "珍珠海滩 (Pearl Beach)", desc: "西海岸一个独特的卵石海滩,非常适合快速停留和欣赏日落,而无需处理沙子。" } ] }, { day: 9, date: "7月1日", title: "象岛至罗勇", drive: "渡轮 + 约 2 - 2.5 小时车程", options: [ { id: "9-1", title: "Khao Laem Ya 国家公园", desc: "停车场旁边就是一个令人惊叹的木板路和岩石观景点,可以俯瞰大海和沙美岛。步行量极少。" }, { id: "9-2", title: "罗勇植物园", desc: "租一艘小型摩托艇,带您穿过令人难以置信的淹水森林和荷花池。无需步行即可欣赏到惊人的自然风光。" }, { id: "9-3", title: "Mae Phim 海滩", desc: "沿着 Mae Phim 海滩的风景优美的沿海公路行驶。可以在数英里的沙滩旁的任何地方停车,立即进入海滩。" }, { id:could not translate: "9-4", title: "Tung Prong Thong", desc: "一条广阔而美丽的木板路,穿过巨大的金色红树林。它完全平坦,您可以走进去500米然后返回。" }, { id: "9-5", title: "Suan Son 松树海滩", desc: "沿着一条独特的沿海公路行驶,路旁紧邻海滩,高大的松树成荫。从车里看风景非常优美。" } ] }, { day: 10, date: "7月2日", title: "罗勇至曼谷", drive: "约 2.5 - 3 小时", options: [ { id: "10-1", title: "Bang Saen 观景台", desc: "在返回的路上,开车上这座沿海小山,观看野生猴子和广阔的海景。" }, { id: "10-2", title: "春武里大峡谷", desc: "一个旧采石场变成了一个风景优美的湖泊。从停车场到观景边缘只需走很短的一段路。" }, { id: "10-3", title: "Bang Pu 休闲中心", desc: "走上一个长而完全平坦的、延伸到海上的码头,您可以在那里喂养成百上千只越冬的海鸥。" }, { id: "10-4", title: "北榄古城 (Samut Prakan Ancient City)", desc: "租一辆高尔夫球车,自己在一个巨大的户外公园里兜风,公园里有来自泰国各地的精美复制建筑(无需步行)。" }, { id: "10-5", title: "Bang Krachao 河畔", desc: "开车到湄南河畔的一家码头餐厅,就在曼谷的边缘。在还车前,立即坐在水边。" } ] } ]; const getRealisticImage = (title, id) => { const lower = title.toLowerCase(); let term = "nature,thailand"; if (lower.includes("beach") || lower.includes("bay") || lower.includes("cape")) term = "beach,thailand"; if (lower.includes("waterfall")) term = "waterfall,jungle"; if (lower.includes("viewpoint") || lower.includes("cliff")) term = "mountain,view,thailand"; if (lower.includes("mangrove") || lower.includes("garden")) term = "mangrove,forest"; if (lower.includes("dam") || lower.includes("lake")) term = "lake,reservoir"; return `https://loremflickr.com/400/250/${term}?lock=${id.replace('-', '')}`; }; const PlaceImage = ({ title, id, isSelected, isConfirmed, info, onSelect, searchQuery }) => { const defaultUrl = getRealisticImage(title, id); const displayUrl = info?.photoUrl || defaultUrl; const rating = info?.rating; const userRatingsTotal = info?.userRatingsTotal; const finalQuery = searchQuery || `${title} Thailand`; const mapLink = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(finalQuery)}`; return (
{title}
{/* Checkbox Overlay */} {!isConfirmed && (
{isSelected ? : } {isSelected ? '已选择' : '选择'}
)} {isConfirmed && (
已确认的计划
)} {/* Google Rating Overlay */} {rating && (
{rating} ({userRatingsTotal > 999 ? '1k+' : userRatingsTotal})
)} {/* External Map Link Button */} e.stopPropagation()} className="absolute bottom-3 right-3 z-20 flex items-center gap-1.5 bg-black/50 hover:bg-black/80 backdrop-blur-md px-3 py-1.5 rounded-lg text-white/90 text-xs font-medium transition-colors border border-white/20" > 地图与评论
); }; const MapView = ({ activeLocation, apiLoaded }) => { const mapRef = useRef(null); const mapInstance = useRef(null); const markerRef = useRef(null); useEffect(() => { if (!apiLoaded || !window.google || !mapRef.current) return; if (!mapInstance.current) { mapInstance.current = new window.google.maps.Map(mapRef.current, { center: { lat: 13.7563, lng: 100.5018 }, zoom: 6, styles: [ { featureType: "poi", stylers: [{ visibility: "off" }] }, { featureType: "transit", stylers: [{ visibility: "off" }] } ] }); } }, [apiLoaded]); useEffect(() => { if (!mapInstance.current || !window.google || !activeLocation || !activeLocation.location) return; /* Remove old marker */ if (markerRef.current) { markerRef.current.setMap(null); } /* Create new marker for just the single clicked location */ markerRef.current = new window.google.maps.Marker({ position: activeLocation.location, map: mapInstance.current, title: activeLocation.title, icon: { path: window.google.maps.SymbolPath.CIRCLE, fillColor: '#10b981', fillOpacity: 1, strokeColor: '#ffffff', strokeWeight: 2, scale: 14 }, animation: window.google.maps.Animation.DROP }); /* Smoothly pan and zoom to the single location */ mapInstance.current.panTo(activeLocation.location); mapInstance.current.setZoom(12); }, [activeLocation]); return (
{!apiLoaded && (

正在加载实时地图...

)} {!activeLocation && apiLoaded && (

地图已准备就绪

点击任意卡片在地图上查看。

)}
); }; export default function App() { const [selections, setSelections] = useState({}); const [placesInfo, setPlacesInfo] = useState({}); const [activeMapLocation, setActiveMapLocation] = useState(null); const [apiKey] = useState('AIzaSyCd9ILWS8BfAzMfh5ki0KTIypMRSWpDy7I'); const [apiLoaded, setApiLoaded] = useState(false); const [isMapOpen, setIsMapOpen] = useState(false); const [apiLimitReached, setApiLimitReached] = useState(false); /* Listen for the API limit event */ useEffect(() => { const handler = () => setApiLimitReached(true); window.addEventListener('api-limit-reached', handler); return () => window.removeEventListener('api-limit-reached', handler); }, []); useEffect(() => { if (apiKey && !window.google) { const script = document.createElement('script'); script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`; script.async = true; script.defer = true; script.onload = () => setApiLoaded(true); document.head.appendChild(script); } else if (window.google) { setApiLoaded(true); } }, [apiKey]); const handleActivatePlace = useCallback((id, title, dayIndex, isSelection, customQuery) => { /* 1. Set as selected if it's an option card */ if (isSelection) { setSelections(prev => ({ ...prev, [dayIndex]: id })); } /* 2. Open the map automatically if it's closed */ setIsMapOpen(true); /* 3. If we already fetched this place's API data before, just show it on the map and stop */ if (placesInfo[id]) { setActiveMapLocation(placesInfo[id]); return; } /* 4. Otherwise, queue EXACTLY ONE Google API call for this click */ if (!apiLoaded || !window.google) return; enqueueApiCall(() => { const finalSearchQuery = customQuery || `${title} Thailand`; const service = new window.google.maps.places.PlacesService(document.createElement('div')); service.findPlaceFromQuery({ query: finalSearchQuery, fields: ['photos', 'geometry', 'rating', 'user_ratings_total'] }, (results, status) => { if (status === window.google.maps.places.PlacesServiceStatus.OK && results && results[0]) { const newInfo = { location: results[0].geometry?.location, title: title, photoUrl: results[0].photos ? results[0].photos[0].getUrl({ maxWidth: 400 }) : null, rating: results[0].rating, userRatingsTotal: results[0].user_ratings_total }; setPlacesInfo(prev => ({ ...prev, [id]: newInfo })); setActiveMapLocation(newInfo); } }); }); }, [apiLoaded, placesInfo]); const scrollToBottom = () => { document.getElementById('final-itinerary')?.scrollIntoView({ behavior: 'smooth' }); }; const progress = Object.keys(selections).length; const totalDaysToSelect = tripData.length; return (
{/* Header */}

泰国自然环线

6月23日 - 7月2日 • 曼谷 ➔ 考艾 ➔ 那空那育 ➔ 尖竹汶 ➔ 象岛 ➔ 罗勇

少量步行 (<1000米) 租车路线 最多1座寺庙
{/* Progress Bar */}
选择进度: {progress} / {totalDaysToSelect} 天
{/* API Limit Warning Banner */} {apiLimitReached && (

已达到 Google API 速率限制 ({MAX_API_CALLS} 次请求)。应用程序已暂停连接以保护您的 Google Cloud 账单限额。未加载位置的地图图钉和图像将不会显示。刷新页面以重置。

)} {/* Main Content Layout */}
{/* Left Column: Itinerary List */}
{} {tripData.map((day, dIdx) => (

{day.day} {day.title}

{day.date}

车程: {day.drive}

{/* Confirmed Activity Card */} {day.confirmed && (

已确认的主要活动

handleActivatePlace(day.confirmed.id, day.confirmed.title, dIdx, false, day.confirmed.searchQuery)} >

{day.confirmed.title}

{day.confirmed.desc}

)}

选择今日的1个选项:

{day.options.map((opt) => { const isSelected = selections[dIdx] === opt.id; return (
handleActivatePlace(opt.id, opt.title, dIdx, true, opt.searchQuery)} />
handleActivatePlace(opt.id, opt.title, dIdx, true, opt.searchQuery)}>

{opt.title}

{opt.desc}

); })}
))}
{/* Right Column: Sticky Map (Only visible if isMapOpen) */} {isMapOpen && ( )}
{}

您选择的行程

查看您为曼谷至象岛环线所做的选择。

{progress === 0 ? (

您尚未选择任何选项。

向上滚动并点击卡片来规划您的旅程!

) : (
{tripData.map((day, idx) => { const selectedOptId = selections[idx]; const selectedOpt = day.options.find(o => o.id === selectedOptId); if (!day.confirmed && !selectedOpt) return null; /* Skip days with no selections */ return (
第 {day.day} 天 • {day.date}

{day.title}

{day.confirmed && (

{day.confirmed.title}

{day.confirmed.desc}

)} {selectedOpt && (

{selectedOpt.title}

{selectedOpt.desc}

)}
); })}
)}
); }