From 3b87230173f622ecbb1d20db60677aa53df91d98 Mon Sep 17 00:00:00 2001 From: kyuuseiryuu Date: Wed, 11 Feb 2026 16:00:01 +0900 Subject: [PATCH] feat: add favorite player functionality and improve navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - components: add FavButton for player favoriting - store: create useFavPlayerStore for managing favorites - pages: add FavePlayersPage for displaying favorites - components: add MenuButtons for navigation controls - ui: update App and ProfilePage for new features - meta: update index.html title to "我的开球网" --- src/App.tsx | 4 +- src/components/FavButton.tsx | 38 ++++++++++++++++++ src/components/MenuButtons.tsx | 15 +++++++ src/frontend.tsx | 21 +++++++--- src/index.html | 2 +- src/page/FavPlayersPage.tsx | 72 ++++++++++++++++++++++++++++++++++ src/page/ProfilePage.tsx | 16 +++++++- src/store/useFavPlayerStore.ts | 33 ++++++++++++++++ 8 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 src/components/FavButton.tsx create mode 100644 src/components/MenuButtons.tsx create mode 100644 src/page/FavPlayersPage.tsx create mode 100644 src/store/useFavPlayerStore.ts diff --git a/src/App.tsx b/src/App.tsx index 83e6d54..7b0af1c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,8 +2,7 @@ import { useCallback } from "react"; import { ClubSelector } from "./components/GameSelector"; import type { IEventInfo } from "./types"; import { useNavigate } from "react-router"; -import { FloatButton, Typography } from "antd"; -import { SearchOutlined } from "@ant-design/icons"; +import { Typography } from "antd"; import "./index.css"; export function App() { @@ -15,7 +14,6 @@ export function App() {
开球网比赛分组预测 - } onClick={() => navigate('/find')} />
); } diff --git a/src/components/FavButton.tsx b/src/components/FavButton.tsx new file mode 100644 index 0000000..96bd29c --- /dev/null +++ b/src/components/FavButton.tsx @@ -0,0 +1,38 @@ +import { Rate } from "antd"; +import { useCallback, useState } from "react"; +import { useFavPlayerStore, type FavPlayer } from "../store/useFavPlayerStore"; +import styled from "styled-components"; + +interface Props { + user?: FavPlayer; +} + +const StyledContainer = styled.div` + display: flex; + align-items: center; + .ant-rate { + margin: 0; + .ant-rate-star { + margin: 0; + } + } +`; + +export function FavButton(props: Props) { + const favStore = useFavPlayerStore(state => state); + const [value, setValue] = useState(favStore.isFav(props.user?.uid) ? 1 : 0); + const handleFavClick = useCallback((value: number) => { + if (!props.user) return; + setValue(value); + if (value) { + favStore.fav(props.user); + } else { + favStore.unFav(props.user.uid); + } + }, [favStore]); + return ( + + + + ); +} \ No newline at end of file diff --git a/src/components/MenuButtons.tsx b/src/components/MenuButtons.tsx new file mode 100644 index 0000000..62bded1 --- /dev/null +++ b/src/components/MenuButtons.tsx @@ -0,0 +1,15 @@ +import { ArrowLeftOutlined, HeartOutlined, MenuOutlined, ScheduleOutlined, SearchOutlined } from "@ant-design/icons"; +import { FloatButton } from "antd"; +import { useNavigate } from "react-router"; + +export function MenuButtons() { + const navigate = useNavigate(); + return ( + }> + } onClick={() => navigate('/')} /> + } onClick={() => navigate('/fav-players')} /> + } onClick={() => navigate('/find')} /> + } onClick={() => navigate(-1)} /> + + ); +} \ No newline at end of file diff --git a/src/frontend.tsx b/src/frontend.tsx index 70f1a9f..f4bae4e 100644 --- a/src/frontend.tsx +++ b/src/frontend.tsx @@ -15,6 +15,8 @@ import ProfilePage from "./page/ProfilePage"; import EventPage from "./page/EventPage"; import type { MatchInfo, XCXMember } from "./types"; import { FindUserPage } from "./page/FindUserPage"; +import { FavePlayersPage } from "./page/FavPlayersPage"; +import { MenuButtons } from "./components/MenuButtons"; const elem = document.getElementById("root")!; @@ -26,9 +28,13 @@ const route = createBrowserRouter([ { path: '', index: true, - element: , + Component: App, HydrateFallback: () => }, + { + path: '/fav-players', + Component: FavePlayersPage, + }, { path: 'event/:matchId', loader: async ({ params }) => { @@ -57,12 +63,12 @@ const route = createBrowserRouter([ Component: ProfilePage, HydrateFallback: () => }, + { + path: 'find', + Component: FindUserPage, + } ], }, - { - path: 'find', - Component: FindUserPage, - } ]); function HydrateFallback() { @@ -76,7 +82,10 @@ function HydrateFallback() { function Layout() { const navigation = useNavigation(); const loading = navigation.state === 'loading'; - return loading ? : + return loading ? : (<> + + + ); } const app = ( diff --git a/src/index.html b/src/index.html index f4d5805..3d6c0a1 100644 --- a/src/index.html +++ b/src/index.html @@ -5,7 +5,7 @@ - 开球网比赛分组预测 + 我的开球网
diff --git a/src/page/FavPlayersPage.tsx b/src/page/FavPlayersPage.tsx new file mode 100644 index 0000000..91c5341 --- /dev/null +++ b/src/page/FavPlayersPage.tsx @@ -0,0 +1,72 @@ +import { Avatar, Card, Divider, Flex, Image, Segmented, Space, Typography } from "antd"; +import { useFavPlayerStore } from "../store/useFavPlayerStore"; +import { useNavigate } from "react-router"; +import { useMemo, useState } from "react"; + +enum SortType { + DEFAULT = '默认排序', + SCORE_UP = '积分升序', + SCORE_DOWN = '积分降序', +} + +export function FavePlayersPage() { + const { favMap } = useFavPlayerStore(state => state); + const [sortType, setSortType] = useState(SortType.SCORE_DOWN); + const list = useMemo(() => { + const l = Object.values(favMap); + switch (sortType) { + case SortType.DEFAULT: return l; + case SortType.SCORE_UP: return l.sort(({ score: a }, { score: b }) => +a - +b); + case SortType.SCORE_DOWN: return l.sort(({ score: b }, { score: a }) => +a - +b); + default: return l; + } + }, [favMap, sortType]); + const navigate = useNavigate(); + return ( +
+ + 收藏的球员 +
+ setSortType(e)} + options={[ + SortType.SCORE_DOWN, + SortType.SCORE_UP, + ]} + /> +
+ {list.length === 0 ? ( + 暂无收藏的球员 + ) : ( + + {list.map(e => ( + + + {e.avatar?.includes('noavatar') ? {e.name} : } + navigate(`/profile/${e.uid}`)} + > +

{e.name}

+ {e.realname} +
+
+
+ ))} +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/page/ProfilePage.tsx b/src/page/ProfilePage.tsx index caa1223..7a0e40a 100644 --- a/src/page/ProfilePage.tsx +++ b/src/page/ProfilePage.tsx @@ -9,6 +9,7 @@ import { ChangeBackground } from "../components/ChangeBackground"; import UserTags from "../components/Tags"; import { GameTable } from "../components/GameTable"; import { useTitle } from "ahooks"; +import { FavButton } from "../components/FavButton"; function Honor(props: { honors?: XCXProfile['honors'] }) { if (!props.honors?.length) return null; @@ -96,7 +97,20 @@ export default function ProfilePage() { } onClick={() => navigate('/')} /> - {profile?.username} + + + {profile?.username} + + + 姓名:{profile?.realname} 积分:{profile?.score} diff --git a/src/store/useFavPlayerStore.ts b/src/store/useFavPlayerStore.ts new file mode 100644 index 0000000..f0a33ff --- /dev/null +++ b/src/store/useFavPlayerStore.ts @@ -0,0 +1,33 @@ +import { create } from 'zustand'; +import { persist, createJSONStorage } from 'zustand/middleware'; +import type { BasePlayer } from '../types'; + +export interface FavPlayer extends BasePlayer { + realname?: string; + avatar?: string; +} + +interface FavPlayerStore { + favMap: Record; + fav(player: FavPlayer): void; + unFav(uid: string): void; + isFav(uid?: string): boolean; +} + +export const useFavPlayerStore = create(persist((set, get) => ({ + favMap: {}, + fav: user => { + const { favMap } = get(); + favMap[user.uid] = user; + set({ favMap: { ...favMap } }); + }, + unFav: uid => { + const { favMap } = get(); + delete favMap[uid]; + set({ favMap: { ...favMap } }); + }, + isFav(uid) { + if (!uid) return false; + return !!get().favMap[uid]; + }, +}), { name: 'fav-player-store', storage: createJSONStorage(() => localStorage) })); \ No newline at end of file