优化BGM生成面板界面
Project CI / Repository checks (pull_request) Successful in 1m27s
Project CI / Frontend tests (pull_request) Successful in 3m7s
Project CI / Backend tests (pull_request) Successful in 3m51s
Project CI / Native shell tests (pull_request) Successful in 12m40s

按设计稿重排 BGM composer 的控件布局,并统一输入框行为。

- 字数计数移到「预设」标题行右端,与预设展开控件同排
- AI补全 / 一键简化 / 撤销 移入底部行,与 Suno、生成同区域
- 助手按钮补图标并压回 2.25rem 高度与胶囊圆角;shape="pill" 与
  size="xs" 走的是 Tailwind 分层 utility,压不过未分层的
  .platform-button,因此显式写死
- 预设展开 / 收起改为裸 chevron 图标按钮,可访问名由 aria-label 承担
- 预设滚动箭头去边框,hover 才出淡底
- BGM footer 改 flex 并作用域隔离,共享的 5 列 grid 不受影响
- 生成按钮收到 2.25rem,样式、文案与泥点角标保持不变
- 面板 min-height 归零并移除多余显式行,消除收起预设时的空白
- 输入框改用 AutoGrowTextArea,与生图 / 生视频 / 角色 / 图标同源,
  53.6px 起自动增高至 128px 后内滚

交互逻辑未改动。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 08:44:26 +00:00
parent 59b92123b9
commit b79f72391b
4 changed files with 267 additions and 75 deletions
@@ -1,6 +1,12 @@
import { ChevronLeft, ChevronRight } from 'lucide-react';
import {
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronUp,
} from 'lucide-react';
import {
type PointerEvent as ReactPointerEvent,
type ReactNode,
useCallback,
useEffect,
useId,
@@ -77,10 +83,13 @@ export function ImageCanvasBackgroundMusicPresetMarquee({
presets,
isLocked,
onSelectPreset,
headerTrailing,
}: {
presets: readonly BackgroundMusicPromptPreset[];
isLocked: boolean;
onSelectPreset: (preset: BackgroundMusicPromptPreset) => void;
/** 标题行右端的展示位。字数计数要和「预设」同排,但它属于 composer 的状态。 */
headerTrailing?: ReactNode;
}) {
const trackId = useId();
const [isExpanded, setIsExpanded] = useState(false);
@@ -325,13 +334,20 @@ export function ImageCanvasBackgroundMusicPresetMarquee({
size="xs"
shape="pill"
className="image-canvas-editor__background-music-presets-toggle"
// 图标化后文案不再进入可访问名,必须由 aria-label 承担。
aria-label={isExpanded ? '收起' : '展开'}
aria-expanded={isExpanded}
aria-controls={trackId}
disabled={isLocked}
onClick={() => setIsExpanded((expanded) => !expanded)}
>
{isExpanded ? '收起' : '展开'}
{isExpanded ? (
<ChevronUp aria-hidden="true" />
) : (
<ChevronDown aria-hidden="true" />
)}
</PlatformActionButton>
{headerTrailing}
</div>
{isExpanded ? (
<div
@@ -999,12 +999,25 @@ describe('ImageCanvasGenerationComposerView', () => {
'.image-canvas-editor__generation-composer-footer',
);
expect(footer).toBeTruthy();
// 助手动作与模型、生成同处底部一行:左侧动作,右侧「Suno + 生成」。
expect(footer?.children[0]?.className).toContain(
'image-canvas-editor__option-popover-anchor--model',
'image-canvas-editor__background-music-prompt-actions',
);
expect(footer?.children[1]?.className).toContain(
'image-canvas-editor__option-popover-anchor--model',
);
expect(footer?.children[2]?.className).toContain(
'image-canvas-editor__generation-submit',
);
// 字数计数和「预设」同排,收起预设时也必须还在。
const presetsHeader = panel.querySelector(
'.image-canvas-editor__background-music-presets-header',
);
expect(
presetsHeader?.querySelector(
'.image-canvas-editor__background-music-prompt-count',
)?.textContent,
).toBe('0 / 200');
const backgroundMusicModelButton = within(panel).getByRole('button', {
name: '背景音乐模型 Suno',
});
@@ -1,4 +1,14 @@
import { Check, ChevronDown, Cpu, Film, ImageIcon, Music } from 'lucide-react';
import {
Check,
ChevronDown,
Cpu,
Film,
ImageIcon,
Music,
Sparkles,
Undo2,
WandSparkles,
} from 'lucide-react';
import {
type CSSProperties,
type Dispatch,
@@ -19,7 +29,6 @@ import {
} from '../common/PlatformFloatingMenu';
import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformTextField } from '../common/PlatformTextField';
import { ImageCanvasBackgroundMusicPresetMarquee } from './ImageCanvasBackgroundMusicPresetMarquee';
import {
appendBackgroundMusicPromptPreset,
@@ -908,84 +917,33 @@ function ImageCanvasAudioGenerationComposerView({
onSubmit(backgroundMusicDialog);
}}
>
<PlatformTextField
variant="textarea"
{/* 与生图 / 生视频 / 角色 / 图标同源:自动增高到 max-h-32 后内部滚动。 */}
<AutoGrowTextArea
aria-label="gpt_description_prompt"
aria-describedby={promptCounterId}
aria-invalid={isPromptOverLimit || undefined}
value={backgroundMusicDialog.prompt}
readOnly={isLocked}
placeholder="你希望生成什么音乐?"
size="sm"
density="compact"
className="image-canvas-editor__generation-prompt"
onChange={(event) => updateBackgroundMusicPrompt(event.target.value)}
/>
<div className="image-canvas-editor__background-music-prompt-actions">
<span
id={promptCounterId}
className={
isPromptOverLimit
? 'image-canvas-editor__background-music-prompt-count image-canvas-editor__background-music-prompt-count--over-limit'
: 'image-canvas-editor__background-music-prompt-count'
}
>
{`${promptCodePointCount} / ${BACKGROUND_MUSIC_PROMPT_MAX_CODE_POINTS}`}
</span>
<PlatformActionButton
type="button"
tone="secondary"
size="xs"
shape="pill"
className="image-canvas-editor__background-music-prompt-action"
aria-label="AI 补全"
disabled={!canComplete}
onClick={() => {
void backgroundMusicPromptAssist.completePrompt(
backgroundMusicDialog.id,
);
}}
>
{assistState.status === 'completing' ? '补全中' : 'AI 补全'}
</PlatformActionButton>
<PlatformActionButton
type="button"
tone="secondary"
size="xs"
shape="pill"
className="image-canvas-editor__background-music-prompt-action"
aria-label="一键简化"
disabled={!canSimplify}
onClick={() => {
void backgroundMusicPromptAssist.simplifyPrompt(
backgroundMusicDialog.id,
);
}}
>
{assistState.status === 'simplifying' ? '简化中' : '一键简化'}
</PlatformActionButton>
{isUndoVisible ? (
<PlatformActionButton
type="button"
tone="secondary"
size="xs"
shape="pill"
className="image-canvas-editor__background-music-prompt-action"
disabled={!canUndo}
onClick={() => {
backgroundMusicPromptAssist.undoPrompt(
backgroundMusicDialog.id,
);
}}
>
</PlatformActionButton>
) : null}
</div>
<ImageCanvasBackgroundMusicPresetMarquee
presets={BACKGROUND_MUSIC_PROMPT_PRESETS}
isLocked={isLocked}
onSelectPreset={handlePresetClick}
headerTrailing={
<span
id={promptCounterId}
className={
isPromptOverLimit
? 'image-canvas-editor__background-music-prompt-count image-canvas-editor__background-music-prompt-count--over-limit'
: 'image-canvas-editor__background-music-prompt-count'
}
>
{`${promptCodePointCount} / ${BACKGROUND_MUSIC_PROMPT_MAX_CODE_POINTS}`}
</span>
}
/>
{assistState.errorMessage ? (
<PlatformStatusMessage
@@ -1010,6 +968,79 @@ function ImageCanvasAudioGenerationComposerView({
</PlatformStatusMessage>
) : null}
<div className="image-canvas-editor__generation-composer-footer">
<div className="image-canvas-editor__background-music-prompt-actions">
<PlatformActionButton
type="button"
tone="secondary"
size="xs"
shape="pill"
className="image-canvas-editor__background-music-prompt-action image-canvas-editor__background-music-prompt-action--complete"
aria-label="AI 补全"
disabled={!canComplete}
onClick={() => {
void backgroundMusicPromptAssist.completePrompt(
backgroundMusicDialog.id,
);
}}
>
<span
className="image-canvas-editor__background-music-prompt-action-icon"
aria-hidden="true"
>
<Sparkles />
</span>
<span>
{assistState.status === 'completing' ? '补全中' : 'AI 补全'}
</span>
</PlatformActionButton>
<PlatformActionButton
type="button"
tone="secondary"
size="xs"
shape="pill"
className="image-canvas-editor__background-music-prompt-action image-canvas-editor__background-music-prompt-action--simplify"
aria-label="一键简化"
disabled={!canSimplify}
onClick={() => {
void backgroundMusicPromptAssist.simplifyPrompt(
backgroundMusicDialog.id,
);
}}
>
<span
className="image-canvas-editor__background-music-prompt-action-icon"
aria-hidden="true"
>
<WandSparkles />
</span>
<span>
{assistState.status === 'simplifying' ? '简化中' : '一键简化'}
</span>
</PlatformActionButton>
{isUndoVisible ? (
<PlatformActionButton
type="button"
tone="ghost"
size="xs"
shape="pill"
className="image-canvas-editor__background-music-prompt-action image-canvas-editor__background-music-prompt-action--undo"
disabled={!canUndo}
onClick={() => {
backgroundMusicPromptAssist.undoPrompt(
backgroundMusicDialog.id,
);
}}
>
<span
className="image-canvas-editor__background-music-prompt-action-icon"
aria-hidden="true"
>
<Undo2 />
</span>
<span></span>
</PlatformActionButton>
) : null}
</div>
<div className="image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--model image-canvas-editor__readonly-generation-option">
<PlatformInlineOptionButton
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--model"
+137 -5
View File
@@ -16800,20 +16800,110 @@ button {
/* BGM composerPrompt 计数、助手动作与预设轨道。 */
.image-canvas-editor__generation-composer--background-music {
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto auto auto auto;
width: min(44rem, calc(100% - 1.5rem));
/*
* 行数交给 `--image` `auto auto auto` 与隐式行多写的显式行即使高度为 0行间距
* 仍然照算收起预设时会凭空多出一段空白同理 `min-height` 归零否则 grid 默认的
* align-content: stretch 会把撑出来的余量摊到各行上
*/
min-height: 0;
}
/* 助手动作与「Suno + 生成」同处底部一行,共享 footer 的 5 列 grid 在这里换成 flex。 */
.image-canvas-editor__generation-composer--background-music
.image-canvas-editor__generation-composer-footer {
display: flex;
align-items: center;
gap: 0.4rem;
padding-top: 0.5rem;
}
/* flex 布局下提交按钮硬编码的 grid-column 自动失效,无需为 BGM 覆盖它。 */
.image-canvas-editor__generation-composer--background-music
.image-canvas-editor__option-popover-anchor--model {
margin-left: auto;
}
.image-canvas-editor__generation-composer--background-music
.image-canvas-editor__generation-submit {
height: 2.25rem;
min-height: 0;
}
.image-canvas-editor__background-music-prompt-actions {
grid-column: 1 / -1;
display: flex;
align-items: center;
gap: 0.4rem;
min-width: 0;
}
/*
* 助手动作按钮`.platform-button` 给的是 2.9rem 高与 0.7rem/1rem 内边距比同一面板里
* 2.25rem 的生成按钮高出一截这里按两级选择器压回去避免整排按钮比主操作还重
*/
.image-canvas-editor__background-music-prompt-actions
.image-canvas-editor__background-music-prompt-action {
/* 2.25rem 是本 composer 的控件高度基准(模型 chip 与生成按钮同源)。 */
height: 2.25rem;
min-height: 0;
gap: 0.3rem;
/* `shape="pill"` 走的是 Tailwind 分层 utility压不过未分层的 `.platform-button`
* 1rem 圆角所以胶囊圆角必须在这里显式写死 */
border: 1px solid rgba(15, 23, 42, 0.12);
border-radius: 999px;
background: transparent;
padding: 0 0.82rem;
color: #475569;
font-size: 0.78rem;
font-weight: 760;
box-shadow: none;
}
.image-canvas-editor__background-music-prompt-action-icon {
display: inline-flex;
align-items: center;
justify-content: center;
}
.image-canvas-editor__background-music-prompt-action-icon svg {
width: 0.85rem;
height: 0.85rem;
}
/* AI 补全是这一排的主操作,用品牌描边与中性的简化、撤销拉开层级。 */
.image-canvas-editor__background-music-prompt-actions
.image-canvas-editor__background-music-prompt-action--complete {
border-color: var(--image-canvas-brand-border-strong);
color: var(--image-canvas-brand-accent);
}
.image-canvas-editor__background-music-prompt-action--complete:not(
:disabled
):hover {
background: var(--image-canvas-brand-soft);
}
.image-canvas-editor__background-music-prompt-action--simplify:not(
:disabled
):hover {
background: rgba(15, 23, 42, 0.04);
}
/* 撤销权重最低:无边框无底色,只在 hover 时给一层淡底。 */
.image-canvas-editor__background-music-prompt-actions
.image-canvas-editor__background-music-prompt-action--undo {
border-color: transparent;
color: #64748b;
}
.image-canvas-editor__background-music-prompt-action--undo:not(
:disabled
):hover {
background: rgba(15, 23, 42, 0.05);
}
.image-canvas-editor__background-music-prompt-count {
margin-right: auto;
margin-left: auto;
color: #64748b;
font-size: 0.72rem;
font-weight: 700;
@@ -16843,6 +16933,32 @@ button {
font-weight: 800;
}
/*
* 展开 / 收起只是预设标题旁的一个箭头不该是实心 pill命中区靠 1.75rem 方形保住
* 不做成裸图标那种小到点不中的按钮
*/
.image-canvas-editor__background-music-presets-toggle {
width: 1.75rem;
height: 1.75rem;
min-height: 0;
border: 0;
border-radius: 999px;
background: transparent;
padding: 0;
color: #64748b;
box-shadow: none;
}
.image-canvas-editor__background-music-presets-toggle:not(:disabled):hover {
background: rgba(15, 23, 42, 0.05);
color: #334155;
}
.image-canvas-editor__background-music-presets-toggle svg {
width: 0.9rem;
height: 0.9rem;
}
.image-canvas-editor__background-music-presets-track {
position: relative;
display: grid;
@@ -16853,15 +16969,25 @@ button {
padding-bottom: 0.28rem;
}
/* 轨道两侧的箭头是辅助控件默认贴边无框hover 才浮出淡底宽度保持 1.5rem
* 细线两端的 1.74rem 缩进箭头 1.5rem + 轨道 0.24rem 间距因此不用跟着改 */
.image-canvas-editor__background-music-presets-arrow {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
border: 1px solid rgba(15, 23, 42, 0.08);
border: 0;
border-radius: 999px;
background: #f8fafc;
background: transparent;
color: #94a3b8;
transition:
background-color 160ms ease,
color 160ms ease;
}
.image-canvas-editor__background-music-presets-arrow:not(:disabled):hover {
background: rgba(15, 23, 42, 0.05);
color: #475569;
}
@@ -16983,6 +17109,12 @@ button {
flex-wrap: wrap;
}
/* 一行放不下三个助手按钮加 Suno 和生成,窄屏必须允许换行。 */
.image-canvas-editor__generation-composer--background-music
.image-canvas-editor__generation-composer-footer {
flex-wrap: wrap;
}
.image-canvas-editor__background-music-preset {
max-width: 9rem;
}