From 0b4403b67b12d442dd219007bf59456b7ea22eb3 Mon Sep 17 00:00:00 2001 From: Sline Date: Wed, 15 Oct 2025 18:57:44 +0800 Subject: [PATCH] refactor: frontend (#5068) * refactor: setting components * refactor: frontend * fix: settings router --- .../controller/window-controller.tsx | 2 +- .../setting/mods/backup-config-viewer.tsx | 7 +- .../setting/mods/backup-table-viewer.tsx | 173 +++--- src/components/setting/mods/backup-viewer.tsx | 15 +- src/components/setting/mods/dns-viewer.tsx | 588 +++++++++--------- .../setting/mods/external-controller-cors.tsx | 19 +- src/components/setting/mods/guard-state.tsx | 10 +- src/components/setting/mods/hotkey-viewer.tsx | 4 +- src/components/setting/mods/layout-viewer.tsx | 6 +- .../setting/mods/network-interface-viewer.tsx | 16 +- .../setting/mods/password-input.tsx | 6 +- src/components/setting/mods/setting-comp.tsx | 15 +- .../setting/mods/sysproxy-viewer.tsx | 57 +- src/components/setting/mods/web-ui-item.tsx | 41 +- src/components/setting/mods/web-ui-viewer.tsx | 32 +- src/components/test/test-item.tsx | 30 +- src/components/test/test-viewer.tsx | 7 +- src/hooks/use-log-data-new.ts | 15 +- src/hooks/use-traffic-monitor.ts | 16 +- src/hooks/use-visibility.ts | 13 +- src/hooks/use-window.ts | 46 ++ src/hooks/use-window.tsx | 129 ---- src/main.tsx | 2 +- src/pages/_layout.tsx | 286 ++++++--- src/pages/connections.tsx | 8 +- src/pages/profiles.tsx | 14 +- src/pages/proxies.tsx | 70 ++- src/providers/app-data-provider.tsx | 134 ++-- src/providers/chain-proxy-context.ts | 20 + src/providers/chain-proxy-provider.tsx | 38 +- src/providers/window/WindowContext.ts | 18 + src/providers/window/WindowProvider.tsx | 91 +++ src/providers/window/index.ts | 2 + src/services/ipc-log-service.ts | 3 +- 34 files changed, 1072 insertions(+), 861 deletions(-) create mode 100644 src/hooks/use-window.ts delete mode 100644 src/hooks/use-window.tsx create mode 100644 src/providers/chain-proxy-context.ts create mode 100644 src/providers/window/WindowContext.ts create mode 100644 src/providers/window/WindowProvider.tsx create mode 100644 src/providers/window/index.ts diff --git a/src/components/controller/window-controller.tsx b/src/components/controller/window-controller.tsx index 07b7ab37..f8ad6dcc 100644 --- a/src/components/controller/window-controller.tsx +++ b/src/components/controller/window-controller.tsx @@ -37,7 +37,7 @@ export const WindowControls = forwardRef(function WindowControls(props, ref) { ); // 通过前端对 tauri 窗口进行翻转全屏时会短暂地与系统图标重叠渲染。 - // 这可能是上游缺陷,保险起见跨平台以窗口的最大化翻转为准 + // 这可能是上游缺陷,保险起见跨平台以窗口的最大化翻转为准。 return ( { - if (webdav_url && webdav_username && webdav_password) { - onInit(); + if (!webdav_url || !webdav_username || !webdav_password) { + return; } - }, []); + void onInit(); + }, [webdav_url, webdav_username, webdav_password, onInit]); const checkForm = () => { const username = usernameRef.current?.value; diff --git a/src/components/setting/mods/backup-table-viewer.tsx b/src/components/setting/mods/backup-table-viewer.tsx index fdb0de81..1805e27e 100644 --- a/src/components/setting/mods/backup-table-viewer.tsx +++ b/src/components/setting/mods/backup-table-viewer.tsx @@ -103,94 +103,97 @@ export const BackupTableViewer = memo( {datasource.length > 0 ? ( - datasource?.map((file, index) => ( - - - {file.platform === "windows" ? ( - - ) : file.platform === "linux" ? ( - - ) : ( - - )} - {file.filename} - - - {file.backup_time.fromNow()} - - - - {onExport && ( - <> - { - e.preventDefault(); - await handleExport(file.filename); - }} - > - - - - + datasource.map((file) => { + const rowKey = `${file.platform}-${file.filename}-${file.backup_time.valueOf()}`; + return ( + + + {file.platform === "windows" ? ( + + ) : file.platform === "linux" ? ( + + ) : ( + )} - { - e.preventDefault(); - const confirmed = await window.confirm( - t("Confirm to delete this backup file?"), - ); - if (confirmed) { - await handleDelete(file.filename); - } + {file.filename} + + + {file.backup_time.fromNow()} + + + - - - - { - e.preventDefault(); - const confirmed = await window.confirm( - t("Confirm to restore this backup file?"), - ); - if (confirmed) { - await handleRestore(file.filename); - } - }} - > - - - - - - )) + {onExport && ( + <> + { + e.preventDefault(); + await handleExport(file.filename); + }} + > + + + + + )} + { + e.preventDefault(); + const confirmed = window.confirm( + t("Confirm to delete this backup file?"), + ); + if (confirmed) { + void handleDelete(file.filename); + } + }} + > + + + + { + e.preventDefault(); + const confirmed = window.confirm( + t("Confirm to restore this backup file?"), + ); + if (confirmed) { + void handleRestore(file.filename); + } + }} + > + + + + + + ); + }) ) : ( diff --git a/src/components/setting/mods/backup-viewer.tsx b/src/components/setting/mods/backup-viewer.tsx index 77db7665..b3e532b0 100644 --- a/src/components/setting/mods/backup-viewer.tsx +++ b/src/components/setting/mods/backup-viewer.tsx @@ -7,6 +7,7 @@ import { useEffect, useImperativeHandle, useMemo, + useReducer, useRef, useState, } from "react"; @@ -36,16 +37,20 @@ dayjs.extend(customParseFormat); const DATE_FORMAT = "YYYY-MM-DD_HH-mm-ss"; const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/; type BackupSource = "local" | "webdav"; +type CloseButtonPosition = { top: number; left: number } | null; export function BackupViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const contentRef = useRef(null); - const [dialogPaper, setDialogPaper] = useState(null); - const [closeButtonPosition, setCloseButtonPosition] = useState<{ - top: number; - left: number; - } | null>(null); + const [dialogPaper, setDialogPaper] = useReducer( + (_: HTMLElement | null, next: HTMLElement | null) => next, + null as HTMLElement | null, + ); + const [closeButtonPosition, setCloseButtonPosition] = useReducer( + (_: CloseButtonPosition, next: CloseButtonPosition) => next, + null as CloseButtonPosition, + ); const [isLoading, setIsLoading] = useState(false); const [backupFiles, setBackupFiles] = useState([]); diff --git a/src/components/setting/mods/dns-viewer.tsx b/src/components/setting/mods/dns-viewer.tsx index 3286cb37..7a5b12b8 100644 --- a/src/components/setting/mods/dns-viewer.tsx +++ b/src/components/setting/mods/dns-viewer.tsx @@ -16,7 +16,14 @@ import { invoke } from "@tauri-apps/api/core"; import { useLockFn } from "ahooks"; import yaml from "js-yaml"; import type { Ref } from "react"; -import { useEffect, useImperativeHandle, useState } from "react"; +import { + useCallback, + useEffect, + useImperativeHandle, + useReducer, + useRef, + useState, +} from "react"; import { useTranslation } from "react-i18next"; import MonacoEditor from "react-monaco-editor"; @@ -35,6 +42,91 @@ const Item = styled(ListItem)(() => ({ }, })); +type NameserverPolicy = Record; + +function parseNameserverPolicy(str: string): NameserverPolicy { + const result: NameserverPolicy = {}; + if (!str) return result; + + const ruleRegex = /\s*([^=]+?)\s*=\s*([^,]+)(?:,|$)/g; + let match: RegExpExecArray | null; + + while ((match = ruleRegex.exec(str)) !== null) { + const [, domainsPart, serversPart] = match; + + const domains = [domainsPart.trim()]; + const servers = serversPart.split(";").map((s) => s.trim()); + + domains.forEach((domain) => { + result[domain] = servers; + }); + } + + return result; +} + +function formatNameserverPolicy(policy: unknown): string { + if (!policy || typeof policy !== "object") return ""; + + return Object.entries(policy as Record) + .map(([domain, servers]) => { + const serversStr = Array.isArray(servers) ? servers.join(";") : servers; + return `${domain}=${serversStr}`; + }) + .join(", "); +} + +function formatHosts(hosts: unknown): string { + if (!hosts || typeof hosts !== "object") return ""; + + const result: string[] = []; + + Object.entries(hosts as Record).forEach( + ([domain, value]) => { + if (Array.isArray(value)) { + const ipsStr = value.join(";"); + result.push(`${domain}=${ipsStr}`); + } else { + result.push(`${domain}=${value}`); + } + }, + ); + + return result.join(", "); +} + +function parseHosts(str: string): NameserverPolicy { + const result: NameserverPolicy = {}; + if (!str) return result; + + str.split(",").forEach((item) => { + const parts = item.trim().split("="); + if (parts.length < 2) return; + + const domain = parts[0].trim(); + const valueStr = parts.slice(1).join("=").trim(); + + if (valueStr.includes(";")) { + result[domain] = valueStr + .split(";") + .map((s) => s.trim()) + .filter(Boolean); + } else { + result[domain] = valueStr; + } + }); + + return result; +} + +function parseList(str: string): string[] { + if (!str?.trim()) return []; + return str + .split(",") + .map((item) => item.trim()) + .filter(Boolean); +} + // 默认DNS配置 const DEFAULT_DNS_CONFIG = { enable: true, @@ -95,6 +187,7 @@ export function DnsViewer({ ref }: { ref?: Ref }) { const [open, setOpen] = useState(false); const [visualization, setVisualization] = useState(true); + const skipYamlSyncRef = useRef(false); const [values, setValues] = useState<{ enable: boolean; listen: string; @@ -150,304 +243,91 @@ export function DnsViewer({ ref }: { ref?: Ref }) { }); // 用于YAML编辑模式 - const [yamlContent, setYamlContent] = useState(""); - - useImperativeHandle(ref, () => ({ - open: () => { - setOpen(true); - // 获取DNS配置文件并初始化表单 - initDnsConfig(); - }, - close: () => setOpen(false), - })); - - // 初始化DNS配置 - const initDnsConfig = async () => { - try { - // 尝试从dns_config.yaml文件读取配置 - const dnsConfigExists = await invoke( - "check_dns_config_exists", - {}, - ); - - if (dnsConfigExists) { - // 如果存在配置文件,加载其内容 - const dnsConfig = await invoke("get_dns_config_content", {}); - const config = yaml.load(dnsConfig) as any; - - // 更新表单数据 - updateValuesFromConfig(config); - // 更新YAML编辑器内容 - setYamlContent(dnsConfig); - } else { - // 如果不存在配置文件,使用默认值 - resetToDefaults(); - } - } catch (err) { - console.error("Failed to initialize DNS config", err); - resetToDefaults(); - } - }; + const [yamlContent, setYamlContent] = useReducer( + (_: string, next: string) => next, + "", + ); // 从配置对象更新表单值 - const updateValuesFromConfig = (config: any) => { - if (!config) return; + const updateValuesFromConfig = useCallback( + (config: any) => { + if (!config) return; - // 提取dns配置 - const dnsConfig = config.dns || {}; - // 提取hosts配置(与dns同级) - const hostsConfig = config.hosts || {}; + const dnsConfig = config.dns || {}; + const hostsConfig = config.hosts || {}; - const enhancedMode = - dnsConfig["enhanced-mode"] || DEFAULT_DNS_CONFIG["enhanced-mode"]; - const validEnhancedMode = - enhancedMode === "fake-ip" || enhancedMode === "redir-host" - ? enhancedMode - : DEFAULT_DNS_CONFIG["enhanced-mode"]; + const enhancedMode = + dnsConfig["enhanced-mode"] || DEFAULT_DNS_CONFIG["enhanced-mode"]; + const validEnhancedMode = + enhancedMode === "fake-ip" || enhancedMode === "redir-host" + ? enhancedMode + : DEFAULT_DNS_CONFIG["enhanced-mode"]; - const fakeIpFilterMode = - dnsConfig["fake-ip-filter-mode"] || - DEFAULT_DNS_CONFIG["fake-ip-filter-mode"]; - const validFakeIpFilterMode = - fakeIpFilterMode === "blacklist" || fakeIpFilterMode === "whitelist" - ? fakeIpFilterMode - : DEFAULT_DNS_CONFIG["fake-ip-filter-mode"]; + const fakeIpFilterMode = + dnsConfig["fake-ip-filter-mode"] || + DEFAULT_DNS_CONFIG["fake-ip-filter-mode"]; + const validFakeIpFilterMode = + fakeIpFilterMode === "blacklist" || fakeIpFilterMode === "whitelist" + ? fakeIpFilterMode + : DEFAULT_DNS_CONFIG["fake-ip-filter-mode"]; - setValues({ - enable: dnsConfig.enable ?? DEFAULT_DNS_CONFIG.enable, - listen: dnsConfig.listen ?? DEFAULT_DNS_CONFIG.listen, - enhancedMode: validEnhancedMode, - fakeIpRange: - dnsConfig["fake-ip-range"] ?? DEFAULT_DNS_CONFIG["fake-ip-range"], - fakeIpFilterMode: validFakeIpFilterMode, - preferH3: dnsConfig["prefer-h3"] ?? DEFAULT_DNS_CONFIG["prefer-h3"], - respectRules: - dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"], - useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"], - useSystemHosts: - dnsConfig["use-system-hosts"] ?? DEFAULT_DNS_CONFIG["use-system-hosts"], - ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6, - fakeIpFilter: - dnsConfig["fake-ip-filter"]?.join(", ") ?? - DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "), - nameserver: - dnsConfig.nameserver?.join(", ") ?? - DEFAULT_DNS_CONFIG.nameserver.join(", "), - fallback: - dnsConfig.fallback?.join(", ") ?? - DEFAULT_DNS_CONFIG.fallback.join(", "), - defaultNameserver: - dnsConfig["default-nameserver"]?.join(", ") ?? - DEFAULT_DNS_CONFIG["default-nameserver"].join(", "), - proxyServerNameserver: - dnsConfig["proxy-server-nameserver"]?.join(", ") ?? - (DEFAULT_DNS_CONFIG["proxy-server-nameserver"]?.join(", ") || ""), - directNameserver: - dnsConfig["direct-nameserver"]?.join(", ") ?? - (DEFAULT_DNS_CONFIG["direct-nameserver"]?.join(", ") || ""), - directNameserverFollowPolicy: - dnsConfig["direct-nameserver-follow-policy"] ?? - DEFAULT_DNS_CONFIG["direct-nameserver-follow-policy"], - fallbackGeoip: - dnsConfig["fallback-filter"]?.geoip ?? - DEFAULT_DNS_CONFIG["fallback-filter"].geoip, - fallbackGeoipCode: - dnsConfig["fallback-filter"]?.["geoip-code"] ?? - DEFAULT_DNS_CONFIG["fallback-filter"]["geoip-code"], - fallbackIpcidr: - dnsConfig["fallback-filter"]?.ipcidr?.join(", ") ?? - DEFAULT_DNS_CONFIG["fallback-filter"].ipcidr.join(", "), - fallbackDomain: - dnsConfig["fallback-filter"]?.domain?.join(", ") ?? - DEFAULT_DNS_CONFIG["fallback-filter"].domain.join(", "), - nameserverPolicy: - formatNameserverPolicy(dnsConfig["nameserver-policy"]) || "", - hosts: formatHosts(hostsConfig) || "", - }); - }; - - // 重置为默认值 - const resetToDefaults = () => { - setValues({ - enable: DEFAULT_DNS_CONFIG.enable, - listen: DEFAULT_DNS_CONFIG.listen, - enhancedMode: DEFAULT_DNS_CONFIG["enhanced-mode"], - fakeIpRange: DEFAULT_DNS_CONFIG["fake-ip-range"], - fakeIpFilterMode: DEFAULT_DNS_CONFIG["fake-ip-filter-mode"], - preferH3: DEFAULT_DNS_CONFIG["prefer-h3"], - respectRules: DEFAULT_DNS_CONFIG["respect-rules"], - useHosts: DEFAULT_DNS_CONFIG["use-hosts"], - useSystemHosts: DEFAULT_DNS_CONFIG["use-system-hosts"], - ipv6: DEFAULT_DNS_CONFIG.ipv6, - fakeIpFilter: DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "), - defaultNameserver: DEFAULT_DNS_CONFIG["default-nameserver"].join(", "), - nameserver: DEFAULT_DNS_CONFIG.nameserver.join(", "), - fallback: DEFAULT_DNS_CONFIG.fallback.join(", "), - proxyServerNameserver: - DEFAULT_DNS_CONFIG["proxy-server-nameserver"]?.join(", ") || "", - directNameserver: - DEFAULT_DNS_CONFIG["direct-nameserver"]?.join(", ") || "", - directNameserverFollowPolicy: - DEFAULT_DNS_CONFIG["direct-nameserver-follow-policy"] || false, - fallbackGeoip: DEFAULT_DNS_CONFIG["fallback-filter"].geoip, - fallbackGeoipCode: DEFAULT_DNS_CONFIG["fallback-filter"]["geoip-code"], - fallbackIpcidr: - DEFAULT_DNS_CONFIG["fallback-filter"].ipcidr?.join(", ") || "", - fallbackDomain: - DEFAULT_DNS_CONFIG["fallback-filter"].domain?.join(", ") || "", - nameserverPolicy: "", - hosts: "", - }); - - // 更新YAML编辑器内容 - updateYamlFromValues(); - }; - - // 从表单值更新YAML内容 - const updateYamlFromValues = () => { - const config: Record = {}; - - const dnsConfig = generateDnsConfig(); - if (Object.keys(dnsConfig).length > 0) { - config.dns = dnsConfig; - } - - const hosts = parseHosts(values.hosts); - if (Object.keys(hosts).length > 0) { - config.hosts = hosts; - } - - setYamlContent(yaml.dump(config, { forceQuotes: true })); - }; - - // 从YAML更新表单值 - const updateValuesFromYaml = () => { - try { - const parsedYaml = yaml.load(yamlContent) as any; - if (!parsedYaml) return; - - updateValuesFromConfig(parsedYaml); - } catch { - showNotice("error", t("Invalid YAML format")); - } - }; - - // 解析nameserver-policy为对象 - const parseNameserverPolicy = (str: string): Record => { - const result: Record = {}; - if (!str) return result; - - // 处理geosite:xxx,yyy格式 - const ruleRegex = /\s*([^=]+?)\s*=\s*([^,]+)(?:,|$)/g; - let match; - - while ((match = ruleRegex.exec(str)) !== null) { - const [, domainsPart, serversPart] = match; - - // 处理域名部分 - let domains; - if (domainsPart.startsWith("geosite:")) { - domains = [domainsPart.trim()]; - } else { - domains = [domainsPart.trim()]; - } - - // 处理服务器部分 - const servers = serversPart.split(";").map((s) => s.trim()); - - // 为每个域名组分配相同的服务器列表 - domains.forEach((domain) => { - result[domain] = servers; + setValues({ + enable: dnsConfig.enable ?? DEFAULT_DNS_CONFIG.enable, + listen: dnsConfig.listen ?? DEFAULT_DNS_CONFIG.listen, + enhancedMode: validEnhancedMode, + fakeIpRange: + dnsConfig["fake-ip-range"] ?? DEFAULT_DNS_CONFIG["fake-ip-range"], + fakeIpFilterMode: validFakeIpFilterMode, + preferH3: dnsConfig["prefer-h3"] ?? DEFAULT_DNS_CONFIG["prefer-h3"], + respectRules: + dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"], + useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"], + useSystemHosts: + dnsConfig["use-system-hosts"] ?? + DEFAULT_DNS_CONFIG["use-system-hosts"], + ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6, + fakeIpFilter: + dnsConfig["fake-ip-filter"]?.join(", ") ?? + DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "), + nameserver: + dnsConfig.nameserver?.join(", ") ?? + DEFAULT_DNS_CONFIG.nameserver.join(", "), + fallback: + dnsConfig.fallback?.join(", ") ?? + DEFAULT_DNS_CONFIG.fallback.join(", "), + defaultNameserver: + dnsConfig["default-nameserver"]?.join(", ") ?? + DEFAULT_DNS_CONFIG["default-nameserver"].join(", "), + proxyServerNameserver: + dnsConfig["proxy-server-nameserver"]?.join(", ") ?? + (DEFAULT_DNS_CONFIG["proxy-server-nameserver"]?.join(", ") || ""), + directNameserver: + dnsConfig["direct-nameserver"]?.join(", ") ?? + (DEFAULT_DNS_CONFIG["direct-nameserver"]?.join(", ") || ""), + directNameserverFollowPolicy: + dnsConfig["direct-nameserver-follow-policy"] ?? + DEFAULT_DNS_CONFIG["direct-nameserver-follow-policy"], + fallbackGeoip: + dnsConfig["fallback-filter"]?.geoip ?? + DEFAULT_DNS_CONFIG["fallback-filter"].geoip, + fallbackGeoipCode: + dnsConfig["fallback-filter"]?.["geoip-code"] ?? + DEFAULT_DNS_CONFIG["fallback-filter"]["geoip-code"], + fallbackIpcidr: + dnsConfig["fallback-filter"]?.ipcidr?.join(", ") ?? + DEFAULT_DNS_CONFIG["fallback-filter"].ipcidr.join(", "), + fallbackDomain: + dnsConfig["fallback-filter"]?.domain?.join(", ") ?? + DEFAULT_DNS_CONFIG["fallback-filter"].domain.join(", "), + nameserverPolicy: + formatNameserverPolicy(dnsConfig["nameserver-policy"]) || "", + hosts: formatHosts(hostsConfig) || "", }); - } + }, + [setValues], + ); - return result; - }; - - // 格式化nameserver-policy为字符串 - const formatNameserverPolicy = (policy: any): string => { - if (!policy || typeof policy !== "object") return ""; - - // 直接将对象转换为字符串格式 - return Object.entries(policy) - .map(([domain, servers]) => { - const serversStr = Array.isArray(servers) ? servers.join(";") : servers; - return `${domain}=${serversStr}`; - }) - .join(", "); - }; - - // 格式化hosts为字符串 - const formatHosts = (hosts: any): string => { - if (!hosts || typeof hosts !== "object") return ""; - - const result: string[] = []; - - Object.entries(hosts).forEach(([domain, value]) => { - if (Array.isArray(value)) { - // 处理数组格式的IP - const ipsStr = value.join(";"); - result.push(`${domain}=${ipsStr}`); - } else { - // 处理单个IP或域名 - result.push(`${domain}=${value}`); - } - }); - - return result.join(", "); - }; - - // 解析hosts字符串为对象 - const parseHosts = (str: string): Record => { - const result: Record = {}; - if (!str) return result; - - str.split(",").forEach((item) => { - const parts = item.trim().split("="); - if (parts.length < 2) return; - - const domain = parts[0].trim(); - const valueStr = parts.slice(1).join("=").trim(); - - // 检查是否包含多个分号分隔的IP - if (valueStr.includes(";")) { - result[domain] = valueStr - .split(";") - .map((s) => s.trim()) - .filter(Boolean); - } else { - result[domain] = valueStr; - } - }); - - return result; - }; - - // 初始化时设置默认YAML - useEffect(() => { - updateYamlFromValues(); - }, []); - - // 切换编辑模式时的处理 - useEffect(() => { - if (visualization) { - updateValuesFromYaml(); - } else { - updateYamlFromValues(); - } - }, [visualization]); - - // 解析列表字符串为数组 - const parseList = (str: string): string[] => { - if (!str?.trim()) return []; - return str - .split(",") - .map((item) => item.trim()) - .filter(Boolean); - }; - - // 生成DNS配置对象 - const generateDnsConfig = () => { + const generateDnsConfig = useCallback(() => { const dnsConfig: any = { enable: values.enable, listen: values.listen, @@ -481,8 +361,132 @@ export function DnsViewer({ ref }: { ref?: Ref }) { } return dnsConfig; - }; + }, [values]); + const updateYamlFromValues = useCallback(() => { + const config: Record = {}; + + const dnsConfig = generateDnsConfig(); + if (Object.keys(dnsConfig).length > 0) { + config.dns = dnsConfig; + } + + const hosts = parseHosts(values.hosts); + if (Object.keys(hosts).length > 0) { + config.hosts = hosts; + } + + setYamlContent(yaml.dump(config, { forceQuotes: true })); + }, [generateDnsConfig, setYamlContent, values.hosts]); + + // 重置为默认值 + const resetToDefaults = useCallback(() => { + setValues({ + enable: DEFAULT_DNS_CONFIG.enable, + listen: DEFAULT_DNS_CONFIG.listen, + enhancedMode: DEFAULT_DNS_CONFIG["enhanced-mode"], + fakeIpRange: DEFAULT_DNS_CONFIG["fake-ip-range"], + fakeIpFilterMode: DEFAULT_DNS_CONFIG["fake-ip-filter-mode"], + preferH3: DEFAULT_DNS_CONFIG["prefer-h3"], + respectRules: DEFAULT_DNS_CONFIG["respect-rules"], + useHosts: DEFAULT_DNS_CONFIG["use-hosts"], + useSystemHosts: DEFAULT_DNS_CONFIG["use-system-hosts"], + ipv6: DEFAULT_DNS_CONFIG.ipv6, + fakeIpFilter: DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "), + defaultNameserver: DEFAULT_DNS_CONFIG["default-nameserver"].join(", "), + nameserver: DEFAULT_DNS_CONFIG.nameserver.join(", "), + fallback: DEFAULT_DNS_CONFIG.fallback.join(", "), + proxyServerNameserver: + DEFAULT_DNS_CONFIG["proxy-server-nameserver"]?.join(", ") || "", + directNameserver: + DEFAULT_DNS_CONFIG["direct-nameserver"]?.join(", ") || "", + directNameserverFollowPolicy: + DEFAULT_DNS_CONFIG["direct-nameserver-follow-policy"] || false, + fallbackGeoip: DEFAULT_DNS_CONFIG["fallback-filter"].geoip, + fallbackGeoipCode: DEFAULT_DNS_CONFIG["fallback-filter"]["geoip-code"], + fallbackIpcidr: + DEFAULT_DNS_CONFIG["fallback-filter"].ipcidr?.join(", ") || "", + fallbackDomain: + DEFAULT_DNS_CONFIG["fallback-filter"].domain?.join(", ") || "", + nameserverPolicy: "", + hosts: "", + }); + + updateYamlFromValues(); + }, [setValues, updateYamlFromValues]); + + // 从YAML更新表单值 + const updateValuesFromYaml = useCallback(() => { + try { + const parsedYaml = yaml.load(yamlContent) as any; + if (!parsedYaml) return; + + skipYamlSyncRef.current = true; + updateValuesFromConfig(parsedYaml); + } catch { + showNotice("error", t("Invalid YAML format")); + } + }, [yamlContent, t, updateValuesFromConfig]); + + useEffect(() => { + if (skipYamlSyncRef.current) { + skipYamlSyncRef.current = false; + return; + } + updateYamlFromValues(); + }, [updateYamlFromValues]); + + const latestUpdateValuesFromYamlRef = useRef(updateValuesFromYaml); + const latestUpdateYamlFromValuesRef = useRef(updateYamlFromValues); + + useEffect(() => { + latestUpdateValuesFromYamlRef.current = updateValuesFromYaml; + latestUpdateYamlFromValuesRef.current = updateYamlFromValues; + }, [updateValuesFromYaml, updateYamlFromValues]); + + useEffect(() => { + if (visualization) { + latestUpdateValuesFromYamlRef.current(); + } else { + latestUpdateYamlFromValuesRef.current(); + } + }, [visualization]); + + const initDnsConfig = useCallback(async () => { + try { + const dnsConfigExists = await invoke( + "check_dns_config_exists", + {}, + ); + + if (dnsConfigExists) { + const dnsConfig = await invoke("get_dns_config_content", {}); + const config = yaml.load(dnsConfig) as any; + + updateValuesFromConfig(config); + setYamlContent(dnsConfig); + } else { + resetToDefaults(); + } + } catch (err) { + console.error("Failed to initialize DNS config", err); + resetToDefaults(); + } + }, [resetToDefaults, setYamlContent, updateValuesFromConfig]); + + useImperativeHandle( + ref, + () => ({ + open: () => { + setOpen(true); + void initDnsConfig(); + }, + close: () => setOpen(false), + }), + [initDnsConfig], + ); + + // 生成DNS配置对象 // 处理保存操作 const onSave = useLockFn(async () => { try { diff --git a/src/components/setting/mods/external-controller-cors.tsx b/src/components/setting/mods/external-controller-cors.tsx index d2522f35..440274b7 100644 --- a/src/components/setting/mods/external-controller-cors.tsx +++ b/src/components/setting/mods/external-controller-cors.tsx @@ -1,7 +1,7 @@ import { Delete as DeleteIcon } from "@mui/icons-material"; import { Box, Button, Divider, List, ListItem, TextField } from "@mui/material"; import { useLockFn, useRequest } from "ahooks"; -import { forwardRef, useImperativeHandle, useState } from "react"; +import { forwardRef, useImperativeHandle, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { BaseDialog, Switch } from "@/components/base"; @@ -165,6 +165,19 @@ export const HeaderConfiguration = forwardRef( await saveConfig(); }); + const originEntries = useMemo(() => { + const counts: Record = {}; + return corsConfig.allowOrigins.map((origin, index) => { + const occurrence = (counts[origin] = (counts[origin] ?? 0) + 1); + const keyBase = origin || "origin"; + return { + origin, + index, + key: `${keyBase}-${occurrence}`, + }; + }); + }, [corsConfig.allowOrigins]); + return ( (
{t("Allowed Origins")}
- {corsConfig.allowOrigins.map((origin, index) => ( + {originEntries.map(({ origin, index, key }) => (
(props: Props) { onGuard = noop, onCatch = noop, onChange = noop, - onFormat = (v: T) => v, + onFormat, } = props; const lockRef = useRef(false); @@ -45,7 +45,7 @@ export function GuardState(props: Props) { lockRef.current = true; try { - const newValue = (onFormat as any)(...args); + const newValue = onFormat ? (onFormat as any)(...args) : (args[0] as T); // 先在ui上响应操作 onChange(newValue); @@ -81,5 +81,7 @@ export function GuardState(props: Props) { } lockRef.current = false; }; - return cloneElement(children, childProps); + const { children: nestedChildren, ...restProps } = childProps; + + return createElement(children.type, restProps, nestedChildren); } diff --git a/src/components/setting/mods/hotkey-viewer.tsx b/src/components/setting/mods/hotkey-viewer.tsx index cd09e244..9f0e7280 100644 --- a/src/components/setting/mods/hotkey-viewer.tsx +++ b/src/components/setting/mods/hotkey-viewer.tsx @@ -33,7 +33,7 @@ export const HotkeyViewer = forwardRef((props, ref) => { const { verge, patchVerge } = useVerge(); const [hotkeyMap, setHotkeyMap] = useState>({}); - const [enableGlobalHotkey, setEnableHotkey] = useState( + const [enableGlobalHotkey, setEnableGlobalHotkey] = useState( verge?.enable_global_hotkey ?? true, ); @@ -102,7 +102,7 @@ export const HotkeyViewer = forwardRef((props, ref) => { setEnableHotkey(e.target.checked)} + onChange={(e) => setEnableGlobalHotkey(e.target.checked)} /> diff --git a/src/components/setting/mods/layout-viewer.tsx b/src/components/setting/mods/layout-viewer.tsx index 3b7333c4..980f161c 100644 --- a/src/components/setting/mods/layout-viewer.tsx +++ b/src/components/setting/mods/layout-viewer.tsx @@ -118,7 +118,7 @@ export const LayoutViewer = forwardRef((props, ref) => { valueProps="checked" onCatch={onError} onFormat={onSwitchFormat} - onChange={async (e) => { + onChange={async () => { await toggleDecorations(); }} > @@ -198,8 +198,8 @@ export const LayoutViewer = forwardRef((props, ref) => { value={verge?.menu_icon ?? "monochrome"} onCatch={onError} onFormat={(e: any) => e.target.value} - onChange={(e) => onChangeData({ menu_icon: e })} - onGuard={(e) => patchVerge({ menu_icon: e })} + onChange={(value) => onChangeData({ menu_icon: value })} + onGuard={(value) => patchVerge({ menu_icon: value })} >