2026-07-11 12:13:20 +08:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
2026-07-16 20:43:00 +08:00
|
|
|
|
import { useState, useCallback, useMemo, useEffect, useRef } from "react";
|
2026-07-11 12:13:20 +08:00
|
|
|
|
import {
|
|
|
|
|
|
Modal,
|
|
|
|
|
|
Steps,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Space,
|
|
|
|
|
|
Table,
|
|
|
|
|
|
Select,
|
|
|
|
|
|
Row,
|
|
|
|
|
|
Col,
|
2026-07-16 15:22:06 +08:00
|
|
|
|
Flex,
|
2026-07-11 12:13:20 +08:00
|
|
|
|
Progress,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
Empty,
|
2026-07-16 20:26:13 +08:00
|
|
|
|
Tag,
|
2026-07-17 19:15:41 +08:00
|
|
|
|
Alert,
|
|
|
|
|
|
App,
|
2026-07-16 14:34:56 +08:00
|
|
|
|
theme,
|
2026-07-11 12:13:20 +08:00
|
|
|
|
} from "antd";
|
|
|
|
|
|
import { ReloadOutlined } from "@ant-design/icons";
|
2026-07-17 19:15:41 +08:00
|
|
|
|
import { useMutation } from "@tanstack/react-query";
|
|
|
|
|
|
import { useSubscription } from "@trpc/tanstack-react-query";
|
2026-07-18 10:04:16 +08:00
|
|
|
|
import type { BaseVersion, Config, CurseForgeFile, UploadReleaseType, Version } from "@/types";
|
2026-07-11 12:13:20 +08:00
|
|
|
|
import type { ParsedProject } from "@/services/project";
|
2026-07-13 09:29:00 +08:00
|
|
|
|
import { platforms } from "@/services/publish.schemas";
|
2026-07-17 19:15:41 +08:00
|
|
|
|
import type { PlatformResult, ProgressEvent } from "@/services/publish.schemas";
|
2026-07-16 16:05:13 +08:00
|
|
|
|
import { Template } from "@/lib/utils/template";
|
|
|
|
|
|
import type { McVersionEntry } from "@/lib/utils/mcVersion";
|
2026-07-16 20:26:13 +08:00
|
|
|
|
import { computeVersionRanges } from "@/lib/utils/mcVersion";
|
2026-07-18 10:04:16 +08:00
|
|
|
|
import { SemVer } from "@/lib/utils/templates/semver";
|
2026-07-16 20:26:13 +08:00
|
|
|
|
import { getModrinthMcVersions, getCurseForgeMcVersions } from "@/services/meta";
|
2026-07-18 10:04:16 +08:00
|
|
|
|
import { compareVersions } from "@/services/versions";
|
|
|
|
|
|
import { errorMessage } from "@/lib/utils/error";
|
|
|
|
|
|
import { compareParsed, parseExisting } from "@/lib/utils/existingVersion";
|
2026-07-17 19:15:41 +08:00
|
|
|
|
import { trpc } from "@/lib/trpc";
|
2026-07-17 16:09:03 +08:00
|
|
|
|
import { ChangelogEditor } from "./ChangelogEditor";
|
2026-07-11 12:13:20 +08:00
|
|
|
|
|
|
|
|
|
|
const { Text } = Typography;
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Types
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
interface PublishVersion {
|
|
|
|
|
|
key: string;
|
|
|
|
|
|
mc_version: string;
|
|
|
|
|
|
artifact: string | null;
|
|
|
|
|
|
sources: string | null;
|
2026-07-16 20:26:13 +08:00
|
|
|
|
mrRange: string;
|
|
|
|
|
|
cfRange: string;
|
2026-07-16 16:05:13 +08:00
|
|
|
|
modrinthVersion: string;
|
|
|
|
|
|
modrinthVersionName: string;
|
2026-07-11 12:13:20 +08:00
|
|
|
|
existsModrinth: boolean;
|
|
|
|
|
|
existsCurseforge: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-18 10:04:16 +08:00
|
|
|
|
/** Modrinth 表格行:待发布(kind="pending")+ 平台已有版本(kind="existing") */
|
|
|
|
|
|
interface ModrinthTableRow {
|
|
|
|
|
|
key: string;
|
|
|
|
|
|
kind: "pending" | "existing";
|
|
|
|
|
|
pending: PublishVersion | null;
|
|
|
|
|
|
modrinthVersion: string;
|
|
|
|
|
|
mrRange: string;
|
|
|
|
|
|
modrinthVersionName: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** CurseForge 表格行 */
|
|
|
|
|
|
interface CurseforgeTableRow {
|
|
|
|
|
|
key: string;
|
|
|
|
|
|
kind: "pending" | "existing";
|
|
|
|
|
|
pending: PublishVersion | null;
|
|
|
|
|
|
mc_version: string;
|
|
|
|
|
|
cfRange: string;
|
|
|
|
|
|
artifact: string | null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 12:13:20 +08:00
|
|
|
|
interface PublishModalProps {
|
|
|
|
|
|
open: boolean;
|
2026-07-17 19:15:41 +08:00
|
|
|
|
/** 配置文件名(configs/ 下的 .json 文件名) */
|
|
|
|
|
|
configFile: string;
|
2026-07-11 12:13:20 +08:00
|
|
|
|
config: Config;
|
|
|
|
|
|
project: ParsedProject;
|
2026-07-16 20:26:13 +08:00
|
|
|
|
lastVersionEnd: string | null;
|
2026-07-11 12:13:20 +08:00
|
|
|
|
onClose: () => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 09:29:00 +08:00
|
|
|
|
type ReleaseType = Extract<BaseVersion["version_type"], UploadReleaseType>;
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Helpers
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
2026-07-16 20:26:13 +08:00
|
|
|
|
function entriesToMap(
|
|
|
|
|
|
entries: McVersionEntry[] | null,
|
|
|
|
|
|
field: "explicit" | "wildcard",
|
|
|
|
|
|
): Map<string, string> {
|
|
|
|
|
|
const map = new Map<string, string>();
|
|
|
|
|
|
if (entries) {
|
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
|
map.set(entry.version, entry[field]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return map;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 09:29:00 +08:00
|
|
|
|
const RELEASE_TYPE_LABEL_MAP: Record<ReleaseType, string> = {
|
|
|
|
|
|
release: "Release",
|
|
|
|
|
|
beta: "Beta",
|
|
|
|
|
|
alpha: "Alpha"
|
|
|
|
|
|
};
|
|
|
|
|
|
const RELEASE_TYPES = Object.entries(RELEASE_TYPE_LABEL_MAP).map(v => ({ value: v[0], label: v[1] }));
|
|
|
|
|
|
|
2026-07-11 12:13:20 +08:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Component
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
export function PublishModal({
|
|
|
|
|
|
open,
|
2026-07-17 19:15:41 +08:00
|
|
|
|
configFile,
|
2026-07-11 12:13:20 +08:00
|
|
|
|
config,
|
|
|
|
|
|
project,
|
2026-07-16 20:26:13 +08:00
|
|
|
|
lastVersionEnd,
|
2026-07-11 12:13:20 +08:00
|
|
|
|
onClose,
|
|
|
|
|
|
}: PublishModalProps) {
|
2026-07-16 14:34:56 +08:00
|
|
|
|
const { token } = theme.useToken();
|
2026-07-17 19:15:41 +08:00
|
|
|
|
const { message } = App.useApp();
|
2026-07-11 12:13:20 +08:00
|
|
|
|
const [currentStep, setCurrentStep] = useState(0);
|
|
|
|
|
|
|
|
|
|
|
|
// Version selection
|
2026-07-16 20:43:00 +08:00
|
|
|
|
const [selectedModrinth, setSelectedModrinth] = useState<string[]>([]);
|
|
|
|
|
|
const [selectedCurseforge, setSelectedCurseforge] = useState<string[]>([]);
|
2026-07-11 12:13:20 +08:00
|
|
|
|
|
|
|
|
|
|
// Publish settings
|
|
|
|
|
|
const [changelog, setChangelog] = useState("");
|
2026-07-13 09:29:00 +08:00
|
|
|
|
const [versionType, setVersionType] = useState<ReleaseType>(
|
2026-07-11 12:13:20 +08:00
|
|
|
|
"release",
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-07-16 14:34:56 +08:00
|
|
|
|
// Progress & result
|
2026-07-11 12:13:20 +08:00
|
|
|
|
const [publishing, setPublishing] = useState(false);
|
2026-07-16 14:34:56 +08:00
|
|
|
|
const [publishDone, setPublishDone] = useState(false);
|
2026-07-13 09:29:00 +08:00
|
|
|
|
const [progress, setProgress] = useState<
|
2026-07-17 19:15:41 +08:00
|
|
|
|
Record<
|
|
|
|
|
|
typeof platforms[number],
|
|
|
|
|
|
{ done: number; total: number; status: ProgressEvent["status"] }
|
|
|
|
|
|
>
|
2026-07-13 09:29:00 +08:00
|
|
|
|
>({
|
2026-07-17 19:15:41 +08:00
|
|
|
|
modrinth: { done: 0, total: 0, status: "running" },
|
|
|
|
|
|
curseforge: { done: 0, total: 0, status: "running" },
|
2026-07-13 09:29:00 +08:00
|
|
|
|
});
|
2026-07-16 14:34:56 +08:00
|
|
|
|
const [publishResult, setPublishResult] = useState<{
|
|
|
|
|
|
success: number;
|
|
|
|
|
|
failed: number;
|
2026-07-17 19:15:41 +08:00
|
|
|
|
errors: Record<typeof platforms[number], string[]>;
|
2026-07-16 14:34:56 +08:00
|
|
|
|
} | null>(null);
|
2026-07-11 12:13:20 +08:00
|
|
|
|
|
2026-07-16 20:26:13 +08:00
|
|
|
|
// Platform MC versions — loaded when modal opens
|
2026-07-11 12:13:20 +08:00
|
|
|
|
const [loadingExisting, setLoadingExisting] = useState(false);
|
2026-07-16 20:26:13 +08:00
|
|
|
|
const [modrinthMcVersions, setModrinthMcVersions] = useState<string[]>([]);
|
|
|
|
|
|
const [curseforgeMcVersions, setCurseforgeMcVersions] = useState<string[]>([]);
|
2026-07-11 12:13:20 +08:00
|
|
|
|
const [existingModrinth, setExistingModrinth] = useState<string[]>([]);
|
|
|
|
|
|
const [existingCurseforge, setExistingCurseforge] = useState<string[]>([]);
|
2026-07-18 10:04:16 +08:00
|
|
|
|
// 两平台全量已有版本(用于第一页下方灰色行展示)
|
|
|
|
|
|
const [modrinthExistingVersions, setModrinthExistingVersions] = useState<
|
|
|
|
|
|
Version[]
|
|
|
|
|
|
>([]);
|
|
|
|
|
|
const [curseforgeExistingFiles, setCurseforgeExistingFiles] = useState<
|
|
|
|
|
|
CurseForgeFile[]
|
|
|
|
|
|
>([]);
|
2026-07-11 12:13:20 +08:00
|
|
|
|
|
|
|
|
|
|
const loadExisting = useCallback(async () => {
|
|
|
|
|
|
setLoadingExisting(true);
|
|
|
|
|
|
try {
|
2026-07-18 10:04:16 +08:00
|
|
|
|
const mcVersions = project.artifacts.map((a) => a.mc_version);
|
|
|
|
|
|
// 元数据列表和已有版本比对同属第一页数据,任一失败都不允许进入下一步,
|
|
|
|
|
|
// 否则无法区分"未发布"和"没查到",有重复发布的风险
|
|
|
|
|
|
const [mrVersions, cfVersions, comparison] = await Promise.all([
|
2026-07-16 20:26:13 +08:00
|
|
|
|
getModrinthMcVersions(),
|
|
|
|
|
|
getCurseForgeMcVersions(),
|
2026-07-18 10:04:16 +08:00
|
|
|
|
compareVersions(config, project.version, mcVersions),
|
2026-07-16 20:26:13 +08:00
|
|
|
|
]);
|
|
|
|
|
|
setModrinthMcVersions(mrVersions);
|
|
|
|
|
|
setCurseforgeMcVersions(cfVersions);
|
2026-07-18 10:04:16 +08:00
|
|
|
|
setExistingModrinth(
|
|
|
|
|
|
mcVersions.filter((v) => comparison.modrinth[v]?.exists),
|
|
|
|
|
|
);
|
|
|
|
|
|
setExistingCurseforge(
|
|
|
|
|
|
mcVersions.filter((v) => comparison.curseforge[v]?.exists),
|
|
|
|
|
|
);
|
|
|
|
|
|
setModrinthExistingVersions(comparison.modrinthVersions);
|
|
|
|
|
|
setCurseforgeExistingFiles(comparison.curseforgeFiles);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
message.error(
|
|
|
|
|
|
`加载已有版本失败,请检查网络后点击“刷新已有版本”重试:${errorMessage(e)}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
// 清空比对数据,已有版本状态未知时禁用"下一步"
|
2026-07-11 12:13:20 +08:00
|
|
|
|
setExistingModrinth([]);
|
|
|
|
|
|
setExistingCurseforge([]);
|
2026-07-18 10:04:16 +08:00
|
|
|
|
setModrinthExistingVersions([]);
|
|
|
|
|
|
setCurseforgeExistingFiles([]);
|
2026-07-11 12:13:20 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoadingExisting(false);
|
|
|
|
|
|
}
|
2026-07-18 10:04:16 +08:00
|
|
|
|
}, [config, project, message]);
|
2026-07-11 12:13:20 +08:00
|
|
|
|
|
2026-07-16 20:26:13 +08:00
|
|
|
|
// Auto-load MC versions when modal opens
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (open) {
|
|
|
|
|
|
loadExisting();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [open, loadExisting]);
|
|
|
|
|
|
|
2026-07-16 16:05:13 +08:00
|
|
|
|
// Pre-compute version display values
|
|
|
|
|
|
const versionTmpl = useMemo(() => new Template(config.modrinth.version), [config.modrinth.version]);
|
|
|
|
|
|
const versionNameTmpl = useMemo(() => new Template(config.modrinth.version_name), [config.modrinth.version_name]);
|
|
|
|
|
|
|
2026-07-16 20:26:13 +08:00
|
|
|
|
// Compute per-platform version ranges
|
|
|
|
|
|
const projectVersions = useMemo(
|
|
|
|
|
|
() => project.artifacts.map((a) => a.mc_version),
|
|
|
|
|
|
[project],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const mrRanges = useMemo(() => {
|
|
|
|
|
|
if (modrinthMcVersions.length === 0) return null;
|
|
|
|
|
|
return computeVersionRanges(projectVersions, modrinthMcVersions, lastVersionEnd ?? undefined);
|
|
|
|
|
|
}, [projectVersions, modrinthMcVersions, lastVersionEnd]);
|
|
|
|
|
|
|
|
|
|
|
|
const cfRanges = useMemo(() => {
|
|
|
|
|
|
if (curseforgeMcVersions.length === 0) return null;
|
|
|
|
|
|
return computeVersionRanges(projectVersions, curseforgeMcVersions, lastVersionEnd ?? undefined);
|
|
|
|
|
|
}, [projectVersions, curseforgeMcVersions, lastVersionEnd]);
|
|
|
|
|
|
|
|
|
|
|
|
// Build per-platform range maps (mc_version → explicit)
|
|
|
|
|
|
const mrExplicitMap = useMemo(() => entriesToMap(mrRanges, "explicit"), [mrRanges]);
|
|
|
|
|
|
const cfExplicitMap = useMemo(() => entriesToMap(cfRanges, "explicit"), [cfRanges]);
|
2026-07-16 16:05:13 +08:00
|
|
|
|
|
2026-07-11 12:13:20 +08:00
|
|
|
|
// Build publish version rows from project artifacts
|
2026-07-16 16:05:13 +08:00
|
|
|
|
const publishVersions: PublishVersion[] = useMemo(() =>
|
|
|
|
|
|
project.artifacts.map((a) => {
|
2026-07-16 20:26:13 +08:00
|
|
|
|
// Note: version_name template uses Modrinth explicit range for ${mc_version_range}
|
|
|
|
|
|
const mrRange = mrExplicitMap.get(a.mc_version) ?? a.mc_version;
|
2026-07-16 16:05:13 +08:00
|
|
|
|
const tmplValues = {
|
|
|
|
|
|
version: project.version,
|
|
|
|
|
|
mc_version: a.mc_version,
|
2026-07-16 20:26:13 +08:00
|
|
|
|
mc_version_range: mrRange,
|
2026-07-16 16:05:13 +08:00
|
|
|
|
};
|
|
|
|
|
|
return {
|
|
|
|
|
|
key: a.mc_version,
|
|
|
|
|
|
mc_version: a.mc_version,
|
|
|
|
|
|
artifact: a.artifact,
|
|
|
|
|
|
sources: a.sources,
|
2026-07-16 20:26:13 +08:00
|
|
|
|
mrRange: mrExplicitMap.get(a.mc_version) ?? a.mc_version,
|
|
|
|
|
|
cfRange: cfExplicitMap.get(a.mc_version) ?? a.mc_version,
|
2026-07-16 16:05:13 +08:00
|
|
|
|
modrinthVersion: versionTmpl.format(tmplValues),
|
|
|
|
|
|
modrinthVersionName: versionNameTmpl.format(tmplValues),
|
|
|
|
|
|
existsModrinth: existingModrinth.includes(a.mc_version),
|
|
|
|
|
|
existsCurseforge: existingCurseforge.includes(a.mc_version),
|
|
|
|
|
|
};
|
|
|
|
|
|
}),
|
2026-07-16 20:26:13 +08:00
|
|
|
|
[project, versionTmpl, versionNameTmpl, mrExplicitMap, cfExplicitMap, existingModrinth, existingCurseforge],
|
2026-07-16 16:05:13 +08:00
|
|
|
|
);
|
2026-07-11 12:13:20 +08:00
|
|
|
|
|
2026-07-18 10:04:16 +08:00
|
|
|
|
// ── 第一页表格数据源:待发布行(上)+ 平台已有版本行(下,灰色禁用)──
|
|
|
|
|
|
|
|
|
|
|
|
// 与待发布版本匹配的已有对象不再重复出现在已有版本区
|
|
|
|
|
|
const matchedMrNames = useMemo(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
new Set(
|
|
|
|
|
|
publishVersions
|
|
|
|
|
|
.filter((v) => v.existsModrinth)
|
|
|
|
|
|
.map((v) => v.modrinthVersion),
|
|
|
|
|
|
),
|
|
|
|
|
|
[publishVersions],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const modrinthRows: ModrinthTableRow[] = useMemo(() => {
|
2026-07-18 21:16:32 +08:00
|
|
|
|
// 行 key 发布时会作为 mc_version 直接传入后端 API,严禁加任何前缀:
|
|
|
|
|
|
// 此处曾加 "pending-" 前缀,导致后端准备阶段报 Invalid SemVer: "pending-1.19"。
|
|
|
|
|
|
// 教训:所有要传入 API 的 key 都不能加前缀(行类型区分请用 kind 字段)。
|
2026-07-18 10:04:16 +08:00
|
|
|
|
const pending: ModrinthTableRow[] = publishVersions.map((v) => ({
|
2026-07-18 21:16:32 +08:00
|
|
|
|
key: v.key,
|
2026-07-18 10:04:16 +08:00
|
|
|
|
kind: "pending",
|
|
|
|
|
|
pending: v,
|
|
|
|
|
|
modrinthVersion: v.modrinthVersion,
|
|
|
|
|
|
mrRange: v.mrRange,
|
|
|
|
|
|
modrinthVersionName: v.modrinthVersionName,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
const existingTmpl = new Template(config.modrinth.version);
|
|
|
|
|
|
const existing = modrinthExistingVersions
|
|
|
|
|
|
.filter((v) => !matchedMrNames.has(v.version_number))
|
|
|
|
|
|
.map((v) => ({
|
|
|
|
|
|
row: {
|
|
|
|
|
|
key: `existing-mr-${v.version_number}`,
|
|
|
|
|
|
kind: "existing" as const,
|
|
|
|
|
|
pending: null,
|
|
|
|
|
|
modrinthVersion: v.version_number,
|
|
|
|
|
|
mrRange: "",
|
|
|
|
|
|
modrinthVersionName: v.name,
|
|
|
|
|
|
},
|
|
|
|
|
|
parsed: parseExisting(existingTmpl, v.version_number),
|
|
|
|
|
|
}))
|
|
|
|
|
|
.sort((a, b) => compareParsed(a.parsed, b.parsed))
|
|
|
|
|
|
.map((e) => e.row);
|
|
|
|
|
|
|
|
|
|
|
|
return [...pending, ...existing];
|
|
|
|
|
|
}, [publishVersions, modrinthExistingVersions, matchedMrNames, config.modrinth.version]);
|
|
|
|
|
|
|
|
|
|
|
|
const matchedCfFileNames = useMemo(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
new Set(
|
|
|
|
|
|
publishVersions
|
|
|
|
|
|
.filter((v) => v.existsCurseforge && v.artifact)
|
|
|
|
|
|
.map((v) => v.artifact as string),
|
|
|
|
|
|
),
|
|
|
|
|
|
[publishVersions],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const curseforgeRows: CurseforgeTableRow[] = useMemo(() => {
|
2026-07-18 21:16:32 +08:00
|
|
|
|
// 行 key 发布时会作为 mc_version 直接传入后端 API,严禁加任何前缀:
|
|
|
|
|
|
// 此处曾加 "pending-" 前缀,导致后端准备阶段报 Invalid SemVer: "pending-1.19"。
|
|
|
|
|
|
// 教训:所有要传入 API 的 key 都不能加前缀(行类型区分请用 kind 字段)。
|
2026-07-18 10:04:16 +08:00
|
|
|
|
const pending: CurseforgeTableRow[] = publishVersions.map((v) => ({
|
2026-07-18 21:16:32 +08:00
|
|
|
|
key: v.key,
|
2026-07-18 10:04:16 +08:00
|
|
|
|
kind: "pending",
|
|
|
|
|
|
pending: v,
|
|
|
|
|
|
mc_version: v.mc_version,
|
|
|
|
|
|
cfRange: v.cfRange,
|
|
|
|
|
|
artifact: v.artifact,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
const filenameTmpl = new Template(config.filename_format);
|
|
|
|
|
|
const existing = curseforgeExistingFiles
|
|
|
|
|
|
.filter((f) => !matchedCfFileNames.has(f.fileName))
|
|
|
|
|
|
.map((f) => {
|
|
|
|
|
|
const parsed = parseExisting(filenameTmpl, f.fileName);
|
|
|
|
|
|
return {
|
|
|
|
|
|
row: {
|
|
|
|
|
|
key: `existing-cf-${f.fileName}`,
|
|
|
|
|
|
kind: "existing" as const,
|
|
|
|
|
|
pending: null,
|
|
|
|
|
|
mc_version: parsed.mc_version ?? "—",
|
|
|
|
|
|
cfRange: "",
|
|
|
|
|
|
artifact: f.displayName || f.fileName,
|
|
|
|
|
|
},
|
|
|
|
|
|
parsed,
|
|
|
|
|
|
};
|
|
|
|
|
|
})
|
|
|
|
|
|
.sort((a, b) => compareParsed(a.parsed, b.parsed))
|
|
|
|
|
|
.map((e) => e.row);
|
|
|
|
|
|
|
|
|
|
|
|
return [...pending, ...existing];
|
|
|
|
|
|
}, [publishVersions, curseforgeExistingFiles, matchedCfFileNames, config.filename_format]);
|
|
|
|
|
|
|
2026-07-18 15:14:44 +08:00
|
|
|
|
/** 新增版本行 className;已有版本行用默认样式 */
|
|
|
|
|
|
const rowClassName = useCallback(
|
|
|
|
|
|
(kind: "pending" | "existing", exists: boolean) =>
|
|
|
|
|
|
kind === "pending" && !exists ? "row-new" : "",
|
|
|
|
|
|
[],
|
2026-07-18 10:04:16 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
2026-07-18 15:14:44 +08:00
|
|
|
|
// 注入 antd 绿色系 token 作为 CSS 变量(供 globals.css 的 .row-new 规则使用)。
|
|
|
|
|
|
// 三个过渡色用 color-mix 在 green-1 / green-2 之间混出半步色阶:
|
|
|
|
|
|
// hover = 25%,选中 = 50%,选中+hover = 75%
|
|
|
|
|
|
const newRowCssVars = {
|
|
|
|
|
|
"--row-new-bg": token.colorSuccessBg,
|
|
|
|
|
|
"--row-new-text": token.colorSuccessText,
|
|
|
|
|
|
"--row-new-bg-hover": `color-mix(in srgb, ${token.colorSuccessBg} 75%, ${token.colorSuccessBgHover} 25%)`,
|
|
|
|
|
|
"--row-new-bg-active": `color-mix(in srgb, ${token.colorSuccessBg} 50%, ${token.colorSuccessBgHover} 50%)`,
|
|
|
|
|
|
"--row-new-bg-active-hover": `color-mix(in srgb, ${token.colorSuccessBg} 25%, ${token.colorSuccessBgHover} 75%)`,
|
|
|
|
|
|
"--row-new-text-active": token.colorSuccessTextActive,
|
|
|
|
|
|
} as React.CSSProperties;
|
|
|
|
|
|
|
2026-07-16 20:43:00 +08:00
|
|
|
|
// Auto-select all non-existing versions when data first loads
|
|
|
|
|
|
const initRef = useRef(false);
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (open) {
|
|
|
|
|
|
initRef.current = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [open]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!loadingExisting && publishVersions.length > 0 && !initRef.current) {
|
|
|
|
|
|
initRef.current = true;
|
|
|
|
|
|
setSelectedModrinth(
|
|
|
|
|
|
publishVersions.filter((v) => !v.existsModrinth).map((v) => v.key),
|
|
|
|
|
|
);
|
|
|
|
|
|
setSelectedCurseforge(
|
|
|
|
|
|
publishVersions.filter((v) => !v.existsCurseforge).map((v) => v.key),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [loadingExisting, publishVersions]);
|
|
|
|
|
|
|
2026-07-11 12:13:20 +08:00
|
|
|
|
// --------------- Steps handling ---------------
|
|
|
|
|
|
|
2026-07-18 10:04:16 +08:00
|
|
|
|
// 进入第二页的前置条件:
|
|
|
|
|
|
// 1. 数据加载完成且成功(元数据为空说明加载失败或未加载,已有版本状态未知);
|
|
|
|
|
|
// 2. 至少勾选一个待发布版本。
|
|
|
|
|
|
const canProceed =
|
|
|
|
|
|
!loadingExisting &&
|
|
|
|
|
|
modrinthMcVersions.length > 0 &&
|
|
|
|
|
|
curseforgeMcVersions.length > 0 &&
|
|
|
|
|
|
(selectedModrinth.length > 0 || selectedCurseforge.length > 0);
|
|
|
|
|
|
|
2026-07-11 12:13:20 +08:00
|
|
|
|
const handleNext = useCallback(() => {
|
2026-07-16 14:34:56 +08:00
|
|
|
|
setCurrentStep((prev) => Math.min(prev + 1, 2));
|
2026-07-16 20:43:00 +08:00
|
|
|
|
}, []);
|
2026-07-11 12:13:20 +08:00
|
|
|
|
|
|
|
|
|
|
const handlePrev = useCallback(() => {
|
|
|
|
|
|
setCurrentStep((prev) => Math.max(prev - 1, 0));
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-07-17 19:15:41 +08:00
|
|
|
|
const handleConfirmPublish = useCallback(() => {
|
|
|
|
|
|
// 重置状态并进入进度页;真正发起 mutation 在下方 useSubscription 的
|
|
|
|
|
|
// onStarted 里(保证先连上 SSE 再发布,不漏早期进度事件)
|
2026-07-11 12:13:20 +08:00
|
|
|
|
setPublishing(true);
|
2026-07-16 14:34:56 +08:00
|
|
|
|
setPublishDone(false);
|
|
|
|
|
|
setPublishResult(null);
|
2026-07-17 19:15:41 +08:00
|
|
|
|
setProgress({
|
|
|
|
|
|
modrinth: { done: 0, total: selectedModrinth.length, status: "running" },
|
|
|
|
|
|
curseforge: {
|
|
|
|
|
|
done: 0,
|
|
|
|
|
|
total: selectedCurseforge.length,
|
|
|
|
|
|
status: "running",
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-07-16 14:34:56 +08:00
|
|
|
|
setCurrentStep(2);
|
2026-07-17 19:15:41 +08:00
|
|
|
|
}, [selectedModrinth.length, selectedCurseforge.length]);
|
|
|
|
|
|
|
|
|
|
|
|
const publishMutation = useMutation(
|
|
|
|
|
|
trpc.publish.mutationOptions({
|
|
|
|
|
|
onSuccess: (result) => {
|
|
|
|
|
|
let success = 0;
|
|
|
|
|
|
let failed = 0;
|
|
|
|
|
|
const errors = { modrinth: [], curseforge: [] } as Record<
|
|
|
|
|
|
typeof platforms[number],
|
|
|
|
|
|
string[]
|
|
|
|
|
|
>;
|
|
|
|
|
|
for (const platform of platforms) {
|
|
|
|
|
|
const r: PlatformResult = result[platform];
|
|
|
|
|
|
success += r.success;
|
|
|
|
|
|
failed += r.fail;
|
|
|
|
|
|
errors[platform] = r.errors;
|
|
|
|
|
|
}
|
|
|
|
|
|
setPublishResult({ success, failed, errors });
|
|
|
|
|
|
setPublishDone(true);
|
|
|
|
|
|
setPublishing(false);
|
|
|
|
|
|
},
|
|
|
|
|
|
onError: (error) => {
|
|
|
|
|
|
message.error(`发布请求失败:${error.message}`);
|
|
|
|
|
|
setPublishDone(true);
|
|
|
|
|
|
setPublishing(false);
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
useSubscription(
|
|
|
|
|
|
trpc.progress.subscriptionOptions(undefined, {
|
|
|
|
|
|
enabled: publishing,
|
|
|
|
|
|
onStarted: () => {
|
|
|
|
|
|
// SSE 已连接,现在发起发布不会漏事件
|
|
|
|
|
|
publishMutation.mutate({
|
|
|
|
|
|
configFile,
|
|
|
|
|
|
version: project.version,
|
|
|
|
|
|
mcVersions: {
|
|
|
|
|
|
modrinth: selectedModrinth.map((mc_version) => ({ mc_version })),
|
|
|
|
|
|
curseforge: selectedCurseforge.map((mc_version) => ({ mc_version })),
|
|
|
|
|
|
},
|
|
|
|
|
|
cutoffMcVersion: lastVersionEnd ?? "",
|
|
|
|
|
|
changelog,
|
|
|
|
|
|
versionType,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
onData: (event) => {
|
|
|
|
|
|
setProgress((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[event.platform]: {
|
|
|
|
|
|
done: event.current,
|
|
|
|
|
|
total: event.total,
|
|
|
|
|
|
status: event.status,
|
|
|
|
|
|
},
|
|
|
|
|
|
}));
|
|
|
|
|
|
},
|
|
|
|
|
|
onError: (error) => {
|
|
|
|
|
|
console.error("Progress subscription error:", error);
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
2026-07-11 12:13:20 +08:00
|
|
|
|
|
|
|
|
|
|
const handleClose = useCallback(() => {
|
|
|
|
|
|
if (publishing) return;
|
|
|
|
|
|
setCurrentStep(0);
|
2026-07-16 20:43:00 +08:00
|
|
|
|
setSelectedModrinth([]);
|
|
|
|
|
|
setSelectedCurseforge([]);
|
2026-07-11 12:13:20 +08:00
|
|
|
|
setChangelog("");
|
|
|
|
|
|
setVersionType("release");
|
2026-07-16 14:34:56 +08:00
|
|
|
|
setPublishResult(null);
|
|
|
|
|
|
setPublishDone(false);
|
2026-07-11 12:13:20 +08:00
|
|
|
|
onClose();
|
|
|
|
|
|
}, [publishing, onClose]);
|
|
|
|
|
|
|
|
|
|
|
|
// --------------- Render: Step 1 — Version Selection ---------------
|
|
|
|
|
|
|
|
|
|
|
|
const renderStep1 = () => {
|
|
|
|
|
|
const modrinthColumns = [
|
|
|
|
|
|
{
|
2026-07-16 16:05:13 +08:00
|
|
|
|
title: "版本号",
|
|
|
|
|
|
dataIndex: "modrinthVersion",
|
|
|
|
|
|
key: "modrinthVersion",
|
2026-07-18 10:04:16 +08:00
|
|
|
|
width: 150,
|
2026-07-11 12:13:20 +08:00
|
|
|
|
},
|
2026-07-16 20:26:13 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: "兼容范围",
|
|
|
|
|
|
dataIndex: "mrRange",
|
|
|
|
|
|
key: "mr_range",
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
render: (val: string) => <Tag>{val}</Tag>,
|
|
|
|
|
|
},
|
2026-07-11 12:13:20 +08:00
|
|
|
|
{
|
2026-07-16 16:05:13 +08:00
|
|
|
|
title: "版本名称",
|
|
|
|
|
|
dataIndex: "modrinthVersionName",
|
|
|
|
|
|
key: "modrinthVersionName",
|
2026-07-11 12:13:20 +08:00
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const curseforgeColumns = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "MC 版本",
|
|
|
|
|
|
dataIndex: "mc_version",
|
|
|
|
|
|
key: "mc_version",
|
2026-07-18 10:04:16 +08:00
|
|
|
|
width: 80,
|
2026-07-11 12:13:20 +08:00
|
|
|
|
},
|
2026-07-16 20:26:13 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: "兼容范围",
|
|
|
|
|
|
dataIndex: "cfRange",
|
|
|
|
|
|
key: "cf_range",
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
render: (val: string) => <Tag>{val}</Tag>,
|
|
|
|
|
|
},
|
2026-07-11 12:13:20 +08:00
|
|
|
|
{
|
2026-07-16 16:05:13 +08:00
|
|
|
|
title: "显示名称",
|
2026-07-11 12:13:20 +08:00
|
|
|
|
dataIndex: "artifact",
|
|
|
|
|
|
key: "artifact",
|
|
|
|
|
|
ellipsis: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: "flex",
|
|
|
|
|
|
justifyContent: "space-between",
|
|
|
|
|
|
alignItems: "center",
|
2026-07-16 14:34:56 +08:00
|
|
|
|
marginBottom: token.margin,
|
2026-07-11 12:13:20 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Text strong>选择要发布的版本</Text>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
icon={<ReloadOutlined />}
|
|
|
|
|
|
loading={loadingExisting}
|
|
|
|
|
|
onClick={loadExisting}
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
>
|
|
|
|
|
|
刷新已有版本
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{loadingExisting ? (
|
|
|
|
|
|
<div style={{ textAlign: "center", padding: 40 }}>加载中…</div>
|
|
|
|
|
|
) : (
|
2026-07-18 15:14:44 +08:00
|
|
|
|
<Row gutter={token.margin} className="publish-version-tables" style={newRowCssVars}>
|
2026-07-16 20:26:13 +08:00
|
|
|
|
<Col span={14}>
|
2026-07-16 14:34:56 +08:00
|
|
|
|
<Text strong style={{ display: "block", marginBottom: token.marginXS }}>
|
2026-07-11 12:13:20 +08:00
|
|
|
|
Modrinth
|
|
|
|
|
|
</Text>
|
2026-07-18 10:04:16 +08:00
|
|
|
|
{modrinthRows.length > 0 ? (
|
2026-07-11 12:13:20 +08:00
|
|
|
|
<Table
|
2026-07-18 10:04:16 +08:00
|
|
|
|
dataSource={modrinthRows}
|
2026-07-11 12:13:20 +08:00
|
|
|
|
columns={modrinthColumns}
|
2026-07-18 10:04:16 +08:00
|
|
|
|
rowKey="key"
|
2026-07-18 15:14:44 +08:00
|
|
|
|
rowClassName={(r) =>
|
|
|
|
|
|
rowClassName(
|
2026-07-18 10:04:16 +08:00
|
|
|
|
r.kind,
|
|
|
|
|
|
r.kind === "pending"
|
|
|
|
|
|
? (r.pending?.existsModrinth ?? false)
|
|
|
|
|
|
: true,
|
2026-07-18 15:14:44 +08:00
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-07-16 20:43:00 +08:00
|
|
|
|
rowSelection={{
|
|
|
|
|
|
selectedRowKeys: selectedModrinth,
|
|
|
|
|
|
onChange: (keys) => setSelectedModrinth(keys as string[]),
|
2026-07-18 10:04:16 +08:00
|
|
|
|
getCheckboxProps: (r: ModrinthTableRow) => ({
|
|
|
|
|
|
disabled:
|
|
|
|
|
|
r.kind === "existing" ||
|
|
|
|
|
|
(r.pending?.existsModrinth ?? false),
|
2026-07-16 20:43:00 +08:00
|
|
|
|
}),
|
|
|
|
|
|
}}
|
2026-07-11 12:13:20 +08:00
|
|
|
|
pagination={false}
|
2026-07-18 10:04:16 +08:00
|
|
|
|
scroll={{ y: 480 }}
|
2026-07-11 12:13:20 +08:00
|
|
|
|
size="small"
|
|
|
|
|
|
bordered
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Empty description="无版本" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Col>
|
2026-07-16 20:26:13 +08:00
|
|
|
|
<Col span={10}>
|
2026-07-16 14:34:56 +08:00
|
|
|
|
<Text strong style={{ display: "block", marginBottom: token.marginXS }}>
|
2026-07-11 12:13:20 +08:00
|
|
|
|
CurseForge
|
|
|
|
|
|
</Text>
|
2026-07-18 10:04:16 +08:00
|
|
|
|
{curseforgeRows.length > 0 ? (
|
2026-07-11 12:13:20 +08:00
|
|
|
|
<Table
|
2026-07-18 10:04:16 +08:00
|
|
|
|
dataSource={curseforgeRows}
|
2026-07-11 12:13:20 +08:00
|
|
|
|
columns={curseforgeColumns}
|
2026-07-18 10:04:16 +08:00
|
|
|
|
rowKey="key"
|
2026-07-18 15:14:44 +08:00
|
|
|
|
rowClassName={(r) =>
|
|
|
|
|
|
rowClassName(
|
2026-07-18 10:04:16 +08:00
|
|
|
|
r.kind,
|
|
|
|
|
|
r.kind === "pending"
|
|
|
|
|
|
? (r.pending?.existsCurseforge ?? false)
|
|
|
|
|
|
: true,
|
2026-07-18 15:14:44 +08:00
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-07-16 20:43:00 +08:00
|
|
|
|
rowSelection={{
|
|
|
|
|
|
selectedRowKeys: selectedCurseforge,
|
|
|
|
|
|
onChange: (keys) => setSelectedCurseforge(keys as string[]),
|
2026-07-18 10:04:16 +08:00
|
|
|
|
getCheckboxProps: (r: CurseforgeTableRow) => ({
|
|
|
|
|
|
disabled:
|
|
|
|
|
|
r.kind === "existing" ||
|
|
|
|
|
|
(r.pending?.existsCurseforge ?? false),
|
2026-07-16 20:43:00 +08:00
|
|
|
|
}),
|
|
|
|
|
|
}}
|
2026-07-11 12:13:20 +08:00
|
|
|
|
pagination={false}
|
2026-07-18 10:04:16 +08:00
|
|
|
|
scroll={{ y: 480 }}
|
2026-07-11 12:13:20 +08:00
|
|
|
|
size="small"
|
|
|
|
|
|
bordered
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Empty description="无版本" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// --------------- Render: Step 2 — Publish Settings ---------------
|
|
|
|
|
|
|
|
|
|
|
|
const renderStep2 = () => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
{/* Changelog */}
|
2026-07-16 14:34:56 +08:00
|
|
|
|
<div style={{ marginBottom: token.marginLG }}>
|
|
|
|
|
|
<Text strong style={{ display: "block", marginBottom: token.marginSM }}>
|
2026-07-11 12:13:20 +08:00
|
|
|
|
Changelog (Markdown)
|
|
|
|
|
|
</Text>
|
2026-07-17 16:09:03 +08:00
|
|
|
|
<ChangelogEditor value={changelog} onChange={setChangelog} />
|
2026-07-11 12:13:20 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Version type */}
|
2026-07-16 14:34:56 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<Text strong style={{ display: "block", marginBottom: token.marginSM }}>
|
2026-07-11 12:13:20 +08:00
|
|
|
|
版本类型
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={versionType}
|
|
|
|
|
|
onChange={(v) => setVersionType(v)}
|
|
|
|
|
|
style={{ width: 160 }}
|
2026-07-13 09:29:00 +08:00
|
|
|
|
options={RELEASE_TYPES}
|
2026-07-11 12:13:20 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-07-16 14:34:56 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
2026-07-11 12:13:20 +08:00
|
|
|
|
|
2026-07-16 14:34:56 +08:00
|
|
|
|
// --------------- Render: Step 3 — Progress & Result ---------------
|
|
|
|
|
|
|
|
|
|
|
|
const renderStep3 = () => {
|
2026-07-16 20:43:00 +08:00
|
|
|
|
const hasModrinth = selectedModrinth.length > 0;
|
|
|
|
|
|
const hasCurseforge = selectedCurseforge.length > 0;
|
2026-07-16 14:34:56 +08:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
{/* Progress bars */}
|
|
|
|
|
|
<div style={{ marginBottom: token.marginLG }}>
|
2026-07-16 15:22:06 +08:00
|
|
|
|
<Flex vertical gap={token.marginSM}>
|
|
|
|
|
|
{hasModrinth && (
|
|
|
|
|
|
<Flex align="center" gap={token.marginXS}>
|
|
|
|
|
|
<img
|
|
|
|
|
|
src="/platform/modrinth.svg"
|
|
|
|
|
|
alt="Modrinth"
|
|
|
|
|
|
style={{ width: 30, height: 30, flexShrink: 0 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Text style={{ width: 80, flexShrink: 0, fontFamily: "Inter", fontWeight: "bold" }}>Modrinth</Text>
|
|
|
|
|
|
<Progress
|
|
|
|
|
|
percent={Math.round(
|
|
|
|
|
|
(progress.modrinth.done /
|
|
|
|
|
|
Math.max(progress.modrinth.total, 1)) *
|
|
|
|
|
|
100,
|
|
|
|
|
|
)}
|
2026-07-17 19:15:41 +08:00
|
|
|
|
status={
|
|
|
|
|
|
progress.modrinth.status === "failed"
|
|
|
|
|
|
? "exception"
|
|
|
|
|
|
: undefined
|
|
|
|
|
|
}
|
2026-07-16 15:22:06 +08:00
|
|
|
|
format={() =>
|
|
|
|
|
|
`${progress.modrinth.done} / ${progress.modrinth.total}`
|
|
|
|
|
|
}
|
|
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Flex>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{hasCurseforge && (
|
|
|
|
|
|
<Flex align="center" gap={token.marginXS}>
|
|
|
|
|
|
<img
|
|
|
|
|
|
src="/platform/curseforge.svg"
|
|
|
|
|
|
alt="CurseForge"
|
|
|
|
|
|
style={{ width: 30, height: 30, flexShrink: 0 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Text style={{ width: 80, flexShrink: 0, fontFamily: "Inter", fontWeight: "bold" }}>CurseForge</Text>
|
|
|
|
|
|
<Progress
|
|
|
|
|
|
percent={Math.round(
|
|
|
|
|
|
(progress.curseforge.done /
|
|
|
|
|
|
Math.max(progress.curseforge.total, 1)) *
|
|
|
|
|
|
100,
|
|
|
|
|
|
)}
|
2026-07-17 19:15:41 +08:00
|
|
|
|
status={
|
|
|
|
|
|
progress.curseforge.status === "failed"
|
|
|
|
|
|
? "exception"
|
|
|
|
|
|
: undefined
|
|
|
|
|
|
}
|
2026-07-16 15:22:06 +08:00
|
|
|
|
format={() =>
|
|
|
|
|
|
`${progress.curseforge.done} / ${progress.curseforge.total}`
|
|
|
|
|
|
}
|
|
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Flex>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Flex>
|
2026-07-16 14:34:56 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Result */}
|
|
|
|
|
|
{publishDone && publishResult && (
|
2026-07-17 19:15:41 +08:00
|
|
|
|
<Alert
|
|
|
|
|
|
type={publishResult.failed > 0 ? "warning" : "success"}
|
|
|
|
|
|
title="发布结果"
|
|
|
|
|
|
description={
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
成功 {publishResult.success} 个,失败{" "}
|
|
|
|
|
|
{publishResult.failed} 个
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{platforms.map((platform) => {
|
|
|
|
|
|
const errors = publishResult.errors[platform];
|
|
|
|
|
|
if (errors.length === 0) return null;
|
|
|
|
|
|
const label =
|
|
|
|
|
|
platform === "modrinth" ? "Modrinth" : "CurseForge";
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={platform} style={{ marginTop: token.marginSM }}>
|
|
|
|
|
|
<Text strong>{label}:</Text>
|
|
|
|
|
|
<ul style={{ margin: "4px 0 0", paddingLeft: 20 }}>
|
|
|
|
|
|
{errors.map((e, i) => (
|
|
|
|
|
|
<li key={i}>
|
|
|
|
|
|
<Text type="danger">{e}</Text>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
showIcon
|
|
|
|
|
|
/>
|
2026-07-11 12:13:20 +08:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// --------------- Render ---------------
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title="一键发布"
|
|
|
|
|
|
open={open}
|
|
|
|
|
|
onCancel={handleClose}
|
2026-07-16 20:26:13 +08:00
|
|
|
|
width={1200}
|
2026-07-11 12:13:20 +08:00
|
|
|
|
destroyOnHidden
|
|
|
|
|
|
footer={
|
|
|
|
|
|
publishing ? null : (
|
|
|
|
|
|
<Space>
|
2026-07-16 14:34:56 +08:00
|
|
|
|
{currentStep > 0 && currentStep < 2 && (
|
2026-07-11 12:13:20 +08:00
|
|
|
|
<Button onClick={handlePrev}>上一步</Button>
|
|
|
|
|
|
)}
|
2026-07-16 14:34:56 +08:00
|
|
|
|
{currentStep === 0 ? (
|
2026-07-18 10:04:16 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
onClick={handleNext}
|
|
|
|
|
|
disabled={!canProceed}
|
|
|
|
|
|
>
|
2026-07-11 12:13:20 +08:00
|
|
|
|
下一步
|
|
|
|
|
|
</Button>
|
2026-07-16 14:34:56 +08:00
|
|
|
|
) : currentStep === 1 ? (
|
2026-07-11 12:13:20 +08:00
|
|
|
|
<Button type="primary" onClick={handleConfirmPublish}>
|
|
|
|
|
|
确认发布
|
|
|
|
|
|
</Button>
|
2026-07-16 14:34:56 +08:00
|
|
|
|
) : (
|
|
|
|
|
|
<Button type="primary" onClick={handleClose}>
|
|
|
|
|
|
关闭
|
|
|
|
|
|
</Button>
|
2026-07-11 12:13:20 +08:00
|
|
|
|
)}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Steps
|
|
|
|
|
|
current={currentStep}
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
style={{ marginBottom: 24 }}
|
|
|
|
|
|
items={[
|
|
|
|
|
|
{ title: "版本选择" },
|
|
|
|
|
|
{ title: "发布设置" },
|
2026-07-16 14:34:56 +08:00
|
|
|
|
{ title: "发布进度" },
|
2026-07-11 12:13:20 +08:00
|
|
|
|
]}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-07-16 14:34:56 +08:00
|
|
|
|
{currentStep === 0
|
|
|
|
|
|
? renderStep1()
|
|
|
|
|
|
: currentStep === 1
|
|
|
|
|
|
? renderStep2()
|
|
|
|
|
|
: renderStep3()}
|
2026-07-11 12:13:20 +08:00
|
|
|
|
</Modal>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|