feat: 多页面拖拽上传、滚动条精简、UI优化

- 剧本评测/分辨率提升/数字人/角色迁移/图片工作台/去水印/电商:新增外部拖拽文件上传
- 电商:爆款图复刻上传框支持拖拽+大滚动条,短视频/模特图/详情图滚动条精简回退
- 图片工作台:右侧输出面板移至左侧提示词上方,删除局部重绘遮罩/结果框
- 数字人:生成按钮改为「开始生成」
- 局部重绘:编辑遮罩→编辑页面
- 对话框生成器:新增对话/视频模式、模型/速度/深度选择按钮
- 视频时长默认改为5秒
- 工具箱页面空状态logo统一绿底亮色图标
- 多处CSS滚动条和布局优化
This commit is contained in:
OmniAI Developer
2026-06-05 18:01:55 +08:00
parent 8fbb2ec95e
commit 5b87594e36
22 changed files with 1796 additions and 195 deletions
@@ -23,7 +23,7 @@ import {
TableOutlined,
ThunderboltOutlined,
} from "@ant-design/icons";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState, type DragEvent } from "react";
import type { WebImageWorkbenchTool, WebViewKey } from "../../types";
import { aiGenerationClient } from "../../api/aiGenerationClient";
import { waitForTask } from "../../api/taskSubscription";
@@ -138,6 +138,8 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
const [downloadingResultUrl, setDownloadingResultUrl] = useState<string | null>(null);
const [savingAssetResultUrl, setSavingAssetResultUrl] = useState<string | null>(null);
const [generationError, setGenerationError] = useState<string | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [isCameraDragging, setIsCameraDragging] = useState(false);
const abortRef = useRef(false);
const taskIdRef = useRef<string | null>(null);
const keepaliveRestoredRef = useRef(false);
@@ -229,6 +231,37 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
event.target.value = "";
};
const handleDragOver = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
setIsDragging(true);
};
const handleDragLeave = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
setIsDragging(false);
};
const handleDrop = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
setIsDragging(false);
const files = Array.from(event.dataTransfer.files).filter((f) => f.type.startsWith('image/'));
if (!files.length) return;
const selectedFiles = mode === 'blend' ? files : files.slice(0, 1);
selectedFiles.forEach((file) => {
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result !== 'string') return;
setReferenceImages((current) => (mode === 'blend' ? [...current, reader.result as string] : [reader.result as string]));
setStatus(mode === 'blend' ? `已追加 ${file.name}` : `已导入 ${file.name}`);
};
reader.readAsDataURL(file);
});
};
const handleAddUrl = () => {
const nextUrl = imageUrlInput.trim();
if (!nextUrl) return;
@@ -261,9 +294,15 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
event.target.value = "";
};
const handleInpaintDrop = (event: React.DragEvent) => {
event.preventDefault();
const file = Array.from(event.dataTransfer.files).find((f) => f.type.startsWith("image/"));
const [isInpaintDragging, setIsInpaintDragging] = useState(false);
const handleInpaintDragOver = (e: DragEvent<HTMLDivElement>) => { e.preventDefault(); e.stopPropagation(); if (e.dataTransfer.types.includes("Files")) setIsInpaintDragging(true); };
const handleInpaintDragLeave = (e: DragEvent<HTMLDivElement>) => { e.preventDefault(); e.stopPropagation(); if (e.currentTarget === e.target || !e.currentTarget.contains(e.relatedTarget as Node)) setIsInpaintDragging(false); };
const handleInpaintDrop = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsInpaintDragging(false);
const file = Array.from(e.dataTransfer.files).find((f) => f.type.startsWith("image/"));
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
@@ -302,7 +341,7 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
return;
}
if (!hasMask) {
setStatus("请先编辑遮罩,涂抹需要重绘的区域");
setStatus("请先编辑页面,涂抹需要重绘的区域");
return;
}
if (generating) return;
@@ -364,6 +403,33 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
event.target.value = "";
};
const handleCameraDragOver = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
setIsCameraDragging(true);
};
const handleCameraDragLeave = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
setIsCameraDragging(false);
};
const handleCameraDrop = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
setIsCameraDragging(false);
const file = Array.from(event.dataTransfer.files).find((f) => f.type.startsWith('image/'));
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result !== 'string') return;
setCameraImage(reader.result);
setStatus(`已导入镜头参考图 ${file.name}`);
};
reader.readAsDataURL(file);
};
const handleAddCameraUrl = () => {
const nextUrl = cameraUrlInput.trim();
if (!nextUrl) return;
@@ -696,7 +762,13 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
accept="image/png,image/jpeg,image/webp"
onChange={handleInpaintFileChange}
/>
<div className="image-workbench-upload-shell">
<div
className={`image-workbench-upload-shell${isInpaintDragging ? " is-dragging" : ""}`}
onDragOver={handleInpaintDragOver}
onDragLeave={handleInpaintDragLeave}
onDrop={handleInpaintDrop}
>
{isInpaintDragging ? <div className="image-workbench-upload-drop-overlay"><span></span></div> : null}
<button type="button" className="image-workbench-upload" onClick={() => inpaintFileInputRef.current?.click()}>
{inpaintImage ? <img src={inpaintImage} alt="" /> : <FileImageOutlined />}
<strong>{inpaintImage ? "更换原图" : "选择图片"}</strong>
@@ -789,7 +861,7 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
<img src={inpaintResultImages[0]} alt="重绘结果" style={{ maxWidth: "95%", maxHeight: "95%", borderRadius: 8, objectFit: "contain" }} />
<div className="image-workbench-inpaint-bottom-bar">
<button type="button" className="image-workbench-inpaint-edit-btn" onClick={() => { setInpaintResultImages([]); setIsMaskEditing(true); setInpaintTool("brush"); setCanvasInitCounter((c) => c + 1); }}>
<HighlightOutlined />
<HighlightOutlined />
</button>
{renderResultActions(inpaintResultImages[0], 0)}
{inpaintResultImages.length > 1 && (
@@ -845,7 +917,7 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
<div className="image-workbench-inpaint-bottom-bar">
{!isMaskEditing && (
<button type="button" className="image-workbench-inpaint-edit-btn" onClick={() => { setInpaintTool("brush"); setIsMaskEditing(true); }}>
<HighlightOutlined /> {hasMask ? "重新编辑遮罩" : "编辑遮罩"}
<HighlightOutlined /> {hasMask ? "重新编辑页面" : "编辑页面"}
</button>
)}
<span className="image-workbench-inpaint-zoom-controls">
@@ -858,11 +930,13 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
) : (
<button
type="button"
className="image-workbench-empty image-workbench-empty--button"
className={`image-workbench-empty image-workbench-empty--button${isInpaintDragging ? " is-dragging" : ""}`}
onClick={() => inpaintFileInputRef.current?.click()}
onDragOver={(e) => e.preventDefault()}
onDragOver={handleInpaintDragOver}
onDragLeave={handleInpaintDragLeave}
onDrop={handleInpaintDrop}
>
{isInpaintDragging ? <span className="image-workbench-upload-drop-overlay" style={{ borderRadius: "var(--radius-sm)" }}><span></span></span> : null}
<FileImageOutlined />
<strong></strong>
<span> PNG / JPG / WebP</span>
@@ -870,36 +944,6 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
)}
</section>
<aside className="image-workbench-panel image-workbench-panel--right">
<section className="image-workbench-right-note">
<div className="image-workbench-section-title">
<h3></h3>
<span>{isMaskEditing ? (inpaintTool === "eraser" ? "橡皮中" : "画笔中") : hasMask ? "已保存" : "待编辑"}</span>
</div>
<span>{inpaintImage ? (hasMask ? "遮罩区域已标记,可开始重绘。" : "点击画布上的「编辑遮罩」开始涂抹。") : "上传原图后可编辑遮罩"}</span>
</section>
<section className="image-workbench-right-note">
<div className="image-workbench-section-title">
<h3></h3>
<span>{inpaintResultImages.length > 0 ? `${inpaintResultImages.length}` : "待生成"}</span>
</div>
{inpaintResultImages.length > 0 ? (
<div className="image-workbench-result-grid">
{inpaintResultImages.map((url, i) => (
<div key={url} className="image-workbench-result-card">
<a href={url} target="_blank" rel="noopener noreferrer" className="image-workbench-result-thumb">
<img src={url} alt={`重绘结果 ${i + 1}`} />
</a>
{renderResultActions(url, i)}
</div>
))}
</div>
) : (
<span></span>
)}
</section>
</aside>
</main>
) : activeTool === "camera" ? (
<main className="image-workbench-layout image-workbench-layout--camera">
@@ -915,7 +959,13 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
accept="image/png,image/jpeg,image/webp"
onChange={handleCameraFileChange}
/>
<div className="image-workbench-upload-shell">
<div
className={`image-workbench-upload-shell${isCameraDragging ? " is-dragging" : ""}`}
onDragOver={handleCameraDragOver}
onDragLeave={handleCameraDragLeave}
onDrop={handleCameraDrop}
>
{isCameraDragging && <div className="image-workbench-upload-overlay"></div>}
<button type="button" className="image-workbench-upload" onClick={() => cameraFileInputRef.current?.click()}>
{cameraImage ? <img src={cameraImage} alt="" /> : <FileImageOutlined />}
<strong>{cameraImage ? "更换参考图" : "导入参考图"}</strong>
@@ -1194,7 +1244,13 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
</button>
</div>
) : (
<div className="image-workbench-upload-shell">
<div
className={`image-workbench-upload-shell${isDragging ? " is-dragging" : ""}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{isDragging && <div className="image-workbench-upload-overlay"></div>}
<button type="button" className="image-workbench-upload" onClick={() => fileInputRef.current?.click()}>
{referenceImage ? <img src={referenceImage} alt="" /> : <FileImageOutlined />}
<strong>{referenceImage ? "更换参考图" : "导入参考图"}</strong>
@@ -1225,6 +1281,33 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
</div>
</section>
<section className="image-workbench-control-card">
<h3></h3>
<span className="image-workbench-field-label"></span>
<div className="image-workbench-segmented">
{(["9:16", "16:9", "4:3", "3:4", "1:1"] as OutputSize[]).map((s) => (
<button key={s} type="button" className={outputSize === s ? "is-active" : ""} onClick={() => setOutputSize(s)}>
{s}
</button>
))}
</div>
<div className="image-workbench-count">
<span></span>
<div>
{([1, 2, 3, 4] as OutputCount[]).map((count) => (
<button
key={count}
type="button"
className={outputCount === count ? "is-active" : ""}
onClick={() => setOutputCount(count)}
>
{count}
</button>
))}
</div>
</div>
</section>
<section className="image-workbench-control-card image-workbench-prompt">
<h3></h3>
<textarea
@@ -1297,34 +1380,6 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
)}
</section>
<aside className="image-workbench-panel image-workbench-panel--right">
<section className="image-workbench-control-card">
<h3></h3>
<span className="image-workbench-field-label"></span>
<div className="image-workbench-segmented">
{(["9:16", "16:9", "4:3", "3:4", "1:1"] as OutputSize[]).map((s) => (
<button key={s} type="button" className={outputSize === s ? "is-active" : ""} onClick={() => setOutputSize(s)}>
{s}
</button>
))}
</div>
<div className="image-workbench-count">
<span></span>
<div>
{([1, 2, 3, 4] as OutputCount[]).map((count) => (
<button
key={count}
type="button"
className={outputCount === count ? "is-active" : ""}
onClick={() => setOutputCount(count)}
>
{count}
</button>
))}
</div>
</div>
</section>
</aside>
</main>
)}