init
This commit is contained in:
commit
0fed35fb48
1
.env.example
Normal file
1
.env.example
Normal file
@ -0,0 +1 @@
|
||||
KAIQIUCC_TOKEN=''
|
||||
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
|
||||
__test__/data/*
|
||||
21
README.md
Normal file
21
README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# bun-react-template
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To start a development server:
|
||||
|
||||
```bash
|
||||
bun dev
|
||||
```
|
||||
|
||||
To run for production:
|
||||
|
||||
```bash
|
||||
bun start
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.2.21. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||
18
__test__/utils.load-html.test.ts
Normal file
18
__test__/utils.load-html.test.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { fetchEventContentHTML, fetchEventListHTML } from '../src/utils';
|
||||
|
||||
test('load html', async () => {
|
||||
const saveTo = path.resolve(__dirname, 'data', 'view-event.html');
|
||||
const html = await fetchEventListHTML(`47`);
|
||||
fs.writeFileSync(saveTo, html?? '');
|
||||
expect(html?.length).not.toBe(0);
|
||||
});
|
||||
|
||||
test('load html', async () => {
|
||||
const saveTo = path.resolve(__dirname, 'data', 'view-event-content.html');
|
||||
const html = await fetchEventContentHTML('167684');
|
||||
fs.writeFileSync(saveTo, html?? '');
|
||||
expect(html?.length).not.toBe(0);
|
||||
});
|
||||
45
__test__/utils.test.ts
Normal file
45
__test__/utils.test.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import fs from 'fs';
|
||||
import { expect, test } from 'bun:test';
|
||||
import path from 'path';
|
||||
import { parseEventInfo, parseEventList, sneckGroup } from '../src/utils';
|
||||
|
||||
|
||||
const matchId = '167684';
|
||||
const item_id = '7098063';
|
||||
test('event list not empty', () => {
|
||||
const html = fs.readFileSync(path.resolve(__dirname, 'data', 'view-event.html')).toString();
|
||||
const list = parseEventList(html);
|
||||
expect(list.length).toBe(10);
|
||||
console.log(list[0]?.matchId);
|
||||
});
|
||||
|
||||
test('event content not empty', () => {
|
||||
const html = fs.readFileSync(
|
||||
path.resolve(__dirname, 'data', 'view-event-content.html')
|
||||
).toString();
|
||||
const { itemId, players } = parseEventInfo(html);
|
||||
expect(itemId).toBe(item_id);
|
||||
expect(players.length).toBeGreaterThan(0);
|
||||
console.log(players);
|
||||
expect(players[0]?.uid).not.toBe('');
|
||||
});
|
||||
|
||||
test("group", () => {
|
||||
const group = sneckGroup(48, 6);
|
||||
/**
|
||||
* A B
|
||||
* [01] [07]
|
||||
* [02] [08]
|
||||
* [05] [09]
|
||||
* [06] [10]
|
||||
* A B
|
||||
* [01] [08]
|
||||
* [07] [02]
|
||||
* [05] [10]
|
||||
* [09] [06]
|
||||
*/
|
||||
expect(group?.[0]?.[0]).toBe(0);
|
||||
expect(group?.[0]?.[1]).toBe(11);
|
||||
expect(group?.[1]?.[0]).toBe(1);
|
||||
expect(group?.[1]?.[1]).toBe(10);
|
||||
})
|
||||
21
__test__/xcxapi.test.ts
Normal file
21
__test__/xcxapi.test.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { test, expect } from 'bun:test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { XCXAPI } from '../src/services/xcxApi';
|
||||
|
||||
const xcxApi = new XCXAPI(process.env.KAIQIUCC_TOKEN ?? '');
|
||||
|
||||
test('Test profile', async () => {
|
||||
const uid = '73276';
|
||||
const profile = await xcxApi.getAdvProfile(uid);
|
||||
expect(profile).not.toBe(null)
|
||||
fs.writeFileSync(path.resolve(__dirname, 'data', 'profile.json'), JSON.stringify(profile, null, 2));
|
||||
});
|
||||
|
||||
test('Test game list', async () => {
|
||||
const uid = '73276';
|
||||
const list = await xcxApi.getGameList(uid);
|
||||
expect(list).not.toBe(null)
|
||||
expect(list?.length).toBeGreaterThan(0)
|
||||
fs.writeFileSync(path.resolve(__dirname, 'data', 'gamelist.json'), JSON.stringify(list, null, 2));
|
||||
});
|
||||
17
bun-env.d.ts
vendored
Normal file
17
bun-env.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
// Generated by `bun init`
|
||||
|
||||
declare module "*.svg" {
|
||||
/**
|
||||
* A path to the SVG file
|
||||
*/
|
||||
const path: `${string}.svg`;
|
||||
export = path;
|
||||
}
|
||||
|
||||
declare module "*.module.css" {
|
||||
/**
|
||||
* A record of class names to their corresponding CSS module classes
|
||||
*/
|
||||
const classes: { readonly [key: string]: string };
|
||||
export = classes;
|
||||
}
|
||||
279
bun.lock
Normal file
279
bun.lock
Normal file
@ -0,0 +1,279 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "kaiqiu-rank-list",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"ahooks": "^3.9.6",
|
||||
"antd": "^6.2.1",
|
||||
"cheerio": "^1.2.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"lodash": "^4.17.23",
|
||||
"react": "^19",
|
||||
"react-dom": "^19",
|
||||
"react-router": "^7.13.0",
|
||||
"styled-components": "^6.3.8",
|
||||
"zustand": "^5.0.10",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/lodash": "^4.17.23",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
},
|
||||
},
|
||||
},
|
||||
"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-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/fast-color": ["@ant-design/fast-color@3.0.0", "", {}, "sha512-eqvpP7xEDm2S7dUzl5srEQCBTXZMmY3ekf97zI+M2DHOYyKdJGH0qua0JACHTqbkRnD/KHFQP9J1uMJ/XWVzzA=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@ant-design/icons-svg": ["@ant-design/icons-svg@4.4.2", "", {}, "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA=="],
|
||||
|
||||
"@ant-design/react-slick": ["@ant-design/react-slick@2.0.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "clsx": "^2.1.1", "json2mq": "^0.2.0", "throttle-debounce": "^5.0.0" }, "peerDependencies": { "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
||||
|
||||
"@emotion/hash": ["@emotion/hash@0.8.0", "", {}, "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="],
|
||||
|
||||
"@emotion/is-prop-valid": ["@emotion/is-prop-valid@1.4.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0" } }, "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw=="],
|
||||
|
||||
"@emotion/memoize": ["@emotion/memoize@0.9.0", "", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="],
|
||||
|
||||
"@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="],
|
||||
|
||||
"@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/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/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/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/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/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/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=="],
|
||||
|
||||
"@rc-component/input": ["@rc-component/input@1.1.2", "", { "dependencies": { "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg=="],
|
||||
|
||||
"@rc-component/input-number": ["@rc-component/input-number@1.6.2", "", { "dependencies": { "@rc-component/mini-decimal": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w=="],
|
||||
|
||||
"@rc-component/mentions": ["@rc-component/mentions@1.6.0", "", { "dependencies": { "@rc-component/input": "~1.1.0", "@rc-component/menu": "~1.2.0", "@rc-component/textarea": "~1.1.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ=="],
|
||||
|
||||
"@rc-component/menu": ["@rc-component/menu@1.2.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg=="],
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
"@rc-component/notification": ["@rc-component/notification@1.2.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA=="],
|
||||
|
||||
"@rc-component/overflow": ["@rc-component/overflow@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-GSlBeoE0XTBi5cf3zl8Qh7Uqhn7v8RrlJ8ajeVpEkNe94HWy5l5BQ0Mwn2TVUq9gdgbfEMUmTX7tJFAg7mz0Rw=="],
|
||||
|
||||
"@rc-component/pagination": ["@rc-component/pagination@1.2.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw=="],
|
||||
|
||||
"@rc-component/picker": ["@rc-component/picker@1.9.0", "", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/trigger": "^3.6.15", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "date-fns": ">= 2.x", "dayjs": ">= 1.x", "luxon": ">= 3.x", "moment": ">= 2.x", "react": ">=16.9.0", "react-dom": ">=16.9.0" }, "optionalPeers": ["date-fns", "dayjs", "luxon", "moment"] }, "sha512-OLisdk8AWVCG9goBU1dWzuH5QlBQk8jktmQ6p0/IyBFwdKGwyIZOSjnBYo8hooHiTdl0lU+wGf/OfMtVBw02KQ=="],
|
||||
|
||||
"@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="],
|
||||
|
||||
"@rc-component/progress": ["@rc-component/progress@1.0.2", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ=="],
|
||||
|
||||
"@rc-component/qrcode": ["@rc-component/qrcode@1.1.1", "", { "dependencies": { "@babel/runtime": "^7.24.7" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA=="],
|
||||
|
||||
"@rc-component/rate": ["@rc-component/rate@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-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw=="],
|
||||
|
||||
"@rc-component/resize-observer": ["@rc-component/resize-observer@1.1.1", "", { "dependencies": { "@rc-component/util": "^1.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-NfXXMmiR+SmUuKE1NwJESzEUYUFWIDUn2uXpxCTOLwiRUUakd62DRNFjRJArgzyFW8S5rsL4aX5XlyIXyC/vRA=="],
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
"@rc-component/steps": ["@rc-component/steps@1.2.2", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw=="],
|
||||
|
||||
"@rc-component/switch": ["@rc-component/switch@1.0.3", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw=="],
|
||||
|
||||
"@rc-component/table": ["@rc-component/table@1.9.1", "", { "dependencies": { "@rc-component/context": "^2.0.1", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.1.0", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-FVI5ZS/GdB3BcgexfCYKi3iHhZS3Fr59EtsxORszYGrfpH1eWr33eDNSYkVfLI6tfJ7vftJDd9D5apfFWqkdJg=="],
|
||||
|
||||
"@rc-component/tabs": ["@rc-component/tabs@1.7.0", "", { "dependencies": { "@rc-component/dropdown": "~1.0.0", "@rc-component/menu": "~1.2.0", "@rc-component/motion": "^1.1.3", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w=="],
|
||||
|
||||
"@rc-component/textarea": ["@rc-component/textarea@1.1.2", "", { "dependencies": { "@rc-component/input": "~1.1.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A=="],
|
||||
|
||||
"@rc-component/tooltip": ["@rc-component/tooltip@1.4.0", "", { "dependencies": { "@rc-component/trigger": "^3.7.1", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg=="],
|
||||
|
||||
"@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-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/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=="],
|
||||
|
||||
"@rc-component/upload": ["@rc-component/upload@1.1.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw=="],
|
||||
|
||||
"@rc-component/util": ["@rc-component/util@1.7.0", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-tIvIGj4Vl6fsZFvWSkYw9sAfiCKUXMyhVz6kpKyZbwyZyRPqv2vxYZROdaO1VB4gqTNvUZFXh6i3APUiterw5g=="],
|
||||
|
||||
"@rc-component/virtual-list": ["@rc-component/virtual-list@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
||||
|
||||
"@types/js-cookie": ["@types/js-cookie@3.0.6", "", {}, "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ=="],
|
||||
|
||||
"@types/lodash": ["@types/lodash@4.17.23", "", {}, "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.9", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
"@types/stylis": ["@types/stylis@4.2.7", "", {}, "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
||||
|
||||
"camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="],
|
||||
|
||||
"cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="],
|
||||
|
||||
"cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"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-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="],
|
||||
|
||||
"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-to-react-native": ["css-to-react-native@3.2.0", "", { "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", "postcss-value-parser": "^4.0.2" } }, "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ=="],
|
||||
|
||||
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
|
||||
|
||||
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||
|
||||
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||
|
||||
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
|
||||
|
||||
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||
|
||||
"encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="],
|
||||
|
||||
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"intersection-observer": ["intersection-observer@0.12.2", "", {}, "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg=="],
|
||||
|
||||
"is-mobile": ["is-mobile@5.0.0", "", {}, "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ=="],
|
||||
|
||||
"js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="],
|
||||
|
||||
"json2mq": ["json2mq@0.2.0", "", { "dependencies": { "string-convert": "^0.2.0" } }, "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA=="],
|
||||
|
||||
"lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||
|
||||
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
|
||||
"parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="],
|
||||
|
||||
"parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "^7.0.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="],
|
||||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
|
||||
|
||||
"react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"screenfull": ["screenfull@5.2.0", "", {}, "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"string-convert": ["string-convert@0.2.1", "", {}, "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="],
|
||||
|
||||
"styled-components": ["styled-components@6.3.8", "", { "dependencies": { "@emotion/is-prop-valid": "1.4.0", "@emotion/unitless": "0.10.0", "@types/stylis": "4.2.7", "css-to-react-native": "3.2.0", "csstype": "3.2.3", "postcss": "8.4.49", "shallowequal": "1.1.0", "stylis": "4.3.6", "tslib": "2.8.1" }, "peerDependencies": { "react": ">= 16.8.0", "react-dom": ">= 16.8.0" }, "optionalPeers": ["react-dom"] }, "sha512-Kq/W41AKQloOqKM39zfaMdJ4BcYDw/N5CIq4/GTI0YjU6pKcZ1KKhk6b4du0a+6RA9pIfOP/eu94Ge7cu+PDCA=="],
|
||||
|
||||
"stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="],
|
||||
|
||||
"throttle-debounce": ["throttle-debounce@5.0.2", "", {}, "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"undici": ["undici@7.19.0", "", {}, "sha512-Heho1hJD81YChi+uS2RkSjcVO+EQLmLSyUlHyp7Y/wFbxQaGb4WXVKD073JytrjXJVkSZVzoE2MCSOKugFGtOQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"@ant-design/cssinjs/@emotion/unitless": ["@emotion/unitless@0.7.5", "", {}, "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="],
|
||||
|
||||
"htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
}
|
||||
}
|
||||
2
bunfig.toml
Normal file
2
bunfig.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[serve.static]
|
||||
env = "BUN_PUBLIC_*"
|
||||
7
docker-compose.yml
Normal file
7
docker-compose.yml
Normal file
@ -0,0 +1,7 @@
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- 3005:3000
|
||||
6
dockerfile
Normal file
6
dockerfile
Normal file
@ -0,0 +1,6 @@
|
||||
FROM oven/bun:latest
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN bun install
|
||||
ENTRYPOINT [ "bun", "start"]
|
||||
EXPOSE 3000
|
||||
30
package.json
Normal file
30
package.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "my-kaiqiuwang",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun --hot src/index.tsx",
|
||||
"build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'",
|
||||
"start": "NODE_ENV=production bun src/index.tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"ahooks": "^3.9.6",
|
||||
"antd": "^6.2.1",
|
||||
"cheerio": "^1.2.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"lodash": "^4.17.23",
|
||||
"react": "^19",
|
||||
"react-dom": "^19",
|
||||
"react-router": "^7.13.0",
|
||||
"styled-components": "^6.3.8",
|
||||
"zustand": "^5.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/lodash": "^4.17.23",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19"
|
||||
}
|
||||
}
|
||||
21
src/App.tsx
Normal file
21
src/App.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { useCallback } from "react";
|
||||
import { ClubSelector } from "./components/GameSelector";
|
||||
import type { IEventInfo } from "./types";
|
||||
import { useNavigate } from "react-router";
|
||||
import "./index.css";
|
||||
|
||||
export function App() {
|
||||
const navigate = useNavigate();
|
||||
const handleGameClick = useCallback(async (game: IEventInfo) => {
|
||||
navigate(`/event/${game.matchId}`);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<h1>开球网比赛分组预测</h1>
|
||||
<ClubSelector onGameClick={handleGameClick} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
7
src/components/ChangeBackground.tsx
Normal file
7
src/components/ChangeBackground.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { createGlobalStyle } from "styled-components";
|
||||
|
||||
export const ChangeBackground = createGlobalStyle<{ url?: string }>`
|
||||
body:before {
|
||||
${({ url }) => url ? `background: url(${url});` : ''}
|
||||
}
|
||||
`
|
||||
36
src/components/GamePanel.tsx
Normal file
36
src/components/GamePanel.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import type React from "react";
|
||||
import type { BasePlayer, Player } from "../types";
|
||||
import { Tabs } from "antd";
|
||||
import { PlayerList } from "./PlayerList";
|
||||
import { GroupingPrediction } from "./GroupingPrediction";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
players?: Player[];
|
||||
members?: BasePlayer[];
|
||||
}
|
||||
|
||||
export const GamePanel: React.FC<Props> = props => {
|
||||
const sneckMode = useMemo(() => {
|
||||
return !!props.title?.includes('争霸赛');
|
||||
}, [props.title]);
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
key: 'groups',
|
||||
label: '分组预测',
|
||||
children: <GroupingPrediction sneckMode={sneckMode} players={props.members} />
|
||||
},
|
||||
{
|
||||
key: 'players',
|
||||
label: '成员列表',
|
||||
children: <PlayerList players={props.players} />
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
93
src/components/GameSelector/GameSelector.tsx
Normal file
93
src/components/GameSelector/GameSelector.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import { Card, Divider, Flex, Select, Space, Statistic, Switch, Typography } from 'antd';
|
||||
import type React from 'react';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { clubs } from './clubList';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import type { IEventInfo } from '../../types';
|
||||
|
||||
interface Props {
|
||||
onGameClick?: (info: IEventInfo) => void;
|
||||
}
|
||||
|
||||
export const GameSelector: React.FC<Props> = props => {
|
||||
const requestEvents = useRequest<IEventInfo[], [string]>(
|
||||
async (clubId: string) => (await fetch(`/api/events/${clubId}`)).json()
|
||||
, { manual: true })
|
||||
const [gameList, setGameList] = useState<(IEventInfo & { finished: boolean })[]>([]);
|
||||
const [isEmpty, setIsEmpty] = useState(false);
|
||||
const [clubId, setClubId] = useState(clubs[0].clubId);
|
||||
const [showFinished, setShowFinished] = useState(false);
|
||||
const handleClubChange = useCallback(async (clubId: string) => {
|
||||
const list = await requestEvents.runAsync(clubId);
|
||||
const activeList = list.map(e => ({
|
||||
...e,
|
||||
finished: e.info.join('').includes('已结束'),
|
||||
}));
|
||||
setGameList(activeList);
|
||||
setIsEmpty(activeList.filter(e => !e.finished).length === 0);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const clubId = clubs[0].clubId;
|
||||
handleClubChange(clubId);
|
||||
}, []);
|
||||
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={'请选择俱乐部'}
|
||||
size='large'
|
||||
value={clubId}
|
||||
options={clubs.map(e => ({ label: e.name, value: e.clubId }))}
|
||||
onChange={handleClubChange}
|
||||
/>
|
||||
</Flex>
|
||||
<Divider>{isEmpty && (<Typography.Text type='secondary'>没有未开始的比赛</Typography.Text>)}</Divider>
|
||||
<Flex wrap gap={12} justify='center'>
|
||||
{gameList
|
||||
.filter(e => showFinished || !e.finished)
|
||||
.map(e => <EventCard key={e.matchId} eventInfo={e} onGameClick={props.onGameClick} />)
|
||||
}
|
||||
</Flex>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
interface EventCardProps {
|
||||
eventInfo: IEventInfo & { finished: boolean };
|
||||
onGameClick?: (info: IEventInfo) => void;
|
||||
}
|
||||
|
||||
function EventCard(props: EventCardProps) {
|
||||
const { eventInfo: e } = props;
|
||||
const { y, M, D } = /^(?<y>\d{4})年(?<M>\d+)月(?<D>\d+)日/.exec(e.title)?.groups ?? {} as { y: string; M: string; D: string};
|
||||
const hm = /(?<hm>\d+:\d+)\b/.exec(e.info.join('\n'))?.groups?.hm ?? '10:00';
|
||||
const day = dayjs(`${y}-${M}-${D} ${hm}`);
|
||||
return (
|
||||
<Card
|
||||
key={e.matchId}
|
||||
title={e.title}
|
||||
hoverable
|
||||
style={{ width: '100%' }}
|
||||
onClick={() => props.onGameClick?.(e)}
|
||||
>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
6
src/components/GameSelector/clubList.ts
Normal file
6
src/components/GameSelector/clubList.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export const clubs = [
|
||||
{
|
||||
name: '东华',
|
||||
clubId: '47',
|
||||
},
|
||||
] as const;
|
||||
1
src/components/GameSelector/index.ts
Normal file
1
src/components/GameSelector/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { GameSelector as ClubSelector } from './GameSelector';
|
||||
37
src/components/GroupMember.tsx
Normal file
37
src/components/GroupMember.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { Card, Table } from "antd";
|
||||
import type { BasePlayer } from "../types";
|
||||
import { useMemo } from "react";
|
||||
import User from "./User";
|
||||
|
||||
interface Props {
|
||||
index: number;
|
||||
players?: (BasePlayer & { id: string })[];
|
||||
}
|
||||
|
||||
export const GroupMember: React.FC<Props> = props => {
|
||||
const char = useMemo(() => {
|
||||
const baseCode = 'A'.charCodeAt(0);
|
||||
let idx = props.index;
|
||||
const times = Math.floor(idx / 26);
|
||||
const code = 'A'.repeat(times);
|
||||
const lastChar = baseCode + (idx % 26);
|
||||
return `${code}${String.fromCharCode(lastChar)}`;
|
||||
}, []);
|
||||
return (
|
||||
<Card title={`${char} 组`} style={{ minWidth: 300 }}>
|
||||
<Table
|
||||
size="small"
|
||||
rowKey={e => `${e.id}-${e.name}-${e.score}`}
|
||||
showHeader={false}
|
||||
pagination={false}
|
||||
dataSource={props.players}
|
||||
columns={[
|
||||
{ dataIndex: '_', render: (_, __, i) => `(${i + 1})` },
|
||||
{ dataIndex: 'number' },
|
||||
{ dataIndex: 'name', render: (name, { uid }) => <User name={name} uid={uid} /> },
|
||||
{ dataIndex: 'score' },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
69
src/components/GroupingPrediction.tsx
Normal file
69
src/components/GroupingPrediction.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { Flex, Form, InputNumber, Space, Switch, Typography } from "antd";
|
||||
import { chunk } from 'lodash';
|
||||
import type { BasePlayer } from "../types";
|
||||
import { GroupMember } from "./GroupMember";
|
||||
import { sneckGroup } from "../utils";
|
||||
|
||||
interface Props {
|
||||
players?: BasePlayer[];
|
||||
sneckMode: boolean;
|
||||
}
|
||||
|
||||
type CustomPlayer = (BasePlayer & { index: number; id: string; });
|
||||
export const GroupingPrediction: React.FC<Props> = props => {
|
||||
const [maxPlayerSize, setMaxPlayerSize] = useState(48);
|
||||
const players: CustomPlayer[] = useMemo(() => {
|
||||
return props.players
|
||||
?.slice(0, maxPlayerSize)
|
||||
?.sort((a, b) => Number(b.score) - Number(a.score))
|
||||
?.map((e, i) => ({ ...e, index: i + 1, id: `${i}-${e.name}-${e.score}` })) ?? [];
|
||||
}, [props.players, maxPlayerSize]);
|
||||
const [groupLen, setGroupLen] = useState(6);
|
||||
const [sneckMode, setSneckMode] = useState(props.sneckMode);
|
||||
const chunkSize = useMemo(() => Math.floor((players.length ?? 0) / groupLen) || 1, [players, groupLen]);
|
||||
const grouped = useMemo(() => {
|
||||
return chunk(players, chunkSize);
|
||||
}, [chunkSize]);
|
||||
const sneckedGroups = useMemo(() => {
|
||||
const sneckIndexGroups = sneckGroup(players.length, groupLen);
|
||||
return sneckIndexGroups.map(g => {
|
||||
const subGroup = g.map(i => ({
|
||||
...players[i],
|
||||
})).filter(Boolean) as CustomPlayer[];
|
||||
return subGroup;
|
||||
});
|
||||
}, [grouped, groupLen, maxPlayerSize]);
|
||||
return (
|
||||
<>
|
||||
<Flex gap={10} wrap>
|
||||
<Form.Item label={'取人数'}>
|
||||
<InputNumber
|
||||
value={maxPlayerSize}
|
||||
onChange={e => setMaxPlayerSize(e || props.players?.length || 0)}
|
||||
max={props.players?.length}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="分组数">
|
||||
<InputNumber
|
||||
value={groupLen}
|
||||
onChange={e => setGroupLen(e ?? 1)}
|
||||
min={1}
|
||||
max={26}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="蛇形分组">
|
||||
<Switch checked={sneckMode} onChange={setSneckMode} />
|
||||
</Form.Item>
|
||||
</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} />)}
|
||||
</React.Fragment>
|
||||
<React.Fragment key={'sneck'}>
|
||||
{ sneckMode && sneckedGroups.map((p, i) => <GroupMember key={i} players={p} index={i} />)}
|
||||
</React.Fragment>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
39
src/components/PlayerList.tsx
Normal file
39
src/components/PlayerList.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import type React from "react";
|
||||
import type { Player } from "../types";
|
||||
import { Avatar, Table } from "antd";
|
||||
import User from "./User";
|
||||
|
||||
interface Props {
|
||||
loading?: boolean;
|
||||
players?: Player[];
|
||||
}
|
||||
|
||||
export const PlayerList: React.FC<Props> = props => {
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
bordered={false}
|
||||
size="small"
|
||||
dataSource={props.players}
|
||||
rowKey={(e) => `${e.name}-${e.uid}`}
|
||||
loading={props.loading}
|
||||
pagination={{
|
||||
placement: ['topCenter', 'bottomCenter'],
|
||||
}}
|
||||
>
|
||||
<Table.Column width={32} dataIndex={'number'} align="center" />
|
||||
<Table.Column width={32} dataIndex={'avatar'} align="center" render={src => <Avatar src={src} />} />
|
||||
<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,
|
||||
}} />
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
43
src/components/Tags.tsx
Normal file
43
src/components/Tags.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { useRequest } from "ahooks";
|
||||
import { Divider, Flex, Skeleton, Tag, Typography } from "antd";
|
||||
import { EType, type XCXTag } from "../types";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface Props {
|
||||
uid?: string;
|
||||
}
|
||||
|
||||
const color: {
|
||||
[EType.GREEN]: string;
|
||||
[EType.RED]: string;
|
||||
[EType.YELLOW]: string;
|
||||
} = {
|
||||
[EType.GREEN]: 'success',
|
||||
[EType.RED]: 'error',
|
||||
[EType.YELLOW]: 'warning',
|
||||
};
|
||||
|
||||
export default function UserTags(props: Props) {
|
||||
const fetchTags = useRequest<XCXTag[], []>(async () => (await fetch(`/api/user/${props.uid}/tags`)).json(), {
|
||||
refreshDeps: [props.uid],
|
||||
});
|
||||
if (!props.uid) return null;
|
||||
return (
|
||||
<>
|
||||
<Divider>评价标签</Divider>
|
||||
<Flex wrap gap={12} justify="center">
|
||||
{ fetchTags.loading ? (
|
||||
<>
|
||||
<Skeleton.Button active shape="square" />
|
||||
<Skeleton.Button active shape="square" />
|
||||
<Skeleton.Button active shape="square" />
|
||||
</>
|
||||
) : (
|
||||
fetchTags.data?.length
|
||||
? fetchTags.data?.map(e => (
|
||||
<Tag key={e.eid} color={color[e.etype]}>{e.ename}({e.count})</Tag>
|
||||
)): (<Typography.Text type="secondary">暂时没人评价</Typography.Text>))}
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
12
src/components/User.tsx
Normal file
12
src/components/User.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { Link } from "react-router";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
uid?: string;
|
||||
}
|
||||
export default function User(props: Props) {
|
||||
if (!props.uid) return <span>{props.name}</span>
|
||||
return (
|
||||
<Link to={`/profile/${props.uid}`}>{props.name}</Link>
|
||||
);
|
||||
}
|
||||
59
src/frontend.tsx
Normal file
59
src/frontend.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* This file is the entry point for the React app, it sets up the root
|
||||
* element and renders the App component to the DOM.
|
||||
*
|
||||
* It is included in `src/index.html`.
|
||||
*/
|
||||
|
||||
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();
|
||||
const members = await (await fetch(`/api/match/${params.matchId}/${info.itemId}`)).json();
|
||||
return { info, members };
|
||||
},
|
||||
Component: EventPage,
|
||||
},
|
||||
{
|
||||
path: '/profile/:uid',
|
||||
loader: async ({ params }) => {
|
||||
return fetch(`/api/user/${params.uid}`);
|
||||
},
|
||||
Component: ProfilePage,
|
||||
},
|
||||
]);
|
||||
|
||||
const app = (
|
||||
<StrictMode>
|
||||
<ConfigProvider theme={{
|
||||
algorithm: theme.darkAlgorithm,
|
||||
}}>
|
||||
<RouterProvider router={route} />
|
||||
</ConfigProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
if (import.meta.hot) {
|
||||
// With hot module reloading, `import.meta.hot.data` is persisted.
|
||||
const root = (import.meta.hot.data.root ??= createRoot(elem));
|
||||
root.render(app);
|
||||
} else {
|
||||
// The hot module reloading API is not available in production.
|
||||
createRoot(elem).render(app);
|
||||
}
|
||||
1
src/global.d.ts
vendored
Normal file
1
src/global.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module "*.css";
|
||||
45
src/index.css
Normal file
45
src/index.css
Normal file
@ -0,0 +1,45 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
}
|
||||
#root {
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
}
|
||||
body {
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
opacity: 0.05;
|
||||
background: url("./logo.svg");
|
||||
background-size: 256px;
|
||||
transform: rotate(-12deg) scale(1.35);
|
||||
animation: slide 30s linear infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
@keyframes slide {
|
||||
from {
|
||||
background-position: 0 0;
|
||||
}
|
||||
to {
|
||||
background-position: 256px 224px;
|
||||
}
|
||||
}
|
||||
.app {
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
15
src/index.html
Normal file
15
src/index.html
Normal file
@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="./logo.jpg" />
|
||||
<title>开球网比赛分组预测</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./frontend.tsx"></script>
|
||||
<script defer src="https://umami.ksr.la/script.js" data-website-id="37f547c9-a21b-4e91-83f3-f7f1198a8793"></script>
|
||||
</body>
|
||||
</html>
|
||||
62
src/index.tsx
Normal file
62
src/index.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { serve } from "bun";
|
||||
import { getMatchInfo, listEvent } from "./utils";
|
||||
import index from "./index.html";
|
||||
import { XCXAPI } from "./services/xcxApi";
|
||||
|
||||
if (!process.env.KAIQIUCC_TOKEN) {
|
||||
console.error('env KAIQIUCC_TOKEN not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const xcxApi = new XCXAPI(process.env.KAIQIUCC_TOKEN ?? '');
|
||||
|
||||
const server = serve({
|
||||
port: process.env.PORT || 3000,
|
||||
routes: {
|
||||
// Serve index.html for all unmatched routes.
|
||||
"/*": index,
|
||||
"/api/events/:clubid": {
|
||||
async GET(req) {
|
||||
const data = await listEvent(req.params.clubid);
|
||||
return Response.json(data);
|
||||
}
|
||||
},
|
||||
"/api/match/:matchId": {
|
||||
async GET(req) {
|
||||
const data = await getMatchInfo(req.params.matchId);
|
||||
return Response.json(data);
|
||||
}
|
||||
},
|
||||
"/api/match/:matchId/:itemId": {
|
||||
async GET(req) {
|
||||
const { matchId, itemId } = req.params;
|
||||
const data = await xcxApi.getMemberDetail(matchId, itemId);
|
||||
return Response.json(data);
|
||||
}
|
||||
},
|
||||
"/api/user/:uid": {
|
||||
async GET(req) {
|
||||
const uid = req.params.uid;
|
||||
const profile = await xcxApi.getAdvProfile(uid);
|
||||
return Response.json(profile);
|
||||
},
|
||||
},
|
||||
"/api/user/:uid/tags": {
|
||||
async GET(req) {
|
||||
const uid = req.params.uid;
|
||||
const profile = await xcxApi.getPlayerTags(uid);
|
||||
return Response.json(profile);
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
development: process.env.NODE_ENV !== "production" && {
|
||||
// Enable browser hot reloading in development
|
||||
hmr: true,
|
||||
|
||||
// Echo console logs from the browser to the server
|
||||
console: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`🚀 Server running at ${server.url}`);
|
||||
BIN
src/logo.jpg
Normal file
BIN
src/logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 202 KiB |
1
src/logo.svg
Normal file
1
src/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg id="Bun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 70"><title>Bun Logo</title><path id="Shadow" d="M71.09,20.74c-.16-.17-.33-.34-.5-.5s-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5A26.46,26.46,0,0,1,75.5,35.7c0,16.57-16.82,30.05-37.5,30.05-11.58,0-21.94-4.23-28.83-10.86l.5.5.5.5.5.5.5.5.5.5.5.5.5.5C19.55,65.3,30.14,69.75,42,69.75c20.68,0,37.5-13.48,37.5-30C79.5,32.69,76.46,26,71.09,20.74Z"/><g id="Body"><path id="Background" d="M73,35.7c0,15.21-15.67,27.54-35,27.54S3,50.91,3,35.7C3,26.27,9,17.94,18.22,13S33.18,3,38,3s8.94,4.13,19.78,10C67,17.94,73,26.27,73,35.7Z" style="fill:#fbf0df"/><path id="Bottom_Shadow" data-name="Bottom Shadow" d="M73,35.7a21.67,21.67,0,0,0-.8-5.78c-2.73,33.3-43.35,34.9-59.32,24.94A40,40,0,0,0,38,63.24C57.3,63.24,73,50.89,73,35.7Z" style="fill:#f6dece"/><path id="Light_Shine" data-name="Light Shine" d="M24.53,11.17C29,8.49,34.94,3.46,40.78,3.45A9.29,9.29,0,0,0,38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7c0,.4,0,.8,0,1.19C9.06,15.48,20.07,13.85,24.53,11.17Z" style="fill:#fffefc"/><path id="Top" d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z" style="fill:#ccbea7;fill-rule:evenodd"/><path id="Outline" d="M38,65.75C17.32,65.75.5,52.27.5,35.7c0-10,6.18-19.33,16.53-24.92,3-1.6,5.57-3.21,7.86-4.62,1.26-.78,2.45-1.51,3.6-2.19C32,1.89,35,.5,38,.5s5.62,1.2,8.9,3.14c1,.57,2,1.19,3.07,1.87,2.49,1.54,5.3,3.28,9,5.27C69.32,16.37,75.5,25.69,75.5,35.7,75.5,52.27,58.68,65.75,38,65.75ZM38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7,3,50.89,18.7,63.25,38,63.25S73,50.89,73,35.7C73,26.62,67.31,18.13,57.78,13,54,11,51.05,9.12,48.66,7.64c-1.09-.67-2.09-1.29-3-1.84C42.63,4,40.42,3,38,3Z"/></g><g id="Mouth"><g id="Background-2" data-name="Background"><path d="M45.05,43a8.93,8.93,0,0,1-2.92,4.71,6.81,6.81,0,0,1-4,1.88A6.84,6.84,0,0,1,34,47.71,8.93,8.93,0,0,1,31.12,43a.72.72,0,0,1,.8-.81H44.26A.72.72,0,0,1,45.05,43Z" style="fill:#b71422"/></g><g id="Tongue"><path id="Background-3" data-name="Background" d="M34,47.79a6.91,6.91,0,0,0,4.12,1.9,6.91,6.91,0,0,0,4.11-1.9,10.63,10.63,0,0,0,1-1.07,6.83,6.83,0,0,0-4.9-2.31,6.15,6.15,0,0,0-5,2.78C33.56,47.4,33.76,47.6,34,47.79Z" style="fill:#ff6164"/><path id="Outline-2" data-name="Outline" d="M34.16,47a5.36,5.36,0,0,1,4.19-2.08,6,6,0,0,1,4,1.69c.23-.25.45-.51.66-.77a7,7,0,0,0-4.71-1.93,6.36,6.36,0,0,0-4.89,2.36A9.53,9.53,0,0,0,34.16,47Z"/></g><path id="Outline-3" data-name="Outline" d="M38.09,50.19a7.42,7.42,0,0,1-4.45-2,9.52,9.52,0,0,1-3.11-5.05,1.2,1.2,0,0,1,.26-1,1.41,1.41,0,0,1,1.13-.51H44.26a1.44,1.44,0,0,1,1.13.51,1.19,1.19,0,0,1,.25,1h0a9.52,9.52,0,0,1-3.11,5.05A7.42,7.42,0,0,1,38.09,50.19Zm-6.17-7.4c-.16,0-.2.07-.21.09a8.29,8.29,0,0,0,2.73,4.37A6.23,6.23,0,0,0,38.09,49a6.28,6.28,0,0,0,3.65-1.73,8.3,8.3,0,0,0,2.72-4.37.21.21,0,0,0-.2-.09Z"/></g><g id="Face"><ellipse id="Right_Blush" data-name="Right Blush" cx="53.22" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><ellipse id="Left_Bluch" data-name="Left Bluch" cx="22.95" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><path id="Eyes" d="M25.7,38.8a5.51,5.51,0,1,0-5.5-5.51A5.51,5.51,0,0,0,25.7,38.8Zm24.77,0A5.51,5.51,0,1,0,45,33.29,5.5,5.5,0,0,0,50.47,38.8Z" style="fill-rule:evenodd"/><path id="Iris" d="M24,33.64a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,24,33.64Zm24.77,0a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,48.75,33.64Z" style="fill:#fff;fill-rule:evenodd"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
32
src/page/EventPage.tsx
Normal file
32
src/page/EventPage.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { useLoaderData, useNavigate } from "react-router";
|
||||
import { GamePanel } from "../components/GamePanel";
|
||||
import type { BasePlayer, MatchInfo, XCXMember } from "../types";
|
||||
import { Typography } from "antd";
|
||||
import { HomeOutlined } from "@ant-design/icons";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export default function EventPage() {
|
||||
const {
|
||||
info: game,
|
||||
members
|
||||
} = useLoaderData<{ info: MatchInfo, members: XCXMember[] }>();
|
||||
const navigate = useNavigate();
|
||||
const map = useMemo(() => Object.fromEntries(members.map(e => [e.uid, e])), [members]);
|
||||
const players = useMemo(() => {
|
||||
return game.players
|
||||
.map(e => ({ ...e, name: map[e.uid]?.realname ?? e.name, number: map[e.uid]?.number ?? NaN }))
|
||||
.sort(((a, b) => +a.number - +b.number));
|
||||
}, [game, map]);
|
||||
const basePlayers = useMemo<BasePlayer[]>(() => {
|
||||
return members.map(e => ({ ...e, name: e.realname } as BasePlayer))
|
||||
}, [members]);
|
||||
return (
|
||||
<div style={{ width: '100%', padding: 10, boxSizing: 'border-box' }}>
|
||||
<Typography.Title level={3}>
|
||||
<HomeOutlined style={{ marginRight: 4 }} onClick={() => navigate('/')}/>
|
||||
{game.title}
|
||||
</Typography.Title>
|
||||
<GamePanel members={basePlayers} title={game.title} players={players} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
132
src/page/ProfilePage.tsx
Normal file
132
src/page/ProfilePage.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
import { Link, useLoaderData, useNavigate } from "react-router";
|
||||
import type { XCXProfile } from "../types/profile";
|
||||
import { Avatar, Descriptions, Divider, Flex, FloatButton, Image, Typography } from "antd";
|
||||
import { HomeOutlined } from "@ant-design/icons";
|
||||
|
||||
import User from "../components/User";
|
||||
import React from "react";
|
||||
import { ChangeBackground } from "../components/ChangeBackground";
|
||||
import UserTags from "../components/Tags";
|
||||
|
||||
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} />
|
||||
<Link to={`/event/${honor.eventid}`}>
|
||||
<Typography.Text>
|
||||
{honor.subject}
|
||||
</Typography.Text>
|
||||
</Link>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PlayerList(props: { title: string; names?: string[]; uids?: string[] }) {
|
||||
if (!props.names?.length) return null;
|
||||
return (
|
||||
<>
|
||||
<Typography.Title level={5}>{props.title}</Typography.Title>
|
||||
<Flex vertical align="center">
|
||||
{props.names.map((e, i) => (
|
||||
<User key={e} name={e} uid={props.uids?.[i] ?? ''} />
|
||||
))}
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProfilePage() {
|
||||
const profile = useLoaderData<XCXProfile | null>();
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<>
|
||||
<ChangeBackground url={profile?.realpic} />
|
||||
<FloatButton icon={<HomeOutlined />} onClick={() => navigate('/')} />
|
||||
<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 style={{ textAlign: 'center' }}>
|
||||
{
|
||||
([profile?.province, profile?.sex, profile?.bg ?? '', ...(profile?.allCities ?? [])] as React.ReactNode[])
|
||||
.filter(Boolean)
|
||||
.reduce((a, b) => (<>{a}<Divider orientation="vertical" />{b}</>))
|
||||
}
|
||||
</Typography.Text>
|
||||
<Divider>简介</Divider>
|
||||
<Typography.Paragraph>
|
||||
{profile?.description}
|
||||
</Typography.Paragraph>
|
||||
<UserTags uid={profile?.uid} />
|
||||
<Raket profile={profile} />
|
||||
<Flex wrap gap={24} justify="center">
|
||||
<Flex vertical align="center">
|
||||
<PlayerList title="交手前三名" names={profile?.TopPlayerUsernameScore} />
|
||||
</Flex>
|
||||
<Flex vertical align="center">
|
||||
<PlayerList title="战胜前三名" names={profile?.Top3OfBeatUsernameScore} />
|
||||
</Flex>
|
||||
<Flex vertical align="center">
|
||||
<PlayerList title="战胜的男子" names={profile?.Top3ManOfBeatUsernameScore} />
|
||||
</Flex>
|
||||
<Flex vertical align="center">
|
||||
<PlayerList title="战胜的女子" names={profile?.Top3WomanOfBeatUsernameScore} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex gap={24}>
|
||||
<Flex vertical align="center">
|
||||
<PlayerList title="福星" names={profile?.FuXing?.names} uids={profile?.FuXing?.uids} />
|
||||
</Flex>
|
||||
<Flex vertical align="center">
|
||||
<PlayerList title="苦主" names={profile?.KuZhu?.names} uids={profile?.KuZhu?.uids} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Honor honors={profile?.honors} />
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
54
src/services/xcxApi.ts
Normal file
54
src/services/xcxApi.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import type { GamesData, XCXMember, XCXProfile, XCXTag } from "../types";
|
||||
import { BASE_URL } from "../utils";
|
||||
|
||||
const XCX_BASE_URL = `${BASE_URL}/xcx/public/index.php`;
|
||||
|
||||
export function createXCXHeader(token: string) {
|
||||
const xcxDefaultHeaders = {
|
||||
'token': 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',
|
||||
};
|
||||
return xcxDefaultHeaders;
|
||||
}
|
||||
|
||||
export class XCXAPI {
|
||||
#defaultHeader: any;
|
||||
constructor(token: string) {
|
||||
this.#defaultHeader = createXCXHeader(token);
|
||||
console.log(`XCXAPI init: ${token}`);
|
||||
}
|
||||
|
||||
async #fetch<T = any>(pathAndQuery: string, init?: RequestInit): Promise<T | null> {
|
||||
const resp = await fetch(`${XCX_BASE_URL}${pathAndQuery}`, {
|
||||
headers: this.#defaultHeader,
|
||||
...init,
|
||||
});
|
||||
const response = await resp.json() as { code: number; data: unknown, msg: string; time: string; };
|
||||
if (response.code !== 1) return null;
|
||||
return response.data as T;
|
||||
}
|
||||
|
||||
async getAdvProfile(uid: string) {
|
||||
const url = `/api/User/adv_profile?uid=${uid}`;
|
||||
return this.#fetch<XCXProfile>(url);
|
||||
}
|
||||
|
||||
async getPlayerTags(uid: string) {
|
||||
const url = `/api/User/get_tags?uid=${uid}&limitByCount=50&getNegative=true`;
|
||||
return ((await this.#fetch<XCXTag[]>(url)) ?? []).filter(e => Number(e.count) > 0);
|
||||
}
|
||||
|
||||
async getMemberDetail(matchId: string, itemId: string) {
|
||||
const url = `/api/enter/get_member_detail?id=${itemId}&match_id=${matchId}`;
|
||||
return (await this.#fetch<{ list: XCXMember[] }>(url))?.list;
|
||||
}
|
||||
|
||||
async getGameList(uid: string, page: number = 1) {
|
||||
const url = `/api/User/getGames?uid=${uid}&page=${page}&size=50`;
|
||||
return (await this.#fetch<{ data: GamesData[] }>(url))?.data;
|
||||
}
|
||||
}
|
||||
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;
|
||||
37
src/types/index.ts
Normal file
37
src/types/index.ts
Normal file
@ -0,0 +1,37 @@
|
||||
export * from './profile';
|
||||
export * from './tag';
|
||||
export interface Config {
|
||||
KAIQIUCC_TOKEN: string;
|
||||
}
|
||||
export interface IEventInfo {
|
||||
title: string;
|
||||
info: string[];
|
||||
url: string;
|
||||
matchId: string;
|
||||
}
|
||||
|
||||
export interface MatchInfo {
|
||||
itemId: string;
|
||||
title: string;
|
||||
players: Player[];
|
||||
}
|
||||
|
||||
export interface BasePlayer {
|
||||
uid: string;
|
||||
name: string;
|
||||
score: string;
|
||||
}
|
||||
|
||||
export interface Player extends BasePlayer {
|
||||
avatar: string;
|
||||
info: string;
|
||||
}
|
||||
|
||||
export interface XCXMember extends BasePlayer {
|
||||
uid: string;
|
||||
score: string;
|
||||
realname: string;
|
||||
name: string;
|
||||
number: number;
|
||||
age: 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;
|
||||
}
|
||||
12
src/types/tag.ts
Normal file
12
src/types/tag.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export enum EType {
|
||||
GREEN = "1",
|
||||
RED = "-1",
|
||||
YELLOW = "0",
|
||||
}
|
||||
|
||||
export interface XCXTag {
|
||||
eid: string;
|
||||
etype: EType;
|
||||
ename: string;
|
||||
count: string;
|
||||
}
|
||||
125
src/utils.ts
Normal file
125
src/utils.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import type { IEventInfo, Player, XCXMember } from "./types";
|
||||
import * as cheerio from "cheerio";
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { chunk } from 'lodash';
|
||||
import type { XCXProfile } from "./types/profile";
|
||||
import type { XCXTag } from "./types/tag";
|
||||
|
||||
export const BASE_URL = `https://kaiqiuwang.cc`;
|
||||
|
||||
/**
|
||||
*
|
||||
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
|
||||
*/
|
||||
/**
|
||||
* @param tagid 俱乐部 ID
|
||||
*/
|
||||
export async function listEvent(tagid: string): Promise<IEventInfo[]> {
|
||||
return parseEventList(await fetchEventListHTML(tagid));
|
||||
return parseEventList(
|
||||
fs.readFileSync(
|
||||
path.resolve(__dirname, '..', '__test__', 'data', 'view-event.html')
|
||||
).toString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param tagid 俱乐部 ID
|
||||
* @return HTML
|
||||
*/
|
||||
export async function fetchEventListHTML(tagid: string) {
|
||||
const url = `${BASE_URL}/home/space.php?do=mtag&tagid=${tagid}&view=event`;
|
||||
const resp = await fetch(url);
|
||||
return resp.text() ?? '';
|
||||
}
|
||||
|
||||
export function parseEventList(html: string) {
|
||||
const $ = cheerio.load(html);
|
||||
const blockList = $('div.event_list > ol > li');
|
||||
const list: IEventInfo[] = [];
|
||||
for (const block of blockList) {
|
||||
const titleEl = $(block).find('.event_title');
|
||||
const title = titleEl.text();
|
||||
const url = $(titleEl).find('a').attr('href') ?? '';
|
||||
const startDate = $(block).find('ul li:nth-of-type(1)').text().replace('比赛开始: \\t', '').trim();
|
||||
const place = $(block).find('ul li:nth-of-type(2)').text().replace('比赛地点: \\t', '').trim();
|
||||
const event: IEventInfo = {
|
||||
title,
|
||||
info: [startDate, place],
|
||||
url: `${BASE_URL}/home/${url}`,
|
||||
matchId: /\S+-(\d+).html$/.exec(url)?.[1] ?? '',
|
||||
}
|
||||
list.push(event);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param matchId 比赛 ID
|
||||
* @returns HTML
|
||||
*/
|
||||
export async function fetchEventContentHTML(matchId: string) {
|
||||
const url = `${BASE_URL}/home/space.php?do=event&id=${matchId}&view=member&status=2`;
|
||||
const resp = await fetch(url);
|
||||
return resp.text() ?? ''
|
||||
}
|
||||
|
||||
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[] = [];
|
||||
const playersEl = $('.thumb');
|
||||
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 = /^.*?\b(\d+)\b/.exec(info)?.[1] ?? '';
|
||||
players.push({ name, avatar: img, score, info, uid });
|
||||
}
|
||||
return {
|
||||
itemId,
|
||||
title,
|
||||
players,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMatchInfo(matchId: string) {
|
||||
return parseEventInfo(await fetchEventContentHTML(matchId));
|
||||
return parseEventInfo(
|
||||
fs
|
||||
.readFileSync(
|
||||
path.resolve(__dirname, '..', '__test__', 'data', 'view-event-content.html')
|
||||
)
|
||||
.toString()
|
||||
);
|
||||
}
|
||||
|
||||
export function sneckGroup(size: number, groupLen: number) {
|
||||
const indexArray = new Array<number>(size).fill(0).map((_, i) => i);
|
||||
const chunckSize = Math.round((size / groupLen));
|
||||
const chunckedGroup = chunk(indexArray, groupLen);
|
||||
const reversedGroup = chunckedGroup.map((e, i) => {
|
||||
if (i % 2 === 0) return e;
|
||||
return e.toReversed();
|
||||
});
|
||||
const newGroups: number[][] = [];
|
||||
for (let groupIndex = 0; groupIndex < groupLen; groupIndex++) {
|
||||
const group: number[] = [];
|
||||
for (let colIndex = 0; colIndex < chunckSize; colIndex++) {
|
||||
const data = reversedGroup[colIndex]?.[groupIndex];
|
||||
group.push(data === undefined ? NaN : data);
|
||||
}
|
||||
newGroups.push(group);
|
||||
}
|
||||
return newGroups;
|
||||
}
|
||||
30
tsconfig.json
Normal file
30
tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"types": ["./src/global.d.ts"],
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user