feat:Pagination组件省略部分支持切换页码

This commit is contained in:
yujinghan 2023-12-04 20:52:32 +08:00
parent 51f5d36519
commit afc0996cf0
12 changed files with 450 additions and 16 deletions

View File

@ -8,7 +8,7 @@ icon:
order: 73
---
分页组件
### 基本用法
```schema: scope="body"
{
@ -34,7 +34,38 @@ order: 73
}
```
### 简易模式
### 微型模式
配置`"size": "small"`可实现微型模式
```schema: scope="body"
{
"type": "service",
"api": "/api/mock2/crud/table",
"body": [
{
"type": "pagination",
"layout": "total,perPage,pager,go",
"mode": "normal",
"activePage": 1,
"lastPage": 99999,
"size": "small",
"total": 999,
"perPage": 10,
"maxButtons": 7,
"showPerPage": true,
"perPageAvailable": [10, 20, 50, 100],
"showPageInput": true,
"disabled": false
}
]
}
```
### 简洁模式
配置`"mode": "simple"`可实现简洁模式
```schema: scope="body"
{
@ -63,6 +94,8 @@ order: 73
| activePage | `number` \| `string` | `1` | 当前页数 |
| perPage | `number` \| `string` | `10` | 每页显示多条数据 |
| showPerPage | `boolean` | false | 是否展示 perPage 切换器 layout 和 showPerPage 都可以控制 |
| size | `'default' \| 'small'` | default | 组件尺寸,支持`default`、`small`设置 |
| ellipsisPageGap | `number` \| `string` | 5 | 多页跳转页数,页数较多出现`...`时点击省略号时每次前进/后退的页数默认为5 |
| perPageAvailable | `number[]` | `[10, 20, 50, 100]` | 指定每页可以显示多少条 |
| showPageInput | `boolean` | false | 是否显示快速跳转输入框 layout 和 showPageInput 都可以控制 |
| disabled | `boolean` | false | 是否禁用 |

View File

@ -10,7 +10,8 @@ export const PaginationStore = iRendererStore
perPage: 10,
inputName: '',
outputName: '',
mode: 'normal'
mode: 'normal',
ellipsisPageGap: 5
})
.views(self => ({
get inputItems() {

View File

@ -42,7 +42,8 @@ export class PaginationPlugin extends BasePlugin {
disabled: false,
perPageAvailable: [10, 20, 50, 100],
perPage: 10,
maxButtons: 7
maxButtons: 7,
ellipsisPageGap: 5
};
previewSchema = {
...this.scaffold
@ -255,6 +256,15 @@ export class PaginationPlugin extends BasePlugin {
max: 20,
pipeOut: (value: any) => value || 5,
visibleOn: '!data.mode || data.mode === "normal"'
},
{
name: 'ellipsisPageGap',
label: '多页跳转页数',
type: 'input-number',
min: 1,
pipeIn: (value: any) => value || 5,
pipeOut: (value: any) => value || 5,
visibleOn: 'data.mode && data.mode === "normal"'
}
]
},
@ -271,6 +281,30 @@ export class PaginationPlugin extends BasePlugin {
{
title: '外观',
body: getSchemaTpl('collapseGroup', [
{
title: '基本',
body: [
{
type: 'select',
name: 'size',
label: '尺寸',
value: '',
pipeIn: defaultValue('default'),
options: [
{
label: '正常',
value: 'default'
},
{
label: '微型',
value: 'small'
}
],
visibleOn: 'data.mode === "normal"'
}
]
},
getSchemaTpl('style:classNames', {isFormItem: false})
])
},

View File

@ -595,7 +595,9 @@ $Table-strip-bg: transparent;
--Pagination-fontSize: var(--fonts-size-8);
--Pagination-height: #{px2rem(32px)};
--Pagination-height-sm: #{px2rem(24px)};
--Pagination-minWidth: #{px2rem(32px)};
--Pagination-minWidth-sm: #{px2rem(24px)};
--Pagination-onActive-backgroundColor: var(--colors-neutral-fill-11);
--Pagination-onActive-border: var(--borders-width-2) var(--borders-style-2)
var(--colors-brand-5);
@ -603,6 +605,7 @@ $Table-strip-bg: transparent;
--Pagination-onDisabled-color: var(--colors-neutral-text-6);
--Pagination-onDisabled-backgroundColor: var(--colors-neutral-fill-10);
--Pagination-padding: 0 #{px2rem(8px)};
--Pagination-padding-sm: 0 #{px2rem(4px)};
--Pagination-light-color: #84868c;
--Panel--default-badgeBg: var(--colors-neutral-fill-3);

View File

@ -37,10 +37,28 @@
}
.ellipsis {
cursor: unset;
position: relative;
> a {
cursor: unset;
position: relative;
position: absolute;
top: px2rem(-4px);
opacity: 1;
transition: all 0.2s;
}
> span {
opacity: 0;
transition: all 0.2s;
}
}
.ellipsis:hover {
cursor: pointer;
> a {
opacity: 0;
transition: all 0.2s;
}
> span {
opacity: 1;
transition: all 0.2s;
}
}
@ -153,6 +171,44 @@
}
}
}
&-inputSimple {
display: inline-flex;
flex-wrap: nowrap;
align-items: center;
height: var(--Pagination-height);
input {
min-width: px2rem(40px);
width: px2rem(40px);
height: var(--Pagination-height);
line-height: var(--Pagination-height);
border: none;
border: var(--borderWidth) solid var(--borderColor);
border-radius: var(--borderRadius);
padding: var(--Pagination-padding);
margin-right: px2rem(8px);
text-align: center;
&:focus,
&:hover {
outline: none;
border-color: var(--primary);
}
}
&-right {
display: inline-block;
width: px2rem(32px);
height: var(--Pagination-height);
line-height: var(--Pagination-height);
border: var(--borderWidth) solid var(--borderColor);
font-size: var(--fontSizeSm);
&:hover {
color: #666666 !important;
cursor: default;
}
}
}
}
.#{$ns}Pagination-wrap {
@ -196,3 +252,43 @@
text-align: right;
}
.#{$ns}Pagination-wrap-size--small {
line-height: px2rem(24px);
.#{$ns}Pagination-item {
margin-left: px2rem(4px);
> li {
> a,
> span {
min-width: var(--Pagination-minWidth-sm);
height: var(--Pagination-height-sm);
line-height: var(--Pagination-height-sm);
padding: var(--Pagination-padding-sm);
}
}
.#{$ns}Pagination-inputGroup {
height: var(--Pagination-height-sm);
&-input {
min-width: px2rem(40px);
width: px2rem(40px);
height: var(--Pagination-height-sm);
line-height: var(--Pagination-height-sm);
padding: var(--Pagination-padding-sm);
margin-left: px2rem(4px);
}
&-right {
height: var(--Pagination-height-sm);
line-height: var(--Pagination-height-sm);
}
}
}
.#{$ns}Pagination-perpage {
padding: 0 px2rem(6px);
min-height: px2rem(24px);
vertical-align: baseline;
}
}

View File

@ -20,6 +20,12 @@ export const enum PaginationWidget {
Go = 'go'
}
export const enum KeyCode {
ENTER = 13,
UP = 38,
DOWN = 40
}
export interface BasicPaginationProps {
/**
* layout属性的顺序 total,perPage,pager,go
@ -93,6 +99,20 @@ export interface BasicPaginationProps {
*/
popOverContainerSelector?: string;
/**
*
*
* @default 5
*/
ellipsisPageGap?: number | string;
/**
*
*
* @default 'default'
*/
size?: string;
onPageChange?: (page: number, perPage?: number, dir?: string) => void;
}
export interface PaginationProps
@ -103,6 +123,7 @@ export interface PaginationProps
}
export interface PaginationState {
pageNum: string;
internalPageNum: string;
perPage: number;
}
export class Pagination extends React.Component<
@ -115,11 +136,14 @@ export class Pagination extends React.Component<
mode: 'normal' as MODE_TYPE,
activePage: 1,
perPage: 10,
perPageAvailable: [10, 20, 50, 100]
perPageAvailable: [10, 20, 50, 100],
ellipsisPageGap: 5,
size: 'default'
};
state = {
pageNum: '',
internalPageNum: '1',
perPage: Number(this.props.perPage)
};
@ -139,6 +163,15 @@ export class Pagination extends React.Component<
}
}
componentWillReceiveProps(nextProps: PaginationProps) {
if (
this.props.mode === 'simple' &&
nextProps.activePage !== Number(this.state.internalPageNum)
) {
this.setState({internalPageNum: String(nextProps.activePage)});
}
}
async handlePageNumChange(page: number, perPage?: number, dir?: string) {
const {disabled, onPageChange} = this.props;
const _page = isNaN(Number(page)) || Number(page) < 1 ? 1 : page;
@ -179,10 +212,37 @@ export class Pagination extends React.Component<
* @param page
*/
renderEllipsis(key: string) {
const {classnames: cx} = this.props;
const {classnames: cx, activePage, ellipsisPageGap} = this.props;
const {perPage} = this.state;
const lastPage = this.getLastPage();
const gap: number =
isNaN(Number(ellipsisPageGap)) || Number(ellipsisPageGap) < 1
? 5
: Number(ellipsisPageGap);
const isPrevEllipsis = key === 'prev-ellipsis';
const jumpContent = isPrevEllipsis ? (
<Icon icon="arrow-double-left" className="icon" />
) : (
<Icon icon="arrow-double-right" className="icon" />
);
const jumpPage = isPrevEllipsis
? Math.max(1, activePage - gap)
: Math.min(lastPage, activePage + gap);
return (
<li key={key} className={cx('ellipsis')}>
<li
key={key}
className={cx('ellipsis')}
onClick={(e: any) => {
return this.handlePageNumChange(
jumpPage,
perPage,
isPrevEllipsis ? 'backward' : 'forward'
);
}}
>
<a role="button">...</a>
<span className="icon">{jumpContent}</span>
</li>
);
}
@ -256,6 +316,57 @@ export class Pagination extends React.Component<
this.setState({pageNum: value});
}
/**
* input onChange/onKeyUp事件
*
* @param event
*/
@autobind
handleSimpleKeyUp(
e:
| React.KeyboardEvent<HTMLInputElement>
| React.ChangeEvent<HTMLInputElement>
) {
const lastPage = this.getLastPage();
let v: number = parseInt(e.currentTarget.value, 10);
// handle keyboard up and down events value
switch ((e as React.KeyboardEvent<HTMLInputElement>).keyCode) {
case KeyCode.UP:
v = isNaN(v) || v < 2 ? 1 : v - 1;
break;
case KeyCode.DOWN:
v = v + 1;
break;
default:
break;
}
// validate inputvalue
if (/^\d+$/.test(String(v)) && v >= lastPage) {
v = lastPage;
}
this.setState({internalPageNum: String(v)});
// handle empty val
if (!v) {
this.setState({internalPageNum: ''});
return;
}
if (
[KeyCode.UP, KeyCode.DOWN, KeyCode.ENTER].includes(
(e as React.KeyboardEvent<HTMLInputElement>).keyCode
)
) {
this.handlePageNumChange(v, this.props.perPage);
}
}
/**
* input onBlur事件
*/
@autobind
handleSimpleBlur() {
this.setState({internalPageNum: String(this.props.activePage)});
}
render() {
const {
layout,
@ -273,14 +384,34 @@ export class Pagination extends React.Component<
popOverContainer,
popOverContainerSelector,
mobileUI,
size,
translate: __
} = this.props;
let maxButtons = this.props.maxButtons;
const {pageNum, perPage} = this.state;
const {pageNum, perPage, internalPageNum} = this.state;
const lastPage = this.getLastPage();
// 简易模式
let simplePager: React.ReactNode = null;
// 简洁模式
if (mode === 'simple') {
simplePager = (
<li className={cx('Pagination-inputSimple')} key="simple-go">
<input
className={cx('Pagination-inputSimple-input')}
key="simple-input"
type="text"
disabled={disabled}
onChange={this.handleSimpleKeyUp}
onKeyUp={this.handleSimpleKeyUp}
onBlur={this.handleSimpleBlur}
value={internalPageNum}
/>
/
<span className={cx('Pagination-inputSimple-right')} key="go-right">
{lastPage}
</span>
</li>
);
return (
<div
className={cx(
@ -320,6 +451,7 @@ export class Pagination extends React.Component<
<Icon icon="left-arrow" className="icon" />
</span>
</li>
{simplePager}
<li
className={cx('Pagination-next', {
'is-disabled': !hasNext
@ -530,7 +662,14 @@ export class Pagination extends React.Component<
</div>
);
return (
<div className={cx('Pagination-wrap', {disabled: disabled}, className)}>
<div
className={cx(
'Pagination-wrap',
`Pagination-wrap-size--${size}`,
{disabled: disabled},
className
)}
>
{layoutList.map(layoutItem => {
if (layoutItem === PaginationWidget.Pager) {
return (

View File

@ -17,6 +17,8 @@ import PlayIcon from '../icons/play.svg';
import PauseIcon from '../icons/pause.svg';
import LeftArrowIcon from '../icons/left-arrow.svg';
import RightArrowIcon from '../icons/right-arrow.svg';
import ArrowDoubleLeftIcon from '../icons/arrow-double-left.svg';
import ArrowDoubleRightIcon from '../icons/arrow-double-right.svg';
import CheckIcon from '../icons/check.svg';
import PlusIcon from '../icons/plus.svg';
import MinusIcon from '../icons/minus.svg';
@ -233,6 +235,8 @@ registerIcon('remove', RemoveIcon);
registerIcon('invisible', InvisibleIcon);
registerIcon('down', DownIcon);
registerIcon('right-double-arrow', RightDoubleArrowIcon);
registerIcon('arrow-double-left', ArrowDoubleLeftIcon);
registerIcon('arrow-double-right', ArrowDoubleRightIcon);
registerIcon('new-edit', NewEdit);
registerIcon('rotate-left', RotateLeft);
registerIcon('rotate-right', RotateRight);

View File

@ -0,0 +1 @@
<svg t="1701244927821" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5047" width="200" height="200"><path d="M842.666667 864c-8.533333 0-14.933333-2.133333-21.333334-8.533333l-341.333333-309.333334c-6.4-6.4-10.666667-14.933333-10.666667-23.466666 0-8.533333 4.266667-17.066667 10.666667-23.466667l341.333333-309.333333c12.8-12.8 34.133333-10.666667 44.8 2.133333 12.8 12.8 10.666667 34.133333-2.133333 44.8L548.266667 522.666667l315.733333 285.866666c12.8 10.666667 14.933333 32 2.133333 44.8-6.4 6.4-14.933333 10.666667-23.466666 10.666667z" fill="#1677ff" p-id="5048"></path><path d="M512 864c-8.533333 0-14.933333-2.133333-21.333333-8.533333L149.333333 546.133333c-6.4-6.4-10.666667-14.933333-10.666666-23.466666 0-8.533333 4.266667-17.066667 10.666666-23.466667L490.666667 189.866667c12.8-12.8 34.133333-10.666667 44.8 2.133333 12.8 12.8 10.666667 34.133333-2.133334 44.8L217.6 522.666667 533.333333 808.533333c12.8 12.8 14.933333 32 2.133334 44.8-6.4 6.4-14.933333 10.666667-23.466667 10.666667z" fill="#1677ff" p-id="5049"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg t="1701245431007" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5230" width="200" height="200"><path d="M544 522.666667c0-8.533333-4.266667-17.066667-10.666667-23.466667L192 189.866667c-12.8-12.8-34.133333-10.666667-44.8 2.133333-12.8 12.8-10.666667 34.133333 2.133333 44.8l315.733334 285.866667L149.333333 808.533333c-12.8 12.8-14.933333 32-2.133333 44.8 6.4 6.4 14.933333 10.666667 23.466667 10.666667 8.533333 0 14.933333-2.133333 21.333333-8.533333l341.333333-309.333334c6.4-6.4 10.666667-14.933333 10.666667-23.466666z" fill="#1677ff" p-id="5231"></path><path d="M864 499.2l-341.333333-309.333333c-12.8-12.8-34.133333-10.666667-44.8 2.133333-12.8 12.8-10.666667 34.133333 2.133333 44.8l315.733333 285.866667-315.733333 285.866666c-12.8 12.8-14.933333 32-2.133333 44.8 6.4 6.4 14.933333 10.666667 23.466666 10.666667 8.533333 0 14.933333-2.133333 21.333334-8.533333l341.333333-309.333334c6.4-6.4 10.666667-14.933333 10.666667-23.466666 0-8.533333-4.266667-17.066667-10.666667-23.466667z" fill="#1677ff" p-id="5232"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -10,6 +10,8 @@
6. total & perPage & activePage
7. showPerPage & perPageAvailable & showPageInput
8. disabled
9. size
10. ellipsisPageGap
*/
import {fireEvent, render, waitFor, within} from '@testing-library/react';
@ -364,3 +366,57 @@ test('Renderer:Pagination with disabled', async () => {
replaceReactAriaIds(container);
expect(container).toMatchSnapshot();
});
// 9.组件尺寸
test('pagination: Pagination with size', async () => {
const {container} = render(
amisRender(
{
type: 'service',
body: [
{
type: 'pagination',
size: 'small'
}
]
},
{},
makeEnv({})
)
);
const paginationEl = container.querySelector('.cxd-Pagination-wrap');
expect(paginationEl).toHaveClass('cxd-Pagination-wrap-size--small');
});
// 10.多页跳转页数
test('pagination: Pagination with ellipsisPageGap', async () => {
const pageChange = jest.fn();
const {container} = render(
amisRender(
{
type: 'service',
body: [
{
type: 'pagination',
layout: 'total,perPage,pager,go',
mode: 'normal',
activePage: 1,
lastPage: 99999,
total: 999,
perPage: 1,
ellipsisPageGap: 7,
onPageChange: pageChange
}
]
},
{},
makeEnv({})
)
);
const ellipsisEL = container.querySelector('.ellipsis');
fireEvent.click(ellipsisEL!);
await wait(200);
expect(pageChange).toBeCalled();
});

View File

@ -9,7 +9,7 @@ exports[`Renderer:Pagination 1`] = `
class="cxd-PaginationWrapper"
>
<div
class="cxd-Pagination-wrap cxd-PaginationWrapper-pager"
class="cxd-Pagination-wrap cxd-Pagination-wrap-size--default cxd-PaginationWrapper-pager"
>
<ul
class="cxd-Pagination cxd-Pagination--sm cxd-Pagination-item"
@ -261,7 +261,7 @@ exports[`Renderer:Pagination 2`] = `
class="cxd-Service"
>
<div
class="cxd-Pagination-wrap"
class="cxd-Pagination-wrap cxd-Pagination-wrap-size--default"
>
<div
class="cxd-Pagination-total cxd-Pagination-item"
@ -323,6 +323,14 @@ exports[`Renderer:Pagination 2`] = `
>
...
</a>
<span
class="icon"
>
<icon-mock
classname="icon icon-arrow-double-left"
icon="arrow-double-left"
/>
</span>
</li>
<li
class="cxd-Pagination-pager-item"
@ -350,6 +358,14 @@ exports[`Renderer:Pagination 2`] = `
>
...
</a>
<span
class="icon"
>
<icon-mock
classname="icon icon-arrow-double-right"
icon="arrow-double-right"
/>
</span>
</li>
<li
class="cxd-Pagination-pager-item"
@ -401,7 +417,7 @@ exports[`Renderer:Pagination with disabled 1`] = `
class="cxd-Service"
>
<div
class="cxd-Pagination-wrap disabled"
class="cxd-Pagination-wrap cxd-Pagination-wrap-size--default disabled"
>
<ul
class="cxd-Pagination cxd-Pagination--sm cxd-Pagination-item"
@ -487,6 +503,14 @@ exports[`Renderer:Pagination with disabled 1`] = `
>
...
</a>
<span
class="icon"
>
<icon-mock
classname="icon icon-arrow-double-right"
icon="arrow-double-right"
/>
</span>
</li>
<li
class="cxd-Pagination-pager-item"
@ -519,7 +543,7 @@ exports[`Renderer:Pagination with layout 1`] = `
class="cxd-Service"
>
<div
class="cxd-Pagination-wrap"
class="cxd-Pagination-wrap cxd-Pagination-wrap-size--default"
>
<div
class="cxd-Pagination-total cxd-Pagination-item"
@ -654,6 +678,14 @@ exports[`Renderer:Pagination with layout 1`] = `
>
...
</a>
<span
class="icon"
>
<icon-mock
classname="icon icon-arrow-double-right"
icon="arrow-double-right"
/>
</span>
</li>
<li
class="cxd-Pagination-pager-item"
@ -686,7 +718,7 @@ exports[`Renderer:Pagination with maxButtons 1`] = `
class="cxd-Service"
>
<div
class="cxd-Pagination-wrap"
class="cxd-Pagination-wrap cxd-Pagination-wrap-size--default"
>
<ul
class="cxd-Pagination cxd-Pagination--sm cxd-Pagination-item"
@ -718,6 +750,14 @@ exports[`Renderer:Pagination with maxButtons 1`] = `
>
...
</a>
<span
class="icon"
>
<icon-mock
classname="icon icon-arrow-double-left"
icon="arrow-double-left"
/>
</span>
</li>
<li
class="cxd-Pagination-pager-item"
@ -799,6 +839,14 @@ exports[`Renderer:Pagination with maxButtons 1`] = `
>
...
</a>
<span
class="icon"
>
<icon-mock
classname="icon icon-arrow-double-right"
icon="arrow-double-right"
/>
</span>
</li>
<li
class="cxd-Pagination-pager-item"
@ -1032,6 +1080,21 @@ exports[`Renderer:Pagination with simple mode 1`] = `
/>
</span>
</li>
<li
class="cxd-Pagination-inputSimple"
>
<input
class="cxd-Pagination-inputSimple-input"
type="text"
value="1"
/>
/
<span
class="cxd-Pagination-inputSimple-right"
>
1
</span>
</li>
<li
class="cxd-Pagination-next is-disabled"
>

View File

@ -77,6 +77,7 @@ export class PaginationWrapper extends React.Component<PaginationWrapProps> {
props.store.syncProps(props, undefined, [
'perPage',
'mode',
'ellipsisPageGap',
'inputName',
'outputName'
]);
@ -87,6 +88,7 @@ export class PaginationWrapper extends React.Component<PaginationWrapProps> {
store.syncProps(this.props, prevProps, [
'perPage',
'mode',
'ellipsisPageGap',
'inputName',
'outputName'
]);
@ -114,6 +116,7 @@ export class PaginationWrapper extends React.Component<PaginationWrapProps> {
activePage: store.page,
lastPage: store.lastPage,
mode: store.mode,
ellipsisPageGap: store.ellipsisPageGap,
onPageChange: store.switchTo,
perPage: store.perPage,
className: 'PaginationWrapper-pager'