内容管理页面增强:上传/删除/API文档集成到原有admin页面

- 将"初始化数据库"和"同步到数据库"按钮替换为"上传内容"和"API接口"
- 隐藏"导入"、"导出"、"同步飞书"按钮
- 每个章节条目增加"删除"按钮
- 添加上传面板和API接口文档面板(可展开/收起)
- 保持原有侧边栏和页面风格不变

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
卡若
2026-02-21 15:30:20 +08:00
parent 76d90a0397
commit 6e276fca61
5 changed files with 803 additions and 0 deletions

454
soul-admin/dist/assets/index-CbOmKBRd.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

244
soul-admin/dist/index.html vendored Normal file
View File

@@ -0,0 +1,244 @@
<!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"></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-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-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-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='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 run(){
if(done)return;
if(!location.pathname.includes('content'))return;
var initBtn=findBtn('初始化数据库');
if(!initBtn)return;
done=true;
// === 1. 改造顶部按钮 ===
var syncBtn=findBtn('同步到数据库');
var importBtn=findBtn('导入');
var exportBtn=findBtn('导出');
var feishuBtn=findBtn('同步飞书');
// 把前两个按钮改成"上传内容"和"API接口"
initBtn.textContent='上传内容';
initBtn.onclick=function(e){e.preventDefault();e.stopPropagation();togglePanel('upload')};
// 去掉原来的事件
var newInit=initBtn.cloneNode(true);
newInit.onclick=function(e){e.preventDefault();e.stopPropagation();togglePanel('upload')};
initBtn.parentNode.replaceChild(newInit,initBtn);
if(syncBtn){
var newSync=syncBtn.cloneNode(true);
newSync.textContent='API 接口';
newSync.onclick=function(e){e.preventDefault();e.stopPropagation();togglePanel('api')};
syncBtn.parentNode.replaceChild(newSync,syncBtn);
}
// 隐藏其余按钮
if(importBtn)importBtn.style.display='none';
if(exportBtn)exportBtn.style.display='none';
if(feishuBtn)feishuBtn.style.display='none';
// === 2. 创建面板(插入到 tabs 之前) ===
var tabBar=document.querySelector('[role="tablist"]');
if(!tabBar){
var tabs=findBtn('章节管理');
if(tabs)tabBar=tabs.parentElement;
}
var insertTarget=tabBar||newInit.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>'
+'<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);
// === 3. 给每个章节添加删除按钮 ===
addDelBtns();
new MutationObserver(function(){addDelBtns()}).observe(document.getElementById('root'),{childList:true,subtree:true});
}
var activePanel='';
function togglePanel(name){
var up=document.getElementById('si-upload');
var ap=document.getElementById('si-apidoc');
if(!up||!ap)return;
if(activePanel===name){up.style.display='none';ap.style.display='none';activePanel='';return}
up.style.display=name==='upload'?'block':'none';
ap.style.display=name==='api'?'block':'none';
activePanel=name;
}
function addDelBtns(){
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 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);
b.parentElement.appendChild(del);
}
}
}
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>

52
部署到GitHub与宝塔.sh Executable file
View File

@@ -0,0 +1,52 @@
#!/bin/bash
# 1) 以本地为准推送到 GitHub yongpxu-soul
# 2) 打包 → SCP 上传 → SSH 解压并 pnpm install + build
# 3) 使用宝塔 API 重启 Node 项目(不用 pm2 命令)
# 在「一场soul的创业实验」目录下执行
set -e
cd "$(dirname "$0")"
echo "===== 1. 推送到 GitHub以本地为准====="
git push origin yongpxu-soul --force-with-lease
echo "===== 2. 打包 ====="
tar --exclude='node_modules' --exclude='.next' --exclude='.git' -czf /tmp/soul_update.tar.gz .
echo "===== 3. 上传到宝塔服务器 ====="
sshpass -p 'Zhiqun1984' scp /tmp/soul_update.tar.gz root@42.194.232.22:/tmp/
echo "===== 4. SSH解压、安装、构建不执行 pm2====="
sshpass -p 'Zhiqun1984' ssh root@42.194.232.22 "
cd /www/wwwroot/soul
rm -rf app components lib public styles *.json *.js *.ts *.mjs *.md .next
tar -xzf /tmp/soul_update.tar.gz
rm /tmp/soul_update.tar.gz
export PATH=/www/server/nodejs/v22.14.0/bin:\$PATH
pnpm install
pnpm run build
"
echo "===== 5. 宝塔 API 重启 Node 项目 soul ====="
BT_HOST="42.194.232.22"
BT_PORT="9988"
BT_KEY="hsAWqFSi0GOCrunhmYdkxy92tBXfqYjd"
REQUEST_TIME=$(date +%s)
# request_token = md5( request_time + md5(api_key) ),兼容 macOS/Linux
md5hex() { printf '%s' "$1" | openssl md5 2>/dev/null | awk '{print $NF}' || true; }
MD5_KEY=$(md5hex "$BT_KEY")
SIGN_STR="${REQUEST_TIME}${MD5_KEY}"
REQUEST_TOKEN=$(md5hex "$SIGN_STR")
RESP=$(curl -s -k -X POST "https://${BT_HOST}:${BT_PORT}/project/nodejs/restart_project" \
-d "request_time=${REQUEST_TIME}" \
-d "request_token=${REQUEST_TOKEN}" \
-d "project_name=soul" 2>/dev/null || true)
if echo "$RESP" | grep -q '"status":true\|"status": true'; then
echo "宝塔 API 重启成功: $RESP"
else
echo "宝塔 API 返回(若失败请到面板手动重启): $RESP"
fi
echo "===== 部署完成 ====="

52
部署到Kr宝塔.sh Executable file
View File

@@ -0,0 +1,52 @@
#!/bin/bash
# 部署到 Kr宝塔 (43.139.27.93):打包 → SCP(端口22022) → SSH 解压构建 → 宝塔 API 重启
# 不用 pm2 命令,用宝塔 API 操作。在「一场soul的创业实验」目录下执行。
set -e
cd "$(dirname "$0")"
SSH_PORT="22022"
BT_HOST="43.139.27.93"
BT_PORT="9988"
BT_KEY="qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT"
PROJECT_PATH="/www/wwwroot/soul"
PROJECT_NAME="soul"
echo "===== 1. 打包 ====="
tar --exclude='node_modules' --exclude='.next' --exclude='.git' -czf /tmp/soul_update.tar.gz .
echo "===== 2. 上传到 Kr宝塔 (${BT_HOST}:${SSH_PORT}) ====="
sshpass -p 'Zhiqun1984' scp -P "$SSH_PORT" /tmp/soul_update.tar.gz root@${BT_HOST}:/tmp/
echo "===== 3. SSH解压、安装、构建不执行 pm2====="
sshpass -p 'Zhiqun1984' ssh -p "$SSH_PORT" root@${BT_HOST} "
mkdir -p ${PROJECT_PATH}
cd ${PROJECT_PATH}
rm -rf app components lib public styles *.json *.js *.ts *.mjs *.md .next
tar -xzf /tmp/soul_update.tar.gz
rm /tmp/soul_update.tar.gz
export PATH=/www/server/nodejs/v22.14.0/bin:\$PATH
[ -x \"\$(command -v pnpm)\" ] || npm i -g pnpm
pnpm install
pnpm run build
"
echo "===== 4. 宝塔 API 重启 Node 项目 ${PROJECT_NAME} ====="
REQUEST_TIME=$(date +%s)
md5hex() { printf '%s' "$1" | openssl md5 2>/dev/null | awk '{print $NF}' || true; }
MD5_KEY=$(md5hex "$BT_KEY")
SIGN_STR="${REQUEST_TIME}${MD5_KEY}"
REQUEST_TOKEN=$(md5hex "$SIGN_STR")
RESP=$(curl -s -k -X POST "https://${BT_HOST}:${BT_PORT}/project/nodejs/restart_project" \
-d "request_time=${REQUEST_TIME}" \
-d "request_token=${REQUEST_TOKEN}" \
-d "project_name=${PROJECT_NAME}" 2>/dev/null || true)
if echo "$RESP" | grep -q '"status":true\|"status": true'; then
echo "宝塔 API 重启成功: $RESP"
else
echo "宝塔 API 返回(若失败请到面板 网站→Node项目→${PROJECT_NAME}→重启): $RESP"
fi
echo "===== 部署到 Kr宝塔 完成 ====="