From d3b699231e091ff47e3ce079934046511aa5ee32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E4=B8=B0?= Date: Wed, 18 Dec 2024 17:38:28 +0800 Subject: [PATCH 01/16] feat: audioPlayer UI --- .../audioPlayer/audioPlayer.scss | 77 +++++++++++++ .../semi-foundation/audioPlayer/constants.ts | 7 ++ .../semi-foundation/audioPlayer/foundation.ts | 0 .../audioPlayer/variables.scss | 6 ++ .../audioPlayer/_story/autoPlayer.stories.jsx | 24 +++++ packages/semi-ui/audioPlayer/audioSlider.tsx | 0 packages/semi-ui/audioPlayer/index.tsx | 102 ++++++++++++++++++ 7 files changed, 216 insertions(+) create mode 100644 packages/semi-foundation/audioPlayer/audioPlayer.scss create mode 100644 packages/semi-foundation/audioPlayer/constants.ts create mode 100644 packages/semi-foundation/audioPlayer/foundation.ts create mode 100644 packages/semi-foundation/audioPlayer/variables.scss create mode 100644 packages/semi-ui/audioPlayer/_story/autoPlayer.stories.jsx create mode 100644 packages/semi-ui/audioPlayer/audioSlider.tsx create mode 100644 packages/semi-ui/audioPlayer/index.tsx diff --git a/packages/semi-foundation/audioPlayer/audioPlayer.scss b/packages/semi-foundation/audioPlayer/audioPlayer.scss new file mode 100644 index 0000000000..47ee5df2ee --- /dev/null +++ b/packages/semi-foundation/audioPlayer/audioPlayer.scss @@ -0,0 +1,77 @@ +@import './variables.scss'; + +$module: #{$prefix}-audio-player; + +.#{$module} { + display: flex; + align-items: center; + justify-content: center; + gap: 24px; + width: 1440px; + height: 78px; + background: $color-audio-player-background; + &-control { + display: flex; + align-items: center; + gap: 16px; + + } + + &-control-button-icon { + color: $color-audio-player-control-icon; + } + + &-control-button { + border-radius: 50%; + height: 40px !important; + width: 40px !important; + } + + &-control-button-play { + background: $color-audio-player-control-icon !important; + color: $color-audio-player-control-icon-play !important; + } + + &-info-cover { + display: flex; + align-items: center; + gap: 16px; + } + + &-info { + width: 412px; + display: flex; + flex-direction: column; + gap: 4px; + } + + &-info-title { + width: 56px; + height: 20px; + font-size: 14px; + color: $color-audio-player-font-color; + } + &-info-time { + width: 100%; + height: 22px; + font-size: 14px; + color: $color-audio-player-font-color; + display: flex; + align-items: center; + justify-content: space-between; + } + &-control-speed { + width: 40px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + background: #35363C; + border-radius: 3px; + font-size: 12px; + line-height: 16px; + color: #F9F9F9; + font-weight: 600; + } +} diff --git a/packages/semi-foundation/audioPlayer/constants.ts b/packages/semi-foundation/audioPlayer/constants.ts new file mode 100644 index 0000000000..df642c99de --- /dev/null +++ b/packages/semi-foundation/audioPlayer/constants.ts @@ -0,0 +1,7 @@ +import { BASE_CLASS_PREFIX } from '../base/constants'; + +const cssClasses = { + PREFIX: `${BASE_CLASS_PREFIX}-audio-player`, +}; + +export { cssClasses }; diff --git a/packages/semi-foundation/audioPlayer/foundation.ts b/packages/semi-foundation/audioPlayer/foundation.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/semi-foundation/audioPlayer/variables.scss b/packages/semi-foundation/audioPlayer/variables.scss new file mode 100644 index 0000000000..6b540983d9 --- /dev/null +++ b/packages/semi-foundation/audioPlayer/variables.scss @@ -0,0 +1,6 @@ +$color-audio-player-background: rgba(var(--semi-grey-9), .8); +$color-audio-player-control-icon: var(--semi-color-bg-0); +$color-audio-player-control-icon-play: var(--semi-color-text-0); + +$color-audio-player-font-color: var(--semi-color-bg-0); +$color-audio-player-font-color-speed: rgba(var(--semi-grey-8), 1); diff --git a/packages/semi-ui/audioPlayer/_story/autoPlayer.stories.jsx b/packages/semi-ui/audioPlayer/_story/autoPlayer.stories.jsx new file mode 100644 index 0000000000..fe2842def7 --- /dev/null +++ b/packages/semi-ui/audioPlayer/_story/autoPlayer.stories.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import AudioPlayer from '../index'; + +export default { + title: 'AudioPlayer', +}; + +export const DefaultAutoPlay = () => { + const audioUrl = 'http://m10.music.126.net/20241218110843/57a53b2ae73e9c8a571024ae067788e2/ymusic/5353/0f0f/0358/d99739615f8e5153d77042092f07fd77.mp3'; // 替换为实际的音频URL + const audioUrlArray = [ + 'http://m10.music.126.net/20241218110843/57a53b2ae73e9c8a571024ae067788e2/ymusic/5353/0f0f/0358/d99739615f8e5153d77042092f07fd77.mp3', + 'http://m10.music.126.net/20241218110843/57a53b2ae73e9c8a571024ae067788e2/ymusic/5353/0f0f/0358/d99739615f8e5153d77042092f07fd77.mp3', + ]; + return ( +
+ +
+ ); +}; diff --git a/packages/semi-ui/audioPlayer/audioSlider.tsx b/packages/semi-ui/audioPlayer/audioSlider.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/semi-ui/audioPlayer/index.tsx b/packages/semi-ui/audioPlayer/index.tsx new file mode 100644 index 0000000000..5ea8009b07 --- /dev/null +++ b/packages/semi-ui/audioPlayer/index.tsx @@ -0,0 +1,102 @@ +import { BaseProps } from '_base/baseComponent'; +import React from 'react'; +import cls from 'classnames'; +import '@douyinfe/semi-foundation/audioPlayer/audioPlayer.scss'; +import { cssClasses } from '@douyinfe/semi-foundation/audioPlayer/constants'; +import Button from '../button'; +import Image from '../image'; +import { IconBackward, IconFastForward, IconMusic, IconPause, IconPlay, IconRefresh, IconRestart, IconVolume2 } from '@douyinfe/semi-icons'; +export interface AudioPlayerProps extends BaseProps { + audioUrl: string | string[]; + autoPlay: boolean; + audioCover?: string; + audioTitle?: string; + className?: string; + style?: React.CSSProperties +} + +export interface AudioPlayerState { + isPlaying: boolean; + currentIndex: number; + totalTime: number; + currentTime: number +} + + +const prefixCls = cssClasses.PREFIX; +class AudioPlayer extends React.Component { + audioRef = React.createRef(); + static defaultProps: Partial = { + autoPlay: false, + }; + + constructor(props: AudioPlayerProps) { + super(props); + this.state = { + isPlaying: false, + currentIndex: 0, + totalTime: 0, + currentTime: 0, + }; + this.audioRef = React.createRef(); + } + + handleClick = () => { + if (!this.audioRef.current) return; + if (this.state.isPlaying) { + this.audioRef.current.pause(); + } else { + this.audioRef.current.play(); + } + this.setState({ + isPlaying: !this.state.isPlaying, + }); + } + + componentDidMount() { + console.log(this.audioRef.current?.duration); + } + + render() { + const { audioUrl, autoPlay, className, style, audioTitle, audioCover } = this.props as AudioPlayerProps; + const { currentIndex } = this.state; + const isAudioUrlArray = Array.isArray(audioUrl); + const src = isAudioUrlArray ? audioUrl[currentIndex] : audioUrl; + const iconClass = cls(`${prefixCls}-control-button-icon`); + return ( +
+ +
+ {isAudioUrlArray &&
+
+ {audioCover && } +
+ {audioTitle &&
{audioTitle}
} +
+ 00:50 +
+ 01:40 +
+
+
+
+
+
+ ); + } +} + +export default AudioPlayer; \ No newline at end of file From 1ffc7296574ccc3ba21e07b7ac32ed9b488f6fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E4=B8=B0?= Date: Mon, 23 Dec 2024 18:50:04 +0800 Subject: [PATCH 02/16] feat: AudioPlayer --- .../audioPlayer/audioPlayer.scss | 118 ++++++- .../semi-foundation/audioPlayer/foundation.ts | 69 ++++ .../audioPlayer/variables.scss | 7 +- .../semi-icons/src/icons/IconMiniPlayer.tsx | 24 ++ packages/semi-icons/src/icons/index.ts | 3 +- .../audioPlayer/_story/autoPlayer.stories.jsx | 32 +- packages/semi-ui/audioPlayer/audioSlider.tsx | 169 ++++++++++ packages/semi-ui/audioPlayer/index.tsx | 295 +++++++++++++++--- packages/semi-ui/audioPlayer/utils.ts | 5 + 9 files changed, 666 insertions(+), 56 deletions(-) create mode 100644 packages/semi-icons/src/icons/IconMiniPlayer.tsx create mode 100644 packages/semi-ui/audioPlayer/utils.ts diff --git a/packages/semi-foundation/audioPlayer/audioPlayer.scss b/packages/semi-foundation/audioPlayer/audioPlayer.scss index 47ee5df2ee..d70cb3d33b 100644 --- a/packages/semi-foundation/audioPlayer/audioPlayer.scss +++ b/packages/semi-foundation/audioPlayer/audioPlayer.scss @@ -14,24 +14,21 @@ $module: #{$prefix}-audio-player; display: flex; align-items: center; gap: 16px; - } &-control-button-icon { color: $color-audio-player-control-icon; } - - &-control-button { - border-radius: 50%; - height: 40px !important; - width: 40px !important; - } &-control-button-play { background: $color-audio-player-control-icon !important; color: $color-audio-player-control-icon-play !important; } + &-slider-container { + width: 323px; + } + &-info-cover { display: flex; align-items: center; @@ -46,7 +43,6 @@ $module: #{$prefix}-audio-player; } &-info-title { - width: 56px; height: 20px; font-size: 14px; color: $color-audio-player-font-color; @@ -67,11 +63,113 @@ $module: #{$prefix}-audio-player; align-items: center; justify-content: center; gap: 4px; - background: #35363C; + background: #35363c; border-radius: 3px; font-size: 12px; line-height: 16px; - color: #F9F9F9; + color: #f9f9f9; font-weight: 600; } + + &-control-speed-menu { + background: #35363c; + width: 65px; + } + + &-control-speed-menu-item { + color: #f9f9f9; + } + + &-control-speed-menu-item:hover { + background: #43444a !important; + } + + &-control-volume { + width: 43px; + height: 164px; + background: #35363c; + border-radius: 4px; + padding: 4px 0; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 8px; + } + + &-control-volume-title { + font-size: 12px; + line-height: 16px; + color: #f9f9f9; + font-weight: 600; + } + + &-light { + background: $color-audio-player-background-light; + border: 1px solid var(--semi-color-border); + + .#{$module}-control-button-icon { + color: $color-audio-player-control-icon-light; + } + + .#{$module}-control-button-play { + background: $color-audio-player-control-icon-light !important; + color: $color-audio-player-control-icon-play-light !important; + } + + .#{$module}-info-title, + .#{$module}-info-time { + color: $color-audio-player-font-color-light; + } + + .#{$module}-control-speed-menu-item, + .#{$module}-control-volume-title { + color: #1f1f1f; + } + + .#{$module}-control-speed-menu-item:hover { + background: #e0e0e0 !important; + } + } +} + +.#{$module}-slide { + background: #DDE3E8; + border-radius: 9999px; + position: relative; + + &-vertical { + width: 4px; + height: 100%; + } + + &-horizontal { + width: 100%; + height: 4px; + } + + &-progress { + position: absolute; + background: #3295FB; + border-radius: 9999px; + } + + &-progress-vertical { + bottom: 0; + } + + &-progress-horizontal { + left: 0; + } + + &-dot { + position: absolute; + width: 16px; + height: 16px; + background: #ffffff; + border: 1px solid #3295FB; + box-shadow: 0px 0px 4px 0px #0000004D; + border-radius: 50%; + transition: opacity 0.2s; + } } diff --git a/packages/semi-foundation/audioPlayer/foundation.ts b/packages/semi-foundation/audioPlayer/foundation.ts index e69de29bb2..44ad950f7f 100644 --- a/packages/semi-foundation/audioPlayer/foundation.ts +++ b/packages/semi-foundation/audioPlayer/foundation.ts @@ -0,0 +1,69 @@ + + +import BaseFoundation, { DefaultAdapter } from '../base/foundation'; + +export interface AudioPlayerAdapter

, S = Record> extends DefaultAdapter { + initAudio: () => void; + resetAudioState: () => void; + handleStatusClick: () => void; + handleTimeUpdate: () => void; + handleTrackChange: (direction: 'next' | 'prev') => void; + getAudioRef: () => React.RefObject; + handleTimeChange: (value: number) => void; + handleSpeedChange: (value: { label: string; value: number }) => void; + handleSeek: (direction: number) => void; + handleRefresh: () => void; + handleVolumeChange: (value: number) => void +} + +class AudioPlayerFoundation extends BaseFoundation { + constructor(adapter: AudioPlayerAdapter) { + super({ ...AudioPlayerFoundation, ...adapter }); + } + + initAudio() { + this._adapter.initAudio(); + } + + resetAudioState() { + this._adapter.resetAudioState(); + } + + handleStatusClick() { + this._adapter.handleStatusClick(); + } + + handleTimeUpdate() { + this._adapter.handleTimeUpdate(); + } + + handleTrackChange(direction: 'next' | 'prev') { + this._adapter.handleTrackChange(direction); + } + + getAudioRef() { + return this._adapter.getAudioRef(); + } + + handleTimeChange(value: number) { + this._adapter.handleTimeChange(value); + } + + handleSpeedChange(value: { label: string; value: number }) { + this._adapter.handleSpeedChange(value); + } + + handleSeek(direction: number) { + this._adapter.handleSeek(direction); + } + + handleRefresh() { + this._adapter.handleRefresh(); + } + + handleVolumeChange(value: number) { + this._adapter.handleVolumeChange(value); + } +} + +export default AudioPlayerFoundation; diff --git a/packages/semi-foundation/audioPlayer/variables.scss b/packages/semi-foundation/audioPlayer/variables.scss index 6b540983d9..05ed524fa8 100644 --- a/packages/semi-foundation/audioPlayer/variables.scss +++ b/packages/semi-foundation/audioPlayer/variables.scss @@ -1,6 +1,11 @@ $color-audio-player-background: rgba(var(--semi-grey-9), .8); $color-audio-player-control-icon: var(--semi-color-bg-0); $color-audio-player-control-icon-play: var(--semi-color-text-0); - $color-audio-player-font-color: var(--semi-color-bg-0); $color-audio-player-font-color-speed: rgba(var(--semi-grey-8), 1); + + +$color-audio-player-background-light: var(--semi-color-bg-0); +$color-audio-player-control-icon-light: #1f1f1f; +$color-audio-player-control-icon-play-light: #ffffff; +$color-audio-player-font-color-light: #1f1f1f; \ No newline at end of file diff --git a/packages/semi-icons/src/icons/IconMiniPlayer.tsx b/packages/semi-icons/src/icons/IconMiniPlayer.tsx new file mode 100644 index 0000000000..ce5db83846 --- /dev/null +++ b/packages/semi-icons/src/icons/IconMiniPlayer.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { convertIcon } from '../components/Icon'; +function SvgComponent(props: React.SVGProps) { + return ( + + + + + + + + ); +} +const IconComponent = convertIcon(SvgComponent, 'miniplayer'); +export default IconComponent; \ No newline at end of file diff --git a/packages/semi-icons/src/icons/index.ts b/packages/semi-icons/src/icons/index.ts index 05f483f524..dd951dacf4 100644 --- a/packages/semi-icons/src/icons/index.ts +++ b/packages/semi-icons/src/icons/index.ts @@ -442,4 +442,5 @@ export { default as IconWifi } from './IconWifi'; export { default as IconWindowAdaptionStroked } from './IconWindowAdaptionStroked'; export { default as IconWrench } from './IconWrench'; export { default as IconXiguaLogo } from './IconXiguaLogo'; -export { default as IconYoutube } from './IconYoutube'; \ No newline at end of file +export { default as IconYoutube } from './IconYoutube'; +export { default as IconMiniPlayer } from './IconMiniPlayer'; \ No newline at end of file diff --git a/packages/semi-ui/audioPlayer/_story/autoPlayer.stories.jsx b/packages/semi-ui/audioPlayer/_story/autoPlayer.stories.jsx index fe2842def7..2445365168 100644 --- a/packages/semi-ui/audioPlayer/_story/autoPlayer.stories.jsx +++ b/packages/semi-ui/audioPlayer/_story/autoPlayer.stories.jsx @@ -8,16 +8,38 @@ export default { export const DefaultAutoPlay = () => { const audioUrl = 'http://m10.music.126.net/20241218110843/57a53b2ae73e9c8a571024ae067788e2/ymusic/5353/0f0f/0358/d99739615f8e5153d77042092f07fd77.mp3'; // 替换为实际的音频URL const audioUrlArray = [ - 'http://m10.music.126.net/20241218110843/57a53b2ae73e9c8a571024ae067788e2/ymusic/5353/0f0f/0358/d99739615f8e5153d77042092f07fd77.mp3', - 'http://m10.music.126.net/20241218110843/57a53b2ae73e9c8a571024ae067788e2/ymusic/5353/0f0f/0358/d99739615f8e5153d77042092f07fd77.mp3', + 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + ]; + const audioInfo = { + title: '音频标题', + cover: 'https://picsum.photos/50/50', + src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + }; + const audioInfoArray = [ + { + title: '音频标题1', + cover: 'https://picsum.photos/50/50', + src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + }, + { + title: '音频标题2', + cover: 'https://picsum.photos/100/100', + src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + }, + { + title: '音频标题3', + cover: 'https://picsum.photos/150/150', + src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + }, ]; return (

); diff --git a/packages/semi-ui/audioPlayer/audioSlider.tsx b/packages/semi-ui/audioPlayer/audioSlider.tsx index e69de29bb2..8a04bc2596 100644 --- a/packages/semi-ui/audioPlayer/audioSlider.tsx +++ b/packages/semi-ui/audioPlayer/audioSlider.tsx @@ -0,0 +1,169 @@ +import React from 'react'; +import cls from 'classnames'; +import '@douyinfe/semi-foundation/audioPlayer/audioPlayer.scss'; +import { cssClasses } from '@douyinfe/semi-foundation/audioPlayer/constants'; +import Tooltip from '../tooltip'; +import { formatTime } from './utils'; +import { noop } from 'lodash'; + +interface AudioSliderProps { + value: number; + onChange?: (value: number) => void; + className?: string; + max?: number; + vertical?: boolean; + width?: number; + height?: number; + showTooltip?: boolean; + disabled?: boolean +} + +interface AudioSliderState { + isDragging: boolean; + movingInfo: { progress: number; offset: number } | null; + isHovering: boolean +} +const prefixCls = cssClasses.PREFIX; +export default class AudioSlider extends React.Component { + static defaultProps = { + value: 0, + onChange: noop, + onAfterChange: noop, + max: 100, + vertical: false, + width: '100%', + height: 4, + showTooltip: true, + disabled: false, + }; + + private sliderRef: React.RefObject; + private handleRef: React.RefObject; + + constructor(props: AudioSliderProps) { + super(props); + this.state = { + isDragging: false, + isHovering: false, + movingInfo: null, + }; + + this.sliderRef = React.createRef(); + this.handleRef = React.createRef(); + } + + + handleMouseEnter = (e: React.MouseEvent) => { + this.setState({ isHovering: true }); + this.handleMouseEvent(e, false); + } + + handleMouseDown = (e: React.MouseEvent) => { + this.setState({ isDragging: true }); + this.handleMouseEvent(e, true); + }; + + handleMouseUp = () => { + if (this.state.isDragging) { + this.setState({ isDragging: false }); + } + }; + + handleMouseEvent = (e: React.MouseEvent, shouldSetValue: boolean = true) => { + if (!this.sliderRef.current || this.props.disabled) return; + const rect = this.sliderRef.current.getBoundingClientRect(); + const offset = this.props.vertical ? + (rect.bottom - e.clientY) : + (e.clientX - rect.left); + const total = this.props.vertical ? rect.height : rect.width; + const percentage = Math.min(Math.max(offset / total, 0), 1); + const value = percentage * this.props.max; + + if (shouldSetValue && (this.state.isDragging || e.type === 'mousedown')) { + this.props.onChange(value); + } + + this.setState({ + movingInfo: { + progress: percentage, + offset: this.props.vertical ? offset - rect.height / 2 : offset - rect.width / 2 + }, + }); + }; + + handleMouseMove = (e: React.MouseEvent) => { + this.handleMouseEvent(e, true); + } + + handleMouseLeave = () => { + this.setState({ isHovering: false }); + this.setState({ isDragging: false }); + } + + render() { + const { vertical, width, height, showTooltip, max, value: currentValue } = this.props; + const { movingInfo, isHovering } = this.state; + const sliderContent = ( +
+
+
+
+
+
+ ); + + return showTooltip ? ( + + {sliderContent} + + ) : sliderContent; + } +} \ No newline at end of file diff --git a/packages/semi-ui/audioPlayer/index.tsx b/packages/semi-ui/audioPlayer/index.tsx index 5ea8009b07..c75bf8e039 100644 --- a/packages/semi-ui/audioPlayer/index.tsx +++ b/packages/semi-ui/audioPlayer/index.tsx @@ -1,16 +1,36 @@ -import { BaseProps } from '_base/baseComponent'; +import BaseComponent, { BaseProps } from '../_base/baseComponent'; import React from 'react'; import cls from 'classnames'; import '@douyinfe/semi-foundation/audioPlayer/audioPlayer.scss'; import { cssClasses } from '@douyinfe/semi-foundation/audioPlayer/constants'; import Button from '../button'; +import Dropdown from '../dropdown'; import Image from '../image'; -import { IconBackward, IconFastForward, IconMusic, IconPause, IconPlay, IconRefresh, IconRestart, IconVolume2 } from '@douyinfe/semi-icons'; +import Tooltip from '../tooltip'; +import Popover from '../popover'; +import { IconBackward, IconFastForward, IconMiniPlayer, IconPause, IconPlay, IconRefresh, IconRestart, IconVolume2 } from '@douyinfe/semi-icons'; +import AudioSlider from './audioSlider'; +import AudioPlayerFoundation from '@douyinfe/semi-foundation/audioPlayer/foundation'; +import { AudioPlayerAdapter } from '@douyinfe/semi-foundation/audioPlayer/foundation'; +import { formatTime } from './utils'; + +type AudioSrc = string +type AudioSrcArray = string[] +type AudioInfo = { + title?: string; + cover?: string; + src: string +} +type AudioInfoArray = AudioInfo[] + +type AudioUrl = AudioSrc | AudioSrcArray | AudioInfo | AudioInfoArray + export interface AudioPlayerProps extends BaseProps { - audioUrl: string | string[]; + audioUrl: AudioUrl; autoPlay: boolean; - audioCover?: string; - audioTitle?: string; + showToolbar?: boolean; + skipDuration?: number; + theme?: 'dark' | 'light'; className?: string; style?: React.CSSProperties } @@ -19,17 +39,32 @@ export interface AudioPlayerState { isPlaying: boolean; currentIndex: number; totalTime: number; - currentTime: number + currentTime: number; + currentRate: { label: string; value: number }; + volume: number } const prefixCls = cssClasses.PREFIX; -class AudioPlayer extends React.Component { +class AudioPlayer extends BaseComponent { audioRef = React.createRef(); static defaultProps: Partial = { autoPlay: false, + showToolbar: true, + skipDuration: 10, + theme: 'dark', }; + rateOptions = [ + { label: '0.5x', value: 0.5 }, + { label: '0.75x', value: 0.75 }, + { label: '1.0x', value: 1 }, + { label: '1.5x', value: 1.5 }, + { label: '2.0x', value: 2 }, + ]; + + foundation!: AudioPlayerFoundation; + constructor(props: AudioPlayerProps) { super(props); this.state = { @@ -37,63 +72,245 @@ class AudioPlayer extends React.Component { currentIndex: 0, totalTime: 0, currentTime: 0, + currentRate: { label: '1.0x', value: 1 }, + volume: 100, }; this.audioRef = React.createRef(); + this.foundation = new AudioPlayerFoundation(this.adapter); } - handleClick = () => { - if (!this.audioRef.current) return; - if (this.state.isPlaying) { - this.audioRef.current.pause(); - } else { - this.audioRef.current.play(); - } - this.setState({ - isPlaying: !this.state.isPlaying, - }); + get adapter(): AudioPlayerAdapter { + return { + ...super.adapter, + initAudio: () => { + if (this.audioRef.current) { + this.audioRef.current.addEventListener('loadedmetadata', () => { + this.setState({ + totalTime: this.audioRef.current?.duration || 0, + volume: this.audioRef.current?.volume * 100 || 100, + currentRate: { label: '1.0x', value: this.audioRef.current?.playbackRate || 1 }, + }); + }); + } + }, + handleStatusClick: () => { + if (!this.audioRef.current) return; + if (this.state.isPlaying) { + this.audioRef.current.pause(); + } else { + this.audioRef.current.play(); + } + this.setState({ + isPlaying: !this.state.isPlaying, + }); + }, + getAudioRef: () => this.audioRef, + resetAudioState: () => { + this.setState({ + isPlaying: true, + currentTime: 0, + currentRate: { label: '1.0x', value: 1 } + }, () => { + if (this.audioRef.current) { + this.audioRef.current.currentTime = this.state.currentTime; + this.audioRef.current.playbackRate = this.state.currentRate.value; + this.audioRef.current.play(); + } + }); + }, + handleTimeUpdate: () => { + if (!this.audioRef.current) return; + this.setState({ + currentTime: this.audioRef.current.currentTime, + }); + }, + handleTrackChange: (direction: 'next' | 'prev') => { + if (!this.audioRef.current) return; + const { audioUrl } = this.props as AudioPlayerProps; + const isAudioUrlArray = Array.isArray(audioUrl); + if (isAudioUrlArray) { + if (direction === 'next') { + this.setState({ + currentIndex: (this.state.currentIndex + 1) % audioUrl.length, + }); + } else { + this.setState({ + currentIndex: (this.state.currentIndex - 1 + audioUrl.length) % audioUrl.length, + }); + } + } + this.foundation.resetAudioState(); + }, + handleTimeChange: (value: number) => { + if (!this.audioRef.current) return; + this.audioRef.current.currentTime = value; + this.setState({ + currentTime: value, + }); + }, + handleRefresh: () => { + if (!this.audioRef.current) return; + this.audioRef.current.currentTime = 0; + this.setState({ + currentTime: 0, + }); + }, + handleSpeedChange: (value: { label: string; value: number }) => { + if (!this.audioRef.current) return; + this.audioRef.current.playbackRate = value.value; + this.setState({ + currentRate: value, + }); + }, + handleSeek: (direction: number) => { + if (!this.audioRef.current) return; + const { skipDuration = 10 } = this.props; + const newTime = Math.min( + Math.max(this.audioRef.current.currentTime + (direction * skipDuration), 0), + this.audioRef.current.duration + ); + this.audioRef.current.currentTime = newTime; + }, + handleVolumeChange: (value: number) => { + if (!this.audioRef.current) return; + const volume = Math.round(value); + this.audioRef.current.volume = volume / 100; + this.setState({ + volume: volume, + }); + }, + }; + } + + handleStatusClick = () => { + this.foundation.handleStatusClick(); + } + + handleTrackChange = (direction: 'next' | 'prev') => { + this.foundation.handleTrackChange(direction); + } + + handleTimeChange = (value: number) => { + this.foundation.handleTimeChange(value); + } + + handleRefresh = () => { + this.foundation.handleRefresh(); + } + + handleSpeedChange = (value: { label: string; value: number }) => { + this.foundation.handleSpeedChange(value); } + handleSeek = (direction: number) => { + this.foundation.handleSeek(direction); + } + + handleTimeUpdate = () => { + this.foundation.handleTimeUpdate(); + } + + handleVolumeChange = (value: number) => { + this.foundation.handleVolumeChange(value); + } + + componentDidMount() { - console.log(this.audioRef.current?.duration); + this.foundation.initAudio(); + } + + getAudioInfo = (audioUrl: AudioUrl) => { + const isAudioUrlArray = Array.isArray(audioUrl); + if (isAudioUrlArray) { + const audioInfo = audioUrl[this.state.currentIndex]; + if (typeof audioInfo === 'string') { + return { src: audioInfo, audioTitle: null, audioCover: null }; + } else { + return { src: audioInfo.src, audioTitle: audioInfo.title, audioCover: audioInfo.cover }; + } + } else if (typeof audioUrl === 'string') { + return { src: audioUrl, audioTitle: null, audioCover: null }; + } else { + return { src: audioUrl.src, audioTitle: audioUrl.title, audioCover: audioUrl.cover }; + } } render() { - const { audioUrl, autoPlay, className, style, audioTitle, audioCover } = this.props as AudioPlayerProps; - const { currentIndex } = this.state; + const { audioUrl, autoPlay, className, style, showToolbar = true, skipDuration = 10, theme = 'dark' } = this.props as AudioPlayerProps; + const { currentTime, totalTime, volume } = this.state; const isAudioUrlArray = Array.isArray(audioUrl); - const src = isAudioUrlArray ? audioUrl[currentIndex] : audioUrl; + const { src, audioTitle, audioCover } = this.getAudioInfo(audioUrl); const iconClass = cls(`${prefixCls}-control-button-icon`); + const circleStyle = { + borderRadius: '50%', + }; + const transparentStyle = { + background: 'transparent', + }; return ( -
-
); } diff --git a/packages/semi-ui/audioPlayer/utils.ts b/packages/semi-ui/audioPlayer/utils.ts new file mode 100644 index 0000000000..87af2037df --- /dev/null +++ b/packages/semi-ui/audioPlayer/utils.ts @@ -0,0 +1,5 @@ +export const formatTime = (time: number) => { + const minutes = Math.floor(time / 60); + const seconds = Math.floor(time % 60); + return `${minutes}:${seconds.toString().padStart(2, '0')}`; +}; \ No newline at end of file From 4f46ccfdccdb19bf23dd65d88175c261e721d2aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E4=B8=B0?= Date: Tue, 24 Dec 2024 14:53:46 +0800 Subject: [PATCH 03/16] style: add css variables --- .../audioPlayer/audioPlayer.scss | 88 +++++++++++++------ .../audioPlayer/variables.scss | 14 ++- 2 files changed, 71 insertions(+), 31 deletions(-) diff --git a/packages/semi-foundation/audioPlayer/audioPlayer.scss b/packages/semi-foundation/audioPlayer/audioPlayer.scss index d70cb3d33b..d7625e2c42 100644 --- a/packages/semi-foundation/audioPlayer/audioPlayer.scss +++ b/packages/semi-foundation/audioPlayer/audioPlayer.scss @@ -6,14 +6,14 @@ $module: #{$prefix}-audio-player; display: flex; align-items: center; justify-content: center; - gap: 24px; - width: 1440px; + gap: $gap-audio-player-large; + max-width: 1440px; height: 78px; background: $color-audio-player-background; &-control { display: flex; align-items: center; - gap: 16px; + gap: $gap-audio-player-medium; } &-control-button-icon { @@ -25,36 +25,43 @@ $module: #{$prefix}-audio-player; color: $color-audio-player-control-icon-play !important; } + &-control-button-play-disabled { + background: #f9f9f959 !important; + color: var(--semi-color-grey-7) !important; + } + &-slider-container { width: 323px; + height: 100%; } - &-info-cover { + &-info-container { display: flex; align-items: center; - gap: 16px; + gap: $gap-audio-player-medium; } &-info { - width: 412px; display: flex; flex-direction: column; - gap: 4px; + gap: $gap-audio-player-small; } &-info-title { - height: 20px; - font-size: 14px; + font-size: $font-size-audio-player-text; color: $color-audio-player-font-color; + display: flex; + align-items: center; } &-info-time { width: 100%; height: 22px; - font-size: 14px; + font-size: $font-size-audio-player-text; color: $color-audio-player-font-color; display: flex; align-items: center; justify-content: space-between; + gap: $gap-audio-player-small; } &-control-speed { width: 40px; @@ -62,48 +69,56 @@ $module: #{$prefix}-audio-player; display: flex; align-items: center; justify-content: center; - gap: 4px; - background: #35363c; + gap: $gap-audio-player-small; + background: rgba(var(--semi-grey-8), 1); border-radius: 3px; font-size: 12px; line-height: 16px; - color: #f9f9f9; + color: var(--semi-color-default); font-weight: 600; } &-control-speed-menu { - background: #35363c; + background: rgba(var(--semi-grey-8), 1); width: 65px; } &-control-speed-menu-item { - color: #f9f9f9; + color: var(--semi-color-default); } &-control-speed-menu-item:hover { - background: #43444a !important; + background: var(--semi-color-tertiary-active) !important; } &-control-volume { width: 43px; height: 164px; - background: #35363c; + background: rgba(var(--semi-grey-8), 1); border-radius: 4px; padding: 4px 0; display: flex; align-items: center; justify-content: center; flex-direction: column; - gap: 8px; + gap: $gap-audio-player-small * 2; } &-control-volume-title { font-size: 12px; line-height: 16px; - color: #f9f9f9; + color: var(--semi-color-default); font-weight: 600; } + &-error { + display: flex; + align-items: center; + gap: $gap-audio-player-small; + margin-left: 4px; + color: var(--semi-color-danger); + } + &-light { background: $color-audio-player-background-light; border: 1px solid var(--semi-color-border); @@ -117,6 +132,11 @@ $module: #{$prefix}-audio-player; color: $color-audio-player-control-icon-play-light !important; } + .#{$module}-control-button-play-disabled { + background: var(--semi-color-disabled-text) !important; + color: rgba(var(--semi-white), 1) !important; + } + .#{$module}-info-title, .#{$module}-info-time { color: $color-audio-player-font-color-light; @@ -124,20 +144,34 @@ $module: #{$prefix}-audio-player; .#{$module}-control-speed-menu-item, .#{$module}-control-volume-title { - color: #1f1f1f; + color: rgba(var(--semi-grey-9), 1); } .#{$module}-control-speed-menu-item:hover { - background: #e0e0e0 !important; + background: rgba(var(--semi-grey-1), 1) !important; } } } -.#{$module}-slide { - background: #DDE3E8; +.#{$module}-slider { + background: rgba(var(--semi-grey-1), 1); border-radius: 9999px; position: relative; + &-wrapper { + position: relative; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + &-vertical { + width: 100%; + } + &-horizontal { + height: 100%; + } + } + &-vertical { width: 4px; height: 100%; @@ -150,7 +184,7 @@ $module: #{$prefix}-audio-player; &-progress { position: absolute; - background: #3295FB; + background: rgba(var(--semi-blue-4), 1); border-radius: 9999px; } @@ -166,9 +200,9 @@ $module: #{$prefix}-audio-player; position: absolute; width: 16px; height: 16px; - background: #ffffff; - border: 1px solid #3295FB; - box-shadow: 0px 0px 4px 0px #0000004D; + background: rgba(var(--semi-white), 1); + border: 1px solid var(--semi-color-primary); + box-shadow: 0px 0px 4px 0px var(--semi-color-shadow); border-radius: 50%; transition: opacity 0.2s; } diff --git a/packages/semi-foundation/audioPlayer/variables.scss b/packages/semi-foundation/audioPlayer/variables.scss index 05ed524fa8..6c852e6d60 100644 --- a/packages/semi-foundation/audioPlayer/variables.scss +++ b/packages/semi-foundation/audioPlayer/variables.scss @@ -1,11 +1,17 @@ $color-audio-player-background: rgba(var(--semi-grey-9), .8); $color-audio-player-control-icon: var(--semi-color-bg-0); $color-audio-player-control-icon-play: var(--semi-color-text-0); -$color-audio-player-font-color: var(--semi-color-bg-0); +$color-audio-player-font-color: rgba(var(--semi-white), 1); $color-audio-player-font-color-speed: rgba(var(--semi-grey-8), 1); $color-audio-player-background-light: var(--semi-color-bg-0); -$color-audio-player-control-icon-light: #1f1f1f; -$color-audio-player-control-icon-play-light: #ffffff; -$color-audio-player-font-color-light: #1f1f1f; \ No newline at end of file +$color-audio-player-control-icon-light: rgba(var(--semi-grey-9), 1); +$color-audio-player-control-icon-play-light: rgba(var(--semi-white), 1); +$color-audio-player-font-color-light: rgba(var(--semi-grey-9), 1); + +$font-size-audio-player-text: 14px; + +$gap-audio-player-small: 4px; +$gap-audio-player-medium: 16px; +$gap-audio-player-large: 24px; \ No newline at end of file From 70e776548bd65aba5603144c2c51fca079011313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E4=B8=B0?= Date: Tue, 24 Dec 2024 14:56:48 +0800 Subject: [PATCH 04/16] refactor: optimize code structure --- packages/semi-ui/audioPlayer/audioSlider.tsx | 22 +-- packages/semi-ui/audioPlayer/index.tsx | 191 +++++++++++-------- 2 files changed, 126 insertions(+), 87 deletions(-) diff --git a/packages/semi-ui/audioPlayer/audioSlider.tsx b/packages/semi-ui/audioPlayer/audioSlider.tsx index 8a04bc2596..f65d9a2fae 100644 --- a/packages/semi-ui/audioPlayer/audioSlider.tsx +++ b/packages/semi-ui/audioPlayer/audioSlider.tsx @@ -78,7 +78,6 @@ export default class AudioSlider extends React.Component
{ currentTime: 0, currentRate: { label: '1.0x', value: 1 }, volume: 100, + error: false, }; this.audioRef = React.createRef(); this.foundation = new AudioPlayerFoundation(this.adapter); @@ -91,6 +95,11 @@ class AudioPlayer extends BaseComponent { currentRate: { label: '1.0x', value: this.audioRef.current?.playbackRate || 1 }, }); }); + this.audioRef.current.addEventListener('error', () => { + this.setState({ + error: true, + }); + }); } }, handleStatusClick: () => { @@ -132,10 +141,12 @@ class AudioPlayer extends BaseComponent { if (direction === 'next') { this.setState({ currentIndex: (this.state.currentIndex + 1) % audioUrl.length, + error: false, }); } else { this.setState({ currentIndex: (this.state.currentIndex - 1 + audioUrl.length) % audioUrl.length, + error: false, }); } } @@ -150,10 +161,14 @@ class AudioPlayer extends BaseComponent { }, handleRefresh: () => { if (!this.audioRef.current) return; - this.audioRef.current.currentTime = 0; - this.setState({ - currentTime: 0, - }); + if (this.state.error) { + this.audioRef.current.load(); + } else { + this.audioRef.current.currentTime = 0; + this.setState({ + currentTime: 0, + }); + } }, handleSpeedChange: (value: { label: string; value: number }) => { if (!this.audioRef.current) return; @@ -182,6 +197,10 @@ class AudioPlayer extends BaseComponent { }; } + componentDidMount() { + this.foundation.initAudio(); + } + handleStatusClick = () => { this.foundation.handleStatusClick(); } @@ -214,11 +233,6 @@ class AudioPlayer extends BaseComponent { this.foundation.handleVolumeChange(value); } - - componentDidMount() { - this.foundation.initAudio(); - } - getAudioInfo = (audioUrl: AudioUrl) => { const isAudioUrlArray = Array.isArray(audioUrl); if (isAudioUrlArray) { @@ -235,11 +249,9 @@ class AudioPlayer extends BaseComponent { } } - render() { - const { audioUrl, autoPlay, className, style, showToolbar = true, skipDuration = 10, theme = 'dark' } = this.props as AudioPlayerProps; - const { currentTime, totalTime, volume } = this.state; - const isAudioUrlArray = Array.isArray(audioUrl); - const { src, audioTitle, audioCover } = this.getAudioInfo(audioUrl); + renderControl = () => { + const { error } = this.state; + const isAudioUrlArray = Array.isArray(this.props.audioUrl); const iconClass = cls(`${prefixCls}-control-button-icon`); const circleStyle = { borderRadius: '50%', @@ -247,70 +259,97 @@ class AudioPlayer extends BaseComponent { const transparentStyle = { background: 'transparent', }; + return
+ {isAudioUrlArray && +
; + } + + renderInfo = () => { + const { audioTitle, audioCover } = this.getAudioInfo(this.props.audioUrl); + const { currentTime, totalTime, error } = this.state; + return
+ {audioCover && } +
+ {audioTitle &&
{audioTitle}{error && this.renderError()}
} + {!error &&
+ {formatTime(currentTime)} +
+ +
+ {formatTime(totalTime)} +
} +
+
; + } + + renderToolbar = () => { + const { volume, error } = this.state; + const { skipDuration = 10 } = this.props; + const iconClass = cls(`${prefixCls}-control-button-icon`); + const transparentStyle = { + background: 'transparent', + }; + return !error ? (
+ +
{volume}%
+ +
+ }> +
) : (
+
); + } + + + renderError = () =>
音频加载失败
+ + render() { + const { audioUrl, autoPlay, className, style, showToolbar = true, theme = 'dark' } = this.props as AudioPlayerProps; + const src = this.getAudioInfo(audioUrl).src; return (
-
- {isAudioUrlArray && -
-
- {audioCover && } -
- {audioTitle &&
{audioTitle}
} -
- {formatTime(currentTime)} -
- -
- {formatTime(totalTime)} -
-
-
- {showToolbar &&
- -
{volume}%
- -
- }> -
} + {this.renderControl()} + {this.renderInfo()} + {showToolbar && this.renderToolbar()}
); } From 059f4b57d8695fc93f6e40b385125f4c06545dc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E4=B8=B0?= Date: Wed, 25 Dec 2024 14:06:19 +0800 Subject: [PATCH 05/16] docs: aduioplayer api doc --- content/order.js | 1 + content/plus/audioPlayer/index.md | 179 ++++++++++++++++++++++++ packages/semi-ui/index.ts | 1 + src/images/docIcons/doc-audioplayer.svg | 5 + 4 files changed, 186 insertions(+) create mode 100644 content/plus/audioPlayer/index.md create mode 100644 src/images/docIcons/doc-audioplayer.svg diff --git a/content/order.js b/content/order.js index 5a7c8126db..5f4cf2943c 100644 --- a/content/order.js +++ b/content/order.js @@ -88,6 +88,7 @@ const order = [ 'configprovider', 'locale', 'jsonviewer', + 'audioPlayer', ]; let { exec } = require('child_process'); let fs = require('fs'); diff --git a/content/plus/audioPlayer/index.md b/content/plus/audioPlayer/index.md new file mode 100644 index 0000000000..9c62b3174f --- /dev/null +++ b/content/plus/audioPlayer/index.md @@ -0,0 +1,179 @@ +--- +localeCode: zh-CN +order: 90 +category: Plus +title: AudioPlayer 音频播放器 +icon: doc-audioplayer +width: 60% +brief: 用于方便用户自定义快捷键及相关操作 +showNew: true +--- + +## 代码演示 + +### 如何引入 +AudioPlayer 从 开始支持 + +```jsx import +import { AudioPlayer } from '@douyinfe/semi-ui'; +``` + + +### 基本用法 + +基本使用,通过`audioUrl`传入音频地址 +audioUrl 可以传入字符串,字符串数组,对象,对象数组, 具体参数参考 [AudioPlayer](#AudioPlayer) + +```jsx live=true noInline=true dir="column" +import React from 'react'; +import { AudioPlayer } from '@douyinfe/semi-ui'; + +function Demo() { + const audioUrl = 'http://music.163.com/song/media/outer/url?id=447925558.mp3'; + const audioUrlArr = [ + 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + ]; + const audioUrlObj = { + src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + title: '音频标题', + cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', + }; + const audioUrlArrObj = [ + { + src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + title: '音频标题1', + cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', + }, + { + src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + title: '音频标题2', + cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', + }, + ]; + + return ( +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ ); +} + +render(Demo); + +``` + + +### 隐藏工具栏 + +showToolbar 设置为false,则隐藏工具栏 + + +```jsx live=true noInline=true dir="column" +import React from 'react'; +import { AudioPlayer } from '@douyinfe/semi-ui'; + +function Demo() { + const audioUrlObj = { + src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + title: '音频标题' + }; + + return ( +
+ +
+ ); +} + +render(Demo); + +``` + +### 主题 + +通过 `theme` 设置音频播放器主题,支持 `light` 和 `dark`,默认 `dark` + + +```jsx live=true noInline=true dir="column" +import React from 'react'; +import { AudioPlayer } from '@douyinfe/semi-ui'; + +function Demo() { + const audioUrlArrObj = [ + { + src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + title: '音频标题1', + cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', + }, + { + src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + title: '音频标题2', + cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', + }, + ]; + + return ( +
+ +
+ ); +} + +render(Demo); + +``` + +## API 参考 + +### AudioPlayer + +| 属性 | 说明 | 类型 | 默认值 | +|-------------------|------------------------------------------------|---------------------------------|--------------| +| audioUrl | 音频地址 | string | string[] | AudioInfo | AudioInfo[] | - | +| autoPlay | 自动播放 | boolean | false | +| theme | 主题 | string | dark | +| showToolbar | 是否显示工具栏 | boolean | false | +| skipDuration | 跳转时间 | number | 10 | +| className | 类名 | string | - | +| style | 内联样式 | object | - | + +### AudioInfo + +| 属性 | 说明 | 类型 | 默认值 | +|-------------------|------------------------------------------------|---------------------------------|-----------| +| src | 音频地址 | string | - | +| title | 音频标题 | string | - | +| cover | 封面图片 | string | - | + + diff --git a/packages/semi-ui/index.ts b/packages/semi-ui/index.ts index 687008f69c..1708a2df35 100644 --- a/packages/semi-ui/index.ts +++ b/packages/semi-ui/index.ts @@ -125,3 +125,4 @@ export { export { default as JsonViewer } from './jsonViewer'; export { default as DragMove } from './dragMove'; +export { default as AudioPlayer } from './audioPlayer'; \ No newline at end of file diff --git a/src/images/docIcons/doc-audioplayer.svg b/src/images/docIcons/doc-audioplayer.svg new file mode 100644 index 0000000000..4ef947b274 --- /dev/null +++ b/src/images/docIcons/doc-audioplayer.svg @@ -0,0 +1,5 @@ + + + + + From 6b58a376593f8a63afae3b8ce7284f340b13827c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E4=B8=B0?= Date: Wed, 25 Dec 2024 14:28:50 +0800 Subject: [PATCH 06/16] style: scss variables --- .../audioPlayer/audioPlayer.scss | 73 ++++++++++--------- .../audioPlayer/variables.scss | 43 ++++++++++- 2 files changed, 78 insertions(+), 38 deletions(-) diff --git a/packages/semi-foundation/audioPlayer/audioPlayer.scss b/packages/semi-foundation/audioPlayer/audioPlayer.scss index d7625e2c42..35cbb80dc9 100644 --- a/packages/semi-foundation/audioPlayer/audioPlayer.scss +++ b/packages/semi-foundation/audioPlayer/audioPlayer.scss @@ -7,8 +7,8 @@ $module: #{$prefix}-audio-player; align-items: center; justify-content: center; gap: $gap-audio-player-large; - max-width: 1440px; - height: 78px; + max-width: $width-audio-player-max; + height: $height-audio-player; background: $color-audio-player-background; &-control { display: flex; @@ -26,12 +26,12 @@ $module: #{$prefix}-audio-player; } &-control-button-play-disabled { - background: #f9f9f959 !important; - color: var(--semi-color-grey-7) !important; + background: $color-audio-player-disabled-bg !important; + color: $color-audio-player-disabled-text !important; } &-slider-container { - width: 323px; + width: $width-audio-player-slider; height: 100%; } @@ -55,36 +55,38 @@ $module: #{$prefix}-audio-player; } &-info-time { width: 100%; - height: 22px; + height: $height-audio-player-time; font-size: $font-size-audio-player-text; color: $color-audio-player-font-color; display: flex; align-items: center; justify-content: space-between; gap: $gap-audio-player-small; + user-select: none; } &-control-speed { - width: 40px; - height: 24px; + width: $width-audio-player-speed; + height: $height-audio-player-speed; display: flex; align-items: center; justify-content: center; gap: $gap-audio-player-small; - background: rgba(var(--semi-grey-8), 1); - border-radius: 3px; - font-size: 12px; - line-height: 16px; + background: $color-audio-player-font-color-speed; + border-radius: $border-radius-audio-player-speed; + font-size: $font-size-audio-player-small; + line-height: $line-height-audio-player-small; color: var(--semi-color-default); font-weight: 600; + user-select: none; } &-control-speed-menu { - background: rgba(var(--semi-grey-8), 1); - width: 65px; + background: $color-audio-player-font-color-speed; + width: $width-audio-player-speed-menu; } &-control-speed-menu-item { - color: var(--semi-color-default); + color: $color-audio-player-text-default; } &-control-speed-menu-item:hover { @@ -92,10 +94,10 @@ $module: #{$prefix}-audio-player; } &-control-volume { - width: 43px; - height: 164px; - background: rgba(var(--semi-grey-8), 1); - border-radius: 4px; + width: $width-audio-player-volume; + height: $height-audio-player-volume; + background: $color-audio-player-font-color-speed; + border-radius: $border-radius-audio-player-volume; padding: 4px 0; display: flex; align-items: center; @@ -105,10 +107,11 @@ $module: #{$prefix}-audio-player; } &-control-volume-title { - font-size: 12px; - line-height: 16px; - color: var(--semi-color-default); + font-size: $font-size-audio-player-small; + line-height: $line-height-audio-player-small; + color: $color-audio-player-text-default; font-weight: 600; + user-select: none; } &-error { @@ -133,8 +136,8 @@ $module: #{$prefix}-audio-player; } .#{$module}-control-button-play-disabled { - background: var(--semi-color-disabled-text) !important; - color: rgba(var(--semi-white), 1) !important; + background: $color-audio-player-light-disabled-bg !important; + color: $color-audio-player-light-disabled-text !important; } .#{$module}-info-title, @@ -144,18 +147,18 @@ $module: #{$prefix}-audio-player; .#{$module}-control-speed-menu-item, .#{$module}-control-volume-title { - color: rgba(var(--semi-grey-9), 1); + color: $color-audio-player-light-text; } .#{$module}-control-speed-menu-item:hover { - background: rgba(var(--semi-grey-1), 1) !important; + background: $color-audio-player-light-hover-bg !important; } } } .#{$module}-slider { - background: rgba(var(--semi-grey-1), 1); - border-radius: 9999px; + background: $color-audio-player-slider-bg; + border-radius: $border-radius-audio-player-slider; position: relative; &-wrapper { @@ -173,19 +176,19 @@ $module: #{$prefix}-audio-player; } &-vertical { - width: 4px; + width: $width-audio-player-slider-bar; height: 100%; } &-horizontal { width: 100%; - height: 4px; + height: $width-audio-player-slider-bar; } &-progress { position: absolute; - background: rgba(var(--semi-blue-4), 1); - border-radius: 9999px; + background: $color-audio-player-slider-progress; + border-radius: $border-radius-audio-player-slider; } &-progress-vertical { @@ -198,9 +201,9 @@ $module: #{$prefix}-audio-player; &-dot { position: absolute; - width: 16px; - height: 16px; - background: rgba(var(--semi-white), 1); + width: $size-audio-player-slider-dot; + height: $size-audio-player-slider-dot; + background: $color-audio-player-slider-dot-bg; border: 1px solid var(--semi-color-primary); box-shadow: 0px 0px 4px 0px var(--semi-color-shadow); border-radius: 50%; diff --git a/packages/semi-foundation/audioPlayer/variables.scss b/packages/semi-foundation/audioPlayer/variables.scss index 6c852e6d60..d05ecf4431 100644 --- a/packages/semi-foundation/audioPlayer/variables.scss +++ b/packages/semi-foundation/audioPlayer/variables.scss @@ -1,17 +1,54 @@ $color-audio-player-background: rgba(var(--semi-grey-9), .8); $color-audio-player-control-icon: var(--semi-color-bg-0); $color-audio-player-control-icon-play: var(--semi-color-text-0); -$color-audio-player-font-color: rgba(var(--semi-white), 1); +$color-audio-player-font-color: var(--semi-color-bg-0); $color-audio-player-font-color-speed: rgba(var(--semi-grey-8), 1); $color-audio-player-background-light: var(--semi-color-bg-0); $color-audio-player-control-icon-light: rgba(var(--semi-grey-9), 1); -$color-audio-player-control-icon-play-light: rgba(var(--semi-white), 1); +$color-audio-player-control-icon-play-light: var(--semi-color-bg-0); $color-audio-player-font-color-light: rgba(var(--semi-grey-9), 1); $font-size-audio-player-text: 14px; $gap-audio-player-small: 4px; $gap-audio-player-medium: 16px; -$gap-audio-player-large: 24px; \ No newline at end of file +$gap-audio-player-large: 24px; + +// Size variables +$width-audio-player-max: 1440px; +$height-audio-player: 78px; +$width-audio-player-slider: 323px; +$width-audio-player-speed: 40px; +$height-audio-player-speed: 24px; +$width-audio-player-speed-menu: 65px; +$width-audio-player-volume: 43px; +$height-audio-player-volume: 164px; +$height-audio-player-time: 22px; + +// Border radius +$border-radius-audio-player-speed: 3px; +$border-radius-audio-player-volume: 4px; +$border-radius-audio-player-slider: 9999px; + +// Font sizes +$font-size-audio-player-small: 12px; +$line-height-audio-player-small: 16px; + +// Slider dimensions +$width-audio-player-slider-bar: 4px; +$size-audio-player-slider-dot: 16px; + +// Colors +$color-audio-player-disabled-bg: rgba(var(--semi-grey-0), .35); +$color-audio-player-slider-bg: rgba(var(--semi-grey-1), 1); +$color-audio-player-slider-progress: rgba(var(--semi-blue-4), 1); +$color-audio-player-slider-dot-bg: rgba(var(--semi-white), 1); + +$color-audio-player-disabled-text: var(--semi-color-grey-7); +$color-audio-player-text-default: var(--semi-color-default); +$color-audio-player-light-disabled-bg: var(--semi-color-disabled-text); +$color-audio-player-light-disabled-text: rgba(var(--semi-white), 1); +$color-audio-player-light-text: rgba(var(--semi-grey-9), 1); +$color-audio-player-light-hover-bg: rgba(var(--semi-grey-1), 1); \ No newline at end of file From c653eb5fa68af55bf1156674165808baa5e3329a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E4=B8=B0?= Date: Wed, 25 Dec 2024 14:30:01 +0800 Subject: [PATCH 07/16] fix: resolve minor problems --- .../audioPlayer/_story/autoPlayer.stories.jsx | 6 ++--- packages/semi-ui/audioPlayer/audioSlider.tsx | 1 - packages/semi-ui/audioPlayer/index.tsx | 26 ++++++++++++++++--- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/semi-ui/audioPlayer/_story/autoPlayer.stories.jsx b/packages/semi-ui/audioPlayer/_story/autoPlayer.stories.jsx index 2445365168..9356b62ed4 100644 --- a/packages/semi-ui/audioPlayer/_story/autoPlayer.stories.jsx +++ b/packages/semi-ui/audioPlayer/_story/autoPlayer.stories.jsx @@ -20,7 +20,7 @@ export const DefaultAutoPlay = () => { { title: '音频标题1', cover: 'https://picsum.photos/50/50', - src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + src: 'http://music.163.com/song/media/outer/url?id=447925558.mp', }, { title: '音频标题2', @@ -34,12 +34,10 @@ export const DefaultAutoPlay = () => { }, ]; return ( -
+
); diff --git a/packages/semi-ui/audioPlayer/audioSlider.tsx b/packages/semi-ui/audioPlayer/audioSlider.tsx index f65d9a2fae..50266f06a9 100644 --- a/packages/semi-ui/audioPlayer/audioSlider.tsx +++ b/packages/semi-ui/audioPlayer/audioSlider.tsx @@ -159,7 +159,6 @@ export default class AudioSlider extends React.Component {sliderContent} diff --git a/packages/semi-ui/audioPlayer/index.tsx b/packages/semi-ui/audioPlayer/index.tsx index 29a9c7cc88..635f6748a2 100644 --- a/packages/semi-ui/audioPlayer/index.tsx +++ b/packages/semi-ui/audioPlayer/index.tsx @@ -8,7 +8,7 @@ import Dropdown from '../dropdown'; import Image from '../image'; import Tooltip from '../tooltip'; import Popover from '../popover'; -import { IconAlertCircle, IconBackward, IconFastForward, IconMiniPlayer, IconPause, IconPlay, IconRefresh, IconRestart, IconVolume2 } from '@douyinfe/semi-icons'; +import { IconAlertCircle, IconBackward, IconFastForward, IconMiniPlayer, IconPause, IconPlay, IconRefresh, IconRestart, IconVolume2, IconVolumnSilent } from '@douyinfe/semi-icons'; import AudioSlider from './audioSlider'; import AudioPlayerFoundation from '@douyinfe/semi-foundation/audioPlayer/foundation'; import { AudioPlayerAdapter } from '@douyinfe/semi-foundation/audioPlayer/foundation'; @@ -100,6 +100,16 @@ class AudioPlayer extends BaseComponent { error: true, }); }); + this.audioRef.current.addEventListener('ended', () => { + console.log('ended'); + if (Array.isArray(this.props.audioUrl)) { + this.handleTrackChange('next'); + } else { + this.setState({ + isPlaying: false, + }); + } + }); } }, handleStatusClick: () => { @@ -188,7 +198,7 @@ class AudioPlayer extends BaseComponent { }, handleVolumeChange: (value: number) => { if (!this.audioRef.current) return; - const volume = Math.round(value); + const volume = Math.floor(value); this.audioRef.current.volume = volume / 100; this.setState({ volume: volume, @@ -233,6 +243,14 @@ class AudioPlayer extends BaseComponent { this.foundation.handleVolumeChange(value); } + handleVolumeSilent = () => { + if (!this.audioRef.current) return; + this.audioRef.current.volume = this.state.volume === 0 ? 0.5 : 0; + this.setState({ + volume: this.state.volume === 0 ? 50 : 0, + }); + } + getAudioInfo = (audioUrl: AudioUrl) => { const isAudioUrlArray = Array.isArray(audioUrl); if (isAudioUrlArray) { @@ -295,6 +313,7 @@ class AudioPlayer extends BaseComponent { const transparentStyle = { background: 'transparent', }; + const isVolumeSilent = volume === 0; return !error ? (
@@ -302,7 +321,7 @@ class AudioPlayer extends BaseComponent {
}> -
) : (
); From d2774d8b202cc9da333d56e4bf1f2fe47f2cd791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E4=B8=B0?= Date: Wed, 25 Dec 2024 14:32:58 +0800 Subject: [PATCH 08/16] feat: add storybook instance --- ...er.stories.jsx => audioPlayer.stories.jsx} | 2 +- .../_story/audioPlayer.stories.tsx | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) rename packages/semi-ui/audioPlayer/_story/{autoPlayer.stories.jsx => audioPlayer.stories.jsx} (98%) create mode 100644 packages/semi-ui/audioPlayer/_story/audioPlayer.stories.tsx diff --git a/packages/semi-ui/audioPlayer/_story/autoPlayer.stories.jsx b/packages/semi-ui/audioPlayer/_story/audioPlayer.stories.jsx similarity index 98% rename from packages/semi-ui/audioPlayer/_story/autoPlayer.stories.jsx rename to packages/semi-ui/audioPlayer/_story/audioPlayer.stories.jsx index 9356b62ed4..b0f967f77a 100644 --- a/packages/semi-ui/audioPlayer/_story/autoPlayer.stories.jsx +++ b/packages/semi-ui/audioPlayer/_story/audioPlayer.stories.jsx @@ -20,7 +20,7 @@ export const DefaultAutoPlay = () => { { title: '音频标题1', cover: 'https://picsum.photos/50/50', - src: 'http://music.163.com/song/media/outer/url?id=447925558.mp', + src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', }, { title: '音频标题2', diff --git a/packages/semi-ui/audioPlayer/_story/audioPlayer.stories.tsx b/packages/semi-ui/audioPlayer/_story/audioPlayer.stories.tsx new file mode 100644 index 0000000000..12ee3f22bd --- /dev/null +++ b/packages/semi-ui/audioPlayer/_story/audioPlayer.stories.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import AudioPlayer from '../index'; + +// Define interface for audio info object +interface AudioInfo { + title: string; + cover: string; + src: string; +} + +export default { + title: 'AudioPlayer', +}; + +export const DefaultAutoPlay: React.FC = () => { + const audioUrl = 'http://m10.music.126.net/20241218110843/57a53b2ae73e9c8a571024ae067788e2/ymusic/5353/0f0f/0358/d99739615f8e5153d77042092f07fd77.mp3'; // 替换为实际的音频URL + const audioUrlArray = [ + 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + ]; + const audioInfo: AudioInfo = { + title: '音频标题', + cover: 'https://picsum.photos/50/50', + src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + }; + const audioInfoArray: AudioInfo[] = [ + { + title: '音频标题1', + cover: 'https://picsum.photos/50/50', + src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + }, + { + title: '音频标题2', + cover: 'https://picsum.photos/100/100', + src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + }, + { + title: '音频标题3', + cover: 'https://picsum.photos/150/150', + src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + }, + ]; + return ( +
+ +
+ ); +}; From baedef0f255e908c0daa119281d2c1888f6a9622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E4=B8=B0?= Date: Wed, 25 Dec 2024 16:14:09 +0800 Subject: [PATCH 09/16] refactor: update audio source --- content/plus/audioPlayer/index.md | 18 +++++++++--------- .../_story/audioPlayer.stories.jsx | 19 +++++++------------ .../_story/audioPlayer.stories.tsx | 15 ++++++++------- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/content/plus/audioPlayer/index.md b/content/plus/audioPlayer/index.md index 9c62b3174f..7bd91ce15e 100644 --- a/content/plus/audioPlayer/index.md +++ b/content/plus/audioPlayer/index.md @@ -29,24 +29,24 @@ import React from 'react'; import { AudioPlayer } from '@douyinfe/semi-ui'; function Demo() { - const audioUrl = 'http://music.163.com/song/media/outer/url?id=447925558.mp3'; + const audioUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3'; const audioUrlArr = [ - 'http://music.163.com/song/media/outer/url?id=447925558.mp3', - 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', + 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3', ]; const audioUrlObj = { - src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', title: '音频标题', cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', }; const audioUrlArrObj = [ { - src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', title: '音频标题1', cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', }, { - src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3', title: '音频标题2', cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', }, @@ -98,7 +98,7 @@ import { AudioPlayer } from '@douyinfe/semi-ui'; function Demo() { const audioUrlObj = { - src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', title: '音频标题' }; @@ -129,12 +129,12 @@ import { AudioPlayer } from '@douyinfe/semi-ui'; function Demo() { const audioUrlArrObj = [ { - src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', title: '音频标题1', cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', }, { - src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3', title: '音频标题2', cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', }, diff --git a/packages/semi-ui/audioPlayer/_story/audioPlayer.stories.jsx b/packages/semi-ui/audioPlayer/_story/audioPlayer.stories.jsx index b0f967f77a..6a8fa5c99f 100644 --- a/packages/semi-ui/audioPlayer/_story/audioPlayer.stories.jsx +++ b/packages/semi-ui/audioPlayer/_story/audioPlayer.stories.jsx @@ -6,32 +6,27 @@ export default { }; export const DefaultAutoPlay = () => { - const audioUrl = 'http://m10.music.126.net/20241218110843/57a53b2ae73e9c8a571024ae067788e2/ymusic/5353/0f0f/0358/d99739615f8e5153d77042092f07fd77.mp3'; // 替换为实际的音频URL + const audioUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3'; // 替换为实际的音频URL const audioUrlArray = [ - 'http://music.163.com/song/media/outer/url?id=447925558.mp3', - 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', + 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3', ]; const audioInfo = { title: '音频标题', cover: 'https://picsum.photos/50/50', - src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', }; const audioInfoArray = [ { title: '音频标题1', cover: 'https://picsum.photos/50/50', - src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', }, { title: '音频标题2', cover: 'https://picsum.photos/100/100', - src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', - }, - { - title: '音频标题3', - cover: 'https://picsum.photos/150/150', - src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', - }, + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3', + } ]; return (
diff --git a/packages/semi-ui/audioPlayer/_story/audioPlayer.stories.tsx b/packages/semi-ui/audioPlayer/_story/audioPlayer.stories.tsx index 12ee3f22bd..3250305cce 100644 --- a/packages/semi-ui/audioPlayer/_story/audioPlayer.stories.tsx +++ b/packages/semi-ui/audioPlayer/_story/audioPlayer.stories.tsx @@ -13,31 +13,31 @@ export default { }; export const DefaultAutoPlay: React.FC = () => { - const audioUrl = 'http://m10.music.126.net/20241218110843/57a53b2ae73e9c8a571024ae067788e2/ymusic/5353/0f0f/0358/d99739615f8e5153d77042092f07fd77.mp3'; // 替换为实际的音频URL + const audioUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3'; // 替换为实际的音频URL const audioUrlArray = [ - 'http://music.163.com/song/media/outer/url?id=447925558.mp3', - 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', + 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3', ]; const audioInfo: AudioInfo = { title: '音频标题', cover: 'https://picsum.photos/50/50', - src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', }; const audioInfoArray: AudioInfo[] = [ { title: '音频标题1', cover: 'https://picsum.photos/50/50', - src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', }, { title: '音频标题2', cover: 'https://picsum.photos/100/100', - src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3', }, { title: '音频标题3', cover: 'https://picsum.photos/150/150', - src: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3', }, ]; return ( @@ -49,3 +49,4 @@ export const DefaultAutoPlay: React.FC = () => {
); }; + \ No newline at end of file From 7db0d4812522234a82235f86442fe85be654dcd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E4=B8=B0?= Date: Thu, 26 Dec 2024 10:37:06 +0800 Subject: [PATCH 10/16] docs: add overview cover --- content/start/overview/index.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/content/start/overview/index.md b/content/start/overview/index.md index 68f7655c81..908906af90 100644 --- a/content/start/overview/index.md +++ b/content/start/overview/index.md @@ -25,7 +25,9 @@ CodeHighlight 代码高亮, Markdown 渲染器, Lottie 动画, Chat 聊天, -HotKeys 快捷键 +HotKeys 快捷键, +JsonViewer Json编辑器, +AudioPlayer 音频播放器 ``` ## 输入类 From 905cd5d509af33b22b2a4b1d5c148a53b6f4bf59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E4=B8=B0?= Date: Thu, 26 Dec 2024 10:48:36 +0800 Subject: [PATCH 11/16] docs: correct doc errors --- content/plus/audioPlayer/index.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/content/plus/audioPlayer/index.md b/content/plus/audioPlayer/index.md index 7bd91ce15e..1d617f2c06 100644 --- a/content/plus/audioPlayer/index.md +++ b/content/plus/audioPlayer/index.md @@ -12,7 +12,6 @@ showNew: true ## 代码演示 ### 如何引入 -AudioPlayer 从 开始支持 ```jsx import import { AudioPlayer } from '@douyinfe/semi-ui'; @@ -162,8 +161,8 @@ render(Demo); |-------------------|------------------------------------------------|---------------------------------|--------------| | audioUrl | 音频地址 | string | string[] | AudioInfo | AudioInfo[] | - | | autoPlay | 自动播放 | boolean | false | -| theme | 主题 | string | dark | -| showToolbar | 是否显示工具栏 | boolean | false | +| theme | 主题,可选值:`dark` 和 `light` | string | "dark" | +| showToolbar | 是否显示工具栏 | boolean | true | | skipDuration | 跳转时间 | number | 10 | | className | 类名 | string | - | | style | 内联样式 | object | - | From dc58f1a010321c84ed298c632103b104e91acb11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E4=B8=B0?= Date: Mon, 6 Jan 2025 17:43:00 +0800 Subject: [PATCH 12/16] fix: resolve initial state issue with autoplay --- packages/semi-ui/audioPlayer/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/semi-ui/audioPlayer/index.tsx b/packages/semi-ui/audioPlayer/index.tsx index 635f6748a2..5bc64672c3 100644 --- a/packages/semi-ui/audioPlayer/index.tsx +++ b/packages/semi-ui/audioPlayer/index.tsx @@ -91,6 +91,7 @@ class AudioPlayer extends BaseComponent { this.audioRef.current.addEventListener('loadedmetadata', () => { this.setState({ totalTime: this.audioRef.current?.duration || 0, + isPlaying: this.props.autoPlay, volume: this.audioRef.current?.volume * 100 || 100, currentRate: { label: '1.0x', value: this.audioRef.current?.playbackRate || 1 }, }); From 9ed35df9ef25edecdf8ef16920c9c0b02ff6e363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E4=B8=B0?= Date: Mon, 6 Jan 2025 17:58:05 +0800 Subject: [PATCH 13/16] docs: audioPlayer api docs --- content/plus/audioPlayer/index-en-US.md | 171 ++++++++++++++++++++++++ content/plus/audioPlayer/index.md | 6 +- 2 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 content/plus/audioPlayer/index-en-US.md diff --git a/content/plus/audioPlayer/index-en-US.md b/content/plus/audioPlayer/index-en-US.md new file mode 100644 index 0000000000..52d4f3a202 --- /dev/null +++ b/content/plus/audioPlayer/index-en-US.md @@ -0,0 +1,171 @@ +--- +localeCode: en-US +order: 90 +category: Plus +title: AudioPlayer +icon: doc-audioplayer +width: 60% +brief: Used to play audio +showNew: true +--- + +## Demos + +### How to import + +```jsx import +import { AudioPlayer } from '@douyinfe/semi-ui'; +``` + +### Basic Usage + +Basic usage by passing audio URL through `audioUrl`. +audioUrl supports string, string array, object, and object array. See [AudioPlayer](#AudioPlayer) for detailed parameters. + +```jsx live=true noInline=true dir="column" +import React from 'react'; +import { AudioPlayer } from '@douyinfe/semi-ui'; + +function Demo() { + const audioUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3'; + const audioUrlArr = [ + 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', + 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3', + ]; + const audioUrlObj = { + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', + title: 'Audio Title', + cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', + }; + const audioUrlArrObj = [ + { + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', + title: 'Audio Title 1', + cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', + }, + { + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3', + title: 'Audio Title 2', + cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', + }, + ]; + + return ( +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ ); +} + +render(Demo); +``` + +### Hide Toolbar + +Set showToolbar to false to hide the toolbar + +```jsx live=true noInline=true dir="column" +import React from 'react'; +import { AudioPlayer } from '@douyinfe/semi-ui'; + +function Demo() { + const audioUrlObj = { + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', + title: 'Audio Title' + }; + + return ( +
+ +
+ ); +} + +render(Demo); +``` + +### Theme + +Set the audio player theme through `theme`, supports `light` and `dark`, default is `dark` + +```jsx live=true noInline=true dir="column" +import React from 'react'; +import { AudioPlayer } from '@douyinfe/semi-ui'; + +function Demo() { + const audioUrlArrObj = [ + { + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', + title: 'Audio Title 1', + cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', + }, + { + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3', + title: 'Audio Title 2', + cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', + }, + ]; + + return ( +
+ +
+ ); +} + +render(Demo); +``` + +## API Reference + +### AudioPlayer + +| Property | Description | Type | Default | +|----------|-------------|------|---------| +| audioUrl | Audio address | string | string[] | AudioInfo | AudioInfo[] | - | +| autoPlay | Auto play | boolean | false | +| theme | Theme, optional values: `dark` and `light` | string | "dark" | +| showToolbar | Whether to display the toolbar | boolean | true | +| skipDuration | Skip time | number | 10 | +| className | Class name | string | - | +| style | Inline style | object | - | + +### AudioInfo + +| Property | Description | Type | Default | +|----------|-------------|------|---------| +| src | Audio address | string | - | +| title | Audio title | string | - | +| cover | Cover image | string | - | + + diff --git a/content/plus/audioPlayer/index.md b/content/plus/audioPlayer/index.md index 1d617f2c06..4fd14ddfc1 100644 --- a/content/plus/audioPlayer/index.md +++ b/content/plus/audioPlayer/index.md @@ -5,7 +5,7 @@ category: Plus title: AudioPlayer 音频播放器 icon: doc-audioplayer width: 60% -brief: 用于方便用户自定义快捷键及相关操作 +brief: 用于播放音频 showNew: true --- @@ -20,8 +20,8 @@ import { AudioPlayer } from '@douyinfe/semi-ui'; ### 基本用法 -基本使用,通过`audioUrl`传入音频地址 -audioUrl 可以传入字符串,字符串数组,对象,对象数组, 具体参数参考 [AudioPlayer](#AudioPlayer) +基本使用,通过`audioUrl`传入音频地址 +audioUrl 可以传入字符串,字符串数组,对象,对象数组, 具体参数参考 [AudioPlayer](#AudioPlayer) ```jsx live=true noInline=true dir="column" import React from 'react'; From bfd11399a0ee956e1a15970f4cd62f50ca5993a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E4=B8=B0?= Date: Tue, 7 Jan 2025 13:59:10 +0800 Subject: [PATCH 14/16] refactor: improve code structure for better maintainability --- .../semi-foundation/audioPlayer/foundation.ts | 36 ++++++++++++++- packages/semi-ui/audioPlayer/audioSlider.tsx | 7 +-- packages/semi-ui/audioPlayer/index.tsx | 46 ++++++++++--------- 3 files changed, 63 insertions(+), 26 deletions(-) diff --git a/packages/semi-foundation/audioPlayer/foundation.ts b/packages/semi-foundation/audioPlayer/foundation.ts index 44ad950f7f..43debe8040 100644 --- a/packages/semi-foundation/audioPlayer/foundation.ts +++ b/packages/semi-foundation/audioPlayer/foundation.ts @@ -13,7 +13,8 @@ export interface AudioPlayerAdapter

, S = Record void; handleSeek: (direction: number) => void; handleRefresh: () => void; - handleVolumeChange: (value: number) => void + handleVolumeChange: (value: number) => void; + destroyAudio: () => void } class AudioPlayerFoundation extends BaseFoundation { @@ -21,10 +22,43 @@ class AudioPlayerFoundation extends BaseFoundation { super({ ...AudioPlayerFoundation, ...adapter }); } + initAudioState() { + const audioRef = this.getAudioRef(); + const props = this.getProps(); + + this.setState({ + totalTime: audioRef.current?.duration || 0, + isPlaying: props.autoPlay, + volume: audioRef.current?.volume * 100 || 100, + currentRate: { label: '1.0x', value: audioRef.current?.playbackRate || 1 }, + }); + } + + endHandler() { + const props = this.getProps(); + if (Array.isArray(props.audioUrl)) { + this.handleTrackChange('next'); + } else { + this.setState({ + isPlaying: false, + }); + } + } + + errorHandler() { + this.setState({ + error: true, + }); + } + initAudio() { this._adapter.initAudio(); } + destroyAudio() { + this._adapter.destroyAudio(); + } + resetAudioState() { this._adapter.resetAudioState(); } diff --git a/packages/semi-ui/audioPlayer/audioSlider.tsx b/packages/semi-ui/audioPlayer/audioSlider.tsx index 50266f06a9..1591fdaf90 100644 --- a/packages/semi-ui/audioPlayer/audioSlider.tsx +++ b/packages/semi-ui/audioPlayer/audioSlider.tsx @@ -28,7 +28,6 @@ export default class AudioSlider extends React.Component { - this.setState({ isHovering: false }); - this.setState({ isDragging: false }); + this.setState({ + isHovering: false, + isDragging: false + }); } render() { diff --git a/packages/semi-ui/audioPlayer/index.tsx b/packages/semi-ui/audioPlayer/index.tsx index 5bc64672c3..5fbcfd0814 100644 --- a/packages/semi-ui/audioPlayer/index.tsx +++ b/packages/semi-ui/audioPlayer/index.tsx @@ -15,15 +15,14 @@ import { AudioPlayerAdapter } from '@douyinfe/semi-foundation/audioPlayer/founda import { formatTime } from './utils'; type AudioSrc = string -type AudioSrcArray = string[] type AudioInfo = { title?: string; cover?: string; src: string } -type AudioInfoArray = AudioInfo[] +type AudioUrlArray = (AudioInfo | string)[]; -type AudioUrl = AudioSrc | AudioSrcArray | AudioInfo | AudioInfoArray +type AudioUrl = AudioSrc | AudioInfo | AudioUrlArray type AudioPlayerTheme = 'dark' | 'light' @@ -89,27 +88,26 @@ class AudioPlayer extends BaseComponent { initAudio: () => { if (this.audioRef.current) { this.audioRef.current.addEventListener('loadedmetadata', () => { - this.setState({ - totalTime: this.audioRef.current?.duration || 0, - isPlaying: this.props.autoPlay, - volume: this.audioRef.current?.volume * 100 || 100, - currentRate: { label: '1.0x', value: this.audioRef.current?.playbackRate || 1 }, - }); + this.foundation.initAudioState(); }); this.audioRef.current.addEventListener('error', () => { - this.setState({ - error: true, - }); + this.foundation.errorHandler(); }); this.audioRef.current.addEventListener('ended', () => { - console.log('ended'); - if (Array.isArray(this.props.audioUrl)) { - this.handleTrackChange('next'); - } else { - this.setState({ - isPlaying: false, - }); - } + this.foundation.endHandler(); + }); + } + }, + destroyAudio: () => { + if (this.audioRef.current) { + this.audioRef.current.removeEventListener('loadedmetadata', () => { + this.foundation.initAudioState(); + }); + this.audioRef.current.removeEventListener('error', () => { + this.foundation.errorHandler(); + }); + this.audioRef.current.removeEventListener('ended', () => { + this.foundation.endHandler(); }); } }, @@ -212,6 +210,10 @@ class AudioPlayer extends BaseComponent { this.foundation.initAudio(); } + componentWillUnmount() { + this.foundation.destroyAudio(); + } + handleStatusClick = () => { this.foundation.handleStatusClick(); } @@ -349,9 +351,9 @@ class AudioPlayer extends BaseComponent { {this.state.currentRate.label}

-
) : (
-
); } From 1edc68538c09f808b2d58ef42c24911683515884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E4=B8=B0?= Date: Tue, 7 Jan 2025 14:14:17 +0800 Subject: [PATCH 15/16] refactor: improve code structure for better maintainability --- packages/semi-foundation/audioPlayer/foundation.ts | 12 ++++++------ packages/semi-ui/audioPlayer/index.tsx | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/semi-foundation/audioPlayer/foundation.ts b/packages/semi-foundation/audioPlayer/foundation.ts index 43debe8040..01d6bfbf39 100644 --- a/packages/semi-foundation/audioPlayer/foundation.ts +++ b/packages/semi-foundation/audioPlayer/foundation.ts @@ -3,7 +3,7 @@ import BaseFoundation, { DefaultAdapter } from '../base/foundation'; export interface AudioPlayerAdapter

, S = Record> extends DefaultAdapter { - initAudio: () => void; + init: () => void; resetAudioState: () => void; handleStatusClick: () => void; handleTimeUpdate: () => void; @@ -14,7 +14,7 @@ export interface AudioPlayerAdapter

, S = Record void; handleRefresh: () => void; handleVolumeChange: (value: number) => void; - destroyAudio: () => void + destroy: () => void } class AudioPlayerFoundation extends BaseFoundation { @@ -51,12 +51,12 @@ class AudioPlayerFoundation extends BaseFoundation { }); } - initAudio() { - this._adapter.initAudio(); + init() { + this._adapter.init(); } - destroyAudio() { - this._adapter.destroyAudio(); + destroy() { + this._adapter.destroy(); } resetAudioState() { diff --git a/packages/semi-ui/audioPlayer/index.tsx b/packages/semi-ui/audioPlayer/index.tsx index 5fbcfd0814..4774372f2d 100644 --- a/packages/semi-ui/audioPlayer/index.tsx +++ b/packages/semi-ui/audioPlayer/index.tsx @@ -85,7 +85,7 @@ class AudioPlayer extends BaseComponent { get adapter(): AudioPlayerAdapter { return { ...super.adapter, - initAudio: () => { + init: () => { if (this.audioRef.current) { this.audioRef.current.addEventListener('loadedmetadata', () => { this.foundation.initAudioState(); @@ -98,7 +98,7 @@ class AudioPlayer extends BaseComponent { }); } }, - destroyAudio: () => { + destroy: () => { if (this.audioRef.current) { this.audioRef.current.removeEventListener('loadedmetadata', () => { this.foundation.initAudioState(); @@ -207,11 +207,11 @@ class AudioPlayer extends BaseComponent { } componentDidMount() { - this.foundation.initAudio(); + this.foundation.init(); } componentWillUnmount() { - this.foundation.destroyAudio(); + this.foundation.destroy(); } handleStatusClick = () => { From 91f1e5d92b94a5a8213b0979bc4a04af7c43f0b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E4=B8=B0?= Date: Tue, 7 Jan 2025 15:35:07 +0800 Subject: [PATCH 16/16] style: adjust styles for improved UI consistency --- .../semi-foundation/audioPlayer/audioPlayer.scss | 5 +++++ .../semi-foundation/audioPlayer/variables.scss | 3 ++- packages/semi-ui/audioPlayer/audioSlider.tsx | 9 ++++++--- packages/semi-ui/audioPlayer/index.tsx | 16 ++++++++++------ 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/semi-foundation/audioPlayer/audioPlayer.scss b/packages/semi-foundation/audioPlayer/audioPlayer.scss index 35cbb80dc9..9a0198614f 100644 --- a/packages/semi-foundation/audioPlayer/audioPlayer.scss +++ b/packages/semi-foundation/audioPlayer/audioPlayer.scss @@ -50,6 +50,7 @@ $module: #{$prefix}-audio-player; &-info-title { font-size: $font-size-audio-player-text; color: $color-audio-player-font-color; + font-weight: 600; display: flex; align-items: center; } @@ -160,6 +161,10 @@ $module: #{$prefix}-audio-player; background: $color-audio-player-slider-bg; border-radius: $border-radius-audio-player-slider; position: relative; + + &-light { + background: $color-audio-player-slider-bg-light; + } &-wrapper { position: relative; diff --git a/packages/semi-foundation/audioPlayer/variables.scss b/packages/semi-foundation/audioPlayer/variables.scss index d05ecf4431..c4842a9935 100644 --- a/packages/semi-foundation/audioPlayer/variables.scss +++ b/packages/semi-foundation/audioPlayer/variables.scss @@ -42,7 +42,8 @@ $size-audio-player-slider-dot: 16px; // Colors $color-audio-player-disabled-bg: rgba(var(--semi-grey-0), .35); -$color-audio-player-slider-bg: rgba(var(--semi-grey-1), 1); +$color-audio-player-slider-bg: rgba(var(--semi-grey-5), 1); +$color-audio-player-slider-bg-light: rgba(var(--semi-grey-2), 1); $color-audio-player-slider-progress: rgba(var(--semi-blue-4), 1); $color-audio-player-slider-dot-bg: rgba(var(--semi-white), 1); diff --git a/packages/semi-ui/audioPlayer/audioSlider.tsx b/packages/semi-ui/audioPlayer/audioSlider.tsx index 1591fdaf90..fcc423c456 100644 --- a/packages/semi-ui/audioPlayer/audioSlider.tsx +++ b/packages/semi-ui/audioPlayer/audioSlider.tsx @@ -5,6 +5,7 @@ import { cssClasses } from '@douyinfe/semi-foundation/audioPlayer/constants'; import Tooltip from '../tooltip'; import { formatTime } from './utils'; import { noop } from 'lodash'; +import { AudioPlayerTheme } from './index'; interface AudioSliderProps { value: number; @@ -15,7 +16,8 @@ interface AudioSliderProps { width?: number; height?: number; showTooltip?: boolean; - disabled?: boolean + disabled?: boolean; + theme?: AudioPlayerTheme } interface AudioSliderState { @@ -34,6 +36,7 @@ export default class AudioSlider extends React.Component; @@ -101,7 +104,7 @@ export default class AudioSlider extends React.Component

{ const transparentStyle = { background: 'transparent', }; + const playStyle = { + marginLeft: '1px', + }; return
{isAudioUrlArray &&