Files
soul-yongping/next-project/app/admin/settings/page.tsx

646 lines
27 KiB
TypeScript
Raw Normal View History

"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Label } from "@/components/ui/label"
import { Input } from "@/components/ui/input"
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, Gift, X, Plus, Smartphone } from "lucide-react"
export default function SettingsPage() {
const { settings, updateSettings } = useStore()
const [localSettings, setLocalSettings] = useState({
sectionPrice: settings.sectionPrice,
baseBookPrice: settings.baseBookPrice,
distributorShare: settings.distributorShare,
authorInfo: {
...settings.authorInfo,
startDate: settings.authorInfo?.startDate || "2025年10月15日",
bio: settings.authorInfo?.bio || "连续创业者,私域运营专家,每天早上6-9点在Soul派对房分享真实商业故事",
},
})
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, // 最低提现金额
})
// 功能开关配置
const [featureConfig, setFeatureConfig] = useState({
matchEnabled: true, // 找伙伴功能开关(默认开启)
referralEnabled: true, // 推广功能开关
searchEnabled: true, // 搜索功能开关
aboutEnabled: true // 关于页面开关
})
// 加载配置
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 }))
if (data.features) setFeatureConfig(prev => ({ ...prev, ...data.features }))
}
} catch (e) {
console.log('Load config error:', e)
}
}
loadConfig()
}, [])
useEffect(() => {
setLocalSettings({
sectionPrice: settings.sectionPrice,
baseBookPrice: settings.baseBookPrice,
distributorShare: settings.distributorShare,
authorInfo: {
...settings.authorInfo,
startDate: settings.authorInfo?.startDate || "2025年10月15日",
bio: settings.authorInfo?.bio || "连续创业者,私域运营专家,每天早上6-9点在Soul派对房分享真实商业故事",
},
})
}, [settings])
const handleSave = async () => {
setIsSaving(true)
try {
updateSettings(localSettings)
// 同时保存到数据库
await fetch('/api/db/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(localSettings)
})
// 保存免费章节和小程序配置
const res1 = await fetch('/api/db/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ freeChapters, mpConfig })
})
const result1 = await res1.json()
console.log('保存免费章节和小程序配置:', result1)
// 保存功能开关配置
const res2 = await fetch('/api/db/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
key: 'feature_config',
config: featureConfig,
description: '功能开关配置'
})
})
const result2 = await res2.json()
console.log('保存功能开关配置:', result2)
// 验证保存结果
const verifyRes = await fetch('/api/db/config')
const verifyData = await verifyRes.json()
console.log('验证保存结果:', verifyData.features)
// 立即更新本地状态
if (verifyData.features) {
setFeatureConfig(prev => ({ ...prev, ...verifyData.features }))
}
alert("设置已保存!\n\n找伙伴功能" + (verifyData.features?.matchEnabled ? "✅ 开启" : "❌ 关闭"))
} catch (error) {
console.error('Save settings error:', error)
alert("保存失败: " + (error as Error).message)
} finally {
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">
<div className="flex justify-between items-center mb-8">
<div>
<h2 className="text-2xl font-bold text-white"></h2>
<p className="text-gray-400 mt-1"></p>
</div>
<Button
onClick={handleSave}
disabled={isSaving}
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
>
<Save className="w-4 h-4 mr-2" />
{isSaving ? "保存中..." : "保存设置"}
</Button>
</div>
<div className="space-y-6">
{/* 作者信息 - 重点增强 */}
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<UserCircle 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 htmlFor="author-name" className="text-gray-300 flex items-center gap-1">
<UserCircle className="w-3 h-3" />
</Label>
<Input
id="author-name"
className="bg-[#0a1628] border-gray-700 text-white"
value={localSettings.authorInfo.name}
onChange={(e) =>
setLocalSettings((prev) => ({
...prev,
authorInfo: { ...prev.authorInfo, name: e.target.value },
}))
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="start-date" className="text-gray-300 flex items-center gap-1">
<Calendar className="w-3 h-3" />
</Label>
<Input
id="start-date"
className="bg-[#0a1628] border-gray-700 text-white"
placeholder="例如: 2025年10月15日"
value={localSettings.authorInfo.startDate || ""}
onChange={(e) =>
setLocalSettings((prev) => ({
...prev,
authorInfo: { ...prev.authorInfo, startDate: e.target.value },
}))
}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="live-time" className="text-gray-300 flex items-center gap-1">
<Calendar className="w-3 h-3" />
</Label>
<Input
id="live-time"
className="bg-[#0a1628] border-gray-700 text-white"
placeholder="例如: 06:00-09:00"
value={localSettings.authorInfo.liveTime}
onChange={(e) =>
setLocalSettings((prev) => ({
...prev,
authorInfo: { ...prev.authorInfo, liveTime: e.target.value },
}))
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="platform" className="text-gray-300 flex items-center gap-1">
<MapPin className="w-3 h-3" />
</Label>
<Input
id="platform"
className="bg-[#0a1628] border-gray-700 text-white"
placeholder="例如: Soul派对房"
value={localSettings.authorInfo.platform}
onChange={(e) =>
setLocalSettings((prev) => ({
...prev,
authorInfo: { ...prev.authorInfo, platform: e.target.value },
}))
}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="description" className="text-gray-300 flex items-center gap-1">
<BookOpen className="w-3 h-3" />
</Label>
<Input
id="description"
className="bg-[#0a1628] border-gray-700 text-white"
value={localSettings.authorInfo.description}
onChange={(e) =>
setLocalSettings((prev) => ({
...prev,
authorInfo: { ...prev.authorInfo, description: e.target.value },
}))
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="bio" className="text-gray-300"></Label>
<Textarea
id="bio"
className="bg-[#0a1628] border-gray-700 text-white min-h-[100px]"
placeholder="输入作者详细介绍..."
value={localSettings.authorInfo.bio || ""}
onChange={(e) =>
setLocalSettings((prev) => ({
...prev,
authorInfo: { ...prev.authorInfo, bio: e.target.value },
}))
}
/>
</div>
{/* 预览卡片 */}
<div className="mt-4 p-4 rounded-xl bg-[#0a1628] border border-[#38bdac]/30">
<p className="text-xs text-gray-500 mb-2"></p>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center text-xl font-bold text-white">
{localSettings.authorInfo.name?.charAt(0) || "K"}
</div>
<div>
<p className="text-white font-semibold">{localSettings.authorInfo.name}</p>
<p className="text-gray-400 text-xs">{localSettings.authorInfo.description}</p>
<p className="text-[#38bdac] text-xs mt-1">
{localSettings.authorInfo.liveTime} · {localSettings.authorInfo.platform}
</p>
</div>
</div>
</div>
</CardContent>
</Card>
{/* 价格设置 */}
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<DollarSign 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"> ()</Label>
<Input
type="number"
className="bg-[#0a1628] border-gray-700 text-white"
value={localSettings.sectionPrice}
onChange={(e) =>
setLocalSettings((prev) => ({
...prev,
sectionPrice: Number.parseFloat(e.target.value) || 1,
}))
}
/>
</div>
<div className="space-y-2">
<Label className="text-gray-300"> ()</Label>
<Input
type="number"
className="bg-[#0a1628] border-gray-700 text-white"
value={localSettings.baseBookPrice}
onChange={(e) =>
setLocalSettings((prev) => ({
...prev,
baseBookPrice: Number.parseFloat(e.target.value) || 9.9,
}))
}
/>
</div>
</div>
</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">
<Settings className="w-5 h-5 text-[#38bdac]" />
</CardTitle>
<CardDescription className="text-gray-400">/</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-4">
{/* 找伙伴功能开关 */}
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Users className="w-4 h-4 text-[#38bdac]" />
<Label htmlFor="match-enabled" className="text-white font-medium cursor-pointer">
</Label>
</div>
<p className="text-xs text-gray-400 ml-6">
Web端的找伙伴功能显示
</p>
</div>
<Switch
id="match-enabled"
checked={featureConfig.matchEnabled}
onCheckedChange={(checked) =>
setFeatureConfig(prev => ({ ...prev, matchEnabled: checked }))
}
/>
</div>
{/* 推广功能开关 */}
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Gift className="w-4 h-4 text-[#38bdac]" />
<Label htmlFor="referral-enabled" className="text-white font-medium cursor-pointer">
广
</Label>
</div>
<p className="text-xs text-gray-400 ml-6">
广
</p>
</div>
<Switch
id="referral-enabled"
checked={featureConfig.referralEnabled}
onCheckedChange={(checked) =>
setFeatureConfig(prev => ({ ...prev, referralEnabled: checked }))
}
/>
</div>
{/* 搜索功能开关 */}
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
<div className="space-y-1">
<div className="flex items-center gap-2">
<BookOpen className="w-4 h-4 text-[#38bdac]" />
<Label htmlFor="search-enabled" className="text-white font-medium cursor-pointer">
</Label>
</div>
<p className="text-xs text-gray-400 ml-6">
</p>
</div>
<Switch
id="search-enabled"
checked={featureConfig.searchEnabled}
onCheckedChange={(checked) =>
setFeatureConfig(prev => ({ ...prev, searchEnabled: checked }))
}
/>
</div>
{/* 关于页面开关 */}
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Settings className="w-4 h-4 text-[#38bdac]" />
<Label htmlFor="about-enabled" className="text-white font-medium cursor-pointer">
</Label>
</div>
<p className="text-xs text-gray-400 ml-6">
访
</p>
</div>
<Switch
id="about-enabled"
checked={featureConfig.aboutEnabled}
onCheckedChange={(checked) =>
setFeatureConfig(prev => ({ ...prev, aboutEnabled: checked }))
}
/>
</div>
</div>
<div className="p-3 rounded-lg bg-blue-500/10 border border-blue-500/30">
<p className="text-xs text-blue-300">
💡
</p>
</div>
</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>
<CardTitle className="text-white flex items-center gap-2">
<Users className="w-5 h-5 text-[#38bdac]" />
</CardTitle>
<CardDescription className="text-gray-400"></CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-4">
<div className="flex justify-between items-center">
<Label className="text-gray-300"></Label>
<span className="text-2xl font-bold text-[#38bdac]">{localSettings.distributorShare}%</span>
</div>
<Slider
value={[localSettings.distributorShare]}
onValueChange={([value]) =>
setLocalSettings((prev) => ({
...prev,
distributorShare: value,
}))
}
max={100}
step={5}
className="w-full"
/>
<div className="flex justify-between text-sm text-gray-400">
<span>: {100 - localSettings.distributorShare}%</span>
<span>: {localSettings.distributorShare}%</span>
</div>
</div>
</CardContent>
</Card>
{/* 功能开关 */}
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
<CardHeader>
<CardTitle className="text-white"></CardTitle>
<CardDescription className="text-gray-400"></CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center justify-between">
<Label htmlFor="maintenance-mode" className="flex flex-col space-y-1">
<span className="text-white"></span>
<span className="font-normal text-xs text-gray-500"></span>
</Label>
<Switch id="maintenance-mode" />
</div>
<div className="flex items-center justify-between">
<Label htmlFor="payment-enabled" className="flex flex-col space-y-1">
<span className="text-white"></span>
<span className="font-normal text-xs text-gray-500"></span>
</Label>
<Switch id="payment-enabled" defaultChecked />
</div>
<div className="flex items-center justify-between">
<Label htmlFor="referral-enabled" className="flex flex-col space-y-1">
<span className="text-white"></span>
<span className="font-normal text-xs text-gray-500"></span>
</Label>
<Switch
id="referral-enabled"
checked={featureConfig.referralEnabled}
onCheckedChange={(checked) => setFeatureConfig(prev => ({ ...prev, referralEnabled: checked }))}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="match-enabled" className="flex flex-col space-y-1">
<span className="text-white"></span>
<span className="font-normal text-xs text-gray-500"></span>
</Label>
<Switch
id="match-enabled"
checked={featureConfig.matchEnabled}
onCheckedChange={(checked) => setFeatureConfig(prev => ({ ...prev, matchEnabled: checked }))}
/>
</div>
</CardContent>
</Card>
</div>
</div>
)
}