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