2026-07-11 12:13:20 +08:00
|
|
|
"use client";
|
|
|
|
|
|
2026-07-16 14:17:49 +08:00
|
|
|
import { useEffect, useState, useMemo, useCallback } from "react";
|
|
|
|
|
import { Table, Empty, Tag, Typography, Select, Flex } from "antd";
|
2026-07-11 12:13:20 +08:00
|
|
|
import type { ParsedProject } from "@/services/project";
|
2026-07-16 14:17:49 +08:00
|
|
|
import { computeVersionRanges, collectAutoRange, type McVersionEntry } from "@/lib/utils/mcVersion";
|
|
|
|
|
import { getMcVersionUnion } from "@/services/meta";
|
2026-07-11 12:13:20 +08:00
|
|
|
|
|
|
|
|
interface ArtifactRow {
|
|
|
|
|
key: string;
|
|
|
|
|
mc_version: string;
|
|
|
|
|
artifact: string | null;
|
|
|
|
|
sources: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface VersionTableProps {
|
|
|
|
|
project: ParsedProject | null;
|
|
|
|
|
loading: boolean;
|
|
|
|
|
onLoadingChange: (loading: boolean) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function VersionTable({
|
|
|
|
|
project,
|
|
|
|
|
loading,
|
|
|
|
|
onLoadingChange,
|
|
|
|
|
}: VersionTableProps) {
|
2026-07-16 14:17:49 +08:00
|
|
|
// ── MC version union from both platforms ──
|
|
|
|
|
const [allMcVersions, setAllMcVersions] = useState<string[]>([]);
|
|
|
|
|
|
|
|
|
|
// Last version end selection (the editable dropdown value)
|
|
|
|
|
const [lastVersionEnd, setLastVersionEnd] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
// Fetch MC version union when project changes
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!project || project.artifacts.length === 0) {
|
|
|
|
|
setAllMcVersions([]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
onLoadingChange(true);
|
|
|
|
|
|
|
|
|
|
getMcVersionUnion()
|
|
|
|
|
.then((versions) => {
|
|
|
|
|
if (cancelled) return;
|
|
|
|
|
setAllMcVersions(versions);
|
|
|
|
|
// Default last version end to the latest available version
|
|
|
|
|
setLastVersionEnd(versions[versions.length - 1] ?? null);
|
|
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
onLoadingChange(false);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
};
|
|
|
|
|
}, [project, onLoadingChange]);
|
|
|
|
|
|
|
|
|
|
// ── Compute version ranges ──
|
|
|
|
|
const versionRanges: McVersionEntry[] | null = useMemo(() => {
|
|
|
|
|
if (!project || allMcVersions.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
const projectVersions = project.artifacts.map((a) => a.mc_version);
|
|
|
|
|
|
|
|
|
|
// computeVersionRanges expects ascending order; project versions are descending
|
|
|
|
|
const ascending = [...projectVersions].reverse();
|
|
|
|
|
|
|
|
|
|
const entries = computeVersionRanges(
|
|
|
|
|
ascending,
|
|
|
|
|
allMcVersions,
|
|
|
|
|
lastVersionEnd ?? undefined,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Reverse back to match descending display order
|
|
|
|
|
return [...entries].reverse();
|
|
|
|
|
}, [project, allMcVersions, lastVersionEnd]);
|
|
|
|
|
|
|
|
|
|
// ── Dropdown options: 同系列中 >= 最后一个项目版本的版本 ──
|
|
|
|
|
const lastVersionDropdownOptions = useMemo(() => {
|
|
|
|
|
if (!project || allMcVersions.length === 0) return [];
|
|
|
|
|
|
|
|
|
|
// Descending display, index 0 is the largest (= last in compatibility)
|
|
|
|
|
const lastVer = project.artifacts[0]?.mc_version;
|
|
|
|
|
if (!lastVer) return [];
|
|
|
|
|
|
|
|
|
|
const series = collectAutoRange(lastVer, allMcVersions);
|
|
|
|
|
return series
|
|
|
|
|
.filter(
|
|
|
|
|
(v) =>
|
|
|
|
|
v.localeCompare(lastVer, undefined, { numeric: true }) >= 0,
|
|
|
|
|
)
|
|
|
|
|
.map((v) => ({ value: v, label: v }));
|
|
|
|
|
}, [project, allMcVersions]);
|
|
|
|
|
|
|
|
|
|
// Reset lastVersionEnd to latest in the same series
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (allMcVersions.length === 0 || !project) return;
|
|
|
|
|
const lastVer = project.artifacts[0]?.mc_version;
|
|
|
|
|
if (!lastVer) return;
|
|
|
|
|
const series = collectAutoRange(lastVer, allMcVersions);
|
|
|
|
|
setLastVersionEnd(series[series.length - 1] ?? null);
|
|
|
|
|
}, [allMcVersions, project]);
|
|
|
|
|
|
|
|
|
|
// ── Table data ──
|
|
|
|
|
const dataSource: (ArtifactRow & { rangeEntry?: McVersionEntry })[] =
|
|
|
|
|
useMemo(() => {
|
|
|
|
|
if (!project) return [];
|
|
|
|
|
return project.artifacts.map((a, i) => ({
|
|
|
|
|
key: a.mc_version,
|
|
|
|
|
mc_version: a.mc_version,
|
|
|
|
|
artifact: a.artifact,
|
|
|
|
|
sources: a.sources,
|
|
|
|
|
rangeEntry: versionRanges?.[i],
|
|
|
|
|
}));
|
|
|
|
|
}, [project, versionRanges]);
|
|
|
|
|
|
|
|
|
|
// ── Dropdown change handler ──
|
|
|
|
|
const handleLastVersionEndChange = useCallback((value: string) => {
|
|
|
|
|
setLastVersionEnd(value);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// ── Columns ──
|
|
|
|
|
const columns = useMemo(
|
|
|
|
|
() => [
|
|
|
|
|
{
|
|
|
|
|
title: "MC 版本",
|
|
|
|
|
dataIndex: "mc_version",
|
|
|
|
|
key: "mc_version",
|
|
|
|
|
width: 120,
|
|
|
|
|
render: (val: string) => <Tag>{val}</Tag>,
|
2026-07-11 12:13:20 +08:00
|
|
|
},
|
2026-07-16 14:17:49 +08:00
|
|
|
{
|
|
|
|
|
title: "兼容范围",
|
|
|
|
|
key: "range",
|
|
|
|
|
width: 280,
|
|
|
|
|
render: (
|
|
|
|
|
_: unknown,
|
|
|
|
|
record: ArtifactRow & { rangeEntry?: McVersionEntry },
|
|
|
|
|
index: number,
|
|
|
|
|
) => {
|
|
|
|
|
const entry = record.rangeEntry;
|
|
|
|
|
|
|
|
|
|
if (!entry) {
|
|
|
|
|
return (
|
|
|
|
|
<Typography.Text type="secondary">
|
|
|
|
|
加载元数据中…
|
|
|
|
|
</Typography.Text>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Descending order: index 0 is the largest (= last in compatibility terms)
|
|
|
|
|
const isLast = index === 0;
|
|
|
|
|
|
|
|
|
|
if (!isLast) {
|
|
|
|
|
// Non-last: show wildcard if it differs, otherwise explicit
|
|
|
|
|
return <Tag color="blue">{entry.wildcard}</Tag>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Last version: range + editable end-version dropdown
|
|
|
|
|
return (
|
|
|
|
|
<Flex align="center" gap={6}>
|
|
|
|
|
<Tag color="blue">{entry.wildcard}</Tag>
|
|
|
|
|
<Typography.Text
|
|
|
|
|
type="secondary"
|
|
|
|
|
style={{ fontSize: 12, whiteSpace: "nowrap" }}
|
|
|
|
|
>
|
|
|
|
|
截止
|
|
|
|
|
</Typography.Text>
|
|
|
|
|
<Select
|
|
|
|
|
size="small"
|
|
|
|
|
style={{ width: 110 }}
|
|
|
|
|
showSearch
|
|
|
|
|
optionFilterProp="label"
|
|
|
|
|
value={lastVersionEnd ?? undefined}
|
|
|
|
|
onChange={handleLastVersionEndChange}
|
|
|
|
|
options={lastVersionDropdownOptions}
|
|
|
|
|
/>
|
|
|
|
|
</Flex>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: "构建产物",
|
|
|
|
|
dataIndex: "artifact",
|
|
|
|
|
key: "artifact",
|
|
|
|
|
ellipsis: true,
|
|
|
|
|
render: (val: string | null) =>
|
|
|
|
|
val ? (
|
|
|
|
|
<Typography.Text>{val}</Typography.Text>
|
|
|
|
|
) : (
|
|
|
|
|
<Typography.Text type="danger">文件不存在</Typography.Text>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: "源码",
|
|
|
|
|
dataIndex: "sources",
|
|
|
|
|
key: "sources",
|
|
|
|
|
ellipsis: true,
|
|
|
|
|
render: (val: string | null) =>
|
|
|
|
|
val ? (
|
|
|
|
|
<Typography.Text>{val}</Typography.Text>
|
|
|
|
|
) : (
|
|
|
|
|
<Typography.Text type="danger">文件不存在</Typography.Text>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
[lastVersionEnd, lastVersionDropdownOptions, handleLastVersionEndChange],
|
|
|
|
|
);
|
2026-07-11 12:13:20 +08:00
|
|
|
|
2026-07-16 14:17:49 +08:00
|
|
|
// ── Empty state ──
|
2026-07-11 12:13:20 +08:00
|
|
|
if (!project) {
|
|
|
|
|
return (
|
|
|
|
|
<Empty
|
|
|
|
|
description="请选择一个配置以查看版本信息"
|
|
|
|
|
style={{ padding: 40 }}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-07-16 14:17:49 +08:00
|
|
|
<Table<ArtifactRow & { rangeEntry?: McVersionEntry }>
|
2026-07-11 12:13:20 +08:00
|
|
|
dataSource={dataSource}
|
|
|
|
|
columns={columns}
|
|
|
|
|
loading={loading}
|
|
|
|
|
pagination={false}
|
|
|
|
|
size="middle"
|
|
|
|
|
bordered
|
|
|
|
|
locale={{
|
|
|
|
|
emptyText: "未找到构建产物",
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|