Files

1269 lines
100 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, viewport-fit=cover">
<title>读书笔记工作台</title>
<style>
:root{
--bg:#f2f7f4;--bg-card:#fff;--bg-side:#f7fbf8;--bg-hover:rgba(42,157,120,.06);--bg-active:rgba(42,157,120,.12);
--g1:#7ed8b4;--g2:#2a9d78;--accent:#2a9d78;--accent-h:#1f7d5f;--accent-l:#5cb896;--accent-bg:rgba(42,157,120,.08);
--t1:#24352e;--t2:#5d7268;--t3:#93a89e;--border:#dde8e2;--danger:#c25450;--success:#2a9d78;--warn:#d4a04a;--info:#5a9baf;
--r:12px;--rs:8px;--shadow-s:0 1px 3px rgba(42,157,120,.06);--shadow:0 2px 8px rgba(42,157,120,.08);--shadow-l:0 4px 20px rgba(42,157,120,.15);
--side-w:220px;--tab-h:56px;--top-h:50px;--tr:.2s ease;
}
*{margin:0;padding:0;box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;background:var(--bg);color:var(--t1);-webkit-font-smoothing:antialiased;line-height:1.5;overflow-x:hidden}
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
input,textarea,select{font-family:inherit}
a{color:inherit;text-decoration:none}
img{max-width:100%;display:block}
::-webkit-scrollbar{width:6px;height:6px}
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
/* === Layout === */
#app{min-height:100vh}
#sidebar{position:fixed;left:0;top:0;width:var(--side-w);height:100vh;background:var(--bg-side);border-right:1px solid var(--border);display:flex;flex-direction:column;z-index:100;overflow-y:auto}
.sidebar-logo{padding:20px 18px 16px;font-size:16px;font-weight:700;color:var(--accent);display:flex;align-items:center;gap:8px}
.nav-items{flex:1;padding:4px 10px}
.nav-item{display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:var(--rs);font-size:14px;color:var(--t2);cursor:pointer;transition:all var(--tr);margin-bottom:2px;min-height:44px}
.nav-item:hover{background:var(--bg-hover);color:var(--t1)}
.nav-item.active{background:var(--bg-active);color:var(--accent);font-weight:600}
.nav-item svg{flex-shrink:0}
.sidebar-footer{padding:12px 14px;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:4px}
#topbar{display:none;position:fixed;top:0;left:0;right:0;height:var(--top-h);background:var(--bg-side);border-bottom:1px solid var(--border);align-items:center;padding:0 16px;z-index:100;gap:4px;overflow-x:auto}
.topbar-logo{font-size:15px;font-weight:700;color:var(--accent);white-space:nowrap;margin-right:8px;display:flex;align-items:center;gap:6px}
.topbar-items{display:flex;gap:2px;flex:1}
.topbar-item{padding:6px 10px;border-radius:var(--rs);font-size:13px;color:var(--t2);cursor:pointer;transition:all var(--tr);white-space:nowrap;min-height:36px;display:flex;align-items:center;gap:5px}
.topbar-item:hover{background:var(--bg-hover);color:var(--t1)}
.topbar-item.active{color:var(--accent);font-weight:600}
.topbar-actions{display:flex;gap:4px}
#mobile-header{display:none;position:sticky;top:0;background:var(--bg-side);border-bottom:1px solid var(--border);padding:10px 12px;z-index:50;align-items:center;justify-content:space-between}
.mobile-logo{font-size:15px;font-weight:700;color:var(--accent);display:flex;align-items:center;gap:6px}
.mobile-actions{display:flex;gap:6px}
.icon-btn{width:36px;height:36px;border-radius:var(--rs);display:flex;align-items:center;justify-content:center;color:var(--t2);transition:all var(--tr)}
.icon-btn:hover{background:var(--bg-hover);color:var(--accent)}
#main{margin-left:var(--side-w);padding:24px 28px 80px;min-height:100vh}
#tabbar{display:none;position:fixed;bottom:0;left:0;right:0;height:calc(var(--tab-h) + env(safe-area-inset-bottom));padding-bottom:env(safe-area-inset-bottom);background:var(--bg-card);border-top:1px solid var(--border);z-index:100}
.tabbar-inner{display:flex;height:var(--tab-h)}
.tab-item{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;color:var(--t3);cursor:pointer;transition:color var(--tr);min-height:44px}
.tab-item.active{color:var(--accent)}
.tab-item svg{flex-shrink:0}
.tab-label{font-size:10px;white-space:nowrap}
/* === Common === */
.section{display:none;animation:fadeIn .3s ease}
.section.active{display:block}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
.section-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;flex-wrap:wrap;gap:8px}
.section-title{font-size:20px;font-weight:700;color:var(--t1)}
.section-sub{font-size:13px;color:var(--t3);margin-top:2px}
/* Buttons */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:9px 18px;border-radius:var(--rs);font-size:14px;font-weight:500;transition:all var(--tr);min-height:40px;white-space:nowrap}
.btn-sm{padding:6px 12px;font-size:12px;min-height:32px}
.btn-primary{background:linear-gradient(135deg,var(--g1),var(--g2));color:#fff;box-shadow:0 2px 6px rgba(42,157,120,.2)}
.btn-primary:hover{opacity:.92;box-shadow:0 4px 10px rgba(42,157,120,.25)}
.btn-outline{border:1px solid var(--border);color:var(--t2);background:var(--bg-card)}
.btn-outline:hover{border-color:var(--accent);color:var(--accent)}
.btn-danger{color:var(--danger)}
.btn-danger:hover{background:rgba(194,84,80,.08)}
.btn-ghost{color:var(--t2);padding:6px 10px}
.btn-ghost:hover{background:var(--bg-hover);color:var(--t1)}
/* Cards */
.grid{display:grid;gap:14px}
.grid-3{grid-template-columns:repeat(auto-fill,minmax(280px,1fr))}
.grid-2{grid-template-columns:repeat(auto-fill,minmax(240px,1fr))}
.card{background:var(--bg-card);border-radius:var(--r);box-shadow:var(--shadow-s);overflow:hidden;transition:box-shadow var(--tr),transform var(--tr);cursor:pointer}
.card:hover{box-shadow:var(--shadow);transform:translateY(-2px)}
.card-body{padding:14px 16px}
.card-cover{height:120px;background:linear-gradient(135deg,var(--accent-bg),rgba(126,216,180,.15));display:flex;align-items:center;justify-content:center;color:var(--accent-l);overflow:hidden}
.card-cover img{width:100%;height:100%;object-fit:cover}
.card-title{font-size:15px;font-weight:600;margin-bottom:4px;color:var(--t1)}
.card-meta{font-size:12px;color:var(--t3);display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.tag{display:inline-flex;align-items:center;padding:2px 8px;border-radius:10px;font-size:11px;background:var(--accent-bg);color:var(--accent)}
.tag-danger{background:rgba(194,84,80,.1);color:var(--danger)}
.tag-success{background:rgba(90,138,94,.1);color:var(--success)}
.tag-warn{background:rgba(212,160,74,.1);color:var(--warn)}
.tag-info{background:rgba(106,140,175,.1);color:var(--info)}
/* Forms */
.form-group{margin-bottom:14px}
.form-label{display:block;font-size:13px;font-weight:600;color:var(--t2);margin-bottom:6px}
.input,.textarea,.select{width:100%;padding:10px 14px;border:1px solid var(--border);border-radius:var(--rs);font-size:16px;color:var(--t1);background:var(--bg-card);transition:border-color var(--tr);line-height:1.5}
.input:focus,.textarea:focus,.select:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-bg)}
.textarea{min-height:80px;resize:vertical}
.checkbox-row{display:flex;align-items:center;gap:8px;cursor:pointer;padding:8px 0}
.checkbox-row input{width:18px;height:18px;accent-color:var(--accent)}
.checkbox-row span{font-size:14px;color:var(--t1)}
/* Search */
.search-bar{position:relative;margin-bottom:16px}
.search-bar input{width:100%;padding:10px 14px 10px 40px;border:1px solid var(--border);border-radius:24px;font-size:15px;background:var(--bg-card);transition:border-color var(--tr)}
.search-bar input:focus{outline:none;border-color:var(--accent)}
.search-bar svg{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--t3)}
/* Summary cards - vertical left layout */
.summary-grid{display:flex;flex-direction:column;gap:10px;margin-bottom:20px;max-width:280px}
.summary-card{background:var(--bg-card);border-radius:var(--r);padding:14px 16px;box-shadow:var(--shadow-s);cursor:pointer;transition:all var(--tr);border:1px solid transparent;display:flex;align-items:center;gap:12px}
.summary-card:hover{box-shadow:var(--shadow);border-color:var(--border);transform:translateX(2px)}
.summary-icon{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
.summary-info{flex:1;min-width:0}
.summary-num{font-size:22px;font-weight:700;color:var(--t1);line-height:1.2}
.summary-label{font-size:12px;color:var(--t3)}
.summary-sub{font-size:11px;color:var(--t3);margin-top:2px}
/* Today section */
.today-section{background:linear-gradient(135deg,rgba(126,216,180,.08),rgba(42,157,120,.05));border:1px solid var(--border);border-radius:var(--r);padding:16px;margin-bottom:20px}
.today-title{font-size:15px;font-weight:700;color:var(--accent);margin-bottom:12px;display:flex;align-items:center;gap:6px}
.today-item{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--bg-card);border-radius:var(--rs);margin-bottom:8px;gap:10px}
.today-item:last-child{margin-bottom:0}
.today-item-content{flex:1;min-width:0}
.today-item-title{font-size:14px;font-weight:500;color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.today-item-meta{font-size:12px;color:var(--t3)}
.today-item.overdue{border-left:3px solid var(--danger)}
.today-item.overdue .today-item-title{color:var(--danger)}
/* Sort toggle */
.sort-toggle{display:flex;gap:2px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--rs);padding:2px}
.sort-btn{padding:5px 12px;font-size:12px;border-radius:6px;color:var(--t3);transition:all var(--tr);min-height:30px}
.sort-btn.active{background:var(--accent);color:#fff}
/* Note item */
.note-item{background:var(--bg-card);border-radius:var(--r);box-shadow:var(--shadow-s);overflow:hidden;margin-bottom:12px}
.note-photo{width:100%;max-height:200px;object-fit:cover;background:var(--bg-hover)}
.note-body{padding:14px 16px}
.note-text{font-size:14px;color:var(--t1);line-height:1.7;margin-bottom:8px;border-left:3px solid var(--accent-l);padding-left:10px}
.note-reflection{font-size:14px;color:var(--t2);line-height:1.7;background:var(--accent-bg);padding:10px 12px;border-radius:var(--rs);margin-top:8px}
.note-reflection::before{content:"感悟";display:block;font-size:11px;font-weight:600;color:var(--accent);margin-bottom:4px}
.note-watermark{font-size:11px;color:var(--t3);margin-top:8px;display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.note-actions{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
.note-meta-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:6px}
/* Empty state */
.empty{text-align:center;padding:40px 20px;color:var(--t3)}
.empty svg{margin-bottom:12px;color:var(--border)}
.empty-text{font-size:14px}
/* Modal */
#modal-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(20,45,35,.4);z-index:1000;align-items:flex-start;justify-content:center;padding:20px;overflow-y:auto}
#modal-overlay.active{display:flex}
.modal{background:var(--bg-card);border-radius:var(--r);width:100%;max-width:560px;box-shadow:var(--shadow-l);margin:auto;animation:modalIn .25s ease}
@keyframes modalIn{from{opacity:0;transform:translateY(20px) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}}
.modal-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--border)}
.modal-title{font-size:17px;font-weight:700;color:var(--t1)}
.modal-close{width:32px;height:32px;border-radius:var(--rs);display:flex;align-items:center;justify-content:center;color:var(--t3);transition:all var(--tr)}
.modal-close:hover{background:var(--bg-hover);color:var(--danger)}
.modal-body{padding:20px;max-height:65vh;overflow-y:auto}
.modal-footer{display:flex;justify-content:flex-end;gap:8px;padding:12px 20px;border-top:1px solid var(--border)}
/* Voice recording */
.voice-btn{width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;border:1px solid var(--border);color:var(--t2);transition:all var(--tr);flex-shrink:0}
.voice-btn:hover{border-color:var(--accent);color:var(--accent)}
.voice-btn.recording{background:var(--danger);border-color:var(--danger);color:#fff;animation:pulse 1.2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.6}}
/* Photo upload */
.photo-upload{border:2px dashed var(--border);border-radius:var(--rs);padding:20px;text-align:center;cursor:pointer;transition:all var(--tr)}
.photo-upload:hover{border-color:var(--accent);background:var(--accent-bg)}
.photo-upload svg{color:var(--t3);margin-bottom:8px}
.photo-preview{margin-top:10px;position:relative}
.photo-preview img{width:100%;border-radius:var(--rs);max-height:240px;object-fit:cover}
.photo-remove{position:absolute;top:8px;right:8px;width:28px;height:28px;border-radius:50%;background:rgba(0,0,0,.5);color:#fff;display:flex;align-items:center;justify-content:center}
/* Mood selector */
.mood-grid{display:flex;gap:8px;flex-wrap:wrap}
.mood-option{display:flex;flex-direction:column;align-items:center;gap:4px;padding:8px 10px;border:2px solid var(--border);border-radius:var(--rs);cursor:pointer;transition:all var(--tr);min-width:56px}
.mood-option:hover{border-color:var(--accent-l)}
.mood-option.selected{border-color:var(--accent);background:var(--accent-bg)}
.mood-option svg{width:28px;height:28px}
.mood-label{font-size:11px;color:var(--t2)}
/* Diary entries */
.diary-item{background:var(--bg-card);border-radius:var(--r);box-shadow:var(--shadow-s);padding:14px 16px;margin-bottom:10px;cursor:pointer;transition:all var(--tr)}
.diary-item:hover{box-shadow:var(--shadow)}
.diary-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
.diary-date{font-size:14px;font-weight:600;color:var(--t1)}
.diary-mood{display:flex;align-items:center;gap:4px;font-size:12px;color:var(--t2)}
.diary-location{font-size:12px;color:var(--t3);margin-bottom:6px}
.diary-content{font-size:14px;color:var(--t2);line-height:1.6;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
/* English word */
.word-item{background:var(--bg-card);border-radius:var(--r);box-shadow:var(--shadow-s);padding:14px 16px;margin-bottom:10px;cursor:pointer;transition:all var(--tr)}
.word-item:hover{box-shadow:var(--shadow)}
.word-header{display:flex;align-items:center;justify-content:space-between}
.word-title{font-size:18px;font-weight:700;color:var(--accent)}
.word-phonetic{font-size:13px;color:var(--t3);margin-top:2px}
.word-meaning{font-size:14px;color:var(--t1);margin-top:6px}
.word-example{font-size:13px;color:var(--t2);font-style:italic;margin-top:6px;padding-left:10px;border-left:2px solid var(--border)}
/* Confetti */
.confetti-piece{position:fixed;top:-10px;z-index:9999;pointer-events:none;animation:confettiFall ease-in forwards}
@keyframes confettiFall{0%{transform:translateY(0) rotate(0);opacity:1}100%{transform:translateY(calc(100vh + 50px)) rotate(720deg);opacity:0}}
/* Toast */
#toast{position:fixed;bottom:80px;left:50%;transform:translateX(-50%) translateY(20px);background:var(--t1);color:#fff;padding:10px 22px;border-radius:22px;font-size:14px;opacity:0;pointer-events:none;transition:all .3s;z-index:10000;white-space:nowrap}
#toast.active{opacity:1;transform:translateX(-50%) translateY(0)}
/* Edit mode */
.edit-bar{display:flex;align-items:center;gap:8px;padding:10px 14px;background:var(--bg-card);border-radius:var(--r);margin-bottom:12px;box-shadow:var(--shadow-s);flex-wrap:wrap}
.edit-bar-info{font-size:13px;color:var(--t2);flex:1}
.edit-bar-count{font-size:12px;color:var(--accent);font-weight:600}
.item-checkbox{width:22px;height:22px;accent-color:var(--accent);flex-shrink:0;cursor:pointer;border:2px solid var(--border);border-radius:4px}
.note-item.selectable,.diary-item.selectable,.word-item.selectable,.card.selectable{position:relative}
.note-item.selectable::before,.diary-item.selectable::before,.word-item.selectable::before,.card.selectable::before{content:'';position:absolute;left:0;top:0;right:0;bottom:0;border:2px solid transparent;border-radius:var(--r);pointer-events:none;transition:border-color .2s}
.note-item.selected::before,.diary-item.selected::before,.word-item.selected::before,.card.selected::before{border-color:var(--accent);background:var(--accent-bg)}
.select-cb{width:20px;height:20px;accent-color:var(--accent);flex-shrink:0;cursor:pointer}
.add-reflection-btn{display:inline-flex;align-items:center;gap:4px;font-size:12px;color:var(--accent);cursor:pointer;padding:4px 10px;border-radius:var(--rs);border:1px solid var(--accent-l);background:var(--accent-bg);transition:all var(--tr)}
.add-reflection-btn:hover{background:var(--accent);color:#fff}
/* Back button */
.back-btn{display:inline-flex;align-items:center;gap:4px;font-size:13px;color:var(--t2);cursor:pointer;padding:6px 10px;border-radius:var(--rs);transition:all var(--tr);margin-bottom:12px}
.back-btn:hover{background:var(--bg-hover);color:var(--accent)}
/* Progress */
.progress-bar{height:6px;background:var(--border);border-radius:3px;overflow:hidden;margin-top:8px}
.progress-fill{height:100%;background:linear-gradient(90deg,var(--g1),var(--g2));border-radius:3px;transition:width .5s ease}
/* Hidden file input */
.hidden-input{display:none}
/* Responsive: tablet */
@media(max-width:1024px){
#sidebar{display:none}
#topbar{display:flex}
#main{margin-left:0;padding:calc(var(--top-h) + 16px) 20px 80px}
}
/* Responsive: mobile */
@media(max-width:768px){
#topbar{display:none}
#tabbar{display:block}
#mobile-header{display:flex}
#main{padding:8px 10px calc(var(--tab-h) + env(safe-area-inset-bottom) + 16px)}
.section-title{font-size:17px}
.grid-3,.grid-2{grid-template-columns:1fr}
.summary-grid{max-width:100%}
.modal{max-width:100%}
#modal-overlay{padding:10px}
.modal-body{padding:16px;max-height:60vh}
.input,.textarea,.select{font-size:16px}
.btn{min-height:44px}
}
</style>
</head>
<body>
<div id="app">
<!-- Desktop Sidebar -->
<nav id="sidebar">
<div class="sidebar-logo">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
读书笔记
</div>
<div class="nav-items" id="sidebar-nav"></div>
<div class="sidebar-footer">
<button class="btn btn-outline btn-sm" data-action="export-data" style="width:100%;justify-content:center">导出备份</button>
<button class="btn btn-outline btn-sm" data-action="trigger-import" style="width:100%;justify-content:center">导入恢复</button>
</div>
</nav>
<!-- Tablet Top Bar -->
<nav id="topbar">
<div class="topbar-logo">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
读书笔记
</div>
<div class="topbar-items" id="topbar-nav"></div>
<div class="topbar-actions">
<button class="icon-btn" data-action="export-data" title="导出"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg></button>
<button class="icon-btn" data-action="trigger-import" title="导入"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><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></button>
</div>
</nav>
<!-- Mobile Header -->
<header id="mobile-header">
<div class="mobile-logo">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
读书笔记
</div>
<div class="mobile-actions">
<button class="icon-btn" data-action="export-data"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg></button>
<button class="icon-btn" data-action="trigger-import"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><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></button>
</div>
</header>
<!-- Main Content -->
<main id="main">
<div id="content"></div>
</main>
<!-- Mobile Tab Bar -->
<nav id="tabbar">
<div class="tabbar-inner" id="tabbar-nav"></div>
</nav>
</div>
<!-- Modal -->
<div id="modal-overlay">
<div class="modal">
<div class="modal-header">
<div class="modal-title" id="modal-title">标题</div>
<button class="modal-close" data-action="close-modal"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><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" id="modal-body"></div>
<div class="modal-footer">
<button class="btn btn-outline" id="modal-cancel" data-action="close-modal">取消</button>
<button class="btn btn-primary" id="modal-save">保存</button>
</div>
</div>
</div>
<!-- Confetti container -->
<div id="confetti-container"></div>
<!-- Toast -->
<div id="toast"></div>
<!-- Hidden file input -->
<input type="file" id="import-file" class="hidden-input" accept=".json">
<script>
// ========== SVG Icons ==========
const SVG={
home:'<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>',
paper:'<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>',
ebook:'<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="12" y1="18" x2="12" y2="18"/></svg>',
video:'<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg>',
diary:'<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>',
english:'<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>',
search:'<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
plus:'<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>',
camera:'<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>',
mic:'<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>',
pin:'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>',
clock:'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
trash:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>',
edit:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',
back:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>',
check:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>',
download:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
upload:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><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>',
bookCover:'<svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
emptyBox:'<svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>',
alert:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12" y2="17"/></svg>',
calendar:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>',
// Mood icons
moodHappy:'<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><circle cx="9" cy="10" r="1" fill="currentColor"/><circle cx="15" cy="10" r="1" fill="currentColor"/><path d="M8 14 Q12 17 16 14" stroke-linecap="round"/></svg>',
moodCalm:'<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><circle cx="9" cy="10" r="1" fill="currentColor"/><circle cx="15" cy="10" r="1" fill="currentColor"/><path d="M9 15 L15 15" stroke-linecap="round"/></svg>',
moodThink:'<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><circle cx="9" cy="10" r="1" fill="currentColor"/><circle cx="15" cy="10" r="1" fill="currentColor"/><path d="M9 16 Q12 14 15 16" stroke-linecap="round"/><path d="M14 7 L16 6" stroke-linecap="round"/></svg>',
moodTired:'<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><path d="M8 10 L10 10 M14 10 L16 10" stroke-linecap="round"/><path d="M9 15 Q12 13 15 15" stroke-linecap="round"/></svg>',
moodSad:'<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><circle cx="9" cy="10" r="1" fill="currentColor"/><circle cx="15" cy="10" r="1" fill="currentColor"/><path d="M8 16 Q12 13 16 16" stroke-linecap="round"/></svg>',
moodExcited:'<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><path d="M8 9 L10 10 M14 10 L16 9" stroke-linecap="round"/><path d="M7 13 Q12 18 17 13" stroke-linecap="round"/></svg>',
memo:'<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M8 10h8M8 14h5"/><path d="M16 14l2 2 3-3" stroke-width="2"/></svg>',
sentence:'<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>',
};
const MOODS={
happy:{label:'开心',icon:SVG.moodHappy,color:'#2a9d78'},
calm:{label:'平静',icon:SVG.moodCalm,color:'#5a9baf'},
think:{label:'思考',icon:SVG.moodThink,color:'#3aafa9'},
tired:{label:'疲惫',icon:SVG.moodTired,color:'#93a89e'},
sad:{label:'难过',icon:SVG.moodSad,color:'#5a9baf'},
excited:{label:'兴奋',icon:SVG.moodExcited,color:'#d4a04a'},
};
// ========== Navigation Items ==========
const NAV_ITEMS=[
{tab:'home',label:'首页',short:'首页',icon:SVG.home},
{tab:'books',label:'书籍',short:'书籍',icon:SVG.paper},
{tab:'video',label:'视频转文字',short:'视频',icon:SVG.video},
{tab:'diary',label:'日记',short:'日记',icon:SVG.diary},
{tab:'english',label:'英语',short:'英语',icon:SVG.english},
];
// ========== Storage ==========
const Store={
pfx:'wb_reading_',
get(k){try{return JSON.parse(localStorage.getItem(this.pfx+k)||'[]')}catch(e){return[]}},
set(k,v){localStorage.setItem(this.pfx+k,JSON.stringify(v))}
};
// ========== Utils ==========
function uid(){return Date.now().toString(36)+Math.random().toString(36).slice(2,8)}
function esc(s){if(!s)return'';return String(s).replace(/[&<>"']/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]})}
function fmt(d,wt){d=new Date(d);const p=n=>String(n).padStart(2,'0');let s=d.getFullYear()+'-'+p(d.getMonth()+1)+'-'+p(d.getDate());if(wt)s+=' '+p(d.getHours())+':'+p(d.getMinutes());return s}
function todayStr(){return fmt(new Date(),false)}
function toast(msg){const t=document.getElementById('toast');t.textContent=msg;t.classList.add('active');clearTimeout(t._t);t._t=setTimeout(()=>t.classList.remove('active'),2200)}
// ========== State ==========
let data={
books:Store.get('books'),
memo:Store.get('memo'),
video:Store.get('video'),
diary:Store.get('diary'),
english:Store.get('english'),
};
// migrate legacy paper/ebook to unified books
function migrateBooks(){
var oldPaper=Store.get('paper'),oldEbook=Store.get('ebook');
var hasPaper=localStorage.getItem('wb_reading_paper'),hasEbook=localStorage.getItem('wb_reading_ebook');
if(!localStorage.getItem('wb_reading_books_migrated')&&(hasPaper||hasEbook)){
var books=[];
(oldPaper||[]).forEach(function(b){books.push(Object.assign({type:'paper'},b))});
(oldEbook||[]).forEach(function(b){books.push(Object.assign({type:'ebook'},b))});
data.books=books;Store.set('books',books);
localStorage.setItem('wb_reading_books_migrated','1');
}
}
migrateBooks();
let state={
tab:'home',
bookId:null,
search:'',
sort:'desc',
noteSort:'desc',
editMode:false,
selectedIds:{},
};
let modalCb=null;
let currentRec=null;
let longPressTimer=null;
function startLongPress(cb){
longPressTimer=setTimeout(function(){
state.editMode=true;
state.selectedIds={};
cb();
},600);
}
function cancelLongPress(){clearTimeout(longPressTimer);longPressTimer=null}
function toggleSelect(id){
state.selectedIds[id]=!state.selectedIds[id];
if(!state.selectedIds[id])delete state.selectedIds[id];
render();
}
function selectAll(ids){
var allSelected=ids.every(function(id){return state.selectedIds[id]});
if(allSelected){ids.forEach(function(id){delete state.selectedIds[id]})}
else{ids.forEach(function(id){state.selectedIds[id]=true})}
render();
}
function exitEditMode(){state.editMode=false;state.selectedIds={};render()}
function getSelectedIds(){return Object.keys(state.selectedIds).filter(function(k){return state.selectedIds[k]})}
// ========== Init ==========
function init(){
// Check first run
if(!localStorage.getItem('wb_reading_initialized')){
loadSampleData();
localStorage.setItem('wb_reading_initialized','1');
}
buildNav();
bindEvents();
render();
}
function buildNav(){
document.getElementById('sidebar-nav').innerHTML=NAV_ITEMS.map(i=>'<div class="nav-item" data-action="switch-tab" data-tab="'+i.tab+'">'+i.icon+'<span>'+i.label+'</span></div>').join('');
document.getElementById('topbar-nav').innerHTML=NAV_ITEMS.map(i=>'<div class="topbar-item" data-action="switch-tab" data-tab="'+i.tab+'">'+i.icon+'<span>'+i.label+'</span></div>').join('');
document.getElementById('tabbar-nav').innerHTML=NAV_ITEMS.map(i=>'<div class="tab-item" data-action="switch-tab" data-tab="'+i.tab+'">'+i.icon+'<span class="tab-label">'+i.short+'</span></div>').join('');
updateNavActive();
}
function updateNavActive(){
document.querySelectorAll('[data-action="switch-tab"]').forEach(function(el){el.classList.toggle('active',el.dataset.tab===state.tab)});
}
function bindEvents(){
document.addEventListener('click',function(e){
var el=e.target.closest('[data-action]');if(!el)return;
var a=el.dataset.action,id=el.dataset.id,bid=el.dataset.bid,nid=el.dataset.nid,eid=el.dataset.eid;
var handlers={
'switch-tab':function(){state.tab=el.dataset.tab;state.bookId=null;state.editMode=false;state.selectedIds={};updateNavActive();render();window.scrollTo(0,0)},
'close-modal':closeModal,'export-data':exportData,
'trigger-import':function(){document.getElementById('import-file').click()},
'create-book':createBook,'open-book':function(){state.tab='books';state.bookId=id;state.editMode=false;state.selectedIds={};render()},
'back-books':function(){state.bookId=null;state.editMode=false;state.selectedIds={};render()},'add-book-note':function(){addBookNote(bid||id)},
'delete-book':function(){deleteBook(id)},'delete-book-note':function(){deleteBookNote(bid,nid)},
'finish-book':function(){finishBook(id)},
'edit-book':function(){editBook(id)},
'add-video':addVideoNote,'delete-video':function(){deleteVideoNote(id)},
'edit-video':function(){editVideoNote(id)},
'add-diary':addDiary,'delete-diary':function(){deleteDiary(id)},
'edit-diary':function(){editDiary(id)},
'add-memo-item':addMemoItem,'toggle-memo':function(){toggleMemo(id)},'delete-memo':function(){deleteMemo(id)},
'add-english':addEnglish,'delete-english':function(){deleteEnglish(id)},'master-english':function(){masterEnglish(id)},
'edit-english':function(){editEnglish(id)},
'toggle-select':function(){toggleSelect(id)},
'select-all':function(){selectAll(el.dataset.ids.split(',').filter(Boolean))},
'exit-edit':exitEditMode,
'delete-selected':function(){deleteSelected(el.dataset.cat)},
'add-reflection':function(){addReflectionToNote(bid||id,nid)},
'clear-sample':clearSampleData
};
if(handlers[a])handlers[a]();
});
document.getElementById('import-file').addEventListener('change',function(e){if(e.target.files[0])importData(e.target.files[0]);e.target.value=''});
document.getElementById('modal-save').addEventListener('click',function(){if(modalCb){var r=modalCb();if(r===false)return}closeModal()});
// Long press for edit mode (touch + mouse)
document.addEventListener('touchstart',function(e){
var el=e.target.closest('[data-longpress]');
if(!el)return;
var cat=el.dataset.longpress,id=el.dataset.id;
startLongPress(function(){enterEditMode(cat,id)});
},{passive:true});
document.addEventListener('touchend',cancelLongPress,{passive:true});
document.addEventListener('touchmove',cancelLongPress,{passive:true});
document.addEventListener('mousedown',function(e){
var el=e.target.closest('[data-longpress]');
if(!el)return;
var cat=el.dataset.longpress,id=el.dataset.id;
startLongPress(function(){enterEditMode(cat,id)});
});
document.addEventListener('mouseup',cancelLongPress);
document.addEventListener('mouseleave',cancelLongPress);
}
function enterEditMode(cat,id){
if(state.editMode)return;
state.editMode=true;
state.selectedIds={};
if(id)state.selectedIds[id]=true;
if(cat==='books')state.tab='books';
else if(cat==='video')state.tab='video';
else if(cat==='diary')state.tab='diary';
else if(cat==='english')state.tab='english';
updateNavActive();
render();
toast('已进入编辑模式,选择要操作的内容');
}
function render(){
var c=document.getElementById('content');
if(state.tab==='home')c.innerHTML=renderHome();
else if(state.tab==='books')c.innerHTML=state.bookId?renderBookDetail(state.bookId):renderBooks();
else if(state.tab==='video')c.innerHTML=renderVideo();
else if(state.tab==='diary')c.innerHTML=renderDiary();
else if(state.tab==='english')c.innerHTML=renderEnglish();
updateNavActive();
}
// === HOME ===
function renderHome(){
var h='<div class="section-header"><div><div class="section-title">读书笔记工作台</div><div class="section-sub">记录阅读中的每一段感悟</div></div></div>';
// Search
h+='<div class="search-bar">'+SVG.search+'<input type="text" id="home-search" placeholder="搜索书名、笔记、日记、单词..." value="'+esc(state.search)+'" oninput="state.search=this.value;debounceSearch()"></div>';
// Search results
if(state.search.trim()){
h+='<div style="margin-bottom:20px"><div class="section-title" style="font-size:16px;margin-bottom:10px">搜索结果</div>';
var results=searchAll(state.search);
if(results.length===0)h+='<div class="empty"><div>'+SVG.emptyBox+'</div><div class="empty-text">未找到相关内容</div></div>';
else{results.forEach(function(r){
h+='<div class="note-item" style="cursor:pointer" data-action="'+r.action+'" data-id="'+r.id+'" data-bid="'+(r.bid||'')+'"><div class="note-body"><div style="display:flex;gap:6px;margin-bottom:4px"><span class="tag">'+r.cat+'</span></div><div style="font-weight:600;font-size:14px">'+esc(r.title)+'</div><div style="font-size:13px;color:var(--t3);margin-top:2px">'+esc(r.sub)+'</div></div></div>';
})}
h+='</div>';
}
// Today section
h+=renderTodaySection();
// Summary
h+='<div class="section-title" style="font-size:16px;margin-bottom:10px">分类汇总</div>';
h+='<div class="summary-grid">';
var booksN=data.books.length,booksFin=data.books.filter(function(b){return b.status==='finished'}).length;
var paperN=data.books.filter(function(b){return b.type!=='ebook'}).length;
var ebookN=data.books.filter(function(b){return b.type==='ebook'}).length;
var vc=data.video.length,dc=data.diary.length,wc=data.english.length;
h+=summaryCard('书籍',booksN,paperN+' 纸 / '+ebookN+' 电 · '+booksFin+' 读完',SVG.paper,'#2a9d78','books');
h+=summaryCard('视频笔记',vc,vc+' 条',SVG.video,'#d4a04a','video');
h+=summaryCard('日记',dc,dc+' 篇',SVG.diary,'#3aafa9','diary');
h+=summaryCard('英语',wc,wc+' 条',SVG.english,'#c25450','english');
h+='</div>';
// Sort + Recent
h+='<div class="section-header" style="margin-top:20px"><div class="section-title" style="font-size:16px">最近记录</div>';
h+='<div class="sort-toggle"><button class="sort-btn '+(state.sort==='desc'?'active':'')+'" data-action="sort-desc" onclick="state.sort=\'desc\';render()">最新</button><button class="sort-btn '+(state.sort==='asc'?'active':'')+'" onclick="state.sort=\'asc\';render()">最早</button></div></div>';
var recent=getRecent();
if(recent.length===0)h+='<div class="empty"><div>'+SVG.emptyBox+'</div><div class="empty-text">还没有记录,去添加吧</div></div>';
else recent.forEach(function(r){h+=recentItemHTML(r)});
// Footer actions
h+='<div style="margin-top:20px;display:flex;gap:8px;flex-wrap:wrap">';
h+='<button class="btn btn-outline btn-sm" data-action="clear-sample">清空示例数据</button>';
h+='</div>';
return h;
}
function summaryCard(label,num,sub,icon,color,tab){
return '<div class="summary-card" data-action="switch-tab" data-tab="'+tab+'"><div class="summary-icon" style="background:'+color+'15;color:'+color+'">'+icon+'</div><div class="summary-info"><div class="summary-num">'+num+'</div><div class="summary-label">'+label+'</div><div class="summary-sub">'+sub+'</div></div></div>';
}
function renderTodaySection(){
var h='<div class="today-section"><div class="today-title">'+SVG.alert+' 今天要处理</div>';
var hasItem=false;
// Overdue books (paper type only has reading status/goal)
var today=new Date().toISOString().slice(0,10);
data.books.filter(function(b){return b.type!=='ebook'}).forEach(function(b){
if(b.status==='reading'&&b.goalDate&&b.goalDate<today){
hasItem=true;
h+='<div class="today-item overdue"><div class="today-item-content"><div class="today-item-title">'+esc(b.title)+'</div><div class="today-item-meta">阅读目标已逾期 · '+esc(b.author||'')+'</div></div><button class="btn btn-primary btn-sm" data-action="open-book" data-id="'+b.id+'">继续</button></div>';
}
});
// Books in progress (not overdue)
data.books.filter(function(b){return b.type!=='ebook'}).forEach(function(b){
if(b.status==='reading'&&(!b.goalDate||b.goalDate>=today)){
hasItem=true;
h+='<div class="today-item"><div class="today-item-content"><div class="today-item-title">'+esc(b.title)+'</div><div class="today-item-meta">阅读中 · '+(b.notes.length)+' 条笔记</div></div><button class="btn btn-outline btn-sm" data-action="open-book" data-id="'+b.id+'">继续</button></div>';
}
});
// Today diary
var todayDiary=data.diary.find(function(d){return d.date===today});
if(!todayDiary){
hasItem=true;
h+='<div class="today-item"><div class="today-item-content"><div class="today-item-title">今日日记</div><div class="today-item-meta">尚未记录今天的心情</div></div><button class="btn btn-primary btn-sm" data-action="add-diary">写日记</button></div>';
}else{
h+='<div class="today-item"><div class="today-item-content"><div class="today-item-title" style="color:var(--success)">今日日记已完成</div><div class="today-item-meta">'+esc(MOODS[todayDiary.mood]?MOODS[todayDiary.mood].label:'')+' · '+esc(todayDiary.location||'')+'</div></div></div>';
}
// Memo section (synced to diary on complete)
h+=renderMemoSection();
h+='</div>';
return h;
}
function searchAll(q){
q=q.toLowerCase();var results=[];
data.books.forEach(function(b){
var catLabel=b.type==='ebook'?'电子书':'纸质书';
if(b.title.toLowerCase().indexOf(q)>=0||(b.author||'').toLowerCase().indexOf(q)>=0)results.push({cat:catLabel,title:b.title,sub:b.author||'',action:'open-book',id:b.id});
b.notes.forEach(function(n){
var txt=n.originalText||n.quote||'';
if(txt.toLowerCase().indexOf(q)>=0||(n.reflection||'').toLowerCase().indexOf(q)>=0)results.push({cat:catLabel+'笔记',title:b.title,sub:txt.slice(0,60),action:'open-book',id:b.id});
});
});
data.memo.forEach(function(m){
if((m.text||'').toLowerCase().indexOf(q)>=0)results.push({cat:'备忘',title:m.text,sub:m.done?'已完成':'待办',action:'switch-tab',id:'home'});
});
data.video.forEach(function(v){
if((v.extractedText||'').toLowerCase().indexOf(q)>=0||(v.reflection||'').toLowerCase().indexOf(q)>=0||(v.title||'').toLowerCase().indexOf(q)>=0)results.push({cat:'视频笔记',title:v.title||'视频笔记',sub:(v.extractedText||'').slice(0,60),action:'switch-tab',id:'video'});
});
data.diary.forEach(function(d){
if((d.content||'').toLowerCase().indexOf(q)>=0||(d.location||'').toLowerCase().indexOf(q)>=0)results.push({cat:'日记',title:fmt(d.date),sub:(d.content||'').slice(0,60),action:'switch-tab',id:'diary'});
});
data.english.forEach(function(w){
var title=w.type==='sentence'?(w.sentence||''):(w.word||'');
var sub=w.type==='sentence'?(w.translation||''):(w.meaning||'');
if(title.toLowerCase().indexOf(q)>=0||sub.toLowerCase().indexOf(q)>=0)results.push({cat:w.type==='sentence'?'口语句子':'单词',title:title,sub:sub,action:'switch-tab',id:'english'});
});
return results;
}
function getRecent(){
var items=[];
data.books.forEach(function(b){b.notes.forEach(function(n){
var sub=n.originalText||n.quote||'';
items.push({cat:b.type==='ebook'?'电子书':'纸质书',title:b.title,sub:sub,date:n.createdAt,action:'open-book',id:b.id,watermark:n.hasWatermark?'fmt':'',wtime:n.watermarkTime,wloc:n.watermarkLocation})})});
data.memo.forEach(function(m){items.push({cat:'备忘',title:m.text,sub:m.done?'已完成 · '+fmt(m.completedAt,true):'待办',date:m.done?m.completedAt:m.createdAt,action:'switch-tab',id:'home'})});
data.video.forEach(function(v){items.push({cat:'视频',title:v.title||'视频笔记',sub:v.extractedText||'',date:v.createdAt,action:'switch-tab',id:'video'})});
data.diary.forEach(function(d){items.push({cat:'日记',title:fmt(d.date),sub:d.content||'',date:d.createdAt,action:'switch-tab',id:'diary'})});
data.english.forEach(function(w){items.push({cat:w.type==='sentence'?'口语':'单词',title:w.type==='sentence'?(w.sentence||''):(w.word||''),sub:w.type==='sentence'?(w.translation||''):(w.meaning||''),date:w.createdAt,action:'switch-tab',id:'english'})});
items.sort(function(a,b){return state.sort==='desc'?new Date(b.date)-new Date(a.date):new Date(a.date)-new Date(b.date)});
return items.slice(0,20);
}
function recentItemHTML(r){
var h='<div class="note-item" style="cursor:pointer" data-action="'+r.action+'" data-id="'+r.id+'"><div class="note-body">';
h+='<div style="display:flex;gap:6px;margin-bottom:4px;align-items:center"><span class="tag">'+r.cat+'</span><span style="font-size:11px;color:var(--t3)">'+fmt(r.date,true)+'</span></div>';
h+='<div style="font-weight:600;font-size:14px">'+esc(r.title)+'</div>';
if(r.sub)h+='<div style="font-size:13px;color:var(--t2);margin-top:2px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden">'+esc(r.sub)+'</div>';
if(r.watermark)h+='<div class="note-watermark">'+SVG.clock+' '+esc(r.wtime||'')+' '+SVG.pin+' '+esc(r.wloc||'')+'</div>';
h+='</div></div>';
return h;
}
var searchTimer=null;
function debounceSearch(){clearTimeout(searchTimer);searchTimer=setTimeout(function(){render()},300)}
// === BOOKS (unified: paper + ebook) ===
function renderBooks(){
var h='<div class="section-header"><div><div class="section-title">书籍</div><div class="section-sub">纸质书与电子书,一处管理</div></div><button class="btn btn-primary" data-action="create-book">'+SVG.plus+'新建书籍</button></div>';
if(data.books.length===0){h+='<div class="empty">'+SVG.emptyBox+'<div class="empty-text">还没有书籍,点击「新建书籍」开始</div></div>';return h}
var today=new Date().toISOString().slice(0,10);
var allIds=data.books.map(function(b){return b.id});
if(state.editMode)h+=editBarHTML('books',allIds);
h+='<div class="grid grid-3">';
data.books.forEach(function(b){
var isE=b.type==='ebook';var progress=b.notes.length;
var sel=state.editMode&&state.selectedIds[b.id];
if(state.editMode){
h+='<div class="card selectable'+(sel?' selected':'')+'" data-action="toggle-select" data-id="'+b.id+'" data-longpress="books" style="position:relative">';
}else{
h+='<div class="card" data-action="open-book" data-id="'+b.id+'" data-longpress="books">';
}
h+='<div class="card-cover">'+(b.cover?'<img src="'+b.cover+'">':(isE?SVG.ebook:SVG.bookCover))+'</div>';
h+='<div class="card-body"><div class="card-title">'+esc(b.title)+'</div><div class="card-meta">'+esc(b.author||'')+'</div>';
h+='<div class="card-meta" style="margin-top:6px">';
h+='<span class="tag'+(isE?' tag-info':'')+'">'+(isE?SVG.ebook:SVG.paper)+' '+(isE?'电子书':'纸质书')+'</span>';
if(b.status==='finished')h+=' <span class="tag tag-success">'+SVG.check+' 已读完</span>';
h+=' <span style="font-size:11px;color:var(--t3)">'+progress+' 条'+(isE?'摘录':'笔记')+'</span>';
if(b.status==='reading'&&b.goalDate&&b.goalDate<today)h+=' <span class="tag tag-danger">已逾期</span>';
h+='</div>';
if(state.editMode)h+='<input type="checkbox" class="select-cb" style="position:absolute;top:8px;right:8px" '+(sel?'checked':'')+' onclick="event.stopPropagation();toggleSelect(\''+b.id+'\')">';
h+='</div></div>';
});
h+='</div>';
if(!state.editMode&&data.books.length>0){
h+='<div style="margin-top:12px"><button class="btn btn-outline btn-sm" data-action="" onclick="enterEditMode(\'books\',null)">'+SVG.edit+'编辑模式</button></div>';
}
return h;
}
function renderBookDetail(id){
var b=data.books.find(function(x){return x.id===id});if(!b)return'<div class="empty">书籍不存在</div>';
var isE=b.type==='ebook';
var h='<div class="back-btn" data-action="back-books">'+SVG.back+'返回列表</div>';
h+='<div class="section-header"><div><div class="section-title">'+esc(b.title)+'</div><div class="section-sub"><span class="tag'+(isE?' tag-info':'')+'">'+(isE?'电子书':'纸质书')+'</span> '+esc(b.author||'')+' · '+b.notes.length+' 条'+(isE?'摘录':'笔记')+'</div></div>';
h+='<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center"><button class="btn btn-outline btn-sm" data-action="add-book-note" data-bid="'+b.id+'">'+SVG.plus+'添加'+(isE?'摘录':'笔记')+'</button>';
if(!isE&&b.status!=='finished')h+='<button class="btn btn-outline btn-sm" data-action="finish-book" data-id="'+b.id+'">'+SVG.check+'读完</button>';
h+='<button class="btn btn-outline btn-sm" data-action="edit-book" data-id="'+b.id+'">'+SVG.edit+'编辑书籍</button>';
if(!state.editMode&&b.notes.length>0)h+='<button class="btn btn-outline btn-sm" onclick="state.editMode=true;state.selectedIds={};render()">'+SVG.edit+'编辑笔记</button>';
h+='</div>';
// Sort options
if(b.notes.length>0){
h+='<div style="margin-top:12px;display:flex;align-items:center;gap:8px;flex-wrap:wrap"><span style="font-size:13px;color:var(--t2)">排序:</span>';
h+='<div class="sort-toggle"><button class="sort-btn '+(state.noteSort==='desc'?'active':'')+'" onclick="state.noteSort=\'desc\';render()">最新优先</button><button class="sort-btn '+(state.noteSort==='asc'?'active':'')+'" onclick="state.noteSort=\'asc\';render()">最早优先</button>'+(isE?'<button class="sort-btn '+(state.noteSort==='chapter'?'active':'')+'" onclick="state.noteSort=\'chapter\';render()">按章节</button>':'')+'</div>';
h+='</div>';
}
if(b.notes.length===0){h+='<div class="empty">'+SVG.emptyBox+'<div class="empty-text">还没有'+(isE?'摘录':'笔记')+'</div></div>';return h}
// Sort notes
var notes=b.notes.slice();
if(state.noteSort==='asc')notes.sort(function(a,b2){return new Date(a.createdAt)-new Date(b2.createdAt)});
else if(state.noteSort==='desc')notes.sort(function(a,b2){return new Date(b2.createdAt)-new Date(a.createdAt)});
else if(state.noteSort==='chapter'){
notes.sort(function(a,b2){
var ca=a.chapter||'未分类',cb=b2.chapter||'未分类';
if(ca!==cb)return ca.localeCompare(cb,'zh');
return new Date(a.createdAt)-new Date(b2.createdAt);
});
}
// Edit bar
if(state.editMode){
var allNoteIds=notes.map(function(n){return n.id});
h+=editBarHTML('booknotes',allNoteIds);
}
// Render notes (group by chapter if chapter sort)
if(state.noteSort==='chapter'){
var curChapter='';
notes.forEach(function(n){
var ch=n.chapter||'未分类';
if(ch!==curChapter){curChapter=ch;h+='<div style="margin:16px 0 8px;font-size:14px;font-weight:600;color:var(--accent);padding-left:4px;border-left:3px solid var(--accent-l);padding-left:10px">'+esc(ch)+'</div>'}
h+=bookNoteHTML(b,n,isE);
});
}else{
notes.forEach(function(n){h+=bookNoteHTML(b,n,isE)});
}
return h;
}
function bookNoteHTML(b,n,isE){
var sel=state.editMode&&state.selectedIds[n.id];
var action=state.editMode?'toggle-select':'';
var extraAttrs=state.editMode?'data-id="'+n.id+'"':'data-longpress="booknotes" data-id="'+n.id+'" data-bid="'+b.id+'"';
var h='<div class="note-item selectable'+(sel?' selected':'')+'" '+(action?'data-action="'+action+'"':'')+' '+extraAttrs+'><div class="note-body">';
if(n.photo)h+='<img class="note-photo" src="'+n.photo+'" style="margin-bottom:10px;border-radius:8px">';
if(n.chapter&&state.noteSort!=='chapter')h+='<div style="margin-bottom:6px"><span class="tag tag-info">'+esc(n.chapter)+'</span></div>';
var txt=n.originalText||n.quote||'';
if(txt)h+='<div class="note-text">'+esc(txt)+'</div>';
// Render reflections (support multiple)
var reflections=n.reflections||[];
if(n.reflection&&!reflections.length)reflections=[{text:n.reflection,createdAt:n.createdAt}];
reflections.forEach(function(r,i){
h+='<div class="note-reflection">'+esc(r.text);
if(reflections.length>1)h+='<div style="font-size:11px;color:var(--t3);margin-top:4px">感悟 '+(i+1)+' · '+fmt(r.createdAt,true)+'</div>';
h+='</div>';
});
if(n.hasWatermark)h+='<div class="note-watermark">'+SVG.clock+' '+esc(n.watermarkTime||'')+' '+SVG.pin+' '+esc(n.watermarkLocation||'')+'</div>';
h+='<div class="note-meta-row"><span style="font-size:11px;color:var(--t3)">'+fmt(n.createdAt,true)+'</span>';
if(!state.editMode){
h+='<div style="display:flex;gap:4px">';
h+='<button class="add-reflection-btn" data-action="add-reflection" data-bid="'+b.id+'" data-nid="'+n.id+'" onclick="event.stopPropagation()">'+SVG.plus+'追加感悟</button>';
h+='<button class="btn btn-danger btn-sm" data-action="delete-book-note" data-bid="'+b.id+'" data-nid="'+n.id+'" onclick="event.stopPropagation()">'+SVG.trash+'</button>';
h+='</div>';
}
h+='</div></div></div>';
return h;
}
function addReflectionToNote(bid,nid){
var b=data.books.find(function(x){return x.id===bid});if(!b)return;
var n=b.notes.find(function(x){return x.id===nid});if(!n)return;
openModal('追加感悟','<div class="form-group"><label class="form-label">新增感悟</label><textarea class="textarea" id="ar-text" placeholder="写下你新的感悟和思考" style="min-height:100px"></textarea><button class="voice-btn" data-voice="ar-text" onclick="toggleVoice(\'ar-text\',this)" style="margin-top:6px">'+SVG.mic+' 语音输入</button></div>',{saveText:'追加',onSave:function(){
var text=document.getElementById('ar-text').value.trim();
if(!text){toast('请输入感悟内容');return false}
if(!n.reflections)n.reflections=[];
if(n.reflection&&!n.reflections.length)n.reflections=[{text:n.reflection,createdAt:n.createdAt}];
n.reflections.push({text:text,createdAt:new Date().toISOString()});
Store.set('books',data.books);render();toast('感悟已追加');
}});
}
function createBook(){
openModal('新建书籍','<div class="form-group"><label class="form-label">书籍类型 *</label><div class="mood-grid" id="bk-type-grid"><div class="mood-option selected" data-bktype="paper" onclick="selectBookType(\'paper\')" style="color:var(--accent)">'+SVG.paper+'<span class="mood-label">纸质书</span></div><div class="mood-option" data-bktype="ebook" onclick="selectBookType(\'ebook\')" style="color:var(--info)">'+SVG.ebook+'<span class="mood-label">电子书</span></div></div><input type="hidden" id="bk-type" value="paper"></div><div class="form-group"><label class="form-label">书名 *</label><input type="text" class="input" id="bk-title" placeholder="请输入书名"></div><div class="form-group"><label class="form-label">作者</label><input type="text" class="input" id="bk-author" placeholder="请输入作者"></div><div class="form-group"><label class="form-label">封面(可选,纸质书)</label><div class="photo-upload" onclick="document.getElementById(\'bk-cover\').click()">'+SVG.camera+'<div style="font-size:13px;color:var(--t3)">点击上传封面</div><input type="file" id="bk-cover" accept="image/*" style="display:none" onchange="previewCover(this)"></div><div id="cover-preview"></div></div><div class="form-group"><label class="form-label">阅读目标日期(可选,纸质书)</label><input type="date" class="input" id="bk-goal"></div>',{saveText:'创建',onSave:function(){
var title=document.getElementById('bk-title').value.trim();
if(!title){toast('请输入书名');return false}
var type=document.getElementById('bk-type').value;
var book={id:uid(),type:type,title:title,author:document.getElementById('bk-author').value.trim(),cover:null,status:type==='ebook'?'':'reading',goalDate:document.getElementById('bk-goal').value,createdAt:new Date().toISOString(),notes:[]};
var ci=document.getElementById('bk-cover');
if(ci.files&&ci.files[0]){handlePhoto(ci.files[0],function(b64){book.cover=b64;data.books.unshift(book);Store.set('books',data.books);render();confetti();toast('书籍创建成功')})}
else{data.books.unshift(book);Store.set('books',data.books);render();confetti();toast('书籍创建成功')}
}});
}
function selectBookType(t){
document.getElementById('bk-type').value=t;
document.querySelectorAll('[data-bktype]').forEach(function(el){el.classList.toggle('selected',el.dataset.bktype===t)});
}
function addBookNote(bid){
var b=data.books.find(function(x){return x.id===bid});if(!b)return false;
var isE=b.type==='ebook';var fields='';
if(isE){
fields='<div class="form-group"><label class="form-label">章节(可选)</label><input type="text" class="input" id="bn-chapter" placeholder="如:第三章"></div><div class="form-group"><label class="form-label">原文摘录 *</label><textarea class="textarea" id="bn-quote" placeholder="摘录你喜欢的原文" style="min-height:100px"></textarea></div>';
}else{
fields='<div class="form-group"><label class="form-label">拍照上传</label><div class="photo-upload" onclick="document.getElementById(\'bn-photo\').click()">'+SVG.camera+'<div style="font-size:13px;color:var(--t3)">点击拍照或选择图片</div><input type="file" id="bn-photo" accept="image/*" capture="environment" style="display:none" onchange="previewNotePhoto(this)"></div><div id="photo-preview"></div></div><div class="form-group"><label class="form-label">原文内容</label><textarea class="textarea" id="bn-text" placeholder="输入或口述照片中的文字内容" style="min-height:100px"></textarea><button class="voice-btn" data-voice="bn-text" onclick="toggleVoice(\'bn-text\',this)" style="margin-top:6px">'+SVG.mic+' 语音输入</button></div>';
}
openModal(isE?'添加摘录':'添加读书笔记',fields+'<div class="form-group"><label class="form-label">我的感悟</label><textarea class="textarea" id="bn-reflection" placeholder="写下你的感悟和思考" style="min-height:100px"></textarea><button class="voice-btn" data-voice="bn-reflection" onclick="toggleVoice(\'bn-reflection\',this)" style="margin-top:6px">'+SVG.mic+' 语音感悟</button></div>'+(isE?'':'<label class="checkbox-row"><input type="checkbox" id="bn-watermark" onchange="toggleWatermark(this)"><span>添加时间地点水印</span></label><div id="wm-fields" style="display:none;margin-top:8px"><input type="text" class="input" id="bn-location" placeholder="位置名称(如:书房、咖啡馆)"></div>'),{saveText:isE?'保存摘录':'保存笔记',onSave:function(){
var note={id:uid(),photo:null,originalText:'',quote:'',reflection:document.getElementById('bn-reflection').value,chapter:'',hasWatermark:false,watermarkTime:'',watermarkLocation:'',createdAt:new Date().toISOString()};
if(isE){
var q=document.getElementById('bn-quote').value.trim();
if(!q){toast('请输入原文摘录');return false}
note.quote=q;note.chapter=document.getElementById('bn-chapter').value;
}else{
note.originalText=document.getElementById('bn-text').value;
note.hasWatermark=document.getElementById('bn-watermark').checked;
if(note.hasWatermark){note.watermarkTime=fmt(new Date(),true);note.watermarkLocation=document.getElementById('bn-location').value||'未记录'}
}
var ci=document.getElementById('bn-photo');
if(ci&&ci.files&&ci.files[0]){handlePhoto(ci.files[0],function(b64){note.photo=b64;b.notes.unshift(note);Store.set('books',data.books);render();confetti();toast(isE?'摘录已保存':'笔记已保存')})}
else{b.notes.unshift(note);Store.set('books',data.books);render();confetti();toast(isE?'摘录已保存':'笔记已保存')}
}});
}
function deleteBook(id){
openModal('确认删除','<p style="font-size:14px;color:var(--t2)">确定要删除这本书及其所有笔记吗?此操作不可撤销。</p>',{saveText:'确认删除',onSave:function(){data.books=data.books.filter(function(b){return b.id!==id});Store.set('books',data.books);state.bookId=null;render();toast('已删除')}});
}
function deleteBookNote(bid,nid){
var b=data.books.find(function(x){return x.id===bid});if(!b)return;
b.notes=b.notes.filter(function(n){return n.id!==nid});Store.set('books',data.books);render();toast('已删除');
}
function finishBook(id){
var b=data.books.find(function(x){return x.id===id});if(!b)return;
b.status='finished';Store.set('books',data.books);render();confetti();toast('恭喜读完一本书!');
}
// === MEMO (今天要处理备忘录,同步日记) ===
function renderMemoSection(){
var open=data.memo.filter(function(m){return !m.done});
var done=data.memo.filter(function(m){return m.done});
var h='<div style="margin-top:16px;padding-top:14px;border-top:1px dashed var(--border)">';
h+='<div class="today-title" style="margin-bottom:8px">'+SVG.memo+' 备忘录 <span style="font-size:12px;color:var(--t3);font-weight:400">完成后自动同步到日记</span></div>';
h+='<div style="display:flex;gap:8px;margin-bottom:10px"><input type="text" class="input" id="memo-new" placeholder="新建备忘事项,回车添加" style="flex:1" onkeydown="if(event.key===\'Enter\')addMemoFromInput()"><button class="btn btn-primary btn-sm" data-action="add-memo-item">'+SVG.plus+'添加</button></div>';
open.forEach(function(m){
h+='<div class="today-item"><div style="display:flex;align-items:center;gap:10px;flex:1;min-width:0"><input type="checkbox" data-action="toggle-memo" data-id="'+m.id+'" style="width:20px;height:20px;accent-color:var(--accent);flex-shrink:0;cursor:pointer"><span class="today-item-title">'+esc(m.text)+'</span></div><button class="btn btn-danger btn-sm" data-action="delete-memo" data-id="'+m.id+'">'+SVG.trash+'</button></div>';
});
done.slice(0,5).forEach(function(m){
h+='<div class="today-item" style="opacity:.75"><div class="today-item-content"><div class="today-item-title" style="text-decoration:line-through;color:var(--t3)">'+esc(m.text)+'</div><div class="today-item-meta" style="color:var(--success)">'+SVG.check+' 完成于 '+fmt(m.completedAt,true)+'</div></div><button class="btn btn-danger btn-sm" data-action="delete-memo" data-id="'+m.id+'">'+SVG.trash+'</button></div>';
});
if(data.memo.length===0)h+='<div class="empty-text" style="padding:6px 0 10px">暂无备忘事项</div>';
h+='</div>';
return h;
}
function addMemoFromInput(){
var el=document.getElementById('memo-new');
if(!el)return;var txt=el.value.trim();
if(!txt){toast('请输入备忘内容');return}
data.memo.unshift({id:uid(),text:txt,done:false,completedAt:null,createdAt:new Date().toISOString()});
Store.set('memo',data.memo);render();toast('备忘已添加');
}
function addMemoItem(){addMemoFromInput()}
function toggleMemo(id){
var m=data.memo.find(function(x){return x.id===id});if(!m)return;
if(!m.done){m.done=true;m.completedAt=new Date().toISOString();Store.set('memo',data.memo);render();confetti();toast('已完成,已同步到日记')}
else{m.done=false;m.completedAt=null;Store.set('memo',data.memo);render()}
}
function deleteMemo(id){
data.memo=data.memo.filter(function(m){return m.id!==id});Store.set('memo',data.memo);render();toast('已删除');
}
// === VIDEO ===
function renderVideo(){
var h='<div class="section-header"><div><div class="section-title">视频转文字</div><div class="section-sub">记录视频中的精彩内容</div></div><button class="btn btn-primary" data-action="add-video">'+SVG.plus+'添加视频笔记</button></div>';
if(data.video.length===0){h+='<div class="empty">'+SVG.emptyBox+'<div class="empty-text">还没有视频笔记</div></div>';return h}
var sorted=data.video.slice().sort(function(a,b){return state.sort==='desc'?new Date(b.createdAt)-new Date(a.createdAt):new Date(a.createdAt)-new Date(b.createdAt)});
var allIds=sorted.map(function(v){return v.id});
if(state.editMode)h+=editBarHTML('video',allIds);
sorted.forEach(function(v){
var sel=state.editMode&&state.selectedIds[v.id];
var action=state.editMode?'toggle-select':'edit-video';
var extraAttrs=state.editMode?'data-id="'+v.id+'"':'data-id="'+v.id+'" data-longpress="video"';
h+='<div class="note-item selectable'+(sel?' selected':'')+'" data-action="'+action+'" '+extraAttrs+'><div class="note-body">';
if(v.title)h+='<div style="font-weight:600;font-size:14px;margin-bottom:6px">'+esc(v.title)+'</div>';
if(v.videoUrl)h+='<div style="font-size:12px;color:var(--info);margin-bottom:6px;word-break:break-all">'+esc(v.videoUrl)+'</div>';
if(v.extractedText)h+='<div class="note-text">'+esc(v.extractedText)+'</div>';
if(v.reflection)h+='<div class="note-reflection">'+esc(v.reflection)+'</div>';
if(v.hasWatermark)h+='<div class="note-watermark">'+SVG.clock+' '+esc(v.watermarkTime||'')+' '+SVG.pin+' '+esc(v.watermarkLocation||'')+'</div>';
h+='<div class="note-meta-row"><span style="font-size:11px;color:var(--t3)">'+fmt(v.createdAt,true)+'</span>';
if(!state.editMode)h+='<button class="btn btn-danger btn-sm" data-action="delete-video" data-id="'+v.id+'" onclick="event.stopPropagation()">'+SVG.trash+'</button>';
h+='</div></div></div>';
});
if(!state.editMode)h+='<div style="margin-top:12px"><button class="btn btn-outline btn-sm" onclick="enterEditMode(\'video\',null)">'+SVG.edit+'编辑模式</button></div>';
return h;
}
function addVideoNote(){
openModal('添加视频笔记','<div class="form-group"><label class="form-label">标题(可选)</label><input type="text" class="input" id="vn-title" placeholder="给这条笔记起个名字"></div><div class="form-group"><label class="form-label">视频链接</label><input type="text" class="input" id="vn-url" placeholder="粘贴视频链接"></div><div class="form-group"><label class="form-label">视频中的文字内容</label><textarea class="textarea" id="vn-text" placeholder="粘贴或输入视频中提取的文字" style="min-height:100px"></textarea><button class="voice-btn" data-voice="vn-text" onclick="toggleVoice(\'vn-text\',this)" style="margin-top:6px">'+SVG.mic+' 语音输入</button></div><div class="form-group"><label class="form-label">我的感悟</label><textarea class="textarea" id="vn-reflection" placeholder="写下你的感悟" style="min-height:100px"></textarea></div><label class="checkbox-row"><input type="checkbox" id="vn-watermark" onchange="toggleWatermark(this)"><span>添加时间地点水印</span></label><div id="wm-fields" style="display:none;margin-top:8px"><input type="text" class="input" id="vn-location" placeholder="位置名称"></div>',{saveText:'保存',onSave:function(){
var note={id:uid(),title:document.getElementById('vn-title').value,videoUrl:document.getElementById('vn-url').value,extractedText:document.getElementById('vn-text').value,reflection:document.getElementById('vn-reflection').value,hasWatermark:document.getElementById('vn-watermark').checked,watermarkTime:'',watermarkLocation:'',createdAt:new Date().toISOString()};
if(note.hasWatermark){note.watermarkTime=fmt(new Date(),true);note.watermarkLocation=document.getElementById('vn-location').value||'未记录'}
data.video.unshift(note);Store.set('video',data.video);render();confetti();toast('视频笔记已保存');
}});
}
function deleteVideoNote(id){
openModal('确认删除','<p style="font-size:14px;color:var(--t2)">确定要删除这条视频笔记吗?</p>',{saveText:'确认删除',onSave:function(){data.video=data.video.filter(function(v){return v.id!==id});Store.set('video',data.video);render();toast('已删除')}});
}
// === DIARY (含已完成备忘录同步) ===
function renderDiary(){
var h='<div class="section-header"><div><div class="section-title">日记</div><div class="section-sub">每天的心情记录 · 已完成备忘自动同步</div></div><button class="btn btn-primary" data-action="add-diary">'+SVG.plus+'写日记</button></div>';
var doneMemos=data.memo.filter(function(m){return m.done});
if(data.diary.length===0&&doneMemos.length===0){h+='<div class="empty">'+SVG.emptyBox+'<div class="empty-text">还没有日记记录</div></div>';return h}
var items=[];
data.diary.forEach(function(d){items.push({kind:'diary',sortAt:d.createdAt,d:d})});
doneMemos.forEach(function(m){items.push({kind:'memo',sortAt:m.completedAt,m:m})});
items.sort(function(a,b){return state.sort==='desc'?new Date(b.sortAt)-new Date(a.sortAt):new Date(a.sortAt)-new Date(b.sortAt)});
var allIds=data.diary.map(function(d){return d.id});
if(state.editMode)h+=editBarHTML('diary',allIds);
items.forEach(function(it){
if(it.kind==='memo'){
var m=it.m;
h+='<div class="diary-item"><div class="diary-header"><div class="diary-date">'+fmt(m.completedAt,false)+'</div><div class="diary-mood"><span class="tag tag-success">'+SVG.memo+' 备忘</span></div></div>';
h+='<div class="diary-content" style="text-decoration:line-through;color:var(--t3)">'+esc(m.text)+'</div>';
h+='<div class="diary-location" style="margin-top:6px;color:var(--success)">'+SVG.check+' 完成于 '+fmt(m.completedAt,true)+'</div></div>';
}else{
var d=it.d;var mood=MOODS[d.mood]||MOODS.calm;
var sel=state.editMode&&state.selectedIds[d.id];
var action=state.editMode?'toggle-select':'edit-diary';
var extraAttrs=state.editMode?'data-id="'+d.id+'"':'data-id="'+d.id+'" data-longpress="diary"';
h+='<div class="diary-item selectable'+(sel?' selected':'')+'" data-action="'+action+'" '+extraAttrs+'><div class="diary-header"><div class="diary-date">'+fmt(d.date,false)+'</div><div class="diary-mood" style="color:'+mood.color+'">'+mood.icon+'<span>'+mood.label+'</span></div></div>';
if(d.location)h+='<div class="diary-location">'+SVG.pin+' '+esc(d.location)+'</div>';
h+='<div class="diary-content">'+esc(d.content)+'</div>';
if(!state.editMode)h+='<div style="margin-top:6px"><button class="btn btn-danger btn-sm" data-action="delete-diary" data-id="'+d.id+'" onclick="event.stopPropagation()">'+SVG.trash+'</button></div>';
h+='</div>';
}
});
if(!state.editMode&&data.diary.length>0)h+='<div style="margin-top:12px"><button class="btn btn-outline btn-sm" onclick="enterEditMode(\'diary\',null)">'+SVG.edit+'编辑模式</button></div>';
return h;
}
function addDiary(){
var moodHTML='<div class="mood-grid">';
Object.keys(MOODS).forEach(function(k){moodHTML+='<div class="mood-option" data-mood="'+k+'" onclick="selectMood(\''+k+'\')" style="color:'+MOODS[k].color+'">'+MOODS[k].icon+'<span class="mood-label">'+MOODS[k].label+'</span></div>'});
moodHTML+='</div>';
openModal('写日记','<div class="form-group"><label class="form-label">日期</label><input type="date" class="input" id="di-date" value="'+todayStr()+'"></div><div class="form-group"><label class="form-label">心情</label>'+moodHTML+'<input type="hidden" id="di-mood" value="calm"></div><div class="form-group"><label class="form-label">地点</label><input type="text" class="input" id="di-location" placeholder="你在哪里?"><button class="voice-btn" onclick="getLocationFor(\'di-location\')" style="margin-top:6px">'+SVG.pin+' 自动获取位置</button></div><div class="form-group"><label class="form-label">日记内容</label><textarea class="textarea" id="di-content" placeholder="今天发生了什么?有什么感想?" style="min-height:150px"></textarea><button class="voice-btn" data-voice="di-content" onclick="toggleVoice(\'di-content\',this)" style="margin-top:6px">'+SVG.mic+' 语音输入</button></div>',{saveText:'保存日记',onSave:function(){
var mood=document.getElementById('di-mood').value;
var entry={id:uid(),date:document.getElementById('di-date').value,mood:mood,location:document.getElementById('di-location').value,content:document.getElementById('di-content').value,createdAt:new Date().toISOString()};
data.diary.unshift(entry);Store.set('diary',data.diary);render();confetti();toast('日记已保存');
}});
setTimeout(function(){selectMood('calm')},50);
}
function deleteDiary(id){
openModal('确认删除','<p style="font-size:14px;color:var(--t2)">确定要删除这篇日记吗?</p>',{saveText:'确认删除',onSave:function(){data.diary=data.diary.filter(function(d){return d.id!==id});Store.set('diary',data.diary);render();toast('已删除')}});
}
// === ENGLISH (单词 + 口语句子) ===
function renderEnglish(){
var h='<div class="section-header"><div><div class="section-title">英语</div><div class="section-sub">积累词汇,练习口语</div></div><button class="btn btn-primary" data-action="add-english">'+SVG.plus+'新建</button></div>';
if(data.english.length===0){h+='<div class="empty">'+SVG.emptyBox+'<div class="empty-text">还没有记录,点击「新建」添加单词或口语句子</div></div>';return h}
var sorted=data.english.slice().sort(function(a,b){return state.sort==='desc'?new Date(b.createdAt)-new Date(a.createdAt):new Date(a.createdAt)-new Date(b.createdAt)});
var allIds=sorted.map(function(w){return w.id});
if(state.editMode)h+=editBarHTML('english',allIds);
sorted.forEach(function(w){
var isS=w.type==='sentence';
var title=isS?(w.sentence||''):(w.word||'');
var sel=state.editMode&&state.selectedIds[w.id];
var action=state.editMode?'toggle-select':'edit-english';
var extraAttrs=state.editMode?'data-id="'+w.id+'"':'data-id="'+w.id+'" data-longpress="english"';
h+='<div class="word-item selectable'+(sel?' selected':'')+'" data-action="'+action+'" '+extraAttrs+'><div class="word-header"><div><div class="word-title">'+(isS?SVG.sentence+' ':'')+esc(title)+'</div>'+(w.phonetic?'<div class="word-phonetic">'+esc(w.phonetic)+'</div>':'')+'</div>';
h+='<div style="display:flex;gap:4px;align-items:center">';
if(w.mastered)h+='<span class="tag tag-success">'+SVG.check+' 已掌握</span>';
else h+='<button class="btn btn-outline btn-sm" data-action="master-english" data-id="'+w.id+'" onclick="event.stopPropagation()">掌握</button>';
if(w.tag)h+='<span class="tag'+(isS?' tag-info':'')+'">'+esc(w.tag)+'</span>';
if(!state.editMode)h+=' <button class="btn btn-danger btn-sm" data-action="delete-english" data-id="'+w.id+'" onclick="event.stopPropagation()">'+SVG.trash+'</button>';
h+='</div></div>';
if(isS){
if(w.translation)h+='<div class="word-meaning">'+esc(w.translation)+'</div>';
if(w.scene)h+='<div class="word-example">场景:'+esc(w.scene)+'</div>';
}else{
if(w.meaning)h+='<div class="word-meaning">'+esc(w.meaning)+'</div>';
if(w.example)h+='<div class="word-example">'+esc(w.example)+'</div>';
}
h+='</div>';
});
if(!state.editMode)h+='<div style="margin-top:12px"><button class="btn btn-outline btn-sm" onclick="enterEditMode(\'english\',null)">'+SVG.edit+'编辑模式</button></div>';
return h;
}
function addEnglish(){
openModal('新建英语记录','<div class="form-group"><label class="form-label">类型 *</label><div class="mood-grid" id="en-type-grid"><div class="mood-option selected" data-entype="word" onclick="selectEnglishType(\'word\')" style="color:var(--accent)">'+SVG.english+'<span class="mood-label">单词</span></div><div class="mood-option" data-entype="sentence" onclick="selectEnglishType(\'sentence\')" style="color:var(--info)">'+SVG.sentence+'<span class="mood-label">口语句子</span></div></div><input type="hidden" id="en-type" value="word"></div><div id="en-word-fields"><div class="form-group"><label class="form-label">单词/短语 *</label><input type="text" class="input" id="en-word" placeholder="如:serendipity"></div><div class="form-group"><label class="form-label">音标</label><input type="text" class="input" id="en-phonetic" placeholder="如:/ˌserənˈdɪpəti/"></div><div class="form-group"><label class="form-label">释义</label><input type="text" class="input" id="en-meaning" placeholder="中文释义"></div><div class="form-group"><label class="form-label">例句</label><textarea class="textarea" id="en-example" placeholder="例句"></textarea></div></div><div id="en-sentence-fields" style="display:none"><div class="form-group"><label class="form-label">口语句子 *</label><textarea class="textarea" id="en-sentence" placeholder="如:Could you do me a favor?" style="min-height:70px"></textarea><button class="voice-btn" data-voice="en-sentence" onclick="toggleVoice(\'en-sentence\',this)" style="margin-top:6px">'+SVG.mic+' 语音输入</button></div><div class="form-group"><label class="form-label">中文翻译</label><input type="text" class="input" id="en-translation" placeholder="如:你能帮我个忙吗?"></div><div class="form-group"><label class="form-label">使用场景</label><input type="text" class="input" id="en-scene" placeholder="如:请求帮助、日常对话"></div></div><div class="form-group"><label class="form-label">标签</label><input type="text" class="input" id="en-tag" placeholder="如:日常、商务、学术"></div>',{saveText:'保存',onSave:function(){
var type=document.getElementById('en-type').value;
var w;
if(type==='sentence'){
var sentence=document.getElementById('en-sentence').value.trim();
if(!sentence){toast('请输入口语句子');return false}
w={id:uid(),type:'sentence',sentence:sentence,translation:document.getElementById('en-translation').value,scene:document.getElementById('en-scene').value,tag:document.getElementById('en-tag').value,mastered:false,createdAt:new Date().toISOString()};
}else{
var word=document.getElementById('en-word').value.trim();
if(!word){toast('请输入单词');return false}
w={id:uid(),type:'word',word:word,phonetic:document.getElementById('en-phonetic').value,meaning:document.getElementById('en-meaning').value,example:document.getElementById('en-example').value,tag:document.getElementById('en-tag').value,mastered:false,createdAt:new Date().toISOString()};
}
data.english.unshift(w);Store.set('english',data.english);render();confetti();toast(type==='sentence'?'口语句子已添加':'单词已添加');
}});
}
function selectEnglishType(t){
document.getElementById('en-type').value=t;
document.querySelectorAll('[data-entype]').forEach(function(el){el.classList.toggle('selected',el.dataset.entype===t)});
document.getElementById('en-word-fields').style.display=t==='word'?'block':'none';
document.getElementById('en-sentence-fields').style.display=t==='sentence'?'block':'none';
}
function deleteEnglish(id){
openModal('确认删除','<p style="font-size:14px;color:var(--t2)">确定要删除这个单词吗?</p>',{saveText:'确认删除',onSave:function(){data.english=data.english.filter(function(w){return w.id!==id});Store.set('english',data.english);render();toast('已删除')}});
}
function masterEnglish(id){
var w=data.english.find(function(x){return x.id===id});if(!w)return;
w.mastered=true;Store.set('english',data.english);render();confetti();toast(w.type==='sentence'?'恭喜掌握新句子!':'恭喜掌握新单词!');
}
// === EDIT MODE HELPERS ===
function deleteSelected(cat){
var ids=getSelectedIds();
if(ids.length===0){toast('请先选择内容');return}
openModal('确认删除','<p style="font-size:14px;color:var(--t2)">确定要删除选中的 <b style="color:var(--danger)">'+ids.length+'</b> 条内容吗?此操作不可撤销。</p>',{saveText:'确认删除',onSave:function(){
if(cat==='books'){
data.books=data.books.filter(function(b){return !state.selectedIds[b.id]});
Store.set('books',data.books);
}else if(cat==='video'){
data.video=data.video.filter(function(v){return !state.selectedIds[v.id]});
Store.set('video',data.video);
}else if(cat==='diary'){
data.diary=data.diary.filter(function(d){return !state.selectedIds[d.id]});
Store.set('diary',data.diary);
}else if(cat==='english'){
data.english=data.english.filter(function(w){return !state.selectedIds[w.id]});
Store.set('english',data.english);
}else if(cat==='booknotes'){
var b=data.books.find(function(x){return x.id===state.bookId});
if(b){b.notes=b.notes.filter(function(n){return !state.selectedIds[n.id]});Store.set('books',data.books)}
}
exitEditMode();toast('已删除'+ids.length+'条');
}});
}
function editBarHTML(cat,ids){
var selCount=getSelectedIds().length;
var allSel=ids.length>0&&ids.every(function(id){return state.selectedIds[id]});
var h='<div class="edit-bar">';
h+='<button class="btn btn-ghost btn-sm" data-action="exit-edit">'+SVG.back+'退出</button>';
h+='<span class="edit-bar-info">已选 <span class="edit-bar-count">'+selCount+'</span> / '+ids.length+'</span>';
h+='<button class="btn btn-outline btn-sm" data-action="select-all" data-ids="'+ids.join(',')+'">'+(allSel?'取消全选':'全选')+'</button>';
h+='<button class="btn btn-danger btn-sm" data-action="delete-selected" data-cat="'+cat+'">'+SVG.trash+'删除选中</button>';
h+='</div>';
return h;
}
// === EDIT FUNCTIONS ===
function editBook(id){
var b=data.books.find(function(x){return x.id===id});if(!b)return;
var isE=b.type==='ebook';
openModal('编辑书籍','<div class="form-group"><label class="form-label">书名 *</label><input type="text" class="input" id="eb-title" value="'+esc(b.title)+'"></div><div class="form-group"><label class="form-label">作者</label><input type="text" class="input" id="eb-author" value="'+esc(b.author||'')+'"></div><div class="form-group"><label class="form-label">类型</label><select class="select" id="eb-type"><option value="paper"'+(b.type!=='ebook'?' selected':'')+'>纸质书</option><option value="ebook"'+(b.type==='ebook'?' selected':'')+'>电子书</option></select></div>'+(b.type!=='ebook'?'<div class="form-group"><label class="form-label">阅读目标日期</label><input type="date" class="input" id="eb-goal" value="'+esc(b.goalDate||'')+'"></div><div class="form-group"><label class="form-label">状态</label><select class="select" id="eb-status"><option value="reading"'+(b.status==='reading'?' selected':'')+'>阅读中</option><option value="finished"'+(b.status==='finished'?' selected':'')+'>已读完</option></select></div>':''),{saveText:'保存',onSave:function(){
var title=document.getElementById('eb-title').value.trim();
if(!title){toast('请输入书名');return false}
b.title=title;b.author=document.getElementById('eb-author').value.trim();
b.type=document.getElementById('eb-type').value;
if(b.type!=='ebook'){
b.goalDate=document.getElementById('eb-goal').value;
b.status=document.getElementById('eb-status').value;
}
Store.set('books',data.books);render();toast('已保存');
}});
}
function editVideoNote(id){
var v=data.video.find(function(x){return x.id===id});if(!v)return;
openModal('编辑视频笔记','<div class="form-group"><label class="form-label">标题</label><input type="text" class="input" id="ev-title" value="'+esc(v.title||'')+'"></div><div class="form-group"><label class="form-label">视频链接</label><input type="text" class="input" id="ev-url" value="'+esc(v.videoUrl||'')+'"></div><div class="form-group"><label class="form-label">视频中的文字内容</label><textarea class="textarea" id="ev-text" style="min-height:100px">'+esc(v.extractedText||'')+'</textarea></div><div class="form-group"><label class="form-label">我的感悟</label><textarea class="textarea" id="ev-reflection" style="min-height:100px">'+esc(v.reflection||'')+'</textarea></div>',{saveText:'保存',onSave:function(){
v.title=document.getElementById('ev-title').value;
v.videoUrl=document.getElementById('ev-url').value;
v.extractedText=document.getElementById('ev-text').value;
v.reflection=document.getElementById('ev-reflection').value;
Store.set('video',data.video);render();toast('已保存');
}});
}
function editDiary(id){
var d=data.diary.find(function(x){return x.id===id});if(!d)return;
var moodHTML='<div class="mood-grid">';
Object.keys(MOODS).forEach(function(k){moodHTML+='<div class="mood-option'+(d.mood===k?' selected':'')+'" data-mood="'+k+'" onclick="selectMood(\''+k+'\')" style="color:'+MOODS[k].color+'">'+MOODS[k].icon+'<span class="mood-label">'+MOODS[k].label+'</span></div>'});
moodHTML+='</div>';
openModal('编辑日记','<div class="form-group"><label class="form-label">日期</label><input type="date" class="input" id="ed-date" value="'+esc(d.date)+'"></div><div class="form-group"><label class="form-label">心情</label>'+moodHTML+'<input type="hidden" id="ed-mood" value="'+esc(d.mood)+'"></div><div class="form-group"><label class="form-label">地点</label><input type="text" class="input" id="ed-location" value="'+esc(d.location||'')+'"></div><div class="form-group"><label class="form-label">日记内容</label><textarea class="textarea" id="ed-content" style="min-height:150px">'+esc(d.content||'')+'</textarea></div>',{saveText:'保存',onSave:function(){
d.date=document.getElementById('ed-date').value;
d.mood=document.getElementById('ed-mood').value;
d.location=document.getElementById('ed-location').value;
d.content=document.getElementById('ed-content').value;
Store.set('diary',data.diary);render();toast('已保存');
}});
}
function editEnglish(id){
var w=data.english.find(function(x){return x.id===id});if(!w)return;
var isS=w.type==='sentence';
if(isS){
openModal('编辑口语句子','<div class="form-group"><label class="form-label">口语句子 *</label><textarea class="textarea" id="ee-sentence" style="min-height:70px">'+esc(w.sentence||'')+'</textarea></div><div class="form-group"><label class="form-label">中文翻译</label><input type="text" class="input" id="ee-translation" value="'+esc(w.translation||'')+'"></div><div class="form-group"><label class="form-label">使用场景</label><input type="text" class="input" id="ee-scene" value="'+esc(w.scene||'')+'"></div><div class="form-group"><label class="form-label">标签</label><input type="text" class="input" id="ee-tag" value="'+esc(w.tag||'')+'"></div>',{saveText:'保存',onSave:function(){
w.sentence=document.getElementById('ee-sentence').value;
w.translation=document.getElementById('ee-translation').value;
w.scene=document.getElementById('ee-scene').value;
w.tag=document.getElementById('ee-tag').value;
Store.set('english',data.english);render();toast('已保存');
}});
}else{
openModal('编辑单词','<div class="form-group"><label class="form-label">单词/短语 *</label><input type="text" class="input" id="ee-word" value="'+esc(w.word||'')+'"></div><div class="form-group"><label class="form-label">音标</label><input type="text" class="input" id="ee-phonetic" value="'+esc(w.phonetic||'')+'"></div><div class="form-group"><label class="form-label">释义</label><input type="text" class="input" id="ee-meaning" value="'+esc(w.meaning||'')+'"></div><div class="form-group"><label class="form-label">例句</label><textarea class="textarea" id="ee-example">'+esc(w.example||'')+'</textarea></div><div class="form-group"><label class="form-label">标签</label><input type="text" class="input" id="ee-tag" value="'+esc(w.tag||'')+'"></div>',{saveText:'保存',onSave:function(){
w.word=document.getElementById('ee-word').value;
w.phonetic=document.getElementById('ee-phonetic').value;
w.meaning=document.getElementById('ee-meaning').value;
w.example=document.getElementById('ee-example').value;
w.tag=document.getElementById('ee-tag').value;
Store.set('english',data.english);render();toast('已保存');
}});
}
}
// === MODAL ===
function openModal(title,body,opts){
opts=opts||{};
document.getElementById('modal-title').textContent=title;
document.getElementById('modal-body').innerHTML=body;
document.getElementById('modal-save').style.display=opts.saveBtn===false?'none':'';
document.getElementById('modal-save').textContent=opts.saveText||'保存';
document.getElementById('modal-cancel').textContent=opts.cancelText||'取消';
document.getElementById('modal-overlay').classList.add('active');
modalCb=opts.onSave||null;
}
function closeModal(){document.getElementById('modal-overlay').classList.remove('active');modalCb=null}
// === UTILITIES ===
function handlePhoto(file,cb){
var reader=new FileReader();
reader.onload=function(e){
var img=new Image();
img.onload=function(){
var canvas=document.createElement('canvas');var maxW=800;var scale=Math.min(1,maxW/img.width);
canvas.width=img.width*scale;canvas.height=img.height*scale;
var ctx=canvas.getContext('2d');ctx.drawImage(img,0,0,canvas.width,canvas.height);
cb(canvas.toDataURL('image/jpeg',0.85));
};
img.src=e.target.result;
};
reader.readAsDataURL(file);
}
function toggleVoice(textareaId,btn){
if(btn.classList.contains('recording')){if(currentRec)currentRec.stop();return}
var SR=window.SpeechRecognition||window.webkitSpeechRecognition;
if(!SR){toast('当前浏览器不支持语音输入,请使用Chrome或Edge');return}
var rec=new SR();rec.lang='zh-CN';rec.continuous=true;rec.interimResults=true;
var ta=document.getElementById(textareaId);var original=ta.value;var finalText='';
rec.onresult=function(e){var interim='';for(var i=e.resultIndex;i<e.results.length;i++){if(e.results[i].isFinal)finalText+=e.results[i][0].transcript;else interim+=e.results[i][0].transcript}ta.value=original+finalText+interim};
rec.onerror=function(e){toast('语音识别出错');btn.classList.remove('recording')};
rec.onend=function(){btn.classList.remove('recording');btn.innerHTML=SVG.mic+' 语音输入'};
rec.start();btn.classList.add('recording');btn.innerHTML=SVG.mic+' 录音中...';currentRec=rec;
toast('正在录音,请说话');
}
function toggleWatermark(checkbox){
var f=document.getElementById('wm-fields');
if(f)f.style.display=checkbox.checked?'block':'none';
if(checkbox.checked&&navigator.geolocation){
navigator.geolocation.getCurrentPosition(function(pos){
var lat=pos.coords.latitude.toFixed(4);var lng=pos.coords.longitude.toFixed(4);
var locInput=document.querySelector('#wm-fields input');
if(locInput&&!locInput.value)locInput.value=lat+','+lng;
},function(){},{timeout:5000});
}
}
function getLocationFor(inputId){
if(!navigator.geolocation){toast('无法获取位置');return}
toast('正在获取位置...');
navigator.geolocation.getCurrentPosition(function(pos){
var lat=pos.coords.latitude.toFixed(4);var lng=pos.coords.longitude.toFixed(4);
var el=document.getElementById(inputId);if(el)el.value=lat+','+lng;
toast('位置已获取');
},function(){toast('无法获取位置')},{timeout:5000});
}
function confetti(){
var container=document.getElementById('confetti-container');
var colors=['#2a9d78','#7ed8b4','#5cb896','#3aafa9','#a8e6cf','#2ec4b6','#1f7d5f'];
for(var i=0;i<60;i++){
var piece=document.createElement('div');piece.className='confetti-piece';
piece.style.cssText='left:'+Math.random()*100+'%;background:'+colors[Math.floor(Math.random()*colors.length)]+';animation-delay:'+Math.random()*0.5+'s;animation-duration:'+(1.5+Math.random())+'s;width:'+(6+Math.random()*8)+'px;height:'+(6+Math.random()*8)+'px;border-radius:'+(Math.random()>0.5?'50%':'2px');
container.appendChild(piece);
setTimeout(function(p){return function(){p.remove()}}(piece),3500);
}
}
function exportData(){
var all={books:data.books,memo:data.memo,video:data.video,diary:data.diary,english:data.english,exportDate:new Date().toISOString()};
var blob=new Blob([JSON.stringify(all,null,2)],{type:'application/json'});
var url=URL.createObjectURL(blob);var a=document.createElement('a');
a.href=url;a.download='reading-notes-backup-'+new Date().toISOString().slice(0,10)+'.json';a.click();
URL.revokeObjectURL(url);toast('数据已导出');
}
function importData(file){
var reader=new FileReader();
reader.onload=function(e){
try{
var imp=JSON.parse(e.target.result);
if(imp.books){data.books=imp.books;Store.set('books',imp.books)}
if(imp.memo){data.memo=imp.memo;Store.set('memo',imp.memo)}
// legacy import format
if(imp.paper||imp.ebook){
var legacy=[];(imp.paper||[]).forEach(function(b){legacy.push(Object.assign({type:'paper'},b))});(imp.ebook||[]).forEach(function(b){legacy.push(Object.assign({type:'ebook'},b))});
data.books=legacy.concat(data.books);Store.set('books',data.books);
}
if(imp.video){data.video=imp.video;Store.set('video',imp.video)}
if(imp.diary){data.diary=imp.diary;Store.set('diary',imp.diary)}
if(imp.english){data.english=imp.english;Store.set('english',imp.english)}
render();toast('数据导入成功');
}catch(err){toast('导入失败:文件格式错误')}
};
reader.readAsText(file);
}
function previewCover(input){
if(!input.files[0])return;var reader=new FileReader();
reader.onload=function(e){document.getElementById('cover-preview').innerHTML='<img src="'+e.target.result+'" style="max-width:100%;border-radius:8px;margin-top:8px;max-height:160px;object-fit:cover">'};
reader.readAsDataURL(input.files[0]);
}
function previewNotePhoto(input){
if(!input.files[0])return;var reader=new FileReader();
reader.onload=function(e){document.getElementById('photo-preview').innerHTML='<img src="'+e.target.result+'" style="width:100%;border-radius:8px;margin-top:8px;max-height:240px;object-fit:cover">'};
reader.readAsDataURL(input.files[0]);
}
function selectMood(mood){
var input=document.getElementById('di-mood')||document.getElementById('ed-mood');
if(input)input.value=mood;
document.querySelectorAll('.mood-option').forEach(function(el){el.classList.toggle('selected',el.dataset.mood===mood)});
}
// === SAMPLE DATA ===
function loadSampleData(){
var now=Date.now();var day=86400000;
data.books=[
{id:'sp1',type:'paper',title:'百年孤独',author:'加西亚·马尔克斯',cover:null,status:'reading',goalDate:'',createdAt:new Date(now-7*day).toISOString(),notes:[
{id:'sn1',photo:null,originalText:'多年以后,面对行刑队,奥雷里亚诺·布恩迪亚上校将会回想起父亲带他去见识冰块的那个遥远的下午。',reflection:'开篇即震撼,一句话跨越了过去、现在和未来,时间的交错感扑面而来。',hasWatermark:true,watermarkTime:fmt(new Date(now-2*day),true),watermarkLocation:'书房',createdAt:new Date(now-2*day).toISOString()},
{id:'sn2',photo:null,originalText:'世界新生伊始,许多事物还没有名字,提到的时候尚需用手指指指点。',reflection:'语言诞生之前的世界是什么样子?马尔克斯用诗意的笔触描绘了那个原始而纯粹的时代。',hasWatermark:false,watermarkTime:'',watermarkLocation:'',createdAt:new Date(now-1*day).toISOString()}
]},
{id:'sp2',type:'paper',title:'活着',author:'余华',cover:null,status:'reading',goalDate:new Date(now-3*day).toISOString().slice(0,10),createdAt:new Date(now-14*day).toISOString(),notes:[
{id:'sn3',photo:null,originalText:'人是为活着本身而活着的,而不是为了活着之外的任何事物所活着。',reflection:'这句话道出了生命的本质。福贵的一生经历了太多苦难,却始终在坚持活着。',hasWatermark:true,watermarkTime:fmt(new Date(now-5*day),true),watermarkLocation:'卧室',createdAt:new Date(now-5*day).toISOString()}
]},
{id:'se1',type:'ebook',title:'思考,快与慢',author:'丹尼尔·卡尼曼',cover:null,status:'',goalDate:'',createdAt:new Date(now-5*day).toISOString(),notes:[
{id:'sen1',photo:null,originalText:'',quote:'我们对自己认为熟知的事物,确信度往往过高。',reflection:'这就是过度自信效应。保持谦逊比盲目自信更重要。',chapter:'第一部分',hasWatermark:false,watermarkTime:'',watermarkLocation:'',createdAt:new Date(now-3*day).toISOString()}
]}
];
data.memo=[
{id:'sm1',text:'整理《百年孤独》前三章笔记',done:true,completedAt:new Date(now-1*day).toISOString(),createdAt:new Date(now-2*day).toISOString()},
{id:'sm2',text:'本周读完《活着》剩余部分',done:false,completedAt:null,createdAt:new Date(now-1*day).toISOString()}
];
data.video=[{id:'sv1',title:'关于阅读意义的演讲',videoUrl:'https://example.com/video',extractedText:'阅读不是为了证明自己有多博学,而是为了在别人的人生中找到自己的影子。每一本书都是一扇窗,打开它,你就能看到不同的世界。',reflection:'这段话让我重新思考了阅读的意义。不是为了炫耀,而是为了成长和共情。',hasWatermark:true,watermarkTime:fmt(new Date(now-1*day),true),watermarkLocation:'咖啡馆',createdAt:new Date(now-1*day).toISOString()}];
data.diary=[{id:'sd1',date:new Date(now-1*day).toISOString().slice(0,10),mood:'happy',location:'北京·朝阳区',content:'今天读完了《百年孤独》的前三章,马尔克斯的魔幻现实主义让人着迷。下午去咖啡馆写了读书笔记,感觉非常充实。',createdAt:new Date(now-1*day).toISOString()}];
data.english=[
{id:'sw1',type:'word',word:'serendipity',phonetic:'/ˌserənˈdɪpəti/',meaning:'意外发现美好事物的能力;幸运的偶然',example:'Finding this little bookstore was pure serendipity.',tag:'日常',mastered:false,createdAt:new Date(now-2*day).toISOString()},
{id:'ss1',type:'sentence',sentence:'Could you do me a favor and pass the salt?',translation:'能帮我递一下盐吗?',scene:'餐桌请求',tag:'日常',mastered:false,createdAt:new Date(now-1*day).toISOString()}
];
Store.set('books',data.books);Store.set('memo',data.memo);Store.set('video',data.video);Store.set('diary',data.diary);Store.set('english',data.english);
}
function clearSampleData(){
openModal('清空示例数据','<p style="font-size:14px;color:var(--t2)">确定要清空所有示例数据吗?这将删除所有记录。</p>',{saveText:'确认清空',onSave:function(){
data.books=[];data.memo=[];data.video=[];data.diary=[];data.english=[];
Store.set('books',[]);Store.set('memo',[]);Store.set('video',[]);Store.set('diary',[]);Store.set('english',[]);
localStorage.setItem('wb_reading_initialized','1');
render();toast('示例数据已清空');
}});
}
// === INIT ===
init();
</script>
</body>
</html>