630 lines
19 KiB
JavaScript
630 lines
19 KiB
JavaScript
/**
|
||
* 古诗词阅读网 - 前端 JavaScript
|
||
*/
|
||
|
||
const API_BASE = window.location.origin;
|
||
|
||
// 状态
|
||
let state = {
|
||
poems: [],
|
||
currentPage: 1,
|
||
totalPages: 1,
|
||
pageSize: 20,
|
||
total: 0,
|
||
filters: {
|
||
view: 'all',
|
||
search: '',
|
||
categories: [] // 格式:[{category: 'genre', tag: '山水田园'}]
|
||
},
|
||
currentPoem: null,
|
||
categories: {}
|
||
};
|
||
|
||
// 初始化
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
await loadCategories();
|
||
await loadStats();
|
||
await loadPoems();
|
||
setupEventListeners();
|
||
setupUpload();
|
||
});
|
||
|
||
// 加载分类体系
|
||
async function loadCategories() {
|
||
try {
|
||
const res = await fetch(`${API_BASE}/api/categories`);
|
||
if (res.ok) {
|
||
state.categories = await res.json();
|
||
renderCategoryTabs();
|
||
}
|
||
} catch (err) {
|
||
console.error('加载分类失败:', err);
|
||
}
|
||
}
|
||
|
||
// 加载统计
|
||
async function loadStats() {
|
||
try {
|
||
const res = await fetch(`${API_BASE}/api/stats`);
|
||
if (res.ok) {
|
||
const stats = await res.json();
|
||
updateStats(stats);
|
||
}
|
||
} catch (err) {
|
||
console.error('加载统计失败:', err);
|
||
}
|
||
}
|
||
|
||
// 加载诗词
|
||
async function loadPoems() {
|
||
showLoading(true);
|
||
|
||
try {
|
||
const params = new URLSearchParams({
|
||
page: state.currentPage,
|
||
page_size: state.pageSize
|
||
});
|
||
|
||
// 搜索
|
||
if (state.filters.search) {
|
||
params.append('search', state.filters.search);
|
||
}
|
||
|
||
// 阅读状态
|
||
if (state.filters.view === 'read') {
|
||
params.append('is_read', 'true');
|
||
} else if (state.filters.view === 'unread') {
|
||
params.append('is_read', 'false');
|
||
}
|
||
|
||
// 多类别筛选
|
||
if (state.filters.categories.length > 0) {
|
||
const catStr = state.filters.categories
|
||
.map(c => `${c.category}:${c.tag}`)
|
||
.join(',');
|
||
params.append('categories', catStr);
|
||
}
|
||
|
||
const res = await fetch(`${API_BASE}/api/poems?${params}`);
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
state.poems = data.poems;
|
||
state.total = data.total;
|
||
state.totalPages = data.total_pages;
|
||
|
||
renderPoems();
|
||
updateResultCount();
|
||
}
|
||
} catch (err) {
|
||
console.error('加载诗词失败:', err);
|
||
showToast('加载失败', 'error');
|
||
} finally {
|
||
showLoading(false);
|
||
}
|
||
}
|
||
|
||
// 渲染分类标签页
|
||
function renderCategoryTabs() {
|
||
const container = document.getElementById('categoryTabs');
|
||
const categories = Object.entries(state.categories);
|
||
|
||
// 显示所有分类
|
||
container.innerHTML = categories.map(([key, data]) => `
|
||
<div class="category-tab" data-category="${key}">
|
||
<span class="tab-name">${data.name}</span>
|
||
</div>
|
||
`).join('');
|
||
|
||
// 点击事件
|
||
container.querySelectorAll('.category-tab').forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
const category = tab.dataset.category;
|
||
showTagsForCategory(category);
|
||
|
||
// 切换激活状态
|
||
container.querySelectorAll('.category-tab').forEach(t => t.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
});
|
||
});
|
||
|
||
// 默认显示第一个分类的标签
|
||
if (categories.length > 0) {
|
||
const firstCategory = categories[0][0];
|
||
showTagsForCategory(firstCategory);
|
||
container.querySelector('.category-tab')?.classList.add('active');
|
||
}
|
||
}
|
||
|
||
// 显示某分类的标签
|
||
function showTagsForCategory(category) {
|
||
const container = document.getElementById('tagCloud');
|
||
const data = state.categories[category];
|
||
|
||
if (!data) return;
|
||
|
||
container.innerHTML = data.tags.map(tag => `
|
||
<span class="tag-item" data-category="${category}" data-tag="${tag}">${tag}</span>
|
||
`).join('');
|
||
|
||
// 点击事件
|
||
container.querySelectorAll('.tag-item').forEach(item => {
|
||
item.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
addFilter(category, item.dataset.tag);
|
||
});
|
||
});
|
||
}
|
||
|
||
// 添加筛选条件
|
||
function addFilter(category, tag) {
|
||
// 检查是否已存在
|
||
const exists = state.filters.categories.some(
|
||
c => c.category === category && c.tag === tag
|
||
);
|
||
|
||
if (!exists) {
|
||
state.filters.categories.push({ category, tag });
|
||
renderSelectedFilters();
|
||
state.currentPage = 1;
|
||
loadPoems();
|
||
}
|
||
}
|
||
|
||
// 渲染已选筛选
|
||
function renderSelectedFilters() {
|
||
const container = document.getElementById('selectedFilters');
|
||
const btn = document.getElementById('clearFiltersBtn');
|
||
|
||
if (state.filters.categories.length === 0) {
|
||
container.innerHTML = '';
|
||
btn.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = state.filters.categories.map((f, i) => `
|
||
<span class="filter-tag">
|
||
${state.categories[f.category]?.name || f.category}: ${f.tag}
|
||
<button onclick="removeFilter(${i})">×</button>
|
||
</span>
|
||
`).join('');
|
||
|
||
btn.style.display = 'inline-block';
|
||
}
|
||
|
||
// 移除筛选
|
||
function removeFilter(index) {
|
||
state.filters.categories.splice(index, 1);
|
||
renderSelectedFilters();
|
||
state.currentPage = 1;
|
||
loadPoems();
|
||
}
|
||
|
||
// 清除所有筛选
|
||
function clearFilters() {
|
||
state.filters.categories = [];
|
||
state.filters.search = '';
|
||
state.filters.view = 'all';
|
||
document.getElementById('searchInput').value = '';
|
||
renderSelectedFilters();
|
||
|
||
// 重置导航激活状态
|
||
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
||
document.querySelector('[data-view="all"]').classList.add('active');
|
||
|
||
loadPoems();
|
||
}
|
||
|
||
// 渲染诗词列表
|
||
function renderPoems() {
|
||
const grid = document.getElementById('poemGrid');
|
||
const empty = document.getElementById('emptyState');
|
||
|
||
if (state.poems.length === 0) {
|
||
grid.innerHTML = '';
|
||
empty.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
empty.style.display = 'none';
|
||
grid.innerHTML = '';
|
||
|
||
state.poems.forEach(poem => {
|
||
const card = createPoemCard(poem);
|
||
grid.appendChild(card);
|
||
});
|
||
}
|
||
|
||
// 创建诗词卡片
|
||
function createPoemCard(poem) {
|
||
const isRead = poem.is_read || false;
|
||
const paragraphs = poem.paragraphs || [];
|
||
const excerpt = paragraphs.slice(0, 2).join('<br>');
|
||
const tags = getTopTags(poem.classifications);
|
||
|
||
const card = document.createElement('div');
|
||
card.className = `poem-card ${isRead ? 'read' : ''}`;
|
||
card.dataset.id = poem.id;
|
||
card.innerHTML = `
|
||
<div class="poem-card-header">
|
||
<h3 class="poem-title">${escapeHtml(poem.title)}</h3>
|
||
<span class="poem-author">作者:${escapeHtml(poem.author)}</span>
|
||
</div>
|
||
<div class="poem-excerpt">${excerpt || '暂无内容'}</div>
|
||
<div class="poem-tags">${tags}</div>
|
||
<div class="poem-card-footer">
|
||
<label class="read-toggle">
|
||
<input type="checkbox" ${isRead ? 'checked' : ''}
|
||
onchange="event.stopPropagation(); toggleRead('${poem.id}', this.checked)" />
|
||
<span>${isRead ? '✓ 已读' : '○ 未读'}</span>
|
||
</label>
|
||
<button class="btn btn-sm" onclick="event.stopPropagation(); showPoemDetail('${poem.id}')">详情</button>
|
||
</div>
|
||
`;
|
||
|
||
// 点击卡片显示详情(排除复选框和按钮)
|
||
card.addEventListener('click', (e) => {
|
||
if (!e.target.closest('.read-toggle') && !e.target.closest('button')) {
|
||
showPoemDetail(poem.id);
|
||
}
|
||
});
|
||
|
||
return card;
|
||
}
|
||
|
||
// 获取主要标签
|
||
function getTopTags(classifications) {
|
||
if (!classifications) return '';
|
||
|
||
// 显示所有分类标签
|
||
const priority = ['genre', 'emotion_tone', 'season', 'location', 'time_of_day',
|
||
'philosophy', 'nature_scenery', 'plants', 'animals'];
|
||
const tags = [];
|
||
const shown = new Set();
|
||
|
||
for (const cat of priority) {
|
||
if (classifications[cat] && classifications[cat].length > 0) {
|
||
// 每个分类最多显示 2 个标签
|
||
classifications[cat].slice(0, 2).forEach(tag => {
|
||
if (!shown.has(tag)) {
|
||
tags.push(`<span class="tag tag-${cat}">${tag}</span>`);
|
||
shown.add(tag);
|
||
}
|
||
});
|
||
if (tags.length >= 6) break;
|
||
}
|
||
}
|
||
|
||
return tags.join('');
|
||
}
|
||
|
||
// 显示诗词详情
|
||
async function showPoemDetail(poemId) {
|
||
try {
|
||
const res = await fetch(`${API_BASE}/api/poems/${poemId}`);
|
||
if (!res.ok) {
|
||
showToast('加载失败', 'error');
|
||
return;
|
||
}
|
||
|
||
const poem = await res.json();
|
||
state.currentPoem = poem;
|
||
|
||
// 填充弹窗
|
||
document.getElementById('modalTitle').textContent = poem.title;
|
||
document.getElementById('modalAuthor').textContent = poem.author;
|
||
document.getElementById('modalContent').innerHTML = poem.paragraphs
|
||
.map(p => `<p>${escapeHtml(p)}</p>`).join('');
|
||
|
||
// 渲染标签
|
||
document.getElementById('modalTags').innerHTML = renderAllTags(poem.classifications);
|
||
|
||
// 设置阅读状态
|
||
document.getElementById('modalReadCheck').checked = poem.is_read || false;
|
||
|
||
// 显示弹窗
|
||
document.getElementById('poemModal').style.display = 'flex';
|
||
document.body.style.overflow = 'hidden';
|
||
|
||
} catch (err) {
|
||
console.error('加载详情失败:', err);
|
||
showToast('加载失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 渲染所有标签
|
||
function renderAllTags(classifications) {
|
||
if (!classifications) return '';
|
||
|
||
const categoryNames = state.categories;
|
||
const categoryOrder = ['season', 'solar_terms', 'time_of_day', 'genre', 'emotion_tone',
|
||
'emotions', 'nature_scenery', 'plants', 'animals', 'buildings',
|
||
'philosophy', 'life_stage', 'social_role', 'technique', 'rhetoric',
|
||
'colors', 'sounds', 'location', 'festival'];
|
||
|
||
return categoryOrder.map(cat => {
|
||
if (!classifications[cat] || classifications[cat].length === 0) return '';
|
||
|
||
const catName = categoryNames[cat]?.name || cat;
|
||
const tags = classifications[cat];
|
||
|
||
return `
|
||
<div class="tag-group">
|
||
<span class="tag-group-label">${catName}:</span>
|
||
<div class="tag-group-tags">
|
||
${tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).filter(html => html.trim() !== '').join('');
|
||
}
|
||
|
||
// 切换弹窗阅读状态
|
||
async function toggleModalRead() {
|
||
if (!state.currentPoem) return;
|
||
|
||
const isRead = document.getElementById('modalReadCheck').checked;
|
||
|
||
try {
|
||
const res = await fetch(
|
||
`${API_BASE}/api/poems/${state.currentPoem.id}/read?is_read=${isRead}`,
|
||
{ method: 'PUT' }
|
||
);
|
||
|
||
if (res.ok) {
|
||
state.currentPoem.is_read = isRead;
|
||
await loadStats();
|
||
await loadPoems();
|
||
}
|
||
} catch (err) {
|
||
console.error('更新状态失败:', err);
|
||
}
|
||
}
|
||
|
||
// 关闭弹窗
|
||
function closeModal() {
|
||
document.getElementById('poemModal').style.display = 'none';
|
||
document.body.style.overflow = '';
|
||
state.currentPoem = null;
|
||
}
|
||
|
||
// 切换阅读状态
|
||
async function toggleRead(poemId, isRead) {
|
||
try {
|
||
const res = await fetch(
|
||
`${API_BASE}/api/poems/${poemId}/read?is_read=${isRead}`,
|
||
{ method: 'PUT' }
|
||
);
|
||
|
||
if (res.ok) {
|
||
await loadStats();
|
||
await loadPoems();
|
||
showToast(isRead ? '已标记为已读' : '已标记为未读', 'success');
|
||
}
|
||
} catch (err) {
|
||
console.error('更新失败:', err);
|
||
showToast('更新失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 随机一首
|
||
async function showRandomPoem() {
|
||
try {
|
||
const res = await fetch(`${API_BASE}/api/poems/random`);
|
||
if (res.ok) {
|
||
const poem = await res.json();
|
||
state.currentPoem = poem;
|
||
|
||
document.getElementById('modalTitle').textContent = poem.title;
|
||
document.getElementById('modalAuthor').textContent = poem.author;
|
||
document.getElementById('modalContent').innerHTML = poem.paragraphs
|
||
.map(p => `<p>${escapeHtml(p)}</p>`).join('');
|
||
document.getElementById('modalTags').innerHTML = renderAllTags(poem.classifications);
|
||
document.getElementById('modalReadCheck').checked = poem.is_read || false;
|
||
|
||
document.getElementById('poemModal').style.display = 'flex';
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
} catch (err) {
|
||
showToast('获取失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 上传文件
|
||
async function uploadFile() {
|
||
const fileInput = document.getElementById('fileInput');
|
||
const file = fileInput.files[0];
|
||
|
||
if (!file) {
|
||
showToast('请选择文件', 'error');
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
const status = document.getElementById('uploadStatus');
|
||
status.textContent = '上传中...';
|
||
status.className = 'status-msg';
|
||
|
||
try {
|
||
const res = await fetch(`${API_BASE}/api/poems/import`, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const data = await res.json();
|
||
|
||
if (res.ok) {
|
||
status.textContent = `成功导入 ${data.imported} 首,跳过 ${data.skipped} 首`;
|
||
status.className = 'status-msg success';
|
||
fileInput.value = '';
|
||
await loadStats();
|
||
await loadPoems();
|
||
} else {
|
||
status.textContent = data.detail || '导入失败';
|
||
status.className = 'status-msg error';
|
||
}
|
||
} catch (err) {
|
||
status.textContent = '上传失败:' + err.message;
|
||
status.className = 'status-msg error';
|
||
}
|
||
}
|
||
|
||
// 更新统计显示
|
||
function updateStats(stats) {
|
||
document.getElementById('totalPoems').textContent = stats.total_poems;
|
||
document.getElementById('readCount').textContent = stats.read_count;
|
||
document.getElementById('unreadCount').textContent = stats.unread_count;
|
||
|
||
const percent = stats.reading_progress || 0;
|
||
document.getElementById('progressPercent').textContent = percent;
|
||
document.getElementById('progressText').textContent = `${stats.read_count}/${stats.total_poems}`;
|
||
document.querySelector('.progress-fill').style.width = `${percent}%`;
|
||
}
|
||
|
||
// 更新结果数量
|
||
function updateResultCount() {
|
||
document.getElementById('resultCount').textContent = `${state.total} 首`;
|
||
updatePagination();
|
||
}
|
||
|
||
// 更新分页控件
|
||
function updatePagination() {
|
||
const pagination = document.getElementById('pagination');
|
||
const prevBtn = document.getElementById('prevPage');
|
||
const nextBtn = document.getElementById('nextPage');
|
||
const pageInfo = document.getElementById('pageInfo');
|
||
|
||
if (state.totalPages <= 1) {
|
||
pagination.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
pagination.style.display = 'flex';
|
||
prevBtn.disabled = state.currentPage <= 1;
|
||
nextBtn.disabled = state.currentPage >= state.totalPages;
|
||
prevBtn.style.opacity = prevBtn.disabled ? '0.5' : '1';
|
||
nextBtn.style.opacity = nextBtn.disabled ? '0.5' : '1';
|
||
pageInfo.textContent = `第 ${state.currentPage} / ${state.totalPages} 页,共 ${state.total} 首`;
|
||
}
|
||
|
||
// 跳转到指定页
|
||
function goToPage(page) {
|
||
if (page < 1 || page > state.totalPages) return;
|
||
state.currentPage = page;
|
||
loadPoems();
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
}
|
||
|
||
// 设置事件监听
|
||
function setupEventListeners() {
|
||
// 导航
|
||
document.querySelectorAll('.nav-item').forEach(item => {
|
||
item.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
|
||
const view = item.dataset.view;
|
||
const action = item.dataset.action;
|
||
|
||
if (action === 'random') {
|
||
showRandomPoem();
|
||
return;
|
||
}
|
||
|
||
if (view) {
|
||
state.filters.view = view;
|
||
state.currentPage = 1;
|
||
|
||
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
||
item.classList.add('active');
|
||
|
||
loadPoems();
|
||
}
|
||
});
|
||
});
|
||
|
||
// 搜索
|
||
let searchTimeout;
|
||
document.getElementById('searchInput').addEventListener('input', (e) => {
|
||
clearTimeout(searchTimeout);
|
||
searchTimeout = setTimeout(() => {
|
||
state.filters.search = e.target.value.trim();
|
||
state.currentPage = 1;
|
||
loadPoems();
|
||
}, 500);
|
||
});
|
||
}
|
||
|
||
// 设置上传区域
|
||
function setupUpload() {
|
||
const area = document.getElementById('uploadArea');
|
||
const fileInput = document.getElementById('fileInput');
|
||
const uploadStatus = document.getElementById('uploadStatus');
|
||
|
||
area.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
area.classList.add('dragover');
|
||
});
|
||
|
||
area.addEventListener('dragleave', () => {
|
||
area.classList.remove('dragover');
|
||
});
|
||
|
||
area.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
area.classList.remove('dragover');
|
||
|
||
const files = e.dataTransfer.files;
|
||
if (files.length > 0) {
|
||
fileInput.files = files;
|
||
// 触发 change 事件以更新文件名显示
|
||
fileInput.dispatchEvent(new Event('change'));
|
||
}
|
||
});
|
||
|
||
// 文件选择后显示文件名
|
||
fileInput.addEventListener('change', () => {
|
||
const file = fileInput.files[0];
|
||
if (file) {
|
||
uploadStatus.textContent = `已选择:${file.name}`;
|
||
uploadStatus.className = 'status-msg info';
|
||
} else {
|
||
uploadStatus.textContent = '';
|
||
uploadStatus.className = 'status-msg';
|
||
}
|
||
});
|
||
}
|
||
|
||
// 工具函数
|
||
function showLoading(show) {
|
||
document.getElementById('loading').style.display = show ? 'flex' : 'none';
|
||
}
|
||
|
||
function showToast(message, type = 'info') {
|
||
const toast = document.getElementById('toast');
|
||
const msg = document.getElementById('toastMessage');
|
||
|
||
msg.textContent = message;
|
||
toast.style.display = 'block';
|
||
toast.className = `toast toast-${type}`;
|
||
|
||
setTimeout(() => {
|
||
toast.style.display = 'none';
|
||
}, 3000);
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
if (!text) return '';
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// 导出全局函数
|
||
window.toggleRead = toggleRead;
|
||
window.showPoemDetail = showPoemDetail;
|
||
window.closeModal = closeModal;
|
||
window.uploadFile = uploadFile;
|
||
window.clearFilters = clearFilters;
|
||
window.removeFilter = removeFilter;
|
||
window.goToPage = goToPage; |