本次更新对多个功能页面进行了系统性的 UI/UX 打磨,统一了交互模式并补充了缺失的状态反馈。 ## 新增功能 - WorkbenchPage: 图片提示词案例区域新增加载骨架屏、错误回退、空数据三种状态展示 - CharacterMixPage: 新增左侧设置面板(驱动提示词、图像检测开关、水印开关),支持清除已上传的人物图/参考视频 - DigitalHumanPage: 新增左侧设置面板(提示词输入、去水印/保留原声开关),支持清除已上传的人像/音频,增加取消生成按钮 - ImageWorkbenchPage / ResolutionUpscalePage: 新增参数设置面板和资产清除交互 - MorePage: 新增页面入口 ## UI 优化 - 统一 Toggle 开关组件: 所有设置页面采用一致的 .studio-toggle 交互模式 - 资产清除: 各上传区域新增清除按钮,含二次确认和提示反馈 - 生成按钮: 统一为带图标的 .studio-generate-btn,增加 disabled/loading 状态 - ConversationSidebar / ProjectSidebar: 侧边栏交互细节优化 ## 样式升级 - image-workbench.css: 大幅扩展样式 (+1900 行),覆盖设置面板、上传区、结果展示等 - workbench.css: 新增 666 行样式,含骨架屏动画、案例卡片网格、状态占位等 - subtitle-removal.css: 补充设置面板样式
This commit is contained in:
@@ -281,6 +281,94 @@ function CharacterMixPage({
|
||||
}
|
||||
};
|
||||
|
||||
const clearCharacterAsset = () => {
|
||||
if (characterPreview) URL.revokeObjectURL(characterPreview);
|
||||
setCharacterFile("");
|
||||
setCharacterPreview("");
|
||||
setCharacterDataUrl("");
|
||||
setFaceHint(null);
|
||||
if (characterInputRef.current) characterInputRef.current.value = "";
|
||||
setNotice("已移除人物图");
|
||||
};
|
||||
|
||||
const clearReferenceVideo = () => {
|
||||
if (videoPreview) URL.revokeObjectURL(videoPreview);
|
||||
setVideoFile("");
|
||||
setVideoPreview("");
|
||||
setVideoDataUrl("");
|
||||
if (videoInputRef.current) videoInputRef.current.value = "";
|
||||
setNotice("已移除参考视频");
|
||||
};
|
||||
|
||||
const characterMixSettingsPanel = (
|
||||
<div className="studio-panel__section character-mix-settings-panel">
|
||||
<div className="studio-panel__section-head">
|
||||
<span className="studio-panel__section-title">迁移设置</span>
|
||||
</div>
|
||||
<div className="studio-panel__section-body">
|
||||
<div className="character-mix-prompt-field">
|
||||
<div className="studio-label">驱动提示词</div>
|
||||
<textarea
|
||||
value={promptInput}
|
||||
onChange={(e) => setPromptInput(e.target.value)}
|
||||
placeholder="保持角色原有服装,动作流畅自然"
|
||||
rows={3}
|
||||
maxLength={1000}
|
||||
/>
|
||||
</div>
|
||||
<div className="studio-toggle-row">
|
||||
<div className="studio-toggle-row__copy">
|
||||
<span className="studio-toggle-row__title">图像检测</span>
|
||||
<span className="studio-toggle-row__desc">检测人物朝向</span>
|
||||
</div>
|
||||
<button type="button" className={`studio-toggle${checkImage ? " is-on" : ""}`} onClick={() => setCheckImage(!checkImage)}>
|
||||
<span className="studio-toggle__thumb" />
|
||||
</button>
|
||||
</div>
|
||||
{checkImage && characterPreview && faceHint && (
|
||||
<div className={`character-mix-face-hint character-mix-face-hint--${faceHint}`}>
|
||||
{faceHint === "analyzing" ? (
|
||||
<>
|
||||
<InfoCircleOutlined />
|
||||
<span>正在分析人物图像...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircleOutlined />
|
||||
<span>图像已就绪,将自动检测人物面部朝向</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="studio-toggle-row">
|
||||
<div className="studio-toggle-row__copy">
|
||||
<span className="studio-toggle-row__title">水印输出</span>
|
||||
<span className="studio-toggle-row__desc">含水印版本</span>
|
||||
</div>
|
||||
<button type="button" className={`studio-toggle${watermark ? " is-on" : ""}`} onClick={() => setWatermark(!watermark)}>
|
||||
<span className="studio-toggle__thumb" />
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" className="studio-generate-btn" onClick={() => void handleCreateTask()} disabled={isCreating || !characterDataUrl || !videoDataUrl}>
|
||||
{isCreating ? <LoadingOutlined /> : <PlayCircleOutlined />}
|
||||
{isCreating ? "生成中..." : "开始迁移"}
|
||||
</button>
|
||||
{resultUrl && (
|
||||
<div className="studio-result-actions">
|
||||
<button type="button" onClick={() => void handleDownloadResult()} disabled={isDownloadingResult}>
|
||||
<DownloadOutlined />
|
||||
{isDownloadingResult ? "保存中" : "保存本地"}
|
||||
</button>
|
||||
<button type="button" onClick={() => void handleAddResultToAssets()} disabled={isSavingResultAsset}>
|
||||
<InboxOutlined />
|
||||
{isSavingResultAsset ? "加入中" : "加入资产库"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="image-workbench-page character-mix-page" aria-label="角色迁移">
|
||||
<header className="image-workbench-topbar">
|
||||
@@ -339,8 +427,10 @@ function CharacterMixPage({
|
||||
|
||||
<StudioToolLayout
|
||||
noTop
|
||||
noRight
|
||||
leftPanel={
|
||||
<div
|
||||
className="character-mix-source-panel"
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
@@ -389,6 +479,20 @@ function CharacterMixPage({
|
||||
<strong>{characterFile || "上传人物图"}</strong>
|
||||
<small>单人正面或半身更稳定</small>
|
||||
</span>
|
||||
{characterPreview ? (
|
||||
<button
|
||||
type="button"
|
||||
className="studio-upload-slot__remove"
|
||||
aria-label="移除人物图"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clearCharacterAsset();
|
||||
}}
|
||||
>
|
||||
<DeleteOutlined />
|
||||
</button>
|
||||
) : null}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -427,9 +531,24 @@ function CharacterMixPage({
|
||||
<strong>{videoFile || "上传参考视频"}</strong>
|
||||
<small>MP4 / MOV / AVI</small>
|
||||
</span>
|
||||
{videoPreview ? (
|
||||
<button
|
||||
type="button"
|
||||
className="studio-upload-slot__remove"
|
||||
aria-label="移除参考视频"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clearReferenceVideo();
|
||||
}}
|
||||
>
|
||||
<DeleteOutlined />
|
||||
</button>
|
||||
) : null}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{characterMixSettingsPanel}
|
||||
</div>
|
||||
}
|
||||
canvas={
|
||||
@@ -480,80 +599,6 @@ function CharacterMixPage({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
rightPanel={
|
||||
<div className="studio-panel__section">
|
||||
<div className="studio-panel__section-head">
|
||||
<span className="studio-panel__section-title">迁移设置</span>
|
||||
</div>
|
||||
<div className="studio-panel__section-body">
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<div className="studio-label" style={{ fontSize: 11, color: "var(--fg-muted, #999)", marginBottom: 4 }}>驱动提示词</div>
|
||||
<textarea
|
||||
value={promptInput}
|
||||
onChange={(e) => setPromptInput(e.target.value)}
|
||||
placeholder="保持角色原有服装,动作流畅自然"
|
||||
rows={3}
|
||||
maxLength={1000}
|
||||
style={{
|
||||
width: "100%", resize: "vertical", background: "var(--bg-elevated, #1a1a1a)",
|
||||
border: "1px solid var(--border-subtle, #333)", borderRadius: 6,
|
||||
padding: "6px 8px", fontSize: 12, color: "var(--fg-body, #eee)",
|
||||
fontFamily: "inherit", outline: "none", boxSizing: "border-box",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="studio-toggle-row">
|
||||
<div className="studio-toggle-row__copy">
|
||||
<span className="studio-toggle-row__title">图像检测</span>
|
||||
<span className="studio-toggle-row__desc">检测人物朝向</span>
|
||||
</div>
|
||||
<button type="button" className={`studio-toggle${checkImage ? " is-on" : ""}`} onClick={() => setCheckImage(!checkImage)}>
|
||||
<span className="studio-toggle__thumb" />
|
||||
</button>
|
||||
</div>
|
||||
{checkImage && characterPreview && faceHint && (
|
||||
<div className={`character-mix-face-hint character-mix-face-hint--${faceHint}`}>
|
||||
{faceHint === "analyzing" ? (
|
||||
<>
|
||||
<InfoCircleOutlined />
|
||||
<span>正在分析人物图像...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircleOutlined />
|
||||
<span>图像已就绪,将自动检测人物面部朝向</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="studio-toggle-row">
|
||||
<div className="studio-toggle-row__copy">
|
||||
<span className="studio-toggle-row__title">水印输出</span>
|
||||
<span className="studio-toggle-row__desc">含水印版本</span>
|
||||
</div>
|
||||
<button type="button" className={`studio-toggle${watermark ? " is-on" : ""}`} onClick={() => setWatermark(!watermark)}>
|
||||
<span className="studio-toggle__thumb" />
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" className="studio-generate-btn" onClick={() => void handleCreateTask()} disabled={isCreating || !characterDataUrl || !videoDataUrl}>
|
||||
{isCreating ? <LoadingOutlined /> : <PlayCircleOutlined />}
|
||||
{isCreating ? "生成中..." : "开始迁移"}
|
||||
</button>
|
||||
{resultUrl && (
|
||||
<div className="studio-result-actions">
|
||||
<button type="button" onClick={() => void handleDownloadResult()} disabled={isDownloadingResult}>
|
||||
<DownloadOutlined />
|
||||
{isDownloadingResult ? "保存中" : "保存本地"}
|
||||
</button>
|
||||
<button type="button" onClick={() => void handleAddResultToAssets()} disabled={isSavingResultAsset}>
|
||||
<InboxOutlined />
|
||||
{isSavingResultAsset ? "加入中" : "加入资产库"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
statusBar={
|
||||
<>
|
||||
<span className="studio-status-bar__badge studio-status-bar__badge--idle">就绪</span>
|
||||
|
||||
Reference in New Issue
Block a user