<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>个人知识库管理系统</title> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <style> *{margin:0;padding:0;box-sizing:border-box} body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;line-height:1.6;background:#f5f5f5;color:#333} /* 登录页 */ .auth-container{min-height:100vh;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);padding:1rem} .auth-card{background:#fff;border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,.3);padding:2rem;max-width:400px;width:100%} .auth-card h2{text-align:center;color:#333;margin-bottom:1.5rem} .auth-form input{width:100%;padding:.875rem;margin-bottom:1rem;border:1px solid #e0e0e0;border-radius:8px;font-size:1rem;transition:border-color .2s} .auth-form input:focus{outline:none;border-color:#667eea} .auth-form .btn{width:100%;padding:.875rem;border:none;border-radius:8px;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;font-size:1rem;cursor:pointer;font-weight:600;transition:transform .2s} .auth-form .btn:hover{transform:translateY(-2px)} .auth-switch{text-align:center;margin-top:1rem;color:#666} .auth-switch a{color:#667eea;text-decoration:none;cursor:pointer} /* 主布局 */ .app{min-height:100vh;display:flex;flex-direction:column} .nav{display:flex;gap:.5rem;background:#fff;padding:.75rem 1rem;border-bottom:1px solid #e0e0e0;flex-wrap:wrap;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100;box-shadow:0 1px 3px rgba(0,0,0,.1)} .nav-left{display:flex;gap:.5rem;flex-wrap:wrap} .nav a{text-decoration:none;color:#666;padding:.5rem 1rem;border-radius:6px;transition:all .2s;font-size:.875rem} .nav a:hover{color:#1a73e8;background:rgba(26,115,232,.1)} .nav a.active{background:#1a73e8;color:#fff} .nav-user{display:flex;gap:.5rem;align-items:center} .nav-user .btn{padding:.375rem .75rem;font-size:.875rem} /* 双栏布局 */ .app-container{display:flex;flex:1;min-height:0} .main-content{flex:1;padding:1rem;overflow-y:auto} .chat-panel{width:400px;background:#fff;border-left:1px solid #e0e0e0;display:flex;flex-direction:column} @media(max-width:900px){ .app-container{flex-direction:column} .chat-panel{width:100%;height:400px;border-left:none;border-top:1px solid #e0e0e0} } /* 内容区域 */ .container{max-width:1000px;margin:0 auto;width:100%} .dashboard{} .stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem;margin:1.5rem 0} .stat-card{background:#fff;border-radius:8px;padding:1.25rem;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,.1);transition:transform .2s} .stat-card:hover{transform:translateY(-2px);box-shadow:0 2px 8px rgba(0,0,0,.15)} .stat-icon{font-size:2rem;margin-bottom:.5rem} .stat-value{font-size:1.75rem;font-weight:bold;color:#1a73e8} .stat-label{color:#666;font-size:.875rem;margin-top:.25rem} .recent-list{list-style:none;background:#fff;border-radius:8px;padding:1rem;box-shadow:0 1px 3px rgba(0,0,0,.1)} .recent-list li{padding:.75rem 0;border-bottom:1px solid #f0f0f0} .recent-list li:last-child{border-bottom:none} .recent-list a{color:#1a73e8;text-decoration:none;font-weight:500} .recent-list a:hover{text-decoration:underline} .recent-list .time{float:right;color:#999;font-size:.75rem} .page-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;flex-wrap:wrap;gap:.5rem} .page-header h2{font-size:1.25rem;color:#333} .search-bar{margin-bottom:1rem} .search-bar input{width:100%;padding:.75rem 1rem;border:1px solid #e0e0e0;border-radius:8px;background:#fff;font-size:.875rem;transition:border-color .2s} .search-bar input:focus{outline:none;border-color:#1a73e8} .notes-list,.passwords-list,.memos-list,.files-list{display:grid;gap:1rem;grid-template-columns:repeat(auto-fill,minmax(280px,1fr))} .note-card,.password-card,.memo-card,.file-card{background:#fff;border:1px solid #e0e0e0;border-radius:8px;padding:1rem;transition:box-shadow .2s} .note-card:hover,.password-card:hover,.memo-card:hover,.file-card:hover{box-shadow:0 2px 8px rgba(0,0,0,.1)} .note-title,.password-site,.memo-title{margin:0 0 .5rem 0;font-size:1rem} .note-tags,.note-time,.password-user,.password-time,.memo-date,.memo-time{margin:.25rem 0;font-size:.75rem;color:#666} .note-actions,.password-actions,.memo-actions,.file-actions{display:flex;gap:.5rem;margin-top:.75rem;flex-wrap:wrap} .file-info{display:flex;align-items:center;gap:.5rem} .file-icon{font-size:1.5rem} .file-name{font-weight:500;font-size:.875rem} .file-size,.file-time{font-size:.75rem;color:#666} .btn{padding:.5rem 1rem;border:none;border-radius:6px;cursor:pointer;font-size:.875rem;transition:all .2s} .btn-primary{background:#1a73e8;color:#fff} .btn-primary:hover{background:#1557b0} .btn-secondary{background:#6c757d;color:#fff} .btn-view{background:#34a853;color:#fff} .btn-edit{background:#fbbc04;color:#333} .btn-delete,.btn-delete-pwd,.btn-delete-memo,.btn-delete-file{background:#ea4335;color:#fff} .btn-show{background:#1a73e8;color:#fff} .btn-copy-pwd{background:#9c27b0;color:#fff} .btn-download{background:#34a853;color:#fff} .btn-sm{padding:.375rem .75rem;font-size:.75rem;border-radius:4px} .empty{text-align:center;padding:3rem;color:#999;grid-column:1/-1} .modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);z-index:999} .modal-content{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;border-radius:12px;padding:1.5rem;z-index:1000;min-width:300px;max-width:90vw;max-height:90vh;overflow-y:auto;box-shadow:0 4px 16px rgba(0,0,0,.2)} .modal-content h3{margin:0 0 1rem 0;font-size:1.125rem} .modal-content input,.modal-content textarea,.modal-content select{width:100%;padding:.75rem;margin-bottom:.75rem;border:1px solid #e0e0e0;border-radius:6px;font-size:.875rem} .modal-content input:focus,.modal-content textarea:focus,.modal-content select:focus{outline:none;border-color:#1a73e8} .modal-footer{display:flex;gap:.5rem;justify-content:flex-end;margin-top:1rem} .modal-large{max-width:800px} .markdown-body{line-height:1.6;font-size:.875rem} .markdown-body h1,.markdown-body h2,.markdown-body h3{margin:1rem 0 .5rem 0} .markdown-body p{margin:.5rem 0} .markdown-body code{background:#f5f5f5;padding:.125rem .25rem;border-radius:2px;font-size:.8rem} .markdown-body pre{background:#f5f5f5;padding:1rem;border-radius:4px;overflow-x:auto;margin:.5rem 0} .markdown-body pre code{background:none;padding:0} .markdown-body ul,.markdown-body ol{padding-left:1.5rem} .view-time{color:#999;font-size:.75rem;margin:1rem 0} .priority-badge{display:inline-block;padding:.25rem .5rem;border-radius:4px;font-size:.75rem;color:#fff;font-weight:500} .priority-high{background:#ea4335} .priority-medium{background:#fbbc04;color:#333} .priority-low{background:#34a853} .memo-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem} .memo-actions{display:flex;gap:.5rem;margin-top:.75rem} .upload-area{display:flex;align-items:center;gap:1rem;margin-bottom:1rem} .loading{text-align:center;padding:3rem;color:#999} .toast{position:fixed;top:80px;right:20px;padding:.875rem 1.5rem;border-radius:8px;color:#fff;z-index:9999;font-size:.875rem;transition:opacity .3s;box-shadow:0 4px 12px rgba(0,0,0,.15)} .toast-success{background:#34a853} .toast-error{background:#ea4335} .toast-info{background:#1a73e8} .toast-warning{background:#fbbc04;color:#333} /* 分类样式 */ .category-badge{display:inline-block;padding:.25rem .5rem;border-radius:4px;font-size:.75rem;color:#fff;margin-right:.25rem;margin-bottom:.25rem} .categories-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:.5rem;margin-bottom:1rem} .category-item{padding:.5rem;border:2px solid #e0e0e0;border-radius:6px;cursor:pointer;text-align:center;transition:border-color .2s} .category-item:hover{border-color:#1a73e8} .category-item.selected{border-color:#1a73e8;background:rgba(26,115,232,.1)} .color-picker{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem} .color-option{width:28px;height:28px;border-radius:50%;cursor:pointer;border:2px solid transparent} .color-option:hover,.color-option.selected{border-color:#333} /* 聊天面板 */ .chat-header{padding:1rem;border-bottom:1px solid #e0e0e0;background:#fff} .chat-header h3{font-size:1rem;color:#333} .chat-messages{flex:1;overflow-y:auto;padding:1rem;background:#f8f9fa} .message{margin-bottom:1rem;display:flex} .message.user{justify-content:flex-end} .message-content{max-width:85%;padding:.75rem 1rem;border-radius:12px;font-size:.875rem;word-wrap:break-word} .message.ai .message-content{background:#fff;border:1px solid #e0e0e0} .message.user .message-content{background:#1a73e8;color:#fff} .chat-input{display:flex;gap:.5rem;padding:1rem;border-top:1px solid #e0e0e0;background:#fff} .chat-input textarea{flex:1;padding:.75rem;border:1px solid #e0e0e0;border-radius:6px;resize:none;font-size:.875rem;min-height:40px;max-height:150px} .chat-input textarea:focus{outline:none;border-color:#1a73e8} .chat-input .btn{padding:0 1.25rem;min-height:40px} @media(max-width:768px){ .nav{gap:.25rem;padding:.5rem} .nav a{padding:.5rem .75rem;font-size:.75rem} .stats-grid{grid-template-columns:repeat(2,1fr)} .notes-list,.passwords-list,.memos-list,.files-list{grid-template-columns:1fr} .message-content{max-width:85%} .chat-input{flex-direction:column} .chat-input .btn{width:100%} .page-header{flex-direction:column;align-items:stretch} } @media(max-width:480px){ .stats-grid{grid-template-columns:1fr} .stat-card{padding:1rem} .main-content{padding:.75rem} } </style> </head> <body> <div id="app"></div> <script> const API_BASE='/api'; let currentUser=null; let authToken=localStorage.getItem('authToken'); function debounce(t,e=300){let n=null;return function(...o){n&&clearTimeout(n);n=setTimeout(()=>t.apply(this,o),e)}} function showToast(t,e='info'){let n=document.getElementById('toast');if(n)n.remove();n=document.createElement('div');n.id='toast';n.className='toast toast-'+e;n.textContent=t;n.style.opacity='1';document.body.appendChild(n);setTimeout(()=>{n.style.opacity='0';setTimeout(()=>n.remove(),300)},2500)} function showLoading(){const t=document.getElementById('main-area');if(t)t.innerHTML='<div class="loading">正在加载...</div>'} function formatDate(t){if(!t)return'-';const e=new Date(t);return e.toLocaleString('zh-CN',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'})} function formatSize(t){if(!t)return'0 B';const e=['B','KB','MB','GB'];let n=0;let o=t;while(o>=1024&&n({error:'请求失败'}));if(n.status===401){logout()}throw new Error(t.error||'HTTP '+n.status)}const o=n.headers.get('content-type');if(o&&o.includes('application/json')){return await n.json()}return await n.blob()}catch(e){showToast(e.message,'error');throw e}} // 登录页 function renderAuthPage(isLogin=true){ const app=document.getElementById('app'); app.innerHTML='<div class="auth-container"><div class="auth-card"><h2>'+(isLogin?'登录':'注册')+'</h2><form class="auth-form" onsubmit="handleAuth(event,'+isLogin+')"><input type="text" id="username" placeholder="用户名" required><input type="password" id="password" placeholder="密码" required><button type="submit" class="btn">'+(isLogin?'登录':'注册')+'</button></form><div class="auth-switch">'+(isLogin?'还没有账号?':'已有账号?')+' <a onclick="renderAuthPage('+!isLogin+')">'+(isLogin?'去注册':'去登录')+'</a></div></div></div>'; } async function handleAuth(event,isLogin){ event.preventDefault(); const username=document.getElementById('username').value; const password=document.getElementById('password').value; try{ const endpoint=isLogin?'/auth/login':'/auth/register'; const result=await api(endpoint,{method:'POST',body:JSON.stringify({username,password})}); authToken=result.token; currentUser=result.user; localStorage.setItem('authToken',authToken); showToast(isLogin?'登录成功':'注册成功','success'); renderApp(); }catch(e){} } async function logout(){ try{await api('/auth/logout',{method:'POST'})}catch{} authToken=null; currentUser=null; localStorage.removeItem('authToken'); renderAuthPage(true); showToast('已退出','info'); } // 主应用 let currentPage='dashboard'; let notes=[]; let categories=[]; let selectedCategory=null; let chatMessages=[]; function renderApp(){ const app=document.getElementById('app'); app.innerHTML='<div class="app"><nav class="nav"><div class="nav-left"><a href="#dashboard" class="nav-link" onclick="navigateTo("dashboard")">仪表盘</a><a href="#notes" class="nav-link" onclick="navigateTo("notes")">笔记管理</a><a href="#passwords" class="nav-link" onclick="navigateTo("passwords")">密码管理</a><a href="#memos" class="nav-link" onclick="navigateTo("memos")">备忘录</a><a href="#categories" class="nav-link" onclick="navigateTo("categories")">分类管理</a></div><div class="nav-user"><span>欢迎</span><button class="btn btn-secondary btn-sm" onclick="logout()">退出</button></div></nav><div class="app-container"><div class="main-content"><div class="container" id="main-area"></div></div><div class="chat-panel"><div class="chat-header"><h3>AI 聊天</h3></div><div class="chat-messages" id="chat-messages"><div class="message ai"><div class="message-content">你好!我是你的AI助手,可以帮你管理知识库,自动分类笔记,解答问题。</div></div></div><div class="chat-input"><textarea id="chat-input" placeholder="输入你的问题..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){sendMessage();event.preventDefault()}"></textarea><button class="btn btn-primary" onclick="sendMessage()">发送</button></div></div></div></div>'; updateNav(); loadDashboard(); } function navigateTo(page){ currentPage=page; updateNav(); if(page==='dashboard')loadDashboard(); else if(page==='notes')loadNotes(); else if(page==='passwords')loadPasswords(); else if(page==='memos')loadMemos(); else if(page==='categories')loadCategories(); } function updateNav(){ document.querySelectorAll('.nav-link').forEach(a=>{ a.classList.toggle('active',a.getAttribute('href').substring(1)===currentPage); }); } // 仪表盘 async function loadDashboard(){ showLoading(); try{ const stats=await api('/stats'); const main=document.getElementById('main-area'); main.innerHTML='<div class="dashboard"><h2>数据概览</h2><div class="stats-grid"><div class="stat-card"><div class="stat-icon">📝</div><div class="stat-value">'+stats.notes+'</div><div class="stat-label">笔记</div></div><div class="stat-card"><div class="stat-icon">🔑</div><div class="stat-value">'+stats.passwords+'</div><div class="stat-label">密码</div></div><div class="stat-card"><div class="stat-icon">📌</div><div class="stat-value">'+stats.memos+'</div><div class="stat-label">备忘录</div></div><div class="stat-card"><div class="stat-icon">📁</div><div class="stat-value">'+stats.files+'</div><div class="stat-label">文件</div></div></div><h3 style="margin:2rem 0 1rem">最近笔记</h3>'+((stats.recent&&stats.recent.length>0)?'<ul class="recent-list">'+stats.recent.map(n=>'<li><a href="#notes" onclick="navigateTo("notes")">'+escapeHtml(n.title)+'</a><span class="time">'+formatDate(n.created_at)+'</span></li>').join('')+'</ul>':'<p style="color:#999">暂无笔记</p>')+'</div>'; }catch(e){} } // 笔记管理 let currentNote=null; let noteSearch=''; async function loadNotes(){ showLoading(); try{ const result=await api('/notes'+(noteSearch?'?search='+encodeURIComponent(noteSearch):'')); notes=result.notes; const catResult=await api('/categories'); categories=catResult.categories||[]; renderNotesList(); }catch(e){} } function renderNotesList(){ const main=document.getElementById('main-area'); main.innerHTML='<div class="notes-page"><div class="page-header"><h2>笔记管理</h2><button class="btn btn-primary" onclick="openNoteModal()">+ 新建笔记</button></div><div class="search-bar"><input type="text" placeholder="搜索笔记..." value="'+escapeHtml(noteSearch)+'" oninput="noteSearch=this.value;debounceSearchNotes()"></div><div class="categories-grid" id="category-filter"><div class="category-item'+(!selectedCategory?' selected':'')+'" onclick="selectedCategory=null;renderNotesList()">全部</div>'+categories.map(c=>'<div class="category-item'+(selectedCategory===c.id?' selected':'')+'" style="border-color:'+c.color+'" onclick="selectedCategory='+selectedCategory===c.id?'null':'"'+c.id+'"'+';renderNotesList()">'+escapeHtml(c.name)+'</div>').join('')+'</div><div class="notes-list" id="notes-list">'+(notes.length===0?'<div class="empty">暂无笔记</div>':notes.filter(n=>!selectedCategory||n.category_id===selectedCategory).map(note=>'<div class="note-card" data-id="'+note.id+'"><h3 class="note-title">'+escapeHtml(note.title)+'</h3><p class="note-tags">'+(note.tags?escapeHtml(note.tags):'无标签')+'</p><p class="note-time">'+formatDate(note.updated_at)+'</p><div class="note-actions"><button class="btn btn-sm btn-view" onclick="viewNote("'+note.id+'")">查看</button><button class="btn btn-sm btn-edit" onclick="openNoteModal("'+note.id+'")">编辑</button><button class="btn btn-sm btn-edit" onclick="aiCategorizeNote("'+note.id+'")">AI分类</button><button class="btn btn-sm btn-delete" onclick="deleteNote("'+note.id+'")">删除</button></div></div>').join(''))+'</div></div>'; } const debounceSearchNotes=debounce(loadNotes,300); async function viewNote(id){ try{ const result=await api('/notes/'+id); const note=result.note; const modal=document.createElement('div'); modal.innerHTML='<div class="modal-overlay" onclick="this.parentElement.remove()"></div><div class="modal-content modal-large"><h3>'+escapeHtml(note.title)+'</h3><div class="markdown-body" id="view-content">'+renderMarkdown(note.content)+'</div><p class="view-time">创建于'+formatDate(note.created_at)+',更新于'+formatDate(note.updated_at)+'</p><div class="modal-footer"><button class="btn btn-secondary" onclick="this.closest('.modal-content').parentElement.remove()">关闭</button><button class="btn btn-primary" onclick="openNoteModal("'+id+'")">编辑</button></div></div>'; document.body.appendChild(modal); }catch(e){} } function openNoteModal(id=null){ currentNote=null; const modal=document.createElement('div'); modal.innerHTML='<div class="modal-overlay" onclick="this.parentElement.remove()"></div><div class="modal-content modal-large"><h3 id="modal-title">'+(id?'编辑笔记':'新建笔记')+'</h3><input type="text" id="note-title-input" placeholder="笔记标题"><textarea id="note-content-input" placeholder="笔记内容(支持Markdown)" rows="10"></textarea><input type="text" id="note-tags-input" placeholder="标签(逗号分隔)"><label>分类:<select id="note-category-input"><option value="">无分类</option>'+categories.map(c=>'<option value="'+c.id+'">'+escapeHtml(c.name)+'</option>').join('')+'</select></label><div class="modal-footer"><button class="btn btn-secondary" onclick="this.closest('.modal-content').parentElement.remove()">取消</button><button class="btn btn-primary" onclick="saveNote()">保存</button></div></div>'; document.body.appendChild(modal); if(id){ const note=notes.find(n=>n.id===id); if(note){ document.getElementById('note-title-input').value=note.title||''; document.getElementById('note-content-input').value=note.content||''; document.getElementById('note-tags-input').value=note.tags||''; document.getElementById('note-category-input').value=note.category_id||''; currentNote=note; } } } async function saveNote(){ const title=document.getElementById('note-title-input').value.trim(); const content=document.getElementById('note-content-input').value; const tags=document.getElementById('note-tags-input').value.trim(); const categoryId=document.getElementById('note-category-input').value; if(!title){showToast('请输入标题','warning');return;} try{ const payload={title,content,tags,category_id:categoryId}; if(currentNote){ await api('/notes/'+currentNote.id,{method:'PUT',body:JSON.stringify(payload)}); showToast('更新成功','success'); }else{ await api('/notes',{method:'POST',body:JSON.stringify(payload)}); showToast('创建成功','success'); } document.querySelector('.modal-overlay').parentElement.remove(); loadNotes(); }catch(e){} } async function aiCategorizeNote(id){ const note=notes.find(n=>n.id===id); if(!note)return; showToast('正在分析...','info'); try{ const result=await api('/ai/categorize',{method:'POST',body:JSON.stringify({content:note.content||note.title})}); if(result.category){ const category=categories.find(c=>c.name===result.category); if(category){ await api('/notes/'+id,{method:'PUT',body:JSON.stringify({category_id:category.id})}); showToast('已自动分类为:'+result.category,'success'); loadNotes(); } }else{ showToast('无法自动分类','warning'); } }catch(e){} } async function deleteNote(id){ if(!confirm('确定删除这个笔记?'))return; try{ await api('/notes/'+id,{method:'DELETE'}); showToast('已删除','success'); loadNotes(); }catch(e){} } // 密码管理 let passwordSearch=''; async function loadPasswords(){ showLoading(); try{ const result=await api('/passwords'+(passwordSearch?'?search='+encodeURIComponent(passwordSearch):'')); const main=document.getElementById('main-area'); main.innerHTML='<div class="passwords-page"><div class="page-header"><h2>密码管理</h2><button class="btn btn-primary" onclick="openPasswordModal()">+ 添加密码</button></div><div class="search-bar"><input type="text" placeholder="搜索..." value="'+escapeHtml(passwordSearch)+'" oninput="passwordSearch=this.value;debounceSearchPasswords()"></div><div class="passwords-list" id="passwords-list">'+((result.passwords||[]).length===0?'<div class="empty">暂无密码</div>':(result.passwords||[]).map(pwd=>'<div class="password-card" data-id="'+pwd.id+'"><h3 class="password-site">'+escapeHtml(pwd.site_name)+'</h3><p class="password-user">👤 '+escapeHtml(pwd.username||'-')+'</p><p class="password-time">'+formatDate(pwd.updated_at)+'</p><div class="password-actions"><button class="btn btn-sm btn-show" onclick="viewPassword("'+pwd.id+'")">查看</button><button class="btn btn-sm btn-edit" onclick="openPasswordModal("'+pwd.id+'")">编辑</button><button class="btn btn-sm btn-delete-pwd" onclick="deletePassword("'+pwd.id+'")">删除</button></div></div>').join(''))+'</div></div>'; }catch(e){} } const debounceSearchPasswords=debounce(loadPasswords,300); let currentPassword=null; async function viewPassword(id){ try{ const result=await api('/passwords/'+id); const modal=document.createElement('div'); modal.innerHTML='<div class="modal-overlay" onclick="this.parentElement.remove()"></div><div class="modal-content"><h3>'+escapeHtml(result.site_name)+'</h3><p>用户名:<span id="show-user">'+escapeHtml(result.username||'-')+'</span></p><p>密码:<strong id="show-password">'+escapeHtml(result.password)+'</strong></p><p>备注:<span id="show-notes">'+escapeHtml(result.notes||'-')+'</span></p><div class="modal-footer"><button class="btn btn-secondary" onclick="this.closest('.modal-content').parentElement.remove()">关闭</button><button class="btn btn-primary" onclick="copyText(document.getElementById('show-password').textContent)">复制密码</button></div></div>'; document.body.appendChild(modal); }catch(e){} } function openPasswordModal(id=null){ currentPassword=id; const modal=document.createElement('div'); modal.innerHTML='<div class="modal-overlay" onclick="this.parentElement.remove()"></div><div class="modal-content"><h3 id="pwd-modal-title">'+(id?'编辑密码':'添加密码')+'</h3><input type="text" id="pwd-site-input" placeholder="网站名称"><input type="text" id="pwd-user-input" placeholder="用户名"><input type="password" id="pwd-password-input" placeholder="密码"><textarea id="pwd-notes-input" placeholder="备注" rows="3"></textarea><div class="modal-footer"><button class="btn btn-secondary" onclick="this.closest('.modal-content').parentElement.remove()">取消</button><button class="btn btn-primary" onclick="savePassword()">保存</button></div></div>'; document.body.appendChild(modal); if(id){ api('/passwords/'+id).then(r=>{ document.getElementById('pwd-site-input').value=r.site_name||''; document.getElementById('pwd-user-input').value=r.username||''; document.getElementById('pwd-notes-input').value=r.notes||''; }); } } async function savePassword(){ const siteName=document.getElementById('pwd-site-input').value.trim(); const username=document.getElementById('pwd-user-input').value; const password=document.getElementById('pwd-password-input').value; const notes=document.getElementById('pwd-notes-input').value; if(!siteName){showToast('请输入网站名称','warning');return;} try{ const payload={site_name:siteName,username,notes}; if(password)payload.password=password; if(currentPassword){ await api('/passwords/'+currentPassword,{method:'PUT',body:JSON.stringify(payload)}); showToast('更新成功','success'); }else{ if(!password){showToast('请输入密码','warning');return;} await api('/passwords',{method:'POST',body:JSON.stringify(payload)}); showToast('添加成功','success'); } document.querySelector('.modal-overlay').parentElement.remove(); loadPasswords(); }catch(e){} } async function deletePassword(id){ if(!confirm('确定删除这个密码?'))return; try{ await api('/passwords/'+id,{method:'DELETE'}); showToast('已删除','success'); loadPasswords(); }catch(e){} } // 备忘录管理 let memoPriority=''; async function loadMemos(){ showLoading(); try{ const result=await api('/memos'+(memoPriority?'?priority='+encodeURIComponent(memoPriority):'')); const memos=result.memos||[]; const main=document.getElementById('main-area'); main.innerHTML='<div class="memos-page"><div class="page-header"><h2>备忘录管理</h2><button class="btn btn-primary" onclick="openMemoModal()">+ 新建备忘录</button></div><div style="margin-bottom:1rem"><label>筛选:<select onchange="memoPriority=this.value;loadMemos()"><option value="">全部</option><option value="high">高优先级</option><option value="medium">中优先级</option><option value="low">低优先级</option></select></label></div><div class="memos-list" id="memos-list">'+(memos.length===0?'<div class="empty">暂无备忘录</div>':memos.map(memo=>'<div class="memo-card" data-id="'+memo.id+'"><div class="memo-header"><h3 class="memo-title">'+escapeHtml(memo.title)+'</h3><span class="priority-badge priority-'+memo.priority+'">'+(memo.priority==='high'?'高':memo.priority==='medium'?'中':'低')+'优先级</span></div><p class="memo-date">📅 '+escapeHtml(memo.date||formatDate(memo.created_at))+'</p><p class="memo-time">'+formatDate(memo.updated_at)+'</p><div class="memo-actions"><button class="btn btn-sm btn-edit" onclick="openMemoModal("'+memo.id+'")">编辑</button><button class="btn btn-sm btn-delete-memo" onclick="deleteMemo("'+memo.id+'")">删除</button></div></div>').join(''))+'</div></div>'; }catch(e){} } let currentMemo=null; function openMemoModal(id=null){ currentMemo=id; const modal=document.createElement('div'); modal.innerHTML='<div class="modal-overlay" onclick="this.parentElement.remove()"></div><div class="modal-content"><h3 id="memo-modal-title">'+(id?'编辑备忘录':'新建备忘录')+'</h3><input type="text" id="memo-title-input" placeholder="标题"><textarea id="memo-content-input" placeholder="内容" rows="5"></textarea><input type="date" id="memo-date-input"><select id="memo-priority-input"><option value="low">低优先级</option><option value="medium" selected>中优先级</option><option value="high">高优先级</option></select><div class="modal-footer"><button class="btn btn-secondary" onclick="this.closest('.modal-content').parentElement.remove()">取消</button><button class="btn btn-primary" onclick="saveMemo()">保存</button></div></div>'; document.body.appendChild(modal); if(id){ api('/memos/'+id).then(r=>{ document.getElementById('memo-title-input').value=r.title||''; document.getElementById('memo-content-input').value=r.content||''; document.getElementById('memo-date-input').value=r.date||''; document.getElementById('memo-priority-input').value=r.priority||'medium'; }); }else{ document.getElementById('memo-date-input').value=new Date().toISOString().substring(0,10); } } async function saveMemo(){ const title=document.getElementById('memo-title-input').value.trim(); const content=document.getElementById('memo-content-input').value; const priority=document.getElementById('memo-priority-input').value; const date=document.getElementById('memo-date-input').value; if(!title){showToast('请输入标题','warning');return;} try{ if(currentMemo){ await api('/memos/'+currentMemo,{method:'PUT',body:JSON.stringify({title,content,priority,date})}); showToast('更新成功','success'); }else{ await api('/memos',{method:'POST',body:JSON.stringify({title,content,priority,date})}); showToast('创建成功','success'); } document.querySelector('.modal-overlay').parentElement.remove(); loadMemos(); }catch(e){} } async function deleteMemo(id){ if(!confirm('确定删除这个备忘录?'))return; try{ await api('/memos/'+id,{method:'DELETE'}); showToast('已删除','success'); loadMemos(); }catch(e){} } // 分类管理 let editingCategoryId=null; let selectedColor='#1a73e8'; const colors=['#1a73e8','#34a853','#ea4335','#fbbc04','#9c27b0','#ff6b6b','#4ecdc4','#95e1d3']; async function loadCategories(){ showLoading(); try{ const result=await api('/categories'); categories=result.categories||[]; const main=document.getElementById('main-area'); main.innerHTML='<div class="categories-page"><div class="page-header"><h2>分类管理</h2><button class="btn btn-primary" onclick="openCategoryModal()">+ 新建分类</button></div><div class="categories-grid">'+(categories.length===0?'<div class="empty" style="grid-column:1/-1">暂无分类</div>':categories.map(cat=>'<div class="category-item" style="border-color:'+cat.color+'" onclick="openCategoryModal("'+cat.id+'")">'+escapeHtml(cat.name)+'</div>').join(''))+'</div></div>'; }catch(e){} } function openCategoryModal(id=null){ editingCategoryId=id; const category=id?categories.find(c=>c.id===id):null; if(category)selectedColor=category.color; const modal=document.createElement('div'); modal.innerHTML='<div class="modal-overlay" onclick="this.parentElement.remove()"></div><div class="modal-content"><h3>'+(id?'编辑分类':'新建分类')+'</h3><input type="text" id="category-name-input" placeholder="分类名称" value="'+escapeHtml(category?category.name:'')+'"><label>颜色:<div class="color-picker">'+colors.map(c=>'<div class="color-option'+(c===selectedColor?' selected':'')+'" style="background:'+c+'" onclick="selectedColor=this.style.background;document.querySelectorAll('.color-option').forEach(o=>o.classList.remove('selected'));this.classList.add('selected')"></div>').join('')+'</div></label><div class="modal-footer"><button class="btn btn-secondary" onclick="this.closest('.modal-content').parentElement.remove()">取消</button>'+(id?'<button class="btn btn-delete" onclick="deleteCategory()">删除</button>':'')+'<button class="btn btn-primary" onclick="saveCategory()">保存</button></div></div>'; document.body.appendChild(modal); } async function saveCategory(){ const name=document.getElementById('category-name-input').value.trim(); if(!name){showToast('请输入分类名称','warning');return;} try{ if(editingCategoryId){ await api('/categories/'+editingCategoryId,{method:'PUT',body:JSON.stringify({name,color:selectedColor})}); showToast('更新成功','success'); }else{ await api('/categories',{method:'POST',body:JSON.stringify({name,color:selectedColor})}); showToast('创建成功','success'); } document.querySelector('.modal-overlay').parentElement.remove(); loadCategories(); }catch(e){} } async function deleteCategory(){ if(!editingCategoryId||!confirm('确定删除这个分类?'))return; try{ await api('/categories/'+editingCategoryId,{method:'DELETE'}); showToast('已删除','success'); document.querySelector('.modal-overlay').parentElement.remove(); loadCategories(); }catch(e){} } // AI 聊天 async function sendMessage(){ const input=document.getElementById('chat-input'); const message=input.value.trim(); if(!message)return; const chatMessagesDiv=document.getElementById('chat-messages'); chatMessagesDiv.innerHTML+='<div class="message user"><div class="message-content">'+escapeHtml(message)+'</div></div>'; input.value=''; chatMessagesDiv.scrollTop=chatMessagesDiv.scrollHeight; const loadingDiv=document.createElement('div'); loadingDiv.className='message ai'; loadingDiv.innerHTML='<div class="message-content">思考中...</div>'; chatMessagesDiv.appendChild(loadingDiv); chatMessagesDiv.scrollTop=chatMessagesDiv.scrollHeight; try{ const result=await api('/ai/query',{method:'POST',body:JSON.stringify({question:message})}); loadingDiv.remove(); chatMessagesDiv.innerHTML+='<div class="message ai"><div class="message-content">'+renderMarkdown(result.answer)+'</div></div>'; }catch(e){ loadingDiv.remove(); chatMessagesDiv.innerHTML+='<div class="message ai"><div class="message-content">抱歉,发生了错误。</div></div>'; } chatMessagesDiv.scrollTop=chatMessagesDiv.scrollHeight; } // 初始化 async function init(){ if(authToken){ try{ const result=await api('/auth/me'); currentUser=result.user; renderApp(); }catch(e){ renderAuthPage(true); } }else{ renderAuthPage(true); } } // 暴露到全局 window.navigateTo=navigateTo; window.logout=logout; window.loadDashboard=loadDashboard; window.loadNotes=loadNotes; window.loadPasswords=loadPasswords; window.loadMemos=loadMemos; window.loadCategories=loadCategories; window.renderAuthPage=renderAuthPage; window.handleAuth=handleAuth; window.viewNote=viewNote; window.openNoteModal=openNoteModal; window.saveNote=saveNote; window.deleteNote=deleteNote; window.aiCategorizeNote=aiCategorizeNote; window.debounceSearchNotes=debounceSearchNotes; window.debounceSearchPasswords=debounceSearchPasswords; window.viewPassword=viewPassword; window.openPasswordModal=openPasswordModal; window.savePassword=savePassword; window.deletePassword=deletePassword; window.openMemoModal=openMemoModal; window.saveMemo=saveMemo; window.deleteMemo=deleteMemo; window.openCategoryModal=openCategoryModal; window.saveCategory=saveCategory; window.deleteCategory=deleteCategory; window.sendMessage=sendMessage; window.copyText=copyText; // 启动 init(); </script> </body> </html>