Files
nekonest-cloud/app/dashboard/billing/AccessRequestForm.tsx
T

98 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>首批申请限 13 台,实际名额以审核结果为准。</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>
);
}