humingxing 2 weeks ago
parent 1f1086bdee
commit 5a630aed3f
  1. 1844
      assets/RuntimeBridge.ts
  2. 9
      assets/RuntimeBridge.ts.meta
  3. 85
      assets/scene/main.scene
  4. 1
      extensions/YYKA运行时工作台/.release-sig
  5. 5
      extensions/YYKA运行时工作台/README.txt
  6. 615
      extensions/YYKA运行时工作台/dist/main.js
  7. 517
      extensions/YYKA运行时工作台/dist/mcp-server.js
  8. 69
      extensions/YYKA运行时工作台/dist/panels/default/computed.js
  9. 65
      extensions/YYKA运行时工作台/dist/panels/default/index.js
  10. 56
      extensions/YYKA运行时工作台/dist/panels/default/lifecycle.js
  11. 376
      extensions/YYKA运行时工作台/dist/panels/default/methods/inspector.js
  12. 66
      extensions/YYKA运行时工作台/dist/panels/default/methods/logs.js
  13. 81
      extensions/YYKA运行时工作台/dist/panels/default/methods/mcp.js
  14. 209
      extensions/YYKA运行时工作台/dist/panels/default/methods/perf.js
  15. 121
      extensions/YYKA运行时工作台/dist/panels/default/methods/pickers.js
  16. 1658
      extensions/YYKA运行时工作台/dist/panels/default/methods/preview.js
  17. 60
      extensions/YYKA运行时工作台/dist/panels/default/methods/res.js
  18. 117
      extensions/YYKA运行时工作台/dist/panels/default/methods/server.js
  19. 150
      extensions/YYKA运行时工作台/dist/panels/default/methods/tree.js
  20. 119
      extensions/YYKA运行时工作台/dist/panels/default/methods/utils.js
  21. 84
      extensions/YYKA运行时工作台/dist/panels/default/state.js
  22. 3
      extensions/YYKA运行时工作台/dist/panels/default/types.js
  23. 60
      extensions/YYKA运行时工作台/dist/panels/default/watch.js
  24. 98
      extensions/YYKA运行时工作台/dist/panels/preview/index.js
  25. 152
      extensions/YYKA运行时工作台/dist/ws-server.js
  26. 1
      extensions/YYKA运行时工作台/i18n/en.js
  27. 1
      extensions/YYKA运行时工作台/i18n/zh.js
  28. 142
      extensions/YYKA运行时工作台/package.json
  29. 86
      extensions/YYKA运行时工作台/runtime-bridge-sample/RuntimeBridge.js
  30. 1844
      extensions/YYKA运行时工作台/runtime-bridge-sample/RuntimeBridge.ts
  31. 2056
      extensions/YYKA运行时工作台/static/style/default/index.css
  32. 293
      extensions/YYKA运行时工作台/static/style/preview/index.css
  33. 533
      extensions/YYKA运行时工作台/static/template/default/app.html
  34. 3
      extensions/YYKA运行时工作台/static/template/default/index.html
  35. 38
      extensions/YYKA运行时工作台/static/template/preview/index.html
  36. 1
      extensions/YYKA运行时工作台/static/template/preview/wrapper.html
  37. 5
      extensions/YYKA运行时工作台/static/template/vue/counter.html

File diff suppressed because it is too large Load Diff

@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "8ee138cf-ab76-449a-8b25-5ef56e1fe190",
"files": [],
"subMetas": {},
"userData": {}
}

@ -18,12 +18,15 @@
"_children": [ "_children": [
{ {
"__id__": 2 "__id__": 2
},
{
"__id__": 111
} }
], ],
"_active": true, "_active": true,
"_components": [], "_components": [],
"_prefab": { "_prefab": {
"__id__": 111 "__id__": 113
}, },
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
@ -54,7 +57,7 @@
}, },
"autoReleaseAssets": false, "autoReleaseAssets": false,
"_globals": { "_globals": {
"__id__": 112 "__id__": 114
}, },
"_id": "89cd0565-e1e7-4d4c-b1f5-8a3061282934" "_id": "89cd0565-e1e7-4d4c-b1f5-8a3061282934"
}, },
@ -3094,6 +3097,68 @@
"_lockFlags": 0, "_lockFlags": 0,
"_id": "c5V1EV8IpMtrIvY1OE9t2u" "_id": "c5V1EV8IpMtrIvY1OE9t2u"
}, },
{
"__type__": "cc.Node",
"_name": "__yyka_runtime_bridge__",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 112
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "10Wutln0FALLw7Ag4OGxu6"
},
{
"__type__": "8ee13jPq3ZEmoslXvVuH+GQ",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 111
},
"_enabled": true,
"__prefab": null,
"autoConnect": true,
"host": "192.168.2.17",
"port": 33000,
"reconnectDelay": 2,
"previewFps": 0,
"_id": "8cJOa28jFPzKVV0UycbPQZ"
},
{ {
"__type__": "cc.PrefabInfo", "__type__": "cc.PrefabInfo",
"root": null, "root": null,
@ -3113,28 +3178,28 @@
{ {
"__type__": "cc.SceneGlobals", "__type__": "cc.SceneGlobals",
"ambient": { "ambient": {
"__id__": 113 "__id__": 115
}, },
"shadows": { "shadows": {
"__id__": 114 "__id__": 116
}, },
"_skybox": { "_skybox": {
"__id__": 115 "__id__": 117
}, },
"fog": { "fog": {
"__id__": 116 "__id__": 118
}, },
"octree": { "octree": {
"__id__": 117 "__id__": 119
}, },
"skin": { "skin": {
"__id__": 118 "__id__": 120
}, },
"lightProbeInfo": { "lightProbeInfo": {
"__id__": 119 "__id__": 121
}, },
"postSettings": { "postSettings": {
"__id__": 120 "__id__": 122
}, },
"bakedWithStationaryMainLight": false, "bakedWithStationaryMainLight": false,
"bakedWithHighpLightmap": false "bakedWithHighpLightmap": false

@ -0,0 +1 @@
bc52de6deb41df5b03ab84d33014e35c

@ -0,0 +1,5 @@
yyka-runtime-inspector 1.0.254
----------------------------------------
将整个目录作为 Cocos Creator 扩展安装(或复制到扩展目录)。
依赖已随目录复制(node_modules),无需额外 npm install。
游戏侧桥接脚本样例见 runtime-bridge-sample(未打包,需从源码取)。

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zb3VyY2UvcGFuZWxzL2RlZmF1bHQvdHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBpbnRlcmZhY2UgU2VydmVyU3RhdGUge1xuICAgIHJ1bm5pbmc6IGJvb2xlYW47XG4gICAgcG9ydDogbnVtYmVyO1xuICAgIGNsaWVudHM6IG51bWJlcjtcbiAgICBjbGllbnRUeXBlOiBzdHJpbmc7XG4gICAgY2xpZW50VXJsOiBzdHJpbmc7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgVHJlZU5vZGUge1xuICAgIGlkOiBzdHJpbmc7XG4gICAgbmFtZTogc3RyaW5nO1xuICAgIGFjdGl2ZTogYm9vbGVhbjtcbiAgICBsYXllcjogbnVtYmVyO1xuICAgIGNvbXBzOiBudW1iZXI7XG4gICAgdHlwZT86IHN0cmluZztcbiAgICBjaGlsZHJlbjogVHJlZU5vZGVbXTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBUcmVlUm93IGV4dGVuZHMgVHJlZU5vZGUge1xuICAgIGRlcHRoOiBudW1iZXI7XG4gICAgaGFzQ2hpbGRyZW46IGJvb2xlYW47XG59XG4iXX0=

@ -0,0 +1,60 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
// @ts-ignore
const package_json_1 = __importDefault(require("../../../package.json"));
exports.default = {
// 日志新增/清空/过滤切换/展开时自动滚动到底部
logs: {
deep: true,
handler() {
this.scrollLogToBottom();
},
},
logFilter() {
this.scrollLogToBottom();
},
showLogs(nv) {
if (nv) {
this.scrollLogToBottom();
}
},
showPreview(nv) {
// 游戏 DOM 静态常驻(v-show 仅控制显隐,DOM 不销毁),切换回来无需重建;
// 仅当引擎尚未启动/上次加载失败时才重新渲染
if (nv && !this.previewLoaded) {
this.$nextTick(() => this.renderPreview());
}
},
filter() {
window.clearTimeout(this.filterTimer);
this.filterTimer = window.setTimeout(() => this.refreshTree(), 350);
},
physicsDebug(nv) {
Editor.Message.request(package_json_1.default.name, 'send-command', { cmd: 'set-physics-debug', payload: { enabled: !!nv } }).catch(() => { });
},
showPerf(nv) {
if (nv) {
this.$nextTick(() => this.drawPerf());
// 开启时每秒采样一次(1 点/秒),曲线窗口为最近 90 秒
this.stopPerfSampling();
this.perfTimer = window.setInterval(() => this.samplePerf(), 1000);
}
else {
this.stopPerfSampling();
}
},
showRes(nv) {
if (nv) {
this.loadResStats();
this.resTimer = window.setInterval(() => this.loadResStats(), 3000);
}
else if (this.resTimer) {
window.clearInterval(this.resTimer);
this.resTimer = 0;
}
},
};
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid2F0Y2guanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zb3VyY2UvcGFuZWxzL2RlZmF1bHQvd2F0Y2gudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBQSxhQUFhO0FBQ2IseUVBQWdEO0FBRWhELGtCQUFlO0lBQ1gsMEJBQTBCO0lBQzFCLElBQUksRUFBRTtRQUNGLElBQUksRUFBRSxJQUFJO1FBQ1YsT0FBTztZQUNILElBQUksQ0FBQyxpQkFBaUIsRUFBRSxDQUFDO1FBQzdCLENBQUM7S0FDSjtJQUNELFNBQVM7UUFDTCxJQUFJLENBQUMsaUJBQWlCLEVBQUUsQ0FBQztJQUM3QixDQUFDO0lBQ0QsUUFBUSxDQUFZLEVBQVc7UUFDM0IsSUFBSSxFQUFFLEVBQUUsQ0FBQztZQUNMLElBQUksQ0FBQyxpQkFBaUIsRUFBRSxDQUFDO1FBQzdCLENBQUM7SUFDTCxDQUFDO0lBQ0QsV0FBVyxDQUFZLEVBQVc7UUFDOUIsOENBQThDO1FBQzlDLHdCQUF3QjtRQUN4QixJQUFJLEVBQUUsSUFBSSxDQUFDLElBQUksQ0FBQyxhQUFhLEVBQUUsQ0FBQztZQUM1QixJQUFJLENBQUMsU0FBUyxDQUFDLEdBQUcsRUFBRSxDQUFDLElBQUksQ0FBQyxhQUFhLEVBQUUsQ0FBQyxDQUFDO1FBQy9DLENBQUM7SUFDTCxDQUFDO0lBQ0QsTUFBTTtRQUNGLE1BQU0sQ0FBQyxZQUFZLENBQUMsSUFBSSxDQUFDLFdBQVcsQ0FBQyxDQUFDO1FBQ3RDLElBQUksQ0FBQyxXQUFXLEdBQUcsTUFBTSxDQUFDLFVBQVUsQ0FBQyxHQUFHLEVBQUUsQ0FBQyxJQUFJLENBQUMsV0FBVyxFQUFFLEVBQUUsR0FBRyxDQUFDLENBQUM7SUFDeEUsQ0FBQztJQUNELFlBQVksQ0FBWSxFQUFXO1FBQy9CLE1BQU0sQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUNsQixzQkFBVyxDQUFDLElBQVcsRUFDdkIsY0FBcUIsRUFDckIsRUFBRSxHQUFHLEVBQUUsbUJBQW1CLEVBQUUsT0FBTyxFQUFFLEVBQUUsT0FBTyxFQUFFLENBQUMsQ0FBQyxFQUFFLEVBQUUsRUFBUyxDQUNsRSxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUUsR0FBRSxDQUFDLENBQUMsQ0FBQztJQUN0QixDQUFDO0lBQ0QsUUFBUSxDQUFZLEVBQVc7UUFDM0IsSUFBSSxFQUFFLEVBQUUsQ0FBQztZQUNMLElBQUksQ0FBQyxTQUFTLENBQUMsR0FBRyxFQUFFLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxDQUFDLENBQUM7WUFDdEMsZ0NBQWdDO1lBQ2hDLElBQUksQ0FBQyxnQkFBZ0IsRUFBRSxDQUFDO1lBQ3hCLElBQUksQ0FBQyxTQUFTLEdBQUcsTUFBTSxDQUFDLFdBQVcsQ0FBQyxHQUFHLEVBQUUsQ0FBQyxJQUFJLENBQUMsVUFBVSxFQUFFLEVBQUUsSUFBSSxDQUFDLENBQUM7UUFDdkUsQ0FBQzthQUFNLENBQUM7WUFDSixJQUFJLENBQUMsZ0JBQWdCLEVBQUUsQ0FBQztRQUM1QixDQUFDO0lBQ0wsQ0FBQztJQUNELE9BQU8sQ0FBWSxFQUFXO1FBQzFCLElBQUksRUFBRSxFQUFFLENBQUM7WUFDTCxJQUFJLENBQUMsWUFBWSxFQUFFLENBQUM7WUFDcEIsSUFBSSxDQUFDLFFBQVEsR0FBRyxNQUFNLENBQUMsV0FBVyxDQUFDLEdBQUcsRUFBRSxDQUFDLElBQUksQ0FBQyxZQUFZLEVBQUUsRUFBRSxJQUFJLENBQUMsQ0FBQztRQUN4RSxDQUFDO2FBQU0sSUFBSSxJQUFJLENBQUMsUUFBUSxFQUFFLENBQUM7WUFDdkIsTUFBTSxDQUFDLGFBQWEsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUM7WUFDcEMsSUFBSSxDQUFDLFFBQVEsR0FBRyxDQUFDLENBQUM7UUFDdEIsQ0FBQztJQUNMLENBQUM7Q0FDSixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQHRzLWlnbm9yZVxuaW1wb3J0IHBhY2thZ2VKU09OIGZyb20gJy4uLy4uLy4uL3BhY2thZ2UuanNvbic7XG5cbmV4cG9ydCBkZWZhdWx0IHtcbiAgICAvLyDml6Xlv5fmlrDlop4v5riF56m6L+i/h+a7pOWIh+aNoi/lsZXlvIDml7boh6rliqjmu5rliqjliLDlupXpg6hcbiAgICBsb2dzOiB7XG4gICAgICAgIGRlZXA6IHRydWUsXG4gICAgICAgIGhhbmRsZXIodGhpczogYW55KSB7XG4gICAgICAgICAgICB0aGlzLnNjcm9sbExvZ1RvQm90dG9tKCk7XG4gICAgICAgIH0sXG4gICAgfSxcbiAgICBsb2dGaWx0ZXIodGhpczogYW55KSB7XG4gICAgICAgIHRoaXMuc2Nyb2xsTG9nVG9Cb3R0b20oKTtcbiAgICB9LFxuICAgIHNob3dMb2dzKHRoaXM6IGFueSwgbnY6IGJvb2xlYW4pIHtcbiAgICAgICAgaWYgKG52KSB7XG4gICAgICAgICAgICB0aGlzLnNjcm9sbExvZ1RvQm90dG9tKCk7XG4gICAgICAgIH1cbiAgICB9LFxuICAgIHNob3dQcmV2aWV3KHRoaXM6IGFueSwgbnY6IGJvb2xlYW4pIHtcbiAgICAgICAgLy8g5ri45oiPIERPTSDpnZnmgIHluLjpqbvvvIh2LXNob3cg5LuF5o6n5Yi25pi+6ZqQ77yMRE9NIOS4jemUgOavge+8ie+8jOWIh+aNouWbnuadpeaXoOmcgOmHjeW7uu+8m1xuICAgICAgICAvLyDku4XlvZPlvJXmk47lsJrmnKrlkK/liqgv5LiK5qyh5Yqg6L295aSx6LSl5pe25omN6YeN5paw5riy5p+TXG4gICAgICAgIGlmIChudiAmJiAhdGhpcy5wcmV2aWV3TG9hZGVkKSB7XG4gICAgICAgICAgICB0aGlzLiRuZXh0VGljaygoKSA9PiB0aGlzLnJlbmRlclByZXZpZXcoKSk7XG4gICAgICAgIH1cbiAgICB9LFxuICAgIGZpbHRlcih0aGlzOiBhbnkpIHtcbiAgICAgICAgd2luZG93LmNsZWFyVGltZW91dCh0aGlzLmZpbHRlclRpbWVyKTtcbiAgICAgICAgdGhpcy5maWx0ZXJUaW1lciA9IHdpbmRvdy5zZXRUaW1lb3V0KCgpID0+IHRoaXMucmVmcmVzaFRyZWUoKSwgMzUwKTtcbiAgICB9LFxuICAgIHBoeXNpY3NEZWJ1Zyh0aGlzOiBhbnksIG52OiBib29sZWFuKSB7XG4gICAgICAgIEVkaXRvci5NZXNzYWdlLnJlcXVlc3QoXG4gICAgICAgICAgICBwYWNrYWdlSlNPTi5uYW1lIGFzIGFueSxcbiAgICAgICAgICAgICdzZW5kLWNvbW1hbmQnIGFzIGFueSxcbiAgICAgICAgICAgIHsgY21kOiAnc2V0LXBoeXNpY3MtZGVidWcnLCBwYXlsb2FkOiB7IGVuYWJsZWQ6ICEhbnYgfSB9IGFzIGFueSxcbiAgICAgICAgKS5jYXRjaCgoKSA9PiB7fSk7XG4gICAgfSxcbiAgICBzaG93UGVyZih0aGlzOiBhbnksIG52OiBib29sZWFuKSB7XG4gICAgICAgIGlmIChudikge1xuICAgICAgICAgICAgdGhpcy4kbmV4dFRpY2soKCkgPT4gdGhpcy5kcmF3UGVyZigpKTtcbiAgICAgICAgICAgIC8vIOW8gOWQr+aXtuavj+enkumHh+agt+S4gOasoe+8iDEg54K5L+enku+8ie+8jOabsue6v+eql+WPo+S4uuacgOi/kSA5MCDnp5JcbiAgICAgICAgICAgIHRoaXMuc3RvcFBlcmZTYW1wbGluZygpO1xuICAgICAgICAgICAgdGhpcy5wZXJmVGltZXIgPSB3aW5kb3cuc2V0SW50ZXJ2YWwoKCkgPT4gdGhpcy5zYW1wbGVQZXJmKCksIDEwMDApO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgdGhpcy5zdG9wUGVyZlNhbXBsaW5nKCk7XG4gICAgICAgIH1cbiAgICB9LFxuICAgIHNob3dSZXModGhpczogYW55LCBudjogYm9vbGVhbikge1xuICAgICAgICBpZiAobnYpIHtcbiAgICAgICAgICAgIHRoaXMubG9hZFJlc1N0YXRzKCk7XG4gICAgICAgICAgICB0aGlzLnJlc1RpbWVyID0gd2luZG93LnNldEludGVydmFsKCgpID0+IHRoaXMubG9hZFJlc1N0YXRzKCksIDMwMDApO1xuICAgICAgICB9IGVsc2UgaWYgKHRoaXMucmVzVGltZXIpIHtcbiAgICAgICAgICAgIHdpbmRvdy5jbGVhckludGVydmFsKHRoaXMucmVzVGltZXIpO1xuICAgICAgICAgICAgdGhpcy5yZXNUaW1lciA9IDA7XG4gICAgICAgIH1cbiAgICB9LFxufTtcbiJdfQ==

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
"use strict";module.exports={open_panel:"Run Inspector",preview_panel:"Game Live",default_panel:"Runtime Inspector",menu_group:"Runtime Workbench",menu_root:"YYKA Runtime Inspector",send_to_panel:"Send message to Runtime Inspector",description:"Cocos Creator runtime debug tool: inspect & edit node/component properties, materials & asset references in real time; monitor performance & resources; device-shell preview (rotate/portrait-mute/fps control) and MCP service"};

@ -0,0 +1 @@
"use strict";module.exports={open_panel:"运行检查",preview_panel:"游戏实况",default_panel:"运行时检查器",menu_group:"运行时工作台",menu_root:"YYKA 运行时检查器",send_to_panel:"发送消息给面板",description:"Cocos Creator 运行时调试工具:实时查看并编辑节点/组件属性、材质与资产引用,监控性能与资源,支持设备外壳预览(旋转/横竖屏/静音/帧率控制)及 MCP 服务"};

@ -0,0 +1,142 @@
{
"$schema": "./@types/schema/package/index.json",
"package_version": 2,
"name": "yyka-runtime-inspector",
"version": "1.0.254",
"author": "YYKACN",
"editor": ">=3.8.8",
"description": "i18n:yyka-runtime-inspector.description",
"main": "./dist/main.js",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0",
"fs-extra": "^10.0.0",
"vue": "^3.1.4",
"ws": "^8.18.0"
},
"panels": {
"default": {
"title": "i18n:yyka-runtime-inspector.default_panel",
"type": "dockable",
"main": "dist/panels/default",
"size": {
"min-width": 800,
"min-height": 600,
"width": 1280,
"height": 720
}
},
"preview": {
"title": "i18n:yyka-runtime-inspector.preview_panel",
"type": "dockable",
"main": "dist/panels/preview",
"size": {
"min-width": 320,
"min-height": 240,
"width": 720,
"height": 480
}
}
},
"contributions": {
"menu": [
{
"path": "YYKA插件/i18n:yyka-runtime-inspector.menu_group",
"label": "i18n:yyka-runtime-inspector.open_panel",
"message": "open-panel"
},
{
"path": "YYKA插件/i18n:yyka-runtime-inspector.menu_group",
"label": "i18n:yyka-runtime-inspector.preview_panel",
"message": "open-preview-panel"
}
],
"messages": {
"open-panel": {
"methods": [
"openPanel"
]
},
"open-preview-panel": {
"methods": [
"openPreviewPanel"
]
},
"start-server": {
"methods": [
"startServer"
]
},
"stop-server": {
"methods": [
"stopServer"
]
},
"get-server-state": {
"methods": [
"getServerState"
]
},
"send-command": {
"methods": [
"sendCommand"
]
},
"query-command": {
"methods": [
"queryCommand"
]
},
"get-preview-info": {
"methods": [
"getPreviewInfo"
]
},
"get-projects-settings": {
"methods": [
"getProjectsSettings"
]
},
"query-log": {
"methods": [
"queryLog"
]
},
"install-bridge": {
"methods": [
"installBridge"
]
},
"get-token": {
"methods": [
"getToken"
]
},
"get-logs": {
"methods": [
"getLogs"
]
},
"start-mcp": {
"methods": [
"startMcp"
]
},
"stop-mcp": {
"methods": [
"stopMcp"
]
},
"get-mcp-state": {
"methods": [
"getMcpState"
]
},
"highlight-node": {
"methods": [
"highlightNode"
]
}
}
},
"_storeId": "866bfa25bd721c5d209462bfcfed2da0"
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,293 @@
html,
body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
#app {
height: 100%;
margin: 0;
padding: 0;
}
/* 面板根:占满整个面板,沿用 default 的 preview-col 布局 */
.preview-panel-root {
position: relative;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
border: 1px solid var(--color-normal-border);
background: #171a1f;
overflow: hidden;
}
.preview-toolbar {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
border-bottom: 1px solid var(--color-normal-border);
background: linear-gradient(180deg, #1c1f24, #171a1f);
flex: none;
}
.p-online {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
color: #8b94a3;
white-space: nowrap;
}
.p-online .dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: #5a6270;
flex: none;
}
.p-online.ok {
color: #10b981;
}
.p-online.ok .dot {
background: #34d399;
box-shadow: 0 0 6px #34d399;
}
.p-online.warn {
color: #f59e0b;
}
.p-online.warn .dot {
background: #f59e0b;
box-shadow: 0 0 6px #f59e0b;
}
.p-sep {
width: 1px;
height: 18px;
background: var(--color-normal-border);
margin: 0 4px;
flex: none;
}
.p-group {
display: inline-flex;
align-items: center;
gap: 5px;
color: #8b94a3;
}
.p-group > ui-icon {
font-size: 14px;
}
.p-device {
min-width: 108px;
}
.p-fps-in {
width: 56px;
}
.pbtn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
padding: 0;
border: 1px solid var(--color-normal-border);
border-radius: 5px;
background: #21252b;
color: #a3adbd;
cursor: pointer;
transition: all 0.12s ease;
flex: none;
}
.pbtn:hover {
background: #2a303a;
color: #e6ebf2;
border-color: #3a4250;
}
.pbtn.on {
background: linear-gradient(180deg, #3b82f6, #2563eb);
color: #fff;
border-color: #3b82f6;
box-shadow: 0 1px 6px rgba(59, 130, 246, 0.45);
}
.pbtn.major {
width: 30px;
background: linear-gradient(180deg, #3b82f6, #2563eb);
color: #fff;
border-color: #3b82f6;
box-shadow: 0 1px 6px rgba(59, 130, 246, 0.45);
}
.pbtn.major:hover {
filter: brightness(1.12);
}
.pbtn ui-icon {
font-size: 15px;
}
.spacer {
flex: 1;
}
.p-zoom {
position: absolute;
right: 8px;
bottom: 8px;
z-index: 5;
padding: 2px 8px;
border-radius: 10px;
background: rgba(20, 22, 26, 0.78);
color: #cfe0ff;
font-size: 11px;
font-variant-numeric: tabular-nums;
pointer-events: none;
backdrop-filter: blur(3px);
}
.preview-frame-wrap {
flex: 1;
display: flex;
position: relative;
min-height: 0;
}
/* 无 iframe 游戏渲染容器(引擎脚本直接跑在面板 DOM 里) */
.game-view {
flex: 1;
width: 100%;
min-width: 0;
background: #171a1f;
overflow: hidden;
position: relative;
}
/* 预览器 UI 控件条仅作为 Ui 构造依赖保留,不显示 */
#yyka-game-host .toolbar {
display: none !important;
}
#yyka-game-host .content,
#yyka-game-host .contentWrap,
#yyka-game-host #GameDiv,
#yyka-game-host #Cocos3dGameContainer {
width: 100% !important;
height: 100% !important;
margin: 0 !important;
padding: 0 !important;
}
#yyka-game-host #GameDiv {
display: flex;
align-items: center;
justify-content: center;
background: #171a1f;
}
#yyka-game-host #GameCanvas {
width: 100%;
height: 100%;
display: block;
}
/* 加载画面 */
#yyka-game-host #splash {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: #141519;
}
#yyka-game-host #splash .progress-bar {
width: 200px;
height: 6px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
background: rgba(255, 255, 255, 0.04);
overflow: hidden;
}
#yyka-game-host #splash .progress-bar > span {
display: block;
height: 100%;
width: 0;
background: linear-gradient(90deg, #3b82f6, #60a5fa);
border-radius: 8px;
}
#yyka-game-host #bulletin {
position: absolute;
inset: 0;
display: none;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.45);
font-size: 13px;
}
#yyka-game-host #error {
position: absolute;
inset: 0;
display: none;
z-index: 10;
background: rgba(0, 0, 0, 0.85);
color: #f87171;
padding: 16px;
overflow: auto;
white-space: pre-wrap;
font-size: 12px;
}
/* 面板层渲染错误提示(点击重新加载) */
.preview-err {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(16, 16, 16, 0.88);
color: #f87171;
font-size: 12px;
line-height: 1.7;
padding: 16px;
text-align: center;
cursor: pointer;
z-index: 6;
word-break: break-all;
}
/* 预览服务未启动时的遮罩提示 */
.preview-cover {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(16, 16, 16, 0.82);
z-index: 5;
}
.cover-box {
text-align: center;
color: var(--color-normal-contrast-weak);
font-size: 13px;
line-height: 1.9;
padding: 16px;
}
.cover-box b {
display: block;
font-size: 15px;
color: var(--color-warn);
margin-bottom: 10px;
}
.cover-box button {
margin-top: 12px;
}
.ok {
color: var(--color-success);
}
.warn {
color: var(--color-warn);
}

@ -0,0 +1,533 @@
<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>

@ -0,0 +1,3 @@
<div style="height: 100%; overflow: hidden;">
<div id="app" style="height: 100%;"></div>
</div>

@ -0,0 +1,38 @@
<div class="preview-panel-root">
<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-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="Xiaomi Redmi 8">Redmi8 (360x760)</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: 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>
<span class="spacer"></span>
<button class="pbtn major" @click="onLoadPreview" title="重新加载预览"><ui-icon value="refresh"></ui-icon></button>
</div>
<div class="preview-frame-wrap">
<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 中点击「浏览器预览」启动预览服务。</div>
<button @click="onLoadPreview">重试</button>
</div>
</div>
</div>
</div>

@ -0,0 +1 @@
<div style="height: 100%; overflow: hidden;"><div id="app" style="height: 100%;"></div></div>

@ -0,0 +1,5 @@
<div class="counter">
<h2>{{counter}}</h2>
<ui-button class="blue" @click="addition">+</ui-button>
<ui-button @click="subtraction">-</ui-button>
</div>
Loading…
Cancel
Save