会员配置改为可视化表单管理
This commit is contained in:
parent
95630e735d
commit
86c6a8d169
@ -70,6 +70,8 @@ import { Switch } from "@/components/ui/switch";
|
|||||||
import {
|
import {
|
||||||
defaultMembershipConfig,
|
defaultMembershipConfig,
|
||||||
defaultMembershipUserOverrides,
|
defaultMembershipUserOverrides,
|
||||||
|
type MembershipFaqItem,
|
||||||
|
type MembershipPlanConfig,
|
||||||
normalizeMembershipConfig,
|
normalizeMembershipConfig,
|
||||||
normalizeMembershipUserOverrides,
|
normalizeMembershipUserOverrides,
|
||||||
} from "@/lib/membership/config";
|
} from "@/lib/membership/config";
|
||||||
@ -440,12 +442,9 @@ export function AdminSettingsClient() {
|
|||||||
membershipConfig: defaultMembershipConfig,
|
membershipConfig: defaultMembershipConfig,
|
||||||
membershipUserOverrides: defaultMembershipUserOverrides,
|
membershipUserOverrides: defaultMembershipUserOverrides,
|
||||||
});
|
});
|
||||||
const [membershipConfigDraft, setMembershipConfigDraft] = useState(
|
const [newOverrideUserId, setNewOverrideUserId] = useState("");
|
||||||
JSON.stringify(defaultMembershipConfig, null, 2)
|
const [newOverrideEmail, setNewOverrideEmail] = useState("");
|
||||||
);
|
const [newOverridePlanId, setNewOverridePlanId] = useState("");
|
||||||
const [membershipOverridesDraft, setMembershipOverridesDraft] = useState(
|
|
||||||
JSON.stringify(defaultMembershipUserOverrides, null, 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (siteSettingsData) {
|
if (siteSettingsData) {
|
||||||
@ -513,8 +512,6 @@ export function AdminSettingsClient() {
|
|||||||
membershipConfig: normalizedMembershipConfig,
|
membershipConfig: normalizedMembershipConfig,
|
||||||
membershipUserOverrides: normalizedMembershipOverrides,
|
membershipUserOverrides: normalizedMembershipOverrides,
|
||||||
});
|
});
|
||||||
setMembershipConfigDraft(JSON.stringify(normalizedMembershipConfig, null, 2));
|
|
||||||
setMembershipOverridesDraft(JSON.stringify(normalizedMembershipOverrides, null, 2));
|
|
||||||
}
|
}
|
||||||
}, [siteSettingsData]);
|
}, [siteSettingsData]);
|
||||||
|
|
||||||
@ -1037,26 +1034,10 @@ export function AdminSettingsClient() {
|
|||||||
// --- 网站设置逻辑 ---
|
// --- 网站设置逻辑 ---
|
||||||
const handleSaveSiteSettings = async () => {
|
const handleSaveSiteSettings = async () => {
|
||||||
try {
|
try {
|
||||||
let parsedMembershipConfig: ReturnType<typeof normalizeMembershipConfig>;
|
const parsedMembershipConfig = normalizeMembershipConfig(siteSettings.membershipConfig);
|
||||||
let parsedMembershipOverrides: ReturnType<typeof normalizeMembershipUserOverrides>;
|
const parsedMembershipOverrides = normalizeMembershipUserOverrides(
|
||||||
|
siteSettings.membershipUserOverrides
|
||||||
try {
|
);
|
||||||
parsedMembershipConfig = normalizeMembershipConfig(
|
|
||||||
JSON.parse(membershipConfigDraft)
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
toast.error("会员配置 JSON 格式错误,请检查后再保存");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
parsedMembershipOverrides = normalizeMembershipUserOverrides(
|
|
||||||
JSON.parse(membershipOverridesDraft)
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
toast.error("会员用户覆盖 JSON 格式错误,请检查后再保存");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedPointsPackages = normalizePointsPackages(siteSettings.pointsPackages);
|
const parsedPointsPackages = normalizePointsPackages(siteSettings.pointsPackages);
|
||||||
|
|
||||||
@ -1074,8 +1055,6 @@ export function AdminSettingsClient() {
|
|||||||
});
|
});
|
||||||
if (!response.ok) throw new Error("保存失败");
|
if (!response.ok) throw new Error("保存失败");
|
||||||
setSiteSettings(payload);
|
setSiteSettings(payload);
|
||||||
setMembershipConfigDraft(JSON.stringify(parsedMembershipConfig, null, 2));
|
|
||||||
setMembershipOverridesDraft(JSON.stringify(parsedMembershipOverrides, null, 2));
|
|
||||||
toast.success("网站设置保存成功");
|
toast.success("网站设置保存成功");
|
||||||
mutateSiteSettings();
|
mutateSiteSettings();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -1120,6 +1099,165 @@ export function AdminSettingsClient() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateMembershipConfigState = (
|
||||||
|
updater: (config: ReturnType<typeof normalizeMembershipConfig>) => ReturnType<typeof normalizeMembershipConfig>
|
||||||
|
) => {
|
||||||
|
setSiteSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
membershipConfig: updater(prev.membershipConfig),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMembershipOverridesState = (
|
||||||
|
updater: (
|
||||||
|
overrides: ReturnType<typeof normalizeMembershipUserOverrides>
|
||||||
|
) => ReturnType<typeof normalizeMembershipUserOverrides>
|
||||||
|
) => {
|
||||||
|
setSiteSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
membershipUserOverrides: updater(prev.membershipUserOverrides),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMembershipPlan = (
|
||||||
|
index: number,
|
||||||
|
updater: (plan: MembershipPlanConfig) => MembershipPlanConfig
|
||||||
|
) => {
|
||||||
|
updateMembershipConfigState((config) => {
|
||||||
|
const plans = [...config.plans];
|
||||||
|
if (!plans[index]) return config;
|
||||||
|
plans[index] = updater(plans[index]);
|
||||||
|
return { ...config, plans };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMembershipPlan = () => {
|
||||||
|
updateMembershipConfigState((config) => {
|
||||||
|
const nextIndex = config.plans.length + 1;
|
||||||
|
const id = `plan-${Date.now()}`;
|
||||||
|
const plans = [
|
||||||
|
...config.plans,
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
name: `套餐${nextIndex}`,
|
||||||
|
monthlyPrice: 0,
|
||||||
|
annualPrice: 0,
|
||||||
|
monthlyCredits: 0,
|
||||||
|
dailyFreeCredits: 0,
|
||||||
|
features: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
plans,
|
||||||
|
defaultPlanId: config.defaultPlanId || id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeMembershipPlan = (index: number) => {
|
||||||
|
updateMembershipConfigState((config) => {
|
||||||
|
if (config.plans.length <= 1) {
|
||||||
|
toast.error("至少保留一个会员套餐");
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = config.plans[index];
|
||||||
|
if (!target) return config;
|
||||||
|
|
||||||
|
const plans = [...config.plans];
|
||||||
|
plans.splice(index, 1);
|
||||||
|
const nextDefaultPlanId =
|
||||||
|
config.defaultPlanId === target.id ? plans[0]?.id || config.defaultPlanId : config.defaultPlanId;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
plans,
|
||||||
|
defaultPlanId: nextDefaultPlanId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMembershipFaq = (
|
||||||
|
index: number,
|
||||||
|
updater: (faq: MembershipFaqItem) => MembershipFaqItem
|
||||||
|
) => {
|
||||||
|
updateMembershipConfigState((config) => {
|
||||||
|
const faqs = [...config.faqs];
|
||||||
|
if (!faqs[index]) return config;
|
||||||
|
faqs[index] = updater(faqs[index]);
|
||||||
|
return { ...config, faqs };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMembershipFaq = () => {
|
||||||
|
updateMembershipConfigState((config) => ({
|
||||||
|
...config,
|
||||||
|
faqs: [...config.faqs, { question: "", answer: "" }],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeMembershipFaq = (index: number) => {
|
||||||
|
updateMembershipConfigState((config) => {
|
||||||
|
if (config.faqs.length <= 1) {
|
||||||
|
toast.error("至少保留一条 FAQ");
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
const faqs = [...config.faqs];
|
||||||
|
faqs.splice(index, 1);
|
||||||
|
return { ...config, faqs };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMembershipOverride = (kind: "id" | "email") => {
|
||||||
|
const key = kind === "id" ? newOverrideUserId.trim() : newOverrideEmail.trim().toLowerCase();
|
||||||
|
const planId = newOverridePlanId.trim();
|
||||||
|
|
||||||
|
if (!key || !planId) {
|
||||||
|
toast.error("请先填写标识并选择套餐");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMembershipOverridesState((overrides) => {
|
||||||
|
if (kind === "id") {
|
||||||
|
return {
|
||||||
|
...overrides,
|
||||||
|
userPlansById: {
|
||||||
|
...overrides.userPlansById,
|
||||||
|
[key]: planId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...overrides,
|
||||||
|
userPlansByEmail: {
|
||||||
|
...overrides.userPlansByEmail,
|
||||||
|
[key]: planId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (kind === "id") {
|
||||||
|
setNewOverrideUserId("");
|
||||||
|
} else {
|
||||||
|
setNewOverrideEmail("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeMembershipOverride = (kind: "id" | "email", key: string) => {
|
||||||
|
updateMembershipOverridesState((overrides) => {
|
||||||
|
if (kind === "id") {
|
||||||
|
const next = { ...overrides.userPlansById };
|
||||||
|
delete next[key];
|
||||||
|
return { ...overrides, userPlansById: next };
|
||||||
|
}
|
||||||
|
const next = { ...overrides.userPlansByEmail };
|
||||||
|
delete next[key];
|
||||||
|
return { ...overrides, userPlansByEmail: next };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// --- 抠图API配置逻辑 ---
|
// --- 抠图API配置逻辑 ---
|
||||||
const handleSaveRmbgConfig = async () => {
|
const handleSaveRmbgConfig = async () => {
|
||||||
try {
|
try {
|
||||||
@ -2949,37 +3087,387 @@ export function AdminSettingsClient() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMembershipConfigDraft(JSON.stringify(defaultMembershipConfig, null, 2));
|
setSiteSettings({
|
||||||
setMembershipOverridesDraft(
|
...siteSettings,
|
||||||
JSON.stringify(defaultMembershipUserOverrides, null, 2)
|
membershipConfig: defaultMembershipConfig,
|
||||||
);
|
membershipUserOverrides: defaultMembershipUserOverrides,
|
||||||
|
});
|
||||||
toast.success("已恢复会员配置默认模板");
|
toast.success("已恢复会员配置默认模板");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
恢复默认模板
|
恢复默认模板
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-4">
|
||||||
<Label htmlFor="membershipConfigDraft">会员配置 JSON</Label>
|
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||||
<Textarea
|
<div className="space-y-0.5">
|
||||||
id="membershipConfigDraft"
|
<Label htmlFor="membershipEnabled">启用会员系统</Label>
|
||||||
value={membershipConfigDraft}
|
<div className="text-xs text-muted-foreground">控制升级弹窗及会员规则是否生效</div>
|
||||||
onChange={(e) => setMembershipConfigDraft(e.target.value)}
|
</div>
|
||||||
rows={12}
|
<Switch
|
||||||
className="font-mono text-xs"
|
id="membershipEnabled"
|
||||||
/>
|
checked={siteSettings.membershipConfig.enabled !== false}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
updateMembershipConfigState((config) => ({
|
||||||
|
...config,
|
||||||
|
enabled: checked,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-muted-foreground">货币符号</Label>
|
||||||
|
<Input
|
||||||
|
value={siteSettings.membershipConfig.currency}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateMembershipConfigState((config) => ({
|
||||||
|
...config,
|
||||||
|
currency: e.target.value || "$",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="$"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-muted-foreground">年付折扣标签</Label>
|
||||||
|
<Input
|
||||||
|
value={siteSettings.membershipConfig.annualDiscountLabel}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateMembershipConfigState((config) => ({
|
||||||
|
...config,
|
||||||
|
annualDiscountLabel: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="限时 5 折优惠"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-muted-foreground">默认套餐</Label>
|
||||||
|
<Select
|
||||||
|
value={siteSettings.membershipConfig.defaultPlanId}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateMembershipConfigState((config) => ({
|
||||||
|
...config,
|
||||||
|
defaultPlanId: value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="选择默认套餐" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{siteSettings.membershipConfig.plans.map((plan) => (
|
||||||
|
<SelectItem key={plan.id} value={plan.id}>
|
||||||
|
{plan.name || plan.id}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="membershipOverridesDraft">用户套餐覆盖 JSON</Label>
|
<div className="flex items-center justify-between">
|
||||||
<Textarea
|
<Label>会员套餐</Label>
|
||||||
id="membershipOverridesDraft"
|
<Button type="button" variant="outline" size="sm" onClick={addMembershipPlan}>
|
||||||
value={membershipOverridesDraft}
|
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
||||||
onChange={(e) => setMembershipOverridesDraft(e.target.value)}
|
新增套餐
|
||||||
rows={7}
|
</Button>
|
||||||
className="font-mono text-xs"
|
</div>
|
||||||
/>
|
<div className="space-y-3">
|
||||||
<div className="text-xs text-muted-foreground">
|
{siteSettings.membershipConfig.plans.map((plan, index) => (
|
||||||
可用字段:`userPlansById`(用户ID到planId)和 `userPlansByEmail`(邮箱到planId)。
|
<div key={`${plan.id}-${index}`} className="rounded-lg border p-3 space-y-3">
|
||||||
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-7">
|
||||||
|
<div className="space-y-1 md:col-span-2">
|
||||||
|
<Label className="text-xs text-muted-foreground">套餐名称</Label>
|
||||||
|
<Input
|
||||||
|
value={plan.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateMembershipPlan(index, (item) => ({
|
||||||
|
...item,
|
||||||
|
name: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-muted-foreground">月付</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
step="0.01"
|
||||||
|
value={plan.monthlyPrice}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateMembershipPlan(index, (item) => ({
|
||||||
|
...item,
|
||||||
|
monthlyPrice: Math.max(0, Number.parseFloat(e.target.value || "0")),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-muted-foreground">年付</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
step="0.01"
|
||||||
|
value={plan.annualPrice}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateMembershipPlan(index, (item) => ({
|
||||||
|
...item,
|
||||||
|
annualPrice: Math.max(0, Number.parseFloat(e.target.value || "0")),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-muted-foreground">月积分</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={plan.monthlyCredits}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateMembershipPlan(index, (item) => ({
|
||||||
|
...item,
|
||||||
|
monthlyCredits: Math.max(0, Number.parseInt(e.target.value || "0", 10)),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-muted-foreground">每日免费积分</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={plan.dailyFreeCredits}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateMembershipPlan(index, (item) => ({
|
||||||
|
...item,
|
||||||
|
dailyFreeCredits: Math.max(0, Number.parseInt(e.target.value || "0", 10)),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end justify-end">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => removeMembershipPlan(index)}
|
||||||
|
title="删除套餐"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-muted-foreground">套餐ID</Label>
|
||||||
|
<Input
|
||||||
|
value={plan.id}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateMembershipPlan(index, (item) => ({
|
||||||
|
...item,
|
||||||
|
id: e.target.value.replace(/\s+/g, "-"),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-muted-foreground">徽章文案</Label>
|
||||||
|
<Input
|
||||||
|
value={plan.badge || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateMembershipPlan(index, (item) => ({
|
||||||
|
...item,
|
||||||
|
badge: e.target.value || undefined,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="如:热门"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end">
|
||||||
|
<div className="flex items-center gap-2 rounded-md border px-2 py-1.5">
|
||||||
|
<Switch
|
||||||
|
checked={plan.highlighted === true}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
updateMembershipPlan(index, (item) => ({
|
||||||
|
...item,
|
||||||
|
highlighted: checked,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground">高亮展示</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-muted-foreground">权益(每行一条)</Label>
|
||||||
|
<Textarea
|
||||||
|
value={(plan.features || []).join("\n")}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateMembershipPlan(index, (item) => ({
|
||||||
|
...item,
|
||||||
|
features: e.target.value
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
rows={3}
|
||||||
|
placeholder="访问所有图片模型 优先队列"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>FAQ</Label>
|
||||||
|
<Button type="button" variant="outline" size="sm" onClick={addMembershipFaq}>
|
||||||
|
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
||||||
|
新增 FAQ
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{siteSettings.membershipConfig.faqs.map((faq, index) => (
|
||||||
|
<div key={`faq-${index}`} className="rounded-lg border p-3 space-y-2">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<Input
|
||||||
|
value={faq.question}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateMembershipFaq(index, (item) => ({
|
||||||
|
...item,
|
||||||
|
question: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="问题"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => removeMembershipFaq(index)}
|
||||||
|
title="删除 FAQ"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Textarea
|
||||||
|
value={faq.answer}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateMembershipFaq(index, (item) => ({
|
||||||
|
...item,
|
||||||
|
answer: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
rows={2}
|
||||||
|
placeholder="回答"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>用户套餐覆盖</Label>
|
||||||
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
|
<div className="rounded-lg border p-3 space-y-2">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground">按用户ID覆盖</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
value={newOverrideUserId}
|
||||||
|
onChange={(e) => setNewOverrideUserId(e.target.value)}
|
||||||
|
placeholder="用户ID"
|
||||||
|
/>
|
||||||
|
<Select value={newOverridePlanId} onValueChange={setNewOverridePlanId}>
|
||||||
|
<SelectTrigger className="w-[140px]">
|
||||||
|
<SelectValue placeholder="套餐" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{siteSettings.membershipConfig.plans.map((plan) => (
|
||||||
|
<SelectItem key={`id-plan-${plan.id}`} value={plan.id}>
|
||||||
|
{plan.name || plan.id}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button type="button" onClick={() => addMembershipOverride("id")}>
|
||||||
|
添加
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{Object.entries(siteSettings.membershipUserOverrides.userPlansById || {}).map(
|
||||||
|
([key, planId]) => (
|
||||||
|
<div key={`id-${key}`} className="flex items-center justify-between rounded-md border px-2 py-1.5 text-xs">
|
||||||
|
<span className="truncate pr-2">{key}</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-muted-foreground">{planId}</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => removeMembershipOverride("id", key)}
|
||||||
|
className="h-6 w-6"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border p-3 space-y-2">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground">按邮箱覆盖</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
value={newOverrideEmail}
|
||||||
|
onChange={(e) => setNewOverrideEmail(e.target.value)}
|
||||||
|
placeholder="用户邮箱"
|
||||||
|
/>
|
||||||
|
<Select value={newOverridePlanId} onValueChange={setNewOverridePlanId}>
|
||||||
|
<SelectTrigger className="w-[140px]">
|
||||||
|
<SelectValue placeholder="套餐" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{siteSettings.membershipConfig.plans.map((plan) => (
|
||||||
|
<SelectItem key={`email-plan-${plan.id}`} value={plan.id}>
|
||||||
|
{plan.name || plan.id}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button type="button" onClick={() => addMembershipOverride("email")}>
|
||||||
|
添加
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{Object.entries(siteSettings.membershipUserOverrides.userPlansByEmail || {}).map(
|
||||||
|
([key, planId]) => (
|
||||||
|
<div key={`email-${key}`} className="flex items-center justify-between rounded-md border px-2 py-1.5 text-xs">
|
||||||
|
<span className="truncate pr-2">{key}</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-muted-foreground">{planId}</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => removeMembershipOverride("email", key)}
|
||||||
|
className="h-6 w-6"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user