Creation chat
NHKNANA
请帮我做一个能记录我所阅读的书籍的程序,即可通过我录入的书名、作者、作者国籍、出版社、字数(万字)、书籍分类、阅读状态、阅读时间段、评分、笔记等方面信息,最终形成一个阅读书架。
其中,录入环节的评分模块录入者可以通过在“推理性(2.5分)”“创新性(2分)”“诡计设计(2.5分)”“动机设计(1.5分)”“故事情节及人物(1.5分)”“文笔文风(附加分,可扣)”这6块信息中输入任意数值,这6项分数最终直接加和输出总评分数,即“总评分”,“总评分”保留小数点后一位小数,并同步输出本书的等级,即“等级”。其中,“评分”≥9,则“等级”输出为S;“评分”≥8且“评分”<9,则“等级”输出为A;“评分”≥7且“评分”<8,则“等级”输出为B;“评分”≥6且“评分”<7,则“等级”输出为C;“评分”≥5且“评分”<6,则“等级”输出为D;“评分”≥4且“评分”<5,则“等级”输出为E;“评分”<4,则“等级”输出为F。
09-10 07:23
暂无回复
No comments yet
登录 后可以评论
Assistant
搜索确认了推理类书籍的评分维度背景(诡计、动机、文笔等),我将按您的需求完整实现。现在生成「阅读书架」完整HTML代码。
包含:
- 六维度评分(推理性2.5 / 创新性2 / 诡计设计2.5 / 动机设计1.5 / 情节人物1.5,文笔文风可扣附加分),实时求和输出总评分(保留1位小数)与等级(S/A/B/C/D/E/F)
- 书籍信息录入(书名、作者、国籍、出版社、字数、分类、状态、阅读时间段、笔记)
- 书架卡片展示、搜索、分类/状态筛选、统计概览
- 新增/编辑/删除,localStorage 持久化
- 黑夜/白天模式、自定义弹窗
(HTML code has been generated and updated; see above for the current version.)
### 功能说明
**📥 录入环节**:点击「添加书籍」,填写书名、作者、国籍、出版社、字数(万字)、分类、阅读状态、阅读时间段、笔记。六维评分(推理性2.5 / 创新性2 / 诡计设计2.5 / 动机设计1.5 / 情节人物1.5 / 文笔文风附加可扣)实时求和,**总评分保留1位小数**并同步显示等级徽章。
**🏆 等级规则**:≥9→S,≥8→A,≥7→B,≥6→C,≥5→D,≥4→E,<4→F。每个等级配不同配色徽章。
**📚 书架展示**:卡片式网格,显示书封面信息、分类/出版社/字数标签、状态徽章、阅读时间段、总评分与等级、笔记摘要,支持编辑/删除。
**🔍 筛选搜索**:按书名/作者/分类关键字搜索,按分类、阅读状态下拉筛选。
**📊 统计概览**:藏书总数、已读完、在读、S级佳作数量。
**💾 持久化**:所有数据存于 localStorage,刷新后自动恢复;支持黑夜/白天模式。
> 💡 提示:预览区右下角有 **Console** 面板,我在新增/编辑/删除、评分实时计算、数据加载/保存等关键节点都输出了日志。您可以把 Console 日志内容复制后作为输入发给我,便于我根据实际运行输出继续调整或排查问题。
▼ Code
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>阅读书架 · 推理评分簿</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>tailwind.config = { darkMode: 'class' }</script>
</head>
<body class="min-h-screen bg-slate-50 dark:bg-slate-900 text-slate-700 dark:text-slate-200 antialiased">
<!-- 主题切换按钮 -->
<button id="theme-toggle-btn" class="fixed top-4 right-4 z-10 p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors flex items-center justify-center" aria-label="Toggle theme" onclick="toggleTheme()">
<svg id="icon-moon" class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998z"/></svg>
<svg id="icon-sun" class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24" aria-hidden="true" style="display:none"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"></path></svg>
</button>
<script>
function toggleTheme(){ var el=document.documentElement; var isDark=!el.classList.contains('dark'); el.classList.toggle('dark',isDark); localStorage.setItem('theme',isDark?'dark':'light'); applyTheme(isDark); }
function applyTheme(isDark){ document.getElementById('icon-moon').style.display=isDark?'none':'block'; document.getElementById('icon-sun').style.display=isDark?'block':'none'; }
document.addEventListener('DOMContentLoaded',function(){ var saved=localStorage.getItem('theme'); var isDark=saved==='dark'||(!saved&&window.matchMedia('(prefers-color-scheme: dark)').matches); document.documentElement.classList.toggle('dark',isDark); applyTheme(isDark); });
</script>
<!-- 自定义弹窗 -->
<div id="custom-modal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 hidden" aria-hidden="true">
<div class="rounded-xl bg-white dark:bg-slate-800 p-6 shadow-xl max-w-sm w-full border border-slate-200 dark:border-slate-700">
<p id="custom-modal-msg" class="text-slate-700 dark:text-slate-200 mb-4"></p>
<button type="button" onclick="hideModal()" class="w-full rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600">确定</button>
</div>
</div>
<script>
function showModal(msg){ var el=document.getElementById('custom-modal'); document.getElementById('custom-modal-msg').textContent=msg||''; el.classList.remove('hidden'); }
function hideModal(){ document.getElementById('custom-modal').classList.add('hidden'); }
</script>
<header class="max-w-6xl mx-auto px-4 pt-6 pb-4">
<div class="flex items-center justify-between gap-4 pr-12">
<div>
<h1 class="text-2xl sm:text-3xl font-bold text-slate-900 dark:text-slate-100 flex items-center gap-2">
<span class="text-amber-600 dark:text-amber-400 text-3xl">📚</span> 阅读书架
</h1>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">记录推理书籍,六维评分,自动评定等级</p>
</div>
<button id="addBtn" onclick="openForm()" class="shrink-0 text-sm sm:text-base rounded-lg bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 font-medium transition-colors shadow-sm">+ 添加书籍</button>
</div>
<!-- 统计概览 -->
<div id="statsBar" class="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-6"></div>
</header>
<!-- 筛选与搜索 -->
<section class="max-w-6xl mx-auto px-4 mb-4">
<div class="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
<input id="searchInput" type="text" placeholder="搜索书名 / 作者 / 分类…" class="flex-1 rounded-lg border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-800 px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" oninput="renderBooks()">
<select id="filterCategory" onchange="renderBooks()" class="rounded-lg border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-800 px-4 py-2 text-sm">
<option value="">全部分类</option>
</select>
<select id="filterStatus" onchange="renderBooks()" class="rounded-lg border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-800 px-4 py-2 text-sm">
<option value="">全部状态</option>
<option value="在读">在读</option>
<option value="想读">想读</option>
<option value="已读完">已读完</option>
<option value="搁置">搁置</option>
<option value="弃读">弃读</option>
</select>
</div>
</section>
<!-- 书架 -->
<main class="max-w-6xl mx-auto px-4 pb-16">
<div id="bookGrid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5"></div>
<div id="emptyState" class="hidden text-center py-16 text-slate-400 dark:text-slate-500">
<p class="text-5xl mb-3">📭</p>
<p class="text-lg">书架还是空的,点击右上角「添加书籍」开始记录吧!</p>
</div>
</main>
<!-- 新增/编辑 表单弹层 -->
<div id="formOverlay" class="fixed inset-0 z-40 hidden items-center justify-center bg-black/60 p-3 sm:p-6 overflow-y-auto">
<div id="formCard" class="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl w-full max-w-2xl my-auto border border-slate-200 dark:border-slate-700">
<div class="p-5 sm:p-6">
<h2 id="formTitle" class="text-xl font-bold text-slate-900 dark:text-slate-100 mb-5 flex items-center gap-2">📖 添加书籍</h2>
<form id="bookForm" class="space-y-4" onsubmit="return handleSubmit(event)">
<input type="hidden" id="bookId">
<!-- 基本信息 -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium mb-1" for="fTitle">书名 <span class="text-rose-500">*</span></label>
<input id="fTitle" required class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium mb-1" for="fAuthor">作者 <span class="text-rose-500">*</span></label>
<input id="fAuthor" required class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium mb-1" for="fNation">作者国籍</label>
<input id="fNation" placeholder="如:日本 / 英国" class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium mb-1" for="fPublisher">出版社</label>
<input id="fPublisher" class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium mb-1" for="fWords">字数(万字)</label>
<input id="fWords" type="number" min="0" step="0.1" class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium mb-1" for="fCategory">书籍分类</label>
<input id="fCategory" list="categoryList" placeholder="如:本格推理 / 社会派 / 密室" class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
<datalist id="categoryList">
<option value="本格推理"></option>
<option value="社会派推理"></option>
<option value="新本格"></option>
<option value="密室推理"></option>
<option value="叙述性诡计"></option>
<option value="硬汉派"></option>
<option value="悬疑惊悚"></option>
<option value="其他"></option>
</datalist>
</div>
<div>
<label class="block text-sm font-medium mb-1" for="fStatus">阅读状态</label>
<select id="fStatus" class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 text-sm">
<option value="想读">想读</option>
<option value="在读">在读</option>
<option value="已读完">已读完</option>
<option value="搁置">搁置</option>
<option value="弃读">弃读</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1" for="fPeriod">阅读时间段</label>
<input id="fPeriod" placeholder="如:2024.03 - 2024.05" class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</div>
<!-- 六维评分 -->
<div class="rounded-xl border border-slate-200 dark:border-slate-600 p-4 bg-slate-50 dark:bg-slate-700/50">
<p class="text-sm font-semibold text-slate-900 dark:text-slate-100 mb-3">🎯 六维评分</p>
<div id="scoreInputs" class="grid grid-cols-2 sm:grid-cols-3 gap-3"></div>
<div class="mt-4 pt-4 border-t border-slate-200 dark:border-slate-600">
<div class="flex flex-wrap items-center gap-4">
<div>
<span class="text-sm text-slate-500 dark:text-slate-400">总评分:</span>
<span id="liveTotal" class="text-xl font-bold text-slate-900 dark:text-slate-100">0.0</span>
</div>
<div class="flex items-center gap-2">
<span class="text-sm text-slate-500 dark:text-slate-400">等级:</span>
<span id="liveGrade" class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-slate-200 dark:bg-slate-600 text-slate-700 dark:text-slate-200 font-bold">-</span>
</div>
</div>
</div>
</div>
<!-- 笔记 -->
<div>
<label class="block text-sm font-medium mb-1" for="fNotes">📝 笔记</label>
<textarea id="fNotes" rows="3" class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 resize-y"></textarea>
</div>
<div class="flex justify-end gap-3 pt-2">
<button type="button" onclick="closeForm()" class="rounded-lg border border-slate-300 dark:border-slate-600 px-4 py-2 text-sm text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">取消</button>
<button type="submit" class="rounded-lg bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-6 py-2 text-sm font-medium transition-colors">保存</button>
</div>
</form>
</div>
</div>
</div>
<script>
/* ================= 评分维度配置 ================= */
// 除「文笔文风」外均为固定上限;文笔文风为附加分(可扣,允许负值)
const SCORE_DIMS = [
{ key: 'reasoning', label: '推理性', max: 2.5, canNegative: false },
{ key: 'innovation', label: '创新性', max: 2.0, canNegative: false },
{ key: 'trick', label: '诡计设计', max: 2.5, canNegative: false },
{ key: 'motive', label: '动机设计', max: 1.5, canNegative: false },
{ key: 'plot', label: '故事情节及人物', max: 1.5, canNegative: false },
{ key: 'style', label: '文笔文风', max: 1.0, canNegative: true },
];
/* ================= 等级判定 ================= */
function gradeOf(total) {
if (total >= 9) return 'S';
if (total >= 8) return 'A';
if (total >= 7) return 'B';
if (total >= 6) return 'C';
if (total >= 5) return 'D';
if (total >= 4) return 'E';
return 'F';
}
const GRADE_COLOR = { S:'text-amber-600 dark:text-amber-400 border-amber-400', A:'text-blue-600 dark:text-blue-400 border-blue-400', B:'text-emerald-600 dark:text-emerald-400 border-emerald-400', C:'text-teal-600 dark:text-teal-400 border-teal-400', D:'text-orange-600 dark:text-orange-400 border-orange-400', E:'text-purple-600 dark:text-purple-400 border-purple-400', F:'text-rose-600 dark:text-rose-400 border-rose-400' };
/* ================= 数据存储 ================= */
let books = [];
function loadBooks() {
try { books = JSON.parse(localStorage.getItem('bookShelf') || '[]'); } catch (e) { books = []; }
console.log('已加载书籍数据,共', books.length, '本');
}
function saveBooks() { localStorage.setItem('bookShelf', JSON.stringify(books)); console.log('数据已保存,共', books.length, '本'); }
/* ================= 渲染评分输入 ================= */
function buildScoreInputs() {
const wrap = document.getElementById('scoreInputs');
wrap.innerHTML = '';
SCORE_DIMS.forEach(d => {
const div = document.createElement('div');
div.innerHTML = `
<label class="block text-xs font-medium mb-1 ${d.canNegative ? 'text-amber-600 dark:text-amber-400' : 'text-slate-500 dark:text-slate-400'}">
${d.label} <span class="text-slate-400">(${d.max}分${d.canNegative ? '·可扣' : ''})</span>
</label>
<input type="number" step="0.1" ${d.canNegative ? 'min="-2" max="1"' : `min="0" max="${d.max}"`} value="0"
class="score-input w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
data-key="${d.key}" oninput="updateLiveScore()">`;
wrap.appendChild(div);
});
}
/* ================= 实时总分与等级 ================= */
function calcTotal() {
const inputs = document.querySelectorAll('.score-input');
let sum = 0;
inputs.forEach(inp => {
const v = parseFloat(inp.value);
if (!isNaN(v)) sum += v;
});
return Math.round(sum * 10) / 10; // 保留一位小数
}
function updateLiveScore() {
const total = calcTotal();
document.getElementById('liveTotal').textContent = total.toFixed(1);
const grade = gradeOf(total);
const el = document.getElementById('liveGrade');
el.textContent = grade;
el.className = 'inline-flex items-center justify-center w-8 h-8 rounded-full border font-bold ' + (GRADE_COLOR[grade] || '');
console.log('评分实时计算:总评', total.toFixed(1), ',等级', grade);
}
/* ================= 表单打开/关闭 ================= */
let editingId = null;
function openForm(id) {
editingId = id || null;
document.getElementById('bookForm').reset();
document.getElementById('bookId').value = '';
buildScoreInputs();
updateLiveScore();
document.getElementById('formTitle').textContent = id ? '✏️ 编辑书籍' : '📖 添加书籍';
if (id) {
const b = books.find(x => x.id === id);
if (b) fillForm(b);
}
const overlay = document.getElementById('formOverlay');
overlay.classList.remove('hidden');
overlay.classList.add('flex');
console.log(id ? '打开编辑表单,书籍ID=' + id : '打开新增表单');
}
function fillForm(b) {
document.getElementById('bookId').value = b.id;
document.getElementById('fTitle').value = b.title || '';
document.getElementById('fAuthor').value = b.author || '';
document.getElementById('fNation').value = b.nation || '';
document.getElementById('fPublisher').value = b.publisher || '';
document.getElementById('fWords').value = b.words != null ? b.words : '';
document.getElementById('fCategory').value = b.category || '';
document.getElementById('fStatus').value = b.status || '想读';
document.getElementById('fPeriod').value = b.period || '';
document.getElementById('fNotes').value = b.notes || '';
// 回填评分
const dims = b.scores || {};
document.querySelectorAll('.score-input').forEach(inp => {
const key = inp.dataset.key;
if (key in dims) inp.value = dims[key];
});
updateLiveScore();
}
function closeForm() {
const overlay = document.getElementById('formOverlay');
overlay.classList.add('hidden');
overlay.classList.remove('flex');
}
/* ================= 表单提交 ================= */
function handleSubmit(e) {
e.preventDefault();
const title = document.getElementById('fTitle').value.trim();
const author = document.getElementById('fAuthor').value.trim();
if (!title || !author) { showModal('书名和作者为必填项'); return false; }
const scores = {};
let total = 0;
document.querySelectorAll('.score-input').forEach(inp => {
let v = parseFloat(inp.value);
if (isNaN(v)) v = 0;
scores[inp.dataset.key] = v;
total += v;
});
total = Math.round(total * 10) / 10;
const id = document.getElementById('bookId').value;
const book = {
id: id || 'b' + Date.now(),
title, author,
nation: document.getElementById('fNation').value.trim(),
publisher: document.getElementById('fPublisher').value.trim(),
words: document.getElementById('fWords').value === '' ? null : parseFloat(document.getElementById('fWords').value),
category: document.getElementById('fCategory').value.trim(),
status: document.getElementById('fStatus').value,
period: document.getElementById('fPeriod').value.trim(),
notes: document.getElementById('fNotes').value.trim(),
scores, total, grade: gradeOf(total),
updatedAt: Date.now()
};
const idx = books.findIndex(x => x.id === book.id);
if (idx >= 0) { books[idx] = book; console.log('更新书籍:', book.title); }
else { books.push(book); console.log('新增书籍:', book.title); }
saveBooks();
closeForm();
buildCategoryOptions();
renderBooks();
renderStats();
return false;
}
/* ================= 删除 ================= */
function deleteBook(id) {
const b = books.find(x => x.id === id);
const msg = `确定要删除《${b ? b.title : '这本书'}》吗?此操作不可恢复。`;
// 使用自定义确认(用 showModal 展示,另加隐藏删除按钮的方案→简化为原生 confirm)
if (confirm(msg)) {
books = books.filter(x => x.id !== id);
saveBooks();
buildCategoryOptions();
renderBooks();
renderStats();
console.log('已删除书籍,ID=', id);
}
}
/* ================= 渲染书架 ================= */
function statusBadge(status) {
const map = {
'想读': 'bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300',
'在读': 'bg-blue-100 dark:bg-blue-500/20 text-blue-600 dark:text-blue-300',
'已读完': 'bg-emerald-100 dark:bg-emerald-500/20 text-emerald-600 dark:text-emerald-300',
'搁置': 'bg-amber-100 dark:bg-amber-500/20 text-amber-600 dark:text-amber-300',
'弃读': 'bg-rose-100 dark:bg-rose-500/20 text-rose-600 dark:text-rose-300'
};
return map[status] || 'bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300';
}
function renderBooks() {
const q = (document.getElementById('searchInput').value || '').trim().toLowerCase();
const cat = document.getElementById('filterCategory').value;
const st = document.getElementById('filterStatus').value;
let list = books.filter(b => {
const matchQ = !q || (b.title||'').toLowerCase().includes(q) || (b.author||'').toLowerCase().includes(q) || (b.category||'').toLowerCase().includes(q);
const matchCat = !cat || (b.category||'') === cat;
const matchSt = !st || b.status === st;
return matchQ && matchCat && matchSt;
});
// 按最近更新排序
list.sort((a, b) => (b.updatedAt||0) - (a.updatedAt||0));
const grid = document.getElementById('bookGrid');
grid.innerHTML = '';
const empty = document.getElementById('emptyState');
if (list.length === 0) {
empty.classList.remove('hidden');
grid.classList.add('hidden');
console.log('书架为空或无匹配结果');
return;
}
empty.classList.add('hidden');
grid.classList.remove('hidden');
list.forEach(b => {
const card = document.createElement('div');
card.className = 'rounded-xl bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 shadow-sm hover:shadow-md transition-shadow p-5 flex flex-col';
const gradeColor = GRADE_COLOR[b.grade] || '';
card.innerHTML = `
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<h3 class="font-bold text-slate-900 dark:text-slate-100 text-lg leading-snug truncate">${escapeHtml(b.title)}</h3>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-0.5">${escapeHtml(b.author||'')}${b.nation ? ' · '+escapeHtml(b.nation) : ''}</p>
</div>
<span class="inline-flex items-center justify-center w-9 h-9 rounded-full border-2 font-bold shrink-0 ${gradeColor}">${b.grade}</span>
</div>
<div class="mt-3 flex flex-wrap gap-2 text-xs">
<span class="inline-flex rounded-full ${statusBadge(b.status)} px-2.5 py-0.5">${escapeHtml(b.status||'')}</span>
${b.category ? `<span class="inline-flex rounded-full bg-indigo-100 dark:bg-indigo-500/20 text-indigo-600 dark:text-indigo-300 px-2.5 py-0.5">${escapeHtml(b.category)}</span>` : ''}
${b.publisher ? `<span class="inline-flex rounded-full bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 px-2.5 py-0.5">${escapeHtml(b.publisher)}</span>` : ''}
${b.words != null ? `<span class="inline-flex rounded-full bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 px-2.5 py-0.5">${b.words} 万字</span>` : ''}
</div>
${b.period ? `<p class="mt-3 text-xs text-slate-400 dark:text-slate-500">📅 ${escapeHtml(b.period)}</p>` : ''}
<div class="mt-3 pt-3 border-t border-slate-100 dark:border-slate-700 flex items-baseline gap-3">
<span class="text-2xl font-extrabold ${b.total>=7?'text-amber-600 dark:text-amber-400':'text-slate-700 dark:text-slate-200'}">${b.total != null ? b.total.toFixed(1) : '-'}</span>
<span class="text-xs text-slate-400 dark:text-slate-500">总评分 / 满分 10</span>
</div>
${b.notes ? `<p class="mt-2 text-sm text-slate-600 dark:text-slate-300 whitespace-pre-line line-clamp-3">📝 ${escapeHtml(b.notes)}</p>` : ''}
<div class="mt-4 pt-3 border-t border-slate-100 dark:border-slate-700 flex justify-end gap-2">
<button type="button" onclick="openForm('${b.id}')" class="text-xs rounded-lg border border-blue-300 dark:border-blue-500/50 text-blue-600 dark:text-blue-400 px-3 py-1 hover:bg-blue-50 dark:hover:bg-blue-500/10 transition-colors">编辑</button>
<button type="button" onclick="deleteBook('${b.id}')" class="text-xs rounded-lg border border-rose-300 dark:border-rose-500/50 text-rose-600 dark:text-rose-400 px-3 py-1 hover:bg-rose-50 dark:hover:bg-rose-500/10 transition-colors">删除</button>
</div>`;
grid.appendChild(card);
});
}
/* ================= 统计概览 ================= */
function renderStats() {
const bar = document.getElementById('statsBar');
const total = books.length;
const done = books.filter(b => b.status === '已读完').length;
const reading = books.filter(b => b.status === '在读').length;
const best = books.filter(b => b.total >= 9).length;
const cards = [
{ label: '藏书总数', value: total, icon: '📚', cls: '' },
{ label: '已读完', value: done, icon: '✅', cls: 'text-emerald-600 dark:text-emerald-400' },
{ label: '在读', value: reading, icon: '📖', cls: 'text-blue-600 dark:text-blue-400' },
{ label: 'S级佳作', value: best, icon: '🏆', cls: 'text-amber-600 dark:text-amber-400' }
];
bar.innerHTML = cards.map(c => `
<div class="rounded-xl bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 p-4">
<p class="text-xs text-slate-500 dark:text-slate-400">${c.icon} ${c.label}</p>
<p class="text-2xl font-bold text-slate-900 dark:text-slate-100 mt-1 ${c.cls}">${c.value}</p>
</div>`).join('');
}
/* ================= 分类筛选选项 ================= */
function buildCategoryOptions() {
const sel = document.getElementById('filterCategory');
const current = sel.value;
const cats = [...new Set(books.map(b => b.category).filter(Boolean))];
sel.innerHTML = '<option value="">全部分类</option>';
cats.forEach(c => {
const opt = document.createElement('option');
opt.value = c; opt.textContent = c;
sel.appendChild(opt);
});
if (current) sel.value = current;
}
/* ================= 工具 ================= */
function escapeHtml(str) {
return String(str == null ? '' : str).replace(/[&<>"']/g, m => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m]));
}
/* ================= 初始化 ================= */
document.addEventListener('DOMContentLoaded', function () {
loadBooks();
buildScoreInputs();
buildCategoryOptions();
renderStats();
renderBooks();
updateLiveScore();
// 点击遮罩关闭表单
document.getElementById('formOverlay').addEventListener('click', function (e) {
if (e.target === this) closeForm();
});
// Esc 关闭表单
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeForm(); });
console.log('📚 阅读书架初始化完成');
});
</script>
</body>
</html>
09-10 07:25
暂无回复
No comments yet
登录 后可以评论
NHKNANA
书籍分类请改成如文学、科技、社科、推理、历史、战争、哲学、艺术、政治、其他等分类
09-10 07:35
暂无回复
No comments yet
登录 后可以评论
NHKNANA
书籍分类请改成如文学、科技、社科、推理、历史、战争、哲学、艺术、政治、其他等分类,并把主页“全部分类”的下拉菜单选项改为相应的分类
09-10 08:18
暂无回复
No comments yet
登录 后可以评论
NHKNANA
书籍分类请改成如文学、科技、社科、推理、历史、战争、哲学、艺术、政治、其他等分类,并把主页“全部分类”的下拉菜单选项改为相应的分类
09-10 08:58
暂无回复
No comments yet
登录 后可以评论
Assistant
好的,我把书籍分类改为固定的「文学、科技、社科、推理、历史、战争、哲学、艺术、政治、其他」这几个分类,并让主页「全部分类」下拉菜单直接列出这些分类。以下是修改后的完整代码:
(HTML code has been generated and updated; see above for the current version.)
### 本次修改
- **📚 分类改为固定选项**:`文学、科技、社科、推理、历史、战争、哲学、艺术、政治、其他`,定义在 `CATEGORIES` 常量中。
- **录入表单**:书籍分类从自由输入框改为**下拉选择框**,只能从上述固定分类中选择。
- **主页「全部分类」下拉**:直接列出上述固定分类(不再根据已有书籍动态生成)。
- **兼容旧数据**:编辑旧书籍时若其分类不在固定分类中,会自动回退为「其他」,避免报错。
> 💡 提示:预览区右下角有 **Console** 面板,我在分类初始化、数据加载/保存、评分实时计算、增删改等关键节点都输出了日志。您可以把 Console 日志复制后作为输入发给我,方便我根据运行时输出继续调整。
▼ Code
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>阅读书架 · 推理评分簿</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>tailwind.config = { darkMode: 'class' }</script>
</head>
<body class="min-h-screen bg-slate-50 dark:bg-slate-900 text-slate-700 dark:text-slate-200 antialiased">
<!-- 主题切换按钮 -->
<button id="theme-toggle-btn" class="fixed top-4 right-4 z-10 p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors flex items-center justify-center" aria-label="Toggle theme" onclick="toggleTheme()">
<svg id="icon-moon" class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998z"/></svg>
<svg id="icon-sun" class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24" aria-hidden="true" style="display:none"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"></path></svg>
</button>
<script>
function toggleTheme(){ var el=document.documentElement; var isDark=!el.classList.contains('dark'); el.classList.toggle('dark',isDark); localStorage.setItem('theme',isDark?'dark':'light'); applyTheme(isDark); }
function applyTheme(isDark){ document.getElementById('icon-moon').style.display=isDark?'none':'block'; document.getElementById('icon-sun').style.display=isDark?'block':'none'; }
document.addEventListener('DOMContentLoaded',function(){ var saved=localStorage.getItem('theme'); var isDark=saved==='dark'||(!saved&&window.matchMedia('(prefers-color-scheme: dark)').matches); document.documentElement.classList.toggle('dark',isDark); applyTheme(isDark); });
</script>
<!-- 自定义弹窗 -->
<div id="custom-modal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 hidden" aria-hidden="true">
<div class="rounded-xl bg-white dark:bg-slate-800 p-6 shadow-xl max-w-sm w-full border border-slate-200 dark:border-slate-700">
<p id="custom-modal-msg" class="text-slate-700 dark:text-slate-200 mb-4"></p>
<button type="button" onclick="hideModal()" class="w-full rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600">确定</button>
</div>
</div>
<script>
function showModal(msg){ var el=document.getElementById('custom-modal'); document.getElementById('custom-modal-msg').textContent=msg||''; el.classList.remove('hidden'); }
function hideModal(){ document.getElementById('custom-modal').classList.add('hidden'); }
</script>
<header class="max-w-6xl mx-auto px-4 pt-6 pb-4">
<div class="flex items-center justify-between gap-4 pr-12">
<div>
<h1 class="text-2xl sm:text-3xl font-bold text-slate-900 dark:text-slate-100 flex items-center gap-2">
<span class="text-amber-600 dark:text-amber-400 text-3xl">📚</span> 阅读书架
</h1>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">记录推理书籍,六维评分,自动评定等级</p>
</div>
<button id="addBtn" onclick="openForm()" class="shrink-0 text-sm sm:text-base rounded-lg bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 font-medium transition-colors shadow-sm">+ 添加书籍</button>
</div>
<!-- 统计概览 -->
<div id="statsBar" class="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-6"></div>
</header>
<!-- 筛选与搜索 -->
<section class="max-w-6xl mx-auto px-4 mb-4">
<div class="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
<input id="searchInput" type="text" placeholder="搜索书名 / 作者 / 分类…" class="flex-1 rounded-lg border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-800 px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" oninput="renderBooks()">
<select id="filterCategory" onchange="renderBooks()" class="rounded-lg border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-800 px-4 py-2 text-sm"></select>
<select id="filterStatus" onchange="renderBooks()" class="rounded-lg border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-800 px-4 py-2 text-sm">
<option value="">全部状态</option>
<option value="在读">在读</option>
<option value="想读">想读</option>
<option value="已读完">已读完</option>
<option value="搁置">搁置</option>
<option value="弃读">弃读</option>
</select>
</div>
</section>
<!-- 书架 -->
<main class="max-w-6xl mx-auto px-4 pb-16">
<div id="bookGrid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5"></div>
<div id="emptyState" class="hidden text-center py-16 text-slate-400 dark:text-slate-500">
<p class="text-5xl mb-3">📭</p>
<p class="text-lg">书架还是空的,点击右上角「添加书籍」开始记录吧!</p>
</div>
</main>
<!-- 新增/编辑 表单弹层 -->
<div id="formOverlay" class="fixed inset-0 z-40 hidden items-center justify-center bg-black/60 p-3 sm:p-6 overflow-y-auto">
<div id="formCard" class="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl w-full max-w-2xl my-auto border border-slate-200 dark:border-slate-700">
<div class="p-5 sm:p-6">
<h2 id="formTitle" class="text-xl font-bold text-slate-900 dark:text-slate-100 mb-5 flex items-center gap-2">📖 添加书籍</h2>
<form id="bookForm" class="space-y-4" onsubmit="return handleSubmit(event)">
<input type="hidden" id="bookId">
<!-- 基本信息 -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium mb-1" for="fTitle">书名 <span class="text-rose-500">*</span></label>
<input id="fTitle" required class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium mb-1" for="fAuthor">作者 <span class="text-rose-500">*</span></label>
<input id="fAuthor" required class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium mb-1" for="fNation">作者国籍</label>
<input id="fNation" placeholder="如:日本 / 英国" class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium mb-1" for="fPublisher">出版社</label>
<input id="fPublisher" class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium mb-1" for="fWords">字数(万字)</label>
<input id="fWords" type="number" min="0" step="0.1" class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium mb-1" for="fCategory">书籍分类</label>
<select id="fCategory" class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="文学">文学</option>
<option value="科技">科技</option>
<option value="社科">社科</option>
<option value="推理">推理</option>
<option value="历史">历史</option>
<option value="战争">战争</option>
<option value="哲学">哲学</option>
<option value="艺术">艺术</option>
<option value="政治">政治</option>
<option value="其他">其他</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1" for="fStatus">阅读状态</label>
<select id="fStatus" class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 text-sm">
<option value="想读">想读</option>
<option value="在读">在读</option>
<option value="已读完">已读完</option>
<option value="搁置">搁置</option>
<option value="弃读">弃读</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1" for="fPeriod">阅读时间段</label>
<input id="fPeriod" placeholder="如:2024.03 - 2024.05" class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</div>
<!-- 六维评分 -->
<div class="rounded-xl border border-slate-200 dark:border-slate-600 p-4 bg-slate-50 dark:bg-slate-700/50">
<p class="text-sm font-semibold text-slate-900 dark:text-slate-100 mb-3">🎯 六维评分</p>
<div id="scoreInputs" class="grid grid-cols-2 sm:grid-cols-3 gap-3"></div>
<div class="mt-4 pt-4 border-t border-slate-200 dark:border-slate-600">
<div class="flex flex-wrap items-center gap-4">
<div>
<span class="text-sm text-slate-500 dark:text-slate-400">总评分:</span>
<span id="liveTotal" class="text-xl font-bold text-slate-900 dark:text-slate-100">0.0</span>
</div>
<div class="flex items-center gap-2">
<span class="text-sm text-slate-500 dark:text-slate-400">等级:</span>
<span id="liveGrade" class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-slate-200 dark:bg-slate-600 text-slate-700 dark:text-slate-200 font-bold">-</span>
</div>
</div>
</div>
</div>
<!-- 笔记 -->
<div>
<label class="block text-sm font-medium mb-1" for="fNotes">📝 笔记</label>
<textarea id="fNotes" rows="3" class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 resize-y"></textarea>
</div>
<div class="flex justify-end gap-3 pt-2">
<button type="button" onclick="closeForm()" class="rounded-lg border border-slate-300 dark:border-slate-600 px-4 py-2 text-sm text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">取消</button>
<button type="submit" class="rounded-lg bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-6 py-2 text-sm font-medium transition-colors">保存</button>
</div>
</form>
</div>
</div>
</div>
<script>
/* ================= 分类配置(固定分类) ================= */
const CATEGORIES = ['文学', '科技', '社科', '推理', '历史', '战争', '哲学', '艺术', '政治', '其他'];
/* ================= 评分维度配置 ================= */
// 除「文笔文风」外均为固定上限;文笔文风为附加分(可扣,允许负值)
const SCORE_DIMS = [
{ key: 'reasoning', label: '推理性', max: 2.5, canNegative: false },
{ key: 'innovation', label: '创新性', max: 2.0, canNegative: false },
{ key: 'trick', label: '诡计设计', max: 2.5, canNegative: false },
{ key: 'motive', label: '动机设计', max: 1.5, canNegative: false },
{ key: 'plot', label: '故事情节及人物', max: 1.5, canNegative: false },
{ key: 'style', label: '文笔文风', max: 1.0, canNegative: true },
];
/* ================= 等级判定 ================= */
function gradeOf(total) {
if (total >= 9) return 'S';
if (total >= 8) return 'A';
if (total >= 7) return 'B';
if (total >= 6) return 'C';
if (total >= 5) return 'D';
if (total >= 4) return 'E';
return 'F';
}
const GRADE_COLOR = { S:'text-amber-600 dark:text-amber-400 border-amber-400', A:'text-blue-600 dark:text-blue-400 border-blue-400', B:'text-emerald-600 dark:text-emerald-400 border-emerald-400', C:'text-teal-600 dark:text-teal-400 border-teal-400', D:'text-orange-600 dark:text-orange-400 border-orange-400', E:'text-purple-600 dark:text-purple-400 border-purple-400', F:'text-rose-600 dark:text-rose-400 border-rose-400' };
/* ================= 数据存储 ================= */
let books = [];
function loadBooks() {
try { books = JSON.parse(localStorage.getItem('bookShelf') || '[]'); } catch (e) { books = []; }
console.log('已加载书籍数据,共', books.length, '本');
}
function saveBooks() { localStorage.setItem('bookShelf', JSON.stringify(books)); console.log('数据已保存,共', books.length, '本'); }
/* ================= 渲染评分输入 ================= */
function buildScoreInputs() {
const wrap = document.getElementById('scoreInputs');
wrap.innerHTML = '';
SCORE_DIMS.forEach(d => {
const div = document.createElement('div');
div.innerHTML = `
<label class="block text-xs font-medium mb-1 ${d.canNegative ? 'text-amber-600 dark:text-amber-400' : 'text-slate-500 dark:text-slate-400'}">
${d.label} <span class="text-slate-400">(${d.max}分${d.canNegative ? '·可扣' : ''})</span>
</label>
<input type="number" step="0.1" ${d.canNegative ? 'min="-2" max="1"' : `min="0" max="${d.max}"`} value="0"
class="score-input w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
data-key="${d.key}" oninput="updateLiveScore()">`;
wrap.appendChild(div);
});
}
/* ================= 实时总分与等级 ================= */
function calcTotal() {
const inputs = document.querySelectorAll('.score-input');
let sum = 0;
inputs.forEach(inp => {
const v = parseFloat(inp.value);
if (!isNaN(v)) sum += v;
});
return Math.round(sum * 10) / 10; // 保留一位小数
}
function updateLiveScore() {
const total = calcTotal();
document.getElementById('liveTotal').textContent = total.toFixed(1);
const grade = gradeOf(total);
const el = document.getElementById('liveGrade');
el.textContent = grade;
el.className = 'inline-flex items-center justify-center w-8 h-8 rounded-full border font-bold ' + (GRADE_COLOR[grade] || '');
console.log('评分实时计算:总评', total.toFixed(1), ',等级', grade);
}
/* ================= 表单打开/关闭 ================= */
let editingId = null;
function openForm(id) {
editingId = id || null;
document.getElementById('bookForm').reset();
document.getElementById('bookId').value = '';
buildScoreInputs();
updateLiveScore();
document.getElementById('formTitle').textContent = id ? '✏️ 编辑书籍' : '📖 添加书籍';
if (id) {
const b = books.find(x => x.id === id);
if (b) fillForm(b);
}
const overlay = document.getElementById('formOverlay');
overlay.classList.remove('hidden');
overlay.classList.add('flex');
console.log(id ? '打开编辑表单,书籍ID=' + id : '打开新增表单');
}
function fillForm(b) {
document.getElementById('bookId').value = b.id;
document.getElementById('fTitle').value = b.title || '';
document.getElementById('fAuthor').value = b.author || '';
document.getElementById('fNation').value = b.nation || '';
document.getElementById('fPublisher').value = b.publisher || '';
document.getElementById('fWords').value = b.words != null ? b.words : '';
// 分类:若旧数据不在固定分类内,回退为「其他」
const catVal = CATEGORIES.includes(b.category) ? b.category : '其他';
document.getElementById('fCategory').value = catVal;
document.getElementById('fStatus').value = b.status || '想读';
document.getElementById('fPeriod').value = b.period || '';
document.getElementById('fNotes').value = b.notes || '';
// 回填评分
const dims = b.scores || {};
document.querySelectorAll('.score-input').forEach(inp => {
const key = inp.dataset.key;
if (key in dims) inp.value = dims[key];
});
updateLiveScore();
}
function closeForm() {
const overlay = document.getElementById('formOverlay');
overlay.classList.add('hidden');
overlay.classList.remove('flex');
}
/* ================= 表单提交 ================= */
function handleSubmit(e) {
e.preventDefault();
const title = document.getElementById('fTitle').value.trim();
const author = document.getElementById('fAuthor').value.trim();
if (!title || !author) { showModal('书名和作者为必填项'); return false; }
const scores = {};
let total = 0;
document.querySelectorAll('.score-input').forEach(inp => {
let v = parseFloat(inp.value);
if (isNaN(v)) v = 0;
scores[inp.dataset.key] = v;
total += v;
});
total = Math.round(total * 10) / 10;
const id = document.getElementById('bookId').value;
const book = {
id: id || 'b' + Date.now(),
title, author,
nation: document.getElementById('fNation').value.trim(),
publisher: document.getElementById('fPublisher').value.trim(),
words: document.getElementById('fWords').value === '' ? null : parseFloat(document.getElementById('fWords').value),
category: document.getElementById('fCategory').value,
status: document.getElementById('fStatus').value,
period: document.getElementById('fPeriod').value.trim(),
notes: document.getElementById('fNotes').value.trim(),
scores, total, grade: gradeOf(total),
updatedAt: Date.now()
};
const idx = books.findIndex(x => x.id === book.id);
if (idx >= 0) { books[idx] = book; console.log('更新书籍:', book.title); }
else { books.push(book); console.log('新增书籍:', book.title); }
saveBooks();
closeForm();
renderBooks();
renderStats();
return false;
}
/* ================= 删除 ================= */
function deleteBook(id) {
const b = books.find(x => x.id === id);
const msg = `确定要删除《${b ? b.title : '这本书'}》吗?此操作不可恢复。`;
if (confirm(msg)) {
books = books.filter(x => x.id !== id);
saveBooks();
renderBooks();
renderStats();
console.log('已删除书籍,ID=', id);
}
}
/* ================= 渲染书架 ================= */
function statusBadge(status) {
const map = {
'想读': 'bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300',
'在读': 'bg-blue-100 dark:bg-blue-500/20 text-blue-600 dark:text-blue-300',
'已读完': 'bg-emerald-100 dark:bg-emerald-500/20 text-emerald-600 dark:text-emerald-300',
'搁置': 'bg-amber-100 dark:bg-amber-500/20 text-amber-600 dark:text-amber-300',
'弃读': 'bg-rose-100 dark:bg-rose-500/20 text-rose-600 dark:text-rose-300'
};
return map[status] || 'bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300';
}
function renderBooks() {
const q = (document.getElementById('searchInput').value || '').trim().toLowerCase();
const cat = document.getElementById('filterCategory').value;
const st = document.getElementById('filterStatus').value;
let list = books.filter(b => {
const matchQ = !q || (b.title||'').toLowerCase().includes(q) || (b.author||'').toLowerCase().includes(q) || (b.category||'').toLowerCase().includes(q);
const matchCat = !cat || (b.category||'') === cat;
const matchSt = !st || b.status === st;
return matchQ && matchCat && matchSt;
});
// 按最近更新排序
list.sort((a, b) => (b.updatedAt||0) - (a.updatedAt||0));
const grid = document.getElementById('bookGrid');
grid.innerHTML = '';
const empty = document.getElementById('emptyState');
if (list.length === 0) {
empty.classList.remove('hidden');
grid.classList.add('hidden');
console.log('书架为空或无匹配结果');
return;
}
empty.classList.add('hidden');
grid.classList.remove('hidden');
list.forEach(b => {
const card = document.createElement('div');
card.className = 'rounded-xl bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 shadow-sm hover:shadow-md transition-shadow p-5 flex flex-col';
const gradeColor = GRADE_COLOR[b.grade] || '';
card.innerHTML = `
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<h3 class="font-bold text-slate-900 dark:text-slate-100 text-lg leading-snug truncate">${escapeHtml(b.title)}</h3>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-0.5">${escapeHtml(b.author||'')}${b.nation ? ' · '+escapeHtml(b.nation) : ''}</p>
</div>
<span class="inline-flex items-center justify-center w-9 h-9 rounded-full border-2 font-bold shrink-0 ${gradeColor}">${b.grade}</span>
</div>
<div class="mt-3 flex flex-wrap gap-2 text-xs">
<span class="inline-flex rounded-full ${statusBadge(b.status)} px-2.5 py-0.5">${escapeHtml(b.status||'')}</span>
${b.category ? `<span class="inline-flex rounded-full bg-indigo-100 dark:bg-indigo-500/20 text-indigo-600 dark:text-indigo-300 px-2.5 py-0.5">${escapeHtml(b.category)}</span>` : ''}
${b.publisher ? `<span class="inline-flex rounded-full bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 px-2.5 py-0.5">${escapeHtml(b.publisher)}</span>` : ''}
${b.words != null ? `<span class="inline-flex rounded-full bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 px-2.5 py-0.5">${b.words} 万字</span>` : ''}
</div>
${b.period ? `<p class="mt-3 text-xs text-slate-400 dark:text-slate-500">📅 ${escapeHtml(b.period)}</p>` : ''}
<div class="mt-3 pt-3 border-t border-slate-100 dark:border-slate-700 flex items-baseline gap-3">
<span class="text-2xl font-extrabold ${b.total>=7?'text-amber-600 dark:text-amber-400':'text-slate-700 dark:text-slate-200'}">${b.total != null ? b.total.toFixed(1) : '-'}</span>
<span class="text-xs text-slate-400 dark:text-slate-500">总评分 / 满分 10</span>
</div>
${b.notes ? `<p class="mt-2 text-sm text-slate-600 dark:text-slate-300 whitespace-pre-line line-clamp-3">📝 ${escapeHtml(b.notes)}</p>` : ''}
<div class="mt-4 pt-3 border-t border-slate-100 dark:border-slate-700 flex justify-end gap-2">
<button type="button" onclick="openForm('${b.id}')" class="text-xs rounded-lg border border-blue-300 dark:border-blue-500/50 text-blue-600 dark:text-blue-400 px-3 py-1 hover:bg-blue-50 dark:hover:bg-blue-500/10 transition-colors">编辑</button>
<button type="button" onclick="deleteBook('${b.id}')" class="text-xs rounded-lg border border-rose-300 dark:border-rose-500/50 text-rose-600 dark:text-rose-400 px-3 py-1 hover:bg-rose-50 dark:hover:bg-rose-500/10 transition-colors">删除</button>
</div>`;
grid.appendChild(card);
});
}
/* ================= 统计概览 ================= */
function renderStats() {
const bar = document.getElementById('statsBar');
const total = books.length;
const done = books.filter(b => b.status === '已读完').length;
const reading = books.filter(b => b.status === '在读').length;
const best = books.filter(b => b.total >= 9).length;
const cards = [
{ label: '藏书总数', value: total, icon: '📚', cls: '' },
{ label: '已读完', value: done, icon: '✅', cls: 'text-emerald-600 dark:text-emerald-400' },
{ label: '在读', value: reading, icon: '📖', cls: 'text-blue-600 dark:text-blue-400' },
{ label: 'S级佳作', value: best, icon: '🏆', cls: 'text-amber-600 dark:text-amber-400' }
];
bar.innerHTML = cards.map(c => `
<div class="rounded-xl bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 p-4">
<p class="text-xs text-slate-500 dark:text-slate-400">${c.icon} ${c.label}</p>
<p class="text-2xl font-bold text-slate-900 dark:text-slate-100 mt-1 ${c.cls}">${c.value}</p>
</div>`).join('');
}
/* ================= 分类筛选选项(固定分类) ================= */
function buildCategoryOptions() {
const sel = document.getElementById('filterCategory');
const current = sel.value;
sel.innerHTML = '<option value="">全部分类</option>';
CATEGORIES.forEach(c => {
const opt = document.createElement('option');
opt.value = c; opt.textContent = c;
sel.appendChild(opt);
});
if (current) sel.value = current;
console.log('分类筛选选项已更新为固定分类');
}
/* ================= 工具 ================= */
function escapeHtml(str) {
return String(str == null ? '' : str).replace(/[&<>"']/g, m => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m]));
}
/* ================= 初始化 ================= */
document.addEventListener('DOMContentLoaded', function () {
loadBooks();
buildScoreInputs();
buildCategoryOptions(); // 填充固定分类到筛选下拉
renderStats();
renderBooks();
updateLiveScore();
// 点击遮罩关闭表单
document.getElementById('formOverlay').addEventListener('click', function (e) {
if (e.target === this) closeForm();
});
// Esc 关闭表单
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeForm(); });
console.log('📚 阅读书架初始化完成,分类:', CATEGORIES.join(' / '));
});
</script>
</body>
</html>
09-10 08:59
暂无回复
No comments yet
登录 后可以评论
NHKNANA
请在主页增加按“等级”筛选的下拉菜单功能
09-10 09:03
暂无回复
No comments yet
登录 后可以评论
Assistant
好的,我在主页筛选区增加「按等级筛选」的下拉菜单,可筛选 S/A/B/C/D/E/F 任意等级。以下是修改后的完整代码:
(已生成并更新HTML代码,当前版本见上方)
### 本次修改
在筛选区新增**等级筛选下拉菜单**:
- 选项包含:`全部等级`、`S`、`A`、`B`、`C`、`D`、`E`、`F`
- 与搜索、分类、状态筛选可自由组合使用
- 根据每本书的总评分对应等级进行过滤
> 💡 提示:预览区右下角有 **Console** 面板,我在筛选/搜索、数据加载、评分计算、增删改等节点都输出了日志。您可以把 Console 日志复制后作为输入发给我,方便我根据实际运行输出继续调整或排查问题。
09-10 09:04
暂无回复
No comments yet
登录 后可以评论
Comments
暂无回复
No comments yet
登录 后可以评论