134 lines
4.5 KiB
TypeScript
134 lines
4.5 KiB
TypeScript
import Link from "next/link";
|
||
import { prisma } from "@/lib/db";
|
||
import { Button } from "@/components/ui/button";
|
||
import {
|
||
Card,
|
||
CardContent,
|
||
CardHeader,
|
||
CardTitle,
|
||
CardDescription,
|
||
} from "@/components/ui/card";
|
||
import { Plus } from "lucide-react";
|
||
import { SoftwareVersionTable } from "@/components/admin/SoftwareVersionTable";
|
||
|
||
const SOFTWARE_DEFS = [
|
||
{
|
||
slug: "nanami-launcher",
|
||
name: "Nanami 启动器(全量包)",
|
||
description: "Nanami 插件启动器完整安装包,用户首次下载或大版本更新",
|
||
badgeLabel: "全量",
|
||
},
|
||
{
|
||
slug: "nanami-launcher-patch",
|
||
name: "Nanami 热更新包",
|
||
description: "app.asar 热更新包,客户端静默下载替换实现热更新",
|
||
badgeLabel: "热更新",
|
||
},
|
||
];
|
||
|
||
async function ensureSoftware(slug: string, name: string, description: string) {
|
||
let sw = await prisma.software.findUnique({ where: { slug } });
|
||
if (!sw) {
|
||
sw = await prisma.software.create({ data: { name, slug, description } });
|
||
}
|
||
return sw;
|
||
}
|
||
|
||
export const dynamic = "force-dynamic";
|
||
|
||
export default async function AdminSoftwarePage() {
|
||
const softwareItems = await Promise.all(
|
||
SOFTWARE_DEFS.map(async (def) => {
|
||
const sw = await ensureSoftware(def.slug, def.name, def.description);
|
||
const versions = await prisma.softwareVersion.findMany({
|
||
where: { softwareId: sw.id },
|
||
orderBy: { versionCode: "desc" },
|
||
});
|
||
const totalDownloads = versions.reduce((s, v) => s + v.downloadCount, 0);
|
||
const latestVersion = versions.find((v) => v.isLatest);
|
||
return { ...def, sw, versions, totalDownloads, latestVersion };
|
||
})
|
||
);
|
||
|
||
return (
|
||
<div className="space-y-10">
|
||
<div>
|
||
<h1 className="text-3xl font-bold">软件管理</h1>
|
||
<p className="mt-1 text-muted-foreground">
|
||
管理启动器全量包和热更新包,客户端通过 slug 分别检查更新
|
||
</p>
|
||
</div>
|
||
|
||
{softwareItems.map((item) => {
|
||
const serializedVersions = item.versions.map((v) => ({
|
||
...v,
|
||
createdAt: v.createdAt.toISOString(),
|
||
}));
|
||
|
||
return (
|
||
<div key={item.slug} className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<h2 className="text-xl font-semibold">{item.name}</h2>
|
||
<span className="rounded-md bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">
|
||
slug: {item.slug}
|
||
</span>
|
||
</div>
|
||
<Button
|
||
render={
|
||
<Link
|
||
href={`/admin/software/${item.sw.id}/versions/new`}
|
||
/>
|
||
}
|
||
>
|
||
<Plus className="mr-2 h-4 w-4" />
|
||
发布新版本
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="grid gap-4 md:grid-cols-2">
|
||
{(["1.18", "1.17"] as const).map((wv) => {
|
||
const latest = item.versions.find(
|
||
(v) => v.isLatest && v.wowVersion === wv
|
||
);
|
||
const total = item.versions.filter(
|
||
(v) => v.wowVersion === wv
|
||
).length;
|
||
const downloads = item.versions
|
||
.filter((v) => v.wowVersion === wv)
|
||
.reduce((s, v) => s + v.downloadCount, 0);
|
||
return (
|
||
<Card key={wv} className="border-amber-500/20">
|
||
<CardHeader className="pb-2">
|
||
<CardDescription className="flex items-center gap-2">
|
||
<span>WoW {wv}</span>
|
||
<span className="text-xs text-muted-foreground/70">
|
||
({total} 个版本 · {downloads} 次下载)
|
||
</span>
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<p className="text-2xl font-bold">
|
||
{latest ? `v${latest.version}` : "未发布"}
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>版本历史</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="p-0">
|
||
<SoftwareVersionTable versions={serializedVersions} />
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
}
|