feat: 管理后台增加免费章节和小程序配置
1. 系统设置页新增免费章节管理(可动态添加/删除) 2. 新增小程序配置项(API域名、购买优惠、绑定天数等) 3. 前端从后端读取免费章节配置 4. 配置API支持新格式
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
// 计算阅读进度
|
||||
|
||||
Reference in New Issue
Block a user