mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-10-08 20:01:47 +08:00
353 lines
11 KiB
TypeScript
353 lines
11 KiB
TypeScript
import {
|
||
computed,
|
||
defineComponent,
|
||
nextTick,
|
||
onMounted,
|
||
ref,
|
||
SlotsType,
|
||
VNode,
|
||
watch
|
||
} from 'vue';
|
||
import { clamp, isNil } from 'lodash-es';
|
||
import { DefaultProps, ElementLocator, Font } from '@motajs/render';
|
||
import { SetupComponentOptions } from '@motajs/system-ui';
|
||
|
||
/** 圆角矩形页码距离容器的边框大小,与 pageSize 相乘 */
|
||
const RECT_PAD = 0.1;
|
||
|
||
export interface PageProps extends DefaultProps {
|
||
/** 共有多少页 */
|
||
pages: number;
|
||
/** 页码组件的定位 */
|
||
loc: ElementLocator;
|
||
/** 当前页码 */
|
||
page?: number;
|
||
/** 页码的字体 */
|
||
font?: Font;
|
||
/** 只有一页的时候,是否隐藏页码 */
|
||
hideIfSingle?: boolean;
|
||
}
|
||
|
||
export type PageEmits = {
|
||
pageChange: (page: number) => void;
|
||
|
||
'update:page': (page: number) => void;
|
||
};
|
||
|
||
export interface PageExpose {
|
||
/**
|
||
* 切换页码
|
||
* @param page 要切换至的页码数,0 表示第一页
|
||
*/
|
||
changePage(page: number): void;
|
||
|
||
/**
|
||
* 切换到传入的页码数加上当前页码数的页码
|
||
* @param delta 页码数增量
|
||
*/
|
||
movePage(delta: number): void;
|
||
|
||
/**
|
||
* 获取当前在第几页
|
||
*/
|
||
now(): number;
|
||
}
|
||
|
||
type PageSlots = SlotsType<{
|
||
default: (page: number) => VNode | VNode[];
|
||
}>;
|
||
|
||
const pageProps = {
|
||
props: ['pages', 'loc', 'page', 'font', 'hideIfSingle'],
|
||
emits: ['pageChange', 'update:page']
|
||
} satisfies SetupComponentOptions<
|
||
PageProps,
|
||
PageEmits,
|
||
keyof PageEmits,
|
||
PageSlots
|
||
>;
|
||
|
||
/**
|
||
* 分页组件,用于多页切换,例如存档界面等。参数参考 {@link PageProps},函数接口参考 {@link PageExpose}
|
||
*
|
||
* ---
|
||
*
|
||
* 用例如下,是一个在每页显示文字的用例,其中 page 表示页码索引,第一页就是 0,第二页就是 1,以此类推:
|
||
* ```tsx
|
||
* <Page maxPage={5}>
|
||
* {
|
||
* (page: number) => {
|
||
* return items[page].map(v => <text text={v.text} />)
|
||
* }
|
||
* }
|
||
* </Page>
|
||
* ```
|
||
*/
|
||
export const Page = defineComponent<
|
||
PageProps,
|
||
PageEmits,
|
||
keyof PageEmits,
|
||
PageSlots
|
||
>((props, { slots, expose, emit }) => {
|
||
const nowPage = ref(props.page ?? 0);
|
||
|
||
// 五个元素的位置
|
||
const leftLoc = ref<ElementLocator>([]);
|
||
const leftPageLoc = ref<ElementLocator>([]);
|
||
const nowPageLoc = ref<ElementLocator>([]);
|
||
const rightPageLoc = ref<ElementLocator>([]);
|
||
const rightLoc = ref<ElementLocator>([]);
|
||
/** 内容的位置 */
|
||
const contentLoc = ref<ElementLocator>([]);
|
||
/** 页码容器的位置 */
|
||
const pageLoc = ref<ElementLocator>([]);
|
||
/** 页码的矩形框的位置 */
|
||
const rectLoc = ref<ElementLocator>([0, 0, 0, 0]);
|
||
/** 页面文字的位置 */
|
||
const textLoc = ref<ElementLocator>([0, 0, 0, 0]);
|
||
|
||
// 两个监听的参数
|
||
const leftArrow = ref<Path2D>();
|
||
const rightArrow = ref<Path2D>();
|
||
|
||
const hide = computed(() => props.hideIfSingle && props.pages === 1);
|
||
const font = computed(() => props.font ?? new Font());
|
||
const isFirst = computed(() => nowPage.value === 0);
|
||
const isLast = computed(() => nowPage.value === props.pages - 1);
|
||
const width = computed(() => props.loc[2] ?? 200);
|
||
const height = computed(() => props.loc[3] ?? 200);
|
||
const round = computed(() => font.value.size / 4);
|
||
const nowPageFont = computed(() => Font.clone(font.value, { weight: 700 }));
|
||
/** 页码的横向间距 */
|
||
const interval = computed(() => {
|
||
const size = font.value.size * 1.5;
|
||
const max = size * 9;
|
||
if (width.value > max) {
|
||
return size;
|
||
} else {
|
||
return (width.value - size * 5) / 4;
|
||
}
|
||
});
|
||
|
||
// 左右箭头的颜色
|
||
const leftColor = computed(() => (isFirst.value ? '#666' : '#ddd'));
|
||
const rightColor = computed(() => (isLast.value ? '#666' : '#ddd'));
|
||
|
||
let updating = false;
|
||
const updatePagePos = () => {
|
||
if (updating) return;
|
||
updating = true;
|
||
nextTick(() => {
|
||
updating = false;
|
||
});
|
||
const pageH = hide.value ? 0 : 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;
|
||
const int = size + interval.value;
|
||
nowPageLoc.value = [center, 0, size, size, 0.5, 0];
|
||
leftPageLoc.value = [center - int, 0, size, size, 0.5, 0];
|
||
leftLoc.value = [center - int * 2, 0, size, size, 0.5, 0];
|
||
rightPageLoc.value = [center + int, 0, size, size, 0.5, 0];
|
||
rightLoc.value = [center + int * 2, 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(
|
||
() => props.loc,
|
||
() => {
|
||
updatePagePos();
|
||
updateRectAndText();
|
||
}
|
||
);
|
||
watch(
|
||
() => props.page,
|
||
page => {
|
||
if (!isNil(page)) {
|
||
const target = clamp(page, 0, props.pages - 1);
|
||
if (nowPage.value !== target) {
|
||
nowPage.value = target;
|
||
emit('pageChange', target);
|
||
}
|
||
}
|
||
}
|
||
);
|
||
|
||
/**
|
||
* 切换页码
|
||
*/
|
||
const changePage = (page: number) => {
|
||
const target = clamp(page, 0, props.pages - 1);
|
||
if (nowPage.value !== target) {
|
||
nowPage.value = target;
|
||
emit('pageChange', target);
|
||
emit('update:page', target);
|
||
}
|
||
};
|
||
|
||
const movePage = (delta: number) => {
|
||
changePage(nowPage.value + delta);
|
||
};
|
||
|
||
const now = () => nowPage.value;
|
||
|
||
const lastPage = () => {
|
||
changePage(nowPage.value - 1);
|
||
};
|
||
|
||
const nextPage = () => {
|
||
changePage(nowPage.value + 1);
|
||
};
|
||
|
||
onMounted(() => {
|
||
updatePagePos();
|
||
updateArrowPath();
|
||
updateRectAndText();
|
||
});
|
||
|
||
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} nocache>
|
||
<container
|
||
key={1}
|
||
loc={leftLoc.value}
|
||
onClick={lastPage}
|
||
cursor="pointer"
|
||
nocache
|
||
>
|
||
<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
|
||
key={2}
|
||
loc={leftPageLoc.value}
|
||
onClick={lastPage}
|
||
cursor="pointer"
|
||
nocache
|
||
>
|
||
<g-rectr
|
||
loc={rectLoc.value}
|
||
circle={[round.value]}
|
||
strokeStyle="#ddd"
|
||
lineWidth={1}
|
||
stroke
|
||
></g-rectr>
|
||
<text
|
||
loc={textLoc.value}
|
||
text={nowPage.value.toString()}
|
||
font={font.value}
|
||
></text>
|
||
</container>
|
||
)}
|
||
<container loc={nowPageLoc.value} key={3} nocache>
|
||
<g-rectr
|
||
loc={rectLoc.value}
|
||
circle={[round.value]}
|
||
strokeStyle="#ddd"
|
||
fillStyle="#ddd"
|
||
lineWidth={1}
|
||
fill
|
||
stroke
|
||
></g-rectr>
|
||
<text
|
||
loc={textLoc.value}
|
||
text={(nowPage.value + 1).toString()}
|
||
fillStyle="#222"
|
||
font={nowPageFont.value}
|
||
></text>
|
||
</container>
|
||
{!isLast.value && (
|
||
<container
|
||
key={4}
|
||
loc={rightPageLoc.value}
|
||
onClick={nextPage}
|
||
cursor="pointer"
|
||
nocache
|
||
>
|
||
<g-rectr
|
||
loc={rectLoc.value}
|
||
circle={[round.value]}
|
||
strokeStyle="#ddd"
|
||
lineWidth={1}
|
||
stroke
|
||
></g-rectr>
|
||
<text
|
||
loc={textLoc.value}
|
||
text={(nowPage.value + 2).toString()}
|
||
font={font.value}
|
||
></text>
|
||
</container>
|
||
)}
|
||
<container
|
||
key={5}
|
||
loc={rightLoc.value}
|
||
onClick={nextPage}
|
||
cursor="pointer"
|
||
nocache
|
||
>
|
||
<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>
|
||
);
|
||
};
|
||
}, pageProps);
|