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.
764 lines
29 KiB
764 lines
29 KiB
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();
|
|
}
|
|
}
|
|
}
|
|
|