feat: UI 控制器

This commit is contained in:
unanmed 2025-02-19 22:58:08 +08:00
parent fa7d2b2c16
commit 230e08e8a6
10 changed files with 523 additions and 49 deletions

View File

@ -64,7 +64,8 @@ export default tseslint.config(
],
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-this-alias': 'off',
'no-console': 'warn'
'no-console': 'warn',
'vue/multi-word-component-names': 'off'
}
},
eslintPluginPrettierRecommended

View File

@ -14,9 +14,9 @@ export type Props<
> = T extends keyof JSX.IntrinsicElements
? JSX.IntrinsicElements[T]
: T extends DefineSetupFnComponent<any>
? InstanceType<T>['$props']
? InstanceType<T>['$props'] & InstanceType<T>['$emits']
: T extends DefineComponent
? InstanceType<T>['$props']
? InstanceType<T>['$props'] & InstanceType<T>['$emits']
: unknown;
export function disableViewport() {

View File

@ -0,0 +1,56 @@
import { computed, defineComponent } from 'vue';
import { IUIMountable, UIBaseElementSlots, UIComponent } from './shared';
import { SetupComponentOptions } from '@/module';
import { ContainerProps } from '@/core/render';
export interface UIContainerProps {
controller: IUIMountable<UIComponent>;
}
const containerConfig = {
props: ['controller']
} satisfies SetupComponentOptions<UIContainerProps>;
export const UIContainer = defineComponent<UIContainerProps>(props => {
const data = props.controller;
const back = data.backIns;
const show = computed(() => data.stack.filter(v => !v.hidden));
return () => {
return (
<data.baseElement>
{(() => {
const b = back.value;
if (!b || !data.showBack.value || b.hidden) return;
return (
<b.ui.component
{...b.vBind}
key={b.key}
></b.ui.component>
);
})()}
{show.value.map(v => (
<v.ui.component {...v.vBind} key={v.key}></v.ui.component>
))}
</data.baseElement>
);
};
}, containerConfig);
export const UIRenderBase = defineComponent<
ContainerProps,
{},
string,
UIBaseElementSlots
>((_props, { slots }) => {
return () => {
return <container>{slots.defaults()}</container>;
};
});
export const UIDomBase = defineComponent<{}, {}, string, UIBaseElementSlots>(
(_props, { slots }) => {
return () => {
return <div>{slots.defaults()}</div>;
};
}
);

View File

@ -1,55 +1,307 @@
import { Component, VNodeProps } from 'vue';
import { logger } from '@/core/common/logger';
import EventEmitter from 'eventemitter3';
import {
IGameUI,
IKeepController,
IUIInstance,
IUIMountable,
UIBaseElement,
UIComponent
} from './shared';
import { Props } from '@/core/render';
import { UIInstance } from './instance';
import {
computed,
ComputedRef,
h,
reactive,
ref,
Ref,
shallowRef,
ShallowRef
} from 'vue';
import { UIContainer } from './container';
export interface IUIControllerConfig<Element, UI> {
/**
* ui挂载至目标元素时的操作
* @param element
* @param ui ui对象
*/
insert(element: Element, ui: UI): void;
/**
* ui从目标元素上移除时的操作
* @param element ui的父元素
* @param ui ui元素
*/
remove(element: Element, ui: UI): void;
/**
* UI
* @param component UI组件
* @param props UI传递的props
*/
createUI(
component: Component,
props?: (VNodeProps & { [key: string]: any }) | null
): UI;
export const enum UIMode {
/** 仅显示最后一个 UI在关闭时只会关闭指定的 UI */
LastOnly,
/** 显示所有非手动隐藏的 UI在关闭时只会关闭指定 UI */
All,
/** 仅显示最后一个 UI在关闭时在此之后的所有 UI 会全部关闭 */
LastOnlyStack,
/** 显示所有非手动隐藏的 UI在关闭时在此之后的所有 UI 会全部关闭 */
AllStack,
/** 自定义 UI 显示模式 */
Custom
}
export const enum OpenOption {
Push,
Unshift
}
export const enum CloseOption {
Splice,
Pop,
Shift
}
export class UIController<Element, UI> {
constructor(config: IUIControllerConfig<Element, UI>) {}
export interface IUICustomConfig<C extends UIComponent> {
/**
* UI
* @param ins UI
* @param stack UI UI
*/
open(ins: IUIInstance<C>, stack: IUIInstance<C>[]): void;
/**
* ui改变时控制器的行为
* @param open
* @param close
* UI
* @param ins UI
* @param stack UI UI
* @param index UI UI
*/
setChangeMode(open: OpenOption, close: CloseOption) {}
close(ins: IUIInstance<C>, stack: IUIInstance<C>[], index: number): void;
/**
* UI控制器挂载至容器上
* @param container
* UI
* @param ins UI
* @param stack UI
* @param index UI UI
*/
mount(container: Element) {}
hide(ins: IUIInstance<C>, stack: IUIInstance<C>[], index: number): void;
/**
* UI
* @param ins UI
* @param stack UI
* @param index UI UI
*/
show(ins: IUIInstance<C>, stack: IUIInstance<C>[], index: number): void;
/**
* UI
* @param stack UI
*/
update(stack: IUIInstance<C>[]): void;
}
interface UIControllerEvent {}
export class UIController<C extends UIComponent = UIComponent>
extends EventEmitter<UIControllerEvent>
implements IUIMountable<C>
{
static controllers: Map<string, UIController> = new Map();
/** 当前的 ui 栈 */
readonly stack: IUIInstance<C>[] = reactive([]);
/** UI 显示方式 */
mode: UIMode = UIMode.LastOnlyStack;
/** 这个 UI 实例的背景,当这个 UI 处于显示模式时,会显示背景 */
background?: IGameUI<C>;
/** 背景 UI 实例 */
readonly backIns: ShallowRef<IUIInstance<C> | null> = shallowRef(null);
/** 当前是否显示背景 UI */
readonly showBack: ComputedRef<boolean> = computed(
() => this.userShowBack.value && this.sysShowBack.value
);
/** 自定义显示模式下的配置信息 */
private config?: IUICustomConfig<C>;
/** 是否维持背景 UI */
private keepBack: boolean = false;
/** 用户是否显示背景 UI */
private readonly userShowBack: Ref<boolean> = ref(true);
/** 系统是否显示背景 UI */
private readonly sysShowBack: Ref<boolean> = ref(false);
/**
* ui
* @param id
*/
constructor(
public readonly id: string,
public readonly baseElement: UIBaseElement
) {
super();
if (UIController.controllers.has(id)) {
logger.warn(57, id);
} else {
UIController.controllers.set(id, this);
}
}
/**
* UI
*/
render() {
return h(UIContainer, { controller: this });
}
/**
* UI
* @param back UI UI
*/
setBackground(back: IGameUI<C>) {
this.background = back;
}
/**
* UI
*/
hideBackground() {
this.userShowBack.value = false;
}
/**
* UI
*/
showBackground() {
this.userShowBack.value = true;
}
/**
* UI使 UI
*/
keep(): IKeepController {
this.keepBack = true;
return {
safelyUnload: () => {
if (this.stack.length > 0) return;
this.sysShowBack.value = false;
this.keepBack = false;
},
unload: () => {
this.sysShowBack.value = false;
this.keepBack = false;
}
};
}
/**
* ui
* @param ui ui
* @param vBind ui
*/
open(ui: IGameUI<C>, vBind: Props<C>) {
const ins = new UIInstance(ui, vBind);
switch (this.mode) {
case UIMode.LastOnly:
case UIMode.LastOnlyStack:
this.stack.forEach(v => v.hide());
this.stack.push(ins);
break;
case UIMode.All:
case UIMode.AllStack:
this.stack.push(ins);
break;
case UIMode.Custom:
this.config?.open(ins, this.stack);
break;
}
this.sysShowBack.value = true;
return ins;
}
/**
* ui
* @param ui ui
*/
close(ui: UIInstance<C>) {
const index = this.stack.indexOf(ui);
if (index === -1) return;
switch (this.mode) {
case UIMode.LastOnly: {
this.stack.splice(index, 1);
this.stack.forEach(v => v.hide());
const last = this.stack.at(-1);
if (!last) break;
last.show();
break;
}
case UIMode.LastOnlyStack: {
this.stack.splice(index);
this.stack.forEach(v => v.hide());
const last = this.stack[index - 1];
if (!last) break;
last.show();
break;
}
case UIMode.All: {
this.stack.splice(index, 1);
break;
}
case UIMode.AllStack: {
this.stack.splice(index);
break;
}
case UIMode.Custom: {
this.config?.close(ui, this.stack, index);
break;
}
}
if (!this.keepBack && this.stack.length === 0) {
this.sysShowBack.value = false;
}
this.keepBack = false;
}
hide(ins: IUIInstance<C>): void {
const index = this.stack.indexOf(ins);
if (index === -1) return;
if (this.mode === UIMode.Custom) {
this.config?.hide(ins, this.stack, index);
} else {
ins.hide();
}
}
show(ins: IUIInstance<C>): void {
const index = this.stack.indexOf(ins);
if (index === -1) return;
if (this.mode === UIMode.Custom) {
this.config?.show(ins, this.stack, index);
} else {
ins.show();
}
}
/**
* UI
* @param stack UI UI
*/
lastOnly(stack: boolean = true) {
if (stack) {
this.mode = UIMode.LastOnlyStack;
} else {
this.mode = UIMode.LastOnly;
}
this.stack.forEach(v => v.hide());
this.stack.at(-1)?.show();
}
/**
* UI
* @param stack UI UI
*/
showAll(stack: boolean = false) {
if (stack) {
this.mode = UIMode.AllStack;
} else {
this.mode = UIMode.All;
}
this.stack.forEach(v => v.show());
}
/**
* 使
* @param config
*/
showCustom(config: IUICustomConfig<C>) {
this.mode = UIMode.Custom;
this.config = config;
config.update(this.stack);
}
/**
* ui
* @param id ui
*/
static getController<T extends UIComponent>(
id: string
): UIController<T> | null {
const res = this.controllers.get(id) as UIController<T>;
return res ?? null;
}
}

View File

@ -1 +1,5 @@
export * from './controller';
export * from './ui';
export * from './shared';
export * from './instance';
export * from './container';

View File

@ -0,0 +1,50 @@
import { Props } from '@/core/render';
import { IGameUI, IUIInstance, UIComponent } from './shared';
import EventEmitter from 'eventemitter3';
import { markRaw, mergeProps } from 'vue';
interface UIInstanceEvent {
hide: [];
show: [];
close: [];
}
export class UIInstance<C extends UIComponent>
extends EventEmitter<UIInstanceEvent>
implements IUIInstance<C>
{
private static counter: number = 0;
readonly key: number = UIInstance.counter++;
readonly ui: IGameUI<C>;
hidden: boolean = false;
constructor(
ui: IGameUI<C>,
public vBind: Props<C>
) {
super();
this.ui = markRaw(ui);
}
/**
* UI
* @param data
* @param merge truefalse
*/
setVBind(data: Props<C>, merge: boolean = true) {
if (merge) {
this.vBind = mergeProps(this.vBind, data) as Props<C>;
} else {
this.vBind = data;
}
}
hide(): void {
this.hidden = true;
}
show(): void {
this.hidden = false;
}
}

View File

@ -0,0 +1,87 @@
import { Props } from '@/core/render';
import {
DefineComponent,
DefineSetupFnComponent,
Ref,
ShallowRef,
SlotsType,
VNode
} from 'vue';
export type UIComponent = DefineSetupFnComponent<any> | DefineComponent;
export interface IGameUI<C extends UIComponent> {
/** 这个 UI 的名称 */
readonly name: string;
/** 这个 UI 的组件 */
readonly component: C;
}
export interface IKeepController {
/**
* UI UI
*/
safelyUnload(): void;
/**
* UI
*/
unload(): void;
}
export type UIBaseElementSlots = SlotsType<{
defaults: () => VNode[];
}>;
export type UIBaseElement =
| DefineComponent<{}, {}, string, UIBaseElementSlots>
| DefineSetupFnComponent<{}, {}, UIBaseElementSlots>;
export interface IUIMountable<C extends UIComponent> {
/** 当前的 UI 栈 */
readonly stack: IUIInstance<C>[];
/** 当前的背景 UI */
readonly backIns: ShallowRef<IUIInstance<C> | null>;
/** 当前是否显示背景 UI */
readonly showBack: Ref<boolean>;
/** UI 控制器的根元素 */
readonly baseElement: UIBaseElement;
/**
* UI
* @param ins UI
*/
hide(ins: IUIInstance<C>): void;
/**
* UI
* @param ins UI
*/
show(ins: IUIInstance<C>): void;
/**
* UI
*/
keep(): IKeepController;
}
export interface IUIInstance<C extends UIComponent> {
/** 这个 ui 实例的唯一 key用于 vue */
readonly key: number;
/** 这个 ui 实例的 ui 信息 */
readonly ui: IGameUI<C>;
/** 传递给这个 ui 实例的响应式数据 */
readonly vBind: Props<C>;
/** 当前元素是否被隐藏 */
readonly hidden: boolean;
/**
* ui
*/
hide(): void;
/**
* ui
*/
show(): void;
}

19
src/core/system/ui/ui.ts Normal file
View File

@ -0,0 +1,19 @@
import { IGameUI, UIComponent } from './shared';
export class GameUI<C extends UIComponent> implements IGameUI<C> {
static list: Map<string, GameUI<UIComponent>> = new Map();
constructor(
public readonly name: string,
public readonly component: C
) {}
/**
* ui ui
* @param id ui
*/
static get<T extends UIComponent>(id: string): GameUI<T> | null {
const ui = this.list.get(id) as GameUI<T>;
return ui ?? null;
}
}

View File

@ -87,7 +87,8 @@
"53": "Cannot $1 audio route '$2', since there is not added route named it.",
"54": "Missing start tag for '$1', index: $2.",
"55": "Unchildable tag '$1' should follow with param.",
"56": "Method '$1' is deprecated. Consider using '$2' instead.",
"56": "Method '$1' has been deprecated. Consider using '$2' instead.",
"57": "Repeated UI controller on item '$1', new controller will not work.",
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency.",
"1101": "Cannot add new effect to point effect instance, for there's no more reserve space for it. Please increase the max count of the instance."
}

View File

@ -17,6 +17,7 @@ import { Textbox } from './components';
import { ILayerGroupRenderExtends, ILayerRenderExtends } from '@/core/render';
import { Props } from '@/core/render';
import { WeatherController } from '../weather';
import { UIController, UIRenderBase } from '@/core/system';
export function create() {
const main = new MotaRenderer();
@ -63,8 +64,11 @@ export function create() {
weather.bind(map.value);
});
const ui = new UIController('main-ui', UIRenderBase);
return () => (
<container id="map-draw" {...mapDrawProps}>
{ui.render()}
<layer-group id="layer-main" ex={layerGroupExtends} ref={map}>
<layer layer="bg" zIndex={10}></layer>
<layer layer="bg2" zIndex={20}></layer>