Spaces:
Sleeping
Sleeping
Commit
·
6d01f8a
1
Parent(s):
73f3779
update GUI
Browse files- .snow/notebook/exchangeRates.json +29 -0
- static/css/style.css +482 -245
- static/js/app.js +556 -417
- templates/index.html +143 -71
.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 |
-
|
| 10 |
-
--
|
| 11 |
-
--
|
| 12 |
-
--
|
| 13 |
-
--
|
| 14 |
-
--
|
| 15 |
-
--
|
| 16 |
-
--
|
| 17 |
-
--
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
--
|
| 21 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
| 27 |
min-height: 100vh;
|
| 28 |
-
|
| 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:
|
| 44 |
-
padding:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
}
|
| 46 |
|
| 47 |
header h1 {
|
| 48 |
-
font-size:
|
| 49 |
-
font-weight:
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
}
|
| 54 |
|
| 55 |
.header-info {
|
| 56 |
display: flex;
|
|
|
|
| 57 |
justify-content: center;
|
| 58 |
-
gap:
|
| 59 |
flex-wrap: wrap;
|
| 60 |
}
|
| 61 |
|
| 62 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
font-size: 0.9375rem;
|
| 64 |
-
|
| 65 |
-
|
| 66 |
}
|
| 67 |
|
| 68 |
-
.
|
| 69 |
-
|
| 70 |
-
|
|
|
|
| 71 |
}
|
| 72 |
|
| 73 |
-
/* ====================
|
| 74 |
.toolbar {
|
| 75 |
display: flex;
|
| 76 |
-
gap:
|
| 77 |
-
margin-bottom:
|
| 78 |
flex-wrap: wrap;
|
| 79 |
}
|
| 80 |
|
| 81 |
-
/*
|
| 82 |
.search-box {
|
| 83 |
flex: 1;
|
| 84 |
-
min-width:
|
| 85 |
position: relative;
|
| 86 |
}
|
| 87 |
|
| 88 |
-
.search-
|
| 89 |
position: absolute;
|
| 90 |
-
left:
|
| 91 |
top: 50%;
|
| 92 |
transform: translateY(-50%);
|
| 93 |
-
width:
|
| 94 |
-
height:
|
| 95 |
-
color: var(--text-
|
|
|
|
| 96 |
}
|
| 97 |
|
| 98 |
.search-box input {
|
| 99 |
width: 100%;
|
| 100 |
-
padding: 14px 20px 14px
|
| 101 |
-
border:
|
| 102 |
-
border-radius:
|
| 103 |
-
font-size:
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
}
|
| 110 |
|
| 111 |
.search-box input:focus {
|
| 112 |
outline: none;
|
| 113 |
-
border-color: var(--
|
| 114 |
-
box-shadow: 0 0 0 4px rgba(0, 122, 255, 0.1), var(--
|
|
|
|
| 115 |
}
|
| 116 |
|
| 117 |
-
/*
|
| 118 |
.filter-buttons {
|
| 119 |
display: flex;
|
| 120 |
-
gap:
|
| 121 |
}
|
| 122 |
|
| 123 |
.filter-btn {
|
| 124 |
-
padding:
|
| 125 |
-
border:
|
| 126 |
-
border-radius:
|
| 127 |
-
background: var(--
|
| 128 |
-
backdrop-filter: blur
|
| 129 |
-
-webkit-backdrop-filter: blur
|
| 130 |
-
color: var(--text-
|
| 131 |
font-size: 0.9375rem;
|
| 132 |
-
font-weight:
|
| 133 |
cursor: pointer;
|
| 134 |
-
box-shadow: var(--
|
| 135 |
-
transition: all
|
|
|
|
| 136 |
}
|
| 137 |
|
| 138 |
.filter-btn:hover {
|
| 139 |
-
background:
|
| 140 |
transform: translateY(-1px);
|
|
|
|
| 141 |
}
|
| 142 |
|
| 143 |
.filter-btn.active {
|
| 144 |
-
background: var(--
|
| 145 |
color: white;
|
| 146 |
-
border-color: var(--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
}
|
| 148 |
|
| 149 |
-
/* ====================
|
| 150 |
-
.
|
| 151 |
display: flex;
|
| 152 |
align-items: center;
|
| 153 |
-
gap:
|
| 154 |
-
background:
|
| 155 |
-
backdrop-filter: blur
|
| 156 |
-
-webkit-backdrop-filter: blur
|
| 157 |
-
padding:
|
| 158 |
-
border-radius:
|
| 159 |
-
margin-bottom:
|
| 160 |
-
border:
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
.tips svg {
|
| 165 |
width: 20px;
|
| 166 |
height: 20px;
|
| 167 |
-
color: var(--
|
| 168 |
flex-shrink: 0;
|
| 169 |
}
|
| 170 |
|
| 171 |
-
.
|
| 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:
|
| 184 |
}
|
| 185 |
|
| 186 |
-
.spinner {
|
| 187 |
-
width:
|
| 188 |
-
height:
|
| 189 |
-
border: 3px solid rgba(0, 122, 255, 0.
|
| 190 |
-
border-top-color: var(--
|
| 191 |
border-radius: 50%;
|
| 192 |
animation: spin 0.8s linear infinite;
|
| 193 |
-
margin-bottom:
|
| 194 |
}
|
| 195 |
|
| 196 |
@keyframes spin {
|
|
@@ -199,169 +309,214 @@ header h1 {
|
|
| 199 |
}
|
| 200 |
}
|
| 201 |
|
| 202 |
-
.loading
|
| 203 |
font-size: 1.0625rem;
|
| 204 |
-
color: var(--text-secondary);
|
| 205 |
-
font-weight:
|
| 206 |
}
|
| 207 |
|
| 208 |
-
.loading.hidden {
|
| 209 |
display: none;
|
| 210 |
}
|
| 211 |
|
| 212 |
-
/* ====================
|
| 213 |
-
.error-
|
| 214 |
display: flex;
|
|
|
|
| 215 |
align-items: center;
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 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-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
}
|
| 237 |
|
| 238 |
-
.
|
| 239 |
-
|
| 240 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
color: white;
|
| 242 |
border: none;
|
| 243 |
-
border-radius:
|
| 244 |
cursor: pointer;
|
| 245 |
-
font-size: 0.
|
| 246 |
-
font-weight:
|
| 247 |
-
transition: all
|
|
|
|
| 248 |
}
|
| 249 |
|
| 250 |
-
.retry-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
background: #d70015;
|
| 252 |
-
transform:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
}
|
| 254 |
|
| 255 |
-
/* ====================
|
| 256 |
.currency-grid {
|
| 257 |
display: grid;
|
| 258 |
-
grid-template-columns: repeat(auto-fill, minmax(
|
| 259 |
-
gap:
|
|
|
|
| 260 |
}
|
| 261 |
|
| 262 |
-
/* ====================
|
| 263 |
.currency-card {
|
| 264 |
-
background: var(--
|
| 265 |
-
backdrop-filter: blur
|
| 266 |
-
-webkit-backdrop-filter: blur
|
| 267 |
-
border-radius:
|
| 268 |
-
padding:
|
| 269 |
-
box-shadow: var(--
|
| 270 |
display: flex;
|
| 271 |
align-items: center;
|
| 272 |
-
gap:
|
| 273 |
-
transition: all
|
| 274 |
-
border:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
}
|
| 276 |
|
| 277 |
.currency-card:hover {
|
| 278 |
transform: translateY(-2px);
|
| 279 |
-
box-shadow: var(--
|
| 280 |
-
|
| 281 |
}
|
| 282 |
|
| 283 |
-
.currency-card.priority {
|
| 284 |
-
|
| 285 |
}
|
| 286 |
|
| 287 |
.currency-card.active {
|
| 288 |
-
border-color: var(--
|
| 289 |
-
|
| 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:
|
| 300 |
-
height:
|
| 301 |
-
background: linear-gradient(135deg, rgba(0, 122, 255, 0.
|
| 302 |
-
border-radius:
|
| 303 |
display: flex;
|
| 304 |
align-items: center;
|
| 305 |
justify-content: center;
|
| 306 |
-
font-size: 1.
|
| 307 |
flex-shrink: 0;
|
| 308 |
-
border:
|
|
|
|
| 309 |
}
|
| 310 |
|
| 311 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
.currency-info {
|
| 313 |
-
flex: 0 0
|
| 314 |
}
|
| 315 |
|
| 316 |
.currency-code {
|
| 317 |
font-size: 1.125rem;
|
| 318 |
-
font-weight:
|
| 319 |
-
color: var(--text-
|
| 320 |
-
letter-spacing: -0.
|
|
|
|
| 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:
|
| 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:
|
| 342 |
-
border-radius:
|
| 343 |
font-size: 1.0625rem;
|
|
|
|
| 344 |
text-align: right;
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
background: rgba(255, 255, 255, 0.6);
|
| 349 |
}
|
| 350 |
|
| 351 |
.currency-input input:focus {
|
| 352 |
outline: none;
|
| 353 |
-
border-color: var(--
|
| 354 |
-
box-shadow: 0 0 0
|
| 355 |
-
background:
|
| 356 |
}
|
| 357 |
|
| 358 |
.currency-input input::placeholder {
|
| 359 |
-
color: var(--text-
|
| 360 |
-
|
| 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-
|
| 379 |
text-align: right;
|
| 380 |
-
margin-top:
|
| 381 |
-
font-weight:
|
|
|
|
| 382 |
}
|
| 383 |
|
| 384 |
-
/* ====================
|
| 385 |
footer {
|
| 386 |
text-align: center;
|
| 387 |
-
padding:
|
| 388 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
font-size: 0.875rem;
|
|
|
|
| 390 |
}
|
| 391 |
|
| 392 |
-
footer a {
|
| 393 |
-
color: var(--
|
| 394 |
text-decoration: none;
|
| 395 |
-
font-weight:
|
| 396 |
-
transition: opacity
|
| 397 |
}
|
| 398 |
|
| 399 |
-
footer a:hover {
|
| 400 |
opacity: 0.7;
|
| 401 |
}
|
| 402 |
|
| 403 |
.footer-note {
|
| 404 |
-
|
| 405 |
-
color: var(--text-secondary);
|
| 406 |
font-size: 0.8125rem;
|
|
|
|
| 407 |
}
|
| 408 |
|
| 409 |
-
/* ====================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
@media (max-width: 768px) {
|
| 411 |
body {
|
| 412 |
-
padding:
|
| 413 |
}
|
| 414 |
|
| 415 |
header {
|
| 416 |
-
padding:
|
|
|
|
| 417 |
}
|
| 418 |
|
| 419 |
header h1 {
|
| 420 |
-
font-size:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
}
|
| 422 |
|
| 423 |
.header-info {
|
| 424 |
flex-direction: column;
|
| 425 |
-
gap:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 446 |
}
|
| 447 |
|
| 448 |
.currency-icon {
|
| 449 |
-
width:
|
| 450 |
-
height:
|
| 451 |
-
font-size: 1.
|
| 452 |
}
|
| 453 |
|
| 454 |
.currency-info {
|
| 455 |
-
flex: 0 0
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
}
|
| 457 |
|
| 458 |
.currency-name {
|
| 459 |
-
|
|
|
|
| 460 |
}
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
@keyframes fadeIn {
|
| 465 |
-
from {
|
| 466 |
-
opacity: 0;
|
| 467 |
-
transform: translateY(10px);
|
| 468 |
}
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
}
|
| 473 |
}
|
| 474 |
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
.
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
.
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
.
|
| 489 |
-
|
| 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 |
-
|
| 508 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
}
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
}
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
| 90 |
}
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
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 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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}">${
|
|
|
|
|
|
|
| 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 ${
|
|
|
|
|
|
|
| 165 |
</div>
|
| 166 |
</div>
|
| 167 |
`;
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 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 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
}
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 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 |
-
|
| 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 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
}
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 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 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
}
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 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 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
}
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
}
|
|
|
|
| 465 |
}
|
| 466 |
|
| 467 |
-
|
| 468 |
// ==================== 初始化应用 ====================
|
| 469 |
-
document.addEventListener(
|
| 470 |
-
|
| 471 |
-
|
| 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
|
| 7 |
-
<link rel="stylesheet" href="/static/css/style.css"
|
| 8 |
-
<link
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
| 11 |
<div class="container">
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
</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>
|