From 340fb61a61633e95e6b5ccc824c9da95df8c64e5 Mon Sep 17 00:00:00 2001 From: Bot Date: Wed, 13 May 2026 00:52:29 +0800 Subject: [PATCH] feat: improve notification settings and interactive TG bot --- src/components/batch-edit-note-icon.tsx | 227 ++++++++++++++++++++++++ src/components/notifier.tsx | 159 +++++++---------- src/components/server.tsx | 1 - src/locales/en/translation.json | 3 + src/locales/zh-CN/translation.json | 3 + src/main.tsx | 6 +- src/routes/settings.tsx | 156 ++++++++++++---- src/types/api.ts | 8 + 8 files changed, 435 insertions(+), 128 deletions(-) create mode 100644 src/components/batch-edit-note-icon.tsx diff --git a/src/components/batch-edit-note-icon.tsx b/src/components/batch-edit-note-icon.tsx new file mode 100644 index 0000000..968e5e5 --- /dev/null +++ b/src/components/batch-edit-note-icon.tsx @@ -0,0 +1,227 @@ +import { updateServer } from "@/api/server" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Textarea } from "@/components/ui/textarea" +import { IconButton } from "@/components/xui/icon-button" +import { ModelServer as Server } from "@/types" +import { zodResolver } from "@hookform/resolvers/zod" +import { useEffect, useState } from "react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { toast } from "sonner" +import { KeyedMutator } from "swr" +import { z } from "zod" + +interface BatchEditNoteIconProps { + servers: Server[] + selectedIds: number[] + mutate: KeyedMutator +} + +const batchNoteFormSchema = z.object({ + note: z.string().optional(), + public_note: z.string().optional(), +}) + +export const BatchEditNoteIcon: React.FC = ({ + servers, + selectedIds, + mutate, +}) => { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + + const form = useForm>({ + resolver: zodResolver(batchNoteFormSchema), + defaultValues: { + note: "", + public_note: "", + }, + }) + + useEffect(() => { + const handleMessage = (e: MessageEvent) => { + if (e.data?.type === "NZCFG_JSON") { + const target = e.data.target === "traffic" ? "public_note" : e.data.target + if (target === "public_note" || target === "note") { + form.setValue(target, e.data.payload) + toast(t("Success"), { + description: `批量配置已自动填入${ + target === "public_note" ? t("PublicNote.Label") : t("Private") + t("Note") + }`, + }) + } + } + } + window.addEventListener("message", handleMessage) + return () => window.removeEventListener("message", handleMessage) + }, [form, t]) + + const onSubmit = async (values: z.infer) => { + if (selectedIds.length === 0) { + toast(t("Error"), { description: t("Results.SelectAtLeastOneServer") }) + return + } + + const promises = selectedIds.map((id) => { + const server = servers.find((s) => s.id === id) + if (!server) return Promise.resolve() + + // Only update note/public_note if the batch form field is not empty! + // Wait, what if they WANT to clear the note? + // For batch, usually we only override if they typed something. + // We can provide a checkbox or just update if not empty. + // Let's just update if not empty for safety. + let updatePayload = { ...server } + if (values.note && values.note.trim() !== "") { + updatePayload.note = values.note + } + if (values.public_note && values.public_note.trim() !== "") { + updatePayload.public_note = values.public_note + } + + // Clean up non-API fields + return updateServer(id, { + name: updatePayload.name, + display_index: updatePayload.display_index, + note: updatePayload.note, + public_note: updatePayload.public_note, + hide_for_guest: updatePayload.hide_for_guest, + enable_ddns: updatePayload.enable_ddns, + ddns_profiles: updatePayload.ddns_profiles, + override_ddns_domains: updatePayload.override_ddns_domains, + }) + }) + + toast.promise(Promise.all(promises), { + loading: t("Saving..."), + success: () => { + setOpen(false) + mutate() + form.reset() + return t("Success") + }, + error: t("Results.UnExpectedError"), + }) + } + + return ( + <> + { + if (selectedIds.length === 0) { + toast(t("Error"), { description: t("Results.SelectAtLeastOneServer") }) + return + } + setOpen(true) + }} + /> + + + + 批量修改配置/备注 (已选 {selectedIds.length} 台) + +
+ + ( + + + {t("Private") + t("Note")} (留空则不修改) + + + +