From 269e7024a299dda5cfa49c5881a1fcbe8a7d654b Mon Sep 17 00:00:00 2001
From: CPTProgrammer <46586216+CPTProgrammer@users.noreply.github.com>
Date: Wed, 15 Jul 2026 22:16:12 +0800
Subject: [PATCH] Add loader icons, colors, and sort order to ConfigModal
selects
---
src/components/ConfigModal.tsx | 100 ++++++++++++++++++++++++++++++++-
src/components/loaders.tsx | 94 +++++++++++++++++++++++++++++++
2 files changed, 191 insertions(+), 3 deletions(-)
create mode 100644 src/components/loaders.tsx
diff --git a/src/components/ConfigModal.tsx b/src/components/ConfigModal.tsx
index fed91ae..8ba9cd5 100644
--- a/src/components/ConfigModal.tsx
+++ b/src/components/ConfigModal.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useState, useCallback } from "react";
+import React, { useEffect, useState, useCallback, useMemo } from "react";
import {
Modal,
Form,
@@ -8,9 +8,11 @@ import {
InputNumber,
Select,
Tabs,
+ Tag,
Button,
Space,
App,
+ theme,
} from "antd";
import { PlusOutlined, DeleteOutlined } from "@ant-design/icons";
import { createConfig, updateConfig } from "@/services/config";
@@ -40,6 +42,7 @@ import {
} from "@/hooks/useFieldValidations";
import type { Config, DependencyType, UploadRelationType } from "@/types";
import { Template } from "@/lib/utils/template";
+import { getModLoaderMetadata, MOD_LOADERS_METADATA, type ModLoaderMetadata } from "./loaders";
// ---------------------------------------------------------------------------
// 工具
@@ -485,6 +488,10 @@ export function ConfigModal({
// Inner component — Form with independently-loading Selects
// ---------------------------------------------------------------------------
+function LoaderIcon({ meta, size }: { meta: ModLoaderMetadata; size: number }) {
+ return ;
+}
+
interface ConfigModalFormProps {
mode: "add" | "edit";
initialConfig: Config | null;
@@ -497,12 +504,37 @@ function ConfigModalForm({
onSave,
}: ConfigModalFormProps) {
const [form] = Form.useForm();
+ const { token } = theme.useToken();
// Each Select loads its own data independently
const mrLoaders = useModrinthLoaders();
const mrEnvs = useModrinthEnvironments();
const cfMeta = useCurseforgeMeta();
+ const sortedMrOptions = useMemo(() => {
+ const orderMap = new Map(
+ MOD_LOADERS_METADATA.map((m, i) => [m.name.toLowerCase(), i]),
+ );
+ const sortFn = (a: { label: string }, b: { label: string }) => {
+ const ai = orderMap.get(a.label.toLowerCase()) ?? Infinity;
+ const bi = orderMap.get(b.label.toLowerCase()) ?? Infinity;
+ return ai - bi;
+ };
+ return [...mrLoaders.options].sort(sortFn);
+ }, [mrLoaders.options]);
+
+ const sortedCfOptions = useMemo(() => {
+ const orderMap = new Map(
+ MOD_LOADERS_METADATA.map((m, i) => [m.name.toLowerCase(), i]),
+ );
+ const sortFn = (a: string, b: string) => {
+ const ai = orderMap.get(a.toLowerCase()) ?? Infinity;
+ const bi = orderMap.get(b.toLowerCase()) ?? Infinity;
+ return ai - bi;
+ };
+ return [...cfMeta.loaders].sort(sortFn);
+ }, [cfMeta.loaders]);
+
// 表单项实时校验
const { Provider, results } = useFieldValidations(form, STATIC_DEFS, LIST_DEFS);
@@ -676,7 +708,38 @@ function ConfigModalForm({
mode="multiple"
placeholder="选择加载器…"
loading={mrLoaders.loading}
- options={mrLoaders.options}
+ options={sortedMrOptions}
+ optionRender={(option) => {
+ const meta = getModLoaderMetadata(option.label as string);
+ return (
+
+ {meta && }
+
+ {meta?.name ?? option.label}
+
+
+ );
+ }}
+ tagRender={(props) => {
+ const { label, closable, onClose } = props;
+ const meta = getModLoaderMetadata(label as string);
+ const onPreventMouseDown = (event: React.MouseEvent) => {
+ event.preventDefault();
+ event.stopPropagation();
+ };
+ return (
+ : undefined}
+ style={{ marginInlineEnd: token.marginXXS, fontWeight: 500 }}
+ >
+ {meta?.name ?? label}
+
+ );
+ }}
notFoundContent={
mrLoaders.loading ? "加载中…" : "无可用选项"
}
@@ -817,10 +880,41 @@ function ConfigModalForm({
mode="multiple"
placeholder="选择加载器…"
loading={cfMeta.loading}
- options={cfMeta.loaders.map((l) => ({
+ options={sortedCfOptions.map((l) => ({
label: l,
value: l,
}))}
+ optionRender={(option) => {
+ const meta = getModLoaderMetadata(option.label as string);
+ return (
+
+ {meta && }
+
+ {meta?.name ?? option.label}
+
+
+ );
+ }}
+ tagRender={(props) => {
+ const { label, closable, onClose } = props;
+ const meta = getModLoaderMetadata(label as string);
+ const onPreventMouseDown = (event: React.MouseEvent) => {
+ event.preventDefault();
+ event.stopPropagation();
+ };
+ return (
+ : undefined}
+ style={{ marginInlineEnd: token.marginXXS, fontWeight: 500 }}
+ >
+ {meta?.name ?? label}
+
+ );
+ }}
notFoundContent={
cfMeta.loading ? "加载中…" : "无可用选项"
}
diff --git a/src/components/loaders.tsx b/src/components/loaders.tsx
new file mode 100644
index 0000000..84838e6
--- /dev/null
+++ b/src/components/loaders.tsx
@@ -0,0 +1,94 @@
+import Icon from "@ant-design/icons";
+import { GetProps } from "antd";
+import React from "react";
+
+export type ModLoaderMetadataPart = {
+ icon: React.ReactElement>,
+ name: string,
+ /** 加载器别名 */
+ alias?: string[],
+ color: string,
+};
+
+export type ModLoaderMetadata = ModLoaderMetadataPart & {
+ iconComponent: React.FC>>,
+};
+
+function preprocessMetadata(metadata: ModLoaderMetadataPart[]): ModLoaderMetadata[] {
+ return metadata.map(meta => ({
+ ...meta,
+ iconComponent: (props) => (
+ ) => {
+ const { children, ...rest } = svgProps;
+ return React.cloneElement(meta.icon, rest);
+ }
+ } {...props} />
+ )
+ }));
+}
+
+export const MOD_LOADERS_METADATA: ModLoaderMetadata[] = preprocessMetadata([
+ {
+ icon: (
+
+ ),
+ name: "Fabric",
+ color: "#8a7b71"
+ },
+ {
+ icon: (
+
+ ),
+ name: "Forge",
+ color: "#5b6197"
+ },
+ {
+ icon: (
+
+ ),
+ name: "NeoForge",
+ color: "#dc895c"
+ },
+ {
+ icon: (
+
+ ),
+ name: "Quilt",
+ color: "#8b61b4"
+ },
+]);
+
+export function getModLoaderMetadata(name: string): ModLoaderMetadata | undefined {
+ return MOD_LOADERS_METADATA.find(loader =>
+ loader.name.toLowerCase() === name.toLowerCase() ||
+ (loader.alias && loader.alias.some(alias => alias.toLowerCase() === name.toLowerCase()))
+ );
+}