重构对话框组件,使用 React.forwardRef 优化 DialogOverlay 和 DialogContent 的实现,提升代码可读性和可维护性。同时,更新 ReferralSettingsPage 和 SettingsPage 以使用新的 API 路径,确保数据获取和保存逻辑的一致性,增强用户体验。
This commit is contained in:
@@ -11,25 +11,26 @@ function DialogPortal(props: React.ComponentProps<typeof DialogPrimitive.Portal>
|
|||||||
return <DialogPrimitive.Portal {...props} />
|
return <DialogPrimitive.Portal {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
const DialogOverlay = React.forwardRef<
|
||||||
return (
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
className={cn('fixed inset-0 z-50 bg-black/50', className)}
|
className={cn('fixed inset-0 z-50 bg-black/50', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
))
|
||||||
}
|
DialogOverlay.displayName = 'DialogOverlay'
|
||||||
|
|
||||||
function DialogContent({
|
const DialogContent = React.forwardRef<
|
||||||
className,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
children,
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { showCloseButton?: boolean }
|
||||||
showCloseButton = true,
|
>(({ className, children, showCloseButton = true, ...props }, ref) => (
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & { showCloseButton?: boolean }) {
|
|
||||||
return (
|
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
className={cn(
|
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',
|
'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,
|
className,
|
||||||
@@ -45,8 +46,8 @@ function DialogContent({
|
|||||||
)}
|
)}
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
)
|
))
|
||||||
}
|
DialogContent.displayName = 'DialogContent'
|
||||||
|
|
||||||
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
return <div className={cn('flex flex-col gap-2 text-center sm:text-left', className)} {...props} />
|
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)
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
get<{ success?: boolean; data?: ReferralConfig }>('/api/db/config/full?key=referral_config')
|
get<{ success?: boolean; data?: ReferralConfig }>('/api/admin/referral-settings')
|
||||||
.then((data) => {
|
.then((res) => {
|
||||||
const c = (data as { data?: ReferralConfig; config?: ReferralConfig })?.data ?? (data as { config?: ReferralConfig })?.config
|
const c = (res as { data?: ReferralConfig })?.data
|
||||||
if (c) {
|
if (c && typeof c === 'object') {
|
||||||
setConfig({
|
setConfig({
|
||||||
distributorShare: c.distributorShare ?? 90,
|
distributorShare: c.distributorShare ?? 90,
|
||||||
minWithdrawAmount: c.minWithdrawAmount ?? 10,
|
minWithdrawAmount: c.minWithdrawAmount ?? 10,
|
||||||
@@ -51,19 +51,14 @@ export function ReferralSettingsPage() {
|
|||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
try {
|
try {
|
||||||
const safeConfig = {
|
const body = {
|
||||||
distributorShare: Number(config.distributorShare) || 0,
|
distributorShare: Number(config.distributorShare) || 0,
|
||||||
minWithdrawAmount: Number(config.minWithdrawAmount) || 0,
|
minWithdrawAmount: Number(config.minWithdrawAmount) || 0,
|
||||||
bindingDays: Number(config.bindingDays) || 0,
|
bindingDays: Number(config.bindingDays) || 0,
|
||||||
userDiscount: Number(config.userDiscount) || 0,
|
userDiscount: Number(config.userDiscount) || 0,
|
||||||
enableAutoWithdraw: Boolean(config.enableAutoWithdraw),
|
enableAutoWithdraw: Boolean(config.enableAutoWithdraw),
|
||||||
}
|
}
|
||||||
const body = {
|
const res = await post<{ success?: boolean; error?: string }>('/api/admin/referral-settings', body)
|
||||||
key: 'referral_config',
|
|
||||||
value: safeConfig,
|
|
||||||
description: '分销 / 推广规则配置',
|
|
||||||
}
|
|
||||||
const res = await post<{ success?: boolean; error?: string }>('/api/db/config', body)
|
|
||||||
if (!res || (res as { success?: boolean }).success === false) {
|
if (!res || (res as { success?: boolean }).success === false) {
|
||||||
alert('保存失败: ' + (res && typeof res === 'object' && 'error' in res ? (res as { error?: string }).error : ''))
|
alert('保存失败: ' + (res && typeof res === 'object' && 'error' in res ? (res as { error?: string }).error : ''))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ import {
|
|||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import {
|
import {
|
||||||
@@ -23,7 +31,6 @@ import {
|
|||||||
Gift,
|
Gift,
|
||||||
X,
|
X,
|
||||||
Plus,
|
Plus,
|
||||||
Smartphone,
|
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { get, post } from '@/api/client'
|
import { get, post } from '@/api/client'
|
||||||
|
|
||||||
@@ -43,14 +50,6 @@ interface LocalSettings {
|
|||||||
authorInfo: AuthorInfo
|
authorInfo: AuthorInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MpConfig {
|
|
||||||
appId: string
|
|
||||||
apiDomain: string
|
|
||||||
buyerDiscount: number
|
|
||||||
referralBindDays: number
|
|
||||||
minWithdraw: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FeatureConfig {
|
interface FeatureConfig {
|
||||||
matchEnabled: boolean
|
matchEnabled: boolean
|
||||||
referralEnabled: boolean
|
referralEnabled: boolean
|
||||||
@@ -74,14 +73,6 @@ const defaultSettings: LocalSettings = {
|
|||||||
authorInfo: { ...defaultAuthor },
|
authorInfo: { ...defaultAuthor },
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultMp: MpConfig = {
|
|
||||||
appId: 'wxb8bbb2b10dec74aa',
|
|
||||||
apiDomain: 'https://soul.quwanzhi.com',
|
|
||||||
buyerDiscount: 5,
|
|
||||||
referralBindDays: 30,
|
|
||||||
minWithdraw: 10,
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultFeatures: FeatureConfig = {
|
const defaultFeatures: FeatureConfig = {
|
||||||
matchEnabled: true,
|
matchEnabled: true,
|
||||||
referralEnabled: true,
|
referralEnabled: true,
|
||||||
@@ -89,54 +80,6 @@ const defaultFeatures: FeatureConfig = {
|
|||||||
aboutEnabled: true,
|
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() {
|
export function SettingsPage() {
|
||||||
const [localSettings, setLocalSettings] = useState<LocalSettings>(defaultSettings)
|
const [localSettings, setLocalSettings] = useState<LocalSettings>(defaultSettings)
|
||||||
const [freeChapters, setFreeChapters] = useState<string[]>([
|
const [freeChapters, setFreeChapters] = useState<string[]>([
|
||||||
@@ -148,53 +91,47 @@ export function SettingsPage() {
|
|||||||
'appendix-3',
|
'appendix-3',
|
||||||
])
|
])
|
||||||
const [newFreeChapter, setNewFreeChapter] = useState('')
|
const [newFreeChapter, setNewFreeChapter] = useState('')
|
||||||
const [mpConfig, setMpConfig] = useState<MpConfig>(defaultMp)
|
|
||||||
const [featureConfig, setFeatureConfig] = useState<FeatureConfig>(defaultFeatures)
|
const [featureConfig, setFeatureConfig] = useState<FeatureConfig>(defaultFeatures)
|
||||||
const [isSaving, setIsSaving] = useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
const [loading, setLoading] = useState(true)
|
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(() => {
|
useEffect(() => {
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
const [configRes, appConfigRes] = await Promise.all([
|
const res = await get<{
|
||||||
get<{ success?: boolean; data?: unknown } | Record<string, unknown>>('/api/db/config/full'),
|
success?: boolean
|
||||||
get<Record<string, unknown>>('/api/config').catch(() => null),
|
freeChapters?: string[]
|
||||||
])
|
featureConfig?: Partial<FeatureConfig>
|
||||||
let parsed = parseConfigResponse(
|
siteSettings?: { sectionPrice?: number; baseBookPrice?: number; distributorShare?: number; authorInfo?: AuthorInfo }
|
||||||
(configRes as { data?: unknown })?.data ?? configRes,
|
}>('/api/admin/settings')
|
||||||
)
|
if (!res || (res as { success?: boolean }).success === false) return
|
||||||
const data = (configRes as { data?: unknown })?.data
|
if (Array.isArray(res.freeChapters) && res.freeChapters.length) setFreeChapters(res.freeChapters)
|
||||||
if (Array.isArray(data)) parsed = { ...parsed, ...mergeFromConfigList(data) }
|
if (res.featureConfig && Object.keys(res.featureConfig).length)
|
||||||
if (parsed.freeChapters?.length) setFreeChapters(parsed.freeChapters)
|
setFeatureConfig((prev) => ({ ...prev, ...res.featureConfig }))
|
||||||
if (parsed.mpConfig && Object.keys(parsed.mpConfig).length)
|
if (res.siteSettings && typeof res.siteSettings === 'object') {
|
||||||
setMpConfig((prev) => ({ ...prev, ...parsed.mpConfig }))
|
const s = res.siteSettings
|
||||||
if (parsed.features && Object.keys(parsed.features).length)
|
|
||||||
setFeatureConfig((prev) => ({ ...prev, ...parsed.features }))
|
|
||||||
if (
|
|
||||||
appConfigRes?.authorInfo &&
|
|
||||||
typeof appConfigRes.authorInfo === 'object'
|
|
||||||
) {
|
|
||||||
setLocalSettings((prev) => ({
|
setLocalSettings((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
authorInfo: { ...prev.authorInfo, ...(appConfigRes.authorInfo as AuthorInfo) },
|
...(typeof s.sectionPrice === 'number' && { sectionPrice: s.sectionPrice }),
|
||||||
}))
|
...(typeof s.baseBookPrice === 'number' && { baseBookPrice: s.baseBookPrice }),
|
||||||
}
|
...(typeof s.distributorShare === 'number' && { distributorShare: s.distributorShare }),
|
||||||
if (
|
...(s.authorInfo && typeof s.authorInfo === 'object' && { authorInfo: { ...prev.authorInfo, ...s.authorInfo } }),
|
||||||
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 } }),
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Load config error:', e)
|
console.error('Load settings error:', e)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -202,37 +139,58 @@ export function SettingsPage() {
|
|||||||
load()
|
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 () => {
|
const handleSave = async () => {
|
||||||
setIsSaving(true)
|
setIsSaving(true)
|
||||||
try {
|
try {
|
||||||
await post('/api/db/config', {
|
const res = await post<{ success?: boolean; error?: string }>('/api/admin/settings', {
|
||||||
key: 'free_chapters',
|
freeChapters,
|
||||||
value: freeChapters,
|
featureConfig,
|
||||||
description: '免费章节ID列表',
|
siteSettings: {
|
||||||
}).catch(() => {})
|
sectionPrice: localSettings.sectionPrice,
|
||||||
await post('/api/db/config', {
|
baseBookPrice: localSettings.baseBookPrice,
|
||||||
key: 'mp_config',
|
distributorShare: localSettings.distributorShare,
|
||||||
value: mpConfig,
|
authorInfo: localSettings.authorInfo,
|
||||||
description: '小程序配置',
|
},
|
||||||
}).catch(() => {})
|
|
||||||
await post('/api/db/config', {
|
|
||||||
key: 'feature_config',
|
|
||||||
value: featureConfig,
|
|
||||||
description: '功能开关配置',
|
|
||||||
})
|
})
|
||||||
const verifyRes = await get<{ features?: FeatureConfig }>('/api/db/config/full').catch(() => ({}))
|
if (!res || (res as { success?: boolean }).success === false) {
|
||||||
const verifyData = Array.isArray((verifyRes as { data?: unknown })?.data)
|
showResult('保存失败', (res as { error?: string })?.error ?? '未知错误', true)
|
||||||
? mergeFromConfigList((verifyRes as { data: unknown[] }).data)
|
return
|
||||||
: parseConfigResponse((verifyRes as { data?: unknown })?.data ?? verifyRes)
|
}
|
||||||
if (verifyData.features)
|
showResult('已保存', '设置已保存成功。')
|
||||||
setFeatureConfig((prev) => ({ ...prev, ...verifyData.features }))
|
|
||||||
alert(
|
|
||||||
'设置已保存!\n\n找伙伴功能:' +
|
|
||||||
(verifyData.features?.matchEnabled ? '✅ 开启' : '❌ 关闭'),
|
|
||||||
)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Save settings error:', 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 {
|
} finally {
|
||||||
setIsSaving(false)
|
setIsSaving(false)
|
||||||
}
|
}
|
||||||
@@ -523,9 +481,8 @@ export function SettingsPage() {
|
|||||||
<Switch
|
<Switch
|
||||||
id="match-enabled"
|
id="match-enabled"
|
||||||
checked={featureConfig.matchEnabled}
|
checked={featureConfig.matchEnabled}
|
||||||
onCheckedChange={(checked) =>
|
disabled={featureSwitchSaving}
|
||||||
setFeatureConfig((prev) => ({ ...prev, matchEnabled: checked }))
|
onCheckedChange={(checked) => handleFeatureSwitch('matchEnabled', checked)}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
|
<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
|
<Switch
|
||||||
id="referral-enabled"
|
id="referral-enabled"
|
||||||
checked={featureConfig.referralEnabled}
|
checked={featureConfig.referralEnabled}
|
||||||
onCheckedChange={(checked) =>
|
disabled={featureSwitchSaving}
|
||||||
setFeatureConfig((prev) => ({ ...prev, referralEnabled: checked }))
|
onCheckedChange={(checked) => handleFeatureSwitch('referralEnabled', checked)}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
|
<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
|
<Switch
|
||||||
id="search-enabled"
|
id="search-enabled"
|
||||||
checked={featureConfig.searchEnabled}
|
checked={featureConfig.searchEnabled}
|
||||||
onCheckedChange={(checked) =>
|
disabled={featureSwitchSaving}
|
||||||
setFeatureConfig((prev) => ({ ...prev, searchEnabled: checked }))
|
onCheckedChange={(checked) => handleFeatureSwitch('searchEnabled', checked)}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
|
<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
|
<Switch
|
||||||
id="about-enabled"
|
id="about-enabled"
|
||||||
checked={featureConfig.aboutEnabled}
|
checked={featureConfig.aboutEnabled}
|
||||||
onCheckedChange={(checked) =>
|
disabled={featureSwitchSaving}
|
||||||
setFeatureConfig((prev) => ({ ...prev, aboutEnabled: checked }))
|
onCheckedChange={(checked) => handleFeatureSwitch('aboutEnabled', checked)}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -590,72 +544,31 @@ export function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
<CardHeader>
|
<DialogContent
|
||||||
<CardTitle className="text-white flex items-center gap-2">
|
className="bg-[#0f2137] border-gray-700 text-white"
|
||||||
<Smartphone className="w-5 h-5 text-[#38bdac]" />
|
showCloseButton={true}
|
||||||
小程序配置
|
>
|
||||||
</CardTitle>
|
<DialogHeader>
|
||||||
<CardDescription className="text-gray-400">微信小程序相关参数设置</CardDescription>
|
<DialogTitle className={dialogIsError ? 'text-red-400' : 'text-[#38bdac]'}>
|
||||||
</CardHeader>
|
{dialogTitle}
|
||||||
<CardContent className="space-y-4">
|
</DialogTitle>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<DialogDescription className="text-gray-400 whitespace-pre-wrap pt-2">
|
||||||
<div className="space-y-2">
|
{dialogMessage}
|
||||||
<Label className="text-gray-300">AppID</Label>
|
</DialogDescription>
|
||||||
<Input
|
</DialogHeader>
|
||||||
className="bg-[#0a1628] border-gray-700 text-white"
|
<DialogFooter className="mt-4">
|
||||||
value={mpConfig.appId}
|
<Button
|
||||||
onChange={(e) => setMpConfig((prev) => ({ ...prev, appId: e.target.value }))}
|
onClick={() => setDialogOpen(false)}
|
||||||
/>
|
className={dialogIsError ? 'bg-gray-600 hover:bg-gray-500' : 'bg-[#38bdac] hover:bg-[#2da396]'}
|
||||||
</div>
|
>
|
||||||
<div className="space-y-2">
|
确定
|
||||||
<Label className="text-gray-300">API域名</Label>
|
</Button>
|
||||||
<Input
|
</DialogFooter>
|
||||||
className="bg-[#0a1628] border-gray-700 text-white"
|
</DialogContent>
|
||||||
value={mpConfig.apiDomain}
|
</Dialog>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,167 @@ func DBConfigGet(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": data})
|
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
|
// DBConfigPost POST /api/db/config
|
||||||
func DBConfigPost(c *gin.Context) {
|
func DBConfigPost(c *gin.Context) {
|
||||||
var body struct {
|
var body struct {
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ func Setup(cfg *config.Config) *gin.Engine {
|
|||||||
admin.DELETE("/referral", handler.AdminReferral)
|
admin.DELETE("/referral", handler.AdminReferral)
|
||||||
admin.GET("/withdrawals", handler.AdminWithdrawalsList)
|
admin.GET("/withdrawals", handler.AdminWithdrawalsList)
|
||||||
admin.PUT("/withdrawals", handler.AdminWithdrawalsAction)
|
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