Spaces:
Running
Running
| <html lang="vi"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Từ Điển Thuật Ngữ Địa Lý</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| /* --- CSS VARIABLES & RESET --- */ | |
| :root { | |
| --primary: #0ea5e9; /* Sky Blue */ | |
| --primary-dark: #0284c7; | |
| --bg-body: #f8fafc; | |
| --bg-card: #ffffff; | |
| --text-main: #1e293b; | |
| --text-muted: #64748b; | |
| --border: #e2e8f0; | |
| --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); | |
| --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); | |
| --radius: 12px; | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Inter', sans-serif; } | |
| body { | |
| background-color: var(--bg-body); | |
| color: var(--text-main); | |
| line-height: 1.6; | |
| padding-bottom: 40px; | |
| } | |
| /* --- LAYOUT --- */ | |
| .container { | |
| max-width: 800px; | |
| margin: 0 auto; | |
| padding: 0 20px; | |
| } | |
| /* --- HEADER & SEARCH --- */ | |
| header { | |
| background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); | |
| color: white; | |
| padding: 60px 0 40px; | |
| text-align: center; | |
| margin-bottom: 30px; | |
| box-shadow: var(--shadow-md); | |
| } | |
| h1 { | |
| font-size: 2.2rem; | |
| font-weight: 700; | |
| margin-bottom: 10px; | |
| letter-spacing: -0.025em; | |
| } | |
| p.subtitle { | |
| color: #94a3b8; | |
| font-size: 0.95rem; | |
| margin-bottom: 30px; | |
| } | |
| .search-box { | |
| position: relative; | |
| max-width: 600px; | |
| margin: 0 auto; | |
| } | |
| .search-input { | |
| width: 100%; | |
| padding: 16px 24px 16px 50px; | |
| border-radius: 50px; | |
| border: none; | |
| font-size: 1.1rem; | |
| box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); | |
| outline: none; | |
| transition: all 0.3s ease; | |
| } | |
| .search-input:focus { | |
| box-shadow: 0 0 0 4px rgba(14, 165, 233, 0.3); | |
| } | |
| .search-icon { | |
| position: absolute; | |
| left: 20px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: #94a3b8; | |
| width: 20px; | |
| height: 20px; | |
| } | |
| /* --- RESULTS LIST --- */ | |
| #results-area { | |
| display: grid; | |
| gap: 20px; | |
| } | |
| .term-card { | |
| background: var(--bg-card); | |
| padding: 24px; | |
| border-radius: var(--radius); | |
| border: 1px solid var(--border); | |
| box-shadow: var(--shadow-sm); | |
| transition: transform 0.2s ease, box-shadow 0.2s ease; | |
| animation: fadeIn 0.4s ease-out; | |
| } | |
| .term-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow-md); | |
| border-color: var(--primary); | |
| } | |
| .card-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 12px; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| } | |
| .term-title { | |
| font-size: 1.25rem; | |
| font-weight: 700; | |
| color: var(--primary-dark); | |
| } | |
| .term-type { | |
| font-size: 0.75rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| background-color: #f1f5f9; | |
| color: var(--text-muted); | |
| padding: 4px 10px; | |
| border-radius: 6px; | |
| font-weight: 600; | |
| } | |
| .term-def { | |
| color: #334155; | |
| font-size: 1rem; | |
| text-align: justify; | |
| } | |
| /* --- STATES --- */ | |
| .loading, .error, .empty { | |
| text-align: center; | |
| padding: 40px; | |
| color: var(--text-muted); | |
| } | |
| .error { color: #ef4444; } | |
| .highlight { | |
| background-color: #fef08a; /* Yellow highlight */ | |
| padding: 0 2px; | |
| border-radius: 2px; | |
| } | |
| /* --- ANIMATION --- */ | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| /* --- RESPONSIVE --- */ | |
| @media (max-width: 600px) { | |
| h1 { font-size: 1.8rem; } | |
| .container { padding: 0 15px; } | |
| header { padding: 40px 0 30px; border-radius: 0 0 20px 20px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="container"> | |
| <h1>Sổ Tay Thuật Ngữ Địa Lý</h1> | |
| <p class="subtitle">Tra cứu nhanh chóng, chính xác và tiện lợi</p> | |
| <div class="search-box"> | |
| <svg class="search-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> | |
| </svg> | |
| <input type="text" id="searchInput" class="search-input" placeholder="Nhập từ khoá (ví dụ: Bazan, Thủy triều, Khí hậu...)" autocomplete="off"> | |
| </div> | |
| </div> | |
| </header> | |
| <main class="container"> | |
| <div id="count-display" style="margin-bottom: 15px; color: var(--text-muted); font-size: 0.9rem;"></div> | |
| <div id="results-area"> | |
| </div> | |
| </main> | |
| <script> | |
| /** | |
| * LOGIC ỨNG DỤNG TRA CỨU | |
| * Chuyên gia: AI Assistant (100 Years Exp Sim) | |
| */ | |
| const resultsArea = document.getElementById('results-area'); | |
| const searchInput = document.getElementById('searchInput'); | |
| const countDisplay = document.getElementById('count-display'); | |
| let dictionaryData = []; | |
| // 1. Hàm khởi tạo: Tải dữ liệu JSON | |
| async function initApp() { | |
| renderState('loading', 'Đang tải dữ liệu từ điển...'); | |
| try { | |
| // Lưu ý: Cần chạy qua Live Server hoặc Local Server để tránh lỗi CORS khi fetch file local | |
| const response = await fetch('tudien_dialy.json'); | |
| if (!response.ok) throw new Error("Không tìm thấy file tudien_dialy.json"); | |
| dictionaryData = await response.json(); | |
| // Sắp xếp theo bảng chữ cái | |
| dictionaryData.sort((a, b) => a.tu_vung.localeCompare(b.tu_vung, 'vi')); | |
| renderData(dictionaryData); // Hiển thị toàn bộ ban đầu | |
| countDisplay.textContent = `Tổng số thuật ngữ: ${dictionaryData.length}`; | |
| } catch (error) { | |
| console.error(error); | |
| renderState('error', ` | |
| <strong>Lỗi tải dữ liệu!</strong><br> | |
| 1. Hãy chắc chắn file "tudien_dialy.json" nằm cùng thư mục.<br> | |
| 2. Nếu mở trực tiếp file HTML, trình duyệt có thể chặn (CORS).<br> | |
| Hãy dùng VS Code "Live Server" hoặc upload lên host. | |
| `); | |
| } | |
| } | |
| // 2. Hàm xử lý hiển thị (Render) | |
| function renderData(data, keyword = '') { | |
| resultsArea.innerHTML = ''; | |
| if (data.length === 0) { | |
| renderState('empty', 'Không tìm thấy thuật ngữ nào phù hợp.'); | |
| return; | |
| } | |
| // Sử dụng DocumentFragment để tối ưu hiệu năng render | |
| const fragment = document.createDocumentFragment(); | |
| data.forEach(item => { | |
| const card = document.createElement('div'); | |
| card.className = 'term-card'; | |
| // Highlight từ khóa nếu đang tìm kiếm | |
| let displayTitle = item.tu_vung; | |
| if (keyword) { | |
| const regex = new RegExp(`(${keyword})`, 'gi'); | |
| displayTitle = item.tu_vung.replace(regex, '<span class="highlight">$1</span>'); | |
| } | |
| card.innerHTML = ` | |
| <div class="card-header"> | |
| <div class="term-title">${displayTitle}</div> | |
| <span class="term-type">${item.phan_loai || 'Thuật ngữ'}</span> | |
| </div> | |
| <div class="term-def"> | |
| ${item.nghia} | |
| </div> | |
| `; | |
| fragment.appendChild(card); | |
| }); | |
| resultsArea.appendChild(fragment); | |
| // Cập nhật số lượng | |
| if(keyword) { | |
| countDisplay.textContent = `Tìm thấy ${data.length} kết quả cho "${keyword}"`; | |
| } else { | |
| countDisplay.textContent = `Tổng số thuật ngữ: ${dictionaryData.length}`; | |
| } | |
| } | |
| // 3. Hàm hiển thị trạng thái (Loading/Error) | |
| function renderState(type, message) { | |
| resultsArea.innerHTML = `<div class="${type}">${message}</div>`; | |
| } | |
| // 4. Hàm chuẩn hóa tiếng Việt để tìm kiếm thông minh | |
| // Ví dụ: Nhập "khi hau" vẫn tìm ra "Khí hậu" | |
| function removeVietnameseTones(str) { | |
| str = str.replace(/à|á|ạ|ả|ã|â|ầ|ấ|ậ|ẩ|ẫ|ă|ằ|ắ|ặ|ẳ|ẵ/g,"a"); | |
| str = str.replace(/è|é|ẹ|ẻ|ẽ|ê|ề|ế|ệ|ể|ễ/g,"e"); | |
| str = str.replace(/ì|í|ị|ỉ|ĩ/g,"i"); | |
| str = str.replace(/ò|ó|ọ|ỏ|õ|ô|ồ|ố|ộ|ổ|ỗ|ơ|ờ|ớ|ợ|ở|ỡ/g,"o"); | |
| str = str.replace(/ù|ú|ụ|ủ|ũ|ư|ừ|ứ|ự|ử|ữ/g,"u"); | |
| str = str.replace(/ỳ|ý|ỵ|ỷ|ỹ/g,"y"); | |
| str = str.replace(/đ/g,"d"); | |
| str = str.replace(/À|Á|Ạ|Ả|Ã|Â|Ầ|Ấ|Ậ|Ẩ|Ẫ|Ă|Ằ|Ắ|Ặ|Ẳ|Ẵ/g, "A"); | |
| str = str.replace(/È|É|Ẹ|Ẻ|Ẽ|Ê|Ề|Ế|Ệ|Ể|Ễ/g, "E"); | |
| str = str.replace(/Ì|Í|Ị|Ỉ|Ĩ/g, "I"); | |
| str = str.replace(/Ò|Ó|Ọ|Ỏ|Õ|Ô|Ồ|Ố|Ộ|Ổ|Ỗ|Ơ|Ờ|Ớ|Ợ|Ở|Ỡ/g, "O"); | |
| str = str.replace(/Ù|Ú|Ụ|Ủ|Ũ|Ư|Ừ|Ứ|Ự|Ử|Ữ/g, "U"); | |
| str = str.replace(/Ỳ|Ý|Ỵ|Ỷ|Ỹ/g, "Y"); | |
| str = str.replace(/Đ/g, "D"); | |
| // Một số ký tự đặc biệt | |
| str = str.replace(/\u0300|\u0301|\u0303|\u0309|\u0323/g, ""); | |
| return str; | |
| } | |
| // 5. Sự kiện tìm kiếm (Debounce nhẹ để mượt mà) | |
| let timeout = null; | |
| searchInput.addEventListener('input', (e) => { | |
| clearTimeout(timeout); | |
| timeout = setTimeout(() => { | |
| const rawKeyword = e.target.value.trim(); | |
| const normalizeKeyword = removeVietnameseTones(rawKeyword).toLowerCase(); | |
| if (!rawKeyword) { | |
| renderData(dictionaryData); | |
| return; | |
| } | |
| // Lọc dữ liệu: So sánh cả từ gốc và từ đã bỏ dấu | |
| const filtered = dictionaryData.filter(item => { | |
| const rawTerm = item.tu_vung.toLowerCase(); | |
| const normTerm = removeVietnameseTones(item.tu_vung).toLowerCase(); | |
| const rawDef = item.nghia.toLowerCase(); | |
| const normDef = removeVietnameseTones(item.nghia).toLowerCase(); | |
| return rawTerm.includes(rawKeyword.toLowerCase()) || | |
| normTerm.includes(normalizeKeyword) || | |
| rawDef.includes(rawKeyword.toLowerCase()) || | |
| normDef.includes(normalizeKeyword); | |
| }); | |
| renderData(filtered, rawKeyword); | |
| }, 300); // Đợi 300ms sau khi ngừng gõ mới tìm | |
| }); | |
| // Khởi chạy ứng dụng | |
| initApp(); | |
| </script> | |
| </body> | |
| </html> |