Files
browser/platform_web/pages/PluginPageHostPage.tsx
T

112 lines
5.5 KiB
TypeScript

import { ArrowLeft, PlugZap } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { platformApiClient } from "../api/client";
import type { GamePluginResponse } from "../api/types";
import { PageFrame } from "../components/PageFrame";
import { ScumFileConfigWorkbench } from "../components/ScumFileConfigWorkbench";
import { EmptyState, ErrorState, LoadingState } from "../components/StateViews";
import type { PageComponentProps } from "../contracts/page";
import { pluginBridgeManifestContractFromResponse } from "../contracts/pluginBridge";
import { normalizeScumRouteKey, resolveScumOperationsPageContract, scumOperationsPluginId, scumOperationsRouteKey } from "../contracts/scumOperations";
import { createPluginBridgeHostContext } from "../utils/pluginBridgeHost";
type PluginPageState =
| { status: "loading" }
| { status: "error"; reason: string }
| { status: "ready"; plugin: GamePluginResponse };
interface PluginPageHostPageProps extends PageComponentProps {
initialPlugin?: GamePluginResponse;
}
export function PluginPageHostPage({ params, onNavigate, initialPlugin }: PluginPageHostPageProps) {
const pluginId = params.pluginId ?? "";
const routeKey = normalizeScumRouteKey(pluginId, params.routeKey ?? "");
const serverId = params.serverId ?? "";
const [state, setState] = useState<PluginPageState>(() => initialPlugin ? { status: "ready", plugin: initialPlugin } : { status: "loading" });
const load = useCallback(async () => {
if (!pluginId || !routeKey) {
setState({ status: "error", reason: "插件页面路由缺少 pluginId 或 routeKey。" });
return;
}
setState({ status: "loading" });
try {
const response = await platformApiClient.listGamePlugins();
const plugin = response.items.find((candidate) => candidate.id === pluginId);
setState(plugin ? { status: "ready", plugin } : { status: "error", reason: "未找到已注册的插件声明。" });
} catch (error) {
setState({ status: "error", reason: error instanceof Error ? error.message : "插件页面声明加载失败。" });
}
}, [pluginId, routeKey]);
useEffect(() => {
if (!initialPlugin) {
void load();
}
}, [initialPlugin, load]);
if (state.status === "loading") {
return <LoadingState label="正在加载插件页面声明…" />;
}
if (state.status === "error") {
return <ErrorState title="插件页面不可用" reason={state.reason} onRetry={() => void load()} />;
}
const page = state.plugin.pages.find((candidate) => candidate.key === routeKey);
if (!page) {
return <ErrorState title="插件页面不可用" reason="当前插件没有声明该 routeKey。" />;
}
const manifestContract = pluginBridgeManifestContractFromResponse(state.plugin);
const hostContext = createPluginBridgeHostContext({
plugin: manifestContract,
routeKey,
serverInstanceId: serverId || undefined,
themeTokens: { colorScheme: "dark", accentColor: "#7dd3fc" }
});
const isScumOperations = state.plugin.id === scumOperationsPluginId && routeKey === scumOperationsRouteKey;
const scumResolution = isScumOperations ? resolveScumOperationsPageContract(state.plugin, serverId) : null;
return (
<div className="console-page">
<PageFrame
kicker={`${state.plugin.name} / PLUGIN PAGE`}
title={page.title}
status={serverId ? `服务器 ${serverId}` : "未绑定服务器"}
metrics={[
{ label: "有效权限", value: String(hostContext.permissions.length), tone: hostContext.permissions.length > 0 ? "success" : "warning" },
{ label: "桥接动作", value: String(hostContext.bridgeActions.length), tone: hostContext.bridgeActions.length > 0 ? "success" : "warning" },
{ label: "页面路由", value: routeKey, tone: "neutral" }
]}
/>
<section className="console-panel" aria-label="plugin page host context">
<div className="panel-header">
<h2><PlugZap size={16} aria-hidden="true" /> 平台托管上下文</h2>
<button type="button" className="icon-command" onClick={() => onNavigate(serverId ? "serverDetail" : "plugins", serverId ? { serverId } : {})}>
<ArrowLeft size={14} aria-hidden="true" />
<span>{serverId ? "返回服务器" : "返回插件市场"}</span>
</button>
</div>
{scumResolution && !scumResolution.available && <ErrorState title="SCUM 运维声明不可用" reason={scumResolution.reason} compact />}
{scumResolution?.available && (
<div className="action-list" aria-label="SCUM operations declarations">
<span><strong>命令目录</strong> {scumResolution.contract.commands.length} 项,均来自插件 operations 页面声明</span>
<span><strong>快照目录</strong> {scumResolution.contract.snapshots.length} 项,按插件 schemaVersion 投影</span>
<span><strong>查询模板</strong> {scumResolution.contract.queryTemplates.length} 项,仅保留声明式 SQLite 模板引用</span>
</div>
)}
{!scumResolution && (
<EmptyState
title="插件页面已接入 Host Bridge"
description="当前页面只接收 manifest 声明与平台安全上下文;具体操作由对应插件页面实现。"
/>
)}
</section>
{scumResolution?.available && state.plugin.fileWorkspace && <ScumFileConfigWorkbench contract={scumResolution.contract} workspace={state.plugin.fileWorkspace} />}
</div>
);
}