Initial commit: NetMesh smart ops platform with 3D topology, inspection reports, RCA and AI ops assistant
Some checks failed
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled

This commit is contained in:
2026-10-15 13:53:04 +08:00
commit 2f9f8da48e
3299 changed files with 926841 additions and 0 deletions

View File

@@ -0,0 +1,248 @@
import React, { useEffect, useState } from "react";
import { ArrowUpCircle, Check, Loader2, RefreshCcw } from "lucide-react";
import AppLogo from "./AppLogo";
import AppWordmark from "./AppWordmark";
import { Button } from "./ui/button";
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";
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<AppInfo>({ name: "Netcatty", version: "" });
const [lastCheckResult, setLastCheckResult] = useState<'none' | 'available' | 'upToDate'>('none');
const [monitoringTitle, setMonitoringTitle] = useStoredString<string>(STORAGE_KEY_MONITORING_DASHBOARD_TITLE, '云启Net智能巡检监测系统');
const [reportProductName, setReportProductName] = useStoredString<string>(STORAGE_KEY_REPORT_PRODUCT_NAME, '');
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);
};
return (
<SettingsTabContent value="application">
<div className="flex flex-col lg:flex-row gap-10 lg:gap-14">
<div className="lg:w-[320px] shrink-0">
<div className="flex items-center gap-4">
<AppLogo className="w-16 h-16" />
<div>
<AppWordmark accessibleLabel={productName} text={productName} className="h-8 w-auto text-foreground" />
<div className="flex items-center gap-2 mt-1">
<span className="text-sm text-muted-foreground">
{appInfo.version ? appInfo.version : " "}
</span>
{/* Update badge - reflects auto-download state */}
{updateState.latestRelease && (updateState.hasUpdate || updateState.autoDownloadStatus === 'downloading' || updateState.autoDownloadStatus === 'ready') && (
<button
onClick={() => updateState.autoDownloadStatus === 'ready' ? installUpdate() : updateState.autoDownloadStatus === 'downloading' ? undefined : startDownload()}
className={cn(
"inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium",
updateState.autoDownloadStatus === 'ready'
? "bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 hover:bg-green-200 dark:hover:bg-green-800"
: "bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 hover:bg-blue-200 dark:hover:bg-blue-800",
"transition-colors cursor-pointer"
)}
>
<ArrowUpCircle size={12} />
v{updateState.latestRelease.version}{' '}
{updateState.autoDownloadStatus === 'ready'
? t('update.restartNow')
: updateState.autoDownloadStatus === 'downloading'
? `${updateState.downloadPercent}%`
: t('update.downloadNow')}
</button>
)}
</div>
</div>
</div>
{/* 检查更新按钮已隐藏 */}
{/* 产品名称配置 —— 本地 state 缓冲输入,blur/Enter 才提交,避免每次按键都触发全局持久化 + IPC 导致闪动 */}
<SettingsAnchor anchorId="application-product-name" className="mt-6">
<ProductNameInput value={productName} onChange={setProductName} />
</SettingsAnchor>
{/* 大屏名称配置 —— 同样用本地 state 缓冲,避免每次按键都触发 localStorage 写入和事件广播 */}
<SettingsAnchor anchorId="application-monitoring-title" className="mt-6">
<SettingsInputBuffered
value={monitoringTitle}
placeholder="云启Net智能巡检监测系统"
label="大屏标题"
hint="3D 拓扑监控大屏页面顶部显示的系统标题(回车或失焦后生效)"
onCommit={(v) => {
setMonitoringTitle(v);
try { window.dispatchEvent(new CustomEvent('monitoringTitleChanged')); } catch {}
}}
/>
</SettingsAnchor>
{/* 报告产品名配置 —— 专门用于 PDF 巡检报告标题 */}
<SettingsAnchor anchorId="application-report-product-name" className="mt-6">
<SettingsInputBuffered
value={reportProductName}
placeholder={productName}
label="报告产品名"
hint="巡检报告 PDF 首页显示的产品名称(回车或失焦后生效)。留空则使用上方「产品名称」。"
trim
onCommit={(v) => setReportProductName(v)}
/>
</SettingsAnchor>
</div>
</div>
</SettingsTabContent>
);
}
/**
* 带本地 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 (
<div className="space-y-1.5">
<label className="text-sm font-medium text-foreground">{label}</label>
<input
type="text"
value={draft}
onChange={(e) => 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 && <p className="text-xs text-muted-foreground">{hint}</p>}
</div>
);
}
/**
* 产品名称输入框 —— SettingsInputBuffered + trim 空白。
*/
function ProductNameInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
return (
<SettingsInputBuffered
value={value}
onCommit={onChange}
placeholder="云启智能运维平台"
label="产品名称"
hint="用于窗口标题、侧边栏和设置页面(回车或失焦后生效)"
trim
/>
);
}