diff --git a/__test__/utils.load-html.test.ts b/__test__/utils.load-html.test.ts
index 6531a4e..3f0309f 100644
--- a/__test__/utils.load-html.test.ts
+++ b/__test__/utils.load-html.test.ts
@@ -1,7 +1,7 @@
import { expect, test } from 'bun:test';
import path from 'path';
import fs from 'fs';
-import { fetchEventContentHTML, fetchEventListHTML } from '../src/utils/utils';
+import { fetchEventContentHTML, fetchEventListHTML } from '../src/utils/';
test('load html', async () => {
const saveTo = path.resolve(__dirname, 'data', 'view-event.html');
diff --git a/__test__/utils.test.ts b/__test__/utils.test.ts
index 1fe7d25..c9a40d0 100644
--- a/__test__/utils.test.ts
+++ b/__test__/utils.test.ts
@@ -1,7 +1,7 @@
import fs from 'fs';
import { expect, test } from 'bun:test';
import path from 'path';
-import { parseEventInfo, parseEventList, sneckGroup } from '../src/utils/utils';
+import { parseEventInfo, parseEventList, sneckGroup } from '../src/utils/';
const matchId = '167684';
diff --git a/src/components/GroupMember.tsx b/src/components/GroupMember.tsx
index 4860ae8..e9fc84b 100644
--- a/src/components/GroupMember.tsx
+++ b/src/components/GroupMember.tsx
@@ -1,7 +1,7 @@
import { useMemo, useState } from "react";
import { Button, Card, Divider, Drawer, Flex, Space, Table } from "antd";
import type { BasePlayer } from "../types";
-import { getRoundTable } from "../utils/utils";
+import { getRoundTable } from "../utils/common";
import User from "./User";
interface Props {
diff --git a/src/components/GroupingPrediction.tsx b/src/components/GroupingPrediction.tsx
index 0c51aac..47aef94 100644
--- a/src/components/GroupingPrediction.tsx
+++ b/src/components/GroupingPrediction.tsx
@@ -3,7 +3,7 @@ import { Flex, Form, InputNumber, Segmented, Switch } from "antd";
import { chunk } from 'lodash';
import type { BasePlayer } from "../types";
import { GroupMember } from "./GroupMember";
-import { sneckGroup } from "../utils/utils";
+import { sneckGroup } from "../utils/common";
interface Player extends BasePlayer {
nowScore?: string;
diff --git a/src/index.tsx b/src/index.tsx
index 1352a3f..5b04488 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,7 +1,6 @@
import { serve } from "bun";
-import { getMatchInfo, listEvent } from "./utils/utils";
+import { getMatchInfo, listEvent, xcxApi } from "./utils/server";
import index from "./index.html";
-import { xcxApi } from "./utils/server";
import { getUidScore } from "./services/uidScoreStore";
if (!process.env.KAIQIUCC_TOKEN) {
diff --git a/src/services/xcxApi.ts b/src/services/xcxApi.ts
index 7e35117..0fedf11 100644
--- a/src/services/xcxApi.ts
+++ b/src/services/xcxApi.ts
@@ -1,5 +1,5 @@
-import type { GamesData, XCXFindUser, XCXFindUserResp, XCXMember, XCXProfile, XCXTag } from "../types";
-import { BASE_URL } from "../utils/utils";
+import type { GamesData, XCXFindUserResp, XCXMember, XCXProfile, XCXTag } from "../types";
+import { BASE_URL } from "../utils/common";
const XCX_BASE_URL = `${BASE_URL}/xcx/public/index.php`;
diff --git a/src/utils/common.ts b/src/utils/common.ts
new file mode 100644
index 0000000..5ebae3a
--- /dev/null
+++ b/src/utils/common.ts
@@ -0,0 +1,47 @@
+import { chunk } from "lodash";
+
+export const BASE_URL = `https://kaiqiuwang.cc`;
+
+export function sneckGroup(size: number, groupLen: number) {
+ const indexArray = new Array(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;
+}
+
+export function getRoundTable(nameList: T[], round: number) {
+ const list = [...nameList];
+ const half = list.length / 2;
+ if (round > list.length - 1) {
+ const left = [...list].slice(0, half);
+ const right = [...list].slice(half);
+ return [left, right];
+ }
+ const sliceStart = (list.length) - round;
+ const slice = list.slice(sliceStart);
+ // console.debug(JSON.stringify({ list }));
+ list.splice(sliceStart);
+ const [first, ...others] = list;
+ const newList = [first, ...slice, ...others].filter(Boolean);
+ // console.debug(JSON.stringify({ sliceStart, len: list.length, first, others, slice, newList }));
+ const left = [...newList].slice(0, half);
+ const right = [...newList].slice(half).reverse();
+ return [left, right];
+}
+
+export function createGameID(matchId: string, user1: string, user2: string) {
+ return [matchId, user1, user2].join('-');
+}
\ No newline at end of file
diff --git a/src/utils/server.ts b/src/utils/server.ts
index e0cff57..d87471c 100644
--- a/src/utils/server.ts
+++ b/src/utils/server.ts
@@ -1,3 +1,98 @@
+import type { IEventInfo, Player } from "../types";
+import * as cheerio from "cheerio";
import { XCXAPI } from "../services/xcxApi";
+import { BASE_URL } from "./common";
-export const xcxApi = new XCXAPI(process.env.KAIQIUCC_TOKEN ?? '');
\ No newline at end of file
+export const xcxApi = new XCXAPI(process.env.KAIQIUCC_TOKEN ?? '');
+
+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",
+ "priority": "u=0, i",
+ "sec-ch-ua": "\"Not(A:Brand\";v=\"8\", \"Chromium\";v=\"144\", \"Brave\";v=\"144\"",
+ "sec-ch-ua-mobile": "?0",
+ "sec-ch-ua-platform": "\"Windows\"",
+ "sec-fetch-dest": "document",
+ "sec-fetch-mode": "navigate",
+ "sec-fetch-site": "none",
+ "sec-fetch-user": "?1",
+ "sec-gpc": "1",
+ "upgrade-insecure-requests": "1",
+ "cookie": "SECKEY_ABVK=oTGgqH4ypGPFVdQ3J9K7PoAOPdZ+8R7CsUzi75gelcg%3D; uchome_sendmail=1"
+}
+
+/**
+ * @param tagid 俱乐部 ID
+ */
+export async function listEvent(tagid: string): Promise {
+ return parseEventList(await fetchEventListHTML(tagid));
+}
+
+/**
+ * @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, { headers: htmlRequestHeaders });
+ 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, { headers: htmlRequestHeaders });
+ 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-(?\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));
+}
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
deleted file mode 100644
index c529a25..0000000
--- a/src/utils/utils.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import type { IEventInfo, Player } from "../types";
-import * as cheerio from "cheerio";
-import { chunk } from 'lodash';
-
-export const BASE_URL = `https://kaiqiuwang.cc`;
-
-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",
- "priority": "u=0, i",
- "sec-ch-ua": "\"Not(A:Brand\";v=\"8\", \"Chromium\";v=\"144\", \"Brave\";v=\"144\"",
- "sec-ch-ua-mobile": "?0",
- "sec-ch-ua-platform": "\"Windows\"",
- "sec-fetch-dest": "document",
- "sec-fetch-mode": "navigate",
- "sec-fetch-site": "none",
- "sec-fetch-user": "?1",
- "sec-gpc": "1",
- "upgrade-insecure-requests": "1",
- "cookie": "SECKEY_ABVK=oTGgqH4ypGPFVdQ3J9K7PoAOPdZ+8R7CsUzi75gelcg%3D; uchome_sendmail=1"
-}
-
-/**
- * @param tagid 俱乐部 ID
- */
-export async function listEvent(tagid: string): Promise {
- return parseEventList(await fetchEventListHTML(tagid));
-}
-
-/**
- * @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, { headers: htmlRequestHeaders });
- 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, { headers: htmlRequestHeaders });
- 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-(?\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));
-}
-
-export function sneckGroup(size: number, groupLen: number) {
- const indexArray = new Array(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;
-}
-
-export function getRoundTable(nameList: T[], round: number) {
- const list = [...nameList];
- const half = list.length / 2;
- if (round > list.length - 1) {
- const left = [...list].slice(0, half);
- const right = [...list].slice(half);
- return [left, right];
- }
- const sliceStart = (list.length) - round;
- const slice = list.slice(sliceStart);
- // console.debug(JSON.stringify({ list }));
- list.splice(sliceStart);
- const [first, ...others] = list;
- const newList = [first, ...slice, ...others].filter(Boolean);
- // console.debug(JSON.stringify({ sliceStart, len: list.length, first, others, slice, newList }));
- const left = [...newList].slice(0, half);
- const right = [...newList].slice(half).reverse();
- return [left, right];
-}
-
-export function createGameID(matchId: string, user1: string, user2: string) {
- return [matchId, user1, user2].join('-');
-}