Skip to content

Commit 7a9dbf9

Browse files
committed
feat: 實現 Apple 風格的場景卡片橫向滾動
- 將場景列表改為橫向滾動佈局 - 添加滾動指示器(8個小圓點) - 實現觸摸和滑鼠拖曳滾動 - 添加左右漸變效果提示可滾動 - 支援點擊指示器跳轉到對應場景 - 優化響應式設計和使用體驗
1 parent 2db4174 commit 7a9dbf9

File tree

1 file changed

+218
-7
lines changed

1 file changed

+218
-7
lines changed

index.html

Lines changed: 218 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -149,19 +149,46 @@
149149
}
150150

151151
.scenarios {
152-
margin: 40px 0;
152+
margin: 80px 0;
153+
position: relative;
153154
}
154155

155156
.scenarios h2 {
156-
color: #333;
157-
margin-bottom: 25px;
157+
color: #1d1d1f;
158+
margin-bottom: 32px;
158159
text-align: center;
160+
font-size: 2.5rem;
161+
font-weight: 600;
162+
letter-spacing: -0.03em;
163+
}
164+
165+
.scenario-wrapper {
166+
position: relative;
167+
margin: 0 -20px;
168+
padding: 0 20px;
159169
}
160170

161171
.scenario-list {
162-
display: grid;
163-
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
172+
display: flex;
164173
gap: 20px;
174+
overflow-x: auto;
175+
scroll-snap-type: x mandatory;
176+
scroll-behavior: smooth;
177+
-webkit-overflow-scrolling: touch;
178+
padding: 20px 0;
179+
margin: -20px 0;
180+
cursor: grab;
181+
}
182+
183+
.scenario-list::-webkit-scrollbar {
184+
height: 0;
185+
}
186+
187+
@media (min-width: 769px) {
188+
.scenario-list {
189+
padding: 20px calc(max((100vw - 1200px) / 2, 20px));
190+
margin: -20px calc(min((100vw - 1200px) / -2, -20px));
191+
}
165192
}
166193

167194
.scenario-item {
@@ -174,6 +201,15 @@
174201
position: relative;
175202
overflow: hidden;
176203
box-shadow: none;
204+
flex: 0 0 320px;
205+
scroll-snap-align: start;
206+
}
207+
208+
@media (max-width: 768px) {
209+
.scenario-item {
210+
flex: 0 0 280px;
211+
padding: 32px;
212+
}
177213
}
178214

179215
.scenario-item::before {
@@ -399,6 +435,66 @@
399435
font-size: 0.9rem;
400436
}
401437

438+
/* 滾動指示器 */
439+
.scroll-indicator {
440+
display: flex;
441+
justify-content: center;
442+
gap: 8px;
443+
margin-top: 32px;
444+
padding: 20px;
445+
}
446+
447+
.scroll-dot {
448+
width: 8px;
449+
height: 8px;
450+
border-radius: 50%;
451+
background: rgba(0, 0, 0, 0.2);
452+
transition: all 0.3s ease;
453+
cursor: pointer;
454+
}
455+
456+
.scroll-dot.active {
457+
background: #1d1d1f;
458+
transform: scale(1.2);
459+
}
460+
461+
/* 添加滑動提示漸變 */
462+
.scenario-wrapper::after {
463+
content: '';
464+
position: absolute;
465+
top: 20px;
466+
right: 0;
467+
bottom: 20px;
468+
width: 100px;
469+
background: linear-gradient(to left, #ffffff 0%, transparent 100%);
470+
pointer-events: none;
471+
z-index: 1;
472+
opacity: 1;
473+
transition: opacity 0.3s ease;
474+
}
475+
476+
.scenario-wrapper::before {
477+
content: '';
478+
position: absolute;
479+
top: 20px;
480+
left: 0;
481+
bottom: 20px;
482+
width: 100px;
483+
background: linear-gradient(to right, #ffffff 0%, transparent 100%);
484+
pointer-events: none;
485+
z-index: 1;
486+
opacity: 0;
487+
transition: opacity 0.3s ease;
488+
}
489+
490+
.scenario-wrapper.scrolled::before {
491+
opacity: 1;
492+
}
493+
494+
.scenario-wrapper.scrolled-end::after {
495+
opacity: 0;
496+
}
497+
402498
@media (max-width: 768px) {
403499
.header h1 {
404500
font-size: 2rem;
@@ -411,6 +507,10 @@
411507
.features {
412508
grid-template-columns: 1fr;
413509
}
510+
511+
.scenario-wrapper::after {
512+
width: 50px;
513+
}
414514
}
415515
</style>
416516
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
@@ -727,8 +827,9 @@ <h3 style="font-size: 1.25rem; font-weight: 600; color: #1d1d1f;">手機掃描 Q
727827
</div>
728828

729829
<div class="scenarios">
730-
<h2 style="font-size: 2rem; font-weight: 600; margin-bottom: 32px;">8 個學習場景</h2>
731-
<div class="scenario-list">
830+
<h2>8 個學習場景</h2>
831+
<div class="scenario-wrapper">
832+
<div class="scenario-list">
732833
<div class="scenario-item">
733834
<div class="scenario-number">01</div>
734835
<div class="scenario-title">初次對話體驗</div>
@@ -820,6 +921,17 @@ <h2 style="font-size: 2rem; font-weight: 600; margin-bottom: 32px;">8 個學習
820921
</div>
821922
</div>
822923
</div>
924+
<div class="scroll-indicator">
925+
<span class="scroll-dot active" data-index="0"></span>
926+
<span class="scroll-dot" data-index="1"></span>
927+
<span class="scroll-dot" data-index="2"></span>
928+
<span class="scroll-dot" data-index="3"></span>
929+
<span class="scroll-dot" data-index="4"></span>
930+
<span class="scroll-dot" data-index="5"></span>
931+
<span class="scroll-dot" data-index="6"></span>
932+
<span class="scroll-dot" data-index="7"></span>
933+
</div>
934+
</div>
823935
</div>
824936
</div>
825937

@@ -1110,6 +1222,105 @@ <h2>❌ 載入失敗</h2>
11101222
colorLight: "#ffffff",
11111223
correctLevel: QRCode.CorrectLevel.H
11121224
});
1225+
1226+
// 初始化場景滾動
1227+
initScenarioScroll();
1228+
}
1229+
1230+
// 場景滾動功能
1231+
function initScenarioScroll() {
1232+
const scenarioList = document.querySelector('.scenario-list');
1233+
const scrollDots = document.querySelectorAll('.scroll-dot');
1234+
const scenarioItems = document.querySelectorAll('.scenario-item');
1235+
const scenarioWrapper = document.querySelector('.scenario-wrapper');
1236+
1237+
if (!scenarioList || !scrollDots.length || !scenarioItems.length) return;
1238+
1239+
// 計算每個項目的寬度和間距
1240+
const itemWidth = scenarioItems[0].offsetWidth + 20; // 包含 gap
1241+
1242+
// 更新滾動指示器
1243+
function updateScrollIndicator() {
1244+
const scrollLeft = scenarioList.scrollLeft;
1245+
const scrollWidth = scenarioList.scrollWidth;
1246+
const clientWidth = scenarioList.clientWidth;
1247+
1248+
// 計算當前顯示的場景索引
1249+
const currentIndex = Math.round(scrollLeft / itemWidth);
1250+
1251+
// 更新指示器狀態
1252+
scrollDots.forEach((dot, index) => {
1253+
if (index === currentIndex) {
1254+
dot.classList.add('active');
1255+
} else {
1256+
dot.classList.remove('active');
1257+
}
1258+
});
1259+
1260+
// 檢查滾動位置,控制左右漸變
1261+
if (scrollLeft > 10) {
1262+
scenarioWrapper.classList.add('scrolled');
1263+
} else {
1264+
scenarioWrapper.classList.remove('scrolled');
1265+
}
1266+
1267+
if (scrollLeft + clientWidth >= scrollWidth - 10) {
1268+
scenarioWrapper.classList.add('scrolled-end');
1269+
} else {
1270+
scenarioWrapper.classList.remove('scrolled-end');
1271+
}
1272+
}
1273+
1274+
// 監聽滾動事件
1275+
let scrollTimeout;
1276+
scenarioList.addEventListener('scroll', () => {
1277+
clearTimeout(scrollTimeout);
1278+
scrollTimeout = setTimeout(updateScrollIndicator, 50);
1279+
});
1280+
1281+
// 點擊指示器滾動到對應場景
1282+
scrollDots.forEach((dot, index) => {
1283+
dot.addEventListener('click', () => {
1284+
const targetScroll = index * itemWidth;
1285+
scenarioList.scrollTo({
1286+
left: targetScroll,
1287+
behavior: 'smooth'
1288+
});
1289+
});
1290+
});
1291+
1292+
// 初始化指示器狀態
1293+
updateScrollIndicator();
1294+
1295+
// 添加觸摸滑動支持
1296+
let startX = 0;
1297+
let scrollLeftStart = 0;
1298+
let isDragging = false;
1299+
1300+
scenarioList.addEventListener('mousedown', (e) => {
1301+
isDragging = true;
1302+
startX = e.pageX - scenarioList.offsetLeft;
1303+
scrollLeftStart = scenarioList.scrollLeft;
1304+
scenarioList.style.cursor = 'grabbing';
1305+
});
1306+
1307+
scenarioList.addEventListener('mouseleave', () => {
1308+
isDragging = false;
1309+
scenarioList.style.cursor = 'grab';
1310+
});
1311+
1312+
scenarioList.addEventListener('mouseup', () => {
1313+
isDragging = false;
1314+
scenarioList.style.cursor = 'grab';
1315+
});
1316+
1317+
scenarioList.addEventListener('mousemove', (e) => {
1318+
if (!isDragging) return;
1319+
e.preventDefault();
1320+
const x = e.pageX - scenarioList.offsetLeft;
1321+
const walk = (x - startX) * 1.5;
1322+
scenarioList.scrollLeft = scrollLeftStart - walk;
1323+
});
11131324
}
11141325
</script>
11151326
</body>

0 commit comments

Comments
 (0)