积分套餐改为可视化表单配置

This commit is contained in:
Your Name 2026-02-14 14:35:50 +08:00
parent 7555a6d7f6
commit 95630e735d

View File

@ -446,9 +446,6 @@ export function AdminSettingsClient() {
const [membershipOverridesDraft, setMembershipOverridesDraft] = useState(
JSON.stringify(defaultMembershipUserOverrides, null, 2)
);
const [pointsPackagesDraft, setPointsPackagesDraft] = useState(
JSON.stringify(defaultPointsPackages, null, 2)
);
useEffect(() => {
if (siteSettingsData) {
@ -518,7 +515,6 @@ export function AdminSettingsClient() {
});
setMembershipConfigDraft(JSON.stringify(normalizedMembershipConfig, null, 2));
setMembershipOverridesDraft(JSON.stringify(normalizedMembershipOverrides, null, 2));
setPointsPackagesDraft(JSON.stringify(normalizedPointsPackages, null, 2));
}
}, [siteSettingsData]);
@ -1043,7 +1039,6 @@ export function AdminSettingsClient() {
try {
let parsedMembershipConfig: ReturnType<typeof normalizeMembershipConfig>;
let parsedMembershipOverrides: ReturnType<typeof normalizeMembershipUserOverrides>;
let parsedPointsPackages: ReturnType<typeof normalizePointsPackages>;
try {
parsedMembershipConfig = normalizeMembershipConfig(
@ -1063,12 +1058,7 @@ export function AdminSettingsClient() {
return;
}
try {
parsedPointsPackages = normalizePointsPackages(JSON.parse(pointsPackagesDraft));
} catch {
toast.error("积分套餐 JSON 格式错误,请检查后再保存");
return;
}
const parsedPointsPackages = normalizePointsPackages(siteSettings.pointsPackages);
const payload = {
...siteSettings,
@ -1086,7 +1076,6 @@ export function AdminSettingsClient() {
setSiteSettings(payload);
setMembershipConfigDraft(JSON.stringify(parsedMembershipConfig, null, 2));
setMembershipOverridesDraft(JSON.stringify(parsedMembershipOverrides, null, 2));
setPointsPackagesDraft(JSON.stringify(parsedPointsPackages, null, 2));
toast.success("网站设置保存成功");
mutateSiteSettings();
} catch (error) {
@ -1094,6 +1083,43 @@ export function AdminSettingsClient() {
}
};
const updatePointsPackage = (index: number, updater: (item: PointsPackage) => PointsPackage) => {
setSiteSettings((prev) => {
const list = [...(prev.pointsPackages || [])];
if (!list[index]) return prev;
list[index] = updater(list[index]);
return { ...prev, pointsPackages: list };
});
};
const addPointsPackage = () => {
setSiteSettings((prev) => {
const list = [...(prev.pointsPackages || [])];
const nextIndex = list.length + 1;
list.push({
id: `package-${Date.now()}`,
name: `套餐${nextIndex}`,
points: 1000,
price: 9.9,
currency: "CNY",
isEnabled: true,
});
return { ...prev, pointsPackages: list };
});
};
const removePointsPackage = (index: number) => {
setSiteSettings((prev) => {
const list = [...(prev.pointsPackages || [])];
if (list.length <= 1) {
toast.error("至少保留一个积分套餐");
return prev;
}
list.splice(index, 1);
return { ...prev, pointsPackages: list };
});
};
// --- 抠图API配置逻辑 ---
const handleSaveRmbgConfig = async () => {
try {
@ -2973,8 +2999,8 @@ export function AdminSettingsClient() {
setSiteSettings({
...siteSettings,
pointsSystemConfig: defaultPointsSystemConfig,
pointsPackages: defaultPointsPackages,
});
setPointsPackagesDraft(JSON.stringify(defaultPointsPackages, null, 2));
toast.success("已恢复积分配置默认模板");
}}
>
@ -3026,16 +3052,111 @@ export function AdminSettingsClient() {
</div>
</div>
<div className="space-y-2">
<Label htmlFor="pointsPackagesDraft">积分套餐 JSON</Label>
<Textarea
id="pointsPackagesDraft"
value={pointsPackagesDraft}
onChange={(e) => setPointsPackagesDraft(e.target.value)}
rows={8}
className="font-mono text-xs"
/>
<div className="text-xs text-muted-foreground">
字段示例:`id`、`name`、`points`、`price`、`currency`、`isEnabled`。
<div className="flex items-center justify-between">
<Label>积分套餐</Label>
<Button type="button" variant="outline" size="sm" onClick={addPointsPackage}>
<Plus className="mr-1.5 h-3.5 w-3.5" />
新增套餐
</Button>
</div>
<div className="space-y-3">
{(siteSettings.pointsPackages || []).map((pkg, index) => (
<div key={`${pkg.id}-${index}`} className="rounded-lg border p-3 space-y-3">
<div className="grid grid-cols-1 gap-3 md:grid-cols-6">
<div className="space-y-1 md:col-span-2">
<Label className="text-xs text-muted-foreground">套餐名称</Label>
<Input
value={pkg.name}
onChange={(e) =>
updatePointsPackage(index, (item) => ({
...item,
name: e.target.value,
}))
}
placeholder="如:入门包"
/>
</div>
<div className="space-y-1">
<Label className="text-xs text-muted-foreground">积分数量</Label>
<Input
type="number"
min={1}
value={pkg.points}
onChange={(e) =>
updatePointsPackage(index, (item) => ({
...item,
points: Math.max(1, Number.parseInt(e.target.value || "1", 10)),
}))
}
/>
</div>
<div className="space-y-1">
<Label className="text-xs text-muted-foreground">售价</Label>
<Input
type="number"
min={0}
step="0.01"
value={pkg.price}
onChange={(e) =>
updatePointsPackage(index, (item) => ({
...item,
price: Math.max(0, Number.parseFloat(e.target.value || "0")),
}))
}
/>
</div>
<div className="space-y-1">
<Label className="text-xs text-muted-foreground">货币</Label>
<Input
value={pkg.currency}
onChange={(e) =>
updatePointsPackage(index, (item) => ({
...item,
currency: String(e.target.value || "CNY").toUpperCase(),
}))
}
placeholder="CNY"
/>
</div>
<div className="flex items-end justify-end gap-2">
<div className="flex items-center gap-2 rounded-md border px-2 py-1.5">
<Switch
checked={pkg.isEnabled !== false}
onCheckedChange={(checked) =>
updatePointsPackage(index, (item) => ({
...item,
isEnabled: checked,
}))
}
/>
<span className="text-xs text-muted-foreground">启用</span>
</div>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => removePointsPackage(index)}
title="删除套餐"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
<div className="space-y-1">
<Label className="text-xs text-muted-foreground">套餐ID(系统内部标识)</Label>
<Input
value={pkg.id}
onChange={(e) =>
updatePointsPackage(index, (item) => ({
...item,
id: e.target.value.replace(/\s+/g, "-"),
}))
}
placeholder="starter-1000"
/>
</div>
</div>
))}
</div>
</div>
</div>