feat(app): split game selector and implement club detail page

- **Frontend Refactoring**: Extracted EventCard logic and finished game filtering from GameSelector into a new ClubEventsList component. Removed direct routing logic (handleGameClick, navigate) from App.tsx in favor of router loaders.

- **New Feature - Club Detail Page**: Implemented /club/:id route with ClubEventsPage. Added server-side loader to fetch club info and events in parallel via new API endpoints (/api/club/find, /api/club/:id). Created KaiqiuService for direct data fetching.

- **API & Types**: Extended IEventInfo with isFinished flag; updated server-side parsing in utils/server.ts. Added ClubInfo type definition. Migrated club search and filtering logic to the server side.

- **Dependencies**: Updated antd from 6.2.1 to 6.3.2.
This commit is contained in:
kyuuseiryuu 2026-03-13 10:27:05 +09:00
parent c239b8bf40
commit b560684dfb
14 changed files with 293 additions and 82 deletions

28
__test__/kaiqiu.test.ts Normal file
View File

@ -0,0 +1,28 @@
import { test, expect } from 'bun:test';
import { KaiqiuService } from '../src/services/KaiqiuService';
test("Test find 东华 club", async () => {
const result = await KaiqiuService.findClub('东华', 1);
console.debug(result);
expect(result.clubs.length).toBeGreaterThan(0);
expect(result.total).toBeGreaterThan(result.clubs.length - 1);
const clubId = result.clubs[0]?.clubId ?? '';
const club = await KaiqiuService.getClubInfo(clubId);
console.debug(club);
expect(club).toBeDefined();
expect(club?.name).toInclude('东华');
expect(club?.img).toBeDefined();
});
test("Test find 飞酷 club", async () => {
const result = await KaiqiuService.findClub('飞酷', 1);
console.debug(result);
expect(result.clubs.length).toBeGreaterThan(0);
expect(result.total).toBeGreaterThan(result.clubs.length - 1);
});
test("Test find normal club", async () => {
const result = await KaiqiuService.findClub('政和', 1, true);
console.debug(result);
expect(result.clubs.length).toBeGreaterThan(0);
expect(result.total).toBeGreaterThan(result.clubs.length - 1);
});

View File

@ -10,7 +10,7 @@
"@prisma/adapter-mariadb": "^7.4.2",
"@prisma/client": "^7.4.2",
"ahooks": "^3.9.6",
"antd": "^6.2.1",
"antd": "^6.3.2",
"cheerio": "^1.2.0",
"dayjs": "^1.11.19",
"lodash": "^4.17.23",
@ -33,11 +33,11 @@
"packages": {
"@ant-design/colors": ["@ant-design/colors@8.0.1", "", { "dependencies": { "@ant-design/fast-color": "^3.0.0" } }, "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ=="],
"@ant-design/cssinjs": ["@ant-design/cssinjs@2.0.3", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1", "csstype": "^3.1.3", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-HAo8SZ3a6G8v6jT0suCz1270na6EA3obeJWM4uzRijBhdwdoMAXWK2f4WWkwB28yUufsfk3CAhN1coGPQq4kNQ=="],
"@ant-design/cssinjs": ["@ant-design/cssinjs@2.1.2", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1", "csstype": "^3.1.3", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ=="],
"@ant-design/cssinjs-utils": ["@ant-design/cssinjs-utils@2.0.2", "", { "dependencies": { "@ant-design/cssinjs": "^2.0.1", "@babel/runtime": "^7.23.2", "@rc-component/util": "^1.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-Mq3Hm6fJuQeFNKSp3+yT4bjuhVbdrsyXE2RyfpJFL0xiYNZdaJ6oFaE3zFrzmHbmvTd2Wp3HCbRtkD4fU+v2ZA=="],
"@ant-design/cssinjs-utils": ["@ant-design/cssinjs-utils@2.1.2", "", { "dependencies": { "@ant-design/cssinjs": "^2.1.2", "@babel/runtime": "^7.23.2", "@rc-component/util": "^1.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA=="],
"@ant-design/fast-color": ["@ant-design/fast-color@3.0.0", "", {}, "sha512-eqvpP7xEDm2S7dUzl5srEQCBTXZMmY3ekf97zI+M2DHOYyKdJGH0qua0JACHTqbkRnD/KHFQP9J1uMJ/XWVzzA=="],
"@ant-design/fast-color": ["@ant-design/fast-color@3.0.1", "", {}, "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw=="],
"@ant-design/icons": ["@ant-design/icons@6.1.0", "", { "dependencies": { "@ant-design/colors": "^8.0.0", "@ant-design/icons-svg": "^4.4.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-KrWMu1fIg3w/1F2zfn+JlfNDU8dDqILfA5Tg85iqs1lf8ooyGlbkA+TkwfOKKgqpUmAiRY1PTFpuOU2DAIgSUg=="],
@ -109,23 +109,23 @@
"@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=="],
"@rc-component/cascader": ["@rc-component/cascader@1.14.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.2.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-Ip9356xwZUR2nbW5PRVGif4B/bDve4pLa/N+PGbvBaTnjbvmN4PFMBGQSmlDlzKP1ovxaYMvwF/dI9lXNLT4iQ=="],
"@rc-component/checkbox": ["@rc-component/checkbox@1.0.1", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-08yTH8m+bSm8TOqbybbJ9KiAuIATti6bDs2mVeSfu4QfEnyeF6X0enHVvD1NEAyuBWEAo56QtLe++MYs2D9XiQ=="],
"@rc-component/checkbox": ["@rc-component/checkbox@2.0.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ=="],
"@rc-component/collapse": ["@rc-component/collapse@1.2.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-ZRYSKSS39qsFx93p26bde7JUZJshsUBEQRlRXPuJYlAiNX0vyYlF5TsAm8JZN3LcF8XvKikdzPbgAtXSbkLUkw=="],
"@rc-component/color-picker": ["@rc-component/color-picker@3.0.3", "", { "dependencies": { "@ant-design/fast-color": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-V7gFF9O7o5XwIWafdbOtqI4BUUkEUkgdBwp6favy3xajMX/2dDqytFaiXlcwrpq6aRyPLp5dKLAG5RFKLXMeGA=="],
"@rc-component/color-picker": ["@rc-component/color-picker@3.1.1", "", { "dependencies": { "@ant-design/fast-color": "^3.0.1", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-OHaCHLHszCegdXmIq2ZRIZBN/EtpT6Wm8SG/gpzLATHbVKc/avvuKi+zlOuk05FTWvgaMmpxAko44uRJ3M+2pg=="],
"@rc-component/context": ["@rc-component/context@2.0.1", "", { "dependencies": { "@rc-component/util": "^1.3.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw=="],
"@rc-component/dialog": ["@rc-component/dialog@1.8.0", "", { "dependencies": { "@rc-component/motion": "^1.1.3", "@rc-component/portal": "^2.1.0", "@rc-component/util": "^1.5.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-zGksezfULKixYCIWctIhUC2M3zUJrc81JKWbi9dJrQdPaM7J+8vSOrhLoOHHkZFpBpb2Ri6JqnSuGYb2N+FrRA=="],
"@rc-component/dialog": ["@rc-component/dialog@1.8.4", "", { "dependencies": { "@rc-component/motion": "^1.1.3", "@rc-component/portal": "^2.1.0", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-Ay6PM7phkTkquplG8fWfUGFZ2GTLx9diTl4f0d8Eqxd7W1u1KjE9AQooFQHOHnhZf0Ya3z51+5EKCWHmt/dNEw=="],
"@rc-component/drawer": ["@rc-component/drawer@1.4.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.1.3", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-Zr1j1LRLDauz4a5JXHEmeYQfvEzfh4CddNa7tszyJnfd5GySYdZ5qLO63Tt2tgG4k+qi6tkFDKmcT46ikZfzbQ=="],
"@rc-component/drawer": ["@rc-component/drawer@1.4.2", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.1.3", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q=="],
"@rc-component/dropdown": ["@rc-component/dropdown@1.0.2", "", { "dependencies": { "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.11.0", "react-dom": ">=16.11.0" } }, "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg=="],
"@rc-component/form": ["@rc-component/form@1.6.2", "", { "dependencies": { "@rc-component/async-validator": "^5.1.0", "@rc-component/util": "^1.6.2", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-OgIn2RAoaSBqaIgzJf/X6iflIa9LpTozci1lagLBdURDFhGA370v0+T0tXxOi8YShMjTha531sFhwtnrv+EJaQ=="],
"@rc-component/form": ["@rc-component/form@1.7.2", "", { "dependencies": { "@rc-component/async-validator": "^5.1.0", "@rc-component/util": "^1.6.2", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-5C90rXH7aZvvvxB4M5ew+QxROvimdL/lqhSshR8NsyiR7HKOoGQYSitxdfENnH6/0KNFxEy2ranVe2LrTnHZIw=="],
"@rc-component/image": ["@rc-component/image@1.6.0", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/portal": "^2.1.2", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-tSfn2ZE/oP082g4QIOxeehkmgnXB7R+5AFj/lIFr4k7pEuxHBdyGIq9axoCY9qea8NN0DY6p4IB/F07tLqaT5A=="],
@ -139,7 +139,7 @@
"@rc-component/mini-decimal": ["@rc-component/mini-decimal@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.18.0" } }, "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ=="],
"@rc-component/motion": ["@rc-component/motion@1.1.6", "", { "dependencies": { "@rc-component/util": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aEQobs/YA0kqRvHIPjQvOytdtdRVyhf/uXAal4chBjxDu6odHckExJzjn2D+Ju1aKK6hx3pAs6BXdV9+86xkgQ=="],
"@rc-component/motion": ["@rc-component/motion@1.3.1", "", { "dependencies": { "@rc-component/util": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Wo1mkd0tCcHtvYvpPOmlYJz546z16qlsiwaygmW7NPJpOZOF9GBjhGzdzZSsC2lEJ1IUkWLF4gMHlRA1aSA+Yw=="],
"@rc-component/mutate-observer": ["@rc-component/mutate-observer@2.0.1", "", { "dependencies": { "@rc-component/util": "^1.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w=="],
@ -163,7 +163,7 @@
"@rc-component/segmented": ["@rc-component/segmented@1.3.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg=="],
"@rc-component/select": ["@rc-component/select@1.5.1", "", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-ARXtwfCVnpDJj1bQjh1cimUlNQkZiN72hvtL2G4mKXIYfkokYdA2Vyu2deAfY7kuHSWpmZygVuohQt6TxOYjnA=="],
"@rc-component/select": ["@rc-component/select@1.6.14", "", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-T1IWeLlSas7Z/igZtPtJ/bweCxMMkXIGKQBtnigK+I/n1AVNjCs+ZdL3Fj42mq3uqm4sd1uzeQLZkdCqR26ADw=="],
"@rc-component/slider": ["@rc-component/slider@1.0.1", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g=="],
@ -181,9 +181,9 @@
"@rc-component/tour": ["@rc-component/tour@2.3.0", "", { "dependencies": { "@rc-component/portal": "^2.2.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.7.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-K04K9r32kUC+auBSQfr+Fss4SpSIS9JGe56oq/ALAX0p+i2ylYOI1MgR83yBY7v96eO6ZFXcM/igCQmubps0Ow=="],
"@rc-component/tree": ["@rc-component/tree@1.1.0", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/util": "^1.2.1", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-HZs3aOlvFgQdgrmURRc/f4IujiNBf4DdEeXUlkS0lPoLlx9RoqsZcF0caXIAMVb+NaWqKtGQDnrH8hqLCN5zlA=="],
"@rc-component/tree": ["@rc-component/tree@1.2.4", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/util": "^1.8.1", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-5Gli43+m4R7NhpYYz3Z61I6LOw9yI6CNChxgVtvrO6xB1qML7iE6QMLVMB3+FTjo2yF6uFdAHtqWPECz/zbX5w=="],
"@rc-component/tree-select": ["@rc-component/tree-select@1.6.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": "*", "react-dom": "*" } }, "sha512-UvEGmZT+gcVvRwImAZg3/sXw9nUdn4FmCs1rSIMWjEXEIAo0dTGmIyWuLCvs+1rGe9AZ7CHMPiQUEbdadwV0fw=="],
"@rc-component/tree-select": ["@rc-component/tree-select@1.8.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.2.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-iYsPq3nuLYvGqdvFAW+l+I9ASRIOVbMXyA8FGZg2lGym/GwkaWeJGzI4eJ7c9IOEhRj0oyfIN4S92Fl3J05mjQ=="],
"@rc-component/trigger": ["@rc-component/trigger@3.9.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.2.0", "@rc-component/resize-observer": "^1.1.1", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-X8btpwfrT27AgrZVOz4swclhEHTZcqaHeQMXXBgveagOiakTa36uObXbdwerXffgV8G9dH1fAAE0DHtVQs8EHg=="],
@ -215,7 +215,7 @@
"ahooks": ["ahooks@3.9.6", "", { "dependencies": { "@babel/runtime": "^7.21.0", "@types/js-cookie": "^3.0.6", "dayjs": "^1.9.1", "intersection-observer": "^0.12.0", "js-cookie": "^3.0.5", "lodash": "^4.17.21", "react-fast-compare": "^3.2.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.0", "tslib": "^2.4.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Mr7f05swd5SmKlR9SZo5U6M0LsL4ErweLzpdgXjA1JPmnZ78Vr6wzx0jUtvoxrcqGKYnX0Yjc02iEASVxHFPjQ=="],
"antd": ["antd@6.2.1", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/cssinjs": "^2.0.3", "@ant-design/cssinjs-utils": "^2.0.2", "@ant-design/fast-color": "^3.0.0", "@ant-design/icons": "^6.1.0", "@ant-design/react-slick": "~2.0.0", "@babel/runtime": "^7.28.4", "@rc-component/cascader": "~1.11.0", "@rc-component/checkbox": "~1.0.1", "@rc-component/collapse": "~1.2.0", "@rc-component/color-picker": "~3.0.3", "@rc-component/dialog": "~1.8.0", "@rc-component/drawer": "~1.4.0", "@rc-component/dropdown": "~1.0.2", "@rc-component/form": "~1.6.2", "@rc-component/image": "~1.6.0", "@rc-component/input": "~1.1.2", "@rc-component/input-number": "~1.6.2", "@rc-component/mentions": "~1.6.0", "@rc-component/menu": "~1.2.0", "@rc-component/motion": "~1.1.6", "@rc-component/mutate-observer": "^2.0.1", "@rc-component/notification": "~1.2.0", "@rc-component/pagination": "~1.2.0", "@rc-component/picker": "~1.9.0", "@rc-component/progress": "~1.0.2", "@rc-component/qrcode": "~1.1.1", "@rc-component/rate": "~1.0.1", "@rc-component/resize-observer": "^1.1.1", "@rc-component/segmented": "~1.3.0", "@rc-component/select": "~1.5.0", "@rc-component/slider": "~1.0.1", "@rc-component/steps": "~1.2.2", "@rc-component/switch": "~1.0.3", "@rc-component/table": "~1.9.1", "@rc-component/tabs": "~1.7.0", "@rc-component/textarea": "~1.1.2", "@rc-component/tooltip": "~1.4.0", "@rc-component/tour": "~2.3.0", "@rc-component/tree": "~1.1.0", "@rc-component/tree-select": "~1.6.0", "@rc-component/trigger": "^3.9.0", "@rc-component/upload": "~1.1.0", "@rc-component/util": "^1.7.0", "clsx": "^2.1.1", "dayjs": "^1.11.11", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-ycw/XX7So4MdrwYKGfvZJdkGiCYUOSTebAIi+ejE95WJ138b11oy/iJg7iH0qydaD/B5sFd7Tz8XfPBuW7CRmw=="],
"antd": ["antd@6.3.2", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/cssinjs": "^2.1.2", "@ant-design/cssinjs-utils": "^2.1.2", "@ant-design/fast-color": "^3.0.1", "@ant-design/icons": "^6.1.0", "@ant-design/react-slick": "~2.0.0", "@babel/runtime": "^7.28.4", "@rc-component/cascader": "~1.14.0", "@rc-component/checkbox": "~2.0.0", "@rc-component/collapse": "~1.2.0", "@rc-component/color-picker": "~3.1.1", "@rc-component/dialog": "~1.8.4", "@rc-component/drawer": "~1.4.2", "@rc-component/dropdown": "~1.0.2", "@rc-component/form": "~1.7.1", "@rc-component/image": "~1.6.0", "@rc-component/input": "~1.1.2", "@rc-component/input-number": "~1.6.2", "@rc-component/mentions": "~1.6.0", "@rc-component/menu": "~1.2.0", "@rc-component/motion": "^1.3.1", "@rc-component/mutate-observer": "^2.0.1", "@rc-component/notification": "~1.2.0", "@rc-component/pagination": "~1.2.0", "@rc-component/picker": "~1.9.0", "@rc-component/progress": "~1.0.2", "@rc-component/qrcode": "~1.1.1", "@rc-component/rate": "~1.0.1", "@rc-component/resize-observer": "^1.1.1", "@rc-component/segmented": "~1.3.0", "@rc-component/select": "~1.6.14", "@rc-component/slider": "~1.0.1", "@rc-component/steps": "~1.2.2", "@rc-component/switch": "~1.0.3", "@rc-component/table": "~1.9.1", "@rc-component/tabs": "~1.7.0", "@rc-component/textarea": "~1.1.2", "@rc-component/tooltip": "~1.4.0", "@rc-component/tour": "~2.3.0", "@rc-component/tree": "~1.2.3", "@rc-component/tree-select": "~1.8.0", "@rc-component/trigger": "^3.9.0", "@rc-component/upload": "~1.1.0", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1", "dayjs": "^1.11.11", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-IlMoqaXlq5Bgxi0ANERhAzmDREYyGwr/U7MCVihaUQbE/ZOB3r4ArakUxjA1ULYNDA6K00dawSrB8aalGnZlLA=="],
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
@ -471,8 +471,14 @@
"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=="],
"@ant-design/colors/@ant-design/fast-color": ["@ant-design/fast-color@3.0.0", "", {}, "sha512-eqvpP7xEDm2S7dUzl5srEQCBTXZMmY3ekf97zI+M2DHOYyKdJGH0qua0JACHTqbkRnD/KHFQP9J1uMJ/XWVzzA=="],
"@ant-design/cssinjs/@emotion/unitless": ["@emotion/unitless@0.7.5", "", {}, "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="],
"@ant-design/cssinjs/@rc-component/util": ["@rc-component/util@1.9.0", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg=="],
"@ant-design/cssinjs-utils/@rc-component/util": ["@rc-component/util@1.9.0", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg=="],
"@chevrotain/cst-dts-gen/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"@chevrotain/gast/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
@ -485,6 +491,42 @@
"@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="],
"@rc-component/cascader/@rc-component/util": ["@rc-component/util@1.9.0", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg=="],
"@rc-component/checkbox/@rc-component/util": ["@rc-component/util@1.9.0", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg=="],
"@rc-component/collapse/@rc-component/motion": ["@rc-component/motion@1.1.6", "", { "dependencies": { "@rc-component/util": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aEQobs/YA0kqRvHIPjQvOytdtdRVyhf/uXAal4chBjxDu6odHckExJzjn2D+Ju1aKK6hx3pAs6BXdV9+86xkgQ=="],
"@rc-component/color-picker/@rc-component/util": ["@rc-component/util@1.9.0", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg=="],
"@rc-component/dialog/@rc-component/util": ["@rc-component/util@1.9.0", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg=="],
"@rc-component/drawer/@rc-component/util": ["@rc-component/util@1.9.0", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg=="],
"@rc-component/form/@rc-component/util": ["@rc-component/util@1.9.0", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg=="],
"@rc-component/image/@rc-component/motion": ["@rc-component/motion@1.1.6", "", { "dependencies": { "@rc-component/util": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aEQobs/YA0kqRvHIPjQvOytdtdRVyhf/uXAal4chBjxDu6odHckExJzjn2D+Ju1aKK6hx3pAs6BXdV9+86xkgQ=="],
"@rc-component/menu/@rc-component/motion": ["@rc-component/motion@1.1.6", "", { "dependencies": { "@rc-component/util": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aEQobs/YA0kqRvHIPjQvOytdtdRVyhf/uXAal4chBjxDu6odHckExJzjn2D+Ju1aKK6hx3pAs6BXdV9+86xkgQ=="],
"@rc-component/motion/@rc-component/util": ["@rc-component/util@1.9.0", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg=="],
"@rc-component/notification/@rc-component/motion": ["@rc-component/motion@1.1.6", "", { "dependencies": { "@rc-component/util": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aEQobs/YA0kqRvHIPjQvOytdtdRVyhf/uXAal4chBjxDu6odHckExJzjn2D+Ju1aKK6hx3pAs6BXdV9+86xkgQ=="],
"@rc-component/segmented/@rc-component/motion": ["@rc-component/motion@1.1.6", "", { "dependencies": { "@rc-component/util": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aEQobs/YA0kqRvHIPjQvOytdtdRVyhf/uXAal4chBjxDu6odHckExJzjn2D+Ju1aKK6hx3pAs6BXdV9+86xkgQ=="],
"@rc-component/select/@rc-component/util": ["@rc-component/util@1.9.0", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg=="],
"@rc-component/tabs/@rc-component/motion": ["@rc-component/motion@1.1.6", "", { "dependencies": { "@rc-component/util": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aEQobs/YA0kqRvHIPjQvOytdtdRVyhf/uXAal4chBjxDu6odHckExJzjn2D+Ju1aKK6hx3pAs6BXdV9+86xkgQ=="],
"@rc-component/tree/@rc-component/util": ["@rc-component/util@1.9.0", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg=="],
"@rc-component/tree-select/@rc-component/util": ["@rc-component/util@1.9.0", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg=="],
"@rc-component/trigger/@rc-component/motion": ["@rc-component/motion@1.1.6", "", { "dependencies": { "@rc-component/util": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aEQobs/YA0kqRvHIPjQvOytdtdRVyhf/uXAal4chBjxDu6odHckExJzjn2D+Ju1aKK6hx3pAs6BXdV9+86xkgQ=="],
"antd/@rc-component/util": ["@rc-component/util@1.9.0", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg=="],
"chevrotain/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],

View File

@ -14,7 +14,7 @@
"@prisma/adapter-mariadb": "^7.4.2",
"@prisma/client": "^7.4.2",
"ahooks": "^3.9.6",
"antd": "^6.2.1",
"antd": "^6.3.2",
"cheerio": "^1.2.0",
"dayjs": "^1.11.19",
"lodash": "^4.17.23",

View File

@ -1,19 +1,12 @@
import { useCallback } from "react";
import { ClubSelector } from "./components/GameSelector";
import type { IEventInfo } from "./types";
import { useNavigate } from "react-router";
import { Typography } from "antd";
import "./index.css";
export function App() {
const navigate = useNavigate();
const handleGameClick = useCallback(async (game: IEventInfo) => {
navigate(`/event/${game.matchId}`);
}, []);
return (
<div className="app">
<Typography.Title level={1}></Typography.Title>
<ClubSelector onGameClick={handleGameClick} />
<ClubSelector />
</div>
);
}

View File

@ -0,0 +1,34 @@
import { Divider, Empty, Flex, Space, Switch, Typography } from "antd";
import type { IEventInfo } from "../types";
import { useMemo, useState } from "react";
import { EventCard } from "./EventCard";
interface Props {
events: IEventInfo[];
}
export const ClubEvenList = (props: Props) => {
const [showAll, setShowAll] = useState(false);
const visibleEvents = useMemo(() => {
if (showAll) return props.events;
return props.events.filter(e => !e.isFinished);
}, [showAll, props.events]);
return (
<>
<Divider>
<Space>
<Typography.Text type="secondary"></Typography.Text>
<Switch checked={showAll} onChange={() => setShowAll(!showAll)} unCheckedChildren="隐藏" checkedChildren="显示" />
</Space>
</Divider>
<Flex wrap vertical gap={12} justify="center" align="center">
{visibleEvents.length ? visibleEvents.map(e => {
return (
<div key={e.matchId} style={{ width: '100%', maxWidth: 600 }}>
<EventCard eventInfo={e} />
</div>
);
}) : <Empty description="暂无活动" />}
</Flex>
</>
);
}

View File

@ -0,0 +1,48 @@
import { Button, Card, Statistic, Typography } from "antd";
import type { IEventInfo } from "../types";
import dayjs from "dayjs";
import { EyeOutlined } from "@ant-design/icons";
import { useCallback, useMemo } from "react";
import { useNavigate } from "react-router";
interface EventCardProps {
eventInfo: IEventInfo;
}
export function EventCard(props: EventCardProps) {
const { eventInfo: e } = props;
const day = dayjs(e.startDate);
const navigate = useNavigate();
const handleView = useCallback(() => {
navigate(`/event/${e.matchId}`);
}, [e]);
return (
<Card
key={e.matchId}
title={e.title}
style={{ width: '100%' }}
actions={[
<Button
type="link"
onClick={handleView}
icon={<EyeOutlined />}
>
</Button>
]}
>
<Typography.Text type={e.isFinished ? undefined : 'success'}>{e.title}</Typography.Text>
<Statistic.Timer
type={e.isFinished ? 'countup' : 'countdown'}
value={day.toDate().getTime()}
format={`距离比赛${e.isFinished ? '结束' : '开始'}: DD 天${e.isFinished ? '' : ' HH 时'}`}
styles={{ content: e.isFinished ? { color: 'gray' } : {} }}
/>
{e.info.map(e => (
<div key={e}>
<Typography.Text type='secondary'>{e}</Typography.Text>
</div>
))}
</Card>
)
}

View File

@ -1,14 +1,13 @@
import { Button, Card, Divider, Flex, Select, Skeleton, Space, Statistic, Switch, Typography } from 'antd';
import { Flex, Select, Space } from 'antd';
import type React from 'react';
import { useRequest } from 'ahooks';
import { clubs } from './clubList';
import { useCallback, useEffect, useMemo, useState } from 'react';
import dayjs from 'dayjs';
import type { IEventInfo } from '../../types';
import { EyeOutlined } from '@ant-design/icons';
import { ClubEvenList } from '../ClubEventList';
interface Props {
onGameClick?: (info: IEventInfo) => void;
}
export const GameSelector: React.FC<Props> = props => {
@ -44,10 +43,6 @@ export const GameSelector: React.FC<Props> = props => {
return (
<Space orientation='vertical' style={{ width: '100%' }}>
<Flex vertical gap={12} justify='center'>
<Space style={{ alignItems: 'self-end' }}>
<Typography.Text></Typography.Text>
<Switch checkedChildren='显示' unCheckedChildren='隐藏' checked={showFinished} onChange={e => setShowFinished(e)} />
</Space>
<Select
style={{ width: '100%' }}
placeholder={'请选择俱乐部'}
@ -57,54 +52,7 @@ export const GameSelector: React.FC<Props> = props => {
onChange={handleClubChange}
/>
</Flex>
<Divider>{isEmpty && (<Typography.Text type='secondary'></Typography.Text>)}</Divider>
{requestEvents.loading ? <Skeleton.Button active block style={{ height: 300 }} /> : (
<Flex wrap gap={12} justify='center'>
{gameList
?.filter(e => showFinished || !e.finished)
?.map(e => <EventCard key={e.matchId} eventInfo={e} onGameClick={props.onGameClick} />)
}
</Flex>
)}
<ClubEvenList events={requestEvents.data ?? []} />
</Space>
);
}
interface EventCardProps {
eventInfo: IEventInfo & { finished: boolean };
onGameClick?: (info: IEventInfo) => void;
}
function EventCard(props: EventCardProps) {
const { eventInfo: e } = props;
const day = dayjs(e.startDate);
return (
<Card
key={e.matchId}
title={e.title}
style={{ width: '100%' }}
actions={[
<Button
type="link"
onClick={() => props.onGameClick?.(e)}
icon={<EyeOutlined />}
>
</Button>
]}
>
<Typography.Text type={e.finished ? undefined : 'success'}>{e.title}</Typography.Text>
<Statistic.Timer
type={e.finished ? 'countup' : 'countdown'}
value={day.toDate().getTime()}
format={`距离比赛${e.finished ? '结束' : '开始'}: DD 天${e.finished ? '' : ' HH 时'}`}
styles={{ content: e.finished ? { color: 'gray' } : {} }}
/>
{e.info.map(e => (
<div key={e}>
<Typography.Text type='secondary'>{e}</Typography.Text>
</div>
))}
</Card>
)
}

View File

@ -4,12 +4,30 @@ import index from "./index.html";
import { getUidScore } from "./services/uidScoreStore";
import { checkIsUserFav, favPlayer, listFavPlayers, unFavPlayer } from "./services/favPlayerService";
import { BattleService } from "./services/BattleService";
import { KaiqiuService } from "./services/KaiqiuService";
const server = serve({
port: process.env.PORT || 3000,
routes: {
// Serve index.html for all unmatched routes.
"/*": index,
"/api/club/find": {
async GET(req) {
const searchParams = new URL(req.url).searchParams;
const key = searchParams.get('key') ?? '';
const normalClub = searchParams.get('normalClub');
const page = Number(searchParams.get('page'));
const data = await KaiqiuService.findClub(key, page, Boolean(normalClub));
return Response.json(data);
}
},
"/api/club/:id": {
async GET(req) {
const id = req.params.id;
const data = await KaiqiuService.getClubInfo(id);
return Response.json(data);
}
},
"/api/events/:clubid": {
async GET(req) {
const data = await listEvent(req.params.clubid);

29
src/page/ClubEvents.tsx Normal file
View File

@ -0,0 +1,29 @@
import { Avatar, Button, Drawer, Typography } from "antd";
import { useLoaderData } from "react-router";
import type { ClubInfo, IEventInfo } from "../types";
import { useState } from "react";
import { ChangeBackground } from "../components/ChangeBackground";
import { ClubEvenList } from "../components/ClubEventList";
export const ClubEventsPage = () => {
const { info, events } = useLoaderData<{
info: ClubInfo;
events: IEventInfo[];
}>();
const [isArticleOpen, setIsArticleOpen] = useState(false);
return (
<div className="app">
<ChangeBackground url={info.img} />
<Avatar src={info.img} size={80} />
<Typography.Title>{info.name}</Typography.Title>
<Button onClick={() => setIsArticleOpen(true)}></Button>
<ClubEvenList events={events} />
<Drawer size={'60vh'} open={isArticleOpen} onClose={() => setIsArticleOpen(false)} placement="bottom">
<div style={{ textAlign: 'center' }}>
<Typography.Paragraph style={{ whiteSpace: 'pre', textWrap: 'auto' }}>{info.article}</Typography.Paragraph>
</div>
</Drawer>
</div>
);
}

View File

@ -1,5 +1,5 @@
import { useLocalStorageState, useRequest } from "ahooks";
import { Input, Table, Spin, Typography, Flex, Space } from "antd";
import { Input, Table, Spin, Typography, Flex } from "antd";
import type { XCXFindUser, XCXFindUserResp } from "../types";
import User from "../components/User";
import { SEX } from "../utils/front";

View File

@ -9,6 +9,7 @@ import { FavePlayersPage } from "./page/FavPlayersPage";
import { UserCenter } from "./page/UserCenter";
import { CallbackPage } from "./page/Logto/Callback";
import App from "./App";
import { ClubEventsPage } from "./page/ClubEvents";
function HydrateFallback() {
return (
@ -38,12 +39,25 @@ export const route = createBrowserRouter([
Component: App,
HydrateFallback: () => <HydrateFallback />
},
{
path: '/club/:id',
Component: ClubEventsPage,
HydrateFallback: () => <HydrateFallback />,
loader: async ({ params }) => {
const id = params.id;
const [info, events] = await Promise.all([
fetch(`/api/club/${id}`).then(r => r.json()),
fetch(`/api/events/${id}`).then(r => r.json()),
]);
return { info, events };
},
},
{
path: '/fav-players',
Component: FavePlayersPage,
},
{
path: 'event/:matchId',
path: '/event/:matchId',
loader: async ({ params }) => {
const info: MatchInfo = await (await fetch(`/api/match/${params.matchId}`)).json();
const members = info.itemId

View File

@ -0,0 +1,47 @@
import * as cheerio from "cheerio";
import { htmlRequestHeaders } from "../utils/server";
export class KaiqiuService {
static #baseURL = 'https://kaiqiuwang.cc';
public static async findClub(name: string, page = 1, normalClub?: boolean) {
const searchKey = encodeURIComponent(name);
const url = `${this.#baseURL}/home/space.php?province=&city=&searchkey=${searchKey}&searchsubmit=%E6%90%9C%E7%B4%A2&searchmode=1&do=mtag&view=hot&page=${page}`;
const html = await fetch(url, {
headers: htmlRequestHeaders,
}).then(res => res.text());
return this.parseSearchClubPage(html, Boolean(normalClub));
}
private static parseSearchClubPage(html: string, normal: boolean) {
const $ = cheerio.load(html);
const infos = $(`#content > div:nth-of-type(${normal ? 2 : 1}) td[width="250"]`).toArray();
const imgs = $(`#content > div:nth-of-type(${normal ? 2 : 1}) td[width="80"]`).toArray();
const parseInfo = (info: typeof infos[number], index: number) => {
const name = $(info).find('a').text().trim();
const url = $(info).find('a').attr('href') ?? '';
const clubId = /-(?<id>\d+).html$/.exec(url)?.groups?.id
const members = parseInt($(info).find('span.num').text().trim());
const area = $(info).find('div.gray:nth-of-type(1)').text().trim();
const src = $(imgs[index]).find('.threadimg60 img').attr('src') ?? '';
const img = src.startsWith('http') ? src : 'https://kaiqiuwang.cc/home/image/nologo.jpg';
return { name, clubId, members, area, img, url: `${this.#baseURL}/home/${url}` };
}
const clubs = infos.map((e, i) => parseInfo(e, i));
const total = parseInt($('#content > div:nth-of-type(1) div.page > em').text().trim()) || clubs.length;
return { clubs, total };
}
public static async getClubInfo(clubId: string) {
if (!clubId) return null;
const url = `${this.#baseURL}/home/space-mtag-tagid-${clubId}.html`;
const html = await fetch(url, { headers: htmlRequestHeaders }).then(res => res.text());
const $ = cheerio.load(html);
const name = $('h2.title a:nth-of-type(2)').text().trim();
const src = $('#space_avatar img').attr('src') ?? '';
const img = src.startsWith('http') ? src : 'https://kaiqiuwang.cc/home/image/nologo.jpg';
const article = $('.article').text().trim();
return { id: clubId, name, img, article };
}
}

View File

@ -9,6 +9,7 @@ export interface IEventInfo {
url: string;
matchId: string;
startDate: string;
isFinished: boolean;
}
export interface MatchInfo {
@ -56,4 +57,11 @@ export interface XCXFindUserResp {
per_page: number;
total: number;
data: XCXFindUser[];
}
export interface ClubInfo {
id: string;
name: string;
article: string;
img: string;
}

View File

@ -3,6 +3,7 @@ import * as cheerio from "cheerio";
import { XCXAPI } from "../services/xcxApi";
import { BASE_URL } from "./common";
import { RedisClient } from "bun";
import dayjs from "dayjs";
const REQUIRED_ENVS = [
process.env.KAIQIUCC_TOKEN,
@ -22,7 +23,7 @@ export const xcxApi = new XCXAPI(process.env.KAIQIUCC_TOKEN ?? '');
export const redis = new RedisClient(process.env.REDIS ?? '');
const htmlRequestHeaders = {
export const htmlRequestHeaders = {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"accept-language": "zh-CN,zh;q=0.8",
"cache-control": "max-age=0",
@ -89,6 +90,7 @@ export async function parseEventList(html: string) {
url: eventURL,
startDate,
matchId,
isFinished: dayjs(startDate).isBefore(),
}
list.push(event);
}