import { useId, useState } from "react"; import { Button, Input, InputNumber } from "antd"; import { Check, Plus } from "lucide-react"; import { Switch } from "@/components/ui/base/switch"; import "./image-size-picker.css"; import type { ImageCapabilityConfig } from "@/lib/model-capabilities"; import { DEFAULT_IMAGE_RATIOS, IMAGE_RATIOS, imageResolutionTiers, imagePresetForRatio, imageResolutionUsesQuality, imageSizeConfigWithPresets, imageSizePresets, imageTierAvailable } from "@/lib/image-size-presets"; import { validateImageSize } from "@/services/api/image-validation"; import type { ImageResolutionTier } from "@/lib/image-resolution-tiers"; export function ImageSizePresetsEditor({ profile, disabled, onChange, tiers, ratios: allowedRatios, allowCustomRatio = true, grouped = true }: { profile: ImageCapabilityConfig; disabled?: boolean; onChange: (size: ImageCapabilityConfig["size"]) => void; tiers?: string[]; ratios?: string[]; allowCustomRatio?: boolean; grouped?: boolean }) { const IMAGE_RESOLUTIONS = tiers ?? imageResolutionTiers(profile); const presets = imageSizePresets(profile); const id = useId(); const [drafts, setDrafts] = useState>>({}); const [errors, setErrors] = useState>>({}); const candidate = (tier: string, ratio: string) => { const existing = presets.find((preset) => preset.tier === tier && preset.ratio === ratio); if (existing) return existing; try { return imagePresetForRatio(tier, ratio); } catch { return { tier, ratio, width: 0, height: 0, size: "" }; } }; const toggleTier = (tier: ImageResolutionTier, ratios: string[], enabled: boolean) => { if (disabled) return; update(tier, enabled ? ratios : []); }; const update = (tier: ImageResolutionTier, ratios: string[]) => { if (disabled) return false; try { const next = [ ...new Map( ratios.map((ratio) => { if (!/^\d+[::]\d+$/.test(ratio) || ratio.split(/[::]/).some((part) => Number(part) <= 0)) throw new Error("请输入正数比例,例如 16:9"); const preset = candidate(tier, ratio); return [preset.ratio, preset] as const; }), ).values(), ]; onChange(imageSizeConfigWithPresets(profile, [...presets.filter((preset) => preset.tier !== tier), ...next])); setErrors((current) => ({ ...current, [tier]: "" })); return true; } catch (reason) { setErrors((current) => ({ ...current, [tier]: reason instanceof Error ? reason.message : "比例格式无效" })); return false; } }; const editDimensions = (tier: ImageResolutionTier, ratio: string, field: "width" | "height", rawValue: number | null) => { if (disabled) return; const value = rawValue == null ? NaN : Math.round(rawValue); const selected = presets.find((preset) => preset.tier === tier && preset.ratio === ratio); if (!selected || !Number.isInteger(value) || value <= 0) { setErrors((current) => ({ ...current, [tier]: "请输入正整数像素" })); return; } const width = field === "width" ? value : selected.width; const height = field === "height" ? value : selected.height; try { if (width > 0 && height > 0) validateImageSize(width, height, !profile.parameters); const next = presets.map((preset) => preset === selected ? { ...preset, width, height, size: `${width}x${height}` } : preset); onChange(imageSizeConfigWithPresets(profile, next)); setErrors((current) => ({ ...current, [tier]: "" })); } catch (reason) { setErrors((current) => ({ ...current, [tier]: reason instanceof Error ? reason.message : "像素尺寸无效" })); } }; const addRatio = (tier: ImageResolutionTier) => { const ratios = (drafts[tier] || "") .split(/[,,\n]/) .map((value) => value.trim()) .filter(Boolean); if (!ratios.length) return; if (update(tier, [...presets.filter((preset) => preset.tier === tier).map((preset) => preset.ratio), ...ratios])) { setDrafts((current) => ({ ...current, [tier]: "" })); } }; return (

点选支持的比例,再次点击可移除。已有精确尺寸保持不变,有换算规则时自动填入。

{IMAGE_RESOLUTIONS.map((tier) => { const items = presets.filter((preset) => preset.tier === tier); const ratios = [...new Set([...(allowedRatios ?? IMAGE_RATIOS), ...items.map((item) => item.ratio)])]; return (
{grouped ?
{tier.toUpperCase()} {items.length ? `${items.length} 个比例` : "未启用"} {!profile.parameters ? 0} disabled={disabled} aria-label={`启用 ${tier.toUpperCase()} 规格`} onChange={(enabled) => toggleTier(tier, ratios, enabled)} /> : null}
{!items.length && DEFAULT_IMAGE_RATIOS.some((ratio) => ratios.includes(ratio)) ? : null}
: null}
{imageResolutionUsesQuality(profile) && !imageTierAvailable(profile, tier) ?

需先配置此档位对应的质量值,用户端才会开放。

: null}
{ratios.map((ratio) => { const selected = items.find((item) => item.ratio === ratio); const dimensions = selected || candidate(tier, ratio); return (
{selected ? (
) : null}
); })}
{allowCustomRatio ?
{ setDrafts((current) => ({ ...current, [tier]: event.target.value })); setErrors((current) => ({ ...current, [tier]: "" })); }} onPressEnter={(event) => { event.preventDefault(); event.stopPropagation(); addRatio(tier); }} />
: null} {errors[tier] ? ( ) : null}
); })}
默认输出 用户选择模型时的初始尺寸
{profile.size.values.map((value) => { const preset = presets.find((item) => (profile.size.parameter === "aspect_ratio" ? item.ratio === value : item.size === value)); const label = value === "auto" ? "自动" : preset && profile.size.parameter === "size" ? `${grouped ? preset.tier.toUpperCase() + " · " : ""}${preset.ratio}` : value; return ( ); })}
); }