| | <!DOCTYPE html> |
| | <html lang="vi"> |
| | <head> |
| | <meta charset="UTF-8" /> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
| | <title>Ứng dụng PDF Viewer với Thumbnail & Slideshow</title> |
| | <script src="https://cdn.tailwindcss.com"></script> |
| | <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.min.js"></script> |
| | <style> |
| | .highlight { |
| | background-color: yellow; |
| | font-weight: bold; |
| | } |
| | .thumbnail { |
| | position: relative; |
| | cursor: pointer; |
| | transition: transform 0.2s ease, box-shadow 0.2s ease; |
| | } |
| | .thumbnail:hover { |
| | transform: scale(1.05); |
| | box-shadow: 0 0 12px rgba(0, 0, 0, 0.2); |
| | } |
| | .thumbnail img { |
| | width: 100%; |
| | height: auto; |
| | border-radius: 8px; |
| | } |
| | .thumbnail .title { |
| | position: absolute; |
| | bottom: 0; |
| | left: 0; |
| | right: 0; |
| | background: rgba(0,0,0,0.6); |
| | color: white; |
| | text-align: center; |
| | padding: 4px 0; |
| | font-size: 14px; |
| | border-bottom-left-radius: 8px; |
| | border-bottom-right-radius: 8px; |
| | } |
| | #pdfViewer canvas { |
| | display: block; |
| | margin: 0 auto; |
| | max-width: 100%; |
| | height: auto; |
| | border-radius: 8px; |
| | } |
| | .text-layer { |
| | position: absolute; |
| | top: 0; |
| | left: 0; |
| | width: 100%; |
| | height: 100%; |
| | pointer-events: none; |
| | z-index: 10; |
| | font-size: 16px; |
| | line-height: 1.5; |
| | } |
| | </style> |
| | </head> |
| | <body class="bg-gray-100 text-gray-800 font-sans min-h-screen flex flex-col items-center"> |
| |
|
| | <header class="w-full bg-gradient-to-r from-indigo-600 to-purple-700 text-white py-5 shadow-md"> |
| | <h1 class="text-3xl font-bold text-center">Ứng dụng PDF Viewer với Thumbnail & Tìm kiếm</h1> |
| | <p class="text-center mt-1 text-sm">Xem, trình chiếu và tìm kiếm văn bản trong file PDF</p> |
| | </header> |
| |
|
| | <main class="max-w-7xl w-full mx-auto p-6 mt-6 flex flex-col gap-6"> |
| |
|
| | |
| | <div class="border border-gray-300 rounded-lg p-4 bg-gray-50"> |
| | <h2 class="text-lg font-semibold mb-3">Chọn tài liệu PDF:</h2> |
| | <div id="pdfGrid" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4"></div> |
| | </div> |
| |
|
| | |
| | <div class="flex flex-col items-center"> |
| | <div id="pdfViewer" class="relative border border-gray-300 rounded-lg p-4 bg-gray-100 min-h-96 w-full max-w-4xl"> |
| | <p class="text-center text-gray-400 mt-20">Chọn một file PDF từ danh sách để xem</p> |
| | </div> |
| |
|
| | |
| | <div class="flex justify-between items-center w-full max-w-4xl mt-4"> |
| | <button onclick="prevPage()" id="prevBtn" class="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600 transition disabled:opacity-50 disabled:cursor-not-allowed">⬅️ Trang trước</button> |
| | <span id="pageInfo" class="text-lg font-semibold">Trang: 1 / 1</span> |
| | <button onclick="nextPage()" id="nextBtn" class="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600 transition disabled:opacity-50 disabled:cursor-not-allowed">Trang sau ➡️</button> |
| | </div> |
| |
|
| | |
| | <div class="flex gap-4 items-center mt-4"> |
| | <button onclick="startSlideshow()" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition">▶️ Bắt đầu Slideshow</button> |
| | <button onclick="stopSlideshow()" class="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 transition">⏹️ Dừng Slideshow</button> |
| | <span>Chuyển trang mỗi:</span> |
| | <select id="intervalSelect" class="border rounded px-3 py-1"> |
| | <option value="2000">2 giây</option> |
| | <option value="3000" selected>3 giây</option> |
| | <option value="5000">5 giây</option> |
| | </select> |
| | </div> |
| |
|
| | |
| | <div class="flex gap-2 items-center mt-6 w-full max-w-4xl"> |
| | <input type="text" id="searchText" placeholder="Nhập từ khóa tìm kiếm..." class="border p-2 rounded flex-grow focus:ring-2 focus:ring-blue-400 outline-none"> |
| | <button onclick="searchText()" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 transition">🔍 Tìm kiếm</button> |
| | </div> |
| | </div> |
| |
|
| | </main> |
| |
|
| | <footer class="mt-10 text-gray-500 text-center pb-4 text-sm"> |
| | © 2025 - Ứng dụng xem PDF với trang bìa, slideshow và tìm kiếm |
| | </footer> |
| |
|
| | <script> |
| | const pdfViewer = document.getElementById('pdfViewer'); |
| | const pageInfo = document.getElementById('pageInfo'); |
| | const prevBtn = document.getElementById('prevBtn'); |
| | const nextBtn = document.getElementById('nextBtn'); |
| | const pdfGrid = document.getElementById('pdfGrid'); |
| | const searchTextEl = document.getElementById('searchText'); |
| | const intervalSelect = document.getElementById('intervalSelect'); |
| | |
| | let pdfDocs = []; |
| | let currentDocIndex = -1; |
| | let currentPage = 1; |
| | let totalPages = 0; |
| | let currentHighlight = ''; |
| | let slideshowInterval = null; |
| | |
| | |
| | const pdfFiles = [ |
| | { name: 'Tài liệu 1', url: 'files/document1.pdf', thumb: 'files/thumb1.jpg' }, |
| | { name: 'Tài liệu 2', url: 'files/document2.pdf', thumb: 'files/thumb2.jpg' }, |
| | { name: 'Tài liệu 3', url: 'files/document3.pdf', thumb: 'files/thumb3.jpg' }, |
| | { name: 'Tài liệu 4', url: 'files/document4.pdf', thumb: 'files/thumb4.jpg' }, |
| | { name: 'Tài liệu 5', url: 'files/document5.pdf', thumb: 'files/thumb5.jpg' } |
| | ]; |
| | |
| | |
| | function updatePdfGrid() { |
| | pdfGrid.innerHTML = ''; |
| | pdfFiles.forEach((file, index) => { |
| | const div = document.createElement('div'); |
| | div.className = 'thumbnail'; |
| | div.innerHTML = ` |
| | <img src="${file.thumb}" alt="${file.name}" class="w-full h-auto object-cover"> |
| | <div class="title">${file.name}</div> |
| | `; |
| | div.onclick = () => { |
| | currentDocIndex = index; |
| | currentPage = 1; |
| | currentHighlight = ''; |
| | loadPdf(index); |
| | }; |
| | pdfGrid.appendChild(div); |
| | }); |
| | } |
| | |
| | |
| | function loadPdf(index) { |
| | const file = pdfFiles[index]; |
| | pdfjsLib.getDocument(file.url).promise.then(pdf => { |
| | pdfDocs[index] = pdf; |
| | totalPages = pdf.numPages; |
| | pageInfo.textContent = `Trang: 1 / ${totalPages}`; |
| | renderPage(currentPage); |
| | enableNavigationButtons(); |
| | }); |
| | } |
| | |
| | |
| | function renderPage(pageNumber) { |
| | if (currentDocIndex === -1) { |
| | pdfViewer.innerHTML = '<p class="text-center text-gray-400 mt-20">Vui lòng chọn một file PDF từ danh sách.</p>'; |
| | return; |
| | } |
| | |
| | const pdf = pdfDocs[currentDocIndex]; |
| | if (!pdf) { |
| | pdfViewer.innerHTML = '<p class="text-center text-gray-400 mt-20">Đang tải tài liệu...</p>'; |
| | return; |
| | } |
| | |
| | pdf.getPage(pageNumber).then(page => { |
| | pdfViewer.innerHTML = ''; |
| | const viewport = page.getViewport({ scale: 1.5 }); |
| | const canvas = document.createElement('canvas'); |
| | const context = canvas.getContext('2d'); |
| | canvas.height = viewport.height; |
| | canvas.width = viewport.width; |
| | |
| | const renderContext = { |
| | canvasContext: context, |
| | viewport: viewport |
| | }; |
| | |
| | pdfViewer.appendChild(canvas); |
| | page.render(renderContext).promise.then(() => { |
| | if (currentHighlight.trim() !== '') { |
| | renderTextLayer(page, currentHighlight); |
| | } |
| | }); |
| | }); |
| | } |
| | |
| | |
| | function renderTextLayer(page, highlightText) { |
| | const viewport = page.getViewport({ scale: 1.5 }); |
| | const textLayer = document.createElement('div'); |
| | textLayer.className = 'text-layer'; |
| | pdfViewer.appendChild(textLayer); |
| | |
| | page.getTextContent().then(textContent => { |
| | textLayer.innerHTML = ''; |
| | textContent.items.forEach(item => { |
| | const x = item.transform[4]; |
| | const y = viewport.height - item.transform[5]; |
| | const span = document.createElement('span'); |
| | span.style.position = 'absolute'; |
| | span.style.left = `${x}px`; |
| | span.style.top = `${y}px`; |
| | span.style.fontSize = `${viewport.scale * 14}px`; |
| | span.style.pointerEvents = 'auto'; |
| | span.innerHTML = item.str.replace(new RegExp(`(${highlightText})`, 'gi'), '<span class="highlight">$1</span>'); |
| | textLayer.appendChild(span); |
| | }); |
| | }); |
| | } |
| | |
| | |
| | function nextPage() { |
| | if (currentDocIndex === -1) return; |
| | if (currentPage < totalPages) { |
| | currentPage++; |
| | pageInfo.textContent = `Trang: ${currentPage} / ${totalPages}`; |
| | renderPage(currentPage); |
| | } |
| | enableNavigationButtons(); |
| | } |
| | |
| | function prevPage() { |
| | if (currentDocIndex === -1) return; |
| | if (currentPage > 1) { |
| | currentPage--; |
| | pageInfo.textContent = `Trang: ${currentPage} / ${totalPages}`; |
| | renderPage(currentPage); |
| | } |
| | enableNavigationButtons(); |
| | } |
| | |
| | function enableNavigationButtons() { |
| | prevBtn.disabled = currentPage <= 1; |
| | nextBtn.disabled = currentPage >= totalPages; |
| | } |
| | |
| | |
| | function searchText() { |
| | const query = searchTextEl.value.trim(); |
| | if (!query) return alert("Vui lòng nhập từ khóa để tìm kiếm."); |
| | if (currentDocIndex === -1) return alert("Vui lòng chọn một file PDF từ danh sách."); |
| | |
| | const pdf = pdfDocs[currentDocIndex]; |
| | let found = false; |
| | |
| | for (let i = 1; i <= pdf.numPages; i++) { |
| | pdf.getPage(i).then(page => { |
| | page.getTextContent().then(textContent => { |
| | const text = textContent.items.map(item => item.str).join(' ').toLowerCase(); |
| | if (text.includes(query.toLowerCase())) { |
| | currentPage = i; |
| | pageInfo.textContent = `Trang: ${currentPage} / ${totalPages}`; |
| | currentHighlight = query; |
| | renderPage(currentPage); |
| | found = true; |
| | } |
| | }); |
| | }); |
| | } |
| | |
| | setTimeout(() => { |
| | if (!found) alert(`Không tìm thấy từ khóa "${query}" trong tài liệu này.`); |
| | }, 1500); |
| | } |
| | |
| | |
| | function startSlideshow() { |
| | if (currentDocIndex === -1 || totalPages <= 1) return; |
| | stopSlideshow(); |
| | const interval = parseInt(intervalSelect.value); |
| | slideshowInterval = setInterval(() => { |
| | if (currentPage < totalPages) { |
| | currentPage++; |
| | pageInfo.textContent = `Trang: ${currentPage} / ${totalPages}`; |
| | renderPage(currentPage); |
| | } else { |
| | stopSlideshow(); |
| | } |
| | }, interval); |
| | } |
| | |
| | function stopSlideshow() { |
| | if (slideshowInterval) clearInterval(slideshowInterval); |
| | } |
| | |
| | |
| | updatePdfGrid(); |
| | </script> |
| | <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-qwensite.hf.space/logo.svg" alt="qwensite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-qwensite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >QwenSite</a> - 🧬 <a href="https://enzostvs-qwensite.hf.space?remix=bep40/invoice" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| | </html> |