feat(Profile)
- add react-router
This commit is contained in:
parent
7ca9c46ba6
commit
2f82a4edf5
384
__test__/data/profile.json
Normal file
384
__test__/data/profile.json
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
@ -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", () => {
|
||||
|
||||
12
__test__/xcxapi.test.ts
Normal file
12
__test__/xcxapi.test.ts
Normal file
@ -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));
|
||||
});
|
||||
10
bun.lock
10
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=="],
|
||||
|
||||
@ -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",
|
||||
|
||||
22
src/App.tsx
22
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<IEventInfo>();
|
||||
const navigate = useNavigate();
|
||||
const handleGameClick = useCallback(async (game: IEventInfo) => {
|
||||
setGame(game);
|
||||
navigate(`/event/${game.matchId}`);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<h1>开球网比赛分组预测</h1>
|
||||
<ClubSelector onGameClick={handleGameClick} />
|
||||
<Drawer
|
||||
placement="bottom"
|
||||
title={game?.title}
|
||||
open={Boolean(game)}
|
||||
onClose={() => setGame(undefined)}
|
||||
size={'calc(100vh - 100px)'}
|
||||
>
|
||||
<GamePanel game={game} />
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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> = props => {
|
||||
const fetchPlayers = useRequest<MatchInfo | null, []>(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 (
|
||||
<Spin spinning={fetchPlayers.loading}>
|
||||
<>
|
||||
<Tabs
|
||||
items={fetchPlayers.loading ? [] : [
|
||||
items={[
|
||||
{
|
||||
key: 'groups',
|
||||
label: '分组预测',
|
||||
children: <GroupingPrediction sneckMode={sneckMode} players={fetchPlayers.data?.players} />
|
||||
children: <GroupingPrediction sneckMode={sneckMode} players={props.players} />
|
||||
},
|
||||
{
|
||||
key: 'players',
|
||||
label: '成员列表',
|
||||
children: <PlayerList players={fetchPlayers.data?.players} />
|
||||
children: <PlayerList players={props.players} />
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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> = props => {
|
||||
columns={[
|
||||
{ dataIndex: '_', render: (_, __, i) => `(${i + 1})` },
|
||||
{ dataIndex: 'index' },
|
||||
{ dataIndex: 'name' },
|
||||
{ dataIndex: 'name', render: (name, { uid }) => <User name={name} uid={uid} /> },
|
||||
{ dataIndex: 'score' },
|
||||
]}
|
||||
/>
|
||||
|
||||
@ -33,7 +33,7 @@ export const GroupingPrediction: React.FC<Props> = props => {
|
||||
}, [grouped, groupLen, maxPlayerSize]);
|
||||
return (
|
||||
<>
|
||||
<Form layout='horizontal'>
|
||||
<Flex gap={10} wrap>
|
||||
<Form.Item label={'取人数'}>
|
||||
<InputNumber
|
||||
value={maxPlayerSize}
|
||||
@ -52,7 +52,7 @@ export const GroupingPrediction: React.FC<Props> = props => {
|
||||
<Form.Item label="蛇形分组">
|
||||
<Switch checked={sneckMode} onChange={setSneckMode} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Flex>
|
||||
<Flex gap='middle' wrap align="center" justify="center">
|
||||
<React.Fragment key={'normal'}>
|
||||
{ !sneckMode && grouped.map((p, i) => <GroupMember key={i} players={p} index={i} />)}
|
||||
|
||||
@ -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> = props => {
|
||||
>
|
||||
<Table.Column width={32} dataIndex={'_'} align="center" render={(_, __, i) => `${i + 1}`} />
|
||||
<Table.Column width={32} dataIndex={'avatar'} align="center" render={src => <Avatar src={src} />} />
|
||||
<Table.Column width={200} title="姓名" align="center" dataIndex={'name'} />
|
||||
<Table.Column
|
||||
width={200}
|
||||
title="姓名"
|
||||
align="center"
|
||||
dataIndex={'name'}
|
||||
render={(name, { uid }) => <User name={name} uid={uid} />}
|
||||
/>
|
||||
<Table.Column width={200} dataIndex={'score'} title="积分" sorter={{
|
||||
compare: ({ score: a }: Player, { score: b}: Player) => a - b,
|
||||
}} />
|
||||
|
||||
11
src/components/User.tsx
Normal file
11
src/components/User.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { Link } from "react-router";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
uid: string;
|
||||
}
|
||||
export default function User(props: Props) {
|
||||
return (
|
||||
<Link to={`/profile/${props.uid}`}>{props.name}</Link>
|
||||
);
|
||||
}
|
||||
@ -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: <App />
|
||||
},
|
||||
{
|
||||
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 = (
|
||||
<StrictMode>
|
||||
<ConfigProvider theme={{
|
||||
algorithm: theme.darkAlgorithm,
|
||||
}}>
|
||||
<App />
|
||||
<RouterProvider router={route} />
|
||||
</ConfigProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
background-color: #242424;
|
||||
}
|
||||
#root {
|
||||
width: 100vw;
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
}
|
||||
body {
|
||||
margin: 0 auto;
|
||||
|
||||
@ -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" && {
|
||||
|
||||
12
src/page/EventPage.tsx
Normal file
12
src/page/EventPage.tsx
Normal file
@ -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<MatchInfo>();
|
||||
return (
|
||||
<div style={{ width: '100%', padding: 10, boxSizing: 'border-box' }}>
|
||||
<GamePanel title={game.title} players={game.players} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
72
src/page/ProfilePage.tsx
Normal file
72
src/page/ProfilePage.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<Divider>荣誉</Divider>
|
||||
<Flex vertical gap={12}>
|
||||
{props.honors?.map(honor => {
|
||||
return (
|
||||
<Flex key={honor.hid} gap={12} style={{ width: '100%' }}>
|
||||
<Image style={{ mixBlendMode: 'multiply' }} src={honor.honor} />
|
||||
<Typography.Text>{honor.subject}</Typography.Text>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Divider>装备</Divider>
|
||||
<Descriptions>
|
||||
<Descriptions.Item label="底板">
|
||||
{qiupaitype}
|
||||
<Typography.Text type="secondary" style={{ marginLeft: 4 }}>({qiupai})</Typography.Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="正手">
|
||||
{zhengshoutype}
|
||||
<Typography.Text type="secondary" style={{ marginLeft: 4 }}>({zhengshou})</Typography.Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="反手">
|
||||
{fanshoutype}
|
||||
<Typography.Text type="secondary" style={{ marginLeft: 4 }}>({fanshou})</Typography.Text>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProfilePage() {
|
||||
const profile = useLoaderData<XCXProfile | null>();
|
||||
return (
|
||||
<Flex vertical align="center" style={{ padding: 24 }}>
|
||||
<Avatar src={profile?.realpic} size={128} />
|
||||
<Typography.Title level={2}>{profile?.username}</Typography.Title>
|
||||
<Typography.Text>姓名:{profile?.realname}</Typography.Text>
|
||||
<Typography.Text>积分:{profile?.score}</Typography.Text>
|
||||
<Typography.Text>
|
||||
{
|
||||
[profile?.province, profile?.sex, profile?.bg, profile?.scope, ...profile?.allCities ?? []]
|
||||
.filter(Boolean).join(' | ')
|
||||
}
|
||||
</Typography.Text>
|
||||
<Divider>简介</Divider>
|
||||
<Typography.Paragraph>
|
||||
{profile?.description}
|
||||
</Typography.Paragraph>
|
||||
<Raket profile={profile} />
|
||||
<Honor honors={profile?.honors} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
18
src/store/useGameStore.ts
Normal file
18
src/store/useGameStore.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { create } from "zustand";
|
||||
import { type IEventInfo } from '../types';
|
||||
|
||||
interface StoreType {
|
||||
eventInfo?: IEventInfo;
|
||||
setEventInfo: (info?: IEventInfo) => void;
|
||||
}
|
||||
|
||||
|
||||
const useGameStore = create<StoreType>((set) => {
|
||||
return {
|
||||
setEventInfo: (info) => {
|
||||
set({ eventInfo: info });
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export default useGameStore;
|
||||
@ -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;
|
||||
}
|
||||
|
||||
export interface MatchInfo {
|
||||
itemId: string;
|
||||
players: Player[];
|
||||
uid: string;
|
||||
}
|
||||
116
src/types/profile.ts
Normal file
116
src/types/profile.ts
Normal file
@ -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;
|
||||
}
|
||||
39
src/utils.ts
39
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-(?<uid>\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,
|
||||
}
|
||||
}
|
||||
@ -109,3 +132,17 @@ export function sneckGroup(size: number, groupLen: number) {
|
||||
}
|
||||
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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user