feat: integrate Logto for authentication
- Add @logto/react dependencies to package.json and lockfile. - Replace custom App layout with LogtoProvider for authentication handling. - Configure Logto settings (endpoint, appId) in frontend.tsx. - Refactor FindUserPage search logic to trigger request on value change instead of manual search key refresh.
This commit is contained in:
parent
4d73ce5f2b
commit
23888b31bc
25
bun.lock
25
bun.lock
@ -6,6 +6,7 @@
|
||||
"name": "kaiqiu-rank-list",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"@logto/react": "^4.0.13",
|
||||
"ahooks": "^3.9.6",
|
||||
"antd": "^6.2.1",
|
||||
"cheerio": "^1.2.0",
|
||||
@ -50,6 +51,14 @@
|
||||
|
||||
"@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="],
|
||||
|
||||
"@logto/browser": ["@logto/browser@3.0.12", "", { "dependencies": { "@logto/client": "^3.1.7", "@silverhand/essentials": "^2.9.3", "js-base64": "^3.7.4" } }, "sha512-Ec45IExLYS64bF22wS7dZuWgOMmC2w3FZmWWnVCv2fX2vKQVs0wiI+FE/PlNhEvi8up4AW0zHO4NTGwF7ipFsQ=="],
|
||||
|
||||
"@logto/client": ["@logto/client@3.1.7", "", { "dependencies": { "@logto/js": "^6.1.1", "@silverhand/essentials": "^2.9.3", "camelcase-keys": "^9.1.3", "jose": "^5.2.2" } }, "sha512-t/5wXMhiXtmbmP6Cmcl4uMsYetq21vSZuYZztPHXv6QX0dx7lSKBvYi/65ERoS+fmNmtV2/i4Ojf1U41o0TLPQ=="],
|
||||
|
||||
"@logto/js": ["@logto/js@6.1.1", "", { "dependencies": { "@silverhand/essentials": "^2.9.3", "camelcase-keys": "^9.1.3" } }, "sha512-G0lRS7VyOXdB06WYajEh9Kq2E3m11JshiKIKLj6LRPI1qZ06JYQ+Jsej3K60/4OIZMSzUas4FVnY+ORrhDdktA=="],
|
||||
|
||||
"@logto/react": ["@logto/react@4.0.13", "", { "dependencies": { "@logto/browser": "^3.0.12", "@silverhand/essentials": "^2.9.3" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-CU4rjJmueY0CQoJZq7BDZt/9sQYpxKDwVBrGHR55ljl4zPFF2URJPixqCtEEfWq5/pFk7MEnIOePOYbj7BWKfQ=="],
|
||||
|
||||
"@rc-component/async-validator": ["@rc-component/async-validator@5.1.0", "", { "dependencies": { "@babel/runtime": "^7.24.4" } }, "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA=="],
|
||||
|
||||
"@rc-component/cascader": ["@rc-component/cascader@1.11.0", "", { "dependencies": { "@rc-component/select": "~1.5.0", "@rc-component/tree": "~1.1.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-VDiEsskThWi8l0/1Nquc9I4ytcMKQYAb9Jkm6wiX5O5fpcMRsm+b8OulBMbr/b4rFTl/2y2y4GdKqQ+2whD+XQ=="],
|
||||
@ -136,6 +145,8 @@
|
||||
|
||||
"@rc-component/virtual-list": ["@rc-component/virtual-list@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ=="],
|
||||
|
||||
"@silverhand/essentials": ["@silverhand/essentials@2.9.3", "", {}, "sha512-OM9pyGc/yYJMVQw+fFOZZaTHXDWc45sprj+ky+QjC9inhf5w51L1WBmzAwFuYkHAwO1M19fxVf2sTH9KKP48yg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
||||
|
||||
"@types/js-cookie": ["@types/js-cookie@3.0.6", "", {}, "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ=="],
|
||||
@ -158,6 +169,10 @@
|
||||
|
||||
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
||||
|
||||
"camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="],
|
||||
|
||||
"camelcase-keys": ["camelcase-keys@9.1.3", "", { "dependencies": { "camelcase": "^8.0.0", "map-obj": "5.0.0", "quick-lru": "^6.1.1", "type-fest": "^4.3.2" } }, "sha512-Rircqi9ch8AnZscQcsA1C47NFdaO3wukpmIRzYcDOrmvgt78hM/sj5pZhZNec2NM12uk5vTwRHZ4anGcrC4ZTg=="],
|
||||
|
||||
"camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="],
|
||||
|
||||
"cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="],
|
||||
@ -202,12 +217,18 @@
|
||||
|
||||
"is-mobile": ["is-mobile@5.0.0", "", {}, "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ=="],
|
||||
|
||||
"jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
|
||||
|
||||
"js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="],
|
||||
|
||||
"js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="],
|
||||
|
||||
"json2mq": ["json2mq@0.2.0", "", { "dependencies": { "string-convert": "^0.2.0" } }, "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA=="],
|
||||
|
||||
"lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
|
||||
|
||||
"map-obj": ["map-obj@5.0.0", "", {}, "sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||
@ -224,6 +245,8 @@
|
||||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"quick-lru": ["quick-lru@6.1.2", "", {}, "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ=="],
|
||||
|
||||
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
|
||||
@ -260,6 +283,8 @@
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"undici": ["undici@7.19.0", "", {}, "sha512-Heho1hJD81YChi+uS2RkSjcVO+EQLmLSyUlHyp7Y/wFbxQaGb4WXVKD073JytrjXJVkSZVzoE2MCSOKugFGtOQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"@logto/react": "^4.0.13",
|
||||
"ahooks": "^3.9.6",
|
||||
"antd": "^6.2.1",
|
||||
"cheerio": "^1.2.0",
|
||||
|
||||
@ -7,93 +7,18 @@
|
||||
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { App } from "./App";
|
||||
import { ConfigProvider, Empty, Spin, theme } from "antd";
|
||||
import { ConfigProvider, Empty, theme } from "antd";
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
import { createBrowserRouter, RouterProvider, Outlet, useNavigation } from "react-router";
|
||||
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";
|
||||
import { RouterProvider } from "react-router";
|
||||
import { LogtoProvider, type LogtoConfig } from '@logto/react';
|
||||
import { route } from "./routes";
|
||||
|
||||
const elem = document.getElementById("root")!;
|
||||
|
||||
const route = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <Layout />,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
index: true,
|
||||
Component: App,
|
||||
HydrateFallback: () => <HydrateFallback />
|
||||
},
|
||||
{
|
||||
path: '/fav-players',
|
||||
Component: FavePlayersPage,
|
||||
},
|
||||
{
|
||||
path: 'event/:matchId',
|
||||
loader: async ({ params }) => {
|
||||
const info: MatchInfo = await (await fetch(`/api/match/${params.matchId}`)).json();
|
||||
const members = info.itemId
|
||||
? await (await fetch(`/api/match/${params.matchId}/${info.itemId}`)).json()
|
||||
: info.players.map((e, i) => ({
|
||||
number: i + 1,
|
||||
uid: e.uid,
|
||||
name: e.name,
|
||||
score: e.score,
|
||||
realname: e.name,
|
||||
} as XCXMember));
|
||||
const uids = info.players.map(e => e.uid);
|
||||
const uidScore = await fetch(`/api/user/nowScores`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ uids }),
|
||||
});
|
||||
return { info, members, uidScore: new Map(Object.entries(await uidScore.json())) };
|
||||
},
|
||||
Component: EventPage,
|
||||
HydrateFallback: () => <HydrateFallback />
|
||||
},
|
||||
{
|
||||
path: 'profile/:uid',
|
||||
loader: async ({ params }) => {
|
||||
const { uid } = params;
|
||||
const profile = await (await fetch(`/api/user/${uid}`)).json();
|
||||
const tags = await (await fetch(`/api/user/${uid}/tags`)).json();
|
||||
return { profile, uid, tags };
|
||||
},
|
||||
Component: ProfilePage,
|
||||
HydrateFallback: () => <HydrateFallback />
|
||||
},
|
||||
{
|
||||
path: 'find',
|
||||
Component: FindUserPage,
|
||||
}
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
function HydrateFallback() {
|
||||
return (
|
||||
<Spin spinning>
|
||||
<div style={{ height: '100vh' }} />
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
|
||||
function Layout() {
|
||||
const navigation = useNavigation();
|
||||
const loading = navigation.state === 'loading';
|
||||
return loading ? <HydrateFallback /> : (<>
|
||||
<Outlet />
|
||||
<MenuButtons />
|
||||
</>);
|
||||
}
|
||||
|
||||
const config: LogtoConfig = {
|
||||
endpoint: 'https://logto.ksr.la/',
|
||||
appId: 'iq0oceaeqazpcned7hzil',
|
||||
};
|
||||
const app = (
|
||||
<StrictMode>
|
||||
<ConfigProvider
|
||||
@ -101,7 +26,9 @@ const app = (
|
||||
locale={zhCN}
|
||||
renderEmpty={() => <Empty description={'暂无数据'} />}
|
||||
>
|
||||
<LogtoProvider config={config}>
|
||||
<RouterProvider router={route} />
|
||||
</LogtoProvider>
|
||||
</ConfigProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
@ -7,10 +7,10 @@ import dayjs from "dayjs";
|
||||
|
||||
export function FindUserPage() {
|
||||
const [searchKey, setSearchKey] = useLocalStorageState<string>('findUser:searchKey');
|
||||
const findUserReq = useRequest(async (page: number = 1) => {
|
||||
const findUserReq = useRequest(async (searchKey: string, page: number = 1) => {
|
||||
const findOutUsers: XCXFindUserResp = await (await fetch(`/api/user/find?page=${page}&key=${searchKey}`)).json();
|
||||
return findOutUsers;
|
||||
}, { manual: true, refreshDeps: [searchKey], cacheKey: 'findUser:result' });
|
||||
}, { manual: true, cacheKey: 'findUser:result' });
|
||||
return (
|
||||
<div className="app">
|
||||
<Typography.Title level={1}>查找用户</Typography.Title>
|
||||
@ -22,7 +22,11 @@ export function FindUserPage() {
|
||||
placeholder="输入昵称或姓名查找"
|
||||
value={searchKey}
|
||||
onChange={e => setSearchKey(e.target.value)}
|
||||
onSearch={async () => findUserReq.runAsync()}
|
||||
onSearch={async (value) => {
|
||||
setSearchKey(value);
|
||||
if (!value) return;
|
||||
return findUserReq.runAsync(value);
|
||||
}}
|
||||
/>
|
||||
<Table
|
||||
size="small"
|
||||
@ -34,7 +38,7 @@ export function FindUserPage() {
|
||||
current: findUserReq.data?.current_page,
|
||||
pageSize: findUserReq.data?.per_page,
|
||||
onChange(page) {
|
||||
findUserReq.run(page);
|
||||
findUserReq.run(searchKey, page);
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
27
src/page/Logto/Callback.tsx
Normal file
27
src/page/Logto/Callback.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { useHandleSignInCallback } from '@logto/react';
|
||||
import { Spin } from 'antd';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
export const CallbackPage = () => {
|
||||
const { isLoading, isAuthenticated, error } = useHandleSignInCallback(() => {
|
||||
// Navigate to root path when finished
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
console.debug({ isLoading, isAuthenticated, error });
|
||||
navigate('/user-center');
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
// When it's working in progress
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Spin spinning>
|
||||
<div style={{ height: '100vh', width: '100vw' }} />
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
81
src/page/UserCenter.tsx
Normal file
81
src/page/UserCenter.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { EditOutlined, HomeOutlined, KeyOutlined, LockOutlined, LoginOutlined, LogoutOutlined, MailOutlined, MobileOutlined } from "@ant-design/icons";
|
||||
import { useLogto, type IdTokenClaims } from "@logto/react";
|
||||
import { Avatar, Button, Divider, Flex, Modal, Typography } from "antd";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
enum modifyRoutes {
|
||||
username = '/account/username',
|
||||
email = '/account/email',
|
||||
passwd = '/account/password',
|
||||
passkey = '/account/passkey/manage',
|
||||
auth_app = '/account/authenticator-app',
|
||||
}
|
||||
|
||||
const redirect = encodeURIComponent(`${window.location.origin}/user-center`);
|
||||
const logto = 'https://logto.ksr.la';
|
||||
|
||||
export const UserCenter = () => {
|
||||
const { signIn, isAuthenticated, signOut, getIdTokenClaims } = useLogto();
|
||||
const [user, setUser] = useState<IdTokenClaims>();
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) return;
|
||||
getIdTokenClaims().then(claims => setUser(claims));
|
||||
}, [isAuthenticated]);
|
||||
const handleModifyInfo = useCallback((url: string) => {
|
||||
window.location.href = `${logto}${url}?redirect=${redirect}`;
|
||||
}, []);
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="app">
|
||||
<Button
|
||||
block
|
||||
type="primary"
|
||||
icon={<LogoutOutlined />}
|
||||
size="large"
|
||||
onClick={() => signIn(`${window.location.origin}/auth/callback`)}
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
console.debug(user);
|
||||
return (
|
||||
<Flex className="app" gap={12} vertical align="center" style={{ maxWidth: 600 }}>
|
||||
<Avatar size={128} src={user?.picture ?? user?.name} />
|
||||
<Flex>
|
||||
<Typography.Text>{user?.username ?? user?.name ?? '未设置'}</Typography.Text>
|
||||
</Flex>
|
||||
<Divider>修改信息</Divider>
|
||||
<Button block icon={<EditOutlined />} onClick={() => handleModifyInfo(modifyRoutes.username)}>修改用户名({user?.username ?? '未设置'})</Button>
|
||||
<Button block icon={<MailOutlined />} onClick={() => handleModifyInfo(modifyRoutes.email)}>修改 E-Mail</Button>
|
||||
<Button block icon={<LockOutlined />} onClick={() => handleModifyInfo(modifyRoutes.auth_app)}>修改密码</Button>
|
||||
<Button block icon={<MobileOutlined />} onClick={() => handleModifyInfo(modifyRoutes.auth_app)}>修改验证器</Button>
|
||||
<Button block icon={<KeyOutlined />} onClick={() => handleModifyInfo(modifyRoutes.passkey)}>管理 Passkey</Button>
|
||||
<Divider />
|
||||
<Button block type="primary" onClick={() => navigate('/')} icon={<HomeOutlined />}>回到首页</Button>
|
||||
<Divider />
|
||||
<Button
|
||||
block
|
||||
danger
|
||||
icon={<LoginOutlined />}
|
||||
onClick={() => Modal.confirm({
|
||||
title: '确认登出?',
|
||||
cancelText: '回到首页',
|
||||
cancelButtonProps: { icon: <HomeOutlined /> },
|
||||
onCancel: () => navigate('/'),
|
||||
okText: '确认登出',
|
||||
okButtonProps: {
|
||||
icon: <LogoutOutlined />,
|
||||
danger: true,
|
||||
},
|
||||
onOk: () => signOut(`${window.location.origin}/user-center`),
|
||||
})}
|
||||
>
|
||||
登出
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
98
src/routes.tsx
Normal file
98
src/routes.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import { Spin } from "antd";
|
||||
import { createBrowserRouter, Outlet, useNavigation } from "react-router";
|
||||
import { MenuButtons } from "./components/MenuButtons";
|
||||
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 { UserCenter } from "./page/UserCenter";
|
||||
import { CallbackPage } from "./page/Logto/Callback";
|
||||
import App from "./App";
|
||||
|
||||
function HydrateFallback() {
|
||||
return (
|
||||
<Spin spinning>
|
||||
<div style={{ height: '100vh' }} />
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
|
||||
function Layout() {
|
||||
const navigation = useNavigation();
|
||||
const loading = navigation.state === 'loading';
|
||||
return loading ? <HydrateFallback /> : (<>
|
||||
<Outlet />
|
||||
<MenuButtons />
|
||||
</>);
|
||||
}
|
||||
|
||||
export const route = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <Layout />,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
index: true,
|
||||
Component: App,
|
||||
HydrateFallback: () => <HydrateFallback />
|
||||
},
|
||||
{
|
||||
path: '/fav-players',
|
||||
Component: FavePlayersPage,
|
||||
},
|
||||
{
|
||||
path: 'event/:matchId',
|
||||
loader: async ({ params }) => {
|
||||
const info: MatchInfo = await (await fetch(`/api/match/${params.matchId}`)).json();
|
||||
const members = info.itemId
|
||||
? await (await fetch(`/api/match/${params.matchId}/${info.itemId}`)).json()
|
||||
: info.players.map((e, i) => ({
|
||||
number: i + 1,
|
||||
uid: e.uid,
|
||||
name: e.name,
|
||||
score: e.score,
|
||||
realname: e.name,
|
||||
} as XCXMember));
|
||||
const uids = info.players.map(e => e.uid);
|
||||
const uidScore = await fetch(`/api/user/nowScores`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ uids }),
|
||||
});
|
||||
return { info, members, uidScore: new Map(Object.entries(await uidScore.json())) };
|
||||
},
|
||||
Component: EventPage,
|
||||
HydrateFallback: () => <HydrateFallback />
|
||||
},
|
||||
{
|
||||
path: 'profile/:uid',
|
||||
loader: async ({ params }) => {
|
||||
const { uid } = params;
|
||||
const profile = await (await fetch(`/api/user/${uid}`)).json();
|
||||
const tags = await (await fetch(`/api/user/${uid}/tags`)).json();
|
||||
return { profile, uid, tags };
|
||||
},
|
||||
Component: ProfilePage,
|
||||
HydrateFallback: () => <HydrateFallback />
|
||||
},
|
||||
{
|
||||
path: 'find',
|
||||
Component: FindUserPage,
|
||||
},
|
||||
{
|
||||
path: 'user-center',
|
||||
Component: UserCenter,
|
||||
},
|
||||
{
|
||||
path: 'auth',
|
||||
children: [
|
||||
{
|
||||
path: 'callback',
|
||||
Component: CallbackPage,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
Loading…
Reference in New Issue
Block a user