import { useTranslation } from "react-i18next"; import { Card, CardContent, CardHeader, CardTitle, } from "@/components/ui/card.tsx"; import Paragraph, { ParagraphDescription, ParagraphFooter, ParagraphItem, ParagraphSpace, } from "@/components/Paragraph.tsx"; import { Button } from "@/components/ui/button.tsx"; import { Label } from "@/components/ui/label.tsx"; import { Input } from "@/components/ui/input.tsx"; import { useMemo, useReducer, useState } from "react"; import { formReducer } from "@/utils/form.ts"; import { NumberInput } from "@/components/ui/number-input.tsx"; import { CommonState, commonWhiteList, GeneralState, getConfig, initialSystemState, MailState, SearchState, setConfig, SiteState, SystemProps, updateRootPassword, } from "@/admin/api/system.ts"; import { useEffectAsync } from "@/utils/hook.ts"; import { toastState } from "@/api/common.ts"; import { toast, useToast } from "@/components/ui/use-toast.ts"; import { doVerify } from "@/api/auth.ts"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTrigger, } from "@/components/ui/dialog.tsx"; import { DialogTitle } from "@radix-ui/react-dialog"; import Require from "@/components/Require.tsx"; import { Loader2, PencilLine, Settings2 } from "lucide-react"; import { FlexibleTextarea } from "@/components/ui/textarea.tsx"; import Tips from "@/components/Tips.tsx"; import { cn } from "@/components/ui/lib/utils.ts"; import { Switch } from "@/components/ui/switch.tsx"; import { MultiCombobox } from "@/components/ui/multi-combobox.tsx"; import { allGroups } from "@/utils/groups.ts"; import { useChannelModels } from "@/admin/hook.tsx"; import { useSelector } from "react-redux"; import { selectSupportModels } from "@/store/chat.ts"; import { JSONEditorProvider } from "@/components/EditorProvider.tsx"; type CompProps = { data: T; dispatch: (action: any) => void; onChange: (doToast?: boolean) => Promise; }; function RootDialog() { const { t } = useTranslation(); const { toast } = useToast(); const [open, setOpen] = useState(false); const [password, setPassword] = useState(""); const [repeat, setRepeat] = useState(""); const onPost = async () => { const res = await updateRootPassword(password); toastState(toast, t, res, true); if (res.status) { setPassword(""); setRepeat(""); setOpen(false); setTimeout(() => { window.location.reload(); }, 1000); } }; return ( {t("admin.system.updateRoot")}
{t("admin.system.updateRootTip")}
setPassword(e.target.value)} /> setRepeat(e.target.value)} />
); } function General({ data, dispatch, onChange }: CompProps) { const { t } = useTranslation(); return ( dispatch({ type: "update:general.title", value: e.target.value, }) } placeholder={t("admin.system.titleTip")} /> dispatch({ type: "update:general.docs", value: e.target.value, }) } placeholder={t("admin.system.docsTip")} /> dispatch({ type: "update:general.logo", value: e.target.value, }) } placeholder={t("admin.system.logoTip", { logo: `${window.location.protocol}//${window.location.host}/favicon.ico`, })} /> dispatch({ type: "update:general.backend", value: e.target.value, }) } placeholder={t("admin.system.backendPlaceholder", { backend: `${window.location.protocol}//${window.location.host}/api`, })} /> {t("admin.system.backendTip")} dispatch({ type: "update:general.file", value: e.target.value, }) } placeholder={t("admin.system.filePlaceholder")} /> {t("admin.system.fileTip")} dispatch({ type: "update:general.pwa_manifest", value }) } >
); } function Mail({ data, dispatch, onChange }: CompProps) { const { t } = useTranslation(); const [email, setEmail] = useState(""); const [mailDialog, setMailDialog] = useState(false); const valid = useMemo((): boolean => { return ( data.host.length > 0 && data.port > 0 && data.port < 65535 && data.username.length > 0 && data.password.length > 0 && data.from.length > 0 && /\w+@\w+\.\w+/.test(data.from) && !data.username.includes("@") ); }, [data]); const onTest = async () => { if (!email.trim()) return; await onChange(false); const res = await doVerify(email); toastState(toast, t, res, true); if (res.status) setMailDialog(false); }; const white_list = useMemo(() => { const raw = data.white_list.custom .split(",") .map((item) => item.trim()) .filter((item) => item.length > 0); return [...commonWhiteList, ...raw]; }, [data]); return ( {!valid && ( {t("admin.system.mailConfNotValid")} )} dispatch({ type: "update:mail.host", value: e.target.value, }) } placeholder={`smtp.qcloudmail.com`} /> dispatch({ type: "update:mail.port", value }) } placeholder={`465`} min={0} max={65535} /> dispatch({ type: "update:mail.username", value: e.target.value, }) } className={cn( "transition-all duration-300", data.username.includes("@") && `border-red-700`, )} placeholder={t("admin.system.mailUser")} /> dispatch({ type: "update:mail.password", value: e.target.value, }) } placeholder={t("admin.system.mailPass")} /> dispatch({ type: "update:mail.from", value: e.target.value, }) } placeholder={`${data.username}@${location.hostname}`} className={cn( "transition-all duration-300", data.from.length > 0 && !/\w+@\w+\.\w+/.test(data.from) && `border-red-700`, )} /> { dispatch({ type: "update:mail.white_list.enabled", value, }); }} /> { dispatch({ type: "update:mail.white_list.white_list", value, }); }} placeholder={t("admin.system.mailWhitelistSelected", { length: data.white_list.white_list.length, })} searchPlaceholder={t("admin.system.mailWhitelistSearchPlaceholder")} /> dispatch({ type: "update:mail.white_list.custom", value: e.target.value, }) } disabled={!data.white_list.enabled} placeholder={t("admin.system.customWhitelistPlaceholder")} />
{t("admin.system.test")} setEmail(e.target.value)} /> ); } function Site({ data, dispatch, onChange }: CompProps) { const { t } = useTranslation(); return ( { dispatch({ type: "update:site.close_register", value }); }} /> { dispatch({ type: "update:site.relay_plan", value }); }} /> dispatch({ type: "update:site.quota", value }) } placeholder={`5`} min={0} /> dispatch({ type: "update:site.buy_link", value: e.target.value, }) } placeholder={t("admin.system.buyLinkPlaceholder")} /> dispatch({ type: "update:site.announcement", value: e.target.value, }) } placeholder={t("admin.system.announcementPlaceholder")} /> dispatch({ type: "update:site.contact", value: e.target.value, }) } placeholder={t("admin.system.contactPlaceholder")} /> dispatch({ type: "update:site.footer", value: e.target.value, }) } placeholder={t("admin.system.footerPlaceholder")} /> { dispatch({ type: "update:site.auth_footer", value }); }} />
); } function Common({ data, dispatch, onChange }: CompProps) { const { t } = useTranslation(); const { channelModels } = useChannelModels(); const supportModels = useSelector(selectSupportModels); return ( { dispatch({ type: "update:common.cache", value }); }} list={channelModels} placeholder={t("admin.system.cachePlaceholder", { length: (data.cache ?? []).length, })} /> dispatch({ type: "update:common.expire", value }) } min={0} /> dispatch({ type: "update:common.size", value }) } min={0} />
{ dispatch({ type: "update:common.article", value }); }} list={allGroups} listTranslate={`admin.channels.groups`} placeholder={t("admin.system.groupPlaceholder", { length: (data.article ?? []).length, })} /> { dispatch({ type: "update:common.generation", value }); }} list={allGroups} listTranslate={`admin.channels.groups`} placeholder={t("admin.system.groupPlaceholder", { length: (data.generation ?? []).length, })} />
); } function Search({ data, dispatch, onChange }: CompProps) { const { t } = useTranslation(); return ( dispatch({ type: "update:search.endpoint", value: e.target.value, }) } placeholder={t("admin.system.searchPlaceholder")} /> dispatch({ type: "update:search.query", value }) } placeholder={`5`} min={0} max={50} /> {t("admin.system.searchTip")}
); } function System() { const { t } = useTranslation(); const { toast } = useToast(); const [data, setData] = useReducer( formReducer(), initialSystemState, ); const [loading, setLoading] = useState(false); const doSaving = async (doToast?: boolean) => { const res = await setConfig(data); if (doToast !== false) toastState(toast, t, res, true); }; useEffectAsync(async () => { setLoading(true); const res = await getConfig(); setLoading(false); toastState(toast, t, res); if (res.status) { setData({ type: "set", value: res.data }); } }, []); return (
{t("admin.settings")} {loading && }
); } export default System;