Files
nanami-web/src/components/admin/SoftwareVersionForm.tsx
2026-05-12 09:58:25 +08:00

198 lines
7.5 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 { useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "sonner";
import { Upload } from "lucide-react";
export function SoftwareVersionForm({ softwareId }: { softwareId: string }) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [downloadType, setDownloadType] = useState("local");
const [wowVersion, setWowVersion] = useState<"1.18" | "1.17">("1.18");
const [uploadedFilePath, setUploadedFilePath] = useState("");
const [fileSize, setFileSize] = useState(0);
const [uploading, setUploading] = useState(false);
async function handleFileUpload(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
const formData = new FormData();
formData.append("file", file);
const res = await fetch("/api/upload", { method: "POST", body: formData });
if (res.ok) {
const data = await res.json();
setUploadedFilePath(data.filePath);
setFileSize(data.size);
toast.success(`文件 ${data.originalName} 上传成功`);
} else {
toast.error("文件上传失败");
}
setUploading(false);
}
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setLoading(true);
const fd = new FormData(e.currentTarget);
if (downloadType === "local" && !uploadedFilePath) {
toast.error("请先上传文件");
setLoading(false);
return;
}
const res = await fetch(`/api/software/${softwareId}/versions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
version: fd.get("version"),
versionCode: Number(fd.get("versionCode")),
changelog: fd.get("changelog"),
changelogEn: fd.get("changelogEn"),
downloadType,
filePath: downloadType === "local" ? uploadedFilePath : null,
externalUrl: downloadType === "url" ? fd.get("externalUrl") : null,
fileSize,
forceUpdate: fd.get("forceUpdate") === "on",
minVersion: fd.get("minVersion") || null,
wowVersion,
}),
});
if (res.ok) {
toast.success("版本发布成功");
router.push("/admin/software");
router.refresh();
} else {
const err = await res.json();
toast.error(err.error || "发布失败");
}
setLoading(false);
}
return (
<Card>
<CardHeader><CardTitle></CardTitle></CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid gap-4 md:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="version"> *</Label>
<Input id="version" name="version" required placeholder="1.0.0" />
</div>
<div className="space-y-2">
<Label htmlFor="versionCode"> () *</Label>
<Input id="versionCode" name="versionCode" type="number" required placeholder="100" />
</div>
<div className="space-y-2">
<Label>WoW *</Label>
<div className="inline-flex w-full rounded-md border bg-muted/40 p-0.5">
{(["1.18", "1.17"] as const).map((v) => (
<button
key={v}
type="button"
onClick={() => setWowVersion(v)}
className={`flex-1 rounded px-3 py-1.5 text-sm font-medium transition-colors ${
v === wowVersion
? "bg-background shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
{v}
</button>
))}
</div>
<p className="text-xs text-muted-foreground">
WoW
</p>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="minVersion"></Label>
<Input id="minVersion" name="minVersion" placeholder="0.9.0(可选,低于此版本需升级)" />
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="changelog"></Label>
<Textarea
id="changelog"
name="changelog"
rows={6}
placeholder="- 新增xxx功能&#10;- 修复xxx问题"
/>
</div>
<div className="space-y-2">
<Label htmlFor="changelogEn">Changelog (English)</Label>
<Textarea
id="changelogEn"
name="changelogEn"
rows={6}
placeholder="- Added xxx feature&#10;- Fixed xxx issue"
/>
<p className="text-xs text-muted-foreground">
访退
</p>
</div>
</div>
<div className="space-y-4">
<Label></Label>
<div className="flex gap-4">
<Button type="button" variant={downloadType === "local" ? "default" : "outline"} onClick={() => setDownloadType("local")}>
</Button>
<Button type="button" variant={downloadType === "url" ? "default" : "outline"} onClick={() => setDownloadType("url")}>
</Button>
</div>
{downloadType === "local" ? (
<div className="space-y-2">
<Label htmlFor="file" className="flex cursor-pointer items-center gap-2 rounded-lg border-2 border-dashed px-6 py-4 transition-colors hover:border-primary">
<Upload className="h-5 w-5" />
{uploading ? "上传中..." : uploadedFilePath ? "重新选择文件" : "选择文件"}
</Label>
<Input id="file" type="file" className="hidden" onChange={handleFileUpload} />
{uploadedFilePath && (
<p className="text-sm text-muted-foreground">: {uploadedFilePath} ({(fileSize / 1024).toFixed(1)} KB)</p>
)}
</div>
) : (
<div className="space-y-2">
<Label htmlFor="externalUrl"></Label>
<Input id="externalUrl" name="externalUrl" type="url" placeholder="https://..." />
</div>
)}
</div>
<div className="flex items-center gap-2">
<input type="checkbox" id="forceUpdate" name="forceUpdate" className="h-4 w-4 rounded border-input" />
<Label htmlFor="forceUpdate">使</Label>
</div>
<div className="flex gap-3">
<Button type="submit" disabled={loading}>
{loading ? "发布中..." : "发布版本"}
</Button>
<Button type="button" variant="outline" onClick={() => router.back()}>
</Button>
</div>
</form>
</CardContent>
</Card>
);
}