feat: BGM 弹窗改为分类+列表+试听交互
- 顶部分类标签栏(全部/知识科普/案例展示/促销活动/家居生活/智能家居) - 列表布局:每项含试听按钮、标题、时长、选中标记 - 试听功能:点击圆形按钮播放/暂停,播放中按钮呼吸动画 - 选中 BGM 后关闭弹窗,本地上传入口移至底部 - 弹窗关闭时自动停止音频播放
This commit is contained in:
@@ -48,6 +48,9 @@ export default function VideoCompose() {
|
||||
|
||||
// BGM
|
||||
const [bgmList, setBgmList] = useState<BgmMusicItem[]>([]);
|
||||
const [bgmCategory, setBgmCategory] = useState<string>('all');
|
||||
const [previewId, setPreviewId] = useState<number | null>(null);
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const bgmMusicId = useProjectStore(state => state.bgmMusicId);
|
||||
const bgmMusicPath = useProjectStore(state => state.bgmMusicPath);
|
||||
const bgmMusicTitle = useProjectStore(state => state.bgmMusicTitle);
|
||||
@@ -58,6 +61,34 @@ export default function VideoCompose() {
|
||||
const [bgmModalOpen, setBgmModalOpen] = useState(false);
|
||||
const localBgmRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
// 弹窗关闭时停止试听
|
||||
useEffect(() => {
|
||||
if (!bgmModalOpen) {
|
||||
audioRef.current?.pause();
|
||||
audioRef.current = null;
|
||||
setPreviewId(null);
|
||||
}
|
||||
}, [bgmModalOpen]);
|
||||
|
||||
const handlePreview = (item: BgmMusicItem) => {
|
||||
if (previewId === item.id) {
|
||||
audioRef.current?.pause();
|
||||
audioRef.current = null;
|
||||
setPreviewId(null);
|
||||
return;
|
||||
}
|
||||
audioRef.current?.pause();
|
||||
const audio = new Audio(item.url || '');
|
||||
audio.play().catch(() => {});
|
||||
audio.onended = () => setPreviewId(null);
|
||||
audioRef.current = audio;
|
||||
setPreviewId(item.id);
|
||||
};
|
||||
|
||||
const filteredBgmList = bgmCategory === 'all'
|
||||
? bgmList
|
||||
: bgmList.filter(item => item.category === bgmCategory);
|
||||
|
||||
// 积分不足弹窗
|
||||
const [showPointsModal, setShowPointsModal] = useState(false);
|
||||
const showRechargeModal = usePointStore((state) => state.showRechargeModal);
|
||||
@@ -598,21 +629,90 @@ export default function VideoCompose() {
|
||||
open={bgmModalOpen}
|
||||
onClose={() => setBgmModalOpen(false)}
|
||||
title="选择背景音乐"
|
||||
width="440px"
|
||||
width="480px"
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div className="modal-bgm-grid">
|
||||
{/* 本地上传 */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{/* 分类标签 */}
|
||||
<div className="modal-bgm-tabs">
|
||||
<button
|
||||
className="modal-bgm-card modal-bgm-upload"
|
||||
className={`modal-bgm-tab ${bgmCategory === 'all' ? 'active' : ''}`}
|
||||
onClick={() => setBgmCategory('all')}
|
||||
>
|
||||
全部
|
||||
</button>
|
||||
{Object.entries(CATEGORY_LABELS).map(([key, label]) => (
|
||||
<button
|
||||
key={key}
|
||||
className={`modal-bgm-tab ${bgmCategory === key ? 'active' : ''}`}
|
||||
onClick={() => setBgmCategory(key)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* BGM 列表 */}
|
||||
<div className="modal-bgm-list">
|
||||
{filteredBgmList.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`modal-bgm-row ${bgmMusicId === item.id ? 'active' : ''}`}
|
||||
>
|
||||
<button
|
||||
className={`modal-bgm-preview-btn ${previewId === item.id ? 'playing' : ''}`}
|
||||
onClick={() => handlePreview(item)}
|
||||
title={previewId === item.id ? '暂停试听' : '试听'}
|
||||
>
|
||||
{previewId === item.id ? (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="6" y="4" width="4" height="16" rx="1" />
|
||||
<rect x="14" y="4" width="4" height="16" rx="1" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
<div
|
||||
className="modal-bgm-info"
|
||||
onClick={() => {
|
||||
setBgmMusic(item.id, item.title, item.url || item.filePath);
|
||||
setBgmModalOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="modal-bgm-row-title">{item.title}</div>
|
||||
<div className="modal-bgm-row-meta">
|
||||
{CATEGORY_LABELS[item.category] || item.category}
|
||||
{item.duration ? ` · ${Math.floor(item.duration / 60)}:${String(Math.floor(item.duration % 60)).padStart(2, '0')}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
{bgmMusicId === item.id && (
|
||||
<div className="modal-bgm-check">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{filteredBgmList.length === 0 && (
|
||||
<div className="modal-bgm-empty">该分类暂无背景音乐</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部操作 */}
|
||||
<div className="modal-bgm-footer">
|
||||
<button
|
||||
className="modal-bgm-upload-btn"
|
||||
onClick={() => localBgmRef.current?.click()}
|
||||
>
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
|
||||
<polyline points="17,8 12,3 7,8" />
|
||||
<line x1="12" y1="3" x2="12" y2="15" />
|
||||
</svg>
|
||||
<span>本地上传</span>
|
||||
本地上传
|
||||
</button>
|
||||
<input
|
||||
ref={localBgmRef}
|
||||
@@ -621,29 +721,6 @@ export default function VideoCompose() {
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleLocalBgmUpload}
|
||||
/>
|
||||
{/* 系统 BGM */}
|
||||
{bgmList.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
className={`modal-bgm-card ${bgmMusicId === item.id ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setBgmMusic(item.id, item.title, item.url || item.filePath);
|
||||
setBgmModalOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="modal-bgm-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
|
||||
<path d="M9 18V5l12-2v13" />
|
||||
<circle cx="6" cy="18" r="3" />
|
||||
<circle cx="18" cy="16" r="3" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="modal-bgm-title">{item.title}</div>
|
||||
<div className="modal-bgm-category">{CATEGORY_LABELS[item.category] || item.category}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => setBgmModalOpen(false)}>取消</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -424,74 +424,171 @@
|
||||
BGM 选择弹窗
|
||||
============================================ */
|
||||
|
||||
.modal-bgm-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--spacing-sm);
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
.modal-bgm-tabs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 4px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.modal-bgm-card {
|
||||
position: relative;
|
||||
.modal-bgm-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-bgm-tab {
|
||||
flex-shrink: 0;
|
||||
padding: 5px 12px;
|
||||
border-radius: var(--radius-full);
|
||||
border: 1px solid transparent;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-xs);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.modal-bgm-tab:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-bgm-tab.active {
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.modal-bgm-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: var(--spacing-md);
|
||||
border: 2px solid transparent;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.modal-bgm-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1.5px solid transparent;
|
||||
background: var(--bg-secondary);
|
||||
cursor: pointer;
|
||||
aspect-ratio: 1;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.modal-bgm-card:hover {
|
||||
border-color: var(--border-color);
|
||||
transform: scale(1.02);
|
||||
.modal-bgm-row:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.modal-bgm-card.active {
|
||||
.modal-bgm-row.active {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px var(--primary-light);
|
||||
}
|
||||
|
||||
.modal-bgm-upload {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.modal-bgm-upload:hover {
|
||||
color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.modal-bgm-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-light);
|
||||
}
|
||||
|
||||
.modal-bgm-preview-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.modal-bgm-title {
|
||||
font-size: var(--font-xs);
|
||||
.modal-bgm-preview-btn:hover {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.modal-bgm-preview-btn.playing {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
animation: bgm-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes bgm-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
|
||||
.modal-bgm-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.modal-bgm-row-title {
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
line-height: 1.3;
|
||||
max-width: 100%;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.modal-bgm-category {
|
||||
font-size: 10px;
|
||||
.modal-bgm-row-meta {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
text-align: center;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.modal-bgm-check {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-bgm-empty {
|
||||
text-align: center;
|
||||
padding: 32px;
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-sm);
|
||||
}
|
||||
|
||||
.modal-bgm-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.modal-bgm-upload-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px dashed var(--border-color);
|
||||
background: transparent;
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-xs);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.modal-bgm-upload-btn:hover {
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
background: var(--primary-light);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user