所有功能已完成,运行OK
This commit is contained in:
630
frontend/script.js
Normal file
630
frontend/script.js
Normal 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;
|
||||
Reference in New Issue
Block a user