feat: add player profile cache & manual sync button

- Cache user profile data in Redis with 10-minute expiration in XcxAPI service to reduce API overhead.
- Added a refresh/sync button on FavPlayersPage to manually trigger fetching players from the account.
- Refactored authentication logic to properly set and use ID token claims for syncing.
- Improved UX by removing automatic view switching logic that caused layout shifts, relying on state-driven rendering instead.
- Unified login redirect flow using the new `useAutoLogin` hook.
This commit is contained in:
kyuuseiryuu 2026-03-10 11:19:30 +09:00
parent 06665f3371
commit 54d275796e
3 changed files with 38 additions and 14 deletions

15
src/hooks/useAutoLogin.ts Normal file
View File

@ -0,0 +1,15 @@
import { useCallback } from "react"
import { useNavigate } from "react-router";
const useAutoLogin = () => {
const navigate = useNavigate();
const login = useCallback((redirect?: string) => {
if (redirect) {
sessionStorage.setItem('redirect', redirect);
}
navigate('/user-center?autoSignIn=true');
}, []);
return { login };
}
export default useAutoLogin;

View File

@ -2,10 +2,11 @@ import { Avatar, Button, Card, Divider, Flex, Image, Popconfirm, Radio, Segmente
import { useFavPlayerStore, type FavPlayer } from "../store/useFavPlayerStore";
import { useNavigate } from "react-router";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useLogto } from "@logto/react";
import { DeleteOutlined, LoginOutlined, UploadOutlined } from "@ant-design/icons";
import { useLogto, type IdTokenClaims } from "@logto/react";
import { DeleteOutlined, LoginOutlined, SyncOutlined, UploadOutlined } from "@ant-design/icons";
import { useRequest } from "ahooks";
import type { XCXProfile } from "../types";
import useAutoLogin from "../hooks/useAutoLogin";
enum SortType {
DEFAULT = '注册时间',
@ -21,6 +22,8 @@ export function FavePlayersPage() {
const { favMap, unFav } = useFavPlayerStore(state => state);
const [sortType, setSortType] = useState<SortType>(SortType.DEFAULT);
const [showType, setShowType] = useState(ShowType.LOCAL);
const [claims, setClaims] = useState<IdTokenClaims>();
const { login } = useAutoLogin();
const localList = Object.values(favMap);
const favListRequest = useRequest<XCXProfile[], [string]>(async (aud: string) => {
const res = await fetch(`/api/fav/${aud}`);
@ -47,6 +50,7 @@ export function FavePlayersPage() {
if (!isAuthenticated) return;
const id = setTimeout(async () => {
const claims = await getIdTokenClaims();
setClaims(claims);
favListRequest.runAsync(claims?.aud!);
}, 300);
return () => clearTimeout(id);
@ -64,14 +68,11 @@ export function FavePlayersPage() {
const handleClearLocal = useCallback(() => {
list.forEach(e => unFav(e.uid));
}, []);
useEffect(() => {
setShowType(isAuthenticated ? ShowType.ACCOUNT : ShowType.LOCAL);
}, [isAuthenticated]);
return (
<div className="app">
<Flex vertical gap={48}>
<Typography.Title></Typography.Title>
<div style={{ display: isAuthenticated ? 'block' : 'none' }}>
<Flex justify="center" align="center" gap={12} style={{ display: isAuthenticated ? '' : 'none' }}>
<Radio.Group
optionType="button"
value={showType}
@ -81,7 +82,11 @@ export function FavePlayersPage() {
{ label: `${ShowType.ACCOUNT}(${favListRequest.data?.length ?? 0})`, value: ShowType.ACCOUNT}
]}
/>
</div>
<Button
onClick={async () => favListRequest.runAsync(claims?.aud!)}
icon={<SyncOutlined spin={favListRequest.loading} />}
/>
</Flex>
<div style={{ position: 'sticky', zIndex: 1 }}>
<Segmented
value={sortType}
@ -93,7 +98,7 @@ export function FavePlayersPage() {
]}
/>
</div>
<Spin spinning={favListRequest.loading}>
<Spin spinning={showType === ShowType.ACCOUNT && favListRequest.loading}>
{!favListRequest.loading && list.length === 0 ? (
<>
<Divider></Divider>
@ -132,10 +137,7 @@ export function FavePlayersPage() {
) }
{ (showType === ShowType.LOCAL && !isAuthenticated && list.length > 0) && (
<Button
onClick={() => {
sessionStorage.setItem('redirect', window.location.pathname);
navigate('/user-center?autoSignIn=true');
}}
onClick={() => login(window.location.pathname)}
icon={<LoginOutlined />}
></Button>
) }

View File

@ -1,5 +1,6 @@
import type { GamesData, XCXFindUserResp, XCXMember, XCXProfile, XCXTag } from "../types";
import { BASE_URL } from "../utils/common";
import { redis } from "../utils/server";
const XCX_BASE_URL = `${BASE_URL}/xcx/public/index.php`;
@ -33,8 +34,14 @@ export class XCXAPI {
}
async getAdvProfile(uid: string) {
const url = `/api/User/adv_profile?uid=${uid}`;
return this.#fetch<XCXProfile>(url);
const cacheProfile = await redis.get(`my-kaiqiuwang-profile:${uid}`);
if (!cacheProfile) {
const url = `/api/User/adv_profile?uid=${uid}`;
const profile = await this.#fetch<XCXProfile>(url);
await redis.setex(`my-kaiqiuwang-profile:${uid}`, 60 * 10, JSON.stringify(profile));
return profile;
}
return JSON.parse(cacheProfile);
}
async getPlayerTags(uid: string) {