Files
2026-09-04 16:30:37 +08:00

211 lines
8.4 KiB
TypeScript

"use client";
import { useRef, useState } from "react";
export type HttpHeaderItem = {
id: string;
key: string;
value: string;
};
export type HttpProbeOptions = {
body: string;
headers: HttpHeaderItem[];
method: string;
};
type PingHttpOptionsProps = {
disabled?: boolean;
onChange: (value: HttpProbeOptions) => void;
value: HttpProbeOptions;
};
const HTTP_METHODS = ["GET", "POST", "HEAD", "PUT", "PATCH", "DELETE"] as const;
const COMMON_HEADERS = [
"Accept",
"Accept-Language",
"Authorization",
"Cache-Control",
"Content-Type",
"Cookie",
"Origin",
"Referer",
"User-Agent",
"X-Requested-With",
] as const;
function cleanHeaders(headers: HttpHeaderItem[]) {
return headers.filter((header) => header.key.trim() || header.value.trim());
}
export function createDefaultHttpOptions(): HttpProbeOptions {
return {
body: "",
headers: [{ id: "header-1", key: "", value: "" }],
method: "GET",
};
}
export function toHttpOptionsPayload(value: HttpProbeOptions) {
return {
body: value.body,
headers: value.headers
.filter((header) => header.key.trim() && header.value.trim())
.reduce<Record<string, string>>((headers, header) => {
headers[header.key.trim()] = header.value.trim();
return headers;
}, {}),
method: value.method,
};
}
export function PingHttpOptions({
disabled = false,
onChange,
value,
}: PingHttpOptionsProps) {
const [expanded, setExpanded] = useState(false);
const nextHeaderIdRef = useRef(2);
function update(nextValue: HttpProbeOptions) {
onChange({
...nextValue,
headers: nextValue.headers.length ? nextValue.headers : createDefaultHttpOptions().headers,
});
}
function updateHeader(id: string, field: "key" | "value", nextFieldValue: string) {
update({
...value,
headers: value.headers.map((header) =>
header.id === id ? { ...header, [field]: nextFieldValue } : header,
),
});
}
function addHeader() {
const nextId = nextHeaderIdRef.current;
nextHeaderIdRef.current += 1;
update({
...value,
headers: [...value.headers, { id: `header-${nextId}`, key: "", value: "" }],
});
}
function removeHeader(id: string) {
update({
...value,
headers: value.headers.filter((header) => header.id !== id),
});
}
const activeHeaderCount = cleanHeaders(value.headers).length;
return (
<section className="mx-auto w-full max-w-7xl px-4 pb-2">
<div className="rounded-lg border border-zinc-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-900">
<button
className="flex w-full items-center justify-between px-4 py-3 text-left text-sm font-medium text-zinc-700 transition hover:bg-zinc-50 disabled:cursor-not-allowed dark:text-gray-200 dark:hover:bg-gray-800"
disabled={disabled}
onClick={() => setExpanded((current) => !current)}
type="button"
>
<span>高级功能</span>
<span className="flex items-center gap-3 text-xs text-zinc-500 dark:text-gray-400">
{value.method}
{activeHeaderCount ? `${activeHeaderCount} Headers` : "无 Header"}
<span className={`transition ${expanded ? "rotate-180" : ""}`}>⌄</span>
</span>
</button>
{expanded ? (
<div className="border-t border-zinc-100 p-4 dark:border-gray-700">
<div className="grid grid-cols-[160px_1fr] gap-4">
<div>
<label className="mb-2 block text-xs font-medium text-zinc-500 dark:text-gray-400">
请求类型
</label>
<select
className="h-10 w-full rounded-lg border border-zinc-300 bg-zinc-50 px-3 text-sm text-zinc-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 disabled:cursor-not-allowed disabled:bg-zinc-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:focus:border-blue-400 dark:focus:bg-gray-900 dark:focus:ring-blue-950"
disabled={disabled}
onChange={(event) => update({ ...value, method: event.target.value })}
value={value.method}
>
{HTTP_METHODS.map((method) => (
<option key={method} value={method}>
{method}
</option>
))}
</select>
</div>
<div>
<div className="mb-2 flex items-center justify-between">
<span className="text-xs font-medium text-zinc-500 dark:text-gray-400">
Headers
</span>
<button
className="rounded text-xs font-medium text-blue-600 transition hover:text-blue-700 disabled:cursor-not-allowed disabled:text-zinc-400 dark:text-blue-400 dark:hover:text-blue-300"
disabled={disabled}
onClick={addHeader}
type="button"
>
+ 添加 Header
</button>
</div>
<div className="space-y-2">
{value.headers.map((header) => (
<div className="grid grid-cols-[220px_1fr_34px] gap-2" key={header.id}>
<input
className="h-10 rounded-lg border border-zinc-300 bg-zinc-50 px-3 text-sm text-zinc-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 disabled:cursor-not-allowed disabled:bg-zinc-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:focus:border-blue-400 dark:focus:bg-gray-900 dark:focus:ring-blue-950"
disabled={disabled}
list="ping-http-header-names"
onChange={(event) => updateHeader(header.id, "key", event.target.value)}
placeholder="Header 名称"
value={header.key}
/>
<input
className="h-10 rounded-lg border border-zinc-300 bg-zinc-50 px-3 text-sm text-zinc-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 disabled:cursor-not-allowed disabled:bg-zinc-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:focus:border-blue-400 dark:focus:bg-gray-900 dark:focus:ring-blue-950"
disabled={disabled}
onChange={(event) => updateHeader(header.id, "value", event.target.value)}
placeholder="Header 值"
value={header.value}
/>
<button
className="inline-flex h-10 items-center justify-center rounded-lg text-zinc-400 transition hover:bg-zinc-100 hover:text-zinc-700 disabled:cursor-not-allowed disabled:text-zinc-300 dark:hover:bg-gray-800 dark:hover:text-gray-200"
disabled={disabled || value.headers.length <= 1}
onClick={() => removeHeader(header.id)}
type="button"
>
x
</button>
</div>
))}
</div>
<datalist id="ping-http-header-names">
{COMMON_HEADERS.map((header) => (
<option key={header} value={header} />
))}
</datalist>
</div>
</div>
<div className="mt-4">
<label className="mb-2 block text-xs font-medium text-zinc-500 dark:text-gray-400">
请求体
</label>
<textarea
className="min-h-24 w-full rounded-lg border border-zinc-300 bg-zinc-50 px-3 py-2 text-sm text-zinc-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 disabled:cursor-not-allowed disabled:bg-zinc-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:focus:border-blue-400 dark:focus:bg-gray-900 dark:focus:ring-blue-950"
disabled={disabled}
onChange={(event) => update({ ...value, body: event.target.value })}
placeholder="POST/PUT/PATCH 请求体,可留空"
value={value.body}
/>
</div>
</div>
) : null}
</div>
</section>
);
}