Skip to content
On this page

在 GUI 上打开子页面

子页面适合制作确认框、详情卡片和临时设置面板。它不会替换当前 GUI,而是显示在已有页面上方。父页面和子页面都能点击:鼠标先从上到下检查子页面,子页面空白处没有命中目标时,点击会继续到父页面。

子页面本身必须是 display.mode=screen。根页面可以是普通 screen 页面,也可以是正在替换原版容器的 container 页面;HUD 和世界页面不能作为子页面宿主。页面链最多包含 8 层子页面,同一个页面 ID 不能在当前链中重复,因此页面自循环和 A → B → A 都会被拒绝。

第一步:创建父页面

保存为 subpage_parent.yml。点击按钮后,脚本会调用 openSub("subpage_child")

yaml
id: subpage_parent
version: 1
title: 子页面父页
size:
  width: 260
  height: 150
coordinateMode: absolute
display:
  mode: screen
  screen:
    dimBackground: false
vars:
  parentClicks: 0
events:
  mouse.left: |-
    vars.parentClicks = vars.parentClicks + 1
methods: {}
elements:
  - id: parent_panel
    type: rect
    layout:
      x: 20
      y: 20
      width: 220
      height: 110
    rect:
      color: "#26303A"
  - id: parent_tip
    type: text
    layout:
      x: 38
      y: 38
      width: 184
      height: 20
    text:
      value: "父页面点击次数:{vars.parentClicks}"
      textSize: 1.0
      color: "#FFFFFF"
  - id: open_child
    type: button
    layout:
      x: 80
      y: 82
      width: 100
      height: 24
    button:
      label: 打开子页面
      textSize: 1.0
    events:
      leftClick: |-
        openSub("subpage_child")

页面根级鼠标事件只在指针路由实际到达这一层时触发。点击子页面自身按钮时,父页的 mouse.left 不会同时执行;点击子页面未拦截的空白处时,路由会继续到父页。

第二步:创建可穿透空白处的子页面

保存为 subpage_child.yml。这个页面只在中央绘制一块小面板;全页辅助矩形使用 pointerEvents: pass,因此它完全退出命中,面板外仍能操作父页面。

yaml
id: subpage_child
version: 1
title: 非模态子页面
size:
  width: 260
  height: 150
coordinateMode: absolute
display:
  mode: screen
  screen:
    dimBackground: false
vars:
  childClicks: 0
events: {}
methods: {}
elements:
  - id: click_surface
    type: rect
    layout:
      x: 0
      y: 0
      width: window.width
      height: window.height
    pointerEvents: pass
    rect:
      color: "#000000"
  - id: child_panel
    type: rect
    layout:
      x: 70
      y: 42
      width: 120
      height: 68
    pointerEvents: block
    rect:
      color: "#3B4652"
  - id: child_text
    type: text
    layout:
      x: 84
      y: 56
      width: 92
      height: 16
    text:
      value: "子页:{vars.childClicks}"
      textSize: 1.0
      color: "#FFFFFF"
  - id: child_button
    type: button
    layout:
      x: 92
      y: 78
      width: 76
      height: 20
    button:
      label: 点击子页
      textSize: 1.0
    events:
      leftClick: |-
        vars.childClicks = vars.childClicks + 1

每层页面都有独立的 sessionId、变量、输入值、动画、计时器和素材资源。父页和子页即使使用相同变量名,也不会互相覆盖。

改成模态子页面

需要阻止父页面点击时,不要依赖隐式“模态”开关。创建一个铺满页面的 rect,并设置 pointerEvents: block。保存下面的完整示例为 subpage_modal.yml

yaml
id: subpage_modal
version: 1
title: 模态子页面
size:
  width: 260
  height: 150
coordinateMode: absolute
display:
  mode: screen
  screen:
    dimBackground: false
vars: {}
events: {}
methods: {}
elements:
  - id: modal_mask
    type: rect
    layout:
      x: 0
      y: 0
      width: window.width
      height: window.height
    pointerEvents: block
    opacity: 0.6
    rect:
      color: "#000000"
  - id: modal_panel
    type: rect
    layout:
      x: 65
      y: 40
      width: 130
      height: 72
    pointerEvents: block
    rect:
      color: "#303A45"
  - id: modal_title
    type: text
    layout:
      x: 82
      y: 56
      width: 96
      height: 18
    text:
      value: 确认操作?
      textSize: 1.0
      align: center
      color: "#FFFFFF"
  - id: modal_close
    type: button
    layout:
      x: 92
      y: 82
      width: 76
      height: 20
    button:
      label: 返回
      textSize: 1.0
    events:
      leftClick: close()

遮罩只阻断指针路由,不会暂停父页面的计时器、变量刷新或生命周期。需要暂停业务逻辑时,请用页面变量和脚本明确控制。

焦点、Esc 和关闭范围

页面栈只有一个键盘焦点归属。点击输入框所在的页面后,键盘输入会转移到那一层;没有焦点时默认由栈顶页面接收。Esc 的处理顺序固定为:先收起当前展开的下拉选择框,再关闭栈顶子页面,最后才按根 GUI 原有行为关闭页面。

  • close() / 关闭():关闭触发脚本的当前页面。子页面调用时只关闭自己及其上方页面。
  • /chaui closesubChaUIAPI.closeSubPage(...):只关闭当前栈顶子页面。
  • closeAll() / 全部关闭():从栈顶一路关闭到根页面,不影响 HUD 和世界页面。若根页面是容器替换页面,真实原版容器也会正常关闭,并继续触发 Bukkit 和第三方插件的库存关闭流程。

按下鼠标后,目标页面和组件会被捕获到本次点击结束;释放不会因为页面间临时重叠而误发给另一层。

指令与服务端 API

text
/chaui opensub <玩家> <页面ID>
/chaui closesub <玩家>
java
ChaUIAPI.openSubPage(player, "subpage_child");
ChaUIAPI.closeSubPage(player);

用子页面制作同级 Tab

顶部 Tab、分类页和分页式功能区通常不是“继续叠一层”,而是把根页面上方的当前内容页替换成另一个内容页。这类场景应使用 replaceSubPage

java
ChaUIAPI.replaceSubPage(player, "barn_shop").thenAccept(sessionId -> {
    ChaUIAPI.setVariables(player, "barn_shop", Map.of(
            "selected", "cow",
            "quantity", 1
    ));
});

这个方法会先让旧子页面正常关闭,再把新页面直接挂到根页面。返回结果只会在新页面已经打开后完成,因此适合在回调中发送该页变量、物品展示和其他初始状态。

快速连续点击多个 Tab 时只保留最后一次目标,不会把每个内容页依次压入更深的页面栈。当前子页面已经是目标页面时会直接复用当前会话。

不要手动拼接关闭与打开

不要为了切换同级内容而先调用 closeSubPage,紧接着调用 openSubPage。关闭页面需要完成生命周期和临时槽物品收尾,立即打开可能把新页面挂到旧页面之上。请直接使用 replaceSubPage

根页面适合只保留标题、状态、Tab 和公共按钮;每个 Tab 的卡片、物品展示、临时槽和操作按钮放在各自子页面中。这样页面更容易维护,打开时也只需要处理当前可见功能。

reload 时会发生什么

执行 /chaui reload 时,旧链从栈顶到根页面依次执行 close 并释放资源;新配置加载后,先恢复根页面,再从第一层到栈顶逐层执行 open 和组件 create。某一层页面缺失、无效或不再是 screen 时,会从这一层停止恢复,已恢复的父级继续保留。容器替换根页面不会因为 reload 关闭真实容器。

常见错误

子页面打不开

确认子页面是 display.mode=screen,根页面是当前活动的 screencontainer GUI,并且当前链未达到 8 层。HUD、世界页面、重复页面 ID 和循环链都会被拒绝。

父页面完全点不到

检查子页面是否放置了 pointerEvents: block 的全屏组件。希望空白处穿透时,请缩小拦截组件,或把只作展示的全屏元素改为 pointerEvents: pass

父页面错误地收到鼠标事件

页面根级鼠标事件会在路由到达该层时执行。如果子页面需要吃掉某个区域的点击,请确保该区域存在 auto 可交互组件或 pointerEvents: block 组件。