-
Notifications
You must be signed in to change notification settings - Fork 96
Open
Description
<title>2026 東北雪國之旅</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
muji: {
bg: '#F7F6F2', // 溫暖米白背景
paper: '#FFFFFF', // 卡片純白
accent: '#7F8C8D', // 灰褐色
red: '#9E3D3D', // 無印經典紅
text: '#333333', // 深灰文字
border: '#E0DDD5' // 邊框色
}
},
fontFamily: {
sans: ['Noto Sans TC', 'sans-serif'],
serif: ['Noto Serif JP', 'serif']
}
}
}
}
</script>
<style>
body {
background-color: #F7F6F2;
color: #333333;
-webkit-tap-highlight-color: transparent;
padding-bottom: 80px;
}
.active-tab {
color: #9E3D3D !important;
border-bottom: 3px solid #9E3D3D;
}
.muji-card {
background: white;
border: 1px solid #E0DDD5;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.02);
}
.timeline-line::before {
content: '';
position: absolute;
left: 11px;
top: 0;
bottom: 0;
width: 1px;
background: #E0DDD5;
}
/* 隱藏捲軸 */
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
.btn-press:active { transform: scale(0.96); }
</style>
<!-- 頂部標題列 -->
<header class="sticky top-0 z-50 bg-muji-paper/90 backdrop-blur-md border-b border-muji-border px-6 py-4 flex justify-between items-center">
<div>
<h1 class="font-serif text-xl font-black text-muji-text">2026 東北雪國之旅</h1>
<p class="text-[10px] text-muji-accent tracking-widest uppercase">Jan 06 — Jan 13</p>
</div>
<div class="flex items-center gap-2">
<div class="h-2 w-2 bg-muji-red rounded-full animate-pulse"></div>
<span class="text-[10px] font-bold text-muji-red">行程進行中</span>
</div>
</header>
<!-- 主要內容區 -->
<main id="app-content" class="max-w-md mx-auto p-5">
<!-- JS 將在此處動態生成內容 -->
</main>
<!-- 底部導航列 -->
<nav class="fixed bottom-0 left-0 right-0 bg-muji-paper border-t border-muji-border flex justify-around items-center h-16 z-50">
<button onclick="switchTab('plan')" id="tab-plan" class="flex flex-col items-center justify-center w-full h-full text-muji-accent">
<i class="fa-solid fa-calendar-check text-lg"></i>
<span class="text-[9px] mt-1 font-bold">PLAN</span>
</button>
<button onclick="switchTab('guide')" id="tab-guide" class="flex flex-col items-center justify-center w-full h-full text-muji-accent">
<i class="fa-solid fa-map-location-dot text-lg"></i>
<span class="text-[9px] mt-1 font-bold">GUIDE</span>
</button>
<button onclick="switchTab('wallet')" id="tab-wallet" class="flex flex-col items-center justify-center w-full h-full text-muji-accent">
<i class="fa-solid fa-wallet text-lg"></i>
<span class="text-[9px] mt-1 font-bold">WALLET</span>
</button>
<button onclick="switchTab('lists')" id="tab-lists" class="flex flex-col items-center justify-center w-full h-full text-muji-accent">
<i class="fa-solid fa-list-ul text-lg"></i>
<span class="text-[9px] mt-1 font-bold">LISTS</span>
</button>
<button onclick="switchTab('info')" id="tab-info" class="flex flex-col items-center justify-center w-full h-full text-muji-accent">
<i class="fa-solid fa-info-circle text-lg"></i>
<span class="text-[9px] mt-1 font-bold">INFO</span>
</button>
</nav>
<!-- 資料腳本 -->
<script>
const TRAVEL_DATA = {
itinerary: [
{ date: "1/6 (週二)", title: "雪國啟動", items: [
{ time: "08:30", text: "竹北接送至桃機 T1", icon: "fa-car", important: true },
{ time: "16:00", text: "抵達仙台機場", icon: "fa-plane-arrival" },
{ time: "17:00", text: "仙台站取車 (4WD/雪胎)", icon: "fa-key" },
{ time: "19:00", text: "藏王辦理租借雪具", icon: "fa-skiing" },
{ time: "20:00", text: "入住「五感之湯」", icon: "fa-hot-tub-person", important: true }
]},
{ date: "1/7-1/8", title: "滑雪修行", items: [
{ time: "09:00", text: "滑雪課程開始", icon: "fa-person-skiing" },
{ time: "16:30", text: "溫泉放鬆 (藏王硫磺泉)", icon: "fa-water" },
{ time: "18:30", text: "晚餐:成吉思汗烤羊肉", icon: "fa-utensils" }
]},
{ date: "1/9 (週五)", title: "絕景巡禮", items: [
{ time: "08:30", text: "藏王樹冰登頂 (纜車)", icon: "fa-mountain", important: true },
{ time: "13:30", text: "出發前往銀山溫泉", icon: "fa-car-side" },
{ time: "16:30", text: "銀山溫泉點燈拍照", icon: "fa-camera", important: true },
{ time: "20:30", text: "入住仙台東大都會飯店", icon: "fa-hotel" }
]},
{ date: "1/10 (週六)", title: "南宮城自駕", items: [
{ time: "10:00", text: "ICHIGO WORLD 採草莓", icon: "fa-strawberry", link: "https://ichigo-world.jp/" },
{ time: "13:30", text: "金蛇水神社參拜", icon: "fa-shrine" },
{ time: "15:00", text: "地底之森博物館", icon: "fa-museum" }
]},
{ date: "1/11 (週日)", title: "松島海鮮", items: [
{ time: "11:30", text: "さんとり茶屋 (生蠔)", icon: "fa-fish" },
{ time: "17:00", text: "仙台站歸還租車", icon: "fa-flag-checkered", important: true },
{ time: "18:30", text: "燒肉 仔虎 (仙台牛)", icon: "fa-fire-burner" }
]},
{ date: "1/12 (週一)", title: "仙台散策", items: [
{ time: "10:00", text: "仙台城跡 (Loople 巴士)", icon: "fa-bus" },
{ time: "12:30", text: "午餐:牛舌名店 (善治郎)", icon: "fa-utensils" },
{ time: "14:00", text: "購物:S-PAL / LoFt", icon: "fa-bag-shopping" }
]},
{ date: "1/13 (週二)", title: "最後巡禮", items: [
{ time: "09:00", text: "仙台朝市巡禮", icon: "fa-shrimp" },
{ time: "15:00", text: "搭機場快線往機場", icon: "fa-train" },
{ time: "17:25", text: "搭機返台 (JX863)", icon: "fa-plane-departure" }
]}
],
guides: [
{ name: "藏王樹冰", desc: "自然形成的『雪怪』奇觀,全世界僅有東北等少數地區可見。", loc: "山形縣藏王溫泉" },
{ name: "銀山溫泉", desc: "大正浪漫風格的溫泉街,入夜後的橘色燈火極具夢幻感。", loc: "山形縣尾花澤市" },
{ name: "金蛇水神社", desc: "供奉水神與蛇神,以求財運與生意興隆聞名,建築極美。", loc: "宮城縣岩沼市" }
],
checkList: ["護照", "日文譯本", "駕照正本", "滑雪手套", "發熱衣", "行動電源", "保暖毛帽", "日本網卡"]
};
// 狀態管理
let state = {
currentTab: 'plan',
expenses: JSON.parse(localStorage.getItem('muji_expenses') || '[]'),
checks: JSON.parse(localStorage.getItem('muji_checks') || '{}'),
memos: JSON.parse(localStorage.getItem('muji_memos') || '[]'),
rate: parseFloat(localStorage.getItem('muji_rate') || '0.21')
};
function switchTab(tab) {
state.currentTab = tab;
render();
window.scrollTo(0, 0);
}
// --- 記帳邏輯 ---
function addExpense() {
const item = document.getElementById('ex-item').value;
const formula = document.getElementById('ex-amount').value;
const imgInput = document.getElementById('ex-file');
if (!item || !formula) return;
let amount = 0;
try { amount = eval(formula.replace(/[^-()\d/*+.]/g, '')); } catch { amount = 0; }
const save = (imgBase64 = null) => {
state.expenses.push({ id: Date.now(), item, amount, img: imgBase64 });
localStorage.setItem('muji_expenses', JSON.stringify(state.expenses));
render();
};
if (imgInput.files[0]) {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const maxW = 150;
const scale = maxW / img.width;
canvas.width = maxW;
canvas.height = img.height * scale;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
save(canvas.toDataURL('image/jpeg', 0.6));
};
img.src = e.target.result;
};
reader.readAsDataURL(imgInput.files[0]);
} else {
save();
}
}
function deleteExpense(id) {
state.expenses = state.expenses.filter(e => e.id !== id);
localStorage.setItem('muji_expenses', JSON.stringify(state.expenses));
render();
}
// --- 清單與備忘錄邏輯 ---
function toggleCheck(item) {
state.checks[item] = !state.checks[item];
localStorage.setItem('muji_checks', JSON.stringify(state.checks));
render();
}
function addMemo() {
const val = document.getElementById('memo-in').value;
if (!val) return;
state.memos.push(val);
localStorage.setItem('muji_memos', JSON.stringify(state.memos));
render();
}
function deleteMemo(idx) {
state.memos.splice(idx, 1);
localStorage.setItem('muji_memos', JSON.stringify(state.memos));
render();
}
// --- 渲染引擎 ---
function render() {
const container = document.getElementById('app-content');
document.querySelectorAll('nav button').forEach(b => b.classList.remove('active-tab'));
document.getElementById(`tab-${state.currentTab}`).classList.add('active-tab');
let html = '';
if (state.currentTab === 'plan') {
html += `<div class="mb-6 bg-muji-red text-white p-4 rounded-xl text-xs flex justify-between items-center">
<span><i class="fa-solid fa-bell mr-2"></i>1/6 11:00 搶銀山門票</span>
<i class="fa-solid fa-chevron-right"></i>
</div>`;
TRAVEL_DATA.itinerary.forEach(day => {
html += `
<div class="mb-8">
<h2 class="font-serif font-black text-lg mb-4 flex items-center gap-2">
<span class="text-muji-red">/</span> ${day.date} ${day.title}
</h2>
<div class="relative timeline-line ml-3">
${day.items.map(it => `
<div class="ml-8 mb-6 relative">
<div class="absolute -left-[27px] top-1 w-3 h-3 rounded-full bg-white border-2 ${it.important ? 'border-muji-red' : 'border-muji-border'} z-10"></div>
<div class="${it.important ? 'bg-white p-3 rounded-xl border-l-4 border-l-muji-red muji-card' : ''}">
<div class="flex items-center gap-2 text-[10px] text-muji-accent font-mono">
<span>${it.time}</span>
<i class="fa-solid ${it.icon}"></i>
</div>
<div class="text-sm mt-1 ${it.important ? 'font-bold' : 'font-medium'}">${it.text}</div>
<div class="mt-2 flex gap-2">
<button onclick="window.open('https://www.google.com/maps/search/${it.text}')" class="text-[9px] bg-muji-bg px-2 py-1 rounded border border-muji-border text-muji-accent">MAP</button>
${it.link ? `<button onclick="window.open('${it.link}')" class="text-[9px] bg-muji-bg px-2 py-1 rounded border border-muji-border text-muji-red">WEB</button>` : ''}
</div>
</div>
</div>
`).join('')}
</div>
</div>`;
});
}
if (state.currentTab === 'guide') {
html += `<h2 class="font-serif text-2xl font-black mb-6 text-center">深度導覽</h2>`;
TRAVEL_DATA.guides.forEach(g => {
html += `
<div class="muji-card overflow-hidden mb-6">
<div class="h-32 bg-muji-bg flex items-center justify-center text-muji-border">
<i class="fa-solid fa-image text-4xl"></i>
</div>
<div class="p-5">
<span class="text-[9px] text-muji-red font-bold uppercase tracking-tighter">${g.loc}</span>
<h3 class="font-serif text-lg font-bold mt-1 mb-2">${g.name}</h3>
<p class="text-xs leading-relaxed text-muji-accent">${g.desc}</p>
</div>
</div>`;
});
}
if (state.currentTab === 'wallet') {
const totalJpy = state.expenses.reduce((s, e) => s + e.amount, 0);
html += `
<div class="muji-card p-6 mb-6">
<div class="flex justify-between items-start">
<div>
<p class="text-[10px] text-muji-accent uppercase tracking-widest">Total Spent</p>
<h2 class="text-3xl font-black font-mono">¥${totalJpy.toLocaleString()}</h2>
<p class="text-sm font-bold text-muji-red">≈ NT$${Math.round(totalJpy * state.rate).toLocaleString()}</p>
</div>
<input type="number" step="0.001" onchange="state.rate=this.value;localStorage.setItem('muji_rate',this.value);render()" value="${state.rate}" class="w-16 text-right text-xs bg-muji-bg p-1 rounded font-mono">
</div>
<div class="mt-6 space-y-3 pt-6 border-t border-muji-border">
<input id="ex-item" type="text" placeholder="品項名稱" class="w-full bg-muji-bg p-3 rounded-xl text-sm outline-none">
<div class="flex gap-2">
<input id="ex-amount" type="text" placeholder="金額 (可輸入 500+200)" class="flex-1 bg-muji-bg p-3 rounded-xl text-sm font-mono outline-none">
<label class="bg-muji-accent text-white px-4 flex items-center rounded-xl btn-press cursor-pointer">
<i class="fa-solid fa-camera"></i>
<input type="file" id="ex-file" accept="image/*" class="hidden">
</label>
</div>
<button onclick="addExpense()" class="w-full bg-muji-text text-white p-3 rounded-xl font-bold btn-press">新增紀錄</button>
</div>
</div>
<div class="space-y-3">
${state.expenses.map(e => `
<div class="muji-card p-4 flex items-center justify-between">
<div class="flex items-center gap-3">
${e.img ? `<img src="${e.img}" class="w-10 h-10 object-cover rounded-lg">` : `<div class="w-10 h-10 bg-muji-bg flex items-center justify-center rounded-lg text-muji-border"><i class="fa-solid fa-receipt"></i></div>`}
<div>
<p class="text-sm font-bold">${e.item}</p>
<p class="text-[10px] text-muji-accent">${new Date(e.id).toLocaleTimeString()}</p>
</div>
</div>
<div class="text-right flex items-center gap-4">
<div>
<p class="text-sm font-mono font-bold">¥${e.amount}</p>
<p class="text-[9px] text-muji-red">NT$${Math.round(e.amount * state.rate)}</p>
</div>
<button onclick="deleteExpense(${e.id})" class="text-muji-border"><i class="fa-solid fa-trash-can text-xs"></i></button>
</div>
</div>
`).reverse().join('')}
</div>`;
}
if (state.currentTab === 'lists') {
html += `
<div class="muji-card p-6 mb-6">
<h3 class="font-serif font-bold mb-4 flex items-center gap-2"><i class="fa-solid fa-check-circle text-muji-red"></i> 裝備檢查</h3>
<div class="grid grid-cols-2 gap-3">
${TRAVEL_DATA.checkList.map(item => `
<div onclick="toggleCheck('${item}')" class="flex items-center gap-2 p-3 rounded-xl ${state.checks[item] ? 'bg-muji-bg text-muji-accent opacity-50' : 'bg-muji-paper border border-muji-border'} transition-all cursor-pointer">
<i class="fa-solid ${state.checks[item] ? 'fa-square-check' : 'fa-square'}"></i>
<span class="text-xs ${state.checks[item] ? 'line-through' : ''}">${item}</span>
</div>
`).join('')}
</div>
</div>
<div class="muji-card p-6">
<h3 class="font-serif font-bold mb-4">隨手筆記</h3>
<div class="flex gap-2 mb-4">
<input id="memo-in" type="text" placeholder="記事或連結..." class="flex-1 bg-muji-bg p-3 rounded-xl text-sm outline-none">
<button onclick="addMemo()" class="bg-muji-red text-white px-4 rounded-xl btn-press"><i class="fa-solid fa-plus"></i></button>
</div>
<div class="space-y-2">
${state.memos.map((m, i) => {
const isLink = m.startsWith('http');
return `
<div class="flex items-center justify-between p-3 bg-muji-bg/30 rounded-lg border border-muji-border/50">
${isLink ? `<a href="${m}" target="_blank" class="text-xs text-muji-accent underline truncate flex-1"><i class="fa-solid fa-link mr-1"></i> 打開連結</a>` : `<span class="text-sm flex-1">${m}</span>`}
<button onclick="deleteMemo(${i})" class="text-muji-border ml-2"><i class="fa-solid fa-xmark"></i></button>
</div>`;
}).reverse().join('')}
</div>
</div>`;
}
if (state.currentTab === 'info') {
html += `
<div class="space-y-6">
<div class="muji-card p-6 border-l-8 border-l-muji-red">
<h3 class="font-serif font-bold mb-2">緊急聯絡資訊</h3>
<p class="text-xs text-muji-accent mb-4">在日本遇到緊急狀況請撥打以下號碼:</p>
<div class="grid grid-cols-2 gap-2 text-xs font-bold">
<div class="p-2 bg-muji-bg rounded">警察:110</div>
<div class="p-2 bg-muji-bg rounded">救護:119</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<a href="https://www.jma.go.jp/" class="muji-card p-6 flex flex-col items-center gap-2">
<i class="fa-solid fa-cloud-sun text-2xl text-muji-accent"></i>
<span class="text-xs font-bold">天氣預報</span>
</a>
<a href="https://translate.google.com/" class="muji-card p-6 flex flex-col items-center gap-2">
<i class="fa-solid fa-language text-2xl text-muji-accent"></i>
<span class="text-xs font-bold">即時翻譯</span>
</a>
</div>
<div class="muji-card p-6">
<h4 class="text-sm font-bold mb-3 border-b border-muji-border pb-2">注意事項</h4>
<ul class="text-xs space-y-2 text-muji-accent">
<li>• 1/12 為成人之日,市區餐廳會排隊,建議提早 11:15 用餐。</li>
<li>• 冬季駕駛請保持車距,避免急剎。</li>
<li>• 1/11 17:00 準時還車,避免延遲。</li>
</ul>
</div>
</div>`;
}
container.innerHTML = html;
}
window.onload = render;
</script>
Metadata
Metadata
Assignees
Labels
No labels