yexingyangPDF / index.html
quejing's picture
Update index.html
2b35dc9 verified
Raw
History Blame Contribute Delete
28.4 kB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>叶兴阳双语音标有声读物 PDF 下载站</title>
<style>
:root {
--primary: #6366f1; --primary-dark: #4f46e5; --primary-light: #818cf8;
--accent: #f59e0b; --accent-light: #fbbf24;
--bg: #f8fafc; --card-bg: #ffffff;
--text: #1e293b; --text-secondary: #64748b; --text-muted: #94a3b8;
--border: #e2e8f0; --hover: #f1f5f9;
--shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
--shadow-lg: 0 10px 40px rgba(0,0,0,0.1), 0 4px 12px rgba(0,0,0,0.05);
--radius: 16px; --radius-sm: 12px; --selected: #e0e7ff;
}
*{margin:0;padding:0;box-sizing:border-box}
body{
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei','PingFang SC',sans-serif;
background:var(--bg);color:var(--text);min-height:100vh;line-height:1.6;font-size:15px;
}
/* === Navbar === */
.navbar{
background:rgba(255,255,255,0.92);
backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
border-bottom:1px solid var(--border);padding:0 2rem;
display:flex;align-items:center;height:68px;
position:sticky;top:0;z-index:100;gap:1.5rem;
}
.navbar .brand{
display:flex;align-items:center;gap:0.65rem;
font-size:1.05rem;font-weight:700;color:var(--primary-dark);
white-space:nowrap;letter-spacing:-0.01em;text-decoration:none;
}
.navbar .brand-icon{
width:38px;height:38px;border-radius:10px;
background:linear-gradient(135deg,#4f46e5,#7c3aed);
display:flex;align-items:center;justify-content:center;font-size:1.2rem;
color:white;box-shadow:0 2px 8px rgba(99,102,241,0.3);flex-shrink:0;
}
.tabs{
display:flex;gap:0.2rem;overflow-x:auto;flex:1;
-ms-overflow-style:none;scrollbar-width:none;align-items:center;
}
.tabs::-webkit-scrollbar{display:none}
.tab-btn{
text-decoration:none;padding:0.45rem 0.9rem;border-radius:10px;border:none;
background:transparent;cursor:pointer;font-size:0.82rem;
white-space:nowrap;color:var(--text-secondary);
transition:all 0.2s cubic-bezier(0.4,0,0.2,1);
font-family:inherit;font-weight:500;display:inline-flex;align-items:center;gap:0.3rem;
}
.tab-btn:hover{background:var(--hover);color:var(--primary-dark)}
.tab-btn.active{
background:linear-gradient(135deg,#4f46e5,#6366f1);
color:white;font-weight:600;box-shadow:0 2px 8px rgba(79,70,229,0.35);
}
/* === Hero === */
.hero{
background:linear-gradient(135deg,#312e81 0%,#4f46e5 30%,#6366f1 60%,#7c3aed 100%);
color:white;padding:2.75rem 1.5rem 2.25rem;text-align:center;position:relative;overflow:hidden;
}
.hero::before{
content:'';position:absolute;inset:0;
background:url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none'%3E%3Cg fill='%23ffffff' fill-opacity='0.04'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
pointer-events:none;
}
.hero h1{
font-size:2.2rem;font-weight:800;margin-bottom:0.5rem;letter-spacing:-0.5px;
position:relative;background:linear-gradient(to right,#ffffff,#e0e7ff);
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;
}
.hero .subtitle{opacity:0.88;font-size:0.95rem;position:relative;font-weight:400;margin-bottom:0.25rem}
.hero .stats-row{display:flex;justify-content:center;gap:2.5rem;margin-top:1.5rem;position:relative;flex-wrap:wrap}
.hero .stat-item{text-align:center}
.hero .stat-num{font-size:1.85rem;font-weight:800;line-height:1.1}
.hero .stat-label{font-size:0.78rem;opacity:0.72;margin-top:0.2rem;text-transform:uppercase;letter-spacing:1px}
.hero .contact-bar{
margin-top:1.4rem;position:relative;display:inline-block;
background:rgba(255,255,255,0.14);backdrop-filter:blur(8px);
padding:0.55rem 1.4rem;border-radius:28px;
font-size:0.9rem;cursor:pointer;border:1px solid rgba(255,255,255,0.2);
transition:all 0.2s;
}
.hero .contact-bar:hover{background:rgba(255,255,255,0.22)}
.hero .contact-bar strong{color:var(--accent);font-size:1.05rem;letter-spacing:0.5px}
.hero .backup-bar{
margin-top:0.85rem;position:relative;display:inline-block;
background:rgba(245,158,11,0.15);backdrop-filter:blur(6px);
padding:0.45rem 1.15rem;border-radius:20px;
font-size:0.82rem;border:1px solid rgba(245,158,11,0.25);
}
.hero .backup-bar a{color:var(--accent);font-weight:600;text-decoration:none}
.hero .backup-bar a:hover{text-decoration:underline}
/* === Content === */
.container{max-width:1200px;margin:0 auto;padding:1.25rem 1.25rem 2.5rem}
/* 分类卡片 */
.cat-card{
background:var(--card-bg);border-radius:var(--radius-sm);box-shadow:var(--shadow);
padding:1rem 1.25rem;margin-bottom:1rem;display:flex;align-items:center;
justify-content:space-between;flex-wrap:wrap;gap:0.75rem;border:1px solid var(--border);
}
.cat-card .cat-info{display:flex;align-items:center;gap:0.75rem}
.cat-card .cat-icon{
width:44px;height:44px;border-radius:12px;
background:linear-gradient(135deg,#fef3c7,#fde68a);display:flex;
align-items:center;justify-content:center;font-size:1.35rem;flex-shrink:0;
}
.cat-card h2{font-size:1.15rem;font-weight:700}
.cat-card .tags{display:flex;gap:0.4rem}
.cat-card .tag{
font-size:0.76rem;padding:0.18rem 0.6rem;border-radius:8px;
background:#fef3c7;color:#92400e;font-weight:500;
}
.cat-card .count-badge{
font-size:0.82rem;padding:0.35rem 0.95rem;border-radius:20px;
background:linear-gradient(135deg,var(--accent),#d97706);color:white;font-weight:600;
box-shadow:0 2px 8px rgba(245,158,11,0.3);
}
/* 搜索 */
.search-area{
margin-bottom:0.75rem;position:relative;
}
.search-area .search-icon{position:absolute;left:0.75rem;top:50%;transform:translateY(-50%);color:var(--text-muted);font-size:1rem;pointer-events:none}
.search-area input{
width:100%;padding:0.65rem 1rem 0.65rem 2.6rem;
border:1.5px solid var(--border);border-radius:14px;font-size:0.92rem;
outline:none;background:white;transition:all 0.2s;font-family:inherit;
}
.search-area input:focus{border-color:var(--primary);box-shadow:0 0 0 3px rgba(99,102,241,0.08)}
/* 表格 */
.file-card{background:var(--card-bg);border-radius:var(--radius-sm);box-shadow:var(--shadow);overflow:hidden;border:1px solid var(--border)}
.file-table{width:100%;border-collapse:collapse;font-size:0.92rem}
.file-table th{
background:#f8fafc;padding:0.7rem 1rem;text-align:left;font-weight:600;
color:var(--text-secondary);border-bottom:2px solid var(--border);
font-size:0.78rem;text-transform:uppercase;letter-spacing:0.5px;
}
.file-table td{padding:0.6rem 1rem;border-bottom:1px solid #f1f5f9;vertical-align:middle}
.file-table tbody tr{transition:all 0.15s}
.file-table tbody tr:hover{background:var(--hover)}
.file-table .file-link{color:var(--primary-dark);text-decoration:none;font-weight:500;transition:color 0.15s;word-break:break-all;font-size:0.92rem}
.file-table .file-link:hover{color:var(--primary);text-decoration:underline}
.file-table .file-link:visited{color:#7c3aed}
/* 操作按钮组 */
.actions{display:flex;gap:0.35rem;align-items:center;flex-wrap:nowrap;justify-content:flex-end}
.action-btn{
display:inline-flex;align-items:center;gap:0.2rem;padding:0.32rem 0.7rem;
border-radius:8px;border:1px solid var(--border);background:white;
cursor:pointer;font-size:0.78rem;transition:all 0.15s;font-family:inherit;
font-weight:500;white-space:nowrap;text-decoration:none;color:var(--text-secondary);
}
.action-btn:hover{transform:translateY(-1px);box-shadow:0 1px 3px rgba(0,0,0,0.06)}
.action-btn.btn-dl:hover{background:#eff6ff;color:#2563eb;border-color:#bfdbfe}
.action-btn.btn-view:hover{background:#f0fdf4;color:#16a34a;border-color:#bbf7d0}
.action-btn.btn-src:hover{background:#fffbeb;color:#d97706;border-color:#fde68a}
/* 分页 */
.pager-wrap{
display:flex;align-items:center;justify-content:center;gap:0.3rem;
padding:0.85rem;border-top:1px solid var(--border);flex-wrap:wrap;
}
.page-btn{
min-width:38px;height:38px;padding:0 0.6rem;border:1px solid var(--border);
border-radius:10px;background:white;cursor:pointer;font-size:0.84rem;
transition:all 0.15s;font-family:inherit;font-weight:500;
display:inline-flex;align-items:center;justify-content:center;
}
.page-btn:hover:not(:disabled):not(.active){background:var(--primary-light);color:white;border-color:var(--primary-light)}
.page-btn:disabled{opacity:0.25;cursor:not-allowed}
.page-btn.active{background:var(--primary);color:white;border-color:var(--primary);box-shadow:0 2px 8px rgba(99,102,241,0.3)}
.page-btn.arrow{font-size:0.95rem}
.page-btn.ellipsis{pointer-events:none;border:none;background:transparent}
.page-info{font-size:0.82rem;color:var(--text-muted);padding:0 0.6rem}
.count-info{text-align:center;padding:0.6rem 1rem;font-size:0.83rem;color:var(--text-muted)}
/* 加载 */
.loading-wrap{text-align:center;padding:4rem 2rem;color:var(--text-secondary)}
.spinner{width:36px;height:36px;border:3px solid var(--border);border-top-color:var(--primary);border-radius:50%;animation:spin 0.7s linear infinite;margin:0 auto 1rem}
@keyframes spin{to{transform:rotate(360deg)}}
.empty-state{text-align:center;padding:3rem 2rem;color:var(--text-muted)}
.empty-state .empty-icon{font-size:2.5rem;margin-bottom:0.5rem;display:block}
/* 页脚 */
.footer{text-align:center;padding:2rem;color:var(--text-muted);font-size:0.83rem;border-top:1px solid var(--border);margin-top:2rem}
.footer .brand-name{font-weight:700;color:var(--text-secondary);font-size:0.92rem;margin-bottom:0.35rem}
.footer a{color:var(--primary);text-decoration:none;font-weight:500}
.footer a:hover{text-decoration:underline}
/* 响应式 */
@media(max-width:768px){
.navbar{padding:0 0.75rem;height:56px}
.navbar .brand{font-size:0.9rem}
.navbar .brand-icon{width:30px;height:30px;border-radius:8px;font-size:1rem}
.hero{padding:1.75rem 1rem}
.hero h1{font-size:1.35rem}
.hero .stats-row{gap:1rem}
.hero .stat-num{font-size:1.35rem}
.tab-btn{padding:0.35rem 0.6rem;font-size:0.73rem}
.container{padding:0.75rem}
.file-table{font-size:0.8rem}
.file-table td,.file-table th{padding:0.45rem 0.6rem}
.actions{flex-direction:column;align-items:flex-start}
.action-btn{width:100%;justify-content:center}
}
</style>
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar">
<a class="brand" href="/">
<span class="brand-icon">&#128218;</span>
<span>叶兴阳有声读物</span>
</a>
<div class="tabs" id="tabsContainer"></div>
</nav>
<!-- Hero -->
<div class="hero">
<h1>叶兴阳双语音标有声读物 PDF 下载站</h1>
<p class="subtitle">美式排版 &middot; 英式排版 &mdash; 长篇名著 &middot; 短篇精选 &middot; 杂书经典 &middot; 古典中文</p>
<div class="stats-row">
<div class="stat-item"><div class="stat-num" id="statSources">-</div><div class="stat-label">分类</div></div>
<div class="stat-item"><div class="stat-num" id="statTotal">-</div><div class="stat-label">PDF 文件</div></div>
<div class="stat-item"><div class="stat-num" id="statCurrent">-</div><div class="stat-label">当前分类</div></div>
</div>
<div class="contact-bar" onclick="copyQQ()">
&#128172; 如需定制书籍,请联系 QQ:<strong>704100972</strong>
</div>
<div class="backup-bar">
&#128161; 阅读或下载遇到问题?请访问 <a href="https://huggingface.co/quejing" target="_blank" rel="noopener">HuggingFace / quejing</a> 直接下载
</div>
</div>
<div class="container">
<!-- 分类卡片 -->
<div class="cat-card" id="catCard">
<div class="cat-info">
<div class="cat-icon" id="catIcon">&#128220;</div>
<div>
<h2 id="catTitle">加载中...</h2>
<div class="tags" id="catTags"></div>
</div>
</div>
<span class="count-badge" id="catCount"></span>
</div>
<!-- 搜索 -->
<div class="search-area">
<span class="search-icon">&#128269;</span>
<input type="text" id="searchInput" placeholder="输入文件名关键词搜索..." oninput="onSearch(this.value)">
</div>
<!-- 文件表格 -->
<div class="file-card" id="fileContainer">
<div class="loading-wrap"><div class="spinner"></div><p>正在加载数据...</p></div>
</div>
</div>
<!-- 页脚 -->
<div class="footer">
<p class="brand-name">叶兴阳双语音标有声读物</p>
<p>数据来源于 <a href="https://huggingface.co/quejing" target="_blank" rel="noopener">HuggingFace / quejing</a> &middot; 本站仅提供索引链接,版权归原作者所有</p>
</div>
<script>
// ==================== 全局状态 ====================
const DATA_DIR = 'data/';
const PAGE_SIZE = 50;
let indexData = null;
let currentIdx = 0;
let currentPage = 1;
let loadedChunks = {};
let chunkInFlight = {};
const CAT_ICONS = {
'长篇·美式': '&#128214;', '长篇·英式': '&#128215;',
'短篇·美式': '&#128216;', '短篇·英式': '&#128217;',
'杂书名著·美式': '&#128218;', '杂书名著·英式': '&#128219;',
'古典中文·美式': '&#127983;', '古典中文·英式': '&#127983;',
};
const TAG_MAP = {
normal:'普通', zashu:'杂书名著', zhongwen:'古典中文',
meishi:'美式', yingshi:'英式', long:'长篇', short:'短篇'
};
// ==================== 路由解析 ====================
function parseRoute() {
const params = new URLSearchParams(window.location.search);
const page = parseInt(params.get('page')) || 1;
// 优先从 pathname 解析: /category/<id>
const pathMatch = window.location.pathname.match(/^\/category\/(.+)$/);
if (pathMatch) return { type: 'path', value: pathMatch[1], page: page };
// 回退 hash: #ES6%code
const hash = window.location.hash.slice(1);
if (hash) return { type: 'hash', value: decodeURIComponent(hash), page: page };
return null;
}
function updateUrl(catId, catName, page) {
// 同时更新 path 和 hash,保持兼容
const pathUrl = '/category/' + catId;
const query = (page && page > 1) ? '?page=' + page : '';
const hashUrl = '#' + encodeURIComponent(catName);
const fullUrl = pathUrl + query + hashUrl;
if (window.location.pathname !== pathUrl || window.location.search !== query) {
history.pushState(null, '', fullUrl);
} else {
history.replaceState(null, '', fullUrl);
}
}
// ==================== 初始化 ====================
document.addEventListener('DOMContentLoaded', async () => {
// 改用 API 接口加载索引
try {
const resp = await fetch('/api/sources');
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
indexData = await resp.json();
} catch (err) {
document.getElementById('fileContainer').innerHTML =
`<div class="empty-state"><span class="empty-icon">&#9888;&#65039;</span>索引加载失败: ${err.message}<br><small>请确认数据已生成</small></div>`;
return;
}
const totalPdfs = indexData.sources.reduce((s, d) => s + d.total, 0);
document.getElementById('statSources').textContent = indexData.sources.length;
document.getElementById('statTotal').textContent = totalPdfs.toLocaleString();
if (indexData.sources.length > 0) {
document.getElementById('statCurrent').textContent = indexData.sources[0].total.toLocaleString();
}
// 渲染 Tab 导航 — 使用干净 URL: /category/<id>
const tabsEl = document.getElementById('tabsContainer');
tabsEl.innerHTML = indexData.sources.map((src, i) =>
`<a class="tab-btn" href="/category/${src.id}" data-idx="${i}">${escHtml(src.name)}</a>`
).join('');
// Tab 点击拦截 — SPA 路由,不刷新页面
tabsEl.addEventListener('click', e => {
const a = e.target.closest('.tab-btn');
if (!a) return;
e.preventDefault();
const idx = parseInt(a.dataset.idx);
if (!isNaN(idx) && idx !== currentIdx) switchSource(idx);
});
// 解析当前路由
const route = parseRoute();
if (route) {
let idx = -1;
if (route.type === 'path') {
idx = indexData.sources.findIndex(d => d.id === route.value);
} else {
idx = indexData.sources.findIndex(d => d.name === route.value);
}
if (idx >= 0) currentIdx = idx;
if (route.page > 1) currentPage = route.page;
}
// 监听浏览器前进/后退
window.addEventListener('popstate', () => {
const r = parseRoute();
if (r) {
let idx = -1;
if (r.type === 'path') idx = indexData.sources.findIndex(d => d.id === r.value);
else idx = indexData.sources.findIndex(d => d.name === r.value);
if (idx >= 0 && idx !== currentIdx) {
currentIdx = idx;
currentPage = r.page;
applyTab();
} else if (idx >= 0 && r.page !== currentPage) {
currentPage = r.page;
renderCurrent();
document.getElementById('catCard')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
});
// 旧版 hash 监听(兼容直接修改 hash 的场景)
window.addEventListener('hashchange', () => {
const h = decodeURIComponent(window.location.hash.slice(1));
const idx = indexData.sources.findIndex(d => d.name === h);
if (idx >= 0 && idx !== currentIdx) { currentIdx = idx; applyTab(); }
});
applyTab();
});
function applyTab() {
document.querySelectorAll('.tab-btn').forEach((b, i) => b.classList.toggle('active', i === currentIdx));
currentPage = 1;
const inp = document.getElementById('searchInput');
if (inp) inp.value = '';
renderCurrent();
}
function switchSource(idx) {
if (!indexData || idx === currentIdx) return;
currentIdx = idx;
currentPage = 1;
const inp = document.getElementById('searchInput');
if (inp) inp.value = '';
const src = indexData.sources[idx];
updateUrl(src.id, src.name);
applyTab();
}
// ==================== 获取数据 ====================
function getSourceId() { return indexData.sources[currentIdx].id; }
async function getFullFileList() {
const src = indexData.sources[currentIdx];
const sid = src.id;
// 优先使用 API 一次性获取
const searchTerm = (document.getElementById('searchInput')?.value || '').toLowerCase().trim();
let url;
if (searchTerm) {
url = '/api/search/' + sid + '?q=' + encodeURIComponent(searchTerm);
} else if (!loadedChunks[sid]) {
url = '/api/files/' + sid;
} else {
// 已缓存,直接使用
const allFiles = [];
for (let ci = 0; ci < src.chunks.length; ci++) {
if (loadedChunks[sid][ci]) allFiles.push(...loadedChunks[sid][ci]);
}
return allFiles;
}
if (chunkInFlight[sid]) {
// 正在加载中,返回已缓存数据
const allFiles = [];
for (let ci = 0; ci < src.chunks.length; ci++) {
if (loadedChunks[sid][ci]) allFiles.push(...loadedChunks[sid][ci]);
}
return allFiles;
}
chunkInFlight[sid] = true;
try {
const resp = await fetch(url);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const files = await resp.json();
// 缓存结果(非搜索时)
if (!searchTerm) {
if (!loadedChunks[sid]) loadedChunks[sid] = {};
loadedChunks[sid][0] = files; // 全量缓存在 chunk 0
}
return files;
} catch (err) {
console.error('加载数据失败:', err);
// 回退到分片加载
return fallbackChunkLoad(src);
} finally {
delete chunkInFlight[sid];
}
}
async function fallbackChunkLoad(src) {
const sid = src.id;
if (!loadedChunks[sid]) loadedChunks[sid] = {};
const missingChunks = [];
for (let ci = 0; ci < src.chunks.length; ci++) {
if (!loadedChunks[sid][ci]) missingChunks.push(ci);
}
if (missingChunks.length > 0) {
const promises = missingChunks.map(async ci => {
if (chunkInFlight[sid + '_' + ci]) return;
chunkInFlight[sid + '_' + ci] = true;
try {
const resp = await fetch(DATA_DIR + src.chunks[ci].f);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
loadedChunks[sid][ci] = await resp.json();
} catch (err) {
console.error('加载分片失败:', err);
loadedChunks[sid][ci] = [];
} finally { delete chunkInFlight[sid + '_' + ci]; }
});
await Promise.all(promises);
}
const allFiles = [];
for (let ci = 0; ci < src.chunks.length; ci++) {
if (loadedChunks[sid][ci]) allFiles.push(...loadedChunks[sid][ci]);
}
return allFiles;
}
// ==================== 搜索 ====================
function onSearch(val) {
clearTimeout(onSearch._t);
stateSearchQ = val.trim().toLowerCase();
onSearch._t = setTimeout(() => { currentPage = 1; renderCurrent(); }, 250);
}
// ==================== 渲染 ====================
let lastRenderData = []; // 缓存当前渲染的文件列表,避免重复请求
async function renderCurrent() {
const src = indexData.sources[currentIdx];
// 更新分类卡片
document.getElementById('catIcon').innerHTML = CAT_ICONS[src.name] || '&#128220;';
document.getElementById('catTitle').textContent = src.name;
document.getElementById('catCount').innerHTML = '<strong>' + src.total.toLocaleString() + '</strong> 个 PDF';
document.getElementById('statCurrent').textContent = src.total.toLocaleString();
document.getElementById('catTags').innerHTML = [
`<span class="tag">${TAG_MAP[src.genre] || src.genre}</span>`,
`<span class="tag">${TAG_MAP[src.layout] || src.layout}</span>`,
`<span class="tag">${TAG_MAP[src.length] || src.length}</span>`,
].join('');
const container = document.getElementById('fileContainer');
container.innerHTML = '<div class="loading-wrap"><div class="spinner"></div><p>正在加载数据...</p></div>';
try {
const allFiles = await getFullFileList();
lastRenderData = allFiles; // 缓存
// API 搜索模式返回的是已过滤结果,不再二次过滤
const searchTerm = (document.getElementById('searchInput')?.value || '').toLowerCase().trim();
let filteredFiles;
if (searchTerm && !chunkInFlight[src.id]) {
// 缓存命中的情况下,需要客户端过滤
filteredFiles = allFiles.filter(f => f[0].toLowerCase().includes(searchTerm));
} else {
filteredFiles = allFiles;
}
const totalPages = Math.ceil(filteredFiles.length / PAGE_SIZE) || 1;
if (currentPage > totalPages) currentPage = totalPages;
const start = (currentPage - 1) * PAGE_SIZE;
const pageFiles = filteredFiles.slice(start, start + PAGE_SIZE);
let html = '';
if (filteredFiles.length === 0) {
html = '<div class="empty-state"><span class="empty-icon">&#128196;</span>没有匹配的文件</div>';
} else {
html = `<table class="file-table">
<thead><tr><th style="width:60%">文件名</th><th style="text-align:right">操作</th></tr></thead><tbody>`;
for (let i = 0; i < pageFiles.length; i++) {
const f = pageFiles[i];
const fn = f[0], url = f[1];
const previewUrl = '/reader?url=' + encodeURIComponent(url) + '&name=' + encodeURIComponent(fn);
html += `<tr>
<td><a class="file-link" href="${escUrl(url)}" target="_blank" rel="noopener" title="${escHtml(fn)}">${escHtml(fn)}</a></td>
<td><div class="actions">
<a class="action-btn btn-dl" href="${escUrl(url)}" target="_blank" rel="noopener" title="直接下载">&#11015; 下载</a>
<a class="action-btn btn-view" href="${escUrl(previewUrl)}" target="_blank" rel="noopener" title="在线预览">&#128214; 预览</a>
<a class="action-btn btn-src" href="${escUrl(url)}" target="_blank" rel="noopener" title="查看原地址">&#128279; 原地址</a>
</div></td>
</tr>`;
}
html += '</tbody></table>';
// 分页
html += '<div class="count-info">共 <strong>' + filteredFiles.length.toLocaleString() + '</strong> 个文件</div>';
html += '<div class="pager-wrap">';
html += `<button class="page-btn arrow" onclick="goPage(${currentPage - 1})" ${currentPage <= 1 ? 'disabled' : ''}>&larr;</button>`;
const range = 2;
let s = Math.max(1, currentPage - range);
let e = Math.min(totalPages, s + range * 2);
if (s > 1) {
html += `<button class="page-btn num-btn" onclick="goPage(1)">1</button>`;
if (s > 2) html += '<span class="page-btn ellipsis">&hellip;</span>';
}
for (let p = s; p <= e; p++) {
html += `<button class="page-btn num-btn ${p === currentPage ? 'active' : ''}" onclick="goPage(${p})">${p}</button>`;
}
if (e < totalPages) {
if (e < totalPages - 1) html += '<span class="page-btn ellipsis">&hellip;</span>';
html += `<button class="page-btn num-btn" onclick="goPage(${totalPages})">${totalPages}</button>`;
}
html += `<button class="page-btn arrow" onclick="goPage(${currentPage + 1})" ${currentPage >= totalPages ? 'disabled' : ''}>&rarr;</button>`;
html += `<span class="page-info">${currentPage} / ${totalPages}</span>`;
html += '</div>';
}
container.innerHTML = html;
} catch (err) {
container.innerHTML = `<div class="empty-state"><span class="empty-icon">&#9888;&#65039;</span>数据加载失败: ${err.message}</div>`;
}
}
function goPage(page) {
if (!indexData || !lastRenderData.length) return;
const searchTerm = (document.getElementById('searchInput')?.value || '').toLowerCase().trim();
const filtered = searchTerm
? lastRenderData.filter(f => f[0].toLowerCase().includes(searchTerm))
: lastRenderData;
const total = Math.ceil(filtered.length / PAGE_SIZE) || 1;
if (page < 1 || page > total) return;
currentPage = page;
const src = indexData.sources[currentIdx];
updateUrl(src.id, src.name, page);
renderCurrent();
const el = document.getElementById('catCard');
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
let stateSearchQ = '';
// ==================== 工具 ====================
function copyQQ() {
navigator.clipboard.writeText('704100972').then(() => {
alert('QQ号 704100972 已复制到剪贴板');
}).catch(() => {
prompt('QQ号:704100972(请手动复制)', '704100972');
});
}
function escHtml(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
function escUrl(s) { return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
function escAttr(s) { return escUrl(s).replace(/'/g, "&#39;"); }
</script>
</body>
</html>