273 lines
11 KiB
TypeScript
273 lines
11 KiB
TypeScript
"use client"
|
||
|
||
import { useEffect, useState } 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 { Badge } from "@/components/ui/badge"
|
||
import { Save, Percent, Users, Wallet, Info } from "lucide-react"
|
||
|
||
type ReferralConfig = {
|
||
distributorShare: number
|
||
minWithdrawAmount: number
|
||
bindingDays: number
|
||
userDiscount: number
|
||
enableAutoWithdraw: boolean
|
||
}
|
||
|
||
const DEFAULT_REFERRAL_CONFIG: ReferralConfig = {
|
||
distributorShare: 90,
|
||
minWithdrawAmount: 10,
|
||
bindingDays: 30,
|
||
userDiscount: 5,
|
||
enableAutoWithdraw: false,
|
||
}
|
||
|
||
export default function ReferralSettingsPage() {
|
||
const [config, setConfig] = useState<ReferralConfig>(DEFAULT_REFERRAL_CONFIG)
|
||
const [loading, setLoading] = useState(true)
|
||
const [saving, setSaving] = useState(false)
|
||
|
||
useEffect(() => {
|
||
const loadConfig = async () => {
|
||
try {
|
||
const res = await fetch("/api/db/config?key=referral_config")
|
||
if (res.ok) {
|
||
const data = await res.json()
|
||
if (data?.success && data.config) {
|
||
setConfig({
|
||
distributorShare: data.config.distributorShare ?? 90,
|
||
minWithdrawAmount: data.config.minWithdrawAmount ?? 10,
|
||
bindingDays: data.config.bindingDays ?? 30,
|
||
userDiscount: data.config.userDiscount ?? 5,
|
||
enableAutoWithdraw: data.config.enableAutoWithdraw ?? false,
|
||
})
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error("加载 referral_config 失败:", e)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
loadConfig()
|
||
}, [])
|
||
|
||
const handleSave = async () => {
|
||
setSaving(true)
|
||
try {
|
||
// 确保所有字段都是正确类型(防止字符串导致计算错误)
|
||
const safeConfig = {
|
||
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",
|
||
config: safeConfig,
|
||
description: "分销 / 推广规则配置",
|
||
}
|
||
const res = await fetch("/api/db/config", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(body),
|
||
})
|
||
const data = await res.json()
|
||
if (!res.ok || !data?.success) {
|
||
alert("保存失败: " + (data?.error || res.statusText))
|
||
return
|
||
}
|
||
|
||
alert("✅ 分销配置已保存成功!\n\n• 小程序与网站的推广规则会一起生效\n• 绑定关系会使用新的天数配置\n• 佣金比例会立即应用到新订单\n\n如有缓存,请刷新前台/小程序页面。")
|
||
} catch (e: any) {
|
||
console.error("保存 referral_config 失败:", e)
|
||
alert("保存失败: " + (e?.message || String(e)))
|
||
} finally {
|
||
setSaving(false)
|
||
}
|
||
}
|
||
|
||
const handleNumberChange = (field: keyof ReferralConfig) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const value = parseFloat(e.target.value || "0")
|
||
setConfig((prev) => ({ ...prev, [field]: isNaN(value) ? 0 : value }))
|
||
}
|
||
|
||
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 flex items-center gap-2">
|
||
<Wallet className="w-5 h-5 text-[#38bdac]" />
|
||
推广 / 分销设置
|
||
</h2>
|
||
<p className="text-gray-400 mt-1">
|
||
统一管理「好友优惠」「你得 90% 收益」「绑定期 30 天」「提现门槛」等规则,小程序和 Web 共用这套配置。
|
||
</p>
|
||
</div>
|
||
<Button
|
||
onClick={handleSave}
|
||
disabled={saving || loading}
|
||
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||
>
|
||
<Save className="w-4 h-4 mr-2" />
|
||
{saving ? "保存中..." : "保存配置"}
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="space-y-6">
|
||
{/* 核心规则卡片 */}
|
||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2 text-white">
|
||
<Percent className="w-4 h-4 text-[#38bdac]" />
|
||
推广规则
|
||
</CardTitle>
|
||
<CardDescription className="text-gray-400">
|
||
这三项会直接体现在小程序「推广规则」卡片上,同时影响实收佣金计算。
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-6">
|
||
<div className="grid grid-cols-3 gap-6">
|
||
<div className="space-y-2">
|
||
<Label className="text-gray-300 flex items-center gap-2">
|
||
<Info className="w-3 h-3 text-[#38bdac]" />
|
||
好友优惠(%)
|
||
</Label>
|
||
<Input
|
||
type="number"
|
||
min={0}
|
||
max={100}
|
||
className="bg-[#0a1628] border-gray-700 text-white"
|
||
value={config.userDiscount}
|
||
onChange={handleNumberChange("userDiscount")}
|
||
/>
|
||
<p className="text-xs text-gray-500">例如 5 表示好友立减 5%(在价格配置基础上生效)。</p>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label className="text-gray-300 flex items-center gap-2">
|
||
<Users className="w-3 h-3 text-[#38bdac]" />
|
||
推广者分成(%)
|
||
</Label>
|
||
<div className="flex items-center gap-4">
|
||
<Slider
|
||
className="flex-1"
|
||
min={10}
|
||
max={100}
|
||
step={1}
|
||
value={[config.distributorShare]}
|
||
onValueChange={([val]) => setConfig((prev) => ({ ...prev, distributorShare: val }))}
|
||
/>
|
||
<Input
|
||
type="number"
|
||
min={0}
|
||
max={100}
|
||
className="w-20 bg-[#0a1628] border-gray-700 text-white text-center"
|
||
value={config.distributorShare}
|
||
onChange={handleNumberChange("distributorShare")}
|
||
/>
|
||
</div>
|
||
<p className="text-xs text-gray-500">
|
||
实际佣金 = 订单金额 × {" "}
|
||
<span className="text-[#38bdac] font-mono">{config.distributorShare}%</span>,支付回调和分销统计都会用这个值。
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label className="text-gray-300 flex items-center gap-2">
|
||
<Users className="w-3 h-3 text-[#38bdac]" />
|
||
绑定有效期(天)
|
||
</Label>
|
||
<Input
|
||
type="number"
|
||
min={1}
|
||
max={365}
|
||
className="bg-[#0a1628] border-gray-700 text-white"
|
||
value={config.bindingDays}
|
||
onChange={handleNumberChange("bindingDays")}
|
||
/>
|
||
<p className="text-xs text-gray-500">好友通过你的链接进来并登录后,绑定在你名下的天数。</p>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 提现与自动提现 */}
|
||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2 text-white">
|
||
<Wallet className="w-4 h-4 text-[#38bdac]" />
|
||
提现规则
|
||
</CardTitle>
|
||
<CardDescription className="text-gray-400">
|
||
与「提现中心」「自动提现」相关的参数,影响推广者看到的可提现金额和最低门槛。
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-6">
|
||
<div className="grid grid-cols-2 gap-6">
|
||
<div className="space-y-2">
|
||
<Label className="text-gray-300">最低提现金额(元)</Label>
|
||
<Input
|
||
type="number"
|
||
min={0}
|
||
step={1}
|
||
className="bg-[#0a1628] border-gray-700 text-white"
|
||
value={config.minWithdrawAmount}
|
||
onChange={handleNumberChange("minWithdrawAmount")}
|
||
/>
|
||
<p className="text-xs text-gray-500">小程序「满 X 元可提现」展示的门槛,同时用于后端接口校验。</p>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label className="text-gray-300 flex items-center gap-2">
|
||
自动提现开关
|
||
<Badge variant="outline" className="border-[#38bdac]/40 text-[#38bdac] text-[10px]">
|
||
预留
|
||
</Badge>
|
||
</Label>
|
||
<div className="flex items-center gap-3 mt-1">
|
||
<Switch
|
||
checked={config.enableAutoWithdraw}
|
||
onCheckedChange={(checked) => setConfig((prev) => ({ ...prev, enableAutoWithdraw: checked }))}
|
||
/>
|
||
<span className="text-sm text-gray-400">
|
||
开启后,可结合定时任务实现「收益自动打款到微信零钱」。
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 提示卡片 */}
|
||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2 text-gray-200 text-sm">
|
||
<Info className="w-4 h-4 text-[#38bdac]" />
|
||
使用说明
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-2 text-xs text-gray-400 leading-relaxed">
|
||
<p>
|
||
1. 以上配置会写入 <code className="font-mono text-[11px] text-[#38bdac]">system_config.referral_config</code>,小程序「推广中心」、
|
||
Web 推广页以及支付回调都会读取同一份配置。
|
||
</p>
|
||
<p>
|
||
2. 修改后新订单立即生效;旧订单的历史佣金不会自动重算,只影响之后产生的订单。
|
||
</p>
|
||
<p>
|
||
3. 如遇前端展示与实际结算不一致,优先以此处配置为准,再排查缓存和小程序版本。
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|