feat: 自定义状态栏的编辑

This commit is contained in:
unanmed 2024-01-19 22:09:48 +08:00
parent 1e341fb63e
commit ad2cb0b757
8 changed files with 714 additions and 94 deletions

1
components.d.ts vendored
View File

@ -9,6 +9,7 @@ declare module '@vue/runtime-core' {
export interface GlobalComponents {
AButton: typeof import('ant-design-vue/es')['Button']
ADivider: typeof import('ant-design-vue/es')['Divider']
AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AProgress: typeof import('ant-design-vue/es')['Progress']
ASelect: typeof import('ant-design-vue/es')['Select']

View File

@ -1,5 +1,5 @@
<template>
<div id="ui-new">
<div id="ui">
<div id="ui-main">
<div id="ui-list">
<div class="ui-one" v-for="(ui, index) of mainUi.stack">
@ -41,14 +41,6 @@ function show(index: number) {
<style lang="less" scoped>
#ui {
width: 90%;
height: 90%;
display: flex;
justify-content: center;
overflow: hidden;
}
#ui-new {
width: 0;
height: 0;
left: 0;
@ -56,6 +48,7 @@ function show(index: number) {
position: fixed;
overflow: visible;
display: block;
font-family: 'normal';
}
#ui-main {

View File

@ -93,8 +93,8 @@ export class Keyboard extends EventEmitter<VirtualKeyboardEvent> {
* @param assist
*/
withAssist(assist: number) {
this.assist = assist;
const symbol = this.createScope();
this.assist = assist;
return symbol;
}

View File

@ -1,6 +1,6 @@
import { EmitableEvent, EventEmitter } from '@/core/common/eventEmitter';
import { KeyCode } from '@/plugin/keyCodes';
import { flipBinary, has } from '@/plugin/utils';
import { deleteWith, flipBinary, has } from '@/plugin/utils';
import {
FunctionalComponent,
markRaw,
@ -8,7 +8,10 @@ import {
reactive,
shallowReactive
} from 'vue';
import { createToolbarComponents } from '../init/toolbar';
import {
createToolbarComponents,
createToolbarEditorComponents
} from '../init/toolbar';
import { gameKey } from '../init/hotkey';
import { unwarpBinary } from './hotkey';
import { fixedUi } from '../init/ui';
@ -24,7 +27,6 @@ interface CustomToolbarEvent extends EmitableEvent {
interface ToolbarItemBase<T extends ToolbarItemType> {
type: T;
id: string;
com: CustomToolbarComponent<T>;
}
// 快捷键
@ -49,7 +51,7 @@ interface ToolbarItemMap {
assistKey: AssistKeyToolbarItem;
}
type ToolbarItemType = keyof ToolbarItemMap;
export type ToolbarItemType = keyof ToolbarItemMap;
export type SettableItemData<T extends ToolbarItemType = ToolbarItemType> =
Omit<ToolbarItemMap[T], 'id' | 'type'>;
@ -64,19 +66,27 @@ export type CustomToolbarComponent<
T extends ToolbarItemType = ToolbarItemType
> = FunctionalComponent<CustomToolbarProps<T>>;
const COM = createToolbarComponents();
type ToolItemEmitFn<T extends ToolbarItemType> = (
this: CustomToolbar,
id: string,
item: ToolbarItemMap[T]
) => boolean;
const comMap: {
[P in ToolbarItemType]: CustomToolbarComponent<P>;
} = {
hotkey: COM.KeyTool,
item: COM.ItemTool,
assistKey: COM.AssistKeyTool
};
interface RegisteredCustomToolInfo {
name: string;
onEmit: ToolItemEmitFn<ToolbarItemType>;
show: CustomToolbarComponent;
editor: CustomToolbarComponent;
onCreate: (item: any) => ToolbarItemBase<ToolbarItemType>;
}
const COM = createToolbarComponents();
const EDITOR = createToolbarEditorComponents();
export class CustomToolbar extends EventEmitter<CustomToolbarEvent> {
static num: number = 0;
static list: CustomToolbar[] = shallowReactive([]);
static info: Record<string, RegisteredCustomToolInfo> = {};
items: ValueOf<ToolbarItemMap>[] = reactive([]);
num: number = CustomToolbar.num++;
@ -88,6 +98,7 @@ export class CustomToolbar extends EventEmitter<CustomToolbarEvent> {
height: number = 70;
// ----- other
assistKey: number = 0;
showIds: number[] = [];
constructor(id: string) {
super();
@ -100,14 +111,15 @@ export class CustomToolbar extends EventEmitter<CustomToolbarEvent> {
*
* @param item
*/
add<K extends ToolbarItemType>(item: Omit<ToolbarItemMap[K], 'com'>) {
// @ts-ignore
const data: ToolbarItemMap[K] = {
com: markRaw(comMap[item.type]),
...item
} as ToolbarItemMap[K];
this.items.push(data);
this.emit('add', data);
add<K extends ToolbarItemType>(item: ToolbarItemMap[K]) {
const index = this.items.findIndex(v => v.id === item.id);
if (index !== -1) {
console.warn(`添加了id重复的自定义工具已将其覆盖`);
this.items[index] = item;
} else {
this.items.push(item);
}
this.emit('add', item);
return this;
}
@ -148,34 +160,21 @@ export class CustomToolbar extends EventEmitter<CustomToolbarEvent> {
const item = this.items.find(v => v.id === id);
if (!item) return this;
this.emit('emit', id, item);
if (item.type === 'hotkey') {
// 按键
const assist = item.assist | this.assistKey;
const { ctrl, shift, alt } = unwarpBinary(assist);
const ev = new KeyboardEvent('keyup', {
ctrlKey: ctrl,
shiftKey: shift,
altKey: alt
});
// todo: Advanced KeyboardEvent simulate
gameKey.emitKey(item.key, assist, 'up', ev);
} else if (item.type === 'item') {
// 道具
core.tryUseItem(item.item);
} else if (item.type === 'assistKey') {
// 辅助按键
if (item.assist === KeyCode.Ctrl) {
this.assistKey = flipBinary(this.assistKey, 0);
} else if (item.assist === KeyCode.Shift) {
this.assistKey = flipBinary(this.assistKey, 1);
} else if (item.assist === KeyCode.Alt) {
this.assistKey = flipBinary(this.assistKey, 2);
const info = CustomToolbar.info[item.type];
if (!info) {
console.warn(`触发了未知的自定义工具类型:'${item.type}'`);
return this;
}
const success = info.onEmit.call(this, id, item);
if (!success) {
console.warn(`触发自定义工具失败id:'${id}',type:${item.type}`);
}
return this;
}
/**
*
*/
refresh() {
const items = this.items.splice(0);
nextTick(() => {
@ -194,15 +193,135 @@ export class CustomToolbar extends EventEmitter<CustomToolbarEvent> {
has(height) && (this.height = height);
}
/**
*
*/
show() {
fixedUi.open('toolbar', { bar: this });
const id = fixedUi.open('toolbar', { bar: this });
this.showIds.push(id);
return id;
}
/**
*
* @param id id
*/
close(id: number) {
fixedUi.close(id);
deleteWith(this.showIds, id);
}
/**
*
*/
closeAll() {
this.showIds.forEach(v => fixedUi.close(v));
}
static get(id: string) {
return this.list.find(v => v.id === id);
}
/**
*
* @param type
* @param name
* @param onEmit
* @param show
* @param editor
* @param onCreate
*/
static register<K extends ToolbarItemType>(
type: K,
name: string,
onEmit: ToolItemEmitFn<K>,
show: CustomToolbarComponent<K>,
editor: CustomToolbarComponent<K>,
onCreate: (item: any) => ToolbarItemMap[K]
) {
if (type in this.info) {
console.warn(`已存在名为'${type}'的自定义工具类型,已将其覆盖!`);
}
const info: RegisteredCustomToolInfo = {
name,
onEmit: onEmit as ToolItemEmitFn<ToolbarItemType>,
show: show as CustomToolbarComponent,
editor: editor as CustomToolbarComponent,
// @ts-ignore
onCreate
};
this.info[type] = info;
}
}
CustomToolbar.register(
'hotkey',
'快捷键',
function (id, item) {
// 按键
const assist = item.assist | this.assistKey;
const { ctrl, shift, alt } = unwarpBinary(assist);
const ev = new KeyboardEvent('keyup', {
ctrlKey: ctrl,
shiftKey: shift,
altKey: alt
});
// todo: Advanced KeyboardEvent simulate
gameKey.emitKey(item.key, assist, 'up', ev);
return true;
},
COM.KeyTool,
EDITOR.KeyTool,
item => {
return {
key: KeyCode.Unknown,
assist: 0,
...item
};
}
);
CustomToolbar.register(
'item',
'使用道具',
function (id, item) {
// 道具
core.tryUseItem(item.item);
return true;
},
COM.ItemTool,
EDITOR.ItemTool,
item => {
return {
item: 'book',
...item
};
}
);
CustomToolbar.register(
'assistKey',
'辅助按键',
function (id, item) {
// 辅助按键
if (item.assist === KeyCode.Ctrl) {
this.assistKey = flipBinary(this.assistKey, 0);
} else if (item.assist === KeyCode.Shift) {
this.assistKey = flipBinary(this.assistKey, 1);
} else if (item.assist === KeyCode.Alt) {
this.assistKey = flipBinary(this.assistKey, 2);
}
return true;
},
COM.AssistKeyTool,
EDITOR.AssistKeyTool,
item => {
return {
assist: KeyCode.Ctrl,
...item
};
}
);
hook.once('reset', () => {
const toolbar = new CustomToolbar('test');
toolbar

View File

@ -1,10 +1,13 @@
import { KeyCodeUtils } from '@/plugin/keyCodes';
import { KeyCode, KeyCodeUtils } from '@/plugin/keyCodes';
import type {
CustomToolbarComponent,
CustomToolbarProps
} from '../custom/toolbar';
import BoxAnimate from '@/components/boxAnimate.vue';
import { checkAssist } from '../custom/hotkey';
import { getVitualKeyOnce } from '@/plugin/utils';
import { cloneDeep } from 'lodash-es';
import { Select, SelectOption } from 'ant-design-vue';
// todo: 新增更改设置的ToolItem
@ -25,6 +28,16 @@ export function createToolbarComponents() {
return com;
}
export function createToolbarEditorComponents() {
const com: Components = {
DefaultTool: DefaultToolEditor,
KeyTool: KeyToolEdtior,
ItemTool: ItemToolEditor,
AssistKeyTool: AssistKeyToolEditor
};
return com;
}
function DefaultTool(props: CustomToolbarProps) {
return <span></span>;
}
@ -70,3 +83,123 @@ function AssistKeyTool(props: CustomToolbarProps<'assistKey'>) {
</span>
);
}
function DefaultToolEditor(props: CustomToolbarProps) {
return <span></span>;
}
function KeyToolEdtior(props: CustomToolbarProps<'hotkey'>) {
const { item, toolbar } = props;
const getKey = async () => {
const { key, assist } = await getVitualKeyOnce(false, item.assist);
toolbar.set<'hotkey'>(item.id, {
key,
assist
});
};
const unwarpAssist = (assist: number) => {
let res = '';
if (assist & (1 << 0)) {
res += 'Ctrl + ';
}
if (assist & (1 << 1)) {
res += 'Shift + ';
}
if (assist & (1 << 2)) {
res += 'Alt + ';
}
return res;
};
const getKeyShow = (key: KeyCode, assist: number) => {
return (
unwarpAssist(assist) +
(key === KeyCode.Unknown ? '' : KeyCodeUtils.toString(key))
);
};
return (
<div
style="
display: flex; flex-direction: row; justify-content: space-between;
align-items: center; padding: 0 5%; margin: 1%
"
>
<span></span>
<span
style="background-color: #000; width: 50%; text-align: end; padding: 0 5%"
onClick={getKey}
>
{getKeyShow(item.key, item.assist)}
</span>
</div>
);
}
function ItemToolEditor(props: CustomToolbarProps<'item'>) {
const { item, toolbar } = props;
const items = cloneDeep(core.status.hero.items.constants);
Object.assign(items, core.status.hero.items.tools);
return (
<div
style="
display: flex; flex-direction: row; justify-content: space-between;
align-items: center; padding: 0 5%; margin: 1%
"
>
<span>使</span>
<Select
style="width: 180px; font-size: 80%; height: 100%; background-color: #000"
value={item.item}
onChange={value =>
toolbar.set<'item'>(item.id, {
item: value as ItemIdOf<'tools' | 'constants'>
})
}
>
{Object.entries(items).map(v => {
return (
<SelectOption value={v[0]}>
{
core.material.items[v[0] as AllIdsOf<'items'>]
.name
}
</SelectOption>
);
})}
</Select>
</div>
);
}
function AssistKeyToolEditor(props: CustomToolbarProps<'assistKey'>) {
const { item, toolbar } = props;
return (
<div
style="
display: flex; flex-direction: row; justify-content: space-between;
align-items: center; padding: 0 5%; margin: 1%
"
>
<span></span>
<Select
style="width: 180px; font-size: 80%; height: 100%; background-color: #000"
value={item.assist}
onChange={value =>
toolbar.set<'assistKey'>(item.id, {
assist: value as KeyCode.Ctrl
})
}
>
<SelectOption value={KeyCode.Ctrl}>Ctrl</SelectOption>
<SelectOption value={KeyCode.Shift}>Shift</SelectOption>
<SelectOption value={KeyCode.Alt}>Alt</SelectOption>
</Select>
</div>
);
}

View File

@ -356,7 +356,8 @@ export function flipBinary(num: number, col: number) {
*/
export function getVitualKeyOnce(
emitAssist: boolean = false,
assist: number = 0
assist: number = 0,
emittable: KeyCode[] = []
): Promise<KeyboardEmits> {
return new Promise(res => {
const key = Keyboard.get('full')!;
@ -365,11 +366,16 @@ export function getVitualKeyOnce(
key.on('emit', (item, assist, index, ev) => {
ev.preventDefault();
if (emitAssist) {
if (emittable.length === 0 || emittable.includes(item.key)) {
res({ key: item.key, assist: 0 });
key.disposeScope();
mainUi.close(id);
}
} else {
if (!isAssist(item.key)) {
if (
!isAssist(item.key) &&
(emittable.length === 0 || emittable.includes(item.key))
) {
res({ key: item.key, assist });
key.disposeScope();
mainUi.close(id);

View File

@ -1,6 +1,9 @@
<template>
<Column :left="30" @close="exit">
<template #left>
<div id="tools">
<span class="button-text" @click="exit"><left-outlined /> 返回</span>
</div>
<div id="tool-editor">
<Scroll class="tool-list-scroll">
<div id="tool-list">
<div
v-for="(item, i) of list"
@ -12,48 +15,172 @@
<a-button
type="danger"
class="tool-list-delete"
@click.stop="deleteTool"
@click.stop="deleteTool(item.id)"
>删除</a-button
>
</div>
<div id="tool-list-add">
<div id="tool-add-div" @click="addTool">
<div
id="tool-add-div"
@click="addingTool = true"
v-if="!addingTool"
>
<PlusOutlined></PlusOutlined>&nbsp;&nbsp;
<span>新增工具栏</span>
</div>
<div v-else>
<a-input
style="height: 100%; font-size: 80%; width: 100%"
v-model:value="addingToolId"
@blur="addTool"
></a-input>
</div>
</div>
</template>
<template #right>
</div>
</Scroll>
<a-divider
class="divider"
dashed
style="border-color: #ddd4"
:type="isMobile ? 'horizontal' : 'vertical'"
></a-divider>
<div id="tool-info">
<div id="tool-detail"></div>
<div id="tool-detail">
<Scroll class="tool-item-list-scroll">
<div class="tool-item-list">
<div
class="tool-item-list-item"
v-for="item of bar.items"
>
<div
class="tool-item-header"
:folded="!unfolded[item.id]"
@click="triggerFold(item.id)"
>
<div
style="
display: flex;
flex-direction: row;
width: 100%;
"
>
<span
class="tool-fold"
:folded="!unfolded[item.id]"
>
<RightOutlined></RightOutlined>
</span>
<span class="tool-name">
<span
v-if="editId !== item.id"
style="cursor: text"
@click.stop="editName(item.id)"
>
{{ item.id }}
</span>
<span v-else>
<a-input
@blur="editNameSuccess(item.id)"
@click.stop=""
v-model:value="editValue"
class="tool-name-edit"
></a-input>
</span>
</span>
</div>
<a-button
type="danger"
class="tool-item-delete"
@click.stop="deleteItem(item)"
>删除</a-button
>
</div>
<div v-if="!!unfolded[item.id]">
<component
:is="CustomToolbar.info[item.type].editor"
:item="item"
:toolbar="bar"
></component>
</div>
</div>
<div id="tool-item-add">
<div
id="tool-item-add-div"
v-if="!addingItem"
@click="addingItem = true"
>
<PlusOutlined></PlusOutlined>&nbsp;&nbsp;
<span>新增工具</span>
</div>
<div id="tool-item-add-type" v-else>
<span>工具类型</span>
<a-select
v-model:value="addingType"
style="
width: 120px;
height: 100%;
font-size: 80%;
background-color: #222;
"
>
<a-select-option
v-for="(
info, type
) of CustomToolbar.info"
:value="type"
>{{ info.name }}</a-select-option
>
</a-select>
<a-button
type="primary"
style="font-size: 80%; height: 100%"
@click="addItem(true)"
>确定</a-button
>
<a-button
@click="addItem(false)"
style="
background-color: #222;
font-size: 80%;
height: 100%;
"
>取消</a-button
>
</div>
</div>
</div>
</Scroll>
</div>
<a-divider dashed></a-divider>
<div id="tool-preview">
<div id="tool-preview-container">
<div
class="tool-preview-item"
v-for="item of bar.items"
>
<div class="tool-preview-item" v-for="item of bar.items">
<component
:is="(item.com as any)"
:is="(CustomToolbar.info[item.type].show as any)"
:item="item"
:bar="bar"
:toolbar="bar"
></component>
</div>
</div>
</div>
</div>
</template>
</Column>
</div>
</template>
<script lang="ts" setup>
import { CustomToolbar } from '@/core/main/custom/toolbar';
import { CustomToolbar, ToolbarItemType } from '@/core/main/custom/toolbar';
import { GameUi } from '@/core/main/custom/ui';
import Column from '../components/colomn.vue';
import { computed, ref } from 'vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { computed, reactive, ref } from 'vue';
import {
PlusOutlined,
RightOutlined,
LeftOutlined
} from '@ant-design/icons-vue';
import { mainUi } from '@/core/main/init/ui';
import { isMobile } from '@/plugin/use';
import Scroll from '@/components/scroll.vue';
import { deleteWith, tip } from '@/plugin/utils';
import { Modal } from 'ant-design-vue';
const props = defineProps<{
ui: GameUi;
@ -65,25 +192,148 @@ const list = CustomToolbar.list;
const selected = ref(0);
const bar = computed(() => list[selected.value]);
const unfolded = reactive<Record<string, boolean>>({});
const editId = ref<string>();
const editValue = ref<string>();
//
const addingItem = ref(false);
const addingType = ref<ToolbarItemType>('item');
//
const addingTool = ref(false);
const addingToolId = ref('');
/**
* 编辑名称
*/
function editName(id: string) {
editId.value = id;
editValue.value = id;
}
/**
* 编辑名称完成
*/
function editNameSuccess(id: string) {
if (bar.value.items.some(v => v.id === editId.value)) {
tip('error', '名称重复!');
} else {
const item = bar.value.items.find(v => v.id === id)!;
item.id = editValue.value!;
}
editId.value = void 0;
editValue.value = void 0;
}
/**
* 删除自定义工具
*/
function deleteItem(item: any) {
Modal.confirm({
title: '确定要删除这个自定义工具吗?',
onOk() {
deleteWith(bar.value.items, item);
},
onCancel() {}
});
}
function addItem(add: boolean) {
if (add) {
bar.value.add(
// @ts-ignore
CustomToolbar.info[addingType.value].onCreate({
id: `tool-item-${bar.value.items.length}`,
type: addingType.value
})
);
}
addingItem.value = false;
addingType.value = 'item';
}
/**
* 更改折叠
*/
function triggerFold(id: string) {
unfolded[id] = !unfolded[id];
}
function exit() {
mainUi.close(props.num);
}
function deleteTool() {}
function deleteTool(id: string) {
Modal.confirm({
title: '确定要删除这个自定义工具栏吗?',
onOk() {
const index = CustomToolbar.list.findIndex(v => v.id === id);
if (index !== -1) {
CustomToolbar.list[index].closeAll();
CustomToolbar.list.splice(index, 1);
}
selected.value = 0;
},
onCancel() {}
});
}
function addTool() {}
function addTool() {
if (addingToolId.value === '') {
addingToolId.value = '';
addingTool.value = false;
return;
}
if (CustomToolbar.list.some(v => v.id === addingToolId.value)) {
tip('error', '工具栏名称重复!');
return;
} else {
const bar = new CustomToolbar(addingToolId.value);
}
addingToolId.value = '';
addingTool.value = false;
}
</script>
<style lang="less" scoped>
#tools {
width: 100%;
font-family: 'normal';
font-size: 3.2vh;
height: 5vh;
position: fixed;
left: 10vw;
top: 5vh;
}
#tool-editor {
width: 70%;
height: 70%;
display: flex;
justify-content: center;
align-items: center;
font-family: 'normal';
font-size: 150%;
user-select: none;
align-self: center;
}
.tool-list-item {
display: flex;
align-items: center;
justify-content: space-between;
}
.tool-list-scroll {
height: 100%;
width: 30%;
}
#tool-list {
display: flex;
flex-direction: column;
height: 100%;
}
#tool-list-add {
@ -119,22 +369,140 @@ function addTool() {}
#tool-info {
display: flex;
flex-direction: column;
height: 100%;
width: 70%;
}
#tool-detail {
height: 60%;
.tool-item-header {
cursor: pointer;
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
}
.tool-item-header[folded='false'] {
border-bottom: 0.5px solid #888;
}
.tool-item-list-scroll {
height: 100%;
width: 100%;
}
.tool-item-list {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.tool-item-list-item {
border: 1px solid #ddd8;
margin-bottom: 5%;
background-color: #222;
padding-left: 2%;
}
.tool-fold {
::v-deep(span) {
transition: all 0.2s ease;
}
}
.tool-fold[folded='false'] {
::v-deep(span) {
transform: rotate(90deg);
}
}
.tool-name {
margin-left: 3%;
width: 40%;
display: flex;
.tool-name-edit {
width: 100%;
font-size: 100%;
height: 100%;
background-color: #000;
}
}
.tool-item-delete {
height: 100%;
justify-self: end;
font-size: 80%;
padding: 2px 15px;
}
#tool-item-add-div {
padding: 1% 3% 1% 3%;
display: flex;
align-items: center;
flex-direction: row;
cursor: pointer;
padding: 1% 4%;
transition: background-color linear 0.1s;
border-radius: 5px;
text-overflow: ellipsis;
}
#tool-item-add-div:hover {
background-color: rgba(39, 251, 209, 0.316);
}
#tool-item-add-div:active {
background-color: rgba(39, 251, 209, 0.202);
}
#tool-item-add-type {
display: flex;
align-items: center;
flex-direction: row;
padding: 1% 4%;
justify-content: space-between;
border-radius: 5px;
background-color: rgba(39, 251, 209, 0.316);
}
}
#tool-preview {
height: 40%;
display: flex;
justify-content: center;
align-items: center;
align-items: start;
#tool-preview-container {
width: 90%;
display: flex;
flex-direction: row;
border: 2px solid #ddd9;
background-color: #0009;
flex-wrap: wrap;
}
.tool-preview-item {
display: flex;
margin: 5px;
min-width: 50px;
height: 50px;
background-color: #222;
border: 1.5px solid #ddd8;
justify-content: center;
align-items: center;
transition: all 0.1s linear;
::v-deep(*) {
pointer-events: none;
}
}
}
.divider {
height: 100%;
}
</style>

View File

@ -15,7 +15,7 @@
@click.stop="click"
>
<component
:is="(item.com as any)"
:is="(CustomToolbar.info[item.type].show as any)"
:item="item"
:toolbar="bar"
></component>