From 9754af3bf9dd5a82ec9acb69b8aec622669e587e Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Tue, 26 May 2026 11:26:27 +0800 Subject: [PATCH] =?UTF-8?q?fix(image):=20=E4=BF=AE=E5=A4=8D=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E7=94=9F=E6=88=90=E5=92=8C=E7=BC=96=E8=BE=91=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E4=B8=AD=E5=B0=BA=E5=AF=B8=E5=8F=82=E6=95=B0=E4=BC=A0?= =?UTF-8?q?=E9=80=92=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 resolveRequestSize 函数处理尺寸参数解析逻辑 - 将 pixelSize 变量重命名为 requestSize 以提高代码可读性 - 修复当尺寸为 auto 时不发送 size 参数的功能 - 确保所有非 auto 尺寸值都会正确传递到请求中 - 更新图片生成和编辑函数中的尺寸参数处理逻辑 --- docs/pending-test.md | 2 + .../canvas-image-settings-popover.tsx | 187 +--------------- .../canvas-video-settings-popover.tsx | 167 +------------- web/src/app/(user)/image/page.tsx | 33 +-- web/src/app/(user)/video/page.tsx | 37 ++-- web/src/components/image-settings-panel.tsx | 209 ++++++++++++++++++ web/src/components/video-settings-panel.tsx | 181 +++++++++++++++ web/src/services/api/video.ts | 3 +- 8 files changed, 427 insertions(+), 392 deletions(-) create mode 100644 web/src/components/image-settings-panel.tsx create mode 100644 web/src/components/video-settings-panel.tsx diff --git a/docs/pending-test.md b/docs/pending-test.md index 6c58e61..167a4cd 100644 --- a/docs/pending-test.md +++ b/docs/pending-test.md @@ -1,5 +1,7 @@ # 待测试 +- 生图工作台的图片参数区复用画布里的紧凑版图像设置面板,尺寸、质量、生成张数的交互保持一致;工作台仍保留独立的模型选择。 +- 视频设置抽成画布和视频创作台共用的紧凑面板,清晰度、尺寸、秒数按固定网格选择并支持手动输入;尺寸选择 `auto` 时请求不传 `size`。 - 修复画布和生图工作台选择图片尺寸后,请求图片生成/编辑接口未携带 `size` 参数的问题;`auto` 不传,其余比例或像素尺寸会随请求发送。 - 修复生图工作台和画布生图请求中 `quality` 参数可能传入上游不支持值导致 400 的问题;请求前会归一化质量枚举,`auto` 或异常值不再发送给上游。 - 管理后台新增/编辑渠道时,渠道可用模型支持通过弹窗按“新获取、已有”分组选择,并可在弹窗内手动增加模型或拉取模型列表后再写回表单。 diff --git a/web/src/app/(user)/canvas/components/canvas-image-settings-popover.tsx b/web/src/app/(user)/canvas/components/canvas-image-settings-popover.tsx index f0ce3dd..e0c1f45 100644 --- a/web/src/app/(user)/canvas/components/canvas-image-settings-popover.tsx +++ b/web/src/app/(user)/canvas/components/canvas-image-settings-popover.tsx @@ -1,34 +1,13 @@ "use client"; -import { type ReactNode } from "react"; import { Settings2 } from "lucide-react"; -import { Button, ConfigProvider, Popover } from "antd"; +import { Button, Popover } from "antd"; +import { ImageSettingsPanel, imageQualityLabel, imageSizeLabel } from "@/components/image-settings-panel"; import { canvasThemes } from "@/lib/canvas-theme"; import { useThemeStore } from "@/stores/use-theme-store"; import type { AiConfig } from "@/stores/use-config-store"; -const qualityOptions = [ - { value: "auto", label: "自动" }, - { value: "high", label: "高" }, - { value: "medium", label: "中" }, - { value: "low", label: "低" }, -]; -const aspectOptions = [ - { value: "1:1", label: "1:1", width: 1024, height: 1024, icon: "square" }, - { value: "3:2", label: "3:2", width: 1536, height: 1024, icon: "landscape" }, - { value: "2:3", label: "2:3", width: 1024, height: 1536, icon: "portrait" }, - { value: "4:3", label: "4:3", width: 1344, height: 1024, icon: "landscape" }, - { value: "3:4", label: "3:4", width: 1024, height: 1344, icon: "portrait" }, - { value: "9:16", label: "9:16", width: 1024, height: 1792, icon: "portrait" }, - { value: "1:1-2k", label: "1:1(2k)", size: "2048x2048", width: 2048, height: 2048, icon: "square" }, - { value: "16:9-2k", label: "16:9(2k)", size: "2048x1152", width: 2048, height: 1152, icon: "landscape" }, - { value: "9:16-2k", label: "9:16(2k)", size: "1152x2048", width: 1152, height: 2048, icon: "portrait" }, - { value: "16:9-4k", label: "16:9(4k)", size: "3840x2160", width: 3840, height: 2160, icon: "landscape" }, - { value: "9:16-4k", label: "9:16(4k)", size: "2160x3840", width: 2160, height: 3840, icon: "portrait" }, - { value: "auto", label: "auto", width: 0, height: 0, icon: "auto" }, -]; - type CanvasImageSettingsPopoverProps = { config: AiConfig; onConfigChange: (key: keyof AiConfig, value: string) => void; @@ -44,16 +23,6 @@ export function CanvasImageSettingsPopover({ config, onConfigChange, onOpenChang const quality = config.quality || "auto"; const count = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(config.count)) || 1))); const activeSize = config.size || "auto"; - const selectedAspect = aspectOptions.find((item) => (item.size || item.value) === activeSize || item.value === activeSize); - const dimensions = readSizeDimensions(activeSize, selectedAspect || aspectOptions[0]); - const selectAspect = (value: string) => { - const option = aspectOptions.find((item) => item.value === value); - onConfigChange("size", option?.size || option?.value || "auto"); - }; - 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}`); - }; return ( triggerNode.parentElement || document.body)} onOpenChange={onOpenChange} - content={ - -
event.stopPropagation()}> -
图像设置
-
- 质量 -
- {qualityOptions.map((item) => ( - onConfigChange("quality", item.value)}> - {item.label} - - ))} -
-
-
- 尺寸 -
- updateDimension("width", value)} /> - - updateDimension("height", value)} /> -
-
-
- 宽高比 -
- {aspectOptions.map((item) => ( - - ))} -
-
-
- 生成张数 -
- {Array.from({ length: 10 }, (_, index) => index + 1).map((value) => ( - onConfigChange("count", String(value))}> - {value} 张 - - ))} - onConfigChange("count", String(value || 1))} /> -
-
-
-
- } + content={ onConfigChange(key, value)} theme={theme} />} >
); } - -export function CanvasImageSettingsTheme({ theme, children }: { theme: (typeof canvasThemes)[keyof typeof canvasThemes]; children: ReactNode }) { - return ( - - {children} - - ); -} - -function OptionPill({ selected, theme, onClick, children }: { selected: boolean; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onClick: () => void; children: ReactNode }) { - return ( - - ); -} - -function DimensionInput({ prefix, value, disabled, theme, onChange }: { prefix: string; value: number; disabled: boolean; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onChange: (value: number | null) => void }) { - return ( - - ); -} - -function CountInput({ value, theme, onChange }: { value: number; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onChange: (value: number | null) => void }) { - return ( - - ); -} - -function AspectIcon({ type, width, height, color }: { type: string; width: number; height: number; color: string }) { - if (type === "auto") return null; - const ratio = width / Math.max(1, height); - const boxWidth = ratio >= 1 ? 28 : Math.max(12, 28 * ratio); - const boxHeight = ratio >= 1 ? Math.max(12, 28 / ratio) : 28; - return ( - - - - ); -} - -function SettingTitle({ children, color }: { children: string; color: string }) { - return ( -
- {children} -
- ); -} - -function qualityLabel(value: string) { - return ({ auto: "自动", high: "高", medium: "中", low: "低" } as Record)[value] || value; -} - -function readSizeDimensions(size: string, fallback: { width: number; height: number }) { - const match = size?.match(/^(\d+)x(\d+)$/); - return { - width: match ? Number(match[1]) : fallback.width, - height: match ? Number(match[2]) : fallback.height, - }; -} diff --git a/web/src/app/(user)/canvas/components/canvas-video-settings-popover.tsx b/web/src/app/(user)/canvas/components/canvas-video-settings-popover.tsx index a748318..06a5962 100644 --- a/web/src/app/(user)/canvas/components/canvas-video-settings-popover.tsx +++ b/web/src/app/(user)/canvas/components/canvas-video-settings-popover.tsx @@ -1,26 +1,12 @@ "use client"; -import { type ReactNode } from "react"; import { Settings2 } from "lucide-react"; import { Button, Popover } from "antd"; +import { VideoSettingsPanel, videoResolutionLabel, videoSecondsLabel, videoSizeLabel } from "@/components/video-settings-panel"; import { canvasThemes } from "@/lib/canvas-theme"; import { useThemeStore } from "@/stores/use-theme-store"; import type { AiConfig } from "@/stores/use-config-store"; -import { CanvasImageSettingsTheme } from "./canvas-image-settings-popover"; - -const resolutionOptions = [ - { value: "720", label: "720p" }, - { value: "480", label: "480p" }, -]; -const sizeOptions = [ - { value: "1280x720", label: "横屏", width: 1280, height: 720 }, - { value: "720x1280", label: "竖屏", width: 720, height: 1280 }, - { value: "1024x1024", label: "方形", width: 1024, height: 1024 }, - { value: "1792x1024", label: "宽屏", width: 1792, height: 1024 }, - { value: "1024x1792", label: "长图", width: 1024, height: 1792 }, -]; -const secondOptions = [6, 10, 12, 16, 20]; type CanvasVideoSettingsPopoverProps = { config: AiConfig; @@ -31,14 +17,6 @@ type CanvasVideoSettingsPopoverProps = { export function CanvasVideoSettingsPopover({ config, onConfigChange, buttonClassName, placement = "topLeft" }: CanvasVideoSettingsPopoverProps) { const theme = canvasThemes[useThemeStore((state) => state.theme)]; - const seconds = config.videoSeconds || "6"; - const size = normalizeVideoSize(config.size); - const dimensions = readSizeDimensions(size); - const resolution = normalizeVideoResolution(config.vquality); - const updateDimension = (key: "width" | "height", value: number | null) => { - const next = Math.max(1, Math.floor(value || dimensions[key] || 720)); - onConfigChange("size", `${key === "width" ? next : dimensions.width}x${key === "height" ? next : dimensions.height}`); - }; return ( document.body} - content={ - -
event.stopPropagation()}> -
视频设置
- -
- {resolutionOptions.map((item) => ( - onConfigChange("vquality", item.value)}> - {item.label} - - ))} -
- onConfigChange("vquality", value)} /> -
- -
- updateDimension("width", value)} /> - - updateDimension("height", value)} /> -
-
- {sizeOptions.map((item) => ( - - ))} -
-
- -
- {secondOptions.map((value) => ( - onConfigChange("videoSeconds", String(value))}> - {value}s - - ))} - onConfigChange("videoSeconds", event.target.value)} - onMouseDown={(event) => event.stopPropagation()} - /> -
-
-
-
- } + content={ onConfigChange(key, value)} theme={theme} />} >
); } - -function OptionPill({ selected, theme, onClick, children }: { selected: boolean; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onClick: () => void; children: ReactNode }) { - return ( - - ); -} - -function SettingGroup({ title, color, children }: { title: string; color: string; children: ReactNode }) { - return ( -
-
- {title} -
- {children} -
- ); -} - -function ResolutionInput({ value, theme, onChange }: { value: string; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onChange: (value: string) => void }) { - return ( - - ); -} - -function DimensionInput({ prefix, value, theme, onChange }: { prefix: string; value: number; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onChange: (value: number | null) => void }) { - return ( - - ); -} - -function SizePreview({ width, height, color }: { width: number; height: number; color: string }) { - const longSide = Math.max(width, height); - const previewWidth = Math.max(12, Math.round((width / longSide) * 34)); - const previewHeight = Math.max(12, Math.round((height / longSide) * 34)); - return ; -} - -function normalizeVideoSize(value: string) { - if (/^\d+x\d+$/.test(value || "")) return value; - return ["9:16", "2:3", "3:4"].includes(value) ? "720x1280" : "1280x720"; -} - -function normalizeVideoResolution(value: string) { - if (value === "480p" || value === "low") return "480"; - if (value === "720p" || value === "auto" || value === "high" || value === "medium") return "720"; - return value.replace(/p$/i, "") || "720"; -} - -function readSizeDimensions(size: string) { - const match = size.match(/^(\d+)x(\d+)$/); - return { width: Number(match?.[1]) || 1280, height: Number(match?.[2]) || 720 }; -} - -function sizeLabel(value: string) { - return ({ "720x1280": "竖屏", "1280x720": "横屏", "1024x1024": "方形", "1792x1024": "宽屏", "1024x1792": "长图" } as Record)[value] || value; -} diff --git a/web/src/app/(user)/image/page.tsx b/web/src/app/(user)/image/page.tsx index 4640f29..ba3efcd 100644 --- a/web/src/app/(user)/image/page.tsx +++ b/web/src/app/(user)/image/page.tsx @@ -2,13 +2,16 @@ import { BookOpen, CheckSquare, ClipboardPaste, Download, FolderPlus, History, ImagePlus, LoaderCircle, PenLine, Plus, SlidersHorizontal, Sparkles, Trash2, Upload } from "lucide-react"; import { useEffect, useRef, useState } from "react"; -import { App, AutoComplete, Button, Checkbox, Drawer, Empty, Image, Input, InputNumber, Modal, Select, Tag, Typography } from "antd"; +import { App, Button, Checkbox, Drawer, Empty, Image, Input, Modal, Tag, Typography } from "antd"; import localforage from "localforage"; +import { ImageSettingsPanel } from "@/components/image-settings-panel"; import { ModelPicker } from "@/components/model-picker"; import { PromptSelectDialog } from "@/components/prompts/prompt-select-dialog"; import { AssetPickerModal, type InsertAssetPayload } from "@/app/(user)/canvas/components/asset-picker-modal"; +import { canvasThemes } from "@/lib/canvas-theme"; import { useConfigStore, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store"; +import { useThemeStore } from "@/stores/use-theme-store"; import { nanoid } from "nanoid"; import { formatBytes, formatDuration, getDataUrlByteSize, readImageMeta } from "@/lib/image-utils"; import { requestEdit, requestGeneration } from "@/services/api/image"; @@ -50,13 +53,6 @@ type GenerationLog = { type UpdateAiConfig = (key: K, value: AiConfig[K]) => void; -const sizeOptions = ["auto", "1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "9:16"].map((value) => ({ label: value, value })); -const qualityOptions = [ - { label: "auto", value: "auto" }, - { label: "low / 1K", value: "low" }, - { label: "medium / 2K", value: "medium" }, - { label: "high / 4K", value: "high" }, -]; const LOG_STORE_KEY = "infinite-canvas:image_generation_logs"; const LOG_STORE_PREFIX = `${LOG_STORE_KEY}:`; const logStore = localforage.createInstance({ name: "infinite-canvas", storeName: "image_generation_logs" }); @@ -450,8 +446,8 @@ export default function ImagePage() { onPreviewLog={(log) => void previewGenerationLog(log)} /> - setSettingsOpen(false)}> -
+ setSettingsOpen(false)}> +
@@ -465,24 +461,17 @@ export default function ImagePage() { } function GenerationSettings({ config, model, updateConfig, openConfigDialog }: { config: AiConfig; model: string; updateConfig: UpdateAiConfig; openConfigDialog: (shouldPromptContinue?: boolean) => void }) { + const theme = canvasThemes[useThemeStore((state) => state.theme)]; + return ( <> - - - + ); +} + +function CountInput({ value, max, theme, onChange }: { value: number; max: number; theme: CanvasTheme; onChange: (value: number | null) => void }) { + return ( + + ); +} + +function AspectIcon({ type, width, height, color }: { type: string; width: number; height: number; color: string }) { + if (type === "auto") return null; + const ratio = width / Math.max(1, height); + const boxWidth = ratio >= 1 ? 24 : Math.max(10, 24 * ratio); + const boxHeight = ratio >= 1 ? Math.max(10, 24 / ratio) : 24; + return ( + + + + ); +} + +function SettingTitle({ children, color }: { children: string; color: string }) { + return ( +
+ {children} +
+ ); +} + +function readSizeDimensions(size: string, fallback: { width: number; height: number }) { + const match = size?.match(/^(\d+)x(\d+)$/); + return { + width: match ? Number(match[1]) : fallback.width, + height: match ? Number(match[2]) : fallback.height, + }; +} diff --git a/web/src/components/video-settings-panel.tsx b/web/src/components/video-settings-panel.tsx new file mode 100644 index 0000000..cae6b3f --- /dev/null +++ b/web/src/components/video-settings-panel.tsx @@ -0,0 +1,181 @@ +"use client"; + +import { type ReactNode } from "react"; + +import { ImageSettingsTheme } from "@/components/image-settings-panel"; +import { type CanvasTheme } from "@/lib/canvas-theme"; +import type { AiConfig } from "@/stores/use-config-store"; + +const resolutionOptions = [ + { value: "720", label: "720p" }, + { value: "480", label: "480p" }, +]; + +const sizeOptions = [ + { value: "1280x720", label: "横屏", width: 1280, height: 720 }, + { value: "720x1280", label: "竖屏", width: 720, height: 1280 }, + { value: "1024x1024", label: "方形", width: 1024, height: 1024 }, + { value: "1792x1024", label: "宽屏", width: 1792, height: 1024 }, + { value: "1024x1792", label: "长图", width: 1024, height: 1792 }, + { value: "auto", label: "auto", width: 0, height: 0 }, +]; + +const secondOptions = [6, 10, 12, 16, 20]; + +type VideoSettingsPanelProps = { + config: AiConfig; + onConfigChange: (key: "vquality" | "size" | "videoSeconds", value: string) => void; + theme: CanvasTheme; + showTitle?: boolean; + className?: string; +}; + +export function VideoSettingsPanel({ config, onConfigChange, theme, showTitle = true, className = "w-[320px] space-y-4 rounded-2xl px-1 py-0.5" }: VideoSettingsPanelProps) { + const seconds = config.videoSeconds || "6"; + const size = normalizeVideoSizeValue(config.size); + const dimensions = readSizeDimensions(size); + const resolution = normalizeVideoResolutionValue(config.vquality); + const updateDimension = (key: "width" | "height", value: number | null) => { + const next = Math.max(1, Math.floor(value || dimensions[key] || 720)); + onConfigChange("size", `${key === "width" ? next : dimensions.width}x${key === "height" ? next : dimensions.height}`); + }; + + return ( + +
event.stopPropagation()}> + {showTitle ?
视频设置
: null} + +
+ {resolutionOptions.map((item) => ( + onConfigChange("vquality", item.value)}> + {item.label} + + ))} + onConfigChange("vquality", value)} /> +
+
+ +
+ updateDimension("width", value)} /> + + updateDimension("height", value)} /> +
+
+ {sizeOptions.map((item) => ( + + ))} +
+
+ +
+ {secondOptions.map((value) => ( + onConfigChange("videoSeconds", String(value))}> + {value}s + + ))} + onConfigChange("videoSeconds", value)} /> +
+
+
+
+ ); +} + +export function videoResolutionLabel(value: string) { + return `${normalizeVideoResolutionValue(value)}p`; +} + +export function videoSizeLabel(value: string) { + const size = normalizeVideoSizeValue(value); + return sizeOptions.find((item) => item.value === size)?.label || size; +} + +export function videoSecondsLabel(value: string) { + return `${value || "6"}s`; +} + +export function normalizeVideoSizeValue(value: string) { + if (value === "auto") return "auto"; + if (/^\d+x\d+$/.test(value || "")) return value; + return ["9:16", "2:3", "3:4"].includes(value) ? "720x1280" : "1280x720"; +} + +export function normalizeVideoResolutionValue(value: string) { + if (value === "480p" || value === "low") return "480"; + if (value === "720p" || value === "auto" || value === "high" || value === "medium") return "720"; + return value.replace(/p$/i, "") || "720"; +} + +function OptionPill({ selected, theme, onClick, children }: { selected: boolean; theme: CanvasTheme; onClick: () => void; children: ReactNode }) { + return ( + + ); +} + +function SettingGroup({ title, color, children }: { title: string; color: string; children: ReactNode }) { + return ( +
+
+ {title} +
+ {children} +
+ ); +} + +function ResolutionInput({ value, theme, onChange }: { value: string; theme: CanvasTheme; onChange: (value: string) => void }) { + return ( + + ); +} + +function DimensionInput({ prefix, value, disabled, theme, onChange }: { prefix: string; value: number; disabled: boolean; theme: CanvasTheme; onChange: (value: number | null) => void }) { + return ( + + ); +} + +function NumberInput({ value, min, max, theme, onChange }: { value: string; min: number; max: number; theme: CanvasTheme; onChange: (value: string) => void }) { + return onChange(event.target.value)} onMouseDown={(event) => event.stopPropagation()} />; +} + +function SizePreview({ width, height, color }: { width: number; height: number; color: string }) { + if (!width || !height) return null; + const longSide = Math.max(width, height); + const previewWidth = Math.max(10, Math.round((width / longSide) * 26)); + const previewHeight = Math.max(10, Math.round((height / longSide) * 26)); + return ; +} + +function readSizeDimensions(size: string) { + if (size === "auto") return { width: 0, height: 0 }; + const match = size.match(/^(\d+)x(\d+)$/); + return { width: Number(match?.[1]) || 1280, height: Number(match?.[2]) || 720 }; +} diff --git a/web/src/services/api/video.ts b/web/src/services/api/video.ts index ef78daa..863bcab 100644 --- a/web/src/services/api/video.ts +++ b/web/src/services/api/video.ts @@ -53,10 +53,11 @@ export async function requestVideoGeneration(config: AiConfig, prompt: string, r function normalizeVideoSeconds(value: string) { const seconds = Math.floor(Number(value) || 6); - return String([6, 10, 12, 16, 20].includes(seconds) ? seconds : 6); + return String(Math.max(1, Math.min(20, seconds))); } function normalizeVideoSize(value: string) { + if (value === "auto") return null; const size = value || "1280x720"; if (/^\d+x\d+$/.test(size)) return size; return ["9:16", "2:3", "3:4"].includes(size) ? "720x1280" : "1280x720";