🔧 数据库配置: - 切换到腾讯云外网数据库 - 配置连接参数和连接池 🎨 界面优化: - 未登录时只显示登录按钮,隐藏其他功能 - 优化登录卡片样式 - 修复章节图标和标题对齐问题 💳 支付流程优化: - 增加重复购买检测,避免重复支付 - 优化openId获取逻辑,支持静默获取 - 已登录用户可直接支付,无需重复登录 📊 后台管理: - 创建章节管理API (/api/admin/chapters) - 创建章节管理页面 (/admin/chapters) - 支持查看所有章节、修改价格、设置免费状态
293 lines
12 KiB
TypeScript
293 lines
12 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import Link from 'next/link'
|
|
|
|
interface Section {
|
|
id: string
|
|
title: string
|
|
price: number
|
|
isFree: boolean
|
|
status: string
|
|
}
|
|
|
|
interface Chapter {
|
|
id: string
|
|
title: string
|
|
sections?: Section[]
|
|
price?: number
|
|
isFree?: boolean
|
|
status?: string
|
|
}
|
|
|
|
interface Part {
|
|
id: string
|
|
title: string
|
|
type: string
|
|
chapters: Chapter[]
|
|
}
|
|
|
|
interface Stats {
|
|
totalSections: number
|
|
freeSections: number
|
|
paidSections: number
|
|
totalParts: number
|
|
}
|
|
|
|
export default function ChaptersManagement() {
|
|
const [structure, setStructure] = useState<Part[]>([])
|
|
const [stats, setStats] = useState<Stats | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [expandedParts, setExpandedParts] = useState<string[]>([])
|
|
const [editingSection, setEditingSection] = useState<string | null>(null)
|
|
const [editPrice, setEditPrice] = useState<number>(1)
|
|
|
|
useEffect(() => {
|
|
loadChapters()
|
|
}, [])
|
|
|
|
const loadChapters = async () => {
|
|
try {
|
|
const response = await fetch('/api/admin/chapters')
|
|
const data = await response.json()
|
|
if (data.success) {
|
|
setStructure(data.data.structure)
|
|
setStats(data.data.stats)
|
|
}
|
|
} catch (error) {
|
|
console.error('加载章节失败:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const togglePart = (partId: string) => {
|
|
setExpandedParts(prev =>
|
|
prev.includes(partId)
|
|
? prev.filter(id => id !== partId)
|
|
: [...prev, partId]
|
|
)
|
|
}
|
|
|
|
const handleUpdatePrice = async (sectionId: string) => {
|
|
try {
|
|
const response = await fetch('/api/admin/chapters', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action: 'updatePrice',
|
|
chapterId: sectionId,
|
|
data: { price: editPrice }
|
|
})
|
|
})
|
|
const result = await response.json()
|
|
if (result.success) {
|
|
alert('价格更新成功')
|
|
setEditingSection(null)
|
|
loadChapters()
|
|
}
|
|
} catch (error) {
|
|
console.error('更新价格失败:', error)
|
|
}
|
|
}
|
|
|
|
const handleToggleFree = async (sectionId: string, currentFree: boolean) => {
|
|
try {
|
|
const response = await fetch('/api/admin/chapters', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action: 'toggleFree',
|
|
chapterId: sectionId,
|
|
data: { isFree: !currentFree }
|
|
})
|
|
})
|
|
const result = await response.json()
|
|
if (result.success) {
|
|
alert('状态更新成功')
|
|
loadChapters()
|
|
}
|
|
} catch (error) {
|
|
console.error('更新状态失败:', error)
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-black text-white flex items-center justify-center">
|
|
<div className="text-xl">加载中...</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-black text-white">
|
|
{/* 导航栏 */}
|
|
<div className="sticky top-0 bg-black/90 backdrop-blur border-b border-white/10 z-50">
|
|
<div className="max-w-6xl mx-auto px-4 py-4 flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<Link href="/admin" className="text-white/60 hover:text-white">← 返回</Link>
|
|
<h1 className="text-xl font-bold">章节管理</h1>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
onClick={() => setExpandedParts(structure.map(p => p.id))}
|
|
className="px-4 py-2 bg-white/10 rounded-lg hover:bg-white/20"
|
|
>
|
|
展开全部
|
|
</button>
|
|
<button
|
|
onClick={() => setExpandedParts([])}
|
|
className="px-4 py-2 bg-white/10 rounded-lg hover:bg-white/20"
|
|
>
|
|
收起全部
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="max-w-6xl mx-auto px-4 py-8">
|
|
{/* 统计卡片 */}
|
|
{stats && (
|
|
<div className="grid grid-cols-4 gap-4 mb-8">
|
|
<div className="bg-gradient-to-br from-cyan-500/20 to-cyan-500/5 border border-cyan-500/30 rounded-xl p-4">
|
|
<div className="text-3xl font-bold text-cyan-400">{stats.totalSections}</div>
|
|
<div className="text-white/60 text-sm mt-1">总章节数</div>
|
|
</div>
|
|
<div className="bg-gradient-to-br from-green-500/20 to-green-500/5 border border-green-500/30 rounded-xl p-4">
|
|
<div className="text-3xl font-bold text-green-400">{stats.freeSections}</div>
|
|
<div className="text-white/60 text-sm mt-1">免费章节</div>
|
|
</div>
|
|
<div className="bg-gradient-to-br from-yellow-500/20 to-yellow-500/5 border border-yellow-500/30 rounded-xl p-4">
|
|
<div className="text-3xl font-bold text-yellow-400">{stats.paidSections}</div>
|
|
<div className="text-white/60 text-sm mt-1">付费章节</div>
|
|
</div>
|
|
<div className="bg-gradient-to-br from-purple-500/20 to-purple-500/5 border border-purple-500/30 rounded-xl p-4">
|
|
<div className="text-3xl font-bold text-purple-400">{stats.totalParts}</div>
|
|
<div className="text-white/60 text-sm mt-1">篇章数</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 章节列表 */}
|
|
<div className="space-y-4">
|
|
{structure.map(part => (
|
|
<div key={part.id} className="bg-white/5 border border-white/10 rounded-xl overflow-hidden">
|
|
{/* 篇标题 */}
|
|
<div
|
|
className="flex items-center justify-between p-4 cursor-pointer hover:bg-white/5"
|
|
onClick={() => togglePart(part.id)}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-2xl">
|
|
{part.type === 'preface' ? '📖' :
|
|
part.type === 'epilogue' ? '🎬' :
|
|
part.type === 'appendix' ? '📎' : '📚'}
|
|
</span>
|
|
<span className="font-semibold">{part.title}</span>
|
|
<span className="text-white/40 text-sm">
|
|
({part.chapters.reduce((acc, ch) => acc + (ch.sections?.length || 1), 0)} 节)
|
|
</span>
|
|
</div>
|
|
<span className="text-white/40">
|
|
{expandedParts.includes(part.id) ? '▲' : '▼'}
|
|
</span>
|
|
</div>
|
|
|
|
{/* 章节内容 */}
|
|
{expandedParts.includes(part.id) && (
|
|
<div className="border-t border-white/10">
|
|
{part.chapters.map(chapter => (
|
|
<div key={chapter.id} className="border-b border-white/5 last:border-b-0">
|
|
{/* 章标题 */}
|
|
{chapter.sections ? (
|
|
<>
|
|
<div className="px-6 py-3 bg-white/5 text-white/70 font-medium">
|
|
{chapter.title}
|
|
</div>
|
|
{/* 小节列表 */}
|
|
<div className="divide-y divide-white/5">
|
|
{chapter.sections.map(section => (
|
|
<div key={section.id} className="flex items-center justify-between px-6 py-3 hover:bg-white/5">
|
|
<div className="flex items-center gap-3">
|
|
<span className={section.isFree ? 'text-green-400' : 'text-yellow-400'}>
|
|
{section.isFree ? '🔓' : '🔒'}
|
|
</span>
|
|
<span className="text-white/80">{section.id}</span>
|
|
<span className="text-white/60">{section.title}</span>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{editingSection === section.id ? (
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="number"
|
|
value={editPrice}
|
|
onChange={(e) => setEditPrice(Number(e.target.value))}
|
|
className="w-20 px-2 py-1 bg-white/10 border border-white/20 rounded text-white"
|
|
min="0"
|
|
step="0.1"
|
|
/>
|
|
<button
|
|
onClick={() => handleUpdatePrice(section.id)}
|
|
className="px-3 py-1 bg-cyan-500 text-black rounded text-sm"
|
|
>
|
|
保存
|
|
</button>
|
|
<button
|
|
onClick={() => setEditingSection(null)}
|
|
className="px-3 py-1 bg-white/20 rounded text-sm"
|
|
>
|
|
取消
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<span className={`px-2 py-1 rounded text-xs ${section.isFree ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-400'}`}>
|
|
{section.isFree ? '免费' : `¥${section.price}`}
|
|
</span>
|
|
<button
|
|
onClick={() => {
|
|
setEditingSection(section.id)
|
|
setEditPrice(section.price)
|
|
}}
|
|
className="px-2 py-1 text-xs bg-white/10 rounded hover:bg-white/20"
|
|
>
|
|
编辑价格
|
|
</button>
|
|
<button
|
|
onClick={() => handleToggleFree(section.id, section.isFree)}
|
|
className="px-2 py-1 text-xs bg-white/10 rounded hover:bg-white/20"
|
|
>
|
|
{section.isFree ? '设为付费' : '设为免费'}
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="flex items-center justify-between px-6 py-3 hover:bg-white/5">
|
|
<div className="flex items-center gap-3">
|
|
<span className={chapter.isFree ? 'text-green-400' : 'text-yellow-400'}>
|
|
{chapter.isFree ? '🔓' : '🔒'}
|
|
</span>
|
|
<span className="text-white/80">{chapter.title}</span>
|
|
</div>
|
|
<span className={`px-2 py-1 rounded text-xs ${chapter.isFree ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-400'}`}>
|
|
{chapter.isFree ? '免费' : `¥${chapter.price || 1}`}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
} |