feat(client): add offline digital employee overview
This commit is contained in:
@@ -28,6 +28,7 @@ import {
|
||||
ArrowLeftOutlined,
|
||||
ReloadOutlined,
|
||||
ApiOutlined,
|
||||
AppstoreOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import {
|
||||
setupService,
|
||||
@@ -57,6 +58,7 @@ import AboutPage from "./components/pages/AboutPage";
|
||||
import LogViewer from "./components/pages/LogViewer";
|
||||
import PermissionsPage from "./components/pages/PermissionsPage";
|
||||
import SessionsPage from "./components/pages/SessionsPage";
|
||||
import OverviewPage from "./components/pages/OverviewPage";
|
||||
import MCPSettings from "./components/settings/MCPSettings";
|
||||
import type { WebviewHeaderActions } from "./components/pages/SessionsPage";
|
||||
import { createLogger } from "./services/utils/rendererLog";
|
||||
@@ -104,6 +106,7 @@ export function useI18nLang(): I18nContextValue {
|
||||
// Tab 类型定义(对齐 Tauri 客户端)
|
||||
type TabKey =
|
||||
| "client"
|
||||
| "overview"
|
||||
| "sessions"
|
||||
| "mcp"
|
||||
| "settings"
|
||||
@@ -1054,6 +1057,11 @@ function App() {
|
||||
icon: <DashboardOutlined />,
|
||||
label: t("Claw.Menu.client"),
|
||||
},
|
||||
{
|
||||
key: "overview",
|
||||
icon: <AppstoreOutlined />,
|
||||
label: t("Claw.Menu.overview"),
|
||||
},
|
||||
{
|
||||
key: "sessions",
|
||||
icon: <TeamOutlined />,
|
||||
@@ -1353,6 +1361,7 @@ function App() {
|
||||
onLoginStarted={handleLoginStarted}
|
||||
/>
|
||||
)}
|
||||
{activeTab === "overview" && <OverviewPage />}
|
||||
{activeTab === "sessions" && (
|
||||
<SessionsPage
|
||||
autoOpen={sessionsAutoOpen}
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Alert, Button, Input, message } from "antd";
|
||||
import { ReloadOutlined, SaveOutlined } from "@ant-design/icons";
|
||||
import { APP_DISPLAY_NAME } from "@shared/constants";
|
||||
import { t } from "../../services/core/i18n";
|
||||
import styles from "../../styles/components/OverviewPage.module.css";
|
||||
|
||||
const OVERVIEW_URL_SETTING_KEY = "overview.digital_employee_url";
|
||||
const DEFAULT_OVERVIEW_URL = "sgrobot-digital/index.html#/digital";
|
||||
|
||||
function normalizeOverviewUrl(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return DEFAULT_OVERVIEW_URL;
|
||||
const url = new URL(trimmed, window.location.href);
|
||||
const legacySgRobotEndpoint =
|
||||
(url.hostname === "localhost" && url.port === "5173") ||
|
||||
(url.hostname === "127.0.0.1" && url.port === "42617");
|
||||
if (legacySgRobotEndpoint && url.pathname === "/_app/digital") {
|
||||
return DEFAULT_OVERVIEW_URL;
|
||||
}
|
||||
if (!["http:", "https:", "file:"].includes(url.protocol)) {
|
||||
throw new Error("Unsupported protocol");
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function OverviewPage() {
|
||||
const webviewRef = useRef<HTMLElement | null>(null);
|
||||
const [url, setUrl] = useState(DEFAULT_OVERVIEW_URL);
|
||||
const [draftUrl, setDraftUrl] = useState(DEFAULT_OVERVIEW_URL);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [userAgent, setUserAgent] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
window.electronAPI?.app
|
||||
.getVersion()
|
||||
.then((version) => {
|
||||
setUserAgent(navigator.userAgent + ` ${APP_DISPLAY_NAME}/${version}`);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
window.electronAPI?.settings
|
||||
.get(OVERVIEW_URL_SETTING_KEY)
|
||||
.then((saved) => {
|
||||
if (!mounted || typeof saved !== "string" || !saved.trim()) return;
|
||||
const normalized = normalizeOverviewUrl(saved);
|
||||
setUrl(normalized);
|
||||
setDraftUrl(normalized);
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const el = webviewRef.current as any;
|
||||
if (!el) return;
|
||||
|
||||
const handleStartLoading = () => setError(null);
|
||||
const handleFailLoad = (e: any) => {
|
||||
if (e.errorCode && e.errorCode !== -3) {
|
||||
setError(
|
||||
t(
|
||||
"Claw.Overview.loadFailed",
|
||||
e.errorDescription || t("Claw.EmbeddedWebview.unknownError"),
|
||||
String(e.errorCode),
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
el.addEventListener("did-start-loading", handleStartLoading);
|
||||
el.addEventListener("did-fail-load", handleFailLoad);
|
||||
return () => {
|
||||
el.removeEventListener("did-start-loading", handleStartLoading);
|
||||
el.removeEventListener("did-fail-load", handleFailLoad);
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
const canSave = useMemo(() => draftUrl.trim() !== url, [draftUrl, url]);
|
||||
const webviewUrl = useMemo(
|
||||
() => new URL(url, window.location.href).toString(),
|
||||
[url],
|
||||
);
|
||||
|
||||
const handleReload = useCallback(() => {
|
||||
setError(null);
|
||||
(webviewRef.current as any)?.reload?.();
|
||||
}, []);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
try {
|
||||
const normalized = normalizeOverviewUrl(draftUrl);
|
||||
await window.electronAPI?.settings.set(
|
||||
OVERVIEW_URL_SETTING_KEY,
|
||||
normalized,
|
||||
);
|
||||
setUrl(normalized);
|
||||
setDraftUrl(normalized);
|
||||
setError(null);
|
||||
message.success(t("Claw.Overview.urlSaved"));
|
||||
} catch {
|
||||
message.error(t("Claw.Overview.invalidUrl"));
|
||||
}
|
||||
}, [draftUrl]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.toolbar}>
|
||||
<span className={styles.title}>{t("Claw.Overview.title")}</span>
|
||||
<Input
|
||||
size="small"
|
||||
className={styles.urlInput}
|
||||
value={draftUrl}
|
||||
onChange={(e) => setDraftUrl(e.target.value)}
|
||||
onPressEnter={handleSave}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSave}
|
||||
disabled={!canSave}
|
||||
>
|
||||
{t("Claw.Common.save")}
|
||||
</Button>
|
||||
<Button size="small" icon={<ReloadOutlined />} onClick={handleReload}>
|
||||
{t("Claw.Common.refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
{error && (
|
||||
<Alert
|
||||
message={error}
|
||||
type="error"
|
||||
showIcon
|
||||
closable
|
||||
onClose={() => setError(null)}
|
||||
className={styles.error}
|
||||
/>
|
||||
)}
|
||||
<webview
|
||||
ref={webviewRef as any}
|
||||
src={webviewUrl}
|
||||
useragent={userAgent}
|
||||
className={styles.webview}
|
||||
allowpopups={"true" as any}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default OverviewPage;
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<link rel="icon" type="image/png" href="/icon.png" />
|
||||
<!-- 与 @shared/constants APP_DISPLAY_NAME 保持一致 -->
|
||||
<title>启明</title>
|
||||
<title>飞天</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
background: var(--color-bg-layout);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: var(--color-bg-container);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 0 0 auto;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.urlInput {
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 8px 12px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.webview {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
border: none;
|
||||
}
|
||||
Reference in New Issue
Block a user