feat: 管理后台改造 + 小程序最新章节逻辑 + 变更文档
【soul-admin 管理后台】 - 交易中心 → 推广中心(侧边栏与页面标题) - 移除 5 个冗余按钮,仅保留「API 接口」 - 删除按钮改为悬停显示 - 免费/付费可点击切换(单击切换,双击付费可设金额) - 加号移至章节右侧(序言、附录等),小节内移除加号 - 章节与小节支持拖拽排序 - 持续隐藏「上传内容」等按钮,解决双页面问题 【小程序首页 - 最新章节】 - latest-chapters API: 2 日内有新章取最新 3 章,否则随机免费章 - 首页 Banner 调用 /api/book/latest-chapters - 标签动态显示「最新更新」或「为你推荐」 【开发文档】 - 新增 soul-admin变更记录_v2026-02.md Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,55 +1,96 @@
|
||||
// app/api/book/latest-chapters/route.ts
|
||||
// 获取最新章节列表
|
||||
// 获取最新章节:有2日内更新则取最新3章,否则随机取免费章节
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getBookStructure } from '@/lib/book-file-system'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const TWO_DAYS_MS = 2 * 24 * 60 * 60 * 1000
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const bookStructure = getBookStructure()
|
||||
let allChapters: Array<{
|
||||
id: string
|
||||
title: string
|
||||
part: string
|
||||
isFree: boolean
|
||||
price: number
|
||||
updatedAt: Date | string | null
|
||||
createdAt: Date | string | null
|
||||
}> = []
|
||||
|
||||
// 获取所有章节并按时间排序
|
||||
const allChapters: any[] = []
|
||||
try {
|
||||
const dbRows = (await query(`
|
||||
SELECT id, part_title, section_title, is_free, price, created_at, updated_at
|
||||
FROM chapters
|
||||
ORDER BY sort_order ASC, id ASC
|
||||
`)) as any[]
|
||||
|
||||
bookStructure.forEach((part: any) => {
|
||||
part.chapters.forEach((chapter: any) => {
|
||||
allChapters.push({
|
||||
id: chapter.slug,
|
||||
title: chapter.title,
|
||||
part: part.title,
|
||||
words: Math.floor(Math.random() * 3000) + 1500, // 模拟字数
|
||||
updateTime: getRelativeTime(new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000)),
|
||||
readTime: Math.ceil((Math.random() * 3000 + 1500) / 300)
|
||||
})
|
||||
if (dbRows?.length > 0) {
|
||||
allChapters = dbRows.map((row: any) => ({
|
||||
id: row.id,
|
||||
title: row.section_title || row.title || '',
|
||||
part: row.part_title || '真实的行业',
|
||||
isFree: !!row.is_free,
|
||||
price: row.price || 0,
|
||||
updatedAt: row.updated_at || row.created_at,
|
||||
createdAt: row.created_at
|
||||
}))
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[latest-chapters] 数据库读取失败:', (e as Error).message)
|
||||
}
|
||||
|
||||
if (allChapters.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
banner: { id: '1.1', title: '荷包:电动车出租的被动收入模式', part: '真实的人' },
|
||||
label: '为你推荐',
|
||||
chapters: [],
|
||||
hasNewUpdates: false
|
||||
})
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const sorted = [...allChapters].sort((a, b) => {
|
||||
const ta = a.updatedAt ? new Date(a.updatedAt).getTime() : 0
|
||||
const tb = b.updatedAt ? new Date(b.updatedAt).getTime() : 0
|
||||
return tb - ta
|
||||
})
|
||||
|
||||
// 取最新的3章
|
||||
const latestChapters = allChapters.slice(0, 3)
|
||||
const mostRecentTime = sorted[0]?.updatedAt ? new Date(sorted[0].updatedAt).getTime() : 0
|
||||
const hasNewUpdates = now - mostRecentTime < TWO_DAYS_MS
|
||||
|
||||
let banner: { id: string; title: string; part: string }
|
||||
let label: string
|
||||
let chapters: typeof allChapters
|
||||
|
||||
if (hasNewUpdates && sorted.length > 0) {
|
||||
chapters = sorted.slice(0, 3)
|
||||
banner = { id: chapters[0].id, title: chapters[0].title, part: chapters[0].part }
|
||||
label = '最新更新'
|
||||
} else {
|
||||
const freeChapters = allChapters.filter((c) => c.isFree || c.price === 0)
|
||||
const candidates = freeChapters.length > 0 ? freeChapters : allChapters
|
||||
const shuffled = [...candidates].sort(() => Math.random() - 0.5)
|
||||
chapters = shuffled.slice(0, 3)
|
||||
banner = chapters[0]
|
||||
? { id: chapters[0].id, title: chapters[0].title, part: chapters[0].part }
|
||||
: { id: allChapters[0].id, title: allChapters[0].title, part: allChapters[0].part }
|
||||
label = '为你推荐'
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
chapters: latestChapters,
|
||||
total: allChapters.length
|
||||
banner,
|
||||
label,
|
||||
chapters: chapters.map((c) => ({ id: c.id, title: c.title, part: c.part, isFree: c.isFree })),
|
||||
hasNewUpdates
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取章节失败:', error)
|
||||
console.error('[latest-chapters] Error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: '获取章节失败' },
|
||||
{ success: false, error: '获取失败' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取相对时间
|
||||
function getRelativeTime(date: Date): string {
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (days === 0) return '今天'
|
||||
if (days === 1) return '昨天'
|
||||
if (days < 7) return `${days}天前`
|
||||
if (days < 30) return `${Math.floor(days / 7)}周前`
|
||||
return `${Math.floor(days / 30)}个月前`
|
||||
}
|
||||
|
||||
@@ -77,59 +77,72 @@ Page({
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
// 获取书籍数据
|
||||
await this.loadBookData()
|
||||
// 计算推荐章节
|
||||
this.computeLatestSection()
|
||||
await this.loadLatestSection()
|
||||
} catch (e) {
|
||||
console.error('初始化失败:', e)
|
||||
this.computeLatestSectionFallback()
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 计算推荐章节(根据用户ID随机、优先未付款)
|
||||
computeLatestSection() {
|
||||
const { hasFullBook, purchasedSections } = app.globalData
|
||||
const userId = app.globalData.userInfo?.id || wx.getStorageSync('userId') || 'guest'
|
||||
// 从后端获取最新章节(2日内有新章取最新3章,否则随机免费章)
|
||||
async loadLatestSection() {
|
||||
try {
|
||||
const res = await app.request('/api/book/latest-chapters')
|
||||
if (res && res.success && res.banner) {
|
||||
this.setData({
|
||||
latestSection: res.banner,
|
||||
latestLabel: res.label || '最新更新'
|
||||
})
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('latest-chapters API 失败,使用兜底逻辑:', e.message)
|
||||
}
|
||||
this.computeLatestSectionFallback()
|
||||
},
|
||||
|
||||
// 所有章节列表
|
||||
const allSections = [
|
||||
{ id: '9.14', title: '大健康私域:一个月150万的70后', part: '真实的赚钱' },
|
||||
{ id: '9.13', title: 'AI工具推广:一个隐藏的高利润赛道', part: '真实的赚钱' },
|
||||
{ id: '9.12', title: '美业整合:一个人的公司如何月入十万', part: '真实的赚钱' },
|
||||
{ id: '8.6', title: '云阿米巴:分不属于自己的钱', part: '真实的赚钱' },
|
||||
{ id: '8.1', title: '流量杠杆:抖音、Soul、飞书', part: '真实的赚钱' },
|
||||
{ id: '3.1', title: '3000万流水如何跑出来', part: '真实的行业' },
|
||||
{ id: '5.1', title: '拍卖行抱朴:一天240万的摇号生意', part: '真实的行业' },
|
||||
{ id: '4.1', title: '旅游号:30天10万粉的真实逻辑', part: '真实的行业' }
|
||||
]
|
||||
|
||||
// 用户ID生成的随机种子(同一用户每天看到的不同)
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const seed = (userId + today).split('').reduce((a, b) => a + b.charCodeAt(0), 0)
|
||||
|
||||
// 筛选未付款章节
|
||||
let candidates = allSections
|
||||
if (!hasFullBook) {
|
||||
const purchased = purchasedSections || []
|
||||
const unpurchased = allSections.filter(s => !purchased.includes(s.id))
|
||||
if (unpurchased.length > 0) {
|
||||
candidates = unpurchased
|
||||
// 兜底:API 失败时从 bookData 计算(随机选免费章节)
|
||||
computeLatestSectionFallback() {
|
||||
const bookData = app.globalData.bookData || this.data.bookData || []
|
||||
let sections = []
|
||||
if (Array.isArray(bookData)) {
|
||||
sections = bookData.map(s => ({
|
||||
id: s.id,
|
||||
title: s.title || s.sectionTitle,
|
||||
part: s.part || s.sectionTitle || '真实的行业',
|
||||
isFree: s.isFree,
|
||||
price: s.price
|
||||
}))
|
||||
} else if (bookData && typeof bookData === 'object') {
|
||||
const parts = bookData.parts || (Array.isArray(bookData) ? bookData : [])
|
||||
if (Array.isArray(parts)) {
|
||||
parts.forEach(p => {
|
||||
(p.chapters || p.sections || []).forEach(c => {
|
||||
(c.sections || [c]).forEach(s => {
|
||||
sections.push({
|
||||
id: s.id,
|
||||
title: s.title || s.section_title,
|
||||
part: p.title || p.part_title || c.title || '',
|
||||
isFree: s.isFree,
|
||||
price: s.price
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 根据种子选择章节
|
||||
const index = seed % candidates.length
|
||||
const selected = candidates[index]
|
||||
|
||||
// 设置标签(如果有新增章节显示"最新更新",否则显示"推荐阅读")
|
||||
const label = candidates === allSections ? '推荐阅读' : '为你推荐'
|
||||
|
||||
this.setData({
|
||||
latestSection: selected,
|
||||
latestLabel: label
|
||||
})
|
||||
const free = sections.filter(s => s.isFree !== false && (s.price === 0 || !s.price))
|
||||
const candidates = free.length > 0 ? free : sections
|
||||
if (candidates.length === 0) {
|
||||
this.setData({ latestSection: { id: '1.1', title: '开始阅读', part: '真实的人' }, latestLabel: '为你推荐' })
|
||||
return
|
||||
}
|
||||
const idx = Math.floor(Math.random() * candidates.length)
|
||||
const selected = { id: candidates[idx].id, title: candidates[idx].title, part: candidates[idx].part || '真实的行业' }
|
||||
this.setData({ latestSection: selected, latestLabel: '为你推荐' })
|
||||
},
|
||||
|
||||
// 加载书籍数据
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<!-- Banner卡片 - 最新章节 -->
|
||||
<view class="banner-card" bindtap="goToRead" data-id="{{latestSection.id}}">
|
||||
<view class="banner-glow"></view>
|
||||
<view class="banner-tag">最新更新</view>
|
||||
<view class="banner-tag">{{latestLabel}}</view>
|
||||
<view class="banner-title">{{latestSection.title}}</view>
|
||||
<view class="banner-part">{{latestSection.part}}</view>
|
||||
<view class="banner-action">
|
||||
|
||||
4
soul-admin/dist/assets/index-CbOmKBRd.js
vendored
4
soul-admin/dist/assets/index-CbOmKBRd.js
vendored
File diff suppressed because one or more lines are too long
240
soul-admin/dist/index.html
vendored
240
soul-admin/dist/index.html
vendored
@@ -4,7 +4,7 @@
|
||||
<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=2"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CbOmKBRd.js?v=5"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DBQ1UORI.css">
|
||||
</head>
|
||||
<body>
|
||||
@@ -13,9 +13,23 @@
|
||||
(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}
|
||||
@@ -76,46 +90,39 @@
|
||||
|
||||
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'))return;
|
||||
if(!location.pathname.includes('content')&&!location.hash.includes('content'))return;
|
||||
var initBtn=findBtn('初始化数据库');
|
||||
if(!initBtn)return;
|
||||
done=true;
|
||||
|
||||
// === 1. 改造顶部按钮 ===
|
||||
var syncBtn=findBtn('同步到数据库');
|
||||
var importBtn=findBtn('导入');
|
||||
var exportBtn=findBtn('导出');
|
||||
var feishuBtn=findBtn('同步飞书');
|
||||
// === 1. 移除5个按钮+上传内容,只保留一个"API 接口"(持续执行防重复页)===
|
||||
hideRedundantButtons();
|
||||
setInterval(hideRedundantButtons,800);
|
||||
|
||||
// 把前两个按钮改成"上传内容"和"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);
|
||||
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);
|
||||
}
|
||||
|
||||
// 隐藏其余按钮
|
||||
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 insertTarget=tabBar||(initBtn&&initBtn.parentElement);
|
||||
|
||||
// 上传面板
|
||||
var upP=document.createElement('div');
|
||||
@@ -180,28 +187,54 @@
|
||||
};
|
||||
document.getElementById('si-token-input').onclick=function(){this.select()};
|
||||
|
||||
// === 3. 给每个章节添加删除按钮 ===
|
||||
addDelBtns();
|
||||
new MutationObserver(function(){addDelBtns()}).observe(document.getElementById('root'),{childList:true,subtree:true});
|
||||
// === 3. 内容操作:删除(hover)、免费/付费、加号在章节、拖拽 ===
|
||||
addContentActions();
|
||||
addChapterPlus();
|
||||
addDragDrop();
|
||||
new MutationObserver(function(){addContentActions();addChapterPlus();addDragDrop();}).observe(document.getElementById('root'),{childList:true,subtree:true});
|
||||
}
|
||||
|
||||
var activePanel='';
|
||||
function togglePanel(name){
|
||||
var siPrefill={};
|
||||
function togglePanel(name,prefill){
|
||||
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;
|
||||
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 addDelBtns(){
|
||||
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='删除';
|
||||
@@ -212,7 +245,7 @@
|
||||
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();
|
||||
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}
|
||||
@@ -228,7 +261,140 @@
|
||||
})
|
||||
}
|
||||
})(b);
|
||||
b.parentElement.appendChild(del);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
79
开发文档/soul-admin变更记录_v2026-02.md
Normal file
79
开发文档/soul-admin变更记录_v2026-02.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Soul 管理后台 (soul-admin) 变更记录 v2026-02
|
||||
|
||||
> 更新时间:2026-02-21
|
||||
> 适用站点:souladmin.quwanzhi.com
|
||||
> 部署路径:`/www/wwwroot/自营/soul-admin/dist/`
|
||||
|
||||
---
|
||||
|
||||
## 一、变更概览
|
||||
|
||||
| 模块 | 变更项 | 说明 |
|
||||
|:---|:---|:---|
|
||||
| 侧边栏 | 交易中心 → 推广中心 | 菜单及页面标题统一改为「推广中心」 |
|
||||
| 内容管理 | 顶部 5 按钮移除 | 移除:初始化数据库、同步到数据库、导入、导出、同步飞书 |
|
||||
| 内容管理 | 仅保留 API 接口 | 仅保留「API 接口」按钮,打开 API 文档面板 |
|
||||
| 内容管理 | 删除按钮 | 删除按钮改为悬停才显示(与读取/编辑一致) |
|
||||
| 内容管理 | 免费/付费 | 可点击切换免费 ↔ 付费 |
|
||||
| 内容管理 | 小节加号 | 每小节旁增加「+」按钮,可在此小节下新建章节 |
|
||||
|
||||
---
|
||||
|
||||
## 二、部署说明
|
||||
|
||||
### 2.1 正确部署路径
|
||||
|
||||
nginx 实际指向:
|
||||
|
||||
```nginx
|
||||
root /www/wwwroot/自营/soul-admin/dist;
|
||||
```
|
||||
|
||||
**重要**:需将 `soul-admin/dist` 部署到上述目录,而非 `/www/wwwroot/souladmin.quwanzhi.com/`。
|
||||
|
||||
### 2.2 部署步骤
|
||||
|
||||
```bash
|
||||
# 1. 本地打包
|
||||
cd /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验/soul-admin/dist
|
||||
tar -czf /tmp/souladmin.tar.gz index.html assets/
|
||||
|
||||
# 2. 上传并解压到正确路径
|
||||
scp -P 22022 /tmp/souladmin.tar.gz root@43.139.27.93:/tmp/
|
||||
ssh -p 22022 root@43.139.27.93 'cd /www/wwwroot/自营/soul-admin/dist && tar -xzf /tmp/souladmin.tar.gz && chown -R www:www . && rm /tmp/souladmin.tar.gz'
|
||||
```
|
||||
|
||||
### 2.3 缓存处理
|
||||
|
||||
- `index.html` 内引用 `index-CbOmKBRd.js?v=版本号`,每次发布建议递增版本号
|
||||
- 建议在 `index.html` 中调整:`?v=3` 或更高
|
||||
|
||||
---
|
||||
|
||||
## 三、技术说明
|
||||
|
||||
### 3.1 修改文件
|
||||
|
||||
- `index.html`:内联注入脚本(按钮改造、删除 hover、免费切换、加号新建)
|
||||
- `assets/index-CbOmKBRd.js`:侧边栏「交易中心」→「推广中心」
|
||||
|
||||
### 3.2 注入脚本触发条件
|
||||
|
||||
- 路径包含 `content`(如 `/content`)
|
||||
- 页面上存在「初始化数据库」按钮(内容管理页加载完成)
|
||||
|
||||
### 3.3 免费/付费切换
|
||||
|
||||
- 调用 `POST /api/db/book`,传入 `{ id, isFree, price }`
|
||||
- 需后端支持按 id 更新 isFree/price
|
||||
|
||||
---
|
||||
|
||||
## 四、问题排查
|
||||
|
||||
| 现象 | 可能原因 | 处理方式 |
|
||||
|:---|:---|:---|
|
||||
| 界面未变化 | 部署到错误目录 | 确认部署到 `/www/wwwroot/自营/soul-admin/dist/` |
|
||||
| 界面未变化 | 浏览器/CDN 缓存 | 清除缓存或使用无痕模式,或增加 `?v=` 版本号 |
|
||||
| 内容管理注入不生效 | 路由为 hash 模式 | 检查 `location.pathname` 是否包含 `content`,必要时改用 `location.hash` |
|
||||
| 免费切换失败 | 后端未实现更新 | 检查 soul-api 是否支持 `POST /api/db/book` 的更新逻辑 |
|
||||
Reference in New Issue
Block a user