feat: unify boolean controls as buttons

This commit is contained in:
JetSprow
2026-04-30 16:04:59 +10:00
parent 2591402f70
commit 901219f39c
7 changed files with 209 additions and 156 deletions

View File

@@ -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({
</div>
<div className="space-y-2">
<Label htmlFor={`dismissible-${announcement.id}`}></Label>
<select
<BooleanToggle
id={`dismissible-${announcement.id}`}
name="dismissible"
defaultValue={announcement.dismissible ? "true" : "false"}
className="h-10 w-full px-3 text-sm outline-none"
>
<option value="true"></option>
<option value="false"></option>
</select>
defaultValue={announcement.dismissible}
trueLabel="允许"
falseLabel="不允许"
ariaLabel="允许用户关闭"
/>
</div>
</div>
@@ -200,15 +200,14 @@ export function AnnouncementForm({
<div className="space-y-2">
<Label htmlFor={`sendNotification-${announcement.id}`}></Label>
<select
<BooleanToggle
id={`sendNotification-${announcement.id}`}
name="sendNotification"
defaultValue={announcement.sendNotification ? "true" : "false"}
className="h-10 w-full px-3 text-sm outline-none"
>
<option value="true"></option>
<option value="false"></option>
</select>
defaultValue={announcement.sendNotification}
trueLabel="发送"
falseLabel="不发送"
ariaLabel="同步发送站内通知"
/>
</div>
<PendingSubmitButton className="w-full" pendingLabel="保存中...">
@@ -285,15 +284,14 @@ export function CreateAnnouncementButton({
</div>
<div className="space-y-2">
<Label htmlFor="create-announcement-dismissible"></Label>
<select
<BooleanToggle
id="create-announcement-dismissible"
name="dismissible"
defaultValue="true"
className="h-10 w-full px-3 text-sm outline-none"
>
<option value="true"></option>
<option value="false"></option>
</select>
defaultValue
trueLabel="允许"
falseLabel="不允许"
ariaLabel="允许用户关闭"
/>
</div>
</div>
@@ -333,15 +331,14 @@ export function CreateAnnouncementButton({
<div className="space-y-2">
<Label htmlFor="create-announcement-sendNotification"></Label>
<select
<BooleanToggle
id="create-announcement-sendNotification"
name="sendNotification"
defaultValue="true"
className="h-10 w-full px-3 text-sm outline-none"
>
<option value="true"></option>
<option value="false"></option>
</select>
defaultValue
trueLabel="发送"
falseLabel="不发送"
ariaLabel="同步发送站内通知"
/>
</div>
<PendingSubmitButton className="w-full" pendingLabel="发布中...">

View File

@@ -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() {
</div>
<div className="space-y-2">
<Label htmlFor="coupon-public"></Label>
<select id="coupon-public" name="isPublic" className={selectClassName} defaultValue="true">
<option value="true"></option>
<option value="false"></option>
</select>
<BooleanToggle
id="coupon-public"
name="isPublic"
defaultValue
trueLabel="公开展示"
falseLabel="仅发放"
ariaLabel="用户可见"
/>
</div>
<PendingSubmitButton className="w-full" pendingLabel="创建中..."></PendingSubmitButton>
</form>

View File

@@ -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({
)}
</div>
<div className="flex flex-col gap-3 border-t border-border/50 pt-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-2">
<Switch checked={enabled} onCheckedChange={setEnabled} />
<span className="text-sm">{enabled ? "已启用" : "未启用"}</span>
<div className="w-full sm:w-56">
<Label className="mb-2 block text-xs text-muted-foreground"></Label>
<BooleanToggle
value={enabled}
onChange={setEnabled}
trueLabel="启用"
falseLabel="停用"
ariaLabel="支付通道状态"
/>
</div>
<Button type="submit" size="sm" disabled={saving}>
{saving ? "保存中..." : "保存配置"}

View File

@@ -1,6 +1,7 @@
"use client";
import type { Dispatch, SetStateAction } from "react";
import { BooleanToggle } from "@/components/ui/boolean-toggle";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
@@ -10,7 +11,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import type { PlanFormValue, PlanType } from "./plan-form-types";
type FieldId = (name: string) => string;
@@ -52,7 +52,15 @@ export function PlanPolicySection({
<p id={fieldId("allowRenewal-label")} className="text-sm font-medium"></p>
<p className="text-xs text-muted-foreground"></p>
</div>
<Switch aria-labelledby={fieldId("allowRenewal-label")} checked={allowRenewal} onCheckedChange={setAllowRenewal} />
<div className="w-40">
<BooleanToggle
value={allowRenewal}
onChange={setAllowRenewal}
trueLabel="开放"
falseLabel="关闭"
ariaLabel="开放续费"
/>
</div>
</div>
{type === "PROXY" && (
<div className="flex items-center justify-between gap-4 rounded-lg bg-muted/20 p-3">
@@ -60,7 +68,15 @@ export function PlanPolicySection({
<p id={fieldId("allowTrafficTopup-label")} className="text-sm font-medium"></p>
<p className="text-xs text-muted-foreground"> GB</p>
</div>
<Switch aria-labelledby={fieldId("allowTrafficTopup-label")} checked={allowTrafficTopup} onCheckedChange={setAllowTrafficTopup} />
<div className="w-40">
<BooleanToggle
value={allowTrafficTopup}
onChange={setAllowTrafficTopup}
trueLabel="开放"
falseLabel="关闭"
ariaLabel="开放增流量"
/>
</div>
</div>
)}
</div>

View File

@@ -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:
<div className="grid gap-5 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="autoReminderDispatchEnabled"></Label>
<select
<BooleanToggle
id="autoReminderDispatchEnabled"
name="autoReminderDispatchEnabled"
defaultValue={String(config.autoReminderDispatchEnabled)}
className={selectClassName}
>
<option value="true"></option>
<option value="false"></option>
</select>
defaultValue={config.autoReminderDispatchEnabled}
ariaLabel="自动提醒派发"
/>
</div>
<div className="space-y-2">
<Label htmlFor="reminderDispatchIntervalMinutes"></Label>
@@ -221,15 +219,12 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
</div>
<div className="space-y-2">
<Label htmlFor="trafficSyncEnabled">3x-ui </Label>
<select
<BooleanToggle
id="trafficSyncEnabled"
name="trafficSyncEnabled"
defaultValue={String(config.trafficSyncEnabled)}
className={selectClassName}
>
<option value="true"></option>
<option value="false"></option>
</select>
defaultValue={config.trafficSyncEnabled}
ariaLabel="3x-ui 流量定时同步"
/>
</div>
<div className="space-y-2">
<Label htmlFor="trafficSyncIntervalSeconds"></Label>
@@ -254,30 +249,24 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
<div className="grid gap-5 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="networkRecommendationsEnabled"></Label>
<select
<BooleanToggle
id="networkRecommendationsEnabled"
name="networkRecommendationsEnabled"
defaultValue={String(config.networkRecommendationsEnabled)}
className={selectClassName}
>
<option value="false"></option>
<option value="true"></option>
</select>
defaultValue={config.networkRecommendationsEnabled}
ariaLabel="三网推荐"
/>
<p className="text-xs leading-5 text-muted-foreground">
</p>
</div>
<div className="space-y-2">
<Label htmlFor="networkInsightsEnabled">线</Label>
<select
<BooleanToggle
id="networkInsightsEnabled"
name="networkInsightsEnabled"
defaultValue={String(config.networkInsightsEnabled)}
className={selectClassName}
>
<option value="false"></option>
<option value="true"></option>
</select>
defaultValue={config.networkInsightsEnabled}
ariaLabel="线路体验"
/>
<p className="text-xs leading-5 text-muted-foreground">
访线
</p>
@@ -313,27 +302,23 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
<div className="grid gap-5 md:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="subscriptionRiskEnabled"></Label>
<select
<BooleanToggle
id="subscriptionRiskEnabled"
name="subscriptionRiskEnabled"
defaultValue={String(config.subscriptionRiskEnabled)}
className={selectClassName}
>
<option value="true"></option>
<option value="false"></option>
</select>
defaultValue={config.subscriptionRiskEnabled}
ariaLabel="风控总控"
/>
</div>
<div className="space-y-2">
<Label htmlFor="subscriptionRiskAutoSuspend"></Label>
<select
<BooleanToggle
id="subscriptionRiskAutoSuspend"
name="subscriptionRiskAutoSuspend"
defaultValue={String(config.subscriptionRiskAutoSuspend)}
className={selectClassName}
>
<option value="true"></option>
<option value="false"></option>
</select>
defaultValue={config.subscriptionRiskAutoSuspend}
trueLabel="开启自动封停"
falseLabel="只记录警告"
ariaLabel="自动暂停"
/>
</div>
<div className="space-y-2">
<Label htmlFor="subscriptionRiskWindowHours"></Label>
@@ -436,15 +421,14 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
</div>
<div className="space-y-2">
<Label htmlFor="nodeAccessRiskEnabled"></Label>
<select
<BooleanToggle
id="nodeAccessRiskEnabled"
name="nodeAccessRiskEnabled"
defaultValue={String(config.nodeAccessRiskEnabled)}
className={selectClassName}
>
<option value="true"> Agent Xray </option>
<option value="false"></option>
</select>
defaultValue={config.nodeAccessRiskEnabled}
trueLabel="接收日志"
falseLabel="仅订阅风控"
ariaLabel="节点日志风控"
/>
</div>
<div className="space-y-2">
<Label htmlFor="nodeAccessConnectionWarning"></Label>
@@ -477,39 +461,36 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
<div className="grid gap-5 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="allowRegistration"></Label>
<select
<BooleanToggle
id="allowRegistration"
name="allowRegistration"
defaultValue={String(config.allowRegistration)}
className={selectClassName}
>
<option value="true"></option>
<option value="false"></option>
</select>
defaultValue={config.allowRegistration}
trueLabel="开放"
falseLabel="关闭"
ariaLabel="开放注册"
/>
</div>
<div className="space-y-2">
<Label htmlFor="requireInviteCode"></Label>
<select
<BooleanToggle
id="requireInviteCode"
name="requireInviteCode"
defaultValue={String(config.requireInviteCode)}
className={selectClassName}
>
<option value="false"></option>
<option value="true"></option>
</select>
defaultValue={config.requireInviteCode}
trueLabel="必须"
falseLabel="不需要"
ariaLabel="注册必须邀请码"
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="emailVerificationRequired"></Label>
<select
<BooleanToggle
id="emailVerificationRequired"
name="emailVerificationRequired"
defaultValue={String(config.emailVerificationRequired)}
className={selectClassName}
>
<option value="false"></option>
<option value="true"></option>
</select>
defaultValue={config.emailVerificationRequired}
trueLabel="开启验证"
falseLabel="关闭"
ariaLabel="注册邮箱验证"
/>
<p className="text-xs leading-5 text-muted-foreground"></p>
</div>
</div>
@@ -525,10 +506,12 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
<div className="grid gap-5 md:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="smtpEnabled"></Label>
<select id="smtpEnabled" name="smtpEnabled" defaultValue={String(config.smtpEnabled)} className={selectClassName}>
<option value="false"></option>
<option value="true"></option>
</select>
<BooleanToggle
id="smtpEnabled"
name="smtpEnabled"
defaultValue={config.smtpEnabled}
ariaLabel="邮件服务"
/>
</div>
<div className="space-y-2">
<Label htmlFor="smtpHost">SMTP </Label>
@@ -540,10 +523,14 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
</div>
<div className="space-y-2">
<Label htmlFor="smtpSecure">TLS / SSL</Label>
<select id="smtpSecure" name="smtpSecure" defaultValue={String(config.smtpSecure)} className={selectClassName}>
<option value="false">STARTTLS / </option>
<option value="true">SSL </option>
</select>
<BooleanToggle
id="smtpSecure"
name="smtpSecure"
defaultValue={config.smtpSecure}
trueLabel="SSL 直连"
falseLabel="STARTTLS"
ariaLabel="TLS / SSL"
/>
</div>
<div className="space-y-2">
<Label htmlFor="smtpUser">SMTP </Label>
@@ -581,15 +568,12 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
<div className="grid gap-5 md:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="inviteRewardEnabled"></Label>
<select
<BooleanToggle
id="inviteRewardEnabled"
name="inviteRewardEnabled"
defaultValue={String(config.inviteRewardEnabled)}
className={selectClassName}
>
<option value="false"></option>
<option value="true"></option>
</select>
defaultValue={config.inviteRewardEnabled}
ariaLabel="自动发放奖励"
/>
</div>
<div className="space-y-2">
<Label htmlFor="inviteRewardRate">%</Label>