---
title: "Panel"
description: "XBase 自带的通用网页面板，模组只描述界面并挂钩子，不写前端"
---


`include/XBase/Panel.h` · `XBase::Panel`

> XBase 自带一个 React 面板，模组把界面结构挂进去、绑上读写回调就能用。多个模组聚合到同一个面板的侧栏上，谁都不用写前端。

## 适用场景与边界

适合想给模组加设置界面、又不想维护一套前端工程的场景：描述控件、绑回调、完事。

不属于本模块的内容：

- 不渲染任何业务控件，界面内容全部来自模组的 `Mount`
- 不代管多语言，`label` 直接给显示文本，词条由模组自己决定
- 不提供面板以外的界面形态，要完全自定义的页面走 `WebView` 与 `WebBridge`
- 不做能力之外的校验，例如数值的业务合法性由模组的 `write` 回调自己判断

面板状态归共享运行时 `XBase{ver}.dll`。`Bootstrap::Attach()` 对每个模组都会先加载共享库，所以 `Panel::` 的调用全部转发到那里，各模组看到的是同一份注册表。共享库没就位时退回模块内的本地副本，功能还在但不再聚合。

## 挂载流程

```cpp
#include <XBase/Panel.h>

XBase::Panel::ModSpec spec;
spec.modId = "MyMod";               // 与 XBasePayloadBaseName 保持一致
spec.title = "我的模组";
spec.version = "v0.1.0";

XBase::Panel::Page page;
page.id = "main";
page.label = "主页面";

XBase::Panel::Section section;
section.id = "general";
section.label = "通用";

XBase::Panel::Control godMode;
godMode.kind = XBase::Panel::ControlKind::Toggle;
godMode.id = "mymod.godMode";
godMode.label = "无敌";
section.controls.push_back(godMode);

page.sections.push_back(section);
spec.pages.push_back(page);

// 先挂载结构再绑钩子，绑定到的控件必须已经在注册表里
XBase::Panel::Mount(spec);
XBase::Panel::BindValue(
    "mymod.godMode",
    [] { return gGodMode ? 1.0 : 0.0; },
    [](double value) { gGodMode = value != 0.0; });

XBase::Panel::SetHotkey(XBase::Input::Hotkey{XBase::Input::Key::F7, 0});
```

顺序反了会返回假并写一条 `Panel: 控件未登记就绑定` 的警告。

## 值的约定

所有读写都用 `double`：

| 控件 | 读 | 写 |
|---|---|---|
| `Toggle` | 0 或 1 | 0 或 1 |
| `Float` / `Int` | 数值 | 数值 |
| `Select` | `options` 下标 | `options` 下标 |
| `Action` | 不读不写，走 `BindAction` | — |

这么定是因为 C 接口上不必传任何 STL 容器。XBase 用 `/MT` 静态运行库，跨模块传 `std::string` 与 `std::function` 会因 CRT 堆不匹配崩溃，函数表里只放函数指针加 `void*`。

## 结构体

| 字段 | 作用 |
|---|---|
| `Control::id` | 全局唯一，约定按 `模组名.分区.项` 起名 |
| `Control::kind` | `Toggle` / `Float` / `Int` / `Action` / `Select` |
| `Control::label` `hint` | 显示文本，多语言由模组自己决定 |
| `Control::bounded` `min` `max` `step` | 有边界才渲染成拖动条，否则渲染成输入框 |
| `Control::format` | 数值显示格式，形如 `%.1f` |
| `Control::capability` | 填 `FeatureCapability`，不支持时控件置灰而不是消失 |
| `Control::games` | 限定版本，`sa` / `vc` / `iii`，留空表示三个版本都显示 |
| `Control::visibleWhen` | 依赖同分区另一个控件，前置 `!` 取反 |
| `Control::options` | 仅下拉使用，读写的是下标 |
| `Section::columns` `inlineLayout` | 多列排布；内联分区按固定宽度换行，适合一排动作按钮 |
| `Section::capability` | 整块置灰，与控件级门控可以叠加 |

分区与控件的版本筛选、能力门控都在下发 schema 时算好，网页端拿到的已经是 `enabled` 布尔与筛过的列表。

## 常用调用

| 调用 | 作用 |
|---|---|
| `Mount(ModSpec)` | 提交整棵树，同 `modId` 重复挂载会整体替换并清空绑定 |
| `Unmount(modId)` | 摘掉整个模组的界面与绑定 |
| `BindValue(id, read, write)` | 绑读写，回调在游戏线程触发 |
| `BindAction(id, run)` | 绑按钮动作 |
| `NotifyChanged(id, value)` | 模组自己在游戏里改了状态，推事件让界面跟上 |
| `IsAvailable()` | 网页视图运行时与面板资源都在才算可用 |
| `Show(modId)` / `Hide()` / `Toggle()` / `IsVisible()` | 面板开合，`modId` 留空表示回到上次打开的模组 |
| `SetHotkey(hotkey)` / `GetHotkey()` | 开关热键，默认 F8 |

回调在游戏线程触发。存档这类不能从界面线程直触发的动作，要自己挂起到游戏线程再执行。

## 桥接协议

面板在共享运行时里注册下面这些方法，模组一般不用直接调：

| 方法 | 参数 | 返回 |
|---|---|---|
| `panel.schema` | — | 全部挂载内容、游戏版本、上次打开的模组 |
| `panel.get` | `{ id }` | `{ ok, value }` |
| `panel.set` | `{ id, value }` | `{ ok }` |
| `panel.run` | `{ id }` | `{ ok }` |
| `panel.hide` | — | `{ ok }` |
| `panel.setSize` / `panel.setPos` | `{ width, height }` / `{ x, y }` | `{ ok }` |
| `panel.rect` | — | `{ x, y, width, height }` |

原生往网页推事件 `panel.changed`，载荷 `{ id, value }`。

## 前端

`XBase/panel/` 是面板源码，React 19 + Vite + TypeScript + Tailwind 4，不引组件库。产物由 `XBase/Build.bat Release` stage 到 `XBase\Library\panel\`。

```bash
cd panel
npm install
npm run build
```

产物是 file 协议与虚拟主机都能加载的经典脚本：Vite 插件剥掉 `type="module"` 与 `crossorigin`，输出 iife，资源路径全部相对。

## 排查路径

1. `Panel::IsAvailable()` 返回假 —— 检查 `XBase\Library\panel\index.html` 是否存在，以及机器上有无 WebView2 运行时。
2. 面板打开了但侧栏空 —— 没有模组调用 `Mount`，或模组挂在自己的静态库副本上（共享库没加载成功）。
3. 开关显示与真实状态相反 —— 模组没绑 `BindValue`，`panel.get` 取不到值。
4. 界面改了不生效 —— 模组没每帧推送状态，被下一帧覆盖。
5. 控件全部置灰 —— `capability` 填的能力在本版本上是 `Unsupported`，换能力名或去掉门控。
6. 页面全白 —— 面板资源没构建，或 Vite 插件被改坏导致 `type="module"` 没剥掉。

日志在 `XBase\Mods\<模组名>\debug.log`，也可以用 XBase 自带的查看器打开。
