feat(image): 以插件声明统一图片参数、尺寸选择与定价合同 (#760)
* feat(image): 图片参数 - 统一插件声明配置与生成合同 * docs(image): 图片参数 - 说明声明合同与验证边界 * fix(plugins): 图片声明 - 保留内联素材支持并同步制品文档 * fix(image): 参数合同 - 对齐审批意图和尺寸预检调用方 * fix(image): 调用链 - 保留公开意图并接通独立分辨率 * fix(image): 图片参数 - 补齐分辨率接线与目录价格投影
This commit is contained in:
1 parent
88de4ce422
commit
7466119867
104 files changed
+6013
-568
No files matched your search
@@ -4,16 +4,21 @@ 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 { IMAGE_RATIOS, imageResolutionTiers, imagePresetForRatio, imageResolutionUsesQuality, imageSizeConfigWithPresets, imageSizePresets, imageTierAvailable } from "@/lib/image-size-presets";
|
||||
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 }: { profile: ImageCapabilityConfig; disabled?: boolean; onChange: (size: ImageCapabilityConfig["size"]) => void }) {
|
||||
const IMAGE_RESOLUTIONS = imageResolutionTiers(profile);
|
||||
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<Partial<Record<ImageResolutionTier, string>>>({});
|
||||
const [errors, setErrors] = useState<Partial<Record<ImageResolutionTier, string>>>({});
|
||||
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 : []);
|
||||
@@ -24,8 +29,8 @@ export function ImageSizePresetsEditor({ profile, disabled, onChange }: { profil
|
||||
const next = [
|
||||
...new Map(
|
||||
ratios.map((ratio) => {
|
||||
const normalized = imagePresetForRatio(tier, ratio);
|
||||
const preset = presets.find((preset) => preset.tier === tier && preset.ratio === normalized.ratio) || normalized;
|
||||
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(),
|
||||
@@ -49,7 +54,7 @@ export function ImageSizePresetsEditor({ profile, disabled, onChange }: { profil
|
||||
const width = field === "width" ? value : selected.width;
|
||||
const height = field === "height" ? value : selected.height;
|
||||
try {
|
||||
validateImageSize(width, height);
|
||||
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]: "" }));
|
||||
@@ -69,34 +74,35 @@ export function ImageSizePresetsEditor({ profile, disabled, onChange }: { profil
|
||||
};
|
||||
return (
|
||||
<div className="image-size-presets-editor">
|
||||
<p className="image-size-editor-intro">点选支持的比例,再次点击可移除。尺寸自动换算,已有精确尺寸保持不变。</p>
|
||||
<p className="image-size-editor-intro">点选支持的比例,再次点击可移除。已有精确尺寸保持不变,有换算规则时自动填入。</p>
|
||||
{IMAGE_RESOLUTIONS.map((tier) => {
|
||||
const items = presets.filter((preset) => preset.tier === tier);
|
||||
const ratios = [...new Set([...IMAGE_RATIOS, ...items.map((item) => item.ratio)])];
|
||||
const ratios = [...new Set([...(allowedRatios ?? IMAGE_RATIOS), ...items.map((item) => item.ratio)])];
|
||||
return (
|
||||
<section key={tier} className="image-size-preset-group" aria-labelledby={`${id}-${tier}-title`}>
|
||||
<header className="image-size-preset-heading">
|
||||
<section key={tier} className={`image-size-preset-group${grouped ? "" : " is-ungrouped"}`} aria-label={grouped ? undefined : "比例配置"} aria-labelledby={grouped ? `${id}-${tier}-title` : undefined}>
|
||||
{grouped ? <header className="image-size-preset-heading">
|
||||
<div className="image-size-preset-heading-label">
|
||||
<strong id={`${id}-${tier}-title`}>{tier.toUpperCase()}</strong>
|
||||
<span>{items.length ? `${items.length} 个比例` : "未启用"}</span>
|
||||
<Switch size="sm" checked={items.length > 0} disabled={disabled} aria-label={`启用 ${tier.toUpperCase()} 规格`} onChange={(enabled) => toggleTier(tier, ratios, enabled)} />
|
||||
{!profile.parameters ? <Switch size="sm" checked={items.length > 0} disabled={disabled} aria-label={`启用 ${tier.toUpperCase()} 规格`} onChange={(enabled) => toggleTier(tier, ratios, enabled)} /> : null}
|
||||
</div>
|
||||
</header>
|
||||
{!items.length && DEFAULT_IMAGE_RATIOS.some((ratio) => ratios.includes(ratio)) ? <Button size="small" disabled={disabled} onClick={() => update(tier, DEFAULT_IMAGE_RATIOS.filter((ratio) => ratios.includes(ratio)))}>选择常用比例</Button> : null}
|
||||
</header> : null}
|
||||
<div className="image-size-preset-body">
|
||||
{imageResolutionUsesQuality(profile) && !imageTierAvailable(profile, tier) ? <p className="image-size-hint">需先配置此档位对应的质量值,用户端才会开放。</p> : null}
|
||||
<div className="image-size-preset-options" role="group" aria-label={`${tier.toUpperCase()} 支持的宽高比`}>
|
||||
<div className="image-size-preset-options" role="group" aria-label={grouped ? `${tier.toUpperCase()} 支持的宽高比` : "支持的宽高比"}>
|
||||
{ratios.map((ratio) => {
|
||||
const selected = items.find((item) => item.ratio === ratio);
|
||||
const dimensions = selected || imagePresetForRatio(tier, ratio);
|
||||
const dimensions = selected || candidate(tier, ratio);
|
||||
return (
|
||||
<div key={ratio} className="image-size-preset-option-wrap">
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
aria-pressed={Boolean(selected)}
|
||||
aria-label={`${tier.toUpperCase()} ${ratio}`}
|
||||
aria-label={grouped ? `${tier.toUpperCase()} ${ratio}` : ratio}
|
||||
className="image-size-preset-option"
|
||||
title={`${ratio} · ${dimensions.width} × ${dimensions.height} px${selected ? " · 点击移除" : " · 点击添加"}`}
|
||||
title={`${ratio}${dimensions.width && dimensions.height ? ` · ${dimensions.width} × ${dimensions.height} px` : ""}${selected ? " · 点击移除" : " · 点击添加"}`}
|
||||
onClick={() => update(tier, selected ? items.filter((item) => item.ratio !== ratio).map((item) => item.ratio) : [...items.map((item) => item.ratio), ratio])}
|
||||
>
|
||||
<span className="image-size-preset-option-label">
|
||||
@@ -104,19 +110,19 @@ export function ImageSizePresetsEditor({ profile, disabled, onChange }: { profil
|
||||
{selected ? <Check size={12} aria-hidden="true" /> : <Plus size={12} aria-hidden="true" />}
|
||||
</span>
|
||||
<small>
|
||||
{dimensions.width} × {dimensions.height}
|
||||
{dimensions.width && dimensions.height ? `${dimensions.width} × ${dimensions.height}` : "设置尺寸"}
|
||||
</small>
|
||||
</button>
|
||||
{selected ? (
|
||||
<div className="image-size-preset-dimensions" aria-label={`${tier.toUpperCase()} ${ratio} 精确像素`}>
|
||||
<label>
|
||||
<span>宽</span>
|
||||
<InputNumber min={1} precision={0} value={selected.width} disabled={disabled} aria-label={`${tier.toUpperCase()} ${ratio} 宽度像素`} onChange={(value) => editDimensions(tier, ratio, "width", value)} />
|
||||
<InputNumber controls={false} min={1} precision={0} value={selected.width || null} disabled={disabled} aria-label={`${grouped ? tier.toUpperCase() + " " : ""}${ratio} 宽度像素`} onChange={(value) => editDimensions(tier, ratio, "width", value)} />
|
||||
</label>
|
||||
<span aria-hidden="true">×</span>
|
||||
<label>
|
||||
<span>高</span>
|
||||
<InputNumber min={1} precision={0} value={selected.height} disabled={disabled} aria-label={`${tier.toUpperCase()} ${ratio} 高度像素`} onChange={(value) => editDimensions(tier, ratio, "height", value)} />
|
||||
<InputNumber controls={false} min={1} precision={0} value={selected.height || null} disabled={disabled} aria-label={`${grouped ? tier.toUpperCase() + " " : ""}${ratio} 高度像素`} onChange={(value) => editDimensions(tier, ratio, "height", value)} />
|
||||
</label>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -124,7 +130,7 @@ export function ImageSizePresetsEditor({ profile, disabled, onChange }: { profil
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="image-size-preset-add">
|
||||
{allowCustomRatio ? <div className="image-size-preset-add">
|
||||
<Input
|
||||
aria-label={`${tier.toUpperCase()} 自定义比例`}
|
||||
aria-invalid={Boolean(errors[tier])}
|
||||
@@ -145,7 +151,7 @@ export function ImageSizePresetsEditor({ profile, disabled, onChange }: { profil
|
||||
<Button disabled={disabled || !drafts[tier]?.trim()} icon={<Plus size={14} />} onClick={() => addRatio(tier)} aria-label={`添加 ${tier.toUpperCase()} 比例`}>
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
</div> : null}
|
||||
{errors[tier] ? (
|
||||
<p id={`${id}-${tier}-error`} role="alert" className="image-size-error">
|
||||
{errors[tier]}
|
||||
@@ -163,7 +169,7 @@ export function ImageSizePresetsEditor({ profile, disabled, onChange }: { profil
|
||||
<div className="image-size-default-options" role="group" aria-label="默认输出">
|
||||
{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" ? `${preset.tier.toUpperCase()} · ${preset.ratio}` : value;
|
||||
const label = value === "auto" ? "自动" : preset && profile.size.parameter === "size" ? `${grouped ? preset.tier.toUpperCase() + " · " : ""}${preset.ratio}` : value;
|
||||
return (
|
||||
<button key={value} type="button" disabled={disabled} aria-pressed={profile.size.default === value} title={value} aria-label={`默认输出 ${label} ${value}`} onClick={() => onChange({ ...profile.size, default: value })}>
|
||||
{label}
|
||||
|
||||
Reference in new issue
Block a user