# UI 编写 新样板对 UI 系统进行了完全性重构,构建了一种新的 UI 系统。当然,你还可以继续使用旧的`createCanvas`流程进行 UI 编写,但是在新版 UI 的加持下,你可以更方便地管理多级 UI 以及内嵌 UI。同时,新版 UI 系统也提供了画布 API,你依然可以通过画布进行 UI 绘制及交互处理。 注意,本页所说内容全部基于[渲染进程](./system.md#进程分离),请注意进程分离。 本页面注重于 UI 编写的指引,如果你想了解新版 UI 系统,请查看[UI 系统](./ui-control.md) ## VNode 新版 UI 系统基于 Vue 的 `h` 函数,它用于生成出 `VNode`,当显示时可以直接渲染到页面上。而对于新版 UI,可以认为是一种 `VNode` 生成器,用于生成出模板化的 `VNode`,从而简化了 API,避免写出冗长的 `h` 函数链。因此,应当注意区分 `VNode` 与新版 UI 系统。 ## MComponent 对于新版 UI,应当使用系统提供的类 `MComponent` 进行编写。它的类型过于复杂,这里不再列出。下面我们将会以怪物手册为例,一步步做出一个属于你自己的怪物手册。 ## 创建组件 想要写出一个 UI,就要先创建一个组件,我们可以直接构造`MComponent`类,也可以通过函数`m`进行创建: ```js const MComponent = Mota.require('class', 'MComponent'); const m = Mota.require('fn', 'm'); const myUI = new MComponent(); // 等价于 const myUI = m(); ``` 因此,方便起见,我们更多地使用`m`函数创建组件。 ## 编写 UI 创建组件之后,我们就可以编写 UI 了。我们首先来看下面这些基础函数: ```ts // 在 MComponent 类上,也就是创建的组件上 function div(children?: any | any[], config?: any): this function span(children?: any | any[], config?: any): this function canvas(config?: any): this function text(text: string | (() => string), config?: any): this ``` 这些函数都是渲染单独内容的,分别是渲染 `div` `span` `canvas` 和文字。例如,我想在 UI 上渲染出一个画布,和一句话,可以写么写: ```js myUI .canvas() .text('这是要渲染的内容'); ``` 这便是这四个函数的最基础的用法。除此之外,文字还可以传入一个函数,每次渲染 UI 的时候都会获取其返回值作为渲染内容,也就可以实现单个组件在不同条件下渲染内容不同了。例如: ```js let cnt = 0; core.registerAnimationFrame('example', true, () => { cnt++; // 每帧让 cnt 加一 }); myUi.text(() => cnt.toString()); // 显示文字,文字是一个函数,返回cnt // 其效果便是每次显示这个组件的时候,都会显示出cnt的值,注意并不会实时更新,因为cnt不是一个响应式变量 // 如果想要实时更新,请了解vue的响应式机制 ``` 对于 `div` 和 `span`,还可以添加子内容,例如可以在 `div` 里面套一个 `div`,再套一个 `span`,再套一个 `canvas`。每个子内容都可以是一个 `MComponent` 组件,或其数组: ```js myUi .div( m() .div( m() .span( m().canvas() ) ) ); ``` 这样,我们就实现了元素的嵌套功能,布局也能够更加灵活。对于 `config` 参数,由于其非常复杂,请参考本页之后的内容。 ## 导出并渲染 UI 当我们将 UI 编写完毕后,我们并不能直接渲染在页面上,我们需要将它导出,使用 `export` 函数: ```js const show = myUI.export(); ``` 然后我们需要将其注册为一个 `GameUi`。这里都是模式化的编写方式,直接照葫芦画瓢即可,一般不需要理解很多。详见[UI 系统](./ui-control.md) ```js // 这里 mainUi 指的是有优先级的 UI,例如怪物手册及怪物详细信息 // 而 fixedUi 指的是没有优先级的 UI,例如状态栏与自定义工具栏,多个自定义工具栏等 const { mainUi, fixedUi } = Mota.requireAll('var'); const { GameUi } = Mota.requireAll('class'); // 注册使用 register 函数 // 这里的 'myUI' 指的是 UI 名称,而第二个参数指的是 UI 内容。 mainUi.register(new GameUi('myUI', myUI)); // fixedUi 注册也是同样,mainUi 与 fixedUi 间可以重名 fixedUi.register(new GameUi('myUI', myUI)); ``` 下面我们可以来打开这个 UI 了: ```js // 直接调用 open 函数即可打开 UI mainUi.open('myUI'); ``` ::: tip UI 的导出具有顺序无关性,也就是说,如果导出后继续新增内容,新增的内容不需要重新导出也可以显示。 ::: ## 列表渲染 我们可以对一个列表进行渲染,例如我们要渲染怪物手册,就需要获取每个怪物信息,这时,我们就需要用到列表渲染了: ```ts function vfor(items: any[] | (() => any[]), map: (value: any, index: number) => VNode): this ``` 函数要求传入两个参数: - `items`: 要渲染的列表,是一个数组,或者是一个返回数组的函数。 - `map`: 渲染函数,接受列表项与索引作为参数,返回 `VNode`,注意不是 `MComponent` 组件。 除此之外,我们还会用到 `MComponent` 上的静态函数 `vNodeS`: ```ts function vNodeS(mc: MotaComponent, mount?: number): VNode ``` 这个函数用于将单个渲染内容生成为单个的 `VNode`。 为了生成单个渲染内容,我们需要获取到模块 `MCGenerator`,使用它上面的函数,`MComponent` 上的函数也是基于它的。 例如,我获取了所有怪物的信息,需要渲染每个怪物的名称,就可以这么写: ```js // 首先获取渲染内容生成器,用于生成渲染内容 const { text, div } = Mota.require('module', 'MCGenerator'); // 定义渲染列表,例如这里我们就要渲染当前楼层的所有怪物 // 由于当前楼层id会随着游戏进行而变化,因此是一个动态列表,因此需要函数包裹,这样可以确保每次获取的都是正确的楼层 const enemys = () => core.getCurrentEnemys(); myUI.vfor(enemys, (value, index) => { // 要求返回一个 VNode,因此需要经过 MComponent.vNodeS 的包裹 return MComponent.vNodeS( // 输出内容,是一个由div包裹的文字显示 div(text(value.enemy.enemy.name)) ); }) ``` 这样,我们就把每个怪物的名称渲染出来了!相比于直接使用 `canvas` 绘制,是不是简便了很多。 ## 渲染组件 假如我们在很多组件里面需要共用一部分内容,每个组件里面都写一遍显然很麻烦,因此我们可以把这部分组件单独拿出来,然后做成一个组件,供其他组件使用。而这也是 `Vue` 组件一个相当强大的功能,在 `MComponent` 组件中也有相应的功能。 例如,我们先编写一个组件,内容是显示 `lhjnb`: ```js const lhjnb = m() .div( m().text('lhjnb') ) .export(); ``` 然后,我们可以通过调用 `com` 函数来渲染组件。`com` 函数用法如下: ```ts function com(component: any, config?: any): this ``` 我们就可以将这个组件添加到我们的 UI 中了: ```js // 传入 lbjnb 组件 myUI.com(lhjnb); ``` 对于传入的组件,也可以是一个 `MComponent` 未导出组件,这样的话,这个未导出组件会被视为与当前组件在同一级,对于组件分级,请查看[组件嵌套关系](#组件嵌套关系) ## 内置组件 样板还内置了部分组件,以及一些包装好的函数可以使用。你甚至还可以把样板自带的 UI 渲染到你的 UI 上,当然由于样式的改变,效果可能会不尽人意。例如,样板就内置了一个渲染图标的组件,它使用 `canvas` 逐帧绘制实现: ```ts // 在模块 MCGenerator 中 function icon( id: string, width?: number, height?: number, noBoarder?: number // 怪物图标是否设置为无边框无背景形式 ): VNode ``` 例如,这个时候我就可以给怪物手册中添加一个图标了: ```js {8-9} const { text, div, icon } = Mota.require('module', 'MCGenerator'); const enemys = () => core.getCurrentEnemys(); myUI.vfor(enemys, (value, index) => { return MComponent.vNodeS( // 输出先由一个 div 包裹,参数可以填数组,因此这里直接填入了数组作为 children div([ // 渲染图标 icon(value.enemy.id), // 渲染怪物名称 text(value.enemy.enemy.name) ]) ); }) ``` ## 组件嵌套关系 对于任何组件,其子组件都不应该影响父组件本身,而在这里也是如此。除非你使用全局变量作为桥梁,任何子组件都不会影响父组件的渲染与参数等。 在渲染子组件时,我们可以选择子组件是否经过 `export`,而经不经过导出处理,最终结果是不一样的。具体表现如下: - 如果子组件经过了 `export` 处理,那么子组件会被视为新一级的组件 - 而如果没有经过 `export` 处理,则会视为当前级组件 - 组件是否为当前级影响着画布的获取,一个组件在接口中只能获取到当前级的画布,而不能获取其子级画布 获取画布接口请参考[钩子](#钩子) 如果不使用 `MComponent` 进行渲染,直接调用 `MComponent.vNode` 等函数生成 `VNode`,依然会被视为一级新的组件。 ## 条件渲染 很多时候我们会根据一些条件去判断一个内容是否渲染,样板同样有相关的接口。接口在 `config` 参数中: ```ts interface Config { vif: () => boolean velse: boolean } ``` 其中`vif`表示渲染条件,不填时视为永远满足,当满足这个条件的时候渲染内容,`velse`表示是否是否则选项,当`velse`与`vif`同时出现时,效果等同于`else if`。于是,我们便可以依此来判断怪物的特殊属性,如果没有特殊属性,那么就渲染为`无属性`,否则渲染怪物的属性列表。 ```js {16-25} const { text, div, icon, vfor, span } = Mota.require('module', 'MCGenerator'); const enemys = () => core.getCurrentEnemys(); // 用于获取怪物应该显示在手册上的信息的函数 const { getDetailedEnemy } = Mota.require('module', 'UITools').fixed; myUI.vfor(enemys, (value, index) => { // 首先要获取怪物的详细信息 const detail = getDetailedEnemy(value.enemy); return MComponent.vNodeS( // 输出先由一个 div 包裹,参数可以填数组,因此这里直接填入了数组作为 children div([ // 渲染图标 icon(value.enemy.id), // 渲染怪物名称 text(value.enemy.enemy.name), // 当怪物拥有特殊属性时 div([ // 渲染怪物的特殊属性,还需要一层 for 嵌套 vfor(detail.special, ([name, desc, color], index) => { // 这里的special是一个数组,第一项表示名称,第二项表示说明,第三项表示名称颜色 // 这里我们只渲染名称 return span(text(name)) }) ], { vif: () => detail.special.length > 0 }), // 当怪物拥有特殊属性时 div(span(text('无属性')), { velse: true }) // 否则渲染无属性 ]) ); }) ``` ## 传递参数 我们会发现,`icon` 函数可以将怪物 id 传入组件中并显示出来,这种行为叫做传递`props`,在这里我们也称为传递参数。拥有了传递参数的功能,我们可以接收来自父组件的参数,同时根据父组件的意愿渲染出对应的内容。 例如,就拿样板内置的 `BoxAnimate` 组件为例,也就是 `icon` 函数渲染的组件,我们可以通过 `config` 传递参数。对于每个 `props`,都要求是一个函数,其返回值作为真正传递的参数。如果想要传递函数,那么就需要是一个返回函数的函数。如果只想传递一个常量,又不想写一个函数,那么可以使用 `MCGenerator` 提供的 `f` 函数。 ```js const { BoxAnimate } = Mota.require('module', 'UIComponents'); const { f } = Mota.require('module', 'MCGenerator'); myUI.com(BoxAnimate, { props: { id: f('redSlime') } }); // 这个就等效于使用 icon 函数: myUI.h(icon('redSlime')); ``` ## 定义参数 如果我们想给自己的组件定义一些参数,可以使用 `defineProps` 函数: ```ts function defineProps(props: Record): this ``` 这个函数传入一个对象,其键表示参数名称,值表示参数类型,例如: ```js const com = m().defineProps({ id: String, // 对于字符串、数字、布尔值,类型表示为String, Number, Boolean num: Number, ui: GameUi // 对于对象,可以表示为 Object 或者类名 }) ``` 这样,我们就可以向这个组件传递参数了,如果参数类型不正确,或者传递的参数并没有定义,依然可以正确运行,不过会在控制台输出警告。例如: ```js const { f } = Mota.require('module', 'MCGenerator'); // 传入lhjnb作为参数,但是由于还有两个参数没有传递,会在控制台有报错,不过依然可以正确运行 myUI.com(com, { props: { id: f('lhjnb') } }); ``` 对于如何获取传入的参数,请参考[钩子](#钩子) ## 传递 Attribute 我们还可以向浏览器的 DOM 元素中添加信息,只要我们将值传递给 `props` 即可,例如我们可以给一个 `span` 元素添加 id,设置样式等。就拿上面的怪物特殊属性举例,我们如果想要让不同的属性显示出对应的颜色,那么可以这么写: ```js {15-18} const { text, div, icon, vfor, span } = Mota.require('module', 'MCGenerator'); const enemys = () => core.getCurrentEnemys(); const { getDetailedEnemy } = Mota.require('module', 'UITools').fixed; myUI.vfor(enemys, (value, index) => { const detail = getDetailedEnemy(value.enemy); return MComponent.vNodeS( div([ // 渲染图标 icon(value.enemy.id), // 渲染怪物名称 text(value.enemy.enemy.name), // 当怪物拥有特殊属性时 div([ vfor(detail.special, ([name, desc, color], index) => { // 将 style 作为 props 传入即可,这样颜色就能正确显示出来了 return span(text(name), { props: { style: f(`color: ${color}`) } }); }) ], { vif: () => detail.special.length > 0 }), // 当怪物拥有特殊属性时 div(span(text('无属性')), { velse: true }) // 否则渲染无属性 ]) ); }) ``` ## 监听事件与 emits 对于 `props`,有一类特殊的属性,由 `on` 开头,然后紧跟着大写字母,这种形式的 `props` 会被视为监听函数。例如,我想监听一个 `div` 的点击事件,当玩家点击这个 `div` 时,在控制台输出 `clicked!`,那么可以这么写: ```js myUI.div( [], // 没有子元素,填空数组 { props: { onClick: f((e) => { // 当点击时,输出 clicked! 与点击位置相对于浏览器左上角的坐标 console.log('clicked!', e.clientX, e.clientY); }) } } ) ``` 对于自定义组件,依然可以使用同样的方法监听其 `emits`,`emits` 可以认为是组件的事件,可以在组件内部触发。例如,对于样板自带的 `Column` 组件,它有一个 `close` 事件,我们便可以用这种方式进行监听: ```js const { Column } = Mota.require('module', 'UIComponents'); myUI.com( Column, { props: { // 监听close事件,所以名为 onClose onClose: f(() => { // 当 close 事件触发时,会在控制台输出 close emitted! console.log('close emitted!'); }) } } ) ``` 你也可以为自己的组件定义自己的 `emits`,通过 `defineEmits` 函数: ```js myUI.defineEmits(['myEmit']); // 传入一个字符串数组,表示组件的 emits 名称列表 ``` 这样,你就可以在其他地方监听这个组件的 `emits` 了 ## 渲染函数 `MComponent` 上也有一个名为 `h` 的渲染函数,但是它与 `Vue` 的渲染函数不同。在这里,`h` 函数的用法如下: ```ts function h(type: any, children?: any | any[], config?: any): this ``` 上面用到的所有函数,包括 `div` `span` `canvas` `text` `com` 在内的函数都基于这个函数。借助于这个函数,我们可以更加灵活地编写我们的 UI。 对于第一个参数,它可以填写这些内容: - `text`: 表示渲染文字,等价于 `text` 函数,需要填写 `config` 参数的 `innerText` 属性 - `component`: 表示渲染组件,需要在 `config` 参数中填写 `component` 属性或 `dComponent` 属性,详见[动态组件](#动态组件) - 任何 DOM 元素的名称,例如 `span` `li` `input` 等,表示渲染 DOM 元素 - 任何 `MComponent` 组件,表示渲染组件,等价于填写 `component`,然后填写对应的 `config` 属性,也等价于 `com` 函数 - 任何导出后的组件,或者是样板自带组件,与填写 `MComponent` 时类似 例如: ```js myUI .h('div') // 注意,如果你确保递归调用组件没问题,那么你可以按下面这样递归调用自身作为组件,否则出问题概不负责! .h('component', [], { component: myUI }) .h('text', [], { innerText: 'lhjnb' }); ``` ## 动态组件 当 `h` 函数传入 `component` 作为类型时,可以作为动态组件使用,要求填写 `config` 参数的 `component` 属性或者 `dComponent` 属性。填写 `component` 属性时,等价于 `com` 函数,这里不再赘述,下面我们来看填写 `dComponent` 属性时的情况。 这个属性要求传入一个函数,函数返回一个导出组件,注意不能是 `MComponent` 组件,也不能是 `VNode`。例如: ```js const randomComponent = [BoxAnimate, Box, Column]; // 随机渲染一个组件上去! myUI.h( 'component', [], { dComponent: () => randomComponent[Math.floor(Math.random() * 3)] } ); ``` ## 传递插槽 插槽的功能是向子组件中渲染内容,在目前样板中,没有提供定义插槽的方法,只能向子组件中传递插槽。这也意味着,你只能向样板内置组件或者 `Vue` 内置组件中传递插槽。不过,在[自定义 setup](#自定义-setup)中,提供了另一种方式可以渲染插槽(定义插槽) 你可以通过 `config` 参数的 `slots` 属性传递插槽,每个插槽都是一个函数,要求返回一个 `VNode` 或其数组用于渲染。例如,对于滚动条组件 `Scroll`,它有一个默认插槽 `default`,你可以这样传递插槽: ```js const { icon, div } = Mota.require('module', 'MCGenerator'); myUI.com(Scroll, [], { slots: { // 传递默认插槽 default: () => { // 返回 VNode,使用 MComponent 上的静态函数 vNode,它可以将 MCGenerator 生成的渲染内容生成为 vNode return MComponent.vNode([ div(icon('greenSlime')), div(icon('redSlime')) ]); } } }) ``` ## 钩子 下面是样板提供的几个钩子接口。什么是钩子呢?就是在组件渲染的不同时刻执行传入的函数。目前样板中,提供了两个接口,分别是: - `onSetup`: 当组件开始渲染时,这时组件内容还没有挂载到页面上 - `onMounted`: 当组件渲染完毕时,这时组件已经完全渲染到了页面上 这两个函数都会接收两个参数,分别是 `props` 和 `ctx`。 - `props`: 表示父组件传递过来的参数(`props`) - `ctx`: `setup` 上下文,用于触发 `emits`,渲染插槽(定义插槽)等,不过在这里渲染插槽没有用处 例如: ```js myUI.onSetup((props, ctx) => { // 输出父组件传递的参数 console.log(props); // 触发名为 close 的 emits,同时将 123 作为数据输出,数据可以由父组件的监听函数的参数中获取 ctx.emits('close', 123); }); ``` 对于 `onMounted` 函数,还会另外多一个参数,描述的是当前级组件(不包括子组件,但包括 `MComponent` 未导出组件,详见[组件嵌套关系](#组件嵌套关系))中所有的画布。例如: ```js myUI // 创建一个 id 为 my-canvas 的画布 .canvas({ props: { id: f('my-canvas') } }) // 当组件渲染完毕时 .onMounted((props, ctx, canvas) => { const myCanvas = canvas[0]; if (myCanvas?.id === 'my-canvas') { // 获取画布的绘制上下文 const ctx = myCanvas.getContext('2d'); // 绘制一条线,也可以使用样板的 core.drawLine 等 api 绘制 ctx.moveTo(0, 0); ctx.lineTo(200, 200); ctx.stroke(); } }); ``` ## 自定义 setup 自定义 `setup` 允许你完全控制组件的渲染内容,并在组件渲染时执行脚本。它包含两个接口: - `ret`: 设置 `setup` 的返回值,即组件的最终渲染函数 - `setup`: 完全重写设置组件的 `setup` 函数,让组件完全自定义 `setup` 的含义是,这个组件被渲染的时候,执行的函数 例如,你可以通过 `ret` 接口来修改你的组件的渲染内容,而由于可以直接操作渲染内容,你便可以做到渲染插槽(定义插槽) ```js const { icon, div, span, text } = Mota.require('module', 'MCGenerator'); myUI.ret((props, ctx) => { // 函数要求返回 VNode,因此要有 vNode 函数包裹 // 注意在这里请尽量避免使用导出组件直接作为返回值,这会对性能造成一定的影响 return MComponent.vNode([ span(text('lhjnb')), div(icon('greenSlime')), ctx.slots.defaults(), // 渲染插槽(定义插槽) ]); }) ``` 还可以通过 `setup` 函数完全重写组件。对于稍微复杂点的组件,我们都需要使用这个功能。 ::: tip 重写 setup 函数后,应当重新 export 组件,否则内容不会变化 ::: ```js // 这里就需要获取 vue 的函数了,还可以使用 ref 实现响应式布局 const { onMounted, ref } = Mota.Package.require('vue'); myUI.setup((props, ctx) => { // 当组件渲染完毕时 onMounted(() => { console.log('mounted!'); }); const cnt = ref(0); // setup 函数要返回一个函数,函数返回值为 VNode return () => { return MComponent.vNode([ span(text('lhjnb')), div(icon('greenSlime')), span(text(cnt.value)) // 使用响应式变量cnt,当其更改时,渲染内容也会随之更改 ]); } }) ``` 实际上,样板自带的 `setup` 函数也是使用它来实现的: ```ts const setup = (props, ctx) => { const mountNum = MComponent.mountNum++; this.onSetupFn?.(props, ctx); onMounted(() => { this.onMountedFn?.( props, ctx, Array.from( document.getElementsByClassName( `--mota-component-canvas-${mountNum}` ) as HTMLCollectionOf ) ); }); if (this.retFn) return () => this.retFn!(props, ctx); else { return () => { const vNodes = MComponent.vNode( this.content, mountNum ); return vNodes; }; } } ``` ## 怪物手册示例 下面我们将做一个简易的怪物手册作为 UI 示例。在这里,我们使用样板组件 `Column` 进行编写,它可以让一个 UI 分为两栏,同时还可以设置大小,及左右栏占比,对于制作说明式的 UI 非常有用。示例中包含了按键操作的说明,详见[按键系统](./hotkey.md) 内容较长,可以复制到编辑器内查看。 ```js // 首先引入需要的接口,注意以下所有内容均基于渲染进程 const { MComponent, GameUi } = Mota.requireAll('class'); const { getDetailedEnemy } = Mota.require('module', 'UITools').fixed; const { Column } = Mota.require('module' ,'UIComponents'); const { com, div, span, text, icon, f, vfor } = Mota.require('module', 'MCGenerator'); const { mainUi, gameKey } = Mota.requireAll('var'); const { m } = Mota.requireAll('fn'); const { ref, computed, onUnmounted } = Mota.Package.require('vue'); // 怪物手册属于较为复杂的内容,必须要完全重写 setup const myBook = m().setup((props, ctx) => { const close = () => { // 对于每个直接由 UI 系统打开的 UI,都会有一个 num 参数,表示这个 UI 的标识符 // 这个标识符可以用于关闭 UI,例如下面就是关闭这个 UI // 更多内容请查阅 UI 系统章节 mainUi.close(props.num); } onUnmounted(() => { // 当关闭 UI 时,跳出当前层按键作用域 // 注意由于 UI 可能是被其他原因被关闭的,不会执行 close 函数,因此要使用 onUnmounted 钩子 gameKey.dispose(props.ui.symbol); }); const enemys = core.getCurrentEnemys(); // 对于 number string boolean,要做响应式需要 ref,对于对象,需要 reactive,更多信息请了解 vue 的响应式 const selected = ref(0); // computed 会自动追踪用到的响应式变量,同时在响应式变量更改时更改 enemy 的值 const enemy = computed(() => enemys[selected.value]); // 按键系统,首先创建新作用域,然后实现按键操作 gameKey .use(props.ui.symbol) // 上移一位 .realize('@book_up', () => { if (selected.value > 0) selected.value--; }) // 下移一位 .realize('@book_down', () => { if (selected.value < enemys.length - 1) selected.value++; }) // 下移5位 .realize('@book_pageDown', () => { if (selected.value >= enemys.length - 5) { selected.value = enemys.length - 1; } else { selected.value += 5; } }) // 上移5位 .realize('@book_pageUp', () => { if (selected.value <= 4) { selected.value = 0; } else { selected.value -= 5; } }) // 退出 .realize('exit', () => { close(); }); // 要渲染的怪物属性 const status = [ ['hp', '生命', 'lightgreen'] ['atk', '攻击', 'lightcoral'], ['def', '防御', 'lightblue'], ['exp', '经验', 'lightgreen'], ['gold', '金币', 'lightyellow'] ]; // 简写数据格式化 const format = core.formatBigNumber; // 属性样式,由于多次用到在这里声明为一个常量 const statusStyle = { props: { style: f(` display: flex; flex-direction: row; color: ${color}; `) } }; const statusSpanStyle = { props: { style: f(` flex-basis: 30%; text-align: right; margin-right: 5%; `) } } return () => { // 要求返回 VNode。对于大部分情况,如果你不确定是否要包裹 vNode 的话,包裹一层不会有任何副作用 return MComponent.vNode([ com(Column, { // Column 组件共有四个参数,分别是宽高,与左右占比,单位都是百分比 // 宽高作为百分比时描述的一般是相对于父组件宽高的占比,对于根组件自然就是屏幕的占比了 props: { width: f(70), height: f(70), left: f(30), right: f(70), onClose: f(close) // Column 组件自带关闭的功能,监听 close 事件即可,然后将 close 函数传入 }, // Column 组件包含名为 left 和 right 的插槽,向其中传入要渲染的内容即可 // 在这里,左侧渲染怪物列表,右侧渲染怪物信息 slots: { // 左侧内容,为当前楼层的怪物列表 left: () => { return MComponent.vNode([ // 列表渲染,遍历函数要返回单个 VNode vfor(enemys, (value, index) => { return MComponent.vNodeS( div( [ // 渲染怪物的图标,设置为无边框与背景 icon(value.enemy.id, void 0, void 0, true), // 然后是怪物名称 span(text(value.enemy.enemy.name)) ], { props: { // selectable 是样板内置的样式类,选中时会有选中动画 class: f('selectable'), // selected 属性表示是否选中 selected: () => selected.value === index, style: f(` margin: 0 0 4px 0; display: flex; flex-direction: row; align-items: center; `) }, // 当点击这个怪物时 onClick: f(() => { selected.value = index; }) } ) ); }); ]); }, // 右侧内容,显示怪物的详细信息 right: () => { return MComponent.vNode([ // 顶部,渲染怪物图标与名称 div([ // 没有边框 icon(enemy.value.enemy.id, void 0, void 0, true); span(text(enemy.value.enemy.enemy.name), { props: { // 注意所有的文字尽量使用百分比进行大小限定,这样可以被设置所更改 style: f(`font-size: 200%`) } }) ], { props: { // 顶部样式 style: f(` display: flex; flex-direction: row; align-items: center; justify-content: space-around; padding-bottom: 5%; border-bottom: dashed 1px #ddd8; `) } }), // 然后是下面的部分,包括怪物的详细信息 // 首先是怪物的基础属性 div( [ // 使用列表渲染,对上面定义的怪物属性进行渲染 vfor(status, ([key, name, color]) => { return MComponent.vNodeS( div([ // 怪物属性名称,右对齐 span(text(name), statusSpanStyle), span(text(format(enemy.value.enemy.info[key]))) ], statusStyle) ); }), // 然后是伤害、临界 div([ span(text('伤害'), statusSpanStyle), span(text(format(enemy.value.damage))) ], statusStyle), div([ span(text('临界'), statusSpanStyle), span(text(format(enemy.value.critical))) ], statusStyle), div([ span(text('临界减伤'), statusSpanStyle), span(text(format(enemy.value.criticalDam))) ], statusStyle), div([ span(text(core.status.thisMap.ratio + '防'), statusSpanStyle), span(text(format(enemy.value.defDam))) ], statusStyle), // 下面是特殊属性,依然要用列表渲染 vfor(enemy.value.showSpecial, ([name, desc, color]) => { return MComponent.vNodeS(div( span(text(name), { props: { styles: f(`color: ${color}`) } }), span(text(': ')), span(text(desc)) )); }) ], { props: { style: f(` display: flex; flex-direction: column; width: 100%; `) } } ) ]); } } }); ]); } }) // 由 UI 系统直接打开的 UI 都需要包含这两个参数 .defineProps({ num: Number, ui: GameUi }) .export(); // 注册 UI mainUi.register(new GameUi('myBook', myBook)); ```