mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-04-18 17:48:52 +08:00
feat: Choice 选项框
This commit is contained in:
parent
ee5b962743
commit
109e77d6a2
@ -126,7 +126,8 @@ export class Text extends RenderItem<ETextEvent> {
|
|||||||
this.length = width;
|
this.length = width;
|
||||||
this.descent = actualBoundingBoxAscent;
|
this.descent = actualBoundingBoxAscent;
|
||||||
const height = actualBoundingBoxAscent + actualBoundingBoxDescent;
|
const height = actualBoundingBoxAscent + actualBoundingBoxDescent;
|
||||||
this.size(width, height + this.strokeWidth * 2 + SAFE_PAD * 2);
|
const stroke = this.strokeWidth * 2;
|
||||||
|
this.size(width + stroke, height + stroke + SAFE_PAD * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected handleProps(
|
protected handleProps(
|
||||||
|
@ -562,13 +562,17 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private toTagString(item: RenderItem, space: number, deep: number): string {
|
private toTagString(item: RenderItem, space: number, deep: number): string {
|
||||||
|
if (item.isComment) return '';
|
||||||
const name = item.constructor.name;
|
const name = item.constructor.name;
|
||||||
if (item.children.size === 0) {
|
if (item.children.size === 0) {
|
||||||
return `${' '.repeat(deep * space)}<${name} id="${item.id}" uid="${item.uid}" type="${item.type}"${item.hidden ? ' hidden' : ''}></${name}>\n`;
|
return `${' '.repeat(deep * space)}<${name} ${item.id ? `id="${item.id}" ` : ''}uid="${item.uid}"${item.hidden ? ' hidden' : ''} />\n`;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
`${' '.repeat(deep * space)}<${name} id="${item.id}" uid="${item.uid}" type="${item.type}" ${item.hidden ? 'hidden' : ''}>\n` +
|
`${' '.repeat(deep * space)}<${name} ${item.id ? `${item.id} ` : ''}uid="${item.uid}" ${item.hidden ? 'hidden' : ''}>\n` +
|
||||||
`${[...item.children].map(v => this.toTagString(v, space, deep + 1)).join('')}` +
|
`${[...item.children]
|
||||||
|
.filter(v => !v.isComment)
|
||||||
|
.map(v => this.toTagString(v, space, deep + 1))
|
||||||
|
.join('')}` +
|
||||||
`${' '.repeat(deep * space)}</${name}>\n`
|
`${' '.repeat(deep * space)}</${name}>\n`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -33,16 +33,22 @@ type _FontStretch =
|
|||||||
type _FontVariant = 'normal' | 'small-caps';
|
type _FontVariant = 'normal' | 'small-caps';
|
||||||
|
|
||||||
export class Font implements IFontConfig {
|
export class Font implements IFontConfig {
|
||||||
|
static defaultFamily: string = 'Verdana';
|
||||||
|
static defaultSize: number = 16;
|
||||||
|
static defaultSizeUnit: string = 'px';
|
||||||
|
static defaultWeight: number = 400;
|
||||||
|
static defaultItalic: boolean = false;
|
||||||
|
|
||||||
private readonly fallbacks: Font[] = [];
|
private readonly fallbacks: Font[] = [];
|
||||||
|
|
||||||
private fontString: string = '';
|
private fontString: string = '';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly family: string = 'Verdana',
|
public readonly family: string = Font.defaultFamily,
|
||||||
public readonly size: number = 16,
|
public readonly size: number = Font.defaultSize,
|
||||||
public readonly sizeUnit: string = 'px',
|
public readonly sizeUnit: string = Font.defaultSizeUnit,
|
||||||
public readonly weight: number = 400,
|
public readonly weight: number = Font.defaultWeight,
|
||||||
public readonly italic: boolean = false
|
public readonly italic: boolean = Font.defaultItalic
|
||||||
) {
|
) {
|
||||||
this.fontString = this.getFont();
|
this.fontString = this.getFont();
|
||||||
}
|
}
|
||||||
@ -93,11 +99,11 @@ export class Font implements IFontConfig {
|
|||||||
|
|
||||||
private static parseOne(str: string) {
|
private static parseOne(str: string) {
|
||||||
if (!str) return new Font();
|
if (!str) return new Font();
|
||||||
let italic = false;
|
let italic = this.defaultItalic;
|
||||||
let weight = 400;
|
let weight = this.defaultWeight;
|
||||||
let size = 16;
|
let size = this.defaultSize;
|
||||||
let unit = 'px';
|
let unit = this.defaultSizeUnit;
|
||||||
let family = 'Verdana';
|
let family = this.defaultFamily;
|
||||||
const tokens = str.split(/\s+/);
|
const tokens = str.split(/\s+/);
|
||||||
tokens.forEach(v => {
|
tokens.forEach(v => {
|
||||||
// font-italic
|
// font-italic
|
||||||
@ -136,6 +142,24 @@ export class Font implements IFontConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置默认字体
|
||||||
|
*/
|
||||||
|
static setDefaults(font: Font) {
|
||||||
|
this.defaultFamily = font.family;
|
||||||
|
this.defaultItalic = font.italic;
|
||||||
|
this.defaultSize = font.size;
|
||||||
|
this.defaultSizeUnit = font.sizeUnit;
|
||||||
|
this.defaultWeight = font.weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认字体
|
||||||
|
*/
|
||||||
|
static defaults() {
|
||||||
|
return new Font();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 复制一个字体,同时修改字体的一部分属性
|
* 复制一个字体,同时修改字体的一部分属性
|
||||||
* @param font 要复制的字体
|
* @param font 要复制的字体
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { DefaultProps, ElementLocator, useKey } from '@/core/render';
|
import { DefaultProps, ElementLocator, Font, useKey } from '@/core/render';
|
||||||
import { computed, defineComponent, ref } from 'vue';
|
import { computed, defineComponent, reactive, ref } from 'vue';
|
||||||
import { Background, Selection } from './misc';
|
import { Background, Selection } from './misc';
|
||||||
import { TextContent, TextContentExpose, TextContentProps } from './textbox';
|
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';
|
||||||
|
|
||||||
export interface ConfirmBoxProps extends DefaultProps, TextContentProps {
|
export interface ConfirmBoxProps extends DefaultProps, TextContentProps {
|
||||||
text: string;
|
text: string;
|
||||||
width: number;
|
width: number;
|
||||||
loc: ElementLocator;
|
loc: ElementLocator;
|
||||||
selFont?: string;
|
selFont?: Font;
|
||||||
selFill?: CanvasStyle;
|
selFill?: CanvasStyle;
|
||||||
pad?: number;
|
pad?: number;
|
||||||
yesText?: string;
|
yesText?: string;
|
||||||
@ -47,6 +48,34 @@ const confirmBoxProps = {
|
|||||||
keyof ConfirmBoxEmits
|
keyof ConfirmBoxEmits
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认框组件,与 2.x 的 drawConfirm 类似,可以键盘操作,
|
||||||
|
* 参数参考 {@link ConfirmBoxProps},事件参考 {@link ConfirmBoxEmits},用例如下:
|
||||||
|
* ```tsx
|
||||||
|
* const onYes = () => console.log('yes');
|
||||||
|
* const onNo = () => console.log('no');
|
||||||
|
*
|
||||||
|
* <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="16px Verdana"
|
||||||
|
* // 设置选项的文本颜色
|
||||||
|
* selFill="#d48"
|
||||||
|
* // 完全继承 TextContent 的参数,因此可以填写 fontFamily 参数指定文本字体
|
||||||
|
* fontFamily="Arial"
|
||||||
|
* onYes={onYes}
|
||||||
|
* onNo={onNo}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
export const ConfirmBox = defineComponent<
|
export const ConfirmBox = defineComponent<
|
||||||
ConfirmBoxProps,
|
ConfirmBoxProps,
|
||||||
ConfirmBoxEmits,
|
ConfirmBoxEmits,
|
||||||
@ -132,7 +161,6 @@ export const ConfirmBox = defineComponent<
|
|||||||
<Selection
|
<Selection
|
||||||
loc={selectLoc.value}
|
loc={selectLoc.value}
|
||||||
winskin={props.winskin}
|
winskin={props.winskin}
|
||||||
color={props.color}
|
|
||||||
border={props.border}
|
border={props.border}
|
||||||
noevent
|
noevent
|
||||||
zIndex={10}
|
zIndex={10}
|
||||||
@ -162,3 +190,305 @@ export const ConfirmBox = defineComponent<
|
|||||||
</container>
|
</container>
|
||||||
);
|
);
|
||||||
}, confirmBoxProps);
|
}, confirmBoxProps);
|
||||||
|
|
||||||
|
export interface ChoicesProps extends DefaultProps, TextContentProps {
|
||||||
|
choices: [key: string | number | symbol, text: string][];
|
||||||
|
loc: ElementLocator;
|
||||||
|
width: number;
|
||||||
|
maxHeight?: number;
|
||||||
|
text?: string;
|
||||||
|
title?: string;
|
||||||
|
winskin?: ImageIds;
|
||||||
|
color?: CanvasStyle;
|
||||||
|
border?: CanvasStyle;
|
||||||
|
selFont?: Font;
|
||||||
|
selFill?: CanvasStyle;
|
||||||
|
titleFont?: Font;
|
||||||
|
titleFill?: CanvasStyle;
|
||||||
|
pad?: number;
|
||||||
|
defaultChoice?: string | number | symbol;
|
||||||
|
interval?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChoicesEmits = {
|
||||||
|
choice: (key: string | number | symbol) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const choicesProps = {
|
||||||
|
props: [
|
||||||
|
'choices',
|
||||||
|
'loc',
|
||||||
|
'width',
|
||||||
|
'maxHeight',
|
||||||
|
'text',
|
||||||
|
'title',
|
||||||
|
'winskin',
|
||||||
|
'color',
|
||||||
|
'border',
|
||||||
|
'selFont',
|
||||||
|
'selFill',
|
||||||
|
'titleFont',
|
||||||
|
'titleFill',
|
||||||
|
'pad',
|
||||||
|
'defaultChoice',
|
||||||
|
'interval'
|
||||||
|
],
|
||||||
|
emits: ['choice']
|
||||||
|
} satisfies SetupComponentOptions<
|
||||||
|
ChoicesProps,
|
||||||
|
ChoicesEmits,
|
||||||
|
keyof ChoicesEmits
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const Choices = defineComponent<
|
||||||
|
ChoicesProps,
|
||||||
|
ChoicesEmits,
|
||||||
|
keyof ChoicesEmits
|
||||||
|
>((props, { emit, attrs }) => {
|
||||||
|
const titleHeight = ref(0);
|
||||||
|
const contentHeight = ref(0);
|
||||||
|
const selected = ref(0);
|
||||||
|
const pageCom = ref<PageExpose>();
|
||||||
|
const choiceSize = reactive<[number, number][]>([]);
|
||||||
|
|
||||||
|
const selFont = computed(() => props.selFont ?? new Font());
|
||||||
|
const maxHeight = computed(() => props.maxHeight ?? 360);
|
||||||
|
const pad = computed(() => props.pad ?? 28);
|
||||||
|
const choiceInterval = computed(() => props.interval ?? 16);
|
||||||
|
const hasText = computed(() => !!props.text);
|
||||||
|
const hasTitle = computed(() => !!props.title);
|
||||||
|
const contentWidth = computed(() => props.width - pad.value * 2);
|
||||||
|
const choiceHeight = computed(
|
||||||
|
() => selFont.value.size + 8 + choiceInterval.value
|
||||||
|
);
|
||||||
|
const contentY = computed(() => {
|
||||||
|
if (hasTitle.value) {
|
||||||
|
return pad.value * 2 + titleHeight.value;
|
||||||
|
} else {
|
||||||
|
return pad.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const choicesY = computed(() => {
|
||||||
|
const padding = pad.value;
|
||||||
|
const text = hasText.value;
|
||||||
|
let y = padding;
|
||||||
|
if (hasTitle.value) {
|
||||||
|
y += titleHeight.value;
|
||||||
|
if (text) {
|
||||||
|
y += padding / 2;
|
||||||
|
} else {
|
||||||
|
y += padding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (text) {
|
||||||
|
y += contentHeight.value;
|
||||||
|
y += padding / 2;
|
||||||
|
}
|
||||||
|
return y;
|
||||||
|
});
|
||||||
|
const choicesMaxHeight = computed(
|
||||||
|
() =>
|
||||||
|
maxHeight.value -
|
||||||
|
choicesY.value -
|
||||||
|
pad.value * 2 -
|
||||||
|
selFont.value.size -
|
||||||
|
8
|
||||||
|
);
|
||||||
|
const choiceCountPerPage = computed(() =>
|
||||||
|
Math.max(Math.floor(choicesMaxHeight.value / choiceHeight.value), 1)
|
||||||
|
);
|
||||||
|
const pages = computed(() =>
|
||||||
|
Math.ceil(props.choices.length / choiceCountPerPage.value)
|
||||||
|
);
|
||||||
|
const choicesHeight = computed(() => {
|
||||||
|
const padBottom = pages.value > 1 ? pad.value + selFont.value.size : 0;
|
||||||
|
if (props.choices.length > choiceCountPerPage.value) {
|
||||||
|
return choiceCountPerPage.value * choiceHeight.value + padBottom;
|
||||||
|
} else {
|
||||||
|
return props.choices.length * choiceHeight.value + padBottom;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const boxHeight = computed(() => {
|
||||||
|
if (props.choices.length > choiceCountPerPage.value) {
|
||||||
|
return (
|
||||||
|
choicesHeight.value +
|
||||||
|
choicesY.value +
|
||||||
|
// 不乘2是因为 choiceY 已经算上了顶部填充
|
||||||
|
pad.value
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
choicesHeight.value +
|
||||||
|
choicesY.value +
|
||||||
|
// 不乘2是因为 choiceY 已经算上了顶部填充
|
||||||
|
pad.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const boxLoc = computed<ElementLocator>(() => {
|
||||||
|
const [x = 0, y = 0, , , ax = 0, ay = 0] = props.loc;
|
||||||
|
return [x, y, props.width, boxHeight.value, ax, ay];
|
||||||
|
});
|
||||||
|
const titleLoc = computed<ElementLocator>(() => {
|
||||||
|
return [props.width / 2, pad.value, void 0, void 0, 0.5, 0];
|
||||||
|
});
|
||||||
|
const contentLoc = computed<ElementLocator>(() => {
|
||||||
|
return [
|
||||||
|
props.width / 2,
|
||||||
|
contentY.value,
|
||||||
|
contentWidth.value,
|
||||||
|
void 0,
|
||||||
|
0.5,
|
||||||
|
0
|
||||||
|
];
|
||||||
|
});
|
||||||
|
const choiceLoc = computed<ElementLocator>(() => {
|
||||||
|
return [
|
||||||
|
props.width / 2,
|
||||||
|
choicesY.value,
|
||||||
|
contentWidth.value,
|
||||||
|
choicesHeight.value,
|
||||||
|
0.5,
|
||||||
|
0
|
||||||
|
];
|
||||||
|
});
|
||||||
|
const selectionLoc = computed<ElementLocator>(() => {
|
||||||
|
const [width = 200, height = 200] = choiceSize[selected.value] ?? [];
|
||||||
|
return [
|
||||||
|
props.width / 2 - pad.value,
|
||||||
|
(selected.value + 0.5) * choiceHeight.value,
|
||||||
|
width + 8,
|
||||||
|
height + 8,
|
||||||
|
0.5,
|
||||||
|
0.5
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const getPageContent = (page: number) => {
|
||||||
|
const count = choiceCountPerPage.value;
|
||||||
|
return props.choices.slice(page * count, (page + 1) * count);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChoiceLoc = (index: number): ElementLocator => {
|
||||||
|
return [
|
||||||
|
props.width / 2 - pad.value,
|
||||||
|
choiceHeight.value * (index + 0.5),
|
||||||
|
void 0,
|
||||||
|
void 0,
|
||||||
|
0.5,
|
||||||
|
0.5
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateContentHeight = (height: number) => {
|
||||||
|
contentHeight.value = height;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTitleHeight = (_0: string, _1: number, height: number) => {
|
||||||
|
titleHeight.value = height;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateChoiceSize = (index: number, width: number, height: number) => {
|
||||||
|
choiceSize[index] = [width, height];
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPageChange = () => {
|
||||||
|
selected.value = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [key] = useKey();
|
||||||
|
key.realize('moveUp', () => {
|
||||||
|
if (selected.value === 0) {
|
||||||
|
if (pageCom.value?.now() !== 0) {
|
||||||
|
pageCom.value?.movePage(-1);
|
||||||
|
selected.value = choiceCountPerPage.value - 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selected.value--;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
key.realize('moveDown', () => {
|
||||||
|
if (selected.value === choiceCountPerPage.value - 1) {
|
||||||
|
pageCom.value?.movePage(1);
|
||||||
|
selected.value = 0;
|
||||||
|
} else {
|
||||||
|
const page = pageCom.value?.now() ?? 1;
|
||||||
|
const index = page * choiceCountPerPage.value + selected.value;
|
||||||
|
if (index < props.choices.length - 1) {
|
||||||
|
selected.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
key.realize('moveLeft', () => pageCom.value?.movePage(-1));
|
||||||
|
key.realize('moveRight', () => pageCom.value?.movePage(1));
|
||||||
|
key.realize('confirm', () => {
|
||||||
|
const page = pageCom.value?.now() ?? 1;
|
||||||
|
const index = page * choiceCountPerPage.value + selected.value;
|
||||||
|
emit('choice', props.choices[index][0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<container loc={boxLoc.value}>
|
||||||
|
<Background
|
||||||
|
loc={[0, 0, props.width, boxHeight.value]}
|
||||||
|
winskin={props.winskin}
|
||||||
|
color={props.color}
|
||||||
|
border={props.border}
|
||||||
|
/>
|
||||||
|
{hasTitle.value && (
|
||||||
|
<text
|
||||||
|
loc={titleLoc.value}
|
||||||
|
text={props.title}
|
||||||
|
font={props.titleFont ?? new Font(void 0, 18)}
|
||||||
|
fillStyle={props.titleFill ?? 'gold'}
|
||||||
|
zIndex={5}
|
||||||
|
onSetText={updateTitleHeight}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{hasText.value && (
|
||||||
|
<TextContent
|
||||||
|
{...attrs}
|
||||||
|
text={props.text}
|
||||||
|
loc={contentLoc.value}
|
||||||
|
width={contentWidth.value}
|
||||||
|
zIndex={5}
|
||||||
|
autoHeight
|
||||||
|
onUpdateHeight={updateContentHeight}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Page
|
||||||
|
ref={pageCom}
|
||||||
|
loc={choiceLoc.value}
|
||||||
|
pages={pages.value}
|
||||||
|
font={props.selFont}
|
||||||
|
hideIfSingle
|
||||||
|
onPageChange={onPageChange}
|
||||||
|
>
|
||||||
|
{(page: number) => [
|
||||||
|
<Selection
|
||||||
|
loc={selectionLoc.value}
|
||||||
|
winskin={props.winskin}
|
||||||
|
color={props.color}
|
||||||
|
border={props.border}
|
||||||
|
/>,
|
||||||
|
...getPageContent(page).map((v, i) => {
|
||||||
|
return (
|
||||||
|
<text
|
||||||
|
text={v[1]}
|
||||||
|
loc={getChoiceLoc(i)}
|
||||||
|
font={props.selFont}
|
||||||
|
cursor="pointer"
|
||||||
|
zIndex={5}
|
||||||
|
onClick={() => emit('choice', v[0])}
|
||||||
|
onSetText={(_, width, height) =>
|
||||||
|
updateChoiceSize(i, width, height)
|
||||||
|
}
|
||||||
|
onEnter={() => (selected.value = i)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
]}
|
||||||
|
</Page>
|
||||||
|
</container>
|
||||||
|
);
|
||||||
|
}, choicesProps);
|
||||||
|
@ -26,12 +26,27 @@ export interface PageProps extends DefaultProps {
|
|||||||
hideIfSingle?: boolean;
|
hideIfSingle?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PageEmits = {
|
||||||
|
pageChange: (page: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
export interface PageExpose {
|
export interface PageExpose {
|
||||||
/**
|
/**
|
||||||
* 切换页码
|
* 切换页码
|
||||||
* @param page 要切换至的页码数,1 表示第一页
|
* @param page 要切换至的页码数,1 表示第一页
|
||||||
*/
|
*/
|
||||||
changePage(page: number): void;
|
changePage(page: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换到传入的页码数加上当前页码数的页码
|
||||||
|
* @param delta 页码数增量
|
||||||
|
*/
|
||||||
|
movePage(delta: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前在第几页
|
||||||
|
*/
|
||||||
|
now(): number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PageSlots = SlotsType<{
|
type PageSlots = SlotsType<{
|
||||||
@ -39,251 +54,267 @@ type PageSlots = SlotsType<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
const pageProps = {
|
const pageProps = {
|
||||||
props: ['pages', 'loc', 'font', 'hideIfSingle']
|
props: ['pages', 'loc', 'font', 'hideIfSingle'],
|
||||||
} satisfies SetupComponentOptions<PageProps, {}, string, PageSlots>;
|
emits: ['pageChange']
|
||||||
|
} satisfies SetupComponentOptions<
|
||||||
|
PageProps,
|
||||||
|
PageEmits,
|
||||||
|
keyof PageEmits,
|
||||||
|
PageSlots
|
||||||
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页组件,用于多页切换,例如存档界面等。参数参考 {@link PageProps},函数接口参考 {@link PageExpose}
|
* 分页组件,用于多页切换,例如存档界面等。参数参考 {@link PageProps},函数接口参考 {@link PageExpose}
|
||||||
*
|
*
|
||||||
* ---
|
* ---
|
||||||
*
|
*
|
||||||
* 用例如下,是一个在每页显示文字的用例,其中 page 表示第几页:
|
* 用例如下,是一个在每页显示文字的用例,其中 page 表示页码索引,第一页就是 0,第二页就是 1,以此类推:
|
||||||
* ```tsx
|
* ```tsx
|
||||||
* <Page maxPage={5}>
|
* <Page maxPage={5}>
|
||||||
* {
|
* {
|
||||||
* (page: number) => {
|
* (page: number) => {
|
||||||
* // 页码从第一页开始,因此这里索引要减一
|
* return items[page].map(v => <text text={v.text} />)
|
||||||
* return items[page - 1].map(v => <text text={v.text} />)
|
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
* </Page>
|
* </Page>
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const Page = defineComponent<PageProps, {}, string, PageSlots>(
|
export const Page = defineComponent<
|
||||||
(props, { slots, expose }) => {
|
PageProps,
|
||||||
const nowPage = ref(1);
|
PageEmits,
|
||||||
|
keyof PageEmits,
|
||||||
|
PageSlots
|
||||||
|
>((props, { slots, expose, emit }) => {
|
||||||
|
const nowPage = ref(0);
|
||||||
|
|
||||||
// 五个元素的位置
|
// 五个元素的位置
|
||||||
const leftLoc = ref<ElementLocator>([]);
|
const leftLoc = ref<ElementLocator>([]);
|
||||||
const leftPageLoc = ref<ElementLocator>([]);
|
const leftPageLoc = ref<ElementLocator>([]);
|
||||||
const nowPageLoc = ref<ElementLocator>([]);
|
const nowPageLoc = ref<ElementLocator>([]);
|
||||||
const rightPageLoc = ref<ElementLocator>([]);
|
const rightPageLoc = ref<ElementLocator>([]);
|
||||||
const rightLoc = ref<ElementLocator>([]);
|
const rightLoc = ref<ElementLocator>([]);
|
||||||
/** 内容的位置 */
|
/** 内容的位置 */
|
||||||
const contentLoc = ref<ElementLocator>([]);
|
const contentLoc = ref<ElementLocator>([]);
|
||||||
/** 页码容器的位置 */
|
/** 页码容器的位置 */
|
||||||
const pageLoc = ref<ElementLocator>([]);
|
const pageLoc = ref<ElementLocator>([]);
|
||||||
/** 页码的矩形框的位置 */
|
/** 页码的矩形框的位置 */
|
||||||
const rectLoc = ref<ElementLocator>([0, 0, 0, 0]);
|
const rectLoc = ref<ElementLocator>([0, 0, 0, 0]);
|
||||||
/** 页面文字的位置 */
|
/** 页面文字的位置 */
|
||||||
const textLoc = ref<ElementLocator>([0, 0, 0, 0]);
|
const textLoc = ref<ElementLocator>([0, 0, 0, 0]);
|
||||||
|
|
||||||
// 两个监听的参数
|
// 两个监听的参数
|
||||||
const leftArrow = ref<Path2D>();
|
const leftArrow = ref<Path2D>();
|
||||||
const rightArrow = ref<Path2D>();
|
const rightArrow = ref<Path2D>();
|
||||||
|
|
||||||
const font = computed(() => props.font ?? new Font());
|
const hide = computed(() => props.hideIfSingle && props.pages === 1);
|
||||||
const isFirst = computed(() => nowPage.value === 1);
|
const font = computed(() => props.font ?? new Font());
|
||||||
const isLast = computed(() => nowPage.value === props.pages);
|
const isFirst = computed(() => nowPage.value === 0);
|
||||||
const width = computed(() => props.loc[2] ?? 200);
|
const isLast = computed(() => nowPage.value === props.pages - 1);
|
||||||
const height = computed(() => props.loc[3] ?? 200);
|
const width = computed(() => props.loc[2] ?? 200);
|
||||||
const round = computed(() => font.value.size / 4);
|
const height = computed(() => props.loc[3] ?? 200);
|
||||||
const nowPageFont = computed(() =>
|
const round = computed(() => font.value.size / 4);
|
||||||
Font.clone(font.value, { weight: 700 })
|
const nowPageFont = computed(() => Font.clone(font.value, { weight: 700 }));
|
||||||
);
|
|
||||||
|
|
||||||
// 左右箭头的颜色
|
// 左右箭头的颜色
|
||||||
const leftColor = computed(() => (isFirst.value ? '#666' : '#ddd'));
|
const leftColor = computed(() => (isFirst.value ? '#666' : '#ddd'));
|
||||||
const rightColor = computed(() => (isLast.value ? '#666' : '#ddd'));
|
const rightColor = computed(() => (isLast.value ? '#666' : '#ddd'));
|
||||||
|
|
||||||
let updating = false;
|
let updating = false;
|
||||||
const updatePagePos = () => {
|
const updatePagePos = () => {
|
||||||
if (updating) return;
|
if (updating) return;
|
||||||
updating = true;
|
updating = true;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
updating = false;
|
updating = false;
|
||||||
});
|
|
||||||
const pageH = font.value.size + 8;
|
|
||||||
contentLoc.value = [0, 0, width.value, height.value - pageH];
|
|
||||||
pageLoc.value = [0, height.value - pageH, width.value, pageH];
|
|
||||||
const center = width.value / 2;
|
|
||||||
const size = font.value.size * 1.5;
|
|
||||||
nowPageLoc.value = [center, 0, size, size, 0.5, 0];
|
|
||||||
leftPageLoc.value = [center - size * 1.5, 0, size, size, 0.5, 0];
|
|
||||||
leftLoc.value = [center - size * 3, 0, size, size, 0.5, 0];
|
|
||||||
rightPageLoc.value = [center + size * 1.5, 0, size, size, 0.5, 0];
|
|
||||||
rightLoc.value = [center + size * 3, 0, size, size, 0.5, 0];
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateArrowPath = () => {
|
|
||||||
const rectSize = font.value.size * 1.5;
|
|
||||||
const size = font.value.size;
|
|
||||||
const pad = rectSize - size;
|
|
||||||
const left = new Path2D();
|
|
||||||
left.moveTo(size, pad);
|
|
||||||
left.lineTo(pad, rectSize / 2);
|
|
||||||
left.lineTo(size, rectSize - pad);
|
|
||||||
const right = new Path2D();
|
|
||||||
right.moveTo(pad, pad);
|
|
||||||
right.lineTo(size, rectSize / 2);
|
|
||||||
right.lineTo(pad, rectSize - pad);
|
|
||||||
leftArrow.value = left;
|
|
||||||
rightArrow.value = right;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateRectAndText = () => {
|
|
||||||
const size = font.value.size * 1.5;
|
|
||||||
const pad = RECT_PAD * size;
|
|
||||||
rectLoc.value = [pad, pad, size - pad * 2, size - pad * 2];
|
|
||||||
textLoc.value = [size / 2, size / 2, void 0, void 0, 0.5, 0.5];
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(font, () => {
|
|
||||||
updatePagePos();
|
|
||||||
updateArrowPath();
|
|
||||||
updateRectAndText();
|
|
||||||
});
|
});
|
||||||
watch(
|
const pageH = hide.value ? 0 : font.value.size + 8;
|
||||||
() => props.loc,
|
contentLoc.value = [0, 0, width.value, height.value - pageH];
|
||||||
() => {
|
pageLoc.value = [0, height.value - pageH, width.value, pageH];
|
||||||
updatePagePos();
|
const center = width.value / 2;
|
||||||
updateRectAndText();
|
const size = font.value.size * 1.5;
|
||||||
}
|
nowPageLoc.value = [center, 0, size, size, 0.5, 0];
|
||||||
);
|
leftPageLoc.value = [center - size * 1.5, 0, size, size, 0.5, 0];
|
||||||
|
leftLoc.value = [center - size * 3, 0, size, size, 0.5, 0];
|
||||||
|
rightPageLoc.value = [center + size * 1.5, 0, size, size, 0.5, 0];
|
||||||
|
rightLoc.value = [center + size * 3, 0, size, size, 0.5, 0];
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
const updateArrowPath = () => {
|
||||||
* 切换页码
|
const rectSize = font.value.size * 1.5;
|
||||||
*/
|
const size = font.value.size;
|
||||||
const changePage = (page: number) => {
|
const pad = rectSize - size;
|
||||||
const target = clamp(page, 1, props.pages);
|
const left = new Path2D();
|
||||||
|
left.moveTo(size, pad);
|
||||||
|
left.lineTo(pad, rectSize / 2);
|
||||||
|
left.lineTo(size, rectSize - pad);
|
||||||
|
const right = new Path2D();
|
||||||
|
right.moveTo(pad, pad);
|
||||||
|
right.lineTo(size, rectSize / 2);
|
||||||
|
right.lineTo(pad, rectSize - pad);
|
||||||
|
leftArrow.value = left;
|
||||||
|
rightArrow.value = right;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRectAndText = () => {
|
||||||
|
const size = font.value.size * 1.5;
|
||||||
|
const pad = RECT_PAD * size;
|
||||||
|
rectLoc.value = [pad, pad, size - pad * 2, size - pad * 2];
|
||||||
|
textLoc.value = [size / 2, size / 2, void 0, void 0, 0.5, 0.5];
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(font, () => {
|
||||||
|
updatePagePos();
|
||||||
|
updateArrowPath();
|
||||||
|
updateRectAndText();
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
() => props.loc,
|
||||||
|
() => {
|
||||||
|
updatePagePos();
|
||||||
|
updateRectAndText();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换页码
|
||||||
|
*/
|
||||||
|
const changePage = (page: number) => {
|
||||||
|
const target = clamp(page, 0, props.pages - 1);
|
||||||
|
if (nowPage.value !== target) {
|
||||||
nowPage.value = target;
|
nowPage.value = target;
|
||||||
};
|
emit('pageChange', target);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const lastPage = () => {
|
const movePage = (delta: number) => {
|
||||||
changePage(nowPage.value - 1);
|
changePage(nowPage.value + delta);
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextPage = () => {
|
const now = () => nowPage.value;
|
||||||
changePage(nowPage.value + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
const lastPage = () => {
|
||||||
updatePagePos();
|
changePage(nowPage.value - 1);
|
||||||
updateArrowPath();
|
};
|
||||||
updateRectAndText();
|
|
||||||
});
|
|
||||||
|
|
||||||
expose({ changePage });
|
const nextPage = () => {
|
||||||
|
changePage(nowPage.value + 1);
|
||||||
|
};
|
||||||
|
|
||||||
return () => {
|
onMounted(() => {
|
||||||
return (
|
updatePagePos();
|
||||||
<container loc={props.loc}>
|
updateArrowPath();
|
||||||
<container loc={contentLoc.value}>
|
updateRectAndText();
|
||||||
{slots.default?.(nowPage.value)}
|
});
|
||||||
</container>
|
|
||||||
|
expose<PageExpose>({ changePage, movePage, now });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
return (
|
||||||
|
<container loc={props.loc}>
|
||||||
|
<container loc={contentLoc.value}>
|
||||||
|
{slots.default?.(nowPage.value)}
|
||||||
|
</container>
|
||||||
|
<container loc={pageLoc.value} hidden={hide.value}>
|
||||||
<container
|
<container
|
||||||
loc={pageLoc.value}
|
key={1}
|
||||||
hidden={props.hideIfSingle && props.pages === 1}
|
loc={leftLoc.value}
|
||||||
|
onClick={lastPage}
|
||||||
|
cursor="pointer"
|
||||||
>
|
>
|
||||||
|
<g-rectr
|
||||||
|
loc={rectLoc.value}
|
||||||
|
circle={[round.value]}
|
||||||
|
strokeStyle={leftColor.value}
|
||||||
|
lineWidth={1}
|
||||||
|
stroke
|
||||||
|
></g-rectr>
|
||||||
|
<g-path
|
||||||
|
path={leftArrow.value}
|
||||||
|
stroke
|
||||||
|
strokeStyle={leftColor.value}
|
||||||
|
lineWidth={1}
|
||||||
|
></g-path>
|
||||||
|
</container>
|
||||||
|
{!isFirst.value && (
|
||||||
<container
|
<container
|
||||||
loc={leftLoc.value}
|
key={2}
|
||||||
|
loc={leftPageLoc.value}
|
||||||
onClick={lastPage}
|
onClick={lastPage}
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
>
|
>
|
||||||
<g-rectr
|
|
||||||
loc={rectLoc.value}
|
|
||||||
circle={[round.value]}
|
|
||||||
strokeStyle={leftColor.value}
|
|
||||||
lineWidth={1}
|
|
||||||
stroke
|
|
||||||
></g-rectr>
|
|
||||||
<g-path
|
|
||||||
path={leftArrow.value}
|
|
||||||
stroke
|
|
||||||
strokeStyle={leftColor.value}
|
|
||||||
lineWidth={1}
|
|
||||||
></g-path>
|
|
||||||
</container>
|
|
||||||
{!isFirst.value && (
|
|
||||||
<container
|
|
||||||
loc={leftPageLoc.value}
|
|
||||||
onClick={lastPage}
|
|
||||||
cursor="pointer"
|
|
||||||
>
|
|
||||||
<g-rectr
|
|
||||||
loc={rectLoc.value}
|
|
||||||
circle={[round.value]}
|
|
||||||
strokeStyle="#ddd"
|
|
||||||
lineWidth={1}
|
|
||||||
stroke
|
|
||||||
></g-rectr>
|
|
||||||
<text
|
|
||||||
loc={textLoc.value}
|
|
||||||
text={(nowPage.value - 1).toString()}
|
|
||||||
font={font.value}
|
|
||||||
></text>
|
|
||||||
</container>
|
|
||||||
)}
|
|
||||||
<container loc={nowPageLoc.value}>
|
|
||||||
<g-rectr
|
<g-rectr
|
||||||
loc={rectLoc.value}
|
loc={rectLoc.value}
|
||||||
circle={[round.value]}
|
circle={[round.value]}
|
||||||
strokeStyle="#ddd"
|
strokeStyle="#ddd"
|
||||||
fillStyle="#ddd"
|
|
||||||
lineWidth={1}
|
lineWidth={1}
|
||||||
fill
|
|
||||||
stroke
|
stroke
|
||||||
></g-rectr>
|
></g-rectr>
|
||||||
<text
|
<text
|
||||||
loc={textLoc.value}
|
loc={textLoc.value}
|
||||||
text={nowPage.value.toString()}
|
text={nowPage.value.toString()}
|
||||||
fillStyle="#222"
|
font={font.value}
|
||||||
font={nowPageFont.value}
|
|
||||||
></text>
|
></text>
|
||||||
</container>
|
</container>
|
||||||
{!isLast.value && (
|
)}
|
||||||
<container
|
<container loc={nowPageLoc.value} key={3}>
|
||||||
loc={rightPageLoc.value}
|
<g-rectr
|
||||||
onClick={nextPage}
|
loc={rectLoc.value}
|
||||||
cursor="pointer"
|
circle={[round.value]}
|
||||||
>
|
strokeStyle="#ddd"
|
||||||
<g-rectr
|
fillStyle="#ddd"
|
||||||
loc={rectLoc.value}
|
lineWidth={1}
|
||||||
circle={[round.value]}
|
fill
|
||||||
strokeStyle="#ddd"
|
stroke
|
||||||
lineWidth={1}
|
></g-rectr>
|
||||||
stroke
|
<text
|
||||||
></g-rectr>
|
loc={textLoc.value}
|
||||||
<text
|
text={(nowPage.value + 1).toString()}
|
||||||
loc={textLoc.value}
|
fillStyle="#222"
|
||||||
text={(nowPage.value + 1).toString()}
|
font={nowPageFont.value}
|
||||||
font={font.value}
|
></text>
|
||||||
></text>
|
</container>
|
||||||
</container>
|
{!isLast.value && (
|
||||||
)}
|
|
||||||
<container
|
<container
|
||||||
loc={rightLoc.value}
|
key={4}
|
||||||
|
loc={rightPageLoc.value}
|
||||||
onClick={nextPage}
|
onClick={nextPage}
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
>
|
>
|
||||||
<g-rectr
|
<g-rectr
|
||||||
loc={rectLoc.value}
|
loc={rectLoc.value}
|
||||||
circle={[round.value]}
|
circle={[round.value]}
|
||||||
strokeStyle={rightColor.value}
|
strokeStyle="#ddd"
|
||||||
lineWidth={1}
|
lineWidth={1}
|
||||||
stroke
|
stroke
|
||||||
></g-rectr>
|
></g-rectr>
|
||||||
<g-path
|
<text
|
||||||
path={rightArrow.value}
|
loc={textLoc.value}
|
||||||
stroke
|
text={(nowPage.value + 2).toString()}
|
||||||
strokeStyle={rightColor.value}
|
font={font.value}
|
||||||
lineWidth={1}
|
></text>
|
||||||
></g-path>
|
|
||||||
</container>
|
</container>
|
||||||
|
)}
|
||||||
|
<container
|
||||||
|
key={5}
|
||||||
|
loc={rightLoc.value}
|
||||||
|
onClick={nextPage}
|
||||||
|
cursor="pointer"
|
||||||
|
>
|
||||||
|
<g-rectr
|
||||||
|
loc={rectLoc.value}
|
||||||
|
circle={[round.value]}
|
||||||
|
strokeStyle={rightColor.value}
|
||||||
|
lineWidth={1}
|
||||||
|
stroke
|
||||||
|
></g-rectr>
|
||||||
|
<g-path
|
||||||
|
path={rightArrow.value}
|
||||||
|
stroke
|
||||||
|
strokeStyle={rightColor.value}
|
||||||
|
lineWidth={1}
|
||||||
|
></g-path>
|
||||||
</container>
|
</container>
|
||||||
</container>
|
</container>
|
||||||
);
|
</container>
|
||||||
};
|
);
|
||||||
},
|
};
|
||||||
pageProps
|
}, pageProps);
|
||||||
);
|
|
||||||
|
@ -19,7 +19,7 @@ import { FloorItemDetail } from '@/plugin/fx/itemDetail';
|
|||||||
import { PopText } from '@/plugin/fx/pop';
|
import { PopText } from '@/plugin/fx/pop';
|
||||||
import { LayerGroupPortal } from '@/plugin/fx/portal';
|
import { LayerGroupPortal } from '@/plugin/fx/portal';
|
||||||
import { defineComponent, onMounted, reactive, ref } from 'vue';
|
import { defineComponent, onMounted, reactive, ref } from 'vue';
|
||||||
import { Textbox } from '../components';
|
import { Textbox, Tip } from '../components';
|
||||||
import { GameUI, UIController } from '@/core/system';
|
import { GameUI, UIController } from '@/core/system';
|
||||||
import {
|
import {
|
||||||
MAIN_HEIGHT,
|
MAIN_HEIGHT,
|
||||||
@ -35,7 +35,6 @@ import {
|
|||||||
} from './statusBar';
|
} from './statusBar';
|
||||||
import { onLoaded } from '../use';
|
import { onLoaded } from '../use';
|
||||||
import { ReplayingStatus } from './toolbar';
|
import { ReplayingStatus } from './toolbar';
|
||||||
import { Tip } from '../components/tip';
|
|
||||||
|
|
||||||
const MainScene = defineComponent(() => {
|
const MainScene = defineComponent(() => {
|
||||||
const layerGroupExtends: ILayerGroupRenderExtends[] = [
|
const layerGroupExtends: ILayerGroupRenderExtends[] = [
|
||||||
|
Loading…
Reference in New Issue
Block a user