diff --git a/__test__/data/profile.json b/__test__/data/profile.json new file mode 100644 index 0000000..99c0268 --- /dev/null +++ b/__test__/data/profile.json @@ -0,0 +1,384 @@ +{ + "code": 1, + "msg": "获得用户高级信息", + "time": "1769560179", + "data": { + "uid": "73276", + "username": "武藏野", + "Top3ManOfBeat": "12332,379345,562751,", + "Top3ManOfBeatUsernameScore": [ + "陳允文(1889)", + "邵博文(1854)", + "张磊(1845)" + ], + "Top3WomanOfBeat": "100252,100748,452471,", + "Top3WomanOfPlay": "0", + "Top3OfBeat": "379345,562751,452471,", + "Top3WomanOfBeatUsernameScore": [ + "惠宇晨(1965)", + "楡原al(1815)", + "三島悠華(1766)" + ], + "TopPlayer": "107828,428006,427831,", + "TopPlayerUsernameScore": [ + "陈永航(2150)", + "金田優陽(2058)", + "李冠远(2006)" + ], + "OftenPlayer": "铁板李(7),中河(9),cldws(10),大国(12),滨町张磊(16),chen5274(16),", + "maxConsWin": "11", + "maxConsWinLastGameId": "10193043", + "allCities": [ + "日本" + ], + "win": "153", + "lose": "89", + "total": "242", + "dateline": "1455960573", + "province": "上海", + "city": "浦东新", + "brand": "银河", + "if_event_uid": "0", + "score": "1711", + "goldNum": "2", + "cityNum": "1", + "path": "", + "pathNum": "0", + "champion": "", + "orgTimes": "0", + "ly_event_times": "6", + "ly_event_games": "49", + "ly_event_rank": "0", + "ly_max_inc": "35", + "ly_max_dis": "15.78", + "ly_beatit": "惠惠惠yc(1965)", + "ly_shopid": "2135", + "sleep": "0", + "pathWords": "", + "fuxing": "张成成,张成成,1666,22992,4012390,3胜0负 胜率:100% 胜负局:9/1;cldws,吴庶,1631,71669,14807559,10胜0负 胜率:100% 胜负局:28/8", + "kuzhu": "邵仁爱,邵仁爱,1781,68186,13745808,0胜4负 胜率:0% 胜负局:3/11;卡斯柏,汪志浩,1893,89518,5038808,0胜4负 胜率:0% 胜负局:1/11;萍姐,尹艳萍,1897,381359,5846361,0胜3负 胜率:0% 胜负局:2/9", + "rank": "53424", + "ifHonorShow": "1", + "locationupdatetime": "0", + "resideupdatetime": "0", + "ifManage": 0, + "age": 62, + "maxscore": "1828", + "maxScoreTheYear": "1819", + "beat": "惠宇晨(1965)", + "rate": "87.18%", + "Top3OfBeatUsernameScore": [ + "惠宇晨(1965)", + "陳允文(1889)", + "邵博文(1854)" + ], + "KuZhu": { + "names": [ + "邵仁爱(1781)", + "汪志浩(1893)", + "尹艳萍(1897)" + ], + "uids": [ + "68186", + "89518", + "381359" + ], + "gameids": [ + "13745808", + "5038808", + "5846361" + ], + "winlose": [ + "0胜4负 胜率:0% 胜负局:3/11", + "0胜4负 胜率:0% 胜负局:1/11", + "0胜3负 胜率:0% 胜负局:2/9" + ] + }, + "FuXing": { + "names": [ + "张成成(1666)", + "吴庶(1631)" + ], + "uids": [ + "22992", + "71669" + ], + "gameids": [ + "4012390", + "14807559" + ], + "winlose": [ + "3胜0负 胜率:100% 胜负局:9/1", + "10胜0负 胜率:100% 胜负局:28/8" + ] + }, + "lastUpdate": "2026年01月22日", + "realpic": "https://oss.kaiqiu.cc/avatar/000/07/32/76_avatar_big.jpg", + "realname": "lihua", + "qiupai": "蝴蝶Butterfly", + "qiupaitype": "普里莫拉茨碳", + "fanshou": "蝴蝶BUTTERFLY", + "fanshoutype": "Tenergy 05 FX", + "zhengshou": "蝴蝶BUTTERFLY", + "zhengshoutype": "Flarestorm Ⅱ", + "resideprovince": "上海 浦东新", + "sex": "男", + "bg": "业余选手", + "scope": "全国", + "description": "lihua 于 2016年02月20日 这一天开启了开球网ChinaTT积分赛的神奇之旅,他是开球网的第 73276 位忠实用户。lihua共进行了 242 盘单打比赛,其中获胜 153 盘,失利 89 盘。lihua的积分超过了全国 87.18% 的乒乓球选手,应是业余准高手,小圈子里前几名的水平,对乒乓球有一定的理解。lihua曾战胜的最高分选手:惠宇晨(1965)", + "ifHonor": 1, + "honors": [ + { + "uid": "73276", + "hid": "450465", + "eventid": "70476", + "itemid": "6034415", + "honor": "https://kaiqiuwang.cc/home/image/icon/bronze_medal16.png", + "subject": "2024年12月22日 东华乒乓球俱乐部2024年1季军", + "posttime": "0" + }, + { + "uid": "73276", + "hid": "274549", + "eventid": "47712", + "itemid": "38822", + "honor": "https://kaiqiuwang.cc/home/image/icon/gold_medal16.png", + "subject": "2024年6月15日 东华乒乓球俱乐部2024年6月冠军", + "posttime": "0" + }, + { + "uid": "73276", + "hid": "26332", + "eventid": "10049", + "itemid": "0", + "honor": "https://kaiqiuwang.cc/home/image/icon/silver_medal16.png", + "subject": "东华乒乓球俱乐部2018年2月份积分争霸赛亚军", + "posttime": "0" + }, + { + "uid": "73276", + "hid": "25609", + "eventid": "9708", + "itemid": "0", + "honor": "https://kaiqiuwang.cc/home/image/icon/gold_medal16.png", + "subject": "东华乒乓球俱乐部2017年12月份积分争霸赛冠军", + "posttime": "0" + }, + { + "uid": "73276", + "hid": "16388", + "eventid": "6078", + "itemid": "0", + "honor": "https://kaiqiuwang.cc/home/image/icon/bronze_medal16.png", + "subject": "东华乒乓球俱乐部2016年4月份积分赛季军", + "posttime": "0" + } + ], + "games": { + "data": [ + { + "gameid": "20730731", + "uid1": "73276", + "uid2": "666599", + "uid11": "0", + "uid22": "0", + "username1": "武藏野", + "username2": "庞程万里", + "username11": "", + "username22": "", + "result1": "3", + "result2": "0", + "score1": "+16", + "score2": "-16", + "eventid": "157007", + "ascore1": "1711", + "dateline": "2026-01-17", + "groupid": "708465111", + "flag": "0" + }, + { + "gameid": "20730724", + "uid1": "73276", + "uid2": "563021", + "uid11": "0", + "uid22": "0", + "username1": "武藏野", + "username2": "Tracy_god", + "username11": "", + "username22": "", + "result1": "0", + "result2": "3", + "score1": "-7", + "score2": "7", + "eventid": "157007", + "ascore1": "1695", + "dateline": "2026-01-17", + "groupid": "708465111", + "flag": "0" + }, + { + "gameid": "20730720", + "uid1": "73276", + "uid2": "72155", + "uid11": "0", + "uid22": "0", + "username1": "武藏野", + "username2": "大国", + "username11": "", + "username22": "", + "result1": "1", + "result2": "3", + "score1": "-2", + "score2": "2", + "eventid": "157007", + "ascore1": "1866", + "dateline": "2026-01-17", + "groupid": "708465111", + "flag": "0" + }, + { + "gameid": "20730718", + "uid1": "73276", + "uid2": "58891", + "uid11": "0", + "uid22": "0", + "username1": "武藏野", + "username2": "阿迪73", + "username11": "", + "username22": "", + "result1": "2", + "result2": "3", + "score1": "-10", + "score2": "10", + "eventid": "157007", + "ascore1": "1708", + "dateline": "2026-01-17", + "groupid": "708465111", + "flag": "0" + }, + { + "gameid": "20730715", + "uid1": "73276", + "uid2": "68186", + "uid11": "0", + "uid22": "0", + "username1": "武藏野", + "username2": "邵仁爱", + "username11": "", + "username22": "", + "result1": "1", + "result2": "3", + "score1": "-5", + "score2": "5", + "eventid": "157007", + "ascore1": "1787", + "dateline": "2026-01-17", + "groupid": "708465111", + "flag": "0" + }, + { + "gameid": "20730713", + "uid1": "73276", + "uid2": "376886", + "uid11": "0", + "uid22": "0", + "username1": "武藏野", + "username2": "中河", + "username11": "", + "username22": "", + "result1": "1", + "result2": "3", + "score1": "-6", + "score2": "6", + "eventid": "157007", + "ascore1": "1770", + "dateline": "2026-01-17", + "groupid": "708465111", + "flag": "0" + }, + { + "gameid": "17112521", + "uid1": "73276", + "uid2": "434868", + "uid11": "0", + "uid22": "0", + "username1": "武藏野", + "username2": "薛高远", + "username11": "", + "username22": "", + "result1": "0", + "result2": "3", + "score1": "-13", + "score2": "13", + "eventid": "117651", + "ascore1": "1692", + "dateline": "2025-08-24", + "groupid": "-1", + "flag": "0" + }, + { + "gameid": "17070397", + "uid1": "73276", + "uid2": "439944", + "uid11": "0", + "uid22": "0", + "username1": "武藏野", + "username2": "周诗博", + "username11": "", + "username22": "", + "result1": "0", + "result2": "3", + "score1": "-13", + "score2": "13", + "eventid": "117651", + "ascore1": "1738", + "dateline": "2025-08-24", + "groupid": "703644614", + "flag": "0" + }, + { + "gameid": "17070393", + "uid1": "73276", + "uid2": "90032", + "uid11": "0", + "uid22": "0", + "username1": "武藏野", + "username2": "骏骏", + "username11": "", + "username22": "", + "result1": "3", + "result2": "1", + "score1": "+4", + "score2": "-4", + "eventid": "117651", + "ascore1": "1751", + "dateline": "2025-08-24", + "groupid": "703644614", + "flag": "0" + }, + { + "gameid": "17070389", + "uid1": "73276", + "uid2": "318418", + "uid11": "0", + "uid22": "0", + "username1": "武藏野", + "username2": "SYF777", + "username11": "", + "username22": "", + "result1": "3", + "result2": "2", + "score1": "+2", + "score2": "-2", + "eventid": "117651", + "ascore1": "1747", + "dateline": "2025-08-24", + "groupid": "703644614", + "flag": "0" + } + ] + }, + "latest_headtohead_gameid": "19767882", + "hasFollowed": 0 + } +} \ No newline at end of file diff --git a/__test__/utils.test.ts b/__test__/utils.test.ts index 5c9b788..4252efe 100644 --- a/__test__/utils.test.ts +++ b/__test__/utils.test.ts @@ -21,6 +21,7 @@ test('event content not empty', () => { expect(itemId).toBe(item_id); expect(players.length).toBeGreaterThan(0); console.log(players); + expect(players[0]?.uid).not.toBe(''); }); test("group", () => { diff --git a/__test__/xcxapi.test.ts b/__test__/xcxapi.test.ts new file mode 100644 index 0000000..b53d0d1 --- /dev/null +++ b/__test__/xcxapi.test.ts @@ -0,0 +1,12 @@ +import { test, expect } from 'bun:test'; +import { getAdvProfile } from '../src/utils'; +import fs from 'fs'; +import path from 'path'; + +test('Test profile', async () => { + const uid = '73276'; + const profile = await getAdvProfile(uid); + console.log(profile) + expect(profile).not.toBe(null) + fs.writeFileSync(path.resolve(__dirname, 'data', 'profile.json'), JSON.stringify(profile, null, 2)); +}); \ No newline at end of file diff --git a/bun.lock b/bun.lock index 34ba1ae..96e1a89 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,8 @@ "lodash": "^4.17.23", "react": "^19", "react-dom": "^19", + "react-router": "^7.13.0", + "zustand": "^5.0.10", }, "devDependencies": { "@types/bun": "latest", @@ -156,6 +158,8 @@ "compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], @@ -206,6 +210,8 @@ "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "react-router": ["react-router@7.13.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw=="], + "resize-observer-polyfill": ["resize-observer-polyfill@1.5.1", "", {}, "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], @@ -216,6 +222,8 @@ "scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="], + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + "string-convert": ["string-convert@0.2.1", "", {}, "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="], "stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="], @@ -232,6 +240,8 @@ "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + "zustand": ["zustand@5.0.10", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg=="], + "htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], diff --git a/package.json b/package.json index 164c2f8..8509595 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "cheerio": "^1.2.0", "lodash": "^4.17.23", "react": "^19", - "react-dom": "^19" + "react-dom": "^19", + "react-router": "^7.13.0", + "zustand": "^5.0.10" }, "devDependencies": { "@types/bun": "latest", diff --git a/src/App.tsx b/src/App.tsx index eded40c..a0b95ef 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,29 +1,19 @@ -import { useCallback, useState } from "react"; +import { useCallback } from "react"; import { ClubSelector } from "./components/GameSelector"; +import type { IEventInfo } from "./types"; +import { useNavigate } from "react-router"; import "./index.css"; -import type { IEventInfo, Player } from "./types"; -import { Drawer, Tabs } from "antd"; -import { PlayerList } from "./components/PlayerList"; -import { GamePanel } from "./components/GamePanel"; export function App() { - const [game, setGame] = useState(); + const navigate = useNavigate(); const handleGameClick = useCallback(async (game: IEventInfo) => { - setGame(game); + navigate(`/event/${game.matchId}`); }, []); + return (

开球网比赛分组预测

- setGame(undefined)} - size={'calc(100vh - 100px)'} - > - -
); } diff --git a/src/components/GamePanel.tsx b/src/components/GamePanel.tsx index d14c486..a518688 100644 --- a/src/components/GamePanel.tsx +++ b/src/components/GamePanel.tsx @@ -1,5 +1,5 @@ import type React from "react"; -import type { IEventInfo, MatchInfo } from "../types"; +import type { IEventInfo, MatchInfo, Player } from "../types"; import { useRequest } from "ahooks"; import { Spin, Tabs } from "antd"; import { PlayerList } from "./PlayerList"; @@ -7,34 +7,30 @@ import { GroupingPrediction } from "./GroupingPrediction"; import { useMemo } from "react"; interface Props { - game?: IEventInfo; + title: string; + players?: Player[]; } export const GamePanel: React.FC = props => { - const fetchPlayers = useRequest(async () => { - if (!props.game) return null; - const info: MatchInfo = await (await fetch(`/api/match/${props.game.matchId}`)).json(); - return info; - }, { refreshDeps: [props] }); const sneckMode = useMemo(() => { - return !!props.game?.title?.includes('争霸赛'); - }, [props.game]); + return !!props.title?.includes('争霸赛'); + }, [props.title]); return ( - + <> + children: }, { key: 'players', label: '成员列表', - children: + children: }, ]} /> - + ); } \ No newline at end of file diff --git a/src/components/GameSelector/GameSelector.tsx b/src/components/GameSelector/GameSelector.tsx index 07f0b5b..5a48cc0 100644 --- a/src/components/GameSelector/GameSelector.tsx +++ b/src/components/GameSelector/GameSelector.tsx @@ -4,7 +4,7 @@ import { clubs } from './clubList'; import { useCallback, useEffect, useState } from 'react'; import { useRequest } from 'ahooks'; import { GlobalOutlined } from '@ant-design/icons'; -import type { IEventInfo } from '../../types'; +import type { IEventInfo } from '../..'; interface Props { onGameClick?: (info: IEventInfo) => void; diff --git a/src/components/GroupMember.tsx b/src/components/GroupMember.tsx index 3805e49..6fa8f8f 100644 --- a/src/components/GroupMember.tsx +++ b/src/components/GroupMember.tsx @@ -1,6 +1,7 @@ -import { Card, Space, Table, Typography } from "antd"; +import { Card, Table } from "antd"; import type { Player } from "../types"; import { useMemo } from "react"; +import User from "./User"; interface Props { index: number; @@ -27,7 +28,7 @@ export const GroupMember: React.FC = props => { columns={[ { dataIndex: '_', render: (_, __, i) => `(${i + 1})` }, { dataIndex: 'index' }, - { dataIndex: 'name' }, + { dataIndex: 'name', render: (name, { uid }) => }, { dataIndex: 'score' }, ]} /> diff --git a/src/components/GroupingPrediction.tsx b/src/components/GroupingPrediction.tsx index 370282e..b8158e7 100644 --- a/src/components/GroupingPrediction.tsx +++ b/src/components/GroupingPrediction.tsx @@ -33,7 +33,7 @@ export const GroupingPrediction: React.FC = props => { }, [grouped, groupLen, maxPlayerSize]); return ( <> -
+ = props => { - +
{ !sneckMode && grouped.map((p, i) => )} diff --git a/src/components/PlayerList.tsx b/src/components/PlayerList.tsx index b3ae1a4..0e2b3ed 100644 --- a/src/components/PlayerList.tsx +++ b/src/components/PlayerList.tsx @@ -1,6 +1,7 @@ import type React from "react"; import type { Player } from "../types"; import { Avatar, Table } from "antd"; +import User from "./User"; interface Props { loading?: boolean; @@ -17,7 +18,13 @@ export const PlayerList: React.FC = props => { > `${i + 1}`} /> } /> - + } + /> a - b, }} /> diff --git a/src/components/User.tsx b/src/components/User.tsx new file mode 100644 index 0000000..89b1e16 --- /dev/null +++ b/src/components/User.tsx @@ -0,0 +1,11 @@ +import { Link } from "react-router"; + +interface Props { + name: string; + uid: string; +} +export default function User(props: Props) { + return ( + {props.name} + ); +} \ No newline at end of file diff --git a/src/frontend.tsx b/src/frontend.tsx index aca8de9..41b5c92 100644 --- a/src/frontend.tsx +++ b/src/frontend.tsx @@ -5,18 +5,45 @@ * It is included in `src/index.html`. */ -import { StrictMode } from "react"; +import { Component, StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { App } from "./App"; import { ConfigProvider, theme } from "antd"; +import { createBrowserRouter, RouterProvider } from "react-router"; +import ProfilePage from "./page/ProfilePage"; +import EventPage from "./page/EventPage"; +import type { MatchInfo } from "./types"; const elem = document.getElementById("root")!; + +const route = createBrowserRouter([ + { + path: '/', + element: + }, + { + path: '/event/:matchId', + loader: async ({ params }) => { + const info: MatchInfo = await (await fetch(`/api/match/${params.matchId}`)).json(); + return info; + }, + Component: EventPage, + }, + { + path: '/profile/:uid', + loader: async ({ params }) => { + return fetch(`/api/user/${params.uid}`); + }, + Component: ProfilePage, + }, +]); + const app = ( - + ); diff --git a/src/index.css b/src/index.css index b7c4bda..00094d7 100644 --- a/src/index.css +++ b/src/index.css @@ -6,7 +6,8 @@ background-color: #242424; } #root { - width: 100vw; + width: 100%; + max-width: 100vw; } body { margin: 0 auto; diff --git a/src/index.tsx b/src/index.tsx index 4725bab..ba656ad 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,6 @@ import { serve } from "bun"; import index from "./index.html"; -import { getMatchInfo, listEvent } from "./utils"; +import { getAdvProfile, getMatchInfo, listEvent } from "./utils"; const server = serve({ port: process.env.PORT || 3000, @@ -19,6 +19,13 @@ const server = serve({ return Response.json(data); } }, + "/api/user/:uid": { + async GET(req) { + const uid = req.params.uid; + const profile = await getAdvProfile(uid); + return Response.json(profile); + }, + } }, development: process.env.NODE_ENV !== "production" && { diff --git a/src/page/EventPage.tsx b/src/page/EventPage.tsx new file mode 100644 index 0000000..c821a49 --- /dev/null +++ b/src/page/EventPage.tsx @@ -0,0 +1,12 @@ +import { useLoaderData, useNavigate } from "react-router"; +import { GamePanel } from "../components/GamePanel"; +import type { MatchInfo } from "../types"; + +export default function EventPage() { + const game = useLoaderData(); + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/src/page/ProfilePage.tsx b/src/page/ProfilePage.tsx new file mode 100644 index 0000000..e983b75 --- /dev/null +++ b/src/page/ProfilePage.tsx @@ -0,0 +1,72 @@ +import { useLoaderData } from "react-router"; +import type { XCXProfile } from "../types/profile"; +import { Avatar, Descriptions, Divider, Flex, Image, Typography } from "antd"; + +function Honor(props: { honors?: XCXProfile['honors'] }) { + if (!props.honors?.length) return null; + return ( + <> + 荣誉 + + {props.honors?.map(honor => { + return ( + + + {honor.subject} + + ); + })} + + + ); +} + +function Raket(props: { profile?: XCXProfile | null }) { + const { qiupaitype, zhengshoutype, fanshoutype, qiupai, zhengshou, fanshou } = props.profile || {}; + if ([qiupaitype, zhengshoutype, fanshoutype].every(e => !e)) { + return null; + } + return ( + <> + 装备 + + + {qiupaitype} + ({qiupai}) + + + {zhengshoutype} + ({zhengshou}) + + + {fanshoutype} + ({fanshou}) + + + + ); +} + +export default function ProfilePage() { + const profile = useLoaderData(); + return ( + + + {profile?.username} + 姓名:{profile?.realname} + 积分:{profile?.score} + + { + [profile?.province, profile?.sex, profile?.bg, profile?.scope, ...profile?.allCities ?? []] + .filter(Boolean).join(' | ') + } + + 简介 + + {profile?.description} + + + + + ); +} \ No newline at end of file diff --git a/src/store/useGameStore.ts b/src/store/useGameStore.ts new file mode 100644 index 0000000..cb5fa37 --- /dev/null +++ b/src/store/useGameStore.ts @@ -0,0 +1,18 @@ +import { create } from "zustand"; +import { type IEventInfo } from '../types'; + +interface StoreType { + eventInfo?: IEventInfo; + setEventInfo: (info?: IEventInfo) => void; +} + + +const useGameStore = create((set) => { + return { + setEventInfo: (info) => { + set({ eventInfo: info }); + }, + } +}); + +export default useGameStore; \ No newline at end of file diff --git a/src/types.ts b/src/types/index.ts similarity index 88% rename from src/types.ts rename to src/types/index.ts index b8e84f4..9413d75 100644 --- a/src/types.ts +++ b/src/types/index.ts @@ -5,14 +5,16 @@ export interface IEventInfo { matchId: string; } +export interface MatchInfo { + itemId: string; + title: string; + players: Player[]; +} + export interface Player { name: string; score: number; avatar: string; info: string; + uid: string; } - -export interface MatchInfo { - itemId: string; - players: Player[]; -} \ No newline at end of file diff --git a/src/types/profile.ts b/src/types/profile.ts new file mode 100644 index 0000000..a3e59ef --- /dev/null +++ b/src/types/profile.ts @@ -0,0 +1,116 @@ +export interface XCXProfile { + uid: string; + username: string; + Top3ManOfBeat: string; + Top3ManOfBeatUsernameScore: string[]; + Top3WomanOfBeat: string; + Top3WomanOfPlay: string; + Top3OfBeat: string; + Top3WomanOfBeatUsernameScore: string[]; + TopPlayer: string; + TopPlayerUsernameScore: string[]; + OftenPlayer: string; + maxConsWin: string; + maxConsWinLastGameId: string; + allCities: string[]; + win: string; + lose: string; + total: string; + dateline: string; + province: string; + city: string; + brand: string; + if_event_uid: string; + score: string; + goldNum: string; + cityNum: string; + path: string; + pathNum: string; + champion: string; + orgTimes: string; + ly_event_times: string; + ly_event_games: string; + ly_event_rank: string; + ly_max_inc: string; + ly_max_dis: string; + ly_beatit: string; + ly_shopid: string; + sleep: string; + pathWords: string; + fuxing: string; + kuzhu: string; + rank: string; + ifHonorShow: string; + locationupdatetime: string; + resideupdatetime: string; + ifManage: number; + age: number; + maxscore: string; + maxScoreTheYear: string; + beat: string; + rate: string; + Top3OfBeatUsernameScore: string[]; + KuZhu: KuZhu; + FuXing: KuZhu; + lastUpdate: string; + realpic: string; + realname: string; + qiupai: string; + qiupaitype: string; + fanshou: string; + fanshoutype: string; + zhengshou: string; + zhengshoutype: string; + resideprovince: string; + sex: string; + bg: string; + scope: string; + description: string; + ifHonor: number; + honors: Honor[]; + games: Games; + hasFollowed: number; + followed_count: number; +} + +interface Games { + data: GamesData[]; +} + +export interface GamesData { + gameid: string; + uid1: string; + uid2: string; + uid11: string; + uid22: string; + username1: string; + username2: string; + username11: string; + username22: string; + result1: string; + result2: string; + score1: string; + score2: string; + eventid: string; + ascore1: string; + dateline: string; + groupid: string; + flag: string; +} + +export interface KuZhu { + names: string[]; + uids: string[]; + gameids: string[]; + winlose: string[]; +} + +export interface Honor { + uid: string; + hid: string; + eventid: string; + itemid: string; + honor: string; + subject: string; + posttime: string; +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index f3c4e9c..da281d1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,8 +4,28 @@ import * as cheerio from "cheerio"; import fs from 'fs'; import path from 'path'; import { chunk } from 'lodash'; +import type { XCXProfile } from "./types/profile"; const BASE_URL = `https://kaiqiuwang.cc`; +const XCX_BASE_URL = `${BASE_URL}/xcx/public/index.php`; + +/** + * token: +XX-Device-Type: wxapp +content-type: application/json +Accept-Encoding: gzip,compress,br,deflate +User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.68(0x1800442a) NetType/WIFI Language/zh_CN +Referer: https://servicewechat.com/wxff09fb0e92aa456a/464/page-frame.html + */ +const xcxDefaultHeaders = { + 'token': 'e72b91bb-a690-44fe-9274-6a4a251f611b', + 'XX-Device-Type': 'wxapp', + 'content-type': 'application/json', + 'Accept-Encoding': 'gzip,compress,br,deflate', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.68(0x1800442a) NetType/WIFI Language/zh_CN', + 'Referer': 'https://servicewechat.com/wxff09fb0e92aa456a/464/page-frame.html', +}; + /** * @param tagid 俱乐部 ID */ @@ -62,6 +82,7 @@ export async function fetchEventContentHTML(matchId: string) { export function parseEventInfo(html: string) { const $ = cheerio.load(html); + const title = $('h2.title a').text(); const itemHref = $('.sub_menu a.active').attr('href') ?? ''; const itemId = /\S+item_id=(\d+)$/.exec(itemHref)?.[1] ?? ''; const players: Player[] = []; @@ -69,12 +90,14 @@ export function parseEventInfo(html: string) { for (const player of playersEl) { const img = $(player).find('.image img').attr('src') ?? ''; const name = $(player).find('h6').text().trim(); + const uid = /space-(?\d+).html/.exec($(player).find('h6 a').attr('href') ?? '')?.groups?.uid ?? ''; const info = $(player).find('p:nth-of-type(2)').text().replace(/\s/g, ''); const score = Number(/^.*?\b(\d+)\b/.exec(info)?.[1]); - players.push({ name, avatar: img, score, info }); + players.push({ name, avatar: img, score, info, uid }); } return { itemId, + title, players, } } @@ -108,4 +131,18 @@ export function sneckGroup(size: number, groupLen: number) { newGroups.push(group); } return newGroups; +} + +export async function getAdvProfile(uid: string) { + // return JSON.parse(fs.readFileSync( + // path.resolve(__dirname, '..', '__test__', 'data', 'profile.json'), + // ).toString()).data; + if (!/^\d+$/.test(uid)) return null; + if (!uid) return null; + const resp = await fetch(`${XCX_BASE_URL}/api/User/adv_profile?uid=${uid}`, { + headers: xcxDefaultHeaders, + }); + const data = await resp.json(); + if (data.code !== 1) return null; + return data.data as XCXProfile; } \ No newline at end of file