mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-05-15 14:23:25 +08:00
feat: input 组件
This commit is contained in:
parent
425ac1b684
commit
771e61e6aa
@ -41,6 +41,7 @@
|
|||||||
"@babel/cli": "^7.26.4",
|
"@babel/cli": "^7.26.4",
|
||||||
"@babel/core": "^7.26.10",
|
"@babel/core": "^7.26.10",
|
||||||
"@babel/preset-env": "^7.26.9",
|
"@babel/preset-env": "^7.26.9",
|
||||||
|
"@eslint/js": "^9.24.0",
|
||||||
"@rollup/plugin-babel": "^6.0.4",
|
"@rollup/plugin-babel": "^6.0.4",
|
||||||
"@rollup/plugin-commonjs": "^25.0.8",
|
"@rollup/plugin-commonjs": "^25.0.8",
|
||||||
"@rollup/plugin-json": "^6.1.0",
|
"@rollup/plugin-json": "^6.1.0",
|
||||||
|
521
packages-user/client-modules/src/render/components/input.tsx
Normal file
521
packages-user/client-modules/src/render/components/input.tsx
Normal file
@ -0,0 +1,521 @@
|
|||||||
|
import { DefaultProps } from '@motajs/render-vue';
|
||||||
|
import { computed, defineComponent, onUnmounted, ref, watch } from 'vue';
|
||||||
|
import { TextContent, TextContentProps } from './textbox';
|
||||||
|
import { SetupComponentOptions } from './types';
|
||||||
|
import { RectRCircleParams } from '@motajs/render-elements';
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
ElementLocator,
|
||||||
|
MotaRenderer,
|
||||||
|
RenderItem,
|
||||||
|
Transform
|
||||||
|
} from '@motajs/render-core';
|
||||||
|
import { Font } from '@motajs/render-style';
|
||||||
|
import { transitionedColor } from '../use';
|
||||||
|
import { linear } from 'mutate-animate';
|
||||||
|
import { Background, Selection } from './misc';
|
||||||
|
import { GameUI, IUIMountable } from '@motajs/system-ui';
|
||||||
|
|
||||||
|
export interface InputProps extends DefaultProps, Partial<TextContentProps> {
|
||||||
|
/** 输入框的提示内容 */
|
||||||
|
placeholder?: string;
|
||||||
|
/** 输入框的值 */
|
||||||
|
value?: string;
|
||||||
|
/** 是否是多行输入,多行输入时,允许换行 */
|
||||||
|
multiline?: boolean;
|
||||||
|
/** 边框颜色 */
|
||||||
|
border?: string;
|
||||||
|
/** 边框圆角 */
|
||||||
|
circle?: RectRCircleParams;
|
||||||
|
/** 边框宽度 */
|
||||||
|
borderWidth?: number;
|
||||||
|
/** 内边距 */
|
||||||
|
pad?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InputEmits = {
|
||||||
|
/**
|
||||||
|
* 当输入框的值被确认时触发,例如失焦时
|
||||||
|
* @param value 输入框的值
|
||||||
|
*/
|
||||||
|
change: (value: string) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当输入框的值发生改变时触发,例如输入了一个字符,或者删除了一个字母
|
||||||
|
* @param value 输入框的值
|
||||||
|
*/
|
||||||
|
input: (value: string) => void;
|
||||||
|
|
||||||
|
'update:value': (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputProps = {
|
||||||
|
props: ['placeholder', 'value', 'multiline'],
|
||||||
|
emits: ['change', 'input', 'update:value']
|
||||||
|
} satisfies SetupComponentOptions<InputProps, InputEmits, keyof InputEmits>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入组件,点击后允许编辑。参数参考 {@link InputProps},事件参考 {@link InputEmits}。
|
||||||
|
* 完全继承 TextContent 组件的参数,用于控制输入内容的显示方式。用法示例:
|
||||||
|
* ```tsx
|
||||||
|
* const inputValue = ref('');
|
||||||
|
* <Input
|
||||||
|
* placeholder="提示词"
|
||||||
|
* // 双向数据绑定,当输入内容改变时,inputValue 会同时改变
|
||||||
|
* v-model={inputValue.value}
|
||||||
|
* // 设置为多行模式
|
||||||
|
* multiline
|
||||||
|
* // 边框颜色
|
||||||
|
* border="#eee"
|
||||||
|
* // 圆角参数,与 g-rectr 参数一致
|
||||||
|
* circle={[4]}
|
||||||
|
* // 边框宽度
|
||||||
|
* borderWidth={3}
|
||||||
|
* // 内边距
|
||||||
|
* pad={8}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const Input = defineComponent<InputProps, InputEmits, keyof InputEmits>(
|
||||||
|
(props, { attrs, emit }) => {
|
||||||
|
let ele: HTMLInputElement | HTMLTextAreaElement | null = null;
|
||||||
|
|
||||||
|
const value = ref(props.value ?? '');
|
||||||
|
const root = ref<Container>();
|
||||||
|
|
||||||
|
const width = computed(() => props.loc?.[2] ?? 200);
|
||||||
|
const height = computed(() => props.loc?.[3] ?? 200);
|
||||||
|
const showText = computed(() => value.value || props.placeholder || '');
|
||||||
|
const padding = computed(() => props.pad ?? 4);
|
||||||
|
const textLoc = computed<ElementLocator>(() => [
|
||||||
|
padding.value,
|
||||||
|
padding.value,
|
||||||
|
width.value - padding.value * 2,
|
||||||
|
height.value - padding.value * 2
|
||||||
|
]);
|
||||||
|
|
||||||
|
const borderColor = transitionedColor(
|
||||||
|
props.border ?? '#ddd',
|
||||||
|
200,
|
||||||
|
linear()
|
||||||
|
)!;
|
||||||
|
|
||||||
|
const listenInput = () => {
|
||||||
|
if (!ele) return;
|
||||||
|
ele.addEventListener('input', () => {
|
||||||
|
if (ele) {
|
||||||
|
updateInput(ele.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ele.addEventListener('change', () => {
|
||||||
|
if (ele) {
|
||||||
|
updateValue(ele.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ele.addEventListener('blur', () => {
|
||||||
|
ele?.remove();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createInput = (mul: boolean) => {
|
||||||
|
if (mul) {
|
||||||
|
ele = document.createElement('textarea');
|
||||||
|
} else {
|
||||||
|
ele = document.createElement('input');
|
||||||
|
}
|
||||||
|
// See file://./../../../../../src/styles.less
|
||||||
|
ele.classList.add('motajs-input-element');
|
||||||
|
listenInput();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateValue = (newValue: string) => {
|
||||||
|
value.value = newValue;
|
||||||
|
emit('update:value', newValue);
|
||||||
|
emit('change', newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateInput = (newValue: string) => {
|
||||||
|
value.value = newValue;
|
||||||
|
emit('update:value', newValue);
|
||||||
|
emit('input', newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const click = () => {
|
||||||
|
if (!ele) createInput(props.multiline ?? false);
|
||||||
|
if (!ele) return;
|
||||||
|
// 计算当前绝对位置
|
||||||
|
const renderer = MotaRenderer.get('render-main');
|
||||||
|
const canvas = renderer?.getCanvas();
|
||||||
|
if (!canvas) return;
|
||||||
|
const chain: RenderItem[] = [];
|
||||||
|
let now: RenderItem | undefined = root.value;
|
||||||
|
if (!now) return;
|
||||||
|
while (now) {
|
||||||
|
chain.unshift(now);
|
||||||
|
now = now.parent;
|
||||||
|
}
|
||||||
|
const { clientLeft, clientTop } = canvas;
|
||||||
|
const trans = new Transform();
|
||||||
|
trans.translate(clientLeft, clientTop);
|
||||||
|
trans.scale(core.domStyle.scale);
|
||||||
|
for (const item of chain) {
|
||||||
|
const { anchorX, anchorY, width, height } = item;
|
||||||
|
trans.translate(-anchorX * width, -anchorY * height);
|
||||||
|
trans.multiply(item.transform);
|
||||||
|
}
|
||||||
|
trans.translate(padding.value, padding.value);
|
||||||
|
const [a, b, , c, d, , e, f] = trans.mat;
|
||||||
|
const str = `matrix(${a},${b},${c},${d},${e},${f})`;
|
||||||
|
const w = width.value * core.domStyle.scale;
|
||||||
|
const h = height.value * core.domStyle.scale;
|
||||||
|
const font = props.font ?? Font.defaults();
|
||||||
|
ele.style.transform = str;
|
||||||
|
ele.style.width = `${w - padding.value * 2}px`;
|
||||||
|
ele.style.height = `${h - padding.value * 2}px`;
|
||||||
|
ele.style.font = font.string();
|
||||||
|
ele.style.color = String(props.fillStyle ?? 'white');
|
||||||
|
document.body.appendChild(ele);
|
||||||
|
ele.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const enter = () => {
|
||||||
|
borderColor.set('#0ff');
|
||||||
|
};
|
||||||
|
|
||||||
|
const leave = () => {
|
||||||
|
borderColor.set(props.border ?? '#ddd');
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.value,
|
||||||
|
newValue => {
|
||||||
|
value.value = newValue ?? '';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.multiline,
|
||||||
|
value => {
|
||||||
|
createInput(value ?? false);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
ele?.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<container
|
||||||
|
ref={root}
|
||||||
|
cursor="text"
|
||||||
|
onClick={click}
|
||||||
|
onEnter={enter}
|
||||||
|
onLeave={leave}
|
||||||
|
>
|
||||||
|
<g-rectr
|
||||||
|
loc={[0, 0, width.value, height.value]}
|
||||||
|
circle={props.circle}
|
||||||
|
lineWidth={props.borderWidth}
|
||||||
|
strokeStyle={borderColor.ref.value}
|
||||||
|
zIndex={10}
|
||||||
|
/>
|
||||||
|
<TextContent
|
||||||
|
{...attrs}
|
||||||
|
noevent
|
||||||
|
loc={textLoc.value}
|
||||||
|
width={width.value - padding.value * 2}
|
||||||
|
text={showText.value}
|
||||||
|
alpha={value.value.length === 0 ? 0.6 : 1}
|
||||||
|
zIndex={0}
|
||||||
|
/>
|
||||||
|
</container>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
inputProps
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface InputBoxProps extends TextContentProps {
|
||||||
|
loc: ElementLocator;
|
||||||
|
input?: InputProps;
|
||||||
|
winskin?: ImageIds;
|
||||||
|
color?: CanvasStyle;
|
||||||
|
border?: CanvasStyle;
|
||||||
|
pad?: number;
|
||||||
|
inputHeight?: number;
|
||||||
|
text?: string;
|
||||||
|
yesText?: string;
|
||||||
|
noText?: string;
|
||||||
|
selFont?: Font;
|
||||||
|
selFill?: CanvasStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InputBoxEmits = {
|
||||||
|
/**
|
||||||
|
* 当确认输入框的内容时触发
|
||||||
|
* @param value 输入框的内容
|
||||||
|
*/
|
||||||
|
confirm: (value: string) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当取消时触发
|
||||||
|
* @param value 输入框的内容
|
||||||
|
*/
|
||||||
|
cancel: (value: string) => void;
|
||||||
|
} & InputEmits;
|
||||||
|
|
||||||
|
const inputBoxProps = {
|
||||||
|
props: [
|
||||||
|
'loc',
|
||||||
|
'input',
|
||||||
|
'winskin',
|
||||||
|
'color',
|
||||||
|
'border',
|
||||||
|
'pad',
|
||||||
|
'inputHeight',
|
||||||
|
'text',
|
||||||
|
'yesText',
|
||||||
|
'noText',
|
||||||
|
'selFont',
|
||||||
|
'selFill',
|
||||||
|
'width'
|
||||||
|
],
|
||||||
|
emits: ['confirm', 'cancel', 'change', 'input', 'update:value']
|
||||||
|
} satisfies SetupComponentOptions<
|
||||||
|
InputBoxProps,
|
||||||
|
InputBoxEmits,
|
||||||
|
keyof InputBoxEmits
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入框组件,与 2.x 的 myconfirm 类似,单次调用参考 {@link getInput}。用法与 `ConfirmBox` 类似。
|
||||||
|
* 参数参考 {@link InputBoxProps},事件参考 {@link InputBoxEmits},用例如下:
|
||||||
|
* ```tsx
|
||||||
|
* const onConfirm = (value: string) => console.log(value);
|
||||||
|
*
|
||||||
|
* <ConfirmBox
|
||||||
|
* text="请输入文本:"
|
||||||
|
* width={240}
|
||||||
|
* // 输入框会自动计算宽度和高度,因此不需要手动指定,即使手动指定也无效
|
||||||
|
* loc={[240, 240, void 0, void 0, 0.5, 0.5]}
|
||||||
|
* // 使用 winskin 图片作为背景
|
||||||
|
* winskin="winskin.png"
|
||||||
|
* // 使用颜色作为背景和边框,如果设置了 winskin,那么此参数无效
|
||||||
|
* color="#333"
|
||||||
|
* border="gold"
|
||||||
|
* // 设置选项的字体
|
||||||
|
* selFont={new Font('Verdana', 16)}
|
||||||
|
* // 设置选项的文本颜色
|
||||||
|
* selFill="#d48"
|
||||||
|
* // 完全继承 TextContent 的参数,因此可以填写 font 参数指定文本字体
|
||||||
|
* font={new Font('arial')}
|
||||||
|
* onConfirm={onYes}
|
||||||
|
* // 可以使用 input 参数调整输入组件
|
||||||
|
* input={{
|
||||||
|
* // 例如在输入组件中添加占位符(未输入任何东西时显示此内容)
|
||||||
|
* placeholder: '在这里输入你的文本'
|
||||||
|
* }}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const InputBox = defineComponent<
|
||||||
|
InputBoxProps,
|
||||||
|
InputBoxEmits,
|
||||||
|
keyof InputBoxEmits
|
||||||
|
>((props, { attrs, emit }) => {
|
||||||
|
const contentHeight = ref(0);
|
||||||
|
const value = ref('');
|
||||||
|
const selected = ref(false);
|
||||||
|
const yesSize = ref<[number, number]>([0, 0]);
|
||||||
|
const noSize = ref<[number, number]>([0, 0]);
|
||||||
|
const height = ref(200);
|
||||||
|
|
||||||
|
const yesText = computed(() => props.yesText ?? '确认');
|
||||||
|
const noText = computed(() => props.noText ?? '取消');
|
||||||
|
const text = computed(() => props.text ?? '请输入内容:');
|
||||||
|
const padding = computed(() => props.pad ?? 8);
|
||||||
|
const inputHeight = computed(() => props.inputHeight ?? 16);
|
||||||
|
const inputLoc = computed<ElementLocator>(() => [
|
||||||
|
padding.value,
|
||||||
|
padding.value * 2 + contentHeight.value,
|
||||||
|
props.width - padding.value * 2,
|
||||||
|
inputHeight.value - padding.value * 2
|
||||||
|
]);
|
||||||
|
const yesLoc = computed<ElementLocator>(() => {
|
||||||
|
const y = height.value - padding.value;
|
||||||
|
return [props.width / 3, y, void 0, void 0, 0.5, 1];
|
||||||
|
});
|
||||||
|
const noLoc = computed<ElementLocator>(() => {
|
||||||
|
const y = height.value - padding.value;
|
||||||
|
return [(props.width / 3) * 2, y, void 0, void 0, 0.5, 1];
|
||||||
|
});
|
||||||
|
const selectionLoc = computed<ElementLocator>(() => {
|
||||||
|
if (selected.value) {
|
||||||
|
const [x = 0, y = 0] = yesLoc.value;
|
||||||
|
const [width, height] = yesSize.value;
|
||||||
|
return [x, y + 4, width + 8, height + 8, 0.5, 1];
|
||||||
|
} else {
|
||||||
|
const [x = 0, y = 0] = noLoc.value;
|
||||||
|
const [width, height] = noSize.value;
|
||||||
|
return [x, y + 4, width + 8, height + 8, 0.5, 1];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateHeight = (h: number) => {
|
||||||
|
contentHeight.value = h;
|
||||||
|
height.value = h + inputHeight.value + padding.value * 4;
|
||||||
|
};
|
||||||
|
|
||||||
|
const change = (value: string) => {
|
||||||
|
emit('change', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const input = (value: string) => {
|
||||||
|
emit('update:value', value);
|
||||||
|
emit('input', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setYes = (_: string, width: number, height: number) => {
|
||||||
|
yesSize.value = [width, height];
|
||||||
|
};
|
||||||
|
|
||||||
|
const setNo = (_: string, width: number, height: number) => {
|
||||||
|
noSize.value = [width, height];
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirm = () => {
|
||||||
|
emit('confirm', value.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
emit('cancel', value.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<container>
|
||||||
|
<Background
|
||||||
|
loc={[0, 0, props.width, height.value]}
|
||||||
|
winskin={props.winskin}
|
||||||
|
color={props.color}
|
||||||
|
border={props.border}
|
||||||
|
zIndex={0}
|
||||||
|
/>
|
||||||
|
<TextContent
|
||||||
|
{...attrs}
|
||||||
|
loc={[padding.value, padding.value]}
|
||||||
|
text={text.value}
|
||||||
|
width={props.width - padding.value * 2}
|
||||||
|
zIndex={5}
|
||||||
|
onUpdateHeight={updateHeight}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...(props.input ?? {})}
|
||||||
|
loc={inputLoc.value}
|
||||||
|
v-model={value.value}
|
||||||
|
zIndex={10}
|
||||||
|
onChange={change}
|
||||||
|
onInput={input}
|
||||||
|
/>
|
||||||
|
<Selection
|
||||||
|
loc={selectionLoc.value}
|
||||||
|
winskin={props.winskin}
|
||||||
|
border={props.border}
|
||||||
|
noevent
|
||||||
|
zIndex={10}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
loc={yesLoc.value}
|
||||||
|
text={yesText.value}
|
||||||
|
fillStyle={props.selFill}
|
||||||
|
font={props.selFont}
|
||||||
|
cursor="pointer"
|
||||||
|
zIndex={15}
|
||||||
|
onClick={confirm}
|
||||||
|
onEnter={() => (selected.value = true)}
|
||||||
|
onSetText={setYes}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
loc={noLoc.value}
|
||||||
|
text={noText.value}
|
||||||
|
fillStyle={props.selFill}
|
||||||
|
font={props.selFont}
|
||||||
|
cursor="pointer"
|
||||||
|
zIndex={15}
|
||||||
|
onClick={cancel}
|
||||||
|
onEnter={() => (selected.value = false)}
|
||||||
|
onSetText={setNo}
|
||||||
|
/>
|
||||||
|
</container>
|
||||||
|
);
|
||||||
|
}, inputBoxProps);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹出一个输入框,然后将结果返回:
|
||||||
|
* ```ts
|
||||||
|
* const value = await getInput(
|
||||||
|
* // 在哪个 UI 控制器上打开,对于一般 UI 组件来说,直接填写 props.controller 即可
|
||||||
|
* props.controller,
|
||||||
|
* // 提示内容
|
||||||
|
* '请输入文本:',
|
||||||
|
* // 输入框的位置,宽度由下一个参数指定,高度参数由组件内部计算得出,指定无效
|
||||||
|
* [240, 240, void 0, void 0, 0.5, 0.5],
|
||||||
|
* // 宽度设为 240
|
||||||
|
* 240,
|
||||||
|
* // 可以给选择框传入其他的 props,例如指定字体,此项可选
|
||||||
|
* { font: new Font('Verdana', 20) }
|
||||||
|
* );
|
||||||
|
* // 之后,就可以根据 value 来执行不同的操作了
|
||||||
|
* console.log(value); // 输出用户输入的内容
|
||||||
|
* ```
|
||||||
|
* @param controller UI 控制器
|
||||||
|
* @param text 确认文本内容
|
||||||
|
* @param loc 确认框的位置
|
||||||
|
* @param width 确认框的宽度
|
||||||
|
* @param props 额外的 props,参考 {@link ConfirmBoxProps}
|
||||||
|
*/
|
||||||
|
export function getInput(
|
||||||
|
controller: IUIMountable,
|
||||||
|
text: string,
|
||||||
|
loc: ElementLocator,
|
||||||
|
width: number,
|
||||||
|
props?: InputBoxProps
|
||||||
|
) {
|
||||||
|
return new Promise<string>(res => {
|
||||||
|
const instance = controller.open(
|
||||||
|
InputBoxUI,
|
||||||
|
{
|
||||||
|
...(props ?? {}),
|
||||||
|
text,
|
||||||
|
loc,
|
||||||
|
width,
|
||||||
|
onConfirm: value => {
|
||||||
|
controller.close(instance);
|
||||||
|
res(value);
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
controller.close(instance);
|
||||||
|
res('');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 与 `getInput` 类似,不过会将结果转为数字。用法参考 {@link getInput}
|
||||||
|
*/
|
||||||
|
export async function getInputNumber(
|
||||||
|
controller: IUIMountable,
|
||||||
|
text: string,
|
||||||
|
loc: ElementLocator,
|
||||||
|
width: number,
|
||||||
|
props?: InputBoxProps
|
||||||
|
) {
|
||||||
|
const value = await getInput(controller, text, loc, width, props);
|
||||||
|
return parseFloat(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InputBoxUI = new GameUI('input-box', InputBox);
|
@ -8,7 +8,7 @@ import {
|
|||||||
import { computed, defineComponent, ref, SetupContext, watch } from 'vue';
|
import { computed, defineComponent, ref, SetupContext, watch } from 'vue';
|
||||||
import { SetupComponentOptions } from './types';
|
import { SetupComponentOptions } from './types';
|
||||||
import { MotaOffscreenCanvas2D } from '@motajs/render';
|
import { MotaOffscreenCanvas2D } from '@motajs/render';
|
||||||
import { TextboxProps, TextContent, TextContentProps } from './textbox';
|
import { TextContent, TextContentProps } from './textbox';
|
||||||
import { Scroll, ScrollExpose, ScrollProps } from './scroll';
|
import { Scroll, ScrollExpose, ScrollProps } from './scroll';
|
||||||
import { transitioned } from '../use';
|
import { transitioned } from '../use';
|
||||||
import { hyper } from 'mutate-animate';
|
import { hyper } from 'mutate-animate';
|
||||||
@ -457,6 +457,8 @@ const waitBoxProps = {
|
|||||||
* winskin="winskin2.png"
|
* winskin="winskin2.png"
|
||||||
* // 完全继承 TextContent 的参数,因此可以直接指定字体
|
* // 完全继承 TextContent 的参数,因此可以直接指定字体
|
||||||
* font={new Font('Verdana', 28)}
|
* font={new Font('Verdana', 28)}
|
||||||
|
* // 当传入的 Promise 兑现时触发此事件,注意此事件只可能触发一次,触发后便不会再次触发
|
||||||
|
* onResolve={(time) => console.log(time)}
|
||||||
* />
|
* />
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
@ -14,7 +14,8 @@ import { mainUi } from '@motajs/legacy-ui';
|
|||||||
import { gameKey } from '@motajs/system-action';
|
import { gameKey } from '@motajs/system-action';
|
||||||
import { generateKeyboardEvent } from '@motajs/system-action';
|
import { generateKeyboardEvent } from '@motajs/system-action';
|
||||||
import { getVitualKeyOnce } from '@motajs/legacy-ui';
|
import { getVitualKeyOnce } from '@motajs/legacy-ui';
|
||||||
import { getAllSavesData, getSaveData } from '../../utils';
|
import { getAllSavesData, getSaveData, syncFromServer } from '../../utils';
|
||||||
|
import { getInput } from '../components/input';
|
||||||
|
|
||||||
export interface SettingsProps extends Partial<ChoicesProps>, UIComponentProps {
|
export interface SettingsProps extends Partial<ChoicesProps>, UIComponentProps {
|
||||||
loc: ElementLocator;
|
loc: ElementLocator;
|
||||||
@ -294,14 +295,20 @@ export const SyncSave = defineComponent<SettingsProps>(props => {
|
|||||||
[SyncSaveChoice.Back, '返回上一级']
|
[SyncSaveChoice.Back, '返回上一级']
|
||||||
];
|
];
|
||||||
|
|
||||||
const choose = (key: ChoiceKey) => {
|
const choose = async (key: ChoiceKey) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case SyncSaveChoice.ToServer: {
|
case SyncSaveChoice.ToServer: {
|
||||||
props.controller.open(SyncSaveSelectUI, { loc: props.loc });
|
props.controller.open(SyncSaveSelectUI, { loc: props.loc });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SyncSaveChoice.FromServer: {
|
case SyncSaveChoice.FromServer: {
|
||||||
// todo
|
const replay = await getInput(
|
||||||
|
props.controller,
|
||||||
|
'请输入存档编号+密码',
|
||||||
|
[240, 240, void 0, void 0, 0.5, 0.5],
|
||||||
|
240
|
||||||
|
);
|
||||||
|
await syncFromServer(props.controller, replay);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SyncSaveChoice.ToLocal: {
|
case SyncSaveChoice.ToLocal: {
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
import { compressToBase64, decompressFromBase64 } from 'lz-string';
|
||||||
|
import { getConfirm, waitbox } from '../render';
|
||||||
|
import { IUIMountable } from '@motajs/system-ui';
|
||||||
|
import { SyncSaveFromServerResponse } from '@motajs/client-base';
|
||||||
|
|
||||||
export function getAllSavesData() {
|
export function getAllSavesData() {
|
||||||
return new Promise<string>(res => {
|
return new Promise<string>(res => {
|
||||||
core.getAllSaves(saves => {
|
core.getAllSaves(saves => {
|
||||||
@ -10,8 +15,7 @@ export function getAllSavesData() {
|
|||||||
version: core.firstData.version,
|
version: core.firstData.version,
|
||||||
data: saves
|
data: saves
|
||||||
};
|
};
|
||||||
// @ts-expect-error 暂时无法推导
|
res(compressToBase64(JSON.stringify(content)));
|
||||||
res(LZString.compressToBase64(JSON.stringify(content)));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -28,8 +32,140 @@ export function getSaveData(index: number) {
|
|||||||
version: core.firstData.version,
|
version: core.firstData.version,
|
||||||
data: data
|
data: data
|
||||||
};
|
};
|
||||||
// @ts-expect-error 暂时无法推导
|
res(compressToBase64(JSON.stringify(content)));
|
||||||
res(LZString.compressToBase64(JSON.stringify(content)));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#region 服务器加载
|
||||||
|
|
||||||
|
const enum FromServerResponse {
|
||||||
|
Success,
|
||||||
|
ErrorCannotParse,
|
||||||
|
ErrorCannotSync,
|
||||||
|
ErrorUnexpectedCode
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseIdPassword(id: string): [string, string] {
|
||||||
|
if (id.length === 7) return [id.slice(0, 4), id.slice(4)];
|
||||||
|
else return [id.slice(0, 6), id.slice(6)];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function parseResponse(response: SyncSaveFromServerResponse) {
|
||||||
|
let msg: Save | Save[] | null = null;
|
||||||
|
try {
|
||||||
|
msg = JSON.parse(decompressFromBase64(response.msg));
|
||||||
|
} catch {
|
||||||
|
// 无视报错
|
||||||
|
}
|
||||||
|
if (!msg) {
|
||||||
|
try {
|
||||||
|
msg = JSON.parse(response.msg);
|
||||||
|
} catch {
|
||||||
|
// 无视报错
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (msg) {
|
||||||
|
return msg;
|
||||||
|
} else {
|
||||||
|
return FromServerResponse.ErrorCannotParse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncLoad(id: string, password: string) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('type', 'load');
|
||||||
|
formData.append('name', core.firstData.name);
|
||||||
|
formData.append('id', id);
|
||||||
|
formData.append('password', password);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/games/sync.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = (await response.json()) as SyncSaveFromServerResponse;
|
||||||
|
if (data.code === 0) {
|
||||||
|
return parseResponse(data);
|
||||||
|
} else {
|
||||||
|
return FromServerResponse.ErrorUnexpectedCode;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return FromServerResponse.ErrorCannotSync;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncFromServer(
|
||||||
|
controller: IUIMountable,
|
||||||
|
identifier: string
|
||||||
|
): Promise<void> {
|
||||||
|
if (!/^\d{6}\w{4}$/.test(identifier) && !/^\d{4}\w{3}$/.test(identifier)) {
|
||||||
|
return void getConfirm(
|
||||||
|
controller,
|
||||||
|
'不合法的存档编号+密码!请检查格式!',
|
||||||
|
[240, 240, void 0, void 0, 0.5, 0.5],
|
||||||
|
240
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const [id, password] = parseIdPassword(identifier);
|
||||||
|
const result = await waitbox(
|
||||||
|
controller,
|
||||||
|
[240, 240, void 0, void 0, 0.5, 0.5],
|
||||||
|
240,
|
||||||
|
syncLoad(id, password)
|
||||||
|
);
|
||||||
|
if (typeof result === 'number') {
|
||||||
|
const map = {
|
||||||
|
[FromServerResponse.ErrorCannotParse]: '出错啦!\n存档解析失败',
|
||||||
|
[FromServerResponse.ErrorCannotSync]:
|
||||||
|
'出错啦!\n无法从服务器同步存档。',
|
||||||
|
[FromServerResponse.ErrorUnexpectedCode]:
|
||||||
|
'出错啦!\n无法从服务器同步存档。'
|
||||||
|
};
|
||||||
|
return void getConfirm(
|
||||||
|
controller,
|
||||||
|
map[result],
|
||||||
|
[240, 240, void 0, void 0, 0.5, 0.5],
|
||||||
|
240
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (result instanceof Array) {
|
||||||
|
const confirm = await getConfirm(
|
||||||
|
controller,
|
||||||
|
'所有本地存档都将被覆盖,确认?',
|
||||||
|
[240, 240, void 0, void 0, 0.5, 0.5],
|
||||||
|
240,
|
||||||
|
{
|
||||||
|
defaultYes: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (confirm) {
|
||||||
|
const max = 5 * (main.savePages || 30);
|
||||||
|
for (let i = 1; i <= max; i++) {
|
||||||
|
if (i <= result.length) {
|
||||||
|
core.setLocalForage('save' + i, result[i - 1]);
|
||||||
|
} else if (core.saves.ids[i]) {
|
||||||
|
core.removeLocalForage('save' + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return void getConfirm(
|
||||||
|
controller,
|
||||||
|
'同步成功!\n你的本地所有存档均已被覆盖。',
|
||||||
|
[240, 240, void 0, void 0, 0.5, 0.5],
|
||||||
|
240
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const idx = core.saves.saveIndex;
|
||||||
|
await new Promise<void>(res => {
|
||||||
|
core.setLocalForage(`save${idx}`, result, res);
|
||||||
|
});
|
||||||
|
return void getConfirm(
|
||||||
|
controller,
|
||||||
|
`同步成功!\n单存档已覆盖至存档 ${idx}`,
|
||||||
|
[240, 240, void 0, void 0, 0.5, 0.5],
|
||||||
|
240
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { getCurrentInstance, onUnmounted } from 'vue';
|
import { onUnmounted } from 'vue';
|
||||||
import { WeatherController } from '../weather';
|
import { WeatherController } from '../weather';
|
||||||
|
|
||||||
let weatherId = 0;
|
let weatherId = 0;
|
||||||
|
|
||||||
export function useWeather(): [WeatherController] {
|
export function useWeather(): [WeatherController] {
|
||||||
const weather = new WeatherController(`@weather-${weatherId}`);
|
const weather = new WeatherController(`@weather-${weatherId}`);
|
||||||
|
weatherId++;
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
weather.destroy();
|
weather.destroy();
|
||||||
|
@ -2,3 +2,7 @@ export interface ResponseBase {
|
|||||||
code: number;
|
code: number;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SyncSaveFromServerResponse extends ResponseBase {
|
||||||
|
msg: string;
|
||||||
|
}
|
||||||
|
@ -69,7 +69,7 @@ export class Font implements IFontConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getFallbackFont(used: Set<Font>) {
|
private getFallbackFont(used: Set<Font>) {
|
||||||
let font = '';
|
let font = this.build();
|
||||||
this.fallbacks.forEach(v => {
|
this.fallbacks.forEach(v => {
|
||||||
if (used.has(v)) {
|
if (used.has(v)) {
|
||||||
logger.warn(62, this.build());
|
logger.warn(62, this.build());
|
||||||
@ -86,7 +86,7 @@ export class Font implements IFontConfig {
|
|||||||
return this.build();
|
return this.build();
|
||||||
} else {
|
} else {
|
||||||
const usedFont = new Set<Font>();
|
const usedFont = new Set<Font>();
|
||||||
return this.build() + this.getFallbackFont(usedFont);
|
return this.getFallbackFont(usedFont);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,11 +23,11 @@ import {
|
|||||||
import { UIContainer } from './container';
|
import { UIContainer } from './container';
|
||||||
|
|
||||||
export const enum UIMode {
|
export const enum UIMode {
|
||||||
/** 仅显示最后一个 UI,在关闭时,只会关闭指定的 UI */
|
/** 仅显示最后一个非 alwaysShow 的 UI,在关闭时,只会关闭指定的 UI */
|
||||||
LastOnly,
|
LastOnly,
|
||||||
/** 显示所有非手动隐藏的 UI,在关闭时,只会关闭指定 UI */
|
/** 显示所有非手动隐藏的 UI,在关闭时,只会关闭指定 UI */
|
||||||
All,
|
All,
|
||||||
/** 仅显示最后一个 UI,在关闭时,在此之后的所有 UI 会全部关闭 */
|
/** 仅显示最后一个非 alwaysShow 的 UI,在关闭时,在此之后的所有 UI 会全部关闭 */
|
||||||
LastOnlyStack,
|
LastOnlyStack,
|
||||||
/** 显示所有非手动隐藏的 UI,在关闭时,在此之后的所有 UI 会全部关闭 */
|
/** 显示所有非手动隐藏的 UI,在关闭时,在此之后的所有 UI 会全部关闭 */
|
||||||
AllStack,
|
AllStack,
|
||||||
@ -181,8 +181,9 @@ export class UIController
|
|||||||
switch (this.mode) {
|
switch (this.mode) {
|
||||||
case UIMode.LastOnly:
|
case UIMode.LastOnly:
|
||||||
case UIMode.LastOnlyStack:
|
case UIMode.LastOnlyStack:
|
||||||
this.stack.forEach(v => v.hide());
|
|
||||||
this.stack.push(ins);
|
this.stack.push(ins);
|
||||||
|
this.stack.forEach(v => v.hide());
|
||||||
|
this.stack.findLast(v => !v.alwaysShow)?.show();
|
||||||
break;
|
break;
|
||||||
case UIMode.All:
|
case UIMode.All:
|
||||||
case UIMode.AllStack:
|
case UIMode.AllStack:
|
||||||
@ -204,17 +205,13 @@ export class UIController
|
|||||||
case UIMode.LastOnly: {
|
case UIMode.LastOnly: {
|
||||||
this.stack.splice(index, 1);
|
this.stack.splice(index, 1);
|
||||||
this.stack.forEach(v => v.hide());
|
this.stack.forEach(v => v.hide());
|
||||||
const last = this.stack.at(-1);
|
this.stack.findLast(v => !v.alwaysShow)?.show();
|
||||||
if (!last) break;
|
|
||||||
last.show();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case UIMode.LastOnlyStack: {
|
case UIMode.LastOnlyStack: {
|
||||||
this.stack.splice(index);
|
this.stack.splice(index);
|
||||||
this.stack.forEach(v => v.hide());
|
this.stack.forEach(v => v.hide());
|
||||||
const last = this.stack[index - 1];
|
this.stack.findLast(v => !v.alwaysShow)?.show();
|
||||||
if (!last) break;
|
|
||||||
last.show();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case UIMode.All: {
|
case UIMode.All: {
|
||||||
|
@ -69,6 +69,9 @@ importers:
|
|||||||
'@babel/preset-env':
|
'@babel/preset-env':
|
||||||
specifier: ^7.26.9
|
specifier: ^7.26.9
|
||||||
version: 7.26.9(@babel/core@7.26.10)
|
version: 7.26.9(@babel/core@7.26.10)
|
||||||
|
'@eslint/js':
|
||||||
|
specifier: ^9.24.0
|
||||||
|
version: 9.24.0
|
||||||
'@rollup/plugin-babel':
|
'@rollup/plugin-babel':
|
||||||
specifier: ^6.0.4
|
specifier: ^6.0.4
|
||||||
version: 6.0.4(@babel/core@7.26.10)(@types/babel__core@7.20.5)(rollup@3.29.5)
|
version: 6.0.4(@babel/core@7.26.10)(@types/babel__core@7.20.5)(rollup@3.29.5)
|
||||||
@ -1750,6 +1753,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==}
|
resolution: {integrity: sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
'@eslint/js@9.24.0':
|
||||||
|
resolution: {integrity: sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@eslint/object-schema@2.1.6':
|
'@eslint/object-schema@2.1.6':
|
||||||
resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
|
resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@ -7059,6 +7066,8 @@ snapshots:
|
|||||||
|
|
||||||
'@eslint/js@9.22.0': {}
|
'@eslint/js@9.22.0': {}
|
||||||
|
|
||||||
|
'@eslint/js@9.24.0': {}
|
||||||
|
|
||||||
'@eslint/object-schema@2.1.6': {}
|
'@eslint/object-schema@2.1.6': {}
|
||||||
|
|
||||||
'@eslint/plugin-kit@0.2.7':
|
'@eslint/plugin-kit@0.2.7':
|
||||||
|
@ -120,3 +120,12 @@ div.toolbar-editor-item {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.motajs-input-element {
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
position: fixed;
|
||||||
|
border: none;
|
||||||
|
z-index: 1000;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user