Skip to content

Instantly share code, notes, and snippets.

@joannewang0807-prog
Created January 31, 2026 04:32
Show Gist options
  • Select an option

  • Save joannewang0807-prog/c3727f6a87bdc185691148b45ce5f081 to your computer and use it in GitHub Desktop.

Select an option

Save joannewang0807-prog/c3727f6a87bdc185691148b45ce5f081 to your computer and use it in GitHub Desktop.
2026家族旅遊
<!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: 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); } }
</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-[10px] py-1.5 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-2 py-0.5 rounded">
<i class="fas fa-sync-alt mr-1" :class="{'sync-btn-active': syncing}"></i>
{{ syncing ? '同步中...' : '手動同步' }}
</button>
</div>
<header class="p-6 pt-6 flex justify-between items-end">
<div>
<h1 class="text-3xl font-light tracking-widest text-gray-700">{{ currentCity }}</h1>
<p class="text-[10px] text-gray-400 mt-1 uppercase">2026 {{ currentRange }}</p>
</div>
<div class="text-right">
<span class="text-2xl font-light text-teal-600">{{ weather.temp }}°C</span>
<p class="text-[10px] text-gray-400">當前氣溫</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-50">
<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 v-if="activeTab === 'itinerary'">
<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" @change="debouncedSync" class="text-[10px] font-mono text-teal-600 w-12 text-center focus:outline-none bg-transparent">
<div class="bg-white p-4 rounded-3xl shadow-sm flex-grow border border-gray-100">
<input v-model="item.location" @change="debouncedSync" placeholder="點擊編輯行程..." class="w-full font-medium text-gray-700 focus:outline-none mb-2 bg-transparent">
<div class="flex justify-between">
<a :href="'https://www.google.com/maps/search/' + encodeURIComponent(item.location + ' ' + currentCity)" target="_blank" class="text-[10px] text-gray-400">
<i class="fas fa-location-arrow mr-1"></i> 地圖導航
</a>
<button @click="deleteItem(index)" class="text-red-200 text-[10px]">刪除</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-if="activeTab === 'split'">
<div class="bg-white p-6 rounded-[30px] shadow-sm mb-6 border border-gray-100">
<h3 class="text-xs font-bold text-gray-400 mb-6 uppercase tracking-widest text-center">共享開銷記錄</h3>
<div class="space-y-4">
<input v-model="newExpense.item" placeholder="項目名稱 (例:拉麵)" class="w-full border-b pb-2 text-sm focus:outline-none bg-transparent">
<div class="flex gap-4">
<input v-model.number="newExpense.amount" type="number" placeholder="日幣金額" class="w-1/2 border-b pb-2 text-sm focus:outline-none bg-transparent">
<select v-model="newExpense.payer" class="w-1/2 border-b pb-2 text-sm focus:outline-none bg-transparent">
<option v-for="p in people" :value="p">{{ p }} 先墊錢</option>
</select>
</div>
<button @click="addExpense" class="w-full bg-[#78a5a3] text-white py-3 rounded-xl text-sm font-bold shadow-lg">新增記錄</button>
</div>
</div>
<div class="space-y-2">
<div v-for="(ex, i) in expenses" :key="i" class="bg-white/60 p-3 rounded-xl flex justify-between text-xs border border-white">
<span><b class="text-teal-600">{{ ex.payer }}</b> 支付了 {{ ex.item }}</span>
<span class="font-bold text-gray-600">¥{{ ex.amount }}</span>
</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 @click="activeTab = 'itinerary'" :class="activeTab === 'itinerary' ? 'text-teal-600' : 'text-gray-300'"><i class="fas fa-calendar-alt text-xl"></i></button>
<button @click="activeTab = 'split'" :class="activeTab === 'split' ? 'text-teal-600' : 'text-gray-300'"><i class="fas fa-wallet text-xl"></i></button>
</nav>
</div>
<script>
const { createApp, ref, computed, onMounted, watch } = Vue;
// --- 這裡已經填入你的正確資訊 ---
const BIN_ID = '697d5c3bae596e708f050afc';
const API_KEY = '$2a$10$2Kgm6wzaFeScgJsNqCvl1.r2p9oWzCjF3hoR8KqsY0IPDfTAJgeSa';
createApp({
setup() {
const activeTab = ref('itinerary');
const selectedDay = ref(1);
const syncing = ref(false);
const weather = ref({ temp: '--' });
const people = ['我', '爸爸', '媽媽'];
const itinerary = ref([]);
const expenses = ref([]);
const newExpense = ref({ item: '', amount: null, payer: '我' });
// 生成 25 天日期
const days = [];
const startDate = new Date(2026, 1, 9);
const cities = ['大阪','大阪','大阪','京都','京都','京都','神戶','神戶','神戶','神戶','曼谷','曼谷','曼谷','曼谷','清邁','清邁','清邁','清邁','清邁','檳城','檳城','檳城','檳城','檳城','檳城'];
for(let i=1; i<=25; i++) {
const cur = new Date(startDate);
cur.setDate(startDate.getDate() + (i-1));
days.push({
id: i,
date: `${cur.getMonth()+1}/${cur.getDate()}`,
city: cities[i-1],
lat: cities[i-1] === '曼谷' ? 13.7 : 34.6,
lng: cities[i-1] === '曼谷' ? 100.5 : 135.5
});
}
// 核心功能:同步雲端
const syncCloud = async () => {
syncing.value = true;
try {
// 1. 先把本地當前的資料傳上去 (PUT)
const saveRes = await fetch(`https://api.jsonbin.io/v3/b/${BIN_ID}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Master-Key': API_KEY
},
body: JSON.stringify({ itinerary: itinerary.value, expenses: expenses.value })
});
// 2. 再抓取最新的下來確保同步 (GET)
const loadRes = await fetch(`https://api.jsonbin.io/v3/b/${BIN_ID}/latest`, {
headers: { 'X-Master-Key': API_KEY }
});
const data = await loadRes.json();
itinerary.value = data.record.itinerary || [];
expenses.value = data.record.expenses || [];
} catch (e) {
console.error('同步失敗', e);
}
syncing.value = false;
};
const addExpense = () => {
if(!newExpense.value.amount || !newExpense.value.item) return;
expenses.value.push({...newExpense.value});
newExpense.value = { item: '', amount: null, payer: '我' };
syncCloud(); // 自動同步
};
const addItem = () => {
itinerary.value.push({ day: selectedDay.value, time: '12:00', location: '新行程' });
syncCloud();
};
const deleteItem = (idx) => {
itinerary.value.splice(idx, 1);
syncCloud();
};
const currentCity = computed(() => days[selectedDay.value-1].city);
const currentRange = computed(() => days[selectedDay.value-1].date);
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}&current_weather=true`);
const data = await res.json();
weather.value.temp = Math.round(data.current_weather.temperature);
} catch (e) { weather.value.temp = '--'; }
};
onMounted(() => {
syncCloud();
fetchWeather();
});
watch(selectedDay, fetchWeather);
return {
activeTab, selectedDay, days, itinerary, expenses, newExpense,
currentCity, currentRange, addItem, deleteItem, addExpense, weather, syncing, syncCloud, people
};
}
}).mount('#app');
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment