HumanBreak/docs/guide/ui.md
2024-02-08 16:50:58 +08:00

838 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<string, any>): 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<HTMLCanvasElement>
)
);
});
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));
```