Sách Giáo Khoa Việt Nam - sachgiaokhoa.org
CodeIgniter 4 website:
- Catalog 999 SGK (lớp 1-12) từ API, lọc theo lớp/môn/NXB, tìm kiếm
- Trang chủ grouped browse (bậc học -> lớp -> môn, ưu tiên môn quan trọng)
- Chi tiết sách: metadata đầy đủ, cover nghệ thuật /thumb/{W}x{H}/
- Đọc online qua gate captcha + countdown, PDF viewer pdf.js
- Download: captcha + token một lần + auto cache-bust version
- Tự đồng bộ catalog/cover khi API thay đổi (refresh:catalog)
- Legal: DMCA, miễn trừ trách nhiệm, điều khoản, bảo mật, liên hệ
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
<?= $this->extend('layouts/main') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="hero">
|
||||
<h1>Sách Giáo Khoa Việt Nam — Lớp 1 đến lớp 12</h1>
|
||||
<p>Kho tra cứu đầy đủ <?= number_format($total) ?> đầu sách giáo khoa thuộc mọi bộ sách: Kết nối tri thức với cuộc sống, Chân trời sáng tạo, Cánh Diều, Giáo dục Việt Nam... Chọn lớp hoặc môn học để bắt đầu.</p>
|
||||
<div class="stats">
|
||||
<div class="stat"><b><?= number_format($total) ?></b><span>Đầu sách</span></div>
|
||||
<div class="stat"><b>12</b><span>Khoảng lớp</span></div>
|
||||
<div class="stat"><b><?= count($publishers) ?></b><span>Nhà xuất bản / bộ sách</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php foreach ($levels as $lv): ?>
|
||||
<section class="level-section">
|
||||
<h2 class="level-title"><?= esc($lv['label']) ?></h2>
|
||||
<div class="grade-grid">
|
||||
<?php foreach ($lv['grades'] as $g): ?>
|
||||
<div class="grade-card">
|
||||
<div class="grade-covers">
|
||||
<?php foreach (array_slice($g['covers'], 0, 5) as $c): ?>
|
||||
<a href="/sach/<?= esc($c['id']) ?>" title="<?= esc($c['title']) ?>">
|
||||
<img loading="lazy" src="/thumb/240x320/<?= esc($c['id']) ?>.jpg?v=<?= media_version() ?>" alt="<?= esc($c['title']) ?>"
|
||||
onerror="this.parentNode.style.display='none'">
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<a class="grade-head" href="/?lop=<?= $g['grade'] ?>">
|
||||
<span class="grade-num">Lớp <?= $g['grade'] ?></span>
|
||||
<span class="grade-count"><?= $g['total'] ?> sách</span>
|
||||
</a>
|
||||
<div class="subject-pills">
|
||||
<?php $i = 0; foreach ($g['subjects'] as $s => $n): if ($i++ >= 8): ?>
|
||||
<a class="pill more" href="/?lop=<?= $g['grade'] ?>">+ <?= count($g['subjects']) - 8 ?> môn khác</a>
|
||||
<?php break; endif; ?>
|
||||
<a class="pill" href="/?lop=<?= $g['grade'] ?>&mon=<?= urlencode($s) ?>"><?= esc($s) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<section class="level-section">
|
||||
<h2 class="level-title">Theo nhà xuất bản / bộ sách</h2>
|
||||
<div class="pub-grid">
|
||||
<?php foreach ($publishers as $p => $n): ?>
|
||||
<a class="pub-card" href="/?nxb=<?= urlencode($p) ?>">
|
||||
<span class="pub-name"><?= esc($p) ?></span>
|
||||
<span class="pub-count"><?= $n ?> đầu sách</span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Xác nhận đọc online — <?= esc($book['title']) ?></title>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/assets/app.css">
|
||||
</head>
|
||||
<body class="reader-body">
|
||||
<main class="container">
|
||||
<div class="gate-card" id="tai-xuong">
|
||||
<div class="gate-book">
|
||||
<img src="/thumb/240x320/<?= esc($book['id']) ?>.jpg?v=<?= media_version() ?>" alt="Bìa <?= esc($book['title']) ?>" onerror="this.style.display='none'">
|
||||
<div>
|
||||
<h1><?= esc($book['title']) ?></h1>
|
||||
<p class="gate-sub">
|
||||
<?= $book['grade'] ? 'Lớp ' . esc($book['grade']) . ' · ' : '' ?><?= esc($book['subject']) ?> · <?= esc($book['publisher']) ?>
|
||||
<?php if (! empty($book['meta']['pages'])): ?> · <?= (int) $book['meta']['pages'] ?> trang<?php endif; ?>
|
||||
</p>
|
||||
<a href="/sach/<?= esc($book['id']) ?>">← Quay lại trang sách</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="countdown-note">
|
||||
⏳ Nhập mã captcha và chờ <b id="cd-seconds"><?= $countdown ?></b> giây để tiếp tục đọc online.
|
||||
Phiên đọc có hiệu lực 2 giờ, bao gồm cả tải xuống PDF.
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="error-box"><?= esc($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="/doc/<?= esc($book['id']) ?>" id="dl-form">
|
||||
<input type="hidden" name="download" value="1">
|
||||
<div class="captcha-row">
|
||||
<img src="/captcha" alt="Mã captcha" id="captcha-img">
|
||||
<button type="button" class="refresh-captcha" title="Đổi mã khác" onclick="document.getElementById('captcha-img').src='/captcha?'+Date.now()">🔄</button>
|
||||
<input type="text" name="captcha" required maxlength="5" autocomplete="off" placeholder="Nhập mã">
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block" type="submit" id="dl-btn" disabled>
|
||||
Tiếp tục đọc online
|
||||
</button>
|
||||
<p class="gate-legal">⚖️ Tài liệu thuộc bản quyền Nhà xuất bản — chỉ phục vụ mục đích tra cứu, phi thương mại.</p>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var seconds = <?= $countdown ?>;
|
||||
var btn = document.getElementById('dl-btn');
|
||||
var s1 = document.getElementById('cd-seconds');
|
||||
|
||||
var t = setInterval(function () {
|
||||
seconds--;
|
||||
if (s1) s1.textContent = seconds;
|
||||
if (seconds <= 0) {
|
||||
clearInterval(t);
|
||||
btn.disabled = false;
|
||||
btn.textContent = '✓ Tiếp tục đọc online';
|
||||
}
|
||||
}, 1000);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,103 @@
|
||||
<?= $this->extend('layouts/main') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="hero">
|
||||
<h1>
|
||||
<?php if ($filters['q'] !== ''): ?>
|
||||
Kết quả tìm kiếm "<?= esc($filters['q']) ?>"
|
||||
<?php elseif ($filters['lop'] !== ''): ?>
|
||||
Sách lớp <?= esc($filters['lop']) ?>
|
||||
<?php elseif ($filters['mon'] !== ''): ?>
|
||||
Môn <?= esc($filters['mon']) ?>
|
||||
<?php else: ?>
|
||||
Sách <?= esc($filters['nxb']) ?>
|
||||
<?php endif; ?>
|
||||
</h1>
|
||||
<p><?= number_format($total) ?> đầu sách phù hợp.</p>
|
||||
</div>
|
||||
|
||||
<form class="filters" method="get" action="/">
|
||||
<div class="grade-pills">
|
||||
<a href="<?= $filters['q'] !== '' ? '/?q=' . urlencode($filters['q']) : '/' ?>" class="<?= $filters['lop'] === '' ? 'active' : '' ?>">Tất cả</a>
|
||||
<?php foreach ($grades as $g): ?>
|
||||
<a class="<?= $filters['lop'] === (string) $g ? 'active' : '' ?>"
|
||||
href="/?<?= esc(http_build_query(array_merge($_GET, ['trang' => 1, 'lop' => $g]))) ?>">Lớp <?= $g ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<label>Môn học
|
||||
<select name="mon" onchange="this.form.submit()">
|
||||
<option value="">-- Tất cả --</option>
|
||||
<?php foreach ($subjects as $s => $n): ?>
|
||||
<option value="<?= esc($s) ?>" <?= $filters['mon'] === $s ? 'selected' : '' ?>><?= esc($s ?: 'Khác') ?> (<?= $n ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label>Nhà xuất bản
|
||||
<select name="nxb" onchange="this.form.submit()">
|
||||
<option value="">-- Tất cả --</option>
|
||||
<?php foreach ($publishers as $p => $n): ?>
|
||||
<option value="<?= esc($p) ?>" <?= $filters['nxb'] === $p ? 'selected' : '' ?>><?= esc($p) ?> (<?= $n ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<?php if ($filters['q'] !== ''): ?><input type="hidden" name="q" value="<?= esc($filters['q']) ?>"><?php endif; ?>
|
||||
<button class="btn btn-primary" type="submit">Lọc</button>
|
||||
<a class="btn btn-outline reset" href="/">Xóa lọc</a>
|
||||
</form>
|
||||
|
||||
<?php foreach ($grouped as $groupLabel => $groupBooks): ?>
|
||||
<div class="group-header">
|
||||
<h2><?= esc($groupLabel) ?> <span class="group-count"><?= count($groupBooks) ?> sách</span></h2>
|
||||
</div>
|
||||
<div class="book-grid">
|
||||
<?php foreach ($groupBooks as $b): ?>
|
||||
<?php $m = $b['meta']; ?>
|
||||
<a class="book-card" href="/sach/<?= esc($b['id']) ?>">
|
||||
<div class="book-cover">
|
||||
<img loading="lazy" src="/thumb/360x480/<?= esc($b['id']) ?>.jpg?v=<?= media_version() ?>" alt="Bìa <?= esc($b['title']) ?>"
|
||||
onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 3 4%22%3E%3Crect width=%223%22 height=%224%22 fill=%22%23e8ecf4%22/%3E%3C/svg%3E'">
|
||||
<?php if (! empty($m['pages'])): ?><span class="cover-pages"><?= (int) $m['pages'] ?> tr</span><?php endif; ?>
|
||||
</div>
|
||||
<div class="book-info">
|
||||
<h3><?= esc($b['title']) ?></h3>
|
||||
<?php if ($b['subject']): ?><span class="book-subject"><?= esc($b['subject']) ?></span><?php endif; ?>
|
||||
<div class="badges">
|
||||
<?php if ($b['grade']): ?><span class="badge">Lớp <?= esc($b['grade']) ?></span><?php endif; ?>
|
||||
<?php if ($b['publisher']): ?><span class="badge pub"><?= esc($b['publisher']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
<div class="book-meta">
|
||||
<?php if (! empty($m['sizeMB'])): ?><span>💾 <?= number_format((float) $m['sizeMB'], 1) ?> MB</span><?php endif; ?>
|
||||
<?php if (! empty($m['authors'])): ?><span class="authors">✍ <?= esc(mb_strimwidth($m['authors'], 0, 46, '…')) ?></span><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if (empty($grouped)): ?>
|
||||
<div class="legal-page"><p>Không tìm thấy sách phù hợp với bộ lọc. <a href="/">Xóa bộ lọc</a>.</p></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($totalPage > 1): ?>
|
||||
<div class="pagination">
|
||||
<?php
|
||||
$qs = fn($p) => '/?' . esc(http_build_query(array_merge($_GET, ['trang' => $p])));
|
||||
?>
|
||||
<?php if ($page > 1): ?><a href="<?= $qs($page - 1) ?>">‹ Trước</a><?php else: ?><span class="disabled">‹ Trước</span><?php endif; ?>
|
||||
|
||||
<?php
|
||||
$start = max(1, $page - 2);
|
||||
$end = min($totalPage, $start + 4);
|
||||
$start = max(1, $end - 4);
|
||||
for ($p = $start; $p <= $end; $p++):
|
||||
?>
|
||||
<?php if ($p === $page): ?><span class="active"><?= $p ?></span>
|
||||
<?php else: ?><a href="<?= $qs($p) ?>"><?= $p ?></a><?php endif; ?>
|
||||
<?php endfor; ?>
|
||||
|
||||
<?php if ($page < $totalPage): ?><a href="<?= $qs($page + 1) ?>">Sau ›</a><?php else: ?><span class="disabled">Sau ›</span><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Đọc online — <?= esc($book['title']) ?></title>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/assets/app.css">
|
||||
</head>
|
||||
<body class="reader-body">
|
||||
<header class="reader-header">
|
||||
<a class="reader-back" href="/sach/<?= esc($book['id']) ?>" title="Về trang sách">←</a>
|
||||
<div class="reader-title">
|
||||
<strong><?= esc($book['title']) ?></strong>
|
||||
<span><?= $book['grade'] ? 'Lớp ' . esc($book['grade']) . ' · ' : '' ?><?= esc($book['subject']) ?> · <?= esc($book['publisher']) ?></span>
|
||||
</div>
|
||||
<button class="btn btn-green reader-dl" id="dl-btn" data-id="<?= esc($book['id']) ?>">⬇ Tải PDF</button>
|
||||
</header>
|
||||
<main class="reader-main">
|
||||
<div class="viewer-frame reader-frame" id="pdfViewer" data-src="/media/pdf/<?= esc($book['id']) ?>"></div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var el = document.getElementById('pdfViewer');
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.style.width = '100%';
|
||||
iframe.style.height = '100%';
|
||||
iframe.style.border = '0';
|
||||
iframe.title = 'Trình đọc PDF';
|
||||
iframe.src = '/assets/pdfjs/web/viewer.html?file=' + encodeURIComponent(el.dataset.src);
|
||||
el.appendChild(iframe);
|
||||
|
||||
var dl = document.getElementById('dl-btn');
|
||||
dl.addEventListener('click', function () {
|
||||
dl.disabled = true; dl.textContent = 'Đang tạo liên kết…';
|
||||
fetch('/pdf-token/' + dl.dataset.id, { method: 'POST', credentials: 'same-origin' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
if (d.ok) { window.location.href = d.url; dl.textContent = '⬇ Đang tải xuống…'; }
|
||||
else { dl.textContent = 'Hết phiên — tải lại trang'; }
|
||||
})
|
||||
.catch(function () { dl.textContent = 'Lỗi mạng, thử lại'; dl.disabled = false; });
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,60 @@
|
||||
<?= $this->extend('layouts/main') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?php $meta = $book['meta']; ?>
|
||||
|
||||
<div class="book-detail">
|
||||
<div>
|
||||
<div class="detail-cover">
|
||||
<img src="/media/cover/<?= esc($book['id']) ?>?v=<?= media_version() ?>" alt="Bìa sách <?= esc($book['title']) ?>"
|
||||
onerror="this.style.display='none'">
|
||||
</div>
|
||||
<div class="actions" style="flex-direction:column">
|
||||
<a class="btn btn-primary" href="/doc/<?= esc($book['id']) ?>">📖 Đọc online</a>
|
||||
<a class="btn btn-green" href="/doc/<?= esc($book['id']) ?>#tai-xuong">⬇ Tải xuống PDF</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-info">
|
||||
<h1><?= esc($book['title']) ?></h1>
|
||||
<table class="meta-table">
|
||||
<?php if ($book['grade']): ?>
|
||||
<tr><th>Lớp</th><td>Lớp <?= esc($book['grade']) ?></td></tr>
|
||||
<?php endif; ?>
|
||||
<?php if ($book['subject']): ?>
|
||||
<tr><th>Môn học</th><td><?= esc($book['subject']) ?></td></tr>
|
||||
<?php endif; ?>
|
||||
<tr><th>Nhà xuất bản / Bộ sách</th><td><?= esc($book['publisher']) ?><?= ! empty($meta['publisherFull']) ? ' — ' . esc($meta['publisherFull']) : '' ?></td></tr>
|
||||
<?php if (! empty($meta['authors'])): ?>
|
||||
<tr><th>Tác giả / Chủ biên</th><td><?= esc($meta['authors']) ?></td></tr>
|
||||
<?php endif; ?>
|
||||
<?php if (! empty($meta['pages'])): ?>
|
||||
<tr><th>Số trang</th><td><?= (int) $meta['pages'] ?> trang</td></tr>
|
||||
<?php endif; ?>
|
||||
<?php if (! empty($meta['sizeMB'])): ?>
|
||||
<tr><th>Dung lượng</th><td><?= number_format((float) $meta['sizeMB'], 2) ?> MB</td></tr>
|
||||
<?php endif; ?>
|
||||
<?php if (! empty($meta['isbn'])): ?>
|
||||
<tr><th>ISBN</th><td><?= esc($meta['isbn']) ?></td></tr>
|
||||
<?php endif; ?>
|
||||
<?php if (! empty($meta['price'])): ?>
|
||||
<tr><th>Giá bìa</th><td><?= esc($meta['price']) ?></td></tr>
|
||||
<?php endif; ?>
|
||||
<?php if (! empty($meta['pdfVersion'])): ?>
|
||||
<tr><th>Phiên bản PDF</th><td>PDF <?= esc($meta['pdfVersion']) ?></td></tr>
|
||||
<?php endif; ?>
|
||||
<?php if (! empty($meta['creationDate'])): ?>
|
||||
<tr><th>Ngày tạo tài liệu</th><td><?= esc($meta['creationDate']) ?></td></tr>
|
||||
<?php endif; ?>
|
||||
<tr><th>Lượt tải</th><td><?= number_format((int) $downloads) ?></td></tr>
|
||||
</table>
|
||||
|
||||
<div class="countdown-note">
|
||||
⚖️ <strong>Lưu ý bản quyền:</strong> Tài liệu này thuộc bản quyền của Nhà xuất bản và Bộ Giáo dục & Đào tạo.
|
||||
Nội dung chỉ được hiển thị phục vụ mục đích tra cứu, tham khảo. Vui lòng mua sách giấy chính hãng để hỗ trợ tác giả và nhà xuất bản.
|
||||
Báo cáo vi phạm <a href="/lien-he">tại đây</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
Reference in New Issue
Block a user