Files
PoemClassify/frontend/script.js
2026-03-23 22:31:48 +08:00

630 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 古诗词阅读网 - 前端 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;