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: number) => { * return items[page].map(v => ) * } * } * * ``` */ export const Page = defineComponent< PageProps, PageEmits, keyof PageEmits, PageSlots >((props, { slots, expose, emit }) => { const nowPage = ref(props.page ?? 0); // 五个元素的位置 const leftLoc = ref([]); const leftPageLoc = ref([]); const nowPageLoc = ref([]); const rightPageLoc = ref([]); const rightLoc = ref([]); /** 内容的位置 */ const contentLoc = ref([]); /** 页码容器的位置 */ const pageLoc = ref([]); /** 页码的矩形框的位置 */ const rectLoc = ref([0, 0, 0, 0]); /** 页面文字的位置 */ const textLoc = ref([0, 0, 0, 0]); // 两个监听的参数 const leftArrow = ref(); const rightArrow = ref(); 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({ changePage, movePage, now }); return () => { return ( {slots.default?.(nowPage.value)} ); }; }, pageProps);