import React, { useEffect, useState } from "react"; import { ArrowUpCircle, Check, Copy, Globe, Loader2, RefreshCcw, Server } from "lucide-react"; import AppLogo from "./AppLogo"; import AppWordmark from "./AppWordmark"; import { Button } from "./ui/button"; import { Switch } from "./ui/switch"; import { useApplicationBackend } from "../application/state/useApplicationBackend"; import type { UpdateState, UseUpdateCheckResult } from "../application/state/useUpdateCheck"; import { useI18n } from "../application/i18n/I18nProvider"; import { SettingsAnchor, SettingsTabContent } from "./settings/settings-ui"; import { toast } from "./ui/toast"; import { useStoredString } from "../application/state/useStoredString"; import { STORAGE_KEY_MONITORING_DASHBOARD_TITLE, STORAGE_KEY_REPORT_PRODUCT_NAME } from "../infrastructure/config/storageKeys"; import { cn } from "../lib/utils"; type AppInfo = { name: string; version: string; platform?: string; }; interface SettingsApplicationTabProps { updateState: UpdateState; checkNow: UseUpdateCheckResult['checkNow']; openReleasePage: UseUpdateCheckResult['openReleasePage']; installUpdate: UseUpdateCheckResult['installUpdate']; startDownload: UseUpdateCheckResult['startDownload']; isUpdateDemoMode: boolean; productName: string; setProductName: (value: string) => void; } export default function SettingsApplicationTab({ updateState, checkNow, openReleasePage, installUpdate, startDownload, isUpdateDemoMode, productName, setProductName }: SettingsApplicationTabProps) { const { t } = useI18n(); const { openExternal, getApplicationInfo } = useApplicationBackend(); const [appInfo, setAppInfo] = useState({ name: "Netcatty", version: "" }); const [lastCheckResult, setLastCheckResult] = useState<'none' | 'available' | 'upToDate'>('none'); const [monitoringTitle, setMonitoringTitle] = useStoredString(STORAGE_KEY_MONITORING_DASHBOARD_TITLE, '云启Net智能巡检监测系统'); const [reportProductName, setReportProductName] = useStoredString(STORAGE_KEY_REPORT_PRODUCT_NAME, ''); // Electron 本地:启动时从主进程恢复 dashboardTitle / reportProductName // 解决绿色便携版首次启动 Local Storage 空时显示旧默认值的问题 useEffect(() => { const IS_REMOTE = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__; if (IS_REMOTE) return; const nc = (window as any).netcatty; if (!nc?.inspection) return; // 恢复大屏标题(如果 Local Storage 空或用的是默认值) nc.inspection.getDashboardTitle?.().then((v: string) => { if (v && v.length > 0 && v !== '云启Net智能巡检监测系统') { const local = localStorage.getItem(STORAGE_KEY_MONITORING_DASHBOARD_TITLE); if (!local || local === '云启Net智能巡检监测系统') { setMonitoringTitle(v); console.info('[Settings] restored monitoringTitle:', v); } } }).catch(() => {}); // 恢复报告产品名 nc.inspection.getReportProductName?.().then((v: string) => { if (v && v.length > 0) { const local = localStorage.getItem(STORAGE_KEY_REPORT_PRODUCT_NAME); if (!local || local === '') { setReportProductName(v); console.info('[Settings] restored reportProductName:', v); } } }).catch(() => {}); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 同步到主进程 inspection-data.json(远程浏览器 API 可读) useEffect(() => { try { const nc = (window as any).netcatty; if (monitoringTitle) nc?.inspection?.saveDashboardTitle?.(monitoringTitle).catch(() => {}); nc?.inspection?.saveReportProductName?.(reportProductName || '').catch(() => {}); } catch {} }, [monitoringTitle, reportProductName]); // 远程 HTTP Server 状态 const [remoteStatus, setRemoteStatus] = useState<{ running: boolean; port: number; token: string | null; url: string | null }>({ running: false, port: 8848, token: null, url: null, }); const [remoteLoading, setRemoteLoading] = useState(false); const [remotePortDraft, setRemotePortDraft] = useState("8848"); const loadRemoteStatus = async () => { try { const s = await (window as any).netcatty?.remoteHttp?.getStatus?.(); if (s) { setRemoteStatus({ running: !!s.running, port: s.port || 8848, token: s.token || null, url: s.url || null, }); setRemotePortDraft(String(s.port || 8848)); } } catch {} }; useEffect(() => { void loadRemoteStatus(); }, []); useEffect(() => { let cancelled = false; const load = async () => { try { const info = await getApplicationInfo(); if (!cancelled && info?.name && typeof info?.version === "string") { setAppInfo(info); } } catch { // Ignore: running in browser/dev without Electron bridge } }; void load(); return () => { cancelled = true; }; }, [getApplicationInfo]); const handleOpenExternal = async (url: string) => { try { await openExternal(url); } catch (err) { console.warn("[SettingsApplicationTab] openExternal failed:", err); toast.error( t("settings.application.openExternal.failedBody"), t("settings.application.openExternal.failedTitle"), ); } }; const handleCheckForUpdates = async () => { // In demo mode, allow checking even for dev builds if (!isUpdateDemoMode && (!appInfo.version || appInfo.version === '0.0.0')) { // Dev build - just open releases page openReleasePage(); return; } setLastCheckResult('none'); const result = await checkNow(); if (result?.hasUpdate && result.latestRelease) { setLastCheckResult('available'); toast.info( t('update.available.message', { version: result.latestRelease.version }), t('update.available.title') ); // Don't auto-open the release page here — checkNow() already triggers // electron-updater on supported platforms, and the Settings > System tab // shows a "Manual Download" link on unsupported platforms. } else if (result) { setLastCheckResult('upToDate'); toast.success( t('update.upToDate.message', { version: appInfo.version }), t('update.upToDate.title') ); } // Reset the result after 3 seconds setTimeout(() => setLastCheckResult('none'), 3000); }; // ─── 远程访问 handlers ─── const handleToggleRemote = async (checked: boolean) => { setRemoteLoading(true); try { const port = parseInt(remotePortDraft, 10) || 8848; const res = await (window as any).netcatty?.remoteHttp?.configure?.(checked, port); if (res?.ok) { setRemoteStatus({ running: !!checked, port: res.port || port, token: res.token || remoteStatus.token, url: res.url || null, }); setRemotePortDraft(String(res.port || port)); if (checked) toast.success("远程访问已启动"); else toast.success("远程访问已停止"); } else { toast.error(res?.error || "操作失败"); await loadRemoteStatus(); } } catch (e: any) { toast.error(e?.message || "操作失败"); await loadRemoteStatus(); } finally { setRemoteLoading(false); } }; const handleCopyUrl = async () => { if (!remoteStatus.url) { toast.error("请先启用远程访问"); return; } try { await navigator.clipboard.writeText(remoteStatus.url); toast.success("访问链接已复制"); } catch { toast.error("复制失败"); } }; return (
{appInfo.version ? appInfo.version : " "} {/* Update badge - reflects auto-download state */} {updateState.latestRelease && (updateState.hasUpdate || updateState.autoDownloadStatus === 'downloading' || updateState.autoDownloadStatus === 'ready') && ( )}
{/* 检查更新按钮已隐藏 */} {/* 产品名称配置 —— 本地 state 缓冲输入,blur/Enter 才提交,避免每次按键都触发全局持久化 + IPC 导致闪动 */} {/* 大屏名称配置 —— 同样用本地 state 缓冲,避免每次按键都触发 localStorage 写入和事件广播 */} { setMonitoringTitle(v); try { window.dispatchEvent(new CustomEvent('monitoringTitleChanged')); } catch {} }} /> {/* 报告产品名配置 —— 专门用于 PDF 巡检报告标题 */} setReportProductName(v)} /> {/* 远程访问(浏览器 HTTP Server) */}
{remoteStatus.running ? ( 运行中 ) : ( 已停止 )}
setRemotePortDraft(e.target.value)} className="w-24 px-2 py-1 rounded-md border border-input bg-background text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring" /> (默认 8848,修改后需重启远程访问生效)
{remoteStatus.running && remoteStatus.url && (
{remoteStatus.url}
)}

启用后可在同一局域网内通过浏览器访问本机数据看板(设备列表、告警、巡检快照)。 连接时需带上上方 token,建议仅在可信内网环境使用。

); } /** * 带本地 state 缓冲的设置输入框。输入过程只更新本地 draft, * 回车或失焦才调用 onCommit,避免每次按键都触发全局持久化 + IPC 导致闪动。 */ function SettingsInputBuffered({ value, onCommit, placeholder, label, hint, trim = false, }: { value: string; onCommit: (v: string) => void; placeholder?: string; label: string; hint?: string; trim?: boolean; }) { const [draft, setDraft] = useState(value); useEffect(() => { setDraft(value); }, [value]); const commit = () => { const next = trim ? draft.trim() : draft; if (next !== value) onCommit(next); }; return (
setDraft(e.target.value)} onBlur={commit} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); commit(); (e.target as HTMLInputElement).blur(); } else if (e.key === "Escape") { setDraft(value); (e.target as HTMLInputElement).blur(); } }} placeholder={placeholder} className="w-full px-3 py-2 rounded-lg border border-input bg-background text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring" /> {hint &&

{hint}

}
); } /** * 产品名称输入框 —— SettingsInputBuffered + trim 空白。 */ function ProductNameInput({ value, onChange }: { value: string; onChange: (v: string) => void }) { return ( ); }