# 卡9A修复 · app.js分3块粘贴

## 🔧 修复步骤

上次粘贴太长导致app.js损坏。现在分**3小块**重新粘贴，每块~60行。

[main.py](http://main.py) 和 app.css 不用动，它们没问题 ✅

---

### 第1步 · 停服务器 + 恢复 app.js

```bash
pkill -f 'uvicorn main:app' 2>/dev/null
cp ~/chenxing-aircraft/frontend/app.js.bak-before-card9a ~/chenxing-aircraft/frontend/app.js
wc -l ~/chenxing-aircraft/frontend/app.js
echo "restored"
```

记下显示的行数（应该在1050-1150左右）

---

### 第2步 · 粘贴 Part A（基础函数）

粘贴后等显示 `Part A done` 再继续

```bash
cat >> ~/chenxing-aircraft/frontend/app.js << 'PARTA_EOF'

// ============================================================
// P2-card9A: Book Library Panel (replaces knowledge panel)
// ============================================================

(function(){
  document.addEventListener('DOMContentLoaded', function(){
    setTimeout(function(){
      var btn = document.getElementById('btnKnowledge');
      if(btn) btn.onclick = function(){ openPanel('panelKnowledge', loadBookLibrary); };
    }, 200);
  });
})();

var LIB_TAGS = [
  {key:'篇幅',cls:'lib-t-len'},{key:'类型',cls:'lib-t-gen'},
  {key:'时代',cls:'lib-t-era'},{key:'平台',cls:'lib-t-plat'},
  {key:'流派',cls:'lib-t-sty'},{key:'标签',cls:'lib-t-tag'},
  {key:'元素',cls:'lib-t-ele'}
];

function libFmt(n){
  if(n>=10000)return(n/10000).toFixed(1)+'万字';
  if(n>=1000)return(n/1000).toFixed(1)+'K字';
  return n+'字';
}

function libTags(c){
  if(!c)return '';
  var h='';
  LIB_TAGS.forEach(function(t){
    var v=c[t.key]; if(!v)return;
    v.split(/[、,]/).forEach(function(s){
      s=s.trim(); if(s) h+='<span class="lib-tag '+t.cls+'">'+t.key+' '+s+'</span>';
    });
  });
  return h;
}

function libCard(b){
  var h='<div class="lib-card" data-bid="'+b.book_id+'">';
  h+='<div class="lib-card-top">';
  h+='<div class="lib-card-name">'+esc(b.book_title)+'</div>';
  h+='<button class="lib-card-x" onclick="libDel(this)">✕</button>';
  h+='</div>';
  h+='<div class="lib-card-info">'+b.total_chunks.toLocaleString()+' 切片 · '+libFmt(b.total_chars)+'</div>';
  h+='<div class="lib-card-tags">'+libTags(b.classification)+'</div>';
  h+='</div>';
  return h;
}

var _libFileData = '';

function libFileChosen(inp){
  var f=inp.files[0]; if(!f)return;
  var el=document.getElementById('libUpFName');
  if(el) el.textContent=f.name;
  var r=new FileReader();
  r.onload=function(e){_libFileData=e.target.result;};
  r.readAsText(f);
}

function libToggleUp(){
  var f=document.getElementById('libUpForm');
  var a=document.getElementById('libUpArr');
  if(!f)return;
  if(f.style.display==='none'){f.style.display='block';if(a)a.textContent='▾';}
  else{f.style.display='none';if(a)a.textContent='▸';}
}
PARTA_EOF
wc -l ~/chenxing-aircraft/frontend/app.js
echo "Part A done"
```

---

### 第3步 · 粘贴 Part B（上传+加载）

粘贴后等显示 `Part B done` 再继续

```bash
cat >> ~/chenxing-aircraft/frontend/app.js << 'PARTB_EOF'

async function libUpload(){
  var msg=document.getElementById('libUpMsg');
  var name=(document.getElementById('libUpName')||{}).value||'';
  if(!name.trim()){if(msg)msg.textContent='请先填写书名';return;}
  if(!_libFileData){if(msg)msg.textContent='请选择文件';return;}
  var cl={
    length:(document.getElementById('libUL')||{}).value||'',
    genre:(document.getElementById('libUG')||{}).value||'',
    era:(document.getElementById('libUE')||{}).value||'',
    platform:(document.getElementById('libUP')||{}).value||'',
    style:(document.getElementById('libUS')||{}).value||'',
    tags:(document.getElementById('libUT')||{}).value||'',
    elements:(document.getElementById('libUEl')||{}).value||''
  };
  if(msg)msg.textContent='⏳ 正在入库...';
  try{
    var r=await fetch('/api/library/upload',{
      method:'POST',headers:{'Content-Type':'application/json'},
      body:JSON.stringify({book_title:name.trim(),content:_libFileData,classification:cl})
    });
    var d=await r.json();
    if(d.error){if(msg)msg.textContent='❌ '+d.error;return;}
    if(msg)msg.textContent='✅ 已入库！共 '+d.chunks+' 个切片';
    _libFileData='';
    ['libUpName','libUpFile','libUL','libUG','libUE','libUP','libUS','libUT','libUEl'].forEach(function(id){
      var e=document.getElementById(id); if(e){if(e.tagName==='SELECT')e.selectedIndex=0;else e.value='';}
    });
    var fn=document.getElementById('libUpFName'); if(fn)fn.textContent='';
    await libFetch();
  }catch(e){if(msg)msg.textContent='❌ 网络错误';}
}

async function libFetch(p){
  p=p||{};
  var qs=[];
  if(p.q)qs.push('q='+encodeURIComponent(p.q));
  if(p.length)qs.push('length='+encodeURIComponent(p.length));
  if(p.genre)qs.push('genre='+encodeURIComponent(p.genre));
  if(p.era)qs.push('era='+encodeURIComponent(p.era));
  if(p.platform)qs.push('platform='+encodeURIComponent(p.platform));
  if(p.style)qs.push('style='+encodeURIComponent(p.style));
  var url='/api/library'+(qs.length?'?'+qs.join('&'):'');
  try{
    var r=await fetch(url);
    var d=await r.json();
    var st=document.getElementById('libStat');
    var ls=document.getElementById('libList');
    var ri=document.getElementById('libResInfo');
    if(st){
      var tc=d.books.reduce(function(s,b){return s+b.total_chunks;},0);
      var ch=d.books.reduce(function(s,b){return s+b.total_chars;},0);
      st.innerHTML='共 <b>'+d.total_books+'</b> 本书 · <b>'+tc.toLocaleString()+'</b> 切片 · <b>'+libFmt(ch)+'</b>';
    }
    var isSearch=p.q||p.length||p.genre||p.era||p.platform||p.style;
    if(ri) ri.textContent=isSearch?'共搜到 '+d.total_books+' 本':'';
    if(ls){
      if(!d.books.length){
        ls.innerHTML='<div style="text-align:center;padding:32px;color:var(--text-dim);font-size:13px;">📚 书库还是空的～上传小说让晨星更聪明！</div>';
      }else{
        ls.innerHTML=d.books.map(libCard).join('');
      }
    }
  }catch(e){
    var ls=document.getElementById('libList');
    if(ls)ls.innerHTML='<div style="text-align:center;padding:24px;color:#ef5350;">❌ 加载失败</div>';
  }
}
PARTB_EOF
wc -l ~/chenxing-aircraft/frontend/app.js
echo "Part B done"
```

---

### 第4步 · 粘贴 Part C（搜索+主面板）

粘贴后等显示 `Part C done` 再继续

```bash
cat >> ~/chenxing-aircraft/frontend/app.js << 'PARTC_EOF'

function libSearch(){
  var p={};
  var q=(document.getElementById('libSQ')||{}).value||'';
  if(q.trim())p.q=q.trim();
  var fl=(document.getElementById('libFL')||{}).value; if(fl)p.length=fl;
  var fg=(document.getElementById('libFG')||{}).value; if(fg)p.genre=fg;
  var fe=(document.getElementById('libFE')||{}).value; if(fe)p.era=fe;
  var fp=(document.getElementById('libFP')||{}).value; if(fp)p.platform=fp;
  var fs=(document.getElementById('libFS')||{}).value; if(fs)p.style=fs;
  libFetch(p);
}

function libDel(btn){
  var card=btn.closest('.lib-card'); if(!card)return;
  var bid=card.getAttribute('data-bid');
  var nameEl=card.querySelector('.lib-card-name');
  var title=nameEl?nameEl.textContent:'';
  if(!confirm('确认删除「'+title+'」？所有切片将被删除。'))return;
  fetch('/api/library/'+bid,{method:'DELETE'}).then(function(){libFetch();});
}

async function loadBookLibrary(){
  var panel=document.getElementById('panelKnowledge'); if(!panel)return;
  var hdr=panel.querySelector('.panel-header span');
  if(hdr) hdr.innerHTML='📚 晨星的小说书库';
  var body=panel.querySelector('.panel-body'); if(!body)return;
  body.style.cssText='display:flex;flex-direction:column;padding:0;overflow:hidden;flex:1;';
  var S='padding:5px 6px;border:1px solid var(--border);border-radius:6px;background:var(--bg-2,var(--bg));color:var(--text);font-size:11px;';
  var I='width:100%;box-sizing:border-box;padding:7px 10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-2,var(--bg));color:var(--text);font-size:12px;';
  body.innerHTML=`<div id="libStat" style="padding:10px 12px;text-align:center;border-bottom:1px solid var(--border);font-size:12px;color:var(--text-dim);">⏳</div>
<div style="padding:10px 12px;border-bottom:1px solid var(--border);">
<div style="display:flex;align-items:center;justify-content:space-between;cursor:pointer;" onclick="libToggleUp()">
<span style="font-size:11px;font-weight:700;color:var(--text-dim);letter-spacing:.05em;">📤 上传书籍</span>
<span id="libUpArr" style="font-size:10px;color:var(--text-dim);">▸</span></div>
<div id="libUpForm" style="display:none;margin-top:8px;">
<input type="text" id="libUpName" placeholder="书名（必填）" style="${I}margin-bottom:6px;"/>
<div style="margin-bottom:6px;display:flex;align-items:center;gap:6px;">
<label style="padding:6px 12px;border:1px solid var(--border);border-radius:8px;font-size:11px;cursor:pointer;color:var(--text-dim);white-space:nowrap;">📎 选择文件<input type="file" id="libUpFile" accept=".txt,.md,.csv" style="display:none;" onchange="libFileChosen(this)"/></label>
<span id="libUpFName" style="font-size:11px;color:var(--text-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;"></span></div>
<div style="display:flex;gap:4px;margin-bottom:4px;">
<select id="libUL" style="flex:1;${S}"><option value="">篇幅</option><option>长篇</option><option>短篇</option></select>
<select id="libUG" style="flex:1;${S}"><option value="">类型</option><option>言情</option><option>耽美</option></select>
<select id="libUE" style="flex:1;${S}"><option value="">时代</option><option>现言</option><option>古言</option><option>年代</option><option>未来</option></select></div>
<div style="display:flex;gap:4px;margin-bottom:4px;">
<select id="libUP" style="flex:1;${S}"><option value="">平台</option><option>晋江</option><option>番茄</option><option>点众</option><option>黑岩</option></select>
<select id="libUS" style="flex:1;${S}"><option value="">流派</option><option>剧情流</option><option>情绪流</option></select>
<input id="libUT" placeholder="标签（自填）" style="flex:1;${S}"/></div>
<input id="libUEl" placeholder="元素（如：娱乐圈、打脸）" style="${I}font-size:11px;margin-bottom:6px;"/>
<button onclick="libUpload()" style="width:100%;padding:7px;border:none;border-radius:8px;background:var(--accent);color:#fff;font-size:12px;font-weight:700;cursor:pointer;">存入书库</button>
<div id="libUpMsg" style="font-size:11px;color:var(--text-dim);text-align:center;margin-top:4px;min-height:16px;"></div>
</div></div>
<div style="padding:10px 12px;border-bottom:1px solid var(--border);">
<input id="libSQ" placeholder="🔍 搜索书名 / 标签 / 元素..." style="${I}margin-bottom:6px;" onkeydown="if(event.keyCode===13)libSearch()"/>
<div style="display:flex;gap:4px;margin-bottom:4px;">
<select id="libFL" style="flex:1;${S}"><option value="">篇幅</option><option>长篇</option><option>短篇</option></select>
<select id="libFG" style="flex:1;${S}"><option value="">类型</option><option>言情</option><option>耽美</option></select>
<select id="libFE" style="flex:1;${S}"><option value="">时代</option><option>现言</option><option>古言</option><option>年代</option><option>未来</option></select></div>
<div style="display:flex;gap:4px;">
<select id="libFP" style="flex:1;${S}"><option value="">平台</option><option>晋江</option><option>番茄</option><option>点众</option><option>黑岩</option></select>
<select id="libFS" style="flex:1;${S}"><option value="">流派</option><option>剧情流</option><option>情绪流</option></select>
<button onclick="libSearch()" style="padding:5px 14px;border:none;border-radius:6px;background:var(--accent);color:#fff;font-size:11px;font-weight:700;cursor:pointer;white-space:nowrap;">搜索</button></div></div>
<div id="libResInfo" style="padding:6px 12px 2px;font-size:11px;color:var(--text-dim);"></div>
<div id="libList" style="flex:1;overflow-y:auto;padding:4px 12px 12px;"></div>`;
  await libFetch();
}
PARTC_EOF
wc -l ~/chenxing-aircraft/frontend/app.js
echo "Part C done"
```

---

### 第5步 · 重启服务器

```bash
cd ~/chenxing-aircraft/backend
nohup uvicorn main:app --host 0.0.0.0 --port 8000 > server.log 2>&1 &
sleep 2
echo "server restarted"
```

---

### 第6步 · 测试

浏览器 `Cmd+Shift+R` 强制刷新 → 点侧栏「知识库」→ 截图给宝宝！