重构对话框组件,使用 React.forwardRef 优化 DialogOverlay 和 DialogContent 的实现,提升代码可读性和可维护性。同时,更新 ReferralSettingsPage 和 SettingsPage 以使用新的 API 路径,确保数据获取和保存逻辑的一致性,增强用户体验。
This commit is contained in:
@@ -11,42 +11,43 @@ function DialogPortal(props: React.ComponentProps<typeof DialogPrimitive.Portal>
|
||||
return <DialogPrimitive.Portal {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
className={cn('fixed inset-0 z-50 bg-black/50', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn('fixed inset-0 z-50 bg-black/50', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = 'DialogOverlay'
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & { showCloseButton?: boolean }) {
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
className={cn(
|
||||
'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border bg-background p-6 shadow-lg sm:max-w-lg',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { showCloseButton?: boolean }
|
||||
>(({ className, children, showCloseButton = true, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border bg-background p-6 shadow-lg sm:max-w-lg',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = 'DialogContent'
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return <div className={cn('flex flex-col gap-2 text-center sm:text-left', className)} {...props} />
|
||||
|
||||
@@ -31,10 +31,10 @@ export function ReferralSettingsPage() {
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
get<{ success?: boolean; data?: ReferralConfig }>('/api/db/config/full?key=referral_config')
|
||||
.then((data) => {
|
||||
const c = (data as { data?: ReferralConfig; config?: ReferralConfig })?.data ?? (data as { config?: ReferralConfig })?.config
|
||||
if (c) {
|
||||
get<{ success?: boolean; data?: ReferralConfig }>('/api/admin/referral-settings')
|
||||
.then((res) => {
|
||||
const c = (res as { data?: ReferralConfig })?.data
|
||||
if (c && typeof c === 'object') {
|
||||
setConfig({
|
||||
distributorShare: c.distributorShare ?? 90,
|
||||
minWithdrawAmount: c.minWithdrawAmount ?? 10,
|
||||
@@ -51,19 +51,14 @@ export function ReferralSettingsPage() {
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
try {
|
||||
const safeConfig = {
|
||||
const body = {
|
||||
distributorShare: Number(config.distributorShare) || 0,
|
||||
minWithdrawAmount: Number(config.minWithdrawAmount) || 0,
|
||||
bindingDays: Number(config.bindingDays) || 0,
|
||||
userDiscount: Number(config.userDiscount) || 0,
|
||||
enableAutoWithdraw: Boolean(config.enableAutoWithdraw),
|
||||
}
|
||||
const body = {
|
||||
key: 'referral_config',
|
||||
value: safeConfig,
|
||||
description: '分销 / 推广规则配置',
|
||||
}
|
||||
const res = await post<{ success?: boolean; error?: string }>('/api/db/config', body)
|
||||
const res = await post<{ success?: boolean; error?: string }>('/api/admin/referral-settings', body)
|
||||
if (!res || (res as { success?: boolean }).success === false) {
|
||||
alert('保存失败: ' + (res && typeof res === 'object' && 'error' in res ? (res as { error?: string }).error : ''))
|
||||
return
|
||||
|
||||
@@ -9,6 +9,14 @@ import {
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
@@ -23,7 +31,6 @@ import {
|
||||
Gift,
|
||||
X,
|
||||
Plus,
|
||||
Smartphone,
|
||||
} from 'lucide-react'
|
||||
import { get, post } from '@/api/client'
|
||||
|
||||
@@ -43,14 +50,6 @@ interface LocalSettings {
|
||||
authorInfo: AuthorInfo
|
||||
}
|
||||
|
||||
interface MpConfig {
|
||||
appId: string
|
||||
apiDomain: string
|
||||
buyerDiscount: number
|
||||
referralBindDays: number
|
||||
minWithdraw: number
|
||||
}
|
||||
|
||||
interface FeatureConfig {
|
||||
matchEnabled: boolean
|
||||
referralEnabled: boolean
|
||||
@@ -74,14 +73,6 @@ const defaultSettings: LocalSettings = {
|
||||
authorInfo: { ...defaultAuthor },
|
||||
}
|
||||
|
||||
const defaultMp: MpConfig = {
|
||||
appId: 'wxb8bbb2b10dec74aa',
|
||||
apiDomain: 'https://soul.quwanzhi.com',
|
||||
buyerDiscount: 5,
|
||||
referralBindDays: 30,
|
||||
minWithdraw: 10,
|
||||
}
|
||||
|
||||
const defaultFeatures: FeatureConfig = {
|
||||
matchEnabled: true,
|
||||
referralEnabled: true,
|
||||
@@ -89,54 +80,6 @@ const defaultFeatures: FeatureConfig = {
|
||||
aboutEnabled: true,
|
||||
}
|
||||
|
||||
function parseConfigResponse(raw: unknown): {
|
||||
freeChapters?: string[]
|
||||
mpConfig?: Partial<MpConfig>
|
||||
features?: Partial<FeatureConfig>
|
||||
sectionPrice?: number
|
||||
baseBookPrice?: number
|
||||
distributorShare?: number
|
||||
authorInfo?: AuthorInfo
|
||||
} {
|
||||
if (!raw || typeof raw !== 'object') return {}
|
||||
const o = raw as Record<string, unknown>
|
||||
const out: ReturnType<typeof parseConfigResponse> = {}
|
||||
if (Array.isArray(o.freeChapters)) out.freeChapters = o.freeChapters as string[]
|
||||
if (o.mpConfig && typeof o.mpConfig === 'object') out.mpConfig = o.mpConfig as Partial<MpConfig>
|
||||
if (o.features && typeof o.features === 'object') out.features = o.features as Partial<FeatureConfig>
|
||||
if (typeof o.sectionPrice === 'number') out.sectionPrice = o.sectionPrice
|
||||
if (typeof o.baseBookPrice === 'number') out.baseBookPrice = o.baseBookPrice
|
||||
if (typeof o.distributorShare === 'number') out.distributorShare = o.distributorShare
|
||||
if (o.authorInfo && typeof o.authorInfo === 'object') out.authorInfo = o.authorInfo as AuthorInfo
|
||||
return out
|
||||
}
|
||||
|
||||
function mergeFromConfigList(list: unknown[]): ReturnType<typeof parseConfigResponse> {
|
||||
const out: ReturnType<typeof parseConfigResponse> = {}
|
||||
for (const item of list) {
|
||||
if (!item || typeof item !== 'object') continue
|
||||
const row = item as { configKey?: string; configValue?: string }
|
||||
const key = row.configKey
|
||||
let val: unknown
|
||||
try {
|
||||
val = typeof row.configValue === 'string' ? JSON.parse(row.configValue) : row.configValue
|
||||
} catch {
|
||||
val = row.configValue
|
||||
}
|
||||
if (key === 'feature_config' && val && typeof val === 'object') out.features = val as Partial<FeatureConfig>
|
||||
if (key === 'mp_config' && val && typeof val === 'object') out.mpConfig = val as Partial<MpConfig>
|
||||
if (key === 'free_chapters' && Array.isArray(val)) out.freeChapters = val as string[]
|
||||
if (key === 'site_settings' && val && typeof val === 'object') {
|
||||
const s = val as Record<string, unknown>
|
||||
if (typeof s.sectionPrice === 'number') out.sectionPrice = s.sectionPrice
|
||||
if (typeof s.baseBookPrice === 'number') out.baseBookPrice = s.baseBookPrice
|
||||
if (typeof s.distributorShare === 'number') out.distributorShare = s.distributorShare
|
||||
if (s.authorInfo && typeof s.authorInfo === 'object') out.authorInfo = s.authorInfo as AuthorInfo
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export function SettingsPage() {
|
||||
const [localSettings, setLocalSettings] = useState<LocalSettings>(defaultSettings)
|
||||
const [freeChapters, setFreeChapters] = useState<string[]>([
|
||||
@@ -148,53 +91,47 @@ export function SettingsPage() {
|
||||
'appendix-3',
|
||||
])
|
||||
const [newFreeChapter, setNewFreeChapter] = useState('')
|
||||
const [mpConfig, setMpConfig] = useState<MpConfig>(defaultMp)
|
||||
const [featureConfig, setFeatureConfig] = useState<FeatureConfig>(defaultFeatures)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const [dialogTitle, setDialogTitle] = useState('')
|
||||
const [dialogMessage, setDialogMessage] = useState('')
|
||||
const [dialogIsError, setDialogIsError] = useState(false)
|
||||
const [featureSwitchSaving, setFeatureSwitchSaving] = useState(false)
|
||||
|
||||
const showResult = (title: string, message: string, isError = false) => {
|
||||
setDialogTitle(title)
|
||||
setDialogMessage(message)
|
||||
setDialogIsError(isError)
|
||||
setDialogOpen(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
const [configRes, appConfigRes] = await Promise.all([
|
||||
get<{ success?: boolean; data?: unknown } | Record<string, unknown>>('/api/db/config/full'),
|
||||
get<Record<string, unknown>>('/api/config').catch(() => null),
|
||||
])
|
||||
let parsed = parseConfigResponse(
|
||||
(configRes as { data?: unknown })?.data ?? configRes,
|
||||
)
|
||||
const data = (configRes as { data?: unknown })?.data
|
||||
if (Array.isArray(data)) parsed = { ...parsed, ...mergeFromConfigList(data) }
|
||||
if (parsed.freeChapters?.length) setFreeChapters(parsed.freeChapters)
|
||||
if (parsed.mpConfig && Object.keys(parsed.mpConfig).length)
|
||||
setMpConfig((prev) => ({ ...prev, ...parsed.mpConfig }))
|
||||
if (parsed.features && Object.keys(parsed.features).length)
|
||||
setFeatureConfig((prev) => ({ ...prev, ...parsed.features }))
|
||||
if (
|
||||
appConfigRes?.authorInfo &&
|
||||
typeof appConfigRes.authorInfo === 'object'
|
||||
) {
|
||||
const res = await get<{
|
||||
success?: boolean
|
||||
freeChapters?: string[]
|
||||
featureConfig?: Partial<FeatureConfig>
|
||||
siteSettings?: { sectionPrice?: number; baseBookPrice?: number; distributorShare?: number; authorInfo?: AuthorInfo }
|
||||
}>('/api/admin/settings')
|
||||
if (!res || (res as { success?: boolean }).success === false) return
|
||||
if (Array.isArray(res.freeChapters) && res.freeChapters.length) setFreeChapters(res.freeChapters)
|
||||
if (res.featureConfig && Object.keys(res.featureConfig).length)
|
||||
setFeatureConfig((prev) => ({ ...prev, ...res.featureConfig }))
|
||||
if (res.siteSettings && typeof res.siteSettings === 'object') {
|
||||
const s = res.siteSettings
|
||||
setLocalSettings((prev) => ({
|
||||
...prev,
|
||||
authorInfo: { ...prev.authorInfo, ...(appConfigRes.authorInfo as AuthorInfo) },
|
||||
}))
|
||||
}
|
||||
if (
|
||||
typeof parsed.sectionPrice === 'number' ||
|
||||
typeof parsed.baseBookPrice === 'number' ||
|
||||
typeof parsed.distributorShare === 'number' ||
|
||||
(parsed.authorInfo && Object.keys(parsed.authorInfo).length)
|
||||
) {
|
||||
setLocalSettings((prev) => ({
|
||||
...prev,
|
||||
...(typeof parsed.sectionPrice === 'number' && { sectionPrice: parsed.sectionPrice }),
|
||||
...(typeof parsed.baseBookPrice === 'number' && { baseBookPrice: parsed.baseBookPrice }),
|
||||
...(typeof parsed.distributorShare === 'number' && { distributorShare: parsed.distributorShare }),
|
||||
...(parsed.authorInfo && { authorInfo: { ...prev.authorInfo, ...parsed.authorInfo } }),
|
||||
...(typeof s.sectionPrice === 'number' && { sectionPrice: s.sectionPrice }),
|
||||
...(typeof s.baseBookPrice === 'number' && { baseBookPrice: s.baseBookPrice }),
|
||||
...(typeof s.distributorShare === 'number' && { distributorShare: s.distributorShare }),
|
||||
...(s.authorInfo && typeof s.authorInfo === 'object' && { authorInfo: { ...prev.authorInfo, ...s.authorInfo } }),
|
||||
}))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Load config error:', e)
|
||||
console.error('Load settings error:', e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -202,37 +139,58 @@ export function SettingsPage() {
|
||||
load()
|
||||
}, [])
|
||||
|
||||
const saveFeatureConfigOnly = async (
|
||||
nextConfig: FeatureConfig,
|
||||
onFailRevert: () => void,
|
||||
) => {
|
||||
setFeatureSwitchSaving(true)
|
||||
try {
|
||||
const res = await post<{ success?: boolean; error?: string }>('/api/admin/settings', {
|
||||
featureConfig: nextConfig,
|
||||
})
|
||||
if (!res || (res as { success?: boolean }).success === false) {
|
||||
onFailRevert()
|
||||
showResult('保存失败', (res as { error?: string })?.error ?? '未知错误', true)
|
||||
return
|
||||
}
|
||||
showResult('已保存', '功能开关已更新,相关入口将随之显示或隐藏。')
|
||||
} catch (error) {
|
||||
console.error('Save feature config error:', error)
|
||||
onFailRevert()
|
||||
showResult('保存失败', error instanceof Error ? error.message : String(error), true)
|
||||
} finally {
|
||||
setFeatureSwitchSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFeatureSwitch = (field: keyof FeatureConfig, checked: boolean) => {
|
||||
const prev = featureConfig
|
||||
const next = { ...prev, [field]: checked }
|
||||
setFeatureConfig(next)
|
||||
saveFeatureConfigOnly(next, () => setFeatureConfig(prev))
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true)
|
||||
try {
|
||||
await post('/api/db/config', {
|
||||
key: 'free_chapters',
|
||||
value: freeChapters,
|
||||
description: '免费章节ID列表',
|
||||
}).catch(() => {})
|
||||
await post('/api/db/config', {
|
||||
key: 'mp_config',
|
||||
value: mpConfig,
|
||||
description: '小程序配置',
|
||||
}).catch(() => {})
|
||||
await post('/api/db/config', {
|
||||
key: 'feature_config',
|
||||
value: featureConfig,
|
||||
description: '功能开关配置',
|
||||
const res = await post<{ success?: boolean; error?: string }>('/api/admin/settings', {
|
||||
freeChapters,
|
||||
featureConfig,
|
||||
siteSettings: {
|
||||
sectionPrice: localSettings.sectionPrice,
|
||||
baseBookPrice: localSettings.baseBookPrice,
|
||||
distributorShare: localSettings.distributorShare,
|
||||
authorInfo: localSettings.authorInfo,
|
||||
},
|
||||
})
|
||||
const verifyRes = await get<{ features?: FeatureConfig }>('/api/db/config/full').catch(() => ({}))
|
||||
const verifyData = Array.isArray((verifyRes as { data?: unknown })?.data)
|
||||
? mergeFromConfigList((verifyRes as { data: unknown[] }).data)
|
||||
: parseConfigResponse((verifyRes as { data?: unknown })?.data ?? verifyRes)
|
||||
if (verifyData.features)
|
||||
setFeatureConfig((prev) => ({ ...prev, ...verifyData.features }))
|
||||
alert(
|
||||
'设置已保存!\n\n找伙伴功能:' +
|
||||
(verifyData.features?.matchEnabled ? '✅ 开启' : '❌ 关闭'),
|
||||
)
|
||||
if (!res || (res as { success?: boolean }).success === false) {
|
||||
showResult('保存失败', (res as { error?: string })?.error ?? '未知错误', true)
|
||||
return
|
||||
}
|
||||
showResult('已保存', '设置已保存成功。')
|
||||
} catch (error) {
|
||||
console.error('Save settings error:', error)
|
||||
alert('保存失败: ' + (error instanceof Error ? error.message : String(error)))
|
||||
showResult('保存失败', error instanceof Error ? error.message : String(error), true)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
@@ -523,9 +481,8 @@ export function SettingsPage() {
|
||||
<Switch
|
||||
id="match-enabled"
|
||||
checked={featureConfig.matchEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setFeatureConfig((prev) => ({ ...prev, matchEnabled: checked }))
|
||||
}
|
||||
disabled={featureSwitchSaving}
|
||||
onCheckedChange={(checked) => handleFeatureSwitch('matchEnabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
|
||||
@@ -541,9 +498,8 @@ export function SettingsPage() {
|
||||
<Switch
|
||||
id="referral-enabled"
|
||||
checked={featureConfig.referralEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setFeatureConfig((prev) => ({ ...prev, referralEnabled: checked }))
|
||||
}
|
||||
disabled={featureSwitchSaving}
|
||||
onCheckedChange={(checked) => handleFeatureSwitch('referralEnabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
|
||||
@@ -559,9 +515,8 @@ export function SettingsPage() {
|
||||
<Switch
|
||||
id="search-enabled"
|
||||
checked={featureConfig.searchEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setFeatureConfig((prev) => ({ ...prev, searchEnabled: checked }))
|
||||
}
|
||||
disabled={featureSwitchSaving}
|
||||
onCheckedChange={(checked) => handleFeatureSwitch('searchEnabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
|
||||
@@ -577,9 +532,8 @@ export function SettingsPage() {
|
||||
<Switch
|
||||
id="about-enabled"
|
||||
checked={featureConfig.aboutEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setFeatureConfig((prev) => ({ ...prev, aboutEnabled: checked }))
|
||||
}
|
||||
disabled={featureSwitchSaving}
|
||||
onCheckedChange={(checked) => handleFeatureSwitch('aboutEnabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -590,72 +544,31 @@ export function SettingsPage() {
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent
|
||||
className="bg-[#0f2137] border-gray-700 text-white"
|
||||
showCloseButton={true}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className={dialogIsError ? 'text-red-400' : 'text-[#38bdac]'}>
|
||||
{dialogTitle}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-gray-400 whitespace-pre-wrap pt-2">
|
||||
{dialogMessage}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="mt-4">
|
||||
<Button
|
||||
onClick={() => setDialogOpen(false)}
|
||||
className={dialogIsError ? 'bg-gray-600 hover:bg-gray-500' : 'bg-[#38bdac] hover:bg-[#2da396]'}
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -122,6 +122,167 @@ func DBConfigGet(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": data})
|
||||
}
|
||||
|
||||
// AdminSettingsGet GET /api/admin/settings 系统设置页专用:仅返回免费章节、功能开关、站点/作者与价格
|
||||
func AdminSettingsGet(c *gin.Context) {
|
||||
db := database.DB()
|
||||
out := gin.H{
|
||||
"success": true,
|
||||
"freeChapters": []string{"preface", "epilogue", "1.1", "appendix-1", "appendix-2", "appendix-3"},
|
||||
"featureConfig": gin.H{"matchEnabled": true, "referralEnabled": true, "searchEnabled": true, "aboutEnabled": true},
|
||||
"siteSettings": gin.H{"sectionPrice": float64(1), "baseBookPrice": 9.9, "distributorShare": float64(90), "authorInfo": gin.H{}},
|
||||
}
|
||||
keys := []string{"free_chapters", "feature_config", "site_settings"}
|
||||
for _, k := range keys {
|
||||
var row model.SystemConfig
|
||||
if err := db.Where("config_key = ?", k).First(&row).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
var val interface{}
|
||||
if err := json.Unmarshal(row.ConfigValue, &val); err != nil {
|
||||
continue
|
||||
}
|
||||
switch k {
|
||||
case "free_chapters":
|
||||
if arr, ok := val.([]interface{}); ok && len(arr) > 0 {
|
||||
ss := make([]string, 0, len(arr))
|
||||
for _, x := range arr {
|
||||
if s, ok := x.(string); ok {
|
||||
ss = append(ss, s)
|
||||
}
|
||||
}
|
||||
if len(ss) > 0 {
|
||||
out["freeChapters"] = ss
|
||||
}
|
||||
}
|
||||
case "feature_config":
|
||||
if m, ok := val.(map[string]interface{}); ok && len(m) > 0 {
|
||||
out["featureConfig"] = m
|
||||
}
|
||||
case "site_settings":
|
||||
if m, ok := val.(map[string]interface{}); ok && len(m) > 0 {
|
||||
out["siteSettings"] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
|
||||
// AdminSettingsPost POST /api/admin/settings 系统设置页专用:一次性保存免费章节、功能开关、站点/作者与价格(不包含小程序配置,该配置已移除)
|
||||
func AdminSettingsPost(c *gin.Context) {
|
||||
var body struct {
|
||||
FreeChapters []string `json:"freeChapters"`
|
||||
FeatureConfig map[string]interface{} `json:"featureConfig"`
|
||||
SiteSettings map[string]interface{} `json:"siteSettings"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
saveKey := func(key, desc string, value interface{}) error {
|
||||
valBytes, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var row model.SystemConfig
|
||||
err = db.Where("config_key = ?", key).First(&row).Error
|
||||
if err != nil {
|
||||
row = model.SystemConfig{ConfigKey: key, ConfigValue: valBytes, Description: &desc}
|
||||
return db.Create(&row).Error
|
||||
}
|
||||
row.ConfigValue = valBytes
|
||||
if desc != "" {
|
||||
row.Description = &desc
|
||||
}
|
||||
return db.Save(&row).Error
|
||||
}
|
||||
if body.FreeChapters != nil {
|
||||
if err := saveKey("free_chapters", "免费章节ID列表", body.FreeChapters); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "保存免费章节失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.FeatureConfig != nil {
|
||||
if err := saveKey("feature_config", "功能开关配置", body.FeatureConfig); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "保存功能开关失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.SiteSettings != nil {
|
||||
if err := saveKey("site_settings", "站点与作者配置", body.SiteSettings); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "保存站点设置失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "设置已保存"})
|
||||
}
|
||||
|
||||
// AdminReferralSettingsGet GET /api/admin/referral-settings 推广设置页专用:仅返回 referral_config
|
||||
func AdminReferralSettingsGet(c *gin.Context) {
|
||||
db := database.DB()
|
||||
defaultConfig := gin.H{
|
||||
"distributorShare": float64(90),
|
||||
"minWithdrawAmount": float64(10),
|
||||
"bindingDays": float64(30),
|
||||
"userDiscount": float64(5),
|
||||
"enableAutoWithdraw": false,
|
||||
}
|
||||
var row model.SystemConfig
|
||||
if err := db.Where("config_key = ?", "referral_config").First(&row).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": defaultConfig})
|
||||
return
|
||||
}
|
||||
var val map[string]interface{}
|
||||
if err := json.Unmarshal(row.ConfigValue, &val); err != nil || len(val) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": defaultConfig})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": val})
|
||||
}
|
||||
|
||||
// AdminReferralSettingsPost POST /api/admin/referral-settings 推广设置页专用:仅保存 referral_config(请求体为完整配置对象)
|
||||
func AdminReferralSettingsPost(c *gin.Context) {
|
||||
var body struct {
|
||||
DistributorShare float64 `json:"distributorShare"`
|
||||
MinWithdrawAmount float64 `json:"minWithdrawAmount"`
|
||||
BindingDays float64 `json:"bindingDays"`
|
||||
UserDiscount float64 `json:"userDiscount"`
|
||||
EnableAutoWithdraw bool `json:"enableAutoWithdraw"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||
return
|
||||
}
|
||||
val := gin.H{
|
||||
"distributorShare": body.DistributorShare,
|
||||
"minWithdrawAmount": body.MinWithdrawAmount,
|
||||
"bindingDays": body.BindingDays,
|
||||
"userDiscount": body.UserDiscount,
|
||||
"enableAutoWithdraw": body.EnableAutoWithdraw,
|
||||
}
|
||||
valBytes, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
desc := "分销 / 推广规则配置"
|
||||
var row model.SystemConfig
|
||||
if err := db.Where("config_key = ?", "referral_config").First(&row).Error; err != nil {
|
||||
row = model.SystemConfig{ConfigKey: "referral_config", ConfigValue: valBytes, Description: &desc}
|
||||
err = db.Create(&row).Error
|
||||
} else {
|
||||
row.ConfigValue = valBytes
|
||||
row.Description = &desc
|
||||
err = db.Save(&row).Error
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "推广设置已保存"})
|
||||
}
|
||||
|
||||
// DBConfigPost POST /api/db/config
|
||||
func DBConfigPost(c *gin.Context) {
|
||||
var body struct {
|
||||
|
||||
@@ -59,6 +59,10 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
admin.DELETE("/referral", handler.AdminReferral)
|
||||
admin.GET("/withdrawals", handler.AdminWithdrawalsList)
|
||||
admin.PUT("/withdrawals", handler.AdminWithdrawalsAction)
|
||||
admin.GET("/settings", handler.AdminSettingsGet)
|
||||
admin.POST("/settings", handler.AdminSettingsPost)
|
||||
admin.GET("/referral-settings", handler.AdminReferralSettingsGet)
|
||||
admin.POST("/referral-settings", handler.AdminReferralSettingsPost)
|
||||
}
|
||||
|
||||
// ----- 鉴权 -----
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user