Skip to content

Commit a25ff8c

Browse files
committed
feat(mobile): 優化手機版時間軸與搜尋欄交互體驗
- 調整手機版底部時間軸的位置與高度,避免被系統導覽條遮擋 - 在時間軸黃色指標上方新增動態年份氣泡,並優化文字大小與清晰度 - 在搜尋框右側新增獨立的書本篩選按鈕,避免點選篩選時強制喚醒虛擬鍵盤 - 實作點選外部自動關閉搜尋/篩選面板的機制,取代原先的延時失焦邏輯
1 parent e7b389c commit a25ff8c

7 files changed

Lines changed: 119 additions & 58 deletions

File tree

webapp/src/app/app.component.css

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,10 @@
8686

8787
@media (max-width: 1024px) {
8888
.bottom-overlay {
89-
bottom: 12px;
89+
bottom: 16px;
9090
left: 12px;
9191
right: 12px;
92-
height: 90px;
92+
height: 95px;
9393
}
9494
}
9595

@@ -98,15 +98,16 @@
9898
top: 12px;
9999
}
100100
.bottom-overlay {
101-
height: 100px;
101+
bottom: 24px;
102+
height: 110px;
102103
}
103104
}
104105

105106
@media (max-width: 480px) {
106107
.bottom-overlay {
107-
height: 90px;
108-
left: 8px;
109-
right: 8px;
110-
bottom: 8px;
108+
height: 115px;
109+
left: 12px;
110+
right: 12px;
111+
bottom: 24px;
111112
}
112113
}

webapp/src/app/search-bar/search-bar.component.css

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,4 +287,40 @@
287287
.search-results-container {
288288
max-height: 45vh;
289289
}
290+
}
291+
292+
/* 篩選切換按鈕樣式 */
293+
.filter-toggle-btn {
294+
background: transparent;
295+
border: none;
296+
cursor: pointer;
297+
padding: 6px;
298+
display: flex;
299+
align-items: center;
300+
justify-content: center;
301+
color: var(--color-brown, #8b7355);
302+
border-radius: 50%;
303+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
304+
margin-left: auto;
305+
}
306+
307+
.filter-toggle-btn:hover {
308+
background: rgba(200, 164, 39, 0.12);
309+
color: #7a5c00;
310+
}
311+
312+
.filter-toggle-btn.active {
313+
background: rgba(200, 164, 39, 0.2);
314+
color: #7a5c00;
315+
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
316+
}
317+
318+
.book-icon {
319+
width: 18px;
320+
height: 18px;
321+
transition: transform 0.2s ease;
322+
}
323+
324+
.filter-toggle-btn:hover .book-icon {
325+
transform: scale(1.08);
290326
}

webapp/src/app/search-bar/search-bar.component.html

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,24 @@
1616
type="text"
1717
class="search-input"
1818
id="search-input"
19-
[placeholder]="selectedTextbooks.length > 0 ? '輸入關鍵字進一步篩選...' : '選擇課本或輸入關鍵字...'"
19+
[placeholder]="selectedTextbooks.length > 0 ? '輸入關鍵字進一步篩選...' : '輸入關鍵字搜尋事件...'"
2020
(input)="onSearch($event)"
2121
(focus)="onFocus()"
22-
(blur)="onBlur()"
2322
autocomplete="off"
2423
/>
24+
25+
<button
26+
type="button"
27+
class="filter-toggle-btn"
28+
[class.active]="showFilterPanel"
29+
(mousedown)="$event.preventDefault()"
30+
(click)="toggleFilterPanel($event)"
31+
title="課本範圍篩選">
32+
<svg class="book-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
33+
<path d="M4 19.5C4 18.837 4.26339 18.2011 4.73223 17.7322C5.20107 17.2634 5.83696 17 6.5 17H20M4 19.5C4 20.163 4.26339 20.7989 4.73223 21.2678C5.20107 21.7366 5.83696 22 6.5 22H20M4 19.5V4.5C4 3.83696 4.26339 3.20107 4.73223 2.73223C5.20107 2.26339 5.83696 2 6.5 2H20V17H6.5C5.83696 17 5.20107 17.2634 4.73223 17.7322C4.26339 18.2011 4 18.837 4 19.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
34+
</svg>
35+
</button>
36+
2537
@if (searchResults.length > 0 || queryText || selectedTextbooks.length > 0) {
2638
<button class="clear-button" (click)="clearSearch()">×</button>
2739
}

webapp/src/app/search-bar/search-bar.component.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, OnInit, OnDestroy } from '@angular/core';
1+
import { Component, OnInit, OnDestroy, HostListener, ElementRef } from '@angular/core';
22
import { AsyncPipe } from '@angular/common';
33
import { Store } from '@ngrx/store';
44
import { AppState } from '../store/app.state';
@@ -34,14 +34,18 @@ export class SearchBarComponent implements OnInit, OnDestroy {
3434
fuse!: Fuse<any>;
3535
isFocused = false;
3636
queryText = '';
37+
showFilterPanel = false;
3738

3839
textbookOptions = TEXTBOOK_OPTIONS;
3940
selectedTextbooks: string[] = [];
4041

4142
private allEvents: any[] = [];
4243
private subscriptions: Subscription[] = [];
4344

44-
constructor(private store: Store<AppState>) {}
45+
constructor(
46+
private store: Store<AppState>,
47+
private elementRef: ElementRef
48+
) {}
4549

4650
ngOnInit(): void {
4751
this.events$ = this.store.select(EventSelectors.selectEvents);
@@ -61,10 +65,6 @@ export class SearchBarComponent implements OnInit, OnDestroy {
6165
this.subscriptions.forEach(sub => sub.unsubscribe());
6266
}
6367

64-
get showFilterPanel(): boolean {
65-
return this.isFocused && !this.queryText;
66-
}
67-
6868
get showResults(): boolean {
6969
return this.isFocused && this.searchResults.length > 0;
7070
}
@@ -83,10 +83,23 @@ export class SearchBarComponent implements OnInit, OnDestroy {
8383

8484
onFocus(): void {
8585
this.isFocused = true;
86+
this.showFilterPanel = false;
87+
}
88+
89+
toggleFilterPanel(event: Event): void {
90+
event.stopPropagation();
91+
this.showFilterPanel = !this.showFilterPanel;
92+
if (this.showFilterPanel) {
93+
this.isFocused = false;
94+
}
8695
}
8796

88-
onBlur(): void {
89-
setTimeout(() => { this.isFocused = false; }, 200);
97+
@HostListener('document:click', ['$event'])
98+
onDocumentClick(event: MouseEvent): void {
99+
if (!this.elementRef.nativeElement.contains(event.target)) {
100+
this.isFocused = false;
101+
this.showFilterPanel = false;
102+
}
90103
}
91104

92105
toggleTextbook(value: string): void {

webapp/src/app/timeline/timeline.component.css

Lines changed: 29 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,6 @@
88
pointer-events: auto !important;
99
}
1010

11-
.year-indicator {
12-
position: absolute;
13-
top: 2px;
14-
right: 10px;
15-
z-index: 30;
16-
font-size: 0.75rem;
17-
font-weight: 600;
18-
color: rgba(44, 36, 27, 0.8);
19-
font-family: var(--font-heading, serif);
20-
pointer-events: none;
21-
letter-spacing: 0.5px;
22-
}
23-
2411
.center-needle {
2512
position: absolute;
2613
left: 50%;
@@ -39,17 +26,24 @@
3926
pointer-events: none;
4027
}
4128

42-
.center-needle::before {
43-
content: '';
29+
.needle-year-bubble {
4430
position: absolute;
45-
top: 6px;
31+
top: 4px;
4632
left: 50%;
4733
transform: translateX(-50%);
48-
width: 0;
49-
height: 0;
50-
border-left: 5px solid transparent;
51-
border-right: 5px solid transparent;
52-
border-top: 7px solid rgba(200, 164, 39, 0.8);
34+
background: linear-gradient(135deg, rgba(235, 195, 85, 0.98), rgba(200, 164, 39, 0.98));
35+
color: #2c241b;
36+
padding: 2px 8px;
37+
border-radius: 12px;
38+
font-size: 0.78rem;
39+
font-weight: 800;
40+
white-space: nowrap;
41+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
42+
border: 1px solid #fff;
43+
font-family: var(--font-heading, serif);
44+
z-index: 30;
45+
letter-spacing: 0.5px;
46+
text-shadow: 0 1px 0px rgba(255, 255, 255, 0.4);
5347
}
5448

5549
.center-needle::after {
@@ -75,7 +69,7 @@
7569
pointer-events: auto !important;
7670
user-select: none;
7771
box-sizing: border-box;
78-
padding-bottom: 10px;
72+
padding-bottom: 16px;
7973
}
8074

8175
.timeline-container.dragging {
@@ -155,15 +149,15 @@
155149
top: 50%;
156150
left: 50%;
157151
transform: translate(-50%, -50%);
158-
font-size: 0.85rem;
159-
font-weight: 600;
152+
font-size: 0.95rem;
153+
font-weight: 700;
160154
font-family: var(--font-heading);
161-
color: rgba(44, 36, 27, 0.9);
155+
color: rgba(44, 36, 27, 0.98);
162156
white-space: nowrap;
163-
letter-spacing: 0.5px;
157+
letter-spacing: 0.8px;
164158
pointer-events: none;
165159
text-align: center;
166-
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.6);
160+
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
167161
}
168162

169163
.dynasty-years {
@@ -250,16 +244,17 @@
250244
top: 13px;
251245
left: 50%;
252246
transform: translateX(-50%);
253-
font-size: 0.7rem;
254-
color: rgba(44, 36, 27, 0.8);
247+
font-size: 0.82rem;
248+
color: rgba(44, 36, 27, 0.95);
255249
white-space: nowrap;
256-
font-weight: 600;
257-
letter-spacing: 0.2px;
250+
font-weight: 700;
251+
letter-spacing: 0.3px;
258252
}
259253

260254
@media (max-width: 768px) {
261-
.dynasty-bands { height: 30px; }
262-
.event-dots { margin-top: 8px; }
263-
.dynasty-label { font-size: 0.7rem; }
255+
.dynasty-bands { height: 32px; }
256+
.event-dots { margin-top: 10px; }
257+
.dynasty-label { font-size: 0.85rem; font-weight: 700; }
258+
.year-label { font-size: 0.75rem; font-weight: 700; }
264259
.dynasty-years { display: none; }
265260
}

webapp/src/app/timeline/timeline.component.html

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<div class="timeline-wrapper">
2-
@if (currentCenterYear !== 0) {
3-
<div class="year-indicator">
4-
{{ currentCenterYear < 0 ? '公元前' + (-currentCenterYear) : currentCenterYear + ' 年' }}
5-
</div>
6-
}
7-
8-
<div class="center-needle"></div>
2+
<div class="center-needle">
3+
@if (currentCenterYear !== 0) {
4+
<div class="needle-year-bubble">
5+
{{ currentCenterYear < 0 ? '前 ' + (-currentCenterYear) : currentCenterYear }}
6+
</div>
7+
}
8+
</div>
99

1010
<div class="timeline-container" #scrollContainer
1111
[class.dragging]="isDragging"

webapp/src/app/timeline/timeline.component.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ export class TimelineComponent implements OnInit, AfterViewInit, OnDestroy {
8686
}
8787

8888
ngAfterViewInit(): void {
89+
setTimeout(() => {
90+
this.onScroll();
91+
}, 100);
92+
8993
const selectedSub = this.store.select(selectSelectedEvent).pipe(
9094
filter(event => !!event)
9195
).subscribe(event => {

0 commit comments

Comments
 (0)