所有功能已完成,运行OK

This commit is contained in:
JimmysAIPG
2026-03-23 22:31:48 +08:00
commit 44fd84d380
8 changed files with 3495 additions and 0 deletions

630
frontend/script.js Normal file
View File

@@ -0,0 +1,630 @@
/**
* 古诗词阅读网 - 前端 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;