Hugo-Jiang commited on
Commit
6d01f8a
·
1 Parent(s): 73f3779

update GUI

Browse files
.snow/notebook/exchangeRates.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "templates/index.html": [
3
+ {
4
+ "id": "notebook-1765162045130_96lg7j8bb",
5
+ "filePath": "templates/index.html",
6
+ "note": "✅ Redesigned with Apple design philosophy: Clean header with structured info items, improved semantic HTML structure, removed emoji from title for cleaner look, added proper aria labels and accessibility considerations",
7
+ "createdAt": "2025-12-08T10:47:25.130",
8
+ "updatedAt": "2025-12-08T10:47:25.130"
9
+ }
10
+ ],
11
+ "static/css/style.css": [
12
+ {
13
+ "id": "notebook-1765162045131_g1ouh1xmh",
14
+ "filePath": "static/css/style.css",
15
+ "note": "✅ Complete Apple Design System implementation: CSS variables for colors/spacing/transitions, refined shadows and blur effects, improved button states with proper hover/active feedback, staggered card animations, responsive design for all screen sizes, professional scrollbar styling",
16
+ "createdAt": "2025-12-08T10:47:25.131",
17
+ "updatedAt": "2025-12-08T10:47:25.131"
18
+ }
19
+ ],
20
+ "static/js/app.js": [
21
+ {
22
+ "id": "notebook-1765162045132_ob1xtd6gr",
23
+ "filePath": "static/js/app.js",
24
+ "note": "✅ Enhanced currency symbol display: Comprehensive currencyFlags mapping (50+ countries), added currencySymbols mapping for currencies without flags (₹ ₽ ₩ ฿ etc.), priority order: flag → symbol → API symbol → code abbreviation. Ensures all currencies display appropriate visual identifiers.",
25
+ "createdAt": "2025-12-08T10:47:25.132",
26
+ "updatedAt": "2025-12-08T10:47:25.132"
27
+ }
28
+ ]
29
+ }
static/css/style.css CHANGED
@@ -1,196 +1,306 @@
1
- /* ==================== 基础样式 ==================== */
2
- * {
 
 
 
3
  margin: 0;
4
  padding: 0;
5
  box-sizing: border-box;
6
  }
7
 
8
  :root {
9
- --primary-color: #007AFF;
10
- --primary-dark: #0051D5;
11
- --secondary-color: #5856D6;
12
- --success-color: #34C759;
13
- --error-color: #FF3B30;
14
- --warning-color: #FF9500;
15
- --text-color: #1d1d1f;
16
- --text-secondary: #86868b;
17
- --bg-light: #f5f5f7;
18
- --border-color: rgba(0, 0, 0, 0.08);
19
- --card-bg: rgba(255, 255, 255, 0.72);
20
- --card-shadow: 0 2px 16px rgba(0, 0, 0, 0.08);
21
- --card-shadow-hover: 0 8px 32px rgba(0, 0, 0, 0.12);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  }
23
 
24
  body {
25
  font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif;
26
- background: linear-gradient(180deg, #f5f5f7 0%, #e8e8ed 100%);
 
27
  min-height: 100vh;
28
- padding: 20px;
29
- color: var(--text-color);
30
  -webkit-font-smoothing: antialiased;
31
  -moz-osx-font-smoothing: grayscale;
 
 
32
  }
33
 
34
- /* ==================== 容器 ==================== */
35
  .container {
36
  max-width: 1400px;
37
  margin: 0 auto;
38
  }
39
 
40
- /* ==================== 头部 ==================== */
41
  header {
42
  text-align: center;
43
- margin-bottom: 40px;
44
- padding: 40px 0 20px;
 
 
 
 
45
  }
46
 
47
  header h1 {
48
- font-size: 3rem;
49
- font-weight: 600;
50
- margin-bottom: 12px;
51
- color: var(--text-color);
52
- letter-spacing: -0.5px;
 
 
 
 
 
 
 
53
  }
54
 
55
  .header-info {
56
  display: flex;
 
57
  justify-content: center;
58
- gap: 32px;
59
  flex-wrap: wrap;
60
  }
61
 
62
- .header-info p {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  font-size: 0.9375rem;
64
- color: var(--text-secondary);
65
- font-weight: 400;
66
  }
67
 
68
- .header-info .label {
69
- color: var(--text-secondary);
70
- margin-right: 4px;
 
71
  }
72
 
73
- /* ==================== 工具栏 ==================== */
74
  .toolbar {
75
  display: flex;
76
- gap: 15px;
77
- margin-bottom: 20px;
78
  flex-wrap: wrap;
79
  }
80
 
81
- /* 搜索框 */
82
  .search-box {
83
  flex: 1;
84
- min-width: 250px;
85
  position: relative;
86
  }
87
 
88
- .search-box .search-icon {
89
  position: absolute;
90
- left: 15px;
91
  top: 50%;
92
  transform: translateY(-50%);
93
- width: 20px;
94
- height: 20px;
95
- color: var(--text-light);
 
96
  }
97
 
98
  .search-box input {
99
  width: 100%;
100
- padding: 14px 20px 14px 45px;
101
- border: 1px solid var(--border-color);
102
- border-radius: 10px;
103
- font-size: 1rem;
104
- background: var(--card-bg);
105
- backdrop-filter: blur(20px);
106
- -webkit-backdrop-filter: blur(20px);
107
- box-shadow: var(--card-shadow);
108
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
 
 
 
 
 
 
109
  }
110
 
111
  .search-box input:focus {
112
  outline: none;
113
- border-color: var(--primary-color);
114
- box-shadow: 0 0 0 4px rgba(0, 122, 255, 0.1), var(--card-shadow);
 
115
  }
116
 
117
- /* 筛选按钮 */
118
  .filter-buttons {
119
  display: flex;
120
- gap: 10px;
121
  }
122
 
123
  .filter-btn {
124
- padding: 10px 20px;
125
- border: 1px solid var(--border-color);
126
- border-radius: 10px;
127
- background: var(--card-bg);
128
- backdrop-filter: blur(20px);
129
- -webkit-backdrop-filter: blur(20px);
130
- color: var(--text-color);
131
  font-size: 0.9375rem;
132
- font-weight: 500;
133
  cursor: pointer;
134
- box-shadow: var(--card-shadow);
135
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
 
136
  }
137
 
138
  .filter-btn:hover {
139
- background: rgba(255, 255, 255, 0.9);
140
  transform: translateY(-1px);
 
141
  }
142
 
143
  .filter-btn.active {
144
- background: var(--primary-color);
145
  color: white;
146
- border-color: var(--primary-color);
 
 
 
 
 
 
 
147
  }
148
 
149
- /* ==================== 提示信息 ==================== */
150
- .tips {
151
  display: flex;
152
  align-items: center;
153
- gap: 12px;
154
- background: var(--card-bg);
155
- backdrop-filter: blur(20px);
156
- -webkit-backdrop-filter: blur(20px);
157
- padding: 16px 20px;
158
- border-radius: 12px;
159
- margin-bottom: 24px;
160
- border: 1px solid var(--border-color);
161
- box-shadow: var(--card-shadow);
162
- }
163
-
164
- .tips svg {
165
  width: 20px;
166
  height: 20px;
167
- color: var(--primary-color);
168
  flex-shrink: 0;
169
  }
170
 
171
- .tips span {
172
- color: var(--text-secondary);
173
  font-size: 0.875rem;
 
174
  line-height: 1.5;
175
  }
176
 
177
- /* ==================== 加载状态 ==================== */
178
- .loading {
179
  display: flex;
180
  flex-direction: column;
181
  align-items: center;
182
  justify-content: center;
183
- padding: 80px 20px;
184
  }
185
 
186
- .spinner {
187
- width: 44px;
188
- height: 44px;
189
- border: 3px solid rgba(0, 122, 255, 0.2);
190
- border-top-color: var(--primary-color);
191
  border-radius: 50%;
192
  animation: spin 0.8s linear infinite;
193
- margin-bottom: 20px;
194
  }
195
 
196
  @keyframes spin {
@@ -199,169 +309,214 @@ header h1 {
199
  }
200
  }
201
 
202
- .loading p {
203
  font-size: 1.0625rem;
204
- color: var(--text-secondary);
205
- font-weight: 500;
206
  }
207
 
208
- .loading.hidden {
209
  display: none;
210
  }
211
 
212
- /* ==================== 错误提示 ==================== */
213
- .error-message {
214
  display: flex;
 
215
  align-items: center;
216
- gap: 16px;
217
- background: rgba(255, 59, 48, 0.08);
218
- border: 1px solid rgba(255, 59, 48, 0.2);
219
- padding: 20px 24px;
220
- border-radius: 12px;
221
- margin-bottom: 24px;
222
- }
223
-
224
- .error-message svg {
225
- width: 22px;
226
- height: 22px;
227
- color: var(--error-color);
228
- flex-shrink: 0;
229
  }
230
 
231
- .error-message span {
232
- flex: 1;
233
- color: var(--error-color);
234
- font-size: 0.9375rem;
235
- font-weight: 500;
 
 
 
 
 
 
 
 
 
236
  }
237
 
238
- .retry-btn {
239
- padding: 8px 18px;
240
- background: var(--error-color);
 
 
 
 
 
 
 
 
 
241
  color: white;
242
  border: none;
243
- border-radius: 8px;
244
  cursor: pointer;
245
- font-size: 0.875rem;
246
- font-weight: 600;
247
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
 
248
  }
249
 
250
- .retry-btn:hover {
 
 
 
 
 
251
  background: #d70015;
252
- transform: scale(1.02);
 
 
 
 
 
253
  }
254
 
255
- /* ==================== 货币网格 ==================== */
256
  .currency-grid {
257
  display: grid;
258
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
259
- gap: 15px;
 
260
  }
261
 
262
- /* ==================== 货币卡片 ==================== */
263
  .currency-card {
264
- background: var(--card-bg);
265
- backdrop-filter: blur(20px);
266
- -webkit-backdrop-filter: blur(20px);
267
- border-radius: 16px;
268
- padding: 20px;
269
- box-shadow: var(--card-shadow);
270
  display: flex;
271
  align-items: center;
272
- gap: 16px;
273
- transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
274
- border: 1px solid var(--border-color);
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  }
276
 
277
  .currency-card:hover {
278
  transform: translateY(-2px);
279
- box-shadow: var(--card-shadow-hover);
280
- background: rgba(255, 255, 255, 0.85);
281
  }
282
 
283
- .currency-card.priority {
284
- border-left: 3px solid var(--primary-color);
285
  }
286
 
287
  .currency-card.active {
288
- border-color: var(--primary-color);
289
- background: rgba(255, 255, 255, 0.9);
290
- box-shadow: 0 0 0 4px rgba(0, 122, 255, 0.1), var(--card-shadow-hover);
291
  }
292
 
293
  .currency-card.hidden {
294
  display: none;
295
  }
296
 
297
- /* 货币图标/符号 */
298
  .currency-icon {
299
- width: 52px;
300
- height: 52px;
301
- background: linear-gradient(135deg, rgba(0, 122, 255, 0.1), rgba(88, 86, 214, 0.1));
302
- border-radius: 14px;
303
  display: flex;
304
  align-items: center;
305
  justify-content: center;
306
- font-size: 1.5rem;
307
  flex-shrink: 0;
308
- border: 1px solid rgba(0, 122, 255, 0.15);
 
309
  }
310
 
311
- /* 货币信息 */
 
 
 
 
 
312
  .currency-info {
313
- flex: 0 0 90px;
314
  }
315
 
316
  .currency-code {
317
  font-size: 1.125rem;
318
- font-weight: 600;
319
- color: var(--text-color);
320
- letter-spacing: -0.2px;
 
321
  }
322
 
323
  .currency-name {
324
  font-size: 0.8125rem;
325
- color: var(--text-secondary);
 
326
  white-space: nowrap;
327
  overflow: hidden;
328
  text-overflow: ellipsis;
329
- max-width: 100px;
330
- font-weight: 400;
331
  }
332
 
333
- /* 输入区域 */
334
  .currency-input {
335
  flex: 1;
 
 
336
  }
337
 
338
  .currency-input input {
339
  width: 100%;
340
  padding: 12px 14px;
341
- border: 1px solid var(--border-color);
342
- border-radius: 10px;
343
  font-size: 1.0625rem;
 
344
  text-align: right;
345
- font-weight: 500;
346
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
347
- color: var(--text-color);
348
- background: rgba(255, 255, 255, 0.6);
349
  }
350
 
351
  .currency-input input:focus {
352
  outline: none;
353
- border-color: var(--primary-color);
354
- box-shadow: 0 0 0 4px rgba(0, 122, 255, 0.1);
355
- background: rgba(255, 255, 255, 0.9);
356
  }
357
 
358
  .currency-input input::placeholder {
359
- color: var(--text-secondary);
360
- opacity: 0.5;
361
- font-weight: 400;
362
  }
363
 
364
- /* 禁用输入时去除 spinner */
365
  .currency-input input::-webkit-outer-spin-button,
366
  .currency-input input::-webkit-inner-spin-button {
367
  -webkit-appearance: none;
@@ -372,57 +527,129 @@ header h1 {
372
  -moz-appearance: textfield;
373
  }
374
 
375
- /* 汇率显示 */
376
  .currency-rate {
377
  font-size: 0.6875rem;
378
- color: var(--text-secondary);
379
  text-align: right;
380
- margin-top: 6px;
381
- font-weight: 400;
 
382
  }
383
 
384
- /* ==================== 底部 ==================== */
385
  footer {
386
  text-align: center;
387
- padding: 40px 20px 30px;
388
- color: var(--text-secondary);
 
 
 
 
 
 
 
 
 
 
389
  font-size: 0.875rem;
 
390
  }
391
 
392
- footer a {
393
- color: var(--primary-color);
394
  text-decoration: none;
395
- font-weight: 500;
396
- transition: opacity 0.2s;
397
  }
398
 
399
- footer a:hover {
400
  opacity: 0.7;
401
  }
402
 
403
  .footer-note {
404
- margin-top: 8px;
405
- color: var(--text-secondary);
406
  font-size: 0.8125rem;
 
407
  }
408
 
409
- /* ==================== 响应式设计 ==================== */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  @media (max-width: 768px) {
411
  body {
412
- padding: 16px;
413
  }
414
 
415
  header {
416
- padding: 24px 0 16px;
 
417
  }
418
 
419
  header h1 {
420
- font-size: 2rem;
 
 
 
 
421
  }
422
 
423
  .header-info {
424
  flex-direction: column;
425
- gap: 10px;
 
 
 
 
 
426
  }
427
 
428
  .toolbar {
@@ -435,6 +662,11 @@ footer a:hover {
435
 
436
  .filter-buttons {
437
  justify-content: center;
 
 
 
 
 
438
  }
439
 
440
  .currency-grid {
@@ -442,68 +674,73 @@ footer a:hover {
442
  }
443
 
444
  .currency-card {
445
- padding: 15px;
446
  }
447
 
448
  .currency-icon {
449
- width: 44px;
450
- height: 44px;
451
- font-size: 1.25rem;
452
  }
453
 
454
  .currency-info {
455
- flex: 0 0 80px;
 
 
 
 
456
  }
457
 
458
  .currency-name {
459
- max-width: 80px;
 
460
  }
461
- }
462
-
463
- /* ==================== 动画 ==================== */
464
- @keyframes fadeIn {
465
- from {
466
- opacity: 0;
467
- transform: translateY(10px);
468
  }
469
- to {
470
- opacity: 1;
471
- transform: translateY(0);
472
  }
473
  }
474
 
475
- .currency-card {
476
- animation: fadeIn 0.3s ease-out;
477
- }
478
-
479
- /* 卡片依次出现的延迟效果 */
480
- .currency-card:nth-child(1) { animation-delay: 0.02s; }
481
- .currency-card:nth-child(2) { animation-delay: 0.04s; }
482
- .currency-card:nth-child(3) { animation-delay: 0.06s; }
483
- .currency-card:nth-child(4) { animation-delay: 0.08s; }
484
- .currency-card:nth-child(5) { animation-delay: 0.1s; }
485
- .currency-card:nth-child(6) { animation-delay: 0.12s; }
486
- .currency-card:nth-child(7) { animation-delay: 0.14s; }
487
- .currency-card:nth-child(8) { animation-delay: 0.16s; }
488
- .currency-card:nth-child(9) { animation-delay: 0.18s; }
489
- .currency-card:nth-child(10) { animation-delay: 0.2s; }
490
-
491
- /* ==================== 滚动条美化 ==================== */
492
- ::-webkit-scrollbar {
493
- width: 8px;
494
- height: 8px;
495
- }
496
-
497
- ::-webkit-scrollbar-track {
498
- background: rgba(255, 255, 255, 0.1);
499
- border-radius: 4px;
500
- }
501
-
502
- ::-webkit-scrollbar-thumb {
503
- background: rgba(255, 255, 255, 0.3);
504
- border-radius: 4px;
505
  }
506
 
507
- ::-webkit-scrollbar-thumb:hover {
508
- background: rgba(255, 255, 255, 0.5);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509
  }
 
1
+ /* ==================== Apple Design System ==================== */
2
+ /* Reset & Base */
3
+ *,
4
+ *::before,
5
+ *::after {
6
  margin: 0;
7
  padding: 0;
8
  box-sizing: border-box;
9
  }
10
 
11
  :root {
12
+ /* Apple Color Palette */
13
+ --color-blue: #007AFF;
14
+ --color-blue-dark: #0051D5;
15
+ --color-blue-light: #5AC8FA;
16
+ --color-indigo: #5856D6;
17
+ --color-green: #34C759;
18
+ --color-red: #FF3B30;
19
+ --color-orange: #FF9500;
20
+ --color-yellow: #FFCC00;
21
+
22
+ /* Neutral Colors */
23
+ --color-gray-1: #F5F5F7;
24
+ --color-gray-2: #E8E8ED;
25
+ --color-gray-3: #D1D1D6;
26
+ --color-gray-4: #C7C7CC;
27
+ --color-gray-5: #AEAEB2;
28
+ --color-gray-6: #8E8E93;
29
+ --color-gray-7: #636366;
30
+ --color-gray-8: #48484A;
31
+ --color-gray-9: #3A3A3C;
32
+ --color-gray-10: #2C2C2E;
33
+ --color-gray-11: #1C1C1E;
34
+
35
+ /* Semantic Colors */
36
+ --color-text-primary: #1D1D1F;
37
+ --color-text-secondary: #86868B;
38
+ --color-text-tertiary: #AEAEB2;
39
+ --color-background: #FFFFFF;
40
+ --color-background-secondary: #F5F5F7;
41
+ --color-background-elevated: #FFFFFF;
42
+
43
+ /* Borders & Dividers */
44
+ --color-border: rgba(0, 0, 0, 0.06);
45
+ --color-divider: rgba(0, 0, 0, 0.08);
46
+
47
+ /* Shadows */
48
+ --shadow-small: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.06);
49
+ --shadow-medium: 0 4px 6px rgba(0, 0, 0, 0.04), 0 2px 4px rgba(0, 0, 0, 0.06);
50
+ --shadow-large: 0 10px 15px rgba(0, 0, 0, 0.05), 0 4px 6px rgba(0, 0, 0, 0.08);
51
+ --shadow-xlarge: 0 20px 25px rgba(0, 0, 0, 0.06), 0 8px 10px rgba(0, 0, 0, 0.08);
52
+
53
+ /* Blur Effects */
54
+ --blur-small: blur(10px);
55
+ --blur-medium: blur(20px);
56
+ --blur-large: blur(40px);
57
+
58
+ /* Border Radius */
59
+ --radius-small: 8px;
60
+ --radius-medium: 12px;
61
+ --radius-large: 16px;
62
+ --radius-xlarge: 20px;
63
+
64
+ /* Spacing */
65
+ --space-xs: 4px;
66
+ --space-sm: 8px;
67
+ --space-md: 12px;
68
+ --space-lg: 16px;
69
+ --space-xl: 24px;
70
+ --space-2xl: 32px;
71
+ --space-3xl: 48px;
72
+ --space-4xl: 64px;
73
+
74
+ /* Typography */
75
+ --font-weight-regular: 400;
76
+ --font-weight-medium: 500;
77
+ --font-weight-semibold: 600;
78
+ --font-weight-bold: 700;
79
+
80
+ /* Transitions */
81
+ --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
82
+ --transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1);
83
+ --transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1);
84
+ --transition-bounce: 500ms cubic-bezier(0.34, 1.56, 0.64, 1);
85
  }
86
 
87
  body {
88
  font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif;
89
+ background: linear-gradient(135deg, #F5F5F7 0%, #E8E8ED 100%);
90
+ background-attachment: fixed;
91
  min-height: 100vh;
92
+ color: var(--color-text-primary);
 
93
  -webkit-font-smoothing: antialiased;
94
  -moz-osx-font-smoothing: grayscale;
95
+ line-height: 1.5;
96
+ padding: var(--space-xl);
97
  }
98
 
99
+ /* ==================== Container ==================== */
100
  .container {
101
  max-width: 1400px;
102
  margin: 0 auto;
103
  }
104
 
105
+ /* ==================== Header ==================== */
106
  header {
107
  text-align: center;
108
+ margin-bottom: var(--space-4xl);
109
+ padding: var(--space-3xl) 0;
110
+ }
111
+
112
+ .header-content {
113
+ margin-bottom: var(--space-2xl);
114
  }
115
 
116
  header h1 {
117
+ font-size: 3.5rem;
118
+ font-weight: var(--font-weight-semibold);
119
+ color: var(--color-text-primary);
120
+ letter-spacing: -0.02em;
121
+ margin-bottom: var(--space-md);
122
+ }
123
+
124
+ .header-subtitle {
125
+ font-size: 1.125rem;
126
+ font-weight: var(--font-weight-regular);
127
+ color: var(--color-text-secondary);
128
+ letter-spacing: -0.01em;
129
  }
130
 
131
  .header-info {
132
  display: flex;
133
+ align-items: center;
134
  justify-content: center;
135
+ gap: var(--space-xl);
136
  flex-wrap: wrap;
137
  }
138
 
139
+ .info-item {
140
+ display: flex;
141
+ flex-direction: column;
142
+ align-items: center;
143
+ gap: var(--space-xs);
144
+ }
145
+
146
+ .info-label {
147
+ font-size: 0.75rem;
148
+ font-weight: var(--font-weight-medium);
149
+ color: var(--color-text-tertiary);
150
+ text-transform: uppercase;
151
+ letter-spacing: 0.05em;
152
+ }
153
+
154
+ .info-value {
155
  font-size: 0.9375rem;
156
+ font-weight: var(--font-weight-semibold);
157
+ color: var(--color-text-primary);
158
  }
159
 
160
+ .info-divider {
161
+ width: 1px;
162
+ height: 24px;
163
+ background: var(--color-divider);
164
  }
165
 
166
+ /* ==================== Toolbar ==================== */
167
  .toolbar {
168
  display: flex;
169
+ gap: var(--space-md);
170
+ margin-bottom: var(--space-xl);
171
  flex-wrap: wrap;
172
  }
173
 
174
+ /* Search Box */
175
  .search-box {
176
  flex: 1;
177
+ min-width: 280px;
178
  position: relative;
179
  }
180
 
181
+ .search-icon {
182
  position: absolute;
183
+ left: var(--space-lg);
184
  top: 50%;
185
  transform: translateY(-50%);
186
+ width: 18px;
187
+ height: 18px;
188
+ color: var(--color-text-tertiary);
189
+ pointer-events: none;
190
  }
191
 
192
  .search-box input {
193
  width: 100%;
194
+ padding: 14px 20px 14px 48px;
195
+ border: 1.5px solid var(--color-border);
196
+ border-radius: var(--radius-large);
197
+ font-size: 0.9375rem;
198
+ font-weight: var(--font-weight-regular);
199
+ background: var(--color-background-elevated);
200
+ backdrop-filter: var(--blur-medium);
201
+ -webkit-backdrop-filter: var(--blur-medium);
202
+ box-shadow: var(--shadow-small);
203
+ transition: all var(--transition-base);
204
+ color: var(--color-text-primary);
205
+ }
206
+
207
+ .search-box input::placeholder {
208
+ color: var(--color-text-tertiary);
209
  }
210
 
211
  .search-box input:focus {
212
  outline: none;
213
+ border-color: var(--color-blue);
214
+ box-shadow: 0 0 0 4px rgba(0, 122, 255, 0.1), var(--shadow-medium);
215
+ background: var(--color-background);
216
  }
217
 
218
+ /* Filter Buttons */
219
  .filter-buttons {
220
  display: flex;
221
+ gap: var(--space-sm);
222
  }
223
 
224
  .filter-btn {
225
+ padding: 12px 24px;
226
+ border: 1.5px solid var(--color-border);
227
+ border-radius: var(--radius-large);
228
+ background: var(--color-background-elevated);
229
+ backdrop-filter: var(--blur-medium);
230
+ -webkit-backdrop-filter: var(--blur-medium);
231
+ color: var(--color-text-primary);
232
  font-size: 0.9375rem;
233
+ font-weight: var(--font-weight-medium);
234
  cursor: pointer;
235
+ box-shadow: var(--shadow-small);
236
+ transition: all var(--transition-base);
237
+ white-space: nowrap;
238
  }
239
 
240
  .filter-btn:hover {
241
+ background: var(--color-background);
242
  transform: translateY(-1px);
243
+ box-shadow: var(--shadow-medium);
244
  }
245
 
246
  .filter-btn.active {
247
+ background: var(--color-blue);
248
  color: white;
249
+ border-color: var(--color-blue);
250
+ box-shadow: 0 0 0 4px rgba(0, 122, 255, 0.15), var(--shadow-medium);
251
+ }
252
+
253
+ .filter-btn.active:hover {
254
+ background: var(--color-blue-dark);
255
+ border-color: var(--color-blue-dark);
256
+ transform: translateY(-1px);
257
  }
258
 
259
+ /* ==================== Info Banner ==================== */
260
+ .info-banner {
261
  display: flex;
262
  align-items: center;
263
+ gap: var(--space-md);
264
+ background: rgba(0, 122, 255, 0.05);
265
+ backdrop-filter: var(--blur-medium);
266
+ -webkit-backdrop-filter: var(--blur-medium);
267
+ padding: var(--space-lg) var(--space-xl);
268
+ border-radius: var(--radius-large);
269
+ margin-bottom: var(--space-xl);
270
+ border: 1.5px solid rgba(0, 122, 255, 0.12);
271
+ }
272
+
273
+ .info-icon {
 
274
  width: 20px;
275
  height: 20px;
276
+ color: var(--color-blue);
277
  flex-shrink: 0;
278
  }
279
 
280
+ .info-banner span {
281
+ color: var(--color-text-secondary);
282
  font-size: 0.875rem;
283
+ font-weight: var(--font-weight-regular);
284
  line-height: 1.5;
285
  }
286
 
287
+ /* ==================== Loading State ==================== */
288
+ .loading-state {
289
  display: flex;
290
  flex-direction: column;
291
  align-items: center;
292
  justify-content: center;
293
+ padding: var(--space-4xl) var(--space-xl);
294
  }
295
 
296
+ .loading-spinner {
297
+ width: 48px;
298
+ height: 48px;
299
+ border: 3px solid rgba(0, 122, 255, 0.15);
300
+ border-top-color: var(--color-blue);
301
  border-radius: 50%;
302
  animation: spin 0.8s linear infinite;
303
+ margin-bottom: var(--space-xl);
304
  }
305
 
306
  @keyframes spin {
 
309
  }
310
  }
311
 
312
+ .loading-text {
313
  font-size: 1.0625rem;
314
+ color: var(--color-text-secondary);
315
+ font-weight: var(--font-weight-medium);
316
  }
317
 
318
+ .loading-state.hidden {
319
  display: none;
320
  }
321
 
322
+ /* ==================== Error State ==================== */
323
+ .error-state {
324
  display: flex;
325
+ flex-direction: column;
326
  align-items: center;
327
+ justify-content: center;
328
+ gap: var(--space-lg);
329
+ background: rgba(255, 59, 48, 0.05);
330
+ border: 1.5px solid rgba(255, 59, 48, 0.15);
331
+ padding: var(--space-3xl) var(--space-xl);
332
+ border-radius: var(--radius-xlarge);
333
+ margin-bottom: var(--space-xl);
334
+ text-align: center;
 
 
 
 
 
335
  }
336
 
337
+ .error-icon {
338
+ width: 56px;
339
+ height: 56px;
340
+ display: flex;
341
+ align-items: center;
342
+ justify-content: center;
343
+ background: rgba(255, 59, 48, 0.1);
344
+ border-radius: 50%;
345
+ }
346
+
347
+ .error-icon svg {
348
+ width: 28px;
349
+ height: 28px;
350
+ color: var(--color-red);
351
  }
352
 
353
+ .error-text {
354
+ font-size: 1.0625rem;
355
+ color: var(--color-red);
356
+ font-weight: var(--font-weight-medium);
357
+ }
358
+
359
+ .retry-button {
360
+ display: flex;
361
+ align-items: center;
362
+ gap: var(--space-sm);
363
+ padding: 12px 24px;
364
+ background: var(--color-red);
365
  color: white;
366
  border: none;
367
+ border-radius: var(--radius-medium);
368
  cursor: pointer;
369
+ font-size: 0.9375rem;
370
+ font-weight: var(--font-weight-semibold);
371
+ transition: all var(--transition-base);
372
+ box-shadow: var(--shadow-medium);
373
  }
374
 
375
+ .retry-button svg {
376
+ width: 16px;
377
+ height: 16px;
378
+ }
379
+
380
+ .retry-button:hover {
381
  background: #d70015;
382
+ transform: translateY(-1px);
383
+ box-shadow: var(--shadow-large);
384
+ }
385
+
386
+ .retry-button:active {
387
+ transform: translateY(0);
388
  }
389
 
390
+ /* ==================== Currency Grid ==================== */
391
  .currency-grid {
392
  display: grid;
393
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
394
+ gap: var(--space-lg);
395
+ animation: fadeIn 0.4s ease-out;
396
  }
397
 
398
+ /* ==================== Currency Card ==================== */
399
  .currency-card {
400
+ background: var(--color-background-elevated);
401
+ backdrop-filter: var(--blur-medium);
402
+ -webkit-backdrop-filter: var(--blur-medium);
403
+ border-radius: var(--radius-large);
404
+ padding: var(--space-lg);
405
+ box-shadow: var(--shadow-small);
406
  display: flex;
407
  align-items: center;
408
+ gap: var(--space-md);
409
+ transition: all var(--transition-base);
410
+ border: 1.5px solid var(--color-border);
411
+ position: relative;
412
+ overflow: hidden;
413
+ }
414
+
415
+ .currency-card::before {
416
+ content: '';
417
+ position: absolute;
418
+ top: 0;
419
+ left: 0;
420
+ width: 3px;
421
+ height: 100%;
422
+ background: transparent;
423
+ transition: all var(--transition-base);
424
  }
425
 
426
  .currency-card:hover {
427
  transform: translateY(-2px);
428
+ box-shadow: var(--shadow-large);
429
+ border-color: var(--color-gray-3);
430
  }
431
 
432
+ .currency-card.priority::before {
433
+ background: var(--color-blue);
434
  }
435
 
436
  .currency-card.active {
437
+ border-color: var(--color-blue);
438
+ box-shadow: 0 0 0 4px rgba(0, 122, 255, 0.1), var(--shadow-large);
 
439
  }
440
 
441
  .currency-card.hidden {
442
  display: none;
443
  }
444
 
445
+ /* Currency Icon */
446
  .currency-icon {
447
+ width: 56px;
448
+ height: 56px;
449
+ background: linear-gradient(135deg, rgba(0, 122, 255, 0.08), rgba(88, 86, 214, 0.08));
450
+ border-radius: var(--radius-medium);
451
  display: flex;
452
  align-items: center;
453
  justify-content: center;
454
+ font-size: 1.75rem;
455
  flex-shrink: 0;
456
+ border: 1.5px solid rgba(0, 122, 255, 0.12);
457
+ transition: all var(--transition-base);
458
  }
459
 
460
+ .currency-card:hover .currency-icon {
461
+ transform: scale(1.05);
462
+ background: linear-gradient(135deg, rgba(0, 122, 255, 0.12), rgba(88, 86, 214, 0.12));
463
+ }
464
+
465
+ /* Currency Info */
466
  .currency-info {
467
+ flex: 0 0 100px;
468
  }
469
 
470
  .currency-code {
471
  font-size: 1.125rem;
472
+ font-weight: var(--font-weight-semibold);
473
+ color: var(--color-text-primary);
474
+ letter-spacing: -0.01em;
475
+ margin-bottom: var(--space-xs);
476
  }
477
 
478
  .currency-name {
479
  font-size: 0.8125rem;
480
+ color: var(--color-text-secondary);
481
+ font-weight: var(--font-weight-regular);
482
  white-space: nowrap;
483
  overflow: hidden;
484
  text-overflow: ellipsis;
485
+ max-width: 110px;
 
486
  }
487
 
488
+ /* Currency Input */
489
  .currency-input {
490
  flex: 1;
491
+ display: flex;
492
+ flex-direction: column;
493
  }
494
 
495
  .currency-input input {
496
  width: 100%;
497
  padding: 12px 14px;
498
+ border: 1.5px solid var(--color-border);
499
+ border-radius: var(--radius-medium);
500
  font-size: 1.0625rem;
501
+ font-weight: var(--font-weight-medium);
502
  text-align: right;
503
+ transition: all var(--transition-base);
504
+ color: var(--color-text-primary);
505
+ background: rgba(0, 0, 0, 0.02);
 
506
  }
507
 
508
  .currency-input input:focus {
509
  outline: none;
510
+ border-color: var(--color-blue);
511
+ box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1);
512
+ background: var(--color-background);
513
  }
514
 
515
  .currency-input input::placeholder {
516
+ color: var(--color-text-tertiary);
517
+ font-weight: var(--font-weight-regular);
 
518
  }
519
 
 
520
  .currency-input input::-webkit-outer-spin-button,
521
  .currency-input input::-webkit-inner-spin-button {
522
  -webkit-appearance: none;
 
527
  -moz-appearance: textfield;
528
  }
529
 
 
530
  .currency-rate {
531
  font-size: 0.6875rem;
532
+ color: var(--color-text-tertiary);
533
  text-align: right;
534
+ margin-top: var(--space-sm);
535
+ font-weight: var(--font-weight-regular);
536
+ letter-spacing: 0.01em;
537
  }
538
 
539
+ /* ==================== Footer ==================== */
540
  footer {
541
  text-align: center;
542
+ padding: var(--space-4xl) var(--space-xl) var(--space-2xl);
543
+ }
544
+
545
+ .footer-content {
546
+ display: flex;
547
+ flex-direction: column;
548
+ align-items: center;
549
+ gap: var(--space-sm);
550
+ }
551
+
552
+ .footer-text {
553
+ color: var(--color-text-secondary);
554
  font-size: 0.875rem;
555
+ font-weight: var(--font-weight-regular);
556
  }
557
 
558
+ .footer-text a {
559
+ color: var(--color-blue);
560
  text-decoration: none;
561
+ font-weight: var(--font-weight-medium);
562
+ transition: opacity var(--transition-fast);
563
  }
564
 
565
+ .footer-text a:hover {
566
  opacity: 0.7;
567
  }
568
 
569
  .footer-note {
570
+ color: var(--color-text-tertiary);
 
571
  font-size: 0.8125rem;
572
+ font-weight: var(--font-weight-regular);
573
  }
574
 
575
+ /* ==================== Animations ==================== */
576
+ @keyframes fadeIn {
577
+ from {
578
+ opacity: 0;
579
+ transform: translateY(20px);
580
+ }
581
+ to {
582
+ opacity: 1;
583
+ transform: translateY(0);
584
+ }
585
+ }
586
+
587
+ .currency-card {
588
+ animation: fadeIn 0.4s ease-out backwards;
589
+ }
590
+
591
+ /* Staggered animation for cards */
592
+ .currency-card:nth-child(1) { animation-delay: 0.02s; }
593
+ .currency-card:nth-child(2) { animation-delay: 0.04s; }
594
+ .currency-card:nth-child(3) { animation-delay: 0.06s; }
595
+ .currency-card:nth-child(4) { animation-delay: 0.08s; }
596
+ .currency-card:nth-child(5) { animation-delay: 0.1s; }
597
+ .currency-card:nth-child(6) { animation-delay: 0.12s; }
598
+ .currency-card:nth-child(7) { animation-delay: 0.14s; }
599
+ .currency-card:nth-child(8) { animation-delay: 0.16s; }
600
+ .currency-card:nth-child(9) { animation-delay: 0.18s; }
601
+ .currency-card:nth-child(10) { animation-delay: 0.2s; }
602
+ .currency-card:nth-child(11) { animation-delay: 0.22s; }
603
+ .currency-card:nth-child(12) { animation-delay: 0.24s; }
604
+
605
+ /* ==================== Scrollbar ==================== */
606
+ ::-webkit-scrollbar {
607
+ width: 10px;
608
+ height: 10px;
609
+ }
610
+
611
+ ::-webkit-scrollbar-track {
612
+ background: var(--color-gray-1);
613
+ border-radius: var(--radius-small);
614
+ }
615
+
616
+ ::-webkit-scrollbar-thumb {
617
+ background: var(--color-gray-4);
618
+ border-radius: var(--radius-small);
619
+ transition: background var(--transition-fast);
620
+ }
621
+
622
+ ::-webkit-scrollbar-thumb:hover {
623
+ background: var(--color-gray-5);
624
+ }
625
+
626
+ /* ==================== Responsive Design ==================== */
627
  @media (max-width: 768px) {
628
  body {
629
+ padding: var(--space-md);
630
  }
631
 
632
  header {
633
+ padding: var(--space-xl) 0;
634
+ margin-bottom: var(--space-2xl);
635
  }
636
 
637
  header h1 {
638
+ font-size: 2.25rem;
639
+ }
640
+
641
+ .header-subtitle {
642
+ font-size: 1rem;
643
  }
644
 
645
  .header-info {
646
  flex-direction: column;
647
+ gap: var(--space-md);
648
+ }
649
+
650
+ .info-divider {
651
+ width: 40px;
652
+ height: 1px;
653
  }
654
 
655
  .toolbar {
 
662
 
663
  .filter-buttons {
664
  justify-content: center;
665
+ width: 100%;
666
+ }
667
+
668
+ .filter-btn {
669
+ flex: 1;
670
  }
671
 
672
  .currency-grid {
 
674
  }
675
 
676
  .currency-card {
677
+ padding: var(--space-md);
678
  }
679
 
680
  .currency-icon {
681
+ width: 48px;
682
+ height: 48px;
683
+ font-size: 1.5rem;
684
  }
685
 
686
  .currency-info {
687
+ flex: 0 0 85px;
688
+ }
689
+
690
+ .currency-code {
691
+ font-size: 1rem;
692
  }
693
 
694
  .currency-name {
695
+ font-size: 0.75rem;
696
+ max-width: 90px;
697
  }
698
+
699
+ .currency-input input {
700
+ font-size: 0.9375rem;
 
 
 
 
701
  }
702
+
703
+ footer {
704
+ padding: var(--space-2xl) var(--space-md) var(--space-xl);
705
  }
706
  }
707
 
708
+ @media (max-width: 480px) {
709
+ header h1 {
710
+ font-size: 1.875rem;
711
+ }
712
+
713
+ .header-subtitle {
714
+ font-size: 0.9375rem;
715
+ }
716
+
717
+ .info-label {
718
+ font-size: 0.6875rem;
719
+ }
720
+
721
+ .info-value {
722
+ font-size: 0.875rem;
723
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
724
  }
725
 
726
+ /* ==================== Print Styles ==================== */
727
+ @media print {
728
+ body {
729
+ background: white;
730
+ padding: 0;
731
+ }
732
+
733
+ .toolbar,
734
+ .info-banner,
735
+ .loading-state,
736
+ .error-state,
737
+ footer {
738
+ display: none;
739
+ }
740
+
741
+ .currency-card {
742
+ break-inside: avoid;
743
+ box-shadow: none;
744
+ border: 1px solid var(--color-border);
745
+ }
746
  }
static/js/app.js CHANGED
@@ -1,6 +1,6 @@
1
  /**
2
  * 汇率换算器前端交互逻辑
3
- *
4
  * 功能:
5
  * - 加载货币列表和汇率数据
6
  * - 实时输入实时换算
@@ -9,150 +9,278 @@
9
  */
10
 
11
  class CurrencyConverter {
12
- constructor() {
13
- // 数据
14
- this.currencies = [];
15
- this.rates = {};
16
- this.baseCurrency = 'CNY';
17
-
18
- // 状态
19
- this.activeInput = null;
20
- this.debounceTimer = null;
21
- this.currentFilter = 'all';
22
-
23
- // 常用货币列表
24
- this.priorityCodes = ['CNY', 'USD', 'EUR', 'GBP', 'JPY', 'HKD', 'AUD', 'CAD', 'CHF', 'SGD', 'KRW', 'TWD', 'THB', 'MYR', 'INR', 'RUB'];
25
-
26
- // 货币国旗映射
27
- this.currencyFlags = {
28
- 'USD': '🇺🇸', 'EUR': '🇪🇺', 'GBP': '🇬🇧', 'JPY': '🇯🇵', 'CNY': '🇨🇳', 'AUD': '🇦🇺',
29
- 'CAD': '🇨🇦', 'CHF': '🇨🇭', 'HKD': '🇭🇰', 'SGD': '🇸🇬', 'SEK': '🇸🇪', 'KRW': '🇰🇷',
30
- 'NOK': '🇳🇴', 'NZD': '🇳🇿', 'INR': '🇮🇳', 'MXN': '🇲🇽', 'TWD': '🇹🇼', 'ZAR': '🇿🇦',
31
- 'BRL': '🇧🇷', 'DKK': '🇩🇰', 'PLN': '🇵🇱', 'THB': '🇹🇭', 'MYR': '🇲🇾', 'HUF': '🇭🇺',
32
- 'CZK': '🇨🇿', 'ILS': '🇮🇱', 'CLP': '🇨🇱', 'PHP': '🇵🇭', 'AED': '🇦🇪', 'SAR': '🇸🇦',
33
- 'TRY': '🇹🇷', 'RUB': '🇷🇺', 'IDR': '🇮🇩', 'VND': '🇻🇳', 'ARS': '🇦🇷', 'EGP': '🇪🇬',
34
- 'PKR': '🇵🇰', 'BGN': '🇧🇬', 'RON': '🇷🇴', 'ISK': '🇮🇸', 'HRK': '🇭🇷', 'UAH': '🇺🇦'
35
- };
36
-
37
- // 初始化
38
- this.init();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  }
40
-
41
- /**
42
- * 初始化应用
43
- */
44
- async init() {
45
- try {
46
- // 并行加载数据
47
- await Promise.all([
48
- this.loadCurrencies(),
49
- this.loadRates()
50
- ]);
51
-
52
- // 隐藏加载状态
53
- this.hideLoading();
54
-
55
- // 渲染界面
56
- this.renderCurrencyGrid();
57
- this.setupEventListeners();
58
- this.updateStatusInfo();
59
-
60
- } catch (error) {
61
- console.error('Initialization failed:', error);
62
- this.showError('加载汇率数据失败,请检查网络连接');
63
- }
64
  }
65
-
66
- /**
67
- * 加载货币列表
68
- */
69
- async loadCurrencies() {
70
- try {
71
- const response = await fetch('/api/currencies');
72
-
73
- if (!response.ok) {
74
- throw new Error(`HTTP ${response.status}`);
75
- }
76
-
77
- const data = await response.json();
78
-
79
- if (data.success) {
80
- this.currencies = data.currencies;
81
- console.log(`Loaded ${this.currencies.length} currencies`);
82
- } else {
83
- throw new Error('API returned unsuccessful response');
84
- }
85
-
86
- } catch (error) {
87
- console.error('Failed to load currencies:', error);
88
- throw error;
89
- }
 
 
90
  }
91
-
92
- /**
93
- * 加载汇率数据
94
- */
95
- async loadRates() {
96
- try {
97
- const response = await fetch('/api/rates');
98
-
99
- if (!response.ok) {
100
- throw new Error(`HTTP ${response.status}`);
101
- }
102
-
103
- const data = await response.json();
104
-
105
- if (data.success) {
106
- this.rates = data.rates;
107
- this.baseCurrency = data.base_currency;
108
- console.log(`Loaded rates for ${Object.keys(this.rates).length} currencies`);
109
- } else {
110
- throw new Error('API returned unsuccessful response');
111
- }
112
-
113
- } catch (error) {
114
- console.error('Failed to load rates:', error);
115
- throw error;
116
- }
117
  }
118
-
119
- /**
120
- * 获取货币图标(国旗或符号)
121
- */
122
- getCurrencyIcon(currency) {
123
- // 优先使用国旗 emoji
124
- if (this.currencyFlags[currency.code]) {
125
- return this.currencyFlags[currency.code];
126
- }
127
- // 使用货币符号
128
- if (currency.symbol && currency.symbol.length <= 3) {
129
- return currency.symbol;
130
- }
131
- // 默认使用代码前两个字母
132
- return currency.code.substring(0, 2);
133
  }
134
-
135
- /**
136
- * 渲染货币网格
137
- */
138
- renderCurrencyGrid() {
139
- const grid = document.getElementById('currencyGrid');
140
-
141
- if (!grid) return;
142
-
143
- grid.innerHTML = this.currencies.map(currency => {
144
- const isPriority = this.priorityCodes.includes(currency.code);
145
- const rate = this.rates[currency.code] || 0;
146
- const icon = this.getCurrencyIcon(currency);
147
-
148
- return `
149
- <div class="currency-card ${isPriority ? 'priority' : ''}"
 
 
 
 
 
 
 
 
150
  data-code="${currency.code}"
151
  data-priority="${isPriority}">
152
  <div class="currency-icon">${icon}</div>
153
  <div class="currency-info">
154
  <div class="currency-code">${currency.code}</div>
155
- <div class="currency-name" title="${currency.name}">${currency.name_cn}</div>
 
 
156
  </div>
157
  <div class="currency-input">
158
  <input type="number"
@@ -161,312 +289,323 @@ class CurrencyConverter {
161
  placeholder="0.00"
162
  step="any"
163
  inputmode="decimal">
164
- <div class="currency-rate">1 ${this.baseCurrency} = ${this.formatRate(rate)} ${currency.code}</div>
 
 
165
  </div>
166
  </div>
167
  `;
168
- }).join('');
169
- }
170
-
171
- /**
172
- * 设置事件监听器
173
- */
174
- setupEventListeners() {
175
- const grid = document.getElementById('currencyGrid');
176
- const searchInput = document.getElementById('searchInput');
177
- const retryBtn = document.getElementById('retryBtn');
178
-
179
- // ========== 输入事件(事件委托)==========
180
- if (grid) {
181
- // 输入事件
182
- grid.addEventListener('input', (e) => {
183
- if (e.target.tagName === 'INPUT') {
184
- this.handleInput(e.target);
185
- }
186
- });
187
-
188
- // 焦点事件
189
- grid.addEventListener('focusin', (e) => {
190
- if (e.target.tagName === 'INPUT') {
191
- this.activeInput = e.target;
192
- const card = e.target.closest('.currency-card');
193
- if (card) {
194
- card.classList.add('active');
195
- }
196
- }
197
- });
198
-
199
- grid.addEventListener('focusout', (e) => {
200
- if (e.target.tagName === 'INPUT') {
201
- const card = e.target.closest('.currency-card');
202
- if (card) {
203
- card.classList.remove('active');
204
- }
205
- }
206
- });
207
  }
208
-
209
- // ========== 搜索事件 ==========
210
- if (searchInput) {
211
- searchInput.addEventListener('input', (e) => {
212
- this.filterCurrencies(e.target.value);
213
- });
 
 
 
 
214
  }
215
-
216
- // ========== 筛选按钮 ==========
217
- document.querySelectorAll('.filter-btn').forEach(btn => {
218
- btn.addEventListener('click', (e) => {
219
- // 更新按钮状态
220
- document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
221
- e.target.classList.add('active');
222
-
223
- // 应用筛选
224
- this.currentFilter = e.target.dataset.filter;
225
- this.applyFilter();
226
- });
227
- });
228
-
229
- // ========== 重试按钮 ==========
230
- if (retryBtn) {
231
- retryBtn.addEventListener('click', () => {
232
- this.hideError();
233
- this.showLoading();
234
- this.init();
235
- });
236
  }
 
237
  }
238
 
239
- /**
240
- * 处理输入事件
241
- */
242
- handleInput(input) {
243
- // 清除之前的定时器
244
- clearTimeout(this.debounceTimer);
245
-
246
- // 防抖处理:100ms 后执行计算
247
- this.debounceTimer = setTimeout(() => {
248
- this.calculateAll(input);
249
- }, 100);
250
- }
251
-
252
- /**
253
- * 计算所有货币的换算值
254
- */
255
- calculateAll(sourceInput) {
256
- const sourceCode = sourceInput.dataset.code;
257
- const sourceValue = parseFloat(sourceInput.value);
258
-
259
- // 如果输入为空、无效或为零,清空所有其他输入
260
- if (isNaN(sourceValue) || sourceValue === 0 || sourceInput.value.trim() === '') {
261
- this.clearAll(sourceInput);
262
- return;
263
- }
264
-
265
- const sourceRate = this.rates[sourceCode];
266
-
267
- if (!sourceRate) {
268
- console.warn(`Rate not found for ${sourceCode}`);
269
- return;
270
- }
271
-
272
- // 计算并更新所有其他货币
273
- this.currencies.forEach(currency => {
274
- if (currency.code === sourceCode) return;
275
-
276
- const targetRate = this.rates[currency.code];
277
-
278
- if (!targetRate) return;
279
-
280
- // 交叉汇率计算
281
- // sourceRate = 1 CNY = X source_currency
282
- // targetRate = 1 CNY = Y target_currency
283
- // 所以 1 source_currency = (targetRate / sourceRate) target_currency
284
- const crossRate = targetRate / sourceRate;
285
- const result = sourceValue * crossRate;
286
-
287
- const input = document.getElementById(`input-${currency.code}`);
288
-
289
- if (input) {
290
- input.value = this.formatNumber(result);
291
- }
292
- });
293
  }
294
 
295
- /**
296
- * 清空所有输入(除了指定的输入框)
297
- */
298
- clearAll(exceptInput = null) {
299
- this.currencies.forEach(currency => {
300
- const input = document.getElementById(`input-${currency.code}`);
301
-
302
- if (input && input !== exceptInput) {
303
- input.value = '';
304
- }
305
- });
 
 
 
 
 
 
 
 
 
 
 
306
  }
307
-
308
- /**
309
- * 格式化数字显示
310
- */
311
- formatNumber(num) {
312
- if (num === 0) return '';
313
-
314
- // 根据数值大小选择精度
315
- if (Math.abs(num) >= 1000) {
316
- return num.toFixed(2);
317
- } else if (Math.abs(num) >= 1) {
318
- return num.toFixed(4);
319
- } else if (Math.abs(num) >= 0.0001) {
320
- return num.toFixed(6);
321
- } else {
322
- return num.toExponential(4);
323
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  }
325
 
326
- /**
327
- * 格式化汇率显示
328
- */
329
- formatRate(rate) {
330
- if (rate >= 1) {
331
- return rate.toFixed(4);
332
- } else if (rate >= 0.0001) {
333
- return rate.toFixed(6);
334
- } else {
335
- return rate.toExponential(4);
336
- }
337
- }
338
 
339
- /**
340
- * 搜索过滤货币
341
- */
342
- filterCurrencies(keyword) {
343
- const cards = document.querySelectorAll('.currency-card');
344
- const lowerKeyword = keyword.toLowerCase().trim();
345
-
346
- cards.forEach(card => {
347
- const code = card.dataset.code.toLowerCase();
348
- const currency = this.currencies.find(c => c.code === card.dataset.code);
349
- const name = currency ? currency.name.toLowerCase() : '';
350
- const nameCn = currency ? currency.name_cn : '';
351
-
352
- // 匹配代码、英文名或中文名
353
- const matchesSearch = !lowerKeyword ||
354
- code.includes(lowerKeyword) ||
355
- name.includes(lowerKeyword) ||
356
- nameCn.includes(keyword);
357
-
358
- // 同时考虑当前筛选状态
359
- const matchesFilter = this.currentFilter === 'all' ||
360
- (this.currentFilter === 'priority' && card.dataset.priority === 'true');
361
-
362
- card.classList.toggle('hidden', !(matchesSearch && matchesFilter));
363
- });
364
  }
365
 
366
- /**
367
- * 应用筛选(常用/全部)
368
- */
369
- applyFilter() {
370
- const searchInput = document.getElementById('searchInput');
371
- const keyword = searchInput ? searchInput.value : '';
372
- this.filterCurrencies(keyword);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  }
374
-
375
- /**
376
- * 更新状态信息
377
- */
378
- async updateStatusInfo() {
379
- try {
380
- const response = await fetch('/api/status');
381
- const data = await response.json();
382
-
383
- // 更新时间
384
- const lastUpdateEl = document.getElementById('lastUpdate');
385
- if (lastUpdateEl && data.last_update) {
386
- const date = new Date(data.last_update);
387
- lastUpdateEl.textContent = date.toLocaleString('zh-CN');
388
- }
389
-
390
- // 货币数量
391
- const countEl = document.getElementById('currencyCount');
392
- if (countEl) {
393
- countEl.textContent = data.currencies_count || this.currencies.length;
394
- }
395
-
396
- // 基准货币
397
- const baseEl = document.getElementById('baseCurrency');
398
- if (baseEl) {
399
- baseEl.textContent = this.baseCurrency;
400
- }
401
-
402
- } catch (error) {
403
- console.error('Failed to update status:', error);
404
- }
405
  }
406
-
407
- /**
408
- * 显示加载状态
409
- */
410
- showLoading() {
411
- const loading = document.getElementById('loading');
412
- const grid = document.getElementById('currencyGrid');
413
- const tips = document.getElementById('tips');
414
-
415
- if (loading) loading.classList.remove('hidden');
416
- if (loading) loading.style.display = 'flex';
417
- if (grid) grid.style.display = 'none';
418
- if (tips) tips.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  }
420
-
421
- /**
422
- * 隐藏加载状态
423
- */
424
- hideLoading() {
425
- const loading = document.getElementById('loading');
426
- const grid = document.getElementById('currencyGrid');
427
- const tips = document.getElementById('tips');
428
-
429
- if (loading) loading.classList.add('hidden');
430
- if (loading) loading.style.display = 'none';
431
- if (grid) grid.style.display = 'grid';
432
- if (tips) tips.style.display = 'flex';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  }
434
 
435
- /**
436
- * 显示错误信息
437
- */
438
- showError(message) {
439
- const errorDiv = document.getElementById('errorMessage');
440
- const errorText = document.getElementById('errorText');
441
- const loading = document.getElementById('loading');
442
- const grid = document.getElementById('currencyGrid');
443
-
444
- if (loading) loading.style.display = 'none';
445
- if (grid) grid.style.display = 'none';
446
-
447
- if (errorDiv) {
448
- errorDiv.style.display = 'flex';
449
- }
450
-
451
- if (errorText) {
452
- errorText.textContent = message;
453
- }
454
  }
455
-
456
- /**
457
- * 隐藏错误信息
458
- */
459
- hideError() {
460
- const errorDiv = document.getElementById('errorMessage');
461
- if (errorDiv) {
462
- errorDiv.style.display = 'none';
463
- }
464
  }
 
465
  }
466
 
467
-
468
  // ==================== 初始化应用 ====================
469
- document.addEventListener('DOMContentLoaded', () => {
470
- // 创建全局实例
471
- window.currencyConverter = new CurrencyConverter();
472
  });
 
1
  /**
2
  * 汇率换算器前端交互逻辑
3
+ *
4
  * 功能:
5
  * - 加载货币列表和汇率数据
6
  * - 实时输入实时换算
 
9
  */
10
 
11
  class CurrencyConverter {
12
+ constructor() {
13
+ // 数据
14
+ this.currencies = [];
15
+ this.rates = {};
16
+ this.baseCurrency = "CNY";
17
+
18
+ // 状态
19
+ this.activeInput = null;
20
+ this.debounceTimer = null;
21
+ this.currentFilter = "all";
22
+
23
+ // 常用货币列表
24
+ this.priorityCodes = [
25
+ "CNY",
26
+ "USD",
27
+ "EUR",
28
+ "GBP",
29
+ "JPY",
30
+ "HKD",
31
+ "AUD",
32
+ "CAD",
33
+ "CHF",
34
+ "SGD",
35
+ "KRW",
36
+ "TWD",
37
+ "THB",
38
+ "MYR",
39
+ "INR",
40
+ "RUB",
41
+ ];
42
+
43
+ // 货币国旗映射(更全面的覆盖)
44
+ this.currencyFlags = {
45
+ // 主要货币
46
+ USD: "🇺🇸",
47
+ EUR: "🇪🇺",
48
+ GBP: "🇬🇧",
49
+ JPY: "🇯🇵",
50
+ CNY: "🇨🇳",
51
+ AUD: "🇦🇺",
52
+ CAD: "🇨🇦",
53
+ CHF: "🇨🇭",
54
+ HKD: "🇭🇰",
55
+ SGD: "🇸🇬",
56
+
57
+ // 欧洲
58
+ SEK: "🇸🇪",
59
+ NOK: "🇳🇴",
60
+ DKK: "🇩🇰",
61
+ PLN: "🇵🇱",
62
+ HUF: "🇭🇺",
63
+ CZK: "🇨🇿",
64
+ RON: "🇷🇴",
65
+ BGN: "🇧🇬",
66
+ HRK: "🇭🇷",
67
+ ISK: "🇮🇸",
68
+
69
+ // 亚洲
70
+ KRW: "🇰🇷",
71
+ TWD: "🇹🇼",
72
+ THB: "🇹🇭",
73
+ MYR: "🇲🇾",
74
+ INR: "🇮🇳",
75
+ IDR: "🇮🇩",
76
+ VND: "🇻🇳",
77
+ PHP: "🇵🇭",
78
+ PKR: "🇵🇰",
79
+
80
+ // 中东
81
+ AED: "🇦🇪",
82
+ SAR: "🇸🇦",
83
+ ILS: "🇮🇱",
84
+ TRY: "🇹🇷",
85
+ EGP: "🇪🇬",
86
+
87
+ // 美洲
88
+ MXN: "🇲🇽",
89
+ BRL: "🇧🇷",
90
+ ARS: "🇦🇷",
91
+ CLP: "🇨🇱",
92
+ COP: "🇨🇴",
93
+ PEN: "🇵🇪",
94
+ UYU: "🇺🇾",
95
+
96
+ // 非洲
97
+ ZAR: "🇿🇦",
98
+ NGN: "🇳🇬",
99
+ KES: "🇰🇪",
100
+
101
+ // 大洋洲
102
+ NZD: "🇳🇿",
103
+
104
+ // 东欧与独联体
105
+ RUB: "🇷🇺",
106
+ UAH: "🇺🇦",
107
+ KZT: "🇰🇿",
108
+ };
109
+
110
+ // 货币符号映射(用于没有国旗的货币)
111
+ this.currencySymbols = {
112
+ USD: "$",
113
+ EUR: "€",
114
+ GBP: "£",
115
+ JPY: "¥",
116
+ CNY: "¥",
117
+ AUD: "$",
118
+ CAD: "$",
119
+ CHF: "Fr",
120
+ HKD: "$",
121
+ SGD: "$",
122
+ KRW: "₩",
123
+ TWD: "$",
124
+ THB: "฿",
125
+ MYR: "RM",
126
+ INR: "₹",
127
+ IDR: "Rp",
128
+ VND: "₫",
129
+ PHP: "₱",
130
+ AED: "د.إ",
131
+ SAR: "﷼",
132
+ ILS: "₪",
133
+ TRY: "₺",
134
+ RUB: "₽",
135
+ MXN: "$",
136
+ BRL: "R$",
137
+ ARS: "$",
138
+ ZAR: "R",
139
+ NZD: "$",
140
+ SEK: "kr",
141
+ NOK: "kr",
142
+ DKK: "kr",
143
+ PLN: "zł",
144
+ CZK: "Kč",
145
+ HUF: "Ft",
146
+ RON: "lei",
147
+ BGN: "лв",
148
+ HRK: "kn",
149
+ ISK: "kr",
150
+ PKR: "₨",
151
+ EGP: "£",
152
+ CLP: "$",
153
+ COP: "$",
154
+ PEN: "S/",
155
+ UYU: "$",
156
+ NGN: "₦",
157
+ KES: "KSh",
158
+ UAH: "₴",
159
+ KZT: "₸",
160
+ };
161
+
162
+ // 初始化
163
+ this.init();
164
+ }
165
+
166
+ /**
167
+ * 初始化应用
168
+ */
169
+ async init() {
170
+ try {
171
+ // 并行加载数据
172
+ await Promise.all([this.loadCurrencies(), this.loadRates()]);
173
+
174
+ // 隐藏加载状态
175
+ this.hideLoading();
176
+
177
+ // 渲染界面
178
+ this.renderCurrencyGrid();
179
+ this.setupEventListeners();
180
+ this.updateStatusInfo();
181
+ } catch (error) {
182
+ console.error("Initialization failed:", error);
183
+ this.showError("加载汇率数据失败,请检查���络连接");
184
  }
185
+ }
186
+
187
+ /**
188
+ * 加载货币列表
189
+ */
190
+ async loadCurrencies() {
191
+ try {
192
+ const response = await fetch("/api/currencies");
193
+
194
+ if (!response.ok) {
195
+ throw new Error(`HTTP ${response.status}`);
196
+ }
197
+
198
+ const data = await response.json();
199
+
200
+ if (data.success) {
201
+ this.currencies = data.currencies;
202
+ console.log(`Loaded ${this.currencies.length} currencies`);
203
+ } else {
204
+ throw new Error("API returned unsuccessful response");
205
+ }
206
+ } catch (error) {
207
+ console.error("Failed to load currencies:", error);
208
+ throw error;
209
  }
210
+ }
211
+
212
+ /**
213
+ * 加载汇率数据
214
+ */
215
+ async loadRates() {
216
+ try {
217
+ const response = await fetch("/api/rates");
218
+
219
+ if (!response.ok) {
220
+ throw new Error(`HTTP ${response.status}`);
221
+ }
222
+
223
+ const data = await response.json();
224
+
225
+ if (data.success) {
226
+ this.rates = data.rates;
227
+ this.baseCurrency = data.base_currency;
228
+ console.log(
229
+ `Loaded rates for ${Object.keys(this.rates).length} currencies`
230
+ );
231
+ } else {
232
+ throw new Error("API returned unsuccessful response");
233
+ }
234
+ } catch (error) {
235
+ console.error("Failed to load rates:", error);
236
+ throw error;
237
  }
238
+ }
239
+
240
+ /**
241
+ * 获取货币图标(国旗或符号)
242
+ */
243
+ getCurrencyIcon(currency) {
244
+ // 优先使用国旗 emoji
245
+ if (this.currencyFlags[currency.code]) {
246
+ return this.currencyFlags[currency.code];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  }
248
+ // 使用货币符号
249
+ if (this.currencySymbols[currency.code]) {
250
+ return this.currencySymbols[currency.code];
 
 
 
 
 
 
 
 
 
 
 
 
251
  }
252
+ // 备选:使用 currency.symbol(如果API提供)
253
+ if (currency.symbol && currency.symbol.length <= 3) {
254
+ return currency.symbol;
255
+ }
256
+ // 最后:使用货币代码的前两个字母
257
+ return currency.code.substring(0, 2);
258
+ }
259
+
260
+ /**
261
+ * 渲染货币网格
262
+ */
263
+ renderCurrencyGrid() {
264
+ const grid = document.getElementById("currencyGrid");
265
+
266
+ if (!grid) return;
267
+
268
+ grid.innerHTML = this.currencies
269
+ .map((currency) => {
270
+ const isPriority = this.priorityCodes.includes(currency.code);
271
+ const rate = this.rates[currency.code] || 0;
272
+ const icon = this.getCurrencyIcon(currency);
273
+
274
+ return `
275
+ <div class="currency-card ${isPriority ? "priority" : ""}"
276
  data-code="${currency.code}"
277
  data-priority="${isPriority}">
278
  <div class="currency-icon">${icon}</div>
279
  <div class="currency-info">
280
  <div class="currency-code">${currency.code}</div>
281
+ <div class="currency-name" title="${currency.name}">${
282
+ currency.name_cn
283
+ }</div>
284
  </div>
285
  <div class="currency-input">
286
  <input type="number"
 
289
  placeholder="0.00"
290
  step="any"
291
  inputmode="decimal">
292
+ <div class="currency-rate">1 ${
293
+ this.baseCurrency
294
+ } = ${this.formatRate(rate)} ${currency.code}</div>
295
  </div>
296
  </div>
297
  `;
298
+ })
299
+ .join("");
300
+ }
301
+
302
+ /**
303
+ * 设置事件监听器
304
+ */
305
+ setupEventListeners() {
306
+ const grid = document.getElementById("currencyGrid");
307
+ const searchInput = document.getElementById("searchInput");
308
+ const retryBtn = document.getElementById("retryBtn");
309
+
310
+ // ========== 输入事件(事件委托)==========
311
+ if (grid) {
312
+ // 输入事件
313
+ grid.addEventListener("input", (e) => {
314
+ if (e.target.tagName === "INPUT") {
315
+ this.handleInput(e.target);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  }
317
+ });
318
+
319
+ // 焦点事件
320
+ grid.addEventListener("focusin", (e) => {
321
+ if (e.target.tagName === "INPUT") {
322
+ this.activeInput = e.target;
323
+ const card = e.target.closest(".currency-card");
324
+ if (card) {
325
+ card.classList.add("active");
326
+ }
327
  }
328
+ });
329
+
330
+ grid.addEventListener("focusout", (e) => {
331
+ if (e.target.tagName === "INPUT") {
332
+ const card = e.target.closest(".currency-card");
333
+ if (card) {
334
+ card.classList.remove("active");
335
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  }
337
+ });
338
  }
339
 
340
+ // ========== 搜索事件 ==========
341
+ if (searchInput) {
342
+ searchInput.addEventListener("input", (e) => {
343
+ this.filterCurrencies(e.target.value);
344
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  }
346
 
347
+ // ========== 筛选按钮 ==========
348
+ document.querySelectorAll(".filter-btn").forEach((btn) => {
349
+ btn.addEventListener("click", (e) => {
350
+ // 更新按钮状态
351
+ document
352
+ .querySelectorAll(".filter-btn")
353
+ .forEach((b) => b.classList.remove("active"));
354
+ e.target.classList.add("active");
355
+
356
+ // 应用筛选
357
+ this.currentFilter = e.target.dataset.filter;
358
+ this.applyFilter();
359
+ });
360
+ });
361
+
362
+ // ========== 重试按钮 ==========
363
+ if (retryBtn) {
364
+ retryBtn.addEventListener("click", () => {
365
+ this.hideError();
366
+ this.showLoading();
367
+ this.init();
368
+ });
369
  }
370
+ }
371
+
372
+ /**
373
+ * 处理输入事件
374
+ */
375
+ handleInput(input) {
376
+ // 清除之前的定时器
377
+ clearTimeout(this.debounceTimer);
378
+
379
+ // 防抖处理:100ms 后执行计算
380
+ this.debounceTimer = setTimeout(() => {
381
+ this.calculateAll(input);
382
+ }, 100);
383
+ }
384
+
385
+ /**
386
+ * 计算所有货币的换算值
387
+ */
388
+ calculateAll(sourceInput) {
389
+ const sourceCode = sourceInput.dataset.code;
390
+ const sourceValue = parseFloat(sourceInput.value);
391
+
392
+ // 如果输入为空、无效或为零,清空所有其他输入
393
+ if (
394
+ isNaN(sourceValue) ||
395
+ sourceValue === 0 ||
396
+ sourceInput.value.trim() === ""
397
+ ) {
398
+ this.clearAll(sourceInput);
399
+ return;
400
  }
401
 
402
+ const sourceRate = this.rates[sourceCode];
 
 
 
 
 
 
 
 
 
 
 
403
 
404
+ if (!sourceRate) {
405
+ console.warn(`Rate not found for ${sourceCode}`);
406
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  }
408
 
409
+ // 计算并更新所有其他货币
410
+ this.currencies.forEach((currency) => {
411
+ if (currency.code === sourceCode) return;
412
+
413
+ const targetRate = this.rates[currency.code];
414
+
415
+ if (!targetRate) return;
416
+
417
+ // 交叉汇率计算
418
+ // sourceRate = 1 CNY = X source_currency
419
+ // targetRate = 1 CNY = Y target_currency
420
+ // 所以 1 source_currency = (targetRate / sourceRate) target_currency
421
+ const crossRate = targetRate / sourceRate;
422
+ const result = sourceValue * crossRate;
423
+
424
+ const input = document.getElementById(`input-${currency.code}`);
425
+
426
+ if (input) {
427
+ input.value = this.formatNumber(result);
428
+ }
429
+ });
430
+ }
431
+
432
+ /**
433
+ * 清空所有输入(除了指定的输入框)
434
+ */
435
+ clearAll(exceptInput = null) {
436
+ this.currencies.forEach((currency) => {
437
+ const input = document.getElementById(`input-${currency.code}`);
438
+
439
+ if (input && input !== exceptInput) {
440
+ input.value = "";
441
+ }
442
+ });
443
+ }
444
+
445
+ /**
446
+ * 格式化数字显示
447
+ */
448
+ formatNumber(num) {
449
+ if (num === 0) return "";
450
+
451
+ // 根据数值大小选择精度
452
+ if (Math.abs(num) >= 1000) {
453
+ return num.toFixed(2);
454
+ } else if (Math.abs(num) >= 1) {
455
+ return num.toFixed(4);
456
+ } else if (Math.abs(num) >= 0.0001) {
457
+ return num.toFixed(6);
458
+ } else {
459
+ return num.toExponential(4);
460
  }
461
+ }
462
+
463
+ /**
464
+ * 格式化汇率显示
465
+ */
466
+ formatRate(rate) {
467
+ if (rate >= 1) {
468
+ return rate.toFixed(4);
469
+ } else if (rate >= 0.0001) {
470
+ return rate.toFixed(6);
471
+ } else {
472
+ return rate.toExponential(4);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
473
  }
474
+ }
475
+
476
+ /**
477
+ * 搜索过滤货币
478
+ */
479
+ filterCurrencies(keyword) {
480
+ const cards = document.querySelectorAll(".currency-card");
481
+ const lowerKeyword = keyword.toLowerCase().trim();
482
+
483
+ cards.forEach((card) => {
484
+ const code = card.dataset.code.toLowerCase();
485
+ const currency = this.currencies.find(
486
+ (c) => c.code === card.dataset.code
487
+ );
488
+ const name = currency ? currency.name.toLowerCase() : "";
489
+ const nameCn = currency ? currency.name_cn : "";
490
+
491
+ // 匹配代码、英文名或中文名
492
+ const matchesSearch =
493
+ !lowerKeyword ||
494
+ code.includes(lowerKeyword) ||
495
+ name.includes(lowerKeyword) ||
496
+ nameCn.includes(keyword);
497
+
498
+ // 同时考虑当前筛选状态
499
+ const matchesFilter =
500
+ this.currentFilter === "all" ||
501
+ (this.currentFilter === "priority" && card.dataset.priority === "true");
502
+
503
+ card.classList.toggle("hidden", !(matchesSearch && matchesFilter));
504
+ });
505
+ }
506
+
507
+ /**
508
+ * 应用筛选(常用/全部)
509
+ */
510
+ applyFilter() {
511
+ const searchInput = document.getElementById("searchInput");
512
+ const keyword = searchInput ? searchInput.value : "";
513
+ this.filterCurrencies(keyword);
514
+ }
515
+
516
+ /**
517
+ * 更新状态信息
518
+ */
519
+ async updateStatusInfo() {
520
+ try {
521
+ const response = await fetch("/api/status");
522
+ const data = await response.json();
523
+
524
+ // 更新时间
525
+ const lastUpdateEl = document.getElementById("lastUpdate");
526
+ if (lastUpdateEl && data.last_update) {
527
+ const date = new Date(data.last_update);
528
+ lastUpdateEl.textContent = date.toLocaleString("zh-CN");
529
+ }
530
+
531
+ // 货币数量
532
+ const countEl = document.getElementById("currencyCount");
533
+ if (countEl) {
534
+ countEl.textContent = data.currencies_count || this.currencies.length;
535
+ }
536
+
537
+ // 基准货币
538
+ const baseEl = document.getElementById("baseCurrency");
539
+ if (baseEl) {
540
+ baseEl.textContent = this.baseCurrency;
541
+ }
542
+ } catch (error) {
543
+ console.error("Failed to update status:", error);
544
  }
545
+ }
546
+
547
+ /**
548
+ * 显示加载状态
549
+ */
550
+ showLoading() {
551
+ const loading = document.getElementById("loading");
552
+ const grid = document.getElementById("currencyGrid");
553
+ const tips = document.getElementById("tips");
554
+
555
+ if (loading) loading.classList.remove("hidden");
556
+ if (loading) loading.style.display = "flex";
557
+ if (grid) grid.style.display = "none";
558
+ if (tips) tips.style.display = "none";
559
+ }
560
+
561
+ /**
562
+ * 隐藏加载状态
563
+ */
564
+ hideLoading() {
565
+ const loading = document.getElementById("loading");
566
+ const grid = document.getElementById("currencyGrid");
567
+ const tips = document.getElementById("tips");
568
+
569
+ if (loading) loading.classList.add("hidden");
570
+ if (loading) loading.style.display = "none";
571
+ if (grid) grid.style.display = "grid";
572
+ if (tips) tips.style.display = "flex";
573
+ }
574
+
575
+ /**
576
+ * 显示错误信息
577
+ */
578
+ showError(message) {
579
+ const errorDiv = document.getElementById("errorMessage");
580
+ const errorText = document.getElementById("errorText");
581
+ const loading = document.getElementById("loading");
582
+ const grid = document.getElementById("currencyGrid");
583
+
584
+ if (loading) loading.style.display = "none";
585
+ if (grid) grid.style.display = "none";
586
+
587
+ if (errorDiv) {
588
+ errorDiv.style.display = "flex";
589
  }
590
 
591
+ if (errorText) {
592
+ errorText.textContent = message;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
593
  }
594
+ }
595
+
596
+ /**
597
+ * 隐藏错误信息
598
+ */
599
+ hideError() {
600
+ const errorDiv = document.getElementById("errorMessage");
601
+ if (errorDiv) {
602
+ errorDiv.style.display = "none";
603
  }
604
+ }
605
  }
606
 
 
607
  // ==================== 初始化应用 ====================
608
+ document.addEventListener("DOMContentLoaded", () => {
609
+ // 创建全局实例
610
+ window.currencyConverter = new CurrencyConverter();
611
  });
templates/index.html CHANGED
@@ -1,84 +1,156 @@
1
  <!DOCTYPE html>
2
  <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Currency Converter · 汇率换算</title>
7
- <link rel="stylesheet" href="/static/css/style.css">
8
- <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>💱</text></svg>">
9
- </head>
10
- <body>
 
 
 
11
  <div class="container">
12
- <!-- 头部 -->
13
- <header>
14
- <h1>💱 Currency Converter</h1>
15
- <div class="header-info">
16
- <p class="update-time">
17
- <span class="label">Last Update</span>
18
- <span id="lastUpdate">Loading...</span>
19
- </p>
20
- <p class="currency-count">
21
- <span class="label">Currencies</span>
22
- <span id="currencyCount">-</span>
23
- </p>
24
- </div>
25
- </header>
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
- <!-- 主体内容 -->
28
- <main>
29
- <!-- 搜索和筛选 -->
30
- <div class="toolbar">
31
- <div class="search-box">
32
- <svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
33
- <circle cx="11" cy="11" r="8"/>
34
- <path d="M21 21l-4.35-4.35"/>
35
- </svg>
36
- <input type="text" id="searchInput" placeholder="Search currency code or name...">
37
- </div>
38
- <div class="filter-buttons">
39
- <button class="filter-btn active" data-filter="all">All</button>
40
- <button class="filter-btn" data-filter="priority">Popular</button>
41
- </div>
42
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
- <!-- 提示信息 -->
45
- <div class="tips" id="tips">
46
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
47
- <circle cx="12" cy="12" r="10"/>
48
- <path d="M12 16v-4M12 8h.01"/>
49
- </svg>
50
- <span>Enter amount in any currency field to convert automatically</span>
51
- </div>
 
 
 
 
 
 
 
 
 
52
 
53
- <!-- 加载状态 -->
54
- <div class="loading" id="loading">
55
- <div class="spinner"></div>
56
- <p>Loading exchange rates...</p>
57
- </div>
58
 
59
- <!-- 错误提示 -->
60
- <div class="error-message" id="errorMessage" style="display: none;">
61
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
62
- <circle cx="12" cy="12" r="10"/>
63
- <path d="M15 9l-6 6M9 9l6 6"/>
64
- </svg>
65
- <span id="errorText">Failed to load</span>
66
- <button id="retryBtn" class="retry-btn">Retry</button>
67
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
- <!-- 货币列表 -->
70
- <div class="currency-grid" id="currencyGrid">
71
- <!-- 动态生成货币卡片 -->
72
- </div>
73
- </main>
74
 
75
- <!-- 底部 -->
76
- <footer>
77
- <p>Data Source: <a href="https://www.exchangerate-api.com/" target="_blank" rel="noopener">ExchangeRate-API</a></p>
78
- <p class="footer-note">Daily Updates · Base Currency: <span id="baseCurrency">CNY</span></p>
79
- </footer>
 
 
 
 
 
 
 
 
 
 
80
  </div>
81
 
82
  <script src="/static/js/app.js"></script>
83
- </body>
84
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Currency Converter</title>
7
+ <link rel="stylesheet" href="/static/css/style.css" />
8
+ <link
9
+ rel="icon"
10
+ href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>💱</text></svg>"
11
+ />
12
+ </head>
13
+ <body>
14
  <div class="container">
15
+ <!-- Header -->
16
+ <header>
17
+ <div class="header-content">
18
+ <h1>Currency Converter</h1>
19
+ <p class="header-subtitle">
20
+ Real-time exchange rates for 161 currencies
21
+ </p>
22
+ </div>
23
+ <div class="header-info">
24
+ <div class="info-item">
25
+ <span class="info-label">Last Update</span>
26
+ <span class="info-value" id="lastUpdate">Loading...</span>
27
+ </div>
28
+ <div class="info-divider"></div>
29
+ <div class="info-item">
30
+ <span class="info-label">Base Currency</span>
31
+ <span class="info-value" id="baseCurrency">CNY</span>
32
+ </div>
33
+ <div class="info-divider"></div>
34
+ <div class="info-item">
35
+ <span class="info-label">Supported</span>
36
+ <span class="info-value"
37
+ ><span id="currencyCount">-</span> currencies</span
38
+ >
39
+ </div>
40
+ </div>
41
+ </header>
42
 
43
+ <!-- Main Content -->
44
+ <main>
45
+ <!-- Toolbar -->
46
+ <div class="toolbar">
47
+ <div class="search-box">
48
+ <svg
49
+ class="search-icon"
50
+ viewBox="0 0 24 24"
51
+ fill="none"
52
+ stroke="currentColor"
53
+ stroke-width="2.5"
54
+ stroke-linecap="round"
55
+ >
56
+ <circle cx="11" cy="11" r="8" />
57
+ <path d="M21 21l-4.35-4.35" />
58
+ </svg>
59
+ <input
60
+ type="text"
61
+ id="searchInput"
62
+ placeholder="Search currencies..."
63
+ autocomplete="off"
64
+ />
65
+ </div>
66
+ <div class="filter-buttons">
67
+ <button class="filter-btn active" data-filter="all">
68
+ <span>All</span>
69
+ </button>
70
+ <button class="filter-btn" data-filter="priority">
71
+ <span>Popular</span>
72
+ </button>
73
+ </div>
74
+ </div>
75
 
76
+ <!-- Info Banner -->
77
+ <div class="info-banner" id="tips">
78
+ <svg
79
+ class="info-icon"
80
+ viewBox="0 0 24 24"
81
+ fill="none"
82
+ stroke="currentColor"
83
+ stroke-width="2"
84
+ stroke-linecap="round"
85
+ >
86
+ <circle cx="12" cy="12" r="10" />
87
+ <path d="M12 16v-4M12 8h.01" />
88
+ </svg>
89
+ <span
90
+ >Enter an amount in any currency to see instant conversions</span
91
+ >
92
+ </div>
93
 
94
+ <!-- Loading State -->
95
+ <div class="loading-state" id="loading">
96
+ <div class="loading-spinner"></div>
97
+ <p class="loading-text">Loading exchange rates</p>
98
+ </div>
99
 
100
+ <!-- Error State -->
101
+ <div class="error-state" id="errorMessage" style="display: none">
102
+ <div class="error-icon">
103
+ <svg
104
+ viewBox="0 0 24 24"
105
+ fill="none"
106
+ stroke="currentColor"
107
+ stroke-width="2"
108
+ stroke-linecap="round"
109
+ >
110
+ <circle cx="12" cy="12" r="10" />
111
+ <path d="M15 9l-6 6M9 9l6 6" />
112
+ </svg>
113
+ </div>
114
+ <p class="error-text" id="errorText">Failed to load exchange rates</p>
115
+ <button id="retryBtn" class="retry-button">
116
+ <svg
117
+ viewBox="0 0 24 24"
118
+ fill="none"
119
+ stroke="currentColor"
120
+ stroke-width="2"
121
+ stroke-linecap="round"
122
+ >
123
+ <path
124
+ d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"
125
+ />
126
+ </svg>
127
+ <span>Try Again</span>
128
+ </button>
129
+ </div>
130
 
131
+ <!-- Currency Grid -->
132
+ <div class="currency-grid" id="currencyGrid">
133
+ <!-- Dynamically generated currency cards -->
134
+ </div>
135
+ </main>
136
 
137
+ <!-- Footer -->
138
+ <footer>
139
+ <div class="footer-content">
140
+ <p class="footer-text">
141
+ Powered by
142
+ <a
143
+ href="https://www.exchangerate-api.com/"
144
+ target="_blank"
145
+ rel="noopener"
146
+ >ExchangeRate-API</a
147
+ >
148
+ </p>
149
+ <p class="footer-note">Updates daily at midnight UTC</p>
150
+ </div>
151
+ </footer>
152
  </div>
153
 
154
  <script src="/static/js/app.js"></script>
155
+ </body>
156
  </html>