520 lines
21 KiB
HTML
520 lines
21 KiB
HTML
|
|
<!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>
|
|||
|
|
<style>
|
|||
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|||
|
|
body{background:#0a0e17;color:#e0e6ed;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;min-height:100vh}
|
|||
|
|
a{color:#2dd4a8;text-decoration:none}
|
|||
|
|
a:hover{text-decoration:underline}
|
|||
|
|
.header{background:#111827;border-bottom:1px solid #1e293b;padding:16px 24px;display:flex;justify-content:space-between;align-items:center}
|
|||
|
|
.header h1{font-size:20px;font-weight:600}
|
|||
|
|
.header .back{color:#94a3b8;font-size:14px}
|
|||
|
|
.container{max-width:1200px;margin:0 auto;padding:24px}
|
|||
|
|
.tabs{display:flex;gap:8px;margin-bottom:24px;flex-wrap:wrap}
|
|||
|
|
.tab{padding:8px 20px;border-radius:8px;cursor:pointer;font-size:14px;border:1px solid #1e293b;background:#111827;color:#94a3b8;transition:all .2s}
|
|||
|
|
.tab.active{background:#2dd4a8;color:#0a0e17;border-color:#2dd4a8;font-weight:600}
|
|||
|
|
.tab:hover:not(.active){background:#1e293b}
|
|||
|
|
.card{background:#111827;border:1px solid #1e293b;border-radius:12px;padding:20px;margin-bottom:16px}
|
|||
|
|
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-bottom:24px}
|
|||
|
|
.stat{background:#111827;border:1px solid #1e293b;border-radius:10px;padding:16px;text-align:center}
|
|||
|
|
.stat .num{font-size:28px;font-weight:700;color:#2dd4a8}
|
|||
|
|
.stat .label{font-size:12px;color:#64748b;margin-top:4px}
|
|||
|
|
.part-header{display:flex;justify-content:space-between;align-items:center;padding:12px 0;cursor:pointer;border-bottom:1px solid #1e293b}
|
|||
|
|
.part-title{font-size:16px;font-weight:600;color:#2dd4a8}
|
|||
|
|
.part-count{font-size:12px;color:#64748b;background:#1e293b;padding:2px 10px;border-radius:10px}
|
|||
|
|
.chapter-group{padding:8px 0 8px 16px}
|
|||
|
|
.chapter-title{font-size:14px;color:#94a3b8;margin:12px 0 8px;font-weight:500}
|
|||
|
|
.section-item{display:flex;justify-content:space-between;align-items:center;padding:10px 12px;border-radius:8px;transition:background .15s}
|
|||
|
|
.section-item:hover{background:#1e293b}
|
|||
|
|
.section-left{display:flex;align-items:center;gap:10px;flex:1;min-width:0}
|
|||
|
|
.section-id{font-size:12px;color:#64748b;min-width:40px}
|
|||
|
|
.section-title{font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|||
|
|
.section-right{display:flex;align-items:center;gap:8px;flex-shrink:0}
|
|||
|
|
.badge{font-size:11px;padding:2px 8px;border-radius:4px;font-weight:500}
|
|||
|
|
.badge-free{background:rgba(45,212,168,.15);color:#2dd4a8}
|
|||
|
|
.badge-paid{background:rgba(234,179,8,.15);color:#eab308}
|
|||
|
|
.btn{padding:5px 12px;border-radius:6px;font-size:12px;cursor:pointer;border:1px solid #1e293b;background:#1e293b;color:#e0e6ed;transition:all .15s}
|
|||
|
|
.btn:hover{background:#334155}
|
|||
|
|
.btn-danger{border-color:#7f1d1d;color:#ef4444}
|
|||
|
|
.btn-danger:hover{background:#7f1d1d}
|
|||
|
|
.btn-primary{background:#2dd4a8;color:#0a0e17;border-color:#2dd4a8;font-weight:600}
|
|||
|
|
.btn-primary:hover{background:#22b896}
|
|||
|
|
.form-group{margin-bottom:16px}
|
|||
|
|
.form-group label{display:block;font-size:13px;color:#94a3b8;margin-bottom:6px;font-weight:500}
|
|||
|
|
.form-group input,.form-group select,.form-group textarea{width:100%;padding:10px 12px;background:#0a0e17;border:1px solid #1e293b;border-radius:8px;color:#e0e6ed;font-size:14px;outline:none;transition:border .2s}
|
|||
|
|
.form-group input:focus,.form-group select:focus,.form-group textarea:focus{border-color:#2dd4a8}
|
|||
|
|
.form-group textarea{min-height:200px;font-family:monospace;resize:vertical}
|
|||
|
|
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
|||
|
|
.api-doc{font-family:monospace;font-size:13px;line-height:1.7}
|
|||
|
|
.api-doc pre{background:#0a0e17;border:1px solid #1e293b;border-radius:8px;padding:14px;overflow-x:auto;margin:8px 0 16px}
|
|||
|
|
.api-doc code{color:#2dd4a8}
|
|||
|
|
.api-doc h3{color:#e0e6ed;font-size:15px;margin:20px 0 8px;padding-top:12px;border-top:1px solid #1e293b}
|
|||
|
|
.api-doc h3:first-child{border-top:none;margin-top:0}
|
|||
|
|
.toast{position:fixed;top:20px;right:20px;padding:12px 20px;border-radius:8px;font-size:14px;z-index:9999;animation:slideIn .3s}
|
|||
|
|
.toast-success{background:#065f46;color:#6ee7b7}
|
|||
|
|
.toast-error{background:#7f1d1d;color:#fca5a5}
|
|||
|
|
@keyframes slideIn{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}
|
|||
|
|
.loading{text-align:center;padding:40px;color:#64748b}
|
|||
|
|
.empty{text-align:center;padding:60px;color:#475569}
|
|||
|
|
.search-bar{display:flex;gap:12px;margin-bottom:20px}
|
|||
|
|
.search-bar input{flex:1}
|
|||
|
|
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:100;display:flex;align-items:center;justify-content:center}
|
|||
|
|
.modal{background:#111827;border:1px solid #1e293b;border-radius:16px;width:90%;max-width:700px;max-height:85vh;overflow-y:auto;padding:24px}
|
|||
|
|
.modal h2{font-size:18px;margin-bottom:16px}
|
|||
|
|
.modal-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:20px}
|
|||
|
|
.hidden{display:none}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="header">
|
|||
|
|
<h1>内容管理 · Soul创业派对</h1>
|
|||
|
|
<a class="back" href="/">← 返回管理后台</a>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="container">
|
|||
|
|
<div class="tabs">
|
|||
|
|
<div class="tab active" data-tab="chapters" onclick="switchTab('chapters')">章节管理</div>
|
|||
|
|
<div class="tab" data-tab="upload" onclick="switchTab('upload')">上传内容</div>
|
|||
|
|
<div class="tab" data-tab="api" onclick="switchTab('api')">API 接口文档</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 章节管理 -->
|
|||
|
|
<div id="tab-chapters">
|
|||
|
|
<div class="stats" id="stats"></div>
|
|||
|
|
<div class="search-bar">
|
|||
|
|
<input type="text" id="searchInput" placeholder="搜索章节标题..." oninput="filterSections()">
|
|||
|
|
<button class="btn btn-primary" onclick="loadChapters()">刷新</button>
|
|||
|
|
</div>
|
|||
|
|
<div id="chapterList"><div class="loading">加载中...</div></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 上传内容 -->
|
|||
|
|
<div id="tab-upload" class="hidden">
|
|||
|
|
<div class="card">
|
|||
|
|
<h2 style="margin-bottom:16px">上传新章节</h2>
|
|||
|
|
<div class="form-row">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>章节ID (如 1.6,留空自动生成)</label>
|
|||
|
|
<input type="text" id="up_id" placeholder="自动生成">
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>定价 (0=免费)</label>
|
|||
|
|
<input type="number" id="up_price" value="1" step="0.1" min="0">
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>标题 *</label>
|
|||
|
|
<input type="text" id="up_title" placeholder="章节标题">
|
|||
|
|
</div>
|
|||
|
|
<div class="form-row">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>所属篇</label>
|
|||
|
|
<select id="up_part">
|
|||
|
|
<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 class="form-group">
|
|||
|
|
<label>所属章</label>
|
|||
|
|
<select id="up_chapter">
|
|||
|
|
<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章|我人生错过的4件大钱</option>
|
|||
|
|
<option value="chapter-7">第7章|别人犯的错误</option>
|
|||
|
|
<option value="chapter-8">第8章|底层结构</option>
|
|||
|
|
<option value="chapter-9">第9章|我在Soul上亲访的赚钱案例</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>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>内容 (Markdown格式) *</label>
|
|||
|
|
<textarea id="up_content" placeholder="# 标题 正文内容... 图片用 {{image_1}} 占位"></textarea>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>图片URL (每行一个,替换 {{image_1}}, {{image_2}}...)</label>
|
|||
|
|
<textarea id="up_images" style="min-height:80px" placeholder="https://example.com/img1.png https://example.com/img2.png"></textarea>
|
|||
|
|
</div>
|
|||
|
|
<button class="btn btn-primary" style="width:100%;padding:12px;font-size:15px" onclick="uploadContent()">上传章节</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- API 接口文档 -->
|
|||
|
|
<div id="tab-api" class="hidden">
|
|||
|
|
<div class="card api-doc">
|
|||
|
|
<h2 style="margin-bottom:16px;font-family:sans-serif">内容管理 API 接口文档</h2>
|
|||
|
|
<p style="color:#94a3b8;margin-bottom:20px;font-family:sans-serif">基础域名:<code>https://soulapi.quwanzhi.com</code>(正式)/ <code>https://souldev.quwanzhi.com</code>(开发)</p>
|
|||
|
|
|
|||
|
|
<h3>1. 获取所有章节</h3>
|
|||
|
|
<pre>GET /api/book/all-chapters
|
|||
|
|
|
|||
|
|
# 无需认证,返回全部章节
|
|||
|
|
curl https://soulapi.quwanzhi.com/api/book/all-chapters</pre>
|
|||
|
|
<p>响应:<code>{"success": true, "data": [{"id":"1.1", "sectionTitle":"...", "isFree":true, "price":0, ...}]}</code></p>
|
|||
|
|
|
|||
|
|
<h3>2. 获取单章内容</h3>
|
|||
|
|
<pre>GET /api/book/chapter/:id
|
|||
|
|
|
|||
|
|
curl https://soulapi.quwanzhi.com/api/book/chapter/1.1</pre>
|
|||
|
|
<p>响应:<code>{"success": true, "data": {"id":"1.1", "content":"# 正文...", ...}}</code></p>
|
|||
|
|
|
|||
|
|
<h3>3. 管理员登录(获取Token)</h3>
|
|||
|
|
<pre>POST /api/admin
|
|||
|
|
Content-Type: application/json
|
|||
|
|
|
|||
|
|
{"username": "admin", "password": "admin123"}
|
|||
|
|
|
|||
|
|
# 响应包含 token,后续请求需带 Authorization: Bearer {token}</pre>
|
|||
|
|
|
|||
|
|
<h3>4. 章节列表(管理员)</h3>
|
|||
|
|
<pre>GET /api/db/book?action=list
|
|||
|
|
Authorization: Bearer {token}
|
|||
|
|
|
|||
|
|
# 返回所有章节的元数据(不含正文)</pre>
|
|||
|
|
|
|||
|
|
<h3>5. 读取章节内容(管理员)</h3>
|
|||
|
|
<pre>GET /api/db/book?action=read&id={section_id}
|
|||
|
|
Authorization: Bearer {token}</pre>
|
|||
|
|
|
|||
|
|
<h3>6. 创建/更新章节(管理员)</h3>
|
|||
|
|
<pre>POST /api/db/book
|
|||
|
|
Authorization: Bearer {token}
|
|||
|
|
Content-Type: application/json
|
|||
|
|
|
|||
|
|
{
|
|||
|
|
"id": "1.6", // 章节ID,不传则自动生成
|
|||
|
|
"title": "章节标题",
|
|||
|
|
"content": "Markdown正文",
|
|||
|
|
"price": 1.0, // 定价,0=免费
|
|||
|
|
"partId": "part-1", // 所属篇
|
|||
|
|
"chapterId": "chapter-1" // 所属章
|
|||
|
|
}</pre>
|
|||
|
|
|
|||
|
|
<h3>7. 上传内容(数据库直写)</h3>
|
|||
|
|
<p style="color:#94a3b8;font-family:sans-serif">支持从 Cursor Skill / 命令行 直接写入数据库:</p>
|
|||
|
|
<pre># 命令行方式
|
|||
|
|
python3 content_upload.py \
|
|||
|
|
--title "标题" \
|
|||
|
|
--price 1.0 \
|
|||
|
|
--content "正文内容" \
|
|||
|
|
--part part-1 \
|
|||
|
|
--chapter chapter-1 \
|
|||
|
|
--format markdown
|
|||
|
|
|
|||
|
|
# JSON方式
|
|||
|
|
python3 content_upload.py --json '{
|
|||
|
|
"title": "标题",
|
|||
|
|
"price": 1.0,
|
|||
|
|
"content": "正文...",
|
|||
|
|
"part_id": "part-1",
|
|||
|
|
"chapter_id": "chapter-1",
|
|||
|
|
"images": ["https://img.com/1.png"]
|
|||
|
|
}'
|
|||
|
|
|
|||
|
|
# 查看篇章结构
|
|||
|
|
python3 content_upload.py --list-structure
|
|||
|
|
|
|||
|
|
# 列出所有章节
|
|||
|
|
python3 content_upload.py --list-chapters</pre>
|
|||
|
|
|
|||
|
|
<h3>8. 删除章节</h3>
|
|||
|
|
<pre>DELETE /api/admin/content/:id
|
|||
|
|
Authorization: Bearer {token}
|
|||
|
|
|
|||
|
|
curl -X DELETE https://soulapi.quwanzhi.com/api/admin/content/1.6 \
|
|||
|
|
-H "Authorization: Bearer {token}"</pre>
|
|||
|
|
|
|||
|
|
<h3>9. 数据库连接信息</h3>
|
|||
|
|
<pre># 如需直连数据库
|
|||
|
|
Host: 56b4c23f6853c.gz.cdb.myqcloud.com
|
|||
|
|
Port: 14413
|
|||
|
|
User: cdb_outerroot
|
|||
|
|
DB: soul_miniprogram
|
|||
|
|
表: chapters (mid自增主键, id章节号唯一索引)</pre>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 编辑弹窗 -->
|
|||
|
|
<div id="editModal" class="modal-overlay hidden">
|
|||
|
|
<div class="modal">
|
|||
|
|
<h2 id="editTitle">编辑章节</h2>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>标题</label>
|
|||
|
|
<input type="text" id="edit_title">
|
|||
|
|
</div>
|
|||
|
|
<div class="form-row">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>定价</label>
|
|||
|
|
<input type="number" id="edit_price" step="0.1" min="0">
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>免费</label>
|
|||
|
|
<select id="edit_free"><option value="1">是</option><option value="0">否</option></select>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>内容 (Markdown)</label>
|
|||
|
|
<textarea id="edit_content" style="min-height:300px"></textarea>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-actions">
|
|||
|
|
<button class="btn" onclick="closeModal()">取消</button>
|
|||
|
|
<button class="btn btn-primary" onclick="saveEdit()">保存</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
const API_PROD = 'https://soulapi.quwanzhi.com';
|
|||
|
|
const API_DEV = 'https://souldev.quwanzhi.com';
|
|||
|
|
const DB_API = 'https://souldev.quwanzhi.com';
|
|||
|
|
|
|||
|
|
let token = localStorage.getItem('admin_token') || '';
|
|||
|
|
let allSections = [];
|
|||
|
|
let editingId = null;
|
|||
|
|
|
|||
|
|
async function api(method, path, body, base) {
|
|||
|
|
const url = (base || DB_API) + path;
|
|||
|
|
const opts = {method, headers: {'Content-Type':'application/json'}};
|
|||
|
|
if (token) opts.headers['Authorization'] = 'Bearer ' + token;
|
|||
|
|
if (body) opts.body = JSON.stringify(body);
|
|||
|
|
const r = await fetch(url, opts);
|
|||
|
|
return r.json();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function ensureAuth() {
|
|||
|
|
if (token) {
|
|||
|
|
const r = await api('GET', '/api/admin');
|
|||
|
|
if (r.success) return true;
|
|||
|
|
}
|
|||
|
|
const r = await api('POST', '/api/admin', {username:'admin', password:'admin123'});
|
|||
|
|
if (r.success && r.token) {
|
|||
|
|
token = r.token;
|
|||
|
|
localStorage.setItem('admin_token', token);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
showToast('登录失败', 'error');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadChapters() {
|
|||
|
|
document.getElementById('chapterList').innerHTML = '<div class="loading">加载中...</div>';
|
|||
|
|
if (!await ensureAuth()) return;
|
|||
|
|
|
|||
|
|
const r = await api('GET', '/api/db/book?action=list');
|
|||
|
|
let items = r.sections || r.data || r.chapters || [];
|
|||
|
|
allSections = items;
|
|||
|
|
|
|||
|
|
const parts = {};
|
|||
|
|
items.forEach(s => {
|
|||
|
|
const pk = s.partId || s.part_id || 'unknown';
|
|||
|
|
const pt = s.partTitle || s.part_title || pk;
|
|||
|
|
const ck = s.chapterId || s.chapter_id || 'unknown';
|
|||
|
|
const ct = s.chapterTitle || s.chapter_title || ck;
|
|||
|
|
if (!parts[pk]) parts[pk] = {title: pt, chapters: {}};
|
|||
|
|
if (!parts[pk].chapters[ck]) parts[pk].chapters[ck] = {title: ct, sections: []};
|
|||
|
|
parts[pk].chapters[ck].sections.push(s);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const partOrder = ['intro','part-1','part-2','part-3','part-4','part-5','outro','appendix'];
|
|||
|
|
const sortedParts = Object.entries(parts).sort((a,b) => {
|
|||
|
|
const ia = partOrder.indexOf(a[0]), ib = partOrder.indexOf(b[0]);
|
|||
|
|
return (ia===-1?99:ia) - (ib===-1?99:ib);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const totalParts = sortedParts.length;
|
|||
|
|
const freeCount = items.filter(s => s.isFree || s.is_free).length;
|
|||
|
|
const paidCount = items.length - freeCount;
|
|||
|
|
|
|||
|
|
document.getElementById('stats').innerHTML = `
|
|||
|
|
<div class="stat"><div class="num">${totalParts}</div><div class="label">篇</div></div>
|
|||
|
|
<div class="stat"><div class="num">${items.length}</div><div class="label">节</div></div>
|
|||
|
|
<div class="stat"><div class="num">${freeCount}</div><div class="label">免费</div></div>
|
|||
|
|
<div class="stat"><div class="num">${paidCount}</div><div class="label">付费</div></div>
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
let html = '';
|
|||
|
|
let partIdx = 0;
|
|||
|
|
sortedParts.forEach(([pk, pv]) => {
|
|||
|
|
partIdx++;
|
|||
|
|
const totalSec = Object.values(pv.chapters).reduce((s,c) => s + c.sections.length, 0);
|
|||
|
|
html += `<div class="card">
|
|||
|
|
<div class="part-header" onclick="this.nextElementSibling.classList.toggle('hidden')">
|
|||
|
|
<span class="part-title">${String(partIdx).padStart(2,'0')} ${pv.title}</span>
|
|||
|
|
<span class="part-count">${totalSec} 节</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="chapter-group">`;
|
|||
|
|
|
|||
|
|
Object.entries(pv.chapters).forEach(([ck, cv]) => {
|
|||
|
|
html += `<div class="chapter-title">${cv.title}</div>`;
|
|||
|
|
cv.sections.forEach(s => {
|
|||
|
|
const isFree = s.isFree || s.is_free;
|
|||
|
|
const price = s.price || 0;
|
|||
|
|
const title = s.sectionTitle || s.section_title || s.title || '';
|
|||
|
|
html += `<div class="section-item" data-title="${title.toLowerCase()}" data-id="${s.id}">
|
|||
|
|
<div class="section-left">
|
|||
|
|
<span class="section-id">${s.id}</span>
|
|||
|
|
<span class="section-title">${title}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="section-right">
|
|||
|
|
<span class="badge ${isFree?'badge-free':'badge-paid'}">${isFree?'免费':'¥'+price}</span>
|
|||
|
|
<button class="btn" onclick="editSection('${s.id}')">编辑</button>
|
|||
|
|
<button class="btn btn-danger" onclick="deleteSection('${s.id}','${title.replace(/'/g,"\\'")}')">删除</button>
|
|||
|
|
</div>
|
|||
|
|
</div>`;
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
html += '</div></div>';
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.getElementById('chapterList').innerHTML = html || '<div class="empty">暂无内容</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function filterSections() {
|
|||
|
|
const q = document.getElementById('searchInput').value.toLowerCase();
|
|||
|
|
document.querySelectorAll('.section-item').forEach(el => {
|
|||
|
|
el.style.display = el.dataset.title.includes(q) ? '' : 'none';
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function editSection(id) {
|
|||
|
|
if (!await ensureAuth()) return;
|
|||
|
|
showToast('加载中...');
|
|||
|
|
const r = await api('GET', `/api/db/book?action=read&id=${id}`);
|
|||
|
|
const s = r.data || r.section || r;
|
|||
|
|
editingId = id;
|
|||
|
|
document.getElementById('editTitle').textContent = `编辑: ${id}`;
|
|||
|
|
document.getElementById('edit_title').value = s.sectionTitle || s.section_title || s.title || '';
|
|||
|
|
document.getElementById('edit_price').value = s.price || 0;
|
|||
|
|
document.getElementById('edit_free').value = (s.isFree || s.is_free) ? '1' : '0';
|
|||
|
|
document.getElementById('edit_content').value = s.content || '';
|
|||
|
|
document.getElementById('editModal').classList.remove('hidden');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function closeModal() {
|
|||
|
|
document.getElementById('editModal').classList.add('hidden');
|
|||
|
|
editingId = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function saveEdit() {
|
|||
|
|
if (!editingId) return;
|
|||
|
|
const data = {
|
|||
|
|
id: editingId,
|
|||
|
|
title: document.getElementById('edit_title').value,
|
|||
|
|
content: document.getElementById('edit_content').value,
|
|||
|
|
price: parseFloat(document.getElementById('edit_price').value) || 0,
|
|||
|
|
isFree: document.getElementById('edit_free').value === '1'
|
|||
|
|
};
|
|||
|
|
const r = await api('POST', '/api/db/book', data);
|
|||
|
|
if (r.success !== false) {
|
|||
|
|
showToast('保存成功');
|
|||
|
|
closeModal();
|
|||
|
|
loadChapters();
|
|||
|
|
} else {
|
|||
|
|
showToast(r.error || '保存失败', 'error');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function deleteSection(id, title) {
|
|||
|
|
if (!confirm(`确定删除章节「${title}」(${id})?此操作不可恢复!`)) return;
|
|||
|
|
if (!await ensureAuth()) return;
|
|||
|
|
|
|||
|
|
let r = await api('DELETE', `/api/admin/content/${id}`);
|
|||
|
|
if (r.success === false && r.error) {
|
|||
|
|
r = await api('POST', '/api/db/book', {action:'delete', id});
|
|||
|
|
}
|
|||
|
|
if (r.success !== false) {
|
|||
|
|
showToast('已删除');
|
|||
|
|
loadChapters();
|
|||
|
|
} else {
|
|||
|
|
const ok = confirm('API删除失败,是否通过数据库直接删除?');
|
|||
|
|
if (ok) {
|
|||
|
|
showToast('正在通过数据库删除...');
|
|||
|
|
try {
|
|||
|
|
const resp = await fetch(DB_API + `/api/db/book?action=delete&id=${id}`, {
|
|||
|
|
method: 'DELETE',
|
|||
|
|
headers: {'Authorization': 'Bearer ' + token}
|
|||
|
|
});
|
|||
|
|
const d = await resp.json();
|
|||
|
|
if (d.success !== false) { showToast('已删除'); loadChapters(); }
|
|||
|
|
else showToast('删除失败: ' + (d.error||''), 'error');
|
|||
|
|
} catch(e) { showToast('删除失败', 'error'); }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function uploadContent() {
|
|||
|
|
const title = document.getElementById('up_title').value.trim();
|
|||
|
|
const content = document.getElementById('up_content').value.trim();
|
|||
|
|
if (!title) return showToast('请填写标题', 'error');
|
|||
|
|
if (!content) return showToast('请填写内容', 'error');
|
|||
|
|
|
|||
|
|
const images = document.getElementById('up_images').value.trim().split('\n').filter(Boolean);
|
|||
|
|
let processedContent = content;
|
|||
|
|
images.forEach((url, i) => {
|
|||
|
|
processedContent = processedContent.replace(`{{image_${i+1}}}`, `})`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const price = parseFloat(document.getElementById('up_price').value) || 0;
|
|||
|
|
const data = {
|
|||
|
|
id: document.getElementById('up_id').value.trim() || undefined,
|
|||
|
|
title: title,
|
|||
|
|
content: processedContent,
|
|||
|
|
price: price,
|
|||
|
|
isFree: price === 0,
|
|||
|
|
partId: document.getElementById('up_part').value,
|
|||
|
|
chapterId: document.getElementById('up_chapter').value
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (!await ensureAuth()) return;
|
|||
|
|
showToast('上传中...');
|
|||
|
|
const r = await api('POST', '/api/db/book', data);
|
|||
|
|
if (r.success !== false) {
|
|||
|
|
showToast('上传成功!');
|
|||
|
|
document.getElementById('up_title').value = '';
|
|||
|
|
document.getElementById('up_content').value = '';
|
|||
|
|
document.getElementById('up_images').value = '';
|
|||
|
|
document.getElementById('up_id').value = '';
|
|||
|
|
switchTab('chapters');
|
|||
|
|
loadChapters();
|
|||
|
|
} else {
|
|||
|
|
showToast('上传失败: ' + (r.error || ''), 'error');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function switchTab(name) {
|
|||
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === name));
|
|||
|
|
['chapters','upload','api'].forEach(t => {
|
|||
|
|
document.getElementById('tab-' + t).classList.toggle('hidden', t !== name);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showToast(msg, type='success') {
|
|||
|
|
const t = document.createElement('div');
|
|||
|
|
t.className = `toast toast-${type}`;
|
|||
|
|
t.textContent = msg;
|
|||
|
|
document.body.appendChild(t);
|
|||
|
|
setTimeout(() => t.remove(), 3000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
loadChapters();
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|