From 317f9084e01940e848cb9717f18314a5fb907d5a Mon Sep 17 00:00:00 2001 From: jialin Date: Wed, 4 Jun 2025 15:53:01 +0800 Subject: [PATCH] style: simple audio player --- src/components/audio-player/audio-element.tsx | 8 +- src/components/audio-player/simple-audio.tsx | 368 ++++++++++++++++++ src/components/icon-font/index.tsx | 4 +- src/config/theme/dark.ts | 1 + src/config/theme/light.ts | 1 + src/pages/dashboard/apis/index.ts | 3 +- .../playground/components/message-input.tsx | 18 +- .../components/multiple-chat/message-body.tsx | 34 +- 8 files changed, 408 insertions(+), 29 deletions(-) create mode 100644 src/components/audio-player/simple-audio.tsx diff --git a/src/components/audio-player/audio-element.tsx b/src/components/audio-player/audio-element.tsx index 8bbb1a27..9601b9eb 100644 --- a/src/components/audio-player/audio-element.tsx +++ b/src/components/audio-player/audio-element.tsx @@ -11,9 +11,11 @@ const AudioWrapper = styled.div` const AudioElement: React.FC = (props) => { return ( - - - +
+ + + +
); }; diff --git a/src/components/audio-player/simple-audio.tsx b/src/components/audio-player/simple-audio.tsx new file mode 100644 index 00000000..8492593c --- /dev/null +++ b/src/components/audio-player/simple-audio.tsx @@ -0,0 +1,368 @@ +import { formatTime } from '@/utils/index'; +import { DeleteOutlined, DownloadOutlined } from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import { Button, Dropdown, Slider, type MenuProps } from 'antd'; +import { createStyles } from 'antd-style'; +import { round } from 'lodash'; +import React, { + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo +} from 'react'; +import IconFont from '../icon-font'; + +type ActionItem = 'download' | 'delete' | 'speed'; + +interface AudioPlayerProps { + autoplay?: boolean; + url: string; + speed?: number; + ref?: any; + name?: string; + height?: number; + width?: number; + duration?: number; + actions?: ActionItem[]; + onDelete?: () => void; +} + +const useStyles = createStyles(({ css, token }) => { + // @ts-ignore + const isDarkMode = token.darkMode as boolean; + return { + wrapper: css` + position: relative; + min-width: 300px; + height: 54px; + display: flex; + align-items: center; + justify-content: flex-start; + padding: 6px 10px; + background-color: ${isDarkMode + ? 'var(--ant-color-fill-secondary)' + : '#F1F3F4'}; + border-radius: 28px; + .inner { + display: flex; + align-items: center; + justify-content: flex-start; + flex: 1; + gap: 10px; + .slider { + flex: 1; + &:hover { + .ant-slider-handle { + opacity: 1; + transition: opacity 0.3s ease-in-out; + } + } + &:focus-within { + .ant-slider-handle { + opacity: 1; + } + } + } + + .ant-slider-handle { + opacity: 0; + &::before { + background-color: var(--ant-color-bg-spotlight); + border-radius: 50%; + } + &::after { + display: none; + } + } + } + ` + }; +}); + +const sliderStyles = { + rail: { + borderRadius: '4px', + backgroundColor: 'var(--ant-color-bg-spotlight)' + } +}; + +const speedOptions = [ + { label: '1x', value: 1 }, + { label: '2x', value: 2 }, + { label: '3x', value: 3 }, + { label: '4x', value: 4 } +]; + +const speedConfig = { + min: 0.5, + max: 2, + step: 0.25 +}; + +const AudioPlayer: React.FC = forwardRef((props, ref) => { + const intl = useIntl(); + const { styles } = useStyles(); + const { + autoplay = false, + speed: defaultSpeed = 1, + actions = ['delete'], + onDelete + } = props; + const audioRef = React.useRef(null); + const [audioState, setAudioState] = React.useState<{ + currentTime: number; + duration: number; + }>({ + currentTime: 0, + duration: 0 + }); + const [playOn, setPlayOn] = React.useState(false); + const [speakerOn, setSpeakerOn] = React.useState(false); + const [volume, setVolume] = React.useState(1); + const [speed, setSpeed] = React.useState(defaultSpeed); + const timer = React.useRef(null); + + useImperativeHandle(ref, () => ({ + play: () => { + audioRef.current?.play(); + }, + pause: () => { + audioRef.current?.pause(); + } + })); + const handleShowVolume = useCallback(() => { + setSpeakerOn(!speakerOn); + }, [speakerOn]); + + const handleSeepdChange = useCallback((value: number | string) => { + setSpeed(value as number); + audioRef.current!.playbackRate = value as number; + }, []); + + const handleAudioOnPlay = useCallback(() => { + timer.current = setInterval(() => { + setAudioState((prestate) => { + return { + currentTime: Math.ceil(audioRef.current?.currentTime || 0), + duration: + prestate.duration || Math.ceil(audioRef.current?.duration || 0) + }; + }); + + if (audioRef.current?.paused || audioRef.current?.ended) { + clearInterval(timer.current); + setPlayOn(false); + setAudioState((prestate: any) => { + return { + currentTime: audioRef.current?.ended ? 0 : prestate.currentTime, + duration: prestate.duration + }; + }); + } + }, 500); + }, []); + + const handlePlay = useCallback(() => { + setPlayOn(!playOn); + if (playOn) { + audioRef.current?.pause(); + } else { + audioRef.current?.play(); + } + }, [playOn]); + + const handleFormatVolume = (val?: number) => { + if (val === undefined) { + return `${round(volume * 100)}%`; + } + return `${round(val * 100)}%`; + }; + + const handleVolumeChange = useCallback((value: number) => { + audioRef.current!.volume = round(value, 2); + setVolume(round(value, 2)); + }, []); + + const initPlayerConfig = () => { + if (audioRef.current) { + audioRef.current!.volume = volume; + audioRef.current!.playbackRate = speed; + } + }; + + const handleLoadedMetadata = useCallback( + (data: any) => { + const duration = Math.ceil(audioRef.current?.duration || 0); + setAudioState({ + currentTime: 0, + duration: + duration && duration !== Infinity ? duration : props.duration || 0 + }); + setPlayOn(autoplay); + }, + [autoplay, props.duration] + ); + + const handleCurrentChange = useCallback((val: number) => { + audioRef.current!.currentTime = val; + setAudioState((prestate) => { + return { + currentTime: val, + duration: prestate.duration + }; + }); + }, []); + + const handleReduceSpeed = () => { + setSpeed((pre) => { + if (pre - speedConfig.step < speedConfig.min) { + return speedConfig.min; + } + const next = pre - speedConfig.step; + audioRef.current!.playbackRate = next; + return next; + }); + }; + + const handleAddSpeed = () => { + setSpeed((pre) => { + if (pre + speedConfig.step > speedConfig.max) { + return speedConfig.max; + } + const next = pre + speedConfig.step; + audioRef.current!.playbackRate = next; + return next; + }); + }; + + const handleOnLoad = (e: any) => { + console.log('onload', e); + }; + + const onDownload = useCallback(() => { + const url = props.url || ''; + const filename = props.name; + + const link = document.createElement('a'); + link.href = url; + link.download = 'audio.mp3'; // Default filename + document.body.appendChild(link); + link.click(); + link.remove(); + }, [props.url, props.name]); + + const items: MenuProps['items'] = useMemo(() => { + return [ + { + key: 'download', + label: intl.formatMessage({ id: 'common.button.download' }), + icon: , + onClick: onDownload + }, + { + key: 'speed', + label: intl.formatMessage({ id: 'playground.params.speed' }), + icon: , + children: speedOptions.map((item) => ({ + key: item.value, + label: item.label, + onClick: () => handleSeepdChange(item.value) + })) + }, + { + key: 'delete', + label: intl.formatMessage({ id: 'common.button.delete' }), + icon: , + danger: true, + onClick: () => { + if (audioRef.current) { + audioRef.current.pause(); + audioRef.current.src = ''; + audioRef.current.load(); + } + setAudioState({ currentTime: 0, duration: 0 }); + setPlayOn(false); + onDelete?.(); + } + } + ].filter((item) => actions.includes(item.key as ActionItem)); + }, [actions, intl, onDownload, onDelete, handleSeepdChange]); + + useEffect(() => { + if (audioRef.current) { + initPlayerConfig(); + } + }, [audioRef.current]); + + useEffect(() => { + return () => { + clearInterval(timer.current); + }; + }, []); + + return ( +
+
+ + + {formatTime(audioState.currentTime)} /{' '} + {formatTime(audioState.duration)} + +
+ +
+ + + +
+ +
+ ); +}); + +export default React.memo(AudioPlayer); diff --git a/src/components/icon-font/index.tsx b/src/components/icon-font/index.tsx index 2c4fbc7b..f614518b 100644 --- a/src/components/icon-font/index.tsx +++ b/src/components/icon-font/index.tsx @@ -1,8 +1,8 @@ import { createFromIconfontCN } from '@ant-design/icons'; -import './iconfont/iconfont.js'; +// import './iconfont/iconfont.js'; const IconFont = createFromIconfontCN({ - scriptUrl: '' + scriptUrl: '//at.alicdn.com/t/c/font_4613488_gxrotg9r0p.js' }); export default IconFont; diff --git a/src/config/theme/dark.ts b/src/config/theme/dark.ts index 32c515de..9ccefad4 100644 --- a/src/config/theme/dark.ts +++ b/src/config/theme/dark.ts @@ -69,6 +69,7 @@ export default { } }, token: { + darkMode: true, fontFamily: "Helvetica Neue, -apple-system, BlinkMacSystemFont, Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'", colorText: '#ccc', diff --git a/src/config/theme/light.ts b/src/config/theme/light.ts index 998c2aae..e9d973a0 100644 --- a/src/config/theme/light.ts +++ b/src/config/theme/light.ts @@ -66,6 +66,7 @@ export default { } }, token: { + darkMode: false, fontFamily: "Helvetica Neue, -apple-system, BlinkMacSystemFont, Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'", colorText: 'rgba(0,0,0,1)', diff --git a/src/pages/dashboard/apis/index.ts b/src/pages/dashboard/apis/index.ts index cb115ed0..888f1432 100644 --- a/src/pages/dashboard/apis/index.ts +++ b/src/pages/dashboard/apis/index.ts @@ -7,5 +7,6 @@ export async function queryDashboardData() { } export async function queryDashboardUsageData() { - return request(`${DASHBOARD_API}/usage`); + // return request(`${DASHBOARD_API}/usage`); + return {}; } diff --git a/src/pages/playground/components/message-input.tsx b/src/pages/playground/components/message-input.tsx index 0a75f10e..a18e7db2 100644 --- a/src/pages/playground/components/message-input.tsx +++ b/src/pages/playground/components/message-input.tsx @@ -1,4 +1,4 @@ -import AudioElement from '@/components/audio-player/audio-element'; +import SimpleAudio from '@/components/audio-player/simple-audio'; import IconFont from '@/components/icon-font'; import UploadAudio from '@/components/upload-audio'; import HotKeys, { KeyMap } from '@/config/hotkeys'; @@ -322,14 +322,10 @@ const MessageInput: React.FC = forwardRef( }); }; - const handleDeleteAudio = (uid: number | string) => { - const list = _.filter( - message.audio, - (item: MessageItem) => item.uid !== uid - ); + const handleDeleteAudio = () => { setMessage({ ...message, - audio: list + audio: [] }); }; @@ -517,11 +513,11 @@ const MessageInput: React.FC = forwardRef( > {message.audio && message.audio.length > 0 && ( - + > )} diff --git a/src/pages/playground/components/multiple-chat/message-body.tsx b/src/pages/playground/components/multiple-chat/message-body.tsx index 88bc4bcd..892d4668 100644 --- a/src/pages/playground/components/multiple-chat/message-body.tsx +++ b/src/pages/playground/components/multiple-chat/message-body.tsx @@ -1,4 +1,4 @@ -import AudioElement from '@/components/audio-player/audio-element'; +import SimpleAudio from '@/components/audio-player/simple-audio'; import FullMarkdown from '@/components/markdown-viewer/full-markdown'; import { Input } from 'antd'; import classNames from 'classnames'; @@ -17,9 +17,7 @@ import ThinkContent from './think-content'; const AudioWrapper = styled.div` padding-left: 10px; - audio { - padding-top: 10px; - } + padding-top: 10px; `; interface MessageBodyProps { @@ -132,6 +130,16 @@ const MessageBody: React.FC = forwardRef( }); }; + const handleDeleteAudio = () => { + updateMessage?.({ + role: data.role, + content: data.content, + uid: data.uid, + audio: [], + imgs: data.imgs || [] + }); + }; + const handleMessageChange = (e: any) => { updateMessage?.({ imgs: data.imgs || [], @@ -207,10 +215,11 @@ const MessageBody: React.FC = forwardRef( /> {data.audio && data.audio.length > 0 && ( - + )} @@ -236,10 +245,11 @@ const MessageBody: React.FC = forwardRef( /> {data.audio && data.audio.length > 0 && ( - + )}