-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
273 lines (246 loc) · 32 KB
/
Copy pathindex.html
File metadata and controls
273 lines (246 loc) · 32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#f6f8fc">
<meta name="description" content="TravelLog — lịch trình Trung Quốc 19–26/10/2026.">
<link rel="manifest" href="./manifest.webmanifest">
<link rel="icon" href="./icon.svg" type="image/svg+xml">
<link rel="stylesheet" href="./app.css">
<style>
.language-select{height:32px;min-width:76px;padding:0 25px 0 9px;border:1px solid var(--line);border-radius:8px;background:var(--surface);color:var(--text);font-size:11px;font-weight:700;outline:none;cursor:pointer}
.language-select:focus{border-color:#8eb0f6;box-shadow:0 0 0 3px var(--primary-soft)}
.mobile-header .language-select{max-width:92px}
</style>
<title>TravelLog · Trung Quốc 2026</title>
</head>
<body>
<div class="app">
<aside class="sidebar">
<a class="brand" href="#top"><span class="brand-mark"><svg><use href="#i-plane"/></svg></span>TravelLog</a>
<nav class="side-nav">
<a class="active" href="#top"><span class="nav-icon"><svg><use href="#i-home"/></svg></span>Tổng quan</a>
<a href="#itinerary"><span class="nav-icon"><svg><use href="#i-calendar"/></svg></span>Lịch trình</a>
<a href="./flights/"><span class="nav-icon"><svg><use href="#i-plane"/></svg></span>Giá vé</a>
<a href="./flight-status.html"><span class="nav-icon"><svg><use href="#i-status"/></svg></span>Trạng thái</a>
<a href="./hotels/"><span class="nav-icon"><svg><use href="#i-pin"/></svg></span>Giá phòng</a>
<a href="#budget"><span class="nav-icon"><svg><use href="#i-wallet"/></svg></span>Ngân sách</a>
<a href="#checklist"><span class="nav-icon"><svg><use href="#i-check"/></svg></span>Chuẩn bị</a>
<a href="#notes"><span class="nav-icon"><svg><use href="#i-note"/></svg></span>Ghi chú</a>
</nav>
<div class="sidebar-foot">
<button class="sidebar-action" data-theme-toggle><span class="nav-icon"><svg><use href="#i-moon"/></svg></span>Đổi giao diện</button>
</div>
</aside>
<div class="main">
<header class="topbar">
<div class="crumb"><strong>Trung Quốc 2026</strong><span>19–26/10/2026</span></div>
<div class="top-actions">
<select class="language-select" data-language-select aria-label="Ngôn ngữ">
<option value="vi">VI</option><option value="en">EN</option><option value="zh">中文</option><option value="ja">日本語</option>
</select>
<a class="btn sm" href="./hotels/">Giá phòng</a>
<a class="btn sm" href="./flights/">Xem giá vé</a>
<a class="btn sm" href="./flight-status.html">Trạng thái chuyến bay</a>
<button class="icon-btn" data-theme-toggle aria-label="Đổi giao diện"><svg class="ui-icon"><use href="#i-moon"/></svg></button>
</div>
</header>
<header class="mobile-header">
<a class="brand" href="#top"><span class="brand-mark"><svg><use href="#i-plane"/></svg></span>TravelLog</a>
<div class="right">
<select class="language-select" data-language-select aria-label="Ngôn ngữ">
<option value="vi">VI</option><option value="en">EN</option><option value="zh">中文</option><option value="ja">日本語</option>
</select>
<button class="icon-btn" data-theme-toggle aria-label="Đổi giao diện"><svg class="ui-icon"><use href="#i-moon"/></svg></button>
</div>
</header>
<main class="content" id="top">
<section class="trip-hero">
<div class="hero-copy">
<div class="hero-kicker">Chuyến đi sắp tới</div>
<h1>Trung Quốc · 19–26/10/2026</h1>
<div class="hero-route">TP.HCM → Thượng Hải → Bắc Kinh → TP.HCM</div>
<div class="hero-meta">8 ngày 7 đêm · 6 người lớn + 1 em bé</div>
<div class="hero-actions"><a class="btn" href="#itinerary">Xem lịch trình</a><a class="btn secondary" href="./flights/">Theo dõi giá vé</a><a class="btn secondary" href="./flight-status.html">Trạng thái chuyến bay</a><a class="btn secondary" href="./hotels/">Theo dõi giá phòng</a></div>
</div>
<aside class="countdown"><span id="daysPrefix">Còn</span><strong id="daysLeft">—</strong><span id="daysUnit">ngày đến chuyến đi</span><b>Khởi hành 19/10/2026</b></aside>
</section>
<section class="stat-grid">
<article class="card stat-card"><div class="stat-top"><span class="label">Ngân sách dự kiến</span><span class="stat-icon"><svg><use href="#i-wallet"/></svg></span></div><strong id="summaryBudget">Chưa thiết lập</strong><small id="budgetPerPerson">Nhập ngân sách bên dưới</small></article>
<article class="card stat-card"><div class="stat-top"><span class="label">Chuyến bay</span><span class="stat-icon"><svg><use href="#i-plane"/></svg></span></div><strong>2 chặng</strong><small id="flightStatNote">Đang tải dữ liệu giá vé…</small></article>
<article class="card stat-card"><div class="stat-top"><span class="label">Thành phố chính</span><span class="stat-icon"><svg><use href="#i-pin"/></svg></span></div><strong>2</strong><small>Thượng Hải · Bắc Kinh</small></article>
<article class="card stat-card"><div class="stat-top"><span class="label">Việc còn lại</span><span class="stat-icon"><svg><use href="#i-check"/></svg></span></div><strong id="taskCount">7</strong><small id="taskStatus">0/7 đã hoàn thành</small></article>
</section>
<section class="dashboard-grid">
<article class="card" id="itinerary">
<div class="card-head"><h2>Lịch trình chính</h2><a href="./flights/">Xem giá vé</a></div>
<div class="timeline">
<div class="timeline-item"><div class="date-box"><strong>19</strong><span>TH10</span></div><span class="time-dot"></span><div class="timeline-info"><strong>TP.HCM → Thượng Hải <span class="pill success" id="outboundBadge">Đang kiểm tra</span></strong><div class="sub" id="outboundFlightText">Đang tải chuyến bay phù hợp…</div><div class="detail" id="outboundFlightMeta">SGN → PVG / SHA</div></div><div class="timeline-photo shanghai">上海</div></div>
<div class="timeline-item"><div class="date-box"><strong>20</strong><span>TH10</span></div><span class="time-dot"></span><div class="timeline-info"><strong>Thượng Hải</strong><div class="sub">Bến Thượng Hải · Dự Viên · phố cổ</div><div class="detail">Lịch nhẹ ngày đầu, ưu tiên nghỉ ngơi</div></div><div class="timeline-photo shanghai">上海</div></div>
<div class="timeline-item"><div class="date-box"><strong>23</strong><span>TH10</span></div><span class="time-dot"></span><div class="timeline-info"><strong>Thượng Hải → Bắc Kinh</strong><div class="sub">Di chuyển nội địa</div><div class="detail">Chốt tàu cao tốc hoặc chuyến bay sau</div></div><div class="timeline-photo beijing">北京</div></div>
<div class="timeline-item"><div class="date-box"><strong>26</strong><span>TH10</span></div><span class="time-dot"></span><div class="timeline-info"><strong>Bắc Kinh → TP.HCM <span class="pill success" id="returnBadge">Đang kiểm tra</span></strong><div class="sub" id="returnFlightText">Đang tải chuyến bay phù hợp…</div><div class="detail" id="returnFlightMeta">PEK / PKX → SGN</div></div><div class="timeline-photo beijing">北京</div></div>
</div>
</article>
<div class="stack">
<article class="card" id="checklist">
<div class="card-head"><h3>Chuẩn bị trước chuyến đi</h3><span class="pill primary" id="checkHeaderStatus">0%</span></div>
<div class="checklist">
<label class="check-item"><input id="c1" type="checkbox">Hộ chiếu còn hạn > 6 tháng</label>
<label class="check-item"><input id="c2" type="checkbox">Visa Trung Quốc</label>
<label class="check-item"><input id="c3" type="checkbox">Khách sạn</label>
<label class="check-item"><input id="c4" type="checkbox">eSIM / SIM Trung Quốc</label>
<label class="check-item"><input id="c5" type="checkbox">Bảo hiểm du lịch</label>
<label class="check-item"><input id="c6" type="checkbox">Ngoại tệ / thanh toán</label>
<label class="check-item"><input id="c7" type="checkbox">Vé máy bay</label>
<div class="progress"><span id="checkProgress"></span></div>
<div class="progress-meta"><span id="checkText">0/7 đã hoàn thành</span><span id="checkPercent">0%</span></div>
</div>
</article>
<article class="card" id="budget">
<div class="card-head"><h3>Ngân sách</h3><span class="pill primary">VND</span></div>
<div class="budget">
<div class="budget-total"><div><span>Tổng dự kiến</span><strong id="budgetTotal">Chưa thiết lập</strong></div><span id="budgetHint">0/5 hạng mục</span></div>
<div class="progress"><span id="budgetProgress"></span></div>
<div class="budget-row"><span>Chuyến bay</span><input data-budget="flight" inputmode="numeric" placeholder="0"></div>
<div class="budget-row"><span>Khách sạn</span><input data-budget="hotel" inputmode="numeric" placeholder="0"></div>
<div class="budget-row"><span>Ăn uống</span><input data-budget="food" inputmode="numeric" placeholder="0"></div>
<div class="budget-row"><span>Di chuyển nội địa</span><input data-budget="transport" inputmode="numeric" placeholder="0"></div>
<div class="budget-row"><span>Mua sắm & khác</span><input data-budget="other" inputmode="numeric" placeholder="0"></div>
</div>
</article>
</div>
<article class="card notes-card" id="notes">
<div class="card-head"><h3>Ghi chú</h3><span class="pill">Lưu trên thiết bị này</span></div>
<div class="notes-wrap"><textarea id="tripNotes" placeholder="Địa chỉ khách sạn, mã đặt chỗ, quán ăn, chi tiết tàu, điều cần nhớ..."></textarea><div class="notes-actions"><button class="btn sm" id="copyNotes">Sao chép</button><button class="btn sm" id="exportData">Xuất dữ liệu</button><label class="btn sm" for="importData">Nhập dữ liệu<input id="importData" type="file" accept="application/json" hidden></label><span class="save-state" id="saveState">Sẵn sàng</span></div></div>
</article>
</section>
</main>
</div>
</div>
<nav class="mobile-bottom">
<a class="active" href="#top"><span class="nav-icon"><svg><use href="#i-home"/></svg></span>Tổng quan</a>
<a href="./flight-status.html"><span class="nav-icon"><svg><use href="#i-status"/></svg></span>Trạng thái</a>
<a href="./flights/"><span class="nav-icon"><svg><use href="#i-plane"/></svg></span>Giá vé</a>
<a href="./hotels/"><span class="nav-icon"><svg><use href="#i-pin"/></svg></span>Giá phòng</a>
</nav>
<svg width="0" height="0" style="position:absolute;overflow:hidden" aria-hidden="true">
<symbol id="i-plane" viewBox="0 0 24 24"><path d="M10.5 4h3l2.2 6H20a2 2 0 1 1 0 4h-4.3l-2.2 6h-3l1.2-6H7l-2 2H3l1.2-4L3 8h2l2 2h4.7l-1.2-6Z"/></symbol>
<symbol id="i-home" viewBox="0 0 24 24"><path d="m3 11 9-8 9 8v9a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1Z"/></symbol>
<symbol id="i-calendar" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="16" rx="2.5"/><path d="M16 3v4M8 3v4M3 10h18"/></symbol>
<symbol id="i-wallet" viewBox="0 0 24 24"><path d="M4 6h14a2 2 0 0 1 2 2v11H4a2 2 0 0 1-2-2V6a3 3 0 0 1 3-3h12"/><path d="M16 11h5v5h-5a2.5 2.5 0 0 1 0-5Z"/></symbol>
<symbol id="i-note" viewBox="0 0 24 24"><rect x="4" y="3" width="16" height="18" rx="2.5"/><path d="M8 8h8M8 12h8M8 16h5"/></symbol>
<symbol id="i-check" viewBox="0 0 24 24"><rect x="3.5" y="3.5" width="17" height="17" rx="4"/><path d="m8 12 2.5 2.5L16.5 9"/></symbol>
<symbol id="i-pin" viewBox="0 0 24 24"><path d="M20 10c0 5-8 12-8 12S4 15 4 10a8 8 0 1 1 16 0Z"/><circle cx="12" cy="10" r="2.5"/></symbol>
<symbol id="i-status" viewBox="0 0 24 24"><path d="M4 17h4l2-10 4 13 2-8h4"/><path d="M3 4h18v16H3z"/></symbol>
<symbol id="i-moon" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8Z"/></symbol>
<symbol id="i-arrow-right" viewBox="0 0 24 24"><path d="M5 12h14M13 6l6 6-6 6"/></symbol>
</svg>
<script>
(() => {
'use strict';
const STORAGE_KEY = 'travel-language';
const LANGS = {vi:'vi',en:'en',zh:'zh-CN',ja:'ja'};
const DICT = {
vi: {'上海':'TH','北京':'BK'},
en: {
'Tổng quan':'Overview','Lịch trình':'Itinerary','Giá vé':'Flight prices','Ngân sách':'Budget','Chuẩn bị':'Preparation','Ghi chú':'Notes','Đổi giao diện':'Switch theme','Ngôn ngữ':'Language','Trung Quốc 2026':'China 2026','Xem giá vé':'View fares','Chuyến đi sắp tới':'Upcoming trip','Trung Quốc · 19–26/10/2026':'China · 19–26 Oct 2026','TP.HCM → Thượng Hải → Bắc Kinh → TP.HCM':'Ho Chi Minh City → Shanghai → Beijing → Ho Chi Minh City','8 ngày 7 đêm · 6 người lớn + 1 em bé':'8 days 7 nights · 6 adults + 1 infant','Xem lịch trình':'View itinerary','Theo dõi giá vé':'Track flight prices','Còn':'Departure in','ngày đến chuyến đi':'days','khởi hành hôm nay':'departing today','chuyến đi đã bắt đầu':'trip has started','Khởi hành 19/10/2026':'Departure 19 Oct 2026','Ngân sách dự kiến':'Planned budget','Chưa thiết lập':'Not set','Nhập ngân sách bên dưới':'Enter budget below','Chuyến bay':'Flights','2 chặng':'2 legs','Đang tải dữ liệu giá vé…':'Loading flight data…','Thành phố chính':'Main cities','Thượng Hải · Bắc Kinh':'Shanghai · Beijing','Việc còn lại':'Remaining tasks','Lịch trình chính':'Main itinerary','Đang kiểm tra':'Checking','Đang tải chuyến bay phù hợp…':'Loading a suitable flight…','TP.HCM → Thượng Hải':'Ho Chi Minh City → Shanghai','上海':'SHA','Thượng Hải':'Shanghai','Bến Thượng Hải · Dự Viên · phố cổ':'The Bund · Yu Garden · Old Town','Lịch nhẹ ngày đầu, ưu tiên nghỉ ngơi':'Keep the first day light and prioritize rest','Thượng Hải → Bắc Kinh':'Shanghai → Beijing','Di chuyển nội địa':'Domestic transfer','Chốt tàu cao tốc hoặc chuyến bay sau':'Finalize high-speed rail or a flight later','北京':'BJS','Bắc Kinh → TP.HCM':'Beijing → Ho Chi Minh City','Chuẩn bị trước chuyến đi':'Before the trip','Hộ chiếu còn hạn > 6 tháng':'Passport valid for more than 6 months','Visa Trung Quốc':'China visa','Khách sạn':'Hotel','eSIM / SIM Trung Quốc':'China eSIM / SIM','Bảo hiểm du lịch':'Travel insurance','Ngoại tệ / thanh toán':'Cash / payments','Vé máy bay':'Flight tickets','Tổng dự kiến':'Planned total','Ăn uống':'Food','Mua sắm & khác':'Shopping & other','Lưu trên thiết bị này':'Saved on this device','Địa chỉ khách sạn, mã đặt chỗ, quán ăn, chi tiết tàu, điều cần nhớ...':'Hotel address, booking codes, places to eat, train details, things to remember...','Sao chép':'Copy','Xuất dữ liệu':'Export data','Nhập dữ liệu':'Import data','Sẵn sàng':'Ready','TH10':'OCT','{done}/{total} đã hoàn thành':'{done}/{total} completed','Khoảng {amount} / người':'About {amount} / person','{filled}/{total} hạng mục':'{filled}/{total} categories','Đã lưu':'Saved','Đã sao chép':'Copied','Không thể sao chép':'Could not copy','File không hợp lệ':'Invalid file','Chưa có chuyến bay phù hợp':'No suitable flight found','Chưa có dữ liệu':'No data','Hãng bay':'Airline','Bay thẳng':'Direct','1 điểm dừng':'1 stop','{count} lựa chọn bay thẳng đang được theo dõi':'Tracking {count} direct-flight options','Có dữ liệu chuyến bay, chưa thấy lựa chọn bay thẳng':'Flight data loaded; no direct option found','Không tải được dữ liệu giá vé':'Could not load flight-price data','Trạng thái':'Status','Trạng thái chuyến bay':'Flight status'
},
zh: {
'Tổng quan':'概览','Lịch trình':'行程','Giá vé':'机票价格','Ngân sách':'预算','Chuẩn bị':'行前准备','Ghi chú':'备注','Đổi giao diện':'切换主题','Ngôn ngữ':'语言','Trung Quốc 2026':'中国 2026','Xem giá vé':'查看机票','Chuyến đi sắp tới':'即将出发','Trung Quốc · 19–26/10/2026':'中国 · 2026年10月19–26日','TP.HCM → Thượng Hải → Bắc Kinh → TP.HCM':'胡志明市 → 上海 → 北京 → 胡志明市','8 ngày 7 đêm · 6 người lớn + 1 em bé':'8天7晚 · 6名成人 + 1名婴儿','Xem lịch trình':'查看行程','Theo dõi giá vé':'跟踪机票价格','Còn':'距离出发还有','ngày đến chuyến đi':'天','khởi hành hôm nay':'今天出发','chuyến đi đã bắt đầu':'旅程已开始','Khởi hành 19/10/2026':'出发:2026年10月19日','Ngân sách dự kiến':'计划预算','Chưa thiết lập':'未设置','Nhập ngân sách bên dưới':'在下方输入预算','Chuyến bay':'航班','2 chặng':'2个航段','Đang tải dữ liệu giá vé…':'正在加载机票数据…','Thành phố chính':'主要城市','Thượng Hải · Bắc Kinh':'上海 · 北京','Việc còn lại':'剩余事项','Lịch trình chính':'主要行程','Đang kiểm tra':'正在检查','Đang tải chuyến bay phù hợp…':'正在加载合适的航班…','TP.HCM → Thượng Hải':'胡志明市 → 上海','上海':'上海','Thượng Hải':'上海','Bến Thượng Hải · Dự Viên · phố cổ':'外滩 · 豫园 · 老城','Lịch nhẹ ngày đầu, ưu tiên nghỉ ngơi':'首日安排轻松,优先休息','Thượng Hải → Bắc Kinh':'上海 → 北京','Di chuyển nội địa':'国内交通','Chốt tàu cao tốc hoặc chuyến bay sau':'稍后确认高铁或航班','北京':'北京','Bắc Kinh → TP.HCM':'北京 → 胡志明市','Chuẩn bị trước chuyến đi':'出发前准备','Hộ chiếu còn hạn > 6 tháng':'护照有效期超过6个月','Visa Trung Quốc':'中国签证','Khách sạn':'酒店','eSIM / SIM Trung Quốc':'中国 eSIM / SIM','Bảo hiểm du lịch':'旅行保险','Ngoại tệ / thanh toán':'现金 / 支付','Vé máy bay':'机票','Tổng dự kiến':'预计总额','Ăn uống':'餐饮','Mua sắm & khác':'购物及其他','Lưu trên thiết bị này':'保存在此设备','Địa chỉ khách sạn, mã đặt chỗ, quán ăn, chi tiết tàu, điều cần nhớ...':'酒店地址、预订编号、餐厅、火车信息、需要记住的事项...','Sao chép':'复制','Xuất dữ liệu':'导出数据','Nhập dữ liệu':'导入数据','Sẵn sàng':'就绪','TH10':'10月','{done}/{total} đã hoàn thành':'已完成 {done}/{total}','Khoảng {amount} / người':'约 {amount} / 人','{filled}/{total} hạng mục':'{filled}/{total} 个类别','Đã lưu':'已保存','Đã sao chép':'已复制','Không thể sao chép':'无法复制','File không hợp lệ':'文件无效','Chưa có chuyến bay phù hợp':'没有合适的航班','Chưa có dữ liệu':'暂无数据','Hãng bay':'航空公司','Bay thẳng':'直飞','1 điểm dừng':'1次中转','{count} lựa chọn bay thẳng đang được theo dõi':'正在跟踪 {count} 个直飞选项','Có dữ liệu chuyến bay, chưa thấy lựa chọn bay thẳng':'已加载航班数据,暂未发现直飞选项','Không tải được dữ liệu giá vé':'无法加载机票价格数据','Trạng thái':'状态','Trạng thái chuyến bay':'航班状态'
},
ja: {
'Tổng quan':'概要','Lịch trình':'旅程','Giá vé':'航空券価格','Ngân sách':'予算','Chuẩn bị':'準備','Ghi chú':'メモ','Đổi giao diện':'テーマ切替','Ngôn ngữ':'言語','Trung Quốc 2026':'中国 2026','Xem giá vé':'航空券を見る','Chuyến đi sắp tới':'次の旅行','Trung Quốc · 19–26/10/2026':'中国 · 2026年10月19日〜26日','TP.HCM → Thượng Hải → Bắc Kinh → TP.HCM':'ホーチミン市 → 上海 → 北京 → ホーチミン市','8 ngày 7 đêm · 6 người lớn + 1 em bé':'8日7泊 · 大人6名 + 乳児1名','Xem lịch trình':'旅程を見る','Theo dõi giá vé':'航空券価格を追跡','Còn':'出発まであと','ngày đến chuyến đi':'日','khởi hành hôm nay':'本日出発','chuyến đi đã bắt đầu':'旅行は開始済み','Khởi hành 19/10/2026':'出発:2026年10月19日','Ngân sách dự kiến':'予定予算','Chưa thiết lập':'未設定','Nhập ngân sách bên dưới':'下に予算を入力','Chuyến bay':'フライト','2 chặng':'2区間','Đang tải dữ liệu giá vé…':'航空券データを読み込み中…','Thành phố chính':'主要都市','Thượng Hải · Bắc Kinh':'上海 · 北京','Việc còn lại':'残りのタスク','Lịch trình chính':'主な旅程','Đang kiểm tra':'確認中','Đang tải chuyến bay phù hợp…':'適切なフライトを読み込み中…','TP.HCM → Thượng Hải':'ホーチミン市 → 上海','上海':'上海','Thượng Hải':'上海','Bến Thượng Hải · Dự Viên · phố cổ':'外灘 · 豫園 · 旧市街','Lịch nhẹ ngày đầu, ưu tiên nghỉ ngơi':'初日はゆったり、休息を優先','Thượng Hải → Bắc Kinh':'上海 → 北京','Di chuyển nội địa':'国内移動','Chốt tàu cao tốc hoặc chuyến bay sau':'高速鉄道またはフライトを後で確定','北京':'北京','Bắc Kinh → TP.HCM':'北京 → ホーチミン市','Chuẩn bị trước chuyến đi':'旅行前の準備','Hộ chiếu còn hạn > 6 tháng':'パスポート残存期間6か月超','Visa Trung Quốc':'中国ビザ','Khách sạn':'ホテル','eSIM / SIM Trung Quốc':'中国 eSIM / SIM','Bảo hiểm du lịch':'海外旅行保険','Ngoại tệ / thanh toán':'現金 / 支払い','Vé máy bay':'航空券','Tổng dự kiến':'予定合計','Ăn uống':'食事','Mua sắm & khác':'買い物・その他','Lưu trên thiết bị này':'この端末に保存','Địa chỉ khách sạn, mã đặt chỗ, quán ăn, chi tiết tàu, điều cần nhớ...':'ホテル住所、予約番号、食事場所、鉄道情報、覚えておくこと...','Sao chép':'コピー','Xuất dữ liệu':'データを書き出す','Nhập dữ liệu':'データを読み込む','Sẵn sàng':'準備完了','TH10':'10月','{done}/{total} đã hoàn thành':'{done}/{total} 完了','Khoảng {amount} / người':'約 {amount} / 人','{filled}/{total} hạng mục':'{filled}/{total} カテゴリ','Đã lưu':'保存済み','Đã sao chép':'コピーしました','Không thể sao chép':'コピーできません','File không hợp lệ':'無効なファイル','Chưa có chuyến bay phù hợp':'適切なフライトがありません','Chưa có dữ liệu':'データなし','Hãng bay':'航空会社','Bay thẳng':'直行便','1 điểm dừng':'乗継1回','{count} lựa chọn bay thẳng đang được theo dõi':'直行便 {count} 件を追跡中','Có dữ liệu chuyến bay, chưa thấy lựa chọn bay thẳng':'フライトデータはありますが直行便は見つかりません','Không tải được dữ liệu giá vé':'航空券価格データを読み込めません','Trạng thái':'運航状況','Trạng thái chuyến bay':'フライト状況'
}
};
const TITLES = {vi:'TravelLog · Trung Quốc 2026',en:'TravelLog · China 2026',zh:'TravelLog · 中国 2026',ja:'TravelLog · 中国 2026'};
const DESCRIPTIONS = {vi:'TravelLog — lịch trình Trung Quốc 19–26/10/2026.',en:'TravelLog — China itinerary, 19–26 Oct 2026.',zh:'TravelLog — 2026年10月19–26日中国行程。',ja:'TravelLog — 2026年10月19日〜26日の中国旅行。'};
const originalText = new WeakMap();
const originalAttrs = new WeakMap();
let currentLang = localStorage.getItem(STORAGE_KEY);
if (!LANGS[currentLang]) currentLang = 'vi';
const format = (template, vars={}) => String(template).replace(/\{(\w+)\}/g, (_, key) => vars[key] ?? `{${key}}`);
const t = (source, vars={}) => format(DICT[currentLang]?.[source] ?? source, vars);
function translateTextNode(node) {
const raw = node.nodeValue;
if (!raw || !raw.trim() || node.parentElement?.closest('script,style,textarea,option')) return;
if (!originalText.has(node)) originalText.set(node, raw.trim());
const source = originalText.get(node);
const lead = raw.match(/^\s*/)?.[0] || '';
const tail = raw.match(/\s*$/)?.[0] || '';
node.nodeValue = lead + t(source) + tail;
}
function translateAttributes(el) {
if (!(el instanceof Element)) return;
let saved = originalAttrs.get(el);
if (!saved) { saved = {}; originalAttrs.set(el, saved); }
for (const attr of ['placeholder','aria-label','title']) {
if (!el.hasAttribute(attr)) continue;
if (!(attr in saved)) saved[attr] = el.getAttribute(attr);
el.setAttribute(attr, t(saved[attr]));
}
}
function translateTree() {
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
let node;
while ((node = walker.nextNode())) {
if (node.nodeType === Node.TEXT_NODE) translateTextNode(node);
else translateAttributes(node);
}
}
function setLanguage(lang, notify=true) {
if (!LANGS[lang]) lang = 'vi';
currentLang = lang;
localStorage.setItem(STORAGE_KEY, lang);
document.documentElement.lang = LANGS[lang];
document.title = TITLES[lang];
const meta = document.querySelector('meta[name="description"]');
if (meta) meta.content = DESCRIPTIONS[lang];
translateTree();
document.querySelectorAll('[data-language-select]').forEach(select => { select.value = lang; });
if (notify) document.dispatchEvent(new CustomEvent('travel-language-change', {detail:{lang}}));
}
document.querySelectorAll('[data-language-select]').forEach(select => select.addEventListener('change', () => setLanguage(select.value)));
window.TravelDashboardI18n = {t:(source, vars={})=>t(source, vars), getLanguage:()=>currentLang, setLanguage};
setLanguage(currentLang, false);
})();
</script>
<script>
(() => {
const S='travel-log-v3', $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)], state=JSON.parse(localStorage.getItem(S)||'{}'), save=()=>localStorage.setItem(S,JSON.stringify(state));
const I18N=window.TravelDashboardI18n, t=(source,vars={})=>I18N.t(source,vars);
const locale=()=>({vi:'vi-VN',en:'en-US',zh:'zh-CN',ja:'ja-JP'}[I18N.getLanguage()]||'vi-VN');
const money=n=>`${new Intl.NumberFormat(locale()).format(Number(n)||0)}${I18N.getLanguage()==='vi'?' đ':' VND'}`;
$$('[data-theme-toggle]').forEach(b=>b.onclick=()=>{const dark=document.documentElement.dataset.theme==='dark';document.documentElement.dataset.theme=dark?'light':'dark';localStorage.setItem('travel-theme',dark?'light':'dark')});
const theme=localStorage.getItem('travel-theme'); if(theme) document.documentElement.dataset.theme=theme;
const dep=new Date('2026-10-19T20:00:00+07:00');
function renderCountdown(){const days=Math.ceil((dep-new Date())/86400000);if(days>0){$('#daysPrefix').textContent=t('Còn');$('#daysLeft').textContent=days;$('#daysUnit').textContent=t('ngày đến chuyến đi')}else if(days===0){$('#daysPrefix').textContent='';$('#daysLeft').textContent='0';$('#daysUnit').textContent=t('khởi hành hôm nay')}else{$('#daysPrefix').textContent='';$('#daysLeft').textContent='✓';$('#daysUnit').textContent=t('chuyến đi đã bắt đầu')}}
renderCountdown();
const checks=$$('.check-item input');
checks.forEach(c=>{c.checked=!!state[c.id];c.onchange=()=>{state[c.id]=c.checked;save();renderChecks()}});
function renderChecks(){const done=checks.filter(c=>c.checked).length,p=Math.round(done/checks.length*100);$('#taskCount').textContent=checks.length-done;const status=t('{done}/{total} đã hoàn thành',{done,total:checks.length});$('#taskStatus').textContent=status;$('#checkText').textContent=status;$('#checkPercent').textContent=p+'%';$('#checkHeaderStatus').textContent=p+'%';$('#checkProgress').style.width=p+'%'}
renderChecks();
const inputs=$$('[data-budget]');
inputs.forEach(i=>{i.value=state.budget?.[i.dataset.budget]||'';i.oninput=()=>{state.budget=state.budget||{};state.budget[i.dataset.budget]=i.value.replace(/\D/g,'');i.value=state.budget[i.dataset.budget];save();renderBudget()}});
function renderBudget(){const vals=inputs.map(i=>Number(i.value)||0),total=vals.reduce((a,b)=>a+b,0),filled=inputs.filter(i=>Number(i.value)>0).length,p=filled/inputs.length*100;$('#budgetTotal').textContent=total?money(total):t('Chưa thiết lập');$('#summaryBudget').textContent=total?money(total):t('Chưa thiết lập');$('#budgetPerPerson').textContent=total?t('Khoảng {amount} / người',{amount:money(Math.round(total/7))}):t('Nhập ngân sách bên dưới');$('#budgetHint').textContent=t('{filled}/{total} hạng mục',{filled,total:inputs.length});$('#budgetProgress').style.width=p+'%'}
renderBudget();
let saveStateMessage='Sẵn sàng';
const setSaveState=source=>{saveStateMessage=source;$('#saveState').textContent=t(source)};
const notes=$('#tripNotes'); notes.value=state.notes||'';
notes.oninput=()=>{state.notes=notes.value;save();setSaveState('Đã lưu')};
$('#copyNotes').onclick=async()=>{try{await navigator.clipboard.writeText(notes.value);setSaveState('Đã sao chép')}catch{setSaveState('Không thể sao chép')}};
$('#exportData').onclick=()=>{const a=document.createElement('a'),blob=new Blob([JSON.stringify({version:3,state},null,2)],{type:'application/json'});a.href=URL.createObjectURL(blob);a.download='travellog-backup.json';a.click();URL.revokeObjectURL(a.href)};
$('#importData').onchange=e=>{const f=e.target.files[0];if(!f)return;const r=new FileReader;r.onload=()=>{try{const d=JSON.parse(r.result);localStorage.setItem(S,JSON.stringify(d.state||d));location.reload()}catch{setSaveState('File không hợp lệ')}};r.readAsText(f)};
const isDirect=o=>o?.is_direct===true||Number(o?.stops)===0;
const time=v=>!v?'—':String(v).slice(11,16);
const preferred=r=>[...(r?.offers||[])].sort((a,b)=>{if(isDirect(a)!==isDirect(b))return isDirect(a)?-1:1;return Number(a.total_amount)-Number(b.total_amount)})[0];
function paintFlight(r,text,meta,badge){const o=preferred(r);if(!o){$(text).textContent=t('Chưa có chuyến bay phù hợp');$(badge).textContent=t('Chưa có dữ liệu');$(badge).className='pill';return}const s=o.slices?.[0],seg=s?.segments?.[0],last=s?.segments?.at(-1);$(text).textContent=`${time(seg?.departing_at)} ${s?.origin||seg?.origin} → ${time(last?.arriving_at)} ${s?.destination||last?.destination}`;$(meta).textContent=`${o.owner?.name||t('Hãng bay')}${seg?.flight_number?' · '+seg.flight_number:''} · ${money(o.total_amount)}`;$(badge).textContent=isDirect(o)?t('Bay thẳng'):t('1 điểm dừng');$(badge).className='pill '+(isDirect(o)?'success':'warning')}
let flightData=null;
function renderFlights(data){const out=data.routes?.find(r=>r.id==='outbound'),ret=data.routes?.find(r=>r.id==='return');paintFlight(out,'#outboundFlightText','#outboundFlightMeta','#outboundBadge');paintFlight(ret,'#returnFlightText','#returnFlightMeta','#returnBadge');const direct=(data.routes||[]).reduce((n,r)=>n+(r.offers||[]).filter(isDirect).length,0);$('#flightStatNote').textContent=direct?t('{count} lựa chọn bay thẳng đang được theo dõi',{count:direct}):t('Có dữ liệu chuyến bay, chưa thấy lựa chọn bay thẳng')}
fetch(`./data/flights.json?t=${Date.now()}`,{cache:'no-store'}).then(r=>r.json()).then(data=>{flightData=data;renderFlights(data)}).catch(()=>{$('#flightStatNote').textContent=t('Không tải được dữ liệu giá vé')});
document.addEventListener('travel-language-change',()=>{renderCountdown();renderChecks();renderBudget();setSaveState(saveStateMessage);if(flightData)renderFlights(flightData)});
if('serviceWorker'in navigator)window.addEventListener('load',()=>navigator.serviceWorker.register('./sw.js').catch(()=>{}));
})();
</script>
</body>
</html>