mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
fix: prevent duplicate support ticket submissions
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" /> 自动化任务
|
||||
|
||||
@@ -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 }) {
|
||||
仅支持 JPG、PNG、WEBP、GIF、AVIF 图片,最多 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user