76 lines
5.5 KiB
TypeScript
76 lines
5.5 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
||
import { Cpu, Database, FolderCog, Globe2, HardDrive, Info, ShieldCheck } from "lucide-react";
|
||
import type { LucideIcon } from "lucide-react";
|
||
import type { ReactNode } from "react";
|
||
import type { AppInfo, Catalog } from "@shared/types";
|
||
|
||
type SettingsTab = "about" | "catalog" | "execution" | "browser";
|
||
|
||
export function SettingsPage({ catalog }: { catalog: Catalog }) {
|
||
const [tab, setTab] = useState<SettingsTab>("about");
|
||
const [info, setInfo] = useState<AppInfo | null>(null);
|
||
const [handlers, setHandlers] = useState<{ count: number; ids: string[] } | null>(null);
|
||
|
||
useEffect(() => {
|
||
void window.rpa.app.info().then(setInfo).catch(() => setInfo(null));
|
||
void window.rpa.run.capabilities().then(setHandlers).catch(() => setHandlers(null));
|
||
}, []);
|
||
|
||
const executable = catalog.components.filter((item) => item.implemented).length;
|
||
const groups = useMemo(() => catalog.groups.map((group) => {
|
||
const components = catalog.components.filter((component) => component.group === group.id);
|
||
const connected = components.filter((component) => component.implemented).length;
|
||
return { ...group, connected, pending: components.length - connected };
|
||
}), [catalog]);
|
||
|
||
return (
|
||
<div className="settings">
|
||
<aside className="panel setting-nav">
|
||
<button type="button" className={tab === "about" ? "active" : ""} onClick={() => setTab("about")}>关于与运行环境</button>
|
||
<button type="button" className={tab === "catalog" ? "active" : ""} onClick={() => setTab("catalog")}>组件目录</button>
|
||
<button type="button" className={tab === "execution" ? "active" : ""} onClick={() => setTab("execution")}>执行与证据</button>
|
||
<button type="button" className={tab === "browser" ? "active" : ""} onClick={() => setTab("browser")}>浏览器白名单</button>
|
||
</aside>
|
||
|
||
<section className="panel setting-body">
|
||
{tab === "about" && (
|
||
<>
|
||
<SettingCard icon={Info} title="版本与运行环境">
|
||
<dl className="kv"><dt>应用版本</dt><dd>{info?.version || "0.1.0"}</dd><dt>Electron / Chromium</dt><dd>{info?.electron || "-"} / {info?.chrome || "-"}</dd><dt>Node</dt><dd>{info?.node || "-"}</dd><dt>运行模式</dt><dd>{info?.packaged ? "打包运行(离线)" : "开发模式"}</dd><dt>数据目录</dt><dd>{info?.userData || "-"}</dd><dt>目录文件</dt><dd>{info?.catalogPath || "-"}</dd></dl>
|
||
</SettingCard>
|
||
<SettingCard icon={FolderCog} title="产品边界"><p className="field-hint setting-copy">组件元数据来自项目内 1016 项目录。移动端、机械臂、U 盾、远程桌面、SAP/IE 专用通道、云服务、数据库和 FTP 保持待接入,不随当前版本承诺执行。</p></SettingCard>
|
||
</>
|
||
)}
|
||
|
||
{tab === "catalog" && (
|
||
<>
|
||
<SettingCard icon={Database} title="组件接入概览">
|
||
<dl className="kv"><dt>目录总数</dt><dd>{catalog.counts.total} 项</dd><dt>已接入执行器</dt><dd>{executable} 项({((executable / catalog.counts.total) * 100).toFixed(1)}%)</dd><dt>待接入</dt><dd>{catalog.counts.total - executable} 项</dd><dt>注册处理器 ID</dt><dd>{handlers?.count ?? "—"}</dd></dl>
|
||
</SettingCard>
|
||
<div className="setting-card"><h3><Database className="icon" />分类覆盖</h3><div className="setting-group-table">{groups.map((group) => <div key={group.id}><strong>{group.label}</strong><span>{group.connected} 已接入</span><span>{group.pending} 待接入</span><progress max={group.count} value={group.connected} /></div>)}</div></div>
|
||
</>
|
||
)}
|
||
|
||
{tab === "execution" && (
|
||
<>
|
||
<SettingCard icon={Cpu} title="执行方式"><dl className="kv"><dt>执行位置</dt><dd>本机主进程</dd><dt>浏览器</dt><dd>独立 Electron 运行窗口 + CDP</dd><dt>桌面</dt><dd>PowerShell / user32 本地桥</dd><dt>Office</dt><dd>ExcelJS / PDF.js / pdf-lib / Word COM</dd><dt>运行前检查</dt><dd>待接入、必填、多出口、断链</dd></dl></SettingCard>
|
||
<SettingCard icon={HardDrive} title="数据与证据"><p className="field-hint setting-copy">流程保存到数据目录的 flows/;运行结果、错误和截图写入 runs/<runId>/。任务列表会读取最近一次运行的状态、时间、耗时和失败原因。</p></SettingCard>
|
||
<SettingCard icon={ShieldCheck} title="安全边界"><p className="field-hint setting-copy">运行期默认离线。桌面动作只在流程显式运行时执行;未接入能力不会静默成功,而是在运行前检查或节点执行时返回明确错误。</p></SettingCard>
|
||
</>
|
||
)}
|
||
|
||
{tab === "browser" && (
|
||
<>
|
||
<SettingCard icon={Globe2} title="浏览器域名白名单"><dl className="kv"><dt>默认允许</dt><dd>127.0.0.1、localhost、example.com</dd><dt>扩展方式</dt><dd>环境变量 RPA_ALLOWED_DOMAINS</dd><dt>格式</dt><dd>逗号分隔域名,不含协议和路径</dd></dl></SettingCard>
|
||
<SettingCard icon={ShieldCheck} title="限制说明"><p className="field-hint setting-copy">导航只允许 HTTP/HTTPS。未列入白名单的域名会返回 blocked_url;文件协议、脚本协议和任意本地路径不会由浏览器导航组件打开。</p></SettingCard>
|
||
</>
|
||
)}
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function SettingCard({ icon: Icon, title, children }: { icon: LucideIcon; title: string; children: ReactNode }) {
|
||
return <div className="setting-card"><h3><Icon className="icon" />{title}</h3>{children}</div>;
|
||
}
|