diff --git a/romantic-map-site/index.html b/romantic-map-site/index.html new file mode 100644 index 00000000..3dcb3aa3 --- /dev/null +++ b/romantic-map-site/index.html @@ -0,0 +1,117 @@ + + + + + + 星光旅途 · 浪漫中国地图 + + + + + + +
+
+

给 TA 的惊喜礼物

+

星光旅途 · 立体中国浪漫地图

+

点亮每一座我们去过的城市,让回忆成为永恒的星座。

+
+ +
+
+
+
+ + + + + + + + + + +
+
+
+ +
+ + +
+ + + + + + + + diff --git a/romantic-map-site/script.js b/romantic-map-site/script.js new file mode 100644 index 00000000..a7ab5266 --- /dev/null +++ b/romantic-map-site/script.js @@ -0,0 +1,199 @@ +const cities = [ + { + name: "北京", + province: "北京", + position: { x: 55, y: 30 }, + status: "visited", + description: "在故宫的红墙下,我们许下了下一次旅行的约定。", + memories: ["https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=400&q=80"], + }, + { + name: "上海", + province: "上海", + position: { x: 68, y: 45 }, + status: "visited", + description: "外滩的灯光映着我们的笑,像城市的星河。", + memories: ["https://images.unsplash.com/photo-1505764706515-aa95265c5abc?auto=format&fit=crop&w=400&q=80"], + }, + { + name: "成都", + province: "四川", + position: { x: 38, y: 50 }, + status: "visited", + description: "宽窄巷子里,一杯盖碗茶,两颗心慢慢靠近。", + memories: ["https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?auto=format&fit=crop&w=400&q=80"], + }, + { + name: "杭州", + province: "浙江", + position: { x: 66, y: 52 }, + status: "wishlist", + description: "西湖的风还在等我们一起去看。", + memories: [], + }, + { + name: "厦门", + province: "福建", + position: { x: 72, y: 63 }, + status: "wishlist", + description: "海风与日落,是我们下一场浪漫。", + memories: [], + }, + { + name: "西安", + province: "陕西", + position: { x: 47, y: 40 }, + status: "wishlist", + description: "城墙上的夕阳,等待我们一起收集。", + memories: [], + }, +]; + +const markersContainer = document.getElementById("city-markers"); +const visitedCount = document.getElementById("visited-count"); +const wishlistCount = document.getElementById("wishlist-count"); +const previewContainer = document.getElementById("memories-preview"); +const modal = document.getElementById("city-modal"); +const modalTitle = document.getElementById("modal-title"); +const modalStatus = document.getElementById("modal-status"); +const modalDescription = document.getElementById("modal-description"); +const modalGallery = document.getElementById("modal-gallery"); +const uploadInput = document.getElementById("upload-input"); +const markVisitedButton = document.getElementById("mark-visited"); +const letterModal = document.getElementById("letter-modal"); + +let activeCity = null; + +const statusLabel = { + visited: "已点亮", + wishlist: "待探索", +}; + +const updateStats = () => { + const visited = cities.filter((city) => city.status === "visited").length; + const wishlist = cities.filter((city) => city.status === "wishlist").length; + visitedCount.textContent = visited; + wishlistCount.textContent = wishlist; +}; + +const renderMarkers = () => { + markersContainer.innerHTML = ""; + cities.forEach((city, index) => { + const marker = document.createElement("button"); + marker.className = "city-marker"; + marker.dataset.status = city.status; + marker.style.left = `${city.position.x}%`; + marker.style.top = `${city.position.y}%`; + marker.textContent = `${city.name}`; + marker.addEventListener("click", () => openCityModal(index)); + markersContainer.appendChild(marker); + }); +}; + +const renderPreview = () => { + previewContainer.innerHTML = ""; + cities + .filter((city) => city.status === "visited") + .slice(0, 3) + .forEach((city) => { + const card = document.createElement("div"); + card.className = "memory-card"; + card.textContent = `${city.name} · ${city.description}`; + previewContainer.appendChild(card); + }); +}; + +const openCityModal = (index) => { + activeCity = cities[index]; + modalTitle.textContent = `${activeCity.name} · ${activeCity.province}`; + modalStatus.textContent = statusLabel[activeCity.status]; + modalDescription.textContent = activeCity.description; + modalStatus.style.background = + activeCity.status === "visited" ? "rgba(255, 143, 177, 0.2)" : "rgba(255, 255, 255, 0.1)"; + modalStatus.style.color = activeCity.status === "visited" ? "var(--accent)" : "var(--muted)"; + markVisitedButton.style.display = activeCity.status === "wishlist" ? "inline-flex" : "none"; + renderGallery(); + modal.setAttribute("aria-hidden", "false"); +}; + +const renderGallery = () => { + modalGallery.innerHTML = ""; + if (activeCity.memories.length === 0) { + const empty = document.createElement("div"); + empty.className = "memory-card"; + empty.textContent = "待探索 · 期待我们一起写下故事"; + modalGallery.appendChild(empty); + return; + } + + activeCity.memories.forEach((url) => { + const img = document.createElement("img"); + img.src = url; + img.alt = `${activeCity.name} 回忆`; + modalGallery.appendChild(img); + }); +}; + +const closeModal = (targetModal) => { + targetModal.setAttribute("aria-hidden", "true"); +}; + +const handleFiles = (files) => { + if (!activeCity) return; + Array.from(files).forEach((file) => { + const reader = new FileReader(); + reader.onload = (event) => { + if (event.target?.result) { + activeCity.memories.push(event.target.result.toString()); + activeCity.status = "visited"; + renderMarkers(); + updateStats(); + renderPreview(); + renderGallery(); + modalStatus.textContent = statusLabel[activeCity.status]; + markVisitedButton.style.display = "none"; + } + }; + reader.readAsDataURL(file); + }); +}; + +uploadInput.addEventListener("change", (event) => { + if (event.target.files) { + handleFiles(event.target.files); + } +}); + +markVisitedButton.addEventListener("click", () => { + if (!activeCity) return; + activeCity.status = "visited"; + renderMarkers(); + updateStats(); + renderPreview(); + renderGallery(); + modalStatus.textContent = statusLabel.visited; + markVisitedButton.style.display = "none"; +}); + +window.addEventListener("click", (event) => { + const target = event.target; + if (target instanceof HTMLElement && target.dataset.close === "true") { + closeModal(target.closest(".modal")); + } +}); + +window.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + closeModal(modal); + closeModal(letterModal); + } +}); + +const openLetterButton = document.getElementById("open-letter"); +openLetterButton.addEventListener("click", () => { + letterModal.setAttribute("aria-hidden", "false"); +}); + +renderMarkers(); +updateStats(); +renderPreview(); diff --git a/romantic-map-site/styles.css b/romantic-map-site/styles.css new file mode 100644 index 00000000..5b8b3660 --- /dev/null +++ b/romantic-map-site/styles.css @@ -0,0 +1,361 @@ +:root { + color-scheme: light; + --bg: #0c0f1a; + --panel: rgba(255, 255, 255, 0.08); + --panel-solid: #161b2b; + --accent: #ff8fb1; + --accent-strong: #ff5f8a; + --glow: rgba(255, 143, 177, 0.4); + --text: #f8f2ff; + --muted: rgba(255, 255, 255, 0.7); +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: "Noto Sans SC", system-ui, sans-serif; +} + +body { + min-height: 100vh; + background: radial-gradient(circle at top, #20294b 0%, #0c0f1a 60%); + color: var(--text); + overflow-x: hidden; + padding: 32px 48px 64px; +} + +.aurora { + position: fixed; + inset: 0; + background: radial-gradient(circle at 20% 20%, rgba(255, 170, 210, 0.2), transparent 50%), + radial-gradient(circle at 80% 10%, rgba(147, 204, 255, 0.18), transparent 45%), + radial-gradient(circle at 50% 80%, rgba(255, 126, 156, 0.18), transparent 55%); + pointer-events: none; + z-index: 0; +} + +.hero { + text-align: center; + margin-bottom: 32px; + position: relative; + z-index: 1; +} + +.hero__eyebrow { + letter-spacing: 2px; + text-transform: uppercase; + color: var(--accent); + margin-bottom: 8px; +} + +.hero__title { + font-family: "Pacifico", cursive; + font-size: clamp(2.2rem, 3vw, 3.5rem); + margin-bottom: 12px; +} + +.hero__subtitle { + color: var(--muted); +} + +.layout { + display: grid; + grid-template-columns: minmax(320px, 1fr) 320px; + gap: 28px; + align-items: start; + position: relative; + z-index: 1; +} + +.map-panel { + background: var(--panel); + border-radius: 24px; + padding: 24px; + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.08); + backdrop-filter: blur(18px); +} + +.map-stage { + position: relative; + height: 520px; + border-radius: 20px; + background: linear-gradient(130deg, rgba(37, 46, 74, 0.8), rgba(22, 28, 52, 0.9)); + overflow: hidden; + perspective: 1200px; +} + +.map-depth { + position: absolute; + inset: 0; + background: radial-gradient(circle at 30% 20%, rgba(255, 255, 255, 0.08), transparent 55%); +} + +.china-map { + position: absolute; + inset: 50% 0 0 50%; + transform: translate(-50%, -52%) rotateX(20deg) rotateZ(-8deg); + width: 90%; + height: auto; + filter: drop-shadow(0 20px 30px rgba(0, 0, 0, 0.35)); +} + +.china-map__shape { + stroke: rgba(255, 255, 255, 0.3); + stroke-width: 2; +} + +.china-map__inner { + fill: rgba(255, 255, 255, 0.08); + stroke: rgba(255, 255, 255, 0.2); + stroke-width: 1; +} + +.map-glow { + position: absolute; + inset: 0; + background: radial-gradient(circle at 50% 60%, rgba(255, 143, 177, 0.25), transparent 60%); + mix-blend-mode: screen; +} + +.city-markers { + position: absolute; + inset: 0; +} + +.city-marker { + position: absolute; + transform: translate(-50%, -50%); + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.4); + padding: 6px 10px; + border-radius: 999px; + font-size: 12px; + cursor: pointer; + color: var(--text); + box-shadow: 0 0 16px var(--glow); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.city-marker::after { + content: ""; + position: absolute; + width: 10px; + height: 10px; + background: var(--accent); + border-radius: 50%; + left: 8px; + top: 50%; + transform: translate(-50%, -50%); + box-shadow: 0 0 12px var(--accent); +} + +.city-marker:hover { + transform: translate(-50%, -50%) scale(1.08); + box-shadow: 0 0 20px rgba(255, 143, 177, 0.7); +} + +.city-marker[data-status="wishlist"] { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.2); + color: rgba(255, 255, 255, 0.8); +} + +.map-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 16px; + color: var(--muted); +} + +.wish-button { + background: var(--accent-strong); + border: none; + color: var(--text); + padding: 10px 18px; + border-radius: 999px; + cursor: pointer; + font-weight: 500; +} + +.info-panel { + display: flex; + flex-direction: column; + gap: 16px; +} + +.panel-card { + background: var(--panel-solid); + border-radius: 18px; + padding: 18px; + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 16px 40px rgba(0, 0, 0, 0.25); +} + +.stats { + display: flex; + justify-content: space-between; + margin-top: 12px; +} + +.stats__label { + color: var(--muted); + font-size: 13px; +} + +.stats__value { + font-size: 24px; + font-weight: 600; +} + +.panel-note { + margin-top: 10px; + color: var(--muted); + font-size: 14px; +} + +.memories { + margin-top: 12px; + display: grid; + gap: 12px; +} + +.memory-card { + background: rgba(255, 255, 255, 0.08); + border-radius: 12px; + padding: 12px; + font-size: 13px; + color: var(--muted); +} + +.quote { + font-style: italic; + line-height: 1.6; + color: rgba(255, 255, 255, 0.8); +} + +.modal { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease; + z-index: 10; +} + +.modal[aria-hidden="false"] { + opacity: 1; + pointer-events: auto; +} + +.modal__backdrop { + position: absolute; + inset: 0; + background: rgba(8, 10, 20, 0.7); +} + +.modal__content { + position: relative; + background: #121827; + border-radius: 20px; + padding: 24px; + width: min(520px, 90vw); + z-index: 1; + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45); +} + +.modal__close { + position: absolute; + top: 16px; + right: 16px; + background: transparent; + border: none; + color: var(--muted); + font-size: 24px; + cursor: pointer; +} + +.modal__header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.status { + padding: 4px 12px; + border-radius: 999px; + font-size: 12px; + background: rgba(255, 143, 177, 0.2); + color: var(--accent); +} + +.modal__gallery { + margin-top: 16px; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 12px; +} + +.modal__gallery img { + width: 100%; + height: 90px; + border-radius: 12px; + object-fit: cover; +} + +.modal__actions { + display: flex; + gap: 12px; + margin-top: 18px; +} + +.upload { + padding: 10px 14px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.1); + cursor: pointer; + font-size: 13px; +} + +.secondary { + padding: 10px 14px; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.2); + background: transparent; + color: var(--text); + cursor: pointer; + font-size: 13px; +} + +.letter p { + margin-top: 12px; + line-height: 1.6; + color: var(--muted); +} + +.signature { + margin-top: 16px; + text-align: right; + font-family: "Pacifico", cursive; + color: var(--accent); +} + +@media (max-width: 1024px) { + body { + padding: 24px; + } + + .layout { + grid-template-columns: 1fr; + } + + .map-stage { + height: 420px; + } +}