feat(image): enhance image generation settings with dimension snapping and thumbnail filtering

This commit is contained in:
HouYunFei
2026-06-02 16:00:20 +08:00
parent 50176d8c7a
commit d12cceeecf
4 changed files with 56 additions and 15 deletions
@@ -42,6 +42,7 @@ export function CanvasImageSettingsPopover({ config, onConfigChange, onOpenChang
const target = event.target;
if (!(target instanceof Node)) return;
if (buttonRef.current?.contains(target) || panelRef.current?.contains(target)) return;
if (document.activeElement instanceof HTMLElement && panelRef.current?.contains(document.activeElement)) document.activeElement.blur();
setOpen(false);
onOpenChange?.(false);
};
+7 -5
View File
@@ -465,7 +465,7 @@ export default function ImagePage() {
onPreviewLog={(log) => void previewGenerationLog(log)}
/>
</Drawer>
<Drawer title="参数" placement="bottom" height="82vh" open={settingsOpen} onClose={() => setSettingsOpen(false)}>
<Drawer title="参数" placement="bottom" size="82vh" open={settingsOpen} onClose={() => setSettingsOpen(false)}>
<div className="grid grid-cols-2 gap-3 pb-4">
<GenerationSettings config={effectiveConfig} model={model} updateConfig={updateConfig} openConfigDialog={openConfigDialog} />
</div>
@@ -638,6 +638,8 @@ function LogPanel({
}
function LogCard({ log, selected, active, onSelectedChange, onClick }: { log: GenerationLog; selected: boolean; active: boolean; onSelectedChange: (checked: boolean) => void; onClick: () => void }) {
const thumbnails = (log.thumbnails || []).filter(Boolean).slice(0, 4);
return (
<button
type="button"
@@ -649,9 +651,9 @@ function LogCard({ log, selected, active, onSelectedChange, onClick }: { log: Ge
<Checkbox className="mt-0.5" checked={selected} onClick={(event) => event.stopPropagation()} onChange={(event) => onSelectedChange(event.target.checked)} />
<div className="min-w-0">
<div className="truncate text-sm font-semibold leading-5">{log.title}</div>
{log.thumbnails?.length ? (
{thumbnails.length ? (
<div className="mt-2 flex gap-1 overflow-hidden">
{log.thumbnails.slice(0, 4).map((image, index) => (
{thumbnails.map((image, index) => (
<img key={`${log.id}-${index}`} src={image} alt="" className="size-8 shrink-0 rounded-md object-cover" />
))}
</div>
@@ -729,7 +731,7 @@ async function normalizeLog(log: Partial<GenerationLog>): Promise<GenerationLog>
quality: log.quality || config.quality || "",
status: log.status || "成功",
images,
thumbnails: images.map((image) => image.dataUrl),
thumbnails: images.map((image) => image.dataUrl).filter(Boolean),
};
}
@@ -815,6 +817,6 @@ function buildLog({
quality: logConfig.quality,
status,
images,
thumbnails: images.map((image) => image.dataUrl),
thumbnails: images.map((image) => image.dataUrl).filter(Boolean),
};
}
+46 -10
View File
@@ -1,7 +1,7 @@
"use client";
import { type ReactNode } from "react";
import { ConfigProvider } from "antd";
import { type ReactNode, useState } from "react";
import { ConfigProvider, Switch } from "antd";
import { type CanvasTheme } from "@/lib/canvas-theme";
import type { AiConfig } from "@/stores/use-config-store";
@@ -12,6 +12,7 @@ const qualityOptions = [
{ value: "medium", label: "中" },
{ value: "low", label: "低" },
];
const DIMENSION_STEP = 16;
const aspectOptions = [
{ value: "1:1", label: "1:1", width: 1024, height: 1024, icon: "square" },
@@ -40,6 +41,7 @@ type ImageSettingsPanelProps = {
};
export function ImageSettingsPanel({ config, onConfigChange, theme, showTitle = true, className = "w-[320px] space-y-4 rounded-2xl px-1 py-0.5", maxCount = 15, quickCount = 10 }: ImageSettingsPanelProps) {
const [snapDimensionToStep, setSnapDimensionToStep] = useState(true);
const quality = config.quality || "auto";
const count = Math.max(1, Math.min(maxCount, Math.floor(Math.abs(Number(config.count)) || 1)));
const activeSize = config.size || "auto";
@@ -51,12 +53,22 @@ export function ImageSettingsPanel({ config, onConfigChange, theme, showTitle =
};
const updateDimension = (key: "width" | "height", value: number | null) => {
const next = Math.max(1, Math.floor(value || dimensions[key] || 1024));
onConfigChange("size", `${key === "width" ? next : dimensions.width}x${key === "height" ? next : dimensions.height}`);
const width = key === "width" ? next : dimensions.width;
const height = key === "height" ? next : dimensions.height;
onConfigChange("size", `${alignDimension(width, snapDimensionToStep)}x${alignDimension(height, snapDimensionToStep)}`);
};
return (
<ImageSettingsTheme theme={theme}>
<div className={className} style={{ color: theme.node.text }} onMouseDown={(event) => event.stopPropagation()}>
<div
className={className}
style={{ color: theme.node.text }}
onMouseDown={(event) => {
event.stopPropagation();
if (event.target instanceof HTMLInputElement) return;
if (document.activeElement instanceof HTMLInputElement && event.currentTarget.contains(document.activeElement)) document.activeElement.blur();
}}
>
{showTitle ? <div className="text-lg font-semibold"></div> : null}
<div className="space-y-2.5">
<SettingTitle color={theme.node.muted}></SettingTitle>
@@ -69,11 +81,21 @@ export function ImageSettingsPanel({ config, onConfigChange, theme, showTitle =
</div>
</div>
<div className="space-y-2.5">
<SettingTitle color={theme.node.muted}></SettingTitle>
<div className="flex items-center justify-between gap-3">
<SettingTitle color={theme.node.muted}></SettingTitle>
<div className="flex items-center gap-2">
<span className="text-xs font-medium" style={{ color: theme.node.muted }}>
16
</span>
<span title="输入完成后自动向上补成 16 的倍数" onMouseDown={(event) => event.stopPropagation()}>
<Switch size="small" checked={snapDimensionToStep} onChange={setSnapDimensionToStep} />
</span>
</div>
</div>
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-2.5">
<DimensionInput prefix="W" value={dimensions.width} disabled={activeSize === "auto"} theme={theme} onChange={(value) => updateDimension("width", value)} />
<DimensionInput prefix="W" value={dimensions.width} disabled={activeSize === "auto"} theme={theme} alignToStep={snapDimensionToStep} onChange={(value) => updateDimension("width", value)} />
<span className="text-lg opacity-45"></span>
<DimensionInput prefix="H" value={dimensions.height} disabled={activeSize === "auto"} theme={theme} onChange={(value) => updateDimension("height", value)} />
<DimensionInput prefix="H" value={dimensions.height} disabled={activeSize === "auto"} theme={theme} alignToStep={snapDimensionToStep} onChange={(value) => updateDimension("height", value)} />
</div>
</div>
<div className="space-y-2.5">
@@ -145,7 +167,13 @@ function OptionPill({ selected, theme, onClick, children }: { selected: boolean;
);
}
function DimensionInput({ prefix, value, disabled, theme, onChange }: { prefix: string; value: number; disabled: boolean; theme: CanvasTheme; onChange: (value: number | null) => void }) {
function DimensionInput({ prefix, value, disabled, theme, alignToStep, onChange }: { prefix: string; value: number; disabled: boolean; theme: CanvasTheme; alignToStep: boolean; onChange: (value: number | null) => void }) {
const commit = (input: HTMLInputElement) => {
const next = alignDimension(Math.max(1, Math.floor(Number(input.value) || value || 1024)), alignToStep);
input.value = String(next);
onChange(next);
};
return (
<label className="flex h-9 overflow-hidden rounded-xl text-sm" style={{ background: theme.node.fill, color: theme.node.text, opacity: disabled ? 0.55 : 1 }}>
<span className="grid w-9 place-items-center" style={{ color: theme.node.muted }}>
@@ -156,8 +184,12 @@ function DimensionInput({ prefix, value, disabled, theme, onChange }: { prefix:
min={1}
disabled={disabled}
className="min-w-0 flex-1 bg-transparent px-2 outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
value={value || ""}
onChange={(event) => onChange(Number(event.target.value) || null)}
defaultValue={value || ""}
key={`${prefix}-${value}`}
onBlur={(event) => commit(event.currentTarget)}
onKeyDown={(event) => {
if (event.key === "Enter") event.currentTarget.blur();
}}
onMouseDown={(event) => event.stopPropagation()}
/>
</label>
@@ -208,3 +240,7 @@ function readSizeDimensions(size: string, fallback: { width: number; height: num
height: match ? Number(match[2]) : fallback.height,
};
}
function alignDimension(value: number, enabled: boolean) {
return enabled ? Math.ceil(value / DIMENSION_STEP) * DIMENSION_STEP : value;
}