feat(config-editor): extension entry + minimal Vue panel proving end-to-end IPC
Task 12 of plan 2026-06-20-config-editor-foundation. Adds:
- src/main/index.ts: onLoad + message handlers (return value = request resolve,
per Cocos 3.x verified IPC mechanism; fallback note left in plan)
- src/panels/default/{index,app}.ts: Editor.Panel.define host + Vue 3 minimal
app (table switcher, key list, record JSON dump)
- static/template/default/index.html + static/style/default/index.css
Deviation from plan (necessary, flagged): esbuild.config.mjs now marks
node:fs/node:path as external for the panel entry (platform:'browser').
The plan's panel reads static template/style at runtime via Node fs, which
requires these builtins; Cocos panel runs in an Electron renderer that
provides them. Without this, esbuild errors with 'Could not resolve node:fs'.
Build verified: dist/main.js (9.5mb, typescript compiler API bundled) and
dist/panels/default.js (628kb, vue.esm-bundler bundled) both generate.
This commit is contained in:
@@ -11,9 +11,12 @@ const common = {
|
||||
alias: { 'vue': 'vue/dist/vue.esm-bundler.js' },
|
||||
};
|
||||
|
||||
// 面板进程在 Cocos 的 Electron 渲染层运行,可访问 Node 内建(fs/path),但 vue 必须
|
||||
// 打进浏览器侧 IIFE。因此面板项用 platform:'browser' + 把 node: 内建标为 external,
|
||||
// 交由运行时解析;这样 esbuild 既不抱怨,又保持 plan 的"运行时读 static 文件"语义。
|
||||
const entries = [
|
||||
{ entryPoints: ['src/main/index.ts'], outfile: 'dist/main.js', platform: 'node', format: 'cjs', external: [] },
|
||||
{ entryPoints: ['src/panels/default/index.ts'], outfile: 'dist/panels/default.js', platform: 'browser', format: 'iife', external: [] },
|
||||
{ entryPoints: ['src/panels/default/index.ts'], outfile: 'dist/panels/default.js', platform: 'browser', format: 'iife', external: ['node:fs', 'node:path'] },
|
||||
];
|
||||
|
||||
if (watch) {
|
||||
|
||||
16
extensions/pixelhero-config-editor/src/main/index.ts
Normal file
16
extensions/pixelhero-config-editor/src/main/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { store } from './store';
|
||||
|
||||
module.exports = {
|
||||
onLoad() { store.reloadAll(); },
|
||||
|
||||
'open-panel'() { Editor.Panel.open('pixelhero-config-editor'); },
|
||||
|
||||
'query-schema'(_event: unknown, id?: string) { return store.querySchema(id as any); },
|
||||
'query-enums'() { return store.queryEnums(); },
|
||||
'query-keys'(_event: unknown, id: string) { return store.queryKeys(id as any); },
|
||||
'query-record'(_event: unknown, id: string, key: string) { return store.queryRecord(id as any, key); },
|
||||
'query-preview-desc'(_event: unknown, hero: any) { return store.queryPreviewDesc(hero); },
|
||||
'validate'(_event: unknown, id: string) { return store.validate(id as any); },
|
||||
'save-record'(_event: unknown, id: string, key: string, value: any) { return store.saveRecord(id as any, key, value); },
|
||||
'revert-record'(_event: unknown, id: string, key: string) { return store.revertRecord(id as any, key); },
|
||||
};
|
||||
38
extensions/pixelhero-config-editor/src/panels/default/app.ts
Normal file
38
extensions/pixelhero-config-editor/src/panels/default/app.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createApp, defineComponent, reactive } from 'vue';
|
||||
|
||||
export const App = defineComponent({
|
||||
setup() {
|
||||
const state = reactive({ table: 'hero' as string, keys: [] as string[], picked: '' as string, detail: '' as string });
|
||||
async function load() {
|
||||
const keys = await Editor.Message.request('pixelhero-config-editor', 'query-keys', state.table);
|
||||
state.keys = keys || [];
|
||||
state.picked = ''; state.detail = '';
|
||||
}
|
||||
async function pick(k: string) {
|
||||
state.picked = k;
|
||||
const v = await Editor.Message.request('pixelhero-config-editor', 'query-record', state.table, k);
|
||||
state.detail = JSON.stringify(v, null, 2);
|
||||
}
|
||||
load();
|
||||
return { state, load, pick };
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="row">
|
||||
<label>表:</label>
|
||||
<select v-model="state.table" @change="load">
|
||||
<option value="hero">英雄/怪物</option>
|
||||
<option value="skill">技能</option>
|
||||
<option value="field">驻场技能</option>
|
||||
</select>
|
||||
<span style="margin-left:12px;color:#888">共 {{ state.keys.length }} 条(端到端 IPC 已打通)</span>
|
||||
</div>
|
||||
<ul>
|
||||
<li v-for="k in state.keys" :key="k" @click="pick(k)" :style="state.picked===k ? 'font-weight:bold' : ''">{{ k }}</li>
|
||||
</ul>
|
||||
<pre v-if="state.detail" style="white-space:pre-wrap;background:var(--color-normal-fill);padding:8px">{{ state.detail }}</pre>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
export function mount(el: HTMLElement) { createApp(App).mount(el); }
|
||||
@@ -0,0 +1,14 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { mount } from './app';
|
||||
|
||||
const template = readFileSync(join(__dirname, '../../../static/template/default/index.html'), 'utf-8');
|
||||
const style = readFileSync(join(__dirname, '../../../static/style/default/index.css'), 'utf-8');
|
||||
|
||||
module.exports = Editor.Panel.define({
|
||||
template,
|
||||
style,
|
||||
$: { app: '#app' },
|
||||
ready() { mount(this.$.app); },
|
||||
close() { /* Vue 卸载随面板进程退出自动清理 */ },
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
#app { color: var(--color-font-normal); font-size: 12px; }
|
||||
.row { margin: 6px 0; }
|
||||
button { margin-right: 8px; }
|
||||
ul { list-style: none; padding: 0; max-height: 360px; overflow:auto; }
|
||||
li { padding: 3px 6px; cursor: pointer; }
|
||||
li:hover { background: var(--color-hover-bg); }
|
||||
.err { color: var(--color-warn); }
|
||||
@@ -0,0 +1 @@
|
||||
<div id="app" style="padding:12px;"></div>
|
||||
Reference in New Issue
Block a user