From 901219f39ca8bac64d647a4e6bb0828d400e1a44 Mon Sep 17 00:00:00 2001 From: JetSprow Date: Thu, 30 Apr 2026 16:04:59 +1000 Subject: [PATCH] feat: unify boolean controls as buttons --- .../admin/announcements/announcement-form.tsx | 53 +++--- src/app/(admin)/admin/commerce/page.tsx | 13 +- .../(admin)/admin/payments/config-form.tsx | 14 +- .../admin/plans/plan-policy-section.tsx | 22 ++- .../(admin)/admin/settings/settings-form.tsx | 154 ++++++++---------- src/components/ui/boolean-toggle.tsx | 77 +++++++++ src/components/ui/switch.tsx | 32 ---- 7 files changed, 209 insertions(+), 156 deletions(-) create mode 100644 src/components/ui/boolean-toggle.tsx delete mode 100644 src/components/ui/switch.tsx diff --git a/src/app/(admin)/admin/announcements/announcement-form.tsx b/src/app/(admin)/admin/announcements/announcement-form.tsx index 5744ad2..016ce3a 100644 --- a/src/app/(admin)/admin/announcements/announcement-form.tsx +++ b/src/app/(admin)/admin/announcements/announcement-form.tsx @@ -11,6 +11,7 @@ import { updateAnnouncement, } from "@/actions/admin/announcements"; import { PendingSubmitButton } from "@/components/shared/pending-submit-button"; +import { BooleanToggle } from "@/components/ui/boolean-toggle"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -136,15 +137,14 @@ export function AnnouncementForm({
- + defaultValue={announcement.dismissible} + trueLabel="允许" + falseLabel="不允许" + ariaLabel="允许用户关闭" + />
@@ -200,15 +200,14 @@ export function AnnouncementForm({
- + defaultValue={announcement.sendNotification} + trueLabel="发送" + falseLabel="不发送" + ariaLabel="同步发送站内通知" + />
@@ -285,15 +284,14 @@ export function CreateAnnouncementButton({
- + defaultValue + trueLabel="允许" + falseLabel="不允许" + ariaLabel="允许用户关闭" + />
@@ -333,15 +331,14 @@ export function CreateAnnouncementButton({
- + defaultValue + trueLabel="发送" + falseLabel="不发送" + ariaLabel="同步发送站内通知" + />
diff --git a/src/app/(admin)/admin/commerce/page.tsx b/src/app/(admin)/admin/commerce/page.tsx index 64ba487..98d7a96 100644 --- a/src/app/(admin)/admin/commerce/page.tsx +++ b/src/app/(admin)/admin/commerce/page.tsx @@ -5,6 +5,7 @@ import { DetailItem, DetailList } from "@/components/admin/detail-list"; import { ActiveStatusBadge, StatusBadge } from "@/components/admin/status-badge"; import { PageHeader, PageShell, SectionHeader } from "@/components/shared/page-shell"; import { PendingSubmitButton } from "@/components/shared/pending-submit-button"; +import { BooleanToggle } from "@/components/ui/boolean-toggle"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -68,10 +69,14 @@ export default async function AdminCommercePage() {
- +
创建优惠券 diff --git a/src/app/(admin)/admin/payments/config-form.tsx b/src/app/(admin)/admin/payments/config-form.tsx index 6adfef1..8e598c1 100644 --- a/src/app/(admin)/admin/payments/config-form.tsx +++ b/src/app/(admin)/admin/payments/config-form.tsx @@ -1,10 +1,10 @@ "use client"; import { useState } from "react"; +import { BooleanToggle } from "@/components/ui/boolean-toggle"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; import { savePaymentConfig } from "@/actions/admin/payments"; import { getErrorMessage } from "@/lib/errors"; import { toast } from "sonner"; @@ -121,9 +121,15 @@ export function PaymentConfigForm({ )}
-
- - {enabled ? "已启用" : "未启用"} +
+ +
- +
+ +
{type === "PROXY" && (
@@ -60,7 +68,15 @@ export function PlanPolicySection({

开放增流量

用户可拖动选择加多少 GB

- +
+ +
)} diff --git a/src/app/(admin)/admin/settings/settings-form.tsx b/src/app/(admin)/admin/settings/settings-form.tsx index d7dbc0d..d777b0d 100644 --- a/src/app/(admin)/admin/settings/settings-form.tsx +++ b/src/app/(admin)/admin/settings/settings-form.tsx @@ -3,6 +3,7 @@ import { useState, type FormEvent } from "react"; import { useRouter } from "next/navigation"; import { Bell, ChevronDown, Clock3, Gift, LifeBuoy, Mail, RadioTower, Send, Settings2, ShieldAlert, ShieldCheck } from "lucide-react"; +import { BooleanToggle } from "@/components/ui/boolean-toggle"; import { Button, buttonVariants } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -205,15 +206,12 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
- + defaultValue={config.autoReminderDispatchEnabled} + ariaLabel="自动提醒派发" + />
@@ -221,15 +219,12 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
- + defaultValue={config.trafficSyncEnabled} + ariaLabel="3x-ui 流量定时同步" + />
@@ -254,30 +249,24 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
- + defaultValue={config.networkRecommendationsEnabled} + ariaLabel="三网推荐" + />

开启后,商城展示电信、联通、移动当前最低延迟推荐;点击推荐会直接打开对应套餐详情。

- + defaultValue={config.networkInsightsEnabled} + ariaLabel="线路体验" + />

开启后,套餐详情展示节点延迟、趋势和访问路径;关闭后只保留购买所需的线路入口选择。

@@ -313,27 +302,23 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
- + defaultValue={config.subscriptionRiskEnabled} + ariaLabel="风控总控" + />
- + defaultValue={config.subscriptionRiskAutoSuspend} + trueLabel="开启自动封停" + falseLabel="只记录警告" + ariaLabel="自动暂停" + />
@@ -436,15 +421,14 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
- + defaultValue={config.nodeAccessRiskEnabled} + trueLabel="接收日志" + falseLabel="仅订阅风控" + ariaLabel="节点日志风控" + />
@@ -477,39 +461,36 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
- + defaultValue={config.allowRegistration} + trueLabel="开放" + falseLabel="关闭" + ariaLabel="开放注册" + />
- + defaultValue={config.requireInviteCode} + trueLabel="必须" + falseLabel="不需要" + ariaLabel="注册必须邀请码" + />
- + defaultValue={config.emailVerificationRequired} + trueLabel="开启验证" + falseLabel="关闭" + ariaLabel="注册邮箱验证" + />

开启后,新用户注册会先收到验证邮件,完成验证后才能登录;关闭后注册成功即可登录。

@@ -525,10 +506,12 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
- +
@@ -540,10 +523,14 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
- +
@@ -581,15 +568,12 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
- + defaultValue={config.inviteRewardEnabled} + ariaLabel="自动发放奖励" + />
diff --git a/src/components/ui/boolean-toggle.tsx b/src/components/ui/boolean-toggle.tsx new file mode 100644 index 0000000..369b2b8 --- /dev/null +++ b/src/components/ui/boolean-toggle.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { useId, useState } from "react"; +import { cn } from "@/lib/utils"; + +interface BooleanToggleProps { + id?: string; + name?: string; + value?: boolean; + defaultValue?: boolean; + onChange?: (value: boolean) => void; + trueLabel?: string; + falseLabel?: string; + ariaLabel?: string; + className?: string; + disabled?: boolean; +} + +export function BooleanToggle({ + id, + name, + value, + defaultValue = false, + onChange, + trueLabel = "开启", + falseLabel = "关闭", + ariaLabel, + className, + disabled = false, +}: BooleanToggleProps) { + const generatedId = useId(); + const inputId = id ?? generatedId; + const controlled = value != null; + const [internalValue, setInternalValue] = useState(defaultValue); + const currentValue = controlled ? value : internalValue; + + function select(nextValue: boolean) { + if (disabled) return; + if (!controlled) setInternalValue(nextValue); + onChange?.(nextValue); + } + + return ( +
+ {name && } +
+ {[ + { value: true, label: trueLabel }, + { value: false, label: falseLabel }, + ].map((option) => { + const active = currentValue === option.value; + return ( + + ); + })} +
+
+ ); +} diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx deleted file mode 100644 index 9b8b44b..0000000 --- a/src/components/ui/switch.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client" - -import { Switch as SwitchPrimitive } from "@base-ui/react/switch" - -import { cn } from "@/lib/utils" - -function Switch({ - className, - size = "default", - ...props -}: SwitchPrimitive.Root.Props & { - size?: "sm" | "default" -}) { - return ( - - - - ) -} - -export { Switch }