feat: 分页组件基础逻辑开发

This commit is contained in:
RubyLiu 2023-07-31 18:42:49 +08:00 committed by rubylliu
parent 0a4503c9ea
commit 9ea2a05d19
17 changed files with 1009 additions and 3 deletions

View File

@ -150,6 +150,12 @@
}
/** 输入框,选择器,文本域 **/
.arco-select {
.arco-icon {
font-size: 16px;
color: var(--color-text-brand);
}
}
.arco-input-wrapper,
.arco-textarea-wrapper,
.arco-input-tag,
@ -483,3 +489,25 @@
.arco-switch-checked {
background: rgb(var(--primary-6)) !important;
}
/** 分页 **/
.arco-pagination-total {
color: var(--color-text-2) !important;
}
.arco-pagination-options {
margin-left: 0 !important;
}
.arco-pagination-total {
margin-right: 16px !important;
}
.arco-pagination-item-previous {
margin-left: 14px !important;
}
.arco-pagination-size-small .arco-pagination-item {
border: 1px solid var(--color-text-input-border);
}
.arco-pagination-item-active {
border-color: rgb(var(--primary-5)) !important;
color: rgb(var(--primary-5)) !important;
background-color: rgb(var(--primary-1)) !important;
}

View File

@ -17,3 +17,9 @@
extraProps: { ...props },
});
</script>
<style lang="less">
.arco-icon {
font-size: 14px !important;
}
</style>

View File

@ -0,0 +1,18 @@
import type { App } from 'vue';
import type { ArcoOptions } from './types';
import { setGlobalConfig, getComponentPrefix } from './utils';
import _Pagination from './pagination';
const MsPagination = Object.assign(_Pagination, {
install: (app: App, options?: ArcoOptions) => {
setGlobalConfig(app, options);
const componentPrefix = getComponentPrefix(options);
app.component(componentPrefix + _Pagination.name, _Pagination);
},
});
export type PaginationInstance = InstanceType<typeof _Pagination>;
export type { PaginationProps } from './interface';
export default MsPagination;

View File

@ -0,0 +1,29 @@
import { CSSProperties } from 'vue';
import { Size } from './types';
import { SelectProps } from '@arco-design/web-vue';
export const PAGE_ITEM_TYPES = ['page', 'more', 'previous', 'next'] as const;
export type PageItemType = (typeof PAGE_ITEM_TYPES)[number];
export interface PaginationProps {
total?: number;
current?: number;
defaultCurrent?: number;
pageSize?: number;
defaultPageSize?: number;
disabled?: boolean;
hideOnSinglePage?: boolean;
simple?: boolean;
showTotal?: boolean;
showMore?: boolean;
showJumper?: boolean;
showPageSize?: boolean;
pageSizeOptions?: number[];
pageSizeProps?: SelectProps;
size?: Size;
pageItemStyle?: CSSProperties;
activePageItemStyle?: CSSProperties;
baseSize?: number;
bufferSize?: number;
}

View File

@ -0,0 +1,10 @@
export default {
msPagination: {
total: 'A total of {total} items',
current: 'Current {current}',
pageSize: 'Page size',
goto: 'Goto',
page: 'Page',
countPerPage: ' / Page',
},
};

View File

@ -0,0 +1,10 @@
export default {
msPagination: {
total: '共 {total} 项数据',
current: '当前页数 {current}',
countPerPage: '条/页',
pageSize: '每页条数',
goto: '前往',
page: '页',
},
};

View File

@ -0,0 +1,58 @@
<template>
<li :class="cls" @click="handleClick">
<slot>
<MsIcon type="icon-icon_more_outlined" />
</slot>
</li>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { getPrefixCls, getLegalPage } from './utils';
import MsIcon from '../ms-icon-font/index.vue';
export default defineComponent({
name: 'EllipsisPager',
components: {
MsIcon,
},
props: {
current: {
type: Number,
required: true,
},
step: {
type: Number,
default: 5,
},
pages: {
type: Number,
required: true,
},
},
emits: ['click'],
setup(props, { emit }) {
const prefixCls = getPrefixCls('pagination-item');
const nextPage = computed(() =>
getLegalPage(props.current + props.step, {
min: 1,
max: props.pages,
})
);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleClick = (e: MouseEvent) => {
emit('click', nextPage.value);
};
const cls = computed(() => [prefixCls, `${prefixCls}-ellipsis`]);
return {
prefixCls,
cls,
handleClick,
};
},
});
</script>

View File

@ -0,0 +1,88 @@
<template>
<component :is="simple ? 'span' : 'li'" :class="cls" @click="handleClick">
<slot :type="isNext ? 'next' : 'previous'">
<MsIcon v-if="isNext" type="icon-icon_right_outlined" />
<MsIcon v-else type="icon-icon_left_outlined" />
</slot>
</component>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { getPrefixCls, getLegalPage } from './utils';
import MsIcon from '../ms-icon-font/index.vue';
export default defineComponent({
name: 'StepPager',
components: {
MsIcon,
},
props: {
pages: {
type: Number,
required: true,
},
current: {
type: Number,
required: true,
},
type: {
type: String,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
simple: {
type: Boolean,
default: false,
},
},
emits: ['click'],
setup(props, { emit }) {
const prefixCls = getPrefixCls('pagination-item');
const isNext = props.type === 'next';
const mergedDisabled = computed(() => {
if (props.disabled) {
return props.disabled;
}
if (!props.pages) {
return true;
}
if (isNext && props.current === props.pages) {
return true;
}
return !isNext && props.current <= 1;
});
const nextPage = computed(() =>
getLegalPage(props.current + (isNext ? 1 : -1), {
min: 1,
max: props.pages,
})
);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleClick = (e: MouseEvent) => {
if (!mergedDisabled.value) {
emit('click', nextPage.value);
}
};
const cls = computed(() => [
prefixCls,
`${prefixCls}-${props.type}`,
{
[`${prefixCls}-disabled`]: mergedDisabled.value,
},
]);
return {
prefixCls,
cls,
isNext,
handleClick,
};
},
});
</script>

View File

@ -0,0 +1,64 @@
<template>
<li :class="cls" :style="mergedStyle" @click="handleClick">
<slot :page="pageNumber">
{{ pageNumber }}
</slot>
</li>
</template>
<script lang="ts">
import type { PropType, CSSProperties } from 'vue';
import { computed, defineComponent } from 'vue';
import { getPrefixCls } from './utils';
export default defineComponent({
name: 'Pager',
props: {
pageNumber: {
type: Number,
},
current: {
type: Number,
},
disabled: {
type: Boolean,
default: false,
},
style: {
type: Object as PropType<CSSProperties>,
},
activeStyle: {
type: Object as PropType<CSSProperties>,
},
},
emits: ['click'],
setup(props, { emit }) {
const prefixCls = getPrefixCls('pagination-item');
const isActive = computed(() => props.current === props.pageNumber);
const handleClick = (e: MouseEvent) => {
if (!props.disabled) {
emit('click', props.pageNumber, e);
}
};
const cls = computed(() => [
prefixCls,
{
[`${prefixCls}-active`]: isActive.value,
},
]);
const mergedStyle = computed(() => {
return isActive.value ? props.activeStyle : props.style;
});
return {
prefixCls,
cls,
mergedStyle,
handleClick,
};
},
});
</script>

View File

@ -0,0 +1,103 @@
<template>
<span :class="cls">
<span v-if="!simple" :class="[`${prefixCls}-prepend`, `${prefixCls}-text-goto`]">
<slot name="jumper-prepend">{{ t('msPagination.goto') }}</slot>
</span>
<a-input-number
v-model="inputValue"
:class="`${prefixCls}-input`"
:min="1"
:max="pages"
:size="size"
:disabled="disabled"
hide-button
:formatter="handleFormatter"
@change="handleChange"
/>
<span v-if="$slots['jumper-append']" :class="`${prefixCls}-append`"><slot name="jumper-append" /></span>
<template v-if="simple">
<span :class="`${prefixCls}-separator`">/</span>
<span :class="`${prefixCls}-total-page`">{{ pages }}</span>
</template>
</span>
</template>
<script lang="ts">
import { computed, defineComponent, nextTick, PropType, ref, watch } from 'vue';
import { useI18n } from '@/hooks/useI18n';
import { getPrefixCls } from './utils';
export default defineComponent({
name: 'PageJumper',
props: {
current: {
type: Number,
required: true,
},
simple: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
pages: {
type: Number,
required: true,
},
size: {
type: String as PropType<'small' | 'mini' | 'medium' | 'large' | undefined>,
},
onChange: {
type: Function as PropType<(value: number) => void>,
},
},
emits: ['change'],
setup(props, { emit }) {
const prefixCls = getPrefixCls('pagination-jumper');
const { t } = useI18n();
const inputValue = ref(props.simple ? props.current : undefined);
const handleFormatter = (value: number) => {
const parseIntVal = parseInt(value.toString(), 10);
return Number.isNaN(parseIntVal) ? undefined : String(parseIntVal);
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleChange = (value: number | undefined) => {
emit('change', inputValue.value);
nextTick(() => {
if (!props.simple) {
inputValue.value = undefined;
}
});
};
watch(
() => props.current,
(value) => {
if (props.simple && value !== inputValue.value) {
inputValue.value = value;
}
}
);
const cls = computed(() => [
prefixCls,
{
[`${prefixCls}-simple`]: props.simple,
},
]);
return {
prefixCls,
cls,
t,
inputValue,
handleChange,
handleFormatter,
};
},
});
</script>

View File

@ -0,0 +1,51 @@
<template>
<span :class="prefixCls">
<a-select
:model-value="pageSize"
:options="options"
:size="size"
:disabled="disabled"
v-bind="selectProps"
@change="handleChange"
/>
</span>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { useI18n } from '@/hooks/useI18n';
import { getPrefixCls } from './utils';
import { Size } from './types';
import { SelectProps } from '@arco-design/web-vue/es/select/interface';
defineOptions({ name: 'PageOptions' });
interface PageOptionsProps {
sizeOptions: number[];
pageSize: number;
disabled: boolean;
size: Size;
onChange: (value: number) => void;
selectProps?: SelectProps;
}
const prefixCls = getPrefixCls('pagination-options');
const { t } = useI18n();
const emit = defineEmits<{
(e: 'change', value: number): void;
}>();
const props = defineProps<PageOptionsProps>();
const handleChange = (value: string | number | Record<string, any> | (string | number | Record<string, any>)[]) => {
emit('change', value as number);
};
const options = computed(() =>
props.sizeOptions.map((value) => ({
value,
label: `${value} ${t('msPagination.countPerPage')}`,
}))
);
</script>

View File

@ -0,0 +1,428 @@
import type { PropType, CSSProperties } from 'vue';
import { computed, defineComponent, reactive, ref, toRefs, watch } from 'vue';
import { getPrefixCls, isNumber } from './utils';
import { Size } from './types';
import Pager from './page-item.vue';
import StepPager from './page-item-step.vue';
import EllipsisPager from './page-item-ellipsis.vue';
import PageJumper from './page-jumper.vue';
import PageOptions from './page-options.vue';
import { useI18n } from '@/hooks/useI18n';
import type { PageItemType } from './interface';
import { SelectProps } from '@arco-design/web-vue/es/select/interface';
import useSize from './useSize';
export type Data = Record<string, any>;
export default defineComponent({
name: 'MsPagination',
props: {
/**
* @zh
* @en Total number of data
*/
total: {
type: Number,
required: true,
},
/**
* @zh
* @en Current page number
* @vModel
*/
current: Number,
/**
* @zh
* @en The default number of pages (uncontrolled state)
*/
defaultCurrent: {
type: Number,
default: 1,
},
/**
* @zh
* @en Number of data items displayed per page
* @vModel
*/
pageSize: Number,
/**
* @zh
* @en The number of data items displayed per page by default (uncontrolled state)
*/
defaultPageSize: {
type: Number,
default: 10,
},
/**
* @zh
* @en Whether to disable
*/
disabled: {
type: Boolean,
default: false,
},
/**
* @zh
* @en Whether to hide pagination when single page
*/
hideOnSinglePage: {
type: Boolean,
default: false,
},
/**
* @zh
* @en Whether it is simple mode
*/
simple: {
type: Boolean,
default: false,
},
/**
* @zh
* @en Whether to display the total number of data
*/
showTotal: {
type: Boolean,
default: false,
},
/**
* @zh
* @en Whether to show more buttons
*/
showMore: {
type: Boolean,
default: false,
},
/**
* @zh
* @en Whether to show jump
*/
showJumper: {
type: Boolean,
default: false,
},
/**
* @zh
* @en Whether to display the data number selector
*/
showPageSize: {
type: Boolean,
default: false,
},
/**
* @zh
* @en Selection list of data number selector
*/
pageSizeOptions: {
type: Array as PropType<number[]>,
default: () => [10, 20, 30, 40, 50],
},
/**
* @zh Props
* @en Props of data number selector
*/
pageSizeProps: {
type: Object as PropType<SelectProps>,
},
/**
* @zh
* @en The size of the page selector
* @values 'mini', 'small', 'medium', 'large'
* @defaultValue 'medium'
*/
size: {
type: String as PropType<Size>,
},
/**
* @zh
* @en The style of the paging button
*/
pageItemStyle: {
type: Object as PropType<CSSProperties>,
},
/**
* @zh
* @en The style of the current paging button
*/
activePageItemStyle: {
type: Object as PropType<CSSProperties>,
},
/**
* @zh `baseSize + 2 * bufferSize`
* @en Calculate and display the number of omitted bases. Display the omitted number as `baseSize + 2 * bufferSize`
*/
baseSize: {
type: Number,
default: 6,
},
/**
* @zh
* @en When the ellipsis is displayed, the number of page numbers displayed on the left and right of the current page number
*/
bufferSize: {
type: Number,
default: 2,
},
/**
* @zh
* @en Whether to adjust the page number when changing the number of data
* @version 2.34.0
*/
autoAdjust: {
type: Boolean,
default: true,
},
},
emits: {
/* eslint-disable @typescript-eslint/no-unused-vars */
'update:current': (current: number) => true,
'update:pageSize': (pageSize: number) => true,
/**
* @zh
* @en Triggered when page number changes
* @param {number} current
*/
'change': (current: number) => true,
/**
* @zh
* @en Triggered when the number of data items changes
* @param {number} pageSize
*/
'pageSizeChange': (pageSize: number) => true,
},
/**
* @zh
* @en Page item
* @version 2.9.0
* @slot page-item
* @binding {number} page The page number of the paging button
*/
/**
* @zh
* @en Page item (step)
* @version 2.9.0
* @slot page-item-step
* @binding {'previous'|'next'} type The type of page item step
*/
/**
* @zh
* @en Page item (ellipsis)
* @version 2.9.0
* @slot page-item-ellipsis
*/
/**
* @zh
* @en Total
* @version 2.9.0
* @slot total
* @binding {number} total
*/
setup(props, { emit, slots }) {
const prefixCls = getPrefixCls('pagination');
const { t } = useI18n();
const { disabled, pageItemStyle, activePageItemStyle, size } = toRefs(props);
const { mergedSize } = useSize(size);
const _current = ref(props.defaultCurrent);
const _pageSize = ref(props.defaultPageSize);
const computedCurrent = computed(() => props.current ?? _current.value);
const computedPageSize = computed(() => props.pageSize ?? _pageSize.value);
const pages = computed(() => Math.ceil(props.total / computedPageSize.value));
const handleClick = (page: number) => {
// when pageJumper blur and input.value is undefined, page is illegal
if (page !== computedCurrent.value && isNumber(page) && !props.disabled) {
_current.value = page;
emit('update:current', page);
emit('change', page);
}
};
const handlePageSizeChange = (pageSize: number) => {
_pageSize.value = pageSize;
emit('update:pageSize', pageSize);
emit('pageSizeChange', pageSize);
};
const pagerProps = reactive({
current: computedCurrent,
pages,
disabled,
style: pageItemStyle,
activeStyle: activePageItemStyle,
onClick: handleClick,
});
const getPageItemElement = (type: PageItemType, prop: Data = {}) => {
if (type === 'more') {
return <EllipsisPager v-slots={{ default: slots['page-item-ellipsis'] }} {...prop} {...pagerProps} />;
}
if (type === 'previous') {
return <StepPager v-slots={{ default: slots['page-item-step'] }} type="previous" {...prop} {...pagerProps} />;
}
if (type === 'next') {
return <StepPager v-slots={{ default: slots['page-item-step'] }} type="next" {...prop} {...pagerProps} />;
}
return <Pager v-slots={{ default: slots['page-item'] }} {...prop} {...pagerProps} />;
};
const pageList = computed(() => {
const pageListArr: Array<JSX.Element | JSX.Element[]> = [];
if (pages.value < props.baseSize + props.bufferSize * 2) {
for (let i = 1; i <= pages.value; i++) {
pageListArr.push(getPageItemElement('page', { key: i, pageNumber: i }));
}
} else {
let left = 1;
let right = pages.value;
let hasLeftEllipsis = false;
let hasRightEllipsis = false;
if (computedCurrent.value > 2 + props.bufferSize) {
hasLeftEllipsis = true;
left = Math.min(computedCurrent.value - props.bufferSize, pages.value - 2 * props.bufferSize);
}
if (computedCurrent.value < pages.value - (props.bufferSize + 1)) {
hasRightEllipsis = true;
right = Math.max(computedCurrent.value + props.bufferSize, 2 * props.bufferSize + 1);
}
if (hasLeftEllipsis) {
pageListArr.push(getPageItemElement('page', { key: 1, pageNumber: 1 }));
pageListArr.push(
getPageItemElement('more', {
key: 'left-ellipsis-pager',
step: -(props.bufferSize * 2 + 1),
})
);
}
for (let i = left; i <= right; i++) {
pageListArr.push(getPageItemElement('page', { key: i, pageNumber: i }));
}
if (hasRightEllipsis) {
pageListArr.push(
getPageItemElement('more', {
key: 'right-ellipsis-pager',
step: props.bufferSize * 2 + 1,
})
);
pageListArr.push(
getPageItemElement('page', {
key: pages.value,
pageNumber: pages.value,
})
);
}
}
return pageListArr;
});
const renderPager = () => {
if (props.simple) {
return (
<span class={`${prefixCls}-simple`}>
{getPageItemElement('previous', { simple: true })}
<PageJumper
disabled={props.disabled}
current={computedCurrent.value}
size={mergedSize.value}
pages={pages.value}
simple
onChange={handleClick}
/>
{getPageItemElement('next', { simple: true })}
</span>
);
}
return (
<ul class={`${prefixCls}-list`}>
{getPageItemElement('previous', { simple: true })}
{pageList.value}
{props.showMore &&
getPageItemElement('more', {
key: 'more',
step: props.bufferSize * 2 + 1,
})}
{getPageItemElement('next', { simple: true })}
</ul>
);
};
// When the number of data items changes, recalculate the page number
watch(computedPageSize, (curPageSize, prePageSize) => {
if (props.autoAdjust && curPageSize !== prePageSize && computedCurrent.value > 1) {
const index = prePageSize * (computedCurrent.value - 1) + 1;
const newPage = Math.ceil(index / curPageSize);
if (newPage !== computedCurrent.value) {
_current.value = newPage;
emit('update:current', newPage);
emit('change', newPage);
}
}
});
watch(pages, (curPages, prePages) => {
if (props.autoAdjust && curPages !== prePages && computedCurrent.value > 1 && computedCurrent.value > curPages) {
_current.value = curPages;
emit('update:current', curPages);
emit('change', curPages);
}
});
const cls = computed(() => [
prefixCls,
`${prefixCls}-size-${mergedSize.value}`,
{
[`${prefixCls}-simple`]: props.simple,
[`${prefixCls}-disabled`]: props.disabled,
},
]);
return () => {
if (props.hideOnSinglePage && pages.value <= 1) {
return null;
}
return (
<div class={cls.value}>
{props.showTotal && (
<span class={`${prefixCls}-total`}>
{slots.total?.({ total: props.total }) ?? t('msPagination.total', { total: props.total })}
</span>
)}
{props.showPageSize && (
<PageOptions
disabled={props.disabled}
sizeOptions={props.pageSizeOptions}
pageSize={computedPageSize.value}
size={mergedSize.value}
onChange={(v: number) => handlePageSizeChange(v)}
selectProps={props.pageSizeProps}
/>
)}
{renderPager()}
{!props.simple && props.showJumper && (
<PageJumper
v-slots={{
'jumper-prepend': slots['jumper-prepend'],
'jumper-append': slots['jumper-append'],
}}
disabled={props.disabled}
current={computedCurrent.value}
pages={pages.value}
size={mergedSize.value}
onChange={handleClick}
/>
)}
</div>
);
};
},
});

View File

@ -0,0 +1,20 @@
import { Slots } from 'vue';
import { ArcoLang } from '@arco-design/web-vue/es/locale/interface';
export interface ArcoOptions {
classPrefix?: string;
componentPrefix?: string;
}
export const SIZES = ['mini', 'small', 'medium', 'large'] as const;
export type Size = (typeof SIZES)[number];
export interface ConfigProvider {
slots: Slots;
prefixCls?: string;
locale?: ArcoLang;
size?: Size;
updateAtScroll?: boolean;
scrollToClose?: boolean;
exchangeTime?: boolean;
}

View File

@ -0,0 +1,15 @@
import { computed, inject, Ref } from 'vue';
import { Size } from './types';
import { configProviderInjectionKey } from './utils';
const useSize = (size?: Ref<Size | undefined>, { defaultValue = 'medium' }: { defaultValue?: Size } = {}) => {
const configProviderCtx = inject(configProviderInjectionKey, undefined);
const mergedSize = computed(() => size?.value ?? configProviderCtx?.size ?? defaultValue);
return {
mergedSize,
};
};
export default useSize;

View File

@ -0,0 +1,50 @@
import type { App } from 'vue';
import { getCurrentInstance, inject, InjectionKey } from 'vue';
import type { ArcoOptions, ConfigProvider } from './types';
const COMPONENT_PREFIX = 'A';
const CLASS_PREFIX = 'arco';
const GLOBAL_CONFIG_NAME = '$arco';
export const configProviderInjectionKey: InjectionKey<ConfigProvider> = Symbol('ArcoConfigProvider');
export const getComponentPrefix = (options?: ArcoOptions) => {
return options?.componentPrefix ?? COMPONENT_PREFIX;
};
export const setGlobalConfig = (app: App, options?: ArcoOptions): void => {
if (options && options.classPrefix) {
app.config.globalProperties[GLOBAL_CONFIG_NAME] = {
...(app.config.globalProperties[GLOBAL_CONFIG_NAME] ?? {}),
classPrefix: options.classPrefix,
};
}
};
export const getPrefixCls = (componentName?: string): string => {
const instance = getCurrentInstance();
const configProvider = inject(configProviderInjectionKey, undefined);
const prefix =
configProvider?.prefixCls ??
instance?.appContext.config.globalProperties[GLOBAL_CONFIG_NAME]?.classPrefix ??
CLASS_PREFIX;
if (componentName) {
return `${prefix}-${componentName}`;
}
return prefix;
};
export const getLegalPage = (page: number, { min, max }: { min: number; max: number }): number => {
if (page < min) {
return min;
}
if (page > max) {
return max;
}
return page;
};
export function isNumber(obj: any): obj is number {
return Object.prototype.toString.call(obj) === '[object Number]' && obj === obj; // eslint-disable-line
}

View File

@ -75,7 +75,7 @@
} from './type';
import BatchAction from './batchAction.vue';
import type { TableColumnData, TableData } from '@arco-design/web-vue';
import type { TableData } from '@arco-design/web-vue';
import ColumnSelector from './columnSelector.vue';
const batchleft = ref('10px');

View File

@ -1,3 +1,31 @@
<template> BugManagement is waiting for development </template>
<template>
<div>BugManagement is waiting for development </div>
<div class="continer">
<ms-pagination
v-modal:page-size="pageSize"
size="small"
:total="100"
show-total
show-jumper
show-more
show-page-size
/>
</div>
</template>
<script setup></script>
<script setup>
import { ref } from 'vue';
import MsPagination from '@/components/pure/ms-pagination/index';
const pageSize = ref(10);
</script>
<style lang="less" scoped>
.continer {
margin-top: 200px;
padding: 50px;
width: 100%;
height: 500px;
background-color: #ffffff;
}
</style>