refactor: 将 system-action 与 system-ui 合并至 system

This commit is contained in:
AncTe committed 2026-03-08 17:01:05 +08:00
1 parent 744a21e042
commit 03cf6a8917
64 files changed
+82 -174

No files matched your search

+508
View File
@@ -0,0 +1,508 @@
import { KeyCode } from '@motajs/client-base';
import {
deleteWith,
generateBinary,
keycode,
spliceBy
} from '@motajs/legacy-common';
import { EventEmitter } from 'eventemitter3';
import { isNil } from 'lodash-es';
// todo: 按下加速节流触发
interface HotkeyEvent {
set: [id: string, key: KeyCode, assist: number];
emit: [key: KeyCode, assist: number, type: KeyEmitType];
press: [key: KeyCode];
release: [key: KeyCode];
}
type KeyEmitType =
| 'up'
| 'down'
| 'down-repeat'
| 'down-throttle'
// todo: | 'down-accelerate'
| 'down-timeout';
type KeyEventType = 'up' | 'down';
interface AssistHoykey {
ctrl: boolean;
shift: boolean;
alt: boolean;
}
interface RegisterHotkeyData extends Partial<AssistHoykey> {
id: string;
name: string;
defaults: KeyCode;
}
export interface HotkeyData extends Required<RegisterHotkeyData> {
key: KeyCode;
emits: Map<symbol, HotkeyEmitData>;
group?: string;
}
/**
* @param id 此处的id包含数字后缀
* @returns 返回 `@void` 时,表示此次触发没有包含副作用,不会致使 `preventDefault` 被执行
*/
type HotkeyFunc = (
id: string,
code: KeyCode,
assist: number,
ev: KeyboardEvent
) => void | '@void';
interface HotkeyEmitData {
func: HotkeyFunc;
onUp?: HotkeyFunc;
config?: HotkeyEmitConfig;
}
export interface HotkeyJSON {
key: KeyCode;
assist: number;
}
export interface HotkeyEmitConfig {
type: KeyEmitType;
throttle?: number;
accelerate?: number;
timeout?: number;
}
export class Hotkey extends EventEmitter<HotkeyEvent> {
static list: Hotkey[];
id: string;
name: string;
data: Record<string, HotkeyData> = {};
keyMap: Map<KeyCode, HotkeyData[]> = new Map();
/** id to name */
groupName: Record<string, string> = {
none: '未分类按键'
};
/** id to keys */
groups: Record<string, string[]> = {
none: []
};
enabled: boolean = false;
conditionMap: Map<symbol, () => boolean> = new Map();
/** 当前作用域 */
scope: symbol = Symbol();
private scopeStack: symbol[] = [];
private grouping: string = 'none';
/** 当前正在按下的按键 */
private pressed: Set<KeyCode> = new Set();
/** 有哪些单次按下的按键已经触发 */
private downEmitted: Set<KeyCode> = new Set();
/** 按键按下时的时间 */
private pressTime: Map<KeyCode, number> = new Map();
/** 按键节流时间 */
private throttleMap: Map<KeyCode, number> = new Map();
constructor(id: string, name: string) {
super();
this.id = id;
this.name = name;
}
/**
* 注册一个按键,id可以包含数字后缀,可以显示为同一个按键操作拥有多个按键可以触发
* @param data 要注册的按键信息
*/
register(data: RegisterHotkeyData): this {
const d: HotkeyData = {
...data,
ctrl: !!data.ctrl,
shift: !!data.shift,
alt: !!data.alt,
key: data.defaults,
emits: new Map(),
group: this.grouping
};
this.ensureMap(d.key);
if (d.id in this.data) {
// console.warn(`已存在id为${d.id}的按键,已将其覆盖`);
}
this.data[d.id] = d;
const arr = this.keyMap.get(d.key)!;
arr.push(d);
this.groups[this.grouping].push(d.id);
return this;
}
/**
* 实现一个按键按下时的操作
* @param id 要实现的按键id,可以不包含数字后缀
* @param func 按键按下时执行的函数
* @param config 按键的配置信息
*/
realize(id: string, func: HotkeyFunc, config?: HotkeyEmitConfig): this {
const toSet = Object.values(this.data).filter(v => {
const split = v.id.split('_');
const last = !isNaN(Number(split.at(-1)));
return v.id === id || (last && split.slice(0, -1).join('_') === id);
});
if (toSet.length === 0) {
throw new Error(`Realize nonexistent key '${id}'.`);
}
for (const key of toSet) {
const data = key.emits.get(this.scope);
if (!data) {
key.emits.set(this.scope, { func, config });
} else {
// 同时注册抬起和按下
const dataType = data.config?.type ?? 'up';
const configType = config?.type ?? 'up';
if (dataType === 'up' && configType !== 'up') {
data.onUp = func;
data.config ??= { type: configType };
data.config.type = configType;
} else if (dataType !== 'up' && configType === 'up') {
data.onUp = func;
} else {
data.config = config;
data.func = func;
}
}
}
return this;
}
/**
* 使用一个symbol作为当前作用域,之后调用{@link realize}所实现的按键功能将会添加至此作用域
* @param symbol 当前作用域的symbol
*/
use(symbol: symbol): void {
if (symbol === this.scope) return;
this.dispose(symbol);
this.scopeStack.push(symbol);
this.scope = symbol;
this.conditionMap.set(symbol, () => true);
}
/**
* 释放一个作用域,释放后作用域将退回至删除的作用域的上一级
* @param symbol 要释放的作用域的symbol
*/
dispose(symbol: symbol = this.scopeStack.at(-1) ?? Symbol()): void {
const disposed = spliceBy(this.scopeStack, symbol);
disposed.forEach(v => {
for (const key of Object.values(this.data)) {
key.emits.delete(v);
}
});
this.scope = this.scopeStack.at(-1) ?? Symbol();
}
/**
* 设置一个按键信息
* @param id 要设置的按键的id
* @param key 要设置成的按键
* @param assist 辅助按键,三位二进制数据,从低到高依次为`ctrl` `shift` `alt`
* @param emit 是否触发set事件,当且仅当从fromJSON方法调用时为false
*/
set(id: string, key: KeyCode, assist: number, emit: boolean = true): void {
const { ctrl, shift, alt } = unwarpBinary(assist);
const data = this.data[id];
if (!data) return;
const before = this.keyMap.get(data?.key ?? KeyCode.Unknown)! ?? [];
deleteWith(before, data);
this.ensureMap(key);
const after = this.keyMap.get(key)!;
after.push(data);
data.key = key;
data.ctrl = ctrl;
data.shift = shift;
data.alt = alt;
if (emit) this.emit('set', id, key, assist);
}
/**
* 判断指定按键和辅助按键是否可能会触发给定 id 的按键操作
* @param key 按键
* @param assist 辅助按键
* @param id 注册的按键操作 id
*/
willEmit(key: KeyCode, assist: number, id: string) {
const emittable = this.keyMap.get(key);
if (!emittable) return false;
const { ctrl, shift, alt } = unwarpBinary(assist);
return emittable.some(v => {
if (
v.id === id &&
v.ctrl === ctrl &&
v.shift === shift &&
v.alt === alt
) {
return true;
} else {
return false;
}
});
}
/**
* 触发一个按键
* @param key 要触发的按键
* @param assist 辅助按键,三位二进制数据,从低到高依次为`ctrl` `shift` `alt`
* @returns 是否有按键被触发
*/
emitKey(
key: KeyCode,
assist: number,
type: KeyEventType,
ev: KeyboardEvent
): boolean {
// 检查全局启用情况
if (!this.enabled) return false;
const when = this.conditionMap.get(this.scope);
if (type === 'up') this.checkPressEnd(key);
else if (type === 'down') this.checkPress(key);
if (when && !when()) return false;
const toEmit = this.keyMap.get(key);
if (!toEmit) return false;
// 进行按键初始处理
const { ctrl, shift, alt } = unwarpBinary(assist);
// 真正开始触发按键
let emitted = false;
toEmit.forEach(v => {
if (ctrl === v.ctrl && shift === v.shift && alt === v.alt) {
const data = v.emits.get(this.scope);
if (!data) return;
if (type === 'up' && data.onUp) {
data.onUp(v.id, key, assist, ev);
emitted = true;
return;
}
if (!this.canEmit(v.id, key, type, data)) return;
const res = data.func(v.id, key, assist, ev);
if (res !== '@void') emitted = true;
}
});
this.emit('emit', key, assist, type);
if (type === 'down') {
this.downEmitted.add(key);
}
return emitted;
}
/**
* 检查按键按下情况,如果没有按下则添加
* @param keyCode 按下的按键
*/
private checkPress(keyCode: KeyCode): void {
if (this.pressed.has(keyCode)) return;
this.pressed.add(keyCode);
this.pressTime.set(keyCode, Date.now());
this.emit('press', keyCode);
}
/**
* 当按键松开时,移除相应的按下配置
* @param keyCode 松开的按键
*/
private checkPressEnd(keyCode: KeyCode): void {
if (!this.pressed.has(keyCode)) return;
this.pressed.delete(keyCode);
this.pressTime.delete(keyCode);
this.downEmitted.delete(keyCode);
this.emit('release', keyCode);
}
/**
* 检查一个按键是否可以被触发
* @param id 触发的按键id
* @param keyCode 按键码
*/
private canEmit(
_id: string,
keyCode: KeyCode,
type: KeyEventType,
data: HotkeyEmitData
) {
const config = data?.config;
// 这时默认为抬起触发,始终可触发
if (type === 'up') {
if (!config || config.type === 'up') return true;
else return false;
}
if (!config) return false;
// 按下单次触发
if (config.type === 'down') return !this.downEmitted.has(keyCode);
// 按下重复触发
if (config.type === 'down-repeat') return true;
if (config.type === 'down-timeout') {
const time = this.pressTime.get(keyCode);
if (isNil(time) || isNil(config.timeout)) return false;
return Date.now() - time >= config.timeout;
}
if (config.type === 'down-throttle') {
const thorttleTime = this.throttleMap.get(keyCode);
if (isNil(config.throttle)) return false;
if (isNil(thorttleTime)) {
this.throttleMap.set(keyCode, Date.now());
return true;
}
if (Date.now() - thorttleTime >= config.throttle) {
this.throttleMap.set(keyCode, Date.now());
return true;
}
return false;
}
return false;
}
/**
* 按键分组,执行后 register 的按键将会加入此分组
* @param id 组的id
* @param name 组的名称
*/
group(id: string, name: string, keys?: RegisterHotkeyData[]): this {
this.grouping = id;
this.groupName[id] = name;
this.groups[id] ??= [];
keys?.forEach(v => this.register(v));
return this;
}
/**
* 启用这个按键控制器
*/
enable(): void {
this.enabled = true;
}
/**
* 禁用这个按键控制器
*/
disable(): void {
this.enabled = false;
}
/**
* 在当前作用域下,满足什么条件时触发按键
* @param fn 条件函数
*/
when(fn: () => boolean): this {
this.conditionMap.set(this.scope, fn);
return this;
}
toJSON(): string {
const res: Record<string, HotkeyJSON> = {};
for (const [key, data] of Object.entries(this.data)) {
res[key] = {
key: data.key,
assist: generateBinary([data.ctrl, data.shift, data.alt])
};
}
return JSON.stringify(res);
}
fromJSON(data: string): void {
const json: Record<string, HotkeyJSON> = JSON.parse(data);
for (const [key, data] of Object.entries(json)) {
this.set(key, data.key, data.assist, false);
}
}
private ensureMap(key: KeyCode) {
if (!this.keyMap.has(key)) {
this.keyMap.set(key, []);
}
}
/**
* 根据id获取hotkey实例
* @param id 要获取的hotkey实例的id
*/
static get(id: string): Hotkey | undefined {
return this.list.find(v => v.id === id);
}
}
export function unwarpBinary(bin: number): AssistHoykey {
return {
ctrl: !!(bin & (1 << 0)),
shift: !!(bin & (1 << 1)),
alt: !!(bin & (1 << 2))
};
}
export function checkAssist(bin: number, key: KeyCode): boolean {
return (
isAssist(key) &&
!!(
(1 << (key === KeyCode.Ctrl ? 0 : key === KeyCode.Shift ? 1 : 2)) &
bin
)
);
}
export function isAssist(
key: KeyCode
): key is KeyCode.Ctrl | KeyCode.Shift | KeyCode.Alt {
return key === KeyCode.Ctrl || key === KeyCode.Shift || key === KeyCode.Alt;
}
export const gameKey = new Hotkey('gameKey', '游戏按键');
// ----- Listening
document.addEventListener('keyup', e => {
const assist = generateBinary([e.ctrlKey, e.shiftKey, e.altKey]);
const code = keycode(e.keyCode);
if (
code === KeyCode.Alt ||
code === KeyCode.Shift ||
code === KeyCode.Ctrl
) {
e.preventDefault();
}
if (gameKey.emitKey(code, assist, 'up', e)) {
e.preventDefault();
if (core.status.holdingKeys) {
deleteWith(core.status.holdingKeys, e.keyCode);
}
} else {
// polyfill样板
if (main.dom.inputDiv.style.display === 'block') {
if (e.keyCode === 13) {
setTimeout(function () {
main.dom.inputYes.click();
}, 50);
} else if (e.keyCode === 27) {
setTimeout(function () {
main.dom.inputNo.click();
}, 50);
}
return;
}
if (
core &&
core.isPlaying &&
core.status &&
(core.isPlaying() || core.status.lockControl)
)
core.onkeyUp(e);
}
});
document.addEventListener('keydown', e => {
const assist = generateBinary([e.ctrlKey, e.shiftKey, e.altKey]);
const code = keycode(e.keyCode);
if (gameKey.emitKey(code, assist, 'down', e)) {
e.preventDefault();
}
});
+2
View File
@@ -0,0 +1,2 @@
export * from './hotkey';
export * from './keyboard';
+205
View File
@@ -0,0 +1,205 @@
import {
EventEmitter,
Listener,
deleteWith,
flipBinary
} from '@motajs/legacy-common';
import { KeyCode } from '@motajs/client-base';
import { gameKey } from './hotkey';
import { unwarpBinary } from './hotkey';
import { cloneDeep } from 'lodash-es';
import { shallowReactive } from 'vue';
export interface KeyboardEmits {
key: KeyCode;
assist: number;
}
interface KeyboardItem {
key: KeyCode;
text?: string;
x: number;
y: number;
width: number;
height: number;
}
interface VirtualKeyEmit {
preventDefault(): void;
preventAssist(): void;
}
type VirtualKeyEmitFn = (
item: KeyboardItem,
assist: number,
index: number,
ev: VirtualKeyEmit
) => void;
interface VirtualKeyboardEvent {
add: (item: KeyboardItem) => void;
remove: (item: KeyboardItem) => void;
extend: (extended: Keyboard) => void;
emit: VirtualKeyEmitFn;
scopeCreate: (scope: symbol) => void;
scopeDispose: (scope: symbol) => void;
}
/**
* 虚拟按键,同一个虚拟按键实例应当只能同时操作一个,但可以显示多个
*/
export class Keyboard extends EventEmitter<VirtualKeyboardEvent> {
static list: Keyboard[] = [];
id: string;
keys: KeyboardItem[] = [];
assist: number = 0;
fontSize: number = 18;
scope: symbol = Symbol();
private scopeStack: symbol[] = [];
private onEmitKey: Record<symbol, Listener<VirtualKeyEmitFn>[]> = {};
private scopeAssist: Record<symbol, number> = {};
constructor(id: string) {
super();
this.id = id;
Keyboard.list.push(this);
}
/**
* 给虚拟键盘添加一个按键
* @param item 按键信息
*/
add(item: KeyboardItem) {
const i = shallowReactive(item);
this.keys.push(i);
this.emit('add', i);
return this;
}
/**
* 移除一个按键
* @param item 按键信息
*/
remove(item: KeyboardItem) {
deleteWith(this.keys, item);
this.emit('remove', item);
return this;
}
/**
* 创造一个在某些辅助按键已经按下的情况下的作用域,这些被按下的辅助按键还可以被玩家手动取消
* @param assist 辅助按键
*/
withAssist(assist: number) {
const symbol = this.createScope();
this.assist = assist;
return symbol;
}
/**
* 创造一个虚拟按键作用域,所有监听的事件与其他作用域不冲突
* @returns 作用域的唯一标识符
*/
createScope() {
const last = this.scopeStack.at(-1);
const symbol = Symbol();
this.scope = symbol;
this.scopeStack.push(symbol);
const ev: Listener<VirtualKeyEmitFn>[] = [];
this.onEmitKey[symbol] = ev;
// @ts-expect-error 无法推导
this.events = ev;
this.emit('scopeCreate', symbol);
if (last) {
this.scopeAssist[symbol] = this.assist;
}
this.assist = 0;
this.scopeAssist[symbol] = 0;
return symbol;
}
/**
* 释放一个作用域,同时删除其中的所有监听器
*/
disposeScope() {
if (this.scopeStack.length === 0) {
throw new Error(
`Cannot dispose virtual key scope since there's no scope to be disposed.`
);
}
const now = this.scopeStack.pop()!;
delete this.onEmitKey[now];
delete this.scopeAssist[now];
const symbol = this.scopeStack.at(-1);
this.emit('scopeDispose', now);
if (!symbol) return;
this.scope = symbol;
this.assist = this.scopeAssist[symbol];
// @ts-expect-error 无法推导
this.events = this.onEmitKey[symbol];
}
/**
* 继承一个按键的按键信息
* @param keyboard 要被继承的按键
* @param offsetX 被继承的按键的横坐标偏移量
* @param offsetY 被继承的按键的纵坐标偏移量
*/
extend(keyboard: Keyboard, offsetX: number = 0, offsetY: number = 0) {
const toClone = cloneDeep(keyboard.keys);
toClone.forEach(v => {
v.x += offsetX;
v.y += offsetY;
});
this.keys.push(...toClone);
this.emit('extend', keyboard);
return this;
}
/**
* 触发按键
* @param key 要触发的按键
*/
emitKey(key: KeyboardItem, index: number) {
let prevent = false;
let preventAss = false;
const preventDefault = () => (prevent = true);
const preventAssist = () => (preventAss = true);
this.emit('emit', key, this.assist, index, {
preventDefault,
preventAssist
});
if (!preventAss) {
if (key.key === KeyCode.Ctrl) {
this.assist = flipBinary(this.assist, 0);
} else if (key.key === KeyCode.Shift) {
this.assist = flipBinary(this.assist, 1);
} else if (key.key === KeyCode.Alt) {
this.assist = flipBinary(this.assist, 2);
}
}
if (!prevent) {
const ev = generateKeyboardEvent(key.key, this.assist);
gameKey.emitKey(key.key, this.assist, 'up', ev);
}
}
static get(id: string) {
return this.list.find(v => v.id === id);
}
}
export function generateKeyboardEvent(_key: KeyCode, assist: number) {
const { ctrl, alt, shift } = unwarpBinary(assist);
const ev = new KeyboardEvent('keyup', {
ctrlKey: ctrl,
shiftKey: shift,
altKey: alt
});
return ev;
}
+2 -2
View File
@@ -1,2 +1,2 @@
export * from '@motajs/system-action';
export * from '@motajs/system-ui';
export * from './action';
export * from './ui';
+44
View File
@@ -0,0 +1,44 @@
import { defineComponent, VNode } from 'vue';
import { IUIMountable } from './shared';
import { SetupComponentOptions } from './types';
export interface UIContainerProps {
controller: IUIMountable;
}
const containerConfig = {
props: ['controller']
} satisfies SetupComponentOptions<UIContainerProps>;
export const UIContainer = defineComponent<UIContainerProps>(props => {
const data = props.controller;
const back = data.backIns;
return (): VNode[] => {
const elements: VNode[] = [];
const b = back.value;
if (b && data.showBack.value && !b.hidden) {
elements.push(
<b.ui.component
{...b.vBind}
controller={data}
instance={b}
key={b.key}
hidden={b.hidden && !b.alwaysShow}
zIndex={0}
></b.ui.component>
);
}
return elements.concat(
data.stack.map((v, i) => (
<v.ui.component
{...v.vBind}
key={v.key}
controller={data}
instance={v}
hidden={v.hidden && !v.alwaysShow}
zIndex={i * 5}
></v.ui.component>
))
);
};
}, containerConfig);
+313
View File
@@ -0,0 +1,313 @@
import { logger } from '@motajs/common';
import EventEmitter from 'eventemitter3';
import {
IGameUI,
IKeepController,
IUIInstance,
IUIMountable,
UIComponent,
UIProps
} from './shared';
import { UIInstance } from './instance';
import {
computed,
ComputedRef,
h,
reactive,
ref,
Ref,
shallowRef,
ShallowRef,
VNode
} from 'vue';
import { UIContainer } from './container';
export const enum UIMode {
/** 仅显示最后一个非 alwaysShow 的 UI,在关闭时,只会关闭指定的 UI */
LastOnly,
/** 显示所有非手动隐藏的 UI,在关闭时,只会关闭指定 UI */
All,
/** 仅显示最后一个非 alwaysShow 的 UI,在关闭时,在此之后的所有 UI 会全部关闭 */
LastOnlyStack,
/** 显示所有非手动隐藏的 UI,在关闭时,在此之后的所有 UI 会全部关闭 */
AllStack,
/** 自定义 UI 显示模式 */
Custom
}
export interface IUICustomConfig {
/**
* 打开一个新的 UI
* @param ins 要打开的 UI 实例
* @param stack 当前的 UI 栈,还未将 UI 实例加入栈中
*/
open(ins: IUIInstance, stack: IUIInstance[]): void;
/**
* 关闭一个 UI
* @param ins 要关闭的 UI 实例
* @param stack 当前的 UI 栈,还未将 UI 实例移除
* @param index 这个 UI 实例在 UI 栈中的索引
*/
close(ins: IUIInstance, stack: IUIInstance[], index: number): void;
/**
* 隐藏一个 UI
* @param ins 要隐藏的 UI 实例,还未进入隐藏状态
* @param stack 当前的 UI 栈
* @param index 这个 UI 实例在 UI 栈中的索引
*/
hide(ins: IUIInstance, stack: IUIInstance[], index: number): void;
/**
* 显示一个 UI
* @param ins 要显示的 UI 实例,还未进入显示状态
* @param stack 当前的 UI 栈
* @param index 这个 UI 实例在 UI 栈中的索引
*/
show(ins: IUIInstance, stack: IUIInstance[], index: number): void;
/**
* 更新所有 UI 的显示,一般会在显示模式更改时调用
* @param stack 当前的 UI 栈
*/
update(stack: IUIInstance[]): void;
}
interface UIControllerEvent {
open: [ui: IGameUI, ins: IUIInstance];
close: [ui: IUIInstance];
}
export class UIController
extends EventEmitter<UIControllerEvent>
implements IUIMountable
{
static controllers: Map<string, UIController> = new Map();
/** 当前的 ui 栈 */
readonly stack: IUIInstance[] = reactive([]);
/** UI 显示方式 */
mode: UIMode = UIMode.LastOnlyStack;
/** 这个 UI 实例的背景,当这个 UI 处于显示模式时,会显示背景 */
background?: IGameUI;
/** 背景 UI 实例 */
readonly backIns: ShallowRef<IUIInstance | null> = shallowRef(null);
/** 当前是否显示背景 UI */
readonly showBack: ComputedRef<boolean> = computed(
() => this.userShowBack.value && this.sysShowBack.value
);
/** 当前 UI 是否激活 */
readonly active: Ref<boolean> = ref(false);
/** 自定义显示模式下的配置信息 */
private config?: IUICustomConfig;
/** 是否维持背景 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) {
super();
if (UIController.controllers.has(id)) {
logger.warn(57, id);
} else {
UIController.controllers.set(id, this);
}
}
render(): VNode {
return h(UIContainer, { controller: this });
}
/**
* 设置背景 UI
* @param back 这个 UI 控制器的背景 UI
*/
setBackground<T extends UIComponent>(back: IGameUI<T>, vBind: UIProps<T>) {
this.background = back;
this.backIns.value = new UIInstance(back, vBind, true);
}
/**
* 隐藏背景 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;
}
};
}
open<T extends UIComponent>(
ui: IGameUI<T>,
vBind: UIProps<T>,
alwaysShow: boolean = false
): IUIInstance<T> {
const ins = new UIInstance(ui, vBind, alwaysShow);
switch (this.mode) {
case UIMode.LastOnly:
case UIMode.LastOnlyStack:
this.stack.push(ins);
this.stack.forEach(v => v.hide());
this.stack.findLast(v => !v.alwaysShow)?.show();
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;
this.active.value = true;
this.emit('open', ui, ins);
return ins;
}
close(ui: IUIInstance) {
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());
this.stack.findLast(v => !v.alwaysShow)?.show();
break;
}
case UIMode.LastOnlyStack: {
this.stack.splice(index);
this.stack.forEach(v => v.hide());
this.stack.findLast(v => !v.alwaysShow)?.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.active.value = false;
}
this.keepBack = false;
this.emit('close', ui);
}
closeAll(ui?: IGameUI): void {
if (!ui) {
const list = this.stack.slice();
list.forEach(v => this.close(v));
} 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);
if (index === -1) return;
if (this.mode === UIMode.Custom) {
this.config?.hide(ins, this.stack, index);
} else {
ins.hide();
}
}
show(ins: IUIInstance): 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) {
this.mode = UIMode.Custom;
this.config = config;
config.update(this.stack);
}
/**
* 获取一个元素上的 ui 控制器
* @param id 要获取的 ui 控制器的唯一标识符
*/
static getController(id: string): UIController | null {
const res = this.controllers.get(id);
return res ?? null;
}
}
+6
View File
@@ -0,0 +1,6 @@
export * from './container';
export * from './controller';
export * from './instance';
export * from './shared';
export * from './types';
export * from './ui';
+40
View File
@@ -0,0 +1,40 @@
import { Props } from '@motajs/render';
import { IGameUI, IUIInstance, UIComponent, UIProps } from './shared';
import { markRaw, mergeProps } from 'vue';
export class UIInstance<C extends UIComponent> 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: UIProps<C>,
public readonly alwaysShow: boolean = false
) {
this.ui = markRaw(ui);
}
/**
* 设置这个 UI 实例的响应式数据的值
* @param data 要设置的值
* @param merge 是将传入的值与原先的值合并(true),还是将当前值覆盖掉原先的值(false),默认合并
*/
setVBind(data: Partial<Props<C>>, merge: boolean = true) {
if (merge) {
this.vBind = mergeProps(this.vBind, data) as UIProps<C>;
} else {
this.vBind = data as UIProps<C>;
}
}
hide(): void {
this.hidden = true;
}
show(): void {
this.hidden = false;
}
}
+133
View File
@@ -0,0 +1,133 @@
import { Props } from '@motajs/render';
import {
DefineComponent,
DefineSetupFnComponent,
Ref,
ShallowRef,
VNode
} from 'vue';
export type UIComponent = DefineSetupFnComponent<any> | DefineComponent;
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 的名称 */
readonly name: string;
/** 这个 UI 的组件 */
readonly component: C;
}
export interface IKeepController {
/**
* 安全关闭背景 UI,如果当前没有 UI 已开启,那么直接关闭,否则维持
*/
safelyUnload(): void;
/**
* 不论当前是否有 UI 已开启,都关闭背景
*/
unload(): void;
}
export interface IUIMountable {
/** 当前的 UI 栈 */
readonly stack: IUIInstance<UIComponent>[];
/** 当前的背景 UI */
readonly backIns: ShallowRef<IUIInstance<UIComponent> | null>;
/** 当前是否显示背景 UI */
readonly showBack: Ref<boolean>;
/**
* 隐藏一个 UI
* @param ins 要隐藏的 UI 实例
*/
hide(ins: IUIInstance<UIComponent>): void;
/**
* 显示一个 UI
* @param ins 要显示的 UI 实例
*/
show(ins: IUIInstance<UIComponent>): void;
/**
* 渲染这个 UI,可以直接嵌入渲染树中
*/
render(): VNode;
/**
* 隐藏背景 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 都被关闭
*/
keep(): IKeepController;
}
export interface IUIInstance<C extends UIComponent = UIComponent> {
/** 这个 ui 实例的唯一 key,用于 vue */
readonly key: number;
/** 这个 ui 实例的 ui 信息 */
readonly ui: IGameUI<C>;
/** 传递给这个 ui 实例的响应式数据 */
readonly vBind: UIProps<C>;
/** 当前元素是否被隐藏 */
readonly hidden: boolean;
/** 是否永远保持开启 */
readonly alwaysShow: boolean;
/**
* 设置这个 UI 实例的响应式数据的值
* @param data 要设置的值
* @param merge 是将传入的值与原先的值合并(true),还是将当前值覆盖掉原先的值(false),默认合并
*/
setVBind(data: Partial<Props<C>>, merge?: boolean): void;
/**
* 隐藏这个 ui
*/
hide(): void;
/**
* 显示这个 ui
*/
show(): void;
}
+12
View File
@@ -0,0 +1,12 @@
import { ComponentOptions, EmitsOptions, SlotsType } from 'vue';
export type SetupComponentOptions<
Props extends Record<string, any>,
E extends EmitsOptions = {},
EE extends string = string,
S extends SlotsType = {}
> = Pick<ComponentOptions, 'name' | 'inheritAttrs'> & {
props?: (keyof Props)[];
emits?: E | EE[];
slots?: S;
};
+19
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;
}
}