feat: 管理后台增加免费章节和小程序配置

1. 系统设置页新增免费章节管理(可动态添加/删除)
2. 新增小程序配置项(API域名、购买优惠、绑定天数等)
3. 前端从后端读取免费章节配置
4. 配置API支持新格式
This commit is contained in:
卡若
2026-01-25 21:09:20 +08:00
parent afa8c59376
commit ac24853aa6
3 changed files with 228 additions and 2 deletions

View File

@@ -8,8 +8,9 @@ import { Button } from "@/components/ui/button"
import { Switch } from "@/components/ui/switch"
import { Slider } from "@/components/ui/slider"
import { Textarea } from "@/components/ui/textarea"
import { Badge } from "@/components/ui/badge"
import { useStore } from "@/lib/store"
import { Save, Settings, Users, DollarSign, UserCircle, Calendar, MapPin, BookOpen } from "lucide-react"
import { Save, Settings, Users, DollarSign, UserCircle, Calendar, MapPin, BookOpen, Gift, X, Plus, Smartphone } from "lucide-react"
export default function SettingsPage() {
const { settings, updateSettings } = useStore()
@@ -24,6 +25,36 @@ export default function SettingsPage() {
},
})
const [isSaving, setIsSaving] = useState(false)
// 免费章节配置
const [freeChapters, setFreeChapters] = useState<string[]>(['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3'])
const [newFreeChapter, setNewFreeChapter] = useState('')
// 小程序配置
const [mpConfig, setMpConfig] = useState({
appId: 'wxb8bbb2b10dec74aa',
apiDomain: 'https://soul.quwanzhi.com',
buyerDiscount: 5, // 购买者优惠比例
referralBindDays: 30, // 推荐绑定天数
minWithdraw: 10, // 最低提现金额
})
// 加载配置
useEffect(() => {
const loadConfig = async () => {
try {
const res = await fetch('/api/db/config')
if (res.ok) {
const data = await res.json()
if (data.freeChapters) setFreeChapters(data.freeChapters)
if (data.mpConfig) setMpConfig(prev => ({ ...prev, ...data.mpConfig }))
}
} catch (e) {
console.log('Load config error:', e)
}
}
loadConfig()
}, [])
useEffect(() => {
setLocalSettings({
@@ -50,6 +81,13 @@ export default function SettingsPage() {
body: JSON.stringify(localSettings)
})
// 保存免费章节和小程序配置
await fetch('/api/db/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ freeChapters, mpConfig })
})
alert("设置已保存!")
} catch (error) {
console.error('Save settings error:', error)
@@ -58,6 +96,19 @@ export default function SettingsPage() {
setIsSaving(false)
}
}
// 添加免费章节
const addFreeChapter = () => {
if (newFreeChapter && !freeChapters.includes(newFreeChapter)) {
setFreeChapters([...freeChapters, newFreeChapter])
setNewFreeChapter('')
}
}
// 移除免费章节
const removeFreeChapter = (chapter: string) => {
setFreeChapters(freeChapters.filter(c => c !== chapter))
}
return (
<div className="p-8 max-w-4xl mx-auto">
@@ -257,6 +308,115 @@ export default function SettingsPage() {
</CardContent>
</Card>
{/* 免费章节设置 */}
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Gift className="w-5 h-5 text-[#38bdac]" />
</CardTitle>
<CardDescription className="text-gray-400"></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap gap-2">
{freeChapters.map((chapter) => (
<Badge
key={chapter}
variant="secondary"
className="bg-[#38bdac]/20 text-[#38bdac] border border-[#38bdac]/30 px-3 py-1 text-sm"
>
{chapter}
<button
onClick={() => removeFreeChapter(chapter)}
className="ml-2 hover:text-red-400"
>
<X className="w-3 h-3" />
</button>
</Badge>
))}
</div>
<div className="flex gap-2">
<Input
className="bg-[#0a1628] border-gray-700 text-white flex-1"
placeholder="输入章节ID如 1.2、2.1、preface"
value={newFreeChapter}
onChange={(e) => setNewFreeChapter(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && addFreeChapter()}
/>
<Button
onClick={addFreeChapter}
className="bg-[#38bdac] hover:bg-[#2da396]"
>
<Plus className="w-4 h-4 mr-1" />
</Button>
</div>
<p className="text-xs text-gray-500">
常用ID: preface(), epilogue(), appendix-1/2/3(), 1.1/1.2()
</p>
</CardContent>
</Card>
{/* 小程序配置 */}
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Smartphone className="w-5 h-5 text-[#38bdac]" />
</CardTitle>
<CardDescription className="text-gray-400"></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-gray-300">AppID</Label>
<Input
className="bg-[#0a1628] border-gray-700 text-white"
value={mpConfig.appId}
onChange={(e) => setMpConfig(prev => ({ ...prev, appId: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label className="text-gray-300">API域名</Label>
<Input
className="bg-[#0a1628] border-gray-700 text-white"
value={mpConfig.apiDomain}
onChange={(e) => setMpConfig(prev => ({ ...prev, apiDomain: e.target.value }))}
/>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label className="text-gray-300"> (%)</Label>
<Input
type="number"
className="bg-[#0a1628] border-gray-700 text-white"
value={mpConfig.buyerDiscount}
onChange={(e) => setMpConfig(prev => ({ ...prev, buyerDiscount: Number(e.target.value) }))}
/>
</div>
<div className="space-y-2">
<Label className="text-gray-300"></Label>
<Input
type="number"
className="bg-[#0a1628] border-gray-700 text-white"
value={mpConfig.referralBindDays}
onChange={(e) => setMpConfig(prev => ({ ...prev, referralBindDays: Number(e.target.value) }))}
/>
</div>
<div className="space-y-2">
<Label className="text-gray-300"> ()</Label>
<Input
type="number"
className="bg-[#0a1628] border-gray-700 text-white"
value={mpConfig.minWithdraw}
onChange={(e) => setMpConfig(prev => ({ ...prev, minWithdraw: Number(e.target.value) }))}
/>
</div>
</div>
</CardContent>
</Card>
{/* 分销设置 */}
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
<CardHeader>

View File

@@ -142,10 +142,28 @@ export async function GET(request: NextRequest) {
}
}
// 获取小程序配置
let mpConfig = null
try {
mpConfig = await getConfig('mp_config')
} catch (e) {}
// 提取前端需要的格式
const bookConfig = allConfigs.book_config || DEFAULT_CONFIGS.book_config
return NextResponse.json({
success: true,
configs: allConfigs,
sources
sources,
// 前端直接使用的格式
freeChapters: bookConfig.freeSections || DEFAULT_CONFIGS.book_config.freeSections,
mpConfig: mpConfig || {
appId: 'wxb8bbb2b10dec74aa',
apiDomain: 'https://soul.quwanzhi.com',
buyerDiscount: 5,
referralBindDays: 30,
minWithdraw: 10
}
})
} catch (error) {
@@ -159,10 +177,42 @@ export async function GET(request: NextRequest) {
/**
* POST - 保存配置到数据库
* 支持两种格式:
* 1. { key, config } - 单个配置
* 2. { freeChapters, mpConfig } - 批量配置
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// 支持批量配置格式
if (body.freeChapters || body.mpConfig) {
let successCount = 0
// 保存免费章节配置
if (body.freeChapters) {
const bookConfig = {
...DEFAULT_CONFIGS.book_config,
freeSections: body.freeChapters
}
const success = await setConfig('book_config', bookConfig, '书籍配置-免费章节')
if (success) successCount++
}
// 保存小程序配置
if (body.mpConfig) {
const success = await setConfig('mp_config', body.mpConfig, '小程序配置')
if (success) successCount++
}
return NextResponse.json({
success: true,
message: `配置保存成功 (${successCount}项)`,
successCount
})
}
// 原有的单配置格式
const { key, config, description } = body
if (!key || !config) {

View File

@@ -71,8 +71,24 @@ Page({
app.handleReferralCode({ query: { ref } })
}
// 加载免费章节配置
this.loadFreeChaptersConfig()
this.initSection(id)
},
// 从后端加载免费章节配置
async loadFreeChaptersConfig() {
try {
const res = await app.request('/api/db/config')
if (res.success && res.freeChapters) {
this.setData({ freeIds: res.freeChapters })
console.log('[Read] 加载免费章节配置:', res.freeChapters)
}
} catch (e) {
console.log('[Read] 使用默认免费章节配置')
}
},
onPageScroll(e) {
// 计算阅读进度