Skip to content

Commit 576383b

Browse files
authored
Add files via upload
1 parent 5b526bb commit 576383b

3 files changed

Lines changed: 60 additions & 89 deletions

File tree

app.js

Lines changed: 33 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
/* (L)TravelCal - Version 0.29 */
1+
/* (L)TravelCal - Version 0.31 */
22

3-
let map, ds, drGo;
3+
let map, ds, drGo, drBack;
44
let returnMode = false;
55

66
const TUNNEL_DATA = [
@@ -17,7 +17,15 @@ const TUNNEL_DATA = [
1717

1818
function initApp() {
1919
ds = new google.maps.DirectionsService();
20-
drGo = new google.maps.DirectionsRenderer({ polylineOptions: { strokeColor: "#E3193F", strokeWeight: 5 } });
20+
// ✅ 去程:紅色
21+
drGo = new google.maps.DirectionsRenderer({
22+
polylineOptions: { strokeColor: "#E3193F", strokeWeight: 6, strokeOpacity: 0.8 }
23+
});
24+
// ✅ 回程:藍色
25+
drBack = new google.maps.DirectionsRenderer({
26+
polylineOptions: { strokeColor: "#1976D2", strokeWeight: 5, strokeOpacity: 0.7 },
27+
suppressMarkers: true
28+
});
2129

2230
const now = new Date();
2331
const offset = now.getTimezoneOffset() * 60000;
@@ -27,7 +35,6 @@ function initApp() {
2735
document.querySelectorAll('.node-input').forEach(bindAutocomplete);
2836
renderButtons('goTunnels', 'go');
2937
renderButtons('backTunnels', 'back');
30-
smartFilterTunnels();
3138
}
3239

3340
function bindAutocomplete(inp) {
@@ -73,65 +80,52 @@ function smartFilterTunnels() {
7380
const combined = Array.from(document.querySelectorAll('.node-input')).map(i => i.value.toLowerCase()).join(" ");
7481
document.querySelectorAll('.t-btn').forEach(btn => {
7582
const data = TUNNEL_DATA.find(d => d.loc === btn.getAttribute('data-loc'));
76-
const matched = data.match.toLowerCase().split('|').some(term => combined.includes(term));
77-
if (showAll || matched) btn.classList.add('visible');
83+
if (showAll || data.match.toLowerCase().split('|').some(term => combined.includes(term))) btn.classList.add('visible');
7884
else btn.classList.remove('visible', 'active');
7985
});
8086
}
8187

8288
async function calculate() {
83-
const btn = document.querySelector('.primary-btn');
84-
btn.style.opacity = "0.7";
85-
8689
const inputs = document.querySelectorAll('.node-input');
8790
const locs = Array.from(inputs).map(i => i.value).filter(v => v.length > 2);
88-
if (locs.length < 2) { btn.style.opacity = "1"; return; }
91+
if (locs.length < 2) return;
92+
93+
if (!map) map = new google.maps.Map(document.getElementById('map'), {
94+
zoom: 12, center: { lat: 22.3, lng: 114.1 },
95+
disableDefaultUI: true,
96+
styles: [{stylers:[{invert_lightness:true}]}]
97+
});
98+
99+
drGo.setMap(null); drBack.setMap(null);
89100

90101
const goTime = new Date(document.getElementById('start-time').value);
91-
const goSelected = Array.from(document.querySelectorAll('.go-t.active')).map(b =>
92-
TUNNEL_DATA.find(d => d.loc === b.getAttribute('data-loc'))
93-
);
102+
const goSelected = Array.from(document.querySelectorAll('.go-t.active')).map(b => TUNNEL_DATA.find(d => d.loc === b.getAttribute('data-loc')));
94103
const go = await getRouteData(locs[0], locs[locs.length-1], goSelected, goTime);
95104

96105
let totalKm = go.km, totalToll = go.toll, totalSec = go.sec;
97106

107+
if (go.raw) { drGo.setMap(map); drGo.setDirections(go.raw); }
108+
98109
if (returnMode) {
99110
const backTime = new Date(document.getElementById('return-time').value);
100-
const backSelected = Array.from(document.querySelectorAll('.back-t.active')).map(b =>
101-
TUNNEL_DATA.find(d => d.loc === b.getAttribute('data-loc'))
102-
);
111+
const backSelected = Array.from(document.querySelectorAll('.back-t.active')).map(b => TUNNEL_DATA.find(d => d.loc === b.getAttribute('data-loc')));
103112
const back = await getRouteData(locs[locs.length-1], locs[0], backSelected, backTime);
104-
totalKm += back.km;
105-
totalToll += back.toll;
106-
totalSec += back.sec;
113+
totalKm += back.km; totalToll += back.toll; totalSec += back.sec;
114+
if (back.raw) { drBack.setMap(map); drBack.setDirections(back.raw); }
107115
}
108116

109-
if (go.raw) {
110-
document.getElementById('map').style.display = 'block';
111-
if (!map) map = new google.maps.Map(document.getElementById('map'), { zoom: 12, center: { lat: 22.3, lng: 114.1 }, disableDefaultUI: true, styles: [{stylers:[{invert_lightness:true}]}] });
112-
drGo.setMap(map);
113-
drGo.setDirections(go.raw);
114-
}
115117
updateUI(totalKm, totalToll, totalSec);
116-
btn.style.opacity = "1";
117118
}
118119

119120
async function getRouteData(start, end, tunnels, time) {
120121
return new Promise(resolve => {
121122
let pts = tunnels.map(t => ({ location: t.loc, stopover: true, lat: t.lat, toll: getToll(t.loc, time) }));
122-
const ntKeywords = ['sha tin', 'tai po', 'fanling', 'yuen long', 'tuen mun', 'fo tan', '沙田', '火炭', '大埔'];
123-
const isNorthbound = ntKeywords.some(k => end.toLowerCase().includes(k));
124-
if (isNorthbound) pts.sort((a, b) => a.lat - b.lat);
125-
else pts.sort((a, b) => b.lat - a.lat);
126-
127-
const tollSum = pts.reduce((a, b) => a + b.toll, 0);
128-
const cleanWays = pts.map(p => ({ location: p.location, stopover: true }));
129-
130-
ds.route({ origin: start, destination: end, waypoints: cleanWays, travelMode: 'DRIVING', optimizeWaypoints: false }, (res, stat) => {
123+
pts.sort((a, b) => end.includes('Sha Tin') ? a.lat - b.lat : b.lat - a.lat);
124+
ds.route({ origin: start, destination: end, waypoints: pts.map(p=>({location:p.location, stopover:true})), travelMode: 'DRIVING' }, (res, stat) => {
131125
if (stat === 'OK') {
132126
const km = res.routes[0].legs.reduce((a, b) => a + b.distance.value, 0) / 1000;
133127
const sec = res.routes[0].legs.reduce((a, b) => a + b.duration.value, 0);
134-
resolve({ km, toll: tollSum, sec, raw: res });
128+
resolve({ km, toll: pts.reduce((a,b)=>a+b.toll, 0), sec, raw: res });
135129
} else resolve({ km: 0, toll: 0, sec: 0, raw: null });
136130
});
137131
});
@@ -140,29 +134,19 @@ async function getRouteData(start, end, tunnels, time) {
140134
function updateUI(km, toll, sec) {
141135
const car = document.getElementById('car-model').value.split('|');
142136
const energy = km * parseFloat(car[0]) * parseFloat(car[1]);
143-
const evTotal = energy + toll;
144-
145137
document.getElementById('km').innerText = km.toFixed(1) + " km";
146138
document.getElementById('duration').innerText = Math.round(sec / 60) + " min";
147139
document.getElementById('t-fee').innerText = "$" + toll;
148140
document.getElementById('e-cost').innerText = "$" + energy.toFixed(1);
149-
document.getElementById('total').innerText = evTotal.toFixed(1);
150-
151-
const fuelCostBase = km * 1.8;
152-
const savedAmount = fuelCostBase - energy;
153-
const savingsEl = document.getElementById('savings');
154-
savingsEl.innerText = "$" + Math.max(0, savedAmount).toFixed(1);
155-
156-
savingsEl.style.color = "#FFD700";
157-
setTimeout(() => { savingsEl.style.color = "#4CD964"; }, 500);
141+
document.getElementById('total').innerText = (energy + toll).toFixed(1);
142+
document.getElementById('savings').innerText = "$" + (km * 1.8 - energy).toFixed(1);
158143
}
159144

160145
function addNode() {
161-
const container = document.getElementById('nodes-container');
162146
const div = document.createElement('div');
163147
div.className = 'input-group';
164148
div.innerHTML = `<input class="node-input" placeholder="中途站" autocomplete="off">`;
165-
container.appendChild(div);
149+
document.getElementById('nodes-container').appendChild(div);
166150
bindAutocomplete(div.querySelector('.node-input'));
167151
}
168152

index.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<link rel="icon" type="image/x-icon" href="favicon.ico">
99
<link rel="apple-touch-icon" href="favicon.ico">
1010

11-
<link rel="stylesheet" href="style.css?v=0.30">
11+
<link rel="stylesheet" href="style.css?v=0.31">
1212
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
1313
</head>
1414
<body>
@@ -116,9 +116,9 @@ <h1>(L)TravelCal</h1>
116116
</footer>
117117
</div>
118118

119-
<div class="version-tag">(L)TravelCal v0.30</div>
119+
<div class="version-tag">(L)TravelCal v0.31</div>
120120

121121
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBrlGmF4KTgFE8TeZyntoyaxxc6Qq0e7jY&libraries=places&callback=initApp" async defer></script>
122-
<script src="app.js?v=0.30"></script>
122+
<script src="app.js?v=0.31"></script>
123123
</body>
124124
</html>

style.css

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,79 +4,66 @@
44
--savings: #4CD964;
55
}
66

7-
body {
8-
background-color: var(--bg); color: var(--text);
9-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
10-
margin: 0; padding: 0;
11-
}
7+
body { background-color: var(--bg); color: var(--text); font-family: -apple-system, sans-serif; margin: 0; padding: 0; }
128

9+
/* ✅ 優化 Banner:減輕漸層以露出更多白色 Model 3 */
1310
.app-banner {
1411
width: 100%; height: 260px;
15-
/* ✅ 確保檔名大小寫與 GitHub 一致 */
16-
background-image: url('banner.jpg');
17-
background-size: cover; background-position: center; position: relative;
18-
background-color: #141414;
12+
background-image: url('banner.jpg'); background-size: cover; background-position: center; position: relative;
1913
}
20-
2114
.banner-overlay {
2215
position: absolute; bottom: 0; left: 0; right: 0; padding: 40px 20px;
23-
background: linear-gradient(to top, var(--bg) 40%, transparent);
24-
}
25-
26-
.banner-overlay h1 {
27-
font-size: 28px; font-weight: 800; margin: 0; letter-spacing: -1px;
28-
text-shadow: 0 4px 15px rgba(0,0,0,0.8);
16+
background: linear-gradient(to top, #050505 10%, rgba(5,5,5,0.6) 40%, transparent 100%);
2917
}
18+
.banner-overlay h1 { font-size: 32px; font-weight: 800; margin: 0; text-shadow: 0 4px 15px rgba(0,0,0,0.9); }
19+
.banner-overlay p { font-size: 13px; color: var(--text-dim); margin-top: 5px; text-shadow: 0 2px 8px rgba(0,0,0,0.8); }
3020

31-
.banner-overlay p {
32-
font-size: 12px; color: var(--text-dim); margin: 6px 0 0;
33-
font-weight: 400; letter-spacing: 0.5px;
34-
text-shadow: 0 2px 8px rgba(0,0,0,0.8);
35-
}
36-
37-
.app-container { max-width: 500px; margin: -30px auto 0; position: relative; z-index: 2; padding: 0 16px; box-sizing: border-box; }
38-
21+
.app-container { max-width: 500px; margin: -20px auto 0; position: relative; z-index: 2; padding: 0 16px; box-sizing: border-box; }
3922
.card { background: var(--card); border-radius: 18px; padding: 18px; margin-bottom: 12px; border: 1px solid var(--border); }
4023
.card-header { display: flex; align-items: center; gap: 8px; margin-bottom: 15px; }
4124
.card-header label { font-size: 11px; font-weight: 700; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; }
4225

26+
/* ✅ 強化的深色 Datetime 與輸入框樣式 */
4327
.node-input, input[type="datetime-local"], select {
4428
width: 100%; background: #1C1C1E; border: 1px solid var(--border); border-radius: 12px;
4529
padding: 14px; color: white; font-size: 15px; margin-bottom: 8px; box-sizing: border-box;
46-
color-scheme: dark; /* 讓日期控制項變深色 */
30+
color-scheme: dark; /* 讓 iOS/Android 的日期面板也變深色 */
4731
}
4832

49-
.action-row { display: flex; gap: 10px; margin-top: 10px; align-items: stretch; }
33+
.action-row { display: flex; gap: 10px; margin-top: 10px; }
5034
.secondary-btn {
51-
flex: 1; height: 44px; background: #2C2C2E; border: 1px solid transparent; color: white;
52-
border-radius: 12px; font-size: 13px; display: flex; align-items: center; justify-content: center; gap: 6px; cursor: pointer;
35+
flex: 1; height: 44px; background: #2C2C2E; color: white; border: none;
36+
border-radius: 12px; font-size: 13px; display: flex; align-items: center; justify-content: center; gap: 6px;
5337
}
5438
.secondary-btn.active { background: #2C1014; color: var(--primary); border: 1px solid var(--primary); }
5539

5640
.tunnel-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
5741
.t-btn {
5842
display: none; background: #1C1C1E; border: 1px solid var(--border);
59-
padding: 12px 4px; border-radius: 10px; color: #666; font-size: 11px; text-align: center; cursor: pointer;
43+
padding: 12px 4px; border-radius: 10px; color: #666; font-size: 11px; text-align: center;
6044
}
6145
.t-btn.visible { display: block; }
6246
.t-btn.active { background: #2C1014; border-color: var(--primary); color: var(--primary); font-weight: bold; }
6347

64-
#map { height: 260px; border-radius: 16px; margin: 15px 0; border: 1px solid var(--border); display: none; }
48+
#map { height: 280px; border-radius: 16px; margin: 15px 0; border: 1px solid var(--border); }
6549

6650
.stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
6751
.stat-card { background: var(--card); padding: 15px; border-radius: 16px; border: 1px solid var(--border); text-align: center; }
68-
.stat-card span { display: block; font-size: 10px; color: var(--text-dim); text-transform: uppercase; margin-bottom: 5px; }
69-
.stat-card b { font-size: 18px; color: var(--text); }
52+
.stat-card span { display: block; font-size: 10px; color: var(--text-dim); text-transform: uppercase; }
53+
.stat-card b { font-size: 18px; }
7054

7155
.bottom-nav {
7256
position: fixed; bottom: 0; left: 0; right: 0;
73-
background: rgba(20, 20, 20, 0.85); backdrop-filter: blur(25px);
57+
background: rgba(20, 20, 20, 0.9); backdrop-filter: blur(20px);
7458
border-top: 1px solid var(--border); padding: 15px 20px 45px; z-index: 100;
7559
}
7660
.nav-content { max-width: 460px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; }
7761
.price-val { font-size: 30px; font-weight: 800; color: var(--primary); }
78-
.savings-tag { font-size: 11px; color: var(--savings); margin-top: 4px; display: flex; align-items: center; gap: 4px; font-weight: 500; }
79-
.primary-btn { background: var(--primary); color: white; border: none; padding: 15px 30px; border-radius: 14px; font-weight: bold; cursor: pointer; }
62+
.primary-btn { background: var(--primary); color: white; border: none; padding: 15px 25px; border-radius: 14px; font-weight: bold; }
63+
.savings-tag { font-size: 11px; color: var(--savings); margin-top: 4px; display: flex; align-items: center; gap: 4px; }
8064

81-
.checkbox-row { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-dim); margin-top: 10px; }
82-
.version-tag { position: fixed; right: 15px; bottom: 130px; font-size: 9px; color: var(--text-dim); opacity: 0.3; }
65+
/* ✅ 修正後的可視版本號 */
66+
.version-tag {
67+
position: fixed; right: 15px; bottom: 145px; z-index: 1000;
68+
font-size: 9px; color: var(--text-dim); background: rgba(0,0,0,0.5); padding: 2px 6px; border-radius: 4px;
69+
}

0 commit comments

Comments
 (0)