mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-07-24 06:54:06 +08:00
feat(image): enhance image generation settings with dimension snapping and thumbnail filtering
This commit is contained in:
@@ -22,6 +22,8 @@ description: 当前版本已实现但仍需人工验证的变更项
|
||||
- 未登录状态下,画布右上角不再显示用户头像菜单、用户名称、算力点余额和退出登录入口,改为显示登录入口;快捷键入口仍可直接打开。
|
||||
- 生图工作台的图片参数区复用画布里的紧凑版图像设置面板,尺寸、质量、生成张数的交互保持一致;工作台仍保留独立的模型选择。
|
||||
- 生图工作台新增生成记录配置持久化:每次生成会保存提示词、参考图、模型、质量、尺寸和张数,结果图写入本地图片存储后记录只保存 `storageKey`;点击历史记录会回填本次生成配置并预览结果。
|
||||
- 生图工作台生成记录会过滤空缩略图地址,避免历史记录卡片渲染 `src=""` 图片。
|
||||
- 图像设置面板新增默认开启的“16倍数对齐”尺寸 toggle;开启时手动输入宽高并失焦、回车或点击浮层外关闭后,会把非 16 倍数的宽高向上补齐。
|
||||
- 视频设置抽成画布和视频创作台共用的紧凑面板,清晰度、尺寸、秒数按固定网格选择并支持手动输入;尺寸选择 `auto` 时请求不传 `size`。
|
||||
- 修复画布和生图工作台选择图片尺寸后,请求图片生成/编辑接口未携带 `size` 参数的问题;`auto` 不传,其余比例或像素尺寸会随请求发送。
|
||||
- 修复生图工作台和画布生图请求中 `quality` 参数可能传入上游不支持值导致 400 的问题;请求前会归一化质量枚举,`auto` 或异常值不再发送给上游。
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user