fix: prevent duplicate support ticket submissions

This commit is contained in:
JetSprow
2026-04-29 16:38:31 +10:00
parent 2a3c9959bd
commit 16573c67c3
24 changed files with 327 additions and 120 deletions

View File

@@ -10,6 +10,7 @@ import {
createAnnouncement,
updateAnnouncement,
} from "@/actions/admin/announcements";
import { PendingSubmitButton } from "@/components/shared/pending-submit-button";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -210,9 +211,9 @@ export function AnnouncementForm({
</select>
</div>
<Button type="submit" className="w-full">
<PendingSubmitButton className="w-full" pendingLabel="保存中...">
</Button>
</PendingSubmitButton>
</form>
</DialogContent>
</Dialog>
@@ -343,9 +344,9 @@ export function CreateAnnouncementButton({
</select>
</div>
<Button type="submit" className="w-full">
<PendingSubmitButton className="w-full" pendingLabel="发布中...">
</Button>
</PendingSubmitButton>
</form>
</DialogContent>
</Dialog>

View File

@@ -4,7 +4,7 @@ import { createCoupon, createPromotionRule } from "@/actions/admin/commerce";
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 { Button } from "@/components/ui/button";
import { PendingSubmitButton } from "@/components/shared/pending-submit-button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -73,7 +73,7 @@ export default async function AdminCommercePage() {
<option value="false"></option>
</select>
</div>
<Button type="submit" className="w-full"></Button>
<PendingSubmitButton className="w-full" pendingLabel="创建中..."></PendingSubmitButton>
</form>
<form action={createPromotionRule} className="form-panel space-y-4">
@@ -96,7 +96,7 @@ export default async function AdminCommercePage() {
<Label htmlFor="promotion-sort"></Label>
<Input id="promotion-sort" name="sortOrder" type="number" defaultValue={100} />
</div>
<Button type="submit" className="w-full"></Button>
<PendingSubmitButton className="w-full" pendingLabel="创建中..."></PendingSubmitButton>
</form>
</section>
</TabsContent>

View File

@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { PendingSubmitButton } from "@/components/shared/pending-submit-button";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -98,9 +99,9 @@ export function NodeForm({
<p className="text-xs leading-5 text-muted-foreground">
线使 Token 3x-ui API
</p>
<Button type="submit" size="lg" className="w-full">
<PendingSubmitButton size="lg" className="w-full" pendingLabel={isEdit ? "保存中..." : "创建中..."}>
{isEdit ? "保存并同步入站" : "创建并同步入站"}
</Button>
</PendingSubmitButton>
</form>
</DialogContent>
</Dialog>

View File

@@ -2,6 +2,7 @@
import { useState } from "react";
import type { StreamingService } from "@prisma/client";
import { PendingSubmitButton } from "@/components/shared/pending-submit-button";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -80,9 +81,9 @@ export function ServiceForm({
<Label></Label>
<Input name="description" defaultValue={service?.description ?? ""} />
</div>
<Button type="submit" size="lg" className="w-full">
<PendingSubmitButton size="lg" className="w-full" pendingLabel={isEdit ? "保存中..." : "创建中..."}>
{isEdit ? "保存" : "创建"}
</Button>
</PendingSubmitButton>
</form>
</DialogContent>
</Dialog>

View File

@@ -27,6 +27,7 @@ export default async function AdminSettingsPage() {
siteUrl: config.siteUrl,
subscriptionUrl: config.subscriptionUrl,
supportContact: config.supportContact,
supportOpenTicketLimit: config.supportOpenTicketLimit,
maintenanceNotice: config.maintenanceNotice,
siteNotice: config.siteNotice,
allowRegistration: config.allowRegistration,

View File

@@ -2,7 +2,7 @@
import { useState, type FormEvent } from "react";
import { useRouter } from "next/navigation";
import { Bell, Clock3, Gift, Mail, Send, Settings2, ShieldAlert, ShieldCheck } from "lucide-react";
import { Bell, Clock3, Gift, LifeBuoy, Mail, Send, 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";
@@ -16,6 +16,7 @@ interface AppConfig {
siteUrl: string | null;
subscriptionUrl: string | null;
supportContact: string | null;
supportOpenTicketLimit: number;
maintenanceNotice: string | null;
siteNotice: string | null;
allowRegistration: boolean;
@@ -65,6 +66,7 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (saving) return;
const form = event.currentTarget;
setSaving(true);
@@ -85,6 +87,8 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
}
async function handleTestEmail() {
if (testingEmail) return;
const form = document.getElementById("app-settings-form") as HTMLFormElement | null;
if (!form) return;
@@ -158,6 +162,29 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
</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">
<LifeBuoy className="size-4 text-primary" />
</div>
<div className="grid gap-5 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="supportOpenTicketLimit"></Label>
<Input
id="supportOpenTicketLimit"
name="supportOpenTicketLimit"
type="number"
min={1}
max={20}
step={1}
defaultValue={config.supportOpenTicketLimit}
/>
<p className="text-xs leading-5 text-muted-foreground">
2
</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">
<Clock3 className="size-4 text-primary" />

View File

@@ -1,6 +1,6 @@
import { Paperclip, Send } from "lucide-react";
import { replySupportAsAdmin } from "@/actions/admin/support";
import { Button } from "@/components/ui/button";
import { PendingSubmitButton } from "@/components/shared/pending-submit-button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
@@ -42,7 +42,7 @@ export function AdminSupportReplyForm({ ticketId }: { ticketId: string }) {
JPGPNGWEBPGIFAVIF 3 3MB
</p>
</div>
<Button type="submit" size="lg" className="w-full sm:w-auto"></Button>
<PendingSubmitButton size="lg" className="w-full sm:w-auto" pendingLabel="发送中..."></PendingSubmitButton>
</form>
);
}

View File

@@ -1,4 +1,5 @@
import Link from "next/link";
import { Eye } from "lucide-react";
import { DataTableShell } from "@/components/admin/data-table-shell";
import {
DataTable,
@@ -14,6 +15,7 @@ import {
SupportTicketStatusBadge,
} from "@/components/support/ticket-badges";
import { AdminSupportTicketActions } from "@/components/support/admin-ticket-actions";
import { buttonVariants } from "@/components/ui/button";
import { formatDate } from "@/lib/utils";
import type { AdminSupportTicketRow } from "../support-data";
@@ -63,7 +65,14 @@ export function AdminSupportTable({ tickets }: AdminSupportTableProps) {
<time dateTime={ticket.updatedAt.toISOString()}>{formatDate(ticket.updatedAt)}</time>
</DataTableCell>
<DataTableCell>
<div className="flex justify-end">
<div className="flex flex-wrap justify-end gap-2">
<Link
href={`/admin/support/${ticket.id}`}
className={buttonVariants({ variant: "outline", size: "sm" })}
>
<Eye className="size-3.5" />
</Link>
<AdminSupportTicketActions ticketId={ticket.id} />
</div>
</DataTableCell>

View File

@@ -1,5 +1,5 @@
import { updateSupportTicketMeta } from "@/actions/admin/support";
import { Button } from "@/components/ui/button";
import { PendingSubmitButton } from "@/components/shared/pending-submit-button";
import { Label } from "@/components/ui/label";
import type { AdminSupportTicketDetail } from "../support-data";
@@ -38,7 +38,7 @@ export function SupportTicketMetaForm({ ticket }: { ticket: AdminSupportTicketDe
<option value="URGENT"></option>
</select>
</div>
<Button type="submit" variant="outline" size="lg"></Button>
<PendingSubmitButton variant="outline" size="lg" pendingLabel="更新中..."></PendingSubmitButton>
</form>
);
}

View File

@@ -1,6 +1,6 @@
import { BellRing } from "lucide-react";
import { runReminderTask } from "@/actions/admin/tasks";
import { Button } from "@/components/ui/button";
import { PendingSubmitButton } from "@/components/shared/pending-submit-button";
export function TaskLaunchPanel() {
return (
@@ -13,7 +13,7 @@ export function TaskLaunchPanel() {
<p className="font-semibold"></p>
<p className="mt-1 text-xs leading-5 text-muted-foreground"></p>
</div>
<Button type="submit" size="sm" variant="outline" className="mt-auto w-full"></Button>
<PendingSubmitButton size="sm" variant="outline" className="mt-auto w-full" pendingLabel="派发中..."></PendingSubmitButton>
</form>
</div>
);

View File

@@ -11,7 +11,7 @@ import {
DataTableRow,
} from "@/components/shared/data-table";
import { TaskStatusBadge, taskKindLabels } from "@/components/shared/domain-badges";
import { Button } from "@/components/ui/button";
import { PendingSubmitButton } from "@/components/shared/pending-submit-button";
import { formatDate } from "@/lib/utils";
import type { AdminTaskRunRow } from "../tasks-data";
@@ -85,7 +85,7 @@ export function TaskRunsTable({ tasks }: TaskRunsTableProps) {
await retryTaskRun(task.id);
}}
>
<Button type="submit" size="sm" variant="outline"></Button>
<PendingSubmitButton size="sm" variant="outline" pendingLabel="重试中..."></PendingSubmitButton>
</form>
)}
</div>

View File

@@ -2,6 +2,7 @@
import { useState } from "react";
import type { User } from "@prisma/client";
import { PendingSubmitButton } from "@/components/shared/pending-submit-button";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -82,9 +83,9 @@ export function UserForm({
<option value="ADMIN"></option>
</select>
</div>
<Button type="submit" className="w-full">
<PendingSubmitButton className="w-full" pendingLabel={isEdit ? "保存中..." : "创建中..."}>
{isEdit ? "保存" : "创建"}
</Button>
</PendingSubmitButton>
</form>
</DialogContent>
</Dialog>