Files
nanami-web/src/app/admin/(dashboard)/software/page.tsx
2026-05-12 09:58:25 +08:00

134 lines
4.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.

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>
);
}