1269 lines
100 KiB
HTML
1269 lines
100 KiB
HTML
<!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{'&':'&','<':'<','>':'>','"':'"',"'":'''}[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> |