优化BGM生成面板界面
按设计稿重排 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:
@@ -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
@@ -16800,20 +16800,110 @@ button {
|
||||
/* BGM composer:Prompt 计数、助手动作与预设轨道。 */
|
||||
.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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user