98 lines
4.2 KiB
TypeScript
98 lines
4.2 KiB
TypeScript
"use client";
|
||
|
||
import { useRef, useState } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import type { BetaAccessRequestRecord } from "@/db/repository";
|
||
|
||
type SubmitState = { loading: boolean; message: string; error: boolean };
|
||
|
||
export function AccessRequestForm({
|
||
pendingRequest,
|
||
}: {
|
||
pendingRequest: BetaAccessRequestRecord | null;
|
||
}) {
|
||
const router = useRouter();
|
||
const pendingKey = useRef<string | null>(null);
|
||
const [preferredOs, setPreferredOs] = useState("windows");
|
||
const [requestedSlots, setRequestedSlots] = useState(1);
|
||
const [useCase, setUseCase] = useState("");
|
||
const [state, setState] = useState<SubmitState>({ loading: false, message: "", error: false });
|
||
|
||
async function post(payload: Record<string, unknown>) {
|
||
pendingKey.current ??= crypto.randomUUID();
|
||
const response = await fetch("/api/beta-access", {
|
||
method: "POST",
|
||
headers: { "content-type": "application/json" },
|
||
body: JSON.stringify({ ...payload, idempotencyKey: pendingKey.current }),
|
||
});
|
||
const body = (await response.json()) as { message?: string };
|
||
if (!response.ok) throw new Error(body.message || "操作失败");
|
||
pendingKey.current = null;
|
||
}
|
||
|
||
async function submit(event: React.FormEvent) {
|
||
event.preventDefault();
|
||
setState({ loading: true, message: "", error: false });
|
||
try {
|
||
await post({ action: "request", preferredOs, requestedSlots, useCase });
|
||
setState({ loading: false, message: "申请已提交,处理结果会显示在本页。", error: false });
|
||
router.refresh();
|
||
} catch (error) {
|
||
setState({ loading: false, message: error instanceof Error ? error.message : "提交失败", error: true });
|
||
}
|
||
}
|
||
|
||
async function cancel() {
|
||
if (!pendingRequest) return;
|
||
setState({ loading: true, message: "", error: false });
|
||
try {
|
||
await post({ action: "cancel", requestId: pendingRequest.id });
|
||
setState({ loading: false, message: "申请已撤回。", error: false });
|
||
router.refresh();
|
||
} catch (error) {
|
||
setState({ loading: false, message: error instanceof Error ? error.message : "撤回失败", error: true });
|
||
}
|
||
}
|
||
|
||
if (pendingRequest) {
|
||
return (
|
||
<div className="cloud-form feedback-form">
|
||
<p>申请正在等待人工处理。测试资格没有承诺顺序,也不会因此建立付费关系。</p>
|
||
<button className="button button-secondary" type="button" onClick={cancel} disabled={state.loading}>
|
||
{state.loading ? "正在撤回…" : "撤回这条申请"}
|
||
</button>
|
||
{state.message && <p className={state.error ? "form-error" : "form-success"} role="status">{state.message}</p>}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<form className="cloud-form feedback-form" onSubmit={submit}>
|
||
<div className="admin-form-grid">
|
||
<label>
|
||
<span>计划使用的主机</span>
|
||
<select value={preferredOs} onChange={(event) => setPreferredOs(event.target.value)}>
|
||
<option value="windows">Windows</option>
|
||
<option value="linux">Linux</option>
|
||
<option value="both">Windows 和 Linux</option>
|
||
</select>
|
||
</label>
|
||
<label>
|
||
<span>希望接入几台</span>
|
||
<input type="number" min={1} max={3} value={requestedSlots} onChange={(event) => setRequestedSlots(Number(event.target.value))} required />
|
||
<small>首批申请限 1–3 台,实际名额以审核结果为准。</small>
|
||
</label>
|
||
</div>
|
||
<label>
|
||
<span>你准备怎么使用 NekoNest?</span>
|
||
<textarea minLength={20} maxLength={1000} value={useCase} onChange={(event) => setUseCase(event.target.value)} placeholder="例如:我主要在 Windows 主机上使用 Codex,希望从手机查看并继续已有任务。请勿粘贴令牌、密码、项目代码或私密会话内容。" required />
|
||
<small>{useCase.length}/1000 · 只需说明设备和使用场景,不要提交任何密钥或会话正文</small>
|
||
</label>
|
||
<button className="button button-primary" type="submit" disabled={state.loading}>
|
||
{state.loading ? "正在提交…" : "申请免费闭测资格"}
|
||
</button>
|
||
{state.message && <p className={state.error ? "form-error" : "form-success"} role="status">{state.message}</p>}
|
||
</form>
|
||
);
|
||
}
|