feat: 多页面UI打磨 — 设置面板、状态反馈与样式升级
Web Quality / verify (pull_request) Has been cancelled

本次更新对多个功能页面进行了系统性的 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:
2026-06-10 17:54:45 +08:00
parent 77ffd01a50
commit a6626beb32
15 changed files with 2949 additions and 251 deletions
+119 -74
View File
@@ -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>