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.
 
 
 
 

533 lines
44 KiB

<div class="inspector-root">
<base href="http://localhost:7456/" />
<div class="panel-toolbar">
<button class="server-btn" :class="{ running: state.running }" @click="onToggleServer">{{ state.running ? '停止服务' : '启动服务' }}</button>
<button class="toggle" :class="{ active: autoRefresh }" @click="autoRefresh = !autoRefresh">自动刷新</button>
<button class="toggle" :class="{ active: showPreview }" @click="showPreview = !showPreview">预览画面</button>
<button class="toggle" :class="{ active: showPerf }" @click="showPerf = !showPerf">性能曲线</button>
<button class="toggle" :class="{ active: showRes }" @click="showRes = !showRes" :disabled="!state.running || state.clients === 0">资源统计</button>
<button class="toggle" :class="{ active: physicsDebug }" @click="physicsDebug = !physicsDebug" :disabled="!state.running || state.clients === 0">物理调试</button>
<button class="toggle" :class="{ active: highlightOn }" @click="toggleHighlight" :disabled="!state.running || state.clients === 0">游戏高亮</button>
<button class="toggle" :class="{ active: highlightMode === 'mask' }" @click="toggleHighlightMode" :disabled="!highlightOn || !state.running || state.clients === 0" title="切换高亮/遮罩模式">{{ highlightMode === 'mask' ? '遮罩' : '框选' }}</button>
<button class="toggle" :class="{ active: mcp.running }" @click="openMcpDialog" title="打开 MCP 服务配置(供 Codex 等 AI Agent 连接)">MCP 服务</button>
<button @click="refreshTree" :disabled="!state.running || state.clients === 0">刷新节点</button>
<button class="install-btn" @click="onInstallBridge">安装脚本</button>
<span v-if="state.clients > 0" class="client-badge" :class="clientTypeMeta.cls" :title="'客户端来源: ' + clientTypeMeta.label + (state.clientUrl ? '\n' + state.clientUrl : '')">
<i class="client-icon"></i>{{ clientTypeMeta.label }}
</span>
</div>
<div class="mcp-mask" v-if="showMcpDialog" @click.self="closeMcpDialog">
<div class="mcp-dialog">
<div class="mcp-dialog-head">
<span>MCP 服务配置</span>
<button class="mini" @click="closeMcpDialog" title="关闭"><ui-icon value="close"></ui-icon></button>
</div>
<div class="mcp-dialog-body">
<div class="mcp-row">
<span class="mcp-label">状态</span>
<span class="mcp-st" :class="mcp.running ? 'on' : 'off'">{{ mcp.running ? '运行中' : '未启动' }}</span>
<span class="mcp-st-url" v-if="mcp.running">{{ mcp.url }}</span>
</div>
<div class="mcp-row">
<span class="mcp-label">端口</span>
<input class="cmd-input mcp-port" type="number" v-model.number="mcpPort" min="1" max="65535" @change="applyMcpPort" />
<span class="mcp-help">修改后立即生效(运行中会重启)</span>
</div>
<div class="mcp-row">
<span class="mcp-label">令牌</span>
<input class="token-input" :value="mcp.token" readonly spellcheck="false" @focus="selectToken" title="固定静态令牌" />
</div>
<div class="mcp-row">
<span class="mcp-label">授权头</span>
<span class="mcp-auth" title="Authorization 请求头,MCP 客户端鉴权用">Authorization: Bearer {{ mcp.token }}</span>
</div>
<div class="mcp-row mcp-actions">
<button class="toggle" :class="{ active: mcp.running }" @click="toggleMcp" :disabled="mcpBusy">{{ mcp.running ? '停止服务' : '启动服务' }}</button>
<button class="mini" @click="copyMcpConfig" title="复制 Codex MCP 配置 JSON(mcpServers 片段)">{{ mcpCopied ? '已复制' : '复制配置' }}</button>
</div>
<div class="mcp-tip">在 Codex/支持 MCP 的客户端中粘贴复制的 mcpServers 配置,即可让 AI 连接当前游戏。</div>
</div>
</div>
</div>
<div class="split">
<div class="tree-panel" :style="{ width: treeWidth + 'px' }">
<div class="tree-head">
<input v-model="filter" placeholder="筛选节点名…" class="cmd-input" />
<span class="scene">场景: {{ tree.scene || '-' }}</span>
</div>
<div class="tree-body" v-if="!tree.lastError">
<div v-for="row in flatTree"
:key="row.id"
class="tree-row"
:class="{ selected: selectedId === row.id }"
:data-id="row.id"
:style="{ paddingLeft: (8 + row.depth * 14) + 'px', '--indent': (8 + row.depth * 14) + 'px' }"
@click="select(row)">
<span class="expander" v-if="row.hasChildren"
:class="{ open: expanded[row.id] }"
@click.stop="toggle(row)">
<svg viewBox="0 0 10 10" aria-hidden="true"><path d="M2.5 1.5 7.5 5 2.5 8.5z"/></svg>
</span>
<span v-else class="expander placeholder"></span>
<ui-icon class="node-ico" :class="{ inactive: !row.active }" :value="row.type || 'node'" default="node"></ui-icon>
<span class="name" :class="{ inactive: !row.active }">{{ row.name }}</span>
<span class="badge hide-badge" v-if="!row.active">隐藏</span>
<span class="badge">{{ row.comps }}</span>
</div>
<div v-if="!flatTree.length" class="empty-hint">(空)无节点或未连接</div>
</div>
<div class="empty-hint" v-else>{{ tree.lastError }}</div>
</div>
<div class="divider" @pointerdown="startDrag('left', $event)"></div>
<div class="preview-col">
<div class="preview-toolbar">
<span class="p-online" :class="{ ok: !!previewLoaded }">
<i class="dot"></i>{{ previewLoaded ? '在线' : (previewOnline === false ? '未启动' : '') }}
</span>
<span class="p-sep"></span>
<span class="p-group" title="设备 / 分辨率">
<ui-icon value="computer"></ui-icon>
<ui-select class="p-device" :value="previewDevice" @confirm="onPreviewDevice($event.target.value)">
<option value="Default">设计分辨率</option>
<option value="FullScreen">全屏</option>
<option value="WebpageFullScreen">网页全屏</option>
<option value="Apple iPhone 12; 13; 14; 12/13 Pro">iPhone 12/13/14 (390×844)</option>
<option value="Apple iPhone 14 Pro">iPhone 14 Pro (393×852)</option>
<option value="Apple iPhone 12/13 Pro Max; 14 Plus">iPhone Pro Max/14 Plus (428×926)</option>
<option value="Apple iPhone XR; 11">iPhone XR/11 (414×896)</option>
<option value="Apple iPhone X; XS; 11 Pro; 12/13 Mini">iPhone X/XS (375×812)</option>
<option value="Apple iPad 10.2">iPad 10.2 (1620×2160)</option>
<option value="HUAWEI Mate 40 Pro">HUAWEI Mate 40 Pro (448×924)</option>
<option value="OPPO Reno 2">OPPO Reno 2 (360×800)</option>
<option value="Xiaomi Redmi 8">Xiaomi Redmi 8 (360×760)</option>
<option value="Lenovo Legion 2 Pro">Lenovo Legion 2 Pro (360×820)</option>
</ui-select>
</span>
<button class="pbtn" :class="{ on: previewRotated }" @click="onPreviewRotate" title="横竖屏切换"><ui-icon value="rotate"></ui-icon></button>
<span class="p-group p-fps" title="帧率上限">
<ui-icon value="setting"></ui-icon>
<ui-num-input class="p-fps-in" :value="String(previewFps)" @confirm="onPreviewFps($event.target.value)"></ui-num-input>
</span>
<span class="p-sep"></span>
<button class="pbtn" :class="{ on: previewShowFps }" @click="onPreviewShowFps" title="显示 / 隐藏帧率"><ui-icon :value="previewShowFps ? 'eye-open' : 'eye-close'"></ui-icon></button>
<button class="pbtn" :class="{ on: previewPaused }" @click="onPreviewPause" :title="previewPaused ? '继续' : '暂停'"><ui-icon :value="previewPaused ? 'play' : 'pause'"></ui-icon></button>
<button class="pbtn" :class="{ on: previewMuted }" @click="onPreviewMute" title="静音 / 取消静音"><ui-icon value="music"></ui-icon></button>
<button class="pbtn" v-if="previewPaused" @click="onPreviewStep" title="步进一帧"><ui-icon value="next-step"></ui-icon></button>
<span class="spacer"></span>
<button class="pbtn major" @click="doRefreshPreview" title="重新加载预览"><ui-icon value="refresh"></ui-icon></button>
</div>
<div class="preview-frame-wrap">
<!-- 游戏 DOM 由 preview.ts 构建到顶层 document 的 #yyka-game-host 定位容器上
(面板内容可能位于 shadow DOM,Ui 构造的 document.querySelector 找不到面板内部元素);
.game-view 仅作布局占位与定位参考(getBoundingClientRect 对 shadow 内元素同样返回视口坐标)。
滚轮在预览区缩放:#GameDiv 做 CSS transform,不触发引擎重排 -->
<div v-show="showPreview" ref="gameView" class="game-view"></div>
<div class="p-zoom" v-show="showPreview && previewZoom !== 1">{{ Math.round(previewZoom * 100) }}%</div>
<div class="preview-err" v-if="showPreview && previewRenderErr" @click="onLoadPreview" title="点击重新加载">{{ previewRenderErr }}</div>
<div class="preview-cover" v-if="showPreview && previewOnline === false">
<div class="cover-box">
<b>预览服务未启动</b>
<div>请在 Cocos Creator 中点击「浏览器预览」启动预览服务,<br/>或在上方地址栏修改预览地址后点「刷新」。</div>
<button @click="onLoadPreview">重试</button>
</div>
</div>
</div>
<div class="statusbar">
<span class="spacer"></span>
<span v-if="state.clients > 0" class="st-item-group">
<span class="st-item st-fps"><b>{{ stats.fps }}</b> 帧率</span>
<span class="st-sep">|</span>
<span class="st-item st-dc">绘制调用 <b>{{ stats.drawCall }}</b></span>
<span class="st-sep">|</span>
<span class="st-item st-tri">三角面 <b>{{ stats.triangles.toLocaleString() }}</b></span>
<span class="st-sep" v-if="stats.memUsed > 0">|</span>
<span class="st-item st-mem" v-if="stats.memUsed > 0">内存 <b>{{ stats.memUsed }}MB</b></span>
<span class="st-sep" v-if="physicsInfo">|</span>
<span class="st-item st-phy" v-if="physicsInfo">物理 <b>{{ physicsInfo.colliders + physicsInfo.colliders2d }}</b>碰撞 / <b>{{ physicsInfo.bodies + physicsInfo.bodies2d }}</b>刚体</span>
</span>
<span v-else class="st-item dim">未连接</span>
<span class="spacer"></span>
</div>
<div class="perf-panel" v-if="showPerf">
<div class="perf-head">
<b>性能曲线</b>
<span class="legend"><i class="dot fps"></i>FPS</span>
<span class="legend"><i class="dot dc"></i>DrawCall</span>
<span class="spacer"></span>
<button class="mini" @click="showPerf = false">关闭</button>
</div>
<canvas ref="perfCanvas" class="perf-canvas" @mousemove="onPerfMove" @mouseleave="onPerfLeave"></canvas>
</div>
</div>
<div class="divider" v-if="showPreview" @pointerdown="startDrag('right', $event)"></div>
<div class="detail-panel" :style="{ width: detailWidth + 'px' }">
<div class="detail-head">
节点属性
</div>
<div class="detail-body">
<div class="empty-hint" v-if="!selectedId">选中左侧节点查看属性</div>
<div v-else-if="inspectorLoading" class="empty-hint">加载中…</div>
<div v-else-if="inspector && inspector.error" class="empty-hint">{{ inspector.error }}</div>
<template v-else-if="inspector">
<div class="detail-section">
<div class="section-title">节点</div>
<div class="detail-row"><span class="k">名称</span><span class="v">{{ inspector.name }}</span></div>
<div class="detail-row">
<span class="k">激活</span>
<ui-checkbox :value="String(!!inspector.props.active.v)" @confirm="onNodeBool('active', chkVal($event.target.value))"></ui-checkbox>
</div>
<div class="detail-row">
<span class="k">层级</span>
<ui-num-input class="v-editor" step="1" :value="fmtNum(inspector.props.layer.v)" @confirm="onNodeNumber('layer', $event.target.value)"></ui-num-input>
</div>
<div class="detail-row" v-if="inspector.props.position.editable">
<span class="k">位置</span>
<span class="vec">
<ui-num-input :value="fmtNum(inspector.props.position.v.x)" @confirm="onNodeVec('position','x',$event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(inspector.props.position.v.y)" @confirm="onNodeVec('position','y',$event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(inspector.props.position.v.z)" @confirm="onNodeVec('position','z',$event.target.value)"></ui-num-input>
</span>
</div>
<div class="detail-row" v-if="inspector.props.euler.editable">
<span class="k">旋转</span>
<span class="vec">
<ui-num-input :value="fmtNum(inspector.props.euler.v.x)" @confirm="onNodeVec('euler','x',$event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(inspector.props.euler.v.y)" @confirm="onNodeVec('euler','y',$event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(inspector.props.euler.v.z)" @confirm="onNodeVec('euler','z',$event.target.value)"></ui-num-input>
</span>
</div>
<div class="detail-row" v-if="inspector.props.scale.editable">
<span class="k">缩放</span>
<span class="vec">
<ui-num-input :value="fmtNum(inspector.props.scale.v.x)" @confirm="onNodeVec('scale','x',$event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(inspector.props.scale.v.y)" @confirm="onNodeVec('scale','y',$event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(inspector.props.scale.v.z)" @confirm="onNodeVec('scale','z',$event.target.value)"></ui-num-input>
</span>
</div>
<div class="detail-row"><span class="k">世界坐标</span><span class="v">{{ fmtNum(inspector.props.worldPosition.v.x) }}, {{ fmtNum(inspector.props.worldPosition.v.y) }}, {{ fmtNum(inspector.props.worldPosition.v.z) }}</span></div>
</div>
<div class="detail-section" v-for="(comp, ci) in inspector.components" :key="ci">
<div class="comp-head">
<ui-checkbox :value="String(!!comp.enabled)" @confirm="onCompEnabled(ci, chkVal($event.target.value))"></ui-checkbox>
<b class="comp-type">{{ comp.type }}</b>
</div>
<template v-if="comp.widget">
<div class="detail-row">
<span class="k">对齐模式</span>
<ui-select class="v-editor" :value="String(comp.widget.alignMode)" @confirm="onWidgetSet(ci, 'alignMode', $event.target.value)">
<option value="0">对齐一次</option>
<option value="1">窗口缩放时</option>
</ui-select>
</div>
<div class="widget-grid">
<div class="wg-cell" :class="{ on: comp.widget.isAlignTop && comp.widget.isAlignLeft }" @click="onWidgetToggle(ci, '__top_left')" title="左上角对齐"></div>
<div class="wg-cell" :class="{ on: comp.widget.isAlignTop }" @click="onWidgetToggle(ci, 'isAlignTop')" title="顶部对齐"></div>
<div class="wg-cell" :class="{ on: comp.widget.isAlignTop && comp.widget.isAlignRight }" @click="onWidgetToggle(ci, '__top_right')" title="右上角对齐"></div>
<div class="wg-cell" :class="{ on: comp.widget.isAlignLeft }" @click="onWidgetToggle(ci, 'isAlignLeft')" title="左对齐"></div>
<div class="wg-cell wg-center" :class="{ on: comp.widget.isAlignHorizontalCenter && comp.widget.isAlignVerticalCenter }" @click="onWidgetToggle(ci, 'center')" title="水平+垂直居中"></div>
<div class="wg-cell" :class="{ on: comp.widget.isAlignRight }" @click="onWidgetToggle(ci, 'isAlignRight')" title="右对齐"></div>
<div class="wg-cell" :class="{ on: comp.widget.isAlignBottom && comp.widget.isAlignLeft }" @click="onWidgetToggle(ci, '__bottom_left')" title="左下角对齐"></div>
<div class="wg-cell" :class="{ on: comp.widget.isAlignBottom }" @click="onWidgetToggle(ci, 'isAlignBottom')" title="底部对齐"></div>
<div class="wg-cell" :class="{ on: comp.widget.isAlignBottom && comp.widget.isAlignRight }" @click="onWidgetToggle(ci, '__bottom_right')" title="右下角对齐"></div>
</div>
<div class="widget-row">
<span class="k">左</span>
<button class="wg-abs" :class="{ on: comp.widget.isAbsLeft }" @click="onWidgetToggle(ci, 'isAbsLeft')" title="绝对/百分比">Abs</button>
<ui-num-input class="v-editor" :value="fmtNum(comp.widget.left)" @confirm="onWidgetSet(ci, 'left', $event.target.value)"></ui-num-input>
<span class="k">右</span>
<button class="wg-abs" :class="{ on: comp.widget.isAbsRight }" @click="onWidgetToggle(ci, 'isAbsRight')">Abs</button>
<ui-num-input class="v-editor" :value="fmtNum(comp.widget.right)" @confirm="onWidgetSet(ci, 'right', $event.target.value)"></ui-num-input>
</div>
<div class="widget-row">
<span class="k">上</span>
<button class="wg-abs" :class="{ on: comp.widget.isAbsTop }" @click="onWidgetToggle(ci, 'isAbsTop')">Abs</button>
<ui-num-input class="v-editor" :value="fmtNum(comp.widget.top)" @confirm="onWidgetSet(ci, 'top', $event.target.value)"></ui-num-input>
<span class="k">下</span>
<button class="wg-abs" :class="{ on: comp.widget.isAbsBottom }" @click="onWidgetToggle(ci, 'isAbsBottom')">Abs</button>
<ui-num-input class="v-editor" :value="fmtNum(comp.widget.bottom)" @confirm="onWidgetSet(ci, 'bottom', $event.target.value)"></ui-num-input>
</div>
<div class="detail-row">
<span class="k">目标</span>
<span class="ref-wrap">
<span class="ref-pick" :class="{ empty: !comp.widget.targetName }" @click="openNodePicker('widget', ci, '__widget_target', 'cc.Node')">
<ui-icon value="node"></ui-icon>
<span class="ref-name">{{ comp.widget.targetName || '选择节点' }}</span>
</span>
<button class="mini" v-if="comp.widget.targetName" @click="onWidgetSet(ci, '__widget_target', '')">置空</button>
</span>
</div>
</template>
<template v-else>
<div class="detail-row" v-for="(p, pi) in comp.props" :key="pi">
<span class="k">{{ p.key }}</span>
<ui-num-input v-if="p.t==='number'" class="v-editor" :value="fmtNum(p.v)" @confirm="onCompScalar(ci, p.key, $event.target.value, 'number')"></ui-num-input>
<ui-input v-else-if="p.t==='string'" class="v-editor" :value="p.v" @confirm="onCompScalar(ci, p.key, $event.target.value, 'string')"></ui-input>
<ui-checkbox v-else-if="p.t==='bool'" :value="String(!!p.v)" @confirm="onCompScalar(ci, p.key, chkVal($event.target.value), 'bool')"></ui-checkbox>
<ui-select v-else-if="p.t==='enum'" class="v-editor" :value="String(p.v)" @confirm="onCompEnum(ci, p.key, $event.target.value)">
<option v-for="op in p.options" :key="op.value" :value="String(op.value)">{{ op.name }} ({{ op.value }})</option>
</ui-select>
<span v-else-if="p.t==='vec3'" class="vec">
<ui-num-input :value="fmtNum(p.v.x)" @confirm="onCompVec(ci, p.key, 'x', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(p.v.y)" @confirm="onCompVec(ci, p.key, 'y', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(p.v.z)" @confirm="onCompVec(ci, p.key, 'z', $event.target.value)"></ui-num-input>
</span>
<span v-else-if="p.t==='vec2'" class="vec">
<ui-num-input :value="fmtNum(p.v.x)" @confirm="onCompVec(ci, p.key, 'x', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(p.v.y)" @confirm="onCompVec(ci, p.key, 'y', $event.target.value)"></ui-num-input>
</span>
<span v-else-if="p.t==='quat'" class="vec">
<ui-num-input :value="fmtNum(p.v.x)" @confirm="onCompVec(ci, p.key, 'x', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(p.v.y)" @confirm="onCompVec(ci, p.key, 'y', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(p.v.z)" @confirm="onCompVec(ci, p.key, 'z', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(p.v.w)" @confirm="onCompVec(ci, p.key, 'w', $event.target.value)"></ui-num-input>
</span>
<span v-else-if="p.t==='vec4'" class="vec">
<ui-num-input :value="fmtNum(p.v.x)" @confirm="onCompVec(ci, p.key, 'x', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(p.v.y)" @confirm="onCompVec(ci, p.key, 'y', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(p.v.z)" @confirm="onCompVec(ci, p.key, 'z', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(p.v.w)" @confirm="onCompVec(ci, p.key, 'w', $event.target.value)"></ui-num-input>
</span>
<span v-else-if="p.t==='size'" class="vec">
<ui-num-input :value="fmtNum(p.v.w)" @confirm="onCompVec(ci, p.key, 'w', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(p.v.h)" @confirm="onCompVec(ci, p.key, 'h', $event.target.value)"></ui-num-input>
</span>
<span v-else-if="p.t==='rect'" class="vec">
<ui-num-input :value="fmtNum(p.v.x)" @confirm="onCompVec(ci, p.key, 'x', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(p.v.y)" @confirm="onCompVec(ci, p.key, 'y', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(p.v.w)" @confirm="onCompVec(ci, p.key, 'w', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(p.v.h)" @confirm="onCompVec(ci, p.key, 'h', $event.target.value)"></ui-num-input>
</span>
<ui-color v-else-if="p.t==='color'" class="color-editor" :value="colorArr(p.v)" @confirm="onCompColor(ci, p.key, $event.target.value)"></ui-color>
<span v-else-if="p.t==='array'" class="array-wrap">
<div v-for="(it, ii) in p.v" :key="ii" class="array-item">
<span class="array-idx">{{ ii }}</span>
<ui-num-input v-if="it.t==='number'" class="v-editor" :value="fmtNum(it.v)" @confirm="onCompArray(ci, p.key, ii, $event.target.value, 'number')"></ui-num-input>
<ui-input v-else-if="it.t==='string'" class="v-editor" :value="it.v" @confirm="onCompArray(ci, p.key, ii, $event.target.value, 'string')"></ui-input>
<ui-checkbox v-else-if="it.t==='bool'" :value="String(!!it.v)" @confirm="onCompArray(ci, p.key, ii, chkVal($event.target.value), 'bool')"></ui-checkbox>
<span v-else-if="it.t==='vec3'" class="vec">
<ui-num-input :value="fmtNum(it.v.x)" @confirm="onCompArrayVec(ci, p.key, ii, 'x', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(it.v.y)" @confirm="onCompArrayVec(ci, p.key, ii, 'y', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(it.v.z)" @confirm="onCompArrayVec(ci, p.key, ii, 'z', $event.target.value)"></ui-num-input>
</span>
<span v-else-if="it.t==='vec2'" class="vec">
<ui-num-input :value="fmtNum(it.v.x)" @confirm="onCompArrayVec(ci, p.key, ii, 'x', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(it.v.y)" @confirm="onCompArrayVec(ci, p.key, ii, 'y', $event.target.value)"></ui-num-input>
</span>
<span v-else-if="it.t==='size'" class="vec">
<ui-num-input :value="fmtNum(it.v.w)" @confirm="onCompArrayVec(ci, p.key, ii, 'w', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(it.v.h)" @confirm="onCompArrayVec(ci, p.key, ii, 'h', $event.target.value)"></ui-num-input>
</span>
<span v-else-if="it.t==='rect'" class="vec">
<ui-num-input :value="fmtNum(it.v.x)" @confirm="onCompArrayVec(ci, p.key, ii, 'x', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(it.v.y)" @confirm="onCompArrayVec(ci, p.key, ii, 'y', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(it.v.w)" @confirm="onCompArrayVec(ci, p.key, ii, 'w', $event.target.value)"></ui-num-input>
<ui-num-input :value="fmtNum(it.v.h)" @confirm="onCompArrayVec(ci, p.key, ii, 'h', $event.target.value)"></ui-num-input>
</span>
<ui-color v-else-if="it.t==='color'" class="color-editor" :value="colorArr(it.v)" @confirm="onCompArrayColor(ci, p.key, ii, $event.target.value)"></ui-color>
<span v-else-if="it.t==='material'" class="ref-wrap mat">
<span class="asset-pick pick" :class="{ empty: !it.v.name }" @click="openAssetPicker(ci, p.key, ii, 'Material')" title="选择材质资产">
<ui-icon value="material"></ui-icon>
<span class="ref-name">{{ it.v.name || it.v.type }}</span>
<span class="asset-type">{{ it.v.type }}</span>
</span>
<button class="mini" @click="onCompArrayNull(ci, p.key, ii)">置空</button>
<span v-if="it.v.props && it.v.props.length" class="mat-props">
<div v-for="mp in it.v.props" :key="mp.key" class="mat-prop">
<span class="k" :title="mp.key">{{ mp.key }}</span>
<ui-num-input v-if="mp.t==='float'" :value="fmtNum(mp.v)" @confirm="onCompMaterialProp(ci, p.key, ii, mp.key, toNum($event.target.value))"></ui-num-input>
<ui-color v-else-if="mp.t==='color'" class="color-editor" :value="colorArr(mp.v)" @confirm="onCompMaterialProp(ci, p.key, ii, mp.key, { r: $event.target.value[0], g: $event.target.value[1], b: $event.target.value[2], a: $event.target.value[3] })"></ui-color>
<span v-else-if="mp.t==='vec4' || mp.t==='vec3' || mp.t==='vec2'" class="vec">
<ui-num-input v-for="ax in vecAxes(mp.t)" :key="ax" :value="fmtNum(mp.v[ax])" @confirm="onCompMaterialProp(ci, p.key, ii, mp.key, { ...mp.v, [ax]: toNum($event.target.value) })"></ui-num-input>
</span>
<span v-else-if="mp.t==='texture'" class="asset-pick tex">
<ui-icon :value="assetIcon(mp.v.type)"></ui-icon>
<span class="ref-name">{{ mp.v.name || mp.v.type }}</span>
<span class="asset-type">{{ mp.v.type }}</span>
</span>
</div>
</span>
</span>
<span v-else-if="it.t==='asset'" class="ref-wrap">
<span class="asset-pick pick" :class="{ empty: !it.v.name }" @click="it.v.settable && openAssetPicker(ci, p.key, ii, it.v.type)" title="选择资产">
<ui-icon :value="assetIcon(it.v.type)"></ui-icon>
<span class="ref-name">{{ it.v.name || it.v.type }}</span>
<span class="asset-type">{{ it.v.type }}</span>
</span>
<button class="mini" @click="onCompArrayNull(ci, p.key, ii)">置空</button>
</span>
<span v-else-if="it.t==='ref'" class="ref-wrap">
<span class="ref-pick" :class="{ component: !isNodeRef(it.v) }">
<ui-icon :value="isNodeRef(it.v) ? 'node' : 'component'"></ui-icon>
<span class="ref-name">{{ it.v.name || '?' }}</span>
<span class="ref-type" v-if="!isNodeRef(it.v)">{{ it.v.componentType }}</span>
</span>
<button class="mini" @click="onCompArrayNull(ci, p.key, ii)">置空</button>
</span>
<span v-else class="v">{{ it.v }}</span>
</div>
<span v-if="p.len > p.v.length" class="array-more">… 共 {{ p.len }} 项</span>
</span>
<span v-else-if="p.t==='material'" class="ref-wrap mat">
<span class="asset-pick pick" :class="{ empty: !p.v.name }" @click="p.v.settable && openAssetPicker(ci, p.key, -1, 'Material')" title="选择材质资产">
<ui-icon value="material"></ui-icon>
<span class="ref-name">{{ p.v.name || p.v.type }}</span>
<span class="asset-type">{{ p.v.type }}</span>
</span>
<button class="mini" v-if="p.v.name && p.v.settable" @click="onCompNull(ci, p.key)">置空</button>
<span v-if="p.v.props && p.v.props.length" class="mat-props">
<div v-for="mp in p.v.props" :key="mp.key" class="mat-prop">
<span class="k" :title="mp.key">{{ mp.key }}</span>
<ui-num-input v-if="mp.t==='float'" :value="fmtNum(mp.v)" @confirm="onCompMaterialProp(ci, p.key, undefined, mp.key, toNum($event.target.value))"></ui-num-input>
<ui-color v-else-if="mp.t==='color'" class="color-editor" :value="colorArr(mp.v)" @confirm="onCompMaterialProp(ci, p.key, undefined, mp.key, { r: $event.target.value[0], g: $event.target.value[1], b: $event.target.value[2], a: $event.target.value[3] })"></ui-color>
<span v-else-if="mp.t==='vec4' || mp.t==='vec3' || mp.t==='vec2'" class="vec">
<ui-num-input v-for="ax in vecAxes(mp.t)" :key="ax" :value="fmtNum(mp.v[ax])" @confirm="onCompMaterialProp(ci, p.key, undefined, mp.key, { ...mp.v, [ax]: toNum($event.target.value) })"></ui-num-input>
</span>
<span v-else-if="mp.t==='texture'" class="asset-pick tex">
<ui-icon :value="assetIcon(mp.v.type)"></ui-icon>
<span class="ref-name">{{ mp.v.name || mp.v.type }}</span>
<span class="asset-type">{{ mp.v.type }}</span>
</span>
</div>
</span>
</span>
<span v-else-if="p.t==='asset'" class="ref-wrap">
<span class="asset-pick pick" :class="{ empty: !p.v.name }" @click="p.v.settable && openAssetPicker(ci, p.key, -1, p.v.type)" title="选择资产">
<ui-icon :value="assetIcon(p.v.type)"></ui-icon>
<span class="ref-name">{{ p.v.name || p.v.type }}</span>
<span class="asset-type">{{ p.v.type }}</span>
</span>
<button class="mini" v-if="p.v.name && p.v.settable" @click="onCompNull(ci, p.key)">置空</button>
</span>
<span v-else-if="p.t==='ref'" class="ref-wrap">
<span class="ref-pick" :class="{ empty: !p.v.name, component: !isNodeRef(p.v) }" @click="openNodePicker('ref', ci, p.key, p.v.componentType || 'cc.Node')">
<ui-icon :value="isNodeRef(p.v) ? 'node' : 'component'"></ui-icon>
<span class="ref-name">{{ p.v.name || '选择节点' }}</span>
<span class="ref-type" v-if="!isNodeRef(p.v)">{{ p.v.componentType }}</span>
</span>
<button class="mini" v-if="p.v.name" @click="onCompNull(ci, p.key)">置空</button>
</span>
<span v-else class="v">{{ fmt(p.v) }}</span>
</div>
<div v-if="!comp.props.length" class="empty-hint">(无可编辑属性)</div>
</template>
</div>
</template>
</div>
</div>
</div>
<div class="bottom-row">
<div class="res-panel" v-if="showRes">
<div class="res-head">
<b>资源统计</b>
<span class="res-total" v-if="resStats">已加载 <b>{{ totalAssets() }}</b> 项</span>
<span class="spacer"></span>
<button class="mini" @click="showRes = false">关闭</button>
</div>
<div class="res-body" v-if="resStats">
<div class="res-group">
<div class="res-group-title">运行时对象</div>
<div class="res-grid">
<div class="res-item" v-for="it in runItems()" :key="it.label">
<span class="res-dot" :class="it.color">{{ it.ico }}</span>
<span class="res-label">{{ it.label }}</span>
<b class="res-value">{{ it.value }}</b>
</div>
</div>
</div>
<div class="res-group">
<div class="res-group-title">已加载资源</div>
<div class="res-grid">
<div class="res-item" v-for="it in resItems()" :key="it.label">
<span class="res-dot" :class="it.color">{{ it.ico }}</span>
<span class="res-label">{{ it.label }}</span>
<b class="res-value">{{ it.value }}</b>
</div>
</div>
</div>
</div>
<div class="empty-hint" v-else>(加载中…)</div>
</div>
<div class="logbar-wrap">
<div class="logbar-head">
<span class="log-title" @click="showLogs = !showLogs" title="展开/收起日志">
<svg class="log-fold" :class="{ open: showLogs }" viewBox="0 0 10 10" aria-hidden="true"><path d="M2.5 1.5 7.5 5 2.5 8.5z"/></svg>
日志<span class="log-count">({{ logs.length }})</span>
</span>
<span class="spacer"></span>
<span class="log-filters" v-if="showLogs">
<button class="mini" :class="{ active: logFilter === 'all' }" @click="logFilter = 'all'">全部</button>
<button class="mini" :class="{ active: logFilter === 'log' }" @click="logFilter = 'log'">log</button>
<button class="mini" :class="{ active: logFilter === 'warn' }" @click="logFilter = 'warn'">warn</button>
<button class="mini" :class="{ active: logFilter === 'error' }" @click="logFilter = 'error'">error</button>
</span>
<span class="copied-hint" v-if="copyHint">已复制</span>
<button v-if="showLogs" class="mini" @click="clearLogs">清空</button>
<button v-if="showLogs" class="mini" @click="copyLogs">复制全部</button>
<button class="mini" @click="showLogs = !showLogs">{{ showLogs ? '收起' : '展开' }}</button>
</div>
<div class="logbar" v-if="showLogs" ref="logList" @scroll="onLogScroll">
<div v-for="(l, i) in filteredLogs" :key="i" class="log-item" :class="logClass(l)">
<span class="log-badge" :class="'b-' + logLevel(l)">{{ logLevelText(l) }}</span>
<span class="log-time">{{ logTime(l) }}</span>
<span class="log-text">{{ logText(l) }}</span>
<button class="mini copy-line" title="复制此行" @click="copyLogLine(l)">⧉</button>
</div>
<div v-if="!filteredLogs.length" class="empty-hint">(暂无日志)</div>
</div>
</div>
</div>
<!-- 节点选择弹层(Widget 对齐目标 / 组件引用) -->
<div class="node-picker-mask" v-if="nodePicker.visible" @click.self="closeNodePicker">
<div class="node-picker">
<div class="np-head">
<span class="np-title">
{{ nodePicker.mode === 'widget' ? '选择对齐目标节点' : (nodePicker.componentType === 'cc.Node' ? '选择引用节点' : '选择 ' + nodePicker.componentType + ' 组件所在节点') }}
</span>
<ui-input class="np-filter" :value="nodePicker.filter" @change="nodePicker.filter = $event.target.value" placeholder="搜索节点…" show-clear></ui-input>
<button class="mini" @click="closeNodePicker">关闭</button>
</div>
<div class="np-list">
<div v-for="r in pickerRows" :key="r.id" class="np-row" :class="{ 'has-children': r.hasChildren }" :style="{ paddingLeft: (14 + r.depth * 14) + 'px' }" @click="onNodePick(r)">
<ui-icon :value="r.hasChildren ? 'folder' : 'node'"></ui-icon>
<span class="np-name">{{ r.name }}</span>
<span class="np-type" v-if="r.type">{{ r.type }}</span>
</div>
<div v-if="!pickerRows.length" class="empty-hint">(无匹配节点)</div>
</div>
</div>
</div>
<!-- 资产选择弹层(spriteFrame / material 等资产引用) -->
<div class="node-picker-mask" v-if="assetPicker.visible" @click.self="closeAssetPicker">
<div class="node-picker">
<div class="np-head">
<span class="np-title">选择资产{{ assetPicker.type ? '(' + assetPicker.type + ')' : '' }}</span>
<select class="ap-type" :value="assetPicker.type" @change="assetPicker.type = $event.target.value">
<option value="">全部类型</option>
<option v-for="t in assetPicker.types" :key="t" :value="t">{{ t }}</option>
</select>
<ui-input class="np-filter" :value="assetPicker.filter" @change="assetPicker.filter = $event.target.value" placeholder="搜索资产…" show-clear></ui-input>
<button class="mini" @click="closeAssetPicker">关闭</button>
</div>
<div class="np-list">
<div v-if="assetPicker.loading" class="empty-hint">(加载中…)</div>
<div v-else v-for="r in assetRows" :key="r.uuid || r.name" class="np-row" @click="onAssetPick(r)">
<ui-icon :value="assetIcon(r.type)"></ui-icon>
<span class="np-name">{{ r.name || r.uuid }}</span>
<span class="np-type">{{ r.type }}</span>
</div>
<div v-if="!assetPicker.loading && !assetRows.length" class="empty-hint">(无匹配资产)</div>
</div>
</div>
</div>
</div>