Compare commits

...

2 Commits

Author SHA1 Message Date
0cc76bd475 feat: 选择框和确认框的包装函数 2025-03-03 11:11:10 +08:00
5b2f42f692 refactor: UI 系统类型加强 2025-03-03 11:10:34 +08:00
5 changed files with 235 additions and 65 deletions

View File

@ -1,9 +1,9 @@
import { computed, defineComponent, VNode } from 'vue'; import { defineComponent, VNode } from 'vue';
import { IUIMountable, UIComponent } from './shared'; import { IUIMountable } from './shared';
import { SetupComponentOptions } from '@/module'; import { SetupComponentOptions } from '@/module';
export interface UIContainerProps { export interface UIContainerProps {
controller: IUIMountable<UIComponent>; controller: IUIMountable;
} }
const containerConfig = { const containerConfig = {
@ -13,18 +13,29 @@ const containerConfig = {
export const UIContainer = defineComponent<UIContainerProps>(props => { export const UIContainer = defineComponent<UIContainerProps>(props => {
const data = props.controller; const data = props.controller;
const back = data.backIns; const back = data.backIns;
const show = computed(() => data.stack.filter(v => !v.hidden));
return (): VNode[] => { return (): VNode[] => {
const elements: VNode[] = []; const elements: VNode[] = [];
const b = back.value; const b = back.value;
if (b && data.showBack.value && !b.hidden) { if (b && data.showBack.value && !b.hidden) {
elements.push( elements.push(
<b.ui.component {...b.vBind} key={b.key}></b.ui.component> <b.ui.component
{...b.vBind}
controller={data}
instance={b}
key={b.key}
hidden={b.hidden}
></b.ui.component>
); );
} }
return elements.concat( return elements.concat(
show.value.map(v => ( data.stack.map(v => (
<v.ui.component {...v.vBind} key={v.key}></v.ui.component> <v.ui.component
{...v.vBind}
key={v.key}
controller={data}
instance={v}
hidden={v.hidden}
></v.ui.component>
)) ))
); );
}; };

View File

@ -5,9 +5,9 @@ import {
IKeepController, IKeepController,
IUIInstance, IUIInstance,
IUIMountable, IUIMountable,
UIComponent UIComponent,
UIProps
} from './shared'; } from './shared';
import { Props } from '@/core/render';
import { UIInstance } from './instance'; import { UIInstance } from './instance';
import { import {
computed, computed,
@ -35,13 +35,13 @@ export const enum UIMode {
Custom Custom
} }
export interface IUICustomConfig<C extends UIComponent> { export interface IUICustomConfig {
/** /**
* UI * UI
* @param ins UI * @param ins UI
* @param stack UI UI * @param stack UI UI
*/ */
open(ins: IUIInstance<C>, stack: IUIInstance<C>[]): void; open(ins: IUIInstance, stack: IUIInstance[]): void;
/** /**
* UI * UI
@ -49,7 +49,7 @@ export interface IUICustomConfig<C extends UIComponent> {
* @param stack UI UI * @param stack UI UI
* @param index UI UI * @param index UI UI
*/ */
close(ins: IUIInstance<C>, stack: IUIInstance<C>[], index: number): void; close(ins: IUIInstance, stack: IUIInstance[], index: number): void;
/** /**
* UI * UI
@ -57,7 +57,7 @@ export interface IUICustomConfig<C extends UIComponent> {
* @param stack UI * @param stack UI
* @param index UI UI * @param index UI UI
*/ */
hide(ins: IUIInstance<C>, stack: IUIInstance<C>[], index: number): void; hide(ins: IUIInstance, stack: IUIInstance[], index: number): void;
/** /**
* UI * UI
@ -65,32 +65,32 @@ export interface IUICustomConfig<C extends UIComponent> {
* @param stack UI * @param stack UI
* @param index UI UI * @param index UI UI
*/ */
show(ins: IUIInstance<C>, stack: IUIInstance<C>[], index: number): void; show(ins: IUIInstance, stack: IUIInstance[], index: number): void;
/** /**
* UI * UI
* @param stack UI * @param stack UI
*/ */
update(stack: IUIInstance<C>[]): void; update(stack: IUIInstance[]): void;
} }
interface UIControllerEvent {} interface UIControllerEvent {}
export class UIController<C extends UIComponent = UIComponent> export class UIController
extends EventEmitter<UIControllerEvent> extends EventEmitter<UIControllerEvent>
implements IUIMountable<C> implements IUIMountable
{ {
static controllers: Map<string, UIController> = new Map(); static controllers: Map<string, UIController> = new Map();
/** 当前的 ui 栈 */ /** 当前的 ui 栈 */
readonly stack: IUIInstance<C>[] = reactive([]); readonly stack: IUIInstance[] = reactive([]);
/** UI 显示方式 */ /** UI 显示方式 */
mode: UIMode = UIMode.LastOnlyStack; mode: UIMode = UIMode.LastOnlyStack;
/** 这个 UI 实例的背景,当这个 UI 处于显示模式时,会显示背景 */ /** 这个 UI 实例的背景,当这个 UI 处于显示模式时,会显示背景 */
background?: IGameUI<C>; background?: IGameUI;
/** 背景 UI 实例 */ /** 背景 UI 实例 */
readonly backIns: ShallowRef<IUIInstance<C> | null> = shallowRef(null); readonly backIns: ShallowRef<IUIInstance | null> = shallowRef(null);
/** 当前是否显示背景 UI */ /** 当前是否显示背景 UI */
readonly showBack: ComputedRef<boolean> = computed( readonly showBack: ComputedRef<boolean> = computed(
() => this.userShowBack.value && this.sysShowBack.value () => this.userShowBack.value && this.sysShowBack.value
@ -102,7 +102,7 @@ export class UIController<C extends UIComponent = UIComponent>
} }
/** 自定义显示模式下的配置信息 */ /** 自定义显示模式下的配置信息 */
private config?: IUICustomConfig<C>; private config?: IUICustomConfig;
/** 是否维持背景 UI */ /** 是否维持背景 UI */
private keepBack: boolean = false; private keepBack: boolean = false;
/** 用户是否显示背景 UI */ /** 用户是否显示背景 UI */
@ -134,7 +134,7 @@ export class UIController<C extends UIComponent = UIComponent>
* UI * UI
* @param back UI UI * @param back UI UI
*/ */
setBackground(back: IGameUI<C>) { setBackground(back: IGameUI) {
this.background = back; this.background = back;
} }
@ -171,13 +171,12 @@ export class UIController<C extends UIComponent = UIComponent>
}; };
} }
/** open<T extends UIComponent>(
* ui ui: IGameUI<T>,
* @param ui ui vBind: UIProps<T>,
* @param vBind ui alwaysShow: boolean = false
*/ ): IUIInstance<T> {
open(ui: IGameUI<C>, vBind: Props<C>) { const ins = new UIInstance(ui, vBind, alwaysShow);
const ins = new UIInstance(ui, vBind);
switch (this.mode) { switch (this.mode) {
case UIMode.LastOnly: case UIMode.LastOnly:
case UIMode.LastOnlyStack: case UIMode.LastOnlyStack:
@ -196,11 +195,7 @@ export class UIController<C extends UIComponent = UIComponent>
return ins; return ins;
} }
/** close(ui: IUIInstance) {
* ui
* @param ui ui
*/
close(ui: UIInstance<C>) {
const index = this.stack.indexOf(ui); const index = this.stack.indexOf(ui);
if (index === -1) return; if (index === -1) return;
switch (this.mode) { switch (this.mode) {
@ -239,7 +234,16 @@ export class UIController<C extends UIComponent = UIComponent>
this.keepBack = false; this.keepBack = false;
} }
hide(ins: IUIInstance<C>): void { closeAll(ui?: IGameUI): void {
if (!ui) {
this.stack.splice(0);
} else {
const list = this.stack.filter(v => v.ui === ui);
list.forEach(v => this.close(v));
}
}
hide(ins: IUIInstance): void {
const index = this.stack.indexOf(ins); const index = this.stack.indexOf(ins);
if (index === -1) return; if (index === -1) return;
if (this.mode === UIMode.Custom) { if (this.mode === UIMode.Custom) {
@ -249,7 +253,7 @@ export class UIController<C extends UIComponent = UIComponent>
} }
} }
show(ins: IUIInstance<C>): void { show(ins: IUIInstance): void {
const index = this.stack.indexOf(ins); const index = this.stack.indexOf(ins);
if (index === -1) return; if (index === -1) return;
if (this.mode === UIMode.Custom) { if (this.mode === UIMode.Custom) {
@ -290,7 +294,7 @@ export class UIController<C extends UIComponent = UIComponent>
* 使 * 使
* @param config * @param config
*/ */
showCustom(config: IUICustomConfig<C>) { showCustom(config: IUICustomConfig) {
this.mode = UIMode.Custom; this.mode = UIMode.Custom;
this.config = config; this.config = config;
config.update(this.stack); config.update(this.stack);
@ -300,10 +304,8 @@ export class UIController<C extends UIComponent = UIComponent>
* ui * ui
* @param id ui * @param id ui
*/ */
static getController<T extends UIComponent>( static getController(id: string): UIController | null {
id: string const res = this.controllers.get(id);
): UIController<T> | null {
const res = this.controllers.get(id) as UIController<T>;
return res ?? null; return res ?? null;
} }
} }

View File

@ -1,5 +1,5 @@
import { Props } from '@/core/render'; import { Props } from '@/core/render';
import { IGameUI, IUIInstance, UIComponent } from './shared'; import { IGameUI, IUIInstance, UIComponent, UIProps } from './shared';
import EventEmitter from 'eventemitter3'; import EventEmitter from 'eventemitter3';
import { markRaw, mergeProps } from 'vue'; import { markRaw, mergeProps } from 'vue';
@ -21,7 +21,8 @@ export class UIInstance<C extends UIComponent>
constructor( constructor(
ui: IGameUI<C>, ui: IGameUI<C>,
public vBind: Props<C> public vBind: UIProps<C>,
public readonly alwaysShow: boolean = false
) { ) {
super(); super();
this.ui = markRaw(ui); this.ui = markRaw(ui);
@ -30,13 +31,13 @@ export class UIInstance<C extends UIComponent>
/** /**
* UI * UI
* @param data * @param data
* @param merge truefalse * @param merge truefalse
*/ */
setVBind(data: Props<C>, merge: boolean = true) { setVBind(data: Partial<Props<C>>, merge: boolean = true) {
if (merge) { if (merge) {
this.vBind = mergeProps(this.vBind, data) as Props<C>; this.vBind = mergeProps(this.vBind, data) as UIProps<C>;
} else { } else {
this.vBind = data; this.vBind = data as UIProps<C>;
} }
} }

View File

@ -3,7 +3,17 @@ import { DefineComponent, DefineSetupFnComponent, Ref, ShallowRef } from 'vue';
export type UIComponent = DefineSetupFnComponent<any> | DefineComponent; export type UIComponent = DefineSetupFnComponent<any> | DefineComponent;
export interface IGameUI<C extends UIComponent> { export interface UIComponentProps<T extends UIComponent = UIComponent> {
controller: IUIMountable;
instance: IUIInstance<T>;
}
export type UIProps<C extends UIComponent = UIComponent> = Omit<
Props<C>,
keyof UIComponentProps<C>
>;
export interface IGameUI<C extends UIComponent = UIComponent> {
/** 这个 UI 的名称 */ /** 这个 UI 的名称 */
readonly name: string; readonly name: string;
/** 这个 UI 的组件 */ /** 这个 UI 的组件 */
@ -22,11 +32,11 @@ export interface IKeepController {
unload(): void; unload(): void;
} }
export interface IUIMountable<C extends UIComponent> { export interface IUIMountable {
/** 当前的 UI 栈 */ /** 当前的 UI 栈 */
readonly stack: IUIInstance<C>[]; readonly stack: IUIInstance<UIComponent>[];
/** 当前的背景 UI */ /** 当前的背景 UI */
readonly backIns: ShallowRef<IUIInstance<C> | null>; readonly backIns: ShallowRef<IUIInstance<UIComponent> | null>;
/** 当前是否显示背景 UI */ /** 当前是否显示背景 UI */
readonly showBack: Ref<boolean>; readonly showBack: Ref<boolean>;
@ -34,13 +44,46 @@ export interface IUIMountable<C extends UIComponent> {
* UI * UI
* @param ins UI * @param ins UI
*/ */
hide(ins: IUIInstance<C>): void; hide(ins: IUIInstance<UIComponent>): void;
/** /**
* UI * UI
* @param ins UI * @param ins UI
*/ */
show(ins: IUIInstance<C>): void; show(ins: IUIInstance<UIComponent>): void;
/**
* UI
*/
hideBackground(): void;
/**
* UI
*/
showBackground(): void;
/**
* ui
* @param ui ui
* @param vBind ui
* @param alwaysShow ui ui
*/
open<T extends UIComponent>(
ui: IGameUI<T>,
vBind: UIProps<T>,
alwaysShow?: boolean
): IUIInstance<T>;
/**
* ui
* @param ui ui
*/
close(ui: IUIInstance<UIComponent>): void;
/**
* UI
*/
closeAll(ui?: IGameUI<UIComponent>): void;
/** /**
* UI * UI
@ -48,15 +91,17 @@ export interface IUIMountable<C extends UIComponent> {
keep(): IKeepController; keep(): IKeepController;
} }
export interface IUIInstance<C extends UIComponent> { export interface IUIInstance<C extends UIComponent = UIComponent> {
/** 这个 ui 实例的唯一 key用于 vue */ /** 这个 ui 实例的唯一 key用于 vue */
readonly key: number; readonly key: number;
/** 这个 ui 实例的 ui 信息 */ /** 这个 ui 实例的 ui 信息 */
readonly ui: IGameUI<C>; readonly ui: IGameUI<C>;
/** 传递给这个 ui 实例的响应式数据 */ /** 传递给这个 ui 实例的响应式数据 */
readonly vBind: Props<C>; readonly vBind: UIProps<C>;
/** 当前元素是否被隐藏 */ /** 当前元素是否被隐藏 */
readonly hidden: boolean; readonly hidden: boolean;
/** 是否永远保持开启 */
readonly alwaysShow: boolean;
/** /**
* ui * ui

View File

@ -5,6 +5,7 @@ import { TextContent, TextContentExpose, TextContentProps } from './textbox';
import { SetupComponentOptions } from './types'; import { SetupComponentOptions } from './types';
import { TextAlign } from './textboxTyper'; import { TextAlign } from './textboxTyper';
import { Page, PageExpose } from './page'; import { Page, PageExpose } from './page';
import { GameUI, IUIMountable } from '@/core/system';
export interface ConfirmBoxProps extends DefaultProps, TextContentProps { export interface ConfirmBoxProps extends DefaultProps, TextContentProps {
text: string; text: string;
@ -66,11 +67,11 @@ const confirmBoxProps = {
* color="#333" * color="#333"
* border="gold" * border="gold"
* // 设置选项的字体 * // 设置选项的字体
* selFont="16px Verdana" * selFont={new Font('Verdana', 16)}
* // 设置选项的文本颜色 * // 设置选项的文本颜色
* selFill="#d48" * selFill="#d48"
* // 完全继承 TextContent 的参数,因此可以填写 fontFamily 参数指定文本字体 * // 完全继承 TextContent 的参数,因此可以填写 font 参数指定文本字体
* fontFamily="Arial" * font={new Font('arial')}
* onYes={onYes} * onYes={onYes}
* onNo={onNo} * onNo={onNo}
* /> * />
@ -191,8 +192,11 @@ export const ConfirmBox = defineComponent<
); );
}, confirmBoxProps); }, confirmBoxProps);
export type ChoiceKey = string | number | symbol;
export type ChoiceItem = [key: ChoiceKey, text: string];
export interface ChoicesProps extends DefaultProps, TextContentProps { export interface ChoicesProps extends DefaultProps, TextContentProps {
choices: [key: string | number | symbol, text: string][]; choices: ChoiceItem[];
loc: ElementLocator; loc: ElementLocator;
width: number; width: number;
maxHeight?: number; maxHeight?: number;
@ -206,12 +210,11 @@ export interface ChoicesProps extends DefaultProps, TextContentProps {
titleFont?: Font; titleFont?: Font;
titleFill?: CanvasStyle; titleFill?: CanvasStyle;
pad?: number; pad?: number;
defaultChoice?: string | number | symbol;
interval?: number; interval?: number;
} }
export type ChoicesEmits = { export type ChoicesEmits = {
choice: (key: string | number | symbol) => void; choose: (key: ChoiceKey) => void;
}; };
const choicesProps = { const choicesProps = {
@ -230,16 +233,49 @@ const choicesProps = {
'titleFont', 'titleFont',
'titleFill', 'titleFill',
'pad', 'pad',
'defaultChoice',
'interval' 'interval'
], ],
emits: ['choice'] emits: ['choose']
} satisfies SetupComponentOptions< } satisfies SetupComponentOptions<
ChoicesProps, ChoicesProps,
ChoicesEmits, ChoicesEmits,
keyof ChoicesEmits keyof ChoicesEmits
>; >;
/**
*
* {@link ChoicesProps} {@link ChoicesEmits}
* ```tsx
* <Choices
* // 选项数组,每一项是一个二元素数组,第一项表示这个选项的 id在选中时会以此作为参数传递给事件
* // 第二项表示这一项的内容,即展示给玩家看的内容
* choices={[[0, '选项1'], [100, '选项2']]}
* // 选项框会自动计算宽度和高度,因此不需要手动指定,即使手动指定也无效
* loc={[240, 240, void 0, void 0, 0.5, 0.5]}
* text="请选择一项"
* title="选项"
* // 使用 winskin 图片作为背景
* winskin="winskin.png"
* // 使用颜色作为背景和边框,如果设置了 winskin那么此参数无效
* color="#333"
* border="gold"
* // 调整每两个选项之间的间隔
* interval={12}
* // 设置选项的字体
* selFont={new Font('Verdana', 16)}
* // 设置选项的文本颜色
* selFill="#d48"
* // 设置标题的字体
* titleFont={new Font('Verdana', 16)}
* // 设置标题的文本颜色
* selFill="gold"
* // 完全继承 TextContent 的参数,因此可以填写 font 参数指定文本字体
* font={new Font('arial')}
* // 当选择某一项时触发
* onChoice={(choice) => console.log(choice)}
* />
* ```
*/
export const Choices = defineComponent< export const Choices = defineComponent<
ChoicesProps, ChoicesProps,
ChoicesEmits, ChoicesEmits,
@ -424,7 +460,7 @@ export const Choices = defineComponent<
key.realize('confirm', () => { key.realize('confirm', () => {
const page = pageCom.value?.now() ?? 1; const page = pageCom.value?.now() ?? 1;
const index = page * choiceCountPerPage.value + selected.value; const index = page * choiceCountPerPage.value + selected.value;
emit('choice', props.choices[index][0]); emit('choose', props.choices[index][0]);
}); });
return () => ( return () => (
@ -479,7 +515,7 @@ export const Choices = defineComponent<
font={props.selFont} font={props.selFont}
cursor="pointer" cursor="pointer"
zIndex={5} zIndex={5}
onClick={() => emit('choice', v[0])} onClick={() => emit('choose', v[0])}
onSetText={(_, width, height) => onSetText={(_, width, height) =>
updateChoiceSize(i, width, height) updateChoiceSize(i, width, height)
} }
@ -492,3 +528,78 @@ export const Choices = defineComponent<
</container> </container>
); );
}, choicesProps); }, choicesProps);
/**
*
* @param controller UI
* @param text
* @param loc
* @param width
* @param props props
*/
export function getConfirm(
controller: IUIMountable,
text: string,
loc: ElementLocator,
width: number,
props?: Partial<ConfirmBoxProps>
) {
return new Promise<boolean>(res => {
const instance = controller.open(
ConfirmBoxUI,
{
...(props ?? {}),
loc,
text,
width,
onYes: () => {
controller.close(instance);
res(true);
},
onNo: () => {
controller.close(instance);
res(false);
}
},
true
);
});
}
/**
*
* @param controller UI
* @param choices
* @param loc
* @param width
* @param props props
*/
export function getChoice<T extends ChoiceKey = ChoiceKey>(
controller: IUIMountable,
choices: ChoiceItem[],
loc: ElementLocator,
width: number,
props?: Partial<ChoicesProps>
) {
return new Promise<T>(res => {
const instance = controller.open(
ChoicesUI,
{
...(props ?? {}),
choices,
loc,
width,
onChoose: key => {
controller.close(instance);
res(key as T);
}
},
true
);
});
}
/** @see {@link ConfirmBox} */
export const ConfirmBoxUI = new GameUI('confirm-box', ConfirmBox);
/** @see {@link Choices} */
export const ChoicesUI = new GameUI('choices', Choices);