Spaces:
Running
Running
| /** | |
| * HÁN VIỆT TỪ ĐIỂN - 2025 | |
| * Phiên bản viết bởi Long Ngo, dự án này được hỗ trợ bởi CVNSS4.0 xem tại https://chuvnsongsong.com/ | |
| * Xin chân thành cảm ơn 2 Nhà nghiên cứu: Anh Kiều Trường Lâm và Thầy Trần Tư Bình đã ủng hộ cho Dự án. | |
| */ | |
| $(document).ready(function() { | |
| // --- BIẾN TOÀN CỤC --- | |
| let dictData = []; // Sẽ lấy từ dictionaryData.js | |
| let radData = []; // Sẽ lấy từ bothudata.js | |
| // Canvas vars | |
| let canvas = document.getElementById('handCanvas'); | |
| let ctx = canvas.getContext('2d'); | |
| let isDrawing = false; | |
| let strokeCount = 0; | |
| // --- KHỞI TẠO DỮ LIỆU --- | |
| initData(); | |
| initEvents(); | |
| function initData() { | |
| // Kiểm tra xem file dictionaryData.js đã load chưa | |
| if (typeof localDictionary !== 'undefined') { | |
| dictData = localDictionary; | |
| } else if (typeof dictionaryData !== 'undefined') { | |
| dictData = dictionaryData; // Fallback | |
| } else { | |
| $('#searchCount').text("Lỗi: Không tìm thấy dữ liệu từ điển!"); | |
| } | |
| // Kiểm tra file bothudata.js | |
| if (typeof universalRadical !== 'undefined') { | |
| radData = universalRadical; | |
| initRadicalDropdown(); | |
| renderRadicals(0); // Render all initially | |
| } | |
| // Render trang đầu | |
| renderList(dictData.slice(0, 50)); // Load 50 từ đầu tiên | |
| $('#searchCount').text(`Tổng: ${dictData.length} mục từ`); | |
| // Init Canvas Style | |
| ctx.lineWidth = 6; | |
| ctx.lineCap = 'round'; | |
| ctx.lineJoin = 'round'; | |
| ctx.strokeStyle = '#2C2C2C'; | |
| } | |
| // --- XỬ LÝ SỰ KIỆN (EVENTS) --- | |
| function initEvents() { | |
| // 1. Chuyển Tab | |
| $('.nav-btn').click(function() { | |
| const tabId = $(this).data('tab'); | |
| // UI Update | |
| $('.nav-btn').removeClass('active'); | |
| $(this).addClass('active'); | |
| $('.view-section').removeClass('active'); | |
| $(`#view-${tabId}`).addClass('active'); | |
| // Mobile sidebar logic | |
| if ($(window).width() <= 768) { | |
| openSidebar(); | |
| } | |
| }); | |
| // 2. Tìm kiếm (Debounce nhẹ) | |
| let searchTimeout; | |
| $('#searchInput').on('keyup', function() { | |
| clearTimeout(searchTimeout); | |
| const keyword = $(this).val().toLowerCase(); | |
| searchTimeout = setTimeout(() => { | |
| handleSearch(keyword); | |
| }, 300); | |
| }); | |
| // 3. Mobile Toggle | |
| $('#btnToggleSidebar').click(toggleSidebar); | |
| $('#overlay').click(closeSidebar); | |
| // 4. Canvas Events | |
| $('#btnRecognize').click(recognizeHandwriting); | |
| $('#btnClearHand').click(clearCanvas); | |
| // Mouse/Touch drawing | |
| $(canvas).on('mousedown touchstart', startDraw); | |
| $(canvas).on('mousemove touchmove', draw); | |
| $(canvas).on('mouseup mouseout touchend', stopDraw); | |
| // Prevent scrolling when drawing on mobile | |
| $(canvas).on('touchstart touchmove', function(e) { e.preventDefault(); }); | |
| } | |
| // --- LOGIC TÌM KIẾM & HIỂN THỊ LIST --- | |
| function handleSearch(keyword) { | |
| if (!keyword) { | |
| renderList(dictData.slice(0, 50)); | |
| $('#searchCount').text(`Tổng: ${dictData.length} mục từ`); | |
| return; | |
| } | |
| const results = dictData.filter(item => { | |
| return item.hanviet.toLowerCase().includes(keyword); | |
| }); | |
| renderList(results.slice(0, 100)); // Limit render | |
| $('#searchCount').text(`Tìm thấy ${results.length} kết quả`); | |
| } | |
| function renderList(items) { | |
| const list = $('#textResultList'); | |
| list.empty(); | |
| items.forEach(item => { | |
| // Tách chữ Hán và phiên âm (Giả định format: "Chữ PhienAm Nghia...") | |
| // Format thường gặp trong file data của bạn: "HánViệt..." | |
| let displayHan = ""; | |
| let displayViet = ""; | |
| if (item.hanviet) { | |
| const parts = item.hanviet.split(' '); | |
| displayHan = parts[0]; | |
| displayViet = parts.slice(1).join(' '); | |
| } | |
| const li = $(` | |
| <li class="list-item" data-id="${item.id}"> | |
| <span class="item-han">${displayHan}</span> | |
| <span class="item-viet">${displayViet}</span> | |
| </li> | |
| `); | |
| li.click(() => showDetail(item)); | |
| list.append(li); | |
| }); | |
| } | |
| // --- LOGIC HIỂN THỊ CHI TIẾT --- | |
| function showDetail(item) { | |
| // Tự động đóng sidebar trên mobile | |
| if ($(window).width() <= 768) { | |
| closeSidebar(); | |
| } | |
| const parts = item.hanviet.split(' '); | |
| const char = parts[0]; | |
| const phonetics = parts.slice(1).join(' '); | |
| // Format lại nghĩa (thay thế ký tự đặc biệt) | |
| let formattedMean = item.nghia || ""; | |
| formattedMean = formattedMean.replace(/◇/g, '<br><span class="meaning-label">◇</span>'); | |
| formattedMean = formattedMean.replace(/♦/g, '<br><span class="meaning-label">♦</span>'); | |
| formattedMean = formattedMean.replace(/§/g, '<span class="meaning-label">§</span>'); | |
| const html = ` | |
| <div class="detail-header"> | |
| <div class="big-char">${char}</div> | |
| <div class="detail-meta"> | |
| <div class="hanviet-main">${phonetics}</div> | |
| <div class="pinyin">Unicode: U+${char.charCodeAt(0).toString(16).toUpperCase()}</div> | |
| </div> | |
| </div> | |
| <div class="meaning-group"> | |
| ${formattedMean} | |
| </div> | |
| `; | |
| $('#detailCard').html(html); | |
| // Highlight active item | |
| $('.list-item').removeClass('selected'); | |
| $(`.list-item[data-id="${item.id}"]`).addClass('selected'); | |
| } | |
| // --- LOGIC BỘ THỦ --- | |
| function initRadicalDropdown() { | |
| // Tạo set các số nét để render dropdown | |
| // Format radData: "số_nét|ký_tự|..." | |
| // Bỏ qua phần tử 0 rỗng | |
| for(let i=1; i<=17; i++) { // Max nét bộ thủ thường là 17 | |
| $('#radicalStrokes').append(`<option value="${i}">${i} nét</option>`); | |
| } | |
| $('#radicalStrokes').change(function() { | |
| renderRadicals($(this).val()); | |
| }); | |
| } | |
| function renderRadicals(strokeCount) { | |
| const grid = $('#radicalList'); | |
| grid.empty(); | |
| // Bỏ qua index 0 | |
| for (let i = 1; i < radData.length; i++) { | |
| const line = radData[i]; | |
| const parts = line.split('|'); | |
| if (parts.length < 4) continue; | |
| const rStroke = parts[0]; | |
| const rChar = parts[1]; | |
| const rPinyin = parts[2]; | |
| const rViet = parts[3]; | |
| if (strokeCount == 0 || rStroke == strokeCount) { | |
| const box = $(` | |
| <div class="radical-box" title="${rViet} (${rPinyin})"> | |
| <span class="r-char">${rChar}</span> | |
| <span class="r-stroke">${rStroke}n</span> | |
| </div> | |
| `); | |
| box.click(() => { | |
| // Chuyển sang tab search và tìm theo bộ thủ này | |
| $('.nav-btn[data-tab="text"]').click(); | |
| $('#searchInput').val(rChar); | |
| handleSearch(rChar); | |
| }); | |
| grid.append(box); | |
| } | |
| } | |
| } | |
| // --- LOGIC VIẾT TAY (CANVAS) --- | |
| function startDraw(e) { | |
| isDrawing = true; | |
| strokeCount++; // Đếm nét sơ bộ | |
| draw(e); | |
| } | |
| function stopDraw() { | |
| isDrawing = false; | |
| ctx.beginPath(); // Reset path để nét sau không dính nét trước | |
| } | |
| function draw(e) { | |
| if (!isDrawing) return; | |
| const rect = canvas.getBoundingClientRect(); | |
| let clientX, clientY; | |
| if (e.type.includes('touch')) { | |
| clientX = e.touches[0].clientX; | |
| clientY = e.touches[0].clientY; | |
| } else { | |
| clientX = e.clientX; | |
| clientY = e.clientY; | |
| } | |
| const x = clientX - rect.left; | |
| const y = clientY - rect.top; | |
| ctx.lineTo(x, y); | |
| ctx.stroke(); | |
| ctx.beginPath(); | |
| ctx.moveTo(x, y); | |
| } | |
| function clearCanvas() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| strokeCount = 0; | |
| $('#handResults').empty(); | |
| } | |
| function recognizeHandwriting() { | |
| // Giả lập nhận dạng để UI hoạt động | |
| // (Logic thực tế sẽ cần port từ file handict.js cũ vốn rất phức tạp) | |
| const resDiv = $('#handResults'); | |
| resDiv.html('<div style="width:100%; text-align:center; color:#888;">Đang phân tích...</div>'); | |
| setTimeout(() => { | |
| resDiv.empty(); | |
| // Demo vài chữ | |
| const demo = ["一", "人", "大", "木", "本"]; | |
| demo.forEach(c => { | |
| const item = $(`<div class="hand-res-item">${c}</div>`); | |
| item.click(() => { | |
| $('.nav-btn[data-tab="text"]').click(); | |
| $('#searchInput').val(c); | |
| handleSearch(c); | |
| }); | |
| resDiv.append(item); | |
| }); | |
| }, 500); | |
| } | |
| // --- UI HELPERS --- | |
| function toggleSidebar() { | |
| $('#sidebar').toggleClass('open'); | |
| $('#overlay').toggleClass('active'); | |
| } | |
| function openSidebar() { | |
| $('#sidebar').addClass('open'); | |
| $('#overlay').addClass('active'); | |
| } | |
| function closeSidebar() { | |
| $('#sidebar').removeClass('open'); | |
| $('#overlay').removeClass('active'); | |
| } | |
| }); |