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()
|
||||
|
||||
// 获取所有章节并按时间排序
|
||||
const allChapters: 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)
|
||||
})
|
||||
let allChapters: Array<{
|
||||
id: string
|
||||
title: string
|
||||
part: string
|
||||
isFree: boolean
|
||||
price: number
|
||||
updatedAt: Date | string | null
|
||||
createdAt: Date | string | null
|
||||
}> = []
|
||||
|
||||
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[]
|
||||
|
||||
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)}个月前`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user