You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AdLike2048/assets/mx/module/gm/GmView.ts

765 lines
29 KiB

2 days ago
import {
Button, Color, EditBox, Graphics, Label, Layout, Mask, Node, ScrollView, Widget,
UITransform, HorizontalTextAlignment, VerticalTextAlignment, Layers, Overflow,
view,
} from 'cc';
import { GmBuiltinPanelId, GmKind, type IGmCommand, type IGmContentPanel, type IGmHost, type IGmNumberStepperData, type IGmPanelContext } from './GmTypes';
import { GmRegistry } from './GmRegistry';
/** 高对比主题 */
const C = {
bg: new Color(6, 8, 14, 255),
head: new Color(16, 22, 38, 255),
card: new Color(28, 36, 56, 255),
cardLine: new Color(120, 160, 220, 200),
quick: new Color(18, 92, 78, 255),
quickLine: new Color(100, 255, 200, 255),
accent: new Color(255, 220, 70, 255),
text: new Color(255, 255, 255, 255),
textDim: new Color(200, 210, 230, 255),
on: new Color(40, 210, 120, 255),
off: new Color(70, 78, 96, 255),
tabOn: new Color(255, 220, 70, 255),
tabOff: new Color(36, 46, 68, 255),
chip: new Color(52, 70, 110, 255),
setBtn: new Color(220, 140, 40, 255),
outline: new Color(0, 0, 0, 220),
};
const PAD = 16;
/** 头栏高度(含刘海安全区余量) */
const HEAD_H = 108;
const TAB_H = 60;
const ROW_H = 76;
const QUICK_H = 72;
const SECTION_H = 36;
/**
* GM 主视图:设备全屏、高对比;数值支持设为覆盖 + 增减。
*/
export class GmView {
readonly root: Node;
private readonly _host: IGmHost;
private readonly _onClose: () => void;
private _panel: Node = null!;
private _bg: Node = null!;
private _head: Node = null!;
private _tabBar: Node = null!;
private _listHost: Node = null!;
private _scrollContent: Node = null!;
private _contentHost: Node = null!;
private _groups: string[] = [];
private _groupIdx = 0;
private _activePanel: IGmContentPanel | null = null;
private _embedPanel: IGmContentPanel | null = null;
private _listMode = true;
private _listW = 700;
private _listH = 900;
private _screenW = 720;
private _screenH = 1280;
constructor(host: IGmHost, onClose: () => void) {
this._host = host;
this._onClose = onClose;
this.root = new Node('GmShell');
this.root.layer = Layers.Enum.UI_2D;
const vs = view.getVisibleSize();
this._screenW = vs.width;
this._screenH = vs.height;
this.root.addComponent(UITransform).setContentSize(vs.width, vs.height);
const w = this.root.addComponent(Widget);
w.isAlignLeft = w.isAlignRight = w.isAlignTop = w.isAlignBottom = true;
w.left = w.right = w.top = w.bottom = 0;
w.alignMode = Widget.AlignMode.ALWAYS;
this._buildChrome();
this.refresh();
}
/** 按父节点 / 可见区域铺满全屏,并重绘背景 */
layoutFullscreen(): void {
let W = view.getVisibleSize().width;
let H = view.getVisibleSize().height;
const parent = this.root.parent;
if (parent?.isValid) {
const put = parent.getComponent(UITransform);
if (put && put.width > 1 && put.height > 1) {
W = put.width;
H = put.height;
}
}
const ut = this.root.getComponent(UITransform)!;
ut.setContentSize(W, H);
this.root.setPosition(0, 0);
const widget = this.root.getComponent(Widget);
if (widget) {
widget.left = widget.right = widget.top = widget.bottom = 0;
widget.updateAlignment();
W = ut.width;
H = ut.height;
}
this._screenW = W;
this._screenH = H;
if (this._bg?.isValid) {
this._bg.getComponent(UITransform)!.setContentSize(W, H);
this._fillRect(this._bg, C.bg, W, H);
}
if (this._head?.isValid) {
this._head.getComponent(UITransform)!.setContentSize(W, HEAD_H);
this._fillRect(this._head, C.head, W, HEAD_H);
}
if (this._panel?.isValid) {
this._panel.getComponent(UITransform)!.setContentSize(W, H);
this._panel.getComponent(Widget)?.updateAlignment();
}
this._relayout(W, H);
this._rebuildTabs();
if (this._listMode) this._rebuildList();
}
/** 重建当前分组列表 */
refresh(): void {
this._groups = GmRegistry.listGroups();
if (!GmRegistry.hasCommands()) {
console.warn('[GM] 未注册任何命令');
}
if (this._groupIdx >= this._groups.length) this._groupIdx = 0;
this._rebuildTabs();
if (this._listMode) this._rebuildList();
}
/** 销毁视图 */
destroy(): void {
this._closeActivePanel();
this._closeEmbed();
if (this.root?.isValid) this.root.destroy();
}
private _buildChrome(): void {
const W = this._screenW;
const H = this._screenH;
this._bg = new Node('bg');
this._bg.layer = Layers.Enum.UI_2D;
this._bg.addComponent(UITransform).setContentSize(W, H);
this._fillRect(this._bg, C.bg, W, H);
const bgW = this._bg.addComponent(Widget);
bgW.isAlignLeft = bgW.isAlignRight = bgW.isAlignTop = bgW.isAlignBottom = true;
bgW.left = bgW.right = bgW.top = bgW.bottom = 0;
bgW.alignMode = Widget.AlignMode.ALWAYS;
this.root.addChild(this._bg);
this._panel = new Node('panel');
this._panel.layer = Layers.Enum.UI_2D;
this._panel.addComponent(UITransform).setContentSize(W, H);
const pw = this._panel.addComponent(Widget);
pw.isAlignLeft = pw.isAlignRight = pw.isAlignTop = pw.isAlignBottom = true;
pw.left = pw.right = pw.top = pw.bottom = 0;
pw.alignMode = Widget.AlignMode.ALWAYS;
this.root.addChild(this._panel);
this._head = new Node('head');
this._head.layer = Layers.Enum.UI_2D;
this._head.addComponent(UITransform).setContentSize(W, HEAD_H);
this._fillRect(this._head, C.head, W, HEAD_H);
const hw = this._head.addComponent(Widget);
hw.isAlignTop = hw.isAlignLeft = hw.isAlignRight = true;
hw.isAlignBottom = false;
hw.top = 0;
hw.left = hw.right = 0;
hw.alignMode = Widget.AlignMode.ALWAYS;
this._panel.addChild(this._head);
const closeBtn = this._pillBtn(this._head, '关闭', 100, 48, () => this._onClose(), C.card, C.text);
const cw = closeBtn.addComponent(Widget);
cw.isAlignLeft = true;
cw.isAlignBottom = true;
cw.left = PAD;
cw.bottom = 12;
cw.isAlignRight = cw.isAlignTop = cw.isAlignVerticalCenter = false;
const title = this._label(this._head, 'GM', 42, C.accent, HorizontalTextAlignment.LEFT);
title.getComponent(UITransform)!.setContentSize(200, 48);
const titleLb = title.getComponent(Label)!;
titleLb.isBold = true;
const tw = title.addComponent(Widget);
tw.isAlignLeft = true;
tw.isAlignBottom = true;
tw.left = PAD + 112;
tw.bottom = 12;
tw.isAlignRight = tw.isAlignTop = tw.isAlignHorizontalCenter = tw.isAlignVerticalCenter = false;
this._tabBar = new Node('tabs');
this._tabBar.layer = Layers.Enum.UI_2D;
this._tabBar.addComponent(UITransform).setContentSize(W - PAD * 2, TAB_H);
const tabW = this._tabBar.addComponent(Widget);
tabW.isAlignTop = tabW.isAlignLeft = tabW.isAlignRight = true;
tabW.top = HEAD_H + 6;
tabW.left = tabW.right = PAD;
tabW.alignMode = Widget.AlignMode.ALWAYS;
const tabLayout = this._tabBar.addComponent(Layout);
tabLayout.type = Layout.Type.HORIZONTAL;
tabLayout.resizeMode = Layout.ResizeMode.CONTAINER;
tabLayout.spacingX = 10;
this._panel.addChild(this._tabBar);
this._listH = Math.max(200, H - HEAD_H - TAB_H - 28);
this._listW = Math.max(200, W - PAD * 2);
this._listHost = this._makeScrollArea('list', this._listW, this._listH);
const lw = this._listHost.addComponent(Widget);
lw.isAlignTop = lw.isAlignLeft = lw.isAlignRight = lw.isAlignBottom = true;
lw.top = HEAD_H + TAB_H + 14;
lw.bottom = PAD;
lw.left = lw.right = PAD;
lw.alignMode = Widget.AlignMode.ALWAYS;
this._panel.addChild(this._listHost);
this._scrollContent = this._listHost.getChildByName('view')!.getChildByName('content')!;
this._contentHost = new Node('contentHost');
this._contentHost.layer = Layers.Enum.UI_2D;
this._contentHost.addComponent(UITransform).setContentSize(this._listW, this._listH);
const chw = this._contentHost.addComponent(Widget);
chw.isAlignTop = chw.isAlignLeft = chw.isAlignRight = chw.isAlignBottom = true;
chw.top = HEAD_H + TAB_H + 14;
chw.bottom = PAD;
chw.left = chw.right = PAD;
chw.alignMode = Widget.AlignMode.ALWAYS;
this._contentHost.active = false;
this._panel.addChild(this._contentHost);
}
private _relayout(W: number, H: number): void {
this._listW = Math.max(200, W - PAD * 2);
this._listH = Math.max(200, H - HEAD_H - TAB_H - 28);
const listUt = this._listHost?.getComponent(UITransform);
if (listUt) listUt.setContentSize(this._listW, this._listH);
const viewN = this._listHost?.getChildByName('view');
viewN?.getComponent(UITransform)?.setContentSize(this._listW, this._listH);
const content = this._scrollContent;
if (content) {
const cut = content.getComponent(UITransform);
if (cut) {
cut.setContentSize(this._listW, this._listH);
content.setPosition(0, this._listH / 2);
}
}
this._contentHost?.getComponent(UITransform)?.setContentSize(this._listW, this._listH);
}
private _makeScrollArea(name: string, w: number, h: number): Node {
const host = new Node(name);
host.layer = Layers.Enum.UI_2D;
host.addComponent(UITransform).setContentSize(w, h);
const viewN = new Node('view');
viewN.layer = Layers.Enum.UI_2D;
viewN.addComponent(UITransform).setContentSize(w, h);
viewN.addComponent(Mask);
const vw = viewN.addComponent(Widget);
vw.isAlignLeft = vw.isAlignRight = vw.isAlignTop = vw.isAlignBottom = true;
vw.left = vw.right = vw.top = vw.bottom = 0;
vw.alignMode = Widget.AlignMode.ALWAYS;
host.addChild(viewN);
const content = new Node('content');
content.layer = Layers.Enum.UI_2D;
const cut = content.addComponent(UITransform);
cut.setContentSize(w, h);
cut.setAnchorPoint(0.5, 1);
content.setPosition(0, h / 2);
const layout = content.addComponent(Layout);
layout.type = Layout.Type.VERTICAL;
layout.resizeMode = Layout.ResizeMode.CONTAINER;
layout.spacingY = 12;
layout.paddingTop = 6;
layout.paddingBottom = 48;
viewN.addChild(content);
const sv = host.addComponent(ScrollView);
sv.content = content;
sv.horizontal = false;
sv.vertical = true;
sv.brake = 0.75;
sv.elastic = true;
return host;
}
private _rebuildTabs(): void {
this._tabBar.removeAllChildren();
const n = this._groups.length || 1;
const barW = this._tabBar.getComponent(UITransform)?.width ?? this._listW;
const btnW = Math.max(110, (barW - (n - 1) * 10) / n);
for (let i = 0; i < this._groups.length; i++) {
const gname = this._groups[i];
const idx = i;
const on = idx === this._groupIdx;
const btn = this._pillBtn(this._tabBar, gname, btnW, TAB_H - 6, () => {
this._groupIdx = idx;
this._showList();
this._rebuildTabs();
this._rebuildList();
}, on ? C.tabOn : C.tabOff, on ? new Color(20, 16, 4, 255) : C.text);
const lb = btn.getChildByName('lb')?.getComponent(Label);
if (lb) {
lb.isBold = true;
lb.fontSize = 26;
}
}
this._tabBar.getComponent(Layout)?.updateLayout();
}
private _rebuildList(): void {
this._closeActivePanel();
this._closeEmbed();
this._scrollContent.removeAllChildren();
const group = this._groups[this._groupIdx];
if (!group) {
this._addHint('无分组');
return;
}
const cmds = GmRegistry.listByGroup(group);
if (cmds.length === 0) {
this._addHint('本组无命令');
return;
}
let embedCmd: IGmCommand | null = null;
const quicks: IGmCommand[] = [];
const toggles: IGmCommand[] = [];
const contents: IGmCommand[] = [];
for (let i = 0; i < cmds.length; i++) {
const c = cmds[i];
if (c.embed && c.kind === GmKind.Content) {
embedCmd = c;
continue;
}
if (c.kind === GmKind.Quick) quicks.push(c);
else if (c.kind === GmKind.Toggle) toggles.push(c);
else if (c.kind === GmKind.Content) contents.push(c);
else console.warn('[GM] 未知 kind', c.id, c.kind);
}
if (embedCmd) {
this._openEmbed(embedCmd);
return;
}
this._listHost.active = true;
this._contentHost.active = false;
if (quicks.length) {
this._addSection('快捷 · 点一下即执行');
this._addQuickGrid(quicks);
}
if (toggles.length) {
this._addSection('开关 · 点右侧切换');
for (let i = 0; i < toggles.length; i++) this._addToggle(toggles[i]);
}
if (contents.length) {
this._addSection('数值 / 选择');
for (let i = 0; i < contents.length; i++) {
const c = contents[i];
if (c.panelId === GmBuiltinPanelId.numberStepper) this._addInlineStepper(c);
else this._addContentEntry(c);
}
}
this._scrollContent.getComponent(Layout)?.updateLayout();
}
/** Tab 内嵌整页面板 */
private _openEmbed(cmd: IGmCommand): void {
if (!cmd.panelId) {
console.warn('[GM] embed 无 panelId', cmd.id);
return;
}
const factory = GmRegistry.getPanelFactory(cmd.panelId);
if (!factory) {
console.warn('[GM] embed 面板未注册', cmd.panelId);
this._host.toast(`面板未注册: ${cmd.panelId}`);
return;
}
this._closeActivePanel();
this._listHost.active = false;
this._contentHost.active = true;
this._contentHost.removeAllChildren();
this._listMode = true;
this._embedPanel = factory();
const ctx: IGmPanelContext = {
command: cmd,
host: this._host,
goBack: () => { /* 嵌入页无返回 */ },
refreshList: () => this.refresh(),
};
this._embedPanel.open(this._contentHost, ctx);
}
private _closeEmbed(): void {
if (this._embedPanel) {
this._embedPanel.close();
this._embedPanel = null;
}
}
private _addHint(text: string): void {
const n = new Node('hint');
n.layer = Layers.Enum.UI_2D;
n.addComponent(UITransform).setContentSize(this._listW, 48);
const lb = n.addComponent(Label);
this._styleLabel(lb, text, 28, C.textDim);
lb.horizontalAlign = HorizontalTextAlignment.CENTER;
this._scrollContent.addChild(n);
}
private _addSection(title: string): void {
const n = new Node(`sec_${title}`);
n.layer = Layers.Enum.UI_2D;
n.addComponent(UITransform).setContentSize(this._listW, SECTION_H);
const lb = n.addComponent(Label);
this._styleLabel(lb, title, 24, C.accent);
lb.isBold = true;
lb.horizontalAlign = HorizontalTextAlignment.LEFT;
this._scrollContent.addChild(n);
}
private _addQuickGrid(quicks: IGmCommand[]): void {
const gap = 12;
const colW = (this._listW - gap) / 2;
for (let i = 0; i < quicks.length; i += 2) {
const row = new Node(`qrow_${i}`);
row.layer = Layers.Enum.UI_2D;
row.addComponent(UITransform).setContentSize(this._listW, QUICK_H);
const lay = row.addComponent(Layout);
lay.type = Layout.Type.HORIZONTAL;
lay.spacingX = gap;
lay.resizeMode = Layout.ResizeMode.NONE;
this._scrollContent.addChild(row);
this._addQuickCell(row, quicks[i], colW);
if (i + 1 < quicks.length) this._addQuickCell(row, quicks[i + 1], colW);
else {
const spacer = new Node('sp');
spacer.layer = Layers.Enum.UI_2D;
spacer.addComponent(UITransform).setContentSize(colW, QUICK_H);
row.addChild(spacer);
}
lay.updateLayout();
}
}
private _addQuickCell(parent: Node, cmd: IGmCommand, w: number): void {
const n = new Node(cmd.id);
n.layer = Layers.Enum.UI_2D;
n.addComponent(UITransform).setContentSize(w, QUICK_H);
this._fillRound(n, C.quick, w, QUICK_H, 14, C.quickLine);
const lbNode = new Node('lb');
lbNode.layer = Layers.Enum.UI_2D;
lbNode.addComponent(UITransform).setContentSize(w - 12, QUICK_H - 10);
const lb = lbNode.addComponent(Label);
this._styleLabel(lb, cmd.title, 24, C.text);
lb.isBold = true;
lb.horizontalAlign = HorizontalTextAlignment.CENTER;
lb.verticalAlign = VerticalTextAlignment.CENTER;
lb.overflow = Overflow.SHRINK;
lb.enableWrapText = true;
n.addChild(lbNode);
n.addComponent(Button);
n.on(Button.EventType.CLICK, () => { void this._runQuick(cmd); }, this);
parent.addChild(n);
}
private async _runQuick(cmd: IGmCommand): Promise<void> {
if (!cmd.run) {
console.warn('[GM] Quick 无 run', cmd.id);
this._host.toast('命令未实现');
return;
}
try {
await cmd.run();
this.refresh();
} catch (e) {
console.warn('[GM] Quick 执行失败', cmd.id, e);
this._host.toast('执行失败');
}
}
private _addToggle(cmd: IGmCommand): void {
const n = new Node(cmd.id);
n.layer = Layers.Enum.UI_2D;
n.addComponent(UITransform).setContentSize(this._listW, ROW_H);
this._fillRound(n, C.card, this._listW, ROW_H, 14, C.cardLine);
const title = this._label(n, cmd.title, 28, C.text, HorizontalTextAlignment.LEFT);
title.getComponent(UITransform)!.setContentSize(this._listW - 180, ROW_H - 8);
title.setPosition(-(this._listW / 2) + (this._listW - 180) / 2 + 18, 0);
const on = !!(cmd.get?.());
const sw = this._makeSwitch(n, on, () => {
if (!cmd.set || !cmd.get) {
console.warn('[GM] Toggle 缺少 get/set', cmd.id);
return;
}
cmd.set(!cmd.get());
this.refresh();
});
sw.setPosition(this._listW / 2 - 72, 0);
this._scrollContent.addChild(n);
}
private _makeSwitch(parent: Node, on: boolean, onClick: () => void): Node {
const trackW = 100;
const trackH = 48;
const n = new Node('switch');
n.layer = Layers.Enum.UI_2D;
n.addComponent(UITransform).setContentSize(trackW, trackH);
this._fillRound(n, on ? C.on : C.off, trackW, trackH, trackH / 2);
const knob = new Node('knob');
knob.layer = Layers.Enum.UI_2D;
knob.addComponent(UITransform).setContentSize(38, 38);
this._fillRound(knob, C.text, 38, 38, 19);
knob.setPosition(on ? 24 : -24, 0);
n.addChild(knob);
const tag = this._label(n, on ? '开' : '关', 20, on ? new Color(8, 40, 20, 255) : C.text, HorizontalTextAlignment.CENTER);
tag.getComponent(UITransform)!.setContentSize(44, 30);
tag.setPosition(on ? -20 : 20, 0);
n.addComponent(Button);
n.on(Button.EventType.CLICK, onClick, this);
parent.addChild(n);
return n;
}
/**
* 数值卡:EditBox +「设为」覆盖;下方 ± 立刻增减。
*/
private _addInlineStepper(cmd: IGmCommand): void {
const data = cmd.panelData as IGmNumberStepperData;
if (!data?.getValue || !data?.apply) {
console.warn('[GM] numberStepper 缺少 panelData', cmd.id);
this._addContentEntry(cmd);
return;
}
const cardH = 148;
const n = new Node(cmd.id);
n.layer = Layers.Enum.UI_2D;
n.addComponent(UITransform).setContentSize(this._listW, cardH);
this._fillRound(n, C.card, this._listW, cardH, 14, C.cardLine);
const title = this._label(n, cmd.title, 26, C.textDim, HorizontalTextAlignment.LEFT);
title.getComponent(UITransform)!.setContentSize(this._listW * 0.4, 30);
title.setPosition(-(this._listW / 2) + this._listW * 0.2 + 14, 48);
const cur = Math.floor(Number(data.getValue()) || 0);
const editNode = new Node('edit');
editNode.layer = Layers.Enum.UI_2D;
const editW = Math.min(200, this._listW * 0.32);
editNode.addComponent(UITransform).setContentSize(editW, 48);
this._fillRound(editNode, new Color(12, 16, 28, 255), editW, 48, 10, C.accent);
const textLbN = new Node('TEXT_LABEL');
textLbN.layer = Layers.Enum.UI_2D;
textLbN.addComponent(UITransform).setContentSize(editW - 12, 40);
const textLb = textLbN.addComponent(Label);
this._styleLabel(textLb, `${cur}`, 28, C.accent);
textLb.isBold = true;
textLb.horizontalAlign = HorizontalTextAlignment.CENTER;
textLb.overflow = Overflow.SHRINK;
editNode.addChild(textLbN);
const phN = new Node('PLACEHOLDER_LABEL');
phN.layer = Layers.Enum.UI_2D;
phN.addComponent(UITransform).setContentSize(editW - 12, 40);
const phLb = phN.addComponent(Label);
this._styleLabel(phLb, '输入数值', 22, C.textDim);
phLb.horizontalAlign = HorizontalTextAlignment.CENTER;
editNode.addChild(phN);
const eb = editNode.addComponent(EditBox);
eb.textLabel = textLb;
eb.placeholderLabel = phLb;
eb.string = `${cur}`;
eb.maxLength = 12;
eb.inputMode = EditBox.InputMode.NUMERIC;
eb.inputFlag = EditBox.InputFlag.DEFAULT;
eb.returnType = EditBox.KeyboardReturnType.DONE;
editNode.setPosition(this._listW / 2 - editW / 2 - 130, 48);
n.addChild(editNode);
const syncEdit = (): void => {
eb.string = `${Math.floor(Number(data.getValue()) || 0)}`;
};
const setBtn = this._pillBtn(n, '设为', 96, 48, () => {
let v = Math.floor(Number(eb.string));
if (!Number.isFinite(v)) {
this._host.toast('请输入有效数字');
return;
}
if (data.min !== undefined && v < data.min) v = data.min;
if (data.max !== undefined && v > data.max) v = data.max;
data.apply(v, 'set');
syncEdit();
this._host.toast(`${cmd.title} 已设为 ${Math.floor(Number(data.getValue()) || 0)}`);
}, C.setBtn, C.text);
setBtn.setPosition(this._listW / 2 - 52, 48);
const steps = [-100, -10, -1, 1, 10, 100];
const gap = 8;
const chipW = Math.min(92, (this._listW - 28 - gap * (steps.length - 1)) / steps.length);
const totalW = steps.length * chipW + (steps.length - 1) * gap;
const baseX = -totalW / 2 + chipW / 2;
for (let i = 0; i < steps.length; i++) {
const s = steps[i];
const chip = this._pillBtn(n, s > 0 ? `+${s}` : `${s}`, chipW, 46, () => {
const now = Math.floor(Number(data.getValue()) || 0);
let next = now + s;
if (data.min !== undefined && next < data.min) next = data.min;
if (data.max !== undefined && next > data.max) next = data.max;
data.apply(next, 'set');
syncEdit();
}, C.chip, C.text);
chip.setPosition(baseX + i * (chipW + gap), -36);
}
const hint = this._label(n, '输入后点「设为」覆盖 · 下方按钮增减', 18, C.textDim, HorizontalTextAlignment.CENTER);
hint.getComponent(UITransform)!.setContentSize(this._listW - 24, 24);
hint.setPosition(0, 14);
this._scrollContent.addChild(n);
}
private _addContentEntry(cmd: IGmCommand): void {
const n = new Node(cmd.id);
n.layer = Layers.Enum.UI_2D;
n.addComponent(UITransform).setContentSize(this._listW, ROW_H);
this._fillRound(n, C.card, this._listW, ROW_H, 14, C.cardLine);
const title = this._label(n, cmd.title, 28, C.text, HorizontalTextAlignment.LEFT);
title.getComponent(UITransform)!.setContentSize(280, ROW_H - 8);
title.setPosition(-(this._listW / 2) + 150, 0);
const sumText = cmd.summary ? cmd.summary() : '选择';
const sum = this._label(n, `${sumText} ›`, 24, C.accent, HorizontalTextAlignment.RIGHT);
sum.getComponent(UITransform)!.setContentSize(this._listW - 320, ROW_H - 8);
sum.setPosition(this._listW / 2 - (this._listW - 320) / 2 - 16, 0);
n.addComponent(Button);
n.on(Button.EventType.CLICK, () => this._openContent(cmd), this);
this._scrollContent.addChild(n);
}
private _openContent(cmd: IGmCommand): void {
if (!cmd.panelId) {
console.warn('[GM] Content 无 panelId', cmd.id);
this._host.toast('未配置面板');
return;
}
const factory = GmRegistry.getPanelFactory(cmd.panelId);
if (!factory) {
console.warn('[GM] 面板未注册', cmd.panelId);
this._host.toast(`面板未注册: ${cmd.panelId}`);
return;
}
this._closeActivePanel();
this._listHost.active = false;
this._contentHost.active = true;
this._contentHost.removeAllChildren();
this._listMode = false;
this._activePanel = factory();
const ctx: IGmPanelContext = {
command: cmd,
host: this._host,
goBack: () => this._showList(),
refreshList: () => this.refresh(),
};
this._activePanel.open(this._contentHost, ctx);
}
private _showList(): void {
this._closeActivePanel();
this._closeEmbed();
this._contentHost.active = false;
this._listHost.active = true;
this._listMode = true;
this._rebuildList();
}
private _closeActivePanel(): void {
if (this._activePanel) {
this._activePanel.close();
this._activePanel = null;
}
if (this._contentHost?.isValid) this._contentHost.removeAllChildren();
}
private _styleLabel(lb: Label, text: string, size: number, color: Color): void {
lb.string = text;
lb.fontSize = size;
lb.lineHeight = Math.ceil(size * 1.25);
lb.color = color;
lb.isBold = true;
lb.enableOutline = false;
lb.verticalAlign = VerticalTextAlignment.CENTER;
}
private _label(parent: Node, text: string, size: number, color: Color, align: HorizontalTextAlignment): Node {
const n = new Node('lb');
n.layer = Layers.Enum.UI_2D;
n.addComponent(UITransform).setContentSize(100, 40);
const lb = n.addComponent(Label);
this._styleLabel(lb, text, size, color);
lb.horizontalAlign = align;
parent.addChild(n);
return n;
}
private _pillBtn(parent: Node, text: string, w: number, h: number, onClick: () => void, bg: Color, fg: Color): Node {
const n = new Node(text);
n.layer = Layers.Enum.UI_2D;
n.addComponent(UITransform).setContentSize(w, h);
this._fillRound(n, bg, w, h, Math.min(16, h / 2));
const lbNode = new Node('lb');
lbNode.layer = Layers.Enum.UI_2D;
lbNode.addComponent(UITransform).setContentSize(w - 8, h - 4);
const lb = lbNode.addComponent(Label);
this._styleLabel(lb, text, Math.min(26, Math.floor(h * 0.48)), fg);
lb.isBold = true;
lb.horizontalAlign = HorizontalTextAlignment.CENTER;
lb.overflow = Overflow.SHRINK;
n.addChild(lbNode);
n.addComponent(Button);
n.on(Button.EventType.CLICK, onClick, this);
parent.addChild(n);
return n;
}
private _fillRect(node: Node, color: Color, w: number, h: number): void {
const g = node.getComponent(Graphics) || node.addComponent(Graphics);
g.clear();
g.fillColor = color;
g.rect(-w / 2, -h / 2, w, h);
g.fill();
}
private _fillRound(node: Node, color: Color, w: number, h: number, r: number, stroke?: Color): void {
const g = node.getComponent(Graphics) || node.addComponent(Graphics);
g.clear();
g.fillColor = color;
g.roundRect(-w / 2, -h / 2, w, h, r);
g.fill();
if (stroke) {
g.strokeColor = stroke;
g.lineWidth = 2;
g.roundRect(-w / 2, -h / 2, w, h, r);
g.stroke();
}
}
}