198 lines
7.5 KiB
TypeScript
198 lines
7.5 KiB
TypeScript
"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功能 - 修复xxx问题"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="changelogEn">Changelog (English)</Label>
|
||
<Textarea
|
||
id="changelogEn"
|
||
name="changelogEn"
|
||
rows={6}
|
||
placeholder="- Added xxx feature - 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>
|
||
);
|
||
}
|