diff --git a/src/app/(admin)/admin/payments/config-form.tsx b/src/app/(admin)/admin/payments/config-form.tsx index 8e598c1..d705a1f 100644 --- a/src/app/(admin)/admin/payments/config-form.tsx +++ b/src/app/(admin)/admin/payments/config-form.tsx @@ -1,12 +1,25 @@ "use client"; -import { useState } from "react"; +import { useMemo, useState, type FormEvent } from "react"; +import { useRouter } from "next/navigation"; +import { Check, CreditCard, Pencil, ShieldCheck } from "lucide-react"; +import { savePaymentConfig } from "@/actions/admin/payments"; +import { ActiveStatusBadge, StatusBadge } from "@/components/shared/status-badge"; import { BooleanToggle } from "@/components/ui/boolean-toggle"; import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { savePaymentConfig } from "@/actions/admin/payments"; import { getErrorMessage } from "@/lib/errors"; +import { cn } from "@/lib/utils"; import { toast } from "sonner"; interface Field { @@ -20,48 +33,102 @@ interface Field { interface Props { provider: string; + providerName: string; + providerDescription: string; fields: Field[]; currentConfig?: Record; secretConfigured?: Record; enabled: boolean; } -export function PaymentConfigForm({ +function selectedOptionLabels(field: Field, rawValue: string | undefined) { + const selected = new Set((rawValue ?? "").split(",").map((item) => item.trim()).filter(Boolean)); + return (field.options ?? []) + .filter((option) => selected.has(option.value)) + .map((option) => option.label); +} + +function buildInitialCheckboxValues(fields: Field[], currentConfig?: Record) { + const values: Record> = {}; + + for (const field of fields) { + if (field.type !== "checkboxes") continue; + values[field.key] = new Set( + (currentConfig?.[field.key] ?? "") + .split(",") + .map((item) => item.trim()) + .filter(Boolean), + ); + } + + return values; +} + +function configCompleteness(fields: Field[], currentConfig: Record | undefined, secretConfigured: Record) { + let configured = 0; + + for (const field of fields) { + if (field.secret) { + if (secretConfigured[field.key]) configured += 1; + continue; + } + if (field.type === "checkboxes") { + if (selectedOptionLabels(field, currentConfig?.[field.key]).length > 0) configured += 1; + continue; + } + if (currentConfig?.[field.key]?.trim()) configured += 1; + } + + return { configured, total: fields.length }; +} + +export function PaymentConfigItem({ provider, + providerName, + providerDescription, fields, currentConfig, secretConfigured = {}, enabled: initialEnabled, }: Props) { + const router = useRouter(); + const [open, setOpen] = useState(false); const [enabled, setEnabled] = useState(initialEnabled); const [saving, setSaving] = useState(false); - - // Track checkbox field values (comma-separated strings) - const [checkboxValues, setCheckboxValues] = useState>>(() => { - const init: Record> = {}; - for (const field of fields) { - if (field.type === "checkboxes") { - const raw = currentConfig?.[field.key] || ""; - init[field.key] = new Set(raw.split(",").map((s) => s.trim()).filter(Boolean)); - } - } - return init; - }); + const [checkboxValues, setCheckboxValues] = useState>>(() => + buildInitialCheckboxValues(fields, currentConfig), + ); + const completeness = useMemo( + () => configCompleteness(fields, currentConfig, secretConfigured), + [currentConfig, fields, secretConfigured], + ); + const secretFields = fields.filter((field) => field.secret); + const configuredSecretCount = secretFields.filter((field) => secretConfigured[field.key]).length; + const displayName = currentConfig?.displayName?.trim(); + const checkboxSummaries = fields + .filter((field) => field.type === "checkboxes") + .flatMap((field) => selectedOptionLabels(field, currentConfig?.[field.key])); function toggleCheckbox(fieldKey: string, value: string) { - setCheckboxValues((prev) => { - const next = new Set(prev[fieldKey]); - if (next.has(value)) next.delete(value); - else next.add(value); - return { ...prev, [fieldKey]: next }; + setCheckboxValues((current) => { + const next = new Set(current[fieldKey] ?? []); + if (next.has(value)) { + next.delete(value); + } else { + next.add(value); + } + return { ...current, [fieldKey]: next }; }); } - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - setSaving(true); - const formData = new FormData(e.currentTarget); + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + if (saving) return; + + const form = event.currentTarget; + const formData = new FormData(form); const config: Record = {}; + for (const field of fields) { if (field.type === "checkboxes") { config[field.key] = Array.from(checkboxValues[field.key] ?? []).join(","); @@ -70,71 +137,147 @@ export function PaymentConfigForm({ } } + setSaving(true); try { await savePaymentConfig(provider, config, enabled); for (const field of fields) { if (!field.secret) continue; - const input = e.currentTarget.elements.namedItem(field.key); + const input = form.elements.namedItem(field.key); if (input instanceof HTMLInputElement) { input.value = ""; } } - toast.success("保存成功"); + toast.success("支付配置已保存"); + setOpen(false); + router.refresh(); } catch (error) { - toast.error(getErrorMessage(error, "保存失败")); + toast.error(getErrorMessage(error, "保存支付配置失败")); + } finally { + setSaving(false); } - setSaving(false); } return ( -
-
- {fields.map((field) => - field.type === "checkboxes" ? ( -
- -
- {field.options?.map((opt) => ( -
-
-
- - -
- -
- + + + + + + + + + ); } diff --git a/src/app/(admin)/admin/payments/page.tsx b/src/app/(admin)/admin/payments/page.tsx index a64ad37..b1ba198 100644 --- a/src/app/(admin)/admin/payments/page.tsx +++ b/src/app/(admin)/admin/payments/page.tsx @@ -1,8 +1,6 @@ import type { Metadata } from "next"; -import { CreditCard } from "lucide-react"; import { PageHeader, PageShell } from "@/components/shared/page-shell"; -import { ActiveStatusBadge } from "@/components/shared/status-badge"; -import { PaymentConfigForm } from "./config-form"; +import { PaymentConfigItem } from "./config-form"; import { getPaymentProviderConfigs } from "./payments-data"; export const metadata: Metadata = { @@ -19,29 +17,18 @@ export default async function PaymentsPage() { eyebrow="系统" title="支付配置" /> -
+
{providerConfigs.map(({ provider, config, secretConfigured }) => ( -
-
-
- - - -
-

{provider.name}

-

{provider.description}

-
-
- -
- -
+ ))}