From e3003f2a48c19298782ebecc4e8caf37b29e83fe Mon Sep 17 00:00:00 2001 From: xst Date: Fri, 22 Dec 2023 18:31:06 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.test.js | 10 +- src/actions/PlayersActions.js | 16 ++- src/components/AddPlayerInput.css | 1 + src/components/AddPlayerInput.js | 125 +++++++++++++++------ src/components/Pagination.css | 28 +++++ src/components/Pagination.js | 87 +++++++++++++++ src/components/PlayerList.css | 3 +- src/components/PlayerList.js | 46 ++++---- src/components/PlayerListItem.js | 90 ++++++++------- src/components/SearchContent.js | 71 ++++++++++++ src/components/index.js | 8 +- src/constants/ActionTypes.js | 7 +- src/containers/PlayerListApp.js | 161 ++++++++++++++++++++------- src/hooks/index.js | 5 + src/hooks/useFirstMount.js | 18 +++ src/hooks/useMount.js | 5 + src/hooks/useStorageState.js | 86 ++++++++++++++ src/hooks/useUpdateDebounceEffect.js | 31 ++++++ src/hooks/useUpdateEffect.js | 12 ++ src/reducers/playerlist.js | 75 +++++++++---- 20 files changed, 706 insertions(+), 179 deletions(-) create mode 100644 src/components/Pagination.css create mode 100644 src/components/Pagination.js create mode 100644 src/components/SearchContent.js create mode 100644 src/hooks/index.js create mode 100644 src/hooks/useFirstMount.js create mode 100644 src/hooks/useMount.js create mode 100644 src/hooks/useStorageState.js create mode 100644 src/hooks/useUpdateDebounceEffect.js create mode 100644 src/hooks/useUpdateEffect.js diff --git a/src/App.test.js b/src/App.test.js index b84af98..c9ac3ef 100755 --- a/src/App.test.js +++ b/src/App.test.js @@ -1,8 +1,8 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; +import React from "react"; +import ReactDOM from "react-dom"; +import App from "./App"; -it('renders without crashing', () => { - const div = document.createElement('div'); +it("renders without crashing", () => { + const div = document.createElement("div"); ReactDOM.render(, div); }); diff --git a/src/actions/PlayersActions.js b/src/actions/PlayersActions.js index 8f427c2..20818fd 100755 --- a/src/actions/PlayersActions.js +++ b/src/actions/PlayersActions.js @@ -1,9 +1,9 @@ -import * as types from '../constants/ActionTypes'; +import * as types from "../constants/ActionTypes"; -export function addPlayer(name) { +export function addPlayer(playerInfo) { return { type: types.ADD_PLAYER, - name, + playerInfo, }; } @@ -20,3 +20,13 @@ export function starPlayer(id) { id, }; } + +// 获取最新球员的信息 +export const getLatestPlayerData = ({ page, pageSize, keyword }) => { + return { + type: types.GET_LATEST_PLAYER_LIST, + page, + pageSize, + keyword, + }; +}; diff --git a/src/components/AddPlayerInput.css b/src/components/AddPlayerInput.css index 1507c6e..919190b 100755 --- a/src/components/AddPlayerInput.css +++ b/src/components/AddPlayerInput.css @@ -1,4 +1,5 @@ :local(.addPlayerInput) { + width: 200px; border-radius: 0; border-color: #abaaaa; border-left: 0; diff --git a/src/components/AddPlayerInput.js b/src/components/AddPlayerInput.js index 5d914d8..3ebe22f 100755 --- a/src/components/AddPlayerInput.js +++ b/src/components/AddPlayerInput.js @@ -1,45 +1,100 @@ -import React, { Component } from 'react'; -import classnames from 'classnames'; -import PropTypes from 'prop-types'; -import styles from './AddPlayerInput.css'; +import React, { useState } from "react"; +import classnames from "classnames"; +import PropTypes from "prop-types"; +import styles from "./AddPlayerInput.css"; -class AddPlayerInput extends Component { - render() { - return ( - - ); - } +// 定义球员位置 +const PLAYER_POSITION = ["SF", "PF", "PG", "SG"]; +// 定义球队 +const PLAYER_TEAM = [ + "LOS ANGELES LAKERS", + "GOLDEN STATE WARRIORS", + "NEW ORLEANS PELICANS", + "HOUSTON ROCKETS", + "TORONTO RAPTORS", +]; - constructor(props, context) { - super(props, context); - this.state = { - name: this.props.name || '', - }; - } +const AddPlayerInput = ({ addPlayer, addPlayerSuccessCallback }) => { + // 球员基本信息 + const [playerInfo, setPlayerInfo] = useState({ + name: "", + position: PLAYER_POSITION[0], + team: PLAYER_TEAM[0], + }); - handleChange(e) { - this.setState({ name: e.target.value }); - } - - handleSubmit(e) { - const name = e.target.value.trim(); - if (e.which === 13) { - this.props.addPlayer(name); - this.setState({ name: '' }); + const handleSubmit = (e) => { + if (e.which === 13 && playerInfo?.name !== "") { + addPlayer(playerInfo); + setPlayerInfo({ + name: "", + position: PLAYER_POSITION[0], + team: PLAYER_TEAM[0], + }); + addPlayerSuccessCallback(); } - } -} + }; + + return ( + <> +

add player:

+
+ 球员姓名: + + setPlayerInfo({ ...playerInfo, name: e.target.value.trim() }) + } + onKeyDown={handleSubmit} + /> + 球员位置: + + 所属团队: + +
+ + ); +}; AddPlayerInput.propTypes = { addPlayer: PropTypes.func.isRequired, + addPlayerSuccessCallback: PropTypes.func.isRequired, }; export default AddPlayerInput; diff --git a/src/components/Pagination.css b/src/components/Pagination.css new file mode 100644 index 0000000..c6174a8 --- /dev/null +++ b/src/components/Pagination.css @@ -0,0 +1,28 @@ +.pagination-container { + display: flex; + justify-content: center; + margin-top: 20px; + align-items: center; +} + +button { + padding: 8px; + margin: 0 5px; + border: 1px solid #ddd; + background-color: #f9f9f9; + cursor: pointer; +} + +button:hover { + background-color: #ddd; +} + +button:disabled { + color: #999; + cursor: not-allowed; +} + +button.active { + background-color: #007bff; + color: #fff; +} diff --git a/src/components/Pagination.js b/src/components/Pagination.js new file mode 100644 index 0000000..dea04d0 --- /dev/null +++ b/src/components/Pagination.js @@ -0,0 +1,87 @@ +/** + * 分页器,按照 antd 的用法来写的 + */ + +import React, { useState, useEffect } from "react"; +import PropTypes from "prop-types"; +import "./Pagination.css"; // 根据需要调整样式导入 + +/** + * current:当前页数 + * pageSize:每页条数 + * total:总数 + * onChange:改变页码的回调 + */ +const Pagination = ({ current = 1, pageSize = 5, total, onChange }) => { + // 使用状态钩子来追踪当前页码 + const [currentPage, setCurrentPage] = useState(current); + + // 计算总页数 + const totalPages = Math.ceil(total / pageSize); + + // 处理页码变化的函数 + const handlePageChange = (newPage) => { + // 确保新页码在有效范围内,并且与当前页码不同 + if (newPage >= 1 && newPage <= totalPages && newPage !== currentPage) { + setCurrentPage(newPage); + // 调用传入的 onChange 回调函数处理页码变化 + onChange(newPage); + } + }; + + // 渲染页码按钮 + const renderPageNumbers = () => { + const pageNumbers = []; + for (let i = 1; i <= totalPages; i++) { + pageNumbers.push( + + ); + } + return pageNumbers; + }; + + // 当传入的 current 发生变化时更新当前页码 + useEffect(() => { + setCurrentPage(current); + }, [current]); + + return ( +
+
+ {pageSize}条/页 + + + {renderPageNumbers()} + + + 共: {total}条 +
+
+ ); +}; + +// 定义组件的属性类型 +Pagination.propTypes = { + current: PropTypes.number, + pageSize: PropTypes.number, + total: PropTypes.number.isRequired, + onChange: PropTypes.func.isRequired, +}; + +export default Pagination; diff --git a/src/components/PlayerList.css b/src/components/PlayerList.css index b8d6f00..403ee77 100755 --- a/src/components/PlayerList.css +++ b/src/components/PlayerList.css @@ -1,4 +1,5 @@ -:local(.playerList) { +.player-list { padding-left: 0; margin-bottom: 0; + height: 400px; } diff --git a/src/components/PlayerList.js b/src/components/PlayerList.js index 7b40246..efd619b 100755 --- a/src/components/PlayerList.js +++ b/src/components/PlayerList.js @@ -1,29 +1,25 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import styles from './PlayerList.css'; -import PlayerListItem from './PlayerListItem'; +import React from "react"; +import PropTypes from "prop-types"; +import styles from "./PlayerList.css"; +import PlayerListItem from "./PlayerListItem"; -class PlayerList extends Component { - render() { - return ( - - ); - } -} +const PlayerList = ({ players, actions }) => { + return ( + + ); +}; PlayerList.propTypes = { players: PropTypes.array.isRequired, diff --git a/src/components/PlayerListItem.js b/src/components/PlayerListItem.js index ec9758c..9c2d704 100755 --- a/src/components/PlayerListItem.js +++ b/src/components/PlayerListItem.js @@ -1,45 +1,56 @@ -import React, { Component } from 'react'; -import classnames from 'classnames'; -import PropTypes from 'prop-types'; -import styles from './PlayerListItem.css'; +import React from "react"; +import classnames from "classnames"; +import PropTypes from "prop-types"; +import styles from "./PlayerListItem.css"; -class PlayerListItem extends Component { - render() { - return ( -
  • -
    -
    - {this.props.name} -
    -
    - - {this.props.team} · {this.props.position} - -
    +const PlayerListItem = ({ + id, + name, + team, + position, + starred, + starPlayer, + deletePlayer, + getLatestPlayerData, +}) => { + return ( +
  • +
    +
    + {name}
    -
    - - +
    + + {team} · {position} +
    -
  • - ); - } -} + +
    + + +
    + + ); +}; PlayerListItem.propTypes = { id: PropTypes.number.isRequired, @@ -48,6 +59,7 @@ PlayerListItem.propTypes = { position: PropTypes.string.isRequired, starred: PropTypes.bool, starPlayer: PropTypes.func.isRequired, + deletePlayer: PropTypes.func.isRequired, }; export default PlayerListItem; diff --git a/src/components/SearchContent.js b/src/components/SearchContent.js new file mode 100644 index 0000000..0297647 --- /dev/null +++ b/src/components/SearchContent.js @@ -0,0 +1,71 @@ +/** + * 搜索区域 + */ + +import React from "react"; +import classnames from "classnames"; + +import styles from "./AddPlayerInput.css"; + +const SearchContent = ({ filterParams, setFilterParams }) => { + return ( +
    +

    search player:

    + +
    + 球员姓名: + + setFilterParams({ + ...filterParams, + keyword: e.target.value.trim(), + }) + } + /> + {/* 球员位置: + + 所属团队: + */} +
    +
    + ); +}; + +export default SearchContent; diff --git a/src/components/index.js b/src/components/index.js index 12ce117..8b15b6e 100755 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,3 +1,5 @@ -export { default as AddPlayerInput } from './AddPlayerInput'; -export { default as PlayerList } from './PlayerList'; -export { default as PlayerListItem } from './PlayerListItem'; +export { default as AddPlayerInput } from "./AddPlayerInput"; +export { default as PlayerList } from "./PlayerList"; +export { default as PlayerListItem } from "./PlayerListItem"; +export { default as Pagination } from "./Pagination"; +export { default as SearchContent } from "./SearchContent"; diff --git a/src/constants/ActionTypes.js b/src/constants/ActionTypes.js index b796fae..ed0e851 100755 --- a/src/constants/ActionTypes.js +++ b/src/constants/ActionTypes.js @@ -1,3 +1,4 @@ -export const ADD_PLAYER = 'ADD_PLAYER'; -export const STAR_PLAYER = 'STAR_PLAYER'; -export const DELETE_PLAYER = 'DELETE_PLAYER'; +export const ADD_PLAYER = "ADD_PLAYER"; +export const STAR_PLAYER = "STAR_PLAYER"; +export const DELETE_PLAYER = "DELETE_PLAYER"; +export const GET_LATEST_PLAYER_LIST = "GET_LATEST_PLAYER_LIST"; diff --git a/src/containers/PlayerListApp.js b/src/containers/PlayerListApp.js index 0e4bfa5..75a746e 100755 --- a/src/containers/PlayerListApp.js +++ b/src/containers/PlayerListApp.js @@ -1,41 +1,120 @@ -import React, { Component } from 'react'; -import styles from './PlayerListApp.css'; -import { connect } from 'react-redux'; - -import { addPlayer, deletePlayer, starPlayer } from '../actions/PlayersActions'; -import { PlayerList, AddPlayerInput } from '../components'; - -class PlayerListApp extends Component { - render() { - const { - playerlist: { playersById }, - } = this.props; - - const actions = { - addPlayer: this.props.addPlayer, - deletePlayer: this.props.deletePlayer, - starPlayer: this.props.starPlayer, - }; - - return ( -
    -

    NBA Players

    - - -
    - ); - } -} - -function mapStateToProps(state) { - return state; -} - -export default connect( - mapStateToProps, - { - addPlayer, - deletePlayer, - starPlayer, - }, -)(PlayerListApp); +import React from "react"; +import { connect } from "react-redux"; + +import { + useMount, + useUpdateDebounceEffect, + useUpdateEffect, + useStorageState, +} from "../hooks"; + +import { + PlayerList, + AddPlayerInput, + Pagination, + SearchContent, +} from "../components"; + +import { + addPlayer, + deletePlayer, + starPlayer, + getLatestPlayerData, +} from "../actions/PlayersActions"; + +import styles from "./PlayerListApp.css"; + +// 每页显示的条目数 +export const PAGE_SIZE = 5; + +const PlayerListApp = ({ showPlayersById, total, ...rest }) => { + // 当前的筛选参数,这里可以防止刷新搜索条件丢失,后期甚至可以做到通过 url 中的参数来定位搜索条件 + const [filterParams, setFilterParams] = useStorageState("filterParams", { + page: 1, + pageSize: PAGE_SIZE, + keyword: "", + }); + + // 这里是接口定义 + const actions = { + addPlayer: rest.addPlayer, + deletePlayer: rest.deletePlayer, + starPlayer: rest.starPlayer, + getLatestPlayerData: (params = filterParams) => + rest.getLatestPlayerData(params), + }; + + // 初始渲染时,请求列表数据 + useMount(() => { + actions.getLatestPlayerData(filterParams); + }); + + // 当改变 pageSize、keyword 时,需要重置分页 + useUpdateEffect(() => { + setFilterParams({ ...filterParams, page: 1 }); + }, [filterParams?.keyword, filterParams?.pageSize]); + + // 使用防抖的hook,当这些参数改变时,重新请求数据 + useUpdateDebounceEffect( + () => { + // 当搜索条件改变的时候,重置页码,并重新请求数据 + actions.getLatestPlayerData(filterParams); + }, + [filterParams?.keyword, filterParams?.page, filterParams?.pageSize], + 300 + ); + + return ( +
    +

    NBA Players

    + + + actions.getLatestPlayerData(filterParams) + } + /> +
    + +
    + + + setFilterParams({ ...filterParams, page: newPage }) + } + /> +
    + ); +}; + +const mapStateToProps = (store) => { + return store?.playerlist; +}; + +export default connect(mapStateToProps, { + addPlayer, + deletePlayer, + starPlayer, + getLatestPlayerData, +})(PlayerListApp); diff --git a/src/hooks/index.js b/src/hooks/index.js new file mode 100644 index 0000000..73aa38f --- /dev/null +++ b/src/hooks/index.js @@ -0,0 +1,5 @@ +export { useMount } from "./useMount"; +export { useUpdateDebounceEffect } from "./useUpdateDebounceEffect"; +export { useStorageState } from "./useStorageState"; +export { useFirstMount } from "./useFirstMount"; +export { useUpdateEffect } from "./useUpdateEffect"; diff --git a/src/hooks/useFirstMount.js b/src/hooks/useFirstMount.js new file mode 100644 index 0000000..113cbf5 --- /dev/null +++ b/src/hooks/useFirstMount.js @@ -0,0 +1,18 @@ +/** + * 判断是否是第一次渲染 + */ + +import { useRef } from "react"; + +export function useFirstMount() { + const isFirst = useRef(true); + + //如果是初次渲染 + if (isFirst.current) { + isFirst.current = false; + + return true; + } + + return isFirst.current; +} diff --git a/src/hooks/useMount.js b/src/hooks/useMount.js new file mode 100644 index 0000000..875e704 --- /dev/null +++ b/src/hooks/useMount.js @@ -0,0 +1,5 @@ +import { useEffect } from "react"; + +export const useMount = (callback) => { + useEffect(callback, []); +}; diff --git a/src/hooks/useStorageState.js b/src/hooks/useStorageState.js new file mode 100644 index 0000000..48cafac --- /dev/null +++ b/src/hooks/useStorageState.js @@ -0,0 +1,86 @@ +/** + * 用为缓存的hook,可以像setState一样使用 + */ +import { useState } from "react"; + +// 保存值到本地存储或会话存储 +const valueToStorageFun = ({ type, key, value }) => { + if (typeof window !== "undefined") { + switch (type) { + case "localStorage": + window.localStorage.setItem(key, JSON.stringify(value)); + break; + case "sessionStorage": + window.sessionStorage.setItem(key, JSON.stringify(value)); + break; + default: + window.localStorage.setItem(key, JSON.stringify(value)); + } + } +}; + +// 自定义 Hook,用于在组件中处理具有本地存储/会话存储支持的状态 +export function useStorageState(key, initialValue, options) { + const { priority = "local", type = "localStorage" } = { + priority: "local", + type: "localStorage", + ...options, + }; + + // 使用 useState 初始化状态值 + const [storedValue, setStoredValue] = useState(() => { + if (typeof window === "undefined") { + return initialValue; + } + try { + // 根据类型判断从何处读取数据 + let item; + switch (type) { + case "localStorage": + item = window.localStorage.getItem(key); + break; + case "sessionStorage": + item = window.sessionStorage.getItem(key); + break; + default: + item = window.localStorage.getItem(key); + } + + // 解析 state 并按优先级进行处理 + if (item) { + switch (priority) { + case "local": + return JSON.parse(item); + case "initialValue": + valueToStorageFun({ key, type, value: initialValue }); + return initialValue; + default: + return JSON.parse(item); + } + } else { + return initialValue; + } + } catch (error) { + console.error(error); + return initialValue; + } + }); + + // 定义 setValue 函数,用于更新状态值并同步到存储中 + const setValue = (value) => { + try { + // 和 useState 保持相同用法,支持函数和默认值 + const valueToStore = + value instanceof Function ? value(storedValue) : value; + // 保存 state + setStoredValue(valueToStore); + // 同步到 storage 中 + valueToStorageFun({ key, type, value: valueToStore }); + } catch (error) { + console.log(error); + } + }; + + // 返回状态值和更新函数 + return [storedValue, setValue]; +} diff --git a/src/hooks/useUpdateDebounceEffect.js b/src/hooks/useUpdateDebounceEffect.js new file mode 100644 index 0000000..f19357d --- /dev/null +++ b/src/hooks/useUpdateDebounceEffect.js @@ -0,0 +1,31 @@ +/** + * 忽略首次渲染、防抖 + */ + +import { useEffect, useRef, useCallback } from "react"; + +export const useUpdateDebounceEffect = (callback, dependencies, delay) => { + const isFirstRender = useRef(true); + + // 使用 useCallback 确保在依赖项变化时返回新的回调 + const debouncedCallback = useCallback(callback, dependencies); + + useEffect(() => { + // 首次渲染时不执行回调 + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + + // 设置一个定时器 + const timer = setTimeout(() => { + // 当定时器触发时执行回调 + debouncedCallback(); + }, delay); + + // 在每次 effect 运行之前清除上一个定时器 + return () => clearTimeout(timer); + + // 在依赖项或延迟改变时重新运行 effect + }, [debouncedCallback, delay]); +}; diff --git a/src/hooks/useUpdateEffect.js b/src/hooks/useUpdateEffect.js new file mode 100644 index 0000000..aef1494 --- /dev/null +++ b/src/hooks/useUpdateEffect.js @@ -0,0 +1,12 @@ +import { useEffect } from "react"; +import { useFirstMount } from "./useFirstMount"; + +export const useUpdateEffect = (effect, deps) => { + const isFirstMount = useFirstMount(); //判断是否是初次渲染 + + useEffect(() => { + if (!isFirstMount) { + return effect(); //二次渲染才执行 + } + }, deps); +}; diff --git a/src/reducers/playerlist.js b/src/reducers/playerlist.js index 1bc7457..f1e7ac7 100755 --- a/src/reducers/playerlist.js +++ b/src/reducers/playerlist.js @@ -1,65 +1,70 @@ -import * as types from '../constants/ActionTypes'; +import * as types from "../constants/ActionTypes"; const initialState = { + // 这里相当于是数据库 playersById: [ { - name: 'LeBron James', - team: 'LOS ANGELES LAKERS', - position: 'SF', + name: "LeBron James", + team: "LOS ANGELES LAKERS", + position: "SF", starred: true, }, { - name: 'Kevin Duran', - team: 'GOLDEN STATE WARRIORS', - position: 'SF', + name: "Kevin Duran", + team: "GOLDEN STATE WARRIORS", + position: "SF", starred: false, }, { - name: 'Anthony Davis', - team: 'NEW ORLEANS PELICANS', - position: 'PF', + name: "Anthony Davis", + team: "NEW ORLEANS PELICANS", + position: "PF", starred: false, }, { - name: 'Stephen Curry', - team: 'GOLDEN STATE WARRIORS', - position: 'PG', + name: "Stephen Curry", + team: "GOLDEN STATE WARRIORS", + position: "PG", starred: false, }, { - name: 'James Harden', - team: 'HOUSTON ROCKETS', - position: 'SG', + name: "James Harden", + team: "HOUSTON ROCKETS", + position: "SG", starred: false, }, { - name: 'Kawhi Leonard', - team: 'TORONTO RAPTORS', - position: 'SF', + name: "Kawhi Leonard", + team: "TORONTO RAPTORS", + position: "SF", starred: false, }, ], + total: 6, + // 这里是给前端显示的数据 + showPlayersById: [], }; export default function players(state = initialState, action) { switch (action.type) { case types.ADD_PLAYER: + const { playerInfo } = action; return { ...state, playersById: [ - ...state.playersById, { - name: action.name, - team: 'LOS ANGELES LAKERS', - position: 'SF', + name: playerInfo.name, + team: playerInfo.team, + position: playerInfo.position, }, + ...state.playersById, ], }; case types.DELETE_PLAYER: return { ...state, playersById: state.playersById.filter( - (item, index) => index !== action.id, + (item, index) => index !== action.id ), }; case types.STAR_PLAYER: @@ -71,6 +76,28 @@ export default function players(state = initialState, action) { playersById: players, }; + case types.GET_LATEST_PLAYER_LIST: + const { page, pageSize, keyword } = action; + // 先根据 keyword 去过滤数据 + const data = + state.playersById.filter((player) => + player?.name?.toLowerCase()?.includes(keyword?.toLowerCase()) + ) || []; + + // 计算切片的起始和结束索引 + const startIndex = (page - 1) * pageSize; + const endIndex = startIndex + pageSize; + + // 获取对应页码的数据切片 + const updatedPlayers = data.slice(startIndex, endIndex); + + // 这里是给前端返回的数据 + return { + ...state, + showPlayersById: updatedPlayers, + total: data?.length || 0, + }; + default: return state; } From 957e2f14170a79f298d393dcf71ed2e906926f0b Mon Sep 17 00:00:00 2001 From: xst Date: Fri, 22 Dec 2023 18:39:26 +0800 Subject: [PATCH 2/3] =?UTF-8?q?docs:=20=E8=AE=B0=E5=BD=95=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E7=9A=84=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 29cead6..df7eb86 100755 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ Building a user friendly interface with React.js, Redux/Mobx and LESS/SCSS. - Step1: clone this repository to your account. - Step2: finish the following tasks. -- Step3: Send Pull Request to repository *bridge5/react-examination*. +- Step3: Send Pull Request to repository _bridge5/react-examination_. ## Tasks -- Please add pagination support to the list when there are more than *5* entries. +- Please add pagination support to the list when there are more than _5_ entries. - Please add option to select position of a player SF/PG and display it. - Please add tests using your preferred testing tool (jest, enzyme, mocha, ...). - Please add some features that could help you show your personal abilities. @@ -24,4 +24,16 @@ Building a user friendly interface with React.js, Redux/Mobx and LESS/SCSS. - 第一步:克隆这个仓库到你自己的账号里。 - 第二步:完成下列任务。 -- 第三步:发送 * Pull Request* 到仓库 *bridge5/react-examination*。 \ No newline at end of file +- 第三步:发送 _ Pull Request_ 到仓库 _bridge5/react-examination_。 + +### 修改的点 + +1. 启动项目报错,经排查是 webpack 内部 createHash 函数有问题,更新 react-scripts 包可解决 +2. 由于该部分逻辑并不复杂,因此都改为函数式组件进行编写 +3. 将 store 当做接口层,封装 useMount hook,当初次渲染时 dispatch action 去拿到最新数据、渲染列表 +4. 增加:当输入 名称、定位、球队信息提交时,将数据传给 store,成功后清空输入框,并重新请求最新的列表数据 +5. 删除:当点击删除按钮时,需要将信息传给 store,删除成功后请求最新的列表数据,如果此时列表数据为空,判断 page,如果 page>1,则使用 page-1 重新请求列表数据 +6. 查找:封装 useStorageState、useUpdateEffect,当用户搜索时,可自动进行防抖,且忽略首次请求。当改变 pageSize、keyword 时,会重置分页 +7. 分页器:按照 antd 用法封装的简易分页器 + +- 这里面遇到的卡点:select 中的 onChange 触发时,获取到的 event.target.value 为空,经研究,跟 react 的版本有关系,16.12.0 版本应该用 e.nativeEvent.target.value 来取值 From 67e798b20b38abb28183564054be09564b7bc65f Mon Sep 17 00:00:00 2001 From: xst Date: Sat, 23 Dec 2023 17:22:05 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=E6=A0=B7=E5=BC=8F=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=EF=BC=8C=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 43 ++++++-- package.json | 9 +- src/actions/PlayersActions.js | 6 +- src/components/AddPlayerInput.css | 7 -- src/components/AddPlayerInput.js | 58 +++++------ src/components/AddPlayerInput.module.css | 32 ++++++ src/components/Pagination.js | 49 +++++---- src/components/PlayerList.js | 45 ++++++--- .../{PlayerList.css => PlayerList.module.css} | 1 + src/components/PlayerListItem.js | 30 +++--- ...ListItem.css => PlayerListItem.module.css} | 1 + src/components/SearchContent.js | 58 ++++++----- src/constants/index.js | 45 +++++++++ src/containers/PlayerListApp.js | 99 ++++++++++++------- ...erListApp.css => PlayerListApp.module.css} | 12 +-- src/hooks/useFirstMount.test.js | 26 +++++ src/reducers/playerlist.js | 50 +++++++--- 17 files changed, 390 insertions(+), 181 deletions(-) delete mode 100755 src/components/AddPlayerInput.css create mode 100755 src/components/AddPlayerInput.module.css rename src/components/{PlayerList.css => PlayerList.module.css} (78%) rename src/components/{PlayerListItem.css => PlayerListItem.module.css} (96%) create mode 100644 src/constants/index.js rename src/containers/{PlayerListApp.css => PlayerListApp.module.css} (64%) create mode 100644 src/hooks/useFirstMount.test.js diff --git a/README.md b/README.md index df7eb86..bc5f85e 100755 --- a/README.md +++ b/README.md @@ -26,14 +26,39 @@ Building a user friendly interface with React.js, Redux/Mobx and LESS/SCSS. - 第二步:完成下列任务。 - 第三步:发送 _ Pull Request_ 到仓库 _bridge5/react-examination_。 -### 修改的点 +## 修改 1. 启动项目报错,经排查是 webpack 内部 createHash 函数有问题,更新 react-scripts 包可解决 -2. 由于该部分逻辑并不复杂,因此都改为函数式组件进行编写 -3. 将 store 当做接口层,封装 useMount hook,当初次渲染时 dispatch action 去拿到最新数据、渲染列表 -4. 增加:当输入 名称、定位、球队信息提交时,将数据传给 store,成功后清空输入框,并重新请求最新的列表数据 -5. 删除:当点击删除按钮时,需要将信息传给 store,删除成功后请求最新的列表数据,如果此时列表数据为空,判断 page,如果 page>1,则使用 page-1 重新请求列表数据 -6. 查找:封装 useStorageState、useUpdateEffect,当用户搜索时,可自动进行防抖,且忽略首次请求。当改变 pageSize、keyword 时,会重置分页 -7. 分页器:按照 antd 用法封装的简易分页器 - -- 这里面遇到的卡点:select 中的 onChange 触发时,获取到的 event.target.value 为空,经研究,跟 react 的版本有关系,16.12.0 版本应该用 e.nativeEvent.target.value 来取值 +2. 整体思路 + - 由于该功能类似于留言板功能,因此增删改查、分页等为必要功能。这里将 PlayerListApp 组件作为容器组件,负责逻辑处理 + - 其他 添加、搜索、查看等 UI 组件负责渲染即可,不参与逻辑处理 + - 将 store 当做数据层,数据层需要返回两个数据:需要显示的列表数据 showPlayersById 和 总数据量 total,分页、过滤等逻辑都应在数据层做处理 + - 添加逻辑:当输入名称、定位、球队信息提交时,将数据传给 store,成功后清空输入框,并重新请求最新的列表数据 + - 删除逻辑:当点击删除按钮时,需要将 id 传给 数据处理层,删除成功后请求最新的列表数据。(这里需要注意:如果此时列表数据为空 且 page > 1,则使用 page-1 重新请求列表数据) + - 查找逻辑:封装 useMount 处理第一次渲染的逻辑请求,封装 useUpdateEffect 和 useUpdateDebounceEffect,当重新渲染时处理其中的逻辑 + - 分页器:按照 antd 用法封装的简易分页器 + - 加入 form 必填检验,自动生成 id 等,筛选框支持过滤收藏的等 +3. 样式修复 +4. 这里面遇到的卡点:select 中的 onChange 触发时,获取到的 event.target.value 为空,经研究,跟 react 的版本有关系,16.12.0 版本应该用 e.nativeEvent.target.value 来取值 +5. 用户体验优化:封装 useStorageState,该 hook 可用来缓存用户的查询参数,防止用户刷新或再次进入时查询数据丢失 + +## 自测用例 + +1. 添加球员 + - 当输入球员姓名、位置、团队时,列表有无正常显示 + - 当球员姓名为空时,应添加失败 +2. 删除球员 + - 当点击删除按钮,列表是否正常显示 + - 先选择第二页,将第二页删完,此时是否重置到了第一页且正常显示 +3. 查询列表 + - 输入球员姓名、位置、团队时,列表有无正常过滤 + - 当输入大小写时,列表有无正常过滤 + - 当改变搜索条件时,分页是否重置并正常显示数据 + - 当输入搜索条件后,刷新列表,搜索条件是否依然保留 +4. 收藏球员 + - 点击收藏按钮时,列表有无正常显示 + - 点击取消收藏按钮时,列表有无正常显示 + +## 单元测试 + +由于单元测试只是了解,而且在写的过程中库报错,简单判断下是因为 react-dom 包变更所致,因此只写了一个 useFirstMount.test.js diff --git a/package.json b/package.json index f6c7705..7167ffa 100755 --- a/package.json +++ b/package.json @@ -18,7 +18,12 @@ "redux": "^4.0.5" }, "devDependencies": { - "react-scripts": "3.4.0" + "react-scripts": "^5.0.1" }, - "browserslist": [">0.2%", "not dead", "not ie <= 11", "not op_mini all"] + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ] } diff --git a/src/actions/PlayersActions.js b/src/actions/PlayersActions.js index 20818fd..471938e 100755 --- a/src/actions/PlayersActions.js +++ b/src/actions/PlayersActions.js @@ -22,11 +22,9 @@ export function starPlayer(id) { } // 获取最新球员的信息 -export const getLatestPlayerData = ({ page, pageSize, keyword }) => { +export const getLatestPlayerData = (params) => { return { type: types.GET_LATEST_PLAYER_LIST, - page, - pageSize, - keyword, + ...params, }; }; diff --git a/src/components/AddPlayerInput.css b/src/components/AddPlayerInput.css deleted file mode 100755 index 919190b..0000000 --- a/src/components/AddPlayerInput.css +++ /dev/null @@ -1,7 +0,0 @@ -:local(.addPlayerInput) { - width: 200px; - border-radius: 0; - border-color: #abaaaa; - border-left: 0; - border-right: 0; -} diff --git a/src/components/AddPlayerInput.js b/src/components/AddPlayerInput.js index 3ebe22f..8c4f743 100755 --- a/src/components/AddPlayerInput.js +++ b/src/components/AddPlayerInput.js @@ -1,34 +1,27 @@ import React, { useState } from "react"; import classnames from "classnames"; import PropTypes from "prop-types"; -import styles from "./AddPlayerInput.css"; -// 定义球员位置 -const PLAYER_POSITION = ["SF", "PF", "PG", "SG"]; -// 定义球队 -const PLAYER_TEAM = [ - "LOS ANGELES LAKERS", - "GOLDEN STATE WARRIORS", - "NEW ORLEANS PELICANS", - "HOUSTON ROCKETS", - "TORONTO RAPTORS", -]; +import { PLAYER_POSITION, PLAYER_TEAM } from "../constants"; -const AddPlayerInput = ({ addPlayer, addPlayerSuccessCallback }) => { +import styles from "./AddPlayerInput.module.css"; + +const AddPlayerInput = ({ handleAddPlayer, addPlayerSuccessCallback }) => { // 球员基本信息 const [playerInfo, setPlayerInfo] = useState({ name: "", - position: PLAYER_POSITION[0], - team: PLAYER_TEAM[0], + position: PLAYER_POSITION[0].value, + team: PLAYER_TEAM[0].value, }); const handleSubmit = (e) => { if (e.which === 13 && playerInfo?.name !== "") { - addPlayer(playerInfo); + handleAddPlayer(playerInfo); + // 添加成功后初始化数据、并重新请求数据 setPlayerInfo({ name: "", - position: PLAYER_POSITION[0], - team: PLAYER_TEAM[0], + position: PLAYER_POSITION[0].value, + team: PLAYER_TEAM[0].value, }); addPlayerSuccessCallback(); } @@ -37,23 +30,22 @@ const AddPlayerInput = ({ addPlayer, addPlayerSuccessCallback }) => { return ( <>

    add player:

    -
    - 球员姓名: +
    + 球员姓名: setPlayerInfo({ ...playerInfo, name: e.target.value.trim() }) } onKeyDown={handleSubmit} /> - 球员位置: + 球员位置: - 所属团队: + 所属团队: @@ -93,7 +87,7 @@ const AddPlayerInput = ({ addPlayer, addPlayerSuccessCallback }) => { }; AddPlayerInput.propTypes = { - addPlayer: PropTypes.func.isRequired, + handleAddPlayer: PropTypes.func.isRequired, addPlayerSuccessCallback: PropTypes.func.isRequired, }; diff --git a/src/components/AddPlayerInput.module.css b/src/components/AddPlayerInput.module.css new file mode 100755 index 0000000..39c35de --- /dev/null +++ b/src/components/AddPlayerInput.module.css @@ -0,0 +1,32 @@ +h3 { + color: #fff; + font-size: 16px; +} + +.add-player-layout { + display: flex; + align-items: center; +} + +.label { + margin-left: 20px; + color: #e9eada; +} + +.add-player-layout :first-child { + margin-left: 0; +} + +.addPlayerInput { + width: 200px; + border-radius: 0; + border-color: #abaaaa; + border-left: 0; + border-right: 0; +} + +.select { + width: 200px; + height: 34px; + margin: "0 20px"; +} diff --git a/src/components/Pagination.js b/src/components/Pagination.js index dea04d0..c63d57e 100644 --- a/src/components/Pagination.js +++ b/src/components/Pagination.js @@ -11,8 +11,15 @@ import "./Pagination.css"; // 根据需要调整样式导入 * pageSize:每页条数 * total:总数 * onChange:改变页码的回调 + * hideSinglePage:一页时是否隐藏 */ -const Pagination = ({ current = 1, pageSize = 5, total, onChange }) => { +const Pagination = ({ + current = 1, + pageSize = 5, + total, + onChange, + hideSinglePage = false, +}) => { // 使用状态钩子来追踪当前页码 const [currentPage, setCurrentPage] = useState(current); @@ -37,6 +44,7 @@ const Pagination = ({ current = 1, pageSize = 5, total, onChange }) => { +
    + {pageSize}条/页 + - {renderPageNumbers()} + {renderPageNumbers()} - - 共: {total}条 -
    + + 共: {total}条
    ); }; diff --git a/src/components/PlayerList.js b/src/components/PlayerList.js index efd619b..86269b9 100755 --- a/src/components/PlayerList.js +++ b/src/components/PlayerList.js @@ -1,29 +1,44 @@ import React from "react"; import PropTypes from "prop-types"; -import styles from "./PlayerList.css"; +import styles from "./PlayerList.module.css"; import PlayerListItem from "./PlayerListItem"; -const PlayerList = ({ players, actions }) => { +const PlayerList = ({ players, ...rest }) => { return ( -
      - {players.map((player, index) => ( - - ))} +
        + {players?.length > 0 ? ( + players.map((player, index) => ( + + )) + ) : ( +
        + 暂无数据 +
        + )}
      ); }; PlayerList.propTypes = { players: PropTypes.array.isRequired, - actions: PropTypes.object.isRequired, + handleDeletePlayer: PropTypes.func.isRequired, + handleStarPlayer: PropTypes.func.isRequired, + successCallback: PropTypes.func.isRequired, }; export default PlayerList; diff --git a/src/components/PlayerList.css b/src/components/PlayerList.module.css similarity index 78% rename from src/components/PlayerList.css rename to src/components/PlayerList.module.css index 403ee77..bfd9ea5 100755 --- a/src/components/PlayerList.css +++ b/src/components/PlayerList.module.css @@ -2,4 +2,5 @@ padding-left: 0; margin-bottom: 0; height: 400px; + overflow-y: auto; } diff --git a/src/components/PlayerListItem.js b/src/components/PlayerListItem.js index 9c2d704..ee6b106 100755 --- a/src/components/PlayerListItem.js +++ b/src/components/PlayerListItem.js @@ -1,7 +1,7 @@ import React from "react"; import classnames from "classnames"; import PropTypes from "prop-types"; -import styles from "./PlayerListItem.css"; +import styles from "./PlayerListItem.module.css"; const PlayerListItem = ({ id, @@ -9,13 +9,13 @@ const PlayerListItem = ({ team, position, starred, - starPlayer, - deletePlayer, - getLatestPlayerData, + handleDeletePlayer, + handleStarPlayer, + successCallback, }) => { return ( -
    • -
      +
    • +
      {name}
      @@ -25,10 +25,13 @@ const PlayerListItem = ({
    -
    +