Merge branch 'dev'

This commit is contained in:
LazuliKao 2024-09-24 22:23:54 +08:00
commit b1ab6cd2a5
32 changed files with 1593 additions and 237 deletions

View file

@ -225,4 +225,15 @@ export default defineConfig({
experimentalReactChildren: true,
}),
],
vite: {
server: {
proxy: {
"/active": {
target: "https://active.nbtca.space",
changeOrigin: true,
rewrite: path => path.replace(/^\/active/, ""),
},
}
}
}
})

View file

@ -54,6 +54,12 @@ export default [
},
},
{
ignores: ["dist/**/*", "public/**/*", "node_modules/**/*"],
ignores: [
"dist/**/*",
"public/**/*",
"node_modules/**/*",
"src/utils/active/**/*",
"openapi-ts.active.config.ts",
],
},
]

View file

@ -27,5 +27,14 @@ http {
location / {
try_files $uri $uri/index.html =404;
}
location /active {
proxy_pass https://active.nbtca.space;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
rewrite ^/active/(.*)$ /$1 break;
}
}
}

View file

@ -0,0 +1,21 @@
import { defineConfig } from '@hey-api/openapi-ts'
export default defineConfig({
input: 'https://active.nbtca.space/openapi.json',
output: {
path: './src/utils/active',
format: 'prettier',
lint: 'eslint',
},
client: 'legacy/fetch',
types: {
dates: true,
enums: 'typescript+namespace',
name: 'PascalCase',
tree: false,
},
services: {
response: 'body',
},
schemas: false,
name: 'ApiClient',
})

View file

@ -10,7 +10,8 @@
"lint": "eslint",
"astro": "astro",
"prepare": "husky",
"gen-type": "openapi-typescript http://localhost:4000/openapi-3.0.json -o ./src/types/saturday.d.ts"
"gen-type": "openapi-typescript http://localhost:4000/openapi-3.0.json -o ./src/types/saturday.d.ts",
"active": "openapi-ts -f openapi-ts.active.config.ts"
},
"dependencies": {
"@astrojs/react": "^3.3.1",
@ -45,6 +46,7 @@
"@astrojs/markdown-remark": "^5.2.0",
"@cspell/eslint-plugin": "^8.8.1",
"@eslint/js": "^9.11.0",
"@hey-api/openapi-ts": "^0.53.3",
"@types/eslint__js": "^8.42.3",
"@types/md5": "^2.3.5",
"@types/qrcode": "^1.5.5",
@ -65,4 +67,4 @@
"lint-staged": {
"*.{js,jsx,mjs,ts,tsx,mts,astro}": "eslint --fix"
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -27,17 +27,18 @@ export default function App() {
return (
<Navbar onMenuOpenChange={setIsMenuOpen} height="48px">
<NavbarContent className="flex justify-between items-center">
<NavbarBrand className="flex gap-4" onClick={() => window.location.href = "/"}>
<NavbarBrand className="flex gap-4">
<img
src="https://oss.nbtca.space/CA-logo.svg"
alt=""
className="w-8 aspect-square"
className="w-8 aspect-square cursor-pointer"
onClick={() => window.location.href = "/"}
/>
<span className="text-lg text-[#1d1d1f]">
<span className="hidden sm:flex select-none cursor-default text-lg text-[#1d1d1f]">
{SITE_TITLE}
</span>
</NavbarBrand>
<NavbarContent className="hidden sm:flex gap-4" justify="center">
<NavbarContent className="hidden sm:flex gap-[24px]" justify="center">
{
menuItems.map(item => (
<NavbarItem key={item.name}>

View file

@ -1,8 +0,0 @@
---
import FullCalendar from "../components/react/Calendar"
import BaseLayout from "../layouts/BaseLayout.astro"
---
<BaseLayout primaryTitle="日历">
<FullCalendar client:load />
</BaseLayout>

View file

@ -0,0 +1,8 @@
---
import FullCalendar from "./Calendar"
import BaseLayout from "../../layouts/BaseLayout.astro"
---
<BaseLayout primaryTitle="日历">
<FullCalendar client:load />
</BaseLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View file

@ -1,11 +1,17 @@
---
import BaseLayout from "../../layouts/BaseLayout.astro"
// import { Button } from "@nextui-org/react";
import ReactChild from "./react/acontent"
import ReactChild from "./react/join.tsx"
import logo from "./_assets/nbtca.gif";
---
<BaseLayout title="Welcome to Astro.">
<BaseLayout title="加入我们">
<main>
<ReactChild client:visible />
<img src={logo.src} />
<ReactChild client:load />
<div>
1.本页面仅作计算机协会新人信息登记使用,原则上登记即可加入。但入社后可能仍需要在学校的平台登记,后续请留意群内通知。
<br />
2.允许多次提交表单,通常以最后一次提交为准。
</div>
</main>
</BaseLayout>

View file

@ -0,0 +1,10 @@
---
import BaseLayout from "../../layouts/BaseLayout.astro"
// import ReactChild from "./react/list.tsx";
---
<BaseLayout title="列表">
<main>
<!-- <ReactChild client:load /> -->
</main>
</BaseLayout>

View file

@ -1,12 +0,0 @@
// app/page.tsx
import { Button } from "@nextui-org/react"
// import { NextUIProvider } from "@nextui-org/react";
export default function Page() {
return (
<div>
<Button>Click me</Button>
</div>
)
}

View file

@ -0,0 +1,124 @@
import { useState } from "react"
import { Button, Input, Card, CardBody, CardFooter } from "@nextui-org/react"
import { activeClient } from "../../../utils/client"
export default function JoinForm() {
const [formData, setFormData] = useState({
name: "",
class: "",
number: "",
major: "",
phone: "",
qq: "",
email: "",
memo: "",
})
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target
setFormData(prevData => ({
...prevData,
[name]: value,
}))
}
const handleSubmit = async () => {
try {
await activeClient.freshman.postFreshmanAdd({
requestBody: formData,
})
alert("提交成功! 后续请加群获取!")
window.location.href = "/about"
}
catch (error) {
console.error("Error submitting form:", error)
alert("Failed to submit form.")
}
}
return (
<div
style={{
justifyContent: "center",
alignItems: "center",
display: "flex",
}}
>
<form>
<Card>
<CardBody
style={{
maxWidth: "700px",
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "1rem",
}}
>
<Input
name="name"
placeholder="姓名"
value={formData.name}
onChange={handleChange}
required
/>
<Input
name="class"
placeholder="班级"
value={formData.class}
onChange={handleChange}
required
/>
<Input
name="number"
placeholder="学号"
value={formData.number}
onChange={handleChange}
required
/>
<Input
name="major"
placeholder="专业"
value={formData.major}
onChange={handleChange}
required
/>
<Input
name="phone"
placeholder="电话"
value={formData.phone}
onChange={handleChange}
required
/>
<Input
name="qq"
placeholder="QQ"
value={formData.qq}
onChange={handleChange}
required
/>
<Input
name="email"
placeholder="邮箱"
value={formData.email}
onChange={handleChange}
required
/>
<Input
name="memo"
placeholder="备注"
value={formData.memo}
onChange={handleChange}
required
/>
</CardBody>
<CardFooter
style={{
justifyContent: "center",
alignItems: "center",
display: "flex",
}}
>
<Button onClick={handleSubmit}></Button>
</CardFooter>
</Card>
</form>
</div>
)
}

View file

@ -0,0 +1,61 @@
import {
// Button,
// Input,
Card,
CardBody,
CardFooter,
// PaginationItem,
} from "@nextui-org/react"
import {
activeClient,
type GetFreshmanListResponse,
} from "../../../utils/client"
// activeClient.freshman.getFreshmanList();
import { useEffect, useState } from "react"
// import { Pagination } from "@nextui-org/react";
const FreshmanList = () => {
const [freshmen, setFreshmen] = useState<GetFreshmanListResponse>({
list: [],
total: 0,
})
const [currentPage] = useState(1) // setCurrentPage
useEffect(() => {
const fetchFreshmen = async () => {
const result = await activeClient.freshman.getFreshmanList({
page: currentPage,
})
setFreshmen(result)
}
fetchFreshmen()
}, [])
return (
<div>
<Card>
<CardBody>
{freshmen.list.map(freshman => (
<div key={freshman.number}>
<div>{freshman.name}</div>
<div>{freshman.number}</div>
<div>{freshman.major}</div>
<div>{freshman.class}</div>
<div>{freshman.email}</div>
<div>{freshman.phone}</div>
<div>{freshman.qq}</div>
</div>
))}
</CardBody>
<CardFooter>
{/* <PaginationItem
// count={Math.ceil(freshmen.total / ITEMS_PER_PAGE)}
page={currentPage}
onChange={(page) => {
setCurrentPage(page);
}}
/> */}
</CardFooter>
</Card>
</div>
)
}
export default FreshmanList

View file

@ -4,10 +4,12 @@ import Tile from "../layouts/Tile.astro"
import MoreTile from "../layouts/MoreTile.astro"
const allPosts = await Astro.glob("../pages/posts/*.md")
const Blogs = await Astro.glob("../pages/posts/blogs/**/*.md")
allPosts.sort(
(a, b) =>
Date.parse(b.frontmatter.pubDate) - Date.parse(a.frontmatter.pubDate),
)
allPosts.sort((a, b) => {
if ((a.frontmatter.title as string).startsWith("计算机协会入社指南")) {
return -99999999999
}
return Date.parse(b.frontmatter.pubDate) - Date.parse(a.frontmatter.pubDate)
})
Blogs.sort(
(a, b) =>

View file

@ -18,14 +18,24 @@ import mp4 from "./_assets/nbtca.mp4";
<p class="life-at-apple__body-text">九月二十五日,百团等你!</p>
</div>
<div class="learn-more-link-container">
<a
href="/posts/计算机协会入社指南"
class="learn-more-link"
aria-label="进一步了解 NBTCA 的社区和文化"
>
了解在 NBTCA
</a>
<a class="learn-more-link-after">&gt; </a>
<div class="learn-more-link">
<a
href="/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%8D%8F%E4%BC%9A%E5%85%A5%E7%A4%BE%E6%8C%87%E5%8D%97"
aria-label="进一步了解 NBTCA 的社区和文化"
>
了解在 NBTCA
</a>
<a class="learn-more-link-after">&gt; </a>
<br />
<br />
<a
href="/freshman/join"
aria-label="申请加入 NBTCA"
>
填写入社信息
</a>
<a class="learn-more-link-after">&gt; </a>
</div>
</div>
<video width="640" height="360" autoplay muted>
<source src={mp4} type="video/mp4" />

View file

@ -57,7 +57,7 @@ tags: ["指南"]
# 看不见的地方 🗿
- 上面的很多工作是由一群维护[服务器](https://i.nbtca.space/)和代码的同志提供服务支持的,所以我们在看不见的地方有一些工作,如果你希望参与进来,这里有一份[参考 💾](此条目等待扩充),当然你也可以[加入我们](此条目等待扩充)同我们一起研究。
- 上面的很多工作是由一群维护[服务器](https://i.nbtca.space/)和代码的同志提供服务支持的,所以我们在看不见的地方有一些工作,如果你希望参与进来,这里有一份[参考 💾](https://github.com/nbtca/documents/blob/main/%E5%BC%80%E5%8F%91%E7%BB%84/2023.10.%E5%BC%80%E5%8F%91%E7%BB%84%E6%96%B0%E4%BA%BA%E5%9F%B9%E5%85%BB%E6%96%B9%E6%A1%88.md),当然你也可以[加入我们](/join-us)同我们一起研究。
- mc 服务器建设[🎮](https://mc.nbtca.space/)
- 电子游戏竞赛?(你唠这个我可就不困了 🤠)
- EDC、桌搭、摄影、户外进来唠

View file

@ -0,0 +1,37 @@
import type { BaseHttpRequest } from "./core/BaseHttpRequest";
import type { OpenAPIConfig } from "./core/OpenAPI";
import { Interceptors } from "./core/OpenAPI";
import { FetchHttpRequest } from "./core/FetchHttpRequest";
import { FreshmanService } from "./services.gen";
type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest;
export class ApiClient {
public readonly freshman: FreshmanService;
public readonly request: BaseHttpRequest;
constructor(
config?: Partial<OpenAPIConfig>,
HttpRequest: HttpRequestConstructor = FetchHttpRequest,
) {
this.request = new HttpRequest({
BASE: config?.BASE ?? "",
VERSION: config?.VERSION ?? "1.0.0",
WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false,
CREDENTIALS: config?.CREDENTIALS ?? "include",
TOKEN: config?.TOKEN,
USERNAME: config?.USERNAME,
PASSWORD: config?.PASSWORD,
HEADERS: config?.HEADERS,
ENCODE_PATH: config?.ENCODE_PATH,
interceptors: {
request: config?.interceptors?.request ?? new Interceptors(),
response: config?.interceptors?.response ?? new Interceptors(),
},
});
this.freshman = new FreshmanService(this.request);
}
}

View file

@ -0,0 +1,25 @@
import type { ApiRequestOptions } from "./ApiRequestOptions";
import type { ApiResult } from "./ApiResult";
export class ApiError extends Error {
public readonly url: string;
public readonly status: number;
public readonly statusText: string;
public readonly body: unknown;
public readonly request: ApiRequestOptions;
constructor(
request: ApiRequestOptions,
response: ApiResult,
message: string,
) {
super(message);
this.name = "ApiError";
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
this.body = response.body;
this.request = request;
}
}

View file

@ -0,0 +1,21 @@
export type ApiRequestOptions<T = unknown> = {
readonly body?: any;
readonly cookies?: Record<string, unknown>;
readonly errors?: Record<number | string, string>;
readonly formData?: Record<string, unknown> | any[] | Blob | File;
readonly headers?: Record<string, unknown>;
readonly mediaType?: string;
readonly method:
| "DELETE"
| "GET"
| "HEAD"
| "OPTIONS"
| "PATCH"
| "POST"
| "PUT";
readonly path?: Record<string, unknown>;
readonly query?: Record<string, unknown>;
readonly responseHeader?: string;
readonly responseTransformer?: (data: unknown) => Promise<T>;
readonly url: string;
};

View file

@ -0,0 +1,7 @@
export type ApiResult<TData = any> = {
readonly body: TData;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly url: string;
};

View file

@ -0,0 +1,11 @@
import type { ApiRequestOptions } from "./ApiRequestOptions";
import type { CancelablePromise } from "./CancelablePromise";
import type { OpenAPIConfig } from "./OpenAPI";
export abstract class BaseHttpRequest {
constructor(public readonly config: OpenAPIConfig) {}
public abstract request<T>(
options: ApiRequestOptions<T>,
): CancelablePromise<T>;
}

View file

@ -0,0 +1,126 @@
export class CancelError extends Error {
constructor(message: string) {
super(message);
this.name = "CancelError";
}
public get isCancelled(): boolean {
return true;
}
}
export interface OnCancel {
readonly isResolved: boolean;
readonly isRejected: boolean;
readonly isCancelled: boolean;
(cancelHandler: () => void): void;
}
export class CancelablePromise<T> implements Promise<T> {
private _isResolved: boolean;
private _isRejected: boolean;
private _isCancelled: boolean;
readonly cancelHandlers: (() => void)[];
readonly promise: Promise<T>;
private _resolve?: (value: T | PromiseLike<T>) => void;
private _reject?: (reason?: unknown) => void;
constructor(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: unknown) => void,
onCancel: OnCancel,
) => void,
) {
this._isResolved = false;
this._isRejected = false;
this._isCancelled = false;
this.cancelHandlers = [];
this.promise = new Promise<T>((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
const onResolve = (value: T | PromiseLike<T>): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this._isResolved = true;
if (this._resolve) this._resolve(value);
};
const onReject = (reason?: unknown): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this._isRejected = true;
if (this._reject) this._reject(reason);
};
const onCancel = (cancelHandler: () => void): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this.cancelHandlers.push(cancelHandler);
};
Object.defineProperty(onCancel, "isResolved", {
get: (): boolean => this._isResolved,
});
Object.defineProperty(onCancel, "isRejected", {
get: (): boolean => this._isRejected,
});
Object.defineProperty(onCancel, "isCancelled", {
get: (): boolean => this._isCancelled,
});
return executor(onResolve, onReject, onCancel as OnCancel);
});
}
get [Symbol.toStringTag]() {
return "Cancellable Promise";
}
public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
): Promise<TResult1 | TResult2> {
return this.promise.then(onFulfilled, onRejected);
}
public catch<TResult = never>(
onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null,
): Promise<T | TResult> {
return this.promise.catch(onRejected);
}
public finally(onFinally?: (() => void) | null): Promise<T> {
return this.promise.finally(onFinally);
}
public cancel(): void {
if (this._isResolved || this._isRejected || this._isCancelled) {
return;
}
this._isCancelled = true;
if (this.cancelHandlers.length) {
try {
for (const cancelHandler of this.cancelHandlers) {
cancelHandler();
}
} catch (error) {
console.warn("Cancellation threw an error", error);
return;
}
}
this.cancelHandlers.length = 0;
if (this._reject) this._reject(new CancelError("Request aborted"));
}
public get isCancelled(): boolean {
return this._isCancelled;
}
}

View file

@ -0,0 +1,23 @@
import type { ApiRequestOptions } from "./ApiRequestOptions";
import { BaseHttpRequest } from "./BaseHttpRequest";
import type { CancelablePromise } from "./CancelablePromise";
import type { OpenAPIConfig } from "./OpenAPI";
import { request as __request } from "./request";
export class FetchHttpRequest extends BaseHttpRequest {
constructor(config: OpenAPIConfig) {
super(config);
}
/**
* Request method
* @param options The request options from the service
* @returns CancelablePromise<T>
* @throws ApiError
*/
public override request<T>(
options: ApiRequestOptions<T>,
): CancelablePromise<T> {
return __request(this.config, options);
}
}

View file

@ -0,0 +1,56 @@
import type { ApiRequestOptions } from "./ApiRequestOptions";
type Headers = Record<string, string>;
type Middleware<T> = (value: T) => T | Promise<T>;
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
export class Interceptors<T> {
_fns: Middleware<T>[];
constructor() {
this._fns = [];
}
eject(fn: Middleware<T>): void {
const index = this._fns.indexOf(fn);
if (index !== -1) {
this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)];
}
}
use(fn: Middleware<T>): void {
this._fns = [...this._fns, fn];
}
}
export type OpenAPIConfig = {
BASE: string;
CREDENTIALS: "include" | "omit" | "same-origin";
ENCODE_PATH?: ((path: string) => string) | undefined;
HEADERS?: Headers | Resolver<Headers> | undefined;
PASSWORD?: string | Resolver<string> | undefined;
TOKEN?: string | Resolver<string> | undefined;
USERNAME?: string | Resolver<string> | undefined;
VERSION: string;
WITH_CREDENTIALS: boolean;
interceptors: {
request: Interceptors<RequestInit>;
response: Interceptors<Response>;
};
};
export const OpenAPI: OpenAPIConfig = {
BASE: "",
CREDENTIALS: "include",
ENCODE_PATH: undefined,
HEADERS: undefined,
PASSWORD: undefined,
TOKEN: undefined,
USERNAME: undefined,
VERSION: "1.0.0",
WITH_CREDENTIALS: false,
interceptors: {
request: new Interceptors(),
response: new Interceptors(),
},
};

View file

@ -0,0 +1,400 @@
import { ApiError } from "./ApiError";
import type { ApiRequestOptions } from "./ApiRequestOptions";
import type { ApiResult } from "./ApiResult";
import { CancelablePromise } from "./CancelablePromise";
import type { OnCancel } from "./CancelablePromise";
import type { OpenAPIConfig } from "./OpenAPI";
export const isString = (value: unknown): value is string => {
return typeof value === "string";
};
export const isStringWithValue = (value: unknown): value is string => {
return isString(value) && value !== "";
};
export const isBlob = (value: any): value is Blob => {
return value instanceof Blob;
};
export const isFormData = (value: unknown): value is FormData => {
return value instanceof FormData;
};
export const base64 = (str: string): string => {
try {
return btoa(str);
} catch (err) {
// @ts-ignore
return Buffer.from(str).toString("base64");
}
};
export const getQueryString = (params: Record<string, unknown>): string => {
const qs: string[] = [];
const append = (key: string, value: unknown) => {
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
};
const encodePair = (key: string, value: unknown) => {
if (value === undefined || value === null) {
return;
}
if (value instanceof Date) {
append(key, value.toISOString());
} else if (Array.isArray(value)) {
value.forEach((v) => encodePair(key, v));
} else if (typeof value === "object") {
Object.entries(value).forEach(([k, v]) => encodePair(`${key}[${k}]`, v));
} else {
append(key, value);
}
};
Object.entries(params).forEach(([key, value]) => encodePair(key, value));
return qs.length ? `?${qs.join("&")}` : "";
};
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
const encoder = config.ENCODE_PATH || encodeURI;
const path = options.url
.replace("{api-version}", config.VERSION)
.replace(/{(.*?)}/g, (substring: string, group: string) => {
if (options.path?.hasOwnProperty(group)) {
return encoder(String(options.path[group]));
}
return substring;
});
const url = config.BASE + path;
return options.query ? url + getQueryString(options.query) : url;
};
export const getFormData = (
options: ApiRequestOptions,
): FormData | undefined => {
if (options.formData) {
const formData = new FormData();
const process = (key: string, value: unknown) => {
if (isString(value) || isBlob(value)) {
formData.append(key, value);
} else {
formData.append(key, JSON.stringify(value));
}
};
Object.entries(options.formData)
.filter(([, value]) => value !== undefined && value !== null)
.forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((v) => process(key, v));
} else {
process(key, value);
}
});
return formData;
}
return undefined;
};
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
export const resolve = async <T>(
options: ApiRequestOptions<T>,
resolver?: T | Resolver<T>,
): Promise<T | undefined> => {
if (typeof resolver === "function") {
return (resolver as Resolver<T>)(options);
}
return resolver;
};
export const getHeaders = async <T>(
config: OpenAPIConfig,
options: ApiRequestOptions<T>,
): Promise<Headers> => {
const [token, username, password, additionalHeaders] = await Promise.all([
// @ts-ignore
resolve(options, config.TOKEN),
// @ts-ignore
resolve(options, config.USERNAME),
// @ts-ignore
resolve(options, config.PASSWORD),
// @ts-ignore
resolve(options, config.HEADERS),
]);
const headers = Object.entries({
Accept: "application/json",
...additionalHeaders,
...options.headers,
})
.filter(([, value]) => value !== undefined && value !== null)
.reduce(
(headers, [key, value]) => ({
...headers,
[key]: String(value),
}),
{} as Record<string, string>,
);
if (isStringWithValue(token)) {
headers["Authorization"] = `Bearer ${token}`;
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers["Authorization"] = `Basic ${credentials}`;
}
if (options.body !== undefined) {
if (options.mediaType) {
headers["Content-Type"] = options.mediaType;
} else if (isBlob(options.body)) {
headers["Content-Type"] = options.body.type || "application/octet-stream";
} else if (isString(options.body)) {
headers["Content-Type"] = "text/plain";
} else if (!isFormData(options.body)) {
headers["Content-Type"] = "application/json";
}
}
return new Headers(headers);
};
export const getRequestBody = (options: ApiRequestOptions): unknown => {
if (options.body !== undefined) {
if (
options.mediaType?.includes("application/json") ||
options.mediaType?.includes("+json")
) {
return JSON.stringify(options.body);
} else if (
isString(options.body) ||
isBlob(options.body) ||
isFormData(options.body)
) {
return options.body;
} else {
return JSON.stringify(options.body);
}
}
return undefined;
};
export const sendRequest = async (
config: OpenAPIConfig,
options: ApiRequestOptions,
url: string,
body: any,
formData: FormData | undefined,
headers: Headers,
onCancel: OnCancel,
): Promise<Response> => {
const controller = new AbortController();
let request: RequestInit = {
headers,
body: body ?? formData,
method: options.method,
signal: controller.signal,
};
if (config.WITH_CREDENTIALS) {
request.credentials = config.CREDENTIALS;
}
for (const fn of config.interceptors.request._fns) {
request = await fn(request);
}
onCancel(() => controller.abort());
return await fetch(url, request);
};
export const getResponseHeader = (
response: Response,
responseHeader?: string,
): string | undefined => {
if (responseHeader) {
const content = response.headers.get(responseHeader);
if (isString(content)) {
return content;
}
}
return undefined;
};
export const getResponseBody = async (response: Response): Promise<unknown> => {
if (response.status !== 204) {
try {
const contentType = response.headers.get("Content-Type");
if (contentType) {
const binaryTypes = [
"application/octet-stream",
"application/pdf",
"application/zip",
"audio/",
"image/",
"video/",
];
if (
contentType.includes("application/json") ||
contentType.includes("+json")
) {
return await response.json();
} else if (binaryTypes.some((type) => contentType.includes(type))) {
return await response.blob();
} else if (contentType.includes("multipart/form-data")) {
return await response.formData();
} else if (contentType.includes("text/")) {
return await response.text();
}
}
} catch (error) {
console.error(error);
}
}
return undefined;
};
export const catchErrorCodes = (
options: ApiRequestOptions,
result: ApiResult,
): void => {
const errors: Record<number, string> = {
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Payload Too Large",
414: "URI Too Long",
415: "Unsupported Media Type",
416: "Range Not Satisfiable",
417: "Expectation Failed",
418: "Im a teapot",
421: "Misdirected Request",
422: "Unprocessable Content",
423: "Locked",
424: "Failed Dependency",
425: "Too Early",
426: "Upgrade Required",
428: "Precondition Required",
429: "Too Many Requests",
431: "Request Header Fields Too Large",
451: "Unavailable For Legal Reasons",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
505: "HTTP Version Not Supported",
506: "Variant Also Negotiates",
507: "Insufficient Storage",
508: "Loop Detected",
510: "Not Extended",
511: "Network Authentication Required",
...options.errors,
};
const error = errors[result.status];
if (error) {
throw new ApiError(options, result, error);
}
if (!result.ok) {
const errorStatus = result.status ?? "unknown";
const errorStatusText = result.statusText ?? "unknown";
const errorBody = (() => {
try {
return JSON.stringify(result.body, null, 2);
} catch (e) {
return undefined;
}
})();
throw new ApiError(
options,
result,
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`,
);
}
};
/**
* Request method
* @param config The OpenAPI configuration object
* @param options The request options from the service
* @returns CancelablePromise<T>
* @throws ApiError
*/
export const request = <T>(
config: OpenAPIConfig,
options: ApiRequestOptions<T>,
): CancelablePromise<T> => {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(config, options);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(config, options);
if (!onCancel.isCancelled) {
let response = await sendRequest(
config,
options,
url,
body,
formData,
headers,
onCancel,
);
for (const fn of config.interceptors.response._fns) {
response = await fn(response);
}
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(
response,
options.responseHeader,
);
let transformedBody = responseBody;
if (options.responseTransformer && response.ok) {
transformedBody = await options.responseTransformer(responseBody);
}
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader ?? transformedBody,
};
catchErrorCodes(options, result);
resolve(result.body);
}
} catch (error) {
reject(error);
}
});
};

View file

@ -0,0 +1,8 @@
// This file is auto-generated by @hey-api/openapi-ts
export { ApiClient } from "./ApiClient";
export { ApiError } from "./core/ApiError";
export { BaseHttpRequest } from "./core/BaseHttpRequest";
export { CancelablePromise, CancelError } from "./core/CancelablePromise";
export { OpenAPI, type OpenAPIConfig } from "./core/OpenAPI";
export * from "./services.gen";
export * from "./types.gen";

View file

@ -0,0 +1,54 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { CancelablePromise } from "./core/CancelablePromise";
import type { BaseHttpRequest } from "./core/BaseHttpRequest";
import type {
PostFreshmanAddData,
PostFreshmanAddResponse,
GetFreshmanListData,
GetFreshmanListResponse,
} from "./types.gen";
export class FreshmanService {
constructor(public readonly httpRequest: BaseHttpRequest) {}
/**
*
* @param data The data for the request.
* @param data.requestBody
* @returns unknown
* @throws ApiError
*/
public postFreshmanAdd(
data: PostFreshmanAddData = {},
): CancelablePromise<PostFreshmanAddResponse> {
return this.httpRequest.request({
method: "POST",
url: "/api/freshman",
body: data.requestBody,
mediaType: "application/json",
});
}
/**
*
* @param data The data for the request.
* @param data.page
* @returns unknown
* @throws ApiError
*/
public getFreshmanList(
data: GetFreshmanListData = {},
): CancelablePromise<GetFreshmanListResponse> {
return this.httpRequest.request({
method: "GET",
url: "/api/freshman",
query: {
page: data.page,
},
errors: {
500: "服务器错误",
},
});
}
}

View file

@ -0,0 +1,47 @@
// This file is auto-generated by @hey-api/openapi-ts
export type PostFreshmanAddData = {
requestBody?: {
name: string;
number: string;
major: string;
class: string;
email: string;
phone: string;
qq: string;
};
};
export type PostFreshmanAddResponse = {
success: boolean;
result?: {
name: string;
number: string;
major: string;
class: string;
email: string;
phone: string;
qq: string;
};
error?: string;
};
export type GetFreshmanListData = {
/**
*
*/
page?: number;
};
export type GetFreshmanListResponse = {
list: Array<{
name: string;
number: string;
major: string;
class: string;
email: string;
phone: string;
qq: string;
}>;
total: number;
};

View file

@ -1,6 +1,14 @@
import createClient from "openapi-fetch"
import type { paths } from "../types/saturday"
export const saturdayClient = createClient<paths>({
import type { paths as saturdayPaths } from "../types/saturday"
// import type { paths as activePaths } from "../types/active"
import { ApiClient } from "./active"
export const saturdayClient = createClient<saturdayPaths>({
baseUrl: "https://api.nbtca.space/v2/",
})
// export const activeClient = createClient<activePaths>({
// baseUrl: "https://active.nbtca.space/",
// })
export const activeClient = new ApiClient({
BASE: "/active",
})
export * from "./active/types.gen"