所有功能已完成,运行OK
This commit is contained in:
193
frontend/index.html
Normal file
193
frontend/index.html
Normal file
@@ -0,0 +1,193 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>古诗词阅读网</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- 顶部导航 -->
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<div class="brand">
|
||||
<span class="brand-icon">詩</span>
|
||||
<div>
|
||||
<h1>古诗词阅读网</h1>
|
||||
<p class="tagline">品读经典 · 感悟人生</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="nav">
|
||||
<a href="#" class="nav-item active" data-view="all">全部诗词</a>
|
||||
<a href="#" class="nav-item" data-view="unread">未读</a>
|
||||
<a href="#" class="nav-item" data-view="read">已读</a>
|
||||
<a href="#" class="nav-item" data-action="random">随机一首</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主容器 -->
|
||||
<main class="main">
|
||||
<!-- 侧边栏 -->
|
||||
<aside class="sidebar">
|
||||
<!-- 统计面板 -->
|
||||
<div class="card stats-card">
|
||||
<h3 class="card-title">阅读统计</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="totalPoems">0</span>
|
||||
<span class="stat-label">总数</span>
|
||||
</div>
|
||||
<div class="stat-item read">
|
||||
<span class="stat-value" id="readCount">0</span>
|
||||
<span class="stat-label">已读</span>
|
||||
</div>
|
||||
<div class="stat-item unread">
|
||||
<span class="stat-value" id="unreadCount">0</span>
|
||||
<span class="stat-label">未读</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="progress-text">
|
||||
<span>进度:<span id="progressPercent">0</span>%</span>
|
||||
<span><span id="progressText">0/0</span>首</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传面板 -->
|
||||
<div class="card upload-card">
|
||||
<h3 class="card-title">导入诗词</h3>
|
||||
<div class="upload-area" id="uploadArea">
|
||||
<input type="file" id="fileInput" accept=".json,.jsonl" />
|
||||
<label for="fileInput" class="upload-label">
|
||||
<svg class="upload-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="17 8 12 3 7 8"/>
|
||||
<line x1="12" y1="3" x2="12" y2="15"/>
|
||||
</svg>
|
||||
<span class="upload-text">点击或拖拽上传</span>
|
||||
<span class="upload-hint">支持 JSON / JSONL 格式</span>
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block" onclick="uploadFile()">导入</button>
|
||||
<div id="uploadStatus" class="status-msg"></div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索 -->
|
||||
<div class="card search-card">
|
||||
<h3 class="card-title">搜索</h3>
|
||||
<div class="search-box">
|
||||
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
<input type="text" id="searchInput" class="search-input" placeholder="搜索标题、作者..." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类筛选 -->
|
||||
<div class="card filter-card">
|
||||
<h3 class="card-title">分类筛选</h3>
|
||||
<div id="categoryTabs" class="category-tabs"></div>
|
||||
<div id="tagCloud" class="tag-cloud"></div>
|
||||
<div class="selected-filters" id="selectedFilters"></div>
|
||||
<button class="btn btn-ghost btn-sm" onclick="clearFilters()" id="clearFiltersBtn" style="display:none">清除筛选</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 内容区 -->
|
||||
<div class="content">
|
||||
<!-- 结果头部 -->
|
||||
<div class="content-header">
|
||||
<h2 id="resultsTitle">诗词列表</h2>
|
||||
<span class="badge" id="resultCount">0 首</span>
|
||||
</div>
|
||||
|
||||
<!-- 诗词网格 -->
|
||||
<div id="poemGrid" class="poem-grid"></div>
|
||||
|
||||
<!-- 分页控件 -->
|
||||
<div id="pagination" class="pagination" style="display:none">
|
||||
<button class="page-btn" id="prevPage" onclick="goToPage(state.currentPage - 1)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="15 18 9 12 15 6"/>
|
||||
</svg>
|
||||
上一页
|
||||
</button>
|
||||
<div class="page-info">
|
||||
<span id="pageInfo">第 1 / 1 页</span>
|
||||
</div>
|
||||
<button class="page-btn" id="nextPage" onclick="goToPage(state.currentPage + 1)">
|
||||
下一页
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="9 18 15 12 9 6"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div id="emptyState" class="empty-state" style="display:none">
|
||||
<div class="empty-icon">
|
||||
<svg viewBox="0 0 120 120" fill="none">
|
||||
<circle cx="60" cy="60" r="50" fill="#f8f5f0" stroke="#d4c5a9" stroke-width="1"/>
|
||||
<path d="M40 45 L60 35 L80 45 L80 80 L40 80 Z" fill="#ede8df" stroke="#c4b5a0" stroke-width="1"/>
|
||||
<line x1="50" y1="52" x2="70" y2="52" stroke="#c4b5a0" stroke-width="1"/>
|
||||
<line x1="50" y1="60" x2="70" y2="60" stroke="#c4b5a0" stroke-width="1"/>
|
||||
<line x1="50" y1="68" x2="65" y2="68" stroke="#c4b5a0" stroke-width="1"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>暂无诗词</h3>
|
||||
<p>上传 JSON / JSONL 格式的诗词文件开始阅读</p>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div id="loading" class="loading" style="display:none">
|
||||
<div class="spinner"></div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 诗词详情弹窗 -->
|
||||
<div id="poemModal" class="modal" style="display:none">
|
||||
<div class="modal-backdrop" onclick="closeModal()"></div>
|
||||
<div class="modal-panel">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h3 id="modalTitle" class="modal-title"></h3>
|
||||
<p class="modal-author">作者:<span id="modalAuthor"></span></p>
|
||||
</div>
|
||||
<button class="modal-close" onclick="closeModal()" aria-label="关闭">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-content" id="modalContent"></div>
|
||||
<div class="modal-section">
|
||||
<h4 class="section-title">分类标签</h4>
|
||||
<div id="modalTags" class="modal-tags"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<label class="read-checkbox">
|
||||
<input type="checkbox" id="modalReadCheck" onchange="toggleModalRead()" />
|
||||
<span>标记为已读</span>
|
||||
</label>
|
||||
<button class="btn btn-primary" onclick="closeModal()">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示消息 -->
|
||||
<div id="toast" class="toast" style="display:none">
|
||||
<span id="toastMessage"></span>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
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;
|
||||
1027
frontend/style.css
Normal file
1027
frontend/style.css
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user