Initial commit

This commit is contained in:
JetSprow
2026-04-29 05:12:39 +10:00
commit 27dbca9cbf
379 changed files with 43486 additions and 0 deletions

View File

@@ -0,0 +1,258 @@
"use client";
import { useState } from "react";
import { Bell, Clock3, Gift, Settings2, ShieldAlert, ShieldCheck } from "lucide-react";
import { Button, buttonVariants } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { saveAppSettings } from "@/actions/admin/settings";
import { toast } from "sonner";
import { getErrorMessage } from "@/lib/errors";
interface AppConfig {
siteName: string;
siteUrl: string | null;
supportContact: string | null;
maintenanceNotice: string | null;
siteNotice: string | null;
allowRegistration: boolean;
requireInviteCode: boolean;
autoReminderDispatchEnabled: boolean;
reminderDispatchIntervalMinutes: number;
trafficSyncEnabled: boolean;
trafficSyncIntervalSeconds: number;
inviteRewardEnabled: boolean;
inviteRewardRate: number;
inviteRewardCouponId: string | null;
turnstileSiteKey: string | null;
turnstileSecretKey: string | null;
}
interface CouponOption {
id: string;
code: string;
name: string;
}
const selectClassName = "premium-input w-full appearance-none px-3.5 py-2 text-sm outline-none";
export function SettingsForm({ config, coupons }: { config: AppConfig; coupons: CouponOption[] }) {
const [saving, setSaving] = useState(false);
async function handleSubmit(formData: FormData) {
setSaving(true);
try {
await saveAppSettings(formData);
toast.success("设置已保存");
} catch (error) {
toast.error(getErrorMessage(error, "保存失败"));
} finally {
setSaving(false);
}
}
return (
<form action={handleSubmit} className="form-panel space-y-6">
<div className="flex items-start gap-3">
<span className="flex size-11 shrink-0 items-center justify-center rounded-lg border border-primary/15 bg-primary/10 text-primary">
<Settings2 className="size-5" />
</span>
<div>
<h3 className="text-lg font-semibold tracking-tight"></h3>
<p className="mt-1 text-sm leading-6 text-muted-foreground"></p>
</div>
</div>
<section className="space-y-4 rounded-lg border border-border bg-muted/25 p-3">
<div className="flex items-center gap-2 text-sm font-semibold">
<Settings2 className="size-4 text-primary" />
</div>
<div className="grid gap-5 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="siteName"></Label>
<Input id="siteName" name="siteName" defaultValue={config.siteName} required />
</div>
<div className="space-y-2">
<Label htmlFor="siteUrl"> / URL</Label>
<Input id="siteUrl" name="siteUrl" defaultValue={config.siteUrl ?? ""} placeholder="https://example.com" />
<p className="text-xs leading-5 text-muted-foreground"> Agent </p>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="supportContact"></Label>
<Input id="supportContact" name="supportContact" defaultValue={config.supportContact ?? ""} />
</div>
</div>
</section>
<section className="space-y-4 rounded-lg border border-border bg-muted/25 p-3">
<div className="flex items-center gap-2 text-sm font-semibold">
<Clock3 className="size-4 text-primary" />
</div>
<div className="grid gap-5 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="autoReminderDispatchEnabled"></Label>
<select
id="autoReminderDispatchEnabled"
name="autoReminderDispatchEnabled"
defaultValue={String(config.autoReminderDispatchEnabled)}
className={selectClassName}
>
<option value="true"></option>
<option value="false"></option>
</select>
</div>
<div className="space-y-2">
<Label htmlFor="reminderDispatchIntervalMinutes"></Label>
<Input id="reminderDispatchIntervalMinutes" name="reminderDispatchIntervalMinutes" type="number" min={1} defaultValue={config.reminderDispatchIntervalMinutes} />
</div>
<div className="space-y-2">
<Label htmlFor="trafficSyncEnabled">3x-ui </Label>
<select
id="trafficSyncEnabled"
name="trafficSyncEnabled"
defaultValue={String(config.trafficSyncEnabled)}
className={selectClassName}
>
<option value="true"></option>
<option value="false"></option>
</select>
</div>
<div className="space-y-2">
<Label htmlFor="trafficSyncIntervalSeconds"></Label>
<Input
id="trafficSyncIntervalSeconds"
name="trafficSyncIntervalSeconds"
type="number"
min={10}
step={1}
defaultValue={config.trafficSyncIntervalSeconds}
placeholder="60"
/>
<p className="text-xs leading-5 text-muted-foreground"> 60 10 </p>
</div>
</div>
</section>
<section className="space-y-4 rounded-lg border border-border bg-muted/25 p-3">
<div className="flex items-center gap-2 text-sm font-semibold">
<ShieldCheck className="size-4 text-primary" />
</div>
<div className="grid gap-5 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="allowRegistration"></Label>
<select
id="allowRegistration"
name="allowRegistration"
defaultValue={String(config.allowRegistration)}
className={selectClassName}
>
<option value="true"></option>
<option value="false"></option>
</select>
</div>
<div className="space-y-2">
<Label htmlFor="requireInviteCode"></Label>
<select
id="requireInviteCode"
name="requireInviteCode"
defaultValue={String(config.requireInviteCode)}
className={selectClassName}
>
<option value="false"></option>
<option value="true"></option>
</select>
</div>
</div>
</section>
<section className="space-y-4 rounded-lg border border-border bg-muted/25 p-3">
<div className="flex items-center gap-2 text-sm font-semibold">
<Gift className="size-4 text-primary" />
</div>
<div className="grid gap-5 md:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="inviteRewardEnabled"></Label>
<select
id="inviteRewardEnabled"
name="inviteRewardEnabled"
defaultValue={String(config.inviteRewardEnabled)}
className={selectClassName}
>
<option value="false"></option>
<option value="true"></option>
</select>
</div>
<div className="space-y-2">
<Label htmlFor="inviteRewardRate">%</Label>
<Input id="inviteRewardRate" name="inviteRewardRate" type="number" min={0} max={100} step="0.01" defaultValue={config.inviteRewardRate} />
</div>
<div className="space-y-2">
<Label htmlFor="inviteRewardCouponId"></Label>
<select
id="inviteRewardCouponId"
name="inviteRewardCouponId"
defaultValue={config.inviteRewardCouponId ?? ""}
className={selectClassName}
>
<option value=""></option>
{coupons.map((coupon) => (
<option key={coupon.id} value={coupon.id}>
{coupon.name} · {coupon.code}
</option>
))}
</select>
</div>
</div>
<p className="text-xs leading-5 text-muted-foreground">
</p>
</section>
<section className="space-y-4 rounded-lg border border-border bg-muted/25 p-3">
<div className="flex items-center gap-2 text-sm font-semibold">
<ShieldAlert className="size-4 text-primary" /> Cloudflare Turnstile
</div>
<p className="text-xs leading-5 text-muted-foreground">
</p>
<div className="grid gap-5 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="turnstileSiteKey">Site Key</Label>
<Input id="turnstileSiteKey" name="turnstileSiteKey" defaultValue={config.turnstileSiteKey ?? ""} placeholder="0x4AAAAAAA..." />
</div>
<div className="space-y-2">
<Label htmlFor="turnstileSecretKey">Secret Key</Label>
<Input id="turnstileSecretKey" name="turnstileSecretKey" type="password" defaultValue={config.turnstileSecretKey ?? ""} placeholder="0x4AAAAAAA..." />
</div>
</div>
</section>
<section className="space-y-4 rounded-lg border border-border bg-muted/25 p-3">
<div className="flex items-center gap-2 text-sm font-semibold">
<Bell className="size-4 text-primary" />
</div>
<div className="grid gap-5 lg:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="siteNotice"></Label>
<Textarea id="siteNotice" name="siteNotice" rows={4} defaultValue={config.siteNotice ?? ""} />
</div>
<div className="space-y-2">
<Label htmlFor="maintenanceNotice"></Label>
<Textarea id="maintenanceNotice" name="maintenanceNotice" rows={4} defaultValue={config.maintenanceNotice ?? ""} />
</div>
</div>
</section>
<div className="flex flex-col gap-3 sm:flex-row">
<Button type="submit" size="lg" disabled={saving}>
{saving ? "保存中..." : "保存设置"}
</Button>
<a href="/api/admin/export/config" className={buttonVariants({ variant: "outline", size: "lg" })}>
</a>
</div>
</form>
);
}