Created
January 31, 2026 05:53
-
-
Save joannewang0807-prog/198f793724793a26320e9bd03cd05565 to your computer and use it in GitHub Desktop.
2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="zh-TW"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>家族旅遊同步 2026</title> | |
| <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { --lake-green: #78a5a3; --soft-bg: #f5f7f6; } | |
| body { background-color: var(--soft-bg); color: #4a4a4a; font-family: "Microsoft JhengHei", sans-serif; } | |
| .hide-scrollbar::-webkit-scrollbar { display: none; } | |
| .active-day { background-color: #78a5a3 !important; color: white !important; transform: scale(1.05); } | |
| .sync-btn-active { animation: spin 1s linear infinite; } | |
| @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } | |
| /* 橘紅奔馬圖示 */ | |
| .horse-icon { | |
| width: 48px; | |
| height: auto; | |
| margin-bottom: 2px; | |
| } | |
| </style> | |
| </head> | |
| <body class="max-w-md mx-auto min-h-screen relative flex flex-col shadow-inner"> | |
| <div id="app" class="flex-grow pb-28"> | |
| <div class="bg-teal-700 text-white text-[11px] py-2 px-4 flex justify-between items-center shadow-md"> | |
| <span><i class="fas fa-users mr-1"></i> 王敏苓家族同步中</span> | |
| <button @click="syncCloud" class="flex items-center bg-teal-600 px-3 py-1 rounded-full text-[10px]"> | |
| <i class="fas fa-sync-alt mr-1" :class="{'sync-btn-active': syncing}"></i> | |
| {{ syncing ? '同步中...' : '發布至雲端' }} | |
| </button> | |
| </div> | |
| <header class="p-6 pt-10 flex justify-between items-end"> | |
| <div> | |
| <h1 class="text-3xl font-bold tracking-widest text-gray-700">{{ currentCity }}</h1> | |
| <p class="text-[11px] text-gray-400 mt-1 uppercase">2026 / {{ currentRange }}</p> | |
| </div> | |
| <div class="flex flex-col items-center"> | |
| <svg class="horse-icon" viewBox="0 0 100 60" xmlns="http://www.w3.org/2000/svg"> | |
| <path d="M15,45 C20,35 35,25 50,25 C65,25 80,30 90,20 C100,10 85,0 70,0 C55,0 45,10 40,20 C35,30 20,35 10,50 C7,55 15,60 30,60 L40,45" fill="#E67E22" /> | |
| <path d="M65,28 L70,40 M75,30 L80,42" stroke="#E67E22" stroke-width="4" stroke-linecap="round"/> | |
| </svg> | |
| <span class="text-2xl font-light text-teal-600 leading-none">{{ weather.temp }}°C</span> | |
| <p class="text-[10px] text-gray-400 mt-1">當地氣溫</p> | |
| </div> | |
| </header> | |
| <div class="px-4 mb-6"> | |
| <div class="flex overflow-x-auto hide-scrollbar space-x-3 py-2"> | |
| <button v-for="day in days" :key="day.id" @click="selectedDay = day.id" | |
| :class="selectedDay === day.id ? 'active-day' : 'bg-white text-gray-400'" | |
| class="flex-shrink-0 w-16 h-20 rounded-2xl shadow-sm flex flex-col items-center justify-center transition-all border border-gray-100"> | |
| <span class="text-[9px] font-bold uppercase">{{ day.city }}</span> | |
| <span class="text-sm font-bold my-0.5">{{ day.date }}</span> | |
| <span class="text-[10px] opacity-60">D{{ day.id }}</span> | |
| </button> | |
| </div> | |
| </div> | |
| <main class="px-6"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-sm font-bold text-gray-500 border-l-4 border-teal-500 pl-3">每日行程</h2> | |
| <button @click="addItem" class="text-teal-600 text-xs font-bold">+ 新增行程</button> | |
| </div> | |
| <div class="space-y-4"> | |
| <div v-for="(item, index) in itinerary" :key="index"> | |
| <div v-if="item.day === selectedDay" class="flex gap-4"> | |
| <input v-model="item.time" class="text-[10px] font-mono text-teal-600 w-12 text-center bg-transparent focus:outline-none"> | |
| <div class="bg-white p-4 rounded-3xl shadow-sm flex-grow border border-gray-100"> | |
| <input v-model="item.location" class="w-full font-medium text-gray-700 focus:outline-none bg-transparent"> | |
| <div class="flex justify-between mt-2"> | |
| <span class="text-[10px] text-gray-400"><i class="fas fa-map-marker-alt mr-1"></i> 點擊編輯地點</span> | |
| <button @click="deleteItem(index)" class="text-red-200 text-[10px]">刪除</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <nav class="fixed bottom-6 left-1/2 -translate-x-1/2 w-[80%] bg-white/90 backdrop-blur-md rounded-full shadow-2xl flex justify-around py-3 border border-white"> | |
| <button class="text-teal-600"><i class="fas fa-calendar-alt text-xl"></i></button> | |
| <button class="text-gray-300"><i class="fas fa-wallet text-xl"></i></button> | |
| </nav> | |
| </div> | |
| <script> | |
| const { createApp, ref, computed, onMounted, watch } = Vue; | |
| createApp({ | |
| setup() { | |
| const selectedDay = ref(1); | |
| const syncing = ref(false); | |
| const weather = ref({ temp: '--' }); | |
| const itinerary = ref([ | |
| { day: 1, time: "14:00", location: "抵達大阪 / 飯店 Check-in" }, | |
| { day: 1, time: "18:30", location: "道頓堀吃晚餐" }, | |
| { day: 2, time: "10:00", location: "大阪城公園" } | |
| ]); | |
| const days = []; | |
| const cities = ['大阪','大阪','大阪','京都','京都','京都','神戶','神戶','神戶','神戶','曼谷','曼谷','曼谷','曼谷','清邁','清邁','清邁','清邁','清邁','檳城','檳城','檳城','檳城','檳城','檳城']; | |
| for(let i=1; i<=25; i++) { | |
| days.push({ id: i, date: `1/${30+i}`, city: cities[i-1], lat: 34.6, lng: 135.5 }); | |
| } | |
| const fetchWeather = async () => { | |
| try { | |
| const d = days[selectedDay.value-1]; | |
| const res = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${d.lat}&longitude=${d.lng}¤t_weather=true`); | |
| const data = await res.json(); | |
| weather.value.temp = Math.round(data.current_weather.temperature); | |
| } catch (e) { weather.value.temp = '25'; } | |
| }; | |
| onMounted(fetchWeather); | |
| watch(selectedDay, fetchWeather); | |
| return { | |
| selectedDay, days, itinerary, weather, syncing, | |
| currentCity: computed(() => days[selectedDay.value-1].city), | |
| currentRange: computed(() => days[selectedDay.value-1].date), | |
| addItem: () => itinerary.value.push({ day: selectedDay.value, time: '12:00', location: '新地點' }), | |
| deleteItem: (i) => itinerary.value.splice(i, 1), | |
| syncCloud: () => { syncing.value = true; setTimeout(() => { syncing.value = false; alert('繁體版同步成功!'); }, 800); } | |
| }; | |
| } | |
| }).mount('#app'); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment