首页 > 解决方案 > 反应中的Spring Boot分页实现

问题描述

大家好 !我希望你一切都好 ?

我目前正在开发一个简单的应用程序来了解更多关于 spring-boot、spring-security、jpa 和服务器端分页的信息。

我成功构建了我的 API,并且我有一个使用分页的控制器。为了使用这个 API,我开发了一个 react 应用程序,一切都按照我的意愿运行(2FA、帐户创建、登录、忘记密码、更改密码……)。

但是现在,我想开发一个管理界面,我希望能够在其中管理用户帐户。

因此,我创建了一个需要管理员角色并进行连接的控制器。要返回用户列表,我使用扩展 PagingAndSortingRepository 的 DAO,它工作正常!

现在我想在我的反应应用程序中实现这个分页,这就是我遇到问题的地方。

我已经尝试了许多分页库来做出反应。但是他们都需要在一个列表中恢复所有数据,这是我不想要的。于是,我开始开发自己的分页组件。我可以轻松地进行正面分页,但仅用于访问第一页、最后一页、前一页和后一页,但是我不能像这样添加页面选择按钮:1 3 [...][n- 2][n-1][n]。

目前我的分页组件看起来像这样:我的分页组件 我想要这样的东西:在此处输入图像描述

这是我的代码:

用户.java

@Entity
@Table(
    name = "USER",
    uniqueConstraints = {
            @UniqueConstraint(columnNames = "username"),
            @UniqueConstraint(columnNames = "email")
    }
)
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    @Size(min = 3, max = 16)
    private String username;

    @NaturalId
    @NotBlank
    @Email
    private String email;

    @NotBlank
    @Size(max = 100)
    private String password;

    private boolean isUsingTwoFA;

    private String twoFASecret;

    @Column(columnDefinition = "DATE NOT NULL")
    private LocalDate accountCreationDate;

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
        name = "USER_ROLE",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<Role> roles = new HashSet<>();
}

UserDto.java

public class UserDto {

    private String username;
    private String email;
    private String accountCreationDate;
    private boolean isUsingTwoFA;
    List<String> roles;
}

PagedUserRepository.java

public interface PagedUserRepository extends PagingAndSortingRepository<User, Long> {

    Page<User> findAll(Pageable pageable);
}

仪表板用户服务.java

@Service
public class DashboardUsersService {

    private UserRepository userRepository;
    private PagedUserRepository pagedUserRepository;

    @Autowired
    public DashboardUsersService(UserRepository userRepository, PagedUserRepository pagedUserRepository) {
        this.userRepository = userRepository;
        this.pagedUserRepository = pagedUserRepository;
    }

    public ResponseEntity<?> getUsers(int page, int size) {

        Pageable pageable = PageRequest.of(page, size);
        Page<User> usersPage = pagedUserRepository.findAll(pageable);

        if (usersPage.getContent().isEmpty()) {
            return new ResponseEntity<>(new ApiResponseDto(false, "Unable to retrieve any user"), HttpStatus.INTERNAL_SERVER_ERROR);
        }

        final List<UserDto> users = usersPage.getContent()
                .stream()
                .map(UserDto::new)
                .collect(Collectors.toList());

        return new ResponseEntity<>(new PagedResponseDto(users, usersPage), HttpStatus.OK);
    }
}

仪表板用户控制器.java

@CrossOrigin(maxAge = 36000)
@RestController
@RequestMapping(path = "/api/secure/admin/dashboard/users")
public class DashboardUsersController {

    @Autowired
    private DashboardUsersService dashboardUsersService;

    @Secured("ROLE_ADMIN")
    @GetMapping
    public ResponseEntity<?> getUsers(
            @RequestParam(value = "page", defaultValue = "0") int page,
            @RequestParam(value = "size", defaultValue = "10") int size
    ) {
        return dashboardUsersService.getUsers(page, size);
    }
}

用户.js

import React, {Component} from 'react';
import {withRouter} from 'react-router-dom';
import {getPageUsers} from "../../../../api/AdminApi";
import UserTableLine from "./component/UserTableLine";
import UserPagination from "./component/UserPagination";

class Users extends Component{

    state = {
        pagedResponse: {},
        users: [],
        showLoading: false
    };

    constructor(props){
        super(props);

        this.getFirstPageUsers = this.getFirstPageUsers.bind(this);
        this.handleChangePage = this.handleChangePage.bind(this);
    }

    componentDidMount(){
        document.title = "Users management";
        this.getFirstPageUsers();
    }

    getFirstPageUsers(){
        const defaultPageable = {
            pageNumber: 0
        };
        this.setState({showLoading: true});
        getPageUsers(defaultPageable).then(res => {
            this.setState({
                pagedResponse: res,
                users: res.content,
                showLoading: false
            });
        }).catch(error => {
            if(error.message && error.success === false){
                this.props.showAlert(error.message, "error");
            } else {
                this.props.showAlert("Sorry! Something went wrong. Please try again!", "error");
            }
            this.setState({showLoading: false});
            console.log(error);
        });
    }

    handleChangePage(pageable){
        this.setState({showLoading: true});
        getPageUsers(pageable).then(res => {
            this.setState({
                pagedResponse: res,
                users: res.content,
                showLoading: false
            });
        }).catch(error => {
            if(error.message && error.success === false){
                this.props.showAlert(error.message, "error");
            } else {
                this.props.showAlert("Sorry! Something went wrong. Please try again!", "error");
            }
            this.setState({showLoading: false});
            console.log(error);
        });
    }

    render(){

        let tableLines = [];

        if(this.state.pagedResponse && this.state.users.length > 0){
            tableLines = Object.keys(this.state.users)
                .map(key => <UserTableLine key={key} user={this.state.users[key]}/>);
        }

        return(
            <div>
                <h1>Users <span className="text-muted" style={{fontSize: 11}}>management</span></h1>
                <hr/>
                {
                    this.state.showLoading
                    ?
                        <div className="align-content-center text-center">
                            <h4 className="text-muted">Loading. Please Wait...</h4>
                            <i className="material-icons w3-xxxlarge w3-spin align-content-center">refresh</i>
                        </div>
                    :
                        <div>
                            <table className="table table-hover">
                                <thead>
                                    <tr>
                                        <th scope="col">Avatar</th>
                                        <th scope="col">Username</th>
                                        <th scope="col">email</th>
                                        <th scope="col">Action</th>
                                    </tr>
                                </thead>
                                <tbody>
                                {tableLines}
                                </tbody>
                            </table>
                            <UserPagination
                                showAlert={this.props.showAlert}
                                page={this.state.pagedResponse}
                                handleChangePage={this.handleChangePage}
                            />
                        </div>
                }
            </div>
        );
    }
}

export default withRouter(Users);

UserTableLine.js

import React, {Component} from 'react';
import {withRouter} from 'react-router-dom';

import {Modal, ModalBody, ModalHeader} from 'reactstrap';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
import {faSearch} from '@fortawesome/free-solid-svg-icons';

class UserTableLine extends Component {

    state = {
        showModalUserInfo: false,
        user: {}
    };

    constructor(props) {
        super(props);

        this.toggle = this.toggle.bind(this);
    }

    componentDidMount() {
        this.setState({
            user: this.props.user
        });
    }

    toggle() {
        this.setState({
            showModalUserInfo: !this.state.showModalUserInfo
        });
    }

    render() {

        let roles;

        if (this.state.user && this.state.user.roles) {
            roles = Object.keys(this.state.user.roles).map(
                key => " " + this.state.user.roles[key]
            );
        }

        return (
            <tr>
                <th scope="row">
                    <img src={"http://cravatar.eu/helmavatar/" + this.state.user.username + "/32.png"}
                     alt={this.state.user.username} className="img-fluid"/>
                </th>
                <th>
                    {this.state.user.username}
                </th>
                <th>
                    {this.state.user.email}
                </th>
                <th>
                    <button className="btn btn-dark" onClick={this.toggle}><FontAwesomeIcon icon={faSearch}/></button>
                </th>

                <Modal isOpen={this.state.showModalUserInfo} toggle={this.toggle} className={this.props.className}>
                    <ModalHeader toggle={this.toggle}>
                        <div className="align-content-center align-items-center align-self-center text-center">
                            <img src={"http://cravatar.eu/helmavatar/" + this.state.user.username + "/50.png"}
                             alt={this.state.user.username} className="img-fluid rounded align-self-center"/>
                        {" " + this.state.user.username + ' { ' + roles + ' }'}
                        </div>
                    </ModalHeader>
                    <ModalBody>
                        <p>
                            <b>Email adresse:</b> {this.state.user.email}
                        </p>
                        <p>
                            <b>Account creation date:</b> {this.state.user.accountCreationDate}
                        </p>
                        <p>
                            <b>2FA status:</b>
                            {
                                this.state.user.usingTwoFA
                                ?
                                    <span className="badge badge-success">enabled</span>
                                :
                                    <span className="badge badge-danger">disabled</span>
                            }
                        </p>
                    </ModalBody>
                </Modal>
            </tr>
        );
    }
}

export default withRouter(UserTableLine);

最后,UserPagination.js

import React, {Component} from 'react';
import {withRouter} from 'react-router-dom';

class UserPagination extends Component {

    state = {
        pagination: {}
    };

    constructor(props) {
        super(props);

        this.onPageChange = this.onPageChange.bind(this);
        this.goToFirstPage = this.goToFirstPage.bind(this);
        this.goToLastPage = this.goToLastPage.bind(this);
        this.goToPreviousPage = this.goToPreviousPage.bind(this);
        this.goToNextPage = this.goToNextPage.bind(this);
        this.setStatePromise = this.setStatePromise.bind(this);
    }

    componentDidMount() {
        const pagination = {
            firstPage: this.props.page.firstPage,
            lastPage: this.props.page.lastPage,
            currentPageable: {
                sort: {
                    sorted: false,
                    unsorted: true
                },
                offset: this.props.page.offset,
                pageSize: this.props.page.pageSize,
                pageNumber: this.props.page.number
            },
            previousPageable: this.props.page.previousPageable,
            nextPageable: this.props.page.nextPageable,
            totalPages: this.props.page.totalPages,
            totalElement: this.props.page.totalElement
        };
        this.setState({pagination});
    }

    setStatePromise(newState) {
        return new Promise((resolve) => {
            this.setState(newState, () => {
                resolve();
            });
        });
    }

    onPageChange = (pageable) => {
        this.props.handleChangePage(pageable);
    };

    goToFirstPage() {
        const firstPage = {
            sort: {
                sorted: false,
                unsorted: true
            },
            offset: 0,
            pageSize: 10,
            pageNumber: 0
        };
        this.onPageChange(firstPage);
    }

    goToLastPage() {
        const lastPage = {
            sort: {
                sorted: false,
                unsorted: true
            },
            pageSize: 10,
            pageNumber: this.state.pagination.totalPages - 1
        };
        this.onPageChange(lastPage);
    }

    goToPreviousPage() {
        const previousPage = this.state.pagination.previousPageable;
        if (previousPage !== "INSTANCE") {
            this.onPageChange(previousPage);
        }
    }

    goToNextPage() {
        const nextPage = this.state.pagination.nextPageable;
        if (nextPage !== "INSTANCE") {
            this.onPageChange(nextPage);
        }
    }

    getPagesNumberButtons(){
        let pages = [];
        if (this.state.pagination) {
            pages.push(
                <li key={1} className="page-item active">
                    <p className="page-link">{this.state.pagination.currentPageable.pageNumber}</p>
                </li>
            );
        }
        return pages;
    }

    render() {

        return (
            <div>
                <ul className="pagination">
                    <li className="page-item" onClick={this.goToFirstPage}>
                        <p className="page-link">&laquo;</p>
                    </li>
                    <li className="page-item" onClick={this.goToPreviousPage}>
                        <p className="page-link">Prev</p>
                    </li>
                    {this.getPagesNumberButtons}
                    <li id="nextPage" className="page-item" onClick={this.goToNextPage}>
                        <p className="page-link">Next</p>
                    </li>
                    <li id="lastPage" className="page-item" onClick={this.goToLastPage}>
                        <p className="page-link">&raquo;</p>
                    </li>
                </ul>
            </div>
        );
    }

}

export default withRouter(UserPagination);

但如果你更喜欢完整的代码:Github

如果您有解决我的问题的想法,或者您知道处理服务器端分页的库,我很感兴趣 :)

预先感谢您花时间阅读所有这些内容,更感谢您的帮助。

亚历克西斯

编辑 1:这是我从控制器得到的响应:

{
    "content": [
        {
            "username": "test1",
            "email": "test1@gmail.com",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        },
        {
            "username": "test2",
            "email": "test2@gmail.com",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        },
        {
            "username": "test3",
            "email": "test3@gmail.com",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        },
        {
            "username": "test4",
            "email": "test4@gmail.com",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        },
        {
            "username": "test5",
            "email": "test5@gmail.com",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        },
        {
            "username": "test6",
            "email": "test6@gmail.com",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        },
        {
            "username": "test7",
            "email": "test7@gmail.com",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        },
        {
            "username": "test8",
            "email": "test8@gmail.com",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        },
        {
            "username": "test9",
            "email": "test9@gmail.com",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        },
        {
            "username": "test10",
            "email": "test10@gmail.com",
            "accountCreationDate": "2018-08-22",
            "roles": [
                "USER"
            ],
            "usingTwoFA": false
        }
    ],
    "offset": 0,
    "pageNumber": 0,
    "pageSize": 10,
    "lastPage": false,
    "totalElement": 24,
    "totalPages": 3,
    "size": 10,
    "number": 0,
    "numberOfElements": 10,
    "firstPage": true,
    "previousPageable": "INSTANCE",
    "nextPageable": {
        "sort": {
            "sorted": false,
            "unsorted": true
        },
        "offset": 10,
        "pageSize": 10,
        "pageNumber": 1,
        "paged": true,
        "unpaged": false
    }
}

标签: springreactjsspring-bootpaginationspring-data-jpa

解决方案


在与@SGhaleb 互动后,我终于为这种分页开发了自己的组件并且它可以工作。这不是一个最佳解决方案(+ 1k 行代码),但对于第一个版本来说已经足够了,我稍后会对其进行优化。

这是所述组件的代码:

import React, {Component} from 'react';

class UserPagination extends Component {

    constructor(props) {
        super(props);

        this.state = {
            page: props.page,
            pageSize: props.pageSize,
            currentPage: props.currentPage,
            totalNumberOfElements: props.totalNumberOfElements
        };

        this.onPageChange = this.onPageChange.bind(this);
        this.goToFirstPage = this.goToFirstPage.bind(this);
        this.goToLastPage = this.goToLastPage.bind(this);
        this.goToPreviousPage = this.goToPreviousPage.bind(this);
        this.goToNextPage = this.goToNextPage.bind(this);
        this.buildPagination = this.buildPagination.bind(this);
    }

    onPageChange = (pageNumber) => {
        this.props.handleChangePage(pageNumber);
    };

    static getDerivedStateFromProps(props, state) {
        state = props;
        return state;
    }

    goToFirstPage() {
        this.onPageChange(0);
    }

    goToLastPage() {
        this.onPageChange(this.state.page.totalNumberOfPages - 1);
    }

    goToPreviousPage() {
        const previousPage = this.state.page.previousPageable;
        if (previousPage !== "INSTANCE") {
            this.onPageChange(previousPage.pageNumber);
        }
    }

    goToNextPage() {
        const {currentPage, page} = this.state;
        const nextPage = page.nextPageable;
        if (nextPage !== "INSTANCE") {
            this.onPageChange(currentPage + 1);
        }
    }

    buildPagination(page, currentPage) {
        //PAGINATION LOGIC
        //SEE LINK TO PASTEBIN.COM
    }

    render() {

        const {page, currentPage} = this.state;

        let pagination = this.buildPagination(page, currentPage);


        return (
            <ul className="pagination">
                {pagination}
            </ul>
        );
    }
}

export default UserPagination;

https://pastebin.com/x4Fx9pLm

现在分页看起来像我想要的:

分页组件


推荐阅读