Spaces:
Running
Running
| <html lang="vi"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>Cổng Tra Cứu Đơn Vị Hành Chính</title> | |
| <meta name="description" content="Hệ thống tra cứu thông tin sáp nhập đơn vị hành chính cấp xã/phường giai đoạn mới."> | |
| <!-- Thư viện xử lý YAML --> | |
| <script src="https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js"></script> | |
| <!-- Google Fonts: Merriweather (Tiêu đề trang trọng), Roboto (Nội dung hiện đại) --> | |
| <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=Merriweather:wght@400;700;900&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| /* Bảng màu Hành chính Pháp luật */ | |
| --gov-red: #d60000; /* Đỏ cờ */ | |
| --gov-red-dark: #9b0000; /* Đỏ thẫm */ | |
| --gov-gold: #ffda00; /* Vàng sao */ | |
| --gov-gold-light: #fff59d; /* Vàng nhạt nền */ | |
| --text-primary: #2d2d2d; /* Đen văn bản */ | |
| --text-secondary: #555555; /* Xám ghi */ | |
| --bg-body: #f8f9fa; /* Xám nhạt toàn trang */ | |
| --bg-paper: #ffffff; /* Trắng giấy */ | |
| --border-color: #e0e0e0; | |
| --shadow: 0 4px 12px rgba(0, 0, 0, 0.08); | |
| --radius: 4px; /* Bo góc nhẹ, cứng cáp */ | |
| } | |
| * { box-sizing: border-box; } | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| font-family: 'Roboto', sans-serif; | |
| background-color: var(--bg-body); | |
| color: var(--text-primary); | |
| line-height: 1.6; | |
| font-size: 16px; | |
| display: flex; | |
| flex-direction: column; | |
| min-height: 100vh; | |
| } | |
| /* --- Header: Phong cách Quốc huy/Cờ --- */ | |
| header { | |
| background: linear-gradient(to bottom, var(--gov-red), var(--gov-red-dark)); | |
| color: var(--gov-gold); | |
| padding: 1.5rem 1rem; | |
| text-align: center; | |
| border-bottom: 5px solid var(--gov-gold); | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.2); | |
| position: relative; | |
| } | |
| .header-inner { | |
| max-width: 960px; | |
| margin: 0 auto; | |
| position: relative; | |
| z-index: 2; | |
| } | |
| h1 { | |
| font-family: 'Merriweather', serif; | |
| text-transform: uppercase; | |
| font-weight: 900; | |
| font-size: 1.5rem; | |
| margin: 0; | |
| letter-spacing: 1px; | |
| line-height: 1.4; | |
| text-shadow: 1px 1px 2px rgba(0,0,0,0.3); | |
| } | |
| .subtitle { | |
| font-family: 'Roboto', sans-serif; | |
| font-weight: 300; | |
| font-size: 0.95rem; | |
| margin-top: 0.5rem; | |
| color: #ffffff; | |
| opacity: 0.9; | |
| } | |
| /* --- Main Content --- */ | |
| main { | |
| flex: 1; | |
| width: 100%; | |
| max-width: 960px; | |
| margin: 2rem auto; | |
| padding: 0 1rem; | |
| display: grid; | |
| grid-template-columns: 1fr; | |
| gap: 1.5rem; | |
| } | |
| /* --- Card Styles --- */ | |
| .card { | |
| background: var(--bg-paper); | |
| border-radius: var(--radius); | |
| box-shadow: var(--shadow); | |
| border: 1px solid var(--border-color); | |
| overflow: hidden; | |
| } | |
| .card-header { | |
| background-color: #fcfcfc; | |
| border-bottom: 2px solid var(--gov-red); | |
| padding: 1rem 1.5rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .card-title { | |
| font-family: 'Merriweather', serif; | |
| font-weight: 700; | |
| color: var(--gov-red-dark); | |
| margin: 0; | |
| font-size: 1.1rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .card-title::before { | |
| content: ''; | |
| display: block; | |
| width: 4px; | |
| height: 1.1rem; | |
| background: var(--gov-red); | |
| border-radius: 2px; | |
| } | |
| .card-body { | |
| padding: 1.5rem; | |
| } | |
| /* --- Tabs --- */ | |
| .tabs { | |
| display: flex; | |
| border-bottom: 1px solid var(--border-color); | |
| margin-bottom: 1.5rem; | |
| } | |
| .tab-btn { | |
| padding: 0.75rem 1.5rem; | |
| cursor: pointer; | |
| font-weight: 600; | |
| color: var(--text-secondary); | |
| border: none; | |
| background: none; | |
| border-bottom: 3px solid transparent; | |
| transition: all 0.3s ease; | |
| font-family: 'Roboto', sans-serif; | |
| text-transform: uppercase; | |
| font-size: 0.85rem; | |
| } | |
| .tab-btn:hover { | |
| color: var(--gov-red); | |
| background-color: #fffbfb; | |
| } | |
| .tab-btn.active { | |
| color: var(--gov-red-dark); | |
| border-bottom-color: var(--gov-red); | |
| } | |
| /* --- Form Elements --- */ | |
| .form-group { | |
| margin-bottom: 1.25rem; | |
| } | |
| label { | |
| display: block; | |
| font-weight: 600; | |
| margin-bottom: 0.5rem; | |
| color: var(--text-primary); | |
| font-size: 0.95rem; | |
| } | |
| select, input[type="text"] { | |
| width: 100%; | |
| padding: 0.75rem; | |
| border: 1px solid #ccc; | |
| border-radius: var(--radius); | |
| font-size: 1rem; | |
| font-family: inherit; | |
| background-color: #fff; | |
| transition: border-color 0.2s; | |
| } | |
| select:focus, input[type="text"]:focus { | |
| outline: none; | |
| border-color: var(--gov-red); | |
| box-shadow: 0 0 0 3px rgba(214, 0, 0, 0.1); | |
| } | |
| select:disabled { | |
| background-color: #f5f5f5; | |
| color: #999; | |
| cursor: not-allowed; | |
| } | |
| .btn-container { | |
| display: flex; | |
| gap: 1rem; | |
| margin-top: 1.5rem; | |
| } | |
| button { | |
| padding: 0.75rem 1.5rem; | |
| font-size: 0.95rem; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| border: none; | |
| border-radius: var(--radius); | |
| cursor: pointer; | |
| transition: background 0.2s; | |
| font-family: 'Roboto', sans-serif; | |
| } | |
| .btn-primary { | |
| background-color: var(--gov-red); | |
| color: white; | |
| box-shadow: 0 2px 4px rgba(214, 0, 0, 0.2); | |
| } | |
| .btn-primary:hover { | |
| background-color: var(--gov-red-dark); | |
| } | |
| .btn-secondary { | |
| background-color: #e0e0e0; | |
| color: #333; | |
| } | |
| .btn-secondary:hover { | |
| background-color: #d0d0d0; | |
| } | |
| /* --- Results Area --- */ | |
| .result-area { | |
| margin-top: 1.5rem; | |
| background-color: #fffff0; /* Màu giấy vàng nhạt cổ điển */ | |
| border: 1px solid #e6dbb9; | |
| border-left: 4px solid var(--gov-gold-dark); | |
| padding: 1.25rem; | |
| border-radius: var(--radius); | |
| animation: fadeIn 0.3s ease-in-out; | |
| } | |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } } | |
| .result-title { | |
| font-family: 'Merriweather', serif; | |
| font-weight: 700; | |
| color: var(--gov-red-dark); | |
| margin-top: 0; | |
| margin-bottom: 1rem; | |
| border-bottom: 1px dashed #ccc; | |
| padding-bottom: 0.5rem; | |
| font-size: 1.1rem; | |
| } | |
| .result-row { | |
| display: flex; | |
| margin-bottom: 0.75rem; | |
| align-items: baseline; | |
| } | |
| .result-label { | |
| width: 140px; | |
| font-weight: 600; | |
| color: #666; | |
| flex-shrink: 0; | |
| font-size: 0.9rem; | |
| } | |
| .result-val { | |
| flex: 1; | |
| font-weight: 500; | |
| color: #222; | |
| } | |
| .highlight { | |
| color: var(--gov-red); | |
| font-weight: 700; | |
| } | |
| /* --- Footer --- */ | |
| footer { | |
| background-color: #333; | |
| color: #ddd; | |
| text-align: center; | |
| padding: 1.5rem; | |
| font-size: 0.85rem; | |
| border-top: 4px solid var(--gov-red); | |
| } | |
| /* --- Status Bar --- */ | |
| .status-bar { | |
| background-color: #fff3cd; | |
| color: #856404; | |
| padding: 0.75rem 1rem; | |
| border-radius: var(--radius); | |
| margin-bottom: 1.5rem; | |
| border: 1px solid #ffeeba; | |
| display: flex; | |
| align-items: center; | |
| font-size: 0.9rem; | |
| } | |
| .status-dot { | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| background-color: #ccc; | |
| margin-right: 10px; | |
| display: inline-block; | |
| } | |
| .status-dot.active { background-color: #28a745; box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.2); } | |
| .status-dot.error { background-color: #dc3545; } | |
| .status-dot.loading { background-color: var(--gov-gold); animation: pulse 1s infinite; } | |
| @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } } | |
| /* --- Responsive --- */ | |
| @media (max-width: 600px) { | |
| h1 { font-size: 1.2rem; } | |
| .result-row { flex-direction: column; } | |
| .result-label { width: 100%; margin-bottom: 2px; } | |
| .btn-container { flex-direction: column; } | |
| button { width: 100%; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="header-inner"> | |
| <h1>Cổng Thông Tin Tra Cứu<br>Đơn Vị Hành Chính</h1> | |
| <div class="subtitle">Phục vụ công tác chuyển đổi số và quản lý địa giới hành chính</div> | |
| </div> | |
| </header> | |
| <main> | |
| <!-- Trạng thái hệ thống --> | |
| <div id="systemStatus" class="status-bar"> | |
| <span id="statusDot" class="status-dot loading"></span> | |
| <span id="statusText">Đang kết nối cơ sở dữ liệu quốc gia...</span> | |
| </div> | |
| <!-- Khu vực tra cứu chính --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title">Tra Cứu Thông Tin</h2> | |
| </div> | |
| <div class="card-body"> | |
| <div class="tabs"> | |
| <button class="tab-btn active" onclick="switchTab('new')">Tra cứu theo Đơn vị Mới</button> | |
| <button class="tab-btn" onclick="switchTab('old')">Tra cứu theo Tên Cũ</button> | |
| </div> | |
| <!-- Panel 1: Tra cứu xuôi (Mới -> Cũ) --> | |
| <div id="panelNew"> | |
| <div class="form-group"> | |
| <label for="newProvince">Tỉnh / Thành phố trực thuộc Trung ương</label> | |
| <select id="newProvince" disabled> | |
| <option value="">Đang tải dữ liệu...</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="newUnit">Đơn vị hành chính cấp xã (Phường/Xã/Thị trấn)</label> | |
| <select id="newUnit" disabled> | |
| <option value="">-- Vui lòng chọn Tỉnh/Thành phố trước --</option> | |
| </select> | |
| </div> | |
| <div class="btn-container"> | |
| <button class="btn-primary" id="btnLookupNew">Thực hiện Tra cứu</button> | |
| <button class="btn-secondary" id="btnResetNew">Làm mới</button> | |
| </div> | |
| <div id="outNew"></div> | |
| </div> | |
| <!-- Panel 2: Tra cứu ngược (Cũ -> Mới) --> | |
| <div id="panelOld" style="display: none;"> | |
| <div class="form-group"> | |
| <label for="oldUnit">Tên đơn vị hành chính cũ (trước sáp nhập)</label> | |
| <input type="text" id="oldUnit" placeholder="Nhập tên xã/phường cũ (ví dụ: Xã Đông Yên, Phường 2...)"> | |
| <div style="font-size: 0.85rem; color: #666; margin-top: 5px;">* Hệ thống hỗ trợ tìm kiếm có dấu và không dấu.</div> | |
| </div> | |
| <div class="btn-container"> | |
| <button class="btn-primary" id="btnLookupOld">Tìm kiếm</button> | |
| <button class="btn-secondary" id="btnResetOld">Xóa trắng</button> | |
| </div> | |
| <div id="outOld"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Thông tin thống kê --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title">Số Liệu Tổng Quan</h2> | |
| </div> | |
| <div class="card-body" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; text-align: center;"> | |
| <div> | |
| <div style="font-size: 1.5rem; font-weight: 900; color: var(--gov-red);" id="stProvinces">-</div> | |
| <div style="font-size: 0.85rem; color: #555;">Tỉnh/TP có biến động</div> | |
| </div> | |
| <div> | |
| <div style="font-size: 1.5rem; font-weight: 900; color: var(--gov-red);" id="stNewUnits">-</div> | |
| <div style="font-size: 0.85rem; color: #555;">Đơn vị hành chính mới</div> | |
| </div> | |
| <div> | |
| <div style="font-size: 1.5rem; font-weight: 900; color: var(--gov-red);" id="stRows">-</div> | |
| <div style="font-size: 0.85rem; color: #555;">Tổng số bản ghi</div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <footer> | |
| <p>HỆ THỐNG TRA CỨU HÀNH CHÍNH QUỐC GIA (PHIÊN BẢN THỬ NGHIỆM 1.0)</p> | |
| <p>Mã nguồn mở | Hosting: Hugging Face Spaces</p> | |
| <p>© Long Ngo 2025. Căn cứ Nghị quyết 202/2025/QH15 do Quốc hội ban hành.</p> | |
| </footer> | |
| <script> | |
| (function() { | |
| let DB = null; | |
| // Cấu hình URL dữ liệu: Sử dụng đường dẫn tuyệt đối cho môi trường Space/Blob | |
| // Đây là URL trực tiếp tới file raw trên Hugging Face Space của bạn | |
| const DATA_URL = "https://huggingface.co/spaces/CVNSS/tracuusapnhap/resolve/main/tinhthanh.lookup.yaml"; | |
| const $ = (id) => document.getElementById(id); | |
| // --- Hàm xử lý chuỗi (Slugify) --- | |
| function slugify(s) { | |
| return (s || "") | |
| .trim().toLowerCase() | |
| .normalize("NFKD").replace(/[\u0300-\u036f]/g, "") | |
| .replace(/đ/g, "d") | |
| .replace(/[^a-z0-9]+/g, "-") | |
| .replace(/^-+|-+$/g, ""); | |
| } | |
| function escapeHtml(s) { | |
| return (s ?? "").toString() | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """) | |
| .replace(/'/g, "'"); | |
| } | |
| // --- Hàm hiển thị trạng thái --- | |
| function updateStatus(state, msg) { | |
| const dot = $("statusDot"); | |
| const text = $("statusText"); | |
| // Reset classes | |
| dot.classList.remove("active", "error", "loading"); | |
| text.textContent = msg; | |
| if (state === "loading") { | |
| dot.classList.add("loading"); | |
| } else if (state === "success") { | |
| dot.classList.add("active"); | |
| } else if (state === "error") { | |
| dot.classList.add("error"); | |
| } | |
| } | |
| // --- Hàm Render Kết quả --- | |
| function renderResultBox(container, htmlContent) { | |
| container.innerHTML = htmlContent; | |
| } | |
| function renderEmpty(container, msg) { | |
| container.innerHTML = `<div style="padding: 15px; color: #856404; background-color: #fff3cd; border: 1px solid #ffeeba; border-radius: 4px; margin-top: 15px;">⚠️ ${msg}</div>`; | |
| } | |
| // --- Chuyển Tab --- | |
| window.switchTab = function(mode) { | |
| const tabBtns = document.querySelectorAll(".tab-btn"); | |
| const panelNew = $("panelNew"); | |
| const panelOld = $("panelOld"); | |
| tabBtns.forEach(btn => btn.classList.remove("active")); | |
| if (mode === 'new') { | |
| tabBtns[0].classList.add("active"); | |
| panelNew.style.display = "block"; | |
| panelOld.style.display = "none"; | |
| } else { | |
| tabBtns[1].classList.add("active"); | |
| panelNew.style.display = "none"; | |
| panelOld.style.display = "block"; | |
| } | |
| } | |
| // --- Logic Dữ liệu --- | |
| function getProvinceNames() { | |
| const arr = (DB?.data?.provinces || []).map(p => p.name).filter(Boolean); | |
| return arr.sort((a,b) => a.localeCompare(b, "vi")); | |
| } | |
| function getUnitsForProvince(provinceName) { | |
| const p = (DB?.data?.provinces || []).find(x => x.name === provinceName); | |
| if (!p) return []; | |
| const units = (p.units || []).map(u => u.name).filter(Boolean); | |
| return units.sort((a,b) => a.localeCompare(b, "vi")); | |
| } | |
| function lookupByNewUnit(provinceName, unitName) { | |
| const key = `${slugify(provinceName)}/${slugify(unitName)}`; | |
| return DB?.index?.by_new_unit?.[key] || null; | |
| } | |
| function lookupByOldUnit(oldUnitName) { | |
| const k = slugify(oldUnitName); | |
| const hits = DB?.index?.by_old_unit_slug?.[k] || []; | |
| return hits; | |
| } | |
| // --- Xử lý sự kiện --- | |
| function wireControls() { | |
| const pSel = $("newProvince"); | |
| const uSel = $("newUnit"); | |
| // Khi chọn Tỉnh | |
| pSel.addEventListener("change", () => { | |
| const pName = pSel.value; | |
| uSel.innerHTML = '<option value="">-- Chọn Đơn vị hành chính --</option>'; | |
| $("outNew").innerHTML = ""; | |
| if (pName) { | |
| const units = getUnitsForProvince(pName); | |
| units.forEach(u => { | |
| const opt = document.createElement("option"); | |
| opt.value = u; | |
| opt.textContent = u; | |
| uSel.appendChild(opt); | |
| }); | |
| uSel.disabled = false; | |
| } else { | |
| uSel.disabled = true; | |
| } | |
| }); | |
| // Nút Tra cứu Mới -> Cũ | |
| $("btnLookupNew").addEventListener("click", () => { | |
| const pVal = pSel.value; | |
| const uVal = uSel.value; | |
| const out = $("outNew"); | |
| if (!pVal || !uVal) { | |
| renderEmpty(out, "Vui lòng chọn đầy đủ Tỉnh và Đơn vị hành chính."); | |
| return; | |
| } | |
| const hit = lookupByNewUnit(pVal, uVal); | |
| if (!hit) { | |
| renderEmpty(out, "Không tìm thấy dữ liệu trong hệ thống."); | |
| return; | |
| } | |
| const oldList = (hit.merged_from_units || []).map(x => `<li>${escapeHtml(x)}</li>`).join(""); | |
| const provs = (hit.merged_from_provinces || []).join(", "); | |
| renderResultBox(out, ` | |
| <div class="result-area"> | |
| <h3 class="result-title">KẾT QUẢ TRA CỨU</h3> | |
| <div class="result-row"> | |
| <div class="result-label">Tỉnh / Thành phố:</div> | |
| <div class="result-val highlight">${escapeHtml(hit.new_province)}</div> | |
| </div> | |
| <div class="result-row"> | |
| <div class="result-label">Đơn vị mới:</div> | |
| <div class="result-val highlight">${escapeHtml(hit.new_unit)} <span style="font-weight:400; font-size:0.8em; color:#666">(${escapeHtml(hit.new_unit_type)})</span></div> | |
| </div> | |
| <div class="result-row"> | |
| <div class="result-label">Hình thành từ:</div> | |
| <div class="result-val"> | |
| <ul style="margin: 0; padding-left: 20px; color: var(--gov-red-dark); font-weight: 500;"> | |
| ${oldList || "<li>(Không có dữ liệu chi tiết)</li>"} | |
| </ul> | |
| </div> | |
| </div> | |
| <div class="result-row"> | |
| <div class="result-label">Ghi chú sáp nhập:</div> | |
| <div class="result-val" style="font-style: italic; color: #555;">Sáp nhập từ tỉnh: ${escapeHtml(provs)}</div> | |
| </div> | |
| </div> | |
| `); | |
| }); | |
| // Nút Reset Mới -> Cũ | |
| $("btnResetNew").addEventListener("click", () => { | |
| pSel.value = ""; | |
| uSel.innerHTML = '<option value="">-- Vui lòng chọn Tỉnh/Thành phố trước --</option>'; | |
| uSel.disabled = true; | |
| $("outNew").innerHTML = ""; | |
| }); | |
| // Nút Tra cứu Cũ -> Mới | |
| $("btnLookupOld").addEventListener("click", () => { | |
| const input = $("oldUnit"); | |
| const val = input.value.trim(); | |
| const out = $("outOld"); | |
| if (!val) { | |
| renderEmpty(out, "Vui lòng nhập tên đơn vị cũ cần tìm."); | |
| return; | |
| } | |
| const hits = lookupByOldUnit(val); | |
| if (!hits || hits.length === 0) { | |
| renderEmpty(out, `Không tìm thấy đơn vị cũ nào có tên "${escapeHtml(val)}". Hãy thử nhập không dấu hoặc kiểm tra chính tả.`); | |
| return; | |
| } | |
| let html = `<div style="margin-top:15px; font-weight:bold; color:var(--gov-red-dark)">Tìm thấy ${hits.length} kết quả phù hợp:</div>`; | |
| hits.forEach(h => { | |
| html += ` | |
| <div class="result-area" style="margin-top: 10px;"> | |
| <div class="result-row"> | |
| <div class="result-label">Đơn vị cũ:</div> | |
| <div class="result-val" style="color: #555;">${escapeHtml(h.old_unit || val)}</div> | |
| </div> | |
| <div style="border-top: 1px solid #ddd; margin: 8px 0;"></div> | |
| <div class="result-row"> | |
| <div class="result-label">Nay thuộc về:</div> | |
| <div class="result-val highlight">${escapeHtml(h.new_unit)}</div> | |
| </div> | |
| <div class="result-row"> | |
| <div class="result-label">Tỉnh / TP:</div> | |
| <div class="result-val">${escapeHtml(h.new_province)}</div> | |
| </div> | |
| </div>`; | |
| }); | |
| renderResultBox(out, html); | |
| }); | |
| // Enter để tìm kiếm | |
| $("oldUnit").addEventListener("keydown", (e) => { | |
| if (e.key === "Enter") { | |
| e.preventDefault(); | |
| $("btnLookupOld").click(); | |
| } | |
| }); | |
| // Nút Reset Cũ -> Mới | |
| $("btnResetOld").addEventListener("click", () => { | |
| $("oldUnit").value = ""; | |
| $("outOld").innerHTML = ""; | |
| }); | |
| } | |
| function initSelectors() { | |
| const provinces = getProvinceNames(); | |
| const pSel = $("newProvince"); | |
| pSel.innerHTML = '<option value="">-- Chọn Tỉnh / Thành phố --</option>'; | |
| provinces.forEach(p => { | |
| const opt = document.createElement("option"); | |
| opt.value = p; | |
| opt.textContent = p; | |
| pSel.appendChild(opt); | |
| }); | |
| pSel.disabled = false; | |
| } | |
| // --- Hàm tải DB (Fix lỗi Fetch) --- | |
| async function loadDB() { | |
| try { | |
| updateStatus("loading", "Đang tải dữ liệu từ máy chủ..."); | |
| // Xử lý logic Fetch thông minh | |
| let response; | |
| const isLocal = window.location.hostname === "localhost" || window.location.protocol === "file:"; | |
| // Nếu đang chạy local hoặc trên đúng domain space, thử tải relative trước | |
| // Tuy nhiên, để an toàn tuyệt đối cho blob (preview) và production, ta ưu tiên URL tuyệt đối nếu URL hiện tại là blob | |
| if (window.location.protocol === 'blob:') { | |
| console.log("Phát hiện môi trường Blob/Preview. Sử dụng URL tuyệt đối."); | |
| response = await fetch(DATA_URL); | |
| } else { | |
| // Thử tải file cùng cấp (relative) trước | |
| try { | |
| const localResp = await fetch("tinhthanh.lookup.yaml"); | |
| if (localResp.ok) { | |
| response = localResp; | |
| } else { | |
| throw new Error("Local file not found"); | |
| } | |
| } catch (err) { | |
| console.warn("Không tải được file nội bộ, chuyển sang CDN/Remote...", err); | |
| response = await fetch(DATA_URL); | |
| } | |
| } | |
| if (!response.ok) throw new Error(`HTTP Error: ${response.status}`); | |
| const txt = await response.text(); | |
| DB = jsyaml.load(txt); | |
| // Cập nhật thống kê | |
| const c = DB?.meta?.counts || {}; | |
| $("stRows").textContent = (c.rows || 0).toLocaleString('vi-VN'); | |
| $("stProvinces").textContent = (c.new_provinces || DB?.data?.provinces?.length || 0).toLocaleString('vi-VN'); | |
| $("stNewUnits").textContent = (c.new_units || 0).toLocaleString('vi-VN'); | |
| updateStatus("success", "Hệ thống sẵn sàng. Dữ liệu đã được cập nhật."); | |
| initSelectors(); | |
| } catch (e) { | |
| console.error(e); | |
| updateStatus("error", "Lỗi tải dữ liệu! Vui lòng kiểm tra kết nối mạng."); | |
| const pSel = $("newProvince"); | |
| pSel.innerHTML = "<option>Lỗi kết nối CSDL</option>"; | |
| } | |
| } | |
| // --- Khởi động --- | |
| wireControls(); | |
| loadDB(); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |