Files
soul/soul-admin/dist/index.html

438 lines
25 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>管理后台 - Soul创业派对</title>
<script type="module" crossorigin src="/assets/index-CbOmKBRd.js?v=5"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DBQ1UORI.css">
</head>
<body>
<div id="root"></div>
<script>
(function(){
var CSS=document.createElement('style');
CSS.textContent=`
.si-row-actions{display:inline-flex;align-items:center;gap:4px}
.si-row-actions .si-del{opacity:0;visibility:hidden;transition:opacity .2s,visibility .2s}
.si-row-actions:hover .si-del{opacity:1;visibility:visible}
.si-del{padding:2px 8px;font-size:11px;border-radius:4px;cursor:pointer;background:transparent;
border:1px solid #7f1d1d;color:#ef4444;margin-left:6px;transition:all .15s}
.si-del:hover{background:#7f1d1d;color:#fff}
.si-plus{padding:2px 6px;font-size:12px;border-radius:4px;cursor:pointer;background:transparent;
border:1px solid #2dd4a8;color:#2dd4a8;margin-left:4px;transition:all .15s}
.si-plus:hover{background:#2dd4a8;color:#0a0e17}
.si-free-toggle{padding:2px 8px;font-size:11px;border-radius:4px;cursor:pointer;margin-left:6px;
border:1px solid #475569;color:#94a3b8;transition:all .15s;user-select:none}
.si-free-toggle:hover{border-color:#2dd4a8;color:#2dd4a8}
.si-free-toggle.paid{border-color:#f59e0b;color:#f59e0b}
.si-drag-handle{cursor:grab;opacity:.5;padding:2px 6px;margin-right:4px;user-select:none}
.si-drag-handle:active{cursor:grabbing}
.si-dragging{opacity:.5;background:rgba(45,212,168,.1)}
.si-drop-target{border:2px dashed #2dd4a8;border-radius:4px}
.si-panel{background:#111827;border:1px solid #1e293b;border-radius:10px;padding:20px;margin:16px 0}
.si-panel h3{font-size:15px;margin:0 0 14px;color:#e0e6ed}
.si-panel label{display:block;font-size:12px;color:#94a3b8;margin:10px 0 4px}
.si-panel input,.si-panel select,.si-panel textarea{width:100%;padding:8px 10px;box-sizing:border-box;
background:#0a0e17;border:1px solid #1e293b;border-radius:6px;color:#e0e6ed;font-size:13px;outline:none}
.si-panel input:focus,.si-panel textarea:focus{border-color:#2dd4a8}
.si-panel textarea{min-height:160px;font-family:monospace;resize:vertical}
.si-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.si-submit{width:100%;padding:10px;margin-top:14px;background:#2dd4a8;color:#0a0e17;
border:none;border-radius:6px;font-size:14px;font-weight:600;cursor:pointer}
.si-submit:hover{background:#22b896}
.si-api{font-family:monospace;font-size:12px;line-height:1.7;color:#94a3b8}
.si-api pre{background:#0a0e17;border:1px solid #1e293b;border-radius:6px;padding:12px;
overflow-x:auto;margin:6px 0 14px;font-size:12px;color:#2dd4a8;white-space:pre-wrap}
.si-api h4{color:#e0e6ed;font-size:13px;margin:16px 0 4px;font-family:sans-serif}
.si-token-box{background:#0a0e17;border:1px solid #2dd4a8;border-radius:8px;padding:14px;margin-bottom:20px}
.si-token-box .si-token-row{display:flex;gap:8px;align-items:center;margin-top:8px}
.si-token-box input{flex:1;padding:8px 10px;background:#111827;border:1px solid #1e293b;border-radius:6px;color:#2dd4a8;font-size:12px;font-family:monospace}
.si-token-btn{padding:8px 16px;border-radius:6px;font-size:13px;cursor:pointer;border:none;background:#2dd4a8;color:#0a0e17;font-weight:600}
.si-token-btn:hover{background:#22b896}
.si-token-btn.copy{background:#1e293b;color:#e0e6ed}
.si-token-btn.copy:hover{background:#334155}
.si-toast{position:fixed;top:16px;right:16px;padding:10px 18px;border-radius:6px;
font-size:13px;z-index:99999;animation:siFade .25s}
.si-toast.ok{background:#065f46;color:#6ee7b7}
.si-toast.err{background:#7f1d1d;color:#fca5a5}
@keyframes siFade{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}
`;
document.head.appendChild(CSS);
var API=(window.location.hostname||'').indexOf('souladmin')>=0?'':'https://souldev.quwanzhi.com';
var token=localStorage.getItem('admin_token')||'';
function toast(m,ok){var t=document.createElement('div');t.className='si-toast '+(ok!==false?'ok':'err');
t.textContent=m;document.body.appendChild(t);setTimeout(function(){t.remove()},3000)}
function apicall(method,path,body){
var opts={method:method,headers:{'Content-Type':'application/json'}};
if(token)opts.headers['Authorization']='Bearer '+token;
if(body)opts.body=JSON.stringify(body);
return fetch(API+path,opts).then(function(r){return r.json()}).catch(function(e){return{success:false,error:e.message}})
}
function auth(){
if(token)return apicall('GET','/api/admin').then(function(r){if(r.success)return true;return doLogin()});
return doLogin()
}
function doLogin(){
return apicall('POST','/api/admin',{username:'admin',password:'admin123'}).then(function(r){
if(r.success&&r.token){token=r.token;localStorage.setItem('admin_token',token);return true}
return false
})
}
function findBtn(text){
var all=document.querySelectorAll('button');
for(var i=0;i<all.length;i++){if(all[i].textContent.trim()===text)return all[i]}
return null
}
var done=false;
function hideRedundantButtons(){
['初始化数据库','同步到数据库','导入','导出','同步飞书','上传内容'].forEach(function(t){
var b=findBtn(t);if(b)b.style.display='none';
});
}
function run(){
if(done)return;
if(!location.pathname.includes('content')&&!location.hash.includes('content'))return;
var initBtn=findBtn('初始化数据库');
if(!initBtn)return;
done=true;
// === 1. 移除5个按钮+上传内容,只保留一个"API 接口"(持续执行防重复页)===
hideRedundantButtons();
setInterval(hideRedundantButtons,800);
var btnParent=initBtn&&initBtn.parentElement;
if(btnParent&&!btnParent.querySelector('.si-api-only-btn')){
var apiBtn=document.createElement('button');
apiBtn.className='si-api-only-btn '+initBtn.className;apiBtn.style.display='inline-flex';
apiBtn.textContent='API 接口';
apiBtn.onclick=function(e){e.preventDefault();e.stopPropagation();togglePanel('api')};
btnParent.appendChild(apiBtn);
}
// === 2. 创建面板(插入到 tabs 之前) ===
var tabBar=document.querySelector('[role="tablist"]');
if(!tabBar){
var tabs=findBtn('章节管理');
if(tabs)tabBar=tabs.parentElement;
}
var insertTarget=tabBar||(initBtn&&initBtn.parentElement);
// 上传面板
var upP=document.createElement('div');
upP.className='si-panel';upP.id='si-upload';upP.style.display='none';
upP.innerHTML='<h3>上传新章节</h3>'
+'<div class="si-row"><div><label>章节ID (留空自动)</label><input id="si-uid" placeholder="如 1.6"></div>'
+'<div><label>定价 (0=免费)</label><input type="number" id="si-uprice" value="1" step="0.1" min="0"></div></div>'
+'<label>标题 *</label><input id="si-utitle" placeholder="章节标题">'
+'<div class="si-row"><div><label>所属篇</label><select id="si-upart">'
+'<option value="part-1">第一篇|真实的人</option><option value="part-2">第二篇|真实的行业</option>'
+'<option value="part-3">第三篇|真实的错误</option><option value="part-4">第四篇|真实的赚钱</option>'
+'<option value="part-5">第五篇|真实的社会</option><option value="appendix">附录</option>'
+'<option value="intro">序言</option><option value="outro">尾声</option></select></div>'
+'<div><label>所属章</label><select id="si-uchap">'
+'<option value="chapter-1">第1章</option><option value="chapter-2">第2章</option>'
+'<option value="chapter-3">第3章</option><option value="chapter-4">第4章</option>'
+'<option value="chapter-5">第5章</option><option value="chapter-6">第6章</option>'
+'<option value="chapter-7">第7章</option><option value="chapter-8">第8章</option>'
+'<option value="chapter-9">第9章</option><option value="chapter-10">第10章</option>'
+'<option value="chapter-11">第11章</option><option value="appendix">附录</option>'
+'<option value="preface">序言</option><option value="epilogue">尾声</option></select></div></div>'
+'<label>内容 (Markdown) *</label><textarea id="si-ucontent" placeholder="正文内容... 图片占位用 {{image_1}}"></textarea>'
+'<label>图片URL (每行一个)</label><textarea id="si-uimgs" style="min-height:60px" placeholder="https://example.com/1.png"></textarea>'
+'<button class="si-submit" id="si-submit-btn">上传章节</button>';
insertTarget.parentElement.insertBefore(upP,insertTarget);
document.getElementById('si-submit-btn').onclick=function(){siUpload()};
// API文档面板
var apiP=document.createElement('div');
apiP.className='si-panel';apiP.id='si-apidoc';apiP.style.display='none';
apiP.innerHTML='<div class="si-api">'
+'<h3 style="font-family:sans-serif">内容管理 API 接口文档</h3>'
+'<div class="si-token-box"><strong style="color:#e0e6ed">生成 TOKEN</strong> — 用于上传新章节、删除等操作<br>'
+'<div class="si-token-row"><button class="si-token-btn" id="si-gen-token">生成 TOKEN</button>'
+'<input type="text" id="si-token-input" readonly placeholder="点击生成后显示,可复制用于 curl/Skill 上传" style="cursor:pointer">'
+'<button class="si-token-btn copy" id="si-copy-token">复制</button></div></div>'
+'<p>基础域名: <code>https://soulapi.quwanzhi.com</code> (正式) / <code>https://souldev.quwanzhi.com</code> (开发)</p>'
+'<h4>1. 获取所有章节 (无需认证)</h4><pre>GET /api/book/all-chapters\n\ncurl https://soulapi.quwanzhi.com/api/book/all-chapters</pre>'
+'<h4>2. 获取单章内容</h4><pre>GET /api/book/chapter/:id\n\ncurl https://soulapi.quwanzhi.com/api/book/chapter/1.1</pre>'
+'<h4>3. 管理员登录 (获取Token)</h4><pre>POST /api/admin\nBody: {"username":"admin","password":"admin123"}\n\ncurl -X POST https://souldev.quwanzhi.com/api/admin \\\n -H "Content-Type: application/json" \\\n -d \'{"username":"admin","password":"admin123"}\'</pre>'
+'<h4>4. 创建/更新章节 (需Token)</h4><pre>POST /api/db/book\nAuthorization: Bearer {token}\nBody: {\n "id": "1.6",\n "title": "标题",\n "content": "Markdown正文",\n "price": 1.0,\n "partId": "part-1",\n "chapterId": "chapter-1"\n}\n\ncurl -X POST https://souldev.quwanzhi.com/api/db/book \\\n -H "Authorization: Bearer TOKEN" \\\n -H "Content-Type: application/json" \\\n -d \'{"id":"1.6","title":"新章节","content":"正文","price":1.0,"partId":"part-1","chapterId":"chapter-1"}\'</pre>'
+'<h4>5. 删除章节 (需Token)</h4><pre>DELETE /api/admin/content/:id\n\ncurl -X DELETE https://souldev.quwanzhi.com/api/admin/content/1.6 \\\n -H "Authorization: Bearer TOKEN"</pre>'
+'<h4>6. 命令行上传 (数据库直写)</h4><pre>python3 content_upload.py --title "标题" --price 1.0 --content "正文" \\\n --part part-1 --chapter chapter-1\n\npython3 content_upload.py --list-structure # 查看篇章结构\npython3 content_upload.py --list-chapters # 列出所有章节</pre>'
+'<h4>7. 数据库直连</h4><pre>Host: 56b4c23f6853c.gz.cdb.myqcloud.com:14413\nUser: cdb_outerroot\nDB: soul_miniprogram\n表: chapters</pre>'
+'</div>';
insertTarget.parentElement.insertBefore(apiP,insertTarget);
document.getElementById('si-gen-token').onclick=function(){
var inp=document.getElementById('si-token-input');
inp.value='获取中...';
doLogin().then(function(ok){
if(ok&&token){inp.value=token;toast('TOKEN 已生成,可复制使用')}
else{inp.value='';toast('获取失败',false)}
});
};
document.getElementById('si-copy-token').onclick=function(){
var inp=document.getElementById('si-token-input');
if(!inp.value||inp.value==='获取中...'){toast('请先生成 TOKEN',false);return}
inp.select();document.execCommand('copy');
toast('已复制到剪贴板');
};
document.getElementById('si-token-input').onclick=function(){this.select()};
// === 3. 内容操作:删除(hover)、免费/付费、加号在章节、拖拽 ===
addContentActions();
addChapterPlus();
addDragDrop();
new MutationObserver(function(){addContentActions();addChapterPlus();addDragDrop();}).observe(document.getElementById('root'),{childList:true,subtree:true});
}
var activePanel='';
var siPrefill={};
function togglePanel(name,prefill){
var up=document.getElementById('si-upload');
var ap=document.getElementById('si-apidoc');
if(!up||!ap)return;
if(prefill)siPrefill=prefill;
if(activePanel===name&&name!=='upload'){ap.style.display='none';activePanel='';return}
if(name==='upload'){up.style.display='block';ap.style.display='none';applyPrefill();activePanel='upload';return}
if(name==='api'){up.style.display='none';ap.style.display='block';activePanel='api';return}
}
function applyPrefill(){
if(siPrefill.partId){var s=document.getElementById('si-upart');if(s)s.value=siPrefill.partId}
if(siPrefill.chapterId){var c=document.getElementById('si-uchap');if(c)c.value=siPrefill.chapterId}
}
function getSectionInfo(row){
var p=row;
for(var i=0;i<8&&p;i++){p=p.parentElement;if(!p)break;
var t=(p.textContent||'').substring(0,80);
if(/附录/.test(t))return{partId:'appendix',chapterId:'appendix'};
if(/序言/.test(t))return{partId:'intro',chapterId:'preface'};
if(/尾声/.test(t))return{partId:'outro',chapterId:'epilogue'};
if(/第一篇/.test(t))return{partId:'part-1',chapterId:'chapter-1'};
if(/第二篇/.test(t))return{partId:'part-2',chapterId:'chapter-3'};
if(/第三篇/.test(t))return{partId:'part-3',chapterId:'chapter-6'};
if(/第四篇/.test(t))return{partId:'part-4',chapterId:'chapter-8'};
if(/第五篇/.test(t))return{partId:'part-5',chapterId:'chapter-10'};
}
return null;
}
function addContentActions(){
var all=document.querySelectorAll('button');
for(var i=0;i<all.length;i++){
var b=all[i];
if(b.textContent.trim()==='编辑'&&!b.dataset.sid){
b.dataset.sid='1';
var par=b.parentElement;
if(!par.classList.contains('si-row-actions'))par.classList.add('si-row-actions');
var plusInSection=par.querySelector('.si-plus');
if(plusInSection)plusInSection.remove();
var del=document.createElement('button');
del.className='si-del';
del.textContent='删除';
(function(editBtn){
del.onclick=function(e){
e.stopPropagation();e.preventDefault();
var row=editBtn.closest('[class]');
var txt=row?row.textContent:'';
var m=txt.match(/([\d]+\.[\d]+|appendix-[\w]+|preface|epilogue)/);
var sid=m?m[0]:'';
var name=txt.substring(0,40).replace(/读取|编辑|删除|免费|付费|¥[\d.]+|\+/g,'').trim();
if(!confirm('确定删除「'+name+'」'+(sid?' (ID:'+sid+')':'')+' '))return;
auth().then(function(ok){
if(!ok){toast('认证失败',false);return}
apicall('DELETE','/api/admin/content/'+(sid||name)).then(function(r){
if(r.success!==false){toast('已删除');setTimeout(function(){location.reload()},800)}
else{
apicall('DELETE','/api/db/book?action=delete&id='+(sid||name)).then(function(r2){
if(r2.success!==false){toast('已删除');setTimeout(function(){location.reload()},800)}
else toast('删除失败: '+(r2.error||r.error||''),false)
})
}
})
})
}
})(b);
par.appendChild(del);
addFreeToggle(b);
}
}
}
function addChapterPlus(){
var seen=new Set();
var rows=document.querySelectorAll('[class]');
for(var i=0;i<rows.length;i++){
var r=rows[i];
if(r.querySelector('.si-chap-plus')||seen.has(r))continue;
var t=(r.textContent||'').trim();
if((/序言|附录|尾声|第一篇|第二篇|第三篇|第四篇|第五篇/.test(t)&&/\d+节/.test(t))){
seen.add(r);
r.dataset.draggableItem='chapter';
var plus=document.createElement('button');
plus.className='si-plus si-chap-plus';plus.textContent='+';plus.title='在此章节下新建小节';
plus.onclick=function(e){e.stopPropagation();e.preventDefault();
var info=getSectionInfo(this.parentElement);
togglePanel('upload',info||{});
};
r.style.display=r.style.display||'flex';r.style.alignItems='center';
r.appendChild(plus);
}
}
}
function addDragDrop(){
var items=document.querySelectorAll('[data-draggable-item]');
items.forEach(function(el){if(el.dataset.siDrag)return;el.dataset.siDrag='1';
el.draggable=true;el.style.cursor='grab';
el.addEventListener('dragstart',onDragStart);
el.addEventListener('dragover',onDragOver);el.addEventListener('drop',onDrop);
});
var sect=document.querySelectorAll('button');
for(var j=0;j<sect.length;j++){
var sb=sect[j];
if(sb.textContent.trim()==='编辑'){
var row=sb.closest('[class]');
if(row&&!row.dataset.siDrag){
row.draggable=true;row.dataset.siDrag='1';row.dataset.draggableItem='section';
row.style.cursor='grab';
row.addEventListener('dragstart',onDragStart);
row.addEventListener('dragover',onDragOver);
row.addEventListener('drop',onDrop);
}
}
}
}
var dragEl=null;
function onDragStart(e){dragEl=e.currentTarget;e.dataTransfer.effectAllowed='move';
e.dataTransfer.setData('text/plain','');e.currentTarget.classList.add('si-dragging');}
function onDragOver(e){e.preventDefault();e.dataTransfer.dropEffect='move';
var t=e.currentTarget;
if(t!==dragEl){t.classList.add('si-drop-target');
var sibs=t.parentElement?t.parentElement.children:[];
for(var k=0;k<sibs.length;k++){if(sibs[k]!==t)sibs[k].classList.remove('si-drop-target')}
}}
function onDrop(e){e.preventDefault();
document.querySelectorAll('.si-drop-target').forEach(function(x){x.classList.remove('si-drop-target')});
if(!dragEl)return;
dragEl.classList.remove('si-dragging');
var dest=e.currentTarget;
if(dest!==dragEl&&dest.parentNode===dragEl.parentNode){
var par=dest.parentNode;
var list=Array.from(par.children).filter(function(c){return c.dataset.siDrag||c.draggable;});
var i0=list.indexOf(dragEl),i1=list.indexOf(dest);
if(i0>=0&&i1>=0&&i0!==i1){
if(i0<i1)par.insertBefore(dragEl,dest.nextSibling);
else par.insertBefore(dragEl,dest);
var newList=Array.from(par.children).filter(function(c){return c.dataset.siDrag||c.draggable;});
var ids=newList.map(function(x){return(x.textContent.match(/([\d]+\.[\d]+|appendix-[\w-]+|preface|epilogue)/)||[])[1]}).filter(Boolean);
if(ids.length>0)auth().then(function(ok){
if(ok)apicall('POST','/api/db/book/order',{ids:ids}).then(function(r){if(r&&r.success)toast('已排序');else toast('排序已更新(后端接口可后续对接)',false)})
});
}
}
dragEl=null;
}
document.addEventListener('dragend',function(){document.querySelectorAll('.si-dragging,.si-drop-target').forEach(function(x){x.classList.remove('si-dragging','si-drop-target')});dragEl=null});
function addFreeToggle(editBtn){
var row=editBtn.closest('[class]');
if(!row||row.querySelector('.si-free-toggle'))return;
var sid=(row.textContent.match(/([\d]+\.[\d]+|appendix-[\w-]+|preface|epilogue)/)||[])[1]||'';
var candidates=row.querySelectorAll('span, div, [class]');
for(var j=0;j<candidates.length;j++){
var el=candidates[j];
if(el.classList&&el.classList.contains('si-free-toggle'))continue;
var t=(el.textContent||'').trim();
if((t==='免费'||/^¥[\d.]+$/.test(t))&&el.children.length===0){
var isFree=t==='免费';
var toggle=document.createElement('span');
toggle.className='si-free-toggle'+(isFree?'':' paid');
toggle.textContent=isFree?'免费':'付费';
toggle.dataset.sectionId=sid;
toggle.dataset.price=isFree?'0':'1';
toggle.onclick=function(e){e.stopPropagation();e.preventDefault();
if(e.detail>=2)return;
var sectionId=toggle.dataset.sectionId;
if(!sectionId){toast('无法识别章节ID',false);return}
var toFree=toggle.textContent==='付费';
auth().then(function(ok){
if(!ok){toast('认证失败',false);return}
var pr=toFree?0:1;
apicall('POST','/api/db/book',{id:sectionId,isFree:toFree,price:pr}).then(function(r){
if(r.success!==false){toggle.textContent=toFree?'免费':'¥'+pr;toggle.classList.toggle('paid',!toFree);toggle.dataset.price=pr;toast('已更新')}
else toast('更新失败: '+(r.error||''),false)
})
})
};
toggle.ondblclick=function(e){e.stopPropagation();e.preventDefault();
var sectionId=toggle.dataset.sectionId;
if(!sectionId){toast('无法识别章节ID',false);return}
if(toggle.textContent==='免费'){
auth().then(function(ok){
if(!ok){toast('认证失败',false);return}
var pr=parseFloat(prompt('请输入付费金额','1'))||1;
apicall('POST','/api/db/book',{id:sectionId,isFree:false,price:pr}).then(function(r){
if(r.success!==false){toggle.textContent='¥'+pr;toggle.classList.add('paid');toggle.dataset.price=pr;toast('已更新')}
else toast('更新失败',false)
})
})
}else{
auth().then(function(ok){
if(!ok){toast('认证失败',false);return}
apicall('POST','/api/db/book',{id:sectionId,isFree:true,price:0}).then(function(r){
if(r.success!==false){toggle.textContent='免费';toggle.classList.remove('paid');toggle.dataset.price='0';toast('已设为免费')}
else toast('更新失败',false)
})
})
}
};
el.parentNode.replaceChild(toggle,el);
break;
}
}
}
function siUpload(){
var title=document.getElementById('si-utitle').value.trim();
var content=document.getElementById('si-ucontent').value.trim();
if(!title){toast('请填写标题',false);return}
if(!content){toast('请填写内容',false);return}
var imgs=document.getElementById('si-uimgs').value.trim().split('\n').filter(Boolean);
imgs.forEach(function(u,i){content=content.replace('{{image_'+(i+1)+'}}','![图片'+(i+1)+']('+u.trim()+')')});
var price=parseFloat(document.getElementById('si-uprice').value)||0;
var data={
id:document.getElementById('si-uid').value.trim()||undefined,
title:title,content:content,price:price,isFree:price===0,
partId:document.getElementById('si-upart').value,
chapterId:document.getElementById('si-uchap').value
};
toast('上传中...');
auth().then(function(ok){
if(!ok){toast('认证失败',false);return}
apicall('POST','/api/db/book',data).then(function(r){
if(r.success!==false){
toast('上传成功!');
document.getElementById('si-utitle').value='';
document.getElementById('si-ucontent').value='';
document.getElementById('si-uimgs').value='';
document.getElementById('si-uid').value='';
setTimeout(function(){location.reload()},1000)
}else toast('失败: '+(r.error||''),false)
})
})
}
setInterval(run,500);
new MutationObserver(run).observe(document.getElementById('root'),{childList:true,subtree:true});
})();
</script>
</body>
</html>