会员配置改为可视化表单管理
This commit is contained in:
parent
95630e735d
commit
86c6a8d169
@ -70,6 +70,8 @@ import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
defaultMembershipConfig,
|
||||
defaultMembershipUserOverrides,
|
||||
type MembershipFaqItem,
|
||||
type MembershipPlanConfig,
|
||||
normalizeMembershipConfig,
|
||||
normalizeMembershipUserOverrides,
|
||||
} from "@/lib/membership/config";
|
||||
@ -440,12 +442,9 @@ export function AdminSettingsClient() {
|
||||
membershipConfig: defaultMembershipConfig,
|
||||
membershipUserOverrides: defaultMembershipUserOverrides,
|
||||
});
|
||||
const [membershipConfigDraft, setMembershipConfigDraft] = useState(
|
||||
JSON.stringify(defaultMembershipConfig, null, 2)
|
||||
);
|
||||
const [membershipOverridesDraft, setMembershipOverridesDraft] = useState(
|
||||
JSON.stringify(defaultMembershipUserOverrides, null, 2)
|
||||
);
|
||||
const [newOverrideUserId, setNewOverrideUserId] = useState("");
|
||||
const [newOverrideEmail, setNewOverrideEmail] = useState("");
|
||||
const [newOverridePlanId, setNewOverridePlanId] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (siteSettingsData) {
|
||||
@ -513,8 +512,6 @@ export function AdminSettingsClient() {
|
||||
membershipConfig: normalizedMembershipConfig,
|
||||
membershipUserOverrides: normalizedMembershipOverrides,
|
||||
});
|
||||
setMembershipConfigDraft(JSON.stringify(normalizedMembershipConfig, null, 2));
|
||||
setMembershipOverridesDraft(JSON.stringify(normalizedMembershipOverrides, null, 2));
|
||||
}
|
||||
}, [siteSettingsData]);
|
||||
|
||||
@ -1037,26 +1034,10 @@ export function AdminSettingsClient() {
|
||||
// --- 网站设置逻辑 ---
|
||||
const handleSaveSiteSettings = async () => {
|
||||
try {
|
||||
let parsedMembershipConfig: ReturnType<typeof normalizeMembershipConfig>;
|
||||
let parsedMembershipOverrides: ReturnType<typeof normalizeMembershipUserOverrides>;
|
||||
|
||||
try {
|
||||
parsedMembershipConfig = normalizeMembershipConfig(
|
||||
JSON.parse(membershipConfigDraft)
|
||||
);
|
||||
} catch {
|
||||
toast.error("会员配置 JSON 格式错误,请检查后再保存");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
parsedMembershipOverrides = normalizeMembershipUserOverrides(
|
||||
JSON.parse(membershipOverridesDraft)
|
||||
);
|
||||
} catch {
|
||||
toast.error("会员用户覆盖 JSON 格式错误,请检查后再保存");
|
||||
return;
|
||||
}
|
||||
const parsedMembershipConfig = normalizeMembershipConfig(siteSettings.membershipConfig);
|
||||
const parsedMembershipOverrides = normalizeMembershipUserOverrides(
|
||||
siteSettings.membershipUserOverrides
|
||||
);
|
||||
|
||||
const parsedPointsPackages = normalizePointsPackages(siteSettings.pointsPackages);
|
||||
|
||||
@ -1074,8 +1055,6 @@ export function AdminSettingsClient() {
|
||||
});
|
||||
if (!response.ok) throw new Error("保存失败");
|
||||
setSiteSettings(payload);
|
||||
setMembershipConfigDraft(JSON.stringify(parsedMembershipConfig, null, 2));
|
||||
setMembershipOverridesDraft(JSON.stringify(parsedMembershipOverrides, null, 2));
|
||||
toast.success("网站设置保存成功");
|
||||
mutateSiteSettings();
|
||||
} 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配置逻辑 ---
|
||||
const handleSaveRmbgConfig = async () => {
|
||||
try {
|
||||
@ -2949,37 +3087,387 @@ export function AdminSettingsClient() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setMembershipConfigDraft(JSON.stringify(defaultMembershipConfig, null, 2));
|
||||
setMembershipOverridesDraft(
|
||||
JSON.stringify(defaultMembershipUserOverrides, null, 2)
|
||||
);
|
||||
setSiteSettings({
|
||||
...siteSettings,
|
||||
membershipConfig: defaultMembershipConfig,
|
||||
membershipUserOverrides: defaultMembershipUserOverrides,
|
||||
});
|
||||
toast.success("已恢复会员配置默认模板");
|
||||
}}
|
||||
>
|
||||
恢复默认模板
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="membershipConfigDraft">会员配置 JSON</Label>
|
||||
<Textarea
|
||||
id="membershipConfigDraft"
|
||||
value={membershipConfigDraft}
|
||||
onChange={(e) => setMembershipConfigDraft(e.target.value)}
|
||||
rows={12}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-4">
|
||||
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="membershipEnabled">启用会员系统</Label>
|
||||
<div className="text-xs text-muted-foreground">控制升级弹窗及会员规则是否生效</div>
|
||||
</div>
|
||||
<Switch
|
||||
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 className="space-y-2">
|
||||
<Label htmlFor="membershipOverridesDraft">用户套餐覆盖 JSON</Label>
|
||||
<Textarea
|
||||
id="membershipOverridesDraft"
|
||||
value={membershipOverridesDraft}
|
||||
onChange={(e) => setMembershipOverridesDraft(e.target.value)}
|
||||
rows={7}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
可用字段:`userPlansById`(用户ID到planId)和 `userPlansByEmail`(邮箱到planId)。
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>会员套餐</Label>
|
||||
<Button type="button" variant="outline" size="sm" onClick={addMembershipPlan}>
|
||||
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
||||
新增套餐
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{siteSettings.membershipConfig.plans.map((plan, index) => (
|
||||
<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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user