乌龟服官服
This commit is contained in:
83
API.md
83
API.md
@@ -11,57 +11,27 @@ Bilingual (zh / en) public REST API for the Nanami Turtle WoW platform.
|
||||
|
||||
---
|
||||
|
||||
## 🎮 WoW 客户端版本(多版本支持)
|
||||
## 🎮 WoW 客户端版本
|
||||
|
||||
平台为 Turtle WoW **1.18** 与 **1.17** 两套客户端独立维护启动器、插件版本与发布渠道。每个 `Release` / `SoftwareVersion` 都打了 `wowVersion` 标签,且 `isLatest` 在 `(addon|software, wowVersion)` 维度内唯一 —— 同一插件可以同时存在「1.18 最新版」和「1.17 最新版」两条 latest。
|
||||
平台当前只对外提供 Turtle WoW **1.18** 版本。每个 `Release` / `SoftwareVersion` 仍保留 `wowVersion` 字段用于标识历史数据和数据库约束,但公共网页、后台、下载和更新接口都会固定使用 `1.18`。
|
||||
|
||||
### 如何选择 wow 版本
|
||||
### 版本解析
|
||||
|
||||
不同接口的解析策略略有差异:
|
||||
|
||||
#### 列表 / 浏览类接口(addons / releases / software / changelog)
|
||||
|
||||
| 来源 | 示例 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 查询参数 `wow` | `?wow=1.18` 或 `?wow=1.17` | 1(最高) |
|
||||
| 查询参数 `wowVersion` | `?wowVersion=1.17` | 2(别名) |
|
||||
| Cookie `wow` | 浏览器侧由前台切换设置 | 3 |
|
||||
| 默认 | — | `1.18` |
|
||||
|
||||
这些接口让浏览器在导航过程中保留用户选择的 wow 频道(cookie 持久化)。多数列表型接口还支持 `?wow=all` 表示不过滤、返回所有版本。
|
||||
|
||||
#### 下载 / 自更新类接口(latest / check-update / download/launcher)
|
||||
|
||||
| 来源 | 示例 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 查询参数 `wow` | `?wow=1.18` | 1(最高) |
|
||||
| 查询参数 `wowVersion` | `?wowVersion=1.17` | 2(别名) |
|
||||
| 默认 | — | `1.18` |
|
||||
|
||||
⚠️ **下载类接口故意不读 Cookie**。这是一个强约定:**URL 自己完全决定下载到的二进制**。
|
||||
|
||||
为什么这样设计:
|
||||
1. `https://nanami.rucky.cn/download/launcher` 这种第三方嵌入直链,必须永远稳定指向 1.18 版本,不能被访客之前在前台切换过的 cookie 污染。
|
||||
2. 启动器自更新(`/api/software/check-update`)的频道由启动器自身声明(`?wow=1.17`),避免 1.18 客户端因为 cookie 串台拿到 1.17 的更新。
|
||||
3. 不论中英文(`lang`)切换,下载到的二进制都是同一个,只跟 `?wow=` 相关。
|
||||
|
||||
合法值:`1.18`、`1.17`。
|
||||
- `wow` / `wowVersion` 查询参数可以省略;即使传入其他历史值,也会回落到 `1.18`
|
||||
- 浏览器 Cookie 不再参与 wow 版本选择
|
||||
- 列表接口不再支持 `?wow=all` 返回所有历史版本
|
||||
|
||||
### 响应字段
|
||||
|
||||
- 单条对象上含 `wowVersion`,标识该 release / version 所属的客户端版本
|
||||
- 列表型响应顶层含 `wowVersion` 字段,回显本次过滤值(`"all"` 表示未过滤)
|
||||
- 单条对象上含 `wowVersion`,当前对外返回值为 `"1.18"`
|
||||
- 列表型响应顶层含 `wowVersion` 字段,回显当前固定过滤值
|
||||
|
||||
### 启动器自更新建议
|
||||
|
||||
启动器调用 `/api/software/check-update` 时务必带上 `wow=1.18` 或 `wow=1.17`,否则会按默认 `1.18` 返回 —— 1.17 客户端拿到 1.18 的更新就出问题了。
|
||||
启动器调用 `/api/software/check-update` 时无需传 wow 频道;服务端始终返回 1.18 通道。
|
||||
|
||||
```bash
|
||||
# 1.17 启动器自检更新(已安装 versionCode=1010)
|
||||
curl 'https://nanami.rucky.cn/api/software/check-update?slug=nanami-launcher&versionCode=1010&wow=1.17'
|
||||
|
||||
# 1.18 启动器
|
||||
curl 'https://nanami.rucky.cn/api/software/check-update?slug=nanami-launcher&versionCode=1010&wow=1.18'
|
||||
curl 'https://nanami.rucky.cn/api/software/check-update?slug=nanami-launcher&versionCode=1010'
|
||||
```
|
||||
|
||||
---
|
||||
@@ -114,7 +84,7 @@ GET /api/software/check-update?slug={slug}&versionCode={n}&wow={wow}&lang={lang}
|
||||
|------|------|------|------|
|
||||
| slug | string | ✅ | 软件标识,例如 `nanami-launcher`、`nanami-launcher-patch` |
|
||||
| versionCode | number | ❌ | 当前客户端版本号(整数),缺省为 0 |
|
||||
| wow | string | ❌ | `1.18` / `1.17`,缺省 `1.18` |
|
||||
| wow | string | ❌ | 固定为 `1.18`;可省略 |
|
||||
| lang | string | ❌ | `zh` / `en`,缺省 `zh` |
|
||||
|
||||
**响应示例(wow=1.18, lang=en):**
|
||||
@@ -140,7 +110,7 @@ GET /api/software/check-update?slug={slug}&versionCode={n}&wow={wow}&lang={lang}
|
||||
}
|
||||
```
|
||||
|
||||
`forceUpdate=true` 仅当存在更新且最新版被标记为强制更新。`isLatest` 是 `(software, wowVersion)` 维度的 —— 1.18 与 1.17 各自有独立的 latest,互不影响。下载 URL 自动带 `source=launcher`,启动器更新下载会单独计数。
|
||||
`forceUpdate=true` 仅当存在更新且最新版被标记为强制更新。下载 URL 自动带 `source=launcher`,启动器更新下载会单独计数。
|
||||
|
||||
---
|
||||
|
||||
@@ -206,14 +176,13 @@ GET /download/launcher?wow={wow}
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| wow | string | ❌ | `1.18` / `1.17`,缺省 `1.18` |
|
||||
| wow | string | ❌ | 固定为 `1.18`;可省略 |
|
||||
|
||||
⚠️ **第三方链接安全**:这条 URL 是公开嵌入用的,**只看显式 `?wow=` 参数**,**不读用户 cookie**。意思是:即使用户之前在前台切到 1.17,再点这条不带 `?wow=` 的链接仍然会下载到 1.18。这样可以让站外的「下载启动器」按钮稳定指向 1.18。
|
||||
⚠️ **第三方链接安全**:这条 URL 是公开嵌入用的,不读用户 cookie,始终下载 1.18 通道。
|
||||
|
||||
```html
|
||||
<a href="https://nanami.rucky.cn/download/launcher">下载 Nanami 启动器(默认 WoW 1.18)</a>
|
||||
<a href="https://nanami.rucky.cn/download/launcher?wow=1.18">下载 Nanami 启动器(WoW 1.18)</a>
|
||||
<a href="https://nanami.rucky.cn/download/launcher?wow=1.17">下载 Nanami 启动器(WoW 1.17)</a>
|
||||
```
|
||||
|
||||
---
|
||||
@@ -258,7 +227,7 @@ GET /api/software/changelog?slug={slug}&wow={wow}&lang={lang}
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| slug | string | ✅ | 软件标识 |
|
||||
| wow | string | ❌ | `1.18` / `1.17`(缺省 `1.18`),传 `all` 不过滤 |
|
||||
| wow | string | ❌ | 固定为 `1.18`;可省略 |
|
||||
| lang | string | ❌ | `zh` / `en` |
|
||||
|
||||
**响应示例(wow=1.18, lang=en):**
|
||||
@@ -331,7 +300,7 @@ GET /api/software/changelog?slug={slug}&wow={wow}&lang={lang}
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| lang | string | ❌ | `zh` / `en` |
|
||||
| wow | string | ❌ | `1.18` / `1.17` —— 仅返回此 WoW 版本下的 latest release;传 `all` 表示不过滤 |
|
||||
| wow | string | ❌ | 固定为 `1.18`;可省略 |
|
||||
| published | string | ❌ | 默认 `true`;传 `false` 包括草稿 |
|
||||
| category | string | ❌ | 按分类过滤 |
|
||||
| search | string | ❌ | 名称 / 简介模糊匹配,中英都搜 |
|
||||
@@ -390,7 +359,7 @@ GET /api/software/changelog?slug={slug}&wow={wow}&lang={lang}
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| addonId | string | ❌ | 限定某个插件;缺省返回全平台所有插件的 release |
|
||||
| wow | string | ❌ | `1.18` / `1.17`,缺省 `1.18`;传 `all` 不过滤 |
|
||||
| wow | string | ❌ | 固定为 `1.18`;可省略 |
|
||||
| lang | string | ❌ | `zh` / `en` |
|
||||
|
||||
**响应示例:**
|
||||
@@ -538,7 +507,7 @@ GET /api/server-time
|
||||
| GET | `/api/software/check-update` | ✏️ | ✅ | ✅ | 启动器自更新检查 |
|
||||
| GET | `/api/software/download/{id}` | ✏️ | ❌ | ❌ | 下载启动器文件(按 versionId 精确) |
|
||||
| GET | `/api/software/latest` | ✏️ | ✅ | ✅ | 最新启动器信息 / 下载(网页用) |
|
||||
| GET | `/download/launcher` | 🆕 | ❌ | ✅ | 友好直链:按 wow 始终下载最新 |
|
||||
| GET | `/download/launcher` | 🆕 | ❌ | ✅ | 友好直链:始终下载 1.18 最新 |
|
||||
| POST | `/api/launcher/heartbeat` | — | ❌ | ❌ | 上报启动器在线状态 |
|
||||
| GET | `/api/software/changelog` | ✏️ | ✅ | ✅ | 启动器历史 changelog |
|
||||
| GET | `/api/software` | 🆕 | ✅ | ✅ | 软件列表 |
|
||||
@@ -561,27 +530,15 @@ GET /api/server-time
|
||||
# 1.18 启动器最新版(英文 changelog)
|
||||
curl 'https://nanami.rucky.cn/api/software/latest?info=1&lang=en&wow=1.18'
|
||||
|
||||
# 1.17 启动器最新版
|
||||
curl 'https://nanami.rucky.cn/api/software/latest?info=1&wow=1.17'
|
||||
|
||||
# 1.18 启动器全部历史 changelog(中文)
|
||||
curl 'https://nanami.rucky.cn/api/software/changelog?slug=nanami-launcher&wow=1.18&lang=zh'
|
||||
|
||||
# 1.17 启动器自更新检查(已安装 versionCode=1010)
|
||||
curl 'https://nanami.rucky.cn/api/software/check-update?slug=nanami-launcher&versionCode=1010&wow=1.17'
|
||||
|
||||
# 1.17 客户端的 UI 类插件(英文)
|
||||
curl 'https://nanami.rucky.cn/api/addons?lang=en&category=ui&wow=1.17'
|
||||
|
||||
# 不限 WoW 版本,列出所有 release
|
||||
curl 'https://nanami.rucky.cn/api/releases?wow=all'
|
||||
# 1.18 客户端的 UI 类插件(英文)
|
||||
curl 'https://nanami.rucky.cn/api/addons?lang=en&category=ui'
|
||||
|
||||
# 1.18 启动器友好直链
|
||||
curl -L -o nanami-launcher-1.18.exe 'https://nanami.rucky.cn/download/launcher?wow=1.18'
|
||||
|
||||
# 1.17 启动器友好直链
|
||||
curl -L -o nanami-launcher-1.17.exe 'https://nanami.rucky.cn/download/launcher?wow=1.17'
|
||||
|
||||
# 通过 Accept-Language 协商语言
|
||||
curl -H 'Accept-Language: en-US,en;q=0.9' 'https://nanami.rucky.cn/api/articles'
|
||||
```
|
||||
|
||||
@@ -46,7 +46,7 @@ model Release {
|
||||
filePath String?
|
||||
externalUrl String?
|
||||
gameVersion String @default("")
|
||||
// Turtle WoW client major version this build targets: "1.18" or "1.17"
|
||||
// Turtle WoW client major version this build targets. Only "1.18" is active.
|
||||
wowVersion String @default("1.18")
|
||||
downloadCount Int @default(0)
|
||||
isLatest Boolean @default(false)
|
||||
@@ -95,7 +95,7 @@ model SoftwareVersion {
|
||||
isLatest Boolean @default(false)
|
||||
forceUpdate Boolean @default(false)
|
||||
minVersion String?
|
||||
// Turtle WoW client major version this build targets: "1.18" or "1.17"
|
||||
// Turtle WoW client major version this build targets. Only "1.18" is active.
|
||||
wowVersion String @default("1.18")
|
||||
createdAt DateTime @default(now())
|
||||
software Software @relation(fields: [softwareId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { AddonDetail } from "@/components/public/AddonDetail";
|
||||
import { getServerWowVersion } from "@/lib/get-server-wow";
|
||||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -30,7 +30,7 @@ export default async function AddonDetailPage({
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const wowVersion = await getServerWowVersion();
|
||||
const wowVersion = DEFAULT_WOW_VERSION;
|
||||
const addon = await prisma.addon.findUnique({
|
||||
where: { slug },
|
||||
include: {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { prisma } from "@/lib/db";
|
||||
import { AddonCard } from "@/components/public/AddonCard";
|
||||
import { AddonsCategoryFilter } from "@/components/public/AddonsCategoryFilter";
|
||||
import { T } from "@/components/public/T";
|
||||
import { getServerWowVersion } from "@/lib/get-server-wow";
|
||||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||||
import Link from "next/link";
|
||||
import { Package, Search, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
@@ -24,7 +24,7 @@ export default async function AddonsPage({
|
||||
}) {
|
||||
const { category, search, page: pageStr } = await searchParams;
|
||||
const page = Math.max(1, parseInt(pageStr || "1", 10) || 1);
|
||||
const wowVersion = await getServerWowVersion();
|
||||
const wowVersion = DEFAULT_WOW_VERSION;
|
||||
|
||||
const where: Record<string, unknown> = { published: true };
|
||||
if (category) where.category = category;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { ChangelogTimeline } from "@/components/public/ChangelogTimeline";
|
||||
import { getServerWowVersion } from "@/lib/get-server-wow";
|
||||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -12,7 +12,7 @@ export const metadata = {
|
||||
export const revalidate = 120;
|
||||
|
||||
export default async function ChangelogPage() {
|
||||
const wowVersion = await getServerWowVersion();
|
||||
const wowVersion = DEFAULT_WOW_VERSION;
|
||||
const software = await prisma.software.findUnique({
|
||||
where: { slug: "nanami-launcher" },
|
||||
include: {
|
||||
|
||||
@@ -8,14 +8,14 @@ import { ShutdownBanner } from "@/components/public/ShutdownBanner";
|
||||
import { T } from "@/components/public/T";
|
||||
import { ArticleCard } from "@/components/public/ArticleCard";
|
||||
import { getSiteSettings } from "@/lib/site-settings";
|
||||
import { getServerWowVersion } from "@/lib/get-server-wow";
|
||||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||||
import { Sparkles, Shield, Zap } from "lucide-react";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function HomePage() {
|
||||
const siteSettings = await getSiteSettings();
|
||||
const wowVersion = await getServerWowVersion();
|
||||
const wowVersion = DEFAULT_WOW_VERSION;
|
||||
const [featuredAddons, launcher, launcherDownloads, banners, galleryImages, latestArticles] =
|
||||
await Promise.all([
|
||||
prisma.addon.findMany({
|
||||
|
||||
@@ -3,11 +3,13 @@ import { prisma } from "@/lib/db";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus } from "lucide-react";
|
||||
import { ReleasesTable } from "@/components/admin/ReleasesTable";
|
||||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminReleasesPage() {
|
||||
const releases = await prisma.release.findMany({
|
||||
where: { wowVersion: DEFAULT_WOW_VERSION },
|
||||
include: { addon: { select: { name: true, slug: true } } },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { SoftwareEditForm } from "@/components/admin/SoftwareEditForm";
|
||||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -12,7 +13,12 @@ export default async function EditSoftwarePage({
|
||||
const { id } = await params;
|
||||
const software = await prisma.software.findUnique({
|
||||
where: { id },
|
||||
include: { versions: { orderBy: { versionCode: "desc" } } },
|
||||
include: {
|
||||
versions: {
|
||||
where: { wowVersion: DEFAULT_WOW_VERSION },
|
||||
orderBy: { versionCode: "desc" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!software) notFound();
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { Plus } from "lucide-react";
|
||||
import { SoftwareVersionTable } from "@/components/admin/SoftwareVersionTable";
|
||||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||||
|
||||
const SOFTWARE_DEFS = [
|
||||
{
|
||||
@@ -41,7 +42,7 @@ export default async function AdminSoftwarePage() {
|
||||
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 },
|
||||
where: { softwareId: sw.id, wowVersion: DEFAULT_WOW_VERSION },
|
||||
orderBy: { versionCode: "desc" },
|
||||
});
|
||||
const totalDownloads = versions.reduce((s, v) => s + v.downloadCount, 0);
|
||||
@@ -87,34 +88,21 @@ export default async function AdminSoftwarePage() {
|
||||
</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">
|
||||
<Card className="border-amber-500/20">
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription className="flex items-center gap-2">
|
||||
<span>WoW {wv}</span>
|
||||
<span>WoW {DEFAULT_WOW_VERSION}</span>
|
||||
<span className="text-xs text-muted-foreground/70">
|
||||
({total} 个版本 · {downloads} 次下载)
|
||||
({item.versions.length} 个版本 · {item.totalDownloads} 次下载)
|
||||
</span>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-bold">
|
||||
{latest ? `v${latest.version}` : "未发布"}
|
||||
{item.latestVersion ? `v${item.latestVersion.version}` : "未发布"}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
|
||||
@@ -11,14 +11,12 @@ export async function GET(
|
||||
const { id } = await params;
|
||||
const lang = getApiLang(request);
|
||||
const wowVersion = getApiWowVersion(request);
|
||||
const { searchParams } = new URL(request.url);
|
||||
const wowAll = searchParams.get("wow") === "all";
|
||||
|
||||
const addon = await prisma.addon.findFirst({
|
||||
where: { OR: [{ id }, { slug: id }] },
|
||||
include: {
|
||||
releases: {
|
||||
...(wowAll ? {} : { where: { wowVersion } }),
|
||||
where: { wowVersion },
|
||||
orderBy: { createdAt: "desc" },
|
||||
},
|
||||
screenshots: { orderBy: { sortOrder: "asc" } },
|
||||
@@ -49,7 +47,7 @@ export async function GET(
|
||||
descriptionEn: addon.descriptionEn,
|
||||
releases,
|
||||
lang,
|
||||
wowVersion: wowAll ? "all" : wowVersion,
|
||||
wowVersion,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ export async function GET(request: NextRequest) {
|
||||
const publishedOnly = searchParams.get("published") !== "false";
|
||||
const lang = getApiLang(request);
|
||||
const wowVersion = getApiWowVersion(request);
|
||||
const wowAll = searchParams.get("wow") === "all";
|
||||
|
||||
const where: Record<string, unknown> = {};
|
||||
if (publishedOnly) where.published = true;
|
||||
@@ -31,7 +30,7 @@ export async function GET(request: NextRequest) {
|
||||
releases: {
|
||||
where: {
|
||||
isLatest: true,
|
||||
...(wowAll ? {} : { wowVersion }),
|
||||
wowVersion,
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
@@ -66,7 +65,7 @@ export async function GET(request: NextRequest) {
|
||||
descriptionEn: a.descriptionEn,
|
||||
releases,
|
||||
lang,
|
||||
wowVersion: wowAll ? "all" : wowVersion,
|
||||
wowVersion,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { readFile, stat } from "fs/promises";
|
||||
import path from "path";
|
||||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
@@ -9,8 +10,8 @@ export async function GET(
|
||||
) {
|
||||
const { id } = await params;
|
||||
|
||||
const release = await prisma.release.findUnique({
|
||||
where: { id },
|
||||
const release = await prisma.release.findFirst({
|
||||
where: { id, wowVersion: DEFAULT_WOW_VERSION },
|
||||
include: { addon: { select: { name: true, slug: true } } },
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { isWowVersion } from "@/lib/wow-versions";
|
||||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
@@ -22,7 +22,6 @@ export async function PUT(
|
||||
filePath,
|
||||
externalUrl,
|
||||
gameVersion,
|
||||
wowVersion,
|
||||
isLatest,
|
||||
} = body;
|
||||
|
||||
@@ -31,11 +30,7 @@ export async function PUT(
|
||||
return NextResponse.json({ error: "Release not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Determine the effective wow scope for isLatest demotion.
|
||||
const newWow =
|
||||
wowVersion !== undefined && isWowVersion(wowVersion)
|
||||
? wowVersion
|
||||
: existing.wowVersion;
|
||||
const newWow = DEFAULT_WOW_VERSION;
|
||||
|
||||
if (isLatest === true && !existing.isLatest) {
|
||||
await prisma.release.updateMany({
|
||||
@@ -59,9 +54,7 @@ export async function PUT(
|
||||
...(filePath !== undefined && { filePath }),
|
||||
...(externalUrl !== undefined && { externalUrl }),
|
||||
...(gameVersion !== undefined && { gameVersion }),
|
||||
...(wowVersion !== undefined && isWowVersion(wowVersion)
|
||||
? { wowVersion }
|
||||
: {}),
|
||||
wowVersion: DEFAULT_WOW_VERSION,
|
||||
...(isLatest !== undefined && { isLatest }),
|
||||
},
|
||||
include: { addon: { select: { name: true, slug: true } } },
|
||||
|
||||
@@ -2,22 +2,17 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { getApiLang, pickText } from "@/lib/api-locale";
|
||||
import {
|
||||
DEFAULT_WOW_VERSION,
|
||||
getApiWowVersion,
|
||||
isWowVersion,
|
||||
} from "@/lib/wow-versions";
|
||||
import { DEFAULT_WOW_VERSION, getApiWowVersion } from "@/lib/wow-versions";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const addonId = searchParams.get("addonId");
|
||||
const lang = getApiLang(request);
|
||||
const wowAll = searchParams.get("wow") === "all";
|
||||
const wowVersion = getApiWowVersion(request);
|
||||
|
||||
const where: Record<string, unknown> = {};
|
||||
if (addonId) where.addonId = addonId;
|
||||
if (!wowAll) where.wowVersion = wowVersion;
|
||||
where.wowVersion = wowVersion;
|
||||
|
||||
const releases = await prisma.release.findMany({
|
||||
where,
|
||||
@@ -68,7 +63,6 @@ export async function POST(request: NextRequest) {
|
||||
filePath,
|
||||
externalUrl,
|
||||
gameVersion,
|
||||
wowVersion,
|
||||
} = body;
|
||||
|
||||
if (!addonId || !version) {
|
||||
@@ -85,7 +79,7 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const wow = isWowVersion(wowVersion) ? wowVersion : DEFAULT_WOW_VERSION;
|
||||
const wow = DEFAULT_WOW_VERSION;
|
||||
|
||||
const addon = await prisma.addon.findUnique({ where: { id: addonId } });
|
||||
if (!addon) {
|
||||
|
||||
@@ -11,14 +11,12 @@ export async function GET(
|
||||
const { id } = await params;
|
||||
const lang = getApiLang(request);
|
||||
const wowVersion = getApiWowVersion(request);
|
||||
const { searchParams } = new URL(request.url);
|
||||
const wowAll = searchParams.get("wow") === "all";
|
||||
|
||||
const software = await prisma.software.findFirst({
|
||||
where: { OR: [{ id }, { slug: id }] },
|
||||
include: {
|
||||
versions: {
|
||||
...(wowAll ? {} : { where: { wowVersion } }),
|
||||
where: { wowVersion },
|
||||
orderBy: { versionCode: "desc" },
|
||||
},
|
||||
},
|
||||
@@ -43,7 +41,7 @@ export async function GET(
|
||||
changelogEn: v.changelogEn,
|
||||
})),
|
||||
lang,
|
||||
wowVersion: wowAll ? "all" : wowVersion,
|
||||
wowVersion,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { DEFAULT_WOW_VERSION, isWowVersion } from "@/lib/wow-versions";
|
||||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
@@ -25,7 +25,6 @@ export async function POST(
|
||||
fileSize,
|
||||
forceUpdate,
|
||||
minVersion,
|
||||
wowVersion,
|
||||
} = body;
|
||||
|
||||
if (!version || !versionCode) {
|
||||
@@ -35,7 +34,7 @@ export async function POST(
|
||||
);
|
||||
}
|
||||
|
||||
const wow = isWowVersion(wowVersion) ? wowVersion : DEFAULT_WOW_VERSION;
|
||||
const wow = DEFAULT_WOW_VERSION;
|
||||
|
||||
const software = await prisma.software.findUnique({
|
||||
where: { id: softwareId },
|
||||
|
||||
@@ -8,8 +8,6 @@ export async function GET(request: NextRequest) {
|
||||
const slug = searchParams.get("slug");
|
||||
const lang = getApiLang(request);
|
||||
const wowVersion = getApiWowVersion(request);
|
||||
// Pass `?wow=all` to disable filtering (returns every version)
|
||||
const wowFilter = searchParams.get("wow") === "all" ? undefined : wowVersion;
|
||||
|
||||
if (!slug) {
|
||||
return NextResponse.json(
|
||||
@@ -22,7 +20,7 @@ export async function GET(request: NextRequest) {
|
||||
where: { slug },
|
||||
include: {
|
||||
versions: {
|
||||
...(wowFilter ? { where: { wowVersion: wowFilter } } : {}),
|
||||
where: { wowVersion },
|
||||
orderBy: { versionCode: "desc" },
|
||||
select: {
|
||||
version: true,
|
||||
@@ -52,7 +50,7 @@ export async function GET(request: NextRequest) {
|
||||
nameEn: software.nameEn,
|
||||
slug: software.slug,
|
||||
lang,
|
||||
wowVersion: wowFilter ?? "all",
|
||||
wowVersion,
|
||||
versions: software.versions.map((v) => ({
|
||||
version: v.version,
|
||||
versionCode: v.versionCode,
|
||||
|
||||
@@ -8,10 +8,8 @@ export async function GET(request: NextRequest) {
|
||||
const slug = searchParams.get("slug");
|
||||
const currentVersionCode = searchParams.get("versionCode");
|
||||
const lang = getApiLang(request);
|
||||
// Self-update endpoint for launchers: ignore cookies. The launcher must
|
||||
// declare which wow channel it speaks for via ?wow=, otherwise we serve the
|
||||
// canonical 1.18 channel. This prevents a 1.18 launcher from accidentally
|
||||
// pulling 1.17 updates (or vice versa) just because of stale cookies.
|
||||
// Self-update endpoint for launchers: ignore cookies and always serve the
|
||||
// canonical 1.18 channel.
|
||||
const wowVersion = getDownloadWowVersion(request);
|
||||
|
||||
if (!slug) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { readFile, stat } from "fs/promises";
|
||||
import path from "path";
|
||||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
@@ -10,8 +11,8 @@ export async function GET(
|
||||
const { id } = await params;
|
||||
const source = new URL(request.url).searchParams.get("source");
|
||||
|
||||
const sv = await prisma.softwareVersion.findUnique({
|
||||
where: { id },
|
||||
const sv = await prisma.softwareVersion.findFirst({
|
||||
where: { id, wowVersion: DEFAULT_WOW_VERSION },
|
||||
include: { software: { select: { slug: true } } },
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { isWowVersion } from "@/lib/wow-versions";
|
||||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
@@ -22,12 +22,7 @@ export async function PUT(
|
||||
return NextResponse.json({ error: "Version not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Resolve the wow scope after this update applies. If the wow tag is being
|
||||
// changed, the new tag governs which siblings get demoted.
|
||||
const newWow =
|
||||
body.wowVersion !== undefined && isWowVersion(body.wowVersion)
|
||||
? body.wowVersion
|
||||
: existing.wowVersion;
|
||||
const newWow = DEFAULT_WOW_VERSION;
|
||||
|
||||
const setLatest = body.isLatest === true && !existing.isLatest;
|
||||
if (setLatest) {
|
||||
@@ -65,9 +60,7 @@ export async function PUT(
|
||||
minVersion: body.minVersion || null,
|
||||
}),
|
||||
...(body.isLatest !== undefined && { isLatest: body.isLatest }),
|
||||
...(body.wowVersion !== undefined && isWowVersion(body.wowVersion)
|
||||
? { wowVersion: body.wowVersion }
|
||||
: {}),
|
||||
wowVersion: DEFAULT_WOW_VERSION,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@ import type { Metadata } from "next";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { ThemeProvider } from "@/components/ThemeProvider";
|
||||
import { LocaleProvider } from "@/i18n/LocaleProvider";
|
||||
import { WowVersionProvider } from "@/i18n/WowVersionProvider";
|
||||
import { getServerLocale } from "@/i18n/getLocale";
|
||||
import { getServerWowVersion } from "@/lib/get-server-wow";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -31,20 +29,15 @@ export default async function RootLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const [locale, wowVersion] = await Promise.all([
|
||||
getServerLocale(),
|
||||
getServerWowVersion(),
|
||||
]);
|
||||
const locale = await getServerLocale();
|
||||
return (
|
||||
<html lang={locale === "en" ? "en" : "zh-CN"} suppressHydrationWarning>
|
||||
<body className="antialiased">
|
||||
<LocaleProvider initialLocale={locale}>
|
||||
<WowVersionProvider initial={wowVersion}>
|
||||
<ThemeProvider>
|
||||
{children}
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</WowVersionProvider>
|
||||
</LocaleProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { toast } from "sonner";
|
||||
import { Upload } from "lucide-react";
|
||||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||||
|
||||
interface ReleaseFormProps {
|
||||
addons: { id: string; name: string }[];
|
||||
@@ -25,7 +26,6 @@ export function ReleaseForm({ addons }: ReleaseFormProps) {
|
||||
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 [uploading, setUploading] = useState(false);
|
||||
const [selectedAddonId, setSelectedAddonId] = useState("");
|
||||
@@ -63,7 +63,7 @@ export function ReleaseForm({ addons }: ReleaseFormProps) {
|
||||
version: formData.get("version"),
|
||||
changelog: formData.get("changelog"),
|
||||
changelogEn: formData.get("changelogEn") || "",
|
||||
wowVersion,
|
||||
wowVersion: DEFAULT_WOW_VERSION,
|
||||
downloadType,
|
||||
filePath: downloadType === "local" ? uploadedFilePath : null,
|
||||
externalUrl:
|
||||
@@ -153,28 +153,6 @@ export function ReleaseForm({ addons }: ReleaseFormProps) {
|
||||
placeholder="例如 1.18.1"
|
||||
/>
|
||||
</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="grid gap-4 md:grid-cols-2">
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { toast } from "sonner";
|
||||
import { Download, Pencil, Trash2, Check, X, Upload, Star } from "lucide-react";
|
||||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||||
|
||||
interface Release {
|
||||
id: string;
|
||||
@@ -70,7 +71,6 @@ export function ReleasesTable({ releases: initial }: { releases: Release[] }) {
|
||||
<TableRow>
|
||||
<TableHead>插件</TableHead>
|
||||
<TableHead>版本</TableHead>
|
||||
<TableHead>WoW</TableHead>
|
||||
<TableHead>游戏版本</TableHead>
|
||||
<TableHead>下载方式</TableHead>
|
||||
<TableHead>下载量</TableHead>
|
||||
@@ -82,7 +82,7 @@ export function ReleasesTable({ releases: initial }: { releases: Release[] }) {
|
||||
<TableBody>
|
||||
{initial.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="text-center text-muted-foreground">
|
||||
<TableCell colSpan={8} className="text-center text-muted-foreground">
|
||||
暂无版本发布
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -91,11 +91,6 @@ export function ReleasesTable({ releases: initial }: { releases: Release[] }) {
|
||||
<TableRow key={r.id}>
|
||||
<TableCell className="font-medium">{r.addon.name}</TableCell>
|
||||
<TableCell>v{r.version}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="border-amber-500/40 text-amber-300/90">
|
||||
{r.wowVersion}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{r.gameVersion || "-"}
|
||||
</TableCell>
|
||||
@@ -182,9 +177,6 @@ function ReleaseEditPanel({
|
||||
const [version, setVersion] = useState(r.version);
|
||||
const [changelog, setChangelog] = useState(r.changelog);
|
||||
const [changelogEn, setChangelogEn] = useState(r.changelogEn || "");
|
||||
const [wowVersion, setWowVersion] = useState<"1.18" | "1.17">(
|
||||
(r.wowVersion as "1.18" | "1.17") || "1.18"
|
||||
);
|
||||
const [gameVersion, setGameVersion] = useState(r.gameVersion);
|
||||
const [downloadType, setDownloadType] = useState(r.downloadType);
|
||||
const [externalUrl, setExternalUrl] = useState(r.externalUrl || "");
|
||||
@@ -228,7 +220,7 @@ function ReleaseEditPanel({
|
||||
version,
|
||||
changelog,
|
||||
changelogEn,
|
||||
wowVersion,
|
||||
wowVersion: DEFAULT_WOW_VERSION,
|
||||
gameVersion,
|
||||
downloadType,
|
||||
filePath: downloadType === "local" ? filePath : null,
|
||||
@@ -258,7 +250,7 @@ function ReleaseEditPanel({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>版本号</Label>
|
||||
<Input value={version} onChange={(e) => setVersion(e.target.value)} />
|
||||
@@ -271,25 +263,6 @@ function ReleaseEditPanel({
|
||||
placeholder="1.18.1"
|
||||
/>
|
||||
</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((wv) => (
|
||||
<button
|
||||
key={wv}
|
||||
type="button"
|
||||
onClick={() => setWowVersion(wv)}
|
||||
className={`flex-1 rounded px-3 py-1.5 text-sm font-medium transition-colors ${
|
||||
wv === wowVersion
|
||||
? "bg-background shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{wv}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { toast } from "sonner";
|
||||
import { Pencil, Trash2, Upload, X, Check } from "lucide-react";
|
||||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||||
|
||||
interface SoftwareVersion {
|
||||
id: string;
|
||||
@@ -143,9 +144,6 @@ function VersionItem({ version: v }: { version: SoftwareVersion }) {
|
||||
const [versionCode, setVersionCode] = useState(v.versionCode.toString());
|
||||
const [changelog, setChangelog] = useState(v.changelog);
|
||||
const [changelogEn, setChangelogEn] = useState(v.changelogEn || "");
|
||||
const [wowVersion, setWowVersion] = useState<"1.18" | "1.17">(
|
||||
(v.wowVersion as "1.18" | "1.17") || "1.18"
|
||||
);
|
||||
const [downloadType, setDownloadType] = useState(v.downloadType);
|
||||
const [externalUrl, setExternalUrl] = useState(v.externalUrl || "");
|
||||
const [forceUpdate, setForceUpdate] = useState(v.forceUpdate);
|
||||
@@ -192,7 +190,7 @@ function VersionItem({ version: v }: { version: SoftwareVersion }) {
|
||||
versionCode: Number(versionCode),
|
||||
changelog,
|
||||
changelogEn,
|
||||
wowVersion,
|
||||
wowVersion: DEFAULT_WOW_VERSION,
|
||||
downloadType,
|
||||
filePath: downloadType === "local" ? filePath : null,
|
||||
externalUrl: downloadType === "url" ? externalUrl : null,
|
||||
@@ -241,7 +239,7 @@ function VersionItem({ version: v }: { version: SoftwareVersion }) {
|
||||
(code: {v.versionCode})
|
||||
</span>
|
||||
<Badge variant="outline" className="border-amber-500/40 text-amber-300/90 text-xs">
|
||||
WoW {v.wowVersion}
|
||||
WoW {DEFAULT_WOW_VERSION}
|
||||
</Badge>
|
||||
{v.isLatest && <Badge>最新</Badge>}
|
||||
{v.forceUpdate && (
|
||||
@@ -327,25 +325,6 @@ function VersionItem({ version: v }: { version: SoftwareVersion }) {
|
||||
placeholder="可选"
|
||||
/>
|
||||
</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((wv) => (
|
||||
<button
|
||||
key={wv}
|
||||
type="button"
|
||||
onClick={() => setWowVersion(wv)}
|
||||
className={`flex-1 rounded px-3 py-1.5 text-sm font-medium transition-colors ${
|
||||
wv === wowVersion
|
||||
? "bg-background shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{wv}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
|
||||
@@ -9,12 +9,12 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { toast } from "sonner";
|
||||
import { Upload } from "lucide-react";
|
||||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||||
|
||||
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);
|
||||
@@ -65,7 +65,7 @@ export function SoftwareVersionForm({ softwareId }: { softwareId: string }) {
|
||||
fileSize,
|
||||
forceUpdate: fd.get("forceUpdate") === "on",
|
||||
minVersion: fd.get("minVersion") || null,
|
||||
wowVersion,
|
||||
wowVersion: DEFAULT_WOW_VERSION,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -85,7 +85,7 @@ export function SoftwareVersionForm({ softwareId }: { softwareId: string }) {
|
||||
<CardHeader><CardTitle>版本信息</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="version">版本号 *</Label>
|
||||
<Input id="version" name="version" required placeholder="1.0.0" />
|
||||
@@ -94,28 +94,6 @@ export function SoftwareVersionForm({ softwareId }: { softwareId: string }) {
|
||||
<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">
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
Upload,
|
||||
Star,
|
||||
} from "lucide-react";
|
||||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||||
|
||||
interface Version {
|
||||
id: string;
|
||||
@@ -86,7 +87,6 @@ export function SoftwareVersionTable({
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>版本</TableHead>
|
||||
<TableHead>WoW</TableHead>
|
||||
<TableHead>版本代码</TableHead>
|
||||
<TableHead>下载方式</TableHead>
|
||||
<TableHead>直接下载</TableHead>
|
||||
@@ -100,7 +100,7 @@ export function SoftwareVersionTable({
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={10}
|
||||
colSpan={9}
|
||||
className="text-center text-muted-foreground"
|
||||
>
|
||||
暂无版本,点击上方按钮发布第一个版本
|
||||
@@ -117,7 +117,6 @@ export function SoftwareVersionTable({
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>版本</TableHead>
|
||||
<TableHead>WoW</TableHead>
|
||||
<TableHead>版本代码</TableHead>
|
||||
<TableHead>下载方式</TableHead>
|
||||
<TableHead>直接下载</TableHead>
|
||||
@@ -132,11 +131,6 @@ export function SoftwareVersionTable({
|
||||
{initialVersions.map((v) => (
|
||||
<TableRow key={v.id}>
|
||||
<TableCell className="font-medium">v{v.version}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="border-amber-500/40 text-amber-300/90">
|
||||
{v.wowVersion}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{v.versionCode}
|
||||
</TableCell>
|
||||
@@ -234,9 +228,6 @@ function VersionEditPanel({
|
||||
const [versionCode, setVersionCode] = useState(v.versionCode.toString());
|
||||
const [changelog, setChangelog] = useState(v.changelog);
|
||||
const [changelogEn, setChangelogEn] = useState(v.changelogEn || "");
|
||||
const [wowVersion, setWowVersion] = useState<"1.18" | "1.17">(
|
||||
(v.wowVersion as "1.18" | "1.17") || "1.18"
|
||||
);
|
||||
const [downloadType, setDownloadType] = useState(v.downloadType);
|
||||
const [externalUrl, setExternalUrl] = useState(v.externalUrl || "");
|
||||
const [forceUpdate, setForceUpdate] = useState(v.forceUpdate);
|
||||
@@ -284,7 +275,7 @@ function VersionEditPanel({
|
||||
versionCode: Number(versionCode),
|
||||
changelog,
|
||||
changelogEn,
|
||||
wowVersion,
|
||||
wowVersion: DEFAULT_WOW_VERSION,
|
||||
downloadType,
|
||||
filePath: downloadType === "local" ? filePath : null,
|
||||
externalUrl: downloadType === "url" ? externalUrl : null,
|
||||
@@ -339,25 +330,6 @@ function VersionEditPanel({
|
||||
placeholder="可选"
|
||||
/>
|
||||
</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((wv) => (
|
||||
<button
|
||||
key={wv}
|
||||
type="button"
|
||||
onClick={() => setWowVersion(wv)}
|
||||
className={`flex-1 rounded px-3 py-1.5 text-sm font-medium transition-colors ${
|
||||
wv === wowVersion
|
||||
? "bg-background shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{wv}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import Link from "next/link";
|
||||
import { Package, Download, FileText, Clock } from "lucide-react";
|
||||
import { useLocale } from "@/i18n/LocaleProvider";
|
||||
import { useWowVersion } from "@/i18n/WowVersionProvider";
|
||||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||||
|
||||
export function Footer() {
|
||||
const { t } = useLocale();
|
||||
const { wowVersion } = useWowVersion();
|
||||
const wowVersion = DEFAULT_WOW_VERSION;
|
||||
|
||||
const quickLinks = [
|
||||
{ href: "/addons", label: t("footer", "addons"), icon: Package },
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { Download, ChevronRight } from "lucide-react";
|
||||
import { useLocale } from "@/i18n/LocaleProvider";
|
||||
import { useWowVersion } from "@/i18n/WowVersionProvider";
|
||||
import { DEFAULT_WOW_VERSION } from "@/lib/wow-versions";
|
||||
|
||||
interface Particle {
|
||||
x: number;
|
||||
@@ -48,7 +48,7 @@ export function HeroBanner({
|
||||
banners?: { imageUrl: string }[];
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
const { wowVersion } = useWowVersion();
|
||||
const wowVersion = DEFAULT_WOW_VERSION;
|
||||
const slides =
|
||||
banners && banners.length > 0
|
||||
? banners.map((b) => ({ image: b.imageUrl }))
|
||||
|
||||
@@ -6,7 +6,6 @@ import { usePathname } from "next/navigation";
|
||||
import { Package, Menu, X } from "lucide-react";
|
||||
import { useLocale } from "@/i18n/LocaleProvider";
|
||||
import { LanguageSwitcher } from "./LanguageSwitcher";
|
||||
import { WowVersionSwitcher } from "./WowVersionSwitcher";
|
||||
|
||||
export function Navbar() {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -51,7 +50,6 @@ export function Navbar() {
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
<WowVersionSwitcher />
|
||||
<LanguageSwitcher />
|
||||
</nav>
|
||||
|
||||
@@ -86,7 +84,6 @@ export function Navbar() {
|
||||
);
|
||||
})}
|
||||
<div className="mt-1 border-t border-amber-900/15 pt-2">
|
||||
<WowVersionSwitcher variant="mobile" />
|
||||
<LanguageSwitcher variant="mobile" />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Gamepad2 } from "lucide-react";
|
||||
import { useWowVersion } from "@/i18n/WowVersionProvider";
|
||||
|
||||
export function WowVersionSwitcher({
|
||||
variant = "navbar",
|
||||
}: {
|
||||
variant?: "navbar" | "mobile";
|
||||
}) {
|
||||
const { wowVersion, setWowVersion, versions } = useWowVersion();
|
||||
|
||||
if (variant === "mobile") {
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-lg px-3 py-2.5 text-sm text-gray-400">
|
||||
<Gamepad2 className="h-4 w-4" />
|
||||
<span>WoW</span>
|
||||
<div className="ml-auto inline-flex rounded border border-amber-500/20 bg-black/20 p-0.5 text-xs">
|
||||
{versions.map((v) => (
|
||||
<button
|
||||
key={v}
|
||||
type="button"
|
||||
onClick={() => setWowVersion(v)}
|
||||
className={`rounded px-2 py-0.5 transition-colors ${
|
||||
v === wowVersion
|
||||
? "bg-amber-500/30 text-amber-100"
|
||||
: "text-gray-400 hover:text-amber-200"
|
||||
}`}
|
||||
>
|
||||
{v}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-label="WoW client version"
|
||||
title={`WoW ${wowVersion}`}
|
||||
className="inline-flex h-7 items-center gap-0.5 rounded-md border border-amber-500/20 bg-black/20 p-0.5 text-xs"
|
||||
>
|
||||
<Gamepad2 className="ml-1 h-3.5 w-3.5 text-amber-300/70" />
|
||||
{versions.map((v) => (
|
||||
<button
|
||||
key={v}
|
||||
type="button"
|
||||
onClick={() => setWowVersion(v)}
|
||||
className={`rounded px-1.5 py-0.5 font-medium tracking-tight transition-colors ${
|
||||
v === wowVersion
|
||||
? "bg-amber-500/30 text-amber-100"
|
||||
: "text-amber-200/60 hover:text-amber-100"
|
||||
}`}
|
||||
>
|
||||
{v}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import {
|
||||
WOW_COOKIE,
|
||||
WOW_VERSIONS,
|
||||
type WowVersion,
|
||||
} from "@/lib/wow-versions";
|
||||
|
||||
interface WowVersionContextValue {
|
||||
wowVersion: WowVersion;
|
||||
setWowVersion: (next: WowVersion) => void;
|
||||
versions: readonly WowVersion[];
|
||||
}
|
||||
|
||||
const WowVersionContext = createContext<WowVersionContextValue | null>(null);
|
||||
|
||||
export function WowVersionProvider({
|
||||
initial,
|
||||
children,
|
||||
}: {
|
||||
initial: WowVersion;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [wowVersion, setLocal] = useState<WowVersion>(initial);
|
||||
|
||||
const setWowVersion = useCallback((next: WowVersion) => {
|
||||
setLocal(next);
|
||||
if (typeof document !== "undefined") {
|
||||
document.cookie = `${WOW_COOKIE}=${next}; path=/; max-age=${
|
||||
60 * 60 * 24 * 365
|
||||
}; samesite=lax`;
|
||||
// Server components rely on the cookie — reload so the SSR'd lists refresh.
|
||||
// Use a microtask so React state has a chance to commit first.
|
||||
setTimeout(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.reload();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const value = useMemo<WowVersionContextValue>(
|
||||
() => ({ wowVersion, setWowVersion, versions: WOW_VERSIONS }),
|
||||
[wowVersion, setWowVersion]
|
||||
);
|
||||
|
||||
return (
|
||||
<WowVersionContext.Provider value={value}>
|
||||
{children}
|
||||
</WowVersionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useWowVersion() {
|
||||
const ctx = useContext(WowVersionContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useWowVersion must be used inside <WowVersionProvider>");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { cookies } from "next/headers";
|
||||
import {
|
||||
DEFAULT_WOW_VERSION,
|
||||
WOW_COOKIE,
|
||||
isWowVersion,
|
||||
type WowVersion,
|
||||
} from "./wow-versions";
|
||||
|
||||
/**
|
||||
* Resolve the wow version from the request cookie on the server.
|
||||
* Used by server components to filter DB queries before render.
|
||||
*/
|
||||
export async function getServerWowVersion(): Promise<WowVersion> {
|
||||
const store = await cookies();
|
||||
const v = store.get(WOW_COOKIE)?.value;
|
||||
if (isWowVersion(v)) return v;
|
||||
return DEFAULT_WOW_VERSION;
|
||||
}
|
||||
@@ -1,70 +1,37 @@
|
||||
/**
|
||||
* Turtle WoW client major-version constants and request resolvers.
|
||||
*
|
||||
* The platform tracks separate addon / launcher builds for each supported
|
||||
* client version. Each Release / SoftwareVersion is tagged with one of
|
||||
* these strings, and APIs accept `?wow=` to filter.
|
||||
* The public site, admin console, and download APIs currently expose only the
|
||||
* 1.18 channel. Historical rows may still carry a wowVersion tag in the
|
||||
* database, but request resolvers intentionally collapse every request to 1.18.
|
||||
*/
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
export const WOW_VERSIONS = ["1.18", "1.17"] as const;
|
||||
export const WOW_VERSIONS = ["1.18"] as const;
|
||||
export type WowVersion = (typeof WOW_VERSIONS)[number];
|
||||
|
||||
export const DEFAULT_WOW_VERSION: WowVersion = "1.18";
|
||||
export const WOW_COOKIE = "wow";
|
||||
|
||||
export function isWowVersion(v: unknown): v is WowVersion {
|
||||
return typeof v === "string" && (WOW_VERSIONS as readonly string[]).includes(v);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the requested wow version on a public listing/browsing API.
|
||||
*
|
||||
* 1. ?wow=1.18|1.17
|
||||
* 2. ?wowVersion=1.18|1.17 (alias)
|
||||
* 3. cookie `wow`
|
||||
* 4. Default DEFAULT_WOW_VERSION
|
||||
*
|
||||
* Used for endpoints whose response shape depends on the user's currently
|
||||
* selected wow channel (addons list, releases list, etc.). The cookie lets a
|
||||
* browser session display the right list across navigations.
|
||||
* Resolve the requested wow version on a public listing/browsing API. Only
|
||||
* the canonical 1.18 channel is supported, so explicit legacy values are
|
||||
* ignored instead of changing the query scope.
|
||||
*/
|
||||
export function getApiWowVersion(request: NextRequest | URL): WowVersion {
|
||||
const url = request instanceof URL ? request : new URL(request.url);
|
||||
const explicit =
|
||||
url.searchParams.get("wow") || url.searchParams.get("wowVersion");
|
||||
if (isWowVersion(explicit)) return explicit;
|
||||
|
||||
if (!(request instanceof URL)) {
|
||||
const cookieHeader = request.headers.get("cookie") || "";
|
||||
const m = cookieHeader.match(/(?:^|;\s*)wow=([^;]+)/);
|
||||
if (m && isWowVersion(m[1])) return m[1] as WowVersion;
|
||||
}
|
||||
|
||||
export function getApiWowVersion(_request: NextRequest | URL): WowVersion {
|
||||
return DEFAULT_WOW_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the wow version for **download endpoints**.
|
||||
*
|
||||
* 1. ?wow=1.18|1.17 (explicit only)
|
||||
* 2. ?wowVersion=1.18|1.17 (alias)
|
||||
* 3. Default DEFAULT_WOW_VERSION
|
||||
*
|
||||
* Cookies are deliberately **NOT** consulted — download URLs must be fully
|
||||
* determined by the URL itself so that:
|
||||
*
|
||||
* - third-party links (`/download/launcher`) always serve the canonical 1.18
|
||||
* build regardless of the visitor's previous browsing state, and
|
||||
* - which binary you get is never tied to language or any other cookie state.
|
||||
* Resolve the wow version for download endpoints. Download URLs are now pinned
|
||||
* to 1.18 regardless of query params, cookies, or historical client channels.
|
||||
*/
|
||||
export function getDownloadWowVersion(
|
||||
request: NextRequest | URL
|
||||
_request: NextRequest | URL
|
||||
): WowVersion {
|
||||
const url = request instanceof URL ? request : new URL(request.url);
|
||||
const explicit =
|
||||
url.searchParams.get("wow") || url.searchParams.get("wowVersion");
|
||||
if (isWowVersion(explicit)) return explicit;
|
||||
return DEFAULT_WOW_VERSION;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user